/* ---------- Tile Map Management ---------- */
/* DOM Elements - only declare tilemap-specific elements */
let tilemapBtn;
/* State */
let isTilemapMode = false;
let tileMapGrid = [];
let gridWidth = 32; // Default grid size
let gridHeight = 24;
let baseTileSize = 64; // Base tile size - all tiles scale from this
let selectedTile = null; // Currently selected tile for painting
/* ---------- Tile Map Mode Controls ---------- */
function enableTilemapMode() {
try {
// Get workspace elements from DOM directly
const objectsBar = document.getElementById('objectsBar');
const linesBar = document.getElementById('linesBar');
const tankBar = document.getElementById('tankBar');
const metaEl = document.getElementById('meta');
// Hide workspace elements
if (objectsBar) objectsBar.style.display = 'none';
if (linesBar) linesBar.style.display = 'none';
if (tankBar) tankBar.style.display = 'none';
// Update meta text
if (metaEl) {
metaEl.textContent = 'Tile Map Mode: Click a tile to select it, then click grid squares to place it. Right-click to erase.';
}
// Update button appearance
if (tilemapBtn) {
tilemapBtn.style.background = 'var(--accent)';
tilemapBtn.style.color = '#000';
}
isTilemapMode = true;
// Create blank tile map
createBlankTileMap();
// Show tank for dragging tiles
showTileMapTank();
} catch (error) {
alert('Error enabling tilemap mode: ' + error.message);
}
}
function disableTilemapMode() {
try {
// Get workspace elements from DOM directly
const objectsBar = document.getElementById('objectsBar');
const linesBar = document.getElementById('linesBar');
const tankBar = document.getElementById('tankBar');
const metaEl = document.getElementById('meta');
// Show workspace elements
if (objectsBar) objectsBar.style.display = '';
if (linesBar) linesBar.style.display = '';
if (tankBar) tankBar.style.display = '';
// Restore meta text
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.';
}
}
// Reset button appearance
if (tilemapBtn) {
tilemapBtn.style.background = '';
tilemapBtn.style.color = '';
}
isTilemapMode = false;
// Hide tile map
hideTileMap();
// Hide tile map tank
hideTileMapTank();
} catch (error) {
alert('Error disabling tilemap mode: ' + error.message);
}
}
function toggleTilemapMode() {
if (isTilemapMode) {
disableTilemapMode();
} else {
enableTilemapMode();
}
}
/* ---------- Tile Scaling Utilities ---------- */
function getTileScale(tileData) {
// Calculate how many grid squares this tile should occupy
const scaleX = Math.ceil(tileData.w / baseTileSize);
const scaleY = Math.ceil(tileData.h / baseTileSize);
return { scaleX, scaleY };
}
function canPlaceTileAt(gridX, gridY, scaleX, scaleY) {
// Check if there's room for a tile of this size at this position
if (gridX + scaleX > gridWidth || gridY + scaleY > gridHeight) {
return false; // Doesn't fit in grid
}
// Check if all required squares are empty
for (let y = gridY; y < gridY + scaleY; y++) {
for (let x = gridX; x < gridX + scaleX; x++) {
if (tileMapGrid[y][x]) {
return false; // Square is occupied
}
}
}
return true;
}
function clearTileArea(gridX, gridY, scaleX, scaleY) {
// Clear all squares in the specified area
for (let y = gridY; y < gridY + scaleY; y++) {
for (let x = gridX; x < gridX + scaleX; x++) {
if (tileMapGrid[y][x]) {
removeTileFromGrid(x, y);
}
}
}
}
/* ---------- Div-Based Tile Map Creation ---------- */
let tileMapContainer = null;
let gridCells = []; // Array of div elements for each grid square
function createBlankTileMap() {
try {
// Remove existing tile map if any
if (tileMapContainer) {
tileMapContainer.remove();
}
// Create tile map container
tileMapContainer = document.createElement('div');
tileMapContainer.id = 'tileMapContainer';
tileMapContainer.style.position = 'absolute';
tileMapContainer.style.left = '50px';
tileMapContainer.style.top = '50px';
tileMapContainer.style.width = (gridWidth * baseTileSize) + 'px';
tileMapContainer.style.height = (gridHeight * baseTileSize) + 'px';
tileMapContainer.style.background = '#1a1a1a';
tileMapContainer.style.border = '2px solid #4fc3f7';
tileMapContainer.style.zIndex = '15';
tileMapContainer.style.position = 'relative'; // Important for absolute positioning of children
// Initialize empty grid
tileMapGrid = Array(gridHeight).fill().map(() => Array(gridWidth).fill(null));
gridCells = [];
// Create individual grid cells as divs
for (let y = 0; y < gridHeight; y++) {
gridCells[y] = [];
for (let x = 0; x < gridWidth; x++) {
const cell = document.createElement('div');
cell.className = 'grid-cell';
cell.style.position = 'absolute';
cell.style.left = (x * baseTileSize) + 'px';
cell.style.top = (y * baseTileSize) + 'px';
cell.style.width = baseTileSize + 'px';
cell.style.height = 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';
// Store grid coordinates on the element
cell.dataset.gridX = x;
cell.dataset.gridY = y;
// Add hover effect
cell.addEventListener('mouseenter', () => {
if (selectedTile) {
const { scaleX, scaleY } = getTileScale(selectedTile);
highlightPlacementArea(x, y, scaleX, scaleY);
} else {
cell.style.backgroundColor = 'rgba(255, 68, 68, 0.3)'; // Red for eraser
}
});
cell.addEventListener('mouseleave', () => {
clearHighlights();
});
// Add click handlers
cell.addEventListener('click', handleCellClick);
cell.addEventListener('contextmenu', handleCellRightClick);
tileMapContainer.appendChild(cell);
gridCells[y][x] = cell;
}
}
const viewport = document.getElementById('viewport');
if (viewport) {
viewport.appendChild(tileMapContainer);
} else {
alert('ERROR: Viewport not found!');
return;
}
} catch (error) {
alert('Error creating blank tile map: ' + error.message);
}
}
function highlightPlacementArea(startX, startY, scaleX, scaleY) {
clearHighlights();
const canPlace = canPlaceTileAt(startX, startY, scaleX, scaleY);
const color = canPlace ? 'rgba(79, 195, 247, 0.4)' : 'rgba(255, 68, 68, 0.4)';
for (let y = startY; y < startY + scaleY && y < gridHeight; y++) {
for (let x = startX; x < startX + scaleX && x < gridWidth; x++) {
if (gridCells[y] && gridCells[y][x]) {
gridCells[y][x].style.backgroundColor = color;
}
}
}
}
function clearHighlights() {
for (let y = 0; y < gridHeight; y++) {
for (let x = 0; x < gridWidth; x++) {
if (gridCells[y] && gridCells[y][x] && !tileMapGrid[y][x]) {
gridCells[y][x].style.backgroundColor = 'transparent';
}
}
}
}
function hideTileMap() {
if (tileMapContainer) {
tileMapContainer.style.display = 'none';
}
}
/* ---------- Click Handlers for Grid Cells ---------- */
function handleCellClick(e) {
e.preventDefault();
e.stopPropagation();
const gridX = parseInt(e.target.dataset.gridX);
const gridY = parseInt(e.target.dataset.gridY);
if (selectedTile === null) {
// Eraser mode - remove any tile that occupies this square
removeTileFromGrid(gridX, gridY);
} else {
// Paint selected tile
const { scaleX, scaleY } = getTileScale(selectedTile);
// Check if tile fits at this position
if (canPlaceTileAt(gridX, gridY, scaleX, scaleY)) {
placeTileOnGrid(gridX, gridY, selectedTile);
} else {
// Show message about why it can't be placed
if (gridX + scaleX > gridWidth || gridY + scaleY > 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) {
e.preventDefault();
e.stopPropagation();
const gridX = parseInt(e.target.dataset.gridX);
const gridY = parseInt(e.target.dataset.gridY);
removeTileFromGrid(gridX, gridY);
}
/* ---------- Tile Placement and Removal ---------- */
function placeTileOnGrid(gridX, gridY, tileData) {
try {
const { scaleX, scaleY } = getTileScale(tileData);
// Create tile element that spans multiple grid squares
const tileEl = document.createElement('div');
tileEl.className = 'placed-tile';
tileEl.style.position = 'absolute';
tileEl.style.left = (gridX * baseTileSize) + 'px';
tileEl.style.top = (gridY * baseTileSize) + 'px';
tileEl.style.width = (scaleX * baseTileSize) + 'px';
tileEl.style.height = (scaleY * 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'; // Above grid cells
tileEl.style.pointerEvents = 'none'; // Allow clicks to pass through to grid cells
// Add ID badge
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);
// Add size indicator for multi-square tiles
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);
}
tileMapContainer.appendChild(tileEl);
// Create tile reference object
const tileRef = {
element: tileEl,
data: tileData,
scaleX,
scaleY,
originX: gridX,
originY: gridY
};
// Mark all squares this tile occupies and update their background
for (let y = gridY; y < gridY + scaleY; y++) {
for (let x = gridX; x < gridX + scaleX; x++) {
tileMapGrid[y][x] = tileRef;
// Mark grid cell as occupied
if (gridCells[y] && gridCells[y][x]) {
gridCells[y][x].style.backgroundColor = 'rgba(79, 195, 247, 0.1)';
}
}
}
} catch (error) {
alert('Error placing tile on grid: ' + error.message);
}
}
function removeTileFromGrid(gridX, gridY) {
try {
const tileRef = tileMapGrid[gridY][gridX];
if (tileRef) {
// Remove the visual element
tileRef.element.remove();
// Clear all grid squares this tile occupied and restore their appearance
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 < gridHeight && x >= 0 && x < gridWidth) {
tileMapGrid[y][x] = null;
// Restore grid cell appearance
if (gridCells[y] && gridCells[y][x]) {
gridCells[y][x].style.backgroundColor = 'transparent';
}
}
}
}
}
} catch (error) {
alert('Error removing tile from grid: ' + error.message);
}
}
/* ---------- Grid Resizing with Div Updates ---------- */
function resizeGrid(newWidth, newHeight) {
try {
// Minimum size constraints
if (newWidth < 1 || newHeight < 1) {
alert('Grid size must be at least 1x1');
return;
}
// Maximum size constraints to prevent performance issues
if (newWidth > 100 || newHeight > 100) {
alert('Grid size cannot exceed 100x100');
return;
}
const oldWidth = gridWidth;
const oldHeight = gridHeight;
// Clear existing tiles that would be outside new bounds
if (newWidth < oldWidth || newHeight < oldHeight) {
for (let y = 0; y < oldHeight; y++) {
for (let x = 0; x < oldWidth; x++) {
if (tileMapGrid[y] && tileMapGrid[y][x] && (x >= newWidth || y >= newHeight)) {
removeTileFromGrid(x, y);
}
}
}
}
// Update grid dimensions
gridWidth = newWidth;
gridHeight = newHeight;
// Update container size
if (tileMapContainer) {
tileMapContainer.style.width = (gridWidth * baseTileSize) + 'px';
tileMapContainer.style.height = (gridHeight * baseTileSize) + 'px';
}
// Recreate the entire grid (simpler than trying to add/remove cells)
createBlankTileMap();
// Update display
updateTileMapTank();
} catch (error) {
alert('Error resizing grid: ' + error.message);
}
}
/* ---------- Base Size Control with Grid Rebuild ---------- */
function changeBaseSize(newSize) {
try {
// Validate size (powers of 2 between 8 and 128)
if (newSize < 8 || newSize > 128 || !Number.isInteger(Math.log2(newSize))) {
alert('Base size must be 8, 16, 32, 64, or 128 pixels');
return;
}
// Confirm if there are tiles placed (since it will clear the map)
const hasTiles = tileMapGrid.some(row => row.some(cell => cell !== null));
if (hasTiles) {
if (!confirm(`Changing base size will clear the current tilemap. Continue?`)) {
return;
}
}
// Update base size
baseTileSize = newSize;
// Recreate the grid with new base size
createBlankTileMap();
// Update the tank display
updateTileMapTank();
} catch (error) {
alert('Error changing base size: ' + error.message);
}
}
/* ---------- Tile Map Tank (shows available tiles) ---------- */
let tileMapTank = null;
function showTileMapTank() {
try {
// Create or show tile map tank
if (!tileMapTank) {
tileMapTank = document.createElement('div');
tileMapTank.id = 'tileMapTank';
tileMapTank.style.position = 'fixed';
tileMapTank.style.bottom = '20px';
tileMapTank.style.left = '20px';
tileMapTank.style.right = '20px';
tileMapTank.style.height = '140px';
tileMapTank.style.background = '#161616';
tileMapTank.style.border = '1px solid #2a2a2a';
tileMapTank.style.borderRadius = '.6rem';
tileMapTank.style.padding = '.5rem';
tileMapTank.style.zIndex = '20';
tileMapTank.style.display = 'flex';
tileMapTank.style.flexDirection = 'column';
tileMapTank.style.gap = '.5rem';
document.body.appendChild(tileMapTank);
}
tileMapTank.style.display = 'flex';
updateTileMapTank();
} catch (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 = '';
// Create controls row
const controlsRow = document.createElement('div');
controlsRow.style.display = 'flex';
controlsRow.style.alignItems = 'center';
controlsRow.style.gap = '1rem';
controlsRow.style.marginBottom = '.5rem';
// Grid size controls
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 = '.3rem';
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 .5rem';
baseSizeMinus.style.borderRadius = '.3rem';
baseSizeMinus.style.cursor = 'pointer';
baseSizeMinus.style.fontSize = '.9rem';
baseSizeMinus.disabled = baseTileSize <= 8;
baseSizeMinus.addEventListener('click', () => changeBaseSize(baseTileSize / 2));
const baseSizeDisplay = document.createElement('span');
baseSizeDisplay.textContent = `${baseTileSize}px`;
baseSizeDisplay.style.color = '#eee';
baseSizeDisplay.style.minWidth = '40px';
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 .5rem';
baseSizePlus.style.borderRadius = '.3rem';
baseSizePlus.style.cursor = 'pointer';
baseSizePlus.style.fontSize = '.9rem';
baseSizePlus.disabled = baseTileSize >= 128;
baseSizePlus.addEventListener('click', () => changeBaseSize(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 = '.3rem';
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 .5rem';
widthMinus.style.borderRadius = '.3rem';
widthMinus.style.cursor = 'pointer';
widthMinus.style.fontSize = '.9rem';
widthMinus.addEventListener('click', () => resizeGrid(gridWidth - 1, gridHeight));
const widthDisplay = document.createElement('span');
widthDisplay.textContent = `${gridWidth}`;
widthDisplay.style.color = '#eee';
widthDisplay.style.minWidth = '25px';
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 .5rem';
widthPlus.style.borderRadius = '.3rem';
widthPlus.style.cursor = 'pointer';
widthPlus.style.fontSize = '.9rem';
widthPlus.addEventListener('click', () => resizeGrid(gridWidth + 1, 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 = '.3rem';
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 .5rem';
heightMinus.style.borderRadius = '.3rem';
heightMinus.style.cursor = 'pointer';
heightMinus.style.fontSize = '.9rem';
heightMinus.addEventListener('click', () => resizeGrid(gridWidth, gridHeight - 1));
const heightDisplay = document.createElement('span');
heightDisplay.textContent = `${gridHeight}`;
heightDisplay.style.color = '#eee';
heightDisplay.style.minWidth = '25px';
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 .5rem';
heightPlus.style.borderRadius = '.3rem';
heightPlus.style.cursor = 'pointer';
heightPlus.style.fontSize = '.9rem';
heightPlus.addEventListener('click', () => resizeGrid(gridWidth, gridHeight + 1));
widthControls.appendChild(widthMinus);
widthControls.appendChild(widthDisplay);
widthControls.appendChild(widthPlus);
heightControls.appendChild(heightMinus);
heightControls.appendChild(heightDisplay);
heightControls.appendChild(heightPlus);
controlsRow.appendChild(sizeLabel);
controlsRow.appendChild(baseSizeControls);
controlsRow.appendChild(gridSizeLabel);
controlsRow.appendChild(widthControls);
controlsRow.appendChild(xLabel);
controlsRow.appendChild(heightControls);
// Add clear button
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 .6rem';
clearBtn.style.borderRadius = '.3rem';
clearBtn.style.cursor = 'pointer';
clearBtn.style.fontSize = '.9rem';
clearBtn.style.marginLeft = 'auto';
clearBtn.addEventListener('click', clearTileMap);
controlsRow.appendChild(clearBtn);
tileMapTank.appendChild(controlsRow);
// Create tiles row
const tilesRow = document.createElement('div');
tilesRow.style.display = 'flex';
tilesRow.style.gap = '.5rem';
tilesRow.style.overflowX = 'auto';
tilesRow.style.alignItems = 'center';
tilesRow.style.flex = '1';
// Add title
const title = document.createElement('div');
title.textContent = 'Select:';
title.style.color = '#4fc3f7';
title.style.fontWeight = 'bold';
title.style.marginRight = '.5rem';
title.style.minWidth = 'fit-content';
title.style.fontSize = '.9rem';
tilesRow.appendChild(title);
// Add eraser tool first
const eraser = document.createElement('div');
eraser.className = 'tilemap-source-tile eraser';
eraser.style.position = 'relative';
eraser.style.width = '60px';
eraser.style.height = '60px';
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 = '18px';
eraser.textContent = '๐๏ธ';
eraser.title = 'Click to select eraser';
eraser.addEventListener('click', () => {
selectedTile = null;
updateTileMapTank();
});
tilesRow.appendChild(eraser);
// Get tiles from all objects and lines
if (window.WorkspaceAPI && window.WorkspaceAPI.workspace) {
const workspace = window.WorkspaceAPI.workspace;
workspace.objects.forEach(obj => {
obj.lines.forEach(line => {
line.items.forEach((tile, tileIndex) => {
const { scaleX, scaleY } = getTileScale(tile);
const tileEl = document.createElement('div');
tileEl.className = 'tilemap-source-tile';
tileEl.style.position = 'relative';
tileEl.style.width = '60px';
tileEl.style.height = '60px';
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 .25rem';
badge.style.fontWeight = 'bold';
badge.style.fontSize = '.7rem';
badge.textContent = `#${tile.id}`;
// Add size indicator for multi-square tiles
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 = '.65rem';
sizeIndicator.style.fontWeight = 'bold';
sizeIndicator.textContent = `${scaleX}ร${scaleY}`;
tileEl.appendChild(sizeIndicator);
}
tileEl.appendChild(img);
tileEl.appendChild(badge);
// Store tile data
tileEl.tileData = tile;
tileEl.title = `${tile.w}ร${tile.h}px (${scaleX}ร${scaleY} squares)`;
// Add click handler to select tile
tileEl.addEventListener('click', () => {
selectedTile = tile;
updateTileMapTank(); // Refresh to show selection
});
tilesRow.appendChild(tileEl);
});
});
});
}
tileMapTank.appendChild(tilesRow);
} catch (error) {
alert('Error updating tile map tank: ' + error.message);
}
}
function clearTileMap() {
try {
for (let y = 0; y < gridHeight; y++) {
for (let x = 0; x < gridWidth; x++) {
if (tileMapGrid[y][x]) {
removeTileFromGrid(x, y);
}
}
}
} catch (error) {
alert('Error clearing tile map: ' + error.message);
}
}
/* ---------- Export Functions ---------- */
function exportTileMap() {
try {
const mapData = [];
for (let y = 0; y < gridHeight; y++) {
const row = [];
for (let x = 0; x < gridWidth; x++) {
if (tileMapGrid[y][x]) {
row.push(tileMapGrid[y][x].data.id);
} else {
row.push(0); // Empty cell
}
}
mapData.push(row);
}
const exportData = {
width: gridWidth,
height: gridHeight,
baseTileSize: baseTileSize,
map: mapData,
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.json';
a.click();
URL.revokeObjectURL(url);
} catch (error) {
alert('Error exporting tile map: ' + error.message);
}
}
/* ---------- Event Handlers ---------- */
function initTilemapEvents() {
try {
if (tilemapBtn) {
tilemapBtn.addEventListener('click', toggleTilemapMode);
}
// Keyboard shortcuts
document.addEventListener('keydown', (e) => {
if (isTilemapMode) {
if (e.key === 'c' && e.ctrlKey) {
e.preventDefault();
clearTileMap();
} else if (e.key === 's' && e.ctrlKey) {
e.preventDefault();
exportTileMap();
} else if (e.key === 'Escape') {
disableTilemapMode();
} else if (e.key === 'e' || e.key === 'E') {
// Quick eraser selection
selectedTile = null;
updateTileMapTank();
}
}
});
} catch (error) {
alert('Failed to initialize tilemap events: ' + error.message);
}
}
/* ---------- Initialization ---------- */
function initializeTilemap() {
try {
// Get DOM elements - only the ones this module owns
tilemapBtn = document.getElementById('tilemapBtn');
// Check required DOM elements
if (!tilemapBtn) {
throw new Error('Tilemap button not found');
}
initTilemapEvents();
} catch (error) {
alert('Tilemap module failed to initialize: ' + error.message);
// Don't throw here since tilemap is an optional feature
}
}
// Export API for other modules
window.TilemapAPI = {
enableTilemapMode,
disableTilemapMode,
toggleTilemapMode,
isTilemapMode: () => isTilemapMode,
clearTileMap,
exportTileMap,
updateTileMapTank,
tileMapGrid: () => [...tileMapGrid] // Return copy
};