Add balance quota to cap faction territory sizes

- Branch sizes are inherently skewed and a branch IS its faction's
  territory, so without a cap one faction would dominate the start.
- Each branch now keeps only min(eligible, quota) of its eligible
  middle systems, closest to the middle band (|d−0.5|, index tiebreak);
  quota = the median of the three branches' eligible sizes. The big
  branch's overflow stays neutral wild space; a small branch keeps all.
- Expose g.factions.quota and g.factions.territories for the map's
  territory-region fill.
- Add tests verifying the quota, kept-set membership, near-equal
  faction sizes, and no-faction-outruns-typical-branch invariants.
- Fix landing.json: entry 4 now references ice assets (was mislabeled
  lava land with ice clips); entry 5 uses lava-land-02 + lava surface/
  takeoff clips.
- Add original art asset lava-shop-02.png and update docs to describe
  the balance rule.
This commit is contained in:
Brian Fertig 2026-09-10 22:18:07 -06:00
parent e2878da59c
commit 42bf6c17e0
7 changed files with 133 additions and 33 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

View File

@ -1,5 +1,5 @@
{ {
"_comment": "FACTIONS — the three powers + neutral space. WORLDGEN LAYER (js/galaxy/Galaxy.js → the faction pass, at roster build): the jump tree's top-level BRANCHES (subtrees rooted at the home system's gate neighbors — home has ≤ maxGates gates, so ≤ 3 branches) each become ONE FACTION's territory. TERRITORY rule: a branch's systems at depth 2 … (maxDepth−1) (home = depth 0, its gate neighbors = depth 1) — the ring right outside home and every branch's dead-end tips stay NEUTRAL WILD SPACE, and anything not in a territory is neutral. CAPITALS: the NEUTRAL CAPITAL is a seeded pick of the home system's direct gate neighbors (ALWAYS); its system stays faction-neutral even though it roots a faction's branch (a neutral hub at the edge of that corridor). Each FACTION CAPITAL (apex / mindweave / zenith) is a seeded pick inside its branch's territory, preferred in the middle band (d in 0.37..0.63 — data/galaxy.json → distribution.zones' middle slice), on a non-barren system (it must host a planet); the branch↔faction assignment is a SEEDED SHUFFLE — new galaxy ⇒ new order/placement, same seed ⇒ identical (the determinism contract). Each capital's system grows its capital planet in content generation (js/galaxy/SystemGenerator.js: class capitalNeutral / capitalApex / capitalMindweave / capitalZenith>, planets.png frames 18..21 per data/planets.json → capitals, the 'capital' settlement per data/settlements.json — the faction ones owner:<faction>, the neutral one owner-less; that is the reputation faction seam, js/reputation/Reputation.js → factionStanding()). BRIDGES (data/gates.json → bridges) link the branches across the middle belt so the factions can reach — and fight — each other without routing through the trunk. REACHABILITY / COMBAT / TRADE off the territories is the NEXT layer (the reserved reputation standing + the map's territory-region pass): `color` = the faction's map/diplomacy color (the galaxy plate's star-ring layer, js/ui/GalaxyView.js); the capital planet's ART carries the same hue (frames 18..21).", "_comment": "FACTIONS — the three powers + neutral space. WORLDGEN LAYER (js/galaxy/Galaxy.js → the faction pass, at roster build): the jump tree's top-level BRANCHES (subtrees rooted at the home system's gate neighbors — home has ≤ maxGates gates, so ≤ 3 branches) each become ONE FACTION's territory. TERRITORY rule: a branch's systems at depth 2 … (maxDepth−1) (home = depth 0, its gate neighbors = depth 1) are ELIGIBLE — the ring right outside home and every branch's dead-end tips stay NEUTRAL WILD SPACE, and anything not in a territory is neutral. BALANCE QUOTA (no dominant faction at the start — branch sizes are inherently skewed, and a branch IS its faction's territory): each branch keeps only its min(eligible, quota) systems, closest to the middle band (|d−0.5|), quota = the MEDIAN of the three branches' eligible sizes (the big branch's overflow stays neutral wild space; a small branch keeps everything it has; a balanced galaxy is untouched) — exposed as g.factions.quota / .territories. CAPITALS: the NEUTRAL CAPITAL is a seeded pick of the home system's direct gate neighbors (ALWAYS); its system stays faction-neutral even though it roots a faction's branch (a neutral hub at the edge of that corridor). Each FACTION CAPITAL (apex / mindweave / zenith) is a seeded pick inside its branch's territory, preferred in the middle band (d in 0.37..0.63 — data/galaxy.json → distribution.zones' middle slice), on a non-barren system (it must host a planet); the branch↔faction assignment is a SEEDED SHUFFLE — new galaxy ⇒ new order/placement, same seed ⇒ identical (the determinism contract). Each capital's system grows its capital planet in content generation (js/galaxy/SystemGenerator.js: class capitalNeutral / capitalApex / capitalMindweave / capitalZenith>, planets.png frames 18..21 per data/planets.json → capitals, the 'capital' settlement per data/settlements.json — the faction ones owner:<faction>, the neutral one owner-less; that is the reputation faction seam, js/reputation/Reputation.js → factionStanding()). BRIDGES (data/gates.json → bridges) link the branches across the middle belt so the factions can reach — and fight — each other without routing through the trunk. REACHABILITY / COMBAT / TRADE off the territories is the NEXT layer (the reserved reputation standing + the map's territory-region pass): `color` = the faction's map/diplomacy color (the galaxy plate's star-ring layer, js/ui/GalaxyView.js); the capital planet's ART carries the same hue (frames 18..21).",
"factions": { "factions": {
"neutral": { "label": "Neutral", "color": "#b9c4d6" }, "neutral": { "label": "Neutral", "color": "#b9c4d6" },
"apex": { "label": "Apex", "color": "#ff4d4d" }, "apex": { "label": "Apex", "color": "#ff4d4d" },

View File

@ -66,8 +66,8 @@
{ "land": "ice-land-01.mp4", "surface": "ice-surface-01.mp4", "takeoff": "ice-takeoff-01.mp4", "shop": "ice-shop-01.mp4" }, { "land": "ice-land-01.mp4", "surface": "ice-surface-01.mp4", "takeoff": "ice-takeoff-01.mp4", "shop": "ice-shop-01.mp4" },
{ "land": "ice-land-02.mp4", "surface": "ice-surface-02.mp4", "takeoff": "ice-takeoff-02.mp4", "shop": "ice-shop-02.mp4" }, { "land": "ice-land-02.mp4", "surface": "ice-surface-02.mp4", "takeoff": "ice-takeoff-02.mp4", "shop": "ice-shop-02.mp4" },
{ "land": "ice-land-03.mp4", "surface": "ice-surface-03.mp4", "takeoff": "ice-takeoff-03.mp4", "shop": "ice-shop-03.mp4" }, { "land": "ice-land-03.mp4", "surface": "ice-surface-03.mp4", "takeoff": "ice-takeoff-03.mp4", "shop": "ice-shop-03.mp4" },
{ "land": "lava-land-01.mp4", "surface": "ice-surface-01.mp4", "takeoff": "ice-takeoff-01.mp4", "shop": "ice-shop-01.mp4" }, { "land": "ice-land-01.mp4", "surface": "ice-surface-01.mp4", "takeoff": "ice-takeoff-01.mp4", "shop": "ice-shop-01.mp4" },
{ "land": "ice-land-02.mp4", "surface": "ice-surface-02.mp4", "takeoff": "ice-takeoff-02.mp4", "shop": "ice-shop-02.mp4" }, { "land": "lava-land-02.mp4", "surface": "lava-surface-02.mp4", "takeoff": "lava-takeoff-02.mp4", "shop": "ice-shop-02.mp4" },
{ "land": "ice-land-03.mp4", "surface": "ice-surface-03.mp4", "takeoff": "ice-takeoff-03.mp4", "shop": "ice-shop-03.mp4" } { "land": "ice-land-03.mp4", "surface": "ice-surface-03.mp4", "takeoff": "ice-takeoff-03.mp4", "shop": "ice-shop-03.mp4" }
], ],

View File

@ -121,7 +121,7 @@
] ]
}, },
"galaxy": { "galaxy": {
"_comment": "THE GALAXY TAB (js/ui/GalaxyView.js) — the whole-galaxy chart on the plate: one GLOWING STAR per system (colored by its archetype, data/systems.json → types, pulsing on that archetype's heartbeat in `pulse`), THIN LANES where the jump gates connect (the JumpNetwork spanning tree — a maze), the CHARTED REGION (the convex hull of the visited systems, inflated — the discovered-area shading), HOME + SHIP markers, ambient dust + the core glow. TRAVELED lanes (the run jumped them — saved with the run) glow brighter + carry a flow packet; FRONTIER lanes (one end visited) are the 'next step'; unexplored lanes are faint threads. Hover = the star's readout + its link state to the current system — STARS ONLY, and the zone hugs each star's drawn dot (glow radius + a few px of mouse pad), so the lanes between the stars never trigger it; click a CHARTED star = its chart in the SYSTEM tab; uncharted stars are readouts only. FACTIONS (planned, not yet implemented): the snapshot carries per-system `faction: null` — the reserved seams are a faction color layer over the stars + a per-faction territory region (the same hull/fill recipe as `hull`, per faction color + relation alpha).", "_comment": "THE GALAXY TAB (js/ui/GalaxyView.js) — the whole-galaxy chart on the plate: one GLOWING STAR per system (colored by its archetype, data/systems.json → types, pulsing on that archetype's heartbeat in `pulse`), THIN LANES where the jump gates connect (the JumpNetwork spanning tree — a maze), the CHARTED REGION (the convex hull of the visited systems, inflated — the discovered-area shading), HOME + SHIP markers, ambient dust + the core glow. TRAVELED lanes (the run jumped them — saved with the run) glow brighter + carry a flow packet; FRONTIER lanes (one end visited) are the 'next step'; unexplored lanes are faint threads. Hover = the star's readout + its link state to the current system — STARS ONLY, and the zone hugs each star's drawn dot (glow radius + a few px of mouse pad), so the lanes between the stars never trigger it; click a CHARTED star = its chart in the SYSTEM tab; uncharted stars are readouts only. FACTIONS (live — data/factions.json, the roster's faction pass, js/galaxy/Galaxy.js): the snapshot carries per-system `faction` (null = neutral wild space) + `capital` — the live seams are the faction RING layer over the stars (see `factions` below) + a per-faction territory-region FILL (the same hull/fill recipe as `hull`, per faction color + relation alpha — the next layer).",
"name": "GALAXY", "name": "GALAXY",
"platePadding": 26, "platePadding": 26,
"_platePadding": "margin kept between the outermost stars and the plate edge at 1× zoom, in PLATE px (js/ui/GalaxyView.js → fitPadded). The field is 2:1 to match the plate, so the fully-zoomed-out galaxy fills the plate with this much breathing room on all sides.", "_platePadding": "margin kept between the outermost stars and the plate edge at 1× zoom, in PLATE px (js/ui/GalaxyView.js → fitPadded). The field is 2:1 to match the plate, so the fully-zoomed-out galaxy fills the plate with this much breathing room on all sides.",

View File

@ -22,11 +22,15 @@
* non-barren system of its branch, each system carrying the capital * non-barren system of its branch, each system carrying the capital
* world (class capital<faction>, frame 19/20/21) + the 'capital' * world (class capital<faction>, frame 19/20/21) + the 'capital'
* settlement owned by the faction; * settlement owned by the faction;
* - the TERRITORY RULE: every system of a faction's branch at depth 2 … * - the TERRITORY RULE + BALANCE QUOTA: each branch's middle systems
* (branch maxDepth − 1) carries that branch's faction; the ring right * (depth 2 … maxDepth − 1) are ELIGIBLE; each branch keeps its
* outside home (depth 1) and the branch tips (maxDepth) stay neutral — * min(eligible, quota) closest to the middle band (|d − 0.5|, index
* except a faction capital demoted onto a branch root (only when the * tiebreak), quota = the MEDIAN of the eligible sizes — no faction
* branch has no territory at all); * outruns a typical branch (no dominant faction; the overflow stays
* neutral wild space, and a small branch keeps everything it has);
* the ring right outside home (depth 1) + the branch tips (maxDepth)
* stay neutral — except a faction capital demoted onto a branch root
* (only when the branch has no territory at all);
* - ARID: every arid planet is ALWAYS habitable (no roll) and hosts a * - ARID: every arid planet is ALWAYS habitable (no roll) and hosts a
* COLONY (data/settlements.json → settledKindByClass); arid worlds * COLONY (data/settlements.json → settledKindByClass); arid worlds
* show up in the roster (rebalanced planetClasses); * show up in the roster (rebalanced planetClasses);
@ -180,30 +184,73 @@ const isBarren = (id) => {
} }
check('faction capitals: capital world (class + frame + habitable) + faction-owned settlement', capWorlds, capWhy); check('faction capitals: capital world (class + frame + habitable) + faction-owned settlement', capWorlds, capWhy);
// TERRITORY: depth 2 … (branch maxDepth − 1) of each branch = that // TERRITORY + BALANCE QUOTA: the branch's middle systems (depth 2 …
// branch's faction; the depth-1 ring + the branch tips stay neutral // maxDepth − 1) are ELIGIBLE; each branch keeps its min(eligible, quota)
// (except a faction capital demoted onto a branch root — only when the // closest to the middle band (|d − 0.5|, index tiebreak) — quota = the
// branch has no territory at all). // MEDIAN of the eligible sizes, so no faction can outsize a typical
// branch (the overflow stays neutral wild space); a small branch keeps
// everything it has (except a faction capital demoted onto a branch
// root — only when the branch has no territory at all).
const branchMax = new Map(g.factions.branches.map((b) => [b, 0])); const branchMax = new Map(g.factions.branches.map((b) => [b, 0]));
for (const [id, d] of depth) if (id !== HOME && branchOf.has(id)) branchMax.set(branchOf.get(id), Math.max(branchMax.get(branchOf.get(id)), d)); const eligibleOf = new Map(g.factions.branches.map((b) => [b, []]));
for (const [id, d] of depth) {
if (id === HOME || !branchOf.has(id)) continue;
const b = branchOf.get(id);
branchMax.set(b, Math.max(branchMax.get(b), d));
}
for (const r of g.records) {
if (r.id === HOME || !branchOf.has(r.id)) continue;
const b = branchOf.get(r.id);
const d = depth.get(r.id);
if (d >= 2 && d <= branchMax.get(b) - 1) eligibleOf.get(b).push(r.id);
}
const sizes = g.factions.branches.map((b) => eligibleOf.get(b).length).sort((x, y) => x - y);
const quota = sizes[Math.floor((sizes.length - 1) / 2)];
const idx = new Map(g.records.map((r, i) => [r.id, i]));
const mid = (id) => Math.abs((g.byId.get(id)?.d ?? 0.5) - 0.5);
const keepOf = new Map(g.factions.branches.map((b) => {
const elig = eligibleOf.get(b);
return [b, new Set(elig.slice().sort((a, c) => mid(a) - mid(c) || idx.get(a) - idx.get(c)).slice(0, Math.min(elig.length, quota)))];
}));
check('balance quota: the median of the branches’ eligible sizes', g.factions.quota === quota, `${g.factions.quota} vs ${quota} (sizes ${sizes.join(',')})`);
let terrOk = true; let terrOk = true;
let terrWhy = ''; let terrWhy = '';
for (const r of g.records) { for (const r of g.records) {
if (r.id === HOME || !branchOf.has(r.id)) continue; if (r.id === HOME || !branchOf.has(r.id)) continue;
const d = depth.get(r.id);
const b = branchOf.get(r.id); const b = branchOf.get(r.id);
const fid = g.factions.branchFaction[b]; const fid = g.factions.branchFaction[b];
const maxD = branchMax.get(b); const expectFaction = keepOf.get(b).has(r.id) ? fid : null;
const expectFaction = d >= 2 && d <= maxD - 1 ? fid : null;
const got = r.faction === fid ? fid : null; const got = r.faction === fid ? fid : null;
const demoted = g.factions.capitals[fid] === r.id && expectFaction === null; const demoted = g.factions.capitals[fid] === r.id && expectFaction === null;
if (got !== expectFaction && !(demoted && got === fid)) { if (got !== expectFaction && !(demoted && got === fid)) {
terrOk = false; terrOk = false;
terrWhy = `${r.id}: d=${d} maxD=${maxD} expect=${expectFaction} got=${got}`; terrWhy = `${r.id}: keep=${keepOf.get(b).has(r.id)} expect=${expectFaction} got=${got}`;
break; break;
} }
} }
check('territory rule: depth 2…(maxDepth−1) per branch = the branch\u2019s faction; ring + tips neutral', terrOk, terrWhy); check('territory rule: the branch’s kept (quota-capped) middle systems = its faction; the rest neutral', terrOk, terrWhy);
// NO DOMINANT FACTION: every territory is exactly its branch's
// min(eligible, quota) systems — the big branch is trimmed to the
// median, the small branch keeps everything; and the kept set is the
// middle-band-nearest one (the map's territory-region fill will read
// factions.territories).
let terrSizeOk = true;
let terrSizeWhy = '';
for (const b of g.factions.branches) {
const terr = g.factions.territories?.[b] ?? [];
const want = Math.min(eligibleOf.get(b).length, quota);
if (terr.length !== want) { terrSizeOk = false; terrSizeWhy = `${b}: ${terr.length} systems, want ${want}`; break; }
for (const id of terr) {
if (!keepOf.get(b).has(id)) { terrSizeOk = false; terrSizeWhy = `${b}: ${id} is not among its ${want} middle-band-nearest eligible systems`; break; }
}
if (!terrSizeOk) break;
}
check('no dominant faction: every territory is its branch’s min(eligible, quota) middle-band systems', terrSizeOk, terrSizeWhy);
const terrSizes = g.factions.branches.map((b) => g.factions.territories[b].length);
check('faction sizes near-equal: max territory ≤ quota (no faction outruns a typical branch)', Math.max(...terrSizes) <= quota, `sizes ${terrSizes.join('/')} quota ${quota}`);
// OWNERS: every settlement in a faction-controlled system is owned by // OWNERS: every settlement in a faction-controlled system is owned by
// that faction (neutral systems: owner null) — the reputation seam is // that faction (neutral systems: owner null) — the reputation seam is

View File

@ -460,10 +460,15 @@ defaults to "current system charted, nothing traveled" (old saves load).
**FACTIONS (the worldgen layer is LIVE — data/factions.json):** the **FACTIONS (the worldgen layer is LIVE — data/factions.json):** the
roster's faction pass (`js/galaxy/Galaxy.js → _assignFactions`) assigns roster's faction pass (`js/galaxy/Galaxy.js → _assignFactions`) assigns
**territories** — each top-level BRANCH of the jump tree (a subtree **territories** — each top-level BRANCH of the jump tree (a subtree
rooted at one of home's gate neighbors) is one faction's territory, rooted at one of home's gate neighbors) is one faction's territory: a
starting one system into the branch and stopping one before its branch's middle systems (depth 2 … maxDepth−1) are ELIGIBLE, and each
dead-end tips (the ring right outside home + the branch tips stay branch keeps its min(eligible, quota) closest to the middle band (|d −
neutral wild space) — and designates the **four capitals**: the NEUTRAL 0.5|), **quota = the MEDIAN of the three branches' eligible sizes** —
the BALANCE rule: branch sizes are inherently skewed (a branch IS its
faction's territory), so without the cap one faction would dominate the
start; the big branch's overflow stays neutral wild space, a small
branch keeps everything it has (the ring right outside home + the
branch tips always stay neutral) — and designates the **four capitals**: the NEUTRAL
capital (always a direct gate neighbor of home, its system stays capital (always a direct gate neighbor of home, its system stays
neutral) + one faction capital per faction (apex / mindweave / zenith, neutral) + one faction capital per faction (apex / mindweave / zenith,
middle band, seeded). The branch↔faction order is a SEEDED SHUFFLE — a middle band, seeded). The branch↔faction order is a SEEDED SHUFFLE — a

View File

@ -354,11 +354,19 @@ export class Galaxy {
* reshuffles who sits where (never a fixed order), same seed ⇒ * reshuffles who sits where (never a fixed order), same seed ⇒
* identical placement. * identical placement.
* TERRITORY — a branch's systems at depth 2 … (maxDepth−1) (home = * TERRITORY — a branch's systems at depth 2 … (maxDepth−1) (home =
* depth 0, its gate neighbors = depth 1): the ring right outside * depth 0, its gate neighbors = depth 1) are ELIGIBLE; the ring
* home and every branch's dead-end tips stay NEUTRAL WILD SPACE; * right outside home and every branch's dead-end tips stay NEUTRAL
* anything not in a territory is neutral. A faction capital always * WILD SPACE; anything not in a territory is neutral. A faction
* sits in its faction's territory (fallbacks only when the branch * capital always sits in its faction's territory (fallbacks only
* is too short to have one). * when the branch is too short to have one).
* BALANCE QUOTA (no dominant faction — branch sizes are inherently
* skewed, and a branch IS its faction's territory): each branch
* keeps only the min(eligible, quota) of its eligible systems,
* closest to the middle band (|d − 0.5|, index tiebreak —
* deterministic); quota = the MEDIAN of the three branches'
* eligible sizes. The big branch's overflow stays neutral wild
* space; a small branch keeps everything it has (a working
* territory beats an empty one).
* *
* @param {Set<string>|null} barren the gate-only dead ends (excluded from * @param {Set<string>|null} barren the gate-only dead ends (excluded from
* capital candidacy — they can't host a world) * capital candidacy — they can't host a world)
@ -414,12 +422,45 @@ export class Galaxy {
for (const id of nodesOf.get(b)) m = Math.max(m, depth.get(id) ?? 1); for (const id of nodesOf.get(b)) m = Math.max(m, depth.get(id) ?? 1);
maxDepthOf.set(b, m); maxDepthOf.set(b, m);
} }
// Territory = the branch's middle systems (depth 2 … maxDepth−1): // TERRITORY = the branch's middle systems (depth 2 … maxDepth−1):
// the depth-1 ring at home and the branch tips stay neutral. // the depth-1 ring at home and the branch tips stay neutral.
const inTerritory = (id, b) => { const eligibleOf = new Map();
const d = depth.get(id) ?? 0; for (const b of branchRoots) {
return d >= 2 && d <= (maxDepthOf.get(b) ?? 1) - 1; eligibleOf.set(
}; b,
(nodesOf.get(b) ?? []).filter((id) => {
const d = depth.get(id) ?? 0;
return d >= 2 && d <= (maxDepthOf.get(b) ?? 1) - 1;
}),
);
}
// BALANCE QUOTA (no dominant faction at the start — branch sizes are
// inherently skewed, and a branch IS its faction's territory, so an
// unbalanced galaxy would crown a dominant faction): quota = the
// MEDIAN of the three branches' eligible sizes; the big branch keeps
// only its systems nearest the middle band (|d − 0.5| — the "middle
// of the galaxy" design, where the bridges sit too; index tiebreak
// for determinism) and its overflow stays NEUTRAL WILD SPACE; a small
// branch keeps everything it has (a working territory beats an empty
// one). A balanced galaxy is untouched (median = each size).
const sizes = branchRoots.map((b) => eligibleOf.get(b).length).sort((x, y) => x - y);
const quota = sizes.length ? sizes[Math.floor((sizes.length - 1) / 2)] : 0;
const index = new Map(this.records.map((r, i) => [r.id, i]));
const midRank = (id) => Math.abs((this.byId.get(id)?.d ?? 0.5) - 0.5);
const territoryOf = new Map();
for (const b of branchRoots) {
const elig = eligibleOf.get(b) ?? [];
territoryOf.set(
b,
new Set(
elig
.slice()
.sort((a, c) => midRank(a) - midRank(c) || index.get(a) - index.get(c))
.slice(0, Math.min(elig.length, quota)),
),
);
}
const inTerritory = (id, b) => territoryOf.get(b)?.has(id) ?? false;
const inBand = (id) => { const inBand = (id) => {
const rec = this.byId.get(id); const rec = this.byId.get(id);
return !!rec && rec.d >= dMin && rec.d <= dMax; return !!rec && rec.d >= dMin && rec.d <= dMax;
@ -490,6 +531,11 @@ export class Galaxy {
capitals, capitals,
branches: branchRoots, branches: branchRoots,
branchFaction: Object.fromEntries(branchFaction), branchFaction: Object.fromEntries(branchFaction),
// The BALANCE QUOTA (the median of the branches' eligible territory
// sizes) + each branch's final territory (its kept systems, sorted
// — the map's territory-region fill will read these).
quota,
territories: Object.fromEntries(branchRoots.map((b) => [b, [...(territoryOf.get(b) ?? [])].sort()])),
}; };
const missing = FACTIONS.filter((f) => !capitals[f]); const missing = FACTIONS.filter((f) => !capitals[f]);
if (missing.length > 0) { if (missing.length > 0) {
@ -774,6 +820,8 @@ export class Galaxy {
capitals: this.factions.capitals, capitals: this.factions.capitals,
branches: this.factions.branches, branches: this.factions.branches,
branchFaction: this.factions.branchFaction, branchFaction: this.factions.branchFaction,
quota: this.factions.quota,
territories: this.factions.territories,
} : null, } : null,
jumpNetworkEdges: this.jumpNetwork ? [...this.jumpNetwork.gates.values()].reduce((n, t) => n + t.length, 0) : null, jumpNetworkEdges: this.jumpNetwork ? [...this.jumpNetwork.gates.values()].reduce((n, t) => n + t.length, 0) : null,
jumpNetworkBridges: this.jumpNetwork?.bridges ?? [], jumpNetworkBridges: this.jumpNetwork?.bridges ?? [],