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