TypeScript · 11691 bytes Raw Blame History
1 "use client";
2
3 import React, { useEffect, useCallback, useState } from 'react';
4 import TreeVisualizer from './TreeVisualizer';
5 import Terminal from './Terminal';
6 import HelpModals from './HelpModals';
7 import GameStatus from './GameStatus';
8 import GameOverModal from './GameOverModal';
9 import { gameApi, CommandResponse, GameStats } from '@/lib/api';
10
11 // Import our custom hooks
12 import { useGameState } from '@/hooks/useGameState';
13 import { useCommandExecution } from '@/hooks/useCommandExecution';
14 import { useTerminal } from '@/hooks/useTerminal';
15 import { useHelpModals } from '@/hooks/useHelpModals';
16 import { useTreeUtils } from '@/hooks/useTreeUtils';
17
18 const Game: React.FC = () => {
19 // Canvas background color - always dark mode
20 const canvasBackground = 'bg-gray-900';
21 const isDarkMode = true;
22
23 // Use our custom hooks
24 const {
25 gameState,
26 gameStats,
27 hasPlayedIntro,
28 startNewGame,
29 updatePlayerLocation,
30 updateTreeData,
31 setMoleDirection,
32 setMoleKilled,
33 updateScore,
34 setHasPlayedIntro,
35 } = useGameState();
36
37 const {
38 command,
39 setCommand,
40 clearCommand,
41 terminalMinimized,
42 setTerminalMinimized,
43 } = useTerminal();
44
45 const helpModals = useHelpModals(gameState.tree?.id || null);
46
47 const { updateTreeDataToShowMole, removeMoleFromTree } = useTreeUtils();
48
49 // Game over modal state
50 const [showGameOver, setShowGameOver] = useState(false);
51 const [finalStats, setFinalStats] = useState<GameStats | null>(null);
52
53 // Handle mole kill animation and updates
54 const handleMoleKilled = useCallback((response: CommandResponse) => {
55 if (!gameState.tree) return;
56
57 // First, show the killed mole at the player's current location (where it was killed)
58 updateTreeData((tree) =>
59 updateTreeDataToShowMole(tree, gameState.tree!.player_location)
60 );
61
62 // Trigger falling animation on the current mole
63 setMoleKilled(true);
64
65 // After animation completes, remove old mole and show new mole
66 setTimeout(() => {
67 setMoleKilled(false);
68
69 // Now update tree to remove old mole and show new mole location
70 if (response.new_mole_location) {
71 updateTreeData((tree) => {
72 // First remove all moles from the tree
73 const cleanTree = removeMoleFromTree(tree);
74 // Then add the new mole at its new location
75 return updateTreeDataToShowMole(cleanTree, response.new_mole_location!);
76 });
77 }
78
79 // Set new mole direction
80 if (response.mole_direction) {
81 setMoleDirection(response.mole_direction);
82 }
83 }, 1500); // Wait for falling animation to complete
84
85 // Update score and moles killed
86 if (response.score !== undefined && response.moles_killed !== undefined) {
87 updateScore(response.score, response.moles_killed);
88 }
89 }, [gameState.tree, updateTreeData, setMoleKilled, setMoleDirection, updateScore, updateTreeDataToShowMole, removeMoleFromTree]);
90
91 const {
92 executing,
93 commandHistory,
94 executeCommand: executeCommandBase,
95 addToHistory,
96 clearHistory,
97 } = useCommandExecution(
98 gameState.tree?.id || null,
99 gameState.sessionId,
100 updatePlayerLocation,
101 handleMoleKilled,
102 updateTreeData,
103 (stats) => { // Add this callback for game completion
104 setFinalStats(stats);
105 setShowGameOver(true);
106 }
107 );
108
109 // Wrap executeCommand to clear the command input
110 const executeCommand = useCallback(async (cmd: string) => {
111 await executeCommandBase(cmd);
112 clearCommand();
113 }, [executeCommandBase, clearCommand]);
114
115 // Initialize game with welcome message
116 const initializeGame = useCallback(async () => {
117 const response = await startNewGame();
118 if (!response) return;
119
120 // Clear history for new game
121 clearHistory();
122
123 // Create a dynamic starting message
124 const startLocation = response.tree.player_location;
125 const homeDir = response.home_directory || '/home';
126 let locationContext = '';
127
128 if (startLocation.startsWith('/home')) {
129 locationContext = "You've been dropped in someone's home directory. ";
130 } else if (startLocation.startsWith('/usr')) {
131 locationContext = "You're in the system's usr hierarchy. ";
132 } else if (startLocation.startsWith('/var')) {
133 locationContext = "You're somewhere in the variable data area. ";
134 } else if (startLocation.startsWith('/opt')) {
135 locationContext = "You're in the optional packages directory. ";
136 } else {
137 locationContext = "You've been placed somewhere in the filesystem. ";
138 }
139
140 // Add timer info to starting message
141 let timerInfo = '';
142 if (response.initial_timer && response.timer_reason) {
143 timerInfo = `\nTimer: ${response.initial_timer}s (mole is ${response.timer_reason})`;
144 }
145
146 addToHistory({
147 command: 'Hunt started!',
148 output: `${response.mole_hint}\n${locationContext}Your home directory is ${homeDir}.\nUse 'pwd' to see where you are, 'cd ~' to go home.${timerInfo}\nType "help" for available commands.`,
149 success: true,
150 });
151 }, [startNewGame, addToHistory, clearHistory]);
152
153 // Handle timer expiration
154 const handleTimerExpire = useCallback(async () => {
155 if (!gameState.tree) return;
156
157 try {
158 const response = await gameApi.checkTimer(gameState.tree.id, gameState.sessionId || undefined);
159 if (response.mole_escaped) {
160 // Build the escape message
161 let escapeMessage = response.message || 'The mole escaped!';
162
163 // Add distance info for new mole if available
164 if (response.escape_data?.timer_reason) {
165 escapeMessage += `\nNew mole detected ${response.escape_data.timer_reason}!`;
166 }
167
168 // Update command history with escape message
169 addToHistory({
170 command: 'Mole escaped!',
171 output: escapeMessage,
172 success: false,
173 });
174
175 // Update mole direction if provided
176 if (response.escape_data?.new_location) {
177 // Update tree to show new mole location
178 updateTreeData((tree) => {
179 const cleanTree = removeMoleFromTree(tree);
180 return updateTreeDataToShowMole(cleanTree, response.escape_data!.new_location);
181 });
182
183 // Show mole direction indicator if provided
184 if (response.escape_data?.mole_direction) {
185 setMoleDirection(response.escape_data.mole_direction);
186 }
187 }
188 }
189 } catch (error) {
190 console.error('Failed to check timer:', error);
191 }
192 }, [gameState.tree, gameState.sessionId, addToHistory, updateTreeData, setMoleDirection, removeMoleFromTree, updateTreeDataToShowMole]);
193
194 // Handle node click in visualizer
195 const handleNodeClick = useCallback((path: string) => {
196 executeCommand(`cd ${path}`);
197 }, [executeCommand]);
198
199 // Handler for starting a new game from the modal
200 const handleNewGameFromModal = () => {
201 setShowGameOver(false);
202 setFinalStats(null);
203 initializeGame();
204 };
205
206 // Start game on mount
207 useEffect(() => {
208 initializeGame();
209 }, []); // eslint-disable-line react-hooks/exhaustive-deps
210
211 // Mark intro as played after delay
212 useEffect(() => {
213 if (gameState.tree && !hasPlayedIntro) {
214 const timer = setTimeout(() => {
215 setHasPlayedIntro(true);
216 }, 10000); // Add a buffer to ensure animation completes
217 return () => clearTimeout(timer);
218 }
219 }, [gameState.tree, hasPlayedIntro, setHasPlayedIntro]);
220
221 // Loading state
222 if (gameState.loading) {
223 return (
224 <div className={`flex items-center justify-center min-h-screen ${canvasBackground} text-white`}>
225 <div className="text-center">
226 <div className="text-2xl mb-4">Loading Bashamole...</div>
227 <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-current mx-auto"></div>
228 </div>
229 </div>
230 );
231 }
232
233 // Error state
234 if (gameState.error) {
235 return (
236 <div className={`flex items-center justify-center min-h-screen ${canvasBackground} text-white`}>
237 <div className="text-center">
238 <div className="text-red-400 mb-4">{gameState.error}</div>
239 <button
240 onClick={initializeGame}
241 className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition"
242 >
243 Try Again
244 </button>
245 </div>
246 </div>
247 );
248 }
249
250 // Initial state
251 if (!gameState.tree) {
252 return (
253 <div className={`flex items-center justify-center min-h-screen ${canvasBackground} text-white`}>
254 <div className="text-center">
255 <h1 className="text-4xl font-bold mb-4">Bashamole</h1>
256 <p className="text-gray-400 mb-8">Hunt the mole in the Unix filesystem!</p>
257 <button
258 onClick={initializeGame}
259 className="px-8 py-4 bg-green-600 text-white rounded-lg hover:bg-green-700 text-xl transition transform hover:scale-105"
260 >
261 Start New Game
262 </button>
263 </div>
264 </div>
265 );
266 }
267
268 // Main game UI
269 return (
270 <div className={`relative min-h-screen ${canvasBackground} overflow-hidden`}>
271 {/* Tree Canvas - Full Screen Background */}
272 <div className={`absolute inset-0 ${canvasBackground}`}>
273 <TreeVisualizer
274 treeData={gameState.tree.tree_data}
275 playerLocation={gameState.tree.player_location}
276 onNodeClick={handleNodeClick}
277 playIntro={!hasPlayedIntro}
278 isDarkMode={isDarkMode}
279 moleKilled={gameStats.moleKilled}
280 />
281 </div>
282
283 {/* Game Status (Timer, Score, Direction Indicator) */}
284 <GameStatus
285 gameTreeId={gameState.tree.id}
286 sessionId={gameState.sessionId}
287 onTimerExpire={handleTimerExpire}
288 score={gameStats.score}
289 molesKilled={gameStats.molesKilled}
290 moleDirection={gameStats.moleDirection}
291 />
292
293 {/* Terminal */}
294 <Terminal
295 commandHistory={commandHistory}
296 command={command}
297 setCommand={setCommand}
298 executeCommand={executeCommand}
299 executing={executing}
300 currentPath={gameState.tree.player_location}
301 terminalMinimized={terminalMinimized}
302 setTerminalMinimized={setTerminalMinimized}
303 onGetCommandReference={helpModals.getCommandReference}
304 onGetHints={helpModals.getHints}
305 onGetFHSReference={helpModals.getFHSReference}
306 />
307
308 {/* Help Modals */}
309 <HelpModals {...helpModals} />
310
311 {/* Game Over Modal */}
312 <GameOverModal
313 isOpen={showGameOver}
314 gameStats={finalStats}
315 sessionId={gameState.sessionId}
316 onClose={() => setShowGameOver(false)}
317 onNewGame={handleNewGameFromModal}
318 />
319
320 {/* Bottom Game Bar */}
321 <div className="absolute bottom-0 left-0 right-0 bg-slate-800/90 backdrop-blur-sm border-t border-slate-700 p-3 z-20">
322 <div className="max-w-7xl mx-auto flex justify-between items-center px-4">
323 <div className="flex items-center gap-4">
324 <h1 className="text-2xl font-bold text-white">
325 <span className='font-terminal bg-gray-500 text-red-400 px-0.5 py-0 rounded'>bash</span>
326 amole
327 </h1>
328 </div>
329
330 <div className="flex items-center gap-3">
331 <div className="text-xs text-slate-400">
332 click adjacent nodes or use the terminal
333 </div>
334 <button
335 onClick={initializeGame}
336 className="px-3 py-1.5 bg-slate-700 hover:bg-slate-600 text-white text-sm rounded transition"
337 >
338 New Game
339 </button>
340 </div>
341 </div>
342 </div>
343 </div>
344 );
345 };
346
347 export default Game;