/* ---------- Object Overlay Management ---------- */
/* DOM Elements */
let attributesObjectViewBtn, objectOverlay, ovTitle, ovBody, closeOverlayBtn;
/* ---------- Attribute Row Creation ---------- */
function makeAttrRow(scope, scopeId, idx, attr) {
const row = document.createElement('div');
row.className = 'attr-row';
row.style.position = 'relative';
row.style.zIndex = '1010'; // Above overlay content but below autocomplete
const keyInput = document.createElement('input');
keyInput.type = 'text';
keyInput.placeholder = 'Attribute name';
keyInput.className = 'attr-key';
keyInput.value = attr.key ?? '';
keyInput.dataset.scope = scope;
keyInput.dataset.scopeId = scopeId;
keyInput.dataset.index = String(idx);
if (scope === 'tilemap' && ['id'].includes(attr.key)) {
keyInput.disabled = true; // Disable for automatic attributes
}
const valueContainer = document.createElement('div');
valueContainer.style.position = 'relative';
valueContainer.style.flex = '1';
const valInput = document.createElement('input');
valInput.type = 'text';
valInput.placeholder = 'Value';
valInput.className = 'attr-val';
valInput.value = attr.value ?? '';
valInput.dataset.scope = scope;
valInput.dataset.scopeId = scopeId;
valInput.dataset.index = String(idx);
if (scope === 'tilemap' && ['id'].includes(attr.key)) {
valInput.disabled = true; // Disable for automatic attributes
}
// Create autocomplete dropdown
const autocompleteDropdown = document.createElement('div');
autocompleteDropdown.style.position = 'absolute';
autocompleteDropdown.style.top = '100%';
autocompleteDropdown.style.left = '0';
autocompleteDropdown.style.right = '0';
autocompleteDropdown.style.background = '#1e1e1e';
autocompleteDropdown.style.border = '1px solid #333';
autocompleteDropdown.style.borderTop = 'none';
autocompleteDropdown.style.borderRadius = '0 0 .35rem .35rem';
autocompleteDropdown.style.maxHeight = '150px';
autocompleteDropdown.style.overflowY = 'auto';
autocompleteDropdown.style.display = 'none';
autocompleteDropdown.style.zIndex = '1020'; // Above all overlay content
// Value options for different attribute types
const valueOptions = {
// Object and Line attributes
randomizeOnPlace: ['false', 'true'],
shuffleFrames: ['false', 'true'],
immovable: ['false', 'true'],
rotationLock: ['false', 'true'],
float: ['false', 'true'],
collides: ['false', 'true'],
playMode: ['loop', 'once', 'pingpong'],
'physics.bodyType': ['static', 'dynamic', 'kinematic'],
blockType: ['ground', 'platform', 'wall', 'ladder', 'water', 'lava', 'decor'],
license: ['CC0', 'CC-BY', 'custom'],
randomFlip: ['none', 'x', 'y', 'xy'],
trigger: ['onTouch', 'onOverlap', 'onClick'],
frameRate: ['10', '12', '15', '24', '30', '60'],
damage: ['0', '1', '2', '5', '10'],
health: ['1', '3', '5', '10', '100'],
speed: ['50', '100', '150', '200', '300'],
jumpStrength: ['200', '300', '400', '500'],
bounce: ['0', '0.2', '0.5', '0.8', '1'],
friction: ['0', '0.1', '0.5', '0.8', '1'],
yoyoChance: ['0', '0.1', '0.25', '0.5', '0.75', '1'],
'origin.x': ['0', '0.5', '1'],
'origin.y': ['0', '0.5', '1'],
jitterX: ['[0,0]', '[-5,5]', '[-10,10]', '[-20,20]'],
jitterY: ['[0,0]', '[-5,5]', '[-10,10]', '[-20,20]'],
jitterRot: ['[0,0]', '[-15,15]', '[-45,45]', '[-90,90]'],
jitterScale: ['[1,1]', '[0.8,1.2]', '[0.5,1.5]', '[0.5,2]'],
randomPick: ['[]', '[1]', '[1,2,3]', '[{"weight":1,"value":"A"}]'],
paletteRandom: ['[]', '["0xff0000","0x00ff00","0x0000ff"]'],
magnet: ['{"strength":0,"radius":0}', '{"strength":100,"radius":50}'],
patrol: ['{"range":100,"speed":50}', '{"range":200,"speed":100}'],
respawn: ['{"time":3}', '{"time":1}', '{"time":5}'],
collectible: ['{"value":1}', '{"value":5}', '{"value":10}'],
tags: ['', 'enemy', 'hazard', 'collectible', 'enemy,flying', 'hazard,spikes'],
// Tilemap attributes
orientation: ['isometric', 'orthographic'],
defaultDepthOrder: ['y-then-x', 'explicit'],
gameType: ['arcade', 'rpg', 'puzzle', 'sandbox'],
weather: ['clear', 'rain', 'snow', 'fog'],
'lighting.enabled': ['false', 'true'],
virtualPad: ['false', 'true'],
pauseAllowed: ['false', 'true'],
dither: ['false', 'true'],
'chunking.enabled': ['false', 'true'],
'nav.type': ['grid', 'navmesh'],
shadowMode: ['none', 'blob', 'drop', 'dynamic'],
'camera.zoom': ['0.5', '1', '1.5', '2'],
parallax: ['0', '0.5', '1', '2'],
'lighting.falloff': ['0', '0.5', '1', '2'],
'weather.intensity': ['0', '0.25', '0.5', '0.75', '1'],
'music.volume': ['0', '0.25', '0.5', '0.75', '1'],
'sfx.volume': ['0', '0.25', '0.5', '0.75', '1'],
timeOfDay: ['0', '6', '12', '18', '24'],
timeLimit: ['0', '30', '60', '120', '300'],
'camera.bounds': ['{x:0,y:0,w:800,h:600}', '{x:0,y:0,w:1600,h:1200}'],
'camera.follow': ['', 'player', 'enemy', 'tag:follow'],
gravity: ['{x:0,y:0}', '{x:0,y:600}', '{x:100,y:0}'],
airDrag: ['0', '0.1', '0.5', '1'],
collisionTileTags: ['[]', '["wall","platform"]', '["ground","wall"]'],
win: ['{collect:{tag:"coin",count:10}}', '{collect:{tag:"gem",count:5}}'],
lose: ['{touch:{tag:"hazard"}}', '{touch:{tag:"enemy"}}'],
checkpoints: ['[]', '[{id:"spawn1",x:0,y:0}]'],
startingSpawn: ['', 'spawn1', '{x:0,y:0}'],
'lighting.ambient': ['#1a1a1a', '#333333', '#555555'],
'music.url': ['', 'assets/music/bgm1.mp3', 'assets/music/bgm2.mp3'],
reverbPreset: ['', 'smallRoom', 'largeHall', 'cathedral'],
'nav.gridCost': ['{water:5,mud:2}', '{grass:1,water:10}'],
'nav.blockTags': ['[]', '["wall","hazard"]'],
fog: ['{color:"#000000",start:0,end:100}', '{color:"#333333",start:50,end:200}'],
hud: ['[]', '["health","coins"]', '["timer"]'],
chunkSize: ['{w:16,h:16}', '{w:32,h:32}'],
preloadRadius: ['0', '1', '2', '3'],
atlasHint: ['[]', '["spritesheet1","spritesheet2"]'],
antiZFightBias: ['0', '0.001', '0.01']
};
function showAutocomplete(attributeKey, currentValue = '') {
const options = valueOptions[attributeKey];
if (!options || options.length === 0) {
autocompleteDropdown.style.display = 'none';
return;
}
autocompleteDropdown.innerHTML = '';
options.forEach(option => {
const optionDiv = document.createElement('div');
optionDiv.style.padding = '.3rem .5rem';
optionDiv.style.cursor = 'pointer';
optionDiv.style.borderBottom = '1px solid #2a2a2a';
optionDiv.textContent = option;
if (option === currentValue) {
optionDiv.style.background = '#444';
optionDiv.style.color = '#4fc3f7';
}
optionDiv.onmouseover = () => {
optionDiv.style.background = '#333';
};
optionDiv.onmouseout = () => {
optionDiv.style.background = option === currentValue ? '#444' : 'transparent';
};
optionDiv.onclick = () => {
valInput.value = option;
autocompleteDropdown.style.display = 'none';
valInput.focus();
valInput.dispatchEvent(new Event('input'));
};
autocompleteDropdown.appendChild(optionDiv);
});
autocompleteDropdown.style.display = 'block';
}
function hideAutocomplete() {
setTimeout(() => {
autocompleteDropdown.style.display = 'none';
}, 150);
}
valInput.onfocus = () => {
const attributeKey = keyInput.value;
showAutocomplete(attributeKey, valInput.value);
};
valInput.oninput = () => {
const attributeKey = keyInput.value;
showAutocomplete(attributeKey, valInput.value);
};
valInput.onblur = hideAutocomplete;
valInput.onkeydown = (e) => {
if (autocompleteDropdown.style.display === 'none') return;
const options = autocompleteDropdown.children;
const currentSelected = Array.from(options).findIndex(opt =>
opt.style.background === 'rgb(51, 51, 51)'
);
if (e.key === 'ArrowDown') {
e.preventDefault();
const nextIndex = Math.min(currentSelected + 1, options.length - 1);
Array.from(options).forEach((opt, i) => {
opt.style.background = i === nextIndex ? '#333' : 'transparent';
});
} else if (e.key === 'ArrowUp') {
e.preventDefault();
const prevIndex = Math.max(currentSelected - 1, 0);
Array.from(options).forEach((opt, i) => {
opt.style.background = i === prevIndex ? '#333' : 'transparent';
});
} else if (e.key === 'Enter') {
e.preventDefault();
if (currentSelected >= 0 && options[currentSelected]) {
options[currentSelected].click();
}
} else if (e.key === 'Escape') {
autocompleteDropdown.style.display = 'none';
}
};
const delBtn = document.createElement('button');
delBtn.textContent = '🗑️';
delBtn.title = 'Remove attribute';
delBtn.className = 'attr-del';
delBtn.dataset.scope = scope;
delBtn.dataset.scopeId = scopeId;
delBtn.dataset.index = String(idx);
delBtn.style.position = 'relative';
delBtn.style.zIndex = '1010'; // Ensure delete button is above other content
if (scope === 'tilemap' && ['id'].includes(attr.key)) {
delBtn.style.display = 'none'; // Hide delete for automatic attributes
}
valueContainer.appendChild(valInput);
valueContainer.appendChild(autocompleteDropdown);
row.appendChild(keyInput);
row.appendChild(valueContainer);
row.appendChild(delBtn);
return row;
}
/* ---------- Object Attributes Block ---------- */
function buildObjectAttributesBlock(obj) {
const wrap = document.createElement('div');
wrap.className = 'ov-obj-attrs';
wrap.style.background = '#151515';
wrap.style.border = '1px solid #2a2a2a';
wrap.style.borderRadius = '.6rem';
wrap.style.padding = '.6rem';
wrap.style.marginBottom = '.8rem';
wrap.style.position = 'relative';
wrap.style.zIndex = '1010'; // Above tile picker
const head = document.createElement('div');
head.style.display = 'flex';
head.style.flexDirection = 'column'; // Column layout
head.style.gap = '.3rem';
head.style.position = 'relative';
head.style.zIndex = '1010'; // Ensure header is above other content
const title = document.createElement('h3');
title.textContent = 'Object Attributes';
title.style.margin = '.1rem 0';
title.style.color = '#ddd';
const inputContainer = document.createElement('div');
inputContainer.style.display = 'flex';
inputContainer.style.gap = '.6rem';
inputContainer.style.alignItems = 'center';
const addKey = document.createElement('input');
addKey.type = 'text';
addKey.placeholder = 'Attribute name';
addKey.id = 'ov-add-obj-key';
const addVal = document.createElement('input');
addVal.type = 'text';
addVal.placeholder = 'Value';
addVal.id = 'ov-add-obj-val';
const addBtn = document.createElement('button');
addBtn.textContent = '➕ Add';
addBtn.className = 'attr-add';
addBtn.dataset.scope = 'object';
addBtn.dataset.scopeId = obj.id;
addBtn.style.position = 'relative';
addBtn.style.zIndex = '1010'; // Ensure add button is visible
inputContainer.appendChild(addKey);
inputContainer.appendChild(addVal);
head.appendChild(title);
head.appendChild(inputContainer);
head.appendChild(addBtn); // Add button below inputs
const list = document.createElement('div');
list.className = 'attr-list';
list.dataset.scope = 'object';
list.dataset.scopeId = obj.id;
for (let i = 0; i < (obj.attributes?.length || 0); i++) {
list.appendChild(makeAttrRow('object', obj.id, i, obj.attributes[i]));
}
wrap.appendChild(head);
wrap.appendChild(list);
return wrap;
}
/* ---------- Line Block with Collapsible Sections ---------- */
function buildLineBlock(line) {
const block = document.createElement('div');
block.className = 'ov-line';
block.style.position = 'relative';
block.style.zIndex = '1010'; // Above tile picker
const h = document.createElement('h3');
h.textContent = line.name;
function createCollapsibleSection(title, icon, content) {
const section = document.createElement('div');
section.style.border = '1px solid #2a2a2a';
section.style.borderRadius = '.4rem';
section.style.marginBottom = '.6rem';
section.style.overflow = 'visible';
section.style.position = 'relative';
section.style.zIndex = '1010'; // Above tile picker
const header = document.createElement('div');
header.style.background = '#1a1a1a';
header.style.padding = '.5rem .6rem';
header.style.cursor = 'pointer';
header.style.display = 'flex';
header.style.alignItems = 'center';
header.style.gap = '.5rem';
header.style.borderBottom = 'none';
const arrow = document.createElement('span');
arrow.textContent = '▶';
arrow.style.fontSize = '.9rem';
arrow.style.color = '#4fc3f7';
arrow.style.transition = 'transform 0.2s ease';
const titleSpan = document.createElement('span');
titleSpan.textContent = `${icon} ${title}`;
titleSpan.style.color = '#4fc3f7';
titleSpan.style.fontWeight = 'bold';
const contentDiv = document.createElement('div');
contentDiv.style.background = '#151515';
contentDiv.style.maxHeight = '0px';
contentDiv.style.overflow = 'hidden';
contentDiv.style.transition = 'max-height 0.3s ease, padding 0.3s ease';
contentDiv.style.padding = '0 .6rem';
header.onclick = () => {
const isExpanded = contentDiv.style.maxHeight !== '0px';
if (isExpanded) {
contentDiv.style.maxHeight = '0px';
contentDiv.style.padding = '0 .6rem';
arrow.textContent = '▶';
arrow.style.transform = 'rotate(0deg)';
header.style.borderBottom = 'none';
} else {
contentDiv.style.maxHeight = '2000px';
contentDiv.style.padding = '.6rem';
arrow.textContent = '▼';
arrow.style.transform = 'rotate(90deg)';
header.style.borderBottom = '1px solid #2a2a2a';
}
};
header.appendChild(arrow);
header.appendChild(titleSpan);
contentDiv.appendChild(content);
section.appendChild(header);
section.appendChild(contentDiv);
return section;
}
const automaticAttrs = ['id', 'url', 'imgWidth', 'imgHeight', 'tileWidth', 'tileHeight', 'rows', 'cols', 'index', 'frames', 'frameCount', 'centerX', 'centerY', 'atlasKey', 'frameKey', 'type'];
const changeableAttrs = ['frameRate', 'playMode', 'origin.x', 'origin.y', 'physics.bodyType', 'collides', 'tags', 'blockType', 'license'];
const automatic = line.attributes.filter(attr => automaticAttrs.includes(attr.key));
const changeable = line.attributes.filter(attr => changeableAttrs.includes(attr.key));
const optional = line.attributes.filter(attr => !automaticAttrs.includes(attr.key) && !changeableAttrs.includes(attr.key));
const autoContent = document.createElement('div');
automatic.forEach(attr => {
const row = document.createElement('div');
row.style.display = 'flex';
row.style.gap = '.4rem';
row.style.margin = '.2rem 0';
row.style.alignItems = 'center';
const keySpan = document.createElement('span');
keySpan.style.flex = '0 0 120px';
keySpan.style.fontSize = '.85rem';
keySpan.style.color = '#bbb';
keySpan.textContent = attr.key;
const valueSpan = document.createElement('span');
valueSpan.style.flex = '1';
valueSpan.style.fontSize = '.85rem';
valueSpan.style.color = '#ddd';
valueSpan.style.background = '#1a1a1a';
valueSpan.style.padding = '.2rem .4rem';
valueSpan.style.borderRadius = '.3rem';
valueSpan.textContent = attr.value;
row.appendChild(keySpan);
row.appendChild(valueSpan);
autoContent.appendChild(row);
});
const changeContent = document.createElement('div');
changeable.forEach((attr, i) => {
changeContent.appendChild(makeAttrRow('line', line.id, line.attributes.indexOf(attr), attr));
});
const addChangeableDiv = document.createElement('div');
addChangeableDiv.style.display = 'flex';
addChangeableDiv.style.flexDirection = 'column'; // Column layout
addChangeableDiv.style.gap = '.3rem';
addChangeableDiv.style.margin = '.3rem 0';
addChangeableDiv.style.position = 'relative';
addChangeableDiv.style.zIndex = '1010'; // Ensure add button is visible
const changeableSelect = document.createElement('select');
changeableSelect.style.background = '#1e1e1e';
changeableSelect.style.border = '1px solid #333';
changeableSelect.style.color = '#eee';
changeableSelect.style.borderRadius = '.3rem';
changeableSelect.style.padding = '.2rem';
const changeableOptions = [
'frameRate', 'playMode', 'origin.x', 'origin.y', 'physics.bodyType',
'collides', 'tags', 'blockType', 'license'
];
changeableOptions.forEach(opt => {
if (!changeable.find(attr => attr.key === opt)) {
const option = document.createElement('option');
option.value = opt;
option.textContent = opt;
changeableSelect.appendChild(option);
}
});
const addChangeableBtn = document.createElement('button');
addChangeableBtn.textContent = '+ Add';
addChangeableBtn.className = 'attr-add';
addChangeableBtn.style.position = 'relative';
addChangeableBtn.style.zIndex = '1010'; // Ensure add button is visible
addChangeableBtn.onclick = () => {
const selectedKey = changeableSelect.value;
if (selectedKey && !line.attributes.find(attr => attr.key === selectedKey)) {
const defaultValues = {
frameRate: '10', playMode: 'loop', 'origin.x': '0.5', 'origin.y': '0.5',
'physics.bodyType': 'static', collides: 'false', tags: '', blockType: 'decor', license: 'CC0'
};
line.attributes.push({ key: selectedKey, value: defaultValues[selectedKey] || '' });
window.AttributesAPI.renderObjectOverlay();
}
};
if (changeableSelect.children.length > 0) {
addChangeableDiv.appendChild(changeableSelect);
addChangeableDiv.appendChild(addChangeableBtn); // Add button below select
changeContent.appendChild(addChangeableDiv);
}
const optionalContent = document.createElement('div');
optional.forEach((attr, i) => {
optionalContent.appendChild(makeAttrRow('line', line.id, line.attributes.indexOf(attr), attr));
});
const customAttrDiv = document.createElement('div');
customAttrDiv.style.display = 'flex';
customAttrDiv.style.flexDirection = 'column'; // Column layout
customAttrDiv.style.gap = '.3rem'; // Fixed typo
customAttrDiv.style.margin = '.3rem 0';
customAttrDiv.style.position = 'relative';
customAttrDiv.style.zIndex = '1010'; // Ensure add button is visible
const presetSelect = document.createElement('select');
presetSelect.style.background = '#1e1e1e';
presetSelect.style.border = '1px solid #333';
presetSelect.style.color = '#eee';
presetSelect.style.borderRadius = '.3rem';
presetSelect.style.padding = '.3rem';
presetSelect.style.fontSize = '.9rem';
const optionalCategories = {
'🎲 Randomization': [
'randomizeOnPlace', 'shuffleFrames', 'randomPick', 'jitterX', 'jitterY',
'jitterRot', 'jitterScale', 'randomFlip', 'paletteRandom', 'yoyoChance', 'seed'
],
'⚙️ Physics Extensions': [
'gravityY', 'gravityX', 'dragX', 'dragY', 'maxVelocityX', 'maxVelocityY',
'bounce', 'friction', 'immovable', 'angularVelocity', 'rotationLock',
'magnet', 'float'
],
'🎮 Gameplay': [
'damage', 'health', 'speed', 'jumpStrength', 'trigger', 'script',
'patrol', 'respawn', 'collectible'
],
'📝 Meta': [
'name', 'author', 'notes', 'group', 'layer'
]
};
const defaultOption = document.createElement('option');
defaultOption.value = '';
defaultOption.textContent = 'Select preset attribute...';
presetSelect.appendChild(defaultOption);
Object.entries(optionalCategories).forEach(([category, attrs]) => {
const optgroup = document.createElement('optgroup');
optgroup.label = category;
attrs.forEach(attr => {
if (!optional.find(a => a.key === attr)) {
const option = document.createElement('option');
option.value = attr;
option.textContent = attr;
optgroup.appendChild(option);
}
});
if (optgroup.children.length > 0) {
presetSelect.appendChild(optgroup);
}
});
const inputRow = document.createElement('div');
inputRow.style.display = 'flex';
inputRow.style.gap = '.4rem';
inputRow.style.position = 'relative';
inputRow.style.zIndex = '1010'; // Ensure content is visible
const customKeyInput = document.createElement('input');
customKeyInput.type = 'text';
customKeyInput.placeholder = 'Custom attribute name';
customKeyInput.className = 'ov-add-line-key';
customKeyInput.dataset.lineId = line.id;
const customValInput = document.createElement('input');
customValInput.type = 'text';
customValInput.placeholder = 'Value';
customValInput.className = 'ov-add-line-val';
customValInput.dataset.lineId = line.id;
const addCustomBtn = document.createElement('button');
addCustomBtn.textContent = '+ Add';
addCustomBtn.className = 'attr-add';
addCustomBtn.dataset.scope = 'line';
addCustomBtn.dataset.scopeId = line.id;
addCustomBtn.style.position = 'relative';
addCustomBtn.style.zIndex = '1010'; // Ensure add button is visible
const defaultValues = {
randomizeOnPlace: 'false', shuffleFrames: 'false', randomPick: '[]',
jitterX: '[0,0]', jitterY: '[0,0]', jitterRot: '[0,0]', jitterScale: '[1,1]',
randomFlip: 'none', paletteRandom: '[]', yoyoChance: '0', seed: '12345',
gravityY: '0', gravityX: '0', dragX: '0', dragY: '0',
maxVelocityX: '1000', maxVelocityY: '1000', bounce: '0', friction: '0',
immovable: 'false', angularVelocity: '0', rotationLock: 'false',
magnet: '{"strength":0,"radius":0}', float: 'false',
damage: '0', health: '1', speed: '100', jumpStrength: '300',
trigger: 'onTouch', script: '', patrol: '{"range":100,"speed":50}',
respawn: '{"time":3}', collectible: '{"value":1}',
name: '', author: '', notes: '', group: '', layer: ''
};
presetSelect.onchange = () => {
if (presetSelect.value) {
customKeyInput.value = presetSelect.value;
customValInput.value = defaultValues[presetSelect.value] || '';
customValInput.focus();
}
};
inputRow.appendChild(customKeyInput);
inputRow.appendChild(customValInput);
customAttrDiv.appendChild(presetSelect);
customAttrDiv.appendChild(inputRow);
customAttrDiv.appendChild(addCustomBtn); // Add button below inputs
optionalContent.appendChild(customAttrDiv);
// Tiles content
const tilesContent = document.createElement('div');
const tileInspector = document.createElement('div');
tileInspector.style.marginBottom = '1rem';
tileInspector.style.padding = '.5rem';
tileInspector.style.background = '#1a1a1a';
tileInspector.style.borderRadius = '.4rem';
tileInspector.style.border = '1px solid #333';
const inspectorTitle = document.createElement('div');
inspectorTitle.style.color = '#4fc3f7';
inspectorTitle.style.fontWeight = 'bold';
inspectorTitle.style.marginBottom = '.5rem';
inspectorTitle.textContent = 'Tile Inspector';
const inspectorContent = document.createElement('div');
inspectorContent.id = `tile-inspector-${line.id}`;
inspectorContent.style.color = '#bbb';
inspectorContent.style.fontSize = '.9rem';
inspectorContent.textContent = 'Click on a tile below to see detailed information';
tileInspector.appendChild(inspectorTitle);
tileInspector.appendChild(inspectorContent);
tilesContent.appendChild(tileInspector);
const strip = document.createElement('div');
strip.className = 'ov-strip';
for (const t of (line.items || [])) {
const item = document.createElement('div');
item.className = 'ov-item';
item.title = `Click to inspect: ID ${t.id}, Index: ${t.index || 'N/A'}`;
item.style.cursor = 'pointer';
item.onclick = () => {
const inspector = document.getElementById(`tile-inspector-${line.id}`);
if (inspector) {
inspector.innerHTML = `
<div style="display: grid; grid-template-columns: 120px 1fr; gap: .3rem; font-size: .85rem;">
<div style="color: #4fc3f7; font-weight: bold;">Tile ID:</div>
<div style="color: #fff;">${t.id}</div>
<div style="color: #4fc3f7; font-weight: bold;">Grid Index:</div>
<div style="color: #fff;">${t.index !== undefined ? t.index : 'N/A'}</div>
<div style="color: #4fc3f7; font-weight: bold;">Row:</div>
<div style="color: #fff;">${t.row}</div>
<div style="color: #4fc3f7; font-weight: bold;">Column:</div>
<div style="color: #fff;">${t.col}</div>
<div style="color: #4fc3f7; font-weight: bold;">Position:</div>
<div style="color: #fff;">(${t.x}, ${t.y})</div>
<div style="color: #4fc3f7; font-weight: bold;">Size:</div>
<div style="color: #fff;">${t.w} × ${t.h}</div>
<div style="color: #4fc3f7; font-weight: bold;">Center:</div>
<div style="color: #fff;">(${t.centerX || 'N/A'}, ${t.centerY || 'N/A'})</div>
<div style="color: #4fc3f7; font-weight: bold;">Frame Key:</div>
<div style="color: #fff;">${t.frameKey || 'N/A'}</div>
<div style="color: #4fc3f7; font-weight: bold;">Atlas Key:</div>
<div style="color: #fff;">${t.atlasKey || 'N/A'}</div>
<div style="color: #4fc3f7; font-weight: bold;">Type:</div>
<div style="color: #fff;">${t.type || 'static'}</div>
</div>
`;
}
};
const img = document.createElement('img');
img.src = t.thumbDataURL;
const badge = document.createElement('div');
badge.className = 'ov-badge';
badge.textContent = `#${t.id}`;
badge.style.background = '#4fc3f7';
badge.style.color = '#000';
badge.style.fontWeight = 'bold';
item.appendChild(img);
item.appendChild(badge);
strip.appendChild(item);
}
tilesContent.appendChild(strip);
// Tilemaps content
const tilemapsContent = document.createElement('div');
// Define tilemap attribute categories once
const automaticTilemapAttrs = ['id'];
const changeableTilemapAttrs = [
'orientation', 'defaultDepthOrder', 'gameType', 'weather', 'lighting.enabled',
'virtualPad', 'pauseAllowed', 'dither', 'chunking.enabled', 'nav.type', 'shadowMode'
];
const optionalTilemapAttrs = [
'name', 'tileWidth', 'tileHeight', 'gridWidth', 'gridHeight', 'backgroundColor', 'seed',
'camera.bounds', 'camera.zoom', 'camera.follow', 'parallax', 'pixelArt',
'gravity', 'airDrag', 'collisionTileTags', 'timeLimit', 'win', 'lose', 'checkpoints',
'startingSpawn', 'lighting.ambient', 'lighting.falloff', 'timeOfDay', 'weather.intensity',
'music.url', 'music.volume', 'sfx.volume', 'reverbPreset', 'nav.gridCost', 'nav.blockTags',
'fog', 'hud', 'chunkSize', 'preloadRadius', 'atlasHint', 'antiZFightBias', 'author', 'version', 'license', 'notes'
];
if (window.TilemapAPI && window.TilemapAPI.getTilemaps) {
const tilemaps = window.TilemapAPI.getTilemaps();
if (tilemaps.length > 0) {
tilemaps.forEach((tilemap, index) => {
const tilemapDiv = document.createElement('div');
tilemapDiv.style.marginBottom = '1rem';
tilemapDiv.style.padding = '.5rem';
tilemapDiv.style.background = '#1a1a1a';
tilemapDiv.style.borderRadius = '.4rem';
tilemapDiv.style.border = '1px solid #333';
tilemapDiv.style.position = 'relative';
tilemapDiv.style.zIndex = '1010'; // Above tile picker
const tilemapTitle = document.createElement('div');
tilemapTitle.style.color = '#4fc3f7';
tilemapTitle.style.fontWeight = 'bold';
tilemapTitle.style.marginBottom = '.5rem';
tilemapTitle.textContent = `Tilemap: ${tilemap.name || `Tilemap ${index + 1}`}`;
// Initialize tilemap attributes if not present
tilemap.attributes = tilemap.attributes || [];
if (!tilemap.attributes.find(attr => attr.key === 'id')) {
tilemap.attributes.push({ key: 'id', value: tilemap.id });
}
if (!tilemap.attributes.find(attr => attr.key === 'name')) {
tilemap.attributes.push({ key: 'name', value: tilemap.name });
}
if (!tilemap.attributes.find(attr => attr.key === 'tileWidth')) {
tilemap.attributes.push({ key: 'tileWidth', value: String(tilemap.baseTileSize) });
}
if (!tilemap.attributes.find(attr => attr.key === 'gridWidth')) {
tilemap.attributes.push({ key: 'gridWidth', value: String(tilemap.gridWidth) });
}
if (!tilemap.attributes.find(attr => attr.key === 'gridHeight')) {
tilemap.attributes.push({ key: 'gridHeight', value: String(tilemap.gridHeight) });
}
// Automatic attributes
const autoTilemapContent = document.createElement('div');
const autoTilemapAttrsFiltered = tilemap.attributes.filter(attr => automaticTilemapAttrs.includes(attr.key));
autoTilemapAttrsFiltered.forEach(attr => {
const row = document.createElement('div');
row.style.display = 'flex';
row.style.gap = '.4rem';
row.style.margin = '.2rem 0';
row.style.alignItems = 'center';
const keySpan = document.createElement('span');
keySpan.style.flex = '0 0 120px';
keySpan.style.fontSize = '.85rem';
keySpan.style.color = '#bbb';
keySpan.textContent = attr.key;
const valueSpan = document.createElement('span');
valueSpan.style.flex = '1';
valueSpan.style.fontSize = '.85rem';
valueSpan.style.color = '#ddd';
valueSpan.style.background = '#1a1a1a';
valueSpan.style.padding = '.2rem .4rem';
valueSpan.style.borderRadius = '.3rem';
valueSpan.textContent = attr.value;
row.appendChild(keySpan);
row.appendChild(valueSpan);
autoTilemapContent.appendChild(row);
});
// Changeable attributes
const changeTilemapContent = document.createElement('div');
const changeTilemapAttrsFiltered = tilemap.attributes.filter(attr => changeableTilemapAttrs.includes(attr.key));
changeTilemapAttrsFiltered.forEach((attr, i) => {
changeTilemapContent.appendChild(makeAttrRow('tilemap', tilemap.id, tilemap.attributes.indexOf(attr), attr));
});
const addChangeableTilemapDiv = document.createElement('div');
addChangeableTilemapDiv.style.display = 'flex';
addChangeableTilemapDiv.style.flexDirection = 'column'; // Column layout
addChangeableTilemapDiv.style.gap = '.3rem';
addChangeableTilemapDiv.style.margin = '.3rem 0';
addChangeableTilemapDiv.style.position = 'relative';
addChangeableTilemapDiv.style.zIndex = '1010'; // Ensure add button is visible
const changeableTilemapSelect = document.createElement('select');
changeableTilemapSelect.style.background = '#1e1e1e';
changeableTilemapSelect.style.border = '1px solid #333';
changeableTilemapSelect.style.color = '#eee';
changeableTilemapSelect.style.borderRadius = '.3rem';
changeableTilemapSelect.style.padding = '.2rem';
changeableTilemapAttrs.forEach(opt => {
if (!changeTilemapAttrsFiltered.find(attr => attr.key === opt)) {
const option = document.createElement('option');
option.value = opt;
option.textContent = opt;
changeableTilemapSelect.appendChild(option);
}
});
const addChangeableTilemapBtn = document.createElement('button');
addChangeableTilemapBtn.textContent = '+ Add';
addChangeableTilemapBtn.className = 'attr-add';
addChangeableTilemapBtn.style.position = 'relative';
addChangeableTilemapBtn.style.zIndex = '1010'; // Ensure add button is visible
addChangeableTilemapBtn.onclick = () => {
const selectedKey = changeableTilemapSelect.value;
if (selectedKey && !tilemap.attributes.find(attr => attr.key === selectedKey)) {
const defaultValues = {
orientation: 'orthographic',
defaultDepthOrder: 'y-then-x',
gameType: 'arcade',
weather: 'clear',
'lighting.enabled': 'false',
virtualPad: 'false',
pauseAllowed: 'true',
dither: 'false',
'chunking.enabled': 'false',
'nav.type': 'grid',
shadowMode: 'none'
};
tilemap.attributes.push({ key: selectedKey, value: defaultValues[selectedKey] || '' });
window.AttributesAPI.renderObjectOverlay();
}
};
if (changeableTilemapSelect.children.length > 0) {
addChangeableTilemapDiv.appendChild(changeableTilemapSelect);
addChangeableTilemapDiv.appendChild(addChangeableTilemapBtn); // Add button below select
changeTilemapContent.appendChild(addChangeableTilemapDiv);
}
// Optional attributes
const optionalTilemapContent = document.createElement('div');
const optionalTilemapAttrsFiltered = tilemap.attributes.filter(attr => optionalTilemapAttrs.includes(attr.key));
optionalTilemapAttrsFiltered.forEach((attr, i) => {
optionalTilemapContent.appendChild(makeAttrRow('tilemap', tilemap.id, tilemap.attributes.indexOf(attr), attr));
});
const customTilemapAttrDiv = document.createElement('div');
customTilemapAttrDiv.style.display = 'flex';
customTilemapAttrDiv.style.flexDirection = 'column'; // Column layout
customTilemapAttrDiv.style.gap = '.3rem';
customTilemapAttrDiv.style.margin = '.3rem 0';
customTilemapAttrDiv.style.position = 'relative';
customTilemapAttrDiv.style.zIndex = '1010'; // Ensure add button is visible
const presetTilemapSelect = document.createElement('select');
presetTilemapSelect.style.background = '#1e1e1e';
presetTilemapSelect.style.border = '1px solid #333';
presetTilemapSelect.style.color = '#eee';
presetTilemapSelect.style.borderRadius = '.3rem';
presetTilemapSelect.style.padding = '.3rem';
presetTilemapSelect.style.fontSize = '.9rem';
const tilemapOptionalCategories = {
'📏 Map Settings': [
'name', 'tileWidth', 'tileHeight', 'gridWidth', 'gridHeight', 'backgroundColor', 'seed'
],
'📷 Camera & World': [
'camera.bounds', 'camera.zoom', 'camera.follow', 'parallax', 'pixelArt'
],
'⚙️ Physics': [
'gravity', 'airDrag', 'collisionTileTags'
],
'🎮 Rules & Goals': [
'gameType', 'timeLimit', 'win', 'lose', 'checkpoints', 'startingSpawn'
],
'💡 Lighting & Day/Night': [
'lighting.ambient', 'lighting.falloff', 'timeOfDay', 'weather.intensity'
],
'🎵 Audio': [
'music.url', 'music.volume', 'sfx.volume', 'reverbPreset'
],
'🧭 Navigation / AI': [
'nav.gridCost', 'nav.blockTags'
],
'🎨 Rendering': [
'fog', 'antiZFightBias'
],
'📱 Input & UI': [
'hud'
],
'⚡ Performance / Streaming': [
'chunkSize', 'preloadRadius', 'atlasHint'
],
'📝 Metadata': [
'author', 'version', 'license', 'notes'
]
};
const defaultTilemapOption = document.createElement('option');
defaultTilemapOption.value = '';
defaultTilemapOption.textContent = 'Select preset attribute...';
presetTilemapSelect.appendChild(defaultTilemapOption);
Object.entries(tilemapOptionalCategories).forEach(([category, attrs]) => {
const optgroup = document.createElement('optgroup');
optgroup.label = category;
attrs.forEach(attr => {
if (!optionalTilemapAttrsFiltered.find(a => a.key === attr)) {
const option = document.createElement('option');
option.value = attr;
option.textContent = attr;
optgroup.appendChild(option);
}
});
if (optgroup.children.length > 0) {
presetTilemapSelect.appendChild(optgroup);
}
});
const inputTilemapRow = document.createElement('div');
inputTilemapRow.style.display = 'flex';
inputTilemapRow.style.gap = '.4rem';
inputTilemapRow.style.position = 'relative';
inputTilemapRow.style.zIndex = '1010'; // Ensure content is visible
const customTilemapKeyInput = document.createElement('input');
customTilemapKeyInput.type = 'text';
customTilemapKeyInput.placeholder = 'Custom attribute name';
customTilemapKeyInput.className = 'ov-add-tilemap-key';
customTilemapKeyInput.dataset.tilemapId = tilemap.id;
const customTilemapValInput = document.createElement('input');
customTilemapValInput.type = 'text';
customTilemapValInput.placeholder = 'Value';
customTilemapValInput.className = 'ov-add-tilemap-val';
customTilemapValInput.dataset.tilemapId = tilemap.id;
const addCustomTilemapBtn = document.createElement('button');
addCustomTilemapBtn.textContent = '+ Add';
addCustomTilemapBtn.className = 'attr-add';
addCustomTilemapBtn.dataset.scope = 'tilemap';
addCustomTilemapBtn.dataset.scopeId = tilemap.id;
addCustomTilemapBtn.style.position = 'relative';
addCustomTilemapBtn.style.zIndex = '1010'; // Ensure add button is visible
const defaultTilemapValues = {
name: tilemap.name,
tileWidth: String(tilemap.baseTileSize),
tileHeight: String(tilemap.baseTileSize / 2),
gridWidth: String(tilemap.gridWidth),
gridHeight: String(tilemap.gridHeight),
backgroundColor: '#0e0e10',
seed: '12345',
'camera.bounds': '{x:0,y:0,w:800,h:600}',
'camera.zoom': '1',
'camera.follow': '',
parallax: '1',
pixelArt: 'false',
gravity: '{x:0,y:600}',
airDrag: '0.1',
collisionTileTags: '["wall","platform"]',
timeLimit: '0',
win: '{collect:{tag:"coin",count:10}}',
lose: '{touch:{tag:"hazard"}}',
checkpoints: '[]',
startingSpawn: '',
'lighting.ambient': '#1a1a1a',
'lighting.falloff': '1',
timeOfDay: '12',
'weather.intensity': '0',
'music.url': '',
'music.volume': '0.5',
'sfx.volume': '0.5',
reverbPreset: '',
'nav.gridCost': '{water:5,mud:2}',
'nav.blockTags': '["wall","hazard"]',
fog: '{color:"#000000",start:0,end:100}',
hud: '[]',
chunkSize: '{w:16,h:16}',
preloadRadius: '1',
atlasHint: '[]',
antiZFightBias: '0',
author: '',
version: '',
license: 'CC0',
notes: ''
};
presetTilemapSelect.onchange = () => {
if (presetTilemapSelect.value) {
customTilemapKeyInput.value = presetTilemapSelect.value;
customTilemapValInput.value = defaultTilemapValues[presetTilemapSelect.value] || '';
customTilemapValInput.focus();
}
};
inputTilemapRow.appendChild(customTilemapKeyInput);
inputTilemapRow.appendChild(customTilemapValInput);
customTilemapAttrDiv.appendChild(presetTilemapSelect);
customTilemapAttrDiv.appendChild(inputTilemapRow);
customTilemapAttrDiv.appendChild(addCustomTilemapBtn); // Add button below inputs
optionalTilemapContent.appendChild(customTilemapAttrDiv);
// Tilemap grid display
const gridTitle = document.createElement('div');
gridTitle.style.color = '#4fc3f7';
gridTitle.style.fontWeight = 'bold';
gridTitle.style.margin = '.5rem 0';
gridTitle.textContent = 'Grid (Tile IDs, 0 = Empty)';
const gridContainer = document.createElement('div');
gridContainer.className = 'tilemap-grid';
gridContainer.style.fontSize = '.75rem';
gridContainer.style.color = '#ddd';
gridContainer.style.background = '#1e1e1e';
gridContainer.style.padding = '.4rem';
gridContainer.style.borderRadius = '.3rem';
gridContainer.style.maxHeight = '200px';
gridContainer.style.overflowY = 'auto';
gridContainer.style.whiteSpace = 'pre';
gridContainer.style.fontFamily = 'monospace';
gridContainer.style.position = 'relative';
gridContainer.style.zIndex = '1010'; // Above tile picker
const gridText = tilemap.tileMapGrid.map(row =>
row.map(cell => cell ? cell.data.id : '0').join(' ')
).join('\n');
gridContainer.textContent = gridText;
// Combine sections
const tilemapInfo = document.createElement('div');
tilemapInfo.appendChild(autoTilemapContent);
tilemapInfo.appendChild(changeTilemapContent);
tilemapInfo.appendChild(optionalTilemapContent);
tilemapInfo.appendChild(gridTitle);
tilemapInfo.appendChild(gridContainer);
tilemapDiv.appendChild(tilemapTitle);
tilemapDiv.appendChild(tilemapInfo);
tilemapsContent.appendChild(tilemapDiv);
});
} else {
const noTilemapsMessage = document.createElement('div');
noTilemapsMessage.textContent = 'No tilemaps available';
noTilemapsMessage.style.color = '#bbb';
noTilemapsMessage.style.padding = '.5rem';
noTilemapsMessage.style.position = 'relative';
noTilemapsMessage.style.zIndex = '1010'; // Above tile picker
tilemapsContent.appendChild(noTilemapsMessage);
}
} else {
const noTilemapsMessage = document.createElement('div');
noTilemapsMessage.textContent = 'Tilemap module not loaded';
noTilemapsMessage.style.color = '#bbb';
noTilemapsMessage.style.padding = '.5rem';
noTilemapsMessage.style.position = 'relative';
noTilemapsMessage.style.zIndex = '1010'; // Above tile picker
tilemapsContent.appendChild(noTilemapsMessage);
}
const autoSection = createCollapsibleSection('Automatic (Read-only)', '⚙️', autoContent);
const changeSection = createCollapsibleSection('Changeable', '🟡', changeContent);
const optionalSection = createCollapsibleSection('Optional / Creative', '🔵', optionalContent);
const tilesSection = createCollapsibleSection('Tiles', '🎨', tilesContent);
const tilemapsSection = createCollapsibleSection('Tilemaps', '🗺️', tilemapsContent);
block.appendChild(h);
block.appendChild(autoSection);
block.appendChild(changeSection);
block.appendChild(optionalSection);
block.appendChild(tilesSection);
block.appendChild(tilemapsSection);
return block;
}
/* ---------- Overlay Rendering ---------- */
function renderObjectOverlay() {
const o = window.WorkspaceAPI.activeObject();
if (!o) return;
ovTitle.textContent = o.name;
ovBody.innerHTML = '';
ovBody.appendChild(buildObjectAttributesBlock(o));
for (const line of o.lines) {
ovBody.appendChild(buildLineBlock(line));
}
}
/* ---------- Overlay Controls ---------- */
function openObjectView() {
renderObjectOverlay();
objectOverlay.classList.add('open');
objectOverlay.setAttribute('aria-hidden', 'false');
}
function closeObjectView() {
objectOverlay.classList.remove('open');
objectOverlay.setAttribute('aria-hidden', 'true');
}
/* ---------- Attribute Event Handlers ---------- */
function initAttributeEvents() {
ovBody.addEventListener('click', (e) => {
const btn = e.target.closest('.attr-add');
const del = e.target.closest('.attr-del');
if (btn) {
const scope = btn.dataset.scope;
const scopeId = btn.dataset.scopeId;
if (scope === 'object') {
const keyEl = document.getElementById('ov-add-obj-key');
const valEl = document.getElementById('ov-add-obj-val');
const key = (keyEl.value || '').trim();
const value = (valEl.value || '').trim();
if (!key) {
keyEl.focus();
return;
}
const o = window.WorkspaceAPI.activeObject();
if (!o) return;
o.attributes = o.attributes || [];
o.attributes.push({ key, value });
keyEl.value = '';
valEl.value = '';
renderObjectOverlay();
return;
} else if (scope === 'line') {
const keyEl = document.querySelector(`.ov-add-line-key[data-line-id="${scopeId}"]`);
const valEl = document.querySelector(`.ov-add-line-val[data-line-id="${scopeId}"]`);
const key = (keyEl.value || '').trim();
const value = (valEl.value || '').trim();
if (!key) {
keyEl.focus();
return;
}
const o = window.WorkspaceAPI.activeObject();
if (!o) return;
const line = o.lines.find(l => l.id === scopeId);
if (!line) return;
line.attributes = line.attributes || [];
line.attributes.push({ key, value });
keyEl.value = '';
valEl.value = '';
renderObjectOverlay();
return;
} else if (scope === 'tilemap') {
const keyEl = document.querySelector(`.ov-add-tilemap-key[data-tilemap-id="${scopeId}"]`);
const valEl = document.querySelector(`.ov-add-tilemap-val[data-tilemap-id="${scopeId}"]`);
const key = (keyEl.value || '').trim();
const value = (valEl.value || '').trim();
if (!key) {
keyEl.focus();
return;
}
const tilemaps = window.TilemapAPI.getTilemaps();
const tilemap = tilemaps.find(tm => tm.id === scopeId);
if (!tilemap) return;
tilemap.attributes = tilemap.attributes || [];
tilemap.attributes.push({ key, value });
keyEl.value = '';
valEl.value = '';
renderObjectOverlay();
return;
}
}
if (del) {
const scope = del.dataset.scope;
const scopeId = del.dataset.scopeId;
const idx = parseInt(del.dataset.index, 10);
if (scope === 'object') {
const o = window.WorkspaceAPI.activeObject();
if (!o) return;
if (o.attributes && o.attributes[idx]) {
o.attributes.splice(idx, 1);
}
renderObjectOverlay();
return;
} else if (scope === 'line') {
const o = window.WorkspaceAPI.activeObject();
if (!o) return;
const line = o.lines.find(l => l.id === scopeId);
if (line && line.attributes && line.attributes[idx]) {
line.attributes.splice(idx, 1);
}
renderObjectOverlay();
return;
} else if (scope === 'tilemap') {
const tilemaps = window.TilemapAPI.getTilemaps();
const tilemap = tilemaps.find(tm => tm.id === scopeId);
if (tilemap && tilemap.attributes && tilemap.attributes[idx]) {
tilemap.attributes.splice(idx, 1);
}
renderObjectOverlay();
return;
}
}
});
ovBody.addEventListener('input', (e) => {
const keyEl = e.target.closest('.attr-key');
const valEl = e.target.closest('.attr-val');
if (keyEl) {
const scope = keyEl.dataset.scope;
const scopeId = keyEl.dataset.scopeId;
const idx = parseInt(keyEl.dataset.index, 10);
if (scope === 'object') {
const o = window.WorkspaceAPI.activeObject();
if (!o || !o.attributes || !o.attributes[idx]) return;
o.attributes[idx].key = keyEl.value;
} else if (scope == 'line') {
const o = window.WorkspaceAPI.activeObject();
if (!o) return;
const line = o.lines.find(l => l.id === scopeId);
if (!line || !line.attributes || !line.attributes[idx]) return;
line.attributes[idx].key = keyEl.value;
} else if (scope === 'tilemap') {
const tilemaps = window.TilemapAPI.getTilemaps();
const tilemap = tilemaps.find(tm => tm.id === scopeId);
if (!tilemap || !tilemap.attributes || !tilemap.attributes[idx]) return;
tilemap.attributes[idx].key = keyEl.value;
}
}
if (valEl) {
const scope = valEl.dataset.scope;
const scopeId = valEl.dataset.scopeId;
const idx = parseInt(valEl.dataset.index, 10);
if (scope === 'object') {
const o = window.WorkspaceAPI.activeObject();
if (!o || !o.attributes || !o.attributes[idx]) return;
o.attributes[idx].value = valEl.value;
} else if (scope === 'line') {
const o = window.WorkspaceAPI.activeObject();
if (!o) return;
const line = o.lines.find(l => l.id === scopeId);
if (!line || !line.attributes || !line.attributes[idx]) return;
line.attributes[idx].value = valEl.value;
} else if (scope === 'tilemap') {
const tilemaps = window.TilemapAPI.getTilemaps();
const tilemap = tilemaps.find(tm => tm.id === scopeId);
if (!tilemap || !tilemap.attributes || !tilemap.attributes[idx]) return;
tilemap.attributes[idx].value = valEl.value;
}
}
});
}
/* ---------- Initialization ---------- */
function initializeAttributes() {
try {
attributesObjectViewBtn = document.getElementById('objectViewBtn');
objectOverlay = document.getElementById('objectOverlay');
ovTitle = document.getElementById('ovTitle');
ovBody = document.getElementById('ovBody');
closeOverlayBtn = document.getElementById('closeOverlayBtn');
if (!attributesObjectViewBtn || !objectOverlay || !ovBody) {
throw new Error('Required DOM elements not found');
}
if (!window.WorkspaceAPI || typeof window.WorkspaceAPI.activeObject !== 'function') {
throw new Error('WorkspaceAPI not available');
}
// Set high z-index for overlay
objectOverlay.style.position = 'fixed';
objectOverlay.style.zIndex = '100'; // Above tile picker (z-index: 30)
// Add responsive styles
const style = document.createElement('style');
style.textContent = `
@media (max-width: 600px) {
.ov-line > div, .ov-obj-attrs > div {
font-size: 0.8rem;
}
.tilemap-grid {
font-size: 0.7rem;
max-height: 150px;
}
.attr-add, .attr-del {
padding: 0.15rem 0.4rem;
font-size: 0.8rem;
}
.ov-add-obj-key, .ov-add-obj-val, .ov-add-line-key, .ov-add-line-val,
.ov-add-tilemap-key, .ov-add-tilemap-val {
width: 100%;
box-sizing: border-box;
}
}
`;
document.head.appendChild(style);
attributesObjectViewBtn.addEventListener('click', openObjectView);
closeOverlayBtn.addEventListener('click', closeObjectView);
objectOverlay.addEventListener('click', (e) => {
if (e.target === objectOverlay) closeObjectView();
});
initAttributeEvents();
} catch (error) {
console.error(`Attributes module failed to initialize: ${error.message}`);
alert(`Attributes module failed to initialize: ${error.message}`);
}
}
window.AttributesAPI = {
renderObjectOverlay,
openObjectView,
closeObjectView
};