Overview
Comment: | add to lore, add weather data, etc |
---|---|
Downloads: | Tarball | ZIP archive | SQL archive |
Timelines: | family | ancestors | descendants | both | trunk |
Files: | files | file ages | folders |
SHA3-256: |
caec179da98b03b03ac6b8b215692f17 |
User & Date: | lexi on 2025-01-19 19:05:09 |
Other Links: | manifest | tags |
Context
2025-01-19
| ||
19:18 | we have always been at war with east minecraft Leaf check-in: 4732f8d454 user: lexi tags: trunk | |
19:05 | add to lore, add weather data, etc check-in: caec179da9 user: lexi tags: trunk | |
2024-12-19
| ||
20:03 | unfuck cpio invocations check-in: e926621707 user: root tags: trunk | |
Changes
Modified dev.ct from [912982b94d] to [51e6dca729].
1 1 # starlit development 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. 2 +this file contains information meant for those who wish to develop for Starlit 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 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 -to run a trunk version of Starsoul, you'll need to install the above tools and run `make` from the base directory. this will: 11 +to run a trunk version of Starlit, 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 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 mods/starlit-eco/init.lua from [fb6e06be08] to [f0d21c2132].
11 11 node_filler = 'starlit:soil', depth_filler = 4; 12 12 node_riverbed = 'starlit:sand', depth_riverbed = 4; 13 13 y_min = 0; 14 14 y_max = 56; 15 15 heat_point = 50; 16 16 humidity_point = 40; 17 17 }; 18 + weather = { 19 + {-0.900, 'starlit:meteorShower'}; 20 + {-0.700, 'starlit:sstorm'}; 21 + {-0.100, 'starlit:clear'}; 22 + {0.300, 'starlit:cloudy'}; 23 + {0.400, 'starlit:precip'}; 24 + {0.450, 'starlit:storm'}; 25 + {0.500, 'starlit:tstorm'}; 26 + }; 18 27 }) 19 28 20 29 world.ecology.biomes.link('starlit:forest', { 21 30 nightTempDelta = -20; 22 31 waterTempDelta = 0; 23 32 -- W Sp Su Au W 24 33 seasonalTemp = {-40, -8, 10, 10, -14, -40}; ................................................................................ 27 36 node_filler = 'starlit:soil', depth_filler = 4; 28 37 node_riverbed = 'starlit:sand', depth_riverbed = 4; 29 38 y_min = 0; 30 39 y_max = 256; 31 40 heat_point = 60; 32 41 humidity_point = 45; 33 42 }; 43 + weather = { 44 + {-0.900, 'starlit:meteorShower'}; 45 + {-0.700, 'starlit:sstorm'}; 46 + {-0.100, 'starlit:clear'}; 47 + {0.200, 'starlit:cloudy'}; 48 + {0.400, 'starlit:precip'}; 49 + {0.650, 'starlit:storm'}; 50 + {0.800, 'starlit:tstorm'}; 51 + }; 34 52 }) 35 53 36 54 world.ecology.biomes.link('starlit:desert', { 37 55 nightTempDelta = -40; 38 56 waterTempDelta = 0; 39 57 -- W Sp Su Au W 40 58 seasonalTemp = {-10, -5, 15, 15, -5, -10}; ................................................................................ 43 61 node_filler = 'starlit:sand', depth_filler = 4; 44 62 node_riverbed = 'starlit:sand', depth_riverbed = 4; 45 63 y_min = 0; 46 64 y_max = 512; 47 65 heat_point = 70; 48 66 humidity_point = 10; 49 67 }; 68 + weather = { 69 + {-0.900, 'starlit:meteorShower'}; 70 + {-0.700, 'starlit:sstorm'}; 71 + {-0.100, 'starlit:clear'}; 72 + {0.400, 'starlit:cloudy'}; 73 + {0.850, 'starlit:tstorm'}; 74 + }; 50 75 }) 51 76 52 77 world.ecology.biomes.link('starlit:ocean', { 53 78 nightTempDelta = -35; 54 79 waterTempDelta = 5; 55 80 seasonalTemp = {0}; -- no seasonal variance 56 81 def = { ................................................................................ 57 82 y_max = 3; 58 83 y_min = -512; 59 84 heat_point = 60; 60 85 humidity_point = 70; 61 86 node_top = 'starlit:sand', depth_top = 1; 62 87 node_filler = 'starlit:sand', depth_filler = 3; 63 88 }; 89 + weather = { 90 + {-0.900, 'starlit:meteorShower'}; 91 + {-0.700, 'starlit:sstorm'}; 92 + {-0.100, 'starlit:clear'}; 93 + {0.300, 'starlit:cloudy'}; 94 + {0.500, 'starlit:precip'}; 95 + {0.650, 'starlit:storm'}; 96 + {0.800, 'starlit:tstorm'}; 97 + }; 64 98 }) 65 99 66 100 world.ecology.biomes.link('starlit:shiverdeep', { 67 101 nightTempDelta = -25; 68 102 waterTempDelta = 5; 69 103 -- W Sp Su Au W 70 104 seasonalTemp = {-70, -30, 0, -60, -70}; ................................................................................ 73 107 y_min = 0; 74 108 heat_point = 20; 75 109 humidity_point = 30; 76 110 node_water_top = 'starlit:ice', depth_water_top = 1; 77 111 node_top = 'starlit:undergloam', depth_top = 1; 78 112 node_filler = 'starlit:soil', depth_filler = 2; 79 113 }; 114 + weather = { 115 + {-0.900, 'starlit:meteorShower'}; 116 + {-0.700, 'starlit:sstorm'}; 117 + {-0.100, 'starlit:clear'}; 118 + {0.200, 'starlit:cloudy'}; 119 + {0.400, 'starlit:precip'}; 120 + {0.650, 'starlit:storm'}; 121 + {0.900, 'starlit:tstorm'}; 122 + }; 80 123 }) 81 124 82 125 world.ecology.biomes.link('starlit:silthaven', { 83 126 nightTempDelta = -5; 84 127 waterTempDelta = 5; 85 128 -- W Sp Su Au W 86 129 seasonalTemp = {-15, 5, 15, 7, -15}; ................................................................................ 88 131 y_max = 30; 89 132 y_min = 0; 90 133 heat_point = 30; 91 134 humidity_point = 30; 92 135 -- node_top = 'starlit:undergloam', depth_top = 1; 93 136 node_filler = 'starlit:lifesilt', depth_filler = 5; 94 137 }; 138 + weather = { 139 + {-0.900, 'starlit:meteorShower'}; 140 + {-0.700, 'starlit:sstorm'}; 141 + {-0.100, 'starlit:clear'}; 142 + {0.400, 'starlit:cloudy'}; 143 + {0.600, 'starlit:precip'}; 144 + {0.750, 'starlit:storm'}; 145 + {0.900, 'starlit:tstorm'}; 146 + }; 95 147 }) 96 148 97 149 world.ecology.biomes.link('starlit:barrens', { 98 150 nightTempDelta = -20; 99 151 waterTempDelta = 5; 100 152 -- W Sp Su Au W 101 153 seasonalTemp = {-30, -20, 0, -20, -30}; 102 154 def = { 103 155 y_max = 512; 104 156 y_min = -512; 105 157 heat_point = 0; 106 158 humidity_point = 0; 107 159 }; 160 + weather = { 161 + {-0.900, 'starlit:meteorShower'}; 162 + {-0.600, 'starlit:sstorm'}; 163 + {-0.100, 'starlit:clear'}; 164 + { 0.300, 'starlit:cloudy'}; 165 + { 0.600, 'starlit:precip'}; 166 + { 0.850, 'starlit:storm'}; 167 + { 0.900, 'starlit:tstorm'}; 168 + }; 108 169 }) 109 170 minetest.register_craftitem('starlit_eco:fiber', { 110 171 description = "Plant Fiber"; 111 172 groups = {fiber = 1}; 112 173 inventory_image = lib.image('starlit-eco-plant-fiber.png'):shift(lib.color(0,1,0)):render(); 113 174 _starlit = { 114 175 recover_vary = function(rng, ctx)
Modified mods/starlit-tech/init.lua from [22b6956759] to [91056054d4].
158 158 } end; 159 159 } 160 160 161 161 minetest.register_node('starlit_tech:crate', { 162 162 short_description = 'Crate'; 163 163 description = starlit.ui.tooltip { 164 164 title = 'Crate'; 165 - desc = 'A sturdy but lightweight storage crate woven from graphene.'; 165 + desc = 'A sturdy but lightweight aluminum storage crate.'; 166 166 props = { {title='Mass', affinity='info', desc='100g'} }; 167 167 }; 168 168 drawtype = 'nodebox'; 169 169 node_box = { 170 170 type = 'fixed'; 171 171 fixed = { 172 172 .4, .2, .4; ................................................................................ 191 191 _starlit = { 192 192 mass = 100; 193 193 reverseEngineer = { 194 194 complexity = 1; 195 195 sw = 'starlit_tech:schematic_crate'; 196 196 }; 197 197 recover = starlit.type.fab { 198 - element = { carbon = 100; }; 198 + element = { aluminum = 100; }; 199 199 time = { 200 200 shred = 1; 201 201 shredPower = 3; 202 202 }; 203 203 }; 204 204 }; 205 205 on_construct = function(pos)
Modified mods/starlit/init.lua from [44e70e86d4] to [277b90dbc5].
88 88 liquid = lib.registry.mk 'starlit:liquid'; 89 89 }; 90 90 ecology = { 91 91 plants = lib.registry.mk 'starlit:plants'; 92 92 trees = lib.registry.mk 'starlit:trees'; 93 93 biomes = lib.registry.mk 'starlit:biome'; 94 94 }; 95 - climate = {}; 95 + climate = { 96 + weather = lib.registry.mk 'starlit:weather'; 97 + weatherMap = {} 98 + }; 96 99 scenario = {}; 97 100 planet = { 98 101 gravity = 7.44; 99 102 orbit = 189; -- 1 year is 189 days 100 103 revolve = 20; -- 1 day is 20 irl minutes 101 104 }; 102 105 fact = lib.registry.mk 'starlit:fact'; ................................................................................ 171 174 }; 172 175 }; 173 176 }; 174 177 }; 175 178 176 179 jobs = {}; 177 180 } 181 + 182 +-- TODO deal with core.DEFAULT_PHYSICS once it hits master 178 183 179 184 starlit.cfgDir = minetest.get_worldpath() .. '/' .. starlit.ident 180 185 181 186 local logger = function(module) 182 187 local function argjoin(arg, nxt, ...) 183 188 if arg and not nxt then return tostring(arg) end 184 189 if not arg then return "(nil)" end
Modified mods/starlit/user.lua from [ae84fc4236] to [9accce5f34].
103 103 calendar = 'commune'; 104 104 }; 105 105 overlays = {}; 106 106 cooldownTimes = { 107 107 stamina = 0; 108 108 alarm = 0; 109 109 }; 110 + env = { 111 + weather = nil; 112 + }; 110 113 } 111 114 end; 112 115 __index = { 113 116 -------------- 114 117 -- overlays -- 115 118 -------------- 116 119 updateOverlays = function(self)
Modified mods/starlit/world.lua from [d1f4916ac1] to [de51a702a5].
42 42 surfaceTemp = heat; 43 43 waterTemp = heat + biome.waterTempDelta; 44 44 surfaceHumid = humid; 45 45 } 46 46 end 47 47 48 48 local vdsq = lib.math.vdsq 49 -function world.climate.temp(pos) --> irradiance at pos in W 49 +function world.climate.temp(pos, timeshift) --> irradiance at pos in W 50 50 local cl = world.climate.eval(pos) 51 51 local radCenters = starlit.region.radiator.store:get_areas_for_pos(pos, false, true) 52 52 local irradiance = 0 53 53 for _,e in pairs(radCenters) do 54 54 local rpos = minetest.string_to_pos(e.data) 55 55 local rdef = assert(minetest.registered_nodes[assert(minetest.get_node(rpos)).name]) 56 56 local rc = rdef._starlit.radiator ................................................................................ 79 79 power = power * (1 - (dist_sq / ((r_max+1)^2))) 80 80 end 81 81 power = power * (1 - (obstruct/5)) 82 82 irradiance = irradiance + power 83 83 end 84 84 end 85 85 end 86 + local w = world.climate.weatherAt(pos, timeshift) 87 + 86 88 return irradiance + cl.surfaceTemp 87 89 end 90 + 91 +function world.ecology.biomeAt(pos) 92 + return world.ecology.biomes.db[minetest.get_biome_name(minetest.get_biome_data(pos).biome)] 93 +end 94 + 95 + 96 +minetest.after(0, function() 97 + world.climate.weatherMap.kind = minetest.get_perlin { 98 + seed = 0x925afe; 99 + octaves = 2; 100 + spread = vector.new(256,256,120); 101 + }; 102 + world.climate.weatherMap.severity = minetest.get_perlin { 103 + seed = 0x39de1d; 104 + octaves = 1; 105 + spread = vector.new(256,256,60); 106 + }; 107 +end) 108 + 109 +function world.climate.weatherAt(pos, timeshift) 110 + timeshift = timeshift or 0 111 + local wv = world.climate.weatherMap.kind:get_3d(vector.new(pos.x, pos.z, minetest.get_gametime() + timeshift)) 112 + local sev = world.climate.weatherMap.severity:get_3d(vector.new(pos.x, pos.z, minetest.get_gametime() + timeshift)) 113 + local b = world.ecology.biomeAt(pos) 114 + local w = 'starlit:clear' 115 + for i,v in ipairs(b.weather) do 116 + if wv < v[1] then 117 + w = v[2] 118 + break 119 + end 120 + end 121 + local mods = { 122 + cloudCover = 0; 123 + rain = 0; -- affects plant growth 124 + snow = 0; -- spawns snow layer 125 + fog = 0; 126 + temp = 0; 127 + hum = 0; 128 + rad = 0; 129 + } 130 + return world.climate.weather.db[w], sev 131 +end 132 + 133 + 134 +-- weather manages particle systems, and provides modifiers for 135 +-- temp, cloud cover, received precipitation, and fog 136 + 137 +world.climate.weather.link('starlit:clear', { 138 + name = 'Clear'; 139 +}) 140 +world.climate.weather.link('starlit:cloudy', { 141 + name = 'Cloudy'; 142 + mod = function(m, temp, hum, sev) 143 + m.cloudCover = math.max(m.cloudCover, sev) 144 + end; 145 +}) 146 +world.climate.weather.link('starlit:precip', { 147 + name = function(temp, hum, sev) 148 + if temp < 0 then return 'Snow' else return 'Rain' end 149 + end; 150 + mod = function(m, temp, hum, sev) 151 + m.cloudCover = math.max(m.cloudCover, sev) 152 + if temp < 0 then 153 + m.snow = math.max(m.snow, sev/2) 154 + else 155 + m.rain = math.max(m.rain, sev/2) 156 + end 157 + end; 158 +}) 159 +world.climate.weather.link('starlit:storm', { 160 + name = function(temp, hum, sev) 161 + if temp < 0 then 162 + if sev > .5 163 + then return 'Blizzard' 164 + else return 'Snowstorm' 165 + end 166 + else 167 + if sev > .5 168 + then return 'Monsoon' 169 + else return 'Rainstorm' 170 + end 171 + end 172 + end; 173 + mod = function(m, temp, hum, sev) 174 + m.cloudCover = math.max(m.cloudCover, sev) 175 + if temp < 0 then 176 + m.snow = math.max(m.snow, sev/2 + .5) 177 + else 178 + m.rain = math.max(m.rain, sev/2 + .5) 179 + end 180 + end; 181 +}) 182 +world.climate.weather.link('starlit:tstorm', { 183 + name = 'Thunderstorm'; 184 + danger = 1; 185 + mod = function(m, temp, hum, sev) 186 + m.cloudCover = math.max(m.cloudCover, sev) 187 + m.danger = (sev>.5) and 2 or 1 188 + end; 189 +}) 190 +world.climate.weather.link('starlit:sstorm', { 191 + name = 'Solar Storm'; 192 + danger = 2; 193 +}) 194 +world.climate.weather.link('starlit:meteorShower', { 195 + name = 'Meteor Shower'; 196 + danger = 2; 197 +}) 88 198 89 199 world.ecology.biomes.foreach('starlit:biome-gen', {}, function(id, b) 90 200 b.def.name = id 91 201 minetest.register_biome(b.def) 92 202 end) 93 203 94 204 world.ecology.plants.foreach('starlit:plant-gen', {}, function(id, b) ................................................................................ 160 270 } 161 271 for k,v in pairs(b.decoration) do dec[k] = v end 162 272 b.decoration = minetest.register_decoration(dec) 163 273 end) 164 274 165 275 local toward = lib.math.toward 166 276 local hfinterval = 1.5 167 -starlit.startJob('starlit:heatflow', hfinterval, function(delta) 277 +starlit.startJob('starlit:temps', hfinterval, function(delta) 168 278 169 279 -- our base thermal conductivity (κ) is measured in °C/°C/s. say the 170 280 -- player is in -30°C weather, and has an internal temperature of 171 281 -- 10°C. then: 172 282 -- κ = .1°C/C/s (which is apparently 100mHz) 173 283 -- Tₚ = 10°C 174 284 -- Tₑ = -30°C ................................................................................ 175 285 -- d = Tₑ − Tₚ = -40°C 176 286 -- ΔT = κ×d = -.4°C/s 177 287 -- too cold: 178 288 -- x = beginning of danger zone 179 289 -- κ × (x - Tₚ) = y where y < Tₚ 180 290 -- our final change in temperature is computed as tΔC where t is time 181 291 local kappa = starlit.constant.heat.thermalConductivity 292 + local now = minetest.get_gametime() 182 293 for name,user in pairs(starlit.activeUsers) do 183 294 local tr = user:species().tempRange 184 295 local t = starlit.world.climate.temp(user.entity:get_pos()) 296 + 297 + local weather,wsev = starlit.world.climate.weatherAt(user.entity:get_pos()) 298 + local wfac 299 + if user.env.weather == nil 300 + then wfac = 1 301 + else wfac = (now - user.env.weather.when) / 10 302 + end 303 + if user.env.weather == nil or now - user.env.weather.when >= 10 then 304 + user.env.weather = {when = now, what = weather} 305 + end 185 306 186 307 do -- this bit probably belongs in starlit:bio but we do it here in order 187 308 -- to spare ourselves another call into the dark swamp of climate.temp 188 309 local urg = 1 189 310 local hz = user:tempHazard(t) 190 311 local tr = user:species().tempRange.survivable 191 312 if hz == 'cold' then
Modified mods/vtlib/init.lua from [d31a921c62] to [d021d078eb].
12 12 if chunk == nil then error(err) end 13 13 lib[name] = chunk(lib, ident, path) 14 14 end 15 15 16 16 component 'dbg' 17 17 18 18 -- primitive manip 19 -component 'tbl' 20 19 component 'class' 21 20 component 'math' 21 +component 'tbl' 22 22 component 'str' 23 23 24 24 -- reading and writing data formats 25 25 component 'marshal' 26 26 27 27 -- classes 28 28 component 'color'
Modified mods/vtlib/math.lua from [3e36361725] to [65dcc21fc6].
20 20 end 21 21 local dsq = (d.x ^ 2) + (d.y ^ 2) + (d.z ^ 2) 22 22 return dsq / (dist^2) 23 23 -- [0,1) == less then 24 24 -- 1 == equal 25 25 -- >1 == greater than 26 26 end 27 + 28 +local unpack = table.unpack or unpack 27 29 28 30 -- produce an SI expression for a quantity 29 31 fn.si = function(unit, val, full, uncommonScales, prec) 30 32 if val == 0 then return '0 ' .. unit end 31 33 local scales = { 32 34 {30, 'Q', 'quetta',true, 'q', 'quecto',true}; 33 35 {27, 'R', 'ronna', true, 'r', 'ronto', true}; ................................................................................ 51 53 end 52 54 end 53 55 return unit 54 56 end 55 57 56 58 for i, s in ipairs(scales) do 57 59 local amt, smaj, pmaj, cmaj, 58 - smin, pmin, cmin = lib.tbl.unpack(s) 60 + smin, pmin, cmin = unpack(s) 59 61 60 62 61 63 if math.abs(val) > 1 then 62 64 if uncommonScales or cmaj then 63 65 local denom = 10^amt 64 66 local vd = val/denom 65 67 if prec then vd = lib.math.trim(vd, prec) end
Modified mods/vtlib/tbl.lua from [ed1f208dfe] to [ae7901fd4c].
107 107 108 108 fn.pick = function(lst) 109 109 local keys = fn.keys(lst) 110 110 local k = keys[math.random(#keys)] 111 111 return k, lst[k] 112 112 end 113 113 114 -fn.unpack = table.unpack or unpack or function(tbl,i) 114 +fn.unpack = table.unpack or unpack --[[or function(tbl,i) 115 115 i = i or 1 116 116 if #tbl == i then return tbl[i] end 117 117 return tbl[i], fn.unpack(tbl, i+1) 118 -end 118 +end]] 119 119 120 120 fn.split = function(...) return fn.unpack(lib.str.explode(...)) end 121 121 122 122 fn.each = function(tbl,f) 123 123 local r = {} 124 124 for k,v in pairs(tbl) do 125 125 local v, c = f(v,k) ................................................................................ 251 251 252 252 fn.set = function(...) 253 253 local s = {} 254 254 fn.setOrD(s, ...) 255 255 return s 256 256 end 257 257 258 +fn.lerp = function(t, a, b) 259 + local r = {} 260 + for k in next, a do 261 + r[k] = lib.math.lerp(t, a[k], b[k]) 262 + end 263 + return r 264 +end 258 265 259 266 return fn
Modified src/lore.ct from [176ec871ae] to [dab20c55bd].
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 74 74 early Eluthran history was extremely warlike, and they could have easily devastated the whole of the Reach in their fanatical pursuit of competing ideologies. however, a philosophical tradition emerged from the rubble of a particularly ruinous exchange that offered the correct tools for neutering the more dangerous aspects of their intelligence -- after the centuries proved its value, the Philosophers exterminated all the remaining Eluthrai who had not adopted their practices. it was a coldblooded but rational act of genocide: an individual Eluthra is intelligent enough to bootstrap an industrial civilization from first principles with a few years of effort. an entire civilization of them, devoid of self-control? that wasn't merely a threat to the Philosophers; it was a threat to the Galaxy entire. 75 75 76 -the Eluthrai have a single common language, Eluthric, which they use in interstellar discourse and in the sciences. however, the different far-flung colonies have their own individual tongues as well. Eluthric has the largest vocabulary of any known language, with over twenty million words. an Eluthra who hasn't learned at least a million of them by adolescence is deemed slow. 76 +the Eluthrai have a single common language, Iluthanna ("Eluthric" as the Crystal Sea calls it), which they use in interstellar discourse and in the sciences. however, the different far-flung colonies have their own individual tongues as well. Eluthric has the largest vocabulary of any known language, with over twenty million words. an Eluthra who hasn't learned at least a million of them by adolescence is deemed slow. 77 77 78 78 they have developed very slowly since the Philosophers came to power, but were already so advanced that nobody is expected to exceed them any time soon. 79 79 80 80 Eluthran civilization is united under the rule of the Philosopher-King, an enlightened despot with unrestricted power, in a complex web of fealty and patronage collectively named the Corcordance of the Eluthrai. while the First Philosopher died tens of thousands of years ago, he had the foresight to prepare a successor to take his place in case of his assassination or ill-fortune. in all those years, power has changed hands only three times. the current Philosopher-King has ruled for eight thousand years. 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