class EmulatorJS { version = { a5200: 1, beetle_vb: 1, desmume2015: 1, fbalpha2012_cps1: 1, fbalpha2012_cps2: 1, fceumm: 1, gambatte: 1, mame2003: 1, mednafen_psx_hw: 1, melonds: 1, mgba: 1, mupen64plus_next: 1, nestopia: 1, snes9x: 1 } getCore(generic) { const core = this.config.system; if (generic) { const options = { 'fceumm': 'nes', 'snes9x': 'snes', 'a5200': 'atari5200', 'gambatte': 'gb', 'mgba': 'gba', 'beetle_vb': 'vb', 'mupen64plus_next': 'n64', 'desmume2015': 'nds', 'mame2003': 'mame2003', 'fbalpha2012_cps1': 'arcade', 'fbalpha2012_cps2': 'arcade', 'mednafen_psx': 'psx', 'mednafen_psx_hw': 'psx', 'melonds': 'nds', 'nestopia': 'nes', 'opera': '3do' } return options[core] || core; } const options = { 'nes': 'fceumm', 'snes': 'snes9x', 'atari5200': 'a5200', 'gb': 'gambatte', 'gba': 'mgba', 'vb': 'beetle_vb', 'n64': 'mupen64plus_next', 'nds': 'desmume2015', 'mame2003': 'mame2003', 'arcade': 'fbalpha2012_cps1', // I need to find a more compatible arcade core 'psx': 'mednafen_psx_hw', '3do': 'opera' } return options[core] || core; } extensions = { 'fceumm': ['fds', 'nes', 'unif', 'unf'], 'snes9x': ['smc', 'sfc', 'swc', 'fig', 'bs', 'st'], 'a5200': ['a52', 'bin'], 'gambatte': ['gb', 'gbc', 'dmg'], 'mgba': ['gb', 'gbc', 'gba'], 'beetle_vb': ['vb', 'vboy', 'bin'], 'mupen64plus_next': ['n64', 'v64', 'z64', 'bin', 'u1', 'ndd', 'gb'], 'fbalpha2012_cps1': ['zip'], 'fbalpha2012_cps2': ['zip'], 'mame2003': ['zip'], 'desmume2015': ['nds', 'bin'], 'melonds': ['nds'], 'mednafen_psx': ['cue', 'toc', 'm3u', 'ccd', 'exe', 'pbp', 'chd'], 'mednafen_psx_hw': ['cue', 'toc', 'm3u', 'ccd', 'exe', 'pbp', 'chd'], 'nestopia': ['fds', 'nes', 'unif', 'unf'], 'opera': ['iso', 'bin', 'chd', 'cue'] } createElement(type) { return document.createElement(type); } addEventListener(element, listener, callback) { const listeners = listener.split(" "); let rv = []; for (let i=0; i { const progress = e.total ? ' '+Math.floor(e.loaded / e.total * 100).toString()+'%' : ' '+(e.loaded/1048576).toFixed(2)+'MB'; progressCB(progress); }); } xhr.onload = function() { if (xhr.readyState === xhr.DONE) { let data = xhr.response; try {data=JSON.parse(data)}catch(e){} cb({ data: data, headers: { "content-length": xhr.getResponseHeader('content-length') } }); } } if (opts.responseType) xhr.responseType = opts.responseType; xhr.onerror = () => cb(-1); xhr.open(opts.method, path, true); xhr.send(); } else { (async () => { //Most commonly blob: urls. Not sure what else it could be if (opts.method === 'HEAD') { cb({headers:{}}); return; } let res; try { res = await fetch(path); if (opts.type && opts.type.toLowerCase() === 'arraybuffer') { res = await res.arrayBuffer(); } else { res = await res.text(); try {res = JSON.parse(res)} catch(e) {} } } catch(e) { cb(-1); } if (path.startsWith('blob:')) URL.revokeObjectURL(path); cb({ data: res, headers: {} }); }) } } constructor(element, config) { window.EJS_TESTING = this; this.currentPopup = null; this.touch = false; this.debug = (window.EJS_DEBUG_XX === true); this.cheats = []; this.started = false; this.volume = 0.5; this.muted = false; this.paused = true; this.listeners = []; this.config = config; this.setElements(element); this.setColor(this.config.color || ""); if (this.config.adUrl) this.setupAds(this.config.adUrl); this.canvas = this.createElement('canvas'); this.canvas.classList.add('ejs_canvas'); this.bindListeners(); this.fullscreen = false; this.storage = { rom: new window.EJS_STORAGE("EmulatorJS-roms", "rom"), bios: new window.EJS_STORAGE("EmulatorJS-bios", "bios"), core: new window.EJS_STORAGE("EmulatorJS-core", "core") } this.game.classList.add("ejs_game"); this.createStartButton(); console.log(this) } setColor(color) { if (typeof color !== "string") color = ""; let getColor = function(color) { color = color.toLowerCase(); if (color && /^#([0-9a-fA-f]{3}|[0-9a-fA-f]{6})$/.test(color)) { if (color.length === 4) { let rv = '#' for (let i=1; i<4; i++) { rv += color.slice(i, i+1)+color.slice(i, i+1); } color = rv; } let rv = []; for (let i=1; i<7; i+=2) { rv.push(parseInt('0x'+color.slice(i, i+2), 16)); } return rv.join(", "); } return null; } if (!color || getColor(color) === null) { this.elements.parent.setAttribute("style", "--ejs-primary-color: 26,175,255;"); return; } this.elements.parent.setAttribute("style", "--ejs-primary-color:" + getColor(color) + ";"); } setupAds(ads) { const div = this.createElement("div"); div.classList.add("ejs_ad_iframe"); const frame = this.createElement("iframe"); frame.src = ads; frame.setAttribute("scrolling", "no"); frame.setAttribute("frameborder", "no"); frame.style.width = "300px"; frame.style.height = "250px"; const closeParent = this.createElement("div"); closeParent.classList.add("ejs_ad_close"); const closeButton = this.createElement("a"); closeParent.appendChild(closeButton); closeParent.setAttribute("hidden", ""); div.appendChild(closeParent); div.appendChild(frame); this.elements.parent.appendChild(div); this.addEventListener(closeButton, "click", () => { div.remove(); }) this.on("start", () => { closeParent.removeAttribute("hidden"); const time = (typeof this.config.adTimer === "number" && this.config.adTimer > 0) ? this.config.adTimer : 10000; if (this.config.adTimer === 0) return; setTimeout(() => { div.remove(); }, time); }) } functions = {}; on(event, func) { if (!Array.isArray(this.functions[event])) this.functions[event] = []; this.functions[event].push(func); } callEvent(event, data) { if (!Array.isArray(this.functions[event])) return 0; this.functions[event].forEach(e => e(data)); return this.functions[event].length; } setElements(element) { const game = this.createElement("div"); const elem = document.querySelector(element); elem.innerHTML = ""; elem.appendChild(game); this.game = game; this.elements = { main: this.game, parent: elem } this.elements.parent.classList.add("ejs_parent"); this.elements.parent.setAttribute("tabindex", -1); } // Start button createStartButton() { const button = this.createElement("div"); button.classList.add("ejs_start_button"); button.innerText = this.localization("Start Game"); this.elements.parent.appendChild(button); this.addEventListener(button, "touchstart", () => { this.touch = true; }) this.addEventListener(button, "click", this.startButtonClicked.bind(this)); } startButtonClicked(e) { e.preventDefault(); e.target.remove(); this.createText(); this.downloadGameCore(); } // End start button createText() { this.textElem = this.createElement("div"); this.textElem.classList.add("ejs_loading_text"); this.textElem.innerText = this.localization("Loading..."); this.elements.parent.appendChild(this.textElem); } localization(text) { //todo return text; } checkCompression(data, msg) { if (msg) { this.textElem.innerText = msg; } //to be put in another file function isCompressed(data) { //https://www.garykessler.net/library/file_sigs.html //todo. Use hex instead of numbers if ((data[0] === 80 && data[1] === 75) && ((data[2] === 3 && data[3] === 4) || (data[2] === 5 && data[3] === 6) || (data[2] === 7 && data[3] === 8))) { return 'zip'; } else if (data[0] === 55 && data[1] === 122 && data[2] === 188 && data[3] === 175 && data[4] === 39 && data[5] === 28) { return '7z'; } else if ((data[0] === 82 && data[1] === 97 && data[2] === 114 && data[3] === 33 && data[4] === 26 && data[5] === 7) && ((data[6] === 0) || (data[6] === 1 && data[7] == 0))) { return 'rar'; } } const createWorker = (path) => { return new Promise((resolve, reject) => { this.downloadFile(path, (res) => { if (res === -1) { this.textElem.innerText = "Error"; this.textElem.style.color = "red"; return; } const blob = new Blob([res.data], { 'type': 'application/javascript' }) const url = window.URL.createObjectURL(blob); resolve(new Worker(url)); }, null, false, {responseType: "arraybuffer", method: "GET"}); }) } const decompress7z = (file) => { return new Promise((resolve, reject) => { const files = {}; const onMessage = (data) => { if (!data.data) return; //data.data.t/ 4=progress, 2 is file, 1 is zip done if (data.data.t === 4 && msg) { const pg = data.data; const num = Math.floor(pg.current / pg.total * 100); if (isNaN(num)) return; const progress = ' '+num.toString()+'%'; this.textElem.innerText = msg + progress; } if (data.data.t === 2) { files[data.data.file] = data.data.data; } if (data.data.t === 1) { resolve(files); } } createWorker('compression/extract7z.js').then((worker) => { worker.onmessage = onMessage; worker.postMessage(file); //console.log(file); }) }) } const decompressRar = (file) => { return new Promise((resolve, reject) => { const files = {}; const onMessage = (data) => { if (!data.data) return; //data.data.t/ 4=progress, 2 is file, 1 is zip done if (data.data.t === 4 && msg) { const pg = data.data; const num = Math.floor(pg.current / pg.total * 100); if (isNaN(num)) return; const progress = ' '+num.toString()+'%'; this.textElem.innerText = msg + progress; } if (data.data.t === 2) { files[data.data.file] = data.data.data; } if (data.data.t === 1) { resolve(files); } } this.downloadFile("compression/libunrar.js", (res) => { if (res === -1) { this.textElem.innerText = "Error"; this.textElem.style.color = "red"; return; } const path = origin + this.config.dataPath + 'compression/libunrar.js.mem'; let data = '\nlet dataToPass = [];\nModule = {\n monitorRunDependencies: function(left) {\n if (left == 0) {\n setTimeout(function() {\n unrar(dataToPass, null);\n }, 100);\n }\n },\n onRuntimeInitialized: function() {\n },\n locateFile: function(file) {\n return \''+path+'\';\n }\n};\n'+res.data+'\nlet unrar = function(data, password) {\n let cb = function(fileName, fileSize, progress) {\n postMessage({"t":4,"current":progress,"total":fileSize, "name": fileName});\n };\n\n let rarContent = readRARContent(data.map(function(d) {\n return {\n name: d.name,\n content: new Uint8Array(d.content)\n }\n }), password, cb)\n let rec = function(entry) {\n if (entry.type === \'file\') {\n postMessage({"t":2,"file":entry.fullFileName,"size":entry.fileSize,"data":entry.fileContent});\n } else if (entry.type === \'dir\') {\n Object.keys(entry.ls).forEach(function(k) {\n rec(entry.ls[k]);\n })\n } else {\n throw "Unknown type";\n }\n }\n rec(rarContent);\n postMessage({"t":1});\n return rarContent;\n};\nonmessage = function(data) {\n dataToPass.push({name: \'test.rar\', content: data.data});\n};\n '; const blob = new Blob([data], { 'type': 'application/javascript' }) const url = window.URL.createObjectURL(blob); const worker = new Worker(url); worker.onmessage = onMessage; worker.postMessage(file); }, null, false, {responseType: "text", method: "GET"}); }) } const decompressZip = (file) => { return new Promise((resolve, reject) => { const files = {}; const onMessage = (data) => { //console.log(data); if (!data.data) return; //data.data.t/ 4=progress, 2 is file, 1 is zip done if (data.data.t === 4 && msg) { const pg = data.data; const num = Math.floor(pg.current / pg.total * 100); if (isNaN(num)) return; const progress = ' '+num.toString()+'%'; this.textElem.innerText = msg + progress; } if (data.data.t === 2) { files[data.data.file] = data.data.data; } if (data.data.t === 1) { resolve(files); } } createWorker('compression/extractzip.js').then((worker) => { worker.onmessage = onMessage; worker.postMessage(file); }) }) } const compression = isCompressed(data.slice(0, 10)); if (compression) { //Need to do zip and rar still if (compression === "7z") { return decompress7z(data); } else if (compression === "zip") { return decompressZip(data); } else if (compression === "rar") { return decompressRar(data); } } else { return new Promise(resolve => resolve({"!!notCompressedData": data})); } } downloadGameCore() { this.textElem.innerText = this.localization("Download Game Core"); const gotCore = (data) => { this.checkCompression(new Uint8Array(data), this.localization("Decompress Game Core")).then((data) => { //console.log(data); let js, wasm; for (let k in data) { if (k.endsWith(".wasm")) { wasm = data[k]; } else if (k.endsWith(".js")) { js = data[k]; } } this.initGameCore(js, wasm); }); } this.storage.core.get(this.getCore()+'-wasm.data').then((result) => { if (result && result.version === this.version[this.getCore()] && !this.debug) { gotCore(result.data); return; } this.downloadFile('cores/'+this.getCore()+'-wasm.data', (res) => { if (res === -1) { this.textElem.innerText = "Error"; this.textElem.style.color = "red"; return; } gotCore(res.data); this.storage.core.put(this.getCore()+'-wasm.data', { version: this.version[this.getCore()], data: res.data }); }, (progress) => { this.textElem.innerText = this.localization("Download Game Core") + progress; }, false, {responseType: "arraybuffer", method: "GET"}); }) } initGameCore(js, wasm) { this.initModule(wasm); let script = this.createElement("script"); script.src = URL.createObjectURL(new Blob([js], {type: "application/javascript"})); document.body.appendChild(script); } getBaseFileName() { //Only once game and core is loaded if (!this.started) return null; if (this.config.gameName) { const invalidCharacters = /[#<$+%>!`&*'|{}/\\?"=@:^\r\n]/ig; const name = this.config.gameName.replace(invalidCharacters, "").trim(); if (name) return name; } let parts = this.fileName.split("."); parts.splice(parts.length-1, 1); return parts.join("."); } downloadBios() { if (!this.config.biosUrl) { this.startGame(); return; } this.textElem.innerText = this.localization("Download Game BIOS"); const gotBios = (data) => { this.checkCompression(new Uint8Array(data), this.localization("Decompress Game BIOS")).then((data) => { for (const k in data) { if (k === "!!notCompressedData") { FS.writeFile(this.config.biosUrl.split('/').pop().split("#")[0].split("?")[0], data[k]); break; } if (k.endsWith('/')) continue; console.log(k.split('/').pop()); FS.writeFile(k.split('/').pop(), data[k]); } this.startGame(); }) } this.downloadFile(this.config.biosUrl, (res) => { this.storage.bios.get(this.config.biosUrl.split("/").pop()).then((result) => { if (result && result['content-length'] === res.headers['content-length'] && !this.debug) { gotBios(result.data); return; } this.downloadFile(this.config.biosUrl, (res) => { if (res === -1) { this.textElem.innerText = "Error"; this.textElem.style.color = "red"; return; } gotBios(res.data); this.storage.bios.put(this.config.biosUrl.split("/").pop(), { "content-length": res.headers['content-length'], data: res.data }) }, (progress) => { this.textElem.innerText = this.localization("Download Game BIOS") + progress; }, true, {responseType: "arraybuffer", method: "GET"}); }) }, null, true, {method: "HEAD"}) } downloadRom() { this.gameManager = new window.EJS_GameManager(this.Module); this.textElem.innerText = this.localization("Download Game Data"); const gotGameData = (data) => { if (['arcade', 'mame2003'].includes(this.getCore(true))) { this.fileName = this.config.gameUrl.split('/').pop().split("#")[0].split("?")[0]; FS.writeFile(this.fileName, data[k]); this.downloadBios(); return; } this.checkCompression(new Uint8Array(data), this.localization("Decompress Game Data")).then((data) => { for (const k in data) { if (k === "!!notCompressedData") { this.fileName = this.config.gameUrl.split('/').pop().split("#")[0].split("?")[0]; FS.writeFile(this.fileName, data[k]); break; } if (k.endsWith('/')) { FS.mkdir(k); continue; } if (!this.fileName || (this.extensions[this.getCore()].includes(k.split(".").pop()) && //always prefer m3u files for psx cores !(this.getCore(true) === "psx" && this.fileName.split(".").pop() === "m3u"))) { this.fileName = k; } console.log(k); FS.writeFile(k, data[k]); } this.downloadBios(); }); } this.downloadFile(this.config.gameUrl, (res) => { this.storage.rom.get(this.config.gameUrl.split("/").pop()).then((result) => { if (result && result['content-length'] === res.headers['content-length'] && !this.debug) { gotGameData(result.data); return; } this.downloadFile(this.config.gameUrl, (res) => { if (res === -1) { this.textElem.innerText = "Error"; this.textElem.style.color = "red"; return; } gotGameData(res.data); this.storage.rom.put(this.config.gameUrl.split("/").pop(), { "content-length": res.headers['content-length'], data: res.data }) }, (progress) => { this.textElem.innerText = this.localization("Download Game Data") + progress; }, true, {responseType: "arraybuffer", method: "GET"}); }) }, null, true, {method: "HEAD"}) } initModule(wasmData) { window.Module = { 'TOTAL_MEMORY': 0x10000000, 'noInitialRun': true, 'onRuntimeInitialized': this.downloadRom.bind(this), 'arguments': [], 'preRun': [], 'postRun': [], 'canvas': this.canvas, 'print': (msg) => { if (this.debug) { console.log(msg); } }, 'printErr': (msg) => { if (this.debug) { console.log(msg); } }, 'totalDependencies': 0, 'monitorRunDependencies': () => {}, 'locateFile': function(fileName) { console.log(fileName); if (fileName.endsWith(".wasm")) { return URL.createObjectURL(new Blob([wasmData], {type: "application/wasm"})); } } }; this.Module = window.Module; } startGame() { this.textElem.remove(); this.textElem = null; this.game.classList.remove("ejs_game"); this.game.appendChild(this.canvas); const args = []; if (this.debug) args.push('-v'); args.push('/'+this.fileName); console.log(args); this.Module.callMain(args); this.Module.resumeMainLoop(); this.started = true; this.paused = false; if (this.touch) { this.virtualGamepad.style.display = ""; } this.setupSettingsMenu(); this.handleResize(); this.updateCheatUI(); this.updateGamepadLabels(); this.setVolume(this.volume); this.elements.parent.focus(); this.callEvent("start"); } bindListeners() { this.createContextMenu(); this.createBottomMenuBar(); this.createControlSettingMenu(); this.createCheatsMenu() this.setVirtualGamepad(); this.addEventListener(this.elements.parent, "keydown keyup", this.keyChange.bind(this)); this.addEventListener(this.elements.parent, "mousedown mouseup click touchstart touchend touchcancel", (e) => { this.elements.parent.focus(); }) this.addEventListener(window, "resize", this.handleResize.bind(this)); //this.addEventListener(window, "blur", e => console.log(e), true); //TODO - add "click to make keyboard keys work" message this.gamepad = new GamepadHandler(); //https://github.com/ethanaobrien/Gamepad this.gamepad.on('connected', (e) => { if (!this.gamepadLabels) return; this.updateGamepadLabels(); }) this.gamepad.on('disconnected', (e) => { setTimeout(this.updateGamepadLabels.bind(this), 10); }) this.gamepad.on('axischanged', this.gamepadEvent.bind(this)); this.gamepad.on('buttondown', this.gamepadEvent.bind(this)); this.gamepad.on('buttonup', this.gamepadEvent.bind(this)); } updateGamepadLabels() { for (let i=0; i { if (this.started) { this.elements.contextmenu.style.display = "block"; this.elements.contextmenu.style.left = e.offsetX; this.elements.contextmenu.style.top = e.offsetY; } e.preventDefault(); }) const hideMenu = () => { this.elements.contextmenu.style.display = "none"; } this.addEventListener(this.elements.contextmenu, 'contextmenu', (e) => e.preventDefault()); this.addEventListener(this.elements.parent, 'contextmenu', (e) => e.preventDefault()); this.addEventListener(this.game, 'mousedown', hideMenu); const parent = this.createElement("ul"); const addButton = (title, hidden, functi0n) => { //
  • '+title+'
  • const li = this.createElement("li"); if (hidden) li.hidden = true; const a = this.createElement("a"); if (functi0n instanceof Function) { this.addEventListener(li, 'click', (e) => { e.preventDefault(); functi0n(); }); } a.href = "#"; a.onclick = "return false"; a.innerText = title; li.appendChild(a); parent.appendChild(li); hideMenu(); return li; } let screenshotUrl; const screenshot = addButton("Take Screenshot", false, () => { if (screenshotUrl) URL.revokeObjectURL(screenshotUrl); const screenshot = this.gameManager.screenshot(); const blob = new Blob([screenshot]); screenshotUrl = URL.createObjectURL(blob); const a = this.createElement("a"); a.href = screenshotUrl; const date = new Date(); a.download = this.getBaseFileName()+"-"+date.getMonth()+"-"+date.getDate()+"-"+date.getFullYear()+".png"; a.click(); hideMenu(); }); const qSave = addButton("Quick Save", false, () => { this.gameManager.quickSave(); hideMenu(); }); const qLoad = addButton("Quick Load", false, () => { this.gameManager.quickLoad(); hideMenu(); }); addButton("EmulatorJS", false, () => { hideMenu(); const body = this.createPopup("EmulatorJS", { "Close": () => { this.closePopup(); } }); body.innerText = "Todo. Write about, include tabs on side with licenses, links to docs/repo/discord?"; }); if (this.config.buttonOpts) { if (!this.config.buttonOpts.screenshot) screenshot.setAttribute("hidden", ""); if (!this.config.buttonOpts.quickSave) qSave.setAttribute("hidden", ""); if (!this.config.buttonOpts.quickLoad) qLoad.setAttribute("hidden", ""); } this.elements.contextmenu.appendChild(parent); this.elements.parent.appendChild(this.elements.contextmenu); } closePopup() { if (this.currentPopup !== null) { try { this.currentPopup.remove(); } catch(e){} this.currentPopup = null; } } //creates a full box popup. createPopup(popupTitle, buttons, hidden) { if (!hidden) this.closePopup(); const popup = this.createElement('div'); popup.classList.add("ejs_popup_container"); this.elements.parent.appendChild(popup); const title = this.createElement("h4"); title.innerText = popupTitle; const main = this.createElement("div"); main.classList.add("ejs_popup_body"); popup.appendChild(title); popup.appendChild(main); for (let k in buttons) { const button = this.createElement("a"); if (buttons[k] instanceof Function) { button.addEventListener("click", (e) => { buttons[k](); e.preventDefault(); }); } button.classList.add("ejs_button"); button.innerText = k; popup.appendChild(button); } if (!hidden) { this.currentPopup = popup; } else { popup.style.display = "none"; } return main; } selectFile() { return new Promise((resolve, reject) => { const file = this.createElement("input"); file.type = "file"; this.addEventListener(file, "change", (e) => { resolve(e.target.files[0]); }) file.click(); }) } isPopupOpen() { return this.cheatMenu.style.display !== "none" || this.controlMenu.style.display !== "none" || this.currentPopup !== null; } createBottomMenuBar() { this.elements.menu = this.createElement("div"); this.elements.menu.classList.add("ejs_menu_bar"); this.elements.menu.classList.add("ejs_menu_bar_hidden"); let timeout = null; const hide = () => { if (this.paused || this.settingsMenuOpen) return; this.elements.menu.classList.add("ejs_menu_bar_hidden"); } this.addEventListener(this.elements.parent, 'mousemove click', (e) => { if (!this.started) return; if (this.isPopupOpen()) return; if (timeout !== null) clearTimeout(timeout); timeout = setTimeout(hide, 3000); this.elements.menu.classList.remove("ejs_menu_bar_hidden"); }) this.menu = { close: () => { if (!this.started) return; if (timeout !== null) clearTimeout(timeout); this.elements.menu.classList.remove("ejs_menu_bar_hidden"); }, open: () => { if (!this.started) return; if (timeout !== null) clearTimeout(timeout); timeout = setTimeout(hide, 3000); this.elements.menu.classList.remove("ejs_menu_bar_hidden"); } } this.elements.parent.appendChild(this.elements.menu); //Now add buttons const addButton = (title, image, callback, element, both) => { const button = this.createElement("button"); button.type = "button"; const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); svg.setAttribute("role", "presentation"); svg.setAttribute("focusable", "false"); svg.innerHTML = image; const text = this.createElement("span"); text.innerText = title; text.classList.add("ejs_menu_text"); button.classList.add("ejs_menu_button"); button.appendChild(svg); button.appendChild(text); if (element) { element.appendChild(button); } else { this.elements.menu.appendChild(button); } if (callback instanceof Function) { this.addEventListener(button, 'click', callback); } return both ? [button, svg, text] : button; } //todo. Center text on not restart button const restartButton = addButton("Restart", '', () => { this.gameManager.restart(); }); const pauseButton = addButton("Pause", '', () => { this.togglePlaying(); }); const playButton = addButton("Play", '', () => { this.togglePlaying(); }); playButton.style.display = "none"; this.togglePlaying = () => { this.paused = !this.paused; if (this.paused) { pauseButton.style.display = "none"; playButton.style.display = ""; } else { pauseButton.style.display = ""; playButton.style.display = "none"; } this.gameManager.toggleMainLoop(this.paused ? 0 : 1); } let stateUrl; const saveState = addButton("Save State", '', async () => { const state = await this.gameManager.getState(); const called = this.callEvent("save", { screenshot: this.gameManager.screenshot(), state: state }); if (called > 0) return; if (stateUrl) URL.revokeObjectURL(stateUrl); const blob = new Blob([state]); stateUrl = URL.createObjectURL(blob); const a = this.createElement("a"); a.href = stateUrl; a.download = this.getBaseFileName()+".state"; a.click(); }); const loadState = addButton("Load State", '', async () => { const file = await this.selectFile(); const state = new Uint8Array(await file.arrayBuffer()); this.gameManager.loadState(state); }); const controlMenu = addButton("Control Settings", '', () => { this.controlMenu.style.display = ""; }); const cheatMenu = addButton("Cheats", '', () => { this.cheatMenu.style.display = ""; }); const cache = addButton("Cache Manager", '', () => { this.openCacheMenu(); }); const spacer = this.createElement("span"); spacer.style = "flex:1;"; this.elements.menu.appendChild(spacer); const volumeSettings = this.createElement("div"); volumeSettings.classList.add("ejs_volume_parent"); const muteButton = addButton("Mute", '', () => { muteButton.style.display = "none"; unmuteButton.style.display = ""; this.muted = true; this.setVolume(0); }, volumeSettings); const unmuteButton = addButton("Unmute", '', () => { muteButton.style.display = ""; unmuteButton.style.display = "none"; this.muted = false; this.setVolume(this.volume); }, volumeSettings); unmuteButton.style.display = "none"; const volumeSlider = this.createElement("input"); volumeSlider.setAttribute("data-range", "volume"); volumeSlider.setAttribute("type", "range"); volumeSlider.setAttribute("min", 0); volumeSlider.setAttribute("max", 1); volumeSlider.setAttribute("step", 0.01); volumeSlider.setAttribute("autocomplete", "off"); volumeSlider.setAttribute("role", "slider"); volumeSlider.setAttribute("aria-label", "Volume"); volumeSlider.setAttribute("aria-valuemin", 0); volumeSlider.setAttribute("aria-valuemax", 100); this.setVolume = (volume) => { volumeSlider.setAttribute("value", volume); volumeSlider.setAttribute("aria-valuenow", volume*100); volumeSlider.setAttribute("aria-valuetext", (volume*100).toFixed(1) + "%"); volumeSlider.setAttribute("style", "--value: "+volume*100+"%;margin-left: 5px;position: relative;z-index: 2;"); if (this.gameManager) this.gameManager.setVolume(volume); } this.setVolume(this.volume); this.addEventListener(volumeSlider, "change mousemove touchmove mousedown touchstart mouseup", (e) => { setTimeout(() => { this.volume = parseFloat(volumeSlider.value); this.setVolume(this.volume); unmuteButton.style.display = (this.volume === 0) ? "" : "none"; muteButton.style.display = (this.volume === 0) ? "none" : ""; }, 5); }) volumeSettings.appendChild(volumeSlider); //this.volume this.muted this.elements.menu.appendChild(volumeSettings); this.settingParent = this.createElement("div"); this.settingsMenuOpen = false; const settingButton = addButton("Settings", '', () => { this.settingsMenuOpen = !this.settingsMenuOpen; settingButton[1].classList.toggle("ejs_svg_rotate", this.settingsMenuOpen); settingButton[2].style.display = this.settingsMenuOpen ? "none" : ""; this.settingsMenu.style.display = this.settingsMenuOpen ? "" : "none"; }, this.settingParent, true); this.elements.menu.appendChild(this.settingParent); this.closeSettingsMenu = () => { if (!this.settingsMenu) return; this.settingsMenuOpen = false; settingButton[1].classList.toggle("ejs_svg_rotate", this.settingsMenuOpen); settingButton[2].style.display = ""; this.settingsMenu.style.display = "none"; } this.addEventListener(this.elements.parent, "click", (e) => { if (e.target === settingButton[0]) return; setTimeout(() => { if (this.settingsJustClicked) { this.settingsJustClicked = false; return; } this.closeSettingsMenu(); }, 10) }) const enter = addButton("Enter Fullscreen", '', () => { if (this.elements.parent.requestFullscreen) { this.elements.parent.requestFullscreen(); } else if (this.elements.parent.mozRequestFullScreen) { this.elements.parent.mozRequestFullScreen(); } else if (this.elements.parent.webkitRequestFullscreen) { this.elements.parent.webkitRequestFullscreen(); } else if (this.elements.parent.msRequestFullscreen) { this.elements.parent.msRequestFullscreen(); } exit.style.display = ""; enter.style.display = "none"; }); //todo, when user exits by pressing esc, labels dont currently change const exit = addButton("Exit Fullscreen", '', () => { if (document.exitFullscreen) { document.exitFullscreen(); } else if (document.webkitExitFullscreen) { document.webkitExitFullscreen(); } else if (document.mozCancelFullScreen) { document.mozCancelFullScreen(); } else if (document.msExitFullscreen) { document.msExitFullscreen(); } exit.style.display = "none"; enter.style.display = ""; }); exit.style.display = "none"; this.addEventListener(document, "webkitfullscreenchange mozfullscreenchange fullscreenchange", (e) => { if (e.target !== this.elements.parent) return; if (document.fullscreenElement === null) { exit.style.display = "none"; enter.style.display = ""; } else { //not sure if this is possible, lets put it here anyways exit.style.display = ""; enter.style.display = "none"; } }) if (this.config.buttonOpts) { if (!this.config.buttonOpts.playPause) { pauseButton.style.display = "none"; playButton.style.display = "none"; } if (!this.config.buttonOpts.restart) restartButton.setAttribute("hidden", ""); if (!this.config.buttonOpts.settings) settingButton[0].setAttribute("hidden", ""); if (!this.config.buttonOpts.fullscreen) { enter.style.display = "none"; exit.style.display = "none"; } if (!this.config.buttonOpts.saveState) saveState.setAttribute("hidden", ""); if (!this.config.buttonOpts.loadState) loadState.setAttribute("hidden", ""); if (!this.config.buttonOpts.gamepad) controlMenu.setAttribute("hidden", ""); if (!this.config.buttonOpts.cheat) cheatMenu.setAttribute("hidden", ""); if (!this.config.buttonOpts.cacheManager) cache.setAttribute("hidden", ""); } } openCacheMenu() { (async () => { const list = this.createElement("table"); const tbody = this.createElement("tbody"); const body = this.createPopup("Cache Manager", { "Clear All": async () => { const roms = await this.storage.rom.getSizes(); for (const k in roms) { await this.storage.rom.remove(k); } tbody.innerHTML = ""; }, "Close": () => { this.closePopup(); } }); const roms = await this.storage.rom.getSizes(); list.style.width = "100%"; list.style["padding-left"] = "10px"; list.style["text-align"] = "left"; body.appendChild(list); list.appendChild(tbody); const getSize = function(size) { let i = -1; do { size /= 1024, i++; } while (size > 1024); return Math.max(size, 0.1).toFixed(1) + [' kB', ' MB', ' GB', ' TB', 'PB', 'EB', 'ZB', 'YB'][i]; } for (const k in roms) { const line = this.createElement("tr"); const name = this.createElement("td"); const size = this.createElement("td"); const remove = this.createElement("td"); remove.style.cursor = "pointer"; name.innerText = k; size.innerText = getSize(roms[k]); const a = this.createElement("a"); a.innerText = "Remove"; this.addEventListener(remove, "click", () => { this.storage.rom.remove(k); line.remove(); }) remove.appendChild(a); line.appendChild(name); line.appendChild(size); line.appendChild(remove); tbody.appendChild(line); } })(); } createControlSettingMenu() { let buttonListeners = []; this.checkGamepadInputs = () => buttonListeners.forEach(elem => elem()); this.gamepadLabels = []; this.controls = JSON.parse(JSON.stringify(this.defaultControllers)); const body = this.createPopup("Control Settings", { "Reset": () => { this.controls = JSON.parse(JSON.stringify(this.defaultControllers)); this.checkGamepadInputs(); }, "Clear": () => { this.controls = {0:{},1:{},2:{},3:{}}; this.checkGamepadInputs(); }, "Close": () => { this.controlMenu.style.display = "none"; } }, true); this.controlMenu = body.parentElement; body.classList.add("ejs_control_body"); const buttons = { 0: 'B', 1: 'Y', 2: 'SELECT', 3: 'START', 4: 'UP', 5: 'DOWN', 6: 'LEFT', 7: 'RIGHT', 8: 'A', 9: 'X', 10: 'L', 11: 'R', 12: 'L2', 13: 'R2', 14: 'L3', 15: 'R3', 19: 'L STICK UP', 18: 'L STICK DOWN', 17: 'L STICK LEFT', 16: 'L STICK RIGHT', 23: 'R STICK UP', 22: 'R STICK DOWN', 21: 'R STICK LEFT', 20: 'R STICK RIGHT', 24: this.localization('QUICK SAVE STATE'), 25: this.localization('QUICK LOAD STATE'), 26: this.localization('CHANGE STATE SLOT') } let selectedPlayer; let players = []; let playerDivs = []; const playerSelect = this.createElement("ul"); playerSelect.classList.add("ejs_control_player_bar"); for (let i=1; i<5; i++) { const playerContainer = this.createElement("li"); playerContainer.classList.add("tabs-title"); playerContainer.setAttribute("role", "presentation"); const player = this.createElement("a"); player.innerText = "Player "+i; player.setAttribute("role", "tab"); player.setAttribute("aria-controls", "controls-"+(i-1)); player.setAttribute("aria-selected", "false"); player.id = "controls-"+(i-1)+"-label"; this.addEventListener(player, "click", (e) => { e.preventDefault(); players[selectedPlayer].classList.remove("ejs_control_selected"); playerDivs[selectedPlayer].setAttribute("hidden", ""); selectedPlayer = i-1; players[i-1].classList.add("ejs_control_selected"); playerDivs[i-1].removeAttribute("hidden"); }) playerContainer.appendChild(player); playerSelect.appendChild(playerContainer); players.push(playerContainer); } body.appendChild(playerSelect); const controls = this.createElement("div"); for (let i=0; i<4; i++) { const player = this.createElement("div"); const playerTitle = this.createElement("div"); const gamepadTitle = this.createElement("div"); gamepadTitle.style = "font-size:12px;"; gamepadTitle.innerText = "Connected Gamepad: "; const gamepadName = this.createElement("span"); this.gamepadLabels.push(gamepadName); gamepadName.innerText = "n/a"; gamepadTitle.appendChild(gamepadName); const leftPadding = this.createElement("div"); leftPadding.style = "width:25%;float:left;"; leftPadding.innerHTML = " "; const aboutParent = this.createElement("div"); aboutParent.style = "font-size:12px;width:50%;float:left;"; const gamepad = this.createElement("div"); gamepad.style = "text-align:center;width:50%;float:left;"; gamepad.innerText = "Gamepad"; aboutParent.appendChild(gamepad); const keyboard = this.createElement("div"); keyboard.style = "text-align:center;width:50%;float:left;"; keyboard.innerText = "Keyboard"; aboutParent.appendChild(keyboard); const headingPadding = this.createElement("div"); headingPadding.style = "clear:both;"; playerTitle.appendChild(gamepadTitle); playerTitle.appendChild(leftPadding); playerTitle.appendChild(aboutParent); playerTitle.appendChild(headingPadding); player.appendChild(playerTitle); for (const k in buttons) { const buttonText = this.createElement("div"); buttonText.setAttribute("data-id", k); buttonText.setAttribute("data-index", i); buttonText.setAttribute("data-label", buttons[k]); buttonText.style = "margin-bottom:10px;"; buttonText.classList.add("ejs_control_bar"); const title = this.createElement("div"); title.style = "width:25%;float:left;font-size:12px;"; const label = this.createElement("label"); label.innerText = buttons[k]+":"; title.appendChild(label); const textBoxes = this.createElement("div"); textBoxes.style = "width:50%;float:left;"; const textBox1Parent = this.createElement("div"); textBox1Parent.style = "width:50%;float:left;padding: 0 5px;"; const textBox1 = this.createElement("input"); textBox1.style = "text-align:center;height:25px;width: 100%;"; textBox1.type = "text"; textBox1.setAttribute("readonly", ""); textBox1.setAttribute("placeholder", ""); textBox1Parent.appendChild(textBox1); const textBox2Parent = this.createElement("div"); textBox2Parent.style = "width:50%;float:left;padding: 0 5px;"; const textBox2 = this.createElement("input"); textBox2.style = "text-align:center;height:25px;width: 100%;"; textBox2.type = "text"; textBox2.setAttribute("readonly", ""); textBox2.setAttribute("placeholder", ""); textBox2Parent.appendChild(textBox2); buttonListeners.push(() => { textBox2.value = ""; textBox1.value = ""; if (this.controls[i][k] && this.controls[i][k].value !== undefined) { textBox2.value = this.controls[i][k].value; } if (this.controls[i][k] && this.controls[i][k].value2 !== undefined) { textBox1.value = this.controls[i][k].value2; } }) if (this.controls[i][k] && this.controls[i][k].value) { textBox2.value = this.controls[i][k].value; } if (this.controls[i][k] && this.controls[i][k].value2) { textBox1.value = "button " + this.controls[i][k].value2; } textBoxes.appendChild(textBox1Parent); textBoxes.appendChild(textBox2Parent); const padding = this.createElement("div"); padding.style = "clear:both;"; textBoxes.appendChild(padding); const setButton = this.createElement("div"); setButton.style = "width:25%;float:left;"; const button = this.createElement("a"); button.classList.add("ejs_control_set_button"); button.innerText = "Set"; setButton.appendChild(button); const padding2 = this.createElement("div"); padding2.style = "clear:both;"; buttonText.appendChild(title); buttonText.appendChild(textBoxes); buttonText.appendChild(setButton); buttonText.appendChild(padding2); player.appendChild(buttonText); this.addEventListener(buttonText, "mousedown", (e) => { e.preventDefault(); this.controlPopup.parentElement.removeAttribute("hidden"); this.controlPopup.innerText = "[ " + buttons[k] + " ]\n"; this.controlPopup.setAttribute("button-num", k); this.controlPopup.setAttribute("player-num", i); }) } controls.appendChild(player); player.setAttribute("hidden", ""); playerDivs.push(player); } body.appendChild(controls); selectedPlayer = 0; players[0].classList.add("ejs_control_selected"); playerDivs[0].removeAttribute("hidden"); const popup = this.createElement('div'); popup.classList.add("ejs_popup_container"); const popupMsg = this.createElement("div"); popupMsg.classList.add("ejs_popup_box"); popupMsg.innerText = ""; popup.setAttribute("hidden", ""); this.controlPopup = popupMsg; popup.appendChild(popupMsg); this.controlMenu.appendChild(popup); } defaultControllers = { 0: { 0: { 'value': 'x' }, 1: { 'value': 's' }, 2: { 'value': 'v' }, 3: { 'value': 'enter' }, 4: { 'value': 'arrowup' }, 5: { 'value': 'arrowdown' }, 6: { 'value': 'arrowleft' }, 7: { 'value': 'arrowright' }, 8: { 'value': 'z' }, 9: { 'value': 'a' }, 10: { 'value': 'q' }, 11: { 'value': 'e' }, 12: { 'value': 'e' }, 13: { 'value': 'w' }, 14: {}, 15: {}, 16: { 'value': 'h' }, 17: { 'value': 'f' }, 18: { 'value': 'g' }, 19: { 'value': 't' }, 20: {'value': 'l'}, 21: {'value': 'j'}, 22: {'value': 'k'}, 23: {'value': 'i'}, 24: {}, 25: {}, 26: {} }, 1: {}, 2: {}, 3: {} } controls; keyChange(e) { if (!this.started) return; if (this.controlPopup.parentElement.getAttribute("hidden") === null) { const num = this.controlPopup.getAttribute("button-num"); const player = this.controlPopup.getAttribute("player-num"); if (!this.controls[player][num]) { this.controls[player][num] = {}; } this.controls[player][num].value = e.key.toLowerCase(); this.controlPopup.parentElement.setAttribute("hidden", ""); this.checkGamepadInputs(); return; } if (this.settingsMenu.style.display !== "none" || this.isPopupOpen()) return; e.preventDefault(); const special = [16, 17, 18, 19, 20, 21, 22, 23]; for (let i=0; i<4; i++) { for (let j=0; j<26; j++) { if (this.controls[i][j] && this.controls[i][j].value === e.key.toLowerCase()) { this.gameManager.simulateInput(i, j, (e.type === 'keyup' ? 0 : (special.includes(j) ? 0x7fff : 1))); } } } } gamepadEvent(e) { if (!this.started) return; const value = function(value) { if (value > 0.5 || value < -0.5) { return (value > 0) ? 1 : -1; } else { return 0; } }(e.value || 0); if (this.controlPopup.parentElement.getAttribute("hidden") === null) { if ('buttonup' === e.type || (e.type === "axischanged" && value === 0)) return; const num = this.controlPopup.getAttribute("button-num"); const player = this.controlPopup.getAttribute("player-num"); if (!this.controls[player][num]) { this.controls[player][num] = {}; } this.controls[player][num].value2 = (e.type === "axischanged" ? e.axis+":"+value : e.index); this.controlPopup.parentElement.setAttribute("hidden", ""); this.checkGamepadInputs(); return; } if (this.settingsMenu.style.display !== "none" || this.isPopupOpen()) return; const special = [16, 17, 18, 19, 20, 21, 22, 23]; for (let i=0; i<4; i++) { for (let j=0; j<26; j++) { if (['buttonup', 'buttondown'].includes(e.type) && (this.controls[i][j] && this.controls[i][j].value2 === e.index)) { this.gameManager.simulateInput(i, j, (e.type === 'buttondown' ? 0 : (special.includes(j) ? 0x7fff : 1))); } else if (e.type === "axischanged") { if (this.controls[i][j] && typeof this.controls[i][j].value2 === 'string' && this.controls[i][j].value2.split(":")[0] === e.axis) { if (special.includes(j)) { if (e.axis === 'LEFT_STICK_X') { if (e.value > 0) { this.gameManager.simulateInput(e.gamepadIndex, 16, 0x7fff * e.value); this.gameManager.simulateInput(e.gamepadIndex, 17, 0); } else { this.gameManager.simulateInput(e.gamepadIndex, 17, -0x7fff * e.value); this.gameManager.simulateInput(e.gamepadIndex, 16, 0); } } else if (e.axis === 'LEFT_STICK_Y') { if (e.value > 0) { this.gameManager.simulateInput(e.gamepadIndex, 18, 0x7fff * e.value); this.gameManager.simulateInput(e.gamepadIndex, 19, 0); } else { this.gameManager.simulateInput(e.gamepadIndex, 19, -0x7fff * e.value); this.gameManager.simulateInput(e.gamepadIndex, 18, 0); } } else if (e.axis === 'RIGHT_STICK_X') { if (e.value > 0) { this.gameManager.simulateInput(e.gamepadIndex, 20, 0x7fff * e.value); this.gameManager.simulateInput(e.gamepadIndex, 21, 0); } else { this.gameManager.simulateInput(e.gamepadIndex, 21, -0x7fff * e.value); this.gameManager.simulateInput(e.gamepadIndex, 20, 0); } } else if (e.axis === 'RIGHT_STICK_Y') { if (e.value > 0) { this.gameManager.simulateInput(e.gamepadIndex, 22, 0x7fff * e.value); this.gameManager.simulateInput(e.gamepadIndex, 23, 0); } else { this.gameManager.simulateInput(e.gamepadIndex, 23, 0x7fff * e.value); this.gameManager.simulateInput(e.gamepadIndex, 22, 0); } } } else if (this.controls[i][j].value2 === e.axis+":"+value || value === 0) { this.gameManager.simulateInput(i, j, ((value === 0) ? 0 : 1)); } } } } } } setVirtualGamepad() { this.virtualGamepad = this.createElement("div"); this.virtualGamepad.style.display = "none"; this.virtualGamepad.classList.add("ejs_virtualGamepad_parent"); this.elements.parent.appendChild(this.virtualGamepad); let info; if (this.config.VirtualGamepadSettings && function(set) { if (!Array.isArray(set)) { console.warn("Vritual gamepad settings is not array! Using default gamepad settings"); return false; } if (!set.length) { console.warn("Virtual gamepad settings is empty! Using default gamepad settings"); return false; } for (let i=0; i { e.preventDefault(); if (e.type === 'touchend' || e.type === 'touchcancel') { e.target.classList.remove("ejs_virtualGamepad_button_down"); window.setTimeout(() => { this.gameManager.simulateInput(0, value, 0); }) } else { e.target.classList.add("ejs_virtualGamepad_button_down"); this.gameManager.simulateInput(0, value, 1); } }) } } const createDPad = (opts) => { const container = opts.container; const callback = opts.event; const dpadMain = this.createElement("div"); dpadMain.classList.add("ejs_dpad_main"); const vertical = this.createElement("div"); vertical.classList.add("ejs_dpad_vertical"); const horizontal = this.createElement("div"); horizontal.classList.add("ejs_dpad_horizontal"); const bar1 = this.createElement("div"); bar1.classList.add("ejs_dpad_bar"); const bar2 = this.createElement("div"); bar2.classList.add("ejs_dpad_bar"); horizontal.appendChild(bar1); vertical.appendChild(bar2); dpadMain.appendChild(vertical); dpadMain.appendChild(horizontal); const updateCb = (e) => { e.preventDefault(); const touch = e.targetTouches[0]; if (!touch) return; const rect = dpadMain.getBoundingClientRect(); const x = touch.clientX - rect.left - dpadMain.clientWidth / 2; const y = touch.clientY - rect.top - dpadMain.clientHeight / 2; let up = 0, down = 0, left = 0, right = 0, angle = Math.atan(x / y) / (Math.PI / 180); if (y <= -10) { up = 1; } if (y >= 10) { down = 1; } if (x >= 10) { right = 1; left = 0; if (angle < 0 && angle >= -35 || angle > 0 && angle <= 35) { right = 0; } up = (angle < 0 && angle >= -55 ? 1 : 0); down = (angle > 0 && angle <= 55 ? 1 : 0); } if (x <= -10) { right = 0; left = 1; if (angle < 0 && angle >= -35 || angle > 0 && angle <= 35) { left = 0; } up = (angle > 0 && angle <= 55 ? 1 : 0); down = (angle < 0 && angle >= -55 ? 1 : 0); } dpadMain.classList.toggle("ejs_dpad_up_pressed", up); dpadMain.classList.toggle("ejs_dpad_down_pressed", down); dpadMain.classList.toggle("ejs_dpad_right_pressed", right); dpadMain.classList.toggle("ejs_dpad_left_pressed", left); callback(up, down, left, right); } const cancelCb = (e) => { e.preventDefault(); dpadMain.classList.remove("ejs_dpad_up_pressed"); dpadMain.classList.remove("ejs_dpad_down_pressed"); dpadMain.classList.remove("ejs_dpad_right_pressed"); dpadMain.classList.remove("ejs_dpad_left_pressed"); callback(0, 0, 0, 0); } this.addEventListener(dpadMain, 'touchstart touchmove', updateCb); this.addEventListener(dpadMain, 'touchend touchcancel', cancelCb); container.appendChild(dpadMain); } info.forEach((dpad, index) => { if (dpad.type !== 'dpad') return; if (leftHandedMode && ['left', 'right'].includes(dpad.location)) { dpad.location = (dpad.location==='left') ? 'right' : 'left'; const amnt = JSON.parse(JSON.stringify(dpad)); if (amnt.left) { dpad.right = amnt.left; } if (amnt.right) { dpad.left = amnt.right; } } const elem = this.createElement("div"); let style = ''; if (dpad.left) { style += 'left:'+dpad.left+';'; } if (dpad.right) { style += 'right:'+dpad.right+';'; } if (dpad.top) { style += 'top:'+dpad.top+';'; } elem.style = style; elems[dpad.location].appendChild(elem); createDPad({container: elem, event: (up, down, left, right) => { if (dpad.joystickInput) { if (up === 1) up=0x7fff; if (down === 1) up=0x7fff; if (left === 1) up=0x7fff; if (right === 1) up=0x7fff; } this.gameManager.simulateInput(0, dpad.inputValues[0], up); this.gameManager.simulateInput(0, dpad.inputValues[1], down); this.gameManager.simulateInput(0, dpad.inputValues[2], left); this.gameManager.simulateInput(0, dpad.inputValues[3], right); }}); }) info.forEach((zone, index) => { if (zone.type !== 'zone') return; if (leftHandedMode && ['left', 'right'].includes(zone.location)) { zone.location = (zone.location==='left') ? 'right' : 'left'; const amnt = JSON.parse(JSON.stringify(zone)); if (amnt.left) { zone.right = amnt.left; } if (amnt.right) { zone.left = amnt.right; } } const elem = this.createElement("div"); this.addEventListener(elem, "touchstart touchmove touchend touchcancel", (e) => { e.preventDefault(); }); elems[zone.location].appendChild(elem); const zoneObj = nipplejs.create({ 'zone': elem, 'mode': 'static', 'position': { 'left': zone.left, 'top': zone.top }, 'color': zone.color || 'red' }); zoneObj.on('end', () => { this.gameManager.simulateInput(0, zone.inputValues[0], 0); this.gameManager.simulateInput(0, zone.inputValues[1], 0); this.gameManager.simulateInput(0, zone.inputValues[2], 0); this.gameManager.simulateInput(0, zone.inputValues[3], 0); }); zoneObj.on('move', (e, info) => { const degree = info.angle.degree; const distance = info.distance; if (zone.joystickInput === true) { let x = 0, y = 0; if (degree > 0 && degree <= 45) { x = distance / 50; y = -0.022222222222222223 * degree * distance / 50; } if (degree > 45 && degree <= 90) { x = 0.022222222222222223 * (90 - degree) * distance / 50; y = -distance / 50; } if (degree > 90 && degree <= 135) { x = 0.022222222222222223 * (90 - degree) * distance / 50; y = -distance / 50; } if (degree > 135 && degree <= 180) { x = -distance / 50; y = -0.022222222222222223 * (180 - degree) * distance / 50; } if (degree > 135 && degree <= 225) { x = -distance / 50; y = -0.022222222222222223 * (180 - degree) * distance / 50; } if (degree > 225 && degree <= 270) { x = -0.022222222222222223 * (270 - degree) * distance / 50; y = distance / 50; } if (degree > 270 && degree <= 315) { x = -0.022222222222222223 * (270 - degree) * distance / 50; y = distance / 50; } if (degree > 315 && degree <= 359.9) { x = distance / 50; y = 0.022222222222222223 * (360 - degree) * distance / 50; } if (x > 0) { this.gameManager.simulateInput(0, zone.inputValues[0], 0x7fff * x); this.gameManager.simulateInput(0, zone.inputValues[1], 0); } else { this.gameManager.simulateInput(0, zone.inputValues[1], 0x7fff * -x); this.gameManager.simulateInput(0, zone.inputValues[0], 0); } if (y > 0) { this.gameManager.simulateInput(0, zone.inputValues[2], 0x7fff * y); this.gameManager.simulateInput(0, zone.inputValues[3], 0); } else { this.gameManager.simulateInput(0, zone.inputValues[3], 0x7fff * -y); this.gameManager.simulateInput(0, zone.inputValues[2], 0); } } else { if (degree >= 30 && degree < 150) { this.gameManager.simulateInput(0, zone.inputValues[0], 1); } else { window.setTimeout(() => { this.gameManager.simulateInput(0, zone.inputValues[0], 0); }, 30); } if (degree >= 210 && degree < 330) { this.gameManager.simulateInput(0, zone.inputValues[1], 1); } else { window.setTimeout(() => { this.gameManager.simulateInput(0, zone.inputValues[1], 0); }, 30); } if (degree >= 120 && degree < 240) { this.gameManager.simulateInput(0, zone.inputValues[2], 1); } else { window.setTimeout(() => { this.gameManager.simulateInput(0, zone.inputValues[2], 0); }, 30); } if (degree >= 300 || degree >= 0 && degree < 60) { this.gameManager.simulateInput(0, zone.inputValues[3], 1); } else { window.setTimeout(() => { this.gameManager.simulateInput(0, zone.inputValues[3], 0); }, 30); } } }); }) } handleResize() { if (!this.Module) return; const dpr = window.devicePixelRatio || 1; const positionInfo = this.game.getBoundingClientRect(); const width = positionInfo.width * dpr; const height = (positionInfo.height * dpr); this.Module.setCanvasSize(width, height); } getElementSize(element) { let elem = element.cloneNode(true); elem.style.position = 'absolute'; elem.style.opacity = 0; elem.removeAttribute('hidden'); element.parentNode.appendChild(elem); let width = elem.scrollWidth, height = elem.scrollHeight; elem.remove(); return { 'width': width, 'height': height }; } menuOptionChanged(option, value) { if (option === "shader") { try { this.Module.FS.unlink("/shader/shader.glslp"); } catch(e) {} if (value === "disabled") { this.gameManager.toggleShader(0); return; } this.Module.FS.writeFile("/shader/shader.glslp", window.EJS_SHADERS[value]); this.gameManager.toggleShader(1); return; } else if (option === "disk") { this.gameManager.setCurrentDisk(value); return; } this.gameManager.setVariable(option, value); } setupSettingsMenu() { this.settingsMenu = this.createElement("div"); this.addEventListener(this.settingsMenu, "click", (e) => { this.settingsJustClicked = true; }) this.settingsMenu.classList.add("ejs_settings_parent"); const nested = this.createElement("div"); nested.classList.add("ejs_settings_transition"); this.settings = {}; const home = this.createElement("div"); home.classList.add("ejs_setting_home"); home.classList.add("ejs_setting_menu"); nested.appendChild(home); const addToMenu = (title, id, options, defaultOption) => { const menuOption = this.createElement("div"); menuOption.classList.add("ejs_settings_main_bar"); const span = this.createElement("span"); span.innerText = title; const current = this.createElement("div"); current.innerText = ""; current.classList.add("ejs_settings_main_bar_selected"); span.appendChild(current); menuOption.appendChild(span); home.appendChild(menuOption); const menu = this.createElement("div"); menu.style["max-height"] = "385px"; menu.style.overflow = "auto"; menu.setAttribute("hidden", ""); const button = this.createElement("button"); const goToHome = () => { const homeSize = this.getElementSize(home); nested.style.width = (homeSize.width+20) + "px"; nested.style.height = homeSize.height + "px"; menu.setAttribute("hidden", ""); home.removeAttribute("hidden"); } this.addEventListener(menuOption, "click", (e) => { const targetSize = this.getElementSize(menu); nested.style.width = (targetSize.width+20) + "px"; nested.style.height = targetSize.height + "px"; menu.removeAttribute("hidden"); home.setAttribute("hidden", ""); }) this.addEventListener(button, "click", goToHome); button.type = "button"; button.classList.add("ejs_back_button"); menu.appendChild(button); const pageTitle = this.createElement("span"); pageTitle.innerText = title; pageTitle.classList.add("ejs_menu_text_a"); button.appendChild(pageTitle); const optionsMenu = this.createElement("div"); optionsMenu.classList.add("ejs_setting_menu"); //optionsMenu.style["max-height"] = "385px"; //optionsMenu.style.overflow = "auto"; let buttons = []; let opts = options; if (Array.isArray(options)) { opts = {}; for (let i=0; i { this.settings[id] = opt; for (let j=0; j 1) { const diskLabels = {}; for (let i=0; i { let option = line.split('; '); let name = option[0]; let options = option[1].split('|'), optionName = name.split("|")[0].replace(/_/g, ' ').replace(/.+\-(.+)/, '$1'); options.slice(1, -1); if (options.length === 1) return; let availableOptions = {}; for (let i=0; i 1) ? name.split("|")[1] : options[0].replace('(Default) ', '')); }) this.settingsMenu.appendChild(nested); this.settingParent.appendChild(this.settingsMenu); this.settingParent.style.position = "relative"; const homeSize = this.getElementSize(home); nested.style.width = homeSize.width + "px"; nested.style.height = homeSize.height + "px"; this.settingsMenu.style.display = "none"; } createSubPopup(hidden) { const popup = this.createElement('div'); popup.classList.add("ejs_popup_container"); popup.classList.add("ejs_popup_container_box"); const popupMsg = this.createElement("div"); popupMsg.innerText = ""; if (hidden) popup.setAttribute("hidden", ""); popup.appendChild(popupMsg); return [popup, popupMsg]; } createCheatsMenu() { const body = this.createPopup("Cheats", { "Add Cheat": () => { const popups = this.createSubPopup(); this.cheatMenu.appendChild(popups[0]); popups[1].classList.add("ejs_cheat_parent"); popups[1].style.width = "100%"; const popup = popups[1]; const header = this.createElement("div"); header.classList.add("ejs_cheat_header"); const title = this.createElement("h2"); title.innerText = "Add Cheat Code"; title.classList.add("ejs_cheat_heading"); const close = this.createElement("button"); close.classList.add("ejs_cheat_close"); header.appendChild(title); header.appendChild(close); popup.appendChild(header); this.addEventListener(close, "click", (e) => { popups[0].remove(); }) const main = this.createElement("div"); main.classList.add("ejs_cheat_main"); const header3 = this.createElement("strong"); header3.innerText = "Code"; main.appendChild(header3); main.appendChild(this.createElement("br")); const mainText = this.createElement("textarea"); mainText.classList.add("ejs_cheat_code"); mainText.style.width = "100%"; mainText.style.height = "80px"; main.appendChild(mainText); main.appendChild(this.createElement("br")); const header2 = this.createElement("strong"); header2.innerText = "Description"; main.appendChild(header2); main.appendChild(this.createElement("br")); const mainText2 = this.createElement("input"); mainText2.type = "text"; mainText2.classList.add("ejs_cheat_code"); main.appendChild(mainText2); main.appendChild(this.createElement("br")); popup.appendChild(main); const footer = this.createElement("footer"); const submit = this.createElement("button"); const closeButton = this.createElement("button"); submit.innerText = "Submit"; closeButton.innerText = "Close"; submit.classList.add("ejs_button_button"); closeButton.classList.add("ejs_button_button"); submit.classList.add("ejs_popup_submit"); closeButton.classList.add("ejs_popup_submit"); submit.style["background-color"] = "rgba(var(--ejs-primary-color),1)"; footer.appendChild(submit); const span = this.createElement("span"); span.innerText = " "; footer.appendChild(span); footer.appendChild(closeButton); popup.appendChild(footer); this.addEventListener(submit, "click", (e) => { if (!mainText.value.trim() || !mainText2.value.trim()) return; popups[0].remove(); this.cheats.push({ code: mainText.value, desc: mainText2.value, checked: false }); this.updateCheatUI(); }) this.addEventListener(closeButton, "click", (e) => { popups[0].remove(); }) }, "Close": () => { this.cheatMenu.style.display = "none"; } }, true); this.cheatMenu = body.parentElement; const rows = this.createElement("div"); body.appendChild(rows); rows.classList.add("ejs_cheat_rows"); this.elements.cheatRows = rows; } updateCheatUI() { if (!this.cheats) this.cheats = []; this.elements.cheatRows.innerHTML = ""; const addToMenu = (desc, checked, code, i) => { const row = this.createElement("div"); row.classList.add("ejs_cheat_row"); const input = this.createElement("input"); input.type = "checkbox"; input.checked = checked; input.value = i; input.id = "ejs_cheat_switch_"+i; row.appendChild(input); const label = this.createElement("label"); label.for = "ejs_cheat_switch_"+i; label.innerText = desc; row.appendChild(label); label.addEventListener("click", (e) => { input.checked = !input.checked; this.cheats[i].checked = input.checked; this.cheatChanged(input.checked, code, i); }) const close = this.createElement("a"); close.classList.add("ejs_cheat_row_button"); close.innerText = "×"; row.appendChild(close); this.elements.cheatRows.appendChild(row); this.cheatChanged(checked, code, i); } this.gameManager.resetCheat(); for (let i=0; i