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:
parent
06f326cce0
commit
683d1cf337
1 changed files with 83 additions and 12 deletions
|
|
@ -3586,21 +3586,85 @@ 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
|
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)
|
// Load dungeon tile textures (614 tiles, ~287KB JSON)
|
||||||
function loadDungeonTiles() {
|
function loadDungeonTiles() {
|
||||||
if (dungeonTileImages) return; // already loading/loaded
|
if (dungeonTileCanvases) return; // already loading/loaded
|
||||||
dungeonTileImages = {};
|
dungeonTileCanvases = {};
|
||||||
fetch('dungeon_tiles.json')
|
fetch('dungeon_tiles.json')
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
|
let loaded = 0;
|
||||||
|
const total = Object.keys(data).length;
|
||||||
Object.entries(data).forEach(([envId, dataUrl]) => {
|
Object.entries(data).forEach(([envId, dataUrl]) => {
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
|
img.onload = () => {
|
||||||
|
dungeonTileCanvases[envId] = processTileImage(img);
|
||||||
|
loaded++;
|
||||||
|
if (loaded === total) console.log(`Processed ${total} dungeon tiles`);
|
||||||
|
};
|
||||||
img.src = dataUrl;
|
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));
|
.catch(err => console.warn('Failed to load dungeon tiles:', err));
|
||||||
}
|
}
|
||||||
|
|
@ -3791,7 +3855,7 @@ function updateRadarWindow(msg) {
|
||||||
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;
|
const hasTiles = dungeonTileCanvases && Object.keys(dungeonTileCanvases).length > 0;
|
||||||
|
|
||||||
// Normalize rotation value to radians (same as UB's DungeonCell.cs)
|
// Normalize rotation value to radians (same as UB's DungeonCell.cs)
|
||||||
function cellRotation(rot) {
|
function cellRotation(rot) {
|
||||||
|
|
@ -3801,21 +3865,28 @@ function updateRadarWindow(msg) {
|
||||||
return 0;
|
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);
|
const isCurrentFloor = (level.z === playerRoundedZ);
|
||||||
ctx.globalAlpha = isCurrentFloor ? 0.7 : 0.15;
|
ctx.globalAlpha = isCurrentFloor ? 0.85 : 0.12;
|
||||||
|
|
||||||
(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
|
||||||
const tileImg = hasTiles ? dungeonTileImages[String(cell.env_id)] : null;
|
const tileCanvas = hasTiles ? dungeonTileCanvases[String(cell.env_id)] : null;
|
||||||
|
|
||||||
if (tileImg && tileImg.complete && tileImg.naturalWidth) {
|
if (tileCanvas) {
|
||||||
// Draw textured tile with rotation
|
// Draw processed tile with rotation
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.translate(dx, dy);
|
ctx.translate(dx, dy);
|
||||||
ctx.rotate(cellRotation(cell.rotation));
|
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();
|
ctx.restore();
|
||||||
} else {
|
} else {
|
||||||
// Fallback: colored rectangle
|
// Fallback: colored rectangle
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue