@muhammedaksam/opentui-doom 0.3.5 → 0.3.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/doom-input.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * DOOM Input Handler
3
- *
3
+ *
4
4
  * Maps OpenTUI keyboard events to DOOM key codes
5
5
  */
6
6
 
@@ -9,47 +9,47 @@ import type { DoomEngine } from "./doom-engine";
9
9
 
10
10
  // DOOM key codes (from doomkeys.h)
11
11
  export const DoomKeys = {
12
- KEY_RIGHTARROW: 0xae,
13
- KEY_LEFTARROW: 0xac,
14
- KEY_UPARROW: 0xad,
15
- KEY_DOWNARROW: 0xaf,
16
- KEY_STRAFE_L: 0xa0,
17
- KEY_STRAFE_R: 0xa1,
18
- KEY_USE: 0xa2,
19
- KEY_FIRE: 0xa3,
20
- KEY_ESCAPE: 27,
21
- KEY_ENTER: 13,
22
- KEY_TAB: 9,
23
- KEY_F1: 0x80 + 0x3b,
24
- KEY_F2: 0x80 + 0x3c,
25
- KEY_F3: 0x80 + 0x3d,
26
- KEY_F4: 0x80 + 0x3e,
27
- KEY_F5: 0x80 + 0x3f,
28
- KEY_F6: 0x80 + 0x40,
29
- KEY_F7: 0x80 + 0x41,
30
- KEY_F8: 0x80 + 0x42,
31
- KEY_F9: 0x80 + 0x43,
32
- KEY_F10: 0x80 + 0x44,
33
- KEY_F11: 0x80 + 0x57,
34
- KEY_F12: 0x80 + 0x58,
35
- KEY_BACKSPACE: 127,
36
- KEY_PAUSE: 0xff,
37
- KEY_EQUALS: 0x3d,
38
- KEY_MINUS: 0x2d,
39
- KEY_RSHIFT: 0x80 + 0x36,
40
- KEY_RCTRL: 0x80 + 0x1d,
41
- KEY_RALT: 0x80 + 0x38,
42
- KEY_LALT: 0x80 + 0x38,
43
- KEY_CAPSLOCK: 0x80 + 0x3a,
44
- KEY_NUMLOCK: 0x80 + 0x45,
45
- KEY_SCRLCK: 0x80 + 0x46,
46
- KEY_PRTSCR: 0x80 + 0x59,
47
- KEY_HOME: 0x80 + 0x47,
48
- KEY_END: 0x80 + 0x4f,
49
- KEY_PGUP: 0x80 + 0x49,
50
- KEY_PGDN: 0x80 + 0x51,
51
- KEY_INS: 0x80 + 0x52,
52
- KEY_DEL: 0x80 + 0x53,
12
+ KEY_RIGHTARROW: 0xae,
13
+ KEY_LEFTARROW: 0xac,
14
+ KEY_UPARROW: 0xad,
15
+ KEY_DOWNARROW: 0xaf,
16
+ KEY_STRAFE_L: 0xa0,
17
+ KEY_STRAFE_R: 0xa1,
18
+ KEY_USE: 0xa2,
19
+ KEY_FIRE: 0xa3,
20
+ KEY_ESCAPE: 27,
21
+ KEY_ENTER: 13,
22
+ KEY_TAB: 9,
23
+ KEY_F1: 0x80 + 0x3b,
24
+ KEY_F2: 0x80 + 0x3c,
25
+ KEY_F3: 0x80 + 0x3d,
26
+ KEY_F4: 0x80 + 0x3e,
27
+ KEY_F5: 0x80 + 0x3f,
28
+ KEY_F6: 0x80 + 0x40,
29
+ KEY_F7: 0x80 + 0x41,
30
+ KEY_F8: 0x80 + 0x42,
31
+ KEY_F9: 0x80 + 0x43,
32
+ KEY_F10: 0x80 + 0x44,
33
+ KEY_F11: 0x80 + 0x57,
34
+ KEY_F12: 0x80 + 0x58,
35
+ KEY_BACKSPACE: 127,
36
+ KEY_PAUSE: 0xff,
37
+ KEY_EQUALS: 0x3d,
38
+ KEY_MINUS: 0x2d,
39
+ KEY_RSHIFT: 0x80 + 0x36,
40
+ KEY_RCTRL: 0x80 + 0x1d,
41
+ KEY_RALT: 0x80 + 0x38,
42
+ KEY_LALT: 0x80 + 0x38,
43
+ KEY_CAPSLOCK: 0x80 + 0x3a,
44
+ KEY_NUMLOCK: 0x80 + 0x45,
45
+ KEY_SCRLCK: 0x80 + 0x46,
46
+ KEY_PRTSCR: 0x80 + 0x59,
47
+ KEY_HOME: 0x80 + 0x47,
48
+ KEY_END: 0x80 + 0x4f,
49
+ KEY_PGUP: 0x80 + 0x49,
50
+ KEY_PGDN: 0x80 + 0x51,
51
+ KEY_INS: 0x80 + 0x52,
52
+ KEY_DEL: 0x80 + 0x53,
53
53
  } as const;
54
54
 
55
55
  // Key state tracking for press/release
@@ -62,68 +62,68 @@ const keyStates = new Map<string, boolean>();
62
62
  * so both gameplay movement and text input work.
63
63
  */
64
64
  function mapKeyToDoom(key: KeyEvent): number[] {
65
- const name = key.name?.toLowerCase() ?? "";
66
-
67
- // Arrow keys
68
- if (name === "up" || key.sequence === "\x1b[A") return [DoomKeys.KEY_UPARROW];
69
- if (name === "down" || key.sequence === "\x1b[B") return [DoomKeys.KEY_DOWNARROW];
70
- if (name === "left" || key.sequence === "\x1b[D") return [DoomKeys.KEY_LEFTARROW];
71
- if (name === "right" || key.sequence === "\x1b[C") return [DoomKeys.KEY_RIGHTARROW];
72
-
73
- // WASD movement - send BOTH movement key AND character
74
- // Movement key ensures gameplay works, character ensures text input works
75
- if (name === "w") return [DoomKeys.KEY_UPARROW, "w".charCodeAt(0)];
76
- if (name === "s") return [DoomKeys.KEY_DOWNARROW, "s".charCodeAt(0)];
77
- if (name === "a") return [DoomKeys.KEY_STRAFE_L, "a".charCodeAt(0)];
78
- if (name === "d") return [DoomKeys.KEY_STRAFE_R, "d".charCodeAt(0)];
79
-
80
- // Action keys
81
- if (name === "space") return [" ".charCodeAt(0)]; // Use
82
- if (name === "return" || name === "enter") return [DoomKeys.KEY_ENTER];
83
- if (name === "escape") return [DoomKeys.KEY_ESCAPE];
84
- if (name === "tab") return [DoomKeys.KEY_TAB];
85
- if (name === "backspace") return [DoomKeys.KEY_BACKSPACE];
86
-
87
- // Fire (Ctrl) - but not Ctrl+C which should exit
88
- if (key.ctrl && key.name !== "c") return [DoomKeys.KEY_FIRE];
89
-
90
- // Alt for strafe
91
- if (key.meta || key.name === "alt") return [DoomKeys.KEY_LALT];
92
-
93
- // Shift for run
94
- if (key.shift) return [DoomKeys.KEY_RSHIFT];
95
-
96
- // Function keys
97
- if (name === "f1") return [DoomKeys.KEY_F1];
98
- if (name === "f2") return [DoomKeys.KEY_F2];
99
- if (name === "f3") return [DoomKeys.KEY_F3];
100
- if (name === "f4") return [DoomKeys.KEY_F4];
101
- if (name === "f5") return [DoomKeys.KEY_F5];
102
- if (name === "f6") return [DoomKeys.KEY_F6];
103
- if (name === "f7") return [DoomKeys.KEY_F7];
104
- if (name === "f8") return [DoomKeys.KEY_F8];
105
- if (name === "f9") return [DoomKeys.KEY_F9];
106
- if (name === "f10") return [DoomKeys.KEY_F10];
107
- if (name === "f11") return [DoomKeys.KEY_F11];
108
- if (name === "f12") return [DoomKeys.KEY_F12];
109
-
110
- // Weapon selection (1-9, 0)
111
- if (name >= "0" && name <= "9") return [name.charCodeAt(0)];
112
-
113
- // Plus/minus for gamma/zoom
114
- if (name === "+" || name === "=") return [DoomKeys.KEY_EQUALS];
115
- if (name === "-") return [DoomKeys.KEY_MINUS];
116
-
117
- // Y/N for prompts
118
- if (name === "y") return ["y".charCodeAt(0)];
119
- if (name === "n") return ["n".charCodeAt(0)];
120
-
121
- // Other letter keys (for cheats, etc)
122
- if (name.length === 1 && name >= "a" && name <= "z") {
123
- return [name.charCodeAt(0)];
124
- }
125
-
126
- return [];
65
+ const name = key.name?.toLowerCase() ?? "";
66
+
67
+ // Arrow keys
68
+ if (name === "up" || key.sequence === "\x1b[A") return [DoomKeys.KEY_UPARROW];
69
+ if (name === "down" || key.sequence === "\x1b[B") return [DoomKeys.KEY_DOWNARROW];
70
+ if (name === "left" || key.sequence === "\x1b[D") return [DoomKeys.KEY_LEFTARROW];
71
+ if (name === "right" || key.sequence === "\x1b[C") return [DoomKeys.KEY_RIGHTARROW];
72
+
73
+ // WASD movement - send BOTH movement key AND character
74
+ // Movement key ensures gameplay works, character ensures text input works
75
+ if (name === "w") return [DoomKeys.KEY_UPARROW, "w".charCodeAt(0)];
76
+ if (name === "s") return [DoomKeys.KEY_DOWNARROW, "s".charCodeAt(0)];
77
+ if (name === "a") return [DoomKeys.KEY_STRAFE_L, "a".charCodeAt(0)];
78
+ if (name === "d") return [DoomKeys.KEY_STRAFE_R, "d".charCodeAt(0)];
79
+
80
+ // Action keys
81
+ if (name === "space") return [" ".charCodeAt(0)]; // Use
82
+ if (name === "return" || name === "enter") return [DoomKeys.KEY_ENTER];
83
+ if (name === "escape") return [DoomKeys.KEY_ESCAPE];
84
+ if (name === "tab") return [DoomKeys.KEY_TAB];
85
+ if (name === "backspace") return [DoomKeys.KEY_BACKSPACE];
86
+
87
+ // Fire (Ctrl) - but not Ctrl+C which should exit
88
+ if (key.ctrl && key.name !== "c") return [DoomKeys.KEY_FIRE];
89
+
90
+ // Alt for strafe
91
+ if (key.meta || key.name === "alt") return [DoomKeys.KEY_LALT];
92
+
93
+ // Shift for run
94
+ if (key.shift) return [DoomKeys.KEY_RSHIFT];
95
+
96
+ // Function keys
97
+ if (name === "f1") return [DoomKeys.KEY_F1];
98
+ if (name === "f2") return [DoomKeys.KEY_F2];
99
+ if (name === "f3") return [DoomKeys.KEY_F3];
100
+ if (name === "f4") return [DoomKeys.KEY_F4];
101
+ if (name === "f5") return [DoomKeys.KEY_F5];
102
+ if (name === "f6") return [DoomKeys.KEY_F6];
103
+ if (name === "f7") return [DoomKeys.KEY_F7];
104
+ if (name === "f8") return [DoomKeys.KEY_F8];
105
+ if (name === "f9") return [DoomKeys.KEY_F9];
106
+ if (name === "f10") return [DoomKeys.KEY_F10];
107
+ if (name === "f11") return [DoomKeys.KEY_F11];
108
+ if (name === "f12") return [DoomKeys.KEY_F12];
109
+
110
+ // Weapon selection (1-9, 0)
111
+ if (name >= "0" && name <= "9") return [name.charCodeAt(0)];
112
+
113
+ // Plus/minus for gamma/zoom
114
+ if (name === "+" || name === "=") return [DoomKeys.KEY_EQUALS];
115
+ if (name === "-") return [DoomKeys.KEY_MINUS];
116
+
117
+ // Y/N for prompts
118
+ if (name === "y") return ["y".charCodeAt(0)];
119
+ if (name === "n") return ["n".charCodeAt(0)];
120
+
121
+ // Other letter keys (for cheats, etc)
122
+ if (name.length === 1 && name >= "a" && name <= "z") {
123
+ return [name.charCodeAt(0)];
124
+ }
125
+
126
+ return [];
127
127
  }
128
128
 
129
129
  /**
@@ -133,89 +133,89 @@ function mapKeyToDoom(key: KeyEvent): number[] {
133
133
  const keyTimers = new Map<string, ReturnType<typeof setTimeout>>();
134
134
 
135
135
  export interface DoomInputOptions {
136
- engine: DoomEngine;
137
- onExit?: () => void;
136
+ engine: DoomEngine;
137
+ onExit?: () => void;
138
138
  }
139
139
 
140
140
  export function createDoomInputHandler(options: DoomInputOptions) {
141
- const { engine, onExit } = options;
142
-
143
- return (key: KeyEvent) => {
144
- // Handle Ctrl+C for exit
145
- if (key.ctrl && (key.name === "c" || key.sequence === "\x03")) {
146
- if (onExit) {
147
- onExit();
148
- }
149
- return;
150
- }
141
+ const { engine, onExit } = options;
142
+
143
+ return (key: KeyEvent) => {
144
+ // Handle Ctrl+C for exit
145
+ if (key.ctrl && (key.name === "c" || key.sequence === "\x03")) {
146
+ if (onExit) {
147
+ onExit();
148
+ }
149
+ return;
150
+ }
151
151
 
152
- const doomKeys = mapKeyToDoom(key);
152
+ const doomKeys = mapKeyToDoom(key);
153
153
 
154
- if (doomKeys.length === 0) return;
154
+ if (doomKeys.length === 0) return;
155
155
 
156
- const keyId = key.name || key.sequence || "";
157
- const wasPressed = keyStates.get(keyId) ?? false;
158
- const keyName = key.name?.toLowerCase() ?? "";
156
+ const keyId = key.name || key.sequence || "";
157
+ const wasPressed = keyStates.get(keyId) ?? false;
158
+ const keyName = key.name?.toLowerCase() ?? "";
159
159
 
160
- // Menu confirmation keys (y/n) should always send keydown on every press
161
- // These are used for quit dialogs and other prompts
162
- const isMenuConfirmKey = keyName === "y" || keyName === "n";
160
+ // Menu confirmation keys (y/n) should always send keydown on every press
161
+ // These are used for quit dialogs and other prompts
162
+ const isMenuConfirmKey = keyName === "y" || keyName === "n";
163
163
 
164
- // Clear any existing release timer for this key
165
- const existingTimer = keyTimers.get(keyId);
166
- if (existingTimer) {
167
- clearTimeout(existingTimer);
168
- keyTimers.delete(keyId);
169
- }
164
+ // Clear any existing release timer for this key
165
+ const existingTimer = keyTimers.get(keyId);
166
+ if (existingTimer) {
167
+ clearTimeout(existingTimer);
168
+ keyTimers.delete(keyId);
169
+ }
170
170
 
171
- // Key press - send if not already pressed, OR if it's a menu confirmation key
172
- if (!wasPressed || isMenuConfirmKey) {
173
- keyStates.set(keyId, true);
174
- // Send all mapped keys (for WASD this includes both movement and character)
175
- for (const doomKey of doomKeys) {
176
- engine.pushKey(true, doomKey);
177
- }
178
-
179
- // For menu confirmation keys, immediately send release too
180
- // since DOOM only cares about the keydown event
181
- if (isMenuConfirmKey) {
182
- setTimeout(() => {
183
- for (const doomKey of doomKeys) {
184
- engine.pushKey(false, doomKey);
185
- }
186
- keyStates.set(keyId, false);
187
- }, 50);
188
- return;
189
- }
190
- }
171
+ // Key press - send if not already pressed, OR if it's a menu confirmation key
172
+ if (!wasPressed || isMenuConfirmKey) {
173
+ keyStates.set(keyId, true);
174
+ // Send all mapped keys (for WASD this includes both movement and character)
175
+ for (const doomKey of doomKeys) {
176
+ engine.pushKey(true, doomKey);
177
+ }
178
+
179
+ // For menu confirmation keys, immediately send release too
180
+ // since DOOM only cares about the keydown event
181
+ if (isMenuConfirmKey) {
182
+ setTimeout(() => {
183
+ for (const doomKey of doomKeys) {
184
+ engine.pushKey(false, doomKey);
185
+ }
186
+ keyStates.set(keyId, false);
187
+ }, 50);
188
+ return;
189
+ }
190
+ }
191
191
 
192
- // Schedule key release after 300ms of no input (for non-menu keys)
193
- const timer = setTimeout(() => {
194
- if (keyStates.get(keyId)) {
195
- keyStates.set(keyId, false);
196
- for (const doomKey of doomKeys) {
197
- engine.pushKey(false, doomKey);
198
- }
199
- keyTimers.delete(keyId);
200
- }
201
- }, 300);
202
- keyTimers.set(keyId, timer);
203
- };
192
+ // Schedule key release after 300ms of no input (for non-menu keys)
193
+ const timer = setTimeout(() => {
194
+ if (keyStates.get(keyId)) {
195
+ keyStates.set(keyId, false);
196
+ for (const doomKey of doomKeys) {
197
+ engine.pushKey(false, doomKey);
198
+ }
199
+ keyTimers.delete(keyId);
200
+ }
201
+ }, 300);
202
+ keyTimers.set(keyId, timer);
203
+ };
204
204
  }
205
205
 
206
206
  /**
207
207
  * Get help text for controls
208
208
  */
209
209
  export function getControlsHelp(): string {
210
- return [
211
- "Controls:",
212
- " Movement: Arrow Keys or WASD",
213
- " Fire: Ctrl",
214
- " Use/Open: Space",
215
- " Run: Shift",
216
- " Strafe: A/D or Alt+Arrows",
217
- " Weapons: 1-7",
218
- " Menu: Escape",
219
- " Map: Tab",
220
- ].join("\n");
210
+ return [
211
+ "Controls:",
212
+ " Movement: Arrow Keys or WASD",
213
+ " Fire: Ctrl",
214
+ " Use/Open: Space",
215
+ " Run: Shift",
216
+ " Strafe: A/D or Alt+Arrows",
217
+ " Weapons: 1-7",
218
+ " Menu: Escape",
219
+ " Map: Tab",
220
+ ].join("\n");
221
221
  }
package/src/doom-mouse.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * DOOM Mouse Input Handler
3
- *
3
+ *
4
4
  * Provides mouse-based turning and firing for DOOM.
5
5
  * Mouse horizontal movement translates to left/right turning.
6
6
  * Left click fires the weapon.
@@ -11,119 +11,119 @@ import { DoomKeys } from "./doom-input";
11
11
  import { debugLog } from "./debug";
12
12
 
13
13
  export interface DoomMouseOptions {
14
- engine: DoomEngine;
15
- sensitivity?: number; // Cells of movement before triggering turn (default: 2)
14
+ engine: DoomEngine;
15
+ sensitivity?: number; // Cells of movement before triggering turn (default: 2)
16
16
  }
17
17
 
18
18
  export interface DoomMouseHandler {
19
- onMouseMove: (x: number, y: number) => void;
20
- onMouseDown: (button: number) => void;
21
- onMouseUp: (button: number) => void;
22
- reset: () => void;
19
+ onMouseMove: (x: number, y: number) => void;
20
+ onMouseDown: (button: number) => void;
21
+ onMouseUp: (button: number) => void;
22
+ reset: () => void;
23
23
  }
24
24
 
25
25
  /**
26
26
  * Create a mouse handler that forwards mouse events to DOOM
27
27
  */
28
28
  export function createDoomMouseHandler(options: DoomMouseOptions): DoomMouseHandler {
29
- const { engine } = options;
30
-
31
- let lastMouseX: number | null = null;
32
- let isLeftMouseDown = false;
33
- let currentTurnKey: number | null = null;
34
- let releaseTimer: ReturnType<typeof setTimeout> | null = null;
35
-
36
- const RELEASE_DELAY = 100; // Release key 100ms after last movement
37
-
38
- return {
39
- /**
40
- * Handle mouse movement - hold turn key while moving
41
- */
42
- onMouseMove(x: number, _y: number): void {
43
- if (lastMouseX === null) {
44
- lastMouseX = x;
45
- return;
46
- }
47
-
48
- const delta = x - lastMouseX;
49
- lastMouseX = x;
50
-
51
- if (delta === 0) return;
52
-
53
- const newTurnKey = delta > 0 ? DoomKeys.KEY_RIGHTARROW : DoomKeys.KEY_LEFTARROW;
54
-
55
- // Clear any pending release
56
- if (releaseTimer) {
57
- clearTimeout(releaseTimer);
58
- releaseTimer = null;
59
- }
60
-
61
- // If direction changed, release old and press new
62
- if (currentTurnKey !== newTurnKey) {
63
- if (currentTurnKey !== null) {
64
- engine.pushKey(false, currentTurnKey);
65
- }
66
- engine.pushKey(true, newTurnKey);
67
- currentTurnKey = newTurnKey;
68
- }
69
-
70
- // Schedule release after delay (will be cancelled if more movement comes)
71
- releaseTimer = setTimeout(() => {
72
- if (currentTurnKey !== null) {
73
- engine.pushKey(false, currentTurnKey);
74
- currentTurnKey = null;
75
- }
76
- releaseTimer = null;
77
- }, RELEASE_DELAY);
78
- },
79
-
80
- /**
81
- * Handle mouse button press
82
- */
83
- onMouseDown(button: number): void {
84
- // Left click (button 0) = fire
85
- if (button === 0 && !isLeftMouseDown) {
86
- isLeftMouseDown = true;
87
- engine.pushKey(true, DoomKeys.KEY_FIRE);
88
- debugLog("Mouse", "Fire pressed");
89
- }
90
- },
91
-
92
- /**
93
- * Handle mouse button release
94
- */
95
- onMouseUp(button: number): void {
96
- // Left click release = stop firing
97
- if (button === 0 && isLeftMouseDown) {
98
- isLeftMouseDown = false;
99
- engine.pushKey(false, DoomKeys.KEY_FIRE);
100
- debugLog("Mouse", "Fire released");
101
- }
102
- },
103
-
104
- /**
105
- * Reset mouse state (useful when window loses focus)
106
- */
107
- reset(): void {
108
- lastMouseX = null;
109
-
110
- // Clear pending release timer
111
- if (releaseTimer) {
112
- clearTimeout(releaseTimer);
113
- releaseTimer = null;
114
- }
115
-
116
- // Release any held turn key
117
- if (currentTurnKey !== null) {
118
- engine.pushKey(false, currentTurnKey);
119
- currentTurnKey = null;
120
- }
121
-
122
- // Release fire if held
123
- if (isLeftMouseDown) {
124
- isLeftMouseDown = false;
125
- engine.pushKey(false, DoomKeys.KEY_FIRE);
126
- }
29
+ const { engine } = options;
30
+
31
+ let lastMouseX: number | null = null;
32
+ let isLeftMouseDown = false;
33
+ let currentTurnKey: number | null = null;
34
+ let releaseTimer: ReturnType<typeof setTimeout> | null = null;
35
+
36
+ const RELEASE_DELAY = 100; // Release key 100ms after last movement
37
+
38
+ return {
39
+ /**
40
+ * Handle mouse movement - hold turn key while moving
41
+ */
42
+ onMouseMove(x: number, _y: number): void {
43
+ if (lastMouseX === null) {
44
+ lastMouseX = x;
45
+ return;
46
+ }
47
+
48
+ const delta = x - lastMouseX;
49
+ lastMouseX = x;
50
+
51
+ if (delta === 0) return;
52
+
53
+ const newTurnKey = delta > 0 ? DoomKeys.KEY_RIGHTARROW : DoomKeys.KEY_LEFTARROW;
54
+
55
+ // Clear any pending release
56
+ if (releaseTimer) {
57
+ clearTimeout(releaseTimer);
58
+ releaseTimer = null;
59
+ }
60
+
61
+ // If direction changed, release old and press new
62
+ if (currentTurnKey !== newTurnKey) {
63
+ if (currentTurnKey !== null) {
64
+ engine.pushKey(false, currentTurnKey);
127
65
  }
128
- };
66
+ engine.pushKey(true, newTurnKey);
67
+ currentTurnKey = newTurnKey;
68
+ }
69
+
70
+ // Schedule release after delay (will be cancelled if more movement comes)
71
+ releaseTimer = setTimeout(() => {
72
+ if (currentTurnKey !== null) {
73
+ engine.pushKey(false, currentTurnKey);
74
+ currentTurnKey = null;
75
+ }
76
+ releaseTimer = null;
77
+ }, RELEASE_DELAY);
78
+ },
79
+
80
+ /**
81
+ * Handle mouse button press
82
+ */
83
+ onMouseDown(button: number): void {
84
+ // Left click (button 0) = fire
85
+ if (button === 0 && !isLeftMouseDown) {
86
+ isLeftMouseDown = true;
87
+ engine.pushKey(true, DoomKeys.KEY_FIRE);
88
+ debugLog("Mouse", "Fire pressed");
89
+ }
90
+ },
91
+
92
+ /**
93
+ * Handle mouse button release
94
+ */
95
+ onMouseUp(button: number): void {
96
+ // Left click release = stop firing
97
+ if (button === 0 && isLeftMouseDown) {
98
+ isLeftMouseDown = false;
99
+ engine.pushKey(false, DoomKeys.KEY_FIRE);
100
+ debugLog("Mouse", "Fire released");
101
+ }
102
+ },
103
+
104
+ /**
105
+ * Reset mouse state (useful when window loses focus)
106
+ */
107
+ reset(): void {
108
+ lastMouseX = null;
109
+
110
+ // Clear pending release timer
111
+ if (releaseTimer) {
112
+ clearTimeout(releaseTimer);
113
+ releaseTimer = null;
114
+ }
115
+
116
+ // Release any held turn key
117
+ if (currentTurnKey !== null) {
118
+ engine.pushKey(false, currentTurnKey);
119
+ currentTurnKey = null;
120
+ }
121
+
122
+ // Release fire if held
123
+ if (isLeftMouseDown) {
124
+ isLeftMouseDown = false;
125
+ engine.pushKey(false, DoomKeys.KEY_FIRE);
126
+ }
127
+ },
128
+ };
129
129
  }