// Debug alert for mobile debugging
if (typeof debugAlert === 'function') {
debugAlert('tilemap.js starting to load');
}
// Multiple tilemap support
let tilemaps = [
{
id: 0,
name: "Map 1",
width: 20,
height: 15,
tileSize: 32,
data: []
}
];
let currentTilemapIndex = 0;
let nextTilemapId = 1;
// Current tilemap state (replaces legacy variables)
let selectedMapTile = null;
let activePaletteGroup = 0;
// Helper functions
function getCurrentTilemap() {
return tilemaps[currentTilemapIndex];
}
/**
* Initialize the map data array for current tilemap
*/
function initializeMapData() {
const tilemap = getCurrentTilemap();
tilemap.data = new Array(tilemap.width * tilemap.height).fill(0);
}
/**
* 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: name,
width: width,
height: height,
tileSize: 32,
data: new Array(width * height).fill(0)
};
tilemaps.push(newTilemap);
currentTilemapIndex = tilemaps.length - 1;
// Refresh the interface
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));
// Add delete button for inactive tabs (only if more than one map exists)
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);
});
// Add new tilemap button
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');
if (tileSizeSelect) tileSizeSelect.value = tilemap.tileSize;
if (gridWidth) gridWidth.value = tilemap.width;
if (gridHeight) gridHeight.value = tilemap.height;
}
/**
* Resize the grid with data preservation
*/
function resizeGrid(newWidth, newHeight) {
const tilemap = getCurrentTilemap();
if (newWidth === tilemap.width && newHeight === tilemap.height) {
return; // No change needed
}
// Ask for confirmation if grid has data
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;
}
// Save existing data
const oldData = [...tilemap.data];
const oldWidth = tilemap.width;
const oldHeight = tilemap.height;
// Update dimensions
tilemap.width = newWidth;
tilemap.height = newHeight;
// Create new data array
tilemap.data = new Array(newWidth * newHeight).fill(0);
// Copy existing tiles that fit in new dimensions
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];
}
}
// Recreate the grid
createMapGrid();
// Update the input fields
updateControls();
}
/**
* 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;
// Update the visual grid cell
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 = '';
// Restore or create coordinate label
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 = '';
if (tileId === 0) {
cell.style.backgroundColor = 'transparent';
return;
}
// Check if groups exist before trying to find tiles
if (typeof groups === 'undefined' || !groups) {
console.warn('No tile groups available');
return;
}
// Find the tile in our groups
for (const group of groups) {
const tile = group.tiles.find(t => t.uniqueId === tileId);
if (tile) {
// Create canvas to display the tile
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');
// Scale the tile to fit the map cell
const tempCanvas = document.createElement('canvas');
tempCanvas.width = tile.size;
tempCanvas.height = tile.size;
const tempCtx = tempCanvas.getContext('2d');
tempCtx.putImageData(tile.data, 0, 0);
ctx.drawImage(tempCanvas, 0, 0, tile.size, tile.size, 0, 0, tilemap.tileSize, tilemap.tileSize);
cell.appendChild(canvas);
// Add 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);
break;
}
}
}
/**
* 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;
`;
// Create grid cells
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: pointer; position: relative;
`;
// Add coordinate label
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);
// Add click handler
cell.addEventListener('click', () => {
if (selectedMapTile !== null) {
setMapTile(x, y, selectedMapTile);
}
});
// Add hover effect
cell.addEventListener('mouseenter', () => {
cell.style.backgroundColor = '#444';
});
cell.addEventListener('mouseleave', () => {
if (getMapTile(x, y) === 0) {
cell.style.backgroundColor = '#222';
}
});
container.appendChild(cell);
}
}
// Load existing map data
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;
const tilemap = getCurrentTilemap();
container.innerHTML = '';
// Check if groups exist
if (typeof groups === 'undefined' || !groups || groups.length === 0) {
container.innerHTML = '<div style="color: #888; padding: 10px;">No tile groups available. Use the Tile Picker first.</div>';
return;
}
// Create controls container
const controlsContainer = document.createElement('div');
controlsContainer.style.cssText = 'margin-bottom: 5px; display: flex; align-items: center; gap: 10px;';
// Group tabs
const tabContainer = document.createElement('div');
tabContainer.style.cssText = 'display: flex; gap: 2px;';
groups.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 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());
controlsContainer.appendChild(tabContainer);
controlsContainer.appendChild(clearBtn);
container.appendChild(controlsContainer);
// Create tiles container
const tilesContainer = document.createElement('div');
tilesContainer.style.cssText = 'display: flex; align-items: center; gap: 3px; flex-wrap: wrap;';
// Add eraser tool
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);
// Add tiles from active group
if (activePaletteGroup < groups.length) {
const activeGroup = groups[activePaletteGroup];
activeGroup.tiles.forEach(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;
`;
// Create canvas for tile preview
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');
// Draw scaled tile
const tempCanvas = document.createElement('canvas');
tempCanvas.width = tile.size;
tempCanvas.height = tile.size;
const tempCtx = tempCanvas.getContext('2d');
tempCtx.putImageData(tile.data, 0, 0);
ctx.drawImage(tempCanvas, 0, 0, tile.size, tile.size, 0, 0, tilemap.tileSize, tilemap.tileSize);
tileDiv.appendChild(canvas);
// Add ID badge
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);
// Add selection handler
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;">
<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% - 120px); overflow: auto; padding: 0 10px;">
<div id="mapGrid"></div>
</div>
`;
// Set up event handlers
setupEventHandlers();
// Initialize interface
updateTilemapTabs();
updateControls();
// Initialize the map data if needed
const tilemap = getCurrentTilemap();
if (!tilemap.data || tilemap.data.length === 0) {
initializeMapData();
}
createMapGrid();
createTilePalette();
}
/**
* Set up event handlers for controls
*/
function setupEventHandlers() {
// Tile size change handler
document.getElementById('tileSizeSelect').addEventListener('change', (e) => {
const tilemap = getCurrentTilemap();
tilemap.tileSize = parseInt(e.target.value);
createMapGrid();
createTilePalette();
});
// Grid resize handler
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');
}
});
// Allow Enter key to resize
['gridWidth', 'gridHeight'].forEach(id => {
document.getElementById(id).addEventListener('keypress', (e) => {
if (e.key === 'Enter') document.getElementById('resizeGrid').click();
});
});
}
/**
* Clear the entire current map
*/
function clearMap() {
if (confirm('Clear the entire map?')) {
const tilemap = getCurrentTilemap();
tilemap.data.fill(0);
// Refresh each cell
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');
}