@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/README.md
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
[](https://www.npmjs.com/package/@muhammedaksam/opentui-doom)
|
|
2
|
+
[](https://opensource.org/licenses/MIT)
|
|
3
|
+
[](https://www.typescriptlang.org/)
|
|
4
|
+
[](https://bun.sh/)
|
|
5
|
+
[](https://alacritty.org/)
|
|
6
|
+
[](https://github.com/muhammedaksam/opentui-doom/actions)
|
|
7
|
+
[](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
|
-
│
|
|
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.
|
|
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
|
-
"@
|
|
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.
|
|
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 (
|
|
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
|
|
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
|
-
|
|
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
|
|
39
|
+
let _currentMusicLooping: boolean = false;
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
42
|
* Initialize the audio system
|
|
43
43
|
*/
|
|
44
44
|
export function initAudio(): void {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
73
|
+
}
|
|
74
|
+
activeProcesses.clear();
|
|
75
75
|
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
129
|
+
if (!initialized) {
|
|
130
|
+
log("playSound called but not initialized");
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
133
|
|
|
134
|
-
|
|
134
|
+
const soundPath = join(soundDir, `ds${name.toLowerCase()}.wav`);
|
|
135
135
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
//
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
}
|