diff options
author | elioat <elioat@tilde.institute> | 2024-12-24 15:53:19 -0500 |
---|---|---|
committer | elioat <elioat@tilde.institute> | 2024-12-24 15:53:19 -0500 |
commit | 8d4ebce0ef9eb7495e2b27a9a4df5f6001f7e65f (patch) | |
tree | d5a394ab15985715a70c1fb4ed8c4c09dcb65da4 | |
parent | bb977cc4e502e3d35f3c82e1fca1a3535fda5e19 (diff) | |
download | tour-8d4ebce0ef9eb7495e2b27a9a4df5f6001f7e65f.tar.gz |
*
-rw-r--r-- | html/rogue/index.html | 13 | ||||
-rw-r--r-- | html/rogue/js/input.js | 19 | ||||
-rw-r--r-- | html/rogue/js/renderer.js | 93 | ||||
-rw-r--r-- | html/rogue/js/rogue.js | 134 |
4 files changed, 160 insertions, 99 deletions
diff --git a/html/rogue/index.html b/html/rogue/index.html index e564517..2920841 100644 --- a/html/rogue/index.html +++ b/html/rogue/index.html @@ -1,15 +1,10 @@ <!DOCTYPE html> -<html lang="en"> +<html> <head> - <meta charset="UTF-8"> - <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Rogue</title> <style> - body, html { + body { margin: 0; - padding: 0; - width: 100%; - height: 100%; overflow: hidden; } canvas { @@ -19,9 +14,13 @@ </head> <body> <canvas id="gameCanvas"></canvas> + <!-- Load renderer.js first since other files depend on its functions --> + <script src="js/renderer.js"></script> + <script src="js/input.js"></script> <script src="js/world.js"></script> <script src="js/player.js"></script> <script src="js/camera.js"></script> + <!-- Load rogue.js last as it uses functions from all other files --> <script src="js/rogue.js"></script> </body> </html> diff --git a/html/rogue/js/input.js b/html/rogue/js/input.js new file mode 100644 index 0000000..047321c --- /dev/null +++ b/html/rogue/js/input.js @@ -0,0 +1,19 @@ +const setupInputHandlers = (canvas, gameState) => { + let mouseMoveThrottle; + + canvas.addEventListener('mousemove', (e) => { + if (!mouseMoveThrottle) { + mouseMoveThrottle = setTimeout(() => { + gameState.debug.mouseX = e.clientX + gameState.camera.x; + gameState.debug.mouseY = e.clientY + gameState.camera.y; + mouseMoveThrottle = null; + }, 16); + } + }); + + window.addEventListener('keydown', (e) => { + if (e.key === 'd') { + gameState.debug.enabled = !gameState.debug.enabled; + } + }); +}; \ No newline at end of file diff --git a/html/rogue/js/renderer.js b/html/rogue/js/renderer.js new file mode 100644 index 0000000..e0fd489 --- /dev/null +++ b/html/rogue/js/renderer.js @@ -0,0 +1,93 @@ +// Rendering constants +const RENDER_CONSTANTS = { + VIEWPORT_BUFFER: 100, + SKY_COLORS: { + TOP: '#1a1a2e', + UPPER_MID: '#2d1b3d', + LOWER_MID: '#462639', + BOTTOM: '#1f1f2f' + }, + GROUND_COLOR: '#4a4', + DEBUG_FONT: '14px monospace', + DEBUG_COLOR: '#ffffff' +}; + +// Helper functions for rendering +const createSkyGradient = (ctx, groundY) => { + const gradient = ctx.createLinearGradient(0, 0, 0, groundY); + gradient.addColorStop(0, RENDER_CONSTANTS.SKY_COLORS.TOP); + gradient.addColorStop(0.4, RENDER_CONSTANTS.SKY_COLORS.UPPER_MID); + gradient.addColorStop(0.7, RENDER_CONSTANTS.SKY_COLORS.LOWER_MID); + gradient.addColorStop(1, RENDER_CONSTANTS.SKY_COLORS.BOTTOM); + return gradient; +}; + +const getViewBounds = (camera) => ({ + left: camera.x - RENDER_CONSTANTS.VIEWPORT_BUFFER, + right: camera.x + camera.width + RENDER_CONSTANTS.VIEWPORT_BUFFER, + top: camera.y - RENDER_CONSTANTS.VIEWPORT_BUFFER, + bottom: camera.y + camera.height + RENDER_CONSTANTS.VIEWPORT_BUFFER +}); + +const isInView = (x, viewBounds) => + x > viewBounds.left && x < viewBounds.right; + +// Layer rendering functions +const renderBackground = (ctx, state, groundY, viewBounds) => { + // Save the current transform + ctx.save(); + + // Reset transform for viewport-fixed sky + ctx.setTransform(1, 0, 0, 1, 0, 0); + + // Sky (fixed to viewport) + ctx.fillStyle = state.cachedGradient; + ctx.fillRect(0, 0, ctx.canvas.width, groundY); + + // Restore transform for underground + ctx.restore(); + + // Underground (follows camera) + ctx.fillStyle = '#000000'; + ctx.fillRect( + viewBounds.left, + groundY, + viewBounds.right - viewBounds.left, + ctx.canvas.height + ); +}; + +const renderBackgroundObjects = (ctx, state, groundY, viewBounds) => { + state.world.backgroundTrees + .filter(tree => isInView(tree.x, viewBounds)) + .forEach(tree => renderTree(ctx, tree, groundY)); + + state.world.backgroundRocks + .filter(rock => isInView(rock.x, viewBounds)) + .forEach(rock => renderRock(ctx, rock, groundY)); + + state.world.backgroundGrass + .filter(grass => isInView(grass.x, viewBounds)) + .forEach(grass => renderGrass(ctx, grass, groundY, state.time)); +}; + +const renderForegroundObjects = (ctx, state, groundY, viewBounds) => { + state.world.foregroundTrees + .filter(tree => isInView(tree.x, viewBounds)) + .forEach(tree => renderTree(ctx, tree, groundY)); + + state.world.foregroundRocks + .filter(rock => isInView(rock.x, viewBounds)) + .forEach(rock => renderRock(ctx, rock, groundY)); + + state.world.foregroundGrass + .filter(grass => isInView(grass.x, viewBounds)) + .forEach(grass => renderGrass(ctx, grass, groundY, state.time)); +}; + +const renderDebugInfo = (ctx, state) => { + ctx.fillStyle = RENDER_CONSTANTS.DEBUG_COLOR; + ctx.font = RENDER_CONSTANTS.DEBUG_FONT; + const text = `x: ${Math.round(state.debug.mouseX)}, y: ${Math.round(state.debug.mouseY)}`; + ctx.fillText(text, 10, ctx.canvas.height - 20); +}; \ No newline at end of file diff --git a/html/rogue/js/rogue.js b/html/rogue/js/rogue.js index b4379ea..75a4c86 100644 --- a/html/rogue/js/rogue.js +++ b/html/rogue/js/rogue.js @@ -3,11 +3,20 @@ window.gameState = null; const initGame = () => { const canvas = document.getElementById('gameCanvas'); const ctx = canvas.getContext('2d'); + + // Target frame rate + const FPS = 60; + const FRAME_TIME = 1000 / FPS; + let lastFrameTime = 0; // Set canvas to full viewport size const resizeCanvas = () => { canvas.width = window.innerWidth; canvas.height = window.innerHeight; + // Clear any cached gradients when resizing + if (window.gameState) { + window.gameState.cachedGradient = null; + } }; window.addEventListener('resize', resizeCanvas); @@ -16,7 +25,7 @@ const initGame = () => { // Game state let gameState = { time: 0, - player: createPlayer(100, 100), // Starting position + player: createPlayer(100, 100), camera: createCamera(0, 0), world: createWorld(), debug: { @@ -29,42 +38,37 @@ const initGame = () => { // Make gameState globally accessible window.gameState = gameState; - // Throttle mouse move updates - let mouseMoveThrottle; - canvas.addEventListener('mousemove', (e) => { - if (!mouseMoveThrottle) { - mouseMoveThrottle = setTimeout(() => { - gameState.debug.mouseX = e.clientX + gameState.camera.x; - gameState.debug.mouseY = e.clientY + gameState.camera.y; - mouseMoveThrottle = null; - }, 16); // ~60fps - } - }); - - // Add debug toggle listener - window.addEventListener('keydown', (e) => { - if (e.key === 'd') { - gameState.debug.enabled = !gameState.debug.enabled; - } - }); + // Set up input handlers + setupInputHandlers(canvas, gameState); // Game loop const gameLoop = (timestamp) => { - // Calculate delta time - const deltaTime = timestamp - gameState.time; + // Check if enough time has passed since last frame + if (timestamp - lastFrameTime < FRAME_TIME) { + requestAnimationFrame(gameLoop); + return; + } + + // Calculate delta time (capped at 1 second to prevent huge jumps) + const deltaTime = Math.min(timestamp - lastFrameTime, 1000); + lastFrameTime = timestamp; gameState.time = timestamp; + // Clear the entire canvas + ctx.clearRect(0, 0, canvas.width, canvas.height); + // Update gameState = updateGame(gameState, deltaTime); // Render render(ctx, gameState); - // Next frame + // Schedule next frame requestAnimationFrame(gameLoop); }; // Start the game loop + lastFrameTime = performance.now(); requestAnimationFrame(gameLoop); }; @@ -80,107 +84,53 @@ const updateGame = (state, deltaTime) => { }; const render = (ctx, state) => { - // Calculate groundY once at the start const groundY = ctx.canvas.height - state.world.groundHeight; - // Create gradient for sky - cache this instead of recreating every frame + // Cache sky gradient if (!state.cachedGradient) { - const gradient = ctx.createLinearGradient(0, 0, 0, groundY); - gradient.addColorStop(0, '#1a1a2e'); - gradient.addColorStop(0.4, '#2d1b3d'); - gradient.addColorStop(0.7, '#462639'); - gradient.addColorStop(1, '#1f1f2f'); - state.cachedGradient = gradient; + state.cachedGradient = createSkyGradient(ctx, groundY); } - // Fill sky using cached gradient - ctx.fillStyle = state.cachedGradient; - ctx.fillRect(0, 0, ctx.canvas.width, groundY); - - // Implement view frustum culling - only render visible objects - const viewBounds = { - left: state.camera.x - 100, // Add small buffer - right: state.camera.x + state.camera.width + 100, - top: state.camera.y - 100, - bottom: state.camera.y + state.camera.height + 100 - }; + const viewBounds = getViewBounds(state.camera); // Apply camera transform ctx.save(); ctx.translate(-state.camera.x, -state.camera.y); - // Fill black background - ctx.fillStyle = '#000000'; - ctx.fillRect( + // Clear the transformed canvas area + ctx.clearRect( viewBounds.left, - groundY, + 0, viewBounds.right - viewBounds.left, ctx.canvas.height ); - // 1. Render only visible background objects - state.world.backgroundTrees.forEach(tree => { - if (tree.x > viewBounds.left && tree.x < viewBounds.right) { - renderTree(ctx, tree, groundY); - } - }); + // Render layers + renderBackground(ctx, state, groundY, viewBounds); + renderBackgroundObjects(ctx, state, groundY, viewBounds); - state.world.backgroundRocks.forEach(rock => { - if (rock.x > viewBounds.left && rock.x < viewBounds.right) { - renderRock(ctx, rock, groundY); - } - }); - - state.world.backgroundGrass.forEach(grass => { - if (grass.x > viewBounds.left && grass.x < viewBounds.right) { - renderGrass(ctx, grass, groundY, state.time); - } - }); - - // 2. Render platforms + // Render platforms state.world.platforms.forEach(platform => { ctx.fillStyle = platform.color; ctx.fillRect(platform.x, platform.y, platform.width, platform.height); }); - // 3. Render player renderPlayer(ctx, state.player); + renderForegroundObjects(ctx, state, groundY, viewBounds); - // 4. Render foreground objects - state.world.foregroundTrees.forEach(tree => { - if (tree.x > viewBounds.left && tree.x < viewBounds.right) { - renderTree(ctx, tree, groundY); - } - }); - - state.world.foregroundRocks.forEach(rock => { - if (rock.x > viewBounds.left && rock.x < viewBounds.right) { - renderRock(ctx, rock, groundY); - } - }); - - state.world.foregroundGrass.forEach(grass => { - if (grass.x > viewBounds.left && grass.x < viewBounds.right) { - renderGrass(ctx, grass, groundY, state.time); - } - }); - - // 5. Render ground (just the grass top) - ctx.fillStyle = '#4a4'; + // Render ground line + ctx.fillStyle = RENDER_CONSTANTS.GROUND_COLOR; ctx.fillRect( state.camera.x - 1000, groundY, ctx.canvas.width + 2000, - 1 // Only render the grass line + 1 ); - // Render debug information if enabled + // Handle debug rendering if (state.debug.enabled) { ctx.restore(); - ctx.fillStyle = '#ffffff'; - ctx.font = '14px monospace'; - const text = `x: ${Math.round(state.debug.mouseX)}, y: ${Math.round(state.debug.mouseY)}`; - ctx.fillText(text, 10, ctx.canvas.height - 20); + renderDebugInfo(ctx, state); } else { ctx.restore(); } |