// gameObject.js — Game Object Manager UI
// - Reads sprite data from cutout.js localStorage
// - Displays sprites, groups, and images in a management interface
// - Allows creating and organizing game objects from sprites
(function () {
const CUTOUT_KEY = 'tile_holders_v4';
// ---------- Data Manager ----------
const DataManager = {
sprites: [],
groups: [],
images: [],
loadFromCutout() {
try {
const raw = localStorage.getItem(CUTOUT_KEY);
if (!raw) {
this.sprites = [];
this.groups = [];
this.images = [];
return false;
}
const data = JSON.parse(raw);
this.sprites = [];
this.groups = [];
this.images = [];
// Process each image
for (const [imageUrl, imageData] of Object.entries(data.images || {})) {
const { tileSize, groups = [] } = imageData;
// Store image info
this.images.push({
url: imageUrl,
name: imageUrl.split('/').pop() || 'image',
tileSize,
groupCount: groups.length,
spriteCount: groups.reduce((sum, g) => sum + (g.tiles?.length || 0), 0)
});
// Process groups and sprites
for (const group of groups) {
const groupData = {
id: group.id,
name: group.name,
imageUrl,
imageName: imageUrl.split('/').pop() || 'image',
tileSize,
sprites: []
};
// Process sprites in group
for (const tile of group.tiles || []) {
const sprite = {
id: tile.spriteId,
number: tile.spriteNumber,
col: tile.col,
row: tile.row,
imageUrl,
imageName: imageUrl.split('/').pop() || 'image',
groupId: group.id,
groupName: group.name,
tileSize,
x: tile.col * tileSize,
y: tile.row * tileSize,
width: tileSize,
height: tileSize
};
this.sprites.push(sprite);
groupData.sprites.push(sprite);
}
this.groups.push(groupData);
}
}
return true;
} catch (error) {
console.error('[gameObject] Failed to load cutout data:', error);
return false;
}
},
getSpriteById(id) {
return this.sprites.find(s => s.id === id) || null;
},
getSpritesByGroup(groupId) {
return this.sprites.filter(s => s.groupId === groupId);
},
getStats() {
return {
totalSprites: this.sprites.length,
totalGroups: this.groups.length,
totalImages: this.images.length
};
}
};
// ---------- UI State ----------
let currentView = 'overview'; // overview, sprites, groups, images
let selectedSprites = new Set();
// ---------- Render Functions ----------
function renderOverview() {
const stats = DataManager.getStats();
return `
<div style="padding:1rem;">
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:1rem;margin-bottom:1rem;">
<div style="padding:1rem;background:rgba(30,41,59,0.6);border-radius:.5rem;border:1px solid rgba(71,85,105,0.4);text-align:center;">
<div style="font-size:2rem;font-weight:bold;color:#3b82f6;">${stats.totalSprites}</div>
<div style="color:#94a3b8;font-size:0.875rem;">Total Sprites</div>
</div>
<div style="padding:1rem;background:rgba(30,41,59,0.6);border-radius:.5rem;border:1px solid rgba(71,85,105,0.4);text-align:center;">
<div style="font-size:2rem;font-weight:bold;color:#10b981;">${stats.totalGroups}</div>
<div style="color:#94a3b8;font-size:0.875rem;">Groups</div>
</div>
<div style="padding:1rem;background:rgba(30,41,59,0.6);border-radius:.5rem;border:1px solid rgba(71,85,105,0.4);text-align:center;">
<div style="font-size:2rem;font-weight:bold;color:#f59e0b;">${stats.totalImages}</div>
<div style="color:#94a3b8;font-size:0.875rem;">Images</div>
</div>
</div>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:.5rem;">
<button class="view-btn" data-view="sprites" style="padding:1rem;background:rgba(59,130,246,0.2);border:1px solid rgba(59,130,246,0.4);border-radius:.5rem;color:#3b82f6;cursor:pointer;transition:all 0.2s;">
🎮 View Sprites
</button>
<button class="view-btn" data-view="groups" style="padding:1rem;background:rgba(16,185,129,0.2);border:1px solid rgba(16,185,129,0.4);border-radius:.5rem;color:#10b981;cursor:pointer;transition:all 0.2s;">
📁 View Groups
</button>
<button class="view-btn" data-view="images" style="padding:1rem;background:rgba(245,158,11,0.2);border:1px solid rgba(245,158,11,0.4);border-radius:.5rem;color:#f59e0b;cursor:pointer;transition:all 0.2s;">
🖼️ View Images
</button>
</div>
${stats.totalSprites === 0 ? `
<div style="margin-top:2rem;padding:1rem;border:1px dashed rgba(71,85,105,0.5);border-radius:.5rem;text-align:center;color:#94a3b8;">
No sprites found. Use the Cut tool to create sprites first.
</div>
` : ''}
</div>
`;
}
function renderSprites() {
if (DataManager.sprites.length === 0) {
return `
<div style="padding:1rem;text-align:center;color:#94a3b8;">
No sprites available. Create some in the Cut tool first.
</div>
`;
}
return `
<div style="padding:1rem;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;">
<button class="back-btn" data-view="overview" style="padding:.5rem 1rem;background:rgba(71,85,105,0.4);border:1px solid rgba(71,85,105,0.6);border-radius:.375rem;color:#e2e8f0;cursor:pointer;">
← Back
</button>
<div style="color:#94a3b8;font-size:0.875rem;">${DataManager.sprites.length} sprites</div>
</div>
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:.5rem;max-height:60vh;overflow-y:auto;">
${DataManager.sprites.map(sprite => `
<div class="sprite-card" data-sprite-id="${sprite.id}" style="padding:.5rem;background:rgba(30,41,59,0.6);border:1px solid rgba(71,85,105,0.4);border-radius:.375rem;cursor:pointer;text-align:center;">
<div style="width:60px;height:60px;background:#0a0f1c;border-radius:.25rem;margin:0 auto .375rem;border:1px solid rgba(71,85,105,0.4);display:flex;align-items:center;justify-content:center;position:relative;overflow:hidden;">
<div class="sprite-thumbnail" data-sprite-id="${sprite.id}" style="width:100%;height:100%;background-size:cover;background-position:center;image-rendering:pixelated;">
<div style="font-size:10px;color:#64748b;">…</div>
</div>
</div>
<div style="color:#e2e8f0;font-size:0.8rem;font-weight:600;">#${sprite.number}</div>
<div style="color:#94a3b8;font-size:0.75rem;">ID:${sprite.id}</div>
<div style="color:#64748b;font-size:0.7rem;">${sprite.groupName}</div>
</div>
`).join('')}
</div>
</div>
`;
}
function renderGroups() {
if (DataManager.groups.length === 0) {
return `
<div style="padding:1rem;text-align:center;color:#94a3b8;">
No groups available.
</div>
`;
}
return `
<div style="padding:1rem;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;">
<button class="back-btn" data-view="overview" style="padding:.5rem 1rem;background:rgba(71,85,105,0.4);border:1px solid rgba(71,85,105,0.6);border-radius:.375rem;color:#e2e8f0;cursor:pointer;">
← Back
</button>
<div style="color:#94a3b8;font-size:0.875rem;">${DataManager.groups.length} groups</div>
</div>
<div style="display:flex;flex-direction:column;gap:.5rem;max-height:60vh;overflow-y:auto;">
${DataManager.groups.map(group => `
<div style="padding:1rem;background:rgba(30,41,59,0.6);border:1px solid rgba(71,85,105,0.4);border-radius:.5rem;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:.5rem;">
<div>
<div style="color:#e2e8f0;font-weight:600;margin-bottom:.25rem;">${group.name}</div>
<div style="color:#94a3b8;font-size:0.875rem;">${group.imageName} • ${group.sprites.length} sprites • ${group.tileSize}px tiles</div>
</div>
</div>
<div style="display:flex;gap:.375rem;overflow-x:auto;padding:.25rem 0;">
${group.sprites.slice(0, 10).map(sprite => `
<div style="flex:0 0 auto;width:40px;height:40px;background:#0a0f1c;border-radius:.25rem;border:1px solid rgba(71,85,105,0.4);display:flex;align-items:center;justify-content:center;position:relative;overflow:hidden;">
<div class="sprite-thumbnail" data-sprite-id="${sprite.id}" style="width:100%;height:100%;background-size:cover;background-position:center;image-rendering:pixelated;">
<div style="font-size:8px;color:#64748b;">#${sprite.number}</div>
</div>
</div>
`).join('')}
${group.sprites.length > 10 ? `
<div style="flex:0 0 auto;width:40px;height:40px;background:rgba(71,85,105,0.4);border-radius:.25rem;display:flex;align-items:center;justify-content:center;color:#94a3b8;font-size:0.75rem;">
+${group.sprites.length - 10}
</div>
` : ''}
</div>
</div>
`).join('')}
</div>
</div>
`;
}
function renderImages() {
if (DataManager.images.length === 0) {
return `
<div style="padding:1rem;text-align:center;color:#94a3b8;">
No images available.
</div>
`;
}
return `
<div style="padding:1rem;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;">
<button class="back-btn" data-view="overview" style="padding:.5rem 1rem;background:rgba(71,85,105,0.4);border:1px solid rgba(71,85,105,0.6);border-radius:.375rem;color:#e2e8f0;cursor:pointer;">
← Back
</button>
<div style="color:#94a3b8;font-size:0.875rem;">${DataManager.images.length} images</div>
</div>
<div style="display:flex;flex-direction:column;gap:.5rem;max-height:60vh;overflow-y:auto;">
${DataManager.images.map(image => `
<div style="padding:1rem;background:rgba(30,41,59,0.6);border:1px solid rgba(71,85,105,0.4);border-radius:.5rem;">
<div style="display:flex;gap:1rem;align-items:center;">
<div style="width:80px;height:80px;background:#0a0f1c;border-radius:.375rem;border:1px solid rgba(71,85,105,0.4);overflow:hidden;flex-shrink:0;">
<img src="${image.url}" alt="" style="width:100%;height:100%;object-fit:cover;" loading="lazy">
</div>
<div style="flex:1;">
<div style="color:#e2e8f0;font-weight:600;margin-bottom:.25rem;">${image.name}</div>
<div style="color:#94a3b8;font-size:0.875rem;margin-bottom:.375rem;">
${image.spriteCount} sprites in ${image.groupCount} groups • ${image.tileSize}px tiles
</div>
<div style="display:flex;gap:.5rem;">
<div style="padding:.25rem .5rem;background:rgba(59,130,246,0.2);border:1px solid rgba(59,130,246,0.4);border-radius:.25rem;color:#3b82f6;font-size:0.75rem;">
${image.spriteCount} sprites
</div>
<div style="padding:.25rem .5rem;background:rgba(16,185,129,0.2);border:1px solid rgba(16,185,129,0.4);border-radius:.25rem;color:#10b981;font-size:0.75rem;">
${image.groupCount} groups
</div>
</div>
</div>
</div>
</div>
`).join('')}
</div>
</div>
`;
}
// ---------- Sprite Thumbnails ----------
function generateSpriteThumbnails() {
document.querySelectorAll('.sprite-thumbnail[data-sprite-id]').forEach(thumbEl => {
const spriteId = parseInt(thumbEl.getAttribute('data-sprite-id'));
const sprite = DataManager.getSpriteById(spriteId);
if (!sprite) return;
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = () => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const thumbSize = 60;
canvas.width = thumbSize;
canvas.height = thumbSize;
ctx.imageSmoothingEnabled = false;
ctx.drawImage(img, sprite.x, sprite.y, sprite.width, sprite.height, 0, 0, thumbSize, thumbSize);
thumbEl.style.backgroundImage = `url(${canvas.toDataURL()})`;
thumbEl.innerHTML = '';
};
img.onerror = () => {
thumbEl.innerHTML = '<div style="font-size:10px;color:#64748b;">?</div>';
};
img.src = sprite.imageUrl;
});
}
// ---------- Update Content ----------
function updateContent() {
const container = document.getElementById('gameobject-content');
if (!container) return;
DataManager.loadFromCutout();
let content = '';
switch (currentView) {
case 'sprites': content = renderSprites(); break;
case 'groups': content = renderGroups(); break;
case 'images': content = renderImages(); break;
default: content = renderOverview(); break;
}
container.innerHTML = content;
wireUpControls();
// Generate thumbnails after DOM update
setTimeout(() => generateSpriteThumbnails(), 100);
}
function wireUpControls() {
// View navigation
document.querySelectorAll('.view-btn, .back-btn').forEach(btn => {
btn.addEventListener('click', () => {
const view = btn.getAttribute('data-view');
if (view) {
currentView = view;
updateContent();
}
});
});
// Sprite card interactions
document.querySelectorAll('.sprite-card').forEach(card => {
card.addEventListener('click', () => {
const spriteId = parseInt(card.getAttribute('data-sprite-id'));
const sprite = DataManager.getSpriteById(spriteId);
if (sprite) {
console.log('Selected sprite:', sprite);
// Could open detailed view or add to selection
}
});
});
}
// ---------- Initial Mount ----------
const initialHtml = `
<div id="gameobject-content" style="height:100%;overflow-y:auto;">
<div style="padding:1rem;text-align:center;color:#94a3b8;">Loading game objects...</div>
</div>
`;
window.AppItems = window.AppItems || [];
window.AppItems.push({
title: '🎮 Objects',
html: initialHtml,
onRender() {
// Always refresh data when the overlay is opened
setTimeout(() => updateContent(), 0);
}
});
// Listen for cutout data changes
window.addEventListener('storage', (e) => {
if (e.key === CUTOUT_KEY) {
updateContent();
}
});
})();