๐Ÿ“œ
tilemap.js
โ† Back
๐Ÿ“ Javascript โšก Executable Ctrl+S: Save โ€ข Ctrl+R: Run โ€ข Ctrl+F: Find
/* ---------- Tile Map Management ---------- */ /* DOM Elements - only declare tilemap-specific elements */ let tilemapBtn; /* State */ let isTilemapMode = false; let tilemaps = []; // Array to store multiple tilemaps let activeTilemapId = null; // ID of the currently active tilemap let selectedTile = null; // Currently selected tile for painting let activeTab = 0; // Track the active line tab (index) /* Tilemap Object Structure */ function createTilemap(id, name, width = 32, height = 24, baseTileSize = 64) { return { id, name, gridWidth: width, gridHeight: height, baseTileSize, tileMapGrid: Array(height).fill().map(() => Array(width).fill(null)), tileMapContainer: null, gridCells: [], attributes: [] // Initialize an empty attributes array }; } /* ---------- Tile Map Mode Controls ---------- */ function enableTilemapMode() { try { const objectsBar = document.getElementById('objectsBar'); const linesBar = document.getElementById('linesBar'); const tankBar = document.getElementById('tankBar'); const metaEl = document.getElementById('meta'); if (objectsBar) objectsBar.style.display = 'none'; if (linesBar) linesBar.style.display = 'none'; if (tankBar) tankBar.style.display = 'none'; if (metaEl) { metaEl.textContent = 'Tile Map Mode: Click a tile to select it, then click grid squares to place it. Right-click or use trash can to erase.'; } if (tilemapBtn) { tilemapBtn.style.background = 'var(--accent)'; tilemapBtn.style.color = '#000'; } isTilemapMode = true; if (tilemaps.length === 0) { tilemaps.push(createTilemap('tilemap-1', 'Tilemap 1')); activeTilemapId = 'tilemap-1'; } showTileMap(); showTileMapTank(); } catch (error) { console.error('Error enabling tilemap mode:', error); alert('Error enabling tilemap mode: ' + error.message); } } function disableTilemapMode() { try { const objectsBar = document.getElementById('objectsBar'); const linesBar = document.getElementById('linesBar'); const tankBar = document.getElementById('tankBar'); const metaEl = document.getElementById('meta'); if (objectsBar) objectsBar.style.display = ''; if (linesBar) linesBar.style.display = ''; if (tankBar) tankBar.style.display = ''; if (metaEl) { if (window.CoreAPI && window.CoreAPI.imgW && window.CoreAPI.imgH) { metaEl.textContent = `Image: ${window.CoreAPI.imgW}ร—${window.CoreAPI.imgH} โ€” tap tiles to add to the selected line`; } else { metaEl.textContent = 'Open an image, then tap grid tiles to add thumbnails to the selected line.'; } } if (tilemapBtn) { tilemapBtn.style.background = ''; tilemapBtn.style.color = ''; } isTilemapMode = false; tilemaps.forEach(tm => { if (tm.tileMapContainer) { tm.tileMapContainer.style.display = 'none'; } }); hideTileMapTank(); } catch (error) { console.error('Error disabling tilemap mode:', error); alert('Error disabling tilemap mode: ' + error.message); } } function toggleTilemapMode() { if (isTilemapMode) { disableTilemapMode(); } else { enableTilemapMode(); } } /* ---------- Tile Scaling Utilities ---------- */ function getTileScale(tileData, tilemap) { const scaleX = Math.ceil(tileData.w / tilemap.baseTileSize); const scaleY = Math.ceil(tileData.h / tilemap.baseTileSize); return { scaleX, scaleY }; } function canPlaceTileAt(gridX, gridY, scaleX, scaleY, tilemap) { if (gridX + scaleX > tilemap.gridWidth || gridY + scaleY > tilemap.gridHeight) { return false; } for (let y = gridY; y < gridY + scaleY; y++) { for (let x = gridX; x < gridX + scaleX; x++) { if (tilemap.tileMapGrid[y] && tilemap.tileMapGrid[y][x]) { return false; } } } return true; } function clearTileArea(gridX, gridY, scaleX, scaleY, tilemap) { for (let y = gridY; y < gridY + scaleY; y++) { for (let x = gridX; x < gridX + scaleX; x++) { if (tilemap.tileMapGrid[y] && tilemap.tileMapGrid[y][x]) { removeTileFromGrid(x, y, tilemap); } } } } /* ---------- Div-Based Tile Map Creation ---------- */ function createBlankTileMap(tilemap) { try { if (!tilemap) { tilemap = tilemaps.find(tm => tm.id === activeTilemapId); if (!tilemap) { throw new Error('No active tilemap found'); } } if (tilemap.tileMapContainer) { tilemap.tileMapContainer.remove(); } tilemap.tileMapContainer = document.createElement('div'); tilemap.tileMapContainer.id = `tileMapContainer-${tilemap.id}`; tilemap.tileMapContainer.style.position = 'absolute'; tilemap.tileMapContainer.style.left = '50px'; tilemap.tileMapContainer.style.top = '50px'; tilemap.tileMapContainer.style.width = (tilemap.gridWidth * tilemap.baseTileSize) + 'px'; tilemap.tileMapContainer.style.height = (tilemap.gridHeight * tilemap.baseTileSize) + 'px'; tilemap.tileMapContainer.style.background = '#1a1a1a'; tilemap.tileMapContainer.style.border = '2px solid #4fc3f7'; tilemap.tileMapContainer.style.zIndex = '15'; tilemap.tileMapContainer.style.position = 'relative'; tilemap.gridCells = []; for (let y = 0; y < tilemap.gridHeight; y++) { tilemap.gridCells[y] = []; for (let x = 0; x < tilemap.gridWidth; x++) { const cell = document.createElement('div'); cell.className = 'grid-cell'; cell.style.position = 'absolute'; cell.style.left = (x * tilemap.baseTileSize) + 'px'; cell.style.top = (y * tilemap.baseTileSize) + 'px'; cell.style.width = tilemap.baseTileSize + 'px'; cell.style.height = tilemap.baseTileSize + 'px'; cell.style.border = '1px solid rgba(79, 195, 247, 0.2)'; cell.style.boxSizing = 'border-box'; cell.style.cursor = 'pointer'; cell.style.backgroundColor = 'transparent'; cell.dataset.gridX = x; cell.dataset.gridY = y; cell.addEventListener('mouseenter', () => { if (selectedTile) { const { scaleX, scaleY } = getTileScale(selectedTile, tilemap); highlightPlacementArea(x, y, scaleX, scaleY, tilemap); } else { cell.style.backgroundColor = 'rgba(255, 68, 68, 0.3)'; } }); cell.addEventListener('mouseleave', () => { clearHighlights(tilemap); }); cell.addEventListener('click', (e) => handleCellClick(e, tilemap)); cell.addEventListener('contextmenu', (e) => handleCellRightClick(e, tilemap)); tilemap.tileMapContainer.appendChild(cell); tilemap.gridCells[y][x] = cell; } } const viewport = document.getElementById('viewport'); if (viewport) { viewport.appendChild(tilemap.tileMapContainer); } else { alert('ERROR: Viewport not found!'); return; } // Re-place tiles from the grid data for (let y = 0; y < tilemap.gridHeight; y++) { for (let x = 0; x < tilemap.gridWidth; x++) { if (tilemap.tileMapGrid[y] && tilemap.tileMapGrid[y][x]) { const tileData = tilemap.tileMapGrid[y][x]; // We must ensure the tile is placed only once per multi-tile object if (x === tileData.originX && y === tileData.originY) { placeTileOnGridDOM(x, y, tileData.data, tilemap); } } } } } catch (error) { console.error('Error creating blank tile map:', error); alert('Error creating blank tile map: ' + error.message); } } function showTileMap() { try { tilemaps.forEach(tm => { if (tm.tileMapContainer) { tm.tileMapContainer.style.display = 'none'; } }); const tilemap = tilemaps.find(tm => tm.id === activeTilemapId); if (tilemap) { if (!tilemap.tileMapContainer) { createBlankTileMap(tilemap); } else { tilemap.tileMapContainer.style.display = 'block'; } } } catch (error) { console.error('Error showing tile map:', error); alert('Error showing tile map: ' + error.message); } } function highlightPlacementArea(startX, startY, scaleX, scaleY, tilemap) { clearHighlights(tilemap); const canPlace = canPlaceTileAt(startX, startY, scaleX, scaleY, tilemap); const color = canPlace ? 'rgba(79, 195, 247, 0.4)' : 'rgba(255, 68, 68, 0.4)'; for (let y = startY; y < startY + scaleY && y < tilemap.gridHeight; y++) { for (let x = startX; x < startX + scaleX && x < tilemap.gridWidth; x++) { if (tilemap.gridCells[y] && tilemap.gridCells[y][x]) { tilemap.gridCells[y][x].style.backgroundColor = color; } } } } function clearHighlights(tilemap) { for (let y = 0; y < tilemap.gridHeight; y++) { for (let x = 0; x < tilemap.gridWidth; x++) { if (tilemap.gridCells[y] && tilemap.gridCells[y][x] && !tilemap.tileMapGrid[y][x]) { tilemap.gridCells[y][x].style.backgroundColor = 'transparent'; } } } } function hideTileMap() { tilemaps.forEach(tm => { if (tm.tileMapContainer) { tm.tileMapContainer.style.display = 'none'; } }); } /* ---------- Click Handlers for Grid Cells ---------- */ function handleCellClick(e, tilemap) { e.preventDefault(); e.stopPropagation(); const gridX = parseInt(e.target.dataset.gridX); const gridY = parseInt(e.target.dataset.gridY); if (selectedTile === null) { removeTileFromGrid(gridX, gridY, tilemap); } else { const { scaleX, scaleY } = getTileScale(selectedTile, tilemap); if (canPlaceTileAt(gridX, gridY, scaleX, scaleY, tilemap)) { placeTileOnGrid(gridX, gridY, selectedTile, tilemap); } else { if (gridX + scaleX > tilemap.gridWidth || gridY + scaleY > tilemap.gridHeight) { alert(`Tile (${selectedTile.w}ร—${selectedTile.h}) doesn't fit here. Need ${scaleX}ร—${scaleY} free squares.`); } else { alert('Cannot place tile here - area is occupied.'); } } } } function handleCellRightClick(e, tilemap) { e.preventDefault(); e.stopPropagation(); const gridX = parseInt(e.target.dataset.gridX); const gridY = parseInt(e.target.dataset.gridY); removeTileFromGrid(gridX, gridY, tilemap); } /* ---------- Tile Placement and Removal ---------- */ function placeTileOnGridDOM(gridX, gridY, tileData, tilemap) { try { const { scaleX, scaleY } = getTileScale(tileData, tilemap); const tileEl = document.createElement('div'); tileEl.className = 'placed-tile'; tileEl.style.position = 'absolute'; tileEl.style.left = (gridX * tilemap.baseTileSize) + 'px'; tileEl.style.top = (gridY * tilemap.baseTileSize) + 'px'; tileEl.style.width = (scaleX * tilemap.baseTileSize) + 'px'; tileEl.style.height = (scaleY * tilemap.baseTileSize) + 'px'; tileEl.style.backgroundImage = `url(${tileData.thumbDataURL})`; tileEl.style.backgroundSize = 'contain'; tileEl.style.backgroundRepeat = 'no-repeat'; tileEl.style.backgroundPosition = 'center'; tileEl.style.imageRendering = 'pixelated'; tileEl.style.border = '2px solid #4fc3f7'; tileEl.style.boxSizing = 'border-box'; tileEl.style.zIndex = '10'; tileEl.style.pointerEvents = 'none'; const idBadge = document.createElement('div'); idBadge.style.position = 'absolute'; idBadge.style.top = '2px'; idBadge.style.left = '2px'; idBadge.style.background = '#4fc3f7'; idBadge.style.color = '#000'; idBadge.style.padding = '1px 4px'; idBadge.style.borderRadius = '3px'; idBadge.style.fontSize = '10px'; idBadge.style.fontWeight = 'bold'; idBadge.style.lineHeight = '1'; idBadge.textContent = `#${tileData.id}`; tileEl.appendChild(idBadge); if (scaleX > 1 || scaleY > 1) { const sizeIndicator = document.createElement('div'); sizeIndicator.style.position = 'absolute'; sizeIndicator.style.top = '2px'; sizeIndicator.style.right = '2px'; sizeIndicator.style.background = 'rgba(0,0,0,0.8)'; sizeIndicator.style.color = '#ffff00'; sizeIndicator.style.padding = '1px 4px'; sizeIndicator.style.borderRadius = '3px'; sizeIndicator.style.fontSize = '10px'; sizeIndicator.style.fontWeight = 'bold'; sizeIndicator.style.lineHeight = '1'; sizeIndicator.textContent = `${scaleX}ร—${scaleY}`; tileEl.appendChild(sizeIndicator); } tilemap.tileMapContainer.appendChild(tileEl); return tileEl; } catch (error) { console.error('Error placing tile on grid:', error); alert('Error placing tile on grid: ' + error.message); return null; } } function placeTileOnGrid(gridX, gridY, tileData, tilemap) { const { scaleX, scaleY } = getTileScale(tileData, tilemap); const tileEl = placeTileOnGridDOM(gridX, gridY, tileData, tilemap); if (tileEl) { const tileRef = { element: tileEl, data: tileData, scaleX, scaleY, originX: gridX, originY: gridY }; for (let y = gridY; y < gridY + scaleY; y++) { for (let x = gridX; x < gridX + scaleX; x++) { tilemap.tileMapGrid[y][x] = tileRef; if (tilemap.gridCells[y] && tilemap.gridCells[y][x]) { tilemap.gridCells[y][x].style.backgroundColor = 'rgba(79, 195, 247, 0.1)'; } } } } } function removeTileFromGrid(gridX, gridY, tilemap) { try { const tileRef = tilemap.tileMapGrid[gridY][gridX]; if (tileRef) { tileRef.element.remove(); for (let y = tileRef.originY; y < tileRef.originY + tileRef.scaleY; y++) { for (let x = tileRef.originX; x < tileRef.originX + tileRef.scaleX; x++) { if (y >= 0 && y < tilemap.gridHeight && x >= 0 && x < tilemap.gridWidth) { tilemap.tileMapGrid[y][x] = null; if (tilemap.gridCells[y] && tilemap.gridCells[y][x]) { tilemap.gridCells[y][x].style.backgroundColor = 'transparent'; } } } } } } catch (error) { console.error('Error removing tile from grid:', error); alert('Error removing tile from grid: ' + error.message); } } /* ---------- Grid Resizing with Div Updates ---------- */ function resizeGrid(newWidth, newHeight) { try { const tilemap = tilemaps.find(tm => tm.id === activeTilemapId); if (!tilemap) { throw new Error('No active tilemap found'); } if (newWidth < 1 || newHeight < 1) { alert('Grid size must be at least 1x1'); return; } if (newWidth > 100 || newHeight > 100) { alert('Grid size cannot exceed 100x100'); return; } const oldWidth = tilemap.gridWidth; const oldHeight = tilemap.gridHeight; const newGrid = Array(newHeight).fill().map(() => Array(newWidth).fill(null)); for (let y = 0; y < Math.min(oldHeight, newHeight); y++) { for (let x = 0; x < Math.min(oldWidth, newWidth); x++) { if (tilemap.tileMapGrid[y] && tilemap.tileMapGrid[y][x]) { // Check if this is the origin of a multi-tile object if (x === tilemap.tileMapGrid[y][x].originX && y === tilemap.tileMapGrid[y][x].originY) { const tileRef = tilemap.tileMapGrid[y][x]; // Check if the whole tile fits in the new grid if (x + tileRef.scaleX <= newWidth && y + tileRef.scaleY <= newHeight) { for (let sy = y; sy < y + tileRef.scaleY; sy++) { for (let sx = x; sx < x + tileRef.scaleX; sx++) { newGrid[sy][sx] = tileRef; } } } } } } } tilemap.gridWidth = newWidth; tilemap.gridHeight = newHeight; tilemap.tileMapGrid = newGrid; createBlankTileMap(tilemap); updateTileMapTank(); } catch (error) { console.error('Error resizing grid:', error); alert('Error resizing grid: ' + error.message); } } /* ---------- Base Size Control with Grid Rebuild ---------- */ function changeBaseSize(newSize) { try { const tilemap = tilemaps.find(tm => tm.id === activeTilemapId); if (!tilemap) { throw new Error('No active tilemap found'); } if (newSize < 8 || newSize > 128 || !Number.isInteger(Math.log2(newSize))) { alert('Base size must be a power of 2 between 8 and 128 pixels'); return; } const hasTiles = tilemap.tileMapGrid.some(row => row.some(cell => cell !== null)); if (hasTiles) { if (!confirm(`Changing base size will clear the current tilemap (${tilemap.name}). Continue?`)) { return; } tilemap.tileMapGrid = Array(tilemap.gridHeight).fill().map(() => Array(tilemap.gridWidth).fill(null)); } tilemap.baseTileSize = newSize; createBlankTileMap(tilemap); updateTileMapTank(); } catch (error) { console.error('Error changing base size:', error); alert('Error changing base size: ' + error.message); } } /* ---------- Tile Map Tank (shows available tiles) ---------- */ let tileMapTank = null; function showTileMapTank() { try { if (!tileMapTank) { tileMapTank = document.createElement('div'); tileMapTank.id = 'tileMapTank'; tileMapTank.style.position = 'fixed'; tileMapTank.style.top = '60px'; tileMapTank.style.left = '10px'; tileMapTank.style.right = '10px'; tileMapTank.style.maxHeight = '180px'; tileMapTank.style.background = '#161616'; tileMapTank.style.border = '1px solid #2a2a2a'; tileMapTank.style.borderRadius = '.6rem'; tileMapTank.style.padding = '.5rem'; tileMapTank.style.zIndex = '30'; tileMapTank.style.display = 'flex'; tileMapTank.style.flexDirection = 'column'; tileMapTank.style.gap = '.3rem'; tileMapTank.style.overflow = 'hidden'; const style = document.createElement('style'); style.textContent = ` @media (max-width: 600px) { #tileMapTank { left: 5px; right: 5px; max-height: 150px; top: 60px; } .tilemap-controls-row { flex-wrap: wrap; gap: 0.3rem; } .tilemap-controls-row > div, .tilemap-controls-row > button, .tilemap-controls-row > select { font-size: 0.8rem; padding: 0.15rem 0.4rem; } .tilemap-controls-row > div > button, .tilemap-controls-row > div > span { font-size: 0.8rem; padding: 0.15rem 0.4rem; } .tilemap-controls-row > span { font-size: 0.8rem; } .tilemap-tabs-row { gap: 0.3rem; } .tilemap-tabs-row > button { font-size: 0.8rem; padding: 0.15rem 0.4rem; } } `; document.head.appendChild(style); document.body.appendChild(tileMapTank); } tileMapTank.style.display = 'flex'; updateTileMapTank(); } catch (error) { console.error('Error showing tile map tank:', error); alert('Error showing tile map tank: ' + error.message); } } function hideTileMapTank() { if (tileMapTank) { tileMapTank.style.display = 'none'; } } function updateTileMapTank() { if (!tileMapTank) return; try { tileMapTank.innerHTML = ''; const tilemap = tilemaps.find(tm => tm.id === activeTilemapId); if (!tilemap) { throw new Error('No active tilemap found'); } const controlsRow = document.createElement('div'); controlsRow.className = 'tilemap-controls-row'; controlsRow.style.display = 'flex'; controlsRow.style.alignItems = 'center'; controlsRow.style.gap = '0.5rem'; controlsRow.style.marginBottom = '.3rem'; const tilemapLabel = document.createElement('div'); tilemapLabel.textContent = 'Tilemap:'; tilemapLabel.style.color = '#4fc3f7'; tilemapLabel.style.fontWeight = 'bold'; tilemapLabel.style.fontSize = '.9rem'; const tilemapSelect = document.createElement('select'); tilemapSelect.style.background = '#333'; tilemapSelect.style.border = '1px solid #555'; tilemapSelect.style.color = '#eee'; tilemapSelect.style.padding = '.2rem'; tilemapSelect.style.borderRadius = '.3rem'; tilemapSelect.style.fontSize = '.9rem'; tilemapSelect.style.maxWidth = '120px'; tilemaps.forEach(tm => { const option = document.createElement('option'); option.value = tm.id; option.textContent = tm.name; if (tm.id === activeTilemapId) { option.selected = true; } tilemapSelect.appendChild(option); }); tilemapSelect.addEventListener('change', (e) => { console.log(`Switching to tilemap: ${e.target.value}`); activeTilemapId = e.target.value; showTileMap(); updateTileMapTank(); }); const newTilemapBtn = document.createElement('button'); newTilemapBtn.textContent = '+ New'; newTilemapBtn.style.background = '#333'; newTilemapBtn.style.border = '1px solid #555'; newTilemapBtn.style.color = '#eee'; newTilemapBtn.style.padding = '.2rem .4rem'; newTilemapBtn.style.borderRadius = '.3rem'; newTilemapBtn.style.cursor = 'pointer'; newTilemapBtn.style.fontSize = '.9rem'; newTilemapBtn.addEventListener('click', () => { const newId = `tilemap-${tilemaps.length + 1}`; const newName = `Tilemap ${tilemaps.length + 1}`; tilemaps.push(createTilemap(newId, newName)); activeTilemapId = newId; createBlankTileMap(); updateTileMapTank(); console.log(`Created new tilemap: ${newId}`); }); const sizeLabel = document.createElement('div'); sizeLabel.textContent = 'Base Size:'; sizeLabel.style.color = '#4fc3f7'; sizeLabel.style.fontWeight = 'bold'; sizeLabel.style.fontSize = '.9rem'; const baseSizeControls = document.createElement('div'); baseSizeControls.style.display = 'flex'; baseSizeControls.style.alignItems = 'center'; baseSizeControls.style.gap = '.2rem'; const baseSizeMinus = document.createElement('button'); baseSizeMinus.textContent = '-'; baseSizeMinus.style.background = '#333'; baseSizeMinus.style.border = '1px solid #555'; baseSizeMinus.style.color = '#eee'; baseSizeMinus.style.padding = '.2rem .4rem'; baseSizeMinus.style.borderRadius = '.3rem'; baseSizeMinus.style.cursor = 'pointer'; baseSizeMinus.style.fontSize = '.9rem'; baseSizeMinus.disabled = tilemap.baseTileSize <= 8; baseSizeMinus.addEventListener('click', () => changeBaseSize(tilemap.baseTileSize / 2)); const baseSizeDisplay = document.createElement('span'); baseSizeDisplay.textContent = `${tilemap.baseTileSize}px`; baseSizeDisplay.style.color = '#eee'; baseSizeDisplay.style.minWidth = '35px'; baseSizeDisplay.style.textAlign = 'center'; baseSizeDisplay.style.fontSize = '.9rem'; const baseSizePlus = document.createElement('button'); baseSizePlus.textContent = '+'; baseSizePlus.style.background = '#333'; baseSizePlus.style.border = '1px solid #555'; baseSizePlus.style.color = '#eee'; baseSizePlus.style.padding = '.2rem .4rem'; baseSizePlus.style.borderRadius = '.3rem'; baseSizePlus.style.cursor = 'pointer'; baseSizePlus.style.fontSize = '.9rem'; baseSizePlus.disabled = tilemap.baseTileSize >= 128; baseSizePlus.addEventListener('click', () => changeBaseSize(tilemap.baseTileSize * 2)); baseSizeControls.appendChild(baseSizeMinus); baseSizeControls.appendChild(baseSizeDisplay); baseSizeControls.appendChild(baseSizePlus); const gridSizeLabel = document.createElement('div'); gridSizeLabel.textContent = 'Grid:'; gridSizeLabel.style.color = '#4fc3f7'; gridSizeLabel.style.fontWeight = 'bold'; gridSizeLabel.style.fontSize = '.9rem'; const widthControls = document.createElement('div'); widthControls.style.display = 'flex'; widthControls.style.alignItems = 'center'; widthControls.style.gap = '.2rem'; const widthMinus = document.createElement('button'); widthMinus.textContent = '-'; widthMinus.style.background = '#333'; widthMinus.style.border = '1px solid #555'; widthMinus.style.color = '#eee'; widthMinus.style.padding = '.2rem .4rem'; widthMinus.style.borderRadius = '.3rem'; widthMinus.style.cursor = 'pointer'; widthMinus.style.fontSize = '.9rem'; widthMinus.addEventListener('click', () => resizeGrid(tilemap.gridWidth - 1, tilemap.gridHeight)); const widthDisplay = document.createElement('span'); widthDisplay.textContent = `${tilemap.gridWidth}`; widthDisplay.style.color = '#eee'; widthDisplay.style.minWidth = '20px'; widthDisplay.style.textAlign = 'center'; widthDisplay.style.fontSize = '.9rem'; const widthPlus = document.createElement('button'); widthPlus.textContent = '+'; widthPlus.style.background = '#333'; widthPlus.style.border = '1px solid #555'; widthPlus.style.color = '#eee'; widthPlus.style.padding = '.2rem .4rem'; widthPlus.style.borderRadius = '.3rem'; widthPlus.style.cursor = 'pointer'; widthPlus.style.fontSize = '.9rem'; widthPlus.addEventListener('click', () => resizeGrid(tilemap.gridWidth + 1, tilemap.gridHeight)); const xLabel = document.createElement('span'); xLabel.textContent = 'ร—'; xLabel.style.color = '#bbb'; xLabel.style.fontSize = '.9rem'; const heightControls = document.createElement('div'); heightControls.style.display = 'flex'; heightControls.style.alignItems = 'center'; heightControls.style.gap = '.2rem'; const heightMinus = document.createElement('button'); heightMinus.textContent = '-'; heightMinus.style.background = '#333'; heightMinus.style.border = '1px solid #555'; heightMinus.style.color = '#eee'; heightMinus.style.padding = '.2rem .4rem'; heightMinus.style.borderRadius = '.3rem'; heightMinus.style.cursor = 'pointer'; heightMinus.style.fontSize = '.9rem'; heightMinus.addEventListener('click', () => resizeGrid(tilemap.gridWidth, tilemap.gridHeight - 1)); const heightDisplay = document.createElement('span'); heightDisplay.textContent = `${tilemap.gridHeight}`; heightDisplay.style.color = '#eee'; heightDisplay.style.minWidth = '20px'; heightDisplay.style.textAlign = 'center'; heightDisplay.style.fontSize = '.9rem'; const heightPlus = document.createElement('button'); heightPlus.textContent = '+'; heightPlus.style.background = '#333'; heightPlus.style.border = '1px solid #555'; heightPlus.style.color = '#eee'; heightPlus.style.padding = '.2rem .4rem'; heightPlus.style.borderRadius = '.3rem'; heightPlus.style.cursor = 'pointer'; heightPlus.style.fontSize = '.9rem'; heightPlus.addEventListener('click', () => resizeGrid(tilemap.gridWidth, tilemap.gridHeight + 1)); widthControls.appendChild(widthMinus); widthControls.appendChild(widthDisplay); widthControls.appendChild(widthPlus); heightControls.appendChild(heightMinus); heightControls.appendChild(heightDisplay); heightControls.appendChild(heightPlus); const clearBtn = document.createElement('button'); clearBtn.textContent = '๐Ÿงน Clear'; clearBtn.style.background = '#444'; clearBtn.style.border = '1px solid #666'; clearBtn.style.color = '#eee'; clearBtn.style.padding = '.3rem .5rem'; clearBtn.style.borderRadius = '.3rem'; clearBtn.style.cursor = 'pointer'; clearBtn.style.fontSize = '.9rem'; clearBtn.addEventListener('click', () => clearTileMap(tilemap)); controlsRow.appendChild(tilemapLabel); controlsRow.appendChild(tilemapSelect); controlsRow.appendChild(newTilemapBtn); controlsRow.appendChild(sizeLabel); controlsRow.appendChild(baseSizeControls); controlsRow.appendChild(gridSizeLabel); controlsRow.appendChild(widthControls); controlsRow.appendChild(xLabel); controlsRow.appendChild(heightControls); controlsRow.appendChild(clearBtn); const tabsRow = document.createElement('div'); tabsRow.className = 'tilemap-tabs-row'; tabsRow.style.display = 'flex'; tabsRow.style.gap = '.3rem'; tabsRow.style.alignItems = 'center'; controlsRow.appendChild(tabsRow); const contentContainer = document.createElement('div'); contentContainer.style.flex = '1'; contentContainer.style.overflowY = 'auto'; const lineTabs = []; const lineContents = []; if (window.WorkspaceAPI && window.WorkspaceAPI.workspace) { const workspace = window.WorkspaceAPI.workspace; const obj = workspace.objects[0]; if (obj) { if (activeTab >= obj.lines.length) { activeTab = 0; } obj.lines.forEach((line, index) => { const lineTab = document.createElement('button'); lineTab.textContent = line.name; lineTab.style.background = activeTab === index ? '#4fc3f7' : '#333'; lineTab.style.border = '1px solid #555'; lineTab.style.color = activeTab === index ? '#000' : '#eee'; lineTab.style.padding = '.2rem .4rem'; lineTab.style.borderRadius = '.3rem'; lineTab.style.cursor = 'pointer'; lineTab.style.fontSize = '.9rem'; lineTab.dataset.tabId = `line-${index}`; const lineContainer = document.createElement('div'); lineContainer.id = `line-${index}-content`; lineContainer.style.display = activeTab === index ? 'block' : 'none'; lineContainer.style.padding = '.2rem'; const contentRow = document.createElement('div'); contentRow.style.display = 'flex'; contentRow.style.alignItems = 'center'; contentRow.style.gap = '.3rem'; const eraser = document.createElement('div'); eraser.className = 'tilemap-source-tile eraser'; eraser.style.position = 'relative'; eraser.style.width = '50px'; eraser.style.height = '50px'; eraser.style.border = selectedTile === null ? '3px solid #ff4444' : '1px solid #333'; eraser.style.borderRadius = '.35rem'; eraser.style.background = '#333'; eraser.style.cursor = 'pointer'; eraser.style.flexShrink = '0'; eraser.style.display = 'flex'; eraser.style.alignItems = 'center'; eraser.style.justifyContent = 'center'; eraser.style.fontSize = '16px'; eraser.textContent = '๐Ÿ—‘๏ธ'; eraser.title = 'Click to select eraser'; eraser.addEventListener('click', () => { console.log('Eraser clicked'); selectedTile = null; updateTileMapTank(); }); contentRow.appendChild(eraser); const tilesRow = document.createElement('div'); tilesRow.style.display = 'flex'; tilesRow.style.gap = '.3rem'; tilesRow.style.overflowX = 'auto'; line.items.forEach((tile) => { const { scaleX, scaleY } = getTileScale(tile, tilemap); const tileEl = document.createElement('div'); tileEl.className = 'tilemap-source-tile'; tileEl.style.position = 'relative'; tileEl.style.width = '50px'; tileEl.style.height = '50px'; tileEl.style.border = selectedTile && selectedTile.id === tile.id ? '3px solid #4fc3f7' : '1px solid #333'; tileEl.style.borderRadius = '.35rem'; tileEl.style.overflow = 'hidden'; tileEl.style.background = '#111'; tileEl.style.cursor = 'pointer'; tileEl.style.flexShrink = '0'; const img = document.createElement('img'); img.src = tile.thumbDataURL; img.style.width = '100%'; img.style.height = '100%'; img.style.objectFit = 'contain'; img.style.imageRendering = 'pixelated'; img.draggable = false; const badge = document.createElement('div'); badge.style.position = 'absolute'; badge.style.left = '2px'; badge.style.top = '2px'; badge.style.background = '#4fc3f7'; badge.style.color = '#000'; badge.style.borderRadius = '.2rem'; badge.style.padding = '.05rem .2rem'; badge.style.fontWeight = 'bold'; badge.style.fontSize = '.6rem'; badge.textContent = `#${tile.id}`; if (scaleX > 1 || scaleY > 1) { const sizeIndicator = document.createElement('div'); sizeIndicator.style.position = 'absolute'; sizeIndicator.style.bottom = '2px'; sizeIndicator.style.right = '2px'; sizeIndicator.style.background = 'rgba(0,0,0,0.8)'; sizeIndicator.style.color = '#ffff00'; sizeIndicator.style.padding = '.05rem .2rem'; sizeIndicator.style.borderRadius = '.2rem'; sizeIndicator.style.fontSize = '.6rem'; sizeIndicator.style.fontWeight = 'bold'; sizeIndicator.textContent = `${scaleX}ร—${scaleY}`; tileEl.appendChild(sizeIndicator); } tileEl.appendChild(img); tileEl.appendChild(badge); tileEl.tileData = tile; tileEl.title = `${tile.w}ร—${tile.h}px (${scaleX}ร—${scaleY} squares)`; tileEl.addEventListener('click', () => { console.log(`Tile ${tile.id} clicked in line ${index}`); selectedTile = tile; activeTab = index; updateTileMapTank(); }); tilesRow.appendChild(tileEl); }); contentRow.appendChild(tilesRow); lineContainer.appendChild(contentRow); lineTabs.push(lineTab); lineContents.push(lineContainer); tabsRow.appendChild(lineTab); contentContainer.appendChild(lineContainer); }); } else { const noLinesMessage = document.createElement('div'); noLinesMessage.textContent = 'No lines available'; noLinesMessage.style.color = '#eee'; noLinesMessage.style.padding = '.5rem'; contentContainer.appendChild(noLinesMessage); } } else { const noDataMessage = document.createElement('div'); noDataMessage.textContent = 'No workspace data available'; noDataMessage.style.color = '#eee'; noDataMessage.style.padding = '.5rem'; contentContainer.appendChild(noDataMessage); } lineTabs.forEach((tab, index) => { tab.addEventListener('click', () => { console.log(`Line tab ${index} clicked`); activeTab = index; updateTileMapTank(); }); }); tileMapTank.appendChild(controlsRow); tileMapTank.appendChild(contentContainer); console.log(`Active tilemap: ${activeTilemapId}, Active tab: ${activeTab}`); } catch (error) { console.error('Error updating tile map tank:', error); alert('Error updating tile map tank: ' + error.message); } } /* ---------- Export Functions ---------- */ function clearTileMap(tilemap) { try { for (let y = 0; y < tilemap.gridHeight; y++) { for (let x = 0; x < tilemap.gridWidth; x++) { if (tilemap.tileMapGrid[y] && tilemap.tileMapGrid[y][x]) { removeTileFromGrid(x, y, tilemap); } } } createBlankTileMap(tilemap); updateTileMapTank(); } catch (error) { console.error('Error clearing tile map:', error); alert('Error clearing tile map: ' + error.message); } } function exportTileMap() { try { const tilemap = tilemaps.find(tm => tm.id === activeTilemapId); if (!tilemap) { throw new Error('No active tilemap found'); } const mapData = []; for (let y = 0; y < tilemap.gridHeight; y++) { const row = []; for (let x = 0; x < tilemap.gridWidth; x++) { if (tilemap.tileMapGrid[y] && tilemap.tileMapGrid[y][x]) { row.push(tilemap.tileMapGrid[y][x].data.id); } else { row.push(0); } } mapData.push(row); } const exportData = { id: tilemap.id, name: tilemap.name, width: tilemap.gridWidth, height: tilemap.gridHeight, baseTileSize: tilemap.baseTileSize, map: mapData, attributes: tilemap.attributes, // Include attributes in the export timestamp: new Date().toISOString() }; const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `${tilemap.name}.json`; a.click(); URL.revokeObjectURL(url); } catch (error) { console.error('Error exporting tile map:', error); alert('Error exporting tile map: ' + error.message); } } /* ---------- Event Handlers ---------- */ function initTilemapEvents() { try { if (tilemapBtn) { tilemapBtn.addEventListener('click', toggleTilemapMode); } document.addEventListener('keydown', (e) => { if (isTilemapMode) { const tilemap = tilemaps.find(tm => tm.id === activeTilemapId); if (!tilemap) return; if (e.key === 'c' && e.ctrlKey) { e.preventDefault(); clearTileMap(tilemap); } else if (e.key === 's' && e.ctrlKey) { e.preventDefault(); exportTileMap(); } else if (e.key === 'Escape') { disableTilemapMode(); } else if (e.key === 'e' || e.key === 'E') { selectedTile = null; updateTileMapTank(); } } }); } catch (error) { console.error('Failed to initialize tilemap events:', error); alert('Failed to initialize tilemap events: ' + error.message); } } /* ---------- Initialization ---------- */ function initializeTilemap() { try { tilemapBtn = document.getElementById('tilemapBtn'); if (!tilemapBtn) { throw new Error('Tilemap button not found'); } initTilemapEvents(); } catch (error) { console.error('Tilemap module failed to initialize:', error); alert('Tilemap module failed to initialize: ' + error.message); } } /* Export API for other modules */ window.TilemapAPI = { enableTilemapMode, disableTilemapMode, toggleTilemapMode, isTilemapMode: () => isTilemapMode, clearTileMap: () => { const tilemap = tilemaps.find(tm => tm.id === activeTilemapId); if (tilemap) clearTileMap(tilemap); }, exportTileMap, updateTileMapTank, getTilemaps: () => tilemaps // Return the direct reference };