/* ---------- 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 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][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][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;
}
for (let y = 0; y < tilemap.gridHeight; y++) {
for (let x = 0; x < tilemap.gridWidth; x++) {
if (tilemap.tileMapGrid[y][x]) {
placeTileOnGrid(x, y, tilemap.tileMapGrid[y][x].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 placeTileOnGrid(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);
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)';
}
}
}
} catch (error) {
console.error('Error placing tile on grid:', error);
alert('Error placing tile on grid: ' + error.message);
}
}
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]) {
newGrid[y][x] = tilemap.tileMapGrid[y][x];
}
}
}
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 8, 16, 32, 64, or 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, tileIndex) => {
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][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][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,
attributes: tilemap.attributes, // Include attributes
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.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,
tileMapGrid: () => {
const tilemap = tilemaps.find(tm => tm.id === activeTilemapId);
return tilemap ? [...tilemap.tileMapGrid] : [];
},
getTilemaps: () => {
return tilemaps.map(tm => ({
id: tm.id,
name: tm.name,
gridWidth: tm.gridWidth,
gridHeight: tm.gridHeight,
baseTileSize: tm.baseTileSize,
tileMapGrid: [...tm.tileMapGrid],
attributes: tm.attributes ? [...tm.attributes] : []
}));
},
setTilemapAttributes: (tilemapId, attributes) => {
const tilemap = tilemaps.find(tm => tm.id === tilemapId);
if (tilemap) {
tilemap.attributes = attributes;
console.log(`Updated attributes for tilemap ${tilemapId}:`, tilemap.attributes);
} else {
console.error(`Tilemap with ID ${tilemapId} not found`);
}
}
};