@muhammedaksam/opentui-doom 0.3.5 → 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 +8 -0
- package/package.json +11 -4
- package/src/debug.ts +3 -3
- package/src/doom-audio.ts +146 -146
- package/src/doom-engine.ts +299 -304
- package/src/doom-input.ts +173 -173
- package/src/doom-mouse.ts +106 -106
- package/src/doom-saves.ts +60 -60
- package/src/index.ts +27 -28
package/src/doom-saves.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Save Game Manager for OpenTUI-DOOM
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* Handles persistence of DOOM save games to ~/.opentui-doom/
|
|
5
5
|
* DOOM uses 6 save slots (0-5) with files named doomsav{N}.dsg
|
|
6
6
|
*/
|
|
@@ -20,28 +20,28 @@ const SAVE_FILE_PATTERN = /^doomsav([0-5])\.dsg$/;
|
|
|
20
20
|
* Ensure the save directory exists
|
|
21
21
|
*/
|
|
22
22
|
export function ensureSaveDir(): void {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
if (!existsSync(SAVE_DIR)) {
|
|
24
|
+
mkdirSync(SAVE_DIR, { recursive: true });
|
|
25
|
+
debugLog("Saves", `Created save directory: ${SAVE_DIR}`);
|
|
26
|
+
}
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
30
|
* Get the save game directory path
|
|
31
31
|
*/
|
|
32
32
|
export function getSaveGameDir(): string {
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
ensureSaveDir();
|
|
34
|
+
return SAVE_DIR;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
38
|
* Get the path to a save file for a given slot (0-5)
|
|
39
39
|
*/
|
|
40
40
|
export function getSaveFilePath(slot: number): string {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
if (slot < 0 || slot > 5) {
|
|
42
|
+
throw new Error(`Invalid save slot: ${slot}. Must be 0-5.`);
|
|
43
|
+
}
|
|
44
|
+
return join(SAVE_DIR, `doomsav${slot}.dsg`);
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
/**
|
|
@@ -49,73 +49,73 @@ export function getSaveFilePath(slot: number): string {
|
|
|
49
49
|
* Returns a Map of slot number to file contents (Uint8Array)
|
|
50
50
|
*/
|
|
51
51
|
export function loadExistingSaves(): Map<number, Uint8Array> {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
-
}
|
|
52
|
+
ensureSaveDir();
|
|
53
|
+
const saves = new Map<number, Uint8Array>();
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const files = readdirSync(SAVE_DIR);
|
|
57
|
+
for (const file of files) {
|
|
58
|
+
const match = file.match(SAVE_FILE_PATTERN);
|
|
59
|
+
if (match && match[1]) {
|
|
60
|
+
const slot = parseInt(match[1], 10);
|
|
61
|
+
const filePath = join(SAVE_DIR, file);
|
|
62
|
+
try {
|
|
63
|
+
const data = readFileSync(filePath);
|
|
64
|
+
saves.set(slot, new Uint8Array(data));
|
|
65
|
+
debugLog("Saves", `Loaded save slot ${slot}: ${data.length} bytes`);
|
|
66
|
+
} catch (e) {
|
|
67
|
+
debugLog("Saves", `Failed to read save file ${filePath}: ${e}`);
|
|
70
68
|
}
|
|
71
|
-
|
|
72
|
-
debugLog("Saves", `Failed to list save directory: ${e}`);
|
|
69
|
+
}
|
|
73
70
|
}
|
|
74
|
-
|
|
75
|
-
debugLog("Saves", `
|
|
76
|
-
|
|
71
|
+
} catch (e) {
|
|
72
|
+
debugLog("Saves", `Failed to list save directory: ${e}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
debugLog("Saves", `Loaded ${saves.size} existing saves`);
|
|
76
|
+
return saves;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
/**
|
|
80
80
|
* Write a save game to disk
|
|
81
81
|
*/
|
|
82
82
|
export function writeSave(slot: number, data: Uint8Array): boolean {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
83
|
+
ensureSaveDir();
|
|
84
|
+
const filePath = getSaveFilePath(slot);
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
writeFileSync(filePath, data);
|
|
88
|
+
debugLog("Saves", `Wrote save slot ${slot}: ${data.length} bytes to ${filePath}`);
|
|
89
|
+
return true;
|
|
90
|
+
} catch (e) {
|
|
91
|
+
debugLog("Saves", `Failed to write save slot ${slot}: ${e}`);
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
/**
|
|
97
97
|
* Check if a save exists for a given slot
|
|
98
98
|
*/
|
|
99
99
|
export function saveExists(slot: number): boolean {
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
const filePath = getSaveFilePath(slot);
|
|
101
|
+
return existsSync(filePath);
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
/**
|
|
105
105
|
* Read a save game from disk
|
|
106
106
|
*/
|
|
107
107
|
export function readSave(slot: number): Uint8Array | null {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
108
|
+
const filePath = getSaveFilePath(slot);
|
|
109
|
+
|
|
110
|
+
if (!existsSync(filePath)) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const data = readFileSync(filePath);
|
|
116
|
+
return new Uint8Array(data);
|
|
117
|
+
} catch (e) {
|
|
118
|
+
debugLog("Saves", `Failed to read save slot ${slot}: ${e}`);
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
121
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
/**
|
|
3
3
|
* DOOM for OpenTUI
|
|
4
|
-
*
|
|
4
|
+
*
|
|
5
5
|
* Plays DOOM in your terminal using OpenTUI's framebuffer rendering.
|
|
6
|
-
*
|
|
6
|
+
*
|
|
7
7
|
* Usage: bun run dev -- --wad /path/to/doom1.wad
|
|
8
8
|
*/
|
|
9
9
|
|
|
@@ -54,7 +54,7 @@ Options:
|
|
|
54
54
|
-w, --wad Path to DOOM WAD file (default: doom1.wad)
|
|
55
55
|
-h, --help Show this help message
|
|
56
56
|
|
|
57
|
-
${getControlsHelp()}${values.mouse ?
|
|
57
|
+
${getControlsHelp()}${values.mouse ? "\n Mouse=Aim/Fire" : ""}
|
|
58
58
|
`);
|
|
59
59
|
process.exit(0);
|
|
60
60
|
}
|
|
@@ -67,42 +67,42 @@ const renderer = await createCliRenderer({
|
|
|
67
67
|
|
|
68
68
|
// Handle graceful shutdown
|
|
69
69
|
const cleanup = (signal?: string) => {
|
|
70
|
-
debugLog(
|
|
71
|
-
|
|
70
|
+
debugLog("Exit", `cleanup called with signal: ${signal}`);
|
|
71
|
+
|
|
72
72
|
// Set flag to stop the game loop FIRST - this is critical
|
|
73
73
|
isExiting = true;
|
|
74
|
-
debugLog(
|
|
75
|
-
|
|
74
|
+
debugLog("Exit", "isExiting set to true");
|
|
75
|
+
|
|
76
76
|
// Sync saves before exiting
|
|
77
77
|
if (doomEngine) {
|
|
78
78
|
try {
|
|
79
79
|
doomEngine.syncSaves();
|
|
80
|
-
debugLog(
|
|
80
|
+
debugLog("Exit", "saves synced to disk");
|
|
81
81
|
} catch (e) {
|
|
82
|
-
debugLog(
|
|
82
|
+
debugLog("Exit", `failed to sync saves: ${e}`);
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
|
-
|
|
85
|
+
|
|
86
86
|
// Clear the frame callback to stop DOOM from ticking
|
|
87
87
|
try {
|
|
88
88
|
renderer.setFrameCallback(null as any);
|
|
89
|
-
debugLog(
|
|
89
|
+
debugLog("Exit", "frame callback cleared");
|
|
90
90
|
} catch (e) {
|
|
91
|
-
debugLog(
|
|
91
|
+
debugLog("Exit", `failed to clear frame callback: ${e}`);
|
|
92
92
|
}
|
|
93
|
-
|
|
93
|
+
|
|
94
94
|
shutdownAudio();
|
|
95
|
-
debugLog(
|
|
96
|
-
|
|
95
|
+
debugLog("Exit", "shutdownAudio completed");
|
|
96
|
+
|
|
97
97
|
try {
|
|
98
98
|
renderer.stop();
|
|
99
|
-
debugLog(
|
|
99
|
+
debugLog("Exit", "renderer.stop completed");
|
|
100
100
|
} catch (e) {
|
|
101
|
-
debugLog(
|
|
101
|
+
debugLog("Exit", `renderer.stop error: ${e}`);
|
|
102
102
|
}
|
|
103
|
-
|
|
103
|
+
|
|
104
104
|
// Exit the process
|
|
105
|
-
debugLog(
|
|
105
|
+
debugLog("Exit", "calling process.exit(0)");
|
|
106
106
|
process.exit(0);
|
|
107
107
|
};
|
|
108
108
|
|
|
@@ -164,7 +164,7 @@ async function initDoom() {
|
|
|
164
164
|
renderer.root.add(framebufferRenderable);
|
|
165
165
|
|
|
166
166
|
// Add controls overlay
|
|
167
|
-
const controlsContent = values.mouse
|
|
167
|
+
const controlsContent = values.mouse
|
|
168
168
|
? "DOOM | Ctrl+C to exit | WASD=Move Mouse=Aim Click=Fire"
|
|
169
169
|
: "DOOM | Ctrl+C to exit | Arrow/WASD=Move Space=Use Ctrl=Fire";
|
|
170
170
|
const controlsText = new TextRenderable(renderer, {
|
|
@@ -190,9 +190,9 @@ async function initDoom() {
|
|
|
190
190
|
if (values.mouse) {
|
|
191
191
|
mouseHandler = createDoomMouseHandler({
|
|
192
192
|
engine: doomEngine,
|
|
193
|
-
sensitivity: 2,
|
|
193
|
+
sensitivity: 2, // Adjust for terminal cell size
|
|
194
194
|
});
|
|
195
|
-
|
|
195
|
+
|
|
196
196
|
// Attach mouse events to framebuffer
|
|
197
197
|
framebufferRenderable.onMouseMove = (event) => {
|
|
198
198
|
mouseHandler?.onMouseMove(event.x, event.y);
|
|
@@ -206,13 +206,12 @@ async function initDoom() {
|
|
|
206
206
|
framebufferRenderable.onMouseUp = (event) => {
|
|
207
207
|
mouseHandler?.onMouseUp(event.button);
|
|
208
208
|
};
|
|
209
|
-
|
|
209
|
+
|
|
210
210
|
debugLog("Input", "Mouse look enabled");
|
|
211
211
|
}
|
|
212
212
|
|
|
213
213
|
// Start game loop
|
|
214
214
|
renderer.setFrameCallback(gameLoop);
|
|
215
|
-
|
|
216
215
|
} catch (error) {
|
|
217
216
|
loadingText.content = `Error: ${error}`;
|
|
218
217
|
loadingText.fg = RGBA.fromInts(255, 100, 100);
|
|
@@ -242,15 +241,15 @@ async function initDoom() {
|
|
|
242
241
|
}
|
|
243
242
|
}
|
|
244
243
|
|
|
245
|
-
async function gameLoop(
|
|
244
|
+
async function gameLoop(_deltaMs: number) {
|
|
246
245
|
// Bail out immediately if we're exiting
|
|
247
246
|
if (isExiting) return;
|
|
248
|
-
|
|
247
|
+
|
|
249
248
|
if (!doomEngine || !framebufferRenderable) return;
|
|
250
249
|
|
|
251
250
|
// Run DOOM tick
|
|
252
251
|
doomEngine.tick();
|
|
253
|
-
|
|
252
|
+
|
|
254
253
|
// Periodic save sync (every 5 seconds)
|
|
255
254
|
const now = Date.now();
|
|
256
255
|
if (now - lastSaveSyncTime > SAVE_SYNC_INTERVAL) {
|
|
@@ -270,7 +269,7 @@ async function gameLoop(deltaMs: number) {
|
|
|
270
269
|
// Render to OpenTUI framebuffer using half-block characters
|
|
271
270
|
// The upper half-block character (▀) uses foreground for top pixel, background for bottom
|
|
272
271
|
for (let y = 0; y < fb.height; y++) {
|
|
273
|
-
const srcY1 = Math.floor(y * 2 * scaleY);
|
|
272
|
+
const srcY1 = Math.floor(y * 2 * scaleY); // Top pixel row
|
|
274
273
|
const srcY2 = Math.floor((y * 2 + 1) * scaleY); // Bottom pixel row
|
|
275
274
|
|
|
276
275
|
for (let x = 0; x < fb.width; x++) {
|