Files
the-nexus/modules/portals.js
Claude (Opus 4.6) 1c18fbf0d1
Some checks failed
Deploy Nexus / deploy (push) Failing after 3s
Staging Smoke Test / smoke-test (push) Successful in 6s
[claude] InstancedMesh for portal tori + merged tile edge geometry (#415) (#464)
Co-authored-by: Claude (Opus 4.6) <claude@hermes.local>
Co-committed-by: Claude (Opus 4.6) <claude@hermes.local>
2026-03-25 01:30:31 +00:00

91 lines
3.3 KiB
JavaScript

// === PORTALS ===
import * as THREE from 'three';
import { scene } from './scene-setup.js';
import { rebuildRuneRing, setPortalsRef } from './effects.js';
import { setPortalsRefAudio, startPortalHums } from './audio.js';
import { S } from './state.js';
export const portalGroup = new THREE.Group();
scene.add(portalGroup);
export let portals = [];
// Shared geometry and material for all portal tori — populated in createPortals()
const _portalGeo = new THREE.TorusGeometry(3.0, 0.2, 16, 100);
const _portalMat = new THREE.MeshBasicMaterial({
color: 0xffffff, // instance color provides per-portal tint
transparent: true,
opacity: 1.0, // online/offline brightness encoded into instance color
blending: THREE.AdditiveBlending,
side: THREE.DoubleSide,
depthWrite: false,
});
const _portalDummy = new THREE.Object3D();
const _portalColor = new THREE.Color();
/** @type {THREE.InstancedMesh|null} */
let _portalIM = null;
function createPortals() {
// One InstancedMesh for all portal tori — N portals = 1 draw call.
_portalIM = new THREE.InstancedMesh(_portalGeo, _portalMat, portals.length);
_portalIM.instanceMatrix.setUsage(THREE.DynamicDrawUsage);
_portalIM.userData.zoomLabel = 'Portal';
_portalIM.userData.portals = portals; // for instanceId look-up on click
portals.forEach((portal, i) => {
const isOnline = portal.status === 'online';
// Encode online/offline brightness into the instance color so we need
// only one shared material (AdditiveBlending: output = bg + color).
_portalColor.set(portal.color).convertSRGBToLinear()
.multiplyScalar(isOnline ? 0.7 : 0.15);
_portalIM.setColorAt(i, _portalColor);
_portalDummy.position.set(portal.position.x, portal.position.y + 0.5, portal.position.z);
_portalDummy.rotation.set(Math.PI / 2, portal.rotation.y || 0, 0);
_portalDummy.updateMatrix();
_portalIM.setMatrixAt(i, _portalDummy.matrix);
});
_portalIM.instanceColor.needsUpdate = true;
_portalIM.instanceMatrix.needsUpdate = true;
portalGroup.add(_portalIM);
}
/** Update per-instance colors after a portal health check. */
export function refreshPortalInstanceColors() {
if (!_portalIM) return;
portals.forEach((portal, i) => {
const brightness = portal.status === 'online' ? 0.7 : 0.15;
_portalColor.set(portal.color).convertSRGBToLinear().multiplyScalar(brightness);
_portalIM.setColorAt(i, _portalColor);
});
_portalIM.instanceColor.needsUpdate = true;
}
// rebuildGravityZones forward ref
let _rebuildGravityZonesFn = null;
export function setRebuildGravityZonesFn(fn) { _rebuildGravityZonesFn = fn; }
// runPortalHealthChecks forward ref
let _runPortalHealthChecksFn = null;
export function setRunPortalHealthChecksFn(fn) { _runPortalHealthChecksFn = fn; }
export async function loadPortals() {
try {
const res = await fetch('./portals.json');
if (!res.ok) throw new Error('Portals not found');
portals = await res.json();
console.log('Loaded portals:', portals);
setPortalsRef(portals);
setPortalsRefAudio(portals);
createPortals();
rebuildRuneRing();
if (_rebuildGravityZonesFn) _rebuildGravityZonesFn();
startPortalHums();
if (_runPortalHealthChecksFn) _runPortalHealthChecksFn();
} catch (error) {
console.error('Failed to load portals:', error);
}
}