@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/README.md +37 -13
- package/package.json +12 -5
- package/src/debug.ts +3 -3
- package/src/doom-audio.ts +146 -146
- package/src/doom-engine.ts +299 -287
- package/src/doom-input.ts +178 -167
- package/src/doom-mouse.ts +129 -0
- package/src/doom-saves.ts +60 -60
- package/src/index.ts +58 -25
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
|
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
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
133
|
-
|
|
136
|
+
engine: DoomEngine;
|
|
137
|
+
onExit?: () => void;
|
|
134
138
|
}
|
|
135
139
|
|
|
136
140
|
export function createDoomInputHandler(options: DoomInputOptions) {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
152
|
+
const doomKeys = mapKeyToDoom(key);
|
|
149
153
|
|
|
150
|
-
|
|
154
|
+
if (doomKeys.length === 0) return;
|
|
151
155
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
156
|
+
const keyId = key.name || key.sequence || "";
|
|
157
|
+
const wasPressed = keyStates.get(keyId) ?? false;
|
|
158
|
+
const keyName = key.name?.toLowerCase() ?? "";
|
|
155
159
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
+
}
|