📜
tilepicker.js
Back
📝 Javascript ⚡ Executable Ctrl+S: Save • Ctrl+R: Run • Ctrl+F: Find
// 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'); }