about summary refs log tree commit diff stats
path: root/html/plains
diff options
context:
space:
mode:
authorelioat <elioat@tilde.institute>2024-12-13 08:14:25 -0500
committerelioat <elioat@tilde.institute>2024-12-13 08:14:25 -0500
commit001c721b39cebf3cccce884cb38211b8833a1c12 (patch)
tree831ba3491e088a5a0dee5fe59a67da32a08cfb76 /html/plains
parent012c8f7e62ad28d649eedef3591afc57232254c6 (diff)
downloadtour-001c721b39cebf3cccce884cb38211b8833a1c12.tar.gz
*
Diffstat (limited to 'html/plains')
-rw-r--r--html/plains/game.js634
1 files changed, 507 insertions, 127 deletions
diff --git a/html/plains/game.js b/html/plains/game.js
index 6054455..cd37bd1 100644
--- a/html/plains/game.js
+++ b/html/plains/game.js
@@ -1,19 +1,60 @@
 const CONFIG = {
+    display: {
+        fps: 60,
+        grid: {
+            size: 100,
+            color: '#ddd'
+        },
+        camera: {
+            deadzoneMultiplierX: 0.6,
+            deadzoneMultiplierY: 0.6,
+            ease: 0.08
+        }
+    },
+    effects: {
+        colors: {
+            primary: '#4169E1',
+            secondary: '#1E90FF',
+            tertiary: '#0000CD',
+            glow: 'rgba(30, 144, 255, 0.25)',
+            inner: '#0000CD'
+        }
+    },
     player: {
         size: 30,
         speed: 5,
         sprintMultiplier: 2,
-        color: '#111'
+        color: '#111',
+        strafeKey: ' ',
+        directionIndicator: {
+            size: 10,
+            color: 'rgba(32, 178, 170, 1)'
+        },
+        dash: {
+            duration: 3000,    // 3 seconds of use
+            cooldown: 1000,    // 1 second cooldown
+            exhaustedAt: 0     // Track when dash was exhausted
+        }
     },
     sword: {
         length: 60,
         swingSpeed: 0.6,
-        colors: {
-            primary: '#4169E1',
-            secondary: '#1E90FF',
-            tertiary: '#0000CD',
-            glow: 'rgba(30, 144, 255, 0.3)'
-        }
+        colors: null
+    },
+    bubble: {
+        size: 20,
+        speed: 8,
+        lifetime: 800,
+        cooldown: 1000,
+        arcWidth: Math.PI / 3,
+        colors: null,
+        particleEmitRate: 0.3,
+        fadeExponent: 2.5
+    },
+    bubbleParticle: {
+        lifetime: 700,
+        speedMultiplier: 0.3,
+        size: 3
     },
     defense: {
         numLayers: 6,
@@ -23,46 +64,43 @@ const CONFIG = {
         orbitRadiusMultiplier: 0.8,
         rotationSpeed: 1.5
     },
-    particles: {
-        max: 100,
-        lifetime: 1.0,
-        speed: 1.5
-    },
     footprints: {
         lifetime: 1000,
         spacing: 300,
         size: 5
-    },
-    camera: {
-        deadzoneMultiplierX: 0.6,
-        deadzoneMultiplierY: 0.6,
-        ease: 0.08
-    },
-    grid: {
-        size: 100,
-        color: '#ddd'
-    },
-    fps: 60
+    }
 };
 
+// Set references to shared colors
+CONFIG.sword.colors = CONFIG.effects.colors;
+CONFIG.bubble.colors = CONFIG.effects.colors;
+
 let GAME_WIDTH = window.innerWidth;
 let GAME_HEIGHT = window.innerHeight;
 
 let lastFrameTime = 0;
 let animationTime = 0;
 
-const FRAME_TIME = 1000 / CONFIG.fps;
-const CAMERA_DEADZONE_X = GAME_WIDTH * CONFIG.camera.deadzoneMultiplierX;
-const CAMERA_DEADZONE_Y = GAME_HEIGHT * CONFIG.camera.deadzoneMultiplierY;
+const FRAME_TIME = 1000 / CONFIG.display.fps;
+const CAMERA_DEADZONE_X = GAME_WIDTH * CONFIG.display.camera.deadzoneMultiplierX;
+const CAMERA_DEADZONE_Y = GAME_HEIGHT * CONFIG.display.camera.deadzoneMultiplierY;
 
-const state = {
+const createInitialState = () => ({
     player: {
-        x: GAME_WIDTH / 2,
-        y: GAME_HEIGHT / 2,
+        x: window.innerWidth / 2,
+        y: window.innerHeight / 2,
         isDefending: false,
         direction: { x: 0, y: -1 },
         swordAngle: 0,
-        isSwinging: false
+        isSwinging: false,
+        equipment: 'sword',
+        bubbles: [],
+        bubbleParticles: [],
+        lastBubbleTime: 0,
+        dashStartTime: 0,      // When the current dash started
+        isDashing: false,      // Currently dashing?
+        dashExhausted: false,  // Is dash on cooldown?
+        lastDashEnd: 0         // When the last dash ended
     },
     particles: [],
     footprints: [],
@@ -73,117 +111,301 @@ const state = {
         targetX: 0,
         targetY: 0
     }
-};
+});
+
+let state = createInitialState();
 
 const canvas = document.getElementById('gameCanvas');
 const ctx = canvas.getContext('2d');
 
 const keys = new Set();
 
-const handleKeyDown = (e) => {
-    keys.add(e.key);
-    
-    if (e.key === 'z' && !state.player.isSwinging && !state.player.isDefending) {
-        state.player.isSwinging = true;
-        state.player.swordAngle = Math.atan2(state.player.direction.y, state.player.direction.x) - Math.PI / 2;
-    }
-    
-    if (e.key === 'x') {
-        state.player.isDefending = true;
+const inputHandlers = {
+    handleAttack: (state, animationTime) => {
+        if (state.player.isDefending) return state;
+        
+        if (state.player.equipment === 'sword' && !state.player.isSwinging) {
+            return {
+                ...state,
+                player: {
+                    ...state.player,
+                    isSwinging: true,
+                    swordAngle: Math.atan2(state.player.direction.y, state.player.direction.x) - Math.PI / 2
+                }
+            };
+        } else if (state.player.equipment === 'unarmed') {
+            return createBubbleAttack(state, animationTime);
+        }
+        return state;
+    },
+
+    handleEquipmentSwitch: (state) => {
+        const equipment = ['sword', 'unarmed'];
+        const currentIndex = equipment.indexOf(state.player.equipment);
+        return {
+            ...state,
+            player: {
+                ...state.player,
+                equipment: equipment[(currentIndex + 1) % equipment.length]
+            }
+        };
     }
 };
 
-const handleKeyUp = (e) => {
-    keys.delete(e.key);
-    if (e.key === 'x') {
-        state.player.isDefending = false;
-    }
+const updateBubble = (bubble, animationTime) => {
+    const age = animationTime - bubble.createdAt;
+    const ageRatio = age / CONFIG.bubble.lifetime;
+    const speedMultiplier = Math.pow(1 - ageRatio, 0.5);
+    
+    return {
+        ...bubble,
+        x: bubble.x + bubble.dx * speedMultiplier,
+        y: bubble.y + bubble.dy * speedMultiplier
+    };
 };
 
-const updatePlayer = () => {
-    if (state.player.isDefending) {
-        return;
+const generateBubbleParticles = (bubble, animationTime) => {
+    const age = animationTime - bubble.createdAt;
+    const ageRatio = age / CONFIG.bubble.lifetime;
+    
+    if (Math.random() >= CONFIG.bubble.particleEmitRate * (1 - ageRatio)) {
+        return [];
     }
     
-    let dx = 0;
-    let dy = 0;
+    const trailDistance = Math.random() * 20;
+    const particleX = bubble.x - bubble.dx * (trailDistance / CONFIG.bubble.speed);
+    const particleY = bubble.y - bubble.dy * (trailDistance / CONFIG.bubble.speed);
     
-    if (keys.has('ArrowLeft')) dx -= 1;
-    if (keys.has('ArrowRight')) dx += 1;
-    if (keys.has('ArrowUp')) dy -= 1;
-    if (keys.has('ArrowDown')) dy += 1;
+    const particleAngle = bubble.angle + (Math.random() - 0.5) * CONFIG.bubble.arcWidth * 2;
+    const spreadSpeed = CONFIG.bubble.speed * 0.2 * (1 - ageRatio);
+    const spreadAngle = Math.random() * Math.PI * 2;
+    const speedMultiplier = Math.pow(1 - ageRatio, 0.5);
     
-    if (dx !== 0 || dy !== 0) {
-        const length = Math.sqrt(dx * dx + dy * dy);
-        state.player.direction = { x: dx / length, y: dy / length };
-        
-        const currentSpeed = keys.has('Shift') ? 
-            CONFIG.player.speed * CONFIG.player.sprintMultiplier : 
-            CONFIG.player.speed;
+    return [{
+        x: particleX,
+        y: particleY,
+        dx: (Math.cos(particleAngle) * CONFIG.bubble.speed * CONFIG.bubbleParticle.speedMultiplier * speedMultiplier) +
+            (Math.cos(spreadAngle) * spreadSpeed),
+        dy: (Math.sin(particleAngle) * CONFIG.bubble.speed * CONFIG.bubbleParticle.speedMultiplier * speedMultiplier) +
+            (Math.sin(spreadAngle) * spreadSpeed),
+        size: CONFIG.bubbleParticle.size * (0.5 + Math.random() * 0.5),
+        createdAt: animationTime
+    }];
+};
+
+const updateBubbleParticles = (particles, animationTime) => {
+    return particles.filter(particle => {
+        const age = animationTime - particle.createdAt;
+        return age < CONFIG.bubbleParticle.lifetime;
+    }).map(particle => ({
+        ...particle,
+        x: particle.x + particle.dx,
+        y: particle.y + particle.dy
+    }));
+};
+
+const createBubbleAttack = (state, animationTime) => {
+    const timeSinceLastBubble = animationTime - state.player.lastBubbleTime;
+    if (timeSinceLastBubble < CONFIG.bubble.cooldown) return state;
+    
+    const angle = Math.atan2(state.player.direction.y, state.player.direction.x);
+    const bubble = {
+        x: state.player.x,
+        y: state.player.y,
+        dx: state.player.direction.x * CONFIG.bubble.speed,
+        dy: state.player.direction.y * CONFIG.bubble.speed,
+        angle: angle,
+        createdAt: animationTime,
+        size: CONFIG.bubble.size * (0.8 + Math.random() * 0.4)
+    };
+    
+    return {
+        ...state,
+        player: {
+            ...state.player,
+            bubbles: [...state.player.bubbles, bubble],
+            lastBubbleTime: animationTime
+        }
+    };
+};
+
+const weaponSystems = {
+    updateBubbles: (state, animationTime) => {
+        const updatedBubbles = state.player.bubbles
+            .filter(bubble => animationTime - bubble.createdAt < CONFIG.bubble.lifetime)
+            .map(bubble => updateBubble(bubble, animationTime));
+            
+        const newParticles = updatedBubbles
+            .flatMap(bubble => generateBubbleParticles(bubble, animationTime));
+            
+        return {
+            ...state,
+            player: {
+                ...state.player,
+                bubbles: updatedBubbles,
+                bubbleParticles: [
+                    ...updateBubbleParticles(state.player.bubbleParticles, animationTime),
+                    ...newParticles
+                ]
+            }
+        };
+    },
+
+    updateSwordSwing: (state, animationTime) => {
+        if (!state.player.isSwinging) return state;
         
-        state.player.x += (dx / length) * currentSpeed;
-        state.player.y += (dy / length) * currentSpeed;
+        const newAngle = state.player.swordAngle + CONFIG.sword.swingSpeed;
+        const swingComplete = newAngle > Math.atan2(state.player.direction.y, state.player.direction.x) + Math.PI / 2;
         
-        if (!state.player.isDefending) {
-            const timeSinceLastFootprint = animationTime - state.lastFootprintTime;
-            const currentSpacing = keys.has('Shift') ? 
-                CONFIG.footprints.spacing * CONFIG.player.sprintMultiplier : 
-                CONFIG.footprints.spacing;
-                
-            if (timeSinceLastFootprint > currentSpacing / currentSpeed) {
-                const offset = (Math.random() - 0.5) * 6;
-                const perpX = -state.player.direction.y * offset;
-                const perpY = state.player.direction.x * offset;
-                
-                state.footprints.push(createFootprint(
-                    state.player.x + perpX,
-                    state.player.y + perpY,
-                    Math.atan2(dy, dx)
-                ));
-                state.lastFootprintTime = animationTime;
+        return {
+            ...state,
+            player: {
+                ...state.player,
+                swordAngle: newAngle,
+                isSwinging: !swingComplete
             }
-        }
+        };
     }
-    
-    state.footprints = state.footprints.filter(footprint => {
-        return (animationTime - footprint.createdAt) < CONFIG.footprints.lifetime;
-    });
-    
-    if (state.player.isSwinging) {
-        state.player.swordAngle += CONFIG.sword.swingSpeed;
+};
+
+const movementSystem = {
+    updatePosition: (state, keys) => {
+        if (state.player.isDefending) return state;
         
-        if (Math.random() < 0.3) {
-            const tipX = state.player.x + Math.cos(state.player.swordAngle) * CONFIG.sword.length;
-            const tipY = state.player.y + Math.sin(state.player.swordAngle) * CONFIG.sword.length;
-            state.particles.push(createParticle(tipX, tipY, state.player.swordAngle));
+        const movement = calculateMovement(keys);
+        if (!movement.moving) {
+            // Reset dash when not moving
+            return {
+                ...state,
+                player: {
+                    ...state.player,
+                    isDashing: false,
+                    dashStartTime: 0
+                }
+            };
         }
         
-        if (state.player.swordAngle > Math.atan2(state.player.direction.y, state.player.direction.x) + Math.PI / 2) {
-            state.player.isSwinging = false;
+        const wantsToDash = keys.has('Shift');
+        const canDash = !state.player.dashExhausted && 
+            (animationTime - state.player.lastDashEnd) >= CONFIG.player.dash.cooldown;
+        
+        let isDashing = false;
+        let dashExhausted = state.player.dashExhausted;
+        let dashStartTime = state.player.dashStartTime;
+        let lastDashEnd = state.player.lastDashEnd;
+        
+        if (wantsToDash && canDash) {
+            if (!state.player.isDashing) {
+                dashStartTime = animationTime;
+            }
+            isDashing = true;
+            
+            // Check if dash duration exceeded
+            if (animationTime - dashStartTime >= CONFIG.player.dash.duration) {
+                isDashing = false;
+                dashExhausted = true;
+                lastDashEnd = animationTime;
+            }
+        } else if (state.player.dashExhausted && 
+            (animationTime - state.player.lastDashEnd >= CONFIG.player.dash.cooldown)) {
+            dashExhausted = false;
         }
+        
+        const speed = isDashing ? 
+            CONFIG.player.speed * CONFIG.player.sprintMultiplier : 
+            CONFIG.player.speed;
+            
+        // Handle footprint creation
+        const timeSinceLastFootprint = animationTime - state.lastFootprintTime;
+        const currentSpacing = isDashing ? 
+            CONFIG.footprints.spacing * CONFIG.player.sprintMultiplier : 
+            CONFIG.footprints.spacing;
+            
+        let newFootprints = state.footprints;
+        if (timeSinceLastFootprint > currentSpacing / speed) {
+            const offset = (Math.random() - 0.5) * 6;
+            const perpX = -movement.direction.y * offset;
+            const perpY = movement.direction.x * offset;
+            
+            newFootprints = [...state.footprints, createFootprint(
+                state.player.x + perpX,
+                state.player.y + perpY,
+                Math.atan2(movement.dy, movement.dx)
+            )];
+        }
+            
+        return {
+            ...state,
+            player: {
+                ...state.player,
+                x: state.player.x + movement.dx * speed,
+                y: state.player.y + movement.dy * speed,
+                direction: movement.direction,
+                isDashing,
+                dashStartTime,
+                dashExhausted,
+                lastDashEnd
+            },
+            footprints: newFootprints,
+            lastFootprintTime: timeSinceLastFootprint > currentSpacing / speed ? 
+                animationTime : state.lastFootprintTime
+        };
+    }
+};
+
+const handleKeyDown = (e) => {
+    keys.add(e.key);
+    
+    if (e.key === 'z' && !state.player.isDefending) {
+        Object.assign(state, inputHandlers.handleAttack(state, animationTime));
     }
     
-    state.particles = state.particles.filter(particle => {
-        particle.lifetime -= 1/60;
-        if (particle.lifetime <= 0) return false;
-        
-        particle.x += Math.cos(particle.angle) * particle.speed;
-        particle.y += Math.sin(particle.angle) * particle.speed;
-        return true;
-    });
+    if (e.key === 'e') {
+        Object.assign(state, inputHandlers.handleEquipmentSwitch(state));
+    }
     
-    if (state.particles.length > CONFIG.particles.max) {
-        state.particles.splice(0, state.particles.length - CONFIG.particles.max);
+    if (e.key === 'x') {
+        Object.assign(state, {
+            ...state,
+            player: {
+                ...state.player,
+                isDefending: true
+            }
+        });
+    }
+};
+
+const handleKeyUp = (e) => {
+    keys.delete(e.key);
+    if (e.key === 'x') {
+        Object.assign(state, {
+            ...state,
+            player: {
+                ...state.player,
+                isDefending: false
+            }
+        });
     }
 };
 
+const updatePlayer = () => {
+    Object.assign(state, weaponSystems.updateBubbles(state, animationTime));
+    Object.assign(state, weaponSystems.updateSwordSwing(state, animationTime));
+    Object.assign(state, movementSystem.updatePosition(state, keys));
+    
+    state.footprints = state.footprints.filter(footprint => {
+        return (animationTime - footprint.createdAt) < CONFIG.footprints.lifetime;
+    });
+};
+
 const createParticle = (x, y, angle) => ({
     x,
     y,
     angle,
-    lifetime: CONFIG.particles.lifetime,
-    speed: CONFIG.particles.speed * (0.5 + Math.random() * 0.5),
-    size: 2 + Math.random() * 2
+    createdAt: animationTime,
+    lifetime: CONFIG.swordParticles.lifetime,
+    speed: CONFIG.swordParticles.speed * (0.5 + Math.random() * 0.5),
+    size: CONFIG.swordParticles.size.min + Math.random() * (CONFIG.swordParticles.size.max - CONFIG.swordParticles.size.min)
 });
 
 const createFootprint = (x, y, direction) => ({
@@ -198,7 +420,80 @@ const createFootprint = (x, y, direction) => ({
 const renderPlayer = () => {
     ctx.save();
     
-    if (state.player.isSwinging) {
+    // Render bubble particles
+    state.player.bubbleParticles.forEach(particle => {
+        const age = (animationTime - particle.createdAt) / CONFIG.bubbleParticle.lifetime;
+        const alpha = (1 - age) * 0.8;
+        
+        // Draw outer glow
+        ctx.fillStyle = `rgba(65, 105, 225, ${alpha * 0.3})`; // Royal Blue
+        ctx.beginPath();
+        ctx.arc(particle.x, particle.y, particle.size * 2.5, 0, Math.PI * 2);
+        ctx.fill();
+        
+        // Draw core
+        ctx.fillStyle = `rgba(30, 144, 255, ${alpha})`; // Dodger Blue
+        ctx.beginPath();
+        ctx.arc(particle.x, particle.y, particle.size * 1.5, 0, Math.PI * 2);
+        ctx.fill();
+    });
+    
+    // Render bubbles with adjusted fade
+    state.player.bubbles.forEach(bubble => {
+        const age = (animationTime - bubble.createdAt) / CONFIG.bubble.lifetime;
+        const alpha = Math.pow(1 - age, CONFIG.bubble.fadeExponent);
+        const expandedSize = bubble.size * (1 + age * 2);
+        
+        ctx.save();
+        ctx.translate(bubble.x, bubble.y);
+        ctx.rotate(bubble.angle);
+        
+        // Draw outer glow with adjusted fade
+        ctx.beginPath();
+        ctx.arc(0, 0, expandedSize * 1.5, 
+            -CONFIG.bubble.arcWidth * (1 + age * 0.5),
+            CONFIG.bubble.arcWidth * (1 + age * 0.5), 
+            false
+        );
+        ctx.lineCap = 'round';
+        ctx.lineWidth = expandedSize * 0.5 * (1 - age * 0.3);
+        ctx.strokeStyle = CONFIG.bubble.colors.glow.replace(')', `, ${alpha * 0.5})`);
+        ctx.stroke();
+        
+        // Draw main arc with gradient
+        const gradient = ctx.createLinearGradient(
+            -expandedSize, 0,
+            expandedSize, 0
+        );
+        gradient.addColorStop(0, `rgba(65, 105, 225, ${alpha})`);
+        gradient.addColorStop(0.6, `rgba(30, 144, 255, ${alpha})`);
+        gradient.addColorStop(1, `rgba(0, 0, 205, ${alpha})`);
+        
+        ctx.beginPath();
+        ctx.arc(0, 0, expandedSize, 
+            -CONFIG.bubble.arcWidth * 0.8 * (1 + age * 0.5),
+            CONFIG.bubble.arcWidth * 0.8 * (1 + age * 0.5),
+            false
+        );
+        ctx.lineWidth = expandedSize * 0.3 * (1 - age * 0.3);
+        ctx.strokeStyle = gradient;
+        ctx.stroke();
+        
+        // Draw inner bright line
+        ctx.beginPath();
+        ctx.arc(0, 0, expandedSize * 0.9, 
+            -CONFIG.bubble.arcWidth * 0.6 * (1 + age * 0.5),
+            CONFIG.bubble.arcWidth * 0.6 * (1 + age * 0.5),
+            false
+        );
+        ctx.lineWidth = expandedSize * 0.1 * (1 - age * 0.3);
+        ctx.strokeStyle = `rgba(255, 255, 255, ${alpha * 0.8})`;
+        ctx.stroke();
+        
+        ctx.restore();
+    });
+    
+    if (state.player.isSwinging && state.player.equipment === 'sword') {
         const blurSteps = 12;
         const blurSpread = 0.2;
         
@@ -217,19 +512,6 @@ const renderPlayer = () => {
             ctx.stroke();
         }
         
-        state.particles.forEach(particle => {
-            const alpha = (particle.lifetime / CONFIG.particles.lifetime) * 0.8;
-            ctx.fillStyle = `rgba(135, 206, 250, ${alpha})`;
-            ctx.beginPath();
-            ctx.arc(particle.x, particle.y, particle.size * 1.5, 0, Math.PI * 2);
-            ctx.fill();
-            
-            ctx.fillStyle = `rgba(30, 144, 255, ${alpha * 0.3})`;
-            ctx.beginPath();
-            ctx.arc(particle.x, particle.y, particle.size * 2.5, 0, Math.PI * 2);
-            ctx.fill();
-        });
-        
         const gradient = ctx.createLinearGradient(
             state.player.x,
             state.player.y,
@@ -348,7 +630,30 @@ const renderPlayer = () => {
                 ctx.stroke();
             }
         }
+        
+        // Add direction indicator square in the center
+        const dotSize = CONFIG.player.directionIndicator.size;
+        
+        // Calculate cooldown progress (0 to 1)
+        const timeSinceLastBubble = animationTime - state.player.lastBubbleTime;
+        const cooldownProgress = Math.min(timeSinceLastBubble / CONFIG.bubble.cooldown, 1);
+        
+        // Set opacity based on cooldown (0.1 to 1)
+        const dotOpacity = getDotOpacity(state, animationTime);
+        
+        ctx.fillStyle = CONFIG.player.directionIndicator.color.replace(
+            '1)',
+            `${dotOpacity})`
+        );
+        
+        ctx.fillRect(
+            state.player.x - dotSize/2,
+            state.player.y - dotSize/2,
+            dotSize,
+            dotSize
+        );
     } else {
+        // Draw player square
         ctx.fillStyle = CONFIG.player.color;
         ctx.fillRect(
             state.player.x - CONFIG.player.size / 2,
@@ -356,6 +661,31 @@ const renderPlayer = () => {
             CONFIG.player.size,
             CONFIG.player.size
         );
+        
+        // Draw direction indicator square with cooldown opacity
+        const dotSize = CONFIG.player.directionIndicator.size;
+        const dotDistance = CONFIG.player.size / 3;
+        const dotX = state.player.x + state.player.direction.x * dotDistance;
+        const dotY = state.player.y + state.player.direction.y * dotDistance;
+        
+        // Calculate cooldown progress (0 to 1)
+        const timeSinceLastBubble = animationTime - state.player.lastBubbleTime;
+        const cooldownProgress = Math.min(timeSinceLastBubble / CONFIG.bubble.cooldown, 1);
+        
+        // Set opacity based on cooldown (0.1 to 1)
+        const dotOpacity = getDotOpacity(state, animationTime);
+        
+        ctx.fillStyle = CONFIG.player.directionIndicator.color.replace(
+            '1)',  // Replace the full opacity with our calculated opacity
+            `${dotOpacity})`
+        );
+        
+        ctx.fillRect(
+            dotX - dotSize/2,
+            dotY - dotSize/2,
+            dotSize,
+            dotSize
+        );
     }
     
     ctx.restore();
@@ -385,14 +715,14 @@ const render = () => {
         state.camera.targetY = -(state.player.y - targetOffsetY);
     }
     
-    state.camera.x = lerp(state.camera.x, state.camera.targetX, CONFIG.camera.ease);
-    state.camera.y = lerp(state.camera.y, state.camera.targetY, CONFIG.camera.ease);
+    state.camera.x = lerp(state.camera.x, state.camera.targetX, CONFIG.display.camera.ease);
+    state.camera.y = lerp(state.camera.y, state.camera.targetY, CONFIG.display.camera.ease);
     
     ctx.save();
     ctx.translate(state.camera.x, state.camera.y);
     
-    const gridSize = CONFIG.grid.size;
-    ctx.strokeStyle = CONFIG.grid.color;
+    const gridSize = CONFIG.display.grid.size;
+    ctx.strokeStyle = CONFIG.display.grid.color;
     ctx.lineWidth = 1;
     
     const startX = Math.floor((-state.camera.x) / gridSize) * gridSize;
@@ -483,4 +813,54 @@ window.addEventListener('resize', resizeCanvas);
 
 resizeCanvas();
 
-requestAnimationFrame(gameLoop);
\ No newline at end of file
+requestAnimationFrame(gameLoop);
+
+const calculateMovement = (keys) => {
+    let dx = 0;
+    let dy = 0;
+    
+    if (keys.has('ArrowLeft')) dx -= 1;
+    if (keys.has('ArrowRight')) dx += 1;
+    if (keys.has('ArrowUp')) dy -= 1;
+    if (keys.has('ArrowDown')) dy += 1;
+    
+    if (dx === 0 && dy === 0) {
+        return { moving: false };
+    }
+    
+    const length = Math.sqrt(dx * dx + dy * dy);
+    const normalizedDx = dx / length;
+    const normalizedDy = dy / length;
+    
+    const isStrafing = keys.has(CONFIG.player.strafeKey);
+    
+    return {
+        moving: true,
+        dx: normalizedDx,
+        dy: normalizedDy,
+        direction: isStrafing ? 
+            { ...state.player.direction } : // Keep current direction while strafing
+            { x: normalizedDx, y: normalizedDy } // Update direction normally
+    };
+};
+
+const getDotOpacity = (state, animationTime) => {
+    // Get bubble cooldown opacity
+    const timeSinceLastBubble = animationTime - state.player.lastBubbleTime;
+    const bubbleCooldownProgress = Math.min(timeSinceLastBubble / CONFIG.bubble.cooldown, 1);
+    const bubbleOpacity = 0.1 + (bubbleCooldownProgress * 0.9);
+    
+    // Get dash cooldown opacity
+    let dashOpacity = 1;
+    if (state.player.dashExhausted) {
+        const timeSinceExhaustion = animationTime - state.player.lastDashEnd;
+        const dashCooldownProgress = timeSinceExhaustion / CONFIG.player.dash.cooldown;
+        dashOpacity = 0.1 + (Math.min(dashCooldownProgress, 1) * 0.9);
+    } else if (state.player.isDashing) {
+        const dashProgress = (animationTime - state.player.dashStartTime) / CONFIG.player.dash.duration;
+        dashOpacity = 1 - (dashProgress * 0.7);  // Fade to 0.3 during dash
+    }
+    
+    // Return the lower opacity of the two systems
+    return Math.min(bubbleOpacity, dashOpacity);
+};
\ No newline at end of file