about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rwxr-xr-xawk/scheme/scheme/bin/compiler.awk5
-rwxr-xr-xawk/scheme/scheme/bin/vm.awk7
-rw-r--r--html/tower/index.html110
-rw-r--r--html/tower/js/game.js227
-rw-r--r--html/tower/js/gameState.js82
-rw-r--r--html/tower/js/path.js83
-rw-r--r--html/tower/js/renderer.js199
7 files changed, 705 insertions, 8 deletions
diff --git a/awk/scheme/scheme/bin/compiler.awk b/awk/scheme/scheme/bin/compiler.awk
index e7e8081..5371bb2 100755
--- a/awk/scheme/scheme/bin/compiler.awk
+++ b/awk/scheme/scheme/bin/compiler.awk
@@ -7,7 +7,7 @@ BEGIN {
     next_label = 0
     program = ""
     
-    # Debug mode
+    # Debug mode, 0 off, 1 on
     DEBUG = 0
 }
 
@@ -15,7 +15,7 @@ function debug(msg) {
     if (DEBUG) printf("[DEBUG] %s\n", msg) > "/dev/stderr"
 }
 
-# Process input line - just accumulate the raw input
+# Process input line - accumulate the raw input
 {
     if (program != "") program = program "\n"
     program = program $0
@@ -29,7 +29,6 @@ END {
     split_expressions(program)
 }
 
-# New function to handle multiple expressions
 function split_expressions(prog,    current, paren_count, i, c, expr, cleaned) {
     current = ""
     paren_count = 0
diff --git a/awk/scheme/scheme/bin/vm.awk b/awk/scheme/scheme/bin/vm.awk
index cb2b992..aa85280 100755
--- a/awk/scheme/scheme/bin/vm.awk
+++ b/awk/scheme/scheme/bin/vm.awk
@@ -1,6 +1,5 @@
 #!/usr/bin/awk -f
 
-# VM State Initialization
 BEGIN {
     # Type tags
     T_NUMBER = "N"
@@ -10,24 +9,22 @@ BEGIN {
     T_FUNCTION = "F"
     T_NIL = "NIL"
 
-    # VM registers
+    # Registers
     stack_ptr = 0    # Stack pointer
     heap_ptr = 0     # Heap pointer
     pc = 0           # Program counter
     
-    # Debug mode
+    # Debug mode, 0 off, 1 on
     DEBUG = 0
 
     # Environment for variables
     env_size = 0
     
-    # Function table (make it persistent)
     delete func_name
     delete func_pc
     delete func_code
     func_size = 0
     
-    # Call stack
     call_stack_ptr = 0
 
     # State persistence
diff --git a/html/tower/index.html b/html/tower/index.html
new file mode 100644
index 0000000..23755fa
--- /dev/null
+++ b/html/tower/index.html
@@ -0,0 +1,110 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>Tower Defense</title>
+    <style>
+        body {
+            margin: 0;
+            display: flex;
+            justify-content: center;
+            align-items: center;
+            min-height: 100vh;
+            background-color: #f0f0f0;
+        }
+        .game-container {
+            position: relative;
+            display: flex;
+            gap: 20px;
+        }
+        #gameCanvas {
+            border: 2px solid #333;
+            background-color: white;
+        }
+        .tower-palette {
+            width: 100px;
+            background: white;
+            border: 2px solid #333;
+            padding: 10px;
+            display: flex;
+            flex-direction: column;
+            gap: 10px;
+        }
+        .tower-option {
+            width: 80px;
+            height: 80px;
+            border: 2px solid #666;
+            cursor: grab;
+            position: relative;
+            display: flex;
+            flex-direction: column;
+            align-items: center;
+            justify-content: center;
+        }
+        .tower-option:active {
+            cursor: grabbing;
+        }
+        .tower-preview {
+            width: 40px;
+            height: 40px;
+            margin-bottom: 5px;
+        }
+        .tower-cost {
+            font-size: 12px;
+            color: #666;
+        }
+        .start-button {
+            margin-top: 20px;
+            padding: 10px;
+            background-color: #2ecc71;
+            color: white;
+            border: none;
+            border-radius: 4px;
+            cursor: pointer;
+            font-size: 16px;
+            width: 100%;
+        }
+        .start-button:hover {
+            background-color: #27ae60;
+        }
+        .start-button:disabled {
+            background-color: #95a5a6;
+            cursor: not-allowed;
+        }
+    </style>
+</head>
+<body>
+    <div class="game-container">
+        <div class="tower-palette">
+            <div class="tower-option" draggable="true" data-tower-type="BASIC">
+                <div class="tower-preview" style="background: #4a90e2;"></div>
+                <div class="tower-cost">$20</div>
+            </div>
+            <div class="tower-option" draggable="true" data-tower-type="SNIPER">
+                <div class="tower-preview" style="background: #9b59b6;"></div>
+                <div class="tower-cost">$40</div>
+            </div>
+            <div class="tower-option" draggable="true" data-tower-type="RAPID">
+                <div class="tower-preview" style="background: #2ecc71;"></div>
+                <div class="tower-cost">$35</div>
+            </div>
+            <button id="startCombat" class="start-button">Start Combat</button>
+        </div>
+        <canvas id="gameCanvas" width="600" height="600"></canvas>
+    </div>
+    
+    <!-- Core game modules -->
+    <script src="js/grid.js"></script>
+    <script src="js/path.js"></script>
+    <script src="js/tower.js"></script>
+    <script src="js/enemy.js"></script>
+    
+    <!-- Rendering modules -->
+    <script src="js/renderer.js"></script>
+    
+    <!-- Game state and main loop -->
+    <script src="js/gameState.js"></script>
+    <script src="js/game.js"></script>
+</body>
+</html>
\ No newline at end of file
diff --git a/html/tower/js/game.js b/html/tower/js/game.js
new file mode 100644
index 0000000..e36c291
--- /dev/null
+++ b/html/tower/js/game.js
@@ -0,0 +1,227 @@
+const canvas = document.getElementById('gameCanvas');
+const ctx = canvas.getContext('2d');
+
+// Initialize game state
+let gameState = {
+    grid: Array(20).fill().map(() => Array(20).fill('empty')),
+    path: [],
+    towers: [],
+    enemies: [],
+    playerCurrency: 100,
+    phase: 'placement',
+    isGameOver: false,
+    particles: [],
+    projectiles: []
+};
+
+let lastTimestamp = 0;
+const ENEMY_SPAWN_INTERVAL = 1000; // 1 second
+let lastEnemySpawn = 0;
+let enemiesRemaining = 0;
+
+let draggedTowerType = null;
+let hoverCell = null;
+
+function gameLoop(timestamp) {
+    const deltaTime = timestamp - lastTimestamp;
+    lastTimestamp = timestamp;
+    
+    // Clear canvas
+    ctx.clearRect(0, 0, canvas.width, canvas.height);
+    
+    if (gameState.phase === GamePhase.COMBAT) {
+        // Spawn enemies
+        if (enemiesRemaining > 0 && timestamp - lastEnemySpawn > ENEMY_SPAWN_INTERVAL) {
+            gameState.enemies.push(createEnemy({ x: 0, y: gameState.path[0].y }));
+            lastEnemySpawn = timestamp;
+            enemiesRemaining--;
+        }
+        
+        // Update enemy positions
+        gameState.enemies.forEach(enemy => {
+            if (enemy.pathIndex < gameState.path.length - 1) {
+                const targetPos = gameState.path[enemy.pathIndex + 1];
+                const dx = targetPos.x - enemy.position.x;
+                const dy = targetPos.y - enemy.position.y;
+                const distance = Math.sqrt(dx * dx + dy * dy);
+                
+                if (distance < enemy.speed * deltaTime / 1000) {
+                    enemy.position = { ...targetPos };
+                    enemy.pathIndex++;
+                } else {
+                    enemy.position.x += (dx / distance) * enemy.speed * deltaTime / 1000;
+                    enemy.position.y += (dy / distance) * enemy.speed * deltaTime / 1000;
+                }
+            }
+        });
+        
+        // Update particles
+        gameState.particles = gameState.particles.filter(particle => {
+            const age = timestamp - particle.createdAt;
+            if (age > particle.lifetime) return false;
+            
+            particle.position.x += particle.velocity.x * deltaTime;
+            particle.position.y += particle.velocity.y * deltaTime;
+            return true;
+        });
+        
+        // Update projectiles
+        gameState.projectiles = gameState.projectiles.filter(projectile => {
+            return timestamp - projectile.createdAt < projectile.lifetime;
+        });
+        
+        // Process tower attacks
+        gameState.towers.forEach(tower => {
+            if (timestamp - tower.lastAttackTime > 1000 / tower.attackSpeed) {
+                const enemiesInRange = gameState.enemies.filter(enemy => {
+                    const dx = enemy.position.x - tower.position.x;
+                    const dy = enemy.position.y - tower.position.y;
+                    return Math.sqrt(dx * dx + dy * dy) <= tower.range;
+                });
+                
+                if (enemiesInRange.length > 0) {
+                    const target = enemiesInRange[0];
+                    
+                    // Create projectile
+                    gameState.projectiles.push({
+                        startPos: tower.position,
+                        targetPos: target.position,
+                        createdAt: timestamp,
+                        lifetime: 300 // 300ms travel time
+                    });
+                    
+                    // Apply damage
+                    target.currentHealth -= tower.damage;
+                    tower.lastAttackTime = timestamp;
+                    
+                    // Create death particles if enemy dies
+                    if (target.currentHealth <= 0) {
+                        const cellSize = canvas.width / 20;
+                        const centerX = (target.position.x + 0.5) * cellSize;
+                        const centerY = (target.position.y + 0.5) * cellSize;
+                        
+                        // Create explosion particles
+                        for (let i = 0; i < 12; i++) {
+                            const angle = (Math.PI * 2 * i) / 12;
+                            gameState.particles.push(
+                                createParticle(
+                                    ParticleTypes.DEATH_PARTICLE,
+                                    { x: centerX, y: centerY },
+                                    angle
+                                )
+                            );
+                        }
+                    }
+                }
+            }
+        });
+        
+        // Remove dead enemies
+        gameState.enemies = gameState.enemies.filter(enemy => enemy.currentHealth > 0);
+    }
+    
+    // Render game state
+    renderGrid(ctx, gameState.grid);
+    renderProjectiles(ctx, gameState.projectiles);
+    renderEnemies(ctx, gameState.enemies);
+    renderTowers(ctx, gameState.towers);
+    renderParticles(ctx, gameState.particles);
+    renderUI(ctx, gameState);
+    
+    // Request next frame
+    requestAnimationFrame(gameLoop);
+}
+
+// Start the game
+generatePath(gameState.grid).then(path => {
+    gameState.path = path;
+    enemiesRemaining = Math.floor(Math.random() * 26) + 5; // 5-30 enemies
+    initializeEventListeners();
+    requestAnimationFrame(gameLoop);
+});
+
+function startCombat() {
+    if (gameState.phase === GamePhase.PLACEMENT && gameState.towers.length > 0) {
+        gameState.phase = GamePhase.COMBAT;
+        document.getElementById('startCombat').disabled = true;
+        
+        // Disable tower palette
+        document.querySelectorAll('.tower-option').forEach(option => {
+            option.draggable = false;
+            option.style.cursor = 'not-allowed';
+            option.style.opacity = '0.5';
+        });
+    }
+}
+
+// Add event listeners for tower placement
+function initializeEventListeners() {
+    // Tower palette drag events
+    document.querySelectorAll('.tower-option').forEach(option => {
+        option.addEventListener('dragstart', (e) => {
+            draggedTowerType = e.target.dataset.towerType;
+            e.dataTransfer.setData('text/plain', ''); // Required for Firefox
+        });
+        
+        option.addEventListener('dragend', () => {
+            draggedTowerType = null;
+            hoverCell = null;
+        });
+    });
+
+    // Canvas drag events
+    canvas.addEventListener('dragover', (e) => {
+        e.preventDefault();
+        const rect = canvas.getBoundingClientRect();
+        const x = Math.floor((e.clientX - rect.left) / (canvas.width / 20));
+        const y = Math.floor((e.clientY - rect.top) / (canvas.height / 20));
+        
+        if (x >= 0 && x < 20 && y >= 0 && y < 20) {
+            hoverCell = { x, y };
+        } else {
+            hoverCell = null;
+        }
+    });
+
+    canvas.addEventListener('dragleave', () => {
+        hoverCell = null;
+    });
+
+    canvas.addEventListener('drop', (e) => {
+        e.preventDefault();
+        if (!draggedTowerType || !hoverCell) return;
+
+        const tower = TowerTypes[draggedTowerType];
+        if (
+            gameState.grid[hoverCell.y][hoverCell.x] === 'empty' &&
+            gameState.playerCurrency >= tower.cost
+        ) {
+            gameState.grid[hoverCell.y][hoverCell.x] = 'tower';
+            gameState.towers.push(createTower(draggedTowerType, { ...hoverCell }));
+            gameState.playerCurrency -= tower.cost;
+        }
+        
+        draggedTowerType = null;
+        hoverCell = null;
+    });
+
+    // Replace the space key event with button click
+    document.getElementById('startCombat').addEventListener('click', startCombat);
+    
+    // Update button state when towers are placed
+    const updateStartButton = () => {
+        const button = document.getElementById('startCombat');
+        button.disabled = gameState.towers.length === 0;
+    };
+    
+    // Add tower placement observer
+    const originalPush = gameState.towers.push;
+    gameState.towers.push = function(...args) {
+        const result = originalPush.apply(this, args);
+        updateStartButton();
+        return result;
+    };
+    
+    // Initial button state
+    updateStartButton();
+} 
\ No newline at end of file
diff --git a/html/tower/js/gameState.js b/html/tower/js/gameState.js
new file mode 100644
index 0000000..a1ab765
--- /dev/null
+++ b/html/tower/js/gameState.js
@@ -0,0 +1,82 @@
+const GamePhase = {
+    PLACEMENT: 'placement',
+    COMBAT: 'combat'
+};
+
+const TowerTypes = {
+    BASIC: {
+        name: 'Basic Tower',
+        cost: 20,
+        range: 3,
+        damage: 1,
+        attackSpeed: 1,
+        color: '#4a90e2'
+    },
+    SNIPER: {
+        name: 'Sniper Tower',
+        cost: 40,
+        range: 6,
+        damage: 2,
+        attackSpeed: 0.5,
+        color: '#9b59b6'
+    },
+    RAPID: {
+        name: 'Rapid Tower',
+        cost: 35,
+        range: 2,
+        damage: 0.5,
+        attackSpeed: 2,
+        color: '#2ecc71'
+    }
+};
+
+const ParticleTypes = {
+    DEATH_PARTICLE: {
+        lifetime: 1000, // milliseconds
+        speed: 0.1,
+        colors: ['#e74c3c', '#c0392b', '#f1c40f', '#e67e22']
+    },
+    PROJECTILE: {
+        lifetime: 300,
+        speed: 0.3,
+        color: '#ffffff'
+    }
+};
+
+function createTower(type, position) {
+    return {
+        ...TowerTypes[type],
+        position,
+        lastAttackTime: 0
+    };
+}
+
+function createEnemy(startPosition) {
+    return {
+        position: { ...startPosition },
+        currentHealth: Math.floor(Math.random() * 5) + 2, // 2-6 health
+        maxHealth: this.currentHealth,
+        speed: 1 + Math.random() * 0.5, // 1-1.5 speed
+        pathIndex: 0
+    };
+}
+
+function createParticle(type, position, angle) {
+    return {
+        position: { ...position },
+        velocity: {
+            x: Math.cos(angle) * type.speed,
+            y: Math.sin(angle) * type.speed
+        },
+        color: Array.isArray(type.colors) 
+            ? type.colors[Math.floor(Math.random() * type.colors.length)]
+            : type.color,
+        createdAt: performance.now(),
+        lifetime: type.lifetime,
+        size: 3 + Math.random() * 2
+    };
+}
+
+// Add to gameState object
+gameState.particles = [];
+gameState.projectiles = []; 
\ No newline at end of file
diff --git a/html/tower/js/path.js b/html/tower/js/path.js
new file mode 100644
index 0000000..f594ee2
--- /dev/null
+++ b/html/tower/js/path.js
@@ -0,0 +1,83 @@
+// Path generation using a modified depth-first search algorithm
+function generatePath(grid) {
+    const width = grid[0].length;
+    const height = grid.length;
+    
+    // Pick random start point on left edge
+    const startY = Math.floor(Math.random() * height);
+    let currentPos = { x: 0, y: startY };
+    
+    // Initialize path with start position
+    const path = [currentPos];
+    grid[startY][0] = 'path';
+    
+    function getValidMoves(pos) {
+        const moves = [];
+        const directions = [
+            { x: 1, y: 0 },  // right
+            { x: 0, y: -1 }, // up
+            { x: 0, y: 1 }   // down
+        ];
+        
+        for (const dir of directions) {
+            const newX = pos.x + dir.x;
+            const newY = pos.y + dir.y;
+            
+            // Check bounds
+            if (newX < 0 || newX >= width || newY < 0 || newY >= height) {
+                continue;
+            }
+            
+            // Check if cell is empty and not adjacent to path (except previous cell)
+            if (grid[newY][newX] === 'empty' && !hasAdjacentPath(newX, newY, grid)) {
+                moves.push({ x: newX, y: newY });
+            }
+        }
+        
+        return moves;
+    }
+    
+    function hasAdjacentPath(x, y, grid) {
+        const adjacentCells = [
+            { x: x, y: y - 1 },     // up
+            { x: x, y: y + 1 },     // down
+            { x: x - 1, y: y },     // left
+            { x: x + 1, y: y },     // right
+        ];
+        
+        return adjacentCells.some(cell => {
+            if (cell.x < 0 || cell.x >= width || cell.y < 0 || cell.y >= height) {
+                return false;
+            }
+            return grid[cell.y][cell.x] === 'path' && 
+                   !path.some(p => p.x === cell.x && p.y === cell.y);
+        });
+    }
+    
+    // Generate path until we reach the right edge
+    while (currentPos.x < width - 1) {
+        const moves = getValidMoves(currentPos);
+        
+        if (moves.length === 0) {
+            // If no valid moves, backtrack
+            if (path.length <= 1) {
+                // Start over if we can't backtrack
+                return generatePath(grid);
+            }
+            
+            path.pop();
+            const lastPos = path[path.length - 1];
+            grid[currentPos.y][currentPos.x] = 'empty';
+            currentPos = lastPos;
+            continue;
+        }
+        
+        // Choose random valid move
+        const nextMove = moves[Math.floor(Math.random() * moves.length)];
+        currentPos = nextMove;
+        path.push(currentPos);
+        grid[currentPos.y][currentPos.x] = 'path';
+    }
+    
+    return Promise.resolve(path);
+} 
\ No newline at end of file
diff --git a/html/tower/js/renderer.js b/html/tower/js/renderer.js
new file mode 100644
index 0000000..10444aa
--- /dev/null
+++ b/html/tower/js/renderer.js
@@ -0,0 +1,199 @@
+function renderGrid(ctx, grid) {
+    const cellSize = canvas.width / 20;
+    
+    // Draw grid lines
+    ctx.strokeStyle = '#ccc';
+    ctx.lineWidth = 1;
+    
+    for (let i = 0; i <= 20; i++) {
+        // Vertical lines
+        ctx.beginPath();
+        ctx.moveTo(i * cellSize, 0);
+        ctx.lineTo(i * cellSize, canvas.height);
+        ctx.stroke();
+        
+        // Horizontal lines
+        ctx.beginPath();
+        ctx.moveTo(0, i * cellSize);
+        ctx.lineTo(canvas.width, i * cellSize);
+        ctx.stroke();
+    }
+    
+    // Draw cells
+    grid.forEach((row, y) => {
+        row.forEach((cell, x) => {
+            if (cell === 'path') {
+                ctx.fillStyle = '#f4a460';
+                ctx.fillRect(x * cellSize, y * cellSize, cellSize, cellSize);
+            }
+        });
+    });
+
+    // Draw hover preview
+    if (gameState.phase === GamePhase.PLACEMENT && draggedTowerType && hoverCell) {
+        const tower = TowerTypes[draggedTowerType];
+        const canPlace = grid[hoverCell.y][hoverCell.x] === 'empty' &&
+                        gameState.playerCurrency >= tower.cost;
+        
+        ctx.fillStyle = canPlace ? tower.color + '80' : 'rgba(255, 0, 0, 0.3)';
+        ctx.fillRect(
+            hoverCell.x * cellSize,
+            hoverCell.y * cellSize,
+            cellSize,
+            cellSize
+        );
+        
+        // Draw range preview
+        ctx.beginPath();
+        ctx.arc(
+            (hoverCell.x + 0.5) * cellSize,
+            (hoverCell.y + 0.5) * cellSize,
+            tower.range * cellSize,
+            0,
+            Math.PI * 2
+        );
+        ctx.strokeStyle = canPlace ? tower.color + '40' : 'rgba(255, 0, 0, 0.2)';
+        ctx.stroke();
+    }
+}
+
+function renderEnemies(ctx, enemies) {
+    const cellSize = canvas.width / 20;
+    
+    enemies.forEach(enemy => {
+        // Draw enemy health bar
+        const healthPercent = enemy.currentHealth / enemy.maxHealth;
+        const barWidth = cellSize * 0.8;
+        const barHeight = 4;
+        
+        ctx.fillStyle = '#e74c3c';
+        ctx.fillRect(
+            (enemy.position.x + 0.1) * cellSize,
+            (enemy.position.y - 0.1) * cellSize - barHeight,
+            barWidth,
+            barHeight
+        );
+        
+        ctx.fillStyle = '#2ecc71';
+        ctx.fillRect(
+            (enemy.position.x + 0.1) * cellSize,
+            (enemy.position.y - 0.1) * cellSize - barHeight,
+            barWidth * healthPercent,
+            barHeight
+        );
+        
+        // Draw enemy
+        ctx.fillStyle = 'red';
+        ctx.beginPath();
+        ctx.arc(
+            (enemy.position.x + 0.5) * cellSize,
+            (enemy.position.y + 0.5) * cellSize,
+            cellSize / 3,
+            0,
+            Math.PI * 2
+        );
+        ctx.fill();
+    });
+}
+
+function renderUI(ctx, gameState) {
+    ctx.fillStyle = 'black';
+    ctx.font = '20px Arial';
+    ctx.fillText(`Currency: ${gameState.playerCurrency}`, 10, 30);
+    ctx.fillText(`Phase: ${gameState.phase}`, 10, 60);
+}
+
+function renderTowers(ctx, towers) {
+    const cellSize = canvas.width / 20;
+    
+    towers.forEach(tower => {
+        // Draw tower body
+        ctx.fillStyle = tower.color;
+        ctx.fillRect(
+            tower.position.x * cellSize + cellSize * 0.1,
+            tower.position.y * cellSize + cellSize * 0.1,
+            cellSize * 0.8,
+            cellSize * 0.8
+        );
+        
+        // Draw range indicator (only during placement phase)
+        if (gameState.phase === GamePhase.PLACEMENT) {
+            ctx.beginPath();
+            ctx.arc(
+                (tower.position.x + 0.5) * cellSize,
+                (tower.position.y + 0.5) * cellSize,
+                tower.range * cellSize,
+                0,
+                Math.PI * 2
+            );
+            ctx.strokeStyle = tower.color + '40'; // 40 is hex for 25% opacity
+            ctx.stroke();
+        }
+    });
+}
+
+// Add new render function for particles
+function renderParticles(ctx, particles) {
+    particles.forEach(particle => {
+        const age = performance.now() - particle.createdAt;
+        const lifePercent = age / particle.lifetime;
+        
+        if (lifePercent <= 1) {
+            ctx.globalAlpha = 1 - lifePercent;
+            ctx.fillStyle = particle.color;
+            ctx.beginPath();
+            ctx.arc(
+                particle.position.x,
+                particle.position.y,
+                particle.size * (1 - lifePercent),
+                0,
+                Math.PI * 2
+            );
+            ctx.fill();
+        }
+    });
+    ctx.globalAlpha = 1;
+}
+
+// Add new render function for projectiles
+function renderProjectiles(ctx, projectiles) {
+    const cellSize = canvas.width / 20;
+    
+    projectiles.forEach(projectile => {
+        const age = performance.now() - projectile.createdAt;
+        const progress = age / projectile.lifetime;
+        
+        if (progress <= 1) {
+            // Draw projectile trail
+            ctx.beginPath();
+            ctx.moveTo(
+                projectile.startPos.x * cellSize + cellSize / 2,
+                projectile.startPos.y * cellSize + cellSize / 2
+            );
+            
+            const currentX = projectile.startPos.x + (projectile.targetPos.x - projectile.startPos.x) * progress;
+            const currentY = projectile.startPos.y + (projectile.targetPos.y - projectile.startPos.y) * progress;
+            
+            ctx.lineTo(
+                currentX * cellSize + cellSize / 2,
+                currentY * cellSize + cellSize / 2
+            );
+            
+            ctx.strokeStyle = '#fff';
+            ctx.lineWidth = 2;
+            ctx.stroke();
+            
+            // Draw projectile head
+            ctx.beginPath();
+            ctx.arc(
+                currentX * cellSize + cellSize / 2,
+                currentY * cellSize + cellSize / 2,
+                4,
+                0,
+                Math.PI * 2
+            );
+            ctx.fillStyle = '#fff';
+            ctx.fill();
+        }
+    });
+} 
\ No newline at end of file