// Creation wizard: 6 steps. Atmosphere → Surface → Moons → Rings → Star → Name. // Each step renders a control surface; a pinned live 3D preview stays visible. const { useState, useEffect, useRef, useMemo } = React; const WIZ_STEPS = [ { id: 'atmosphere', label: 'Atmosphere' }, { id: 'surface', label: 'Surface' }, { id: 'moons', label: 'Moons' }, { id: 'rings', label: 'Rings' }, { id: 'star', label: 'Star' }, { id: 'seal', label: 'Seal' }, ]; function defaultSpec() { return { atmosphere: [{sym:'N',pct:60},{sym:'O',pct:30},{sym:'H₂O',pct:10}], surface: 'oceanic', moons: [{name:'Moon I', size:0.2, tint:'#ccd8ff'}], rings: { enabled: false, count: 1, tilt: 18, density: 0.5 }, rotationHours: 24, orbitAU: 1.0, star: 'G', weather: 'mild', life: 'microbial', axial: 15, name: '', discoverer: '', }; } // ============ Step 1: Atmosphere ============ // Atmospheric chemistry constants used for inline science hints. const GREENHOUSE_SET = new Set(['CO₂','CH4','H₂O','NH₃','O₃']); const SHIELD_SET = new Set(['O₃']); const LIGHT_GAS_SET = new Set(['H','He']); // escape easily from small/hot worlds const BIOSIG_PAIR = ['O','CH4']; // disequilibrium suggests an active biosphere function atmosphereAnalytics(atmosphere) { const els = window.PLANETALIN_ELEMENTS; const byS = new Map(els.map(e => [e.sym, e])); const total = atmosphere.reduce((s,a) => s+a.pct, 0) || 0; if (!total) return null; // Mass-weighted mean molecular mass (u) let mmm = 0, greenhouse = 0, lightGas = 0; for (const a of atmosphere) { const el = byS.get(a.sym); if (!el) continue; const frac = a.pct / total; mmm += el.mass * frac; if (GREENHOUSE_SET.has(a.sym)) greenhouse += frac; if (LIGHT_GAS_SET.has(a.sym)) lightGas += frac; } const hasO = atmosphere.some(a => a.sym === BIOSIG_PAIR[0] && a.pct > 0); const hasCH4 = atmosphere.some(a => a.sym === BIOSIG_PAIR[1] && a.pct > 0); const hasO3 = atmosphere.some(a => a.sym === 'O₃' && a.pct > 0); return { mmm, greenhouse, // 0..1 — fraction by volume of GHGs lightGas, // 0..1 — fraction of easily-escaping light gases biosignature: hasO && hasCH4, uvShield: hasO3, rayleighBlue: mmm > 20 && atmosphere.some(a => a.sym === 'N' || a.sym === 'O'), }; } function StepAtmosphere({ spec, setSpec }) { const els = window.PLANETALIN_ELEMENTS; const inAtmos = (sym) => spec.atmosphere.find(a => a.sym === sym); const totalPct = spec.atmosphere.reduce((s,a) => s+a.pct, 0); const sci = atmosphereAnalytics(spec.atmosphere); function addElement(sym) { if (inAtmos(sym) || spec.atmosphere.length >= 5) return; const newAtm = [...spec.atmosphere, { sym, pct: 10 }]; setSpec({ ...spec, atmosphere: newAtm }); } function removeElement(sym) { setSpec({ ...spec, atmosphere: spec.atmosphere.filter(a => a.sym !== sym) }); } function updatePct(sym, pct) { setSpec({ ...spec, atmosphere: spec.atmosphere.map(a => a.sym === sym ? {...a, pct: Math.round(pct)} : a) }); } function normalize() { if (totalPct === 0) return; setSpec({ ...spec, atmosphere: window.PLANETALIN.normalizeAtmosphere(spec.atmosphere) }); } return (
01 · Composition

What air will your world breathe?

Select up to 5 elements. Their signature colors blend into the atmosphere and drive the planet's palette.

{els.map(el => { const sel = inAtmos(el.sym); return ( ); })}
MIXTURE · {totalPct}%
{spec.atmosphere.length === 0 && (
No elements selected yet. Tap above.
)}
{spec.atmosphere.map(a => { const el = els.find(e => e.sym === a.sym); return (
{a.sym}
); })}
{spec.atmosphere.map(a => { const el = els.find(e => e.sym === a.sym); return (
{el.sym} {el.name} {el.mass} u {el.spectrum} updatePct(a.sym, +e.target.value)} /> {a.pct}%
); })}
{sci && (
ATMOSPHERIC ANALYSIS
Mean molecular mass
{sci.mmm.toFixed(1)} u
{sci.mmm < 10 ? 'Very light — escapes small worlds' : sci.mmm < 20 ? 'Light — retained only with enough gravity' : sci.mmm < 35 ? 'Earth-like weight' : 'Heavy — sinks, dense lower atmosphere'}
Greenhouse index
{Math.round(sci.greenhouse*100)}%
{sci.greenhouse < 0.1 ? 'Transparent to IR — runs cool' : sci.greenhouse < 0.35 ? 'Moderate warming' : sci.greenhouse < 0.7 ? 'Strong greenhouse — surface baked' : 'Runaway greenhouse (Venus-type)'}
Light-gas fraction
{Math.round(sci.lightGas*100)}%
{sci.lightGas > 0.3 ? 'H/He dominated — needs giant-world gravity to hold' : sci.lightGas > 0 ? 'Trace light gases may slowly escape' : 'Stable against Jeans escape'}
{sci.rayleighBlue && ☁︎ Rayleigh-blue sky} {sci.uvShield && ☀︎ UV shield · ozone absorbs 200–300 nm} {sci.greenhouse > 0.6 && ♨ Runaway greenhouse risk} {sci.biosignature && ✦ O₂ + CH₄ disequilibrium — biosignature pair} {sci.lightGas > 0.5 && ↑ Light gases escape — needs Jovian gravity}
)}
); } // ============ Step 2: Surface ============ function StepSurface({ spec, setSpec }) { const surfaces = window.PLANETALIN_SURFACES; return (
02 · Crust

What lies beneath the sky?

Choose the character of your world's surface, and the weather that moves across it.

{surfaces.map(s => ( ))}
Weather
{['clear','mild','storms','auroras','inferno'].map(w => ( ))}
Life indicators
{['none','microbial','complex','intelligent'].map(w => ( ))}
Rotation · day length {spec.rotationHours} h
setSpec({...spec, rotationHours:+e.target.value})} />
Axial tilt {spec.axial}°
{/* Extended to 180° so retrograde worlds (Venus 177°, Uranus 98°) are expressible. */} setSpec({...spec, axial:+e.target.value})} />
); } // ============ Step 3: Moons ============ function StepMoons({ spec, setSpec }) { function addMoon() { if (spec.moons.length >= 5) return; const n = spec.moons.length; const names = ['I','II','III','IV','V']; setSpec({...spec, moons:[...spec.moons, { name:`Moon ${names[n]}`, size:0.15+Math.random()*0.15, tint:'#cccfe8' }]}); } function removeMoon(i) { setSpec({...spec, moons: spec.moons.filter((_,idx)=>idx!==i)}); } function updateMoon(i, patch) { setSpec({...spec, moons: spec.moons.map((m,idx)=> idx===i ? {...m,...patch} : m)}); } return (
03 · Satellites

How many moons circle your world?

Up to five. Each can be tinted and sized independently.

{spec.moons.map((m,i) => (
updateMoon(i, {name: e.target.value})} />
size updateMoon(i, {size: +e.target.value})} />
updateMoon(i, {tint: e.target.value})} className="moon-color" />
))} {spec.moons.length === 0 && (
A lonely world. No moons.
)}
); } // ============ Step 4: Rings ============ function StepRings({ spec, setSpec }) { const r = spec.rings; return (
04 · Rings

Will it wear a halo?

Dust and ice can orbit in bands around your planet. Not every world needs them.

{r.enabled && (
Bands {r.count}
setSpec({...spec, rings:{...r, count:+e.target.value}})} />
Tilt {r.tilt}°
setSpec({...spec, rings:{...r, tilt:+e.target.value}})} />
Density {Math.round(r.density*100)}%
setSpec({...spec, rings:{...r, density:+e.target.value}})} />
)}
); } // ============ Step 5: Star ============ function StepStar({ spec, setSpec }) { const stars = window.PLANETALIN_STARS; // Range goes to 40 AU so Neptune (30 AU) and outer bodies are expressible. const ORBIT_MIN = 0.2, ORBIT_MAX = 40; const hz = window.PLANETALIN.habitableZone(spec.star); const regime = window.PLANETALIN.orbitRegime(spec.star, spec.orbitAU); const tempK = window.PLANETALIN.equilibriumTempK(spec.star, spec.orbitAU); const tempC = Math.round(tempK - 273.15); // HZ band position on the orbit slider, in % of the slider range. // Uses a log scale so tight inner orbits don't get crushed against the start. const lo = Math.log(ORBIT_MIN), hi = Math.log(ORBIT_MAX); const pctOf = au => Math.max(0, Math.min(100, ((Math.log(Math.max(ORBIT_MIN, au)) - lo) / (hi - lo)) * 100)); const hzLeft = pctOf(hz.inner); const hzRight = pctOf(hz.outer); return (
05 · Primary

Which sun does it orbit?

Stellar class shapes the colour of daylight. Orbital distance sets the season — and the habitable zone.

{stars.map(s => ( ))}
Orbital distance {spec.orbitAU.toFixed(2)} AU · {regime.label} · ~{tempC > -100 ? tempC : Math.round(tempK)+'K'}{tempC > -100 ? '°C' : ''}
setSpec({...spec, orbitAU:+e.target.value})} />
scorchedhabitable {hz.inner.toFixed(2)}–{hz.outer.toFixed(2)} AUfrozen
{regime.detail}. Luminosity ≈ {hz.luminosity >= 1 ? hz.luminosity.toFixed(1) : hz.luminosity.toFixed(2)} L☉.
); } // ============ Step 6: Seal ============ function StepSeal({ spec, setSpec, onSeal, sealState }) { const [localName, setLocalName] = useState(spec.name); const [localDisc, setLocalDisc] = useState(spec.discoverer); const [cooldownMs, setCooldownMs] = useState(window.PLANETALIN.msUntilNextWindow()); useEffect(() => { setSpec({...spec, name: localName, discoverer: localDisc}); }, [localName, localDisc]); useEffect(() => { const iv = setInterval(() => setCooldownMs(window.PLANETALIN.msUntilNextWindow()), 1000); return () => clearInterval(iv); }, []); const canCreate = cooldownMs === 0; const hrs = Math.floor(cooldownMs/3600000); const mins = Math.floor((cooldownMs%3600000)/60000); const secs = Math.floor((cooldownMs%60000)/1000); const cdLabel = hrs > 0 ? `${hrs}h ${String(mins).padStart(2,'0')}m` : `${mins}m ${String(secs).padStart(2,'0')}s`; return (
06 · Seal

Name it. Sign it. Seal it.

Once sealed, the specification is permanent. You may fork it into a new world, but you may not alter this one.

Specification
Atmosphere
{spec.atmosphere.map(a=>`${a.sym} ${a.pct}%`).join(' · ')||'—'}
Surface
{spec.surface}
Weather
{spec.weather} · life: {spec.life}
Moons
{spec.moons.length ? spec.moons.map(m=>m.name).join(', ') : 'none'}
Rings
{spec.rings.enabled ? `${spec.rings.count} bands @ ${spec.rings.tilt}°` : 'none'}
Star
{spec.star}-type · {spec.orbitAU.toFixed(2)} AU
Day
{spec.rotationHours}h · tilt {spec.axial}°
{!canCreate && sealState === 'idle' && (
SKY CLOSED
You've already shaped a world in the current window. Your spec is held here — you can come back and seal it in {cdLabel}.
)}
); } // Global window.PLANETALINWizard = { defaultSpec, StepAtmosphere, StepSurface, StepMoons, StepRings, StepStar, StepSeal, WIZ_STEPS };