| |
| 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; |
| |
| |
| this.resize(); |
| window.addEventListener('resize', () => this.resize()); |
| |
| |
| document.addEventListener('keydown', (e) => this.keys[e.key.toLowerCase()] = true); |
| document.addEventListener('keyup', (e) => this.keys[e.key.toLowerCase()] = false); |
| |
| |
| 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; |
| } |
| |
| |
| project(x, y, z) { |
| const cosA = Math.cos(this.camera.angle); |
| const sinA = Math.sin(this.camera.angle); |
| |
| |
| const x1 = x * cosA - z * sinA; |
| const z1 = x * sinA + z * cosA; |
| |
| |
| x1 -= this.camera.x; |
| y -= this.camera.y; |
| z1 -= this.camera.z; |
| |
| |
| 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 }; |
| } |
| |
| |
| 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); |
| |
| |
| if (p1.scale <= 0) return; |
| |
| this.ctx.strokeStyle = color; |
| this.ctx.fillStyle = color + '80'; |
| this.ctx.lineWidth = 2; |
| |
| |
| 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(); |
| |
| |
| 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(); |
| |
| |
| 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(); |
| } |
| |
| |
| drawBulldozer(x, y, z) { |
| |
| this.drawBox(x - 1, y - 0.5, z - 1.5, 2, 1, 3, '#4a5568'); |
| |
| |
| this.drawBox(x - 1.5, y - 0.3, z - 2, 3, 0.6, 0.3, '#e53e3e'); |
| |
| |
| this.drawBox(x - 0.8, y - 0.5, z + 0.5, 1.6, 1.2, 1.2, '#2d3748'); |
| |
| |
| 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'); |
| } |
| |
| |
| drawDebris(x, y, z, size) { |
| this.drawBox(x, y, z, size, size, size, '#a0aec0'); |
| } |
| |
| |
| createScene() { |
| this.objects = []; |
| this.buildingsDestroyed = 0; |
| this.debrisCount = 0; |
| |
| |
| for (let i = 0; i < 5; i++) { |
| for (let j = 0; j < 5; j++) { |
| if (i === 2 && j === 2) continue; |
| 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 |
| }); |
| } |
| } |
| |
| |
| this.ground = { |
| x: -25, y: -0.1, z: -25, |
| width: 50, depth: 50 |
| }; |
| } |
| |
| |
| update(deltaTime) { |
| |
| 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; |
| |
| |
| if (this.keys['arrowleft']) this.camera.angle -= deltaTime * 0.002; |
| if (this.keys['arrowright']) this.camera.angle += deltaTime * 0.002; |
| |
| |
| if (this.keys['r']) { |
| this.camera = { x: 0, y: 0, z: 0, angle: 0, elevation: 0.5 }; |
| this.createScene(); |
| } |
| |
| |
| 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++; |
| |
| |
| 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++; |
| } |
| } |
| } |
| }); |
| |
| |
| document.getElementById('buildings-destroyed').textContent = this.buildingsDestroyed; |
| document.getElementById('debris-count').textContent = this.debrisCount; |
| } |
| |
| |
| render() { |
| this.ctx.clearRect(0, 0, this.width, this.height); |
| |
| |
| this.ctx.fillStyle = '#2d3748'; |
| this.ctx.fillRect(0, 0, this.width, this.height); |
| |
| |
| 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(); |
| } |
| |
| |
| const sortedObjects = [...this.objects].sort((a, b) => { |
| const aDepth = a.z + (a.depth || 0); |
| const bDepth = b.z + (b.depth || 0); |
| return bDepth - aDepth; |
| }); |
| |
| |
| 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); |
| } |
| }); |
| |
| |
| this.drawBulldozer(this.camera.x, this.camera.y, this.camera.z); |
| } |
| |
| |
| animate = (timestamp) => { |
| const deltaTime = timestamp - this.lastTime; |
| this.lastTime = timestamp; |
| |
| this.update(deltaTime); |
| this.render(); |
| |
| requestAnimationFrame(this.animate); |
| } |
| } |
|
|
| |
| window.addEventListener('load', () => { |
| const canvas = document.getElementById('gameCanvas'); |
| const engine = new Simple3DEngine(canvas); |
| engine.createScene(); |
| }); |