@muhammedaksam/opentui-doom 0.3.0 → 0.3.6

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,117 +9,121 @@ 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
56
56
  const keyStates = new Map<string, boolean>();
57
57
 
58
58
  /**
59
- * Map an OpenTUI key event to a DOOM key code
59
+ * Map an OpenTUI key event to DOOM key code(s)
60
+ * Returns an array of key codes - for most keys this is a single code,
61
+ * but for WASD we return both the movement key AND the character
62
+ * so both gameplay movement and text input work.
60
63
  */
61
- function mapKeyToDoom(key: KeyEvent): number | null {
62
- const name = key.name?.toLowerCase() ?? "";
63
-
64
- // Arrow keys
65
- if (name === "up" || key.sequence === "\x1b[A") return DoomKeys.KEY_UPARROW;
66
- if (name === "down" || key.sequence === "\x1b[B") return DoomKeys.KEY_DOWNARROW;
67
- if (name === "left" || key.sequence === "\x1b[D") return DoomKeys.KEY_LEFTARROW;
68
- if (name === "right" || key.sequence === "\x1b[C") return DoomKeys.KEY_RIGHTARROW;
69
-
70
- // WASD movement (alternative to arrows)
71
- if (name === "w") return DoomKeys.KEY_UPARROW;
72
- if (name === "s") return DoomKeys.KEY_DOWNARROW;
73
- if (name === "a") return DoomKeys.KEY_STRAFE_L;
74
- if (name === "d") return DoomKeys.KEY_STRAFE_R;
75
-
76
- // Action keys
77
- if (name === "space") return " ".charCodeAt(0); // Use
78
- if (name === "return" || name === "enter") return DoomKeys.KEY_ENTER;
79
- if (name === "escape") return DoomKeys.KEY_ESCAPE;
80
- if (name === "tab") return DoomKeys.KEY_TAB;
81
- if (name === "backspace") return DoomKeys.KEY_BACKSPACE;
82
-
83
- // Fire (Ctrl) - but not Ctrl+C which should exit
84
- if (key.ctrl && key.name !== "c") return DoomKeys.KEY_FIRE;
85
-
86
- // Alt for strafe
87
- if (key.meta || key.name === "alt") return DoomKeys.KEY_LALT;
88
-
89
- // Shift for run
90
- if (key.shift) return DoomKeys.KEY_RSHIFT;
91
-
92
- // Function keys
93
- if (name === "f1") return DoomKeys.KEY_F1;
94
- if (name === "f2") return DoomKeys.KEY_F2;
95
- if (name === "f3") return DoomKeys.KEY_F3;
96
- if (name === "f4") return DoomKeys.KEY_F4;
97
- if (name === "f5") return DoomKeys.KEY_F5;
98
- if (name === "f6") return DoomKeys.KEY_F6;
99
- if (name === "f7") return DoomKeys.KEY_F7;
100
- if (name === "f8") return DoomKeys.KEY_F8;
101
- if (name === "f9") return DoomKeys.KEY_F9;
102
- if (name === "f10") return DoomKeys.KEY_F10;
103
- if (name === "f11") return DoomKeys.KEY_F11;
104
- if (name === "f12") return DoomKeys.KEY_F12;
105
-
106
- // Weapon selection (1-9, 0)
107
- if (name >= "0" && name <= "9") return name.charCodeAt(0);
108
-
109
- // Plus/minus for gamma/zoom
110
- if (name === "+" || name === "=") return DoomKeys.KEY_EQUALS;
111
- if (name === "-") return DoomKeys.KEY_MINUS;
112
-
113
- // Y/N for prompts
114
- if (name === "y") return "y".charCodeAt(0);
115
- if (name === "n") return "n".charCodeAt(0);
116
-
117
- // Other letter keys (for cheats, etc)
118
- if (name.length === 1 && name >= "a" && name <= "z") {
119
- return name.charCodeAt(0);
120
- }
121
-
122
- return null;
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 [];
123
127
  }
124
128
 
125
129
  /**
@@ -129,82 +133,89 @@ function mapKeyToDoom(key: KeyEvent): number | null {
129
133
  const keyTimers = new Map<string, ReturnType<typeof setTimeout>>();
130
134
 
131
135
  export interface DoomInputOptions {
132
- engine: DoomEngine;
133
- onExit?: () => void;
136
+ engine: DoomEngine;
137
+ onExit?: () => void;
134
138
  }
135
139
 
136
140
  export function createDoomInputHandler(options: DoomInputOptions) {
137
- const { engine, onExit } = options;
138
-
139
- return (key: KeyEvent) => {
140
- // Handle Ctrl+C for exit
141
- if (key.ctrl && (key.name === "c" || key.sequence === "\x03")) {
142
- if (onExit) {
143
- onExit();
144
- }
145
- return;
146
- }
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
+ }
147
151
 
148
- const doomKey = mapKeyToDoom(key);
152
+ const doomKeys = mapKeyToDoom(key);
149
153
 
150
- if (doomKey === null) return;
154
+ if (doomKeys.length === 0) return;
151
155
 
152
- const keyId = key.name || key.sequence || "";
153
- const wasPressed = keyStates.get(keyId) ?? false;
154
- 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() ?? "";
155
159
 
156
- // Menu confirmation keys (y/n) should always send keydown on every press
157
- // These are used for quit dialogs and other prompts
158
- 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";
159
163
 
160
- // Clear any existing release timer for this key
161
- const existingTimer = keyTimers.get(keyId);
162
- if (existingTimer) {
163
- clearTimeout(existingTimer);
164
- keyTimers.delete(keyId);
165
- }
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
+ }
166
170
 
167
- // Key press - send if not already pressed, OR if it's a menu confirmation key
168
- if (!wasPressed || isMenuConfirmKey) {
169
- keyStates.set(keyId, true);
170
- engine.pushKey(true, doomKey);
171
-
172
- // For menu confirmation keys, immediately send release too
173
- // since DOOM only cares about the keydown event
174
- if (isMenuConfirmKey) {
175
- setTimeout(() => {
176
- engine.pushKey(false, doomKey);
177
- keyStates.set(keyId, false);
178
- }, 50);
179
- return;
180
- }
181
- }
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
+ }
182
191
 
183
- // Schedule key release after 300ms of no input (for non-menu keys)
184
- const timer = setTimeout(() => {
185
- if (keyStates.get(keyId)) {
186
- keyStates.set(keyId, false);
187
- engine.pushKey(false, doomKey);
188
- keyTimers.delete(keyId);
189
- }
190
- }, 300);
191
- keyTimers.set(keyId, timer);
192
- };
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
+ };
193
204
  }
194
205
 
195
206
  /**
196
207
  * Get help text for controls
197
208
  */
198
209
  export function getControlsHelp(): string {
199
- return [
200
- "Controls:",
201
- " Movement: Arrow Keys or WASD",
202
- " Fire: Ctrl",
203
- " Use/Open: Space",
204
- " Run: Shift",
205
- " Strafe: A/D or Alt+Arrows",
206
- " Weapons: 1-7",
207
- " Menu: Escape",
208
- " Map: Tab",
209
- ].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");
210
221
  }
@@ -0,0 +1,129 @@
1
+ /**
2
+ * DOOM Mouse Input Handler
3
+ *
4
+ * Provides mouse-based turning and firing for DOOM.
5
+ * Mouse horizontal movement translates to left/right turning.
6
+ * Left click fires the weapon.
7
+ */
8
+
9
+ import type { DoomEngine } from "./doom-engine";
10
+ import { DoomKeys } from "./doom-input";
11
+ import { debugLog } from "./debug";
12
+
13
+ export interface DoomMouseOptions {
14
+ engine: DoomEngine;
15
+ sensitivity?: number; // Cells of movement before triggering turn (default: 2)
16
+ }
17
+
18
+ export interface DoomMouseHandler {
19
+ onMouseMove: (x: number, y: number) => void;
20
+ onMouseDown: (button: number) => void;
21
+ onMouseUp: (button: number) => void;
22
+ reset: () => void;
23
+ }
24
+
25
+ /**
26
+ * Create a mouse handler that forwards mouse events to DOOM
27
+ */
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
+ }
127
+ },
128
+ };
129
+ }