· 2 months ago · Jul 11, 2025, 07:15 PM
1==++ Here's the full source code for (File 1\3) "Pool-Game-CloneV18.cpp"::: ++==
2```"Pool-Game-CloneV18.cpp"
3 #define WIN32_LEAN_AND_MEAN
4 #define NOMINMAX
5 #include <windows.h>
6 #include <d2d1.h>
7 #include <dwrite.h>
8 #include <fstream> // For file I/O
9 #include <iostream> // For some basic I/O, though not strictly necessary for just file ops
10 #include <vector>
11 #include <cmath>
12 #include <string>
13 #include <sstream> // Required for wostringstream
14 #include <algorithm> // Required for std::max, std::min
15 #include <ctime> // Required for srand, time
16 #include <cstdlib> // Required for srand, rand (often included by others, but good practice)
17 #include <commctrl.h> // Needed for radio buttons etc. in dialog (if using native controls)
18 #include <mmsystem.h> // For PlaySound
19 #include <tchar.h> //midi func
20 #include <thread>
21 #include <atomic>
22 #include "resource.h"
23
24 #ifndef HAS_STD_CLAMP
25 template <typename T>
26 T clamp(const T& v, const T& lo, const T& hi)
27 {
28 return (v < lo) ? lo : (v > hi) ? hi : v;
29 }
30 namespace std { using ::clamp; } // inject into std:: for seamless use
31 #define HAS_STD_CLAMP
32 #endif
33
34 #pragma comment(lib, "Comctl32.lib") // Link against common controls library
35 #pragma comment(lib, "d2d1.lib")
36 #pragma comment(lib, "dwrite.lib")
37 #pragma comment(lib, "Winmm.lib") // Link against Windows Multimedia library
38
39 // --- Constants ---
40 const float PI = 3.1415926535f;
41 const float BALL_RADIUS = 10.0f;
42 const float TABLE_LEFT = 100.0f;
43 const float TABLE_TOP = 100.0f;
44 const float TABLE_WIDTH = 700.0f;
45 const float TABLE_HEIGHT = 350.0f;
46 const float TABLE_RIGHT = TABLE_LEFT + TABLE_WIDTH;
47 const float TABLE_BOTTOM = TABLE_TOP + TABLE_HEIGHT;
48 const float CUSHION_THICKNESS = 20.0f;
49 const float HOLE_VISUAL_RADIUS = 22.0f; // Visual size of the hole
50 const float POCKET_RADIUS = HOLE_VISUAL_RADIUS * 1.05f; // Make detection radius slightly larger // Make detection radius match visual size (or slightly larger)
51 const float MAX_SHOT_POWER = 15.0f;
52 const float FRICTION = 0.985f; // Friction factor per frame
53 const float MIN_VELOCITY_SQ = 0.01f * 0.01f; // Stop balls below this squared velocity
54 const float HEADSTRING_X = TABLE_LEFT + TABLE_WIDTH * 0.30f; // 30% line
55 const float RACK_POS_X = TABLE_LEFT + TABLE_WIDTH * 0.65f; // 65% line for rack apex
56 const float RACK_POS_Y = TABLE_TOP + TABLE_HEIGHT / 2.0f;
57 const UINT ID_TIMER = 1;
58 const int TARGET_FPS = 60; // Target frames per second for timer
59
60 // --- Enums ---
61 // --- MODIFIED/NEW Enums ---
62 enum GameState {
63 SHOWING_DIALOG, // NEW: Game is waiting for initial dialog input
64 PRE_BREAK_PLACEMENT,// Player placing cue ball for break
65 BREAKING, // Player is aiming/shooting the break shot
66 CHOOSING_POCKET_P1, // NEW: Player 1 needs to call a pocket for the 8-ball
67 CHOOSING_POCKET_P2, // NEW: Player 2 needs to call a pocket for the 8-ball
68 AIMING, // Player is aiming
69 AI_THINKING, // NEW: AI is calculating its move
70 SHOT_IN_PROGRESS, // Balls are moving
71 ASSIGNING_BALLS, // Turn after break where ball types are assigned
72 PLAYER1_TURN,
73 PLAYER2_TURN,
74 BALL_IN_HAND_P1,
75 BALL_IN_HAND_P2,
76 GAME_OVER
77 };
78
79 enum BallType {
80 NONE,
81 SOLID, // Yellow (1-7)
82 STRIPE, // Red (9-15)
83 EIGHT_BALL, // Black (8)
84 CUE_BALL // White (0)
85 };
86
87 // NEW Enums for Game Mode and AI Difficulty
88 enum GameMode {
89 HUMAN_VS_HUMAN,
90 HUMAN_VS_AI
91 };
92
93 enum AIDifficulty {
94 EASY,
95 MEDIUM,
96 HARD
97 };
98
99 enum OpeningBreakMode {
100 CPU_BREAK,
101 P1_BREAK,
102 FLIP_COIN_BREAK
103 };
104
105 // --- Structs ---
106 struct Ball {
107 int id; // 0=Cue, 1-7=Solid, 8=Eight, 9-15=Stripe
108 BallType type;
109 float x, y;
110 float vx, vy;
111 D2D1_COLOR_F color;
112 bool isPocketed;
113 };
114
115 struct PlayerInfo {
116 BallType assignedType;
117 int ballsPocketedCount;
118 std::wstring name;
119 };
120
121 // --- Global Variables ---
122
123 // Direct2D & DirectWrite
124 ID2D1Factory* pFactory = nullptr;
125 //ID2D1Factory* g_pD2DFactory = nullptr;
126 ID2D1HwndRenderTarget* pRenderTarget = nullptr;
127 IDWriteFactory* pDWriteFactory = nullptr;
128 IDWriteTextFormat* pTextFormat = nullptr;
129 IDWriteTextFormat* pLargeTextFormat = nullptr; // For "Foul!"
130
131 // Game State
132 HWND hwndMain = nullptr;
133 GameState currentGameState = SHOWING_DIALOG; // Start by showing dialog
134 std::vector<Ball> balls;
135 int currentPlayer = 1; // 1 or 2
136 PlayerInfo player1Info = { BallType::NONE, 0, L"Vince Woods"/*"Player 1"*/ };
137 PlayerInfo player2Info = { BallType::NONE, 0, L"Virtus Pro"/*"CPU"*/ }; // Default P2 name
138 bool foulCommitted = false;
139 std::wstring gameOverMessage = L"";
140 bool firstBallPocketedAfterBreak = false;
141 std::vector<int> pocketedThisTurn;
142 // --- NEW: 8-Ball Pocket Call Globals ---
143 int calledPocketP1 = -1; // Pocket index (0-5) called by Player 1 for the 8-ball. -1 means not called.
144 int calledPocketP2 = -1; // Pocket index (0-5) called by Player 2 for the 8-ball.
145 int currentlyHoveredPocket = -1; // For visual feedback on which pocket is being hovered
146 std::wstring pocketCallMessage = L""; // Message like "Choose a pocket..."
147 // --- NEW: Remember which pocket the 8?ball actually went into last shot
148 int lastEightBallPocketIndex = -1;
149 //int lastPocketedIndex = -1; // pocket index (0–5) of the last ball pocketed
150 int called = -1;
151 bool cueBallPocketed = false;
152
153 // --- NEW: Foul Tracking Globals ---
154 int firstHitBallIdThisShot = -1; // ID of the first object ball hit by cue ball (-1 if none)
155 bool cueHitObjectBallThisShot = false; // Did cue ball hit an object ball this shot?
156 bool railHitAfterContact = false; // Did any ball hit a rail AFTER cue hit an object ball?
157 // --- End New Foul Tracking Globals ---
158
159 // NEW Game Mode/AI Globals
160 GameMode gameMode = HUMAN_VS_HUMAN; // Default mode
161 AIDifficulty aiDifficulty = MEDIUM; // Default difficulty
162 OpeningBreakMode openingBreakMode = CPU_BREAK; // Default opening break mode
163 bool isPlayer2AI = false; // Is Player 2 controlled by AI?
164 bool aiTurnPending = false; // Flag: AI needs to take its turn when possible
165 // bool aiIsThinking = false; // Replaced by AI_THINKING game state
166 // NEW: Flag to indicate if the current shot is the opening break of the game
167 bool isOpeningBreakShot = false;
168
169 // NEW: For AI shot planning and visualization
170 struct AIPlannedShot {
171 float angle;
172 float power;
173 float spinX;
174 float spinY;
175 bool isValid; // Is there a valid shot planned?
176 };
177 AIPlannedShot aiPlannedShotDetails; // Stores the AI's next shot
178 bool aiIsDisplayingAim = false; // True when AI has decided a shot and is in "display aim" mode
179 int aiAimDisplayFramesLeft = 0; // How many frames left to display AI aim
180 const int AI_AIM_DISPLAY_DURATION_FRAMES = 45; // Approx 0.75 seconds at 60 FPS, adjust as needed
181
182 // Input & Aiming
183 POINT ptMouse = { 0, 0 };
184 bool isAiming = false;
185 bool isDraggingCueBall = false;
186 // --- ENSURE THIS LINE EXISTS HERE ---
187 bool isDraggingStick = false; // True specifically when drag initiated on the stick graphic
188 // --- End Ensure ---
189 bool isSettingEnglish = false;
190 D2D1_POINT_2F aimStartPoint = { 0, 0 };
191 float cueAngle = 0.0f;
192 float shotPower = 0.0f;
193 float cueSpinX = 0.0f; // Range -1 to 1
194 float cueSpinY = 0.0f; // Range -1 to 1
195 float pocketFlashTimer = 0.0f;
196 bool cheatModeEnabled = false; // Cheat Mode toggle (G key)
197 int draggingBallId = -1;
198 bool keyboardAimingActive = false; // NEW FLAG: true when arrow keys modify aim/power
199 MCIDEVICEID midiDeviceID = 0; //midi func
200 std::atomic<bool> isMusicPlaying(false); //midi func
201 std::thread musicThread; //midi func
202 void StartMidi(HWND hwnd, const TCHAR* midiPath);
203 void StopMidi();
204
205 // UI Element Positions
206 D2D1_RECT_F powerMeterRect = { TABLE_RIGHT + CUSHION_THICKNESS + 10, TABLE_TOP, TABLE_RIGHT + CUSHION_THICKNESS + 40, TABLE_BOTTOM };
207 D2D1_RECT_F spinIndicatorRect = { TABLE_LEFT - CUSHION_THICKNESS - 60, TABLE_TOP + 20, TABLE_LEFT - CUSHION_THICKNESS - 20, TABLE_TOP + 60 }; // Circle area
208 D2D1_POINT_2F spinIndicatorCenter = { spinIndicatorRect.left + (spinIndicatorRect.right - spinIndicatorRect.left) / 2.0f, spinIndicatorRect.top + (spinIndicatorRect.bottom - spinIndicatorRect.top) / 2.0f };
209 float spinIndicatorRadius = (spinIndicatorRect.right - spinIndicatorRect.left) / 2.0f;
210 D2D1_RECT_F pocketedBallsBarRect = { TABLE_LEFT, TABLE_BOTTOM + CUSHION_THICKNESS + 30, TABLE_RIGHT, TABLE_BOTTOM + CUSHION_THICKNESS + 70 };
211
212 // Corrected Pocket Center Positions (aligned with table corners/edges)
213 const D2D1_POINT_2F pocketPositions[6] = {
214 {TABLE_LEFT, TABLE_TOP}, // Top-Left
215 {TABLE_LEFT + TABLE_WIDTH / 2.0f, TABLE_TOP}, // Top-Middle
216 {TABLE_RIGHT, TABLE_TOP}, // Top-Right
217 {TABLE_LEFT, TABLE_BOTTOM}, // Bottom-Left
218 {TABLE_LEFT + TABLE_WIDTH / 2.0f, TABLE_BOTTOM}, // Bottom-Middle
219 {TABLE_RIGHT, TABLE_BOTTOM} // Bottom-Right
220 };
221
222 // Colors
223 const D2D1_COLOR_F TABLE_COLOR = D2D1::ColorF(0.05f, 0.09f, 0.28f); // Darker Green NEWCOLOR (0.0f, 0.5f, 0.1f) => (0.1608f, 0.4000f, 0.1765f)
224 //const D2D1_COLOR_F TABLE_COLOR = D2D1::ColorF(0.0f, 0.5f, 0.1f); // Darker Green NEWCOLOR (0.0f, 0.5f, 0.1f) => (0.1608f, 0.4000f, 0.1765f)
225 const D2D1_COLOR_F CUSHION_COLOR = D2D1::ColorF(D2D1::ColorF(0.3608f, 0.0275f, 0.0078f)); // NEWCOLOR ::Red => (0.3608f, 0.0275f, 0.0078f)
226 //const D2D1_COLOR_F CUSHION_COLOR = D2D1::ColorF(D2D1::ColorF::Red); // NEWCOLOR ::Red => (0.3608f, 0.0275f, 0.0078f)
227 const D2D1_COLOR_F POCKET_COLOR = D2D1::ColorF(D2D1::ColorF::Black);
228 const D2D1_COLOR_F CUE_BALL_COLOR = D2D1::ColorF(D2D1::ColorF::White);
229 const D2D1_COLOR_F EIGHT_BALL_COLOR = D2D1::ColorF(D2D1::ColorF::Black);
230 const D2D1_COLOR_F SOLID_COLOR = D2D1::ColorF(D2D1::ColorF::Goldenrod); // Solids = Yellow Goldenrod
231 const D2D1_COLOR_F STRIPE_COLOR = D2D1::ColorF(D2D1::ColorF::DarkOrchid); // Stripes = Red DarkOrchid
232 const D2D1_COLOR_F AIM_LINE_COLOR = D2D1::ColorF(D2D1::ColorF::White, 0.7f); // Semi-transparent white
233 const D2D1_COLOR_F FOUL_TEXT_COLOR = D2D1::ColorF(D2D1::ColorF::Red);
234 const D2D1_COLOR_F TURN_ARROW_COLOR = D2D1::ColorF(0.1333f, 0.7294f, 0.7490f); //NEWCOLOR 0.1333f, 0.7294f, 0.7490f => ::Blue
235 //const D2D1_COLOR_F TURN_ARROW_COLOR = D2D1::ColorF(D2D1::ColorF::Blue);
236 const D2D1_COLOR_F ENGLISH_DOT_COLOR = D2D1::ColorF(D2D1::ColorF::Red);
237 const D2D1_COLOR_F UI_TEXT_COLOR = D2D1::ColorF(D2D1::ColorF::Black);
238
239 // --------------------------------------------------------------------
240// Realistic colours for each id (0-15)
241// 0 = cue-ball (white) | 1-7 solids | 8 = eight-ball | 9-15 stripes
242// --------------------------------------------------------------------
243 static const D2D1_COLOR_F BALL_COLORS[16] =
244 {
245 D2D1::ColorF(D2D1::ColorF::White), // 0 cue
246 D2D1::ColorF(1.00f, 0.85f, 0.00f), // 1 yellow
247 D2D1::ColorF(0.05f, 0.30f, 1.00f), // 2 blue
248 D2D1::ColorF(0.90f, 0.10f, 0.10f), // 3 red
249 D2D1::ColorF(0.55f, 0.25f, 0.85f), // 4 purple
250 D2D1::ColorF(1.00f, 0.55f, 0.00f), // 5 orange
251 D2D1::ColorF(0.00f, 0.60f, 0.30f), // 6 green
252 D2D1::ColorF(0.50f, 0.05f, 0.05f), // 7 maroon / burgundy
253 D2D1::ColorF(D2D1::ColorF::Black), // 8 black
254 D2D1::ColorF(1.00f, 0.85f, 0.00f), // 9 (yellow stripe)
255 D2D1::ColorF(0.05f, 0.30f, 1.00f), // 10 blue stripe
256 D2D1::ColorF(0.90f, 0.10f, 0.10f), // 11 red stripe
257 D2D1::ColorF(0.55f, 0.25f, 0.85f), // 12 purple stripe
258 D2D1::ColorF(1.00f, 0.55f, 0.00f), // 13 orange stripe
259 D2D1::ColorF(0.00f, 0.60f, 0.30f), // 14 green stripe
260 D2D1::ColorF(0.50f, 0.05f, 0.05f) // 15 maroon stripe
261 };
262
263 // Quick helper
264 inline D2D1_COLOR_F GetBallColor(int id)
265 {
266 return (id >= 0 && id < 16) ? BALL_COLORS[id]
267 : D2D1::ColorF(D2D1::ColorF::White);
268 }
269
270 // --- Forward Declarations ---
271 HRESULT CreateDeviceResources();
272 void DiscardDeviceResources();
273 void OnPaint();
274 void OnResize(UINT width, UINT height);
275 void InitGame();
276 void GameUpdate();
277 void UpdatePhysics();
278 void CheckCollisions();
279 bool CheckPockets(); // Returns true if any ball was pocketed
280 void ProcessShotResults();
281 void ApplyShot(float power, float angle, float spinX, float spinY);
282 void RespawnCueBall(bool behindHeadstring);
283 bool AreBallsMoving();
284 void SwitchTurns();
285 //bool AssignPlayerBallTypes(BallType firstPocketedType);
286 bool AssignPlayerBallTypes(BallType firstPocketedType,
287 bool creditShooter = true);
288 void CheckGameOverConditions(bool eightBallPocketed, bool cueBallPocketed);
289 Ball* GetBallById(int id);
290 Ball* GetCueBall();
291 //void PlayGameMusic(HWND hwnd); //midi func
292 void AIBreakShot();
293
294 // Drawing Functions
295 void DrawScene(ID2D1RenderTarget* pRT);
296 void DrawTable(ID2D1RenderTarget* pRT, ID2D1Factory* pFactory);
297 void DrawBalls(ID2D1RenderTarget* pRT);
298 void DrawCueStick(ID2D1RenderTarget* pRT);
299 void DrawAimingAids(ID2D1RenderTarget* pRT);
300 void DrawUI(ID2D1RenderTarget* pRT);
301 void DrawPowerMeter(ID2D1RenderTarget* pRT);
302 void DrawSpinIndicator(ID2D1RenderTarget* pRT);
303 void DrawPocketedBallsIndicator(ID2D1RenderTarget* pRT);
304 void DrawBallInHandIndicator(ID2D1RenderTarget* pRT);
305 // NEW
306 void DrawPocketSelectionIndicator(ID2D1RenderTarget* pRT);
307
308 // Helper Functions
309 float GetDistance(float x1, float y1, float x2, float y2);
310 float GetDistanceSq(float x1, float y1, float x2, float y2);
311 bool IsValidCueBallPosition(float x, float y, bool checkHeadstring);
312 template <typename T> void SafeRelease(T** ppT);
313 // --- NEW HELPER FORWARD DECLARATIONS ---
314 bool IsPlayerOnEightBall(int player);
315 void CheckAndTransitionToPocketChoice(int playerID);
316 // --- ADD FORWARD DECLARATION FOR NEW HELPER HERE ---
317 float PointToLineSegmentDistanceSq(D2D1_POINT_2F p, D2D1_POINT_2F a, D2D1_POINT_2F b);
318 // --- End Forward Declaration ---
319 bool LineSegmentIntersection(D2D1_POINT_2F p1, D2D1_POINT_2F p2, D2D1_POINT_2F p3, D2D1_POINT_2F p4, D2D1_POINT_2F& intersection); // Keep this if present
320
321 // --- NEW Forward Declarations ---
322
323 // AI Related
324 struct AIShotInfo; // Define below
325 void TriggerAIMove();
326 void AIMakeDecision();
327 void AIPlaceCueBall();
328 AIShotInfo AIFindBestShot();
329 AIShotInfo EvaluateShot(Ball* targetBall, int pocketIndex);
330 bool IsPathClear(D2D1_POINT_2F start, D2D1_POINT_2F end, int ignoredBallId1, int ignoredBallId2);
331 Ball* FindFirstHitBall(D2D1_POINT_2F start, float angle, float& hitDistSq); // Added hitDistSq output
332 float CalculateShotPower(float cueToGhostDist, float targetToPocketDist);
333 D2D1_POINT_2F CalculateGhostBallPos(Ball* targetBall, int pocketIndex);
334 bool IsValidAIAimAngle(float angle); // Basic check
335
336 // Dialog Related
337 INT_PTR CALLBACK NewGameDialogProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam);
338 void ShowNewGameDialog(HINSTANCE hInstance);
339 void LoadSettings(); // For deserialization
340 void SaveSettings(); // For serialization
341 const std::wstring SETTINGS_FILE_NAME = L"Pool-Settings.txt";
342 void ResetGame(HINSTANCE hInstance); // Function to handle F2 reset
343
344 // --- Forward Declaration for Window Procedure --- <<< Add this line HERE
345 LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);
346
347 // --- NEW Struct for AI Shot Evaluation ---
348 struct AIShotInfo {
349 bool possible = false; // Is this shot considered viable?
350 Ball* targetBall = nullptr; // Which ball to hit
351 int pocketIndex = -1; // Which pocket to aim for (0-5)
352 D2D1_POINT_2F ghostBallPos = { 0,0 }; // Where cue ball needs to hit target ball
353 float angle = 0.0f; // Calculated shot angle
354 float power = 0.0f; // Calculated shot power
355 float score = -1.0f; // Score for this shot (higher is better)
356 bool involves8Ball = false; // Is the target the 8-ball?
357 float spinX = 0.0f;
358 float spinY = 0.0f;
359 };
360
361 /*
362 table = TABLE_COLOR new: #29662d (0.1608, 0.4000, 0.1765) => old: (0.0f, 0.5f, 0.1f)
363 rail CUSHION_COLOR = #5c0702 (0.3608, 0.0275, 0.0078) => ::Red
364 gap = #e99d33 (0.9157, 0.6157, 0.2000) => ::Orange
365 winbg = #5e8863 (0.3686, 0.5333, 0.3882) => 1.0f, 1.0f, 0.803f
366 headstring = #47742f (0.2784, 0.4549, 0.1843) => ::White
367 bluearrow = #08b0a5 (0.0314, 0.6902, 0.6471) *#22babf (0.1333,0.7294,0.7490) => ::Blue
368 */
369
370 // --- NEW Settings Serialization Functions ---
371 void SaveSettings() {
372 std::ofstream outFile(SETTINGS_FILE_NAME);
373 if (outFile.is_open()) {
374 outFile << static_cast<int>(gameMode) << std::endl;
375 outFile << static_cast<int>(aiDifficulty) << std::endl;
376 outFile << static_cast<int>(openingBreakMode) << std::endl;
377 outFile.close();
378 }
379 // else: Handle error, e.g., log or silently fail
380 }
381
382 void LoadSettings() {
383 std::ifstream inFile(SETTINGS_FILE_NAME);
384 if (inFile.is_open()) {
385 int gm, aid, obm;
386 if (inFile >> gm) {
387 gameMode = static_cast<GameMode>(gm);
388 }
389 if (inFile >> aid) {
390 aiDifficulty = static_cast<AIDifficulty>(aid);
391 }
392 if (inFile >> obm) {
393 openingBreakMode = static_cast<OpeningBreakMode>(obm);
394 }
395 inFile.close();
396
397 // Validate loaded settings (optional, but good practice)
398 if (gameMode < HUMAN_VS_HUMAN || gameMode > HUMAN_VS_AI) gameMode = HUMAN_VS_HUMAN; // Default
399 if (aiDifficulty < EASY || aiDifficulty > HARD) aiDifficulty = MEDIUM; // Default
400 if (openingBreakMode < CPU_BREAK || openingBreakMode > FLIP_COIN_BREAK) openingBreakMode = CPU_BREAK; // Default
401 }
402 // else: File doesn't exist or couldn't be opened, use defaults (already set in global vars)
403 }
404 // --- End Settings Serialization Functions ---
405
406 // --- NEW Dialog Procedure ---
407 INT_PTR CALLBACK NewGameDialogProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam) {
408 switch (message) {
409 case WM_INITDIALOG:
410 {
411 // --- ACTION 4: Center Dialog Box ---
412 // Optional: Force centering if default isn't working
413 RECT rcDlg, rcOwner, rcScreen;
414 HWND hwndOwner = GetParent(hDlg); // GetParent(hDlg) might be better if hwndMain is passed
415 if (hwndOwner == NULL) hwndOwner = GetDesktopWindow();
416
417 GetWindowRect(hwndOwner, &rcOwner);
418 GetWindowRect(hDlg, &rcDlg);
419 CopyRect(&rcScreen, &rcOwner); // Use owner rect as reference bounds
420
421 // Offset the owner rect relative to the screen if it's not the desktop
422 if (GetParent(hDlg) != NULL) { // If parented to main window (passed to DialogBoxParam)
423 OffsetRect(&rcOwner, -rcScreen.left, -rcScreen.top);
424 OffsetRect(&rcDlg, -rcScreen.left, -rcScreen.top);
425 OffsetRect(&rcScreen, -rcScreen.left, -rcScreen.top);
426 }
427
428
429 // Calculate centered position
430 int x = rcOwner.left + (rcOwner.right - rcOwner.left - (rcDlg.right - rcDlg.left)) / 2;
431 int y = rcOwner.top + (rcOwner.bottom - rcOwner.top - (rcDlg.bottom - rcDlg.top)) / 2;
432
433 // Ensure it stays within screen bounds (optional safety)
434 x = std::max(static_cast<int>(rcScreen.left), x);
435 y = std::max(static_cast<int>(rcScreen.top), y);
436 if (x + (rcDlg.right - rcDlg.left) > rcScreen.right)
437 x = rcScreen.right - (rcDlg.right - rcDlg.left);
438 if (y + (rcDlg.bottom - rcDlg.top) > rcScreen.bottom)
439 y = rcScreen.bottom - (rcDlg.bottom - rcDlg.top);
440
441
442 // Set the dialog position
443 SetWindowPos(hDlg, HWND_TOP, x, y, 0, 0, SWP_NOSIZE);
444
445 // --- End Centering Code ---
446
447 // Set initial state based on current global settings (or defaults)
448 CheckRadioButton(hDlg, IDC_RADIO_2P, IDC_RADIO_CPU, (gameMode == HUMAN_VS_HUMAN) ? IDC_RADIO_2P : IDC_RADIO_CPU);
449
450 CheckRadioButton(hDlg, IDC_RADIO_EASY, IDC_RADIO_HARD,
451 (aiDifficulty == EASY) ? IDC_RADIO_EASY : ((aiDifficulty == MEDIUM) ? IDC_RADIO_MEDIUM : IDC_RADIO_HARD));
452
453 // Enable/Disable AI group based on initial mode
454 EnableWindow(GetDlgItem(hDlg, IDC_GROUP_AI), gameMode == HUMAN_VS_AI);
455 EnableWindow(GetDlgItem(hDlg, IDC_RADIO_EASY), gameMode == HUMAN_VS_AI);
456 EnableWindow(GetDlgItem(hDlg, IDC_RADIO_MEDIUM), gameMode == HUMAN_VS_AI);
457 EnableWindow(GetDlgItem(hDlg, IDC_RADIO_HARD), gameMode == HUMAN_VS_AI);
458 // Set initial state for Opening Break Mode
459 CheckRadioButton(hDlg, IDC_RADIO_CPU_BREAK, IDC_RADIO_FLIP_BREAK,
460 (openingBreakMode == CPU_BREAK) ? IDC_RADIO_CPU_BREAK : ((openingBreakMode == P1_BREAK) ? IDC_RADIO_P1_BREAK : IDC_RADIO_FLIP_BREAK));
461 // Enable/Disable Opening Break group based on initial mode
462 EnableWindow(GetDlgItem(hDlg, IDC_GROUP_BREAK_MODE), gameMode == HUMAN_VS_AI);
463 EnableWindow(GetDlgItem(hDlg, IDC_RADIO_CPU_BREAK), gameMode == HUMAN_VS_AI);
464 EnableWindow(GetDlgItem(hDlg, IDC_RADIO_P1_BREAK), gameMode == HUMAN_VS_AI);
465 EnableWindow(GetDlgItem(hDlg, IDC_RADIO_FLIP_BREAK), gameMode == HUMAN_VS_AI);
466 }
467 return (INT_PTR)TRUE;
468
469 case WM_COMMAND:
470 switch (LOWORD(wParam)) {
471 case IDC_RADIO_2P:
472 case IDC_RADIO_CPU:
473 {
474 bool isCPU = IsDlgButtonChecked(hDlg, IDC_RADIO_CPU) == BST_CHECKED;
475 // Enable/Disable AI group controls based on selection
476 EnableWindow(GetDlgItem(hDlg, IDC_GROUP_AI), isCPU);
477 EnableWindow(GetDlgItem(hDlg, IDC_RADIO_EASY), isCPU);
478 EnableWindow(GetDlgItem(hDlg, IDC_RADIO_MEDIUM), isCPU);
479 EnableWindow(GetDlgItem(hDlg, IDC_RADIO_HARD), isCPU);
480 // Also enable/disable Opening Break Mode group
481 EnableWindow(GetDlgItem(hDlg, IDC_GROUP_BREAK_MODE), isCPU);
482 EnableWindow(GetDlgItem(hDlg, IDC_RADIO_CPU_BREAK), isCPU);
483 EnableWindow(GetDlgItem(hDlg, IDC_RADIO_P1_BREAK), isCPU);
484 EnableWindow(GetDlgItem(hDlg, IDC_RADIO_FLIP_BREAK), isCPU);
485 }
486 return (INT_PTR)TRUE;
487
488 case IDOK:
489 // Retrieve selected options and store in global variables
490 if (IsDlgButtonChecked(hDlg, IDC_RADIO_CPU) == BST_CHECKED) {
491 gameMode = HUMAN_VS_AI;
492 if (IsDlgButtonChecked(hDlg, IDC_RADIO_EASY) == BST_CHECKED) aiDifficulty = EASY;
493 else if (IsDlgButtonChecked(hDlg, IDC_RADIO_MEDIUM) == BST_CHECKED) aiDifficulty = MEDIUM;
494 else if (IsDlgButtonChecked(hDlg, IDC_RADIO_HARD) == BST_CHECKED) aiDifficulty = HARD;
495
496 if (IsDlgButtonChecked(hDlg, IDC_RADIO_CPU_BREAK) == BST_CHECKED) openingBreakMode = CPU_BREAK;
497 else if (IsDlgButtonChecked(hDlg, IDC_RADIO_P1_BREAK) == BST_CHECKED) openingBreakMode = P1_BREAK;
498 else if (IsDlgButtonChecked(hDlg, IDC_RADIO_FLIP_BREAK) == BST_CHECKED) openingBreakMode = FLIP_COIN_BREAK;
499 }
500 else {
501 gameMode = HUMAN_VS_HUMAN;
502 // openingBreakMode doesn't apply to HvsH, can leave as is or reset
503 }
504 SaveSettings(); // Save settings when OK is pressed
505 EndDialog(hDlg, IDOK); // Close dialog, return IDOK
506 return (INT_PTR)TRUE;
507
508 case IDCANCEL: // Handle Cancel or closing the dialog
509 // Optionally, could reload settings here if you want cancel to revert to previously saved state
510 EndDialog(hDlg, IDCANCEL);
511 return (INT_PTR)TRUE;
512 }
513 break; // End WM_COMMAND
514 }
515 return (INT_PTR)FALSE; // Default processing
516 }
517
518 // --- NEW Helper to Show Dialog ---
519 void ShowNewGameDialog(HINSTANCE hInstance) {
520 if (DialogBoxParam(hInstance, MAKEINTRESOURCE(IDD_NEWGAMEDLG), hwndMain, NewGameDialogProc, 0) == IDOK) {
521 // User clicked Start, reset game with new settings
522 isPlayer2AI = (gameMode == HUMAN_VS_AI); // Update AI flag
523 if (isPlayer2AI) {
524 switch (aiDifficulty) {
525 case EASY: player2Info.name = L"Virtus Pro (Easy)"/*"CPU (Easy)"*/; break;
526 case MEDIUM: player2Info.name = L"Virtus Pro (Medium)"/*"CPU (Medium)"*/; break;
527 case HARD: player2Info.name = L"Virtus Pro (Hard)"/*"CPU (Hard)"*/; break;
528 }
529 }
530 else {
531 player2Info.name = L"Billy Ray Cyrus"/*"Player 2"*/;
532 }
533 // Update window title
534 std::wstring windowTitle = L"Midnight Pool 4"/*"Direct2D 8-Ball Pool"*/;
535 if (gameMode == HUMAN_VS_HUMAN) windowTitle += L" (Human vs Human)";
536 else windowTitle += L" (Human vs " + player2Info.name + L")";
537 SetWindowText(hwndMain, windowTitle.c_str());
538
539 InitGame(); // Re-initialize game logic & board
540 InvalidateRect(hwndMain, NULL, TRUE); // Force redraw
541 }
542 else {
543 // User cancelled dialog - maybe just resume game? Or exit?
544 // For simplicity, we do nothing, game continues as it was.
545 // To exit on cancel from F2, would need more complex state management.
546 }
547 }
548
549 // --- NEW Reset Game Function ---
550 void ResetGame(HINSTANCE hInstance) {
551 // Call the helper function to show the dialog and re-init if OK clicked
552 ShowNewGameDialog(hInstance);
553 }
554
555 // --- WinMain ---
556 int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, PWSTR, int nCmdShow) {
557 if (FAILED(CoInitialize(NULL))) {
558 MessageBox(NULL, L"COM Initialization Failed.", L"Error", MB_OK | MB_ICONERROR);
559 return -1;
560 }
561
562 // --- NEW: Load settings at startup ---
563 LoadSettings();
564
565 // --- NEW: Show configuration dialog FIRST ---
566 if (DialogBoxParam(hInstance, MAKEINTRESOURCE(IDD_NEWGAMEDLG), NULL, NewGameDialogProc, 0) != IDOK) {
567 // User cancelled the dialog
568 CoUninitialize();
569 return 0; // Exit gracefully if dialog cancelled
570 }
571 // Global gameMode and aiDifficulty are now set by the DialogProc
572
573 // Set AI flag based on game mode
574 isPlayer2AI = (gameMode == HUMAN_VS_AI);
575 if (isPlayer2AI) {
576 switch (aiDifficulty) {
577 case EASY: player2Info.name = L"Virtus Pro (Easy)"/*"CPU (Easy)"*/; break;
578 case MEDIUM:player2Info.name = L"Virtus Pro (Medium)"/*"CPU (Medium)"*/; break;
579 case HARD: player2Info.name = L"Virtus Pro (Hard)"/*"CPU (Hard)"*/; break;
580 }
581 }
582 else {
583 player2Info.name = L"Billy Ray Cyrus"/*"Player 2"*/;
584 }
585 // --- End of Dialog Logic ---
586
587
588 WNDCLASS wc = { };
589 wc.lpfnWndProc = WndProc;
590 wc.hInstance = hInstance;
591 wc.lpszClassName = L"BLISS_GameEngine"/*"Direct2D_8BallPool"*/;
592 wc.hCursor = LoadCursor(NULL, IDC_ARROW);
593 wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
594 wc.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_ICON1)); // Use your actual icon ID here
595
596 if (!RegisterClass(&wc)) {
597 MessageBox(NULL, L"Window Registration Failed.", L"Error", MB_OK | MB_ICONERROR);
598 CoUninitialize();
599 return -1;
600 }
601
602 // --- ACTION 4: Calculate Centered Window Position ---
603 const int WINDOW_WIDTH = 1000; // Define desired width
604 const int WINDOW_HEIGHT = 700; // Define desired height
605 int screenWidth = GetSystemMetrics(SM_CXSCREEN);
606 int screenHeight = GetSystemMetrics(SM_CYSCREEN);
607 int windowX = (screenWidth - WINDOW_WIDTH) / 2;
608 int windowY = (screenHeight - WINDOW_HEIGHT) / 2;
609
610 // --- Change Window Title based on mode ---
611 std::wstring windowTitle = L"Midnight Pool 4"/*"Direct2D 8-Ball Pool"*/;
612 if (gameMode == HUMAN_VS_HUMAN) windowTitle += L" (Human vs Human)";
613 else windowTitle += L" (Human vs " + player2Info.name + L")";
614
615 DWORD dwStyle = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX; // No WS_THICKFRAME, No WS_MAXIMIZEBOX
616
617 hwndMain = CreateWindowEx(
618 0, L"BLISS_GameEngine"/*"Direct2D_8BallPool"*/, windowTitle.c_str(), dwStyle,
619 windowX, windowY, WINDOW_WIDTH, WINDOW_HEIGHT,
620 NULL, NULL, hInstance, NULL
621 );
622
623 if (!hwndMain) {
624 MessageBox(NULL, L"Window Creation Failed.", L"Error", MB_OK | MB_ICONERROR);
625 CoUninitialize();
626 return -1;
627 }
628
629 // Initialize Direct2D Resources AFTER window creation
630 if (FAILED(CreateDeviceResources())) {
631 MessageBox(NULL, L"Failed to create Direct2D resources.", L"Error", MB_OK | MB_ICONERROR);
632 DestroyWindow(hwndMain);
633 CoUninitialize();
634 return -1;
635 }
636
637 InitGame(); // Initialize game state AFTER resources are ready & mode is set
638 Sleep(500); // Allow window to fully initialize before starting the countdown //midi func
639 StartMidi(hwndMain, TEXT("BSQ.MID")); // Replace with your MIDI filename
640 //PlayGameMusic(hwndMain); //midi func
641
642 ShowWindow(hwndMain, nCmdShow);
643 UpdateWindow(hwndMain);
644
645 if (!SetTimer(hwndMain, ID_TIMER, 1000 / TARGET_FPS, NULL)) {
646 MessageBox(NULL, L"Could not SetTimer().", L"Error", MB_OK | MB_ICONERROR);
647 DestroyWindow(hwndMain);
648 CoUninitialize();
649 return -1;
650 }
651
652 MSG msg = { };
653 // --- Modified Main Loop ---
654 // Handles the case where the game starts in SHOWING_DIALOG state (handled now before loop)
655 // or gets reset to it via F2. The main loop runs normally once game starts.
656 while (GetMessage(&msg, NULL, 0, 0)) {
657 // We might need modeless dialog handling here if F2 shows dialog
658 // while window is active, but DialogBoxParam is modal.
659 // Let's assume F2 hides main window, shows dialog, then restarts game loop.
660 // Simpler: F2 calls ResetGame which calls DialogBoxParam (modal) then InitGame.
661 TranslateMessage(&msg);
662 DispatchMessage(&msg);
663 }
664
665
666 KillTimer(hwndMain, ID_TIMER);
667 DiscardDeviceResources();
668 SaveSettings(); // Save settings on exit
669 CoUninitialize();
670
671 return (int)msg.wParam;
672 }
673
674 // --- WndProc ---
675 LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
676 // Declare cueBall pointer once at the top, used in multiple cases
677 // For clarity, often better to declare within each case where needed.
678 Ball* cueBall = nullptr; // Initialize to nullptr
679 switch (msg) {
680 case WM_CREATE:
681 // Resources are now created in WinMain after CreateWindowEx
682 return 0;
683
684 case WM_PAINT:
685 OnPaint();
686 // Validate the entire window region after painting
687 ValidateRect(hwnd, NULL);
688 return 0;
689
690 case WM_SIZE: {
691 UINT width = LOWORD(lParam);
692 UINT height = HIWORD(lParam);
693 OnResize(width, height);
694 return 0;
695 }
696
697 case WM_TIMER:
698 if (wParam == ID_TIMER) {
699 GameUpdate(); // Update game logic and physics
700 InvalidateRect(hwnd, NULL, FALSE); // Request redraw
701 }
702 return 0;
703
704 // --- NEW: Handle F2 Key for Reset ---
705 // --- MODIFIED: Handle More Keys ---
706 case WM_KEYDOWN:
707 { // Add scope for variable declarations
708
709 // --- FIX: Get Cue Ball pointer for this scope ---
710 cueBall = GetCueBall();
711 // We might allow some keys even if cue ball is gone (like F1/F2), but actions need it
712 // --- End Fix ---
713
714 // Check which player can interact via keyboard (Humans only)
715 bool canPlayerControl = ((currentPlayer == 1 && (currentGameState == PLAYER1_TURN || currentGameState == AIMING || currentGameState == BREAKING || currentGameState == BALL_IN_HAND_P1 || currentGameState == PRE_BREAK_PLACEMENT)) ||
716 (currentPlayer == 2 && !isPlayer2AI && (currentGameState == PLAYER2_TURN || currentGameState == AIMING || currentGameState == BREAKING || currentGameState == BALL_IN_HAND_P2 || currentGameState == PRE_BREAK_PLACEMENT)));
717
718 // --- F1 / F2 Keys (Always available) ---
719 if (wParam == VK_F2) {
720 HINSTANCE hInstance = (HINSTANCE)GetWindowLongPtr(hwnd, GWLP_HINSTANCE);
721 ResetGame(hInstance); // Call reset function
722 return 0; // Indicate key was processed
723 }
724 else if (wParam == VK_F1) {
725 MessageBox(hwnd,
726 L"Direct2D-based StickPool game made in C++ from scratch (4827+ lines of code)\n" // Update line count if needed {2764+ lines}
727 L"First successful Clone in C++ (no other sites or projects were there to glean from.) Made /w AI assist\n"
728 L"(others were in JS/ non-8-Ball in C# etc.) w/o OOP and Graphics Frameworks all in a Single file.\n"
729 L"Copyright (C) 2025 Evans Thorpemorton, Entisoft Solutions.\n"
730 L"Includes AI Difficulty Modes, Aim-Trajectory For Table Rails + Hard Angles TipShots. || F2=New Game",
731 L"About This Game", MB_OK | MB_ICONINFORMATION);
732 return 0; // Indicate key was processed
733 }
734
735 // Check for 'M' key (uppercase or lowercase)
736 // Toggle music with "M"
737 if (wParam == 'M' || wParam == 'm') {
738 //static bool isMusicPlaying = false;
739 if (isMusicPlaying) {
740 // Stop the music
741 StopMidi();
742 isMusicPlaying = false;
743 }
744 else {
745 // Build the MIDI file path
746 TCHAR midiPath[MAX_PATH];
747 GetModuleFileName(NULL, midiPath, MAX_PATH);
748 // Keep only the directory part
749 TCHAR* lastBackslash = _tcsrchr(midiPath, '\\');
750 if (lastBackslash != NULL) {
751 *(lastBackslash + 1) = '\0';
752 }
753 // Append the MIDI filename
754 _tcscat_s(midiPath, MAX_PATH, TEXT("BSQ.MID")); // Adjust filename if needed
755
756 // Start playing MIDI
757 StartMidi(hwndMain, midiPath);
758 isMusicPlaying = true;
759 }
760 }
761
762
763 // --- Player Interaction Keys (Only if allowed) ---
764 if (canPlayerControl) {
765 // --- Get Shift Key State ---
766 bool shiftPressed = (GetKeyState(VK_SHIFT) & 0x8000) != 0;
767 float angleStep = shiftPressed ? 0.05f : 0.01f; // Base step / Faster step (Adjust as needed) // Multiplier was 5x
768 float powerStep = 0.2f; // Power step (Adjust as needed)
769
770 switch (wParam) {
771 case VK_LEFT: // Rotate Cue Stick Counter-Clockwise
772 if (currentGameState != SHOT_IN_PROGRESS && currentGameState != AI_THINKING) {
773 cueAngle -= angleStep;
774 // Normalize angle (keep between 0 and 2*PI)
775 if (cueAngle < 0) cueAngle += 2 * PI;
776 // Ensure state shows aiming visuals if turn just started
777 if (currentGameState == PLAYER1_TURN || currentGameState == PLAYER2_TURN) currentGameState = AIMING;
778 isAiming = false; // Keyboard adjust doesn't use mouse aiming state
779 isDraggingStick = false;
780 keyboardAimingActive = true;
781 }
782 break;
783
784 case VK_RIGHT: // Rotate Cue Stick Clockwise
785 if (currentGameState != SHOT_IN_PROGRESS && currentGameState != AI_THINKING) {
786 cueAngle += angleStep;
787 // Normalize angle (keep between 0 and 2*PI)
788 if (cueAngle >= 2 * PI) cueAngle -= 2 * PI;
789 // Ensure state shows aiming visuals if turn just started
790 if (currentGameState == PLAYER1_TURN || currentGameState == PLAYER2_TURN) currentGameState = AIMING;
791 isAiming = false;
792 isDraggingStick = false;
793 keyboardAimingActive = true;
794 }
795 break;
796
797 case VK_UP: // Decrease Shot Power
798 if (currentGameState != SHOT_IN_PROGRESS && currentGameState != AI_THINKING) {
799 shotPower -= powerStep;
800 if (shotPower < 0.0f) shotPower = 0.0f;
801 // Ensure state shows aiming visuals if turn just started
802 if (currentGameState == PLAYER1_TURN || currentGameState == PLAYER2_TURN) currentGameState = AIMING;
803 isAiming = true; // Keyboard adjust doesn't use mouse aiming state
804 isDraggingStick = false;
805 keyboardAimingActive = true;
806 }
807 break;
808
809 case VK_DOWN: // Increase Shot Power
810 if (currentGameState != SHOT_IN_PROGRESS && currentGameState != AI_THINKING) {
811 shotPower += powerStep;
812 if (shotPower > MAX_SHOT_POWER) shotPower = MAX_SHOT_POWER;
813 // Ensure state shows aiming visuals if turn just started
814 if (currentGameState == PLAYER1_TURN || currentGameState == PLAYER2_TURN) currentGameState = AIMING;
815 isAiming = true;
816 isDraggingStick = false;
817 keyboardAimingActive = true;
818 }
819 break;
820
821 case VK_SPACE: // Trigger Shot
822 if ((currentGameState == AIMING || currentGameState == BREAKING || currentGameState == PLAYER1_TURN || currentGameState == PLAYER2_TURN)
823 && currentGameState != SHOT_IN_PROGRESS && currentGameState != AI_THINKING)
824 {
825 if (shotPower > 0.15f) { // Use same threshold as mouse
826 // Reset foul flags BEFORE applying shot
827 firstHitBallIdThisShot = -1;
828 cueHitObjectBallThisShot = false;
829 railHitAfterContact = false;
830
831 // Play sound & Apply Shot
832 std::thread([](const TCHAR* soundName) { PlaySound(soundName, NULL, SND_FILENAME | SND_NODEFAULT); }, TEXT("cue.wav")).detach();
833 ApplyShot(shotPower, cueAngle, cueSpinX, cueSpinY);
834
835 // Update State
836 currentGameState = SHOT_IN_PROGRESS;
837 foulCommitted = false;
838 pocketedThisTurn.clear();
839 shotPower = 0; // Reset power after shooting
840 isAiming = false; isDraggingStick = false; // Reset aiming flags
841 keyboardAimingActive = false;
842 }
843 }
844 break;
845
846 case VK_ESCAPE: // Cancel Aim/Shot Setup
847 if ((currentGameState == AIMING || currentGameState == BREAKING) || shotPower > 0)
848 {
849 shotPower = 0.0f;
850 isAiming = false;
851 isDraggingStick = false;
852 keyboardAimingActive = false;
853 // Revert to basic turn state if not breaking
854 if (currentGameState != BREAKING) {
855 currentGameState = (currentPlayer == 1) ? PLAYER1_TURN : PLAYER2_TURN;
856 }
857 //if (currentPlayer == 1) calledPocketP1 = -1;
858 //else calledPocketP2 = -1;
859 }
860 break;
861
862 case 'G': // Toggle Cheat Mode
863 cheatModeEnabled = !cheatModeEnabled;
864 if (cheatModeEnabled)
865 MessageBeep(MB_ICONEXCLAMATION); // Play a beep when enabling
866 else
867 MessageBeep(MB_OK); // Play a different beep when disabling
868 break;
869
870 default:
871 // Allow default processing for other keys if needed
872 // return DefWindowProc(hwnd, msg, wParam, lParam); // Usually not needed for WM_KEYDOWN
873 break;
874 } // End switch(wParam) for player controls
875 return 0; // Indicate player control key was processed
876 } // End if(canPlayerControl)
877 } // End scope for WM_KEYDOWN case
878 // If key wasn't F1/F2 and player couldn't control, maybe allow default processing?
879 // return DefWindowProc(hwnd, msg, wParam, lParam); // Or just return 0
880 return 0;
881
882 case WM_MOUSEMOVE: {
883 ptMouse.x = LOWORD(lParam);
884 ptMouse.y = HIWORD(lParam);
885
886 // --- NEW LOGIC: Handle Pocket Hover ---
887 if ((currentGameState == CHOOSING_POCKET_P1 && currentPlayer == 1) ||
888 (currentGameState == CHOOSING_POCKET_P2 && currentPlayer == 2 && !isPlayer2AI)) {
889 int oldHover = currentlyHoveredPocket;
890 currentlyHoveredPocket = -1; // Reset
891 for (int i = 0; i < 6; ++i) {
892 if (GetDistanceSq((float)ptMouse.x, (float)ptMouse.y, pocketPositions[i].x, pocketPositions[i].y) < HOLE_VISUAL_RADIUS * HOLE_VISUAL_RADIUS * 2.25f) {
893 currentlyHoveredPocket = i;
894 break;
895 }
896 }
897 if (oldHover != currentlyHoveredPocket) {
898 InvalidateRect(hwnd, NULL, FALSE);
899 }
900 // Do NOT return 0 here, allow normal mouse angle update to continue
901 }
902 // --- END NEW LOGIC ---
903
904
905 cueBall = GetCueBall(); // Declare and get cueBall pointer
906
907 if (isDraggingCueBall && cheatModeEnabled && draggingBallId != -1) {
908 Ball* ball = GetBallById(draggingBallId);
909 if (ball) {
910 ball->x = (float)ptMouse.x;
911 ball->y = (float)ptMouse.y;
912 ball->vx = ball->vy = 0.0f;
913 }
914 return 0;
915 }
916
917 if (!cueBall) return 0;
918
919 // Update Aiming Logic (Check player turn)
920 if (isDraggingCueBall &&
921 ((currentPlayer == 1 && currentGameState == BALL_IN_HAND_P1) ||
922 (!isPlayer2AI && currentPlayer == 2 && currentGameState == BALL_IN_HAND_P2) ||
923 currentGameState == PRE_BREAK_PLACEMENT))
924 {
925 bool behindHeadstring = (currentGameState == PRE_BREAK_PLACEMENT);
926 // Tentative position update
927 cueBall->x = (float)ptMouse.x;
928 cueBall->y = (float)ptMouse.y;
929 cueBall->vx = cueBall->vy = 0;
930 }
931 else if ((isAiming || isDraggingStick) &&
932 ((currentPlayer == 1 && (currentGameState == AIMING || currentGameState == BREAKING)) ||
933 (!isPlayer2AI && currentPlayer == 2 && (currentGameState == AIMING || currentGameState == BREAKING))))
934 {
935 //NEW2 MOUSEBOUND CODE = START
936 /*// Clamp mouse inside table bounds during aiming
937 if (ptMouse.x < TABLE_LEFT) ptMouse.x = TABLE_LEFT;
938 if (ptMouse.x > TABLE_RIGHT) ptMouse.x = TABLE_RIGHT;
939 if (ptMouse.y < TABLE_TOP) ptMouse.y = TABLE_TOP;
940 if (ptMouse.y > TABLE_BOTTOM) ptMouse.y = TABLE_BOTTOM;*/
941 //NEW2 MOUSEBOUND CODE = END
942 // Aiming drag updates angle and power
943 float dx = (float)ptMouse.x - cueBall->x;
944 float dy = (float)ptMouse.y - cueBall->y;
945 if (dx != 0 || dy != 0) cueAngle = atan2f(dy, dx);
946 //float pullDist = GetDistance((float)ptMouse.x, (float)ptMouse.y, aimStartPoint.x, aimStartPoint.y);
947 //shotPower = std::min(pullDist / 10.0f, MAX_SHOT_POWER);
948 if (!keyboardAimingActive) { // Only update shotPower if NOT keyboard aiming
949 float pullDist = GetDistance((float)ptMouse.x, (float)ptMouse.y, aimStartPoint.x, aimStartPoint.y);
950 shotPower = std::min(pullDist / 10.0f, MAX_SHOT_POWER);
951 }
952 }
953 else if (isSettingEnglish &&
954 ((currentPlayer == 1 && (currentGameState == PLAYER1_TURN || currentGameState == AIMING || currentGameState == BREAKING)) ||
955 (!isPlayer2AI && currentPlayer == 2 && (currentGameState == PLAYER2_TURN || currentGameState == AIMING || currentGameState == BREAKING))))
956 {
957 // Setting English
958 float dx = (float)ptMouse.x - spinIndicatorCenter.x;
959 float dy = (float)ptMouse.y - spinIndicatorCenter.y;
960 float dist = GetDistance(dx, dy, 0, 0);
961 if (dist > spinIndicatorRadius) { dx *= spinIndicatorRadius / dist; dy *= spinIndicatorRadius / dist; }
962 cueSpinX = dx / spinIndicatorRadius;
963 cueSpinY = dy / spinIndicatorRadius;
964 }
965 else {
966 //DISABLE PERM AIMING = START
967 /*// Update visual angle even when not aiming/dragging (Check player turn)
968 bool canUpdateVisualAngle = ((currentPlayer == 1 && (currentGameState == PLAYER1_TURN || currentGameState == BALL_IN_HAND_P1)) ||
969 (currentPlayer == 2 && !isPlayer2AI && (currentGameState == PLAYER2_TURN || currentGameState == BALL_IN_HAND_P2)) ||
970 currentGameState == PRE_BREAK_PLACEMENT || currentGameState == BREAKING || currentGameState == AIMING);
971
972 if (canUpdateVisualAngle && !isDraggingCueBall && !isAiming && !isDraggingStick && !keyboardAimingActive) // NEW: Prevent mouse override if keyboard aiming
973 {
974 // NEW MOUSEBOUND CODE = START
975 // Only update cue angle if mouse is inside the playable table area
976 if (ptMouse.x >= TABLE_LEFT && ptMouse.x <= TABLE_RIGHT &&
977 ptMouse.y >= TABLE_TOP && ptMouse.y <= TABLE_BOTTOM)
978 {
979 // NEW MOUSEBOUND CODE = END
980 Ball* cb = cueBall; // Use function-scope cueBall // Already got cueBall above
981 if (cb) {
982 float dx = (float)ptMouse.x - cb->x;
983 float dy = (float)ptMouse.y - cb->y;
984 if (dx != 0 || dy != 0) cueAngle = atan2f(dy, dx);
985 }
986 } //NEW MOUSEBOUND CODE LINE = DISABLE
987 }*/
988 //DISABLE PERM AIMING = END
989 }
990 return 0;
991 } // End WM_MOUSEMOVE
992
993 case WM_LBUTTONDOWN: {
994 ptMouse.x = LOWORD(lParam);
995 ptMouse.y = HIWORD(lParam);
996
997 // --- FOOLPROOF FIX: This block implements the two-stage pocket selection ---
998 if ((currentGameState == CHOOSING_POCKET_P1 && currentPlayer == 1) ||
999 (currentGameState == CHOOSING_POCKET_P2 && currentPlayer == 2 && !isPlayer2AI)) {
1000
1001 int clickedPocketIndex = -1;
1002 // STAGE 1, STEP 1: Check if the click was on any of the 6 pockets
1003 for (int i = 0; i < 6; ++i) {
1004 if (GetDistanceSq((float)ptMouse.x, (float)ptMouse.y, pocketPositions[i].x, pocketPositions[i].y) < HOLE_VISUAL_RADIUS * HOLE_VISUAL_RADIUS * 2.25f) {
1005 clickedPocketIndex = i;
1006 break;
1007 }
1008 }
1009
1010 if (clickedPocketIndex != -1) {
1011 // STAGE 1, STEP 2: Player clicked on a pocket. Update the choice.
1012 // We DO NOT change the game state here. This allows re-selection.
1013 if (currentPlayer == 1) calledPocketP1 = clickedPocketIndex;
1014 else calledPocketP2 = clickedPocketIndex;
1015 InvalidateRect(hwnd, NULL, FALSE); // Redraw to show the arrow has moved.
1016 return 0; // Consume the click and stay in CHOOSING_POCKET state.
1017 }
1018
1019 // STAGE 2, STEP 1: Check if the player is clicking the cue ball to confirm.
1020 Ball* cueBall = GetCueBall();
1021 int calledPocket = (currentPlayer == 1) ? calledPocketP1 : calledPocketP2;
1022 if (cueBall && calledPocket != -1 && GetDistanceSq(cueBall->x, cueBall->y, (float)ptMouse.x, (float)ptMouse.y) < BALL_RADIUS * BALL_RADIUS * 25) {
1023 // STAGE 2, STEP 2: A pocket has been selected, and the player now clicks the cue ball.
1024 // NOW we transition to the normal aiming state.
1025 currentGameState = AIMING; // Go to a generic aiming state.
1026 pocketCallMessage = L""; // Clear the "Choose a pocket..." message.
1027 isAiming = true; // Prepare for aiming.
1028 aimStartPoint = D2D1::Point2F((float)ptMouse.x, (float)ptMouse.y); // Use your existing aim start variable.
1029 return 0;
1030 }
1031
1032 // If they click anywhere else (not a pocket, not the cue ball), do nothing.
1033 return 0;
1034 }
1035
1036 /*// --- FOOLPROOF FIX: This block handles re-selectable pocket choice ---
1037 if ((currentGameState == CHOOSING_POCKET_P1 && currentPlayer == 1) ||
1038 (currentGameState == CHOOSING_POCKET_P2 && currentPlayer == 2 && !isPlayer2AI)) {
1039
1040 int clickedPocketIndex = -1;
1041 // Check if the click was on any of the 6 pockets
1042 for (int i = 0; i < 6; ++i) {
1043 if (GetDistanceSq((float)ptMouse.x, (float)ptMouse.y, pocketPositions[i].x, pocketPositions[i].y) < HOLE_VISUAL_RADIUS * HOLE_VISUAL_RADIUS * 2.25f) {
1044 clickedPocketIndex = i;
1045 break;
1046 }
1047 }
1048
1049 if (clickedPocketIndex != -1) { // Player clicked on a pocket
1050 // FIX: Update the called pocket, but DO NOT change the game state.
1051 // This allows the player to click another pocket to change their mind.
1052 if (currentPlayer == 1) calledPocketP1 = clickedPocketIndex;
1053 else calledPocketP2 = clickedPocketIndex;
1054 InvalidateRect(hwnd, NULL, FALSE); // Redraw to show updated arrow
1055 return 0; // Consume the click and stay in CHOOSING_POCKET state
1056 }
1057
1058 // FIX: Add new logic to CONFIRM the choice by clicking the cue ball.
1059 Ball* cueBall = GetCueBall();
1060 int calledPocket = (currentPlayer == 1) ? calledPocketP1 : calledPocketP2;
1061 if (cueBall && calledPocket != -1 && GetDistanceSq(cueBall->x, cueBall->y, (float)ptMouse.x, (float)ptMouse.y) < BALL_RADIUS * BALL_RADIUS * 25) {
1062 // A pocket has been selected, and the player now clicks the cue ball.
1063 // NOW we transition to the normal aiming state.
1064 currentGameState = AIMING; // Go to aiming, not PLAYER1_TURN
1065 pocketCallMessage = L""; // Clear the "Choose a pocket..." message
1066 isAiming = true; // Prepare for aiming
1067 aimStartPoint = D2D1::Point2F((float)ptMouse.x, (float)ptMouse.y);
1068 return 0;
1069 }
1070
1071 // If they click anywhere else (not a pocket, not the cue ball), do nothing.
1072 return 0;
1073 }*/
1074
1075 /*// --- handle pocket re-selection when choosing 8-ball pocket ---
1076 if ((currentGameState == CHOOSING_POCKET_P1 && currentPlayer == 1)
1077 || (currentGameState == CHOOSING_POCKET_P2 && currentPlayer == 2 && !isPlayer2AI))
1078 {
1079 POINT pt = { LOWORD(lParam), HIWORD(lParam) };
1080 for (int i = 0; i < 6; ++i) {
1081 float dx = pt.x - pocketPositions[i].x;
1082 float dy = pt.y - pocketPositions[i].y;
1083 if (dx * dx + dy * dy <= POCKET_RADIUS * POCKET_RADIUS) {
1084 // 1) Record the call
1085 if (currentPlayer == 1) calledPocketP1 = i;
1086 else calledPocketP2 = i;
1087 // 2) Clear any prompt text
1088 pocketCallMessage.clear();
1089 // 3) Return to normal aiming state
1090 currentGameState = (currentPlayer == 1) ? PLAYER1_TURN : PLAYER2_TURN;
1091 // 4) Redraw (arrow stays because calledPocketP* >= 0)
1092 InvalidateRect(hwnd, NULL, FALSE);
1093 return 0; // consume click
1094 }
1095 }
1096 return 0; // clicked outside ? stay in pocket?call until a valid pocket is chosen
1097 }*/
1098
1099 // … rest of your click?to?aim logic …
1100
1101 //replaced /w new code
1102 /*
1103 // --- FIX: Add this entire block at the top of WM_LBUTTONDOWN ---
1104 // This handles input specifically for the pocket selection state.
1105 if ((currentGameState == CHOOSING_POCKET_P1 && currentPlayer == 1) ||
1106 (currentGameState == CHOOSING_POCKET_P2 && currentPlayer == 2 && !isPlayer2AI)) {
1107
1108 int clickedPocketIndex = -1;
1109 // Check if the click was on any of the 6 pockets
1110 for (int i = 0; i < 6; ++i) {
1111 if (GetDistanceSq((float)ptMouse.x, (float)ptMouse.y, pocketPositions[i].x, pocketPositions[i].y) < HOLE_VISUAL_RADIUS * HOLE_VISUAL_RADIUS * 2.25f) {
1112 clickedPocketIndex = i;
1113 break;
1114 }
1115 }
1116
1117 if (clickedPocketIndex != -1) {
1118 // A pocket was clicked. Update the selection but STAY in the choosing state.
1119 // This allows the player to click another pocket to change their mind.
1120 if (currentPlayer == 1) calledPocketP1 = clickedPocketIndex;
1121 else calledPocketP2 = clickedPocketIndex;
1122 InvalidateRect(hwnd, NULL, FALSE); // Redraw to show the arrow has moved.
1123 return 0; // Consume the click and wait for the next action.
1124 }
1125
1126 // If the player clicks the CUE BALL, that confirms their pocket selection.
1127 Ball* cueBall = GetCueBall();
1128 int calledPocket = (currentPlayer == 1) ? calledPocketP1 : calledPocketP2;
1129 if (cueBall && calledPocket != -1 && GetDistanceSq(cueBall->x, cueBall->y, (float)ptMouse.x, (float)ptMouse.y) < BALL_RADIUS * BALL_RADIUS * 25) {
1130 // A pocket has been selected, and the player now clicks the cue ball.
1131 // NOW we transition to the normal aiming state.
1132 currentGameState = (currentPlayer == 1) ? PLAYER1_TURN : PLAYER2_TURN;
1133 pocketCallMessage = L""; // Clear the "Choose a pocket..." message
1134 isAiming = true; // Prepare for aiming
1135 aimStartPoint = D2D1::Point2F((float)ptMouse.x, (float)ptMouse.y); // Use your existing aim start variable
1136 return 0;
1137 }
1138
1139 // If they click anywhere else (not a pocket, not the cue ball), do nothing.
1140 return 0;
1141 }
1142 // --- END OF THE NEW BLOCK ---
1143 */
1144 //new code ends here
1145
1146 if (cheatModeEnabled) {
1147 // Allow dragging any ball freely
1148 for (Ball& ball : balls) {
1149 float distSq = GetDistanceSq(ball.x, ball.y, (float)ptMouse.x, (float)ptMouse.y);
1150 if (distSq <= BALL_RADIUS * BALL_RADIUS * 4) { // Click near ball
1151 isDraggingCueBall = true;
1152 draggingBallId = ball.id;
1153 if (ball.id == 0) {
1154 // If dragging cue ball manually, ensure we stay in Ball-In-Hand state
1155 if (currentPlayer == 1)
1156 currentGameState = BALL_IN_HAND_P1;
1157 else if (currentPlayer == 2 && !isPlayer2AI)
1158 currentGameState = BALL_IN_HAND_P2;
1159 }
1160 return 0;
1161 }
1162 }
1163 }
1164
1165 Ball* cueBall = GetCueBall(); // Declare and get cueBall pointer
1166
1167 // Check which player is allowed to interact via mouse click
1168 bool canPlayerClickInteract = ((currentPlayer == 1) || (currentPlayer == 2 && !isPlayer2AI));
1169 // Define states where interaction is generally allowed
1170 bool canInteractState = (currentGameState == PLAYER1_TURN || currentGameState == PLAYER2_TURN ||
1171 currentGameState == AIMING || currentGameState == BREAKING ||
1172 currentGameState == BALL_IN_HAND_P1 || currentGameState == BALL_IN_HAND_P2 ||
1173 currentGameState == PRE_BREAK_PLACEMENT);
1174
1175 // Check Spin Indicator first (Allow if player's turn/aim phase)
1176 if (canPlayerClickInteract && canInteractState) {
1177 float spinDistSq = GetDistanceSq((float)ptMouse.x, (float)ptMouse.y, spinIndicatorCenter.x, spinIndicatorCenter.y);
1178 if (spinDistSq < spinIndicatorRadius * spinIndicatorRadius * 1.2f) {
1179 isSettingEnglish = true;
1180 float dx = (float)ptMouse.x - spinIndicatorCenter.x;
1181 float dy = (float)ptMouse.y - spinIndicatorCenter.y;
1182 float dist = GetDistance(dx, dy, 0, 0);
1183 if (dist > spinIndicatorRadius) { dx *= spinIndicatorRadius / dist; dy *= spinIndicatorRadius / dist; }
1184 cueSpinX = dx / spinIndicatorRadius;
1185 cueSpinY = dy / spinIndicatorRadius;
1186 isAiming = false; isDraggingStick = false; isDraggingCueBall = false;
1187 return 0;
1188 }
1189 }
1190
1191 if (!cueBall) return 0;
1192
1193 // Check Ball-in-Hand placement/drag
1194 bool isPlacingBall = (currentGameState == BALL_IN_HAND_P1 || currentGameState == BALL_IN_HAND_P2 || currentGameState == PRE_BREAK_PLACEMENT);
1195 bool isPlayerAllowedToPlace = (isPlacingBall &&
1196 ((currentPlayer == 1 && currentGameState == BALL_IN_HAND_P1) ||
1197 (currentPlayer == 2 && !isPlayer2AI && currentGameState == BALL_IN_HAND_P2) ||
1198 (currentGameState == PRE_BREAK_PLACEMENT))); // Allow current player in break setup
1199
1200 if (isPlayerAllowedToPlace) {
1201 float distSq = GetDistanceSq(cueBall->x, cueBall->y, (float)ptMouse.x, (float)ptMouse.y);
1202 if (distSq < BALL_RADIUS * BALL_RADIUS * 9.0f) {
1203 isDraggingCueBall = true;
1204 isAiming = false; isDraggingStick = false;
1205 }
1206 else {
1207 bool behindHeadstring = (currentGameState == PRE_BREAK_PLACEMENT);
1208 if (IsValidCueBallPosition((float)ptMouse.x, (float)ptMouse.y, behindHeadstring)) {
1209 cueBall->x = (float)ptMouse.x; cueBall->y = (float)ptMouse.y;
1210 cueBall->vx = 0; cueBall->vy = 0;
1211 isDraggingCueBall = false;
1212 // Transition state
1213 if (currentGameState == PRE_BREAK_PLACEMENT) currentGameState = BREAKING;
1214 else if (currentGameState == BALL_IN_HAND_P1) currentGameState = PLAYER1_TURN;
1215 else if (currentGameState == BALL_IN_HAND_P2) currentGameState = PLAYER2_TURN;
1216 cueAngle = 0.0f;
1217 }
1218 }
1219 return 0;
1220 }
1221
1222 // Check for starting Aim (Cue Ball OR Stick)
1223 bool canAim = ((currentPlayer == 1 && (currentGameState == PLAYER1_TURN || currentGameState == BREAKING)) ||
1224 (currentPlayer == 2 && !isPlayer2AI && (currentGameState == PLAYER2_TURN || currentGameState == BREAKING)));
1225
1226 if (canAim) {
1227 const float stickDrawLength = 150.0f * 1.4f;
1228 float currentStickAngle = cueAngle + PI;
1229 D2D1_POINT_2F currentStickEnd = D2D1::Point2F(cueBall->x + cosf(currentStickAngle) * stickDrawLength, cueBall->y + sinf(currentStickAngle) * stickDrawLength);
1230 D2D1_POINT_2F currentStickTip = D2D1::Point2F(cueBall->x + cosf(currentStickAngle) * 5.0f, cueBall->y + sinf(currentStickAngle) * 5.0f);
1231 float distToStickSq = PointToLineSegmentDistanceSq(D2D1::Point2F((float)ptMouse.x, (float)ptMouse.y), currentStickTip, currentStickEnd);
1232 float stickClickThresholdSq = 36.0f;
1233 float distToCueBallSq = GetDistanceSq(cueBall->x, cueBall->y, (float)ptMouse.x, (float)ptMouse.y);
1234 float cueBallClickRadiusSq = BALL_RADIUS * BALL_RADIUS * 25;
1235
1236 bool clickedStick = (distToStickSq < stickClickThresholdSq);
1237 bool clickedCueArea = (distToCueBallSq < cueBallClickRadiusSq);
1238
1239 if (clickedStick || clickedCueArea) {
1240 isDraggingStick = clickedStick && !clickedCueArea;
1241 isAiming = clickedCueArea;
1242 aimStartPoint = D2D1::Point2F((float)ptMouse.x, (float)ptMouse.y);
1243 shotPower = 0;
1244 float dx = (float)ptMouse.x - cueBall->x;
1245 float dy = (float)ptMouse.y - cueBall->y;
1246 if (dx != 0 || dy != 0) cueAngle = atan2f(dy, dx);
1247 if (currentGameState != BREAKING) currentGameState = AIMING;
1248 }
1249 }
1250 return 0;
1251 } // End WM_LBUTTONDOWN
1252
1253
1254 case WM_LBUTTONUP: {
1255 // --- FOOLPROOF FIX for Cheat Mode Scoring ---
1256 if (cheatModeEnabled && draggingBallId != -1) {
1257 Ball* b = GetBallById(draggingBallId);
1258 if (b) {
1259 for (int p = 0; p < 6; ++p) {
1260 float dx = b->x - pocketPositions[p].x;
1261 float dy = b->y - pocketPositions[p].y;
1262 if (dx * dx + dy * dy <= POCKET_RADIUS * POCKET_RADIUS) {
1263 // --- This is the new, "smarter" logic ---
1264 b->isPocketed = true; // Pocket the ball visually.
1265
1266 // If the table is open, assign types based on this cheated ball.
1267 if (player1Info.assignedType == BallType::NONE && b->id != 0 && b->id != 8) {
1268 AssignPlayerBallTypes(b->type, false);
1269 }
1270
1271 // Now, correctly update the score for the right player.
1272 if (b->id != 0 && b->id != 8) {
1273 if (b->type == player1Info.assignedType) {
1274 player1Info.ballsPocketedCount++;
1275 }
1276 else if (b->type == player2Info.assignedType) {
1277 player2Info.ballsPocketedCount++;
1278 }
1279 }
1280 break; // Stop checking pockets.
1281 }
1282 }
1283 }
1284 }
1285
1286 /*if (cheatModeEnabled && draggingBallId != -1) {
1287 Ball* b = GetBallById(draggingBallId);
1288 if (b) {
1289 for (int p = 0; p < 6; ++p) {
1290 float dx = b->x - pocketPositions[p].x;
1291 float dy = b->y - pocketPositions[p].y;
1292 if (dx * dx + dy * dy <= POCKET_RADIUS * POCKET_RADIUS) {
1293 // --- Assign ball type on first cheat-pocket if table still open ---
1294 if (player1Info.assignedType == BallType::NONE
1295 && player2Info.assignedType == BallType::NONE
1296 && (b->type == BallType::SOLID || b->type == BallType::STRIPE))
1297 {
1298 // In cheat mode, let's just assign to the current player
1299 AssignPlayerBallTypes(b->type);
1300 }
1301 b->isPocketed = true;
1302 pocketedThisTurn.push_back(b->id);
1303
1304 // --- FIX FOR CHEAT MODE SCORING ---
1305 // Immediately increment the correct player's count based on ball type,
1306 // not whose turn it is.
1307 if (b->id != 0 && b->id != 8) {
1308 if (b->type == player1Info.assignedType) {
1309 player1Info.ballsPocketedCount++;
1310 }
1311 else if (b->type == player2Info.assignedType) {
1312 player2Info.ballsPocketedCount++;
1313 }
1314 }
1315 // --- END FIX ---
1316 // --- NEW: If this was the 7th ball, trigger the arrow call UI ---
1317 if (b->id != 8) {
1318 PlayerInfo& shooter = (currentPlayer == 1 ? player1Info : player2Info);
1319 if (shooter.ballsPocketedCount >= 7
1320 && calledPocketP1 < 0
1321 && calledPocketP2 < 0)
1322 {
1323 currentGameState = (currentPlayer == 1)
1324 ? CHOOSING_POCKET_P1
1325 : CHOOSING_POCKET_P2;
1326 }
1327 else {
1328 // For any other cheat?pocket, keep the turn so you can continue aiming
1329 currentGameState = (currentPlayer == 1)
1330 ? PLAYER1_TURN
1331 : PLAYER2_TURN;
1332 }
1333 }
1334 // --- NEW: If it was the 8-Ball, award instant victory ---
1335 else {
1336 currentGameState = GAME_OVER;
1337 gameOverMessage = (currentPlayer == 1 ? player1Info.name : player2Info.name)
1338 + std::wstring(L" Wins!");
1339 }
1340 break;
1341 }
1342 }
1343 }
1344 }*/
1345
1346 ptMouse.x = LOWORD(lParam);
1347 ptMouse.y = HIWORD(lParam);
1348
1349 Ball* cueBall = GetCueBall(); // Get cueBall pointer
1350
1351 // Check for releasing aim drag (Stick OR Cue Ball)
1352 if ((isAiming || isDraggingStick) &&
1353 ((currentPlayer == 1 && (currentGameState == AIMING || currentGameState == BREAKING)) ||
1354 (!isPlayer2AI && currentPlayer == 2 && (currentGameState == AIMING || currentGameState == BREAKING))))
1355 {
1356 bool wasAiming = isAiming;
1357 bool wasDraggingStick = isDraggingStick;
1358 isAiming = false; isDraggingStick = false;
1359
1360 if (shotPower > 0.15f) { // Check power threshold
1361 if (currentGameState != AI_THINKING) {
1362 firstHitBallIdThisShot = -1; cueHitObjectBallThisShot = false; railHitAfterContact = false; // Reset foul flags
1363 std::thread([](const TCHAR* soundName) { PlaySound(soundName, NULL, SND_FILENAME | SND_NODEFAULT); }, TEXT("cue.wav")).detach();
1364 ApplyShot(shotPower, cueAngle, cueSpinX, cueSpinY);
1365 currentGameState = SHOT_IN_PROGRESS;
1366 foulCommitted = false; pocketedThisTurn.clear();
1367 }
1368 }
1369 else if (currentGameState != AI_THINKING) { // Revert state if power too low
1370 if (currentGameState == BREAKING) { /* Still breaking */ }
1371 else {
1372 currentGameState = (currentPlayer == 1) ? PLAYER1_TURN : PLAYER2_TURN;
1373 if (currentPlayer == 2 && isPlayer2AI) aiTurnPending = false;
1374 }
1375 }
1376 shotPower = 0; // Reset power indicator regardless
1377 }
1378
1379 // Handle releasing cue ball drag (placement)
1380 if (isDraggingCueBall) {
1381 isDraggingCueBall = false;
1382 // Check player allowed to place
1383 bool isPlacingState = (currentGameState == BALL_IN_HAND_P1 || currentGameState == BALL_IN_HAND_P2 || currentGameState == PRE_BREAK_PLACEMENT);
1384 bool isPlayerAllowed = (isPlacingState &&
1385 ((currentPlayer == 1 && currentGameState == BALL_IN_HAND_P1) ||
1386 (currentPlayer == 2 && !isPlayer2AI && currentGameState == BALL_IN_HAND_P2) ||
1387 (currentGameState == PRE_BREAK_PLACEMENT)));
1388
1389 if (isPlayerAllowed && cueBall) {
1390 bool behindHeadstring = (currentGameState == PRE_BREAK_PLACEMENT);
1391 if (IsValidCueBallPosition(cueBall->x, cueBall->y, behindHeadstring)) {
1392 // Finalize position already set by mouse move
1393 // Transition state
1394 if (currentGameState == PRE_BREAK_PLACEMENT) currentGameState = BREAKING;
1395 else if (currentGameState == BALL_IN_HAND_P1) currentGameState = PLAYER1_TURN;
1396 else if (currentGameState == BALL_IN_HAND_P2) currentGameState = PLAYER2_TURN;
1397 cueAngle = 0.0f;
1398 /* ----------------------------------------------------
1399 If the player who now has the turn is already on the
1400 8-ball, immediately switch to pocket-selection state.
1401 ---------------------------------------------------- */
1402 if (currentGameState == PLAYER1_TURN || currentGameState == PLAYER2_TURN)
1403 {
1404 CheckAndTransitionToPocketChoice(currentPlayer);
1405 }
1406 }
1407 else { /* Stay in BALL_IN_HAND state if final pos invalid */ }
1408 }
1409 }
1410
1411 // Handle releasing english setting
1412 if (isSettingEnglish) {
1413 isSettingEnglish = false;
1414 }
1415 return 0;
1416 } // End WM_LBUTTONUP
1417
1418 case WM_DESTROY:
1419 isMusicPlaying = false;
1420 if (midiDeviceID != 0) {
1421 mciSendCommand(midiDeviceID, MCI_CLOSE, 0, NULL);
1422 midiDeviceID = 0;
1423 SaveSettings(); // Save settings on exit
1424 }
1425 PostQuitMessage(0);
1426 return 0;
1427
1428 default:
1429 return DefWindowProc(hwnd, msg, wParam, lParam);
1430 }
1431 return 0;
1432 }
1433
1434 // --- Direct2D Resource Management ---
1435
1436 HRESULT CreateDeviceResources() {
1437 HRESULT hr = S_OK;
1438
1439 // Create Direct2D Factory
1440 if (!pFactory) {
1441 hr = D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, &pFactory);
1442 if (FAILED(hr)) return hr;
1443 }
1444
1445 // Create DirectWrite Factory
1446 if (!pDWriteFactory) {
1447 hr = DWriteCreateFactory(
1448 DWRITE_FACTORY_TYPE_SHARED,
1449 __uuidof(IDWriteFactory),
1450 reinterpret_cast<IUnknown**>(&pDWriteFactory)
1451 );
1452 if (FAILED(hr)) return hr;
1453 }
1454
1455 // Create Text Formats
1456 if (!pTextFormat && pDWriteFactory) {
1457 hr = pDWriteFactory->CreateTextFormat(
1458 L"Segoe UI", NULL, DWRITE_FONT_WEIGHT_NORMAL, DWRITE_FONT_STYLE_NORMAL, DWRITE_FONT_STRETCH_NORMAL,
1459 16.0f, L"en-us", &pTextFormat
1460 );
1461 if (FAILED(hr)) return hr;
1462 // Center align text
1463 pTextFormat->SetTextAlignment(DWRITE_TEXT_ALIGNMENT_CENTER);
1464 pTextFormat->SetParagraphAlignment(DWRITE_PARAGRAPH_ALIGNMENT_CENTER);
1465 }
1466 if (!pLargeTextFormat && pDWriteFactory) {
1467 hr = pDWriteFactory->CreateTextFormat(
1468 L"Impact", NULL, DWRITE_FONT_WEIGHT_BOLD, DWRITE_FONT_STYLE_NORMAL, DWRITE_FONT_STRETCH_NORMAL,
1469 48.0f, L"en-us", &pLargeTextFormat
1470 );
1471 if (FAILED(hr)) return hr;
1472 pLargeTextFormat->SetTextAlignment(DWRITE_TEXT_ALIGNMENT_LEADING); // Align left
1473 pLargeTextFormat->SetParagraphAlignment(DWRITE_PARAGRAPH_ALIGNMENT_CENTER);
1474 }
1475
1476
1477 // Create Render Target (needs valid hwnd)
1478 if (!pRenderTarget && hwndMain) {
1479 RECT rc;
1480 GetClientRect(hwndMain, &rc);
1481 D2D1_SIZE_U size = D2D1::SizeU(rc.right - rc.left, rc.bottom - rc.top);
1482
1483 hr = pFactory->CreateHwndRenderTarget(
1484 D2D1::RenderTargetProperties(),
1485 D2D1::HwndRenderTargetProperties(hwndMain, size),
1486 &pRenderTarget
1487 );
1488 if (FAILED(hr)) {
1489 // If failed, release factories if they were created in this call
1490 SafeRelease(&pTextFormat);
1491 SafeRelease(&pLargeTextFormat);
1492 SafeRelease(&pDWriteFactory);
1493 SafeRelease(&pFactory);
1494 pRenderTarget = nullptr; // Ensure it's null on failure
1495 return hr;
1496 }
1497 }
1498
1499 return hr;
1500 }
1501
1502 void DiscardDeviceResources() {
1503 SafeRelease(&pRenderTarget);
1504 SafeRelease(&pTextFormat);
1505 SafeRelease(&pLargeTextFormat);
1506 SafeRelease(&pDWriteFactory);
1507 // Keep pFactory until application exit? Or release here too? Let's release.
1508 SafeRelease(&pFactory);
1509 }
1510
1511 void OnResize(UINT width, UINT height) {
1512 if (pRenderTarget) {
1513 D2D1_SIZE_U size = D2D1::SizeU(width, height);
1514 pRenderTarget->Resize(size); // Ignore HRESULT for simplicity here
1515 }
1516 }
1517
1518 // --- Game Initialization ---
1519 void InitGame() {
1520 srand((unsigned int)time(NULL)); // Seed random number generator
1521 isOpeningBreakShot = true; // This is the start of a new game, so the next shot is an opening break.
1522 aiPlannedShotDetails.isValid = false; // Reset AI planned shot
1523 aiIsDisplayingAim = false;
1524 aiAimDisplayFramesLeft = 0;
1525 // ... (rest of InitGame())
1526
1527 // --- Ensure pocketed list is clear from the absolute start ---
1528 pocketedThisTurn.clear();
1529
1530 balls.clear(); // Clear existing balls
1531
1532 // Reset Player Info (Names should be set by Dialog/wWinMain/ResetGame)
1533 player1Info.assignedType = BallType::NONE;
1534 player1Info.ballsPocketedCount = 0;
1535 // Player 1 Name usually remains "Player 1"
1536 player2Info.assignedType = BallType::NONE;
1537 player2Info.ballsPocketedCount = 0;
1538 // Player 2 Name is set based on gameMode in ShowNewGameDialog
1539 // --- Reset any 8?Ball call state on new game ---
1540 lastEightBallPocketIndex = -1;
1541 calledPocketP1 = -1;
1542 calledPocketP2 = -1;
1543 pocketCallMessage = L"";
1544 aiPlannedShotDetails.isValid = false; // THIS IS THE CRITICAL FIX: Reset the AI's plan.
1545
1546 // Create Cue Ball (ID 0)
1547 // Initial position will be set during PRE_BREAK_PLACEMENT state
1548 balls.push_back({ 0, BallType::CUE_BALL, TABLE_LEFT + TABLE_WIDTH * 0.15f, RACK_POS_Y, 0, 0, CUE_BALL_COLOR, false });
1549
1550 // --- Create Object Balls (Temporary List) ---
1551 std::vector<Ball> objectBalls;
1552 // Solids (1-7, Yellow)
1553 for (int i = 1; i <= 7; ++i) {
1554 //objectBalls.push_back({ i, BallType::SOLID, 0, 0, 0, 0, SOLID_COLOR, false });
1555 objectBalls.push_back({ i, BallType::SOLID, 0,0,0,0,
1556 GetBallColor(i), false });
1557 }
1558 // Stripes (9-15, Red)
1559 for (int i = 9; i <= 15; ++i) {
1560 //objectBalls.push_back({ i, BallType::STRIPE, 0, 0, 0, 0, STRIPE_COLOR, false });
1561 objectBalls.push_back({ i, BallType::STRIPE, 0,0,0,0,
1562 GetBallColor(i), false });
1563 }
1564 // 8-Ball (ID 8) - Add it to the list to be placed
1565 //objectBalls.push_back({ 8, BallType::EIGHT_BALL, 0, 0, 0, 0, EIGHT_BALL_COLOR, false });
1566 objectBalls.push_back({ 8, BallType::EIGHT_BALL, 0,0,0,0,
1567 GetBallColor(8), false });
1568
1569
1570 // --- Racking Logic (Improved) ---
1571 float spacingX = BALL_RADIUS * 2.0f * 0.866f; // cos(30) for horizontal spacing
1572 float spacingY = BALL_RADIUS * 2.0f * 1.0f; // Vertical spacing
1573
1574 // Define rack positions (0-14 indices corresponding to triangle spots)
1575 D2D1_POINT_2F rackPositions[15];
1576 int rackIndex = 0;
1577 for (int row = 0; row < 5; ++row) {
1578 for (int col = 0; col <= row; ++col) {
1579 if (rackIndex >= 15) break;
1580 float x = RACK_POS_X + row * spacingX;
1581 float y = RACK_POS_Y + (col - row / 2.0f) * spacingY;
1582 rackPositions[rackIndex++] = D2D1::Point2F(x, y);
1583 }
1584 }
1585
1586 // Separate 8-ball
1587 Ball eightBall;
1588 std::vector<Ball> otherBalls; // Solids and Stripes
1589 bool eightBallFound = false;
1590 for (const auto& ball : objectBalls) {
1591 if (ball.id == 8) {
1592 eightBall = ball;
1593 eightBallFound = true;
1594 }
1595 else {
1596 otherBalls.push_back(ball);
1597 }
1598 }
1599 // Ensure 8 ball was actually created (should always be true)
1600 if (!eightBallFound) {
1601 // Handle error - perhaps recreate it? For now, proceed.
1602 eightBall = { 8, BallType::EIGHT_BALL, 0, 0, 0, 0, EIGHT_BALL_COLOR, false };
1603 }
1604
1605
1606 // Shuffle the other 14 balls
1607 // Use std::shuffle if available (C++11 and later) for better randomness
1608 // std::random_device rd;
1609 // std::mt19937 g(rd());
1610 // std::shuffle(otherBalls.begin(), otherBalls.end(), g);
1611 std::random_shuffle(otherBalls.begin(), otherBalls.end()); // Using deprecated for now
1612
1613 // --- Place balls into the main 'balls' vector in rack order ---
1614 // Important: Add the cue ball (already created) first.
1615 // (Cue ball added at the start of the function now)
1616
1617 // 1. Place the 8-ball in its fixed position (index 4 for the 3rd row center)
1618 int eightBallRackIndex = 4;
1619 eightBall.x = rackPositions[eightBallRackIndex].x;
1620 eightBall.y = rackPositions[eightBallRackIndex].y;
1621 eightBall.vx = 0;
1622 eightBall.vy = 0;
1623 eightBall.isPocketed = false;
1624 balls.push_back(eightBall); // Add 8 ball to the main vector
1625
1626 // 2. Place the shuffled Solids and Stripes in the remaining spots
1627 size_t otherBallIdx = 0;
1628 //int otherBallIdx = 0;
1629 for (int i = 0; i < 15; ++i) {
1630 if (i == eightBallRackIndex) continue; // Skip the 8-ball spot
1631
1632 if (otherBallIdx < otherBalls.size()) {
1633 Ball& ballToPlace = otherBalls[otherBallIdx++];
1634 ballToPlace.x = rackPositions[i].x;
1635 ballToPlace.y = rackPositions[i].y;
1636 ballToPlace.vx = 0;
1637 ballToPlace.vy = 0;
1638 ballToPlace.isPocketed = false;
1639 balls.push_back(ballToPlace); // Add to the main game vector
1640 }
1641 }
1642 // --- End Racking Logic ---
1643
1644
1645 // --- Determine Who Breaks and Initial State ---
1646 if (isPlayer2AI) {
1647 /*// AI Mode: Randomly decide who breaks
1648 if ((rand() % 2) == 0) {
1649 // AI (Player 2) breaks
1650 currentPlayer = 2;
1651 currentGameState = PRE_BREAK_PLACEMENT; // AI needs to place ball first
1652 aiTurnPending = true; // Trigger AI logic
1653 }
1654 else {
1655 // Player 1 (Human) breaks
1656 currentPlayer = 1;
1657 currentGameState = PRE_BREAK_PLACEMENT; // Human places cue ball
1658 aiTurnPending = false;*/
1659 switch (openingBreakMode) {
1660 case CPU_BREAK:
1661 currentPlayer = 2; // AI breaks
1662 currentGameState = PRE_BREAK_PLACEMENT;
1663 aiTurnPending = true;
1664 break;
1665 case P1_BREAK:
1666 currentPlayer = 1; // Player 1 breaks
1667 currentGameState = PRE_BREAK_PLACEMENT;
1668 aiTurnPending = false;
1669 break;
1670 case FLIP_COIN_BREAK:
1671 if ((rand() % 2) == 0) { // 0 for AI, 1 for Player 1
1672 currentPlayer = 2; // AI breaks
1673 currentGameState = PRE_BREAK_PLACEMENT;
1674 aiTurnPending = true;
1675 }
1676 else {
1677 currentPlayer = 1; // Player 1 breaks
1678 currentGameState = PRE_BREAK_PLACEMENT;
1679 aiTurnPending = false;
1680 }
1681 break;
1682 default: // Fallback to CPU break
1683 currentPlayer = 2;
1684 currentGameState = PRE_BREAK_PLACEMENT;
1685 aiTurnPending = true;
1686 break;
1687 }
1688 }
1689 else {
1690 // Human vs Human, Player 1 always breaks (or could add a flip coin for HvsH too if desired)
1691 currentPlayer = 1;
1692 currentGameState = PRE_BREAK_PLACEMENT;
1693 aiTurnPending = false; // No AI involved
1694 }
1695
1696 // Reset other relevant game state variables
1697 foulCommitted = false;
1698 gameOverMessage = L"";
1699 firstBallPocketedAfterBreak = false;
1700 // pocketedThisTurn cleared at start
1701 // Reset shot parameters and input flags
1702 shotPower = 0.0f;
1703 cueSpinX = 0.0f;
1704 cueSpinY = 0.0f;
1705 isAiming = false;
1706 isDraggingCueBall = false;
1707 isSettingEnglish = false;
1708 cueAngle = 0.0f; // Reset aim angle
1709 }
1710
1711
1712 // --------------------------------------------------------------------------------
1713 // Full GameUpdate(): integrates AI call?pocket ? aim ? shoot (no omissions)
1714 // --------------------------------------------------------------------------------
1715 void GameUpdate() {
1716 // --- 1) Handle an in?flight shot ---
1717 if (currentGameState == SHOT_IN_PROGRESS) {
1718 UpdatePhysics();
1719 // ? clear old 8?ball pocket info before any new pocket checks
1720 //lastEightBallPocketIndex = -1;
1721 CheckCollisions();
1722 CheckPockets(); // FIX: This line was missing. It's essential to check for pocketed balls every frame.
1723
1724 if (AreBallsMoving()) {
1725 isAiming = false;
1726 aiIsDisplayingAim = false;
1727 }
1728
1729 if (!AreBallsMoving()) {
1730 ProcessShotResults();
1731 }
1732 return;
1733 }
1734
1735 // --- 2) CPU’s turn (table is static) ---
1736 if (isPlayer2AI && currentPlayer == 2 && !AreBallsMoving()) {
1737 // ??? If we've just auto?entered AI_THINKING for the 8?ball call, actually make the decision ???
1738 if (currentGameState == AI_THINKING && aiTurnPending) {
1739 aiTurnPending = false; // consume the pending flag
1740 AIMakeDecision(); // CPU calls its pocket or plans its shot
1741 return; // done this tick
1742 }
1743
1744 // ??? Automate the AI pocket?selection click ???
1745 if (currentGameState == CHOOSING_POCKET_P2) {
1746 // AI immediately confirms its call and moves to thinking/shooting
1747 currentGameState = AI_THINKING;
1748 aiTurnPending = true;
1749 return; // process on next tick
1750 }
1751 // 2A) If AI is displaying its aim line, count down then shoot
1752 if (aiIsDisplayingAim) {
1753 aiAimDisplayFramesLeft--;
1754 if (aiAimDisplayFramesLeft <= 0) {
1755 aiIsDisplayingAim = false;
1756 if (aiPlannedShotDetails.isValid) {
1757 firstHitBallIdThisShot = -1;
1758 cueHitObjectBallThisShot = false;
1759 railHitAfterContact = false;
1760 std::thread([](const TCHAR* soundName) {
1761 PlaySound(soundName, NULL, SND_FILENAME | SND_NODEFAULT);
1762 }, TEXT("cue.wav")).detach();
1763
1764 ApplyShot(
1765 aiPlannedShotDetails.power,
1766 aiPlannedShotDetails.angle,
1767 aiPlannedShotDetails.spinX,
1768 aiPlannedShotDetails.spinY
1769 );
1770 aiPlannedShotDetails.isValid = false;
1771 }
1772 currentGameState = SHOT_IN_PROGRESS;
1773 foulCommitted = false;
1774 pocketedThisTurn.clear();
1775 }
1776 return;
1777 }
1778
1779 // 2B) Immediately after calling pocket, transition into AI_THINKING
1780 if (currentGameState == CHOOSING_POCKET_P2 && aiTurnPending) {
1781 // Start thinking/shooting right away—no human click required
1782 currentGameState = AI_THINKING;
1783 aiTurnPending = false;
1784 AIMakeDecision();
1785 return;
1786 }
1787
1788 // 2C) If AI has pending actions (break, ball?in?hand, or normal turn)
1789 if (aiTurnPending) {
1790 if (currentGameState == BALL_IN_HAND_P2) {
1791 AIPlaceCueBall();
1792 currentGameState = AI_THINKING;
1793 aiTurnPending = false;
1794 AIMakeDecision();
1795 }
1796 else if (isOpeningBreakShot && currentGameState == PRE_BREAK_PLACEMENT) {
1797 AIBreakShot();
1798 }
1799 else if (currentGameState == PLAYER2_TURN || currentGameState == BREAKING) {
1800 currentGameState = AI_THINKING;
1801 aiTurnPending = false;
1802 AIMakeDecision();
1803 }
1804 return;
1805 }
1806 }
1807 }
1808
1809
1810 // --- Physics and Collision ---
1811 void UpdatePhysics() {
1812 for (size_t i = 0; i < balls.size(); ++i) {
1813 Ball& b = balls[i];
1814 if (!b.isPocketed) {
1815 b.x += b.vx;
1816 b.y += b.vy;
1817
1818 // Apply friction
1819 b.vx *= FRICTION;
1820 b.vy *= FRICTION;
1821
1822 // Stop balls if velocity is very low
1823 if (GetDistanceSq(b.vx, b.vy, 0, 0) < MIN_VELOCITY_SQ) {
1824 b.vx = 0;
1825 b.vy = 0;
1826 }
1827
1828 /* -----------------------------------------------------------------
1829 Additional clamp to guarantee the ball never escapes the table.
1830 The existing wall–collision code can momentarily disable the
1831 reflection test while the ball is close to a pocket mouth;
1832 that rare case allowed it to ‘slide’ through the cushion and
1833 leave the board. We therefore enforce a final boundary check
1834 after the normal physics step.
1835 ----------------------------------------------------------------- */
1836 const float leftBound = TABLE_LEFT + BALL_RADIUS;
1837 const float rightBound = TABLE_RIGHT - BALL_RADIUS;
1838 const float topBound = TABLE_TOP + BALL_RADIUS;
1839 const float bottomBound = TABLE_BOTTOM - BALL_RADIUS;
1840
1841 if (b.x < leftBound) { b.x = leftBound; b.vx = fabsf(b.vx); }
1842 if (b.x > rightBound) { b.x = rightBound; b.vx = -fabsf(b.vx); }
1843 if (b.y < topBound) { b.y = topBound; b.vy = fabsf(b.vy); }
1844 if (b.y > bottomBound) { b.y = bottomBound; b.vy = -fabsf(b.vy); }
1845 }
1846 }
1847 }
1848
1849 void CheckCollisions() {
1850 float left = TABLE_LEFT;
1851 float right = TABLE_RIGHT;
1852 float top = TABLE_TOP;
1853 float bottom = TABLE_BOTTOM;
1854 const float pocketMouthCheckRadiusSq = (POCKET_RADIUS + BALL_RADIUS) * (POCKET_RADIUS + BALL_RADIUS) * 1.1f;
1855
1856 // --- Reset Per-Frame Sound Flags ---
1857 bool playedWallSoundThisFrame = false;
1858 bool playedCollideSoundThisFrame = false;
1859 // ---
1860
1861 for (size_t i = 0; i < balls.size(); ++i) {
1862 Ball& b1 = balls[i];
1863 if (b1.isPocketed) continue;
1864
1865 bool nearPocket[6];
1866 for (int p = 0; p < 6; ++p) {
1867 nearPocket[p] = GetDistanceSq(b1.x, b1.y, pocketPositions[p].x, pocketPositions[p].y) < pocketMouthCheckRadiusSq;
1868 }
1869 bool nearTopLeftPocket = nearPocket[0];
1870 bool nearTopMidPocket = nearPocket[1];
1871 bool nearTopRightPocket = nearPocket[2];
1872 bool nearBottomLeftPocket = nearPocket[3];
1873 bool nearBottomMidPocket = nearPocket[4];
1874 bool nearBottomRightPocket = nearPocket[5];
1875
1876 bool collidedWallThisBall = false;
1877
1878 // --- Ball-Wall Collisions ---
1879 // (Check logic unchanged, added sound calls and railHitAfterContact update)
1880 // Left Wall
1881 if (b1.x - BALL_RADIUS < left) {
1882 if (!nearTopLeftPocket && !nearBottomLeftPocket) {
1883 b1.x = left + BALL_RADIUS; b1.vx *= -1.0f; collidedWallThisBall = true;
1884 if (!playedWallSoundThisFrame) {
1885 std::thread([](const TCHAR* soundName) { PlaySound(soundName, NULL, SND_FILENAME | SND_NODEFAULT); }, TEXT("wall.wav")).detach();
1886 playedWallSoundThisFrame = true;
1887 }
1888 if (cueHitObjectBallThisShot) railHitAfterContact = true; // Track rail hit after contact
1889 }
1890 }
1891 // Right Wall
1892 if (b1.x + BALL_RADIUS > right) {
1893 if (!nearTopRightPocket && !nearBottomRightPocket) {
1894 b1.x = right - BALL_RADIUS; b1.vx *= -1.0f; collidedWallThisBall = true;
1895 if (!playedWallSoundThisFrame) {
1896 std::thread([](const TCHAR* soundName) { PlaySound(soundName, NULL, SND_FILENAME | SND_NODEFAULT); }, TEXT("wall.wav")).detach();
1897 playedWallSoundThisFrame = true;
1898 }
1899 if (cueHitObjectBallThisShot) railHitAfterContact = true; // Track rail hit after contact
1900 }
1901 }
1902 // Top Wall
1903 if (b1.y - BALL_RADIUS < top) {
1904 if (!nearTopLeftPocket && !nearTopMidPocket && !nearTopRightPocket) {
1905 b1.y = top + BALL_RADIUS; b1.vy *= -1.0f; collidedWallThisBall = true;
1906 if (!playedWallSoundThisFrame) {
1907 std::thread([](const TCHAR* soundName) { PlaySound(soundName, NULL, SND_FILENAME | SND_NODEFAULT); }, TEXT("wall.wav")).detach();
1908 playedWallSoundThisFrame = true;
1909 }
1910 if (cueHitObjectBallThisShot) railHitAfterContact = true; // Track rail hit after contact
1911 }
1912 }
1913 // Bottom Wall
1914 if (b1.y + BALL_RADIUS > bottom) {
1915 if (!nearBottomLeftPocket && !nearBottomMidPocket && !nearBottomRightPocket) {
1916 b1.y = bottom - BALL_RADIUS; b1.vy *= -1.0f; collidedWallThisBall = true;
1917 if (!playedWallSoundThisFrame) {
1918 std::thread([](const TCHAR* soundName) { PlaySound(soundName, NULL, SND_FILENAME | SND_NODEFAULT); }, TEXT("wall.wav")).detach();
1919 playedWallSoundThisFrame = true;
1920 }
1921 if (cueHitObjectBallThisShot) railHitAfterContact = true; // Track rail hit after contact
1922 }
1923 }
1924
1925 // Spin effect (Unchanged)
1926 if (collidedWallThisBall) {
1927 if (b1.x <= left + BALL_RADIUS || b1.x >= right - BALL_RADIUS) { b1.vy += cueSpinX * b1.vx * 0.05f; }
1928 if (b1.y <= top + BALL_RADIUS || b1.y >= bottom - BALL_RADIUS) { b1.vx -= cueSpinY * b1.vy * 0.05f; }
1929 cueSpinX *= 0.7f; cueSpinY *= 0.7f;
1930 }
1931
1932
1933 // --- Ball-Ball Collisions ---
1934 for (size_t j = i + 1; j < balls.size(); ++j) {
1935 Ball& b2 = balls[j];
1936 if (b2.isPocketed) continue;
1937
1938 float dx = b2.x - b1.x; float dy = b2.y - b1.y;
1939 float distSq = dx * dx + dy * dy;
1940 float minDist = BALL_RADIUS * 2.0f;
1941
1942 if (distSq > 1e-6 && distSq < minDist * minDist) {
1943 float dist = sqrtf(distSq);
1944 float overlap = minDist - dist;
1945 float nx = dx / dist; float ny = dy / dist;
1946
1947 // Separation (Unchanged)
1948 b1.x -= overlap * 0.5f * nx; b1.y -= overlap * 0.5f * ny;
1949 b2.x += overlap * 0.5f * nx; b2.y += overlap * 0.5f * ny;
1950
1951 float rvx = b1.vx - b2.vx; float rvy = b1.vy - b2.vy;
1952 float velAlongNormal = rvx * nx + rvy * ny;
1953
1954 if (velAlongNormal > 0) { // Colliding
1955 // --- Play Ball Collision Sound ---
1956 if (!playedCollideSoundThisFrame) {
1957 std::thread([](const TCHAR* soundName) { PlaySound(soundName, NULL, SND_FILENAME | SND_NODEFAULT); }, TEXT("poolballhit.wav")).detach();
1958 playedCollideSoundThisFrame = true; // Set flag
1959 }
1960 // --- End Sound ---
1961
1962 // --- NEW: Track First Hit and Cue/Object Collision ---
1963 if (firstHitBallIdThisShot == -1) { // If first hit hasn't been recorded yet
1964 if (b1.id == 0) { // Cue ball hit b2 first
1965 firstHitBallIdThisShot = b2.id;
1966 cueHitObjectBallThisShot = true;
1967 }
1968 else if (b2.id == 0) { // Cue ball hit b1 first
1969 firstHitBallIdThisShot = b1.id;
1970 cueHitObjectBallThisShot = true;
1971 }
1972 // If neither is cue ball, doesn't count as first hit for foul purposes
1973 }
1974 else if (b1.id == 0 || b2.id == 0) {
1975 // Track subsequent cue ball collisions with object balls
1976 cueHitObjectBallThisShot = true;
1977 }
1978 // --- End First Hit Tracking ---
1979
1980
1981 // Impulse (Unchanged)
1982 float impulse = velAlongNormal;
1983 b1.vx -= impulse * nx; b1.vy -= impulse * ny;
1984 b2.vx += impulse * nx; b2.vy += impulse * ny;
1985
1986 // Spin Transfer (Unchanged)
1987 if (b1.id == 0 || b2.id == 0) {
1988 float spinEffectFactor = 0.08f;
1989 b1.vx += (cueSpinY * ny - cueSpinX * nx) * spinEffectFactor;
1990 b1.vy += (cueSpinY * nx + cueSpinX * ny) * spinEffectFactor;
1991 b2.vx -= (cueSpinY * ny - cueSpinX * nx) * spinEffectFactor;
1992 b2.vy -= (cueSpinY * nx + cueSpinX * ny) * spinEffectFactor;
1993 cueSpinX *= 0.85f; cueSpinY *= 0.85f;
1994 }
1995 }
1996 }
1997 } // End ball-ball loop
1998 } // End ball loop
1999 } // End CheckCollisions
2000
2001
2002 bool CheckPockets() {
2003 bool anyPocketed = false;
2004 // FIX: Declare a local flag to ensure the sound only plays ONCE per function call.
2005 bool ballPocketedThisCheck = false;
2006 // For each ball not already pocketed:
2007 for (auto& b : balls) {
2008 if (b.isPocketed)
2009 continue;
2010
2011 // Check against each pocket
2012 for (int p = 0; p < 6; ++p) {
2013 float dx = b.x - pocketPositions[p].x;
2014 float dy = b.y - pocketPositions[p].y;
2015 if (dx * dx + dy * dy <= POCKET_RADIUS * POCKET_RADIUS) {
2016 // It's in the pocket—remove it from play
2017 // If it's the 8?ball, remember which pocket it went into
2018 if (b.id == 8) {
2019 lastEightBallPocketIndex = p; // <-- Must set here!
2020 }
2021 b.isPocketed = true;
2022 b.vx = b.vy = 0.0f; // kill any movement
2023 pocketedThisTurn.push_back(b.id);
2024 anyPocketed = true;
2025
2026 // --- FIX: Insert your sound logic here ---
2027 // The 'if' guard prevents multiple sounds on a multi-ball break.
2028 if (!ballPocketedThisCheck) {
2029 std::thread([](const TCHAR* soundName) { PlaySound(soundName, NULL, SND_FILENAME | SND_NODEFAULT); }, TEXT("pocket.wav")).detach();
2030 ballPocketedThisCheck = true;
2031 }
2032 // --- End Sound Fix ---
2033
2034 break; // no need to check other pockets for this ball
2035 }
2036 }
2037 }
2038 return anyPocketed;
2039 }
2040
2041 bool AreBallsMoving() {
2042 for (size_t i = 0; i < balls.size(); ++i) {
2043 if (!balls[i].isPocketed && (balls[i].vx != 0 || balls[i].vy != 0)) {
2044 return true;
2045 }
2046 }
2047 return false;
2048 }
2049
2050 void RespawnCueBall(bool behindHeadstring) {
2051 Ball* cueBall = GetCueBall();
2052 if (cueBall) {
2053 // Determine the initial target position
2054 float targetX, targetY;
2055 if (behindHeadstring) {
2056 targetX = TABLE_LEFT + (HEADSTRING_X - TABLE_LEFT) * 0.5f;
2057 targetY = TABLE_TOP + TABLE_HEIGHT / 2.0f;
2058 }
2059 else {
2060 targetX = TABLE_LEFT + TABLE_WIDTH / 2.0f;
2061 targetY = TABLE_TOP + TABLE_HEIGHT / 2.0f;
2062 }
2063
2064 // FOOLPROOF FIX: Check if the target spot is valid. If not, nudge it until it is.
2065 int attempts = 0;
2066 while (!IsValidCueBallPosition(targetX, targetY, behindHeadstring) && attempts < 100) {
2067 // If the spot is occupied, try nudging the ball slightly.
2068 targetX += (static_cast<float>(rand() % 100 - 50) / 50.0f) * BALL_RADIUS;
2069 targetY += (static_cast<float>(rand() % 100 - 50) / 50.0f) * BALL_RADIUS;
2070 // Clamp to stay within reasonable bounds
2071 targetX = std::max(TABLE_LEFT + BALL_RADIUS, std::min(targetX, TABLE_RIGHT - BALL_RADIUS));
2072 targetY = std::max(TABLE_TOP + BALL_RADIUS, std::min(targetY, TABLE_BOTTOM - BALL_RADIUS));
2073 attempts++;
2074 }
2075
2076 // Set the final, valid position.
2077 cueBall->x = targetX;
2078 cueBall->y = targetY;
2079 cueBall->vx = 0;
2080 cueBall->vy = 0;
2081 cueBall->isPocketed = false;
2082
2083 // Set the correct game state for ball-in-hand.
2084 if (currentPlayer == 1) {
2085 currentGameState = BALL_IN_HAND_P1;
2086 aiTurnPending = false;
2087 }
2088 else {
2089 currentGameState = BALL_IN_HAND_P2;
2090 if (isPlayer2AI) {
2091 aiTurnPending = true;
2092 }
2093 }
2094 }
2095 }
2096
2097
2098 // --- Game Logic ---
2099
2100 void ApplyShot(float power, float angle, float spinX, float spinY) {
2101 Ball* cueBall = GetCueBall();
2102 if (cueBall) {
2103
2104 // --- Play Cue Strike Sound (Threaded) ---
2105 if (power > 0.1f) { // Only play if it's an audible shot
2106 std::thread([](const TCHAR* soundName) { PlaySound(soundName, NULL, SND_FILENAME | SND_NODEFAULT); }, TEXT("cue.wav")).detach();
2107 }
2108 // --- End Sound ---
2109
2110 cueBall->vx = cosf(angle) * power;
2111 cueBall->vy = sinf(angle) * power;
2112
2113 // Apply English (Spin) - Simplified effect (Unchanged)
2114 cueBall->vx += sinf(angle) * spinY * 0.5f;
2115 cueBall->vy -= cosf(angle) * spinY * 0.5f;
2116 cueBall->vx -= cosf(angle) * spinX * 0.5f;
2117 cueBall->vy -= sinf(angle) * spinX * 0.5f;
2118
2119 // Store spin (Unchanged)
2120 cueSpinX = spinX;
2121 cueSpinY = spinY;
2122
2123 // --- Reset Foul Tracking flags for the new shot ---
2124 // (Also reset in LBUTTONUP, but good to ensure here too)
2125 firstHitBallIdThisShot = -1; // No ball hit yet
2126 cueHitObjectBallThisShot = false; // Cue hasn't hit anything yet
2127 railHitAfterContact = false; // No rail hit after contact yet
2128 // --- End Reset ---
2129
2130 // If this was the opening break shot, clear the flag
2131 if (isOpeningBreakShot) {
2132 isOpeningBreakShot = false; // Mark opening break as taken
2133 }
2134 }
2135 }
2136
2137
2138 // ---------------------------------------------------------------------
2139 // ProcessShotResults()
2140 // ---------------------------------------------------------------------
2141 void ProcessShotResults() {
2142 bool cueBallPocketed = false;
2143 bool eightBallPocketed = false;
2144 bool playerContinuesTurn = false;
2145
2146 // --- Step 1: Update Ball Counts FIRST (THE CRITICAL FIX) ---
2147 // We must update the score before any other game logic runs.
2148 PlayerInfo& shootingPlayer = (currentPlayer == 1) ? player1Info : player2Info;
2149 int ownBallsPocketedThisTurn = 0;
2150
2151 for (int id : pocketedThisTurn) {
2152 Ball* b = GetBallById(id);
2153 if (!b) continue;
2154
2155 if (b->id == 0) {
2156 cueBallPocketed = true;
2157 }
2158 else if (b->id == 8) {
2159 eightBallPocketed = true;
2160 }
2161 else {
2162 // This is a numbered ball. Update the pocketed count for the correct player.
2163 if (b->type == player1Info.assignedType && player1Info.assignedType != BallType::NONE) {
2164 player1Info.ballsPocketedCount++;
2165 }
2166 else if (b->type == player2Info.assignedType && player2Info.assignedType != BallType::NONE) {
2167 player2Info.ballsPocketedCount++;
2168 }
2169
2170 if (b->type == shootingPlayer.assignedType) {
2171 ownBallsPocketedThisTurn++;
2172 }
2173 }
2174 }
2175
2176 if (ownBallsPocketedThisTurn > 0) {
2177 playerContinuesTurn = true;
2178 }
2179
2180 // --- Step 2: Handle Game-Ending 8-Ball Shot ---
2181 // Now that the score is updated, this check will have the correct information.
2182 if (eightBallPocketed) {
2183 CheckGameOverConditions(true, cueBallPocketed);
2184 if (currentGameState == GAME_OVER) {
2185 pocketedThisTurn.clear();
2186 return;
2187 }
2188 }
2189
2190 // --- Step 3: Check for Fouls ---
2191 bool turnFoul = false;
2192 if (cueBallPocketed) {
2193 turnFoul = true;
2194 }
2195 else {
2196 Ball* firstHit = GetBallById(firstHitBallIdThisShot);
2197 if (!firstHit) { // Rule: Hitting nothing is a foul.
2198 turnFoul = true;
2199 }
2200 else { // Rule: Hitting the wrong ball type is a foul.
2201 if (player1Info.assignedType != BallType::NONE) { // Colors are assigned.
2202 // We check if the player WAS on the 8-ball BEFORE this shot.
2203 bool wasOnEightBall = (shootingPlayer.assignedType != BallType::NONE && (shootingPlayer.ballsPocketedCount - ownBallsPocketedThisTurn) >= 7);
2204 if (wasOnEightBall) {
2205 if (firstHit->id != 8) turnFoul = true;
2206 }
2207 else {
2208 if (firstHit->type != shootingPlayer.assignedType) turnFoul = true;
2209 }
2210 }
2211 }
2212 } //reenable below disabled for debugging
2213 //if (!turnFoul && cueHitObjectBallThisShot && !railHitAfterContact && pocketedThisTurn.empty()) {
2214 //turnFoul = true;
2215 //}
2216 foulCommitted = turnFoul;
2217
2218 // --- Step 4: Final State Transition ---
2219 if (foulCommitted) {
2220 SwitchTurns();
2221 RespawnCueBall(false);
2222 }
2223 else if (player1Info.assignedType == BallType::NONE && !pocketedThisTurn.empty() && !cueBallPocketed) {
2224 // Assign types on the break.
2225 for (int id : pocketedThisTurn) {
2226 Ball* b = GetBallById(id);
2227 if (b && b->type != BallType::EIGHT_BALL) {
2228 AssignPlayerBallTypes(b->type);
2229 break;
2230 }
2231 }
2232 CheckAndTransitionToPocketChoice(currentPlayer);
2233 }
2234 else if (playerContinuesTurn) {
2235 // The player's turn continues. Now the check will work correctly.
2236 CheckAndTransitionToPocketChoice(currentPlayer);
2237 }
2238 else {
2239 SwitchTurns();
2240 }
2241
2242 pocketedThisTurn.clear();
2243 }
2244
2245 /*
2246 // --- Step 3: Final State Transition ---
2247 if (foulCommitted) {
2248 SwitchTurns();
2249 RespawnCueBall(false);
2250 }
2251 else if (playerContinuesTurn) {
2252 CheckAndTransitionToPocketChoice(currentPlayer);
2253 }
2254 else {
2255 SwitchTurns();
2256 }
2257
2258 pocketedThisTurn.clear();
2259 } */
2260
2261 // Assign groups AND optionally give the shooter his first count.
2262 bool AssignPlayerBallTypes(BallType firstPocketedType, bool creditShooter /*= true*/)
2263 {
2264 if (firstPocketedType != SOLID && firstPocketedType != STRIPE)
2265 return false; // safety
2266
2267 /* ---------------------------------------------------------
2268 1. Decide the groups
2269 --------------------------------------------------------- */
2270 if (currentPlayer == 1)
2271 {
2272 player1Info.assignedType = firstPocketedType;
2273 player2Info.assignedType =
2274 (firstPocketedType == SOLID) ? STRIPE : SOLID;
2275 }
2276 else
2277 {
2278 player2Info.assignedType = firstPocketedType;
2279 player1Info.assignedType =
2280 (firstPocketedType == SOLID) ? STRIPE : SOLID;
2281 }
2282
2283 /* ---------------------------------------------------------
2284 2. Count the very ball that made the assignment
2285 --------------------------------------------------------- */
2286 if (creditShooter)
2287 {
2288 if (currentPlayer == 1)
2289 ++player1Info.ballsPocketedCount;
2290 else
2291 ++player2Info.ballsPocketedCount;
2292 }
2293 return true;
2294 }
2295
2296 /*bool AssignPlayerBallTypes(BallType firstPocketedType) {
2297 if (firstPocketedType == BallType::SOLID || firstPocketedType == BallType::STRIPE) {
2298 if (currentPlayer == 1) {
2299 player1Info.assignedType = firstPocketedType;
2300 player2Info.assignedType = (firstPocketedType == BallType::SOLID) ? BallType::STRIPE : BallType::SOLID;
2301 }
2302 else {
2303 player2Info.assignedType = firstPocketedType;
2304 player1Info.assignedType = (firstPocketedType == BallType::SOLID) ? BallType::STRIPE : BallType::SOLID;
2305 }
2306 return true; // Assignment was successful
2307 }
2308 return false; // No assignment made (e.g., 8-ball was pocketed on break)
2309 }*/
2310 // If 8-ball was first (illegal on break generally), rules vary.
2311 // Here, we might ignore assignment until a solid/stripe is pocketed legally.
2312 // Or assign based on what *else* was pocketed, if anything.
2313 // Simplification: Assignment only happens on SOLID or STRIPE first pocket.
2314
2315
2316 // --- Called in ProcessShotResults() after pocket detection ---
2317 void CheckGameOverConditions(bool eightBallPocketed, bool cueBallPocketed)
2318 {
2319 // Only care if the 8?ball really went in:
2320 if (!eightBallPocketed) return;
2321
2322 // Who’s shooting now?
2323 PlayerInfo& shooter = (currentPlayer == 1) ? player1Info : player2Info;
2324 PlayerInfo& opponent = (currentPlayer == 1) ? player2Info : player1Info;
2325
2326 // Which pocket did we CALL?
2327 int called = (currentPlayer == 1) ? calledPocketP1 : calledPocketP2;
2328 // Which pocket did it ACTUALLY fall into?
2329 int actual = lastEightBallPocketIndex;
2330
2331 // Check legality: must have called a pocket ?0, must match actual,
2332 // must have pocketed all 7 of your balls first, and must not have scratched.
2333 bool legal = (called >= 0)
2334 && (called == actual)
2335 && (shooter.ballsPocketedCount >= 7)
2336 && (!cueBallPocketed);
2337
2338 // Build a message that shows both values for debugging/tracing:
2339 if (legal) {
2340 gameOverMessage = shooter.name
2341 + L" Wins! "
2342 + L"(Called: " + std::to_wstring(called)
2343 + L", Actual: " + std::to_wstring(actual) + L")";
2344 }
2345 else {
2346 gameOverMessage = opponent.name
2347 + L" Wins! (Illegal 8-Ball) "
2348 + L"(Called: " + std::to_wstring(called)
2349 + L", Actual: " + std::to_wstring(actual) + L")";
2350 }
2351
2352 currentGameState = GAME_OVER;
2353 }
2354
2355
2356
2357 /*void CheckGameOverConditions(bool eightBallPocketed, bool cueBallPocketed) {
2358 if (!eightBallPocketed) return;
2359
2360 PlayerInfo& shootingPlayer = (currentPlayer == 1) ? player1Info : player2Info;
2361 PlayerInfo& opponentPlayer = (currentPlayer == 1) ? player2Info : player1Info;
2362
2363 // Handle 8-ball on break: re-spot and continue.
2364 if (player1Info.assignedType == BallType::NONE) {
2365 Ball* b = GetBallById(8);
2366 if (b) { b->isPocketed = false; b->x = RACK_POS_X; b->y = RACK_POS_Y; b->vx = b->vy = 0; }
2367 if (cueBallPocketed) foulCommitted = true;
2368 return;
2369 }
2370
2371 // --- FOOLPROOF WIN/LOSS LOGIC ---
2372 bool wasOnEightBall = IsPlayerOnEightBall(currentPlayer);
2373 int calledPocket = (currentPlayer == 1) ? calledPocketP1 : calledPocketP2;
2374 int actualPocket = -1;
2375
2376 // Find which pocket the 8-ball actually went into.
2377 for (int id : pocketedThisTurn) {
2378 if (id == 8) {
2379 Ball* b = GetBallById(8); // This ball is already marked as pocketed, but we need its last coords.
2380 if (b) {
2381 for (int p_idx = 0; p_idx < 6; ++p_idx) {
2382 // Check last known position against pocket centers
2383 if (GetDistanceSq(b->x, b->y, pocketPositions[p_idx].x, pocketPositions[p_idx].y) < POCKET_RADIUS * POCKET_RADIUS * 1.5f) {
2384 actualPocket = p_idx;
2385 break;
2386 }
2387 }
2388 }
2389 break;
2390 }
2391 }
2392
2393 // Evaluate win/loss based on a clear hierarchy of rules.
2394 if (!wasOnEightBall) {
2395 gameOverMessage = opponentPlayer.name + L" Wins! (8-Ball Pocketed Early)";
2396 }
2397 else if (cueBallPocketed) {
2398 gameOverMessage = opponentPlayer.name + L" Wins! (Scratched on 8-Ball)";
2399 }
2400 else if (calledPocket == -1) {
2401 gameOverMessage = opponentPlayer.name + L" Wins! (Pocket Not Called)";
2402 }
2403 else if (actualPocket != calledPocket) {
2404 gameOverMessage = opponentPlayer.name + L" Wins! (8-Ball in Wrong Pocket)";
2405 }
2406 else {
2407 // WIN! All loss conditions failed, this must be a legal win.
2408 gameOverMessage = shootingPlayer.name + L" Wins!";
2409 }
2410
2411 currentGameState = GAME_OVER;
2412 }*/
2413
2414 /*void CheckGameOverConditions(bool eightBallPocketed, bool cueBallPocketed)
2415 {
2416 if (!eightBallPocketed) return;
2417
2418 PlayerInfo& shooter = (currentPlayer == 1) ? player1Info : player2Info;
2419 PlayerInfo& opponent = (currentPlayer == 1) ? player2Info : player1Info;
2420 // Which pocket did we call?
2421 int called = (currentPlayer == 1) ? calledPocketP1 : calledPocketP2;
2422 // Which pocket did the ball really fall into?
2423 int actual = lastEightBallPocketIndex;
2424
2425 // Legal victory only if:
2426 // 1) Shooter had already pocketed 7 of their object balls,
2427 // 2) They called a pocket,
2428 // 3) The 8?ball actually fell into that same pocket,
2429 // 4) They did not scratch on the 8?ball.
2430 bool legal =
2431 (shooter.ballsPocketedCount >= 7) &&
2432 (called >= 0) &&
2433 (called == actual) &&
2434 (!cueBallPocketed);
2435
2436 if (legal) {
2437 gameOverMessage = shooter.name + L" Wins! "
2438 L"(called: " + std::to_wstring(called) +
2439 L", actual: " + std::to_wstring(actual) + L")";
2440 }
2441 else {
2442 gameOverMessage = opponent.name + L" Wins! (illegal 8-ball) "
2443 // For debugging you can append:
2444 + L" (called: " + std::to_wstring(called)
2445 + L", actual: " + std::to_wstring(actual) + L")";
2446 }
2447
2448 currentGameState = GAME_OVER;
2449 }*/
2450
2451 // ????????????????????????????????????????????????????????????????
2452 // CheckGameOverConditions()
2453 // – Called when the 8-ball has fallen.
2454 // – Decides who wins and builds the gameOverMessage.
2455 // ????????????????????????????????????????????????????????????????
2456 /*void CheckGameOverConditions(bool eightBallPocketed, bool cueBallPocketed)
2457 {
2458 if (!eightBallPocketed) return; // safety
2459
2460 PlayerInfo& shooter = (currentPlayer == 1) ? player1Info : player2Info;
2461 PlayerInfo& opponent = (currentPlayer == 1) ? player2Info : player1Info;
2462
2463 int calledPocket = (currentPlayer == 1) ? calledPocketP1 : calledPocketP2;
2464 int actualPocket = lastEightBallPocketIndex;
2465
2466 bool clearedSeven = (shooter.ballsPocketedCount >= 7);
2467 bool noScratch = !cueBallPocketed;
2468 bool callMade = (calledPocket >= 0);
2469
2470 // helper ? turn “-1” into "None" for readability
2471 auto pocketToStr = [](int idx) -> std::wstring
2472 {
2473 return (idx >= 0) ? std::to_wstring(idx) : L"None";
2474 };
2475
2476 if (clearedSeven && noScratch && callMade && actualPocket == calledPocket)
2477 {
2478 // legitimate win
2479 gameOverMessage =
2480 shooter.name +
2481 L" Wins! (Called pocket: " + pocketToStr(calledPocket) +
2482 L", Actual pocket: " + pocketToStr(actualPocket) + L")";
2483 }
2484 else
2485 {
2486 // wrong pocket, scratch, or early 8-ball
2487 gameOverMessage =
2488 opponent.name +
2489 L" Wins! (Called pocket: " + pocketToStr(calledPocket) +
2490 L", Actual pocket: " + pocketToStr(actualPocket) + L")";
2491 }
2492
2493 currentGameState = GAME_OVER;
2494 }*/
2495
2496 /* void CheckGameOverConditions(bool eightBallPocketed, bool cueBallPocketed) {
2497 if (!eightBallPocketed) return; // Only when 8-ball actually pocketed
2498
2499 PlayerInfo& shooter = (currentPlayer == 1) ? player1Info : player2Info;
2500 PlayerInfo& opponent = (currentPlayer == 1) ? player2Info : player1Info;
2501 bool onEightRoll = IsPlayerOnEightBall(currentPlayer);
2502 int calledPocket = (currentPlayer == 1) ? calledPocketP1 : calledPocketP2;
2503 int actualPocket = -1;
2504 Ball* bEight = GetBallById(8);
2505
2506 // locate which hole the 8-ball went into
2507 if (bEight) {
2508 for (int i = 0; i < 6; ++i) {
2509 if (GetDistanceSq(bEight->x, bEight->y,
2510 pocketPositions[i].x, pocketPositions[i].y)
2511 < POCKET_RADIUS * POCKET_RADIUS * 1.5f) {
2512 actualPocket = i; break;
2513 }
2514 }
2515 }
2516
2517 // 1) On break / pre-assignment: re-spot & continue
2518 if (player1Info.assignedType == BallType::NONE) {
2519 if (bEight) {
2520 bEight->isPocketed = false;
2521 bEight->x = RACK_POS_X; bEight->y = RACK_POS_Y;
2522 bEight->vx = bEight->vy = 0;
2523 }
2524 if (cueBallPocketed) foulCommitted = true;
2525 return;
2526 }
2527
2528 // 2) Loss if pocketed 8 early
2529 if (!onEightRoll) {
2530 gameOverMessage = opponent.name + L" Wins! (" + shooter.name + L" pocketed 8-ball early)";
2531 }
2532 // 3) Loss if scratched
2533 else if (cueBallPocketed) {
2534 gameOverMessage = opponent.name + L" Wins! (" + shooter.name + L" scratched on 8-ball)";
2535 }
2536 // 4) Loss if no pocket call
2537 else if (calledPocket < 0) {
2538 gameOverMessage = opponent.name + L" Wins! (" + shooter.name + L" did not call a pocket)";
2539 }
2540 // 5) Loss if in wrong pocket
2541 else if (actualPocket != calledPocket) {
2542 gameOverMessage = opponent.name + L" Wins! (" + shooter.name + L" 8-ball in wrong pocket)";
2543 }
2544 // 6) Otherwise, valid win
2545 else {
2546 gameOverMessage = shooter.name + L" Wins!";
2547 }
2548
2549 currentGameState = GAME_OVER;
2550 } */
2551
2552
2553 // Switch the shooter, handle fouls and decide what state we go to next.
2554 // ────────────────────────────────────────────────────────────────
2555 // SwitchTurns – final version (arrow–leak bug fixed)
2556 // ────────────────────────────────────────────────────────────────
2557 void SwitchTurns()
2558 {
2559 /* --------------------------------------------------------- */
2560 /* 1. Hand the table over to the other player */
2561 /* --------------------------------------------------------- */
2562 currentPlayer = (currentPlayer == 1) ? 2 : 1;
2563
2564 /* --------------------------------------------------------- */
2565 /* 2. Generic per–turn resets */
2566 /* --------------------------------------------------------- */
2567 isAiming = false;
2568 shotPower = 0.0f;
2569 currentlyHoveredPocket = -1;
2570
2571 /* --------------------------------------------------------- */
2572 /* 3. Wipe every previous pocket call */
2573 /* (the new shooter will choose again if needed) */
2574 /* --------------------------------------------------------- */
2575 calledPocketP1 = -1;
2576 calledPocketP2 = -1;
2577 pocketCallMessage.clear();
2578
2579 /* --------------------------------------------------------- */
2580 /* 4. Handle fouls — cue-ball in hand overrides everything */
2581 /* --------------------------------------------------------- */
2582 if (foulCommitted)
2583 {
2584 if (currentPlayer == 1) // human
2585 {
2586 currentGameState = BALL_IN_HAND_P1;
2587 aiTurnPending = false;
2588 }
2589 else // P2
2590 {
2591 currentGameState = BALL_IN_HAND_P2;
2592 aiTurnPending = isPlayer2AI; // AI will place cue-ball
2593 }
2594
2595 foulCommitted = false;
2596 return; // we're done for this frame
2597 }
2598
2599 /* --------------------------------------------------------- */
2600 /* 5. Normal flow */
2601 /* Will put us in ∘ PLAYER?_TURN */
2602 /* ∘ CHOOSING_POCKET_P? */
2603 /* ∘ AI_THINKING (for CPU) */
2604 /* --------------------------------------------------------- */
2605 CheckAndTransitionToPocketChoice(currentPlayer);
2606 }
2607
2608
2609 void AIBreakShot() {
2610 Ball* cueBall = GetCueBall();
2611 if (!cueBall) return;
2612
2613 // This function is called when it's AI's turn for the opening break and state is PRE_BREAK_PLACEMENT.
2614 // AI will place the cue ball and then plan the shot.
2615 if (isOpeningBreakShot && currentGameState == PRE_BREAK_PLACEMENT) {
2616 // Place cue ball in the kitchen randomly
2617 /*float kitchenMinX = TABLE_LEFT + BALL_RADIUS; // [cite: 1071, 1072, 1587]
2618 float kitchenMaxX = HEADSTRING_X - BALL_RADIUS; // [cite: 1072, 1078, 1588]
2619 float kitchenMinY = TABLE_TOP + BALL_RADIUS; // [cite: 1071, 1072, 1588]
2620 float kitchenMaxY = TABLE_BOTTOM - BALL_RADIUS; // [cite: 1072, 1073, 1589]*/
2621
2622 // --- AI Places Cue Ball for Break ---
2623 // Decide if placing center or side. For simplicity, let's try placing slightly off-center
2624 // towards one side for a more angled break, or center for direct apex hit.
2625 // A common strategy is to hit the second ball of the rack.
2626
2627 float placementY = RACK_POS_Y; // Align vertically with the rack center
2628 float placementX;
2629
2630 // Randomly choose a side or center-ish placement for variation.
2631 int placementChoice = rand() % 3; // 0: Left-ish, 1: Center-ish, 2: Right-ish in kitchen
2632
2633 if (placementChoice == 0) { // Left-ish
2634 placementX = HEADSTRING_X - (TABLE_WIDTH * 0.05f) - (BALL_RADIUS * (1 + (rand() % 3))); // Place slightly to the left within kitchen
2635 }
2636 else if (placementChoice == 2) { // Right-ish
2637 placementX = HEADSTRING_X - (TABLE_WIDTH * 0.05f) + (BALL_RADIUS * (1 + (rand() % 3))); // Place slightly to the right within kitchen
2638 }
2639 else { // Center-ish
2640 placementX = TABLE_LEFT + (HEADSTRING_X - TABLE_LEFT) * 0.5f; // Roughly center of kitchen
2641 }
2642 placementX = std::max(TABLE_LEFT + BALL_RADIUS + 1.0f, std::min(placementX, HEADSTRING_X - BALL_RADIUS - 1.0f)); // Clamp within kitchen X
2643
2644 bool validPos = false;
2645 int attempts = 0;
2646 while (!validPos && attempts < 100) {
2647 /*cueBall->x = kitchenMinX + static_cast<float>(rand()) / (static_cast<float>(RAND_MAX) / (kitchenMaxX - kitchenMinX)); // [cite: 1589]
2648 cueBall->y = kitchenMinY + static_cast<float>(rand()) / (static_cast<float>(RAND_MAX) / (kitchenMaxY - kitchenMinY)); // [cite: 1590]
2649 if (IsValidCueBallPosition(cueBall->x, cueBall->y, true)) { // [cite: 1591]
2650 validPos = true; // [cite: 1591]*/
2651 // Try the chosen X, but vary Y slightly to find a clear spot
2652 cueBall->x = placementX;
2653 cueBall->y = placementY + (static_cast<float>(rand() % 100 - 50) / 100.0f) * BALL_RADIUS * 2.0f; // Vary Y a bit
2654 cueBall->y = std::max(TABLE_TOP + BALL_RADIUS + 1.0f, std::min(cueBall->y, TABLE_BOTTOM - BALL_RADIUS - 1.0f)); // Clamp Y
2655
2656 if (IsValidCueBallPosition(cueBall->x, cueBall->y, true /* behind headstring */)) {
2657 validPos = true;
2658 }
2659 attempts++; // [cite: 1592]
2660 }
2661 if (!validPos) {
2662 // Fallback position
2663 /*cueBall->x = TABLE_LEFT + (HEADSTRING_X - TABLE_LEFT) * 0.5f; // [cite: 1071, 1078, 1593]
2664 cueBall->y = (TABLE_TOP + TABLE_BOTTOM) * 0.5f; // [cite: 1071, 1073, 1594]
2665 if (!IsValidCueBallPosition(cueBall->x, cueBall->y, true)) { // [cite: 1594]
2666 cueBall->x = HEADSTRING_X - BALL_RADIUS * 2; // [cite: 1072, 1078, 1594]
2667 cueBall->y = RACK_POS_Y; // [cite: 1080, 1595]
2668 }
2669 }
2670 cueBall->vx = 0; // [cite: 1595]
2671 cueBall->vy = 0; // [cite: 1596]
2672
2673 // Plan a break shot: aim at the center of the rack (apex ball)
2674 float targetX = RACK_POS_X; // [cite: 1079] Aim for the apex ball X-coordinate
2675 float targetY = RACK_POS_Y; // [cite: 1080] Aim for the apex ball Y-coordinate
2676
2677 float dx = targetX - cueBall->x; // [cite: 1599]
2678 float dy = targetY - cueBall->y; // [cite: 1600]
2679 float shotAngle = atan2f(dy, dx); // [cite: 1600]
2680 float shotPowerValue = MAX_SHOT_POWER; // [cite: 1076, 1600] Use MAX_SHOT_POWER*/
2681
2682 cueBall->x = TABLE_LEFT + (HEADSTRING_X - TABLE_LEFT) * 0.75f; // A default safe spot in kitchen
2683 cueBall->y = RACK_POS_Y;
2684 }
2685 cueBall->vx = 0; cueBall->vy = 0;
2686
2687 // --- AI Plans the Break Shot ---
2688 float targetX, targetY;
2689 // If cue ball is near center of kitchen width, aim for apex.
2690 // Otherwise, aim for the second ball on the side the cue ball is on (for a cut break).
2691 float kitchenCenterRegion = (HEADSTRING_X - TABLE_LEFT) * 0.3f; // Define a "center" region
2692 if (std::abs(cueBall->x - (TABLE_LEFT + (HEADSTRING_X - TABLE_LEFT) / 2.0f)) < kitchenCenterRegion / 2.0f) {
2693 // Center-ish placement: Aim for the apex ball (ball ID 1 or first ball in rack)
2694 targetX = RACK_POS_X; // Apex ball X
2695 targetY = RACK_POS_Y; // Apex ball Y
2696 }
2697 else {
2698 // Side placement: Aim to hit the "second" ball of the rack for a wider spread.
2699 // This is a simplification. A more robust way is to find the actual second ball.
2700 // For now, aim slightly off the apex towards the side the cue ball is on.
2701 targetX = RACK_POS_X + BALL_RADIUS * 2.0f * 0.866f; // X of the second row of balls
2702 targetY = RACK_POS_Y + ((cueBall->y > RACK_POS_Y) ? -BALL_RADIUS : BALL_RADIUS); // Aim at the upper or lower of the two second-row balls
2703 }
2704
2705 float dx = targetX - cueBall->x;
2706 float dy = targetY - cueBall->y;
2707 float shotAngle = atan2f(dy, dx);
2708 float shotPowerValue = MAX_SHOT_POWER * (0.9f + (rand() % 11) / 100.0f); // Slightly vary max power
2709
2710 // Store planned shot details for the AI
2711 /*aiPlannedShotDetails.angle = shotAngle; // [cite: 1102, 1601]
2712 aiPlannedShotDetails.power = shotPowerValue; // [cite: 1102, 1601]
2713 aiPlannedShotDetails.spinX = 0.0f; // [cite: 1102, 1601] No spin for a standard power break
2714 aiPlannedShotDetails.spinY = 0.0f; // [cite: 1103, 1602]
2715 aiPlannedShotDetails.isValid = true; // [cite: 1103, 1602]*/
2716
2717 aiPlannedShotDetails.angle = shotAngle;
2718 aiPlannedShotDetails.power = shotPowerValue;
2719 aiPlannedShotDetails.spinX = 0.0f; // No spin for break usually
2720 aiPlannedShotDetails.spinY = 0.0f;
2721 aiPlannedShotDetails.isValid = true;
2722
2723 // Update global cue parameters for immediate visual feedback if DrawAimingAids uses them
2724 /*::cueAngle = aiPlannedShotDetails.angle; // [cite: 1109, 1603] Update global cueAngle
2725 ::shotPower = aiPlannedShotDetails.power; // [cite: 1109, 1604] Update global shotPower
2726 ::cueSpinX = aiPlannedShotDetails.spinX; // [cite: 1109]
2727 ::cueSpinY = aiPlannedShotDetails.spinY; // [cite: 1110]*/
2728
2729 ::cueAngle = aiPlannedShotDetails.angle;
2730 ::shotPower = aiPlannedShotDetails.power;
2731 ::cueSpinX = aiPlannedShotDetails.spinX;
2732 ::cueSpinY = aiPlannedShotDetails.spinY;
2733
2734 // Set up for AI display via GameUpdate
2735 /*aiIsDisplayingAim = true; // [cite: 1104] Enable AI aiming visualization
2736 aiAimDisplayFramesLeft = AI_AIM_DISPLAY_DURATION_FRAMES; // [cite: 1105] Set duration for display
2737
2738 currentGameState = AI_THINKING; // [cite: 1081] Transition to AI_THINKING state.
2739 // GameUpdate will handle the aiAimDisplayFramesLeft countdown
2740 // and then execute the shot using aiPlannedShotDetails.
2741 // isOpeningBreakShot will be set to false within ApplyShot.
2742
2743 // No immediate ApplyShot or sound here; GameUpdate's AI execution logic will handle it.*/
2744
2745 aiIsDisplayingAim = true;
2746 aiAimDisplayFramesLeft = AI_AIM_DISPLAY_DURATION_FRAMES;
2747 currentGameState = AI_THINKING; // State changes to AI_THINKING, GameUpdate will handle shot execution after display
2748 aiTurnPending = false;
2749
2750 return; // The break shot is now planned and will be executed by GameUpdate
2751 }
2752
2753 // 2. If not in PRE_BREAK_PLACEMENT (e.g., if this function were called at other times,
2754 // though current game logic only calls it for PRE_BREAK_PLACEMENT)
2755 // This part can be extended if AIBreakShot needs to handle other scenarios.
2756 // For now, the primary logic is above.
2757 }
2758
2759 // --- Helper Functions ---
2760
2761 Ball* GetBallById(int id) {
2762 for (size_t i = 0; i < balls.size(); ++i) {
2763 if (balls[i].id == id) {
2764 return &balls[i];
2765 }
2766 }
2767 return nullptr;
2768 }
2769
2770 Ball* GetCueBall() {
2771 return GetBallById(0);
2772 }
2773
2774 float GetDistance(float x1, float y1, float x2, float y2) {
2775 return sqrtf(GetDistanceSq(x1, y1, x2, y2));
2776 }
2777
2778 float GetDistanceSq(float x1, float y1, float x2, float y2) {
2779 float dx = x2 - x1;
2780 float dy = y2 - y1;
2781 return dx * dx + dy * dy;
2782 }
2783
2784 bool IsValidCueBallPosition(float x, float y, bool checkHeadstring) {
2785 // Basic bounds check (inside cushions)
2786 float left = TABLE_LEFT + CUSHION_THICKNESS + BALL_RADIUS;
2787 float right = TABLE_RIGHT - CUSHION_THICKNESS - BALL_RADIUS;
2788 float top = TABLE_TOP + CUSHION_THICKNESS + BALL_RADIUS;
2789 float bottom = TABLE_BOTTOM - CUSHION_THICKNESS - BALL_RADIUS;
2790
2791 if (x < left || x > right || y < top || y > bottom) {
2792 return false;
2793 }
2794
2795 // Check headstring restriction if needed
2796 if (checkHeadstring && x >= HEADSTRING_X) {
2797 return false;
2798 }
2799
2800 // Check overlap with other balls
2801 for (size_t i = 0; i < balls.size(); ++i) {
2802 if (balls[i].id != 0 && !balls[i].isPocketed) { // Don't check against itself or pocketed balls
2803 if (GetDistanceSq(x, y, balls[i].x, balls[i].y) < (BALL_RADIUS * 2.0f) * (BALL_RADIUS * 2.0f)) {
2804 return false; // Overlapping another ball
2805 }
2806 }
2807 }
2808
2809 return true;
2810 }
2811
2812 // --- NEW HELPER FUNCTION IMPLEMENTATIONS ---
2813
2814 // Checks if a player has pocketed all their balls and is now on the 8-ball.
2815 bool IsPlayerOnEightBall(int player) {
2816 PlayerInfo& playerInfo = (player == 1) ? player1Info : player2Info;
2817 if (playerInfo.assignedType != BallType::NONE && playerInfo.assignedType != BallType::EIGHT_BALL && playerInfo.ballsPocketedCount >= 7) {
2818 Ball* eightBall = GetBallById(8);
2819 return (eightBall && !eightBall->isPocketed);
2820 }
2821 return false;
2822 }
2823
2824 void CheckAndTransitionToPocketChoice(int playerID) {
2825 bool needsToCall = IsPlayerOnEightBall(playerID);
2826
2827 if (needsToCall) {
2828 if (playerID == 1) { // Human Player 1
2829 currentGameState = CHOOSING_POCKET_P1;
2830 pocketCallMessage = player1Info.name + L": Choose a pocket for the 8-Ball...";
2831 if (calledPocketP1 == -1) calledPocketP1 = 2; // Default to bottom-right
2832 }
2833 else { // Player 2
2834 if (isPlayer2AI) {
2835 // FOOLPROOF FIX: AI doesn't choose here. It transitions to a thinking state.
2836 // AIMakeDecision will handle the choice and the pocket call.
2837 currentGameState = AI_THINKING;
2838 aiTurnPending = true; // Signal the main loop to run AIMakeDecision
2839 }
2840 else { // Human Player 2
2841 currentGameState = CHOOSING_POCKET_P2;
2842 pocketCallMessage = player2Info.name + L": Choose a pocket for the 8-Ball...";
2843 if (calledPocketP2 == -1) calledPocketP2 = 2; // Default to bottom-right
2844 }
2845 }
2846 }
2847 else {
2848 // Player does not need to call a pocket, proceed to normal turn.
2849 pocketCallMessage = L"";
2850 currentGameState = (playerID == 1) ? PLAYER1_TURN : PLAYER2_TURN;
2851 if (playerID == 2 && isPlayer2AI) {
2852 aiTurnPending = true;
2853 }
2854 }
2855 }
2856
2857
2858 template <typename T>
2859 void SafeRelease(T** ppT) {
2860 if (*ppT) {
2861 (*ppT)->Release();
2862 *ppT = nullptr;
2863 }
2864 }
2865
2866 // --- CPU Ball?in?Hand Placement --------------------------------
2867 // Moves the cue ball to a legal "ball in hand" position for the AI.
2868 void AIPlaceCueBall() {
2869 Ball* cue = GetCueBall();
2870 if (!cue) return;
2871
2872 // Simple strategy: place back behind the headstring at the standard break spot
2873 cue->x = TABLE_LEFT + TABLE_WIDTH * 0.15f;
2874 cue->y = RACK_POS_Y;
2875 cue->vx = cue->vy = 0.0f;
2876 }
2877
2878 // --- Helper Function for Line Segment Intersection ---
2879 // Finds intersection point of line segment P1->P2 and line segment P3->P4
2880 // Returns true if they intersect, false otherwise. Stores intersection point in 'intersection'.
2881 bool LineSegmentIntersection(D2D1_POINT_2F p1, D2D1_POINT_2F p2, D2D1_POINT_2F p3, D2D1_POINT_2F p4, D2D1_POINT_2F& intersection)
2882 {
2883 float denominator = (p4.y - p3.y) * (p2.x - p1.x) - (p4.x - p3.x) * (p2.y - p1.y);
2884
2885 // Check if lines are parallel or collinear
2886 if (fabs(denominator) < 1e-6) {
2887 return false;
2888 }
2889
2890 float ua = ((p4.x - p3.x) * (p1.y - p3.y) - (p4.y - p3.y) * (p1.x - p3.x)) / denominator;
2891 float ub = ((p2.x - p1.x) * (p1.y - p3.y) - (p2.y - p1.y) * (p1.x - p3.x)) / denominator;
2892
2893 // Check if intersection point lies on both segments
2894 if (ua >= 0.0f && ua <= 1.0f && ub >= 0.0f && ub <= 1.0f) {
2895 intersection.x = p1.x + ua * (p2.x - p1.x);
2896 intersection.y = p1.y + ua * (p2.y - p1.y);
2897 return true;
2898 }
2899
2900 return false;
2901 }
2902
2903 // --- INSERT NEW HELPER FUNCTION HERE ---
2904 // Calculates the squared distance from point P to the line segment AB.
2905 float PointToLineSegmentDistanceSq(D2D1_POINT_2F p, D2D1_POINT_2F a, D2D1_POINT_2F b) {
2906 float l2 = GetDistanceSq(a.x, a.y, b.x, b.y);
2907 if (l2 == 0.0f) return GetDistanceSq(p.x, p.y, a.x, a.y); // Segment is a point
2908 // Consider P projecting onto the line AB infinite line
2909 // t = [(P-A) . (B-A)] / |B-A|^2
2910 float t = ((p.x - a.x) * (b.x - a.x) + (p.y - a.y) * (b.y - a.y)) / l2;
2911 t = std::max(0.0f, std::min(1.0f, t)); // Clamp t to the segment [0, 1]
2912 // Projection falls on the segment
2913 D2D1_POINT_2F projection = D2D1::Point2F(a.x + t * (b.x - a.x), a.y + t * (b.y - a.y));
2914 return GetDistanceSq(p.x, p.y, projection.x, projection.y);
2915 }
2916 // --- End New Helper ---
2917
2918 // --- NEW AI Implementation Functions ---
2919
2920 void AIMakeDecision() {
2921 // Start with a clean slate for the AI's plan.
2922 aiPlannedShotDetails.isValid = false;
2923 Ball* cueBall = GetCueBall();
2924 if (!cueBall || !isPlayer2AI || currentPlayer != 2) return;
2925
2926 // Ask the "expert" (AIFindBestShot) for the best possible shot.
2927 AIShotInfo bestShot = AIFindBestShot();
2928
2929 if (bestShot.possible) {
2930 // A good shot was found.
2931 // If it's an 8-ball shot, "call" the pocket.
2932 if (bestShot.involves8Ball) {
2933 calledPocketP2 = bestShot.pocketIndex;
2934 }
2935 else {
2936 calledPocketP2 = -1; // Ensure no pocket is called on a normal shot.
2937 }
2938
2939 // Commit the details of the best shot to the AI's plan.
2940 aiPlannedShotDetails.angle = bestShot.angle;
2941 aiPlannedShotDetails.power = bestShot.power;
2942 aiPlannedShotDetails.spinX = bestShot.spinX;
2943 aiPlannedShotDetails.spinY = bestShot.spinY;
2944 aiPlannedShotDetails.isValid = true;
2945
2946 }
2947 else {
2948 // No good offensive shot found, must play a safe defensive shot.
2949 // (This is a fallback and your current AIFindBestShot should prevent this)
2950 aiPlannedShotDetails.isValid = false;
2951 }
2952
2953 // --- FOOLPROOF FIX: Trigger the Aim Display ---
2954 // If any valid plan was made, update the visuals and start the display pause.
2955 if (aiPlannedShotDetails.isValid) {
2956
2957 // STEP 1: Copy the AI's plan into the global variables used for drawing.
2958 // This is the critical missing link.
2959 cueAngle = aiPlannedShotDetails.angle;
2960 shotPower = aiPlannedShotDetails.power;
2961
2962 // STEP 2: Trigger the visual display pause.
2963 // These are the two lines you correctly identified.
2964 aiIsDisplayingAim = true;
2965 aiAimDisplayFramesLeft = AI_AIM_DISPLAY_DURATION_FRAMES;
2966
2967 }
2968 else {
2969 // Absolute fallback: If no plan could be made, switch turns to prevent a freeze.
2970 SwitchTurns();
2971 }
2972 }
2973
2974
2975 AIShotInfo AIFindBestShot()
2976 {
2977 AIShotInfo best; // .possible == false
2978 Ball* cue = GetCueBall();
2979 if (!cue) return best;
2980
2981 const bool on8 = IsPlayerOnEightBall(2);
2982 const BallType wantType = player2Info.assignedType;
2983
2984 for (Ball& b : balls)
2985 {
2986 if (b.isPocketed || b.id == 0) continue;
2987
2988 // decide if this ball is a legal/interesting target
2989 bool ok =
2990 on8 ? (b.id == 8) :
2991 ((wantType == BallType::NONE) || (b.type == wantType));
2992
2993 if (!ok) continue;
2994
2995 for (int p = 0; p < 6; ++p)
2996 {
2997 AIShotInfo cand = EvaluateShot(&b, p);
2998 if (cand.possible &&
2999 (!best.possible || cand.score > best.score))
3000 best = cand;
3001 }
3002 }
3003
3004 // fall-back: tap cue ball forward (safety) if no potting line exists
3005 if (!best.possible && cue)
3006 {
3007 best.possible = true;
3008 best.angle = static_cast<float>(rand()) / RAND_MAX * 2.0f * PI;
3009 best.power = MAX_SHOT_POWER * 0.30f;
3010 best.spinX = best.spinY = 0.0f;
3011 best.targetBall = nullptr;
3012 best.score = -99999.0f;
3013 best.pocketIndex = -1;
3014 }
3015 return best;
3016 }
3017
3018
3019 // Evaluate a potential shot at a specific target ball towards a specific pocket
3020 AIShotInfo EvaluateShot(Ball* targetBall, int pocketIndex) {
3021 AIShotInfo shotInfo; // Defaults to not possible
3022 shotInfo.targetBall = targetBall;
3023 shotInfo.pocketIndex = pocketIndex;
3024 shotInfo.involves8Ball = (targetBall && targetBall->id == 8);
3025
3026 Ball* cueBall = GetCueBall();
3027 if (!cueBall || !targetBall) return shotInfo;
3028
3029 // 1. Calculate Ghost Ball position (where cue must hit target)
3030 shotInfo.ghostBallPos = CalculateGhostBallPos(targetBall, pocketIndex);
3031
3032 // 2. Check Path: Cue Ball -> Ghost Ball Position
3033 if (!IsPathClear(D2D1::Point2F(cueBall->x, cueBall->y), shotInfo.ghostBallPos, cueBall->id, targetBall->id)) {
3034 return shotInfo; // Path blocked, shot is impossible.
3035 }
3036
3037 // 3. Calculate Angle and Power
3038 float dx = shotInfo.ghostBallPos.x - cueBall->x;
3039 float dy = shotInfo.ghostBallPos.y - cueBall->y;
3040 shotInfo.angle = atan2f(dy, dx);
3041
3042 float cueToGhostDist = GetDistance(cueBall->x, cueBall->y, shotInfo.ghostBallPos.x, shotInfo.ghostBallPos.y);
3043 float targetToPocketDist = GetDistance(targetBall->x, targetBall->y, pocketPositions[pocketIndex].x, pocketPositions[pocketIndex].y);
3044 shotInfo.power = CalculateShotPower(cueToGhostDist, targetToPocketDist);
3045
3046 // 4. Score the shot (simple scoring: closer and straighter is better)
3047 shotInfo.score = 1000.0f - (cueToGhostDist + targetToPocketDist);
3048
3049 // If we reached here, the shot is geometrically possible.
3050 shotInfo.possible = true;
3051 return shotInfo;
3052 }
3053
3054
3055 // Estimate the power that will carry the cue-ball to the ghost position
3056 // *and* push the object-ball the remaining distance to the pocket.
3057 //
3058 // • cueToGhostDist – pixels from cue to ghost-ball centre
3059 // • targetToPocketDist– pixels from object-ball to chosen pocket
3060 //
3061 // The function is fully deterministic (good for AI search) yet produces
3062 // human-looking power levels.
3063 //
3064 float CalculateShotPower(float cueToGhostDist, float targetToPocketDist)
3065 {
3066 // Total distance the *energy* must cover (cue path + object-ball path)
3067 float totalDist = cueToGhostDist + targetToPocketDist;
3068
3069 // Typical diagonal of the playable area (approx.) – used for scaling
3070 constexpr float TABLE_DIAG = 900.0f;
3071
3072 // 1. Convert distance to a 0-1 number (0: tap-in, 1: table length)
3073 float norm = std::clamp(totalDist / TABLE_DIAG, 0.0f, 1.0f);
3074
3075 // 2. Ease-in curve (smoothstep) for nicer progression
3076 norm = norm * norm * (3.0f - 2.0f * norm);
3077
3078 // 3. Blend between a gentle minimum and the absolute maximum
3079 const float MIN_POWER = MAX_SHOT_POWER * 0.18f; // just enough to move
3080 float power = MIN_POWER + norm * (MAX_SHOT_POWER - MIN_POWER);
3081
3082 // 4. Safety clamp (also screens out degenerate calls)
3083 power = std::clamp(power, 0.15f, MAX_SHOT_POWER);
3084
3085 return power;
3086 }
3087
3088 // ------------------------------------------------------------------
3089 // Return the ghost-ball centre needed for the target ball to roll
3090 // straight into the chosen pocket.
3091 // ------------------------------------------------------------------
3092 D2D1_POINT_2F CalculateGhostBallPos(Ball* targetBall, int pocketIndex)
3093 {
3094 if (!targetBall) return D2D1::Point2F(0, 0);
3095
3096 D2D1_POINT_2F P = pocketPositions[pocketIndex];
3097
3098 float vx = P.x - targetBall->x;
3099 float vy = P.y - targetBall->y;
3100 float L = sqrtf(vx * vx + vy * vy);
3101 if (L < 1.0f) L = 1.0f; // safety
3102
3103 vx /= L; vy /= L;
3104
3105 return D2D1::Point2F(
3106 targetBall->x - vx * (BALL_RADIUS * 2.0f),
3107 targetBall->y - vy * (BALL_RADIUS * 2.0f));
3108 }
3109
3110 // Calculate the position the cue ball needs to hit for the target ball to go towards the pocket
3111 // ────────────────────────────────────────────────────────────────
3112 // 2. Shot evaluation & search
3113 // ────────────────────────────────────────────────────────────────
3114
3115 // Calculate ghost-ball position so that cue hits target towards pocket
3116 static inline D2D1_POINT_2F GhostPos(const Ball* tgt, int pocketIdx)
3117 {
3118 D2D1_POINT_2F P = pocketPositions[pocketIdx];
3119 float vx = P.x - tgt->x;
3120 float vy = P.y - tgt->y;
3121 float L = sqrtf(vx * vx + vy * vy);
3122 vx /= L; vy /= L;
3123 return D2D1::Point2F(tgt->x - vx * (BALL_RADIUS * 2.0f),
3124 tgt->y - vy * (BALL_RADIUS * 2.0f));
3125 }
3126
3127 // Heuristic: shorter + straighter + proper group = higher score
3128 static inline float ScoreShot(float cue2Ghost,
3129 float tgt2Pocket,
3130 bool correctGroup,
3131 bool involves8)
3132 {
3133 float base = 2000.0f - (cue2Ghost + tgt2Pocket); // prefer close shots
3134 if (!correctGroup) base -= 400.0f; // penalty
3135 if (involves8) base += 150.0f; // a bit more desirable
3136 return base;
3137 }
3138
3139 // Checks if line segment is clear of obstructing balls
3140 // ────────────────────────────────────────────────────────────────
3141 // 1. Low-level helpers – IsPathClear & FindFirstHitBall
3142 // ────────────────────────────────────────────────────────────────
3143
3144 // Test if the capsule [ start … end ] (radius = BALL_RADIUS)
3145 // intersects any ball except the ids we want to ignore.
3146 bool IsPathClear(D2D1_POINT_2F start,
3147 D2D1_POINT_2F end,
3148 int ignoredBallId1,
3149 int ignoredBallId2)
3150 {
3151 float dx = end.x - start.x;
3152 float dy = end.y - start.y;
3153 float lenSq = dx * dx + dy * dy;
3154 if (lenSq < 1e-3f) return true; // degenerate → treat as clear
3155
3156 for (const Ball& b : balls)
3157 {
3158 if (b.isPocketed) continue;
3159 if (b.id == ignoredBallId1 ||
3160 b.id == ignoredBallId2) continue;
3161
3162 // project ball centre onto the segment
3163 float t = ((b.x - start.x) * dx + (b.y - start.y) * dy) / lenSq;
3164 t = std::clamp(t, 0.0f, 1.0f);
3165
3166 float cx = start.x + t * dx;
3167 float cy = start.y + t * dy;
3168
3169 if (GetDistanceSq(b.x, b.y, cx, cy) < (BALL_RADIUS * BALL_RADIUS))
3170 return false; // blocked
3171 }
3172 return true;
3173 }
3174
3175 // Cast an (infinite) ray and return the first non-pocketed ball hit.
3176 // `hitDistSq` is distance² from the start point to the collision point.
3177 Ball* FindFirstHitBall(D2D1_POINT_2F start,
3178 float angle,
3179 float& hitDistSq)
3180 {
3181 Ball* hitBall = nullptr;
3182 float bestSq = std::numeric_limits<float>::max();
3183 float cosA = cosf(angle);
3184 float sinA = sinf(angle);
3185
3186 for (Ball& b : balls)
3187 {
3188 if (b.id == 0 || b.isPocketed) continue; // ignore cue & sunk balls
3189
3190 float relX = b.x - start.x;
3191 float relY = b.y - start.y;
3192 float proj = relX * cosA + relY * sinA; // distance along the ray
3193
3194 if (proj <= 0) continue; // behind cue
3195
3196 // closest approach of the ray to the sphere centre
3197 float closestX = start.x + proj * cosA;
3198 float closestY = start.y + proj * sinA;
3199 float dSq = GetDistanceSq(b.x, b.y, closestX, closestY);
3200
3201 if (dSq <= BALL_RADIUS * BALL_RADIUS) // intersection
3202 {
3203 float back = sqrtf(BALL_RADIUS * BALL_RADIUS - dSq);
3204 float collDist = proj - back; // front surface
3205 float collSq = collDist * collDist;
3206 if (collSq < bestSq)
3207 {
3208 bestSq = collSq;
3209 hitBall = &b;
3210 }
3211 }
3212 }
3213 hitDistSq = bestSq;
3214 return hitBall;
3215 }
3216
3217 // Basic check for reasonable AI aim angles (optional)
3218 bool IsValidAIAimAngle(float angle) {
3219 // Placeholder - could check for NaN or infinity if calculations go wrong
3220 return isfinite(angle);
3221 }
3222
3223 //midi func = start
3224 void PlayMidiInBackground(HWND hwnd, const TCHAR* midiPath) {
3225 while (isMusicPlaying) {
3226 MCI_OPEN_PARMS mciOpen = { 0 };
3227 mciOpen.lpstrDeviceType = TEXT("sequencer");
3228 mciOpen.lpstrElementName = midiPath;
3229
3230 if (mciSendCommand(0, MCI_OPEN, MCI_OPEN_TYPE | MCI_OPEN_ELEMENT, (DWORD_PTR)&mciOpen) == 0) {
3231 midiDeviceID = mciOpen.wDeviceID;
3232
3233 MCI_PLAY_PARMS mciPlay = { 0 };
3234 mciSendCommand(midiDeviceID, MCI_PLAY, 0, (DWORD_PTR)&mciPlay);
3235
3236 // Wait for playback to complete
3237 MCI_STATUS_PARMS mciStatus = { 0 };
3238 mciStatus.dwItem = MCI_STATUS_MODE;
3239
3240 do {
3241 mciSendCommand(midiDeviceID, MCI_STATUS, MCI_STATUS_ITEM, (DWORD_PTR)&mciStatus);
3242 Sleep(100); // adjust as needed
3243 } while (mciStatus.dwReturn == MCI_MODE_PLAY && isMusicPlaying);
3244
3245 mciSendCommand(midiDeviceID, MCI_CLOSE, 0, NULL);
3246 midiDeviceID = 0;
3247 }
3248 }
3249 }
3250
3251 void StartMidi(HWND hwnd, const TCHAR* midiPath) {
3252 if (isMusicPlaying) {
3253 StopMidi();
3254 }
3255 isMusicPlaying = true;
3256 musicThread = std::thread(PlayMidiInBackground, hwnd, midiPath);
3257 }
3258
3259 void StopMidi() {
3260 if (isMusicPlaying) {
3261 isMusicPlaying = false;
3262 if (musicThread.joinable()) musicThread.join();
3263 if (midiDeviceID != 0) {
3264 mciSendCommand(midiDeviceID, MCI_CLOSE, 0, NULL);
3265 midiDeviceID = 0;
3266 }
3267 }
3268 }
3269
3270 /*void PlayGameMusic(HWND hwnd) {
3271 // Stop any existing playback
3272 if (isMusicPlaying) {
3273 isMusicPlaying = false;
3274 if (musicThread.joinable()) {
3275 musicThread.join();
3276 }
3277 if (midiDeviceID != 0) {
3278 mciSendCommand(midiDeviceID, MCI_CLOSE, 0, NULL);
3279 midiDeviceID = 0;
3280 }
3281 }
3282
3283 // Get the path of the executable
3284 TCHAR exePath[MAX_PATH];
3285 GetModuleFileName(NULL, exePath, MAX_PATH);
3286
3287 // Extract the directory path
3288 TCHAR* lastBackslash = _tcsrchr(exePath, '\\');
3289 if (lastBackslash != NULL) {
3290 *(lastBackslash + 1) = '\0';
3291 }
3292
3293 // Construct the full path to the MIDI file
3294 static TCHAR midiPath[MAX_PATH];
3295 _tcscpy_s(midiPath, MAX_PATH, exePath);
3296 _tcscat_s(midiPath, MAX_PATH, TEXT("BSQ.MID"));
3297
3298 // Start the background playback
3299 isMusicPlaying = true;
3300 musicThread = std::thread(PlayMidiInBackground, hwnd, midiPath);
3301 }*/
3302 //midi func = end
3303
3304 // --- Drawing Functions ---
3305
3306 void OnPaint() {
3307 HRESULT hr = CreateDeviceResources(); // Ensure resources are valid
3308
3309 if (SUCCEEDED(hr)) {
3310 pRenderTarget->BeginDraw();
3311 DrawScene(pRenderTarget); // Pass render target
3312 hr = pRenderTarget->EndDraw();
3313
3314 if (hr == D2DERR_RECREATE_TARGET) {
3315 DiscardDeviceResources();
3316 // Optionally request another paint message: InvalidateRect(hwndMain, NULL, FALSE);
3317 // But the timer loop will trigger redraw anyway.
3318 }
3319 }
3320 // If CreateDeviceResources failed, EndDraw might not be called.
3321 // Consider handling this more robustly if needed.
3322 }
3323
3324 void DrawScene(ID2D1RenderTarget* pRT) {
3325 if (!pRT) return;
3326
3327 //pRT->Clear(D2D1::ColorF(D2D1::ColorF::LightGray)); // Background color
3328 // Set background color to #ffffcd (RGB: 255, 255, 205)
3329 pRT->Clear(D2D1::ColorF(0.3686f, 0.5333f, 0.3882f)); // Clear with light yellow background NEWCOLOR 1.0f, 1.0f, 0.803f => (0.3686f, 0.5333f, 0.3882f)
3330 //pRT->Clear(D2D1::ColorF(1.0f, 1.0f, 0.803f)); // Clear with light yellow background NEWCOLOR 1.0f, 1.0f, 0.803f => (0.3686f, 0.5333f, 0.3882f)
3331
3332 DrawTable(pRT, pFactory);
3333 DrawPocketSelectionIndicator(pRT); // Draw arrow over selected/called pocket
3334 DrawBalls(pRT);
3335 DrawAimingAids(pRT); // Includes cue stick if aiming
3336 DrawUI(pRT);
3337 DrawPowerMeter(pRT);
3338 DrawSpinIndicator(pRT);
3339 DrawPocketedBallsIndicator(pRT);
3340 DrawBallInHandIndicator(pRT); // Draw cue ball ghost if placing
3341
3342 // Draw Game Over Message
3343 if (currentGameState == GAME_OVER && pTextFormat) {
3344 ID2D1SolidColorBrush* pBrush = nullptr;
3345 pRT->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::White), &pBrush);
3346 if (pBrush) {
3347 D2D1_RECT_F layoutRect = D2D1::RectF(TABLE_LEFT, TABLE_TOP + TABLE_HEIGHT / 2 - 30, TABLE_RIGHT, TABLE_TOP + TABLE_HEIGHT / 2 + 30);
3348 pRT->DrawText(
3349 gameOverMessage.c_str(),
3350 (UINT32)gameOverMessage.length(),
3351 pTextFormat, // Use large format maybe?
3352 &layoutRect,
3353 pBrush
3354 );
3355 SafeRelease(&pBrush);
3356 }
3357 }
3358
3359 }
3360
3361 void DrawTable(ID2D1RenderTarget* pRT, ID2D1Factory* pFactory) {
3362 ID2D1SolidColorBrush* pBrush = nullptr;
3363
3364 // === Draw Full Orange Frame (Table Border) ===
3365 ID2D1SolidColorBrush* pFrameBrush = nullptr;
3366 pRT->CreateSolidColorBrush(D2D1::ColorF(0.9157f, 0.6157f, 0.2000f), &pFrameBrush); //NEWCOLOR ::Orange (no brackets) => (0.9157, 0.6157, 0.2000)
3367 //pRT->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::Orange), &pFrameBrush); //NEWCOLOR ::Orange (no brackets) => (0.9157, 0.6157, 0.2000)
3368 if (pFrameBrush) {
3369 D2D1_RECT_F outerRect = D2D1::RectF(
3370 TABLE_LEFT - CUSHION_THICKNESS,
3371 TABLE_TOP - CUSHION_THICKNESS,
3372 TABLE_RIGHT + CUSHION_THICKNESS,
3373 TABLE_BOTTOM + CUSHION_THICKNESS
3374 );
3375 pRT->FillRectangle(&outerRect, pFrameBrush);
3376 SafeRelease(&pFrameBrush);
3377 }
3378
3379 // Draw Table Bed (Green Felt)
3380 pRT->CreateSolidColorBrush(TABLE_COLOR, &pBrush);
3381 if (!pBrush) return;
3382 D2D1_RECT_F tableRect = D2D1::RectF(TABLE_LEFT, TABLE_TOP, TABLE_RIGHT, TABLE_BOTTOM);
3383 pRT->FillRectangle(&tableRect, pBrush);
3384 SafeRelease(&pBrush);
3385
3386 // ------------------------------------------------------------------
3387// Spotlight overlay (soft radial inside a rounded rectangle)
3388// ------------------------------------------------------------------
3389 {
3390 // 2.1 Build a radial gradient brush (edge = base cloth, centre = lighter)
3391 ID2D1RadialGradientBrush* pSpot = nullptr;
3392 ID2D1GradientStopCollection* pStops = nullptr;
3393
3394 D2D1_COLOR_F centreClr = D2D1::ColorF(
3395 std::min(1.f, TABLE_COLOR.r * 1.60f), // lighten ~60 %
3396 std::min(1.f, TABLE_COLOR.g * 1.60f),
3397 std::min(1.f, TABLE_COLOR.b * 1.60f));
3398
3399 const D2D1_GRADIENT_STOP gs[3] =
3400 {
3401 { 0.0f, D2D1::ColorF(centreClr.r, centreClr.g, centreClr.b, 0.95f) },
3402 { 0.6f, D2D1::ColorF(TABLE_COLOR.r, TABLE_COLOR.g, TABLE_COLOR.b, 0.55f) },
3403 { 1.0f, D2D1::ColorF(TABLE_COLOR.r, TABLE_COLOR.g, TABLE_COLOR.b, 0.0f) }
3404 };
3405 pRT->CreateGradientStopCollection(gs, 3, &pStops);
3406
3407 if (pStops)
3408 {
3409 D2D1_RECT_F rc = tableRect;
3410 const float PAD = 18.0f; // inset so corners stay dark
3411 rc.left += PAD; rc.top += PAD;
3412 rc.right -= PAD; rc.bottom -= PAD;
3413
3414 // centre point & radii
3415 D2D1_POINT_2F centre = D2D1::Point2F(
3416 (rc.left + rc.right) / 2.0f,
3417 (rc.top + rc.bottom) / 2.0f);
3418
3419 float rx = (rc.right - rc.left) * 0.55f;
3420 float ry = (rc.bottom - rc.top) * 0.55f;
3421
3422 D2D1_RADIAL_GRADIENT_BRUSH_PROPERTIES props =
3423 D2D1::RadialGradientBrushProperties(
3424 centre, // origin
3425 D2D1::Point2F(0, 0), // offset
3426 rx, ry);
3427
3428 pRT->CreateRadialGradientBrush(props, pStops, &pSpot);
3429 pStops->Release();
3430 }
3431
3432 if (pSpot)
3433 {
3434 // Use the same rounded rectangle the pocket bar uses for subtle round corners
3435 const float RADIUS = 20.0f; // corner radius
3436 D2D1_ROUNDED_RECT spotlightRR =
3437 D2D1::RoundedRect(tableRect, RADIUS, RADIUS);
3438
3439 pRT->FillRoundedRectangle(&spotlightRR, pSpot);
3440 pSpot->Release();
3441 }
3442 }
3443
3444 // Draw Cushions (Red Border)
3445 pRT->CreateSolidColorBrush(CUSHION_COLOR, &pBrush);
3446 if (!pBrush) return;
3447 // Top Cushion (split by middle pocket)
3448 pRT->FillRectangle(D2D1::RectF(TABLE_LEFT + HOLE_VISUAL_RADIUS, TABLE_TOP - CUSHION_THICKNESS, TABLE_LEFT + TABLE_WIDTH / 2.f - HOLE_VISUAL_RADIUS, TABLE_TOP), pBrush);
3449 pRT->FillRectangle(D2D1::RectF(TABLE_LEFT + TABLE_WIDTH / 2.f + HOLE_VISUAL_RADIUS, TABLE_TOP - CUSHION_THICKNESS, TABLE_RIGHT - HOLE_VISUAL_RADIUS, TABLE_TOP), pBrush);
3450 // Bottom Cushion (split by middle pocket)
3451 pRT->FillRectangle(D2D1::RectF(TABLE_LEFT + HOLE_VISUAL_RADIUS, TABLE_BOTTOM, TABLE_LEFT + TABLE_WIDTH / 2.f - HOLE_VISUAL_RADIUS, TABLE_BOTTOM + CUSHION_THICKNESS), pBrush);
3452 pRT->FillRectangle(D2D1::RectF(TABLE_LEFT + TABLE_WIDTH / 2.f + HOLE_VISUAL_RADIUS, TABLE_BOTTOM, TABLE_RIGHT - HOLE_VISUAL_RADIUS, TABLE_BOTTOM + CUSHION_THICKNESS), pBrush);
3453 // Left Cushion
3454 pRT->FillRectangle(D2D1::RectF(TABLE_LEFT - CUSHION_THICKNESS, TABLE_TOP + HOLE_VISUAL_RADIUS, TABLE_LEFT, TABLE_BOTTOM - HOLE_VISUAL_RADIUS), pBrush);
3455 // Right Cushion
3456 pRT->FillRectangle(D2D1::RectF(TABLE_RIGHT, TABLE_TOP + HOLE_VISUAL_RADIUS, TABLE_RIGHT + CUSHION_THICKNESS, TABLE_BOTTOM - HOLE_VISUAL_RADIUS), pBrush);
3457 SafeRelease(&pBrush);
3458
3459
3460 // Draw Pockets (Black Circles)
3461 pRT->CreateSolidColorBrush(POCKET_COLOR, &pBrush);
3462 if (!pBrush) return;
3463 for (int i = 0; i < 6; ++i) {
3464 D2D1_ELLIPSE ellipse = D2D1::Ellipse(pocketPositions[i], HOLE_VISUAL_RADIUS, HOLE_VISUAL_RADIUS);
3465 pRT->FillEllipse(&ellipse, pBrush);
3466 }
3467 SafeRelease(&pBrush);
3468
3469 // Draw Headstring Line (White)
3470 pRT->CreateSolidColorBrush(D2D1::ColorF(0.4235f, 0.5647f, 0.1765f, 1.0f), &pBrush); // NEWCOLOR ::White => (0.2784, 0.4549, 0.1843)
3471 //pRT->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::White, 0.5f), &pBrush); // NEWCOLOR ::White => (0.2784, 0.4549, 0.1843)
3472 if (!pBrush) return;
3473 pRT->DrawLine(
3474 D2D1::Point2F(HEADSTRING_X, TABLE_TOP),
3475 D2D1::Point2F(HEADSTRING_X, TABLE_BOTTOM),
3476 pBrush,
3477 1.0f // Line thickness
3478 );
3479 SafeRelease(&pBrush);
3480
3481 // Draw Semicircle facing West (flat side East)
3482 // Draw Semicircle facing East (curved side on the East, flat side on the West)
3483 ID2D1PathGeometry* pGeometry = nullptr;
3484 HRESULT hr = pFactory->CreatePathGeometry(&pGeometry);
3485 if (SUCCEEDED(hr) && pGeometry)
3486 {
3487 ID2D1GeometrySink* pSink = nullptr;
3488 hr = pGeometry->Open(&pSink);
3489 if (SUCCEEDED(hr) && pSink)
3490 {
3491 float radius = 60.0f; // Radius for the semicircle
3492 D2D1_POINT_2F center = D2D1::Point2F(HEADSTRING_X, (TABLE_TOP + TABLE_BOTTOM) / 2.0f);
3493
3494 // For a semicircle facing East (curved side on the East), use the top and bottom points.
3495 D2D1_POINT_2F startPoint = D2D1::Point2F(center.x, center.y - radius); // Top point
3496
3497 pSink->BeginFigure(startPoint, D2D1_FIGURE_BEGIN_HOLLOW);
3498
3499 D2D1_ARC_SEGMENT arc = {};
3500 arc.point = D2D1::Point2F(center.x, center.y + radius); // Bottom point
3501 arc.size = D2D1::SizeF(radius, radius);
3502 arc.rotationAngle = 0.0f;
3503 // Use the correct identifier with the extra underscore:
3504 arc.sweepDirection = D2D1_SWEEP_DIRECTION_COUNTER_CLOCKWISE;
3505 arc.arcSize = D2D1_ARC_SIZE_SMALL;
3506
3507 pSink->AddArc(&arc);
3508 pSink->EndFigure(D2D1_FIGURE_END_OPEN);
3509 pSink->Close();
3510 SafeRelease(&pSink);
3511
3512 ID2D1SolidColorBrush* pArcBrush = nullptr;
3513 //pRT->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::White, 0.3f), &pArcBrush);
3514 pRT->CreateSolidColorBrush(D2D1::ColorF(0.4235f, 0.5647f, 0.1765f, 1.0f), &pArcBrush);
3515 if (pArcBrush)
3516 {
3517 pRT->DrawGeometry(pGeometry, pArcBrush, 1.5f);
3518 SafeRelease(&pArcBrush);
3519 }
3520 }
3521 SafeRelease(&pGeometry);
3522 }
3523
3524
3525
3526
3527 }
3528
3529
3530 // ----------------------------------------------
3531 // Helper : clamp to [0,1] and lighten a colour
3532 // ----------------------------------------------
3533 static D2D1_COLOR_F Lighten(const D2D1_COLOR_F& c, float factor = 1.25f)
3534 {
3535 return D2D1::ColorF(
3536 std::min(1.0f, c.r * factor),
3537 std::min(1.0f, c.g * factor),
3538 std::min(1.0f, c.b * factor),
3539 c.a);
3540 }
3541
3542 // ------------------------------------------------
3543 // NEW DrawBalls – radial-gradient “spot-light”
3544 // ------------------------------------------------
3545 void DrawBalls(ID2D1RenderTarget* pRT)
3546 {
3547 if (!pRT) return;
3548
3549 ID2D1SolidColorBrush* pStripeBrush = nullptr; // white stripe
3550 ID2D1SolidColorBrush* pBorderBrush = nullptr; // black ring
3551
3552 pRT->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::White), &pStripeBrush);
3553 pRT->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::Black), &pBorderBrush);
3554
3555 for (const Ball& b : balls)
3556 {
3557 if (b.isPocketed) continue;
3558
3559 //------------------------------------------
3560 // Build the radial gradient for THIS ball
3561 //------------------------------------------
3562 ID2D1GradientStopCollection* pStops = nullptr;
3563 ID2D1RadialGradientBrush* pRad = nullptr;
3564
3565 D2D1_GRADIENT_STOP gs[3];
3566 gs[0].position = 0.0f; gs[0].color = D2D1::ColorF(1, 1, 1, 0.95f); // bright spot
3567 gs[1].position = 0.35f; gs[1].color = Lighten(b.color); // transitional
3568 gs[2].position = 1.0f; gs[2].color = b.color; // base colour
3569
3570 pRT->CreateGradientStopCollection(gs, 3, &pStops);
3571
3572 if (pStops)
3573 {
3574 // Place the hot-spot slightly towards top-left to look more 3-D
3575 D2D1_POINT_2F origin = D2D1::Point2F(b.x - BALL_RADIUS * 0.4f,
3576 b.y - BALL_RADIUS * 0.4f);
3577
3578 D2D1_RADIAL_GRADIENT_BRUSH_PROPERTIES props =
3579 D2D1::RadialGradientBrushProperties(
3580 origin, // gradientOrigin
3581 D2D1::Point2F(0, 0), // offset (not used here)
3582 BALL_RADIUS * 1.3f, // radiusX
3583 BALL_RADIUS * 1.3f); // radiusY
3584
3585 pRT->CreateRadialGradientBrush(props, pStops, &pRad);
3586 SafeRelease(&pStops);
3587 }
3588
3589 //------------------------------------------
3590 // Draw the solid or striped ball itself
3591 //------------------------------------------
3592 D2D1_ELLIPSE outer = D2D1::Ellipse(
3593 D2D1::Point2F(b.x, b.y), BALL_RADIUS, BALL_RADIUS);
3594
3595 if (pRad) pRT->FillEllipse(&outer, pRad);
3596
3597 // ---------- Stripe overlay -------------
3598 if (b.type == BallType::STRIPE && pStripeBrush)
3599 {
3600 // White band
3601 D2D1_RECT_F stripe = D2D1::RectF(
3602 b.x - BALL_RADIUS,
3603 b.y - BALL_RADIUS * 0.40f,
3604 b.x + BALL_RADIUS,
3605 b.y + BALL_RADIUS * 0.40f);
3606 pRT->FillRectangle(&stripe, pStripeBrush);
3607
3608 // Inner circle (give stripe area same glossy shading)
3609 if (pRad)
3610 {
3611 D2D1_ELLIPSE inner = D2D1::Ellipse(
3612 D2D1::Point2F(b.x, b.y),
3613 BALL_RADIUS * 0.60f,
3614 BALL_RADIUS * 0.60f);
3615 pRT->FillEllipse(&inner, pRad);
3616 }
3617 }
3618
3619 // Black border
3620 if (pBorderBrush)
3621 pRT->DrawEllipse(&outer, pBorderBrush, 1.5f);
3622
3623 SafeRelease(&pRad);
3624 }
3625
3626 SafeRelease(&pStripeBrush);
3627 SafeRelease(&pBorderBrush);
3628 }
3629
3630 /*void DrawBalls(ID2D1RenderTarget* pRT) {
3631 ID2D1SolidColorBrush* pBrush = nullptr;
3632 ID2D1SolidColorBrush* pStripeBrush = nullptr; // For stripe pattern
3633
3634 pRT->CreateSolidColorBrush(D2D1::ColorF(0, 0, 0), &pBrush); // Placeholder
3635 pRT->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::White), &pStripeBrush);
3636
3637 if (!pBrush || !pStripeBrush) {
3638 SafeRelease(&pBrush);
3639 SafeRelease(&pStripeBrush);
3640 return;
3641 }
3642
3643
3644 for (size_t i = 0; i < balls.size(); ++i) {
3645 const Ball& b = balls[i];
3646 if (!b.isPocketed) {
3647 D2D1_ELLIPSE ellipse = D2D1::Ellipse(D2D1::Point2F(b.x, b.y), BALL_RADIUS, BALL_RADIUS);
3648
3649 // Set main ball color
3650 pBrush->SetColor(b.color);
3651 pRT->FillEllipse(&ellipse, pBrush);
3652
3653 // Draw Stripe if applicable
3654 if (b.type == BallType::STRIPE) {
3655 // Draw a white band across the middle (simplified stripe)
3656 D2D1_RECT_F stripeRect = D2D1::RectF(b.x - BALL_RADIUS, b.y - BALL_RADIUS * 0.4f, b.x + BALL_RADIUS, b.y + BALL_RADIUS * 0.4f);
3657 // Need to clip this rectangle to the ellipse bounds - complex!
3658 // Alternative: Draw two colored arcs leaving a white band.
3659 // Simplest: Draw a white circle inside, slightly smaller.
3660 D2D1_ELLIPSE innerEllipse = D2D1::Ellipse(D2D1::Point2F(b.x, b.y), BALL_RADIUS * 0.6f, BALL_RADIUS * 0.6f);
3661 pRT->FillEllipse(innerEllipse, pStripeBrush); // White center part
3662 pBrush->SetColor(b.color); // Set back to stripe color
3663 pRT->FillEllipse(innerEllipse, pBrush); // Fill again, leaving a ring - No, this isn't right.
3664
3665 // Let's try drawing a thick white line across
3666 // This doesn't look great. Just drawing solid red for stripes for now.
3667 }
3668
3669 // Draw Number (Optional - requires more complex text layout or pre-rendered textures)
3670 // if (b.id != 0 && pTextFormat) {
3671 // std::wstring numStr = std::to_wstring(b.id);
3672 // D2D1_RECT_F textRect = D2D1::RectF(b.x - BALL_RADIUS, b.y - BALL_RADIUS, b.x + BALL_RADIUS, b.y + BALL_RADIUS);
3673 // ID2D1SolidColorBrush* pNumBrush = nullptr;
3674 // D2D1_COLOR_F numCol = (b.type == BallType::SOLID || b.id == 8) ? D2D1::ColorF(D2D1::ColorF::Black) : D2D1::ColorF(D2D1::ColorF::White);
3675 // pRT->CreateSolidColorBrush(numCol, &pNumBrush);
3676 // // Create a smaller text format...
3677 // // pRT->DrawText(numStr.c_str(), numStr.length(), pSmallTextFormat, &textRect, pNumBrush);
3678 // SafeRelease(&pNumBrush);
3679 // }
3680 }
3681 }
3682
3683 SafeRelease(&pBrush);
3684 SafeRelease(&pStripeBrush);
3685 }*/
3686
3687
3688 /*void DrawAimingAids(ID2D1RenderTarget* pRT) {
3689 // Condition check at start (Unchanged)
3690 //if (currentGameState != PLAYER1_TURN && currentGameState != PLAYER2_TURN &&
3691 //currentGameState != BREAKING && currentGameState != AIMING)
3692 //{
3693 //return;
3694 //}
3695 // NEW Condition: Allow drawing if it's a human player's active turn/aiming/breaking,
3696 // OR if it's AI's turn and it's in AI_THINKING state (calculating) or BREAKING (aiming break).
3697 bool isHumanInteracting = (!isPlayer2AI || currentPlayer == 1) &&
3698 (currentGameState == PLAYER1_TURN || currentGameState == PLAYER2_TURN ||
3699 currentGameState == BREAKING || currentGameState == AIMING);
3700 // AI_THINKING state is when AI calculates shot. AIMakeDecision sets cueAngle/shotPower.
3701 // Also include BREAKING state if it's AI's turn and isOpeningBreakShot for break aim visualization.
3702 // NEW Condition: AI is displaying its aim
3703 bool isAiVisualizingShot = (isPlayer2AI && currentPlayer == 2 &&
3704 currentGameState == AI_THINKING && aiIsDisplayingAim);
3705
3706 if (!isHumanInteracting && !(isAiVisualizingShot || (currentGameState == AI_THINKING && aiIsDisplayingAim))) {
3707 return;
3708 }
3709
3710 Ball* cueBall = GetCueBall();
3711 if (!cueBall || cueBall->isPocketed) return; // Don't draw if cue ball is gone
3712
3713 ID2D1SolidColorBrush* pBrush = nullptr;
3714 ID2D1SolidColorBrush* pGhostBrush = nullptr;
3715 ID2D1StrokeStyle* pDashedStyle = nullptr;
3716 ID2D1SolidColorBrush* pCueBrush = nullptr;
3717 ID2D1SolidColorBrush* pReflectBrush = nullptr; // Brush for reflection line
3718
3719 // Ensure render target is valid
3720 if (!pRT) return;
3721
3722 // Create Brushes and Styles (check for failures)
3723 HRESULT hr;
3724 hr = pRT->CreateSolidColorBrush(AIM_LINE_COLOR, &pBrush);
3725 if FAILED(hr) { SafeRelease(&pBrush); return; }
3726 hr = pRT->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::White, 0.5f), &pGhostBrush);
3727 if FAILED(hr) { SafeRelease(&pBrush); SafeRelease(&pGhostBrush); return; }
3728 hr = pRT->CreateSolidColorBrush(D2D1::ColorF(0.6f, 0.4f, 0.2f), &pCueBrush);
3729 if FAILED(hr) { SafeRelease(&pBrush); SafeRelease(&pGhostBrush); SafeRelease(&pCueBrush); return; }
3730 // Create reflection brush (e.g., lighter shade or different color)
3731 hr = pRT->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::LightCyan, 0.6f), &pReflectBrush);
3732 if FAILED(hr) { SafeRelease(&pBrush); SafeRelease(&pGhostBrush); SafeRelease(&pCueBrush); SafeRelease(&pReflectBrush); return; }
3733 // Create a Cyan brush for primary and secondary lines //orig(75.0f / 255.0f, 0.0f, 130.0f / 255.0f);indigoColor
3734 D2D1::ColorF cyanColor(0.0, 255.0, 255.0, 255.0f);
3735 ID2D1SolidColorBrush* pCyanBrush = nullptr;
3736 hr = pRT->CreateSolidColorBrush(cyanColor, &pCyanBrush);
3737 if (FAILED(hr)) {
3738 SafeRelease(&pCyanBrush);
3739 // handle error if needed
3740 }
3741 // Create a Purple brush for primary and secondary lines
3742 D2D1::ColorF purpleColor(255.0f, 0.0f, 255.0f, 255.0f);
3743 ID2D1SolidColorBrush* pPurpleBrush = nullptr;
3744 hr = pRT->CreateSolidColorBrush(purpleColor, &pPurpleBrush);
3745 if (FAILED(hr)) {
3746 SafeRelease(&pPurpleBrush);
3747 // handle error if needed
3748 }
3749
3750 if (pFactory) {
3751 D2D1_STROKE_STYLE_PROPERTIES strokeProps = D2D1::StrokeStyleProperties();
3752 strokeProps.dashStyle = D2D1_DASH_STYLE_DASH;
3753 hr = pFactory->CreateStrokeStyle(&strokeProps, nullptr, 0, &pDashedStyle);
3754 if FAILED(hr) { pDashedStyle = nullptr; }
3755 }
3756
3757
3758 // --- Cue Stick Drawing (Unchanged from previous fix) ---
3759 const float baseStickLength = 150.0f;
3760 const float baseStickThickness = 4.0f;
3761 float stickLength = baseStickLength * 1.4f;
3762 float stickThickness = baseStickThickness * 1.5f;
3763 float stickAngle = cueAngle + PI;
3764 float powerOffset = 0.0f;
3765 //if (isAiming && (currentGameState == AIMING || currentGameState == BREAKING)) {
3766 // Show power offset if human is aiming/dragging, or if AI is preparing its shot (AI_THINKING or AI Break)
3767 if ((isAiming && (currentGameState == AIMING || currentGameState == BREAKING)) || isAiVisualizingShot) { // Use the new condition
3768 powerOffset = shotPower * 5.0f;
3769 }
3770 D2D1_POINT_2F cueStickEnd = D2D1::Point2F(cueBall->x + cosf(stickAngle) * (stickLength + powerOffset), cueBall->y + sinf(stickAngle) * (stickLength + powerOffset));
3771 D2D1_POINT_2F cueStickTip = D2D1::Point2F(cueBall->x + cosf(stickAngle) * (powerOffset + 5.0f), cueBall->y + sinf(stickAngle) * (powerOffset + 5.0f));
3772 pRT->DrawLine(cueStickTip, cueStickEnd, pCueBrush, stickThickness);
3773
3774
3775 // --- Projection Line Calculation ---
3776 float cosA = cosf(cueAngle);
3777 float sinA = sinf(cueAngle);
3778 float rayLength = TABLE_WIDTH + TABLE_HEIGHT; // Ensure ray is long enough
3779 D2D1_POINT_2F rayStart = D2D1::Point2F(cueBall->x, cueBall->y);
3780 D2D1_POINT_2F rayEnd = D2D1::Point2F(rayStart.x + cosA * rayLength, rayStart.y + sinA * rayLength);*/
3781
3782 void DrawAimingAids(ID2D1RenderTarget* pRT) {
3783 // Determine if aiming aids should be drawn.
3784 bool isHumanInteracting = (!isPlayer2AI || currentPlayer == 1) &&
3785 (currentGameState == PLAYER1_TURN || currentGameState == PLAYER2_TURN ||
3786 currentGameState == BREAKING || currentGameState == AIMING ||
3787 currentGameState == CHOOSING_POCKET_P1 || currentGameState == CHOOSING_POCKET_P2);
3788
3789 // FOOLPROOF FIX: This is the new condition to show the AI's aim.
3790 bool isAiVisualizingShot = (isPlayer2AI && currentPlayer == 2 && aiIsDisplayingAim);
3791
3792 if (!isHumanInteracting && !isAiVisualizingShot) {
3793 return;
3794 }
3795
3796 Ball* cueBall = GetCueBall();
3797 if (!cueBall || cueBall->isPocketed) return;
3798
3799 // --- Brush and Style Creation (No changes here) ---
3800 ID2D1SolidColorBrush* pBrush = nullptr;
3801 ID2D1SolidColorBrush* pGhostBrush = nullptr;
3802 ID2D1StrokeStyle* pDashedStyle = nullptr;
3803 ID2D1SolidColorBrush* pCueBrush = nullptr;
3804 ID2D1SolidColorBrush* pReflectBrush = nullptr;
3805 ID2D1SolidColorBrush* pCyanBrush = nullptr;
3806 ID2D1SolidColorBrush* pPurpleBrush = nullptr;
3807 pRT->CreateSolidColorBrush(AIM_LINE_COLOR, &pBrush);
3808 pRT->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::White, 0.5f), &pGhostBrush);
3809 pRT->CreateSolidColorBrush(D2D1::ColorF(0.6f, 0.4f, 0.2f), &pCueBrush);
3810 pRT->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::LightCyan, 0.6f), &pReflectBrush);
3811 pRT->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::Cyan), &pCyanBrush);
3812 pRT->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::Purple), &pPurpleBrush);
3813 if (pFactory) {
3814 D2D1_STROKE_STYLE_PROPERTIES strokeProps = D2D1::StrokeStyleProperties();
3815 strokeProps.dashStyle = D2D1_DASH_STYLE_DASH;
3816 pFactory->CreateStrokeStyle(&strokeProps, nullptr, 0, &pDashedStyle);
3817 }
3818 // --- End Brush Creation ---
3819
3820 // --- FOOLPROOF FIX: Use the AI's planned angle and power for drawing ---
3821 float angleToDraw = cueAngle;
3822 float powerToDraw = shotPower;
3823
3824 if (isAiVisualizingShot) {
3825 // When the AI is showing its aim, force the drawing to use its planned shot details.
3826 angleToDraw = aiPlannedShotDetails.angle;
3827 powerToDraw = aiPlannedShotDetails.power;
3828 }
3829 // --- End AI Aiming Fix ---
3830
3831 // --- Cue Stick Drawing ---
3832 const float baseStickLength = 150.0f;
3833 const float baseStickThickness = 4.0f;
3834 float stickLength = baseStickLength * 1.4f;
3835 float stickThickness = baseStickThickness * 1.5f;
3836 float stickAngle = angleToDraw + PI; // Use the angle we determined
3837 float powerOffset = 0.0f;
3838 if ((isAiming || isDraggingStick) || isAiVisualizingShot) {
3839 powerOffset = powerToDraw * 5.0f; // Use the power we determined
3840 }
3841 D2D1_POINT_2F cueStickEnd = D2D1::Point2F(cueBall->x + cosf(stickAngle) * (stickLength + powerOffset), cueBall->y + sinf(stickAngle) * (stickLength + powerOffset));
3842 D2D1_POINT_2F cueStickTip = D2D1::Point2F(cueBall->x + cosf(stickAngle) * (powerOffset + 5.0f), cueBall->y + sinf(stickAngle) * (powerOffset + 5.0f));
3843 pRT->DrawLine(cueStickTip, cueStickEnd, pCueBrush, stickThickness);
3844
3845 // --- Projection Line Calculation ---
3846 float cosA = cosf(angleToDraw); // Use the angle we determined
3847 float sinA = sinf(angleToDraw);
3848 float rayLength = TABLE_WIDTH + TABLE_HEIGHT;
3849 D2D1_POINT_2F rayStart = D2D1::Point2F(cueBall->x, cueBall->y);
3850 D2D1_POINT_2F rayEnd = D2D1::Point2F(rayStart.x + cosA * rayLength, rayStart.y + sinA * rayLength);
3851
3852 // Find the first ball hit by the aiming ray
3853 Ball* hitBall = nullptr;
3854 float firstHitDistSq = -1.0f;
3855 D2D1_POINT_2F ballCollisionPoint = { 0, 0 }; // Point on target ball circumference
3856 D2D1_POINT_2F ghostBallPosForHit = { 0, 0 }; // Ghost ball pos for the hit ball
3857
3858 hitBall = FindFirstHitBall(rayStart, cueAngle, firstHitDistSq);
3859 if (hitBall) {
3860 // Calculate the point on the target ball's circumference
3861 float collisionDist = sqrtf(firstHitDistSq);
3862 ballCollisionPoint = D2D1::Point2F(rayStart.x + cosA * collisionDist, rayStart.y + sinA * collisionDist);
3863 // Calculate ghost ball position for this specific hit (used for projection consistency)
3864 ghostBallPosForHit = D2D1::Point2F(hitBall->x - cosA * BALL_RADIUS, hitBall->y - sinA * BALL_RADIUS); // Approx.
3865 }
3866
3867 // Find the first rail hit by the aiming ray
3868 D2D1_POINT_2F railHitPoint = rayEnd; // Default to far end if no rail hit
3869 float minRailDistSq = rayLength * rayLength;
3870 int hitRailIndex = -1; // 0:Left, 1:Right, 2:Top, 3:Bottom
3871
3872 // Define table edge segments for intersection checks
3873 D2D1_POINT_2F topLeft = D2D1::Point2F(TABLE_LEFT, TABLE_TOP);
3874 D2D1_POINT_2F topRight = D2D1::Point2F(TABLE_RIGHT, TABLE_TOP);
3875 D2D1_POINT_2F bottomLeft = D2D1::Point2F(TABLE_LEFT, TABLE_BOTTOM);
3876 D2D1_POINT_2F bottomRight = D2D1::Point2F(TABLE_RIGHT, TABLE_BOTTOM);
3877
3878 D2D1_POINT_2F currentIntersection;
3879
3880 // Check Left Rail
3881 if (LineSegmentIntersection(rayStart, rayEnd, topLeft, bottomLeft, currentIntersection)) {
3882 float distSq = GetDistanceSq(rayStart.x, rayStart.y, currentIntersection.x, currentIntersection.y);
3883 if (distSq < minRailDistSq) { minRailDistSq = distSq; railHitPoint = currentIntersection; hitRailIndex = 0; }
3884 }
3885 // Check Right Rail
3886 if (LineSegmentIntersection(rayStart, rayEnd, topRight, bottomRight, currentIntersection)) {
3887 float distSq = GetDistanceSq(rayStart.x, rayStart.y, currentIntersection.x, currentIntersection.y);
3888 if (distSq < minRailDistSq) { minRailDistSq = distSq; railHitPoint = currentIntersection; hitRailIndex = 1; }
3889 }
3890 // Check Top Rail
3891 if (LineSegmentIntersection(rayStart, rayEnd, topLeft, topRight, currentIntersection)) {
3892 float distSq = GetDistanceSq(rayStart.x, rayStart.y, currentIntersection.x, currentIntersection.y);
3893 if (distSq < minRailDistSq) { minRailDistSq = distSq; railHitPoint = currentIntersection; hitRailIndex = 2; }
3894 }
3895 // Check Bottom Rail
3896 if (LineSegmentIntersection(rayStart, rayEnd, bottomLeft, bottomRight, currentIntersection)) {
3897 float distSq = GetDistanceSq(rayStart.x, rayStart.y, currentIntersection.x, currentIntersection.y);
3898 if (distSq < minRailDistSq) { minRailDistSq = distSq; railHitPoint = currentIntersection; hitRailIndex = 3; }
3899 }
3900
3901
3902 // --- Determine final aim line end point ---
3903 D2D1_POINT_2F finalLineEnd = railHitPoint; // Assume rail hit first
3904 bool aimingAtRail = true;
3905
3906 if (hitBall && firstHitDistSq < minRailDistSq) {
3907 // Ball collision is closer than rail collision
3908 finalLineEnd = ballCollisionPoint; // End line at the point of contact on the ball
3909 aimingAtRail = false;
3910 }
3911
3912 // --- Draw Primary Aiming Line ---
3913 pRT->DrawLine(rayStart, finalLineEnd, pBrush, 1.0f, pDashedStyle ? pDashedStyle : NULL);
3914
3915 // --- Draw Target Circle/Indicator ---
3916 D2D1_ELLIPSE targetCircle = D2D1::Ellipse(finalLineEnd, BALL_RADIUS / 2.0f, BALL_RADIUS / 2.0f);
3917 pRT->DrawEllipse(&targetCircle, pBrush, 1.0f);
3918
3919 // --- Draw Projection/Reflection Lines ---
3920 if (!aimingAtRail && hitBall) {
3921 // Aiming at a ball: Draw Ghost Cue Ball and Target Ball Projection
3922 D2D1_ELLIPSE ghostCue = D2D1::Ellipse(ballCollisionPoint, BALL_RADIUS, BALL_RADIUS); // Ghost ball at contact point
3923 pRT->DrawEllipse(ghostCue, pGhostBrush, 1.0f, pDashedStyle ? pDashedStyle : NULL);
3924
3925 // Calculate target ball projection based on impact line (cue collision point -> target center)
3926 float targetProjectionAngle = atan2f(hitBall->y - ballCollisionPoint.y, hitBall->x - ballCollisionPoint.x);
3927 // Clamp angle calculation if distance is tiny
3928 if (GetDistanceSq(hitBall->x, hitBall->y, ballCollisionPoint.x, ballCollisionPoint.y) < 1.0f) {
3929 targetProjectionAngle = cueAngle; // Fallback if overlapping
3930 }
3931
3932 D2D1_POINT_2F targetStartPoint = D2D1::Point2F(hitBall->x, hitBall->y);
3933 D2D1_POINT_2F targetProjectionEnd = D2D1::Point2F(
3934 hitBall->x + cosf(targetProjectionAngle) * 50.0f, // Projection length 50 units
3935 hitBall->y + sinf(targetProjectionAngle) * 50.0f
3936 );
3937 // Draw solid line for target projection
3938 //pRT->DrawLine(targetStartPoint, targetProjectionEnd, pBrush, 1.0f);
3939
3940 //new code start
3941
3942 // Dual trajectory with edge-aware contact simulation
3943 D2D1_POINT_2F dir = {
3944 targetProjectionEnd.x - targetStartPoint.x,
3945 targetProjectionEnd.y - targetStartPoint.y
3946 };
3947 float dirLen = sqrtf(dir.x * dir.x + dir.y * dir.y);
3948 dir.x /= dirLen;
3949 dir.y /= dirLen;
3950
3951 D2D1_POINT_2F perp = { -dir.y, dir.x };
3952
3953 // Approximate cue ball center by reversing from tip
3954 D2D1_POINT_2F cueBallCenterForGhostHit = { // Renamed for clarity if you use it elsewhere
3955 targetStartPoint.x - dir.x * BALL_RADIUS,
3956 targetStartPoint.y - dir.y * BALL_RADIUS
3957 };
3958
3959 // REAL contact-ball center - use your physics object's center:
3960 // (replace 'objectBallPos' with whatever you actually call it)
3961 // (targetStartPoint is already hitBall->x, hitBall->y)
3962 D2D1_POINT_2F contactBallCenter = targetStartPoint; // Corrected: Use the object ball's actual center
3963 //D2D1_POINT_2F contactBallCenter = D2D1::Point2F(hitBall->x, hitBall->y);
3964
3965 // The 'offset' calculation below uses 'cueBallCenterForGhostHit' (originally 'cueBallCenter').
3966 // This will result in 'offset' being 0 because 'cueBallCenterForGhostHit' is defined
3967 // such that (targetStartPoint - cueBallCenterForGhostHit) is parallel to 'dir',
3968 // and 'perp' is perpendicular to 'dir'.
3969 // Consider Change 2 if this 'offset' is not behaving as intended for the secondary line.
3970 /*float offset = ((targetStartPoint.x - cueBallCenterForGhostHit.x) * perp.x +
3971 (targetStartPoint.y - cueBallCenterForGhostHit.y) * perp.y);*/
3972 /*float offset = ((targetStartPoint.x - cueBallCenter.x) * perp.x +
3973 (targetStartPoint.y - cueBallCenter.y) * perp.y);
3974 float absOffset = fabsf(offset);
3975 float side = (offset >= 0 ? 1.0f : -1.0f);*/
3976
3977 // Use actual cue ball center for offset calculation if 'offset' is meant to quantify the cut
3978 D2D1_POINT_2F actualCueBallPhysicalCenter = D2D1::Point2F(cueBall->x, cueBall->y); // This is also rayStart
3979
3980 // Offset calculation based on actual cue ball position relative to the 'dir' line through targetStartPoint
3981 float offset = ((targetStartPoint.x - actualCueBallPhysicalCenter.x) * perp.x +
3982 (targetStartPoint.y - actualCueBallPhysicalCenter.y) * perp.y);
3983 float absOffset = fabsf(offset);
3984 float side = (offset >= 0 ? 1.0f : -1.0f);
3985
3986
3987 // Actual contact point on target ball edge
3988 D2D1_POINT_2F contactPoint = {
3989 contactBallCenter.x + perp.x * BALL_RADIUS * side,
3990 contactBallCenter.y + perp.y * BALL_RADIUS * side
3991 };
3992
3993 // Tangent (cut shot) path from contact point
3994 // Tangent (cut shot) path: from contact point to contact ball center
3995 D2D1_POINT_2F objectBallDir = {
3996 contactBallCenter.x - contactPoint.x,
3997 contactBallCenter.y - contactPoint.y
3998 };
3999 float oLen = sqrtf(objectBallDir.x * objectBallDir.x + objectBallDir.y * objectBallDir.y);
4000 if (oLen != 0.0f) {
4001 objectBallDir.x /= oLen;
4002 objectBallDir.y /= oLen;
4003 }
4004
4005 const float PRIMARY_LEN = 150.0f; //default=150.0f
4006 const float SECONDARY_LEN = 150.0f; //default=150.0f
4007 const float STRAIGHT_EPSILON = BALL_RADIUS * 0.05f;
4008
4009 D2D1_POINT_2F primaryEnd = {
4010 targetStartPoint.x + dir.x * PRIMARY_LEN,
4011 targetStartPoint.y + dir.y * PRIMARY_LEN
4012 };
4013
4014 // Secondary line starts from the contact ball's center
4015 D2D1_POINT_2F secondaryStart = contactBallCenter;
4016 D2D1_POINT_2F secondaryEnd = {
4017 secondaryStart.x + objectBallDir.x * SECONDARY_LEN,
4018 secondaryStart.y + objectBallDir.y * SECONDARY_LEN
4019 };
4020
4021 if (absOffset < STRAIGHT_EPSILON) // straight shot?
4022 {
4023 // Straight: secondary behind primary
4024 // secondary behind primary {pDashedStyle param at end}
4025 pRT->DrawLine(secondaryStart, secondaryEnd, pPurpleBrush, 2.0f);
4026 //pRT->DrawLine(secondaryStart, secondaryEnd, pGhostBrush, 1.0f);
4027 pRT->DrawLine(targetStartPoint, primaryEnd, pCyanBrush, 2.0f);
4028 //pRT->DrawLine(targetStartPoint, primaryEnd, pBrush, 1.0f);
4029 }
4030 else
4031 {
4032 // Cut shot: both visible
4033 // both visible for cut shot
4034 pRT->DrawLine(secondaryStart, secondaryEnd, pPurpleBrush, 2.0f);
4035 //pRT->DrawLine(secondaryStart, secondaryEnd, pGhostBrush, 1.0f);
4036 pRT->DrawLine(targetStartPoint, primaryEnd, pCyanBrush, 2.0f);
4037 //pRT->DrawLine(targetStartPoint, primaryEnd, pBrush, 1.0f);
4038 }
4039 // End improved trajectory logic
4040
4041 //new code end
4042
4043 // -- Cue Ball Path after collision (Optional, requires physics) --
4044 // Very simplified: Assume cue deflects, angle depends on cut angle.
4045 // float cutAngle = acosf(cosf(cueAngle - targetProjectionAngle)); // Angle between paths
4046 // float cueDeflectionAngle = ? // Depends on cutAngle, spin, etc. Hard to predict accurately.
4047 // D2D1_POINT_2F cueProjectionEnd = ...
4048 // pRT->DrawLine(ballCollisionPoint, cueProjectionEnd, pGhostBrush, 1.0f, pDashedStyle ? pDashedStyle : NULL);
4049
4050 // --- Accuracy Comment ---
4051 // Note: The visual accuracy of this projection, especially for cut shots (hitting the ball off-center)
4052 // or shots with spin, is limited by the simplified physics model. Real pool physics involves
4053 // collision-induced throw, spin transfer, and cue ball deflection not fully simulated here.
4054 // The ghost ball method shows the *ideal* line for a center-cue hit without spin.
4055
4056 }
4057 else if (aimingAtRail && hitRailIndex != -1) {
4058 // Aiming at a rail: Draw reflection line
4059 float reflectAngle = cueAngle;
4060 // Reflect angle based on which rail was hit
4061 if (hitRailIndex == 0 || hitRailIndex == 1) { // Left or Right rail
4062 reflectAngle = PI - cueAngle; // Reflect horizontal component
4063 }
4064 else { // Top or Bottom rail
4065 reflectAngle = -cueAngle; // Reflect vertical component
4066 }
4067 // Normalize angle if needed (atan2 usually handles this)
4068 while (reflectAngle > PI) reflectAngle -= 2 * PI;
4069 while (reflectAngle <= -PI) reflectAngle += 2 * PI;
4070
4071
4072 float reflectionLength = 60.0f; // Length of the reflection line
4073 D2D1_POINT_2F reflectionEnd = D2D1::Point2F(
4074 finalLineEnd.x + cosf(reflectAngle) * reflectionLength,
4075 finalLineEnd.y + sinf(reflectAngle) * reflectionLength
4076 );
4077
4078 // Draw the reflection line (e.g., using a different color/style)
4079 pRT->DrawLine(finalLineEnd, reflectionEnd, pReflectBrush, 1.0f, pDashedStyle ? pDashedStyle : NULL);
4080 }
4081
4082 // Release resources
4083 SafeRelease(&pBrush);
4084 SafeRelease(&pGhostBrush);
4085 SafeRelease(&pCueBrush);
4086 SafeRelease(&pReflectBrush); // Release new brush
4087 SafeRelease(&pCyanBrush);
4088 SafeRelease(&pPurpleBrush);
4089 SafeRelease(&pDashedStyle);
4090 }
4091
4092
4093 void DrawUI(ID2D1RenderTarget* pRT) {
4094 if (!pTextFormat || !pLargeTextFormat) return;
4095
4096 ID2D1SolidColorBrush* pBrush = nullptr;
4097 pRT->CreateSolidColorBrush(UI_TEXT_COLOR, &pBrush);
4098 if (!pBrush) return;
4099
4100 //new code
4101 // --- Always draw AI's 8?Ball call arrow when it's Player?2's turn and AI has called ---
4102 //if (isPlayer2AI && currentPlayer == 2 && calledPocketP2 >= 0) {
4103 // FIX: This condition correctly shows the AI's called pocket arrow.
4104 if (isPlayer2AI && IsPlayerOnEightBall(2) && calledPocketP2 >= 0) {
4105 // pocket index that AI called
4106 int idx = calledPocketP2;
4107 // draw large blue arrow
4108 ID2D1SolidColorBrush* pArrow = nullptr;
4109 pRT->CreateSolidColorBrush(TURN_ARROW_COLOR, &pArrow);
4110 if (pArrow) {
4111 auto P = pocketPositions[idx];
4112 D2D1_POINT_2F tri[3] = {
4113 { P.x - 15.0f, P.y - 40.0f },
4114 { P.x + 15.0f, P.y - 40.0f },
4115 { P.x , P.y - 10.0f }
4116 };
4117 ID2D1PathGeometry* geom = nullptr;
4118 pFactory->CreatePathGeometry(&geom);
4119 ID2D1GeometrySink* sink = nullptr;
4120 geom->Open(&sink);
4121 sink->BeginFigure(tri[0], D2D1_FIGURE_BEGIN_FILLED);
4122 sink->AddLines(&tri[1], 2);
4123 sink->EndFigure(D2D1_FIGURE_END_CLOSED);
4124 sink->Close();
4125 pRT->FillGeometry(geom, pArrow);
4126 SafeRelease(&sink);
4127 SafeRelease(&geom);
4128 SafeRelease(&pArrow);
4129 }
4130 // draw “Choose a pocket...” prompt
4131 D2D1_RECT_F txt = D2D1::RectF(
4132 TABLE_LEFT,
4133 TABLE_BOTTOM + CUSHION_THICKNESS + 5.0f,
4134 TABLE_RIGHT,
4135 TABLE_BOTTOM + CUSHION_THICKNESS + 30.0f
4136 );
4137 pRT->DrawText(
4138 L"AI has called this pocket",
4139 (UINT32)wcslen(L"AI has called this pocket"),
4140 pTextFormat,
4141 &txt,
4142 pBrush
4143 );
4144 // note: no return here — we still draw fouls/turn text underneath
4145 }
4146 //end new code
4147
4148 // --- Player Info Area (Top Left/Right) --- (Unchanged)
4149 float uiTop = TABLE_TOP - 80;
4150 float uiHeight = 60;
4151 float p1Left = TABLE_LEFT;
4152 float p1Width = 150;
4153 float p2Left = TABLE_RIGHT - p1Width;
4154 D2D1_RECT_F p1Rect = D2D1::RectF(p1Left, uiTop, p1Left + p1Width, uiTop + uiHeight);
4155 D2D1_RECT_F p2Rect = D2D1::RectF(p2Left, uiTop, p2Left + p1Width, uiTop + uiHeight);
4156
4157 // Player 1 Info Text (Unchanged)
4158 std::wostringstream oss1;
4159 oss1 << player1Info.name.c_str() << L"\n";
4160 if (player1Info.assignedType != BallType::NONE) {
4161 oss1 << ((player1Info.assignedType == BallType::SOLID) ? L"Solids (Yellow)" : L"Stripes (Red)");
4162 oss1 << L" [" << player1Info.ballsPocketedCount << L"/7]";
4163 }
4164 else {
4165 oss1 << L"(Undecided)";
4166 }
4167 pRT->DrawText(oss1.str().c_str(), (UINT32)oss1.str().length(), pTextFormat, &p1Rect, pBrush);
4168 // Draw Player 1 Side Ball
4169 if (player1Info.assignedType != BallType::NONE)
4170 {
4171 ID2D1SolidColorBrush* pBallBrush = nullptr;
4172 D2D1_COLOR_F ballColor = (player1Info.assignedType == BallType::SOLID) ?
4173 D2D1::ColorF(1.0f, 1.0f, 0.0f) : D2D1::ColorF(1.0f, 0.0f, 0.0f);
4174 pRT->CreateSolidColorBrush(ballColor, &pBallBrush);
4175 if (pBallBrush)
4176 {
4177 D2D1_POINT_2F ballCenter = D2D1::Point2F(p1Rect.right + 10.0f, p1Rect.top + 20.0f);
4178 float radius = 10.0f;
4179 D2D1_ELLIPSE ball = D2D1::Ellipse(ballCenter, radius, radius);
4180 pRT->FillEllipse(&ball, pBallBrush);
4181 SafeRelease(&pBallBrush);
4182 // Draw border around the ball
4183 ID2D1SolidColorBrush* pBorderBrush = nullptr;
4184 pRT->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::Black), &pBorderBrush);
4185 if (pBorderBrush)
4186 {
4187 pRT->DrawEllipse(&ball, pBorderBrush, 1.5f); // thin border
4188 SafeRelease(&pBorderBrush);
4189 }
4190
4191 // If stripes, draw a stripe band
4192 if (player1Info.assignedType == BallType::STRIPE)
4193 {
4194 ID2D1SolidColorBrush* pStripeBrush = nullptr;
4195 pRT->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::White), &pStripeBrush);
4196 if (pStripeBrush)
4197 {
4198 D2D1_RECT_F stripeRect = D2D1::RectF(
4199 ballCenter.x - radius,
4200 ballCenter.y - 3.0f,
4201 ballCenter.x + radius,
4202 ballCenter.y + 3.0f
4203 );
4204 pRT->FillRectangle(&stripeRect, pStripeBrush);
4205 SafeRelease(&pStripeBrush);
4206 }
4207 }
4208 }
4209 }
4210
4211
4212 // Player 2 Info Text (Unchanged)
4213 std::wostringstream oss2;
4214 oss2 << player2Info.name.c_str() << L"\n";
4215 if (player2Info.assignedType != BallType::NONE) {
4216 oss2 << ((player2Info.assignedType == BallType::SOLID) ? L"Solids (Yellow)" : L"Stripes (Red)");
4217 oss2 << L" [" << player2Info.ballsPocketedCount << L"/7]";
4218 }
4219 else {
4220 oss2 << L"(Undecided)";
4221 }
4222 pRT->DrawText(oss2.str().c_str(), (UINT32)oss2.str().length(), pTextFormat, &p2Rect, pBrush);
4223 // Draw Player 2 Side Ball
4224 if (player2Info.assignedType != BallType::NONE)
4225 {
4226 ID2D1SolidColorBrush* pBallBrush = nullptr;
4227 D2D1_COLOR_F ballColor = (player2Info.assignedType == BallType::SOLID) ?
4228 D2D1::ColorF(1.0f, 1.0f, 0.0f) : D2D1::ColorF(1.0f, 0.0f, 0.0f);
4229 pRT->CreateSolidColorBrush(ballColor, &pBallBrush);
4230 if (pBallBrush)
4231 {
4232 D2D1_POINT_2F ballCenter = D2D1::Point2F(p2Rect.right + 10.0f, p2Rect.top + 20.0f);
4233 float radius = 10.0f;
4234 D2D1_ELLIPSE ball = D2D1::Ellipse(ballCenter, radius, radius);
4235 pRT->FillEllipse(&ball, pBallBrush);
4236 SafeRelease(&pBallBrush);
4237 // Draw border around the ball
4238 ID2D1SolidColorBrush* pBorderBrush = nullptr;
4239 pRT->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::Black), &pBorderBrush);
4240 if (pBorderBrush)
4241 {
4242 pRT->DrawEllipse(&ball, pBorderBrush, 1.5f); // thin border
4243 SafeRelease(&pBorderBrush);
4244 }
4245
4246 // If stripes, draw a stripe band
4247 if (player2Info.assignedType == BallType::STRIPE)
4248 {
4249 ID2D1SolidColorBrush* pStripeBrush = nullptr;
4250 pRT->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::White), &pStripeBrush);
4251 if (pStripeBrush)
4252 {
4253 D2D1_RECT_F stripeRect = D2D1::RectF(
4254 ballCenter.x - radius,
4255 ballCenter.y - 3.0f,
4256 ballCenter.x + radius,
4257 ballCenter.y + 3.0f
4258 );
4259 pRT->FillRectangle(&stripeRect, pStripeBrush);
4260 SafeRelease(&pStripeBrush);
4261 }
4262 }
4263 }
4264 }
4265
4266 // --- MODIFIED: Current Turn Arrow (Blue, Bigger, Beside Name) ---
4267 ID2D1SolidColorBrush* pArrowBrush = nullptr;
4268 pRT->CreateSolidColorBrush(TURN_ARROW_COLOR, &pArrowBrush);
4269 if (pArrowBrush && currentGameState != GAME_OVER && currentGameState != SHOT_IN_PROGRESS && currentGameState != AI_THINKING) {
4270 float arrowSizeBase = 32.0f; // Base size for width/height offsets (4x original ~8)
4271 float arrowCenterY = p1Rect.top + uiHeight / 2.0f; // Center vertically with text box
4272 float arrowTipX, arrowBackX;
4273
4274 D2D1_RECT_F playerBox = (currentPlayer == 1) ? p1Rect : p2Rect;
4275 arrowBackX = playerBox.left - 25.0f;
4276 arrowTipX = arrowBackX + arrowSizeBase * 0.75f;
4277
4278 float notchDepth = 12.0f; // Increased from 6.0f to make the rectangle longer
4279 float notchWidth = 10.0f;
4280
4281 float cx = arrowBackX;
4282 float cy = arrowCenterY;
4283
4284 // Define triangle + rectangle tail shape
4285 D2D1_POINT_2F tip = D2D1::Point2F(arrowTipX, cy); // tip
4286 D2D1_POINT_2F baseTop = D2D1::Point2F(cx, cy - arrowSizeBase / 2.0f); // triangle top
4287 D2D1_POINT_2F baseBot = D2D1::Point2F(cx, cy + arrowSizeBase / 2.0f); // triangle bottom
4288
4289 // Rectangle coordinates for the tail portion:
4290 D2D1_POINT_2F r1 = D2D1::Point2F(cx - notchDepth, cy - notchWidth / 2.0f); // rect top-left
4291 D2D1_POINT_2F r2 = D2D1::Point2F(cx, cy - notchWidth / 2.0f); // rect top-right
4292 D2D1_POINT_2F r3 = D2D1::Point2F(cx, cy + notchWidth / 2.0f); // rect bottom-right
4293 D2D1_POINT_2F r4 = D2D1::Point2F(cx - notchDepth, cy + notchWidth / 2.0f); // rect bottom-left
4294
4295 ID2D1PathGeometry* pPath = nullptr;
4296 if (SUCCEEDED(pFactory->CreatePathGeometry(&pPath))) {
4297 ID2D1GeometrySink* pSink = nullptr;
4298 if (SUCCEEDED(pPath->Open(&pSink))) {
4299 pSink->BeginFigure(tip, D2D1_FIGURE_BEGIN_FILLED);
4300 pSink->AddLine(baseTop);
4301 pSink->AddLine(r2); // transition from triangle into rectangle
4302 pSink->AddLine(r1);
4303 pSink->AddLine(r4);
4304 pSink->AddLine(r3);
4305 pSink->AddLine(baseBot);
4306 pSink->EndFigure(D2D1_FIGURE_END_CLOSED);
4307 pSink->Close();
4308 SafeRelease(&pSink);
4309 pRT->FillGeometry(pPath, pArrowBrush);
4310 }
4311 SafeRelease(&pPath);
4312 }
4313
4314
4315 SafeRelease(&pArrowBrush);
4316 }
4317
4318 //original
4319 /*
4320 // --- MODIFIED: Current Turn Arrow (Blue, Bigger, Beside Name) ---
4321 ID2D1SolidColorBrush* pArrowBrush = nullptr;
4322 pRT->CreateSolidColorBrush(TURN_ARROW_COLOR, &pArrowBrush);
4323 if (pArrowBrush && currentGameState != GAME_OVER && currentGameState != SHOT_IN_PROGRESS && currentGameState != AI_THINKING) {
4324 float arrowSizeBase = 32.0f; // Base size for width/height offsets (4x original ~8)
4325 float arrowCenterY = p1Rect.top + uiHeight / 2.0f; // Center vertically with text box
4326 float arrowTipX, arrowBackX;
4327
4328 if (currentPlayer == 1) {
4329 arrowBackX = p1Rect.left - 25.0f; // Position left of the box
4330 arrowTipX = arrowBackX + arrowSizeBase * 0.75f; // Pointy end extends right
4331 // Define points for right-pointing arrow
4332 //D2D1_POINT_2F pt1 = D2D1::Point2F(arrowTipX, arrowCenterY); // Tip
4333 //D2D1_POINT_2F pt2 = D2D1::Point2F(arrowBackX, arrowCenterY - arrowSizeBase / 2.0f); // Top-Back
4334 //D2D1_POINT_2F pt3 = D2D1::Point2F(arrowBackX, arrowCenterY + arrowSizeBase / 2.0f); // Bottom-Back
4335 // Enhanced arrow with base rectangle intersection
4336 float notchDepth = 6.0f; // Depth of square base "stem"
4337 float notchWidth = 4.0f; // Thickness of square part
4338
4339 D2D1_POINT_2F pt1 = D2D1::Point2F(arrowTipX, arrowCenterY); // Tip
4340 D2D1_POINT_2F pt2 = D2D1::Point2F(arrowBackX, arrowCenterY - arrowSizeBase / 2.0f); // Top-Back
4341 D2D1_POINT_2F pt3 = D2D1::Point2F(arrowBackX - notchDepth, arrowCenterY - notchWidth / 2.0f); // Square Left-Top
4342 D2D1_POINT_2F pt4 = D2D1::Point2F(arrowBackX - notchDepth, arrowCenterY + notchWidth / 2.0f); // Square Left-Bottom
4343 D2D1_POINT_2F pt5 = D2D1::Point2F(arrowBackX, arrowCenterY + arrowSizeBase / 2.0f); // Bottom-Back
4344
4345
4346 ID2D1PathGeometry* pPath = nullptr;
4347 if (SUCCEEDED(pFactory->CreatePathGeometry(&pPath))) {
4348 ID2D1GeometrySink* pSink = nullptr;
4349 if (SUCCEEDED(pPath->Open(&pSink))) {
4350 pSink->BeginFigure(pt1, D2D1_FIGURE_BEGIN_FILLED);
4351 pSink->AddLine(pt2);
4352 pSink->AddLine(pt3);
4353 pSink->EndFigure(D2D1_FIGURE_END_CLOSED);
4354 pSink->Close();
4355 SafeRelease(&pSink);
4356 pRT->FillGeometry(pPath, pArrowBrush);
4357 }
4358 SafeRelease(&pPath);
4359 }
4360 }
4361
4362
4363 //==================else player 2
4364 else { // Player 2
4365 // Player 2: Arrow left of P2 box, pointing right (or right of P2 box pointing left?)
4366 // Let's keep it consistent: Arrow left of the active player's box, pointing right.
4367 // Let's keep it consistent: Arrow left of the active player's box, pointing right.
4368 arrowBackX = p2Rect.left - 25.0f; // Position left of the box
4369 arrowTipX = arrowBackX + arrowSizeBase * 0.75f; // Pointy end extends right
4370 // Define points for right-pointing arrow
4371 D2D1_POINT_2F pt1 = D2D1::Point2F(arrowTipX, arrowCenterY); // Tip
4372 D2D1_POINT_2F pt2 = D2D1::Point2F(arrowBackX, arrowCenterY - arrowSizeBase / 2.0f); // Top-Back
4373 D2D1_POINT_2F pt3 = D2D1::Point2F(arrowBackX, arrowCenterY + arrowSizeBase / 2.0f); // Bottom-Back
4374
4375 ID2D1PathGeometry* pPath = nullptr;
4376 if (SUCCEEDED(pFactory->CreatePathGeometry(&pPath))) {
4377 ID2D1GeometrySink* pSink = nullptr;
4378 if (SUCCEEDED(pPath->Open(&pSink))) {
4379 pSink->BeginFigure(pt1, D2D1_FIGURE_BEGIN_FILLED);
4380 pSink->AddLine(pt2);
4381 pSink->AddLine(pt3);
4382 pSink->EndFigure(D2D1_FIGURE_END_CLOSED);
4383 pSink->Close();
4384 SafeRelease(&pSink);
4385 pRT->FillGeometry(pPath, pArrowBrush);
4386 }
4387 SafeRelease(&pPath);
4388 }
4389 }
4390 */
4391
4392
4393 // --- Persistent Blue 8?Ball Call Arrow & Prompt ---
4394 /*if (calledPocketP1 >= 0 || calledPocketP2 >= 0)
4395 {
4396 // determine index (default top?right)
4397 int idx = (currentPlayer == 1 ? calledPocketP1 : calledPocketP2);
4398 if (idx < 0) idx = (currentPlayer == 1 ? calledPocketP2 : calledPocketP1);
4399 if (idx < 0) idx = 2;
4400
4401 // draw large blue arrow
4402 ID2D1SolidColorBrush* pArrow = nullptr;
4403 pRT->CreateSolidColorBrush(TURN_ARROW_COLOR, &pArrow);
4404 if (pArrow) {
4405 auto P = pocketPositions[idx];
4406 D2D1_POINT_2F tri[3] = {
4407 {P.x - 15.0f, P.y - 40.0f},
4408 {P.x + 15.0f, P.y - 40.0f},
4409 {P.x , P.y - 10.0f}
4410 };
4411 ID2D1PathGeometry* geom = nullptr;
4412 pFactory->CreatePathGeometry(&geom);
4413 ID2D1GeometrySink* sink = nullptr;
4414 geom->Open(&sink);
4415 sink->BeginFigure(tri[0], D2D1_FIGURE_BEGIN_FILLED);
4416 sink->AddLines(&tri[1], 2);
4417 sink->EndFigure(D2D1_FIGURE_END_CLOSED);
4418 sink->Close();
4419 pRT->FillGeometry(geom, pArrow);
4420 SafeRelease(&sink); SafeRelease(&geom); SafeRelease(&pArrow);
4421 }
4422
4423 // draw prompt
4424 D2D1_RECT_F txt = D2D1::RectF(
4425 TABLE_LEFT,
4426 TABLE_BOTTOM + CUSHION_THICKNESS + 5.0f,
4427 TABLE_RIGHT,
4428 TABLE_BOTTOM + CUSHION_THICKNESS + 30.0f
4429 );
4430 pRT->DrawText(
4431 L"Choose a pocket...",
4432 (UINT32)wcslen(L"Choose a pocket..."),
4433 pTextFormat,
4434 &txt,
4435 pBrush
4436 );
4437 }*/
4438
4439 // --- Persistent Blue 8?Ball Pocket Arrow & Prompt (once called) ---
4440 /* if (calledPocketP1 >= 0 || calledPocketP2 >= 0)
4441 {
4442 // 1) Determine pocket index
4443 int idx = (currentPlayer == 1 ? calledPocketP1 : calledPocketP2);
4444 // If the other player had called but it's now your turn, still show that call
4445 if (idx < 0) idx = (currentPlayer == 1 ? calledPocketP2 : calledPocketP1);
4446 if (idx < 0) idx = 2; // default to top?right if somehow still unset
4447
4448 // 2) Draw large blue arrow
4449 ID2D1SolidColorBrush* pArrow = nullptr;
4450 pRT->CreateSolidColorBrush(TURN_ARROW_COLOR, &pArrow);
4451 if (pArrow) {
4452 auto P = pocketPositions[idx];
4453 D2D1_POINT_2F tri[3] = {
4454 { P.x - 15.0f, P.y - 40.0f },
4455 { P.x + 15.0f, P.y - 40.0f },
4456 { P.x , P.y - 10.0f }
4457 };
4458 ID2D1PathGeometry* geom = nullptr;
4459 pFactory->CreatePathGeometry(&geom);
4460 ID2D1GeometrySink* sink = nullptr;
4461 geom->Open(&sink);
4462 sink->BeginFigure(tri[0], D2D1_FIGURE_BEGIN_FILLED);
4463 sink->AddLines(&tri[1], 2);
4464 sink->EndFigure(D2D1_FIGURE_END_CLOSED);
4465 sink->Close();
4466 pRT->FillGeometry(geom, pArrow);
4467 SafeRelease(&sink);
4468 SafeRelease(&geom);
4469 SafeRelease(&pArrow);
4470 }
4471
4472 // 3) Draw persistent prompt text
4473 D2D1_RECT_F txt = D2D1::RectF(
4474 TABLE_LEFT,
4475 TABLE_BOTTOM + CUSHION_THICKNESS + 5.0f,
4476 TABLE_RIGHT,
4477 TABLE_BOTTOM + CUSHION_THICKNESS + 30.0f
4478 );
4479 pRT->DrawText(
4480 L"Choose a pocket...",
4481 (UINT32)wcslen(L"Choose a pocket..."),
4482 pTextFormat,
4483 &txt,
4484 pBrush
4485 );
4486 // Note: no 'return'; allow foul/turn text to draw beneath if needed
4487 } */
4488
4489 // new code ends here
4490
4491 // --- MODIFIED: Foul Text (Large Red, Bottom Center) ---
4492 if (foulCommitted && currentGameState != SHOT_IN_PROGRESS) {
4493 ID2D1SolidColorBrush* pFoulBrush = nullptr;
4494 pRT->CreateSolidColorBrush(FOUL_TEXT_COLOR, &pFoulBrush);
4495 if (pFoulBrush && pLargeTextFormat) {
4496 // Calculate Rect for bottom-middle area
4497 float foulWidth = 200.0f; // Adjust width as needed
4498 float foulHeight = 60.0f;
4499 float foulLeft = TABLE_LEFT + (TABLE_WIDTH / 2.0f) - (foulWidth / 2.0f);
4500 // Position below the pocketed balls bar
4501 float foulTop = pocketedBallsBarRect.bottom + 10.0f;
4502 D2D1_RECT_F foulRect = D2D1::RectF(foulLeft, foulTop, foulLeft + foulWidth, foulTop + foulHeight);
4503
4504 // --- Set text alignment to center for foul text ---
4505 pLargeTextFormat->SetTextAlignment(DWRITE_TEXT_ALIGNMENT_CENTER);
4506 pLargeTextFormat->SetParagraphAlignment(DWRITE_PARAGRAPH_ALIGNMENT_CENTER);
4507
4508 pRT->DrawText(L"FOUL!", 5, pLargeTextFormat, &foulRect, pFoulBrush);
4509
4510 // --- Restore default alignment for large text if needed elsewhere ---
4511 // pLargeTextFormat->SetTextAlignment(DWRITE_TEXT_ALIGNMENT_LEADING);
4512 // pLargeTextFormat->SetParagraphAlignment(DWRITE_PARAGRAPH_ALIGNMENT_CENTER);
4513
4514 SafeRelease(&pFoulBrush);
4515 }
4516 }
4517
4518 // --- Blue Arrow & Prompt for 8?Ball Call (while choosing or after called) ---
4519 if ((currentGameState == CHOOSING_POCKET_P1
4520 || currentGameState == CHOOSING_POCKET_P2)
4521 || (calledPocketP1 >= 0 || calledPocketP2 >= 0))
4522 {
4523 // determine index:
4524 // - if a call exists, use it
4525 // - if still choosing, use hover if any
4526 // determine index: use only the clicked call; default to top?right if unset
4527 int idx = (currentPlayer == 1 ? calledPocketP1 : calledPocketP2);
4528 if (idx < 0) idx = 2;
4529
4530 // draw large blue arrow
4531 ID2D1SolidColorBrush* pArrow = nullptr;
4532 pRT->CreateSolidColorBrush(TURN_ARROW_COLOR, &pArrow);
4533 if (pArrow) {
4534 auto P = pocketPositions[idx];
4535 D2D1_POINT_2F tri[3] = {
4536 {P.x - 15.0f, P.y - 40.0f},
4537 {P.x + 15.0f, P.y - 40.0f},
4538 {P.x , P.y - 10.0f}
4539 };
4540 ID2D1PathGeometry* geom = nullptr;
4541 pFactory->CreatePathGeometry(&geom);
4542 ID2D1GeometrySink* sink = nullptr;
4543 geom->Open(&sink);
4544 sink->BeginFigure(tri[0], D2D1_FIGURE_BEGIN_FILLED);
4545 sink->AddLines(&tri[1], 2);
4546 sink->EndFigure(D2D1_FIGURE_END_CLOSED);
4547 sink->Close();
4548 pRT->FillGeometry(geom, pArrow);
4549 SafeRelease(&sink); SafeRelease(&geom); SafeRelease(&pArrow);
4550 }
4551
4552 // draw prompt below pockets
4553 D2D1_RECT_F txt = D2D1::RectF(
4554 TABLE_LEFT,
4555 TABLE_BOTTOM + CUSHION_THICKNESS + 5.0f,
4556 TABLE_RIGHT,
4557 TABLE_BOTTOM + CUSHION_THICKNESS + 30.0f
4558 );
4559 pRT->DrawText(
4560 L"Choose a pocket...",
4561 (UINT32)wcslen(L"Choose a pocket..."),
4562 pTextFormat,
4563 &txt,
4564 pBrush
4565 );
4566 // do NOT return here; allow foul/turn text to display under the arrow
4567 }
4568
4569 // Removed Obsolete
4570 /*
4571 // --- 8-Ball Pocket Selection Arrow & Prompt ---
4572 if (currentGameState == CHOOSING_POCKET_P1 || currentGameState == CHOOSING_POCKET_P2) {
4573 // Determine which pocket to highlight (default to Top-Right if unset)
4574 int idx = (currentPlayer == 1) ? calledPocketP1 : calledPocketP2;
4575 if (idx < 0) idx = 2;
4576
4577 // Draw the downward arrow
4578 ID2D1SolidColorBrush* pArrowBrush = nullptr;
4579 pRT->CreateSolidColorBrush(TURN_ARROW_COLOR, &pArrowBrush);
4580 if (pArrowBrush) {
4581 D2D1_POINT_2F P = pocketPositions[idx];
4582 D2D1_POINT_2F tri[3] = {
4583 {P.x - 10.0f, P.y - 30.0f},
4584 {P.x + 10.0f, P.y - 30.0f},
4585 {P.x , P.y - 10.0f}
4586 };
4587 ID2D1PathGeometry* geom = nullptr;
4588 pFactory->CreatePathGeometry(&geom);
4589 ID2D1GeometrySink* sink = nullptr;
4590 geom->Open(&sink);
4591 sink->BeginFigure(tri[0], D2D1_FIGURE_BEGIN_FILLED);
4592 sink->AddLines(&tri[1], 2);
4593 sink->EndFigure(D2D1_FIGURE_END_CLOSED);
4594 sink->Close();
4595 pRT->FillGeometry(geom, pArrowBrush);
4596 SafeRelease(&sink);
4597 SafeRelease(&geom);
4598 SafeRelease(&pArrowBrush);
4599 }
4600
4601 // Draw “Choose a pocket...” text under the table
4602 D2D1_RECT_F prompt = D2D1::RectF(
4603 TABLE_LEFT,
4604 TABLE_BOTTOM + CUSHION_THICKNESS + 5.0f,
4605 TABLE_RIGHT,
4606 TABLE_BOTTOM + CUSHION_THICKNESS + 30.0f
4607 );
4608 pRT->DrawText(
4609 L"Choose a pocket...",
4610 (UINT32)wcslen(L"Choose a pocket..."),
4611 pTextFormat,
4612 &prompt,
4613 pBrush
4614 );
4615
4616 return; // Skip normal turn/foul text
4617 }
4618 */
4619
4620
4621 // Show AI Thinking State (Unchanged from previous step)
4622 if (currentGameState == AI_THINKING && pTextFormat) {
4623 ID2D1SolidColorBrush* pThinkingBrush = nullptr;
4624 pRT->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::Orange), &pThinkingBrush);
4625 if (pThinkingBrush) {
4626 D2D1_RECT_F thinkingRect = p2Rect;
4627 thinkingRect.top += 20; // Offset within P2 box
4628 // Ensure default text alignment for this
4629 pTextFormat->SetTextAlignment(DWRITE_TEXT_ALIGNMENT_CENTER);
4630 pTextFormat->SetParagraphAlignment(DWRITE_PARAGRAPH_ALIGNMENT_CENTER);
4631 pRT->DrawText(L"Thinking...", 11, pTextFormat, &thinkingRect, pThinkingBrush);
4632 SafeRelease(&pThinkingBrush);
4633 }
4634 }
4635
4636 SafeRelease(&pBrush);
4637
4638 // --- Draw CHEAT MODE label if active ---
4639 if (cheatModeEnabled) {
4640 ID2D1SolidColorBrush* pCheatBrush = nullptr;
4641 pRT->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::Red), &pCheatBrush);
4642 if (pCheatBrush && pTextFormat) {
4643 D2D1_RECT_F cheatTextRect = D2D1::RectF(
4644 TABLE_LEFT + 10.0f,
4645 TABLE_TOP + 10.0f,
4646 TABLE_LEFT + 200.0f,
4647 TABLE_TOP + 40.0f
4648 );
4649 pTextFormat->SetTextAlignment(DWRITE_TEXT_ALIGNMENT_LEADING);
4650 pTextFormat->SetParagraphAlignment(DWRITE_PARAGRAPH_ALIGNMENT_NEAR);
4651 pRT->DrawText(L"CHEAT MODE ON", wcslen(L"CHEAT MODE ON"), pTextFormat, &cheatTextRect, pCheatBrush);
4652 }
4653 SafeRelease(&pCheatBrush);
4654 }
4655 }
4656
4657 void DrawPowerMeter(ID2D1RenderTarget* pRT) {
4658 // Draw Border
4659 ID2D1SolidColorBrush* pBorderBrush = nullptr;
4660 pRT->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::Black), &pBorderBrush);
4661 if (!pBorderBrush) return;
4662 pRT->DrawRectangle(&powerMeterRect, pBorderBrush, 2.0f);
4663 SafeRelease(&pBorderBrush);
4664
4665 // Create Gradient Fill
4666 ID2D1GradientStopCollection* pGradientStops = nullptr;
4667 ID2D1LinearGradientBrush* pGradientBrush = nullptr;
4668 D2D1_GRADIENT_STOP gradientStops[4];
4669 gradientStops[0].position = 0.0f;
4670 gradientStops[0].color = D2D1::ColorF(D2D1::ColorF::Green);
4671 gradientStops[1].position = 0.45f;
4672 gradientStops[1].color = D2D1::ColorF(D2D1::ColorF::Yellow);
4673 gradientStops[2].position = 0.7f;
4674 gradientStops[2].color = D2D1::ColorF(D2D1::ColorF::Orange);
4675 gradientStops[3].position = 1.0f;
4676 gradientStops[3].color = D2D1::ColorF(D2D1::ColorF::Red);
4677
4678 pRT->CreateGradientStopCollection(gradientStops, 4, &pGradientStops);
4679 if (pGradientStops) {
4680 D2D1_LINEAR_GRADIENT_BRUSH_PROPERTIES props = {};
4681 props.startPoint = D2D1::Point2F(powerMeterRect.left, powerMeterRect.bottom);
4682 props.endPoint = D2D1::Point2F(powerMeterRect.left, powerMeterRect.top);
4683 pRT->CreateLinearGradientBrush(props, pGradientStops, &pGradientBrush);
4684 SafeRelease(&pGradientStops);
4685 }
4686
4687 // Calculate Fill Height
4688 float fillRatio = 0;
4689 //if (isAiming && (currentGameState == AIMING || currentGameState == BREAKING)) {
4690 // Determine if power meter should reflect shot power (human aiming or AI preparing)
4691 bool humanIsAimingPower = isAiming && (currentGameState == AIMING || currentGameState == BREAKING);
4692 // NEW Condition: AI is displaying its aim, so show its chosen power
4693 bool aiIsVisualizingPower = (isPlayer2AI && currentPlayer == 2 &&
4694 currentGameState == AI_THINKING && aiIsDisplayingAim);
4695
4696 if (humanIsAimingPower || aiIsVisualizingPower) { // Use the new condition
4697 fillRatio = shotPower / MAX_SHOT_POWER;
4698 }
4699 float fillHeight = (powerMeterRect.bottom - powerMeterRect.top) * fillRatio;
4700 D2D1_RECT_F fillRect = D2D1::RectF(
4701 powerMeterRect.left,
4702 powerMeterRect.bottom - fillHeight,
4703 powerMeterRect.right,
4704 powerMeterRect.bottom
4705 );
4706
4707 if (pGradientBrush) {
4708 pRT->FillRectangle(&fillRect, pGradientBrush);
4709 SafeRelease(&pGradientBrush);
4710 }
4711
4712 // Draw scale notches
4713 ID2D1SolidColorBrush* pNotchBrush = nullptr;
4714 pRT->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::Black), &pNotchBrush);
4715 if (pNotchBrush) {
4716 for (int i = 0; i <= 8; ++i) {
4717 float y = powerMeterRect.top + (powerMeterRect.bottom - powerMeterRect.top) * (i / 8.0f);
4718 pRT->DrawLine(
4719 D2D1::Point2F(powerMeterRect.right + 2.0f, y),
4720 D2D1::Point2F(powerMeterRect.right + 8.0f, y),
4721 pNotchBrush,
4722 1.5f
4723 );
4724 }
4725 SafeRelease(&pNotchBrush);
4726 }
4727
4728 // Draw "Power" Label Below Meter
4729 if (pTextFormat) {
4730 ID2D1SolidColorBrush* pTextBrush = nullptr;
4731 pRT->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::Black), &pTextBrush);
4732 if (pTextBrush) {
4733 D2D1_RECT_F textRect = D2D1::RectF(
4734 powerMeterRect.left - 20.0f,
4735 powerMeterRect.bottom + 8.0f,
4736 powerMeterRect.right + 20.0f,
4737 powerMeterRect.bottom + 38.0f
4738 );
4739 pTextFormat->SetTextAlignment(DWRITE_TEXT_ALIGNMENT_CENTER);
4740 pTextFormat->SetParagraphAlignment(DWRITE_PARAGRAPH_ALIGNMENT_NEAR);
4741 pRT->DrawText(L"Power", 5, pTextFormat, &textRect, pTextBrush);
4742 SafeRelease(&pTextBrush);
4743 }
4744 }
4745
4746 // Draw Glow Effect if fully charged or fading out
4747 static float glowPulse = 0.0f;
4748 static bool glowIncreasing = true;
4749 static float glowFadeOut = 0.0f; // NEW: tracks fading out
4750
4751 if (shotPower >= MAX_SHOT_POWER * 0.99f) {
4752 // While fully charged, keep pulsing normally
4753 if (glowIncreasing) {
4754 glowPulse += 0.02f;
4755 if (glowPulse >= 1.0f) glowIncreasing = false;
4756 }
4757 else {
4758 glowPulse -= 0.02f;
4759 if (glowPulse <= 0.0f) glowIncreasing = true;
4760 }
4761 glowFadeOut = 1.0f; // Reset fade out to full
4762 }
4763 else if (glowFadeOut > 0.0f) {
4764 // If shot fired, gradually fade out
4765 glowFadeOut -= 0.02f;
4766 if (glowFadeOut < 0.0f) glowFadeOut = 0.0f;
4767 }
4768
4769 if (glowFadeOut > 0.0f) {
4770 ID2D1SolidColorBrush* pGlowBrush = nullptr;
4771 float effectiveOpacity = (0.3f + 0.7f * glowPulse) * glowFadeOut;
4772 pRT->CreateSolidColorBrush(
4773 D2D1::ColorF(D2D1::ColorF::Red, effectiveOpacity),
4774 &pGlowBrush
4775 );
4776 if (pGlowBrush) {
4777 float glowCenterX = (powerMeterRect.left + powerMeterRect.right) / 2.0f;
4778 float glowCenterY = powerMeterRect.top;
4779 D2D1_ELLIPSE glowEllipse = D2D1::Ellipse(
4780 D2D1::Point2F(glowCenterX, glowCenterY - 10.0f),
4781 12.0f + 3.0f * glowPulse,
4782 6.0f + 2.0f * glowPulse
4783 );
4784 pRT->FillEllipse(&glowEllipse, pGlowBrush);
4785 SafeRelease(&pGlowBrush);
4786 }
4787 }
4788 }
4789
4790 void DrawSpinIndicator(ID2D1RenderTarget* pRT) {
4791 ID2D1SolidColorBrush* pWhiteBrush = nullptr;
4792 ID2D1SolidColorBrush* pRedBrush = nullptr;
4793
4794 pRT->CreateSolidColorBrush(CUE_BALL_COLOR, &pWhiteBrush);
4795 pRT->CreateSolidColorBrush(ENGLISH_DOT_COLOR, &pRedBrush);
4796
4797 if (!pWhiteBrush || !pRedBrush) {
4798 SafeRelease(&pWhiteBrush);
4799 SafeRelease(&pRedBrush);
4800 return;
4801 }
4802
4803 // Draw White Ball Background
4804 D2D1_ELLIPSE bgEllipse = D2D1::Ellipse(spinIndicatorCenter, spinIndicatorRadius, spinIndicatorRadius);
4805 pRT->FillEllipse(&bgEllipse, pWhiteBrush);
4806 pRT->DrawEllipse(&bgEllipse, pRedBrush, 0.5f); // Thin red border
4807
4808
4809 // Draw Red Dot for Spin Position
4810 float dotRadius = 4.0f;
4811 float dotX = spinIndicatorCenter.x + cueSpinX * (spinIndicatorRadius - dotRadius); // Keep dot inside edge
4812 float dotY = spinIndicatorCenter.y + cueSpinY * (spinIndicatorRadius - dotRadius);
4813 D2D1_ELLIPSE dotEllipse = D2D1::Ellipse(D2D1::Point2F(dotX, dotY), dotRadius, dotRadius);
4814 pRT->FillEllipse(&dotEllipse, pRedBrush);
4815
4816 SafeRelease(&pWhiteBrush);
4817 SafeRelease(&pRedBrush);
4818 }
4819
4820
4821 void DrawPocketedBallsIndicator(ID2D1RenderTarget* pRT) {
4822 ID2D1SolidColorBrush* pBgBrush = nullptr;
4823 ID2D1SolidColorBrush* pBallBrush = nullptr;
4824
4825 // Ensure render target is valid before proceeding
4826 if (!pRT) return;
4827
4828 HRESULT hr = pRT->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::Black, 0.8f), &pBgBrush); // Semi-transparent black
4829 if (FAILED(hr)) { SafeRelease(&pBgBrush); return; } // Exit if brush creation fails
4830
4831 hr = pRT->CreateSolidColorBrush(D2D1::ColorF(0, 0, 0), &pBallBrush); // Placeholder, color will be set per ball
4832 if (FAILED(hr)) {
4833 SafeRelease(&pBgBrush);
4834 SafeRelease(&pBallBrush);
4835 return; // Exit if brush creation fails
4836 }
4837
4838 // Draw the background bar (rounded rect)
4839 D2D1_ROUNDED_RECT roundedRect = D2D1::RoundedRect(pocketedBallsBarRect, 10.0f, 10.0f); // Corner radius 10
4840 float baseAlpha = 0.8f;
4841 float flashBoost = pocketFlashTimer * 0.5f; // Make flash effect boost alpha slightly
4842 float finalAlpha = std::min(1.0f, baseAlpha + flashBoost);
4843 pBgBrush->SetOpacity(finalAlpha);
4844 pRT->FillRoundedRectangle(&roundedRect, pBgBrush);
4845 pBgBrush->SetOpacity(1.0f); // Reset opacity after drawing
4846
4847 // --- Draw small circles for pocketed balls inside the bar ---
4848
4849 // Calculate dimensions based on the bar's height for better scaling
4850 float barHeight = pocketedBallsBarRect.bottom - pocketedBallsBarRect.top;
4851 float ballDisplayRadius = barHeight * 0.30f; // Make balls slightly smaller relative to bar height
4852 float spacing = ballDisplayRadius * 2.2f; // Adjust spacing slightly
4853 float padding = spacing * 0.75f; // Add padding from the edges
4854 float center_Y = pocketedBallsBarRect.top + barHeight / 2.0f; // Vertical center
4855
4856 // Starting X positions with padding
4857 float currentX_P1 = pocketedBallsBarRect.left + padding;
4858 float currentX_P2 = pocketedBallsBarRect.right - padding; // Start from right edge minus padding
4859
4860 int p1DrawnCount = 0;
4861 int p2DrawnCount = 0;
4862 const int maxBallsToShow = 7; // Max balls per player in the bar
4863
4864 for (const auto& b : balls) {
4865 if (b.isPocketed) {
4866 // Skip cue ball and 8-ball in this indicator
4867 if (b.id == 0 || b.id == 8) continue;
4868
4869 bool isPlayer1Ball = (player1Info.assignedType != BallType::NONE && b.type == player1Info.assignedType);
4870 bool isPlayer2Ball = (player2Info.assignedType != BallType::NONE && b.type == player2Info.assignedType);
4871
4872 if (isPlayer1Ball && p1DrawnCount < maxBallsToShow) {
4873 pBallBrush->SetColor(b.color);
4874 // Draw P1 balls from left to right
4875 D2D1_ELLIPSE ballEllipse = D2D1::Ellipse(D2D1::Point2F(currentX_P1 + p1DrawnCount * spacing, center_Y), ballDisplayRadius, ballDisplayRadius);
4876 pRT->FillEllipse(&ballEllipse, pBallBrush);
4877 p1DrawnCount++;
4878 }
4879 else if (isPlayer2Ball && p2DrawnCount < maxBallsToShow) {
4880 pBallBrush->SetColor(b.color);
4881 // Draw P2 balls from right to left
4882 D2D1_ELLIPSE ballEllipse = D2D1::Ellipse(D2D1::Point2F(currentX_P2 - p2DrawnCount * spacing, center_Y), ballDisplayRadius, ballDisplayRadius);
4883 pRT->FillEllipse(&ballEllipse, pBallBrush);
4884 p2DrawnCount++;
4885 }
4886 // Note: Balls pocketed before assignment or opponent balls are intentionally not shown here.
4887 // You could add logic here to display them differently if needed (e.g., smaller, grayed out).
4888 }
4889 }
4890
4891 SafeRelease(&pBgBrush);
4892 SafeRelease(&pBallBrush);
4893 }
4894
4895 void DrawBallInHandIndicator(ID2D1RenderTarget* pRT) {
4896 if (!isDraggingCueBall && (currentGameState != BALL_IN_HAND_P1 && currentGameState != BALL_IN_HAND_P2 && currentGameState != PRE_BREAK_PLACEMENT)) {
4897 return; // Only show when placing/dragging
4898 }
4899
4900 Ball* cueBall = GetCueBall();
4901 if (!cueBall) return;
4902
4903 ID2D1SolidColorBrush* pGhostBrush = nullptr;
4904 pRT->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::White, 0.6f), &pGhostBrush); // Semi-transparent white
4905
4906 if (pGhostBrush) {
4907 D2D1_POINT_2F drawPos;
4908 if (isDraggingCueBall) {
4909 drawPos = D2D1::Point2F((float)ptMouse.x, (float)ptMouse.y);
4910 }
4911 else {
4912 // If not dragging but in placement state, show at current ball pos
4913 drawPos = D2D1::Point2F(cueBall->x, cueBall->y);
4914 }
4915
4916 // Check if the placement is valid before drawing differently?
4917 bool behindHeadstring = (currentGameState == PRE_BREAK_PLACEMENT);
4918 bool isValid = IsValidCueBallPosition(drawPos.x, drawPos.y, behindHeadstring);
4919
4920 if (!isValid) {
4921 // Maybe draw red outline if invalid placement?
4922 pGhostBrush->SetColor(D2D1::ColorF(D2D1::ColorF::Red, 0.6f));
4923 }
4924
4925
4926 D2D1_ELLIPSE ghostEllipse = D2D1::Ellipse(drawPos, BALL_RADIUS, BALL_RADIUS);
4927 pRT->FillEllipse(&ghostEllipse, pGhostBrush);
4928 pRT->DrawEllipse(&ghostEllipse, pGhostBrush, 1.0f); // Outline
4929
4930 SafeRelease(&pGhostBrush);
4931 }
4932 }
4933
4934 void DrawPocketSelectionIndicator(ID2D1RenderTarget* pRT) {
4935 /* Never show the arrow while the player is still placing the
4936 cue-ball (ball-in-hand) – it otherwise hides behind the
4937 ghost-ball and can lock the UI. */
4938
4939 /* Still skip the opening-break placement,
4940 but show the arrow during BALL-IN-HAND */
4941 // ? skip when no active call for the CURRENT shooter
4942 if ((currentPlayer == 1 && calledPocketP1 < 0) ||
4943 (currentPlayer == 2 && calledPocketP2 < 0)) return;
4944 /*if (currentGameState == PRE_BREAK_PLACEMENT)
4945 return;*/ //new ai-asked-to-disable
4946 /*if (currentGameState == BALL_IN_HAND_P1 ||
4947 currentGameState == BALL_IN_HAND_P2 ||
4948 currentGameState == PRE_BREAK_PLACEMENT)
4949 {
4950 return;
4951 }*/
4952
4953 int pocketToIndicate = -1;
4954 // Whenever EITHER player has pocketed their first 7 and has called (human or AI),
4955 // we forcibly show their arrow—regardless of currentGameState.
4956 if ((currentPlayer == 1 && player1Info.ballsPocketedCount >= 7 && calledPocketP1 >= 0) ||
4957 (currentPlayer == 2 && player2Info.ballsPocketedCount >= 7 && calledPocketP2 >= 0))
4958 {
4959 pocketToIndicate = (currentPlayer == 1) ? calledPocketP1 : calledPocketP2;
4960 }
4961 /*// A human player is actively choosing if they are in the CHOOSING_POCKET state.
4962 bool isHumanChoosing = (currentGameState == CHOOSING_POCKET_P1 || (currentGameState == CHOOSING_POCKET_P2 && !isPlayer2AI));
4963
4964 if (isHumanChoosing) {
4965 // When choosing, show the currently selected pocket (which has a default).
4966 pocketToIndicate = (currentPlayer == 1) ? calledPocketP1 : calledPocketP2;
4967 }
4968 else if (IsPlayerOnEightBall(currentPlayer)) {
4969 // If it's a normal turn but the player is on the 8-ball, show their called pocket as a reminder.
4970 pocketToIndicate = (currentPlayer == 1) ? calledPocketP1 : calledPocketP2;
4971 }*/
4972
4973 if (pocketToIndicate < 0 || pocketToIndicate > 5) {
4974 return; // Don't draw if no pocket is selected or relevant.
4975 }
4976
4977 ID2D1SolidColorBrush* pArrowBrush = nullptr;
4978 pRT->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::Yellow, 0.9f), &pArrowBrush);
4979 if (!pArrowBrush) return;
4980
4981 // ... The rest of your arrow drawing geometry logic remains exactly the same ...
4982 // (No changes needed to the points/path drawing, only the logic above)
4983 D2D1_POINT_2F targetPocketCenter = pocketPositions[pocketToIndicate];
4984 float arrowHeadSize = HOLE_VISUAL_RADIUS * 0.5f;
4985 float arrowShaftLength = HOLE_VISUAL_RADIUS * 0.3f;
4986 float arrowShaftWidth = arrowHeadSize * 0.4f;
4987 float verticalOffsetFromPocketCenter = HOLE_VISUAL_RADIUS * 1.6f;
4988 D2D1_POINT_2F tip, baseLeft, baseRight, shaftTopLeft, shaftTopRight, shaftBottomLeft, shaftBottomRight;
4989
4990 if (targetPocketCenter.y == TABLE_TOP) {
4991 tip = D2D1::Point2F(targetPocketCenter.x, targetPocketCenter.y + verticalOffsetFromPocketCenter + arrowHeadSize);
4992 baseLeft = D2D1::Point2F(targetPocketCenter.x - arrowHeadSize / 2.0f, targetPocketCenter.y + verticalOffsetFromPocketCenter);
4993 baseRight = D2D1::Point2F(targetPocketCenter.x + arrowHeadSize / 2.0f, targetPocketCenter.y + verticalOffsetFromPocketCenter);
4994 shaftTopLeft = D2D1::Point2F(targetPocketCenter.x - arrowShaftWidth / 2.0f, baseLeft.y);
4995 shaftTopRight = D2D1::Point2F(targetPocketCenter.x + arrowShaftWidth / 2.0f, baseRight.y);
4996 shaftBottomLeft = D2D1::Point2F(targetPocketCenter.x - arrowShaftWidth / 2.0f, baseLeft.y - arrowShaftLength);
4997 shaftBottomRight = D2D1::Point2F(targetPocketCenter.x + arrowShaftWidth / 2.0f, baseRight.y - arrowShaftLength);
4998 }
4999 else {
5000 tip = D2D1::Point2F(targetPocketCenter.x, targetPocketCenter.y - verticalOffsetFromPocketCenter - arrowHeadSize);
5001 baseLeft = D2D1::Point2F(targetPocketCenter.x - arrowHeadSize / 2.0f, targetPocketCenter.y - verticalOffsetFromPocketCenter);
5002 baseRight = D2D1::Point2F(targetPocketCenter.x + arrowHeadSize / 2.0f, targetPocketCenter.y - verticalOffsetFromPocketCenter);
5003 shaftTopLeft = D2D1::Point2F(targetPocketCenter.x - arrowShaftWidth / 2.0f, baseLeft.y + arrowShaftLength);
5004 shaftTopRight = D2D1::Point2F(targetPocketCenter.x + arrowShaftWidth / 2.0f, baseRight.y + arrowShaftLength);
5005 shaftBottomLeft = D2D1::Point2F(targetPocketCenter.x - arrowShaftWidth / 2.0f, baseLeft.y);
5006 shaftBottomRight = D2D1::Point2F(targetPocketCenter.x + arrowShaftWidth / 2.0f, baseRight.y);
5007 }
5008
5009 ID2D1PathGeometry* pPath = nullptr;
5010 if (SUCCEEDED(pFactory->CreatePathGeometry(&pPath))) {
5011 ID2D1GeometrySink* pSink = nullptr;
5012 if (SUCCEEDED(pPath->Open(&pSink))) {
5013 pSink->BeginFigure(tip, D2D1_FIGURE_BEGIN_FILLED);
5014 pSink->AddLine(baseLeft); pSink->AddLine(shaftBottomLeft); pSink->AddLine(shaftTopLeft);
5015 pSink->AddLine(shaftTopRight); pSink->AddLine(shaftBottomRight); pSink->AddLine(baseRight);
5016 pSink->EndFigure(D2D1_FIGURE_END_CLOSED);
5017 pSink->Close();
5018 SafeRelease(&pSink);
5019 pRT->FillGeometry(pPath, pArrowBrush);
5020 }
5021 SafeRelease(&pPath);
5022 }
5023 SafeRelease(&pArrowBrush);
5024 }
5025```