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