diff --git a/data/localization/ar.json b/data/localization/ar.json index 7273758..ae36924 100644 --- a/data/localization/ar.json +++ b/data/localization/ar.json @@ -297,5 +297,6 @@ "DPAD_UP": "DPAD_UP", "DPAD_DOWN": "DPAD_DOWN", "DPAD_LEFT": "DPAD_LEFT", - "DPAD_RIGHT": "DPAD_RIGHT" + "DPAD_RIGHT": "DPAD_RIGHT", + "Autofire": "Autofire" } diff --git a/data/localization/bn.json b/data/localization/bn.json index 00a53e8..c174652 100644 --- a/data/localization/bn.json +++ b/data/localization/bn.json @@ -297,5 +297,6 @@ "DPAD_UP": "DPAD_UP", "DPAD_DOWN": "DPAD_DOWN", "DPAD_LEFT": "DPAD_LEFT", - "DPAD_RIGHT": "DPAD_RIGHT" + "DPAD_RIGHT": "DPAD_RIGHT", + "Autofire": "Autofire" } diff --git a/data/localization/de.json b/data/localization/de.json index 35b6d5d..0768690 100644 --- a/data/localization/de.json +++ b/data/localization/de.json @@ -297,5 +297,6 @@ "DPAD_UP": "DPAD_UP", "DPAD_DOWN": "DPAD_DOWN", "DPAD_LEFT": "DPAD_LEFT", - "DPAD_RIGHT": "DPAD_RIGHT" + "DPAD_RIGHT": "DPAD_RIGHT", + "Autofire": "Autofire" } diff --git a/data/localization/el.json b/data/localization/el.json index 899acb4..a5cc7be 100644 --- a/data/localization/el.json +++ b/data/localization/el.json @@ -297,5 +297,6 @@ "DPAD_UP": "DPAD_UP", "DPAD_DOWN": "DPAD_DOWN", "DPAD_LEFT": "DPAD_LEFT", - "DPAD_RIGHT": "DPAD_RIGHT" + "DPAD_RIGHT": "DPAD_RIGHT", + "Autofire": "Autofire" } diff --git a/data/localization/en.json b/data/localization/en.json index aeee06e..371da02 100644 --- a/data/localization/en.json +++ b/data/localization/en.json @@ -335,5 +335,6 @@ "System Save interval": "System Save interval", "Menu Bar Button": "Menu Bar Button", "visible": "visible", - "hidden": "hidden" + "hidden": "hidden", + "Autofire": "Autofire" } diff --git a/data/localization/es.json b/data/localization/es.json index a780dfb..a6c4c10 100644 --- a/data/localization/es.json +++ b/data/localization/es.json @@ -306,5 +306,6 @@ "DPAD_UP": "DPAD ARRIBA", "DPAD_DOWN": "DPAD_ABAJO", "DPAD_LEFT": "DPAD_IZQUIERDA", - "DPAD_RIGHT": "DPAD_DERECHO" + "DPAD_RIGHT": "DPAD_DERECHO", + "Autofire": "Autofire" } diff --git a/data/localization/fa.json b/data/localization/fa.json index a350473..1565686 100644 --- a/data/localization/fa.json +++ b/data/localization/fa.json @@ -306,5 +306,6 @@ "DPAD_UP": "دی پد بالا", "DPAD_DOWN": "دی پد پایین", "DPAD_LEFT": "دی پد چپ", - "DPAD_RIGHT": "دی پد راست" + "DPAD_RIGHT": "دی پد راست", + "Autofire": "Autofire" } diff --git a/data/localization/fr.json b/data/localization/fr.json index 8a32070..31f619b 100644 --- a/data/localization/fr.json +++ b/data/localization/fr.json @@ -335,5 +335,6 @@ "System Save interval": "Intervalle de sauvegarde du système", "Menu Bar Button": "Bouton de la barre de menu", "visible": "visible", - "hidden": "masqué" + "hidden": "masqué", + "Autofire": "Autofire" } diff --git a/data/localization/hi.json b/data/localization/hi.json index 5fd5cac..00ccf54 100644 --- a/data/localization/hi.json +++ b/data/localization/hi.json @@ -297,5 +297,6 @@ "DPAD_UP": "डीपीएडी_यूपी", "DPAD_DOWN": "डीपीएडी_डाउन", "DPAD_LEFT": "DPAD_LEFT", - "DPAD_RIGHT": "डीपीएडी_दाएँ" + "DPAD_RIGHT": "डीपीएडी_दाएँ", + "Autofire": "Autofire" } diff --git a/data/localization/it.json b/data/localization/it.json index 1dffbd2..7ad647c 100644 --- a/data/localization/it.json +++ b/data/localization/it.json @@ -298,5 +298,6 @@ "DPAD_UP": "DPAD_SU", "DPAD_DOWN": "DPAD_GIU", "DPAD_LEFT": "DPAD_SINISTRA", - "DPAD_RIGHT": "DPAD_DESTRA" + "DPAD_RIGHT": "DPAD_DESTRA", + "Autofire": "Autofire" } diff --git a/data/localization/ja.json b/data/localization/ja.json index a7d93f4..291c879 100644 --- a/data/localization/ja.json +++ b/data/localization/ja.json @@ -297,5 +297,6 @@ "DPAD_UP": "十字キー上", "DPAD_DOWN": "十字キー下", "DPAD_LEFT": "十字キー左", - "DPAD_RIGHT": "十字キー右" + "DPAD_RIGHT": "十字キー右", + "Autofire": "Autofire" } diff --git a/data/localization/jv.json b/data/localization/jv.json index d55a3c7..fc44aa9 100644 --- a/data/localization/jv.json +++ b/data/localization/jv.json @@ -297,5 +297,6 @@ "DPAD_UP": "DPAD_UP", "DPAD_DOWN": "DPAD_DOWN", "DPAD_LEFT": "DPAD_LEFT", - "DPAD_RIGHT": "DPAD_RIGHT" + "DPAD_RIGHT": "DPAD_RIGHT", + "Autofire": "Autofire" } diff --git a/data/localization/ko.json b/data/localization/ko.json index 10271c5..71488f8 100644 --- a/data/localization/ko.json +++ b/data/localization/ko.json @@ -368,5 +368,6 @@ "Screen Recording Audio Bitrate": "화면 녹화 오디오 비트레이트", "customDownload": "상태 파일 다운로드", "customUpload": "상태 파일 업로드", - "customScreenshot": "스크린 샷 찍기" + "customScreenshot": "스크린 샷 찍기", + "Autofire": "Autofire" } diff --git a/data/localization/pt.json b/data/localization/pt.json index d0e88bc..d1f2002 100644 --- a/data/localization/pt.json +++ b/data/localization/pt.json @@ -357,5 +357,6 @@ "pcsx rearmed gunconadjustx": "pcsx rearmed - gunconadjustx", "pcsx rearmed gunconadjusty": "pcsx rearmed - gunconadjusty", "pcsx rearmed gunconadjustratiox": "pcsx rearmed - gunconadjustratiox", - "pcsx rearmed gunconadjustratioy": "pcsx rearmed - gunconadjustratioy" + "pcsx rearmed gunconadjustratioy": "pcsx rearmed - gunconadjustratioy", + "Autofire": "Autofire" } diff --git a/data/localization/ro.json b/data/localization/ro.json index 1b547b1..74c3667 100644 --- a/data/localization/ro.json +++ b/data/localization/ro.json @@ -306,5 +306,6 @@ "DPAD_UP": "DPAD_UP", "DPAD_DOWN": "DPAD_DOWN", "DPAD_LEFT": "DPAD_LEFT", - "DPAD_RIGHT": "DPAD_RIGHT" + "DPAD_RIGHT": "DPAD_RIGHT", + "Autofire": "Autofire" } diff --git a/data/localization/ru.json b/data/localization/ru.json index ca54a75..3fc2faa 100644 --- a/data/localization/ru.json +++ b/data/localization/ru.json @@ -297,5 +297,6 @@ "DPAD_UP": "DPAD_ВВЕРХ", "DPAD_DOWN": "DPAD_ВНИЗ", "DPAD_LEFT": "DPAD_ВЛЕВО", - "DPAD_RIGHT": "DPAD_СПРАВО" + "DPAD_RIGHT": "DPAD_СПРАВО", + "Autofire": "Autofire" } diff --git a/data/localization/tr.json b/data/localization/tr.json index 6521297..9cab6fe 100644 --- a/data/localization/tr.json +++ b/data/localization/tr.json @@ -306,5 +306,6 @@ "DPAD_UP": "DPAD_YUKARI", "DPAD_DOWN": "DPAD_AŞAĞI", "DPAD_LEFT": "DPAD_SOL", - "DPAD_RIGHT": "DPAD_SAĞ" + "DPAD_RIGHT": "DPAD_SAĞ", + "Autofire": "Autofire" } diff --git a/data/localization/vi.json b/data/localization/vi.json index a4990cd..781ddb0 100644 --- a/data/localization/vi.json +++ b/data/localization/vi.json @@ -298,5 +298,6 @@ "DPAD_UP": "DPAD_UP", "DPAD_DOWN": "DPAD_DOWN", "DPAD_LEFT": "DPAD_LEFT", - "DPAD_RIGHT": "DPAD_RIGHT" + "DPAD_RIGHT": "DPAD_RIGHT", + "Autofire": "Autofire" } diff --git a/data/localization/zh.json b/data/localization/zh.json index 4109af6..2322ca3 100644 --- a/data/localization/zh.json +++ b/data/localization/zh.json @@ -347,5 +347,6 @@ "Movement Anywhere": "Movement Anywhere", "Direct Keyboard Input": "Direct Keyboard Input", "Forward Alt key": "Forward Alt key", - "Lock Mouse": "Lock Mouse" + "Lock Mouse": "Lock Mouse", + "Autofire": "Autofire" } diff --git a/data/src/emulator.js b/data/src/emulator.js index f838567..e3c53b5 100644 --- a/data/src/emulator.js +++ b/data/src/emulator.js @@ -238,7 +238,21 @@ 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, buttons] of Object.entries(this.config.defaultControllers)) { + this.defaultControllers[player] = this.defaultControllers[player] || {}; + + for (const [button, config] of Object.entries(buttons)) { + this.defaultControllers[player][button] = { + ...(this.defaultControllers[player][button] || {}), + ...config + }; + } + } + } + this.defaultAutoFireInterval = 100; + this.autofireIntervals = {}; this.muted = false; this.paused = true; this.missingLang = []; @@ -1146,7 +1160,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); @@ -2490,12 +2504,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(); @@ -2991,7 +3007,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"); @@ -3001,12 +3017,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"); @@ -3057,7 +3083,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;"; @@ -3125,23 +3151,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"); @@ -3308,6 +3374,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", @@ -3435,6 +3503,49 @@ 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; + } + const settingValue = this.getSettingValue("autofireInterval"); + return settingValue ? parseInt(settingValue) : 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; + const interval = this.getAutofireInterval(playerIndex, buttonIndex); + this.autofireIntervals[key] = setInterval(() => { + if (this.paused || !this.gameManager) return; + 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; @@ -3452,11 +3563,19 @@ 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; + const isKeyUp = e.type === "keyup"; + const value = isKeyUp ? 0 : inputValue; + + if (this.isAutofireEnabled(i, j) && !isAnalog) { + isKeyUp ? this.stopAutofire(i, j) : this.startAutofire(i, j, inputValue); + } else { + this.gameManager.simulateInput(i, j, value); + } } } } @@ -3489,7 +3608,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++) { @@ -3497,12 +3615,21 @@ 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; + const isButtonUp = e.type === "buttonup"; + const value = isButtonUp ? 0 : inputValue; + + if (this.isAutofireEnabled(i, j) && !isAnalog) { + isButtonUp ? this.stopAutofire(i, j) : this.startAutofire(i, j, inputValue); + } else { + this.gameManager.simulateInput(i, j, value); + } } 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); @@ -3915,14 +4042,19 @@ 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.stopAutofire(0, value); 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); + } } }) } @@ -4433,6 +4565,8 @@ class EmulatorJS { this.gameManager.setAltKeyEnabled(value === "enabled"); } else if (option === "lockMouse") { this.enableMouseLock = (value === "enabled"); + } else if (option === "autofireInterval") { + this.defaultAutoFireInterval = parseInt(value); } } menuOptionChanged(option, value) { @@ -5098,6 +5232,14 @@ class EmulatorJS { "enabled": this.localization("Enabled"), }, (this.enableMouseLock === true ? "enabled" : "disabled"), inputOptions, true); + addToMenu(this.localization("Autofire Interval"), "autofireInterval", { + "20": "20ms", + "50": "50ms", + "100": "100ms", + "200": "200ms", + "500": "500ms", + }, "100", inputOptions, true); + checkForEmptyMenu(inputOptions); if (this.saveInBrowserSupported()) {