152 lines
9.1 KiB
JavaScript
152 lines
9.1 KiB
JavaScript
// SOLARGOD almanac (Stage 6) — the info panel that knows. On focus it shows a
|
||
// grimoire card: a physical fact sheet + hand-written prose + (if generated) a
|
||
// MODELBEAST portrait, plus LIVE computed facts that come free from the ephemeris
|
||
// — distance from Sun and Earth, sunlight age (light-time), orbital speed, and the
|
||
// length of the body's day and year (brief §5). Focusing Earth reveals the GODSIGH
|
||
// easter egg. Not a toggleable feed — no HUD row.
|
||
//
|
||
// Wave 2 (SYZYGY): planets also get a predictive fact — Next opposition (outer) /
|
||
// Next elongation (inner) — computed from the same ephemeris via events.js.
|
||
|
||
import { nextEvent, isOuter, isInner } from './events.js';
|
||
|
||
const GM_SUN = 1.32712440018e11; // km^3/s^2, heliocentric gravitational parameter
|
||
const YEAR_DAYS = 365.256;
|
||
|
||
const BLURBS = {
|
||
sun: "The hearth of the system and 99.86% of its mass — a self-gravitating furnace fusing 600 million tonnes of hydrogen every second. Every world here is a fleck of its leftover disk, still falling around it.",
|
||
mercury: "A cratered iron cinder with almost no air, swinging so close to the Sun that it laps every other world. Its days are longer than its years; three rotations fit in two of its tight orbits.",
|
||
venus: "Earth's twin gone wrong: a runaway greenhouse under sulphuric cloud, hot enough to melt lead and crushing as the deep sea. It turns backwards, so slowly that a Venusian day outlasts its year.",
|
||
earth: "The one world we know that woke up. A thin film of water and air over spinning iron, tilted just so for seasons — the pale blue vantage from which all the rest of this was measured.",
|
||
mars: "A rusted desert half Earth's size, streaked with the dry beds of ancient rivers and crowned by Olympus Mons, the tallest volcano known. Cold, thin-aired, and the most-visited graveyard of robots off-world.",
|
||
jupiter: "A failed star and the system's great vacuum cleaner, its storms wider than Earth and its gravity shepherding the asteroid belt. Four bright moons circle it in a clockwork Galileo used to unseat the Earth from the centre.",
|
||
saturn: "The jewel — a globe you could float on water, wrapped in rings of ice no thicker than a mountain is tall. Titan below hides a haze-orange world of methane rain and hydrocarbon seas.",
|
||
uranus: "An ice giant knocked onto its side, likely by an ancient impact, so it rolls around the Sun pole-first. Its faint rings and pale cyan haze mark the coldest atmosphere of any planet.",
|
||
neptune: "The last great world, a deep-blue storm-lantern found by mathematics before any eye saw it. Its winds are the fastest in the system, and Triton, its captured moon, orbits backwards toward a slow doom.",
|
||
pluto: "The heart-marked dwarf at the edge of the known, half rock and half ice, locked in a slow gravitational dance with its outsized moon Charon. Demoted from planet, it remains the king of the Kuiper Belt.",
|
||
moon: "Earth's single great moon, born of a Mars-sized collision and slowly drifting away. It keeps one face turned to us forever, and its pull raises the tides that may have coaxed life ashore.",
|
||
io: "The most volcanic body in the system, flexed and heated by Jupiter's tides until it turns itself inside out in sulphur.",
|
||
europa: "A cracked shell of ice over a global ocean — perhaps twice Earth's water — and one of the best places to look for life beyond home.",
|
||
ganymede: "The largest moon in the system, bigger than Mercury, and the only one with its own magnetic field.",
|
||
callisto: "The most heavily cratered world known, a dark ancient face that has barely changed in four billion years.",
|
||
titan: "The only moon with a thick atmosphere, raining methane onto seas of liquid hydrocarbon under an orange sky.",
|
||
enceladus: "A tiny snowball that jets plumes of ocean water into space from cracks at its south pole.",
|
||
triton: "A captured Kuiper Belt world orbiting Neptune backwards, spouting nitrogen geysers across a frozen pink surface.",
|
||
charon: "Pluto's giant partner — the two are tidally locked face to face, forever hanging still in each other's sky.",
|
||
};
|
||
|
||
export default function create(ctx) {
|
||
const { bodies, ephem, lib, focus } = ctx;
|
||
const panel = document.getElementById('infopanel');
|
||
let lastFocus = null;
|
||
let lastWall = 0;
|
||
const _b = {}, _e = {};
|
||
// Memo for the predictive fact so we don't rescan the ephemeris every 200 ms —
|
||
// recompute only on focus change, when the event passes, or a backward scrub.
|
||
let neCache = { id: null, fromJd: 0, ev: undefined };
|
||
|
||
function helioOf(id, jd, out) {
|
||
const b = bodies[id];
|
||
if (id === 'sun') { out.x = out.y = out.z = 0; return out; }
|
||
if (b.ephemId) return ephem.helioEcl(b.ephemId, jd, out);
|
||
if (b.elements) return ephem.smallBodyEcl(b.elements, jd, out);
|
||
if (b.moonOrbit && b.parent) {
|
||
// parent heliocentric + the moon's TRUE orbital offset (AU) — so e.g. the
|
||
// Moon's distance from Earth is its real ~384,400 km, not 0.
|
||
helioOf(b.parent, jd, out);
|
||
const theta = 2 * Math.PI * ((jd - lib.J2000_JD) / b.moonOrbit.periodDays);
|
||
const rAU = b.moonOrbit.aKm / lib.AU_KM;
|
||
const tilt = (bodies[b.parent].axialTiltDeg || 0) * lib.DEG;
|
||
const ex = Math.cos(theta) * rAU, ey = Math.sin(theta) * rAU;
|
||
out.x += ex; out.y += ey * Math.cos(tilt); out.z += ey * Math.sin(tilt);
|
||
return out;
|
||
}
|
||
if (b.parent) return helioOf(b.parent, jd, out);
|
||
out.x = out.y = out.z = 0; return out;
|
||
}
|
||
|
||
function render(id) {
|
||
const b = bodies[id];
|
||
const artPath = `assets/art/grimoire/${id}.jpg`;
|
||
const earthEgg = id === 'earth'
|
||
? `<a class="ip-link" href="http://127.0.0.1:8137" target="_blank" rel="noopener">▸ open GODSIGH world view</a>` : '';
|
||
panel.innerHTML = `
|
||
<div class="ip-close" role="button" aria-label="close">×</div>
|
||
<div class="ip-title"></div>
|
||
<div class="ip-type"></div>
|
||
<img class="ip-art" alt="" />
|
||
<div class="ip-blurb"></div>
|
||
<div class="ip-facts"></div>
|
||
${earthEgg}`;
|
||
panel.querySelector('.ip-title').textContent = b.name;
|
||
panel.querySelector('.ip-type').textContent = b.type;
|
||
panel.querySelector('.ip-blurb').textContent = BLURBS[id] || `A ${b.type}${b.parent ? ' of ' + bodies[b.parent].name : ''} in the solar system.`;
|
||
const img = panel.querySelector('.ip-art');
|
||
img.src = artPath;
|
||
img.onerror = () => { img.style.display = 'none'; };
|
||
panel.querySelector('.ip-close').addEventListener('click', () => { panel.hidden = true; });
|
||
panel.hidden = false;
|
||
}
|
||
|
||
function fact(label, value) {
|
||
return `<div class="ip-fact"><span>${label}</span><span>${value}</span></div>`;
|
||
}
|
||
|
||
function updateLive(id, jd) {
|
||
const b = bodies[id];
|
||
helioOf(id, jd, _b);
|
||
const rSun = Math.hypot(_b.x, _b.y, _b.z); // AU
|
||
ephem.helioEcl('earth', jd, _e);
|
||
const rEarth = id === 'earth' ? 0 : Math.hypot(_b.x - _e.x, _b.y - _e.y, _b.z - _e.z);
|
||
const rows = [];
|
||
// physical
|
||
rows.push(fact('Radius', lib.fmtKm(b.radiusKm)));
|
||
if (b.axialTiltDeg != null) rows.push(fact('Axial tilt', `${b.axialTiltDeg.toFixed(1)}°`));
|
||
// live distances
|
||
if (id !== 'sun') rows.push(fact('From Sun', `${lib.fmtAU(rSun)} · ${lib.fmtLightTime(rSun)} light`));
|
||
rows.push(fact('From Earth', id === 'earth' ? '—' : `${lib.fmtAU(rEarth)} · ${lib.fmtLightTime(rEarth)} light`));
|
||
// orbital speed + periods
|
||
if (b.type === 'moon') {
|
||
const per = Math.abs(b.moonOrbit.periodDays);
|
||
const v = (2 * Math.PI * b.moonOrbit.aKm) / (per * 86400);
|
||
rows.push(fact('Orbits', bodies[b.parent].name));
|
||
rows.push(fact('Orbital speed', lib.fmtSpeed(v)));
|
||
rows.push(fact('Orbital period', `${lib.fmtDuration(per)}${b.moonOrbit.periodDays < 0 ? ' (retrograde)' : ''}`));
|
||
} else if (id !== 'sun') {
|
||
const a = b.elements ? b.elements.a : ephem.semiMajor(b.ephemId, jd);
|
||
const rKm = rSun * lib.AU_KM, aKm = a * lib.AU_KM;
|
||
const v = Math.sqrt(GM_SUN * (2 / rKm - 1 / aKm)); // vis-viva
|
||
rows.push(fact('Orbital speed', lib.fmtSpeed(v)));
|
||
rows.push(fact('Year', lib.fmtDuration(YEAR_DAYS * Math.pow(a, 1.5))));
|
||
}
|
||
if (b.rotationHours) {
|
||
rows.push(fact('Day (rotation)', `${lib.fmtDuration(Math.abs(b.rotationHours) / 24)}${b.rotationHours < 0 ? ' (retrograde)' : ''}`));
|
||
}
|
||
// predictive sky event (SYZYGY): outer → Next opposition, inner → Next elongation.
|
||
if (isOuter(id) || isInner(id)) {
|
||
if (neCache.id !== id || neCache.ev === undefined || (neCache.ev && jd > neCache.ev.jd) || jd < neCache.fromJd) {
|
||
neCache = { id, fromJd: jd, ev: nextEvent(id, jd) };
|
||
}
|
||
const ev = neCache.ev;
|
||
if (ev) {
|
||
rows.push(fact(isOuter(id) ? 'Next opposition' : 'Next elongation',
|
||
`${lib.formatUTCDate(lib.unixMsFromJd(ev.jd))} · in ${lib.fmtDuration(ev.jd - jd)}`));
|
||
}
|
||
}
|
||
const facts = panel.querySelector('.ip-facts');
|
||
if (facts) facts.innerHTML = rows.join('');
|
||
}
|
||
|
||
return {
|
||
id: 'almanac',
|
||
onClockTick(simMs, jd) {
|
||
if (focus.id !== lastFocus) { lastFocus = focus.id; render(focus.id); lastWall = 0; }
|
||
if (panel.hidden) return;
|
||
const now = performance.now();
|
||
if (now - lastWall < 200) return; // ≤5 Hz
|
||
lastWall = now;
|
||
updateLive(focus.id, jd);
|
||
},
|
||
};
|
||
}
|