// Debug alert for mobile debugging
if (typeof debugAlert === 'function') {
debugAlert('tilepicker.js loaded');
}
// Global variables for tile picking
let groups = [{ id: 'Group_1', url: null, tiles: [], name: 'Group 1', category: 'None' }];
let currentGroup = 0;
let nextUniqueId = 1; // start at 1 (0 = "no object")
// Category definitions
const TILE_CATEGORIES = [
{ value: 'None', label: 'None', description: 'No specific category' },
{ value: 'Ground', label: 'Ground', description: 'Static collidable terrain (floors, walls)' },
{ value: 'Platform', label: 'Platform', description: 'One-way platform (stand from above, pass through from below)' },
{ value: 'Pushable', label: 'Pushable', description: 'Crates/blocks that can be shoved' },
{ value: 'Passable', label: 'Passable', description: 'Visuals only, no collision (grass, background tiles)' },
{ value: 'Hazard', label: 'Hazard', description: 'Passable but deals damage on touch (spikes, lava)' },
{ value: 'Conveyor', label: 'Conveyor', description: 'Collidable, pushes sideways at a set speed' },
{ value: 'Climbable', label: 'Climbable', description: 'Ladders, ropes, vines' },
{ value: 'Sensor', label: 'Sensor', description: 'Invisible triggers (zone detection, signals)' },
{ value: 'Door', label: 'Door', description: 'Blocks path until triggered/opened' },
{ value: 'SwitchableToggle', label: 'Switchable Toggle', description: 'Flips back & forth; passable (e.g. gate that opens/closes)' },
{ value: 'SwitchableOnce', label: 'Switchable Once', description: 'Changes once to another state; solid (e.g. "?" block → used block)' },
{ value: 'AnimationGround', label: 'Animation Ground', description: 'Like Ground but cycles through frames (e.g. glowing floor)' },
{ value: 'AnimationPassable', label: 'Animation Passable', description: 'Like Passable but cycles through frames (e.g. flickering torch, water)' },
{ value: 'Player', label: 'Player', description: 'The controllable character' },
{ value: 'NPC', label: 'NPC', description: 'AI-driven actors' }
];
/**
* Open the tile picker overlay - main entry point called by files.js
*/
function openTilePickerOverlay() {
const overlayContent = document.getElementById('overlayContent');
if (selectedImage && selectedTileSize) {
overlayContent.innerHTML = `
<h2>Tile Picker 🧩</h2>
<p>Tile size: ${selectedTileSize}px</p>
<div id="groupTabs"></div>
<div id="groupControls"></div>
<div id="pickedImages"></div>
<div id="tileViewport">
<div id="tileContainer">
<img id="tileImage" src="${selectedImage}" alt="${selectedImageName}">
</div>
</div>
`;
// Initialize tile picker functionality
initializeTilePicker();
} else {
overlayContent.innerHTML = `
<h2>Tile Picker 🧩</h2>
<p>Select an image and a numeric folder first.</p>
`;
}
}
/**
* Initialize the tile picker functionality
*/
function initializeTilePicker() {
renderTabs();
renderGroupControls();
renderPicked();
setupTileGrid();
}
/**
* Setup the tile grid overlay on the image
*/
function setupTileGrid() {
const imgEl = document.getElementById('tileImage');
imgEl.onload = () => {
const container = document.getElementById('tileContainer');
const w = imgEl.naturalWidth;
const h = imgEl.naturalHeight;
// Set container and image dimensions
imgEl.style.width = w + "px";
imgEl.style.height = h + "px";
container.style.width = w + "px";
container.style.height = h + "px";
// Remove existing grid cells
container.querySelectorAll('.grid-cell').forEach(c => c.remove());
// Calculate grid dimensions
const cols = Math.floor(w / selectedTileSize);
// Create grid cells
for (let y = 0; y < h; y += selectedTileSize) {
for (let x = 0; x < w; x += selectedTileSize) {
const cell = document.createElement('div');
cell.className = 'grid-cell';
cell.style.cssText = `
position: absolute;
left: ${x}px;
top: ${y}px;
width: ${selectedTileSize}px;
height: ${selectedTileSize}px;
border: 2px solid rgba(102, 204, 255, 0.7);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.3);
color: white;
font-weight: bold;
font-size: 12px;
text-shadow: 1px 1px 2px black;
`;
// Calculate tile index for display
const row = Math.floor(y / selectedTileSize);
const col = Math.floor(x / selectedTileSize);
const tileIndex = row * cols + col + 1;
// Add label to cell
const label = document.createElement('span');
label.textContent = tileIndex;
cell.appendChild(label);
// Add hover effects
cell.addEventListener('mouseenter', () => {
cell.style.background = 'rgba(102, 204, 255, 0.4)';
cell.style.borderColor = '#6cf';
});
cell.addEventListener('mouseleave', () => {
cell.style.background = 'rgba(0, 0, 0, 0.3)';
cell.style.borderColor = 'rgba(102, 204, 255, 0.7)';
});
// Add click handler to pick this tile
cell.onclick = () => pickTile(imgEl, x, y, selectedTileSize, selectedImage);
container.appendChild(cell);
}
}
};
}
/**
* Get category color for visual coding
* @param {string} category - The category value
* @returns {string} CSS color value
*/
function getCategoryColor(category) {
const colors = {
'None': '#666',
'Ground': '#8B4513',
'Platform': '#DEB887',
'Pushable': '#CD853F',
'Passable': '#90EE90',
'Hazard': '#FF4500',
'Conveyor': '#4169E1',
'Climbable': '#228B22',
'Sensor': '#9370DB',
'Door': '#B8860B',
'SwitchableToggle': '#FF69B4',
'SwitchableOnce': '#FF1493',
'AnimationGround': '#FF6347',
'AnimationPassable': '#20B2AA',
'Player': '#FFD700',
'NPC': '#87CEEB'
};
return colors[category] || '#666';
}
/**
* Show category selection dialog when creating a new group
* @param {string} groupName - The name for the new group
*/
function showCategorySelectionDialog(groupName) {
// Create modal overlay
const overlay = document.createElement('div');
overlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
`;
// Create dialog
const dialog = document.createElement('div');
dialog.style.cssText = `
background: #2a2a2a;
border-radius: 8px;
padding: 20px;
max-width: 400px;
width: 90%;
max-height: 80vh;
overflow-y: auto;
`;
// Dialog title
const title = document.createElement('h3');
title.textContent = `Select Category for "${groupName}"`;
title.style.cssText = 'color: #6cf; margin: 0 0 15px 0; text-align: center;';
dialog.appendChild(title);
// Category list
const categoryList = document.createElement('div');
categoryList.style.cssText = 'margin-bottom: 20px;';
TILE_CATEGORIES.forEach(category => {
const categoryOption = document.createElement('div');
categoryOption.style.cssText = `
padding: 10px;
margin: 5px 0;
background: #333;
border-radius: 4px;
cursor: pointer;
border: 2px solid transparent;
transition: all 0.2s;
`;
categoryOption.addEventListener('mouseenter', () => {
categoryOption.style.borderColor = getCategoryColor(category.value);
categoryOption.style.background = '#444';
});
categoryOption.addEventListener('mouseleave', () => {
categoryOption.style.borderColor = 'transparent';
categoryOption.style.background = '#333';
});
const categoryName = document.createElement('div');
categoryName.textContent = category.label;
categoryName.style.cssText = 'font-weight: bold; color: #fff; margin-bottom: 5px;';
const categoryDesc = document.createElement('div');
categoryDesc.textContent = category.description;
categoryDesc.style.cssText = 'font-size: 12px; color: #ccc;';
categoryOption.appendChild(categoryName);
categoryOption.appendChild(categoryDesc);
categoryOption.onclick = () => {
createGroupWithCategory(groupName, category.value);
document.body.removeChild(overlay);
};
categoryList.appendChild(categoryOption);
});
dialog.appendChild(categoryList);
// Cancel button
const cancelBtn = document.createElement('button');
cancelBtn.textContent = 'Cancel';
cancelBtn.style.cssText = `
background: #666;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
width: 100%;
font-size: 14px;
`;
cancelBtn.onclick = () => {
document.body.removeChild(overlay);
};
dialog.appendChild(cancelBtn);
overlay.appendChild(dialog);
document.body.appendChild(overlay);
// Close on overlay click
overlay.onclick = (e) => {
if (e.target === overlay) {
document.body.removeChild(overlay);
}
};
}
/**
* Create a new group with specified name and category
* @param {string} groupName - The name for the new group
* @param {string} category - The category for the new group
*/
function createGroupWithCategory(groupName, category) {
// Create ID from name (replace spaces and special chars with underscores)
const groupId = groupName.replace(/[^a-zA-Z0-9]/g, '_');
groups.push({
id: groupId,
url: null,
tiles: [],
name: groupName,
category: category
});
currentGroup = groups.length - 1;
renderTabs();
renderGroupControls();
renderPicked();
}
/**
* Rename a group
* @param {number} groupIndex - Index of the group to rename
*/
function renameGroup(groupIndex) {
const group = groups[groupIndex];
const currentName = group.name || `Group ${groupIndex + 1}`;
const newName = prompt('Enter new group name:', currentName);
if (newName !== null && newName.trim() !== '') {
group.name = newName.trim();
// Update ID to match name (replace spaces and special chars with underscores)
group.id = newName.trim().replace(/[^a-zA-Z0-9]/g, '_');
renderTabs();
}
}
/**
* Change group category
* @param {number} groupIndex - Index of the group
* @param {string} newCategory - New category value
*/
function changeGroupCategory(groupIndex, newCategory) {
if (groupIndex >= 0 && groupIndex < groups.length) {
groups[groupIndex].category = newCategory;
renderTabs();
renderGroupControls();
}
}
/**
* Render the group controls (category selector, etc.)
*/
function renderGroupControls() {
const controlsContainer = document.getElementById('groupControls');
if (!controlsContainer) return;
controlsContainer.innerHTML = '';
controlsContainer.style.cssText = `
margin-bottom: 10px;
padding: 10px;
background: #333;
border-radius: 6px;
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
`;
const group = groups[currentGroup];
// Category label
const categoryLabel = document.createElement('label');
categoryLabel.textContent = 'Category:';
categoryLabel.style.cssText = 'color: #ccc; font-weight: bold; font-size: 14px;';
controlsContainer.appendChild(categoryLabel);
// Category selector
const categorySelect = document.createElement('select');
categorySelect.style.cssText = `
background: #555;
color: white;
border: 1px solid #777;
border-radius: 4px;
padding: 5px 8px;
font-size: 12px;
min-width: 150px;
`;
TILE_CATEGORIES.forEach(cat => {
const option = document.createElement('option');
option.value = cat.value;
option.textContent = cat.label;
option.title = cat.description;
option.selected = cat.value === group.category;
categorySelect.appendChild(option);
});
categorySelect.onchange = () => {
changeGroupCategory(currentGroup, categorySelect.value);
};
controlsContainer.appendChild(categorySelect);
// Category description
const currentCategory = TILE_CATEGORIES.find(cat => cat.value === group.category);
if (currentCategory) {
const description = document.createElement('span');
description.textContent = currentCategory.description;
description.style.cssText = 'color: #aaa; font-size: 12px; font-style: italic;';
controlsContainer.appendChild(description);
}
}
/**
* Render the group tabs
*/
function renderTabs() {
const tabBar = document.getElementById('groupTabs');
if (!tabBar) return;
tabBar.innerHTML = '';
tabBar.style.cssText = 'margin-bottom: 10px; display: flex; gap: 5px; align-items: center; flex-wrap: wrap;';
// Render existing group tabs
groups.forEach((g, idx) => {
const btn = document.createElement('button');
const groupName = g.name || `Group ${idx + 1}`;
const categoryColor = getCategoryColor(g.category);
btn.textContent = groupName;
btn.style.cssText = `
background: ${idx === currentGroup ? '#6cf' : '#555'};
color: ${idx === currentGroup ? '#000' : '#fff'};
border: 3px solid ${categoryColor};
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
position: relative;
max-width: 120px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
`;
// Single click to switch groups
btn.onclick = () => {
currentGroup = idx;
renderTabs();
renderGroupControls();
renderPicked();
};
// Double click to rename group
btn.ondblclick = (e) => {
e.stopPropagation();
renameGroup(idx);
};
// Add tooltip showing full name, category, and instructions
const categoryLabel = TILE_CATEGORIES.find(cat => cat.value === g.category)?.label || 'None';
btn.title = `${groupName}\nCategory: ${categoryLabel}\nDouble-click to rename`;
// Add tile count badge
if (g.tiles.length > 0) {
const badge = document.createElement('span');
badge.textContent = g.tiles.length;
badge.style.cssText = `
position: absolute;
top: -5px;
right: -5px;
background: #f44;
color: white;
border-radius: 50%;
width: 16px;
height: 16px;
font-size: 9px;
display: flex;
align-items: center;
justify-content: center;
`;
btn.appendChild(badge);
}
// Add category indicator
const categoryIndicator = document.createElement('div');
categoryIndicator.style.cssText = `
position: absolute;
bottom: -2px;
left: 50%;
transform: translateX(-50%);
width: 80%;
height: 3px;
background: ${categoryColor};
border-radius: 2px;
`;
btn.appendChild(categoryIndicator);
tabBar.appendChild(btn);
});
// Add "+" button to create new group
const addBtn = document.createElement('button');
addBtn.textContent = "+";
addBtn.style.cssText = `
background: #4a4;
color: white;
border: none;
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
font-weight: bold;
`;
addBtn.onclick = () => {
// Prompt for group name when creating
const groupName = prompt('Enter name for new group:', `Group ${groups.length + 1}`);
if (groupName !== null && groupName.trim() !== '') {
// Show category selection dialog
showCategorySelectionDialog(groupName.trim());
}
};
addBtn.title = "Create new group";
tabBar.appendChild(addBtn);
// Add clear group button
if (groups[currentGroup] && groups[currentGroup].tiles.length > 0) {
const clearBtn = document.createElement('button');
clearBtn.textContent = "Clear";
clearBtn.style.cssText = `
background: #d44;
color: white;
border: none;
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
margin-left: 10px;
`;
clearBtn.onclick = () => {
if (confirm('Clear all tiles from this group?')) {
clearCurrentGroup();
}
};
clearBtn.title = "Clear all tiles from current group";
tabBar.appendChild(clearBtn);
}
}
/**
* Render the picked tiles for the current group
*/
function renderPicked() {
const container = document.getElementById('pickedImages');
if (!container) return;
container.innerHTML = '';
container.style.cssText = `
margin-bottom: 15px;
padding: 10px;
background: #2a2a2a;
border-radius: 6px;
min-height: 80px;
max-height: 200px;
overflow-y: auto;
`;
const group = groups[currentGroup];
if (group.tiles.length === 0) {
container.innerHTML = '<div style="color: #888; text-align: center; padding: 20px;">No tiles picked yet. Click on the grid below to select tiles.</div>';
return;
}
// Create tiles container
const tilesContainer = document.createElement('div');
tilesContainer.style.cssText = 'display: flex; flex-wrap: wrap; gap: 8px;';
group.tiles.forEach((tile, idx) => {
const wrapper = document.createElement('div');
wrapper.className = 'pickedTile';
const categoryColor = getCategoryColor(group.category);
wrapper.style.cssText = `
position: relative;
display: flex;
flex-direction: column;
align-items: center;
padding: 5px;
background: #333;
border-radius: 4px;
border: 2px solid ${categoryColor};
`;
// Create canvas to display the tile
const canvas = document.createElement('canvas');
canvas.width = Math.min(tile.size, 64);
canvas.height = Math.min(tile.size, 64);
canvas.style.cssText = 'border: 1px solid #666; background: #000;';
const ctx = canvas.getContext('2d');
// Create temporary canvas for the original tile
const tempCanvas = document.createElement('canvas');
tempCanvas.width = tile.size;
tempCanvas.height = tile.size;
const tempCtx = tempCanvas.getContext('2d');
tempCtx.putImageData(tile.data, 0, 0);
// Scale down if needed
ctx.drawImage(tempCanvas, 0, 0, tile.size, tile.size, 0, 0, canvas.width, canvas.height);
// Create remove button
const removeBtn = document.createElement('button');
removeBtn.className = 'removeBtn';
removeBtn.textContent = "×";
removeBtn.style.cssText = `
position: absolute;
top: -5px;
right: -5px;
background: #f44;
color: white;
border: none;
border-radius: 50%;
width: 20px;
height: 20px;
cursor: pointer;
font-size: 12px;
display: flex;
align-items: center;
justify-content: center;
`;
removeBtn.onclick = () => {
group.tiles.splice(idx, 1);
renderPicked();
};
// Create ID label
const idLabel = document.createElement('span');
idLabel.textContent = `ID ${tile.uniqueId}`;
idLabel.style.cssText = `
font-size: 10px;
color: #ccc;
margin-top: 4px;
text-align: center;
`;
wrapper.appendChild(canvas);
wrapper.appendChild(removeBtn);
wrapper.appendChild(idLabel);
tilesContainer.appendChild(wrapper);
});
container.appendChild(tilesContainer);
}
/**
* Pick a tile from the image at specified coordinates
* @param {HTMLImageElement} imgEl - The source image element
* @param {number} x - X coordinate of the tile
* @param {number} y - Y coordinate of the tile
* @param {number} size - Size of the tile (width and height)
* @param {string} url - URL of the source image
*/
function pickTile(imgEl, x, y, size, url) {
const group = groups[currentGroup];
// Check if group already uses a different image
if (group.url && group.url !== url) {
alert("This group already uses a different image. Create a new group or switch to an existing group that uses this image.");
return;
}
// Check if this exact tile has already been picked
const existingTile = group.tiles.find(tile =>
tile.sourceX === x && tile.sourceY === y && tile.sourceUrl === url
);
if (existingTile) {
alert(`This tile is already picked (ID ${existingTile.uniqueId})`);
return;
}
// Set the group's image URL
group.url = url;
// Give the group a default name based on the image if it doesn't have one
if (!group.name) {
const imageName = url.split('/').pop().split('.')[0];
group.name = `${imageName}_${size}px`;
}
// Extract the tile data using canvas
const canvas = document.createElement('canvas');
canvas.width = size;
canvas.height = size;
const ctx = canvas.getContext('2d');
// Draw the tile portion of the image
ctx.drawImage(imgEl, x, y, size, size, 0, 0, size, size);
// Get the image data
const data = ctx.getImageData(0, 0, size, size);
// Add tile to current group
group.tiles.push({
size,
data,
uniqueId: nextUniqueId++,
sourceX: x,
sourceY: y,
sourceUrl: url
});
// Re-render the picked tiles display and tabs
renderPicked();
renderTabs();
// Visual feedback
const cell = document.querySelector(`[style*="left: ${x}px"][style*="top: ${y}px"]`);
if (cell) {
cell.style.background = 'rgba(68, 255, 68, 0.6)';
setTimeout(() => {
cell.style.background = 'rgba(0, 0, 0, 0.3)';
}, 500);
}
}
/**
* Get the current group data
* @returns {Object} Current group object
*/
function getCurrentGroup() {
return groups[currentGroup];
}
/**
* Get all groups data
* @returns {Array} Array of all group objects
*/
function getAllGroups() {
return groups;
}
/**
* Set the current group
* @param {number} groupIndex - Index of the group to set as current
*/
function setCurrentGroup(groupIndex) {
if (groupIndex >= 0 && groupIndex < groups.length) {
currentGroup = groupIndex;
renderTabs();
renderGroupControls();
renderPicked();
}
}
/**
* Clear all tiles from the current group
*/
function clearCurrentGroup() {
const group = groups[currentGroup];
group.tiles = [];
group.url = null;
// Keep the custom name and category when clearing
renderTabs();
renderPicked();
}
/**
* Remove a group by index
* @param {number} groupIndex - Index of the group to remove
*/
function removeGroup(groupIndex) {
if (groups.length > 1 && groupIndex >= 0 && groupIndex < groups.length) {
groups.splice(groupIndex, 1);
// Adjust current group if necessary
if (currentGroup >= groups.length) {
currentGroup = groups.length - 1;
}
renderTabs();
renderGroupControls();
renderPicked();
}
}
// Debug alert for mobile debugging - success
if (typeof debugAlert === 'function') {
debugAlert('tilepicker.js loaded successfully');
}