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();
}