diff --git a/js/terrain.js b/js/terrain.js new file mode 100644 index 0000000..ce82b13 --- /dev/null +++ b/js/terrain.js @@ -0,0 +1,92 @@ +// Terrain Management +const Terrain = { + // Map data + tiles: [], + + // Initialize terrain + init() { + this.generate(); + }, + + // Generate new map + generate() { + this.tiles = []; + for (let y = 0; y < CONFIG.MAP_HEIGHT; y++) { + this.tiles[y] = []; + for (let x = 0; x < CONFIG.MAP_WIDTH; x++) { + this.tiles[y][x] = { + type: 'grass', + resource: null, + amount: 0 + }; + } + } + + // Generate resource patches + this.generatePatch(20, 20, 8, 'iron', 5000); + this.generatePatch(35, 15, 6, 'iron', 4000); + this.generatePatch(60, 30, 7, 'iron', 4500); + this.generatePatch(15, 35, 7, 'copper', 4500); + this.generatePatch(40, 45, 6, 'copper', 4000); + this.generatePatch(70, 50, 6, 'copper', 4000); + this.generatePatch(50, 25, 5, 'coal', 3000); + this.generatePatch(25, 55, 5, 'coal', 3000); + this.generatePatch(65, 15, 4, 'coal', 2500); + }, + + // Generate a resource patch + generatePatch(cx, cy, radius, type, amount) { + for (let dy = -radius; dy <= radius; dy++) { + for (let dx = -radius; dx <= radius; dx++) { + const dist = Math.sqrt(dx * dx + dy * dy); + if (dist <= radius && Math.random() > dist / radius * 0.5) { + const x = cx + dx; + const y = cy + dy; + if (Utils.inBounds(x, y)) { + this.tiles[y][x].resource = type; + this.tiles[y][x].amount = Math.floor(amount * (1 - dist / radius * 0.5)); + } + } + } + } + }, + + // Get tile at position + getTile(x, y) { + if (!Utils.inBounds(x, y)) return null; + return this.tiles[y][x]; + }, + + // Mine from a tile (returns resource type or null) + mine(x, y, amount = 1) { + const tile = this.getTile(x, y); + if (!tile || !tile.resource || tile.amount <= 0) return null; + + const resourceType = tile.resource; + const mined = Math.min(amount, tile.amount); + tile.amount -= mined; + + if (tile.amount <= 0) { + tile.resource = null; + tile.amount = 0; + } + + return { type: resourceType, amount: mined }; + }, + + // Check if tile has resource + hasResource(x, y) { + const tile = this.getTile(x, y); + return tile && tile.resource && tile.amount > 0; + }, + + // Get all tiles data for saving + getData() { + return this.tiles.map(row => row.map(t => ({ ...t }))); + }, + + // Load tiles data + setData(data) { + this.tiles = data.map(row => row.map(t => ({ ...t }))); + } +}; diff --git a/js/towers.js b/js/towers.js new file mode 100644 index 0000000..c472438 --- /dev/null +++ b/js/towers.js @@ -0,0 +1,313 @@ +// Defense Tower System +const Towers = { + list: [], + projectiles: [], + + // Tower types + TYPES: { + 'gun_turret': { + name: 'Gun Turret', + description: 'Rapid-fire bullets', + health: 200, + maxHealth: 200, + range: 200, + damage: 15, + fireRate: 4, // shots per second + projectileSpeed: 500, + projectileColor: '#ffcc00', + cost: { 'iron-plate': 20, 'gear': 10 }, + color: '#888899', + colorDark: '#555566' + }, + 'flame_turret': { + name: 'Flame Turret', + description: 'Short range, high damage, burns', + health: 150, + maxHealth: 150, + range: 120, + damage: 40, + fireRate: 2, + burnDamage: 10, + burnDuration: 3, + projectileSpeed: 300, + projectileColor: '#ff6600', + cost: { 'iron-plate': 15, 'copper-plate': 20, 'coal': 50 }, + color: '#cc4400', + colorDark: '#882200' + }, + 'laser_turret': { + name: 'Laser Turret', + description: 'Long range, high damage', + health: 100, + maxHealth: 100, + range: 350, + damage: 50, + fireRate: 1, + projectileSpeed: 1000, + projectileColor: '#ff0000', + cost: { 'iron-plate': 30, 'copper-plate': 30, 'circuit': 20 }, + color: '#cc0044', + colorDark: '#880022' + }, + 'tesla_turret': { + name: 'Tesla Coil', + description: 'Chain lightning to multiple enemies', + health: 120, + maxHealth: 120, + range: 180, + damage: 25, + fireRate: 0.8, + chainCount: 3, + chainRange: 100, + projectileSpeed: 800, + projectileColor: '#44aaff', + cost: { 'iron-plate': 25, 'copper-plate': 50, 'circuit': 15 }, + color: '#2266aa', + colorDark: '#113366' + }, + 'cannon_turret': { + name: 'Cannon', + description: 'Slow, explosive AoE damage', + health: 300, + maxHealth: 300, + range: 280, + damage: 100, + fireRate: 0.5, + splashRadius: 80, + projectileSpeed: 250, + projectileColor: '#333333', + cost: { 'iron-plate': 50, 'gear': 20, 'coal': 30 }, + color: '#444455', + colorDark: '#222233' + } + }, + + // Initialize + init() { + this.list = []; + this.projectiles = []; + }, + + // Check if can place tower + canPlace(type, x, y) { + // Check bounds + if (!Utils.inBounds(x, y)) return false; + + // Check collision with buildings + if (Buildings.getAt(x, y)) return false; + + // Check collision with other towers + if (this.getAt(x, y)) return false; + + // Check cost + const towerType = this.TYPES[type]; + if (!Resources.canAfford(towerType.cost)) return false; + + return true; + }, + + // Place a tower + place(type, x, y) { + if (!this.canPlace(type, x, y)) { + Audio.playError(); + return false; + } + + const towerType = this.TYPES[type]; + Resources.payCost(towerType.cost); + + this.list.push({ + type, + x, + y, + health: towerType.maxHealth, + maxHealth: towerType.maxHealth, + cooldown: 0, + angle: 0, + target: null + }); + + Audio.playPlace(); + return true; + }, + + // Remove a tower + remove(x, y) { + const tower = this.getAt(x, y); + if (!tower) return false; + + const idx = this.list.indexOf(tower); + if (idx !== -1) { + this.list.splice(idx, 1); + // Refund half + const towerType = this.TYPES[tower.type]; + Resources.refundHalf(towerType.cost); + Audio.playDelete(); + return true; + } + return false; + }, + + // Get tower at position + getAt(x, y) { + return this.list.find(t => t.x === x && t.y === y); + }, + + // Update all towers + update(dt) { + // Update towers + this.list.forEach(tower => this.updateTower(tower, dt)); + + // Update projectiles + for (let i = this.projectiles.length - 1; i >= 0; i--) { + const proj = this.projectiles[i]; + this.updateProjectile(proj, dt); + + // Remove dead projectiles + if (proj.dead) { + this.projectiles.splice(i, 1); + } + } + }, + + // Update single tower + updateTower(tower, dt) { + const type = this.TYPES[tower.type]; + const towerX = (tower.x + 0.5) * CONFIG.TILE_SIZE; + const towerY = (tower.y + 0.5) * CONFIG.TILE_SIZE; + + // Reduce cooldown + tower.cooldown = Math.max(0, tower.cooldown - dt); + + // Find target + const target = Enemies.getNearest(towerX, towerY, type.range); + tower.target = target; + + if (target) { + // Rotate toward target + const dx = target.x - towerX; + const dy = target.y - towerY; + tower.angle = Math.atan2(dy, dx); + + // Fire if ready + if (tower.cooldown <= 0) { + tower.cooldown = 1 / type.fireRate; + this.fire(tower, target); + } + } + }, + + // Fire at target + fire(tower, target) { + const type = this.TYPES[tower.type]; + const startX = (tower.x + 0.5) * CONFIG.TILE_SIZE; + const startY = (tower.y + 0.5) * CONFIG.TILE_SIZE; + + this.projectiles.push({ + towerType: tower.type, + x: startX, + y: startY, + targetX: target.x, + targetY: target.y, + target: target, + speed: type.projectileSpeed, + damage: type.damage, + color: type.projectileColor, + splashRadius: type.splashRadius || 0, + chainCount: type.chainCount || 0, + chainRange: type.chainRange || 0, + burnDamage: type.burnDamage || 0, + burnDuration: type.burnDuration || 0 + }); + + // Visual feedback + tower.firing = true; + setTimeout(() => tower.firing = false, 50); + + // Sound + Audio.playShoot(tower.type); + }, + + // Update projectile + updateProjectile(proj, dt) { + const dx = proj.targetX - proj.x; + const dy = proj.targetY - proj.y; + const dist = Math.sqrt(dx * dx + dy * dy); + + if (dist < 10) { + // Hit target + this.projectileHit(proj); + proj.dead = true; + } else { + // Move toward target + const moveX = (dx / dist) * proj.speed * dt; + const moveY = (dy / dist) * proj.speed * dt; + proj.x += moveX; + proj.y += moveY; + } + + // Timeout + proj.lifetime = (proj.lifetime || 0) + dt; + if (proj.lifetime > 5) proj.dead = true; + }, + + // Projectile hits + projectileHit(proj) { + // Splash damage + if (proj.splashRadius > 0) { + const enemies = Enemies.getInRadius(proj.targetX, proj.targetY, proj.splashRadius); + enemies.forEach(e => { + Enemies.damage(e, proj.damage); + }); + Audio.playExplosion(); + } else if (proj.chainCount > 0) { + // Chain lightning + let targets = [proj.target]; + let lastTarget = proj.target; + + for (let i = 0; i < proj.chainCount; i++) { + const nearby = Enemies.getInRadius(lastTarget.x, lastTarget.y, proj.chainRange) + .filter(e => !targets.includes(e)); + if (nearby.length > 0) { + const next = nearby[0]; + targets.push(next); + lastTarget = next; + } + } + + targets.forEach(e => { + Enemies.damage(e, proj.damage); + }); + + // Store chain for rendering + proj.chainTargets = targets; + } else { + // Single target + if (proj.target && proj.target.health > 0) { + Enemies.damage(proj.target, proj.damage); + + // Apply burn + if (proj.burnDamage > 0) { + proj.target.burning = { + damage: proj.burnDamage, + duration: proj.burnDuration + }; + } + } + } + }, + + // Get data for saving + getData() { + return { + list: this.list.map(t => ({ ...t, target: null })), + projectiles: [] // Don't save projectiles + }; + }, + + // Load data + setData(data) { + this.list = data.list?.map(t => ({ ...t })) || []; + this.projectiles = []; + } +}; diff --git a/js/ui.js b/js/ui.js new file mode 100644 index 0000000..4d5cfd8 --- /dev/null +++ b/js/ui.js @@ -0,0 +1,525 @@ +// UI Management +const UI = { + tooltip: null, + recipeSelect: null, + selectedBuilding: null, + + // Initialize UI + init() { + this.tooltip = document.getElementById('tooltip'); + this.recipeSelect = document.getElementById('recipe-select'); + + this.createResourceBar(); + this.createToolbar(); + this.setupEventListeners(); + }, + + // Create resource bar + createResourceBar() { + const container = document.getElementById('resource-bar'); + container.innerHTML = CONFIG.RESOURCE_ORDER.map(res => ` +
+
+ ${CONFIG.RESOURCE_NAMES[res].split(' ')[0]}: 0 +
+ `).join(''); + }, + + // Create toolbar + createToolbar() { + const toolbar = document.getElementById('toolbar'); + toolbar.innerHTML = ` +

Tools

+
+ + + +
+ +

Extraction

+
+ +
+ +

Logistics

+
+ + + +
+ +

Production

+
+ + +
+ +

Defense

+
+ + + + + +
+ +

Game Speed

+
+
+ + + + +
+
+ +
+ Controls:
+ R Rotate | E Mine
+ WASD Pan | Scroll Zoom
+ Dev: \` Toggle
+ F1 Wave F2 Kill F3 Res +
+ `; + + // Category collapse handlers + toolbar.querySelectorAll('h3[data-category]').forEach(header => { + header.addEventListener('click', () => { + Audio.playUIClick(); + header.classList.toggle('collapsed'); + const category = document.getElementById('cat-' + header.dataset.category); + if (category) { + category.classList.toggle('collapsed'); + } + }); + }); + + // Tool button handlers + toolbar.querySelectorAll('.tool-btn').forEach(btn => { + btn.addEventListener('click', () => { + Audio.playUIClick(); + Input.selectTool(btn.dataset.tool); + }); + }); + + // Speed button handlers + toolbar.querySelectorAll('.speed-btn').forEach(btn => { + btn.addEventListener('click', () => { + Audio.playUIClick(); + Simulation.setSpeed(parseFloat(btn.dataset.speed)); + toolbar.querySelectorAll('.speed-btn').forEach(b => b.classList.remove('active')); + btn.classList.add('active'); + }); + }); + + // Create wave indicator + this.createWaveIndicator(); + + // Create minimap + this.createMinimap(); + + // Create sound toggle + this.createSoundToggle(); + }, + + // Create wave indicator element + createWaveIndicator() { + const indicator = document.createElement('div'); + indicator.id = 'wave-indicator'; + indicator.className = 'wave-indicator peaceful'; + indicator.innerHTML = ` +
Peaceful
+
Next wave in: --:--
+ `; + document.querySelector('.canvas-container').appendChild(indicator); + }, + + // Create minimap + createMinimap() { + const container = document.createElement('div'); + container.className = 'minimap-container'; + container.innerHTML = ` +
+ MINIMAP +
+ + `; + document.querySelector('.canvas-container').appendChild(container); + + // Click to navigate + const minimap = document.getElementById('minimap'); + minimap.addEventListener('click', (e) => { + const rect = minimap.getBoundingClientRect(); + const x = (e.clientX - rect.left) / minimap.width; + const y = (e.clientY - rect.top) / minimap.height; + + Renderer.camera.x = x * CONFIG.MAP_WIDTH * CONFIG.TILE_SIZE * Renderer.camera.zoom - Renderer.canvas.width / 2; + Renderer.camera.y = y * CONFIG.MAP_HEIGHT * CONFIG.TILE_SIZE * Renderer.camera.zoom - Renderer.canvas.height / 2; + }); + }, + + // Update minimap + updateMinimap() { + const canvas = document.getElementById('minimap'); + if (!canvas) return; + + const ctx = canvas.getContext('2d'); + const scale = canvas.width / CONFIG.MAP_WIDTH; + + // Clear + ctx.fillStyle = '#1a2a1a'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + // Draw resources + for (let y = 0; y < CONFIG.MAP_HEIGHT; y++) { + for (let x = 0; x < CONFIG.MAP_WIDTH; x++) { + const tile = Terrain.tiles[y]?.[x]; + if (tile?.resource && tile.amount > 0) { + const colors = { iron: '#8899bb', copper: '#cc9966', coal: '#444' }; + ctx.fillStyle = colors[tile.resource] || '#666'; + ctx.fillRect(x * scale, y * scale, scale + 0.5, scale + 0.5); + } + } + } + + // Draw buildings + ctx.fillStyle = '#ffaa00'; + Buildings.list.forEach(b => { + const size = Utils.getBuildingSize(b.type); + ctx.fillRect(b.x * scale, b.y * scale, size.w * scale + 0.5, size.h * scale + 0.5); + }); + + // Draw towers + ctx.fillStyle = '#4a9eff'; + Towers.list.forEach(t => { + ctx.fillRect(t.x * scale, t.y * scale, scale + 0.5, scale + 0.5); + }); + + // Draw enemies + ctx.fillStyle = '#ff4444'; + Enemies.list.forEach(e => { + const ex = e.x / CONFIG.TILE_SIZE * scale; + const ey = e.y / CONFIG.TILE_SIZE * scale; + ctx.beginPath(); + ctx.arc(ex, ey, 2, 0, Math.PI * 2); + ctx.fill(); + }); + + // Draw viewport + const viewX = Renderer.camera.x / (CONFIG.TILE_SIZE * Renderer.camera.zoom) * scale; + const viewY = Renderer.camera.y / (CONFIG.TILE_SIZE * Renderer.camera.zoom) * scale; + const viewW = Renderer.canvas.width / (CONFIG.TILE_SIZE * Renderer.camera.zoom) * scale; + const viewH = Renderer.canvas.height / (CONFIG.TILE_SIZE * Renderer.camera.zoom) * scale; + + ctx.strokeStyle = '#fff'; + ctx.lineWidth = 1; + ctx.strokeRect(viewX, viewY, viewW, viewH); + }, + + // Create sound toggle + createSoundToggle() { + const toggle = document.createElement('div'); + toggle.className = 'sound-toggle'; + toggle.id = 'sound-toggle'; + toggle.innerHTML = '๐Ÿ”Š Sound'; + toggle.addEventListener('click', () => { + const enabled = Audio.toggle(); + toggle.innerHTML = enabled ? '๐Ÿ”Š Sound' : '๐Ÿ”‡ Muted'; + toggle.classList.toggle('muted', !enabled); + }); + document.querySelector('.canvas-container').appendChild(toggle); + }, + + // Update wave indicator + updateWaveIndicator() { + const indicator = document.getElementById('wave-indicator'); + if (!indicator) return; + + const waveActive = Enemies.waveActive; + const timeUntil = Enemies.getTimeUntilWave(); + + indicator.className = 'wave-indicator ' + (waveActive ? '' : 'peaceful'); + + if (waveActive) { + indicator.innerHTML = ` +
โš” WAVE ${Enemies.wave} โš”
+
Enemies remaining: ${Enemies.enemiesRemaining}
+ `; + } else { + const minutes = Math.floor(timeUntil / 60); + const seconds = Math.floor(timeUntil % 60); + indicator.innerHTML = ` +
Wave ${Enemies.wave + 1} incoming
+
Time until wave: ${minutes}:${seconds.toString().padStart(2, '0')}
+ `; + } + + // Dev mode indicator + this.updateDevModeIndicator(); + }, + + // Show dev mode notification + showDevModeNotification() { + const existing = document.getElementById('dev-notification'); + if (existing) existing.remove(); + + const notification = document.createElement('div'); + notification.id = 'dev-notification'; + notification.style.cssText = ` + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: ${CONFIG.DEV_MODE ? 'rgba(68, 255, 68, 0.9)' : 'rgba(255, 68, 68, 0.9)'}; + color: #000; + padding: 20px 40px; + border-radius: 10px; + font-size: 24px; + font-weight: bold; + z-index: 9999; + pointer-events: none; + `; + notification.textContent = CONFIG.DEV_MODE ? '๐Ÿ›  DEV MODE ON' : '๐ŸŽฎ DEV MODE OFF'; + document.body.appendChild(notification); + + setTimeout(() => notification.remove(), 1500); + }, + + // Update dev mode indicator + updateDevModeIndicator() { + let indicator = document.getElementById('dev-mode-indicator'); + + if (CONFIG.DEV_MODE) { + if (!indicator) { + indicator = document.createElement('div'); + indicator.id = 'dev-mode-indicator'; + indicator.style.cssText = ` + position: fixed; + top: 10px; + left: 10px; + background: rgba(68, 255, 68, 0.8); + color: #000; + padding: 5px 10px; + border-radius: 5px; + font-size: 12px; + font-weight: bold; + z-index: 9999; + `; + indicator.textContent = '๐Ÿ›  DEV MODE'; + document.body.appendChild(indicator); + } + } else { + if (indicator) indicator.remove(); + } + }, + + // Setup event listeners + setupEventListeners() { + // Menu buttons + document.getElementById('btn-new').addEventListener('click', () => this.showModal('new-modal')); + document.getElementById('btn-save').addEventListener('click', () => { + SaveLoad.updateSaveSlots(); + this.showModal('save-modal'); + }); + document.getElementById('btn-load').addEventListener('click', () => { + SaveLoad.updateLoadSlots(); + this.showModal('load-modal'); + }); + document.getElementById('save-new').addEventListener('click', () => { + SaveLoad.saveToNewSlot(); + this.closeModal('save-modal'); + }); + document.getElementById('confirm-new').addEventListener('click', () => { + game.newGame(); + this.closeModal('new-modal'); + }); + + // Recipe select + document.querySelectorAll('.recipe-option').forEach(opt => { + opt.addEventListener('click', () => { + if (this.selectedBuilding && this.selectedBuilding.type === 'assembler') { + this.selectedBuilding.recipe = opt.dataset.recipe; + this.recipeSelect.style.display = 'none'; + } + }); + }); + + // Close recipe select when clicking elsewhere + document.addEventListener('click', (e) => { + if (!this.recipeSelect.contains(e.target)) { + this.recipeSelect.style.display = 'none'; + } + }); + }, + + // Update resource display + updateResources() { + CONFIG.RESOURCE_ORDER.forEach(res => { + const el = document.getElementById(`res-${res}`); + if (el) { + el.textContent = Utils.formatNumber(Resources.get(res)); + } + }); + }, + + // Update tool button states + updateToolButtons(activeTool) { + document.querySelectorAll('.tool-btn[data-tool]').forEach(btn => { + const tool = btn.dataset.tool; + btn.classList.toggle('active', tool === activeTool); + + // Disable if can't afford + if (CONFIG.COSTS[tool]) { + btn.classList.toggle('disabled', !Resources.canAfford(CONFIG.COSTS[tool])); + } + }); + }, + + // Update tooltip + updateTooltip(clientX, clientY, mouseWorld) { + const building = Buildings.getAt(mouseWorld.x, mouseWorld.y); + const tile = Terrain.getTile(mouseWorld.x, mouseWorld.y); + + if (building) { + let info = `${building.type.toUpperCase()}
`; + if (building.type === 'assembler') { + info += `Recipe: ${building.recipe || 'None'}
`; + } + const invItems = Object.entries(building.inventory).filter(([k, v]) => v > 0); + const outItems = Object.entries(building.output).filter(([k, v]) => v > 0); + if (invItems.length > 0) { + info += 'Input: ' + invItems.map(([k, v]) => `${k}: ${v}`).join(', ') + '
'; + } + if (outItems.length > 0) { + info += 'Output: ' + outItems.map(([k, v]) => `${k}: ${v}`).join(', '); + } + this.tooltip.innerHTML = info; + this.tooltip.style.display = 'block'; + this.tooltip.style.left = (clientX + 15) + 'px'; + this.tooltip.style.top = (clientY + 15) + 'px'; + } else if (tile?.resource && tile.amount > 0) { + this.tooltip.innerHTML = `${tile.resource.toUpperCase()} ORE
Amount: ${Utils.formatNumber(tile.amount)}
Click with Mine tool [E] to harvest`; + this.tooltip.style.display = 'block'; + this.tooltip.style.left = (clientX + 15) + 'px'; + this.tooltip.style.top = (clientY + 15) + 'px'; + } else { + this.tooltip.style.display = 'none'; + } + }, + + // Show recipe select + showRecipeSelect(clientX, clientY, building) { + this.selectedBuilding = building; + this.recipeSelect.style.display = 'block'; + this.recipeSelect.style.left = (clientX + 10) + 'px'; + this.recipeSelect.style.top = (clientY + 10) + 'px'; + }, + + // Show modal + showModal(id) { + document.getElementById(id).style.display = 'flex'; + }, + + // Close modal + closeModal(id) { + document.getElementById(id).style.display = 'none'; + } +}; \ No newline at end of file diff --git a/js/utils.js b/js/utils.js new file mode 100644 index 0000000..386a5c3 --- /dev/null +++ b/js/utils.js @@ -0,0 +1,63 @@ +// Utility Functions +const Utils = { + // Convert screen coordinates to world tile coordinates + screenToWorld(sx, sy, camera) { + return { + x: Math.floor((sx + camera.x) / (CONFIG.TILE_SIZE * camera.zoom)), + y: Math.floor((sy + camera.y) / (CONFIG.TILE_SIZE * camera.zoom)) + }; + }, + + // Convert world tile coordinates to screen coordinates + worldToScreen(wx, wy, camera) { + return { + x: wx * CONFIG.TILE_SIZE * camera.zoom - camera.x, + y: wy * CONFIG.TILE_SIZE * camera.zoom - camera.y + }; + }, + + // Draw a rounded rectangle + drawRoundedRect(ctx, x, y, w, h, r) { + ctx.beginPath(); + ctx.moveTo(x + r, y); + ctx.lineTo(x + w - r, y); + ctx.quadraticCurveTo(x + w, y, x + w, y + r); + ctx.lineTo(x + w, y + h - r); + ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h); + ctx.lineTo(x + r, y + h); + ctx.quadraticCurveTo(x, y + h, x, y + h - r); + ctx.lineTo(x, y + r); + ctx.quadraticCurveTo(x, y, x + r, y); + ctx.closePath(); + }, + + // Get building size from config + getBuildingSize(type) { + return CONFIG.BUILDING_SIZES[type] || { w: 1, h: 1 }; + }, + + // Deep clone an object + deepClone(obj) { + return JSON.parse(JSON.stringify(obj)); + }, + + // Format number with commas + formatNumber(num) { + return num.toLocaleString(); + }, + + // Clamp a value between min and max + clamp(value, min, max) { + return Math.max(min, Math.min(max, value)); + }, + + // Linear interpolation + lerp(a, b, t) { + return a + (b - a) * t; + }, + + // Check if a point is within bounds + inBounds(x, y) { + return x >= 0 && x < CONFIG.MAP_WIDTH && y >= 0 && y < CONFIG.MAP_HEIGHT; + } +};