starlit  species.lua at [52a4f364ac]

File mods/starlit/species.lua artifact e0959a01e2 part of check-in 52a4f364ac


local lib = starlit.mod.lib

local paramTypes do local T,G = lib.marshal.t, lib.marshal.g
	paramTypes = {
		tone = G.struct {
			hue = T.angle;
			sat = T.clamp;
			lum = T.clamp;
		};
		str = T.str;
		num = T.decimal;
	}
end

-- constants
local animationFrameRate = 60

local bioAbilities = {
	sprint = {
		id = 'sprint';
		name = 'Sprint';
		desc = 'Put on a short burst of speed at the cost of some stamina';
		img = 'starlit-ui-icon-ability-sprint.png';
		powerKind = 'maneuver';
		run = function(user, ctx)
			local cost = 10
			-- unfortunately stat writes are very expensive, so can't draw from stamina
			-- every single frame
			local function halt()
				if user.action.prog.sprint then
					user:statDelta('stamina', -user.action.prog.sprint.cb)
					user:deleteOverlay(user.action.prog.sprint.id)
					user.action.prog.sprint = nil
				end
			end

			if ctx.how.state == 'init' then
				halt()
				if user:effectiveStat 'stamina' > 0 then
					user.action.prog.sprint = {
						cb = 0;
						id = user:overlay(function(phys) phys.speed = phys.speed * 2 end)
					}
				end
			elseif ctx.how.state == 'prog' then
				local d = ctx.how.delta
				local p = user.action.prog.sprint
				-- is the player currently holding any of WASD
				local isMoving = bit.band(0x0f, user.entity:get_player_control_bits()) ~= 0
				if p and isMoving then
					user.cooldownTimes.stamina = minetest.get_gametime()
					p.cb = p.cb + cost*d
					if p.cb >= 5 then
						user:statDelta('stamina', -p.cb)
						p.cb = 0
						if user:effectiveStat 'stamina' < cost then halt() end
					end
				end
			elseif ctx.how.state == 'halt' then
				halt()
			end
		end;
	};
}

local species = {
	human = {
		name = 'Human';
		desc = 'The weeds of the galactic flowerbed. Humans are one of the Lesser Races, excluded from the ranks of the Starlit by souls that lack, in normal circumstances, external psionic channels. Their mastery of the universe cut unexpectedly short, forever locked out of FTL travel, short-lived without augments, and alternately pitied or scorned by the lowest of the low, humans flourish nonetheless due to a capacity for adaptation unmatched among the Thinking Few, terrifyingly rapid reproductive cycles -- and a keen facility for bribery. While the lack of human psions remains a sensitive topic, humans (unlike the bitter and emotional Kruthandi) are practical enough to hire the talent they cannot possess, and have even built a small number of symbiotic civilizations with the more indulging of the Powers. In a galaxy where nearly all sophont life is specialized to a fault, humans have found the unique niche of occupying no particular niche.';
		scale = 1.0;
		params = {
			{'eyeColor',  'Eye Color',  'tone', {hue=327, sat=0, lum=0}};
			{'hairColor', 'Hair Color', 'tone', {hue=100, sat=0, lum=0}};
			{'skinTone',  'Skin Tone',  'tone', {hue=  0, sat=0, lum=0}};
		};
		tempRange = {
			comfort    = {18.3, 23.8}; -- needed for full stamina regen
			survivable = {5,    33}; -- anything below/above will cause progressively more damage
		};
		variants = {
			female = {
				name = 'Human Female';
				mesh = 'starlit-body-female.x';
				eyeHeight = 1.4;
				texture = function(t, adorn)
					local skin = lib.image 'starlit-body-skin.png' : shift(t.skinTone)
					local eye  = lib.image 'starlit-body-eye.png'  : shift(t.eyeColor)
					local hair = lib.image 'starlit-body-hair.png' : shift(t.hairColor)

					local invis = lib.image '[fill:1x1:0,0:#00000000'
					local plate = adorn.suit and adorn.suit.plate or invis
					local lining = adorn.suit and adorn.suit.lining or invis

					return {lining, plate, skin, skin, eye, hair}
				end;
				stats = {
					numina = 1.2;
					nutrition = .8; -- women have smaller stomachs
					hydration = .8;
					morale = 0.8; -- you are not She-Bear Grylls
					irradiation = 0.8; -- you are smaller, so it takes less rads to kill ya
				};
				traits = {
					health = 400;
					lungCapacity = .6;
					sturdiness = 0; -- women are more fragile and thus susceptible to blunt force trauma
					metabolism = .150; -- kCal/s
					painTolerance = 0.4;
					dehydration = 10e-4; -- L/s
					speed = 1.1;
					staminaRegen = 10.0;
					numinaRegen = 0.05; -- ψ/s
					psi = 1.2;
				};
			};
			male = {
				name = 'Human Male';
				eyeHeight = 1.6;
				stats = {
					numina = 1.0;
					nutrition = 1.0;
					hydration = 1.0;
					staminaRegen = 7; -- men are strong but have inferior endurance
				};
				traits = {
					health = 500;
					painTolerance = 1.0;
					lungCapacity = 1.0;
					sturdiness = 0.3;
					metabolism = .150; -- kCal/s
					dehydration = 15e-4; -- L/s
					speed = 1.0;
					numinaRegen = 0.025;
					psi = 1.0;
				};
			};
		};
		traits = {};
		abilities = {bioAbilities.sprint};
	};
}


starlit.world.species = {
	index = species;
	paramTypes = paramTypes;
}

starlit.world.species.pheno = lib.class {
	name = 'starlit:species.pheno';
	construct = function(pSp, pVar)
		local sp, var = starlit.world.species.lookup(pSp, pVar)
		return {
			species = sp, variant = var;
			pSpecies = pSp, pVariant = pVar;
		};
	end;
	__index = {
		trait = function(me, st, dflt)
			local v = me.variant.traits[st] or me.species.traits[st]
			return v or dflt
		end;
	};
}

function starlit.world.species.mkDefaultParamsTable(pSpecies, pVariant)
	local sp = species[pSpecies]
	local var = sp.variants[pVariant]
	local vpd = var.defaults or {}
	local tbl = {}
	for _, p in pairs(sp.params) do
		local name, desc, ty, dflt = lib.tbl.unpack(p)
		tbl[name] = vpd[name] or dflt
	end
	return tbl
end


function starlit.world.species.mkPersonaFor(pSpecies, pVariant)
	return {
		species = pSpecies;
		speciesVariant = pVariant;
		bodyParams = starlit.world.species.paramsFromTable(pSpecies,
			starlit.world.species.mkDefaultParamsTable(pSpecies, pVariant)
		);
		statDeltas = {};
		facts = {};
	}
end

local function spLookup(pSpecies, pVariant)
	local sp = species[pSpecies]
	local var = sp.variants[pVariant or next(sp.variants)]
	return sp, var
end
starlit.world.species.lookup = spLookup

function starlit.world.species.statRange(pSpecies, pVariant, pStat)
	local sp,spv = spLookup(pSpecies, pVariant)
	local min, max, base
	if pStat == 'health' then
		min,max = 0, spv.traits.health
	elseif pStat == 'breath' then
		min,max = 0, 65535
	else
		local spfac = spv.stats[pStat]
		local basis = starlit.world.stats[pStat]
		min,max = basis.min, basis.max

		if spfac then
			min = min * spfac
			max = max * spfac
		end

		base = basis.base
		if base == true then
			base = max
		elseif base == false then
			base = min
		end

	end
	return min, max, base or 0
end

-- set the necessary properties and create a persona for a newspawned entity
function starlit.world.species.birth(pSpecies, pVariant, entity, circumstances)
	circumstances = circumstances or {}
	local sp,var = spLookup(pSpecies, pVariant)

	local function pct(st, p)
		local min, max = starlit.world.species.statRange(pSpecies, pVariant, st)
		local delta = max - min
		return min + delta*p
	end
	local ps = starlit.world.species.mkPersonaFor(pSpecies,pVariant)
	local startingHP = pct('health', 1.0)
	if circumstances.injured    then startingHP = pct('health', circumstances.injured) end
	if circumstances.numinaCharged then ps.statDeltas.numina = pct('numina', circumstances.numinaCharged) end
	for k,v in pairs(starlit.world.stats) do ps.statDeltas[k] = 0 end
	ps.statDeltas.warmth = 20 -- don't instantly start dying of frostbite
	ps.statDeltas.nutrition = 2000 -- shoulda packed more MRE :c
	ps.statDeltas.hydration = 3 -- stay hydrated uwu

	entity:set_properties{hp_max = var.traits.health or sp.traits.health}
	entity:set_hp(startingHP, 'initial hp')
	return ps
end

function starlit.world.species.paramsFromTable(pSpecies, tbl)
	local lst = {}
	local sp = species[pSpecies]
	for i, par in pairs(sp.params) do
		local name,desc,ty,dflt = lib.tbl.unpack(par)
		if tbl[name] then
			table.insert(lst, {id=name, value=paramTypes[ty].enc(tbl[name])})
		end
	end
	return lst
end
function starlit.world.species.paramsToTable(pSpecies, lst)
	local tymap = {}
	local sp = species[pSpecies]
	for i, par in pairs(sp.params) do
		local name,desc,ty,dflt = lib.tbl.unpack(par)
		tymap[name] = paramTypes[ty]
	end

	local tbl = {}
	for _, e in pairs(lst) do
		tbl[e.id] = tymap[e.id].dec(e.value)
	end
	return tbl
end

for speciesName, sp in pairs(species) do
	for varName, var in pairs(sp.variants) do
		if var.mesh then
			var.animations = starlit.evaluate(string.format('models/%s.nla', var.mesh)).skel.action
		end
	end
end


function starlit.world.species.updateTextures(ent, persona, adornment)
	local s,v = spLookup(persona.species, persona.speciesVariant)
	local paramTable = starlit.world.species.paramsToTable(persona.species, persona.bodyParams)
	local texs = {}
	for i, t in ipairs(v.texture(paramTable, adornment)) do
		texs[i] = t:render()
	end
	ent:set_properties { textures = texs }
end

function starlit.world.species.setupEntity(ent, persona)
	local s,v = spLookup(persona.species, persona.speciesVariant)
	local _, maxHealth = starlit.world.species.statRange(persona.species, persona.speciesVariant, 'health')
	ent:set_properties {
		visual = 'mesh';
		mesh = v.mesh;
		stepheight = .51;
		eye_height = v.eyeHeight;
		collisionbox = { -- FIXME
			-0.3, 0.0, -0.3;
			0.3, 1.5,  0.3;
		};
		visual_size = vector.new(10,10,10) * s.scale;

		hp_max = maxHealth;
	}
	local function P(v)
		if v then return {x=v[1],y=v[2]} end
		return {x=0,y=0}
	end
	ent:set_local_animation(
		P(v.animations.idle),
		P(v.animations.run),
		P(v.animations.work),
		P(v.animations.runWork),
		animationFrameRate)

end