feat: UB-style tile processing — white transparent, color remapping

Process each tile through canvas pixel manipulation:
- White pixels → transparent (same as UB's MakeTransparent)
- Remap UB's 5 source colors (walls, inner walls, ramps, floors, stairs)
  to readable display colors on dark background
- Black outlines made semi-transparent
- Current floor at 85% opacity, other floors at 12%
- Non-current floors drawn first, current floor on top

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-04-08 15:21:58 +02:00
parent 06f326cce0
commit 683d1cf337

View file

@ -3586,21 +3586,85 @@ function openPlayerDashboard() {
const radarWindows = {}; // character_name -> window element
const dungeonMapCache = {}; // landblock hex string -> dungeon map data
let dungeonTileImages = null; // env_id -> Image, loaded once
let dungeonTileCanvases = null; // env_id -> processed offscreen canvas
// UB default source colors (ARGB as signed int32 → R,G,B)
// These are the colors embedded in the tile images that get remapped
const UB_TILE_COLORS = {
walls: { r: 0, g: 0, b: 255 }, // -16777089 → #0000FF
innerWalls: { r: 127, g: 127, b: 255 }, // -8404993 → #7F7FFF
rampedWalls: { r: 77, g: 255, b: 255 }, // -11622657 → #4DFFFF (approx)
floors: { r: 0, g: 127, b: 255 }, // -16744513 → #007FFF
stairs: { r: 0, g: 63, b: 255 }, // -16760961 → #003FFF
};
// Target display colors (UB-like appearance on dark background)
const DUNGEON_DISPLAY_COLORS = {
walls: { r: 140, g: 140, b: 180 }, // light gray-blue walls
innerWalls: { r: 100, g: 100, b: 140 }, // darker inner walls
rampedWalls: { r: 120, g: 160, b: 120 }, // greenish ramps
floors: { r: 60, g: 80, b: 60 }, // dark green floors
stairs: { r: 180, g: 160, b: 80 }, // yellowish stairs
};
// Process a tile image: make white transparent, remap UB colors
function processTileImage(img) {
const c = document.createElement('canvas');
c.width = 10;
c.height = 10;
const ctx = c.getContext('2d');
ctx.drawImage(img, 0, 0, 10, 10);
const imageData = ctx.getImageData(0, 0, 10, 10);
const d = imageData.data;
for (let i = 0; i < d.length; i += 4) {
const r = d[i], g = d[i+1], b = d[i+2];
// Make white (and near-white) transparent
if (r > 240 && g > 240 && b > 240) {
d[i+3] = 0; // alpha = 0
continue;
}
// Color remap: match source colors and replace with display colors
let matched = false;
for (const [key, src] of Object.entries(UB_TILE_COLORS)) {
if (Math.abs(r - src.r) < 15 && Math.abs(g - src.g) < 15 && Math.abs(b - src.b) < 15) {
const dst = DUNGEON_DISPLAY_COLORS[key];
d[i] = dst.r; d[i+1] = dst.g; d[i+2] = dst.b;
matched = true;
break;
}
}
// Make black semi-transparent (outlines)
if (!matched && r < 15 && g < 15 && b < 15) {
d[i+3] = 100;
}
}
ctx.putImageData(imageData, 0, 0);
return c;
}
// Load dungeon tile textures (614 tiles, ~287KB JSON)
function loadDungeonTiles() {
if (dungeonTileImages) return; // already loading/loaded
dungeonTileImages = {};
if (dungeonTileCanvases) return; // already loading/loaded
dungeonTileCanvases = {};
fetch('dungeon_tiles.json')
.then(r => r.json())
.then(data => {
let loaded = 0;
const total = Object.keys(data).length;
Object.entries(data).forEach(([envId, dataUrl]) => {
const img = new Image();
img.onload = () => {
dungeonTileCanvases[envId] = processTileImage(img);
loaded++;
if (loaded === total) console.log(`Processed ${total} dungeon tiles`);
};
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));
}
@ -3791,7 +3855,7 @@ function updateRadarWindow(msg) {
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;
const hasTiles = dungeonTileCanvases && Object.keys(dungeonTileCanvases).length > 0;
// Normalize rotation value to radians (same as UB's DungeonCell.cs)
function cellRotation(rot) {
@ -3801,21 +3865,28 @@ function updateRadarWindow(msg) {
return 0;
}
(dmap.z_levels || []).forEach(level => {
// Draw non-current floors first (dimmed), then current floor on top
const sortedLevels = (dmap.z_levels || []).slice().sort((a, b) => {
const aCur = a.z === playerRoundedZ ? 1 : 0;
const bCur = b.z === playerRoundedZ ? 1 : 0;
return aCur - bCur; // current floor drawn last (on top)
});
sortedLevels.forEach(level => {
const isCurrentFloor = (level.z === playerRoundedZ);
ctx.globalAlpha = isCurrentFloor ? 0.7 : 0.15;
ctx.globalAlpha = isCurrentFloor ? 0.85 : 0.12;
(level.cells || []).forEach(cell => {
const dx = (cell.x - playerX) * scale;
const dy = -(cell.y - playerY) * scale; // Y flipped
const tileImg = hasTiles ? dungeonTileImages[String(cell.env_id)] : null;
const tileCanvas = hasTiles ? dungeonTileCanvases[String(cell.env_id)] : null;
if (tileImg && tileImg.complete && tileImg.naturalWidth) {
// Draw textured tile with rotation
if (tileCanvas) {
// Draw processed tile with rotation
ctx.save();
ctx.translate(dx, dy);
ctx.rotate(cellRotation(cell.rotation));
ctx.drawImage(tileImg, -cellSize / 2, -cellSize / 2, cellSize, cellSize);
ctx.drawImage(tileCanvas, -cellSize / 2, -cellSize / 2, cellSize, cellSize);
ctx.restore();
} else {
// Fallback: colored rectangle