📜
attributes_copy2.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'; 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); 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); // 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 = '1000'; // Value options for different attribute types const valueOptions = { // Boolean attributes randomizeOnPlace: ['false', 'true'], shuffleFrames: ['false', 'true'], immovable: ['false', 'true'], rotationLock: ['false', 'true'], float: ['false', 'true'], collides: ['false', 'true'], // Enum attributes 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'], // Numeric ranges 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'], // Coordinate presets 'origin.x': ['0', '0.5', '1'], 'origin.y': ['0', '0.5', '1'], // Array/Object templates 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 examples tags: ['', 'enemy', 'hazard', 'collectible', 'enemy,flying', 'hazard,spikes'] }; 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); 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'; const head = document.createElement('div'); head.style.display = 'flex'; head.style.alignItems = 'center'; head.style.gap = '.6rem'; const title = document.createElement('h3'); title.textContent = 'Object Attributes'; title.style.margin = '.1rem 0'; title.style.color = '#ddd'; 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; head.appendChild(title); head.appendChild(addKey); head.appendChild(addVal); head.appendChild(addBtn); 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'; 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'; 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.gap = '.4rem'; addChangeableDiv.style.margin = '.3rem 0'; 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.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); 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'; customAttrDiv.style.gap = '.3rem'; customAttrDiv.style.margin = '.3rem 0'; 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 (!line.attributes.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'; 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; 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); inputRow.appendChild(addCustomBtn); customAttrDiv.appendChild(presetSelect); customAttrDiv.appendChild(inputRow); 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'); 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'; 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}`}`; const tilemapInfo = document.createElement('div'); tilemapInfo.style.display = 'grid'; tilemapInfo.style.gridTemplateColumns = '120px 1fr'; tilemapInfo.style.gap = '.3rem'; tilemapInfo.style.fontSize = '.85rem'; tilemapInfo.style.marginBottom = '.5rem'; const tilemapAttrs = [ { key: 'ID', value: tilemap.id }, { key: 'Name', value: tilemap.name }, { key: 'Width', value: tilemap.gridWidth }, { key: 'Height', value: tilemap.gridHeight }, { key: 'Base Tile Size', value: tilemap.baseTileSize } ]; tilemapAttrs.forEach(attr => { const keySpan = document.createElement('span'); keySpan.style.color = '#4fc3f7'; keySpan.style.fontWeight = 'bold'; keySpan.textContent = attr.key; const valueSpan = document.createElement('span'); valueSpan.style.color = '#fff'; valueSpan.textContent = attr.value; tilemapInfo.appendChild(keySpan); tilemapInfo.appendChild(valueSpan); }); // 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.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'; const gridText = tilemap.tileMapGrid.map(row => row.map(cell => cell ? cell.data.id : '0').join(' ') ).join('\n'); gridContainer.textContent = gridText; tilemapDiv.appendChild(tilemapTitle); tilemapDiv.appendChild(tilemapInfo); tilemapDiv.appendChild(gridTitle); tilemapDiv.appendChild(gridContainer); tilemapsContent.appendChild(tilemapDiv); }); } else { const noTilemapsMessage = document.createElement('div'); noTilemapsMessage.textContent = 'No tilemaps available'; noTilemapsMessage.style.color = '#bbb'; noTilemapsMessage.style.padding = '.5rem'; tilemapsContent.appendChild(noTilemapsMessage); } } else { const noTilemapsMessage = document.createElement('div'); noTilemapsMessage.textContent = 'Tilemap module not loaded'; noTilemapsMessage.style.color = '#bbb'; noTilemapsMessage.style.padding = '.5rem'; 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; } } if (del) { const scope = del.dataset.scope; const scopeId = del.dataset.scopeId; const idx = parseInt(del.dataset.index, 10); const o = window.WorkspaceAPI.activeObject(); if (!o) return; if (scope === 'object') { if (o.attributes && o.attributes[idx]) { o.attributes.splice(idx, 1); } renderObjectOverlay(); return; } else if (scope === 'line') { const line = o.lines.find(l => l.id === scopeId); if (line && line.attributes && line.attributes[idx]) { line.attributes.splice(idx, 1); } renderObjectOverlay(); return; } } }); ovBody.addEventListener('input', (e) => { const keyEl = e.target.closest('.attr-key'); const valEl = e.target.closest('.attr-val'); const o = window.WorkspaceAPI.activeObject(); if (!o) return; function apply(el, field) { const scope = el.dataset.scope; const scopeId = el.dataset.scopeId; const idx = parseInt(el.dataset.index, 10); if (scope === 'object') { if (!o.attributes || !o.attributes[idx]) return; o.attributes[idx][field] = el.value; } else if (scope === 'line') { const line = o.lines.find(l => l.id === scopeId); if (!line || !line.attributes || !line.attributes[idx]) return; line.attributes[idx][field] = el.value; } } if (keyEl) apply(keyEl, 'key'); if (valEl) apply(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'); } 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 };