// Simple 3D engine using Canvas 2D with isometric projection class Simple3DEngine { constructor(canvas) { this.canvas = canvas; this.ctx = canvas.getContext('2d'); this.width = canvas.width; this.height = canvas.height; this.camera = { x: 0, y: 0, z: 0, angle: 0, elevation: 0.5 }; this.objects = []; this.keys = {}; this.buildingsDestroyed = 0; this.debrisCount = 0; // Set canvas size this.resize(); window.addEventListener('resize', () => this.resize()); // Setup keyboard controls document.addEventListener('keydown', (e) => this.keys[e.key.toLowerCase()] = true); document.addEventListener('keyup', (e) => this.keys[e.key.toLowerCase()] = false); // Start animation loop this.lastTime = 0; this.animate(); } resize() { this.canvas.width = this.canvas.clientWidth; this.canvas.height = this.canvas.clientHeight; this.width = this.canvas.width; this.height = this.canvas.height; } // Isometric projection project(x, y, z) { const cosA = Math.cos(this.camera.angle); const sinA = Math.sin(this.camera.angle); // Rotate around Y axis const x1 = x * cosA - z * sinA; const z1 = x * sinA + z * cosA; // Apply camera position x1 -= this.camera.x; y -= this.camera.y; z1 -= this.camera.z; // Isometric projection const scale = 80 / (z1 + 10); const screenX = this.width / 2 + (x1 * scale); const screenY = this.height / 2 - (y * scale) + (z1 * scale * this.camera.elevation); return { x: screenX, y: screenY, scale: scale }; } // Draw a 3D box drawBox(x, y, z, width, height, depth, color) { const p1 = this.project(x, y, z); const p2 = this.project(x + width, y, z); const p3 = this.project(x + width, y + height, z); const p4 = this.project(x, y + height, z); const p5 = this.project(x, y, z + depth); const p6 = this.project(x + width, y, z + depth); const p7 = this.project(x + width, y + height, z + depth); const p8 = this.project(x, y + height, z + depth); // Only draw if in front of camera if (p1.scale <= 0) return; this.ctx.strokeStyle = color; this.ctx.fillStyle = color + '80'; this.ctx.lineWidth = 2; // Draw front face this.ctx.beginPath(); this.ctx.moveTo(p1.x, p1.y); this.ctx.lineTo(p2.x, p2.y); this.ctx.lineTo(p3.x, p3.y); this.ctx.lineTo(p4.x, p4.y); this.ctx.closePath(); this.ctx.fill(); this.ctx.stroke(); // Draw top face this.ctx.beginPath(); this.ctx.moveTo(p1.x, p1.y); this.ctx.lineTo(p2.x, p2.y); this.ctx.lineTo(p6.x, p6.y); this.ctx.lineTo(p5.x, p5.y); this.ctx.closePath(); this.ctx.fill(); this.ctx.stroke(); // Draw side face this.ctx.beginPath(); this.ctx.moveTo(p2.x, p2.y); this.ctx.lineTo(p3.x, p3.y); this.ctx.lineTo(p7.x, p7.y); this.ctx.lineTo(p6.x, p6.y); this.ctx.closePath(); this.ctx.fill(); this.ctx.stroke(); } // Draw the bulldozer drawBulldozer(x, y, z) { // Main body this.drawBox(x - 1, y - 0.5, z - 1.5, 2, 1, 3, '#4a5568'); // Blade this.drawBox(x - 1.5, y - 0.3, z - 2, 3, 0.6, 0.3, '#e53e3e'); // Cab this.drawBox(x - 0.8, y - 0.5, z + 0.5, 1.6, 1.2, 1.2, '#2d3748'); // Wheels this.drawBox(x - 0.9, y - 0.5, z - 1, 0.3, 0.3, 0.3, '#2d3748'); this.drawBox(x + 0.6, y - 0.5, z - 1, 0.3, 0.3, 0.3, '#2d3748'); this.drawBox(x - 0.9, y - 0.5, z + 0.5, 0.3, 0.3, 0.3, '#2d3748'); this.drawBox(x + 0.6, y - 0.5, z + 0.5, 0.3, 0.3, 0.3, '#2d3748'); } // Draw debris drawDebris(x, y, z, size) { this.drawBox(x, y, z, size, size, size, '#a0aec0'); } // Create initial scene createScene() { this.objects = []; this.buildingsDestroyed = 0; this.debrisCount = 0; // Create buildings for (let i = 0; i < 5; i++) { for (let j = 0; j < 5; j++) { if (i === 2 && j === 2) continue; // Skip center for bulldozer this.objects.push({ type: 'building', x: (i - 2) * 8, y: 0, z: (j - 2) * 8, width: 3 + Math.random() * 2, height: 4 + Math.random() * 6, depth: 3 + Math.random() * 2, color: `hsl(${200 + Math.random() * 40}, 50%, 50%)`, destroyed: false }); } } // Create ground this.ground = { x: -25, y: -0.1, z: -25, width: 50, depth: 50 }; } // Update game state update(deltaTime) { // Handle bulldozer movement const moveSpeed = deltaTime * 0.1; if (this.keys['w']) this.camera.z -= moveSpeed; if (this.keys['s']) this.camera.z += moveSpeed; if (this.keys['a']) this.camera.x -= moveSpeed; if (this.keys['d']) this.camera.x += moveSpeed; // Handle camera rotation if (this.keys['arrowleft']) this.camera.angle -= deltaTime * 0.002; if (this.keys['arrowright']) this.camera.angle += deltaTime * 0.002; // Reset scene if (this.keys['r']) { this.camera = { x: 0, y: 0, z: 0, angle: 0, elevation: 0.5 }; this.createScene(); } // Check collisions with buildings const bulldozerX = this.camera.x; const bulldozerZ = this.camera.z; this.objects.forEach(obj => { if (obj.type === 'building' && !obj.destroyed) { const dx = bulldozerX - obj.x; const dz = bulldozerZ - obj.z; const distance = Math.sqrt(dx * dx + dz * dz); if (distance < 3) { obj.destroyed = true; this.buildingsDestroyed++; // Create debris for (let i = 0; i < 10; i++) { this.objects.push({ type: 'debris', x: obj.x + (Math.random() - 0.5) * obj.width, y: obj.y + Math.random() * obj.height, z: obj.z + (Math.random() - 0.5) * obj.depth, size: 0.2 + Math.random() * 0.3 }); this.debrisCount++; } } } }); // Update stats display document.getElementById('buildings-destroyed').textContent = this.buildingsDestroyed; document.getElementById('debris-count').textContent = this.debrisCount; } // Render the scene render() { this.ctx.clearRect(0, 0, this.width, this.height); // Draw ground this.ctx.fillStyle = '#2d3748'; this.ctx.fillRect(0, 0, this.width, this.height); // Draw grid on ground this.ctx.strokeStyle = '#4a5568'; this.ctx.lineWidth = 1; for (let i = -10; i <= 10; i++) { const p1 = this.project(i * 5, 0, -50); const p2 = this.project(i * 5, 0, 50); this.ctx.beginPath(); this.ctx.moveTo(p1.x, p1.y); this.ctx.lineTo(p2.x, p2.y); this.ctx.stroke(); const p3 = this.project(-50, 0, i * 5); const p4 = this.project(50, 0, i * 5); this.ctx.beginPath(); this.ctx.moveTo(p3.x, p3.y); this.ctx.lineTo(p4.x, p4.y); this.ctx.stroke(); } // Sort objects by depth for proper rendering const sortedObjects = [...this.objects].sort((a, b) => { const aDepth = a.z + (a.depth || 0); const bDepth = b.z + (b.depth || 0); return bDepth - aDepth; }); // Draw objects sortedObjects.forEach(obj => { if (obj.type === 'building' && !obj.destroyed) { this.drawBox(obj.x, obj.y, obj.z, obj.width, obj.height, obj.depth, obj.color); } else if (obj.type === 'debris') { this.drawDebris(obj.x, obj.y, obj.z, obj.size); } }); // Draw bulldozer at camera position this.drawBulldozer(this.camera.x, this.camera.y, this.camera.z); } // Animation loop animate = (timestamp) => { const deltaTime = timestamp - this.lastTime; this.lastTime = timestamp; this.update(deltaTime); this.render(); requestAnimationFrame(this.animate); } } // Initialize the game when the page loads window.addEventListener('load', () => { const canvas = document.getElementById('gameCanvas'); const engine = new Simple3DEngine(canvas); engine.createScene(); });