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