fix(assets): address PR #119 review — 10 issues across planet-gen pipeline
1. Fix pclass.title() underscore bug in scaffold headings (216+ files) 2. Add 5 extended classes to batch.py valid_classes set 3. Add atmosphere rim colors for cold_arid/hot_arid/tropical/boreal/temperate_terminator 4. Add cloud/tilt ranges for extended classes (boreal 55-75%, tropical 60-80%) 5. Fix legend overflow at 1024px + deduplicate rainforest labels 6. Move _check_habitability() inside loop (was only checking last body) 7. Preserve gas_giant_ringed distinction in profile table 8. Drop meaningless terrain fields from gas giant frontmatter 9. Update stale docstrings/comments for 1024x512 default 10. Add infernal ring color All lookup tables (CLASS_TILT, CLASS_CLOUD, CLASS_POLAR_ICE, CLASS_GEOTHERMAL, CLASS_OBLATENESS, atmo_colors, tectonic_map, substrate_map) now include the 5 extended planet classes. Body content requires full regeneration. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+14
-11
@@ -96,7 +96,7 @@ def _read_frontmatter(md_path: Path) -> dict:
|
||||
# Validation (dry-run)
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
REQUIRED_FIELDS = ["id", "planet_class", "seed", "star", "orbit", "physical", "terrain"]
|
||||
REQUIRED_FIELDS = ["id", "planet_class", "seed", "star", "orbit", "physical"]
|
||||
REQUIRED_STAR = ["type", "luminosity_solar"]
|
||||
REQUIRED_ORBIT = ["distance_au", "period_days", "axial_tilt_deg"]
|
||||
REQUIRED_PHYSICAL = ["gravity_g", "atmosphere"]
|
||||
@@ -130,25 +130,28 @@ def _validate_body_def(bd: dict, body_dir: Path) -> list:
|
||||
if f not in phys:
|
||||
errors.append(f"{bid}: missing physical.{f}")
|
||||
|
||||
terrain = bd.get("terrain", {})
|
||||
for f in REQUIRED_TERRAIN:
|
||||
if f not in terrain:
|
||||
errors.append(f"{bid}: missing terrain.{f}")
|
||||
|
||||
lf = terrain.get("land_fraction")
|
||||
if lf is not None and (lf < 0 or lf > 1):
|
||||
errors.append(f"{bid}: terrain.land_fraction = {lf} — must be [0, 1]")
|
||||
|
||||
seed = bd.get("seed")
|
||||
if seed is None or not isinstance(seed, int):
|
||||
errors.append(f"{bid}: seed must be an integer, got {seed!r}")
|
||||
|
||||
pclass = bd.get("planet_class", "")
|
||||
valid_classes = {"temperate", "oceanic", "forest", "arid", "frozen",
|
||||
"volcanic", "barren", "gas_giant", "gas_giant_ringed"}
|
||||
"volcanic", "barren", "gas_giant", "gas_giant_ringed",
|
||||
"cold_arid", "hot_arid", "tropical", "boreal",
|
||||
"temperate_terminator"}
|
||||
if pclass not in valid_classes:
|
||||
errors.append(f"{bid}: planet_class '{pclass}' not in {valid_classes}")
|
||||
|
||||
is_gas = pclass in ("gas_giant", "gas_giant_ringed")
|
||||
terrain = bd.get("terrain", {})
|
||||
if not is_gas:
|
||||
for f in REQUIRED_TERRAIN:
|
||||
if f not in terrain:
|
||||
errors.append(f"{bid}: missing terrain.{f}")
|
||||
lf = terrain.get("land_fraction")
|
||||
if lf is not None and (lf < 0 or lf > 1):
|
||||
errors.append(f"{bid}: terrain.land_fraction = {lf} — must be [0, 1]")
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
|
||||
@@ -142,47 +142,67 @@ PLANET_CLASS_MAP = {
|
||||
# planet_class → axial tilt range [min, max] degrees
|
||||
# Tidal locking check overrides this for short-period bodies
|
||||
CLASS_TILT = {
|
||||
"temperate": (10, 35),
|
||||
"oceanic": (5, 25),
|
||||
"forest": (10, 40),
|
||||
"arid": (5, 30),
|
||||
"frozen": (15, 60), # high tilt → seasonal extremes → frozen
|
||||
"volcanic": (2, 20),
|
||||
"barren": (0, 45),
|
||||
"temperate": (10, 35),
|
||||
"temperate_terminator": (0, 5), # tidally locked by definition
|
||||
"oceanic": (5, 25),
|
||||
"forest": (10, 40),
|
||||
"arid": (5, 30),
|
||||
"cold_arid": (10, 40),
|
||||
"hot_arid": (2, 15),
|
||||
"tropical": (5, 15),
|
||||
"boreal": (20, 50),
|
||||
"frozen": (15, 60), # high tilt → seasonal extremes → frozen
|
||||
"volcanic": (2, 20),
|
||||
"barren": (0, 45),
|
||||
}
|
||||
|
||||
# planet_class → geothermal flux
|
||||
CLASS_GEOTHERMAL = {
|
||||
"volcanic": "extreme",
|
||||
"temperate": "low",
|
||||
"oceanic": "low",
|
||||
"forest": "low",
|
||||
"arid": "low",
|
||||
"frozen": "low",
|
||||
"barren": "low",
|
||||
"volcanic": "extreme",
|
||||
"temperate": "low",
|
||||
"temperate_terminator": "low",
|
||||
"oceanic": "low",
|
||||
"forest": "low",
|
||||
"arid": "low",
|
||||
"cold_arid": "low",
|
||||
"hot_arid": "low",
|
||||
"tropical": "low",
|
||||
"boreal": "low",
|
||||
"frozen": "low",
|
||||
"barren": "low",
|
||||
}
|
||||
|
||||
# planet_class → polar ice latitude (fraction of 0–1, where 1 = poles)
|
||||
# Lower = ice caps extend further toward equator
|
||||
CLASS_POLAR_ICE = {
|
||||
"temperate": (0.72, 0.85),
|
||||
"oceanic": (0.80, 0.92),
|
||||
"forest": (0.75, 0.88),
|
||||
"arid": (0.90, 0.99),
|
||||
"frozen": (0.10, 0.40),
|
||||
"volcanic": (0.95, 1.00),
|
||||
"barren": (0.92, 1.00),
|
||||
"temperate": (0.72, 0.85),
|
||||
"temperate_terminator": (0.75, 0.90),
|
||||
"oceanic": (0.80, 0.92),
|
||||
"forest": (0.75, 0.88),
|
||||
"arid": (0.90, 0.99),
|
||||
"cold_arid": (0.50, 0.70),
|
||||
"hot_arid": (0.95, 1.00),
|
||||
"tropical": (0.88, 0.96),
|
||||
"boreal": (0.45, 0.65),
|
||||
"frozen": (0.10, 0.40),
|
||||
"volcanic": (0.95, 1.00),
|
||||
"barren": (0.92, 1.00),
|
||||
}
|
||||
|
||||
# planet_class → oblateness range
|
||||
CLASS_OBLATENESS = {
|
||||
"temperate": (0.001, 0.005),
|
||||
"oceanic": (0.001, 0.004),
|
||||
"forest": (0.001, 0.005),
|
||||
"arid": (0.001, 0.004),
|
||||
"frozen": (0.001, 0.003),
|
||||
"volcanic": (0.002, 0.008),
|
||||
"barren": (0.000, 0.003),
|
||||
"temperate": (0.001, 0.005),
|
||||
"temperate_terminator": (0.001, 0.004),
|
||||
"oceanic": (0.001, 0.004),
|
||||
"forest": (0.001, 0.005),
|
||||
"arid": (0.001, 0.004),
|
||||
"cold_arid": (0.001, 0.004),
|
||||
"hot_arid": (0.001, 0.004),
|
||||
"tropical": (0.001, 0.005),
|
||||
"boreal": (0.001, 0.004),
|
||||
"frozen": (0.001, 0.003),
|
||||
"volcanic": (0.002, 0.008),
|
||||
"barren": (0.000, 0.003),
|
||||
}
|
||||
|
||||
# Gas giant band palettes available
|
||||
@@ -190,13 +210,18 @@ from biome_config import GAS_PALETTE_SELECTION as GAS_PALETTES
|
||||
|
||||
# planet_class → cloud coverage base range
|
||||
CLASS_CLOUD = {
|
||||
"temperate": (0.35, 0.55),
|
||||
"oceanic": (0.55, 0.75),
|
||||
"forest": (0.40, 0.60),
|
||||
"arid": (0.05, 0.20),
|
||||
"frozen": (0.20, 0.45),
|
||||
"volcanic": (0.60, 0.85),
|
||||
"barren": (0.00, 0.05),
|
||||
"temperate": (0.35, 0.55),
|
||||
"temperate_terminator": (0.30, 0.50),
|
||||
"oceanic": (0.55, 0.75),
|
||||
"forest": (0.40, 0.60),
|
||||
"arid": (0.05, 0.20),
|
||||
"cold_arid": (0.10, 0.25),
|
||||
"hot_arid": (0.02, 0.10),
|
||||
"tropical": (0.60, 0.80),
|
||||
"boreal": (0.55, 0.75),
|
||||
"frozen": (0.20, 0.45),
|
||||
"volcanic": (0.60, 0.85),
|
||||
"barren": (0.00, 0.05),
|
||||
}
|
||||
|
||||
# Atmosphere classes that allow clouds
|
||||
@@ -219,6 +244,7 @@ RING_COLOURS = {
|
||||
"saturnian": [0.88, 0.78, 0.55], # warm golden
|
||||
"icy": [0.85, 0.90, 0.95], # pale ice
|
||||
"sulfuric": [0.75, 0.70, 0.30], # sulphur-tinted
|
||||
"infernal": [0.60, 0.25, 0.15], # dark ember
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -487,13 +513,18 @@ def _build_body_def(
|
||||
|
||||
# ── Atmosphere colour — from star type + planet class ─────────────────
|
||||
atmo_colors = {
|
||||
"temperate": [0.45, 0.65, 1.00],
|
||||
"oceanic": [0.40, 0.60, 1.00],
|
||||
"forest": [0.42, 0.68, 0.80],
|
||||
"arid": [0.90, 0.72, 0.50],
|
||||
"frozen": [0.75, 0.88, 1.00],
|
||||
"volcanic": [0.55, 0.40, 0.30],
|
||||
"barren": None,
|
||||
"temperate": [0.45, 0.65, 1.00],
|
||||
"temperate_terminator": [0.45, 0.65, 1.00],
|
||||
"oceanic": [0.40, 0.60, 1.00],
|
||||
"forest": [0.42, 0.68, 0.80],
|
||||
"arid": [0.90, 0.72, 0.50],
|
||||
"cold_arid": [0.82, 0.58, 0.40],
|
||||
"hot_arid": [0.90, 0.72, 0.50],
|
||||
"tropical": [0.42, 0.68, 0.80],
|
||||
"boreal": [0.45, 0.65, 1.00],
|
||||
"frozen": [0.75, 0.88, 1.00],
|
||||
"volcanic": [0.55, 0.40, 0.30],
|
||||
"barren": None,
|
||||
}
|
||||
atmo_color = atmo_colors.get(planet_class)
|
||||
if atmo_density == "none":
|
||||
@@ -523,8 +554,11 @@ def _build_body_def(
|
||||
# ── Tectonics ─────────────────────────────────────────────────────────
|
||||
tectonic_map = {
|
||||
"volcanic": "extreme", "temperate": "active",
|
||||
"temperate_terminator": "active",
|
||||
"oceanic": "active", "forest": "active",
|
||||
"arid": "low", "frozen": "low", "barren": "none",
|
||||
"arid": "low", "cold_arid": "low",
|
||||
"hot_arid": "low", "tropical": "active",
|
||||
"boreal": "active", "frozen": "low", "barren": "none",
|
||||
}
|
||||
tectonics = tectonic_map.get(planet_class, "low")
|
||||
tectonics = ov.get("tectonics", tectonics)
|
||||
@@ -548,13 +582,18 @@ def _build_body_def(
|
||||
|
||||
# ── Substrate ─────────────────────────────────────────────────────────
|
||||
substrate_map = {
|
||||
"volcanic": "sulfuric",
|
||||
"arid": "silicate",
|
||||
"frozen": "ice",
|
||||
"barren": "silicate",
|
||||
"temperate":"silicate",
|
||||
"oceanic": "silicate",
|
||||
"forest": "silicate",
|
||||
"volcanic": "sulfuric",
|
||||
"arid": "silicate",
|
||||
"cold_arid": "silicate",
|
||||
"hot_arid": "silicate",
|
||||
"tropical": "silicate",
|
||||
"boreal": "silicate",
|
||||
"frozen": "ice",
|
||||
"barren": "silicate",
|
||||
"temperate": "silicate",
|
||||
"temperate_terminator": "silicate",
|
||||
"oceanic": "silicate",
|
||||
"forest": "silicate",
|
||||
}
|
||||
substrate = substrate_map.get(planet_class, "silicate")
|
||||
if hydro == "subsurface" and planet_class == "frozen":
|
||||
@@ -695,8 +734,9 @@ def _build_body_def(
|
||||
"render": render_cfg,
|
||||
}
|
||||
|
||||
# Gas giant extras
|
||||
# Gas giants: drop meaningless terrain fields
|
||||
if gas_giant_cfg:
|
||||
body_def.pop("terrain", None)
|
||||
body_def["gas_giant"] = gas_giant_cfg
|
||||
if rings_cfg:
|
||||
body_def["rings"] = rings_cfg
|
||||
@@ -776,6 +816,7 @@ def parse_system(
|
||||
log.info(f" {bid:20s} {body_def['planet_class']:20s} "
|
||||
f"scale={body_def['body_scale']:6s} "
|
||||
f"seed={body_def['seed']}")
|
||||
_check_habitability(body_def)
|
||||
|
||||
# Write output files
|
||||
if out_dir:
|
||||
@@ -786,7 +827,6 @@ def parse_system(
|
||||
json.dump(bd, f, indent=2)
|
||||
log.info(f"Wrote {len(body_defs)} body definitions → {out_dir}/")
|
||||
|
||||
_check_habitability(body_def)
|
||||
return body_defs
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
render_heightmap.py
|
||||
-------------------
|
||||
Renders a 4096×2048 annotated equirectangular heightmap PNG from a terrain dict.
|
||||
Renders an annotated equirectangular heightmap PNG from a terrain dict.
|
||||
|
||||
This is the PRIMARY output of the planet generator pipeline.
|
||||
The globe render is a separate downstream step that reads the same terrain dict.
|
||||
@@ -54,7 +54,7 @@ from biome_config import (
|
||||
|
||||
OUT_W = 1024
|
||||
OUT_H = 512
|
||||
UI_SCALE = OUT_W / 1024 # 4.0 — all pixel sizes scale with this
|
||||
UI_SCALE = OUT_W / 1024 # 1.0 at default 1024 — all pixel sizes scale with this
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Biome colour palette
|
||||
@@ -327,9 +327,9 @@ def _biome_legend_items(terrain: dict) -> list:
|
||||
|
||||
LABELS = {
|
||||
0: "ocean deep", 1: "ocean", 2: "coastal water",
|
||||
3: "coast", 5: "rainforest", 6: "trop. forest",
|
||||
3: "coast", 5: "trop. rainforest", 6: "trop. forest",
|
||||
7: "savanna", 8: "grassland", 9: "forest",
|
||||
10: "rainforest", 11: "boreal", 12: "shrubland",
|
||||
10: "temp. rainforest", 11: "boreal", 12: "shrubland",
|
||||
13: "temperate desert", 14: "desert", 15: "hot desert",
|
||||
16: "tundra", 17: "ice / snow", 18: "mountain rock",
|
||||
19: "lava field", 20: "chemosyn. mat", 21: "thermophilic",
|
||||
@@ -365,12 +365,13 @@ def _render_legend(img: Image.Image, terrain: dict) -> Image.Image:
|
||||
leg_y = OUT_H - int(34 * UI_SCALE)
|
||||
font = _load_font(int(10 * UI_SCALE))
|
||||
gap = int(6 * UI_SCALE)
|
||||
step = int(108 * UI_SCALE)
|
||||
# Scale step to fit all items within image width
|
||||
max_items = len(items)
|
||||
max_step = (OUT_W - 2 * pad_x) // max(max_items, 1)
|
||||
step = min(int(108 * UI_SCALE), max_step)
|
||||
|
||||
lx = pad_x
|
||||
for label, rgb in items:
|
||||
if lx + step > OUT_W - pad_x:
|
||||
break
|
||||
draw.rectangle([(lx, leg_y), (lx + sw, leg_y + sh)], fill=rgb)
|
||||
draw.text((lx + sw + gap, leg_y), label,
|
||||
fill=(185, 192, 205), font=font)
|
||||
@@ -452,15 +453,15 @@ if __name__ == "__main__":
|
||||
import sys, json, time
|
||||
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python3 render_heightmap.py body_def.json [--small]")
|
||||
print("Usage: python3 render_heightmap.py body_def.json [--large]")
|
||||
sys.exit(1)
|
||||
|
||||
with open(sys.argv[1]) as f:
|
||||
bd = json.load(f)
|
||||
|
||||
# --small flag renders at 1024×512 for fast iteration
|
||||
small = "--small" in sys.argv
|
||||
w, h = (1024, 512) if small else (OUT_W, OUT_H)
|
||||
# --large flag renders at 4096×2048 for high-res review
|
||||
large = "--large" in sys.argv
|
||||
w, h = (4096, 2048) if large else (OUT_W, OUT_H)
|
||||
|
||||
from planet_simulation import simulate
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ def _body_prose(bd: dict, system_dir: Path) -> str:
|
||||
"""Generate markdown content below the frontmatter."""
|
||||
name = bd.get("name") or bd.get("id")
|
||||
bid = bd["id"]
|
||||
pclass = bd.get("planet_class", "unknown").replace("_ringed", "")
|
||||
pclass = bd.get("planet_class", "unknown")
|
||||
btype = bd.get("body_type", "planet")
|
||||
wiki = bd.get("wiki", {})
|
||||
phys = bd.get("physical", {})
|
||||
@@ -76,11 +76,11 @@ def _body_prose(bd: dict, system_dir: Path) -> str:
|
||||
|
||||
# Type line
|
||||
if btype == "moon":
|
||||
lines.append(f"{pclass.title()} moon.")
|
||||
lines.append(f"{pclass.replace('_', ' ').title()} moon.")
|
||||
elif pclass in ("gas_giant", "gas_giant_ringed"):
|
||||
lines.append(f"Gas giant.")
|
||||
else:
|
||||
lines.append(f"{pclass.title()} {btype}.")
|
||||
lines.append(f"{pclass.replace('_', ' ').title()} {btype}.")
|
||||
lines.append("")
|
||||
|
||||
# System link
|
||||
@@ -92,7 +92,7 @@ def _body_prose(bd: dict, system_dir: Path) -> str:
|
||||
lines.append("")
|
||||
lines.append(f"")
|
||||
lines.append("")
|
||||
if pclass not in ("gas_giant",):
|
||||
if pclass not in ("gas_giant", "gas_giant_ringed"):
|
||||
lines.append(f"")
|
||||
lines.append("")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user