📜
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'); } /* ============================================================ Tile groups bridge (from tilepicker.js) ============================================================ */ let groupsCache = []; // local cache from Tilepicker function readGroups() { // Prefer Tilepicker API if available 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)) { // Fallback to global (legacy) groupsCache = groups; } else { groupsCache = []; } // Keep active group in range if (activePaletteGroup >= groupsCache.length) { activePaletteGroup = Math.max(0, groupsCache.length - 1); } } function getTileDims(tile) { // Support both new (width/height) and legacy (size) tiles const w = tile.width != null ? tile.width : tile.size; const h = tile.height != null ? tile.height : tile.size; return { w, h }; } /* Listen to picker updates and refresh palette */ window.addEventListener('tiles:updated', (e) => { readGroups(); // If the overlay with palette is visible, rebuild it try { createTilePalette(); } catch (_) {} }); /* ============================================================ 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, width, height, tileSize: 32, data: new Array(width * height).fill(0) }; 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)); // 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; 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); 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(); } /** * 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 = ''; if (tileId === 0) { cell.style.backgroundColor = 'transparent'; return; } // Get groups & find tile readGroups(); if (!groupsCache.length) { console.warn('No tile groups available'); return; } 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); // Draw tile to cell 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); // 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); } /** * 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; `; 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; `; 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('click', () => { if (selectedMapTile !== null) setMapTile(x, y, selectedMapTile); }); cell.addEventListener('mouseenter', () => { cell.style.backgroundColor = '#444'; }); cell.addEventListener('mouseleave', () => { if (getMapTile(x, y) === 0) cell.style.backgroundColor = '#222'; }); container.appendChild(cell); } } 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(); // refresh cache 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 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;'; 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()); controlsContainer.appendChild(tabContainer); controlsContainer.appendChild(clearBtn); container.appendChild(controlsContainer); // Tiles container const tilesContainer = document.createElement('div'); tilesContainer.style.cssText = 'display: flex; align-items: center; gap: 3px; flex-wrap: wrap;'; // 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); // 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;"> <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> `; setupEventHandlers(); // Read groups once on open so the first palette is accurate 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(); }); }); } /** * Clear the entire current map */ function clearMap() { if (confirm('Clear the entire map?')) { const tilemap = getCurrentTilemap(); tilemap.data.fill(0); 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'); }