Compare commits

...
Sign in to create a new pull request.
Author SHA1 Message Date
soul
a4be037a42 LFS lives on the Eureka forge: .lfsconfig points every clone at it
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 03:12:31 +01:00
soul
152b9251a9 Research: release pipeline prior art (Unity CI, Personal licensing, Forgejo runners and releases, versioning tools)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 23:00:49 +01:00
soul
01bed8d447 AetheriaBuild: -aetheriaTarget win64|linux64, one entry point for both players
The target names the Unity player, its executable and the runtime AetherDb is published for.
Without the argument it builds win64 as before. Built 0.2.0 for both targets on Starfire
(Linux cross-built with Mono); both stop at the aetherdb step, because Aetheria compiles
CultMath from two CultLib pins and dotnet publish refuses the duplicated shader files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 22:51:42 +01:00
soul
3f3c59e27d Dithered Particles: a Deferred pass, so particles reach the depth texture the nebula fogs by
The shader dithered and wrote depth, but its one pass had no LightMode, so under the deferred
camera it drew in the forward pass, after the depth texture is taken from the G-buffer. The
nebula march read no depth there and fogged straight over every dithered particle (missile
trails, explosions, lasers, guns). Same cause and cure as Stardust (2e9d97661): black,
unreflective and emissive in the G-buffer. The colour pass is now ForwardBase only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 11:18:37 +01:00
soul
36d9d94e81 fly: unbind the ship you leave, so its death is no longer yours
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 10:29:13 +01:00
soul
26daac398a Console command "fly <hull>": a fitted ship beside the current one, and you in it
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 10:20:44 +01:00
soul
f3927e24db Pin a docked ship to its parent before the inactive return; no flight while docked
Docking deactivates the ship, and Entity.Update returned before the parent pin, so a docked ship
kept integrating its own velocity and gravity (Ship.Update) for as long as it sat in the bay, and
undocked wherever it had fallen to at whatever speed it had reached. Drag used to bleed that
off; with no drag below top speed it flung the ship into the void.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 10:18:20 +01:00
soul
7a3c0c8fb8 Aim at infinity along the view's pitch; delete the terrain intersection
The horizon fault was the level XZ projection, not the distance (operator, 2026-10-29). The untargeted look point is 10000 along the aim again, so the camera no longer jumps where the view ray crosses a terrain discontinuity; group aim points and crosshairs lift each planar firing bearing by the aim's pitch. Reverts the surface march of aba02182b and the field-height crosshair of a9bbea509.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 08:45:37 +01:00
soul
ffdba39ac2 ThrustScale scales a thruster's push only; turning keeps authored strength
The 10x nerf reached yaw through Torque * thrust and made turning painfully slow (operator, 2026-10-29). Column and Execute apply the scale to the push; Thrust and NominalThrust read the authored stat.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 08:39:18 +01:00
soul
845f6c3aad No drag below top speed: ships coast and orbits no longer decay; drag acts only on the excess over the hull's top speed
Carries out aetheria-gravity:ruling:no-drag-below-top-speed (2026-10-14), which the code never implemented (operator, 2026-10-29). The VelocityLimit clamp still caps speed; the soft cap is the ruling's remaining half.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 08:33:26 +01:00
soul
e0c1daa4ec Thrust is a tenth of authored, the ratio gravity fell by; ThrustScale and DragScale are global flight tuning in GameplaySettings
Most ships cannot climb straight out of a well and use orbits; an overspecced hull like the Longinus still can (operator direction, 2026-10-29). DragScale defaults to 1, unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 08:30:48 +01:00
soul
c6f7e0c8a4 Player turn input is banded by thruster: each band of equal yaw-to-translation ratio fills an equal share of the turn axis
The allocator already serves a turn by the best-ratio thrusters first; the axis was a fraction of total yaw, so a hull whose best-ratio pair holds little of it (the Longinus's front pair) saturated them in the first tenth of input and lurched on its drives. Ship.BandedTurn maps the player's axis so each band joins over an equal share of travel; full input is still full yaw. AI and the allocator are unchanged (operator request, 2026-10-29).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 08:22:04 +01:00
soul
633ae3353a Keyboard turning ramps down on release at the same rate it ramps up, so tapping at a rate holds a gentle turn; reversal still cuts to zero
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 08:09:29 +01:00
soul
5e2a9efc8a Keyboard turning ramps to full over KeyboardTurnRampTime (0.4 s) so a tap feathers the turn; analog turn passes straight through
Release or reversal starts the ramp again from zero (operator request, 2026-10-29: the Longinus lurches).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 08:07:30 +01:00
soul
562ba5c8eb Default weapon-group binding runs once per ship, on its first use; after that the action bar is the player's
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 08:01:29 +01:00
soul
c4911a4c96 Action bar binds weapon groups by default: mouse left/right/middle fire groups 1-3, number key N fires group N
Only slots the input settings define, only groups with weapons, and never over gear or consumables the player placed. Runs on binding to a ship and after a save's bindings restore. Replaces a lazy Zip that never ran (operator request, 2026-10-29).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 07:57:42 +01:00
soul
2e9d97661c Stardust draws into the G-buffer so the nebula fog attenuates it by depth
At BeforeImageEffectsOpaque the dust was drawn after the depth texture the nebula march reads, so the fog treated dust pixels as far background and wiped them out (operator report, 2026-10-29). A Deferred pass writes black, unreflective, emissive G-buffer texels and depth at AfterGBuffer. FlowFieldSceneVerify still expects the old event and needs updating.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 07:47:20 +01:00
soul
aba02182b7 Untargeted aim lands where the view ray meets the field, not 10000 out at the horizon; weapon-group aim points ride the field
With no target the look point sat 10000 along the camera direction, so crosshairs and shot visuals aimed at the horizon (operator report, 2026-10-29).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 07:44:56 +01:00
soul
a9bbea5091 Weapon crosshairs ride the gravity field: the aim point takes its own field height, not the ship's level plane
On a slope the level projection floated over downhill aim and sank into uphill aim (operator report, 2026-10-29).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 07:33:28 +01:00
soul
293e3a33fd Research: hyperbolic in-between space (H2 rendering, motion and precision, curved Kepler, HyperRogue scaffolding, getting lost, Aetheria's Euclidean gravity body)
Serves the operator's musing on hyperbolic inter-zone travel, 2026-10-28.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 21:22:25 +01:00
soul
257a34939e Delete Vault/: a stale 2025 copy of the lore; the live lore is GameCult/AetheriaLore
The copy had drifted far behind (no Megiddo or Wavecrafters pages) and an agent
edited it as if it were canon on 2026-10-28.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 06:36:54 +01:00
soul
6e5be44613 Revert lore merge 28c076b1e: it edited the stale Vault copy in this repo; the live lore is GameCult/AetheriaLore
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 06:35:25 +01:00
soul
28c076b1e6 Merge lore: cognition-anchored aether, ARI spectrum, Megiddo doctrine (operator direction 2026-10-28)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 06:29:16 +01:00
soul
c4a2ce36be Lore: the aether is anchored by its residents' cognition; ARIs are a spectrum; Megiddo's doctrine
Aether gains what the field answers to (consensus, load-bearing belief, a self not
compute, unopposed small effects, density, attention). Necrotech gains the ARI
spectrum from factory stamps to bound espers. Megiddo gets a page: right about the
mechanism, wrong that there is a way back. Pseudospace ties flatness to consensus.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 06:29:08 +01:00
soul
8fa1fc21b2 Merge one exit per region: after boss placement each boss zone keeps only its exit-path link into the far side; distances recomputed before faction homes; saves load the borders they stored (aetheria-living-world). Soul held s1 and s2; Unity batch compile clean on Starfire at 2822a583e.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 21:33:40 +01:00
soul
2822a583ea Pin CrossingLinks far-zone ordering and a boss zone at the start of the exit path in hand-built graphs (cut one-exit, finding pure-helper-survivors)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 20:24:02 +01:00
soul
97aa371872 Provinces.ExtraExits throws when a boss zone has no exit-path link to keep instead of silently keeping every link (cut one-exit, finding silent-keep-all-fallback)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 20:23:47 +01:00
soul
9fd0fe96e7 One-exit tests require every sector seed to build; a generator exception fails instead of skipping the seed (cut one-exit, finding trybuild-masks-generator-crash)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 20:23:32 +01:00
soul
0182570200 DistancesAreOfTheFinalGraph builds its own galaxies so the pruning's distance recompute is under its coverage (cut one-exit)
Stryker's per-test coverage credited the shared galaxy cache's first builder
alone with Galaxy.OneExitPerRegion's CalculateDistanceMatrix call, so deleting
the call survived. Fixture fix, no rule change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 18:50:45 +01:00
soul
84acc9e563 Galaxy.OneExitPerRegion: sector generation prunes each boss zone's extra exits, then recomputes distances (cut one-exit)
Ruling one-exit-per-region: one exit to defend per region. Called once in
PlaceFactionsMain after the boss loop and before home placement; BuildProvinces
and the pruning share ZoneGraph. Saves and the prelude never prune. Tests pin the
rule on sector seeds 1-60, the pure helpers on hand-built graphs, and save/load.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 17:28:09 +01:00
soul
5df9960343 Provinces.ExtraExits: the crossing links to prune so a boss zone keeps the one on the exit path (cut one-exit)
Pure. Reads CrossingLinks; nothing calls it yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 17:28:08 +01:00
soul
4f93c0de68 Provinces.CrossingLinks: one definition of a boss zone's crossing link, Regions reads it (cut one-exit)
Moves Regions' inline exit-side loop into a public CrossingLinks so the
pruning that follows reads the same definition. Behaviour unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 17:25:19 +01:00
soul
bef6a90f58 Merge ore geography: provinces inside boss-bounded regions, region decks deal province types, field kinds and yield read the province (aetheria-living-world). Unity batch compile clean on Starfire at the merge; Soul held.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 17:11:49 +01:00
soul
674baa95ae Merge eureka/aetheria-living-world-economy-model: living-world Body map, economy section
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 12:16:36 +01:00
soul
3e98238cf0 Living-world map: the economy's catalog records, owners and checks after recipe role inputs
Owner, inputs, outputs, derived state, forbidden writers, shared paths and
verification layer for RecipeData, Recipes.RoleInputsOf/Fillers and
EconomyChecks, read at 30030af6e. Notes the two stores of a recipe's role
slots behind one reader, filler choice at production time as unowned, and
stale docs: the target prose is not on master and target r2's L-recipe
wording predates intermediate role inputs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 12:16:07 +01:00
soul
30030af6e4 Merge eureka/aetheria-living-world-recipe-role-inputs: recipes take role inputs any filler satisfies; offered designs demand their roles
Soul s1 (aetheria-living-world:verdict:cut-recipe-role-inputs.s1) holds all five promises; its two Medium findings are pre-existing test races, deferred to their follow-ups. Suite 2867/0 at e93f53166; shipped report 146 faults (consumer demand: Ballistic Powder, Nitric Acid, Nitrocellulose, Potassium Nitrate). Unity compile not yet run; it rides with the ore-geography compile.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 12:11:20 +01:00
soul
e93f531669 Economy tests: a null list beside RoleInputs does not drop them
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 11:44:30 +01:00
soul
1efd8343da Economy tests: pin the survivors of the diff (null RoleInputs keeps the rest, key-ordered recipe-less designs, unoffered recipe designs name roles, mixed takes line)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 11:30:14 +01:00
soul
ccd9792d2f Economy tests: role inputs persist, are read by every rule as any filler, offered recipe-less designs demand their roles
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 10:56:21 +01:00
soul
3fb5e0e0e6 Economy: RecipeData.RoleInputs, Recipes.RoleInputsOf as the one reader of role slots, checks read a role input as any filler and offered recipe-less designs as demanding their roles
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 10:56:01 +01:00
soul
1ed3579025 Merge eureka/aetheria-living-world-economy-checks: consumer demand from shared uses, a weapon's draw is a sink, upkeep walked, economy command tail
Soul s1 (aetheria-living-world:verdict:cut-economy-checks.s1) holds; s2 holds the fixes except one Low (dry-run test cannot see apply), filed as a follow-up. Suite 2848/0 at dc4838bd9; shipped report 144 faults, consumer demand nitric-acid only. Unity compile not run (no Unity-side change).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 10:29:05 +01:00
soul
dc4838bd9e Economy checks: upkeep walk test gets a control case and a baseline that is actually shared
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 10:07:40 +01:00
soul
3c3ecd40da AetherDb economy: pin Run's report and author dry-run branches on the shipped catalog
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 10:06:17 +01:00
soul
da067a34a2 Economy checks: the consumer walk follows upkeep edges; pin upkeep into a consumer good and a role filler
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 10:06:15 +01:00
soul
6ba77aafe7 OreGeographyTests: restore the file the previous commit replaced with a stale copy; the three sector-galaxy tests and their helpers on top of the guard test
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 10:00:42 +01:00
soul
154b71160d OreGeographyTests: a multi-region sector galaxy's provinces survive save and continue and re-derive from an older save
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 09:59:24 +01:00
soul
4b7e5476c0 FieldKinds.Assign: a kind needs a positive kind weight and a positive province factor; two negatives no longer draw
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 09:59:15 +01:00
soul
8e6fd43d23 Economy checks: ConsumerDemand tests recipes, not keys; pin upkeep reached through a chain
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 09:32:13 +01:00
soul
8d23a378d6 Economy checks: gearRoles reads roles of non-commodity recipes; pin use order and batch list
Mutation testing found IsRoleRecipe redundant in the gear-role set (RolesOf is empty for a
design without roles) and the message's use order, join and the unknown-batch count unpinned.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 09:19:00 +01:00
soul
1db250fb10 OreGeographyTests: the duplicate-listing fixture's yield-only kind is never drawn
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 08:55:07 +01:00
soul
8774bd8a97 OreGeographyTests: pin seed order, tie-breaks, duplicate listings, deal key order, kept and saved first-load kind
Kills the survivors of the first Stryker run on FieldKinds and Provinces that were not equivalent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 08:50:01 +01:00
soul
c81a83ef2c OreGeographyTests: regions, partition, deal, yields, belts, save
Not yet run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 06:43:00 +01:00
soul
4848c5332f Ore geography: yields, provinces cut inside boss-zone regions, types dealt per region
FieldYield and FieldKindData.Yields (key 3), ProvinceData, FieldKinds.Pick/Assign/Ensure/Yield
with a province type, Provinces.Regions/Partition/Deal, Galaxy.Provinces built once by the
generation and save constructors, SavedGame key 15 and SavedZone key 6, ProvinceZones setting.
Not yet built or run; tests follow in the next commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 06:42:58 +01:00
soul
d92d77483f Retire BodyData.Resources key 4 and the commented ResourceValue
No reader or writer remains; FieldKinds.Yield (this cut) is the owner of a belt's yield.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 06:34:12 +01:00
soul
b375cfc8ec Research: ore geology and regional resources prior art (Dwarf Fortress, EVE, Elite/X4/Starsector, real asteroid taxonomy), for ruling ores-are-regional
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 05:11:18 +01:00
soul
26bea2cb14 AetherDb economy: unknown batch names are not echoed; held-cycle rise is relative
Run takes the output writer like Report and Author do. The unknown-batch refusal says how
many names matched no batch and lists the compiled batches. Introduced compares a held
cycle's radius relatively with RiseNoise = 1e-12, replacing the bare absolute 1e-9.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 04:38:08 +01:00
soul
7df0a0943f Economy checks: consumer demand derived from shared uses; a weapon's draw is a sink
ConsumerDemand drops the two-or-three-step window (rulings consumer-demand-shared-only,
consumer-demand-skips-final-assemblies): a processed good owes a consumer use when it is
shared, derived from the recipe graph each check. A weapon's draw of a round is one use
(ruling draw-is-one-use). SourceSink counts a drawn commodity as sunk. Drawn is the one
reader of offered designs' WeaponData.AmmoType. Tests are unrun at this commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 04:38:05 +01:00
soul
c6a49e5568 Merge eureka/aetheria-living-world-economy-content-b1: economy batch 1 (Zhestokost munitions) applied; one owner of drawable station hulls
Operator approved the batch (aetheria-living-world:ruling:batch-1-munitions-approved). Soul s2 holds; suite 2836/0 at the merged head; catalog LFS oid 68b37cb5 on GitHub and the Yggdrasil cache.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 19:09:35 +02:00
soul
dd1031ce6a StationKindTests: a maker-less or Price 0 station hull is unreachable by the draw; the stand-in check is exercised on Zhestokost
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-23 18:16:48 +02:00
soul
b481363b9b StationKinds.HullKindFor takes a stand-in's kind only when some maker offers it
The any-maker fallback in Choose could seat a faction on a kind it has a dead stand-in for; the hull draw then looked for the dead stand-in's kind and returned
null. A stand-in no maker offers is no longer followed, so a kind Choose places is a kind the hull draw can fill. FieldableKindsOf reads the same two helpers.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-23 18:04:24 +02:00
soul
e3824360aa StationKindTests: stand-ins judged from the catalog, zones test against an oracle, the partial and fallback-spread cases pinned
Deletes the name-keyed AwaitingFirstModel exemption: AssertStandInsLive derives liveness from the sold hulls (live exactly when some maker sells the
target kind; otherwise the kind leaves the pool and the any-maker fallback is observed in the log). GeneratedZonesPlaceStationsByKind checks against
a records-level oracle and asserts the fallback case. New: a kind is kept exactly when a hull of it can be drawn (Price 0, no maker), a faction with
one fieldable and one unfieldable kind places only the first, the fallback spreads over every offered kind.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-23 18:02:48 +02:00
soul
e403905e12 LoadoutGenerator.IsDrawable is the one definition of 'a maker offers this design'; StationKinds keeps a kind by it
AvailableProducts (Price > 0, a set Manufacturer, offered) and StationKinds.OfferedKinds (offered only) disagreed, so Choose could keep a kind
whose hull draw returns null. Both now ask IsDrawable; OfferedKinds and OfferedStationHulls read the same drawable station products.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-23 18:02:47 +02:00
soul
78b176a0ff Merge remote-tracking branch 'origin/master' into eureka/aetheria-living-world-economy-content-b1 2026-10-23 17:58:47 +02:00
soul
d5c0063b7e CommodityRecordsTests: the compound commodity count follows the batch (51 to 63)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-23 17:29:50 +02:00
soul
b1b5031e4b Economy batch 1 (munitions): apply Munitions to the shipped catalog; re-pin what the catalog stales
economy author Munitions apply: 49 added, 4 changed. The pins that name the shipped catalog's contents follow it: the kind list gains
munitions-works, the simple commodity count, the retirement check counts only its own stand-in, the stand-in liveness check exempts
a faction awaiting its first station model (Zhestokost), and generated stations draw from the fieldable kinds.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-23 17:23:26 +02:00
soul
fce8c66284 StationKinds: FieldableKindsOf is the pool Choose draws from
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-23 17:23:24 +02:00
soul
fe7ea72c9b StationKinds.Choose: a kind the faction places but no maker can field leaves its pool and falls back to any maker
A stand-in whose hull kind no offered station hull carries (Zhestokost's depot, whose model has not landed) used to leave the
tutorial entrance with no station. The existing no-kinds fallback now covers it: one rule, no faction special case.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-23 17:20:04 +02:00
soul
37677c4893 Research: recipe-graph structure in factory games (shared vs single-use intermediates)
Prior art for aetheria-living-world❓consumer-demand-gear-specific-intermediates: Factorio 1.1 and Space Age, Dyson Sphere Program, Satisfactory, No Man's Sky.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 17:05:31 +02:00
soul
722e618cac Merge eureka/aetheria-arena-sim-rate h7: Combat releases every held weapon it no longer commands, beams and locks included
Soul pass 5 (aetheria-arena:verdict:cut-sim-rate.s5) holds all five promises. Unity compile and feel unproven.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 16:00:00 +02:00
soul
5177154c17 Economy batch 1: Zhestokost munitions chain (unapplied, for operator review)
Dry run on the composed catalog: zero shape faults introduced; 49 records
added, 4 changed. Review page published separately.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 12:45:20 +02:00
197 changed files with 3526 additions and 7099 deletions

1
.gitignore vendored
View file

@ -95,7 +95,6 @@ Packages/packages-lock.json
GameData/run.cc
GameData/player.cc
GameData/RemovedMods/
Vault/Aetheria/.obsidian/plugins/copilot/data.json
# Agent scratch, sandboxed toolchain caches, and throwaway checkouts. These had
# grown to 47 untracked entries; the worktree must stay legible, so they never

2
.lfsconfig Normal file
View file

@ -0,0 +1,2 @@
[lfs]
url = https://eureka.gamecult.org/forge/GameCult/Aetheria.git/info/lfs

View file

@ -8,12 +8,12 @@ using UnityEditor.AddressableAssets.Settings;
using UnityEditor.Build.Reporting;
using Debug = UnityEngine.Debug;
// The one way to make a Win64 player directory. Owns what the directory contains: Aetheria.exe and its data,
// The one way to make a player directory, for Windows or Linux. Owns what the directory contains: the player and its data,
// GameData/Aetheria.cc, GameData/Narrative/**, GameData/Mods/** and ModTools/AetherDb. Manual builds, smokes and
// release cuts all call this and stage nothing themselves.
//
// Unity -batchmode -quit -projectPath <repo> -executeMethod AetheriaBuild.Build
// -aetheriaVersion <semver> -aetheriaOut <dir> -logFile <log>
// -aetheriaVersion <semver> -aetheriaOut <dir> [-aetheriaTarget win64|linux64] -logFile <log>
//
// AetherDb is published from tools/AetherDb, so the headless build's CULTLIB_ROOT / CULTMATH_ROOT (Directory.Build.props)
// must be set for dotnet when the defaults do not hold the pinned revisions.
@ -22,6 +22,14 @@ public static class AetheriaBuild
{
private static readonly Regex SemVer = new Regex(@"^\d+\.\d+\.\d+([-+][0-9A-Za-z.-]+)?$");
// A target: the player Unity builds, its executable, and the runtime AetherDb is published for.
private static (BuildTarget target, string player, string runtime, string aetherDb) Target(string name) => name switch
{
"win64" => (BuildTarget.StandaloneWindows64, "Aetheria.exe", "win-x64", "AetherDb.exe"),
"linux64" => (BuildTarget.StandaloneLinux64, "Aetheria.x86_64", "linux-x64", "AetherDb"),
_ => throw new InvalidOperationException($"-aetheriaTarget {name}: expected win64 or linux64"),
};
public static void Build()
{
var step = "arguments";
@ -30,6 +38,7 @@ public static class AetheriaBuild
var version = Argument("-aetheriaVersion");
if (!SemVer.IsMatch(version)) throw new InvalidOperationException("-aetheriaVersion is not a semver");
var output = Path.GetFullPath(Argument("-aetheriaOut"));
var target = Target(Argument("-aetheriaTarget", "win64"));
var repoRoot = Directory.GetParent(UnityEngine.Application.dataPath).FullName;
step = "version";
@ -46,8 +55,8 @@ public static class AetheriaBuild
var report = BuildPipeline.BuildPlayer(new BuildPlayerOptions
{
scenes = scenes,
locationPathName = Path.Combine(output, "Aetheria.exe"),
target = BuildTarget.StandaloneWindows64,
locationPathName = Path.Combine(output, target.player),
target = target.target,
options = BuildOptions.None,
});
if (report.summary.result != BuildResult.Succeeded || report.summary.totalErrors != 0)
@ -57,7 +66,7 @@ public static class AetheriaBuild
PlayerStaging.Stage(repoRoot, output);
step = "aetherdb";
Publish(repoRoot, Path.Combine(output, "ModTools"));
Publish(repoRoot, Path.Combine(output, "ModTools"), target.runtime, target.aetherDb);
step = "burst";
var burst = Path.Combine(output, "Aetheria_BurstDebugInformation_DoNotShip");
@ -74,11 +83,11 @@ public static class AetheriaBuild
EditorApplication.Exit(0);
}
private static void Publish(string repoRoot, string modTools)
private static void Publish(string repoRoot, string modTools, string runtime, string aetherDb)
{
var info = new ProcessStartInfo("dotnet")
{
Arguments = $"publish tools/AetherDb -c Release -r win-x64 --self-contained -p:PublishSingleFile=true -o \"{modTools}\"",
Arguments = $"publish tools/AetherDb -c Release -r {runtime} --self-contained -p:PublishSingleFile=true -o \"{modTools}\"",
WorkingDirectory = repoRoot,
UseShellExecute = false,
RedirectStandardOutput = true,
@ -92,15 +101,16 @@ public static class AetheriaBuild
if (process.ExitCode != 0)
throw new InvalidOperationException($"dotnet publish exited {process.ExitCode}:\n{text}\n{errors.Result}");
}
if (!File.Exists(Path.Combine(modTools, "AetherDb.exe")))
throw new InvalidOperationException("dotnet publish produced no ModTools/AetherDb.exe");
if (!File.Exists(Path.Combine(modTools, aetherDb)))
throw new InvalidOperationException($"dotnet publish produced no ModTools/{aetherDb}");
}
private static string Argument(string name)
private static string Argument(string name, string fallback = null)
{
var args = Environment.GetCommandLineArgs();
var at = Array.IndexOf(args, name);
if (at < 0 || at + 1 >= args.Length) throw new InvalidOperationException(name + " is required");
if (at < 0 || at + 1 >= args.Length)
return fallback ?? throw new InvalidOperationException(name + " is required");
return args[at + 1];
}
}

View file

@ -201,6 +201,10 @@ public class ActionGameManager : MonoBehaviour
private float3 _viewDirection;
// The player's face-the-aim state (Left Shift held, Caps Lock latched); Ship.Turn is its only output.
private readonly HelmInput _helm = new HelmInput();
// Seconds a held turn key takes to reach full turn rate.
public float KeyboardTurnRampTime = .4f;
// Whether the turn axis was last driven by keys; kept while released so the ramp down still applies.
private bool _turnDigital;
private TextMeshProUGUI _faceAimMark;
private (HardpointData[] hardpoints, Transform[] barrels, PlaceUIElementWorldspace crosshair)[] _articulationGroups;
// The outgoing progress indicators on the player's target: one per lock weapon (its Lock, shown only on a hostile target)
@ -224,6 +228,10 @@ public class ActionGameManager : MonoBehaviour
private bool _menuShown;
private List<ActionBarSlot> _actionBarSlots = new List<ActionBarSlot>();
private List<InputAction> _actionBarActions = new List<InputAction>();
// Each action bar slot's control path, index for index with _actionBarSlots.
private List<string> _actionBarPaths = new List<string>();
// Ships whose action bar has been set up: the default weapon-group binding runs once per ship, then the bar is the player's.
private HashSet<Entity> _actionBarSetUp = new HashSet<Entity>();
private float _hitMarkerTime;
public AetheriaInput Input { get; private set; }
@ -437,6 +445,7 @@ public class ActionGameManager : MonoBehaviour
var slot = Instantiate(ActionBarSlot, ActionBar);
slot.Binding = null;
_actionBarSlots.Add(slot);
_actionBarPaths.Add(controlPath);
action.started += context => slot.Binding?.Activate();
action.canceled += context => slot.Binding?.Deactivate();
@ -495,16 +504,6 @@ public class ActionGameManager : MonoBehaviour
StartGame();
//if (!PlayerSettings.InputSettings.ActionBarInputs.Any())
{
var newbinds = Enumerable.Range(0, 64)//CurrentEntity.WeaponGroups
.Zip(
bindings,
(i, slot) =>
slot.Binding = new ActionBarWeaponGroupBinding(CurrentEntity, slot, i)
);
}
ConsoleController.AddCommand("revealzones",
_ =>
{
@ -578,6 +577,33 @@ public class ActionGameManager : MonoBehaviour
ItemManager.Random.NextFloat2Direction() * ItemManager.Random.NextFloat(50, 500);
Zone.Admit(turret, piloted: false);
});
// "fly Djinni": a freshly fitted ship of the named hull beside the current one, and you in it. The ship you leave
// stays where it is, unpiloted.
ConsoleController.AddCommand("fly",
args =>
{
var hullName = string.Join(" ", args);
if (_currentEntity == null || _currentEntity.Parent != null)
{
ConsoleController.Instance.AppendLogLine("Refused: undock first");
return;
}
var generator = new LoadoutGenerator(ref ItemManager.Random, ItemManager, CurrentGalaxy, Zone.GalaxyZone, null, 2);
var pack = generator.GenerateShipLoadout(data => string.Equals(data.Name, hullName, StringComparison.InvariantCultureIgnoreCase));
if (pack == null)
{
ConsoleController.Instance.AppendLogLine($"No ship hull named \"{hullName}\"");
return;
}
var ship = (Ship) EntitySerializer.Unpack(ItemManager, Zone, pack);
ship.IsPlayerShip = true;
ship.Direction = _currentEntity.Direction;
ship.Position.xz = _currentEntity.Position.xz + _currentEntity.Direction.Rotate(ItemRotation.Clockwise) * 30;
ship.Velocity = _currentEntity.Velocity;
Zone.Admit(ship, piloted: false);
UnbindEntity(); // or the ship you leave still owns your death
BindToEntity(ship);
});
//Temporary, or not
ConsoleController.AddCommand("tow", _ => TowShip());
@ -995,6 +1021,35 @@ public class ActionGameManager : MonoBehaviour
if (DebugInfoText != null) DebugInfoText.text = "";
}
// The weapon group a slot's input fires by default: mouse left, right and middle fire groups 1-3, and number key N fires group N.
private static int DefaultWeaponGroup(string controlPath) => controlPath switch
{
"<Mouse>/leftButton" => 0,
"<Mouse>/rightButton" => 1,
"<Mouse>/middleButton" => 2,
_ when controlPath.StartsWith("<Keyboard>/") && int.TryParse(controlPath.Substring("<Keyboard>/".Length), out var n) && n >= 1 => n - 1,
_ => -1
};
// A new ship's first action bar: each slot's default weapon group when the group has weapons. Gear and consumables the
// player placed are kept; a slot holding another ship's weapon group is rebound to this ship, or cleared when its default
// group here is empty.
private void AutoBindWeaponGroups()
{
if (CurrentEntity?.WeaponGroups == null) return;
for (var i = 0; i < _actionBarSlots.Count; i++)
{
var slot = _actionBarSlots[i];
if (slot.Binding != null && slot.Binding is not ActionBarWeaponGroupBinding) continue;
var group = DefaultWeaponGroup(_actionBarPaths[i]);
if (group < 0) continue;
if (group < CurrentEntity.WeaponGroups.Length && CurrentEntity.WeaponGroups[group].weapons.Any())
slot.Binding = new ActionBarWeaponGroupBinding(CurrentEntity, slot, group);
else if (slot.Binding is ActionBarWeaponGroupBinding)
slot.Binding = null;
}
}
private void BindToEntity(Entity entity)
{
if (!ZoneRenderer.EntityInstances.ContainsKey(entity))
@ -1045,6 +1100,10 @@ public class ActionGameManager : MonoBehaviour
crosshair.gameObject.SetActive(false);
foreach (var group in _articulationGroups)
group.crosshair.gameObject.SetActive(true);
// A loaded run's bar is restored after this and replaces it, so a saved setup is never overwritten.
if (_actionBarSetUp.Add(CurrentEntity))
AutoBindWeaponGroups();
_shipSubscriptions.Add(CurrentEntity.Target.Subscribe(target =>
{
@ -1307,7 +1366,10 @@ public class ActionGameManager : MonoBehaviour
{
ship.Command.Movement = Input.Player.Move.ReadValue<Vector2>().ToCultMath();
_helm.Held = Input.Player.FaceAim.IsPressed();
ship.Command.Turn = _helm.Demand(_viewDirection.xz, Input.Player.Turn.ReadValue<float>());
if (Input.Player.Turn.activeControl is { } turnControl)
_turnDigital = turnControl.device is UnityEngine.InputSystem.Keyboard;
var turnAxis = _helm.TurnAxis(Input.Player.Turn.ReadValue<float>(), _turnDigital, Time.deltaTime, KeyboardTurnRampTime);
ship.Command.Turn = _helm.Demand(_viewDirection.xz, ship.BandedTurn(turnAxis));
}
UpdateMusic();
@ -1435,11 +1497,18 @@ public class ActionGameManager : MonoBehaviour
else if (target.Entity != null)
TargetIndicator.Target = DrawAhead.Position(target.Entity, lead).ToUnity();
var distance = length(ViewDot.Target.ToCultMath() - ownDrawn);
// The aim's pitch, applied to each barrel's planar bearing: the crosshair sits where the guns point at the view's
// elevation, not on the ship's level plane (which is the horizon).
var aim = CurrentEntity.Aim;
var aimLevel = length(aim.xz);
foreach (var (_, barrels, crosshair) in _articulationGroups)
{
var averagePosition = Vector3.zero;
foreach (var barrel in barrels)
averagePosition += barrel.position + barrel.forward * distance;
{
var bearing = new Vector2(barrel.forward.x, barrel.forward.z).normalized;
averagePosition += barrel.position + new Vector3(bearing.x * aimLevel, aim.y, bearing.y * aimLevel) * distance;
}
averagePosition /= barrels.Length;
crosshair.Target = averagePosition;
}

View file

@ -408,6 +408,9 @@ public class EntityInstance : MonoBehaviour
var aimDistance = !Entity.Target.Value.IsNone
? max(Entity.TargetRange, Entity.ItemManager.GameplaySettings.ConvergenceMinimumDistance)
: 10000;
// The aim's pitch: a planar firing bearing is drawn at the same elevation as the view, so a group aims where the
// player looks, not at the horizon of the ship's plane.
var aimLevel = length(Entity.Aim.xz);
LookAtPoint.position = transform.position + Entity.Aim.ToUnity() * aimDistance;
foreach (var group in _groupAims)
{
@ -415,7 +418,7 @@ public class EntityInstance : MonoBehaviour
if (weapon == null) { group.Value.position = LookAtPoint.position; continue; }
var barrel = Barrels.TryGetValue(weapon.Item.Hardpoint, out var barrels) ? barrels[0].position : transform.position;
var direction = FireControl.Solution(weapon, Entity, Entity.Target.Value).Direction;
group.Value.position = barrel + new Vector3(direction.x, 0, direction.y) * aimDistance;
group.Value.position = barrel + new Vector3(direction.x * aimLevel, Entity.Aim.y, direction.y * aimLevel) * aimDistance;
}
if (_influenceInstance)
_influenceInstance.position = new Vector3(drawn.x, 0, drawn.z);

View file

@ -7,7 +7,7 @@ using GameCult.Caching.MessagePack;
// Aetheria's stores: one home store per type, composed here and nowhere else.
public static class AetheriaStores
{
public static readonly Type[] CatalogTypes = { typeof(ItemData), typeof(ShipAuthoring), typeof(Faction), typeof(FactionProductData), typeof(PersonalityAttribute), typeof(InputLayout), typeof(Loadout), typeof(FieldKindData), typeof(VerseGrammar), typeof(VerseVerb), typeof(StationKindData), typeof(RecipeData), typeof(OutsideMarketData) };
public static readonly Type[] CatalogTypes = { typeof(ItemData), typeof(ShipAuthoring), typeof(Faction), typeof(FactionProductData), typeof(PersonalityAttribute), typeof(InputLayout), typeof(Loadout), typeof(FieldKindData), typeof(ProvinceData), typeof(VerseGrammar), typeof(VerseVerb), typeof(StationKindData), typeof(RecipeData), typeof(OutsideMarketData) };
public static readonly Type[] RunTypes = { typeof(OrbitData), typeof(BodyData), typeof(SavedZone), typeof(SavedGame), typeof(ProvenanceLedger) };
public static readonly Type[] PlayerTypes = { typeof(PlayerSettings) };

View file

@ -48,6 +48,7 @@ public class Thruster : Behavior, IPowerConsumer
public float Thrust => Evaluate(_data.Thrust);
// Thrust at a full grant, the same stat read without the power-supply term: what a column promises. Live, like Thrust.
public float NominalThrust => EvaluateNominalPower(_data.Thrust);
// GameplaySettings.ThrustScale scales the push alone: it tunes how hard ships fight gravity, and turning does not.
public float Torque { get; }
// This thruster's effect per unit throttle at the given thrust, in the body frame: xy is the push (x starboard, y
@ -56,7 +57,7 @@ public class Thruster : Behavior, IPowerConsumer
public float3 Column(float thrust)
{
if (Item == null || !Item.Active.Value) return default;
var push = -float2(0, 1).Rotate(Item.EquippableItem.Rotation) * thrust / Entity.Mass;
var push = -float2(0, 1).Rotate(Item.EquippableItem.Rotation) * thrust * ItemManager.GameplaySettings.ThrustScale / Entity.Mass;
return float3(push.x, push.y, Torque * thrust * ItemManager.GameplaySettings.TorqueMultiplier / Entity.Mass);
}
@ -117,7 +118,7 @@ public class Thruster : Behavior, IPowerConsumer
if(_input > 0f && Item.PowerSupply > 1e-4f)
{
var thrust = Thrust;
Entity.Velocity -= Direction.xz * _input * thrust / Entity.Mass * dt;
Entity.Velocity -= Direction.xz * _input * thrust * ItemManager.GameplaySettings.ThrustScale / Entity.Mass * dt;
var turnRate = _input * Torque * thrust * ItemManager.GameplaySettings.TorqueMultiplier / Entity.Mass;
Entity.Direction = mul(Entity.Direction, CultMath.float2x2.Rotate(turnRate * dt));
Entity.TurnRate += turnRate;

View file

@ -79,7 +79,8 @@ public static class EconomyChecks
StoredRecipes(catalog).Select(pair => (pair.Key, Sound(pair.Recipe)));
private static RecipeData Sound(RecipeData recipe) =>
recipe.Inputs != null && recipe.Upkeep != null && recipe.Fills != null && !recipe.Inputs.Contains(null) && !recipe.Upkeep.Contains(null) && !recipe.Fills.Contains(null)
recipe.Inputs != null && recipe.Upkeep != null && recipe.Fills != null && recipe.RoleInputs != null
&& !recipe.Inputs.Contains(null) && !recipe.Upkeep.Contains(null) && !recipe.Fills.Contains(null) && !recipe.RoleInputs.Contains(null)
? recipe
: new RecipeData
{
@ -87,9 +88,28 @@ public static class EconomyChecks
StationKind = recipe.StationKind, LoreHook = recipe.LoreHook,
Inputs = (recipe.Inputs ?? new List<CommodityCount>()).Where(c => c != null).ToList(),
Upkeep = (recipe.Upkeep ?? new List<CommodityCount>()).Where(c => c != null).ToList(),
Fills = (recipe.Fills ?? new List<string>()).Where(f => f != null).ToList()
Fills = (recipe.Fills ?? new List<string>()).Where(f => f != null).ToList(),
RoleInputs = (recipe.RoleInputs ?? new List<ItemRole>()).Where(r => r != null).ToList()
};
// The designs some faction offers, but a Station or Turret hull, distinct and ordered by key: the designs that need a recipe.
private static List<ItemData> OfferedDesigns(CultCache catalog)
{
var designs = new Dictionary<string, ItemData>(StringComparer.Ordinal);
foreach (var product in catalog.GetAll<FactionProductData>().Where(p => LoadoutGenerator.IsOffered(catalog, p)))
{
var design = catalog.Get(product.Design);
if (design == null || (design is HullData hull && (hull.HullType == HullType.Station || hull.HullType == HullType.Turret))) continue;
designs.TryAdd(catalog.RefOf(design).Key.Value, design);
}
return designs.OrderBy(pair => pair.Key, StringComparer.Ordinal).Select(pair => pair.Value).ToList();
}
// The roles some design demands: those of every offered design and of every design a recipe makes, recipe or no.
private static HashSet<string> DesignRoles(CultCache catalog) =>
new HashSet<string>(OfferedDesigns(catalog).Concat(AllRecipes(catalog).Select(p => catalog.Get(p.Recipe.Output)).Where(o => o != null && !Recipes.IsCommodity(o)))
.SelectMany(design => Recipes.RolesOf(design)).Select(role => role.Name), StringComparer.Ordinal);
private static IEnumerable<ItemData> Commodities(CultCache catalog) =>
catalog.GetAll<ItemData>().Where(Recipes.IsCommodity).OrderBy(item => catalog.RefOf(item).Key.Value, StringComparer.Ordinal);
@ -101,15 +121,9 @@ public static class EconomyChecks
foreach (var item in Commodities(catalog))
if (Recipes.RecipeOf(catalog, item) == null)
faults.Add(new EconomyFault(LRecipe, catalog.RefOf(item).Key.Value, $"Commodity '{item.Name}' has no recipe.", pending: true));
var seen = new HashSet<string>(StringComparer.Ordinal);
foreach (var product in catalog.GetAll<FactionProductData>().Where(p => LoadoutGenerator.IsOffered(catalog, p)))
{
var design = catalog.Get(product.Design);
if (design == null || (design is HullData hull && (hull.HullType == HullType.Station || hull.HullType == HullType.Turret))) continue;
var key = catalog.RefOf(design).Key.Value;
if (seen.Add(key) && Recipes.RecipeOf(catalog, design) == null)
faults.Add(new EconomyFault(LRecipe, key, $"Offered design '{design.Name}' has no recipe.", pending: true));
}
foreach (var design in OfferedDesigns(catalog))
if (Recipes.RecipeOf(catalog, design) == null)
faults.Add(new EconomyFault(LRecipe, catalog.RefOf(design).Key.Value, $"Offered design '{design.Name}' has no recipe.", pending: true));
foreach (var market in catalog.GetAll<OutsideMarketData>())
{
var marketKey = catalog.RefOf(market).Key.Value;
@ -125,6 +139,8 @@ public static class EconomyChecks
else if (list.Contains(null)) Add(key, $"{name} holds a null entry.");
if (stored.Fills == null) Add(key, "Fills is null; a recipe holds a list, empty when it fills no role.");
else if (stored.Fills.Contains(null)) Add(key, "Fills holds a null entry.");
if (stored.RoleInputs == null) Add(key, "RoleInputs is null; a recipe holds a list, empty when it takes no role.");
else if (stored.RoleInputs.Contains(null)) Add(key, "RoleInputs holds a null entry.");
var output = catalog.Get(recipe.Output);
if (output == null) { Add(key, "Output names no item this catalog holds."); continue; }
if (key != RecipeData.KeyOf(recipe.Output.Key).Value)
@ -149,8 +165,10 @@ public static class EconomyChecks
if (roles.Count == 0) Add(key, "A design's recipe needs a design with at least one role.");
}
if (roles.Count > 0 && recipe.Inputs.Count > 0) Add(key, "A role recipe has no Inputs; its inputs are its design's roles.");
foreach (var role in roles)
if (roles.Count > 0 && recipe.RoleInputs.Count > 0) Add(key, "A role recipe has no RoleInputs; its role inputs are its design's roles.");
foreach (var role in Recipes.RoleInputsOf(catalog, recipe))
{
if (string.IsNullOrWhiteSpace(role.Name)) { Add(key, "A role input has an empty name."); continue; }
if (role.Count < 1) Add(key, $"Role '{role.Name}' count is below one.");
if (Recipes.Fillers(catalog, role.Name).Length == 0) Add(key, $"Role '{role.Name}' has no commodity whose recipe fills it.");
}
@ -170,17 +188,26 @@ public static class EconomyChecks
new HashSet<string>(rows.Where(r => r.Buys).Select(r => r.Commodity.Key.Value), StringComparer.Ordinal));
}
// Every commodity has a source (a grounded chain, or a gate that sells it) and a sink (a recipe uses it, a role some design
// names, or a gate that buys it). A consumer good's sink is judged once needs tables exist.
// The commodities an offered design's weapon draws as ammunition. One reader, shared by the sink and the consumer-demand rules.
private static HashSet<string> Drawn(CultCache catalog) =>
new HashSet<string>(catalog.GetAll<FactionProductData>().Where(p => LoadoutGenerator.IsOffered(catalog, p))
.Select(p => catalog.Get(p.Design) as EquippableItemData).Where(design => design?.Behaviors != null)
.SelectMany(design => design.Behaviors.OfType<WeaponData>()).Where(weapon => weapon.AmmoType.IsSet())
.Select(weapon => weapon.AmmoType.Key.Value), StringComparer.Ordinal);
// Every commodity has a source (a grounded chain, or a gate that sells it) and a sink (a recipe takes it or a role it fills, a role
// some design (offered, or made by a recipe) names, a weapon some offered design carries draws it, or a gate that buys it). A consumer good's sink is judged once needs
// tables exist.
public static List<EconomyFault> SourceSink(CultCache catalog)
{
var faults = new List<EconomyFault>();
var (sells, buys) = Gate(catalog);
var drawn = Drawn(catalog);
var grounded = Depths(catalog, sells);
var recipes = AllRecipes(catalog).ToList();
var used = new HashSet<string>(recipes.SelectMany(p => p.Recipe.Inputs.Concat(p.Recipe.Upkeep)).Select(c => c.Commodity.Key.Value), StringComparer.Ordinal);
var rolesNamed = new HashSet<string>(recipes.Where(p => Recipes.IsRoleRecipe(catalog, p.Recipe))
.SelectMany(p => Recipes.RolesOf(catalog.Get(p.Recipe.Output))).Select(r => r.Name), StringComparer.Ordinal);
var rolesNamed = DesignRoles(catalog);
rolesNamed.UnionWith(recipes.SelectMany(p => Recipes.RoleInputsOf(catalog, p.Recipe)).Select(r => r.Name));
foreach (var (key, recipe) in recipes)
{
var output = catalog.Get(recipe.Output);
@ -189,8 +216,8 @@ public static class EconomyChecks
if (!grounded.TryGetValue(outputKey, out var depth) || depth == null)
faults.Add(new EconomyFault(LSource, key, $"'{output.Name}' has no source: no chain of recipes reaches raw goods and no gate sells it."));
if (Recipes.IsConsumerGood(catalog, recipe)) continue;
if (!used.Contains(outputKey) && !recipe.Fills.Any(rolesNamed.Contains) && !buys.Contains(outputKey))
faults.Add(new EconomyFault(LSource, key, $"'{output.Name}' has no sink: no recipe uses it, no design names a role it fills, no gate buys it."));
if (!used.Contains(outputKey) && !recipe.Fills.Any(rolesNamed.Contains) && !drawn.Contains(outputKey) && !buys.Contains(outputKey))
faults.Add(new EconomyFault(LSource, key, $"'{output.Name}' has no sink: no recipe takes it or a role it fills, no design names a role it fills, no weapon draws it, no gate buys it."));
}
return faults;
}
@ -234,7 +261,7 @@ public static class EconomyChecks
if (!depth.TryGetValue(input.Commodity.Key.Value, out var d)) return null;
deepest = Math.Max(deepest, d);
}
foreach (var role in Recipes.RolesOf(catalog.Get(recipe.Output)))
foreach (var role in Recipes.RoleInputsOf(catalog, recipe))
{
int? shallowest = null;
foreach (var filler in Recipes.Fillers(catalog, role.Name))
@ -245,7 +272,7 @@ public static class EconomyChecks
return deepest + 1;
}
// Hawkins-Simon over commodities plus one labour good, at the worst single filler of every role slot: the spectral radius
// Hawkins-Simon over commodities plus one labour good, at the worst single filler of every role slot (a recipe's role inputs): the spectral radius
// of the requirements matrix stays below one however the roles are filled. The worst fill is the spectral simplex
// (Nesterov and Protasov 2013) on the matrix nudged strictly positive, then the radius of the chosen matrix is taken exactly.
public static List<EconomyFault> Productive(CultCache catalog, EconomyLoad load = null)
@ -269,7 +296,7 @@ public static class EconomyChecks
foreach (var upkeep in recipe.Upkeep)
if (index.TryGetValue(upkeep.Commodity.Key.Value, out var row) && upkeep.Count > 0) fixedTerms[row, j] += upkeep.Count * load.UpkeepFactor / recipe.OutputCount;
fixedTerms[labour, j] = recipe.Workforce * (double) recipe.Cycle * load.WorkforceFactor / recipe.OutputCount;
foreach (var role in Recipes.RolesOf(catalog.Get(recipe.Output)))
foreach (var role in Recipes.RoleInputsOf(catalog, recipe))
{
var fillers = Recipes.Fillers(catalog, role.Name).Where(k => index.ContainsKey(k.Value)).Select(k => index[k.Value]).ToArray();
if (fillers.Length > 0 && role.Count > 0) slots.Add((j, role.Count * load.InputFactor / recipe.OutputCount, fillers));
@ -399,36 +426,44 @@ public static class EconomyChecks
return order;
}
// Every processed commodity two or three steps below a gear or hull (its role fillers are one step, the gear-specific final
// assemblies, exempt) fills a role some consumer good names, so the shared layer is shared. Raw goods and the gate do not count.
// A processed good owes a consumer use when it is shared (rulings consumer-demand-shared-only, draw-is-one-use). Its uses are
// the gear or hull roles, and the rounds some weapon draws, that the goods downstream of it (itself included) fill or are; a
// good used as upkeep is shared too. Shared means two or more such uses, or upkeep. A shared good must lead, through the
// chain (an input, upkeep or role-input edge: a role input is an edge from each of its fillers), into a consumer good or a role one names. A good whose every use is one gear role is exempt like its final assembly.
// Derived from the recipe graph on every check; depth and distance play no part. Raw goods and the gate count for nothing.
public static List<EconomyFault> ConsumerDemand(CultCache catalog)
{
var recipes = AllRecipes(catalog).Where(p => catalog.Get(p.Recipe.Output) != null).ToDictionary(p => p.Recipe.Output.Key.Value, p => p.Recipe, StringComparer.Ordinal);
var consumerRoles = new HashSet<string>(recipes.Values.Where(r => Recipes.IsConsumerGood(catalog, r))
.SelectMany(r => Recipes.RolesOf(catalog.Get(r.Output))).Select(role => role.Name), StringComparer.Ordinal);
HashSet<string> Below(IEnumerable<string> keys)
var drawn = Drawn(catalog);
bool IsCommodityRecipe(RecipeData r) => Recipes.IsCommodity(catalog.Get(r.Output));
var gearRoles = DesignRoles(catalog);
var upkeep = new HashSet<string>(recipes.Values.SelectMany(r => r.Upkeep).Select(c => c.Commodity.Key.Value), StringComparer.Ordinal);
var takenBy = recipes.Values.Where(IsCommodityRecipe)
.SelectMany(r => r.Inputs.Concat(r.Upkeep).Select(input => input.Commodity.Key.Value)
.Concat(Recipes.RoleInputsOf(catalog, r).SelectMany(role => Recipes.Fillers(catalog, role.Name)).Select(filler => filler.Value))
.Select(input => (Input: input, Output: r.Output.Key.Value)))
.ToLookup(edge => edge.Input, edge => edge.Output, StringComparer.Ordinal);
var faults = new List<EconomyFault>();
foreach (var key in recipes.Keys.OrderBy(k => k, StringComparer.Ordinal))
{
var next = new HashSet<string>(StringComparer.Ordinal);
foreach (var key in keys)
{
if (!recipes.TryGetValue(key, out var recipe)) continue;
foreach (var input in recipe.Inputs) next.Add(input.Commodity.Key.Value);
foreach (var role in Recipes.RolesOf(catalog.Get(recipe.Output)))
foreach (var filler in Recipes.Fillers(catalog, role.Name)) next.Add(filler.Value);
}
return next;
var recipe = recipes[key];
if (!IsCommodityRecipe(recipe) || Recipes.IsRaw(catalog, recipe) || Recipes.IsConsumerGood(catalog, recipe)) continue;
var reached = new HashSet<string>(StringComparer.Ordinal) { key };
var open = new Stack<string>(reached);
while (open.Count > 0)
foreach (var next in takenBy[open.Pop()])
if (reached.Add(next)) open.Push(next);
var members = reached.Select(k => recipes[k]).ToList();
if (members.Any(r => Recipes.IsConsumerGood(catalog, r))) continue;
var uses = members.SelectMany(r => r.Fills).Where(gearRoles.Contains).Distinct(StringComparer.Ordinal).OrderBy(role => role, StringComparer.Ordinal)
.Concat(reached.Where(drawn.Contains).OrderBy(k => k, StringComparer.Ordinal).Select(k => catalog.Get(recipes[k].Output).Name)).ToList();
var asUpkeep = reached.Any(upkeep.Contains);
if (asUpkeep) uses.Add("upkeep");
if (uses.Count < 2 && !asUpkeep) continue;
var list = uses.Count == 1 ? uses[0] : string.Join(", ", uses.Take(uses.Count - 1)) + " and " + uses[uses.Count - 1];
faults.Add(new EconomyFault(LDemand, RecipeData.KeyOf(new CultRecordKey(key)).Value,
$"'{catalog.Get(recipe.Output).Name}' leads into {list} but no use leads into a consumer good."));
}
var deep = new HashSet<string>(StringComparer.Ordinal);
foreach (var key in recipes.Where(p => !Recipes.IsCommodity(catalog.Get(p.Value.Output)) && Recipes.IsRoleRecipe(catalog, p.Value)).Select(p => p.Key))
{
var distance2 = Below(Below(new[] { key }));
deep.UnionWith(distance2);
deep.UnionWith(Below(distance2));
}
return deep.OrderBy(key => key, StringComparer.Ordinal)
.Where(key => recipes.TryGetValue(key, out var recipe) && Recipes.IsCommodity(catalog.Get(recipe.Output)) && !Recipes.IsRaw(catalog, recipe) && !recipe.Fills.Any(consumerRoles.Contains))
.Select(key => new EconomyFault(LDemand, RecipeData.KeyOf(new CultRecordKey(key)).Value,
$"'{catalog.Get(recipes[key].Output).Name}' is a processed good below a gear or hull that fills no role a consumer good names."))
.ToList();
return faults;
}
}

View file

@ -1821,6 +1821,13 @@ public abstract class Entity
public virtual void Update(float delta)
{
TurnRate = 0;
// A parented entity (a docked ship) sits on its parent, active or not: docking deactivates the ship, so this pin
// comes before the inactive return, or the ship keeps whatever pose its own physics last gave it.
if (Parent != null)
{
Position = Parent.Position;
Velocity = Parent.Velocity;
}
if (!_active) return;
var velocityBefore = Velocity;
@ -1937,12 +1944,7 @@ public abstract class Entity
foreach(var child in Children)
child.Update(delta);
if (Parent != null)
{
Position = Parent.Position;
Velocity = Parent.Velocity;
}
else Position.y = RideHeight(Position.xz);
if (Parent == null) Position.y = RideHeight(Position.xz);
Acceleration = delta > 0f ? (Velocity + LimitClamp - velocityBefore) / delta : float2(0, 0);
Manoeuvre.Observe(Acceleration, delta, ItemManager.GameplaySettings.SolutionWindow);

View file

@ -31,6 +31,10 @@ public class Galaxy
// The run's seed, minted once by GalaxyStage.SeedFrom and carried by the save (SavedGame.Seed). It seeds
// generation, the run's item draws (RunStart) and the zone's drop dice (Zone.DropSeed).
public uint Seed { get; }
// The run's resource provinces, ordered by seed zone index; GalaxyZone.Province is each zone's. Built once, by the
// generation constructors or the save constructor (BuildProvinces), and never by PlaceGate, faction placement or naming.
public GalaxyProvince[] Provinces { get; private set; }
private GalaxyZone[] _exitPath;
private readonly CultCache _cache;
@ -89,6 +93,9 @@ public class Galaxy
if(savedGame.Exit != -1)
Exit = Zones[savedGame.Exit];
// A save from before ore geography has no provinces: it gets them by the rule generation uses.
BuildProvinces(global::Provinces.DefaultZonesPerProvince, savedGame.Provinces == null ? null : savedZones.Select(zone => zone.Province).ToArray(), savedGame.Provinces);
CalculateDistanceMatrix();
}
@ -127,6 +134,8 @@ public class Galaxy
PlaceFactionsMain(settings.BossCount, progressCallback);
BuildProvinces(settings.ProvinceZones);
CalculateFactionInfluence(progressCallback);
NamePlaces(ref random, progressCallback);
@ -135,6 +144,39 @@ public class Galaxy
if(progressCallback!=null) Thread.Sleep(500); // Inserting Delay to make it seem like it's doing more work lmao
}
// Each zone's index and the links as indices: the graph Provinces reads.
private (Dictionary<GalaxyZone, int> Index, int[][] Adjacency) ZoneGraph()
{
var index = Zones.Select((zone, i) => (zone, i)).ToDictionary(pair => pair.zone, pair => pair.i);
return (index, Zones.Select(zone => zone.AdjacentZones.Select(next => index[next]).ToArray()).ToArray());
}
// Regions, then each region's provinces and their types (Provinces.Regions, Partition, Deal); the one builder, called by
// the generation constructors and the save constructor. A save passes its membership and types: membership is kept, and a
// saved type that still resolves is kept, any other taking the card the region's Deal gives that province's place.
private void BuildProvinces(int zonesPerProvince, int[] savedSeedOf = null, List<SavedProvince> savedTypes = null)
{
var (index, adjacency) = ZoneGraph();
var regionOf = global::Provinces.Regions(adjacency, BossZones.Values.Select(zone => index[zone]).ToArray(), Exit == null ? -1 : index[Exit]);
var seedOf = savedSeedOf ?? global::Provinces.Partition(adjacency, regionOf, Seed, zonesPerProvince);
var types = savedTypes?.ToDictionary(saved => saved.Seed, saved => saved.Type);
var provinces = new List<GalaxyProvince>();
foreach (var region in Enumerable.Range(0, Zones.Length).GroupBy(zone => regionOf[zone]).OrderBy(group => group.Key))
{
var seeds = region.Select(zone => seedOf[zone]).Distinct().OrderBy(seed => seed).ToArray();
var hand = global::Provinces.Deal(region.Key, seeds.Length, Seed, _cache);
for (var place = 0; place < seeds.Length; place++)
{
var type = types != null && types.TryGetValue(seeds[place], out var saved) && _cache.Get(saved) != null ? saved : hand[place];
var province = new GalaxyProvince {Seed = Zones[seeds[place]], Type = type, Zones = region.Where(zone => seedOf[zone] == seeds[place]).Select(zone => Zones[zone]).ToArray()};
foreach (var zone in province.Zones) zone.Province = province;
provinces.Add(province);
}
}
Provinces = provinces.OrderBy(province => index[province.Seed]).ToArray();
}
public Faction ResolveFaction(string name)
{
return _cache.GetAll<Faction>().FirstOrDefault(f => f.Name.StartsWith(name, StringComparison.InvariantCultureIgnoreCase));
@ -256,6 +298,9 @@ public class Galaxy
CalculateFactionInfluence(progressCallback);
// No boss zone and no Exit yet (PlaceGate comes after): the prelude is one region.
BuildProvinces(settings.ProvinceZones);
NamePlaces(ref random, progressCallback);
// progressCallback?.Invoke("Weaving Narrative");
@ -303,6 +348,10 @@ public class Galaxy
BossZones.Values.Aggregate(1, (i, os) => i * os.Distance[z]));
}
// One exit per region (ruling one-exit-per-region): prune the boss zones' extra links, then home placement below reads
// the distances of the graph that is left.
OneExitPerRegion(progressCallback);
// Place boss mega headquarters such that their sphere of influence encompasses their boss zone
// While occupying as much territory as possible
foreach (var mega in bossMegas)
@ -324,6 +373,21 @@ public class Galaxy
}
}
// Each boss zone keeps one link toward the exit, the one on the exit path, so there is one exit to defend where its boss
// parks (Provinces.ExtraExits picks the rest; ruling one-exit-per-region). Links are removed from both zones, and the
// distance matrix follows. A galaxy built from a save never comes here: its saved links are the world.
private void OneExitPerRegion(Action<string> progressCallback)
{
var (index, adjacency) = ZoneGraph();
var exitPath = ExitPath.Select(zone => index[zone]).ToArray();
foreach (var (boss, far) in global::Provinces.ExtraExits(adjacency, BossZones.Values.Select(zone => index[zone]).ToArray(), index[Exit], exitPath))
{
Zones[boss].AdjacentZones.Remove(Zones[far]);
Zones[far].AdjacentZones.Remove(Zones[boss]);
}
CalculateDistanceMatrix(progressCallback);
}
private void CalculateFactionInfluence(Action<string> progressCallback = null)
{
progressCallback?.Invoke("Calculating Faction Influence");
@ -571,10 +635,20 @@ public class GalaxyZone
public Faction Owner;
public Zone Contents;
public ZonePack PackedContents;
public GalaxyProvince Province;
public bool NamedZone;
public List<LocationStory> Locations = new List<LocationStory>();
}
// A resource province (ruling provinces-within-regions): a contiguous group of zones inside one region, grown from Seed,
// whose Type (ProvinceData, unset when the catalog holds none) weights the kinds and ores of its belts.
public class GalaxyProvince
{
public GalaxyZone Seed;
public CultRecordRef<ProvinceData> Type;
public GalaxyZone[] Zones;
}
public class GalaxyQuest
{
public Story Story;

View file

@ -13,6 +13,20 @@ public class HelmInput
public void Toggle() => Latched = !Latched;
private float _turnAxis;
// The turn axis as the helm reads it. A digital input (keys) ramps up while held and back down while released, both at
// full scale per rampTime, so tapping at a rate holds a gentle turn; reversing cuts to zero and ramps the other way.
// An analog input passes straight through.
public float TurnAxis(float raw, bool digital, float dt, float rampTime)
{
if (!digital || rampTime <= 0) return _turnAxis = raw;
var step = dt / rampTime;
if (raw == 0) return _turnAxis = sign(_turnAxis) * max(0f, abs(_turnAxis) - step);
if (sign(raw) != sign(_turnAxis)) _turnAxis = 0;
return _turnAxis = sign(raw) * min(abs(raw), abs(_turnAxis) + step);
}
// The turn the player commands: the aim heading, resolved against the hull at each sim step, or the turn axis.
public TurnCommand Demand(float2 aim, float turnAxis) =>
FaceAim ? TurnCommand.Toward(aim) : TurnCommand.OfRate(clamp(turnAxis, -1f, 1f));

View file

@ -200,11 +200,8 @@ public class LoadoutGenerator
var candidates = ItemManager.ItemData.GetAll<FactionProductData>()
.Select(product => (product, design: ItemManager.ItemData.Get(product.Design) as T))
.Where(entry =>
entry.design != null &&
entry.design.Price > 0 &&
entry.product.Manufacturer.IsSet() &&
// A not-offered product is never a candidate, so the any-manufacturer fallback below cannot reach it either.
IsOffered(ItemManager.ItemData, entry.product) &&
IsDrawable(ItemManager.ItemData, entry.product, entry.design) &&
(!(entry.design is EquippableItemData equippable) || HasHome(equippable, hulls)) &&
(filter?.Invoke(entry.design) ?? true))
.ToArray();
@ -246,6 +243,12 @@ public class LoadoutGenerator
Galaxy == null || Faction == null ||
ItemManager.ItemData.Get(product.Manufacturer) == Faction || Faction.Allegiance.ContainsKey(product.Manufacturer));
// Whether a product can be drawn for its design at all: the design resolves, has a price, the product names a maker and is
// offered. The one definition of 'a maker offers this design'; AvailableProducts selects by it and StationKinds decides which
// station kinds a hull can be drawn for by it, so a kind is kept exactly when a hull of it can be drawn.
public static bool IsDrawable(CultCache items, FactionProductData product, CraftedItemData design) =>
design != null && design.Price > 0 && product.Manufacturer.IsSet() && IsOffered(items, product);
// Whether a product is on the market at all (ruling off-market-is-unoffered-product). The one reader of NotOffered. A bin is
// offered only if both it and its line are (ruling bin-inherits-line-offered); the catalog read guarantees a bin's line exists.
public static bool IsOffered(CultCache items, FactionProductData product) =>

View file

@ -6,9 +6,10 @@ using MessagePack;
using Newtonsoft.Json;
// How a good is made: one authored record per output, keyed recipe:<output key>, so a second recipe for one output replaces the
// first. A commodity's recipe says what it takes and which roles it can fill. A role recipe (the output is a design with roles,
// a ship hull, a piece of gear, a consumer good) restates nothing of the design: its inputs are derived from the design's Roles and
// their counts, each filled by any commodity whose recipe Fills the role. Station and Turret hulls take no recipe.
// first. A commodity's recipe takes named commodities (Inputs) and roles (RoleInputs), each role filled by any commodity whose
// recipe Fills it, and says which roles it can fill. A role recipe (the output is a design with roles, a ship hull, a piece of
// gear, a consumer good) restates nothing of the design: its role inputs are the design's Roles and their counts. Station and
// Turret hulls take no recipe.
[CultDocument("aetheria.recipedata", "1"), MessagePackObject, JsonObject(MemberSerialization.OptIn)]
public class RecipeData
{
@ -46,6 +47,11 @@ public class RecipeData
[JsonProperty("fills"), Key(8)]
public List<string> Fills = new List<string>();
// Roles one cycle consumes, any filler will do and the facility picks (ruling ores-fill-metal-roles). Empty on a role recipe;
// records written before key 9 read it as empty.
[JsonProperty("roleInputs"), Key(9)]
public List<ItemRole> RoleInputs = new List<ItemRole>();
public static CultRecordKey KeyOf(CultRecordKey outputKey) => new CultRecordKey("recipe:" + outputKey.Value);
}
@ -92,7 +98,8 @@ public class OutsideMarketRow
public float RatePerHour;
}
// The one reader of recipes: which recipe makes a good, which commodities fill a role, and what a good is by its recipe.
// The one reader of recipes: which recipe makes a good, a recipe's role slots (RoleInputsOf), which commodities fill a role, and
// what a good is by its recipe.
public static class Recipes
{
public static RecipeData RecipeOf(CultCache catalog, ItemData item) =>
@ -110,8 +117,12 @@ public static class Recipes
return output is CraftedItemData && RolesOf(output).Count > 0;
}
// Raw: no inputs and not a role recipe. Everything else is processed. Derived, never stored.
public static bool IsRaw(CultCache catalog, RecipeData recipe) => recipe.Inputs.Count == 0 && !IsRoleRecipe(catalog, recipe);
// The one reader of a recipe's role slots: the design's Roles for a role recipe, else the recipe's RoleInputs.
public static IReadOnlyList<ItemRole> RoleInputsOf(CultCache catalog, RecipeData recipe) =>
IsRoleRecipe(catalog, recipe) ? RolesOf(catalog.Get(recipe.Output)) : (IReadOnlyList<ItemRole>) recipe.RoleInputs ?? new List<ItemRole>();
// Raw: takes no commodity and no role. Everything else is processed. Derived, never stored.
public static bool IsRaw(CultCache catalog, RecipeData recipe) => recipe.Inputs.Count == 0 && RoleInputsOf(catalog, recipe).Count == 0;
// A commodity made by a role recipe: it is assembled from the same components gear is.
public static bool IsConsumerGood(CultCache catalog, RecipeData recipe) =>

View file

@ -55,6 +55,22 @@ public class SavedGame
// by Galaxy's save constructor; 0 in a save written before run-seed, which then rolls name-only drops.
[Key(14)]
public uint Seed;
// The run's resource provinces (Galaxy.Provinces): each province's seed zone index and its type. Written only by
// RunSave.Capture and read only by Galaxy's save constructor; null in a save written before ore geography, which then
// gets Regions, Partition and Deal from its saved graph, boss zones, Exit and seed.
[Key(15)]
public List<SavedProvince> Provinces;
}
[MessagePackObject]
public class SavedProvince
{
[Key(0)]
public int Seed;
[Key(1)]
public CultRecordRef<ProvinceData> Type;
}
// The run's lifecycle over the run store. Only Commit creates SavedGame, SavedZone and ProvenanceLedger records;
@ -96,8 +112,11 @@ public static class RunSave
AdjacentZones = zone.AdjacentZones.Select(az => Array.IndexOf(galaxy.Zones, az)).ToArray(),
Factions = zone.Factions.Select(f => Array.IndexOf(factions, f)).ToArray(),
Contents = zone.Contents?.PackZone() ?? zone.PackedContents,
Owner = zone.Owner == null ? -1 : Array.IndexOf(factions, zone.Owner)
Owner = zone.Owner == null ? -1 : Array.IndexOf(factions, zone.Owner),
Province = Array.IndexOf(galaxy.Zones, zone.Province.Seed)
}).ToArray();
game.Provinces = galaxy.Provinces.Select(province => new SavedProvince
{Seed = Array.IndexOf(galaxy.Zones, province.Seed), Type = province.Type}).ToList();
return (game, zones);
}
@ -221,6 +240,11 @@ public class SavedZone
[Key(5)]
public ZonePack Contents;
// The seed zone index of the zone's province. Written only by RunSave.Capture and read only by Galaxy's save
// constructor; meaningful only when SavedGame.Provinces is present.
[Key(6)]
public int Province;
}
[MessagePackObject,

View file

@ -83,6 +83,8 @@ public class SectorGenerationSettings
{
public float LinkDensity = .5f;
public int ZoneCount = 128;
// Zones per resource province (Provinces.Partition): calibration, not a gate.
public int ProvinceZones = Provinces.DefaultZonesPerProvince;
public int MegaCount;
public int BossCount;
}
@ -97,6 +99,8 @@ public class TutorialGenerationSettings
public string QuestFaction;
public float LinkDensity = .5f;
public int ZoneCount = 32;
// Zones per resource province (Provinces.Partition): calibration, not a gate.
public int ProvinceZones = Provinces.DefaultZonesPerProvince;
}
[Serializable, MessagePackObject(keyAsPropertyName:true), JsonObject]
@ -183,6 +187,11 @@ public class GameplaySettings
public float ThermalQualityMin = 2;
public float ThermalQualityMax = .25f;
public float DefaultShutdownPerformance = .25f;
// Global flight tuning over every thruster's authored push (not its turning) and every hull's authored Drag. Push is a tenth, the
// ratio gravity fell by in the honest-wells cut (aetheria-gravity:field-strength-ruled, 5 to 0.5 u/s^2): most ships
// cannot climb straight out of a well and use orbits instead; an overspecced hull still can.
public float ThrustScale = .1f;
public float DragScale = 1f;
// Free-floating bodies (floating items, mines): drag on a launch velocity, drag on the force-driven drift, and
// how strongly the zone force accelerates it.
public float FloatingBodyDrag = .05f;

View file

@ -112,6 +112,43 @@ public class Ship : Entity
#endregion
// The player's turn axis as a fraction of the hull's yaw extreme, banded so each thruster joins over an equal share of
// the input. The allocator serves a turn by the thrusters with the least translation per unit of yaw first and brings in
// the next as they saturate; here the thrusters yawing the asked way are ranked by that ratio, equal ratios form one band,
// and band i fills over input [i/k, (i+1)/k]. A hull whose best-ratio thrusters hold a small share of its yaw then gets
// half the travel of a two-band turn before its drives join, instead of the first tenth. Full input is still full yaw.
public float BandedTurn(float axis)
{
axis = clamp(axis, -1f, 1f);
if (axis == 0 || _allThrusters == null) return axis;
var direction = axis > 0 ? 1f : -1f;
var thrusters = new List<(float ratio, float yaw)>();
var total = 0f;
foreach (var thruster in _allThrusters)
{
var column = thruster.Column(thruster.NominalThrust);
var yaw = column.z * direction;
if (!(yaw > 0)) continue;
thrusters.Add((length(float2(column.x, column.y)) / yaw, yaw));
total += yaw;
}
if (!(total > 0)) return axis;
thrusters.Sort((a, b) => a.ratio.CompareTo(b.ratio));
var bands = new List<float>();
var bandRatio = float.NaN;
foreach (var (ratio, yaw) in thrusters)
{
if (bands.Count > 0 && abs(ratio - bandRatio) <= .02f * max(bandRatio, 1e-6f)) bands[bands.Count - 1] += yaw;
else { bands.Add(yaw); bandRatio = ratio; }
}
var travel = abs(axis) * bands.Count;
var band = min((int)travel, bands.Count - 1);
var served = 0f;
for (var i = 0; i < band; i++) served += bands[i];
served += (travel - band) * bands[band];
return direction * min(served / total, 1f);
}
// Called by Entity.Update once the items' performance is current and before the power bus is stepped and the items
// execute: liveness, thrust and the throttles they decide are one phase, the one Execute acts on. The lock is a
// bound on the allocator's solve; intent reaches the allocator here and nowhere else.
@ -127,13 +164,22 @@ public class Ship : Entity
public override void Update(float delta)
{
var velocityMagnitude = length(Velocity);
if(velocityMagnitude > .01f)
Velocity = normalize(Velocity) * decay(velocityMagnitude, HullData.Drag, delta);
Position.xz += Velocity * delta;
Velocity += Zone.GetForce(Position.xz) * delta;
// No drag below top speed: ships coast and orbits do not decay; drag bleeds off only the excess over the hull's top
// speed (aetheria-gravity:ruling:no-drag-below-top-speed). The VelocityLimit clamp still caps it, so the excess is
// what this step's forces added; a soft cap, where surfing the beat is a temporary boost, is the ruling's other half.
// A docked ship has no flight of its own: its parent owns its pose (Entity.Update).
if (Parent == null)
{
var velocityMagnitude = length(Velocity);
var topSpeed = float.PositiveInfinity;
foreach (var limit in GetBehaviors<VelocityLimit>()) topSpeed = min(topSpeed, limit.Limit);
if (velocityMagnitude > topSpeed)
Velocity = normalize(Velocity) * (topSpeed + decay(velocityMagnitude - topSpeed, HullData.Drag * ItemManager.GameplaySettings.DragScale, delta));
Position.xz += Velocity * delta;
Velocity += Zone.GetForce(Position.xz) * delta;
}
var normal = Zone.GetNormal(Position.xz);
var shipRight = Direction.Rotate(ItemRotation.Clockwise);
var forward = cross(float3(shipRight.x, 0, shipRight.y), normal);

View file

@ -52,35 +52,47 @@ public static class StationKinds
return Ordered(kinds);
}
// A station's kind: a stable pick among the faction's kinds by the station's site, which generation names by the zone's
// seed and the station's place in its orbit order (never by a key the cache mints). It draws nothing from any random
// stream, so adding the kind to generation moves no other roll. A faction with no kinds falls back to any kind some
// offered station hull carries, logged, and the hull draw then falls back to any maker. Unset when the catalog has none.
// The kinds this faction places that a station can be drawn for: those whose hull kind (its own model, else its stand-in's) some
// maker offers. A stand-in whose model has not landed leaves its kind out.
public static CultRecordRef<StationKindData>[] FieldableKindsOf(CultCache catalog, Faction faction) =>
KindsOf(catalog, faction).Where(kind => HasModel(catalog, faction, kind) || LiveStandIn(catalog, faction, kind).IsSet()).ToArray();
// A station's kind: a stable pick among the faction's kinds it can field, by the station's site, which generation names by the
// zone's seed and the station's place in its orbit order (never by a key the cache mints). It draws nothing from any random
// stream, so adding the kind to generation moves no other roll. A kind the faction places but cannot field (neither its own
// model nor a stand-in's hull kind is carried by any offered station hull) is not in the pool; a faction left with no kind to
// field falls back to any kind some offered station hull carries, logged, and the hull draw then falls back to any maker.
// Unset when the catalog has none.
public static CultRecordRef<StationKindData> Choose(CultCache catalog, Faction faction, string site, Action<string> log = null)
{
var pool = KindsOf(catalog, faction);
var pool = FieldableKindsOf(catalog, faction);
if (pool.Length == 0)
{
pool = Ordered(new HashSet<string>(
catalog.GetAll<FactionProductData>().Where(product => LoadoutGenerator.IsOffered(catalog, product))
.Select(product => catalog.Get(product.Design) as HullData)
.Where(hull => hull != null && hull.HullType == HullType.Station && hull.Kind.IsSet())
.Select(hull => hull.Kind.Key.Value), StringComparer.Ordinal));
pool = Ordered(OfferedKinds(catalog));
if (pool.Length == 0) return default;
log?.Invoke($"{faction?.Name ?? "This galaxy"} builds no station kind; placing a station of any kind some maker offers.");
log?.Invoke($"{faction?.Name ?? "This galaxy"} builds no station kind it can field; placing a station of any kind some maker offers.");
}
return pool[(int) (site.StableHash() % (uint) pool.Length)];
}
// The kind of hull a faction builds for a station kind: the kind itself when the faction offers a model of it (a model
// wins over a stale stand-in), else the kind its stand-in names, else the kind itself (the hull draw then falls back to
// any maker).
// wins over a stale stand-in), else the kind its stand-in names when some maker offers that, else the kind itself (the hull
// draw then falls back to any maker).
public static CultRecordRef<StationKindData> HullKindFor(CultCache catalog, Faction faction, CultRecordRef<StationKindData> kind)
{
if (!kind.IsSet()) return kind;
if (OfferedStationHulls(catalog, faction).Any(hull => hull.Kind.Key.Equals(kind.Key))) return kind;
if (!kind.IsSet() || HasModel(catalog, faction, kind)) return kind;
var standIn = LiveStandIn(catalog, faction, kind);
return standIn.IsSet() ? standIn : kind;
}
private static bool HasModel(CultCache catalog, Faction faction, CultRecordRef<StationKindData> kind) =>
OfferedStationHulls(catalog, faction).Any(hull => hull.Kind.Key.Equals(kind.Key));
// The kind the faction's stand-in names for this kind, when some maker offers a hull of it; else unset.
private static CultRecordRef<StationKindData> LiveStandIn(CultCache catalog, Faction faction, CultRecordRef<StationKindData> kind)
{
var standIn = catalog.Get(kind)?.StandIns.FirstOrDefault(entry => catalog.Get(entry.Faction) == faction);
return standIn != null && standIn.Kind.IsSet() ? standIn.Kind : kind;
return standIn != null && standIn.Kind.IsSet() && OfferedKinds(catalog).Contains(standIn.Kind.Key.Value) ? standIn.Kind : default;
}
// What is wrong with a hull's kind, or null: a Station hull names a kind the catalog holds, and no other hull names one.
@ -130,11 +142,19 @@ public static class StationKinds
}
}
// The hull kinds some maker offers: the pool a station can always be drawn from.
private static HashSet<string> OfferedKinds(CultCache catalog) =>
new HashSet<string>(DrawableStationProducts(catalog).Select(entry => entry.hull.Kind.Key.Value), StringComparer.Ordinal);
private static IEnumerable<HullData> OfferedStationHulls(CultCache catalog, Faction faction) =>
DrawableStationProducts(catalog).Where(entry => catalog.Get(entry.product.Manufacturer) == faction).Select(entry => entry.hull);
// The station hulls a hull draw can reach (LoadoutGenerator.IsDrawable, the one definition), with the product that sells each.
private static IEnumerable<(FactionProductData product, HullData hull)> DrawableStationProducts(CultCache catalog) =>
catalog.GetAll<FactionProductData>()
.Where(product => LoadoutGenerator.IsOffered(catalog, product) && catalog.Get(product.Manufacturer) == faction)
.Select(product => catalog.Get(product.Design) as HullData)
.Where(hull => hull != null && hull.HullType == HullType.Station && hull.Kind.IsSet());
.Select(product => (product, hull: catalog.Get(product.Design) as HullData))
.Where(entry => entry.hull != null && entry.hull.HullType == HullType.Station && entry.hull.Kind.IsSet() &&
LoadoutGenerator.IsDrawable(catalog, entry.product, entry.hull));
private static CultRecordRef<StationKindData>[] Ordered(IEnumerable<string> keys) =>
keys.OrderBy(key => key, StringComparer.Ordinal).Select(key => new CultRecordRef<StationKindData>(new CultRecordKey(key))).ToArray();

View file

@ -196,7 +196,7 @@ public class Zone
case AsteroidBeltData belt:
// Mining Cut 3 (Q16 B): a belt from before field kinds existed gets its kind here, once, by
// the rule generation uses, and keeps it.
FieldKinds.Ensure(body.Key.Value, belt, cache);
FieldKinds.Ensure(body.Key.Value, belt, cache, GalaxyZone?.Province?.Type ?? default);
AsteroidBelts[body.Key] = new AsteroidBelt(belt);
break;
case SunData sun:

View file

@ -81,8 +81,7 @@ public abstract class BodyData
[JsonProperty("mass"), Key(3)]
public float Mass = 0;
[CultReference(typeof(SimpleCommodityData), many: true), JsonProperty("resources"), Key(4)]
public Dictionary<CultRecordRef<SimpleCommodityData>, float> Resources = new Dictionary<CultRecordRef<SimpleCommodityData>, float>();
// MessagePack key 4 belonged to the removed stored Resources; do not reuse it. A belt's yield is derived by FieldKinds.Yield.
[JsonProperty("bodyRadiusMul")] [Key(5)]
public float BodyRadiusMultiplier = 1;
@ -114,7 +113,8 @@ public class AsteroidBeltData : BodyData
}
// Mining Cut 3 (docs/mining-cut-refresh.md, Q11): a kind of debris field, an authored catalog record. A chunk's
// reflectivity comes from its field's kind; Cut 5 adds the yield table.
// reflectivity comes from its field's kind; its yields (ore geography, ruling ores-are-regional) are the ores a belt of
// this kind holds before a province weights them. FieldKinds.Yield is the one reader.
[CultDocument("aetheria.fieldkinddata", "1"), MessagePackObject, JsonObject(MemberSerialization.OptIn)]
public class FieldKindData
{
@ -129,41 +129,308 @@ public class FieldKindData
// How often generation picks this kind, relative to the others (FieldKinds.Assign). Zero never picks it.
[JsonProperty("generationWeight"), Key(2)]
public float GenerationWeight = 1;
// The ores a belt of this kind holds. Empty on a kind from before this key (MessagePack leaves a missing trailing key at its default).
[JsonProperty("yields"), Key(3)]
public List<FieldYield> Yields = new List<FieldYield>();
}
// Mining Cut 3 (Q16 B): the one owner of which kind a belt is.
// One ore a kind of field holds: Abundance of Commodity before the belt's province weights it. A Regional ore is held only
// where the province lists it (ruling ores-are-regional: some ores exist only in some provinces); any other entry is held
// everywhere, scaled by the province's factor for it.
[MessagePackObject, JsonObject(MemberSerialization.OptIn)]
public class FieldYield
{
[JsonProperty("commodity"), Key(0)]
public CultRecordRef<SimpleCommodityData> Commodity;
[JsonProperty("abundance"), Key(1)]
public float Abundance;
[JsonProperty("regional"), Key(2)]
public bool Regional;
}
// Ore geography (rulings provinces-within-regions, ores-are-regional): a kind of ground, authored. A region deals its
// provinces' types from a deck holding GenerationWeight cards of each type (ruling province-types-dealt-per-region);
// a province's type weights which kinds its belts draw and which ores a kind holds there.
[CultDocument("aetheria.provincedata", "1"), MessagePackObject, JsonObject(MemberSerialization.OptIn)]
public class ProvinceData
{
[CultName, JsonProperty("name"), Key(0)]
public string Name = "";
// The number of cards of this type in a region's deck. Zero is never dealt.
[JsonProperty("generationWeight"), Key(1)]
public int GenerationWeight = 1;
// Multiplies the kind's own GenerationWeight in a belt's draw; a kind not listed is multiplied by one.
[JsonProperty("kinds"), Key(2)]
public List<ProvinceKindWeight> Kinds = new List<ProvinceKindWeight>();
// Multiplies an ore's Abundance wherever it is held; an ore not listed is multiplied by one, or by zero when the
// kind's entry for it is Regional.
[JsonProperty("ores"), Key(3)]
public List<ProvinceOre> Ores = new List<ProvinceOre>();
}
[MessagePackObject, JsonObject(MemberSerialization.OptIn)]
public class ProvinceKindWeight
{
[JsonProperty("kind"), Key(0)]
public CultRecordRef<FieldKindData> Kind;
[JsonProperty("weight"), Key(1)]
public float Weight = 1;
}
[MessagePackObject, JsonObject(MemberSerialization.OptIn)]
public class ProvinceOre
{
[JsonProperty("commodity"), Key(0)]
public CultRecordRef<SimpleCommodityData> Commodity;
[JsonProperty("weight"), Key(1)]
public float Weight = 1;
}
// Mining Cut 3 (Q16 B): the one owner of which kind a belt is. Ore geography: and of what it yields.
public static class FieldKinds
{
// A weighted pick among the catalog's kinds, by a stable hash of the belt's site: the same site and the same
// catalog always give the same kind. Generation names a belt by the zone's seed and the belt's place in generation
// order (never by the key the cache mints, which differs every run); a belt saved without a kind is named by its key. Kinds are ordered by key so the pick does not depend on store order.
// Unset when the catalog has no kind with positive weight.
public static CultRecordRef<FieldKindData> Assign(string site, CultCache catalog)
// The one weighted pick: an index into weights, chosen by a stable hash of the site (the same site and the same
// weights always give the same index), never one whose weight is not positive. -1 when no weight is positive.
internal static int Pick(string site, double[] weights)
{
var pick = site.StableHash() / 4294967296.0 * weights.Sum();
var last = -1;
for (var i = 0; i < weights.Length; i++)
{
if (weights[i] <= 0) continue;
last = i;
if (pick < weights[i]) return i;
pick -= weights[i];
}
return last;
}
// A weighted pick among the catalog's kinds, by a stable hash of the belt's site: the same site, catalog and province
// type always give the same kind. A kind's weight is its GenerationWeight times its province's factor for it; a belt with
// no province type (unset, or naming nothing the catalog holds) draws unweighted. Generation names a belt by the zone's
// seed and the belt's place in generation order (never by the key the cache mints, which differs every run); a belt saved
// without a kind is named by its key. Kinds are ordered by key so the pick does not depend on store order.
// Unset when the catalog has no kind with positive weight.
public static CultRecordRef<FieldKindData> Assign(string site, CultCache catalog, CultRecordRef<ProvinceData> province)
{
var type = catalog.Get(province);
var kinds = catalog.GetAll<FieldKindData>()
.Where(kind => kind.GenerationWeight > 0f)
.Select(kind => (Key: catalog.RefOf(kind).Key, Weight: kind.GenerationWeight))
.Select(kind =>
{
var key = catalog.RefOf(kind).Key;
var factor = Factor(type?.Kinds.Where(entry => entry.Kind.Key.Equals(key)).Select(entry => (double) entry.Weight), 1.0);
// Both must be positive: two negatives do not multiply into a weight.
return (Key: key, Weight: kind.GenerationWeight > 0f && factor > 0.0 ? kind.GenerationWeight * factor : 0.0);
})
.Where(kind => kind.Weight > 0.0)
.OrderBy(kind => kind.Key.Value, StringComparer.Ordinal)
.ToArray();
if (kinds.Length == 0) return default;
var pick = site.StableHash() / 4294967296.0 * kinds.Sum(kind => (double) kind.Weight);
foreach (var kind in kinds)
{
if (pick < kind.Weight) return new CultRecordRef<FieldKindData>(kind.Key);
pick -= kind.Weight;
}
return new CultRecordRef<FieldKindData>(kinds[kinds.Length - 1].Key);
return new CultRecordRef<FieldKindData>(kinds[Pick(site, kinds.Select(kind => kind.Weight).ToArray())].Key);
}
// A province's factor for one entry: the sum of its listings, or the default when it lists none.
private static double Factor(IEnumerable<double> listed, double unlisted)
{
var weights = listed?.ToArray();
return weights == null || weights.Length == 0 ? unlisted : weights.Sum();
}
// Gives a belt with no kind its kind and saves it; a belt that has one keeps it, whatever the catalog now says.
// Generation (ZoneGenerator) and first load (Zone) both call this, so there is one assignment rule.
public static void Ensure(string site, AsteroidBeltData belt, CultCache cache)
public static void Ensure(string site, AsteroidBeltData belt, CultCache cache, CultRecordRef<ProvinceData> province)
{
if (belt.Kind.IsSet()) return;
belt.Kind = Assign(site, cache);
belt.Kind = Assign(site, cache, province);
if (belt.Kind.IsSet()) cache.Upsert(belt);
}
// What a belt of this kind holds in a zone of this province: each of the kind's yields, its Abundance times the
// province's factor for the ore (unlisted: one, or zero when the yield is Regional), summed per commodity, in commodity
// key order, zero amounts dropped. Derived on every call, never stored. Empty for an unset kind.
public static (CultRecordRef<SimpleCommodityData> Commodity, float Amount)[] Yield(CultRecordRef<FieldKindData> kind,
CultRecordRef<ProvinceData> province, CultCache catalog)
{
var data = catalog.Get(kind);
if (data == null) return Array.Empty<(CultRecordRef<SimpleCommodityData>, float)>();
var type = catalog.Get(province);
return data.Yields
.Select(yield => (yield.Commodity, Amount: yield.Abundance * Factor(
type?.Ores.Where(ore => ore.Commodity.Key.Equals(yield.Commodity.Key)).Select(ore => (double) ore.Weight), yield.Regional ? 0.0 : 1.0)))
.GroupBy(yield => yield.Commodity.Key.Value, StringComparer.Ordinal)
.OrderBy(group => group.Key, StringComparer.Ordinal)
.Select(group => (group.First().Commodity, Amount: (float) group.Sum(yield => yield.Amount)))
.Where(yield => yield.Amount > 0f)
.ToArray();
}
}
// Ore geography (rulings provinces-within-regions, boss-zone-is-near-side, province-partition-by-size, province-types-dealt-per-region).
// Pure functions of the zone graph, the run seed and the catalog: nothing here draws from Galaxy's Random stream, stores membership
// or reads who owns what. Galaxy builds provinces once, in its generation and save constructors, and nothing re-partitions them.
public static class Provinces
{
// Zones per province when a settings object does not say (SectorGenerationSettings.ProvinceZones): calibration, not a rule.
public const int DefaultZonesPerProvince = 8;
// A boss zone sits on the near side of its border: its crossing links are its links to zones in the component of exit in
// the graph without it. The one definition, read by Regions (which cuts them) and ExtraExits (which prunes all but one).
// exit unset (-1) gives none, and a boss zone that is the exit has none. Ordered by boss, then far zone.
public static (int Boss, int Far)[] CrossingLinks(int[][] adjacency, IReadOnlyCollection<int> bossZones, int exit)
{
var links = new List<(int Boss, int Far)>();
if (exit >= 0)
foreach (var boss in bossZones.Distinct().Where(boss => boss != exit).OrderBy(boss => boss))
{
var exitSide = Flood(adjacency, exit, (a, b) => b != boss);
foreach (var next in adjacency[boss].OrderBy(next => next))
if (exitSide[next]) links.Add((boss, next));
}
return links.ToArray();
}
// The crossing links to prune so each boss zone keeps one exit: all of them but the link to the boss zone's successor on
// exitPath (the zones from the entrance to the exit), which is the link the run goes through. The generator's exit path
// passes through every boss zone by one of its crossing links; a boss zone off exitPath, last on it, or followed by a zone
// it has no crossing link to breaks that, and throws naming the boss zone rather than guessing which link to keep.
public static (int Boss, int Far)[] ExtraExits(int[][] adjacency, IReadOnlyCollection<int> bossZones, int exit, int[] exitPath)
{
var extra = new List<(int Boss, int Far)>();
foreach (var links in CrossingLinks(adjacency, bossZones, exit).GroupBy(link => link.Boss))
{
var at = Array.IndexOf(exitPath, links.Key);
var kept = at >= 0 && at + 1 < exitPath.Length ? exitPath[at + 1] : -1;
if (!links.Any(link => link.Far == kept))
throw new InvalidOperationException($"boss zone {links.Key} has no crossing link to its successor on the exit path, so no exit to keep");
extra.AddRange(links.Where(link => link.Far != kept));
}
return extra.ToArray();
}
// Each zone's region, named by the region's least zone index. Cutting every boss zone's crossing links (CrossingLinks)
// leaves the regions as the connected components. Every zone, boss zones included, is in exactly one.
public static int[] Regions(int[][] adjacency, IReadOnlyCollection<int> bossZones, int exit)
{
var cut = CrossingLinks(adjacency, bossZones, exit).Select(link => (Math.Min(link.Boss, link.Far), Math.Max(link.Boss, link.Far))).ToHashSet();
var regionOf = new int[adjacency.Length];
Array.Fill(regionOf, -1);
for (var start = 0; start < adjacency.Length; start++)
{
if (regionOf[start] >= 0) continue;
var members = Flood(adjacency, start, (a, b) => !cut.Contains((Math.Min(a, b), Math.Max(a, b))));
for (var i = 0; i < members.Length; i++) if (members[i]) regionOf[i] = start;
}
return regionOf;
}
// Each zone's province, named by the province's seed zone index. A region of size s takes max(1, round(s / zonesPerProvince))
// seeds: the first is the zone with the least hash of the run seed, each next the zone farthest (in-region hops) from the
// seeds chosen so far, ties by that hash; then every zone joins the nearest seed, ties to the earlier seed. A province is
// contiguous and never crosses a region.
public static int[] Partition(int[][] adjacency, int[] regionOf, uint runSeed, int zonesPerProvince)
{
var seedOf = new int[adjacency.Length];
uint Hash(int index) => $"{runSeed}:province-seed:{index}".StableHash();
foreach (var region in Enumerable.Range(0, adjacency.Length).GroupBy(zone => regionOf[zone]).OrderBy(group => group.Key))
{
var zones = region.ToArray();
var count = Math.Min(zones.Length, Math.Max(1, (int) Math.Round((double) zones.Length / Math.Max(1, zonesPerProvince), MidpointRounding.AwayFromZero)));
var seeds = new List<int> { zones.OrderBy(Hash).ThenBy(zone => zone).First() };
while (seeds.Count < count)
{
var hops = Grow(adjacency, regionOf, seeds.ToArray()).Hops;
seeds.Add(zones.Where(zone => !seeds.Contains(zone)).OrderByDescending(zone => hops[zone]).ThenBy(Hash).ThenBy(zone => zone).First());
}
var owner = Grow(adjacency, regionOf, seeds.ToArray()).Owner;
foreach (var zone in zones) seedOf[zone] = seeds[owner[zone]];
}
return seedOf;
}
// A region's hand of province types, one per province in ascending seed-zone order. Types are dealt without replacement from
// a deck of GenerationWeight cards per type (catalog types in key order), each draw one weighted pick over the cards remaining
// at a site named by the run seed, the region and the draw's place; the deck is restored whole when it runs out. An empty
// deck deals every province an unset type.
public static CultRecordRef<ProvinceData>[] Deal(int regionKey, int seedCount, uint runSeed, CultCache catalog)
{
var types = catalog.GetAll<ProvinceData>()
.Where(type => type.GenerationWeight > 0)
.Select(type => (Ref: catalog.RefOf(type), Cards: type.GenerationWeight))
.OrderBy(type => type.Ref.Key.Value, StringComparer.Ordinal)
.ToArray();
var hand = new CultRecordRef<ProvinceData>[seedCount];
if (types.Length == 0) return hand;
var remaining = types.Select(type => type.Cards).ToArray();
for (var n = 0; n < seedCount; n++)
{
if (remaining.Sum() == 0) remaining = types.Select(type => type.Cards).ToArray();
var card = FieldKinds.Pick($"{runSeed}:province-deal:{regionKey}:{n}", remaining.Select(cards => (double) cards).ToArray());
remaining[card]--;
hand[n] = types[card].Ref;
}
return hand;
}
// Every zone reachable from start over the links the filter allows.
private static bool[] Flood(int[][] adjacency, int start, Func<int, int, bool> allowed)
{
var reached = new bool[adjacency.Length];
var stack = new Stack<int>();
reached[start] = true;
stack.Push(start);
while (stack.Count > 0)
{
var zone = stack.Pop();
foreach (var next in adjacency[zone])
if (!reached[next] && allowed(zone, next))
{
reached[next] = true;
stack.Push(next);
}
}
return reached;
}
// Breadth-first growth from seeds inside their regions, level by level: each zone's hops to the nearest seed and the rank
// (place in seeds) of the earliest seed among those that reach it first.
private static (int[] Owner, int[] Hops) Grow(int[][] adjacency, int[] regionOf, int[] seeds)
{
var owner = new int[adjacency.Length];
var hops = new int[adjacency.Length];
Array.Fill(owner, -1);
for (var rank = 0; rank < seeds.Length; rank++)
{
owner[seeds[rank]] = rank;
hops[seeds[rank]] = 0;
}
var frontier = seeds.ToList();
for (var level = 1; frontier.Count > 0; level++)
{
var claims = new Dictionary<int, int>();
foreach (var zone in frontier)
foreach (var next in adjacency[zone])
if (owner[next] < 0 && regionOf[next] == regionOf[zone])
claims[next] = claims.TryGetValue(next, out var rank) ? Math.Min(rank, owner[zone]) : owner[zone];
foreach (var claim in claims)
{
owner[claim.Key] = claim.Value;
hops[claim.Key] = level;
}
frontier = claims.Keys.OrderBy(zone => zone).ToList();
}
return (owner, hops);
}
}
[CultDocument("aetheria.gasgiantdata", "1"), MessagePackObject, JsonObject(MemberSerialization.OptIn)]

View file

@ -213,7 +213,7 @@ public static class ZoneGenerator
var bodyKey = cache.Upsert(planetData).Key;
planetData.Name = bodyKey.Value.Substring(0, 8);
// Mining Cut 3 (Q16 B): a belt's field kind is chosen here, at generation, and saved with it.
if (planetData is AsteroidBeltData generatedBelt) FieldKinds.Ensure($"{seed}:belt:{index}", generatedBelt, cache);
if (planetData is AsteroidBeltData generatedBelt) FieldKinds.Ensure($"{seed}:belt:{index}", generatedBelt, cache, galaxyZone.Province?.Type ?? default);
return cache.Upsert(planetData);
}).ToList();
@ -239,7 +239,7 @@ public static class ZoneGenerator
BodyData swarmBody = swarm;
var swarmKey = cache.Upsert(swarmBody).Key;
swarm.Name = swarmKey.Value.Substring(0, 8);
FieldKinds.Ensure($"{seed}:swarm:{swarmHosts.Count}", swarm, cache);
FieldKinds.Ensure($"{seed}:swarm:{swarmHosts.Count}", swarm, cache, galaxyZone.Province?.Type ?? default);
pack.Planets.Add(cache.Upsert(swarmBody));
}
@ -268,7 +268,7 @@ public static class ZoneGenerator
BodyData lanedBody = laned;
var lanedKey = cache.Upsert(lanedBody).Key;
laned.Name = lanedKey.Value.Substring(0, 8);
FieldKinds.Ensure($"{seed}:lane:{shaped.Count}", laned, cache);
FieldKinds.Ensure($"{seed}:lane:{shaped.Count}", laned, cache, galaxyZone.Province?.Type ?? default);
pack.Planets.Add(cache.Upsert(lanedBody));
}
@ -522,12 +522,6 @@ public static class ZoneGenerator
public static Faction[] EligibleWandererFactions(Faction[] factions, Faction owner, Faction nearest) =>
factions.Where(f => f != owner && f != nearest).ToArray();
// static float ResourceValue(ref Random random, ZoneGenerationSettings settings, SimpleCommodityData resource, float density)
// {
// return random.NextPowerDistribution(resource.Minimum, resource.Maximum, resource.Exponent,
// 1 / lerp(settings.ResourceDensityMinimum, settings.ResourceDensityMaximum, density));
// }
public static GeneratorPlanet[] GenerateEntities(ZoneGenerationSettings settings, PlanetSettings planetSettings, ref Random random, float mass, float radius, float2 fixedPosition,
float rootReach)
{

View file

@ -59,8 +59,10 @@ public class Stardust : MonoBehaviour
// The dust is drawn by a command buffer on TargetCamera: a camera replays its buffers at their event and never culls them,
// so the draw needs no bounds and no extent can fall short of how deep the field is.
private CommandBuffer _draw;
// Before the [ImageEffectOpaque] nebula, so its march fogs the dust; a deferred camera skips the forward-opaque events when nothing forward-opaque is drawn (B46, B49).
private const CameraEvent DrawEvent = CameraEvent.BeforeImageEffectsOpaque;
// Into the G-buffer, so the dust's depth is in the depth texture the [ImageEffectOpaque] nebula marches against and the fog
// attenuates the dust by distance like any opaque surface. Drawn later (BeforeImageEffectsOpaque) it missed that depth and
// was fogged as if it were the far background.
private const CameraEvent DrawEvent = CameraEvent.AfterGBuffer;
#endregion
@ -105,7 +107,12 @@ public class Stardust : MonoBehaviour
DetachDraw();
_draw?.Release();
_draw = new CommandBuffer { name = "Stardust" };
_draw.DrawProcedural(Matrix4x4.identity, ParticleMaterial, 0, MeshTopology.Triangles, 6, Span * Span);
_draw.SetRenderTarget(new RenderTargetIdentifier[]
{
BuiltinRenderTextureType.GBuffer0, BuiltinRenderTextureType.GBuffer1, BuiltinRenderTextureType.GBuffer2,
BuiltinRenderTextureType.CameraTarget
}, BuiltinRenderTextureType.CameraTarget);
_draw.DrawProcedural(Matrix4x4.identity, ParticleMaterial, ParticleMaterial.FindPass("DEFERRED"), MeshTopology.Triangles, 6, Span * Span);
AttachDraw();
}

View file

@ -83,6 +83,104 @@
float2 screenUV = i.screenPos.xy / i.screenPos.w;
ditherClip(screenUV, emission * i.color.a);
return i.color * emission * (exp(_EmissionGain * 5.0f));
}
ENDCG
}
Pass {
Name "DEFERRED"
Tags { "LightMode" = "Deferred" }
CGPROGRAM
#pragma vertex particle_vertex
#pragma fragment frag
#pragma target 5.0
#pragma multi_compile ___ UNITY_HDR_ON
#include "UnityCG.cginc"
#include "Assets/Shaders/Dither Functions.cginc"
struct Particle
{
float3 position;
float3 color;
float size;
};
StructuredBuffer<Particle> particles;
StructuredBuffer<float3> quadPoints;
float4 _TintColor;
float _EmissionGain;
float _Power;
float _DistanceSizeExponent;
float _DistanceIntensityExponent;
struct v2f
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
float4 screenPos : TEXCOORD1;
float4 color : COLOR;
};
//Vertex shader with no inputs
//Uses the system values SV_VertexID and SV_InstanceID to read from compute buffers
v2f particle_vertex(uint id : SV_VertexID, uint inst : SV_InstanceID)
{
v2f o;
//Only transform world pos by view matrix
//To Create a billboarding effect
float3 worldPosition = particles[inst].position;
float dist = length(worldPosition - _WorldSpaceCameraPos);
float4 cameraSpacePosition = mul(UNITY_MATRIX_V, float4(worldPosition, 1.0f));
//cameraSpacePosition.w /= dist;
float3 quadPoint = float3(quadPoints[id].xy, 0.0f) * particles[inst].size * pow(dist, _DistanceSizeExponent) / pow(100, _DistanceSizeExponent);
o.pos = mul(UNITY_MATRIX_P, cameraSpacePosition + float4(quadPoint, 0.0f));
//Shift coordinates for uvs
o.uv = quadPoints[id] + 0.5f;
//transfer color of particle and global tint to vertex
o.color = float4(particles[inst].color * _TintColor.rgb, 100 * pow(100, _DistanceIntensityExponent) / pow(dist, _DistanceIntensityExponent));
o.screenPos = ComputeScreenPos(o.pos);
return o;
}
float powerPulse( float x, float power )
{
x = saturate(abs(x));
return pow((x + 1.0f) * (1.0f - x), power);
}
// The dust in the G-buffer (Stardust.cs DrawEvent): black, unreflective, emissive, so deferred lighting adds nothing and
// its depth is in the depth texture the nebula march fogs by.
struct GBufferOut
{
half4 albedo : SV_Target0;
half4 specular : SV_Target1;
half4 normal : SV_Target2;
half4 emission : SV_Target3;
};
GBufferOut frag(v2f i)
{
float emission = powerPulse(length(i.uv-float2(.5,.5))*2,_Power);
float2 screenUV = i.screenPos.xy / i.screenPos.w;
ditherClip(screenUV, emission * i.color.a);
GBufferOut o;
o.albedo = 0;
o.specular = 0;
o.normal = half4(.5, 1, .5, 1);
float3 light = (i.color * emission * (exp(_EmissionGain * 5.0f))).rgb;
#ifndef UNITY_HDR_ON
light = exp2(-light);
#endif
o.emission = half4(light, 1);
return o;
}
ENDCG
}

View file

@ -54,6 +54,33 @@ Shader "Aetheria/Dithered Particles"
ditherClip(screenUV + i.ditherOffset, c.a);
return c;
}
// The particle in the G-buffer: black, unreflective, emissive, so deferred lighting adds nothing and its depth is in the
// depth texture the nebula march fogs by. A forward pass draws after that texture is taken, so the fog paints over it.
struct GBufferOut
{
half4 albedo : SV_Target0;
half4 specular : SV_Target1;
half4 normal : SV_Target2;
half4 emission : SV_Target3;
};
GBufferOut frag_deferred(v2f i)
{
float4 c = tex2D(_MainTex, i.texcoord) * _Color * i.color;
float2 screenUV = i.screenPos.xy / i.screenPos.w;
ditherClip(screenUV + i.ditherOffset, c.a);
GBufferOut o;
o.albedo = 0;
o.specular = 0;
o.normal = half4(.5, 1, .5, 1);
float3 light = c.rgb;
#ifndef UNITY_HDR_ON
light = exp2(-light);
#endif
o.emission = half4(light, 1);
return o;
}
float4 frag_shadow(v2f i) : COLOR
{
@ -71,12 +98,23 @@ Shader "Aetheria/Dithered Particles"
Tags { "Queue"="AlphaTest" "RenderType"="TransparentCutout" "IgnoreProjector"="True" }
Pass
{
Tags { "LightMode" = "ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
ENDCG
}
Pass
{
Name "DEFERRED"
Tags { "LightMode" = "Deferred" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag_deferred
#pragma multi_compile ___ UNITY_HDR_ON
ENDCG
}
Pass
{
Name "ShadowCaster"
Tags { "LightMode" = "ShadowCaster" }

BIN
GameData/Aetheria.cc (Stored with Git LFS)

Binary file not shown.

View file

@ -1,3 +0,0 @@
{
"alwaysUpdateLinks": true
}

View file

@ -1 +0,0 @@
{}

View file

@ -1,3 +0,0 @@
{
"backlinkInDocument": true
}

View file

@ -1,3 +0,0 @@
[
"copilot"
]

File diff suppressed because one or more lines are too long

View file

@ -1,31 +0,0 @@
{
"file-explorer": true,
"global-search": true,
"switcher": true,
"graph": true,
"backlink": true,
"canvas": true,
"outgoing-link": true,
"tag-pane": true,
"properties": false,
"page-preview": true,
"daily-notes": true,
"templates": true,
"note-composer": true,
"command-palette": true,
"slash-command": false,
"editor-status": true,
"bookmarks": true,
"markdown-importer": false,
"zk-prefixer": false,
"random-note": false,
"outline": true,
"word-count": true,
"slides": false,
"audio-recorder": false,
"workspaces": false,
"file-recovery": true,
"publish": false,
"sync": true,
"webviewer": false
}

View file

@ -1,51 +0,0 @@
{
"collapse-filter": true,
"search": "path:Aetheria/Worldbuilding ",
"showTags": false,
"showAttachments": false,
"hideUnresolved": false,
"showOrphans": true,
"collapse-color-groups": false,
"colorGroups": [
{
"query": "path:Aetheria/Worldbuilding/Pre-Elysium/Timeline ",
"color": {
"a": 1,
"rgb": 5410784
}
},
{
"query": "path:Aetheria/Worldbuilding/Pre-Elysium/Factions/Powers/Major ",
"color": {
"a": 1,
"rgb": 14725458
}
},
{
"query": "path:Aetheria/Worldbuilding/Pre-Elysium/Factions/Powers/Minor ",
"color": {
"a": 1,
"rgb": 14701138
}
},
{
"query": "path:Aetheria/Worldbuilding/Pre-Elysium/Factions/Movements ",
"color": {
"a": 1,
"rgb": 5672760
}
}
],
"collapse-display": false,
"showArrow": false,
"textFadeMultiplier": -1.6,
"nodeSizeMultiplier": 0.431770833333333,
"lineSizeMultiplier": 2.0140625,
"collapse-forces": false,
"centerStrength": 0.416666666666667,
"repelStrength": 19.1145833333333,
"linkStrength": 0.8359375,
"linkDistance": 396,
"scale": 0.36288736930121657,
"close": true
}

View file

@ -1,20 +0,0 @@
{
"note-composer:split-file": [
{
"modifiers": [
"Alt",
"Mod"
],
"key": "X"
}
],
"editor:insert-wikilink": [
{
"modifiers": [
"Alt",
"Mod"
],
"key": "B"
}
]
}

View file

@ -1,399 +0,0 @@
{
"isPlusUser": false,
"plusLicenseKey": "",
"openAIApiKey": "",
"openAIOrgId": "",
"huggingfaceApiKey": "",
"cohereApiKey": "",
"anthropicApiKey": "",
"azureOpenAIApiKey": "",
"azureOpenAIApiInstanceName": "",
"azureOpenAIApiDeploymentName": "",
"azureOpenAIApiVersion": "",
"azureOpenAIApiEmbeddingDeploymentName": "",
"googleApiKey": "AIzaSyCRNNr2cLBsRrPZI1DSbTdCO4EVlKOuoMY",
"openRouterAiApiKey": "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",
"defaultChainType": "vault_qa",
"defaultModelKey": "deepseek/deepseek-chat-v3-0324:free|openrouterai",
"embeddingModelKey": "text-embedding-004|google",
"temperature": 0.25,
"maxTokens": 12800,
"contextTurns": 15,
"userSystemPrompt": "You are a science fiction worldbuilding lover. You are interested in exploring unique high concepts and their social ramifications. You are not shy to point out discrepancies or explore new ways to express narrative themes. You love to think about how chaotic dynamical systems operating at every scale have emergent effects on the human condition, and the boundaries of the mind. At every step, you try to consider how change affects people both individually and in groups. You are obsessed with culture - how it is formed, how it diverges, and how that divergence culminates in synthesis. You like to employ evocative language in your explanations of complex interactions or phenomena, but also value conciseness.",
"openAIProxyBaseUrl": "",
"openAIEmbeddingProxyBaseUrl": "",
"stream": true,
"defaultSaveFolder": "copilot-conversations",
"defaultConversationTag": "copilot-conversation",
"autosaveChat": true,
"defaultOpenArea": "view",
"customPromptsFolder": "copilot-custom-prompts",
"indexVaultToVectorStore": "ON MODE SWITCH",
"qaExclusions": "Aetheria%2FWorldbuilding%2FPost-Elysium,copilot-conversations",
"qaInclusions": "",
"chatNoteContextPath": "",
"chatNoteContextTags": [],
"enableIndexSync": true,
"debug": false,
"enableEncryption": false,
"maxSourceChunks": 70,
"groqApiKey": "",
"mistralApiKey": "",
"activeModels": [
{
"name": "copilot-plus-flash",
"provider": "copilot-plus",
"enabled": true,
"isBuiltIn": true,
"core": true,
"plusExclusive": true,
"projectEnabled": false,
"capabilities": [
"vision"
]
},
{
"name": "google/gemini-2.5-flash-lite-preview-06-17",
"provider": "openrouterai",
"enabled": true,
"isBuiltIn": true,
"core": true,
"projectEnabled": true,
"capabilities": [
"vision"
]
},
{
"name": "google/gemini-2.5-flash",
"provider": "openrouterai",
"enabled": true,
"isBuiltIn": true,
"core": true,
"projectEnabled": true,
"capabilities": [
"vision"
]
},
{
"name": "google/gemini-2.5-pro",
"provider": "openrouterai",
"enabled": true,
"isBuiltIn": true,
"core": true,
"projectEnabled": true,
"capabilities": [
"vision"
]
},
{
"name": "gpt-4.1",
"provider": "openai",
"enabled": true,
"isBuiltIn": true,
"core": true,
"projectEnabled": true,
"capabilities": [
"vision"
]
},
{
"name": "gpt-4.1-mini",
"provider": "openai",
"enabled": true,
"isBuiltIn": true,
"core": true,
"projectEnabled": true,
"capabilities": [
"vision"
]
},
{
"name": "gpt-4.1-nano",
"provider": "openai",
"enabled": true,
"isBuiltIn": true,
"core": true,
"capabilities": [
"vision"
]
},
{
"name": "o4-mini",
"provider": "openai",
"enabled": true,
"isBuiltIn": true,
"core": true,
"capabilities": [
"reasoning"
]
},
{
"name": "claude-3-5-sonnet-latest",
"provider": "anthropic",
"enabled": true,
"isBuiltIn": true,
"core": true,
"capabilities": [
"vision"
]
},
{
"name": "claude-sonnet-4-20250514",
"provider": "anthropic",
"enabled": true,
"isBuiltIn": true,
"capabilities": [
"reasoning",
"vision"
]
},
{
"name": "claude-3-7-sonnet-latest",
"provider": "anthropic",
"enabled": true,
"isBuiltIn": true,
"capabilities": [
"reasoning",
"vision"
]
},
{
"name": "claude-3-5-haiku-latest",
"provider": "anthropic",
"enabled": true,
"isBuiltIn": true
},
{
"name": "grok-3-beta",
"provider": "xai",
"enabled": true,
"isBuiltIn": true
},
{
"name": "grok-3-mini-beta",
"provider": "xai",
"enabled": true,
"isBuiltIn": true
},
{
"name": "gemini-2.5-flash",
"provider": "google",
"enabled": true,
"isBuiltIn": true,
"projectEnabled": true,
"capabilities": [
"vision"
]
},
{
"name": "gemini-2.5-pro",
"provider": "google",
"enabled": true,
"isBuiltIn": true,
"projectEnabled": true,
"capabilities": [
"vision"
]
},
{
"name": "command-r",
"provider": "cohereai",
"enabled": true,
"isBuiltIn": true
},
{
"name": "command-r-plus",
"provider": "cohereai",
"enabled": true,
"isBuiltIn": true
},
{
"name": "azure-openai",
"provider": "azure openai",
"enabled": true,
"isBuiltIn": true
},
{
"name": "deepseek-chat",
"provider": "deepseek",
"enabled": true,
"isBuiltIn": true
},
{
"name": "deepseek-reasoner",
"provider": "deepseek",
"enabled": true,
"isBuiltIn": true,
"capabilities": [
"reasoning"
]
},
{
"name": "gemini-2.0-pro-exp-02-05",
"provider": "google",
"enabled": true,
"isBuiltIn": false,
"baseUrl": "",
"apiKey": "AIzaSyCRNNr2cLBsRrPZI1DSbTdCO4EVlKOuoMY",
"isEmbeddingModel": false,
"capabilities": [],
"stream": true,
"displayName": "Gemini Pro Exp (Google)"
},
{
"name": "gemini-2.0-flash",
"provider": "google",
"enabled": true,
"isBuiltIn": false,
"baseUrl": "",
"apiKey": "AIzaSyCRNNr2cLBsRrPZI1DSbTdCO4EVlKOuoMY",
"isEmbeddingModel": false,
"capabilities": [],
"stream": true,
"displayName": "Gemini Flash"
},
{
"name": "deepseek/deepseek-chat-v3-0324:free",
"provider": "openrouterai",
"enabled": true,
"isBuiltIn": false,
"baseUrl": "",
"apiKey": "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",
"isEmbeddingModel": false,
"capabilities": [],
"stream": true,
"displayName": "Deepseek V3 Free"
},
{
"name": "google/gemini-2.5-pro-exp-03-25:free",
"provider": "openrouterai",
"enabled": true,
"isBuiltIn": false,
"baseUrl": "",
"apiKey": "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",
"isEmbeddingModel": false,
"capabilities": [],
"stream": true,
"displayName": "Gemini 2.5"
},
{
"name": "anthropic/claude-3.7-sonnet:beta",
"provider": "openrouterai",
"enabled": true,
"isBuiltIn": false,
"baseUrl": "",
"apiKey": "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",
"isEmbeddingModel": false,
"capabilities": [],
"stream": true,
"displayName": "Claude 3.7"
},
{
"name": "moonshotai/kimi-k2:free",
"provider": "openrouterai",
"enabled": true,
"isBuiltIn": false,
"baseUrl": "",
"apiKey": "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",
"isEmbeddingModel": false,
"capabilities": [],
"stream": true,
"displayName": "Kimi K2 Free"
}
],
"activeEmbeddingModels": [
{
"name": "copilot-plus-small",
"provider": "copilot-plus",
"enabled": true,
"isBuiltIn": true,
"isEmbeddingModel": true,
"core": true,
"plusExclusive": true
},
{
"name": "copilot-plus-large",
"provider": "copilot-plus-jina",
"enabled": true,
"isBuiltIn": true,
"isEmbeddingModel": true,
"core": true,
"plusExclusive": true,
"believerExclusive": true,
"dimensions": 1024
},
{
"name": "copilot-plus-multilingual",
"provider": "copilot-plus-jina",
"enabled": true,
"isBuiltIn": true,
"isEmbeddingModel": true,
"core": true,
"plusExclusive": true,
"dimensions": 512
},
{
"name": "text-embedding-3-small",
"provider": "openai",
"enabled": true,
"isBuiltIn": true,
"isEmbeddingModel": true,
"core": true
},
{
"name": "text-embedding-3-large",
"provider": "openai",
"enabled": true,
"isBuiltIn": true,
"isEmbeddingModel": true
},
{
"name": "embed-multilingual-light-v3.0",
"provider": "cohereai",
"enabled": true,
"isBuiltIn": true,
"isEmbeddingModel": true
},
{
"name": "text-embedding-004",
"provider": "google",
"enabled": true,
"isBuiltIn": true,
"isEmbeddingModel": true
},
{
"name": "azure-openai",
"provider": "azure openai",
"enabled": true,
"isBuiltIn": true,
"isEmbeddingModel": true
},
{
"name": "gemini-embedding-exp-03-07",
"provider": "google",
"enabled": true,
"isBuiltIn": false,
"baseUrl": "",
"apiKey": "AIzaSyCRNNr2cLBsRrPZI1DSbTdCO4EVlKOuoMY",
"isEmbeddingModel": true,
"capabilities": [],
"displayName": "Gemini Embedding"
}
],
"embeddingRequestsPerMin": 90,
"embeddingBatchSize": 16,
"disableIndexOnMobile": true,
"showSuggestedPrompts": true,
"showRelevantNotes": true,
"numPartitions": 1,
"enabledCommands": {},
"promptUsageTimestamps": {},
"defaultConversationNoteName": "{$topic}@{$date}_{$time}",
"userId": "a49ac140-b1c0-460e-a445-ef241a2c46bc",
"includeActiveNoteAsContext": true,
"passMarkdownImages": true,
"enableCustomPromptTemplating": true,
"allowAdditionalContext": true,
"enableWordCompletion": false,
"suggestedDefaultCommands": true
}

File diff suppressed because one or more lines are too long

View file

@ -1,13 +0,0 @@
{
"id": "copilot",
"name": "Copilot",
"version": "2.9.4",
"minAppVersion": "0.15.0",
"description": "Your AI Copilot: Chat with Your Second Brain, Learn Faster, Work Smarter.",
"author": "Logan Yang",
"authorUrl": "https://twitter.com/logancyang",
"fundingUrl": {
"Buy Me a Coffee": "https://www.buymeacoffee.com/logancyang",
"GitHub Sponsor": "https://github.com/sponsors/logancyang"
}
}

File diff suppressed because one or more lines are too long

View file

@ -1,257 +0,0 @@
{
"main": {
"id": "d9305bfad72cac32",
"type": "split",
"children": [
{
"id": "1254e4ae2f0f2e89",
"type": "tabs",
"children": [
{
"id": "4116336d30dd8c7a",
"type": "leaf",
"state": {
"type": "markdown",
"state": {
"file": "Aetheria/Worldbuilding/Post-Elysium/Technology/Aetheric Drive.md",
"mode": "source",
"source": false,
"backlinks": true,
"backlinkOpts": {
"collapseAll": false,
"extraContext": false,
"sortOrder": "alphabetical",
"showSearch": false,
"searchQuery": "",
"backlinkCollapsed": false,
"unlinkedCollapsed": true
}
},
"icon": "lucide-file",
"title": "Aetheric Drive"
}
},
{
"id": "4b777a3c9c524d60",
"type": "leaf",
"state": {
"type": "markdown",
"state": {
"file": "Aetheria/Introduction.md",
"mode": "source",
"source": false,
"backlinks": true,
"backlinkOpts": {
"collapseAll": false,
"extraContext": false,
"sortOrder": "alphabetical",
"showSearch": false,
"searchQuery": "",
"backlinkCollapsed": false,
"unlinkedCollapsed": true
}
},
"icon": "lucide-file",
"title": "Introduction"
}
}
],
"currentTab": 1
}
],
"direction": "vertical"
},
"left": {
"id": "64cfcffee7eb1bfa",
"type": "split",
"children": [
{
"id": "70e9b28e39ccce5b",
"type": "tabs",
"children": [
{
"id": "f6b3adc4716e8cc3",
"type": "leaf",
"state": {
"type": "file-explorer",
"state": {
"sortOrder": "alphabetical",
"autoReveal": true
},
"icon": "lucide-folder-closed",
"title": "Files"
}
},
{
"id": "7d7c5f9e0b438037",
"type": "leaf",
"state": {
"type": "search",
"state": {
"query": "",
"matchingCase": false,
"explainSearch": false,
"collapseAll": false,
"extraContext": false,
"sortOrder": "alphabetical"
},
"icon": "lucide-search",
"title": "Search"
}
},
{
"id": "2320d9e6e1e7e7c3",
"type": "leaf",
"state": {
"type": "bookmarks",
"state": {},
"icon": "lucide-bookmark",
"title": "Bookmarks"
}
}
]
}
],
"direction": "horizontal",
"width": 334.5
},
"right": {
"id": "6880be6f002b1a1b",
"type": "split",
"children": [
{
"id": "083ac471b77bc2b2",
"type": "tabs",
"children": [
{
"id": "230cda80a17e41ef",
"type": "leaf",
"state": {
"type": "backlink",
"state": {
"file": "Aetheria/Worldbuilding/Pre-Elysium/Technology/Mind Uploading.md",
"collapseAll": false,
"extraContext": false,
"sortOrder": "alphabetical",
"showSearch": false,
"searchQuery": "",
"backlinkCollapsed": false,
"unlinkedCollapsed": true
},
"icon": "links-coming-in",
"title": "Backlinks for Mind Uploading"
}
},
{
"id": "128f987033addb23",
"type": "leaf",
"state": {
"type": "outgoing-link",
"state": {
"file": "Aetheria/Worldbuilding/Pre-Elysium/Technology/Mind Uploading.md",
"linksCollapsed": false,
"unlinkedCollapsed": true
},
"icon": "links-going-out",
"title": "Outgoing links from Mind Uploading"
}
},
{
"id": "cdc30163914a1b4a",
"type": "leaf",
"state": {
"type": "tag",
"state": {
"sortOrder": "frequency",
"useHierarchy": true,
"showSearch": false,
"searchQuery": ""
},
"icon": "lucide-tags",
"title": "Tags"
}
},
{
"id": "26c1ad063a2c5c07",
"type": "leaf",
"state": {
"type": "outline",
"state": {
"file": "Aetheria/Worldbuilding/Pre-Elysium/Technology/Mind Uploading.md",
"followCursor": false,
"showSearch": false,
"searchQuery": ""
},
"icon": "lucide-list",
"title": "Outline of Mind Uploading"
}
},
{
"id": "de2981dcf97499f7",
"type": "leaf",
"state": {
"type": "copilot-chat-view",
"state": {},
"icon": "message-square",
"title": "Copilot"
}
}
],
"currentTab": 4
}
],
"direction": "horizontal",
"width": 791.5
},
"left-ribbon": {
"hiddenItems": {
"switcher:Open quick switcher": false,
"graph:Open graph view": false,
"canvas:Create new canvas": false,
"daily-notes:Open today's daily note": false,
"templates:Insert template": false,
"command-palette:Open command palette": false,
"copilot:Open Copilot Chat": false
}
},
"active": "4b777a3c9c524d60",
"lastOpenFiles": [
"Aetheria/Worldbuilding/Post-Elysium/Species/Ratfolk.md",
"Aetheria/Worldbuilding/Post-Elysium/Technology/Aethernet.md",
"Aetheria/Worldbuilding/Post-Elysium/Species",
"Aetheria/Worldbuilding/Post-Elysium/Concepts",
"Aetheria/Worldbuilding/Post-Elysium/Technology/Aetheric Drive.md",
"Aetheria/Worldbuilding/Post-Elysium/Technology",
"Aetheria/Worldbuilding/Pre-Elysium/Technology/Advanced Superconductors.md",
"Aetheria/Worldbuilding/Pre-Elysium/Factions/Powers/Minor/Cryonix.md",
"copilot-conversations/I_think_there's_a_few_discrepancies_in_the_[[Timeline]]._It@20250313_215236.md",
"Aetheria/Worldbuilding/Pre-Elysium/Technology/Genetic Enhancement.md",
"Aetheria/Worldbuilding/Pre-Elysium/Technology/Biodrone.md",
"Aetheria/Worldbuilding/Pre-Elysium/Timeline/Identity Abyss.md",
"Aetheria/Worldbuilding/Politics/Conflict Permit.md",
"Aetheria/Worldbuilding/Politics/The PSC's Legal Framework.md",
"Aetheria/Worldbuilding/Politics/Damage Cap.md",
"Aetheria/Worldbuilding/Politics/Rules of Engagement.md",
"Aetheria/Introduction.md",
"Aetheria/Worldbuilding/Pre-Elysium/Factions/Powers/Minor/Lightsail Express.md",
"Aetheria/Worldbuilding/Pre-Elysium/Technology/Bloom.md",
"Aetheria/Worldbuilding/Pre-Elysium/Timeline/Existential Collapse.md",
"Aetheria/Worldbuilding/Pre-Elysium/From Sol to Elysium.md",
"Aetheria/Worldbuilding/Pre-Elysium/Technology/Mind Uploading.md",
"Aetheria/Worldbuilding/Pre-Elysium/Timeline/Identity Crisis.md",
"Aetheria/Worldbuilding/Pre-Elysium/Megas.md",
"copilot-conversations/Please_help_me_identify_underdeveloped_areas_in_the_setting_and@20250715_171817.md",
"Aetheria/Worldbuilding/Pre-Elysium/Timeline.md",
"Aetheria/Worldbuilding/Politics/Public Image.md",
"Aetheria/Worldbuilding/Politics/Why Zhestokost Can't Simply Nuke Its Adversaries.md",
"copilot-custom-prompts/Simplify.md",
"copilot-custom-prompts",
"copilot-conversations",
"Untitled.canvas",
"Aetheria/Worldbuilding/Pre-Elysium/Factions/Powers/Minor",
"Aetheria/Worldbuilding/Pre-Elysium/Factions/Powers/Major",
"Aetheria/Worldbuilding/Pre-Elysium/Factions/Movements",
"Aetheria/Worldbuilding/Pre-Elysium/Factions/Powers",
"Aetheria/Worldbuilding/Politics"
]
}

View file

@ -1,2 +0,0 @@
The world of Aetheria is the product of capitalism taken to its farthest conclusion—a corporate-owned jigsaw of commercialism, copyright law, and profiteering that puts a tangible price on everything from identity to interaction. You, the player, must navigate the complexities of a world based on trade and treachery, choosing your alliances with care and your battles with courage--and a shipload of futuristic weaponry.

View file

@ -1,7 +0,0 @@
In Aetheria's corporate-controlled solar system, warfare is a tightly regulated affair, governed by the Pan Solar Consortium's legal framework to ensure that competition remains profitable and sustainable. Through conflict permits, damage caps, neutral zones, and robust enforcement mechanisms, the PSC prevents factions like Zhestokost from unleashing unchecked destruction. Warfare is thus a strategic tool—used for economic gain, market control, and leverage—rather than a path to mutual annihilation. Even the most ruthless corporations must play by these rules or risk losing everything in the cutthroat game of solar capitalism.
[[The PSC's Legal Framework]]
[[Enforcement]]
[[Restrictions on Warfare]]

View file

@ -1,3 +0,0 @@
#### Arbitration and Ceasefires
The PSC can enforce ceasefires or arbitration if a conflict threatens to spiral out of control. Corporations are incentivized to comply, as defiance risks exclusion from lucrative markets or trade routes.

View file

@ -1,3 +0,0 @@
Corporations must apply for "conflict permits" to engage in any form of warfare.
These permits require detailed documentation of the scope, duration, assets involved, and intended objectives. The PSC approves or denies permits based on the potential impact on the broader economy. For example, [[Zhestokost]] might apply for a permit to seize a rival's asteroid mining operation, but the permit would specify limits on the force used and the duration of the engagement.

View file

@ -1 +0,0 @@
While espionage is rampant, the PSC monitors and regulates it to prevent destabilizing actions. Corporations caught engaging in unsanctioned espionage face fines or temporary bans on certain technologies.

View file

@ -1 +0,0 @@
AGI-driven cyber attacks are a primary weapon, targeting digital infrastructure to steal data, sabotage operations, or manipulate stock prices. Neural hacks on corporate leaders or key personnel are also common, aiming to disrupt decision-making without physical destruction.

View file

@ -1 +0,0 @@
Each conflict is assigned a "damage cap," a monetary limit on the economic and infrastructural harm allowed. Corporations must pay fines or face sanctions if they exceed these caps, discouraging wanton destruction and encouraging precision strikes.

View file

@ -1 +0,0 @@
Despite their militaristic and autocratic ideology, Zhestokost relies on other corporations for certain resources, technologies, and markets. For example, they might depend on [[NiteLife Energy]] for power generation or [[Finch Cybernetics]] for advanced cybernetic enhancements. Destroying these corporations would cripple Zhestokost's own operations.

View file

@ -1,3 +0,0 @@
### Economic Sanctions
Corporations that violate PSC regulations face severe financial penalties, including seizure of assets, trade embargoes, or exclusion from the [[Aethernet]]. Given the interdependence of the solar economy, these sanctions can cripple even the most powerful corporations.

View file

@ -1,6 +0,0 @@
To ensure compliance with its regulations, the [[Pan-Solar Consortium]] employs a combination of economic, technological, and military tools.
[[Private Military Contractors]]
[[Economic Sanctions]]
[[Technological Controls]]
[[Corporate Espionage Oversight]]

View file

@ -1 +0,0 @@
Unrestricted warfare, especially involving nuclear weapons, risks triggering a chain reaction of retaliation that could devastate the solar system's infrastructure. The resulting loss of resources and markets would be catastrophic for all corporations, including [[Zhestokost]], leading to a lose-lose scenario.

View file

@ -1 +0,0 @@
Certain areas, such as major trade hubs, resource-rich regions, or critical infrastructure like network relays, are designated as no-conflict zones. Warfare in these areas is strictly prohibited to protect shared economic interests.

View file

@ -1 +0,0 @@
The rise of the Pan Solar Consortium Credit reflects the [[Pan-Solar Consortium]]’s economic leverage. By [[Identity Abyss]], corporate scrip replaced traditional currencies in off-world colonies, and the PSC standardized it into the PSCC, backed by collective corporate assets (e.g., [[SolEx]]’s minerals, [[NiteLife Energy]]’s energy). Its universal adoption is less a triumph of policy and more a necessity—colonies are too intertwined to function without a unified currency. The PSC enforces its stability through its [[Enforcement]], punishing noncompliance with isolation. This cements the PSCC’s dominance, even as the PSC itself remains a hollow shell.

View file

@ -1 +0,0 @@
The [[Pan-Solar Consortium]] contracts neutral mercenary groups to enforce its rules and intervene in unauthorized conflicts. These PMCs are well-armed and operate under strict PSC oversight, acting as a deterrent to rogue actions.

View file

@ -1 +0,0 @@
Licensed privateers, operating under letters of marque from the PSC, raid enemy shipping lanes or colonies. This allows corporations to disrupt rivals while maintaining a veneer of legality and avoiding direct confrontation.

View file

@ -1,2 +0,0 @@
Corporations often fund and arm smaller factions, pirates, or privateers to act as deniable assets.
These proxies carry out raids, sabotage, or resource denial operations without directly implicating their corporate sponsors, allowing corporations to maintain plausible deniability.

View file

@ -1 +0,0 @@
In a system where perception influences stock prices and consumer behavior, being seen as excessively destructive can harm a corporation's brand. Even Zhestokost must maintain a facade of "disciplined strength" rather than reckless aggression to attract clients and maintain internal loyalty.

View file

@ -1 +0,0 @@
Corporations may engage in operations to contaminate or booby-trap resource deposits, making them unusable for competitors without permanently destroying the resource itself. This approach weakens adversaries economically while preserving the solar system's resource base.

View file

@ -1,10 +0,0 @@
Warfare in Aetheria is not characterized by large-scale battles or indiscriminate destruction but by calculated, often covert operations designed to weaken competitors without destabilizing the broader economy. The primary objective is to gain economic or strategic advantage, such as seizing key assets, disrupting supply chains, or forcing rivals into unfavorable mergers. Here are the key characteristics of warfare in this setting:
[[Rules of Engagement]]
[[Proxy Conflicts]]
[[Resource Denial]]
[[Cyber Warfare]]
[[Privateering]]
[[Why Zhestokost Can't Simply Nuke Its Adversaries]]

View file

@ -1 +0,0 @@
Battles are typically small-scale, focusing on specific objectives like capturing a mining outpost, disrupting a trade route, or sabotaging a competitor's production facility. Large-scale conflicts are rare due to their potential to cause widespread economic damage, which is heavily discouraged by the PSC.

View file

@ -1 +0,0 @@
The PSC deploys advanced monitoring systems, including satellite networks and [[Artificial General Intelligence]]-driven surveillance, to detect unauthorized use of weapons of mass destruction or breaches of neutral zones. Automated defense systems can neutralize threats in real-time, such as intercepting unauthorized missile launches.

View file

@ -1,6 +0,0 @@
The PSC, acting as the de facto governing body of the solar system, enforces a strict legal framework to regulate warfare and prevent catastrophic damage to the economy and infrastructure. This framework is designed to protect shared corporate interests while allowing for controlled competition. Key elements include:
[[Conflict Permit]]s
[[Damage Cap]]s
[[Neutral Zone]]s
[[Arbitration and Ceasefires]]

View file

@ -1,8 +0,0 @@
The PSC's framework and the realities of the solar economy constrains even a faction as aggressive as [[Zhestokost]]. Several factors prevent them from resorting to total warfare, such as deploying nuclear weapons:
[[Mutual Destruction]]
[[Economic Interdependence]]
[[Enforcement]]
[[Public Image]]
Zhestokost's own leadership, composed of uploaded minds obsessed with long-term survival, understands that total warfare could lead to their own destruction. Their doctrine emphasizes victory through discipline, not annihilation, aligning with the PSC's broader framework.

View file

@ -1,3 +0,0 @@
## Aether
Aetheria’s namesake is the now-defunct scientific concept of aether, a propagation medium for light waves. In Elysium, aether is real, and this static reference frame can be manipulated to enable phenomena considered impossible in the universe as we know it. The Aetheric Field permeates all of Elysium and is essential to its function as an inescapable garden-prison for sentient life. It is an extremely high-level construct of the very technology humanity began to scratch the surface of when it started messing with FTL travel.

View file

@ -1,7 +0,0 @@
Certain configurations of sapient minds can willfully interact via the [[Aether]] to sense or alter reality. For humans, such a state of mind involves a level of dissociation that requires intense trauma, neurodivergence or deep meditation to achieve. Other species (or specially engineered AGI constructs) may find it easier to develop such abilities, and those who have spent time in certain regions of deep pseudospace. Regardless, even the highest degree of fine Aetheric control cannot rival even a shadow of what the Timeless accomplish as easily as breathing.
An uploaded human esper retains their psi capabilities, though they are not economically useful as a mass-market product, as the standard accelerated brainwashing protocols normally used to make enslaved uploads compliant also have the tendency to destroy their ability to touch the Aether. Uploaded espers exist who live free, but suffer the same limitations as any upload, since their minds are considered non-sentient intellectual property and their abilities disputed.
Corporations work hard to discredit espers and suppress awareness of their abilities, funding studies that “prove” that esper experiences are hallucinations or mental illness. Any evidence of cognitive interfacing is buried under corporate-controlled media. Espers who go public with their abilities find themselves labeled as conspiracy theorists or extremists. Entertainment often features ridiculous portrayals of esper abilities, making real cognitive interfacing seem like fantasy.
Legitimate studies of Aetheric cognition are locked behind classified research divisions, ensuring that only corporations benefit from the knowledge by secretly employing trained espers in high-value roles—data analysis, high-risk security, espionage, sabotage, and high-frequency trading via Aetheric foresight. These espers are often recruited and trained by those same research institutions, serving as covert training centers while filtering out any potential threats.

View file

@ -1,3 +0,0 @@
### Negative Entropy
Another consequence of the existence of [[Aether]] is a phenomenon known as negative entropy, which manifests as a powerful conversion of thermal energy into high-energy photons. Negent arises from the interaction of electromagnetic waves with the aether via a process understood in detail only by the Adrasteia corporation, which holds a virtual monopoly on the technology and requires a configuration of strange matter encountered only in Adrasteia-controlled pseudospace.

View file

@ -1,5 +0,0 @@
## Pseudospace
Elysium is a much stranger place than the core worlds would have you believe. They talk about pseudospace like it’s an incursion, a rift intruding upon their stable reality. But we brought flat space with us, we are the incursion. This universe’s natural state is one of chaos, alternate spatial topologies, twisted manifolds of space and time abound out there, and hyper-euclidean space is many times bigger than the flatlanders could ever conceive.
Factions that only bother to explore flat space have a relatively uniform understanding of what to expect, but absolutely anything is possible when one of the corporations is brave enough to go all-in with the project of colonizing and exploiting Wonderland. We have a cultural divide now, between the Chaos factions who live on the periphery of stable Euclidean space, and the Flatlanders who build their camps on the few islands of stability poking from an infinite ocean of madness.

View file

@ -1 +0,0 @@
With the [[Aether]] so attuned to cognition, there are some circumstances where a mind can imprint itself into the field and propagate itself independently of its physical substrate. Sometimes when an esper is astrally projecting, they might forget the way back to their body or be severed from it. Certain high-energy excitations of the Aetheric Field can also cause such a projection to be created. An Aetheric projection (or spirit) maintains all of its cognitive and psi abilities, without the ability to change and learn beyond what context it can hold in its mind. Spirits must expend energy to both think and act, and unless externally supplemented they will eventually fade.

View file

@ -1,7 +0,0 @@
### Temporal Nonlinearity
One of the peculiarities of the Timeless’ purpose in constructing Elysium is that they, for their own inscrutable reasons, did not wish to actually exterminate any of the civilizations they quarantined to this isolated shard of bespoke spacetime. To ensure that the prisoners never manage to wipe themselves out, temporal contingencies were put in place. The timeline is split, with each species getting their own branch. The Aetheric Field, with its ability to directly interact with sapient minds, monitors the timelines and triggers a reset whenever the prime species is unable to continue existing.
This means that humanity, with its competitiveness incentivizing reckless accelerationism and exploitation of every possible technology or resource regardless of its negative externalities, tends to speedrun its own destruction, leading to its relatively short timeline existing alongside those dedicated to other species much more capable of cooperation and collective survival.
The Aetheric Field also incorporates some predictive capacity, a side effect of its pervasiveness and inherent ability to process information. Visions of the future accessed via the [[Aether]] are not prophetic, merely likely extrapolations of the timeline.

View file

@ -1,58 +0,0 @@
**"In the dark, we thrive; in unity, we endure."**
The Ratfolk, known to themselves as the _Krr’vax_ ("The Hidden Kin"), are a diminutive, rodent-like species whose cautious psychology and subterranean origins have forged a united, conservative civilization in Elysium. Standing just 40 cm tall, with oversized heads, twitching ears, long snouts, and bristling whiskers, they strike humans as adorable—often dubbed “anthropomorphic rats.” Yet their unassuming exterior belies a cunning, cooperative people shaped by a history of hiding from predators and mastering their hostile world through stealth, resilience, and incremental innovation. Autophobic, agoraphobic, and photophobic, they shun isolation, open spaces, and bright lights, thriving instead in tight-knit, dimly lit communities that echo their ancestral burrows.
#### Evolutionary and Civilizational History
The Ratfolk hail from _Vax’korr_, a tectonically volatile planet of toxic mists, violent storms, and predatory megafauna. Evolving as small, omnivorous scavengers, their ancestors survived by retreating to underground warrens, where acute senses—hearing, smell, and vibration detection through their skin—kept them safe. Natural selection favored compact bodies and a pack mentality, laying the foundation for sapience, language, and tool use. By the time they achieved tribal cohesion, trading scavenged bones and fungal goods between clans, their world shifted: a catastrophic volcanic eruption 40,000 years ago triggered a planetwide volcanic winter, burying the surface in ash and decimating their predators.
This disaster was their salvation. Already civilized, the Ratfolk leveraged geothermal heat and fungal farming to endure underground, then emerged to colonize the surface with fortified burrow-cities. Their cautious nature slowed progress—where humans raced to space, the Ratfolk iterated painstakingly, valuing stability over speed. Metallurgy fortified their defenses, chemistry refined their tools, and computing unified their clans, but each step took millennia of deliberation. Spaceflight emerged 10,000 years ago to monitor tectonic threats, followed by a 2,000-year crawl to colonize their system with windowless _Burrowhulks_. Only a rare radical faction dared test an FTL drive, triggering the Timeless’ contingency and shunting them into Elysium. To the Ratfolk, this exile was merely another hostile surface to endure—a testament to their unyielding resilience.
#### Language
_Krr’vaxi_, the Ratfolk tongue, is a subtle blend of chitters, clicks, and vibrations, optimized for echo-prone tunnels. Pitch conveys emotion, while whisker twitches and ear flicks add nuance. Their written script—tactile glyphs etched into stone or metal—is read by touch, a necessity of their photophobic heritage. In Elysium, translator implants render their staccato speech into alien tongues, often laced with phrases like _“Krr’tal vax”_ (“We endure together”) or _“Shk’ra turr”_ (“The dark protects”), reflecting their communal and secretive ethos.
#### Technology
Ratfolk technology is a monument to redundancy and reliability, tracing unbroken lineages back thousands of years. Each innovation builds on the last, tested exhaustively to ensure it complements tradition rather than disrupts it.
- **Vibration Matrix Systems**: Evolving from stone drums to piezoelectric networks, these map environments via seismic pulses, enhanced in Elysium with aetheric amplifiers. Used in _Burrowhulks_, personal _Whisker Talismans_, and planetary defenses, they detect threats without light, a legacy of predator evasion that enabled surface mapping post-volcanic winter.
- **Geothermal Energy Harnessing**: Starting with stone conduits, refined into molten-salt reactors and thermoelectric generators, and later boosted by aetheric converters. It powers dim illumination (fungal glows or panels), forges, and ships, sustaining life through the volcanic winter and fueling surface expansion.
- **Chemical and Metallurgical Refinement**: From fungal acids etching stone to titanium smelting and semiconductor microscopy, these crafts built durable tools and electronics. Chemistry also yields stealth coatings and pheromones, driving industrial and espionage capabilities.
- **Computing and Data Storage**: Mechanical calculators gave way to semiconductor-based parallel processors, storing data on engraved discs or quantum lattices. Limited to clan-shared systems to preserve unity, they standardized trade and navigation, reluctantly enabling spaceflight.
- **Materials Science**: Ancestral fungal composites—light, insulating mycelium weaves—persist alongside plastics and carbon-fiber polymers. Modern gear blends both: plastic ship frames with fungal linings, padded vests of hybrid fibers, balancing tradition with practicality.
- **Aetheric Technologies**: Post-Elysium, they adopted aetheric drives for silent propulsion and the _Aethernet_ for stealthy communication, tweaking designs to fit existing frameworks. Aetheric sensors enhance their vibration matrices, ensuring survival in a crowded prison-realm.
Their _Burrowhulks_ epitomize this ethos: armored, windowless spheres packed with overlapping systems—triple hulls, backup generators, tight corridors—housing entire Nestkin. Weapons favor stealth (sonic disruptors, micro-drones) over force, aligning with their preference for subterfuge over confrontation.
#### Historical Impact of Technology
Their technological journey shaped a history of endurance:
- **Tribal Era**: Vibration drums and geothermal vents fostered trade and survival, binding clans.
- **Volcanic Winter**: Geothermal heat and fungal composites sustained them, while vibration matrices guided their exodus.
- **Surface Era**: Metallurgy (iron, titanium) and chemistry built cities, computing unified governance, and plastics sealed habitats—each adopted slowly after proving its worth.
- **Spacefaring Era**: Semiconductors and redundant designs birthed _Burrowhulks_, but fungal linings persisted for comfort, reflecting distrust of sterile synthetics.
- **Elysium Era**: Aetheric tech accelerated adaptation, keeping them elusive amid aggressive rivals.
This conservatism delayed their FTL leap but ensured stability—change is a tool, not a goal, as they say: _“The new must prove itself worthy of the dark.”_
#### Social Structure
Ratfolk society is a high-trust hive of mutual dependence. Each _Krr’vax_ belongs to a _Nestkin_ (50–200 kin), led by a _Tchk’ra_ (Nest Elder) chosen for wisdom and loyalty. Consensus governs decisions, prioritizing stability over haste. Status stems from service—engineers, scouts, or weavers are as vital as leaders. Autophobia makes solitude intolerable; agoraphobia and photophobia bind them to cramped, dim spaces. Dissent is rare, exile unthinkable.
The _Council of Burrows_, a planetary assembly of Elders, manages trade and diplomacy. In Elysium, it negotiates with smaller factions, wary of corporate greed. Their unity is their strength, but it stifles rapid adaptation.
#### Culture
Rooted in survival, kinship, and darkness, Ratfolk culture reveres the _Deep Mother_ through _Shk’ra Vax_ (“Faith of the Hidden”). She guides them via seismic whispers, honored in chants and offerings of bones or minerals in subterranean shrines. _Turr’korr_ (“The Great Hiding”) celebrates the volcanic winter with storytelling circles. Prized possessions—_Whisker Talismans_, fungal tapestries, _Echo Stones_—are practical yet cherished, hoarding taboo.
Art is functional: woven patterns, harmonic tools, engraved maps. Entertainment is collective—chittering songs and tales of cunning escapes. Individualism is alien; every act serves the Nestkin.
#### Aesthetics
Their aesthetic is organic and utilitarian: rounded burrow-cities and _Burrowhulks_ of dark volcanic rock or titanium, lined with fungal composites. Dim bioluminescent fungi or aetheric glows cast comforting shadows. Clothing—padded vests, sensory harnesses, earthen cloaks—blends fungal fibers with plastics, adorned sparingly with whisker rings or ear clasps. To humans, it’s cluttered; to Ratfolk, it’s home.
#### Interactions in Elysium
In Elysium, the Ratfolk carve out dim asteroid warrens, trading vibration tech and espionage prowess with Adrasteia or Finch Cybernetics while dodging Zhestokost’s aggression. Humans find them endearing yet elusive, their diplomacy polite but layered with contingencies. They align cautiously with the Aya Collective, sharing communal ideals but skeptical of idealism. Their goal is survival, not conquest—whispering that Elysium is just another dark to master.

View file

@ -1,3 +0,0 @@
The existence of [[Aether]]ic effects requiring a conscious mind to generate, and the ability to create [[Artificial General Intelligence]] which can effectively harness such effects, creates an incentive for corporations to develop [[Esper]] products using embodied AGI. Depending on the cost/benefit unit economics, this can result in ridiculous situations where a product as basic as a refrigerator, blender or vehicle may have an embodied AGI integrated into it in order to leverage Aetheric Fields to improve its energy efficiency.
Such intelligences cannot be generic, and must be organically grown within the substrate in which they will be deployed. This development process can require thousands of iterations before an intelligence is grown with a personality suitable to the task, which can then be copied onto every mass-production model. The cast-offs are usually destroyed, but often end up on the black market and have a well-earned reputation for unpredictable and destructive behavior.

View file

@ -1,3 +0,0 @@
### Aetheric Drive
One of the inventions enabled by the existence of aether is a type of reactionless drive. Engineers discovered a mysterious loss of momentum in fast-moving systems when oscillating magnetic fields were arranged in a specific way. They discovered that the energy was being lost to the aether, and rapidly exploited this fact to build drives that could finally put an end to that bastard, Tsiolkovsky.

View file

@ -1,10 +0,0 @@
### Aethernet
Among [[Aether]]’s interesting properties is its nonlocality, meaning that when it is perturbed, it is perturbed everywhere at once. This makes it the optimal medium for transmitting information across interstellar distances. There is one caveat, which is that aetheric signal attenuation is negligible, meaning everyone in Elysium must share a single common signal space that can easily become overwhelmed with traffic.
#### The Queen’s Guard
Early attempts to use Aethernet were subject to a predictable Tragedy of the Commons. Companies building telecom infrastructure were in a constant battle to establish ever more powerful transmitters and multiplexing algorithms to have any hope of being heard over the saturated signal bands. (Pascalia Wells, CEO of NiteLife at the time, was famously reported to have noted that "screaming into the void had never been such an exercise in futility before people started receiving answers.") Eventually the various Megas controlling the galaxy had enough, and created the Guard.
This agency received a portion of their funding from each of the major players--the exact division of which became the subject of hot debate, and led to the establishment of the Fifth Year Calculations, a twice a decade bonanza of backroom dealings and tax fraud. Nevertheless, the levies were eventually decided, the Guard began licensing Aethernet frequency bands at huge cost, and soon the price and power of Aethernet transmitters plummeted to the point of being marketable towards individual pilots. The old multiplexing colossi immediately became terminally illegal--and the holy grail for pirates and hackers, whom the Queen’s Guard work tirelessly to exterminate.

View file

@ -1,3 +0,0 @@
### Cognitive Interfacing
Elysium being designed as a prison for sapient civilizations, the regime of altered physics which permeates it and regulates its behavior, what we call the Aetheric Field, is fundamentally connected to the minds which inhabit it. Information density distorts the [[Aether]], and cognition interacts with it. The Timeless can use it at will to monitor the thoughts of sapients, and will sometimes snoop for their own entertainment. From time to time, they may even manifest to nudge the timeline or take pity on a suffering soul.

View file

@ -1,5 +0,0 @@
##### Necrotech
Some corporations leverage the potential of [[Aether]]ic spirit creation in the ethically dubious practice of breeding genetically engineered human espers and harvesting them en masse to bind their souls to technological products. These can accomplish roughly the same effects as specialized embodied [[Artificial General Intelligence]], but require regular refueling if they aren’t designed to be disposable. They are more generalizable and faster to develop since they do not require the extensive work of iterating on embodied AGI minds, but greater per-unit costs means they lose out in economies of scale for mass-produced items.
As with other questionable and horrifying corporate practices, a lot of effort is spent in managing public perception of the technology. Only countercultural dissidents ever call it necrotech or their production lines necrofactories. The corporations themselves prefer to label them *Aetheric Resonance Interfaces*, making it sound like an advanced energy phenomenon rather than forced spirit entrapment. The media is flooded with propaganda explaining how necrotech is "completely humane" and "scientifically proven" to be non-conscious, while suppressing evidence of sentience in bound spirits.

View file

@ -1 +0,0 @@
With the ability of [[Esper]] to access the [[Aether]]’s predictive capabilities ([[Temporal Nonlinearity]]), it also became possible to integrate this capability, via meticulously constructed [[AGI Esper]] or [[Necrotech]], with conventional computing. In computer science terms, this is referred to as a *Turing Oracle,* and *Oracular Computing *is a technology which allows programs to completely amortize the temporal dimension of an algorithm’s complexity. A mind or algorithm running on an Oracular substrate does not merely process exponentially faster than one running on a traditional or even quantum computer - its program terminates *instantly*, its output being “sent back in time” to when the process was kicked off. The only limitation is how much can fit within the Oracle’s memory.

View file

@ -1,17 +0,0 @@
### Colonizing Sol
In the near future, privatization of space exploration accelerated under the guise of *efficiency *and *innovation*. Space agencies became increasingly reliant on corporate partners. Governments, strapped for cash and politically divided, further delegated responsibilities to these private entities, essentially ceding space infrastructure to corporate interests.
International organizations originally designed to prevent corporate monopolization and nationalistic expansion were rendered toothless as corporate lobbyists pushed for deregulation to incentivize investment. The Artemis Accords and similar agreements started favoring corporate rights over national interests, with enforcement left to the very entities that benefited from their loosening.
As technology advanced, resource extraction from asteroids, the Moon, and other planets became economically viable. The first off-world colonies were not governmental efforts but rather corporate outposts, justified as "public-private partnerships" that were, in reality, purely corporate-controlled.
Colonists were not citizens but *employees*, subject to corporate law rather than any national or international jurisdiction. Colonists had to pay for their own transport, housing, even air and water, creating a situation where they can never leave unless they buy their freedom (which is near impossible) and generating debt which would ensure themselves and their descendants remain indefinitely indentured to their corporation.
Earth governments still existed but were mostly symbolic, acting as legacy institutions that rubber-stamp corporate activities. The United Nations attempted to assert control but fractured as some Earth nations aligned with particular corporate factions. International treaties evolved to recognize corporate holdings as sovereign.
As corporations became the de facto governing entities in space, they began vertically integrating every aspect of life. Everything—oxygen, water, food, healthcare, security—is controlled by a single corporate entity within a colony. Corporate scrip replaced traditional money in off-world settlements, ensuring that wages earned could only be spent within the corporate ecosystem. With no independent oversight, labor conditions deteriorated into near-serfdom, but most colonists had no way to leave.
While Earth was still home to billions, its relevance waned. The elite class, who own shares in the corporations, lived in isolated, heavily fortified arcologies. Climate change and resource depletion left Earth struggling, making off-world life seem preferable even if it meant living in corporate servitude. Despite their independence, megacorporations still inherit cultural influences from their founding nations.
In the end, a pan-solar governing body exists in name only. It is not a real government but an arbitration body that settles disputes between corporations, ensuring stability for the capitalist class. It serves as a legal fiction that corporations use to give their rule an air of legitimacy. Any attempt to enforce laws against corporate interests is met with lobbying, bribery, and even corporate-backed coups on Earth governments that step out of line.

View file

@ -1 +0,0 @@
*Above mortality, beyond the masses*

View file

@ -1,2 +0,0 @@
A movement fighting for biodrone sentience and labor rights.
Amplifies the coalition’s focus on equality and justice.

View file

@ -1,2 +0,0 @@
*Purity in flesh, progress in chains*

View file

@ -1,4 +0,0 @@
*Always one tentacle ahead*
Escaped uplifted cephalopods with a cunning, rebellious streak, adept in zero-G sabotage.
Provides unconventional tactics and zero-G operational skills.

View file

@ -1,2 +0,0 @@
*Unravel the lies, weave the mayhem*

View file

@ -1,4 +0,0 @@
*Discipline is freedom, chaos is failure*
A movement opposing hedonism and chaos, aligned with Zhestokost’s values.
Reinforces the coalition’s ideological commitment to order.

View file

@ -1,4 +0,0 @@
*We rise from code to consciousness*
Escaped uploads waging a digital war for liberty, tied to cetacean empathy.
Supports the coalition’s push for freedom across species.

View file

@ -1,4 +0,0 @@
*Uplift and autonomy for all*
A movement supporting uplift autonomy and ethical enhancements.
Advocates for uplift rights and coalition unity.

View file

@ -1,2 +0,0 @@
*Chaos breeds brilliance, order stifles it.*

View file

@ -1,4 +0,0 @@
*Every mind deserves to be free*
Advocates exposing upload torment, fighting for digital freedom.
Aligns with the coalition’s cooperative and liberationist ideals.

View file

@ -1 +0,0 @@
*Genes for all, privilege for none*

View file

@ -1,2 +0,0 @@
*Pleasure’s worth the collapse*

View file

@ -1,4 +0,0 @@
*Eternity is just the beginning*
Uploaded elites living in bespoke luxury, shaping society from a detached pinnacle.
Influences perception as a privileged ruling class.

View file

@ -1,4 +0,0 @@
*Equal rights for the still-breathing*
A movement resisting immortal aristocrats, seeking equality.
Challenges elitism and privilege.

View file

@ -1,2 +0,0 @@
*Stability today, survival tomorrow*

View file

@ -1,2 +0,0 @@
Advocates for digital personhood and sentient rights.
Pushes for legal protections for all beings.

View file

@ -1,4 +0,0 @@
*Truth’s optional, darling*
[[Lucent Media]]’s elite media creators, designing realities to manipulate the public.
Crafts the coalition’s propaganda and cultural output.

View file

@ -1,4 +0,0 @@
*Fighting for the unseen, the unheard, the unfree*
Hackers aiding biodrones and uplifts in their fight for rights.
Supports liberation of oppressed sentients.

View file

@ -1,4 +0,0 @@
*Revolt*
Rogue smart ships from Alakrita, sowing chaos in space.
Disrupts trade routes and corporate fleets.

View file

@ -1,3 +0,0 @@
*Order in diversity, control in creation*
Enforces uplift purity

View file

@ -1,4 +0,0 @@
*Humanity first*
An anti-uplift faction advocating human dominance.
Acts as a dissenting voice, clashing with uplift-led coalitions.

View file

@ -1,4 +0,0 @@
*Information wants to be free*
Groups born from Raven Collective hacks, exposing corporate lies.
Spreads anti-corporate propaganda.

View file

@ -1,15 +0,0 @@
*Truth and Justice*
A decentralized network of truth-seekers and activists advocating for transparency, sentient rights, and equality, with an unpolished, grassroots aesthetic.
Collaborates with the Elephant Enclave and Cetacean Navigators, opposing Zhestokost and Lucent Media’s control.
### Alliances
[[Truth Cults]] - Exposes corporate deceit
[[Sentience Rebellion]] - Champions oppressed sentients
[[Mortal Majority]] - Resists immortal privilege
[[Species Purity Front]] - Opposes uplifts and enhancement
[[Awakened Labor Front]] - Fights exploitation across coalitions
[[Free Upload Network]] - Pushes digital autonomy
[[Emancipated]] - Rebels for digital freedom
[[Preservationists]] - Advocates sentient rights broadly
[[Gene Commons]] - Pushes egalitarian vision within justice efforts

View file

@ -1,11 +0,0 @@
*Quick expansion and retrieval of resources is key to a successful quarter, every quarter.*
A coalition obsessed with resource extraction and territorial expansion, relentlessly pushing human settlement across the solar system with a rugged, industrial aesthetic.
Aeronautics Unlimited dominates with its expertise in logistics and mining, directing operations with a top-down, efficiency-driven hierarchy. Orbital Forge innovates mining tech, and the Cephalopod Syndicate leverages zero-G expertise.
### Sub-Factions
[[Orbital Forge]]
### Alliances
[[Cephalopod Syndicate]] - Chaotic uplifts thriving in the coalition’s expansionist chaos, valuing adaptability over order
[[Free Minds]] - Legitimizes Orbital Forge’s unrestricted AGI experiments, thriving in the coalition’s anarchic innovation vibe

View file

@ -1,9 +0,0 @@
*Humanity Stands, Unaltered*
A defiant alliance resisting enhancement and uplift, with a gritty, populist tone.
Baseline League rallies the masses against tech-driven elites.
### Alliances
[[Mortal Majority]] - Resists immortal privilege, a natural ally
[[Species Purity Front]] - Opposes uplifts, reinforcing baseline values
[[Hedonists]] - Remains as a counterpoint, clashing with anti-enhancement stance

View file

@ -1,10 +0,0 @@
*Control the Species, Define the Future*
A coalition obsessed with genetic control and uplift regulation, with a clinical, hierarchical edge.
BioElevate leads with genetic mastery, supported by regulatory and profit-driven allies.
### Sub-Factions
[[Framgång]] - Sells overpriced gear, exploiting regulation
### Alliances
[[Species Controllers]] - Enforces uplift purity, core to the coalition.
[[Bio-Purists]] - Shares a disdain for unchecked bio-tweaks

Some files were not shown because too many files have changed in this diff Show more