diff options
-rw-r--r-- | html/broughlike/broughlike.js | 204 | ||||
-rw-r--r-- | html/mountain/game.js | 810 | ||||
-rw-r--r-- | html/mountain/index.html | 11 | ||||
-rw-r--r-- | html/plains/game.js | 486 | ||||
-rw-r--r-- | html/plains/index.html | 26 | ||||
-rw-r--r-- | rust/bf/.vscode/launch.json | 15 | ||||
-rw-r--r-- | rust/bf/src/main.rs | 6 |
7 files changed, 1431 insertions, 127 deletions
diff --git a/html/broughlike/broughlike.js b/html/broughlike/broughlike.js index 5e82ebb..7efa67d 100644 --- a/html/broughlike/broughlike.js +++ b/html/broughlike/broughlike.js @@ -671,8 +671,6 @@ function resetGame() { generateEnemies(); generateItems(); render(); - if (AUTO_PLAY) - autoPlay(); } function checkPlayerAtExit() { @@ -695,8 +693,6 @@ function checkPlayerAtExit() { generateEnemies(); generateItems(); render(); - if (AUTO_PLAY) - autoPlay(); } } @@ -792,14 +788,14 @@ render(); - +let AUTO_PLAY = false; +let autoPlayInterval = null; function autoPlay() { let lastPosition = { x: player.x, y: player.y }; let stuckCounter = 0; const playerAtExit = () => player.x === exit.x && player.y === exit.y; - const playerCanMove = (dx, dy) => isValidMove(player.x + dx, player.y + dy); const checkIfStuck = () => { if (lastPosition.x === player.x && lastPosition.y === player.y) { @@ -808,160 +804,120 @@ function autoPlay() { stuckCounter = 0; lastPosition = { x: player.x, y: player.y }; } - return stuckCounter > 3; // Consider yourself stuck after 3 turns in the same position + return stuckCounter > 3; }; - const desperateEscape = () => { - const allDirections = [ - {dx: 1, dy: 0}, {dx: -1, dy: 0}, - {dx: 0, dy: 1}, {dx: 0, dy: -1}, - {dx: 1, dy: 1}, {dx: -1, dy: 1}, - {dx: 1, dy: -1}, {dx: -1, dy: -1} - ]; - - allDirections.sort(() => Math.random() - 0.5); - - for (const {dx, dy} of allDirections) { - if (playerCanMove(dx, dy)) { - movePlayer(dx, dy); - return true; + const findSafestPath = (target) => { + const path = findPath(player, target); + if (path.length <= 1) { + // If you can't find a path, find the nearest enemy + const nearestEnemy = enemies.reduce((closest, enemy) => { + const distToCurrent = Math.abs(enemy.x - player.x) + Math.abs(enemy.y - player.y); + const distToClosest = closest ? Math.abs(closest.x - player.x) + Math.abs(closest.y - player.y) : Infinity; + return distToCurrent < distToClosest ? enemy : closest; + }, null); + + if (nearestEnemy) { + return [{x: player.x, y: player.y}, {x: nearestEnemy.x, y: nearestEnemy.y}]; } + return null; } - return false; - }; - const hasAdjacentEnemy = (x, y) => { - return enemies.some(enemy => - Math.abs(enemy.x - x) + Math.abs(enemy.y - y) === 1 + const nextStep = path[1]; + const adjacentEnemies = enemies.filter(enemy => + Math.abs(enemy.x - nextStep.x) + Math.abs(enemy.y - nextStep.y) <= 1 ); - }; - - const findNearestItem = () => { - if (items.length === 0) return null; - return items.reduce((nearest, item) => { - const distToCurrent = Math.abs(player.x - item.x) + Math.abs(player.y - item.y); - const distToNearest = nearest ? Math.abs(player.x - nearest.x) + Math.abs(player.y - nearest.y) : Infinity; - return distToCurrent < distToNearest ? item : nearest; - }, null); - }; - const decideNextTarget = () => { - - const healingItem = items.find(item => - item.type === 'pentagon' && - player.health < CONFIG.PLAYER_HEALTH * 0.5 - ); - if (healingItem) return healingItem; + if (adjacentEnemies.length > 0 && player.health < 3) { + const alternativePaths = [ + {dx: 1, dy: 0}, {dx: -1, dy: 0}, + {dx: 0, dy: 1}, {dx: 0, dy: -1} + ].filter(({dx, dy}) => { + const newX = player.x + dx; + const newY = player.y + dy; + return isValidMove(newX, newY) && + !enemies.some(e => Math.abs(e.x - newX) + Math.abs(e.y - newY) <= 1); + }); - const nearestItem = findNearestItem(); - if (nearestItem && Math.abs(player.x - nearestItem.x) + Math.abs(player.y - nearestItem.y) < 5) { - return nearestItem; + if (alternativePaths.length > 0) { + const randomPath = alternativePaths[Math.floor(Math.random() * alternativePaths.length)]; + return [{x: player.x, y: player.y}, {x: player.x + randomPath.dx, y: player.y + randomPath.dy}]; + } } - return exit; + return path; }; const moveTowardsTarget = (target) => { - const path = findPath(player, target); - if (path.length > 1) { + const path = findSafestPath(target); + if (path && path.length > 1) { const nextStep = path[1]; - - if (Math.random() < 0.2) { - const alternateDirections = [ - {dx: 1, dy: 0}, {dx: -1, dy: 0}, - {dx: 0, dy: 1}, {dx: 0, dy: -1} - ].filter(({dx, dy}) => - playerCanMove(dx, dy) && - !hasAdjacentEnemy(player.x + dx, player.y + dy) - ); - - if (alternateDirections.length > 0) { - const randomDir = alternateDirections[Math.floor(Math.random() * alternateDirections.length)]; - movePlayer(randomDir.dx, randomDir.dy); - return true; - } - } - - if (!hasAdjacentEnemy(nextStep.x, nextStep.y)) { - const dx = nextStep.x - player.x; - const dy = nextStep.y - player.y; - if (playerCanMove(dx, dy)) { - movePlayer(dx, dy); - return true; - } - } + const dx = nextStep.x - player.x; + const dy = nextStep.y - player.y; + movePlayer(dx, dy); + return true; } return false; }; - const handleCombat = () => { - const adjacentEnemy = enemies.find(enemy => - Math.abs(enemy.x - player.x) + Math.abs(enemy.y - player.y) === 1 - ); - - if (adjacentEnemy) { - // Increase the chance of retreating when stuck - const retreatChance = checkIfStuck() ? 0.8 : 0.3; - - if (Math.random() < retreatChance) { - const retreatDirections = [ - {dx: 1, dy: 0}, {dx: -1, dy: 0}, - {dx: 0, dy: 1}, {dx: 0, dy: -1} - ]; - - retreatDirections.sort(() => Math.random() - 0.5); - - for (const {dx, dy} of retreatDirections) { - if (playerCanMove(dx, dy) && !hasAdjacentEnemy(player.x + dx, player.y + dy)) { - movePlayer(dx, dy); - return true; - } - } - } - - // If stuck, try desperate escape - if (checkIfStuck()) { - return desperateEscape(); + const findBestTarget = () => { + // If health is low, prioritize healing items + if (player.health < 3) { + const healingItem = items.find(item => item.type === 'pentagon'); + if (healingItem && findPath(player, healingItem).length > 0) { + return healingItem; } - - // Attack if can't retreat - const dx = adjacentEnemy.x - player.x; - const dy = adjacentEnemy.y - player.y; - movePlayer(dx, dy); - return true; } - return false; + + // If there's a nearby damage boost and we're healthy, grab it + const damageItem = items.find(item => + item.type === 'diamond' && + Math.abs(item.x - player.x) + Math.abs(item.y - player.y) < 5 + ); + if (damageItem && player.health > 2) { + return damageItem; + } + + // Default to exit + return exit; }; const play = () => { - if (playerAtExit()) { + + if (!AUTO_PLAY) { + clearTimeout(autoPlayInterval); + autoPlayInterval = null; return; } - // If stuck, try desperate escape - if (checkIfStuck() && desperateEscape()) { - // Successfully escaped - } else if (!handleCombat()) { - // If no combat, move towards the next target - const target = decideNextTarget(); + if (playerAtExit()) return; + + if (checkIfStuck()) { + const directions = [{dx: 1, dy: 0}, {dx: -1, dy: 0}, {dx: 0, dy: 1}, {dx: 0, dy: -1}]; + const validDirections = directions.filter(({dx, dy}) => isValidMove(player.x + dx, player.y + dy)); + if (validDirections.length > 0) { + const {dx, dy} = validDirections[Math.floor(Math.random() * validDirections.length)]; + movePlayer(dx, dy); + } + } else { + const target = findBestTarget(); moveTowardsTarget(target); } - setTimeout(play, 400); // 400ms is about 1.5 moves per second because 1000ms was terribly slow. + autoPlayInterval = setTimeout(play, 400); }; play(); } -let AUTO_PLAY = false; - document.addEventListener('keydown', (e) => { if (e.key === 'v') { - // alert to confirm that the user wants to toggle auto play - if (confirm("Are you sure you want to toggle auto play? Once auto play is turned on, the only way to turn it off is to reload the page.")) { - AUTO_PLAY = !AUTO_PLAY; - if (AUTO_PLAY) - autoPlay(); + AUTO_PLAY = !AUTO_PLAY; + if (AUTO_PLAY) { + console.log("Auto-play on"); + autoPlay(); + } else { + console.log("Auto-play off"); } } }); \ No newline at end of file diff --git a/html/mountain/game.js b/html/mountain/game.js new file mode 100644 index 0000000..39fff3b --- /dev/null +++ b/html/mountain/game.js @@ -0,0 +1,810 @@ +/* ================================ + +45 + +There's something quieter than sleep +Within this inner room! +It wears a sprig upon its breast— +And will not tell its name. + +Some touch it, and some kiss it— +Some chafe its idle hand— +It has a simple gravity +I do not understand! + +I would not weep if I were they— +How rude in one to sob! +Might scare the quiet fairy +Back to her native wood! + +While simple-hearted neighbors +Chat of the "Early dead"— +We—prone to periphrasis +Remark that Birds have fled! + +Emily Dickinson + +================================*/ + +const canvas = document.createElement('canvas'); +const ctx = canvas.getContext('2d'); +document.body.appendChild(canvas); +canvas.style.display = 'block'; +canvas.style.position = 'fixed'; +canvas.style.top = '0'; +canvas.style.left = '0'; + +const GAME_STATE = { + PLAYING: 'playing', + GAME_OVER: 'game_over' // (ノಠ益ಠ)ノ +}; + +const PLATFORM_TYPE = { + NORMAL: 'normal', + DEADLY: 'deadly', + FALLING: 'falling' +}; + +const PLAYER_SIZE = 20; +const GRAVITY = 0.5; +const JUMP_FORCE = 12; +const MOVE_SPEED = 7; + +const PLATFORM_HEIGHT = 20; +const MIN_PLATFORM_WIDTH = 100; +const MAX_PLATFORM_WIDTH = 300; +const MIN_PARTITION_SIZE = MAX_PLATFORM_WIDTH + 20; +const PARTITION_RATIO = 0.3; +const MIN_PLATFORM_SPACING = 100; +const DEADLY_BORDER_HEIGHT = 7; + +const ENEMY_SPEED = 2; + +const FALLING_PLATFORM_DELAY = 800; +const FALLING_PLATFORM_GRAVITY = 0.5; + +const FPS = 60; +const FRAME_TIME = 1000 / FPS; + +const PARTICLE_COUNT = 20; +const PARTICLE_SPEED = 4; +const PARTICLE_SIZE = 4; +const PARTICLE_LIFETIME = 25; + +const DEATH_PARTICLE_COUNT = 60; +const DEATH_PARTICLE_SPEED = 10; +const DEATH_PARTICLE_SIZE = 4; +const DEATH_PARTICLE_LIFETIME = 50; +const DEATH_ANIMATION_DURATION = 55; + +const PLATFORM_PARTICLE_COUNT = 30; +const PLATFORM_PARTICLE_SPEED = 8; +const PLATFORM_PARTICLE_SIZE = 4; +const PLATFORM_PARTICLE_LIFETIME = 40; + +const HARD_MODE_TIME_LIMIT = 7; // seconds +const SUPER_HARD_MODE_TIME_LIMIT = 5; + +let gameState = GAME_STATE.PLAYING; +let level = 1; +let platforms = []; +let enemies = []; +let particles = []; +let deathParticles = []; +let deathAnimationTimer = 0; +let frameCount = 0; +let lastFpsUpdate = 0; +let currentFps = 0; +let lastFrameTime = 0; +let accumulator = 0; + +let player = { + x: PLAYER_SIZE, + y: window.innerHeight - PLAYER_SIZE * 2, + velocityX: 0, + velocityY: 0, + isJumping: false, + jumpsLeft: 2, + gravityMultiplier: 1, + isDead: false +}; + +let exit = { + x: window.innerWidth - PLAYER_SIZE * 2, + y: PLAYER_SIZE * 2, + size: PLAYER_SIZE +}; + +const keys = {}; +window.addEventListener('keydown', e => keys[e.key] = true); +window.addEventListener('keyup', e => keys[e.key] = false); + +const COLORS = { + BACKGROUND: '#E0E0E0', + PLATFORM: { + NORMAL: '#1A1A1A', + DEADLY: 'tomato', + FALLING: 'rgba(26, 26, 26, 0.5)' + }, + PLAYER: { + NORMAL: '#1A1A1A', + INVERTED: '#4A90E2' + }, + ENEMY: 'tomato', + EXIT: 'teal', + TEXT: '#1A1A1A', + GAME_OVER: { + OVERLAY: 'rgba(0, 0, 0, 0.7)', + TEXT: 'rgba(255, 255, 255, 0.75)' + }, + DEADLY_BORDER: 'tomato' +}; + +function randomRange(min, max) { + return min + Math.random() * (max - min); +} + +function createPartition(x, y, width, height) { + return { + x, + y, + width, + height, + platform: null, + left: null, + right: null + }; +} + +function splitPartition(partition, vertical) { + const splitPoint = randomRange( + vertical ? partition.width * PARTITION_RATIO : partition.height * PARTITION_RATIO, + vertical ? partition.width * (1 - PARTITION_RATIO) : partition.height * (1 - PARTITION_RATIO) + ); + + return { + left: vertical + ? createPartition(partition.x, partition.y, splitPoint, partition.height) + : createPartition(partition.x, partition.y, partition.width, splitPoint), + right: vertical + ? createPartition(partition.x + splitPoint, partition.y, partition.width - splitPoint, partition.height) + : createPartition(partition.x, partition.y + splitPoint, partition.width, partition.height - splitPoint) + }; +} + +function platformsOverlap(platform1, platform2) { + const buffer = 5; + + return !( + platform1.x + platform1.width + buffer < platform2.x || + platform1.x > platform2.x + platform2.width + buffer || + platform1.y + platform1.height + buffer < platform2.y || + platform1.y > platform2.y + platform2.height + buffer + ); +} + +function createParticle(x, y, velocityY) { + return { + // Randomly places particles around the player, within the player's width + x: x + PLAYER_SIZE / 2 + (Math.random() - 0.5) * PLAYER_SIZE, + + // Put particles to the top or bottom of the player depending on the direction + // the player is moving + y: y + (velocityY > 0 ? 0 : PLAYER_SIZE), + + // Zoot out to the left and right of the player + velocityX: (Math.random() - 0.5) * PARTICLE_SPEED * 2, + + // Zoot up or down, matching the player's direction + velocityY: (Math.random() * PARTICLE_SPEED) * Math.sign(velocityY), + + // Add some variety to the particle sizes + size: PARTICLE_SIZE + Math.random() * 2, + + life: PARTICLE_LIFETIME, + initialOpacity: 0.3 + Math.random() * 0.7 + }; +} + +function createDeathParticles(x, y) { + deathParticles = []; + for (let i = 0; i < DEATH_PARTICLE_COUNT; i++) { + const angle = (Math.PI * 2 * i) / DEATH_PARTICLE_COUNT; + deathParticles.push({ + x: x + PLAYER_SIZE / 2, + y: y + PLAYER_SIZE / 2, + velocityX: Math.cos(angle) * DEATH_PARTICLE_SPEED * (0.5 + Math.random()), + velocityY: Math.sin(angle) * DEATH_PARTICLE_SPEED * (0.5 + Math.random()), + size: DEATH_PARTICLE_SIZE + Math.random() * 2, + life: DEATH_PARTICLE_LIFETIME, + initialOpacity: 0.6 + Math.random() * 0.4 + }); + } + deathAnimationTimer = DEATH_ANIMATION_DURATION; +} + +function updateParticles() { + particles = particles.filter(particle => { + particle.x += particle.velocityX; + particle.y += particle.velocityY; + particle.life--; + return particle.life > 0; + }); +} + +function updateDeathParticles() { + if (deathAnimationTimer > 0) { + deathAnimationTimer--; + } + + deathParticles = deathParticles.filter(particle => { + particle.x += particle.velocityX; + particle.y += particle.velocityY; + particle.velocityY += GRAVITY * 0.5; // GRAVITY! Is working against me... + particle.life--; + return particle.life > 0; + }); +} + +function createEnemy(platform) { + const startsOnTop = Math.random() < 0.5; // 50% chance to start on top + return { + x: platform.x, + y: startsOnTop ? + platform.y - PLAYER_SIZE : + platform.y + platform.height, + width: PLAYER_SIZE, + height: PLAYER_SIZE, + platform: platform, + direction: 1, + moveRight: true, + isOnTop: startsOnTop // Track where things started + }; +} + +function updateEnemies() { + enemies.forEach(enemy => { + enemy.x += ENEMY_SPEED * (enemy.moveRight ? 1 : -1); + + if (enemy.moveRight && enemy.x + enemy.width > enemy.platform.x + enemy.platform.width) { + enemy.moveRight = false; + } else if (!enemy.moveRight && enemy.x < enemy.platform.x) { + enemy.moveRight = true; + } + + enemy.y = (player.gravityMultiplier > 0) === enemy.isOnTop ? + enemy.platform.y - enemy.height : + enemy.platform.y + enemy.platform.height; + }); +} + +function generatePlatformsForPartition(partition) { + if (partition.width < MIN_PLATFORM_WIDTH || partition.height < PLATFORM_HEIGHT * 2) { + return []; + } + + const platformCount = partition.width > MAX_PLATFORM_WIDTH * 1.5 + ? Math.floor(randomRange(1, 3)) + : 1; + + const newPlatforms = []; + const maxAttempts = 20; + + for (let i = 0; i < platformCount; i++) { + let validPlatform = null; + let attempts = 0; + + // Try to create a platform, but don't try too hard so that things get stuck + while (!validPlatform && attempts < maxAttempts) { + // Generate a random width for the platform, that is between a min and a max + const platformWidth = randomRange( + MIN_PLATFORM_WIDTH, + Math.min(MAX_PLATFORM_WIDTH, partition.width * 0.8) + ); + + // Generate the minimum x position, which is either the partition's start + // or a position based on the platform's number being placed (i/platformCount) + // This spreads the platforms out more evenly across the partition + const minX = Math.max( + partition.x, + partition.x + (partition.width * (i / platformCount)) + ); + + // Along the same lines, calculate a max x position that accounts for platform width + const maxX = Math.min( + partition.x + partition.width - platformWidth, + partition.x + (partition.width * ((i + 1) / platformCount)) + ); + + // Try to place a platform if there is a valid range + if (maxX > minX) { + // Create a candidate platform with a random position and properties + const candidatePlatform = { + x: randomRange(minX, maxX), + y: randomRange( + partition.y + PLATFORM_HEIGHT, + partition.y + partition.height - PLATFORM_HEIGHT * 2 + ), + width: platformWidth, + height: PLATFORM_HEIGHT, + // After level 1, there is a chance that a platform will be deadly, or falling! + type: (() => { + if (level > 1 && Math.random() < 0.2) return PLATFORM_TYPE.DEADLY; + if (level > 1 && Math.random() < 0.3) { + return PLATFORM_TYPE.FALLING; + } + return PLATFORM_TYPE.NORMAL; + })(), + fallTimer: null, + velocityY: 0 + }; + + // Check if the platform overlaps with any existing platforms + let overlapping = false; + for (const existingPlatform of [...platforms, ...newPlatforms]) { + if (platformsOverlap(candidatePlatform, existingPlatform)) { + overlapping = true; + break; + } + } + + // If there isn't an overlap the platform is valid! + // Place it! + if (!overlapping) { + validPlatform = candidatePlatform; + } + } + + attempts++; + } + + if (validPlatform) { + newPlatforms.push(validPlatform); + } + } + + newPlatforms.forEach(platform => { + if (platform.type === PLATFORM_TYPE.NORMAL && Math.random() < 0.2) { // 20% chance + enemies.push(createEnemy(platform)); + } + }); + + return newPlatforms; +} + +function generateLevel() { + platforms = []; + enemies = []; + + platforms.push({ + x: 0, + y: window.innerHeight - PLATFORM_HEIGHT, + width: MIN_PLATFORM_WIDTH, + height: PLATFORM_HEIGHT, + type: PLATFORM_TYPE.NORMAL + }); + + platforms.push({ + x: window.innerWidth - MIN_PLATFORM_WIDTH, + y: PLAYER_SIZE * 3, + width: MIN_PLATFORM_WIDTH, + height: PLATFORM_HEIGHT, + type: PLATFORM_TYPE.NORMAL + }); + + const horizontalSections = 3; + const verticalSections = 2; + const sectionWidth = window.innerWidth / horizontalSections; + const sectionHeight = (window.innerHeight - PLATFORM_HEIGHT * 4) / verticalSections; + + function subdivide(node, depth) { + if (depth === 0) { + return generatePlatformsForPartition(node); + } + + const vertical = Math.random() > 0.4; + if ((vertical && node.width > MIN_PARTITION_SIZE * 1.5) || + (!vertical && node.height > MIN_PARTITION_SIZE * 1.5)) { + + const { left, right } = splitPartition(node, vertical); + return [ + ...subdivide(left, depth - 1), + ...subdivide(right, depth - 1) + ]; + } + + return generatePlatformsForPartition(node); + } + + for (let i = 0; i < horizontalSections; i++) { + for (let j = 0; j < verticalSections; j++) { + const root = createPartition( + i * sectionWidth, + PLATFORM_HEIGHT * 2 + (j * sectionHeight), + sectionWidth, + sectionHeight + ); + + const newPlatforms = subdivide(root, Math.min(3 + Math.floor(level / 2), 5)); + platforms.push(...newPlatforms); + } + } + + levelStartTime = Date.now(); +} + +function resetPlayer() { + player.x = PLAYER_SIZE; + player.y = window.innerHeight - PLATFORM_HEIGHT - PLAYER_SIZE; + player.velocityX = 0; + player.velocityY = 0; + player.isJumping = false; + player.jumpsLeft = 2; + player.gravityMultiplier = 1; + player.isDead = false; + gameState = GAME_STATE.PLAYING; + level = 1; + generateLevel(); + enemies = []; + levelStartTime = Date.now(); +} + +function killPlayer() { + if (!player.isDead) { + createDeathParticles(player.x, player.y); + player.isDead = true; + gameState = GAME_STATE.GAME_OVER; + } +} + +// Try to compensate for varying viewport widths +function calculateTimeLimit(isSuper) { + const baseLimit = isSuper ? SUPER_HARD_MODE_TIME_LIMIT : HARD_MODE_TIME_LIMIT; + + if (canvas.width <= 2000) return baseLimit; + + const extraWidth = canvas.width - 2000; + const extraSeconds = Math.floor(extraWidth / 1000) * (isSuper ? 0.5 : 1); + + return baseLimit + extraSeconds; +} + +function updatePlayer() { + if (gameState === GAME_STATE.GAME_OVER) { + if (deathAnimationTimer <= 0 && keys['Enter']) { + resetPlayer(); + } + return; + } + + const timeLimit = superHardMode ? + calculateTimeLimit(true) : + calculateTimeLimit(false); + + if ((hardMode || superHardMode) && Date.now() - levelStartTime > timeLimit * 1000) { + killPlayer(); + return; + } + + if (keys['ArrowLeft']) player.velocityX = -MOVE_SPEED; + else if (keys['ArrowRight']) player.velocityX = MOVE_SPEED; + else player.velocityX = 0; + + if ((keys['g'] || keys[' ']) && !player.lastGravityKey) { + player.gravityMultiplier *= -1; + player.velocityY = 0; + } + player.lastGravityKey = keys['g'] || keys[' ']; + + player.velocityY += GRAVITY * player.gravityMultiplier; + + if (keys['ArrowUp'] && keys['ArrowUp'] !== player.lastJumpKey && player.jumpsLeft > 0) { + player.velocityY = -JUMP_FORCE * player.gravityMultiplier; + player.jumpsLeft--; + player.isJumping = true; + + // Add particles for every jump + for (let i = 0; i < PARTICLE_COUNT; i++) { + particles.push(createParticle(player.x, player.y, player.velocityY)); + } + } + player.lastJumpKey = keys['ArrowUp']; + + player.x += player.velocityX; + player.y += player.velocityY; + + player.isJumping = true; + for (let platform of platforms) { + if (player.x + PLAYER_SIZE > platform.x && + player.x < platform.x + platform.width) { + + let collision = false; + + if (player.gravityMultiplier > 0) { + if (player.y + PLAYER_SIZE > platform.y && + player.y + PLAYER_SIZE < platform.y + platform.height + player.velocityY) { + collision = true; + player.y = platform.y - PLAYER_SIZE; + } + } else { + if (player.y < platform.y + platform.height && + player.y > platform.y + player.velocityY) { + collision = true; + player.y = platform.y + platform.height; + } + } + + if (collision) { + if (platform.type === PLATFORM_TYPE.DEADLY) { + killPlayer(); + } else { + if (platform.type === PLATFORM_TYPE.FALLING && !platform.fallTimer) { + platform.fallTimer = setTimeout(() => { + platform.isFalling = true; + }, FALLING_PLATFORM_DELAY); + } + player.velocityY = 0; + player.isJumping = false; + player.jumpsLeft = 2; + } + } + } + } + + if (player.y <= DEADLY_BORDER_HEIGHT) { + if (player.x < canvas.width - exit.size - 300) { + killPlayer(); + } + } else if (player.y + PLAYER_SIZE >= canvas.height - DEADLY_BORDER_HEIGHT) { + killPlayer(); + } + + if (player.x < 0) player.x = 0; + if (player.x + PLAYER_SIZE > canvas.width) player.x = canvas.width - PLAYER_SIZE; + if (player.y < 0) { + player.y = 0; + player.velocityY = 0; + } + if (player.y + PLAYER_SIZE > canvas.height) { + player.y = canvas.height - PLAYER_SIZE; + player.velocityY = 0; + } + + if (player.x + PLAYER_SIZE > exit.x && + player.x < exit.x + exit.size && + player.y + PLAYER_SIZE > exit.y && + player.y < exit.y + exit.size) { + level++; + generateLevel(); + player.x = PLAYER_SIZE; + player.y = window.innerHeight - PLATFORM_HEIGHT - PLAYER_SIZE; + player.velocityY = 0; + player.velocityX = 0; + player.jumpsLeft = 2; + player.isJumping = false; + } + + enemies.forEach(enemy => { + if (player.x < enemy.x + enemy.width && + player.x + PLAYER_SIZE > enemy.x && + player.y < enemy.y + enemy.height && + player.y + PLAYER_SIZE > enemy.y) { + killPlayer(); + } + }); +} + +function resizeCanvas() { + canvas.width = window.innerWidth; + canvas.height = window.innerHeight; +} + +function draw(currentTime) { + if ((hardMode || superHardMode) && gameState === GAME_STATE.PLAYING) { + const timeLimit = superHardMode ? + calculateTimeLimit(true) : + calculateTimeLimit(false); + + const timeElapsed = Date.now() - levelStartTime; + const timeRemaining = Math.max(0, timeLimit * 1000 - timeElapsed); + const progressRatio = timeRemaining / (timeLimit * 1000); + + ctx.fillStyle = COLORS.BACKGROUND; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + ctx.fillStyle = 'rgba(0, 0, 0, 0.1)'; + const progressWidth = canvas.width * progressRatio; + ctx.fillRect(progressWidth, 0, canvas.width - progressWidth, canvas.height); + } else { + ctx.fillStyle = COLORS.BACKGROUND; + ctx.fillRect(0, 0, canvas.width, canvas.height); + } + + ctx.fillStyle = COLORS.DEADLY_BORDER; + ctx.fillRect(0, 0, canvas.width - exit.size - 300, DEADLY_BORDER_HEIGHT); + + ctx.fillRect(0, canvas.height - DEADLY_BORDER_HEIGHT, platforms[0].x, DEADLY_BORDER_HEIGHT); + ctx.fillRect( + platforms[0].x + platforms[0].width, + canvas.height - DEADLY_BORDER_HEIGHT, + canvas.width - (platforms[0].x + platforms[0].width), + DEADLY_BORDER_HEIGHT + ); + + for (let platform of platforms) { + ctx.fillStyle = platform.type === PLATFORM_TYPE.DEADLY ? COLORS.PLATFORM.DEADLY : + platform.type === PLATFORM_TYPE.FALLING ? COLORS.PLATFORM.FALLING : + COLORS.PLATFORM.NORMAL; + ctx.fillRect(platform.x, platform.y, platform.width, platform.height); + } + + ctx.fillStyle = COLORS.EXIT; + ctx.fillRect(exit.x, exit.y, exit.size, exit.size); + + if (!player.isDead) { + ctx.fillStyle = player.gravityMultiplier > 0 ? COLORS.PLAYER.NORMAL : COLORS.PLAYER.INVERTED; + ctx.fillRect(player.x, player.y, PLAYER_SIZE, PLAYER_SIZE); + } + + ctx.fillStyle = COLORS.TEXT; + ctx.font = '20px Arial'; + ctx.textAlign = 'left'; + ctx.fillText(`Level: ${level}`, 10, 30); + + // Particles can have different opacities + particles.forEach(particle => { + const alpha = (particle.life / PARTICLE_LIFETIME) * particle.initialOpacity; + ctx.fillStyle = `rgba(26, 26, 26, ${alpha})`; + ctx.fillRect(particle.x, particle.y, particle.size, particle.size); + }); + + enemies.forEach(enemy => { + ctx.fillStyle = COLORS.ENEMY; + ctx.fillRect(enemy.x, enemy.y, enemy.width, enemy.height); + }); + + // Death particles x_x + deathParticles.forEach(particle => { + const alpha = (particle.life / DEATH_PARTICLE_LIFETIME) * particle.initialOpacity; + ctx.fillStyle = `rgba(26, 26, 26, ${alpha})`; + ctx.fillRect(particle.x, particle.y, particle.size, particle.size); + }); + + if (gameState === GAME_STATE.GAME_OVER && deathAnimationTimer <= 0) { + ctx.fillStyle = COLORS.GAME_OVER.OVERLAY; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + ctx.fillStyle = COLORS.GAME_OVER.TEXT; + ctx.font = '100px Arial'; + ctx.textAlign = 'center'; + ctx.fillText('GAME OVER', canvas.width / 2, canvas.height / 2); + + ctx.font = '24px Arial'; + ctx.fillText('Press ENTER to restart', canvas.width / 2, canvas.height / 2 + 40); + + ctx.textAlign = 'left'; + } + + // Show some help text on the first level so that folks know what to do + if (level === 1 && gameState === GAME_STATE.PLAYING) { + ctx.fillStyle = COLORS.TEXT; + ctx.font = '24px Arial'; + ctx.textAlign = 'center'; + ctx.fillText('Arrow keys move the player.', canvas.width / 2, canvas.height / 2 - 20); + ctx.fillText('Space bar reverses gravity.', canvas.width / 2, canvas.height / 2 + 20); + ctx.textAlign = 'left'; + } +} + +function gameLoop(currentTime) { + if (lastFrameTime === 0) { + lastFrameTime = currentTime; + lastFpsUpdate = currentTime; + } + + const deltaTime = currentTime - lastFrameTime; + lastFrameTime = currentTime; + + accumulator += deltaTime; + + while (accumulator >= FRAME_TIME) { + updatePlayer(); + updateEnemies(); + updateParticles(); + updateDeathParticles(); + updatePlatforms(); + accumulator -= FRAME_TIME; + } + + draw(currentTime); + requestAnimationFrame(gameLoop); +} + +function createPlatformParticles(platform) { + for (let i = 0; i < PLATFORM_PARTICLE_COUNT; i++) { + const angle = (Math.PI * 2 * i) / PLATFORM_PARTICLE_COUNT; + particles.push({ + x: platform.x + platform.width / 2, + y: platform.y + platform.height / 2, + velocityX: Math.cos(angle) * PLATFORM_PARTICLE_SPEED * (0.5 + Math.random()), + velocityY: Math.sin(angle) * PLATFORM_PARTICLE_SPEED * (0.5 + Math.random()), + size: PLATFORM_PARTICLE_SIZE + Math.random() * 2, + life: PLATFORM_PARTICLE_LIFETIME, + initialOpacity: 0.6 + Math.random() * 0.4 + }); + } +} + +function updatePlatforms() { + platforms.forEach(platform => { + if (platform.type === PLATFORM_TYPE.FALLING && platform.isFalling) { + platform.velocityY += FALLING_PLATFORM_GRAVITY * player.gravityMultiplier; + platform.y += platform.velocityY; + + // Create particles when platform goes off screen + if ((player.gravityMultiplier > 0 && platform.y > canvas.height + 50) || + (player.gravityMultiplier < 0 && platform.y < -50)) { + createPlatformParticles(platform); + } + } + }); + + // Remove platforms that have fallen off screen in either direction + platforms = platforms.filter(platform => + platform.type !== PLATFORM_TYPE.FALLING || + (player.gravityMultiplier > 0 ? platform.y < canvas.height + 100 : platform.y > -100) + ); +} + +let hardMode = false; +let superHardMode = false; +let levelStartTime = 0; + +const hardModeButton = document.createElement('button'); +hardModeButton.textContent = 'Hard Mode: OFF'; +hardModeButton.style.position = 'fixed'; +hardModeButton.style.left = '10px'; +hardModeButton.style.top = '40px'; +hardModeButton.style.padding = '5px 10px'; +hardModeButton.style.backgroundColor = COLORS.PLATFORM.NORMAL; +hardModeButton.style.color = 'white'; +hardModeButton.style.border = 'none'; +hardModeButton.style.cursor = 'pointer'; +document.body.appendChild(hardModeButton); + +const superHardModeButton = document.createElement('button'); +superHardModeButton.textContent = 'Super Hard Mode: OFF'; +superHardModeButton.style.position = 'fixed'; +superHardModeButton.style.left = '10px'; +superHardModeButton.style.top = '70px'; +superHardModeButton.style.padding = '5px 10px'; +superHardModeButton.style.backgroundColor = COLORS.PLATFORM.NORMAL; +superHardModeButton.style.color = 'white'; +superHardModeButton.style.border = 'none'; +superHardModeButton.style.cursor = 'pointer'; +document.body.appendChild(superHardModeButton); + +hardModeButton.addEventListener('click', () => { + hardMode = !hardMode; + superHardMode = false; + hardModeButton.textContent = `Hard Mode: ${hardMode ? 'ON' : 'OFF'}`; + superHardModeButton.textContent = 'Super Hard Mode: OFF'; + resetPlayer(); + hardModeButton.blur(); +}); + +superHardModeButton.addEventListener('click', () => { + superHardMode = !superHardMode; + hardMode = false; + superHardModeButton.textContent = `Super Hard Mode: ${superHardMode ? 'ON' : 'OFF'}`; + hardModeButton.textContent = 'Hard Mode: OFF'; + resetPlayer(); + superHardModeButton.blur(); +}); + +document.body.style.margin = '0'; +document.body.style.overflow = 'hidden'; +resizeCanvas(); +window.addEventListener('resize', resizeCanvas); +generateLevel(); +requestAnimationFrame(gameLoop); diff --git a/html/mountain/index.html b/html/mountain/index.html new file mode 100644 index 0000000..cac722a --- /dev/null +++ b/html/mountain/index.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Mountain</title> +</head> +<body> + <script src="game.js"></script> +</body> +</html> \ No newline at end of file diff --git a/html/plains/game.js b/html/plains/game.js new file mode 100644 index 0000000..6054455 --- /dev/null +++ b/html/plains/game.js @@ -0,0 +1,486 @@ +const CONFIG = { + player: { + size: 30, + speed: 5, + sprintMultiplier: 2, + 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) + )); + 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) => ({ + 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 = 3; + 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 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 diff --git a/rust/bf/.vscode/launch.json b/rust/bf/.vscode/launch.json new file mode 100644 index 0000000..33bcb32 --- /dev/null +++ b/rust/bf/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "type": "lldb", + "request": "launch", + "name": "Debug executable", + "cargo": { + "args": ["build"] + }, + "args": ["src/hw.bf"], + "cwd": "${workspaceFolder}" + } + ] +} \ No newline at end of file diff --git a/rust/bf/src/main.rs b/rust/bf/src/main.rs index 9257ae2..e511e69 100644 --- a/rust/bf/src/main.rs +++ b/rust/bf/src/main.rs @@ -16,12 +16,12 @@ fn interpret_brainfuck(code: &str) { '>' => { pointer += 1; if pointer >= memory.len() { - panic!("Pointer out of bounds"); + panic!("Pointer out of bounds (positive)"); } } '<' => { if pointer == 0 { - panic!("Pointer out of bounds"); + panic!("Pointer out of bounds (negative)"); } pointer -= 1; } @@ -36,7 +36,7 @@ fn interpret_brainfuck(code: &str) { } ',' => { let mut input = [0u8]; - io::stdin().read_exact(&mut input).expect("Failed to read input"); + io::stdin().read_exact(&mut input).expect("Failed to read input. Unable to read a single byte from stdin"); memory[pointer] = input[0]; } '[' => { |