Spaces:
Running
Running
| document.addEventListener('DOMContentLoaded', function() { | |
| // Initialize counters | |
| let classCount = 0; | |
| let raceCount = 0; | |
| // Generate RPG button functionality | |
| document.querySelector('button.bg-primary-500').addEventListener('click', function() { | |
| // Collect all form data | |
| const formData = { | |
| worldName: document.querySelector('input[type="text"]').value, | |
| backgroundStory: document.querySelector('textarea').value, | |
| majorFactions: document.querySelectorAll('input[type="text"]')[1].value, | |
| races: [], | |
| classes: [], | |
| settings: { | |
| sessionLength: document.querySelector('select').value, | |
| difficulty: document.querySelectorAll('select')[1].value, | |
| playerCount: document.querySelector('input[type="number"]').value, | |
| experienceSystem: document.querySelectorAll('select')[2].value | |
| } | |
| }; | |
| // Collect races data | |
| document.querySelectorAll('.race-card').forEach(card => { | |
| formData.races.push({ | |
| name: card.querySelector('h3').textContent, | |
| description: card.querySelector('textarea').value, | |
| size: card.querySelector('select').value, | |
| speed: card.querySelector('input[type="text"]').value, | |
| traits: Array.from(card.querySelectorAll('.traits-container input')).map(input => input.value) | |
| }); | |
| }); | |
| // Collect classes data | |
| document.querySelectorAll('.class-card').forEach(card => { | |
| formData.classes.push({ | |
| name: card.querySelector('h3').textContent, | |
| attribute: card.querySelector('select').value, | |
| hitPoints: card.querySelector('input[type="text"]').value, | |
| armor: card.querySelectorAll('select')[1].value, | |
| description: card.querySelector('textarea').value, | |
| abilities: Array.from(card.querySelectorAll('.abilities-container input')).map(input => input.value) | |
| }); | |
| }); | |
| // Create download link | |
| const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(formData, null, 2)); | |
| const downloadAnchor = document.createElement('a'); | |
| downloadAnchor.setAttribute('href', dataStr); | |
| downloadAnchor.setAttribute('download', `${formData.worldName || 'rpg'}_settings.json`); | |
| document.body.appendChild(downloadAnchor); | |
| downloadAnchor.click(); | |
| document.body.removeChild(downloadAnchor); | |
| }); | |
| // Handle lore file upload | |
| document.getElementById('lore-upload').addEventListener('change', function(e) { | |
| const file = e.target.files[0]; | |
| if (!file) return; | |
| const reader = new FileReader(); | |
| reader.onload = function(e) { | |
| try { | |
| let loreData; | |
| if (file.name.endsWith('.doc') || file.name.endsWith('.docx')) { | |
| // For DOC files, we'll just display the raw text | |
| loreData = { | |
| worldName: 'Imported DOC File', | |
| backgroundStory: e.target.result, | |
| majorFactions: 'From imported DOC file' | |
| }; | |
| } else { | |
| loreData = JSON.parse(e.target.result); | |
| } | |
| // Populate lore fields with uploaded data | |
| if (loreData.worldName) { | |
| document.querySelector('input[type="text"]').value = loreData.worldName; | |
| } | |
| if (loreData.backgroundStory) { | |
| document.querySelector('textarea').value = loreData.backgroundStory; | |
| } | |
| if (loreData.majorFactions) { | |
| document.querySelectorAll('input[type="text"]')[1].value = loreData.majorFactions; | |
| } | |
| } catch (error) { | |
| alert('Error parsing lore file. Please check the format.'); | |
| } | |
| }; | |
| reader.readAsText(file); | |
| }); | |
| // Handle races file upload | |
| document.getElementById('races-upload').addEventListener('change', function(e) { | |
| const file = e.target.files[0]; | |
| if (!file) return; | |
| const reader = new FileReader(); | |
| reader.onload = function(e) { | |
| try { | |
| let racesData; | |
| if (file.name.endsWith('.doc') || file.name.endsWith('.docx')) { | |
| // For DOC files, we'll parse into structured data | |
| const docContent = e.target.result; | |
| const lines = docContent.split('\n'); | |
| racesData = []; | |
| let currentRace = {}; | |
| lines.forEach(line => { | |
| if (line.trim().startsWith('Race:')) { | |
| if (Object.keys(currentRace).length > 0) { | |
| racesData.push(currentRace); | |
| } | |
| currentRace = { | |
| name: line.replace('Race:', '').trim(), | |
| description: '', | |
| size: 'Medium', | |
| speed: '30', | |
| traits: [] | |
| }; | |
| } else if (line.trim().startsWith('Description:')) { | |
| currentRace.description = line.replace('Description:', '').trim(); | |
| } else if (line.trim().startsWith('Trait:')) { | |
| currentRace.traits.push(line.replace('Trait:', '').trim()); | |
| } | |
| }); | |
| if (Object.keys(currentRace).length > 0) { | |
| racesData.push(currentRace); | |
| } | |
| if (racesData.length === 0) { | |
| racesData = [{name: 'Imported DOC', description: e.target.result}]; | |
| } | |
| } else { | |
| racesData = JSON.parse(e.target.result); | |
| } | |
| const container = document.getElementById('races-container'); | |
| container.innerHTML = ''; // Clear existing races | |
| racesData.forEach((raceData, index) => { | |
| raceCount++; | |
| const raceCard = document.createElement('div'); | |
| raceCard.className = 'race-card bg-gray-50 p-4 rounded-lg border border-gray-200 mb-4'; | |
| raceCard.innerHTML = ` | |
| <div class="flex justify-between items-start mb-2"> | |
| <h3 class="font-semibold text-lg">${raceData.name || `New Race #${index+1}`}</h3> | |
| <button class="text-red-500 hover:text-red-600"> | |
| <i data-feather="trash-2"></i> | |
| </button> | |
| </div> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| <div class="md:col-span-2"> | |
| <label class="block text-gray-700 mb-1 text-sm">Race Description</label> | |
| <textarea rows="3" class="w-full px-3 py-1 border border-gray-300 rounded-md">${raceData.description || ''}</textarea> | |
| </div> | |
| <div> | |
| <label class="block text-gray-700 mb-1 text-sm">Size</label> | |
| <select class="w-full px-3 py-1 border border-gray-300 rounded-md"> | |
| <option ${raceData.size === 'Small' ? 'selected' : ''}>Small</option> | |
| <option ${raceData.size === 'Medium' ? 'selected' : ''}>Medium</option> | |
| <option ${raceData.size === 'Large' ? 'selected' : ''}>Large</option> | |
| </select> | |
| </div> | |
| <div> | |
| <label class="block text-gray-700 mb-1 text-sm">Speed</label> | |
| <input type="text" class="w-full px-3 py-1 border border-gray-300 rounded-md" value="${raceData.speed || '30'}"> | |
| </div> | |
| <div class="md:col-span-2"> | |
| <label class="block text-gray-700 mb-1 text-sm">Racial Traits</label> | |
| <div class="traits-container space-y-2"> | |
| ${raceData.traits ? raceData.traits.map(trait => ` | |
| <div class="flex items-center"> | |
| <input type="text" class="flex-1 px-2 py-1 border border-gray-300 rounded-md mr-2" value="${trait}"> | |
| <button class="text-red-500 hover:text-red-600"> | |
| <i data-feather="x" class="w-4 h-4"></i> | |
| </button> | |
| </div> | |
| `).join('') : ''} | |
| </div> | |
| <button class="mt-2 text-sm text-secondary-500 hover:text-secondary-600 flex items-center"> | |
| <i data-feather="plus" class="w-4 h-4 mr-1"></i> Add Trait | |
| </button> | |
| </div> | |
| </div> | |
| `; | |
| container.appendChild(raceCard); | |
| feather.replace(); | |
| // Add event listener for delete button | |
| raceCard.querySelector('button').addEventListener('click', function() { | |
| container.removeChild(raceCard); | |
| raceCount--; | |
| }); | |
| // Add event listener for add trait button | |
| const traitsBtn = raceCard.querySelector('.traits-container').nextElementSibling; | |
| traitsBtn.addEventListener('click', function() { | |
| addTraitField(this.previousElementSibling); | |
| }); | |
| }); | |
| } catch (error) { | |
| alert('Error processing races file. Showing raw content.'); | |
| const container = document.getElementById('races-container'); | |
| container.innerHTML = `<div class="bg-gray-50 p-4 rounded-lg border border-gray-200"> | |
| <h3 class="font-semibold text-lg mb-2">File Content</h3> | |
| <pre class="whitespace-pre-wrap">${e.target.result}</pre> | |
| </div>`; | |
| } | |
| }; | |
| if (file.name.endsWith('.doc') || file.name.endsWith('.docx')) { | |
| reader.readAsText(file); | |
| } else { | |
| reader.readAsText(file); | |
| } | |
| }); | |
| // Handle classes file upload | |
| document.getElementById('classes-upload').addEventListener('change', function(e) { | |
| const file = e.target.files[0]; | |
| if (!file) return; | |
| const reader = new FileReader(); | |
| reader.onload = function(e) { | |
| try { | |
| let classesData; | |
| if (file.name.endsWith('.doc') || file.name.endsWith('.docx')) { | |
| // For DOC files, we'll parse into structured data | |
| const docContent = e.target.result; | |
| const lines = docContent.split('\n'); | |
| classesData = []; | |
| let currentClass = {}; | |
| lines.forEach(line => { | |
| if (line.trim().startsWith('Class:')) { | |
| if (Object.keys(currentClass).length > 0) { | |
| classesData.push(currentClass); | |
| } | |
| currentClass = { | |
| name: line.replace('Class:', '').trim(), | |
| attribute: 'Strength', | |
| hitPoints: '1d8', | |
| armor: 'Light', | |
| description: '', | |
| abilities: [] | |
| }; | |
| } else if (line.trim().startsWith('Description:')) { | |
| currentClass.description = line.replace('Description:', '').trim(); | |
| } else if (line.trim().startsWith('Ability:')) { | |
| currentClass.abilities.push(line.replace('Ability:', '').trim()); | |
| } | |
| }); | |
| if (Object.keys(currentClass).length > 0) { | |
| classesData.push(currentClass); | |
| } | |
| if (classesData.length === 0) { | |
| classesData = [{name: 'Imported DOC', description: e.target.result}]; | |
| } | |
| } else { | |
| classesData = JSON.parse(e.target.result); | |
| } | |
| const container = document.getElementById('classes-container'); | |
| container.innerHTML = ''; // Clear existing classes | |
| classesData.forEach((classData, index) => { | |
| classCount++; | |
| const classCard = document.createElement('div'); | |
| classCard.className = 'class-card bg-gray-50 p-4 rounded-lg border border-gray-200 mb-4'; | |
| classCard.innerHTML = ` | |
| <div class="flex justify-between items-start mb-2"> | |
| <h3 class="font-semibold text-lg">${classData.name || `New Class #${index+1}`}</h3> | |
| <button class="text-red-500 hover:text-red-600"> | |
| <i data-feather="trash-2"></i> | |
| </button> | |
| </div> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| <div> | |
| <label class="block text-gray-700 mb-1 text-sm">Class Name</label> | |
| <input type="text" class="w-full px-3 py-1 border border-gray-300 rounded-md" value="${classData.name || ''}"> | |
| </div> | |
| <div> | |
| <label class="block text-gray-700 mb-1 text-sm">Primary Attribute</label> | |
| <select class="w-full px-3 py-1 border border-gray-300 rounded-md"> | |
| <option ${classData.attribute === 'Strength' ? 'selected' : ''}>Strength</option> | |
| <option ${classData.attribute === 'Dexterity' ? 'selected' : ''}>Dexterity</option> | |
| <option ${classData.attribute === 'Intelligence' ? 'selected' : ''}>Intelligence</option> | |
| <option ${classData.attribute === 'Wisdom' ? 'selected' : ''}>Wisdom</option> | |
| <option ${classData.attribute === 'Charisma' ? 'selected' : ''}>Charisma</option> | |
| <option ${classData.attribute === 'Constitution' ? 'selected' : ''}>Constitution</option> | |
| </select> | |
| </div> | |
| <div> | |
| <label class="block text-gray-700 mb-1 text-sm">Hit Points</label> | |
| <input type="text" class="w-full px-3 py-1 border border-gray-300 rounded-md" value="${classData.hitPoints || '1d8'}"> | |
| </div> | |
| <div> | |
| <label class="block text-gray-700 mb-1 text-sm">Armor Proficiency</label> | |
| <select class="w-full px-3 py-1 border border-gray-300 rounded-md"> | |
| <option ${classData.armor === 'Light' ? 'selected' : ''}>Light</option> | |
| <option ${classData.armor === 'Medium' ? 'selected' : ''}>Medium</option> | |
| <option ${classData.armor === 'Heavy' ? 'selected' : ''}>Heavy</option> | |
| <option ${classData.armor === 'None' ? 'selected' : ''}>None</option> | |
| </select> | |
| </div> | |
| <div class="md:col-span-2"> | |
| <label class="block text-gray-700 mb-1 text-sm">Class Description</label> | |
| <textarea rows="2" class="w-full px-3 py-1 border border-gray-300 rounded-md">${classData.description || ''}</textarea> | |
| </div> | |
| <div class="md:col-span-2"> | |
| <label class="block text-gray-700 mb-1 text-sm">Special Abilities</label> | |
| <div class="abilities-container space-y-2"> | |
| ${classData.abilities ? classData.abilities.map(ability => ` | |
| <div class="flex items-center"> | |
| <input type="text" class="flex-1 px-2 py-1 border border-gray-300 rounded-md mr-2" value="${ability}"> | |
| <button class="text-red-500 hover:text-red-600"> | |
| <i data-feather="x" class="w-4 h-4"></i> | |
| </button> | |
| </div> | |
| `).join('') : ''} | |
| </div> | |
| <button class="mt-2 text-sm text-secondary-500 hover:text-secondary-600 flex items-center"> | |
| <i data-feather="plus" class="w-4 h-4 mr-1"></i> Add Ability | |
| </button> | |
| </div> | |
| </div> | |
| `; | |
| container.appendChild(classCard); | |
| feather.replace(); | |
| // Add event listener for delete button | |
| classCard.querySelector('button').addEventListener('click', function() { | |
| container.removeChild(classCard); | |
| classCount--; | |
| }); | |
| // Add event listeners for abilities | |
| const abilitiesBtn = classCard.querySelector('.abilities-container').nextElementSibling; | |
| abilitiesBtn.addEventListener('click', function() { | |
| addAbilityField(this.previousElementSibling); | |
| }); | |
| // Add event listeners for existing ability delete buttons | |
| classCard.querySelectorAll('.abilities-container button').forEach(btn => { | |
| btn.addEventListener('click', function() { | |
| this.parentElement.remove(); | |
| }); | |
| }); | |
| }); | |
| } catch (error) { | |
| alert('Error parsing classes file. Please check the format.'); | |
| } | |
| }; | |
| reader.readAsText(file); | |
| }); | |
| function addAbilityField(container) { | |
| const abilityDiv = document.createElement('div'); | |
| abilityDiv.className = 'flex items-center'; | |
| abilityDiv.innerHTML = ` | |
| <input type="text" class="flex-1 px-2 py-1 border border-gray-300 rounded-md mr-2" placeholder="Ability name"> | |
| <button class="text-red-500 hover:text-red-600"> | |
| <i data-feather="x" class="w-4 h-4"></i> | |
| </button> | |
| `; | |
| container.appendChild(abilityDiv); | |
| feather.replace(); | |
| // Add event listener for delete ability button | |
| abilityDiv.querySelector('button').addEventListener('click', function() { | |
| container.removeChild(abilityDiv); | |
| }); | |
| } | |
| // Add race button functionality | |
| document.getElementById('add-race-btn').addEventListener('click', function() { | |
| raceCount++; | |
| const container = document.getElementById('races-container'); | |
| const raceCard = document.createElement('div'); | |
| raceCard.className = 'race-card bg-gray-50 p-4 rounded-lg border border-gray-200 mb-4'; | |
| raceCard.innerHTML = ` | |
| <div class="flex justify-between items-start mb-2"> | |
| <h3 class="font-semibold text-lg">New Race #${raceCount}</h3> | |
| <button class="text-red-500 hover:text-red-600"> | |
| <i data-feather="trash-2"></i> | |
| </button> | |
| </div> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| <div class="md:col-span-2"> | |
| <label class="block text-gray-700 mb-1 text-sm">Race Description</label> | |
| <textarea rows="3" class="w-full px-3 py-1 border border-gray-300 rounded-md"></textarea> | |
| </div> | |
| <div> | |
| <label class="block text-gray-700 mb-1 text-sm">Size</label> | |
| <select class="w-full px-3 py-1 border border-gray-300 rounded-md"> | |
| <option>Small</option> | |
| <option selected>Medium</option> | |
| <option>Large</option> | |
| </select> | |
| </div> | |
| <div> | |
| <label class="block text-gray-700 mb-1 text-sm">Speed</label> | |
| <input type="text" class="w-full px-3 py-1 border border-gray-300 rounded-md" value="30"> | |
| </div> | |
| <div class="md:col-span-2"> | |
| <label class="block text-gray-700 mb-1 text-sm">Racial Traits</label> | |
| <div class="traits-container space-y-2"></div> | |
| <button class="mt-2 text-sm text-secondary-500 hover:text-secondary-600 flex items-center"> | |
| <i data-feather="plus" class="w-4 h-4 mr-1"></i> Add Trait | |
| </button> | |
| </div> | |
| </div> | |
| `; | |
| container.appendChild(raceCard); | |
| feather.replace(); | |
| // Add event listener for delete button | |
| raceCard.querySelector('button').addEventListener('click', function() { | |
| container.removeChild(raceCard); | |
| raceCount--; | |
| }); | |
| // Add event listener for add trait button | |
| const traitsBtn = raceCard.querySelector('.traits-container').nextElementSibling; | |
| traitsBtn.addEventListener('click', function() { | |
| const traitsContainer = this.previousElementSibling; | |
| const traitDiv = document.createElement('div'); | |
| traitDiv.className = 'flex items-center'; | |
| traitDiv.innerHTML = ` | |
| <input type="text" class="flex-1 px-2 py-1 border border-gray-300 rounded-md mr-2" placeholder="Trait name"> | |
| <button class="text-red-500 hover:text-red-600"> | |
| <i data-feather="x" class="w-4 h-4"></i> | |
| </button> | |
| `; | |
| traitsContainer.appendChild(traitDiv); | |
| feather.replace(); | |
| // Add event listener for delete trait button | |
| traitDiv.querySelector('button').addEventListener('click', function() { | |
| traitsContainer.removeChild(traitDiv); | |
| }); | |
| }); | |
| }); | |
| // Add class button functionality | |
| document.getElementById('add-class-btn').addEventListener('click', function() { | |
| classCount++; | |
| const container = document.getElementById('classes-container'); | |
| const classCard = document.createElement('div'); | |
| classCard.className = 'class-card bg-gray-50 p-4 rounded-lg border border-gray-200 mb-4'; | |
| classCard.innerHTML = ` | |
| <div class="flex justify-between items-start mb-2"> | |
| <h3 class="font-semibold text-lg">New Class #${classCount}</h3> | |
| <button class="text-red-500 hover:text-red-600"> | |
| <i data-feather="trash-2"></i> | |
| </button> | |
| </div> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| <div> | |
| <label class="block text-gray-700 mb-1 text-sm">Class Name</label> | |
| <input type="text" class="w-full px-3 py-1 border border-gray-300 rounded-md"> | |
| </div> | |
| <div> | |
| <label class="block text-gray-700 mb-1 text-sm">Primary Attribute</label> | |
| <select class="w-full px-3 py-1 border border-gray-300 rounded-md"> | |
| <option>Strength</option> | |
| <option>Dexterity</option> | |
| <option>Intelligence</option> | |
| <option>Wisdom</option> | |
| <option>Charisma</option> | |
| <option>Constitution</option> | |
| </select> | |
| </div> | |
| <div> | |
| <label class="block text-gray-700 mb-1 text-sm">Hit Points</label> | |
| <input type="text" class="w-full px-3 py-1 border border-gray-300 rounded-md" value="1d8"> | |
| </div> | |
| <div> | |
| <label class="block text-gray-700 mb-1 text-sm">Armor Proficiency</label> | |
| <select class="w-full px-3 py-1 border border-gray-300 rounded-md"> | |
| <option>Light</option> | |
| <option>Medium</option> | |
| <option>Heavy</option> | |
| <option>None</option> | |
| </select> | |
| </div> | |
| <div class="md:col-span-2"> | |
| <label class="block text-gray-700 mb-1 text-sm">Class Description</label> | |
| <textarea rows="2" class="w-full px-3 py-1 border border-gray-300 rounded-md"></textarea> | |
| </div> | |
| <div class="md:col-span-2"> | |
| <label class="block text-gray-700 mb-1 text-sm">Special Abilities</label> | |
| <div class="abilities-container space-y-2"></div> | |
| <button class="mt-2 text-sm text-secondary-500 hover:text-secondary-600 flex items-center"> | |
| <i data-feather="plus" class="w-4 h-4 mr-1"></i> Add Ability | |
| </button> | |
| </div> | |
| </div> | |
| `; | |
| container.appendChild(classCard); | |
| feather.replace(); | |
| // Add event listener for delete button | |
| classCard.querySelector('button').addEventListener('click', function() { | |
| container.removeChild(classCard); | |
| classCount--; | |
| }); | |
| // Add event listener for add ability button | |
| const abilitiesBtn = classCard.querySelector('.abilities-container').nextElementSibling; | |
| abilitiesBtn.addEventListener('click', function() { | |
| const abilitiesContainer = this.previousElementSibling; | |
| const abilityDiv = document.createElement('div'); | |
| abilityDiv.className = 'flex items-center'; | |
| abilityDiv.innerHTML = ` | |
| <input type="text" class="flex-1 px-2 py-1 border border-gray-300 rounded-md mr-2" placeholder="Ability name"> | |
| <button class="text-red-500 hover:text-red-600"> | |
| <i data-feather="x" class="w-4 h-4"></i> | |
| </button> | |
| `; | |
| abilitiesContainer.appendChild(abilityDiv); | |
| feather.replace(); | |
| // Add event listener for delete ability button | |
| abilityDiv.querySelector('button').addEventListener('click', function() { | |
| abilitiesContainer.removeChild(abilityDiv); | |
| }); | |
| }); | |
| }); | |
| // Initialize tooltips for icons | |
| const tooltipTriggers = document.querySelectorAll('[data-tooltip]'); | |
| tooltipTriggers.forEach(trigger => { | |
| const tooltip = document.createElement('div'); | |
| tooltip.className = 'tooltip hidden absolute bg-black text-white text-xs px-2 py-1 rounded whitespace-nowrap -mt-8'; | |
| tooltip.textContent = trigger.getAttribute('data-tooltip'); | |
| trigger.appendChild(tooltip); | |
| trigger.addEventListener('mouseenter', () => { | |
| tooltip.classList.remove('hidden'); | |
| }); | |
| trigger.addEventListener('mouseleave', () => { | |
| tooltip.classList.add('hidden'); | |
| }); | |
| }); | |
| }); |