Tap anywhere to drop shapes!
1.5, size/2, 12)); break; case 55: // Flat Triangle Tile mesh = new THREE.Mesh(new THREE.CylinderGeometry(size/2, size/2, 0.1, 3), material); body.addShape(new CANNON.Cylinder(size/2, size/2, 0.1, 3)); break; case 56: // Heavy Stone (Randomly scaled box) const scaleX = 0.5 + Math.random(); const scaleY = 0.5 + Math.random(); const scaleZ = 0.5 + Math.random(); mesh = new THREE.Mesh(new THREE.BoxGeometry(size * scaleX, size * scaleY, size * scaleZ), material); body.addShape(new CANNON.Box(new CANNON.Vec3(size * scaleX / 2, size * scaleY / 2, size * scaleZ / 2))); break; case 57: // Mini Pyramid mesh = new THREE.Mesh(new THREE.ConeGeometry(size/3, size/2, 4), material); body.addShape(new CANNON.Box(new CANNON.Vec3(size/6, size/4, size/6))); break; case 58: // Egg (Distorted Sphere) mesh = new THREE.Mesh(new THREE.SphereGeometry(size/2, 32, 32), material); mesh.scale.set(1, 1.4, 1); body.addShape(new CANNON.Sphere(size/2)); break; case 59: // Double Sided Wedge { const dswGroup = new THREE.Group(); const wedge1 = new THREE.Mesh(new THREE.ConeGeometry(size/2, size, 4), material); wedge1.rotation.y = Math.PI/4; dswGroup.add(wedge1); mesh = dswGroup; body.addShape(new CANNON.Box(new CANNON.Vec3(size/2, size/2, size/2))); } break; default: mesh = new THREE.Mesh(new THREE.BoxGeometry(size, size, size), material); body.addShape(new CANNON.Box(new CANNON.Vec3(size/2, size/2, size/2))); } mesh.castShadow = true; mesh.receiveShadow = true; scene.add(mesh); body.quaternion.setFromEuler(Math.random() * Math.PI, Math.random() * Math.PI, Math.random() * Math.PI); world.addBody(body); shapes.push({ mesh, body }); // Respect Auto Cleanup toggle if (window.appState.rules.autoCleanup && shapes.length > window.appState.rules.maxShapes) { const oldest = shapes.shift(); scene.remove(oldest.mesh); world.remove(oldest.body); disposalQueue.push(oldest); } } function clearRoom() { // Immediate UI feedback flash for "lagless" feeling const flash = document.createElement('div'); flash.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:white;opacity:0.25;pointer-events:none;z-index:100;transition:opacity 0.4s ease-out;'; document.body.appendChild(flash); requestAnimationFrame(() => { flash.style.opacity = '0'; setTimeout(() => flash.remove(), 400); }); // Fast removal from scene and physics world while (shapes.length > 0) { const s = shapes.pop(); scene.remove(s.mesh); world.remove(s.body); disposalQueue.push(s); } } function setupControls() { let startPos = { x: 0, y: 0 }; const raycaster = new THREE.Raycaster(); const mouse = new THREE.Vector2(); const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0); // OrbitControls for slight POV movement const controls = new OrbitControls(camera, renderer.domElement); controls.enableDamping = true; controls.dampingFactor = 0.05; controls.maxPolarAngle = Math.PI / 2 - 0.1; // Don't go below ground controls.minDistance = 10; controls.maxDistance = 40; controls.enablePan = false; const container = document.getElementById('game-container'); container.addEventListener('pointerdown', (e) => { if (e.target.closest('.interactive-btn')) return; startPos = { x: e.clientX, y: e.clientY }; }); container.addEventListener('pointerup', (e) => { if (e.target.closest('.interactive-btn')) return; const dx = e.clientX - startPos.x; const dy = e.clientY - startPos.y; const dist = Math.sqrt(dx*dx + dy*dy); // If it was a tap (not a drag) if (dist < 10) { // Calculate drop position mouse.x = (e.clientX / window.innerWidth) * 2 - 1; mouse.y = -(e.clientY / window.innerHeight) * 2 + 1; raycaster.setFromCamera(mouse, camera); const intersectPoint = new THREE.Vector3(); raycaster.ray.intersectPlane(plane, intersectPoint); if (intersectPoint) { // Keep within bounds const limit = 15; const x = Math.max(-limit, Math.min(limit, intersectPoint.x)); const z = Math.max(-limit, Math.min(limit, intersectPoint.z)); spawnShape(x, z); } } }); window.appControls = controls; } function onWindowResize() { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); } const timeStep = 1 / 60; let lastCallTime = performance.now(); let lastSpawnTime = 0; function animate(time) { requestAnimationFrame(animate); if (!time) time = performance.now(); if (window.appControls) window.appControls.update(); // Auto-spawning logic if (window.appState.rules.autoSpawn) { const interval = window.appState.rules.spawnInterval || 500; if (time - lastSpawnTime > interval) { const limit = 12; const x = (Math.random() - 0.5) * (limit * 2); const z = (Math.random() - 0.5) * (limit * 2); spawnShape(x, z); lastSpawnTime = time; } } // Process deferred disposal in small chunks to prevent lag spikes if (disposalQueue.length > 0) { const batchSize = Math.min(disposalQueue.length, 15); for (let i = 0; i < batchSize; i++) { const s = disposalQueue.shift(); if (s && s.mesh) { if (s.mesh.geometry) s.mesh.geometry.dispose(); if (s.mesh.material) { if (Array.isArray(s.mesh.material)) { s.mesh.material.forEach(m => m.dispose()); } else { s.mesh.material.dispose(); } } } } } // Step physics const now = performance.now(); let dt = (now - lastCallTime) / 1000; if (dt > 0.1) dt = 0.1; // Prevent huge leaps lastCallTime = now; world.step(timeStep, dt, 3); // Sync meshes with bodies shapes.forEach(s => { s.mesh.position.copy(s.body.position); s.mesh.quaternion.copy(s.body.quaternion); }); renderer.render(scene, camera); } // Initialize when DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initApp); } else { initApp(); }