diff --git a/data/loader.js b/data/loader.js index 2baadfe..5641b6c 100644 --- a/data/loader.js +++ b/data/loader.js @@ -129,6 +129,7 @@ config.fixedSaveInterval = window.EJS_fixedSaveInterval; config.disableAutoUnload = window.EJS_disableAutoUnload; config.disableBatchBootup = window.EJS_disableBatchBootup; + config.defaultAutoFireInterval = window.EJS_defaultAutoFireInterval; let systemLang; try { diff --git a/data/src/emulator.js b/data/src/emulator.js index f78cf1c..a0e02a6 100644 --- a/data/src/emulator.js +++ b/data/src/emulator.js @@ -237,7 +237,23 @@ class EmulatorJS { this.cheats = []; this.started = false; this.volume = (typeof this.config.volume === "number") ? this.config.volume : 0.5; - if (this.config.defaultControllers) this.defaultControllers = this.config.defaultControllers; + if (this.config.defaultControllers) { + // Merge user config with defaults instead of replacing + for (const player in this.config.defaultControllers) { + if (!this.defaultControllers[player]) { + this.defaultControllers[player] = {}; + } + for (const button in this.config.defaultControllers[player]) { + this.defaultControllers[player][button] = Object.assign( + {}, + this.defaultControllers[player][button] || {}, + this.config.defaultControllers[player][button] + ); + } + } + } + this.defaultAutoFireInterval = this.config.defaultAutoFireInterval || 100; + this.autofireIntervals = {}; this.muted = false; this.paused = true; this.missingLang = []; @@ -1145,7 +1161,7 @@ class EmulatorJS { if (document.activeElement !== this.elements.parent && this.config.noAutoFocus !== true) 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.addEventListener(window, "blur", () => this.stopAllAutofire()); let counter = 0; this.elements.statePopupPanel = this.createPopup("", {}, true); @@ -1961,6 +1977,9 @@ class EmulatorJS { } } this.gameManager.toggleMainLoop(this.paused ? 0 : 1); + if (this.paused) { + this.stopAllAutofire(); + } //I now realize its not easy to pause it while the cursor is locked, just in case I guess if (this.enableMouseLock) { @@ -2489,12 +2508,14 @@ class EmulatorJS { this.controls = JSON.parse(JSON.stringify(this.defaultControllers)); const body = this.createPopup("Control Settings", { "Reset": () => { + this.stopAllAutofire(); this.controls = JSON.parse(JSON.stringify(this.defaultControllers)); this.setupKeys(); this.checkGamepadInputs(); this.saveSettings(); }, "Clear": () => { + this.stopAllAutofire(); this.controls = { 0: {}, 1: {}, 2: {}, 3: {} }; this.setupKeys(); this.checkGamepadInputs(); @@ -2990,7 +3011,7 @@ class EmulatorJS { leftPadding.innerHTML = " "; const aboutParent = this.createElement("div"); - aboutParent.style = "font-size:12px;width:50%;float:left;"; + aboutParent.style = "font-size:12px;width:40%;float:left;"; const gamepad = this.createElement("div"); gamepad.style = "text-align:center;width:50%;float:left;"; gamepad.innerText = this.localization("Gamepad"); @@ -3000,12 +3021,22 @@ class EmulatorJS { keyboard.innerText = this.localization("Keyboard"); aboutParent.appendChild(keyboard); + const setHeader = this.createElement("div"); + setHeader.style = "font-size:12px;width:15%;float:left;text-align:center;"; + setHeader.innerHTML = " "; + + const autofireHeader = this.createElement("div"); + autofireHeader.style = "font-size:12px;width:20%;float:left;text-align:center;"; + autofireHeader.innerText = this.localization("Autofire"); + const headingPadding = this.createElement("div"); headingPadding.style = "clear:both;"; playerTitle.appendChild(gamepadTitle); playerTitle.appendChild(leftPadding); playerTitle.appendChild(aboutParent); + playerTitle.appendChild(setHeader); + playerTitle.appendChild(autofireHeader); if ((this.touch || this.hasTouchScreen) && i === 0) { const vgp = this.createElement("div"); @@ -3056,7 +3087,7 @@ class EmulatorJS { title.appendChild(label); const textBoxes = this.createElement("div"); - textBoxes.style = "width:50%;float:left;"; + textBoxes.style = "width:40%;float:left;"; const textBox1Parent = this.createElement("div"); textBox1Parent.style = "width:50%;float:left;padding: 0 5px;"; @@ -3124,23 +3155,63 @@ class EmulatorJS { textBoxes.appendChild(padding); const setButton = this.createElement("div"); - setButton.style = "width:25%;float:left;"; + setButton.style = "width:15%;float:left;"; const button = this.createElement("a"); button.classList.add("ejs_control_set_button"); button.innerText = this.localization("Set"); setButton.appendChild(button); + // Autofire checkbox - not available for analog stick axes + const autofireColumn = this.createElement("div"); + autofireColumn.style = "width:20%;float:left;text-align:center;"; + + if (!this.analogAxes.includes(k)) { + const autofireCheckbox = this.createElement("input"); + autofireCheckbox.type = "checkbox"; + autofireCheckbox.style = "cursor:pointer;"; + autofireCheckbox.checked = this.controls[i][k] && this.controls[i][k].autofire === true; + autofireCheckbox.setAttribute("data-player", i); + autofireCheckbox.setAttribute("data-button", k); + + // Update checkbox state when controls change + buttonListeners.push(() => { + autofireCheckbox.checked = this.controls[i][k] && this.controls[i][k].autofire === true; + }); + + this.addEventListener(autofireCheckbox, "change", (e) => { + e.stopPropagation(); + const playerIdx = parseInt(e.target.getAttribute("data-player")); + const buttonIdx = parseInt(e.target.getAttribute("data-button")); + if (!this.controls[playerIdx][buttonIdx]) { + this.controls[playerIdx][buttonIdx] = {}; + } + this.controls[playerIdx][buttonIdx].autofire = e.target.checked; + // Stop any active autofire if unchecked + if (!e.target.checked) { + this.stopAutofire(playerIdx, buttonIdx); + } + this.saveSettings(); + }); + + autofireColumn.appendChild(autofireCheckbox); + } + const padding2 = this.createElement("div"); padding2.style = "clear:both;"; buttonText.appendChild(title); buttonText.appendChild(textBoxes); buttonText.appendChild(setButton); + buttonText.appendChild(autofireColumn); buttonText.appendChild(padding2); player.appendChild(buttonText); this.addEventListener(buttonText, "mousedown", (e) => { + // Don't open popup when clicking on the autofire checkbox + if (e.target.tagName === "INPUT" && e.target.type === "checkbox") { + return; + } e.preventDefault(); this.controlPopup.parentElement.parentElement.removeAttribute("hidden"); this.controlPopup.innerText = "[ " + controlLabel + " ]\n" + this.localization("Press Keyboard"); @@ -3307,6 +3378,8 @@ class EmulatorJS { 2: {}, 3: {} } + // Analog stick axes - these use 0x7fff values and don't support autofire + this.analogAxes = [16, 17, 18, 19, 20, 21, 22, 23]; this.keyMap = { 0: "", 8: "backspace", @@ -3434,6 +3507,48 @@ class EmulatorJS { } return -1; } + getAutofireInterval(playerIndex, buttonIndex) { + const control = this.controls[playerIndex] && this.controls[playerIndex][buttonIndex]; + if (control && typeof control.autoFireInterval === "number") { + return control.autoFireInterval; + } + return this.defaultAutoFireInterval; + } + isAutofireEnabled(playerIndex, buttonIndex) { + const control = this.controls[playerIndex] && this.controls[playerIndex][buttonIndex]; + return control && control.autofire === true; + } + startAutofire(playerIndex, buttonIndex, inputValue) { + const key = `${playerIndex}-${buttonIndex}`; + if (this.autofireIntervals[key]) { + return; + } + let pressed = true; + this.gameManager.simulateInput(playerIndex, buttonIndex, inputValue); + const interval = this.getAutofireInterval(playerIndex, buttonIndex); + this.autofireIntervals[key] = setInterval(() => { + pressed = !pressed; + this.gameManager.simulateInput(playerIndex, buttonIndex, pressed ? inputValue : 0); + }, interval); + } + stopAutofire(playerIndex, buttonIndex) { + const key = `${playerIndex}-${buttonIndex}`; + if (this.autofireIntervals[key]) { + clearInterval(this.autofireIntervals[key]); + delete this.autofireIntervals[key]; + this.gameManager.simulateInput(playerIndex, buttonIndex, 0); + } + } + stopAllAutofire() { + for (const key in this.autofireIntervals) { + clearInterval(this.autofireIntervals[key]); + const [playerIndex, buttonIndex] = key.split("-").map(Number); + if (this.gameManager) { + this.gameManager.simulateInput(playerIndex, buttonIndex, 0); + } + } + this.autofireIntervals = {}; + } keyChange(e) { if (e.repeat) return; if (!this.started) return; @@ -3451,11 +3566,24 @@ class EmulatorJS { } if (this.settingsMenu.style.display !== "none" || this.isPopupOpen() || this.getSettingValue("keyboardInput") === "enabled") 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 < 30; j++) { if (this.controls[i][j] && this.controls[i][j].value === e.keyCode) { - this.gameManager.simulateInput(i, j, (e.type === "keyup" ? 0 : (special.includes(j) ? 0x7fff : 1))); + const isAnalog = this.analogAxes.includes(j); + const inputValue = isAnalog ? 0x7fff : 1; + if (e.type === "keyup") { + if (this.isAutofireEnabled(i, j) && !isAnalog) { + this.stopAutofire(i, j); + } else { + this.gameManager.simulateInput(i, j, 0); + } + } else { + if (this.isAutofireEnabled(i, j) && !isAnalog) { + this.startAutofire(i, j, inputValue); + } else { + this.gameManager.simulateInput(i, j, inputValue); + } + } } } } @@ -3488,7 +3616,6 @@ class EmulatorJS { 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++) { if (gamepadIndex !== i) continue; for (let j = 0; j < 30; j++) { @@ -3496,12 +3623,26 @@ class EmulatorJS { continue; } const controlValue = this.controls[i][j].value2; + const isAnalog = this.analogAxes.includes(j); if (["buttonup", "buttondown"].includes(e.type) && (controlValue === e.label || controlValue === e.index)) { - this.gameManager.simulateInput(i, j, (e.type === "buttonup" ? 0 : (special.includes(j) ? 0x7fff : 1))); + const inputValue = isAnalog ? 0x7fff : 1; + if (e.type === "buttonup") { + if (this.isAutofireEnabled(i, j) && !isAnalog) { + this.stopAutofire(i, j); + } else { + this.gameManager.simulateInput(i, j, 0); + } + } else { + if (this.isAutofireEnabled(i, j) && !isAnalog) { + this.startAutofire(i, j, inputValue); + } else { + this.gameManager.simulateInput(i, j, inputValue); + } + } } else if (e.type === "axischanged") { if (typeof controlValue === "string" && controlValue.split(":")[0] === e.axis) { - if (special.includes(j)) { + if (isAnalog) { if (j === 16 || j === 17) { if (e.value > 0) { this.gameManager.simulateInput(i, 16, 0x7fff * e.value); @@ -3914,14 +4055,23 @@ class EmulatorJS { let downValue = info[i].joystickInput === true ? 0x7fff : 1; this.addEventListener(button, "touchstart touchend touchcancel", (e) => { e.preventDefault(); + const isAnalog = this.analogAxes.includes(value); if (e.type === "touchend" || e.type === "touchcancel") { e.target.classList.remove("ejs_virtualGamepad_button_down"); window.setTimeout(() => { - this.gameManager.simulateInput(0, value, 0); + if (this.isAutofireEnabled(0, value) && !isAnalog) { + this.stopAutofire(0, value); + } else { + this.gameManager.simulateInput(0, value, 0); + } }) } else { e.target.classList.add("ejs_virtualGamepad_button_down"); - this.gameManager.simulateInput(0, value, downValue); + if (this.isAutofireEnabled(0, value) && !isAnalog) { + this.startAutofire(0, value, downValue); + } else { + this.gameManager.simulateInput(0, value, downValue); + } } }) }