Erste lauffähige Fassung: 3D-Raum, Echtzeit-Multiplayer (WebSocket), Python-IDE via Pyodide im Browser, eigener Arbeitsplatz mit Live-Code-Monitor. Modularer, self-hostbarer Aufbau (Three.js + Node/Express + ws), ohne CDN- Abhängigkeit zur Laufzeit. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
133 lines
5.2 KiB
JavaScript
133 lines
5.2 KiB
JavaScript
// main.js — Einstiegspunkt: verdrahtet Welt, Steuerung, Avatare, Netzwerk und UI
|
|
// und treibt die Render-Schleife. Hält selbst keine Logik-Details.
|
|
import * as THREE from 'three';
|
|
import { createWorld, createWorkstation } from './scene.js';
|
|
import { Controls } from './controls.js';
|
|
import { Avatars } from './avatars.js';
|
|
import { Net } from './net.js';
|
|
import { UI } from './ui.js';
|
|
import { IDE } from './ide.js';
|
|
import { CodeScreen } from './codescreen.js';
|
|
|
|
const SEND_HZ = 15; // Positions-Updates pro Sekunde
|
|
const SEND_DT = 1 / SEND_HZ;
|
|
|
|
const ui = new UI();
|
|
|
|
try {
|
|
ui.setDbg('renderer');
|
|
const renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: 'high-performance' });
|
|
renderer.setSize(innerWidth, innerHeight);
|
|
renderer.setPixelRatio(Math.min(devicePixelRatio, 1.5));
|
|
renderer.shadowMap.enabled = true;
|
|
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
|
document.getElementById('app').appendChild(renderer.domElement);
|
|
|
|
ui.setDbg('welt');
|
|
const { scene, camera, player, monMat } = createWorld();
|
|
|
|
// Eigener Arbeitsplatz: echter Monitor auf dem Tisch, zeigt den IDE-Code live in 3D.
|
|
const myScreen = new CodeScreen(1.4);
|
|
myScreen.setTitle('mein Monitor');
|
|
const station = createWorkstation(myScreen.mesh, 1.4);
|
|
station.group.position.set(0, 0, 2.0); // vor dem Spawn (Blickrichtung -z)
|
|
scene.add(station.group);
|
|
const seatPos = station.seat.clone().add(station.group.position); // Sitzplatz in Weltkoordinaten
|
|
|
|
ui.setDbg('steuerung');
|
|
const controls = new Controls(camera, player, renderer.domElement);
|
|
const avatars = new Avatars(scene);
|
|
|
|
ui.setDbg('netzwerk');
|
|
let myId = null;
|
|
const refreshCount = () => ui.setCount(avatars.count + 1);
|
|
const net = new Net({
|
|
onWelcome: (id, users) => { myId = id; users.forEach((u) => avatars.add(u)); refreshCount(); },
|
|
onJoin: (u) => { if (u.id !== myId) { avatars.add(u); refreshCount(); } },
|
|
onLeave: (id) => { avatars.remove(id); refreshCount(); },
|
|
onUpdate: (id, p, rot) => avatars.setTarget(id, p, rot),
|
|
onStatus: (id, label) => avatars.setStatus(id, label),
|
|
onHelp: (id, on) => avatars.setHelp(id, on),
|
|
onNet: (text, color) => ui.setNet(text, color),
|
|
});
|
|
|
|
ui.onStart((name, role) => { controls.lock(); net.connect(name, role, player.position); });
|
|
ui.onHelpToggle((on) => net.sendHelp(on));
|
|
|
|
// IDE: pausiert die Steuerung und gibt die Maus frei, solange sie offen ist.
|
|
ui.setDbg('ide');
|
|
const ide = new IDE();
|
|
ide.onOpen(() => { controls.setEnabled(false); if (document.exitPointerLock) document.exitPointerLock(); });
|
|
ide.onClose(() => { controls.setEnabled(true); });
|
|
ide.onChange((text) => myScreen.setCode(text)); // Tippen -> Monitor läuft live mit
|
|
myScreen.setCode(ide.getCode()); // Startcode gleich anzeigen
|
|
ui.onOpenIde(() => ide.open());
|
|
|
|
// An den Platz setzen: in Sitznähe erscheint ein Hinweis, [E] setzt hin + öffnet die IDE.
|
|
const sithint = document.getElementById('sithint');
|
|
const SIT_RADIUS = 1.8;
|
|
let nearSeat = false;
|
|
function sitDown() {
|
|
if (ide.isOpen) return;
|
|
player.position.copy(seatPos);
|
|
player.rotation.set(0, 0, 0);
|
|
camera.quaternion.identity(); // Blick geradeaus nach -z auf den Monitor
|
|
ide.open();
|
|
}
|
|
addEventListener('keydown', (e) => { if (e.code === 'KeyE' && nearSeat) sitDown(); });
|
|
|
|
addEventListener('resize', () => {
|
|
camera.aspect = innerWidth / innerHeight;
|
|
camera.updateProjectionMatrix();
|
|
renderer.setSize(innerWidth, innerHeight);
|
|
renderer.setPixelRatio(Math.min(devicePixelRatio, 1.5));
|
|
});
|
|
|
|
// ── Render-Schleife ──
|
|
const clock = new THREE.Clock();
|
|
let frames = 0, fpsAcc = 0, sendAcc = 0, loopBroken = false;
|
|
let last = { p: [0, 0, 0], rot: 0, anim: 'idle' };
|
|
const round2 = (n) => Math.round(n * 100) / 100;
|
|
|
|
renderer.setAnimationLoop(() => {
|
|
const dt = Math.min(clock.getDelta(), 0.05);
|
|
|
|
let moving = false;
|
|
if (!loopBroken) {
|
|
try {
|
|
moving = controls.update(dt);
|
|
avatars.update(dt);
|
|
} catch (e) { loopBroken = true; ui.showError('update', e); }
|
|
}
|
|
|
|
monMat.emissiveIntensity = 0.5 + 0.15 * Math.sin(performance.now() * 0.0015);
|
|
|
|
// Sitzplatz-Nähe -> Hinweis ein/ausblenden
|
|
const near = !ide.isOpen && player.position.distanceTo(seatPos) < SIT_RADIUS;
|
|
if (near !== nearSeat) { nearSeat = near; sithint.style.display = near ? 'block' : 'none'; }
|
|
|
|
// Position senden — gedrosselt und nur bei tatsächlicher Änderung
|
|
sendAcc += dt;
|
|
if (net.connected && sendAcc >= SEND_DT) {
|
|
sendAcc = 0;
|
|
const p = [round2(player.position.x), round2(player.position.y), round2(player.position.z)];
|
|
const rot = round2(controls.yaw);
|
|
const anim = moving ? 'walk' : 'idle';
|
|
if (anim !== last.anim || Math.abs(rot - last.rot) > 0.02 ||
|
|
Math.abs(p[0] - last.p[0]) > 0.01 || Math.abs(p[2] - last.p[2]) > 0.01) {
|
|
net.sendMove(p, rot, anim);
|
|
last = { p, rot, anim };
|
|
}
|
|
}
|
|
|
|
frames++; fpsAcc += dt;
|
|
if (fpsAcc >= 0.5) { ui.setFps(Math.round(frames / fpsAcc)); frames = 0; fpsAcc = 0; }
|
|
|
|
renderer.render(scene, camera);
|
|
});
|
|
|
|
ui.setDbg('bereit ✓');
|
|
window.__hubReady = true;
|
|
} catch (e) {
|
|
ui.showError('Init', e);
|
|
}
|