// Debug alert for mobile debugging
if (typeof debugAlert === 'function') debugAlert('tilepicker.js loaded');
// ===== Shared state from files.js =====
// selectedImage, selectedImageName, selectedTileSize are globals on window
// We’ll use them directly.
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' }
];
/** Main entry point (called by files.js via openOverlay('tiles')) */
function openTilePickerOverlay() {
const overlayContent = document.getElementById('overlayContent');
const img = window.selectedImage;
const name = window.selectedImageName;
const tileSize = window.selectedTileSize;
if (img && tileSize) {
overlayContent.innerHTML = `
<h2>Tile Picker 🧩</h2>
<p>Tile size: ${tileSize}px</p>
<div id="groupTabs"></div>
<div id="groupControls"></div>
<div id="pickedImages"></div>
<div id="tileViewport">
<div id="tileContainer" style="position:relative; display:inline-block;">
<img id="tileImage" src="${img}" alt="${name}">
</div>
</div>
`;
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();
}
/** Build the grid overlay */
function setupTileGrid() {
const imgEl = document.getElementById('tileImage');
if (!imgEl) return;
imgEl.onload = () => {
const container = document.getElementById('tileContainer');
const w = imgEl.naturalWidth;
const h = imgEl.naturalHeight;
const size = window.selectedTileSize;
// Dimensions
imgEl.style.width = w + "px";
imgEl.style.height = h + "px";
container.style.width = w + "px";
container.style.height = h + "px";
// Clear prior cells
container.querySelectorAll('.grid-cell').forEach(c => c.remove());
const cols = Math.floor(w / size);
for (let y = 0; y < h; y += size) {
for (let x = 0; x < w; x += size) {
const cell = document.createElement('div');
cell.className = 'grid-cell';
cell.style.cssText = `
position: absolute;
left: ${x}px;
top: ${y}px;
width: ${size}px;
height: ${size}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;
box-sizing: border-box;
`;
const row = Math.floor(y / size);
const col = Math.floor(x / size);
const tileIndex = row * cols + col + 1;
const label = document.createElement('span');
label.textContent = tileIndex;
cell.appendChild(label);
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)';
});
cell.onclick = () => pickTile(imgEl, x, y, size, window.selectedImage);
container.appendChild(cell);
}
}
};
// If the image is cached, onload may not fire; force a tick
if (imgEl.complete && imgEl.naturalWidth) {
const src = imgEl.src;
imgEl.src = '';
imgEl.src = src;
}
}
/** Color helper by category */
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';
}
/** Modal to choose category for a new group */
function showCategorySelectionDialog(groupName) {
const overlay = document.createElement('div');
overlay.style.cssText = `
position: fixed; inset: 0; background: rgba(0,0,0,0.8);
display: flex; align-items: center; justify-content: center; z-index: 10000;
`;
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;
`;
const title = document.createElement('h3');
title.textContent = `Select Category for "${groupName}"`;
title.style.cssText = 'color:#6cf;margin:0 0 15px;text-align:center;';
dialog.appendChild(title);
const list = document.createElement('div');
list.style.cssText = 'margin-bottom: 20px;';
TILE_CATEGORIES.forEach(category => {
const item = document.createElement('div');
item.style.cssText = `
padding:10px;margin:5px 0;background:#333;border-radius:4px;cursor:pointer;
border:2px solid transparent;transition:all .2s;
`;
item.addEventListener('mouseenter', () => {
item.style.borderColor = getCategoryColor(category.value);
item.style.background = '#444';
});
item.addEventListener('mouseleave', () => {
item.style.borderColor = 'transparent';
item.style.background = '#333';
});
const name = document.createElement('div');
name.textContent = category.label;
name.style.cssText = 'font-weight:bold;color:#fff;margin-bottom:5px;';
const desc = document.createElement('div');
desc.textContent = category.description;
desc.style.cssText = 'font-size:12px;color:#ccc;';
item.appendChild(name);
item.appendChild(desc);
item.onclick = () => {
createGroupWithCategory(groupName, category.value);
document.body.removeChild(overlay);
};
list.appendChild(item);
});
dialog.appendChild(list);
const cancel = document.createElement('button');
cancel.textContent = 'Cancel';
cancel.style.cssText = `
background:#666;color:#fff;border:none;padding:8px 16px;border-radius:4px;
cursor:pointer;width:100%;font-size:14px;
`;
cancel.onclick = () => document.body.removeChild(overlay);
dialog.appendChild(cancel);
overlay.appendChild(dialog);
document.body.appendChild(overlay);
overlay.onclick = (e) => { if (e.target === overlay) document.body.removeChild(overlay); };
}
/** Create a new group with a category */
function createGroupWithCategory(groupName, category) {
const groupId = groupName.replace(/[^a-zA-Z0-9]/g, '_');
groups.push({ id: groupId, url: null, tiles: [], name: groupName, category });
currentGroup = groups.length - 1;
renderTabs();
renderGroupControls();
renderPicked();
}
/** Rename a group */
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();
group.id = newName.trim().replace(/[^a-zA-Z0-9]/g, '_');
renderTabs();
}
}
/** Change group category */
function changeGroupCategory(groupIndex, newCategory) {
if (groupIndex >= 0 && groupIndex < groups.length) {
groups[groupIndex].category = newCategory;
renderTabs();
renderGroupControls();
}
}
/** Group controls (category etc.) */
function renderGroupControls() {
const controls = document.getElementById('groupControls');
if (!controls) return;
controls.innerHTML = '';
controls.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];
const label = document.createElement('label');
label.textContent = 'Category:';
label.style.cssText = 'color:#ccc;font-weight:bold;font-size:14px;';
controls.appendChild(label);
const sel = document.createElement('select');
sel.style.cssText = `
background:#555;color:#fff;border:1px solid #777;border-radius:4px;
padding:5px 8px;font-size:12px;min-width:150px;
`;
TILE_CATEGORIES.forEach(cat => {
const opt = document.createElement('option');
opt.value = cat.value;
opt.textContent = cat.label;
opt.title = cat.description;
opt.selected = (cat.value === group.category);
sel.appendChild(opt);
});
sel.onchange = () => changeGroupCategory(currentGroup, sel.value);
controls.appendChild(sel);
const currentCategory = TILE_CATEGORIES.find(c => c.value === group.category);
if (currentCategory) {
const desc = document.createElement('span');
desc.textContent = currentCategory.description;
desc.style.cssText = 'color:#aaa;font-size:12px;font-style:italic;';
controls.appendChild(desc);
}
}
/** 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;';
groups.forEach((g, idx) => {
const btn = document.createElement('button');
const name = g.name || `Group ${idx + 1}`;
const color = getCategoryColor(g.category);
btn.textContent = name;
btn.style.cssText = `
background:${idx === currentGroup ? '#6cf' : '#555'};
color:${idx === currentGroup ? '#000' : '#fff'};
border:3px solid ${color};
padding:6px 12px;border-radius:4px;cursor:pointer;font-size:12px;
position:relative;max-width:140px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
`;
btn.onclick = () => { currentGroup = idx; renderTabs(); renderGroupControls(); renderPicked(); };
btn.ondblclick = (e) => { e.stopPropagation(); renameGroup(idx); };
if (g.tiles.length) {
const badge = document.createElement('span');
badge.textContent = g.tiles.length;
badge.style.cssText = `
position:absolute;top:-5px;right:-5px;background:#f44;color:#fff;border-radius:50%;
width:16px;height:16px;font-size:9px;display:flex;align-items:center;justify-content:center;
`;
btn.appendChild(badge);
}
const indicator = document.createElement('div');
indicator.style.cssText = `
position:absolute;bottom:-2px;left:50%;transform:translateX(-50%);
width:80%;height:3px;background:${color};border-radius:2px;
`;
btn.appendChild(indicator);
tabBar.appendChild(btn);
});
const addBtn = document.createElement('button');
addBtn.textContent = '+';
addBtn.style.cssText = `
background:#4a4;color:#fff;border:none;padding:6px 12px;border-radius:4px;
cursor:pointer;font-size:14px;font-weight:bold;
`;
addBtn.onclick = () => {
const name = prompt('Enter name for new group:', `Group ${groups.length + 1}`);
if (name && name.trim()) showCategorySelectionDialog(name.trim());
};
addBtn.title = 'Create new group';
tabBar.appendChild(addBtn);
if (groups[currentGroup] && groups[currentGroup].tiles.length) {
const clearBtn = document.createElement('button');
clearBtn.textContent = 'Clear';
clearBtn.style.cssText = `
background:#d44;color:#fff;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);
}
}
/** Picked tiles panel */
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) {
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;
}
const wrap = document.createElement('div');
wrap.style.cssText = 'display:flex;flex-wrap:wrap;gap:8px;';
group.tiles.forEach((tile, idx) => {
const card = document.createElement('div');
const color = getCategoryColor(group.category);
card.style.cssText = `
position:relative;display:flex;flex-direction:column;align-items:center;
padding:5px;background:#333;border-radius:4px;border:2px solid ${color};
`;
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');
const temp = document.createElement('canvas');
temp.width = tile.size;
temp.height = tile.size;
const tctx = temp.getContext('2d');
tctx.putImageData(tile.data, 0, 0);
ctx.drawImage(temp, 0, 0, tile.size, tile.size, 0, 0, canvas.width, canvas.height);
const rm = document.createElement('button');
rm.textContent = '×';
rm.style.cssText = `
position:absolute;top:-5px;right:-5px;background:#f44;color:#fff;border:none;
border-radius:50%;width:20px;height:20px;cursor:pointer;font-size:12px;
display:flex;align-items:center;justify-content:center;
`;
rm.onclick = () => { group.tiles.splice(idx, 1); renderPicked(); };
const id = document.createElement('span');
id.textContent = `ID ${tile.uniqueId}`;
id.style.cssText = 'font-size:10px;color:#ccc;margin-top:4px;text-align:center;';
card.appendChild(canvas);
card.appendChild(rm);
card.appendChild(id);
wrap.appendChild(card);
});
container.appendChild(wrap);
}
/** Extract a tile at (x,y) */
function pickTile(imgEl, x, y, size, url) {
const group = groups[currentGroup];
// Lock a group to a single spritesheet
if (group.url && group.url !== url) {
alert('This group already uses a different image. Create a new group for another sheet.');
return;
}
// Prevent duplicate picks of same cell
const duplicate = group.tiles.find(t => t.sourceX === x && t.sourceY === y && t.sourceUrl === url);
if (duplicate) {
alert(`This tile is already picked (ID ${duplicate.uniqueId})`);
return;
}
group.url = url;
if (!group.name) {
const base = (url.split('/').pop() || '').split('.')[0] || 'Sheet';
group.name = `${base}_${size}px`;
}
const canvas = document.createElement('canvas');
canvas.width = size;
canvas.height = size;
const ctx = canvas.getContext('2d');
ctx.drawImage(imgEl, x, y, size, size, 0, 0, size, size);
const data = ctx.getImageData(0, 0, size, size);
group.tiles.push({
size,
data,
uniqueId: nextUniqueId++,
sourceX: x,
sourceY: y,
sourceUrl: url
});
renderPicked();
renderTabs();
// Flash feedback on picked cell
const cells = document.querySelectorAll('.grid-cell');
cells.forEach(c => {
// find the one matching this x/y by style match
const style = c.getAttribute('style') || '';
if (style.includes(`left: ${x}px`) && style.includes(`top: ${y}px`)) {
c.style.background = 'rgba(68,255,68,.6)';
setTimeout(() => { c.style.background = 'rgba(0,0,0,.3)'; }, 500);
}
});
}
/** Helpers exposed (optional) */
function getCurrentGroup() { return groups[currentGroup]; }
function getAllGroups() { return groups; }
function setCurrentGroup(idx) {
if (idx >= 0 && idx < groups.length) {
currentGroup = idx;
renderTabs(); renderGroupControls(); renderPicked();
}
}
function clearCurrentGroup() {
const group = groups[currentGroup];
group.tiles = [];
group.url = null; // keep name/category
renderTabs(); renderPicked();
}
function removeGroup(idx) {
if (groups.length > 1 && idx >= 0 && idx < groups.length) {
groups.splice(idx, 1);
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');