@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 CHANGED
@@ -1,3 +1,11 @@
1
+ [![npm version](https://img.shields.io/npm/v/@muhammedaksam/opentui-doom.svg)](https://www.npmjs.com/package/@muhammedaksam/opentui-doom)
2
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
3
+ [![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue.svg)](https://www.typescriptlang.org/)
4
+ [![Bun](https://img.shields.io/badge/Bun-000?logo=bun&logoColor=fff)](https://bun.sh/)
5
+ [![Alacritty](https://img.shields.io/badge/Alacritty-F46D01?logo=alacritty&logoColor=fff)](https://alacritty.org/)
6
+ [![CI](https://github.com/muhammedaksam/opentui-doom/workflows/CI/badge.svg)](https://github.com/muhammedaksam/opentui-doom/actions)
7
+ [![Mentioned in Awesome OpenTUI](https://awesome.re/mentioned-badge.svg)](https://github.com/msmps/awesome-opentui)
8
+
1
9
  # DOOM for OpenTUI
2
10
 
3
11
  🎮 Play DOOM in your terminal using [OpenTUI](https://github.com/sst/opentui)'s framebuffer rendering!
@@ -6,6 +14,7 @@
6
14
 
7
15
  - **Full DOOM gameplay** in your terminal
8
16
  - **High-resolution rendering** using half-block characters (▀) for 2x vertical resolution
17
+ - **Mouse aiming** - Turn and fire with your mouse (enabled by default)
9
18
  - **Keyboard input support** with WASD and arrow keys
10
19
  - **Save/Load game** support - saves persist to `~/.opentui-doom/`
11
20
  - **Sound effects and music** via mpv
@@ -68,20 +77,34 @@ Place the WAD file in the project root.
68
77
  bun run dev -- --wad ./doom1.wad
69
78
  ```
70
79
 
80
+ To disable mouse aiming:
81
+
82
+ ```bash
83
+ bun run dev -- --wad ./doom1.wad --mouse false
84
+ ```
85
+
86
+ ### Debug Mode
87
+
88
+ To run with debug logging enabled (outputs to `debug.log`):
89
+
90
+ ```bash
91
+ bun run dev:debug -- --wad ./doom1.wad
92
+ ```
93
+
71
94
  ## 🎮 Controls
72
95
 
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 |
96
+ | Action | Keys |
97
+ | ----------------- | ------------------ |
98
+ | Move Forward/Back | W / S or ↑ / ↓ |
99
+ | Turn Left/Right | Mouse or ← / → |
100
+ | Strafe | A / D |
101
+ | Fire | Left Click or Ctrl |
102
+ | Use/Open | Space |
103
+ | Run | Shift |
104
+ | Weapons | 1-7 |
105
+ | Menu | Escape |
106
+ | Map | Tab |
107
+ | Quit | Ctrl+C |
85
108
 
86
109
  ## 💾 Save Games
87
110
 
@@ -148,7 +171,8 @@ opentui-doom/
148
171
  ├── src/
149
172
  │ ├── index.ts # Main entry point
150
173
  │ ├── doom-engine.ts # WASM module wrapper
151
- └── doom-input.ts # Keyboard input mapping
174
+ ├── doom-input.ts # Keyboard input mapping
175
+ │ └── doom-mouse.ts # Mouse input handling
152
176
  ├── doom/
153
177
  │ ├── doomgeneric_opentui.c # Platform implementation
154
178
  │ ├── 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.6",
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",
@@ -48,20 +48,27 @@
48
48
  "build:doom": "bash ./scripts/build-doom.sh",
49
49
  "build": "bun build src/index.ts --outdir dist --target node",
50
50
  "typecheck": "bun x tsc --noEmit",
51
+ "lint": "eslint src/",
52
+ "lint:fix": "eslint src/ --fix",
53
+ "format": "prettier --write src/",
54
+ "format:check": "prettier --check src/",
51
55
  "start": "bun run src/index.ts",
52
56
  "prepublishOnly": "bun run build:doom"
53
57
  },
54
58
  "devDependencies": {
55
- "@types/bun": "latest"
59
+ "@eslint/js": "^9.17.0",
60
+ "@types/bun": "latest",
61
+ "eslint": "^9.17.0",
62
+ "prettier": "^3.4.2",
63
+ "typescript-eslint": "^8.18.1"
56
64
  },
57
65
  "peerDependencies": {
58
66
  "typescript": "^5"
59
67
  },
60
68
  "dependencies": {
61
- "@opentui/core": "^0.1.57"
69
+ "@opentui/core": "^0.1.59"
62
70
  },
63
71
  "engines": {
64
- "node": ">=18",
65
72
  "bun": ">=1.0"
66
73
  }
67
- }
74
+ }
package/src/debug.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Debug logging utility for OpenTUI-DOOM
3
- *
3
+ *
4
4
  * Only logs when DOOM_DEBUG environment variable is set.
5
5
  * Usage: DOOM_DEBUG=1 bun run dev
6
6
  */
@@ -16,12 +16,12 @@ const logFile = join(import.meta.dir, "..", "debug.log");
16
16
  */
17
17
  export function debugLog(category: string, message: string): void {
18
18
  if (!DEBUG_ENABLED) return;
19
-
19
+
20
20
  const timestamp = new Date().toISOString();
21
21
  const line = `[${timestamp}] [${category}] ${message}\n`;
22
22
  try {
23
23
  appendFileSync(logFile, line);
24
- } catch (e) {
24
+ } catch (_e) {
25
25
  // Ignore logging errors
26
26
  }
27
27
  }
package/src/doom-audio.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * DOOM Audio Bridge for OpenTUI
3
- *
3
+ *
4
4
  * Handles audio playback using mpv with proper process management.
5
5
  * All spawned processes are tracked and terminated on shutdown.
6
6
  */
@@ -8,12 +8,12 @@
8
8
  import { spawn, ChildProcess } from "child_process";
9
9
  import { join } from "path";
10
10
  import { existsSync, unlinkSync } from "fs";
11
- import { createConnection, Socket } from "net";
11
+ import { createConnection } from "net";
12
12
  import { debugLog } from "./debug";
13
13
 
14
14
  // Local helper to log with Audio category
15
15
  function log(message: string): void {
16
- debugLog('Audio', message);
16
+ debugLog("Audio", message);
17
17
  }
18
18
 
19
19
  // Track all spawned mpv processes for cleanup
@@ -36,88 +36,88 @@ let initialized = false;
36
36
 
37
37
  // Current music state for volume changes
38
38
  let currentMusicName: string | null = null;
39
- let currentMusicLooping: boolean = false;
39
+ let _currentMusicLooping: boolean = false;
40
40
 
41
41
  /**
42
42
  * Initialize the audio system
43
43
  */
44
44
  export function initAudio(): void {
45
- if (initialized) return;
46
- initialized = true;
47
- log(`Initialized, sound dir: ${soundDir}`);
45
+ if (initialized) return;
46
+ initialized = true;
47
+ log(`Initialized, sound dir: ${soundDir}`);
48
48
  }
49
49
 
50
50
  /**
51
51
  * Shutdown the audio system and kill ALL spawned processes
52
52
  */
53
53
  export function shutdownAudio(): void {
54
- if (!initialized) return;
55
-
56
- // Kill music process
57
- if (musicProcess) {
58
- try {
59
- musicProcess.kill("SIGKILL");
60
- } catch (e) {
61
- // Process may have already exited
62
- }
63
- musicProcess = null;
54
+ if (!initialized) return;
55
+
56
+ // Kill music process
57
+ if (musicProcess) {
58
+ try {
59
+ musicProcess.kill("SIGKILL");
60
+ } catch (_e) {
61
+ // Process may have already exited
64
62
  }
63
+ musicProcess = null;
64
+ }
65
65
 
66
- // Kill ALL tracked processes
67
- for (const proc of activeProcesses) {
68
- try {
69
- proc.kill("SIGKILL");
70
- } catch (e) {
71
- // Process may have already exited
72
- }
66
+ // Kill ALL tracked processes
67
+ for (const proc of activeProcesses) {
68
+ try {
69
+ proc.kill("SIGKILL");
70
+ } catch (_e) {
71
+ // Process may have already exited
73
72
  }
74
- activeProcesses.clear();
73
+ }
74
+ activeProcesses.clear();
75
75
 
76
- initialized = false;
77
- log("Shutdown complete");
76
+ initialized = false;
77
+ log("Shutdown complete");
78
78
  }
79
79
 
80
80
  /**
81
81
  * Helper to spawn mpv with common options
82
82
  */
83
83
  function spawnMpv(filePath: string, options: string[] = []): ChildProcess | null {
84
- if (!existsSync(filePath)) {
85
- log(`File not found: ${filePath}`);
86
- return null;
87
- }
88
-
89
- const args = [
90
- "--no-video", // No video output
91
- "--no-terminal", // No terminal output
92
- "--really-quiet", // Suppress all output
93
- ...options,
94
- filePath
95
- ];
96
-
97
- try {
98
- const proc = spawn("mpv", args, {
99
- stdio: "ignore",
100
- detached: false, // Keep attached to parent process
101
- });
102
-
103
- // Track the process
104
- activeProcesses.add(proc);
105
-
106
- // Remove from tracking when process exits
107
- proc.on("exit", () => {
108
- activeProcesses.delete(proc);
109
- });
110
-
111
- proc.on("error", (err) => {
112
- log(`mpv error: ${err.message}`);
113
- activeProcesses.delete(proc);
114
- });
115
-
116
- return proc;
117
- } catch (e) {
118
- log(`Failed to spawn mpv: ${e}`);
119
- return null;
120
- }
84
+ if (!existsSync(filePath)) {
85
+ log(`File not found: ${filePath}`);
86
+ return null;
87
+ }
88
+
89
+ const args = [
90
+ "--no-video", // No video output
91
+ "--no-terminal", // No terminal output
92
+ "--really-quiet", // Suppress all output
93
+ ...options,
94
+ filePath,
95
+ ];
96
+
97
+ try {
98
+ const proc = spawn("mpv", args, {
99
+ stdio: "ignore",
100
+ detached: false, // Keep attached to parent process
101
+ });
102
+
103
+ // Track the process
104
+ activeProcesses.add(proc);
105
+
106
+ // Remove from tracking when process exits
107
+ proc.on("exit", () => {
108
+ activeProcesses.delete(proc);
109
+ });
110
+
111
+ proc.on("error", (err) => {
112
+ log(`mpv error: ${err.message}`);
113
+ activeProcesses.delete(proc);
114
+ });
115
+
116
+ return proc;
117
+ } catch (e) {
118
+ log(`Failed to spawn mpv: ${e}`);
119
+ return null;
120
+ }
121
121
  }
122
122
 
123
123
  /**
@@ -126,20 +126,20 @@ function spawnMpv(filePath: string, options: string[] = []): ChildProcess | null
126
126
  * Volume is 0-127 (DOOM standard)
127
127
  */
128
128
  export function playSound(name: string, volume: number = 127): void {
129
- if (!initialized) {
130
- log("playSound called but not initialized");
131
- return;
132
- }
129
+ if (!initialized) {
130
+ log("playSound called but not initialized");
131
+ return;
132
+ }
133
133
 
134
- const soundPath = join(soundDir, `ds${name.toLowerCase()}.wav`);
134
+ const soundPath = join(soundDir, `ds${name.toLowerCase()}.wav`);
135
135
 
136
- // Convert DOOM volume (0-127) to mpv volume (0-100)
137
- const mpvVolume = Math.round((volume / 127) * 100);
138
- log(`Playing sound: ${soundPath} at volume ${mpvVolume}`);
136
+ // Convert DOOM volume (0-127) to mpv volume (0-100)
137
+ const mpvVolume = Math.round((volume / 127) * 100);
138
+ log(`Playing sound: ${soundPath} at volume ${mpvVolume}`);
139
139
 
140
- // Fire and forget - process will auto-cleanup when done
141
- const proc = spawnMpv(soundPath, [`--volume=${mpvVolume}`]);
142
- log(`Spawn result: ${proc ? "success" : "failed"}`);
140
+ // Fire and forget - process will auto-cleanup when done
141
+ const proc = spawnMpv(soundPath, [`--volume=${mpvVolume}`]);
142
+ log(`Spawn result: ${proc ? "success" : "failed"}`);
143
143
  }
144
144
 
145
145
  /**
@@ -147,57 +147,57 @@ export function playSound(name: string, volume: number = 127): void {
147
147
  * Music files should be in sound/{name}.mp3
148
148
  */
149
149
  export function playMusic(name: string, looping: boolean): void {
150
- if (!initialized) {
151
- log("playMusic called but not initialized");
152
- return;
153
- }
154
-
155
- // Stop any currently playing music
156
- stopMusic();
157
-
158
- // Clean up any stale socket file
159
- try {
160
- if (existsSync(musicSocketPath)) {
161
- unlinkSync(musicSocketPath);
162
- }
163
- } catch (e) {
164
- // Ignore
165
- }
166
-
167
- // Store music state for volume changes
168
- currentMusicName = name;
169
- currentMusicLooping = looping;
170
-
171
- const musicPath = join(soundDir, `${name.toLowerCase()}.mp3`);
172
- log(`Playing music: ${musicPath}, looping: ${looping}`);
173
- const options: string[] = [
174
- `--input-ipc-server=${musicSocketPath}`, // Enable IPC for volume control
175
- ];
176
- if (looping) {
177
- options.push("--loop=inf");
150
+ if (!initialized) {
151
+ log("playMusic called but not initialized");
152
+ return;
153
+ }
154
+
155
+ // Stop any currently playing music
156
+ stopMusic();
157
+
158
+ // Clean up any stale socket file
159
+ try {
160
+ if (existsSync(musicSocketPath)) {
161
+ unlinkSync(musicSocketPath);
178
162
  }
179
-
180
- // Set volume (mpv uses 0-100 scale, DOOM uses 0-127)
181
- const mpvVolume = Math.round((currentVolume / 127) * 100);
182
- options.push(`--volume=${mpvVolume}`);
183
-
184
- musicProcess = spawnMpv(musicPath, options);
163
+ } catch (_e) {
164
+ // Ignore
165
+ }
166
+
167
+ // Store music state for volume changes
168
+ currentMusicName = name;
169
+ _currentMusicLooping = looping;
170
+
171
+ const musicPath = join(soundDir, `${name.toLowerCase()}.mp3`);
172
+ log(`Playing music: ${musicPath}, looping: ${looping}`);
173
+ const options: string[] = [
174
+ `--input-ipc-server=${musicSocketPath}`, // Enable IPC for volume control
175
+ ];
176
+ if (looping) {
177
+ options.push("--loop=inf");
178
+ }
179
+
180
+ // Set volume (mpv uses 0-100 scale, DOOM uses 0-127)
181
+ const mpvVolume = Math.round((currentVolume / 127) * 100);
182
+ options.push(`--volume=${mpvVolume}`);
183
+
184
+ musicProcess = spawnMpv(musicPath, options);
185
185
  }
186
186
 
187
187
  /**
188
188
  * Stop the currently playing music
189
189
  */
190
190
  export function stopMusic(): void {
191
- if (musicProcess) {
192
- try {
193
- musicProcess.kill("SIGTERM");
194
- } catch (e) {
195
- // Process may have already exited
196
- }
197
- activeProcesses.delete(musicProcess);
198
- musicProcess = null;
191
+ if (musicProcess) {
192
+ try {
193
+ musicProcess.kill("SIGTERM");
194
+ } catch (_e) {
195
+ // Process may have already exited
199
196
  }
200
- currentMusicName = null;
197
+ activeProcesses.delete(musicProcess);
198
+ musicProcess = null;
199
+ }
200
+ currentMusicName = null;
201
201
  }
202
202
 
203
203
  /**
@@ -205,32 +205,32 @@ export function stopMusic(): void {
205
205
  * Uses IPC socket to change volume without restarting music
206
206
  */
207
207
  export function setMusicVolume(volume: number): void {
208
- const newVolume = Math.max(0, Math.min(127, volume));
209
- currentVolume = newVolume;
210
-
211
- // If no music is playing, just save the volume for next play
212
- if (!musicProcess || !currentMusicName) {
213
- return;
214
- }
215
-
216
- // Convert to mpv volume (0-100)
217
- const mpvVolume = Math.round((newVolume / 127) * 100);
218
- log(`Setting music volume to ${mpvVolume} via IPC`);
219
-
220
- // Send volume command via IPC socket
221
- try {
222
- const socket = createConnection(musicSocketPath);
223
-
224
- socket.on("connect", () => {
225
- const cmd = JSON.stringify({ command: ["set_property", "volume", mpvVolume] }) + "\n";
226
- socket.write(cmd);
227
- socket.end();
228
- });
229
-
230
- socket.on("error", (err) => {
231
- log(`IPC socket error: ${err.message}`);
232
- });
233
- } catch (e) {
234
- log(`Failed to send IPC command: ${e}`);
235
- }
208
+ const newVolume = Math.max(0, Math.min(127, volume));
209
+ currentVolume = newVolume;
210
+
211
+ // If no music is playing, just save the volume for next play
212
+ if (!musicProcess || !currentMusicName) {
213
+ return;
214
+ }
215
+
216
+ // Convert to mpv volume (0-100)
217
+ const mpvVolume = Math.round((newVolume / 127) * 100);
218
+ log(`Setting music volume to ${mpvVolume} via IPC`);
219
+
220
+ // Send volume command via IPC socket
221
+ try {
222
+ const socket = createConnection(musicSocketPath);
223
+
224
+ socket.on("connect", () => {
225
+ const cmd = JSON.stringify({ command: ["set_property", "volume", mpvVolume] }) + "\n";
226
+ socket.write(cmd);
227
+ socket.end();
228
+ });
229
+
230
+ socket.on("error", (err) => {
231
+ log(`IPC socket error: ${err.message}`);
232
+ });
233
+ } catch (e) {
234
+ log(`Failed to send IPC command: ${e}`);
235
+ }
236
236
  }