@muhammedaksam/opentui-doom 0.1.2 → 0.2.1
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/doom/build/doom.js +1 -1
- package/doom/build/doom.wasm +0 -0
- package/doom/doom_js_sound_bridge.c +240 -0
- package/doom/i_sound.c +327 -0
- package/doom/s_sound.c +566 -0
- package/package.json +5 -1
- package/scripts/build-doom.sh +7 -1
- package/sound/d_bunny.mp3 +0 -0
- package/sound/d_e1m1.mp3 +0 -0
- package/sound/d_e1m5.mp3 +0 -0
- package/sound/d_intro.mp3 +0 -0
- package/sound/dsbarexp.wav +0 -0
- package/sound/dsbdcls.wav +0 -0
- package/sound/dsbdopn.wav +0 -0
- package/sound/dsbfg.wav +0 -0
- package/sound/dsbgact.wav +0 -0
- package/sound/dsbgdth1.wav +0 -0
- package/sound/dsbgdth2.wav +0 -0
- package/sound/dsbgsit1.wav +0 -0
- package/sound/dsbgsit2.wav +0 -0
- package/sound/dsboscub.wav +0 -0
- package/sound/dsbosdth.wav +0 -0
- package/sound/dsbospit.wav +0 -0
- package/sound/dsbospn.wav +0 -0
- package/sound/dsbossit.wav +0 -0
- package/sound/dsbrsdth.wav +0 -0
- package/sound/dsbrssit.wav +0 -0
- package/sound/dsbspact.wav +0 -0
- package/sound/dsbspdth.wav +0 -0
- package/sound/dsbspsit.wav +0 -0
- package/sound/dsbspwlk.wav +0 -0
- package/sound/dscacdth.wav +0 -0
- package/sound/dscacsit.wav +0 -0
- package/sound/dsclaw.wav +0 -0
- package/sound/dscybdth.wav +0 -0
- package/sound/dscybsit.wav +0 -0
- package/sound/dsdbcls.wav +0 -0
- package/sound/dsdbload.wav +0 -0
- package/sound/dsdbopn.wav +0 -0
- package/sound/dsdmact.wav +0 -0
- package/sound/dsdmpain.wav +0 -0
- package/sound/dsdorcls.wav +0 -0
- package/sound/dsdoropn.wav +0 -0
- package/sound/dsdshtgn.wav +0 -0
- package/sound/dsfirsht.wav +0 -0
- package/sound/dsfirxpl.wav +0 -0
- package/sound/dsflame.wav +0 -0
- package/sound/dsflamst.wav +0 -0
- package/sound/dsgetpow.wav +0 -0
- package/sound/dshoof.wav +0 -0
- package/sound/dsitemup.wav +0 -0
- package/sound/dsitmbk.wav +0 -0
- package/sound/dskeendt.wav +0 -0
- package/sound/dskeenpn.wav +0 -0
- package/sound/dskntdth.wav +0 -0
- package/sound/dskntsit.wav +0 -0
- package/sound/dsmanatk.wav +0 -0
- package/sound/dsmandth.wav +0 -0
- package/sound/dsmansit.wav +0 -0
- package/sound/dsmetal.wav +0 -0
- package/sound/dsmnpain.wav +0 -0
- package/sound/dsnoway.wav +0 -0
- package/sound/dsoof.wav +0 -0
- package/sound/dspdiehi.wav +0 -0
- package/sound/dspedth.wav +0 -0
- package/sound/dspepain.wav +0 -0
- package/sound/dspesit.wav +0 -0
- package/sound/dspistol.wav +0 -0
- package/sound/dsplasma.wav +0 -0
- package/sound/dspldeth.wav +0 -0
- package/sound/dsplpain.wav +0 -0
- package/sound/dspodth1.wav +0 -0
- package/sound/dspodth2.wav +0 -0
- package/sound/dspodth3.wav +0 -0
- package/sound/dspopain.wav +0 -0
- package/sound/dsposact.wav +0 -0
- package/sound/dsposit1.wav +0 -0
- package/sound/dsposit2.wav +0 -0
- package/sound/dsposit3.wav +0 -0
- package/sound/dspstart.wav +0 -0
- package/sound/dspstop.wav +0 -0
- package/sound/dspunch.wav +0 -0
- package/sound/dsradio.wav +0 -0
- package/sound/dsrlaunc.wav +0 -0
- package/sound/dsrxplod.wav +0 -0
- package/sound/dssawful.wav +0 -0
- package/sound/dssawhit.wav +0 -0
- package/sound/dssawidl.wav +0 -0
- package/sound/dssawup.wav +0 -0
- package/sound/dssgcock.wav +0 -0
- package/sound/dssgtatk.wav +0 -0
- package/sound/dssgtdth.wav +0 -0
- package/sound/dssgtsit.wav +0 -0
- package/sound/dsshotgn.wav +0 -0
- package/sound/dsskeact.wav +0 -0
- package/sound/dsskeatk.wav +0 -0
- package/sound/dsskedth.wav +0 -0
- package/sound/dsskepch.wav +0 -0
- package/sound/dsskesit.wav +0 -0
- package/sound/dsskeswg.wav +0 -0
- package/sound/dssklatk.wav +0 -0
- package/sound/dsskldth.wav +0 -0
- package/sound/dsslop.wav +0 -0
- package/sound/dsspidth.wav +0 -0
- package/sound/dsspisit.wav +0 -0
- package/sound/dsssdth.wav +0 -0
- package/sound/dssssit.wav +0 -0
- package/sound/dsstnmov.wav +0 -0
- package/sound/dsswtchn.wav +0 -0
- package/sound/dsswtchx.wav +0 -0
- package/sound/dstelept.wav +0 -0
- package/sound/dstink.wav +0 -0
- package/sound/dsvilact.wav +0 -0
- package/sound/dsvilatk.wav +0 -0
- package/sound/dsvildth.wav +0 -0
- package/sound/dsvilsit.wav +0 -0
- package/sound/dsvipain.wav +0 -0
- package/sound/dswpnup.wav +0 -0
- package/src/doom-audio.ts +243 -0
- package/src/doom-engine.ts +32 -6
- package/src/doom-input.ts +18 -3
- package/src/index.ts +21 -2
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/sound/dshoof.wav
ADDED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/sound/dsoof.wav
ADDED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/sound/dsslop.wav
ADDED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/sound/dstink.wav
ADDED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOOM Audio Bridge for OpenTUI
|
|
3
|
+
*
|
|
4
|
+
* Handles audio playback using mpv with proper process management.
|
|
5
|
+
* All spawned processes are tracked and terminated on shutdown.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { spawn, ChildProcess } from "child_process";
|
|
9
|
+
import { join } from "path";
|
|
10
|
+
import { existsSync, appendFileSync, unlinkSync } from "fs";
|
|
11
|
+
import { createConnection, Socket } from "net";
|
|
12
|
+
|
|
13
|
+
// Log file for debugging
|
|
14
|
+
const logFile = join(import.meta.dir, "..", "debug.log");
|
|
15
|
+
|
|
16
|
+
function log(message: string): void {
|
|
17
|
+
const timestamp = new Date().toISOString();
|
|
18
|
+
const line = `[${timestamp}] [Audio] ${message}\n`;
|
|
19
|
+
try {
|
|
20
|
+
appendFileSync(logFile, line);
|
|
21
|
+
} catch (e) {
|
|
22
|
+
// Ignore logging errors
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Track all spawned mpv processes for cleanup
|
|
27
|
+
const activeProcesses = new Set<ChildProcess>();
|
|
28
|
+
|
|
29
|
+
// Current music process (only one music track at a time)
|
|
30
|
+
let musicProcess: ChildProcess | null = null;
|
|
31
|
+
|
|
32
|
+
// Current volume (0-127, DOOM standard)
|
|
33
|
+
let currentVolume = 100;
|
|
34
|
+
|
|
35
|
+
// Sound directory path
|
|
36
|
+
const soundDir = join(import.meta.dir, "..", "sound");
|
|
37
|
+
|
|
38
|
+
// IPC socket path for music volume control
|
|
39
|
+
const musicSocketPath = "/tmp/doom-music-mpv.sock";
|
|
40
|
+
|
|
41
|
+
// Whether audio is initialized
|
|
42
|
+
let initialized = false;
|
|
43
|
+
|
|
44
|
+
// Current music state for volume changes
|
|
45
|
+
let currentMusicName: string | null = null;
|
|
46
|
+
let currentMusicLooping: boolean = false;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Initialize the audio system
|
|
50
|
+
*/
|
|
51
|
+
export function initAudio(): void {
|
|
52
|
+
if (initialized) return;
|
|
53
|
+
initialized = true;
|
|
54
|
+
log(`Initialized, sound dir: ${soundDir}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Shutdown the audio system and kill ALL spawned processes
|
|
59
|
+
*/
|
|
60
|
+
export function shutdownAudio(): void {
|
|
61
|
+
if (!initialized) return;
|
|
62
|
+
|
|
63
|
+
// Kill music process
|
|
64
|
+
if (musicProcess) {
|
|
65
|
+
try {
|
|
66
|
+
musicProcess.kill("SIGKILL");
|
|
67
|
+
} catch (e) {
|
|
68
|
+
// Process may have already exited
|
|
69
|
+
}
|
|
70
|
+
musicProcess = null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Kill ALL tracked processes
|
|
74
|
+
for (const proc of activeProcesses) {
|
|
75
|
+
try {
|
|
76
|
+
proc.kill("SIGKILL");
|
|
77
|
+
} catch (e) {
|
|
78
|
+
// Process may have already exited
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
activeProcesses.clear();
|
|
82
|
+
|
|
83
|
+
initialized = false;
|
|
84
|
+
log("Shutdown complete");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Helper to spawn mpv with common options
|
|
89
|
+
*/
|
|
90
|
+
function spawnMpv(filePath: string, options: string[] = []): ChildProcess | null {
|
|
91
|
+
if (!existsSync(filePath)) {
|
|
92
|
+
log(`File not found: ${filePath}`);
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const args = [
|
|
97
|
+
"--no-video", // No video output
|
|
98
|
+
"--no-terminal", // No terminal output
|
|
99
|
+
"--really-quiet", // Suppress all output
|
|
100
|
+
...options,
|
|
101
|
+
filePath
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const proc = spawn("mpv", args, {
|
|
106
|
+
stdio: "ignore",
|
|
107
|
+
detached: false, // Keep attached to parent process
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Track the process
|
|
111
|
+
activeProcesses.add(proc);
|
|
112
|
+
|
|
113
|
+
// Remove from tracking when process exits
|
|
114
|
+
proc.on("exit", () => {
|
|
115
|
+
activeProcesses.delete(proc);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
proc.on("error", (err) => {
|
|
119
|
+
log(`mpv error: ${err.message}`);
|
|
120
|
+
activeProcesses.delete(proc);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
return proc;
|
|
124
|
+
} catch (e) {
|
|
125
|
+
log(`Failed to spawn mpv: ${e}`);
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Play a sound effect
|
|
132
|
+
* Sound files should be in sound/ds{name}.wav
|
|
133
|
+
* Volume is 0-127 (DOOM standard)
|
|
134
|
+
*/
|
|
135
|
+
export function playSound(name: string, volume: number = 127): void {
|
|
136
|
+
if (!initialized) {
|
|
137
|
+
log("playSound called but not initialized");
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const soundPath = join(soundDir, `ds${name.toLowerCase()}.wav`);
|
|
142
|
+
|
|
143
|
+
// Convert DOOM volume (0-127) to mpv volume (0-100)
|
|
144
|
+
const mpvVolume = Math.round((volume / 127) * 100);
|
|
145
|
+
log(`Playing sound: ${soundPath} at volume ${mpvVolume}`);
|
|
146
|
+
|
|
147
|
+
// Fire and forget - process will auto-cleanup when done
|
|
148
|
+
const proc = spawnMpv(soundPath, [`--volume=${mpvVolume}`]);
|
|
149
|
+
log(`Spawn result: ${proc ? "success" : "failed"}`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Play music track
|
|
154
|
+
* Music files should be in sound/{name}.mp3
|
|
155
|
+
*/
|
|
156
|
+
export function playMusic(name: string, looping: boolean): void {
|
|
157
|
+
if (!initialized) {
|
|
158
|
+
log("playMusic called but not initialized");
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Stop any currently playing music
|
|
163
|
+
stopMusic();
|
|
164
|
+
|
|
165
|
+
// Clean up any stale socket file
|
|
166
|
+
try {
|
|
167
|
+
if (existsSync(musicSocketPath)) {
|
|
168
|
+
unlinkSync(musicSocketPath);
|
|
169
|
+
}
|
|
170
|
+
} catch (e) {
|
|
171
|
+
// Ignore
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Store music state for volume changes
|
|
175
|
+
currentMusicName = name;
|
|
176
|
+
currentMusicLooping = looping;
|
|
177
|
+
|
|
178
|
+
const musicPath = join(soundDir, `${name.toLowerCase()}.mp3`);
|
|
179
|
+
log(`Playing music: ${musicPath}, looping: ${looping}`);
|
|
180
|
+
const options: string[] = [
|
|
181
|
+
`--input-ipc-server=${musicSocketPath}`, // Enable IPC for volume control
|
|
182
|
+
];
|
|
183
|
+
if (looping) {
|
|
184
|
+
options.push("--loop=inf");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Set volume (mpv uses 0-100 scale, DOOM uses 0-127)
|
|
188
|
+
const mpvVolume = Math.round((currentVolume / 127) * 100);
|
|
189
|
+
options.push(`--volume=${mpvVolume}`);
|
|
190
|
+
|
|
191
|
+
musicProcess = spawnMpv(musicPath, options);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Stop the currently playing music
|
|
196
|
+
*/
|
|
197
|
+
export function stopMusic(): void {
|
|
198
|
+
if (musicProcess) {
|
|
199
|
+
try {
|
|
200
|
+
musicProcess.kill("SIGTERM");
|
|
201
|
+
} catch (e) {
|
|
202
|
+
// Process may have already exited
|
|
203
|
+
}
|
|
204
|
+
activeProcesses.delete(musicProcess);
|
|
205
|
+
musicProcess = null;
|
|
206
|
+
}
|
|
207
|
+
currentMusicName = null;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Set music volume (0-127)
|
|
212
|
+
* Uses IPC socket to change volume without restarting music
|
|
213
|
+
*/
|
|
214
|
+
export function setMusicVolume(volume: number): void {
|
|
215
|
+
const newVolume = Math.max(0, Math.min(127, volume));
|
|
216
|
+
currentVolume = newVolume;
|
|
217
|
+
|
|
218
|
+
// If no music is playing, just save the volume for next play
|
|
219
|
+
if (!musicProcess || !currentMusicName) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Convert to mpv volume (0-100)
|
|
224
|
+
const mpvVolume = Math.round((newVolume / 127) * 100);
|
|
225
|
+
log(`Setting music volume to ${mpvVolume} via IPC`);
|
|
226
|
+
|
|
227
|
+
// Send volume command via IPC socket
|
|
228
|
+
try {
|
|
229
|
+
const socket = createConnection(musicSocketPath);
|
|
230
|
+
|
|
231
|
+
socket.on("connect", () => {
|
|
232
|
+
const cmd = JSON.stringify({ command: ["set_property", "volume", mpvVolume] }) + "\n";
|
|
233
|
+
socket.write(cmd);
|
|
234
|
+
socket.end();
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
socket.on("error", (err) => {
|
|
238
|
+
log(`IPC socket error: ${err.message}`);
|
|
239
|
+
});
|
|
240
|
+
} catch (e) {
|
|
241
|
+
log(`Failed to send IPC command: ${e}`);
|
|
242
|
+
}
|
|
243
|
+
}
|
package/src/doom-engine.ts
CHANGED
|
@@ -41,15 +41,30 @@ export interface DoomModule {
|
|
|
41
41
|
getValue: (ptr: number, type: string) => number;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
export interface DoomEngineOptions {
|
|
45
|
+
wadPath: string;
|
|
46
|
+
print?: (text: string) => void;
|
|
47
|
+
printErr?: (text: string) => void;
|
|
48
|
+
}
|
|
49
|
+
|
|
44
50
|
export class DoomEngine {
|
|
45
51
|
private module: DoomModule | null = null;
|
|
46
52
|
private frameBufferPtr: number = 0;
|
|
47
53
|
private initialized: boolean = false;
|
|
48
54
|
private wadPath: string;
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
55
|
+
private print: (text: string) => void;
|
|
56
|
+
private printErr: (text: string) => void;
|
|
57
|
+
|
|
58
|
+
constructor(optionsOrPath: string | DoomEngineOptions) {
|
|
59
|
+
if (typeof optionsOrPath === "string") {
|
|
60
|
+
this.wadPath = resolve(optionsOrPath);
|
|
61
|
+
this.print = (text: string) => console.log('[DOOM]', text);
|
|
62
|
+
this.printErr = (text: string) => console.error('[DOOM]', text);
|
|
63
|
+
} else {
|
|
64
|
+
this.wadPath = resolve(optionsOrPath.wadPath);
|
|
65
|
+
this.print = optionsOrPath.print || ((text: string) => console.log('[DOOM]', text));
|
|
66
|
+
this.printErr = optionsOrPath.printErr || ((text: string) => console.error('[DOOM]', text));
|
|
67
|
+
}
|
|
53
68
|
}
|
|
54
69
|
|
|
55
70
|
async init(): Promise<void> {
|
|
@@ -64,6 +79,9 @@ export class DoomEngine {
|
|
|
64
79
|
// Dynamic import of the compiled DOOM module
|
|
65
80
|
const createDoomModule = require(doomJsPath);
|
|
66
81
|
|
|
82
|
+
// Import audio system
|
|
83
|
+
const audio = await import("./doom-audio");
|
|
84
|
+
|
|
67
85
|
// Create module with proper callbacks
|
|
68
86
|
const moduleConfig: any = {
|
|
69
87
|
locateFile: (path: string) => {
|
|
@@ -72,8 +90,16 @@ export class DoomEngine {
|
|
|
72
90
|
}
|
|
73
91
|
return path;
|
|
74
92
|
},
|
|
75
|
-
print: (text: string) =>
|
|
76
|
-
printErr: (text: string) =>
|
|
93
|
+
print: (text: string) => this.print(text),
|
|
94
|
+
printErr: (text: string) => this.printErr(text),
|
|
95
|
+
|
|
96
|
+
// Audio callbacks - called from C via EM_ASM
|
|
97
|
+
initAudio: () => audio.initAudio(),
|
|
98
|
+
shutdownAudio: () => audio.shutdownAudio(),
|
|
99
|
+
playSound: (name: string, volume: number) => audio.playSound(name, volume),
|
|
100
|
+
playMusic: (name: string, looping: boolean) => audio.playMusic(name, looping),
|
|
101
|
+
stopMusic: () => audio.stopMusic(),
|
|
102
|
+
setMusicVolume: (volume: number) => audio.setMusicVolume(volume),
|
|
77
103
|
|
|
78
104
|
// preRun receives Module as first argument
|
|
79
105
|
preRun: [
|
package/src/doom-input.ts
CHANGED
|
@@ -80,8 +80,8 @@ function mapKeyToDoom(key: KeyEvent): number | null {
|
|
|
80
80
|
if (name === "tab") return DoomKeys.KEY_TAB;
|
|
81
81
|
if (name === "backspace") return DoomKeys.KEY_BACKSPACE;
|
|
82
82
|
|
|
83
|
-
// Fire (Ctrl)
|
|
84
|
-
if (key.ctrl) return DoomKeys.KEY_FIRE;
|
|
83
|
+
// Fire (Ctrl) - but not Ctrl+C which should exit
|
|
84
|
+
if (key.ctrl && key.name !== "c") return DoomKeys.KEY_FIRE;
|
|
85
85
|
|
|
86
86
|
// Alt for strafe
|
|
87
87
|
if (key.meta || key.name === "alt") return DoomKeys.KEY_LALT;
|
|
@@ -128,8 +128,23 @@ function mapKeyToDoom(key: KeyEvent): number | null {
|
|
|
128
128
|
// Track release timers for each key
|
|
129
129
|
const keyTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
130
130
|
|
|
131
|
-
export
|
|
131
|
+
export interface DoomInputOptions {
|
|
132
|
+
engine: DoomEngine;
|
|
133
|
+
onExit?: () => void;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function createDoomInputHandler(options: DoomInputOptions) {
|
|
137
|
+
const { engine, onExit } = options;
|
|
138
|
+
|
|
132
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
|
+
}
|
|
147
|
+
|
|
133
148
|
const doomKey = mapKeyToDoom(key);
|
|
134
149
|
|
|
135
150
|
if (doomKey === null) return;
|
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 { shutdownAudio } from "./doom-audio";
|
|
20
21
|
import { parseArgs } from "util";
|
|
21
22
|
|
|
22
23
|
// Parse command line arguments
|
|
@@ -53,10 +54,25 @@ ${getControlsHelp()}
|
|
|
53
54
|
|
|
54
55
|
// Initialize renderer
|
|
55
56
|
const renderer = await createCliRenderer({
|
|
56
|
-
exitOnCtrlC:
|
|
57
|
+
exitOnCtrlC: false, // We handle exit manually to cleanup audio
|
|
57
58
|
targetFps: 35, // DOOM's native framerate
|
|
58
59
|
});
|
|
59
60
|
|
|
61
|
+
// Handle graceful shutdown
|
|
62
|
+
const cleanup = (signal?: string) => {
|
|
63
|
+
shutdownAudio();
|
|
64
|
+
try {
|
|
65
|
+
renderer.stop();
|
|
66
|
+
} catch (e) {
|
|
67
|
+
// Ignore error if renderer already stopped
|
|
68
|
+
}
|
|
69
|
+
process.exit(0);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
process.on("SIGINT", () => cleanup("SIGINT"));
|
|
73
|
+
process.on("SIGTERM", () => cleanup("SIGTERM"));
|
|
74
|
+
process.on("exit", () => shutdownAudio());
|
|
75
|
+
|
|
60
76
|
renderer.start();
|
|
61
77
|
|
|
62
78
|
// Create UI container
|
|
@@ -117,7 +133,10 @@ async function initDoom() {
|
|
|
117
133
|
renderer.root.add(controlsText);
|
|
118
134
|
|
|
119
135
|
// Set up input handler
|
|
120
|
-
const inputHandler = createDoomInputHandler(
|
|
136
|
+
const inputHandler = createDoomInputHandler({
|
|
137
|
+
engine: doomEngine,
|
|
138
|
+
onExit: cleanup,
|
|
139
|
+
});
|
|
121
140
|
renderer.keyInput.on("keypress", inputHandler);
|
|
122
141
|
|
|
123
142
|
// Start game loop
|