about summary refs log tree commit diff stats
path: root/html/plains
diff options
context:
space:
mode:
authorelioat <elioat@tilde.institute>2024-12-08 22:08:17 -0500
committerelioat <elioat@tilde.institute>2024-12-08 22:08:17 -0500
commit5ae28337d0beb3a265aa8431c91253ca37debe70 (patch)
tree219d2227d374c2aa1503780a25cdd775346f086a /html/plains
parent89a4e6e730498cac67d95952ec647f01652b86c4 (diff)
downloadtour-5ae28337d0beb3a265aa8431c91253ca37debe70.tar.gz
*
Diffstat (limited to 'html/plains')
-rw-r--r--html/plains/game.js488
-rw-r--r--html/plains/index.html26
2 files changed, 514 insertions, 0 deletions
diff --git a/html/plains/game.js b/html/plains/game.js
new file mode 100644
index 0000000..4b7b138
--- /dev/null
+++ b/html/plains/game.js
@@ -0,0 +1,488 @@
+const CONFIG = {
+    player: {
+        size: 30,
+        speed: 5,
+        sprintMultiplier: 1.5,
+        color: '#111'
+    },
+    sword: {
+        length: 60,
+        swingSpeed: 0.6,
+        colors: {
+            primary: '#4169E1',
+            secondary: '#1E90FF',
+            tertiary: '#0000CD',
+            glow: 'rgba(30, 144, 255, 0.3)'
+        }
+    },
+    defense: {
+        numLayers: 6,
+        maxRadiusMultiplier: 2,
+        baseAlpha: 0.15,
+        particleCount: 12,
+        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
+};
+
+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 state = {
+    player: {
+        x: GAME_WIDTH / 2,
+        y: GAME_HEIGHT / 2,
+        isDefending: false,
+        direction: { x: 0, y: -1 },
+        swordAngle: 0,
+        isSwinging: false
+    },
+    particles: [],
+    footprints: [],
+    lastFootprintTime: 0,
+    camera: {
+        x: 0,
+        y: 0,
+        targetX: 0,
+        targetY: 0
+    }
+};
+
+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 handleKeyUp = (e) => {
+    keys.delete(e.key);
+    if (e.key === 'x') {
+        state.player.isDefending = false;
+    }
+};
+
+const updatePlayer = () => {
+    if (state.player.isDefending) {
+        return;
+    }
+    
+    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) {
+        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;
+        
+        state.player.x += (dx / length) * currentSpeed;
+        state.player.y += (dy / length) * currentSpeed;
+        
+        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),
+                    currentSpeed
+                ));
+                state.lastFootprintTime = animationTime;
+            }
+        }
+    }
+    
+    state.footprints = state.footprints.filter(footprint => {
+        return (animationTime - footprint.createdAt) < CONFIG.footprints.lifetime;
+    });
+    
+    if (state.player.isSwinging) {
+        state.player.swordAngle += CONFIG.sword.swingSpeed;
+        
+        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));
+        }
+        
+        if (state.player.swordAngle > Math.atan2(state.player.direction.y, state.player.direction.x) + Math.PI / 2) {
+            state.player.isSwinging = false;
+        }
+    }
+    
+    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 (state.particles.length > CONFIG.particles.max) {
+        state.particles.splice(0, state.particles.length - CONFIG.particles.max);
+    }
+};
+
+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
+});
+
+const createFootprint = (x, y, direction, speed) => ({
+    x,
+    y,
+    direction,
+    createdAt: animationTime,
+    size: CONFIG.footprints.size * (0.8 + Math.random() * 0.4),
+    offset: (Math.random() - 0.5) * 5
+});
+
+const renderPlayer = () => {
+    ctx.save();
+    
+    if (state.player.isSwinging) {
+        const blurSteps = 12;
+        const blurSpread = 0.2;
+        
+        for (let i = 0; i < blurSteps; i++) {
+            const alpha = 0.35 - (i * 0.02);
+            const angleOffset = -blurSpread * i;
+            
+            ctx.strokeStyle = `rgba(30, 144, 255, ${alpha})`;
+            ctx.lineWidth = 4 + (blurSteps - i);
+            ctx.beginPath();
+            ctx.moveTo(state.player.x, state.player.y);
+            ctx.lineTo(
+                state.player.x + Math.cos(state.player.swordAngle + angleOffset) * CONFIG.sword.length,
+                state.player.y + Math.sin(state.player.swordAngle + angleOffset) * CONFIG.sword.length
+            );
+            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,
+            state.player.x + Math.cos(state.player.swordAngle) * CONFIG.sword.length,
+            state.player.y + Math.sin(state.player.swordAngle) * CONFIG.sword.length
+        );
+        gradient.addColorStop(0, CONFIG.sword.colors.primary);
+        gradient.addColorStop(0.6, CONFIG.sword.colors.secondary);
+        gradient.addColorStop(1, CONFIG.sword.colors.tertiary);
+        
+        ctx.strokeStyle = CONFIG.sword.colors.glow;
+        ctx.lineWidth = 10;
+        ctx.beginPath();
+        ctx.moveTo(state.player.x, state.player.y);
+        ctx.lineTo(
+            state.player.x + Math.cos(state.player.swordAngle) * CONFIG.sword.length,
+            state.player.y + Math.sin(state.player.swordAngle) * CONFIG.sword.length
+        );
+        ctx.stroke();
+        
+        ctx.strokeStyle = gradient;
+        ctx.lineWidth = 6;
+        ctx.stroke();
+    }
+    
+    if (state.player.isDefending) {
+        const numLayers = CONFIG.defense.numLayers;
+        const maxRadius = CONFIG.player.size * CONFIG.defense.maxRadiusMultiplier;
+        const baseAlpha = CONFIG.defense.baseAlpha;
+        
+        for (let i = numLayers - 1; i >= 0; i--) {
+            const radius = CONFIG.player.size / 2 + (maxRadius - CONFIG.player.size / 2) * (i / numLayers);
+            const alpha = baseAlpha * (1 - i / numLayers);
+            
+            const pulseOffset = Math.sin(Date.now() / 500) * 3;
+            
+            const glowGradient = ctx.createRadialGradient(
+                state.player.x, state.player.y, radius - 5,
+                state.player.x, state.player.y, radius + pulseOffset
+            );
+            glowGradient.addColorStop(0, `rgba(30, 144, 255, ${alpha})`);
+            glowGradient.addColorStop(1, 'rgba(30, 144, 255, 0)');
+            
+            ctx.beginPath();
+            ctx.arc(state.player.x, state.player.y, radius + pulseOffset, 0, Math.PI * 2);
+            ctx.fillStyle = glowGradient;
+            ctx.fill();
+        }
+        
+        const mainAuraGradient = ctx.createRadialGradient(
+            state.player.x, state.player.y, CONFIG.player.size / 2 - 2,
+            state.player.x, state.player.y, CONFIG.player.size / 2 + 8
+        );
+        mainAuraGradient.addColorStop(0, 'rgba(30, 144, 255, 0.3)');
+        mainAuraGradient.addColorStop(0.5, 'rgba(30, 144, 255, 0.2)');
+        mainAuraGradient.addColorStop(1, 'rgba(30, 144, 255, 0)');
+        
+        ctx.beginPath();
+        ctx.arc(state.player.x, state.player.y, CONFIG.player.size / 2 + 8, 0, Math.PI * 2);
+        ctx.fillStyle = mainAuraGradient;
+        ctx.fill();
+        
+        ctx.beginPath();
+        ctx.arc(state.player.x, state.player.y, CONFIG.player.size / 2, 0, Math.PI * 2);
+        ctx.fillStyle = '#111';
+        ctx.fill();
+        
+        ctx.beginPath();
+        ctx.arc(state.player.x, state.player.y, CONFIG.player.size / 2, 0, Math.PI * 2);
+        ctx.strokeStyle = CONFIG.sword.colors.secondary;
+        ctx.lineWidth = 5;
+        ctx.stroke();
+        
+        ctx.beginPath();
+        ctx.arc(state.player.x, state.player.y, CONFIG.player.size / 2 - 3, 0, Math.PI * 2);
+        ctx.strokeStyle = 'rgba(30, 144, 255, 0.3)';
+        ctx.lineWidth = 2;
+        ctx.stroke();
+        
+        const numParticles = CONFIG.defense.particleCount;
+        const baseOrbitRadius = CONFIG.player.size * CONFIG.defense.orbitRadiusMultiplier;
+        
+        const rotationSpeed = CONFIG.defense.rotationSpeed;
+        
+        for (let i = 0; i < numParticles; i++) {
+            const particleSpeed = 0.15 + Math.sin(animationTime * 0.002 + i) * 0.03;
+            const radialOffset = Math.sin(animationTime * 0.002 + i * 0.5) * 4;
+            const orbitRadius = baseOrbitRadius + radialOffset;
+            
+            const angle = (i / numParticles) * Math.PI * 2 + animationTime * rotationSpeed * 0.001;
+            const x = state.player.x + Math.cos(angle) * orbitRadius;
+            const y = state.player.y + Math.sin(angle) * orbitRadius;
+            
+            const size = 2 + Math.sin(animationTime * 0.003 + i * 0.8) * 1.5;
+            const baseAlpha = 0.6 + Math.sin(animationTime * 0.002 + i) * 0.2;
+            
+            ctx.beginPath();
+            ctx.arc(x, y, size * 2, 0, Math.PI * 2);
+            ctx.fillStyle = `rgba(30, 144, 255, ${baseAlpha * 0.3})`;
+            ctx.fill();
+            
+            ctx.beginPath();
+            ctx.arc(x, y, size, 0, Math.PI * 2);
+            ctx.fillStyle = `rgba(135, 206, 250, ${baseAlpha})`;
+            ctx.fill();
+            
+            if (i > 0) {
+                const prevAngle = ((i - 1) / numParticles) * Math.PI * 2 + animationTime * rotationSpeed * 0.001;
+                const prevX = state.player.x + Math.cos(prevAngle) * orbitRadius;
+                const prevY = state.player.y + Math.sin(prevAngle) * orbitRadius;
+                
+                ctx.beginPath();
+                ctx.moveTo(prevX, prevY);
+                ctx.lineTo(x, y);
+                ctx.strokeStyle = `rgba(30, 144, 255, ${baseAlpha * 0.2})`;
+                ctx.lineWidth = 1;
+                ctx.stroke();
+            }
+        }
+    } else {
+        ctx.fillStyle = CONFIG.player.color;
+        ctx.fillRect(
+            state.player.x - CONFIG.player.size / 2,
+            state.player.y - CONFIG.player.size / 2,
+            CONFIG.player.size,
+            CONFIG.player.size
+        );
+    }
+    
+    ctx.restore();
+};
+
+const lerp = (start, end, t) => {
+    return start * (1 - t) + end * t;
+};
+
+const render = () => {
+    ctx.clearRect(0, 0, GAME_WIDTH, GAME_HEIGHT);
+    
+    const screenCenterX = -state.camera.x + GAME_WIDTH / 2;
+    const screenCenterY = -state.camera.y + GAME_HEIGHT / 2;
+    
+    const distX = state.player.x - screenCenterX;
+    const distY = state.player.y - screenCenterY;
+    
+    if (Math.abs(distX) > CAMERA_DEADZONE_X / 2) {
+        const bufferX = (GAME_WIDTH - CAMERA_DEADZONE_X) / 2;
+        const targetOffsetX = distX > 0 ? GAME_WIDTH - bufferX : bufferX;
+        state.camera.targetX = -(state.player.x - targetOffsetX);
+    }
+    if (Math.abs(distY) > CAMERA_DEADZONE_Y / 2) {
+        const bufferY = (GAME_HEIGHT - CAMERA_DEADZONE_Y) / 2;
+        const targetOffsetY = distY > 0 ? GAME_HEIGHT - bufferY : bufferY;
+        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);
+    
+    ctx.save();
+    ctx.translate(state.camera.x, state.camera.y);
+    
+    const gridSize = CONFIG.grid.size;
+    ctx.strokeStyle = CONFIG.grid.color;
+    ctx.lineWidth = 1;
+    
+    const startX = Math.floor((-state.camera.x) / gridSize) * gridSize;
+    const startY = Math.floor((-state.camera.y) / gridSize) * gridSize;
+    const endX = startX + GAME_WIDTH + gridSize;
+    const endY = startY + GAME_HEIGHT + gridSize;
+    
+    for (let x = startX; x < endX; x += gridSize) {
+        ctx.beginPath();
+        ctx.moveTo(x, startY);
+        ctx.lineTo(x, endY);
+        ctx.stroke();
+    }
+    
+    for (let y = startY; y < endY; y += gridSize) {
+        ctx.beginPath();
+        ctx.moveTo(startX, y);
+        ctx.lineTo(endX, y);
+        ctx.stroke();
+    }
+    
+    state.footprints.forEach(footprint => {
+        const age = (animationTime - footprint.createdAt) / CONFIG.footprints.lifetime;
+        if (age >= 1) return;
+        
+        const alpha = Math.max(0, 1 - age * age);
+        
+        ctx.save();
+        ctx.translate(footprint.x + footprint.offset, footprint.y + footprint.offset);
+        
+        const radius = Math.max(0.1, footprint.size * (1 - age * 0.5));
+        
+        if (radius > 0) {
+            ctx.beginPath();
+            ctx.arc(0, 0, radius * 2, 0, Math.PI * 2);
+            ctx.fillStyle = `rgba(17, 17, 17, ${alpha * 0.1})`;
+            ctx.fill();
+            
+            ctx.beginPath();
+            ctx.arc(0, 0, radius, 0, Math.PI * 2);
+            ctx.fillStyle = `rgba(17, 17, 17, ${alpha * 0.3})`;
+            ctx.fill();
+        }
+        
+        ctx.restore();
+    });
+    
+    renderPlayer();
+    
+    ctx.restore();
+};
+
+const gameLoop = (currentTime) => {
+    if (!lastFrameTime) {
+        lastFrameTime = currentTime;
+        animationTime = 0;
+    }
+
+    const deltaTime = currentTime - lastFrameTime;
+    
+    if (deltaTime >= FRAME_TIME) {
+        animationTime += FRAME_TIME;
+        
+        updatePlayer();
+        render();
+        
+        lastFrameTime = currentTime;
+    }
+    
+    requestAnimationFrame(gameLoop);
+};
+
+window.addEventListener('keydown', handleKeyDown);
+window.addEventListener('keyup', handleKeyUp);
+
+const resizeCanvas = () => {
+    GAME_WIDTH = window.innerWidth;
+    GAME_HEIGHT = window.innerHeight;
+    canvas.width = GAME_WIDTH;
+    canvas.height = GAME_HEIGHT;
+    if (!state.player.x) {
+        state.player.x = GAME_WIDTH / 2;
+        state.player.y = GAME_HEIGHT / 2;
+    }
+};
+
+window.addEventListener('resize', resizeCanvas);
+
+resizeCanvas();
+
+requestAnimationFrame(gameLoop);
\ No newline at end of file
diff --git a/html/plains/index.html b/html/plains/index.html
new file mode 100644
index 0000000..508e7e0
--- /dev/null
+++ b/html/plains/index.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <title>Top-down Adventure</title>
+    <style>
+        * {
+            margin: 0;
+            padding: 0;
+            box-sizing: border-box;
+        }
+        body {
+            background: #f0f0f0;
+            overflow: hidden;
+        }
+        canvas {
+            display: block;
+            width: 100vw;
+            height: 100vh;
+        }
+    </style>
+</head>
+<body>
+    <canvas id="gameCanvas"></canvas>
+    <script src="game.js"></script>
+</body>
+</html>
\ No newline at end of file