feat: render actual dungeon tile textures in radar

Extract 614 UB tile BMPs into dungeon_tiles.json (287KB base64 bundle).
Frontend loads tiles once, then draws them rotated per-cell using
environment IDs. Falls back to colored rectangles if tiles not loaded.
Current floor at 70% opacity, other floors at 15%.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-04-08 15:14:52 +02:00
parent b941a29f04
commit e2982e34b5
2 changed files with 47 additions and 7 deletions

File diff suppressed because one or more lines are too long

View file

@ -3586,6 +3586,24 @@ function openPlayerDashboard() {
const radarWindows = {}; // character_name -> window element
const dungeonMapCache = {}; // landblock hex string -> dungeon map data
let dungeonTileImages = null; // env_id -> Image, loaded once
// Load dungeon tile textures (614 tiles, ~287KB JSON)
function loadDungeonTiles() {
if (dungeonTileImages) return; // already loading/loaded
dungeonTileImages = {};
fetch('dungeon_tiles.json')
.then(r => r.json())
.then(data => {
Object.entries(data).forEach(([envId, dataUrl]) => {
const img = new Image();
img.src = dataUrl;
dungeonTileImages[envId] = img;
});
console.log(`Loaded ${Object.keys(dungeonTileImages).length} dungeon tile textures`);
})
.catch(err => console.warn('Failed to load dungeon tiles:', err));
}
const RADAR_COLORS = {
Monster: '#ff4444',
@ -3705,6 +3723,9 @@ function showRadarWindow(name) {
win._radarSelectedId = closest ? closest.id : null;
});
// Load dungeon tiles on first radar open
loadDungeonTiles();
// Send start_radar command
if (socket && socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify({ player_name: name, command: 'start_radar' }));
@ -3764,25 +3785,43 @@ function updateRadarWindow(msg) {
ctx.clip();
if (isDungeon && landblock && dungeonMapCache[landblock]) {
// ─── Dungeon cell background ───
// ─── Dungeon cell background with tile textures ───
const dmap = dungeonMapCache[landblock];
const playerRoundedZ = Math.floor((playerRawZ + 3) / 6) * 6;
ctx.translate(cx, cy);
ctx.rotate(-playerHeading * Math.PI / 180); // heading-up rotation
const cellSize = 10 * scale; // each cell is 10 game units
const hasTiles = dungeonTileImages && Object.keys(dungeonTileImages).length > 0;
// Normalize rotation value to radians (same as UB's DungeonCell.cs)
function cellRotation(rot) {
if (rot === 1) return Math.PI;
if (rot < -0.70 && rot > -0.8) return Math.PI / 2;
if (rot > 0.70 && rot < 0.8) return -Math.PI / 2;
return 0;
}
(dmap.z_levels || []).forEach(level => {
const isCurrentFloor = (level.z === playerRoundedZ);
ctx.globalAlpha = isCurrentFloor ? 0.5 : 0.15;
ctx.fillStyle = isCurrentFloor ? '#3a5a3a' : '#1a2a1a';
ctx.strokeStyle = isCurrentFloor ? '#4a6a4a' : '#2a3a2a';
ctx.lineWidth = 0.5;
ctx.globalAlpha = isCurrentFloor ? 0.7 : 0.15;
(level.cells || []).forEach(cell => {
const dx = (cell.x - playerX) * scale;
const dy = -(cell.y - playerY) * scale; // Y flipped
ctx.fillRect(dx - cellSize / 2, dy - cellSize / 2, cellSize, cellSize);
ctx.strokeRect(dx - cellSize / 2, dy - cellSize / 2, cellSize, cellSize);
const tileImg = hasTiles ? dungeonTileImages[String(cell.env_id)] : null;
if (tileImg && tileImg.complete && tileImg.naturalWidth) {
// Draw textured tile with rotation
ctx.save();
ctx.translate(dx, dy);
ctx.rotate(cellRotation(cell.rotation));
ctx.drawImage(tileImg, -cellSize / 2, -cellSize / 2, cellSize, cellSize);
ctx.restore();
} else {
// Fallback: colored rectangle
ctx.fillStyle = isCurrentFloor ? '#3a5a3a' : '#1a2a1a';
ctx.fillRect(dx - cellSize / 2, dy - cellSize / 2, cellSize, cellSize);
}
});
});
ctx.globalAlpha = 1.0;