- added support for autofire for all controls

- global autofire delay configurable via EJS_defaultAutoFireInterval
- each button can have custom autofire delay and be toggled
- configurable upon startup
- can be updated in control settings for each button
- joystick not supported
This commit is contained in:
z 2026-01-17 15:57:57 +13:00
parent 5680535946
commit 60baa7269d
2 changed files with 163 additions and 12 deletions

View File

@ -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 {

View File

@ -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);
}
}
})
}