@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/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
- if (!existsSync(SAVE_DIR)) {
24
- mkdirSync(SAVE_DIR, { recursive: true });
25
- debugLog("Saves", `Created save directory: ${SAVE_DIR}`);
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
- ensureSaveDir();
34
- return SAVE_DIR;
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
- 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`);
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
- 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}`);
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
- } catch (e) {
72
- debugLog("Saves", `Failed to list save directory: ${e}`);
69
+ }
73
70
  }
74
-
75
- debugLog("Saves", `Loaded ${saves.size} existing saves`);
76
- return saves;
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
- 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
- }
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
- const filePath = getSaveFilePath(slot);
101
- return existsSync(filePath);
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
- 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
- }
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
 
@@ -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
  }
@@ -61,42 +67,42 @@ const renderer = await createCliRenderer({
61
67
 
62
68
  // Handle graceful shutdown
63
69
  const cleanup = (signal?: string) => {
64
- debugLog('Exit', `cleanup called with signal: ${signal}`);
65
-
70
+ debugLog("Exit", `cleanup called with signal: ${signal}`);
71
+
66
72
  // Set flag to stop the game loop FIRST - this is critical
67
73
  isExiting = true;
68
- debugLog('Exit', 'isExiting set to true');
69
-
74
+ debugLog("Exit", "isExiting set to true");
75
+
70
76
  // Sync saves before exiting
71
77
  if (doomEngine) {
72
78
  try {
73
79
  doomEngine.syncSaves();
74
- debugLog('Exit', 'saves synced to disk');
80
+ debugLog("Exit", "saves synced to disk");
75
81
  } catch (e) {
76
- debugLog('Exit', `failed to sync saves: ${e}`);
82
+ debugLog("Exit", `failed to sync saves: ${e}`);
77
83
  }
78
84
  }
79
-
85
+
80
86
  // Clear the frame callback to stop DOOM from ticking
81
87
  try {
82
88
  renderer.setFrameCallback(null as any);
83
- debugLog('Exit', 'frame callback cleared');
89
+ debugLog("Exit", "frame callback cleared");
84
90
  } catch (e) {
85
- debugLog('Exit', `failed to clear frame callback: ${e}`);
91
+ debugLog("Exit", `failed to clear frame callback: ${e}`);
86
92
  }
87
-
93
+
88
94
  shutdownAudio();
89
- debugLog('Exit', 'shutdownAudio completed');
90
-
95
+ debugLog("Exit", "shutdownAudio completed");
96
+
91
97
  try {
92
98
  renderer.stop();
93
- debugLog('Exit', 'renderer.stop completed');
99
+ debugLog("Exit", "renderer.stop completed");
94
100
  } catch (e) {
95
- debugLog('Exit', `renderer.stop error: ${e}`);
101
+ debugLog("Exit", `renderer.stop error: ${e}`);
96
102
  }
97
-
103
+
98
104
  // Exit the process
99
- debugLog('Exit', 'calling process.exit(0)');
105
+ debugLog("Exit", "calling process.exit(0)");
100
106
  process.exit(0);
101
107
  };
102
108
 
@@ -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,9 +186,32 @@ 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
-
182
215
  } catch (error) {
183
216
  loadingText.content = `Error: ${error}`;
184
217
  loadingText.fg = RGBA.fromInts(255, 100, 100);
@@ -208,15 +241,15 @@ async function initDoom() {
208
241
  }
209
242
  }
210
243
 
211
- async function gameLoop(deltaMs: number) {
244
+ async function gameLoop(_deltaMs: number) {
212
245
  // Bail out immediately if we're exiting
213
246
  if (isExiting) return;
214
-
247
+
215
248
  if (!doomEngine || !framebufferRenderable) return;
216
249
 
217
250
  // Run DOOM tick
218
251
  doomEngine.tick();
219
-
252
+
220
253
  // Periodic save sync (every 5 seconds)
221
254
  const now = Date.now();
222
255
  if (now - lastSaveSyncTime > SAVE_SYNC_INTERVAL) {
@@ -236,7 +269,7 @@ async function gameLoop(deltaMs: number) {
236
269
  // Render to OpenTUI framebuffer using half-block characters
237
270
  // The upper half-block character (▀) uses foreground for top pixel, background for bottom
238
271
  for (let y = 0; y < fb.height; y++) {
239
- const srcY1 = Math.floor(y * 2 * scaleY); // Top pixel row
272
+ const srcY1 = Math.floor(y * 2 * scaleY); // Top pixel row
240
273
  const srcY2 = Math.floor((y * 2 + 1) * scaleY); // Bottom pixel row
241
274
 
242
275
  for (let x = 0; x < fb.width; x++) {