starlit  Artifact [e3ed80cb5c]

Artifact e3ed80cb5c8e9bc5a21699f3ca7fd613fa577577a6cf961280bc72870c513b70:


local lib = starlit.mod.lib

function starlit.ui.setupForUser(user)
	local function cmode(mode)
		if user.actMode == mode then return {hue = 150, sat = 0, lum = .3} end
	end
	user.entity:set_inventory_formspec(starlit.ui.build {
		kind = 'vert', mode = 'sw';
		padding = .5, spacing = 0.1;
		{kind = 'hztl';
			{kind = 'contact', w=1.5,h=1.5, id = 'mode_nano',
				img='starlit-ui-icon-nano.png', close=true, color = cmode'nano'};
			{kind = 'contact', w=1.5,h=1.5, id = 'mode_weapon',
				img='starlit-ui-icon-weapon.png', close=true, color = cmode'weapon'};
			{kind = 'contact', w=1.5,h=1.5, id = 'mode_psi',
				img='starlit-ui-icon-psi.png', close=true, color = cmode'psi'};
		};
		{kind = 'hztl';
			{kind = 'contact', w=1.5,h=1.5, id = 'open_elements',
				img='starlit-ui-icon-element.png'};
			{kind = 'contact', w=1.5,h=1.5, id = 'open_suit',
				img='starlit-item-suit.png^[hsl:200:-.7:0'};
			{kind = 'contact', w=1.5,h=1.5, id = 'open_psi',
				img='starlit-ui-icon-psi-cfg.png'};
			{kind = 'contact', w=1.5,h=1.5, id = 'open_body',
				img='starlit-ui-icon-self.png'};
		};
		{kind = 'list';
			target = 'current_player', inv = 'main';
			w = 6, h = 1, spacing = 0.1;
		};
	})
end

function starlit.ui.userMenuDispatch(user, fields)
	local function setSuitMode(mode)
		if user.actMode == mode then
			user:actModeSet 'off'
		else
			user:actModeSet(mode)
		end
	end

	local modes = { nano = true, psi = false, weapon = true }
	for e,s in pairs(modes) do
		if fields['mode_' .. e] then
			if s and (user:naked() or user:getSuit():powerState() == 'off') then
				user:suitSound 'starlit-error'
			else
				setSuitMode(e)
			end
			return true
		end
	end

	if fields.open_elements then
		user:openUI('starlit:user-menu', 'compiler')
		return true
	elseif fields.open_psi then
		user:openUI('starlit:user-menu', 'psi')
		return true
	elseif fields.open_suit then
		if not user:naked() then
			user:openUI('starlit:user-menu', 'suit')
		end
		return true
	elseif fields.open_body then
		user:openUI('starlit:user-menu', 'body')
	end
	return false
end

local function listWrap(n, max)
	local h = math.ceil(n / max)
	local w = math.min(max, n)
	return w, h
end

local function wrapMenu(w, h, rh, max, l)
	local root = {kind = 'vert', w=w, h=h}
	local bar
	local function flush()
		if bar and bar[1] then table.insert(root, bar) end
		bar = {kind = 'hztl'}
	end
	flush()

	for _, i in ipairs(l) do
		local bw = w/max
		if i.cfg then w = w - rh end

		table.insert(bar, {
			kind = i.img and 'contact' or 'button', close = i.close;
			color = i.color;
			fg = i.fg;
			label = i.label;
			img = i.img;
			id = i.id;
			w = bw, h = rh;
		})
		if i.cfg then 
			table.insert(bar, {
				kind = 'button';
				color = i.color;
				fg = i.fg;
				label = "CFG";
				img = i.img;
				id = i.id .. '_cfg';
				w = rh, h = rh;
			})
		end

		if bar[max] then flush() end
	end
	flush()
	
	return root
end

local function abilityMenu(a)
	-- select primary/secondary abilities or activate ritual abilities
	local p = {kind = 'vert'}
	for _, o in ipairs(a.order) do
		local m = a.menu[o]
		table.insert(p, {kind='hbar', fac=0.5, text=string.format("<b>%s</b>",m.label), w=a.w, h = .5})
		table.insert(p, wrapMenu(a.w, a.h, 1.2, 2, m.opts))
	end
	return p
end

local function pptrMatch(a,b)
	if a == nil or b == nil then return false end
	return (a.chipID ~= nil and (a.chipID == b.chipID and a.pgmIndex == b.pgmIndex))
       or (a.ref ~= nil and a.ref == b.ref)
end

starlit.interface.install(starlit.type.ui {
	id = 'starlit:user-menu';
	pages = {
		compiler = {
			setupState = function(state, user)
				-- nanotech/suit software menu
				local chips = user.entity:get_inventory():get_list 'starlit_suit_chips' -- FIXME need better subinv api
				local sw = starlit.mod.electronics.chip.usableSoftware(chips)
				state.suitSW = {}
				local dedup = {}
				for i, r in ipairs(sw) do if
					r.sw.kind      == 'suitPower'
				then
					if not dedup[r.sw] then
						dedup[r.sw] = true
						table.insert(state.suitSW, r)
					end
				end end
			end;
			handle = function(state, user, act)
				if user:getSuit():powerState() == 'off' then return false end
				local pgm, cfg
				for k in next, act do
					local id, mode = k:match('^suit_pgm_([0-9]+)_(.*)$')
					if id then
						id = tonumber(id)
						if state.suitSW[id] then
							pgm = state.suitSW[id]
							cfg = mode == '_cfg'
							break
						end
					end
				end
				if not pgm then return false end -- HAX

				-- kind=active programs must be assigned to a command slot
				-- kind=direct programs must open their UI
				-- kind=passive programs must toggle on and off
				if pgm.sw.powerKind == 'active' then
					if cfg then
						user:openUI(pgm.sw.ui, 'index', {
							context = 'suit';
							program = pgm;
						})
						return false
					end
					local ptr = {chipID = starlit.mod.electronics.chip.read(pgm.chip).uuid, pgmIndex = pgm.fd.inode}
					local pnan = user.power.nano
					if pnan.primary == nil then
						pnan.primary = ptr
					elseif pptrMatch(ptr, pnan.primary) then
						pnan.primary = nil
					elseif pptrMatch(ptr, pnan.secondary) then
						pnan.secondary = nil
					else
						pnan.secondary = ptr
					end
					user:suitSound 'starlit-configure'
				elseif pgm.sw.powerKind == 'direct' then
					local ctx = {
						context = 'suit';
						program = pgm;
					}
					if pgm.sw.ui then
						user:openUI(pgm.sw.ui, 'index', ctx)
						return false
					else
						pgm.sw.run(user, ctx)
					end
				elseif pgm.sw.powerKind == 'passive' then
					if cfg then
						user:openUI(pgm.sw.ui, 'index', {
							context = 'suit';
							program = pgm;
						})
						return false
					end

					local addDisableRec = true
					for i, e in ipairs(pgm.file.body.conf) do
						if e.key == 'disable' and e.value == 'yes' then
							addDisableRec = false
							table.remove(pgm.file.body.conf, i)
							break
						elseif e.key == 'disable' and e.value == 'no' then
							e.value = 'yes'
							addDisableRec = false
							break
						end
					end
					if addDisableRec then
						table.insert(pgm.file.body.conf, {key='disable',value='yes'})
					end
					-- update the chip *wince*
					pgm.fd:write(pgm.file)
					user.entity:get_inventory():set_stack('starlit_suit_chips',
					pgm.chipSlot, pgm.chip)
					user:reconfigureSuit()
					user:suitSound 'starlit-configure'

				end
				return true, true
			end;
			render = function(state, user)
				local suit = user:getSuit()
				local swm
				if user:getSuit():powerState() ~= 'off' then
					swm = {
						w = 8, h = 3;
						order = {'active','ritual','pasv'};
						menu = {
							active = {
								label = 'Nanoware';
								opts = {};
							};
							ritual = {
								label = 'Programs';
								opts = {};
							};
							pasv = {
								label = 'Passive';
								opts = {};
							};
						};
					}
					for id, r in pairs(state.suitSW) do
						local color = {hue=300,sat=0,lum=0}
						local fg = nil
						local close = nil
						local tbl, cfg if r.sw.powerKind == 'active' then
							tbl = swm.menu.active.opts
							if r.sw.ui then cfg = true end
							local pnan = user.power.nano
							if pnan then
								local ptr = {chipID = starlit.mod.electronics.chip.read(r.chip).uuid, pgmIndex = r.fd.inode}
								if pptrMatch(ptr, pnan.primary) then
									color.lum = 1
								elseif pptrMatch(ptr, pnan.secondary) then
									color.lum = 0.8
								end
							end
						elseif r.sw.powerKind == 'direct' then
							tbl = swm.menu.ritual.opts
							if not r.sw.ui then
								close = true
							end
						elseif r.sw.powerKind == 'passive' then
							tbl = swm.menu.pasv.opts
							if r.sw.ui then cfg = true end
							for i, e in ipairs(r.file.body.conf) do
								if e.key == 'disable' and e.value == 'yes' then
									color.lum = -.2
									fg = lib.color {hue=color.hue,sat=0.7,lum=0.7}
									break
								end
							end
						end
						if tbl then table.insert(tbl, {
							color = color, fg = fg;
							label = r.sw.label or r.sw.name;
							id = string.format('suit_pgm_%s_', id);
							cfg = cfg, close = close;
						}) end
					end
				end
				local menu = { kind = 'vert', mode = 'sw', padding = 0.5 }
				if swm then table.insert(menu, abilityMenu(swm)) end

				local inv = user.entity:get_inventory()
				--[[
				local cans = inv:get_list 'starlit_suit_canisters'
				if cans and next(cans) then for i, st in ipairs(cans) do
					local id = string.format('starlit_canister_%u_elem', i)
					local esz = inv:get_size(id)
					if esz > 0 then
						local eltW, eltH = listWrap(esz, 5)
						table.insert(menu, {kind = 'hztl',
							{kind = 'img', desc='Elements', img = 'starlit-ui-icon-element.png', w=1,h=1};
							{kind = 'list', target = 'current_player', inv = id,
								listContent = 'element', w = eltW, h = eltH, spacing = 0.1};
						})
					end
				end end
				]]

				if #menu == 0 then
					table.insert(menu, {
						kind = 'img';
						img = 'starlit-ui-alert.png';
						w=2, h=2;
					})
					menu.padding = 1;
				end
				return starlit.ui.build(menu)
			end;
		};
		compilerListRecipes = {
		};
		psi = {
			render = function(state, user)
				return starlit.ui.build {
					kind = 'vert', mode = 'sw';
					padding = 0.5;
				}
			end;
		};
		body = {
			render = function(state, user)
				local barh = .75
				local tb = {
					kind = 'vert', mode = 'sw';
					padding = 0.5, 
					{kind = 'hztl', padding = 0.25;
						{kind = 'label', text = 'Name', w = 2, h = barh};
						{kind = 'label', text = user.persona.name, w = 4, h = barh}};
				}
				local statBars = {'nutrition', 'hydration', 'fatigue', 'morale', 'irradiation', 'illness'}
				for idx, id in ipairs(statBars) do
					local s = starlit.world.stats[id]
					local amt, sv = user:effectiveStat(id)
					local min, max = starlit.world.species.statRange(user.persona.species, user.persona.speciesVariant, id)
					local st = string.format('%s / %s', s.desc(amt, true), s.desc(max))
					table.insert(tb, {kind = 'hztl', padding = 0.25;
						{kind = 'label', w=2, h=barh, text = lib.str.capitalize(s.name)};
						{kind = 'hbar',  w=4, h=barh, fac = sv, text = st, color=s.color};
					})
				end
				local abilities = {
					maneuver = {};
					direct = {};
					passive = {};
				}
				state.abilityMap = {}
				for i, a in pairs(user:species().abilities) do
					local id = 'abl_'..a.id;
					state.abilityMap[id] = a;
					table.insert(abilities[a.powerKind], {
						id = id;
						label = a.name;
						desc = a.desc;
-- 						img = a.img;

						-- HACK
						color = pptrMatch(user.power.maneuver, {ref=a}) and
							{hue = 150, sat = 0, lum = .3} or nil;
					});
				end
				for i, n in ipairs {'maneuver', 'direct', 'passive'} do
					if next(abilities[n]) then
						table.insert(tb, wrapMenu(6.25,4, 1,2, abilities[n]))
					end
				end
				return starlit.ui.build(tb)
			end;
			handle = function(state, user, q)
				for k,a in pairs(state.abilityMap) do
					if q[k] then
						if a.powerKind == 'maneuver' then
							if pptrMatch(user.power.maneuver, {ref=a}) then
								user.power.maneuver = nil
							else
								user.power.maneuver = {ref=a}
							end
							user:suitSound 'starlit-configure'
							return true
						elseif a.powerKind == 'direct' then
						elseif a.powerKind == 'passive' then
						else error('bad ability kind ' .. a.powerKind) end
						break
					end
				end
			end;
		};
		suit = {
			render = function(state, user)
				local suit = user:getSuit()
				local suitDef = suit:def()
				local chipW, chipH = listWrap(suitDef.slots.chips, 5)
				local batW, batH = listWrap(suitDef.slots.batteries, 5)
				local canW, canH = listWrap(suitDef.slots.canisters, 5)
				local suitMode = suit:powerState()
				local function modeColor(mode)
					if mode == suitMode then return {hue = 180, sat = 0, lum = .5} end
				end
				return starlit.ui.build {
					kind = 'vert', mode = 'sw';
					padding = 0.5, spacing = 0.1;
					{kind = 'hztl',
						{kind = 'img', desc='Batteries', img = 'starlit-item-battery.png', w=1,h=1};
						{kind = 'list', target = 'current_player', inv = 'starlit_suit_bat',
							listContent = 'power', w = batW, h = batH, spacing = 0.1};
					};
					{kind = 'hztl',
						{kind = 'img', desc='Chips', img = 'starlit-item-chip.png', w=1,h=1};
						{kind = 'list', target = 'current_player', inv = 'starlit_suit_chips',
							listContent = 'chip', w = chipW, h = chipH, spacing = 0.1};
					};
					{kind = 'hztl',
						{kind = 'img', desc='Canisters', img = 'starlit-item-element-canister.png', w=1,h=1};
						{kind = 'list', target = 'current_player', inv = 'starlit_suit_canisters',
							listContent = nil, w = canW, h = canH, spacing = 0.1};
					};
					{kind = 'hztl';
						{kind = 'img', w=1,h=1, item = suit.item:get_name(),
							desc = suit.item:get_definition().short_description};
						{kind = 'button', w=1.5,h=1, id = 'powerMode_off', label = 'Off';
							color=modeColor'off'};
						{kind = 'button', w=2.5,h=1, id = 'powerMode_save', label = 'Power Save';
							color=modeColor'powerSave'};
						{kind = 'button', w=1.5,h=1, id = 'powerMode_on', label = 'On'; 
							color=modeColor'on'};
					};
					{kind = 'list', target = 'current_player', inv = 'main', w = 6, h = 1, spacing = 0.1};
				}
			end;
			handle = function(state, user, q)
				local suitMode
				if     q.powerMode_off  then suitMode = 'off'
				elseif q.powerMode_save then suitMode = 'powerSave'
				elseif q.powerMode_on   then suitMode = 'on' end
				if suitMode then
					user:suitPowerStateSet(suitMode)
					return true
				end
			end;
		};
	};
})

-- TODO destroy suit interfaces when power runs out or suit/chip is otherwise disabled
starlit.interface.install(starlit.type.ui {
	id = 'starlit:compile-matter-component';
	sub = {
		suit = function(state, user, evt)
			if evt.kind == 'disrobe' then state:close()
			elseif evt.kind == 'power' and evt.mode == 'off' then state:close() end
		end;
		playerInventory = function(state,user)
			-- refresh
		end;
	};
	pages = {
		index = {
			setupState = function(state, user, ctx)
				state.pgm = ctx.program
				state.select = {}
				local E = starlit.mod.electronics
				if ctx.context == 'suit' then
					state.fetch = function()
						local cst = user.entity:get_inventory():get_list 'starlit_suit_chips'
						local cl = {order={}, map={}}
						for i, c in ipairs(cst) do
							if not c:is_empty() then
								local d = E.chip.read(c)
								local co = {
									stack = c;
									data = d;
								}
								table.insert(cl.order, co)
								cl.map[d.uuid] = co
							end
						end
						if state.select.chip and not cl.map[state.select.chip.data.uuid] then
							-- chip no longer available
							user:suitSound 'starlit-error'
							state.select = {}
						end
						state.select.chips = cl

						state.select.scms = {}
						if state.select.chip then
							state.select.scms = E.chip.usableSoftware({state.select.chip.stack},nil,
								function(s) return s.sw.kind == 'schematic' end)
						end
					end
				end
			end;

			onClose = function(state, user)
				user:suitSound 'starlit-quit'
			end;
			handle = function(state, user, q)
				local sel = state.select
				state.fetch()
				local chips = state.select.chips
				local function chirp()
					user:suitSound 'starlit-nav'
				end
				local function onPickChip(chip)
					chirp()
					sel.chip = chip
					return true
				end
				local function onPickScm(scm)
					chirp()
					sel.scm = scm
					return true
				end

				if sel.chip == nil then
					for k in next, q do
						local id = k:match "^chip_(%d+)$"
						if id then
							local cm = chips.map[tonumber(id)]
							if cm then return onPickChip(cm) end
						end
					end
				elseif sel.scm == nil then
					if q.back then chirp() sel.chip = nil return true end
					for k in next, q do
						local id = k:match "^scm_(%d+)$"
						if id then
							local cm = state.select.scms[tonumber(id)]
							if cm then return onPickScm(cm) end
						end
					end
				else
					if q.back then chirp() sel.scm = nil return true end
				end
			end;

			render = function(state, user)
				local sel, pgmSelector = state.select, {}
				state.fetch()

				local function pushSelector(id, item, label, desc, req)
					local rh = .5
					local label = {kind = 'text', w = 10-1.5, h=1.5;
							text = '<global valign=middle>'..label }
					if req then
						label.h = label.h - rh - .2

						local imgs = {}
						for ci,c in ipairs(req) do
							for ei, e in ipairs(c.list) do
								table.insert(imgs, {kind = 'img', w=rh, h=rh,  img=e.img})
							end
						end
						label = {kind = 'vert', w = 10-1.5, h=1.5;
							label;
							{kind ='hztl', w=10-1.5, h=rh; unpack(imgs); }
						}
					end
					table.insert(pgmSelector, {kind = 'hztl', w=10,h=1.5;
						{kind = 'contact', id=id, w=1.5, h=1.5;
							item = item;
							color = {hue=220, sat=0, lum=0};
							desc = desc;
						};
						label;
					})
				end

				local back = {kind = 'button', id='back', label = '<- Back', w=10,h=1.2}
				if sel.chips == nil then
					table.insert(pgmSelector, {kind = 'img', img = 'starlit-ui-alert.png', w=2, h=2})
				elseif sel.chip == nil then
					for i, c in ipairs(sel.chips.order) do
					-- TODO filter out chips without schematics?
						pushSelector('chip_' .. c.data.uuid, c.stack, c.data.label)
					end
				else
					if sel.scm == nil then
						for idx, ent in ipairs(sel.scms) do
							local fab = ItemStack(ent.sw.output):get_definition()._starlit.fab
							if fab.flag.print then
								local req = fab:visualize()
								pushSelector('scm_' .. idx, ent.sw.output, ent.sw.name, nil, req)
							end
						end
						table.insert(pgmSelector, back)
					else
						local output = ItemStack(sel.scm.sw.output):get_definition()
						local fab = output._starlit.fab
						local sw = sel.scm.sw
						table.insert(pgmSelector, {kind = 'hztl', w=10, h=1.2;
							{kind = 'img', item = sw.output, w=1.2, h=1.2, desc=output.description};
							{kind = 'text', text = string.format('<global valign=middle><b>%s</b>', sw.name), w=10-1.2,h=1.2};
						})
						local inputTbl = {kind = 'vert', w=5,h=0;
							{kind = 'hbar', w=5, h=.5, text='Input'}};
						local costTbl = {kind = 'vert', w=5,h=0; spacing=.25;
							{kind = 'hbar', w=5, h=.5, text='Process'}};
						local reqPane = {kind = 'pane', id='reqPane', w=10, h=7;
							{kind = 'hztl', w=10,h=0; inputTbl, costTbl}
						}
						local req = fab:visualize()
						for ci,c in ipairs(req) do
							table.insert(inputTbl, {kind = 'label', w=4.5, h=1, x=.5;
								text=lib.str.capitalize(c.header)});
							for ei,e in ipairs(c.list) do
								table.insert(inputTbl, {kind = 'hztl', w=4, h=.5, x=1;
									{kind='img',   w=.5,h=.5, img=e.img};
									{kind='label', w=3.3,h=.5,x=.2, text=e.label};
								});
							end
						end
						if sw.cost then
							local function pushCost(t, val)
								table.insert(costTbl, {kind='text', w=4.5,h=.5,x=.5;
									text=string.format('<b>%s</b>: %s',t,val);
								})
							end
							if sw.cost.cycles then
								pushCost('Energy', lib.math.siUI('J', sel.scm.powerCost))
								pushCost('Compute', lib.math.siUI({'cycle','cycles'}, sw.cost.cycles, true))
							end
						end
						table.insert(pgmSelector, reqPane)
						table.insert(pgmSelector, {kind = 'hztl', w=10,h=1.2;
							{kind = 'button', id='back', label = '<- Back', w=5,h=1.2};
							{kind = 'button', id='print', label = 'Print ->', w=5,h=1.2, color={hue=120,sat=0,lum=0}};
						})
					end
				end

				return starlit.ui.build {
					kind = 'hztl', padding = 0.5; w = 20, h = 10, mode = 'sw';
					{kind = 'vert', w = 5, h = 5;
						{kind = 'hbar', fac=0, w = 5, h = .5, text = '<b><left>Recent Prints</left></b>'};
					};
					{kind = 'vert', w = 10, h = 10;
						{kind = 'hbar', fac=0, w = 10, h = .5, text = '<b>Program Select</b>'};
						{kind = 'pane', w = 10, h = 9.5, id='pgmSelect';
							unpack(pgmSelector)
						};
					};
					{kind = 'vert', w = 5, h = 10;
						{kind = 'hbar', fac=0, w = 5, h = .5, text = '<b><right>Print Queue</right></b>'};
					};
				}
			end;
		};
	};
})