Skip to content
This repository was archived by the owner on Aug 31, 2026. It is now read-only.

Commit 32f2a57

Browse files
committed
feat: a galaxy keeps a retinue of satellites
- satellites are children inside the primary's own cube - count and size drawn from the galaxy type's own band - galaxyContainingSector answers which galaxy a point is in - the seat margin is the whole retinue's reach - a nucleus contrast scales to its own galaxy's population
1 parent 329ece8 commit 32f2a57

12 files changed

Lines changed: 764 additions & 59 deletions

File tree

‎src/main/java/zmaster587/advancedRocketry/universe/ClusterField.java‎

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,18 +72,45 @@ public Optional<StarCluster> nucleusOf(long seed, Galaxy galaxy) {
7272
if (galaxy == null) {
7373
return Optional.empty();
7474
}
75-
double u = CellHash.norm(CellHash.of(seed, galaxy.cellX(), galaxy.cellY(), galaxy.cellZ(),
76-
SALT_NUCLEUS_RADIUS));
75+
// Keyed on the galaxy's own CENTRE, not on its lattice index: a cube holds a primary and its
76+
// satellites, and they share that index — so keying on it would give every galaxy in a group the
77+
// same nucleus, and a satellite's core would be sized by its primary's draw.
78+
double u = CellHash.norm(CellHash.of(seed, galaxy.centre().sectorX(),
79+
galaxy.centre().sectorY(), galaxy.centre().sectorZ(), SALT_NUCLEUS_RADIUS));
7780
GalaxyGenConfig.ClusterType type = GalaxyGenConfig.NUCLEUS;
7881
double radiusLy = type.minRadiusLy + u * (type.maxRadiusLy - type.minRadiusLy);
7982
long s = config.minSpacing;
80-
return Optional.of(new StarCluster(type,
83+
return Optional.of(new StarCluster(type, nucleusSubdivisionFor(galaxy),
8184
Math.floorDiv(galaxy.centre().sectorX(), s),
8285
Math.floorDiv(galaxy.centre().sectorY(), s),
8386
Math.floorDiv(galaxy.centre().sectorZ(), s),
8487
superCellsForLightYears(radiusLy, config.minSpacing)));
8588
}
8689

90+
/**
91+
* How finely a galaxy's NUCLEUS divides the lattice &mdash; scaled to the galaxy it is the centre of,
92+
* never taken flat from the table.
93+
*
94+
* <p>Every other cluster's contrast is measured against the FIELD, whose density is real and the same
95+
* everywhere; a nucleus's is a statement about its own galaxy's POPULATION, so it cannot be one
96+
* number. The table's {@code k} is the real figure for a reference-sized galaxy (10⁷&times; the field
97+
* at 10¹¹ stars), and a galaxy's population goes as its radius cubed, so {@code k} goes as the
98+
* radius: {@code k = k_ref &middot; R / R_ref}. That holds the nucleus at a constant FRACTION of
99+
* whatever it is the centre of.</p>
100+
*
101+
* <p>Measured, and the reason this is derived at all: the flat {@code k = 215} put ~4&middot;10⁷ stars
102+
* inside a 6-light-year core of a 921-light-year dwarf that holds ~10⁷ altogether &mdash; a nucleus
103+
* four times its own galaxy. It is the same error the table's {@code k} was once held down to avoid,
104+
* one level lower, and it became reachable the moment satellite galaxies made small galaxies common.
105+
* A dwarf's nucleus comes out at {@code k = 4}, i.e. barely a concentration, which is what a real
106+
* dwarf spheroidal has.</p>
107+
*/
108+
private static int nucleusSubdivisionFor(Galaxy galaxy) {
109+
double scaled = GalaxyGenConfig.NUCLEUS.subdivision
110+
* galaxy.radiusLy() / UniverseScale.REFERENCE_GALAXY_RADIUS_LY;
111+
return (int) Math.max(1L, Math.min(GalaxyGenConfig.NUCLEUS.subdivision, Math.round(scaled)));
112+
}
113+
87114
/**
88115
* The cluster seated in cluster cell {@code (cx, cy, cz)}, or empty.
89116
*
@@ -118,7 +145,9 @@ public Optional<StarCluster> clusterAtIndex(long seed, Galaxy galaxy, long cx, l
118145
long ox = margin + Math.floorMod(CellHash.of(seed, cx, cy, cz, SALT_CLUSTER_OX), band);
119146
long oy = margin + Math.floorMod(CellHash.of(seed, cx, cy, cz, SALT_CLUSTER_OY), band);
120147
long oz = margin + Math.floorMod(CellHash.of(seed, cx, cy, cz, SALT_CLUSTER_OZ), band);
121-
return Optional.of(new StarCluster(type, cx * spacingSuperCells + ox,
148+
// The type's own k: an open cluster's and a globular's contrast is measured against the FIELD,
149+
// whose density is real and uniform, so it needs no scaling to the galaxy it sits in.
150+
return Optional.of(new StarCluster(type, type.subdivision, cx * spacingSuperCells + ox,
122151
cy * spacingSuperCells + oy, cz * spacingSuperCells + oz, radius));
123152
}
124153

‎src/main/java/zmaster587/advancedRocketry/universe/ClusteredGalaxyGenerator.java‎

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -642,7 +642,8 @@ public List<Nebula> nebulaeAround(long seed, GalacticCoord cell, double radiusLy
642642
return Collections.emptyList();
643643
}
644644
GalacticCoord c = cell.cellCentre();
645-
Optional<Galaxy> galaxy = galaxies.galaxyOwningSector(seed, c.sectorX(), c.sectorY(),
645+
// The galaxy the observer is INSIDE, so a cell in a satellite sees the satellite's clouds.
646+
Optional<Galaxy> galaxy = galaxies.galaxyContainingSector(seed, c.sectorX(), c.sectorY(),
646647
c.sectorZ());
647648
if (!galaxy.isPresent()) {
648649
return Collections.emptyList();
@@ -669,7 +670,7 @@ public double columnDensityBetween(long seed, GalacticCoord from, GalacticCoord
669670
return 0d;
670671
}
671672
GalacticCoord a = from.cellCentre();
672-
Optional<Galaxy> galaxy = galaxies.galaxyOwningSector(seed, a.sectorX(), a.sectorY(),
673+
Optional<Galaxy> galaxy = galaxies.galaxyContainingSector(seed, a.sectorX(), a.sectorY(),
673674
a.sectorZ());
674675
return galaxy.isPresent() ? nebulae.columnDensityBetween(seed, galaxy.get(), from, to) : 0d;
675676
}
@@ -844,7 +845,9 @@ long minEdge() {
844845
*/
845846
private int subdivisionAt(long seed, long supX, long supY, long supZ) {
846847
long s = config.minSpacing;
847-
Optional<Galaxy> galaxy = galaxies.galaxyOwningSector(seed, supX * s + s / 2L,
848+
// The CONTAINING galaxy: a cluster inside a satellite belongs to the satellite, and its nucleus
849+
// sits at the satellite's own centre.
850+
Optional<Galaxy> galaxy = galaxies.galaxyContainingSector(seed, supX * s + s / 2L,
848851
supY * s + s / 2L, supZ * s + s / 2L);
849852
if (!galaxy.isPresent()) {
850853
return 1;
@@ -894,14 +897,19 @@ private static long subIndex(long offsetInCoarse, long coarseEdge, int k) {
894897
}
895898

896899
/**
897-
* How dense the owning galaxy is at this sector triple, in {@code [0, 1]} — zero in the void and
898-
* zero past a galaxy's declared edge.
900+
* How dense the CONTAINING galaxy is at this sector triple, in {@code [0, 1]} — zero in the void and
901+
* zero past every galaxy's declared edge.
899902
*
900903
* <p>The galaxy cell is a coarse reading of the sector, so this is O(1) and needs no stored index:
901-
* every point belongs to exactly one galaxy cell, and that cell either holds a galaxy or is void.</p>
904+
* every point belongs to exactly one galaxy cell, and that cell holds a primary galaxy, its
905+
* satellites, or nothing.</p>
906+
*
907+
* <p><b>The containing galaxy, not the cube's owner.</b> A cube holds a primary and its retinue, so
908+
* reading the owner's profile at a point inside a satellite would answer zero — and the satellites
909+
* would be named, addressable and completely empty of stars.</p>
902910
*/
903911
private double galaxyProfileAt(long seed, long sectorX, long sectorY, long sectorZ) {
904-
Optional<Galaxy> galaxy = galaxies.galaxyOwningSector(seed, sectorX, sectorY, sectorZ);
912+
Optional<Galaxy> galaxy = galaxies.galaxyContainingSector(seed, sectorX, sectorY, sectorZ);
905913
if (!galaxy.isPresent()) {
906914
return 0d;
907915
}

‎src/main/java/zmaster587/advancedRocketry/universe/Galaxy.java‎

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ public final class Galaxy {
5656
private final long cellX;
5757
private final long cellY;
5858
private final long cellZ;
59+
private final int satelliteIndex;
5960
private final GalacticCoord centre;
6061
private final LightYearVector seat;
6162
private final LightYearVector peculiarVelocity;
@@ -79,21 +80,27 @@ public final class Galaxy {
7980

8081
/**
8182
* @param cellX the galaxy-lattice index this galaxy is seated in
83+
* @param satelliteIndex {@code 0} for the cube's PRIMARY galaxy, {@code 1..n} for a satellite of
84+
* it. A cube holds one primary and its retinue, so the lattice index alone no
85+
* longer identifies a galaxy — this is what distinguishes them, in the name and
86+
* in every draw made per galaxy rather than per cell
8287
* @param centre its centre, as a cell name
8388
* @param radiusLy its declared radius in light years — drawn inside {@code type}'s band
8489
* @param tilt the angle its pole makes with the static +Y axis, in radians
8590
* @param node the direction that pole leans in, in radians about +Y
8691
* @param armPitch the arms' pitch angle in radians (ignored when the type has no arms)
8792
* @param armPhase where arm zero starts, in radians
8893
* @param peculiarVelocity its comoving velocity in light years per tick — its own motion through
89-
* the expanding universe, on top of the expansion
94+
* the expanding universe, on top of the expansion. A satellite carries its
95+
* PRIMARY's, so a group travels together
9096
*/
91-
public Galaxy(long cellX, long cellY, long cellZ, GalacticCoord centre,
97+
public Galaxy(long cellX, long cellY, long cellZ, int satelliteIndex, GalacticCoord centre,
9298
GalaxyGenConfig.GalaxyType type, double radiusLy, double tilt, double node,
9399
double armPitch, double armPhase, LightYearVector peculiarVelocity) {
94100
this.cellX = cellX;
95101
this.cellY = cellY;
96102
this.cellZ = cellZ;
103+
this.satelliteIndex = Math.max(0, satelliteIndex);
97104
this.centre = centre;
98105
this.seat = LightYearVector.ofCell(centre);
99106
this.peculiarVelocity = (peculiarVelocity == null) ? LightYearVector.ZERO : peculiarVelocity;
@@ -186,9 +193,29 @@ public double armPhase() {
186193
return armPhase;
187194
}
188195

189-
/** This galaxy's designation — procedurally-generated galaxy, named for the cell it is seated in. */
196+
/**
197+
* {@code 0} for the cube's primary galaxy, {@code 1..n} for one of its satellites. A cube holds a
198+
* primary AND its retinue, so this is the second half of a galaxy's identity.
199+
*/
200+
public int satelliteIndex() {
201+
return satelliteIndex;
202+
}
203+
204+
/** Whether this galaxy is a satellite of the primary seated in the same cube. */
205+
public boolean isSatellite() {
206+
return satelliteIndex > 0;
207+
}
208+
209+
/**
210+
* This galaxy's designation — procedurally-generated galaxy, named for the cell it is seated in,
211+
* and for its place in that cube's retinue when it is not the primary.
212+
*
213+
* <p>The suffix is not decoration: a satellite is a destination with an address, and two galaxies in
214+
* one cube sharing a name would be two places a player could neither tell apart nor write down.</p>
215+
*/
190216
public String name() {
191-
return "PGG-" + cellX + "." + cellY + "." + cellZ;
217+
String cell = "PGG-" + cellX + "." + cellY + "." + cellZ;
218+
return isSatellite() ? cell + "-S" + satelliteIndex : cell;
192219
}
193220

194221
// ─── Membership and profile ────────────────────────────────────────────────

0 commit comments

Comments
 (0)