Monster Battle Simulator
Monster 1
Monster 2
Battle Log:
Enter monster stats and click "Simulate Battle!".
Enter monster stats and click "Simulate Battle!".
'; // Reset health bars visually to full (based on current input values) initialHealth1 = parseInt(monster1HealthInput.value, 10) || 1; initialHealth2 = parseInt(monster2HealthInput.value, 10) || 1; updateHealthBar(monster1HealthBar, initialHealth1, initialHealth1); updateHealthBar(monster2HealthBar, initialHealth2, initialHealth2); // Enable simulate button simulateBtn.disabled = false; simulateBtn.style.opacity = 1; // Restore normal opacity } function simulateBattle() { resetBattle(); // Start with a clean slate // Disable simulate button during battle simulateBtn.disabled = true; simulateBtn.style.opacity = 0.6; // Dim the button const m1Name = monster1NameInput.value.trim() || 'Monster 1'; let m1Health = parseInt(monster1HealthInput.value, 10); const m1Attack = parseInt(monster1AttackInput.value, 10); const m1Defense = parseInt(monster1DefenseInput.value, 10); const m2Name = monster2NameInput.value.trim() || 'Monster 2'; let m2Health = parseInt(monster2HealthInput.value, 10); const m2Attack = parseInt(monster2AttackInput.value, 10); const m2Defense = parseInt(monster2DefenseInput.value, 10); // Validate inputs and set minimums m1Health = Math.max(1, isNaN(m1Health) ? 100 : m1Health); m1Attack = Math.max(0, isNaN(m1Attack) ? 15 : m1Attack); m1Defense = Math.max(0, isNaN(m1Defense) ? 5 : m1Defense); m2Health = Math.max(1, isNaN(m2Health) ? 120 : m2Health); m2Attack = Math.max(0, isNaN(m2Attack) ? 18 : m2Attack); m2Defense = Math.max(0, isNaN(m2Defense) ? 7 : m2Defense); // Update input fields with validated values (if they were empty or invalid) monster1HealthInput.value = m1Health; monster1AttackInput.value = m1Attack; monster1DefenseInput.value = m1Defense; monster2HealthInput.value = m2Health; monster2AttackInput.value = m2Attack; monster2DefenseInput.value = m2Defense; // Store initial health for health bar and percentage calculation initialHealth1 = m1Health; initialHealth2 = m2Health; // Clear initial message in log logContent.innerHTML = ''; logMessage(`=== Battle Start: ${m1Name} vs ${m2Name} ===`); logMessage(`Initial Health - ${m1Name}: ${m1Health}, ${m2Name}: ${m2Health}`); logMessage('---'); // Initial health bar update updateHealthBar(monster1HealthBar, m1Health, initialHealth1); updateHealthBar(monster2HealthBar, m2Health, initialHealth2); let turn = 1; // Decide who goes first (simple coin flip for now) let attacker = (Math.random() < 0.5) ? 1 : 2; const maxTurns = 200; // Prevent excessive loops, assuming most battles end quickly while (m1Health > 0 && m2Health > 0 && turn <= maxTurns) { let currentAttackerName, currentDefenderName; let currentAttackerAttack, currentDefenderDefense; let currentDefenderHealth; let currentDefenderHealthBar; let setDefenderHealth; // Function to set health of the defender monster if (attacker === 1) { currentAttackerName = m1Name; currentDefenderName = m2Name; currentAttackerAttack = m1Attack; currentDefenderDefense = m2Defense; currentDefenderHealth = m2Health; currentDefenderHealthBar = monster2HealthBar; setDefenderHealth = (newHealth) => { m2Health = newHealth; }; } else { currentAttackerName = m2Name; currentDefenderName = m1Name; currentAttackerAttack = m2Attack; currentDefenderDefense = m1Defense; currentDefenderHealth = m1Health; currentDefenderHealthBar = monster1HealthBar; setDefenderHealth = (newHealth) => { m1Health = newHealth; }; } // Calculate damage const damageDealt = Math.max(0, currentAttackerAttack - currentDefenderDefense); // Check for a potential infinite battle (both deal 0 damage from the start) if (turn === 1 && Math.max(0, m1Attack - m2Defense) === 0 && Math.max(0, m2Attack - m1Defense) === 0) { logMessage("Both monsters' defenses are too high! The battle is an indefinite stalemate."); simulateBtn.disabled = false; // Re-enable button simulateBtn.style.opacity = 1; return; // End simulation } // Apply damage let newDefenderHealth = currentDefenderHealth - damageDealt; setDefenderHealth(newDefenderHealth); logMessage(`Turn ${turn}: ${currentAttackerName} attacks ${currentDefenderName} for ${damageDealt} damage.`); logMessage(`${currentDefenderName}'s health is now ${Math.max(0, newDefenderHealth)}.`); // Display non-negative health in log // Update defender's health bar if (attacker === 1) { updateHealthBar(monster2HealthBar, m2Health, initialHealth2); } else { updateHealthBar(monster1HealthBar, m1Health, initialHealth1); } // Check for win/loss if (m1Health <= 0) { logMessage('---'); logMessage(`=== Battle End: ${m2Name} Wins! ===`); updateHealthBar(monster1HealthBar, 0, initialHealth1); // Ensure health bar is 0 break; // End simulation } if (m2Health <= 0) { logMessage('---'); logMessage(`=== Battle End: ${m1Name} Wins! ===`); updateHealthBar(monster2HealthBar, 0, initialHealth2); // Ensure health bar is 0 break; // End simulation } logMessage('---'); // Switch attacker attacker = (attacker === 1) ? 2 : 1; turn++; } if (turn > maxTurns) { logMessage('---'); logMessage(`=== Battle End: Stalemate after ${maxTurns} turns! No clear winner. ===`); } simulateBtn.disabled = false; // Re-enable button simulateBtn.style.opacity = 1; } function downloadPdf() { const { jsPDF } = window.jspdf; const doc = new jsPDF({ orientation: 'P', // Portrait unit: 'mm', format: 'a4' }); // Target the output section for PDF - include setup and log const element = document.getElementById('monsterBattleSimulator'); // Apply temporary print styles for PDF generation // These styles aim for a cleaner, print-friendly output while using the dark theme colors const pdfStyles = ` #monsterBattleSimulator { background-color: #ffffff !important; /* White background for print */ color: #000 !important; /* Black text */ box-shadow: none !important; border: none !important; padding: 10mm !important; /* Use mm for consistency */ margin: 0 !important; font-size: 9pt !important; } #monsterBattleSimulator h2, #monsterBattleSimulator h3 { color: #000 !important; /* Black headings */ text-shadow: none !important; } #monsterBattleSimulator .monster-setup { flex-direction: column !important; align-items: stretch !important; margin-bottom: 10mm !important; } #monsterBattleSimulator .monster-card { width: auto !important; min-width: auto !important; border: 1px solid #ccc !important; background-color: #fff !important; box-shadow: none !important; padding: 10mm !important; margin-bottom: 10mm !important; } #monsterBattleSimulator .monster-card h3 { border-bottom-color: #ccc !important; margin-bottom: 10mm !important; padding-bottom: 2mm !important; } #monsterBattleSimulator .input-group { margin-bottom: 5mm !important; } #monsterBattleSimulator label { color: #000 !important; font-weight: normal !important; } #monsterBattleSimulator input { /* Style all inputs */ border-color: #ccc !important; background-color: #fff !important; color: #000 !important; padding: 4mm !important; width: auto !important; /* Allow auto width for print */ } #monsterBattleSimulator input[type="number"] { width: 15mm !important; /* Specific width for number input in print */ } #monsterBattleSimulator .health-bar-container, #monsterBattleSimulator .controls-section button { display: none !important; /* Hide health bars and buttons */ } #monsterBattleSimulator .battle-log-section { border: 1px solid #ccc !important; background-color: #fff !important; padding: 10mm !important; border-radius: 0 !important; margin-bottom: 0 !important; } #monsterBattleSimulator #logContent { max-height: none !important; overflow-y: visible !important; background-color: #fff !important; padding: 0 !important; color: #000 !important; border-radius: 0 !important; } #monsterBattleSimulator #logContent p { border-bottom-color: #eee !important; margin: 2mm 0 !important; padding-bottom: 2mm !important; } #monsterBattleSimulator #logContent strong { color: #000 !important; /* Black highlights in log */ } `; // Create a style element and append it const styleTag = document.createElement('style'); styleTag.setAttribute('id', 'pdfPrintStyles'); // Give it an ID to easily remove styleTag.innerHTML = pdfStyles; document.head.appendChild(styleTag); // Use html2canvas to render the element as an image/canvas html2canvas(element, { scale: 2, // Increase scale for better resolution in PDF logging: false, // Disable html2canvas logging backgroundColor: '#ffffff' // Explicitly set white background for canvas }).then(canvas => { const imgData = canvas.toDataURL('image/png'); const pdfWidth = doc.internal.pageSize.getWidth(); const pdfHeight = doc.internal.pageSize.getHeight(); const imgWidth = pdfWidth - 20; // Leave 10mm margin on each side const imgHeight = canvas.height * imgWidth / canvas.width; let heightLeft = imgHeight; let position = 10; // Starting Y position (10mm margin) // Add the image to the PDF // Adjust position and height to fit within PDF margins doc.addImage(imgData, 'PNG', 10, position, imgWidth, imgHeight); heightLeft -= (pdfHeight - position); // Handle multiple pages if content exceeds one page while (heightLeft >= 0) { position = 10; // Reset position for new page doc.addPage(); // Add background to the new page doc.setFillColor(255); // White doc.rect(0, 0, pdfWidth, pdfHeight, 'F'); // Calculate the source y position on the canvas image const srcY = imgHeight - heightLeft; // Add the remaining part of the image doc.addImage(imgData, 'PNG', 10, position, imgWidth, heightLeft, null, null, 0, srcY); heightLeft -= (pdfHeight - position); } // Remove the temporary print styles const addedStyleTag = document.getElementById('pdfPrintStyles'); if (addedStyleTag) { addedStyleTag.remove(); } doc.save('monster_battle_log.pdf'); }).catch(error => { console.error("Error generating PDF:", error); alert("Could not generate PDF. Please try again."); // Ensure styles are removed even on error const addedStyleTag = document.getElementById('pdfPrintStyles'); if (addedStyleTag) { addedStyleTag.remove(); } }); } // Event Listeners simulateBtn.addEventListener('click', simulateBattle); resetBtn.addEventListener('click', resetBattle); downloadPdfBtn.addEventListener('click', downloadPdf); // Initial setup resetBattle(); // Set initial health bar widths and log content // Set initial health bar widths based on default input values // This is also done inside resetBattle, but doing it here on load ensures // the initial state is correct if reset isn't clicked immediately. initialHealth1 = parseInt(monster1HealthInput.value, 10) || 1; initialHealth2 = parseInt(monster2HealthInput.value, 10) || 1; updateHealthBar(monster1HealthBar, initialHealth1, initialHealth1); updateHealthBar(monster2HealthBar, initialHealth2, initialHealth2); });Key Features
Dynamic Monster Selection: Randomly selects two monsters from a predefined list for each battle.
Health Bars: Visualizes the health of each monster with animated health bars.
Battle Log: Displays a real-time log of attacks and damage during the battle.
Winner Announcement: Declares the winner at the end of the battle.
PDF Download: Allows users to download the battle log as a PDF with the same color scheme.
Benefits for End Users
Entertainment: Provides a fun and engaging way to simulate monster battles.
Interactive Experience: Real-time updates and animations make the battle feel dynamic and exciting.
Portable Results: The PDF download feature allows users to save and share battle logs offline.
User-Friendly: Simple interface and intuitive design make the tool easy to use for everyone.
