# Browserprüfung eines Orts (verborgener Browser-Bereich) Alle Ausschnitte sind am Hood Yard erprobt (2026-09-14). `javascript_tool` bricht nach 45 s ab: lange Abläufe auf mehrere Aufrufe verteilen. Jeder Aufruf hat eigene Variablen — was später gebraucht wird, an `window` hängen. ## 1. Dev-Server - Das Vorschau-Werkzeug liest Server nur aus `.claude/launch.json` des **Hauptordners**. Auf 5173 läuft oft schon der Hauptordner. - Arbeitet die Sitzung in einem Arbeitsbaum, darf sie diese Datei nicht ändern: Nutzer fragen, dann `ExitWorktree(keep)` → Eintrag setzen → `preview_start` → `EnterWorktree(path)`. Am Ende wieder raus und den Eintrag entfernen. ```json { "name": "place-check", "runtimeExecutable": "bash", "runtimeArgs": ["-lc", "cd /Users/Kenso/Developer/gta6/.claude/worktrees/ && npx vite --port 5181 --strictPort"], "port": 5181, "url": "http://localhost:5181" } ``` - Arbeitsbaum vorbereiten: `cp -Rc node_modules /node_modules`, dazu `three-player-controller/package.json` und `dist/` in `/three-player-controller/` kopieren. Git ignoriert Dateien im nicht ausgecheckten Submodul. ## 2. Debug-Seite (nie committen, danach löschen) Der verborgene Bereich meldet oft 0 × 0 px, `requestAnimationFrame` steht, und three's `Timer` liefert delta 0, solange `document.hidden` gilt. Vor jedem Zug `resize_window` auf 1280 × 800 setzen (die App setzt es am Zugende zurück). `index-debug.html` im Root: Kopie von `index.html` mit diesem Skript im `` vor dem Modul-Skript: ```html ``` ## 3. Szene, Kamera, Renderer greifbar machen Wirft ein Hook, stirbt die Zeichenschleife endgültig. Vite lädt die Seite bei Abhängigkeits-Rebuilds neu (`performance.now()` klein), dann ist `window.__probe` weg — deshalb `??=`. ```js const deps = performance.getEntriesByType("resource").map((e) => e.name); const THREE = await import(deps.find((n) => n.includes("/node_modules/.vite/deps/three.js"))); window.__frames = []; THREE.Scene.prototype.onBeforeRender = function (renderer, scene, camera) { Object.assign((window.__probe ??= {}), { renderer, scene, camera }); window.__frames.push(performance.now()); if (window.__frames.length > 4000) window.__frames.splice(0, 2000); if (window.__applyView) window.__applyView(camera); }; ``` ## 4. Hinspringen über die Minimap `M` per `keydown`/`keyup` (`code: "KeyM"`) auf `window`. Das Kartenbild bleibt im verborgenen Bereich bei „Karte lädt …“, bis ein **Screenshot** gemacht wurde. Danach klicken, mit `X`, `Y` = Welt-Ziel neben dem Ort (nicht auf ein Gebäude): ```js const map = [...document.querySelectorAll("canvas")].find((c) => c !== window.__probe.renderer.domElement); const rect = map.getBoundingClientRect(), dpr = map.width / rect.width; const scale = Math.min(map.width / 3000, map.height / 3000); const ox = (map.width - 3000 * scale) / 2, oy = (map.height - 3000 * scale) / 2; const opts = { clientX: rect.left + ((X + 1000) * scale + ox) / dpr, clientY: rect.top + ((2500 - Y) * scale + oy) / dpr, button: 0, bubbles: true }; map.dispatchEvent(new MouseEvent("mousedown", opts)); map.dispatchEvent(new MouseEvent("mouseup", opts)); ``` Die Wurzel des Orts findet man über ihre Lage: `scene.children.find((o) => Math.abs(o.position.x - at[0]) < 0.01 && Math.abs(o.position.z + at[1]) < 0.01)`. ## 5. Zustand prüfen - **Kollisionsmarken:** Mit `root.traverse` die Meshes ohne `userData.excludeFromCollider` sammeln. Ihre Materialnamen müssen genau die festen Teile sein. - **Quantisierung:** Alle festen Meshes haben `geometry.attributes.position.array instanceof Float32Array`, dafür sorgt `prepareModel`. Fehlt das, landet der Collider bei ±1 m um den Weltursprung. - **Vorwärmen:** Vor dem Sprung ist der Ort noch nicht in der Szene. `renderer.info.memory.textures` liegt am Startpunkt trotzdem schon um die Texturzahl des Orts höher. ## 6. Blicke aufnehmen Kamera übersteuern, auch in einem Wrapper um `CSM.prototype.update`, sonst fehlen die Schatten im Bild: ```js const { CSM } = await import(deps.find((n) => n.includes("three_examples_jsm_csm_CSM"))); const eye = new THREE.Vector3(), target = new THREE.Vector3(), up = new THREE.Vector3(), m = new THREE.Matrix4(); window.__applyView = (camera) => { if (!window.__view) return; eye.fromArray(window.__view.position); target.fromArray(window.__view.target); up.fromArray(window.__view.up ?? [0, 1, 0]); m.lookAt(eye, target, up).setPosition(eye); camera.matrixWorld.copy(m); camera.matrixWorldInverse.copy(m).invert(); }; if (!CSM.prototype.__wrapped) { const update = CSM.prototype.update; CSM.prototype.update = function (...a) { window.__applyView(this.camera); return update.apply(this, a); }; CSM.prototype.__wrapped = true; } ``` - **Blicke:** Augenhöhe von vorn und von hinten (y 1,7), dazu eine Draufsicht (`position [x, 60, z]`, `up [0, 0, -1]`, Norden oben). Pro Blick `window.__view` setzen und ~1 s warten. - **Aufnehmen:** `renderer.render(scene, camera)`, auf ein 2D-Canvas mit 1280 × 800 zeichnen, `toDataURL("image/jpeg", 0.85)` zurückgeben. - **Speichern:** Das Ergebnis ist zu groß und landet unter `tool-results/`. `scripts/decode-view.py ` holt das Bild heraus. - **Aufräumen:** `window.__view = null`. ## 7. Kollision ablaufen ```js Object.defineProperty(document, "pointerLockElement", { configurable: true, get: () => renderer.domElement }); const capsule = scene.getObjectByName("capsule"); const heading = () => { const d = camera.getWorldDirection(new THREE.Vector3()); return Math.atan2(d.x, -d.z) * 180 / Math.PI; }; // 0 = Norden const turn = (deg) => window.dispatchEvent(new MouseEvent("mousemove", { movementX: deg / 8.6 * 300, movementY: 0 })); Object.assign(window, { capsule, heading, turn }); ``` - **Start:** Kapsel mit `capsule.position.set(x, 1, z)` auf freien Boden des Orts setzen, mit Blick auf eine Wand; der Weg darf nicht über Bordsteine führen. Drehen, `heading()` prüfen, dann `keydown` `KeyW`. Jede Sekunde die Lage notieren, danach `keyup`. - **Stand:** Auf blankem Chunk-Boden steht die Kapsel bei y 0,75, auf der Bodenplatte des Orts um deren Oberkante höher (Hood Yard: 0,81). - **Wand:** Die Kapsel muss mit wenigen cm Abstand vor der Wand stehen bleiben. Gras und Blätter muss sie ohne Halt durchlaufen. - **Tempo:** etwa 0,38 m/s bei 33-ms-Takt, also genug Zeit geben (15–25 s). ## 8. Kosten messen `renderer.info.render` zählt die Schattenstufen nicht mit, es wird nach `shadowMap.render` zurückgesetzt. Stattdessen takten, einmal mit und einmal ohne die Wurzel des Orts in der Szene: ```js const gl = renderer.getContext(); const measure = (n) => { const t = []; for (let i = 0; i < n; i++) { gl.finish(); const s = performance.now(); renderer.render(scene, camera); gl.finish(); t.push(performance.now() - s); } t.sort((a, b) => a - b); return t[n >> 1]; }; ``` - **Median:** Beim Hood Yard kosteten Blick vom Rand und Blick im Ort je +0,5 ms. - **Ruckler beim Einsetzen:** Nach einem Sprung die größte Lücke in `window.__frames` rund um den Moment suchen, in dem der Ort in der Szene erscheint. Mit Vorwärmen waren das 86 ms bei 33-ms-Takt, ohne kamen allein ~130 ms für den Texturupload dazu.