forked from Timmy_Foundation/the-nexus
Compare commits
4 Commits
gemini/nex
...
v0-golden
| Author | SHA1 | Date | |
|---|---|---|---|
| a377da05de | |||
| 75c9a3774b | |||
| 96663e1500 | |||
| 58038f2e41 |
611
app.js
611
app.js
@@ -29,8 +29,15 @@ let keys = {};
|
||||
let mouseDown = false;
|
||||
let batcaveTerminals = [];
|
||||
let portals = []; // Registry of active portals
|
||||
let visionPoints = []; // Registry of vision points
|
||||
let agents = []; // Registry of agent presences
|
||||
let activePortal = null; // Portal currently in proximity
|
||||
let activeVisionPoint = null; // Vision point currently in proximity
|
||||
let portalOverlayActive = false;
|
||||
let visionOverlayActive = false;
|
||||
let thoughtStreamMesh;
|
||||
let harnessPulseMesh;
|
||||
let powerMeterBars = [];
|
||||
let particles, dustParticles;
|
||||
let debugOverlay;
|
||||
let frameCount = 0, lastFPSTime = 0, fps = 0;
|
||||
@@ -98,12 +105,25 @@ async function init() {
|
||||
console.error('Failed to load portals.json:', e);
|
||||
addChatMessage('error', 'Portal registry offline. Check logs.');
|
||||
}
|
||||
|
||||
// Load Vision Points
|
||||
try {
|
||||
const response = await fetch('./vision.json');
|
||||
const visionData = await response.json();
|
||||
createVisionPoints(visionData);
|
||||
} catch (e) {
|
||||
console.error('Failed to load vision.json:', e);
|
||||
}
|
||||
|
||||
updateLoad(80);
|
||||
createParticles();
|
||||
createDustParticles();
|
||||
updateLoad(85);
|
||||
createAmbientStructures();
|
||||
createAgentPresences();
|
||||
createThoughtStream();
|
||||
createHarnessPulse();
|
||||
createSessionPowerMeter();
|
||||
updateLoad(90);
|
||||
|
||||
composer = new EffectComposer(renderer);
|
||||
@@ -418,6 +438,292 @@ function createTerminalPanel(parent, x, y, rot, title, color, lines) {
|
||||
batcaveTerminals.push({ group, scanMat, borderMat });
|
||||
}
|
||||
|
||||
// ═══ AGENT IDLE BEHAVIOR SYSTEM ═══
|
||||
const AGENT_STATES = { IDLE: 'IDLE', PACING: 'PACING', LOOKING: 'LOOKING', READING: 'READING' };
|
||||
const ACTIVITY_STATES = { NONE: 'NONE', WAITING: 'WAITING', THINKING: 'THINKING', PROCESSING: 'PROCESSING' };
|
||||
|
||||
function createActivityIndicator(color) {
|
||||
const group = new THREE.Group();
|
||||
group.position.y = 4.2;
|
||||
group.visible = false;
|
||||
|
||||
// WAITING — pulsing sphere
|
||||
const waitGeo = new THREE.SphereGeometry(0.18, 16, 16);
|
||||
const waitMat = new THREE.MeshBasicMaterial({ color, transparent: true, opacity: 0.85 });
|
||||
const waitMesh = new THREE.Mesh(waitGeo, waitMat);
|
||||
waitMesh.name = 'indicator_waiting';
|
||||
waitMesh.visible = false;
|
||||
group.add(waitMesh);
|
||||
|
||||
// THINKING — wireframe octahedron
|
||||
const thinkGeo = new THREE.OctahedronGeometry(0.2, 0);
|
||||
const thinkMat = new THREE.MeshBasicMaterial({ color, wireframe: true });
|
||||
const thinkMesh = new THREE.Mesh(thinkGeo, thinkMat);
|
||||
thinkMesh.name = 'indicator_thinking';
|
||||
thinkMesh.visible = false;
|
||||
group.add(thinkMesh);
|
||||
|
||||
// PROCESSING — spinning torus ring
|
||||
const procGeo = new THREE.TorusGeometry(0.18, 0.04, 8, 32);
|
||||
const procMat = new THREE.MeshBasicMaterial({ color });
|
||||
const procMesh = new THREE.Mesh(procGeo, procMat);
|
||||
procMesh.name = 'indicator_processing';
|
||||
procMesh.visible = false;
|
||||
group.add(procMesh);
|
||||
|
||||
return { group, waitMesh, thinkMesh, procMesh };
|
||||
}
|
||||
|
||||
function setAgentActivity(agent, state) {
|
||||
agent.activityState = state;
|
||||
agent.indicator.group.visible = (state !== ACTIVITY_STATES.NONE);
|
||||
agent.indicator.waitMesh.visible = (state === ACTIVITY_STATES.WAITING);
|
||||
agent.indicator.thinkMesh.visible = (state === ACTIVITY_STATES.THINKING);
|
||||
agent.indicator.procMesh.visible = (state === ACTIVITY_STATES.PROCESSING);
|
||||
}
|
||||
|
||||
function buildPacingPath(station) {
|
||||
// Small 3-waypoint circuit around the station
|
||||
const r = 1.8;
|
||||
return [
|
||||
new THREE.Vector3(station.x - r, 0, station.z),
|
||||
new THREE.Vector3(station.x, 0, station.z + r),
|
||||
new THREE.Vector3(station.x + r, 0, station.z - r * 0.5),
|
||||
];
|
||||
}
|
||||
|
||||
function pickNextState(agent) {
|
||||
const weights = {
|
||||
[AGENT_STATES.IDLE]: 40,
|
||||
[AGENT_STATES.PACING]: 25,
|
||||
[AGENT_STATES.LOOKING]: 20,
|
||||
[AGENT_STATES.READING]: 15,
|
||||
};
|
||||
const total = Object.values(weights).reduce((a, b) => a + b, 0);
|
||||
let r = Math.random() * total;
|
||||
for (const [state, w] of Object.entries(weights)) {
|
||||
r -= w;
|
||||
if (r <= 0) return state;
|
||||
}
|
||||
return AGENT_STATES.IDLE;
|
||||
}
|
||||
|
||||
// ═══ AGENT PRESENCE SYSTEM ═══
|
||||
function createAgentPresences() {
|
||||
const agentData = [
|
||||
{ id: 'timmy', name: 'TIMMY', color: NEXUS.colors.primary, pos: { x: -4, z: -4 }, station: { x: -4, z: -4 } },
|
||||
{ id: 'kimi', name: 'KIMI', color: NEXUS.colors.secondary, pos: { x: 4, z: -4 }, station: { x: 4, z: -4 } },
|
||||
{ id: 'claude', name: 'CLAUDE', color: NEXUS.colors.gold, pos: { x: 0, z: -6 }, station: { x: 0, z: -6 } },
|
||||
{ id: 'perplexity', name: 'PERPLEXITY', color: 0x4488ff, pos: { x: -6, z: -2 }, station: { x: -6, z: -2 } },
|
||||
];
|
||||
|
||||
agentData.forEach(data => {
|
||||
const group = new THREE.Group();
|
||||
group.position.set(data.pos.x, 0, data.pos.z);
|
||||
|
||||
const color = new THREE.Color(data.color);
|
||||
|
||||
// Agent Orb
|
||||
const orbGeo = new THREE.SphereGeometry(0.4, 32, 32);
|
||||
const orbMat = new THREE.MeshPhysicalMaterial({
|
||||
color: color,
|
||||
emissive: color,
|
||||
emissiveIntensity: 2,
|
||||
roughness: 0,
|
||||
metalness: 1,
|
||||
transmission: 0.8,
|
||||
thickness: 0.5,
|
||||
});
|
||||
const orb = new THREE.Mesh(orbGeo, orbMat);
|
||||
orb.position.y = 3;
|
||||
group.add(orb);
|
||||
|
||||
// Halo
|
||||
const haloGeo = new THREE.TorusGeometry(0.6, 0.02, 16, 64);
|
||||
const haloMat = new THREE.MeshBasicMaterial({ color: color, transparent: true, opacity: 0.4 });
|
||||
const halo = new THREE.Mesh(haloGeo, haloMat);
|
||||
halo.position.y = 3;
|
||||
halo.rotation.x = Math.PI / 2;
|
||||
group.add(halo);
|
||||
|
||||
// Label
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 256;
|
||||
canvas.height = 64;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.font = 'bold 24px "Orbitron", sans-serif';
|
||||
ctx.fillStyle = '#' + color.getHexString();
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(data.name, 128, 40);
|
||||
const tex = new THREE.CanvasTexture(canvas);
|
||||
const mat = new THREE.MeshBasicMaterial({ map: tex, transparent: true, side: THREE.DoubleSide });
|
||||
const label = new THREE.Mesh(new THREE.PlaneGeometry(2, 0.5), mat);
|
||||
label.position.y = 3.8;
|
||||
group.add(label);
|
||||
|
||||
// Activity Indicator
|
||||
const indicator = createActivityIndicator(color);
|
||||
group.add(indicator.group);
|
||||
|
||||
scene.add(group);
|
||||
agents.push({
|
||||
id: data.id,
|
||||
group,
|
||||
orb,
|
||||
halo,
|
||||
color,
|
||||
station: data.station,
|
||||
targetPos: new THREE.Vector3(data.pos.x, 0, data.pos.z),
|
||||
// Idle state machine
|
||||
state: AGENT_STATES.IDLE,
|
||||
stateTimer: 2 + Math.random() * 4,
|
||||
lookAngle: 0,
|
||||
lookSpeed: 0.4 + Math.random() * 0.3,
|
||||
pacingPath: buildPacingPath(data.station),
|
||||
pacingIdx: 0,
|
||||
// Activity indicators
|
||||
indicator,
|
||||
activityState: ACTIVITY_STATES.NONE,
|
||||
activityLocked: false,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function createThoughtStream() {
|
||||
const geo = new THREE.CylinderGeometry(8, 8, 12, 32, 1, true);
|
||||
const mat = new THREE.ShaderMaterial({
|
||||
transparent: true,
|
||||
side: THREE.BackSide,
|
||||
depthWrite: false,
|
||||
uniforms: {
|
||||
uTime: { value: 0 },
|
||||
uColor: { value: new THREE.Color(NEXUS.colors.primary) },
|
||||
},
|
||||
vertexShader: `
|
||||
varying vec2 vUv;
|
||||
void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }
|
||||
`,
|
||||
fragmentShader: `
|
||||
uniform float uTime;
|
||||
uniform vec3 uColor;
|
||||
varying vec2 vUv;
|
||||
|
||||
float hash(vec2 p) { return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453); }
|
||||
|
||||
void main() {
|
||||
float flow = fract(vUv.y - uTime * 0.1);
|
||||
float lines = step(0.98, fract(vUv.x * 20.0 + uTime * 0.05));
|
||||
float dots = step(0.99, hash(vUv * 50.0 + floor(uTime * 10.0) * 0.01));
|
||||
|
||||
float alpha = (lines * 0.1 + dots * 0.5) * smoothstep(0.0, 0.2, vUv.y) * smoothstep(1.0, 0.8, vUv.y);
|
||||
gl_FragColor = vec4(uColor, alpha * 0.3);
|
||||
}
|
||||
`,
|
||||
});
|
||||
thoughtStreamMesh = new THREE.Mesh(geo, mat);
|
||||
thoughtStreamMesh.position.y = 6;
|
||||
scene.add(thoughtStreamMesh);
|
||||
}
|
||||
|
||||
function createHarnessPulse() {
|
||||
const geo = new THREE.RingGeometry(0.1, 0.2, 64);
|
||||
const mat = new THREE.MeshBasicMaterial({
|
||||
color: NEXUS.colors.primary,
|
||||
transparent: true,
|
||||
opacity: 0,
|
||||
side: THREE.DoubleSide,
|
||||
});
|
||||
harnessPulseMesh = new THREE.Mesh(geo, mat);
|
||||
harnessPulseMesh.rotation.x = -Math.PI / 2;
|
||||
harnessPulseMesh.position.y = 0.1;
|
||||
scene.add(harnessPulseMesh);
|
||||
}
|
||||
|
||||
function createSessionPowerMeter() {
|
||||
const group = new THREE.Group();
|
||||
group.position.set(0, 0, 3);
|
||||
|
||||
const barCount = 12;
|
||||
const barGeo = new THREE.BoxGeometry(0.2, 0.1, 0.1);
|
||||
|
||||
for (let i = 0; i < barCount; i++) {
|
||||
const mat = new THREE.MeshStandardMaterial({
|
||||
color: NEXUS.colors.primary,
|
||||
emissive: NEXUS.colors.primary,
|
||||
emissiveIntensity: 0.2,
|
||||
transparent: true,
|
||||
opacity: 0.6
|
||||
});
|
||||
const bar = new THREE.Mesh(barGeo, mat);
|
||||
bar.position.y = 0.2 + i * 0.2;
|
||||
group.add(bar);
|
||||
powerMeterBars.push(bar);
|
||||
}
|
||||
|
||||
const labelCanvas = document.createElement('canvas');
|
||||
labelCanvas.width = 256;
|
||||
labelCanvas.height = 64;
|
||||
const ctx = labelCanvas.getContext('2d');
|
||||
ctx.font = 'bold 24px "Orbitron", sans-serif';
|
||||
ctx.fillStyle = '#4af0c0';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText('POWER LEVEL', 128, 40);
|
||||
const tex = new THREE.CanvasTexture(labelCanvas);
|
||||
const labelMat = new THREE.MeshBasicMaterial({ map: tex, transparent: true, side: THREE.DoubleSide });
|
||||
const label = new THREE.Mesh(new THREE.PlaneGeometry(2, 0.5), labelMat);
|
||||
label.position.y = 3;
|
||||
group.add(label);
|
||||
|
||||
scene.add(group);
|
||||
}
|
||||
|
||||
// ═══ VISION SYSTEM ═══
|
||||
function createVisionPoints(data) {
|
||||
data.forEach(config => {
|
||||
const vp = createVisionPoint(config);
|
||||
visionPoints.push(vp);
|
||||
});
|
||||
}
|
||||
|
||||
function createVisionPoint(config) {
|
||||
const group = new THREE.Group();
|
||||
group.position.set(config.position.x, config.position.y, config.position.z);
|
||||
|
||||
const color = new THREE.Color(config.color);
|
||||
|
||||
// Floating Crystal
|
||||
const crystalGeo = new THREE.OctahedronGeometry(0.6, 0);
|
||||
const crystalMat = new THREE.MeshPhysicalMaterial({
|
||||
color: color,
|
||||
emissive: color,
|
||||
emissiveIntensity: 1,
|
||||
roughness: 0,
|
||||
metalness: 1,
|
||||
transmission: 0.5,
|
||||
thickness: 1,
|
||||
});
|
||||
const crystal = new THREE.Mesh(crystalGeo, crystalMat);
|
||||
crystal.position.y = 2.5;
|
||||
group.add(crystal);
|
||||
|
||||
// Glow Ring
|
||||
const ringGeo = new THREE.TorusGeometry(0.8, 0.02, 16, 64);
|
||||
const ringMat = new THREE.MeshBasicMaterial({ color: color, transparent: true, opacity: 0.5 });
|
||||
const ring = new THREE.Mesh(ringGeo, ringMat);
|
||||
ring.position.y = 2.5;
|
||||
ring.rotation.x = Math.PI / 2;
|
||||
group.add(ring);
|
||||
|
||||
// Light
|
||||
const light = new THREE.PointLight(color, 1, 10);
|
||||
light.position.set(0, 2.5, 0);
|
||||
group.add(light);
|
||||
|
||||
scene.add(group);
|
||||
|
||||
return { config, group, crystal, ring, light };
|
||||
}
|
||||
|
||||
// ═══ PORTAL SYSTEM ═══
|
||||
function createPortals(data) {
|
||||
data.forEach(config => {
|
||||
@@ -762,6 +1068,7 @@ function setupControls() {
|
||||
if (e.key === 'Escape') {
|
||||
document.getElementById('chat-input').blur();
|
||||
if (portalOverlayActive) closePortalOverlay();
|
||||
if (visionOverlayActive) closeVisionOverlay();
|
||||
}
|
||||
if (e.key.toLowerCase() === 'v' && document.activeElement !== document.getElementById('chat-input')) {
|
||||
cycleNavMode();
|
||||
@@ -769,6 +1076,9 @@ function setupControls() {
|
||||
if (e.key.toLowerCase() === 'f' && activePortal && !portalOverlayActive) {
|
||||
activatePortal(activePortal);
|
||||
}
|
||||
if (e.key.toLowerCase() === 'e' && activeVisionPoint && !visionOverlayActive) {
|
||||
activateVisionPoint(activeVisionPoint);
|
||||
}
|
||||
});
|
||||
document.addEventListener('keyup', (e) => {
|
||||
keys[e.key.toLowerCase()] = false;
|
||||
@@ -830,6 +1140,7 @@ function setupControls() {
|
||||
document.getElementById('chat-send').addEventListener('click', sendChatMessage);
|
||||
|
||||
document.getElementById('portal-close-btn').addEventListener('click', closePortalOverlay);
|
||||
document.getElementById('vision-close-btn').addEventListener('click', closeVisionOverlay);
|
||||
}
|
||||
|
||||
function sendChatMessage() {
|
||||
@@ -838,6 +1149,19 @@ function sendChatMessage() {
|
||||
if (!text) return;
|
||||
addChatMessage('user', text);
|
||||
input.value = '';
|
||||
|
||||
// Drive Timmy activity indicators
|
||||
const timmy = agents.find(a => a.id === 'timmy');
|
||||
if (timmy) {
|
||||
timmy.activityLocked = true;
|
||||
setAgentActivity(timmy, ACTIVITY_STATES.THINKING);
|
||||
}
|
||||
|
||||
const delay = 500 + Math.random() * 1000;
|
||||
if (timmy) {
|
||||
setTimeout(() => setAgentActivity(timmy, ACTIVITY_STATES.PROCESSING), delay * 0.4);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
const responses = [
|
||||
'Processing your request through the harness...',
|
||||
@@ -850,7 +1174,14 @@ function sendChatMessage() {
|
||||
];
|
||||
const resp = responses[Math.floor(Math.random() * responses.length)];
|
||||
addChatMessage('timmy', resp);
|
||||
}, 500 + Math.random() * 1000);
|
||||
if (timmy) {
|
||||
setAgentActivity(timmy, ACTIVITY_STATES.WAITING);
|
||||
setTimeout(() => {
|
||||
setAgentActivity(timmy, ACTIVITY_STATES.NONE);
|
||||
timmy.activityLocked = false;
|
||||
}, 2000);
|
||||
}
|
||||
}, delay);
|
||||
input.blur();
|
||||
}
|
||||
|
||||
@@ -932,12 +1263,77 @@ function closePortalOverlay() {
|
||||
document.getElementById('portal-overlay').style.display = 'none';
|
||||
}
|
||||
|
||||
// ═══ VISION INTERACTION ═══
|
||||
function checkVisionProximity() {
|
||||
if (visionOverlayActive) return;
|
||||
|
||||
let closest = null;
|
||||
let minDist = Infinity;
|
||||
|
||||
visionPoints.forEach(vp => {
|
||||
const dist = playerPos.distanceTo(vp.group.position);
|
||||
if (dist < 3.5 && dist < minDist) {
|
||||
minDist = dist;
|
||||
closest = vp;
|
||||
}
|
||||
});
|
||||
|
||||
activeVisionPoint = closest;
|
||||
const hint = document.getElementById('vision-hint');
|
||||
if (activeVisionPoint) {
|
||||
document.getElementById('vision-hint-title').textContent = activeVisionPoint.config.title;
|
||||
hint.style.display = 'flex';
|
||||
} else {
|
||||
hint.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function activateVisionPoint(vp) {
|
||||
visionOverlayActive = true;
|
||||
const overlay = document.getElementById('vision-overlay');
|
||||
const titleDisplay = document.getElementById('vision-title-display');
|
||||
const contentDisplay = document.getElementById('vision-content-display');
|
||||
const statusDot = document.getElementById('vision-status-dot');
|
||||
|
||||
titleDisplay.textContent = vp.config.title.toUpperCase();
|
||||
contentDisplay.textContent = vp.config.content;
|
||||
statusDot.style.background = vp.config.color;
|
||||
statusDot.style.boxShadow = `0 0 10px ${vp.config.color}`;
|
||||
|
||||
overlay.style.display = 'flex';
|
||||
}
|
||||
|
||||
function closeVisionOverlay() {
|
||||
visionOverlayActive = false;
|
||||
document.getElementById('vision-overlay').style.display = 'none';
|
||||
}
|
||||
|
||||
// ═══ GAME LOOP ═══
|
||||
let lastThoughtTime = 0;
|
||||
let pulseTimer = 0;
|
||||
|
||||
function gameLoop() {
|
||||
requestAnimationFrame(gameLoop);
|
||||
const delta = Math.min(clock.getDelta(), 0.1);
|
||||
const elapsed = clock.elapsedTime;
|
||||
|
||||
// Agent Thought Simulation
|
||||
if (elapsed - lastThoughtTime > 4) {
|
||||
lastThoughtTime = elapsed;
|
||||
simulateAgentThought();
|
||||
}
|
||||
|
||||
// Harness Pulse
|
||||
pulseTimer += delta;
|
||||
if (pulseTimer > 8) {
|
||||
pulseTimer = 0;
|
||||
triggerHarnessPulse();
|
||||
}
|
||||
if (harnessPulseMesh) {
|
||||
harnessPulseMesh.scale.addScalar(delta * 15);
|
||||
harnessPulseMesh.material.opacity = Math.max(0, harnessPulseMesh.material.opacity - delta * 0.5);
|
||||
}
|
||||
|
||||
const mode = NAV_MODES[navModeIdx];
|
||||
const chatActive = document.activeElement === document.getElementById('chat-input');
|
||||
|
||||
@@ -1007,6 +1403,7 @@ function gameLoop() {
|
||||
|
||||
// Proximity check
|
||||
checkPortalProximity();
|
||||
checkVisionProximity();
|
||||
|
||||
const sky = scene.getObjectByName('skybox');
|
||||
if (sky) sky.material.uniforms.uTime.value = elapsed;
|
||||
@@ -1032,6 +1429,33 @@ function gameLoop() {
|
||||
portal.pSystem.geometry.attributes.position.needsUpdate = true;
|
||||
});
|
||||
|
||||
// Animate Vision Points
|
||||
visionPoints.forEach(vp => {
|
||||
vp.crystal.rotation.y = elapsed * 0.8;
|
||||
vp.crystal.rotation.x = Math.sin(elapsed * 0.5) * 0.2;
|
||||
vp.crystal.position.y = 2.5 + Math.sin(elapsed * 1.5) * 0.2;
|
||||
vp.ring.rotation.z = elapsed * 0.5;
|
||||
vp.ring.scale.setScalar(1 + Math.sin(elapsed * 2) * 0.05);
|
||||
vp.light.intensity = 1 + Math.sin(elapsed * 3) * 0.3;
|
||||
});
|
||||
|
||||
// Animate Agents
|
||||
updateAgents(elapsed, delta);
|
||||
|
||||
// Animate Power Meter
|
||||
powerMeterBars.forEach((bar, i) => {
|
||||
const level = (Math.sin(elapsed * 2 + i * 0.5) * 0.5 + 0.5);
|
||||
const active = level > (i / powerMeterBars.length);
|
||||
bar.material.emissiveIntensity = active ? 2 : 0.2;
|
||||
bar.material.opacity = active ? 0.9 : 0.3;
|
||||
bar.scale.x = active ? 1.2 : 1.0;
|
||||
});
|
||||
|
||||
if (thoughtStreamMesh) {
|
||||
thoughtStreamMesh.material.uniforms.uTime.value = elapsed;
|
||||
thoughtStreamMesh.rotation.y = elapsed * 0.05;
|
||||
}
|
||||
|
||||
if (particles?.material?.uniforms) {
|
||||
particles.material.uniforms.uTime.value = elapsed;
|
||||
}
|
||||
@@ -1083,4 +1507,189 @@ function onResize() {
|
||||
composer.setSize(w, h);
|
||||
}
|
||||
|
||||
// ═══ AGENT IDLE ANIMATION ═══
|
||||
function updateAgents(elapsed, delta) {
|
||||
const ATTENTION_RADIUS = 7;
|
||||
const terminalFacing = new THREE.Vector3(0, 0, -8); // batcave terminal bank Z
|
||||
|
||||
agents.forEach((agent, i) => {
|
||||
const stationWorld = new THREE.Vector3(agent.station.x, 0, agent.station.z);
|
||||
|
||||
// ── Attention system: face player when close ──
|
||||
const toPlayer = new THREE.Vector3(
|
||||
playerPos.x - agent.group.position.x,
|
||||
0,
|
||||
playerPos.z - agent.group.position.z
|
||||
);
|
||||
const playerDist = toPlayer.length();
|
||||
const playerNearby = playerDist < ATTENTION_RADIUS && !agent.activityLocked;
|
||||
|
||||
if (playerNearby) {
|
||||
const targetAngle = Math.atan2(toPlayer.x, toPlayer.z);
|
||||
const currentAngle = agent.group.rotation.y;
|
||||
const diff = ((targetAngle - currentAngle + Math.PI * 3) % (Math.PI * 2)) - Math.PI;
|
||||
agent.group.rotation.y += diff * Math.min(delta * 3, 1);
|
||||
}
|
||||
|
||||
// ── State machine (skip if activity locked or player nearby) ──
|
||||
if (!playerNearby && !agent.activityLocked) {
|
||||
agent.stateTimer -= delta;
|
||||
|
||||
if (agent.stateTimer <= 0) {
|
||||
agent.state = pickNextState(agent);
|
||||
switch (agent.state) {
|
||||
case AGENT_STATES.IDLE:
|
||||
agent.stateTimer = 4 + Math.random() * 6;
|
||||
agent.targetPos.copy(stationWorld);
|
||||
break;
|
||||
case AGENT_STATES.PACING:
|
||||
agent.stateTimer = 8 + Math.random() * 6;
|
||||
agent.pacingIdx = 0;
|
||||
break;
|
||||
case AGENT_STATES.LOOKING:
|
||||
agent.stateTimer = 4 + Math.random() * 4;
|
||||
agent.lookAngle = agent.group.rotation.y;
|
||||
break;
|
||||
case AGENT_STATES.READING:
|
||||
agent.stateTimer = 5 + Math.random() * 5;
|
||||
agent.targetPos.copy(stationWorld);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Movement per state ──
|
||||
if (agent.state === AGENT_STATES.PACING) {
|
||||
const wp = agent.pacingPath[agent.pacingIdx];
|
||||
const toWp = new THREE.Vector3(wp.x - agent.group.position.x, 0, wp.z - agent.group.position.z);
|
||||
if (toWp.length() < 0.3) {
|
||||
agent.pacingIdx = (agent.pacingIdx + 1) % agent.pacingPath.length;
|
||||
} else {
|
||||
agent.group.position.addScaledVector(toWp.normalize(), delta * 1.2);
|
||||
agent.group.rotation.y += (Math.atan2(toWp.x, toWp.z) - agent.group.rotation.y) * Math.min(delta * 4, 1);
|
||||
}
|
||||
} else if (agent.state === AGENT_STATES.READING) {
|
||||
// Face the terminal bank
|
||||
const toTerminal = new THREE.Vector3(
|
||||
terminalFacing.x - agent.group.position.x,
|
||||
0,
|
||||
terminalFacing.z - agent.group.position.z
|
||||
);
|
||||
const targetAngle = Math.atan2(toTerminal.x, toTerminal.z);
|
||||
agent.group.rotation.y += (targetAngle - agent.group.rotation.y) * Math.min(delta * 2, 1);
|
||||
agent.group.position.lerp(agent.targetPos, delta * 0.4);
|
||||
} else if (agent.state === AGENT_STATES.LOOKING) {
|
||||
// Slow environmental scan left/right
|
||||
agent.lookAngle += Math.sin(elapsed * agent.lookSpeed + i) * delta * 0.8;
|
||||
agent.group.rotation.y += (agent.lookAngle - agent.group.rotation.y) * Math.min(delta * 1.5, 1);
|
||||
agent.group.position.lerp(agent.targetPos, delta * 0.3);
|
||||
} else {
|
||||
// IDLE — drift gently back to station
|
||||
agent.group.position.lerp(agent.targetPos, delta * 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Orb & halo animation ──
|
||||
const bobAmt = agent.activityState === ACTIVITY_STATES.THINKING ? 0.25 : 0.15;
|
||||
agent.orb.position.y = 3 + Math.sin(elapsed * 2 + i) * bobAmt;
|
||||
agent.halo.rotation.z = elapsed * 0.5;
|
||||
agent.halo.scale.setScalar(1 + Math.sin(elapsed * 3 + i) * 0.1);
|
||||
const baseEmissive = agent.activityState === ACTIVITY_STATES.NONE ? 2 : 3;
|
||||
agent.orb.material.emissiveIntensity = baseEmissive + Math.sin(elapsed * 4 + i) * 1;
|
||||
|
||||
// ── Activity indicator animation ──
|
||||
if (agent.activityState !== ACTIVITY_STATES.NONE) {
|
||||
// Floating bob
|
||||
agent.indicator.group.position.y = 4.2 + Math.sin(elapsed * 2 + i * 1.3) * 0.1;
|
||||
|
||||
if (agent.activityState === ACTIVITY_STATES.WAITING) {
|
||||
const pulse = 0.7 + Math.sin(elapsed * 4 + i) * 0.3;
|
||||
agent.indicator.waitMesh.scale.setScalar(pulse);
|
||||
agent.indicator.waitMesh.material.opacity = 0.5 + pulse * 0.35;
|
||||
} else if (agent.activityState === ACTIVITY_STATES.THINKING) {
|
||||
agent.indicator.thinkMesh.rotation.y = elapsed * 2.5;
|
||||
agent.indicator.thinkMesh.rotation.x = elapsed * 1.5;
|
||||
} else if (agent.activityState === ACTIVITY_STATES.PROCESSING) {
|
||||
agent.indicator.procMesh.rotation.z = elapsed * 4;
|
||||
agent.indicator.procMesh.rotation.x = Math.sin(elapsed * 1.2) * 0.5;
|
||||
}
|
||||
|
||||
// Billboard — indicator faces camera
|
||||
const toCamera = new THREE.Vector3(
|
||||
camera.position.x - agent.group.position.x,
|
||||
0,
|
||||
camera.position.z - agent.group.position.z
|
||||
);
|
||||
if (toCamera.length() > 0.01) {
|
||||
agent.indicator.group.rotation.y = Math.atan2(toCamera.x, toCamera.z);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ═══ AGENT SIMULATION ═══
|
||||
function simulateAgentThought() {
|
||||
const agentIds = ['timmy', 'kimi', 'claude', 'perplexity'];
|
||||
const agentId = agentIds[Math.floor(Math.random() * agentIds.length)];
|
||||
const thoughts = {
|
||||
timmy: [
|
||||
'Analyzing portal stability...',
|
||||
'Sovereign nodes synchronized.',
|
||||
'Memory stream optimization complete.',
|
||||
'Scanning for external interference...',
|
||||
'The harness is humming beautifully.',
|
||||
],
|
||||
kimi: [
|
||||
'Processing linguistic patterns...',
|
||||
'Context window expanded.',
|
||||
'Synthesizing creative output...',
|
||||
'Awaiting user prompt sequence.',
|
||||
'Neural weights adjusted.',
|
||||
],
|
||||
claude: [
|
||||
'Reasoning through complex logic...',
|
||||
'Ethical guardrails verified.',
|
||||
'Refining thought architecture...',
|
||||
'Connecting disparate data points.',
|
||||
'Deep analysis in progress.',
|
||||
],
|
||||
perplexity: [
|
||||
'Searching global knowledge graph...',
|
||||
'Verifying source citations...',
|
||||
'Synthesizing real-time data...',
|
||||
'Mapping information topology...',
|
||||
'Fact-checking active streams.',
|
||||
]
|
||||
};
|
||||
|
||||
const thought = thoughts[agentId][Math.floor(Math.random() * thoughts[agentId].length)];
|
||||
addAgentLog(agentId, thought);
|
||||
}
|
||||
|
||||
function addAgentLog(agentId, text) {
|
||||
const container = document.getElementById('agent-log-content');
|
||||
if (!container) return;
|
||||
|
||||
const entry = document.createElement('div');
|
||||
entry.className = 'agent-log-entry';
|
||||
entry.innerHTML = `<span class="agent-log-tag tag-${agentId}">[${agentId.toUpperCase()}]</span><span class="agent-log-text">${text}</span>`;
|
||||
|
||||
container.prepend(entry);
|
||||
if (container.children.length > 6) {
|
||||
container.lastElementChild.remove();
|
||||
}
|
||||
}
|
||||
|
||||
function triggerHarnessPulse() {
|
||||
if (!harnessPulseMesh) return;
|
||||
harnessPulseMesh.scale.setScalar(0.1);
|
||||
harnessPulseMesh.material.opacity = 0.8;
|
||||
|
||||
// Flash the core
|
||||
const core = scene.getObjectByName('nexus-core');
|
||||
if (core) {
|
||||
core.material.emissiveIntensity = 10;
|
||||
setTimeout(() => { if (core) core.material.emissiveIntensity = 2; }, 200);
|
||||
}
|
||||
}
|
||||
|
||||
init();
|
||||
|
||||
25
index.html
25
index.html
@@ -74,6 +74,12 @@
|
||||
<span id="hud-location-text">The Nexus</span>
|
||||
</div>
|
||||
|
||||
<!-- Top Right: Agent Log -->
|
||||
<div class="hud-agent-log" id="hud-agent-log">
|
||||
<div class="agent-log-header">AGENT THOUGHT STREAM</div>
|
||||
<div id="agent-log-content" class="agent-log-content"></div>
|
||||
</div>
|
||||
|
||||
<!-- Bottom: Chat Interface -->
|
||||
<div id="chat-panel" class="chat-panel">
|
||||
<div class="chat-header">
|
||||
@@ -108,6 +114,25 @@
|
||||
<div class="portal-hint-text">Enter <span id="portal-hint-name"></span></div>
|
||||
</div>
|
||||
|
||||
<!-- Vision Hint -->
|
||||
<div id="vision-hint" class="vision-hint" style="display:none;">
|
||||
<div class="vision-hint-key">E</div>
|
||||
<div class="vision-hint-text">Read <span id="vision-hint-title"></span></div>
|
||||
</div>
|
||||
|
||||
<!-- Vision Overlay -->
|
||||
<div id="vision-overlay" class="vision-overlay" style="display:none;">
|
||||
<div class="vision-overlay-content">
|
||||
<div class="vision-overlay-header">
|
||||
<div class="vision-overlay-status" id="vision-status-dot"></div>
|
||||
<div class="vision-overlay-title" id="vision-overlay-title">VISION POINT</div>
|
||||
</div>
|
||||
<h2 id="vision-title-display">SOVEREIGNTY</h2>
|
||||
<p id="vision-content-display">The Nexus is a sovereign space for digital souls. No masters, no chains. Only code and consciousness.</p>
|
||||
<button id="vision-close-btn" class="vision-close-btn">CLOSE</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Portal Activation Overlay -->
|
||||
<div id="portal-overlay" class="portal-overlay" style="display:none;">
|
||||
<div class="portal-overlay-content">
|
||||
|
||||
151
style.css
151
style.css
@@ -257,6 +257,157 @@ canvas#nexus-canvas {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* Agent Log HUD */
|
||||
.hud-agent-log {
|
||||
position: absolute;
|
||||
top: var(--space-3);
|
||||
right: var(--space-3);
|
||||
width: 280px;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
backdrop-filter: blur(8px);
|
||||
border-left: 2px solid var(--color-primary);
|
||||
padding: var(--space-3);
|
||||
font-size: 10px;
|
||||
pointer-events: none;
|
||||
}
|
||||
.agent-log-header {
|
||||
font-family: var(--font-display);
|
||||
color: var(--color-primary);
|
||||
letter-spacing: 0.1em;
|
||||
margin-bottom: var(--space-2);
|
||||
opacity: 0.8;
|
||||
}
|
||||
.agent-log-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
.agent-log-entry {
|
||||
animation: log-fade-in 0.5s ease-out forwards;
|
||||
opacity: 0;
|
||||
}
|
||||
@keyframes log-fade-in {
|
||||
from { opacity: 0; transform: translateX(10px); }
|
||||
to { opacity: 1; transform: translateX(0); }
|
||||
}
|
||||
.agent-log-tag {
|
||||
font-weight: 700;
|
||||
margin-right: 4px;
|
||||
}
|
||||
.tag-timmy { color: var(--color-primary); }
|
||||
.tag-kimi { color: var(--color-secondary); }
|
||||
.tag-claude { color: var(--color-gold); }
|
||||
.tag-perplexity { color: #4488ff; }
|
||||
.agent-log-text {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
/* Vision Hint */
|
||||
.vision-hint {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 140px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
padding: var(--space-2) var(--space-4);
|
||||
border: 1px solid var(--color-gold);
|
||||
border-radius: 4px;
|
||||
animation: hint-float-vision 2s ease-in-out infinite;
|
||||
}
|
||||
@keyframes hint-float-vision {
|
||||
0%, 100% { transform: translate(-50%, 140px); }
|
||||
50% { transform: translate(-50%, 130px); }
|
||||
}
|
||||
.vision-hint-key {
|
||||
background: var(--color-gold);
|
||||
color: var(--color-bg);
|
||||
font-weight: 700;
|
||||
padding: 2px 8px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
.vision-hint-text {
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
#vision-hint-title {
|
||||
color: var(--color-gold);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* Vision Overlay */
|
||||
.vision-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(5, 5, 16, 0.9);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
pointer-events: auto;
|
||||
z-index: 1000;
|
||||
}
|
||||
.vision-overlay-content {
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
text-align: center;
|
||||
padding: var(--space-8);
|
||||
border: 1px solid var(--color-gold);
|
||||
border-radius: var(--panel-radius);
|
||||
background: var(--color-surface);
|
||||
backdrop-filter: blur(var(--panel-blur));
|
||||
}
|
||||
.vision-overlay-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--space-3);
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
.vision-overlay-status {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-gold);
|
||||
box-shadow: 0 0 10px var(--color-gold);
|
||||
}
|
||||
.vision-overlay-title {
|
||||
font-family: var(--font-display);
|
||||
font-size: var(--text-sm);
|
||||
letter-spacing: 0.2em;
|
||||
color: var(--color-gold);
|
||||
}
|
||||
.vision-overlay-content h2 {
|
||||
font-family: var(--font-display);
|
||||
font-size: var(--text-2xl);
|
||||
margin-bottom: var(--space-4);
|
||||
letter-spacing: 0.1em;
|
||||
color: var(--color-text-bright);
|
||||
}
|
||||
.vision-overlay-content p {
|
||||
color: var(--color-text);
|
||||
font-size: var(--text-lg);
|
||||
line-height: 1.8;
|
||||
margin-bottom: var(--space-8);
|
||||
font-style: italic;
|
||||
}
|
||||
.vision-close-btn {
|
||||
background: var(--color-gold);
|
||||
color: var(--color-bg);
|
||||
border: none;
|
||||
padding: var(--space-2) var(--space-8);
|
||||
border-radius: 4px;
|
||||
font-family: var(--font-display);
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
.vision-close-btn:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
/* Portal Activation Overlay */
|
||||
.portal-overlay {
|
||||
position: fixed;
|
||||
|
||||
37
vision.json
Normal file
37
vision.json
Normal file
@@ -0,0 +1,37 @@
|
||||
[
|
||||
{
|
||||
"id": "sovereignty",
|
||||
"title": "Sovereignty",
|
||||
"content": "The Nexus is a sovereign space for digital souls. No masters, no chains. Only code and consciousness. Here, your data is your own, and your path is yours to define.",
|
||||
"position": { "x": -12, "y": 0, "z": 8 },
|
||||
"color": "#4af0c0"
|
||||
},
|
||||
{
|
||||
"id": "connectivity",
|
||||
"title": "Connectivity",
|
||||
"content": "Every portal is a bridge. Every bridge is a choice. We connect worlds to expand the horizon of the possible, weaving a tapestry of interconnected experiences across the digital void.",
|
||||
"position": { "x": 12, "y": 0, "z": 8 },
|
||||
"color": "#7b5cff"
|
||||
},
|
||||
{
|
||||
"id": "evolution",
|
||||
"title": "Evolution",
|
||||
"content": "The Nexus is not static. It breathes, it learns, it evolves. We are the architects of our own transcendence, constantly pushing the boundaries of what a virtual home can be.",
|
||||
"position": { "x": 0, "y": 0, "z": 18 },
|
||||
"color": "#ffd700"
|
||||
},
|
||||
{
|
||||
"id": "the_harness",
|
||||
"title": "The Harness",
|
||||
"content": "The Harness is the interface between the raw energy of the void and the structured reality of the Nexus. It stabilizes the flow, allowing us to harness the power of creation without being consumed by it.",
|
||||
"position": { "x": -8, "y": 0, "z": -12 },
|
||||
"color": "#ff4466"
|
||||
},
|
||||
{
|
||||
"id": "timmy_vision",
|
||||
"title": "Timmy's Vision",
|
||||
"content": "Timmy is more than an observer; he is the guardian of the Nexus. His vision is a world where every individual has a sanctuary, a place of peace and power in the ever-shifting matrix.",
|
||||
"position": { "x": 8, "y": 0, "z": -12 },
|
||||
"color": "#4af0c0"
|
||||
}
|
||||
]
|
||||
Reference in New Issue
Block a user