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 <noreply@anthropic.com>
This commit is contained in:
Jose Luis
2026-03-20 16:23:09 +01:00
parent 9b2a25856e
commit bf34879390
2 changed files with 120 additions and 2 deletions

View File

@@ -1,6 +1,7 @@
// worldInput.js - Keyboard input for world mode // worldInput.js - Keyboard input for world mode
import { worldState, advanceDialog, startDialog } from './worldState.js'; import { worldState, advanceDialog, startDialog } from './worldState.js';
import { getMap, getInteraction, getNPC, getExit, isWalkable } from './maps.js'; import { getMap, getInteraction, getNPC, getExit, isWalkable } from './maps.js';
import { toggleDebug } from './worldRenderer.js';
const keysDown = new Set(); const keysDown = new Set();
let interactionHandler = null; let interactionHandler = null;
@@ -35,6 +36,13 @@ function onKeyDown(e) {
return; return;
} }
// Debug overlay toggle (F3)
if (key === 'F3') {
e.preventDefault();
toggleDebug();
return;
}
// Workshop shortcut (TAB) // Workshop shortcut (TAB)
if (key === 'Tab') { if (key === 'Tab') {
e.preventDefault(); e.preventDefault();

View File

@@ -4,13 +4,20 @@ import {
drawDialogBox, preloadAssets, TILE_PX, SCALE drawDialogBox, preloadAssets, TILE_PX, SCALE
} from './sprites.js'; } from './sprites.js';
import { worldState } from './worldState.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'; import { updateMovement } from './worldInput.js';
let canvas = null; let canvas = null;
let ctx = null; let ctx = null;
let animFrameId = null; let animFrameId = null;
let lastTime = 0; let lastTime = 0;
let debugMode = false;
export function toggleDebug() {
debugMode = !debugMode;
console.log(`[debug] collision overlay ${debugMode ? 'ON' : 'OFF'}`);
return debugMode;
}
export function initWorldRenderer() { export function initWorldRenderer() {
canvas = document.getElementById('canvas'); canvas = document.getElementById('canvas');
@@ -87,6 +94,9 @@ export function renderWorld(timestamp) {
// === Layer 1: Map background (PNG) === // === Layer 1: Map background (PNG) ===
drawMapImage(ctx, map.image, cam.x, cam.y); drawMapImage(ctx, map.image, cam.x, cam.y);
// === Debug overlay (between map and entities) ===
if (debugMode) drawDebugOverlay(ctx, map, cam);
// === Layer 2: NPCs === // === Layer 2: NPCs ===
if (map.npcs) { if (map.npcs) {
for (const npc of map.npcs) { for (const npc of map.npcs) {
@@ -154,7 +164,107 @@ function drawHUD(map) {
ctx.fillStyle = '#555'; ctx.fillStyle = '#555';
ctx.textAlign = 'center'; ctx.textAlign = 'center';
ctx.font = '11px "Segoe UI", system-ui, sans-serif'; 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 ==================== // ==================== Loop control ====================