diff options
Diffstat (limited to 'html/rogue/js')
-rw-r--r-- | html/rogue/js/animation.js | 53 | ||||
-rw-r--r-- | html/rogue/js/camera.js | 93 | ||||
-rw-r--r-- | html/rogue/js/config.js | 97 | ||||
-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 | 85 | ||||
-rw-r--r-- | html/rogue/js/game.js | 121 | ||||
-rw-r--r-- | html/rogue/js/hex.js | 98 | ||||
-rw-r--r-- | html/rogue/js/inventory-ui.js | 63 | ||||
-rw-r--r-- | html/rogue/js/items.js | 63 | ||||
-rw-r--r-- | html/rogue/js/player.js | 241 | ||||
-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 |
14 files changed, 1114 insertions, 0 deletions
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 new file mode 100644 index 0000000..05e0f28 --- /dev/null +++ b/html/rogue/js/camera.js @@ -0,0 +1,93 @@ +const Camera = { + x: 0, + y: 0, + + 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 = 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; + + // 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; + + // 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) > 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 new file mode 100644 index 0000000..6ed925f --- /dev/null +++ b/html/rogue/js/config.js @@ -0,0 +1,97 @@ +const Config = { + colors: { + BACKGROUND: 'rgba(135, 207, 235, 1)', + GRID: 'rgba(0, 0, 0, 0.25)', + PLAYER: 'red', + 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: 30, // Number of hexes in the grid (width/height) + get WIDTH() { // Computed hex width + return this.SIZE * 2; + }, + get HEIGHT() { // Computed hex height + return Math.sqrt(3) * this.SIZE; + } + }, + + game: { + FPS: 60, + get FRAME_TIME() { + return 1000 / this.FPS; + } + }, + + player: { + MOVE_SPEED: 0.1, + SIZE_RATIO: 1/3, + VISION_RANGE: 3, + SPRITE_SCALE: 0.8, + ANIMATION_SPEED: 500 + }, + + camera: { + 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 new file mode 100644 index 0000000..291c862 --- /dev/null +++ b/html/rogue/js/fow.js @@ -0,0 +1,85 @@ +const FogOfWar = { + // Set of revealed hex coordinates (as strings) + revealed: new Set(), + + // Configuration + VISION_RANGE: Config.player.VISION_RANGE, + + init() { + this.revealed.clear(); + this.updateVisibility(player.position); + }, + + // Convert hex to string key for Set storage + hexToKey(hex) { + return `${hex.q},${hex.r}`; + }, + + // Check if a hex is currently visible + isVisible(hex) { + const playerPos = player.getCurrentPosition(); + const distance = this.getHexDistance(hex, playerPos); + return distance <= this.VISION_RANGE; + }, + + // Check if a hex has been revealed + isRevealed(hex) { + return this.revealed.has(this.hexToKey(hex)); + }, + + // Calculate distance between two hexes + getHexDistance(a, b) { + return Math.max( + Math.abs(a.q - b.q), + Math.abs(a.r - b.r), + Math.abs((a.q + a.r) - (b.q + b.r)) + ); + }, + + // Update visibility based on player position + updateVisibility(center) { + // Get all hexes within vision range + for (let q = -this.VISION_RANGE; q <= this.VISION_RANGE; q++) { + for (let r = -this.VISION_RANGE; r <= this.VISION_RANGE; r++) { + const hex = { + q: center.q + q, + r: center.r + r + }; + + if (this.getHexDistance(center, hex) <= this.VISION_RANGE) { + this.revealed.add(this.hexToKey(hex)); + } + } + } + }, + + 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) { + HexGrid.getViewportHexes().forEach(hex => { + const fogState = this.getFogState(hex); + if (fogState.alpha > 0) { + const screen = HexGrid.toScreenCoordinates(hex, Camera); + + // 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 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(); + } + } + }); + } +}; \ No newline at end of file diff --git a/html/rogue/js/game.js b/html/rogue/js/game.js new file mode 100644 index 0000000..48133d8 --- /dev/null +++ b/html/rogue/js/game.js @@ -0,0 +1,121 @@ +const FPS = 60; +const FRAME_TIME = 1000 / FPS; +let lastFrameTime = 0; + +const state = { + canvas: null, + ctx: null +}; + +async function init() { + state.canvas = document.getElementById('gameCanvas'); + state.ctx = state.canvas.getContext('2d'); + + await player.init(); + Debug.init(); + + function resize() { + state.canvas.width = window.innerWidth; + state.canvas.height = window.innerHeight; + Camera.centerOn(player.position); + } + + window.addEventListener('resize', resize); + resize(); + state.canvas.addEventListener('click', handleClick); + + requestAnimationFrame(gameLoop); + FogOfWar.init(); + Items.init(); +} + +function drawHex(ctx, x, y, hex) { + const screen = HexGrid.toScreenCoordinates(hex, Camera); + + // Only draw if hex is visible on screen (with padding) + if (!HexGrid.isInViewport(screen.x, screen.y, state.canvas)) { + return; + } + + // 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(); + + // 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) { + return; + } + + const deltaTime = Math.min(currentTime - lastFrameTime, Config.game.FRAME_TIME * 2); + lastFrameTime = currentTime; + + // 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()); + + if (player.hasMoved) { + FogOfWar.updateVisibility(player.position); + player.hasMoved = false; + } + + // Draw everything in one pass to prevent flicker + HexGrid.getViewportHexes().forEach(hex => { + const pixel = HexGrid.toPixel(hex); + drawHex(state.ctx, Math.round(pixel.x), Math.round(pixel.y), hex); + }); + + Items.draw(state.ctx, HexGrid.toPixel.bind(HexGrid), Camera, HexGrid.SIZE); + player.draw(state.ctx, HexGrid.toPixel.bind(HexGrid), Camera, HexGrid.SIZE); + FogOfWar.draw(state.ctx); + InventoryUI.draw(state.ctx); + Debug.draw(state.ctx); +} + +function handleClick(event) { + if (InventoryUI.isOpen) { + InventoryUI.toggleInventory(); + return; + } + + const rect = state.canvas.getBoundingClientRect(); + const x = event.clientX - rect.left; + const y = event.clientY - rect.top; + + const hexCoord = HexGrid.fromPixel(x + Camera.x, y + Camera.y); + + // Check if clicking on player's position + if (hexCoord.q === player.position.q && hexCoord.r === player.position.r) { + InventoryUI.toggleInventory(); + } else { + player.moveTo(hexCoord); + } +} + +init().catch(console.error); \ No newline at end of file diff --git a/html/rogue/js/hex.js b/html/rogue/js/hex.js new file mode 100644 index 0000000..fa08e3d --- /dev/null +++ b/html/rogue/js/hex.js @@ -0,0 +1,98 @@ +// 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, + + // 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}; + }, + + // 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; + return this.round(q, r); + }, + + // Round hex coordinates to nearest hex + round(q, r) { + let x = q; + let z = r; + let y = -x-z; + + let rx = Math.round(x); + let ry = Math.round(y); + let rz = Math.round(z); + + const x_diff = Math.abs(rx - x); + const y_diff = Math.abs(ry - y); + const z_diff = Math.abs(rz - z); + + if (x_diff > y_diff && x_diff > z_diff) { + rx = -ry-rz; + } else if (y_diff > z_diff) { + ry = -rx-rz; + } else { + rz = -rx-ry; + } + + return {q: rx, r: rz}; + }, + + // Is this hex in the viewport? + getViewportHexes() { + const hexes = []; + const halfGrid = Math.floor(this.GRID_SIZE / 2); + + for (let r = -halfGrid; r < halfGrid; r++) { + for (let q = -halfGrid; q < halfGrid; q++) { + hexes.push({q, r}); + } + } + return hexes; + }, + + // 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 new file mode 100644 index 0000000..c7ce63c --- /dev/null +++ b/html/rogue/js/inventory-ui.js @@ -0,0 +1,63 @@ +const InventoryUI = { + isOpen: false, + + toggleInventory() { + this.isOpen = !this.isOpen; + }, + + // Helper function to count items by type + getItemCounts() { + const counts = new Map(); + + player.inventory.forEach(item => { + const itemName = item.type.name; + counts.set(itemName, (counts.get(itemName) || 0) + 1); + }); + + return counts; + }, + + draw(ctx) { + if (!this.isOpen) return; + + // Draw semi-transparent background + ctx.fillStyle = Config.colors.UI.INVENTORY.BACKGROUND; + ctx.fillRect(0, 0, state.canvas.width, state.canvas.height); + + // 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 = 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 = 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 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++; + }); + } +}; \ No newline at end of file diff --git a/html/rogue/js/items.js b/html/rogue/js/items.js new file mode 100644 index 0000000..dc6dc1e --- /dev/null +++ b/html/rogue/js/items.js @@ -0,0 +1,63 @@ +const Items = { + items: new Map(), // Map of items on the grid, key is "q,r" + + // Item types + TYPES: { + COIN: { + name: 'Coin', + color: '#FFD700', + size: 0.2 // Size relative to hex + }, + GEM: { + name: 'Gem', + color: '#50C878', + size: 0.25 + } + }, + + // Initialize items on the map + init() { + this.items.clear(); + + // Add some random items + for (let i = 0; i < 10; i++) { + const q = Math.floor(Math.random() * HexGrid.GRID_SIZE - HexGrid.GRID_SIZE/2); + const r = Math.floor(Math.random() * HexGrid.GRID_SIZE - HexGrid.GRID_SIZE/2); + + // Don't place items at player start position + if (q !== 0 || r !== 0) { + const type = Math.random() < 0.5 ? this.TYPES.COIN : this.TYPES.GEM; + this.addItem(q, r, type); + } + } + }, + + // Add an item to the map + addItem(q, r, type) { + this.items.set(`${q},${r}`, { type, q, r }); + }, + + // Remove an item from the map + removeItem(q, r) { + return this.items.delete(`${q},${r}`); + }, + + // Get item at position + getItem(q, r) { + return this.items.get(`${q},${r}`); + }, + + // Draw all items + draw(ctx, hexToPixel, camera, HEX_SIZE) { + this.items.forEach(item => { + const pixelPos = hexToPixel({ q: item.q, r: item.r }); + const screenX = pixelPos.x - camera.x; + const screenY = pixelPos.y - camera.y; + + ctx.fillStyle = item.type.color; + ctx.beginPath(); + ctx.arc(screenX, screenY, HEX_SIZE * item.type.size, 0, Math.PI * 2); + ctx.fill(); + }); + } +}; \ No newline at end of file diff --git a/html/rogue/js/player.js b/html/rogue/js/player.js new file mode 100644 index 0000000..efecbdf --- /dev/null +++ b/html/rogue/js/player.js @@ -0,0 +1,241 @@ +// Player state and controls +const player = { + position: { q: 0, r: 0 }, // Current hex position + target: null, // Target hex to move to + path: [], // Array of hex coordinates to follow + movementProgress: 0, // Progress of current movement (0 to 1) + moveSpeed: Config.player.MOVE_SPEED, // Movement speed (0 to 1 per frame) + inventory: [], + + // Animation properties + animation: null, + sprites: [], + + // Initialize player + 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; + }, + + // Check if a hex coordinate is within grid bounds + isValidHex(hex) { + const halfGrid = Math.floor(HexGrid.GRID_SIZE / 2); + return Math.abs(hex.q) <= halfGrid && Math.abs(hex.r) <= halfGrid; + }, + + // Get neighbors that share an edge with the given hex + getEdgeNeighbors(hex) { + const directions = [ + {q: 1, r: 0}, // East + {q: 0, r: 1}, // Southeast + {q: -1, r: 1}, // Southwest + {q: -1, r: 0}, // West + {q: 0, r: -1}, // Northwest + {q: 1, r: -1} // Northeast + ]; + + // Only return neighbors that are within grid bounds + return directions + .map(dir => ({ + q: hex.q + dir.q, + r: hex.r + dir.r + })) + .filter(hex => this.isValidHex(hex)); + }, + + // Find path from current position to target + findPath(targetHex) { + const start = this.position; + const goal = targetHex; + + // Simple breadth-first search + const queue = [[start]]; + const visited = new Set(); + const key = hex => `${hex.q},${hex.r}`; + visited.add(key(start)); + + while (queue.length > 0) { + const path = queue.shift(); + const current = path[path.length - 1]; + + if (current.q === goal.q && current.r === goal.r) { + return path; + } + + const neighbors = this.getEdgeNeighbors(current); + for (const neighbor of neighbors) { + const neighborKey = key(neighbor); + if (!visited.has(neighborKey)) { + visited.add(neighborKey); + queue.push([...path, neighbor]); + } + } + } + + return null; // No path found + }, + + // Start moving to a target hex + moveTo(targetHex) { + // Only start new movement if we're not already moving and target is valid + if (!this.target) { + // Check if target is within grid bounds + if (!this.isValidHex(targetHex)) { + return; // Ignore movement request if target is out of bounds + } + + const path = this.findPath(targetHex); + if (path) { + // Filter out any path points that would go out of bounds + this.path = path.slice(1).filter(hex => this.isValidHex(hex)); + if (this.path.length > 0) { + this.target = this.path.shift(); + this.movementProgress = 0; + } + } + } + }, + + // Add item to inventory + addToInventory(item) { + this.inventory.push(item); + }, + + // Check for and collect items + checkForItems() { + const item = Items.getItem(this.position.q, this.position.r); + if (item) { + Items.removeItem(this.position.q, this.position.r); + this.addToInventory(item); + } + }, + + // Update player position + update() { + if (this.target) { + this.movementProgress += this.moveSpeed; + + if (this.movementProgress >= 1) { + this.position = this.target; + this.target = null; + this.movementProgress = 0; + this.hasMoved = true; + + // Check for items when reaching new position + this.checkForItems(); + + if (this.path.length > 0) { + this.target = this.path.shift(); + this.movementProgress = 0; + } + } + } + }, + + // Get current interpolated position + getCurrentPosition() { + if (!this.target) { + return this.position; + } + + // Interpolate between current position and target + return { + q: this.position.q + (this.target.q - this.position.q) * this.movementProgress, + r: this.position.r + (this.target.r - this.position.r) * this.movementProgress + }; + }, + + // Draw the player + draw(ctx, hexToPixel, camera, HEX_SIZE) { + const currentPos = this.getCurrentPosition(); + const pixelPos = hexToPixel(currentPos); + const screenX = pixelPos.x - camera.x; + const screenY = pixelPos.y - camera.y; + + 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'; + ctx.beginPath(); + let lastPos = this.target || this.position; + this.path.forEach(point => { + const from = hexToPixel(lastPos); + const to = hexToPixel(point); + ctx.moveTo(from.x - camera.x, from.y - camera.y); + ctx.lineTo(to.x - camera.x, to.y - camera.y); + lastPos = point; + }); + ctx.stroke(); + } + } +}; \ 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..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 |