mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 13:10:16 +00:00
646d7ecaf6
## Description: During spawn phase, disable Radial Menu further. Its options where already greyed out or non-responding on purpose, except for the attack button (middle button) which could only be used to select a spawn point but two clicks for that is convoluted. It was mostly a nuisance, especially on mobile where you where forced to go through the Radial Menu, so tap and then tap again to pick a spawn point. - Now, left click directly places a spawn point. Even if "Left click opens menu" is enabled. - And right click does not open Radial menu anymore. Which had no use anyway. And also makes touch screen and mouse players more alike in that they now both have no access to the Radial Menu (which didn't have a purpose anyway except picking spawn point in a convoluted way with two clicks). - On touch screen during spawn phase, the Radial Menu also doesn't open anymore. Instead of two taps (open Radial Menu > tap attack button), now one tap suffices to pick a spawn point just like one left mouse click now does. Fixes https://github.com/openfrontio/OpenFrontIO/issues/3609 Also from UnitLayer > onTouch: - remove redundant isValidRef check. Since isValidCoord check was added in PR 3226 above it, we know it is a correct coord and with that correct ref, already. - remove redundant comment about isValidCoord/Ref not being checked there yet intentionally, because it is actually being checked there since PR 3226. ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: tryout33 --------- Co-authored-by: iamlewis <lewismmmm@gmail.com>
743 lines
20 KiB
TypeScript
743 lines
20 KiB
TypeScript
import {
|
|
AutoUpgradeEvent,
|
|
ConfirmGhostStructureEvent,
|
|
InputHandler,
|
|
} from "../src/client/InputHandler";
|
|
import { UIState } from "../src/client/graphics/UIState";
|
|
import { EventBus } from "../src/core/EventBus";
|
|
import { UnitType } from "../src/core/game/Game";
|
|
import { GameView } from "../src/core/game/GameView";
|
|
|
|
class MockPointerEvent {
|
|
button: number;
|
|
clientX: number;
|
|
clientY: number;
|
|
x: number;
|
|
y: number;
|
|
pointerId: number;
|
|
type: string;
|
|
pointerType: string;
|
|
preventDefault: () => void;
|
|
|
|
constructor(type: string, init: any) {
|
|
this.type = type;
|
|
this.button = init.button;
|
|
this.clientX = init.clientX;
|
|
this.clientY = init.clientY;
|
|
this.x = init.x ?? init.clientX;
|
|
this.y = init.y ?? init.clientY;
|
|
this.pointerId = init.pointerId;
|
|
this.pointerType = init.pointerType ?? "mouse";
|
|
this.preventDefault = vi.fn();
|
|
}
|
|
}
|
|
|
|
global.PointerEvent = MockPointerEvent as any;
|
|
|
|
describe("InputHandler AutoUpgrade", () => {
|
|
let inputHandler: InputHandler;
|
|
let mockGameView: GameView;
|
|
let eventBus: EventBus;
|
|
let mockCanvas: HTMLCanvasElement;
|
|
|
|
beforeEach(() => {
|
|
mockGameView = { inSpawnPhase: () => false } as GameView;
|
|
mockCanvas = document.createElement("canvas");
|
|
mockCanvas.width = 800;
|
|
mockCanvas.height = 600;
|
|
|
|
eventBus = new EventBus();
|
|
|
|
inputHandler = new InputHandler(
|
|
mockGameView,
|
|
{
|
|
attackRatio: 20,
|
|
ghostStructure: null,
|
|
rocketDirectionUp: true,
|
|
overlappingRailroads: [],
|
|
ghostRailPaths: [],
|
|
},
|
|
mockCanvas,
|
|
eventBus,
|
|
);
|
|
});
|
|
|
|
afterEach(() => {
|
|
inputHandler.destroy();
|
|
});
|
|
|
|
describe("Middle Mouse Button Handling", () => {
|
|
test("should emit AutoUpgradeEvent on middle mouse button press", () => {
|
|
const mockEmit = vi.spyOn(eventBus, "emit");
|
|
|
|
const pointerEvent = new PointerEvent("pointerdown", {
|
|
button: 1,
|
|
clientX: 150,
|
|
clientY: 250,
|
|
pointerId: 1,
|
|
});
|
|
|
|
inputHandler["onPointerDown"](pointerEvent);
|
|
|
|
expect(mockEmit).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
x: 150,
|
|
y: 250,
|
|
}),
|
|
);
|
|
});
|
|
|
|
test("should emit MouseDownEvent on left mouse button press instead of AutoUpgradeEvent", () => {
|
|
const mockEmit = vi.spyOn(eventBus, "emit");
|
|
|
|
const pointerEvent = new PointerEvent("pointerdown", {
|
|
button: 0,
|
|
clientX: 150,
|
|
clientY: 250,
|
|
pointerId: 1,
|
|
});
|
|
|
|
inputHandler["onPointerDown"](pointerEvent);
|
|
|
|
expect(mockEmit).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
x: 150,
|
|
y: 250,
|
|
}),
|
|
);
|
|
|
|
const calls = mockEmit.mock.calls;
|
|
const lastCall = calls[calls.length - 1];
|
|
expect(lastCall[0]).not.toBeInstanceOf(AutoUpgradeEvent);
|
|
});
|
|
|
|
test("should not emit AutoUpgradeEvent on right mouse button press", () => {
|
|
const mockEmit = vi.spyOn(eventBus, "emit");
|
|
|
|
const pointerEvent = new PointerEvent("pointerdown", {
|
|
button: 2,
|
|
clientX: 150,
|
|
clientY: 250,
|
|
pointerId: 1,
|
|
});
|
|
|
|
inputHandler["onPointerDown"](pointerEvent);
|
|
|
|
expect(mockEmit).not.toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
x: 150,
|
|
y: 250,
|
|
}),
|
|
);
|
|
});
|
|
|
|
test("should handle multiple middle mouse button presses", () => {
|
|
const mockEmit = vi.spyOn(eventBus, "emit");
|
|
|
|
const pointerEvent1 = new PointerEvent("pointerdown", {
|
|
button: 1,
|
|
clientX: 100,
|
|
clientY: 200,
|
|
pointerId: 1,
|
|
});
|
|
inputHandler["onPointerDown"](pointerEvent1);
|
|
|
|
const pointerEvent2 = new PointerEvent("pointerdown", {
|
|
button: 1,
|
|
clientX: 300,
|
|
clientY: 400,
|
|
pointerId: 2,
|
|
});
|
|
inputHandler["onPointerDown"](pointerEvent2);
|
|
|
|
expect(mockEmit).toHaveBeenCalledTimes(2);
|
|
expect(mockEmit).toHaveBeenNthCalledWith(
|
|
1,
|
|
expect.objectContaining({
|
|
x: 100,
|
|
y: 200,
|
|
}),
|
|
);
|
|
expect(mockEmit).toHaveBeenNthCalledWith(
|
|
2,
|
|
expect.objectContaining({
|
|
x: 300,
|
|
y: 400,
|
|
}),
|
|
);
|
|
});
|
|
|
|
test("should handle middle mouse button press with zero coordinates", () => {
|
|
const mockEmit = vi.spyOn(eventBus, "emit");
|
|
|
|
const pointerEvent = new PointerEvent("pointerdown", {
|
|
button: 1,
|
|
clientX: 0,
|
|
clientY: 0,
|
|
pointerId: 1,
|
|
});
|
|
|
|
inputHandler["onPointerDown"](pointerEvent);
|
|
|
|
expect(mockEmit).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
x: 0,
|
|
y: 0,
|
|
}),
|
|
);
|
|
});
|
|
|
|
test("should handle middle mouse button press with negative coordinates", () => {
|
|
const mockEmit = vi.spyOn(eventBus, "emit");
|
|
|
|
const pointerEvent = new PointerEvent("pointerdown", {
|
|
button: 1,
|
|
clientX: -100,
|
|
clientY: -200,
|
|
pointerId: 1,
|
|
});
|
|
|
|
inputHandler["onPointerDown"](pointerEvent);
|
|
|
|
expect(mockEmit).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
x: -100,
|
|
y: -200,
|
|
}),
|
|
);
|
|
});
|
|
|
|
test("should handle middle mouse button press with decimal coordinates", () => {
|
|
const mockEmit = vi.spyOn(eventBus, "emit");
|
|
|
|
const pointerEvent = new PointerEvent("pointerdown", {
|
|
button: 1,
|
|
clientX: 100.5,
|
|
clientY: 200.7,
|
|
pointerId: 1,
|
|
});
|
|
|
|
inputHandler["onPointerDown"](pointerEvent);
|
|
|
|
expect(mockEmit).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
x: 100.5,
|
|
y: 200.7,
|
|
}),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("Spawn Phase Handling", () => {
|
|
test("should emit MouseUpEvent and not ContextMenuEvent on left click release during spawn phase", () => {
|
|
mockGameView.inSpawnPhase = () => true;
|
|
const mockEmit = vi.spyOn(eventBus, "emit");
|
|
|
|
inputHandler["userSettings"].leftClickOpensMenu = () => true;
|
|
|
|
const pointerEvent = new PointerEvent("pointerup", {
|
|
button: 0,
|
|
clientX: 150,
|
|
clientY: 250,
|
|
});
|
|
inputHandler["lastPointerDownX"] = 149;
|
|
inputHandler["lastPointerDownY"] = 249;
|
|
|
|
inputHandler["onPointerUp"](pointerEvent);
|
|
|
|
expect(mockEmit).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
x: 150,
|
|
y: 250,
|
|
}),
|
|
);
|
|
const emittedTypes = mockEmit.mock.calls.map(
|
|
(call) => call[0].constructor.name,
|
|
);
|
|
expect(emittedTypes).toContain("MouseUpEvent");
|
|
expect(emittedTypes).not.toContain("ContextMenuEvent");
|
|
});
|
|
|
|
test("should suppress/ignore context menu events during spawn phase", () => {
|
|
mockGameView.inSpawnPhase = () => true;
|
|
const mockEmit = vi.spyOn(eventBus, "emit");
|
|
|
|
const mouseEvent = new MouseEvent("contextmenu", {
|
|
clientX: 150,
|
|
clientY: 250,
|
|
});
|
|
const preventDefaultSpy = vi.spyOn(mouseEvent, "preventDefault");
|
|
|
|
inputHandler["onContextMenu"](mouseEvent);
|
|
|
|
expect(preventDefaultSpy).toHaveBeenCalled();
|
|
const emittedTypes = mockEmit.mock.calls.map(
|
|
(call) => call[0].constructor.name,
|
|
);
|
|
expect(emittedTypes).not.toContain("ContextMenuEvent");
|
|
});
|
|
});
|
|
|
|
describe("Pointer Event Handling", () => {
|
|
test("should handle pointer events with different pointer IDs", () => {
|
|
const mockEmit = vi.spyOn(eventBus, "emit");
|
|
|
|
const pointerEvent1 = new PointerEvent("pointerdown", {
|
|
button: 1,
|
|
clientX: 100,
|
|
clientY: 200,
|
|
pointerId: 1,
|
|
});
|
|
inputHandler["onPointerDown"](pointerEvent1);
|
|
|
|
const pointerEvent2 = new PointerEvent("pointerdown", {
|
|
button: 1,
|
|
clientX: 300,
|
|
clientY: 400,
|
|
pointerId: 2,
|
|
});
|
|
inputHandler["onPointerDown"](pointerEvent2);
|
|
|
|
expect(mockEmit).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
test("should handle pointer events with same pointer ID", () => {
|
|
const mockEmit = vi.spyOn(eventBus, "emit");
|
|
|
|
const pointerEvent1 = new PointerEvent("pointerdown", {
|
|
button: 1,
|
|
clientX: 100,
|
|
clientY: 200,
|
|
pointerId: 1,
|
|
});
|
|
inputHandler["onPointerDown"](pointerEvent1);
|
|
|
|
const pointerEvent2 = new PointerEvent("pointerdown", {
|
|
button: 1,
|
|
clientX: 300,
|
|
clientY: 400,
|
|
pointerId: 1,
|
|
});
|
|
inputHandler["onPointerDown"](pointerEvent2);
|
|
|
|
expect(mockEmit).toHaveBeenCalledTimes(2);
|
|
});
|
|
});
|
|
|
|
describe("Edge Cases", () => {
|
|
test("should handle very large coordinates", () => {
|
|
const mockEmit = vi.spyOn(eventBus, "emit");
|
|
|
|
const pointerEvent = new PointerEvent("pointerdown", {
|
|
button: 1,
|
|
clientX: Number.MAX_SAFE_INTEGER,
|
|
clientY: Number.MAX_SAFE_INTEGER,
|
|
pointerId: 1,
|
|
});
|
|
|
|
inputHandler["onPointerDown"](pointerEvent);
|
|
|
|
expect(mockEmit).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
x: Number.MAX_SAFE_INTEGER,
|
|
y: Number.MAX_SAFE_INTEGER,
|
|
}),
|
|
);
|
|
});
|
|
|
|
test("should handle very small coordinates", () => {
|
|
const mockEmit = vi.spyOn(eventBus, "emit");
|
|
|
|
const pointerEvent = new PointerEvent("pointerdown", {
|
|
button: 1,
|
|
clientX: Number.MIN_SAFE_INTEGER,
|
|
clientY: Number.MIN_SAFE_INTEGER,
|
|
pointerId: 1,
|
|
});
|
|
|
|
inputHandler["onPointerDown"](pointerEvent);
|
|
|
|
expect(mockEmit).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
x: Number.MIN_SAFE_INTEGER,
|
|
y: Number.MIN_SAFE_INTEGER,
|
|
}),
|
|
);
|
|
});
|
|
|
|
test("should handle NaN coordinates", () => {
|
|
const mockEmit = vi.spyOn(eventBus, "emit");
|
|
|
|
const pointerEvent = new PointerEvent("pointerdown", {
|
|
button: 1,
|
|
clientX: NaN,
|
|
clientY: NaN,
|
|
pointerId: 1,
|
|
});
|
|
|
|
inputHandler["onPointerDown"](pointerEvent);
|
|
|
|
expect(mockEmit).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
x: NaN,
|
|
y: NaN,
|
|
}),
|
|
);
|
|
});
|
|
|
|
test("should handle Infinity coordinates", () => {
|
|
const mockEmit = vi.spyOn(eventBus, "emit");
|
|
|
|
const pointerEvent = new PointerEvent("pointerdown", {
|
|
button: 1,
|
|
clientX: Infinity,
|
|
clientY: -Infinity,
|
|
pointerId: 1,
|
|
});
|
|
|
|
inputHandler["onPointerDown"](pointerEvent);
|
|
|
|
expect(mockEmit).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
x: Infinity,
|
|
y: -Infinity,
|
|
}),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("Integration with Event Bus", () => {
|
|
test("should allow event listeners to receive AutoUpgradeEvents", () => {
|
|
const mockListener = vi.fn();
|
|
|
|
eventBus.on(AutoUpgradeEvent, mockListener);
|
|
|
|
const pointerEvent = new PointerEvent("pointerdown", {
|
|
button: 1,
|
|
clientX: 150,
|
|
clientY: 250,
|
|
pointerId: 1,
|
|
});
|
|
inputHandler["onPointerDown"](pointerEvent);
|
|
|
|
expect(mockListener).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
x: 150,
|
|
y: 250,
|
|
}),
|
|
);
|
|
});
|
|
|
|
test("should allow multiple listeners for AutoUpgradeEvent", () => {
|
|
const mockListener1 = vi.fn();
|
|
const mockListener2 = vi.fn();
|
|
|
|
eventBus.on(AutoUpgradeEvent, mockListener1);
|
|
eventBus.on(AutoUpgradeEvent, mockListener2);
|
|
|
|
const pointerEvent = new PointerEvent("pointerdown", {
|
|
button: 1,
|
|
clientX: 150,
|
|
clientY: 250,
|
|
pointerId: 1,
|
|
});
|
|
inputHandler["onPointerDown"](pointerEvent);
|
|
|
|
expect(mockListener1).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
x: 150,
|
|
y: 250,
|
|
}),
|
|
);
|
|
expect(mockListener2).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
x: 150,
|
|
y: 250,
|
|
}),
|
|
);
|
|
});
|
|
|
|
test("should not call unsubscribed listeners", () => {
|
|
const mockListener = vi.fn();
|
|
|
|
eventBus.on(AutoUpgradeEvent, mockListener);
|
|
eventBus.off(AutoUpgradeEvent, mockListener);
|
|
|
|
const pointerEvent = new PointerEvent("pointerdown", {
|
|
button: 1,
|
|
clientX: 150,
|
|
clientY: 250,
|
|
pointerId: 1,
|
|
});
|
|
inputHandler["onPointerDown"](pointerEvent);
|
|
|
|
expect(mockListener).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe("Keybinds JSON parsing", () => {
|
|
beforeEach(() => {
|
|
localStorage.removeItem("settings.keybinds");
|
|
});
|
|
|
|
test("parses nested object values and flattens them to strings", () => {
|
|
const nested = {
|
|
moveUp: { key: "moveUp", value: "KeyZ" },
|
|
};
|
|
localStorage.setItem("settings.keybinds", JSON.stringify(nested));
|
|
|
|
inputHandler.initialize();
|
|
|
|
expect((inputHandler as any).keybinds.moveUp).toBe("KeyZ");
|
|
});
|
|
|
|
test("accepts legacy string values", () => {
|
|
localStorage.setItem(
|
|
"settings.keybinds",
|
|
JSON.stringify({ moveUp: "KeyX" }),
|
|
);
|
|
|
|
inputHandler.initialize();
|
|
|
|
expect((inputHandler as any).keybinds.moveUp).toBe("KeyX");
|
|
});
|
|
|
|
test("ignores non-string values and preserves defaults, but keeps 'Null' for unbound keys", () => {
|
|
const mixed = {
|
|
moveUp: { key: "moveUp", value: null },
|
|
moveLeft: "Null",
|
|
};
|
|
localStorage.setItem("settings.keybinds", JSON.stringify(mixed));
|
|
|
|
inputHandler.initialize();
|
|
|
|
expect((inputHandler as any).keybinds.moveUp).toBe("KeyW");
|
|
// "Null" is preserved to indicate unbound keybind
|
|
expect((inputHandler as any).keybinds.moveLeft).toBe("Null");
|
|
});
|
|
|
|
test("handles invalid JSON gracefully and warns", () => {
|
|
const spy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
localStorage.setItem("settings.keybinds", "not a json");
|
|
|
|
inputHandler.initialize();
|
|
|
|
expect(spy).toHaveBeenCalled();
|
|
// default remains when parsing fails
|
|
expect((inputHandler as any).keybinds.moveUp).toBe("KeyW");
|
|
spy.mockRestore();
|
|
});
|
|
});
|
|
|
|
describe("Enter key confirm ghost structure", () => {
|
|
let uiState: UIState;
|
|
|
|
beforeEach(() => {
|
|
localStorage.removeItem("settings.keybinds");
|
|
uiState = {
|
|
attackRatio: 20,
|
|
ghostStructure: null,
|
|
rocketDirectionUp: true,
|
|
overlappingRailroads: [],
|
|
ghostRailPaths: [],
|
|
} as UIState;
|
|
inputHandler = new InputHandler(
|
|
mockGameView,
|
|
uiState,
|
|
mockCanvas,
|
|
eventBus,
|
|
);
|
|
inputHandler.initialize();
|
|
});
|
|
|
|
test("emits ConfirmGhostStructureEvent on Enter when ghost structure is set", () => {
|
|
const mockEmit = vi.spyOn(eventBus, "emit");
|
|
uiState.ghostStructure = UnitType.City;
|
|
|
|
window.dispatchEvent(new KeyboardEvent("keydown", { code: "Enter" }));
|
|
|
|
expect(mockEmit).toHaveBeenCalledWith(
|
|
expect.any(ConfirmGhostStructureEvent),
|
|
);
|
|
});
|
|
|
|
test("emits ConfirmGhostStructureEvent on NumpadEnter when ghost structure is set", () => {
|
|
const mockEmit = vi.spyOn(eventBus, "emit");
|
|
uiState.ghostStructure = UnitType.Factory;
|
|
|
|
window.dispatchEvent(
|
|
new KeyboardEvent("keydown", { code: "NumpadEnter" }),
|
|
);
|
|
|
|
expect(mockEmit).toHaveBeenCalledWith(
|
|
expect.any(ConfirmGhostStructureEvent),
|
|
);
|
|
});
|
|
|
|
test("does not emit ConfirmGhostStructureEvent on Enter when no ghost structure", () => {
|
|
const mockEmit = vi.spyOn(eventBus, "emit");
|
|
expect(uiState.ghostStructure).toBeNull();
|
|
|
|
window.dispatchEvent(new KeyboardEvent("keydown", { code: "Enter" }));
|
|
|
|
const confirmCalls = mockEmit.mock.calls.filter(
|
|
(call) => call[0] instanceof ConfirmGhostStructureEvent,
|
|
);
|
|
expect(confirmCalls).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
describe("Numpad number keys for build keybinds", () => {
|
|
beforeEach(() => {
|
|
localStorage.removeItem("settings.keybinds");
|
|
inputHandler.destroy();
|
|
const uiState: UIState = {
|
|
attackRatio: 20,
|
|
ghostStructure: null,
|
|
rocketDirectionUp: true,
|
|
overlappingRailroads: [],
|
|
ghostRailPaths: [],
|
|
} as UIState;
|
|
inputHandler = new InputHandler(
|
|
mockGameView,
|
|
uiState,
|
|
mockCanvas,
|
|
eventBus,
|
|
);
|
|
inputHandler.initialize();
|
|
});
|
|
|
|
test("Numpad1 sets ghost structure to City when buildCity is Digit1", () => {
|
|
window.dispatchEvent(
|
|
new KeyboardEvent("keyup", { code: "Numpad1", key: "1" }),
|
|
);
|
|
expect(inputHandler["uiState"].ghostStructure).toBe(UnitType.City);
|
|
});
|
|
|
|
test("Numpad5 sets ghost structure to MissileSilo when buildMissileSilo is Digit5", () => {
|
|
window.dispatchEvent(
|
|
new KeyboardEvent("keyup", { code: "Numpad5", key: "5" }),
|
|
);
|
|
expect(inputHandler["uiState"].ghostStructure).toBe(UnitType.MissileSilo);
|
|
});
|
|
|
|
test("Numpad0 sets ghost structure to MIRV when buildMIRV is Digit0", () => {
|
|
window.dispatchEvent(
|
|
new KeyboardEvent("keyup", { code: "Numpad0", key: "0" }),
|
|
);
|
|
expect(inputHandler["uiState"].ghostStructure).toBe(UnitType.MIRV);
|
|
});
|
|
});
|
|
|
|
describe("Build keybind two-phase matching (exact code first, then digit/Numpad alias)", () => {
|
|
beforeEach(() => {
|
|
localStorage.removeItem("settings.keybinds");
|
|
inputHandler.destroy();
|
|
const uiState: UIState = {
|
|
attackRatio: 20,
|
|
ghostStructure: null,
|
|
rocketDirectionUp: true,
|
|
overlappingRailroads: [],
|
|
ghostRailPaths: [],
|
|
} as UIState;
|
|
inputHandler = new InputHandler(
|
|
mockGameView,
|
|
uiState,
|
|
mockCanvas,
|
|
eventBus,
|
|
);
|
|
inputHandler.initialize();
|
|
});
|
|
|
|
test("exact code match wins: Digit1 sets City when buildCity=Digit1 and buildFactory=Numpad1", () => {
|
|
localStorage.setItem(
|
|
"settings.keybinds",
|
|
JSON.stringify({
|
|
buildCity: "Digit1",
|
|
buildFactory: "Numpad1",
|
|
}),
|
|
);
|
|
inputHandler.destroy();
|
|
const uiState: UIState = {
|
|
attackRatio: 20,
|
|
ghostStructure: null,
|
|
rocketDirectionUp: true,
|
|
overlappingRailroads: [],
|
|
ghostRailPaths: [],
|
|
} as UIState;
|
|
inputHandler = new InputHandler(
|
|
mockGameView,
|
|
uiState,
|
|
mockCanvas,
|
|
eventBus,
|
|
);
|
|
inputHandler.initialize();
|
|
|
|
window.dispatchEvent(
|
|
new KeyboardEvent("keyup", { code: "Digit1", key: "1" }),
|
|
);
|
|
|
|
expect(inputHandler["uiState"].ghostStructure).toBe(UnitType.City);
|
|
});
|
|
|
|
test("exact code match wins: Numpad1 sets Factory when buildCity=Digit1 and buildFactory=Numpad1", () => {
|
|
localStorage.setItem(
|
|
"settings.keybinds",
|
|
JSON.stringify({
|
|
buildCity: "Digit1",
|
|
buildFactory: "Numpad1",
|
|
}),
|
|
);
|
|
inputHandler.destroy();
|
|
const uiState: UIState = {
|
|
attackRatio: 20,
|
|
ghostStructure: null,
|
|
rocketDirectionUp: true,
|
|
overlappingRailroads: [],
|
|
ghostRailPaths: [],
|
|
} as UIState;
|
|
inputHandler = new InputHandler(
|
|
mockGameView,
|
|
uiState,
|
|
mockCanvas,
|
|
eventBus,
|
|
);
|
|
inputHandler.initialize();
|
|
|
|
window.dispatchEvent(
|
|
new KeyboardEvent("keyup", { code: "Numpad1", key: "1" }),
|
|
);
|
|
|
|
expect(inputHandler["uiState"].ghostStructure).toBe(UnitType.Factory);
|
|
});
|
|
|
|
test("digit alias used when no exact match: Numpad1 sets City when only buildCity=Digit1", () => {
|
|
localStorage.setItem(
|
|
"settings.keybinds",
|
|
JSON.stringify({ buildCity: "Digit1" }),
|
|
);
|
|
inputHandler.destroy();
|
|
const uiState: UIState = {
|
|
attackRatio: 20,
|
|
ghostStructure: null,
|
|
rocketDirectionUp: true,
|
|
overlappingRailroads: [],
|
|
ghostRailPaths: [],
|
|
} as UIState;
|
|
inputHandler = new InputHandler(
|
|
mockGameView,
|
|
uiState,
|
|
mockCanvas,
|
|
eventBus,
|
|
);
|
|
inputHandler.initialize();
|
|
|
|
window.dispatchEvent(
|
|
new KeyboardEvent("keyup", { code: "Numpad1", key: "1" }),
|
|
);
|
|
|
|
expect(inputHandler["uiState"].ghostStructure).toBe(UnitType.City);
|
|
});
|
|
});
|
|
});
|