diff --git a/src/css/main.css b/src/css/main.css index f06e9b7..31efdaf 100644 --- a/src/css/main.css +++ b/src/css/main.css @@ -370,4 +370,101 @@ user-select: none; transition: all .2s; } +.ejs_virtualGamepad_button_down { + background-color:#000000ad; +} +.ejs_dpad_main { + touch-action: none; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + opacity: .7; +} +.ejs_dpad_horizontal { + width: 100%; + height: 36px; + transform: translate(0,-50%); + position: absolute; + left: 0; + top: 50%; + border-radius: 5px; + overflow: hidden; +} +.ejs_dpad_horizontal:before { + content: ""; + position: absolute; + left: 0; + top: 50%; + z-index: 1; + transform: translate(0,-50%); + width: 0; + height: 0; + border: 8px solid; + border-color: transparent #333 transparent transparent; +} +.ejs_dpad_horizontal:after { + content: ""; + position: absolute; + right: 0; + top: 50%; + z-index: 1; + transform: translate(0,-50%); + width: 0; + height: 0; + border: 8px solid; + border-color: transparent transparent transparent #333; +} +.ejs_dpad_vertical { + width: 36px; + height: 100%; + transform: translate(-50%,0); + position: absolute; + left: 50%; + border-radius: 5px; + overflow: hidden; +} +.ejs_dpad_vertical:before { + content: ""; + position: absolute; + top: 0; + left: 50%; + z-index: 1; + transform: translate(-50%,0); + width: 0; + height: 0; + border: 8px solid; + border-color: transparent transparent #333 transparent; +} +.ejs_dpad_vertical:after { + content: ""; + position: absolute; + bottom: 0; + left: 50%; + z-index: 1; + transform: translate(-50%,0); + width: 0; + height: 0; + border: 8px solid; + border-color: #333 transparent transparent transparent; +} +.ejs_dpad_bar { + position: absolute; + width: 100%; + height: 100%; + background: #787878; +} +.ejs_dpad_left_pressed .ejs_dpad_horizontal:before { + border-right-color:#fff; +} +.ejs_dpad_right_pressed .ejs_dpad_horizontal:after { + border-left-color:#fff; +} +.ejs_dpad_up_pressed .ejs_dpad_vertical:before { + border-bottom-color:#fff; +} +.ejs_dpad_down_pressed .ejs_dpad_vertical:after { + border-top-color:#fff +} diff --git a/src/emulator.js b/src/emulator.js index 6ed8ded..932ec23 100644 --- a/src/emulator.js +++ b/src/emulator.js @@ -69,6 +69,7 @@ class EmulatorJS { } constructor(element, config) { window.EJS_TESTING = this; + this.touch = false; this.debug = (window.EJS_DEBUG_XX === true); this.setElements(element); this.started = false; @@ -101,6 +102,9 @@ class EmulatorJS { 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) { @@ -179,15 +183,86 @@ class EmulatorJS { }) }) } - async function decompressRar() { - + 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 - return decompress7z(data); + 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(data)); + return new Promise(resolve => resolve({file: data})); } } @@ -222,8 +297,29 @@ class EmulatorJS { script.src = URL.createObjectURL(new Blob([js], {type: "application/javascript"})); document.body.appendChild(script); } - getCore() { + 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', @@ -251,7 +347,11 @@ class EmulatorJS { return; } this.checkCompression(new Uint8Array(res.data), this.localization("Decompress Game Data")).then((data) => { - FS.writeFile("/game", data); + for (const k in data) { + this.fileName = k; + FS.writeFile(k, data[k]); //needs to be cleaned up + break; + } this.startGame(); }); }, (progress) => { @@ -298,11 +398,14 @@ class EmulatorJS { this.game.appendChild(this.canvas); const args = []; if (this.debug) args.push('-v'); - args.push('/game'); + args.push('/'+this.fileName); this.Module.callMain(args); this.Module.resumeMainLoop(); this.started = true; this.paused = false; + if (this.touch) { + this.virtualGamepad.style.display = ""; + } //this needs to be fixed... setInterval(() => { @@ -890,8 +993,66 @@ class EmulatorJS { this.virtualGamepad.style.display = "none"; this.virtualGamepad.classList.add("ejs_virtualGamepad_parent"); this.elements.parent.appendChild(this.virtualGamepad); - const info = [{"type":"button","text":"B","id":"b","location":"right","right":-10,"top":70,"bold":true,"input_value":0},{"type":"button","text":"A","id":"a","location":"right","right":60,"top":70,"bold":true,"input_value":8},{"type":"dpad","location":"left","left":"50%","right":"50%","joystickInput":false,"inputValues":[4,5,6,7]},{"type":"button","text":"Start","id":"start","location":"center","left":60,"fontSize":15,"block":true,"input_value":3},{"type":"button","text":"Select","id":"select","location":"center","left":-5,"fontSize":15,"block":true,"input_value":2}]; - //todo next + 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); + } + } + }); + }) + + //todo - zone and dpad (and input) diff --git a/src/loader.js b/src/loader.js index 58c2703..50e3bb3 100644 --- a/src/loader.js +++ b/src/loader.js @@ -35,6 +35,7 @@ if (('undefined' != typeof EJS_DEBUG_XX && true === EJS_DEBUG_XX) || true) { await loadScript('emulator.js'); + await loadScript('nipplejs.js'); await loadScript('GameManager.js'); await loadStyle('css/main.css'); } diff --git a/src/nipplejs.js b/src/nipplejs.js new file mode 100644 index 0000000..48fa61c --- /dev/null +++ b/src/nipplejs.js @@ -0,0 +1 @@ +!function(t,i){"object"==typeof exports&&"object"==typeof module?module.exports=i():"function"==typeof define&&define.amd?define("nipplejs",[],i):"object"==typeof exports?exports.nipplejs=i():t.nipplejs=i()}(window,(function(){return function(t){var i={};function e(o){if(i[o])return i[o].exports;var n=i[o]={i:o,l:!1,exports:{}};return t[o].call(n.exports,n,n.exports,e),n.l=!0,n.exports}return e.m=t,e.c=i,e.d=function(t,i,o){e.o(t,i)||Object.defineProperty(t,i,{enumerable:!0,get:o})},e.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},e.t=function(t,i){if(1&i&&(t=e(t)),8&i)return t;if(4&i&&"object"==typeof t&&t&&t.__esModule)return t;var o=Object.create(null);if(e.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:t}),2&i&&"string"!=typeof t)for(var n in t)e.d(o,n,function(i){return t[i]}.bind(null,n));return o},e.n=function(t){var i=t&&t.__esModule?function(){return t.default}:function(){return t};return e.d(i,"a",i),i},e.o=function(t,i){return Object.prototype.hasOwnProperty.call(t,i)},e.p="",e(e.s=0)}([function(t,i,e){"use strict";e.r(i);var o,n=function(t,i){var e=i.x-t.x,o=i.y-t.y;return Math.sqrt(e*e+o*o)},s=function(t){return t*(Math.PI/180)},r=function(t){return t*(180/Math.PI)},d=new Map,a=function(t){d.has(t)&&clearTimeout(d.get(t)),d.set(t,setTimeout(t,100))},p=function(t,i,e){for(var o,n=i.split(/[ ,]+/g),s=0;s=0&&this._handlers_[t].splice(this._handlers_[t].indexOf(i),1),this},_.prototype.trigger=function(t,i){var e,o=this,n=t.split(/[ ,]+/g);o._handlers_=o._handlers_||{};for(var s=0;ss&&n<3*s&&!t.lockX?i="up":n>-s&&n<=s&&!t.lockY?i="left":n>3*-s&&n<=-s&&!t.lockX?i="down":t.lockY||(i="right"),t.lockY||(e=n>-r&&n0?"up":"down"),t.force>this.options.threshold){var d,a={};for(d in this.direction)this.direction.hasOwnProperty(d)&&(a[d]=this.direction[d]);var p={};for(d in this.direction={x:e,y:o,angle:i},t.direction=this.direction,a)a[d]===this.direction[d]&&(p[d]=!0);if(p.x&&p.y&&p.angle)return t;p.x&&p.y||this.trigger("plain",t),p.x||this.trigger("plain:"+e,t),p.y||this.trigger("plain:"+o,t),p.angle||this.trigger("dir dir:"+i,t)}else this.resetDirection();return t};var P=k;function E(t,i){this.nipples=[],this.idles=[],this.actives=[],this.ids=[],this.pressureIntervals={},this.manager=t,this.id=E.id,E.id+=1,this.defaults={zone:document.body,multitouch:!1,maxNumberOfNipples:10,mode:"dynamic",position:{top:0,left:0},catchDistance:200,size:100,threshold:.1,color:"white",fadeTime:250,dataOnly:!1,restJoystick:!0,restOpacity:.5,lockX:!1,lockY:!1,shape:"circle",dynamicPage:!1,follow:!1},this.config(i),"static"!==this.options.mode&&"semi"!==this.options.mode||(this.options.multitouch=!1),this.options.multitouch||(this.options.maxNumberOfNipples=1);var e=getComputedStyle(this.options.zone.parentElement);return e&&"flex"===e.display&&(this.parentIsFlex=!0),this.updateBox(),this.prepareNipples(),this.bindings(),this.begin(),this.nipples}E.prototype=new T,E.constructor=E,E.id=0,E.prototype.prepareNipples=function(){var t=this.nipples;t.on=this.on.bind(this),t.off=this.off.bind(this),t.options=this.options,t.destroy=this.destroy.bind(this),t.ids=this.ids,t.id=this.id,t.processOnMove=this.processOnMove.bind(this),t.processOnEnd=this.processOnEnd.bind(this),t.get=function(i){if(void 0===i)return t[0];for(var e=0,o=t.length;e