diff options
-rw-r--r-- | html/rogue/assets/home/goblin-01.png | bin | 0 -> 11889 bytes | |||
-rw-r--r-- | html/rogue/assets/home/goblin-02.png | bin | 0 -> 11875 bytes | |||
-rw-r--r-- | html/rogue/assets/home/goblin.json | 617 | ||||
-rw-r--r-- | html/rogue/index.html | 6 | ||||
-rw-r--r-- | html/rogue/js/animation.js | 53 | ||||
-rw-r--r-- | html/rogue/js/camera.js | 79 | ||||
-rw-r--r-- | html/rogue/js/config.js | 75 | ||||
-rw-r--r-- | html/rogue/js/debug.js | 113 | ||||
-rw-r--r-- | html/rogue/js/events.js | 16 | ||||
-rw-r--r-- | html/rogue/js/fow.js | 41 | ||||
-rw-r--r-- | html/rogue/js/game.js | 83 | ||||
-rw-r--r-- | html/rogue/js/hex.js | 43 | ||||
-rw-r--r-- | html/rogue/js/inventory-ui.js | 37 | ||||
-rw-r--r-- | html/rogue/js/player.js | 79 | ||||
-rw-r--r-- | html/rogue/js/renderer.js | 29 | ||||
-rw-r--r-- | html/rogue/js/state.js | 17 | ||||
-rw-r--r-- | html/rogue/js/utils.js | 25 | ||||
-rw-r--r-- | html/voxels/index.html | 22 | ||||
-rw-r--r-- | html/voxels/js/game.js | 362 |
19 files changed, 1584 insertions, 113 deletions
diff --git a/html/rogue/assets/home/goblin-01.png b/html/rogue/assets/home/goblin-01.png new file mode 100644 index 0000000..c70d2fd --- /dev/null +++ b/html/rogue/assets/home/goblin-01.png Binary files differdiff --git a/html/rogue/assets/home/goblin-02.png b/html/rogue/assets/home/goblin-02.png new file mode 100644 index 0000000..19411c8 --- /dev/null +++ b/html/rogue/assets/home/goblin-02.png Binary files differdiff --git a/html/rogue/assets/home/goblin.json b/html/rogue/assets/home/goblin.json new file mode 100644 index 0000000..8b0e418 --- /dev/null +++ b/html/rogue/assets/home/goblin.json @@ -0,0 +1,617 @@ +{ + "__projectHeader": "pppppp_v1", + "timestamp": "2024-12-28T22:11:34.632Z", + "data": { + "gridWidth": 16, + "gridHeight": 16, + "cellSize": 29.296875, + "colorHistory": [ + "#eeb243", + "#ffbe47", + "#ff5eeb", + "#ff5feb", + "#9db13a", + "#000000", + "#e7ad41", + "#f5fff6", + "#af2866", + "#a87d2e" + ], + "currentColor": "#9db13a", + "canvases": [ + { + "grid": [ + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + "#000000", + "#000000", + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + "#000000", + "#9db13a", + "#99b213", + "#000000", + null, + null, + null, + null, + null, + null, + null, + "#000000", + "#000000", + null + ], + [ + null, + null, + "#000000", + "#9db13a", + "#ff5feb", + "#99b213", + "#000000", + "#000000", + "#000000", + "#000000", + null, + null, + "#000000", + "#99b213", + "#000000", + null + ], + [ + null, + "#000000", + "#99b213", + "#99b213", + "#99b213", + "#99b213", + "#99b213", + "#99b213", + "#99b213", + "#99b213", + "#000000", + null, + "#000000", + "#99b213", + "#000000", + null + ], + [ + null, + "#000000", + "#99b213", + "#99b213", + "#99b213", + "#99b213", + "#99b213", + "#99b213", + "#99b213", + "#99b213", + "#99b213", + "#000000", + "#c00f68", + "#c00f68", + "#b07b12", + "#000000" + ], + [ + null, + "#000000", + "#99b213", + "#99b213", + "#99b213", + "#000000", + "#000000", + "#99b213", + "#f5fff6", + "#f5fff6", + "#99b213", + "#000000", + "#c00f68", + "#c00f68", + "#b07b12", + "#000000" + ], + [ + null, + "#000000", + "#99b213", + "#99b213", + "#99b213", + "#99b213", + "#99b213", + "#99b213", + "#99b213", + "#000000", + "#99b213", + "#000000", + "#c00f68", + "#c00f68", + "#000000", + null + ], + [ + null, + "#000000", + "#99b213", + "#99b213", + "#99b213", + "#99b213", + "#99b213", + "#99b213", + "#99b213", + "#000000", + "#99b213", + "#000000", + "#c00f68", + "#c00f68", + "#000000", + null + ], + [ + null, + "#000000", + "#99b213", + "#99b213", + "#99b213", + "#000000", + "#000000", + "#99b213", + "#99b213", + "#f5fff6", + "#99b213", + "#000000", + "#c00f68", + "#c00f68", + "#b07b12", + "#000000" + ], + [ + null, + "#000000", + "#99b213", + "#99b213", + "#99b213", + "#99b213", + "#99b213", + "#99b213", + "#99b213", + "#99b213", + "#99b213", + "#000000", + "#c00f68", + "#c00f68", + "#b07b12", + "#000000" + ], + [ + null, + "#000000", + "#99b213", + "#99b213", + "#99b213", + "#99b213", + "#99b213", + "#99b213", + "#99b213", + "#99b213", + "#000000", + null, + "#000000", + "#99b213", + "#000000", + null + ], + [ + null, + null, + "#000000", + "#9db13a", + "#ff5feb", + "#99b213", + "#000000", + "#000000", + "#000000", + "#000000", + null, + null, + "#000000", + "#99b213", + "#000000", + null + ], + [ + null, + null, + "#000000", + "#9db13a", + "#99b213", + "#000000", + null, + null, + null, + null, + null, + null, + null, + "#000000", + "#000000", + null + ], + [ + null, + null, + null, + "#000000", + "#000000", + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ] + ], + "offsetX": 143.1015625, + "offsetY": 218.625, + "hasPixels": false + }, + { + "grid": [ + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + "#000000", + "#000000", + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + "#000000", + "#9db13a", + "#9db13a", + "#000000", + null, + null, + null, + null, + null, + "#000000", + "#000000", + null, + null, + null + ], + [ + null, + null, + "#000000", + "#9db13a", + "#ff5eeb", + "#9db13a", + "#000000", + "#000000", + "#000000", + "#000000", + null, + "#000000", + "#9db13a", + "#000000", + null, + null + ], + [ + null, + "#000000", + "#9db13a", + "#9db13a", + "#9db13a", + "#9db13a", + "#9db13a", + "#9db13a", + "#9db13a", + "#9db13a", + "#000000", + "#000000", + "#9db13a", + "#000000", + "#000000", + null + ], + [ + null, + "#000000", + "#9db13a", + "#9db13a", + "#9db13a", + "#9db13a", + "#9db13a", + "#9db13a", + "#9db13a", + "#9db13a", + "#9db13a", + "#000000", + "#af2866", + "#af2866", + "#a87d2e", + "#000000" + ], + [ + null, + "#000000", + "#9db13a", + "#9db13a", + "#9db13a", + "#000000", + "#000000", + "#9db13a", + "#f5fff6", + "#f5fff6", + "#9db13a", + "#000000", + "#af2866", + "#af2866", + "#a87d2e", + "#000000" + ], + [ + null, + "#000000", + "#9db13a", + "#9db13a", + "#9db13a", + "#9db13a", + "#9db13a", + "#9db13a", + "#9db13a", + "#000000", + "#9db13a", + "#000000", + "#af2866", + "#af2866", + "#000000", + null + ], + [ + null, + "#000000", + "#9db13a", + "#9db13a", + "#9db13a", + "#9db13a", + "#9db13a", + "#9db13a", + "#9db13a", + "#000000", + "#9db13a", + "#000000", + "#af2866", + "#af2866", + "#000000", + null + ], + [ + null, + "#000000", + "#9db13a", + "#9db13a", + "#9db13a", + "#000000", + "#000000", + "#9db13a", + "#9db13a", + "#f5fff6", + "#9db13a", + "#000000", + "#af2866", + "#af2866", + "#a87d2e", + "#000000" + ], + [ + null, + "#000000", + "#9db13a", + "#9db13a", + "#9db13a", + "#9db13a", + "#9db13a", + "#9db13a", + "#9db13a", + "#9db13a", + "#9db13a", + "#000000", + "#af2866", + "#af2866", + "#a87d2e", + "#000000" + ], + [ + null, + "#000000", + "#9db13a", + "#9db13a", + "#9db13a", + "#9db13a", + "#9db13a", + "#9db13a", + "#9db13a", + "#9db13a", + "#000000", + "#000000", + "#9db13a", + "#000000", + "#000000", + null + ], + [ + null, + null, + "#000000", + "#9db13a", + "#ff5eeb", + "#9db13a", + "#000000", + "#000000", + "#000000", + "#000000", + null, + "#000000", + "#9db13a", + "#000000", + null, + null + ], + [ + null, + null, + "#000000", + "#9db13a", + "#9db13a", + "#000000", + null, + null, + null, + null, + null, + "#000000", + "#000000", + null, + null, + null + ], + [ + null, + null, + null, + "#000000", + "#000000", + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ] + ], + "offsetX": 641.1484375, + "offsetY": 218.625, + "hasPixels": false + } + ], + "isPaletteVisible": true, + "globalOffsetX": -87.890625, + "globalOffsetY": 0 + } +} \ No newline at end of file diff --git a/html/rogue/index.html b/html/rogue/index.html index 362fa7a..655c4cb 100644 --- a/html/rogue/index.html +++ b/html/rogue/index.html @@ -20,12 +20,18 @@ <body> <canvas id="gameCanvas"></canvas> <script src="js/config.js"></script> + <script src="js/animation.js"></script> <script src="js/hex.js"></script> <script src="js/camera.js"></script> <script src="js/fow.js"></script> <script src="js/items.js"></script> <script src="js/inventory-ui.js"></script> <script src="js/player.js"></script> + <script src="js/debug.js"></script> <script src="js/game.js"></script> + <script src="js/utils.js"></script> + <script src="js/state.js"></script> + <script src="js/renderer.js"></script> + <script src="js/events.js"></script> </body> </html> \ No newline at end of file diff --git a/html/rogue/js/animation.js b/html/rogue/js/animation.js new file mode 100644 index 0000000..d682b01 --- /dev/null +++ b/html/rogue/js/animation.js @@ -0,0 +1,53 @@ +const Animation = { + // Track loaded images with their paths as keys + images: new Map(), + + // Load an image and return a promise + loadImage(path) { + if (this.images.has(path)) { + return Promise.resolve(this.images.get(path)); + } + + return new Promise((resolve, reject) => { + const img = new Image(); + img.onload = () => { + this.images.set(path, img); + resolve(img); + }; + img.onerror = () => reject(new Error(`Failed to load image: ${path}`)); + img.src = path; + }); + }, + + // Animation class to handle sprite animations + createAnimation(frames, frameTime) { + return { + frames, + frameTime, + currentFrame: 0, + lastFrameTime: 0, + + update(currentTime) { + if (currentTime - this.lastFrameTime >= this.frameTime) { + this.currentFrame = (this.currentFrame + 1) % this.frames.length; + this.lastFrameTime = currentTime; + } + return this.frames[this.currentFrame]; + } + }; + }, + + // Add this new method to scale sprites to fit within bounds + scaleToFit(image, maxWidth, maxHeight) { + const scale = Math.min( + maxWidth / image.width, + maxHeight / image.height + ); + + return { + width: image.width * scale, + height: image.height * scale, + scale + }; + } +}; \ No newline at end of file diff --git a/html/rogue/js/camera.js b/html/rogue/js/camera.js index aaeeea9..05e0f28 100644 --- a/html/rogue/js/camera.js +++ b/html/rogue/js/camera.js @@ -4,31 +4,90 @@ const Camera = { centerOn(hex) { const pixelCoord = HexGrid.toPixel(hex); + + // Calculate desired camera position this.x = pixelCoord.x - state.canvas.width / 2; this.y = pixelCoord.y - state.canvas.height / 2; + + // Calculate grid dimensions + const gridPixelWidth = Config.hex.GRID_SIZE * Config.hex.WIDTH; + const gridPixelHeight = Config.hex.GRID_SIZE * Config.hex.HEIGHT; + + // Calculate grid bounds (accounting for centered grid) + const minX = -gridPixelWidth / 2; + const maxX = gridPixelWidth / 2 - state.canvas.width; + const minY = -gridPixelHeight / 2; + const maxY = gridPixelHeight / 2 - state.canvas.height; + + // Keep camera within grid bounds + this.x = Math.max(minX, Math.min(this.x, maxX)); + this.y = Math.max(minY, Math.min(this.y, maxY)); + + // Round to prevent sub-pixel rendering + this.x = Math.round(this.x); + this.y = Math.round(this.y); }, smoothFollow(target) { const targetPixel = HexGrid.toPixel(target); - const screenX = targetPixel.x - this.x; - const screenY = targetPixel.y - this.y; + const screenX = Math.round(targetPixel.x - this.x); + const screenY = Math.round(targetPixel.y - this.y); const centerX = state.canvas.width / 2; const centerY = state.canvas.height / 2; - // Distance from center of the screen + // Determine if we're on a narrow screen + const isNarrowScreen = state.canvas.width <= Config.camera.NARROW_SCREEN_THRESHOLD; + + // Calculate dynamic deadzones based on screen size + const deadzoneX = Math.min( + Math.max( + state.canvas.width * ( + isNarrowScreen ? + Config.camera.DEADZONE_RATIO_X.NARROW : + Config.camera.DEADZONE_RATIO_X.WIDE + ), + Config.camera.MIN_DEADZONE + ), + Config.camera.MAX_DEADZONE + ); + + const deadzoneY = Math.min( + Math.max(state.canvas.height * Config.camera.DEADZONE_RATIO_Y, + Config.camera.MIN_DEADZONE + ), + Config.camera.MAX_DEADZONE + ); + const deltaX = screenX - centerX; const deltaY = screenY - centerY; - // Only move the camera if the player is outside of the deadzone - if (Math.abs(deltaX) > Config.camera.DEADZONE_X) { - const adjustX = deltaX - (deltaX > 0 ? Config.camera.DEADZONE_X : -Config.camera.DEADZONE_X); - this.x += adjustX * Config.camera.FOLLOW_SPEED; + // Use more aggressive follow speed for narrow screens + const followSpeed = isNarrowScreen ? + Config.camera.FOLLOW_SPEED * 1.5 : + Config.camera.FOLLOW_SPEED; + + // Ensure camera moves if player is near screen edges + if (Math.abs(deltaX) > deadzoneX) { + const adjustX = deltaX - (deltaX > 0 ? deadzoneX : -deadzoneX); + this.x += Math.round(adjustX * followSpeed); } - if (Math.abs(deltaY) > Config.camera.DEADZONE_Y) { - const adjustY = deltaY - (deltaY > 0 ? Config.camera.DEADZONE_Y : -Config.camera.DEADZONE_Y); - this.y += adjustY * Config.camera.FOLLOW_SPEED; + if (Math.abs(deltaY) > deadzoneY) { + const adjustY = deltaY - (deltaY > 0 ? deadzoneY : -deadzoneY); + this.y += Math.round(adjustY * followSpeed); } + + // Calculate grid bounds (accounting for centered grid) + const gridPixelWidth = Config.hex.GRID_SIZE * Config.hex.WIDTH; + const gridPixelHeight = Config.hex.GRID_SIZE * Config.hex.HEIGHT; + const minX = -gridPixelWidth / 2; + const maxX = gridPixelWidth / 2 - state.canvas.width; + const minY = -gridPixelHeight / 2; + const maxY = gridPixelHeight / 2 - state.canvas.height; + + // Keep camera within grid bounds + this.x = Math.max(minX, Math.min(this.x, maxX)); + this.y = Math.max(minY, Math.min(this.y, maxY)); } }; \ No newline at end of file diff --git a/html/rogue/js/config.js b/html/rogue/js/config.js index 90c1f49..6ed925f 100644 --- a/html/rogue/js/config.js +++ b/html/rogue/js/config.js @@ -1,14 +1,30 @@ const Config = { colors: { - BACKGROUND: 'rgba(135, 206, 235, 0.3)', - GRID: '#333333', + BACKGROUND: 'rgba(135, 207, 235, 1)', + GRID: 'rgba(0, 0, 0, 0.25)', PLAYER: 'red', - HEX_FILL: '#ffffff' + HEX_FILL: '#ffffff', + FOG: { + HIDDEN: 'rgba(0, 0, 0, 1)', + REVEALED: 'rgba(0, 0, 0, 0.25)', + GRID_DIM: 'rgba(0, 0, 0, 0.0)' + }, + UI: { + INVENTORY: { + BACKGROUND: 'rgba(0, 0, 0, 0.7)', + WINDOW: '#ffffff', + TEXT: '#000000' + } + }, + ITEMS: { + COIN: '#FFD700', + GEM: '#50C878' + } }, hex: { SIZE: 40, // Size of a single hex - GRID_SIZE: 10, // Number of hexes in the grid (width/height) + GRID_SIZE: 30, // Number of hexes in the grid (width/height) get WIDTH() { // Computed hex width return this.SIZE * 2; }, @@ -27,12 +43,55 @@ const Config = { player: { MOVE_SPEED: 0.1, SIZE_RATIO: 1/3, - VISION_RANGE: 3 + VISION_RANGE: 3, + SPRITE_SCALE: 0.8, + ANIMATION_SPEED: 500 }, camera: { - FOLLOW_SPEED: 0.1, // Camera smoothing factor - DEADZONE_X: 200, // Horizontal deadzone in pixels - DEADZONE_Y: 150 // Vertical deadzone in pixels + FOLLOW_SPEED: 0.1, + DEADZONE_RATIO_X: { + NARROW: 0.1, + WIDE: 0.2 + }, + DEADZONE_RATIO_Y: 0.2, + MIN_DEADZONE: 30, + MAX_DEADZONE: 200, + NARROW_SCREEN_THRESHOLD: 600 + }, + + ui: { + inventory: { + PADDING: 20, + WIDTH: 300, + HEIGHT: 400, + TITLE_FONT: '20px Arial', + ITEM_FONT: '16px Arial', + ITEM_SPACING: 30, + TITLE_OFFSET: 20, + ITEMS_START_OFFSET: 60 + } + }, + + items: { + SPAWN_COUNT: 10, + types: { + COIN: { + name: 'Coin', + size: 0.2 + }, + GEM: { + name: 'Gem', + size: 0.25 + } + } + }, + + fog: { + states: { + HIDDEN: { alpha: 1.0 }, + REVEALED: { alpha: 0.5 }, + VISIBLE: { alpha: 0 } + } } }; \ No newline at end of file diff --git a/html/rogue/js/debug.js b/html/rogue/js/debug.js new file mode 100644 index 0000000..f2e0b02 --- /dev/null +++ b/html/rogue/js/debug.js @@ -0,0 +1,113 @@ +const Debug = { + isEnabled: false, + lastFrameTime: performance.now(), + frameCount: 0, + fps: 0, + fpsUpdateInterval: 500, // Update FPS display every 500ms + lastFpsUpdate: 0, + + init() { + // Add keyboard listener for debug toggle + window.addEventListener('keydown', (e) => { + if (e.key.toLowerCase() === 'd') { + this.isEnabled = !this.isEnabled; + } + }); + }, + + update(currentTime) { + if (!this.isEnabled) return; + + this.frameCount++; + + // Update FPS counter every 500ms + if (currentTime - this.lastFpsUpdate >= this.fpsUpdateInterval) { + this.fps = Math.round((this.frameCount * 1000) / (currentTime - this.lastFpsUpdate)); + this.frameCount = 0; + this.lastFpsUpdate = currentTime; + } + + this.lastFrameTime = currentTime; + }, + + draw(ctx) { + if (!this.isEnabled) return; + + const padding = 30; + const lineHeight = 20; + let y = padding; + + // Save context state + ctx.save(); + + // Set up debug text style + ctx.fillStyle = 'rgba(0, 0, 0, 0.7)'; + ctx.fillRect(0, 0, 300, 200); + ctx.font = '14px monospace'; + ctx.fillStyle = '#00FF00'; + + // Display debug information + const debugInfo = [ + `FPS: ${this.fps}`, + `Camera: (${Math.round(Camera.x)}, ${Math.round(Camera.y)})`, + `Player Hex: (${player.position.q}, ${player.position.r})`, + `Screen: ${state.canvas.width}x${state.canvas.height}`, + `Inventory Items: ${player.inventory.length}`, + `Revealed Hexes: ${FogOfWar.revealed.size}`, + `Moving: ${player.target ? 'Yes' : 'No'}`, + `Narrow Screen: ${state.canvas.width <= Config.camera.NARROW_SCREEN_THRESHOLD}` + ]; + + debugInfo.forEach(info => { + ctx.fillText(info, padding, y); + y += lineHeight; + }); + + // Draw deadzone visualization + if (player.target) { + const isNarrowScreen = state.canvas.width <= Config.camera.NARROW_SCREEN_THRESHOLD; + const deadzoneX = Math.min( + Math.max( + state.canvas.width * ( + isNarrowScreen ? + Config.camera.DEADZONE_RATIO_X.NARROW : + Config.camera.DEADZONE_RATIO_X.WIDE + ), + Config.camera.MIN_DEADZONE + ), + Config.camera.MAX_DEADZONE + ); + const deadzoneY = Math.min( + Math.max(state.canvas.height * Config.camera.DEADZONE_RATIO_Y, + Config.camera.MIN_DEADZONE + ), + Config.camera.MAX_DEADZONE + ); + + // Draw camera deadzone + ctx.strokeStyle = 'rgba(255, 162, 0, 1)'; + ctx.lineWidth = 2; + ctx.strokeRect( + state.canvas.width/2 - deadzoneX, + state.canvas.height/2 - deadzoneY, + deadzoneX * 2, + deadzoneY * 2 + ); + + // Draw a small cross at the center of the camera deadzone + const centerX = state.canvas.width / 2; + const centerY = state.canvas.height / 2; + const crossSize = 10; // Size of the cross arms + + ctx.beginPath(); + ctx.moveTo(centerX - crossSize, centerY); + ctx.lineTo(centerX + crossSize, centerY); + ctx.moveTo(centerX, centerY - crossSize); + ctx.lineTo(centerX, centerY + crossSize); + ctx.stroke(); + } + + // Restore context state + ctx.restore(); + } +}; \ No newline at end of file diff --git a/html/rogue/js/events.js b/html/rogue/js/events.js new file mode 100644 index 0000000..9ae8241 --- /dev/null +++ b/html/rogue/js/events.js @@ -0,0 +1,16 @@ +const EventSystem = { + listeners: new Map(), + + on(event, callback) { + if (!this.listeners.has(event)) { + this.listeners.set(event, new Set()); + } + this.listeners.get(event).add(callback); + }, + + emit(event, data) { + if (this.listeners.has(event)) { + this.listeners.get(event).forEach(callback => callback(data)); + } + } +}; \ No newline at end of file diff --git a/html/rogue/js/fow.js b/html/rogue/js/fow.js index 77f55c0..291c862 100644 --- a/html/rogue/js/fow.js +++ b/html/rogue/js/fow.js @@ -53,33 +53,32 @@ const FogOfWar = { } }, + getFogState(hex) { + if (!this.isRevealed(hex)) return Config.fog.states.HIDDEN; + if (!this.isVisible(hex)) return Config.fog.states.REVEALED; + return Config.fog.states.VISIBLE; + }, + // Draw fog of war effect draw(ctx) { - // Draw fog over unrevealed areas HexGrid.getViewportHexes().forEach(hex => { - if (!this.isRevealed(hex) || !this.isVisible(hex)) { - const pixel = HexGrid.toPixel(hex); - const screenX = pixel.x - Camera.x; - const screenY = pixel.y - Camera.y; + const fogState = this.getFogState(hex); + if (fogState.alpha > 0) { + const screen = HexGrid.toScreenCoordinates(hex, Camera); - ctx.fillStyle = this.isRevealed(hex) ? - 'rgba(0, 0, 0, 0.5)' : // Darker fog for unexplored areas - 'rgba(0, 0, 0, 0.8)'; // Lighter fog for explored but not visible + // Draw fog fill + ctx.fillStyle = fogState === Config.fog.states.HIDDEN ? + Config.colors.FOG.HIDDEN : + Config.colors.FOG.REVEALED; + HexGrid.drawHexPath(ctx, screen.x, screen.y, HexGrid.SIZE, 1); + ctx.fill(); - // Draw fog hex - ctx.beginPath(); - for (let i = 0; i < 6; i++) { - const angle = 2 * Math.PI / 6 * i; - const xPos = screenX + HexGrid.SIZE * Math.cos(angle); - const yPos = screenY + HexGrid.SIZE * Math.sin(angle); - if (i === 0) { - ctx.moveTo(xPos, yPos); - } else { - ctx.lineTo(xPos, yPos); - } + // Draw grid lines only for revealed but not visible hexes + if (fogState === Config.fog.states.REVEALED) { + ctx.strokeStyle = Config.colors.FOG.GRID_DIM; + ctx.lineWidth = 1; + ctx.stroke(); } - ctx.closePath(); - ctx.fill(); } }); } diff --git a/html/rogue/js/game.js b/html/rogue/js/game.js index 918f8e5..48133d8 100644 --- a/html/rogue/js/game.js +++ b/html/rogue/js/game.js @@ -7,11 +7,12 @@ const state = { ctx: null }; -function init() { +async function init() { state.canvas = document.getElementById('gameCanvas'); state.ctx = state.canvas.getContext('2d'); - player.init(); + await player.init(); + Debug.init(); function resize() { state.canvas.width = window.innerWidth; @@ -29,86 +30,72 @@ function init() { } function drawHex(ctx, x, y, hex) { - const screenX = x - Camera.x; - const screenY = y - Camera.y; + const screen = HexGrid.toScreenCoordinates(hex, Camera); - // Only draw if hex is visible on screen (with some padding) - if (screenX < -HexGrid.WIDTH || screenX > state.canvas.width + HexGrid.WIDTH || - screenY < -HexGrid.HEIGHT || screenY > state.canvas.height + HexGrid.HEIGHT) { + // Only draw if hex is visible on screen (with padding) + if (!HexGrid.isInViewport(screen.x, screen.y, state.canvas)) { return; } - ctx.beginPath(); - for (let i = 0; i < 6; i++) { - const angle = 2 * Math.PI / 6 * i; - const xPos = screenX + HexGrid.SIZE * Math.cos(angle); - const yPos = screenY + HexGrid.SIZE * Math.sin(angle); - if (i === 0) { - ctx.moveTo(xPos, yPos); - } else { - ctx.lineTo(xPos, yPos); - } - } - ctx.closePath(); - - // Fill hex with appropriate color - if (HexGrid.isPassable(hex)) { - ctx.fillStyle = Config.colors.HEX_FILL; - } else { - ctx.fillStyle = Config.colors.BACKGROUND; + // Skip drawing completely if hex hasn't been revealed + if (!FogOfWar.isRevealed(hex)) { + return; } + + // Draw the hex fill + HexGrid.drawHexPath(ctx, screen.x, screen.y); + ctx.fillStyle = Config.colors.HEX_FILL; ctx.fill(); - // Draw border - ctx.strokeStyle = HexGrid.COLOR; - ctx.lineWidth = 1; - ctx.stroke(); + // Only draw grid lines for currently visible hexes + // (fog of war will handle the grid lines for revealed but not visible hexes) + if (FogOfWar.isVisible(hex)) { + ctx.strokeStyle = Config.colors.GRID; + ctx.lineWidth = 1; + ctx.stroke(); + } } function gameLoop(currentTime) { + requestAnimationFrame(gameLoop); + if (currentTime - lastFrameTime < Config.game.FRAME_TIME) { - requestAnimationFrame(gameLoop); return; } - const deltaTime = currentTime - lastFrameTime; + const deltaTime = Math.min(currentTime - lastFrameTime, Config.game.FRAME_TIME * 2); lastFrameTime = currentTime; - // Clear the entire canvas first - state.ctx.clearRect(0, 0, state.canvas.width, state.canvas.height); - - // Then fill with background color + // Update debug information + Debug.update(currentTime); + + // Clear with background state.ctx.fillStyle = Config.colors.BACKGROUND; state.ctx.fillRect(0, 0, state.canvas.width, state.canvas.height); + // Round camera position to prevent sub-pixel rendering + Camera.x = Math.round(Camera.x); + Camera.y = Math.round(Camera.y); + player.update(); Camera.smoothFollow(player.getCurrentPosition()); - // Update fog of war when player moves if (player.hasMoved) { FogOfWar.updateVisibility(player.position); player.hasMoved = false; } - // Draw layers in correct order + // Draw everything in one pass to prevent flicker HexGrid.getViewportHexes().forEach(hex => { const pixel = HexGrid.toPixel(hex); - drawHex(state.ctx, pixel.x, pixel.y, hex); + drawHex(state.ctx, Math.round(pixel.x), Math.round(pixel.y), hex); }); - // Draw items Items.draw(state.ctx, HexGrid.toPixel.bind(HexGrid), Camera, HexGrid.SIZE); - - // Draw player player.draw(state.ctx, HexGrid.toPixel.bind(HexGrid), Camera, HexGrid.SIZE); - - // Draw fog of war FogOfWar.draw(state.ctx); - - // Draw inventory UI last InventoryUI.draw(state.ctx); - - requestAnimationFrame(gameLoop); + Debug.draw(state.ctx); } function handleClick(event) { @@ -131,4 +118,4 @@ function handleClick(event) { } } -init(); \ No newline at end of file +init().catch(console.error); \ No newline at end of file diff --git a/html/rogue/js/hex.js b/html/rogue/js/hex.js index 0d1c2e5..fa08e3d 100644 --- a/html/rogue/js/hex.js +++ b/html/rogue/js/hex.js @@ -1,20 +1,20 @@ -// Hex grid utilities and calculations +// This that witchy shit -- we be hexin! + const HexGrid = { get SIZE() { return Config.hex.SIZE }, get WIDTH() { return Config.hex.WIDTH }, get HEIGHT() { return Config.hex.HEIGHT }, get GRID_SIZE() { return Config.hex.GRID_SIZE }, COLOR: Config.colors.GRID, - IMPASSABLE_COLOR: Config.colors.BACKGROUND, - // Convert hex coordinates to pixel coordinates + // hex to pixel toPixel(hex) { const x = this.SIZE * (3/2 * hex.q); const y = this.SIZE * (Math.sqrt(3)/2 * hex.q + Math.sqrt(3) * hex.r); return {x, y}; }, - // Convert pixel coordinates to hex coordinates + // pixel to hex fromPixel(x, y) { const q = (2/3 * x) / this.SIZE; const r = (-1/3 * x + Math.sqrt(3)/3 * y) / this.SIZE; @@ -46,7 +46,7 @@ const HexGrid = { return {q: rx, r: rz}; }, - // Calculate visible hexes + // Is this hex in the viewport? getViewportHexes() { const hexes = []; const halfGrid = Math.floor(this.GRID_SIZE / 2); @@ -59,9 +59,40 @@ const HexGrid = { return hexes; }, - // Add this method to check if a hex is passable + // Check if a hex is passable isPassable(hex) { const halfGrid = Math.floor(this.GRID_SIZE / 2); return Math.abs(hex.q) <= halfGrid && Math.abs(hex.r) <= halfGrid; + }, + + // Centralized hex drawing function + drawHexPath(ctx, x, y, size = this.SIZE, padding = 0) { + ctx.beginPath(); + for (let i = 0; i < 6; i++) { + const angle = 2 * Math.PI / 6 * i; + const xPos = Math.round(x + (size + padding) * Math.cos(angle)); + const yPos = Math.round(y + (size + padding) * Math.sin(angle)); + if (i === 0) { + ctx.moveTo(xPos, yPos); + } else { + ctx.lineTo(xPos, yPos); + } + } + ctx.closePath(); + }, + + toScreenCoordinates(hex, camera) { + const pixel = this.toPixel(hex); + return { + x: Math.round(pixel.x - camera.x), + y: Math.round(pixel.y - camera.y) + }; + }, + + isInViewport(screenX, screenY, canvas) { + return !(screenX < -this.WIDTH || + screenX > canvas.width + this.WIDTH || + screenY < -this.HEIGHT || + screenY > canvas.height + this.HEIGHT); } }; \ No newline at end of file diff --git a/html/rogue/js/inventory-ui.js b/html/rogue/js/inventory-ui.js index a5c7c3f..c7ce63c 100644 --- a/html/rogue/js/inventory-ui.js +++ b/html/rogue/js/inventory-ui.js @@ -21,33 +21,42 @@ const InventoryUI = { if (!this.isOpen) return; // Draw semi-transparent background - ctx.fillStyle = 'rgba(0, 0, 0, 0.7)'; + ctx.fillStyle = Config.colors.UI.INVENTORY.BACKGROUND; ctx.fillRect(0, 0, state.canvas.width, state.canvas.height); - // Draw inventory window - const padding = 20; - const width = 300; - const height = 400; - const x = (state.canvas.width - width) / 2; - const y = (state.canvas.height - height) / 2; + // Calculate positions ensuring integer values + const padding = Config.ui.inventory.PADDING; + const width = Config.ui.inventory.WIDTH; + const height = Config.ui.inventory.HEIGHT; + const x = Math.round((state.canvas.width - width) / 2); + const y = Math.round((state.canvas.height - height) / 2); // Draw window background - ctx.fillStyle = '#ffffff'; + ctx.fillStyle = Config.colors.UI.INVENTORY.WINDOW; ctx.fillRect(x, y, width, height); + // Set text rendering properties for sharper text + ctx.textBaseline = 'top'; + ctx.textAlign = 'left'; + ctx.imageSmoothingEnabled = false; + // Draw title - ctx.fillStyle = '#000000'; - ctx.font = '20px Arial'; - ctx.fillText('Inventory', x + padding, y + padding + 20); + ctx.fillStyle = Config.colors.UI.INVENTORY.TEXT; + ctx.font = Config.ui.inventory.TITLE_FONT; + const titleX = Math.round(x + padding); + const titleY = Math.round(y + padding); + ctx.fillText('Inventory', titleX, titleY); // Get item counts and draw items with quantities const itemCounts = this.getItemCounts(); let index = 0; itemCounts.forEach((count, itemName) => { - const itemY = y + padding + 60 + (index * 30); - ctx.font = '16px Arial'; - ctx.fillText(`${itemName} x${count}`, x + padding, itemY); + const itemX = Math.round(x + padding); + const itemY = Math.round(y + Config.ui.inventory.ITEMS_START_OFFSET + + (index * Config.ui.inventory.ITEM_SPACING)); + ctx.font = Config.ui.inventory.ITEM_FONT; + ctx.fillText(`${itemName} x${count}`, itemX, itemY); index++; }); } diff --git a/html/rogue/js/player.js b/html/rogue/js/player.js index 3c1c383..efecbdf 100644 --- a/html/rogue/js/player.js +++ b/html/rogue/js/player.js @@ -7,12 +7,30 @@ const player = { moveSpeed: Config.player.MOVE_SPEED, // Movement speed (0 to 1 per frame) inventory: [], + // Animation properties + animation: null, + sprites: [], + // Initialize player - init() { + async init() { this.position = { q: 0, r: 0 }; this.target = null; this.path = []; this.inventory = []; + + // Load sprites + try { + const [sprite1, sprite2] = await Promise.all([ + Animation.loadImage('assets/home/goblin-01.png'), + Animation.loadImage('assets/home/goblin-02.png') + ]); + + this.sprites = [sprite1, sprite2]; + this.animation = Animation.createAnimation(this.sprites, 500); // 500ms per frame + } catch (error) { + console.error('Failed to load player sprites:', error); + } + return this; }, @@ -151,14 +169,63 @@ const player = { const screenX = pixelPos.x - camera.x; const screenY = pixelPos.y - camera.y; - ctx.fillStyle = Config.colors.PLAYER; - ctx.beginPath(); - ctx.arc(screenX, screenY, HEX_SIZE * Config.player.SIZE_RATIO, 0, Math.PI * 2); - ctx.fill(); + if (this.animation && this.sprites.length > 0) { + // Get current sprite from animation + const currentSprite = this.animation.update(performance.now()); + + // Scale sprite to fit within hex + // Use slightly smaller than hex size to ensure it fits visually + const hexInnerSize = HEX_SIZE * 0.8; // 80% of hex size + const { width, height, scale } = Animation.scaleToFit( + currentSprite, + hexInnerSize * 2, // width + hexInnerSize * Math.sqrt(3) // height (hex height) + ); + + // Calculate position to center the sprite in the hex + const spriteX = screenX - width / 2; + const spriteY = screenY - height / 2; + + // Save context state + ctx.save(); + + // Optional: add a small bounce effect when moving + if (this.target) { + const bounce = Math.sin(performance.now() / 100) * 2; + ctx.translate(spriteX, spriteY + bounce); + } else { + ctx.translate(spriteX, spriteY); + } + + // Draw the sprite + ctx.drawImage( + currentSprite, + 0, 0, + width, + height + ); + + // Restore context state + ctx.restore(); + + // Debug: draw hex bounds if debug is enabled + if (Debug.isEnabled) { + ctx.strokeStyle = 'rgba(255, 0, 0, 0.5)'; + ctx.beginPath(); + HexGrid.drawHexPath(ctx, screenX, screenY, HEX_SIZE * 0.8); + ctx.stroke(); + } + } else { + // Fallback to circle if sprites aren't loaded + ctx.fillStyle = Config.colors.PLAYER; + ctx.beginPath(); + ctx.arc(screenX, screenY, HEX_SIZE * Config.player.SIZE_RATIO, 0, Math.PI * 2); + ctx.fill(); + } // Draw path if needed if (this.path.length > 0) { - ctx.strokeStyle = Config.colors.PLAYER + '4D'; // 30% opacity version of player color + ctx.strokeStyle = Config.colors.PLAYER + '4D'; ctx.beginPath(); let lastPos = this.target || this.position; this.path.forEach(point => { diff --git a/html/rogue/js/renderer.js b/html/rogue/js/renderer.js new file mode 100644 index 0000000..3e64666 --- /dev/null +++ b/html/rogue/js/renderer.js @@ -0,0 +1,29 @@ +const Renderer = { + drawHex(ctx, hex, x, y, size, fillStyle, strokeStyle) { + ctx.beginPath(); + for (let i = 0; i < 6; i++) { + const angle = 2 * Math.PI / 6 * i; + const xPos = x + size * Math.cos(angle); + const yPos = y + size * Math.sin(angle); + if (i === 0) ctx.moveTo(xPos, yPos); + else ctx.lineTo(xPos, yPos); + } + ctx.closePath(); + + if (fillStyle) { + ctx.fillStyle = fillStyle; + ctx.fill(); + } + if (strokeStyle) { + ctx.strokeStyle = strokeStyle; + ctx.stroke(); + } + }, + + drawCircle(ctx, x, y, radius, fillStyle) { + ctx.fillStyle = fillStyle; + ctx.beginPath(); + ctx.arc(x, y, radius, 0, Math.PI * 2); + ctx.fill(); + } +}; \ No newline at end of file diff --git a/html/rogue/js/state.js b/html/rogue/js/state.js new file mode 100644 index 0000000..f5c713d --- /dev/null +++ b/html/rogue/js/state.js @@ -0,0 +1,17 @@ +const GameState = { + canvas: null, + ctx: null, + lastFrameTime: 0, + + init() { + this.canvas = document.getElementById('gameCanvas'); + this.ctx = this.canvas.getContext('2d'); + this.lastFrameTime = 0; + }, + + resize() { + this.canvas.width = window.innerWidth; + this.canvas.height = window.innerHeight; + Camera.centerOn(player.position); + } +}; \ No newline at end of file diff --git a/html/rogue/js/utils.js b/html/rogue/js/utils.js new file mode 100644 index 0000000..bd329fb --- /dev/null +++ b/html/rogue/js/utils.js @@ -0,0 +1,25 @@ +const Utils = { + hexToKey(hex) { + return `${hex.q},${hex.r}`; + }, + + keyToHex(key) { + const [q, r] = key.split(',').map(Number); + return { q, r }; + }, + + // Screen/canvas coordinate utilities + screenToCanvas(x, y, camera) { + return { + x: x + camera.x, + y: y + camera.y + }; + }, + + canvasToScreen(x, y, camera) { + return { + x: x - camera.x, + y: y - camera.y + }; + } +}; \ No newline at end of file diff --git a/html/voxels/index.html b/html/voxels/index.html new file mode 100644 index 0000000..fda7eba --- /dev/null +++ b/html/voxels/index.html @@ -0,0 +1,22 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <title>Isometric Game</title> + <style> + body { + margin: 0; + overflow: hidden; + } + canvas { + display: block; + width: 100vw; + height: 100vh; + background: #bce8ff; + } + </style> +</head> +<body> + <canvas id="gameCanvas"></canvas> + <script src="js/game.js"></script> +</body> +</html> \ No newline at end of file diff --git a/html/voxels/js/game.js b/html/voxels/js/game.js new file mode 100644 index 0000000..2ced6fd --- /dev/null +++ b/html/voxels/js/game.js @@ -0,0 +1,362 @@ +class IsometricGame { + constructor() { + this.canvas = document.getElementById('gameCanvas'); + this.ctx = this.canvas.getContext('2d'); + + // Grid properties + this.gridSize = 10; + this.tileWidth = 50; + this.tileHeight = 25; + + // Player properties + this.player = { + x: 0, + y: 0, + targetX: 0, + targetY: 0, + size: 20, + path: [], // Array to store waypoints + currentWaypoint: null, + jumpHeight: 0, + jumpProgress: 0, + isJumping: false, + startX: 0, + startY: 0 + }; + + // Add particle system + this.particles = []; + + // Handle window resize + this.resizeCanvas(); + window.addEventListener('resize', () => this.resizeCanvas()); + + this.setupEventListeners(); + this.gameLoop(); + } + + resizeCanvas() { + this.canvas.width = window.innerWidth; + this.canvas.height = window.innerHeight; + + // Recalculate grid offset to center it + this.offsetX = this.canvas.width / 2; + this.offsetY = this.canvas.height / 3; + + // Scale tile size based on screen size + const minDimension = Math.min(this.canvas.width, this.canvas.height); + const scaleFactor = minDimension / 800; // 800 is our reference size + this.tileWidth = 50 * scaleFactor; + this.tileHeight = 25 * scaleFactor; + this.player.size = 20 * scaleFactor; + } + + toIsometric(x, y) { + return { + x: (x - y) * this.tileWidth / 2, + y: (x + y) * this.tileHeight / 2 + }; + } + + fromIsometric(screenX, screenY) { + // Convert screen coordinates back to grid coordinates + screenX -= this.offsetX; + screenY -= this.offsetY; + + const x = (screenX / this.tileWidth + screenY / this.tileHeight) / 1; + const y = (screenY / this.tileHeight - screenX / this.tileWidth) / 1; + + return { x: Math.round(x), y: Math.round(y) }; + } + + drawGrid() { + for (let x = 0; x < this.gridSize; x++) { + for (let y = 0; y < this.gridSize; y++) { + const iso = this.toIsometric(x, y); + + // Draw tile + this.ctx.beginPath(); + this.ctx.moveTo(iso.x + this.offsetX, iso.y + this.offsetY - this.tileHeight/2); + this.ctx.lineTo(iso.x + this.offsetX + this.tileWidth/2, iso.y + this.offsetY); + this.ctx.lineTo(iso.x + this.offsetX, iso.y + this.offsetY + this.tileHeight/2); + this.ctx.lineTo(iso.x + this.offsetX - this.tileWidth/2, iso.y + this.offsetY); + this.ctx.closePath(); + + this.ctx.strokeStyle = '#666'; + this.ctx.stroke(); + this.ctx.fillStyle = '#fff'; + this.ctx.fill(); + } + } + } + + drawPlayer() { + // Convert player grid position to isometric coordinates + const iso = this.toIsometric(this.player.x, this.player.y); + + // Apply jump height offset + const jumpOffset = this.player.jumpHeight || 0; + + // Calculate squash and stretch based on jump progress + let squashStretch = 1; + if (this.player.isJumping) { + // Stretch at the start and middle of jump, squash at landing + const jumpPhase = Math.sin(this.player.jumpProgress * Math.PI); + if (this.player.jumpProgress < 0.2) { + // Initial stretch when jumping + squashStretch = 1 + (0.3 * (1 - this.player.jumpProgress / 0.2)); + } else if (this.player.jumpProgress > 0.8) { + // Squash when landing + squashStretch = 1 - (0.3 * ((this.player.jumpProgress - 0.8) / 0.2)); + } else { + // Slight stretch at peak of jump + squashStretch = 1 + (0.1 * jumpPhase); + } + } + + // Draw player shadow (gets smaller when jumping) + const shadowScale = Math.max(0.2, 1 - (jumpOffset / this.tileHeight)); + this.ctx.beginPath(); + this.ctx.ellipse( + iso.x + this.offsetX, + iso.y + this.offsetY + 2, + this.player.size * 0.8 * shadowScale, + this.player.size * 0.3 * shadowScale, + 0, + 0, + Math.PI * 2 + ); + this.ctx.fillStyle = `rgba(0,0,0,${0.2 * shadowScale})`; + this.ctx.fill(); + + // Draw player body with jump offset and squash/stretch + this.ctx.beginPath(); + this.ctx.fillStyle = '#ff4444'; + this.ctx.strokeStyle = '#aa0000'; + + const bodyHeight = this.player.size * 2 * squashStretch; + const bodyWidth = this.player.size * 0.8 * (1 / squashStretch); // Inverse stretch for width + + this.ctx.save(); + this.ctx.translate(iso.x + this.offsetX, iso.y + this.offsetY - jumpOffset); + this.ctx.scale(1, 0.5); // Apply isometric perspective + this.ctx.fillRect(-bodyWidth/2, -bodyHeight, bodyWidth, bodyHeight); + this.ctx.strokeRect(-bodyWidth/2, -bodyHeight, bodyWidth, bodyHeight); + this.ctx.restore(); + + // Draw player head with jump offset and squash/stretch + this.ctx.beginPath(); + this.ctx.ellipse( + iso.x + this.offsetX, + iso.y + this.offsetY - this.player.size * squashStretch - jumpOffset, + this.player.size * (1 / squashStretch), + this.player.size * 0.5 * squashStretch, + 0, + 0, + Math.PI * 2 + ); + this.ctx.fillStyle = '#ff4444'; + this.ctx.fill(); + this.ctx.strokeStyle = '#aa0000'; + this.ctx.stroke(); + } + + findPath(startX, startY, endX, endY) { + // Simple pathfinding that follows grid edges + const path = []; + + // First move along X axis + if (startX !== endX) { + const stepX = startX < endX ? 1 : -1; + for (let x = startX + stepX; stepX > 0 ? x <= endX : x >= endX; x += stepX) { + path.push({ x: x, y: startY }); + } + } + + // Then move along Y axis + if (startY !== endY) { + const stepY = startY < endY ? 1 : -1; + for (let y = startY + stepY; stepY > 0 ? y <= endY : y >= endY; y += stepY) { + path.push({ x: endX, y: y }); + } + } + + return path; + } + + updatePlayer() { + const jumpDuration = 0.1; // Faster jump for snappier movement + const maxJumpHeight = this.tileHeight; + + // If we don't have a current waypoint but have a path, get next waypoint + if (!this.player.currentWaypoint && this.player.path.length > 0) { + this.player.currentWaypoint = this.player.path.shift(); + this.player.isJumping = true; + this.player.jumpProgress = 0; + + // Store starting position for interpolation + this.player.startX = this.player.x; + this.player.startY = this.player.y; + } + + // Move towards current waypoint + if (this.player.currentWaypoint) { + // Update jump animation + if (this.player.isJumping) { + this.player.jumpProgress += jumpDuration; + + // Clamp progress to 1 + if (this.player.jumpProgress > 1) this.player.jumpProgress = 1; + + // Parabolic jump arc + this.player.jumpHeight = Math.sin(this.player.jumpProgress * Math.PI) * maxJumpHeight; + + // Precise interpolation between points + this.player.x = this.player.startX + (this.player.currentWaypoint.x - this.player.startX) * this.player.jumpProgress; + this.player.y = this.player.startY + (this.player.currentWaypoint.y - this.player.startY) * this.player.jumpProgress; + + // Landing + if (this.player.jumpProgress >= 1) { + this.player.isJumping = false; + this.player.jumpHeight = 0; + this.player.x = this.player.currentWaypoint.x; + this.player.y = this.player.currentWaypoint.y; + this.createDustParticles(this.player.x, this.player.y); + this.player.currentWaypoint = null; + } + } + } + } + + setupEventListeners() { + this.canvas.addEventListener('click', (e) => { + const rect = this.canvas.getBoundingClientRect(); + const clickX = e.clientX - rect.left; + const clickY = e.clientY - rect.top; + + const gridPos = this.fromIsometric(clickX, clickY); + + // Only move if within grid bounds + if (gridPos.x >= 0 && gridPos.x < this.gridSize && + gridPos.y >= 0 && gridPos.y < this.gridSize) { + + // Set target and calculate path + this.player.targetX = Math.round(gridPos.x); + this.player.targetY = Math.round(gridPos.y); + + // Calculate new path + this.player.path = this.findPath( + Math.round(this.player.x), + Math.round(this.player.y), + this.player.targetX, + this.player.targetY + ); + + // Clear current waypoint to start new path + this.player.currentWaypoint = null; + } + }); + } + + gameLoop() { + // Clear canvas + this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); + + // Draw game elements + this.drawGrid(); + this.updateParticles(); + this.drawParticles(); + this.updatePlayer(); + this.drawPlayer(); + + // Continue game loop + requestAnimationFrame(() => this.gameLoop()); + } + + // Add new particle system methods + createDustParticles(x, y) { + const particleCount = 12; // Increased for more particles + for (let i = 0; i < particleCount; i++) { + // Randomize the angle slightly + const baseAngle = (Math.PI * 2 * i) / particleCount; + const randomAngle = baseAngle + (Math.random() - 0.5) * 0.5; + + // Random speed and size variations + const speed = 0.3 + Math.random() * 0.4; + const initialSize = (this.player.size * 0.15) + (Math.random() * this.player.size * 0.15); + + // Random grey color + const greyValue = 220 + Math.floor(Math.random() * 35); + const color = `rgb(${greyValue}, ${greyValue}, ${greyValue})`; + + this.particles.push({ + x: x, + y: y, + dx: Math.cos(randomAngle) * speed, + dy: Math.sin(randomAngle) * speed, + life: 0.8 + Math.random() * 0.4, // Random initial life + size: initialSize, + color: color, + initialSize: initialSize, + rotationSpeed: (Math.random() - 0.5) * 0.2, + rotation: Math.random() * Math.PI * 2 + }); + } + } + + updateParticles() { + for (let i = this.particles.length - 1; i >= 0; i--) { + const particle = this.particles[i]; + + // Update position with slight gravity effect + particle.x += particle.dx; + particle.y += particle.dy; + particle.dy += 0.01; // Slight upward drift + + // Update rotation + particle.rotation += particle.rotationSpeed; + + // Non-linear fade out + particle.life -= 0.03; + particle.size = particle.initialSize * (particle.life * 1.5); // Grow slightly as they fade + + // Remove dead particles + if (particle.life <= 0) { + this.particles.splice(i, 1); + } + } + } + + drawParticles() { + for (const particle of this.particles) { + const iso = this.toIsometric(particle.x, particle.y); + + this.ctx.save(); + this.ctx.translate(iso.x + this.offsetX, iso.y + this.offsetY); + this.ctx.rotate(particle.rotation); + + // Draw a slightly irregular dust puff + this.ctx.beginPath(); + const points = 5; + for (let i = 0; i < points * 2; i++) { + const angle = (i * Math.PI) / points; + const radius = particle.size * (i % 2 ? 0.7 : 1); + const x = Math.cos(angle) * radius; + const y = Math.sin(angle) * radius; + if (i === 0) this.ctx.moveTo(x, y); + else this.ctx.lineTo(x, y); + } + this.ctx.closePath(); + + this.ctx.fillStyle = `rgba(${particle.color.slice(4, -1)}, ${particle.life * 0.5})`; + this.ctx.fill(); + + this.ctx.restore(); + } + } +} + +// Start the game when the page loads +window.onload = () => { + new IsometricGame(); +}; \ No newline at end of file |