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 = `
+
+
+ `;
+ 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;
+ }
+};