From bf348793903e1d120ea396c507bee4afed8fe731 Mon Sep 17 00:00:00 2001 From: Jose Luis Date: Fri, 20 Mar 2026 16:23:09 +0100 Subject: [PATCH] feat: add F3 debug overlay for collision visualization Press F3 to toggle a debug overlay that shows: - Red tiles: walls (collision) - Green tiles: exits (map transitions) - Yellow tiles: interactions (workshop, signs, doors) - Purple tiles: NPCs - Green border: current player tile - Coordinate labels on nearby tiles - Legend bar with player position and current map Co-Authored-By: Claude Opus 4.6 --- js/world/worldInput.js | 8 +++ js/world/worldRenderer.js | 114 +++++++++++++++++++++++++++++++++++++- 2 files changed, 120 insertions(+), 2 deletions(-) diff --git a/js/world/worldInput.js b/js/world/worldInput.js index 8481787..959fd23 100644 --- a/js/world/worldInput.js +++ b/js/world/worldInput.js @@ -1,6 +1,7 @@ // worldInput.js - Keyboard input for world mode import { worldState, advanceDialog, startDialog } from './worldState.js'; import { getMap, getInteraction, getNPC, getExit, isWalkable } from './maps.js'; +import { toggleDebug } from './worldRenderer.js'; const keysDown = new Set(); let interactionHandler = null; @@ -35,6 +36,13 @@ function onKeyDown(e) { return; } + // Debug overlay toggle (F3) + if (key === 'F3') { + e.preventDefault(); + toggleDebug(); + return; + } + // Workshop shortcut (TAB) if (key === 'Tab') { e.preventDefault(); diff --git a/js/world/worldRenderer.js b/js/world/worldRenderer.js index f356d91..7afe27e 100644 --- a/js/world/worldRenderer.js +++ b/js/world/worldRenderer.js @@ -4,13 +4,20 @@ import { drawDialogBox, preloadAssets, TILE_PX, SCALE } from './sprites.js'; import { worldState } from './worldState.js'; -import { getMap, getInteraction, getNPC } from './maps.js'; +import { getMap, getInteraction, getNPC, getExit, isWall } from './maps.js'; import { updateMovement } from './worldInput.js'; let canvas = null; let ctx = null; let animFrameId = null; let lastTime = 0; +let debugMode = false; + +export function toggleDebug() { + debugMode = !debugMode; + console.log(`[debug] collision overlay ${debugMode ? 'ON' : 'OFF'}`); + return debugMode; +} export function initWorldRenderer() { canvas = document.getElementById('canvas'); @@ -87,6 +94,9 @@ export function renderWorld(timestamp) { // === Layer 1: Map background (PNG) === drawMapImage(ctx, map.image, cam.x, cam.y); + // === Debug overlay (between map and entities) === + if (debugMode) drawDebugOverlay(ctx, map, cam); + // === Layer 2: NPCs === if (map.npcs) { for (const npc of map.npcs) { @@ -154,7 +164,107 @@ function drawHUD(map) { ctx.fillStyle = '#555'; ctx.textAlign = 'center'; ctx.font = '11px "Segoe UI", system-ui, sans-serif'; - ctx.fillText('WASD: Move | E: Interact | TAB: Workshop', canvas.width / 2, 16); + ctx.fillText('WASD: Move | E: Interact | TAB: Workshop | F3: Debug', canvas.width / 2, 16); + + // Debug legend + if (debugMode) { + const legendY = 40; + ctx.font = '11px monospace'; + ctx.textAlign = 'left'; + ctx.textBaseline = 'top'; + + const items = [ + ['rgba(255, 50, 50, 0.6)', 'Wall'], + ['rgba(50, 255, 50, 0.6)', 'Exit'], + ['rgba(255, 255, 0, 0.6)', 'Interaction'], + ['rgba(200, 50, 255, 0.6)', 'NPC'], + ['#00e599', 'Player tile'] + ]; + let lx = 12; + for (const [color, label] of items) { + ctx.fillStyle = color; + ctx.fillRect(lx, legendY, 12, 12); + ctx.strokeStyle = '#fff'; + ctx.lineWidth = 0.5; + ctx.strokeRect(lx, legendY, 12, 12); + ctx.fillStyle = '#ccc'; + ctx.fillText(label, lx + 16, legendY + 1); + lx += ctx.measureText(label).width + 28; + } + + // Player coords + const p = worldState.player; + ctx.fillStyle = '#00e599'; + ctx.fillText(`Pos: (${p.x}, ${p.y}) Map: ${worldState.currentMap}`, 12, legendY + 18); + } +} + +// ==================== Debug overlay ==================== + +function drawDebugOverlay(ctx, map, cam) { + const mapId = worldState.currentMap; + const w = map.widthTiles; + const h = map.heightTiles; + + ctx.save(); + + for (let ty = 0; ty < h; ty++) { + for (let tx = 0; tx < w; tx++) { + const sx = tx * TILE_PX + cam.x; + const sy = ty * TILE_PX + cam.y; + + // Skip tiles entirely off-screen + if (sx + TILE_PX < 0 || sx > canvas.width || sy + TILE_PX < 0 || sy > canvas.height) continue; + + const wall = isWall(mapId, tx, ty); + const exit = getExit(mapId, tx, ty); + const inter = getInteraction(mapId, tx, ty); + const npc = getNPC(mapId, tx, ty); + + // Wall = red, Exit = green, Interaction = yellow, NPC = purple, walkable = no fill + if (wall) { + ctx.fillStyle = 'rgba(255, 50, 50, 0.35)'; + ctx.fillRect(sx, sy, TILE_PX, TILE_PX); + } else if (exit) { + ctx.fillStyle = 'rgba(50, 255, 50, 0.4)'; + ctx.fillRect(sx, sy, TILE_PX, TILE_PX); + } + + if (inter) { + ctx.fillStyle = 'rgba(255, 255, 0, 0.35)'; + ctx.fillRect(sx, sy, TILE_PX, TILE_PX); + } + + if (npc) { + ctx.fillStyle = 'rgba(200, 50, 255, 0.4)'; + ctx.fillRect(sx, sy, TILE_PX, TILE_PX); + } + + // Grid lines + ctx.strokeStyle = 'rgba(255, 255, 255, 0.12)'; + ctx.lineWidth = 0.5; + ctx.strokeRect(sx, sy, TILE_PX, TILE_PX); + + // Coordinate labels (only near player to avoid clutter) + const p = worldState.player; + if (Math.abs(tx - p.x) <= 6 && Math.abs(ty - p.y) <= 5) { + ctx.fillStyle = 'rgba(255, 255, 255, 0.5)'; + ctx.font = '9px monospace'; + ctx.textAlign = 'left'; + ctx.textBaseline = 'top'; + ctx.fillText(`${tx},${ty}`, sx + 2, sy + 1); + } + } + } + + // Player tile highlight + const px = worldState.player.x * TILE_PX + cam.x; + const py = worldState.player.y * TILE_PX + cam.y; + ctx.strokeStyle = '#00e599'; + ctx.lineWidth = 2; + ctx.strokeRect(px, py, TILE_PX, TILE_PX); + + ctx.restore(); } // ==================== Loop control ====================