-- [ʞ] user.lua
-- ~ lexi hale <lexi@hale.su>
-- © EUPL v1.2
-- ? defines the starlit.type.user class, which is
-- the main interface between the game world and the
-- client. it provides for initial signup and join,
-- managing the HUD, skinning the player model,
-- effecting weather changes, etc.
local lib = starlit.mod.lib
local function hudAdjustBacklight(img)
local night = math.abs(minetest.get_timeofday() - .5) * 2
local opacity = night*0.8
return img:fade(opacity)
end
local userStore = lib.marshal.metaStore {
persona = {
key = 'starlit:persona';
type = starlit.store.persona;
};
}
local suitStore = starlit.store.suitMeta
local leds = {
freeze = {
icon = lib.image('starlit-ui-alert-temp-cold.png');
bg = lib.image('starlit-ui-alert-bg-temp-cold.png');
side = 'left';
};
overheat = {
icon = lib.image('starlit-ui-alert-temp-hot.png');
bg = lib.image('starlit-ui-alert-bg-temp-hot.png');
side = 'left';
};
hydration = {
icon = lib.image('starlit-ui-alert-hydration.png');
bg = lib.image('starlit-ui-alert-bg-hydration.png');
side = 'left';
};
nutrition = {
icon = lib.image('starlit-ui-alert-nutrition.png');
bg = lib.image('starlit-ui-alert-bg-nutrition.png');
side = 'left';
};
radiation = {
icon = lib.image('starlit-ui-alert-rad.png');
bg = lib.image('starlit-ui-alert-bg-rad.png');
side = 'right';
};
fatigue = {
icon = lib.image('starlit-ui-alert-fatigue.png');
bg = lib.image('starlit-ui-alert-bg-fatigue.png');
side = 'right';
};
item = {
icon = lib.image('starlit-ui-alert-item.png');
bg = lib.image('starlit-ui-alert-bg-success.png');
side = 'right';
};
}
starlit.type.user = lib.class {
name = 'starlit:user';
leds = leds;
construct = function(ident)
local name, luser
if type(ident) == 'string' then
name = ident
luser = minetest.get_player_by_name(name)
else
luser = ident
name = luser:get_player_name()
end
return {
entity = luser;
name = name;
hud = {
elt = {};
bar = {};
alarm = {};
led = { left={}, right={}, map={} };
};
tree = {};
action = {
bits = 0; -- for control deltas
prog = {}; -- for recording action progress on a node; reset on refocus
tgt = {type='nothing'};
sfx = {};
fx = {};
};
actMode = 'off';
power = {
nano = {primary = nil, secondary = nil};
weapon = {primary = nil, secondary = nil};
psi = {primary = nil, secondary = nil};
maneuver = nil;
};
pref = {
calendar = 'commune';
};
overlays = {};
cooldownTimes = {
stamina = 0;
alarm = 0;
};
}
end;
__index = {
--------------
-- overlays --
--------------
updateOverlays = function(self)
-- minetest: because fuck you, that's why
local engineGravity = starlit.constant.phys.engineGravity
local targetGravity = starlit.world.planet.gravity
local phys = {
speed = self.pheno:trait('speed',1);
jump = self.pheno:trait('jump',1);
gravity = targetGravity / engineGravity;
speed_climb = 1;
speed_crouch = 1;
speed_walk = 1;
acceleration_default = 1;
acceleration_air = 1;
}
for i, o in ipairs(self.overlays) do o(phys) end
self.entity:set_physics_override(phys)
end;
overlay = function(self, o)
local id = #self.overlays+1
self.overlays[id] = o
self:updateOverlays()
return id
end;
deleteOverlay = function(self, id)
table.remove(self.overlays, id)
self:updateOverlays()
end;
--------------
-- personae --
--------------
pullPersona = function(self)
-- if later records are added in public updates, extend this function to merge them
-- into one object
local s = userStore(self.entity)
self.persona = s.read 'persona'
self.pheno = starlit.world.species.pheno(self.persona.species, self.persona.speciesVariant)
end;
pushPersona = function(self)
local s = userStore(self.entity)
s.write('persona', self.persona)
end;
uiColor = function(self) return lib.color {hue=238,sat=.5,lum=.5} end;
-----------
-- stats --
-----------
statDelta = function(self, stat, d, cause, abs)
if self.entity:get_hp() == 0 then return end
local dt = self.persona.statDeltas
local min, max, base = self:statRange(stat)
if abs then
if d == true then d = max
elseif d == false then d = min end
end
if stat == 'health' then
self.entity:set_hp(abs and d or (self.entity:get_hp() + d), cause)
elseif stat == 'breath' then
self.entity:set_breath(abs and d or (self.entity:get_breath() + d))
else
if abs then
dt[stat] = d - base
else
dt[stat] = dt[stat] + d
end
if dt[stat]+base > max then dt[stat] = max-base
elseif dt[stat]+base < min then dt[stat] = min-base end
self:pushPersona()
end
local sb = self.hud.bar[stat]
if sb then sb:update() end
-- self:updateHUD()
-- TODO trigger relevant animations?
end;
statRange = function(self, stat) --> min, max, base
return starlit.world.species.statRange(
self.persona.species, self.persona.speciesVariant, stat)
end;
effectiveStat = function(self, stat)
local val
local min, max, base = self:statRange(stat)
if stat == 'health' then
val = self.entity:get_hp()
elseif stat == 'breath' then
val = self.entity:get_breath()
else
val = base + self.persona.statDeltas[stat] or 0
end
local d = max - min
return val, (val - min) / d
end;
---------------
-- phenotype --
---------------
lookupSpecies = function(self)
return starlit.world.species.lookup(self.persona.species, self.persona.speciesVariant)
end;
phenoTrait = function(self, trait, dflt)
-- local s,v = self:lookupSpecies()
-- return v.traits[trait] or s.traits[trait] or 0
return self.pheno:trait(trait, dflt)
end;
damageModifier = function(self, kind, amt)
if kind == 'bluntForceTrauma' then
local std = self:phenoTrait 'sturdiness'
if std < 0 then
amt = amt / 1+std
else
amt = amt * 1-std
end
end
return amt
end;
---------
-- HUD --
---------
attachImage = function(self, def)
local user = self.entity
local img = {def = def}
img.id = user:hud_add {
type = 'image';
text = def.tex;
scale = def.scale;
alignment = def.align;
position = def.pos;
offset = def.ofs;
z_index = def.z;
}
if def.update then
img.update = function()
def.update(user, function(prop, val)
user:hud_change(img.id, prop, val)
end, def)
end
end
return img
end;
attachMeter = function(self, def)
local luser = self.entity
local m = {def = def}
local w = def.size or 80
local szf = w / 80
local h = szf * 260
m.meter = luser:hud_add {
type = 'image';
scale = {x = szf, y = szf};
alignment = def.align;
position = def.pos;
offset = def.ofs;
z_index = def.z or 0;
}
local cx = def.ofs.x + (w/2)*def.align.x
local cy = def.ofs.y + (h/2)*def.align.y
local oy = cy + h/2 - 42
-- this is so fucking fragile holy fuck
m.readout = luser:hud_add {
type = 'text';
scale = {x = w, y = h};
size = szf;
style = 4;
position = def.pos;
alignment = {x=0,0};
offset = {x = cx, y = oy};
z_index = (def.z or 0)+1;
number = 0xffffff;
}
m.destroy = function()
luser:hud_remove(m.meter)
luser:hud_remove(m.readout)
end
m.update = function()
local v,txt,color,txtcolor,hl,hlcolor = def.measure(luser,def)
v = math.max(0, math.min(1, v))
local n = math.floor(v*16) + 1
local function adjust(img)
return hudAdjustBacklight(lib.image(img)):shift(color or def.color)
end
local img = adjust 'starlit-ui-meter.png'
img = img:render()
img = img .. '^[verticalframe:17:' .. tostring(17 - n)
if hl then
hl = math.floor(hl*16) + 1
local hi = hudAdjustBacklight(lib.image 'starlit-ui-meter-hl.png')
:shift(hlcolor or def.color)
:render()
hi = hi .. '^[verticalframe:17:' .. tostring(17 - hl)
img = string.format('%s^(%s)', img, hi)
end
img = string.format('%s^(%s)', img, adjust 'starlit-ui-meter-readout.png':render())
if def.flipX then
img = img .. '^[transformFX'
end
luser:hud_change(m.meter, 'text', img)
if txt then
luser:hud_change(m.readout, 'text', txt)
end
if txtcolor then
luser:hud_change(m.readout, 'number', txtcolor:hex())
end
end
return m
end;
attachTextBox = function(self, def)
local luser = self.entity
local box = {def = def}
box.id = luser:hud_add {
type = 'text';
text = '';
alignment = def.align;
number = def.color and def.color:int24() or 0xFFffFF;
scale = def.bound;
size = {x = def.size, y=0};
style = def.style;
position = def.pos;
offset = def.ofs;
}
box.update = function()
local text, color = def.text(self, box, def)
luser:hud_change(box.id, 'text', text)
if color then
luser:hud_change(box.id, 'number', color:int24())
end
end
return box
end;
attachStatBar = function(self, def)
local luser = self.entity
local bar = {def = def}
local img = lib.image 'starlit-ui-bar.png'
local colorized = img
if type(def.color) ~= 'function' then
colorized = colorized:shift(def.color)
end
bar.id = luser:hud_add {
type = 'statbar';
position = def.pos;
offset = def.ofs;
name = def.name;
text = colorized:render();
text2 = img:tint{hue=0, sat=-1, lum = -0.5}:fade(0.5):render();
number = def.size;
item = def.size;
direction = def.dir;
alignment = def.align;
size = {x=4,y=24};
}
bar.update = function()
local sv, sf = def.stat(self, bar, def)
luser:hud_change(bar.id, 'number', def.size * sf)
if type(def.color) == 'function' then
local clr = def.color(sv, luser, sv, sf)
luser:hud_change(bar.id, 'text', img:tint(clr):render())
end
end
return bar, {x=3 * def.size, y=16} -- x*2??? what
end;
createHUD = function(self)
local function basicStat(statName)
return function(user, bar)
return self:effectiveStat(statName)
end
end
local function attachBasicStat(def)
local statName = def.stat
def.stat = basicStat(def.stat)
local b = self:attachStatBar(def)
self.hud.bar[statName] = b
return b
end
local function batteryLookup(user)
local max = user:suitPowerCapacity()
if max == 0 then return 0, 0 end
local ch = user:suitCharge()
return (ch/max)*100, ch/max
end
local function C(h,s,l) return {hue=h,sat=s,lum=l} end
local hbofs = (1+self.entity:hud_get_hotbar_itemcount()) * 25
local bpad = 8
self.hud.elt.health = attachBasicStat {
name = 'health', stat = 'health';
color = C(10,0,.3), size = 100;
pos = {x=0.5, y=1}, ofs = {x = -hbofs, y=-48 - bpad};
dir = 1;
align = {x=-1, y=-1};
}
self.hud.elt.stamina = attachBasicStat {
name = 'stamina', stat = 'stamina';
color = C(60,0,.2), size = 100;
pos = {x=0.5, y=1}, ofs = {x = -hbofs, y=-24 - bpad};
dir = 1;
align = {x=-1, y=-1};
}
self.hud.elt.bat = self:attachStatBar {
name = 'battery', stat = batteryLookup;
color = C(190,0,.2), size = 100;
pos = {x=0.5, y=1}, ofs = {x = hbofs - 4, y=-48 - bpad};
dir = 0;
align = {x=1, y=-1};
}
self.hud.elt.numina = attachBasicStat {
name = 'numina', stat = 'numina';
color = C(320,0,.2), size = 100;
pos = {x=0.5, y=1}, ofs = {x = hbofs - 4, y=-24 - bpad};
dir = 0;
align = {x=1, y=-1};
}
self.hud.elt.time = self:attachTextBox {
name = 'time';
align = {x=0, y=1};
pos = {x=0.5, y=1};
ofs = {x=0,y=-95};
text = function(user)
local cal = starlit.world.time.calendar[user.pref.calendar]
return cal.time(minetest.get_timeofday())
end;
}
self.hud.elt.temp = self:attachMeter {
name = 'temp';
align = {x=1, y=-1};
pos = {x=0, y=1};
ofs = {x=20, y=-20};
measure = function(user)
local warm = self:effectiveStat 'warmth'
local exposure = starlit.world.climate.temp(self.entity:get_pos())
local function tempVals(warm, br)
local n if warm < 0 then
n = math.min(100, -warm)
-- color = lib.color(0.1,0.3,1):lerp(lib.color(0.7, 1, 1), math.min(1, n/50))
else
n = math.min(100, warm)
-- color = lib.color(0.1,0.3,1):lerp(lib.color(1, 0, 0), math.min(1, n/50))
end
local hue = lib.math.gradient({
205, 264, 281, 360 + 17
}, (warm + 50) / 100) % 360
return {hue=hue, sat = 1, lum = br}, n
end
local color, n = tempVals(warm,0)
local hlcolor, hl = tempVals(exposure,.5)
local txt = string.format("%s°", math.floor(warm))
return (n/50), txt, color, nil, (hl/50), hlcolor
end;
}
self.hud.elt.geiger = self:attachMeter {
name = 'geiger';
align = {x=-1, y=-1};
pos = {x=1, y=1};
ofs = {x=-20, y=-20};
flipX = true;
measure = function(user)
local hot = self:effectiveStat 'irradiation'
local color = self:uiColor():lerp(lib.color(0.3, 1, 0), math.min(1, hot/5))
local txt = string.format("%sGy", math.floor(hot))
return (hot/5), txt, color
end;
}
-- special-case the meters
self.hud.bar.irradiation = self.hud.elt.geiger
self.hud.bar.warmth = self.hud.elt.temp
self.hud.elt.crosshair = self:attachImage {
name = 'crosshair';
tex = '';
pos = {x=.5, y=.5};
scale = {x=1,y=1};
ofs = {x=0, y=0};
align = {x=0, y=0};
update = function(user, set)
local imgs = {
off = '';
nano = 'starlit-ui-crosshair-nano.png';
psi = 'starlit-ui-crosshair-psi.png';
weapon = 'starlit-ui-crosshair-weapon.png';
}
set('text', imgs[self.actMode] or imgs.off)
end;
};
local hudCenterBG = lib.image 'starlit-ui-hud-bg.png':colorize(self:uiColor())
self.hud.elt.bg = self:attachImage {
name = 'hudBg';
tex = hudCenterBG:render();
pos = {x=.5, y=1};
scale = {x=1,y=1};
ofs = {x=0, y=0};
align = {x=0, y=-1};
z = -1;
update = function(user, set)
set('text', hudAdjustBacklight(hudCenterBG):render())
end;
};
self:updateHUD()
end;
deleteHUD = function(self)
for name, e in pairs(self.hud.elt) do
self.entity:hud_remove(e.id)
end
end;
updateHUD = function(self)
for name, e in pairs(self.hud.elt) do
if e.update then e.update() end
end
self:updateLEDs()
end;
updateLEDs = function(self)
local time = minetest.get_gametime()
local function updateSide(name, ofs, tx)
local del = {}
for i, l in ipairs(self.hud.led[name]) do
local idx = 0
if time - l.origin > 3 then
if l.elt then self.entity:hud_remove(l.elt.id) end
self.hud.led.map[l.kind] = nil
table.insert(del, i)
else
local xc = (idx*48 + 400)*ofs
if l.elt and next(del) then
l.elt:update('offset', {x=xc, y=1})
else
local tex = leds[l.kind].icon:blit(hudAdjustBacklight(leds[l.kind].bg))
if tx then tex = lib.image(tex:render()):transform(tx) end
if not l.elt then
l.elt = self:attachImage {
tex = tex:render();
align = {x=ofs, y=-1};
pos = {x=.5, y=1};
scale = {x=1,y=1};
ofs = {x=xc, y=0};
}
end
end
idx = idx + 1
end
end
for _, i in ipairs(del) do
table.remove(self.hud.led[name], i)
end
end
updateSide('left', -1)
updateSide('right', 1, 'FX')
end;
---------------------
-- actions & modes --
---------------------
onModeChange = function(self, oldMode, silent)
self.hud.elt.crosshair.update()
if not silent then
local sfxt = {
off = 'starlit-mode-off';
nano = 'starlit-mode-nano';
psi = 'starlit-mode-psi';
weapon = 'starlit-mode-weapon';
}
local sfx = self.actMode and sfxt[self.actMode] or sfxt.off
self:suitSound(sfx)
self:setModeHand()
end
end;
actModeSet = function(self, mode, silent)
if not mode then mode = 'off' end
local oldMode = self.actMode
self.actMode = mode
self:onModeChange(oldMode, silent)
if mode ~= oldMode then
starlit.ui.setupForUser(self)
end
end;
setModeHand = function(self) -- horrible horrible HACK
local inv = self.entity:get_inventory()
local hnd
if self.actMode == 'off'
then hnd = ItemStack('starlit:_hand_dig')
else hnd = ItemStack()
end
inv:set_stack('hand', 1, hnd)
end;
---------------------
-- intel-gathering --
---------------------
clientInfo = function(self)
return minetest.get_player_information(self.name)
end;
species = function(self)
return starlit.world.species.index[self.persona.species]
end;
-- can the suit heater sustain its current internal temperature in an area of t°C
tempCanSustain = function(self, t)
if self:naked() then return false end
local s = self:getSuit()
if s:powerState() == 'off' then return false end
local sd = s:def()
local w = self:effectiveStat 'warmth'
local kappa = starlit.constant.heat.thermalConductivity
local insul = sd.temp.insulation
local dt = (kappa * (1-insul)) * (t - w)
if (dt > 0 and dt > sd.temp.maxCool)
or (dt < 0 and math.abs(dt) > sd.temp.maxHeat) then return false end
return true
end;
-- will exposure to temperature t cause the player eventual harm
tempHazard = function(self, t)
local tr = self:species().tempRange.survivable
if t >= tr[1] and t <= tr[2] then return nil end
if self:tempCanSustain(t) then return nil end
if t < tr[1] then return 'cold' end
return 'hot'
end;
--------------------
-- event handlers --
--------------------
onSignup = function(self)
local meta = self.entity:get_meta()
local inv = self.entity:get_inventory()
-- 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
inv:set_size('main', 6) -- carried items and tools. main hotbar.
inv:set_size('hand', 1) -- horrible hack to allow both tools and intrinsics
inv:set_size('starlit_suit', 1) -- your environment suit (change at wardrobe)
inv:set_size('starlit_cfg', 1) -- the item you're reconfiguring / container you're accessing
local scenario
for _, e in pairs(starlit.world.scenario) do
if e.id == starlit.world.defaultScenario then
scenario = e break
end
end assert(scenario)
self.persona = starlit.world.species.birth(scenario.species, scenario.speciesVariant, self.entity)
self.persona.name = self.entity:get_player_name() -- a reasonable default
self.persona.background = starlit.world.defaultScenario
self:pushPersona()
local gifts = scenario.startingItems
local inv = self.entity:get_inventory()
inv:set_stack('starlit_suit', 1, starlit.item.mk(gifts.suit, self, {gift=true}))
self:getSuit():establishInventories(self.entity)
local function giveGifts(name, list)
if inv:get_size(name) > 0 then
for i, e in ipairs(list) do
inv:add_item(name, starlit.item.mk(e, self, {gift=true}))
end
end
end
giveGifts('starlit_suit_bat', gifts.suitBatteries)
giveGifts('starlit_suit_chips', gifts.suitChips)
giveGifts('starlit_suit_guns', gifts.suitGuns)
giveGifts('starlit_suit_ammo', gifts.suitAmmo)
giveGifts('starlit_suit_canisters', gifts.suitCans)
giveGifts('main', gifts.carry)
self:reconfigureSuit()
-- i feel like there has to be a better way
local posrng = starlit.world.seedbank[0x13f19] -- TODO player-specific seed
local cx = posrng:int(-500,500) --math.random(-500,500)
local iter, startPoint = 1
repeat local temp = -100
local cz = posrng:int(-500,500)
local cy = minetest.get_spawn_level(cx, cz)
if cy then
startPoint = vector.new(cx,cy,cz)
temp = starlit.world.climate.eval(startPoint,.5,.5).surfaceTemp
end
iter = iter + 1
if iter > 100 then break end -- avoid infiniloop in pathological conditions
until temp > -2
self.entity:set_pos(startPoint)
meta:set_string('starlit_spawn', startPoint:to_string())
end;
onDie = function(self, reason)
local inv = self.entity:get_inventory()
local where = self.entity:get_pos()
local function dropInv(lst)
local l = inv:get_list(lst)
for i, o in ipairs(l) do
if o and not o:is_empty() then
minetest.item_drop(o, self.entity, where)
end
end
inv:set_list(lst, {})
end
dropInv 'main'
dropInv 'starlit_suit'
self:updateSuit()
end;
onRespawn = function(self)
local meta = self.entity:get_meta()
self.entity:set_pos(vector.from_string(meta:get_string'starlit_spawn'))
self:statDelta('numina', 0, 'death', true)
self:statDelta('nutrition', 1500, 'death', true)
self:statDelta('hydration', 2, 'death', true)
self:statDelta('fatigue', 0, 'death', true)
self:statDelta('stamina', 0, 'death', true)
self:updateSuit()
return true
end;
onJoin = function(self)
local me = self.entity
local meta = me:get_meta()
self:pullPersona()
self:setModeHand()
-- formspec_version and real_coordinates are apparently just
-- completely ignored here
me:set_formspec_prepend [[
bgcolor[#00000000;true]
style_type[button,button_exit,image_button,item_image_button;border=false]
style_type[button;bgimg=starlit-ui-button-hw.png;bgimg_middle=8;content_offset=0,-2]
style_type[button:hovered;bgimg=starlit-ui-button-hw-hover.png;bgimg_middle=8]
style_type[button:pressed;bgimg=starlit-ui-button-hw-press.png;bgimg_middle=8;content_offset=0,1]
]]
local hotbarSlots = me:get_inventory():get_size 'main';
-- local slotTex = 'starlit-ui-slot.png'
-- local hbimg = string.format('[combine:%sx128', 128 * hotbarSlots)
-- for i = 0, hotbarSlots-1 do
-- hbimg = hbimg .. string.format(':%s,0=%s', 128 * i, slotTex)
-- end
--me:hud_set_hotbar_image(lib.image(hbimg):colorize(self:uiColor()):fade(.36):render())
-- me:hud_set_hotbar_selected_image(lib.image(slotTex):colorize(self:uiColor()):render())
me:hud_set_hotbar_image('[fill:1x24:0,0:' .. self:uiColor():fade(.1):hex())
me:hud_set_hotbar_selected_image(
'[fill:1x24,0,0:' .. self:uiColor():fade(.4):hex() .. '^[fill:1x1:0,23:#ffFFffff'
)
me:hud_set_hotbar_itemcount(hotbarSlots)
me:hud_set_flags {
hotbar = true;
healthbar = false;
breathbar = false;
basic_debug = false;
crosshair = false;
}
-- disable builtin crafting
local inv = me:get_inventory()
inv:set_size('craftpreview', 0)
inv:set_size('craftresult', 0)
inv:set_size('craft', 0)
me:set_stars {
day_opacity = 0.7;
}
me:set_sky {
sky_color = {
day_sky = '#a7c2cd', day_horizon = '#ddeeff';
dawn_sky = '#003964', dawn_horizon = '#87ebff';
night_sky = '#000000', night_horizon = '#000E29';
fog_sun_tint = '#72e4ff';
fog_moon_tint = '#2983d0';
fog_tint_type = 'custom';
};
fog = { -- not respected??
-- TODO make this seasonal & vary with weather
fog_distance = 40;
fog_start = 0.3;
};
}
me:set_sun {
texture = 'starlit-sun.png';
sunrise = 'sunrisebg.png^[hsl:180:1:.7';
tonemap = 'sun_tonemap.png^[hsl:180:1:.7';
scale = 0.8;
}
me:set_lighting {
shadows = {
intensity = .5;
};
exposure = {
luminance_max = 3.0;
speed_dark_bright = 0.5;
speed_bright_dark = 1.0;
};
volumetric_light = {
strength = 0.3;
};
}
me:set_eye_offset(nil, vector.new(3,-.2,10))
-- TODO set_clouds speed in accordance with wind
starlit.world.species.setupEntity(me, self.persona)
starlit.ui.setupForUser(self)
self:createHUD()
self:updateSuit()
self:updateOverlays()
end;
onPart = function(self)
starlit.liveUI [self.name] = nil
starlit.activeUI [self.name] = nil
starlit.activeUsers[self.name] = nil
end;
-----------------------------
-- environment suit & body --
-----------------------------
suitStack = function(self)
return self.entity:get_inventory():get_stack('starlit_suit', 1)
end;
suitSound = function(self, sfx)
-- trigger a sound effect from the player's suit computer
minetest.sound_play(sfx, {object=self.entity, max_hear_distance=4}, true)
end;
suitPowerStateSet = function(self, state, silent)
-- necessary to enable reacting to power state changes
-- e.g. to play sound effects, display warnings
local os
self:forSuit(function(s)
os=s:powerState()
s:powerStateSet(state)
end)
if state == 'off' then
if self.actMode == 'nano' or self.actMode == 'weapon' then
self:actModeSet('off', silent)
end
end
if not silent and os ~= state then
local sfx
if state == 'off' then
sfx = 'starlit-power-down'
elseif os == 'off' then
sfx = 'starlit-power-up'
elseif state == 'powerSave' or os == 'powerSave' then
sfx = 'starlit-configure'
end
if sfx then self:suitSound(sfx) end
end
end;
updateBody = function(self)
local adornment = {}
local suitStack = self:suitStack()
if suitStack and not suitStack:is_empty() then
local suit = suitStack:get_definition()._starlit.suit
suit.adorn(adornment, suitStack, self.persona)
end
starlit.world.species.updateTextures(self.entity, self.persona, adornment)
end;
updateSuit = function(self)
self:updateBody()
local inv = self.entity:get_inventory()
local sst = suitStore(self:suitStack())
if self:naked() then
starlit.type.suit.purgeInventories(self.entity)
if self.actMode == 'nano' or self.actMode == 'weapon' then
self:actModeSet 'off'
end
else
local suit = self:getSuit()
suit:establishInventories(self.entity)
if self:suitCharge() <= 0 then
self:suitPowerStateSet 'off'
end
end
-- self:updateHUD()
self.hud.elt.bat:update()
end;
reconfigureSuit = function(self)
-- and here's where things get ugly
-- you can't have an inventory inside another item. to hack around this,
-- we use the player as the location of the suit inventories, and whenever
-- there's a change in the content of these inventories, this function is
-- called to serialize those inventories out to the suit stack
if self:naked() then return end
local suit = self:getSuit()
suit:onReconfigure(self.entity:get_inventory())
self:setSuit(suit)
-- reconfiguring the suit can affect player abilities: e.g. removing
-- / inserting a chip with a minimap program
end;
getSuit = function(self)
local st = self:suitStack()
if st:is_empty() then return nil end
return starlit.type.suit(st)
end;
setSuit = function(self, suit)
self.entity:get_inventory():set_stack('starlit_suit', 1, suit.item)
end;
changeSuit = function(self, ...)
self:setSuit(...)
self:updateSuit()
end;
forSuit = function(self, fn)
local s = self:getSuit()
if fn(s) ~= false then
self:setSuit(s)
end
end;
suitPowerCapacity = function(self) -- TODO optimize
if self:naked() then return 0 end
return self:getSuit():powerCapacity()
end;
suitCharge = function(self) -- TODO optimize
if self:naked() then return 0 end
return self:getSuit():powerLeft()
end;
suitDrawCurrent = function(self, power, time, whatFor, min)
if self:naked() then return 0,0 end
local inv = self.entity:get_inventory()
local bl = inv:get_list('starlit_suit_bat')
local supply = 0
local wasteHeat = 0 --TODO handle internally
for slot, ps in ipairs(bl) do
if not ps:is_empty() then
local p, h = starlit.mod.electronics.dynamo.drawCurrent(ps, power - supply, time)
supply = supply + p
wasteHeat = wasteHeat + h
if power-supply <= 0 then break end
end
end
if min and supply < min then return 0,0 end
inv:set_list('starlit_suit_bat', bl)
self:reconfigureSuit()
if whatFor then
-- TODO display power use icon
end
return supply, wasteHeat
end;
naked = function(self)
return self:suitStack():is_empty()
end;
--------
-- ui --
--------
openUI = function(self, id, page, ...)
local ui = assert(starlit.interface.db[id])
ui:open(self, page, ...)
end;
onRespond = function(self, ui, state, resp)
ui:action(self, state, resp)
end;
trigger = function(self, which, how)
local p
local wld = self.entity:get_wielded_item()
if which == 'maneuver' then
p = self.power.maneuver
elseif which == 'retarget' then
self.action.prog = {}
elseif wld and not wld:is_empty() then
local wdef = wld:get_definition()
if wdef._starlit and wdef._starlit.tool then
p = {tool = wdef._starlit.tool}
end
elseif self.actMode ~= 'off' then
p = self.power[self.actMode][which]
end
if p == nil then return false end
local ctx, run = {
how = how;
}
if p.chipID then
local inv = self.entity:get_inventory()
local chips = inv:get_list 'starlit_suit_chips'
for chSlot, ch in pairs(chips) do
if ch and not ch:is_empty() then
local d = starlit.mod.electronics.chip.read(ch)
if d.uuid == p.chipID then
local pgm = assert(d.files[p.pgmIndex], 'file missing for ability')
ctx.file = starlit.mod.electronics.chip.fileHandle(ch, p.pgmIndex)
ctx.saveChip = function()
inv:set_slot('starlit_suit_chips', chSlot, ch)
end
local sw = starlit.item.sw.db[pgm.body.pgmId]
run = assert(sw.run, 'missing run() for active software ability ' .. pgm.body.pgmId)
break
end
end
end
elseif p.ref then
run = p.ref.run
else
error('bad ability pointer ' .. dump(p))
end
if run then
run(self, ctx)
return true
end
return false
end;
alarm = function(self, urgency, kind, minFreq)
minFreq = minFreq or 1.5
local time = minetest.get_gametime()
local led = leds[kind]
local ul = self.hud.led.map[kind]
if ul then
if time - ul.origin > minFreq then
ul.origin = time
else return end
end
if urgency ~= 0 then
local urgencies = {
[-2] = {sound = 'starlit-success'};
[-1] = {sound = 'starlit-nav'};
[1] = {sound = 'starlit-alarm'};
[2] = {sound = 'starlit-alarm-urgent'};
}
local urg = urgencies[urgency] or urgencies[#urgencies]
if time - self.cooldownTimes.alarm > 1.5 then
self.cooldownTimes.alarm = time
self:suitSound(urg.sound)
end
end
local newLed = {
kind = kind;
origin = time;
}
self.hud.led.map[kind] = newLed
table.insert(self.hud.led[led.side], newLed)
self:updateLEDs()
--[[
freq = freq or 3
local urgencies = {
[1] = {sound = 'starlit-alarm'};
[2] = {sound = 'starlit-alarm-urgent'};
}
local gt = minetest.get_gametime()
local urg = urgencies[urgency] or urgencies[#urgencies]
if gt - self.cooldownTimes.alarm < freq then return end
self.cooldownTimes.alarm = gt
self:suitSound(urg.sound)
if where then
local elt = {
tex = where.tex or 'starlit-ui-alert.png';
scale = {x=1, y=1};
align = table.copy(where.elt.def.align);
pos = table.copy(where.elt.def.pos);
ofs = table.copy(where.elt.def.ofs);
}
elt.ofs.x = elt.ofs.x + where.ofs.x
elt.ofs.y = elt.ofs.y + where.ofs.y
local attached = self:attachImage(elt)
table.insert(self.hud.alarm, attached)
-- HATE. HATE. HAAAAAAAAAAATE
minetest.after(freq/2, function()
for k,v in pairs(self.hud.alarm) do
self.entity:hud_remove(v.id)
end
self.hud.alarm={}
end)
end]]
end;
-------------
-- weather --
-------------
updateWeather = function(self)
end;
canInteract = function(self, with)
return true; -- TODO
end;
---------------
-- inventory --
---------------
give = function(self, item)
item = ItemStack(item)
local inv = self.entity:get_inventory()
local function is(grp)
return minetest.get_item_group(item:get_name(), grp) ~= 0
end
-- TODO notif popups
if is 'specialInventory' then
--[[
if is 'powder' then
if self:naked() then return item end
local cans = inv:get_list 'starlit_suit_canisters'
if cans and next(cans) then for i, st in ipairs(cans) do
local lst = string.format('starlit_canister_%u_elem', i)
item = inv:add_item(lst, item)
if item:is_empty() then break end
end end
self:forSuit(function(x) x:pushCanisters(inv) end)
end
return item
]]
else
return inv:add_item('main', item)
end
end;
thrustUpon = function(self, item)
local r = self:give(st)
if not r:is_empty() then
return minetest.add_item(self.entity:get_pos(), r)
end
end;
consume = function(self, stack, n)
n = n or 1
if n == 0 then n = stack:get_count() end
local fd = stack:take_item(n)
local stats = starlit.world.food.effectiveStats(fd)
return stack
end;
};
}
local clockInterval = 1.0
starlit.startJob('starlit:clock', clockInterval, function(delta)
for id, u in pairs(starlit.activeUsers) do
u.hud.elt.time:update()
u:updateLEDs()
local ui = starlit.activeUI[u.name]
if ui and (ui.self.refresh or ui.self.pages[ui.page].refresh) then
ui.self:show(u)
end
end
end)
-- performs a general HUD refresh, mainly to update the HUD backlight brightness
local hudInterval = 10
starlit.startJob('starlit:hud-refresh', hudInterval, function(delta)
for id, u in pairs(starlit.activeUsers) do
u:updateHUD() end
end)
local biointerval = 1.0
starlit.startJob('starlit:bio', biointerval, function(delta)
for id, u in pairs(starlit.activeUsers) do
if u:effectiveStat 'health' ~= 0 then
local bmr = u:phenoTrait 'metabolism' * biointerval
-- TODO apply modifiers
local dehydration = u:phenoTrait 'dehydration' * biointerval
-- you dehydrate faster in higher temp
dehydration = dehydration * math.max(1, starlit.world.climate.temp(u.entity:get_pos()) / 10)
u:statDelta('nutrition', -bmr)
u:statDelta('hydration', -dehydration)
local moralePenalty = -1 -- 1min/min
local fatiguePenalty = 1 -- 1min/min
local heatPenalty = 1 -- stamina regen is divided by this
do local warmth = u:effectiveStat 'warmth'
local tempRange = u:species().tempRange
local tComfMin, tComfMax = tempRange.comfort[1], tempRange.comfort[2]
local tempDiff = 0
if warmth < tComfMin then
tempDiff = tComfMin - warmth
elseif warmth > tComfMax then
tempDiff = warmth-tComfMax
end
-- print('tempDiff', tComfMin, tComfMax, tempDiff)
local tempPenalty = tempDiff/3
moralePenalty = moralePenalty + tempPenalty
heatPenalty = heatPenalty + tempPenalty
end
-- penalize heavy phys. activity
local stamina, sp = u:effectiveStat 'stamina'
local fatigue, fp = u:effectiveStat 'fatigue'
fatiguePenalty = fatiguePenalty * (1 + 9*(1-sp))
local penaltyFromFatigue = 1 - fp
local food = u:effectiveStat 'nutrition'
local water = u:effectiveStat 'hydration'
local rads = u:effectiveStat 'irradiation'
if food < 1000 then moralePenalty = moralePenalty + (1 - (food/1000)) * 5 end
if water < 1 then moralePenalty = moralePenalty + (1 - (water/1)) * 10 end
if rads > 0 then
u:statDelta('irradiation', -0.0001 * biointerval)
local moraleDrainFac = 2^(rads / 2)
moralePenalty = moralePenalty * moraleDrainFac
end
u:statDelta('morale', moralePenalty * biointerval)
u:statDelta('fatigue', fatiguePenalty * biointerval)
if food == 0 then -- starvation
u:statDelta('health', -5*biointerval)
end
if water == 0 then -- dying of thirst
u:statDelta('health', -20*biointerval)
end
if sp < 1.0 and minetest.get_gametime() - u.cooldownTimes.stamina > 5.0 then
u:statDelta('stamina', (u:phenoTrait('staminaRegen',1) * penaltyFromFatigue) / heatPenalty)
-- print('stam', u:effectiveStat 'stamina', u:phenoTrait('staminaRegen',1) / heatPenalty, heatPenalty)
end
local morale, mp = u:effectiveStat 'morale'
local pr = u:phenoTrait 'numinaRegen'
u:statDelta('numina', pr * penaltyFromFatigue * mp)
end
end
end)
local cbit = {
up = 0x001;
down = 0x002;
left = 0x004;
right= 0x008;
jump = 0x010;
manv = 0x020;
snk = 0x040;
dig = 0x080;
put = 0x100;
zoom = 0x200;
}
-- this is the painful part
minetest.register_globalstep(function(delta)
local doNothing,mustInit,mustHalt = 0,1,2
for id, user in pairs(starlit.activeUsers) do
local ent = user.entity
local bits = ent:get_player_control_bits()
local function what(b)
if bit.band(bits, b) ~= 0 and bit.band(user.action.bits, b) == 0 then
return mustInit
elseif bit.band(bits, b) == 0 and bit.band(user.action.bits, b) ~= 0 then
return mustHalt
else return doNothing end
end
local skipBits = 0
if user.action.bits ~= bits then
local mPrimary = what(cbit.dig)
local mSecondary = what(cbit.put)
local mManeuver = what(cbit.manv)
if mPrimary == mustInit then -- ENGINE-BUG
user.action.tgt = {type='nothing'}
user.action.prog = {}
elseif mPrimary == mustHalt then
user:trigger('primary', {state='halt'})
end
if mSecondary == mustHalt then
user:trigger('secondary', {state='halt'})
end
if mManeuver == mustInit then
user:trigger('maneuver', {state='init'})
elseif mManeuver == mustHalt then
user:trigger('maneuver', {state='halt'})
end
end
--bits = bit.band(bits, bit.bnot(skipBits))
local function prog(what)
user:trigger(what, {state='prog', delta=delta})
end
if bit.band(bits, cbit.dig)~=0 then prog 'primary' end
if bit.band(bits, cbit.put)~=0 then prog 'secondary' end
if bit.band(bits, cbit.manv)~=0 then prog 'maneuver' end
user.action.bits = bits
-- ENGINE-BUG: dig and put are not handled equally in the
-- engine. it is possible for the put bit to get stuck on
-- if the key is hammered while the player is not moving.
-- the bit will release as soon as the player looks or turns
-- nonetheless this is obnoxious
end
end)