📜
tilemap.js
Back
📝 Javascript ⚡ Executable Ctrl+S: Save • Ctrl+R: Run • Ctrl+F: Find
// 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; // Legacy variables for compatibility let mapWidth = 20; let mapHeight = 15; let mapTileSize = 32; let mapData = []; let selectedMapTile = null; let activePaletteGroup = 0; // Helper functions function getCurrentTilemap() { return tilemaps[currentTilemapIndex]; } function syncLegacyVars() { const tilemap = getCurrentTilemap(); mapWidth = tilemap.width; mapHeight = tilemap.height; mapTileSize = tilemap.tileSize; mapData = tilemap.data; } function syncToTilemap() { const tilemap = getCurrentTilemap(); tilemap.width = mapWidth; tilemap.height = mapHeight; tilemap.tileSize = mapTileSize; tilemap.data = mapData; } /** * Initialize the map data array */ function initializeMapData() { mapData = new Array(mapWidth * mapHeight).fill(0); syncToTilemap(); } /** * Create a new tilemap */ function createNewTilemap() { const newTilemap = { id: nextTilemapId++, name: `Map ${tilemaps.length + 1}`, width: 20, height: 15, tileSize: 32, data: new Array(20 * 15).fill(0) }; tilemaps.push(newTilemap); currentTilemapIndex = tilemaps.length - 1; syncLegacyVars(); // Refresh the interface updateTilemapTabs(); updateControls(); createMapGrid(); createTilePalette(); } /** * Switch to a different tilemap */ function switchTilemap(index) { if (index >= 0 && index < tilemaps.length) { syncToTilemap(); // Save current state currentTilemapIndex = index; syncLegacyVars(); // Load new state 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--; } syncLegacyVars(); 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 tileSizeSelect = document.getElementById('tileSizeSelect'); const gridWidth = document.getElementById('gridWidth'); const gridHeight = document.getElementById('gridHeight'); if (tileSizeSelect) tileSizeSelect.value = mapTileSize; if (gridWidth) gridWidth.value = mapWidth; if (gridHeight) gridHeight.value = mapHeight; } /** * Resize the grid with data preservation * @param {number} newWidth - New width in tiles * @param {number} newHeight - New height in tiles */ function resizeGrid(newWidth, newHeight) { if (newWidth === mapWidth && newHeight === mapHeight) { return; // No change needed } // Ask for confirmation if grid has data const hasData = mapData.some(tile => tile !== 0); if (hasData && !confirm(`Resize grid from ${mapWidth}×${mapHeight} to ${newWidth}×${newHeight}? This may crop or clear some tiles.`)) { return; } // Save existing data const oldData = [...mapData]; const oldWidth = mapWidth; const oldHeight = mapHeight; // Update dimensions mapWidth = newWidth; mapHeight = newHeight; // Create new data array initializeMapData(); // 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; mapData[newIndex] = oldData[oldIndex]; } } // Sync to current tilemap syncToTilemap(); // Recreate the grid createMapGrid(); // Update the input fields to show current values document.getElementById('gridWidth').value = mapWidth; document.getElementById('gridHeight').value = mapHeight; } /** * Set a tile in the map * @param {number} x - X coordinate * @param {number} y - Y coordinate * @param {number} tileId - Tile ID to place */ function setMapTile(x, y, tileId) { if (x >= 0 && x < mapWidth && y >= 0 && y < mapHeight) { const index = y * mapWidth + x; mapData[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 map * @param {number} x - X coordinate * @param {number} y - Y coordinate * @returns {number} Tile ID */ function getMapTile(x, y) { if (x >= 0 && x < mapWidth && y >= 0 && y < mapHeight) { const index = y * mapWidth + x; return mapData[index]; } return 0; } /** * Update a visual map cell with tile data * @param {HTMLElement} cell - The cell element * @param {number} tileId - Tile ID */ function updateMapCell(cell, tileId) { // Get the coordinate label before clearing const x = parseInt(cell.dataset.mapX); const y = parseInt(cell.dataset.mapY); // Clear previous content but keep coordinate label const coordLabel = cell.querySelector('.coord-label'); cell.innerHTML = ''; // Restore coordinate label if (coordLabel) { cell.appendChild(coordLabel); } else { const label = document.createElement('span'); label.className = 'coord-label'; label.textContent = `${x},${y}`; label.style.position = 'absolute'; label.style.top = '2px'; label.style.left = '2px'; label.style.fontSize = '8px'; label.style.color = 'rgba(255,255,255,0.5)'; label.style.pointerEvents = 'none'; label.style.zIndex = '1'; cell.appendChild(label); } cell.style.backgroundColor = ''; if (tileId === 0) { // Empty tile cell.style.backgroundColor = 'transparent'; 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 = mapTileSize; canvas.height = mapTileSize; canvas.style.width = '100%'; canvas.style.height = '100%'; canvas.style.position = 'absolute'; canvas.style.top = '0'; canvas.style.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, mapTileSize, mapTileSize); cell.appendChild(canvas); // Add ID badge const idBadge = document.createElement('span'); idBadge.className = 'id-badge'; idBadge.textContent = tileId; idBadge.style.position = 'absolute'; idBadge.style.bottom = '2px'; idBadge.style.right = '2px'; idBadge.style.fontSize = '8px'; idBadge.style.color = '#fff'; idBadge.style.backgroundColor = 'rgba(0,0,0,0.7)'; idBadge.style.padding = '1px 3px'; idBadge.style.borderRadius = '3px'; idBadge.style.pointerEvents = 'none'; idBadge.style.zIndex = '2'; cell.appendChild(idBadge); break; } } } /** * Create the map grid */ function createMapGrid() { const container = document.getElementById('mapGrid'); if (!container) return; container.innerHTML = ''; container.style.display = 'grid'; container.style.gridTemplateColumns = `repeat(${mapWidth}, ${mapTileSize}px)`; container.style.gridTemplateRows = `repeat(${mapHeight}, ${mapTileSize}px)`; container.style.gap = '1px'; container.style.backgroundColor = '#333'; container.style.padding = '10px'; container.style.overflow = 'auto'; container.style.maxHeight = '400px'; // Create grid cells for (let y = 0; y < mapHeight; y++) { for (let x = 0; x < mapWidth; x++) { const cell = document.createElement('div'); cell.className = 'map-cell'; cell.dataset.mapX = x; cell.dataset.mapY = y; cell.style.width = mapTileSize + 'px'; cell.style.height = mapTileSize + 'px'; cell.style.border = '1px solid rgba(255,255,255,0.2)'; cell.style.backgroundColor = '#222'; cell.style.cursor = 'pointer'; cell.style.position = 'relative'; // Add coordinate label const label = document.createElement('span'); label.className = 'coord-label'; label.textContent = `${x},${y}`; label.style.position = 'absolute'; label.style.top = '2px'; label.style.left = '2px'; label.style.fontSize = '8px'; label.style.color = 'rgba(255,255,255,0.5)'; label.style.pointerEvents = 'none'; label.style.zIndex = '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 < mapHeight; y++) { for (let x = 0; x < mapWidth; 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; container.innerHTML = ''; // Create group tabs and clear button container const controlsContainer = document.createElement('div'); controlsContainer.style.cssText = 'margin-bottom: 5px; display: flex; align-items: center; gap: 10px;'; // Group tabs container 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(); // Refresh palette }); tabContainer.appendChild(tab); }); // Clear button const clearBtn = document.createElement('button'); clearBtn.textContent = 'Clear'; 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: ${mapTileSize}px; height: ${mapTileSize}px; border: 2px solid #666; display: inline-block; cursor: pointer; background: #333; color: #fff; text-align: center; line-height: ${mapTileSize-4}px; font-size: 12px; position: relative; `; eraserBtn.textContent = '🗑️'; 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 only if (groups[activePaletteGroup]) { const activeGroup = groups[activePaletteGroup]; activeGroup.tiles.forEach(tile => { const tileDiv = document.createElement('div'); tileDiv.className = 'palette-tile'; tileDiv.style.cssText = ` width: ${mapTileSize}px; height: ${mapTileSize}px; border: 2px solid #666; display: inline-block; cursor: pointer; overflow: hidden; position: relative; `; // Create canvas for tile preview const canvas = document.createElement('canvas'); canvas.width = mapTileSize; canvas.height = mapTileSize; canvas.style.width = '100%'; canvas.style.height = '100%'; canvas.style.position = 'absolute'; canvas.style.top = '0'; canvas.style.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, mapTileSize, mapTileSize); tileDiv.appendChild(canvas); // Add ID badge to palette tile const idBadge = document.createElement('span'); idBadge.className = 'palette-id-badge'; idBadge.textContent = tile.uniqueId; idBadge.style.position = 'absolute'; idBadge.style.bottom = '1px'; idBadge.style.right = '1px'; idBadge.style.fontSize = '7px'; idBadge.style.color = '#fff'; idBadge.style.backgroundColor = 'rgba(0,0,0,0.8)'; idBadge.style.padding = '1px 2px'; idBadge.style.borderRadius = '2px'; idBadge.style.pointerEvents = 'none'; idBadge.style.zIndex = '2'; idBadge.style.lineHeight = '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 */ 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" value="${mapWidth}" 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" value="${mapHeight}" 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> `; // Sync current tilemap to legacy vars syncLegacyVars(); // Update tilemap tabs updateTilemapTabs(); // Add tile size change handler document.getElementById('tileSizeSelect').addEventListener('change', (e) => { mapTileSize = parseInt(e.target.value); syncToTilemap(); createMapGrid(); createTilePalette(); }); // Add 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 document.getElementById('gridWidth').addEventListener('keypress', (e) => { if (e.key === 'Enter') document.getElementById('resizeGrid').click(); }); document.getElementById('gridHeight').addEventListener('keypress', (e) => { if (e.key === 'Enter') document.getElementById('resizeGrid').click(); }); // Set initial values updateControls(); // Initialize the map if (!mapData || mapData.length === 0) { initializeMapData(); } createMapGrid(); createTilePalette(); } /** * Clear the entire map */ function clearMap() { if (confirm('Clear the entire map?')) { // Clear the data but don't affect the grid structure mapData.fill(0); // Refresh each cell while preserving coordinate labels for (let y = 0; y < mapHeight; y++) { for (let x = 0; x < mapWidth; x++) { const cell = document.querySelector(`[data-map-x="${x}"][data-map-y="${y}"]`); if (cell) { updateMapCell(cell, 0); } } } } } /** * Export map data */ function exportMap() { const exportData = { width: mapWidth, height: mapHeight, tileSize: mapTileSize, tiles: mapData, groups: groups.map(g => ({ id: g.id, url: g.url, tiles: g.tiles.map(t => ({ uniqueId: t.uniqueId, size: t.size, sourceX: t.sourceX, sourceY: t.sourceY, sourceUrl: t.sourceUrl })) })) }; console.log('Map Export:', exportData); alert('Map data exported to console'); } // 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'); }