| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>RetroVault - Classic Gaming Hub</title> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <script src="https://kit.fontawesome.com/a076d05399.js" crossorigin="anonymous"></script> |
| <script src="https://cdn.jsdelivr.net/npm/jsnes@1.0.0/dist/jsnes.min.js"></script> |
| <script src="https://cdn.jsdelivr.net/npm/gba.js@1.0.0/build/gba.min.js"></script> |
| <style> |
| @font-face { |
| font-family: 'Press Start 2P'; |
| src: url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap'); |
| } |
| |
| body { |
| font-family: 'Press Start 2P', cursive; |
| background-color: #0f0f1a; |
| color: #e0e0ff; |
| overflow-x: hidden; |
| } |
| |
| .neon-text { |
| text-shadow: 0 0 5px #fff, 0 0 10px #fff, 0 0 15px #ff00de, 0 0 20px #ff00de; |
| } |
| |
| .neon-box { |
| box-shadow: 0 0 5px #fff, 0 0 10px #fff, 0 0 15px #0073e6, 0 0 20px #0073e6; |
| } |
| |
| .pixel-border { |
| border-image: repeating-linear-gradient(90deg, #ff00de, #ff00de 10px, #0073e6 10px, #0073e6 20px) 5; |
| border-width: 4px; |
| border-style: solid; |
| } |
| |
| .tab-active { |
| background: linear-gradient(to bottom, #ff00de, #0073e6); |
| color: white; |
| transform: translateY(-5px); |
| } |
| |
| .scanlines { |
| position: relative; |
| } |
| |
| .scanlines:before { |
| content: ""; |
| position: absolute; |
| top: 0; |
| left: 0; |
| width: 100%; |
| height: 100%; |
| background: linear-gradient( |
| to bottom, |
| transparent 50%, |
| rgba(0, 0, 0, 0.2) 51% |
| ); |
| background-size: 100% 4px; |
| pointer-events: none; |
| z-index: 10; |
| } |
| |
| .dark-mode { |
| background-color: #0f0f1a; |
| color: #e0e0ff; |
| } |
| |
| .light-mode { |
| background-color: #f0f0ff; |
| color: #1a1a2e; |
| } |
| |
| |
| .btn-pixel { |
| position: relative; |
| display: inline-block; |
| padding: 10px 20px; |
| color: white; |
| background: #6a5acd; |
| text-decoration: none; |
| border-radius: 0; |
| border-bottom: 5px solid #483d8b; |
| transition: transform 0.1s, border-bottom 0.1s; |
| } |
| |
| .btn-pixel:hover { |
| transform: translate(0, 2px); |
| border-bottom: 3px solid #483d8b; |
| } |
| |
| .btn-pixel:active { |
| transform: translate(0, 5px); |
| border-bottom: 0px solid #483d8b; |
| } |
| |
| |
| .crt-effect { |
| position: relative; |
| overflow: hidden; |
| } |
| |
| .crt-effect:after { |
| content: " "; |
| display: block; |
| position: absolute; |
| top: 0; |
| left: 0; |
| bottom: 0; |
| right: 0; |
| background: rgba(18, 16, 16, 0.1); |
| opacity: 0.2; |
| z-index: 2; |
| pointer-events: none; |
| } |
| |
| .crt-effect:before { |
| content: " "; |
| display: block; |
| position: absolute; |
| top: 0; |
| left: 0; |
| bottom: 0; |
| right: 0; |
| background: linear-gradient(rgba(18, 16, 16, 0) 50%, rgba(0, 0, 0, 0.25) 50%), linear-gradient(90deg, rgba(255, 0, 0, 0.06), rgba(0, 255, 0, 0.02), rgba(0, 0, 255, 0.06)); |
| z-index: 2; |
| background-size: 100% 2px, 3px 100%; |
| pointer-events: none; |
| } |
| |
| |
| #nesCanvas { |
| image-rendering: -moz-crisp-edges; |
| image-rendering: -webkit-crisp-edges; |
| image-rendering: pixelated; |
| image-rendering: crisp-edges; |
| width: 100%; |
| height: 100%; |
| } |
| |
| |
| .loader { |
| border: 5px solid #f3f3f3; |
| border-top: 5px solid #ff00de; |
| border-radius: 50%; |
| width: 50px; |
| height: 50px; |
| animation: spin 1s linear infinite; |
| margin: 20px auto; |
| } |
| |
| @keyframes spin { |
| 0% { transform: rotate(0deg); } |
| 100% { transform: rotate(360deg); } |
| } |
| |
| |
| .status-message { |
| background-color: rgba(0, 0, 0, 0.7); |
| color: white; |
| padding: 10px; |
| border-radius: 5px; |
| text-align: center; |
| margin-top: 10px; |
| font-size: 0.8rem; |
| } |
| |
| .status-success { |
| color: #00ff00; |
| } |
| |
| .status-error { |
| color: #ff0000; |
| } |
| </style> |
| </head> |
| <body class="dark-mode"> |
| <div class="container mx-auto px-4 py-8"> |
| |
| <header class="text-center mb-8"> |
| <h1 class="text-5xl md:text-6xl font-bold mb-4 neon-text">RetroVault</h1> |
| <p class="text-xl md:text-2xl">Your classic gaming emporium</p> |
| |
| |
| <div class="flex justify-center mt-4"> |
| <button id="themeToggle" class="btn-pixel px-6 py-2 rounded-full"> |
| <i class="fas fa-moon"></i> Dark Mode |
| </button> |
| </div> |
| </header> |
| |
| |
| <div class="bg-gray-900 bg-opacity-70 rounded-lg p-6 neon-box"> |
| |
| <div class="flex mb-6 border-b-2 border-purple-500"> |
| <button id="nesTab" class="tab-active px-6 py-3 font-bold rounded-t-lg mr-2 transition-all"> |
| <i class="fas fa-gamepad mr-2"></i> NES |
| </button> |
| <button id="gbaTab" class="px-6 py-3 font-bold rounded-t-lg mr-2 transition-all hover:bg-purple-900 hover:text-white"> |
| <i class="fas fa-mobile-alt mr-2"></i> GBA |
| </button> |
| <button id="controlsTab" class="px-6 py-3 font-bold rounded-t-lg transition-all hover:bg-purple-900 hover:text-white"> |
| <i class="fas fa-keyboard mr-2"></i> Controls |
| </button> |
| </div> |
| |
| |
| <div id="nesSection" class="console-section"> |
| <div class="grid grid-cols-1 md:grid-cols-3 gap-6"> |
| |
| <div class="md:col-span-2"> |
| <div class="bg-black p-4 rounded-lg crt-effect scanlines"> |
| <div id="nesEmulator" class="mx-auto" style="width: 100%; height: 400px;"> |
| <canvas id="nesCanvas" width="256" height="240"></canvas> |
| </div> |
| <div id="nesStatus" class="status-message">Ready to load NES ROM</div> |
| </div> |
| |
| <div class="flex flex-wrap justify-center mt-4 gap-2"> |
| <button id="nesLoad" class="btn-pixel"> |
| <i class="fas fa-folder-open mr-2"></i> Load ROM |
| </button> |
| <button id="nesSaveState" class="btn-pixel"> |
| <i class="fas fa-save mr-2"></i> Save State |
| </button> |
| <button id="nesLoadState" class="btn-pixel"> |
| <i class="fas fa-file-upload mr-2"></i> Load State |
| </button> |
| <button id="nesFullscreen" class="btn-pixel"> |
| <i class="fas fa-expand mr-2"></i> Fullscreen |
| </button> |
| <button id="nesReset" class="btn-pixel"> |
| <i class="fas fa-redo mr-2"></i> Reset |
| </button> |
| <button id="nesPause" class="btn-pixel"> |
| <i class="fas fa-pause mr-2"></i> Pause |
| </button> |
| </div> |
| </div> |
| |
| |
| <div class="bg-gray-800 bg-opacity-70 rounded-lg p-4 pixel-border"> |
| <h3 class="text-xl font-bold mb-4 text-center neon-text">Game Info</h3> |
| <div id="nesGameInfo" class="text-sm"> |
| <p class="mb-2"><span class="text-purple-400">No game loaded</span></p> |
| <p class="mb-2">Controls:</p> |
| <ul class="list-disc pl-5"> |
| <li>Arrow Keys: D-Pad</li> |
| <li>Z: A Button</li> |
| <li>X: B Button</li> |
| <li>Enter: Start</li> |
| <li>Shift: Select</li> |
| </ul> |
| <p class="mt-4 text-xs text-gray-400">Upload your own NES ROMs to play</p> |
| <div class="mt-4"> |
| <h4 class="text-lg font-bold mb-2 text-purple-400">Sample ROMs</h4> |
| <button id="loadSample1" class="btn-pixel text-xs px-3 py-1 mb-2">Super Mario Bros</button> |
| <button id="loadSample2" class="btn-pixel text-xs px-3 py-1 mb-2">Donkey Kong</button> |
| <button id="loadSample3" class="btn-pixel text-xs px-3 py-1">The Legend of Zelda</button> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
| |
| |
| <div id="gbaSection" class="console-section hidden"> |
| <div class="grid grid-cols-1 md:grid-cols-3 gap-6"> |
| |
| <div class="md:col-span-2"> |
| <div class="bg-black p-4 rounded-lg crt-effect scanlines"> |
| <div id="gbaEmulator" class="mx-auto" style="width: 100%; height: 400px;"> |
| <div class="loader"></div> |
| <p class="text-center">GBA emulator loading...</p> |
| </div> |
| </div> |
| |
| <div class="flex flex-wrap justify-center mt-4 gap-2"> |
| <button id="gbaLoad" class="btn-pixel"> |
| <i class="fas fa-folder-open mr-2"></i> Load ROM |
| </button> |
| <button id="gbaSaveState" class="btn-pixel"> |
| <i class="fas fa-save mr-2"></i> Save State |
| </button> |
| <button id="gbaLoadState" class="btn-pixel"> |
| <i class="fas fa-file-upload mr-2"></i> Load State |
| </button> |
| <button id="gbaFullscreen" class="btn-pixel"> |
| <i class="fas fa-expand mr-2"></i> Fullscreen |
| </button> |
| <button id="gbaReset" class="btn-pixel"> |
| <i class="fas fa-redo mr-2"></i> Reset |
| </button> |
| </div> |
| </div> |
| |
| |
| <div class="bg-gray-800 bg-opacity-70 rounded-lg p-4 pixel-border"> |
| <h3 class="text-xl font-bold mb-4 text-center neon-text">Game Info</h3> |
| <div id="gbaGameInfo" class="text-sm"> |
| <p class="mb-2"><span class="text-purple-400">No game loaded</span></p> |
| <p class="mb-2">Controls:</p> |
| <ul class="list-disc pl-5"> |
| <li>Arrow Keys: D-Pad</li> |
| <li>A: A Button</li> |
| <li>S: B Button</li> |
| <li>Enter: Start</li> |
| <li>Shift: Select</li> |
| <li>Q: L Button</li> |
| <li>W: R Button</li> |
| </ul> |
| <p class="mt-4 text-xs text-gray-400">Upload your own GBA ROMs to play</p> |
| </div> |
| </div> |
| </div> |
| </div> |
| |
| |
| <div id="controlsSection" class="console-section hidden"> |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-6"> |
| |
| <div class="bg-gray-800 bg-opacity-70 rounded-lg p-4 pixel-border"> |
| <h3 class="text-xl font-bold mb-4 text-center neon-text">Keyboard Controls</h3> |
| <div class="grid grid-cols-2 gap-4"> |
| <div> |
| <h4 class="text-lg font-bold mb-2 text-purple-400">NES</h4> |
| <ul class="text-sm"> |
| <li class="mb-1">Arrow Keys: D-Pad</li> |
| <li class="mb-1">Z: A Button</li> |
| <li class="mb-1">X: B Button</li> |
| <li class="mb-1">Enter: Start</li> |
| <li class="mb-1">Shift: Select</li> |
| </ul> |
| </div> |
| <div> |
| <h4 class="text-lg font-bold mb-2 text-purple-400">GBA</h4> |
| <ul class="text-sm"> |
| <li class="mb-1">Arrow Keys: D-Pad</li> |
| <li class="mb-1">A: A Button</li> |
| <li class="mb-1">S: B Button</li> |
| <li class="mb-1">Enter: Start</li> |
| <li class="mb-1">Shift: Select</li> |
| <li class="mb-1">Q: L Button</li> |
| <li class="mb-1">W: R Button</li> |
| </ul> |
| </div> |
| </div> |
| </div> |
| |
| |
| <div class="bg-gray-800 bg-opacity-70 rounded-lg p-4 pixel-border"> |
| <h3 class="text-xl font-bold mb-4 text-center neon-text">Gamepad Support</h3> |
| <div id="gamepadInfo" class="text-sm"> |
| <p class="mb-4">Connect a gamepad and press any button to enable.</p> |
| <p class="mb-2">Supported gamepads:</p> |
| <ul class="list-disc pl-5"> |
| <li class="mb-1">Xbox 360/One</li> |
| <li class="mb-1">PlayStation 3/4</li> |
| <li class="mb-1">Generic USB gamepads</li> |
| </ul> |
| <p class="mt-4 text-xs text-gray-400">Buttons will map automatically to standard layout</p> |
| </div> |
| </div> |
| </div> |
| |
| <div class="mt-6 bg-gray-800 bg-opacity-70 rounded-lg p-4 pixel-border"> |
| <h3 class="text-xl font-bold mb-4 text-center neon-text">Customize Controls</h3> |
| <div class="text-center"> |
| <button id="remapControls" class="btn-pixel px-6 py-2"> |
| <i class="fas fa-keyboard mr-2"></i> Remap Keys |
| </button> |
| <p class="mt-2 text-xs text-gray-400">Coming soon: Custom key mapping for keyboard and gamepad</p> |
| </div> |
| </div> |
| </div> |
| </div> |
| |
| |
| <footer class="mt-8 text-center text-sm text-gray-500"> |
| <p>RetroVault - Play classic games in your browser</p> |
| <p class="mt-1">Note: You must provide your own legally obtained ROM files</p> |
| </footer> |
| </div> |
|
|
| |
| <input type="file" id="nesFileInput" accept=".nes" class="hidden"> |
| <input type="file" id="gbaFileInput" accept=".gba" class="hidden"> |
| |
| <script> |
| |
| const themeToggle = document.getElementById('themeToggle'); |
| const body = document.body; |
| |
| themeToggle.addEventListener('click', () => { |
| if (body.classList.contains('dark-mode')) { |
| body.classList.remove('dark-mode'); |
| body.classList.add('light-mode'); |
| themeToggle.innerHTML = '<i class="fas fa-sun mr-2"></i> Light Mode'; |
| } else { |
| body.classList.remove('light-mode'); |
| body.classList.add('dark-mode'); |
| themeToggle.innerHTML = '<i class="fas fa-moon mr-2"></i> Dark Mode'; |
| } |
| }); |
| |
| |
| const nesTab = document.getElementById('nesTab'); |
| const gbaTab = document.getElementById('gbaTab'); |
| const controlsTab = document.getElementById('controlsTab'); |
| |
| const nesSection = document.getElementById('nesSection'); |
| const gbaSection = document.getElementById('gbaSection'); |
| const controlsSection = document.getElementById('controlsSection'); |
| |
| function resetTabs() { |
| nesTab.classList.remove('tab-active'); |
| gbaTab.classList.remove('tab-active'); |
| controlsTab.classList.remove('tab-active'); |
| |
| nesTab.classList.add('hover:bg-purple-900', 'hover:text-white'); |
| gbaTab.classList.add('hover:bg-purple-900', 'hover:text-white'); |
| controlsTab.classList.add('hover:bg-purple-900', 'hover:text-white'); |
| |
| nesSection.classList.add('hidden'); |
| gbaSection.classList.add('hidden'); |
| controlsSection.classList.add('hidden'); |
| } |
| |
| nesTab.addEventListener('click', () => { |
| resetTabs(); |
| nesTab.classList.add('tab-active'); |
| nesTab.classList.remove('hover:bg-purple-900', 'hover:text-white'); |
| nesSection.classList.remove('hidden'); |
| }); |
| |
| gbaTab.addEventListener('click', () => { |
| resetTabs(); |
| gbaTab.classList.add('tab-active'); |
| gbaTab.classList.remove('hover:bg-purple-900', 'hover:text-white'); |
| gbaSection.classList.remove('hidden'); |
| }); |
| |
| controlsTab.addEventListener('click', () => { |
| resetTabs(); |
| controlsTab.classList.add('tab-active'); |
| controlsTab.classList.remove('hover:bg-purple-900', 'hover:text-white'); |
| controlsSection.classList.remove('hidden'); |
| }); |
| |
| |
| const nes = new jsnes.NES({ |
| onFrame: function(frameBuffer) { |
| const canvas = document.getElementById('nesCanvas'); |
| const context = canvas.getContext('2d'); |
| const imageData = context.createImageData(canvas.width, canvas.height); |
| |
| for (let i = 0; i < frameBuffer.length; i++) { |
| imageData.data[i * 4] = frameBuffer[i] >> 16 & 0xFF; |
| imageData.data[i * 4 + 1] = frameBuffer[i] >> 8 & 0xFF; |
| imageData.data[i * 4 + 2] = frameBuffer[i] & 0xFF; |
| imageData.data[i * 4 + 3] = 0xFF; |
| } |
| |
| context.putImageData(imageData, 0, 0); |
| }, |
| onAudioSample: function(left, right) { |
| |
| } |
| }); |
| |
| |
| const keyMap = { |
| 38: jsnes.Controller.BUTTON_UP, |
| 40: jsnes.Controller.BUTTON_DOWN, |
| 37: jsnes.Controller.BUTTON_LEFT, |
| 39: jsnes.Controller.BUTTON_RIGHT, |
| 90: jsnes.Controller.BUTTON_A, |
| 88: jsnes.Controller.BUTTON_B, |
| 13: jsnes.Controller.BUTTON_START, |
| 16: jsnes.Controller.BUTTON_SELECT |
| }; |
| |
| document.addEventListener('keydown', (e) => { |
| if (!nesSection.classList.contains('hidden')) { |
| const button = keyMap[e.keyCode]; |
| if (button !== undefined) { |
| nes.buttonDown(1, button); |
| e.preventDefault(); |
| } |
| } |
| }); |
| |
| document.addEventListener('keyup', (e) => { |
| if (!nesSection.classList.contains('hidden')) { |
| const button = keyMap[e.keyCode]; |
| if (button !== undefined) { |
| nes.buttonUp(1, button); |
| e.preventDefault(); |
| } |
| } |
| }); |
| |
| |
| const nesFileInput = document.getElementById('nesFileInput'); |
| const nesLoadBtn = document.getElementById('nesLoad'); |
| const nesStatus = document.getElementById('nesStatus'); |
| |
| nesLoadBtn.addEventListener('click', () => { |
| nesFileInput.click(); |
| }); |
| |
| nesFileInput.addEventListener('change', (e) => { |
| const file = e.target.files[0]; |
| if (!file) return; |
| |
| |
| nesStatus.textContent = `Loading ${file.name}...`; |
| nesStatus.className = 'status-message'; |
| |
| const reader = new FileReader(); |
| reader.onload = (e) => { |
| try { |
| const rom = new Uint8Array(e.target.result); |
| nes.loadROM(rom); |
| |
| |
| document.getElementById('nesGameInfo').innerHTML = ` |
| <p class="mb-2"><span class="text-purple-400">${file.name}</span></p> |
| <p class="mb-2">Controls:</p> |
| <ul class="list-disc pl-5"> |
| <li>Arrow Keys: D-Pad</li> |
| <li>Z: A Button</li> |
| <li>X: B Button</li> |
| <li>Enter: Start</li> |
| <li>Shift: Select</li> |
| </ul> |
| <p class="mt-4 text-xs text-gray-400">Press Save State to save your progress</p> |
| <div class="mt-4"> |
| <h4 class="text-lg font-bold mb-2 text-purple-400">Sample ROMs</h4> |
| <button id="loadSample1" class="btn-pixel text-xs px-3 py-1 mb-2">Super Mario Bros</button> |
| <button id="loadSample2" class="btn-pixel text-xs px-3 py-1 mb-2">Donkey Kong</button> |
| <button id="loadSample3" class="btn-pixel text-xs px-3 py-1">The Legend of Zelda</button> |
| </div> |
| `; |
| |
| |
| nesStatus.textContent = `Successfully loaded ${file.name}`; |
| nesStatus.className = 'status-message status-success'; |
| |
| |
| if (!nesInterval) { |
| nesInterval = setInterval(() => { |
| nes.frame(); |
| }, 1000 / 60); |
| } |
| |
| } catch (error) { |
| nesStatus.textContent = `Error loading ROM: ${error.message}`; |
| nesStatus.className = 'status-message status-error'; |
| console.error('Error loading ROM:', error); |
| } |
| }; |
| |
| reader.onerror = () => { |
| nesStatus.textContent = 'Error reading file'; |
| nesStatus.className = 'status-message status-error'; |
| }; |
| |
| reader.readAsArrayBuffer(file); |
| }); |
| |
| |
| const sampleRoms = { |
| mario: "BASE64_ENCODED_MARIO_ROM_DATA", |
| donkeykong: "BASE64_ENCODED_DONKEY_KONG_ROM_DATA", |
| zelda: "BASE64_ENCODED_ZELDA_ROM_DATA" |
| }; |
| |
| document.getElementById('loadSample1').addEventListener('click', () => { |
| |
| nesStatus.textContent = "Sample ROM loading not implemented in this demo"; |
| nesStatus.className = 'status-message status-error'; |
| }); |
| |
| document.getElementById('loadSample2').addEventListener('click', () => { |
| nesStatus.textContent = "Sample ROM loading not implemented in this demo"; |
| nesStatus.className = 'status-message status-error'; |
| }); |
| |
| document.getElementById('loadSample3').addEventListener('click', () => { |
| nesStatus.textContent = "Sample ROM loading not implemented in this demo"; |
| nesStatus.className = 'status-message status-error'; |
| }); |
| |
| |
| let nesState = null; |
| let nesInterval = null; |
| let isPaused = false; |
| |
| document.getElementById('nesSaveState').addEventListener('click', () => { |
| try { |
| nesState = nes.toJSON(); |
| nesStatus.textContent = "Game state saved!"; |
| nesStatus.className = 'status-message status-success'; |
| } catch (error) { |
| nesStatus.textContent = `Error saving state: ${error.message}`; |
| nesStatus.className = 'status-message status-error'; |
| } |
| }); |
| |
| document.getElementById('nesLoadState').addEventListener('click', () => { |
| if (nesState) { |
| try { |
| nes.fromJSON(nesState); |
| nesStatus.textContent = "Game state loaded!"; |
| nesStatus.className = 'status-message status-success'; |
| } catch (error) { |
| nesStatus.textContent = `Error loading state: ${error.message}`; |
| nesStatus.className = 'status-message status-error'; |
| } |
| } else { |
| nesStatus.textContent = "No saved state found!"; |
| nesStatus.className = 'status-message status-error'; |
| } |
| }); |
| |
| |
| document.getElementById('nesPause').addEventListener('click', () => { |
| if (isPaused) { |
| |
| nesInterval = setInterval(() => { |
| nes.frame(); |
| }, 1000 / 60); |
| document.getElementById('nesPause').innerHTML = '<i class="fas fa-pause mr-2"></i> Pause'; |
| nesStatus.textContent = "Game resumed"; |
| nesStatus.className = 'status-message status-success'; |
| } else { |
| |
| clearInterval(nesInterval); |
| nesInterval = null; |
| document.getElementById('nesPause').innerHTML = '<i class="fas fa-play mr-2"></i> Resume'; |
| nesStatus.textContent = "Game paused"; |
| nesStatus.className = 'status-message'; |
| } |
| isPaused = !isPaused; |
| }); |
| |
| |
| document.getElementById('nesFullscreen').addEventListener('click', () => { |
| const emulator = document.getElementById('nesEmulator'); |
| if (emulator.requestFullscreen) { |
| emulator.requestFullscreen(); |
| } else if (emulator.webkitRequestFullscreen) { |
| emulator.webkitRequestFullscreen(); |
| } else if (emulator.msRequestFullscreen) { |
| emulator.msRequestFullscreen(); |
| } |
| }); |
| |
| |
| document.getElementById('nesReset').addEventListener('click', () => { |
| if (nesInterval) { |
| clearInterval(nesInterval); |
| nesInterval = null; |
| } |
| |
| nes.reset(); |
| nesStatus.textContent = "Game reset"; |
| nesStatus.className = 'status-message'; |
| |
| |
| nesInterval = setInterval(() => { |
| nes.frame(); |
| }, 1000 / 60); |
| }); |
| |
| |
| let gba = null; |
| let gbaRom = null; |
| |
| |
| const gbaFileInput = document.getElementById('gbaFileInput'); |
| const gbaLoadBtn = document.getElementById('gbaLoad'); |
| |
| gbaLoadBtn.addEventListener('click', () => { |
| gbaFileInput.click(); |
| }); |
| |
| gbaFileInput.addEventListener('change', (e) => { |
| const file = e.target.files[0]; |
| if (!file) return; |
| |
| const reader = new FileReader(); |
| reader.onload = (e) => { |
| const rom = new Uint8Array(e.target.result); |
| gbaRom = rom; |
| |
| |
| if (!gba) { |
| gba = new GameBoyAdvance(); |
| |
| |
| const canvas = document.createElement('canvas'); |
| canvas.width = 240; |
| canvas.height = 160; |
| const context = canvas.getContext('2d'); |
| document.getElementById('gbaEmulator').innerHTML = ''; |
| document.getElementById('gbaEmulator').appendChild(canvas); |
| |
| gba.setCanvas(canvas); |
| |
| |
| |
| |
| |
| gba.setInterval(() => { |
| gba.runStable(); |
| }, 0); |
| } |
| |
| |
| gba.loadRom(rom); |
| |
| |
| document.getElementById('gbaGameInfo').innerHTML = ` |
| <p class="mb-2"><span class="text-purple-400">${file.name}</span></p> |
| <p class="mb-2">Controls:</p> |
| <ul class="list-disc pl-5"> |
| <li>Arrow Keys: D-Pad</li> |
| <li>A: A Button</li> |
| <li>S: B Button</li> |
| <li>Enter: Start</li> |
| <li>Shift: Select</li> |
| <li>Q: L Button</li> |
| <li>W: R Button</li> |
| </ul> |
| <p class="mt-4 text-xs text-gray-400">Press Save State to save your progress</p> |
| `; |
| }; |
| reader.readAsArrayBuffer(file); |
| }); |
| |
| |
| document.addEventListener('keydown', (e) => { |
| if (!gba || gbaSection.classList.contains('hidden')) return; |
| |
| |
| |
| |
| switch(e.keyCode) { |
| case 38: break; |
| case 40: break; |
| case 37: break; |
| case 39: break; |
| case 65: break; |
| case 83: break; |
| case 13: break; |
| case 16: break; |
| case 81: break; |
| case 87: break; |
| } |
| }); |
| |
| |
| let gbaState = null; |
| |
| document.getElementById('gbaSaveState').addEventListener('click', () => { |
| if (!gba) { |
| alert('No game loaded!'); |
| return; |
| } |
| |
| |
| |
| alert('Game state saved! (Placeholder - would save state in real implementation)'); |
| }); |
| |
| document.getElementById('gbaLoadState').addEventListener('click', () => { |
| if (!gbaState) { |
| alert('No saved state found!'); |
| return; |
| } |
| |
| |
| |
| alert('Game state loaded! (Placeholder - would load state in real implementation)'); |
| }); |
| |
| |
| document.getElementById('gbaFullscreen').addEventListener('click', () => { |
| const emulator = document.getElementById('gbaEmulator'); |
| if (emulator.requestFullscreen) { |
| emulator.requestFullscreen(); |
| } else if (emulator.webkitRequestFullscreen) { |
| emulator.webkitRequestFullscreen(); |
| } else if (emulator.msRequestFullscreen) { |
| emulator.msRequestFullscreen(); |
| } |
| }); |
| |
| |
| document.getElementById('gbaReset').addEventListener('click', () => { |
| if (!gba || !gbaRom) { |
| alert('No game loaded!'); |
| return; |
| } |
| |
| gba.loadRom(gbaRom); |
| }); |
| |
| |
| window.addEventListener("gamepadconnected", (e) => { |
| document.getElementById('gamepadInfo').innerHTML = ` |
| <p class="mb-2"><span class="text-green-400">Gamepad connected:</span> ${e.gamepad.id}</p> |
| <p class="mb-2">Standard mapping:</p> |
| <ul class="list-disc pl-5"> |
| <li>D-Pad: D-Pad</li> |
| <li>A Button: A</li> |
| <li>B Button: B</li> |
| <li>Start: Start</li> |
| <li>Select: Select</li> |
| <li>Shoulder Buttons: L/R</li> |
| </ul> |
| <p class="mt-4 text-xs text-gray-400">Press buttons to control the game</p> |
| `; |
| |
| |
| if (!gamepadPolling) { |
| gamepadPolling = true; |
| pollGamepads(); |
| } |
| }); |
| |
| window.addEventListener("gamepaddisconnected", (e) => { |
| document.getElementById('gamepadInfo').innerHTML = ` |
| <p class="mb-4">Connect a gamepad and press any button to enable.</p> |
| <p class="mb-2">Supported gamepads:</p> |
| <ul class="list-disc pl-5"> |
| <li class="mb-1">Xbox 360/One</li> |
| <li class="mb-1">PlayStation 3/4</li> |
| <li class="mb-1">Generic USB gamepads</li> |
| </ul> |
| <p class="mt-4 text-xs text-gray-400">Buttons will map automatically to standard layout</p> |
| `; |
| }); |
| |
| let gamepadPolling = false; |
| let prevGamepadState = {}; |
| |
| function pollGamepads() { |
| if (!gamepadPolling) return; |
| |
| const gamepads = navigator.getGamepads ? navigator.getGamepads() : []; |
| |
| for (let i = 0; i < gamepads.length; i++) { |
| const gamepad = gamepads[i]; |
| if (!gamepad) continue; |
| |
| |
| if (!nesSection.classList.contains('hidden')) { |
| |
| if (gamepad.axes[1] < -0.5) nes.buttonDown(1, jsnes.Controller.BUTTON_UP); |
| else nes.buttonUp(1, jsnes.Controller.BUTTON_UP); |
| |
| if (gamepad.axes[1] > 0.5) nes.buttonDown(1, jsnes.Controller.BUTTON_DOWN); |
| else nes.buttonUp(1, jsnes.Controller.BUTTON_DOWN); |
| |
| if (gamepad.axes[0] < -0.5) nes.buttonDown(1, jsnes.Controller.BUTTON_LEFT); |
| else nes.buttonUp(1, jsnes.Controller.BUTTON_LEFT); |
| |
| if (gamepad.axes[0] > 0.5) nes.buttonDown(1, jsnes.Controller.BUTTON_RIGHT); |
| else nes.buttonUp(1, jsnes.Controller.BUTTON_RIGHT); |
| |
| |
| if (gamepad.buttons[0] && gamepad.buttons[0].pressed) nes.buttonDown(1, jsnes.Controller.BUTTON_A); |
| else nes.buttonUp(1, jsnes.Controller.BUTTON_A); |
| |
| if (gamepad.buttons[1] && gamepad.buttons[1].pressed) nes.buttonDown(1, jsnes.Controller.BUTTON_B); |
| else nes.buttonUp(1, jsnes.Controller.BUTTON_B); |
| |
| if (gamepad.buttons[9] && gamepad.buttons[9].pressed) nes.buttonDown(1, jsnes.Controller.BUTTON_START); |
| else nes.buttonUp(1, jsnes.Controller.BUTTON_START); |
| |
| if (gamepad.buttons[8] && gamepad.buttons[8].pressed) nes.buttonDown(1, jsnes.Controller.BUTTON_SELECT); |
| else nes.buttonUp(1, jsnes.Controller.BUTTON_SELECT); |
| } |
| |
| |
| |
| } |
| |
| requestAnimationFrame(pollGamepads); |
| } |
| |
| |
| document.getElementById('remapControls').addEventListener('click', () => { |
| alert('Key remapping feature coming soon!'); |
| }); |
| </script> |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=pijou/emulator" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> |
| </html> |