starlit  Check-in [caec179da9]

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: caec179da98b03b03ac6b8b215692f175276900ccda8398e8f7d219be393ec13
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