diff options
author | elioat <elioat@tilde.institute> | 2024-12-08 22:08:17 -0500 |
---|---|---|
committer | elioat <elioat@tilde.institute> | 2024-12-08 22:08:17 -0500 |
commit | 5ae28337d0beb3a265aa8431c91253ca37debe70 (patch) | |
tree | 219d2227d374c2aa1503780a25cdd775346f086a /html/plains | |
parent | 89a4e6e730498cac67d95952ec647f01652b86c4 (diff) | |
download | tour-5ae28337d0beb3a265aa8431c91253ca37debe70.tar.gz |
*
Diffstat (limited to 'html/plains')
-rw-r--r-- | html/plains/game.js | 488 | ||||
-rw-r--r-- | html/plains/index.html | 26 |
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 |