· 2 months ago · Jul 11, 2025, 06:20 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.1608f, 0.4000f, 0.1765f); // 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 // Draw Cushions (Red Border)
3387 pRT->CreateSolidColorBrush(CUSHION_COLOR, &pBrush);
3388 if (!pBrush) return;
3389 // Top Cushion (split by middle pocket)
3390 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);
3391 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);
3392 // Bottom Cushion (split by middle pocket)
3393 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);
3394 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);
3395 // Left Cushion
3396 pRT->FillRectangle(D2D1::RectF(TABLE_LEFT - CUSHION_THICKNESS, TABLE_TOP + HOLE_VISUAL_RADIUS, TABLE_LEFT, TABLE_BOTTOM - HOLE_VISUAL_RADIUS), pBrush);
3397 // Right Cushion
3398 pRT->FillRectangle(D2D1::RectF(TABLE_RIGHT, TABLE_TOP + HOLE_VISUAL_RADIUS, TABLE_RIGHT + CUSHION_THICKNESS, TABLE_BOTTOM - HOLE_VISUAL_RADIUS), pBrush);
3399 SafeRelease(&pBrush);
3400
3401
3402 // Draw Pockets (Black Circles)
3403 pRT->CreateSolidColorBrush(POCKET_COLOR, &pBrush);
3404 if (!pBrush) return;
3405 for (int i = 0; i < 6; ++i) {
3406 D2D1_ELLIPSE ellipse = D2D1::Ellipse(pocketPositions[i], HOLE_VISUAL_RADIUS, HOLE_VISUAL_RADIUS);
3407 pRT->FillEllipse(&ellipse, pBrush);
3408 }
3409 SafeRelease(&pBrush);
3410
3411 // Draw Headstring Line (White)
3412 pRT->CreateSolidColorBrush(D2D1::ColorF(0.4235f, 0.5647f, 0.1765f, 1.0f), &pBrush); // NEWCOLOR ::White => (0.2784, 0.4549, 0.1843)
3413 //pRT->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::White, 0.5f), &pBrush); // NEWCOLOR ::White => (0.2784, 0.4549, 0.1843)
3414 if (!pBrush) return;
3415 pRT->DrawLine(
3416 D2D1::Point2F(HEADSTRING_X, TABLE_TOP),
3417 D2D1::Point2F(HEADSTRING_X, TABLE_BOTTOM),
3418 pBrush,
3419 1.0f // Line thickness
3420 );
3421 SafeRelease(&pBrush);
3422
3423 // Draw Semicircle facing West (flat side East)
3424 // Draw Semicircle facing East (curved side on the East, flat side on the West)
3425 ID2D1PathGeometry* pGeometry = nullptr;
3426 HRESULT hr = pFactory->CreatePathGeometry(&pGeometry);
3427 if (SUCCEEDED(hr) && pGeometry)
3428 {
3429 ID2D1GeometrySink* pSink = nullptr;
3430 hr = pGeometry->Open(&pSink);
3431 if (SUCCEEDED(hr) && pSink)
3432 {
3433 float radius = 60.0f; // Radius for the semicircle
3434 D2D1_POINT_2F center = D2D1::Point2F(HEADSTRING_X, (TABLE_TOP + TABLE_BOTTOM) / 2.0f);
3435
3436 // For a semicircle facing East (curved side on the East), use the top and bottom points.
3437 D2D1_POINT_2F startPoint = D2D1::Point2F(center.x, center.y - radius); // Top point
3438
3439 pSink->BeginFigure(startPoint, D2D1_FIGURE_BEGIN_HOLLOW);
3440
3441 D2D1_ARC_SEGMENT arc = {};
3442 arc.point = D2D1::Point2F(center.x, center.y + radius); // Bottom point
3443 arc.size = D2D1::SizeF(radius, radius);
3444 arc.rotationAngle = 0.0f;
3445 // Use the correct identifier with the extra underscore:
3446 arc.sweepDirection = D2D1_SWEEP_DIRECTION_COUNTER_CLOCKWISE;
3447 arc.arcSize = D2D1_ARC_SIZE_SMALL;
3448
3449 pSink->AddArc(&arc);
3450 pSink->EndFigure(D2D1_FIGURE_END_OPEN);
3451 pSink->Close();
3452 SafeRelease(&pSink);
3453
3454 ID2D1SolidColorBrush* pArcBrush = nullptr;
3455 //pRT->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::White, 0.3f), &pArcBrush);
3456 pRT->CreateSolidColorBrush(D2D1::ColorF(0.4235f, 0.5647f, 0.1765f, 1.0f), &pArcBrush);
3457 if (pArcBrush)
3458 {
3459 pRT->DrawGeometry(pGeometry, pArcBrush, 1.5f);
3460 SafeRelease(&pArcBrush);
3461 }
3462 }
3463 SafeRelease(&pGeometry);
3464 }
3465
3466
3467
3468
3469 }
3470
3471
3472 // ----------------------------------------------
3473 // Helper : clamp to [0,1] and lighten a colour
3474 // ----------------------------------------------
3475 static D2D1_COLOR_F Lighten(const D2D1_COLOR_F& c, float factor = 1.25f)
3476 {
3477 return D2D1::ColorF(
3478 std::min(1.0f, c.r * factor),
3479 std::min(1.0f, c.g * factor),
3480 std::min(1.0f, c.b * factor),
3481 c.a);
3482 }
3483
3484 // ------------------------------------------------
3485 // NEW DrawBalls – radial-gradient “spot-light”
3486 // ------------------------------------------------
3487 void DrawBalls(ID2D1RenderTarget* pRT)
3488 {
3489 if (!pRT) return;
3490
3491 ID2D1SolidColorBrush* pStripeBrush = nullptr; // white stripe
3492 ID2D1SolidColorBrush* pBorderBrush = nullptr; // black ring
3493
3494 pRT->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::White), &pStripeBrush);
3495 pRT->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::Black), &pBorderBrush);
3496
3497 for (const Ball& b : balls)
3498 {
3499 if (b.isPocketed) continue;
3500
3501 //------------------------------------------
3502 // Build the radial gradient for THIS ball
3503 //------------------------------------------
3504 ID2D1GradientStopCollection* pStops = nullptr;
3505 ID2D1RadialGradientBrush* pRad = nullptr;
3506
3507 D2D1_GRADIENT_STOP gs[3];
3508 gs[0].position = 0.0f; gs[0].color = D2D1::ColorF(1, 1, 1, 0.95f); // bright spot
3509 gs[1].position = 0.35f; gs[1].color = Lighten(b.color); // transitional
3510 gs[2].position = 1.0f; gs[2].color = b.color; // base colour
3511
3512 pRT->CreateGradientStopCollection(gs, 3, &pStops);
3513
3514 if (pStops)
3515 {
3516 // Place the hot-spot slightly towards top-left to look more 3-D
3517 D2D1_POINT_2F origin = D2D1::Point2F(b.x - BALL_RADIUS * 0.4f,
3518 b.y - BALL_RADIUS * 0.4f);
3519
3520 D2D1_RADIAL_GRADIENT_BRUSH_PROPERTIES props =
3521 D2D1::RadialGradientBrushProperties(
3522 origin, // gradientOrigin
3523 D2D1::Point2F(0, 0), // offset (not used here)
3524 BALL_RADIUS * 1.3f, // radiusX
3525 BALL_RADIUS * 1.3f); // radiusY
3526
3527 pRT->CreateRadialGradientBrush(props, pStops, &pRad);
3528 SafeRelease(&pStops);
3529 }
3530
3531 //------------------------------------------
3532 // Draw the solid or striped ball itself
3533 //------------------------------------------
3534 D2D1_ELLIPSE outer = D2D1::Ellipse(
3535 D2D1::Point2F(b.x, b.y), BALL_RADIUS, BALL_RADIUS);
3536
3537 if (pRad) pRT->FillEllipse(&outer, pRad);
3538
3539 // ---------- Stripe overlay -------------
3540 if (b.type == BallType::STRIPE && pStripeBrush)
3541 {
3542 // White band
3543 D2D1_RECT_F stripe = D2D1::RectF(
3544 b.x - BALL_RADIUS,
3545 b.y - BALL_RADIUS * 0.40f,
3546 b.x + BALL_RADIUS,
3547 b.y + BALL_RADIUS * 0.40f);
3548 pRT->FillRectangle(&stripe, pStripeBrush);
3549
3550 // Inner circle (give stripe area same glossy shading)
3551 if (pRad)
3552 {
3553 D2D1_ELLIPSE inner = D2D1::Ellipse(
3554 D2D1::Point2F(b.x, b.y),
3555 BALL_RADIUS * 0.60f,
3556 BALL_RADIUS * 0.60f);
3557 pRT->FillEllipse(&inner, pRad);
3558 }
3559 }
3560
3561 // Black border
3562 if (pBorderBrush)
3563 pRT->DrawEllipse(&outer, pBorderBrush, 1.5f);
3564
3565 SafeRelease(&pRad);
3566 }
3567
3568 SafeRelease(&pStripeBrush);
3569 SafeRelease(&pBorderBrush);
3570 }
3571
3572 /*void DrawBalls(ID2D1RenderTarget* pRT) {
3573 ID2D1SolidColorBrush* pBrush = nullptr;
3574 ID2D1SolidColorBrush* pStripeBrush = nullptr; // For stripe pattern
3575
3576 pRT->CreateSolidColorBrush(D2D1::ColorF(0, 0, 0), &pBrush); // Placeholder
3577 pRT->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::White), &pStripeBrush);
3578
3579 if (!pBrush || !pStripeBrush) {
3580 SafeRelease(&pBrush);
3581 SafeRelease(&pStripeBrush);
3582 return;
3583 }
3584
3585
3586 for (size_t i = 0; i < balls.size(); ++i) {
3587 const Ball& b = balls[i];
3588 if (!b.isPocketed) {
3589 D2D1_ELLIPSE ellipse = D2D1::Ellipse(D2D1::Point2F(b.x, b.y), BALL_RADIUS, BALL_RADIUS);
3590
3591 // Set main ball color
3592 pBrush->SetColor(b.color);
3593 pRT->FillEllipse(&ellipse, pBrush);
3594
3595 // Draw Stripe if applicable
3596 if (b.type == BallType::STRIPE) {
3597 // Draw a white band across the middle (simplified stripe)
3598 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);
3599 // Need to clip this rectangle to the ellipse bounds - complex!
3600 // Alternative: Draw two colored arcs leaving a white band.
3601 // Simplest: Draw a white circle inside, slightly smaller.
3602 D2D1_ELLIPSE innerEllipse = D2D1::Ellipse(D2D1::Point2F(b.x, b.y), BALL_RADIUS * 0.6f, BALL_RADIUS * 0.6f);
3603 pRT->FillEllipse(innerEllipse, pStripeBrush); // White center part
3604 pBrush->SetColor(b.color); // Set back to stripe color
3605 pRT->FillEllipse(innerEllipse, pBrush); // Fill again, leaving a ring - No, this isn't right.
3606
3607 // Let's try drawing a thick white line across
3608 // This doesn't look great. Just drawing solid red for stripes for now.
3609 }
3610
3611 // Draw Number (Optional - requires more complex text layout or pre-rendered textures)
3612 // if (b.id != 0 && pTextFormat) {
3613 // std::wstring numStr = std::to_wstring(b.id);
3614 // D2D1_RECT_F textRect = D2D1::RectF(b.x - BALL_RADIUS, b.y - BALL_RADIUS, b.x + BALL_RADIUS, b.y + BALL_RADIUS);
3615 // ID2D1SolidColorBrush* pNumBrush = nullptr;
3616 // D2D1_COLOR_F numCol = (b.type == BallType::SOLID || b.id == 8) ? D2D1::ColorF(D2D1::ColorF::Black) : D2D1::ColorF(D2D1::ColorF::White);
3617 // pRT->CreateSolidColorBrush(numCol, &pNumBrush);
3618 // // Create a smaller text format...
3619 // // pRT->DrawText(numStr.c_str(), numStr.length(), pSmallTextFormat, &textRect, pNumBrush);
3620 // SafeRelease(&pNumBrush);
3621 // }
3622 }
3623 }
3624
3625 SafeRelease(&pBrush);
3626 SafeRelease(&pStripeBrush);
3627 }*/
3628
3629
3630 /*void DrawAimingAids(ID2D1RenderTarget* pRT) {
3631 // Condition check at start (Unchanged)
3632 //if (currentGameState != PLAYER1_TURN && currentGameState != PLAYER2_TURN &&
3633 //currentGameState != BREAKING && currentGameState != AIMING)
3634 //{
3635 //return;
3636 //}
3637 // NEW Condition: Allow drawing if it's a human player's active turn/aiming/breaking,
3638 // OR if it's AI's turn and it's in AI_THINKING state (calculating) or BREAKING (aiming break).
3639 bool isHumanInteracting = (!isPlayer2AI || currentPlayer == 1) &&
3640 (currentGameState == PLAYER1_TURN || currentGameState == PLAYER2_TURN ||
3641 currentGameState == BREAKING || currentGameState == AIMING);
3642 // AI_THINKING state is when AI calculates shot. AIMakeDecision sets cueAngle/shotPower.
3643 // Also include BREAKING state if it's AI's turn and isOpeningBreakShot for break aim visualization.
3644 // NEW Condition: AI is displaying its aim
3645 bool isAiVisualizingShot = (isPlayer2AI && currentPlayer == 2 &&
3646 currentGameState == AI_THINKING && aiIsDisplayingAim);
3647
3648 if (!isHumanInteracting && !(isAiVisualizingShot || (currentGameState == AI_THINKING && aiIsDisplayingAim))) {
3649 return;
3650 }
3651
3652 Ball* cueBall = GetCueBall();
3653 if (!cueBall || cueBall->isPocketed) return; // Don't draw if cue ball is gone
3654
3655 ID2D1SolidColorBrush* pBrush = nullptr;
3656 ID2D1SolidColorBrush* pGhostBrush = nullptr;
3657 ID2D1StrokeStyle* pDashedStyle = nullptr;
3658 ID2D1SolidColorBrush* pCueBrush = nullptr;
3659 ID2D1SolidColorBrush* pReflectBrush = nullptr; // Brush for reflection line
3660
3661 // Ensure render target is valid
3662 if (!pRT) return;
3663
3664 // Create Brushes and Styles (check for failures)
3665 HRESULT hr;
3666 hr = pRT->CreateSolidColorBrush(AIM_LINE_COLOR, &pBrush);
3667 if FAILED(hr) { SafeRelease(&pBrush); return; }
3668 hr = pRT->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::White, 0.5f), &pGhostBrush);
3669 if FAILED(hr) { SafeRelease(&pBrush); SafeRelease(&pGhostBrush); return; }
3670 hr = pRT->CreateSolidColorBrush(D2D1::ColorF(0.6f, 0.4f, 0.2f), &pCueBrush);
3671 if FAILED(hr) { SafeRelease(&pBrush); SafeRelease(&pGhostBrush); SafeRelease(&pCueBrush); return; }
3672 // Create reflection brush (e.g., lighter shade or different color)
3673 hr = pRT->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::LightCyan, 0.6f), &pReflectBrush);
3674 if FAILED(hr) { SafeRelease(&pBrush); SafeRelease(&pGhostBrush); SafeRelease(&pCueBrush); SafeRelease(&pReflectBrush); return; }
3675 // Create a Cyan brush for primary and secondary lines //orig(75.0f / 255.0f, 0.0f, 130.0f / 255.0f);indigoColor
3676 D2D1::ColorF cyanColor(0.0, 255.0, 255.0, 255.0f);
3677 ID2D1SolidColorBrush* pCyanBrush = nullptr;
3678 hr = pRT->CreateSolidColorBrush(cyanColor, &pCyanBrush);
3679 if (FAILED(hr)) {
3680 SafeRelease(&pCyanBrush);
3681 // handle error if needed
3682 }
3683 // Create a Purple brush for primary and secondary lines
3684 D2D1::ColorF purpleColor(255.0f, 0.0f, 255.0f, 255.0f);
3685 ID2D1SolidColorBrush* pPurpleBrush = nullptr;
3686 hr = pRT->CreateSolidColorBrush(purpleColor, &pPurpleBrush);
3687 if (FAILED(hr)) {
3688 SafeRelease(&pPurpleBrush);
3689 // handle error if needed
3690 }
3691
3692 if (pFactory) {
3693 D2D1_STROKE_STYLE_PROPERTIES strokeProps = D2D1::StrokeStyleProperties();
3694 strokeProps.dashStyle = D2D1_DASH_STYLE_DASH;
3695 hr = pFactory->CreateStrokeStyle(&strokeProps, nullptr, 0, &pDashedStyle);
3696 if FAILED(hr) { pDashedStyle = nullptr; }
3697 }
3698
3699
3700 // --- Cue Stick Drawing (Unchanged from previous fix) ---
3701 const float baseStickLength = 150.0f;
3702 const float baseStickThickness = 4.0f;
3703 float stickLength = baseStickLength * 1.4f;
3704 float stickThickness = baseStickThickness * 1.5f;
3705 float stickAngle = cueAngle + PI;
3706 float powerOffset = 0.0f;
3707 //if (isAiming && (currentGameState == AIMING || currentGameState == BREAKING)) {
3708 // Show power offset if human is aiming/dragging, or if AI is preparing its shot (AI_THINKING or AI Break)
3709 if ((isAiming && (currentGameState == AIMING || currentGameState == BREAKING)) || isAiVisualizingShot) { // Use the new condition
3710 powerOffset = shotPower * 5.0f;
3711 }
3712 D2D1_POINT_2F cueStickEnd = D2D1::Point2F(cueBall->x + cosf(stickAngle) * (stickLength + powerOffset), cueBall->y + sinf(stickAngle) * (stickLength + powerOffset));
3713 D2D1_POINT_2F cueStickTip = D2D1::Point2F(cueBall->x + cosf(stickAngle) * (powerOffset + 5.0f), cueBall->y + sinf(stickAngle) * (powerOffset + 5.0f));
3714 pRT->DrawLine(cueStickTip, cueStickEnd, pCueBrush, stickThickness);
3715
3716
3717 // --- Projection Line Calculation ---
3718 float cosA = cosf(cueAngle);
3719 float sinA = sinf(cueAngle);
3720 float rayLength = TABLE_WIDTH + TABLE_HEIGHT; // Ensure ray is long enough
3721 D2D1_POINT_2F rayStart = D2D1::Point2F(cueBall->x, cueBall->y);
3722 D2D1_POINT_2F rayEnd = D2D1::Point2F(rayStart.x + cosA * rayLength, rayStart.y + sinA * rayLength);*/
3723
3724 void DrawAimingAids(ID2D1RenderTarget* pRT) {
3725 // Determine if aiming aids should be drawn.
3726 bool isHumanInteracting = (!isPlayer2AI || currentPlayer == 1) &&
3727 (currentGameState == PLAYER1_TURN || currentGameState == PLAYER2_TURN ||
3728 currentGameState == BREAKING || currentGameState == AIMING ||
3729 currentGameState == CHOOSING_POCKET_P1 || currentGameState == CHOOSING_POCKET_P2);
3730
3731 // FOOLPROOF FIX: This is the new condition to show the AI's aim.
3732 bool isAiVisualizingShot = (isPlayer2AI && currentPlayer == 2 && aiIsDisplayingAim);
3733
3734 if (!isHumanInteracting && !isAiVisualizingShot) {
3735 return;
3736 }
3737
3738 Ball* cueBall = GetCueBall();
3739 if (!cueBall || cueBall->isPocketed) return;
3740
3741 // --- Brush and Style Creation (No changes here) ---
3742 ID2D1SolidColorBrush* pBrush = nullptr;
3743 ID2D1SolidColorBrush* pGhostBrush = nullptr;
3744 ID2D1StrokeStyle* pDashedStyle = nullptr;
3745 ID2D1SolidColorBrush* pCueBrush = nullptr;
3746 ID2D1SolidColorBrush* pReflectBrush = nullptr;
3747 ID2D1SolidColorBrush* pCyanBrush = nullptr;
3748 ID2D1SolidColorBrush* pPurpleBrush = nullptr;
3749 pRT->CreateSolidColorBrush(AIM_LINE_COLOR, &pBrush);
3750 pRT->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::White, 0.5f), &pGhostBrush);
3751 pRT->CreateSolidColorBrush(D2D1::ColorF(0.6f, 0.4f, 0.2f), &pCueBrush);
3752 pRT->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::LightCyan, 0.6f), &pReflectBrush);
3753 pRT->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::Cyan), &pCyanBrush);
3754 pRT->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::Purple), &pPurpleBrush);
3755 if (pFactory) {
3756 D2D1_STROKE_STYLE_PROPERTIES strokeProps = D2D1::StrokeStyleProperties();
3757 strokeProps.dashStyle = D2D1_DASH_STYLE_DASH;
3758 pFactory->CreateStrokeStyle(&strokeProps, nullptr, 0, &pDashedStyle);
3759 }
3760 // --- End Brush Creation ---
3761
3762 // --- FOOLPROOF FIX: Use the AI's planned angle and power for drawing ---
3763 float angleToDraw = cueAngle;
3764 float powerToDraw = shotPower;
3765
3766 if (isAiVisualizingShot) {
3767 // When the AI is showing its aim, force the drawing to use its planned shot details.
3768 angleToDraw = aiPlannedShotDetails.angle;
3769 powerToDraw = aiPlannedShotDetails.power;
3770 }
3771 // --- End AI Aiming Fix ---
3772
3773 // --- Cue Stick Drawing ---
3774 const float baseStickLength = 150.0f;
3775 const float baseStickThickness = 4.0f;
3776 float stickLength = baseStickLength * 1.4f;
3777 float stickThickness = baseStickThickness * 1.5f;
3778 float stickAngle = angleToDraw + PI; // Use the angle we determined
3779 float powerOffset = 0.0f;
3780 if ((isAiming || isDraggingStick) || isAiVisualizingShot) {
3781 powerOffset = powerToDraw * 5.0f; // Use the power we determined
3782 }
3783 D2D1_POINT_2F cueStickEnd = D2D1::Point2F(cueBall->x + cosf(stickAngle) * (stickLength + powerOffset), cueBall->y + sinf(stickAngle) * (stickLength + powerOffset));
3784 D2D1_POINT_2F cueStickTip = D2D1::Point2F(cueBall->x + cosf(stickAngle) * (powerOffset + 5.0f), cueBall->y + sinf(stickAngle) * (powerOffset + 5.0f));
3785 pRT->DrawLine(cueStickTip, cueStickEnd, pCueBrush, stickThickness);
3786
3787 // --- Projection Line Calculation ---
3788 float cosA = cosf(angleToDraw); // Use the angle we determined
3789 float sinA = sinf(angleToDraw);
3790 float rayLength = TABLE_WIDTH + TABLE_HEIGHT;
3791 D2D1_POINT_2F rayStart = D2D1::Point2F(cueBall->x, cueBall->y);
3792 D2D1_POINT_2F rayEnd = D2D1::Point2F(rayStart.x + cosA * rayLength, rayStart.y + sinA * rayLength);
3793
3794 // Find the first ball hit by the aiming ray
3795 Ball* hitBall = nullptr;
3796 float firstHitDistSq = -1.0f;
3797 D2D1_POINT_2F ballCollisionPoint = { 0, 0 }; // Point on target ball circumference
3798 D2D1_POINT_2F ghostBallPosForHit = { 0, 0 }; // Ghost ball pos for the hit ball
3799
3800 hitBall = FindFirstHitBall(rayStart, cueAngle, firstHitDistSq);
3801 if (hitBall) {
3802 // Calculate the point on the target ball's circumference
3803 float collisionDist = sqrtf(firstHitDistSq);
3804 ballCollisionPoint = D2D1::Point2F(rayStart.x + cosA * collisionDist, rayStart.y + sinA * collisionDist);
3805 // Calculate ghost ball position for this specific hit (used for projection consistency)
3806 ghostBallPosForHit = D2D1::Point2F(hitBall->x - cosA * BALL_RADIUS, hitBall->y - sinA * BALL_RADIUS); // Approx.
3807 }
3808
3809 // Find the first rail hit by the aiming ray
3810 D2D1_POINT_2F railHitPoint = rayEnd; // Default to far end if no rail hit
3811 float minRailDistSq = rayLength * rayLength;
3812 int hitRailIndex = -1; // 0:Left, 1:Right, 2:Top, 3:Bottom
3813
3814 // Define table edge segments for intersection checks
3815 D2D1_POINT_2F topLeft = D2D1::Point2F(TABLE_LEFT, TABLE_TOP);
3816 D2D1_POINT_2F topRight = D2D1::Point2F(TABLE_RIGHT, TABLE_TOP);
3817 D2D1_POINT_2F bottomLeft = D2D1::Point2F(TABLE_LEFT, TABLE_BOTTOM);
3818 D2D1_POINT_2F bottomRight = D2D1::Point2F(TABLE_RIGHT, TABLE_BOTTOM);
3819
3820 D2D1_POINT_2F currentIntersection;
3821
3822 // Check Left Rail
3823 if (LineSegmentIntersection(rayStart, rayEnd, topLeft, bottomLeft, currentIntersection)) {
3824 float distSq = GetDistanceSq(rayStart.x, rayStart.y, currentIntersection.x, currentIntersection.y);
3825 if (distSq < minRailDistSq) { minRailDistSq = distSq; railHitPoint = currentIntersection; hitRailIndex = 0; }
3826 }
3827 // Check Right Rail
3828 if (LineSegmentIntersection(rayStart, rayEnd, topRight, bottomRight, currentIntersection)) {
3829 float distSq = GetDistanceSq(rayStart.x, rayStart.y, currentIntersection.x, currentIntersection.y);
3830 if (distSq < minRailDistSq) { minRailDistSq = distSq; railHitPoint = currentIntersection; hitRailIndex = 1; }
3831 }
3832 // Check Top Rail
3833 if (LineSegmentIntersection(rayStart, rayEnd, topLeft, topRight, currentIntersection)) {
3834 float distSq = GetDistanceSq(rayStart.x, rayStart.y, currentIntersection.x, currentIntersection.y);
3835 if (distSq < minRailDistSq) { minRailDistSq = distSq; railHitPoint = currentIntersection; hitRailIndex = 2; }
3836 }
3837 // Check Bottom Rail
3838 if (LineSegmentIntersection(rayStart, rayEnd, bottomLeft, bottomRight, currentIntersection)) {
3839 float distSq = GetDistanceSq(rayStart.x, rayStart.y, currentIntersection.x, currentIntersection.y);
3840 if (distSq < minRailDistSq) { minRailDistSq = distSq; railHitPoint = currentIntersection; hitRailIndex = 3; }
3841 }
3842
3843
3844 // --- Determine final aim line end point ---
3845 D2D1_POINT_2F finalLineEnd = railHitPoint; // Assume rail hit first
3846 bool aimingAtRail = true;
3847
3848 if (hitBall && firstHitDistSq < minRailDistSq) {
3849 // Ball collision is closer than rail collision
3850 finalLineEnd = ballCollisionPoint; // End line at the point of contact on the ball
3851 aimingAtRail = false;
3852 }
3853
3854 // --- Draw Primary Aiming Line ---
3855 pRT->DrawLine(rayStart, finalLineEnd, pBrush, 1.0f, pDashedStyle ? pDashedStyle : NULL);
3856
3857 // --- Draw Target Circle/Indicator ---
3858 D2D1_ELLIPSE targetCircle = D2D1::Ellipse(finalLineEnd, BALL_RADIUS / 2.0f, BALL_RADIUS / 2.0f);
3859 pRT->DrawEllipse(&targetCircle, pBrush, 1.0f);
3860
3861 // --- Draw Projection/Reflection Lines ---
3862 if (!aimingAtRail && hitBall) {
3863 // Aiming at a ball: Draw Ghost Cue Ball and Target Ball Projection
3864 D2D1_ELLIPSE ghostCue = D2D1::Ellipse(ballCollisionPoint, BALL_RADIUS, BALL_RADIUS); // Ghost ball at contact point
3865 pRT->DrawEllipse(ghostCue, pGhostBrush, 1.0f, pDashedStyle ? pDashedStyle : NULL);
3866
3867 // Calculate target ball projection based on impact line (cue collision point -> target center)
3868 float targetProjectionAngle = atan2f(hitBall->y - ballCollisionPoint.y, hitBall->x - ballCollisionPoint.x);
3869 // Clamp angle calculation if distance is tiny
3870 if (GetDistanceSq(hitBall->x, hitBall->y, ballCollisionPoint.x, ballCollisionPoint.y) < 1.0f) {
3871 targetProjectionAngle = cueAngle; // Fallback if overlapping
3872 }
3873
3874 D2D1_POINT_2F targetStartPoint = D2D1::Point2F(hitBall->x, hitBall->y);
3875 D2D1_POINT_2F targetProjectionEnd = D2D1::Point2F(
3876 hitBall->x + cosf(targetProjectionAngle) * 50.0f, // Projection length 50 units
3877 hitBall->y + sinf(targetProjectionAngle) * 50.0f
3878 );
3879 // Draw solid line for target projection
3880 //pRT->DrawLine(targetStartPoint, targetProjectionEnd, pBrush, 1.0f);
3881
3882 //new code start
3883
3884 // Dual trajectory with edge-aware contact simulation
3885 D2D1_POINT_2F dir = {
3886 targetProjectionEnd.x - targetStartPoint.x,
3887 targetProjectionEnd.y - targetStartPoint.y
3888 };
3889 float dirLen = sqrtf(dir.x * dir.x + dir.y * dir.y);
3890 dir.x /= dirLen;
3891 dir.y /= dirLen;
3892
3893 D2D1_POINT_2F perp = { -dir.y, dir.x };
3894
3895 // Approximate cue ball center by reversing from tip
3896 D2D1_POINT_2F cueBallCenterForGhostHit = { // Renamed for clarity if you use it elsewhere
3897 targetStartPoint.x - dir.x * BALL_RADIUS,
3898 targetStartPoint.y - dir.y * BALL_RADIUS
3899 };
3900
3901 // REAL contact-ball center - use your physics object's center:
3902 // (replace 'objectBallPos' with whatever you actually call it)
3903 // (targetStartPoint is already hitBall->x, hitBall->y)
3904 D2D1_POINT_2F contactBallCenter = targetStartPoint; // Corrected: Use the object ball's actual center
3905 //D2D1_POINT_2F contactBallCenter = D2D1::Point2F(hitBall->x, hitBall->y);
3906
3907 // The 'offset' calculation below uses 'cueBallCenterForGhostHit' (originally 'cueBallCenter').
3908 // This will result in 'offset' being 0 because 'cueBallCenterForGhostHit' is defined
3909 // such that (targetStartPoint - cueBallCenterForGhostHit) is parallel to 'dir',
3910 // and 'perp' is perpendicular to 'dir'.
3911 // Consider Change 2 if this 'offset' is not behaving as intended for the secondary line.
3912 /*float offset = ((targetStartPoint.x - cueBallCenterForGhostHit.x) * perp.x +
3913 (targetStartPoint.y - cueBallCenterForGhostHit.y) * perp.y);*/
3914 /*float offset = ((targetStartPoint.x - cueBallCenter.x) * perp.x +
3915 (targetStartPoint.y - cueBallCenter.y) * perp.y);
3916 float absOffset = fabsf(offset);
3917 float side = (offset >= 0 ? 1.0f : -1.0f);*/
3918
3919 // Use actual cue ball center for offset calculation if 'offset' is meant to quantify the cut
3920 D2D1_POINT_2F actualCueBallPhysicalCenter = D2D1::Point2F(cueBall->x, cueBall->y); // This is also rayStart
3921
3922 // Offset calculation based on actual cue ball position relative to the 'dir' line through targetStartPoint
3923 float offset = ((targetStartPoint.x - actualCueBallPhysicalCenter.x) * perp.x +
3924 (targetStartPoint.y - actualCueBallPhysicalCenter.y) * perp.y);
3925 float absOffset = fabsf(offset);
3926 float side = (offset >= 0 ? 1.0f : -1.0f);
3927
3928
3929 // Actual contact point on target ball edge
3930 D2D1_POINT_2F contactPoint = {
3931 contactBallCenter.x + perp.x * BALL_RADIUS * side,
3932 contactBallCenter.y + perp.y * BALL_RADIUS * side
3933 };
3934
3935 // Tangent (cut shot) path from contact point
3936 // Tangent (cut shot) path: from contact point to contact ball center
3937 D2D1_POINT_2F objectBallDir = {
3938 contactBallCenter.x - contactPoint.x,
3939 contactBallCenter.y - contactPoint.y
3940 };
3941 float oLen = sqrtf(objectBallDir.x * objectBallDir.x + objectBallDir.y * objectBallDir.y);
3942 if (oLen != 0.0f) {
3943 objectBallDir.x /= oLen;
3944 objectBallDir.y /= oLen;
3945 }
3946
3947 const float PRIMARY_LEN = 150.0f; //default=150.0f
3948 const float SECONDARY_LEN = 150.0f; //default=150.0f
3949 const float STRAIGHT_EPSILON = BALL_RADIUS * 0.05f;
3950
3951 D2D1_POINT_2F primaryEnd = {
3952 targetStartPoint.x + dir.x * PRIMARY_LEN,
3953 targetStartPoint.y + dir.y * PRIMARY_LEN
3954 };
3955
3956 // Secondary line starts from the contact ball's center
3957 D2D1_POINT_2F secondaryStart = contactBallCenter;
3958 D2D1_POINT_2F secondaryEnd = {
3959 secondaryStart.x + objectBallDir.x * SECONDARY_LEN,
3960 secondaryStart.y + objectBallDir.y * SECONDARY_LEN
3961 };
3962
3963 if (absOffset < STRAIGHT_EPSILON) // straight shot?
3964 {
3965 // Straight: secondary behind primary
3966 // secondary behind primary {pDashedStyle param at end}
3967 pRT->DrawLine(secondaryStart, secondaryEnd, pPurpleBrush, 2.0f);
3968 //pRT->DrawLine(secondaryStart, secondaryEnd, pGhostBrush, 1.0f);
3969 pRT->DrawLine(targetStartPoint, primaryEnd, pCyanBrush, 2.0f);
3970 //pRT->DrawLine(targetStartPoint, primaryEnd, pBrush, 1.0f);
3971 }
3972 else
3973 {
3974 // Cut shot: both visible
3975 // both visible for cut shot
3976 pRT->DrawLine(secondaryStart, secondaryEnd, pPurpleBrush, 2.0f);
3977 //pRT->DrawLine(secondaryStart, secondaryEnd, pGhostBrush, 1.0f);
3978 pRT->DrawLine(targetStartPoint, primaryEnd, pCyanBrush, 2.0f);
3979 //pRT->DrawLine(targetStartPoint, primaryEnd, pBrush, 1.0f);
3980 }
3981 // End improved trajectory logic
3982
3983 //new code end
3984
3985 // -- Cue Ball Path after collision (Optional, requires physics) --
3986 // Very simplified: Assume cue deflects, angle depends on cut angle.
3987 // float cutAngle = acosf(cosf(cueAngle - targetProjectionAngle)); // Angle between paths
3988 // float cueDeflectionAngle = ? // Depends on cutAngle, spin, etc. Hard to predict accurately.
3989 // D2D1_POINT_2F cueProjectionEnd = ...
3990 // pRT->DrawLine(ballCollisionPoint, cueProjectionEnd, pGhostBrush, 1.0f, pDashedStyle ? pDashedStyle : NULL);
3991
3992 // --- Accuracy Comment ---
3993 // Note: The visual accuracy of this projection, especially for cut shots (hitting the ball off-center)
3994 // or shots with spin, is limited by the simplified physics model. Real pool physics involves
3995 // collision-induced throw, spin transfer, and cue ball deflection not fully simulated here.
3996 // The ghost ball method shows the *ideal* line for a center-cue hit without spin.
3997
3998 }
3999 else if (aimingAtRail && hitRailIndex != -1) {
4000 // Aiming at a rail: Draw reflection line
4001 float reflectAngle = cueAngle;
4002 // Reflect angle based on which rail was hit
4003 if (hitRailIndex == 0 || hitRailIndex == 1) { // Left or Right rail
4004 reflectAngle = PI - cueAngle; // Reflect horizontal component
4005 }
4006 else { // Top or Bottom rail
4007 reflectAngle = -cueAngle; // Reflect vertical component
4008 }
4009 // Normalize angle if needed (atan2 usually handles this)
4010 while (reflectAngle > PI) reflectAngle -= 2 * PI;
4011 while (reflectAngle <= -PI) reflectAngle += 2 * PI;
4012
4013
4014 float reflectionLength = 60.0f; // Length of the reflection line
4015 D2D1_POINT_2F reflectionEnd = D2D1::Point2F(
4016 finalLineEnd.x + cosf(reflectAngle) * reflectionLength,
4017 finalLineEnd.y + sinf(reflectAngle) * reflectionLength
4018 );
4019
4020 // Draw the reflection line (e.g., using a different color/style)
4021 pRT->DrawLine(finalLineEnd, reflectionEnd, pReflectBrush, 1.0f, pDashedStyle ? pDashedStyle : NULL);
4022 }
4023
4024 // Release resources
4025 SafeRelease(&pBrush);
4026 SafeRelease(&pGhostBrush);
4027 SafeRelease(&pCueBrush);
4028 SafeRelease(&pReflectBrush); // Release new brush
4029 SafeRelease(&pCyanBrush);
4030 SafeRelease(&pPurpleBrush);
4031 SafeRelease(&pDashedStyle);
4032 }
4033
4034
4035 void DrawUI(ID2D1RenderTarget* pRT) {
4036 if (!pTextFormat || !pLargeTextFormat) return;
4037
4038 ID2D1SolidColorBrush* pBrush = nullptr;
4039 pRT->CreateSolidColorBrush(UI_TEXT_COLOR, &pBrush);
4040 if (!pBrush) return;
4041
4042 //new code
4043 // --- Always draw AI's 8?Ball call arrow when it's Player?2's turn and AI has called ---
4044 //if (isPlayer2AI && currentPlayer == 2 && calledPocketP2 >= 0) {
4045 // FIX: This condition correctly shows the AI's called pocket arrow.
4046 if (isPlayer2AI && IsPlayerOnEightBall(2) && calledPocketP2 >= 0) {
4047 // pocket index that AI called
4048 int idx = calledPocketP2;
4049 // draw large blue arrow
4050 ID2D1SolidColorBrush* pArrow = nullptr;
4051 pRT->CreateSolidColorBrush(TURN_ARROW_COLOR, &pArrow);
4052 if (pArrow) {
4053 auto P = pocketPositions[idx];
4054 D2D1_POINT_2F tri[3] = {
4055 { P.x - 15.0f, P.y - 40.0f },
4056 { P.x + 15.0f, P.y - 40.0f },
4057 { P.x , P.y - 10.0f }
4058 };
4059 ID2D1PathGeometry* geom = nullptr;
4060 pFactory->CreatePathGeometry(&geom);
4061 ID2D1GeometrySink* sink = nullptr;
4062 geom->Open(&sink);
4063 sink->BeginFigure(tri[0], D2D1_FIGURE_BEGIN_FILLED);
4064 sink->AddLines(&tri[1], 2);
4065 sink->EndFigure(D2D1_FIGURE_END_CLOSED);
4066 sink->Close();
4067 pRT->FillGeometry(geom, pArrow);
4068 SafeRelease(&sink);
4069 SafeRelease(&geom);
4070 SafeRelease(&pArrow);
4071 }
4072 // draw “Choose a pocket...” prompt
4073 D2D1_RECT_F txt = D2D1::RectF(
4074 TABLE_LEFT,
4075 TABLE_BOTTOM + CUSHION_THICKNESS + 5.0f,
4076 TABLE_RIGHT,
4077 TABLE_BOTTOM + CUSHION_THICKNESS + 30.0f
4078 );
4079 pRT->DrawText(
4080 L"AI has called this pocket",
4081 (UINT32)wcslen(L"AI has called this pocket"),
4082 pTextFormat,
4083 &txt,
4084 pBrush
4085 );
4086 // note: no return here — we still draw fouls/turn text underneath
4087 }
4088 //end new code
4089
4090 // --- Player Info Area (Top Left/Right) --- (Unchanged)
4091 float uiTop = TABLE_TOP - 80;
4092 float uiHeight = 60;
4093 float p1Left = TABLE_LEFT;
4094 float p1Width = 150;
4095 float p2Left = TABLE_RIGHT - p1Width;
4096 D2D1_RECT_F p1Rect = D2D1::RectF(p1Left, uiTop, p1Left + p1Width, uiTop + uiHeight);
4097 D2D1_RECT_F p2Rect = D2D1::RectF(p2Left, uiTop, p2Left + p1Width, uiTop + uiHeight);
4098
4099 // Player 1 Info Text (Unchanged)
4100 std::wostringstream oss1;
4101 oss1 << player1Info.name.c_str() << L"\n";
4102 if (player1Info.assignedType != BallType::NONE) {
4103 oss1 << ((player1Info.assignedType == BallType::SOLID) ? L"Solids (Yellow)" : L"Stripes (Red)");
4104 oss1 << L" [" << player1Info.ballsPocketedCount << L"/7]";
4105 }
4106 else {
4107 oss1 << L"(Undecided)";
4108 }
4109 pRT->DrawText(oss1.str().c_str(), (UINT32)oss1.str().length(), pTextFormat, &p1Rect, pBrush);
4110 // Draw Player 1 Side Ball
4111 if (player1Info.assignedType != BallType::NONE)
4112 {
4113 ID2D1SolidColorBrush* pBallBrush = nullptr;
4114 D2D1_COLOR_F ballColor = (player1Info.assignedType == BallType::SOLID) ?
4115 D2D1::ColorF(1.0f, 1.0f, 0.0f) : D2D1::ColorF(1.0f, 0.0f, 0.0f);
4116 pRT->CreateSolidColorBrush(ballColor, &pBallBrush);
4117 if (pBallBrush)
4118 {
4119 D2D1_POINT_2F ballCenter = D2D1::Point2F(p1Rect.right + 10.0f, p1Rect.top + 20.0f);
4120 float radius = 10.0f;
4121 D2D1_ELLIPSE ball = D2D1::Ellipse(ballCenter, radius, radius);
4122 pRT->FillEllipse(&ball, pBallBrush);
4123 SafeRelease(&pBallBrush);
4124 // Draw border around the ball
4125 ID2D1SolidColorBrush* pBorderBrush = nullptr;
4126 pRT->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::Black), &pBorderBrush);
4127 if (pBorderBrush)
4128 {
4129 pRT->DrawEllipse(&ball, pBorderBrush, 1.5f); // thin border
4130 SafeRelease(&pBorderBrush);
4131 }
4132
4133 // If stripes, draw a stripe band
4134 if (player1Info.assignedType == BallType::STRIPE)
4135 {
4136 ID2D1SolidColorBrush* pStripeBrush = nullptr;
4137 pRT->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::White), &pStripeBrush);
4138 if (pStripeBrush)
4139 {
4140 D2D1_RECT_F stripeRect = D2D1::RectF(
4141 ballCenter.x - radius,
4142 ballCenter.y - 3.0f,
4143 ballCenter.x + radius,
4144 ballCenter.y + 3.0f
4145 );
4146 pRT->FillRectangle(&stripeRect, pStripeBrush);
4147 SafeRelease(&pStripeBrush);
4148 }
4149 }
4150 }
4151 }
4152
4153
4154 // Player 2 Info Text (Unchanged)
4155 std::wostringstream oss2;
4156 oss2 << player2Info.name.c_str() << L"\n";
4157 if (player2Info.assignedType != BallType::NONE) {
4158 oss2 << ((player2Info.assignedType == BallType::SOLID) ? L"Solids (Yellow)" : L"Stripes (Red)");
4159 oss2 << L" [" << player2Info.ballsPocketedCount << L"/7]";
4160 }
4161 else {
4162 oss2 << L"(Undecided)";
4163 }
4164 pRT->DrawText(oss2.str().c_str(), (UINT32)oss2.str().length(), pTextFormat, &p2Rect, pBrush);
4165 // Draw Player 2 Side Ball
4166 if (player2Info.assignedType != BallType::NONE)
4167 {
4168 ID2D1SolidColorBrush* pBallBrush = nullptr;
4169 D2D1_COLOR_F ballColor = (player2Info.assignedType == BallType::SOLID) ?
4170 D2D1::ColorF(1.0f, 1.0f, 0.0f) : D2D1::ColorF(1.0f, 0.0f, 0.0f);
4171 pRT->CreateSolidColorBrush(ballColor, &pBallBrush);
4172 if (pBallBrush)
4173 {
4174 D2D1_POINT_2F ballCenter = D2D1::Point2F(p2Rect.right + 10.0f, p2Rect.top + 20.0f);
4175 float radius = 10.0f;
4176 D2D1_ELLIPSE ball = D2D1::Ellipse(ballCenter, radius, radius);
4177 pRT->FillEllipse(&ball, pBallBrush);
4178 SafeRelease(&pBallBrush);
4179 // Draw border around the ball
4180 ID2D1SolidColorBrush* pBorderBrush = nullptr;
4181 pRT->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::Black), &pBorderBrush);
4182 if (pBorderBrush)
4183 {
4184 pRT->DrawEllipse(&ball, pBorderBrush, 1.5f); // thin border
4185 SafeRelease(&pBorderBrush);
4186 }
4187
4188 // If stripes, draw a stripe band
4189 if (player2Info.assignedType == BallType::STRIPE)
4190 {
4191 ID2D1SolidColorBrush* pStripeBrush = nullptr;
4192 pRT->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::White), &pStripeBrush);
4193 if (pStripeBrush)
4194 {
4195 D2D1_RECT_F stripeRect = D2D1::RectF(
4196 ballCenter.x - radius,
4197 ballCenter.y - 3.0f,
4198 ballCenter.x + radius,
4199 ballCenter.y + 3.0f
4200 );
4201 pRT->FillRectangle(&stripeRect, pStripeBrush);
4202 SafeRelease(&pStripeBrush);
4203 }
4204 }
4205 }
4206 }
4207
4208 // --- MODIFIED: Current Turn Arrow (Blue, Bigger, Beside Name) ---
4209 ID2D1SolidColorBrush* pArrowBrush = nullptr;
4210 pRT->CreateSolidColorBrush(TURN_ARROW_COLOR, &pArrowBrush);
4211 if (pArrowBrush && currentGameState != GAME_OVER && currentGameState != SHOT_IN_PROGRESS && currentGameState != AI_THINKING) {
4212 float arrowSizeBase = 32.0f; // Base size for width/height offsets (4x original ~8)
4213 float arrowCenterY = p1Rect.top + uiHeight / 2.0f; // Center vertically with text box
4214 float arrowTipX, arrowBackX;
4215
4216 D2D1_RECT_F playerBox = (currentPlayer == 1) ? p1Rect : p2Rect;
4217 arrowBackX = playerBox.left - 25.0f;
4218 arrowTipX = arrowBackX + arrowSizeBase * 0.75f;
4219
4220 float notchDepth = 12.0f; // Increased from 6.0f to make the rectangle longer
4221 float notchWidth = 10.0f;
4222
4223 float cx = arrowBackX;
4224 float cy = arrowCenterY;
4225
4226 // Define triangle + rectangle tail shape
4227 D2D1_POINT_2F tip = D2D1::Point2F(arrowTipX, cy); // tip
4228 D2D1_POINT_2F baseTop = D2D1::Point2F(cx, cy - arrowSizeBase / 2.0f); // triangle top
4229 D2D1_POINT_2F baseBot = D2D1::Point2F(cx, cy + arrowSizeBase / 2.0f); // triangle bottom
4230
4231 // Rectangle coordinates for the tail portion:
4232 D2D1_POINT_2F r1 = D2D1::Point2F(cx - notchDepth, cy - notchWidth / 2.0f); // rect top-left
4233 D2D1_POINT_2F r2 = D2D1::Point2F(cx, cy - notchWidth / 2.0f); // rect top-right
4234 D2D1_POINT_2F r3 = D2D1::Point2F(cx, cy + notchWidth / 2.0f); // rect bottom-right
4235 D2D1_POINT_2F r4 = D2D1::Point2F(cx - notchDepth, cy + notchWidth / 2.0f); // rect bottom-left
4236
4237 ID2D1PathGeometry* pPath = nullptr;
4238 if (SUCCEEDED(pFactory->CreatePathGeometry(&pPath))) {
4239 ID2D1GeometrySink* pSink = nullptr;
4240 if (SUCCEEDED(pPath->Open(&pSink))) {
4241 pSink->BeginFigure(tip, D2D1_FIGURE_BEGIN_FILLED);
4242 pSink->AddLine(baseTop);
4243 pSink->AddLine(r2); // transition from triangle into rectangle
4244 pSink->AddLine(r1);
4245 pSink->AddLine(r4);
4246 pSink->AddLine(r3);
4247 pSink->AddLine(baseBot);
4248 pSink->EndFigure(D2D1_FIGURE_END_CLOSED);
4249 pSink->Close();
4250 SafeRelease(&pSink);
4251 pRT->FillGeometry(pPath, pArrowBrush);
4252 }
4253 SafeRelease(&pPath);
4254 }
4255
4256
4257 SafeRelease(&pArrowBrush);
4258 }
4259
4260 //original
4261 /*
4262 // --- MODIFIED: Current Turn Arrow (Blue, Bigger, Beside Name) ---
4263 ID2D1SolidColorBrush* pArrowBrush = nullptr;
4264 pRT->CreateSolidColorBrush(TURN_ARROW_COLOR, &pArrowBrush);
4265 if (pArrowBrush && currentGameState != GAME_OVER && currentGameState != SHOT_IN_PROGRESS && currentGameState != AI_THINKING) {
4266 float arrowSizeBase = 32.0f; // Base size for width/height offsets (4x original ~8)
4267 float arrowCenterY = p1Rect.top + uiHeight / 2.0f; // Center vertically with text box
4268 float arrowTipX, arrowBackX;
4269
4270 if (currentPlayer == 1) {
4271 arrowBackX = p1Rect.left - 25.0f; // Position left of the box
4272 arrowTipX = arrowBackX + arrowSizeBase * 0.75f; // Pointy end extends right
4273 // Define points for right-pointing arrow
4274 //D2D1_POINT_2F pt1 = D2D1::Point2F(arrowTipX, arrowCenterY); // Tip
4275 //D2D1_POINT_2F pt2 = D2D1::Point2F(arrowBackX, arrowCenterY - arrowSizeBase / 2.0f); // Top-Back
4276 //D2D1_POINT_2F pt3 = D2D1::Point2F(arrowBackX, arrowCenterY + arrowSizeBase / 2.0f); // Bottom-Back
4277 // Enhanced arrow with base rectangle intersection
4278 float notchDepth = 6.0f; // Depth of square base "stem"
4279 float notchWidth = 4.0f; // Thickness of square part
4280
4281 D2D1_POINT_2F pt1 = D2D1::Point2F(arrowTipX, arrowCenterY); // Tip
4282 D2D1_POINT_2F pt2 = D2D1::Point2F(arrowBackX, arrowCenterY - arrowSizeBase / 2.0f); // Top-Back
4283 D2D1_POINT_2F pt3 = D2D1::Point2F(arrowBackX - notchDepth, arrowCenterY - notchWidth / 2.0f); // Square Left-Top
4284 D2D1_POINT_2F pt4 = D2D1::Point2F(arrowBackX - notchDepth, arrowCenterY + notchWidth / 2.0f); // Square Left-Bottom
4285 D2D1_POINT_2F pt5 = D2D1::Point2F(arrowBackX, arrowCenterY + arrowSizeBase / 2.0f); // Bottom-Back
4286
4287
4288 ID2D1PathGeometry* pPath = nullptr;
4289 if (SUCCEEDED(pFactory->CreatePathGeometry(&pPath))) {
4290 ID2D1GeometrySink* pSink = nullptr;
4291 if (SUCCEEDED(pPath->Open(&pSink))) {
4292 pSink->BeginFigure(pt1, D2D1_FIGURE_BEGIN_FILLED);
4293 pSink->AddLine(pt2);
4294 pSink->AddLine(pt3);
4295 pSink->EndFigure(D2D1_FIGURE_END_CLOSED);
4296 pSink->Close();
4297 SafeRelease(&pSink);
4298 pRT->FillGeometry(pPath, pArrowBrush);
4299 }
4300 SafeRelease(&pPath);
4301 }
4302 }
4303
4304
4305 //==================else player 2
4306 else { // Player 2
4307 // Player 2: Arrow left of P2 box, pointing right (or right of P2 box pointing left?)
4308 // Let's keep it consistent: Arrow left of the active player's box, pointing right.
4309 // Let's keep it consistent: Arrow left of the active player's box, pointing right.
4310 arrowBackX = p2Rect.left - 25.0f; // Position left of the box
4311 arrowTipX = arrowBackX + arrowSizeBase * 0.75f; // Pointy end extends right
4312 // Define points for right-pointing arrow
4313 D2D1_POINT_2F pt1 = D2D1::Point2F(arrowTipX, arrowCenterY); // Tip
4314 D2D1_POINT_2F pt2 = D2D1::Point2F(arrowBackX, arrowCenterY - arrowSizeBase / 2.0f); // Top-Back
4315 D2D1_POINT_2F pt3 = D2D1::Point2F(arrowBackX, arrowCenterY + arrowSizeBase / 2.0f); // Bottom-Back
4316
4317 ID2D1PathGeometry* pPath = nullptr;
4318 if (SUCCEEDED(pFactory->CreatePathGeometry(&pPath))) {
4319 ID2D1GeometrySink* pSink = nullptr;
4320 if (SUCCEEDED(pPath->Open(&pSink))) {
4321 pSink->BeginFigure(pt1, D2D1_FIGURE_BEGIN_FILLED);
4322 pSink->AddLine(pt2);
4323 pSink->AddLine(pt3);
4324 pSink->EndFigure(D2D1_FIGURE_END_CLOSED);
4325 pSink->Close();
4326 SafeRelease(&pSink);
4327 pRT->FillGeometry(pPath, pArrowBrush);
4328 }
4329 SafeRelease(&pPath);
4330 }
4331 }
4332 */
4333
4334
4335 // --- Persistent Blue 8?Ball Call Arrow & Prompt ---
4336 /*if (calledPocketP1 >= 0 || calledPocketP2 >= 0)
4337 {
4338 // determine index (default top?right)
4339 int idx = (currentPlayer == 1 ? calledPocketP1 : calledPocketP2);
4340 if (idx < 0) idx = (currentPlayer == 1 ? calledPocketP2 : calledPocketP1);
4341 if (idx < 0) idx = 2;
4342
4343 // draw large blue arrow
4344 ID2D1SolidColorBrush* pArrow = nullptr;
4345 pRT->CreateSolidColorBrush(TURN_ARROW_COLOR, &pArrow);
4346 if (pArrow) {
4347 auto P = pocketPositions[idx];
4348 D2D1_POINT_2F tri[3] = {
4349 {P.x - 15.0f, P.y - 40.0f},
4350 {P.x + 15.0f, P.y - 40.0f},
4351 {P.x , P.y - 10.0f}
4352 };
4353 ID2D1PathGeometry* geom = nullptr;
4354 pFactory->CreatePathGeometry(&geom);
4355 ID2D1GeometrySink* sink = nullptr;
4356 geom->Open(&sink);
4357 sink->BeginFigure(tri[0], D2D1_FIGURE_BEGIN_FILLED);
4358 sink->AddLines(&tri[1], 2);
4359 sink->EndFigure(D2D1_FIGURE_END_CLOSED);
4360 sink->Close();
4361 pRT->FillGeometry(geom, pArrow);
4362 SafeRelease(&sink); SafeRelease(&geom); SafeRelease(&pArrow);
4363 }
4364
4365 // draw prompt
4366 D2D1_RECT_F txt = D2D1::RectF(
4367 TABLE_LEFT,
4368 TABLE_BOTTOM + CUSHION_THICKNESS + 5.0f,
4369 TABLE_RIGHT,
4370 TABLE_BOTTOM + CUSHION_THICKNESS + 30.0f
4371 );
4372 pRT->DrawText(
4373 L"Choose a pocket...",
4374 (UINT32)wcslen(L"Choose a pocket..."),
4375 pTextFormat,
4376 &txt,
4377 pBrush
4378 );
4379 }*/
4380
4381 // --- Persistent Blue 8?Ball Pocket Arrow & Prompt (once called) ---
4382 /* if (calledPocketP1 >= 0 || calledPocketP2 >= 0)
4383 {
4384 // 1) Determine pocket index
4385 int idx = (currentPlayer == 1 ? calledPocketP1 : calledPocketP2);
4386 // If the other player had called but it's now your turn, still show that call
4387 if (idx < 0) idx = (currentPlayer == 1 ? calledPocketP2 : calledPocketP1);
4388 if (idx < 0) idx = 2; // default to top?right if somehow still unset
4389
4390 // 2) Draw large blue arrow
4391 ID2D1SolidColorBrush* pArrow = nullptr;
4392 pRT->CreateSolidColorBrush(TURN_ARROW_COLOR, &pArrow);
4393 if (pArrow) {
4394 auto P = pocketPositions[idx];
4395 D2D1_POINT_2F tri[3] = {
4396 { P.x - 15.0f, P.y - 40.0f },
4397 { P.x + 15.0f, P.y - 40.0f },
4398 { P.x , P.y - 10.0f }
4399 };
4400 ID2D1PathGeometry* geom = nullptr;
4401 pFactory->CreatePathGeometry(&geom);
4402 ID2D1GeometrySink* sink = nullptr;
4403 geom->Open(&sink);
4404 sink->BeginFigure(tri[0], D2D1_FIGURE_BEGIN_FILLED);
4405 sink->AddLines(&tri[1], 2);
4406 sink->EndFigure(D2D1_FIGURE_END_CLOSED);
4407 sink->Close();
4408 pRT->FillGeometry(geom, pArrow);
4409 SafeRelease(&sink);
4410 SafeRelease(&geom);
4411 SafeRelease(&pArrow);
4412 }
4413
4414 // 3) Draw persistent prompt text
4415 D2D1_RECT_F txt = D2D1::RectF(
4416 TABLE_LEFT,
4417 TABLE_BOTTOM + CUSHION_THICKNESS + 5.0f,
4418 TABLE_RIGHT,
4419 TABLE_BOTTOM + CUSHION_THICKNESS + 30.0f
4420 );
4421 pRT->DrawText(
4422 L"Choose a pocket...",
4423 (UINT32)wcslen(L"Choose a pocket..."),
4424 pTextFormat,
4425 &txt,
4426 pBrush
4427 );
4428 // Note: no 'return'; allow foul/turn text to draw beneath if needed
4429 } */
4430
4431 // new code ends here
4432
4433 // --- MODIFIED: Foul Text (Large Red, Bottom Center) ---
4434 if (foulCommitted && currentGameState != SHOT_IN_PROGRESS) {
4435 ID2D1SolidColorBrush* pFoulBrush = nullptr;
4436 pRT->CreateSolidColorBrush(FOUL_TEXT_COLOR, &pFoulBrush);
4437 if (pFoulBrush && pLargeTextFormat) {
4438 // Calculate Rect for bottom-middle area
4439 float foulWidth = 200.0f; // Adjust width as needed
4440 float foulHeight = 60.0f;
4441 float foulLeft = TABLE_LEFT + (TABLE_WIDTH / 2.0f) - (foulWidth / 2.0f);
4442 // Position below the pocketed balls bar
4443 float foulTop = pocketedBallsBarRect.bottom + 10.0f;
4444 D2D1_RECT_F foulRect = D2D1::RectF(foulLeft, foulTop, foulLeft + foulWidth, foulTop + foulHeight);
4445
4446 // --- Set text alignment to center for foul text ---
4447 pLargeTextFormat->SetTextAlignment(DWRITE_TEXT_ALIGNMENT_CENTER);
4448 pLargeTextFormat->SetParagraphAlignment(DWRITE_PARAGRAPH_ALIGNMENT_CENTER);
4449
4450 pRT->DrawText(L"FOUL!", 5, pLargeTextFormat, &foulRect, pFoulBrush);
4451
4452 // --- Restore default alignment for large text if needed elsewhere ---
4453 // pLargeTextFormat->SetTextAlignment(DWRITE_TEXT_ALIGNMENT_LEADING);
4454 // pLargeTextFormat->SetParagraphAlignment(DWRITE_PARAGRAPH_ALIGNMENT_CENTER);
4455
4456 SafeRelease(&pFoulBrush);
4457 }
4458 }
4459
4460 // --- Blue Arrow & Prompt for 8?Ball Call (while choosing or after called) ---
4461 if ((currentGameState == CHOOSING_POCKET_P1
4462 || currentGameState == CHOOSING_POCKET_P2)
4463 || (calledPocketP1 >= 0 || calledPocketP2 >= 0))
4464 {
4465 // determine index:
4466 // - if a call exists, use it
4467 // - if still choosing, use hover if any
4468 // determine index: use only the clicked call; default to top?right if unset
4469 int idx = (currentPlayer == 1 ? calledPocketP1 : calledPocketP2);
4470 if (idx < 0) idx = 2;
4471
4472 // draw large blue arrow
4473 ID2D1SolidColorBrush* pArrow = nullptr;
4474 pRT->CreateSolidColorBrush(TURN_ARROW_COLOR, &pArrow);
4475 if (pArrow) {
4476 auto P = pocketPositions[idx];
4477 D2D1_POINT_2F tri[3] = {
4478 {P.x - 15.0f, P.y - 40.0f},
4479 {P.x + 15.0f, P.y - 40.0f},
4480 {P.x , P.y - 10.0f}
4481 };
4482 ID2D1PathGeometry* geom = nullptr;
4483 pFactory->CreatePathGeometry(&geom);
4484 ID2D1GeometrySink* sink = nullptr;
4485 geom->Open(&sink);
4486 sink->BeginFigure(tri[0], D2D1_FIGURE_BEGIN_FILLED);
4487 sink->AddLines(&tri[1], 2);
4488 sink->EndFigure(D2D1_FIGURE_END_CLOSED);
4489 sink->Close();
4490 pRT->FillGeometry(geom, pArrow);
4491 SafeRelease(&sink); SafeRelease(&geom); SafeRelease(&pArrow);
4492 }
4493
4494 // draw prompt below pockets
4495 D2D1_RECT_F txt = D2D1::RectF(
4496 TABLE_LEFT,
4497 TABLE_BOTTOM + CUSHION_THICKNESS + 5.0f,
4498 TABLE_RIGHT,
4499 TABLE_BOTTOM + CUSHION_THICKNESS + 30.0f
4500 );
4501 pRT->DrawText(
4502 L"Choose a pocket...",
4503 (UINT32)wcslen(L"Choose a pocket..."),
4504 pTextFormat,
4505 &txt,
4506 pBrush
4507 );
4508 // do NOT return here; allow foul/turn text to display under the arrow
4509 }
4510
4511 // Removed Obsolete
4512 /*
4513 // --- 8-Ball Pocket Selection Arrow & Prompt ---
4514 if (currentGameState == CHOOSING_POCKET_P1 || currentGameState == CHOOSING_POCKET_P2) {
4515 // Determine which pocket to highlight (default to Top-Right if unset)
4516 int idx = (currentPlayer == 1) ? calledPocketP1 : calledPocketP2;
4517 if (idx < 0) idx = 2;
4518
4519 // Draw the downward arrow
4520 ID2D1SolidColorBrush* pArrowBrush = nullptr;
4521 pRT->CreateSolidColorBrush(TURN_ARROW_COLOR, &pArrowBrush);
4522 if (pArrowBrush) {
4523 D2D1_POINT_2F P = pocketPositions[idx];
4524 D2D1_POINT_2F tri[3] = {
4525 {P.x - 10.0f, P.y - 30.0f},
4526 {P.x + 10.0f, P.y - 30.0f},
4527 {P.x , P.y - 10.0f}
4528 };
4529 ID2D1PathGeometry* geom = nullptr;
4530 pFactory->CreatePathGeometry(&geom);
4531 ID2D1GeometrySink* sink = nullptr;
4532 geom->Open(&sink);
4533 sink->BeginFigure(tri[0], D2D1_FIGURE_BEGIN_FILLED);
4534 sink->AddLines(&tri[1], 2);
4535 sink->EndFigure(D2D1_FIGURE_END_CLOSED);
4536 sink->Close();
4537 pRT->FillGeometry(geom, pArrowBrush);
4538 SafeRelease(&sink);
4539 SafeRelease(&geom);
4540 SafeRelease(&pArrowBrush);
4541 }
4542
4543 // Draw “Choose a pocket...” text under the table
4544 D2D1_RECT_F prompt = D2D1::RectF(
4545 TABLE_LEFT,
4546 TABLE_BOTTOM + CUSHION_THICKNESS + 5.0f,
4547 TABLE_RIGHT,
4548 TABLE_BOTTOM + CUSHION_THICKNESS + 30.0f
4549 );
4550 pRT->DrawText(
4551 L"Choose a pocket...",
4552 (UINT32)wcslen(L"Choose a pocket..."),
4553 pTextFormat,
4554 &prompt,
4555 pBrush
4556 );
4557
4558 return; // Skip normal turn/foul text
4559 }
4560 */
4561
4562
4563 // Show AI Thinking State (Unchanged from previous step)
4564 if (currentGameState == AI_THINKING && pTextFormat) {
4565 ID2D1SolidColorBrush* pThinkingBrush = nullptr;
4566 pRT->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::Orange), &pThinkingBrush);
4567 if (pThinkingBrush) {
4568 D2D1_RECT_F thinkingRect = p2Rect;
4569 thinkingRect.top += 20; // Offset within P2 box
4570 // Ensure default text alignment for this
4571 pTextFormat->SetTextAlignment(DWRITE_TEXT_ALIGNMENT_CENTER);
4572 pTextFormat->SetParagraphAlignment(DWRITE_PARAGRAPH_ALIGNMENT_CENTER);
4573 pRT->DrawText(L"Thinking...", 11, pTextFormat, &thinkingRect, pThinkingBrush);
4574 SafeRelease(&pThinkingBrush);
4575 }
4576 }
4577
4578 SafeRelease(&pBrush);
4579
4580 // --- Draw CHEAT MODE label if active ---
4581 if (cheatModeEnabled) {
4582 ID2D1SolidColorBrush* pCheatBrush = nullptr;
4583 pRT->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::Red), &pCheatBrush);
4584 if (pCheatBrush && pTextFormat) {
4585 D2D1_RECT_F cheatTextRect = D2D1::RectF(
4586 TABLE_LEFT + 10.0f,
4587 TABLE_TOP + 10.0f,
4588 TABLE_LEFT + 200.0f,
4589 TABLE_TOP + 40.0f
4590 );
4591 pTextFormat->SetTextAlignment(DWRITE_TEXT_ALIGNMENT_LEADING);
4592 pTextFormat->SetParagraphAlignment(DWRITE_PARAGRAPH_ALIGNMENT_NEAR);
4593 pRT->DrawText(L"CHEAT MODE ON", wcslen(L"CHEAT MODE ON"), pTextFormat, &cheatTextRect, pCheatBrush);
4594 }
4595 SafeRelease(&pCheatBrush);
4596 }
4597 }
4598
4599 void DrawPowerMeter(ID2D1RenderTarget* pRT) {
4600 // Draw Border
4601 ID2D1SolidColorBrush* pBorderBrush = nullptr;
4602 pRT->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::Black), &pBorderBrush);
4603 if (!pBorderBrush) return;
4604 pRT->DrawRectangle(&powerMeterRect, pBorderBrush, 2.0f);
4605 SafeRelease(&pBorderBrush);
4606
4607 // Create Gradient Fill
4608 ID2D1GradientStopCollection* pGradientStops = nullptr;
4609 ID2D1LinearGradientBrush* pGradientBrush = nullptr;
4610 D2D1_GRADIENT_STOP gradientStops[4];
4611 gradientStops[0].position = 0.0f;
4612 gradientStops[0].color = D2D1::ColorF(D2D1::ColorF::Green);
4613 gradientStops[1].position = 0.45f;
4614 gradientStops[1].color = D2D1::ColorF(D2D1::ColorF::Yellow);
4615 gradientStops[2].position = 0.7f;
4616 gradientStops[2].color = D2D1::ColorF(D2D1::ColorF::Orange);
4617 gradientStops[3].position = 1.0f;
4618 gradientStops[3].color = D2D1::ColorF(D2D1::ColorF::Red);
4619
4620 pRT->CreateGradientStopCollection(gradientStops, 4, &pGradientStops);
4621 if (pGradientStops) {
4622 D2D1_LINEAR_GRADIENT_BRUSH_PROPERTIES props = {};
4623 props.startPoint = D2D1::Point2F(powerMeterRect.left, powerMeterRect.bottom);
4624 props.endPoint = D2D1::Point2F(powerMeterRect.left, powerMeterRect.top);
4625 pRT->CreateLinearGradientBrush(props, pGradientStops, &pGradientBrush);
4626 SafeRelease(&pGradientStops);
4627 }
4628
4629 // Calculate Fill Height
4630 float fillRatio = 0;
4631 //if (isAiming && (currentGameState == AIMING || currentGameState == BREAKING)) {
4632 // Determine if power meter should reflect shot power (human aiming or AI preparing)
4633 bool humanIsAimingPower = isAiming && (currentGameState == AIMING || currentGameState == BREAKING);
4634 // NEW Condition: AI is displaying its aim, so show its chosen power
4635 bool aiIsVisualizingPower = (isPlayer2AI && currentPlayer == 2 &&
4636 currentGameState == AI_THINKING && aiIsDisplayingAim);
4637
4638 if (humanIsAimingPower || aiIsVisualizingPower) { // Use the new condition
4639 fillRatio = shotPower / MAX_SHOT_POWER;
4640 }
4641 float fillHeight = (powerMeterRect.bottom - powerMeterRect.top) * fillRatio;
4642 D2D1_RECT_F fillRect = D2D1::RectF(
4643 powerMeterRect.left,
4644 powerMeterRect.bottom - fillHeight,
4645 powerMeterRect.right,
4646 powerMeterRect.bottom
4647 );
4648
4649 if (pGradientBrush) {
4650 pRT->FillRectangle(&fillRect, pGradientBrush);
4651 SafeRelease(&pGradientBrush);
4652 }
4653
4654 // Draw scale notches
4655 ID2D1SolidColorBrush* pNotchBrush = nullptr;
4656 pRT->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::Black), &pNotchBrush);
4657 if (pNotchBrush) {
4658 for (int i = 0; i <= 8; ++i) {
4659 float y = powerMeterRect.top + (powerMeterRect.bottom - powerMeterRect.top) * (i / 8.0f);
4660 pRT->DrawLine(
4661 D2D1::Point2F(powerMeterRect.right + 2.0f, y),
4662 D2D1::Point2F(powerMeterRect.right + 8.0f, y),
4663 pNotchBrush,
4664 1.5f
4665 );
4666 }
4667 SafeRelease(&pNotchBrush);
4668 }
4669
4670 // Draw "Power" Label Below Meter
4671 if (pTextFormat) {
4672 ID2D1SolidColorBrush* pTextBrush = nullptr;
4673 pRT->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::Black), &pTextBrush);
4674 if (pTextBrush) {
4675 D2D1_RECT_F textRect = D2D1::RectF(
4676 powerMeterRect.left - 20.0f,
4677 powerMeterRect.bottom + 8.0f,
4678 powerMeterRect.right + 20.0f,
4679 powerMeterRect.bottom + 38.0f
4680 );
4681 pTextFormat->SetTextAlignment(DWRITE_TEXT_ALIGNMENT_CENTER);
4682 pTextFormat->SetParagraphAlignment(DWRITE_PARAGRAPH_ALIGNMENT_NEAR);
4683 pRT->DrawText(L"Power", 5, pTextFormat, &textRect, pTextBrush);
4684 SafeRelease(&pTextBrush);
4685 }
4686 }
4687
4688 // Draw Glow Effect if fully charged or fading out
4689 static float glowPulse = 0.0f;
4690 static bool glowIncreasing = true;
4691 static float glowFadeOut = 0.0f; // NEW: tracks fading out
4692
4693 if (shotPower >= MAX_SHOT_POWER * 0.99f) {
4694 // While fully charged, keep pulsing normally
4695 if (glowIncreasing) {
4696 glowPulse += 0.02f;
4697 if (glowPulse >= 1.0f) glowIncreasing = false;
4698 }
4699 else {
4700 glowPulse -= 0.02f;
4701 if (glowPulse <= 0.0f) glowIncreasing = true;
4702 }
4703 glowFadeOut = 1.0f; // Reset fade out to full
4704 }
4705 else if (glowFadeOut > 0.0f) {
4706 // If shot fired, gradually fade out
4707 glowFadeOut -= 0.02f;
4708 if (glowFadeOut < 0.0f) glowFadeOut = 0.0f;
4709 }
4710
4711 if (glowFadeOut > 0.0f) {
4712 ID2D1SolidColorBrush* pGlowBrush = nullptr;
4713 float effectiveOpacity = (0.3f + 0.7f * glowPulse) * glowFadeOut;
4714 pRT->CreateSolidColorBrush(
4715 D2D1::ColorF(D2D1::ColorF::Red, effectiveOpacity),
4716 &pGlowBrush
4717 );
4718 if (pGlowBrush) {
4719 float glowCenterX = (powerMeterRect.left + powerMeterRect.right) / 2.0f;
4720 float glowCenterY = powerMeterRect.top;
4721 D2D1_ELLIPSE glowEllipse = D2D1::Ellipse(
4722 D2D1::Point2F(glowCenterX, glowCenterY - 10.0f),
4723 12.0f + 3.0f * glowPulse,
4724 6.0f + 2.0f * glowPulse
4725 );
4726 pRT->FillEllipse(&glowEllipse, pGlowBrush);
4727 SafeRelease(&pGlowBrush);
4728 }
4729 }
4730 }
4731
4732 void DrawSpinIndicator(ID2D1RenderTarget* pRT) {
4733 ID2D1SolidColorBrush* pWhiteBrush = nullptr;
4734 ID2D1SolidColorBrush* pRedBrush = nullptr;
4735
4736 pRT->CreateSolidColorBrush(CUE_BALL_COLOR, &pWhiteBrush);
4737 pRT->CreateSolidColorBrush(ENGLISH_DOT_COLOR, &pRedBrush);
4738
4739 if (!pWhiteBrush || !pRedBrush) {
4740 SafeRelease(&pWhiteBrush);
4741 SafeRelease(&pRedBrush);
4742 return;
4743 }
4744
4745 // Draw White Ball Background
4746 D2D1_ELLIPSE bgEllipse = D2D1::Ellipse(spinIndicatorCenter, spinIndicatorRadius, spinIndicatorRadius);
4747 pRT->FillEllipse(&bgEllipse, pWhiteBrush);
4748 pRT->DrawEllipse(&bgEllipse, pRedBrush, 0.5f); // Thin red border
4749
4750
4751 // Draw Red Dot for Spin Position
4752 float dotRadius = 4.0f;
4753 float dotX = spinIndicatorCenter.x + cueSpinX * (spinIndicatorRadius - dotRadius); // Keep dot inside edge
4754 float dotY = spinIndicatorCenter.y + cueSpinY * (spinIndicatorRadius - dotRadius);
4755 D2D1_ELLIPSE dotEllipse = D2D1::Ellipse(D2D1::Point2F(dotX, dotY), dotRadius, dotRadius);
4756 pRT->FillEllipse(&dotEllipse, pRedBrush);
4757
4758 SafeRelease(&pWhiteBrush);
4759 SafeRelease(&pRedBrush);
4760 }
4761
4762
4763 void DrawPocketedBallsIndicator(ID2D1RenderTarget* pRT) {
4764 ID2D1SolidColorBrush* pBgBrush = nullptr;
4765 ID2D1SolidColorBrush* pBallBrush = nullptr;
4766
4767 // Ensure render target is valid before proceeding
4768 if (!pRT) return;
4769
4770 HRESULT hr = pRT->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::Black, 0.8f), &pBgBrush); // Semi-transparent black
4771 if (FAILED(hr)) { SafeRelease(&pBgBrush); return; } // Exit if brush creation fails
4772
4773 hr = pRT->CreateSolidColorBrush(D2D1::ColorF(0, 0, 0), &pBallBrush); // Placeholder, color will be set per ball
4774 if (FAILED(hr)) {
4775 SafeRelease(&pBgBrush);
4776 SafeRelease(&pBallBrush);
4777 return; // Exit if brush creation fails
4778 }
4779
4780 // Draw the background bar (rounded rect)
4781 D2D1_ROUNDED_RECT roundedRect = D2D1::RoundedRect(pocketedBallsBarRect, 10.0f, 10.0f); // Corner radius 10
4782 float baseAlpha = 0.8f;
4783 float flashBoost = pocketFlashTimer * 0.5f; // Make flash effect boost alpha slightly
4784 float finalAlpha = std::min(1.0f, baseAlpha + flashBoost);
4785 pBgBrush->SetOpacity(finalAlpha);
4786 pRT->FillRoundedRectangle(&roundedRect, pBgBrush);
4787 pBgBrush->SetOpacity(1.0f); // Reset opacity after drawing
4788
4789 // --- Draw small circles for pocketed balls inside the bar ---
4790
4791 // Calculate dimensions based on the bar's height for better scaling
4792 float barHeight = pocketedBallsBarRect.bottom - pocketedBallsBarRect.top;
4793 float ballDisplayRadius = barHeight * 0.30f; // Make balls slightly smaller relative to bar height
4794 float spacing = ballDisplayRadius * 2.2f; // Adjust spacing slightly
4795 float padding = spacing * 0.75f; // Add padding from the edges
4796 float center_Y = pocketedBallsBarRect.top + barHeight / 2.0f; // Vertical center
4797
4798 // Starting X positions with padding
4799 float currentX_P1 = pocketedBallsBarRect.left + padding;
4800 float currentX_P2 = pocketedBallsBarRect.right - padding; // Start from right edge minus padding
4801
4802 int p1DrawnCount = 0;
4803 int p2DrawnCount = 0;
4804 const int maxBallsToShow = 7; // Max balls per player in the bar
4805
4806 for (const auto& b : balls) {
4807 if (b.isPocketed) {
4808 // Skip cue ball and 8-ball in this indicator
4809 if (b.id == 0 || b.id == 8) continue;
4810
4811 bool isPlayer1Ball = (player1Info.assignedType != BallType::NONE && b.type == player1Info.assignedType);
4812 bool isPlayer2Ball = (player2Info.assignedType != BallType::NONE && b.type == player2Info.assignedType);
4813
4814 if (isPlayer1Ball && p1DrawnCount < maxBallsToShow) {
4815 pBallBrush->SetColor(b.color);
4816 // Draw P1 balls from left to right
4817 D2D1_ELLIPSE ballEllipse = D2D1::Ellipse(D2D1::Point2F(currentX_P1 + p1DrawnCount * spacing, center_Y), ballDisplayRadius, ballDisplayRadius);
4818 pRT->FillEllipse(&ballEllipse, pBallBrush);
4819 p1DrawnCount++;
4820 }
4821 else if (isPlayer2Ball && p2DrawnCount < maxBallsToShow) {
4822 pBallBrush->SetColor(b.color);
4823 // Draw P2 balls from right to left
4824 D2D1_ELLIPSE ballEllipse = D2D1::Ellipse(D2D1::Point2F(currentX_P2 - p2DrawnCount * spacing, center_Y), ballDisplayRadius, ballDisplayRadius);
4825 pRT->FillEllipse(&ballEllipse, pBallBrush);
4826 p2DrawnCount++;
4827 }
4828 // Note: Balls pocketed before assignment or opponent balls are intentionally not shown here.
4829 // You could add logic here to display them differently if needed (e.g., smaller, grayed out).
4830 }
4831 }
4832
4833 SafeRelease(&pBgBrush);
4834 SafeRelease(&pBallBrush);
4835 }
4836
4837 void DrawBallInHandIndicator(ID2D1RenderTarget* pRT) {
4838 if (!isDraggingCueBall && (currentGameState != BALL_IN_HAND_P1 && currentGameState != BALL_IN_HAND_P2 && currentGameState != PRE_BREAK_PLACEMENT)) {
4839 return; // Only show when placing/dragging
4840 }
4841
4842 Ball* cueBall = GetCueBall();
4843 if (!cueBall) return;
4844
4845 ID2D1SolidColorBrush* pGhostBrush = nullptr;
4846 pRT->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::White, 0.6f), &pGhostBrush); // Semi-transparent white
4847
4848 if (pGhostBrush) {
4849 D2D1_POINT_2F drawPos;
4850 if (isDraggingCueBall) {
4851 drawPos = D2D1::Point2F((float)ptMouse.x, (float)ptMouse.y);
4852 }
4853 else {
4854 // If not dragging but in placement state, show at current ball pos
4855 drawPos = D2D1::Point2F(cueBall->x, cueBall->y);
4856 }
4857
4858 // Check if the placement is valid before drawing differently?
4859 bool behindHeadstring = (currentGameState == PRE_BREAK_PLACEMENT);
4860 bool isValid = IsValidCueBallPosition(drawPos.x, drawPos.y, behindHeadstring);
4861
4862 if (!isValid) {
4863 // Maybe draw red outline if invalid placement?
4864 pGhostBrush->SetColor(D2D1::ColorF(D2D1::ColorF::Red, 0.6f));
4865 }
4866
4867
4868 D2D1_ELLIPSE ghostEllipse = D2D1::Ellipse(drawPos, BALL_RADIUS, BALL_RADIUS);
4869 pRT->FillEllipse(&ghostEllipse, pGhostBrush);
4870 pRT->DrawEllipse(&ghostEllipse, pGhostBrush, 1.0f); // Outline
4871
4872 SafeRelease(&pGhostBrush);
4873 }
4874 }
4875
4876 void DrawPocketSelectionIndicator(ID2D1RenderTarget* pRT) {
4877 /* Never show the arrow while the player is still placing the
4878 cue-ball (ball-in-hand) – it otherwise hides behind the
4879 ghost-ball and can lock the UI. */
4880
4881 /* Still skip the opening-break placement,
4882 but show the arrow during BALL-IN-HAND */
4883 // ? skip when no active call for the CURRENT shooter
4884 if ((currentPlayer == 1 && calledPocketP1 < 0) ||
4885 (currentPlayer == 2 && calledPocketP2 < 0)) return;
4886 /*if (currentGameState == PRE_BREAK_PLACEMENT)
4887 return;*/ //new ai-asked-to-disable
4888 /*if (currentGameState == BALL_IN_HAND_P1 ||
4889 currentGameState == BALL_IN_HAND_P2 ||
4890 currentGameState == PRE_BREAK_PLACEMENT)
4891 {
4892 return;
4893 }*/
4894
4895 int pocketToIndicate = -1;
4896 // Whenever EITHER player has pocketed their first 7 and has called (human or AI),
4897 // we forcibly show their arrow—regardless of currentGameState.
4898 if ((currentPlayer == 1 && player1Info.ballsPocketedCount >= 7 && calledPocketP1 >= 0) ||
4899 (currentPlayer == 2 && player2Info.ballsPocketedCount >= 7 && calledPocketP2 >= 0))
4900 {
4901 pocketToIndicate = (currentPlayer == 1) ? calledPocketP1 : calledPocketP2;
4902 }
4903 /*// A human player is actively choosing if they are in the CHOOSING_POCKET state.
4904 bool isHumanChoosing = (currentGameState == CHOOSING_POCKET_P1 || (currentGameState == CHOOSING_POCKET_P2 && !isPlayer2AI));
4905
4906 if (isHumanChoosing) {
4907 // When choosing, show the currently selected pocket (which has a default).
4908 pocketToIndicate = (currentPlayer == 1) ? calledPocketP1 : calledPocketP2;
4909 }
4910 else if (IsPlayerOnEightBall(currentPlayer)) {
4911 // If it's a normal turn but the player is on the 8-ball, show their called pocket as a reminder.
4912 pocketToIndicate = (currentPlayer == 1) ? calledPocketP1 : calledPocketP2;
4913 }*/
4914
4915 if (pocketToIndicate < 0 || pocketToIndicate > 5) {
4916 return; // Don't draw if no pocket is selected or relevant.
4917 }
4918
4919 ID2D1SolidColorBrush* pArrowBrush = nullptr;
4920 pRT->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::Yellow, 0.9f), &pArrowBrush);
4921 if (!pArrowBrush) return;
4922
4923 // ... The rest of your arrow drawing geometry logic remains exactly the same ...
4924 // (No changes needed to the points/path drawing, only the logic above)
4925 D2D1_POINT_2F targetPocketCenter = pocketPositions[pocketToIndicate];
4926 float arrowHeadSize = HOLE_VISUAL_RADIUS * 0.5f;
4927 float arrowShaftLength = HOLE_VISUAL_RADIUS * 0.3f;
4928 float arrowShaftWidth = arrowHeadSize * 0.4f;
4929 float verticalOffsetFromPocketCenter = HOLE_VISUAL_RADIUS * 1.6f;
4930 D2D1_POINT_2F tip, baseLeft, baseRight, shaftTopLeft, shaftTopRight, shaftBottomLeft, shaftBottomRight;
4931
4932 if (targetPocketCenter.y == TABLE_TOP) {
4933 tip = D2D1::Point2F(targetPocketCenter.x, targetPocketCenter.y + verticalOffsetFromPocketCenter + arrowHeadSize);
4934 baseLeft = D2D1::Point2F(targetPocketCenter.x - arrowHeadSize / 2.0f, targetPocketCenter.y + verticalOffsetFromPocketCenter);
4935 baseRight = D2D1::Point2F(targetPocketCenter.x + arrowHeadSize / 2.0f, targetPocketCenter.y + verticalOffsetFromPocketCenter);
4936 shaftTopLeft = D2D1::Point2F(targetPocketCenter.x - arrowShaftWidth / 2.0f, baseLeft.y);
4937 shaftTopRight = D2D1::Point2F(targetPocketCenter.x + arrowShaftWidth / 2.0f, baseRight.y);
4938 shaftBottomLeft = D2D1::Point2F(targetPocketCenter.x - arrowShaftWidth / 2.0f, baseLeft.y - arrowShaftLength);
4939 shaftBottomRight = D2D1::Point2F(targetPocketCenter.x + arrowShaftWidth / 2.0f, baseRight.y - arrowShaftLength);
4940 }
4941 else {
4942 tip = D2D1::Point2F(targetPocketCenter.x, targetPocketCenter.y - verticalOffsetFromPocketCenter - arrowHeadSize);
4943 baseLeft = D2D1::Point2F(targetPocketCenter.x - arrowHeadSize / 2.0f, targetPocketCenter.y - verticalOffsetFromPocketCenter);
4944 baseRight = D2D1::Point2F(targetPocketCenter.x + arrowHeadSize / 2.0f, targetPocketCenter.y - verticalOffsetFromPocketCenter);
4945 shaftTopLeft = D2D1::Point2F(targetPocketCenter.x - arrowShaftWidth / 2.0f, baseLeft.y + arrowShaftLength);
4946 shaftTopRight = D2D1::Point2F(targetPocketCenter.x + arrowShaftWidth / 2.0f, baseRight.y + arrowShaftLength);
4947 shaftBottomLeft = D2D1::Point2F(targetPocketCenter.x - arrowShaftWidth / 2.0f, baseLeft.y);
4948 shaftBottomRight = D2D1::Point2F(targetPocketCenter.x + arrowShaftWidth / 2.0f, baseRight.y);
4949 }
4950
4951 ID2D1PathGeometry* pPath = nullptr;
4952 if (SUCCEEDED(pFactory->CreatePathGeometry(&pPath))) {
4953 ID2D1GeometrySink* pSink = nullptr;
4954 if (SUCCEEDED(pPath->Open(&pSink))) {
4955 pSink->BeginFigure(tip, D2D1_FIGURE_BEGIN_FILLED);
4956 pSink->AddLine(baseLeft); pSink->AddLine(shaftBottomLeft); pSink->AddLine(shaftTopLeft);
4957 pSink->AddLine(shaftTopRight); pSink->AddLine(shaftBottomRight); pSink->AddLine(baseRight);
4958 pSink->EndFigure(D2D1_FIGURE_END_CLOSED);
4959 pSink->Close();
4960 SafeRelease(&pSink);
4961 pRT->FillGeometry(pPath, pArrowBrush);
4962 }
4963 SafeRelease(&pPath);
4964 }
4965 SafeRelease(&pArrowBrush);
4966 }
4967```