From 86d28b867a78928e5d5f18d7ebd37742714a9668 Mon Sep 17 00:00:00 2001 From: Ethan O'Brien <77750390+ethanaobrien@users.noreply.github.com> Date: Thu, 22 Jun 2023 11:00:13 -0500 Subject: [PATCH] Add button bar, some buttons, states, screenshot --- index.html | 1 - src/GameManager.js | 77 ++++++++++++++++++++ src/css/main.css | 95 +++++++++++++++++++++++- src/emulator.js | 176 ++++++++++++++++++++++++++++++++++++--------- src/loader.js | 1 + 5 files changed, 314 insertions(+), 36 deletions(-) create mode 100644 src/GameManager.js diff --git a/index.html b/index.html index 8fb8f05..cb1cd1c 100644 --- a/index.html +++ b/index.html @@ -6,7 +6,6 @@ EJS_player = '#game'; EJS_core = 'nes'; EJS_gameUrl = 'mega_mountain.nes'; - EJS_pathtodata = 'data/'; EJS_DEBUG_XX = true; diff --git a/src/GameManager.js b/src/GameManager.js new file mode 100644 index 0000000..08e0b7b --- /dev/null +++ b/src/GameManager.js @@ -0,0 +1,77 @@ +class EJS_GameManager { + constructor(Module) { + this.Module = Module; + this.FS = this.Module.FS; + this.functions = { + restart: this.Module.cwrap('system_restart', '', []), + getStateInfo: this.Module.cwrap('get_state_info', 'string', []), //these names are dumb + saveStateInfo: this.Module.cwrap('save_state_info', 'null', []), + loadState: this.Module.cwrap('load_state', 'number', ['string', 'number']), + screenshot: this.Module.cwrap('cmd_take_screenshot', '', []) + } + } + restart() { + this.functions.restart(); + } + getState() { + return new Promise(async (resolve, reject) => { + const stateInfo = (await this.getStateInfo()).split('|') + let state; + let size = stateInfo[0] >> 0; + if (size > 0) { + state = new Uint8Array(size); + let start = stateInfo[1] >> 0; + for (let i=0; i { + let a; + let b = setInterval(() => { + a = this.functions.getStateInfo(); + if (a) { + clearInterval(b); + resolve(a); + } + }, 50) + }); + } + loadState(state) { + try { + this.FS.unlink('game.state'); + } catch(e){} + this.FS.writeFile('/game.state', state); + this.functions.loadState("game.state", 0); + setTimeout(() => { + this.FS.unlink('game.state'); + }, 5000) + } + screenshot() { + this.functions.screenshot(); + return this.FS.readFile('screenshot.png'); + } + quickSave() { + (async () => { + let slot = 0; + let name = slot + '-quick.state'; + try { + this.FS.unlink(name); + } catch (e) {} + let data = await this.getState(); + this.FS.writeFile('/'+name, data); + })(); + } + quickLoad() { + (async () => { + let slot = 0; + let name = slot + '-quick.state'; + this.functions.loadState(name, 0); + })(); + } + +} + +window.EJS_GameManager = EJS_GameManager; diff --git a/src/css/main.css b/src/css/main.css index 9137a80..1c34175 100644 --- a/src/css/main.css +++ b/src/css/main.css @@ -3,7 +3,7 @@ overflow: hidden; --ejs-primary-color: 26,175,255; position: relative; - font-family: Avenir,"Avenir Next","Helvetica Neue","Segoe UI",Helvetica,Arial,sans-serif; + font-family: Avenir, "Avenir Next", "Helvetica Neue", "Segoe UI", Helvetica, Arial, sans-serif; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; @@ -105,3 +105,96 @@ box-shadow: 0 0 0 5px rgba(var(--ejs-primary-color),0.5); outline: 0; } + +.ejs_menu_bar { + padding: 15px 10px 10px; + background: linear-gradient(rgba(0,0,0,0),rgba(0,0,0,0.7)); + border-bottom-left-radius: inherit; + border-bottom-right-radius: inherit; + bottom: 0; + color: #fff; + left: 0; + position: absolute; + right: 0; + transition: opacity .4s ease-in-out,transform .4s ease-in-out; + z-index: 3; + align-items: center; + display: flex; + justify-content: flex-start; + text-align: center; +} + +.ejs_menu_bar svg { + display: block; + fill: currentColor; + height: 18px; + pointer-events: none; + width: 18px; +} + +.ejs_menu_bar_hidden { + opacity: 0; + pointer-events: none; + transform: translateY(100%); +} + +.ejs_menu_button { + margin-right: 2px; + touch-action: manipulation; + background: transparent; + border: 0; + border-radius: 3px; + color: inherit; + cursor: pointer; + flex-shrink: 0; + overflow: visible; + padding: 7px; + position: relative; + transition: all .3s ease; +} + +.ejs_menu_button:hover { + background: rgba(var(--ejs-primary-color),1); + color: #fff; +} + +.ejs_menu_button:hover .ejs_menu_text { + transform: translate(0,0) scale(1); + opacity: 1; +} + +.ejs_menu_text { + left: 0; + background: rgba(255,255,255,0.9); + border-radius: 3px; + bottom: 100%; + box-shadow: 0 1px 2px rgba(0,0,0,0.15); + color: #4f5b5f; + font-size: 14px; + font-weight: 500; + line-height: 1.3; + margin-bottom: 10px; + opacity: 0; + padding: 5px 7.5px; + pointer-events: none; + position: absolute; + transform: translate(0,10px) scale(0.8); + transform-origin: 0 100%; + transition: transform .2s .1s ease,opacity .2s .1s ease; + white-space: nowrap; + z-index: 2; +} + +.ejs_menu_text::before { + border-left: 4px solid transparent; + border-right: 4px solid transparent; + border-top: 4px solid rgba(255,255,255,0.9); + bottom: -4px; + content: ''; + height: 0; + left: 16px; + position: absolute; + transform: translateX(-50%); + width: 0; + z-index: 2; +} diff --git a/src/emulator.js b/src/emulator.js index 87559e3..80806f5 100644 --- a/src/emulator.js +++ b/src/emulator.js @@ -2,6 +2,13 @@ class EmulatorJS { createElement(type) { return document.createElement(type); } + addEventListener(element, listener, callback) { + const listeners = listener.split(" "); + for (let i=0; i { if (res === -1) { @@ -247,13 +254,13 @@ class EmulatorJS { 'preRun': [], 'postRun': [], 'canvas': this.canvas, - 'print': function(msg) { - if (window.EJS_DEBUG_XX === true) { + 'print': (msg) => { + if (this.debug) { console.log(msg); } }, - 'printErr': function(msg) { - if (window.EJS_DEBUG_XX === true) { + 'printErr': (msg) => { + if (this.debug) { console.log(msg); } }, @@ -269,6 +276,7 @@ class EmulatorJS { console.log(a, b, c) } }; + this.Module = window.Module; } startGame() { this.textElem.remove(); @@ -276,11 +284,12 @@ class EmulatorJS { this.game.classList.remove("ejs_game"); this.game.appendChild(this.canvas); const args = []; - if (window.EJS_DEBUG_XX === true) args.push('-v'); + if (this.debug) args.push('-v'); args.push('/game'); Module.callMain(args); Module.resumeMainLoop(); this.started = true; + this.paused = false; Module.setCanvasSize(800, 600); let i=0; // this needs to be fixed. Ugh. @@ -292,6 +301,7 @@ class EmulatorJS { } bindListeners() { this.createContextMenu(); + this.createBottomMenuBar(); //keyboard, etc... } createContextMenu() { @@ -305,41 +315,139 @@ class EmulatorJS { } 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', (e) => { - this.elements.contextmenu.style.display = "none"; - }) - let contextHtml = [''] - let contextFunctions = [] + 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) { - contextFunctions.push(functi0n); - } else { - contextFunctions.push(() => {}); - } - let i = contextHtml.length - 1; - if (hidden) { - contextHtml.splice(i, 0, ''); - } else { - contextHtml.splice(i, 0, '
  • '+title+'
  • '); + this.addEventListener(li, 'click', functi0n); } + a.href = "#"; + a.onclick = "return false"; + a.innerText = title; + li.appendChild(a); + parent.appendChild(li); + hideMenu(); } - addButton("test 1", false, () => console.log("1")); - addButton("test 2", false, () => console.log("2")); - addButton("test 3", false, () => console.log("3")); - addButton("test 4", false, () => console.log("4")); + let screenshotUrl; + 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; + a.download = "screenshot.png"; + a.click(); + hideMenu(); + }); + addButton("Quick Save", false, () => { + this.gameManager.quickSave(); + hideMenu(); + }); + addButton("Quick Load", false, () => { + this.gameManager.quickLoad(); + hideMenu(); + }); + addButton("EmulatorJS", false, () => console.log("4")); - this.elements.contextmenu.innerHTML = contextHtml.join(''); - - let buttons = this.elements.contextmenu.getElementsByTagName('li') - for (let i=0; i { + const file = this.createElement("input"); + file.type = "file"; + this.addEventListener(file, "change", (e) => { + resolve(e.target.files[0]); + }) + file.click(); + }) + } + 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) return; + this.elements.menu.classList.add("ejs_menu_bar_hidden"); + } + + this.addEventListener(this.elements.parent, 'mousemove click', (e) => { + if (!this.started) 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) => { + 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); + this.elements.menu.appendChild(button); + if (callback instanceof Function) { + this.addEventListener(button, 'click', callback); + } + } + + //todo. Center text on not restart button + + addButton("Restart", '', () => { + this.gameManager.restart(); + }); + let stateUrl; + addButton("Save State", '', async () => { + if (stateUrl) URL.revokeObjectURL(stateUrl); + const state = await this.gameManager.getState(); + const blob = new Blob([state]); + stateUrl = URL.createObjectURL(blob); + const a = this.createElement("a"); + a.href = stateUrl; + a.download = "game.state"; + a.click(); + }); + addButton("Load State", '', async () => { + const file = await this.selectFile(); + const state = new Uint8Array(await file.arrayBuffer()); + this.gameManager.loadState(state); + }); + + } } diff --git a/src/loader.js b/src/loader.js index aafaede..58c2703 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('GameManager.js'); await loadStyle('css/main.css'); } const config = {};