Spaces:
Runtime error
Runtime error
<html lang="en"> | |
<head> | |
<meta charset="UTF-8" /> | |
<meta name="viewport" content="width=device-width, initial-scale=1" /> | |
<title>Scratch Game JSON Generator</title> | |
<style> | |
body { font-family: Arial, sans-serif; margin: 2rem; background: #f9f9f9; } | |
.asset-row, .sound-row { | |
margin-bottom: .5em; | |
display: flex; | |
align-items: center; | |
gap: 0.5em; /* Add some space between elements */ | |
} | |
.asset-row input, .asset-row select, | |
.sound-row input, .sound-row select { | |
margin-right: 0; /* Reset margin-right for better gap control */ | |
} | |
#backdrops-container .asset-row, | |
#sprites-container .asset-row { | |
flex-direction: column; /* Stack name/file and its sounds */ | |
align-items: flex-start; | |
border: 1px solid #ddd; | |
padding: 1em; | |
margin-bottom: 1em; | |
border-radius: 5px; | |
} | |
.asset-main-row { /* New class for the main part of asset row */ | |
display: flex; | |
align-items: center; | |
width: 100%; | |
gap: 0.5em; | |
} | |
.asset-sounds-container { /* Reusable for both backdrops and sprites */ | |
margin-top: 0.5em; | |
padding-left: 1em; | |
border-left: 2px solid #eee; | |
} | |
button { margin-top: 1rem; padding: .5rem 1rem; font-size: 1rem; cursor: pointer; } | |
button.remove { | |
background: #f44336; | |
color: white; | |
border: none; | |
border-radius: 3px; | |
padding: 0.3em 0.6em; | |
font-size: 0.8em; | |
cursor: pointer; | |
margin-left: 0.5em; | |
} | |
button.add-sound { | |
background: #4CAF50; | |
color: white; | |
border: none; | |
border-radius: 3px; | |
padding: 0.3em 0.6em; | |
font-size: 0.8em; | |
cursor: pointer; | |
margin-top: 0.5em; | |
} | |
pre { background: #222; color: #eee; padding: 1rem; overflow-x: auto; max-height: 60vh; } | |
</style> | |
</head> | |
<body> | |
<h1>Scratch Game JSON Generator</h1> | |
<label for="gameDesc">Enter game description:</label><br /> | |
<textarea id="gameDesc" rows="4" placeholder="e.g. jumping cat game over obstacle"></textarea><br /> | |
<h2>Backdrops</h2> | |
<div id="backdrops-container"></div> | |
<button id="add-backdrop">+ Add Backdrop</button> | |
<h2>Sprites</h2> | |
<div id="sprites-container"></div> | |
<button id="add-sprite">+ Add Sprite</button><br/> | |
<button id="generateBtn">Generate Scratch JSON</button> | |
<h2>Output JSON:</h2> | |
<pre id="output">Waiting for input...</pre> | |
<script> | |
let availableBackdrops = []; | |
let availableSprites = []; | |
let availableSounds = []; // New array for available sounds | |
// Fetch lists of files from server | |
async function loadAssetLists() { | |
const resp = await fetch('/list_assets'); | |
const { backdrops, sprites, sounds } = await resp.json(); | |
availableBackdrops = backdrops; | |
availableSprites = sprites; | |
availableSounds = sounds; | |
} | |
// Helper to create asset (backdrop/sprite) rows | |
function makeAssetRow(containerId, available, type) { | |
const container = document.getElementById(containerId); | |
const row = document.createElement('div'); | |
row.className = 'asset-row'; | |
row.classList.add(`${type.toLowerCase()}-item`); // Add type-specific class | |
let innerHTML = ` | |
<div class="asset-main-row"> | |
<input type="text" placeholder="${type} name" class="asset-name"/> | |
<select class="asset-select"> | |
${available.map(a => `<option value="${a}">${a}</option>`).join('')} | |
</select> | |
<button class="remove">×</button> | |
</div> | |
<div class="asset-sounds-container"> | |
<h3>Sounds for this ${type}:</h3> | |
<div class="asset-individual-sounds"></div> | |
<button type="button" class="add-sound">+ Add Sound</button> | |
</div> | |
`; | |
row.innerHTML = innerHTML; | |
// Attach event listener for adding sounds to this specific asset row | |
row.querySelector('.add-sound').onclick = () => { | |
makeSoundRow(row.querySelector('.asset-individual-sounds'), availableSounds, `${type} Sound`); | |
}; | |
row.querySelector('.remove').onclick = () => row.remove(); | |
container.append(row); | |
} | |
// Helper to create sound rows | |
function makeSoundRow(container, available, type) { | |
const row = document.createElement('div'); | |
row.className = 'sound-row'; | |
row.innerHTML = ` | |
<input type="text" placeholder="${type} name" class="sound-name"/> | |
<select class="sound-select"> | |
${available.map(s => `<option value="${s}">${s}</option>`).join('')} | |
</select> | |
<button class="remove">×</button> | |
`; | |
row.querySelector('.remove').onclick = () => row.remove(); | |
container.append(row); | |
} | |
document.getElementById('add-backdrop').onclick = () => { | |
makeAssetRow('backdrops-container', availableBackdrops, 'Backdrop'); | |
}; | |
// REMOVED: add-stage-sound button listener | |
// document.getElementById('add-stage-sound').onclick = () => { | |
// makeSoundRow('stage-sounds-container', availableSounds, 'Stage Sound'); | |
// }; | |
document.getElementById('add-sprite').onclick = () => { | |
makeAssetRow('sprites-container', availableSprites, 'Sprite'); | |
}; | |
document.getElementById('generateBtn').addEventListener('click', async () => { | |
const desc = document.getElementById('gameDesc').value.trim(); | |
if (!desc) return alert('Please enter a game description.'); | |
const collectAssetAndSounds = (containerId, type) => { | |
const collectedData = []; | |
const soundsPayload = {}; // Keyed by asset name (backdrop or sprite) | |
Array.from(document.getElementById(containerId).querySelectorAll(`.${type.toLowerCase()}-item`)).forEach(itemRow => { | |
const itemNameInput = itemRow.querySelector('.asset-main-row .asset-name'); | |
const itemFilenameSelect = itemRow.querySelector('.asset-main-row .asset-select'); | |
const itemName = itemNameInput.value.trim(); | |
const itemFilename = itemFilenameSelect.value; | |
if (itemName && itemFilename) { | |
collectedData.push({ | |
name: itemName, | |
filename: itemFilename | |
}); | |
const individualSoundsContainer = itemRow.querySelector('.asset-individual-sounds'); | |
if (individualSoundsContainer) { | |
// Associate sounds with the asset's name | |
soundsPayload[itemName] = Array.from(individualSoundsContainer.querySelectorAll('.sound-row')) | |
.map(row => ({ | |
name: row.querySelector('.sound-name').value.trim(), | |
filename: row.querySelector('.sound-select').value | |
})) | |
.filter(s => s.name && s.filename); | |
} | |
} | |
}); | |
return { assets: collectedData, sounds: soundsPayload }; | |
}; | |
const backdropsData = collectAssetAndSounds('backdrops-container', 'Backdrop'); | |
const spritesData = collectAssetAndSounds('sprites-container', 'Sprite'); | |
// The stage is special. If you want a global "stage" sound that isn't tied to a specific backdrop, | |
// you'd need a separate input for it. For now, we're assuming sounds are tied to backdrops. | |
// If there's only one backdrop, its sounds effectively become "stage sounds". | |
const payload = { | |
description: desc, | |
backdrops: backdropsData.assets, | |
sprites: spritesData.assets, | |
// Pass backdrop sounds and sprite sounds separately for the backend | |
backdrop_sounds: backdropsData.sounds, // New: Sounds keyed by backdrop name | |
sprite_sounds: spritesData.sounds | |
}; | |
const output = document.getElementById('output'); | |
output.textContent = 'Generating...'; | |
const resp = await fetch('/generate_game', { | |
method: 'POST', | |
headers: { 'Content-Type': 'application/json' }, | |
body: JSON.stringify(payload) | |
}); | |
if (!resp.ok) { | |
const err = await resp.json(); | |
output.textContent = 'Error: ' + (err.error || resp.statusText); | |
return; | |
} | |
const data = await resp.json(); | |
output.textContent = JSON.stringify(data, null, 2); | |
}); | |
// initial load | |
loadAssetLists(); | |
</script> | |
</body> | |
</html> |