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:
@@ -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();
|
||||
|
||||
@@ -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 ====================
|
||||
|
||||
Reference in New Issue
Block a user