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:
parent
b941a29f04
commit
e2982e34b5
2 changed files with 47 additions and 7 deletions
1
static/dungeon_tiles.json
Normal file
1
static/dungeon_tiles.json
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -3586,6 +3586,24 @@ function openPlayerDashboard() {
|
||||||
|
|
||||||
const radarWindows = {}; // character_name -> window element
|
const radarWindows = {}; // character_name -> window element
|
||||||
const dungeonMapCache = {}; // landblock hex string -> dungeon map data
|
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 = {
|
const RADAR_COLORS = {
|
||||||
Monster: '#ff4444',
|
Monster: '#ff4444',
|
||||||
|
|
@ -3705,6 +3723,9 @@ function showRadarWindow(name) {
|
||||||
win._radarSelectedId = closest ? closest.id : null;
|
win._radarSelectedId = closest ? closest.id : null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Load dungeon tiles on first radar open
|
||||||
|
loadDungeonTiles();
|
||||||
|
|
||||||
// Send start_radar command
|
// Send start_radar command
|
||||||
if (socket && socket.readyState === WebSocket.OPEN) {
|
if (socket && socket.readyState === WebSocket.OPEN) {
|
||||||
socket.send(JSON.stringify({ player_name: name, command: 'start_radar' }));
|
socket.send(JSON.stringify({ player_name: name, command: 'start_radar' }));
|
||||||
|
|
@ -3764,25 +3785,43 @@ function updateRadarWindow(msg) {
|
||||||
ctx.clip();
|
ctx.clip();
|
||||||
|
|
||||||
if (isDungeon && landblock && dungeonMapCache[landblock]) {
|
if (isDungeon && landblock && dungeonMapCache[landblock]) {
|
||||||
// ─── Dungeon cell background ───
|
// ─── Dungeon cell background with tile textures ───
|
||||||
const dmap = dungeonMapCache[landblock];
|
const dmap = dungeonMapCache[landblock];
|
||||||
const playerRoundedZ = Math.floor((playerRawZ + 3) / 6) * 6;
|
const playerRoundedZ = Math.floor((playerRawZ + 3) / 6) * 6;
|
||||||
ctx.translate(cx, cy);
|
ctx.translate(cx, cy);
|
||||||
ctx.rotate(-playerHeading * Math.PI / 180); // heading-up rotation
|
ctx.rotate(-playerHeading * Math.PI / 180); // heading-up rotation
|
||||||
const cellSize = 10 * scale; // each cell is 10 game units
|
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 => {
|
(dmap.z_levels || []).forEach(level => {
|
||||||
const isCurrentFloor = (level.z === playerRoundedZ);
|
const isCurrentFloor = (level.z === playerRoundedZ);
|
||||||
ctx.globalAlpha = isCurrentFloor ? 0.5 : 0.15;
|
ctx.globalAlpha = isCurrentFloor ? 0.7 : 0.15;
|
||||||
ctx.fillStyle = isCurrentFloor ? '#3a5a3a' : '#1a2a1a';
|
|
||||||
ctx.strokeStyle = isCurrentFloor ? '#4a6a4a' : '#2a3a2a';
|
|
||||||
ctx.lineWidth = 0.5;
|
|
||||||
|
|
||||||
(level.cells || []).forEach(cell => {
|
(level.cells || []).forEach(cell => {
|
||||||
const dx = (cell.x - playerX) * scale;
|
const dx = (cell.x - playerX) * scale;
|
||||||
const dy = -(cell.y - playerY) * scale; // Y flipped
|
const dy = -(cell.y - playerY) * scale; // Y flipped
|
||||||
ctx.fillRect(dx - cellSize / 2, dy - cellSize / 2, cellSize, cellSize);
|
const tileImg = hasTiles ? dungeonTileImages[String(cell.env_id)] : null;
|
||||||
ctx.strokeRect(dx - cellSize / 2, dy - cellSize / 2, cellSize, cellSize);
|
|
||||||
|
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;
|
ctx.globalAlpha = 1.0;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue