starlit  Check-in [187bf04c87]

Overview
Comment:rename again
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: 187bf04c875d238d14e4ccedc13983bf5ec0309d3f831757809ebdbd9585a65c
User & Date: lexi on 2024-04-29 22:49:02
Other Links: manifest | tags
Context
2024-05-01
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
2024-03-29
22:43
fix batteries, silence debug noise check-in: 6469428393 user: lexi tags: trunk
Changes

Modified dev.ct from [3b1604bc1d] to [912982b94d].

     1         -# starsoul development
            1  +# starlit development
     2      2   this file contains information meant for those who wish to develop for Starsoul or build the game from trunk. do NOT add any story information, particularly spoilers; those go in src/lore.ct.
     3      3   
     4      4   ## tooling
     5         -starsoul uses the following software in the development process:
            5  +starlit uses the following software in the development process:
     6      6   * [*csound] to generate sound effects
     7      7   * [*GNU make] to automate build tasks
     8      8   * [*lua] to automate configure tasks
     9      9   
    10     10   ## building
    11     11   to run a trunk version of Starsoul, you'll need to install the above tools and run `make` from the base directory. this will:
    12     12   * run lua scripts to generate necessary makefiles
    13         -* generate the game sound effects and install them in mods/starsoul/sounds
           13  +* generate the game sound effects and install them in mods/starlit/sounds
    14     14   
    15     15   ## policy
    16     16   * copyright of all submitted code must be reassigned to the maintainer.
    17     17   * all code is to be indented with tabs and aligned with spaces; formatting is otherwise up to whoever is responsible for maintaining that code
    18     18   * use [`camelCase], not [`snake_case] and CERTAINLY not [`SCREAMING_SNAKE_CASE]
    19     19   * sounds effects should be contributed in the form of csound files; avoid adding audio files to the repository except for foley effects

Modified game.conf from [d6b8d1d1d1] to [b1638bce61].

     1         -title = Starsoul
            1  +title = Starlit
     2      2   author = velartrill
     3      3   description = High-tech survival on a hostile alien world
     4      4   allowed_mapgens = v7
     5      5   disabled_settings = !enable_damage, creative_mode

Added mods/starlit-building/init.lua version [7b434e8bee].

            1  +local lib = starlit.mod.lib
            2  +local B = {}
            3  +starlit.mod.building = B
            4  +
            5  +B.path = {}
            6  +-- this maps stage IDs to tables of the following form
            7  +--[[ {
            8  +	part = {
            9  +		['starlit_building:pipe'] = 'myMod:stage3';
           10  +	};
           11  +	tool = {
           12  +		['starlit:scredriver'] = 'myMod:otherThing_stage1';
           13  +		['starlit:saw'] = function(node, tool)
           14  +			minetest.replace_node(node, {name='myMod:stage1'})
           15  +			minetest.drop_item(node, 'starlit_building:pipe')
           16  +		end;
           17  +		['myMod:laserWrench'] = {
           18  +			allow = function(node, tool) ... end;
           19  +			handle = function(node, tool) ... end;
           20  +		};
           21  +	};
           22  +} ]]
           23  +-- it should only be written by special accessor functions!
           24  +
           25  +B.stage = lib.registry.mk 'starlit_building:stage'
           26  +-- a stage consists of a list of pieces and maps from possible materials
           27  +-- / tool usages to succeeding stages in the build tree. note that all
           28  +-- used pieces must be defined before a stage is defined currently, due
           29  +-- to a lack of cross-registry dependency mechanisms. i will hopefully
           30  +-- improve vtlib to handle this condition eventually.
           31  +--[[
           32  +	starlit.mod.building.stage.link(id, {
           33  +		pieces = {
           34  +			'starlit_building:foundation';
           35  +			'starlit_building:insulation'; -- offset             ofsFac
           36  +			{'starlit_building:pipe',      vector.new(-.5, 0, 0), 0};--
           37  +			{'starlit_building:pipe',      vector.new(-.5, 0, 0), 1};
           38  +			'starlit_building:insulation';
           39  +			'starlit_building:panel';
           40  +		};
           41  +	})
           42  +]]
           43  +
           44  +B.piece = lib.registry.mk 'starlit_building:piece'
           45  +-- a piece is used to produce stage definitions, by means of appending
           46  +-- nodeboxes with appropriate offsets. it also lists the recoverable
           47  +-- materials which can be obtained by destroying a stage containing
           48  +-- this piece using nano. part IDs should correspond with piece IDs
           49  +-- where possible
           50  +--[[
           51  +	starlit.mod.building.piece.link(id, {
           52  +		tex = 'myMod_part.png';
           53  +		height = 0.1; -- used for auto-offset
           54  +		fab = {
           55  +			element = {iron=10};
           56  +		};	
           57  +		shape = {
           58  +			type = "fixed";
           59  +			fixed = { ... };
           60  +		};
           61  +	})
           62  +]]
           63  +
           64  +B.part = lib.registry.mk 'starlit_building:part'
           65  +-- a part is implemented as a special craftitem with the proper callbacks
           66  +-- to index the registries and place/replace noes by reference to the
           67  +-- build tree.
           68  +--[[
           69  +	starlit.mod.building.part.link(id, {
           70  +		name = ''; -- display name
           71  +		desc = ''; -- display desc
           72  +		img = ''; -- display image
           73  +	})
           74  +]]
           75  +
           76  +B.stage.foreach('starlit:stageGen', {}, function(id, e)
           77  +	local box = {type = 'fixed', fixed = {}}
           78  +	local tex = {}
           79  +	local ofs = vector.new(0,0,0)
           80  +	for idx, p in ipairs(e.pieces) do
           81  +		local ho, pieceID, pos
           82  +		if type(p) == 'string' then
           83  +			pieceID, pos, ho = p, vector.zero(), 1.0
           84  +		else
           85  +			pieceID, pos, ho = pc[1],pc[2],pc[3]
           86  +		end
           87  +		local pc = B.piece.db[pieceID]
           88  +		pos = pos + ofs
           89  +		if ho ~= 0.0 then
           90  +			ofs = vector.offset(ofs, 0, pc.height)
           91  +		end
           92  +		local sh = lib.node.boxwarped(pc.shape, function(b)
           93  +			-- { -x, -y, -z;
           94  +			--   +x, +y, +z }
           95  +			b[1] = b[1] + ofs.x  b[4] = b[4] + ofs.x
           96  +			b[2] = b[2] + ofs.y  b[5] = b[5] + ofs.y
           97  +			b[3] = b[3] + ofs.z  b[6] = b[6] + ofs.z
           98  +		end)
           99  +		table.insert(box, sh)
          100  +		if type(pc.tex) == 'string' then
          101  +			table.insert(tex, pc.tex)
          102  +		else
          103  +			for i,t in ipairs(pc.tex) do
          104  +				table.insert(tex, t or '')
          105  +			end
          106  +		end
          107  +	end
          108  +	minetest.register_node(id, {
          109  +		description = 'Construction';
          110  +		drawtype = 'nodebox';
          111  +		paramtype  = 'light';
          112  +		paramtype2 = e.stateful or 'none';
          113  +		textures = tex;
          114  +		node_box = box;
          115  +		group = { stage = 1 };
          116  +		_starlit = {
          117  +			stage = id;
          118  +		};
          119  +	})
          120  +end)
          121  +
          122  +function B.pathLink(from, kind, what, to)
          123  +	if not B.path[from] then
          124  +		B.path[from] = {part={}, tool={}}
          125  +	end
          126  +	local k = B.path[from][kind]
          127  +	assert(k[what] == nil)
          128  +	k[what] = to
          129  +end
          130  +
          131  +function B.pathFind(from, kind, what)
          132  +	if not B.path[from] then return nil end
          133  +	return B.path[from][kind][what]
          134  +end
          135  +

Added mods/starlit-building/mod.conf version [dd26a16b6d].

            1  +name = starlit_building
            2  +title = starlit building
            3  +depends = starlit_electronics, starlit
            4  +description = implements construction elements

Added mods/starlit-electronics/init.lua version [67a7c4791a].

            1  +local lib = starlit.mod.lib
            2  +
            3  +local E = {}
            4  +starlit.mod.electronics = E
            5  +
            6  +---------------------
            7  +-- item registries --
            8  +---------------------
            9  +
           10  +-- a dynamo is any item that produces power and can be slotted into a power
           11  +-- source slot. this includes batteries, but also things like radiothermal
           12  +-- dynamos.
           13  +starlit.item.dynamo = lib.registry.mk 'starlit_electronics:dynamo'
           14  +
           15  +-- batteries hold a charge of power (measured in kJ). how much they can hold
           16  +-- (and how much power they can discharge?) depends on their quality
           17  +starlit.item.battery = lib.registry.mk 'starlit_electronics:battery'
           18  +
           19  +-- a battery has the properties:
           20  +--  class
           21  +--	 |- capacity (J ): amount of energy the battery can hold
           22  +--	 |- dischargeRate  (W ): rate at which battery can supply power/be charged
           23  +--	 |- decay   (J/J): rate at which the battery capacity degrades while
           24  +--	                   discharging. decay=0 batteries require no maintenance;
           25  +--	                   decay=1 batteries are effectively disposable
           26  +--	 |- leak    (fac): charging inefficiency. depends on the energy storage
           27  +--	                   technology. when N J are drawn from a power source,
           28  +--	                   only (N*leak) J actually make it into the battery.
           29  +--	                   leak=0 is a supercapacitor, leak=1 is /dev/null
           30  +--	 |- size      (m): each suit has a limit to how big of a battery it can take
           31  +--	instance
           32  +--	 |- degrade (mJ): how much the battery has degraded. instance max charge is
           33  +--	 |                determined by $capacity - @degrade
           34  +--	 |- %wear ÷ 2¹⁶ : used as a factor to determine battery charge
           35  +
           36  +-- chips are standardized data storage hardware that can contain a certain amount
           37  +-- of software. in addition to their flash storage, they also provide a given amount
           38  +-- of working memory and processor power. processor power speeds up operations like
           39  +-- crafting, while programs require a certain amount of memory.
           40  +-- chips have a variable number of program slots and a single bootloader slot
           41  +--
           42  +starlit.item.chip = lib.registry.mk 'starlit_electronics:chip'
           43  +
           44  +-- software is of one of the following types:
           45  +--   schematic: program for your matter compiler that enables crafting a given item.
           46  +--       output: the result
           47  +--   driver: inserted into a Core to control attached hardware
           48  +--     suitPower: provides suit functionality like nanoshredding or healing
           49  +--                passive powers are iterated on suit application/configuration and upon fst-tick
           50  +--   cost: what the software needs to run. some fields are fab-specific
           51  +--		   energy: for fab, total energy cost of process in joules
           52  +--               for suitPassive, added suit power consumption in watts
           53  +starlit.item.sw = lib.registry.mk 'starlit_electronics:sw'
           54  +-- chip    = lib.color(0, 0, .3);
           55  +
           56  +E.schematicGroups = lib.registry.mk 'starlit_electronics:schematicGroups'
           57  +E.schematicGroupMembers = {}
           58  +E.schematicGroups.foreach('starlit_electronics:ensure-memlist', {}, function(id,g)
           59  +	E.schematicGroupMembers[id] = {}
           60  +end)
           61  +function E.schematicGroupLink(group, item)
           62  +	table.insert(E.schematicGroupMembers[group], item)
           63  +end
           64  +
           65  +E.schematicGroups.link('starlit_electronics:chip', {
           66  +	title = 'Chip', icon = 'starlit-item-chip.png';
           67  +	description = 'Standardized data storage and compute modules';
           68  +})
           69  +
           70  +E.schematicGroups.link('starlit_electronics:battery', {
           71  +	title = 'Battery', icon = 'starlit-item-battery.png';
           72  +	description = 'Portable power storage cells are essential to all aspects of survival';
           73  +})
           74  +
           75  +E.schematicGroups.link('starlit_electronics:decayCell', {
           76  +	title = 'Decay Cell', icon = 'starlit-item-decaycell.png';
           77  +	description = "Radioisotope generators can pack much more power into a smaller amount of space than conventional batteries, but they can't be recharged, dump power and heat whether they're in use or not, and their power yield drops towards zero over their usable lifetime.";
           78  +})
           79  +
           80  +
           81  +-------------------------
           82  +-- batteries & dynamos --
           83  +-------------------------
           84  +
           85  +E.battery = {}
           86  +local function accessor(ty, fn)
           87  +	return function(stack, ...)
           88  +		local function fail()
           89  +			error(string.format('object %q is not a %s', stack:get_name(), ty))
           90  +		end
           91  +
           92  +		if not stack or stack:is_empty() then fail() end
           93  +
           94  +		if minetest.get_item_group(stack:get_name(), ty) == 0 then fail() end
           95  +
           96  +		return fn(stack,
           97  +		          stack:get_definition()._starlit[ty],
           98  +		          stack:get_meta(), ...)
           99  +	end
          100  +end
          101  +
          102  +-- return a wear level that won't destroy the item
          103  +-- local function safeWear(fac) return math.min(math.max(fac,0),1) * 0xFFFE end
          104  +-- local function safeWearToFac(w) return w/0xFFFE end
          105  +
          106  +E.battery.update = accessor('battery', function(stack, batClass, meta)
          107  +	-- local cap = E.battery.capacity(stack)
          108  +	local charge = meta:get_float 'starlit_electronics:battery_charge'
          109  +	meta:set_string('count_meta', string.format('%s%%', math.floor(charge * 100)))
          110  +	meta:set_int('count_alignment', 14)
          111  +end)
          112  +
          113  +-- E.battery.capacity(bat) --> charge (J)
          114  +E.battery.capacity = accessor('battery', function(stack, batClass, meta)
          115  +	local dmg = meta:get_int 'starlit_electronics:battery_degrade' -- µJ/μW
          116  +	local dmg_J = dmg / 1000
          117  +	return (batClass.capacity - dmg_J)
          118  +end)
          119  +
          120  +-- E.battery.charge(bat) --> charge (J)
          121  +E.battery.charge = accessor('battery', function(stack, batClass, meta)
          122  +	local fac = meta:get_float 'starlit_electronics:battery_charge'
          123  +	-- local fac = 1 - safeWearToFac(stack:get_wear())
          124  +	return E.battery.capacity(stack) * fac
          125  +end)
          126  +
          127  +-- E.battery.dischargeRate(bat) --> dischargeRate (W)
          128  +E.battery.dischargeRate = accessor('battery', function(stack, batClass, meta)
          129  +	local dmg = meta:get_int 'starlit_electronics:battery_degrade' -- µJ/μW
          130  +	local dmg_W = dmg / 1000
          131  +	return batClass.dischargeRate - dmg_W
          132  +end);
          133  +
          134  +
          135  +-- E.battery.drawCurrent(bat, power, time, test) --> supply (J), wasteHeat (J)
          136  +--       bat   = battery stack
          137  +--     power J = joules of energy user wishes to consume
          138  +--      time s = the amount of time available for this transaction
          139  +--    supply J = how much power was actually provided in $time seconds
          140  +-- wasteHeat J = how heat is generated in the process
          141  +--      test   = if true, the battery is not actually modified
          142  +E.battery.drawCurrent = accessor('battery', function(s, bc, m, power, time, test)
          143  +	local ch = E.battery.charge(s)
          144  +	local maxPower = math.min(E.battery.dischargeRate(s)*time, power, ch)
          145  +	ch = ch - maxPower
          146  +
          147  +	if not test then
          148  +		local degrade = m:get_int 'starlit_electronics:battery_degrade' or 0
          149  +		degrade = degrade + maxPower * bc.decay
          150  +		-- for each joule of power drawn, capacity degrades by `decay` J
          151  +		-- this should ordinarily be on the order of mJ or smaller
          152  +		m:set_int('starlit_electronics:battery_degrade', degrade)
          153  +		-- s:set_wear(safeWear(1 - (ch / E.battery.capacity(s))))
          154  +		m:set_float('starlit_electronics:battery_charge', ch / E.battery.capacity(s))
          155  +		E.battery.update(s)
          156  +	end
          157  +
          158  +	return maxPower, 0 -- FIXME specify waste heat
          159  +end)
          160  +
          161  +-- E.battery.recharge(bat, power, time) --> draw (J)
          162  +--     bat   = battery stack
          163  +--   power J = joules of energy user wishes to charge the battery with
          164  +--    time s = the amount of time available for this transaction
          165  +--    draw J = how much power was actually drawn in $time seconds
          166  +E.battery.recharge = accessor('battery', function(s, bc, m, power, time)
          167  +	local ch = E.battery.charge(s)
          168  +	local cap = E.battery.capacity(s)
          169  +	local maxPower = math.min(E.battery.dischargeRate(s)*time, power)
          170  +	local total = math.min(ch + maxPower, cap)
          171  +	-- s:set_wear(safeWear(1 - (total/cap)))
          172  +	m:set_float('starlit_electronics:battery_charge', total/cap)
          173  +	E.battery.update(s)
          174  +	return maxPower, 0 -- FIXME
          175  +end)
          176  +
          177  +E.battery.setCharge = accessor('battery', function(s, bc, m, newPower)
          178  +	local cap = E.battery.capacity(s)
          179  +	local power = math.min(cap, newPower)
          180  +	-- s:set_wear(safeWear(1 - (power/cap)))
          181  +	m:set_float('starlit_electronics:battery_charge', power/cap)
          182  +	E.battery.update(s)
          183  +end)
          184  +E.battery.setChargeF = accessor('battery', function(s, bc, m, newPowerF)
          185  +	local power = math.min(1.0, newPowerF)
          186  +	m:set_float('starlit_electronics:battery_charge', power)
          187  +	E.battery.update(s)
          188  +end)
          189  +
          190  +E.dynamo = { kind = {} }
          191  +
          192  +E.dynamo.drawCurrent = accessor('dynamo', function(s,c,m, power, time, test)
          193  +	return c.vtable.drawCurrent(s, power, time, test)
          194  +end)
          195  +E.dynamo.totalPower    = accessor('dynamo', function(s,c,m) return c.vtable.totalPower(s) end)
          196  +E.dynamo.dischargeRate = accessor('dynamo', function(s,c,m) return c.vtable.dischargeRate   (s) end)
          197  +E.dynamo.initialPower  = accessor('dynamo', function(s,c,m) return c.vtable.initialPower(s) end)
          198  +E.dynamo.wasteHeat     = accessor('dynamo', function(s,c,m) return c.vtable.wasteHeat(s) end)
          199  +-- baseline waste heat, produced whether or not power is being drawn. for batteries this is 0, but for
          200  +-- radiothermal generators it may be high
          201  +
          202  +E.dynamo.kind.battery = {
          203  +	drawCurrent = E.battery.drawCurrent;
          204  +	totalPower = E.battery.charge;
          205  +	initialPower = E.battery.capacity;
          206  +	dischargeRate = E.battery.dischargeRate;
          207  +	wasteHeat = function() return 0 end;
          208  +};
          209  +
          210  +starlit.item.battery.foreach('starlit_electronics:battery-gen', {}, function(id, def)
          211  +	minetest.register_tool(id, {
          212  +		short_description = def.name;
          213  +		groups = { battery = 1; dynamo = 1; electronic = 1; };
          214  +		inventory_image = def.img or 'starlit-item-battery.png';
          215  +		description = starlit.ui.tooltip {
          216  +			title = def.name;
          217  +			desc = def.desc;
          218  +			color = lib.color(0,.2,1);
          219  +			props = {
          220  +				{ title = 'Optimal Capacity', affinity = 'info';
          221  +					desc = lib.math.si('J', def.capacity) };
          222  +				{ title = 'Discharge Rate', affinity = 'info';
          223  +					desc = lib.math.si('W', def.dischargeRate) };
          224  +				{ title = 'Charge Efficiency', affinity = 'info';
          225  +					desc = string.format('%s%%', (1-def.leak) * 100) };
          226  +				{ title = 'Size', affinity = 'info';
          227  +					desc = lib.math.si('m', def.fab.size.print) };
          228  +			};
          229  +		};
          230  +		_starlit = {
          231  +			event = {
          232  +				create = function(st, how)
          233  +					--[[if not how.gift then -- cheap hack to make starting batteries fully charged
          234  +						E.battery.setCharge(st, 0)
          235  +					end]]
          236  +					E.battery.update(st)
          237  +				end;
          238  +			};
          239  +			fab = def.fab;
          240  +			dynamo = {
          241  +				vtable = E.dynamo.kind.battery;
          242  +			};
          243  +			battery = def;
          244  +		};
          245  +	})
          246  +end)
          247  +
          248  +
          249  +-- to use the power functions, consider the following situation. you have
          250  +-- a high-tier battery charger that can draw 100kW. (for simplicity, assume
          251  +-- it supports only one battery). if you install a low-tier battery, and
          252  +-- the charging callback is called every five seconds, you might use
          253  +-- a `recharge` call that looks like
          254  +--
          255  +--   starlit.mod.electronics.battery.recharge(bat, 5 * 100*1e4, 5)
          256  +--
          257  +-- this would offer the battery 500kJ over five seconds. the battery will
          258  +-- determine how much power it can actually make use of in 5 five seconds,
          259  +-- and then return that amount.
          260  +--
          261  +-- always remember to save the battery back to its inventory slot after
          262  +-- modifying its ItemStack with one of these functions!
          263  +
          264  +
          265  +-- battery types
          266  +-- supercapacitor: low capacity, no degrade, high dischargeRate, no leak
          267  +-- chemical: high capacity, high degrade, mid dischargeRate, low leak
          268  +
          269  +-- battery tiers
          270  +-- makeshift: cheap, weak, low quality
          271  +-- imperial ("da red wunz go fasta"): powerful, low quality
          272  +-- commune ("snooty sophisticates"): limited power, high quality, expensive
          273  +-- usukwinya ("value engineering"): high power, mid quality, affordable
          274  +-- eluthrai ("uncompromising"): high power, high quality, wildly expensive
          275  +-- firstborn ("god-tier"): exceptional
          276  +
          277  +local batteryTiers = {
          278  +	makeshift = {
          279  +		name = 'Makeshift'; capacity = .5, decay = 3, leak = 2, dischargeRate = 1,
          280  +		fab = starlit.type.fab {
          281  +			metal = {copper=10};
          282  +		};
          283  +		desc = "Every attosecond this electrical abomination doesn't explode in your face is but the unearned grace of the Wild Gods.";
          284  +		complexity = 1;
          285  +		sw = {rarity = 1};
          286  +	};
          287  +	imperial  = {
          288  +		name = 'Imperial'; capacity = 2, decay = 2, leak = 2, dischargeRate = 2; 
          289  +		fab = starlit.type.fab {
          290  +			metal = {copper=15, iron = 20};
          291  +			size = { print = 0.1 };
          292  +		};
          293  +		desc = "The Empire's native technology is a lumbering titan: bulky, inefficient, unreliable, ugly, and awesomely powerful. Their batteries are no exception, with raw capacity and throughput that exceed even Usukinwya designs.";
          294  +		drm = 1;
          295  +		complexity = 2;
          296  +		sw = {rarity = 2};
          297  +	};
          298  +	commune   = {
          299  +		name = 'Commune'; capacity = 1, decay = .5, leak = .2, dischargeRate = 1; 
          300  +		fab = starlit.type.fab {
          301  +			metal = {vanadium=50, steel=10};
          302  +			size = { print = 0.05 };
          303  +		};
          304  +		desc = "The Commune's proprietary battery designs prioritize reliability, compactness, and maintenance concerns above raw throughput, with an elegance of engineering and design that would make a Su'ikuri cry.";
          305  +		complexity = 5;
          306  +		sw = {rarity = 3};
          307  +	};
          308  +	usukwinya = {
          309  +		name = 'Usukwinya'; capacity = 2, decay = 1, leak = 1, dischargeRate = 1.5,
          310  +		fab = starlit.type.fab {
          311  +			metal = {vanadium=30, argon=10};
          312  +			size = { print = 0.07 };
          313  +		};
          314  +		desc = "A race of consummate value engineers, the Usukwinya have spent thousands of years refining their tech to be as cheap to build as possible, without compromising much on quality. The Tradebirds drive an infamously hard bargain, but their batteries are more than worth their meagre cost.";
          315  +		drm = 2;
          316  +		sw = {rarity = 10};
          317  +		complexity = 15;
          318  +	};
          319  +	eluthrai  = {
          320  +		name = 'Eluthrai'; capacity = 3, decay = .4, leak = .1, dischargeRate = 1.5,
          321  +		fab = starlit.type.fab {
          322  +			metal = {beryllium=20, platinum=20, technetium = 1, cinderstone = 10 };
          323  +			size = { print = 0.03 };
          324  +		};
          325  +		desc = "The uncompromising Eluthrai are never satisfied until every quantifiable characteristic of their tech is maximally optimised down to the picoscale. Their batteries are some of the best in the Reach, and unquestionably the most expensive -- especially for those lesser races trying to copy the designs without the benefit of the sublime autofabricator ecosystem of the Eluthrai themselves.";
          326  +		complexity = 200;
          327  +		sw = {rarity = 0}; -- you think you're gonna buy eluthran schematics on SuperDiscountNanoWare.space??
          328  +	};
          329  +	firstborn = {
          330  +		name = 'Firstborn'; capacity = 5, decay = 0.1, leak = 0, dischargeRate = 3;
          331  +		fab = starlit.type.fab {
          332  +			metal = {neodymium=20, xenon=150, technetium=5, sunsteel = 10 };
          333  +			crystal = {astrite = 1};
          334  +			size = { print = 0.05 };
          335  +		};
          336  +		desc = "Firstborn engineering seamlessly merges psionic effects with a mastery of the physical universe unattained by even the greatest of the living Starsouls. Their batteries reach levels of performance that strongly imply Quantum Gravity Theory -- and several major holy books -- need to be rewritten. From the ground up.";
          337  +		complexity = 1000;
          338  +		sw = {rarity = 0}; -- lol no
          339  +	};
          340  +}
          341  +
          342  +local batterySizes = {
          343  +	small = {name = 'Small', capacity = .5, dischargeRate =  .5, complexity = 1, matMult = .5, fab = starlit.type.fab {size={print=0.1}}};
          344  +	mid   = {                capacity =  1, dischargeRate =   1, complexity = 1, matMult = 1, fab = starlit.type.fab {size={print=0.3}}};
          345  +	large = {name = 'Large', capacity =  2, dischargeRate = 1.5, complexity = 1, matMult = 1.5, fab = starlit.type.fab {size={print=0.5}}};
          346  +	huge  = {name = 'Huge',  capacity =  3, dischargeRate =   2, complexity = 1, matMult = 2, fab = starlit.type.fab {size={print=0.8}}};
          347  +}
          348  +
          349  +local batteryTypes = {
          350  +	supercapacitor = {
          351  +		name = 'Supercapacitor';
          352  +		desc = 'Room-temperature superconductors make for very reliable, high-dischargeRate, but low-capacity batteries.';
          353  +		fab = starlit.type.fab {
          354  +			metal = { enodium = 5 };
          355  +			size = {print=0.8};
          356  +		};
          357  +		sw = {
          358  +			cost = {
          359  +				cycles = 5e9; -- 5 bil cycles
          360  +				ram = 10e9; -- 10GB
          361  +			};
          362  +			pgmSize = 2e9; -- 2GB
          363  +			rarity = 5;
          364  +		};
          365  +		capacity = 50e3, dischargeRate = 1000;
          366  +		leak = 0, decay = 1e-6;
          367  +
          368  +		complexity = 3;
          369  +	};
          370  +	chemical = {
          371  +		name = 'Chemical';
          372  +		desc = '';
          373  +		fab = starlit.type.fab {
          374  +			element = { lithium = 3};
          375  +			metal = {iron = 5};
          376  +			size = {print=1.0};
          377  +		};
          378  +		sw = {
          379  +			cost = {
          380  +				cycles = 1e9; -- 1 bil cycles
          381  +				ram = 2e9; -- 2GB
          382  +			};
          383  +			pgmSize = 512e6; -- 512MB
          384  +			rarity = 2;
          385  +		};
          386  +		capacity = 200e3, dischargeRate = 200;
          387  +		leak = 0.2, decay = 1e-2;
          388  +		complexity = 1;
          389  +	};
          390  +	carbon = {
          391  +		name = 'Carbon';
          392  +		desc = 'Carbon nanotubes form the basis of many important metamaterials, chief among them power-polymer.';
          393  +		capacity = 1;
          394  +		fab = starlit.type.fab {
          395  +			element = { carbon = 40 };
          396  +			size = {print=0.5};
          397  +		};
          398  +		sw = {
          399  +			cost = {
          400  +				cycles = 50e9; -- 50 bil cycles
          401  +				ram = 64e9; -- 64GB
          402  +			};
          403  +			pgmSize = 1e9; -- 1GB
          404  +			rarity = 10;
          405  +		};
          406  +		capacity = 100e3, dischargeRate = 500;
          407  +		leak = 0.1, decay = 1e-3;
          408  +		complexity = 10;
          409  +	};
          410  +	hybrid = {
          411  +		name = 'Hybrid';
          412  +		desc = '';
          413  +		capacity = 1;
          414  +		fab = starlit.type.fab {
          415  +			element = {
          416  +				lithium = 3;
          417  +			};
          418  +			metal = {
          419  +				iron = 5;
          420  +			};
          421  +			size = {print=1.5};
          422  +		};
          423  +		sw = {
          424  +			cost = {
          425  +				cycles = 65e9; -- 65 bil cycles
          426  +				ram = 96e9; -- 96GB
          427  +			};
          428  +			pgmSize = 5e9; -- 5GB
          429  +			rarity = 15;
          430  +		};
          431  +		capacity = 300e3, dischargeRate = 350;
          432  +		leak = 0.3, decay = 1e-5;
          433  +		complexity = 30;
          434  +	};
          435  +}
          436  +
          437  +local function elemath(dest, src, mult)
          438  +	dest = dest or {}
          439  +	for k,v in pairs(src) do
          440  +		if not dest[k] then dest[k] = 0 end
          441  +		dest[k] = dest[k] + v*mult
          442  +	end
          443  +	return dest
          444  +end
          445  +
          446  +for bTypeName, bType in pairs(batteryTypes) do
          447  +for bTierName, bTier in pairs(batteryTiers) do
          448  +for bSizeName, bSize in pairs(batterySizes) do
          449  +	-- elemath(elementCost, bType.fab.element or {}, bSize.matMult)
          450  +	-- elemath(elementCost, bTier.fab.element or {}, bSize.matMult)
          451  +	-- elemath(metalCost, bType.fab.metal or {}, bSize.matMult)
          452  +	-- elemath(metalCost, bTier.fab.metal or {}, bSize.matMult)
          453  +	local fab = bType.fab + bTier.fab + bSize.fab + starlit.type.fab {
          454  +		element = {copper = 10, silicon = 5};
          455  +	}
          456  +	local baseID = string.format('battery_%s_%s_%s',
          457  +		bTypeName, bTierName, bSizeName)
          458  +	local id = 'starlit_electronics:'..baseID
          459  +	local name = string.format('%s %s Battery', bTier.name, bType.name)
          460  +	if bSize.name then name = bSize.name .. ' ' .. name end
          461  +	local function batStat(s)
          462  +		if s == 'size' then
          463  +			--return bType.fab[s] * (bTier.fab[s] or 1) * (bSize.fab[s] or 1)
          464  +			return fab.size and fab.size.print or 1
          465  +		else
          466  +			return bType[s] * (bTier[s] or 1) * (bSize[s] or 1)
          467  +		end
          468  +	end
          469  +
          470  +	local swID = 'starlit_electronics:schematic_'..baseID
          471  +	fab.reverseEngineer = {
          472  +		complexity = bTier.complexity * bSize.complexity * bType.complexity;
          473  +		sw = swID;
          474  +	}
          475  +	fab.flag = {print=true}
          476  +
          477  +	starlit.item.battery.link(id, {
          478  +		name = name;
          479  +		desc = table.concat({
          480  +			bType.desc or '';
          481  +			bTier.desc or '';
          482  +			bSize.desc or '';
          483  +		}, ' ');
          484  +
          485  +		fab = fab;
          486  +
          487  +		capacity = batStat 'capacity';
          488  +		dischargeRate  = batStat 'dischargeRate';
          489  +		leak     = batStat 'leak';
          490  +		decay    = batStat 'decay';
          491  +	})
          492  +
          493  +	local rare
          494  +	if bType.sw.rarity == 0 or bTier.sw.rarity == 0 then
          495  +		-- rarity is measured such that the player has a 1/r
          496  +		-- chance of finding a given item, or if r=0, no chance
          497  +		-- whatsoever (the sw must be obtained e.g. by reverse-
          498  +		-- engineering alien tech)
          499  +		rare = 0
          500  +	else
          501  +		rare = bType.sw.rarity + bTier.sw.rarity
          502  +	end
          503  +
          504  +	starlit.item.sw.link(swID, {
          505  +		kind = 'schematic';
          506  +		name = name .. ' Schematic';
          507  +		output = id;
          508  +		size = bType.sw.pgmSize;
          509  +		cost = bType.sw.cost;
          510  +		rarity = rare;
          511  +	})
          512  +
          513  +	E.schematicGroupLink('starlit_electronics:battery', swID)
          514  +
          515  +end end end
          516  +
          517  +
          518  +-----------
          519  +-- chips --
          520  +-----------
          521  +
          522  +E.sw = {}
          523  +function E.sw.findSchematicFor(item)
          524  +	local id = ItemStack(item):get_name()
          525  +	print(id)
          526  +	local fm = minetest.registered_items[id]._starlit
          527  +	if not (fm and fm.fab and fm.fab.reverseEngineer) then return nil end
          528  +	local id = fm.fab.reverseEngineer.sw
          529  +	return id, starlit.item.sw.db[id]
          530  +end
          531  +
          532  +E.chip = { file = {} }
          533  +do local T,G = lib.marshal.t, lib.marshal.g
          534  +	-- love too reinvent unions from first principles
          535  +	E.chip.data = G.struct {
          536  +		label = T.str;
          537  +		uuid = T.u64;
          538  +		files = G.array(16, G.class(G.struct {
          539  +			kind = G.enum {
          540  +				'sw'; -- a piece of installed software
          541  +				'note'; -- a user-readable text file
          542  +				'research'; -- saved RE progress
          543  +				'genome'; -- for use with plant biosequencer?
          544  +				'blob'; -- opaque binary blob, so 3d-pty mods can use the
          545  +                    -- file mechanism to store arbirary data.
          546  +			};
          547  +			drm = T.u8; -- inhibit copying
          548  +			name = T.str;
          549  +			body = T.text;
          550  +		}, function(file) -- enc
          551  +			local b = E.chip.file[file.kind].enc(file.body)
          552  +			return {
          553  +				kind = file.kind;
          554  +				drm = file.drm;
          555  +				name = file.name;
          556  +				body = b;
          557  +			}
          558  +		end, function(file) -- dec
          559  +			local f, ns = E.chip.file[file.kind].dec(file.body)
          560  +			file.body = f
          561  +			return file, ns
          562  +		end));
          563  +		bootSlot = T.u8; -- indexes into files; 0 = no bootloader
          564  +	}
          565  +	E.chip.file.sw = G.struct {
          566  +		pgmId = T.str;
          567  +		conf = G.array(16, G.struct {
          568  +			key = T.str, value = T.str;
          569  +		});
          570  +	}
          571  +	E.chip.file.note = G.struct {
          572  +		author = T.str;
          573  +		entries = G.array(16, G.struct {
          574  +			title = T.str;
          575  +			body = T.str;
          576  +		});
          577  +	}
          578  +	E.chip.file.research = G.struct {
          579  +		itemId = T.str;
          580  +		progress = T.clamp;
          581  +	}
          582  +	E.chip.file.blob = G.struct {
          583  +		kind = T.str; -- MT ID that identifies a blob file type belonging to an external mod
          584  +		size = T.u8; -- this must be manually reported since we don't know how to evaluate it
          585  +		data = T.text;
          586  +	}
          587  +	function E.chip.fileSize(file)
          588  +		-- boy howdy
          589  +		if file.kind == 'blob' then
          590  +			return file.body.size
          591  +		elseif file.kind == 'note' then
          592  +			local sz = 0x10 + #file.body.author
          593  +			for _, e in pairs(file.body.entries) do
          594  +				sz = sz + #e.title + #e.body + 0x10 -- header overhead
          595  +			end
          596  +			return sz
          597  +		elseif file.kind == 'research' then
          598  +			local re = assert(minetest.registered_items[file.body.itemId]._starlit.fab.reverseEngineer)
          599  +			return starlit.item.sw.db[re.sw].size * file.body.progress
          600  +		elseif file.kind == 'sw' then
          601  +			return starlit.item.sw.db[file.body.pgmId].size
          602  +		elseif file.kind == 'genome' then
          603  +			return 0 -- TODO
          604  +		end
          605  +	end
          606  +	local metaKey = 'starlit_electronics:chip'
          607  +	function E.chip.read(chip)
          608  +		local m = chip:get_meta()
          609  +		local blob = m:get_string(metaKey)
          610  +		if blob and blob ~= '' then
          611  +			return E.chip.data.dec(lib.str.meta_dearmor(blob))
          612  +		else -- prepare to format the chip
          613  +			return {
          614  +				label = '';
          615  +				bootSlot = 0;
          616  +				uuid = math.floor(math.random(0,2^32));
          617  +				files = {};
          618  +			}
          619  +		end
          620  +	end
          621  +	function E.chip.write(chip, data)
          622  +		local m = chip:get_meta()
          623  +		m:set_string(metaKey, lib.str.meta_armor(E.chip.data.enc(data)))
          624  +		E.chip.update(chip)
          625  +	end
          626  +	function E.chip.fileOpen(chip, inode, fn)
          627  +		local c = E.chip.read(chip)
          628  +		if fn(c.files[inode]) then
          629  +			E.chip.write(chip, c)
          630  +			return true
          631  +		end
          632  +		return false
          633  +	end
          634  +	function E.chip.fileWrite(chip, inode, file)
          635  +		local c = E.chip.read(chip)
          636  +		c.files[inode] = file
          637  +		E.chip.write(chip, c)
          638  +	end
          639  +	function E.chip.usedSpace(chip, d)
          640  +		d = d or E.chip.read(chip)
          641  +		local sz = 0
          642  +		for _, f in pairs(d.files) do
          643  +			sz = sz + E.chip.fileSize(f)
          644  +		end
          645  +		return sz
          646  +	end
          647  +	function E.chip.freeSpace(chip, d)
          648  +		local used = E.chip.usedSpace(chip,d)
          649  +		local max = assert(chip:get_definition()._starlit.chip.flash)
          650  +		return max - used
          651  +	end
          652  +	function E.chip.install(chip, file)
          653  +		-- remember to write out the itemstack after using this function!
          654  +		local d = E.chip.read(chip)
          655  +		if E.chip.freeSpace(chip, d) - E.chip.fileSize(file) >= 0 then
          656  +			table.insert(d.files, file)
          657  +			E.chip.write(chip, d)
          658  +			return true
          659  +		else
          660  +			return false
          661  +		end
          662  +	end
          663  +end
          664  +
          665  +function E.chip.files(ch)
          666  +	local m = ch:get_meta()
          667  +	if not m:contains 'starlit_electronics:chip' then
          668  +		return nil
          669  +	end
          670  +	local data = E.chip.read(ch)
          671  +	local f = 0
          672  +	return function()
          673  +		f = f + 1
          674  +		return data.files[f], f
          675  +	end
          676  +end
          677  +
          678  +function E.chip.describe(ch, defOnly)
          679  +	local def, data if defOnly then
          680  +		def, data = ch, {}
          681  +	else
          682  +		def = ch:get_definition()
          683  +		local m = ch:get_meta()
          684  +		if m:contains 'starlit_electronics:chip' then
          685  +			data = E.chip.read(ch)
          686  +		else
          687  +			data = {}
          688  +			defOnly = true
          689  +		end
          690  +		def = assert(def._starlit.chip)
          691  +	end
          692  +	local props = {
          693  +		{title = 'Clock Rate', affinity = 'info';
          694  +		 desc  = lib.math.si('Hz', def.clockRate)};
          695  +		{title = 'RAM', affinity = 'info';
          696  +		 desc  = lib.math.si('B', def.ram)};
          697  +	}
          698  +	if not defOnly then
          699  +		table.insert(props, {
          700  +			title = 'Free Storage', affinity = 'info';
          701  +			 desc = lib.math.si('B', E.chip.freeSpace(ch, data)) .. ' / '
          702  +			     .. lib.math.si('B', def.flash);
          703  +		})
          704  +		local swAffMap = {
          705  +			schematic = 'schematic';
          706  +			suitPower = 'ability';
          707  +			driver = 'driver';
          708  +		}
          709  +		for i, e in ipairs(data.files) do
          710  +			local aff = 'neutral'
          711  +			local name = e.name
          712  +			local disabled = false
          713  +			if e.kind == 'sw' then
          714  +				for _,cf in pairs(e.body.conf) do
          715  +					if cf.key == 'disable' and cf.value == 'yes' then
          716  +						disabled = true
          717  +						break
          718  +					end
          719  +				end
          720  +				local sw = starlit.item.sw.db[e.body.pgmId]
          721  +				aff = swAffMap[sw.kind] or 'good'
          722  +				if name == '' then name = sw.name end
          723  +			end
          724  +			name = name or '<???>'
          725  +			table.insert(props, disabled and {
          726  +				title = name;
          727  +				affinity = aff;
          728  +				desc = '<off>';
          729  +			} or {
          730  +				--title = name;
          731  +				affinity = aff;
          732  +				desc = name;
          733  +			})
          734  +		end
          735  +	else
          736  +		table.insert(props, {
          737  +			title = 'Flash Storage', affinity = 'info';
          738  +			 desc = lib.math.si('B', def.flash);
          739  +		 })
          740  +	end
          741  +	return starlit.ui.tooltip {
          742  +		title = data.label and data.label~='' and string.format('<%s>', data.label) or def.name;
          743  +		color = lib.color(.6,.6,.6);
          744  +		desc = def.desc;
          745  +		props = props;
          746  +	};
          747  +end
          748  +
          749  +function E.chip.update(chip)
          750  +	chip:get_meta():set_string('description', E.chip.describe(chip))
          751  +end
          752  +
          753  +starlit.item.chip.foreach('starlit_electronics:chip-gen', {}, function(id, def)
          754  +	minetest.register_craftitem(id, {
          755  +		short_description = def.name;
          756  +		description = E.chip.describe(def, true);
          757  +		inventory_image = def.img or 'starlit-item-chip.png';
          758  +		groups = {chip = 1};
          759  +		_starlit = {
          760  +			fab = def.fab;
          761  +			chip = def;
          762  +		};
          763  +	})
          764  +end)
          765  +
          766  +-- in case other mods want to define their own tiers
          767  +E.chip.tiers = lib.registry.mk 'starlit_electronics:chipTiers'
          768  +E.chip.tiers.meld {
          769  +	-- GP chips
          770  +	tiny    = {name = 'Tiny Chip', clockRate = 512e3, flash = 4096, ram = 1024, powerEfficiency = 1e9, size = 1};
          771  +	small   = {name = 'Small Chip', clockRate = 128e6, flash = 512e6, ram = 512e6, powerEfficiency = 1e8, size = 3};
          772  +	med     = {name = 'Chip', clockRate = 1e9, flash = 4e9, ram = 4e9, powerEfficiency = 1e7, size = 6};
          773  +	large   = {name = 'Large Chip', clockRate = 2e9, flash = 8e9, ram = 8e9, powerEfficiency = 1e6, size = 8};
          774  +	-- specialized chips
          775  +	compute = {name = 'Compute Chip', clockRate = 4e9, flash = 24e6, ram = 64e9, powerEfficiency = 1e8, size = 4};
          776  +	data    = {name = 'Data Chip', clockRate = 128e3, flash = 2e12, ram = 32e3, powerEfficiency = 1e5, size = 4};
          777  +	lp      = {name = 'Low-Power Chip', clockRate = 128e6, flash = 64e6, ram = 1e9, powerEfficiency = 1e10, size = 4};
          778  +	carbon  = {name = 'Carbon Chip', clockRate = 64e6, flash = 32e6, ram = 2e6, powerEfficiency = 2e9, size = 2, circ='carbon'};
          779  +}
          780  +
          781  +E.chip.tiers.foreach('starlit_electronics:genChips', {}, function(id, t)
          782  +	id = t.id or string.format('%s:chip_%s', minetest.get_current_modname(), id)
          783  +	local circMat = t.circ or 'silicon';
          784  +	starlit.item.chip.link(id, {
          785  +		name = t.name;
          786  +		clockRate = t.clockRate;
          787  +		flash = t.flash;
          788  +		ram = t.ram;
          789  +		powerEfficiency = t.powerEfficiency; -- cycles per joule
          790  +		fab = {
          791  +			flag = {
          792  +				silicompile = true;
          793  +			};
          794  +			time = {
          795  +				silicompile = t.size * 24*60;
          796  +			};
          797  +			cost = {
          798  +				energy = 50e3 + t.size * 15e2;
          799  +			};
          800  +			element = {
          801  +				[circMat] = 50 * t.size;
          802  +				copper = 30;
          803  +				gold = 15;
          804  +			};
          805  +		};
          806  +	})
          807  +end)
          808  +
          809  +function E.chip.findBest(test, ...)
          810  +	local chip, bestFitness
          811  +	for id, c in pairs(starlit.item.chip.db) do
          812  +		local fit, fitness = test(c, ...)
          813  +		if fit and (bestFitness == nil or fitness > bestFitness) then
          814  +			chip, bestFitness = id, fitness
          815  +		end
          816  +	end
          817  +	return chip, starlit.item.chip.db[chip], bestFitness
          818  +end
          819  +
          820  +function E.chip.findForStorage(sz)
          821  +	return E.chip.findBest(function(c)
          822  +		return c.flash >= sz, -math.abs(c.flash - sz)
          823  +	end)
          824  +end
          825  +
          826  +function E.chip.sumCompute(chips)
          827  +	local c = {
          828  +		cycles = 0;
          829  +		ram = 0;
          830  +		flashFree = 0;
          831  +		powerEfficiency = 0;
          832  +	}
          833  +	local n = 0
          834  +	for _, e in pairs(chips) do
          835  +		n = n + 1
          836  +		if not e:is_empty() then
          837  +			local ch = e:get_definition()._starlit.chip
          838  +			c.cycles = c.cycles + ch.clockRate
          839  +			c.ram = c.ram + ch.clockRate
          840  +			c.flashFree = c.flashFree + E.chip.freeSpace(e)
          841  +			c.powerEfficiency = c.powerEfficiency + ch.powerEfficiency
          842  +		end
          843  +	end
          844  +	if n > 0 then c.powerEfficiency = c.powerEfficiency / n end
          845  +	return c
          846  +end
          847  +
          848  +E.chip.fileHandle = lib.class {
          849  +	__name = 'starlit_electronics:chip.fileHandle';
          850  +	construct = function(chip, inode) -- stack, int --> fd
          851  +		return { chip = chip, inode = inode }
          852  +	end;
          853  +	__index = {
          854  +		read = function(self)
          855  +			local dat = E.chip.read(self.chip)
          856  +			return dat.files[self.inode]
          857  +		end;
          858  +		write = function(self,data)
          859  +			-- print('writing', self.chip, self.inode)
          860  +			return E.chip.fileWrite(self.chip, self.inode, data)
          861  +		end;
          862  +		erase = function(self)
          863  +			local dat = E.chip.read(self.chip)
          864  +			table.remove(dat.files, self.inode)
          865  +			E.chip.write(self.chip, dat)
          866  +			self.inode = nil
          867  +		end;
          868  +		open = function(self,fn)
          869  +			return E.chip.fileOpen(self.chip, self.inode, fn)
          870  +		end;
          871  +	};
          872  +}
          873  +
          874  +function E.chip.usableSoftware(chips,pgm)
          875  +	local comp = E.chip.sumCompute(chips)
          876  +	local r = {}
          877  +	local unusable = {}
          878  +	local sw if pgm then
          879  +		if type(pgm) == 'string' then
          880  +			pgm = {starlit.item.sw.db[pgm]}
          881  +		end
          882  +		sw = pgm
          883  +	else
          884  +		sw = {}
          885  +		for i, e in ipairs(chips) do
          886  +			if (not e:is_empty())
          887  +			   and minetest.get_item_group(e:get_name(), 'chip') ~= 0
          888  +			then
          889  +				for fl, inode in E.chip.files(e) do
          890  +					if fl.kind == 'sw' then
          891  +						local s = starlit.item.sw.db[fl.body.pgmId]
          892  +						table.insert(sw, {
          893  +							sw = s, chip = e, chipSlot = i;
          894  +							file = fl, inode = inode;
          895  +						})
          896  +					end
          897  +				end
          898  +			end
          899  +		end
          900  +	end
          901  +
          902  +	for _, s in pairs(sw) do
          903  +		if s.sw.cost.ram <= comp.ram then
          904  +			table.insert(r, {
          905  +				sw = s.sw;
          906  +				chip = s.chip, chipSlot = s.chipSlot;
          907  +				file = s.file;
          908  +				fd = E.chip.fileHandle(s.chip, s.inode);
          909  +				speed = s.sw.cost.cycles / comp.cycles;
          910  +				powerCost = s.sw.cost.cycles / comp.powerEfficiency;
          911  +				comp = comp;
          912  +			})
          913  +		else
          914  +			table.insert(unusable, {
          915  +				sw = s.sw;
          916  +				chip = s.chip;
          917  +				ramNeeded = s.sw.cost.ram - comp.ram;
          918  +			})
          919  +		end
          920  +	end
          921  +	return r, unusable
          922  +end
          923  +
          924  +starlit.include 'sw'

Added mods/starlit-electronics/mod.conf version [c7d10b71e3].

            1  +name = starlit_electronics
            2  +title = starlit electronics
            3  +description = basic electronic components and logic
            4  +depends = starlit

Added mods/starlit-electronics/sw.lua version [288b4b5859].

            1  +-- [ʞ] sw.lua
            2  +--  ~ lexi hale <lexi@hale.su>
            3  +--  🄯 EUPL v1.2
            4  +--  ? 
            5  +
            6  +-------------------------------
            7  +-- basic suit nano abilities --
            8  +-------------------------------
            9  +local function shredder(prop)
           10  +	local function getItemsForFab(fab)
           11  +		local elt
           12  +		if fab then
           13  +			elt = fab:elementalize()
           14  +		else
           15  +			elt = {}
           16  +		end
           17  +		local items = {}
           18  +		if elt.element then
           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)
           25  +			end
           26  +		end
           27  +		return items
           28  +	end
           29  +
           30  +	return function(user, ctx)
           31  +		local function cleanup()
           32  +			user.action.prog.shred = nil
           33  +			if user.action.sfx.shred then
           34  +				minetest.sound_fade(user.action.sfx.shred, 1, 0)
           35  +				user.action.sfx.shred = nil
           36  +			end
           37  +			if user.action.fx.shred then
           38  +				user.action.fx.shred.abort()
           39  +			end
           40  +		end
           41  +
           42  +		if user.action.tgt.type ~= 'node' then return end
           43  +		local what = user.action.tgt.under
           44  +		if what == nil or user.entity:get_pos():distance(what) > prop.range then
           45  +			cleanup()
           46  +			return false
           47  +		end
           48  +		local shredTime = 1.0
           49  +		local soundPitch = 1.0 -- TODO
           50  +		local pdraw = prop.powerDraw or 0
           51  +
           52  +		local node = minetest.get_node(what)
           53  +		local nd = minetest.registered_nodes[node.name]
           54  +		local elt, fab, vary
           55  +		if nd._starlit then
           56  +			fab = nd._starlit.recover or nd._starlit.fab
           57  +			vary = nd._starlit.recover_vary
           58  +		end
           59  +		if fab then
           60  +			if fab.flag then
           61  +				if fab.flag.unshreddable then
           62  +					cleanup()
           63  +					return false
           64  +					-- TODO error beep
           65  +				end
           66  +			end
           67  +			shredTime = fab.time and fab.time.shred or shredTime -- FIXME
           68  +			if fab.cost and fab.cost.shredPower then
           69  +				pdraw = pdraw * fab.cost.shredPower
           70  +			end
           71  +		end
           72  +		local maxW = user:getSuit():maxPowerUse()
           73  +		if maxW < pdraw then
           74  +			shredTime = shredTime * (pdraw/maxW)
           75  +			pdraw = maxW
           76  +		end
           77  +		if ctx.how.state == 'prog' then
           78  +			local pdx = pdraw * ctx.how.delta
           79  +			local p = user:suitDrawCurrent(pdx, ctx.how.delta, {kind='nano',label='Shredder'}, pdx)
           80  +			if p < pdx then
           81  +				cleanup()
           82  +				return false
           83  +			elseif not user.action.prog.shred then
           84  +				cleanup() -- kill danglers
           85  +				-- begin
           86  +				user.action.prog.shred = 0
           87  +				user.action.sfx.shred = minetest.sound_play('starlit-nano-shred', {
           88  +					object = user.entity;
           89  +					max_hear_distance = prop.range*2;
           90  +					loop = true;
           91  +					pitch = soundPitch;
           92  +				})
           93  +				user.action.fx.shred = starlit.fx.nano.shred(user, what, prop, shredTime, node)
           94  +			else
           95  +				user.action.prog.shred = user.action.prog.shred + ctx.how.delta or 0
           96  +			end
           97  +			--print('shred progress: ', user.action.prog.shred)
           98  +			if user.action.prog.shred >= shredTime then
           99  +				if minetest.dig_node(what) then
          100  +					--print('shred complete')
          101  +					user:suitSound 'starlit-success'
          102  +					if fab then
          103  +						local vf = fab
          104  +						if vary then
          105  +							local rng = (starlit.world.seedbank+0xa891f62)[minetest.hash_node_position(what)]
          106  +							vf = vf + vary(rng, {})
          107  +						end
          108  +						local items = getItemsForFab(vf)
          109  +						for i, it in ipairs(items) do user:give(it) end
          110  +					end
          111  +				else
          112  +					user:suitSound 'starlit-error'
          113  +				end
          114  +				cleanup()
          115  +			end
          116  +		elseif ctx.how.state == 'halt' then
          117  +			cleanup()
          118  +		end
          119  +		return true
          120  +	end
          121  +end
          122  +
          123  +starlit.item.sw.link('starlit_electronics:shred', {
          124  +	name = 'NanoShred';
          125  +	kind = 'suitPower', powerKind = 'active';
          126  +	desc = 'An open-source program used in its various forks and iterations all across human-inhabited space and beyond. Rumored to contain fragments of code stolen from the nanoware of the Greater Races by an elusive infoterrorist.';
          127  +	size = 500e3;
          128  +	cost = {
          129  +		cycles = 100e6;
          130  +		ram = 500e6;
          131  +	};
          132  +	run = shredder{range=2, powerDraw=200};
          133  +})
          134  +
          135  +starlit.item.sw.link('starlit_electronics:compile_commune', {
          136  +	name = 'Compile Matter';
          137  +	kind = 'suitPower', powerKind = 'direct';
          138  +	desc = "A basic suit matter compiler program, rather slow but ruthlessly optimized for power- and memory-efficiency by some of the Commune's most fanatic coders.";
          139  +	size = 700e3;
          140  +	cost = {
          141  +		cycles = 300e6;
          142  +		ram = 2e9;
          143  +	};
          144  +	ui = 'starlit:compile-matter-component';
          145  +	run = function(user, ctx)
          146  +	end;
          147  +})
          148  +
          149  +starlit.item.sw.link('starlit_electronics:compile_block_commune', {
          150  +	name = 'Compile Block';
          151  +	kind = 'suitPower', powerKind = 'active';
          152  +	desc = "An advanced suit matter compiler program, capable of printing complete devices and structure parts directly into the world.";
          153  +	size = 5e6;
          154  +	cost = {
          155  +		cycles = 700e6;
          156  +		ram = 4e9;
          157  +	};
          158  +	ui = 'starlit:compile-matter-block';
          159  +	run = function(user, ctx)
          160  +	end;
          161  +})
          162  +
          163  +do local J = starlit.store.compilerJob
          164  +	starlit.item.sw.link('starlit_electronics:driver_compiler_commune', {
          165  +		name = 'Matter Compiler';
          166  +		kind = 'driver';
          167  +		desc = "A driver for a standalone matter compiler, suitable for building larger components than your suit alone can handle.";
          168  +		size = 850e3;
          169  +		cost = {
          170  +			cycles = 400e6;
          171  +			ram = 2e9;
          172  +		};
          173  +		ui = 'starlit:device-compile-matter-component';
          174  +		run = function(user, ctx)
          175  +		end;
          176  +		bgProc = function(user, ctx, interval, runState)
          177  +			if runState.flags.compiled == true then return false end
          178  +			-- only so many nanides to go around
          179  +			runState.flags.compiled = true
          180  +			local time = minetest.get_gametime()
          181  +			local cyclesLeft = ctx.comp.cycles * interval
          182  +
          183  +			for id, e in ipairs(ctx.file.body.conf) do
          184  +				if e.key == 'job' then
          185  +					local t = J.dec(e.value)
          186  +					local remove = false
          187  +					local r = starlit.item.sw.db[t.schematic]
          188  +					if not r then -- bad schematic
          189  +						remove = true
          190  +					else
          191  +						local ccost = ctx.sw.cost.cycles + r.cost.cycles
          192  +						local tcost = ccost / cyclesLeft
          193  +						t.progress = t.progress + (1/tcost)*interval
          194  +						cyclesLeft = cyclesLeft - ccost*interval
          195  +						if t.progress >= 1 then
          196  +							-- complete
          197  +							remove = true
          198  +							local i = starlit.item.mk(r.output, {
          199  +								how = 'print';
          200  +								user = user; -- for suit
          201  +								compiler = {
          202  +									node = ctx.compiler; -- for device
          203  +									sw = ctx.sw;
          204  +									install = ctx.fd;
          205  +								};
          206  +								schematic = r;
          207  +							})
          208  +							ctx.giveItem(i)
          209  +						end
          210  +					end
          211  +					if remove then
          212  +						table.remove(ctx.file.body.conf, id)
          213  +					else
          214  +						e.value = J.enc(t)
          215  +					end
          216  +					if not cyclesLeft > 0 then break end
          217  +				end
          218  +			end
          219  +			ctx.saveConf()
          220  +		end;
          221  +	})
          222  +end
          223  +
          224  +local function pasv_heal(effect, energy, lvl, pgmId)
          225  +	return function(user, ctx, interval, runState)
          226  +		if runState.flags.healed == true then return false end
          227  +		-- competing nanosurgical programs?? VERY bad idea
          228  +		runState.flags.healed = true
          229  +
          230  +		local amt, f = user:effectiveStat 'health'
          231  +		local st = user:getSuit():powerState()
          232  +		if (st == 'on' and f < lvl) or (st == 'powerSave' and f < math.min(lvl,0.25)) then
          233  +			local maxPower = energy*interval
          234  +			local p = user:suitDrawCurrent(maxPower, interval, {
          235  +				id = 'heal';
          236  +				src = 'suitPower';
          237  +				pgmId = pgmId;
          238  +				healAmount = effect;
          239  +			})
          240  +			if p > 0 then
          241  +				local heal = (p/maxPower) * ctx.speed * effect*interval
          242  +				--user:statDelta('health', math.max(1, heal))
          243  +				starlit.fx.nano.heal(user, {{player=user.entity}}, heal, 1)
          244  +				return true
          245  +			end
          246  +		end
          247  +		return false -- program did not run
          248  +	end;
          249  +end
          250  +
          251  +starlit.item.sw.link('starlit_electronics:nanomed', {
          252  +	name = 'NanoMed';
          253  +	kind = 'suitPower', powerKind = 'passive';
          254  +	desc = 'Repair of the body is a Commune specialty, and their environment suits all come equipped with highly sophisticated nanomedicine suites, able to repair even the most grievous of wounds given sufficient energy input and time.';
          255  +	size = 2e9;
          256  +	cost = {
          257  +		cycles = 400e6;
          258  +		ram = 3e9;
          259  +	};
          260  +	run = pasv_heal(2, 20, 1);
          261  +})
          262  +
          263  +starlit.item.sw.link('starlit_electronics:autodoc_deluxe', {
          264  +	name = 'AutoDoc Deluxe';
          265  +	kind = 'suitPower', powerKind = 'passive';
          266  +	desc = "A flagship offering of the Excellence Unyielding nanoware division, AutoDoc Deluxe has been the top-rated nanocare package in the Celestial Shores Province for six centuries and counting. Every chip includes our comprehensive database of illnesses, prosyn schematics, and organ repair techniques, with free over-the-ether updates guaranteed for ten solariads from date of purchase! When professional medical care just isn't an option, 9/10 doctors recommend Excellence Unyielding AutoDoc Deluxe! The remaining doctor was bribed by our competitors.";
          267  +	size = 1e9;
          268  +	cost = {
          269  +		cycles = 700e6;
          270  +		ram = 1e9;
          271  +	};
          272  +	run = pasv_heal(4, 50, .7);
          273  +})

Added mods/starlit-material/elements.lua version [5740080b38].

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

Added mods/starlit-material/init.lua version [fffa847df5].

            1  +local lib = starlit.mod.lib
            2  +local M = {
            3  +	canisterSizes = lib.registry.mk 'starlit_material:canister-size';
            4  +}
            5  +M.canisterSizes.foreach('starlit_material:canister_link', {}, function(id, sz)
            6  +	starlit.item.canister.link(minetest.get_current_modname() .. ':canister_' .. id, {
            7  +		name = sz.name;
            8  +		slots = sz.slots;
            9  +		vol = 0.1; -- too big for suit?
           10  +		desc = sz.desc;
           11  +	})
           12  +end)
           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};
           19  +}
           20  +
           21  +starlit.include 'elements'
           22  +

Added mods/starlit-material/mod.conf version [e33ae98ab9].

            1  +name = starlit_material
            2  +title = starlit materials
            3  +description = defines the raw materials and alloys used in printing
            4  +depends = starlit

Added mods/starlit-scenario/init.lua version [bf764e9a62].

            1  +local lib = starlit.mod.lib
            2  +local scenario = starlit.world.scenario
            3  +
            4  +local function makeChip(label, schem, sw)
            5  +	local E = starlit.mod.electronics
            6  +	local files = {}
            7  +	local sz = 0
            8  +	for _, e in ipairs(schem) do
            9  +		local p = E.sw.findSchematicFor(e[1])
           10  +		if p then
           11  +			local file = {
           12  +				kind = 'sw', name = '', drm = e[2];
           13  +				body = {pgmId = p};
           14  +			}
           15  +			table.insert(files, file)
           16  +			sz = sz + E.chip.fileSize(file)
           17  +		end
           18  +	end
           19  +	for _, e in ipairs(sw) do
           20  +		local file = {
           21  +			kind = 'sw', name = '', drm = e[2];
           22  +			body = {pgmId = e[1]};
           23  +		}
           24  +		table.insert(files, file)
           25  +		sz = sz + E.chip.fileSize(file)
           26  +	end
           27  +	local chip = ItemStack(assert(E.chip.findBest(function(c)
           28  +		return c.flash >= sz, c.ram + c.clockRate
           29  +	end)))
           30  +	local r = E.chip.read(chip)
           31  +	r.label = label
           32  +	r.files = files
           33  +	E.chip.write(chip, r)
           34  +	return chip
           35  +end
           36  +
           37  +local chipLibrary = {
           38  +	compendium = makeChip('The Gentleman Adventurer\'s Compleat Wilderness Compendium', {
           39  +		{'starlit_electronics:battery_chemical_imperial_small', 0};
           40  +	}, {
           41  +		{'starlit_electronics:shred', 0};
           42  +		--{'starlit_electronics:compile_empire', 0};
           43  +		{'starlit_electronics:autodoc_deluxe', 1};
           44  +		--{'starlit_electronics:driver_compiler_empire', 0};
           45  +	});
           46  +	survivalware = makeChip('Emergency Survivalware', {
           47  +		{'starlit_electronics:battery_chemical_commune_small', 0};
           48  +	}, {
           49  +		{'starlit_electronics:shred', 0};
           50  +		{'starlit_electronics:compile_commune', 0};
           51  +		{'starlit_electronics:nanomed', 0};
           52  +		{'starlit_electronics:driver_compiler_commune', 0};
           53  +	});
           54  +	misfortune = makeChip("Sold1er0fMisf0rtune TOP Schematic Crackz REPACK", {
           55  +		{'starlit_electronics:battery_chemical_usukwinya_mid', 0};
           56  +		{'starlit_electronics:battery_hybrid_imperial_small', 0};
           57  +		-- ammunition
           58  +	}, {});
           59  +}
           60  +
           61  +local battery = function(name)
           62  +	local s = ItemStack(name)
           63  +	starlit.mod.electronics.battery.setChargeF(s, 1.0)
           64  +	return s
           65  +end
           66  +
           67  +
           68  +table.insert(scenario, {
           69  +	id = 'starlit_scenario:imperialExpat';
           70  +	name = 'Imperial Expat';
           71  +	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  +
           73  +	species = 'human';
           74  +	speciesVariant = 'female';
           75  +	soul = {
           76  +		externalChannel = true; -- able to touch other souls in the spiritual realm
           77  +		physicalChannel = true; -- able to extend influence into physical realm
           78  +		damage = 1;
           79  +	};
           80  +	social = {
           81  +		empire = 'workingClass';
           82  +		commune = 'metic';
           83  +	};
           84  +
           85  +	startingItems = {
           86  +		suit = ItemStack('starlit_suit:suit_survival_commune');
           87  +		suitBatteries = {battery 'starlit_electronics:battery_carbon_commune_small'};
           88  +		suitChips = {
           89  +			chipLibrary.survivalware;
           90  +			-- you didn't notice it earlier, but your Commune environment suit
           91  +			-- came with this chip already plugged in. it's apparently true
           92  +			-- what they say: the Commune is always prepared for everything.
           93  +			-- E V E R Y T H I N G.
           94  +		};
           95  +		suitGuns = {};
           96  +		suitAmmo = {};
           97  +		suitCans = {
           98  +			ItemStack('starlit_material:canister_small');
           99  +		};
          100  +		carry = {
          101  +			chipLibrary.compendium;
          102  +			-- you bought this on a whim before you left the Empire, and
          103  +			-- just happened to still have it on your person when everything
          104  +			-- went straight to the Wild Gods' privy
          105  +		};
          106  +	};
          107  +})
          108  +
          109  +table.insert(scenario, {
          110  +	id = 'starlit_scenario:gentlemanAdventurer';
          111  +	--       Othar Tryggvasson,
          112  +	name = 'Gentleman Adventurer';
          113  +	desc = "Tired of the same-old-same-old, sick of your idiot contemporaries, exasperated with the shallow soul-rotting luxury of life as landless lordling, and earnestly eager to enrage your father, you resolved to see the Reach in all her splendor. Deftly evading the usual tourist traps, you finagled your way into the confidence of the Commune ambassador with a few modest infusions of Father's money -- now *that* should pop his monocle -- and secured yourself a seat on a ride to their brand-new colony at Thousand Petal. How exciting -- a genuine frontier outing!";
          114  +
          115  +	species = 'human';
          116  +	speciesVariant = 'male';
          117  +	soul = {
          118  +		externalChannel = true;
          119  +		physicalChannel = true;
          120  +		damage = 1;
          121  +	};
          122  +	social = {
          123  +		empire = 'aristo';
          124  +	};
          125  +
          126  +	startingItems = {
          127  +		suit = 'starlit_suit:suit_survival_imperial';
          128  +		suitBatteries = {battery 'starlit_electronics:battery_supercapacitor_imperial_mid'};
          129  +		suitChips = {
          130  +			chipLibrary.compendium;
          131  +			-- Mother, bless her soul, simply insisted on buying you this as a parting
          132  +			-- gift. "it's dangerous out there for a young man," she proclaimed as
          133  +			-- if she had profound firsthand experience of the matter. mindful of the
          134  +			-- husband she endures, you suffered to humor her, and made a big show of
          135  +			-- installing it your brand-new nanosuit before you fled the family seat.
          136  +		};
          137  +		suitGuns = {};
          138  +		suitAmmo = {};
          139  +		suitCans = {
          140  +			ItemStack('starlit_material:canister_mid');
          141  +		};
          142  +		carry = {};
          143  +	};
          144  +})
          145  +
          146  +table.insert(scenario, {
          147  +	-- you start out with strong combat abilities but weak engineering,
          148  +	-- and will have to scavenge wrecks to find basic crafting gear
          149  +	id = 'starlit_scenario:terroristTagalong';
          150  +	name = 'Terrorist Tagalong';
          151  +	desc = "It turns out there's a *reason* Crown jobs pay so well.";
          152  +	species = 'human';
          153  +	speciesVariant = 'female';
          154  +	social = {
          155  +		empire = 'lowlife';
          156  +		commune = 'mostWanted';
          157  +		underworldConnections = true;
          158  +	};
          159  +	soul = {
          160  +		externalChannel = true;
          161  +		physicalChannel = true;
          162  +		damage = 2; -- closer to the blast
          163  +	};
          164  +	startingItems = {
          165  +		suit = 'starlit_suit:suit_combat_imperial';
          166  +		suitBatteries = {
          167  +			ItemStack('starlit_electronics:battery_supercapacitor_imperial_small');
          168  +			ItemStack('starlit_electronics:battery_chemical_imperial_large');
          169  +		};
          170  +		suitGuns = {};
          171  +		suitAmmo = {};
          172  +		carry = {};
          173  +	};
          174  +	suitChips = {chipLibrary.misfortune};
          175  +})
          176  +
          177  +table.insert(scenario, {
          178  +	id = 'starlit_scenario:tradebirdBodyguard';
          179  +	name = 'Tradebird Bodyguard';
          180  +	desc = "You've never understood why astropaths of all people *insist* on bodyguards. This one could probably make hash of a good-sized human batallion, if her feathers were sufficiently ruffled. Perhaps it's a status thing. Whatever the case, it's easy money.\nAt least, it was supposed to be.'";
          181  +	species = 'usukwinya';
          182  +	speciesVariant = 'male';
          183  +	soul = {
          184  +		damage = 0; -- Inyukiriku and her entourage fled the ship when she sensed something serious was about to go down. lucky: the humans only survived because their souls were closed off to the Physical. less luckily, the explosion knocked your escape pod off course and the damn astropath was too busy saving her own skin to come after you
          185  +		externalChannel = true; -- usukwinya are already psionic
          186  +		physicalChannel = true; -- usukwinya are Starlit
          187  +	};
          188  +	startingItems = {
          189  +		suit = 'starlit_suit:suit_combat_usukwinya';
          190  +		suitBatteries = {
          191  +			ItemStack('starlit_electronics:battery_hybrid_usukwinya_mid');
          192  +		};
          193  +		suitGuns = {};
          194  +		suitChips = {};
          195  +		suitAmmo = {};
          196  +		carry = {};
          197  +	};
          198  +})

Added mods/starlit-scenario/mod.conf version [8a1ae19f59].

            1  +name = starlit_scenario
            2  +title = starlit scenarios
            3  +description = built-in scenarios for Starsoul
            4  +depends = starlit, starlit_suit, starlit_electronics, starlit_building, starlit_material
            5  +# be sure to add any mods from which you list new starting items!

Added mods/starlit-secrets/init.lua version [0a228a51be].

            1  +----------------------------------------------------
            2  +------------- CONTROLLED INFORMATION  --------------
            3  +----------------------------------------------------
            4  +-- THE INFORMATION CONTAINED IN THIS DOCUMENT IS  --
            5  +-- SUBJECT TO RESTRAINT OF TRANSMISSION PER THE   --
            6  +-- TERMS OF THE COMMUNE CHARTER INFOSECURITY      --
            7  +-- PROVISION. IF YOU ARE NOT AUTHORIZED UNDER THE --
            8  +-- AEGIS OF THE APPROPRITE CONTROLLING AUTHORITY, --
            9  +-- CLOSE THIS DOCUMENT IMMEDIATELY AND REPORT THE --
           10  +-- SECURITY BREACH TO YOUR DESIGNATED INFORMATION --
           11  +-- HYGIENE OVERSEER OR FACE CORRECTIVE DISCIPLINE --
           12  +----------------------------------------------------
           13  +
           14  +local lib = starlit.mod.lib
           15  +local sec = {}
           16  +starlit.mod.secrets = sec
           17  +
           18  +sec.index = lib.registry.mk 'starlit_secrets:secret'
           19  +
           20  +--[==[
           21  +
           22  +a secret is a piece of information that is made available
           23  +for review once certain conditions are met. despite the name,
           24  +it doesn't necessarily have to be secret -- it could include
           25  +e.g. journal entries about a character's background. a secret
           26  +is defined in the following manner:
           27  +
           28  +{
           29  +	title = the string that appears in the UI
           30  +	stages = {
           31  +		{
           32  +			prereqs = {
           33  +				{kind = 'fact', id = 'starlit:terroristEmployer'}
           34  +				{kind = 'item', id = 'starlit_electronic:firstbornDoomBong'}
           35  +				{kind = 'background', id = 'starlit:terroristTagalong'}
           36  +			}
           37  +			body = {
           38  +				'the firstborn smonked hella weed';
           39  +			};
           40  +			-- body can also be a function(user,secret)
           41  +		}
           42  +   }
           43  +}
           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  +a secret can be a single piece of information predicated
           49  +on a fact, in which case the secret and fact should share
           50  +the same ID. the ID should be as non-indicative as possible
           51  +to avoid spoilers for devs of unrelated code.
           52  +
           53  +a secret can also be manually unlocked e.g. by using an item
           54  +
           55  +]==]--
           56  +
           57  +function sec.prereqCheck(user, pr)
           58  +end

Added mods/starlit-secrets/mod.conf version [74f79378c7].

            1  +name = starlit_secrets
            2  +title = starlit secrets
            3  +description = TS//NOFORN
            4  +depends = starlit

Added mods/starlit-suit/init.lua version [078f484c1f].

            1  +local lib = starlit.mod.lib
            2  +local fab = starlit.type.fab
            3  +
            4  +local facDescs = {
            5  +	commune = {
            6  +		survival = {
            7  +			suit = 'A light, simple, bare-bones environment suit that will provide heating, cooling, and nanide support to a stranded cosmonaut';
            8  +			cc = 'The Survival Suit uses compact thermoelectrics to keep the wearer perfectly comfortable in extremes of heat or cold. It makes up for its heavy power usage with effective insulation that substantially reduces any need for climate control.';
            9  +		};
           10  +		engineer = {
           11  +			suit = 'A lightweight environment suit designed for indoor work, the Commune\'s Engineer Suit boasts advanced nanotech capable of constructing objects in place.';
           12  +			cc = 'The Engineer Suit is designed for indoor work. Consequently, it features only a low-power thermoelectric cooler meant to keep its wearer comfortable during strenuous work.';
           13  +		};
           14  +		combat = {
           15  +			suit = 'A military-grade suit with the latest Commune technology. Designed for maximum force multiplication, the suit has dual weapon hardpoints and supports a gargantuan power reserve. Its nanotech systems are specialized for tearing through obstacles, but can also be used to manufacturer ammunition in a pinch.';
           16  +			cc = 'This Combat Suit uses electrothermal cooling to keep an active soldier comfortable and effective, as well as conventional heating coils to enable operation in hostile atmospheres.';
           17  +		};
           18  +	};
           19  +
           20  +}
           21  +
           22  +
           23  +starlit.world.tier.foreach('starlit:suit-gen', {}, function(tid, t)
           24  +	local function hasTech(tech)
           25  +		return starlit.world.tier.tech(tid, tech)
           26  +	end
           27  +	if not hasTech 'suit' then return end
           28  +	-- TODO tier customization
           29  +	--
           30  +	local function fabsum(f)
           31  +		return starlit.world.tier.fabsum(tid, 'suit')
           32  +	end
           33  +	local function fabReq(sz, days)
           34  +		local tierMatBase = (
           35  +			(fabsum 'electric' * 4) +
           36  +			(fabsum 'basis' + fabsum 'suit') * sz
           37  +		)
           38  +		local b = tierMatBase + fab {
           39  +			-- universal suit requirements
           40  +			time = { print    = 60*60*24 * days };
           41  +			size = { printBay = sz              };
           42  +		}
           43  +		b.flag = lib.tbl.set('print');
           44  +		return b
           45  +	end
           46  +	local function facDesc(s, t)
           47  +		local default = 'A protective nanosuit' -- FIXME
           48  +		if not facDescs[tid] then return default end
           49  +		if not facDescs[tid][s] then return default end
           50  +		if not facDescs[tid][s][t] then return default end
           51  +		return facDescs[tid][s][t]
           52  +	end
           53  +	starlit.item.suit.link('starlit_suit:suit_survival_' .. tid, {
           54  +		name = t.name .. ' Survival Suit';
           55  +		desc = facDesc('survival','suit');
           56  +		fab = fabReq(1, 2.2) + fab { };
           57  +		tex = {
           58  +			plate = {
           59  +				id = 'starlit-suit-survival-plate';
           60  +				tint = lib.color {hue = 210, sat = .5, lum = .5};
           61  +			};
           62  +			lining = {
           63  +				id = 'starlit-suit-survival-lining';
           64  +				tint = lib.color {hue = 180, sat = .2, lum = .7};
           65  +			};
           66  +		};
           67  +		tints = {'suit_plate', 'suit_lining'};
           68  +		temp = {
           69  +			desc = facDesc('survival','cc');
           70  +			maxHeat = 0.7; -- can produce a half-degree Δ per second
           71  +			maxCool = 0.5;
           72  +			heatPower = 50; -- 50W
           73  +			coolPower = 50/t.efficiency;
           74  +			insulation = 0.5; -- prevent half of heat loss
           75  +		};
           76  +		protection = {
           77  +			rad = 0.7; -- blocks 70% of ionizing radiation
           78  +		};
           79  +		slots = {
           80  +			canisters = 1;
           81  +			batteries = math.ceil(math.max(1, t.power/2));
           82  +			chips = 3;
           83  +			guns = 0;
           84  +			ammo = 0;
           85  +		};
           86  +		nano = {
           87  +			compileSpeed = 0.1 * t.efficiency;
           88  +			shredSpeed = 0.1 * t.power;
           89  +			fabSizeLimit = 0.6; -- 60cm
           90  +		};
           91  +	})
           92  +
           93  +	starlit.item.suit.link('starlit_suit:suit_engineer_' .. tid, {
           94  +		name = t.name .. ' Engineer Suit';
           95  +		desc = facDesc('engineer','suit');
           96  +		tex = {
           97  +			plate = {
           98  +				id = 'starlit-suit-survival-plate';
           99  +				tint = lib.color {hue = 0, sat = .5, lum = .7};
          100  +			};
          101  +		};
          102  +		tints = {'suit_plate', 'suit_lining'};
          103  +		fab = fabReq(.8, 7) + fab { };
          104  +		temp = {
          105  +			desc = facDesc('engineer','cc');
          106  +			maxHeat = 0;
          107  +			maxCool = 0.2;
          108  +			heatPower = 0;
          109  +			coolPower = 10 / t.efficiency;
          110  +			insulation = 0.1; -- no lining
          111  +		};
          112  +		slots = {
          113  +			canisters = 2;
          114  +			batteries = 2;
          115  +			chips = 6;
          116  +			guns = 0;
          117  +			ammo = 0;
          118  +		};
          119  +		compat = {
          120  +			maxBatterySize = 0.10 * t.power; -- 10cm
          121  +		};
          122  +		protection = {
          123  +			rad = 0.1; -- blocks 10% of ionizing radiation
          124  +		};
          125  +		nano = {
          126  +			compileSpeed = 1 * t.efficiency;
          127  +			shredSpeed = 0.7 * t.power;
          128  +			fabSizeLimit = 1.5; -- 1.5m (enables node compilation)
          129  +		};
          130  +	})
          131  +
          132  +	if hasTech 'suitCombat' then
          133  +		starlit.item.suit.link('starlit_suit:suit_combat_' .. tid, {
          134  +			name = t.name .. ' Combat Suit';
          135  +			desc = facDesc('combat','suit');
          136  +			fab = fabReq(1.5, 14) + fab {
          137  +				metal = {iridium = 1e3};
          138  +			};
          139  +			tex = {
          140  +				plate = {
          141  +					id = 'starlit-suit-survival-plate';
          142  +					tint = lib.color {hue = 0, sat = 0, lum = 0};
          143  +				};
          144  +				lining = {
          145  +					id = 'starlit-suit-survival-lining';
          146  +					tint = lib.color {hue = 180, sat = .5, lum = .3};
          147  +				};
          148  +			};
          149  +			tints = {'suit_plate', 'suit_lining'};
          150  +			slots = {
          151  +				canisters = 1;
          152  +				batteries = math.ceil(math.max(3, 8*(t.power/2)));
          153  +				chips = 5;
          154  +				guns = 2;
          155  +				ammo = 1;
          156  +			};
          157  +			compat = {
          158  +				maxBatterySize = 0.10 * t.power; -- 10cm
          159  +			};
          160  +			temp = {
          161  +				desc = facDesc('combat','cc');
          162  +				maxHeat = 0.3; 
          163  +				maxCool = 0.6;
          164  +				heatPower = 20 / t.efficiency; 
          165  +				coolPower = 40 / t.efficiency;
          166  +				insulation = 0.2; 
          167  +			};
          168  +			protection = {
          169  +				rad = 0.9; -- blocks 90% of ionizing radiation
          170  +			};
          171  +			nano = {
          172  +				compileSpeed = 0.05;
          173  +				shredSpeed = 2 * t.power;
          174  +				fabSizeLimit = 0.3; -- 30cm
          175  +			};
          176  +		})
          177  +	end
          178  +end)
          179  +
          180  +

Added mods/starlit-suit/mod.conf version [785651d288].

            1  +name = starlit_suit
            2  +title = starlit environment suit
            3  +description = defines the environment suits available in starlit
            4  +depends = starlit, starlit_electronics

Added mods/starlit/container.lua version [38768ee087].

            1  +-- a container item defines a 'container' structure listing its
            2  +-- inventories and their properties. a container object is created
            3  +-- in order to interact with a container
            4  +local lib = starlit.mod.lib
            5  +starlit.item.container = lib.class {
            6  +	__name = 'starlit:container';
            7  +	construct = function(stack, inv, def)
            8  +		local T,G = lib.marshal.t, lib.marshal.g
            9  +		local cdef = stack:get_definition()._starlit.container;
           10  +		local sd = {}
           11  +		for k,v in pairs(cdef.list) do
           12  +			sd[k] = {
           13  +				key  = v.key;
           14  +				type = T.inventoryList;
           15  +			}
           16  +		end
           17  +		return {
           18  +			stack = stack, inv = inv, pdef = def, cdef = cdef;
           19  +			store = lib.marshal.metaStore(sd)(stack);
           20  +		}
           21  +	end;
           22  +	__index = {
           23  +		slot = function(self, id)
           24  +			return string.format("%s_%s", self.pdef.pfx, id)
           25  +		end;
           26  +		clear = function(self) -- initialize or empty the metadata
           27  +			self:update(function()
           28  +				for k,v in pairs(self.cdef.list) do
           29  +					if v.sz > 0 then
           30  +						self.store.write(k, {})
           31  +					end
           32  +				end
           33  +			end)
           34  +		end;
           35  +		list = function(self, k) return self.store.read(k) end;
           36  +		read = function(self)
           37  +			local lst = {}
           38  +			for k,v in pairs(self.cdef.list) do
           39  +				if v.sz > 0 then lst[k] = self:list(k) end
           40  +			end
           41  +			return lst
           42  +		end;
           43  +		pull = function(self) -- align the inventories with the metadata
           44  +			for k,v in pairs(self.cdef.list) do
           45  +				if v.sz > 0 then
           46  +					local stacks = self:list(k)
           47  +					local sid    = self:slot(k)
           48  +					self.inv:set_size(sid, v.sz)
           49  +					self.inv:set_list(sid, stacks)
           50  +				end
           51  +			end
           52  +		end;
           53  +		update = function(self, fn)
           54  +			local old = ItemStack(self.stack)
           55  +			if fn then fn() end
           56  +			if self.cdef.handle then
           57  +				self.cdef.handle(self.stack, old)
           58  +			end
           59  +		end;
           60  +		push = function(self) -- align the metadata with the inventories
           61  +			self:update(function()
           62  +				for k,v in pairs(self.cdef.list) do
           63  +					if v.sz > 0 then
           64  +						local sid = self:slot(k)
           65  +						local lst = self.inv:get_list(sid)
           66  +						self.store.write(k, lst)
           67  +					end
           68  +				end
           69  +			end)
           70  +		end;
           71  +		drop = function(self) -- remove the inventories from the node/entity
           72  +			for k,v in pairs(self.cdef.list) do
           73  +				local sid = self:slot(k)
           74  +				self.inv:set_size(sid, 0)
           75  +			end
           76  +		end;
           77  +		slotAccepts = function(self, lst, slot, stack)
           78  +		end;
           79  +	};
           80  +}
           81  +
           82  +function starlit.item.container.dropPrefix(inv, pfx)
           83  +	local lists = inv:get_lists()
           84  +	for k,v in pairs(lists) do
           85  +		if #k > #pfx then
           86  +			if string.sub(k, 1, #pfx + 1) == pfx .. '_' then
           87  +				inv:set_size(k, 0)
           88  +			end
           89  +		end
           90  +	end
           91  +end

Added mods/starlit/effect.lua version [eed7790b6a].

            1  +-- ported from sorcery/spell.lua, hence the lingering refs to "magic"
            2  +--
            3  +-- this file is used to track active effects, for the purposes of metamagic
            4  +-- like disjunction. a "effect" is a table consisting of several properties:
            5  +-- a "disjoin" function that, if present, is called when the effect is
            6  +-- abnormally interrupted, a "terminate" function that calls when the effect
            7  +-- completes, a "duration" property specifying how long the effect lasts in
            8  +-- seconds, and a "timeline" table that maps floats to functions called at 
            9  +-- specific points during the function's activity. it can also have a
           10  +-- 'delay' property that specifies how long to wait until the effect sequence
           11  +-- starts; the effect is however still vulnerable to disjunction during this
           12  +-- period. there can also be a sounds table that maps timepoints to sounds
           13  +-- the same way timeline does. each value should be a table of form {sound,
           14  +-- where}. the `where` field may contain one of 'pos', 'caster', 'subjects', or
           15  +-- a vector specifying a position in the world, and indicate where the sound
           16  +-- should be played. by default 'caster' and 'subjects' sounds will be attached
           17  +-- to the objects they reference; 'attach=false' can be added to prevent this.
           18  +-- by default sounds will be faded out quickly when disjunction occurs; this
           19  +-- can be controlled by the fade parameter.
           20  +--
           21  +-- effects can have various other properties, for instance 'disjunction', which
           22  +-- when true prevents other effects from being cast in its radius while it is
           23  +-- still in effect. disjunction is absolute; there is no way to overwhelm it.
           24  +--
           25  +-- the effect also needs at least one of "anchor", "subjects", or "caster".
           26  +--  * an anchor is a position that, in combination with 'range', specifies the area
           27  +--    where a effect is in effect; this is used for determining whether it
           28  +--    is affected by a disjunction that incorporates part of that position
           29  +--  * subjects is an array of individuals affected by the effect. when
           30  +--    disjunction is cast on one of them, they will be removed from the
           31  +--    table. each entry should have at least a 'player' field; they can
           32  +--    also contain any other data useful to the effect. if a subject has
           33  +--    a 'disjoin' field it must be a function called when they are removed
           34  +--    from the list of effect targets.
           35  +--  * caster is the individual who cast the effect, if any. a disjunction
           36  +--    against their person will totally disrupt the effect.
           37  +local log = starlit.logger 'effect'
           38  +local lib = starlit.mod.lib
           39  +
           40  +-- FIXME saving object refs is iffy, find a better alternative
           41  +starlit.effect = {
           42  +	active = {}
           43  +}
           44  +
           45  +local get_effect_positions = function(effect)
           46  +	local effectpos
           47  +	if effect.anchor then
           48  +		effectpos = {effect.anchor}
           49  +	elseif effect.attach then
           50  +		if effect.attach == 'caster' then
           51  +			effectpos = {effect.caster:get_pos()}
           52  +		elseif effect.attach == 'subjects' or effect.attach == 'both' then
           53  +			if effect.attach == 'both' then
           54  +				effectpos = {effect.caster:get_pos()}
           55  +			else effectpos = {} end
           56  +			for _,s in pairs(effect.subjects) do
           57  +				effectpos[#effectpos+1] = s.player:get_pos()
           58  +			end
           59  +		else effectpos = {effect.attach:get_pos()} end
           60  +	else assert(false) end
           61  +	return effectpos
           62  +end
           63  +
           64  +local ineffectrange = function(effect,pos,range)
           65  +	local effectpos = get_effect_positions(effect)
           66  +
           67  +	for _,p in pairs(effectpos) do
           68  +		if vector.equals(pos,p) or
           69  +			(range       and lib.math.vdcomp(range,      pos,p)<=1) or
           70  +			(effect.range and lib.math.vdcomp(effect.range,p,pos)<=1) then
           71  +			return true
           72  +		end
           73  +	end
           74  +	return false
           75  +end
           76  +
           77  +starlit.effect.probe = function(pos,range)
           78  +	-- this should be called before any effects are performed.
           79  +	-- other mods can overlay their own functions to e.g. protect areas
           80  +	-- from effects
           81  +	local result = {}
           82  +
           83  +	-- first we need to check if any active injunctions are in effect
           84  +	-- injunctions are registered as effects with a 'disjunction = true'
           85  +	-- property
           86  +	for id,effect in pairs(starlit.effect.active) do
           87  +		if not (effect.disjunction and (effect.anchor or effect.attach)) then goto skip end
           88  +		if ineffectrange(effect,pos,range) then
           89  +			result.disjunction = true
           90  +			break
           91  +		end
           92  +	::skip::end
           93  +	
           94  +	-- at some point we might also check to see if certain anti-effect
           95  +	-- blocks are nearby or suchlike. there could also be regions where
           96  +	-- perhaps certain kinds of effect are unusually empowered or weak
           97  +	return result
           98  +end
           99  +starlit.effect.disjoin = function(d)
          100  +	local effects,targets = {},{}
          101  +	if d.effect then effects = {{v=d.effect}}
          102  +	elseif d.target then targets = {d.target}
          103  +	elseif d.pos then -- find effects anchored here and people in range
          104  +		for id,effect in pairs(starlit.effect.active) do
          105  +			if not effect.anchor then goto skip end -- this intentionally excludes attached effects
          106  +			if ineffectrange(effect,d.pos,d.range) then
          107  +				effects[#effects+1] = {v=effect,i=id}
          108  +			end
          109  +		::skip::end
          110  +		local ppl = minetest.get_objects_inside_radius(d.pos,d.range)
          111  +		if #targets == 0 then targets = ppl else
          112  +			for _,p in pairs(ppl) do targets[#targets+1] = p end
          113  +		end
          114  +	end
          115  +
          116  +	-- iterate over targets to remove from any effect's influence
          117  +	for _,t in pairs(targets) do
          118  +		for id,effect in pairs(starlit.effect.active) do
          119  +			if effect.caster == t then effects[#effects+1] = {v=effect,i=id} else
          120  +				for si, sub in pairs(effect.subjects) do
          121  +					if sub.player == t then
          122  +						if sub.disjoin then sub:disjoin(effect) end
          123  +						effect.release_subject(si)
          124  +						break
          125  +					end
          126  +				end
          127  +			end
          128  +		end
          129  +	end
          130  +
          131  +	-- effects to disjoin entirely
          132  +	for _,s in pairs(effects) do local effect = s.v
          133  +		if effect.disjoin then effect:disjoin() end
          134  +		effect.abort()
          135  +		if s.i then starlit.effect.active[s.i] = nil else
          136  +			for k,v in pairs(starlit.effect.active) do
          137  +				if v == effect then starlit.effect.active[k] = nil break end
          138  +			end
          139  +		end
          140  +	end
          141  +end
          142  +
          143  +starlit.effect.ensorcelled = function(player,effect)
          144  +	if type(player) == 'string' then player = minetest.get_player_by_name(player) end
          145  +	for _,s in pairs(starlit.effect.active) do
          146  +		if effect and (s.name ~= effect) then goto skip end
          147  +		for _,sub in pairs(s.subjects) do
          148  +			if sub.player == player then return s end
          149  +		end
          150  +	::skip::end
          151  +	return false
          152  +end
          153  +
          154  +starlit.effect.each = function(player,effect)
          155  +	local idx = 0
          156  +	return function()
          157  +		repeat idx = idx + 1
          158  +			local sp = starlit.effect.active[idx]
          159  +			if sp == nil then return nil end
          160  +			if effect == nil or sp.name == effect then
          161  +				for _,sub in pairs(sp.subjects) do
          162  +					if sub.player == player then return sp end
          163  +				end
          164  +			end
          165  +		until idx >= #starlit.effect.active
          166  +	end
          167  +end
          168  +
          169  +-- when a new effect is created, we analyze it and make the appropriate calls
          170  +-- to minetest.after to queue up the events. each job returned needs to be
          171  +-- saved in 'jobs' so they can be canceled if the effect is disjoined. no polling
          172  +-- necessary :D
          173  +
          174  +starlit.effect.cast = function(proto)
          175  +	local s = table.copy(proto)
          176  +	s.jobs = s.jobs or {} s.vfx = s.vfx or {} s.sfx = s.sfx or {}
          177  +	s.impacts = s.impacts or {} s.subjects = s.subjects or {}
          178  +	s.delay = s.delay or 0
          179  +	s.visual = function(subj, def)
          180  +		s.vfx[#s.vfx + 1] = {
          181  +			handle = minetest.add_particlespawner(def);
          182  +			subject = subj;
          183  +		}
          184  +	end
          185  +	s.visual_caster = function(def) -- convenience function
          186  +		local d = table.copy(def)
          187  +		d.attached = s.caster
          188  +		s.visual(nil, d)
          189  +	end
          190  +	s.visual_subjects = function(def)
          191  +		for _,sub in pairs(s.subjects) do
          192  +			local d = table.copy(def)
          193  +			d.attached = sub.player
          194  +			s.visual(sub, d)
          195  +		end
          196  +	end
          197  +	s.affect = function(i)
          198  +		local etbl = {}
          199  +		for _,sub in pairs(s.subjects) do
          200  +			-- local eff = late.new_effect(sub.player, i)
          201  +			-- starlit will not be using late
          202  +			local rec = {
          203  +				effect = eff;
          204  +				subject = sub;
          205  +			}
          206  +			s.impacts[#s.impacts+1] = rec
          207  +			etbl[#etbl+1] = rec
          208  +		end
          209  +		return etbl
          210  +	end
          211  +	s.abort = function()
          212  +		for _,j in ipairs(s.jobs) do j:cancel() end
          213  +		for _,v in ipairs(s.vfx) do minetest.delete_particlespawner(v.handle) end
          214  +		for _,i in ipairs(s.sfx) do s.silence(i) end
          215  +		for _,i in ipairs(s.impacts) do i.effect:stop() end
          216  +	end
          217  +	s.release_subject = function(si)
          218  +		local t = s.subjects[si]
          219  +		for _,f in pairs(s.sfx)     do if f.subject == t then s.silence(f) end end
          220  +		for _,f in pairs(s.impacts) do if f.subject == t then f.effect:stop() end end
          221  +		for _,f in pairs(s.vfx) do
          222  +			if f.subject == t then minetest.delete_particlespawner(f.handle) end
          223  +		end
          224  +		s.subjects[si] = nil
          225  +	end
          226  +	local interpret_timespec = function(when)
          227  +		if when == nil then return 0 end
          228  +		local t if type(when) == 'number' then
          229  +			t = s.duration * when
          230  +		else
          231  +			t = (s.duration * (when.whence or 0)) + (when.secs or 0)
          232  +		end
          233  +		if t then return math.min(s.duration,math.max(0,t)) end
          234  +
          235  +		log.err('invalid timespec ' .. dump(when))
          236  +		return 0
          237  +	end
          238  +	s.queue = function(when,fn)
          239  +		local elapsed = s.starttime and minetest.get_server_uptime() - s.starttime or 0
          240  +		local timepast = interpret_timespec(when)
          241  +		if not timepast then timepast = 0 end
          242  +		local timeleft = s.duration - timepast
          243  +		local howlong = (s.delay + timepast) - elapsed
          244  +		if howlong < 0 then
          245  +			log.err('cannot time-travel! queue() called with `when` specifying timepoint that has already passed')
          246  +			howlong = 0
          247  +		end
          248  +		s.jobs[#s.jobs+1] = minetest.after(howlong, function()
          249  +			-- this is somewhat awkward. since we're using a non-polling approach, we
          250  +			-- need to find a way to account for a caster or subject walking into an
          251  +			-- existing antimagic field, or someone with an existing antimagic aura
          252  +			-- walking into range of the anchor. so every time a effect effect would
          253  +			-- take place, we first check to see if it's in range of something nasty
          254  +			if not s.disjunction and -- avoid self-disjunction
          255  +				((s.caster and starlit.effect.probe(s.caster:get_pos()).disjunction) or
          256  +				 (s.anchor and starlit.effect.probe(s.anchor,s.range).disjunction)) then
          257  +				starlit.effect.disjoin{effect=s}
          258  +			else
          259  +				if not s.disjunction then for _,sub in pairs(s.subjects) do
          260  +					local sp = sub.player:get_pos()
          261  +					if starlit.effect.probe(sp).disjunction then
          262  +						starlit.effect.disjoin{pos=sp}
          263  +					end
          264  +				end end
          265  +				-- effect still exists and we've removed any subjects who have been
          266  +				-- affected by a disjunction effect, it's now time to actually perform
          267  +				-- the queued-up action
          268  +				fn(s,timepast,timeleft)
          269  +			end
          270  +		end)
          271  +	end
          272  +	s.play_now = function(spec)
          273  +		local specs, stbl = {}, {}
          274  +		local addobj = function(obj,sub)
          275  +			if spec.attach == false then specs[#specs+1] = {
          276  +				spec = { pos = obj:get_pos() };
          277  +				obj = obj, subject = sub;
          278  +			} else specs[#specs+1] = {
          279  +				spec = { object = obj };
          280  +				obj = obj, subject = sub;
          281  +			} end
          282  +		end
          283  +
          284  +		if spec.where == 'caster' then addobj(s.caster)
          285  +		elseif spec.where == 'subjects' then
          286  +			for _,sub in pairs(s.subjects) do addobj(sub.player,sub) end
          287  +		elseif spec.where == 'pos' then specs[#specs+1] = { spec = {pos = s.anchor} }
          288  +		else specs[#specs+1] = { spec = {pos = spec.where} } end
          289  +
          290  +		for _,sp in pairs(specs) do
          291  +			sp.spec.gain = sp.spec.gain or spec.gain
          292  +			local so = {
          293  +				handle = minetest.sound_play(spec.sound, sp.spec, spec.ephemeral);
          294  +				ctl = spec;
          295  +				-- object = sp.obj;
          296  +				subject = sp.subject;
          297  +			}
          298  +			stbl[#stbl+1] = so
          299  +			s.sfx[#s.sfx+1] = so
          300  +		end
          301  +		return stbl
          302  +	end
          303  +	s.play = function(when,spec)
          304  +		s.queue(when, function()
          305  +			local snds = s.play_now(spec)
          306  +			if spec.stop then
          307  +				s.queue(spec.stop, function()
          308  +					for _,snd in pairs(snds) do s.silence(snd) end
          309  +				end)
          310  +			end
          311  +		end)
          312  +	end
          313  +	s.silence = function(sound)
          314  +		if sound.ctl.fade == 0 then minetest.sound_stop(sound.handle)
          315  +		else minetest.sound_fade(sound.handle,sound.ctl.fade or 1,0) end
          316  +	end
          317  +	local startqueued, termqueued = false, false
          318  +	local myid = #starlit.effect.active+1
          319  +	s.cancel = function()
          320  +		s.abort()
          321  +		starlit.effect.active[myid] = nil
          322  +	end
          323  +	local perform_disjunction_calls = function()
          324  +		local positions = get_effect_positions(s)
          325  +		for _,p in pairs(positions) do
          326  +			starlit.effect.disjoin{pos = p, range = s.range}
          327  +		end
          328  +	end
          329  +	if s.timeline then
          330  +		for when_raw,what in pairs(s.timeline) do
          331  +			local when = interpret_timespec(when_raw)
          332  +			if s.delay == 0 and when == 0 then
          333  +				startqueued = true
          334  +				if s.disjunction then perform_disjunction_calls() end
          335  +				what(s,0,s.duration)
          336  +			elseif when_raw == 1 or when >= s.duration then -- avoid race conditions
          337  +				if not termqueued then
          338  +					termqueued = true
          339  +					s.queue(1,function(s,...)
          340  +						what(s,...)
          341  +						if s.terminate then s:terminate() end
          342  +						starlit.effect.active[myid] = nil
          343  +					end)
          344  +				else
          345  +					log.warn('multiple final timeline events not possible, ignoring')
          346  +				end
          347  +			elseif when == 0 and s.disjunction then
          348  +				startqueued = true
          349  +				s.queue(when_raw,function(...)
          350  +					perform_disjunction_calls()
          351  +					what(...)
          352  +				end)
          353  +			else s.queue(when_raw,what) end
          354  +		end
          355  +	end
          356  +	if s.intervals then
          357  +		for _,int in pairs(s.intervals) do
          358  +			local timeleft = s.duration - interpret_timespec(int.after)
          359  +			local iteration, itercount = 0, timeleft / int.period
          360  +			local function iterate(lastreturn)
          361  +				iteration = iteration + 1
          362  +				local nr = int.fn {
          363  +					effect = s;
          364  +					iteration = iteration;
          365  +					iterationcount = itercount;
          366  +					timeleft = timeleft;
          367  +					timeelapsed = s.duration - timeleft;
          368  +					lastreturn = lastreturn;
          369  +				}
          370  +				if nr ~= false and iteration < itercount then
          371  +					s.jobs[#s.jobs+1] = minetest.after(int.period,
          372  +						function() iterate(nr) end)
          373  +				end
          374  +			end
          375  +			if int.after
          376  +				then s.queue(int.after, iterate)
          377  +				else s.queue({whence=0, secs=s.period}, iterate)
          378  +			end
          379  +		end
          380  +	end
          381  +	if s.disjunction and not startqueued then
          382  +		if s.delay == 0 then perform_disjunction_calls() else
          383  +			s.queue(0, function() perform_disjunction_calls() end)
          384  +		end
          385  +	end
          386  +	if s.sounds then
          387  +		for when,what in pairs(s.sounds) do s.play(when,what) end
          388  +	end
          389  +	starlit.effect.active[myid] = s
          390  +	if not termqueued then
          391  +		s.jobs[#s.jobs+1] = minetest.after(s.delay + s.duration, function()
          392  +			if s.terminate then s:terminate() end
          393  +			starlit.effect.active[myid] = nil
          394  +		end)
          395  +	end
          396  +	s.starttime = minetest.get_server_uptime()
          397  +	return s
          398  +end
          399  +
          400  +minetest.register_on_dieplayer(function(player)
          401  +	starlit.effect.disjoin{target=player}
          402  +end)

Added mods/starlit/element.lua version [db8d1e2ad1].

            1  +local lib = starlit.mod.lib
            2  +local W = starlit.world
            3  +local M = W.material
            4  +
            5  +M.element.foreach('starlit:sort', {}, function(id, m)
            6  +	if m.metal then
            7  +		M.metal.link(id, {
            8  +			name = m.name;
            9  +			composition = starlit.type.fab{element = {[id] = 1}};
           10  +			color = m.color;
           11  +			-- n.b. this is a RATIO: it will be appropriately multiplied
           12  +			-- for the object in question; e.g a normal chunk will be
           13  +			-- 100 $element, an ingot will be 1000 $element
           14  +		})
           15  +	elseif m.gas then
           16  +		M.gas.link(id, {
           17  +			name = m.name;
           18  +			composition = starlit.type.fab{element = {[id] = 1}};
           19  +		})
           20  +	elseif m.liquid then
           21  +		M.liquid.link(id, {
           22  +			name = m.name;
           23  +			composition = starlit.type.fab{element = {[id] = 1}};
           24  +		})
           25  +	end
           26  +end)
           27  +
           28  +local F = string.format
           29  +
           30  +local function mkEltIndicator(composition)
           31  +	local indicator = ''
           32  +	local idx = 0
           33  +	local ccount = 0
           34  +	for _ in pairs(composition) do
           35  +		ccount = ccount + 1
           36  +	end
           37  +	local indsz,indpad = 28,4
           38  +	local ofs = math.min(11, (indsz-indpad)/ccount)
           39  +	for id, amt in pairs(composition) do
           40  +		idx = idx + 1
           41  +		indicator = indicator .. F(
           42  +			':%s,3=starlit-element-%s.png',
           43  +			(indsz-indpad) - (idx*ofs), id
           44  +		)
           45  +	end
           46  +	indicator = lib.image(indicator)
           47  +	return function(s)
           48  +		return string.format('(%s^[resize:%sx%s)^[combine:%sx%s%s',
           49  +			s,
           50  +			indsz, indsz,
           51  +			indsz, indsz,
           52  +			indicator);
           53  +	end
           54  +end
           55  +
           56  +M.element.foreach('starlit:gen-forms', {}, function(id, m)
           57  +	local eltID = F('%s:element_%s', minetest.get_current_modname(), id)
           58  +	local eltName = F('Elemental %s', lib.str.capitalize(m.name))
           59  +	local tt = function(t, d, g)
           60  +		return starlit.ui.tooltip {
           61  +			title = t, desc = d;
           62  +			color = lib.color(0.1,0.2,0.1);
           63  +			props = {
           64  +				{title = 'Mass', desc = lib.math.si('g', g), affinity='info'}
           65  +			}
           66  +		}
           67  +	end
           68  +	local comp = {[id] = 1}
           69  +	local iblit = mkEltIndicator(comp)
           70  +	m.form = m.form or {}
           71  +	m.form.element = eltID
           72  +
           73  +	local powder = F('starlit-element-%s-powder.png', id);
           74  +	minetest.register_craftitem(eltID, {
           75  +		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);
           78  +		wield_image = powder;
           79  +		stack_max = 1000; -- 1kg
           80  +		groups = {element = 1, powder = 1, specialInventory = 1};
           81  +		_starlit = {
           82  +			mass = 1;
           83  +			material = {
           84  +				kind = 'element';
           85  +				element = id;
           86  +			};
           87  +			fab = starlit.type.fab {
           88  +				element = comp;
           89  +			};
           90  +		};
           91  +	});
           92  +end)
           93  +
           94  +
           95  +M.metal.foreach('starlit:gen-forms', {}, function(id, m)
           96  +	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))
          101  +	m.form = m.form or {}
          102  +	m.form.brick = brickID
          103  +	m.form.ingot = ingotID
          104  +	local tt = function(t, d, g)
          105  +		return starlit.ui.tooltip {
          106  +			title = t, desc = d;
          107  +			color = lib.color(0.1,0.1,0.1);
          108  +			props = {
          109  +				{title = 'Mass', desc = lib.math.si('g', g), affinity='info'}
          110  +			}
          111  +		}
          112  +	end
          113  +	local mcomp = m.composition:elementalize().element
          114  +	local function comp(n)
          115  +		local t = {}
          116  +		for id, amt in pairs(mcomp) do
          117  +			t[id] = amt * n
          118  +		end
          119  +		return t
          120  +	end
          121  +	local iblit = mkEltIndicator(mcomp)
          122  +	local function img(s)
          123  +		return iblit(s:colorize(m.color):render())
          124  +	end
          125  +
          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  +	minetest.register_craftitem(ingotID, {
          147  +		short_description = ingotName;
          148  +		description = tt(ingotName, F('A solid ingot of %s, ready to be worked by a large matter compiler', m.name), 1e3);
          149  +		inventory_image = img(lib.image('starlit-item-ingot.png'));
          150  +		wield_image = lib.image 'starlit-item-ingot.png':colorize(m.color):render();
          151  +		groups = {metal = 1, ingot = 1};
          152  +		stack_max = 5;
          153  +		_starlit = {
          154  +			mass = 1e3;
          155  +			material = {
          156  +				kind = 'metal';
          157  +				metal = id;
          158  +			};
          159  +			fab = starlit.type.fab {
          160  +				flag = {smelt= true};
          161  +				element = comp(1e3);
          162  +			};
          163  +		};
          164  +	});
          165  +
          166  +
          167  +end)
          168  +
          169  +local function canisterDesc(stack, def)
          170  +	def = def or stack:get_definition()._starlit.canister
          171  +	local props = {
          172  +		{title = 'Charge Slots', affinity = 'info', desc = tostring(def.slots)};
          173  +	};
          174  +	if stack then
          175  +		local inv = starlit.item.container(stack)
          176  +		for i,e in ipairs(inv:list 'elem') do
          177  +			local comp = e:get_definition()._starlit.fab
          178  +			table.insert(props, {
          179  +				title = comp:formula();
          180  +				desc = lib.math.si('g', e:get_count());
          181  +				affinity = 'good';
          182  +			})
          183  +		end
          184  +		-- TODO list masses
          185  +	end
          186  +	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';
          188  +		color = lib.color(0.2,0.1,0.1);
          189  +		props = props;
          190  +	};	
          191  +end
          192  +
          193  +starlit.item.canister = lib.registry.mk 'starlit:canister';
          194  +starlit.item.canister.foreach('starlit:item-gen', {}, function(id, c)
          195  +	minetest.register_craftitem(id, {
          196  +		short_description = c.name;
          197  +		description = canisterDesc(nil, c);
          198  +		inventory_image = c.image or 'starlit-item-element-canister.png';
          199  +		groups = {canister = 1};
          200  +		stack_max = 1;
          201  +		_starlit = {
          202  +			canister = c;
          203  +			container = {
          204  +				handle = function(stack, oldstack)
          205  +					stack:get_meta():set_string('description', canisterDesc(stack))
          206  +					return stack
          207  +				end;
          208  +				list = {
          209  +					elem = {
          210  +						key = 'starlit:canister_elem';
          211  +						accept = 'powder';
          212  +						sz = c.slots;
          213  +					};
          214  +				};
          215  +			};
          216  +		};
          217  +	})
          218  +end)

Added mods/starlit/fab.lua version [9968a2e2d9].

            1  +-- [ʞ] fab.lua
            2  +--  ~ lexi hale <lexi@hale.su>
            3  +--  🄯 EUPL1.2
            4  +--  ? fabrication spec class
            5  +--    a type.fab supports two operators:
            6  +--
            7  +--    + used for compounding recipes. that is,
            8  +--			a+b = compose a new spec from the spec parts a and b.
            9  +--      this is used e.g. for creating tier-based
           10  +--      fabspecs.
           11  +--
           12  +--    * used for determining quantities. that is,
           13  +--			f*x = spec to make x instances of f
           14  +--
           15  +--    new fab fields must be defined in starlit.type.fab.opClass.
           16  +--    this maps a name to fn(a,b,n) -> quant, where a is the first
           17  +--    argument, b is a compounding amount, and n is a quantity of
           18  +--    items to produce. fields that are unnamed will be underwritten
           19  +
           20  +local function fQuant(a,b,n) return ((a or 0)+(b or 0))*n end
           21  +local function fFac  (a,b,n)
           22  +	if a == nil and b == nil then return nil end
           23  +	local f if a == nil or b == nil then
           24  +		f = a or b
           25  +	else
           26  +		f = (a or 1)*(b or 1)
           27  +	end
           28  +	return f*n
           29  +end
           30  +local function fReq  (a,b,n) return a or b         end
           31  +local function fFlag (a,b,n) return a and b        end
           32  +local function fSize (a,b,n) return math.max(a,b)  end
           33  +local opClass = {
           34  +	-- fabrication eligibility will be determined by which kinds
           35  +	-- of input a particular fabricator can introduce. e.g. a
           36  +	-- printer with a  but no cache can only print items whose
           37  +	-- recipe only names elements as ingredients
           38  +
           39  +	-- ingredients
           40  +	element    = fQuant; -- (g)
           41  +	gas        = fQuant; -- ()
           42  +	liquid     = fQuant; -- (l)
           43  +	crystal    = fQuant; -- (g)
           44  +	item       = fQuant; -- n
           45  +	metal      = fQuant; -- (g)
           46  +	metalIngot = fQuant; -- (g)
           47  +	-- factors
           48  +	cost = fFac; -- units vary
           49  +	time = fFac; -- (s)
           50  +		-- print: base printing time
           51  +	size = fSize;
           52  +		-- printBay: size of the printer bay necessary to produce the item
           53  +	req  = fReq;
           54  +	flag = fFlag; -- means that can be used to produce the item & misc flags
           55  +		-- print: allow production with a printer
           56  +		-- smelt: allow production with a smelter
           57  +	-- all else defaults to underwrite
           58  +}
           59  +
           60  +local F = string.format
           61  +local strClass = {
           62  +	element = function(x, n)
           63  +		local el = starlit.world.material.element[x]
           64  +		return lib.math.si('g', n) .. ' ' .. (el.sym or el.name)
           65  +	end;
           66  +	metal = function(x, n)
           67  +		local met = starlit.world.material.metal[x]
           68  +		return lib.math.si('g', n) .. ' ' .. met.name
           69  +	end;
           70  +	liquid = function(x, n)
           71  +		local liq = starlit.world.material.liquid[x]
           72  +		return lib.math.si('L', n) .. ' ' .. liq.name
           73  +	end;
           74  +	gas = function(x, n)
           75  +		local gas = starlit.world.material.gas[x]
           76  +		return lib.math.si('g', n) .. ' ' .. gas.name
           77  +	end;
           78  +	item = function(x, n)
           79  +		local i = minetest.registered_items[x]
           80  +		return tostring(n) .. 'x ' .. i.short_description
           81  +	end;
           82  +}
           83  +
           84  +local order = {
           85  +	'element', 'metal', 'liquid', 'gas', 'item'
           86  +}
           87  +
           88  +local lib = starlit.mod.lib
           89  +local fab fab = lib.class {
           90  +	__name = 'starlit:fab';
           91  +	
           92  +	opClass = opClass;
           93  +	strClass = strClass;
           94  +	order = order;
           95  +	construct = function(q) return q end;
           96  +	__index = {
           97  +		elementalize = function(self)
           98  +			local e = fab {element = self.element or {}}
           99  +			for _, kind in pairs {'metal', 'gas', 'liquid'} do
          100  +				for m,mass in pairs(self[kind] or {}) do
          101  +					local mc = starlit.world.material[kind][m].composition
          102  +					e = e + mc:elementalize()*mass
          103  +				end
          104  +			end
          105  +			return e
          106  +		end;
          107  +
          108  +		elementSeq = function(self)
          109  +			local el = {}
          110  +			local em = self.element
          111  +			local s = 0
          112  +			local eldb = starlit.world.material.element.db
          113  +			for k in pairs(em) do table.insert(el, k) s=s+eldb[k].n end
          114  +			table.sort(el, function(a,b)
          115  +				return eldb[a].n > eldb[b].n
          116  +			end)
          117  +			return el, em, s
          118  +		end;
          119  +
          120  +		formula = function(self)
          121  +			print('make formula', dump(self))
          122  +			local ts,f=0
          123  +			if self.element then
          124  +				f = {}
          125  +				local el, em, s = self:elementSeq()
          126  +				local eldb = starlit.world.material.element.db
          127  +				for i, e in ipairs(el) do
          128  +					local sym, n = eldb[e].sym, em[e]
          129  +					if n > 0 then
          130  +						table.insert(f, string.format("%s%s",
          131  +							sym, n>1 and lib.str.nIdx(n) or ''))
          132  +					end
          133  +				end
          134  +				f = table.concat(f)
          135  +				ts = ts + s
          136  +			end
          137  +
          138  +			local sub = {}
          139  +			for _, w in pairs {'metal', 'gas', 'liquid'} do
          140  +				if self[w] then
          141  +					local mdb = starlit.world.material[w].db
          142  +					for k, amt in pairs(self[w]) do
          143  +						local mf, s = mdb[k].composition:formula()
          144  +						if amt > 0 then table.insert(sub, {
          145  +							f = string.format("(%s)%s",mf,
          146  +								lib.str.nIdx(amt));
          147  +							s = s;
          148  +						}) end
          149  +						ts = ts + s*amt
          150  +					end
          151  +				end
          152  +			end
          153  +			table.sort(sub, function(a,b) return a.s > b.s end)
          154  +			local fml = {}
          155  +			for i, v in ipairs(sub) do fml[i] = v.f end
          156  +			if f then table.insert(fml, f) end
          157  +			fml = table.concat(fml, ' + ')
          158  +
          159  +			return fml, ts
          160  +		end;
          161  +	};
          162  +
          163  +	__tostring = function(self)
          164  +		local t = {}
          165  +		for i,o in ipairs(order) do
          166  +			if self[o] then
          167  +				for mat,amt in pairs(self[o]) do
          168  +					if amt > 0 then
          169  +						table.insert(t, strClass[o](mat, amt))
          170  +					end
          171  +				end
          172  +			end
          173  +		end
          174  +		return table.concat(t, ", ")
          175  +	end;
          176  +
          177  +
          178  +	__add = function(a,b)
          179  +		local new = fab {}
          180  +		for cat, vals in pairs(a) do
          181  +			new[cat] = lib.tbl.copy(vals)
          182  +		end
          183  +		for cat, vals in pairs(b) do
          184  +			if not new[cat] then
          185  +				new[cat] = lib.tbl.copy(vals)
          186  +			else
          187  +				local f = opClass[cat]
          188  +				for k,v in pairs(vals) do
          189  +					local n = f(new[cat][k], v, 1)
          190  +					new[cat][k] = n > 0 and n or nil
          191  +				end
          192  +			end
          193  +		end
          194  +		return new
          195  +	end;
          196  +
          197  +	__mul = function(x,n)
          198  +		local new = fab {}
          199  +		for cat, vals in pairs(x) do
          200  +			new[cat] = {}
          201  +			local f = opClass[cat]
          202  +			for k,v in pairs(vals) do
          203  +				local num = f(v,nil,n)
          204  +				new[cat][k] = num > 0 and num or nil
          205  +			end
          206  +		end
          207  +		return new
          208  +	end;
          209  +
          210  +	__div = function(x,n)
          211  +		return x * (1/n)
          212  +	end;
          213  +}
          214  +
          215  +starlit.type.fab = fab

Added mods/starlit/fx/nano.lua version [85f63bf07b].

            1  +local lib = starlit.mod.lib
            2  +local E = starlit.effect
            3  +local N = {}
            4  +starlit.fx.nano = N
            5  +local nanopool= {
            6  +	{
            7  +		name = 'starlit-fx-nano-spark-small.png';
            8  +		scale_tween = {0,.5, style = 'pulse', rep = 3};
            9  +	};
           10  +	{
           11  +		name = 'starlit-fx-nano-spark-small.png';
           12  +		scale_tween = {0,1, style = 'pulse', rep = 2};
           13  +	};
           14  +	{
           15  +		name = 'starlit-fx-nano-spark-big.png';
           16  +		scale_tween = {0,1, style = 'pulse'};
           17  +	};
           18  +}
           19  +
           20  +function N.heal(user, targets, amt, dur)
           21  +	local amthealed = {}
           22  +	local f = E.cast {
           23  +		caster = user.entity;
           24  +		subjects = targets;
           25  +		duration = dur;
           26  +		intervals = {
           27  +			{
           28  +				after = 0;
           29  +				period = 4;
           30  +				fn = function(c)
           31  +					for i,v in pairs(c.effect.subjects) do
           32  +						local u = starlit.activeUsers[v.player:get_player_name()]
           33  +						if u then
           34  +							local heal = math.max(amt/4, 1)
           35  +							amthealed[u] = amthealed[u] or 0
           36  +							if amthealed[u] < amt then
           37  +								amthealed[u] = amthealed[u] + heal
           38  +								u:statDelta('health', heal)
           39  +							end
           40  +						end
           41  +					end
           42  +				end;
           43  +			}
           44  +		}
           45  +	}
           46  +
           47  +	local casterIsTarget = false
           48  +	for _, sub in pairs(f.subjects) do
           49  +		if sub.player == user.entity then
           50  +			casterIsTarget = true
           51  +		end
           52  +		f.visual(sub, {
           53  +			amount = 50;
           54  +			time = dur;
           55  +			glow = 14;
           56  +			jitter = 0.01;
           57  +			attached = user.entity;
           58  +			vel = { min = -0.1, max = 0.1; };
           59  +			pos = {
           60  +				min = vector.new(0,0.2,0);
           61  +				max = vector.new(0,1.2,0);
           62  +			};
           63  +			radius  = { min = 0.2; max = 0.6; bias = -1; };
           64  +			exptime = {min=0.5,max=2};
           65  +			attract = {
           66  +				kind = 'line';
           67  +				strength = {min = 0.5, max = 2};
           68  +				origin = 0;
           69  +				direction = vector.new(0,1,0);
           70  +				origin_attached = sub.player;
           71  +				direction_attached = sub.player;
           72  +			};
           73  +
           74  +			texpool = nanopool;
           75  +		})
           76  +	end
           77  +	if not casterIsTarget then
           78  +		-- f.visual_caster { }
           79  +	end
           80  +	f.play(0.3, {
           81  +		where = 'subjects';
           82  +		sound = 'starlit-nano-heal';
           83  +		ephemeral = true;
           84  +		spec = {gain = 0.3};
           85  +	})
           86  +
           87  +	return f
           88  +end
           89  +
           90  +function N.shred(user, pos, prop, time, node)
           91  +	local f = E.cast {
           92  +		caster = user.entity;
           93  +		subjects = {};
           94  +		duration = time;
           95  +	}
           96  +	local sp,sv = user:lookupSpecies()
           97  +	local eh = sv.eyeHeight or sp.eyeHeight
           98  +	f.visual_caster {
           99  +		amount = 200 * time;
          100  +		pos =  vector.new(0.12,eh - 0.1,0);
          101  +		radius = 0.2;
          102  +		time = time - (time/3);
          103  +		glow = 14;
          104  +		jitter = 0.1;
          105  +		size = {min = 0.2, max = 0.5};
          106  +		exptime = {min=0.5,max=1};
          107  +		vel_tween = {
          108  +			0;
          109  +			{ min = -0.4, max = 0.4; };
          110  +			style = 'pulse', rep = time * 2; 
          111  +		};
          112  +		attract = {
          113  +			kind = 'point';
          114  +			origin = pos;
          115  +			radius = 0.5;
          116  +			strength = {min=.3,max=2};
          117  +		};
          118  +		texpool = nanopool;
          119  +	};
          120  +	f.queue(0.05, function(s, timepast, timeleft)
          121  +		f.visual(nil, {
          122  +			amount = timeleft * 40;
          123  +			time = timeleft;
          124  +			pos = pos;
          125  +			size_tween = {
          126  +				0, {min = 0.5, max = 2};
          127  +			};
          128  +			vel = {
          129  +				min = vector.new(-1.2,0.5,-1.2);
          130  +				max = vector.new(1.2,3.5,1.2);
          131  +			};
          132  +			acc = vector.new(0,-starlit.world.planet.gravity,0);
          133  +			node = node;
          134  +		})
          135  +	end);
          136  +	f.queue(0.9, function(s, timepast, timeleft)
          137  +		f.visual(nil, {
          138  +			amount = 200;
          139  +			time = timeleft;
          140  +			pos = pos;
          141  +			size = {min = 0.1, max = 0.3};
          142  +			vel = {
          143  +				min = vector.new(-2,0.5,-2);
          144  +				max = vector.new(2,4,2);
          145  +			};
          146  +			acc = vector.new(0,-starlit.world.planet.gravity,0);
          147  +			node = node;
          148  +		})
          149  +	end);
          150  +	f.queue(0.3, function(s, timepast, timeleft)
          151  +		local function v(fn)
          152  +			local def = {
          153  +				amount = timeleft * 100;
          154  +				pos = pos;
          155  +				time = timeleft;
          156  +				radius = 0.5;
          157  +				jitter = {min = 0.0, max = 0.2};
          158  +				size = {min = 0.2, max = 0.5};
          159  +				exptime = {min = 0.5, max = 1};
          160  +				attract = {
          161  +					kind = 'point';
          162  +					strength = {min=0.3, max = 1};
          163  +					origin = vector.new(0,eh-0.1,0);
          164  +					radius = 0.5;
          165  +					origin_attached = user.entity;
          166  +				};
          167  +			}
          168  +			fn(def)
          169  +			f.visual(nil, def)
          170  +		end
          171  +		v(function(t) t.texpool = nanopool t.glow = 14 end)
          172  +		v(function(t)
          173  +			t.node = node
          174  +			t.amount = timeleft * 20
          175  +			t.size = {min = 0.1, max = 0.3};
          176  +		end)
          177  +	end)
          178  +	return f
          179  +
          180  +end

Added mods/starlit/init.lua version [1f09b99871].

            1  +-- [ʞ] starlit/init.lua
            2  +--  ~ lexi hale <lexi@hale.su>
            3  +--  ? basic setup, game rules, terrain
            4  +--  © EUPL v1.2
            5  +
            6  +local T = minetest.get_translator 'starlit'
            7  +
            8  +-- TODO enforce latest engine version
            9  +
           10  +local mod = {
           11  +	-- subordinate mods register here
           12  +	lib = vtlib;
           13  +		-- vtlib should be accessed as starlit.mod.lib by starlit modules for the sake of proper encapsulation. vtlib should simply be a provider, not a hardcoded dependency
           14  +}
           15  +local lib = mod.lib
           16  +
           17  +
           18  +starlit = {
           19  +	ident = minetest.get_current_modname();
           20  +	mod = mod;
           21  +	translator = T;
           22  +
           23  +	constant = {
           24  +		light = { --minetest units
           25  +			dim = 3;
           26  +			lamp = 7;
           27  +			bright = 10;
           28  +			brightest = 14; -- only sun and growlights
           29  +		};
           30  +		heat = { -- celsius
           31  +			freezing = 0;
           32  +			safe = 4;
           33  +			overheat = 32;
           34  +			boiling = 100;
           35  +		};
           36  +		rad = {
           37  +		};
           38  +	};
           39  +
           40  +	activeUsers = {
           41  +		-- map of username -> user object
           42  +	};
           43  +	activeUI = {
           44  +		-- map of username -> UI context
           45  +	};
           46  +	liveUI = {
           47  +		-- cached subset of activeUI containing those UIs needing live updates
           48  +	};
           49  +
           50  +	interface = lib.registry.mk 'starlit:interface';
           51  +	item = {
           52  +	};
           53  +
           54  +	region = {
           55  +		radiator = {
           56  +			store = AreaStore();
           57  +			emitters = {}
           58  +		};
           59  +	};
           60  +
           61  +	-- standardized effects
           62  +	fx = {};
           63  +
           64  +	type = {};
           65  +	world = {
           66  +		defaultScenario = 'starlit_scenario:imperialExpat';
           67  +		seedbank = lib.math.seedbank(minetest.get_mapgen_setting 'seed');
           68  +		mineral = lib.registry.mk 'starlit:mineral';
           69  +		material = { -- raw materials
           70  +			element = lib.registry.mk 'starlit:element';
           71  +			-- elements are automatically sorted into the following categories
           72  +			-- if they match. however, it's possible to have a metal/gas/liquid
           73  +			-- that *isn't* a pure element, so these need separate registries
           74  +			-- for alloys and mixtures like steel and water
           75  +			metal   = lib.registry.mk 'starlit:metal';
           76  +			gas     = lib.registry.mk 'starlit:gas';
           77  +			liquid  = lib.registry.mk 'starlit:liquid';
           78  +		};
           79  +		ecology = {
           80  +			plants = lib.registry.mk 'starlit:plants';
           81  +			trees = lib.registry.mk 'starlit:trees';
           82  +			biomes = lib.registry.mk 'starlit:biome';
           83  +		};
           84  +		climate = {};
           85  +		scenario = {};
           86  +		planet = {
           87  +			gravity = 7.44;
           88  +			orbit = 189; -- 1 year is 189 days
           89  +			revolve = 20; -- 1 day is 20 irl minutes
           90  +		};
           91  +		fact = lib.registry.mk 'starlit:fact';
           92  +		time = {
           93  +			calendar = {
           94  +				empire  = {
           95  +					name = 'Imperial Regnal Calendar';
           96  +					year = function(t, long)
           97  +						local reigns = {
           98  +							-- if anyone actually makes it to his Honor & Glory Unfailing Persigan I i will be
           99  +							-- exceptionally flattered
          100  +							{4, 'Emperor', 'Atavarka', 'the Bold'}; -- died at war
          101  +							{9, 'Emperor', 'Vatikserka', 'the Unconquered'}; -- died at war
          102  +							{22, 'Emperor', 'Rusifend', 'the Wise'}; -- poisoned at diplomacy
          103  +							{61, 'Empress', 'Tafseshendi', 'the Great'}; -- died of an 'insurrection of the innards' after a celebrated reign
          104  +							{291, 'Emperor', 'Treptebaska', 'the Unwise'}; -- murdered by his wife in short order
          105  +							{292, 'Empress', 'Vilintalti', 'the Impious'}; -- removed by the praetorian elite
          106  +							{298, 'Emperor', 'Radavan', 'the Reckless'}; -- died at war
          107  +							{316, 'Emperor', 'Suldibrand', 'the Forsaken of Men'}; -- fucked around. found out.
          108  +							{320, 'Emperor', 'Persigan', 'the Deathless'};
          109  +						}
          110  +						local year, r = math.floor(t / 414)
          111  +						for i=1, #reigns do if reigns[i+1][1] < year then r = reigns[i+1] end end
          112  +						local reignBegin, title, name, epithet = lib.tbl.unpack(r)
          113  +						local ry = 1 + (year - reignBegin)
          114  +						return long and string.format('Year %s of the Reign of HH&GU %s %s %s',
          115  +							ry, title, name, epithet) or string.format('Y. %s %s', name, ry)
          116  +					end;
          117  +					time = function(t, long)
          118  +						local bellsInDay, candleSpansInBell = 5, 7
          119  +						local bell = bellsInDay*t
          120  +						local cspan = (bellsInDay*candleSpansInBell*t) % candleSpansInBell
          121  +						return string.format(long and 'Bell %s, Candlespan %s' or '%sb %sc', math.floor(bell), math.floor(cspan))
          122  +					end;
          123  +				};
          124  +				commune = {
          125  +					name = 'People\'s Calendar';
          126  +					date = function(t, long)
          127  +						local year = math.floor(t / 256) + 314
          128  +						return string.format(long and 'Foundation %s' or 'F:%s', year)
          129  +					end;
          130  +					time = function(t, long)
          131  +						local hoursInDay, minutesInHour = 16, 16
          132  +						local hour = hoursInDay*t
          133  +						local min = (hoursInDay*minutesInHour*t) % minutesInHour
          134  +
          135  +						local dawn     = 0.24*hoursInDay
          136  +						local noon     = 0.5*hoursInDay
          137  +						local dusk     = 0.76*hoursInDay
          138  +						local midnight = 1.0*hoursInDay
          139  +
          140  +						local tl, str
          141  +						if hour < dawn then
          142  +							tl = dawn - hour
          143  +							str = long and 'dawn' or 'D'
          144  +						elseif hour < noon then
          145  +							tl = noon - hour
          146  +							str = long and 'noon' or 'N'
          147  +						elseif hour < dusk then
          148  +							tl = dusk - hour
          149  +							str = long and 'dusk' or 'd'
          150  +						elseif hour < midnight then
          151  +							tl = midnight - hour
          152  +							str = long and 'midnight' or 'M'
          153  +						end
          154  +						return long
          155  +							and string.format('%s hours, %s minutes to %s',
          156  +							    math.floor(tl), math.floor(minutesInHour - min), str)
          157  +							or  string.format('%s.%sH.%sM', str, math.floor(tl),
          158  +							    math.floor(minutesInHour - min))
          159  +					end;
          160  +				};
          161  +			};
          162  +		};
          163  +	};
          164  +
          165  +	jobs = {};
          166  +}
          167  +
          168  +starlit.cfgDir = minetest.get_worldpath() .. '/' .. starlit.ident
          169  +
          170  +local logger = function(module)
          171  +	local function argjoin(arg, nxt, ...)
          172  +		if arg and not nxt then return tostring(arg) end
          173  +		if not arg then return "(nil)" end
          174  +		return tostring(arg) .. ' ' .. argjoin(nxt, ...)
          175  +	end
          176  +	local lg = {}
          177  +	local setup = function(fn, lvl)
          178  +		lvl = lvl or fn
          179  +		local function emit(...)
          180  +			local call = (fn == 'fatal') and error
          181  +				or function(str) minetest.log(lvl, str) end
          182  +			if module
          183  +				then call(string.format('[%s :: %s] %s',starlit.ident,module,argjoin(...)))
          184  +				else call(string.format('[%s] %s',starlit.ident,argjoin(...)))
          185  +			end
          186  +		end
          187  +		lg[fn       ] = function(...) emit(...)                end
          188  +		lg[fn .. 'f'] = function(...) emit(string.format(...)) end -- convenience fn
          189  +	end
          190  +	setup('info')
          191  +	setup('warn','warning')
          192  +	setup('err','error')
          193  +	setup('act','action')
          194  +	setup('fatal')
          195  +	return lg
          196  +end
          197  +
          198  +starlit.logger = logger
          199  +
          200  +local log = logger()
          201  +
          202  +function starlit.evaluate(name, ...)
          203  +	local path = minetest.get_modpath(minetest.get_current_modname())
          204  +	local filename = string.format('%s/%s', path, name)
          205  +	log.info('loading', filename)
          206  +	local chunk, err = loadfile(filename, filename)
          207  +	if not chunk then error(err) end
          208  +	return chunk(...)
          209  +end
          210  +
          211  +function starlit.include(name, ...) -- semantic variant used for loading modules
          212  +	return starlit.evaluate(name..'.lua', ...)
          213  +end
          214  +
          215  +minetest.register_lbm {
          216  +	label = 'build radiator index';
          217  +	name = 'starlit:loadradiatorboxes';
          218  +	nodenames = {'group:radiator'};
          219  +	run_at_every_load = true;
          220  +	action = function(pos, node, dt)
          221  +		local R = starlit.region
          222  +		local phash = minetest.hash_node_position(pos)
          223  +		if R.radiator.sources[phash] then return end -- already loaded
          224  +
          225  +		local def = minetest.registered_nodes[node.name]
          226  +		local cl = def._starlit.radiator
          227  +		local min,max = cl.maxEffectArea(pos)
          228  +		local id = R.radiator.store:insert_area(min,max, minetest.pos_to_string(pos))
          229  +		R.radiator.sources[phash] = id
          230  +	end;
          231  +	-- NOTE: temp emitter nodes are responsible for decaching themselves in their on_destruct cb
          232  +}
          233  +
          234  +function starlit.startJob(id, interval, job)
          235  +	local lastRun
          236  +	local function start()
          237  +		starlit.jobs[id] = minetest.after(interval, function()
          238  +			local t = minetest.get_gametime()
          239  +			local d = lastRun and t - lastRun or nil
          240  +			lastRun = t
          241  +			local continue = job(d, interval)
          242  +			if continue == true or continue == nil then
          243  +				start()
          244  +			elseif continue ~= false then
          245  +				interval = continue
          246  +				start()
          247  +			end
          248  +		end)
          249  +	end
          250  +	start()
          251  +end
          252  +
          253  +starlit.include 'stats'
          254  +starlit.include 'world'
          255  +starlit.include 'fab'
          256  +starlit.include 'tiers'
          257  +starlit.include 'species'
          258  +
          259  +starlit.include 'store'
          260  +
          261  +starlit.include 'ui'
          262  +starlit.include 'item'
          263  +starlit.include 'container'
          264  +starlit.include 'user'
          265  +starlit.include 'effect'
          266  +
          267  +starlit.include 'fx/nano'
          268  +
          269  +starlit.include 'element'
          270  +
          271  +starlit.include 'terrain'
          272  +starlit.include 'interfaces'
          273  +starlit.include 'suit'
          274  +
          275  +minetest.settings:set('movement_gravity', starlit.world.planet.gravity) -- ??? seriously???
          276  +
          277  +---------------
          278  +-- callbacks --
          279  +---------------
          280  +-- here we connect our types up to the minetest API
          281  +
          282  +local function userCB(fn)
          283  +	return function(luser, ...)
          284  +		local name = luser:get_player_name()
          285  +		local user = starlit.activeUsers[name]
          286  +		return fn(user, ...)
          287  +	end
          288  +end
          289  +
          290  +minetest.register_on_joinplayer(function(luser, lastLogin)
          291  +	-- TODO check that necessary CSMs are installed
          292  +	local user = starlit.type.user(luser)
          293  +
          294  +	if lastLogin == nil then
          295  +		user:onSignup()
          296  +	end
          297  +	user:onJoin()
          298  +
          299  +	starlit.activeUsers[user.name] = user
          300  +end)
          301  +
          302  +minetest.register_on_leaveplayer(function(luser)
          303  +	starlit.activeUsers[luser:get_player_name()]:onPart()
          304  +end)
          305  +
          306  +minetest.register_on_player_receive_fields(function(luser, formid, fields)
          307  +	local name = luser:get_player_name()
          308  +	local user = starlit.activeUsers[name]
          309  +	if not user then return false end
          310  +	if formid == '' then -- main menu
          311  +		return starlit.ui.userMenuDispatch(user,fields)
          312  +	end
          313  +	local ui = starlit.interface.db[formid]
          314  +	local state = starlit.activeUI[name] or {}
          315  +	if formid == '__builtin:help_cmds' 
          316  +	or formid == '__builtin:help_privs' 
          317  +		then return false end
          318  +	assert(state.form == formid) -- sanity check
          319  +	user:onRespond(ui, state, fields)
          320  +	if fields.quit then
          321  +		starlit.activeUI[name] = nil
          322  +	end
          323  +	return true
          324  +end)
          325  +
          326  +minetest.register_on_respawnplayer(userCB(function(user)
          327  +	return user:onRespawn()
          328  +end))
          329  +
          330  +minetest.register_on_dieplayer(userCB(function(user, reason)
          331  +	return user:onDie(reason)
          332  +end))
          333  +
          334  +minetest.register_on_punchnode(function(pos,node,puncher,point)
          335  +	local user = starlit.activeUsers[puncher:get_player_name()]
          336  +	local oldTgt = user.action.tgt
          337  +	user.action.tgt = point
          338  +	if bit.band(user.action.bits, 0x80)==0 then
          339  +		user.action.bits = bit.bor(user.action.bits, 0x80)
          340  +		--user:trigger('primary', {state = 'init'})
          341  +	else
          342  +		user:trigger('retarget', {oldTgt = oldTgt})
          343  +	end
          344  +end)
          345  +
          346  +local function pointChanged(a,b)
          347  +	return a.type ~= b.type
          348  +		or a.type == 'node'   and vector.new(a.under) ~= vector.new(b.under)
          349  +		or a.type == 'object' and a.ref ~= b.ref 
          350  +end
          351  +local function triggerPower(_, luser, point)
          352  +	local user = starlit.activeUsers[luser:get_player_name()]
          353  +	local oldTgt = user.action.tgt
          354  +	user.action.tgt = point
          355  +	if bit.band(user.action.bits, 0x100)==0 then
          356  +		user.action.bits = bit.bor(user.action.bits, 0x100)
          357  +		--return user:trigger('secondary', {state = 'prog', delta = 0})
          358  +	elseif pointChanged(oldTgt, point) then
          359  +		user:trigger('retarget', {oldTgt = oldTgt})
          360  +	end
          361  +end
          362  +-- sigh
          363  +core.noneitemdef_default.on_place = function(...)
          364  +	if not triggerPower(...) then
          365  +		minetest.item_place(...)
          366  +	end
          367  +end
          368  +core.noneitemdef_default.on_use           = function(...) triggerPower(...) end
          369  +core.noneitemdef_default.on_secondary_use = function(...) triggerPower(...) end
          370  +
          371  +minetest.register_on_player_inventory_action(function(luser, act, inv, p)
          372  +	local name = luser:get_player_name()
          373  +	local user = starlit.activeUsers[name]
          374  +	-- allow UIs to update on UI changes
          375  +	local state = starlit.activeUI[name]
          376  +	if state then
          377  +		local ui = starlit.interface.db[state.form]
          378  +		ui:cb('onMoveItem', user, act, inv, p)
          379  +	end
          380  +end)
          381  +
          382  +minetest.register_on_player_hpchange(function(luser, delta, cause)
          383  +	local user = starlit.activeUsers[luser:get_player_name()]
          384  +	if cause.type == 'fall' then
          385  +		delta = user:damageModifier('bluntForceTrauma', (delta * 50))
          386  +		-- justification: a short fall can do around
          387  +		-- five points of damage, which is nearly 50%
          388  +		-- of the default hp_max. since we crank up
          389  +		-- hp by a factor of 50~40, damage should be
          390  +		-- cranked by similarly
          391  +	end
          392  +	return delta
          393  +end, true)
          394  +
          395  +function minetest.handle_node_drops(pos, drops, digger)
          396  +	local function jitter(pos)
          397  +		local function r(x) return x+math.random(-0.2, 0.2) end
          398  +		return vector.new(
          399  +			r(pos.x),
          400  +			r(pos.y),
          401  +			r(pos.z)
          402  +		)
          403  +	end
          404  +	for i, it in ipairs(drops) do
          405  +		minetest.add_item(jitter(pos), it)
          406  +	end
          407  +end
          408  +
          409  +
          410  +-- TODO timer iterates live UI
          411  +

Added mods/starlit/interfaces.lua version [1cb802f20f].

            1  +local lib = starlit.mod.lib
            2  +
            3  +function starlit.ui.setupForUser(user)
            4  +	local function cmode(mode)
            5  +		if user.actMode == mode then return {hue = 150, sat = 0, lum = .3} end
            6  +	end
            7  +	user.entity:set_inventory_formspec(starlit.ui.build {
            8  +		kind = 'vert', mode = 'sw';
            9  +		padding = .5, spacing = 0.1;
           10  +		{kind = 'hztl';
           11  +			{kind = 'contact', w=1.5,h=1.5, id = 'mode_nano',
           12  +				img='starlit-ui-icon-nano.png', close=true, color = cmode'nano'};
           13  +			{kind = 'contact', w=1.5,h=1.5, id = 'mode_weapon',
           14  +				img='starlit-ui-icon-weapon.png', close=true, color = cmode'weapon'};
           15  +			{kind = 'contact', w=1.5,h=1.5, id = 'mode_psi',
           16  +				img='starlit-ui-icon-psi.png', close=true, color = cmode'psi'};
           17  +		};
           18  +		{kind = 'hztl';
           19  +			{kind = 'contact', w=1.5,h=1.5, id = 'open_elements',
           20  +				img='starlit-ui-icon-element.png'};
           21  +			{kind = 'contact', w=1.5,h=1.5, id = 'open_suit',
           22  +				img='starlit-item-suit.png^[hsl:200:-.7:0'};
           23  +			{kind = 'contact', w=1.5,h=1.5, id = 'open_psi',
           24  +				img='starlit-ui-icon-psi-cfg.png'};
           25  +			{kind = 'contact', w=1.5,h=1.5, id = 'open_body',
           26  +				img='starlit-ui-icon-self.png'};
           27  +		};
           28  +		{kind = 'list';
           29  +			target = 'current_player', inv = 'main';
           30  +			w = 6, h = 1, spacing = 0.1;
           31  +		};
           32  +	})
           33  +end
           34  +
           35  +function starlit.ui.userMenuDispatch(user, fields)
           36  +	local function setSuitMode(mode)
           37  +		if user.actMode == mode then
           38  +			user:actModeSet 'off'
           39  +		else
           40  +			user:actModeSet(mode)
           41  +		end
           42  +	end
           43  +
           44  +	local modes = { nano = true, psi = false, weapon = true }
           45  +	for e,s in pairs(modes) do
           46  +		if fields['mode_' .. e] then
           47  +			if s and (user:naked() or user:getSuit():powerState() == 'off') then
           48  +				user:suitSound 'starlit-error'
           49  +			else
           50  +				setSuitMode(e)
           51  +			end
           52  +			return true
           53  +		end
           54  +	end
           55  +
           56  +	if fields.open_elements then
           57  +		user:openUI('starlit:user-menu', 'compiler')
           58  +		return true
           59  +	elseif fields.open_psi then
           60  +		user:openUI('starlit:user-menu', 'psi')
           61  +		return true
           62  +	elseif fields.open_suit then
           63  +		if not user:naked() then
           64  +			user:openUI('starlit:user-menu', 'suit')
           65  +		end
           66  +		return true
           67  +	elseif fields.open_body then
           68  +		user:openUI('starlit:user-menu', 'body')
           69  +	end
           70  +	return false
           71  +end
           72  +
           73  +local function listWrap(n, max)
           74  +	local h = math.ceil(n / max)
           75  +	local w = math.min(max, n)
           76  +	return w, h
           77  +end
           78  +
           79  +local function wrapMenu(w, h, rh, max, l)
           80  +	local root = {kind = 'vert', w=w, h=h}
           81  +	local bar
           82  +	local function flush()
           83  +		if bar and bar[1] then table.insert(root, bar) end
           84  +		bar = {kind = 'hztl'}
           85  +	end
           86  +	flush()
           87  +
           88  +	for _, i in ipairs(l) do
           89  +		local bw = w/max
           90  +		if i.cfg then w = w - rh end
           91  +
           92  +		table.insert(bar, {
           93  +			kind = 'button', close = i.close;
           94  +			color = i.color;
           95  +			fg = i.fg;
           96  +			label = i.label;
           97  +			icon = i.img;
           98  +			id = i.id;
           99  +			w = bw, h = rh;
          100  +		})
          101  +		if i.cfg then 
          102  +			table.insert(bar, {
          103  +				kind = 'button';
          104  +				color = i.color;
          105  +				fg = i.fg;
          106  +				label = "CFG";
          107  +				icon = i.img;
          108  +				id = i.id .. '_cfg';
          109  +				w = rh, h = rh;
          110  +			})
          111  +		end
          112  +
          113  +		if bar[max] then flush() end
          114  +	end
          115  +	flush()
          116  +	
          117  +	return root
          118  +end
          119  +
          120  +local function abilityMenu(a)
          121  +	-- select primary/secondary abilities or activate ritual abilities
          122  +	local p = {kind = 'vert'}
          123  +	for _, o in ipairs(a.order) do
          124  +		local m = a.menu[o]
          125  +		table.insert(p, {kind='label', text=m.label, w=a.w, h = .5})
          126  +		table.insert(p, wrapMenu(a.w, a.h, 1.2, 2, m.opts))
          127  +	end
          128  +	return p
          129  +end
          130  +
          131  +local function pptrMatch(a,b)
          132  +	if a == nil or b == nil then return false end
          133  +	return a.chipID == b.chipID and a.pgmIndex == b.pgmIndex
          134  +end
          135  +
          136  +starlit.interface.install(starlit.type.ui {
          137  +	id = 'starlit:user-menu';
          138  +	pages = {
          139  +		compiler = {
          140  +			setupState = function(state, user)
          141  +				-- nanotech/suit software menu
          142  +				local chips = user.entity:get_inventory():get_list 'starlit_suit_chips' -- FIXME need better subinv api
          143  +				local sw = starlit.mod.electronics.chip.usableSoftware(chips)
          144  +				state.suitSW = {}
          145  +				local dedup = {}
          146  +				for i, r in ipairs(sw) do if
          147  +					r.sw.kind      == 'suitPower'
          148  +				then
          149  +					if not dedup[r.sw] then
          150  +						dedup[r.sw] = true
          151  +						table.insert(state.suitSW, r)
          152  +					end
          153  +				end end
          154  +			end;
          155  +			handle = function(state, user, act)
          156  +				if user:getSuit():powerState() == 'off' then return false end
          157  +				local pgm, cfg
          158  +				for k in next, act do
          159  +					local id, mode = k:match('^suit_pgm_([0-9]+)_(.*)$')
          160  +					if id then
          161  +						id = tonumber(id)
          162  +						if state.suitSW[id] then
          163  +							pgm = state.suitSW[id]
          164  +							cfg = mode == '_cfg'
          165  +							break
          166  +						end
          167  +					end
          168  +				end
          169  +				if not pgm then return false end -- HAX
          170  +
          171  +				-- kind=active programs must be assigned to a command slot
          172  +				-- kind=direct programs must open their UI
          173  +				-- kind=passive programs must toggle on and off
          174  +				if pgm.sw.powerKind == 'active' then
          175  +					if cfg then
          176  +						user:openUI(pgm.sw.ui, 'index', {
          177  +							context = 'suit';
          178  +							program = pgm;
          179  +						})
          180  +						return false
          181  +					end
          182  +					local ptr = {chipID = starlit.mod.electronics.chip.read(pgm.chip).uuid, pgmIndex = pgm.fd.inode}
          183  +					local pnan = user.power.nano
          184  +					if pnan.primary == nil then
          185  +						pnan.primary = ptr
          186  +					elseif pptrMatch(ptr, pnan.primary) then
          187  +						pnan.primary = nil
          188  +					elseif pptrMatch(ptr, pnan.secondary) then
          189  +						pnan.secondary = nil
          190  +					else
          191  +						pnan.secondary = ptr
          192  +					end
          193  +					user:suitSound 'starlit-configure'
          194  +				elseif pgm.sw.powerKind == 'direct' then
          195  +					local ctx = {
          196  +						context = 'suit';
          197  +						program = pgm;
          198  +					}
          199  +					if pgm.sw.ui then
          200  +						user:openUI(pgm.sw.ui, 'index', ctx)
          201  +						return false
          202  +					else
          203  +						pgm.sw.run(user, ctx)
          204  +					end
          205  +				elseif pgm.sw.powerKind == 'passive' then
          206  +					if cfg then
          207  +						user:openUI(pgm.sw.ui, 'index', {
          208  +							context = 'suit';
          209  +							program = pgm;
          210  +						})
          211  +						return false
          212  +					end
          213  +
          214  +					local addDisableRec = true
          215  +					for i, e in ipairs(pgm.file.body.conf) do
          216  +						if e.key == 'disable' and e.value == 'yes' then
          217  +							addDisableRec = false
          218  +							table.remove(pgm.file.body.conf, i)
          219  +							break
          220  +						elseif e.key == 'disable' and e.value == 'no' then
          221  +							e.value = 'yes'
          222  +							addDisableRec = false
          223  +							break
          224  +						end
          225  +					end
          226  +					if addDisableRec then
          227  +						table.insert(pgm.file.body.conf, {key='disable',value='yes'})
          228  +					end
          229  +					-- update the chip *wince*
          230  +					pgm.fd:write(pgm.file)
          231  +					user.entity:get_inventory():set_stack('starlit_suit_chips',
          232  +					pgm.chipSlot, pgm.chip)
          233  +					user:reconfigureSuit()
          234  +					user:suitSound('starlit-configure')
          235  +
          236  +				end
          237  +				return true, true
          238  +			end;
          239  +			render = function(state, user)
          240  +				local suit = user:getSuit()
          241  +				local swm
          242  +				if user:getSuit():powerState() ~= 'off' then
          243  +					swm = {
          244  +						w = 8, h = 3;
          245  +						order = {'active','ritual','pasv'};
          246  +						menu = {
          247  +							active = {
          248  +								label = 'Nanoware';
          249  +								opts = {};
          250  +							};
          251  +							ritual = {
          252  +								label = 'Programs';
          253  +								opts = {};
          254  +							};
          255  +							pasv = {
          256  +								label = 'Passive';
          257  +								opts = {};
          258  +							};
          259  +						};
          260  +					}
          261  +					for id, r in pairs(state.suitSW) do
          262  +						local color = {hue=300,sat=0,lum=0}
          263  +						local fg = nil
          264  +						local close = nil
          265  +						local tbl, cfg if r.sw.powerKind == 'active' then
          266  +							tbl = swm.menu.active.opts
          267  +							if r.sw.ui then cfg = true end
          268  +							local pnan = user.power.nano
          269  +							if pnan then
          270  +								local ptr = {chipID = starlit.mod.electronics.chip.read(r.chip).uuid, pgmIndex = r.fd.inode}
          271  +								if pptrMatch(ptr, pnan.primary) then
          272  +									color.lum = 1
          273  +								elseif pptrMatch(ptr, pnan.secondary) then
          274  +									color.lum = 0.8
          275  +								end
          276  +							end
          277  +						elseif r.sw.powerKind == 'direct' then
          278  +							tbl = swm.menu.ritual.opts
          279  +							if not r.sw.ui then
          280  +								close = true
          281  +							end
          282  +						elseif r.sw.powerKind == 'passive' then
          283  +							tbl = swm.menu.pasv.opts
          284  +							if r.sw.ui then cfg = true end
          285  +							for i, e in ipairs(r.file.body.conf) do
          286  +								if e.key == 'disable' and e.value == 'yes' then
          287  +									color.lum = -.2
          288  +									fg = lib.color {hue=color.hue,sat=0.7,lum=0.7}
          289  +									break
          290  +								end
          291  +							end
          292  +						end
          293  +						if tbl then table.insert(tbl, {
          294  +							color = color, fg = fg;
          295  +							label = r.sw.label or r.sw.name;
          296  +							id = string.format('suit_pgm_%s_', id);
          297  +							cfg = cfg, close = close;
          298  +						}) end
          299  +					end
          300  +				end
          301  +				local menu = { kind = 'vert', mode = 'sw', padding = 0.5 }
          302  +				if swm then table.insert(menu, abilityMenu(swm)) end
          303  +
          304  +				local inv = user.entity:get_inventory()
          305  +				local cans = inv:get_list 'starlit_suit_canisters'
          306  +				if cans and next(cans) then for i, st in ipairs(cans) do
          307  +					local id = string.format('starlit_canister_%u_elem', i)
          308  +					local esz = inv:get_size(id)
          309  +					if esz > 0 then
          310  +						local eltW, eltH = listWrap(esz, 5)
          311  +						table.insert(menu, {kind = 'hztl',
          312  +							{kind = 'img', desc='Elements', img = 'starlit-ui-icon-element.png', w=1,h=1};
          313  +							{kind = 'list', target = 'current_player', inv = id,
          314  +								listContent = 'element', w = eltW, h = eltH, spacing = 0.1};
          315  +						})
          316  +					end
          317  +				end end
          318  +
          319  +				if #menu == 0 then
          320  +					table.insert(menu, {
          321  +						kind = 'img';
          322  +						img = 'starlit-ui-alert.png';
          323  +						w=2, h=2;
          324  +					})
          325  +					menu.padding = 1;
          326  +				end
          327  +				return starlit.ui.build(menu)
          328  +			end;
          329  +		};
          330  +		compilerListRecipes = {
          331  +		};
          332  +		psi = {
          333  +			render = function(state, user)
          334  +				return starlit.ui.build {
          335  +					kind = 'vert', mode = 'sw';
          336  +					padding = 0.5;
          337  +				}
          338  +			end;
          339  +		};
          340  +		body = {
          341  +			render = function(state, user)
          342  +				local barh = .75
          343  +				local tb = {
          344  +					kind = 'vert', mode = 'sw';
          345  +					padding = 0.5, 
          346  +					{kind = 'hztl', padding = 0.25;
          347  +						{kind = 'label', text = 'Name', w = 2, h = barh};
          348  +						{kind = 'label', text = user.persona.name, w = 4, h = barh}};
          349  +				}
          350  +				local statBars = {'hunger', 'thirst', 'fatigue', 'morale'}
          351  +				for idx, id in ipairs(statBars) do
          352  +					local s = starlit.world.stats[id]
          353  +					local amt, sv = user:effectiveStat(id)
          354  +					local min, max = starlit.world.species.statRange(user.persona.species, user.persona.speciesVariant, id)
          355  +					local st = string.format('%s / %s', s.desc(amt, true), s.desc(max))
          356  +					table.insert(tb, {kind = 'hztl', padding = 0.25;
          357  +						{kind = 'label', w=2, h=barh, text = s.name};
          358  +						{kind = 'hbar',  w=4, h=barh, fac = sv, text = st, color=s.color};
          359  +					})
          360  +				end
          361  +				local abilities = {
          362  +					{id = 'abl_sprint', label = 'Sprint', img = 'starlit-ui-icon-ability-sprint.png'};
          363  +				}
          364  +				table.insert(tb, wrapMenu(6.25,4, 1,2, abilities))
          365  +				return starlit.ui.build(tb)
          366  +			end;
          367  +		};
          368  +		suit = {
          369  +			render = function(state, user)
          370  +				local suit = user:getSuit()
          371  +				local suitDef = suit:def()
          372  +				local chipW, chipH = listWrap(suitDef.slots.chips, 5)
          373  +				local batW, batH = listWrap(suitDef.slots.batteries, 5)
          374  +				local canW, canH = listWrap(suitDef.slots.canisters, 5)
          375  +				local suitMode = suit:powerState()
          376  +				local function modeColor(mode)
          377  +					if mode == suitMode then return {hue = 180, sat = 0, lum = .5} end
          378  +				end
          379  +				return starlit.ui.build {
          380  +					kind = 'vert', mode = 'sw';
          381  +					padding = 0.5, spacing = 0.1;
          382  +					{kind = 'hztl',
          383  +						{kind = 'img', desc='Batteries', img = 'starlit-item-battery.png', w=1,h=1};
          384  +						{kind = 'list', target = 'current_player', inv = 'starlit_suit_bat',
          385  +							listContent = 'power', w = batW, h = batH, spacing = 0.1};
          386  +					};
          387  +					{kind = 'hztl',
          388  +						{kind = 'img', desc='Chips', img = 'starlit-item-chip.png', w=1,h=1};
          389  +						{kind = 'list', target = 'current_player', inv = 'starlit_suit_chips',
          390  +							listContent = 'chip', w = chipW, h = chipH, spacing = 0.1};
          391  +					};
          392  +					{kind = 'hztl',
          393  +						{kind = 'img', desc='Canisters', img = 'starlit-item-element-canister.png', w=1,h=1};
          394  +						{kind = 'list', target = 'current_player', inv = 'starlit_suit_canisters',
          395  +							listContent = nil, w = canW, h = canH, spacing = 0.1};
          396  +					};
          397  +					{kind = 'hztl';
          398  +						{kind = 'img', w=1,h=1, item = suit.item:get_name(),
          399  +							desc = suit.item:get_definition().short_description};
          400  +						{kind = 'button', w=1.5,h=1, id = 'powerMode_off', label = 'Off';
          401  +							color=modeColor'off'};
          402  +						{kind = 'button', w=2.5,h=1, id = 'powerMode_save', label = 'Power Save';
          403  +							color=modeColor'powerSave'};
          404  +						{kind = 'button', w=1.5,h=1, id = 'powerMode_on', label = 'On'; 
          405  +							color=modeColor'on'};
          406  +					};
          407  +					{kind = 'list', target = 'current_player', inv = 'main', w = 6, h = 1, spacing = 0.1};
          408  +				}
          409  +			end;
          410  +			handle = function(state, user, q)
          411  +				local suitMode
          412  +				if     q.powerMode_off  then suitMode = 'off'
          413  +				elseif q.powerMode_save then suitMode = 'powerSave'
          414  +				elseif q.powerMode_on   then suitMode = 'on' end
          415  +				if suitMode then
          416  +					user:suitPowerStateSet(suitMode)
          417  +					return true
          418  +				end
          419  +			end;
          420  +		};
          421  +	};
          422  +})
          423  +
          424  +starlit.interface.install(starlit.type.ui {
          425  +	id = 'starlit:compile-matter-component';
          426  +	pages = {
          427  +		index = {
          428  +			setupState = function(state, user, ctx)
          429  +				if ctx.context == 'suit' then
          430  +				end
          431  +				state.pgm = ctx.program
          432  +			end;
          433  +			render = function(state, user)
          434  +				return starlit.ui.build {
          435  +					kind = 'vert', padding = 0.5; w = 5, h = 5, mode = 'sw';
          436  +					{kind = 'label', w = 4, h = 1, text = 'hello'};
          437  +				}
          438  +			end;
          439  +		};
          440  +	};
          441  +})

Added mods/starlit/item.lua version [aa837e16cf].

            1  +local lib = starlit.mod.lib
            2  +local I = starlit.item
            3  +
            4  +function I.mk(item, context)
            5  +	local st = ItemStack(item)
            6  +	local md = st:get_definition()._starlit
            7  +	local ctx = context or {}
            8  +	if md and md.event then
            9  +		md.event.create(st, ctx)
           10  +	end
           11  +	if context.how == 'print' then
           12  +		if context.schematic and context.schematic.setup then
           13  +			context.schematic.setup(st, ctx)
           14  +		end
           15  +	end
           16  +	return st
           17  +end

Added mods/starlit/mod.conf version [e7817791d1].

            1  +name = starlit
            2  +author = velartrill
            3  +description = world logic and UI
            4  +depends = vtlib

Added mods/starlit/species.lua version [3944fdb227].

            1  +local lib = starlit.mod.lib
            2  +
            3  +local paramTypes do local T,G = lib.marshal.t, lib.marshal.g
            4  +	paramTypes = {
            5  +		tone = G.struct {
            6  +			hue = T.angle;
            7  +			sat = T.clamp;
            8  +			lum = T.clamp;
            9  +		};
           10  +		str = T.str;
           11  +		num = T.decimal;
           12  +	}
           13  +end
           14  +
           15  +-- constants
           16  +local animationFrameRate = 60
           17  +
           18  +local species = {
           19  +	human = {
           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.';
           22  +		scale = 1.0;
           23  +		params = {
           24  +			{'eyeColor',  'Eye Color',  'tone', {hue=327, sat=0, lum=0}};
           25  +			{'hairColor', 'Hair Color', 'tone', {hue=100, sat=0, lum=0}};
           26  +			{'skinTone',  'Skin Tone',  'tone', {hue=  0, sat=0, lum=0}};
           27  +		};
           28  +		tempRange = {
           29  +			comfort    = {18.3, 23.8}; -- needed for full stamina regen
           30  +			survivable = {5,    33}; -- anything below/above will cause progressively more damage
           31  +		};
           32  +		variants = {
           33  +			female = {
           34  +				name = 'Human Female';
           35  +				mesh = 'starlit-body-female.x';
           36  +				eyeHeight = 1.4;
           37  +				texture = function(t, adorn)
           38  +					local skin = lib.image 'starlit-body-skin.png' : shift(t.skinTone)
           39  +					local eye  = lib.image 'starlit-body-eye.png'  : shift(t.eyeColor)
           40  +					local hair = lib.image 'starlit-body-hair.png' : shift(t.hairColor)
           41  +
           42  +					local invis = lib.image '[fill:1x1:0,0:#00000000'
           43  +					local plate = adorn.suit and adorn.suit.plate or invis
           44  +					local lining = adorn.suit and adorn.suit.lining or invis
           45  +
           46  +					return {lining, plate, skin, skin, eye, hair}
           47  +				end;
           48  +				stats = {
           49  +					psiRegen = 1.3;
           50  +					psiPower = 1.2;
           51  +					psi = 1.2;
           52  +					hunger = .8; -- women have smaller stomachs
           53  +					thirst = .8;
           54  +					staminaRegen = 1.0;
           55  +					morale = 0.8; -- you are not She-Bear Grylls
           56  +				};
           57  +				traits = {
           58  +					health = 400;
           59  +					lungCapacity = .6;
           60  +					irradiation = 0.8; -- you are smaller, so it takes less rads to kill ya
           61  +					sturdiness = 0; -- women are more fragile and thus susceptible to blunt force trauma
           62  +					metabolism = 1800; --Cal
           63  +					painTolerance = 0.4;
           64  +				};
           65  +			};
           66  +			male = {
           67  +				name = 'Human Male';
           68  +				eyeHeight = 1.6;
           69  +				stats = {
           70  +					psiRegen = 1.0;
           71  +					psiPower = 1.0;
           72  +					psi = 1.0;
           73  +					hunger = 1.0;
           74  +					staminaRegen = .7; -- men are strong but have inferior endurance
           75  +				};
           76  +				traits = {
           77  +					health = 500;
           78  +					painTolerance = 1.0;
           79  +					lungCapacity = 1.0;
           80  +					sturdiness = 0.3;
           81  +					metabolism = 2200; --Cal
           82  +				};
           83  +			};
           84  +		};
           85  +		traits = {};
           86  +	};
           87  +}
           88  +
           89  +starlit.world.species = {
           90  +	index = species;
           91  +	paramTypes = paramTypes;
           92  +}
           93  +
           94  +function starlit.world.species.mkDefaultParamsTable(pSpecies, pVariant)
           95  +	local sp = species[pSpecies]
           96  +	local var = sp.variants[pVariant]
           97  +	local vpd = var.defaults or {}
           98  +	local tbl = {}
           99  +	for _, p in pairs(sp.params) do
          100  +		local name, desc, ty, dflt = lib.tbl.unpack(p)
          101  +		tbl[name] = vpd[name] or dflt
          102  +	end
          103  +	return tbl
          104  +end
          105  +
          106  +
          107  +function starlit.world.species.mkPersonaFor(pSpecies, pVariant)
          108  +	return {
          109  +		species = pSpecies;
          110  +		speciesVariant = pVariant;
          111  +		bodyParams = starlit.world.species.paramsFromTable(pSpecies,
          112  +			starlit.world.species.mkDefaultParamsTable(pSpecies, pVariant)
          113  +		);
          114  +		statDeltas = {};
          115  +	}
          116  +end
          117  +
          118  +local function spLookup(pSpecies, pVariant)
          119  +	local sp = species[pSpecies]
          120  +	local var = sp.variants[pVariant or next(sp.variants)]
          121  +	return sp, var
          122  +end
          123  +starlit.world.species.lookup = spLookup
          124  +
          125  +function starlit.world.species.statRange(pSpecies, pVariant, pStat)
          126  +	local sp,spv = spLookup(pSpecies, pVariant)
          127  +	local min, max, base
          128  +	if pStat == 'health' then
          129  +		min,max = 0, spv.traits.health
          130  +	elseif pStat == 'breath' then
          131  +		min,max = 0, 65535
          132  +	else
          133  +		local spfac = spv.stats[pStat]
          134  +		local basis = starlit.world.stats[pStat]
          135  +		min,max = basis.min, basis.max
          136  +
          137  +		if spfac then
          138  +			min = min * spfac
          139  +			max = max * spfac
          140  +		end
          141  +
          142  +		base = basis.base
          143  +		if base == true then
          144  +			base = max
          145  +		elseif base == false then
          146  +			base = min
          147  +		end
          148  +
          149  +	end
          150  +	return min, max, base
          151  +end
          152  +
          153  +-- set the necessary properties and create a persona for a newspawned entity
          154  +function starlit.world.species.birth(pSpecies, pVariant, entity, circumstances)
          155  +	circumstances = circumstances or {}
          156  +	local sp,var = spLookup(pSpecies, pVariant)
          157  +
          158  +	local function pct(st, p)
          159  +		local min, max = starlit.world.species.statRange(pSpecies, pVariant, st)
          160  +		local delta = max - min
          161  +		return min + delta*p
          162  +	end
          163  +	local ps = starlit.world.species.mkPersonaFor(pSpecies,pVariant)
          164  +	local startingHP = pct('health', 1.0)
          165  +	if circumstances.injured    then startingHP = pct('health', circumstances.injured) end
          166  +	if circumstances.psiCharged then ps.statDeltas.psi = pct('psi', circumstances.psiCharged) end
          167  +	ps.statDeltas.warmth = 20 -- don't instantly start dying of frostbite
          168  +
          169  +	entity:set_properties{hp_max = var.traits.health or sp.traits.health}
          170  +	entity:set_hp(startingHP, 'initial hp')
          171  +	return ps
          172  +end
          173  +
          174  +function starlit.world.species.paramsFromTable(pSpecies, tbl)
          175  +	local lst = {}
          176  +	local sp = species[pSpecies]
          177  +	for i, par in pairs(sp.params) do
          178  +		local name,desc,ty,dflt = lib.tbl.unpack(par)
          179  +		if tbl[name] then
          180  +			table.insert(lst, {id=name, value=paramTypes[ty].enc(tbl[name])})
          181  +		end
          182  +	end
          183  +	return lst
          184  +end
          185  +function starlit.world.species.paramsToTable(pSpecies, lst)
          186  +	local tymap = {}
          187  +	local sp = species[pSpecies]
          188  +	for i, par in pairs(sp.params) do
          189  +		local name,desc,ty,dflt = lib.tbl.unpack(par)
          190  +		tymap[name] = paramTypes[ty]
          191  +	end
          192  +
          193  +	local tbl = {}
          194  +	for _, e in pairs(lst) do
          195  +		tbl[e.id] = tymap[e.id].dec(e.value)
          196  +	end
          197  +	return tbl
          198  +end
          199  +
          200  +for speciesName, sp in pairs(species) do
          201  +	for varName, var in pairs(sp.variants) do
          202  +		if var.mesh then
          203  +			var.animations = starlit.evaluate(string.format('models/%s.nla', var.mesh)).skel.action
          204  +		end
          205  +	end
          206  +end
          207  +
          208  +
          209  +function starlit.world.species.updateTextures(ent, persona, adornment)
          210  +	local s,v = spLookup(persona.species, persona.speciesVariant)
          211  +	local paramTable = starlit.world.species.paramsToTable(persona.species, persona.bodyParams)
          212  +	local texs = {}
          213  +	for i, t in ipairs(v.texture(paramTable, adornment)) do
          214  +		texs[i] = t:render()
          215  +	end
          216  +	ent:set_properties { textures = texs }
          217  +end
          218  +
          219  +function starlit.world.species.setupEntity(ent, persona)
          220  +	local s,v = spLookup(persona.species, persona.speciesVariant)
          221  +	local _, maxHealth = starlit.world.species.statRange(persona.species, persona.speciesVariant, 'health')
          222  +	ent:set_properties {
          223  +		visual = 'mesh';
          224  +		mesh = v.mesh;
          225  +		stepheight = .51;
          226  +		eye_height = v.eyeHeight;
          227  +		collisionbox = { -- FIXME
          228  +			-0.3, 0.0, -0.3;
          229  +			0.3, 1.5,  0.3;
          230  +		};
          231  +		visual_size = vector.new(10,10,10) * s.scale;
          232  +
          233  +		hp_max = maxHealth;
          234  +	}
          235  +	local function P(v)
          236  +		if v then return {x=v[1],y=v[2]} end
          237  +		return {x=0,y=0}
          238  +	end
          239  +	ent:set_local_animation(
          240  +		P(v.animations.idle),
          241  +		P(v.animations.run),
          242  +		P(v.animations.work),
          243  +		P(v.animations.runWork),
          244  +		animationFrameRate)
          245  +
          246  +end

Added mods/starlit/stats.lua version [c766e87490].

            1  +local lib = starlit.mod.lib
            2  +
            3  +local function U(unit, prec, fixed)
            4  +	if fixed then
            5  +		return function(amt, excludeUnit)
            6  +			if excludeUnit then return tostring(amt/prec) end
            7  +			return string.format("%s %s", amt/prec, unit)
            8  +		end
            9  +	else
           10  +		return function(amt, excludeUnit)
           11  +			if excludeUnit then return tostring(amt/prec) end
           12  +			return lib.math.si(unit, amt/prec)
           13  +		end
           14  +	end
           15  +end
           16  +
           17  +local function C(h, s, l)
           18  +	return lib.color {hue = h, sat = s or 1, lum = l or .7}
           19  +end
           20  +starlit.world.stats = {
           21  +	psi        = {min = 0, max = 500, base = 0, desc = U('ψ', 10), color = C(320), name = 'Numina'};
           22  +	-- numina is measured in daψ
           23  +	warmth     = {min = -1000, max = 1000, base = 0, desc = U('°C', 10, true), color = C(5), name = 'Warmth'};
           24  +	-- warmth in measured in °C×10
           25  +	fatigue    = {min = 0, max = 76 * 60, base = 0, desc = U('hr', 60, true), color = C(288,.3,.5), name = 'Fatigue'};
           26  +	-- fatigue is measured in minutes one needs to sleep to cure it
           27  +	stamina    = {min = 0, max = 20 * 100, base = true, desc = U('m', 100), color = C(88), name = 'Stamina'};
           28  +	-- 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
           33  +	morale     = {min = 0, max = 24 * 60 * 10, base = true, desc = U('hr', 60, true), color = C(0,0,.8), name = 'Morale'};
           34  +	-- morale is measured in minutes. e.g. at base rate morale degrades by
           35  +	-- 60 points every hour. morale can last up to 10 days
           36  +	irradiation = {min = 0, max = 20000, base = 0, desc = U('Gy', 1000), color = C(141,1,.5), name = 'Irradiation'};
           37  +	-- irrad is measured is milligreys
           38  +	-- 1Gy counters natural healing
           39  +	-- ~3Gy counters basic nanomedicine
           40  +	-- 5Gy causes death within two weeks without nanomedicine
           41  +	-- radiation speeds up psi regen
           42  +	-- morale drain doubles with each 2Gy
           43  +	illness    = {min = 0, max = 1000, base = 0, desc = U('%', 10, true), color = C(71,.4,.25), name = 'Illness'};
           44  +	-- as illness increases, maximum stamina and health gain a corresponding limit
           45  +	-- illness is increased by certain conditions, and decreases on its own as your
           46  +	-- body heals when those conditions wear off. some drugs can lower accumulated illness
           47  +	-- but illness-causing conditions require specific cures
           48  +	-- illness also causes thirst and fatigue to increase proportionately
           49  +}

Added mods/starlit/store.lua version [73c6f814ce].

            1  +-- [ʞ] store.lua
            2  +--  ~ lexi hale <lexi@hale.su>
            3  +--  © EUPLv1.2
            4  +--  ? defines serialization datatypes that don't belong to
            5  +--    any individual class
            6  +
            7  +local lib = starlit.mod.lib
            8  +local T,G = lib.marshal.t, lib.marshal.g
            9  +starlit.store = {} -- the serialization equivalent of .type
           10  +
           11  +-------------
           12  +-- persona --
           13  +------------- -----------------------------------------------
           14  +-- a Persona is a structure that defines the nature of     --
           15  +-- an (N)PC and how it interacts with the Starsoul-managed --
           16  +-- portion of the game world -- things like name, species, --
           17  +-- stat values, physical characteristics, and so forth     --
           18  +
           19  +local statStructFields = {}
           20  +for k,v in pairs(starlit.world.stats) do
           21  +	statStructFields[k] = v.srzType or (
           22  +		(v.base == true or v.base > 0) and T.s16 or T.u16
           23  +	)
           24  +end
           25  +
           26  +starlit.store.compilerJob = G.struct {
           27  +	schematic = T.str;
           28  +	progress = T.clamp;
           29  +}
           30  +
           31  +starlit.store.persona = G.struct {
           32  +	name = T.str;
           33  +	species = T.str;
           34  +	speciesVariant = T.str;
           35  +	background = T.str;
           36  +	bodyParams = G.array(8, G.struct {id = T.str, value = T.str}); --variant
           37  +
           38  +	statDeltas = G.struct(statStructFields);
           39  +
           40  +	facts = G.array(32, G.array(8, T.str));
           41  +	-- facts stores information the player has discovered and narrative choices
           42  +	-- she has made.
           43  +	-- parametric facts are encoded as horn clauses
           44  +	-- non-parametric facts are encoded as {'fact-mod:fact-id'}
           45  +}
           46  +
           47  +starlit.store.suitMeta = lib.marshal.metaStore {
           48  +	batteries = {key = 'starlit:suit_slots_bat', type = T.inventoryList};
           49  +	chips = {key = 'starlit:suit_slots_chips', type = T.inventoryList};
           50  +	elements = {key = 'starlit:suit_slots_elem', type = T.inventoryList};
           51  +	guns = {key = 'starlit:suit_slots_gun', type = T.inventoryList};
           52  +	ammo = {key = 'starlit:suit_slots_ammo', type = T.inventoryList};
           53  +}

Added mods/starlit/suit.lua version [7112e6c94b].

            1  +local lib = starlit.mod.lib
            2  +
            3  +local suitStore = starlit.store.suitMeta
            4  +starlit.item.suit = lib.registry.mk 'starlit:suits';
            5  +
            6  +-- note that this cannot be persisted as a reference to a particular suit in the world
            7  +local function suitContainer(stack, inv)
            8  +	return starlit.item.container(stack, inv, {
            9  +		pfx = 'starlit_suit'
           10  +	})
           11  +end
           12  +starlit.type.suit = lib.class {
           13  +	name = 'starlit:suit';
           14  +	construct = function(stack)
           15  +		return {
           16  +			item = stack;
           17  +			inv = suitStore(stack);
           18  +		}
           19  +	end;
           20  +	__index = {
           21  +		powerState = function(self)
           22  +			local s = self.item
           23  +			if not s then return nil end
           24  +			local m = s:get_meta():get_int('starlit:power_mode')
           25  +			if m == 1 then return 'on'
           26  +			elseif m == 2 then return 'powerSave'
           27  +			else return 'off' end
           28  +		end;
           29  +		powerStateSet = function(self, state)
           30  +			local s = self.item
           31  +			if not s then return nil end
           32  +			local m
           33  +			if state == 'on' then m = 1 -- TODO check power level
           34  +			elseif state == 'powerSave' then m = 2
           35  +			else m = 0 end
           36  +			if self:powerLeft() <= 0 then m = 0 end
           37  +			s:get_meta():set_int('starlit:power_mode', m)
           38  +		end;
           39  +		powerLeft = function(self)
           40  +			local batteries = self.inv.read 'batteries'
           41  +			local power = 0
           42  +			for idx, slot in pairs(batteries) do
           43  +				power = power + starlit.mod.electronics.dynamo.totalPower(slot)
           44  +			end
           45  +			return power
           46  +		end;
           47  +		powerCapacity = function(self)
           48  +			local batteries = self.inv.read 'batteries'
           49  +			local power = 0
           50  +			for idx, slot in pairs(batteries) do
           51  +				power = power + starlit.mod.electronics.dynamo.initialPower(slot)
           52  +			end
           53  +			return power
           54  +		end;
           55  +		maxPowerUse = function(self)
           56  +			local batteries = self.inv.read 'batteries'
           57  +			local w = 0
           58  +			for idx, slot in pairs(batteries) do
           59  +				w = w + starlit.mod.electronics.dynamo.dischargeRate(slot)
           60  +			end
           61  +			return w
           62  +		end;
           63  +		onReconfigure = function(self, inv)
           64  +			-- apply any changes to item metadata and export any subinventories
           65  +			-- to the provided invref, as they may have changed
           66  +			local sc = starlit.item.container(self.item, inv, {pfx = 'starlit_suit'})
           67  +			sc:push()
           68  +			self:pullCanisters(inv)
           69  +		end;
           70  +		onItemMove = function(self, user, list, act, what)
           71  +			-- called when the suit inventory is changed
           72  +			if act == 'put' then
           73  +				if list == 'starlit_suit_bat' then
           74  +					user:suitSound('starlit-suit-battery-in')
           75  +				elseif list == 'starlit_suit_chips' then
           76  +					user:suitSound('starlit-suit-chip-in')
           77  +				elseif list == 'starlit_suit_canisters' then
           78  +					user:suitSound('starlit-insert-snap')
           79  +				end
           80  +			elseif act == 'take' then
           81  +				if list == 'starlit_suit_bat' then
           82  +					user:suitSound('starlit-insert-snap')
           83  +				elseif list == 'starlit_suit_chips' then
           84  +					--user:suitSound('starlit-suit-chip-out')
           85  +				elseif list == 'starlit_suit_canisters' then
           86  +					user:suitSound('starlit-insert-snap')
           87  +				end
           88  +			end
           89  +		end;
           90  +		def = function(self)
           91  +			return self.item:get_definition()._starlit.suit
           92  +		end;
           93  +		pullCanisters = function(self, inv)
           94  +			starlit.item.container.dropPrefix(inv, 'starlit_canister')
           95  +			self:forCanisters(inv, function(sc) sc:pull() end)
           96  +		end;
           97  +		pushCanisters = function(self, inv, st, i)
           98  +			self:forCanisters(inv, function(sc)
           99  +				sc:push()
          100  +				return true
          101  +			end)
          102  +		end;
          103  +		forCanisters = function(self, inv, fn)
          104  +			local cans = inv:get_list 'starlit_suit_canisters'
          105  +			if cans and next(cans) then for i, st in ipairs(cans) do
          106  +				if not st:is_empty() then
          107  +					local pfx = 'starlit_canister_' .. tostring(i)
          108  +					local sc = starlit.item.container(st, inv, {pfx = pfx})
          109  +					if fn(sc, st, i, pfx) then
          110  +						inv:set_stack('starlit_suit_canisters', i, st)
          111  +					end
          112  +				end
          113  +			end end
          114  +		end;
          115  +		establishInventories = function(self, obj)
          116  +			local inv = obj:get_inventory()
          117  +			local ct = suitContainer(self.item, inv)
          118  +			ct:pull()
          119  +			self:pullCanisters(inv)
          120  +
          121  +			--[[
          122  +			local def = self:def()
          123  +			local sst = suitStore(self.item)
          124  +			local function readList(listName, prop)
          125  +				inv:set_size(listName, def.slots[prop])
          126  +				if def.slots[prop] > 0 then
          127  +					local lst = sst.read(prop)
          128  +					inv:set_list(listName, lst)
          129  +				end
          130  +			end
          131  +			readList('starlit_suit_chips', 'chips')
          132  +			readList('starlit_suit_bat',   'batteries')
          133  +			readList('starlit_suit_guns',  'guns')
          134  +			readList('starlit_suit_elem',  'elements')
          135  +			readList('starlit_suit_ammo',  'ammo')
          136  +			]]
          137  +		end;
          138  +	};
          139  +}
          140  +
          141  +-- TODO find a better place for this!
          142  +starlit.type.suit.purgeInventories = function(obj)
          143  +	local inv = obj:get_inventory()
          144  +	starlit.item.container.dropPrefix(inv, 'starlit_suit')
          145  +	starlit.item.container.dropPrefix(inv, 'starlit_canister')
          146  +	--[[inv:set_size('starlit_suit_bat', 0)
          147  +	inv:set_size('starlit_suit_guns', 0)
          148  +	inv:set_size('starlit_suit_chips', 0)
          149  +	inv:set_size('starlit_suit_ammo', 0)
          150  +	inv:set_size('starlit_suit_elem', 0)
          151  +	]]
          152  +end
          153  +
          154  +starlit.item.suit.foreach('starlit:suit-gen', {}, function(id, def)
          155  +	local icon = lib.image(def.img or 'starlit-item-suit.png')
          156  +
          157  +	local iconColor = def.iconColor
          158  +	if not iconColor then
          159  +		iconColor = (def.tex and def.tex.plate and def.tex.plate.tint)
          160  +			or def.defaultColor
          161  +		iconColor = iconColor:to_hsl()
          162  +		iconColor.lum = 0
          163  +	end
          164  +
          165  +	if iconColor then icon = icon:shift(iconColor) end
          166  +
          167  +	if not def.adorn then
          168  +		function def.adorn(a, item, persona)
          169  +			local function imageFor(pfx)
          170  +				return lib.image(string.format("%s-%s-%s.png", pfx, persona.species, persona.speciesVariant))
          171  +			end
          172  +			if not def.tex then return end
          173  +			a.suit = {}
          174  +			for name, t in pairs(def.tex) do
          175  +				local img = imageFor(t.id)
          176  +				local color
          177  +
          178  +				local cstr = item:get_meta():get_string('starlit:tint_suit_' .. name)
          179  +				if cstr and cstr ~= '' then
          180  +					color = lib.color.unmarshal(cstr)
          181  +				elseif t.tint then
          182  +					color = t.tint or def.defaultColor
          183  +				end
          184  +
          185  +				if color then
          186  +					local hsl = color:to_hsl()
          187  +					local adjusted = {
          188  +						hue = hsl.hue;
          189  +						sat = hsl.sat * 2 - 1;
          190  +						lum = hsl.lum * 2 - 1;
          191  +					}
          192  +					img = img:shift(adjusted)
          193  +				end
          194  +
          195  +				a.suit[name] = img
          196  +			end
          197  +		end
          198  +	end
          199  +
          200  +	minetest.register_tool(id, {
          201  +		short_description = def.name;
          202  +		description = starlit.ui.tooltip {
          203  +			title = def.name;
          204  +			desc = def.desc;
          205  +			color = lib.color(.1, .7, 1);
          206  +		};
          207  +		groups = {
          208  +			suit = 1;
          209  +			inv = 1; -- has inventories
          210  +			batteryPowered = 1; -- has a battery inv 
          211  +			programmable = 1; -- has a chip inv
          212  +		};
          213  +		on_use = function(st, luser, pointed)
          214  +			local user = starlit.activeUsers[luser:get_player_name()]
          215  +			if not user then return end
          216  +			-- have mercy on users who've lost their suits and wound
          217  +			-- up naked and dying of exposure
          218  +			if user:naked() then
          219  +				local ss = st:take_item(1)
          220  +				user:setSuit(starlit.type.suit(ss))
          221  +				user:suitSound('starlit-suit-don')
          222  +				return st
          223  +			end
          224  +		end;
          225  +		inventory_image = icon:render();
          226  +		_starlit = {
          227  +			container = {
          228  +				workbench = {
          229  +					order = {'batteries','chips','guns','ammo'}
          230  +				};
          231  +				list = {
          232  +					bat = {
          233  +						key = 'starlit:suit_slots_bat';
          234  +						accept = 'dynamo';
          235  +						sz = def.slots.batteries;
          236  +					};
          237  +					chips = {
          238  +						key = 'starlit:suit_slots_chips';
          239  +						accept = 'chip';
          240  +						sz = def.slots.chips;
          241  +					};
          242  +					canisters = {
          243  +						key = 'starlit:suit_slots_canisters';
          244  +						accept = 'canister';
          245  +						sz = def.slots.canisters;
          246  +					};
          247  +					guns = {
          248  +						key = 'starlit:suit_slots_gun';
          249  +						accept = 'weapon';
          250  +						workbench = {
          251  +							label = 'Weapon';
          252  +							icon = 'starlit-ui-icon-gun';
          253  +							color = lib.color(1,0,0);
          254  +						};
          255  +						sz = def.slots.guns;
          256  +					};
          257  +					ammo = {
          258  +						key = 'starlit:suit_slots_ammo';
          259  +						accept = 'ammo';
          260  +						workbench = {
          261  +							label = 'Ammunition';
          262  +							color = lib.color(1,.5,0);
          263  +							easySlots = true; -- all slots accessible on the go
          264  +						};
          265  +						sz = def.slots.ammo;
          266  +					};
          267  +				};
          268  +			};
          269  +			event = {
          270  +				create = function(st,how)
          271  +					local s = suitStore(st)
          272  +					-- make sure there's a defined powerstate
          273  +					starlit.type.suit(st):powerStateSet 'off'
          274  +					suitContainer(st):clear()
          275  +					--[[ populate meta tables
          276  +					s.write('batteries', {})
          277  +					s.write('guns', {})
          278  +					s.write('ammo', {})
          279  +					s.write('elements', {})
          280  +					s.write('chips', {})]]
          281  +				end;
          282  +			};
          283  +			suit = def;
          284  +		};
          285  +	});
          286  +end)
          287  +
          288  +local slotProps = {
          289  +	starlit_cfg = {
          290  +		itemClass = 'inv';
          291  +	};
          292  +	starlit_suit_bat = {
          293  +		suitSlot = true;
          294  +		powerLock = true;
          295  +		itemClass = 'dynamo';
          296  +	};
          297  +	starlit_suit_chips = {
          298  +		suitSlot = true;
          299  +		powerLock = true;
          300  +		itemClass = 'chip';
          301  +	};
          302  +	starlit_suit_guns = {
          303  +		suitSlot = true;
          304  +		maintenanceNode = '';
          305  +		itemClass = 'suitWeapon';
          306  +	};
          307  +	starlit_suit_ammo = {
          308  +		suitSlot = true;
          309  +		maintenanceNode = '';
          310  +		itemClass = 'suitAmmo';
          311  +	};
          312  +	starlit_suit_canisters = {
          313  +		suitSlot = true;
          314  +		itemClass = 'canister';
          315  +	};
          316  +}
          317  +
          318  +minetest.register_allow_player_inventory_action(function(luser, act, inv, p)
          319  +	local user = starlit.activeUsers[luser:get_player_name()]
          320  +	local function grp(i,g)
          321  +		return minetest.get_item_group(i:get_name(), g) ~= 0
          322  +	end
          323  +	local function checkBaseRestrictions(list)
          324  +		local restrictions = slotProps[list]
          325  +		if not restrictions then return nil, true end
          326  +		if restrictions.suitSlot then
          327  +			if user:naked() then return restrictions, false end
          328  +		end
          329  +		if restrictions.powerLock then
          330  +			if user:getSuit():powerState() ~= 'off' then return restrictions, false end
          331  +		end
          332  +		return restrictions, true
          333  +	end
          334  +	local function itemFits(item, list)
          335  +		local rst, ok = checkBaseRestrictions(list)
          336  +		if not ok then return false end
          337  +		if rst == nil then return true end
          338  +
          339  +		if rst.itemClass and not grp(item, rst.itemClass) then
          340  +			return false
          341  +		end
          342  +		if rst.maintenanceNode then return false end
          343  +		-- FIXME figure out best way to identify when the player is using a maintenance node
          344  +
          345  +		if grp(item, 'specialInventory') then
          346  +			if grp(item, 'powder') and list ~= 'starlit_suit_elem' then return false end
          347  +			-- FIXME handle containers
          348  +			if grp(item, 'psi') and list ~= 'starlit_psi' then return false end
          349  +		end
          350  +
          351  +		return true
          352  +	end
          353  +	local function itemCanLeave(item, list)
          354  +		local rst, ok = checkBaseRestrictions(list)
          355  +		if not ok then return false end
          356  +		if rst == nil then return true end
          357  +
          358  +		if minetest.get_item_group(item:get_name(), 'specialInventory') then
          359  +
          360  +		end
          361  +
          362  +		if rst.maintenanceNode then return false end
          363  +		return true
          364  +	end
          365  +
          366  +	if act == 'move' then
          367  +		local item = inv:get_stack(p.from_list, p.from_index)
          368  +		if not (itemFits(item, p.to_list) and itemCanLeave(item, p.from_list)) then
          369  +			return 0
          370  +		end
          371  +	elseif act == 'put' then
          372  +		if not itemFits(p.stack, p.listname) then return 0 end
          373  +	elseif act == 'take' then
          374  +		if not itemCanLeave(p.stack, p.listname) then return 0 end
          375  +	end
          376  +	return true
          377  +end)
          378  +
          379  +minetest.register_on_player_inventory_action(function(luser, act, inv, p)
          380  +	local user = starlit.activeUsers[luser:get_player_name()]
          381  +	local function slotChange(slot,a,item)
          382  +		local s = slotProps[slot]
          383  +		if slot == 'starlit_suit' then
          384  +			user:updateSuit()
          385  +			if user:naked() then
          386  +				starlit.type.suit.purgeInventories(user.entity)
          387  +				user.power.nano = {}
          388  +			end
          389  +		elseif s and s.suitSlot then
          390  +			local s = user:getSuit()
          391  +			s:onItemMove(user, slot, a, item)
          392  +			s:onReconfigure(user.entity:get_inventory())
          393  +			user:setSuit(s)
          394  +		else return end
          395  +		user:updateHUD()
          396  +	end
          397  +
          398  +	if act == 'put' or act == 'take' then
          399  +		local item = p.stack
          400  +		slotChange(p.listname, act, item)
          401  +	elseif act == 'move' then
          402  +		local item = inv:get_stack(p.to_list, p.to_index)
          403  +		slotChange(p.from_list, 'take', item)
          404  +		slotChange(p.to_list, 'put', item)
          405  +	end
          406  +end)
          407  +
          408  +local suitInterval = 2.0
          409  +starlit.startJob('starlit:suit-software', suitInterval, function(delta)
          410  +	local runState = {
          411  +		pgmsRun = {};
          412  +		flags = {};
          413  +	}
          414  +	for id, u in pairs(starlit.activeUsers) do
          415  +		if not u:naked() then
          416  +			local reconfSuit = false
          417  +			local inv = u.entity:get_inventory()
          418  +			local chips = inv:get_list('starlit_suit_chips')
          419  +			local suitprog = starlit.mod.electronics.chip.usableSoftware(chips)
          420  +			for _, prop in pairs(suitprog) do
          421  +				local s = prop.sw
          422  +				if s.kind == 'suitPower' and (s.powerKind == 'passive' or s.bgProc) and (not runState.pgmsRun[s]) then
          423  +					local conf = prop.file.body.conf
          424  +					local enabled = true
          425  +					for _, e in ipairs(conf) do
          426  +						if e.key == 'disable'  and e.value == 'yes' then
          427  +							enabled = false
          428  +							break
          429  +						end
          430  +					end
          431  +					local fn if s.powerKind == 'passive'
          432  +						then fn = s.run
          433  +						else fn = s.bgProc
          434  +					end
          435  +					function prop.saveConf(cfg) cfg = cfg or conf
          436  +						prop.fd:write(cfg)
          437  +						inv:set_stack('starlit_suit_chips', prop.chipSlot, prop.fd.chip)
          438  +						reconfSuit = true
          439  +					end
          440  +					function prop.giveItem(st)
          441  +						u:thrustUpon(st)
          442  +					end
          443  +					
          444  +					if enabled and fn(u, prop, suitInterval, runState) then
          445  +						runState.pgmsRun[s] = true
          446  +					end
          447  +				end
          448  +			end
          449  +			if reconfSuit then
          450  +				u:reconfigureSuit()
          451  +			end
          452  +		end
          453  +	end
          454  +end)
          455  +

Added mods/starlit/terrain.lua version [5a8b3b76d0].

            1  +local T = starlit.translator
            2  +local lib = starlit.mod.lib
            3  +
            4  +starlit.terrain = {}
            5  +local soilSounds = {}
            6  +local grassSounds = {}
            7  +
            8  +minetest.register_node('starlit:soil', {
            9  +	description = T 'Soil';
           10  +	tiles = {'default_dirt.png'};
           11  +	groups = {dirt = 1};
           12  +	drop = '';
           13  +	sounds = soilSounds;
           14  +	_starlit = {
           15  +		onDestroy = function() end;
           16  +		kind = 'block';
           17  +		elements = {};
           18  +	};
           19  +})
           20  +
           21  +
           22  +minetest.register_node('starlit:sand', {
           23  +	description = T 'Sand';
           24  +	tiles = {'default_sand.png'};
           25  +	groups = {dirt = 1};
           26  +	drop = '';
           27  +	sounds = soilSounds;
           28  +	_starlit = {
           29  +		kind = 'block';
           30  +		fab = starlit.type.fab { element = { silicon = 25 } };
           31  +	};
           32  +})
           33  +minetest.register_craftitem('starlit:soil_clump', {
           34  +	short_description = T 'Soil';
           35  +	description = starlit.ui.tooltip {
           36  +		title = T 'Soil';
           37  +		desc = 'A handful of nutrient-packed soil, suitable for growing plants';
           38  +		color = lib.color(0.3,0.2,0.1);
           39  +	};
           40  +	inventory_image = 'starlit-item-soil.png';
           41  +	groups = {soil = 1};
           42  +	_starlit = {
           43  +		fab = starlit.type.fab { element = { carbon = 12 / 4 } };
           44  +	};
           45  +})
           46  +
           47  +function starlit.terrain.createGrass(def)
           48  +	local function grassfst(i)
           49  +		local nextNode = def.name
           50  +		if i >= 0 then
           51  +			nextNode = nextNode .. '_walk_' .. tostring(i)
           52  +		end
           53  +		return {
           54  +			onWalk = function(pos)
           55  +				minetest.set_node_at(pos, def.name .. '_walk_2');
           56  +			end;
           57  +			onDecay = function(pos,delta)
           58  +				minetest.set_node_at(pos, nextNode);
           59  +			end;
           60  +			onDestroy = function(pos) end;
           61  +			fab = def.fab;
           62  +			recover = def.recover;
           63  +			recover_vary = def.recover_vary;
           64  +		};
           65  +	end
           66  +	local drop = {
           67  +		max_items = 4;
           68  +		items = {
           69  +			{
           70  +				items = {'starlit:soil'}, rarity = 2;
           71  +				tool_groups = { 'shovel', 'trowel' };
           72  +			};
           73  +		};
           74  +	}
           75  +	minetest.register_node(def.name, {
           76  +		description = T 'Greengraze';
           77  +		tiles = {
           78  +			def.img .. '.png';
           79  +			'default_dirt.png';
           80  +			{
           81  +				name = 'default_dirt.png^' .. def.img ..'_side.png';
           82  +				tileable_vertical = false;
           83  +			};
           84  +		};
           85  +		groups = {grass = 1, sub_walk = 1};
           86  +		drop = '';
           87  +		sounds = grassSounds;
           88  +		_starlit = grassfst(2);
           89  +	})
           90  +	for i=2,0,-1 do
           91  +		local opacity = tostring((i/2.0) * 255)
           92  +
           93  +		minetest.register_node(def.name, {
           94  +			description = def.desc;
           95  +			tiles = {
           96  +				def.img .. '.png^(default_footprint.png^[opacity:'..opacity..')';
           97  +				'default_dirt.png';
           98  +				{
           99  +					name = 'default_dirt.png^' .. def.img ..'_side.png';
          100  +					tileable_vertical = false;
          101  +				};
          102  +			};
          103  +			groups = {grass = 1, sub_walk = 1, sub_decay = 5};
          104  +			drop = '';
          105  +			_starlit = grassfst(i-1);
          106  +			sounds = grassSounds;
          107  +		})
          108  +	end
          109  +end
          110  +
          111  +
          112  +starlit.terrain.createGrass {
          113  +	name = 'starlit:greengraze';
          114  +	desc = T 'Greengraze';
          115  +	img = 'default_grass';
          116  +	fab = starlit.type.fab {
          117  +		element = {
          118  +			carbon = 12;
          119  +		};
          120  +		time = {
          121  +			shred = 2.5;
          122  +		};
          123  +	};
          124  +}
          125  +
          126  +for _, w in pairs {false,true} do
          127  +	minetest.register_node('starlit:liquid_water' .. (w and '_flowing' or ''), {
          128  +		description = T 'Water';
          129  +		drawtype = 'liquid';
          130  +		waving = 3;
          131  +		tiles = {
          132  +			{
          133  +				name = "default_water_source_animated.png";
          134  +				backface_culling = false;
          135  +				animation = {
          136  +					type = "vertical_frames";
          137  +					aspect_w = 16;
          138  +					aspect_h = 16;
          139  +					length = 2.0;
          140  +				};
          141  +			};
          142  +			{
          143  +				name = "default_water_source_animated.png";
          144  +				backface_culling = true;
          145  +				animation = {
          146  +					type = "vertical_frames";
          147  +					aspect_w = 16;
          148  +					aspect_h = 16;
          149  +					length = 2.0;
          150  +				};
          151  +			};
          152  +		};
          153  +		use_texture_alpha = 'blend';
          154  +		paramtype = 'light';
          155  +		walkable = false, pointable = "blocking", diggable = false, buildable_to = true;
          156  +		is_ground_content = false;
          157  +		drop = '';
          158  +		drowning = 1;
          159  +		liquidtype = w and 'flowing' or 'source';
          160  +		liquid_alternative_flowing = 'starlit:liquid_water_flowing';
          161  +		liquid_alternative_source = 'starlit:liquid_water';
          162  +		liquid_viscosity = 1;
          163  +		liquid_renewable = true;
          164  +		liquid_range = 2;
          165  +		drowning = 40;
          166  +		post_effect_color = {a=103, r=10, g=40, b=70};
          167  +		groups = {water = 3, liquid = 3};
          168  +	});
          169  +end
          170  +
          171  +
          172  +starlit.world.mineral.foreach('starlit:mineral_generate', {}, function(name,m)
          173  +	local node = string.format('starlit:mineral_%s', name)
          174  +	local grp = {mineral = 1}
          175  +	minetest.register_node(node, {
          176  +		description = m.desc;
          177  +		tiles = m.tiles or 
          178  +				(m.tone and {
          179  +					string.format('default_stone.png^[colorizehsl:%s:%s:%s',
          180  +						m.tone.hue, m.tone.sat, m.tone.lum)
          181  +				}) or {'default_stone.png'};
          182  +		groups = grp;
          183  +		drop = m.rocks or '';
          184  +		_starlit = {
          185  +			kind = 'block';
          186  +			elements = m.elements;
          187  +			fab = m.fab;
          188  +			recover = m.recover;
          189  +			recover_vary = m.recover_vary;
          190  +		};
          191  +	})
          192  +	if not m.excludeOre then
          193  +		local seed = 0
          194  +		grp.ore = 1
          195  +		for i = 1, #m.name do
          196  +			seed = seed*50 + string.byte(name, i)
          197  +		end
          198  +		minetest.register_ore {
          199  +			ore = node;
          200  +			ore_type = m.dist.kind;
          201  +			wherein = {m.dist.among};
          202  +			clust_scarcity = m.dist.rare;
          203  +			y_max = m.dist.height[1], y_min = m.dist.height[2];
          204  +			noise_params = m.dist.noise or {
          205  +				offset = 28;
          206  +				scale = 16;
          207  +				spread = vector.new(128,128,128);
          208  +				seed = seed;
          209  +				octaves = 1;
          210  +			};
          211  +		}
          212  +	end
          213  +end)
          214  +
          215  +starlit.world.mineral.link('feldspar', {
          216  +	desc = T 'Feldspar';
          217  +	excludeOre = true;
          218  +	recover = starlit.type.fab {
          219  +		time = {
          220  +			shred = 3;
          221  +		};
          222  +		cost = {
          223  +			shredPower = 3;
          224  +		};
          225  +	};
          226  +	recover_vary = function(rng, ctx)
          227  +		-- print('vary!', rng:int(), rng:int(0,10))
          228  +		return starlit.type.fab {
          229  +			element = {
          230  +				aluminum  = rng:int(0,4);
          231  +				potassium = rng:int(0,2);
          232  +				calcium   = rng:int(0,2);
          233  +			}
          234  +		};
          235  +	end;
          236  +})
          237  +
          238  +-- map generation
          239  +
          240  +minetest.register_alias('mapgen_stone', 'starlit:mineral_feldspar')
          241  +minetest.register_alias('mapgen_water_source', 'starlit:liquid_water')
          242  +minetest.register_alias('mapgen_river_water_source', 'starlit:liquid_water')
          243  +

Added mods/starlit/tiers.lua version [513c94b946].

            1  +local lib = starlit.mod.lib
            2  +
            3  +starlit.world.tier = lib.registry.mk 'starlit:tier'
            4  +local T = starlit.world.tier
            5  +local fab = starlit.type.fab
            6  +
            7  +function starlit.world.tier.fabsum(name, ty)
            8  +	local dest = fab {}
            9  +	local t = starlit.world.tier.db[name]
           10  +	assert(t, 'reference to nonexisting tier '..name)
           11  +	if t.super then
           12  +		dest = dest+starlit.world.tier.fabsum(t.super, ty)*(t.cost or 1)
           13  +	end
           14  +	if t.fabclasses and t.fabclasses[ty] then
           15  +		dest = dest + t.fabclasses[ty]
           16  +	end
           17  +	return dest
           18  +end
           19  +
           20  +function starlit.world.tier.tech(name, tech)
           21  +	local t = starlit.world.tier.db[name]
           22  +	if t.techs and t.techs[tech] ~= nil then return t.techs[tech] end
           23  +	if t.super then return starlit.world.tier.tech(t.super, tech) end
           24  +	return false
           25  +end
           26  +
           27  +T.meld {
           28  +	base = {
           29  +		fabclass = {
           30  +			electric = fab {metal={copper = 10}};
           31  +			suit = fab {element={carbon = 1e3}};
           32  +			psi = fab {metal={numinium = 1}};
           33  +			bio = fab {element={carbon = 1}};
           34  +		};
           35  +
           36  +	}; -- properties that apply to all tiers
           37  +	------------------
           38  +	-- tier classes --
           39  +	------------------
           40  +
           41  +	lesser = {
           42  +		name = 'Lesser', adj = 'Lesser';
           43  +		super = 'base';
           44  +		fabclasses = {
           45  +			basis = fab {
           46  +				metal = {aluminum=4};
           47  +			};
           48  +		};
           49  +	};
           50  +	greater = {
           51  +		name = 'Greater', adj = 'Greater';
           52  +		super = 'base';
           53  +		fabclasses = {
           54  +			basis = fab {
           55  +				metal = {vanadium=2};
           56  +			};
           57  +		};
           58  +	};
           59  +	starlit = {
           60  +		name = 'Starsoul', adj = 'Starsoul';
           61  +		super = 'base';
           62  +		fabclasses = {
           63  +			basis = fab {
           64  +				metal = {osmiridium=1};
           65  +			};
           66  +		};
           67  +	};
           68  +	forevanished = {
           69  +		name = 'Forevanished One', adj = 'Forevanished';
           70  +		super = 'base';
           71  +		fabclasses = {
           72  +			basis = fab {
           73  +				metal = {elusium=1};
           74  +			};
           75  +		};
           76  +	};
           77  +
           78  +	------------------
           79  +	-- Lesser Races --
           80  +	------------------
           81  +
           82  +	makeshift = { -- regular trash
           83  +		name = 'Makeshift', adj = 'Makeshift';
           84  +		super = 'lesser';
           85  +		techs = {tool = true, prim = true, electric = true};
           86  +		power = 0.5;
           87  +		efficiency = 0.3;
           88  +		reliability = 0.2;
           89  +		cost = 0.3;
           90  +		fabclasses = { -- characteristic materials
           91  +			basis = fab { -- fallback
           92  +				metal = {iron=3};
           93  +			};
           94  +		};
           95  +	};
           96  +
           97  +	imperial = { --powerful trash
           98  +		name = 'Imperial', adj = 'Imperial';
           99  +		super = 'lesser';
          100  +		techs = {tool = true, electric = true, electronic = true, suit = true, combatSuit = true, weapon = true, hover='ion'};
          101  +		power = 2.0;
          102  +		efficiency = 0.5;
          103  +		reliability = 0.5;
          104  +		cost = 1.0;
          105  +		fabclasses = {
          106  +			basis = fab {
          107  +				metal = {steel=2};
          108  +			};
          109  +		};
          110  +	};
          111  +
          112  +	commune = { --reliability
          113  +		name = 'Commune', adj = 'Commune';
          114  +		super = 'lesser';
          115  +		techs = {tool = true, electric = true, electronic = true, suit = true, combatSuit = true, weapon = true, gravitic = true, hover='grav'};
          116  +		power = 1.0;
          117  +		efficiency = 2.0;
          118  +		reliability = 3.0;
          119  +		cost = 1.5;
          120  +		fabclasses = {
          121  +			basis = fab {
          122  +				metal = {titanium=1};
          123  +				time = {print = 1.2}; -- commune stuff is intricate
          124  +			};
          125  +		};
          126  +	};
          127  +
          128  +	-------------------
          129  +	-- Greater Races --
          130  +	-------------------
          131  +
          132  +
          133  +	----------------
          134  +	-- Starsouled --
          135  +	----------------
          136  +
          137  +	suIkuri = { --super-tier
          138  +		name = 'Su\'ikuri', adj = "Su'ikuruk";
          139  +		super = 'starlit';
          140  +		techs = {psi = true, prim = true, bioSuit = true, psiSuit = true};
          141  +		power = 1.5;
          142  +		efficiency = 1.0;
          143  +		reliability = 3.0;
          144  +		cost = 2.0;
          145  +		fabclasses = {
          146  +			psi = fab {
          147  +				metal = {numinium = 2.0};
          148  +				crystal = {beryllium = 1.0};
          149  +			};
          150  +			bio = fab {
          151  +				crystal = {beryllium = 1.0};
          152  +			};
          153  +		};
          154  +	};
          155  +
          156  +	usukwinya = { --value for 'money'; no weapons; no hovertech (they are birds)
          157  +		-- NOTA BENE: the ususkwinya *do* have weapons of their own; however,
          158  +		-- they are extremely restricted and never made available except to a
          159  +		-- very select number of that species. consequently, usuk players
          160  +		-- of a certain scenario may have usuk starting weapons, but these must
          161  +		-- be manually encoded to avoid injecting them into the overall crafting
          162  +		-- /loot system. because there are so few of these weapons in existence,
          163  +		-- all so tightly controlled, the odds of the weapons or plans winding
          164  +		-- up on Farthest Shadow are basically zero unless you bring them yourself
          165  +		name = 'Usukwinya', adj = 'Usuk';
          166  +		super = 'starlit';
          167  +		techs = lib.tbl.set('tool', 'electric', 'electronic', 'suit', 'gravitic');
          168  +		power = 2.0;
          169  +		efficiency = 2.0;
          170  +		reliability = 2.0;
          171  +		cost = 0.5;
          172  +		fabclasses = {
          173  +			basis = fab {
          174  +				crystal = {aluminum = 5}; -- ruby
          175  +			};
          176  +		};
          177  +	};
          178  +
          179  +	eluthrai = { --super-tier
          180  +		name = 'Eluthrai', adj = 'Eluthran';
          181  +		super = 'starlit';
          182  +		techs = {tool = true, electric = true, electronic = true, weapon = true, gravitic = true, gravweapon = true, suit = true, combatSuit = true, hover = 'grav'};
          183  +		power = 4.0;
          184  +		efficiency = 4.0;
          185  +		reliability = 4.0;
          186  +		cost = 4.0;
          187  +		fabclasses = {
          188  +			basis = fab {
          189  +				crystal = {carbon = 5}; -- diamond
          190  +			};
          191  +			special	= fab {
          192  +				metal = {technetium=1, cinderstone=1}
          193  +			};
          194  +		};
          195  +	};
          196  +
          197  +	-----------------------
          198  +	-- Forevanished Ones --
          199  +	-----------------------
          200  +
          201  +	firstborn = { --god-tier
          202  +		name = 'Firstborn', adj = 'Firstborn';
          203  +		super = 'forevanished';
          204  +		techs = {tool = true, electric = true, electronic = true, suit = true, psi = true, combatSuit = true, weapon = true, gravitic = true, gravweapon = true};
          205  +		power = 10.0;
          206  +		efficiency = 5.0;
          207  +		reliability = 3.0;
          208  +		cost = 10.0;
          209  +		fabclasses = {
          210  +			basis = fab {
          211  +				metal = {technetium=2, neodymium=3, sunsteel=1};
          212  +				crystal = {astrite=1};
          213  +			};
          214  +		};
          215  +	};
          216  +
          217  +	forevanisher = { --godslayer-tier
          218  +		name = 'Forevanisher', adj = 'Forevanisher';
          219  +		super = 'forevanished';
          220  +		techs = {tool = true, electric = true, electronic = true, suit = true, psi = true, combatSuit = true, weapon = true, gravitic = true, gravweapon = true};
          221  +		power = 20.0;
          222  +		efficiency = 1.0;
          223  +		reliability = 2.0;
          224  +		cost = 100.0;
          225  +		fabclasses = {
          226  +			basis = fab {
          227  +				metal = {};
          228  +				crystal = {};
          229  +			};
          230  +		};
          231  +	};
          232  +
          233  +}

Added mods/starlit/ui.lua version [7085b387cd].

            1  +local lib = starlit.mod.lib
            2  +
            3  +starlit.ui = {}
            4  +
            5  +starlit.type.ui = lib.class {
            6  +	name = 'starlit:ui';
            7  +	__index = {
            8  +		action = function(self, user, state, fields)
            9  +			local pg = self.pages[state.page or 'index']
           10  +			if not pg then return end
           11  +			if pg.handle then
           12  +				local redraw, reset = pg.handle(state, user, fields)
           13  +				if reset then pg.setupState(state,user) end
           14  +				if redraw then self:show(user) end
           15  +			end
           16  +			if fields.quit then self:cb('onClose', user) end
           17  +		end;
           18  +		cb = function(self, name, user, ...)
           19  +			local state = self:begin(user)
           20  +			if self[name] then self[name](state, user, ...) end
           21  +			local pcb = self.pages[state.page][name] 
           22  +			if pcb then pcb(state, user, ...) end
           23  +		end;
           24  +		begin = function(self, user, page, ...)
           25  +			local state = starlit.activeUI[user.name]
           26  +			if state and state.form ~= self.id then
           27  +				state = nil
           28  +				starlit.activeUI[user.name] = nil
           29  +			end
           30  +			local created = state == nil
           31  +
           32  +			if not state then
           33  +				state = {
           34  +					page = page or 'index';
           35  +					form = self.id;
           36  +				}
           37  +				starlit.activeUI[user.name] = state
           38  +				self:cb('setupState', user, ...)
           39  +			elseif page ~= nil and state.page ~= page then
           40  +				state.page = page
           41  +				local psetup = self.pages[state.page].setupState
           42  +				if psetup then psetup(state,user, ...) end
           43  +			end
           44  +			return state, created
           45  +		end;
           46  +		render = function(self, state, user)
           47  +			return self.pages[state.page].render(state, user)
           48  +		end;
           49  +		show = function(self, user)
           50  +			local state = self:begin(user)
           51  +			minetest.show_formspec(user.name, self.id,self:render(state, user))
           52  +		end;
           53  +		open = function(self, user, page, ...)
           54  +			user:suitSound 'starlit-nav'
           55  +			self:begin(user, page, ...)
           56  +			self:show(user)
           57  +		end;
           58  +		close = function(self, user)
           59  +			local state = starlit.activeUI[user.name]
           60  +			if state and state.form == self.id then
           61  +				self:cb('onClose', user)
           62  +				starlit.activeUI[user.name] = nil
           63  +				minetest.close_formspec(user.name, self.id)
           64  +			end
           65  +		end;
           66  +	};
           67  +	construct = function(p)
           68  +		if not p.id then error('UI missing id') end
           69  +		p.pages = p.pages or {}
           70  +		return p
           71  +	end;
           72  +}
           73  +
           74  +function starlit.interface.install(ui)
           75  +	starlit.interface.link(ui.id, ui)
           76  +end
           77  +
           78  +function starlit.ui.build(def, parent)
           79  +	local clr = def.color
           80  +	if clr and lib.color.id(clr) then
           81  +		clr = clr:to_hsl_o()
           82  +	end
           83  +	local state = {
           84  +		x = (def.x or 0);
           85  +		y = (def.y or 0);
           86  +		w = def.w or 0, h = def.h or 0;
           87  +		fixed = def.fixed or false;
           88  +		spacing = def.spacing or 0;
           89  +		padding = def.padding or 0;
           90  +		align = def.align or (parent and parent.align) or 'left';
           91  +		lines = {};
           92  +		mode = def.mode or (parent and parent.mode or nil); -- hw or sw
           93  +		gen = (parent and parent.gen or 0) + 1;
           94  +		fg = def.fg or (parent and parent.fg);
           95  +		color = clr or (parent and parent.color) or {
           96  +			hue = 260, sat = 0, lum = 0
           97  +		};
           98  +	}
           99  +	local lines = state.lines
          100  +	local cmod = string.format('^[hsl:%s:%s:%s',
          101  +		state.color.hue, state.color.sat*0xff, state.color.lum*0xff)
          102  +
          103  +	local E = minetest.formspec_escape
          104  +	if state.padding/2 > state.x then state.x = state.padding/2 end
          105  +	if state.padding/2 > state.y then state.y = state.padding/2 end
          106  +
          107  +	local function btnColorDef(sel)
          108  +		local function climg(state,img)
          109  +			local selstr
          110  +			if sel == nil then
          111  +				selstr = string.format(
          112  +					'button%s,' ..
          113  +					'button_exit%s,' ..
          114  +					'image_button%s,' ..
          115  +					'item_image_button%s',
          116  +					state, state, state, state)
          117  +			else
          118  +				selstr = E(sel) .. state
          119  +			end
          120  +
          121  +			return string.format('%s[%s;' ..
          122  +					'bgimg=%s;'               ..
          123  +					'bgimg_middle=16;'        ..
          124  +					'content_offset=0,0'      ..
          125  +				']', sel and 'style' or 'style_type',
          126  +				selstr, E(img..'^[resize:48x48'..cmod))
          127  +		end
          128  +
          129  +		return climg('',         'starlit-ui-button-sw.png')       ..
          130  +		       climg(':hovered', 'starlit-ui-button-sw-hover.png') ..
          131  +		       climg(':pressed', 'starlit-ui-button-sw-press.png')
          132  +	end
          133  +	local function widget(...)
          134  +		table.insert(lines, string.format(...))
          135  +	end
          136  +	if def.kind == 'vert' then
          137  +		for _, w in ipairs(def) do
          138  +			local src, st = starlit.ui.build(w, state)
          139  +			widget('container[%s,%s]%scontainer_end[]', state.x, state.y, src)
          140  +			state.y=state.y + state.spacing + st.h
          141  +			state.w = math.max(state.w, st.w)
          142  +		end
          143  +		state.w = state.w + state.padding
          144  +		state.h = state.y + state.padding/2
          145  +	elseif def.kind == 'hztl' then
          146  +		for _, w in ipairs(def) do
          147  +			local src, st = starlit.ui.build(w, state)
          148  +			widget('container[%s,%s]%scontainer_end[]', state.x, state.y, src)
          149  +			-- TODO alignments
          150  +			state.x=state.x + state.spacing + st.w
          151  +			state.h = math.max(state.h, st.h)
          152  +		end
          153  +		state.h = state.h + state.padding
          154  +		state.w = state.x + state.padding/2
          155  +	elseif def.kind == 'list' then
          156  +		local slotTypes = {
          157  +			plain = {hue = 200, sat = -.1, lum = 0};
          158  +			element = {hue = 20, sat = -.3, lum = 0};
          159  +			chip = {hue = 0, sat = -1, lum = 0};
          160  +			psi = {hue = 300, sat = 0, lum = 0};
          161  +			power = {hue = 50, sat = 0, lum = .2};
          162  +		}
          163  +		local img
          164  +		if state.mode == 'hw' then
          165  +			img = lib.image('starlit-ui-slot-physical.png');
          166  +		else
          167  +			img = lib.image('starlit-ui-slot.png'):shift(slotTypes[def.listContent or 'plain']);
          168  +		end
          169  +		local spac = state.spacing
          170  +		widget('style_type[list;spacing=%s,%s]',spac,spac)
          171  +		assert(def.w and def.h, 'ui-lists require a fixed size')
          172  +		for lx = 0, def.w-1 do
          173  +		for ly = 0, def.h-1 do
          174  +			local ox, oy = state.x + lx*(1+spac), state.y + ly*(1+spac)
          175  +			table.insert(lines, string.format('image[%s,%s;1.1,1.1;%s]', ox-0.05,oy-0.05, img:render()))
          176  +		end end
          177  +		table.insert(lines, string.format('listcolors[#00000000;#ffffff10]')) -- FIXME
          178  +		table.insert(lines, string.format('list[%s;%s;%s,%s;%s,%s;%s]',
          179  +			E(def.target), E(def.inv),
          180  +			state.x, state.y,
          181  +			def.w,   def.h,
          182  +			def.idx))
          183  +		local sm = 1
          184  +		state.w = def.w * sm + (spac * (def.w - 1))
          185  +		state.h = def.h * sm + (spac * (def.h - 1))
          186  +	elseif def.kind == 'contact' then
          187  +		if def.color then table.insert(lines, btnColorDef(def.id)) end
          188  +		widget('image_button%s[%s,%s;%s,%s;%s;%s;%s]',
          189  +			def.close and '_exit' or '',
          190  +			state.x, state.y, def.w, def.h,
          191  +			E(def.img), E(def.id), E(def.label or ''))
          192  +	elseif def.kind == 'button' then
          193  +		if def.color then table.insert(lines, btnColorDef(def.id)) end
          194  +		local label = E(def.label or '')
          195  +		if state.fg then label = lib.color(state.fg):fmt(label) end
          196  +		widget('button%s[%s,%s;%s,%s;%s;%s]',
          197  +			def.close and '_exit' or '',
          198  +			state.x, state.y, def.w, def.h,
          199  +			E(def.id), label)
          200  +	elseif def.kind == 'img' then
          201  +		widget('%s[%s,%s;%s,%s;%s]',
          202  +			def.item and 'item_image' or 'image',
          203  +			state.x, state.y, def.w, def.h, E(def.item or def.img))
          204  +	elseif def.kind == 'label' then
          205  +		local txt = E(def.text)
          206  +		if state.fg then txt = lib.color(state.fg):fmt(txt) end
          207  +		widget('label[%s,%s;%s]',
          208  +			state.x, state.y + def.h*.5, txt)
          209  +	elseif def.kind == 'text' then
          210  +		-- TODO paragraph formatter
          211  +		widget('hypertext[%s,%s;%s,%s;%s;%s]',
          212  +			state.x, state.y, def.w, def.h, E(def.id), E(def.text))
          213  +	elseif def.kind == 'hbar' or def.kind == 'vbar' then -- TODO fancy image bars
          214  +		local cl = lib.color(state.color)
          215  +		local fg = state.fg or cl:readable(.8,1)
          216  +		local wfac, hfac = 1,1
          217  +		local clamp = math.min(math.max(def.fac, 0), 1)
          218  +		if def.kind == 'hbar'
          219  +			then wfac = wfac * clamp
          220  +			else hfac = hfac * clamp
          221  +		end
          222  +		local x,y, w,h = state.x, state.y, def.w, def.h
          223  +		widget('box[%s,%s;%s,%s;%s]',
          224  +			x,y, w,h, cl:brighten(0.2):hex())
          225  +		widget('box[%s,%s;%s,%s;%s]',
          226  +			x, y + (h*(1-hfac)), w * wfac, h * hfac, cl:hex())
          227  +		if def.text then
          228  +			widget('hypertext[%s,%s;%s,%s;;%s]',
          229  +				state.x, state.y, def.w, def.h,
          230  +				string.format('<global halign=center valign=middle color=%s>%s', fg:hex(), E(def.text)))
          231  +		end
          232  +	end
          233  +
          234  +	if def.desc then
          235  +		widget('tooltip[%s,%s;%s,%s;%s]',
          236  +			state.x, state.y, def.w, def.h, E(def.desc))
          237  +	end
          238  +
          239  +	local originX = (parent and parent.x or 0)
          240  +	local originY = (parent and parent.y or 0)
          241  +	local l = table.concat(lines)
          242  +	-- if state.fixed and (state.w < state.x or state.h < state.y) then
          243  +	-- 	l = string.format('scroll_container[%s,%s;%s,%s;scroll_%s;%s]%sscroll_container_end[]',
          244  +	-- 		(parent and parent.x or 0), (parent and parent.y or 0),
          245  +	-- 		state.w, state.h, state.gen,
          246  +	-- 		(state.x > state.w) and 'horizontal' or 'vertical', l)
          247  +	-- end
          248  +	
          249  +
          250  +	if def.mode or def.container then
          251  +		if def.mode then
          252  +			l = string.format('background9[%s,%s;%s,%s;%s;false;64]',
          253  +					originX, originY, state.w, state.h,
          254  +					E(string.format('starlit-ui-bg-%s.png%s^[resize:128x128',
          255  +						(def.mode == 'sw') and 'digital'
          256  +											or 'panel', cmod))) .. l
          257  +		end
          258  +		if parent == nil or state.color ~= parent.color then
          259  +			l = btnColorDef() .. l
          260  +		end
          261  +	end
          262  +	if not parent then
          263  +		return string.format('formspec_version[6]size[%s,%s]%s', state.w, state.h, l), state
          264  +	else
          265  +		return l, state
          266  +	end
          267  +end
          268  +
          269  +starlit.ui.tooltip = lib.ui.tooltipper {
          270  +	colors = {
          271  +		-- generic notes
          272  +		neutral = lib.color(.5,.5,.5);
          273  +		good    = lib.color(.2,1,.2);
          274  +		bad     = lib.color(1,.2,.2);
          275  +		info    = lib.color(.4,.4,1);
          276  +		-- chip notes
          277  +		schemaic = lib.color(.2,.7,1);
          278  +		ability  = lib.color(.7,.2,1);
          279  +		driver   = lib.color(1,.7,.2);
          280  +	};
          281  +}

Added mods/starlit/user.lua version [aee7410825].

            1  +-- [ʞ] user.lua
            2  +--  ~ lexi hale <lexi@hale.su>
            3  +--  © EUPL v1.2
            4  +--  ? defines the starlit.type.user class, which is
            5  +--    the main interface between the game world and the
            6  +--    client. it provides for initial signup and join,
            7  +--    managing the HUD, skinning the player model,
            8  +--    effecting weather changes, etc.
            9  +
           10  +local lib = starlit.mod.lib
           11  +
           12  +local function hudAdjustBacklight(img)
           13  +	local night = math.abs(minetest.get_timeofday() - .5) * 2
           14  +	local opacity = night*0.8
           15  +	return img:fade(opacity)
           16  +end
           17  +
           18  +local userStore = lib.marshal.metaStore {
           19  +	persona = {
           20  +		key  = 'starlit:persona';
           21  +		type = starlit.store.persona;
           22  +	};
           23  +}
           24  +
           25  +local suitStore = starlit.store.suitMeta
           26  +
           27  +starlit.type.user = lib.class {
           28  +	name = 'starlit:user';
           29  +	construct = function(ident)
           30  +		local name, luser
           31  +		if type(ident) == 'string' then
           32  +			name = ident
           33  +			luser = minetest.get_player_by_name(name)
           34  +		else
           35  +			luser = ident
           36  +			name = luser:get_player_name()
           37  +		end
           38  +		return {
           39  +			entity = luser;
           40  +			name = name;
           41  +			hud = {
           42  +				elt = {};
           43  +			};
           44  +			tree = {};
           45  +			action = {
           46  +				bits = 0; -- for control deltas
           47  +				prog = {}; -- for recording action progress on a node; reset on refocus
           48  +				tgt = {type='nothing'};
           49  +				sfx = {};
           50  +				fx = {};
           51  +			};
           52  +			actMode = 'off';
           53  +			power = {
           54  +				nano = {primary = nil, secondary = nil};
           55  +				weapon = {primary = nil, secondary = nil};
           56  +				psi = {primary = nil, secondary = nil};
           57  +				maneuver = nil;
           58  +			};
           59  +			pref = {
           60  +				calendar = 'commune';
           61  +			};
           62  +		}
           63  +	end;
           64  +	__index = {
           65  +		pullPersona = function(self)
           66  +			-- if later records are added in public updates, extend this function to merge them
           67  +			-- into one object
           68  +			local s = userStore(self.entity)
           69  +			self.persona = s.read 'persona'
           70  +		end;
           71  +		pushPersona = function(self)
           72  +			local s = userStore(self.entity)
           73  +			s.write('persona', self.persona)
           74  +		end;
           75  +		uiColor = function(self) return lib.color {hue=238,sat=.5,lum=.5} end;
           76  +		statDelta = function(self, stat, d, cause, abs)
           77  +			local dt = self.persona.statDeltas
           78  +			local base
           79  +			if abs then
           80  +				local min, max
           81  +				min, max, base = self:statRange(stat)
           82  +				if     d == true  then d = max
           83  +				elseif d == false then d = min end
           84  +			end
           85  +			if stat == 'health' then
           86  +				self.entity:set_hp(abs and d or (self.entity:get_hp() + d), cause)
           87  +			elseif stat == 'breath' then
           88  +				self.entity:set_breath(abs and d or (self.entity:get_breath() + d))
           89  +			else
           90  +				if abs then
           91  +					dt[stat] = d - base
           92  +				else
           93  +					dt[stat] = dt[stat] + d
           94  +				end
           95  +				self:pushPersona()
           96  +			end
           97  +			self:updateHUD()
           98  +			-- TODO trigger relevant animations?
           99  +		end;
          100  +		lookupSpecies = function(self)
          101  +			return starlit.world.species.lookup(self.persona.species, self.persona.speciesVariant)
          102  +		end;
          103  +		phenoTrait = function(self, trait)
          104  +			local s,v = self:lookupSpecies()
          105  +			return v.traits[trait] or s.traits[trait] or 0
          106  +		end;
          107  +		statRange = function(self, stat) --> min, max, base
          108  +			return starlit.world.species.statRange(
          109  +				self.persona.species, self.persona.speciesVariant, stat)
          110  +		end;
          111  +		effectiveStat = function(self, stat)
          112  +			local val
          113  +			local min, max, base = self:statRange(stat)
          114  +
          115  +			if stat == 'health' then
          116  +				val = self.entity:get_hp()
          117  +			elseif stat == 'breath' then
          118  +				val = self.entity:get_breath()
          119  +			else
          120  +				val = base + self.persona.statDeltas[stat] or 0
          121  +			end
          122  +
          123  +			local d = max - min
          124  +			return val, (val - min) / d
          125  +		end;
          126  +		damageModifier = function(self, kind, amt)
          127  +			if kind == 'bluntForceTrauma' then
          128  +				local std = self:phenoTrait 'sturdiness'
          129  +				if std < 0 then
          130  +					amt = amt / 1+std
          131  +				else
          132  +					amt = amt * 1-std
          133  +				end
          134  +			end
          135  +			return amt
          136  +		end;
          137  +		attachImage = function(self, def)
          138  +			local user = self.entity
          139  +			local img = {}
          140  +			img.id = user:hud_add {
          141  +				type = 'image';
          142  +				text = def.tex;
          143  +				scale = def.scale;
          144  +				alignment = def.align;
          145  +				position = def.pos;
          146  +				offset = def.ofs;
          147  +				z_index = def.z;
          148  +			}
          149  +			if def.update then
          150  +				img.update = function()
          151  +					def.update(user, function(prop, val)
          152  +						user:hud_change(img.id, prop, val)
          153  +					end, def)
          154  +				end
          155  +			end
          156  +			return img
          157  +		end;
          158  +		attachMeter = function(self, def)
          159  +			local luser = self.entity
          160  +			local m = {}
          161  +			local w = def.size or 80
          162  +			local szf = w / 80
          163  +			local h = szf * 260
          164  +			m.meter = luser:hud_add {
          165  +				type = 'image';
          166  +				scale = {x = szf, y = szf};
          167  +				alignment = def.align;
          168  +				position = def.pos;
          169  +				offset = def.ofs;
          170  +				z_index = def.z or 0;
          171  +			}
          172  +			local cx = def.ofs.x + (w/2)*def.align.x
          173  +			local cy = def.ofs.y + (h/2)*def.align.y
          174  +			local oy = cy + h/2 - 42
          175  +			-- this is so fucking fragile holy fuck
          176  +			m.readout = luser:hud_add {
          177  +				type = 'text';
          178  +				scale = {x = w, y = h};
          179  +				size = szf;
          180  +				style = 4;
          181  +				position = def.pos;
          182  +				alignment = {x=0,0};
          183  +				offset = {x = cx, y = oy};
          184  +				z_index = (def.z or 0)+1;
          185  +				number = 0xffffff;
          186  +			}
          187  +			m.destroy = function()
          188  +				luser:hud_remove(m.meter)
          189  +				luser:hud_remove(m.readout)
          190  +			end
          191  +			m.update = function()
          192  +				local v,txt,color,txtcolor = def.measure(luser,def)
          193  +				v = math.max(0, math.min(1, v))
          194  +				local n = math.floor(v*16) + 1
          195  +				local img = hudAdjustBacklight(lib.image('starlit-ui-meter.png'))
          196  +					:colorize(color or def.color)
          197  +				if def.flipX then
          198  +					img = img:transform 'FX'
          199  +				end
          200  +				img = img:render()
          201  +				img = img .. '^[verticalframe:17:' .. tostring(17 - n)
          202  +				luser:hud_change(m.meter, 'text', img)
          203  +				if txt then
          204  +					luser:hud_change(m.readout, 'text', txt)
          205  +				end
          206  +				if txtcolor then
          207  +					luser:hud_change(m.readout, 'number', txtcolor:hex())
          208  +				end
          209  +			end
          210  +			return m
          211  +		end;
          212  +		attachTextBox = function(self, def)
          213  +			local luser = self.entity
          214  +			local box = {}
          215  +			box.id = luser:hud_add {
          216  +				type = 'text';
          217  +				text = '';
          218  +				alignment = def.align;
          219  +				number = def.color and def.color:int24() or 0xFFffFF;
          220  +				scale = def.bound;
          221  +				size = {x = def.size, y=0};
          222  +				style = def.style;
          223  +				position = def.pos;
          224  +				offset = def.ofs;
          225  +			}
          226  +			box.update = function()
          227  +				local text, color = def.text(self, box, def)
          228  +				luser:hud_change(box.id, 'text', text)
          229  +				if color then
          230  +					luser:hud_change(box.id, 'number', color:int24())
          231  +				end
          232  +			end
          233  +			return box
          234  +		end;
          235  +		attachStatBar = function(self, def)
          236  +			local luser = self.entity
          237  +			local bar = {}
          238  +			local img = lib.image 'starlit-ui-bar.png'
          239  +			local colorized = img
          240  +			if type(def.color) ~= 'function' then
          241  +				colorized = colorized:shift(def.color)
          242  +			end
          243  +
          244  +			bar.id = luser:hud_add {
          245  +				type = 'statbar';
          246  +				position = def.pos;
          247  +				offset = def.ofs;
          248  +				name = def.name;
          249  +				text = colorized:render();
          250  +				text2 = img:tint{hue=0, sat=-1, lum = -0.5}:fade(0.5):render();
          251  +				number = def.size;
          252  +				item = def.size;
          253  +				direction = def.dir;
          254  +				alignment = def.align;
          255  +				size = {x=4,y=24};
          256  +			}
          257  +			bar.update = function()
          258  +				local sv, sf = def.stat(self, bar, def)
          259  +				luser:hud_change(bar.id, 'number', def.size * sf)
          260  +				if type(def.color) == 'function' then
          261  +					local clr = def.color(sv, luser, sv, sf)
          262  +					luser:hud_change(bar.id, 'text', img:tint(clr):render())
          263  +				end
          264  +			end
          265  +			return bar, {x=3 * def.size, y=16} -- x*2??? what
          266  +		end;
          267  +		createHUD = function(self)
          268  +			local function basicStat(statName)
          269  +				return function(user, bar)
          270  +					return self:effectiveStat(statName)
          271  +				end
          272  +			end
          273  +			local function batteryLookup(user)
          274  +				local max = user:suitPowerCapacity()
          275  +				if max == 0 then return 0, 0 end
          276  +				local ch = user:suitCharge()
          277  +				return (ch/max)*100, ch/max
          278  +			end
          279  +			local function C(h,s,l) return {hue=h,sat=s,lum=l} end
          280  +			local hbofs = (1+self.entity:hud_get_hotbar_itemcount()) * 25
          281  +			local bpad = 8
          282  +			self.hud.elt.health = self:attachStatBar {
          283  +				name = 'health', stat = basicStat 'health';
          284  +				color = C(340,0,.3), size = 100;
          285  +				pos = {x=0.5, y=1}, ofs = {x = -hbofs, y=-48 - bpad};
          286  +				dir = 1;
          287  +				align = {x=-1, y=-1};
          288  +			}
          289  +			self.hud.elt.stamina = self:attachStatBar {
          290  +				name = 'stamina', stat = basicStat 'stamina';
          291  +				color = C(60,0,.2), size = 100;
          292  +				pos = {x=0.5, y=1}, ofs = {x = -hbofs, y=-24 - bpad};
          293  +				dir = 1;
          294  +				align = {x=-1, y=-1};
          295  +			}
          296  +			self.hud.elt.bat = self:attachStatBar {
          297  +				name = 'battery', stat = batteryLookup;
          298  +				color = C(190,0,.2), size = 100;
          299  +				pos = {x=0.5, y=1}, ofs = {x = hbofs - 4, y=-48 - bpad};
          300  +				dir = 0;
          301  +				align = {x=1, y=-1};
          302  +			}
          303  +			self.hud.elt.psi = self:attachStatBar {
          304  +				name = 'psi', stat = basicStat 'psi';
          305  +				color = C(320,0,.2), size = 100;
          306  +				pos = {x=0.5, y=1}, ofs = {x = hbofs - 4, y=-24 - bpad};
          307  +				dir = 0;
          308  +				align = {x=1, y=-1};
          309  +			}
          310  +			self.hud.elt.time = self:attachTextBox {
          311  +				name = 'time';
          312  +				align = {x=0, y=1};
          313  +				pos = {x=0.5, y=1};
          314  +				ofs = {x=0,y=-95};
          315  +				text = function(user)
          316  +					local cal = starlit.world.time.calendar[user.pref.calendar]
          317  +					return cal.time(minetest.get_timeofday())
          318  +				end;
          319  +			}
          320  +			self.hud.elt.temp = self:attachMeter {
          321  +				name = 'temp';
          322  +				align = {x=1, y=-1};
          323  +				pos = {x=0, y=1};
          324  +				ofs = {x=20, y=-20};
          325  +				measure = function(user)
          326  +					local warm = self:effectiveStat 'warmth'
          327  +					local n, color if warm < 0 then
          328  +						n = math.min(100, -warm)
          329  +						color = lib.color(0.1,0.3,1):lerp(lib.color(0.7, 1, 1), math.min(1, n/50))
          330  +					else
          331  +						n = math.min(100,  warm)
          332  +						color = lib.color(0.1,0.3,1):lerp(lib.color(1, 0, 0), math.min(1, n/50))
          333  +					end
          334  +					local txt = string.format("%s°", math.floor(warm))
          335  +					return (n/50), txt, color
          336  +				end;
          337  +			}
          338  +			self.hud.elt.geiger = self:attachMeter {
          339  +				name = 'geiger';
          340  +				align = {x=-1, y=-1};
          341  +				pos = {x=1, y=1};
          342  +				ofs = {x=-20, y=-20};
          343  +				flipX = true;
          344  +				measure = function(user)
          345  +					local hot = self:effectiveStat 'irradiation'
          346  +					local color = self:uiColor():lerp(lib.color(0.3, 1, 0), math.min(1, hot/5))
          347  +					local txt = string.format("%sGy", math.floor(hot))
          348  +					return (hot/5), txt, color
          349  +				end;
          350  +			}
          351  +			self.hud.elt.crosshair = self:attachImage {
          352  +				name = 'crosshair ';
          353  +				tex = '';
          354  +				pos = {x=.5, y=.5};
          355  +				scale = {x=1,y=1};
          356  +				ofs = {x=0, y=0};
          357  +				align = {x=0, y=0};
          358  +				update = function(user, set)
          359  +					local imgs = {
          360  +						off = '';
          361  +						nano = 'starlit-ui-crosshair-nano.png';
          362  +						psi = 'starlit-ui-crosshair-psi.png';
          363  +						weapon = 'starlit-ui-crosshair-weapon.png';
          364  +					}
          365  +					set('text', imgs[self.actMode] or imgs.off)
          366  +				end;
          367  +			};
          368  +			local hudCenterBG = lib.image 'starlit-ui-hud-bg.png':colorize(self:uiColor())
          369  +			self.hud.elt.bg = self:attachImage {
          370  +				name = 'hudBg';
          371  +				tex = hudCenterBG:render();
          372  +				pos = {x=.5, y=1};
          373  +				scale = {x=1,y=1};
          374  +				ofs = {x=0, y=0};
          375  +				align = {x=0, y=-1};
          376  +				z = -1;
          377  +				update = function(user, set)
          378  +					set('text', hudAdjustBacklight(hudCenterBG):render())
          379  +				end;
          380  +			};
          381  +		end;
          382  +		onModeChange = function(self, oldMode, silent)
          383  +			self.hud.elt.crosshair.update()
          384  +			if not silent then
          385  +				local sfxt = {
          386  +					off = 'starlit-mode-off';
          387  +					nano = 'starlit-mode-nano';
          388  +					psi = 'starlit-mode-psi';
          389  +					weapon = 'starlit-mode-weapon';
          390  +				}
          391  +				local sfx = self.actMode and sfxt[self.actMode] or sfxt.off
          392  +				self:suitSound(sfx)
          393  +			end
          394  +		end;
          395  +		actModeSet = function(self, mode, silent)
          396  +			if not mode then mode = 'off' end
          397  +			local oldMode = self.actMode
          398  +			self.actMode = mode
          399  +			self:onModeChange(oldMode, silent)
          400  +			if mode ~= oldMode then
          401  +				starlit.ui.setupForUser(self)
          402  +			end
          403  +		end;
          404  +		deleteHUD = function(self)
          405  +			for name, e in pairs(self.hud.elt) do
          406  +				self:hud_delete(e.id)
          407  +			end
          408  +		end;
          409  +		updateHUD = function(self)
          410  +			for name, e in pairs(self.hud.elt) do
          411  +				if e.update then e.update() end
          412  +			end
          413  +		end;
          414  +		clientInfo = function(self)
          415  +			return minetest.get_player_information(self.name)
          416  +		end;
          417  +		onSignup = function(self)
          418  +			local meta = self.entity:get_meta()
          419  +			local inv = self.entity:get_inventory()
          420  +			-- 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  +			inv:set_size('main', 6) -- carried items and tools. main hotbar.
          422  +
          423  +			inv:set_size('starlit_suit', 1) -- your environment suit (change at wardrobe)
          424  +			inv:set_size('starlit_cfg', 1) -- the item you're reconfiguring / container you're accessing
          425  +
          426  +			local scenario
          427  +			for _, e in pairs(starlit.world.scenario) do
          428  +				if e.id == starlit.world.defaultScenario then
          429  +					scenario = e break
          430  +				end
          431  +			end assert(scenario)
          432  +			self.persona = starlit.world.species.birth(scenario.species, scenario.speciesVariant, self.entity)
          433  +			self.persona.name = self.entity:get_player_name() -- a reasonable default
          434  +			self.persona.background = starlit.world.defaultScenario
          435  +			self:pushPersona()
          436  +
          437  +			local gifts = scenario.startingItems
          438  +			local inv = self.entity:get_inventory()
          439  +			inv:set_stack('starlit_suit', 1, starlit.item.mk(gifts.suit, self, {gift=true}))
          440  +			self:getSuit():establishInventories(self.entity)
          441  +
          442  +			local function giveGifts(name, list)
          443  +				if inv:get_size(name) > 0 then
          444  +					for i, e in ipairs(list) do
          445  +						inv:add_item(name, starlit.item.mk(e, self, {gift=true}))
          446  +					end
          447  +				end
          448  +			end
          449  +
          450  +			giveGifts('starlit_suit_bat', gifts.suitBatteries)
          451  +			giveGifts('starlit_suit_chips', gifts.suitChips)
          452  +			giveGifts('starlit_suit_guns', gifts.suitGuns)
          453  +			giveGifts('starlit_suit_ammo', gifts.suitAmmo)
          454  +			giveGifts('starlit_suit_canisters', gifts.suitCans)
          455  +
          456  +			giveGifts('main', gifts.carry)
          457  +
          458  +			self:reconfigureSuit()
          459  +
          460  +			-- i feel like there has to be a better way
          461  +			local cx = math.random(-500,500)
          462  +			local startPoint
          463  +			repeat local temp = -100
          464  +				local cz = math.random(-500,500)
          465  +				local cy = minetest.get_spawn_level(cx, cz)
          466  +				if cy then
          467  +					startPoint = vector.new(cx,cy,cz)
          468  +					temp = starlit.world.climate.eval(startPoint,.5,.5).surfaceTemp
          469  +				end
          470  +				if cx > 10000 then break end -- avoid infiniloop in pathological conditions
          471  +			until temp > -2
          472  +			self.entity:set_pos(startPoint)
          473  +			meta:set_string('starlit_spawn', startPoint:to_string())
          474  +		end;
          475  +		onDie = function(self, reason)
          476  +			local inv = self.entity:get_inventory()
          477  +			local where = self.entity:get_pos()
          478  +			local function dropInv(lst)
          479  +				local l = inv:get_list(lst)
          480  +				for i, o in ipairs(l) do
          481  +					if o and not o:is_empty() then
          482  +						minetest.item_drop(o, self.entity, where)
          483  +					end
          484  +				end
          485  +				inv:set_list(lst, {})
          486  +			end
          487  +			dropInv 'main'
          488  +			dropInv 'starlit_suit'
          489  +			self:statDelta('psi',     0, 'death', true)
          490  +			self:statDelta('hunger',  0, 'death', true)
          491  +			self:statDelta('thirst',  0, 'death', true)
          492  +			self:statDelta('fatigue', 0, 'death', true)
          493  +			self:statDelta('stamina', 0, 'death', true)
          494  +			self:updateSuit()
          495  +		end;
          496  +		onRespawn = function(self)
          497  +			local meta = self.entity:get_meta()
          498  +			self.entity:set_pos(vector.from_string(meta:get_string'starlit_spawn'))
          499  +			self:updateSuit()
          500  +			return true
          501  +		end;
          502  +		onJoin = function(self)
          503  +			local me = self.entity
          504  +			local meta = me:get_meta()
          505  +			self:pullPersona()
          506  +
          507  +			-- formspec_version and real_coordinates are apparently just
          508  +			-- completely ignored here
          509  +			me:set_formspec_prepend [[
          510  +				bgcolor[#00000000;true]
          511  +				style_type[button,button_exit,image_button,item_image_button;border=false]
          512  +				style_type[button;bgimg=starlit-ui-button-hw.png;bgimg_middle=8;content_offset=0,-2]
          513  +				style_type[button:hovered;bgimg=starlit-ui-button-hw-hover.png;bgimg_middle=8]
          514  +				style_type[button:pressed;bgimg=starlit-ui-button-hw-press.png;bgimg_middle=8;content_offset=0,1]
          515  +			]]
          516  +			local hotbarSlots = me:get_inventory():get_size 'main';
          517  +-- 			local slotTex = 'starlit-ui-slot.png'
          518  +-- 			local hbimg = string.format('[combine:%sx128', 128 * hotbarSlots)
          519  +-- 			for i = 0, hotbarSlots-1 do
          520  +-- 				hbimg = hbimg .. string.format(':%s,0=%s', 128 * i, slotTex)
          521  +-- 			end
          522  +			--me:hud_set_hotbar_image(lib.image(hbimg):colorize(self:uiColor()):fade(.36):render())
          523  +-- 			me:hud_set_hotbar_selected_image(lib.image(slotTex):colorize(self:uiColor()):render())
          524  +			me:hud_set_hotbar_image('[fill:1x24:0,0:' .. self:uiColor():fade(.1):hex())
          525  +			me:hud_set_hotbar_selected_image(
          526  +				'[fill:1x24,0,0:' .. self:uiColor():fade(.4):hex() .. '^[fill:1x1:0,23:#ffFFffff'
          527  +			)
          528  +			me:hud_set_hotbar_itemcount(hotbarSlots)
          529  +			me:hud_set_flags {
          530  +				hotbar = true;
          531  +				healthbar = false;
          532  +				breathbar = false;
          533  +				basic_debug = false;
          534  +				crosshair = false;
          535  +			}
          536  +			-- disable builtin crafting
          537  +			local inv = me:get_inventory()
          538  +				inv:set_size('craftpreview', 0)
          539  +				inv:set_size('craftresult', 0)
          540  +				inv:set_size('craft', 0)
          541  +
          542  +			me:set_stars {
          543  +				day_opacity = 0.7;
          544  +			}
          545  +			me:set_sky {
          546  +				sky_color = {
          547  +					  day_sky = '#a7c2cd',   day_horizon = '#ddeeff';
          548  +					 dawn_sky = '#003964',  dawn_horizon = '#87ebff';
          549  +					night_sky = '#000000', night_horizon = '#000E29';
          550  +					fog_sun_tint = '#72e4ff';
          551  +					fog_moon_tint = '#2983d0';
          552  +					fog_tint_type = 'custom';
          553  +				};
          554  +				fog = { -- not respected??
          555  +					-- TODO make this seasonal & vary with weather
          556  +					fog_distance = 40;
          557  +					fog_start = 0.3;
          558  +				};
          559  +			}
          560  +			me:set_sun {
          561  +				texture = 'starlit-sun.png';
          562  +				sunrise = 'sunrisebg.png^[hsl:180:1:.7';
          563  +				tonemap = 'sun_tonemap.png^[hsl:180:1:.7';
          564  +				scale = 0.8;
          565  +			}
          566  +			me:set_lighting {
          567  +				shadows = {
          568  +					intensity = .5;
          569  +				};
          570  +				exposure = {
          571  +					luminance_max = 3.0;
          572  +					speed_dark_bright = 0.5;
          573  +					speed_bright_dark = 1.0;
          574  +				};
          575  +				volumetric_light = {
          576  +					strength = 0.3;
          577  +				};
          578  +			}
          579  +			me:set_eye_offset(nil, vector.new(3,-.2,10))
          580  +			-- TODO set_clouds speed in accordance with wind
          581  +			starlit.world.species.setupEntity(me, self.persona)
          582  +			starlit.ui.setupForUser(self)
          583  +			self:createHUD()
          584  +			self:updateSuit()
          585  +		end;
          586  +		suitStack = function(self)
          587  +			return self.entity:get_inventory():get_stack('starlit_suit', 1)
          588  +		end;
          589  +		suitSound = function(self, sfx)
          590  +			-- trigger a sound effect from the player's suit computer
          591  +			minetest.sound_play(sfx, {object=self.entity, max_hear_distance=4}, true)
          592  +		end;
          593  +		suitPowerStateSet = function(self, state, silent)
          594  +			-- necessary to enable reacting to power state changes
          595  +			-- e.g. to play sound effects, display warnings
          596  +			local os
          597  +			self:forSuit(function(s)
          598  +				os=s:powerState()
          599  +				s:powerStateSet(state)
          600  +			end)
          601  +			if state == 'off' then
          602  +				if self.actMode == 'nano' or self.actMode == 'weapon' then
          603  +					self:actModeSet('off', silent)
          604  +				end
          605  +			end
          606  +			if not silent and os ~= state then
          607  +				local sfx
          608  +				if state == 'off' then
          609  +					sfx = 'starlit-power-down'
          610  +				elseif os == 'off' then
          611  +					sfx = 'starlit-power-up'
          612  +				elseif state == 'powerSave' or os == 'powerSave' then
          613  +					sfx = 'starlit-configure'
          614  +				end
          615  +				if sfx then self:suitSound(sfx) end
          616  +			end
          617  +		end;
          618  +		species = function(self)
          619  +			return starlit.world.species.index[self.persona.species]
          620  +		end;
          621  +		updateBody = function(self)
          622  +			local adornment = {}
          623  +			local suitStack = self:suitStack()
          624  +			if suitStack and not suitStack:is_empty() then
          625  +				local suit = suitStack:get_definition()._starlit.suit
          626  +				suit.adorn(adornment, suitStack, self.persona)
          627  +			end
          628  +			starlit.world.species.updateTextures(self.entity, self.persona, adornment)
          629  +		end;
          630  +		updateSuit = function(self)
          631  +			self:updateBody()
          632  +			local inv = self.entity:get_inventory()
          633  +			local sst = suitStore(self:suitStack())
          634  +			if self:naked() then
          635  +				starlit.type.suit.purgeInventories(self.entity)
          636  +				if self.actMode == 'nano' or self.actMode == 'weapon' then
          637  +					self:actModeSet 'off'
          638  +				end
          639  +			else
          640  +				local suit = self:getSuit()
          641  +				suit:establishInventories(self.entity)
          642  +
          643  +				if self:suitCharge() <= 0 then
          644  +					self:suitPowerStateSet 'off'
          645  +				end
          646  +			end
          647  +			self:updateHUD()
          648  +		end;
          649  +		reconfigureSuit = function(self)
          650  +			-- and here's where things get ugly
          651  +			-- you can't have an inventory inside another item. to hack around this,
          652  +			-- we use the player as the location of the suit inventories, and whenever
          653  +			-- there's a change in the content of these inventories, this function is
          654  +			-- called to serialize those inventories out to the suit stack
          655  +			if self:naked() then return end
          656  +			local suit = self:getSuit()
          657  +			suit:onReconfigure(self.entity:get_inventory())
          658  +			self:setSuit(suit)
          659  +
          660  +			-- reconfiguring the suit can affect player abilities: e.g. removing
          661  +			-- / inserting a chip with a minimap program
          662  +		end;
          663  +		getSuit = function(self)
          664  +			local st = self:suitStack()
          665  +			if st:is_empty() then return nil end
          666  +			return starlit.type.suit(st)
          667  +		end;
          668  +		setSuit = function(self, suit)
          669  +			self.entity:get_inventory():set_stack('starlit_suit', 1, suit.item)
          670  +		end;
          671  +		changeSuit = function(self, ...)
          672  +			self:setSuit(...)
          673  +			self:updateSuit()
          674  +		end;
          675  +		forSuit = function(self, fn)
          676  +			local s = self:getSuit()
          677  +			if fn(s) ~= false then
          678  +				self:setSuit(s)
          679  +			end
          680  +		end;
          681  +		suitPowerCapacity = function(self) -- TODO optimize
          682  +			if self:naked() then return 0 end
          683  +			return self:getSuit():powerCapacity()
          684  +		end;
          685  +		suitCharge = function(self) -- TODO optimize
          686  +			if self:naked() then return 0 end
          687  +			return self:getSuit():powerLeft()
          688  +		end;
          689  +		suitDrawCurrent = function(self, power, time, whatFor, min)
          690  +			if self:naked() then return 0,0 end
          691  +			local inv = self.entity:get_inventory()
          692  +			local bl = inv:get_list('starlit_suit_bat')
          693  +			local supply = 0
          694  +			local wasteHeat = 0 --TODO handle internally
          695  +			for slot, ps in ipairs(bl) do
          696  +				if not ps:is_empty() then
          697  +					local p, h = starlit.mod.electronics.dynamo.drawCurrent(ps, power - supply, time)
          698  +					supply = supply + p
          699  +					wasteHeat = wasteHeat + h
          700  +					if power-supply <= 0 then break end
          701  +				end
          702  +			end
          703  +			if min and supply < min then return 0,0 end
          704  +			inv:set_list('starlit_suit_bat', bl)
          705  +			self:reconfigureSuit()
          706  +			if whatFor then
          707  +				-- TODO display power use icon
          708  +			end
          709  +			return supply, wasteHeat
          710  +		end;
          711  +		naked = function(self)
          712  +			return self:suitStack():is_empty()
          713  +		end;
          714  +		onPart = function(self)
          715  +			starlit.liveUI     [self.name] = nil
          716  +			starlit.activeUI   [self.name] = nil
          717  +			starlit.activeUsers[self.name] = nil
          718  +		end;
          719  +		openUI = function(self, id, page, ...)
          720  +			local ui = assert(starlit.interface.db[id])
          721  +			ui:open(self, page, ...)
          722  +		end;
          723  +		onRespond = function(self, ui, state, resp)
          724  +			ui:action(self, state, resp)
          725  +		end;
          726  +
          727  +		updateWeather = function(self)
          728  +		end;
          729  +
          730  +		canInteract = function(self, with)
          731  +			return true; -- TODO
          732  +		end;
          733  +
          734  +		trigger = function(self, which, how)
          735  +			--print('trigger', which, dump(how))
          736  +			local p
          737  +			local wld = self.entity:get_wielded_item()
          738  +			if which == 'maneuver' then
          739  +				p = self.power.maneuver
          740  +			elseif which == 'retarget' then
          741  +				self.action.prog = {}
          742  +			elseif wld and not wld:is_empty() then
          743  +				local wdef = wld:get_definition()
          744  +				if wdef._starlit and wdef._starlit.tool then
          745  +					p = {tool = wdef._starlit.tool}
          746  +				end
          747  +			elseif self.actMode ~= 'off' then
          748  +				p = self.power[self.actMode][which]
          749  +			end
          750  +			if p == nil then return false end
          751  +			local ctx, run = {
          752  +				how = how;
          753  +			}
          754  +			if p.chipID then
          755  +				local inv = self.entity:get_inventory()
          756  +				local chips = inv:get_list 'starlit_suit_chips'
          757  +				for chSlot, ch in pairs(chips) do
          758  +					if ch and not ch:is_empty() then
          759  +						local d = starlit.mod.electronics.chip.read(ch)
          760  +						if d.uuid == p.chipID then
          761  +							local pgm = assert(d.files[p.pgmIndex], 'file missing for ability')
          762  +							ctx.file = starlit.mod.electronics.chip.fileHandle(ch, p.pgmIndex)
          763  +							ctx.saveChip = function()
          764  +								inv:set_slot('starlit_suit_chips', chSlot, ch)
          765  +							end
          766  +							local sw = starlit.item.sw.db[pgm.body.pgmId]
          767  +							run = assert(sw.run, 'missing run() for active software ability ' .. pgm.body.pgmId)
          768  +							break
          769  +						end
          770  +					end
          771  +				end
          772  +			else
          773  +				error('bad ability pointer ' .. dump(p))
          774  +			end
          775  +			if run then
          776  +				run(self, ctx)
          777  +				return true
          778  +			end
          779  +			return false
          780  +		end;
          781  +		give = function(self, item)
          782  +			local inv = self.entity:get_inventory()
          783  +			local function is(grp)
          784  +				return minetest.get_item_group(item:get_name(), grp) ~= 0
          785  +			end
          786  +			-- TODO notif popups
          787  +			if is 'specialInventory' then
          788  +				if is 'powder' then
          789  +					if self:naked() then return item end
          790  +					local cans = inv:get_list 'starlit_suit_canisters'
          791  +					if cans and next(cans) then for i, st in ipairs(cans) do
          792  +						local lst = string.format('starlit_canister_%u_elem', i)
          793  +						item = inv:add_item(lst, item)
          794  +						if item:is_empty() then break end
          795  +					end end
          796  +					self:forSuit(function(x) x:pushCanisters(inv) end)
          797  +				end
          798  +				return item
          799  +			else
          800  +				return inv:add_item('main', item)
          801  +			end
          802  +		end;
          803  +		thrustUpon = function(self, item)
          804  +			local r = self:give(st)
          805  +			if not r:is_empty() then
          806  +				return minetest.add_item(self.entity:get_pos(), r)
          807  +			end
          808  +		end;
          809  +	};
          810  +}
          811  +
          812  +local biointerval = 3.0
          813  +starlit.startJob('starlit:bio', biointerval, function(delta)
          814  +	for id, u in pairs(starlit.activeUsers) do
          815  +
          816  +	end
          817  +end)
          818  +
          819  +local cbit = {
          820  +	up   = 0x001;
          821  +	down = 0x002;
          822  +	left = 0x004;
          823  +	right= 0x008;
          824  +	jump = 0x010;
          825  +	manv = 0x020;
          826  +	snk  = 0x040;
          827  +	dig  = 0x080;
          828  +	put  = 0x100;
          829  +	zoom = 0x200;
          830  +}
          831  +-- this is the painful part
          832  +minetest.register_globalstep(function(delta)
          833  +	local doNothing,mustInit,mustHalt = 0,1,2
          834  +	for id, user in pairs(starlit.activeUsers) do
          835  +		local ent = user.entity
          836  +		local bits = ent:get_player_control_bits()
          837  +
          838  +		local function what(b)
          839  +			if bit.band(bits, b) ~= 0 and bit.band(user.action.bits, b) == 0 then
          840  +				return mustInit
          841  +			elseif bit.band(bits, b) == 0 and bit.band(user.action.bits, b) ~= 0 then
          842  +				return mustHalt
          843  +			else return doNothing end
          844  +		end
          845  +		local skipBits = 0
          846  +		if user.action.bits ~= bits then
          847  +			local mPrimary = what(cbit.dig)
          848  +			local mSecondary = what(cbit.put)
          849  +			if mPrimary == mustInit then -- ENGINE-BUG
          850  +				user.action.tgt = {type='nothing'}
          851  +				user.action.prog = {}
          852  +			elseif mPrimary == mustHalt then
          853  +				user:trigger('primary', {state='halt'})
          854  +			end
          855  +			if mSecondary == mustHalt then
          856  +				user:trigger('secondary', {state='halt'})
          857  +			end
          858  +		end
          859  +		--bits = bit.band(bits, bit.bnot(skipBits))
          860  +		if bit.band(bits, cbit.dig)~=0 then
          861  +			user:trigger('primary', {state='prog', delta=delta})
          862  +		end
          863  +		if bit.band(bits, cbit.put)~=0 then
          864  +			user:trigger('secondary', {state='prog', delta=delta})
          865  +		end
          866  +		user.action.bits = bits
          867  +		-- ENGINE-BUG: dig and put are not handled equally in the
          868  +		-- engine. it is possible for the put bit to get stuck on
          869  +		-- if the key is hammered while the player is not moving.
          870  +		-- the bit will release as soon as the player looks or turns
          871  +		-- nonetheless this is obnoxious
          872  +	end
          873  +end)

Added mods/starlit/world.lua version [830720f731].

            1  +local lib = starlit.mod.lib
            2  +local world = starlit.world
            3  +
            4  +function world.date()
            5  +	local days = minetest.get_day_count()
            6  +	local year = math.floor(days / world.planet.orbit);
            7  +	local day = days % world.planet.orbit;
            8  +	return {
            9  +		year = year, day = day;
           10  +		season = day / world.planet.orbit;
           11  +	}
           12  +end
           13  +local lerp = lib.math.lerp
           14  +
           15  +local function gradient(grad, pos)
           16  +	local n = #grad
           17  +	if n == 1 then return grad[1] end
           18  +	local op = pos*(n-1)
           19  +	local idx = math.floor(op)
           20  +	local t = op-idx
           21  +	return lerp(t, grad[1 + idx], grad[2 + idx])
           22  +end
           23  +
           24  +local altitudeCooling = 10 / 100
           25  +
           26  +-- this function provides the basis for temperature calculation,
           27  +-- which is performed by adding this value to the ambient temperature,
           28  +-- determined by querying nearby group:heatSource items in accordance
           29  +-- with the inverse-square law
           30  +function world.climate.eval(pos, tod, season)
           31  +	local data = minetest.get_biome_data(pos)
           32  +	local biome = world.ecology.biomes.db[minetest.get_biome_name(data.biome)]
           33  +	local heat, humid = data.heat, data.humidity
           34  +	tod = tod or minetest.get_timeofday()
           35  +	heat = lerp(math.abs(tod - 0.5)*2, heat, heat + biome.nightTempDelta)
           36  +
           37  +	local td = world.date()
           38  +	heat = heat + gradient(biome.seasonalTemp, season or td.season)
           39  +	if pos.y > 0 then
           40  +		heat = heat - pos.y*altitudeCooling 
           41  +	end
           42  +
           43  +	return {
           44  +		surfaceTemp = heat;
           45  +		waterTemp = heat + biome.waterTempDelta;
           46  +		surfaceHumid = humid;
           47  +	}
           48  +end
           49  +
           50  +local vdsq = lib.math.vdsq
           51  +function world.climate.temp(pos) --> irradiance at pos in W
           52  +	local cl = world.climate.eval(pos)
           53  +	local radCenters = starlit.region.radiator.store:get_areas_for_pos(pos, false, true)
           54  +	local irradiance = 0
           55  +	for _,e in pairs(radCenters) do
           56  +		local rpos = minetest.string_to_pos(e.data)
           57  +		local rdef = assert(minetest.registered_nodes[assert(minetest.get_node(rpos)).name])
           58  +		local rc = rdef._starlit.radiator
           59  +		local r_max = rc.radius(rpos)
           60  +
           61  +		local dist_sq = vdsq(rpos,pos)
           62  +		if dist_sq <= r_max^2 then
           63  +			-- cheap bad way
           64  +			-- if minetest.line_of_sight(rpos,pos) then
           65  +			--
           66  +			-- expensive way
           67  +			local obstruct = 0
           68  +			local ray = Raycast(rpos, pos, true, true)
           69  +			for p in ray do
           70  +				if p.type == 'node' then obstruct = obstruct + 1 end
           71  +			end
           72  +
           73  +			if obstruct < 4 then
           74  +				local power, customFalloff = rc.radiate(rpos, pos)
           75  +				-- okay this isn't the real inverse square law but i 
           76  +				-- couldn't figure out a better way to simplify the
           77  +				-- model without checking an ENORMOUS number of nodes
           78  +				-- maybe someone else who isn't completely
           79  +				-- mathtarded can do better.
           80  +				if not customFalloff then
           81  +					power = power * (1 - (dist_sq / ((r_max+1)^2)))
           82  +				end
           83  +				power = power * (1 - (obstruct/5))
           84  +				irradiance = irradiance + power
           85  +			end
           86  +		end
           87  +	end
           88  +	return irradiance + cl.surfaceTemp
           89  +end
           90  +
           91  +world.ecology.biomes.foreach('starlit:biome-gen', {}, function(id, b)
           92  +	b.def.name = id
           93  +	minetest.register_biome(b.def)
           94  +end)
           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  +})
          125  +
          126  +local toward = lib.math.toward
          127  +local hfinterval = 1.5
          128  +starlit.startJob('starlit:heatflow', hfinterval, function(delta)
          129  +
          130  +	-- our base thermal conductivity (κ) is measured in °C/°C/s. say the
          131  +	-- player is in -30°C weather, and has an internal temperature of
          132  +	-- 10°C. then:
          133  +	--   κ  = .1°C/C/s (which is apparently 100mHz)
          134  +	--   Tₚ =  10°C
          135  +	--   Tₑ = -30°C
          136  +	--   d  = Tₑ − Tₚ = -40°C
          137  +	--   ΔT = κ×d = -.4°C/s
          138  +	-- our final change in temperature is computed as tΔC where t is time
          139  +	local kappa = .05
          140  +	for name,user in pairs(starlit.activeUsers) do
          141  +		local tr = user:species().tempRange
          142  +		local t = starlit.world.climate.temp(user.entity:get_pos())
          143  +		local insul = 0
          144  +		local naked = user:naked()
          145  +		local suitDef
          146  +		if not naked then
          147  +			suitDef = user:suitStack():get_definition()
          148  +			insul = suitDef._starlit.suit.temp.insulation
          149  +		end
          150  +
          151  +		local warm = user:effectiveStat 'warmth'
          152  +		local tSafeMin, tSafeMax = tr.survivable[1], tr.survivable[2]
          153  +		local tComfMin, tComfMax = tr.comfort[1], tr.comfort[2]
          154  +
          155  +		local tDelta = (kappa * (1-insul)) * (t - warm) * hfinterval
          156  +		local tgt = warm + tDelta
          157  +
          158  +		-- old logic: we move the user towards the exterior temperature, modulated
          159  +		-- by her suit insulation.
          160  +		--local tgt = toward(warm, t, hfinterval * thermalConductivity * (1 - insul))
          161  +
          162  +		if not naked then
          163  +			local suit = user:getSuit()
          164  +			local suitPower = suit:powerState()
          165  +			local suitPowerLeft = suit:powerLeft()
          166  +			if suitPower ~= 'off' then
          167  +				local coilPower = 1.0
          168  +				local st = suitDef._starlit.suit.temp
          169  +				if suitPower == 'powerSave' and (tgt >= tSafeMin and tgt <= tSafeMax) then coilPower = 0.5 end
          170  +				if tgt < tComfMin and st.maxHeat > 0 then
          171  +					local availPower = user:suitDrawCurrent(st.heatPower*coilPower, hfinterval)
          172  +					tgt = tgt + (availPower / st.heatPower) * st.maxHeat * coilPower * hfinterval
          173  +				end
          174  +				if tgt > tComfMax and st.maxCool > 0 then
          175  +					local availPower = user:suitDrawCurrent(st.coolPower*coilPower, hfinterval)
          176  +					tgt = tgt - (availPower / st.coolPower) * st.maxCool * coilPower * hfinterval
          177  +				end
          178  +			end
          179  +		end
          180  +
          181  +		user:statDelta('warmth', tgt - warm) -- dopey but w/e
          182  +
          183  +		warm = tgt -- for the sake of readable code
          184  +
          185  +		if warm < tSafeMin or warm > tSafeMax then
          186  +			local dv
          187  +			if warm < tSafeMin then
          188  +				dv = math.abs(warm - tSafeMin)
          189  +			else
          190  +				dv = math.abs(warm - tSafeMax)
          191  +			end
          192  +			-- for every degree of difference you suffer 2 points of damage/s
          193  +			local dmg = math.ceil(dv * 2)
          194  +			user:statDelta('health', -dmg)
          195  +		end
          196  +	end
          197  +end)

Deleted mods/starsoul-building/init.lua version [a42549c83d].

     1         -local lib = starsoul.mod.lib
     2         -local B = {}
     3         -starsoul.mod.building = B
     4         -
     5         -B.path = {}
     6         --- this maps stage IDs to tables of the following form
     7         ---[[ {
     8         -	part = {
     9         -		['starsoul_building:pipe'] = 'myMod:stage3';
    10         -	};
    11         -	tool = {
    12         -		['starsoul:scredriver'] = 'myMod:otherThing_stage1';
    13         -		['starsoul:saw'] = function(node, tool)
    14         -			minetest.replace_node(node, {name='myMod:stage1'})
    15         -			minetest.drop_item(node, 'starsoul_building:pipe')
    16         -		end;
    17         -		['myMod:laserWrench'] = {
    18         -			allow = function(node, tool) ... end;
    19         -			handle = function(node, tool) ... end;
    20         -		};
    21         -	};
    22         -} ]]
    23         --- it should only be written by special accessor functions!
    24         -
    25         -B.stage = lib.registry.mk 'starsoul_building:stage'
    26         --- a stage consists of a list of pieces and maps from possible materials
    27         --- / tool usages to succeeding stages in the build tree. note that all
    28         --- used pieces must be defined before a stage is defined currently, due
    29         --- to a lack of cross-registry dependency mechanisms. i will hopefully
    30         --- improve vtlib to handle this condition eventually.
    31         ---[[
    32         -	starsoul.mod.building.stage.link(id, {
    33         -		pieces = {
    34         -			'starsoul_building:foundation';
    35         -			'starsoul_building:insulation'; -- offset             ofsFac
    36         -			{'starsoul_building:pipe',      vector.new(-.5, 0, 0), 0};--
    37         -			{'starsoul_building:pipe',      vector.new(-.5, 0, 0), 1};
    38         -			'starsoul_building:insulation';
    39         -			'starsoul_building:panel';
    40         -		};
    41         -	})
    42         -]]
    43         -
    44         -B.piece = lib.registry.mk 'starsoul_building:piece'
    45         --- a piece is used to produce stage definitions, by means of appending
    46         --- nodeboxes with appropriate offsets. it also lists the recoverable
    47         --- materials which can be obtained by destroying a stage containing
    48         --- this piece using nano. part IDs should correspond with piece IDs
    49         --- where possible
    50         ---[[
    51         -	starsoul.mod.building.piece.link(id, {
    52         -		tex = 'myMod_part.png';
    53         -		height = 0.1; -- used for auto-offset
    54         -		fab = {
    55         -			element = {iron=10};
    56         -		};	
    57         -		shape = {
    58         -			type = "fixed";
    59         -			fixed = { ... };
    60         -		};
    61         -	})
    62         -]]
    63         -
    64         -B.part = lib.registry.mk 'starsoul_building:part'
    65         --- a part is implemented as a special craftitem with the proper callbacks
    66         --- to index the registries and place/replace noes by reference to the
    67         --- build tree.
    68         ---[[
    69         -	starsoul.mod.building.part.link(id, {
    70         -		name = ''; -- display name
    71         -		desc = ''; -- display desc
    72         -		img = ''; -- display image
    73         -	})
    74         -]]
    75         -
    76         -B.stage.foreach('starsoul:stageGen', {}, function(id, e)
    77         -	local box = {type = 'fixed', fixed = {}}
    78         -	local tex = {}
    79         -	local ofs = vector.new(0,0,0)
    80         -	for idx, p in ipairs(e.pieces) do
    81         -		local ho, pieceID, pos
    82         -		if type(p) == 'string' then
    83         -			pieceID, pos, ho = p, vector.zero(), 1.0
    84         -		else
    85         -			pieceID, pos, ho = pc[1],pc[2],pc[3]
    86         -		end
    87         -		local pc = B.piece.db[pieceID]
    88         -		pos = pos + ofs
    89         -		if ho ~= 0.0 then
    90         -			ofs = vector.offset(ofs, 0, pc.height)
    91         -		end
    92         -		local sh = lib.node.boxwarped(pc.shape, function(b)
    93         -			-- { -x, -y, -z;
    94         -			--   +x, +y, +z }
    95         -			b[1] = b[1] + ofs.x  b[4] = b[4] + ofs.x
    96         -			b[2] = b[2] + ofs.y  b[5] = b[5] + ofs.y
    97         -			b[3] = b[3] + ofs.z  b[6] = b[6] + ofs.z
    98         -		end)
    99         -		table.insert(box, sh)
   100         -		if type(pc.tex) == 'string' then
   101         -			table.insert(tex, pc.tex)
   102         -		else
   103         -			for i,t in ipairs(pc.tex) do
   104         -				table.insert(tex, t or '')
   105         -			end
   106         -		end
   107         -	end
   108         -	minetest.register_node(id, {
   109         -		description = 'Construction';
   110         -		drawtype = 'nodebox';
   111         -		paramtype  = 'light';
   112         -		paramtype2 = e.stateful or 'none';
   113         -		textures = tex;
   114         -		node_box = box;
   115         -		group = { stage = 1 };
   116         -		_starsoul = {
   117         -			stage = id;
   118         -		};
   119         -	})
   120         -end)
   121         -
   122         -function B.pathLink(from, kind, what, to)
   123         -	if not B.path[from] then
   124         -		B.path[from] = {part={}, tool={}}
   125         -	end
   126         -	local k = B.path[from][kind]
   127         -	assert(k[what] == nil)
   128         -	k[what] = to
   129         -end
   130         -
   131         -function B.pathFind(from, kind, what)
   132         -	if not B.path[from] then return nil end
   133         -	return B.path[from][kind][what]
   134         -end
   135         -

Deleted mods/starsoul-building/mod.conf version [41dba78d03].

     1         -name = starsoul_building
     2         -depends = starsoul_electronics, starsoul
     3         -description = implements construction elements

Deleted mods/starsoul-electronics/init.lua version [4ca9d32d80].

     1         -local lib = starsoul.mod.lib
     2         -
     3         -local E = {}
     4         -starsoul.mod.electronics = E
     5         -
     6         ----------------------
     7         --- item registries --
     8         ----------------------
     9         -
    10         --- a dynamo is any item that produces power and can be slotted into a power
    11         --- source slot. this includes batteries, but also things like radiothermal
    12         --- dynamos.
    13         -starsoul.item.dynamo = lib.registry.mk 'starsoul_electronics:dynamo'
    14         -
    15         --- batteries hold a charge of power (measured in kJ). how much they can hold
    16         --- (and how much power they can discharge?) depends on their quality
    17         -starsoul.item.battery = lib.registry.mk 'starsoul_electronics:battery'
    18         -
    19         --- a battery has the properties:
    20         ---  class
    21         ---	 |- capacity (J ): amount of energy the battery can hold
    22         ---	 |- dischargeRate  (W ): rate at which battery can supply power/be charged
    23         ---	 |- decay   (J/J): rate at which the battery capacity degrades while
    24         ---	                   discharging. decay=0 batteries require no maintenance;
    25         ---	                   decay=1 batteries are effectively disposable
    26         ---	 |- leak    (fac): charging inefficiency. depends on the energy storage
    27         ---	                   technology. when N J are drawn from a power source,
    28         ---	                   only (N*leak) J actually make it into the battery.
    29         ---	                   leak=0 is a supercapacitor, leak=1 is /dev/null
    30         ---	 |- size      (m): each suit has a limit to how big of a battery it can take
    31         ---	instance
    32         ---	 |- degrade (mJ): how much the battery has degraded. instance max charge is
    33         ---	 |                determined by $capacity - @degrade
    34         ---	 |- %wear ÷ 2¹⁶ : used as a factor to determine battery charge
    35         -
    36         --- chips are standardized data storage hardware that can contain a certain amount
    37         --- of software. in addition to their flash storage, they also provide a given amount
    38         --- of working memory and processor power. processor power speeds up operations like
    39         --- crafting, while programs require a certain amount of memory.
    40         --- chips have a variable number of program slots and a single bootloader slot
    41         ---
    42         -starsoul.item.chip = lib.registry.mk 'starsoul_electronics:chip'
    43         -
    44         --- software is of one of the following types:
    45         ---   schematic: program for your matter compiler that enables crafting a given item.
    46         ---       output: the result
    47         ---   driver: inserted into a Core to control attached hardware
    48         ---     suitPower: provides suit functionality like nanoshredding or healing
    49         ---                passive powers are iterated on suit application/configuration and upon fst-tick
    50         ---   cost: what the software needs to run. some fields are fab-specific
    51         ---		   energy: for fab, total energy cost of process in joules
    52         ---               for suitPassive, added suit power consumption in watts
    53         -starsoul.item.sw = lib.registry.mk 'starsoul_electronics:sw'
    54         --- chip    = lib.color(0, 0, .3);
    55         -
    56         -E.schematicGroups = lib.registry.mk 'starsoul_electronics:schematicGroups'
    57         -E.schematicGroupMembers = {}
    58         -E.schematicGroups.foreach('starsoul_electronics:ensure-memlist', {}, function(id,g)
    59         -	E.schematicGroupMembers[id] = {}
    60         -end)
    61         -function E.schematicGroupLink(group, item)
    62         -	table.insert(E.schematicGroupMembers[group], item)
    63         -end
    64         -
    65         -E.schematicGroups.link('starsoul_electronics:chip', {
    66         -	title = 'Chip', icon = 'starsoul-item-chip.png';
    67         -	description = 'Standardized data storage and compute modules';
    68         -})
    69         -
    70         -E.schematicGroups.link('starsoul_electronics:battery', {
    71         -	title = 'Battery', icon = 'starsoul-item-battery.png';
    72         -	description = 'Portable power storage cells are essential to all aspects of survival';
    73         -})
    74         -
    75         -E.schematicGroups.link('starsoul_electronics:decayCell', {
    76         -	title = 'Decay Cell', icon = 'starsoul-item-decaycell.png';
    77         -	description = "Radioisotope generators can pack much more power into a smaller amount of space than conventional batteries, but they can't be recharged, dump power and heat whether they're in use or not, and their power yield drops towards zero over their usable lifetime.";
    78         -})
    79         -
    80         -
    81         --------------------------
    82         --- batteries & dynamos --
    83         --------------------------
    84         -
    85         -E.battery = {}
    86         -local function accessor(ty, fn)
    87         -	return function(stack, ...)
    88         -		local function fail()
    89         -			error(string.format('object %q is not a %s', stack:get_name(), ty))
    90         -		end
    91         -
    92         -		if not stack or stack:is_empty() then fail() end
    93         -
    94         -		if minetest.get_item_group(stack:get_name(), ty) == 0 then fail() end
    95         -
    96         -		return fn(stack,
    97         -		          stack:get_definition()._starsoul[ty],
    98         -		          stack:get_meta(), ...)
    99         -	end
   100         -end
   101         -
   102         --- return a wear level that won't destroy the item
   103         --- local function safeWear(fac) return math.min(math.max(fac,0),1) * 0xFFFE end
   104         --- local function safeWearToFac(w) return w/0xFFFE end
   105         -
   106         -E.battery.update = accessor('battery', function(stack, batClass, meta)
   107         -	-- local cap = E.battery.capacity(stack)
   108         -	local charge = meta:get_float 'starsoul_electronics:battery_charge'
   109         -	meta:set_string('count_meta', string.format('%s%%', math.floor(charge * 100)))
   110         -	meta:set_int('count_alignment', bit.lshift(3, 2) + 2)
   111         -end)
   112         -
   113         --- E.battery.capacity(bat) --> charge (J)
   114         -E.battery.capacity = accessor('battery', function(stack, batClass, meta)
   115         -	local dmg = meta:get_int 'starsoul_electronics:battery_degrade' -- µJ/μW
   116         -	local dmg_J = dmg / 1000
   117         -	return (batClass.capacity - dmg_J)
   118         -end)
   119         -
   120         --- E.battery.charge(bat) --> charge (J)
   121         -E.battery.charge = accessor('battery', function(stack, batClass, meta)
   122         -	local fac = meta:get_float 'starsoul_electronics:battery_charge'
   123         -	-- local fac = 1 - safeWearToFac(stack:get_wear())
   124         -	return E.battery.capacity(stack) * fac
   125         -end)
   126         -
   127         --- E.battery.dischargeRate(bat) --> dischargeRate (W)
   128         -E.battery.dischargeRate = accessor('battery', function(stack, batClass, meta)
   129         -	local dmg = meta:get_int 'starsoul_electronics:battery_degrade' -- µJ/μW
   130         -	local dmg_W = dmg / 1000
   131         -	return batClass.dischargeRate - dmg_W
   132         -end);
   133         -
   134         -
   135         --- E.battery.drawCurrent(bat, power, time, test) --> supply (J), wasteHeat (J)
   136         ---       bat   = battery stack
   137         ---     power J = joules of energy user wishes to consume
   138         ---      time s = the amount of time available for this transaction
   139         ---    supply J = how much power was actually provided in $time seconds
   140         --- wasteHeat J = how heat is generated in the process
   141         ---      test   = if true, the battery is not actually modified
   142         -E.battery.drawCurrent = accessor('battery', function(s, bc, m, power, time, test)
   143         -	local ch = E.battery.charge(s)
   144         -	local maxPower = math.min(E.battery.dischargeRate(s)*time, power, ch)
   145         -	ch = ch - maxPower
   146         -
   147         -	if not test then
   148         -		local degrade = m:get_int 'starsoul_electronics:battery_degrade' or 0
   149         -		degrade = degrade + maxPower * bc.decay
   150         -		-- for each joule of power drawn, capacity degrades by `decay` J
   151         -		-- this should ordinarily be on the order of mJ or smaller
   152         -		m:set_int('starsoul_electronics:battery_degrade', degrade)
   153         -		-- s:set_wear(safeWear(1 - (ch / E.battery.capacity(s))))
   154         -		m:set_float('starsoul_electronics:battery_charge', ch / E.battery.capacity(s))
   155         -		E.battery.update(s)
   156         -	end
   157         -
   158         -	return maxPower, 0 -- FIXME specify waste heat
   159         -end)
   160         -
   161         --- E.battery.recharge(bat, power, time) --> draw (J)
   162         ---     bat   = battery stack
   163         ---   power J = joules of energy user wishes to charge the battery with
   164         ---    time s = the amount of time available for this transaction
   165         ---    draw J = how much power was actually drawn in $time seconds
   166         -E.battery.recharge = accessor('battery', function(s, bc, m, power, time)
   167         -	local ch = E.battery.charge(s)
   168         -	local cap = E.battery.capacity(s)
   169         -	local maxPower = math.min(E.battery.dischargeRate(s)*time, power)
   170         -	local total = math.min(ch + maxPower, cap)
   171         -	-- s:set_wear(safeWear(1 - (total/cap)))
   172         -	m:set_float('starsoul_electronics:battery_charge', total/cap)
   173         -	E.battery.update(s)
   174         -	return maxPower, 0 -- FIXME
   175         -end)
   176         -
   177         -E.battery.setCharge = accessor('battery', function(s, bc, m, newPower)
   178         -	local cap = E.battery.capacity(s)
   179         -	local power = math.min(cap, newPower)
   180         -	-- s:set_wear(safeWear(1 - (power/cap)))
   181         -	m:set_float('starsoul_electronics:battery_charge', power/cap)
   182         -	E.battery.update(s)
   183         -end)
   184         -E.battery.setChargeF = accessor('battery', function(s, bc, m, newPowerF)
   185         -	local power = math.min(1.0, newPowerF)
   186         -	m:set_float('starsoul_electronics:battery_charge', power)
   187         -	E.battery.update(s)
   188         -end)
   189         -
   190         -E.dynamo = { kind = {} }
   191         -
   192         -E.dynamo.drawCurrent = accessor('dynamo', function(s,c,m, power, time, test)
   193         -	return c.vtable.drawCurrent(s, power, time, test)
   194         -end)
   195         -E.dynamo.totalPower    = accessor('dynamo', function(s,c,m) return c.vtable.totalPower(s) end)
   196         -E.dynamo.dischargeRate = accessor('dynamo', function(s,c,m) return c.vtable.dischargeRate   (s) end)
   197         -E.dynamo.initialPower  = accessor('dynamo', function(s,c,m) return c.vtable.initialPower(s) end)
   198         -E.dynamo.wasteHeat     = accessor('dynamo', function(s,c,m) return c.vtable.wasteHeat(s) end)
   199         --- baseline waste heat, produced whether or not power is being drawn. for batteries this is 0, but for
   200         --- radiothermal generators it may be high
   201         -
   202         -E.dynamo.kind.battery = {
   203         -	drawCurrent = E.battery.drawCurrent;
   204         -	totalPower = E.battery.charge;
   205         -	initialPower = E.battery.capacity;
   206         -	dischargeRate = E.battery.dischargeRate;
   207         -	wasteHeat = function() return 0 end;
   208         -};
   209         -
   210         -starsoul.item.battery.foreach('starsoul_electronics:battery-gen', {}, function(id, def)
   211         -	minetest.register_tool(id, {
   212         -		short_description = def.name;
   213         -		groups = { battery = 1; dynamo = 1; electronic = 1; };
   214         -		inventory_image = def.img or 'starsoul-item-battery.png';
   215         -		description = starsoul.ui.tooltip {
   216         -			title = def.name;
   217         -			desc = def.desc;
   218         -			color = lib.color(0,.2,1);
   219         -			props = {
   220         -				{ title = 'Optimal Capacity', affinity = 'info';
   221         -					desc = lib.math.si('J', def.capacity) };
   222         -				{ title = 'Discharge Rate', affinity = 'info';
   223         -					desc = lib.math.si('W', def.dischargeRate) };
   224         -				{ title = 'Charge Efficiency', affinity = 'info';
   225         -					desc = string.format('%s%%', (1-def.leak) * 100) };
   226         -				{ title = 'Size', affinity = 'info';
   227         -					desc = lib.math.si('m', def.fab.size.print) };
   228         -			};
   229         -		};
   230         -		_starsoul = {
   231         -			event = {
   232         -				create = function(st, how)
   233         -					--[[if not how.gift then -- cheap hack to make starting batteries fully charged
   234         -						E.battery.setCharge(st, 0)
   235         -					end]]
   236         -					E.battery.update(st)
   237         -				end;
   238         -			};
   239         -			fab = def.fab;
   240         -			dynamo = {
   241         -				vtable = E.dynamo.kind.battery;
   242         -			};
   243         -			battery = def;
   244         -		};
   245         -	})
   246         -end)
   247         -
   248         -
   249         --- to use the power functions, consider the following situation. you have
   250         --- a high-tier battery charger that can draw 100kW. (for simplicity, assume
   251         --- it supports only one battery). if you install a low-tier battery, and
   252         --- the charging callback is called every five seconds, you might use
   253         --- a `recharge` call that looks like
   254         ---
   255         ---   starsoul.mod.electronics.battery.recharge(bat, 5 * 100*1e4, 5)
   256         ---
   257         --- this would offer the battery 500kJ over five seconds. the battery will
   258         --- determine how much power it can actually make use of in 5 five seconds,
   259         --- and then return that amount.
   260         ---
   261         --- always remember to save the battery back to its inventory slot after
   262         --- modifying its ItemStack with one of these functions!
   263         -
   264         -
   265         --- battery types
   266         --- supercapacitor: low capacity, no degrade, high dischargeRate, no leak
   267         --- chemical: high capacity, high degrade, mid dischargeRate, low leak
   268         -
   269         --- battery tiers
   270         --- makeshift: cheap, weak, low quality
   271         --- imperial ("da red wunz go fasta"): powerful, low quality
   272         --- commune ("snooty sophisticates"): limited power, high quality, expensive
   273         --- usukwinya ("value engineering"): high power, mid quality, affordable
   274         --- eluthrai ("uncompromising"): high power, high quality, wildly expensive
   275         --- firstborn ("god-tier"): exceptional
   276         -
   277         -local batteryTiers = {
   278         -	makeshift = {
   279         -		name = 'Makeshift'; capacity = .5, decay = 3, leak = 2, dischargeRate = 1,
   280         -		fab = starsoul.type.fab {
   281         -			metal = {copper=10};
   282         -		};
   283         -		desc = "Every attosecond this electrical abomination doesn't explode in your face is but the unearned grace of the Wild Gods.";
   284         -		complexity = 1;
   285         -		sw = {rarity = 1};
   286         -	};
   287         -	imperial  = {
   288         -		name = 'Imperial'; capacity = 2, decay = 2, leak = 2, dischargeRate = 2; 
   289         -		fab = starsoul.type.fab {
   290         -			metal = {copper=15, iron = 20};
   291         -			size = { print = 0.1 };
   292         -		};
   293         -		desc = "The Empire's native technology is a lumbering titan: bulky, inefficient, unreliable, ugly, and awesomely powerful. Their batteries are no exception, with raw capacity and throughput that exceed even Usukinwya designs.";
   294         -		drm = 1;
   295         -		complexity = 2;
   296         -		sw = {rarity = 2};
   297         -	};
   298         -	commune   = {
   299         -		name = 'Commune'; capacity = 1, decay = .5, leak = .2, dischargeRate = 1; 
   300         -		fab = starsoul.type.fab {
   301         -			metal = {vanadium=50, steel=10};
   302         -			size = { print = 0.05 };
   303         -		};
   304         -		desc = "The Commune's proprietary battery designs prioritize reliability, compactness, and maintenance concerns above raw throughput, with an elegance of engineering and design that would make a Su'ikuri cry.";
   305         -		complexity = 5;
   306         -		sw = {rarity = 3};
   307         -	};
   308         -	usukwinya = {
   309         -		name = 'Usukwinya'; capacity = 2, decay = 1, leak = 1, dischargeRate = 1.5,
   310         -		fab = starsoul.type.fab {
   311         -			metal = {vanadium=30, argon=10};
   312         -			size = { print = 0.07 };
   313         -		};
   314         -		desc = "A race of consummate value engineers, the Usukwinya have spent thousands of years refining their tech to be as cheap to build as possible, without compromising much on quality. The Tradebirds drive an infamously hard bargain, but their batteries are more than worth their meagre cost.";
   315         -		drm = 2;
   316         -		sw = {rarity = 10};
   317         -		complexity = 15;
   318         -	};
   319         -	eluthrai  = {
   320         -		name = 'Eluthrai'; capacity = 3, decay = .4, leak = .1, dischargeRate = 1.5,
   321         -		fab = starsoul.type.fab {
   322         -			metal = {beryllium=20, platinum=20, technetium = 1, cinderstone = 10 };
   323         -			size = { print = 0.03 };
   324         -		};
   325         -		desc = "The uncompromising Eluthrai are never satisfied until every quantifiable characteristic of their tech is maximally optimised down to the picoscale. Their batteries are some of the best in the Reach, and unquestionably the most expensive -- especially for those lesser races trying to copy the designs without the benefit of the sublime autofabricator ecosystem of the Eluthrai themselves.";
   326         -		complexity = 200;
   327         -		sw = {rarity = 0}; -- you think you're gonna buy eluthran schematics on SuperDiscountNanoWare.space??
   328         -	};
   329         -	firstborn = {
   330         -		name = 'Firstborn'; capacity = 5, decay = 0.1, leak = 0, dischargeRate = 3;
   331         -		fab = starsoul.type.fab {
   332         -			metal = {neodymium=20, xenon=150, technetium=5, sunsteel = 10 };
   333         -			crystal = {astrite = 1};
   334         -			size = { print = 0.05 };
   335         -		};
   336         -		desc = "Firstborn engineering seamlessly merges psionic effects with a mastery of the physical universe unattained by even the greatest of the living Starsouls. Their batteries reach levels of performance that strongly imply Quantum Gravity Theory -- and several major holy books -- need to be rewritten. From the ground up.";
   337         -		complexity = 1000;
   338         -		sw = {rarity = 0}; -- lol no
   339         -	};
   340         -}
   341         -
   342         -local batterySizes = {
   343         -	small = {name = 'Small', capacity = .5, dischargeRate =  .5, complexity = 1, matMult = .5, fab = starsoul.type.fab {size={print=0.1}}};
   344         -	mid   = {                capacity =  1, dischargeRate =   1, complexity = 1, matMult = 1, fab = starsoul.type.fab {size={print=0.3}}};
   345         -	large = {name = 'Large', capacity =  2, dischargeRate = 1.5, complexity = 1, matMult = 1.5, fab = starsoul.type.fab {size={print=0.5}}};
   346         -	huge  = {name = 'Huge',  capacity =  3, dischargeRate =   2, complexity = 1, matMult = 2, fab = starsoul.type.fab {size={print=0.8}}};
   347         -}
   348         -
   349         -local batteryTypes = {
   350         -	supercapacitor = {
   351         -		name = 'Supercapacitor';
   352         -		desc = 'Room-temperature superconductors make for very reliable, high-dischargeRate, but low-capacity batteries.';
   353         -		fab = starsoul.type.fab {
   354         -			metal = { enodium = 5 };
   355         -			size = {print=0.8};
   356         -		};
   357         -		sw = {
   358         -			cost = {
   359         -				cycles = 5e9; -- 5 bil cycles
   360         -				ram = 10e9; -- 10GB
   361         -			};
   362         -			pgmSize = 2e9; -- 2GB
   363         -			rarity = 5;
   364         -		};
   365         -		capacity = 50e3, dischargeRate = 1000;
   366         -		leak = 0, decay = 1e-6;
   367         -
   368         -		complexity = 3;
   369         -	};
   370         -	chemical = {
   371         -		name = 'Chemical';
   372         -		desc = '';
   373         -		fab = starsoul.type.fab {
   374         -			element = { lithium = 3};
   375         -			metal = {iron = 5};
   376         -			size = {print=1.0};
   377         -		};
   378         -		sw = {
   379         -			cost = {
   380         -				cycles = 1e9; -- 1 bil cycles
   381         -				ram = 2e9; -- 2GB
   382         -			};
   383         -			pgmSize = 512e6; -- 512MB
   384         -			rarity = 2;
   385         -		};
   386         -		capacity = 200e3, dischargeRate = 200;
   387         -		leak = 0.2, decay = 1e-2;
   388         -		complexity = 1;
   389         -	};
   390         -	carbon = {
   391         -		name = 'Carbon';
   392         -		desc = 'Carbon nanotubes form the basis of many important metamaterials, chief among them power-polymer.';
   393         -		capacity = 1;
   394         -		fab = starsoul.type.fab {
   395         -			element = { carbon = 40 };
   396         -			size = {print=0.5};
   397         -		};
   398         -		sw = {
   399         -			cost = {
   400         -				cycles = 50e9; -- 50 bil cycles
   401         -				ram = 64e9; -- 64GB
   402         -			};
   403         -			pgmSize = 1e9; -- 1GB
   404         -			rarity = 10;
   405         -		};
   406         -		capacity = 100e3, dischargeRate = 500;
   407         -		leak = 0.1, decay = 1e-3;
   408         -		complexity = 10;
   409         -	};
   410         -	hybrid = {
   411         -		name = 'Hybrid';
   412         -		desc = '';
   413         -		capacity = 1;
   414         -		fab = starsoul.type.fab {
   415         -			element = {
   416         -				lithium = 3;
   417         -			};
   418         -			metal = {
   419         -				iron = 5;
   420         -			};
   421         -			size = {print=1.5};
   422         -		};
   423         -		sw = {
   424         -			cost = {
   425         -				cycles = 65e9; -- 65 bil cycles
   426         -				ram = 96e9; -- 96GB
   427         -			};
   428         -			pgmSize = 5e9; -- 5GB
   429         -			rarity = 15;
   430         -		};
   431         -		capacity = 300e3, dischargeRate = 350;
   432         -		leak = 0.3, decay = 1e-5;
   433         -		complexity = 30;
   434         -	};
   435         -}
   436         -
   437         -local function elemath(dest, src, mult)
   438         -	dest = dest or {}
   439         -	for k,v in pairs(src) do
   440         -		if not dest[k] then dest[k] = 0 end
   441         -		dest[k] = dest[k] + v*mult
   442         -	end
   443         -	return dest
   444         -end
   445         -
   446         -for bTypeName, bType in pairs(batteryTypes) do
   447         -for bTierName, bTier in pairs(batteryTiers) do
   448         -for bSizeName, bSize in pairs(batterySizes) do
   449         -	-- elemath(elementCost, bType.fab.element or {}, bSize.matMult)
   450         -	-- elemath(elementCost, bTier.fab.element or {}, bSize.matMult)
   451         -	-- elemath(metalCost, bType.fab.metal or {}, bSize.matMult)
   452         -	-- elemath(metalCost, bTier.fab.metal or {}, bSize.matMult)
   453         -	local fab = bType.fab + bTier.fab + bSize.fab + starsoul.type.fab {
   454         -		element = {copper = 10, silicon = 5};
   455         -	}
   456         -	local baseID = string.format('battery_%s_%s_%s',
   457         -		bTypeName, bTierName, bSizeName)
   458         -	local id = 'starsoul_electronics:'..baseID
   459         -	local name = string.format('%s %s Battery', bTier.name, bType.name)
   460         -	if bSize.name then name = bSize.name .. ' ' .. name end
   461         -	local function batStat(s)
   462         -		if s == 'size' then
   463         -			--return bType.fab[s] * (bTier.fab[s] or 1) * (bSize.fab[s] or 1)
   464         -			return fab.size and fab.size.print or 1
   465         -		else
   466         -			return bType[s] * (bTier[s] or 1) * (bSize[s] or 1)
   467         -		end
   468         -	end
   469         -
   470         -	local swID = 'starsoul_electronics:schematic_'..baseID
   471         -	fab.reverseEngineer = {
   472         -		complexity = bTier.complexity * bSize.complexity * bType.complexity;
   473         -		sw = swID;
   474         -	}
   475         -	fab.flag = {print=true}
   476         -
   477         -	starsoul.item.battery.link(id, {
   478         -		name = name;
   479         -		desc = table.concat({
   480         -			bType.desc or '';
   481         -			bTier.desc or '';
   482         -			bSize.desc or '';
   483         -		}, ' ');
   484         -
   485         -		fab = fab;
   486         -
   487         -		capacity = batStat 'capacity';
   488         -		dischargeRate  = batStat 'dischargeRate';
   489         -		leak     = batStat 'leak';
   490         -		decay    = batStat 'decay';
   491         -	})
   492         -
   493         -	local rare
   494         -	if bType.sw.rarity == 0 or bTier.sw.rarity == 0 then
   495         -		-- rarity is measured such that the player has a 1/r
   496         -		-- chance of finding a given item, or if r=0, no chance
   497         -		-- whatsoever (the sw must be obtained e.g. by reverse-
   498         -		-- engineering alien tech)
   499         -		rare = 0
   500         -	else
   501         -		rare = bType.sw.rarity + bTier.sw.rarity
   502         -	end
   503         -
   504         -	starsoul.item.sw.link(swID, {
   505         -		kind = 'schematic';
   506         -		name = name .. ' Schematic';
   507         -		output = id;
   508         -		size = bType.sw.pgmSize;
   509         -		cost = bType.sw.cost;
   510         -		rarity = rare;
   511         -	})
   512         -
   513         -	E.schematicGroupLink('starsoul_electronics:battery', swID)
   514         -
   515         -end end end
   516         -
   517         -
   518         ------------
   519         --- chips --
   520         ------------
   521         -
   522         -E.sw = {}
   523         -function E.sw.findSchematicFor(item)
   524         -	local id = ItemStack(item):get_name()
   525         -	print(id)
   526         -	local fm = minetest.registered_items[id]._starsoul
   527         -	if not (fm and fm.fab and fm.fab.reverseEngineer) then return nil end
   528         -	local id = fm.fab.reverseEngineer.sw
   529         -	return id, starsoul.item.sw.db[id]
   530         -end
   531         -
   532         -E.chip = { file = {} }
   533         -do local T,G = lib.marshal.t, lib.marshal.g
   534         -	-- love too reinvent unions from first principles
   535         -	E.chip.data = G.struct {
   536         -		label = T.str;
   537         -		uuid = T.u64;
   538         -		files = G.array(16, G.class(G.struct {
   539         -			kind = G.enum {
   540         -				'sw'; -- a piece of installed software
   541         -				'note'; -- a user-readable text file
   542         -				'research'; -- saved RE progress
   543         -				'genome'; -- for use with plant biosequencer?
   544         -				'blob'; -- opaque binary blob, so 3d-pty mods can use the
   545         -                    -- file mechanism to store arbirary data.
   546         -			};
   547         -			drm = T.u8; -- inhibit copying
   548         -			name = T.str;
   549         -			body = T.text;
   550         -		}, function(file) -- enc
   551         -			local b = E.chip.file[file.kind].enc(file.body)
   552         -			return {
   553         -				kind = file.kind;
   554         -				drm = file.drm;
   555         -				name = file.name;
   556         -				body = b;
   557         -			}
   558         -		end, function(file) -- dec
   559         -			local f, ns = E.chip.file[file.kind].dec(file.body)
   560         -			file.body = f
   561         -			return file, ns
   562         -		end));
   563         -		bootSlot = T.u8; -- indexes into files; 0 = no bootloader
   564         -	}
   565         -	E.chip.file.sw = G.struct {
   566         -		pgmId = T.str;
   567         -		conf = G.array(16, G.struct {
   568         -			key = T.str, value = T.str;
   569         -		});
   570         -	}
   571         -	E.chip.file.note = G.struct {
   572         -		author = T.str;
   573         -		entries = G.array(16, G.struct {
   574         -			title = T.str;
   575         -			body = T.str;
   576         -		});
   577         -	}
   578         -	E.chip.file.research = G.struct {
   579         -		itemId = T.str;
   580         -		progress = T.clamp;
   581         -	}
   582         -	E.chip.file.blob = G.struct {
   583         -		kind = T.str; -- MT ID that identifies a blob file type belonging to an external mod
   584         -		size = T.u8; -- this must be manually reported since we don't know how to evaluate it
   585         -		data = T.text;
   586         -	}
   587         -	function E.chip.fileSize(file)
   588         -		-- boy howdy
   589         -		if file.kind == 'blob' then
   590         -			return file.body.size
   591         -		elseif file.kind == 'note' then
   592         -			local sz = 0x10 + #file.body.author
   593         -			for _, e in pairs(file.body.entries) do
   594         -				sz = sz + #e.title + #e.body + 0x10 -- header overhead
   595         -			end
   596         -			return sz
   597         -		elseif file.kind == 'research' then
   598         -			local re = assert(minetest.registered_items[file.body.itemId]._starsoul.fab.reverseEngineer)
   599         -			return starsoul.item.sw.db[re.sw].size * file.body.progress
   600         -		elseif file.kind == 'sw' then
   601         -			return starsoul.item.sw.db[file.body.pgmId].size
   602         -		elseif file.kind == 'genome' then
   603         -			return 0 -- TODO
   604         -		end
   605         -	end
   606         -	local metaKey = 'starsoul_electronics:chip'
   607         -	function E.chip.read(chip)
   608         -		local m = chip:get_meta()
   609         -		local blob = m:get_string(metaKey)
   610         -		if blob and blob ~= '' then
   611         -			return E.chip.data.dec(lib.str.meta_dearmor(blob))
   612         -		else -- prepare to format the chip
   613         -			return {
   614         -				label = '';
   615         -				bootSlot = 0;
   616         -				uuid = math.floor(math.random(0,2^32));
   617         -				files = {};
   618         -			}
   619         -		end
   620         -	end
   621         -	function E.chip.write(chip, data)
   622         -		local m = chip:get_meta()
   623         -		m:set_string(metaKey, lib.str.meta_armor(E.chip.data.enc(data)))
   624         -		E.chip.update(chip)
   625         -	end
   626         -	function E.chip.fileOpen(chip, inode, fn)
   627         -		local c = E.chip.read(chip)
   628         -		if fn(c.files[inode]) then
   629         -			E.chip.write(chip, c)
   630         -			return true
   631         -		end
   632         -		return false
   633         -	end
   634         -	function E.chip.fileWrite(chip, inode, file)
   635         -		local c = E.chip.read(chip)
   636         -		c.files[inode] = file
   637         -		E.chip.write(chip, c)
   638         -	end
   639         -	function E.chip.usedSpace(chip, d)
   640         -		d = d or E.chip.read(chip)
   641         -		local sz = 0
   642         -		for _, f in pairs(d.files) do
   643         -			sz = sz + E.chip.fileSize(f)
   644         -		end
   645         -		return sz
   646         -	end
   647         -	function E.chip.freeSpace(chip, d)
   648         -		local used = E.chip.usedSpace(chip,d)
   649         -		local max = assert(chip:get_definition()._starsoul.chip.flash)
   650         -		return max - used
   651         -	end
   652         -	function E.chip.install(chip, file)
   653         -		-- remember to write out the itemstack after using this function!
   654         -		local d = E.chip.read(chip)
   655         -		if E.chip.freeSpace(chip, d) - E.chip.fileSize(file) >= 0 then
   656         -			table.insert(d.files, file)
   657         -			E.chip.write(chip, d)
   658         -			return true
   659         -		else
   660         -			return false
   661         -		end
   662         -	end
   663         -end
   664         -
   665         -function E.chip.files(ch)
   666         -	local m = ch:get_meta()
   667         -	if not m:contains 'starsoul_electronics:chip' then
   668         -		return nil
   669         -	end
   670         -	local data = E.chip.read(ch)
   671         -	local f = 0
   672         -	return function()
   673         -		f = f + 1
   674         -		return data.files[f], f
   675         -	end
   676         -end
   677         -
   678         -function E.chip.describe(ch, defOnly)
   679         -	local def, data if defOnly then
   680         -		def, data = ch, {}
   681         -	else
   682         -		def = ch:get_definition()
   683         -		local m = ch:get_meta()
   684         -		if m:contains 'starsoul_electronics:chip' then
   685         -			data = E.chip.read(ch)
   686         -		else
   687         -			data = {}
   688         -			defOnly = true
   689         -		end
   690         -		def = assert(def._starsoul.chip)
   691         -	end
   692         -	local props = {
   693         -		{title = 'Clock Rate', affinity = 'info';
   694         -		 desc  = lib.math.si('Hz', def.clockRate)};
   695         -		{title = 'RAM', affinity = 'info';
   696         -		 desc  = lib.math.si('B', def.ram)};
   697         -	}
   698         -	if not defOnly then
   699         -		table.insert(props, {
   700         -			title = 'Free Storage', affinity = 'info';
   701         -			 desc = lib.math.si('B', E.chip.freeSpace(ch, data)) .. ' / '
   702         -			     .. lib.math.si('B', def.flash);
   703         -		})
   704         -		local swAffMap = {
   705         -			schematic = 'schematic';
   706         -			suitPower = 'ability';
   707         -			driver = 'driver';
   708         -		}
   709         -		for i, e in ipairs(data.files) do
   710         -			local aff = 'neutral'
   711         -			local name = e.name
   712         -			local disabled = false
   713         -			if e.kind == 'sw' then
   714         -				for _,cf in pairs(e.body.conf) do
   715         -					if cf.key == 'disable' and cf.value == 'yes' then
   716         -						disabled = true
   717         -						break
   718         -					end
   719         -				end
   720         -				local sw = starsoul.item.sw.db[e.body.pgmId]
   721         -				aff = swAffMap[sw.kind] or 'good'
   722         -				if name == '' then name = sw.name end
   723         -			end
   724         -			name = name or '<???>'
   725         -			table.insert(props, disabled and {
   726         -				title = name;
   727         -				affinity = aff;
   728         -				desc = '<off>';
   729         -			} or {
   730         -				--title = name;
   731         -				affinity = aff;
   732         -				desc = name;
   733         -			})
   734         -		end
   735         -	else
   736         -		table.insert(props, {
   737         -			title = 'Flash Storage', affinity = 'info';
   738         -			 desc = lib.math.si('B', def.flash);
   739         -		 })
   740         -	end
   741         -	return starsoul.ui.tooltip {
   742         -		title = data.label and data.label~='' and string.format('<%s>', data.label) or def.name;
   743         -		color = lib.color(.6,.6,.6);
   744         -		desc = def.desc;
   745         -		props = props;
   746         -	};
   747         -end
   748         -
   749         -function E.chip.update(chip)
   750         -	chip:get_meta():set_string('description', E.chip.describe(chip))
   751         -end
   752         -
   753         -starsoul.item.chip.foreach('starsoul_electronics:chip-gen', {}, function(id, def)
   754         -	minetest.register_craftitem(id, {
   755         -		short_description = def.name;
   756         -		description = E.chip.describe(def, true);
   757         -		inventory_image = def.img or 'starsoul-item-chip.png';
   758         -		groups = {chip = 1};
   759         -		_starsoul = {
   760         -			fab = def.fab;
   761         -			chip = def;
   762         -		};
   763         -	})
   764         -end)
   765         -
   766         --- in case other mods want to define their own tiers
   767         -E.chip.tiers = lib.registry.mk 'starsoul_electronics:chipTiers'
   768         -E.chip.tiers.meld {
   769         -	-- GP chips
   770         -	tiny    = {name = 'Tiny Chip', clockRate = 512e3, flash = 4096, ram = 1024, powerEfficiency = 1e9, size = 1};
   771         -	small   = {name = 'Small Chip', clockRate = 128e6, flash = 512e6, ram = 512e6, powerEfficiency = 1e8, size = 3};
   772         -	med     = {name = 'Chip', clockRate = 1e9, flash = 4e9, ram = 4e9, powerEfficiency = 1e7, size = 6};
   773         -	large   = {name = 'Large Chip', clockRate = 2e9, flash = 8e9, ram = 8e9, powerEfficiency = 1e6, size = 8};
   774         -	-- specialized chips
   775         -	compute = {name = 'Compute Chip', clockRate = 4e9, flash = 24e6, ram = 64e9, powerEfficiency = 1e8, size = 4};
   776         -	data    = {name = 'Data Chip', clockRate = 128e3, flash = 2e12, ram = 32e3, powerEfficiency = 1e5, size = 4};
   777         -	lp      = {name = 'Low-Power Chip', clockRate = 128e6, flash = 64e6, ram = 1e9, powerEfficiency = 1e10, size = 4};
   778         -	carbon  = {name = 'Carbon Chip', clockRate = 64e6, flash = 32e6, ram = 2e6, powerEfficiency = 2e9, size = 2, circ='carbon'};
   779         -}
   780         -
   781         -E.chip.tiers.foreach('starsoul_electronics:genChips', {}, function(id, t)
   782         -	id = t.id or string.format('%s:chip_%s', minetest.get_current_modname(), id)
   783         -	local circMat = t.circ or 'silicon';
   784         -	starsoul.item.chip.link(id, {
   785         -		name = t.name;
   786         -		clockRate = t.clockRate;
   787         -		flash = t.flash;
   788         -		ram = t.ram;
   789         -		powerEfficiency = t.powerEfficiency; -- cycles per joule
   790         -		fab = {
   791         -			flag = {
   792         -				silicompile = true;
   793         -			};
   794         -			time = {
   795         -				silicompile = t.size * 24*60;
   796         -			};
   797         -			cost = {
   798         -				energy = 50e3 + t.size * 15e2;
   799         -			};
   800         -			element = {
   801         -				[circMat] = 50 * t.size;
   802         -				copper = 30;
   803         -				gold = 15;
   804         -			};
   805         -		};
   806         -	})
   807         -end)
   808         -
   809         -function E.chip.findBest(test, ...)
   810         -	local chip, bestFitness
   811         -	for id, c in pairs(starsoul.item.chip.db) do
   812         -		local fit, fitness = test(c, ...)
   813         -		if fit and (bestFitness == nil or fitness > bestFitness) then
   814         -			chip, bestFitness = id, fitness
   815         -		end
   816         -	end
   817         -	return chip, starsoul.item.chip.db[chip], bestFitness
   818         -end
   819         -
   820         -function E.chip.findForStorage(sz)
   821         -	return E.chip.findBest(function(c)
   822         -		return c.flash >= sz, -math.abs(c.flash - sz)
   823         -	end)
   824         -end
   825         -
   826         -function E.chip.sumCompute(chips)
   827         -	local c = {
   828         -		cycles = 0;
   829         -		ram = 0;
   830         -		flashFree = 0;
   831         -		powerEfficiency = 0;
   832         -	}
   833         -	local n = 0
   834         -	for _, e in pairs(chips) do
   835         -		n = n + 1
   836         -		if not e:is_empty() then
   837         -			local ch = e:get_definition()._starsoul.chip
   838         -			c.cycles = c.cycles + ch.clockRate
   839         -			c.ram = c.ram + ch.clockRate
   840         -			c.flashFree = c.flashFree + E.chip.freeSpace(e)
   841         -			c.powerEfficiency = c.powerEfficiency + ch.powerEfficiency
   842         -		end
   843         -	end
   844         -	if n > 0 then c.powerEfficiency = c.powerEfficiency / n end
   845         -	return c
   846         -end
   847         -
   848         -E.chip.fileHandle = lib.class {
   849         -	__name = 'starsoul_electronics:chip.fileHandle';
   850         -	construct = function(chip, inode) -- stack, int --> fd
   851         -		return { chip = chip, inode = inode }
   852         -	end;
   853         -	__index = {
   854         -		read = function(self)
   855         -			local dat = E.chip.read(self.chip)
   856         -			return dat.files[self.inode]
   857         -		end;
   858         -		write = function(self,data)
   859         -			-- print('writing', self.chip, self.inode)
   860         -			return E.chip.fileWrite(self.chip, self.inode, data)
   861         -		end;
   862         -		erase = function(self)
   863         -			local dat = E.chip.read(self.chip)
   864         -			table.remove(dat.files, self.inode)
   865         -			E.chip.write(self.chip, dat)
   866         -			self.inode = nil
   867         -		end;
   868         -		open = function(self,fn)
   869         -			return E.chip.fileOpen(self.chip, self.inode, fn)
   870         -		end;
   871         -	};
   872         -}
   873         -
   874         -function E.chip.usableSoftware(chips,pgm)
   875         -	local comp = E.chip.sumCompute(chips)
   876         -	local r = {}
   877         -	local unusable = {}
   878         -	local sw if pgm then
   879         -		if type(pgm) == 'string' then
   880         -			pgm = {starsoul.item.sw.db[pgm]}
   881         -		end
   882         -		sw = pgm
   883         -	else
   884         -		sw = {}
   885         -		for i, e in ipairs(chips) do
   886         -			if (not e:is_empty())
   887         -			   and minetest.get_item_group(e:get_name(), 'chip') ~= 0
   888         -			then
   889         -				for fl, inode in E.chip.files(e) do
   890         -					if fl.kind == 'sw' then
   891         -						local s = starsoul.item.sw.db[fl.body.pgmId]
   892         -						table.insert(sw, {
   893         -							sw = s, chip = e, chipSlot = i;
   894         -							file = fl, inode = inode;
   895         -						})
   896         -					end
   897         -				end
   898         -			end
   899         -		end
   900         -	end
   901         -
   902         -	for _, s in pairs(sw) do
   903         -		if s.sw.cost.ram <= comp.ram then
   904         -			table.insert(r, {
   905         -				sw = s.sw;
   906         -				chip = s.chip, chipSlot = s.chipSlot;
   907         -				file = s.file;
   908         -				fd = E.chip.fileHandle(s.chip, s.inode);
   909         -				speed = s.sw.cost.cycles / comp.cycles;
   910         -				powerCost = s.sw.cost.cycles / comp.powerEfficiency;
   911         -				comp = comp;
   912         -			})
   913         -		else
   914         -			table.insert(unusable, {
   915         -				sw = s.sw;
   916         -				chip = s.chip;
   917         -				ramNeeded = s.sw.cost.ram - comp.ram;
   918         -			})
   919         -		end
   920         -	end
   921         -	return r, unusable
   922         -end
   923         -
   924         -starsoul.include 'sw'

Deleted mods/starsoul-electronics/mod.conf version [8bb64074c2].

     1         -name = starsoul_electronics
     2         -description = basic electronic components and logic
     3         -depends = starsoul

Deleted mods/starsoul-electronics/sw.lua version [3b2a0cfbed].

     1         --- [ʞ] sw.lua
     2         ---  ~ lexi hale <lexi@hale.su>
     3         ---  🄯 EUPL v1.2
     4         ---  ? 
     5         -
     6         --------------------------------
     7         --- basic suit nano abilities --
     8         --------------------------------
     9         -local function shredder(prop)
    10         -	local function getItemsForFab(fab)
    11         -		local elt
    12         -		if fab then
    13         -			elt = fab:elementalize()
    14         -		else
    15         -			elt = {}
    16         -		end
    17         -		local items = {}
    18         -		if elt.element then
    19         -			for k,v in pairs(elt.element) do
    20         -				local st = ItemStack {
    21         -					name = starsoul.world.material.element.db[k].form.element;
    22         -					count = v;
    23         -				}
    24         -				table.insert(items, st)
    25         -			end
    26         -		end
    27         -		return items
    28         -	end
    29         -
    30         -	return function(user, ctx)
    31         -		local function cleanup()
    32         -			user.action.prog.shred = nil
    33         -			if user.action.sfx.shred then
    34         -				minetest.sound_fade(user.action.sfx.shred, 1, 0)
    35         -				user.action.sfx.shred = nil
    36         -			end
    37         -			if user.action.fx.shred then
    38         -				user.action.fx.shred.abort()
    39         -			end
    40         -		end
    41         -
    42         -		if user.action.tgt.type ~= 'node' then return end
    43         -		local what = user.action.tgt.under
    44         -		if what == nil or user.entity:get_pos():distance(what) > prop.range then
    45         -			cleanup()
    46         -			return false
    47         -		end
    48         -		local shredTime = 1.0
    49         -		local soundPitch = 1.0 -- TODO
    50         -		local pdraw = prop.powerDraw or 0
    51         -
    52         -		local node = minetest.get_node(what)
    53         -		local nd = minetest.registered_nodes[node.name]
    54         -		local elt, fab, vary
    55         -		if nd._starsoul then
    56         -			fab = nd._starsoul.recover or nd._starsoul.fab
    57         -			vary = nd._starsoul.recover_vary
    58         -		end
    59         -		if fab then
    60         -			if fab.flag then
    61         -				if fab.flag.unshreddable then
    62         -					cleanup()
    63         -					return false
    64         -					-- TODO error beep
    65         -				end
    66         -			end
    67         -			shredTime = fab.time and fab.time.shred or shredTime -- FIXME
    68         -			if fab.cost and fab.cost.shredPower then
    69         -				pdraw = pdraw * fab.cost.shredPower
    70         -			end
    71         -		end
    72         -		local maxW = user:getSuit():maxPowerUse()
    73         -		if maxW < pdraw then
    74         -			shredTime = shredTime * (pdraw/maxW)
    75         -			pdraw = maxW
    76         -		end
    77         -		if ctx.how.state == 'prog' then
    78         -			local pdx = pdraw * ctx.how.delta
    79         -			local p = user:suitDrawCurrent(pdx, ctx.how.delta, {kind='nano',label='Shredder'}, pdx)
    80         -			if p < pdx then
    81         -				cleanup()
    82         -				return false
    83         -			elseif not user.action.prog.shred then
    84         -				cleanup() -- kill danglers
    85         -				-- begin
    86         -				user.action.prog.shred = 0
    87         -				user.action.sfx.shred = minetest.sound_play('starsoul-nano-shred', {
    88         -					object = user.entity;
    89         -					max_hear_distance = prop.range*2;
    90         -					loop = true;
    91         -					pitch = soundPitch;
    92         -				})
    93         -				user.action.fx.shred = starsoul.fx.nano.shred(user, what, prop, shredTime, node)
    94         -			else
    95         -				user.action.prog.shred = user.action.prog.shred + ctx.how.delta or 0
    96         -			end
    97         -			--print('shred progress: ', user.action.prog.shred)
    98         -			if user.action.prog.shred >= shredTime then
    99         -				if minetest.dig_node(what) then
   100         -					--print('shred complete')
   101         -					user:suitSound 'starsoul-success'
   102         -					if fab then
   103         -						local vf = fab
   104         -						if vary then
   105         -							local rng = (starsoul.world.seedbank+0xa891f62)[minetest.hash_node_position(what)]
   106         -							vf = vf + vary(rng, {})
   107         -						end
   108         -						local items = getItemsForFab(vf)
   109         -						for i, it in ipairs(items) do user:give(it) end
   110         -					end
   111         -				else
   112         -					user:suitSound 'starsoul-error'
   113         -				end
   114         -				cleanup()
   115         -			end
   116         -		elseif ctx.how.state == 'halt' then
   117         -			cleanup()
   118         -		end
   119         -		return true
   120         -	end
   121         -end
   122         -
   123         -starsoul.item.sw.link('starsoul_electronics:shred', {
   124         -	name = 'NanoShred';
   125         -	kind = 'suitPower', powerKind = 'active';
   126         -	desc = 'An open-source program used in its various forks and iterations all across human-inhabited space and beyond. Rumored to contain fragments of code stolen from the nanoware of the Greater Races by an elusive infoterrorist.';
   127         -	size = 500e3;
   128         -	cost = {
   129         -		cycles = 100e6;
   130         -		ram = 500e6;
   131         -	};
   132         -	run = shredder{range=2, powerDraw=200};
   133         -})
   134         -
   135         -starsoul.item.sw.link('starsoul_electronics:compile_commune', {
   136         -	name = 'Compile Matter';
   137         -	kind = 'suitPower', powerKind = 'direct';
   138         -	desc = "A basic suit matter compiler program, rather slow but ruthlessly optimized for power- and memory-efficiency by some of the Commune's most fanatic coders.";
   139         -	size = 700e3;
   140         -	cost = {
   141         -		cycles = 300e6;
   142         -		ram = 2e9;
   143         -	};
   144         -	ui = 'starsoul:compile-matter-component';
   145         -	run = function(user, ctx)
   146         -	end;
   147         -})
   148         -
   149         -starsoul.item.sw.link('starsoul_electronics:compile_block_commune', {
   150         -	name = 'Compile Block';
   151         -	kind = 'suitPower', powerKind = 'active';
   152         -	desc = "An advanced suit matter compiler program, capable of printing complete devices and structure parts directly into the world.";
   153         -	size = 5e6;
   154         -	cost = {
   155         -		cycles = 700e6;
   156         -		ram = 4e9;
   157         -	};
   158         -	ui = 'starsoul:compile-matter-block';
   159         -	run = function(user, ctx)
   160         -	end;
   161         -})
   162         -
   163         -do local J = starsoul.store.compilerJob
   164         -	starsoul.item.sw.link('starsoul_electronics:driver_compiler_commune', {
   165         -		name = 'Matter Compiler';
   166         -		kind = 'driver';
   167         -		desc = "A driver for a standalone matter compiler, suitable for building larger components than your suit alone can handle.";
   168         -		size = 850e3;
   169         -		cost = {
   170         -			cycles = 400e6;
   171         -			ram = 2e9;
   172         -		};
   173         -		ui = 'starsoul:device-compile-matter-component';
   174         -		run = function(user, ctx)
   175         -		end;
   176         -		bgProc = function(user, ctx, interval, runState)
   177         -			if runState.flags.compiled == true then return false end
   178         -			-- only so many nanides to go around
   179         -			runState.flags.compiled = true
   180         -			local time = minetest.get_gametime()
   181         -			local cyclesLeft = ctx.comp.cycles * interval
   182         -
   183         -			for id, e in ipairs(ctx.file.body.conf) do
   184         -				if e.key == 'job' then
   185         -					local t = J.dec(e.value)
   186         -					local remove = false
   187         -					local r = starsoul.item.sw.db[t.schematic]
   188         -					if not r then -- bad schematic
   189         -						remove = true
   190         -					else
   191         -						local ccost = ctx.sw.cost.cycles + r.cost.cycles
   192         -						local tcost = ccost / cyclesLeft
   193         -						t.progress = t.progress + (1/tcost)*interval
   194         -						cyclesLeft = cyclesLeft - ccost*interval
   195         -						if t.progress >= 1 then
   196         -							-- complete
   197         -							remove = true
   198         -							local i = starsoul.item.mk(r.output, {
   199         -								how = 'print';
   200         -								user = user; -- for suit
   201         -								compiler = {
   202         -									node = ctx.compiler; -- for device
   203         -									sw = ctx.sw;
   204         -									install = ctx.fd;
   205         -								};
   206         -								schematic = r;
   207         -							})
   208         -							ctx.giveItem(i)
   209         -						end
   210         -					end
   211         -					if remove then
   212         -						table.remove(ctx.file.body.conf, id)
   213         -					else
   214         -						e.value = J.enc(t)
   215         -					end
   216         -					if not cyclesLeft > 0 then break end
   217         -				end
   218         -			end
   219         -			ctx.saveConf()
   220         -		end;
   221         -	})
   222         -end
   223         -
   224         -local function pasv_heal(effect, energy, lvl, pgmId)
   225         -	return function(user, ctx, interval, runState)
   226         -		if runState.flags.healed == true then return false end
   227         -		-- competing nanosurgical programs?? VERY bad idea
   228         -		runState.flags.healed = true
   229         -
   230         -		local amt, f = user:effectiveStat 'health'
   231         -		local st = user:getSuit():powerState()
   232         -		if (st == 'on' and f < lvl) or (st == 'powerSave' and f < math.min(lvl,0.25)) then
   233         -			local maxPower = energy*interval
   234         -			local p = user:suitDrawCurrent(maxPower, interval, {
   235         -				id = 'heal';
   236         -				src = 'suitPower';
   237         -				pgmId = pgmId;
   238         -				healAmount = effect;
   239         -			})
   240         -			if p > 0 then
   241         -				local heal = (p/maxPower) * ctx.speed * effect*interval
   242         -				--user:statDelta('health', math.max(1, heal))
   243         -				starsoul.fx.nano.heal(user, {{player=user.entity}}, heal, 1)
   244         -				return true
   245         -			end
   246         -		end
   247         -		return false -- program did not run
   248         -	end;
   249         -end
   250         -
   251         -starsoul.item.sw.link('starsoul_electronics:nanomed', {
   252         -	name = 'NanoMed';
   253         -	kind = 'suitPower', powerKind = 'passive';
   254         -	desc = 'Repair of the body is a Commune specialty, and their environment suits all come equipped with highly sophisticated nanomedicine suites, able to repair even the most grievous of wounds given sufficient energy input and time.';
   255         -	size = 2e9;
   256         -	cost = {
   257         -		cycles = 400e6;
   258         -		ram = 3e9;
   259         -	};
   260         -	run = pasv_heal(2, 20, 1);
   261         -})
   262         -
   263         -starsoul.item.sw.link('starsoul_electronics:autodoc_deluxe', {
   264         -	name = 'AutoDoc Deluxe';
   265         -	kind = 'suitPower', powerKind = 'passive';
   266         -	desc = "A flagship offering of the Excellence Unyielding nanoware division, AutoDoc Deluxe has been the top-rated nanocare package in the Celestial Shores Province for six centuries and counting. Every chip includes our comprehensive database of illnesses, prosyn schematics, and organ repair techniques, with free over-the-ether updates guaranteed for ten solariads from date of purchase! When professional medical care just isn't an option, 9/10 doctors recommend Excellence Unyielding AutoDoc Deluxe! The remaining doctor was bribed by our competitors.";
   267         -	size = 1e9;
   268         -	cost = {
   269         -		cycles = 700e6;
   270         -		ram = 1e9;
   271         -	};
   272         -	run = pasv_heal(4, 50, .7);
   273         -})

Deleted mods/starsoul-material/elements.lua version [8655f86d81].

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

Deleted mods/starsoul-material/init.lua version [4473abdf0e].

     1         -local lib = starsoul.mod.lib
     2         -local M = {
     3         -	canisterSizes = lib.registry.mk 'starsoul_material:canister-size';
     4         -}
     5         -M.canisterSizes.foreach('starsoul_material:canister_link', {}, function(id, sz)
     6         -	starsoul.item.canister.link(minetest.get_current_modname() .. ':canister_' .. id, {
     7         -		name = sz.name;
     8         -		slots = sz.slots;
     9         -		vol = 0.1; -- too big for suit?
    10         -		desc = sz.desc;
    11         -	})
    12         -end)
    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};
    19         -}
    20         -
    21         -starsoul.include 'elements'
    22         -

Deleted mods/starsoul-material/mod.conf version [4c6f9cbdac].

     1         -name = starsoul_material
     2         -description = defines the raw materials and alloys used in printing
     3         -depends = starsoul

Deleted mods/starsoul-scenario/init.lua version [d3b56b60b6].

     1         -local lib = starsoul.mod.lib
     2         -local scenario = starsoul.world.scenario
     3         -
     4         -local function makeChip(label, schem, sw)
     5         -	local E = starsoul.mod.electronics
     6         -	local files = {}
     7         -	local sz = 0
     8         -	for _, e in ipairs(schem) do
     9         -		local p = E.sw.findSchematicFor(e[1])
    10         -		if p then
    11         -			local file = {
    12         -				kind = 'sw', name = '', drm = e[2];
    13         -				body = {pgmId = p};
    14         -			}
    15         -			table.insert(files, file)
    16         -			sz = sz + E.chip.fileSize(file)
    17         -		end
    18         -	end
    19         -	for _, e in ipairs(sw) do
    20         -		local file = {
    21         -			kind = 'sw', name = '', drm = e[2];
    22         -			body = {pgmId = e[1]};
    23         -		}
    24         -		table.insert(files, file)
    25         -		sz = sz + E.chip.fileSize(file)
    26         -	end
    27         -	local chip = ItemStack(assert(E.chip.findBest(function(c)
    28         -		return c.flash >= sz, c.ram + c.clockRate
    29         -	end)))
    30         -	local r = E.chip.read(chip)
    31         -	r.label = label
    32         -	r.files = files
    33         -	E.chip.write(chip, r)
    34         -	return chip
    35         -end
    36         -
    37         -local chipLibrary = {
    38         -	compendium = makeChip('The Gentleman Adventurer\'s Compleat Wilderness Compendium', {
    39         -		{'starsoul_electronics:battery_chemical_imperial_small', 0};
    40         -	}, {
    41         -		{'starsoul_electronics:shred', 0};
    42         -		--{'starsoul_electronics:compile_empire', 0};
    43         -		{'starsoul_electronics:autodoc_deluxe', 1};
    44         -		--{'starsoul_electronics:driver_compiler_empire', 0};
    45         -	});
    46         -	survivalware = makeChip('Emergency Survivalware', {
    47         -		{'starsoul_electronics:battery_chemical_commune_small', 0};
    48         -	}, {
    49         -		{'starsoul_electronics:shred', 0};
    50         -		{'starsoul_electronics:compile_commune', 0};
    51         -		{'starsoul_electronics:nanomed', 0};
    52         -		{'starsoul_electronics:driver_compiler_commune', 0};
    53         -	});
    54         -	misfortune = makeChip("Sold1er0fMisf0rtune TOP Schematic Crackz REPACK", {
    55         -		{'starsoul_electronics:battery_chemical_usukwinya_mid', 0};
    56         -		{'starsoul_electronics:battery_hybrid_imperial_small', 0};
    57         -		-- ammunition
    58         -	}, {});
    59         -}
    60         -
    61         -local battery = function(name)
    62         -	local s = ItemStack(name)
    63         -	starsoul.mod.electronics.battery.setChargeF(s, 1.0)
    64         -	return s
    65         -end
    66         -
    67         -
    68         -table.insert(scenario, {
    69         -	id = 'starsoul_scenario:imperialExpat';
    70         -	name = 'Imperial Expat';
    71         -	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         -
    73         -	species = 'human';
    74         -	speciesVariant = 'female';
    75         -	soul = {
    76         -		externalChannel = true; -- able to touch other souls in the spiritual realm
    77         -		physicalChannel = true; -- able to extend influence into physical realm
    78         -		damage = 1;
    79         -	};
    80         -	social = {
    81         -		empire = 'workingClass';
    82         -		commune = 'metic';
    83         -	};
    84         -
    85         -	startingItems = {
    86         -		suit = ItemStack('starsoul_suit:suit_survival_commune');
    87         -		suitBatteries = {battery 'starsoul_electronics:battery_carbon_commune_small'};
    88         -		suitChips = {
    89         -			chipLibrary.survivalware;
    90         -			-- you didn't notice it earlier, but your Commune environment suit
    91         -			-- came with this chip already plugged in. it's apparently true
    92         -			-- what they say: the Commune is always prepared for everything.
    93         -			-- E V E R Y T H I N G.
    94         -		};
    95         -		suitGuns = {};
    96         -		suitAmmo = {};
    97         -		suitCans = {
    98         -			ItemStack('starsoul_material:canister_small');
    99         -		};
   100         -		carry = {
   101         -			chipLibrary.compendium;
   102         -			-- you bought this on a whim before you left the Empire, and
   103         -			-- just happened to still have it on your person when everything
   104         -			-- went straight to the Wild Gods' privy
   105         -		};
   106         -	};
   107         -})
   108         -
   109         -table.insert(scenario, {
   110         -	id = 'starsoul_scenario:gentlemanAdventurer';
   111         -	--       Othar Tryggvasson,
   112         -	name = 'Gentleman Adventurer';
   113         -	desc = "Tired of the same-old-same-old, sick of your idiot contemporaries, exasperated with the shallow soul-rotting luxury of life as landless lordling, and earnestly eager to enrage your father, you resolved to see the Reach in all her splendor. Deftly evading the usual tourist traps, you finagled your way into the confidence of the Commune ambassador with a few modest infusions of Father's money -- now *that* should pop his monocle -- and secured yourself a seat on a ride to their brand-new colony at Thousand Petal. How exciting -- a genuine frontier outing!";
   114         -
   115         -	species = 'human';
   116         -	speciesVariant = 'male';
   117         -	soul = {
   118         -		externalChannel = true;
   119         -		physicalChannel = true;
   120         -		damage = 1;
   121         -	};
   122         -	social = {
   123         -		empire = 'lord';
   124         -	};
   125         -
   126         -	startingItems = {
   127         -		suit = 'starsoul_suit:suit_survival_imperial';
   128         -		suitBatteries = {battery 'starsoul_electronics:battery_supercapacitor_imperial_mid'};
   129         -		suitChips = {
   130         -			chipLibrary.compendium;
   131         -			-- Mother, bless her soul, simply insisted on buying you this as a parting
   132         -			-- gift. "it's dangerous out there for a young man," she proclaimed as
   133         -			-- if she had profound firsthand experience of the matter. mindful of the
   134         -			-- husband she endures, you suffered to humor her, and made a big show of
   135         -			-- installing it your brand-new nanosuit before you fled the family seat.
   136         -		};
   137         -		suitGuns = {};
   138         -		suitAmmo = {};
   139         -		suitCans = {
   140         -			ItemStack('starsoul_material:canister_mid');
   141         -		};
   142         -		carry = {};
   143         -	};
   144         -})
   145         -
   146         -table.insert(scenario, {
   147         -	-- you start out with strong combat abilities but weak engineering,
   148         -	-- and will have to scavenge wrecks to find basic crafting gear
   149         -	id = 'starsoul_scenario:terroristTagalong';
   150         -	name = 'Terrorist Tagalong';
   151         -	desc = "It turns out there's a *reason* Crown jobs pay so well.";
   152         -	species = 'human';
   153         -	speciesVariant = 'female';
   154         -	social = {
   155         -		empire = 'lowlife';
   156         -		commune = 'mostWanted';
   157         -		underworldConnections = true;
   158         -	};
   159         -	soul = {
   160         -		externalChannel = true;
   161         -		physicalChannel = true;
   162         -		damage = 2; -- closer to the blast
   163         -	};
   164         -	startingItems = {
   165         -		suit = 'starsoul_suit:suit_combat_imperial';
   166         -		suitBatteries = {
   167         -			ItemStack('starsoul_electronics:battery_supercapacitor_imperial_small');
   168         -			ItemStack('starsoul_electronics:battery_chemical_imperial_large');
   169         -		};
   170         -		suitGuns = {};
   171         -		suitAmmo = {};
   172         -		carry = {};
   173         -	};
   174         -	suitChips = {chipLibrary.misfortune};
   175         -})
   176         -
   177         -table.insert(scenario, {
   178         -	id = 'starsoul_scenario:tradebirdBodyguard';
   179         -	name = 'Tradebird Bodyguard';
   180         -	desc = "You've never understood why astropaths of all people *insist* on bodyguards. This one could probably make hash of a good-sized human batallion, if her feathers were sufficiently ruffled. Perhaps it's a status thing. Whatever the case, it's easy money.\nAt least, it was supposed to be.'";
   181         -	species = 'usukwinya';
   182         -	speciesVariant = 'male';
   183         -	soul = {
   184         -		damage = 0; -- Inyukiriku and her entourage fled the ship when she sensed something serious was about to go down. lucky: the humans only survived because their souls were closed off to the Physical. less luckily, the explosion knocked your escape pod off course and the damn astropath was too busy saving her own skin to come after you
   185         -		externalChannel = true; -- usukwinya are already psionic
   186         -		physicalChannel = true; -- usukwinya are Starsouls
   187         -	};
   188         -	startingItems = {
   189         -		suit = 'starsoul_suit:suit_combat_usukwinya';
   190         -		suitBatteries = {
   191         -			ItemStack('starsoul_electronics:battery_hybrid_usukwinya_mid');
   192         -		};
   193         -		suitGuns = {};
   194         -		suitChips = {};
   195         -		suitAmmo = {};
   196         -		carry = {};
   197         -	};
   198         -})

Deleted mods/starsoul-scenario/mod.conf version [cb3b4db72f].

     1         -name = starsoul_scenario
     2         -description = built-in scenarios for Starsoul
     3         -depends = starsoul, starsoul_suit, starsoul_electronics, starsoul_building, starsoul_material
     4         -# be sure to add any mods from which you list new starting items!

Deleted mods/starsoul-secrets/init.lua version [f98f7f7b59].

     1         -----------------------------------------------------
     2         -------------- CONTROLLED INFORMATION  --------------
     3         -----------------------------------------------------
     4         --- THE INFORMATION CONTAINED IN THIS DOCUMENT IS  --
     5         --- SUBJECT TO RESTRAINT OF TRANSMISSION PER THE   --
     6         --- TERMS OF THE COMMUNE CHARTER INFOSECURITY      --
     7         --- PROVISION. IF YOU ARE NOT AUTHORIZED UNDER THE --
     8         --- AEGIS OF THE APPROPRITE CONTROLLING AUTHORITY, --
     9         --- CLOSE THIS DOCUMENT IMMEDIATELY AND REPORT THE --
    10         --- SECURITY BREACH TO YOUR DESIGNATED INFORMATION --
    11         --- HYGIENE OVERSEER OR FACE CORRECTIVE DISCIPLINE --
    12         -----------------------------------------------------
    13         -
    14         -local lib = starsoul.mod.lib
    15         -local sec = {}
    16         -starsoul.mod.secrets = sec
    17         -
    18         -sec.index = lib.registry.mk 'starsoul_secrets:secret'
    19         -
    20         ---[==[
    21         -
    22         -a secret is a piece of information that is made available
    23         -for review once certain conditions are met. despite the name,
    24         -it doesn't necessarily have to be secret -- it could include
    25         -e.g. journal entries about a character's background. a secret
    26         -is defined in the following manner:
    27         -
    28         -{
    29         -	title = the string that appears in the UI
    30         -	stages = {
    31         -		{
    32         -			prereqs = {
    33         -				{kind = 'fact', id = 'starsoul:terroristEmployer'}
    34         -				{kind = 'item', id = 'starsoul_electronic:firstbornDoomBong'}
    35         -				{kind = 'background', id = 'starsoul:terroristTagalong'}
    36         -			}
    37         -			body = {
    38         -				'the firstborn smonked hella weed';
    39         -			};
    40         -			-- body can also be a function(user,secret)
    41         -		}
    42         -   }
    43         -}
    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         -a secret can be a single piece of information predicated
    49         -on a fact, in which case the secret and fact should share
    50         -the same ID. the ID should be as non-indicative as possible
    51         -to avoid spoilers for devs of unrelated code.
    52         -
    53         -a secret can also be manually unlocked e.g. by using an item
    54         -
    55         -]==]--
    56         -
    57         -function sec.prereqCheck(user, pr)
    58         -end

Deleted mods/starsoul-secrets/mod.conf version [26c2999a4f].

     1         -name = starsoul_secrets
     2         -title = starsoul secrets
     3         -description = TS//NOFORN
     4         -depends = starsoul

Deleted mods/starsoul-suit/init.lua version [d4fabd1162].

     1         -local lib = starsoul.mod.lib
     2         -local fab = starsoul.type.fab
     3         -
     4         -local facDescs = {
     5         -	commune = {
     6         -		survival = {
     7         -			suit = 'A light, simple, bare-bones environment suit that will provide heating, cooling, and nanide support to a stranded cosmonaut';
     8         -			cc = 'The Survival Suit uses compact thermoelectrics to keep the wearer perfectly comfortable in extremes of heat or cold. It makes up for its heavy power usage with effective insulation that substantially reduces any need for climate control.';
     9         -		};
    10         -		engineer = {
    11         -			suit = 'A lightweight environment suit designed for indoor work, the Commune\'s Engineer Suit boasts advanced nanotech capable of constructing objects in place.';
    12         -			cc = 'The Engineer Suit is designed for indoor work. Consequently, it features only a low-power thermoelectric cooler meant to keep its wearer comfortable during strenuous work.';
    13         -		};
    14         -		combat = {
    15         -			suit = 'A military-grade suit with the latest Commune technology. Designed for maximum force multiplication, the suit has dual weapon hardpoints and supports a gargantuan power reserve. Its nanotech systems are specialized for tearing through obstacles, but can also be used to manufacturer ammunition in a pinch.';
    16         -			cc = 'This Combat Suit uses electrothermal cooling to keep an active soldier comfortable and effective, as well as conventional heating coils to enable operation in hostile atmospheres.';
    17         -		};
    18         -	};
    19         -
    20         -}
    21         -
    22         -
    23         -starsoul.world.tier.foreach('starsoul:suit-gen', {}, function(tid, t)
    24         -	local function hasTech(tech)
    25         -		return starsoul.world.tier.tech(tid, tech)
    26         -	end
    27         -	if not hasTech 'suit' then return end
    28         -	-- TODO tier customization
    29         -	--
    30         -	local function fabsum(f)
    31         -		return starsoul.world.tier.fabsum(tid, 'suit')
    32         -	end
    33         -	local function fabReq(sz, days)
    34         -		local tierMatBase = (
    35         -			(fabsum 'electric' * 4) +
    36         -			(fabsum 'basis' + fabsum 'suit') * sz
    37         -		)
    38         -		local b = tierMatBase + fab {
    39         -			-- universal suit requirements
    40         -			time = { print    = 60*60*24 * days };
    41         -			size = { printBay = sz              };
    42         -		}
    43         -		b.flag = lib.tbl.set('print');
    44         -		return b
    45         -	end
    46         -	local function facDesc(s, t)
    47         -		local default = 'A protective nanosuit' -- FIXME
    48         -		if not facDescs[tid] then return default end
    49         -		if not facDescs[tid][s] then return default end
    50         -		if not facDescs[tid][s][t] then return default end
    51         -		return facDescs[tid][s][t]
    52         -	end
    53         -	starsoul.item.suit.link('starsoul_suit:suit_survival_' .. tid, {
    54         -		name = t.name .. ' Survival Suit';
    55         -		desc = facDesc('survival','suit');
    56         -		fab = fabReq(1, 2.2) + fab { };
    57         -		tex = {
    58         -			plate = {
    59         -				id = 'starsoul-suit-survival-plate';
    60         -				tint = lib.color {hue = 210, sat = .5, lum = .5};
    61         -			};
    62         -			lining = {
    63         -				id = 'starsoul-suit-survival-lining';
    64         -				tint = lib.color {hue = 180, sat = .2, lum = .7};
    65         -			};
    66         -		};
    67         -		tints = {'suit_plate', 'suit_lining'};
    68         -		temp = {
    69         -			desc = facDesc('survival','cc');
    70         -			maxHeat = 0.7; -- can produce a half-degree Δ per second
    71         -			maxCool = 0.5;
    72         -			heatPower = 50; -- 50W
    73         -			coolPower = 50/t.efficiency;
    74         -			insulation = 0.5; -- prevent half of heat loss
    75         -		};
    76         -		protection = {
    77         -			rad = 0.7; -- blocks 70% of ionizing radiation
    78         -		};
    79         -		slots = {
    80         -			canisters = 1;
    81         -			batteries = math.ceil(math.max(1, t.power/2));
    82         -			chips = 3;
    83         -			guns = 0;
    84         -			ammo = 0;
    85         -		};
    86         -		nano = {
    87         -			compileSpeed = 0.1 * t.efficiency;
    88         -			shredSpeed = 0.1 * t.power;
    89         -			fabSizeLimit = 0.6; -- 60cm
    90         -		};
    91         -	})
    92         -
    93         -	starsoul.item.suit.link('starsoul_suit:suit_engineer_' .. tid, {
    94         -		name = t.name .. ' Engineer Suit';
    95         -		desc = facDesc('engineer','suit');
    96         -		tex = {
    97         -			plate = {
    98         -				id = 'starsoul-suit-survival-plate';
    99         -				tint = lib.color {hue = 0, sat = .5, lum = .7};
   100         -			};
   101         -		};
   102         -		tints = {'suit_plate', 'suit_lining'};
   103         -		fab = fabReq(.8, 7) + fab { };
   104         -		temp = {
   105         -			desc = facDesc('engineer','cc');
   106         -			maxHeat = 0;
   107         -			maxCool = 0.2;
   108         -			heatPower = 0;
   109         -			coolPower = 10 / t.efficiency;
   110         -			insulation = 0.1; -- no lining
   111         -		};
   112         -		slots = {
   113         -			canisters = 2;
   114         -			batteries = 2;
   115         -			chips = 6;
   116         -			guns = 0;
   117         -			ammo = 0;
   118         -		};
   119         -		compat = {
   120         -			maxBatterySize = 0.10 * t.power; -- 10cm
   121         -		};
   122         -		protection = {
   123         -			rad = 0.1; -- blocks 10% of ionizing radiation
   124         -		};
   125         -		nano = {
   126         -			compileSpeed = 1 * t.efficiency;
   127         -			shredSpeed = 0.7 * t.power;
   128         -			fabSizeLimit = 1.5; -- 1.5m (enables node compilation)
   129         -		};
   130         -	})
   131         -
   132         -	if hasTech 'suitCombat' then
   133         -		starsoul.item.suit.link('starsoul_suit:suit_combat_' .. tid, {
   134         -			name = t.name .. ' Combat Suit';
   135         -			desc = facDesc('combat','suit');
   136         -			fab = fabReq(1.5, 14) + fab {
   137         -				metal = {iridium = 1e3};
   138         -			};
   139         -			tex = {
   140         -				plate = {
   141         -					id = 'starsoul-suit-survival-plate';
   142         -					tint = lib.color {hue = 0, sat = 0, lum = 0};
   143         -				};
   144         -				lining = {
   145         -					id = 'starsoul-suit-survival-lining';
   146         -					tint = lib.color {hue = 180, sat = .5, lum = .3};
   147         -				};
   148         -			};
   149         -			tints = {'suit_plate', 'suit_lining'};
   150         -			slots = {
   151         -				canisters = 1;
   152         -				batteries = math.ceil(math.max(3, 8*(t.power/2)));
   153         -				chips = 5;
   154         -				guns = 2;
   155         -				ammo = 1;
   156         -			};
   157         -			compat = {
   158         -				maxBatterySize = 0.10 * t.power; -- 10cm
   159         -			};
   160         -			temp = {
   161         -				desc = facDesc('combat','cc');
   162         -				maxHeat = 0.3; 
   163         -				maxCool = 0.6;
   164         -				heatPower = 20 / t.efficiency; 
   165         -				coolPower = 40 / t.efficiency;
   166         -				insulation = 0.2; 
   167         -			};
   168         -			protection = {
   169         -				rad = 0.9; -- blocks 90% of ionizing radiation
   170         -			};
   171         -			nano = {
   172         -				compileSpeed = 0.05;
   173         -				shredSpeed = 2 * t.power;
   174         -				fabSizeLimit = 0.3; -- 30cm
   175         -			};
   176         -		})
   177         -	end
   178         -end)
   179         -
   180         -

Deleted mods/starsoul-suit/mod.conf version [dbff224e42].

     1         -name = starsoul_suit
     2         -description = defines the environment suits available in starsoul
     3         -depends = starsoul, starsoul_electronics

Deleted mods/starsoul/container.lua version [72968ffd62].

     1         --- a container item defines a 'container' structure listing its
     2         --- inventories and their properties. a container object is created
     3         --- in order to interact with a container
     4         -local lib = starsoul.mod.lib
     5         -starsoul.item.container = lib.class {
     6         -	__name = 'starsoul:container';
     7         -	construct = function(stack, inv, def)
     8         -		local T,G = lib.marshal.t, lib.marshal.g
     9         -		local cdef = stack:get_definition()._starsoul.container;
    10         -		local sd = {}
    11         -		for k,v in pairs(cdef.list) do
    12         -			sd[k] = {
    13         -				key  = v.key;
    14         -				type = T.inventoryList;
    15         -			}
    16         -		end
    17         -		return {
    18         -			stack = stack, inv = inv, pdef = def, cdef = cdef;
    19         -			store = lib.marshal.metaStore(sd)(stack);
    20         -		}
    21         -	end;
    22         -	__index = {
    23         -		slot = function(self, id)
    24         -			return string.format("%s_%s", self.pdef.pfx, id)
    25         -		end;
    26         -		clear = function(self) -- initialize or empty the metadata
    27         -			self:update(function()
    28         -				for k,v in pairs(self.cdef.list) do
    29         -					if v.sz > 0 then
    30         -						self.store.write(k, {})
    31         -					end
    32         -				end
    33         -			end)
    34         -		end;
    35         -		list = function(self, k) return self.store.read(k) end;
    36         -		read = function(self)
    37         -			local lst = {}
    38         -			for k,v in pairs(self.cdef.list) do
    39         -				if v.sz > 0 then lst[k] = self:list(k) end
    40         -			end
    41         -			return lst
    42         -		end;
    43         -		pull = function(self) -- align the inventories with the metadata
    44         -			for k,v in pairs(self.cdef.list) do
    45         -				if v.sz > 0 then
    46         -					local stacks = self:list(k)
    47         -					local sid    = self:slot(k)
    48         -					self.inv:set_size(sid, v.sz)
    49         -					self.inv:set_list(sid, stacks)
    50         -				end
    51         -			end
    52         -		end;
    53         -		update = function(self, fn)
    54         -			local old = ItemStack(self.stack)
    55         -			if fn then fn() end
    56         -			if self.cdef.handle then
    57         -				self.cdef.handle(self.stack, old)
    58         -			end
    59         -		end;
    60         -		push = function(self) -- align the metadata with the inventories
    61         -			self:update(function()
    62         -				for k,v in pairs(self.cdef.list) do
    63         -					if v.sz > 0 then
    64         -						local sid = self:slot(k)
    65         -						local lst = self.inv:get_list(sid)
    66         -						self.store.write(k, lst)
    67         -					end
    68         -				end
    69         -			end)
    70         -		end;
    71         -		drop = function(self) -- remove the inventories from the node/entity
    72         -			for k,v in pairs(self.cdef.list) do
    73         -				local sid = self:slot(k)
    74         -				self.inv:set_size(sid, 0)
    75         -			end
    76         -		end;
    77         -		slotAccepts = function(self, lst, slot, stack)
    78         -		end;
    79         -	};
    80         -}
    81         -
    82         -function starsoul.item.container.dropPrefix(inv, pfx)
    83         -	local lists = inv:get_lists()
    84         -	for k,v in pairs(lists) do
    85         -		if #k > #pfx then
    86         -			if string.sub(k, 1, #pfx + 1) == pfx .. '_' then
    87         -				inv:set_size(k, 0)
    88         -			end
    89         -		end
    90         -	end
    91         -end

Deleted mods/starsoul/effect.lua version [2bec98314f].

     1         --- ported from sorcery/spell.lua, hence the lingering refs to "magic"
     2         ---
     3         --- this file is used to track active effects, for the purposes of metamagic
     4         --- like disjunction. a "effect" is a table consisting of several properties:
     5         --- a "disjoin" function that, if present, is called when the effect is
     6         --- abnormally interrupted, a "terminate" function that calls when the effect
     7         --- completes, a "duration" property specifying how long the effect lasts in
     8         --- seconds, and a "timeline" table that maps floats to functions called at 
     9         --- specific points during the function's activity. it can also have a
    10         --- 'delay' property that specifies how long to wait until the effect sequence
    11         --- starts; the effect is however still vulnerable to disjunction during this
    12         --- period. there can also be a sounds table that maps timepoints to sounds
    13         --- the same way timeline does. each value should be a table of form {sound,
    14         --- where}. the `where` field may contain one of 'pos', 'caster', 'subjects', or
    15         --- a vector specifying a position in the world, and indicate where the sound
    16         --- should be played. by default 'caster' and 'subjects' sounds will be attached
    17         --- to the objects they reference; 'attach=false' can be added to prevent this.
    18         --- by default sounds will be faded out quickly when disjunction occurs; this
    19         --- can be controlled by the fade parameter.
    20         ---
    21         --- effects can have various other properties, for instance 'disjunction', which
    22         --- when true prevents other effects from being cast in its radius while it is
    23         --- still in effect. disjunction is absolute; there is no way to overwhelm it.
    24         ---
    25         --- the effect also needs at least one of "anchor", "subjects", or "caster".
    26         ---  * an anchor is a position that, in combination with 'range', specifies the area
    27         ---    where a effect is in effect; this is used for determining whether it
    28         ---    is affected by a disjunction that incorporates part of that position
    29         ---  * subjects is an array of individuals affected by the effect. when
    30         ---    disjunction is cast on one of them, they will be removed from the
    31         ---    table. each entry should have at least a 'player' field; they can
    32         ---    also contain any other data useful to the effect. if a subject has
    33         ---    a 'disjoin' field it must be a function called when they are removed
    34         ---    from the list of effect targets.
    35         ---  * caster is the individual who cast the effect, if any. a disjunction
    36         ---    against their person will totally disrupt the effect.
    37         -local log = starsoul.logger 'effect'
    38         -local lib = starsoul.mod.lib
    39         -
    40         --- FIXME saving object refs is iffy, find a better alternative
    41         -starsoul.effect = {
    42         -	active = {}
    43         -}
    44         -
    45         -local get_effect_positions = function(effect)
    46         -	local effectpos
    47         -	if effect.anchor then
    48         -		effectpos = {effect.anchor}
    49         -	elseif effect.attach then
    50         -		if effect.attach == 'caster' then
    51         -			effectpos = {effect.caster:get_pos()}
    52         -		elseif effect.attach == 'subjects' or effect.attach == 'both' then
    53         -			if effect.attach == 'both' then
    54         -				effectpos = {effect.caster:get_pos()}
    55         -			else effectpos = {} end
    56         -			for _,s in pairs(effect.subjects) do
    57         -				effectpos[#effectpos+1] = s.player:get_pos()
    58         -			end
    59         -		else effectpos = {effect.attach:get_pos()} end
    60         -	else assert(false) end
    61         -	return effectpos
    62         -end
    63         -
    64         -local ineffectrange = function(effect,pos,range)
    65         -	local effectpos = get_effect_positions(effect)
    66         -
    67         -	for _,p in pairs(effectpos) do
    68         -		if vector.equals(pos,p) or
    69         -			(range       and lib.math.vdcomp(range,      pos,p)<=1) or
    70         -			(effect.range and lib.math.vdcomp(effect.range,p,pos)<=1) then
    71         -			return true
    72         -		end
    73         -	end
    74         -	return false
    75         -end
    76         -
    77         -starsoul.effect.probe = function(pos,range)
    78         -	-- this should be called before any effects are performed.
    79         -	-- other mods can overlay their own functions to e.g. protect areas
    80         -	-- from effects
    81         -	local result = {}
    82         -
    83         -	-- first we need to check if any active injunctions are in effect
    84         -	-- injunctions are registered as effects with a 'disjunction = true'
    85         -	-- property
    86         -	for id,effect in pairs(starsoul.effect.active) do
    87         -		if not (effect.disjunction and (effect.anchor or effect.attach)) then goto skip end
    88         -		if ineffectrange(effect,pos,range) then
    89         -			result.disjunction = true
    90         -			break
    91         -		end
    92         -	::skip::end
    93         -	
    94         -	-- at some point we might also check to see if certain anti-effect
    95         -	-- blocks are nearby or suchlike. there could also be regions where
    96         -	-- perhaps certain kinds of effect are unusually empowered or weak
    97         -	return result
    98         -end
    99         -starsoul.effect.disjoin = function(d)
   100         -	local effects,targets = {},{}
   101         -	if d.effect then effects = {{v=d.effect}}
   102         -	elseif d.target then targets = {d.target}
   103         -	elseif d.pos then -- find effects anchored here and people in range
   104         -		for id,effect in pairs(starsoul.effect.active) do
   105         -			if not effect.anchor then goto skip end -- this intentionally excludes attached effects
   106         -			if ineffectrange(effect,d.pos,d.range) then
   107         -				effects[#effects+1] = {v=effect,i=id}
   108         -			end
   109         -		::skip::end
   110         -		local ppl = minetest.get_objects_inside_radius(d.pos,d.range)
   111         -		if #targets == 0 then targets = ppl else
   112         -			for _,p in pairs(ppl) do targets[#targets+1] = p end
   113         -		end
   114         -	end
   115         -
   116         -	-- iterate over targets to remove from any effect's influence
   117         -	for _,t in pairs(targets) do
   118         -		for id,effect in pairs(starsoul.effect.active) do
   119         -			if effect.caster == t then effects[#effects+1] = {v=effect,i=id} else
   120         -				for si, sub in pairs(effect.subjects) do
   121         -					if sub.player == t then
   122         -						if sub.disjoin then sub:disjoin(effect) end
   123         -						effect.release_subject(si)
   124         -						break
   125         -					end
   126         -				end
   127         -			end
   128         -		end
   129         -	end
   130         -
   131         -	-- effects to disjoin entirely
   132         -	for _,s in pairs(effects) do local effect = s.v
   133         -		if effect.disjoin then effect:disjoin() end
   134         -		effect.abort()
   135         -		if s.i then starsoul.effect.active[s.i] = nil else
   136         -			for k,v in pairs(starsoul.effect.active) do
   137         -				if v == effect then starsoul.effect.active[k] = nil break end
   138         -			end
   139         -		end
   140         -	end
   141         -end
   142         -
   143         -starsoul.effect.ensorcelled = function(player,effect)
   144         -	if type(player) == 'string' then player = minetest.get_player_by_name(player) end
   145         -	for _,s in pairs(starsoul.effect.active) do
   146         -		if effect and (s.name ~= effect) then goto skip end
   147         -		for _,sub in pairs(s.subjects) do
   148         -			if sub.player == player then return s end
   149         -		end
   150         -	::skip::end
   151         -	return false
   152         -end
   153         -
   154         -starsoul.effect.each = function(player,effect)
   155         -	local idx = 0
   156         -	return function()
   157         -		repeat idx = idx + 1
   158         -			local sp = starsoul.effect.active[idx]
   159         -			if sp == nil then return nil end
   160         -			if effect == nil or sp.name == effect then
   161         -				for _,sub in pairs(sp.subjects) do
   162         -					if sub.player == player then return sp end
   163         -				end
   164         -			end
   165         -		until idx >= #starsoul.effect.active
   166         -	end
   167         -end
   168         -
   169         --- when a new effect is created, we analyze it and make the appropriate calls
   170         --- to minetest.after to queue up the events. each job returned needs to be
   171         --- saved in 'jobs' so they can be canceled if the effect is disjoined. no polling
   172         --- necessary :D
   173         -
   174         -starsoul.effect.cast = function(proto)
   175         -	local s = table.copy(proto)
   176         -	s.jobs = s.jobs or {} s.vfx = s.vfx or {} s.sfx = s.sfx or {}
   177         -	s.impacts = s.impacts or {} s.subjects = s.subjects or {}
   178         -	s.delay = s.delay or 0
   179         -	s.visual = function(subj, def)
   180         -		s.vfx[#s.vfx + 1] = {
   181         -			handle = minetest.add_particlespawner(def);
   182         -			subject = subj;
   183         -		}
   184         -	end
   185         -	s.visual_caster = function(def) -- convenience function
   186         -		local d = table.copy(def)
   187         -		d.attached = s.caster
   188         -		s.visual(nil, d)
   189         -	end
   190         -	s.visual_subjects = function(def)
   191         -		for _,sub in pairs(s.subjects) do
   192         -			local d = table.copy(def)
   193         -			d.attached = sub.player
   194         -			s.visual(sub, d)
   195         -		end
   196         -	end
   197         -	s.affect = function(i)
   198         -		local etbl = {}
   199         -		for _,sub in pairs(s.subjects) do
   200         -			-- local eff = late.new_effect(sub.player, i)
   201         -			-- starsoul will not be using late
   202         -			local rec = {
   203         -				effect = eff;
   204         -				subject = sub;
   205         -			}
   206         -			s.impacts[#s.impacts+1] = rec
   207         -			etbl[#etbl+1] = rec
   208         -		end
   209         -		return etbl
   210         -	end
   211         -	s.abort = function()
   212         -		for _,j in ipairs(s.jobs) do j:cancel() end
   213         -		for _,v in ipairs(s.vfx) do minetest.delete_particlespawner(v.handle) end
   214         -		for _,i in ipairs(s.sfx) do s.silence(i) end
   215         -		for _,i in ipairs(s.impacts) do i.effect:stop() end
   216         -	end
   217         -	s.release_subject = function(si)
   218         -		local t = s.subjects[si]
   219         -		for _,f in pairs(s.sfx)     do if f.subject == t then s.silence(f) end end
   220         -		for _,f in pairs(s.impacts) do if f.subject == t then f.effect:stop() end end
   221         -		for _,f in pairs(s.vfx) do
   222         -			if f.subject == t then minetest.delete_particlespawner(f.handle) end
   223         -		end
   224         -		s.subjects[si] = nil
   225         -	end
   226         -	local interpret_timespec = function(when)
   227         -		if when == nil then return 0 end
   228         -		local t if type(when) == 'number' then
   229         -			t = s.duration * when
   230         -		else
   231         -			t = (s.duration * (when.whence or 0)) + (when.secs or 0)
   232         -		end
   233         -		if t then return math.min(s.duration,math.max(0,t)) end
   234         -
   235         -		log.err('invalid timespec ' .. dump(when))
   236         -		return 0
   237         -	end
   238         -	s.queue = function(when,fn)
   239         -		local elapsed = s.starttime and minetest.get_server_uptime() - s.starttime or 0
   240         -		local timepast = interpret_timespec(when)
   241         -		if not timepast then timepast = 0 end
   242         -		local timeleft = s.duration - timepast
   243         -		local howlong = (s.delay + timepast) - elapsed
   244         -		if howlong < 0 then
   245         -			log.err('cannot time-travel! queue() called with `when` specifying timepoint that has already passed')
   246         -			howlong = 0
   247         -		end
   248         -		s.jobs[#s.jobs+1] = minetest.after(howlong, function()
   249         -			-- this is somewhat awkward. since we're using a non-polling approach, we
   250         -			-- need to find a way to account for a caster or subject walking into an
   251         -			-- existing antimagic field, or someone with an existing antimagic aura
   252         -			-- walking into range of the anchor. so every time a effect effect would
   253         -			-- take place, we first check to see if it's in range of something nasty
   254         -			if not s.disjunction and -- avoid self-disjunction
   255         -				((s.caster and starsoul.effect.probe(s.caster:get_pos()).disjunction) or
   256         -				 (s.anchor and starsoul.effect.probe(s.anchor,s.range).disjunction)) then
   257         -				starsoul.effect.disjoin{effect=s}
   258         -			else
   259         -				if not s.disjunction then for _,sub in pairs(s.subjects) do
   260         -					local sp = sub.player:get_pos()
   261         -					if starsoul.effect.probe(sp).disjunction then
   262         -						starsoul.effect.disjoin{pos=sp}
   263         -					end
   264         -				end end
   265         -				-- effect still exists and we've removed any subjects who have been
   266         -				-- affected by a disjunction effect, it's now time to actually perform
   267         -				-- the queued-up action
   268         -				fn(s,timepast,timeleft)
   269         -			end
   270         -		end)
   271         -	end
   272         -	s.play_now = function(spec)
   273         -		local specs, stbl = {}, {}
   274         -		local addobj = function(obj,sub)
   275         -			if spec.attach == false then specs[#specs+1] = {
   276         -				spec = { pos = obj:get_pos() };
   277         -				obj = obj, subject = sub;
   278         -			} else specs[#specs+1] = {
   279         -				spec = { object = obj };
   280         -				obj = obj, subject = sub;
   281         -			} end
   282         -		end
   283         -
   284         -		if spec.where == 'caster' then addobj(s.caster)
   285         -		elseif spec.where == 'subjects' then
   286         -			for _,sub in pairs(s.subjects) do addobj(sub.player,sub) end
   287         -		elseif spec.where == 'pos' then specs[#specs+1] = { spec = {pos = s.anchor} }
   288         -		else specs[#specs+1] = { spec = {pos = spec.where} } end
   289         -
   290         -		for _,sp in pairs(specs) do
   291         -			sp.spec.gain = sp.spec.gain or spec.gain
   292         -			local so = {
   293         -				handle = minetest.sound_play(spec.sound, sp.spec, spec.ephemeral);
   294         -				ctl = spec;
   295         -				-- object = sp.obj;
   296         -				subject = sp.subject;
   297         -			}
   298         -			stbl[#stbl+1] = so
   299         -			s.sfx[#s.sfx+1] = so
   300         -		end
   301         -		return stbl
   302         -	end
   303         -	s.play = function(when,spec)
   304         -		s.queue(when, function()
   305         -			local snds = s.play_now(spec)
   306         -			if spec.stop then
   307         -				s.queue(spec.stop, function()
   308         -					for _,snd in pairs(snds) do s.silence(snd) end
   309         -				end)
   310         -			end
   311         -		end)
   312         -	end
   313         -	s.silence = function(sound)
   314         -		if sound.ctl.fade == 0 then minetest.sound_stop(sound.handle)
   315         -		else minetest.sound_fade(sound.handle,sound.ctl.fade or 1,0) end
   316         -	end
   317         -	local startqueued, termqueued = false, false
   318         -	local myid = #starsoul.effect.active+1
   319         -	s.cancel = function()
   320         -		s.abort()
   321         -		starsoul.effect.active[myid] = nil
   322         -	end
   323         -	local perform_disjunction_calls = function()
   324         -		local positions = get_effect_positions(s)
   325         -		for _,p in pairs(positions) do
   326         -			starsoul.effect.disjoin{pos = p, range = s.range}
   327         -		end
   328         -	end
   329         -	if s.timeline then
   330         -		for when_raw,what in pairs(s.timeline) do
   331         -			local when = interpret_timespec(when_raw)
   332         -			if s.delay == 0 and when == 0 then
   333         -				startqueued = true
   334         -				if s.disjunction then perform_disjunction_calls() end
   335         -				what(s,0,s.duration)
   336         -			elseif when_raw == 1 or when >= s.duration then -- avoid race conditions
   337         -				if not termqueued then
   338         -					termqueued = true
   339         -					s.queue(1,function(s,...)
   340         -						what(s,...)
   341         -						if s.terminate then s:terminate() end
   342         -						starsoul.effect.active[myid] = nil
   343         -					end)
   344         -				else
   345         -					log.warn('multiple final timeline events not possible, ignoring')
   346         -				end
   347         -			elseif when == 0 and s.disjunction then
   348         -				startqueued = true
   349         -				s.queue(when_raw,function(...)
   350         -					perform_disjunction_calls()
   351         -					what(...)
   352         -				end)
   353         -			else s.queue(when_raw,what) end
   354         -		end
   355         -	end
   356         -	if s.intervals then
   357         -		for _,int in pairs(s.intervals) do
   358         -			local timeleft = s.duration - interpret_timespec(int.after)
   359         -			local iteration, itercount = 0, timeleft / int.period
   360         -			local function iterate(lastreturn)
   361         -				iteration = iteration + 1
   362         -				local nr = int.fn {
   363         -					effect = s;
   364         -					iteration = iteration;
   365         -					iterationcount = itercount;
   366         -					timeleft = timeleft;
   367         -					timeelapsed = s.duration - timeleft;
   368         -					lastreturn = lastreturn;
   369         -				}
   370         -				if nr ~= false and iteration < itercount then
   371         -					s.jobs[#s.jobs+1] = minetest.after(int.period,
   372         -						function() iterate(nr) end)
   373         -				end
   374         -			end
   375         -			if int.after
   376         -				then s.queue(int.after, iterate)
   377         -				else s.queue({whence=0, secs=s.period}, iterate)
   378         -			end
   379         -		end
   380         -	end
   381         -	if s.disjunction and not startqueued then
   382         -		if s.delay == 0 then perform_disjunction_calls() else
   383         -			s.queue(0, function() perform_disjunction_calls() end)
   384         -		end
   385         -	end
   386         -	if s.sounds then
   387         -		for when,what in pairs(s.sounds) do s.play(when,what) end
   388         -	end
   389         -	starsoul.effect.active[myid] = s
   390         -	if not termqueued then
   391         -		s.jobs[#s.jobs+1] = minetest.after(s.delay + s.duration, function()
   392         -			if s.terminate then s:terminate() end
   393         -			starsoul.effect.active[myid] = nil
   394         -		end)
   395         -	end
   396         -	s.starttime = minetest.get_server_uptime()
   397         -	return s
   398         -end
   399         -
   400         -minetest.register_on_dieplayer(function(player)
   401         -	starsoul.effect.disjoin{target=player}
   402         -end)

Deleted mods/starsoul/element.lua version [52033ae372].

     1         -local lib = starsoul.mod.lib
     2         -local W = starsoul.world
     3         -local M = W.material
     4         -
     5         -M.element.foreach('starsoul:sort', {}, function(id, m)
     6         -	if m.metal then
     7         -		M.metal.link(id, {
     8         -			name = m.name;
     9         -			composition = starsoul.type.fab{element = {[id] = 1}};
    10         -			color = m.color;
    11         -			-- n.b. this is a RATIO: it will be appropriately multiplied
    12         -			-- for the object in question; e.g a normal chunk will be
    13         -			-- 100 $element, an ingot will be 1000 $element
    14         -		})
    15         -	elseif m.gas then
    16         -		M.gas.link(id, {
    17         -			name = m.name;
    18         -			composition = starsoul.type.fab{element = {[id] = 1}};
    19         -		})
    20         -	elseif m.liquid then
    21         -		M.liquid.link(id, {
    22         -			name = m.name;
    23         -			composition = starsoul.type.fab{element = {[id] = 1}};
    24         -		})
    25         -	end
    26         -end)
    27         -
    28         -local F = string.format
    29         -
    30         -local function mkEltIndicator(composition)
    31         -	local indicator = ''
    32         -	local idx = 0
    33         -	local ccount = 0
    34         -	for _ in pairs(composition) do
    35         -		ccount = ccount + 1
    36         -	end
    37         -	local indsz,indpad = 28,4
    38         -	local ofs = math.min(11, (indsz-indpad)/ccount)
    39         -	for id, amt in pairs(composition) do
    40         -		idx = idx + 1
    41         -		indicator = indicator .. F(
    42         -			':%s,3=starsoul-element-%s.png',
    43         -			(indsz-indpad) - (idx*ofs), id
    44         -		)
    45         -	end
    46         -	indicator = lib.image(indicator)
    47         -	return function(s)
    48         -		return string.format('(%s^[resize:%sx%s)^[combine:%sx%s%s',
    49         -			s,
    50         -			indsz, indsz,
    51         -			indsz, indsz,
    52         -			indicator);
    53         -	end
    54         -end
    55         -
    56         -M.element.foreach('starsoul:gen-forms', {}, function(id, m)
    57         -	local eltID = F('%s:element_%s', minetest.get_current_modname(), id)
    58         -	local eltName = F('Elemental %s', lib.str.capitalize(m.name))
    59         -	local tt = function(t, d, g)
    60         -		return starsoul.ui.tooltip {
    61         -			title = t, desc = d;
    62         -			color = lib.color(0.1,0.2,0.1);
    63         -			props = {
    64         -				{title = 'Mass', desc = lib.math.si('g', g), affinity='info'}
    65         -			}
    66         -		}
    67         -	end
    68         -	local comp = {[id] = 1}
    69         -	local iblit = mkEltIndicator(comp)
    70         -	m.form = m.form or {}
    71         -	m.form.element = eltID
    72         -
    73         -	local powder = F('starsoul-element-%s-powder.png', id);
    74         -	minetest.register_craftitem(eltID, {
    75         -		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);
    78         -		wield_image = powder;
    79         -		stack_max = 1000; -- 1kg
    80         -		groups = {element = 1, powder = 1, specialInventory = 1};
    81         -		_starsoul = {
    82         -			mass = 1;
    83         -			material = {
    84         -				kind = 'element';
    85         -				element = id;
    86         -			};
    87         -			fab = starsoul.type.fab {
    88         -				element = comp;
    89         -			};
    90         -		};
    91         -	});
    92         -end)
    93         -
    94         -
    95         -M.metal.foreach('starsoul:gen-forms', {}, function(id, m)
    96         -	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))
   101         -	m.form = m.form or {}
   102         -	m.form.brick = brickID
   103         -	m.form.ingot = ingotID
   104         -	local tt = function(t, d, g)
   105         -		return starsoul.ui.tooltip {
   106         -			title = t, desc = d;
   107         -			color = lib.color(0.1,0.1,0.1);
   108         -			props = {
   109         -				{title = 'Mass', desc = lib.math.si('g', g), affinity='info'}
   110         -			}
   111         -		}
   112         -	end
   113         -	local mcomp = m.composition:elementalize().element
   114         -	local function comp(n)
   115         -		local t = {}
   116         -		for id, amt in pairs(mcomp) do
   117         -			t[id] = amt * n
   118         -		end
   119         -		return t
   120         -	end
   121         -	local iblit = mkEltIndicator(mcomp)
   122         -	local function img(s)
   123         -		return iblit(s:colorize(m.color):render())
   124         -	end
   125         -
   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 'starsoul-item-brick.png');
   130         -		wield_image = lib.image 'starsoul-item-brick.png':colorize(m.color):render();
   131         -		stack_max = 10;
   132         -		groups = {metal = 1, ingot = 1};
   133         -		_starsoul = {
   134         -			mass = 100;
   135         -			material = {
   136         -				kind = 'metal';
   137         -				metal = id;
   138         -			};
   139         -			fab = starsoul.type.fab {
   140         -				flag = {smelt= true};
   141         -				element = comp(1e2);
   142         -			};
   143         -		};
   144         -	});
   145         -
   146         -	minetest.register_craftitem(ingotID, {
   147         -		short_description = ingotName;
   148         -		description = tt(ingotName, F('A solid ingot of %s, ready to be worked by a large matter compiler', m.name), 1e3);
   149         -		inventory_image = img(lib.image('starsoul-item-ingot.png'));
   150         -		wield_image = lib.image 'starsoul-item-ingot.png':colorize(m.color):render();
   151         -		groups = {metal = 1, ingot = 1};
   152         -		stack_max = 5;
   153         -		_starsoul = {
   154         -			mass = 1e3;
   155         -			material = {
   156         -				kind = 'metal';
   157         -				metal = id;
   158         -			};
   159         -			fab = starsoul.type.fab {
   160         -				flag = {smelt= true};
   161         -				element = comp(1e3);
   162         -			};
   163         -		};
   164         -	});
   165         -
   166         -
   167         -end)
   168         -
   169         -local function canisterDesc(stack, def)
   170         -	def = def or stack:get_definition()._starsoul.canister
   171         -	local props = {
   172         -		{title = 'Charge Slots', affinity = 'info', desc = tostring(def.slots)};
   173         -	};
   174         -	if stack then
   175         -		local inv = starsoul.item.container(stack)
   176         -		for i,e in ipairs(inv:list 'elem') do
   177         -			local comp = e:get_definition()._starsoul.fab
   178         -			table.insert(props, {
   179         -				title = comp:formula();
   180         -				desc = lib.math.si('g', e:get_count());
   181         -				affinity = 'good';
   182         -			})
   183         -		end
   184         -		-- TODO list masses
   185         -	end
   186         -	return starsoul.ui.tooltip {
   187         -		title = def.name, desc = def.desc or 'A canister that can store a charge of elemental powder, gas, or liquid';
   188         -		color = lib.color(0.2,0.1,0.1);
   189         -		props = props;
   190         -	};	
   191         -end
   192         -
   193         -starsoul.item.canister = lib.registry.mk 'starsoul:canister';
   194         -starsoul.item.canister.foreach('starsoul:item-gen', {}, function(id, c)
   195         -	minetest.register_craftitem(id, {
   196         -		short_description = c.name;
   197         -		description = canisterDesc(nil, c);
   198         -		inventory_image = c.image or 'starsoul-item-element-canister.png';
   199         -		groups = {canister = 1};
   200         -		stack_max = 1;
   201         -		_starsoul = {
   202         -			canister = c;
   203         -			container = {
   204         -				handle = function(stack, oldstack)
   205         -					stack:get_meta():set_string('description', canisterDesc(stack))
   206         -					return stack
   207         -				end;
   208         -				list = {
   209         -					elem = {
   210         -						key = 'starsoul:canister_elem';
   211         -						accept = 'powder';
   212         -						sz = c.slots;
   213         -					};
   214         -				};
   215         -			};
   216         -		};
   217         -	})
   218         -end)

Deleted mods/starsoul/fab.lua version [d8c093fcd3].

     1         --- [ʞ] fab.lua
     2         ---  ~ lexi hale <lexi@hale.su>
     3         ---  🄯 EUPL1.2
     4         ---  ? fabrication spec class
     5         ---    a type.fab supports two operators:
     6         ---
     7         ---    + used for compounding recipes. that is,
     8         ---			a+b = compose a new spec from the spec parts a and b.
     9         ---      this is used e.g. for creating tier-based
    10         ---      fabspecs.
    11         ---
    12         ---    * used for determining quantities. that is,
    13         ---			f*x = spec to make x instances of f
    14         ---
    15         ---    new fab fields must be defined in starsoul.type.fab.opClass.
    16         ---    this maps a name to fn(a,b,n) -> quant, where a is the first
    17         ---    argument, b is a compounding amount, and n is a quantity of
    18         ---    items to produce. fields that are unnamed will be underwritten
    19         -
    20         -local function fQuant(a,b,n) return ((a or 0)+(b or 0))*n end
    21         -local function fFac  (a,b,n)
    22         -	if a == nil and b == nil then return nil end
    23         -	local f if a == nil or b == nil then
    24         -		f = a or b
    25         -	else
    26         -		f = (a or 1)*(b or 1)
    27         -	end
    28         -	return f*n
    29         -end
    30         -local function fReq  (a,b,n) return a or b         end
    31         -local function fFlag (a,b,n) return a and b        end
    32         -local function fSize (a,b,n) return math.max(a,b)  end
    33         -local opClass = {
    34         -	-- fabrication eligibility will be determined by which kinds
    35         -	-- of input a particular fabricator can introduce. e.g. a
    36         -	-- printer with a  but no cache can only print items whose
    37         -	-- recipe only names elements as ingredients
    38         -
    39         -	-- ingredients
    40         -	element    = fQuant; -- (g)
    41         -	gas        = fQuant; -- ()
    42         -	liquid     = fQuant; -- (l)
    43         -	crystal    = fQuant; -- (g)
    44         -	item       = fQuant; -- n
    45         -	metal      = fQuant; -- (g)
    46         -	metalIngot = fQuant; -- (g)
    47         -	-- factors
    48         -	cost = fFac; -- units vary
    49         -	time = fFac; -- (s)
    50         -		-- print: base printing time
    51         -	size = fSize;
    52         -		-- printBay: size of the printer bay necessary to produce the item
    53         -	req  = fReq;
    54         -	flag = fFlag; -- means that can be used to produce the item & misc flags
    55         -		-- print: allow production with a printer
    56         -		-- smelt: allow production with a smelter
    57         -	-- all else defaults to underwrite
    58         -}
    59         -
    60         -local F = string.format
    61         -local strClass = {
    62         -	element = function(x, n)
    63         -		local el = starsoul.world.material.element[x]
    64         -		return lib.math.si('g', n) .. ' ' .. (el.sym or el.name)
    65         -	end;
    66         -	metal = function(x, n)
    67         -		local met = starsoul.world.material.metal[x]
    68         -		return lib.math.si('g', n) .. ' ' .. met.name
    69         -	end;
    70         -	liquid = function(x, n)
    71         -		local liq = starsoul.world.material.liquid[x]
    72         -		return lib.math.si('L', n) .. ' ' .. liq.name
    73         -	end;
    74         -	gas = function(x, n)
    75         -		local gas = starsoul.world.material.gas[x]
    76         -		return lib.math.si('g', n) .. ' ' .. gas.name
    77         -	end;
    78         -	item = function(x, n)
    79         -		local i = minetest.registered_items[x]
    80         -		return tostring(n) .. 'x ' .. i.short_description
    81         -	end;
    82         -}
    83         -
    84         -local order = {
    85         -	'element', 'metal', 'liquid', 'gas', 'item'
    86         -}
    87         -
    88         -local lib = starsoul.mod.lib
    89         -local fab fab = lib.class {
    90         -	__name = 'starsoul:fab';
    91         -	
    92         -	opClass = opClass;
    93         -	strClass = strClass;
    94         -	order = order;
    95         -	construct = function(q) return q end;
    96         -	__index = {
    97         -		elementalize = function(self)
    98         -			local e = fab {element = self.element or {}}
    99         -			for _, kind in pairs {'metal', 'gas', 'liquid'} do
   100         -				for m,mass in pairs(self[kind] or {}) do
   101         -					local mc = starsoul.world.material[kind][m].composition
   102         -					e = e + mc:elementalize()*mass
   103         -				end
   104         -			end
   105         -			return e
   106         -		end;
   107         -
   108         -		elementSeq = function(self)
   109         -			local el = {}
   110         -			local em = self.element
   111         -			local s = 0
   112         -			local eldb = starsoul.world.material.element.db
   113         -			for k in pairs(em) do table.insert(el, k) s=s+eldb[k].n end
   114         -			table.sort(el, function(a,b)
   115         -				return eldb[a].n > eldb[b].n
   116         -			end)
   117         -			return el, em, s
   118         -		end;
   119         -
   120         -		formula = function(self)
   121         -			print('make formula', dump(self))
   122         -			local ts,f=0
   123         -			if self.element then
   124         -				f = {}
   125         -				local el, em, s = self:elementSeq()
   126         -				local eldb = starsoul.world.material.element.db
   127         -				for i, e in ipairs(el) do
   128         -					local sym, n = eldb[e].sym, em[e]
   129         -					if n > 0 then
   130         -						table.insert(f, string.format("%s%s",
   131         -							sym, n>1 and lib.str.nIdx(n) or ''))
   132         -					end
   133         -				end
   134         -				f = table.concat(f)
   135         -				ts = ts + s
   136         -			end
   137         -
   138         -			local sub = {}
   139         -			for _, w in pairs {'metal', 'gas', 'liquid'} do
   140         -				if self[w] then
   141         -					local mdb = starsoul.world.material[w].db
   142         -					for k, amt in pairs(self[w]) do
   143         -						local mf, s = mdb[k].composition:formula()
   144         -						if amt > 0 then table.insert(sub, {
   145         -							f = string.format("(%s)%s",mf,
   146         -								lib.str.nIdx(amt));
   147         -							s = s;
   148         -						}) end
   149         -						ts = ts + s*amt
   150         -					end
   151         -				end
   152         -			end
   153         -			table.sort(sub, function(a,b) return a.s > b.s end)
   154         -			local fml = {}
   155         -			for i, v in ipairs(sub) do fml[i] = v.f end
   156         -			if f then table.insert(fml, f) end
   157         -			fml = table.concat(fml, ' + ')
   158         -
   159         -			return fml, ts
   160         -		end;
   161         -	};
   162         -
   163         -	__tostring = function(self)
   164         -		local t = {}
   165         -		for i,o in ipairs(order) do
   166         -			if self[o] then
   167         -				for mat,amt in pairs(self[o]) do
   168         -					if amt > 0 then
   169         -						table.insert(t, strClass[o](mat, amt))
   170         -					end
   171         -				end
   172         -			end
   173         -		end
   174         -		return table.concat(t, ", ")
   175         -	end;
   176         -
   177         -
   178         -	__add = function(a,b)
   179         -		local new = fab {}
   180         -		for cat, vals in pairs(a) do
   181         -			new[cat] = lib.tbl.copy(vals)
   182         -		end
   183         -		for cat, vals in pairs(b) do
   184         -			if not new[cat] then
   185         -				new[cat] = lib.tbl.copy(vals)
   186         -			else
   187         -				local f = opClass[cat]
   188         -				for k,v in pairs(vals) do
   189         -					local n = f(new[cat][k], v, 1)
   190         -					new[cat][k] = n > 0 and n or nil
   191         -				end
   192         -			end
   193         -		end
   194         -		return new
   195         -	end;
   196         -
   197         -	__mul = function(x,n)
   198         -		local new = fab {}
   199         -		for cat, vals in pairs(x) do
   200         -			new[cat] = {}
   201         -			local f = opClass[cat]
   202         -			for k,v in pairs(vals) do
   203         -				local num = f(v,nil,n)
   204         -				new[cat][k] = num > 0 and num or nil
   205         -			end
   206         -		end
   207         -		return new
   208         -	end;
   209         -
   210         -	__div = function(x,n)
   211         -		return x * (1/n)
   212         -	end;
   213         -}
   214         -
   215         -starsoul.type.fab = fab

Deleted mods/starsoul/fx/nano.lua version [4c580d7de8].

     1         -local lib = starsoul.mod.lib
     2         -local E = starsoul.effect
     3         -local N = {}
     4         -starsoul.fx.nano = N
     5         -local nanopool= {
     6         -	{
     7         -		name = 'starsoul-fx-nano-spark-small.png';
     8         -		scale_tween = {0,.5, style = 'pulse', rep = 3};
     9         -	};
    10         -	{
    11         -		name = 'starsoul-fx-nano-spark-small.png';
    12         -		scale_tween = {0,1, style = 'pulse', rep = 2};
    13         -	};
    14         -	{
    15         -		name = 'starsoul-fx-nano-spark-big.png';
    16         -		scale_tween = {0,1, style = 'pulse'};
    17         -	};
    18         -}
    19         -
    20         -function N.heal(user, targets, amt, dur)
    21         -	local amthealed = {}
    22         -	local f = E.cast {
    23         -		caster = user.entity;
    24         -		subjects = targets;
    25         -		duration = dur;
    26         -		intervals = {
    27         -			{
    28         -				after = 0;
    29         -				period = 4;
    30         -				fn = function(c)
    31         -					for i,v in pairs(c.effect.subjects) do
    32         -						local u = starsoul.activeUsers[v.player:get_player_name()]
    33         -						if u then
    34         -							local heal = math.max(amt/4, 1)
    35         -							amthealed[u] = amthealed[u] or 0
    36         -							if amthealed[u] < amt then
    37         -								amthealed[u] = amthealed[u] + heal
    38         -								u:statDelta('health', heal)
    39         -							end
    40         -						end
    41         -					end
    42         -				end;
    43         -			}
    44         -		}
    45         -	}
    46         -
    47         -	local casterIsTarget = false
    48         -	for _, sub in pairs(f.subjects) do
    49         -		if sub.player == user.entity then
    50         -			casterIsTarget = true
    51         -		end
    52         -		f.visual(sub, {
    53         -			amount = 50;
    54         -			time = dur;
    55         -			glow = 14;
    56         -			jitter = 0.01;
    57         -			attached = user.entity;
    58         -			vel = { min = -0.1, max = 0.1; };
    59         -			pos = {
    60         -				min = vector.new(0,0.2,0);
    61         -				max = vector.new(0,1.2,0);
    62         -			};
    63         -			radius  = { min = 0.2; max = 0.6; bias = -1; };
    64         -			exptime = {min=0.5,max=2};
    65         -			attract = {
    66         -				kind = 'line';
    67         -				strength = {min = 0.5, max = 2};
    68         -				origin = 0;
    69         -				direction = vector.new(0,1,0);
    70         -				origin_attached = sub.player;
    71         -				direction_attached = sub.player;
    72         -			};
    73         -
    74         -			texpool = nanopool;
    75         -		})
    76         -	end
    77         -	if not casterIsTarget then
    78         -		-- f.visual_caster { }
    79         -	end
    80         -	f.play(0.3, {
    81         -		where = 'subjects';
    82         -		sound = 'starsoul-nano-heal';
    83         -		ephemeral = true;
    84         -		spec = {gain = 0.3};
    85         -	})
    86         -
    87         -	return f
    88         -end
    89         -
    90         -function N.shred(user, pos, prop, time, node)
    91         -	local f = E.cast {
    92         -		caster = user.entity;
    93         -		subjects = {};
    94         -		duration = time;
    95         -	}
    96         -	local sp,sv = user:lookupSpecies()
    97         -	local eh = sv.eyeHeight or sp.eyeHeight
    98         -	f.visual_caster {
    99         -		amount = 200 * time;
   100         -		pos =  vector.new(0.12,eh - 0.1,0);
   101         -		radius = 0.2;
   102         -		time = time - (time/3);
   103         -		glow = 14;
   104         -		jitter = 0.1;
   105         -		size = {min = 0.2, max = 0.5};
   106         -		exptime = {min=0.5,max=1};
   107         -		vel_tween = {
   108         -			0;
   109         -			{ min = -0.4, max = 0.4; };
   110         -			style = 'pulse', rep = time * 2; 
   111         -		};
   112         -		attract = {
   113         -			kind = 'point';
   114         -			origin = pos;
   115         -			radius = 0.5;
   116         -			strength = {min=.3,max=2};
   117         -		};
   118         -		texpool = nanopool;
   119         -	};
   120         -	f.queue(0.05, function(s, timepast, timeleft)
   121         -		f.visual(nil, {
   122         -			amount = timeleft * 40;
   123         -			time = timeleft;
   124         -			pos = pos;
   125         -			size_tween = {
   126         -				0, {min = 0.5, max = 2};
   127         -			};
   128         -			vel = {
   129         -				min = vector.new(-1.2,0.5,-1.2);
   130         -				max = vector.new(1.2,3.5,1.2);
   131         -			};
   132         -			acc = vector.new(0,-starsoul.world.planet.gravity,0);
   133         -			node = node;
   134         -		})
   135         -	end);
   136         -	f.queue(0.9, function(s, timepast, timeleft)
   137         -		f.visual(nil, {
   138         -			amount = 200;
   139         -			time = timeleft;
   140         -			pos = pos;
   141         -			size = {min = 0.1, max = 0.3};
   142         -			vel = {
   143         -				min = vector.new(-2,0.5,-2);
   144         -				max = vector.new(2,4,2);
   145         -			};
   146         -			acc = vector.new(0,-starsoul.world.planet.gravity,0);
   147         -			node = node;
   148         -		})
   149         -	end);
   150         -	f.queue(0.3, function(s, timepast, timeleft)
   151         -		local function v(fn)
   152         -			local def = {
   153         -				amount = timeleft * 100;
   154         -				pos = pos;
   155         -				time = timeleft;
   156         -				radius = 0.5;
   157         -				jitter = {min = 0.0, max = 0.2};
   158         -				size = {min = 0.2, max = 0.5};
   159         -				exptime = {min = 0.5, max = 1};
   160         -				attract = {
   161         -					kind = 'point';
   162         -					strength = {min=0.3, max = 1};
   163         -					origin = vector.new(0,eh-0.1,0);
   164         -					radius = 0.5;
   165         -					origin_attached = user.entity;
   166         -				};
   167         -			}
   168         -			fn(def)
   169         -			f.visual(nil, def)
   170         -		end
   171         -		v(function(t) t.texpool = nanopool t.glow = 14 end)
   172         -		v(function(t)
   173         -			t.node = node
   174         -			t.amount = timeleft * 20
   175         -			t.size = {min = 0.1, max = 0.3};
   176         -		end)
   177         -	end)
   178         -	return f
   179         -
   180         -end

Deleted mods/starsoul/init.lua version [aca0a214d9].

     1         --- [ʞ] starsoul/init.lua
     2         ---  ~ lexi hale <lexi@hale.su>
     3         ---  ? basic setup, game rules, terrain
     4         ---  © EUPL v1.2
     5         -
     6         -local T = minetest.get_translator 'starsoul'
     7         -
     8         --- TODO enforce latest engine version
     9         -
    10         -local mod = {
    11         -	-- subordinate mods register here
    12         -	lib = vtlib;
    13         -		-- vtlib should be accessed as starsoul.mod.lib by starsoul modules for the sake of proper encapsulation. vtlib should simply be a provider, not a hardcoded dependency
    14         -}
    15         -local lib = mod.lib
    16         -
    17         -
    18         -starsoul = {
    19         -	ident = minetest.get_current_modname();
    20         -	mod = mod;
    21         -	translator = T;
    22         -
    23         -	constant = {
    24         -		light = { --minetest units
    25         -			dim = 3;
    26         -			lamp = 7;
    27         -			bright = 10;
    28         -			brightest = 14; -- only sun and growlights
    29         -		};
    30         -		heat = { -- celsius
    31         -			freezing = 0;
    32         -			safe = 4;
    33         -			overheat = 32;
    34         -			boiling = 100;
    35         -		};
    36         -		rad = {
    37         -		};
    38         -	};
    39         -
    40         -	activeUsers = {
    41         -		-- map of username -> user object
    42         -	};
    43         -	activeUI = {
    44         -		-- map of username -> UI context
    45         -	};
    46         -	liveUI = {
    47         -		-- cached subset of activeUI containing those UIs needing live updates
    48         -	};
    49         -
    50         -	interface = lib.registry.mk 'starsoul:interface';
    51         -	item = {
    52         -	};
    53         -
    54         -	region = {
    55         -		radiator = {
    56         -			store = AreaStore();
    57         -			emitters = {}
    58         -		};
    59         -	};
    60         -
    61         -	-- standardized effects
    62         -	fx = {};
    63         -
    64         -	type = {};
    65         -	world = {
    66         -		defaultScenario = 'starsoul_scenario:imperialExpat';
    67         -		seedbank = lib.math.seedbank(minetest.get_mapgen_setting 'seed');
    68         -		mineral = lib.registry.mk 'starsoul:mineral';
    69         -		material = { -- raw materials
    70         -			element = lib.registry.mk 'starsoul:element';
    71         -			-- elements are automatically sorted into the following categories
    72         -			-- if they match. however, it's possible to have a metal/gas/liquid
    73         -			-- that *isn't* a pure element, so these need separate registries
    74         -			-- for alloys and mixtures like steel and water
    75         -			metal   = lib.registry.mk 'starsoul:metal';
    76         -			gas     = lib.registry.mk 'starsoul:gas';
    77         -			liquid  = lib.registry.mk 'starsoul:liquid';
    78         -		};
    79         -		ecology = {
    80         -			plants = lib.registry.mk 'starsoul:plants';
    81         -			trees = lib.registry.mk 'starsoul:trees';
    82         -			biomes = lib.registry.mk 'starsoul:biome';
    83         -		};
    84         -		climate = {};
    85         -		scenario = {};
    86         -		planet = {
    87         -			gravity = 7.44;
    88         -			orbit = 189; -- 1 year is 189 days
    89         -			revolve = 20; -- 1 day is 20 irl minutes
    90         -		};
    91         -		fact = lib.registry.mk 'starsoul:fact';
    92         -		time = {
    93         -			calendar = {
    94         -				empire  = {
    95         -					name = 'Imperial Regnal Calendar';
    96         -					year = function(t, long)
    97         -						local reigns = {
    98         -							-- if anyone actually makes it to his Honor & Glory Unfailing Persigan I i will be
    99         -							-- exceptionally flattered
   100         -							{4, 'Emperor', 'Atavarka', 'the Bold'}; -- died at war
   101         -							{9, 'Emperor', 'Vatikserka', 'the Unconquered'}; -- died at war
   102         -							{22, 'Emperor', 'Rusifend', 'the Wise'}; -- poisoned at diplomacy
   103         -							{61, 'Empress', 'Tafseshendi', 'the Great'}; -- died of an 'insurrection of the innards' after a celebrated reign
   104         -							{291, 'Emperor', 'Treptebaska', 'the Unwise'}; -- murdered by his wife in short order
   105         -							{292, 'Empress', 'Vilintalti', 'the Impious'}; -- removed by the praetorian elite
   106         -							{298, 'Emperor', 'Radavan', 'the Reckless'}; -- died at war
   107         -							{316, 'Emperor', 'Suldibrand', 'the Forsaken of Men'}; -- fucked around. found out.
   108         -							{320, 'Emperor', 'Persigan', 'the Deathless'};
   109         -						}
   110         -						local year, r = math.floor(t / 414)
   111         -						for i=1, #reigns do if reigns[i+1][1] < year then r = reigns[i+1] end end
   112         -						local reignBegin, title, name, epithet = lib.tbl.unpack(r)
   113         -						local ry = 1 + (year - reignBegin)
   114         -						return long and string.format('Year %s of the Reign of HH&GU %s %s %s',
   115         -							ry, title, name, epithet) or string.format('Y. %s %s', name, ry)
   116         -					end;
   117         -					time = function(t, long)
   118         -						local bellsInDay, candleSpansInBell = 5, 7
   119         -						local bell = bellsInDay*t
   120         -						local cspan = (bellsInDay*candleSpansInBell*t) % candleSpansInBell
   121         -						return string.format(long and 'Bell %s, Candlespan %s' or '%sb %sc', math.floor(bell), math.floor(cspan))
   122         -					end;
   123         -				};
   124         -				commune = {
   125         -					name = 'People\'s Calendar';
   126         -					date = function(t, long)
   127         -						local year = math.floor(t / 256) + 314
   128         -						return string.format(long and 'Foundation %s' or 'F:%s', year)
   129         -					end;
   130         -					time = function(t, long)
   131         -						local hoursInDay, minutesInHour = 16, 16
   132         -						local hour = hoursInDay*t
   133         -						local min = (hoursInDay*minutesInHour*t) % minutesInHour
   134         -
   135         -						local dawn     = 0.24*hoursInDay
   136         -						local noon     = 0.5*hoursInDay
   137         -						local dusk     = 0.76*hoursInDay
   138         -						local midnight = 1.0*hoursInDay
   139         -
   140         -						local tl, str
   141         -						if hour < dawn then
   142         -							tl = dawn - hour
   143         -							str = long and 'dawn' or 'D'
   144         -						elseif hour < noon then
   145         -							tl = noon - hour
   146         -							str = long and 'noon' or 'N'
   147         -						elseif hour < dusk then
   148         -							tl = dusk - hour
   149         -							str = long and 'dusk' or 'd'
   150         -						elseif hour < midnight then
   151         -							tl = midnight - hour
   152         -							str = long and 'midnight' or 'M'
   153         -						end
   154         -						return long
   155         -							and string.format('%s hours, %s minutes to %s',
   156         -							    math.floor(tl), math.floor(minutesInHour - min), str)
   157         -							or  string.format('%s.%sH.%sM', str, math.floor(tl),
   158         -							    math.floor(minutesInHour - min))
   159         -					end;
   160         -				};
   161         -			};
   162         -		};
   163         -	};
   164         -
   165         -	jobs = {};
   166         -}
   167         -
   168         -starsoul.cfgDir = minetest.get_worldpath() .. '/' .. starsoul.ident
   169         -
   170         -local logger = function(module)
   171         -	local function argjoin(arg, nxt, ...)
   172         -		if arg and not nxt then return tostring(arg) end
   173         -		if not arg then return "(nil)" end
   174         -		return tostring(arg) .. ' ' .. argjoin(nxt, ...)
   175         -	end
   176         -	local lg = {}
   177         -	local setup = function(fn, lvl)
   178         -		lvl = lvl or fn
   179         -		local function emit(...)
   180         -			local call = (fn == 'fatal') and error
   181         -				or function(str) minetest.log(lvl, str) end
   182         -			if module
   183         -				then call(string.format('[%s :: %s] %s',starsoul.ident,module,argjoin(...)))
   184         -				else call(string.format('[%s] %s',starsoul.ident,argjoin(...)))
   185         -			end
   186         -		end
   187         -		lg[fn       ] = function(...) emit(...)                end
   188         -		lg[fn .. 'f'] = function(...) emit(string.format(...)) end -- convenience fn
   189         -	end
   190         -	setup('info')
   191         -	setup('warn','warning')
   192         -	setup('err','error')
   193         -	setup('act','action')
   194         -	setup('fatal')
   195         -	return lg
   196         -end
   197         -
   198         -starsoul.logger = logger
   199         -
   200         -local log = logger()
   201         -
   202         -function starsoul.evaluate(name, ...)
   203         -	local path = minetest.get_modpath(minetest.get_current_modname())
   204         -	local filename = string.format('%s/%s', path, name)
   205         -	log.info('loading', filename)
   206         -	local chunk, err = loadfile(filename, filename)
   207         -	if not chunk then error(err) end
   208         -	return chunk(...)
   209         -end
   210         -
   211         -function starsoul.include(name, ...) -- semantic variant used for loading modules
   212         -	return starsoul.evaluate(name..'.lua', ...)
   213         -end
   214         -
   215         -minetest.register_lbm {
   216         -	label = 'build radiator index';
   217         -	name = 'starsoul:loadradiatorboxes';
   218         -	nodenames = {'group:radiator'};
   219         -	run_at_every_load = true;
   220         -	action = function(pos, node, dt)
   221         -		local R = starsoul.region
   222         -		local phash = minetest.hash_node_position(pos)
   223         -		if R.radiator.sources[phash] then return end -- already loaded
   224         -
   225         -		local def = minetest.registered_nodes[node.name]
   226         -		local cl = def._starsoul.radiator
   227         -		local min,max = cl.maxEffectArea(pos)
   228         -		local id = R.radiator.store:insert_area(min,max, minetest.pos_to_string(pos))
   229         -		R.radiator.sources[phash] = id
   230         -	end;
   231         -	-- NOTE: temp emitter nodes are responsible for decaching themselves in their on_destruct cb
   232         -}
   233         -
   234         -function starsoul.startJob(id, interval, job)
   235         -	local lastRun
   236         -	local function start()
   237         -		starsoul.jobs[id] = minetest.after(interval, function()
   238         -			local t = minetest.get_gametime()
   239         -			local d = lastRun and t - lastRun or nil
   240         -			lastRun = t
   241         -			local continue = job(d, interval)
   242         -			if continue == true or continue == nil then
   243         -				start()
   244         -			elseif continue ~= false then
   245         -				interval = continue
   246         -				start()
   247         -			end
   248         -		end)
   249         -	end
   250         -	start()
   251         -end
   252         -
   253         -starsoul.include 'stats'
   254         -starsoul.include 'world'
   255         -starsoul.include 'fab'
   256         -starsoul.include 'tiers'
   257         -starsoul.include 'species'
   258         -
   259         -starsoul.include 'store'
   260         -
   261         -starsoul.include 'ui'
   262         -starsoul.include 'item'
   263         -starsoul.include 'container'
   264         -starsoul.include 'user'
   265         -starsoul.include 'effect'
   266         -
   267         -starsoul.include 'fx/nano'
   268         -
   269         -starsoul.include 'element'
   270         -
   271         -starsoul.include 'terrain'
   272         -starsoul.include 'interfaces'
   273         -starsoul.include 'suit'
   274         -
   275         -minetest.settings:set('movement_gravity', starsoul.world.planet.gravity) -- ??? seriously???
   276         -
   277         ----------------
   278         --- callbacks --
   279         ----------------
   280         --- here we connect our types up to the minetest API
   281         -
   282         -local function userCB(fn)
   283         -	return function(luser, ...)
   284         -		local name = luser:get_player_name()
   285         -		local user = starsoul.activeUsers[name]
   286         -		return fn(user, ...)
   287         -	end
   288         -end
   289         -
   290         -minetest.register_on_joinplayer(function(luser, lastLogin)
   291         -	-- TODO check that necessary CSMs are installed
   292         -	local user = starsoul.type.user(luser)
   293         -
   294         -	if lastLogin == nil then
   295         -		user:onSignup()
   296         -	end
   297         -	user:onJoin()
   298         -
   299         -	starsoul.activeUsers[user.name] = user
   300         -end)
   301         -
   302         -minetest.register_on_leaveplayer(function(luser)
   303         -	starsoul.activeUsers[luser:get_player_name()]:onPart()
   304         -end)
   305         -
   306         -minetest.register_on_player_receive_fields(function(luser, formid, fields)
   307         -	local name = luser:get_player_name()
   308         -	local user = starsoul.activeUsers[name]
   309         -	if not user then return false end
   310         -	if formid == '' then -- main menu
   311         -		return starsoul.ui.userMenuDispatch(user,fields)
   312         -	end
   313         -	local ui = starsoul.interface.db[formid]
   314         -	local state = starsoul.activeUI[name] or {}
   315         -	if formid == '__builtin:help_cmds' 
   316         -	or formid == '__builtin:help_privs' 
   317         -		then return false end
   318         -	assert(state.form == formid) -- sanity check
   319         -	user:onRespond(ui, state, fields)
   320         -	if fields.quit then
   321         -		starsoul.activeUI[name] = nil
   322         -	end
   323         -	return true
   324         -end)
   325         -
   326         -minetest.register_on_respawnplayer(userCB(function(user)
   327         -	return user:onRespawn()
   328         -end))
   329         -
   330         -minetest.register_on_dieplayer(userCB(function(user, reason)
   331         -	return user:onDie(reason)
   332         -end))
   333         -
   334         -minetest.register_on_punchnode(function(pos,node,puncher,point)
   335         -	local user = starsoul.activeUsers[puncher:get_player_name()]
   336         -	local oldTgt = user.action.tgt
   337         -	user.action.tgt = point
   338         -	if bit.band(user.action.bits, 0x80)==0 then
   339         -		user.action.bits = bit.bor(user.action.bits, 0x80)
   340         -		--user:trigger('primary', {state = 'init'})
   341         -	else
   342         -		user:trigger('retarget', {oldTgt = oldTgt})
   343         -	end
   344         -end)
   345         -
   346         -local function pointChanged(a,b)
   347         -	return a.type ~= b.type
   348         -		or a.type == 'node'   and vector.new(a.under) ~= vector.new(b.under)
   349         -		or a.type == 'object' and a.ref ~= b.ref 
   350         -end
   351         -local function triggerPower(_, luser, point)
   352         -	local user = starsoul.activeUsers[luser:get_player_name()]
   353         -	local oldTgt = user.action.tgt
   354         -	user.action.tgt = point
   355         -	if bit.band(user.action.bits, 0x100)==0 then
   356         -		user.action.bits = bit.bor(user.action.bits, 0x100)
   357         -		--return user:trigger('secondary', {state = 'prog', delta = 0})
   358         -	elseif pointChanged(oldTgt, point) then
   359         -		user:trigger('retarget', {oldTgt = oldTgt})
   360         -	end
   361         -end
   362         --- sigh
   363         -core.noneitemdef_default.on_place = function(...)
   364         -	if not triggerPower(...) then
   365         -		minetest.item_place(...)
   366         -	end
   367         -end
   368         -core.noneitemdef_default.on_use           = function(...) triggerPower(...) end
   369         -core.noneitemdef_default.on_secondary_use = function(...) triggerPower(...) end
   370         -
   371         -minetest.register_on_player_inventory_action(function(luser, act, inv, p)
   372         -	local name = luser:get_player_name()
   373         -	local user = starsoul.activeUsers[name]
   374         -	-- allow UIs to update on UI changes
   375         -	local state = starsoul.activeUI[name]
   376         -	if state then
   377         -		local ui = starsoul.interface.db[state.form]
   378         -		ui:cb('onMoveItem', user, act, inv, p)
   379         -	end
   380         -end)
   381         -
   382         -minetest.register_on_player_hpchange(function(luser, delta, cause)
   383         -	local user = starsoul.activeUsers[luser:get_player_name()]
   384         -	if cause.type == 'fall' then
   385         -		delta = user:damageModifier('bluntForceTrauma', (delta * 50))
   386         -		-- justification: a short fall can do around
   387         -		-- five points of damage, which is nearly 50%
   388         -		-- of the default hp_max. since we crank up
   389         -		-- hp by a factor of 50~40, damage should be
   390         -		-- cranked by similarly
   391         -	end
   392         -	return delta
   393         -end, true)
   394         -
   395         -function minetest.handle_node_drops(pos, drops, digger)
   396         -	local function jitter(pos)
   397         -		local function r(x) return x+math.random(-0.2, 0.2) end
   398         -		return vector.new(
   399         -			r(pos.x),
   400         -			r(pos.y),
   401         -			r(pos.z)
   402         -		)
   403         -	end
   404         -	for i, it in ipairs(drops) do
   405         -		minetest.add_item(jitter(pos), it)
   406         -	end
   407         -end
   408         -
   409         -
   410         --- TODO timer iterates live UI
   411         -

Deleted mods/starsoul/interfaces.lua version [b552296eeb].

     1         -local lib = starsoul.mod.lib
     2         -
     3         -function starsoul.ui.setupForUser(user)
     4         -	local function cmode(mode)
     5         -		if user.actMode == mode then return {hue = 150, sat = 0, lum = .3} end
     6         -	end
     7         -	user.entity:set_inventory_formspec(starsoul.ui.build {
     8         -		kind = 'vert', mode = 'sw';
     9         -		padding = .5, spacing = 0.1;
    10         -		{kind = 'hztl';
    11         -			{kind = 'contact', w=1.5,h=1.5, id = 'mode_nano',
    12         -				img='starsoul-ui-icon-nano.png', close=true, color = cmode'nano'};
    13         -			{kind = 'contact', w=1.5,h=1.5, id = 'mode_weapon',
    14         -				img='starsoul-ui-icon-weapon.png', close=true, color = cmode'weapon'};
    15         -			{kind = 'contact', w=1.5,h=1.5, id = 'mode_psi',
    16         -				img='starsoul-ui-icon-psi.png', close=true, color = cmode'psi'};
    17         -		};
    18         -		{kind = 'hztl';
    19         -			{kind = 'contact', w=1.5,h=1.5, id = 'open_elements',
    20         -				img='starsoul-ui-icon-element.png'};
    21         -			{kind = 'contact', w=1.5,h=1.5, id = 'open_suit',
    22         -				img='starsoul-item-suit.png^[hsl:200:-.7:0'};
    23         -			{kind = 'contact', w=1.5,h=1.5, id = 'open_psi',
    24         -				img='starsoul-ui-icon-psi-cfg.png'};
    25         -			{kind = 'contact', w=1.5,h=1.5, id = 'open_body',
    26         -				img='starsoul-ui-icon-self.png'};
    27         -		};
    28         -		{kind = 'list';
    29         -			target = 'current_player', inv = 'main';
    30         -			w = 6, h = 1, spacing = 0.1;
    31         -		};
    32         -	})
    33         -end
    34         -
    35         -function starsoul.ui.userMenuDispatch(user, fields)
    36         -	local function setSuitMode(mode)
    37         -		if user.actMode == mode then
    38         -			user:actModeSet 'off'
    39         -		else
    40         -			user:actModeSet(mode)
    41         -		end
    42         -	end
    43         -
    44         -	local modes = { nano = true, psi = false, weapon = true }
    45         -	for e,s in pairs(modes) do
    46         -		if fields['mode_' .. e] then
    47         -			if s and (user:naked() or user:getSuit():powerState() == 'off') then
    48         -				user:suitSound 'starsoul-error'
    49         -			else
    50         -				setSuitMode(e)
    51         -			end
    52         -			return true
    53         -		end
    54         -	end
    55         -
    56         -	if fields.open_elements then
    57         -		user:openUI('starsoul:user-menu', 'compiler')
    58         -		return true
    59         -	elseif fields.open_psi then
    60         -		user:openUI('starsoul:user-menu', 'psi')
    61         -		return true
    62         -	elseif fields.open_suit then
    63         -		if not user:naked() then
    64         -			user:openUI('starsoul:user-menu', 'suit')
    65         -		end
    66         -		return true
    67         -	elseif fields.open_body then
    68         -		user:openUI('starsoul:user-menu', 'body')
    69         -	end
    70         -	return false
    71         -end
    72         -
    73         -local function listWrap(n, max)
    74         -	local h = math.ceil(n / max)
    75         -	local w = math.min(max, n)
    76         -	return w, h
    77         -end
    78         -
    79         -local function wrapMenu(w, h, rh, max, l)
    80         -	local root = {kind = 'vert', w=w, h=h}
    81         -	local bar
    82         -	local function flush()
    83         -		if bar and bar[1] then table.insert(root, bar) end
    84         -		bar = {kind = 'hztl'}
    85         -	end
    86         -	flush()
    87         -
    88         -	for _, i in ipairs(l) do
    89         -		local bw = w/max
    90         -		if i.cfg then w = w - rh end
    91         -
    92         -		table.insert(bar, {
    93         -			kind = 'button', close = i.close;
    94         -			color = i.color;
    95         -			fg = i.fg;
    96         -			label = i.label;
    97         -			icon = i.img;
    98         -			id = i.id;
    99         -			w = bw, h = rh;
   100         -		})
   101         -		if i.cfg then 
   102         -			table.insert(bar, {
   103         -				kind = 'button';
   104         -				color = i.color;
   105         -				fg = i.fg;
   106         -				label = "CFG";
   107         -				icon = i.img;
   108         -				id = i.id .. '_cfg';
   109         -				w = rh, h = rh;
   110         -			})
   111         -		end
   112         -
   113         -		if bar[max] then flush() end
   114         -	end
   115         -	flush()
   116         -	
   117         -	return root
   118         -end
   119         -
   120         -local function abilityMenu(a)
   121         -	-- select primary/secondary abilities or activate ritual abilities
   122         -	local p = {kind = 'vert'}
   123         -	for _, o in ipairs(a.order) do
   124         -		local m = a.menu[o]
   125         -		table.insert(p, {kind='label', text=m.label, w=a.w, h = .5})
   126         -		table.insert(p, wrapMenu(a.w, a.h, 1.2, 2, m.opts))
   127         -	end
   128         -	return p
   129         -end
   130         -
   131         -local function pptrMatch(a,b)
   132         -	if a == nil or b == nil then return false end
   133         -	return a.chipID == b.chipID and a.pgmIndex == b.pgmIndex
   134         -end
   135         -
   136         -starsoul.interface.install(starsoul.type.ui {
   137         -	id = 'starsoul:user-menu';
   138         -	pages = {
   139         -		compiler = {
   140         -			setupState = function(state, user)
   141         -				-- nanotech/suit software menu
   142         -				local chips = user.entity:get_inventory():get_list 'starsoul_suit_chips' -- FIXME need better subinv api
   143         -				local sw = starsoul.mod.electronics.chip.usableSoftware(chips)
   144         -				state.suitSW = {}
   145         -				local dedup = {}
   146         -				for i, r in ipairs(sw) do if
   147         -					r.sw.kind      == 'suitPower'
   148         -				then
   149         -					if not dedup[r.sw] then
   150         -						dedup[r.sw] = true
   151         -						table.insert(state.suitSW, r)
   152         -					end
   153         -				end end
   154         -			end;
   155         -			handle = function(state, user, act)
   156         -				if user:getSuit():powerState() == 'off' then return false end
   157         -				local pgm, cfg
   158         -				for k in next, act do
   159         -					local id, mode = k:match('^suit_pgm_([0-9]+)_(.*)$')
   160         -					if id then
   161         -						id = tonumber(id)
   162         -						if state.suitSW[id] then
   163         -							pgm = state.suitSW[id]
   164         -							cfg = mode == '_cfg'
   165         -							break
   166         -						end
   167         -					end
   168         -				end
   169         -				if not pgm then return false end -- HAX
   170         -
   171         -				-- kind=active programs must be assigned to a command slot
   172         -				-- kind=direct programs must open their UI
   173         -				-- kind=passive programs must toggle on and off
   174         -				if pgm.sw.powerKind == 'active' then
   175         -					if cfg then
   176         -						user:openUI(pgm.sw.ui, 'index', {
   177         -							context = 'suit';
   178         -							program = pgm;
   179         -						})
   180         -						return false
   181         -					end
   182         -					local ptr = {chipID = starsoul.mod.electronics.chip.read(pgm.chip).uuid, pgmIndex = pgm.fd.inode}
   183         -					local pnan = user.power.nano
   184         -					if pnan.primary == nil then
   185         -						pnan.primary = ptr
   186         -					elseif pptrMatch(ptr, pnan.primary) then
   187         -						pnan.primary = nil
   188         -					elseif pptrMatch(ptr, pnan.secondary) then
   189         -						pnan.secondary = nil
   190         -					else
   191         -						pnan.secondary = ptr
   192         -					end
   193         -					user:suitSound 'starsoul-configure'
   194         -				elseif pgm.sw.powerKind == 'direct' then
   195         -					local ctx = {
   196         -						context = 'suit';
   197         -						program = pgm;
   198         -					}
   199         -					if pgm.sw.ui then
   200         -						user:openUI(pgm.sw.ui, 'index', ctx)
   201         -						return false
   202         -					else
   203         -						pgm.sw.run(user, ctx)
   204         -					end
   205         -				elseif pgm.sw.powerKind == 'passive' then
   206         -					if cfg then
   207         -						user:openUI(pgm.sw.ui, 'index', {
   208         -							context = 'suit';
   209         -							program = pgm;
   210         -						})
   211         -						return false
   212         -					end
   213         -
   214         -					local addDisableRec = true
   215         -					for i, e in ipairs(pgm.file.body.conf) do
   216         -						if e.key == 'disable' and e.value == 'yes' then
   217         -							addDisableRec = false
   218         -							table.remove(pgm.file.body.conf, i)
   219         -							break
   220         -						elseif e.key == 'disable' and e.value == 'no' then
   221         -							e.value = 'yes'
   222         -							addDisableRec = false
   223         -							break
   224         -						end
   225         -					end
   226         -					if addDisableRec then
   227         -						table.insert(pgm.file.body.conf, {key='disable',value='yes'})
   228         -					end
   229         -					-- update the chip *wince*
   230         -					pgm.fd:write(pgm.file)
   231         -					user.entity:get_inventory():set_stack('starsoul_suit_chips',
   232         -					pgm.chipSlot, pgm.chip)
   233         -					user:reconfigureSuit()
   234         -					user:suitSound('starsoul-configure')
   235         -
   236         -				end
   237         -				return true, true
   238         -			end;
   239         -			render = function(state, user)
   240         -				local suit = user:getSuit()
   241         -				local swm
   242         -				if user:getSuit():powerState() ~= 'off' then
   243         -					swm = {
   244         -						w = 8, h = 3;
   245         -						order = {'active','ritual','pasv'};
   246         -						menu = {
   247         -							active = {
   248         -								label = 'Nanoware';
   249         -								opts = {};
   250         -							};
   251         -							ritual = {
   252         -								label = 'Programs';
   253         -								opts = {};
   254         -							};
   255         -							pasv = {
   256         -								label = 'Passive';
   257         -								opts = {};
   258         -							};
   259         -						};
   260         -					}
   261         -					for id, r in pairs(state.suitSW) do
   262         -						local color = {hue=300,sat=0,lum=0}
   263         -						local fg = nil
   264         -						local close = nil
   265         -						local tbl, cfg if r.sw.powerKind == 'active' then
   266         -							tbl = swm.menu.active.opts
   267         -							if r.sw.ui then cfg = true end
   268         -							local pnan = user.power.nano
   269         -							if pnan then
   270         -								local ptr = {chipID = starsoul.mod.electronics.chip.read(r.chip).uuid, pgmIndex = r.fd.inode}
   271         -								if pptrMatch(ptr, pnan.primary) then
   272         -									color.lum = 1
   273         -								elseif pptrMatch(ptr, pnan.secondary) then
   274         -									color.lum = 0.8
   275         -								end
   276         -							end
   277         -						elseif r.sw.powerKind == 'direct' then
   278         -							tbl = swm.menu.ritual.opts
   279         -							if not r.sw.ui then
   280         -								close = true
   281         -							end
   282         -						elseif r.sw.powerKind == 'passive' then
   283         -							tbl = swm.menu.pasv.opts
   284         -							if r.sw.ui then cfg = true end
   285         -							for i, e in ipairs(r.file.body.conf) do
   286         -								if e.key == 'disable' and e.value == 'yes' then
   287         -									color.lum = -.2
   288         -									fg = lib.color {hue=color.hue,sat=0.7,lum=0.7}
   289         -									break
   290         -								end
   291         -							end
   292         -						end
   293         -						if tbl then table.insert(tbl, {
   294         -							color = color, fg = fg;
   295         -							label = r.sw.label or r.sw.name;
   296         -							id = string.format('suit_pgm_%s_', id);
   297         -							cfg = cfg, close = close;
   298         -						}) end
   299         -					end
   300         -				end
   301         -				local menu = { kind = 'vert', mode = 'sw', padding = 0.5 }
   302         -				if swm then table.insert(menu, abilityMenu(swm)) end
   303         -
   304         -				local inv = user.entity:get_inventory()
   305         -				local cans = inv:get_list 'starsoul_suit_canisters'
   306         -				if cans and next(cans) then for i, st in ipairs(cans) do
   307         -					local id = string.format('starsoul_canister_%u_elem', i)
   308         -					local esz = inv:get_size(id)
   309         -					if esz > 0 then
   310         -						local eltW, eltH = listWrap(esz, 5)
   311         -						table.insert(menu, {kind = 'hztl',
   312         -							{kind = 'img', desc='Elements', img = 'starsoul-ui-icon-element.png', w=1,h=1};
   313         -							{kind = 'list', target = 'current_player', inv = id,
   314         -								listContent = 'element', w = eltW, h = eltH, spacing = 0.1};
   315         -						})
   316         -					end
   317         -				end end
   318         -
   319         -				if #menu == 0 then
   320         -					table.insert(menu, {
   321         -						kind = 'img';
   322         -						img = 'starsoul-ui-alert.png';
   323         -						w=2, h=2;
   324         -					})
   325         -					menu.padding = 1;
   326         -				end
   327         -				return starsoul.ui.build(menu)
   328         -			end;
   329         -		};
   330         -		compilerListRecipes = {
   331         -		};
   332         -		psi = {
   333         -			render = function(state, user)
   334         -				return starsoul.ui.build {
   335         -					kind = 'vert', mode = 'sw';
   336         -					padding = 0.5;
   337         -				}
   338         -			end;
   339         -		};
   340         -		body = {
   341         -			render = function(state, user)
   342         -				local barh = .75
   343         -				local tb = {
   344         -					kind = 'vert', mode = 'sw';
   345         -					padding = 0.5, 
   346         -					{kind = 'hztl', padding = 0.25;
   347         -						{kind = 'label', text = 'Name', w = 2, h = barh};
   348         -						{kind = 'label', text = user.persona.name, w = 4, h = barh}};
   349         -				}
   350         -				local statBars = {'hunger', 'thirst', 'fatigue', 'morale'}
   351         -				for idx, id in ipairs(statBars) do
   352         -					local s = starsoul.world.stats[id]
   353         -					local amt, sv = user:effectiveStat(id)
   354         -					local min, max = starsoul.world.species.statRange(user.persona.species, user.persona.speciesVariant, id)
   355         -					local st = string.format('%s / %s', s.desc(amt, true), s.desc(max))
   356         -					table.insert(tb, {kind = 'hztl', padding = 0.25;
   357         -						{kind = 'label', w=2, h=barh, text = s.name};
   358         -						{kind = 'hbar',  w=4, h=barh, fac = sv, text = st, color=s.color};
   359         -					})
   360         -				end
   361         -				local abilities = {
   362         -					{id = 'abl_sprint', label = 'Sprint', img = 'starsoul-ui-icon-ability-sprint.png'};
   363         -				}
   364         -				table.insert(tb, wrapMenu(6.25,4, 1,2, abilities))
   365         -				return starsoul.ui.build(tb)
   366         -			end;
   367         -		};
   368         -		suit = {
   369         -			render = function(state, user)
   370         -				local suit = user:getSuit()
   371         -				local suitDef = suit:def()
   372         -				local chipW, chipH = listWrap(suitDef.slots.chips, 5)
   373         -				local batW, batH = listWrap(suitDef.slots.batteries, 5)
   374         -				local canW, canH = listWrap(suitDef.slots.canisters, 5)
   375         -				local suitMode = suit:powerState()
   376         -				local function modeColor(mode)
   377         -					if mode == suitMode then return {hue = 180, sat = 0, lum = .5} end
   378         -				end
   379         -				return starsoul.ui.build {
   380         -					kind = 'vert', mode = 'sw';
   381         -					padding = 0.5, spacing = 0.1;
   382         -					{kind = 'hztl',
   383         -						{kind = 'img', desc='Batteries', img = 'starsoul-item-battery.png', w=1,h=1};
   384         -						{kind = 'list', target = 'current_player', inv = 'starsoul_suit_bat',
   385         -							listContent = 'power', w = batW, h = batH, spacing = 0.1};
   386         -					};
   387         -					{kind = 'hztl',
   388         -						{kind = 'img', desc='Chips', img = 'starsoul-item-chip.png', w=1,h=1};
   389         -						{kind = 'list', target = 'current_player', inv = 'starsoul_suit_chips',
   390         -							listContent = 'chip', w = chipW, h = chipH, spacing = 0.1};
   391         -					};
   392         -					{kind = 'hztl',
   393         -						{kind = 'img', desc='Canisters', img = 'starsoul-item-element-canister.png', w=1,h=1};
   394         -						{kind = 'list', target = 'current_player', inv = 'starsoul_suit_canisters',
   395         -							listContent = nil, w = canW, h = canH, spacing = 0.1};
   396         -					};
   397         -					{kind = 'hztl';
   398         -						{kind = 'img', w=1,h=1, item = suit.item:get_name(),
   399         -							desc = suit.item:get_definition().short_description};
   400         -						{kind = 'button', w=1.5,h=1, id = 'powerMode_off', label = 'Off';
   401         -							color=modeColor'off'};
   402         -						{kind = 'button', w=2.5,h=1, id = 'powerMode_save', label = 'Power Save';
   403         -							color=modeColor'powerSave'};
   404         -						{kind = 'button', w=1.5,h=1, id = 'powerMode_on', label = 'On'; 
   405         -							color=modeColor'on'};
   406         -					};
   407         -					{kind = 'list', target = 'current_player', inv = 'main', w = 6, h = 1, spacing = 0.1};
   408         -				}
   409         -			end;
   410         -			handle = function(state, user, q)
   411         -				local suitMode
   412         -				if     q.powerMode_off  then suitMode = 'off'
   413         -				elseif q.powerMode_save then suitMode = 'powerSave'
   414         -				elseif q.powerMode_on   then suitMode = 'on' end
   415         -				if suitMode then
   416         -					user:suitPowerStateSet(suitMode)
   417         -					return true
   418         -				end
   419         -			end;
   420         -		};
   421         -	};
   422         -})
   423         -
   424         -starsoul.interface.install(starsoul.type.ui {
   425         -	id = 'starsoul:compile-matter-component';
   426         -	pages = {
   427         -		index = {
   428         -			setupState = function(state, user, ctx)
   429         -				if ctx.context == 'suit' then
   430         -				end
   431         -				state.pgm = ctx.program
   432         -			end;
   433         -			render = function(state, user)
   434         -				return starsoul.ui.build {
   435         -					kind = 'vert', padding = 0.5; w = 5, h = 5, mode = 'sw';
   436         -					{kind = 'label', w = 4, h = 1, text = 'hello'};
   437         -				}
   438         -			end;
   439         -		};
   440         -	};
   441         -})

Deleted mods/starsoul/item.lua version [597f09c699].

     1         -local lib = starsoul.mod.lib
     2         -local I = starsoul.item
     3         -
     4         -function I.mk(item, context)
     5         -	local st = ItemStack(item)
     6         -	local md = st:get_definition()._starsoul
     7         -	local ctx = context or {}
     8         -	if md and md.event then
     9         -		md.event.create(st, ctx)
    10         -	end
    11         -	if context.how == 'print' then
    12         -		if context.schematic and context.schematic.setup then
    13         -			context.schematic.setup(st, ctx)
    14         -		end
    15         -	end
    16         -	return st
    17         -end

Deleted mods/starsoul/mod.conf version [7e3c22e1be].

     1         -name = starsoul
     2         -author = velartrill
     3         -description = world logic and UI
     4         -depends = vtlib

Deleted mods/starsoul/species.lua version [9c138bbb33].

     1         -local lib = starsoul.mod.lib
     2         -
     3         -local paramTypes do local T,G = lib.marshal.t, lib.marshal.g
     4         -	paramTypes = {
     5         -		tone = G.struct {
     6         -			hue = T.angle;
     7         -			sat = T.clamp;
     8         -			lum = T.clamp;
     9         -		};
    10         -		str = T.str;
    11         -		num = T.decimal;
    12         -	}
    13         -end
    14         -
    15         --- constants
    16         -local animationFrameRate = 60
    17         -
    18         -local species = {
    19         -	human = {
    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.';
    22         -		scale = 1.0;
    23         -		params = {
    24         -			{'eyeColor',  'Eye Color',  'tone', {hue=327, sat=0, lum=0}};
    25         -			{'hairColor', 'Hair Color', 'tone', {hue=100, sat=0, lum=0}};
    26         -			{'skinTone',  'Skin Tone',  'tone', {hue=  0, sat=0, lum=0}};
    27         -		};
    28         -		tempRange = {
    29         -			comfort    = {18.3, 23.8}; -- needed for full stamina regen
    30         -			survivable = {5,    33}; -- anything below/above will cause progressively more damage
    31         -		};
    32         -		variants = {
    33         -			female = {
    34         -				name = 'Human Female';
    35         -				mesh = 'starsoul-body-female.x';
    36         -				eyeHeight = 1.4;
    37         -				texture = function(t, adorn)
    38         -					local skin = lib.image 'starsoul-body-skin.png' : shift(t.skinTone)
    39         -					local eye  = lib.image 'starsoul-body-eye.png'  : shift(t.eyeColor)
    40         -					local hair = lib.image 'starsoul-body-hair.png' : shift(t.hairColor)
    41         -
    42         -					local invis = lib.image '[fill:1x1:0,0:#00000000'
    43         -					local plate = adorn.suit and adorn.suit.plate or invis
    44         -					local lining = adorn.suit and adorn.suit.lining or invis
    45         -
    46         -					return {lining, plate, skin, skin, eye, hair}
    47         -				end;
    48         -				stats = {
    49         -					psiRegen = 1.3;
    50         -					psiPower = 1.2;
    51         -					psi = 1.2;
    52         -					hunger = .8; -- women have smaller stomachs
    53         -					thirst = .8;
    54         -					staminaRegen = 1.0;
    55         -					morale = 0.8; -- you are not She-Bear Grylls
    56         -				};
    57         -				traits = {
    58         -					health = 400;
    59         -					lungCapacity = .6;
    60         -					irradiation = 0.8; -- you are smaller, so it takes less rads to kill ya
    61         -					sturdiness = 0; -- women are more fragile and thus susceptible to blunt force trauma
    62         -					metabolism = 1800; --Cal
    63         -					painTolerance = 0.4;
    64         -				};
    65         -			};
    66         -			male = {
    67         -				name = 'Human Male';
    68         -				eyeHeight = 1.6;
    69         -				stats = {
    70         -					psiRegen = 1.0;
    71         -					psiPower = 1.0;
    72         -					psi = 1.0;
    73         -					hunger = 1.0;
    74         -					staminaRegen = .7; -- men are strong but have inferior endurance
    75         -				};
    76         -				traits = {
    77         -					health = 500;
    78         -					painTolerance = 1.0;
    79         -					lungCapacity = 1.0;
    80         -					sturdiness = 0.3;
    81         -					metabolism = 2200; --Cal
    82         -				};
    83         -			};
    84         -		};
    85         -		traits = {};
    86         -	};
    87         -}
    88         -
    89         -starsoul.world.species = {
    90         -	index = species;
    91         -	paramTypes = paramTypes;
    92         -}
    93         -
    94         -function starsoul.world.species.mkDefaultParamsTable(pSpecies, pVariant)
    95         -	local sp = species[pSpecies]
    96         -	local var = sp.variants[pVariant]
    97         -	local vpd = var.defaults or {}
    98         -	local tbl = {}
    99         -	for _, p in pairs(sp.params) do
   100         -		local name, desc, ty, dflt = lib.tbl.unpack(p)
   101         -		tbl[name] = vpd[name] or dflt
   102         -	end
   103         -	return tbl
   104         -end
   105         -
   106         -
   107         -function starsoul.world.species.mkPersonaFor(pSpecies, pVariant)
   108         -	return {
   109         -		species = pSpecies;
   110         -		speciesVariant = pVariant;
   111         -		bodyParams = starsoul.world.species.paramsFromTable(pSpecies,
   112         -			starsoul.world.species.mkDefaultParamsTable(pSpecies, pVariant)
   113         -		);
   114         -		statDeltas = {};
   115         -	}
   116         -end
   117         -
   118         -local function spLookup(pSpecies, pVariant)
   119         -	local sp = species[pSpecies]
   120         -	local var = sp.variants[pVariant or next(sp.variants)]
   121         -	return sp, var
   122         -end
   123         -starsoul.world.species.lookup = spLookup
   124         -
   125         -function starsoul.world.species.statRange(pSpecies, pVariant, pStat)
   126         -	local sp,spv = spLookup(pSpecies, pVariant)
   127         -	local min, max, base
   128         -	if pStat == 'health' then
   129         -		min,max = 0, spv.traits.health
   130         -	elseif pStat == 'breath' then
   131         -		min,max = 0, 65535
   132         -	else
   133         -		local spfac = spv.stats[pStat]
   134         -		local basis = starsoul.world.stats[pStat]
   135         -		min,max = basis.min, basis.max
   136         -
   137         -		if spfac then
   138         -			min = min * spfac
   139         -			max = max * spfac
   140         -		end
   141         -
   142         -		base = basis.base
   143         -		if base == true then
   144         -			base = max
   145         -		elseif base == false then
   146         -			base = min
   147         -		end
   148         -
   149         -	end
   150         -	return min, max, base
   151         -end
   152         -
   153         --- set the necessary properties and create a persona for a newspawned entity
   154         -function starsoul.world.species.birth(pSpecies, pVariant, entity, circumstances)
   155         -	circumstances = circumstances or {}
   156         -	local sp,var = spLookup(pSpecies, pVariant)
   157         -
   158         -	local function pct(st, p)
   159         -		local min, max = starsoul.world.species.statRange(pSpecies, pVariant, st)
   160         -		local delta = max - min
   161         -		return min + delta*p
   162         -	end
   163         -	local ps = starsoul.world.species.mkPersonaFor(pSpecies,pVariant)
   164         -	local startingHP = pct('health', 1.0)
   165         -	if circumstances.injured    then startingHP = pct('health', circumstances.injured) end
   166         -	if circumstances.psiCharged then ps.statDeltas.psi = pct('psi', circumstances.psiCharged) end
   167         -	ps.statDeltas.warmth = 20 -- don't instantly start dying of frostbite
   168         -
   169         -	entity:set_properties{hp_max = var.traits.health or sp.traits.health}
   170         -	entity:set_hp(startingHP, 'initial hp')
   171         -	return ps
   172         -end
   173         -
   174         -function starsoul.world.species.paramsFromTable(pSpecies, tbl)
   175         -	local lst = {}
   176         -	local sp = species[pSpecies]
   177         -	for i, par in pairs(sp.params) do
   178         -		local name,desc,ty,dflt = lib.tbl.unpack(par)
   179         -		if tbl[name] then
   180         -			table.insert(lst, {id=name, value=paramTypes[ty].enc(tbl[name])})
   181         -		end
   182         -	end
   183         -	return lst
   184         -end
   185         -function starsoul.world.species.paramsToTable(pSpecies, lst)
   186         -	local tymap = {}
   187         -	local sp = species[pSpecies]
   188         -	for i, par in pairs(sp.params) do
   189         -		local name,desc,ty,dflt = lib.tbl.unpack(par)
   190         -		tymap[name] = paramTypes[ty]
   191         -	end
   192         -
   193         -	local tbl = {}
   194         -	for _, e in pairs(lst) do
   195         -		tbl[e.id] = tymap[e.id].dec(e.value)
   196         -	end
   197         -	return tbl
   198         -end
   199         -
   200         -for speciesName, sp in pairs(species) do
   201         -	for varName, var in pairs(sp.variants) do
   202         -		if var.mesh then
   203         -			var.animations = starsoul.evaluate(string.format('models/%s.nla', var.mesh)).skel.action
   204         -		end
   205         -	end
   206         -end
   207         -
   208         -
   209         -function starsoul.world.species.updateTextures(ent, persona, adornment)
   210         -	local s,v = spLookup(persona.species, persona.speciesVariant)
   211         -	local paramTable = starsoul.world.species.paramsToTable(persona.species, persona.bodyParams)
   212         -	local texs = {}
   213         -	for i, t in ipairs(v.texture(paramTable, adornment)) do
   214         -		texs[i] = t:render()
   215         -	end
   216         -	ent:set_properties { textures = texs }
   217         -end
   218         -
   219         -function starsoul.world.species.setupEntity(ent, persona)
   220         -	local s,v = spLookup(persona.species, persona.speciesVariant)
   221         -	local _, maxHealth = starsoul.world.species.statRange(persona.species, persona.speciesVariant, 'health')
   222         -	ent:set_properties {
   223         -		visual = 'mesh';
   224         -		mesh = v.mesh;
   225         -		stepheight = .51;
   226         -		eye_height = v.eyeHeight;
   227         -		collisionbox = { -- FIXME
   228         -			-0.3, 0.0, -0.3;
   229         -			0.3, 1.5,  0.3;
   230         -		};
   231         -		visual_size = vector.new(10,10,10) * s.scale;
   232         -
   233         -		hp_max = maxHealth;
   234         -	}
   235         -	local function P(v)
   236         -		if v then return {x=v[1],y=v[2]} end
   237         -		return {x=0,y=0}
   238         -	end
   239         -	ent:set_local_animation(
   240         -		P(v.animations.idle),
   241         -		P(v.animations.run),
   242         -		P(v.animations.work),
   243         -		P(v.animations.runWork),
   244         -		animationFrameRate)
   245         -
   246         -end

Deleted mods/starsoul/stats.lua version [39aabe1578].

     1         -local lib = starsoul.mod.lib
     2         -
     3         -local function U(unit, prec, fixed)
     4         -	if fixed then
     5         -		return function(amt, excludeUnit)
     6         -			if excludeUnit then return tostring(amt/prec) end
     7         -			return string.format("%s %s", amt/prec, unit)
     8         -		end
     9         -	else
    10         -		return function(amt, excludeUnit)
    11         -			if excludeUnit then return tostring(amt/prec) end
    12         -			return lib.math.si(unit, amt/prec)
    13         -		end
    14         -	end
    15         -end
    16         -
    17         -local function C(h, s, l)
    18         -	return lib.color {hue = h, sat = s or 1, lum = l or .7}
    19         -end
    20         -starsoul.world.stats = {
    21         -	psi        = {min = 0, max = 500, base = 0, desc = U('ψ', 10), color = C(320), name = 'Numina'};
    22         -	-- numina is measured in daψ
    23         -	warmth     = {min = -1000, max = 1000, base = 0, desc = U('°C', 10, true), color = C(5), name = 'Warmth'};
    24         -	-- warmth in measured in °C×10
    25         -	fatigue    = {min = 0, max = 76 * 60, base = 0, desc = U('hr', 60, true), color = C(288,.3,.5), name = 'Fatigue'};
    26         -	-- fatigue is measured in minutes one needs to sleep to cure it
    27         -	stamina    = {min = 0, max = 20 * 100, base = true, desc = U('m', 100), color = C(88), name = 'Stamina'};
    28         -	-- 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
    33         -	morale     = {min = 0, max = 24 * 60 * 10, base = true, desc = U('hr', 60, true), color = C(0,0,.8), name = 'Morale'};
    34         -	-- morale is measured in minutes. e.g. at base rate morale degrades by
    35         -	-- 60 points every hour. morale can last up to 10 days
    36         -	irradiation = {min = 0, max = 20000, base = 0, desc = U('Gy', 1000), color = C(141,1,.5), name = 'Irradiation'};
    37         -	-- irrad is measured is milligreys
    38         -	-- 1Gy counters natural healing
    39         -	-- ~3Gy counters basic nanomedicine
    40         -	-- 5Gy causes death within two weeks without nanomedicine
    41         -	-- radiation speeds up psi regen
    42         -	-- morale drain doubles with each 2Gy
    43         -	illness    = {min = 0, max = 1000, base = 0, desc = U('%', 10, true), color = C(71,.4,.25), name = 'Illness'};
    44         -	-- as illness increases, maximum stamina and health gain a corresponding limit
    45         -	-- illness is increased by certain conditions, and decreases on its own as your
    46         -	-- body heals when those conditions wear off. some drugs can lower accumulated illness
    47         -	-- but illness-causing conditions require specific cures
    48         -	-- illness also causes thirst and fatigue to increase proportionately
    49         -}

Deleted mods/starsoul/store.lua version [efa19f35ab].

     1         --- [ʞ] store.lua
     2         ---  ~ lexi hale <lexi@hale.su>
     3         ---  © EUPLv1.2
     4         ---  ? defines serialization datatypes that don't belong to
     5         ---    any individual class
     6         -
     7         -local lib = starsoul.mod.lib
     8         -local T,G = lib.marshal.t, lib.marshal.g
     9         -starsoul.store = {} -- the serialization equivalent of .type
    10         -
    11         --------------
    12         --- persona --
    13         -------------- -----------------------------------------------
    14         --- a Persona is a structure that defines the nature of     --
    15         --- an (N)PC and how it interacts with the Starsoul-managed --
    16         --- portion of the game world -- things like name, species, --
    17         --- stat values, physical characteristics, and so forth     --
    18         -
    19         -local statStructFields = {}
    20         -for k,v in pairs(starsoul.world.stats) do
    21         -	statStructFields[k] = v.srzType or (
    22         -		(v.base == true or v.base > 0) and T.s16 or T.u16
    23         -	)
    24         -end
    25         -
    26         -starsoul.store.compilerJob = G.struct {
    27         -	schematic = T.str;
    28         -	progress = T.clamp;
    29         -}
    30         -
    31         -starsoul.store.persona = G.struct {
    32         -	name = T.str;
    33         -	species = T.str;
    34         -	speciesVariant = T.str;
    35         -	background = T.str;
    36         -	bodyParams = G.array(8, G.struct {id = T.str, value = T.str}); --variant
    37         -
    38         -	statDeltas = G.struct(statStructFields);
    39         -
    40         -	facts = G.array(32, G.array(8, T.str));
    41         -	-- facts stores information the player has discovered and narrative choices
    42         -	-- she has made.
    43         -	-- parametric facts are encoded as horn clauses
    44         -	-- non-parametric facts are encoded as {'fact-mod:fact-id'}
    45         -}
    46         -
    47         -starsoul.store.suitMeta = lib.marshal.metaStore {
    48         -	batteries = {key = 'starsoul:suit_slots_bat', type = T.inventoryList};
    49         -	chips = {key = 'starsoul:suit_slots_chips', type = T.inventoryList};
    50         -	elements = {key = 'starsoul:suit_slots_elem', type = T.inventoryList};
    51         -	guns = {key = 'starsoul:suit_slots_gun', type = T.inventoryList};
    52         -	ammo = {key = 'starsoul:suit_slots_ammo', type = T.inventoryList};
    53         -}

Deleted mods/starsoul/suit.lua version [5d51eaa4b3].

     1         -local lib = starsoul.mod.lib
     2         -
     3         -local suitStore = starsoul.store.suitMeta
     4         -starsoul.item.suit = lib.registry.mk 'starsoul:suits';
     5         -
     6         --- note that this cannot be persisted as a reference to a particular suit in the world
     7         -local function suitContainer(stack, inv)
     8         -	return starsoul.item.container(stack, inv, {
     9         -		pfx = 'starsoul_suit'
    10         -	})
    11         -end
    12         -starsoul.type.suit = lib.class {
    13         -	name = 'starsoul:suit';
    14         -	construct = function(stack)
    15         -		return {
    16         -			item = stack;
    17         -			inv = suitStore(stack);
    18         -		}
    19         -	end;
    20         -	__index = {
    21         -		powerState = function(self)
    22         -			local s = self.item
    23         -			if not s then return nil end
    24         -			local m = s:get_meta():get_int('starsoul:power_mode')
    25         -			if m == 1 then return 'on'
    26         -			elseif m == 2 then return 'powerSave'
    27         -			else return 'off' end
    28         -		end;
    29         -		powerStateSet = function(self, state)
    30         -			local s = self.item
    31         -			if not s then return nil end
    32         -			local m
    33         -			if state == 'on' then m = 1 -- TODO check power level
    34         -			elseif state == 'powerSave' then m = 2
    35         -			else m = 0 end
    36         -			if self:powerLeft() <= 0 then m = 0 end
    37         -			s:get_meta():set_int('starsoul:power_mode', m)
    38         -		end;
    39         -		powerLeft = function(self)
    40         -			local batteries = self.inv.read 'batteries'
    41         -			local power = 0
    42         -			for idx, slot in pairs(batteries) do
    43         -				power = power + starsoul.mod.electronics.dynamo.totalPower(slot)
    44         -			end
    45         -			return power
    46         -		end;
    47         -		powerCapacity = function(self)
    48         -			local batteries = self.inv.read 'batteries'
    49         -			local power = 0
    50         -			for idx, slot in pairs(batteries) do
    51         -				power = power + starsoul.mod.electronics.dynamo.initialPower(slot)
    52         -			end
    53         -			return power
    54         -		end;
    55         -		maxPowerUse = function(self)
    56         -			local batteries = self.inv.read 'batteries'
    57         -			local w = 0
    58         -			for idx, slot in pairs(batteries) do
    59         -				w = w + starsoul.mod.electronics.dynamo.dischargeRate(slot)
    60         -			end
    61         -			return w
    62         -		end;
    63         -		onReconfigure = function(self, inv)
    64         -			-- apply any changes to item metadata and export any subinventories
    65         -			-- to the provided invref, as they may have changed
    66         -			local sc = starsoul.item.container(self.item, inv, {pfx = 'starsoul_suit'})
    67         -			sc:push()
    68         -			self:pullCanisters(inv)
    69         -		end;
    70         -		onItemMove = function(self, user, list, act, what)
    71         -			-- called when the suit inventory is changed
    72         -			if act == 'put' then
    73         -				if list == 'starsoul_suit_bat' then
    74         -					user:suitSound('starsoul-suit-battery-in')
    75         -				elseif list == 'starsoul_suit_chips' then
    76         -					user:suitSound('starsoul-suit-chip-in')
    77         -				elseif list == 'starsoul_suit_canisters' then
    78         -					user:suitSound('starsoul-insert-snap')
    79         -				end
    80         -			elseif act == 'take' then
    81         -				if list == 'starsoul_suit_bat' then
    82         -					user:suitSound('starsoul-insert-snap')
    83         -				elseif list == 'starsoul_suit_chips' then
    84         -					--user:suitSound('starsoul-suit-chip-out')
    85         -				elseif list == 'starsoul_suit_canisters' then
    86         -					user:suitSound('starsoul-insert-snap')
    87         -				end
    88         -			end
    89         -		end;
    90         -		def = function(self)
    91         -			return self.item:get_definition()._starsoul.suit
    92         -		end;
    93         -		pullCanisters = function(self, inv)
    94         -			starsoul.item.container.dropPrefix(inv, 'starsoul_canister')
    95         -			self:forCanisters(inv, function(sc) sc:pull() end)
    96         -		end;
    97         -		pushCanisters = function(self, inv, st, i)
    98         -			self:forCanisters(inv, function(sc)
    99         -				sc:push()
   100         -				return true
   101         -			end)
   102         -		end;
   103         -		forCanisters = function(self, inv, fn)
   104         -			local cans = inv:get_list 'starsoul_suit_canisters'
   105         -			if cans and next(cans) then for i, st in ipairs(cans) do
   106         -				if not st:is_empty() then
   107         -					local pfx = 'starsoul_canister_' .. tostring(i)
   108         -					local sc = starsoul.item.container(st, inv, {pfx = pfx})
   109         -					if fn(sc, st, i, pfx) then
   110         -						inv:set_stack('starsoul_suit_canisters', i, st)
   111         -					end
   112         -				end
   113         -			end end
   114         -		end;
   115         -		establishInventories = function(self, obj)
   116         -			local inv = obj:get_inventory()
   117         -			local ct = suitContainer(self.item, inv)
   118         -			ct:pull()
   119         -			self:pullCanisters(inv)
   120         -
   121         -			--[[
   122         -			local def = self:def()
   123         -			local sst = suitStore(self.item)
   124         -			local function readList(listName, prop)
   125         -				inv:set_size(listName, def.slots[prop])
   126         -				if def.slots[prop] > 0 then
   127         -					local lst = sst.read(prop)
   128         -					inv:set_list(listName, lst)
   129         -				end
   130         -			end
   131         -			readList('starsoul_suit_chips', 'chips')
   132         -			readList('starsoul_suit_bat',   'batteries')
   133         -			readList('starsoul_suit_guns',  'guns')
   134         -			readList('starsoul_suit_elem',  'elements')
   135         -			readList('starsoul_suit_ammo',  'ammo')
   136         -			]]
   137         -		end;
   138         -	};
   139         -}
   140         -
   141         --- TODO find a better place for this!
   142         -starsoul.type.suit.purgeInventories = function(obj)
   143         -	local inv = obj:get_inventory()
   144         -	starsoul.item.container.dropPrefix(inv, 'starsoul_suit')
   145         -	starsoul.item.container.dropPrefix(inv, 'starsoul_canister')
   146         -	--[[inv:set_size('starsoul_suit_bat', 0)
   147         -	inv:set_size('starsoul_suit_guns', 0)
   148         -	inv:set_size('starsoul_suit_chips', 0)
   149         -	inv:set_size('starsoul_suit_ammo', 0)
   150         -	inv:set_size('starsoul_suit_elem', 0)
   151         -	]]
   152         -end
   153         -
   154         -starsoul.item.suit.foreach('starsoul:suit-gen', {}, function(id, def)
   155         -	local icon = lib.image(def.img or 'starsoul-item-suit.png')
   156         -
   157         -	local iconColor = def.iconColor
   158         -	if not iconColor then
   159         -		iconColor = (def.tex and def.tex.plate and def.tex.plate.tint)
   160         -			or def.defaultColor
   161         -		iconColor = iconColor:to_hsl()
   162         -		iconColor.lum = 0
   163         -	end
   164         -
   165         -	if iconColor then icon = icon:shift(iconColor) end
   166         -
   167         -	if not def.adorn then
   168         -		function def.adorn(a, item, persona)
   169         -			local function imageFor(pfx)
   170         -				return lib.image(string.format("%s-%s-%s.png", pfx, persona.species, persona.speciesVariant))
   171         -			end
   172         -			if not def.tex then return end
   173         -			a.suit = {}
   174         -			for name, t in pairs(def.tex) do
   175         -				local img = imageFor(t.id)
   176         -				local color
   177         -
   178         -				local cstr = item:get_meta():get_string('starsoul:tint_suit_' .. name)
   179         -				if cstr and cstr ~= '' then
   180         -					color = lib.color.unmarshal(cstr)
   181         -				elseif t.tint then
   182         -					color = t.tint or def.defaultColor
   183         -				end
   184         -
   185         -				if color then
   186         -					local hsl = color:to_hsl()
   187         -					local adjusted = {
   188         -						hue = hsl.hue;
   189         -						sat = hsl.sat * 2 - 1;
   190         -						lum = hsl.lum * 2 - 1;
   191         -					}
   192         -					img = img:shift(adjusted)
   193         -				end
   194         -
   195         -				a.suit[name] = img
   196         -			end
   197         -		end
   198         -	end
   199         -
   200         -	minetest.register_tool(id, {
   201         -		short_description = def.name;
   202         -		description = starsoul.ui.tooltip {
   203         -			title = def.name;
   204         -			desc = def.desc;
   205         -			color = lib.color(.1, .7, 1);
   206         -		};
   207         -		groups = {
   208         -			suit = 1;
   209         -			inv = 1; -- has inventories
   210         -			batteryPowered = 1; -- has a battery inv 
   211         -			programmable = 1; -- has a chip inv
   212         -		};
   213         -		on_use = function(st, luser, pointed)
   214         -			local user = starsoul.activeUsers[luser:get_player_name()]
   215         -			if not user then return end
   216         -			-- have mercy on users who've lost their suits and wound
   217         -			-- up naked and dying of exposure
   218         -			if user:naked() then
   219         -				local ss = st:take_item(1)
   220         -				user:setSuit(starsoul.type.suit(ss))
   221         -				user:suitSound('starsoul-suit-don')
   222         -				return st
   223         -			end
   224         -		end;
   225         -		inventory_image = icon:render();
   226         -		_starsoul = {
   227         -			container = {
   228         -				workbench = {
   229         -					order = {'batteries','chips','guns','ammo'}
   230         -				};
   231         -				list = {
   232         -					bat = {
   233         -						key = 'starsoul:suit_slots_bat';
   234         -						accept = 'dynamo';
   235         -						sz = def.slots.batteries;
   236         -					};
   237         -					chips = {
   238         -						key = 'starsoul:suit_slots_chips';
   239         -						accept = 'chip';
   240         -						sz = def.slots.chips;
   241         -					};
   242         -					canisters = {
   243         -						key = 'starsoul:suit_slots_canisters';
   244         -						accept = 'canister';
   245         -						sz = def.slots.canisters;
   246         -					};
   247         -					guns = {
   248         -						key = 'starsoul:suit_slots_gun';
   249         -						accept = 'weapon';
   250         -						workbench = {
   251         -							label = 'Weapon';
   252         -							icon = 'starsoul-ui-icon-gun';
   253         -							color = lib.color(1,0,0);
   254         -						};
   255         -						sz = def.slots.guns;
   256         -					};
   257         -					ammo = {
   258         -						key = 'starsoul:suit_slots_ammo';
   259         -						accept = 'ammo';
   260         -						workbench = {
   261         -							label = 'Ammunition';
   262         -							color = lib.color(1,.5,0);
   263         -							easySlots = true; -- all slots accessible on the go
   264         -						};
   265         -						sz = def.slots.ammo;
   266         -					};
   267         -				};
   268         -			};
   269         -			event = {
   270         -				create = function(st,how)
   271         -					local s = suitStore(st)
   272         -					-- make sure there's a defined powerstate
   273         -					starsoul.type.suit(st):powerStateSet 'off'
   274         -					suitContainer(st):clear()
   275         -					--[[ populate meta tables
   276         -					s.write('batteries', {})
   277         -					s.write('guns', {})
   278         -					s.write('ammo', {})
   279         -					s.write('elements', {})
   280         -					s.write('chips', {})]]
   281         -				end;
   282         -			};
   283         -			suit = def;
   284         -		};
   285         -	});
   286         -end)
   287         -
   288         -local slotProps = {
   289         -	starsoul_cfg = {
   290         -		itemClass = 'inv';
   291         -	};
   292         -	starsoul_suit_bat = {
   293         -		suitSlot = true;
   294         -		powerLock = true;
   295         -		itemClass = 'dynamo';
   296         -	};
   297         -	starsoul_suit_chips = {
   298         -		suitSlot = true;
   299         -		powerLock = true;
   300         -		itemClass = 'chip';
   301         -	};
   302         -	starsoul_suit_guns = {
   303         -		suitSlot = true;
   304         -		maintenanceNode = '';
   305         -		itemClass = 'suitWeapon';
   306         -	};
   307         -	starsoul_suit_ammo = {
   308         -		suitSlot = true;
   309         -		maintenanceNode = '';
   310         -		itemClass = 'suitAmmo';
   311         -	};
   312         -	starsoul_suit_canisters = {
   313         -		suitSlot = true;
   314         -		itemClass = 'canister';
   315         -	};
   316         -}
   317         -
   318         -minetest.register_allow_player_inventory_action(function(luser, act, inv, p)
   319         -	local user = starsoul.activeUsers[luser:get_player_name()]
   320         -	local function grp(i,g)
   321         -		return minetest.get_item_group(i:get_name(), g) ~= 0
   322         -	end
   323         -	local function checkBaseRestrictions(list)
   324         -		local restrictions = slotProps[list]
   325         -		if not restrictions then return nil, true end
   326         -		if restrictions.suitSlot then
   327         -			if user:naked() then return restrictions, false end
   328         -		end
   329         -		if restrictions.powerLock then
   330         -			if user:getSuit():powerState() ~= 'off' then return restrictions, false end
   331         -		end
   332         -		return restrictions, true
   333         -	end
   334         -	local function itemFits(item, list)
   335         -		local rst, ok = checkBaseRestrictions(list)
   336         -		if not ok then return false end
   337         -		if rst == nil then return true end
   338         -
   339         -		if rst.itemClass and not grp(item, rst.itemClass) then
   340         -			return false
   341         -		end
   342         -		if rst.maintenanceNode then return false end
   343         -		-- FIXME figure out best way to identify when the player is using a maintenance node
   344         -
   345         -		if grp(item, 'specialInventory') then
   346         -			if grp(item, 'powder') and list ~= 'starsoul_suit_elem' then return false end
   347         -			-- FIXME handle containers
   348         -			if grp(item, 'psi') and list ~= 'starsoul_psi' then return false end
   349         -		end
   350         -
   351         -		return true
   352         -	end
   353         -	local function itemCanLeave(item, list)
   354         -		local rst, ok = checkBaseRestrictions(list)
   355         -		if not ok then return false end
   356         -		if rst == nil then return true end
   357         -
   358         -		if minetest.get_item_group(item:get_name(), 'specialInventory') then
   359         -
   360         -		end
   361         -
   362         -		if rst.maintenanceNode then return false end
   363         -		return true
   364         -	end
   365         -
   366         -	if act == 'move' then
   367         -		local item = inv:get_stack(p.from_list, p.from_index)
   368         -		if not (itemFits(item, p.to_list) and itemCanLeave(item, p.from_list)) then
   369         -			return 0
   370         -		end
   371         -	elseif act == 'put' then
   372         -		if not itemFits(p.stack, p.listname) then return 0 end
   373         -	elseif act == 'take' then
   374         -		if not itemCanLeave(p.stack, p.listname) then return 0 end
   375         -	end
   376         -	return true
   377         -end)
   378         -
   379         -minetest.register_on_player_inventory_action(function(luser, act, inv, p)
   380         -	local user = starsoul.activeUsers[luser:get_player_name()]
   381         -	local function slotChange(slot,a,item)
   382         -		local s = slotProps[slot]
   383         -		if slot == 'starsoul_suit' then
   384         -			user:updateSuit()
   385         -			if user:naked() then
   386         -				starsoul.type.suit.purgeInventories(user.entity)
   387         -				user.power.nano = {}
   388         -			end
   389         -		elseif s and s.suitSlot then
   390         -			local s = user:getSuit()
   391         -			s:onItemMove(user, slot, a, item)
   392         -			s:onReconfigure(user.entity:get_inventory())
   393         -			user:setSuit(s)
   394         -		else return end
   395         -		user:updateHUD()
   396         -	end
   397         -
   398         -	if act == 'put' or act == 'take' then
   399         -		local item = p.stack
   400         -		slotChange(p.listname, act, item)
   401         -	elseif act == 'move' then
   402         -		local item = inv:get_stack(p.to_list, p.to_index)
   403         -		slotChange(p.from_list, 'take', item)
   404         -		slotChange(p.to_list, 'put', item)
   405         -	end
   406         -end)
   407         -
   408         -local suitInterval = 2.0
   409         -starsoul.startJob('starsoul:suit-software', suitInterval, function(delta)
   410         -	local runState = {
   411         -		pgmsRun = {};
   412         -		flags = {};
   413         -	}
   414         -	for id, u in pairs(starsoul.activeUsers) do
   415         -		if not u:naked() then
   416         -			local reconfSuit = false
   417         -			local inv = u.entity:get_inventory()
   418         -			local chips = inv:get_list('starsoul_suit_chips')
   419         -			local suitprog = starsoul.mod.electronics.chip.usableSoftware(chips)
   420         -			for _, prop in pairs(suitprog) do
   421         -				local s = prop.sw
   422         -				if s.kind == 'suitPower' and (s.powerKind == 'passive' or s.bgProc) and (not runState.pgmsRun[s]) then
   423         -					local conf = prop.file.body.conf
   424         -					local enabled = true
   425         -					for _, e in ipairs(conf) do
   426         -						if e.key == 'disable'  and e.value == 'yes' then
   427         -							enabled = false
   428         -							break
   429         -						end
   430         -					end
   431         -					local fn if s.powerKind == 'passive'
   432         -						then fn = s.run
   433         -						else fn = s.bgProc
   434         -					end
   435         -					function prop.saveConf(cfg) cfg = cfg or conf
   436         -						prop.fd:write(cfg)
   437         -						inv:set_stack('starsoul_suit_chips', prop.chipSlot, prop.fd.chip)
   438         -						reconfSuit = true
   439         -					end
   440         -					function prop.giveItem(st)
   441         -						u:thrustUpon(st)
   442         -					end
   443         -					
   444         -					if enabled and fn(u, prop, suitInterval, runState) then
   445         -						runState.pgmsRun[s] = true
   446         -					end
   447         -				end
   448         -			end
   449         -			if reconfSuit then
   450         -				u:reconfigureSuit()
   451         -			end
   452         -		end
   453         -	end
   454         -end)
   455         -

Deleted mods/starsoul/terrain.lua version [faca82716c].

     1         -local T = starsoul.translator
     2         -local lib = starsoul.mod.lib
     3         -
     4         -starsoul.terrain = {}
     5         -local soilSounds = {}
     6         -local grassSounds = {}
     7         -
     8         -minetest.register_node('starsoul:soil', {
     9         -	description = T 'Soil';
    10         -	tiles = {'default_dirt.png'};
    11         -	groups = {dirt = 1};
    12         -	drop = '';
    13         -	sounds = soilSounds;
    14         -	_starsoul = {
    15         -		onDestroy = function() end;
    16         -		kind = 'block';
    17         -		elements = {};
    18         -	};
    19         -})
    20         -
    21         -
    22         -minetest.register_node('starsoul:sand', {
    23         -	description = T 'Sand';
    24         -	tiles = {'default_sand.png'};
    25         -	groups = {dirt = 1};
    26         -	drop = '';
    27         -	sounds = soilSounds;
    28         -	_starsoul = {
    29         -		kind = 'block';
    30         -		fab = starsoul.type.fab { element = { silicon = 25 } };
    31         -	};
    32         -})
    33         -minetest.register_craftitem('starsoul:soil_clump', {
    34         -	short_description = T 'Soil';
    35         -	description = starsoul.ui.tooltip {
    36         -		title = T 'Soil';
    37         -		desc = 'A handful of nutrient-packed soil, suitable for growing plants';
    38         -		color = lib.color(0.3,0.2,0.1);
    39         -	};
    40         -	inventory_image = 'starsoul-item-soil.png';
    41         -	groups = {soil = 1};
    42         -	_starsoul = {
    43         -		fab = starsoul.type.fab { element = { carbon = 12 / 4 } };
    44         -	};
    45         -})
    46         -
    47         -function starsoul.terrain.createGrass(def)
    48         -	local function grassfst(i)
    49         -		local nextNode = def.name
    50         -		if i >= 0 then
    51         -			nextNode = nextNode .. '_walk_' .. tostring(i)
    52         -		end
    53         -		return {
    54         -			onWalk = function(pos)
    55         -				minetest.set_node_at(pos, def.name .. '_walk_2');
    56         -			end;
    57         -			onDecay = function(pos,delta)
    58         -				minetest.set_node_at(pos, nextNode);
    59         -			end;
    60         -			onDestroy = function(pos) end;
    61         -			fab = def.fab;
    62         -			recover = def.recover;
    63         -			recover_vary = def.recover_vary;
    64         -		};
    65         -	end
    66         -	local drop = {
    67         -		max_items = 4;
    68         -		items = {
    69         -			{
    70         -				items = {'starsoul:soil'}, rarity = 2;
    71         -				tool_groups = { 'shovel', 'trowel' };
    72         -			};
    73         -		};
    74         -	}
    75         -	minetest.register_node(def.name, {
    76         -		description = T 'Greengraze';
    77         -		tiles = {
    78         -			def.img .. '.png';
    79         -			'default_dirt.png';
    80         -			{
    81         -				name = 'default_dirt.png^' .. def.img ..'_side.png';
    82         -				tileable_vertical = false;
    83         -			};
    84         -		};
    85         -		groups = {grass = 1, sub_walk = 1};
    86         -		drop = '';
    87         -		sounds = grassSounds;
    88         -		_starsoul = grassfst(2);
    89         -	})
    90         -	for i=2,0,-1 do
    91         -		local opacity = tostring((i/2.0) * 255)
    92         -
    93         -		minetest.register_node(def.name, {
    94         -			description = def.desc;
    95         -			tiles = {
    96         -				def.img .. '.png^(default_footprint.png^[opacity:'..opacity..')';
    97         -				'default_dirt.png';
    98         -				{
    99         -					name = 'default_dirt.png^' .. def.img ..'_side.png';
   100         -					tileable_vertical = false;
   101         -				};
   102         -			};
   103         -			groups = {grass = 1, sub_walk = 1, sub_decay = 5};
   104         -			drop = '';
   105         -			_starsoul = grassfst(i-1);
   106         -			sounds = grassSounds;
   107         -		})
   108         -	end
   109         -end
   110         -
   111         -
   112         -starsoul.terrain.createGrass {
   113         -	name = 'starsoul:greengraze';
   114         -	desc = T 'Greengraze';
   115         -	img = 'default_grass';
   116         -	fab = starsoul.type.fab {
   117         -		element = {
   118         -			carbon = 12;
   119         -		};
   120         -		time = {
   121         -			shred = 2.5;
   122         -		};
   123         -	};
   124         -}
   125         -
   126         -for _, w in pairs {false,true} do
   127         -	minetest.register_node('starsoul:liquid_water' .. (w and '_flowing' or ''), {
   128         -		description = T 'Water';
   129         -		drawtype = 'liquid';
   130         -		waving = 3;
   131         -		tiles = {
   132         -			{
   133         -				name = "default_water_source_animated.png";
   134         -				backface_culling = false;
   135         -				animation = {
   136         -					type = "vertical_frames";
   137         -					aspect_w = 16;
   138         -					aspect_h = 16;
   139         -					length = 2.0;
   140         -				};
   141         -			};
   142         -			{
   143         -				name = "default_water_source_animated.png";
   144         -				backface_culling = true;
   145         -				animation = {
   146         -					type = "vertical_frames";
   147         -					aspect_w = 16;
   148         -					aspect_h = 16;
   149         -					length = 2.0;
   150         -				};
   151         -			};
   152         -		};
   153         -		use_texture_alpha = 'blend';
   154         -		paramtype = 'light';
   155         -		walkable = false, pointable = false, diggable = false, buildable_to = true;
   156         -		is_ground_content = false;
   157         -		drop = '';
   158         -		drowning = 1;
   159         -		liquidtype = w and 'flowing' or 'source';
   160         -		liquid_alternative_flowing = 'starsoul:liquid_water_flowing';
   161         -		liquid_alternative_source = 'starsoul:liquid_water';
   162         -		liquid_viscosity = 1;
   163         -		liquid_renewable = true;
   164         -		liquid_range = 2;
   165         -		drowning = 40;
   166         -		post_effect_color = {a=103, r=10, g=40, b=70};
   167         -		groups = {water = 3, liquid = 3};
   168         -	});
   169         -end
   170         -
   171         -
   172         -starsoul.world.mineral.foreach('starsoul:mineral_generate', {}, function(name,m)
   173         -	local node = string.format('starsoul:mineral_%s', name)
   174         -	local grp = {mineral = 1}
   175         -	minetest.register_node(node, {
   176         -		description = m.desc;
   177         -		tiles = m.tiles or 
   178         -				(m.tone and {
   179         -					string.format('default_stone.png^[colorizehsl:%s:%s:%s',
   180         -						m.tone.hue, m.tone.sat, m.tone.lum)
   181         -				}) or {'default_stone.png'};
   182         -		groups = grp;
   183         -		drop = m.rocks or '';
   184         -		_starsoul = {
   185         -			kind = 'block';
   186         -			elements = m.elements;
   187         -			fab = m.fab;
   188         -			recover = m.recover;
   189         -			recover_vary = m.recover_vary;
   190         -		};
   191         -	})
   192         -	if not m.excludeOre then
   193         -		local seed = 0
   194         -		grp.ore = 1
   195         -		for i = 1, #m.name do
   196         -			seed = seed*50 + string.byte(name, i)
   197         -		end
   198         -		minetest.register_ore {
   199         -			ore = node;
   200         -			ore_type = m.dist.kind;
   201         -			wherein = {m.dist.among};
   202         -			clust_scarcity = m.dist.rare;
   203         -			y_max = m.dist.height[1], y_min = m.dist.height[2];
   204         -			noise_params = m.dist.noise or {
   205         -				offset = 28;
   206         -				scale = 16;
   207         -				spread = vector.new(128,128,128);
   208         -				seed = seed;
   209         -				octaves = 1;
   210         -			};
   211         -		}
   212         -	end
   213         -end)
   214         -
   215         -starsoul.world.mineral.link('feldspar', {
   216         -	desc = T 'Feldspar';
   217         -	excludeOre = true;
   218         -	recover = starsoul.type.fab {
   219         -		time = {
   220         -			shred = 3;
   221         -		};
   222         -		cost = {
   223         -			shredPower = 3;
   224         -		};
   225         -	};
   226         -	recover_vary = function(rng, ctx)
   227         -		-- print('vary!', rng:int(), rng:int(0,10))
   228         -		return starsoul.type.fab {
   229         -			element = {
   230         -				aluminum  = rng:int(0,4);
   231         -				potassium = rng:int(0,2);
   232         -				calcium   = rng:int(0,2);
   233         -			}
   234         -		};
   235         -	end;
   236         -})
   237         -
   238         --- map generation
   239         -
   240         -minetest.register_alias('mapgen_stone', 'starsoul:mineral_feldspar')
   241         -minetest.register_alias('mapgen_water_source', 'starsoul:liquid_water')
   242         -minetest.register_alias('mapgen_river_water_source', 'starsoul:liquid_water')
   243         -

Deleted mods/starsoul/tiers.lua version [97d9d01bfc].

     1         -local lib = starsoul.mod.lib
     2         -
     3         -starsoul.world.tier = lib.registry.mk 'starsoul:tier'
     4         -local T = starsoul.world.tier
     5         -local fab = starsoul.type.fab
     6         -
     7         -function starsoul.world.tier.fabsum(name, ty)
     8         -	local dest = fab {}
     9         -	local t = starsoul.world.tier.db[name]
    10         -	assert(t, 'reference to nonexisting tier '..name)
    11         -	if t.super then
    12         -		dest = dest+starsoul.world.tier.fabsum(t.super, ty)*(t.cost or 1)
    13         -	end
    14         -	if t.fabclasses and t.fabclasses[ty] then
    15         -		dest = dest + t.fabclasses[ty]
    16         -	end
    17         -	return dest
    18         -end
    19         -
    20         -function starsoul.world.tier.tech(name, tech)
    21         -	local t = starsoul.world.tier.db[name]
    22         -	if t.techs and t.techs[tech] ~= nil then return t.techs[tech] end
    23         -	if t.super then return starsoul.world.tier.tech(t.super, tech) end
    24         -	return false
    25         -end
    26         -
    27         -T.meld {
    28         -	base = {
    29         -		fabclass = {
    30         -			electric = fab {metal={copper = 10}};
    31         -			suit = fab {element={carbon = 1e3}};
    32         -			psi = fab {metal={numinium = 1}};
    33         -			bio = fab {element={carbon = 1}};
    34         -		};
    35         -
    36         -	}; -- properties that apply to all tiers
    37         -	------------------
    38         -	-- tier classes --
    39         -	------------------
    40         -
    41         -	lesser = {
    42         -		name = 'Lesser', adj = 'Lesser';
    43         -		super = 'base';
    44         -		fabclasses = {
    45         -			basis = fab {
    46         -				metal = {aluminum=4};
    47         -			};
    48         -		};
    49         -	};
    50         -	greater = {
    51         -		name = 'Greater', adj = 'Greater';
    52         -		super = 'base';
    53         -		fabclasses = {
    54         -			basis = fab {
    55         -				metal = {vanadium=2};
    56         -			};
    57         -		};
    58         -	};
    59         -	starsoul = {
    60         -		name = 'Starsoul', adj = 'Starsoul';
    61         -		super = 'base';
    62         -		fabclasses = {
    63         -			basis = fab {
    64         -				metal = {osmiridium=1};
    65         -			};
    66         -		};
    67         -	};
    68         -	forevanished = {
    69         -		name = 'Forevanished One', adj = 'Forevanished';
    70         -		super = 'base';
    71         -		fabclasses = {
    72         -			basis = fab {
    73         -				metal = {elusium=1};
    74         -			};
    75         -		};
    76         -	};
    77         -
    78         -	------------------
    79         -	-- Lesser Races --
    80         -	------------------
    81         -
    82         -	makeshift = { -- regular trash
    83         -		name = 'Makeshift', adj = 'Makeshift';
    84         -		super = 'lesser';
    85         -		techs = {tool = true, prim = true, electric = true};
    86         -		power = 0.5;
    87         -		efficiency = 0.3;
    88         -		reliability = 0.2;
    89         -		cost = 0.3;
    90         -		fabclasses = { -- characteristic materials
    91         -			basis = fab { -- fallback
    92         -				metal = {iron=3};
    93         -			};
    94         -		};
    95         -	};
    96         -
    97         -	imperial = { --powerful trash
    98         -		name = 'Imperial', adj = 'Imperial';
    99         -		super = 'lesser';
   100         -		techs = {tool = true, electric = true, electronic = true, suit = true, combatSuit = true, weapon = true, hover='ion'};
   101         -		power = 2.0;
   102         -		efficiency = 0.5;
   103         -		reliability = 0.5;
   104         -		cost = 1.0;
   105         -		fabclasses = {
   106         -			basis = fab {
   107         -				metal = {steel=2};
   108         -			};
   109         -		};
   110         -	};
   111         -
   112         -	commune = { --reliability
   113         -		name = 'Commune', adj = 'Commune';
   114         -		super = 'lesser';
   115         -		techs = {tool = true, electric = true, electronic = true, suit = true, combatSuit = true, weapon = true, gravitic = true, hover='grav'};
   116         -		power = 1.0;
   117         -		efficiency = 2.0;
   118         -		reliability = 3.0;
   119         -		cost = 1.5;
   120         -		fabclasses = {
   121         -			basis = fab {
   122         -				metal = {titanium=1};
   123         -				time = {print = 1.2}; -- commune stuff is intricate
   124         -			};
   125         -		};
   126         -	};
   127         -
   128         -	-------------------
   129         -	-- Greater Races --
   130         -	-------------------
   131         -
   132         -
   133         -	----------------
   134         -	-- Starsouled --
   135         -	----------------
   136         -
   137         -	suIkuri = { --super-tier
   138         -		name = 'Su\'ikuri', adj = "Su'ikuruk";
   139         -		super = 'starsoul';
   140         -		techs = {psi = true, prim = true, bioSuit = true, psiSuit = true};
   141         -		power = 1.5;
   142         -		efficiency = 1.0;
   143         -		reliability = 3.0;
   144         -		cost = 2.0;
   145         -		fabclasses = {
   146         -			psi = fab {
   147         -				metal = {numinium = 2.0};
   148         -				crystal = {beryllium = 1.0};
   149         -			};
   150         -			bio = fab {
   151         -				crystal = {beryllium = 1.0};
   152         -			};
   153         -		};
   154         -	};
   155         -
   156         -	usukwinya = { --value for 'money'; no weapons; no hovertech (they are birds)
   157         -		-- NOTA BENE: the ususkwinya *do* have weapons of their own; however,
   158         -		-- they are extremely restricted and never made available except to a
   159         -		-- very select number of that species. consequently, usuk players
   160         -		-- of a certain scenario may have usuk starting weapons, but these must
   161         -		-- be manually encoded to avoid injecting them into the overall crafting
   162         -		-- /loot system. because there are so few of these weapons in existence,
   163         -		-- all so tightly controlled, the odds of the weapons or plans winding
   164         -		-- up on Farthest Shadow are basically zero unless you bring them yourself
   165         -		name = 'Usukwinya', adj = 'Usuk';
   166         -		super = 'starsoul';
   167         -		techs = lib.tbl.set('tool', 'electric', 'electronic', 'suit', 'gravitic');
   168         -		power = 2.0;
   169         -		efficiency = 2.0;
   170         -		reliability = 2.0;
   171         -		cost = 0.5;
   172         -		fabclasses = {
   173         -			basis = fab {
   174         -				crystal = {aluminum = 5}; -- ruby
   175         -			};
   176         -		};
   177         -	};
   178         -
   179         -	eluthrai = { --super-tier
   180         -		name = 'Eluthrai', adj = 'Eluthran';
   181         -		super = 'starsoul';
   182         -		techs = {tool = true, electric = true, electronic = true, weapon = true, gravitic = true, gravweapon = true, suit = true, combatSuit = true, hover = 'grav'};
   183         -		power = 4.0;
   184         -		efficiency = 4.0;
   185         -		reliability = 4.0;
   186         -		cost = 4.0;
   187         -		fabclasses = {
   188         -			basis = fab {
   189         -				crystal = {carbon = 5}; -- diamond
   190         -			};
   191         -			special	= fab {
   192         -				metal = {technetium=1, cinderstone=1}
   193         -			};
   194         -		};
   195         -	};
   196         -
   197         -	-----------------------
   198         -	-- Forevanished Ones --
   199         -	-----------------------
   200         -
   201         -	firstborn = { --god-tier
   202         -		name = 'Firstborn', adj = 'Firstborn';
   203         -		super = 'forevanished';
   204         -		techs = {tool = true, electric = true, electronic = true, suit = true, psi = true, combatSuit = true, weapon = true, gravitic = true, gravweapon = true};
   205         -		power = 10.0;
   206         -		efficiency = 5.0;
   207         -		reliability = 3.0;
   208         -		cost = 10.0;
   209         -		fabclasses = {
   210         -			basis = fab {
   211         -				metal = {technetium=2, neodymium=3, sunsteel=1};
   212         -				crystal = {astrite=1};
   213         -			};
   214         -		};
   215         -	};
   216         -
   217         -	forevanisher = { --godslayer-tier
   218         -		name = 'Forevanisher', adj = 'Forevanisher';
   219         -		super = 'forevanished';
   220         -		techs = {tool = true, electric = true, electronic = true, suit = true, psi = true, combatSuit = true, weapon = true, gravitic = true, gravweapon = true};
   221         -		power = 20.0;
   222         -		efficiency = 1.0;
   223         -		reliability = 2.0;
   224         -		cost = 100.0;
   225         -		fabclasses = {
   226         -			basis = fab {
   227         -				metal = {};
   228         -				crystal = {};
   229         -			};
   230         -		};
   231         -	};
   232         -
   233         -}

Deleted mods/starsoul/ui.lua version [e13ae59c08].

     1         -local lib = starsoul.mod.lib
     2         -
     3         -starsoul.ui = {}
     4         -
     5         -starsoul.type.ui = lib.class {
     6         -	name = 'starsoul:ui';
     7         -	__index = {
     8         -		action = function(self, user, state, fields)
     9         -			local pg = self.pages[state.page or 'index']
    10         -			if not pg then return end
    11         -			if pg.handle then
    12         -				local redraw, reset = pg.handle(state, user, fields)
    13         -				if reset then pg.setupState(state,user) end
    14         -				if redraw then self:show(user) end
    15         -			end
    16         -			if fields.quit then self:cb('onClose', user) end
    17         -		end;
    18         -		cb = function(self, name, user, ...)
    19         -			local state = self:begin(user)
    20         -			if self[name] then self[name](state, user, ...) end
    21         -			local pcb = self.pages[state.page][name] 
    22         -			if pcb then pcb(state, user, ...) end
    23         -		end;
    24         -		begin = function(self, user, page, ...)
    25         -			local state = starsoul.activeUI[user.name]
    26         -			if state and state.form ~= self.id then
    27         -				state = nil
    28         -				starsoul.activeUI[user.name] = nil
    29         -			end
    30         -			local created = state == nil
    31         -
    32         -			if not state then
    33         -				state = {
    34         -					page = page or 'index';
    35         -					form = self.id;
    36         -				}
    37         -				starsoul.activeUI[user.name] = state
    38         -				self:cb('setupState', user, ...)
    39         -			elseif page ~= nil and state.page ~= page then
    40         -				state.page = page
    41         -				local psetup = self.pages[state.page].setupState
    42         -				if psetup then psetup(state,user, ...) end
    43         -			end
    44         -			return state, created
    45         -		end;
    46         -		render = function(self, state, user)
    47         -			return self.pages[state.page].render(state, user)
    48         -		end;
    49         -		show = function(self, user)
    50         -			local state = self:begin(user)
    51         -			minetest.show_formspec(user.name, self.id,self:render(state, user))
    52         -		end;
    53         -		open = function(self, user, page, ...)
    54         -			user:suitSound 'starsoul-nav'
    55         -			self:begin(user, page, ...)
    56         -			self:show(user)
    57         -		end;
    58         -		close = function(self, user)
    59         -			local state = starsoul.activeUI[user.name]
    60         -			if state and state.form == self.id then
    61         -				self:cb('onClose', user)
    62         -				starsoul.activeUI[user.name] = nil
    63         -				minetest.close_formspec(user.name, self.id)
    64         -			end
    65         -		end;
    66         -	};
    67         -	construct = function(p)
    68         -		if not p.id then error('UI missing id') end
    69         -		p.pages = p.pages or {}
    70         -		return p
    71         -	end;
    72         -}
    73         -
    74         -function starsoul.interface.install(ui)
    75         -	starsoul.interface.link(ui.id, ui)
    76         -end
    77         -
    78         -function starsoul.ui.build(def, parent)
    79         -	local clr = def.color
    80         -	if clr and lib.color.id(clr) then
    81         -		clr = clr:to_hsl_o()
    82         -	end
    83         -	local state = {
    84         -		x = (def.x or 0);
    85         -		y = (def.y or 0);
    86         -		w = def.w or 0, h = def.h or 0;
    87         -		fixed = def.fixed or false;
    88         -		spacing = def.spacing or 0;
    89         -		padding = def.padding or 0;
    90         -		align = def.align or (parent and parent.align) or 'left';
    91         -		lines = {};
    92         -		mode = def.mode or (parent and parent.mode or nil); -- hw or sw
    93         -		gen = (parent and parent.gen or 0) + 1;
    94         -		fg = def.fg or (parent and parent.fg);
    95         -		color = clr or (parent and parent.color) or {
    96         -			hue = 260, sat = 0, lum = 0
    97         -		};
    98         -	}
    99         -	local lines = state.lines
   100         -	local cmod = string.format('^[hsl:%s:%s:%s',
   101         -		state.color.hue, state.color.sat*0xff, state.color.lum*0xff)
   102         -
   103         -	local E = minetest.formspec_escape
   104         -	if state.padding/2 > state.x then state.x = state.padding/2 end
   105         -	if state.padding/2 > state.y then state.y = state.padding/2 end
   106         -
   107         -	local function btnColorDef(sel)
   108         -		local function climg(state,img)
   109         -			local selstr
   110         -			if sel == nil then
   111         -				selstr = string.format(
   112         -					'button%s,' ..
   113         -					'button_exit%s,' ..
   114         -					'image_button%s,' ..
   115         -					'item_image_button%s',
   116         -					state, state, state, state)
   117         -			else
   118         -				selstr = E(sel) .. state
   119         -			end
   120         -
   121         -			return string.format('%s[%s;' ..
   122         -					'bgimg=%s;'               ..
   123         -					'bgimg_middle=16;'        ..
   124         -					'content_offset=0,0'      ..
   125         -				']', sel and 'style' or 'style_type',
   126         -				selstr, E(img..'^[resize:48x48'..cmod))
   127         -		end
   128         -
   129         -		return climg('',         'starsoul-ui-button-sw.png')       ..
   130         -		       climg(':hovered', 'starsoul-ui-button-sw-hover.png') ..
   131         -		       climg(':pressed', 'starsoul-ui-button-sw-press.png')
   132         -	end
   133         -	local function widget(...)
   134         -		table.insert(lines, string.format(...))
   135         -	end
   136         -	if def.kind == 'vert' then
   137         -		for _, w in ipairs(def) do
   138         -			local src, st = starsoul.ui.build(w, state)
   139         -			widget('container[%s,%s]%scontainer_end[]', state.x, state.y, src)
   140         -			state.y=state.y + state.spacing + st.h
   141         -			state.w = math.max(state.w, st.w)
   142         -		end
   143         -		state.w = state.w + state.padding
   144         -		state.h = state.y + state.padding/2
   145         -	elseif def.kind == 'hztl' then
   146         -		for _, w in ipairs(def) do
   147         -			local src, st = starsoul.ui.build(w, state)
   148         -			widget('container[%s,%s]%scontainer_end[]', state.x, state.y, src)
   149         -			-- TODO alignments
   150         -			state.x=state.x + state.spacing + st.w
   151         -			state.h = math.max(state.h, st.h)
   152         -		end
   153         -		state.h = state.h + state.padding
   154         -		state.w = state.x + state.padding/2
   155         -	elseif def.kind == 'list' then
   156         -		local slotTypes = {
   157         -			plain = {hue = 200, sat = -.1, lum = 0};
   158         -			element = {hue = 20, sat = -.3, lum = 0};
   159         -			chip = {hue = 0, sat = -1, lum = 0};
   160         -			psi = {hue = 300, sat = 0, lum = 0};
   161         -			power = {hue = 50, sat = 0, lum = .2};
   162         -		}
   163         -		local img
   164         -		if state.mode == 'hw' then
   165         -			img = lib.image('starsoul-ui-slot-physical.png');
   166         -		else
   167         -			img = lib.image('starsoul-ui-slot.png'):shift(slotTypes[def.listContent or 'plain']);
   168         -		end
   169         -		local spac = state.spacing
   170         -		widget('style_type[list;spacing=%s,%s]',spac,spac)
   171         -		assert(def.w and def.h, 'ui-lists require a fixed size')
   172         -		for lx = 0, def.w-1 do
   173         -		for ly = 0, def.h-1 do
   174         -			local ox, oy = state.x + lx*(1+spac), state.y + ly*(1+spac)
   175         -			table.insert(lines, string.format('image[%s,%s;1.1,1.1;%s]', ox-0.05,oy-0.05, img:render()))
   176         -		end end
   177         -		table.insert(lines, string.format('listcolors[#00000000;#ffffff10]')) -- FIXME
   178         -		table.insert(lines, string.format('list[%s;%s;%s,%s;%s,%s;%s]',
   179         -			E(def.target), E(def.inv),
   180         -			state.x, state.y,
   181         -			def.w,   def.h,
   182         -			def.idx))
   183         -		local sm = 1
   184         -		state.w = def.w * sm + (spac * (def.w - 1))
   185         -		state.h = def.h * sm + (spac * (def.h - 1))
   186         -	elseif def.kind == 'contact' then
   187         -		if def.color then table.insert(lines, btnColorDef(def.id)) end
   188         -		widget('image_button%s[%s,%s;%s,%s;%s;%s;%s]',
   189         -			def.close and '_exit' or '',
   190         -			state.x, state.y, def.w, def.h,
   191         -			E(def.img), E(def.id), E(def.label or ''))
   192         -	elseif def.kind == 'button' then
   193         -		if def.color then table.insert(lines, btnColorDef(def.id)) end
   194         -		local label = E(def.label or '')
   195         -		if state.fg then label = lib.color(state.fg):fmt(label) end
   196         -		widget('button%s[%s,%s;%s,%s;%s;%s]',
   197         -			def.close and '_exit' or '',
   198         -			state.x, state.y, def.w, def.h,
   199         -			E(def.id), label)
   200         -	elseif def.kind == 'img' then
   201         -		widget('%s[%s,%s;%s,%s;%s]',
   202         -			def.item and 'item_image' or 'image',
   203         -			state.x, state.y, def.w, def.h, E(def.item or def.img))
   204         -	elseif def.kind == 'label' then
   205         -		local txt = E(def.text)
   206         -		if state.fg then txt = lib.color(state.fg):fmt(txt) end
   207         -		widget('label[%s,%s;%s]',
   208         -			state.x, state.y + def.h*.5, txt)
   209         -	elseif def.kind == 'text' then
   210         -		-- TODO paragraph formatter
   211         -		widget('hypertext[%s,%s;%s,%s;%s;%s]',
   212         -			state.x, state.y, def.w, def.h, E(def.id), E(def.text))
   213         -	elseif def.kind == 'hbar' or def.kind == 'vbar' then -- TODO fancy image bars
   214         -		local cl = lib.color(state.color)
   215         -		local fg = state.fg or cl:readable(.8,1)
   216         -		local wfac, hfac = 1,1
   217         -		local clamp = math.min(math.max(def.fac, 0), 1)
   218         -		if def.kind == 'hbar'
   219         -			then wfac = wfac * clamp
   220         -			else hfac = hfac * clamp
   221         -		end
   222         -		local x,y, w,h = state.x, state.y, def.w, def.h
   223         -		widget('box[%s,%s;%s,%s;%s]',
   224         -			x,y, w,h, cl:brighten(0.2):hex())
   225         -		widget('box[%s,%s;%s,%s;%s]',
   226         -			x, y + (h*(1-hfac)), w * wfac, h * hfac, cl:hex())
   227         -		if def.text then
   228         -			widget('hypertext[%s,%s;%s,%s;;%s]',
   229         -				state.x, state.y, def.w, def.h,
   230         -				string.format('<global halign=center valign=middle color=%s>%s', fg:hex(), E(def.text)))
   231         -		end
   232         -	end
   233         -
   234         -	if def.desc then
   235         -		widget('tooltip[%s,%s;%s,%s;%s]',
   236         -			state.x, state.y, def.w, def.h, E(def.desc))
   237         -	end
   238         -
   239         -	local originX = (parent and parent.x or 0)
   240         -	local originY = (parent and parent.y or 0)
   241         -	local l = table.concat(lines)
   242         -	-- if state.fixed and (state.w < state.x or state.h < state.y) then
   243         -	-- 	l = string.format('scroll_container[%s,%s;%s,%s;scroll_%s;%s]%sscroll_container_end[]',
   244         -	-- 		(parent and parent.x or 0), (parent and parent.y or 0),
   245         -	-- 		state.w, state.h, state.gen,
   246         -	-- 		(state.x > state.w) and 'horizontal' or 'vertical', l)
   247         -	-- end
   248         -	
   249         -
   250         -	if def.mode or def.container then
   251         -		if def.mode then
   252         -			l = string.format('background9[%s,%s;%s,%s;%s;false;64]',
   253         -					originX, originY, state.w, state.h,
   254         -					E(string.format('starsoul-ui-bg-%s.png%s^[resize:128x128',
   255         -						(def.mode == 'sw') and 'digital'
   256         -											or 'panel', cmod))) .. l
   257         -		end
   258         -		if parent == nil or state.color ~= parent.color then
   259         -			l = btnColorDef() .. l
   260         -		end
   261         -	end
   262         -	if not parent then
   263         -		return string.format('formspec_version[6]size[%s,%s]%s', state.w, state.h, l), state
   264         -	else
   265         -		return l, state
   266         -	end
   267         -end
   268         -
   269         -starsoul.ui.tooltip = lib.ui.tooltipper {
   270         -	colors = {
   271         -		-- generic notes
   272         -		neutral = lib.color(.5,.5,.5);
   273         -		good    = lib.color(.2,1,.2);
   274         -		bad     = lib.color(1,.2,.2);
   275         -		info    = lib.color(.4,.4,1);
   276         -		-- chip notes
   277         -		schemaic = lib.color(.2,.7,1);
   278         -		ability  = lib.color(.7,.2,1);
   279         -		driver   = lib.color(1,.7,.2);
   280         -	};
   281         -}

Deleted mods/starsoul/user.lua version [5326333d8a].

     1         --- [ʞ] user.lua
     2         ---  ~ lexi hale <lexi@hale.su>
     3         ---  © EUPL v1.2
     4         ---  ? defines the starsoul.type.user class, which is
     5         ---    the main interface between the game world and the
     6         ---    client. it provides for initial signup and join,
     7         ---    managing the HUD, skinning the player model,
     8         ---    effecting weather changes, etc.
     9         -
    10         -local lib = starsoul.mod.lib
    11         -
    12         -local function hudAdjustBacklight(img)
    13         -	local night = math.abs(minetest.get_timeofday() - .5) * 2
    14         -	local opacity = night*0.8
    15         -	return img:fade(opacity)
    16         -end
    17         -
    18         -local userStore = lib.marshal.metaStore {
    19         -	persona = {
    20         -		key  = 'starsoul:persona';
    21         -		type = starsoul.store.persona;
    22         -	};
    23         -}
    24         -
    25         -local suitStore = starsoul.store.suitMeta
    26         -
    27         -starsoul.type.user = lib.class {
    28         -	name = 'starsoul:user';
    29         -	construct = function(ident)
    30         -		local name, luser
    31         -		if type(ident) == 'string' then
    32         -			name = ident
    33         -			luser = minetest.get_player_by_name(name)
    34         -		else
    35         -			luser = ident
    36         -			name = luser:get_player_name()
    37         -		end
    38         -		return {
    39         -			entity = luser;
    40         -			name = name;
    41         -			hud = {
    42         -				elt = {};
    43         -			};
    44         -			tree = {};
    45         -			action = {
    46         -				bits = 0; -- for control deltas
    47         -				prog = {}; -- for recording action progress on a node; reset on refocus
    48         -				tgt = {type='nothing'};
    49         -				sfx = {};
    50         -				fx = {};
    51         -			};
    52         -			actMode = 'off';
    53         -			power = {
    54         -				nano = {primary = nil, secondary = nil};
    55         -				weapon = {primary = nil, secondary = nil};
    56         -				psi = {primary = nil, secondary = nil};
    57         -				maneuver = nil;
    58         -			};
    59         -			pref = {
    60         -				calendar = 'commune';
    61         -			};
    62         -		}
    63         -	end;
    64         -	__index = {
    65         -		pullPersona = function(self)
    66         -			-- if later records are added in public updates, extend this function to merge them
    67         -			-- into one object
    68         -			local s = userStore(self.entity)
    69         -			self.persona = s.read 'persona'
    70         -		end;
    71         -		pushPersona = function(self)
    72         -			local s = userStore(self.entity)
    73         -			s.write('persona', self.persona)
    74         -		end;
    75         -		uiColor = function(self) return lib.color {hue=238,sat=.5,lum=.5} end;
    76         -		statDelta = function(self, stat, d, cause, abs)
    77         -			local dt = self.persona.statDeltas
    78         -			local base
    79         -			if abs then
    80         -				local min, max
    81         -				min, max, base = self:statRange(stat)
    82         -				if     d == true  then d = max
    83         -				elseif d == false then d = min end
    84         -			end
    85         -			if stat == 'health' then
    86         -				self.entity:set_hp(abs and d or (self.entity:get_hp() + d), cause)
    87         -			elseif stat == 'breath' then
    88         -				self.entity:set_breath(abs and d or (self.entity:get_breath() + d))
    89         -			else
    90         -				if abs then
    91         -					dt[stat] = d - base
    92         -				else
    93         -					dt[stat] = dt[stat] + d
    94         -				end
    95         -				self:pushPersona()
    96         -			end
    97         -			self:updateHUD()
    98         -			-- TODO trigger relevant animations?
    99         -		end;
   100         -		lookupSpecies = function(self)
   101         -			return starsoul.world.species.lookup(self.persona.species, self.persona.speciesVariant)
   102         -		end;
   103         -		phenoTrait = function(self, trait)
   104         -			local s,v = self:lookupSpecies()
   105         -			return v.traits[trait] or s.traits[trait] or 0
   106         -		end;
   107         -		statRange = function(self, stat) --> min, max, base
   108         -			return starsoul.world.species.statRange(
   109         -				self.persona.species, self.persona.speciesVariant, stat)
   110         -		end;
   111         -		effectiveStat = function(self, stat)
   112         -			local val
   113         -			local min, max, base = self:statRange(stat)
   114         -
   115         -			if stat == 'health' then
   116         -				val = self.entity:get_hp()
   117         -			elseif stat == 'breath' then
   118         -				val = self.entity:get_breath()
   119         -			else
   120         -				val = base + self.persona.statDeltas[stat] or 0
   121         -			end
   122         -
   123         -			local d = max - min
   124         -			return val, (val - min) / d
   125         -		end;
   126         -		damageModifier = function(self, kind, amt)
   127         -			if kind == 'bluntForceTrauma' then
   128         -				local std = self:phenoTrait 'sturdiness'
   129         -				if std < 0 then
   130         -					amt = amt / 1+std
   131         -				else
   132         -					amt = amt * 1-std
   133         -				end
   134         -			end
   135         -			return amt
   136         -		end;
   137         -		attachImage = function(self, def)
   138         -			local user = self.entity
   139         -			local img = {}
   140         -			img.id = user:hud_add {
   141         -				type = 'image';
   142         -				text = def.tex;
   143         -				scale = def.scale;
   144         -				alignment = def.align;
   145         -				position = def.pos;
   146         -				offset = def.ofs;
   147         -				z_index = def.z;
   148         -			}
   149         -			if def.update then
   150         -				img.update = function()
   151         -					def.update(user, function(prop, val)
   152         -						user:hud_change(img.id, prop, val)
   153         -					end, def)
   154         -				end
   155         -			end
   156         -			return img
   157         -		end;
   158         -		attachMeter = function(self, def)
   159         -			local luser = self.entity
   160         -			local m = {}
   161         -			local w = def.size or 80
   162         -			local szf = w / 80
   163         -			local h = szf * 260
   164         -			m.meter = luser:hud_add {
   165         -				type = 'image';
   166         -				scale = {x = szf, y = szf};
   167         -				alignment = def.align;
   168         -				position = def.pos;
   169         -				offset = def.ofs;
   170         -				z_index = def.z or 0;
   171         -			}
   172         -			local cx = def.ofs.x + (w/2)*def.align.x
   173         -			local cy = def.ofs.y + (h/2)*def.align.y
   174         -			local oy = cy + h/2 - 42
   175         -			-- this is so fucking fragile holy fuck
   176         -			m.readout = luser:hud_add {
   177         -				type = 'text';
   178         -				scale = {x = w, y = h};
   179         -				size = szf;
   180         -				style = 4;
   181         -				position = def.pos;
   182         -				alignment = {x=0,0};
   183         -				offset = {x = cx, y = oy};
   184         -				z_index = (def.z or 0)+1;
   185         -				number = 0xffffff;
   186         -			}
   187         -			m.destroy = function()
   188         -				luser:hud_remove(m.meter)
   189         -				luser:hud_remove(m.readout)
   190         -			end
   191         -			m.update = function()
   192         -				local v,txt,color,txtcolor = def.measure(luser,def)
   193         -				v = math.max(0, math.min(1, v))
   194         -				local n = math.floor(v*16) + 1
   195         -				local img = hudAdjustBacklight(lib.image('starsoul-ui-meter.png'))
   196         -					:colorize(color or def.color)
   197         -				if def.flipX then
   198         -					img = img:transform 'FX'
   199         -				end
   200         -				img = img:render()
   201         -				img = img .. '^[verticalframe:17:' .. tostring(17 - n)
   202         -				luser:hud_change(m.meter, 'text', img)
   203         -				if txt then
   204         -					luser:hud_change(m.readout, 'text', txt)
   205         -				end
   206         -				if txtcolor then
   207         -					luser:hud_change(m.readout, 'number', txtcolor:hex())
   208         -				end
   209         -			end
   210         -			return m
   211         -		end;
   212         -		attachTextBox = function(self, def)
   213         -			local luser = self.entity
   214         -			local box = {}
   215         -			box.id = luser:hud_add {
   216         -				type = 'text';
   217         -				text = '';
   218         -				alignment = def.align;
   219         -				number = def.color and def.color:int24() or 0xFFffFF;
   220         -				scale = def.bound;
   221         -				size = {x = def.size, y=0};
   222         -				style = def.style;
   223         -				position = def.pos;
   224         -				offset = def.ofs;
   225         -			}
   226         -			box.update = function()
   227         -				local text, color = def.text(self, box, def)
   228         -				luser:hud_change(box.id, 'text', text)
   229         -				if color then
   230         -					luser:hud_change(box.id, 'number', color:int24())
   231         -				end
   232         -			end
   233         -			return box
   234         -		end;
   235         -		attachStatBar = function(self, def)
   236         -			local luser = self.entity
   237         -			local bar = {}
   238         -			local img = lib.image 'starsoul-ui-bar.png'
   239         -			local colorized = img
   240         -			if type(def.color) ~= 'function' then
   241         -				colorized = colorized:shift(def.color)
   242         -			end
   243         -
   244         -			bar.id = luser:hud_add {
   245         -				type = 'statbar';
   246         -				position = def.pos;
   247         -				offset = def.ofs;
   248         -				name = def.name;
   249         -				text = colorized:render();
   250         -				text2 = img:tint{hue=0, sat=-1, lum = -0.5}:fade(0.5):render();
   251         -				number = def.size;
   252         -				item = def.size;
   253         -				direction = def.dir;
   254         -				alignment = def.align;
   255         -				size = {x=4,y=24};
   256         -			}
   257         -			bar.update = function()
   258         -				local sv, sf = def.stat(self, bar, def)
   259         -				luser:hud_change(bar.id, 'number', def.size * sf)
   260         -				if type(def.color) == 'function' then
   261         -					local clr = def.color(sv, luser, sv, sf)
   262         -					luser:hud_change(bar.id, 'text', img:tint(clr):render())
   263         -				end
   264         -			end
   265         -			return bar, {x=3 * def.size, y=16} -- x*2??? what
   266         -		end;
   267         -		createHUD = function(self)
   268         -			local function basicStat(statName)
   269         -				return function(user, bar)
   270         -					return self:effectiveStat(statName)
   271         -				end
   272         -			end
   273         -			local function batteryLookup(user)
   274         -				local max = user:suitPowerCapacity()
   275         -				if max == 0 then return 0, 0 end
   276         -				local ch = user:suitCharge()
   277         -				return (ch/max)*100, ch/max
   278         -			end
   279         -			local function C(h,s,l) return {hue=h,sat=s,lum=l} end
   280         -			local hbofs = (1+self.entity:hud_get_hotbar_itemcount()) * 25
   281         -			local bpad = 8
   282         -			self.hud.elt.health = self:attachStatBar {
   283         -				name = 'health', stat = basicStat 'health';
   284         -				color = C(340,0,.3), size = 100;
   285         -				pos = {x=0.5, y=1}, ofs = {x = -hbofs, y=-48 - bpad};
   286         -				dir = 1;
   287         -				align = {x=-1, y=-1};
   288         -			}
   289         -			self.hud.elt.stamina = self:attachStatBar {
   290         -				name = 'stamina', stat = basicStat 'stamina';
   291         -				color = C(60,0,.2), size = 100;
   292         -				pos = {x=0.5, y=1}, ofs = {x = -hbofs, y=-24 - bpad};
   293         -				dir = 1;
   294         -				align = {x=-1, y=-1};
   295         -			}
   296         -			self.hud.elt.bat = self:attachStatBar {
   297         -				name = 'battery', stat = batteryLookup;
   298         -				color = C(190,0,.2), size = 100;
   299         -				pos = {x=0.5, y=1}, ofs = {x = hbofs - 4, y=-48 - bpad};
   300         -				dir = 0;
   301         -				align = {x=1, y=-1};
   302         -			}
   303         -			self.hud.elt.psi = self:attachStatBar {
   304         -				name = 'psi', stat = basicStat 'psi';
   305         -				color = C(320,0,.2), size = 100;
   306         -				pos = {x=0.5, y=1}, ofs = {x = hbofs - 4, y=-24 - bpad};
   307         -				dir = 0;
   308         -				align = {x=1, y=-1};
   309         -			}
   310         -			self.hud.elt.time = self:attachTextBox {
   311         -				name = 'time';
   312         -				align = {x=0, y=1};
   313         -				pos = {x=0.5, y=1};
   314         -				ofs = {x=0,y=-95};
   315         -				text = function(user)
   316         -					local cal = starsoul.world.time.calendar[user.pref.calendar]
   317         -					return cal.time(minetest.get_timeofday())
   318         -				end;
   319         -			}
   320         -			self.hud.elt.temp = self:attachMeter {
   321         -				name = 'temp';
   322         -				align = {x=1, y=-1};
   323         -				pos = {x=0, y=1};
   324         -				ofs = {x=20, y=-20};
   325         -				measure = function(user)
   326         -					local warm = self:effectiveStat 'warmth'
   327         -					local n, color if warm < 0 then
   328         -						n = math.min(100, -warm)
   329         -						color = lib.color(0.1,0.3,1):lerp(lib.color(0.7, 1, 1), math.min(1, n/50))
   330         -					else
   331         -						n = math.min(100,  warm)
   332         -						color = lib.color(0.1,0.3,1):lerp(lib.color(1, 0, 0), math.min(1, n/50))
   333         -					end
   334         -					local txt = string.format("%s°", math.floor(warm))
   335         -					return (n/50), txt, color
   336         -				end;
   337         -			}
   338         -			self.hud.elt.geiger = self:attachMeter {
   339         -				name = 'geiger';
   340         -				align = {x=-1, y=-1};
   341         -				pos = {x=1, y=1};
   342         -				ofs = {x=-20, y=-20};
   343         -				flipX = true;
   344         -				measure = function(user)
   345         -					local hot = self:effectiveStat 'irradiation'
   346         -					local color = self:uiColor():lerp(lib.color(0.3, 1, 0), math.min(1, hot/5))
   347         -					local txt = string.format("%sGy", math.floor(hot))
   348         -					return (hot/5), txt, color
   349         -				end;
   350         -			}
   351         -			self.hud.elt.crosshair = self:attachImage {
   352         -				name = 'crosshair ';
   353         -				tex = '';
   354         -				pos = {x=.5, y=.5};
   355         -				scale = {x=1,y=1};
   356         -				ofs = {x=0, y=0};
   357         -				align = {x=0, y=0};
   358         -				update = function(user, set)
   359         -					local imgs = {
   360         -						off = '';
   361         -						nano = 'starsoul-ui-crosshair-nano.png';
   362         -						psi = 'starsoul-ui-crosshair-psi.png';
   363         -						weapon = 'starsoul-ui-crosshair-weapon.png';
   364         -					}
   365         -					set('text', imgs[self.actMode] or imgs.off)
   366         -				end;
   367         -			};
   368         -			local hudCenterBG = lib.image 'starsoul-ui-hud-bg.png':colorize(self:uiColor())
   369         -			self.hud.elt.bg = self:attachImage {
   370         -				name = 'hudBg';
   371         -				tex = hudCenterBG:render();
   372         -				pos = {x=.5, y=1};
   373         -				scale = {x=1,y=1};
   374         -				ofs = {x=0, y=0};
   375         -				align = {x=0, y=-1};
   376         -				z = -1;
   377         -				update = function(user, set)
   378         -					set('text', hudAdjustBacklight(hudCenterBG):render())
   379         -				end;
   380         -			};
   381         -		end;
   382         -		onModeChange = function(self, oldMode, silent)
   383         -			self.hud.elt.crosshair.update()
   384         -			if not silent then
   385         -				local sfxt = {
   386         -					off = 'starsoul-mode-off';
   387         -					nano = 'starsoul-mode-nano';
   388         -					psi = 'starsoul-mode-psi';
   389         -					weapon = 'starsoul-mode-weapon';
   390         -				}
   391         -				local sfx = self.actMode and sfxt[self.actMode] or sfxt.off
   392         -				self:suitSound(sfx)
   393         -			end
   394         -		end;
   395         -		actModeSet = function(self, mode, silent)
   396         -			if not mode then mode = 'off' end
   397         -			local oldMode = self.actMode
   398         -			self.actMode = mode
   399         -			self:onModeChange(oldMode, silent)
   400         -			if mode ~= oldMode then
   401         -				starsoul.ui.setupForUser(self)
   402         -			end
   403         -		end;
   404         -		deleteHUD = function(self)
   405         -			for name, e in pairs(self.hud.elt) do
   406         -				self:hud_delete(e.id)
   407         -			end
   408         -		end;
   409         -		updateHUD = function(self)
   410         -			for name, e in pairs(self.hud.elt) do
   411         -				if e.update then e.update() end
   412         -			end
   413         -		end;
   414         -		clientInfo = function(self)
   415         -			return minetest.get_player_information(self.name)
   416         -		end;
   417         -		onSignup = function(self)
   418         -			local meta = self.entity:get_meta()
   419         -			local inv = self.entity:get_inventory()
   420         -			-- 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         -			inv:set_size('main', 6) -- carried items and tools. main hotbar.
   422         -
   423         -			inv:set_size('starsoul_suit', 1) -- your environment suit (change at wardrobe)
   424         -			inv:set_size('starsoul_cfg', 1) -- the item you're reconfiguring / container you're accessing
   425         -
   426         -			local scenario
   427         -			for _, e in pairs(starsoul.world.scenario) do
   428         -				if e.id == starsoul.world.defaultScenario then
   429         -					scenario = e break
   430         -				end
   431         -			end assert(scenario)
   432         -			self.persona = starsoul.world.species.birth(scenario.species, scenario.speciesVariant, self.entity)
   433         -			self.persona.name = self.entity:get_player_name() -- a reasonable default
   434         -			self.persona.background = starsoul.world.defaultScenario
   435         -			self:pushPersona()
   436         -
   437         -			local gifts = scenario.startingItems
   438         -			local inv = self.entity:get_inventory()
   439         -			inv:set_stack('starsoul_suit', 1, starsoul.item.mk(gifts.suit, self, {gift=true}))
   440         -			self:getSuit():establishInventories(self.entity)
   441         -
   442         -			local function giveGifts(name, list)
   443         -				if inv:get_size(name) > 0 then
   444         -					for i, e in ipairs(list) do
   445         -						inv:add_item(name, starsoul.item.mk(e, self, {gift=true}))
   446         -					end
   447         -				end
   448         -			end
   449         -
   450         -			giveGifts('starsoul_suit_bat', gifts.suitBatteries)
   451         -			giveGifts('starsoul_suit_chips', gifts.suitChips)
   452         -			giveGifts('starsoul_suit_guns', gifts.suitGuns)
   453         -			giveGifts('starsoul_suit_ammo', gifts.suitAmmo)
   454         -			giveGifts('starsoul_suit_canisters', gifts.suitCans)
   455         -
   456         -			giveGifts('main', gifts.carry)
   457         -
   458         -			self:reconfigureSuit()
   459         -
   460         -			-- i feel like there has to be a better way
   461         -			local cx = math.random(-500,500)
   462         -			local startPoint
   463         -			repeat local temp = -100
   464         -				local cz = math.random(-500,500)
   465         -				local cy = minetest.get_spawn_level(cx, cz)
   466         -				if cy then
   467         -					startPoint = vector.new(cx,cy,cz)
   468         -					temp = starsoul.world.climate.eval(startPoint,.5,.5).surfaceTemp
   469         -				end
   470         -				if cx > 10000 then break end -- avoid infiniloop in pathological conditions
   471         -			until temp > -2
   472         -			self.entity:set_pos(startPoint)
   473         -			meta:set_string('starsoul_spawn', startPoint:to_string())
   474         -		end;
   475         -		onDie = function(self, reason)
   476         -			local inv = self.entity:get_inventory()
   477         -			local where = self.entity:get_pos()
   478         -			local function dropInv(lst)
   479         -				local l = inv:get_list(lst)
   480         -				for i, o in ipairs(l) do
   481         -					if o and not o:is_empty() then
   482         -						minetest.item_drop(o, self.entity, where)
   483         -					end
   484         -				end
   485         -				inv:set_list(lst, {})
   486         -			end
   487         -			dropInv 'main'
   488         -			dropInv 'starsoul_suit'
   489         -			self:statDelta('psi',     0, 'death', true)
   490         -			self:statDelta('hunger',  0, 'death', true)
   491         -			self:statDelta('thirst',  0, 'death', true)
   492         -			self:statDelta('fatigue', 0, 'death', true)
   493         -			self:statDelta('stamina', 0, 'death', true)
   494         -			self:updateSuit()
   495         -		end;
   496         -		onRespawn = function(self)
   497         -			local meta = self.entity:get_meta()
   498         -			self.entity:set_pos(vector.from_string(meta:get_string'starsoul_spawn'))
   499         -			self:updateSuit()
   500         -			return true
   501         -		end;
   502         -		onJoin = function(self)
   503         -			local me = self.entity
   504         -			local meta = me:get_meta()
   505         -			self:pullPersona()
   506         -
   507         -			-- formspec_version and real_coordinates are apparently just
   508         -			-- completely ignored here
   509         -			me:set_formspec_prepend [[
   510         -				bgcolor[#00000000;true]
   511         -				style_type[button,button_exit,image_button,item_image_button;border=false]
   512         -				style_type[button;bgimg=starsoul-ui-button-hw.png;bgimg_middle=8;content_offset=0,-2]
   513         -				style_type[button:hovered;bgimg=starsoul-ui-button-hw-hover.png;bgimg_middle=8]
   514         -				style_type[button:pressed;bgimg=starsoul-ui-button-hw-press.png;bgimg_middle=8;content_offset=0,1]
   515         -			]]
   516         -			local hotbarSlots = me:get_inventory():get_size 'main';
   517         --- 			local slotTex = 'starsoul-ui-slot.png'
   518         --- 			local hbimg = string.format('[combine:%sx128', 128 * hotbarSlots)
   519         --- 			for i = 0, hotbarSlots-1 do
   520         --- 				hbimg = hbimg .. string.format(':%s,0=%s', 128 * i, slotTex)
   521         --- 			end
   522         -			--me:hud_set_hotbar_image(lib.image(hbimg):colorize(self:uiColor()):fade(.36):render())
   523         --- 			me:hud_set_hotbar_selected_image(lib.image(slotTex):colorize(self:uiColor()):render())
   524         -			me:hud_set_hotbar_image('[fill:1x24:0,0:' .. self:uiColor():fade(.1):hex())
   525         -			me:hud_set_hotbar_selected_image(
   526         -				'[fill:1x24,0,0:' .. self:uiColor():fade(.4):hex() .. '^[fill:1x1:0,23:#ffFFffff'
   527         -			)
   528         -			me:hud_set_hotbar_itemcount(hotbarSlots)
   529         -			me:hud_set_flags {
   530         -				hotbar = true;
   531         -				healthbar = false;
   532         -				breathbar = false;
   533         -				basic_debug = false;
   534         -				crosshair = false;
   535         -			}
   536         -			-- disable builtin crafting
   537         -			local inv = me:get_inventory()
   538         -				inv:set_size('craftpreview', 0)
   539         -				inv:set_size('craftresult', 0)
   540         -				inv:set_size('craft', 0)
   541         -
   542         -			me:set_stars {
   543         -				day_opacity = 0.7;
   544         -			}
   545         -			me:set_sky {
   546         -				sky_color = {
   547         -					  day_sky = '#a7c2cd',   day_horizon = '#ddeeff';
   548         -					 dawn_sky = '#003964',  dawn_horizon = '#87ebff';
   549         -					night_sky = '#000000', night_horizon = '#000E29';
   550         -					fog_sun_tint = '#72e4ff';
   551         -					fog_moon_tint = '#2983d0';
   552         -					fog_tint_type = 'custom';
   553         -				};
   554         -				fog = { -- not respected??
   555         -					-- TODO make this seasonal & vary with weather
   556         -					fog_distance = 40;
   557         -					fog_start = 0.3;
   558         -				};
   559         -			}
   560         -			me:set_sun {
   561         -				texture = 'starsoul-sun.png';
   562         -				sunrise = 'sunrisebg.png^[hsl:180:1:.7';
   563         -				tonemap = 'sun_tonemap.png^[hsl:180:1:.7';
   564         -				scale = 0.8;
   565         -			}
   566         -			me:set_lighting {
   567         -				shadows = {
   568         -					intensity = .5;
   569         -				};
   570         -				exposure = {
   571         -					luminance_max = 3.0;
   572         -					speed_dark_bright = 0.5;
   573         -					speed_bright_dark = 1.0;
   574         -				};
   575         -				volumetric_light = {
   576         -					strength = 0.3;
   577         -				};
   578         -			}
   579         -			me:set_eye_offset(nil, vector.new(3,-.2,10))
   580         -			-- TODO set_clouds speed in accordance with wind
   581         -			starsoul.world.species.setupEntity(me, self.persona)
   582         -			starsoul.ui.setupForUser(self)
   583         -			self:createHUD()
   584         -			self:updateSuit()
   585         -		end;
   586         -		suitStack = function(self)
   587         -			return self.entity:get_inventory():get_stack('starsoul_suit', 1)
   588         -		end;
   589         -		suitSound = function(self, sfx)
   590         -			-- trigger a sound effect from the player's suit computer
   591         -			minetest.sound_play(sfx, {object=self.entity, max_hear_distance=4}, true)
   592         -		end;
   593         -		suitPowerStateSet = function(self, state, silent)
   594         -			-- necessary to enable reacting to power state changes
   595         -			-- e.g. to play sound effects, display warnings
   596         -			local os
   597         -			self:forSuit(function(s)
   598         -				os=s:powerState()
   599         -				s:powerStateSet(state)
   600         -			end)
   601         -			if state == 'off' then
   602         -				if self.actMode == 'nano' or self.actMode == 'weapon' then
   603         -					self:actModeSet('off', silent)
   604         -				end
   605         -			end
   606         -			if not silent and os ~= state then
   607         -				local sfx
   608         -				if state == 'off' then
   609         -					sfx = 'starsoul-power-down'
   610         -				elseif os == 'off' then
   611         -					sfx = 'starsoul-power-up'
   612         -				elseif state == 'powerSave' or os == 'powerSave' then
   613         -					sfx = 'starsoul-configure'
   614         -				end
   615         -				if sfx then self:suitSound(sfx) end
   616         -			end
   617         -		end;
   618         -		species = function(self)
   619         -			return starsoul.world.species.index[self.persona.species]
   620         -		end;
   621         -		updateBody = function(self)
   622         -			local adornment = {}
   623         -			local suitStack = self:suitStack()
   624         -			if suitStack and not suitStack:is_empty() then
   625         -				local suit = suitStack:get_definition()._starsoul.suit
   626         -				suit.adorn(adornment, suitStack, self.persona)
   627         -			end
   628         -			starsoul.world.species.updateTextures(self.entity, self.persona, adornment)
   629         -		end;
   630         -		updateSuit = function(self)
   631         -			self:updateBody()
   632         -			local inv = self.entity:get_inventory()
   633         -			local sst = suitStore(self:suitStack())
   634         -			if self:naked() then
   635         -				starsoul.type.suit.purgeInventories(self.entity)
   636         -				if self.actMode == 'nano' or self.actMode == 'weapon' then
   637         -					self:actModeSet 'off'
   638         -				end
   639         -			else
   640         -				local suit = self:getSuit()
   641         -				suit:establishInventories(self.entity)
   642         -
   643         -				if self:suitCharge() <= 0 then
   644         -					self:suitPowerStateSet 'off'
   645         -				end
   646         -			end
   647         -			self:updateHUD()
   648         -		end;
   649         -		reconfigureSuit = function(self)
   650         -			-- and here's where things get ugly
   651         -			-- you can't have an inventory inside another item. to hack around this,
   652         -			-- we use the player as the location of the suit inventories, and whenever
   653         -			-- there's a change in the content of these inventories, this function is
   654         -			-- called to serialize those inventories out to the suit stack
   655         -			if self:naked() then return end
   656         -			local suit = self:getSuit()
   657         -			suit:onReconfigure(self.entity:get_inventory())
   658         -			self:setSuit(suit)
   659         -
   660         -			-- reconfiguring the suit can affect player abilities: e.g. removing
   661         -			-- / inserting a chip with a minimap program
   662         -		end;
   663         -		getSuit = function(self)
   664         -			local st = self:suitStack()
   665         -			if st:is_empty() then return nil end
   666         -			return starsoul.type.suit(st)
   667         -		end;
   668         -		setSuit = function(self, suit)
   669         -			self.entity:get_inventory():set_stack('starsoul_suit', 1, suit.item)
   670         -		end;
   671         -		changeSuit = function(self, ...)
   672         -			self:setSuit(...)
   673         -			self:updateSuit()
   674         -		end;
   675         -		forSuit = function(self, fn)
   676         -			local s = self:getSuit()
   677         -			if fn(s) ~= false then
   678         -				self:setSuit(s)
   679         -			end
   680         -		end;
   681         -		suitPowerCapacity = function(self) -- TODO optimize
   682         -			if self:naked() then return 0 end
   683         -			return self:getSuit():powerCapacity()
   684         -		end;
   685         -		suitCharge = function(self) -- TODO optimize
   686         -			if self:naked() then return 0 end
   687         -			return self:getSuit():powerLeft()
   688         -		end;
   689         -		suitDrawCurrent = function(self, power, time, whatFor, min)
   690         -			if self:naked() then return 0,0 end
   691         -			local inv = self.entity:get_inventory()
   692         -			local bl = inv:get_list('starsoul_suit_bat')
   693         -			local supply = 0
   694         -			local wasteHeat = 0 --TODO handle internally
   695         -			for slot, ps in ipairs(bl) do
   696         -				if not ps:is_empty() then
   697         -					local p, h = starsoul.mod.electronics.dynamo.drawCurrent(ps, power - supply, time)
   698         -					supply = supply + p
   699         -					wasteHeat = wasteHeat + h
   700         -					if power-supply <= 0 then break end
   701         -				end
   702         -			end
   703         -			if min and supply < min then return 0,0 end
   704         -			inv:set_list('starsoul_suit_bat', bl)
   705         -			self:reconfigureSuit()
   706         -			if whatFor then
   707         -				-- TODO display power use icon
   708         -			end
   709         -			return supply, wasteHeat
   710         -		end;
   711         -		naked = function(self)
   712         -			return self:suitStack():is_empty()
   713         -		end;
   714         -		onPart = function(self)
   715         -			starsoul.liveUI     [self.name] = nil
   716         -			starsoul.activeUI   [self.name] = nil
   717         -			starsoul.activeUsers[self.name] = nil
   718         -		end;
   719         -		openUI = function(self, id, page, ...)
   720         -			local ui = assert(starsoul.interface.db[id])
   721         -			ui:open(self, page, ...)
   722         -		end;
   723         -		onRespond = function(self, ui, state, resp)
   724         -			ui:action(self, state, resp)
   725         -		end;
   726         -
   727         -		updateWeather = function(self)
   728         -		end;
   729         -
   730         -		canInteract = function(self, with)
   731         -			return true; -- TODO
   732         -		end;
   733         -
   734         -		trigger = function(self, which, how)
   735         -			--print('trigger', which, dump(how))
   736         -			local p
   737         -			local wld = self.entity:get_wielded_item()
   738         -			if which == 'maneuver' then
   739         -				p = self.power.maneuver
   740         -			elseif which == 'retarget' then
   741         -				self.action.prog = {}
   742         -			elseif wld and not wld:is_empty() then
   743         -				local wdef = wld:get_definition()
   744         -				if wdef._starsoul and wdef._starsoul.tool then
   745         -					p = {tool = wdef._starsoul.tool}
   746         -				end
   747         -			elseif self.actMode ~= 'off' then
   748         -				p = self.power[self.actMode][which]
   749         -			end
   750         -			if p == nil then return false end
   751         -			local ctx, run = {
   752         -				how = how;
   753         -			}
   754         -			if p.chipID then
   755         -				local inv = self.entity:get_inventory()
   756         -				local chips = inv:get_list 'starsoul_suit_chips'
   757         -				for chSlot, ch in pairs(chips) do
   758         -					if ch and not ch:is_empty() then
   759         -						local d = starsoul.mod.electronics.chip.read(ch)
   760         -						if d.uuid == p.chipID then
   761         -							local pgm = assert(d.files[p.pgmIndex], 'file missing for ability')
   762         -							ctx.file = starsoul.mod.electronics.chip.fileHandle(ch, p.pgmIndex)
   763         -							ctx.saveChip = function()
   764         -								inv:set_slot('starsoul_suit_chips', chSlot, ch)
   765         -							end
   766         -							local sw = starsoul.item.sw.db[pgm.body.pgmId]
   767         -							run = assert(sw.run, 'missing run() for active software ability ' .. pgm.body.pgmId)
   768         -							break
   769         -						end
   770         -					end
   771         -				end
   772         -			else
   773         -				error('bad ability pointer ' .. dump(p))
   774         -			end
   775         -			if run then
   776         -				run(self, ctx)
   777         -				return true
   778         -			end
   779         -			return false
   780         -		end;
   781         -		give = function(self, item)
   782         -			local inv = self.entity:get_inventory()
   783         -			local function is(grp)
   784         -				return minetest.get_item_group(item:get_name(), grp) ~= 0
   785         -			end
   786         -			-- TODO notif popups
   787         -			if is 'specialInventory' then
   788         -				if is 'powder' then
   789         -					if self:naked() then return item end
   790         -					local cans = inv:get_list 'starsoul_suit_canisters'
   791         -					if cans and next(cans) then for i, st in ipairs(cans) do
   792         -						local lst = string.format('starsoul_canister_%u_elem', i)
   793         -						item = inv:add_item(lst, item)
   794         -						if item:is_empty() then break end
   795         -					end end
   796         -					self:forSuit(function(x) x:pushCanisters(inv) end)
   797         -				end
   798         -				return item
   799         -			else
   800         -				return inv:add_item('main', item)
   801         -			end
   802         -		end;
   803         -		thrustUpon = function(self, item)
   804         -			local r = self:give(st)
   805         -			if not r:is_empty() then
   806         -				return minetest.add_item(self.entity:get_pos(), r)
   807         -			end
   808         -		end;
   809         -	};
   810         -}
   811         -
   812         -local biointerval = 3.0
   813         -starsoul.startJob('starsoul:bio', biointerval, function(delta)
   814         -	for id, u in pairs(starsoul.activeUsers) do
   815         -
   816         -	end
   817         -end)
   818         -
   819         -local cbit = {
   820         -	up   = 0x001;
   821         -	down = 0x002;
   822         -	left = 0x004;
   823         -	right= 0x008;
   824         -	jump = 0x010;
   825         -	manv = 0x020;
   826         -	snk  = 0x040;
   827         -	dig  = 0x080;
   828         -	put  = 0x100;
   829         -	zoom = 0x200;
   830         -}
   831         --- this is the painful part
   832         -minetest.register_globalstep(function(delta)
   833         -	local doNothing,mustInit,mustHalt = 0,1,2
   834         -	for id, user in pairs(starsoul.activeUsers) do
   835         -		local ent = user.entity
   836         -		local bits = ent:get_player_control_bits()
   837         -
   838         -		local function what(b)
   839         -			if bit.band(bits, b) ~= 0 and bit.band(user.action.bits, b) == 0 then
   840         -				return mustInit
   841         -			elseif bit.band(bits, b) == 0 and bit.band(user.action.bits, b) ~= 0 then
   842         -				return mustHalt
   843         -			else return doNothing end
   844         -		end
   845         -		local skipBits = 0
   846         -		if user.action.bits ~= bits then
   847         -			local mPrimary = what(cbit.dig)
   848         -			local mSecondary = what(cbit.put)
   849         -			if mPrimary == mustInit then -- ENGINE-BUG
   850         -				user.action.tgt = {type='nothing'}
   851         -				user.action.prog = {}
   852         -			elseif mPrimary == mustHalt then
   853         -				user:trigger('primary', {state='halt'})
   854         -			end
   855         -			if mSecondary == mustHalt then
   856         -				user:trigger('secondary', {state='halt'})
   857         -			end
   858         -		end
   859         -		--bits = bit.band(bits, bit.bnot(skipBits))
   860         -		if bit.band(bits, cbit.dig)~=0 then
   861         -			user:trigger('primary', {state='prog', delta=delta})
   862         -		end
   863         -		if bit.band(bits, cbit.put)~=0 then
   864         -			user:trigger('secondary', {state='prog', delta=delta})
   865         -		end
   866         -		user.action.bits = bits
   867         -		-- ENGINE-BUG: dig and put are not handled equally in the
   868         -		-- engine. it is possible for the put bit to get stuck on
   869         -		-- if the key is hammered while the player is not moving.
   870         -		-- the bit will release as soon as the player looks or turns
   871         -		-- nonetheless this is obnoxious
   872         -	end
   873         -end)

Deleted mods/starsoul/world.lua version [20212b373d].

     1         -local lib = starsoul.mod.lib
     2         -local world = starsoul.world
     3         -
     4         -function world.date()
     5         -	local days = minetest.get_day_count()
     6         -	local year = math.floor(days / world.planet.orbit);
     7         -	local day = days % world.planet.orbit;
     8         -	return {
     9         -		year = year, day = day;
    10         -		season = day / world.planet.orbit;
    11         -	}
    12         -end
    13         -local lerp = lib.math.lerp
    14         -
    15         -local function gradient(grad, pos)
    16         -	local n = #grad
    17         -	if n == 1 then return grad[1] end
    18         -	local op = pos*(n-1)
    19         -	local idx = math.floor(op)
    20         -	local t = op-idx
    21         -	return lerp(t, grad[1 + idx], grad[2 + idx])
    22         -end
    23         -
    24         -local altitudeCooling = 10 / 100
    25         -
    26         --- this function provides the basis for temperature calculation,
    27         --- which is performed by adding this value to the ambient temperature,
    28         --- determined by querying nearby group:heatSource items in accordance
    29         --- with the inverse-square law
    30         -function world.climate.eval(pos, tod, season)
    31         -	local data = minetest.get_biome_data(pos)
    32         -	local biome = world.ecology.biomes.db[minetest.get_biome_name(data.biome)]
    33         -	local heat, humid = data.heat, data.humidity
    34         -	tod = tod or minetest.get_timeofday()
    35         -	heat = lerp(math.abs(tod - 0.5)*2, heat, heat + biome.nightTempDelta)
    36         -
    37         -	local td = world.date()
    38         -	heat = heat + gradient(biome.seasonalTemp, season or td.season)
    39         -	if pos.y > 0 then
    40         -		heat = heat - pos.y*altitudeCooling 
    41         -	end
    42         -
    43         -	return {
    44         -		surfaceTemp = heat;
    45         -		waterTemp = heat + biome.waterTempDelta;
    46         -		surfaceHumid = humid;
    47         -	}
    48         -end
    49         -
    50         -local vdsq = lib.math.vdsq
    51         -function world.climate.temp(pos) --> irradiance at pos in W
    52         -	local cl = world.climate.eval(pos)
    53         -	local radCenters = starsoul.region.radiator.store:get_areas_for_pos(pos, false, true)
    54         -	local irradiance = 0
    55         -	for _,e in pairs(radCenters) do
    56         -		local rpos = minetest.string_to_pos(e.data)
    57         -		local rdef = assert(minetest.registered_nodes[assert(minetest.get_node(rpos)).name])
    58         -		local rc = rdef._starsoul.radiator
    59         -		local r_max = rc.radius(rpos)
    60         -
    61         -		local dist_sq = vdsq(rpos,pos)
    62         -		if dist_sq <= r_max^2 then
    63         -			-- cheap bad way
    64         -			-- if minetest.line_of_sight(rpos,pos) then
    65         -			--
    66         -			-- expensive way
    67         -			local obstruct = 0
    68         -			local ray = Raycast(rpos, pos, true, true)
    69         -			for p in ray do
    70         -				if p.type == 'node' then obstruct = obstruct + 1 end
    71         -			end
    72         -
    73         -			if obstruct < 4 then
    74         -				local power, customFalloff = rc.radiate(rpos, pos)
    75         -				-- okay this isn't the real inverse square law but i 
    76         -				-- couldn't figure out a better way to simplify the
    77         -				-- model without checking an ENORMOUS number of nodes
    78         -				-- maybe someone else who isn't completely
    79         -				-- mathtarded can do better.
    80         -				if not customFalloff then
    81         -					power = power * (1 - (dist_sq / ((r_max+1)^2)))
    82         -				end
    83         -				power = power * (1 - (obstruct/5))
    84         -				irradiance = irradiance + power
    85         -			end
    86         -		end
    87         -	end
    88         -	return irradiance + cl.surfaceTemp
    89         -end
    90         -
    91         -world.ecology.biomes.foreach('starsoul:biome-gen', {}, function(id, b)
    92         -	b.def.name = id
    93         -	minetest.register_biome(b.def)
    94         -end)
    95         -
    96         -world.ecology.biomes.link('starsoul: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      = 'starsoul:greengraze', depth_top = 1;
   103         -		node_filler   = 'starsoul:soil',    depth_filler = 4;
   104         -		node_riverbed = 'starsoul: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('starsoul: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    = 'starsoul:sand', depth_top    = 1;
   122         -		node_filler = 'starsoul:sand', depth_filler = 3;
   123         -	};
   124         -})
   125         -
   126         -local toward = lib.math.toward
   127         -local hfinterval = 1.5
   128         -starsoul.startJob('starsoul:heatflow', hfinterval, function(delta)
   129         -
   130         -	-- our base thermal conductivity (κ) is measured in °C/°C/s. say the
   131         -	-- player is in -30°C weather, and has an internal temperature of
   132         -	-- 10°C. then:
   133         -	--   κ  = .1°C/C/s (which is apparently 100mHz)
   134         -	--   Tₚ =  10°C
   135         -	--   Tₑ = -30°C
   136         -	--   d  = Tₑ − Tₚ = -40°C
   137         -	--   ΔT = κ×d = -.4°C/s
   138         -	-- our final change in temperature is computed as tΔC where t is time
   139         -	local kappa = .05
   140         -	for name,user in pairs(starsoul.activeUsers) do
   141         -		local tr = user:species().tempRange
   142         -		local t = starsoul.world.climate.temp(user.entity:get_pos())
   143         -		local insul = 0
   144         -		local naked = user:naked()
   145         -		local suitDef
   146         -		if not naked then
   147         -			suitDef = user:suitStack():get_definition()
   148         -			insul = suitDef._starsoul.suit.temp.insulation
   149         -		end
   150         -
   151         -		local warm = user:effectiveStat 'warmth'
   152         -		local tSafeMin, tSafeMax = tr.survivable[1], tr.survivable[2]
   153         -		local tComfMin, tComfMax = tr.comfort[1], tr.comfort[2]
   154         -
   155         -		local tDelta = (kappa * (1-insul)) * (t - warm) * hfinterval
   156         -		local tgt = warm + tDelta
   157         -
   158         -		-- old logic: we move the user towards the exterior temperature, modulated
   159         -		-- by her suit insulation.
   160         -		--local tgt = toward(warm, t, hfinterval * thermalConductivity * (1 - insul))
   161         -
   162         -		if not naked then
   163         -			local suit = user:getSuit()
   164         -			local suitPower = suit:powerState()
   165         -			local suitPowerLeft = suit:powerLeft()
   166         -			if suitPower ~= 'off' then
   167         -				local coilPower = 1.0
   168         -				local st = suitDef._starsoul.suit.temp
   169         -				if suitPower == 'powerSave' and (tgt >= tSafeMin and tgt <= tSafeMax) then coilPower = 0.5 end
   170         -				if tgt < tComfMin and st.maxHeat > 0 then
   171         -					local availPower = user:suitDrawCurrent(st.heatPower*coilPower, hfinterval)
   172         -					tgt = tgt + (availPower / st.heatPower) * st.maxHeat * coilPower * hfinterval
   173         -				end
   174         -				if tgt > tComfMax and st.maxCool > 0 then
   175         -					local availPower = user:suitDrawCurrent(st.coolPower*coilPower, hfinterval)
   176         -					tgt = tgt - (availPower / st.coolPower) * st.maxCool * coilPower * hfinterval
   177         -				end
   178         -			end
   179         -		end
   180         -
   181         -		user:statDelta('warmth', tgt - warm) -- dopey but w/e
   182         -
   183         -		warm = tgt -- for the sake of readable code
   184         -
   185         -		if warm < tSafeMin or warm > tSafeMax then
   186         -			local dv
   187         -			if warm < tSafeMin then
   188         -				dv = math.abs(warm - tSafeMin)
   189         -			else
   190         -				dv = math.abs(warm - tSafeMax)
   191         -			end
   192         -			-- for every degree of difference you suffer 2 points of damage/s
   193         -			local dmg = math.ceil(dv * 2)
   194         -			user:statDelta('health', -dmg)
   195         -		end
   196         -	end
   197         -end)

Modified src/lore.ct from [8a0d33ccae] to [176ec871ae].

     1         -# starsoul lore
            1  +# starlit lore
     2      2   ! spoilers ahoy!
     3      3   
     4      4   ## Thinking Few
     5      5   the Galaxy teems with life, but only one in a trillion of its creatures is fully sophont, with a soul and mentality of their own.
     6      6   
     7      7   ### Lesser Races
     8         -the majority of the Thinking Few are held in thrall to the Starsouled, their null psionic potential locking them out of the higher levels of civilizational power and attainment.
            8  +the majority of the Thinking Few are held in thrall to the Starlit, their null psionic potential locking them out of the higher levels of civilizational power and attainment.
     9      9   
    10     10   #### Humans
    11     11   The weeds of the galactic flowerbed. Humans are one of the Lesser Races, excluded from the ranks of the Greater 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 all but 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.
    12     12   
    13     13   #### Kruthandi
    14     14   The Kruthandi are a race of four-armed marsupialoids, with the rough body proportions of meerkats, though much larger.
    15     15   
    16     16   Geographically, they occupy a small net of systems linked only through their home system. Unable to accept the reality that their lack of psionics doomed them to a subservient, sublight role, the Kruthandi indulged in a brief, petulant, and entirely futile war with a Su'ikuri state[^lizard-war], and then retreated to their home web to sulk. Access to their space is tightly controlled, and psionic races are absolutely barred from their worlds, even the mere Greater Races, tho this is hilariously unenforcible. In practice they are particularly vulnerable to psicrime – when you ban psionics, only criminals will have mind powers. The Kruthandi are generally viewed with pity and amusement as a pathetic basket case of a civilization, and engage in little intercourse with the broader galaxy.
    17     17   
    18     18   	lizard-war: The Su'ikuri sovereign in question was of a decidedly philosophical bent and was commendably gentle dealing with the upstarts, seeming more bemused than angered by the attack. He took a few of their leaders as battle trophies and spanked the remainder out of his system. There were no casualties on either side.
    19     19   
    20         -However, the Kruthandi are an ancient and sophisticated civilization, and there is much more to their culture than mere wallowing in victimhood (though this certainly has a special place in the Kruthandi heart). Their hate-crush on the Starsouled Races has lead to an utter obsession with metric technology, and the Kruthandi have, by sheer brute force and fanatic tenacity, built a surprisingly sophisticated gravitics industry. While this makes little economic sense, it soothes the Kruthandic psyche.
           20  +However, the Kruthandi are an ancient and sophisticated civilization, and there is much more to their culture than mere wallowing in victimhood (though this certainly has a special place in the Kruthandi heart). Their hate-crush on the Starlit Races has lead to an utter obsession with metric technology, and the Kruthandi have, by sheer brute force and fanatic tenacity, built a surprisingly sophisticated gravitics industry. While this makes little economic sense, it soothes the Kruthandic psyche.
    21     21   
    22     22   #### Qir
    23     23   a race of religious fanatics cleaving to no particular faith, Qir enthusiastically adopt and syncretize new religions as fast as Heaven can churn out prophets. their fanaticism seems to be a cultural evolution to compensate for exceptionally weak mememmune systems; unable to properly critique new ideas, the Qir need to outsource much of their reasoning to systems of sacred commandments, ideally those developed by species who know how to deal with memes. good old-fashioned natural selection does the rest.
    24     24   
    25     25   ### Greater Races
    26         -a handful of species have souls that are just barely capable of developing external psionic channels. the respected vassals of the Starsouled, they can touch the spirits of others, sending messages and reading thoughtforms -- or attacking with torturous sensations and ruinous emotion. however, their power does not extend to the physical universe; they rely entirely on their Starsouled masters for access to faster-than-light travel.
           26  +a handful of species have souls that are just barely capable of developing external psionic channels. the respected vassals of the Starlit, they can touch the spirits of others, sending messages and reading thoughtforms -- or attacking with torturous sensations and ruinous emotion. however, their power does not extend to the physical universe; they rely entirely on their Starlit masters for access to faster-than-light travel.
    27     27   
    28         -### Starsouled
    29         -the unquestioned lords of space and time, the Starsouled Races are those lucky few to have evolved cognitive architectures that allow a soul to reach its full development potential, progressing from merely cogitating about the universe to manipulating it directly with innate power. seemingly as a consequence of the necessary neural architecture, the Starsouled are all ferociously intelligent -- IQs below 150 are unheard of. however, they are marred by a proportional tendency toward mental instability and psychosis.
           28  +### Starlit Races
           29  +the unquestioned lords of space and time, the Starlit Races are those lucky few to have evolved cognitive architectures that allow a soul to reach its full development potential, progressing from merely cogitating about the universe to manipulating it directly with innate power. seemingly as a consequence of the necessary neural architecture, the Starlit are all ferociously intelligent -- IQs below 150 are unheard of. however, they are marred by a proportional tendency toward mental instability and psychosis.
    30     30   
    31     31   their civilizations are known as the Powers.
    32     32   
    33     33   #### Su'ikuri
    34     34   (sg. Su'ikutra, adj. Su'ikuruk)
    35     35   a reptilian race of artists, aesthetes, hedonists, monks, and philosophers, the Su'ikuri are an idle, contemplative, and aristocratic people whose massive psionic sophistication numbers them among the Powers -- much to the annoyance of the Eluthrai. as any adult has the requisite level of finesse and raw power to tweak individual alleles throughout the whole body of a living organism, the Su'ikuri are a race of peerless organgineers. they eschew "dead" hylotechnology, and insist on using biotech wherever remotely practicable.
    36     36   
    37     37   their 'spacecraft' are massive tree-like organisms housing whole ecosystems, propelled and protected against radiation by the psionic power of their crew. sometimes they are equipped with technology produced by a vassal race, but only when unavoidable. 
    38     38   
    39     39   Su'ikuri generally use Lesser Races for manual labor, and Greater Races to overseer these laborers. whether these are paid and respected laborers or outright slaves depends entirely on the ethos of the local civilization.
    40     40   
    41         -Su'ikuruk society is strictly feudal, with a hierarchy based on psionic skill and wit. virtually all conflict is resolved with either a polite, prolonged philosophical debate (the Su'ikuruk version of a duel) or a brute psionic struggle -- the party overpowered by the greater psion is compelled to submit totally, and may achieve freedom only by strengthening their soul to the point of being able to overpower their former superior. even other members of other Starsouled Races can wind up enslaved this way.
           41  +Su'ikuruk society is strictly feudal, with a hierarchy based on psionic skill and wit. virtually all conflict is resolved with either a polite, prolonged philosophical debate (the Su'ikuruk version of a duel) or a brute psionic struggle -- the party overpowered by the greater psion is compelled to submit totally, and may achieve freedom only by strengthening their soul to the point of being able to overpower their former superior. even other members of other Starlit Races can wind up enslaved this way.
    42     42   
    43     43   Su'ikuri relations with the Eluthrai are, as a rule, extremely strained, and many small but high-energy wars have been fought between the Su'ikuruk Powers and the Corcordance.
    44     44   
    45         -a motivated and talented Su'ikutra can reach astropathic levels of psionic power with only a century of practice, something otherwise unheard of among Starsouls.
           45  +a motivated and talented Su'ikutra can reach astropathic levels of psionic power with only a century of practice, something otherwise unheard of among the Starlit.
    46     46   
    47     47   #### Usukwinya
    48     48   (sg. Usukwinti, adj. Usuk)
    49     49   
    50     50   the Usukwinya, known affectionately as the "Tradebirds", are a psionic avian race. their adults range in height from 1 to 1.3m, and 20-30kg in weight. they lack precise manipulatory appendages and are physically weak, forcing them to rely heavily on their psionics for everyday dasks. however, they remain fully capable of flight even without psionic assistance.
    51     51   
    52     52   culturally, the Usukwinya are a mercantile race. they exert their power not through physical force, but through obscene wealth, garnered by selling their painstakingly [^drm value-engineered] technologies to the highest bidder. some rare few Usukwinya will also rent out their psionics, even to the Lesser Races (if they can afford their prodigious fees).
    53     53   	drm: Usukwinya DRM is some of the most powerful in the Reach.
    54     54   
    55         -the enthusiastic capitalism of the Usukwinya is tempered by a hardwired loyalty drive so powerful that before First Contact they had no concept of contract law. they are also noteworthy for having never fought a war among themselves, and seem utterly unwilling to resort to force unless physically provoked. their governments all work diligently to maintain peace among the other races, and the somewhat absurd spectacle of a Starsouled diplomat gently negotiating with two hysterical Lesser ambassadors tends to crop up when two factions the Usukwinya have good relationships with threaten war on one another. (the Eluthrai find this patently ridiculous, and prefer to maintain peace with a judicious application of preventive violence. several Usukwinya-organized peace conferences have dissolved when the Eluthrai summarily shattered the offending governments without a note of forewarning.)
           55  +the enthusiastic capitalism of the Usukwinya is tempered by a hardwired loyalty drive so powerful that before First Contact they had no concept of contract law. they are also noteworthy for having never fought a war among themselves, and seem utterly unwilling to resort to force unless physically provoked. their governments all work diligently to maintain peace among the other races, and the somewhat absurd spectacle of a Starlit diplomat gently negotiating with two hysterical Lesser ambassadors tends to crop up when two factions the Usukwinya have good relationships with threaten war on one another. (the Eluthrai find this patently ridiculous, and prefer to maintain peace with a judicious application of preventive violence. several Usukwinya-organized peace conferences have dissolved when the Eluthrai summarily shattered the offending governments without a note of forewarning.)
    56     56   
    57     57   their willingness to trade with or work for anyone and everyone mean that the Usukwinya are the main reason the Lesser Races have any ability to travel beyond the Great Web. however, Usuk astropaths are very selective: they will not use their powers to help their employers to commit acts of aggression, no matter how much you offer to pay them. many human captains chafe under the restrictions of their Tradebird astropaths, but short of relativistic travel, they have no other way to escape the confines of the Web.
    58     58   
    59     59   Usukwinya get along with everyone and make excellent diplomats, so long as they can restrain their urge to make a quick profit at the first available opportunity.
    60     60   
    61     61   #### Eluthrai
    62     62   (sg. Eluthra)
    63     63   
    64         -the greatest and most aloof of the living Starsouled races, the Eluthrai are a race of psionic warrior-poets. they are slim humanoids with subtly iridescent dark grey skin, lustrous white hair, red~violet eyes, and tapered, expressive ears. they are very few in number, with no more than ten thousand Eluthrai in the entire galaxy. it is popularly said, not without some reason, that the only reason the Eluthrai haven't conquered that entire galaxy is because they don't care to.
           64  +the greatest and most aloof of the living Starlit races, the Eluthrai are a race of psionic warrior-poets. they are slim humanoids with subtly iridescent dark grey skin, lustrous white hair, red~violet eyes, and tapered, expressive ears. they are very few in number, with no more than ten thousand Eluthrai in the entire galaxy. it is popularly said, not without some reason, that the only reason the Eluthrai haven't conquered that entire galaxy is because they don't care to.
    65     65   
    66         -natural immortals with a very low reproduction rate, the Eluthrai all have an exceptionally long-term worldview that frequently confounds mortal morals. little about them is known for certain, and they interact with the non-Starsouled very rarely, usually to deliver some form of unforeseen intervention that they typically refuse to explain.
           66  +natural immortals with a very low reproduction rate, the Eluthrai all have an exceptionally long-term worldview that frequently confounds mortal morals. little about them is known for certain, and they interact openly with the non-Starlit very rarely, usually to deliver some form of unforeseen intervention that they typically refuse to explain.
    67     67   
    68     68   the Eluthrai see themselves as the masters of the Thinking Few, and spare no expense in ensuring they maintain their position. to them, the Great Web is a garden, a place  to be tended carefully and protected from the storms outside. their civilization is dedicated to combatting extra-Web threats -- in particular, guarding against the possible return of the Forevanishers. they have cultivated a strong & highly spiritual warrior ethos in consequence
    69     69   
    70     70   within the Web itself, they mostly by clandestine means, using "Agents" selected from the Greater (and, occasionally, Lesser) Races to act on their behalf. in general they act directly only when overwhelming force is required, such as to exclude the Kuradoqshe, or to excise Suldibrand.
    71     71   
    72     72   it is known that the Eluthrai are of great intelligence: a 200pt IQ makes you a laughable simpleton in their eyes. it is estimated that the average individual has an IQ of 290, close to the theoretical maximum where organized intelligence dissolves into a sea of blinding psychosis. consequently, they are very conservative and cautious of new ideas; their culture emphasises skepticism and avoiding rash action.
    73     73   
................................................................................
    81     81   
    82     82   Eluthrai have two genders, and dramatic dimorphism. their women are much more intelligent than their men, and proportionately more prone to psychosis. traditionally most of their societies were matriarchal -- with the brains and psionic brawn to overpower the males, there was very little that could keep the Clan-Queens from exerting their will. the First Philosopher recognized however that the lesser intelligence of men was useful, due to their stabler psyches, and proposed patriarchy as part of his solution. this was made possible through a previously obscure psionic technique known as quelling -- with enough intimate exposure to the soul of another, it becomes possible to negate their psionics, even if that psion is stronger.
    83     83   
    84     84   among the modern Concordant Eluthrai, a female's mate is expected to be capable of quelling her psionics. female Eluthrai generally cooperate with the practice; it is difficult to learn to quell someone who actively tries to stymie you. it is widely understood, however, that the female sex will only cooperate so long as their men rule wisely: in a celebrated case on a far-flung world where the men began to take too many liberties, the women carefully organized to overpower one another's mates and instituted a compensatory subjugation of the local males for a proportionate period, which the Philosopher-King himself agreed was the just and proper punishment.
    85     85   
    86     86   Eluthran technology can be tidily summarized as "uncompromising." the Eluthrai demand excellence from their machines as much as one another, and will happily incur absurd expense to eliminate the smallest flaw. languishing for thousands of years of under such attentions, abetted by the most ferocious living intelligence to be found in the Reach, has created a technological ecosystem that is succeeded in its phenomenal capabilities only by its preposterous expense. an Eluthran computer requires about ten times the time and a hundred times the energy input to fabricate as does a conventional human computer, despite the vast gulf in manufacturing capabilities, but you can be [!damn] sure it'll still be working in ten million years' time.
    87     87   
    88         -they enjoy a post-scarcity economy that is the envy of even the other Starsouled.
           88  +they enjoy a post-scarcity economy that is the envy of even the other Starlit.
    89     89   
    90     90   very few of the Greater Races, and vanishingly few of the Lesser Races, have ever had the opportunity to visit an Eluthran world. they admit only their mysterious Agents and the occasional individual subjected to penal servitude for some great crime against the interests of "the Garden".
    91     91   
    92     92   while even their females are not nearly the psionic match of the Su'ikuri, they are nonetheless vastly powerful. their psionics are not as seamlessly integrated into their nervous system as in Usuk neurology, and deploying their power is consequently more effortful, requiring some concentration and intent, but they can bring far more energy to bear. where the Usukwinya have finesse and the Su'ikuri have brute power, the Eluthrai have technique: they can do things with their psionics that the other races never would have imagined possible.
    93     93   
    94     94   the Eluthrai put a great deal of effort into foremodeling the universe, seeking to predict future events and trends. their models are far from infallible, but reliable enough that some supersitious Lessers have come to believe that Eluthran psionics can be used to see the future. intelligence-gathering is in the modern era the prime industry of that exalted race, second only to warfare.
    95     95   
    96     96   ### Forevanished Ones
    97     97   
    98     98   #### Forevanishers
    99     99   a mysterious race or power thought to be responsible for exterminating a number of Forevanished Ones. no one knows for sure that they have themselves Vanished -- for all we know, they could be one of the contemporary Powers…
   100    100   
   101    101   #### firstborn
   102         -the architects of the Great Web, the Firstborn were the first civilization of which traces remain within the Reach. their psionic and scientific mastery, developed over ten million years of energetic industry, reached levels even the greatest of the modern Starsouled Races cannot hope to equal. while their Continuum Bridges form the backbone of the Lesser civilizations, little else of their manufacture seems to have survived into the present era.
          102  +the architects of the Great Web, the Firstborn were the first civilization of which traces remain within the Reach. their psionic and scientific mastery, developed over ten million years of energetic industry, reached heights even the greatest of the modern Starlit Races cannot hope to equal. while their Continuum Bridges form the backbone of the Lesser civilizations, little else of their manufacture seems to have survived into the present era.
   103    103   
   104    104   practically every trace of their existence that does remain is scored with weaponsfire.
   105    105   
   106    106   the artifact which tore an external channel into the player's soul in the backstory is of Firstborn design and uncertain purpose. Commune scholars had hitherto ascertained only that it was a machine seemingly able to produce psionic effects -- something that [!should] have been a contradiction in terms.
   107    107   
   108    108   ! possible plot: the Firstborn devised a means to produce psionic effects with carefully cultured neurons embedded in a mechanical matrix. essentially creating slaved psionic AI dedicated to a single purpose. while these rudimentary consciousnesses, barely fit to called souls, did not suffer, some other race or perhaps a faction among the Firstborn seems to have taken exception to the practice of trapping souls in metal, outside the thread of reincarnation, and exterminated the civilization to prevent its heresy.
   109         -! if this was a real proper AAA game the player would face some epic choice to release the secret and free the Lesser Races (or a subset of them) from the dominion of the Starsouled, turning psionics into a mere commodity; keeping the secret but placing her power at the disposal of the Commune (or Empire, in return for elevation to the ranks of nobility); or joining the Eluthrai as an honorary citizen in recompense for keeping the secret. alas, i don't have a budget.
          109  +! if this was a real proper AAA game the player would face some epic choice to release the secret and free the Lesser Races (or a subset of them) from the dominion of the Starlit, turning psionics into a mere commodity; keeping the secret but placing her power at the disposal of the Commune (or Empire, in return for elevation to the ranks of nobility); or joining the Eluthrai as an honorary citizen in recompense for keeping the secret. alas, i don't have a budget.
   110    110   
   111    111   ## psionics
   112    112   the ability of the soul to extend its will beyond the confines of its substrate. this power is technically defined as the presence of one or more external psionic channels in the structure of the soul. such a channel allows the soul to direct excess numina into other souls or into the numon field of the physical universe.
   113    113   
   114    114   the delicate interlink between soul and body relies on quantum phenomena, and only carbon-based life seems able to maintain such a link. silicon-based intelligence is at most a simulacrum of true thought.
   115    115   
   116    116   ### farspeakers

Modified src/sem.ct from [5017b1c243] to [770f4fca7d].

     1         -# starsoul semantics
            1  +# starlit semantics
     2      2   
     3      3   ## tool levels
     4      4   some items have a particular level requirement to enable digging. in general, level 0 should be used for things that can be dug by hand
     5      5   nanotech can dismantle anything up to level 2
     6      6   
     7      7   * sediment
     8      8   ** 0: sand, dirt (diggable with hand)

Modified src/sfx/conf.lua from [7fad9bea38] to [ee9864718b].

    16     16   
    17     17   local function envarg(n, default)
    18     18   	local v = os.getenv(n)
    19     19   	if v then return tonumber(v) end
    20     20   	return default
    21     21   end
    22     22   
    23         -local baseSeed = envarg('starsoul_sfx_vary', 420691917)
    24         -local variants = envarg('starsoul_sfx_variants', 4)
    25         -local fmt = os.getenv 'starsoul_sfx_fmt' or 'wav'
           23  +local baseSeed = envarg('starlit_sfx_vary', 420691917)
           24  +local variants = envarg('starlit_sfx_variants', 4)
           25  +local fmt = os.getenv 'starlit_sfx_fmt' or 'wav'
    26     26   
    27     27   local rules = {}
    28     28   local all = {}
    29     29   if fmt ~= 'ogg' then table.insert(rules,
    30         -	'out/starsoul-%.ogg: %.' .. fmt .. '\n' ..
           30  +	'out/starlit-%.ogg: %.' .. fmt .. '\n' ..
    31     31   		'\tffmpeg -y -i "$<" "$@"\n')
    32     32   end
    33     33   local function rule(out, file, seed)
    34     34   	if fmt == 'ogg' then
    35     35   		table.insert(rules, string.format(
    36         -			'out/starsoul-%s.ogg: %s.csd digital.orc physical.orc psi.orc dqual.inc\n' ..
           36  +			'out/starlit-%s.ogg: %s.csd digital.orc physical.orc psi.orc dqual.inc\n' ..
    37     37   			'\tcsound --omacro:seed=%d --format=ogg -o "$@" "$<"\n',
    38     38   			out, file,
    39     39   			seed
    40     40   		))
    41     41   	else
    42     42   		table.insert(rules, string.format(
    43     43   			'%s.%s: %s.csd digital.orc physical.orc psi.orc dqual.inc\n' ..
    44     44   			'\tcsound --omacro:seed=%d --format=%s -o "$@" "$<"\n',
    45     45   			out, fmt, file,
    46     46   			seed, fmt
    47     47   		))
    48     48   	end
    49         -	table.insert(all, string.format('out/starsoul-%s.ogg', out))
           49  +	table.insert(all, string.format('out/starlit-%s.ogg', out))
    50     50   end
    51     51   for _, v in ipairs(polyfx) do
    52     52   	local bn = v:match '^(.+).n.csd$'
    53     53   	for i=1, variants do
    54     54   		rule(bn .. '.' .. tostring(i), bn .. '.n', baseSeed + 4096*i)
    55     55   	end
    56     56   end

Added starlit.ct version [5320fb7a47].

            1  +# starlit
            2  +[*starlit] is a sci-fi survival game. you play the survivor of a disaster in space, stranded on a just-barely-habitable world with nothing but your escape pod, your survival suit, and some handy new psionic powers that the accident seems to have unlocked in you. scavenge resources, search alien ruins, and build a base where you can survive indefinitely, but be careful: winter approaches, and your starting suit heater is already struggling to keep you alive in the comparatively warm days of "summer".
            3  +
            4  +## story
            5  +### "Imperial Expat" background
            6  +about a month ago, you woke up to unexpected good news. your application to join the new Commune colony at Thousand Petal, submitted in a moment of utter existential weariness and almost in jest, was actually accepted. your skillset was a "perfect match" for the budding colony's needs, claimed the Population Control Authority, and you'd earned yourself a free trip to your new home -- on a swanky state transport, no less.
            7  +
            8  +it took a few discreet threats and bribes from Commune diplomats, but after a week of wrangling with the surly Crown Service for Comings & Goings -- whose bureaucrats seemed outright [!offended] that you actually managed to find a way off that hateful rock -- you secured grudging clearance to depart. you celebrated by telling your slackjawed boss exactly what you thought of him in a meeting room packed with his fellow parasites -- and left House Taladran with a new appreciation for the value of your labor, as the nobs found themselves desperately scrabbling for a replacement on short notice.
            9  +
           10  +you almost couldn't believe it when the Commune ship -- a sleek, solid piece of engineering whose graceful descent onto the landing pad seemed to sneer at the lurching rattletraps arrayed all around it -- actually showed up. in a daze you handed over your worldly possessions -- all three of them -- to a valet with impeccable manners, and climbed up out of the wagie nightmare into high orbit around your homeworld. the mercenary psion aboard, a preening Usukwinti with her very own luxury suite, tore a bleeding hole in the spacetime metric, and five hundred hopeful souls dove through towards fancied salvation. "sure," you thought to yourself as you slipped into your sleek new nanotech environment suit, itself worth more than the sum total of your earnings on Flame of Unyielding Purification, "life won't be easy -- but damn it, it'll [!mean] something out there."
           11  +
           12  +a free life on the wild frontier with a nation of comrades to have your back, with the best tech humans can make, fresh, clean water that isn't laced with compliance serum, and -- best of all -- never having to worry about paying rent again. it was too good to be true, you mused.
           13  +
           14  +clearly, the terrorists who blew up your ship agreed.
           15  +
           16  +you're still not certain what happened. all you know for sure is that transport was carrying more than just people. in those last hectic moments, you caught a glimpse of something -- maybe machine, maybe artwork, and [!definitely] ancient beyond measure. you've seen abhuman artifacts before in museums, of course; in fact, thanks to a childhood fascination, you can still name all the Elder Races and the Forevanished Ones off the top of your head.
           17  +
           18  +you have no [!idea] what that [!thing] was or who in the sublimated [!fuck] could possibly have made it.
           19  +
           20  +but one thing is for certain: your ship wasn't the only thing it ripped open when it blew. because when you woke up in your tiny escape pod beyond the furthest edge of the Reach, circling Farthest Shadow in a suicide orbit, you discovered yourself transformed into something impossible. a contradiction in terms.
           21  +
           22  +a human psion.
           23  +
           24  +for years beyond counting, the Starlit species -- three of whom yet live and deign once every so often to notice the Lesser Races -- have held the galaxy in sway through their monopoly on psionic power. of all the Thinking Few, only they are free to wander the distant stars at whim, heedless of the lightspeed barrier. there are no mechanisms for FTL travel or reactionless drive without that innate power, and, they assured us, psionic channels are fixed in the soul. your species either has the power or it doesn't.
           25  +
           26  +[!liars], all of them.
           27  +
           28  +are there other survivors? have they been similarly changed? what was that artifact and who were those terrorists? important questions, all, but they pale in comparison with the most important one:
           29  +
           30  +how the fuck are you going to survive the next 24 hours?
           31  +
           32  +## engine
           33  +starlit is developed against a bleeding-edge version of minetest. it definitely won't work with anything older than v5.7, and ideally you should build directly from master.
           34  +
           35  +starlit is best used with a patched version of minetest, though it is compatible with vanilla. the recommended patches are:
           36  +
           37  +* [>p11143 11143] - fix third-person view orientation
           38  +
           39  +	p11143: https://github.com/minetest/minetest/pull/11143.diff
           40  +
           41  +### shadows
           42  +i was delighted to see dynamic shadows land in minetest, and i hope the implementation will eventually mature. however, as it stands, there are severe issues with shadows that make them essentially incompatible with complex meshes like the Starlit player character meshes. for the sake of those who don't mind these glitches, Starlit does enable shadows, but i unfortunately have to recommend that you disable them until the minetest devs get their act together on this feature.
           43  +
           44  +## gameplay
           45  +starlit is somewhat unusual in how it uses the minetest engine. it's a voxel game but not of the minecraft variety.
           46  +
           47  +### controls
           48  +summon your Suit Interface by pressing the [*E] / [*Inventory] key. this will allow you to move items around in your inventory, but more importantly, it also allows you select or configure your Interaction Mode.
           49  +
           50  +the top three buttons can be used to select (or deactivate) an Interaction Mode. an Interaction Mode can be configured by pressing the button immediately below. the active Interaction Mode controls the behavior of the mouse buttons when no item is selected in the hotbar.
           51  +
           52  +the modes are:
           53  + * [*Fabrication]: use your suit's onboard nanotech to directly manipulate matter in the world.
           54  + ** [*Left Button] / [*Punch]: activate your primary nano program. by default this activates your nanoshredder, reducing the targeted object to monatomic powder and storing the resulting elements in your suit for use with the Matter Compiler
           55  + ** [*Right Button] / [*Place]: activate your secondary nano program. by default, if your suit compiler can generate sufficiently large objects, creates a block of the configured type directly in the world without having to build it by hand
           56  + * [*Psionics]: wield the awesome, if illicitly obtained, power of mind over matter
           57  + ** [*Left Button] / [*Punch]: perform your selected Primary Power
           58  + ** [*Right Button] / [*Place]: perform your selected Secondary Power
           59  + * [*Weapon]: military-grade suits have built-in hardpoints for specialized weapon systems that draw directly on your suit battery for power (and in the most exotic cases, your psi reserve)
           60  +  ** [*Left Button] / [*Punch]: fire your primary weapon
           61  +  ** [*Right Button] / [*Place]: fire your offhand weapon / summon your shield
           62  +
           63  +to use a tool, select it in the hotbar. even if an Interaction Mode is active, the tool will take priority. press [*Left Button] / [*Punch] to use the tool on a block; for instance, to break a stone with a jackhammer. to configure a tool or use its secondary functions, if any, press [*Right Button] / [*Place].
           64  +
           65  +hold [*Aux1] to activate your selected Maneuver. by default this is Sprint, which will consume stamina to allow you to run much faster. certain suits offer the Flight ability, which allows slow, mid-range flight. you can also unlock the psionic ability Lift, which allows very rapid flight but consumes psi at a prodigious rate.
           66  +
           67  +you can only have one Maneuver active at a time, whether this is a Psi Maneuver (consuming psi), a Suit Maneuver (consuming battery), or a Body Maneuver (consuming stamina). Maneuvers are activated in their respective panel.
           68  +
           69  +### psionics
           70  +there are four types of psionic abilities: Manual, Maneuver, Ritual, and Contextual.
           71  +
           72  +you can assign two Manual abilities at any given time and access them with the mouse buttons in Psionics mode.
           73  +
           74  +you can select a Psi Maneuver in the Psionics panel and activate it by holding [*Aux1].
           75  +
           76  +a Ritual is triggered directly from the psionics menu. as the name implies, these are complex, powerful abilities that require large amounts of Psi and time to meditate before they trigger, and any interruption will cancel the ability (though it will not restore any lost psi). the most famous Ritual is of course Conjoin Metric, which Starlit astropaths use in conjunction with powerful amplifiers to perform long-distance FTL jumps -- but without centuries of dedication to the art, the best you can hope for if you manage to learn this storied power is to move yourself a few kilometers.
           77  +
           78  +a Contextual ability is triggered in a specific situation, usually by interacting with a certain kind of object. Contextual abilities often require specialized equipment, to the point that many Starlit practitioners maintain their own Psionics Lab.

Deleted starsoul.ct version [81ada79a74].

     1         -# starsoul
     2         -[*starsoul] is a sci-fi survival game. you play the survivor of a disaster in space, stranded on a just-barely-habitable world with nothing but your escape pod, your survival suit, and some handy new psionic powers that the accident seems to have unlocked in you. scavenge resources, search alien ruins, and build a base where you can survive indefinitely, but be careful: winter approaches, and your starting suit heater is already struggling to keep you alive in the comparatively warm days of "summer".
     3         -
     4         -## story
     5         -about a month ago, you woke up to unexpected good news. your application to join the new Commune colony at Thousand Petal, submitted in a moment of utter existential weariness and almost in jest, was actually accepted. your skillset was a "perfect match" for the budding colony's needs, claimed the Population Control Authority, and you'd earned yourself a free trip to your new home -- on a swanky state transport, no less.
     6         -
     7         -it took a few discreet threats and bribes from Commune diplomats, but after a week of wrangling with the surly Crown Service for Comings & Goings -- whose bureaucrats seemed outright [!offended] that you actually managed to find a way off that hateful rock -- you secured grudging clearance to depart. you celebrated by telling your slackjawed boss exactly what you thought of him in a meeting room packed with his fellow parasites -- and left Circumsolar Megascale with a new appreciation for the value of your labor, as they found themselves desperately scrabbling for a replacement on short notice.
     8         -
     9         -you almost couldn't believe it when the Commune ship -- a sleek, solid piece of engineering whose graceful descent onto the landing pad seemed to sneer at the lurching rattletraps arrayed all around it -- actually showed up. in a daze you handed over your worldly possessions -- all three of them -- to a valet with impeccable manners, and climbed up out of the wagie nightmare into high orbit around your homeworld. the mercenary psion aboard, a preening Usukwinti with her very own luxury suite, tore a bleeding hole in the spacetime metric, and five hundred hopeful souls dove through towards fancied salvation. "sure," you thought to yourself as you slipped into your sleek new nanotech environment suit, itself worth more than the sum total of your earnings on Flame of Unyielding Purification, "life won't be easy -- but damn it, it'll [!mean] something out there."
    10         -
    11         -a free life on the wild frontier with a nation of comrades to have your back, with the best tech humans can make, fresh, clean water that isn't laced with compliance serum, and -- best of all -- never having to worry about paying rent again. it was too good to be true, you mused.
    12         -
    13         -clearly, the terrorists who blew up your ship agreed.
    14         -
    15         -you're still not certain what happened. all you know for sure is that transport was carrying more than just people. in those last hectic moments, you caught a glimpse of something -- maybe machine, maybe artwork, and [!definitely] ancient beyond measure. you've seen artifacts before in museums, of course; in fact, thanks to a childhood fascination, you can still name all the Elder Races and the Forevanished Ones off the top of your head.
    16         -
    17         -you have no [!idea] what that [!thing] was or who in the sublimated [!fuck] could possibly have made it.
    18         -
    19         -but one thing is for certain: your ship wasn't the only thing it ripped open when it blew. because when you woke up in your tiny escape pod beyond the furthest edge of the Reach, circling Farthest Shadow in a suicide orbit, you discovered yourself transformed into something impossible. a contradiction in terms.
    20         -
    21         -a human psionic.
    22         -
    23         -for years beyond counting, the Starsouled species -- three of whom yet live and deign once every so often to notice the Lesser Races -- have held the galaxy in sway through their monopoly on psionic power. of all the Thinking Few, only they are free to wander the distant stars at whim, heedless of the lightspeed barrier. there are no mechanisms for FTL travel or reactionless drive without that innate power, and, they assured us, psionic channels are fixed in the soul. your species either has the power or it doesn't.
    24         -
    25         -[!liars], all of them.
    26         -
    27         -are there other survivors? have they been similarly changed? what was that artifact and who were those terrorists? important questions, all, but they pale in comparison with the most important one:
    28         -
    29         -how the fuck are you going to survive the next 24 hours?
    30         -
    31         -## engine
    32         -starsoul is developed against a bleeding-edge version of minetest. it definitely won't work with anything older than v5.7, and ideally you should build directly from master.
    33         -
    34         -starsoul is best used with a patched version of minetest, though it is compatible with vanilla. the recommended patches are:
    35         -
    36         -* [>p11143 11143] - fix third-person view orientation
    37         -
    38         -	p11143: https://github.com/minetest/minetest/pull/11143.diff
    39         -
    40         -### shadows
    41         -i was delighted to see dynamic shadows land in minetest, and i hope the implementation will eventually mature. however, as it stands, there are severe issues with shadows that make them essentially incompatible with complex meshes like the Starsouled player character meshes. for the sake of those who don't mind these glitches, Starsoul does enable shadows, but i unfortunately have to recommend that you disable them until the minetest devs get their act together on this feature.
    42         -
    43         -## gameplay
    44         -starsoul is somewhat unusual in how it uses the minetest engine. it's a voxel game but not of the minecraft variety.
    45         -
    46         -### controls
    47         -summon your Suit Interface by pressing the [*E] / [*Inventory] key. this will allow you to move items around in your inventory, but more importantly, it also allows you select or configure your Interaction Mode.
    48         -
    49         -the top three buttons can be used to select (or deactivate) an Interaction Mode. an Interaction Mode can be configured by pressing the button immediately below. the active Interaction Mode controls the behavior of the mouse buttons when no item is selected in the hotbar.
    50         -
    51         -the modes are:
    52         - * [*Fabrication]: use your suit's onboard nanotech to directly manipulate matter in the world.
    53         - ** [*Left Button] / [*Punch]: activate your primary nano program. by default this activates your nanoshredder, reducing the targeted object to monatomic powder and storing the resulting elements in your suit for use with the Matter Compiler
    54         - ** [*Right Button] / [*Place]: activate your secondary nano program. by default, if your suit compiler can generate sufficiently large objects, creates a block of the configured type directly in the world without having to build it by hand
    55         - * [*Psionics]: wield the awesome, if illicitly obtained, power of mind over matter
    56         - ** [*Left Button] / [*Punch]: perform your selected Primary Power
    57         - ** [*Right Button] / [*Place]: perform your selected Secondary Power
    58         - * [*Weapon]: military-grade suits have built-in hardpoints for specialized weapon systems that draw directly on your suit battery for power (and in the most exotic cases, your psi reserve)
    59         -  ** [*Left Button] / [*Punch]: fire your primary weapon
    60         -  ** [*Right Button] / [*Place]: fire your offhand weapon / summon your shield
    61         -
    62         -to use a tool, select it in the hotbar. even if an Interaction Mode is active, the tool will take priority. press [*Left Button] / [*Punch] to use the tool on a block; for instance, to break a stone with a jackhammer. to configure a tool or use its secondary functions, if any, press [*Right Button] / [*Place].
    63         -
    64         -hold [*Aux1] to activate your selected Maneuver. by default this is Sprint, which will consume stamina to allow you to run much faster. certain suits offer the Flight ability, which allows slow, mid-range flight. you can also unlock the psionic ability Lift, which allows very rapid flight but consumes psi at a prodigious rate.
    65         -
    66         -you can only have one Maneuver active at a time, whether this is a Psi Maneuver (consuming psi), a Suit Maneuver (consuming battery), or a Body Maneuver (consuming stamina). Maneuvers are activated in their respective panel.
    67         -
    68         -### psionics
    69         -there are four types of psionic abilities: Manual, Maneuver, Ritual, and Contextual.
    70         -
    71         -you can assign two Manual abilities at any given time and access them with the mouse buttons in Psionics mode.
    72         -
    73         -you can select a Psi Maneuver in the Psionics panel and activate it by holding [*Aux1].
    74         -
    75         -a Ritual is triggered directly from the psionics menu. as the name implies, these are complex, powerful abilities that require large amounts of Psi and time to meditate before they trigger, and any interruption will cancel the ability (though it will not restore any lost psi). the most famous Ritual is of course Conjoin Metric, which Starsouled astropaths use in conjunction with powerful amplifiers to perform long-distance FTL jumps -- but without centuries of dedication to the art, the best you can hope for if you manage to learn this storied power is to move yourself a few kilometers.
    76         -
    77         -a Contextual ability is triggered in a specific situation, usually by interacting with a certain kind of object. Contextual abilities often require specialized equipment, to the point that many Starsouled practitioners maintain their own Psionics Lab.