// Debug alert for mobile debugging
if (typeof debugAlert === 'function') {
debugAlert('tilemap.js starting to load');
}
/* ============================================================
Tile groups bridge (from tilepicker.js)
============================================================ */
let groupsCache = []; // local cache from Tilepicker
function readGroups() {
if (window.Tilepicker && typeof window.Tilepicker.getGroups === 'function') {
const g = window.Tilepicker.getGroups();
if (Array.isArray(g)) groupsCache = g;
} else if (typeof groups !== 'undefined' && Array.isArray(groups)) {
groupsCache = groups;
} else {
groupsCache = [];
}
if (activePaletteGroup >= groupsCache.length) {
activePaletteGroup = Math.max(0, groupsCache.length - 1);
}
}
function getTileDims(tile) {
const w = tile.width != null ? tile.width : tile.size;
const h = tile.height != null ? tile.height : tile.size;
return { w, h };
}
window.addEventListener('tiles:updated', () => {
readGroups();
try { createTilePalette(); } catch (_) {}
});
/* ============================================================
Multiple tilemap support (+ stretched anchors per map)
============================================================ */
let tilemaps = [
{ id: 0, name: "Map 1", width: 20, height: 15, tileSize: 32, data: [], stretchedAnchors: new Map() }
];
let currentTilemapIndex = 0;
let nextTilemapId = 1;
// Current tilemap state
let selectedMapTile = null;
let activePaletteGroup = 0;
// ===== Brush state =====
let brushW = 1;
let brushH = 1;
let isMouseDown = false;
let stretchSingle = true; // << NEW: when true and brush > 1 cell, stretch one tile instead of duplicating
/* ===== Helpers ===== */
function getCurrentTilemap() {
const tm = tilemaps[currentTilemapIndex];
if (!tm.stretchedAnchors) tm.stretchedAnchors = new Map();
return tm;
}
/**
* Initialize the map data array for current tilemap
*/
function initializeMapData() {
const tilemap = getCurrentTilemap();
tilemap.data = new Array(tilemap.width * tilemap.height).fill(0);
tilemap.stretchedAnchors.clear();
}
/**
* Create a new tilemap
*/
function createNewTilemap() {
const name = prompt("Enter tilemap name:", `Map ${tilemaps.length + 1}`);
if (!name) return;
const width = parseInt(prompt("Enter width (5-100):", "20"));
const height = parseInt(prompt("Enter height (5-100):", "15"));
if (isNaN(width) || isNaN(height) || width < 5 || width > 100 || height < 5 || height > 100) {
alert("Invalid dimensions. Must be between 5 and 100.");
return;
}
const newTilemap = {
id: nextTilemapId++,
name,
width,
height,
tileSize: 32,
data: new Array(width * height).fill(0),
stretchedAnchors: new Map()
};
tilemaps.push(newTilemap);
currentTilemapIndex = tilemaps.length - 1;
updateTilemapTabs();
updateControls();
createMapGrid();
createTilePalette();
}
/**
* Switch to a different tilemap
*/
function switchTilemap(index) {
if (index >= 0 && index < tilemaps.length) {
currentTilemapIndex = index;
updateTilemapTabs();
updateControls();
createMapGrid();
createTilePalette();
}
}
/**
* Remove a tilemap
*/
function removeTilemap(index) {
if (tilemaps.length <= 1) {
alert("Cannot delete the last tilemap");
return;
}
if (confirm(`Delete "${tilemaps[index].name}"?`)) {
tilemaps.splice(index, 1);
if (currentTilemapIndex >= tilemaps.length) {
currentTilemapIndex = tilemaps.length - 1;
} else if (currentTilemapIndex > index) {
currentTilemapIndex--;
}
updateTilemapTabs();
updateControls();
createMapGrid();
createTilePalette();
}
}
/**
* Update tilemap tabs
*/
function updateTilemapTabs() {
const container = document.getElementById('tilemapTabs');
if (!container) return;
container.innerHTML = '';
tilemaps.forEach((tilemap, index) => {
const tab = document.createElement('button');
tab.textContent = tilemap.name;
tab.style.cssText = `
background: ${index === currentTilemapIndex ? '#6cf' : '#555'};
color: ${index === currentTilemapIndex ? '#000' : '#fff'};
border: none; padding: 4px 8px; margin: 0 2px;
border-radius: 4px; cursor: pointer; font-size: 11px;
position: relative;
`;
tab.addEventListener('click', () => switchTilemap(index));
if (tilemaps.length > 1 && index !== currentTilemapIndex) {
const deleteBtn = document.createElement('span');
deleteBtn.textContent = '×';
deleteBtn.style.cssText = `
position: absolute; top: -2px; right: -2px;
background: #f44; color: white; border-radius: 50%;
width: 14px; height: 14px; font-size: 8px;
display: flex; align-items: center; justify-content: center;
cursor: pointer;
`;
deleteBtn.addEventListener('click', (e) => {
e.stopPropagation();
removeTilemap(index);
});
tab.appendChild(deleteBtn);
}
container.appendChild(tab);
});
const addBtn = document.createElement('button');
addBtn.textContent = '+';
addBtn.style.cssText = `
background: #4a4; color: white; border: none; padding: 4px 8px;
border-radius: 4px; cursor: pointer; font-size: 11px; margin-left: 5px;
`;
addBtn.addEventListener('click', createNewTilemap);
container.appendChild(addBtn);
}
/**
* Update controls to reflect current tilemap
*/
function updateControls() {
const tilemap = getCurrentTilemap();
const tileSizeSelect = document.getElementById('tileSizeSelect');
const gridWidth = document.getElementById('gridWidth');
const gridHeight = document.getElementById('gridHeight');
const brushWInput = document.getElementById('brushW');
const brushHInput = document.getElementById('brushH');
const stretchToggle = document.getElementById('stretchToggle');
if (tileSizeSelect) tileSizeSelect.value = tilemap.tileSize;
if (gridWidth) gridWidth.value = tilemap.width;
if (gridHeight) gridHeight.value = tilemap.height;
if (brushWInput) brushWInput.value = brushW;
if (brushHInput) brushHInput.value = brushH;
if (stretchToggle) stretchToggle.checked = stretchSingle;
}
/**
* Resize the grid with data preservation
*/
function resizeGrid(newWidth, newHeight) {
const tilemap = getCurrentTilemap();
if (newWidth === tilemap.width && newHeight === tilemap.height) return;
const hasData = tilemap.data.some(tile => tile !== 0);
if (hasData && !confirm(`Resize grid from ${tilemap.width}×${tilemap.height} to ${newWidth}×${newHeight}? This may crop or clear some tiles.`)) {
return;
}
const oldData = [...tilemap.data];
const oldWidth = tilemap.width;
const oldHeight = tilemap.height;
tilemap.width = newWidth;
tilemap.height = newHeight;
tilemap.data = new Array(newWidth * newHeight).fill(0);
tilemap.stretchedAnchors.clear();
for (let y = 0; y < Math.min(oldHeight, newHeight); y++) {
for (let x = 0; x < Math.min(oldWidth, newWidth); x++) {
const oldIndex = y * oldWidth + x;
const newIndex = y * newWidth + x;
tilemap.data[newIndex] = oldData[oldIndex];
}
}
createMapGrid();
updateControls();
}
/**
* Utility: overlap check for stretched rects
*/
function rectsOverlap(ax, ay, aw, ah, bx, by, bw, bh) {
return ax < bx + bw && ax + aw > bx && ay < by + bh && ay + ah > by;
}
/**
* Clean up any stretched anchors that overlap a given area
*/
function removeOverlappingStretched(areaX, areaY, areaW, areaH) {
const tilemap = getCurrentTilemap();
const toDelete = [];
for (const [key, meta] of tilemap.stretchedAnchors.entries()) {
const [sx, sy] = key.split(',').map(n => parseInt(n, 10));
if (rectsOverlap(areaX, areaY, areaW, areaH, sx, sy, meta.w, meta.h)) {
toDelete.push(key);
}
}
for (const key of toDelete) {
const [sx, sy] = key.split(',').map(n => parseInt(n, 10));
const meta = tilemap.stretchedAnchors.get(key);
// clear covered cells to 0
for (let dy = 0; dy < meta.h; dy++) {
for (let dx = 0; dx < meta.w; dx++) {
const x = sx + dx, y = sy + dy;
const idx = y * tilemap.width + x;
if (idx >= 0 && idx < tilemap.data.length) tilemap.data[idx] = 0;
}
}
tilemap.stretchedAnchors.delete(key);
}
}
/* ===== Brush placement (with stretch) ===== */
function placeBrushAt(topLeftX, topLeftY) {
const tileId = selectedMapTile ?? 0;
const tilemap = getCurrentTilemap();
// Erase path: if tileId == 0, just clear cells and overlapping stretched rects
if (tileId === 0) {
removeOverlappingStretched(topLeftX, topLeftY, brushW, brushH);
for (let dy = 0; dy < brushH; dy++) {
for (let dx = 0; dx < brushW; dx++) {
const x = topLeftX + dx, y = topLeftY + dy;
if (x >= 0 && x < tilemap.width && y >= 0 && y < tilemap.height) {
setMapTile(x, y, 0);
}
}
}
return;
}
// Stretch mode across area (>1 cell)
if (stretchSingle && (brushW * brushH > 1)) {
// Remove any existing stretched rect overlapping our target area
removeOverlappingStretched(topLeftX, topLeftY, brushW, brushH);
// Mark all covered cells as children (-tileId), anchor as +tileId
for (let dy = 0; dy < brushH; dy++) {
for (let dx = 0; dx < brushW; dx++) {
const x = topLeftX + dx, y = topLeftY + dy;
if (x >= 0 && x < tilemap.width && y >= 0 && y < tilemap.height) {
const isAnchor = (dx === 0 && dy === 0);
setMapTile(x, y, isAnchor ? tileId : -tileId);
}
}
}
// Record anchor meta
tilemap.stretchedAnchors.set(`${topLeftX},${topLeftY}`, { w: brushW, h: brushH, tileId });
return;
}
// Normal (non-stretched) stamping: duplicates per cell
for (let dy = 0; dy < brushH; dy++) {
for (let dx = 0; dx < brushW; dx++) {
const x = topLeftX + dx, y = topLeftY + dy;
if (x >= 0 && x < tilemap.width && y >= 0 && y < tilemap.height) {
// Overwriting a stretched child/anchor? Clean overlapping stretched first for safe overwrite.
removeOverlappingStretched(x, y, 1, 1);
setMapTile(x, y, tileId);
}
}
}
}
/**
* Set a tile in the current map
*/
function setMapTile(x, y, tileId) {
const tilemap = getCurrentTilemap();
if (x >= 0 && x < tilemap.width && y >= 0 && y < tilemap.height) {
const index = y * tilemap.width + x;
tilemap.data[index] = tileId;
const cell = document.querySelector(`[data-map-x="${x}"][data-map-y="${y}"]`);
if (cell) updateMapCell(cell, tileId);
}
}
/**
* Get a tile from the current map
*/
function getMapTile(x, y) {
const tilemap = getCurrentTilemap();
if (x >= 0 && x < tilemap.width && y >= 0 && y < tilemap.height) {
const index = y * tilemap.width + x;
return tilemap.data[index];
}
return 0;
}
/**
* Update a visual map cell with tile data
*/
function updateMapCell(cell, tileId) {
const tilemap = getCurrentTilemap();
const x = parseInt(cell.dataset.mapX);
const y = parseInt(cell.dataset.mapY);
// Clear previous content but preserve coordinate label
const coordLabel = cell.querySelector('.coord-label');
cell.innerHTML = '';
if (coordLabel) {
cell.appendChild(coordLabel);
} else {
const label = document.createElement('span');
label.className = 'coord-label';
label.textContent = `${x},${y}`;
label.style.cssText = `
position: absolute; top: 2px; left: 2px; font-size: 8px;
color: rgba(255,255,255,0.5); pointer-events: none; z-index: 1;
`;
cell.appendChild(label);
}
cell.style.backgroundColor = '';
// Empty
if (tileId === 0) {
cell.style.backgroundColor = 'transparent';
return;
}
// If negative, this is a covered child of a stretch; render nothing.
if (tileId < 0) return;
// Positive: may be normal or an anchor of a stretch
readGroups();
if (!groupsCache.length) {
console.warn('No tile groups available');
return;
}
// Find the tile by id
let foundTile = null;
for (const group of groupsCache) {
const t = group.tiles.find(tt => tt.uniqueId === tileId);
if (t) { foundTile = t; break; }
}
if (!foundTile) return;
const { w, h } = getTileDims(foundTile);
// Is this cell an anchor?
const anchorMeta = getCurrentTilemap().stretchedAnchors.get(`${x},${y}`);
if (anchorMeta) {
// Draw SINGLE scaled sprite across brush area
const totalW = anchorMeta.w * tilemap.tileSize;
const totalH = anchorMeta.h * tilemap.tileSize;
const canvas = document.createElement('canvas');
canvas.width = totalW;
canvas.height = totalH;
// Let it overflow the cell to cover neighbors (simple and effective)
canvas.style.cssText = `
position: absolute; top: 0; left: 0;
width: ${totalW}px; height: ${totalH}px; pointer-events: none;
`;
const ctx = canvas.getContext('2d');
const tempCanvas = document.createElement('canvas');
tempCanvas.width = w;
tempCanvas.height = h;
const tempCtx = tempCanvas.getContext('2d');
tempCtx.putImageData(foundTile.data, 0, 0);
ctx.drawImage(tempCanvas, 0, 0, w, h, 0, 0, totalW, totalH);
cell.appendChild(canvas);
// Anchor ID badge
const idBadge = document.createElement('span');
idBadge.className = 'id-badge';
idBadge.textContent = tileId;
idBadge.style.cssText = `
position: absolute; bottom: 2px; right: 2px; font-size: 8px;
color: #fff; background: rgba(0,0,0,0.7); padding: 1px 3px;
border-radius: 3px; pointer-events: none; z-index: 2;
`;
cell.appendChild(idBadge);
return;
}
// Normal single-cell draw
const canvas = document.createElement('canvas');
canvas.width = tilemap.tileSize;
canvas.height = tilemap.tileSize;
canvas.style.cssText = 'width: 100%; height: 100%; position: absolute; top: 0; left: 0;';
const ctx = canvas.getContext('2d');
const tempCanvas = document.createElement('canvas');
tempCanvas.width = w;
tempCanvas.height = h;
const tempCtx = tempCanvas.getContext('2d');
tempCtx.putImageData(foundTile.data, 0, 0);
ctx.drawImage(tempCanvas, 0, 0, w, h, 0, 0, tilemap.tileSize, tilemap.tileSize);
cell.appendChild(canvas);
const idBadge = document.createElement('span');
idBadge.className = 'id-badge';
idBadge.textContent = tileId;
idBadge.style.cssText = `
position: absolute; bottom: 2px; right: 2px; font-size: 8px;
color: #fff; background: rgba(0,0,0,0.7); padding: 1px 3px;
border-radius: 3px; pointer-events: none; z-index: 2;
`;
cell.appendChild(idBadge);
}
/**
* Create the map grid
*/
function createMapGrid() {
const container = document.getElementById('mapGrid');
if (!container) return;
const tilemap = getCurrentTilemap();
container.innerHTML = '';
container.style.cssText = `
display: grid;
grid-template-columns: repeat(${tilemap.width}, ${tilemap.tileSize}px);
grid-template-rows: repeat(${tilemap.height}, ${tilemap.tileSize}px);
gap: 1px; background: #333; padding: 10px; overflow: auto; max-height: 400px;
user-select: none; position: relative;
`;
// Mouse-up cleanup even if cursor leaves
const onUp = () => { isMouseDown = false; };
document.addEventListener('mouseup', onUp, { passive: true });
for (let y = 0; y < tilemap.height; y++) {
for (let x = 0; x < tilemap.width; x++) {
const cell = document.createElement('div');
cell.className = 'map-cell';
cell.dataset.mapX = x;
cell.dataset.mapY = y;
cell.style.cssText = `
width: ${tilemap.tileSize}px; height: ${tilemap.tileSize}px;
border: 1px solid rgba(255,255,255,0.2); background: #222;
cursor: crosshair; position: relative; overflow: visible;
`;
const label = document.createElement('span');
label.className = 'coord-label';
label.textContent = `${x},${y}`;
label.style.cssText = `
position: absolute; top: 2px; left: 2px; font-size: 8px;
color: rgba(255,255,255,0.5); pointer-events: none; z-index: 1;
`;
cell.appendChild(label);
cell.addEventListener('mousedown', (e) => {
if (e.button !== 0) return;
isMouseDown = true;
placeBrushAt(x, y);
});
cell.addEventListener('mouseenter', () => {
if (isMouseDown) placeBrushAt(x, y);
cell.style.backgroundColor = '#444';
});
cell.addEventListener('mouseleave', () => {
if (getMapTile(x, y) === 0) cell.style.backgroundColor = '#222';
});
container.appendChild(cell);
}
}
// Render existing data (anchors & singles)
for (let y = 0; y < tilemap.height; y++) {
for (let x = 0; x < tilemap.width; x++) {
const tileId = getMapTile(x, y);
if (tileId !== 0) {
const cell = container.querySelector(`[data-map-x="${x}"][data-map-y="${y}"]`);
if (cell) updateMapCell(cell, tileId);
}
}
}
}
/**
* Create the tile palette from picked tiles
*/
function createTilePalette() {
const container = document.getElementById('tilePalette');
if (!container) return;
readGroups();
const tilemap = getCurrentTilemap();
container.innerHTML = '';
if (!groupsCache.length) {
container.innerHTML = '<div style="color: #888; padding: 10px;">No tile groups available. Use the Tile Picker first.</div>';
return;
}
const controlsBar = document.createElement('div');
controlsBar.style.cssText = 'margin-bottom: 6px; display: flex; align-items: center; gap: 8px; flex-wrap: wrap;';
// Group tabs
const tabContainer = document.createElement('div');
tabContainer.style.cssText = 'display: flex; gap: 2px;';
groupsCache.forEach((group, index) => {
const tab = document.createElement('button');
tab.textContent = `G${index + 1}`;
tab.style.cssText = `
background: ${index === activePaletteGroup ? '#6cf' : '#444'};
color: ${index === activePaletteGroup ? '#000' : '#fff'};
border: none; padding: 4px 8px; border-radius: 4px; cursor: pointer; font-size: 11px;
`;
tab.addEventListener('click', () => {
activePaletteGroup = index;
createTilePalette();
});
tabContainer.appendChild(tab);
});
// Clear map button
const clearBtn = document.createElement('button');
clearBtn.textContent = 'Clear All';
clearBtn.style.cssText = `
background: #d44; color: white; border: none; padding: 4px 8px;
border-radius: 4px; cursor: pointer; font-size: 11px;
`;
clearBtn.addEventListener('click', () => clearMap());
// Brush controls
const brushWrap = document.createElement('div');
brushWrap.style.cssText = 'display:flex; align-items:center; gap:6px; background:#2b2b2b; border:1px solid #444; padding:6px 8px; border-radius:6px;';
const labelBW = document.createElement('span'); labelBW.textContent = 'Brush'; labelBW.style.cssText = 'color:#ccc;font-size:11px;';
const bw = document.createElement('input');
bw.type = 'number'; bw.min = '1'; bw.value = String(brushW);
bw.id = 'brushW';
bw.title = 'Brush width (columns)';
bw.style.cssText = 'width:48px;background:#333;color:#fff;border:1px solid #666;border-radius:4px;padding:3px 4px;font-size:11px;';
bw.addEventListener('change', () => { brushW = Math.max(1, parseInt(bw.value || '1', 10)); updateControls(); });
const xLabel = document.createElement('span'); xLabel.textContent = '×'; xLabel.style.cssText = 'color:#aaa;font-size:11px;';
const bh = document.createElement('input');
bh.type = 'number'; bh.min = '1'; bh.value = String(brushH);
bh.id = 'brushH';
bh.title = 'Brush height (rows)';
bh.style.cssText = 'width:48px;background:#333;color:#fff;border:1px solid #666;border-radius:4px;padding:3px 4px;font-size:11px;';
bh.addEventListener('change', () => { brushH = Math.max(1, parseInt(bh.value || '1', 10)); updateControls(); });
const preset11 = document.createElement('button');
preset11.textContent = '1×1';
preset11.style.cssText = 'background:#555;color:#fff;border:1px solid #777;border-radius:4px;padding:4px 6px;font-size:11px;cursor:pointer;';
preset11.addEventListener('click', () => { brushW = 1; brushH = 1; updateControls(); });
const preset12 = document.createElement('button');
preset12.textContent = '1×2 (iso)';
preset12.style.cssText = 'background:#555;color:#fff;border:1px solid #777;border-radius:4px;padding:4px 6px;font-size:11px;cursor:pointer;';
preset12.title = 'Quick isometric-style vertical stretch';
preset12.addEventListener('click', () => { brushW = 1; brushH = 2; stretchSingle = true; updateControls(); });
const preset21 = document.createElement('button');
preset21.textContent = '2×1';
preset21.style.cssText = 'background:#555;color:#fff;border:1px solid #777;border-radius:4px;padding:4px 6px;font-size:11px;cursor:pointer;';
preset21.addEventListener('click', () => { brushW = 2; brushH = 1; updateControls(); });
const preset22 = document.createElement('button');
preset22.textContent = '2×2';
preset22.style.cssText = 'background:#555;color:#fff;border:1px solid #777;border-radius:4px;padding:4px 6px;font-size:11px;cursor:pointer;';
preset22.addEventListener('click', () => { brushW = 2; brushH = 2; updateControls(); });
// Stretch toggle
const stretchLbl = document.createElement('label');
stretchLbl.style.cssText = 'display:flex;align-items:center;gap:6px;color:#ccc;font-size:11px;';
const stretchChk = document.createElement('input');
stretchChk.id = 'stretchToggle';
stretchChk.type = 'checkbox';
stretchChk.checked = stretchSingle;
stretchChk.addEventListener('change', () => { stretchSingle = !!stretchChk.checked; });
stretchLbl.appendChild(stretchChk);
stretchLbl.appendChild(document.createTextNode('Stretch brush'));
brushWrap.appendChild(labelBW);
brushWrap.appendChild(bw);
brushWrap.appendChild(xLabel);
brushWrap.appendChild(bh);
brushWrap.appendChild(preset11);
brushWrap.appendChild(preset12);
brushWrap.appendChild(preset21);
brushWrap.appendChild(preset22);
brushWrap.appendChild(stretchLbl);
controlsBar.appendChild(tabContainer);
controlsBar.appendChild(clearBtn);
controlsBar.appendChild(brushWrap);
container.appendChild(controlsBar);
// Tiles container
const tilesContainer = document.createElement('div');
tilesContainer.style.cssText = 'display: flex; align-items: center; gap: 3px; flex-wrap: wrap;';
// Eraser
const eraserBtn = document.createElement('div');
eraserBtn.className = 'palette-tile';
eraserBtn.style.cssText = `
width: ${tilemap.tileSize}px; height: ${tilemap.tileSize}px; border: 2px solid #666;
cursor: pointer; background: #333; color: #fff; display: flex;
align-items: center; justify-content: center; font-size: 12px; position: relative;
`;
eraserBtn.textContent = 'X';
eraserBtn.title = 'Eraser';
eraserBtn.addEventListener('click', () => {
selectedMapTile = 0;
document.querySelectorAll('.palette-tile').forEach(t => t.classList.remove('selected'));
eraserBtn.classList.add('selected');
});
tilesContainer.appendChild(eraserBtn);
// Tiles from active group
if (activePaletteGroup < groupsCache.length) {
const activeGroup = groupsCache[activePaletteGroup];
activeGroup.tiles.forEach(tile => {
const { w, h } = getTileDims(tile);
const tileDiv = document.createElement('div');
tileDiv.className = 'palette-tile';
tileDiv.style.cssText = `
width: ${tilemap.tileSize}px; height: ${tilemap.tileSize}px; border: 2px solid #666;
cursor: pointer; overflow: hidden; position: relative; background: #222;
`;
const canvas = document.createElement('canvas');
canvas.width = tilemap.tileSize;
canvas.height = tilemap.tileSize;
canvas.style.cssText = 'width: 100%; height: 100%; position: absolute; top: 0; left: 0;';
const ctx = canvas.getContext('2d');
const tempCanvas = document.createElement('canvas');
tempCanvas.width = w;
tempCanvas.height = h;
const tempCtx = tempCanvas.getContext('2d');
tempCtx.putImageData(tile.data, 0, 0);
ctx.drawImage(tempCanvas, 0, 0, w, h, 0, 0, tilemap.tileSize, tilemap.tileSize);
tileDiv.appendChild(canvas);
const idBadge = document.createElement('span');
idBadge.textContent = tile.uniqueId;
idBadge.style.cssText = `
position: absolute; bottom: 1px; right: 1px; font-size: 7px;
color: #fff; background: rgba(0,0,0,0.8); padding: 1px 2px;
border-radius: 2px; pointer-events: none; z-index: 2; line-height: 1;
`;
tileDiv.appendChild(idBadge);
tileDiv.addEventListener('click', () => {
selectedMapTile = tile.uniqueId;
document.querySelectorAll('.palette-tile').forEach(t => t.classList.remove('selected'));
tileDiv.classList.add('selected');
});
tilesContainer.appendChild(tileDiv);
});
}
container.appendChild(tilesContainer);
}
/**
* Open the tilemap overlay - main entry point
*/
function openTilemapOverlay() {
const overlayContent = document.getElementById('overlayContent');
overlayContent.innerHTML = `
<div style="margin-bottom: 10px; padding: 0 10px;">
<div id="tilemapTabs" style="margin-bottom: 10px;"></div>
<div style="display: flex; justify-content: flex-start; align-items: center; gap: 10px; flex-wrap: wrap;">
<select id="tileSizeSelect" style="background: #333; color: #fff; border: 1px solid #666; padding: 4px 8px; border-radius: 4px;">
<option value="8">8px</option>
<option value="16">16px</option>
<option value="32" selected>32px</option>
<option value="64">64px</option>
<option value="128">128px</option>
</select>
<input type="number" id="gridWidth" min="5" max="100" style="width: 50px; background: #333; color: #fff; border: 1px solid #666; padding: 4px; border-radius: 4px;">
<span style="color: #888;">×</span>
<input type="number" id="gridHeight" min="5" max="100" style="width: 50px; background: #333; color: #fff; border: 1px solid #666; padding: 4px; border-radius: 4px;">
<button id="resizeGrid" style="background: #666; color: white; border: none; padding: 4px 8px; border-radius: 4px; cursor: pointer; font-size: 11px;">Resize</button>
</div>
</div>
<div style="margin-bottom: 10px; padding: 0 10px;">
<div id="tilePalette"></div>
</div>
<div style="height: calc(100% - 140px); overflow: auto; padding: 0 10px;">
<div id="mapGrid"></div>
</div>
`;
setupEventHandlers();
readGroups();
updateTilemapTabs();
updateControls();
const tilemap = getCurrentTilemap();
if (!tilemap.data || tilemap.data.length === 0) initializeMapData();
createMapGrid();
createTilePalette();
}
/**
* Set up event handlers for controls
*/
function setupEventHandlers() {
document.getElementById('tileSizeSelect').addEventListener('change', (e) => {
const tilemap = getCurrentTilemap();
tilemap.tileSize = parseInt(e.target.value);
createMapGrid();
createTilePalette();
});
document.getElementById('resizeGrid').addEventListener('click', () => {
const newWidth = parseInt(document.getElementById('gridWidth').value);
const newHeight = parseInt(document.getElementById('gridHeight').value);
if (newWidth >= 5 && newWidth <= 100 && newHeight >= 5 && newHeight <= 100) {
resizeGrid(newWidth, newHeight);
} else {
alert('Grid size must be between 5 and 100');
}
});
['gridWidth', 'gridHeight'].forEach(id => {
document.getElementById(id).addEventListener('keypress', (e) => {
if (e.key === 'Enter') document.getElementById('resizeGrid').click();
});
});
// Toggle brush 1×1 / 1×2 with "B"
window.addEventListener('keydown', (e) => {
if (e.key.toLowerCase() === 'b') {
if (brushW === 1 && brushH === 1) { brushW = 1; brushH = 2; stretchSingle = true; }
else { brushW = 1; brushH = 1; }
updateControls();
}
});
}
/**
* Clear the entire current map
*/
function clearMap() {
if (confirm('Clear the entire map?')) {
const tilemap = getCurrentTilemap();
tilemap.data.fill(0);
tilemap.stretchedAnchors.clear();
for (let y = 0; y < tilemap.height; y++) {
for (let x = 0; x < tilemap.width; x++) {
const cell = document.querySelector(`[data-map-x="${x}"][data-map-y="${y}"]`);
if (cell) updateMapCell(cell, 0);
}
}
}
}
// Add CSS for selected palette tiles
const style = document.createElement('style');
style.textContent = `
.palette-tile.selected { border-color: #6cf !important; box-shadow: 0 0 5px #6cf; }
.map-cell:hover { border-color: #6cf !important; }
`;
document.head.appendChild(style);
// Debug alert for mobile debugging - success
if (typeof debugAlert === 'function') {
debugAlert('tilemap.js loaded successfully');
}