Role-Playing Adventure Game

Role-Playing Adventure Creator

Core Adventure Details

Player Character Template

Character Stats

Starting Inventory

Story Node Editor

Manage Character Stats

Define the *names* of stats available for your game (e.g., Health, Stamina, Magic).

No story nodes created. Click "+ Add Story Node" to begin.

'; } state.storyNodes.forEach(node => { const card = document.createElement('div'); card.className = 'rpg-node-card'; card.dataset.nodeId = node.id; // Choices HTML let choicesHTML = '
'; node.choices.forEach(choice => { choicesHTML += `
`; }); choicesHTML += '
'; // Full card HTML card.innerHTML = `

Choices

${choicesHTML}
`; nodeLiseContainer.appendChild(card); }); } function renderPlaytestView() { const { currentNodeId, lastOutcome } = state.ui; if (currentNodeId === null) { // Start screen playtestTitle.textContent = "Playtest Adventure"; playtestStoryText.textContent = state.adventure.intro; playtestChoicesList.innerHTML = ''; playtestStartBtn.style.display = 'block'; playtestRestartBtn.style.display = 'none'; playtestOutcome.style.display = 'none'; return; } const node = state.storyNodes.find(n => n.id === currentNodeId); if (!node) { // End of branch playtestStoryText.textContent = "This branch of the story has ended."; playtestChoicesList.innerHTML = ''; playtestStartBtn.style.display = 'none'; playtestRestartBtn.style.display = 'block'; } else { // Render the current node playtestTitle.textContent = node.title; playtestStoryText.textContent = node.text; playtestChoicesList.innerHTML = ''; node.choices.forEach(choice => { const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'rpg-btn rpg-playtest-choice-btn'; btn.textContent = choice.text; btn.dataset.leadsTo = choice.leadsToNodeId; btn.dataset.outcome = choice.outcome; playtestChoicesList.appendChild(btn); }); playtestStartBtn.style.display = 'none'; playtestRestartBtn.style.display = 'block'; } // Show outcome from *last* choice if (lastOutcome) { playtestOutcome.textContent = lastOutcome; playtestOutcome.style.display = 'block'; } else { playtestOutcome.style.display = 'none'; } } // --- EVENT LISTENERS --- // function attachTabListeners() { const tabIds = Array.from(tabLinks).map(link => link.dataset.tab); function showTab(tabId) { state.ui.activeTab = tabId; tabContents.forEach(c => c.classList.toggle('rpg-active', c.id === tabId)); tabLinks.forEach(l => l.classList.toggle('rpg-active', l.dataset.tab === tabId)); const currentIndex = tabIds.indexOf(tabId); prevTabBtn.style.visibility = (currentIndex === 0) ? 'hidden' : 'visible'; nextTabBtn.style.visibility = (currentIndex === tabIds.length - 1) ? 'hidden' : 'visible'; } tabLinks.forEach(link => link.addEventListener('click', () => showTab(link.dataset.tab))); nextTabBtn.addEventListener('click', () => { const currentIndex = tabIds.indexOf(state.ui.activeTab); if (currentIndex < tabIds.length - 1) showTab(tabIds[currentIndex + 1]); }); prevTabBtn.addEventListener('click', () => { const currentIndex = tabIds.indexOf(state.ui.activeTab); if (currentIndex > 0) showTab(tabIds[currentIndex - 1]); }); } function attachDashboardListeners() { // Core Details titleInput.addEventListener('input', (e) => state.adventure.title = e.target.value); introTextarea.addEventListener('input', (e) => state.adventure.intro = e.target.value); titleInput.addEventListener('change', saveState); introTextarea.addEventListener('change', saveState); // Player Template addStatBtn.addEventListener('click', () => { const stat = addStatSelect.value; if (stat && !state.playerTemplate.stats.includes(stat)) { state.playerTemplate.stats.push(stat); updateAll(); } }); addItemBtn.addEventListener('click', () => { const item = newItemInput.value.trim(); if (item && !state.playerTemplate.inventory.includes(item)) { state.playerTemplate.inventory.push(item); newItemInput.value = ''; updateAll(); } }); container.addEventListener('click', (e) => { // Remove stat/item from template if (e.target.classList.contains('rpg-remove-item')) { const type = e.target.dataset.type; const id = e.target.dataset.id; if (type === 'stat') { state.playerTemplate.stats = state.playerTemplate.stats.filter(s => s !== id); } else if (type === 'item') { state.playerTemplate.inventory = state.playerTemplate.inventory.filter(i => i !== id); } updateAll(); } }); // Node Editor addNodeBtn.addEventListener('click', () => { const newNode = { id: `n${Date.now()}`, title: "New Story Node", text: "", choices: [] }; state.storyNodes.push(newNode); updateAll(); }); // Node Editor (Event Delegation) nodeLiseContainer.addEventListener('click', (e) => { const nodeCard = e.target.closest('.rpg-node-card'); if (!nodeCard) return; const nodeId = nodeCard.dataset.nodeId; const node = state.storyNodes.find(n => n.id === nodeId); if (!node) return; // Remove Node if (e.target.classList.contains('rpg-remove-node-btn')) { state.storyNodes = state.storyNodes.filter(n => n.id !== nodeId); // Also remove choices leading to this node state.storyNodes.forEach(n => { n.choices.forEach(c => { if (c.leadsToNodeId === nodeId) c.leadsToNodeId = null; }); }); updateAll(); } // Add Choice if (e.target.classList.contains('rpg-add-choice-btn')) { node.choices.push({ id: `c${Date.now()}`, text: "New Choice", leadsToNodeId: null, outcome: "" }); updateAll(); } // Remove Choice if (e.target.classList.contains('rpg-remove-choice-btn')) { const choiceId = e.target.dataset.choiceId; node.choices = node.choices.filter(c => c.id !== choiceId); updateAll(); } }); nodeLiseContainer.addEventListener('input', (e) => { const nodeCard = e.target.closest('.rpg-node-card'); if (!nodeCard) return; const nodeId = nodeCard.dataset.nodeId; const node = state.storyNodes.find(n => n.id === nodeId); if (!node) return; // Update Node Title if (e.target.classList.contains('rpg-node-title-input')) { node.title = e.target.value; // This requires a full re-render of dropdowns renderNodeEditors(); } // Update Node Text if (e.target.classList.contains('rpg-node-text-input')) { node.text = e.target.value; } // Update Choice Fields if (e.target.classList.contains('rpg-choice-input')) { const choiceId = e.target.closest('.rpg-choice-item').dataset.choiceId; const choice = node.choices.find(c => c.id === choiceId); const field = e.target.dataset.field; if (choice) { choice[field] = e.target.value; } } saveState(); }); } function attachPlaytestListeners() { // Toggle Editor/Playtest const toggleViews = () => { state.ui.playMode = !state.ui.playMode; editorView.style.display = state.ui.playMode ? 'none' : 'block'; playtestView.style.display = state.ui.playMode ? 'block' : 'none'; container.querySelector('#rpg-pdf-download-container').style.display = state.ui.playMode ? 'none' : 'block'; if (state.ui.playMode) { // Reset to start screen state.ui.currentNodeId = null; state.ui.lastOutcome = null; renderPlaytestView(); } }; playtestToggleBtn.addEventListener('click', toggleViews); editorToggleBtn.addEventListener('click', toggleViews); // Start button playtestStartBtn.addEventListener('click', () => { if (state.storyNodes.length > 0) { state.ui.currentNodeId = state.storyNodes[0].id; // Start at first node state.ui.lastOutcome = null; renderPlaytestView(); } else { alert("No story nodes have been created yet!"); } }); // Restart button playtestRestartBtn.addEventListener('click', () => { state.ui.currentNodeId = null; state.ui.lastOutcome = null; renderPlaytestView(); }); // Choice buttons playtestChoicesList.addEventListener('click', (e) => { if (e.target.classList.contains('rpg-playtest-choice-btn')) { state.ui.currentNodeId = e.target.dataset.leadsTo || null; state.ui.lastOutcome = e.target.dataset.outcome || null; renderPlaytestView(); } }); } function attachConfigListeners() { // Add Stat Name addStatConfigBtn.addEventListener('click', () => { const name = newStatNameInput.value.trim(); if (name && !state.config.availableStats.includes(name)) { state.config.availableStats.push(name); newStatNameInput.value = ''; updateAll(); } }); // Remove Stat Name (Delegation) statConfigList.addEventListener('click', (e) => { if (e.target.classList.contains('rpg-remove-config-item')) { const name = e.target.dataset.name; state.config.availableStats = state.config.availableStats.filter(s => s !== name); // Also remove from player template if it was in use state.playerTemplate.stats = state.playerTemplate.stats.filter(s => s !== name); updateAll(); } }); } function attachPdfListener() { downloadPdfBtn.addEventListener('click', () => { // Ensure jsPDF and autoTable are loaded if (typeof jspdf === 'undefined' || !jspdf.jsPDF) { alert('Error: PDF library (jsPDF) not loaded.'); return; } if (typeof jspdf.jsPDF.autoTable !== 'function') { alert('Error: PDF library (autoTable) not loaded.'); return; } const { jsPDF } = jspdf; const doc = new jsPDF(); let cursorY = 20; // Y position tracker // 1. Title doc.setFontSize(22); doc.text(state.adventure.title, doc.internal.pageSize.getWidth() / 2, cursorY, { align: 'center' }); cursorY += 15; // 2. Introduction doc.setFontSize(18); doc.text("Introduction", 14, cursorY); cursorY += 8; doc.setFontSize(12); // Split long text const introLines = doc.splitTextToSize(state.adventure.intro, 180); doc.text(introLines, 14, cursorY); cursorY += (introLines.length * 7) + 10; // 3. Player Template doc.setFontSize(18); doc.text("Player Character Template", 14, cursorY); cursorY += 8; doc.setFontSize(12); doc.text("Starting Stats:", 14, cursorY); doc.text(state.playerTemplate.stats.join(', ') || 'N/A', 45, cursorY); cursorY += 7; doc.text("Starting Inventory:", 14, cursorY); doc.text(state.playerTemplate.inventory.join(', ') || 'N/A', 55, cursorY); cursorY += 15; // 4. Story Nodes doc.setFontSize(18); doc.text("Story Nodes", 14, cursorY); cursorY += 8; const getNodeTitle = (nodeId) => { if (!nodeId) return "(End of Branch)"; const node = state.storyNodes.find(n => n.id === nodeId); return node ? node.title : "(Invalid Node)"; }; state.storyNodes.forEach((node, index) => { // Check for page break if (cursorY > 250) { doc.addPage(); cursorY = 20; } doc.setFontSize(16); doc.setFont(undefined, 'bold'); doc.text(node.title, 14, cursorY); cursorY += 8; doc.setFont(undefined, 'normal'); doc.setFontSize(12); const storyLines = doc.splitTextToSize(node.text, 180); doc.text(storyLines, 14, cursorY); cursorY += (storyLines.length * 7) + 5; const choiceBody = node.choices.map(choice => [ choice.text, getNodeTitle(choice.leadsToNodeId), choice.outcome || 'N/A' ]); doc.autoTable({ head: [['Choice Text', 'Leads To', 'Outcome']], body: choiceBody, startY: cursorY, theme: 'grid', headStyles: { fillColor: [0, 123, 255] } }); cursorY = doc.lastAutoTable.finalY + 15; }); // 5. Save const safeFileName = (state.adventure.title || 'adventure-module').replace(/[^a-z0-9]/gi, '_').toLowerCase(); doc.save(`${safeFileName}.pdf`); }); } // --- INITIALIZATION --- // function init() { loadState(); attachTabListeners(); attachDashboardListeners(); attachPlaytestListeners(); attachConfigListeners(); attachPdfListener(); updateAll(); // Initial render of all components } init(); });