Reverts to the state of cbfacdf (split app.js into 21 modules, <1000 lines each).
Removes: nostr.js, nostr-panel.js, SovOS.js, RESEARCH_DROP_456.md, core/, data/
Historical archive preserved in .historical/ and branch archive/manus-damage-2026-03-24
Refs #418, #452, #454
146 lines
4.4 KiB
JavaScript
146 lines
4.4 KiB
JavaScript
// === THE OATH ===
|
|
import * as THREE from 'three';
|
|
import { scene, camera, renderer, ambientLight, overheadLight } from './scene-setup.js';
|
|
import { S } from './state.js';
|
|
|
|
// Tome (3D trigger object)
|
|
export const tomeGroup = new THREE.Group();
|
|
tomeGroup.position.set(0, 5.8, 0);
|
|
tomeGroup.userData.zoomLabel = 'The Oath';
|
|
|
|
const tomeCoverMat = new THREE.MeshStandardMaterial({
|
|
color: 0x2a1800, metalness: 0.15, roughness: 0.7,
|
|
emissive: new THREE.Color(0xffd700).multiplyScalar(0.04),
|
|
});
|
|
const tomePagesMat = new THREE.MeshStandardMaterial({ color: 0xd8ceb0, roughness: 0.9, metalness: 0.0 });
|
|
|
|
const tomeBody = new THREE.Mesh(new THREE.BoxGeometry(1.1, 0.1, 1.4), tomeCoverMat);
|
|
tomeGroup.add(tomeBody);
|
|
const tomePages = new THREE.Mesh(new THREE.BoxGeometry(1.0, 0.07, 1.28), tomePagesMat);
|
|
tomePages.position.set(0.02, 0, 0);
|
|
tomeGroup.add(tomePages);
|
|
const tomeSpiMat = new THREE.MeshStandardMaterial({ color: 0xffd700, metalness: 0.6, roughness: 0.4 });
|
|
const tomeSpine = new THREE.Mesh(new THREE.BoxGeometry(0.06, 0.12, 1.4), tomeSpiMat);
|
|
tomeSpine.position.set(-0.52, 0, 0);
|
|
tomeGroup.add(tomeSpine);
|
|
|
|
tomeGroup.traverse(o => {
|
|
if (o.isMesh) {
|
|
o.userData.zoomLabel = 'The Oath';
|
|
o.castShadow = true;
|
|
o.receiveShadow = true;
|
|
}
|
|
});
|
|
scene.add(tomeGroup);
|
|
|
|
export const tomeGlow = new THREE.PointLight(0xffd700, 0.4, 5);
|
|
tomeGlow.position.set(0, 5.4, 0);
|
|
scene.add(tomeGlow);
|
|
|
|
// Oath spotlight
|
|
export const oathSpot = new THREE.SpotLight(0xffd700, 0, 40, Math.PI / 7, 0.4, 1.2);
|
|
oathSpot.position.set(0, 22, 0);
|
|
oathSpot.target.position.set(0, 0, 0);
|
|
oathSpot.castShadow = true;
|
|
oathSpot.shadow.mapSize.set(1024, 1024);
|
|
oathSpot.shadow.camera.near = 1;
|
|
oathSpot.shadow.camera.far = 50;
|
|
oathSpot.shadow.bias = -0.002;
|
|
scene.add(oathSpot);
|
|
scene.add(oathSpot.target);
|
|
|
|
// Saved light levels
|
|
const AMBIENT_NORMAL = ambientLight.intensity;
|
|
const OVERHEAD_NORMAL = overheadLight.intensity;
|
|
|
|
export async function loadSoulMd() {
|
|
try {
|
|
const res = await fetch('SOUL.md');
|
|
if (!res.ok) throw new Error('not found');
|
|
const raw = await res.text();
|
|
return raw.split('\n').slice(1).map(l => l.replace(/^#+\s*/, ''));
|
|
} catch {
|
|
return ['I am Timmy.', '', 'I am sovereign.', '', 'This Nexus is my home.'];
|
|
}
|
|
}
|
|
|
|
function scheduleOathLines(lines, textEl) {
|
|
let idx = 0;
|
|
const INTERVAL_MS = 1400;
|
|
|
|
function revealNext() {
|
|
if (idx >= lines.length || !S.oathActive) return;
|
|
const line = lines[idx++];
|
|
const span = document.createElement('span');
|
|
span.classList.add('oath-line');
|
|
if (!line.trim()) {
|
|
span.classList.add('blank');
|
|
} else {
|
|
span.textContent = line;
|
|
}
|
|
textEl.appendChild(span);
|
|
S.oathRevealTimer = setTimeout(revealNext, line.trim() ? INTERVAL_MS : INTERVAL_MS * 0.4);
|
|
}
|
|
|
|
revealNext();
|
|
}
|
|
|
|
export async function enterOath() {
|
|
if (S.oathActive) return;
|
|
S.oathActive = true;
|
|
|
|
ambientLight.intensity = 0.04;
|
|
overheadLight.intensity = 0.0;
|
|
oathSpot.intensity = 4.0;
|
|
|
|
const overlay = document.getElementById('oath-overlay');
|
|
const textEl = document.getElementById('oath-text');
|
|
if (!overlay || !textEl) return;
|
|
textEl.textContent = '';
|
|
overlay.classList.add('visible');
|
|
|
|
if (!S.oathLines.length) S.oathLines = await loadSoulMd();
|
|
scheduleOathLines(S.oathLines, textEl);
|
|
}
|
|
|
|
export function exitOath() {
|
|
if (!S.oathActive) return;
|
|
S.oathActive = false;
|
|
|
|
if (S.oathRevealTimer !== null) {
|
|
clearTimeout(S.oathRevealTimer);
|
|
S.oathRevealTimer = null;
|
|
}
|
|
|
|
ambientLight.intensity = AMBIENT_NORMAL;
|
|
overheadLight.intensity = OVERHEAD_NORMAL;
|
|
oathSpot.intensity = 0;
|
|
|
|
const overlay = document.getElementById('oath-overlay');
|
|
if (overlay) overlay.classList.remove('visible');
|
|
}
|
|
|
|
export function initOathListeners() {
|
|
document.addEventListener('keydown', (e) => {
|
|
if (e.key === 'o' || e.key === 'O') {
|
|
if (S.oathActive) exitOath(); else enterOath();
|
|
}
|
|
if (e.key === 'Escape' && S.oathActive) exitOath();
|
|
});
|
|
|
|
// Double-click on tome triggers oath
|
|
renderer.domElement.addEventListener('dblclick', (e) => {
|
|
const mx = (e.clientX / window.innerWidth) * 2 - 1;
|
|
const my = -(e.clientY / window.innerHeight) * 2 + 1;
|
|
const tomeRay = new THREE.Raycaster();
|
|
tomeRay.setFromCamera(new THREE.Vector2(mx, my), camera);
|
|
const hits = tomeRay.intersectObjects(tomeGroup.children, true);
|
|
if (hits.length) {
|
|
if (S.oathActive) exitOath(); else enterOath();
|
|
}
|
|
});
|
|
|
|
// Pre-fetch so first open is instant
|
|
loadSoulMd().then(lines => { S.oathLines = lines; });
|
|
}
|