@muhammedaksam/opentui-doom 0.3.0 → 0.3.5

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 CHANGED
@@ -6,6 +6,7 @@
6
6
 
7
7
  - **Full DOOM gameplay** in your terminal
8
8
  - **High-resolution rendering** using half-block characters (▀) for 2x vertical resolution
9
+ - **Mouse aiming** - Turn and fire with your mouse (enabled by default)
9
10
  - **Keyboard input support** with WASD and arrow keys
10
11
  - **Save/Load game** support - saves persist to `~/.opentui-doom/`
11
12
  - **Sound effects and music** via mpv
@@ -68,20 +69,34 @@ Place the WAD file in the project root.
68
69
  bun run dev -- --wad ./doom1.wad
69
70
  ```
70
71
 
72
+ To disable mouse aiming:
73
+
74
+ ```bash
75
+ bun run dev -- --wad ./doom1.wad --mouse false
76
+ ```
77
+
78
+ ### Debug Mode
79
+
80
+ To run with debug logging enabled (outputs to `debug.log`):
81
+
82
+ ```bash
83
+ bun run dev:debug -- --wad ./doom1.wad
84
+ ```
85
+
71
86
  ## 🎮 Controls
72
87
 
73
- | Action | Keys |
74
- | ----------------- | -------------- |
75
- | Move Forward/Back | W / S or ↑ / ↓ |
76
- | Turn Left/Right | ← / → |
77
- | Strafe | A / D |
78
- | Fire | Ctrl |
79
- | Use/Open | Space |
80
- | Run | Shift |
81
- | Weapons | 1-7 |
82
- | Menu | Escape |
83
- | Map | Tab |
84
- | Quit | Ctrl+C |
88
+ | Action | Keys |
89
+ | ----------------- | ------------------ |
90
+ | Move Forward/Back | W / S or ↑ / ↓ |
91
+ | Turn Left/Right | Mouse or ← / → |
92
+ | Strafe | A / D |
93
+ | Fire | Left Click or Ctrl |
94
+ | Use/Open | Space |
95
+ | Run | Shift |
96
+ | Weapons | 1-7 |
97
+ | Menu | Escape |
98
+ | Map | Tab |
99
+ | Quit | Ctrl+C |
85
100
 
86
101
  ## 💾 Save Games
87
102
 
@@ -148,7 +163,8 @@ opentui-doom/
148
163
  ├── src/
149
164
  │ ├── index.ts # Main entry point
150
165
  │ ├── doom-engine.ts # WASM module wrapper
151
- └── doom-input.ts # Keyboard input mapping
166
+ ├── doom-input.ts # Keyboard input mapping
167
+ │ └── doom-mouse.ts # Mouse input handling
152
168
  ├── doom/
153
169
  │ ├── doomgeneric_opentui.c # Platform implementation
154
170
  │ ├── doomgeneric/ # doomgeneric source (cloned during build)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@muhammedaksam/opentui-doom",
3
- "version": "0.3.0",
3
+ "version": "0.3.5",
4
4
  "description": "Play DOOM in your terminal using OpenTUI's framebuffer rendering and doomgeneric WASM",
5
5
  "module": "src/index.ts",
6
6
  "type": "module",
@@ -58,7 +58,7 @@
58
58
  "typescript": "^5"
59
59
  },
60
60
  "dependencies": {
61
- "@opentui/core": "^0.1.57"
61
+ "@opentui/core": "^0.1.59"
62
62
  },
63
63
  "engines": {
64
64
  "node": ">=18",
@@ -135,6 +135,23 @@ export class DoomEngine {
135
135
  // Create .savegame directory for saves (DOOM looks here by default)
136
136
  module.FS_createPath("/", ".savegame", true, true);
137
137
 
138
+ // Create default.cfg with WASD key bindings
139
+ // This is needed because we send character codes for WASD (to allow typing in save dialogs)
140
+ // Key codes: w=119, a=97, s=115, d=100
141
+ const defaultConfig = [
142
+ "key_up 119", // 'w' for forward
143
+ "key_down 115", // 's' for backward
144
+ "key_strafeleft 97", // 'a' for strafe left
145
+ "key_straferight 100", // 'd' for strafe right
146
+ ].join("\n") + "\n";
147
+ const configArray = Array.from(new TextEncoder().encode(defaultConfig));
148
+ try {
149
+ module.FS_createDataFile("/", "default.cfg", configArray, true, false);
150
+ debugLog("Engine", "Created default.cfg with WASD key bindings");
151
+ } catch (e) {
152
+ debugLog("Engine", `Failed to create default.cfg: ${e}`);
153
+ }
154
+
138
155
  // Load existing saves from ~/.opentui-doom/ into virtual filesystem
139
156
  const existingSaves = loadExistingSaves();
140
157
  for (const [slot, data] of existingSaves) {
package/src/doom-input.ts CHANGED
@@ -56,70 +56,74 @@ export const DoomKeys = {
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 {
64
+ function mapKeyToDoom(key: KeyEvent): number[] {
62
65
  const name = key.name?.toLowerCase() ?? "";
63
66
 
64
67
  // 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;
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)];
75
79
 
76
80
  // 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;
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];
82
86
 
83
87
  // Fire (Ctrl) - but not Ctrl+C which should exit
84
- if (key.ctrl && key.name !== "c") return DoomKeys.KEY_FIRE;
88
+ if (key.ctrl && key.name !== "c") return [DoomKeys.KEY_FIRE];
85
89
 
86
90
  // Alt for strafe
87
- if (key.meta || key.name === "alt") return DoomKeys.KEY_LALT;
91
+ if (key.meta || key.name === "alt") return [DoomKeys.KEY_LALT];
88
92
 
89
93
  // Shift for run
90
- if (key.shift) return DoomKeys.KEY_RSHIFT;
94
+ if (key.shift) return [DoomKeys.KEY_RSHIFT];
91
95
 
92
96
  // 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;
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];
105
109
 
106
110
  // Weapon selection (1-9, 0)
107
- if (name >= "0" && name <= "9") return name.charCodeAt(0);
111
+ if (name >= "0" && name <= "9") return [name.charCodeAt(0)];
108
112
 
109
113
  // Plus/minus for gamma/zoom
110
- if (name === "+" || name === "=") return DoomKeys.KEY_EQUALS;
111
- if (name === "-") return DoomKeys.KEY_MINUS;
114
+ if (name === "+" || name === "=") return [DoomKeys.KEY_EQUALS];
115
+ if (name === "-") return [DoomKeys.KEY_MINUS];
112
116
 
113
117
  // Y/N for prompts
114
- if (name === "y") return "y".charCodeAt(0);
115
- if (name === "n") return "n".charCodeAt(0);
118
+ if (name === "y") return ["y".charCodeAt(0)];
119
+ if (name === "n") return ["n".charCodeAt(0)];
116
120
 
117
121
  // Other letter keys (for cheats, etc)
118
122
  if (name.length === 1 && name >= "a" && name <= "z") {
119
- return name.charCodeAt(0);
123
+ return [name.charCodeAt(0)];
120
124
  }
121
125
 
122
- return null;
126
+ return [];
123
127
  }
124
128
 
125
129
  /**
@@ -145,9 +149,9 @@ export function createDoomInputHandler(options: DoomInputOptions) {
145
149
  return;
146
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
156
  const keyId = key.name || key.sequence || "";
153
157
  const wasPressed = keyStates.get(keyId) ?? false;
@@ -167,13 +171,18 @@ export function createDoomInputHandler(options: DoomInputOptions) {
167
171
  // Key press - send if not already pressed, OR if it's a menu confirmation key
168
172
  if (!wasPressed || isMenuConfirmKey) {
169
173
  keyStates.set(keyId, true);
170
- engine.pushKey(true, doomKey);
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
+ }
171
178
 
172
179
  // For menu confirmation keys, immediately send release too
173
180
  // since DOOM only cares about the keydown event
174
181
  if (isMenuConfirmKey) {
175
182
  setTimeout(() => {
176
- engine.pushKey(false, doomKey);
183
+ for (const doomKey of doomKeys) {
184
+ engine.pushKey(false, doomKey);
185
+ }
177
186
  keyStates.set(keyId, false);
178
187
  }, 50);
179
188
  return;
@@ -184,7 +193,9 @@ export function createDoomInputHandler(options: DoomInputOptions) {
184
193
  const timer = setTimeout(() => {
185
194
  if (keyStates.get(keyId)) {
186
195
  keyStates.set(keyId, false);
187
- engine.pushKey(false, doomKey);
196
+ for (const doomKey of doomKeys) {
197
+ engine.pushKey(false, doomKey);
198
+ }
188
199
  keyTimers.delete(keyId);
189
200
  }
190
201
  }, 300);
@@ -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
+ }
package/src/index.ts CHANGED
@@ -17,6 +17,7 @@ import {
17
17
  } from "@opentui/core";
18
18
  import { DoomEngine, DOOM_WIDTH, DOOM_HEIGHT } from "./doom-engine";
19
19
  import { createDoomInputHandler, getControlsHelp } from "./doom-input";
20
+ import { createDoomMouseHandler, type DoomMouseHandler } from "./doom-mouse";
20
21
  import { shutdownAudio } from "./doom-audio";
21
22
  import { debugLog } from "./debug";
22
23
  import { parseArgs } from "util";
@@ -35,6 +36,11 @@ const { values } = parseArgs({
35
36
  short: "h",
36
37
  default: false,
37
38
  },
39
+ mouse: {
40
+ type: "boolean",
41
+ short: "m",
42
+ default: true,
43
+ },
38
44
  },
39
45
  });
40
46
 
@@ -48,7 +54,7 @@ Options:
48
54
  -w, --wad Path to DOOM WAD file (default: doom1.wad)
49
55
  -h, --help Show this help message
50
56
 
51
- ${getControlsHelp()}
57
+ ${getControlsHelp()}${values.mouse ? '\n Mouse=Aim/Fire' : ''}
52
58
  `);
53
59
  process.exit(0);
54
60
  }
@@ -130,6 +136,7 @@ let framebufferRenderable: FrameBufferRenderable | null = null;
130
136
  let isExiting = false; // Flag to stop the game loop when exiting
131
137
  let lastSaveSyncTime = 0; // Track when we last synced saves
132
138
  const SAVE_SYNC_INTERVAL = 5000; // Sync saves every 5 seconds
139
+ let mouseHandler: DoomMouseHandler | null = null;
133
140
 
134
141
  async function initDoom() {
135
142
  try {
@@ -157,9 +164,12 @@ async function initDoom() {
157
164
  renderer.root.add(framebufferRenderable);
158
165
 
159
166
  // Add controls overlay
167
+ const controlsContent = values.mouse
168
+ ? "DOOM | Ctrl+C to exit | WASD=Move Mouse=Aim Click=Fire"
169
+ : "DOOM | Ctrl+C to exit | Arrow/WASD=Move Space=Use Ctrl=Fire";
160
170
  const controlsText = new TextRenderable(renderer, {
161
171
  id: "controls",
162
- content: "DOOM | Ctrl+C to exit | Arrow/WASD=Move Space=Use Ctrl=Fire",
172
+ content: controlsContent,
163
173
  position: "absolute",
164
174
  left: 1,
165
175
  top: 0,
@@ -176,6 +186,30 @@ async function initDoom() {
176
186
  });
177
187
  renderer.keyInput.on("keypress", inputHandler);
178
188
 
189
+ // Set up mouse handler if enabled
190
+ if (values.mouse) {
191
+ mouseHandler = createDoomMouseHandler({
192
+ engine: doomEngine,
193
+ sensitivity: 2, // Adjust for terminal cell size
194
+ });
195
+
196
+ // Attach mouse events to framebuffer
197
+ framebufferRenderable.onMouseMove = (event) => {
198
+ mouseHandler?.onMouseMove(event.x, event.y);
199
+ };
200
+ framebufferRenderable.onMouseDrag = (event) => {
201
+ mouseHandler?.onMouseMove(event.x, event.y);
202
+ };
203
+ framebufferRenderable.onMouseDown = (event) => {
204
+ mouseHandler?.onMouseDown(event.button);
205
+ };
206
+ framebufferRenderable.onMouseUp = (event) => {
207
+ mouseHandler?.onMouseUp(event.button);
208
+ };
209
+
210
+ debugLog("Input", "Mouse look enabled");
211
+ }
212
+
179
213
  // Start game loop
180
214
  renderer.setFrameCallback(gameLoop);
181
215