feat(assets): 100% globe coverage — generic images + donor fill
Generated oort cloud and asteroid belt generic globe images via Gemini. Background-masked with flood-fill (shadow-preserving). Donor fill script copies matching globe.png from same planet_class+body_type pool with per-body hash selection, no repeats within a system. 629 bodies filled, 3240/3240 now have globe.png. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@@ -0,0 +1,176 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
One-shot script: fill missing globe.png files in the wiki by copying a donor
|
||||
globe from the same planet_class + body_type pool.
|
||||
|
||||
Donor selection:
|
||||
- Hash body_id to pick deterministically from the pool
|
||||
- Track used donors per system to avoid visual repetition
|
||||
- Skip oort_cloud and asteroid_belt (handled by generic images)
|
||||
|
||||
Run once, then the files are baked into the wiki like authored content.
|
||||
|
||||
Usage:
|
||||
python3 tooling/fill-missing-globes.py [--dry-run]
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import os
|
||||
import shutil
|
||||
import sqlite3
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
WIKI_ROOT = REPO_ROOT / "wiki" / "star-systems"
|
||||
DB_PATH = REPO_ROOT / "server" / "data" / "systems.db"
|
||||
|
||||
GENERIC_TYPES = {"oort_cloud", "asteroid_belt"}
|
||||
GENERIC_DIR = WIKI_ROOT / "_generics"
|
||||
|
||||
|
||||
def globe_path(system_id: str, body_id: str) -> Path:
|
||||
sys_dir = system_id.replace(" ", "-")
|
||||
return WIKI_ROOT / sys_dir / "bodies" / body_id / "globe.png"
|
||||
|
||||
|
||||
def body_hash(body_id: str) -> int:
|
||||
return int(hashlib.sha256(body_id.encode()).hexdigest()[:8], 16)
|
||||
|
||||
|
||||
def main():
|
||||
dry_run = "--dry-run" in sys.argv
|
||||
|
||||
conn = sqlite3.connect(str(DB_PATH))
|
||||
conn.row_factory = sqlite3.Row
|
||||
|
||||
bodies = conn.execute(
|
||||
"SELECT body_id, system_id, body_type, planet_class FROM bodies"
|
||||
).fetchall()
|
||||
|
||||
# Build pools of existing globes by (body_type, planet_class)
|
||||
pools: dict[tuple[str, str], list[Path]] = defaultdict(list)
|
||||
missing: list[dict] = []
|
||||
|
||||
for b in bodies:
|
||||
bid = b["body_id"]
|
||||
bt = b["body_type"] or "unknown"
|
||||
pc = (b["planet_class"] or "unknown").lower()
|
||||
path = globe_path(b["system_id"], bid)
|
||||
|
||||
if bt in GENERIC_TYPES:
|
||||
# Handled by generic images, not donors
|
||||
if not path.exists():
|
||||
generic = GENERIC_DIR / bt / "globe.png"
|
||||
if generic.exists():
|
||||
missing.append({
|
||||
"body_id": bid,
|
||||
"system_id": b["system_id"],
|
||||
"body_type": bt,
|
||||
"planet_class": pc,
|
||||
"donor_path": generic,
|
||||
"source": "generic",
|
||||
})
|
||||
continue
|
||||
|
||||
if path.exists():
|
||||
pools[(bt, pc)].append(path)
|
||||
else:
|
||||
missing.append({
|
||||
"body_id": bid,
|
||||
"system_id": b["system_id"],
|
||||
"body_type": bt,
|
||||
"planet_class": pc,
|
||||
"donor_path": None,
|
||||
"source": "donor",
|
||||
})
|
||||
|
||||
# For each missing body, pick a donor
|
||||
used_per_system: dict[str, set[str]] = defaultdict(set)
|
||||
assigned = 0
|
||||
skipped = 0
|
||||
log_lines = []
|
||||
|
||||
for m in missing:
|
||||
if m["source"] == "generic":
|
||||
# Generic image copy
|
||||
target = globe_path(m["system_id"], m["body_id"])
|
||||
if dry_run:
|
||||
log_lines.append(
|
||||
f"[GENERIC] {m['body_id']} <- {m['donor_path'].name} ({m['body_type']})"
|
||||
)
|
||||
else:
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(m["donor_path"], target)
|
||||
log_lines.append(
|
||||
f"[GENERIC] {m['body_id']} <- {m['donor_path']}"
|
||||
)
|
||||
assigned += 1
|
||||
continue
|
||||
|
||||
bt = m["body_type"]
|
||||
pc = m["planet_class"]
|
||||
key = (bt, pc)
|
||||
|
||||
pool = pools.get(key, [])
|
||||
if not pool:
|
||||
# Try broader match: same body_type, any class
|
||||
for (pbt, ppc), p in pools.items():
|
||||
if pbt == bt and p:
|
||||
pool = p
|
||||
break
|
||||
|
||||
if not pool:
|
||||
log_lines.append(
|
||||
f"[SKIP] {m['body_id']} — no donors for {bt}/{pc}"
|
||||
)
|
||||
skipped += 1
|
||||
continue
|
||||
|
||||
# Pick donor by hash, avoid repeats in same system
|
||||
sys_id = m["system_id"]
|
||||
h = body_hash(m["body_id"])
|
||||
pool_size = len(pool)
|
||||
used = used_per_system[sys_id]
|
||||
|
||||
donor = None
|
||||
for offset in range(pool_size):
|
||||
candidate = pool[(h + offset) % pool_size]
|
||||
candidate_key = str(candidate)
|
||||
if candidate_key not in used:
|
||||
donor = candidate
|
||||
used.add(candidate_key)
|
||||
break
|
||||
|
||||
if donor is None:
|
||||
# All donors used in this system — allow repeat from least-used
|
||||
donor = pool[h % pool_size]
|
||||
|
||||
target = globe_path(sys_id, m["body_id"])
|
||||
donor_bid = donor.parent.name
|
||||
|
||||
if dry_run:
|
||||
log_lines.append(
|
||||
f"[DONOR] {m['body_id']} <- {donor_bid} ({bt}/{pc})"
|
||||
)
|
||||
else:
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(donor, target)
|
||||
log_lines.append(
|
||||
f"[DONOR] {m['body_id']} <- {donor_bid} ({bt}/{pc})"
|
||||
)
|
||||
assigned += 1
|
||||
|
||||
conn.close()
|
||||
|
||||
# Report
|
||||
prefix = "[DRY RUN] " if dry_run else ""
|
||||
print(f"{prefix}Assigned: {assigned}, Skipped: {skipped}")
|
||||
print()
|
||||
for line in log_lines:
|
||||
print(line)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 111 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 90 KiB |
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.4 MiB |