· 5 months ago · Apr 29, 2025, 02:55 AM
1Follow Project On Github::: https://github.com/alienfxfiend/Prelude-in-C/tree/main/Yahoo-8Ball-Pool-Clone
2
3==++ Here's the full source for (file 1/3 (No OOP-based)) "Pool-Game-Clone.cpp"::: ++==
4```Pool-Game-Clone.cpp
5#define WIN32_LEAN_AND_MEAN
6#define NOMINMAX
7#include <windows.h>
8#include <d2d1.h>
9#include <dwrite.h>
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 <thread>
20#include "resource.h"
21
22#pragma comment(lib, "Comctl32.lib") // Link against common controls library
23#pragma comment(lib, "d2d1.lib")
24#pragma comment(lib, "dwrite.lib")
25#pragma comment(lib, "Winmm.lib") // Link against Windows Multimedia library
26
27// --- Constants ---
28const float PI = 3.1415926535f;
29const float BALL_RADIUS = 10.0f;
30const float TABLE_LEFT = 100.0f;
31const float TABLE_TOP = 100.0f;
32const float TABLE_WIDTH = 700.0f;
33const float TABLE_HEIGHT = 350.0f;
34const float TABLE_RIGHT = TABLE_LEFT + TABLE_WIDTH;
35const float TABLE_BOTTOM = TABLE_TOP + TABLE_HEIGHT;
36const float CUSHION_THICKNESS = 20.0f;
37const float HOLE_VISUAL_RADIUS = 22.0f; // Visual size of the hole
38const float POCKET_RADIUS = HOLE_VISUAL_RADIUS * 1.05f; // Make detection radius slightly larger // Make detection radius match visual size (or slightly larger)
39const float MAX_SHOT_POWER = 15.0f;
40const float FRICTION = 0.985f; // Friction factor per frame
41const float MIN_VELOCITY_SQ = 0.01f * 0.01f; // Stop balls below this squared velocity
42const float HEADSTRING_X = TABLE_LEFT + TABLE_WIDTH * 0.30f; // 30% line
43const float RACK_POS_X = TABLE_LEFT + TABLE_WIDTH * 0.65f; // 65% line for rack apex
44const float RACK_POS_Y = TABLE_TOP + TABLE_HEIGHT / 2.0f;
45const UINT ID_TIMER = 1;
46const int TARGET_FPS = 60; // Target frames per second for timer
47
48// --- Enums ---
49// --- MODIFIED/NEW Enums ---
50enum GameState {
51 SHOWING_DIALOG, // NEW: Game is waiting for initial dialog input
52 PRE_BREAK_PLACEMENT,// Player placing cue ball for break
53 BREAKING, // Player is aiming/shooting the break shot
54 AIMING, // Player is aiming
55 AI_THINKING, // NEW: AI is calculating its move
56 SHOT_IN_PROGRESS, // Balls are moving
57 ASSIGNING_BALLS, // Turn after break where ball types are assigned
58 PLAYER1_TURN,
59 PLAYER2_TURN,
60 BALL_IN_HAND_P1,
61 BALL_IN_HAND_P2,
62 GAME_OVER
63};
64
65enum BallType {
66 NONE,
67 SOLID, // Yellow (1-7)
68 STRIPE, // Red (9-15)
69 EIGHT_BALL, // Black (8)
70 CUE_BALL // White (0)
71};
72
73// NEW Enums for Game Mode and AI Difficulty
74enum GameMode {
75 HUMAN_VS_HUMAN,
76 HUMAN_VS_AI
77};
78
79enum AIDifficulty {
80 EASY,
81 MEDIUM,
82 HARD
83};
84
85// --- Structs ---
86struct Ball {
87 int id; // 0=Cue, 1-7=Solid, 8=Eight, 9-15=Stripe
88 BallType type;
89 float x, y;
90 float vx, vy;
91 D2D1_COLOR_F color;
92 bool isPocketed;
93};
94
95struct PlayerInfo {
96 BallType assignedType;
97 int ballsPocketedCount;
98 std::wstring name;
99};
100
101// --- Global Variables ---
102
103// Direct2D & DirectWrite
104ID2D1Factory* pFactory = nullptr;
105ID2D1HwndRenderTarget* pRenderTarget = nullptr;
106IDWriteFactory* pDWriteFactory = nullptr;
107IDWriteTextFormat* pTextFormat = nullptr;
108IDWriteTextFormat* pLargeTextFormat = nullptr; // For "Foul!"
109
110// Game State
111HWND hwndMain = nullptr;
112GameState currentGameState = SHOWING_DIALOG; // Start by showing dialog
113std::vector<Ball> balls;
114int currentPlayer = 1; // 1 or 2
115PlayerInfo player1Info = { BallType::NONE, 0, L"Player 1" };
116PlayerInfo player2Info = { BallType::NONE, 0, L"CPU" }; // Default P2 name
117bool foulCommitted = false;
118std::wstring gameOverMessage = L"";
119bool firstBallPocketedAfterBreak = false;
120std::vector<int> pocketedThisTurn;
121// --- NEW: Foul Tracking Globals ---
122int firstHitBallIdThisShot = -1; // ID of the first object ball hit by cue ball (-1 if none)
123bool cueHitObjectBallThisShot = false; // Did cue ball hit an object ball this shot?
124bool railHitAfterContact = false; // Did any ball hit a rail AFTER cue hit an object ball?
125// --- End New Foul Tracking Globals ---
126
127// NEW Game Mode/AI Globals
128GameMode gameMode = HUMAN_VS_HUMAN; // Default mode
129AIDifficulty aiDifficulty = MEDIUM; // Default difficulty
130bool isPlayer2AI = false; // Is Player 2 controlled by AI?
131bool aiTurnPending = false; // Flag: AI needs to take its turn when possible
132// bool aiIsThinking = false; // Replaced by AI_THINKING game state
133
134// Input & Aiming
135POINT ptMouse = { 0, 0 };
136bool isAiming = false;
137bool isDraggingCueBall = false;
138// --- ENSURE THIS LINE EXISTS HERE ---
139bool isDraggingStick = false; // True specifically when drag initiated on the stick graphic
140// --- End Ensure ---
141bool isSettingEnglish = false;
142D2D1_POINT_2F aimStartPoint = { 0, 0 };
143float cueAngle = 0.0f;
144float shotPower = 0.0f;
145float cueSpinX = 0.0f; // Range -1 to 1
146float cueSpinY = 0.0f; // Range -1 to 1
147float pocketFlashTimer = 0.0f;
148bool cheatModeEnabled = false; // Cheat Mode toggle (G key)
149int draggingBallId = -1;
150bool keyboardAimingActive = false; // NEW FLAG: true when arrow keys modify aim/power
151
152// UI Element Positions
153D2D1_RECT_F powerMeterRect = { TABLE_RIGHT + CUSHION_THICKNESS + 10, TABLE_TOP, TABLE_RIGHT + CUSHION_THICKNESS + 40, TABLE_BOTTOM };
154D2D1_RECT_F spinIndicatorRect = { TABLE_LEFT - CUSHION_THICKNESS - 60, TABLE_TOP + 20, TABLE_LEFT - CUSHION_THICKNESS - 20, TABLE_TOP + 60 }; // Circle area
155D2D1_POINT_2F spinIndicatorCenter = { spinIndicatorRect.left + (spinIndicatorRect.right - spinIndicatorRect.left) / 2.0f, spinIndicatorRect.top + (spinIndicatorRect.bottom - spinIndicatorRect.top) / 2.0f };
156float spinIndicatorRadius = (spinIndicatorRect.right - spinIndicatorRect.left) / 2.0f;
157D2D1_RECT_F pocketedBallsBarRect = { TABLE_LEFT, TABLE_BOTTOM + CUSHION_THICKNESS + 30, TABLE_RIGHT, TABLE_BOTTOM + CUSHION_THICKNESS + 70 };
158
159// Corrected Pocket Center Positions (aligned with table corners/edges)
160const D2D1_POINT_2F pocketPositions[6] = {
161 {TABLE_LEFT, TABLE_TOP}, // Top-Left
162 {TABLE_LEFT + TABLE_WIDTH / 2.0f, TABLE_TOP}, // Top-Middle
163 {TABLE_RIGHT, TABLE_TOP}, // Top-Right
164 {TABLE_LEFT, TABLE_BOTTOM}, // Bottom-Left
165 {TABLE_LEFT + TABLE_WIDTH / 2.0f, TABLE_BOTTOM}, // Bottom-Middle
166 {TABLE_RIGHT, TABLE_BOTTOM} // Bottom-Right
167};
168
169// Colors
170const D2D1_COLOR_F TABLE_COLOR = D2D1::ColorF(0.0f, 0.5f, 0.1f); // Darker Green
171const D2D1_COLOR_F CUSHION_COLOR = D2D1::ColorF(D2D1::ColorF::Red);
172const D2D1_COLOR_F POCKET_COLOR = D2D1::ColorF(D2D1::ColorF::Black);
173const D2D1_COLOR_F CUE_BALL_COLOR = D2D1::ColorF(D2D1::ColorF::White);
174const D2D1_COLOR_F EIGHT_BALL_COLOR = D2D1::ColorF(D2D1::ColorF::Black);
175const D2D1_COLOR_F SOLID_COLOR = D2D1::ColorF(D2D1::ColorF::Yellow); // Solids = Yellow
176const D2D1_COLOR_F STRIPE_COLOR = D2D1::ColorF(D2D1::ColorF::Red); // Stripes = Red
177const D2D1_COLOR_F AIM_LINE_COLOR = D2D1::ColorF(D2D1::ColorF::White, 0.7f); // Semi-transparent white
178const D2D1_COLOR_F FOUL_TEXT_COLOR = D2D1::ColorF(D2D1::ColorF::Red);
179const D2D1_COLOR_F TURN_ARROW_COLOR = D2D1::ColorF(D2D1::ColorF::Blue);
180const D2D1_COLOR_F ENGLISH_DOT_COLOR = D2D1::ColorF(D2D1::ColorF::Red);
181const D2D1_COLOR_F UI_TEXT_COLOR = D2D1::ColorF(D2D1::ColorF::Black);
182
183// --- Forward Declarations ---
184HRESULT CreateDeviceResources();
185void DiscardDeviceResources();
186void OnPaint();
187void OnResize(UINT width, UINT height);
188void InitGame();
189void GameUpdate();
190void UpdatePhysics();
191void CheckCollisions();
192bool CheckPockets(); // Returns true if any ball was pocketed
193void ProcessShotResults();
194void ApplyShot(float power, float angle, float spinX, float spinY);
195void RespawnCueBall(bool behindHeadstring);
196bool AreBallsMoving();
197void SwitchTurns();
198void AssignPlayerBallTypes(BallType firstPocketedType);
199void CheckGameOverConditions(bool eightBallPocketed, bool cueBallPocketed);
200Ball* GetBallById(int id);
201Ball* GetCueBall();
202
203// Drawing Functions
204void DrawScene(ID2D1RenderTarget* pRT);
205void DrawTable(ID2D1RenderTarget* pRT);
206void DrawBalls(ID2D1RenderTarget* pRT);
207void DrawCueStick(ID2D1RenderTarget* pRT);
208void DrawAimingAids(ID2D1RenderTarget* pRT);
209void DrawUI(ID2D1RenderTarget* pRT);
210void DrawPowerMeter(ID2D1RenderTarget* pRT);
211void DrawSpinIndicator(ID2D1RenderTarget* pRT);
212void DrawPocketedBallsIndicator(ID2D1RenderTarget* pRT);
213void DrawBallInHandIndicator(ID2D1RenderTarget* pRT);
214
215// Helper Functions
216float GetDistance(float x1, float y1, float x2, float y2);
217float GetDistanceSq(float x1, float y1, float x2, float y2);
218bool IsValidCueBallPosition(float x, float y, bool checkHeadstring);
219template <typename T> void SafeRelease(T** ppT);
220// --- ADD FORWARD DECLARATION FOR NEW HELPER HERE ---
221float PointToLineSegmentDistanceSq(D2D1_POINT_2F p, D2D1_POINT_2F a, D2D1_POINT_2F b);
222// --- End Forward Declaration ---
223bool 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
224
225// --- NEW Forward Declarations ---
226
227// AI Related
228struct AIShotInfo; // Define below
229void TriggerAIMove();
230void AIMakeDecision();
231void AIPlaceCueBall();
232AIShotInfo AIFindBestShot();
233AIShotInfo EvaluateShot(Ball* targetBall, int pocketIndex);
234bool IsPathClear(D2D1_POINT_2F start, D2D1_POINT_2F end, int ignoredBallId1, int ignoredBallId2);
235Ball* FindFirstHitBall(D2D1_POINT_2F start, float angle, float& hitDistSq); // Added hitDistSq output
236float CalculateShotPower(float cueToGhostDist, float targetToPocketDist);
237D2D1_POINT_2F CalculateGhostBallPos(Ball* targetBall, int pocketIndex);
238bool IsValidAIAimAngle(float angle); // Basic check
239
240// Dialog Related
241INT_PTR CALLBACK NewGameDialogProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam);
242void ShowNewGameDialog(HINSTANCE hInstance);
243void ResetGame(HINSTANCE hInstance); // Function to handle F2 reset
244
245// --- Forward Declaration for Window Procedure --- <<< Add this line HERE
246LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);
247
248// --- NEW Struct for AI Shot Evaluation ---
249struct AIShotInfo {
250 bool possible = false; // Is this shot considered viable?
251 Ball* targetBall = nullptr; // Which ball to hit
252 int pocketIndex = -1; // Which pocket to aim for (0-5)
253 D2D1_POINT_2F ghostBallPos = { 0,0 }; // Where cue ball needs to hit target ball
254 float angle = 0.0f; // Calculated shot angle
255 float power = 0.0f; // Calculated shot power
256 float score = -1.0f; // Score for this shot (higher is better)
257 bool involves8Ball = false; // Is the target the 8-ball?
258};
259
260// --- NEW Dialog Procedure ---
261INT_PTR CALLBACK NewGameDialogProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam) {
262 switch (message) {
263 case WM_INITDIALOG:
264 {
265 // --- ACTION 4: Center Dialog Box ---
266// Optional: Force centering if default isn't working
267 RECT rcDlg, rcOwner, rcScreen;
268 HWND hwndOwner = GetParent(hDlg); // GetParent(hDlg) might be better if hwndMain is passed
269 if (hwndOwner == NULL) hwndOwner = GetDesktopWindow();
270
271 GetWindowRect(hwndOwner, &rcOwner);
272 GetWindowRect(hDlg, &rcDlg);
273 CopyRect(&rcScreen, &rcOwner); // Use owner rect as reference bounds
274
275 // Offset the owner rect relative to the screen if it's not the desktop
276 if (GetParent(hDlg) != NULL) { // If parented to main window (passed to DialogBoxParam)
277 OffsetRect(&rcOwner, -rcScreen.left, -rcScreen.top);
278 OffsetRect(&rcDlg, -rcScreen.left, -rcScreen.top);
279 OffsetRect(&rcScreen, -rcScreen.left, -rcScreen.top);
280 }
281
282
283 // Calculate centered position
284 int x = rcOwner.left + (rcOwner.right - rcOwner.left - (rcDlg.right - rcDlg.left)) / 2;
285 int y = rcOwner.top + (rcOwner.bottom - rcOwner.top - (rcDlg.bottom - rcDlg.top)) / 2;
286
287 // Ensure it stays within screen bounds (optional safety)
288 x = std::max(static_cast<int>(rcScreen.left), x);
289 y = std::max(static_cast<int>(rcScreen.top), y);
290 if (x + (rcDlg.right - rcDlg.left) > rcScreen.right)
291 x = rcScreen.right - (rcDlg.right - rcDlg.left);
292 if (y + (rcDlg.bottom - rcDlg.top) > rcScreen.bottom)
293 y = rcScreen.bottom - (rcDlg.bottom - rcDlg.top);
294
295
296 // Set the dialog position
297 SetWindowPos(hDlg, HWND_TOP, x, y, 0, 0, SWP_NOSIZE);
298
299 // --- End Centering Code ---
300
301 // Set initial state based on current global settings (or defaults)
302 CheckRadioButton(hDlg, IDC_RADIO_2P, IDC_RADIO_CPU, (gameMode == HUMAN_VS_HUMAN) ? IDC_RADIO_2P : IDC_RADIO_CPU);
303
304 CheckRadioButton(hDlg, IDC_RADIO_EASY, IDC_RADIO_HARD,
305 (aiDifficulty == EASY) ? IDC_RADIO_EASY : ((aiDifficulty == MEDIUM) ? IDC_RADIO_MEDIUM : IDC_RADIO_HARD));
306
307 // Enable/Disable AI group based on initial mode
308 EnableWindow(GetDlgItem(hDlg, IDC_GROUP_AI), gameMode == HUMAN_VS_AI);
309 EnableWindow(GetDlgItem(hDlg, IDC_RADIO_EASY), gameMode == HUMAN_VS_AI);
310 EnableWindow(GetDlgItem(hDlg, IDC_RADIO_MEDIUM), gameMode == HUMAN_VS_AI);
311 EnableWindow(GetDlgItem(hDlg, IDC_RADIO_HARD), gameMode == HUMAN_VS_AI);
312 }
313 return (INT_PTR)TRUE;
314
315 case WM_COMMAND:
316 switch (LOWORD(wParam)) {
317 case IDC_RADIO_2P:
318 case IDC_RADIO_CPU:
319 {
320 bool isCPU = IsDlgButtonChecked(hDlg, IDC_RADIO_CPU) == BST_CHECKED;
321 // Enable/Disable AI group controls based on selection
322 EnableWindow(GetDlgItem(hDlg, IDC_GROUP_AI), isCPU);
323 EnableWindow(GetDlgItem(hDlg, IDC_RADIO_EASY), isCPU);
324 EnableWindow(GetDlgItem(hDlg, IDC_RADIO_MEDIUM), isCPU);
325 EnableWindow(GetDlgItem(hDlg, IDC_RADIO_HARD), isCPU);
326 }
327 return (INT_PTR)TRUE;
328
329 case IDOK:
330 // Retrieve selected options and store in global variables
331 if (IsDlgButtonChecked(hDlg, IDC_RADIO_CPU) == BST_CHECKED) {
332 gameMode = HUMAN_VS_AI;
333 if (IsDlgButtonChecked(hDlg, IDC_RADIO_EASY) == BST_CHECKED) aiDifficulty = EASY;
334 else if (IsDlgButtonChecked(hDlg, IDC_RADIO_MEDIUM) == BST_CHECKED) aiDifficulty = MEDIUM;
335 else if (IsDlgButtonChecked(hDlg, IDC_RADIO_HARD) == BST_CHECKED) aiDifficulty = HARD;
336 }
337 else {
338 gameMode = HUMAN_VS_HUMAN;
339 }
340 EndDialog(hDlg, IDOK); // Close dialog, return IDOK
341 return (INT_PTR)TRUE;
342
343 case IDCANCEL: // Handle Cancel or closing the dialog
344 EndDialog(hDlg, IDCANCEL);
345 return (INT_PTR)TRUE;
346 }
347 break; // End WM_COMMAND
348 }
349 return (INT_PTR)FALSE; // Default processing
350}
351
352// --- NEW Helper to Show Dialog ---
353void ShowNewGameDialog(HINSTANCE hInstance) {
354 if (DialogBoxParam(hInstance, MAKEINTRESOURCE(IDD_NEWGAMEDLG), hwndMain, NewGameDialogProc, 0) == IDOK) {
355 // User clicked Start, reset game with new settings
356 isPlayer2AI = (gameMode == HUMAN_VS_AI); // Update AI flag
357 if (isPlayer2AI) {
358 switch (aiDifficulty) {
359 case EASY: player2Info.name = L"CPU (Easy)"; break;
360 case MEDIUM: player2Info.name = L"CPU (Medium)"; break;
361 case HARD: player2Info.name = L"CPU (Hard)"; break;
362 }
363 }
364 else {
365 player2Info.name = L"Player 2";
366 }
367 // Update window title
368 std::wstring windowTitle = L"Direct2D 8-Ball Pool";
369 if (gameMode == HUMAN_VS_HUMAN) windowTitle += L" (Human vs Human)";
370 else windowTitle += L" (Human vs " + player2Info.name + L")";
371 SetWindowText(hwndMain, windowTitle.c_str());
372
373 InitGame(); // Re-initialize game logic & board
374 InvalidateRect(hwndMain, NULL, TRUE); // Force redraw
375 }
376 else {
377 // User cancelled dialog - maybe just resume game? Or exit?
378 // For simplicity, we do nothing, game continues as it was.
379 // To exit on cancel from F2, would need more complex state management.
380 }
381}
382
383// --- NEW Reset Game Function ---
384void ResetGame(HINSTANCE hInstance) {
385 // Call the helper function to show the dialog and re-init if OK clicked
386 ShowNewGameDialog(hInstance);
387}
388
389// --- WinMain ---
390int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, PWSTR, int nCmdShow) {
391 if (FAILED(CoInitialize(NULL))) {
392 MessageBox(NULL, L"COM Initialization Failed.", L"Error", MB_OK | MB_ICONERROR);
393 return -1;
394 }
395
396 // --- NEW: Show configuration dialog FIRST ---
397 if (DialogBoxParam(hInstance, MAKEINTRESOURCE(IDD_NEWGAMEDLG), NULL, NewGameDialogProc, 0) != IDOK) {
398 // User cancelled the dialog
399 CoUninitialize();
400 return 0; // Exit gracefully if dialog cancelled
401 }
402 // Global gameMode and aiDifficulty are now set by the DialogProc
403
404 // Set AI flag based on game mode
405 isPlayer2AI = (gameMode == HUMAN_VS_AI);
406 if (isPlayer2AI) {
407 switch (aiDifficulty) {
408 case EASY: player2Info.name = L"CPU (Easy)"; break;
409 case MEDIUM: player2Info.name = L"CPU (Medium)"; break;
410 case HARD: player2Info.name = L"CPU (Hard)"; break;
411 }
412 }
413 else {
414 player2Info.name = L"Player 2";
415 }
416 // --- End of Dialog Logic ---
417
418
419 WNDCLASS wc = { };
420 wc.lpfnWndProc = WndProc;
421 wc.hInstance = hInstance;
422 wc.lpszClassName = L"Direct2D_8BallPool";
423 wc.hCursor = LoadCursor(NULL, IDC_ARROW);
424 wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
425 wc.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_ICON1)); // Use your actual icon ID here
426
427 if (!RegisterClass(&wc)) {
428 MessageBox(NULL, L"Window Registration Failed.", L"Error", MB_OK | MB_ICONERROR);
429 CoUninitialize();
430 return -1;
431 }
432
433 // --- ACTION 4: Calculate Centered Window Position ---
434 const int WINDOW_WIDTH = 1000; // Define desired width
435 const int WINDOW_HEIGHT = 700; // Define desired height
436 int screenWidth = GetSystemMetrics(SM_CXSCREEN);
437 int screenHeight = GetSystemMetrics(SM_CYSCREEN);
438 int windowX = (screenWidth - WINDOW_WIDTH) / 2;
439 int windowY = (screenHeight - WINDOW_HEIGHT) / 2;
440
441 // --- Change Window Title based on mode ---
442 std::wstring windowTitle = L"Direct2D 8-Ball Pool";
443 if (gameMode == HUMAN_VS_HUMAN) windowTitle += L" (Human vs Human)";
444 else windowTitle += L" (Human vs " + player2Info.name + L")";
445
446 DWORD dwStyle = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX; // No WS_THICKFRAME, No WS_MAXIMIZEBOX
447
448 hwndMain = CreateWindowEx(
449 0, L"Direct2D_8BallPool", windowTitle.c_str(), dwStyle,
450 windowX, windowY, WINDOW_WIDTH, WINDOW_HEIGHT,
451 NULL, NULL, hInstance, NULL
452 );
453
454 if (!hwndMain) {
455 MessageBox(NULL, L"Window Creation Failed.", L"Error", MB_OK | MB_ICONERROR);
456 CoUninitialize();
457 return -1;
458 }
459
460 // Initialize Direct2D Resources AFTER window creation
461 if (FAILED(CreateDeviceResources())) {
462 MessageBox(NULL, L"Failed to create Direct2D resources.", L"Error", MB_OK | MB_ICONERROR);
463 DestroyWindow(hwndMain);
464 CoUninitialize();
465 return -1;
466 }
467
468 InitGame(); // Initialize game state AFTER resources are ready & mode is set
469
470 ShowWindow(hwndMain, nCmdShow);
471 UpdateWindow(hwndMain);
472
473 if (!SetTimer(hwndMain, ID_TIMER, 1000 / TARGET_FPS, NULL)) {
474 MessageBox(NULL, L"Could not SetTimer().", L"Error", MB_OK | MB_ICONERROR);
475 DestroyWindow(hwndMain);
476 CoUninitialize();
477 return -1;
478 }
479
480 MSG msg = { };
481 // --- Modified Main Loop ---
482 // Handles the case where the game starts in SHOWING_DIALOG state (handled now before loop)
483 // or gets reset to it via F2. The main loop runs normally once game starts.
484 while (GetMessage(&msg, NULL, 0, 0)) {
485 // We might need modeless dialog handling here if F2 shows dialog
486 // while window is active, but DialogBoxParam is modal.
487 // Let's assume F2 hides main window, shows dialog, then restarts game loop.
488 // Simpler: F2 calls ResetGame which calls DialogBoxParam (modal) then InitGame.
489 TranslateMessage(&msg);
490 DispatchMessage(&msg);
491 }
492
493
494 KillTimer(hwndMain, ID_TIMER);
495 DiscardDeviceResources();
496 CoUninitialize();
497
498 return (int)msg.wParam;
499}
500
501// --- WndProc ---
502LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
503 // Declare cueBall pointer once at the top, used in multiple cases
504 // For clarity, often better to declare within each case where needed.
505 Ball* cueBall = nullptr; // Initialize to nullptr
506 switch (msg) {
507 case WM_CREATE:
508 // Resources are now created in WinMain after CreateWindowEx
509 return 0;
510
511 case WM_PAINT:
512 OnPaint();
513 // Validate the entire window region after painting
514 ValidateRect(hwnd, NULL);
515 return 0;
516
517 case WM_SIZE: {
518 UINT width = LOWORD(lParam);
519 UINT height = HIWORD(lParam);
520 OnResize(width, height);
521 return 0;
522 }
523
524 case WM_TIMER:
525 if (wParam == ID_TIMER) {
526 GameUpdate(); // Update game logic and physics
527 InvalidateRect(hwnd, NULL, FALSE); // Request redraw
528 }
529 return 0;
530
531 // --- NEW: Handle F2 Key for Reset ---
532 // --- MODIFIED: Handle More Keys ---
533 case WM_KEYDOWN:
534 { // Add scope for variable declarations
535
536 // --- FIX: Get Cue Ball pointer for this scope ---
537 cueBall = GetCueBall();
538 // We might allow some keys even if cue ball is gone (like F1/F2), but actions need it
539 // --- End Fix ---
540
541 // Check which player can interact via keyboard (Humans only)
542 bool canPlayerControl = ((currentPlayer == 1 && (currentGameState == PLAYER1_TURN || currentGameState == AIMING || currentGameState == BREAKING || currentGameState == BALL_IN_HAND_P1 || currentGameState == PRE_BREAK_PLACEMENT)) ||
543 (currentPlayer == 2 && !isPlayer2AI && (currentGameState == PLAYER2_TURN || currentGameState == AIMING || currentGameState == BREAKING || currentGameState == BALL_IN_HAND_P2 || currentGameState == PRE_BREAK_PLACEMENT)));
544
545 // --- F1 / F2 Keys (Always available) ---
546 if (wParam == VK_F2) {
547 HINSTANCE hInstance = (HINSTANCE)GetWindowLongPtr(hwnd, GWLP_HINSTANCE);
548 ResetGame(hInstance); // Call reset function
549 return 0; // Indicate key was processed
550 }
551 else if (wParam == VK_F1) {
552 MessageBox(hwnd,
553 L"Direct2D-based StickPool game made in C++ from scratch (2764+ lines of code)\n" // Update line count if needed
554 L"First successful Clone in C++ (no other sites or projects were there to glean from.) Made /w AI assist\n"
555 L"(others were in JS/ non-8-Ball in C# etc.) w/o OOP and Graphics Frameworks all in a Single file.\n"
556 L"Copyright (C) 2025 Evans Thorpemorton, Entisoft Solutions.\n"
557 L"Includes AI Difficulty Modes, Aim-Trajectory For Table Rails + Hard Angles TipShots. || F2=New Game",
558 L"About This Game", MB_OK | MB_ICONINFORMATION);
559 return 0; // Indicate key was processed
560 }
561
562 // --- Player Interaction Keys (Only if allowed) ---
563 if (canPlayerControl) {
564 // --- Get Shift Key State ---
565 bool shiftPressed = (GetKeyState(VK_SHIFT) & 0x8000) != 0;
566 float angleStep = shiftPressed ? 0.05f : 0.01f; // Base step / Faster step (Adjust as needed) // Multiplier was 5x
567 float powerStep = 0.2f; // Power step (Adjust as needed)
568
569 switch (wParam) {
570 case VK_LEFT: // Rotate Cue Stick Counter-Clockwise
571 if (currentGameState != SHOT_IN_PROGRESS && currentGameState != AI_THINKING) {
572 cueAngle -= angleStep;
573 // Normalize angle (keep between 0 and 2*PI)
574 if (cueAngle < 0) cueAngle += 2 * PI;
575 // Ensure state shows aiming visuals if turn just started
576 if (currentGameState == PLAYER1_TURN || currentGameState == PLAYER2_TURN) currentGameState = AIMING;
577 isAiming = false; // Keyboard adjust doesn't use mouse aiming state
578 isDraggingStick = false;
579 keyboardAimingActive = true;
580 }
581 break;
582
583 case VK_RIGHT: // Rotate Cue Stick Clockwise
584 if (currentGameState != SHOT_IN_PROGRESS && currentGameState != AI_THINKING) {
585 cueAngle += angleStep;
586 // Normalize angle (keep between 0 and 2*PI)
587 if (cueAngle >= 2 * PI) cueAngle -= 2 * PI;
588 // Ensure state shows aiming visuals if turn just started
589 if (currentGameState == PLAYER1_TURN || currentGameState == PLAYER2_TURN) currentGameState = AIMING;
590 isAiming = false;
591 isDraggingStick = false;
592 keyboardAimingActive = true;
593 }
594 break;
595
596 case VK_UP: // Decrease Shot Power
597 if (currentGameState != SHOT_IN_PROGRESS && currentGameState != AI_THINKING) {
598 shotPower -= powerStep;
599 if (shotPower < 0.0f) shotPower = 0.0f;
600 // Ensure state shows aiming visuals if turn just started
601 if (currentGameState == PLAYER1_TURN || currentGameState == PLAYER2_TURN) currentGameState = AIMING;
602 isAiming = true; // Keyboard adjust doesn't use mouse aiming state
603 isDraggingStick = false;
604 keyboardAimingActive = true;
605 }
606 break;
607
608 case VK_DOWN: // Increase Shot Power
609 if (currentGameState != SHOT_IN_PROGRESS && currentGameState != AI_THINKING) {
610 shotPower += powerStep;
611 if (shotPower > MAX_SHOT_POWER) shotPower = MAX_SHOT_POWER;
612 // Ensure state shows aiming visuals if turn just started
613 if (currentGameState == PLAYER1_TURN || currentGameState == PLAYER2_TURN) currentGameState = AIMING;
614 isAiming = true;
615 isDraggingStick = false;
616 keyboardAimingActive = true;
617 }
618 break;
619
620 case VK_SPACE: // Trigger Shot
621 if ((currentGameState == AIMING || currentGameState == BREAKING || currentGameState == PLAYER1_TURN || currentGameState == PLAYER2_TURN)
622 && currentGameState != SHOT_IN_PROGRESS && currentGameState != AI_THINKING)
623 {
624 if (shotPower > 0.15f) { // Use same threshold as mouse
625 // Reset foul flags BEFORE applying shot
626 firstHitBallIdThisShot = -1;
627 cueHitObjectBallThisShot = false;
628 railHitAfterContact = false;
629
630 // Play sound & Apply Shot
631 std::thread([](const TCHAR* soundName) { PlaySound(soundName, NULL, SND_FILENAME | SND_NODEFAULT); }, TEXT("cue.wav")).detach();
632 ApplyShot(shotPower, cueAngle, cueSpinX, cueSpinY);
633
634 // Update State
635 currentGameState = SHOT_IN_PROGRESS;
636 foulCommitted = false;
637 pocketedThisTurn.clear();
638 shotPower = 0; // Reset power after shooting
639 isAiming = false; isDraggingStick = false; // Reset aiming flags
640 keyboardAimingActive = false;
641 }
642 }
643 break;
644
645 case VK_ESCAPE: // Cancel Aim/Shot Setup
646 if ((currentGameState == AIMING || currentGameState == BREAKING) || shotPower > 0)
647 {
648 shotPower = 0.0f;
649 isAiming = false;
650 isDraggingStick = false;
651 keyboardAimingActive = false;
652 // Revert to basic turn state if not breaking
653 if (currentGameState != BREAKING) {
654 currentGameState = (currentPlayer == 1) ? PLAYER1_TURN : PLAYER2_TURN;
655 }
656 }
657 break;
658
659 case 'G': // Toggle Cheat Mode
660 cheatModeEnabled = !cheatModeEnabled;
661 if (cheatModeEnabled)
662 MessageBeep(MB_ICONEXCLAMATION); // Play a beep when enabling
663 else
664 MessageBeep(MB_OK); // Play a different beep when disabling
665 break;
666
667 default:
668 // Allow default processing for other keys if needed
669 // return DefWindowProc(hwnd, msg, wParam, lParam); // Usually not needed for WM_KEYDOWN
670 break;
671 } // End switch(wParam) for player controls
672 return 0; // Indicate player control key was processed
673 } // End if(canPlayerControl)
674 } // End scope for WM_KEYDOWN case
675 // If key wasn't F1/F2 and player couldn't control, maybe allow default processing?
676 // return DefWindowProc(hwnd, msg, wParam, lParam); // Or just return 0
677 return 0;
678
679 case WM_MOUSEMOVE: {
680 ptMouse.x = LOWORD(lParam);
681 ptMouse.y = HIWORD(lParam);
682
683 cueBall = GetCueBall(); // Declare and get cueBall pointer
684
685 if (isDraggingCueBall && cheatModeEnabled && draggingBallId != -1) {
686 Ball* ball = GetBallById(draggingBallId);
687 if (ball) {
688 ball->x = (float)ptMouse.x;
689 ball->y = (float)ptMouse.y;
690 ball->vx = ball->vy = 0.0f;
691 }
692 return 0;
693 }
694
695 if (!cueBall) return 0;
696
697 // Update Aiming Logic (Check player turn)
698 if (isDraggingCueBall &&
699 ((currentPlayer == 1 && currentGameState == BALL_IN_HAND_P1) ||
700 (!isPlayer2AI && currentPlayer == 2 && currentGameState == BALL_IN_HAND_P2) ||
701 currentGameState == PRE_BREAK_PLACEMENT))
702 {
703 bool behindHeadstring = (currentGameState == PRE_BREAK_PLACEMENT);
704 // Tentative position update
705 cueBall->x = (float)ptMouse.x;
706 cueBall->y = (float)ptMouse.y;
707 cueBall->vx = cueBall->vy = 0;
708 }
709 else if ((isAiming || isDraggingStick) &&
710 ((currentPlayer == 1 && (currentGameState == AIMING || currentGameState == BREAKING)) ||
711 (!isPlayer2AI && currentPlayer == 2 && (currentGameState == AIMING || currentGameState == BREAKING))))
712 {
713 //NEW2 MOUSEBOUND CODE = START
714 /*// Clamp mouse inside table bounds during aiming
715 if (ptMouse.x < TABLE_LEFT) ptMouse.x = TABLE_LEFT;
716 if (ptMouse.x > TABLE_RIGHT) ptMouse.x = TABLE_RIGHT;
717 if (ptMouse.y < TABLE_TOP) ptMouse.y = TABLE_TOP;
718 if (ptMouse.y > TABLE_BOTTOM) ptMouse.y = TABLE_BOTTOM;*/
719 //NEW2 MOUSEBOUND CODE = END
720 // Aiming drag updates angle and power
721 float dx = (float)ptMouse.x - cueBall->x;
722 float dy = (float)ptMouse.y - cueBall->y;
723 if (dx != 0 || dy != 0) cueAngle = atan2f(dy, dx);
724 //float pullDist = GetDistance((float)ptMouse.x, (float)ptMouse.y, aimStartPoint.x, aimStartPoint.y);
725 //shotPower = std::min(pullDist / 10.0f, MAX_SHOT_POWER);
726 if (!keyboardAimingActive) { // Only update shotPower if NOT keyboard aiming
727 float pullDist = GetDistance((float)ptMouse.x, (float)ptMouse.y, aimStartPoint.x, aimStartPoint.y);
728 shotPower = std::min(pullDist / 10.0f, MAX_SHOT_POWER);
729 }
730 }
731 else if (isSettingEnglish &&
732 ((currentPlayer == 1 && (currentGameState == PLAYER1_TURN || currentGameState == AIMING || currentGameState == BREAKING)) ||
733 (!isPlayer2AI && currentPlayer == 2 && (currentGameState == PLAYER2_TURN || currentGameState == AIMING || currentGameState == BREAKING))))
734 {
735 // Setting English
736 float dx = (float)ptMouse.x - spinIndicatorCenter.x;
737 float dy = (float)ptMouse.y - spinIndicatorCenter.y;
738 float dist = GetDistance(dx, dy, 0, 0);
739 if (dist > spinIndicatorRadius) { dx *= spinIndicatorRadius / dist; dy *= spinIndicatorRadius / dist; }
740 cueSpinX = dx / spinIndicatorRadius;
741 cueSpinY = dy / spinIndicatorRadius;
742 }
743 else {
744 //DISABLE PERM AIMING = START
745 /*// Update visual angle even when not aiming/dragging (Check player turn)
746 bool canUpdateVisualAngle = ((currentPlayer == 1 && (currentGameState == PLAYER1_TURN || currentGameState == BALL_IN_HAND_P1)) ||
747 (currentPlayer == 2 && !isPlayer2AI && (currentGameState == PLAYER2_TURN || currentGameState == BALL_IN_HAND_P2)) ||
748 currentGameState == PRE_BREAK_PLACEMENT || currentGameState == BREAKING || currentGameState == AIMING);
749
750 if (canUpdateVisualAngle && !isDraggingCueBall && !isAiming && !isDraggingStick && !keyboardAimingActive) // NEW: Prevent mouse override if keyboard aiming
751 {
752 // NEW MOUSEBOUND CODE = START
753 // Only update cue angle if mouse is inside the playable table area
754 if (ptMouse.x >= TABLE_LEFT && ptMouse.x <= TABLE_RIGHT &&
755 ptMouse.y >= TABLE_TOP && ptMouse.y <= TABLE_BOTTOM)
756 {
757 // NEW MOUSEBOUND CODE = END
758 Ball* cb = cueBall; // Use function-scope cueBall // Already got cueBall above
759 if (cb) {
760 float dx = (float)ptMouse.x - cb->x;
761 float dy = (float)ptMouse.y - cb->y;
762 if (dx != 0 || dy != 0) cueAngle = atan2f(dy, dx);
763 }
764 } //NEW MOUSEBOUND CODE LINE = DISABLE
765 }*/
766 //DISABLE PERM AIMING = END
767 }
768 return 0;
769 } // End WM_MOUSEMOVE
770
771 case WM_LBUTTONDOWN: {
772 ptMouse.x = LOWORD(lParam);
773 ptMouse.y = HIWORD(lParam);
774
775 if (cheatModeEnabled) {
776 // Allow dragging any ball freely
777 for (Ball& ball : balls) {
778 float distSq = GetDistanceSq(ball.x, ball.y, (float)ptMouse.x, (float)ptMouse.y);
779 if (distSq <= BALL_RADIUS * BALL_RADIUS * 4) { // Click near ball
780 isDraggingCueBall = true;
781 draggingBallId = ball.id;
782 if (ball.id == 0) {
783 // If dragging cue ball manually, ensure we stay in Ball-In-Hand state
784 if (currentPlayer == 1)
785 currentGameState = BALL_IN_HAND_P1;
786 else if (currentPlayer == 2 && !isPlayer2AI)
787 currentGameState = BALL_IN_HAND_P2;
788 }
789 return 0;
790 }
791 }
792 }
793
794 Ball* cueBall = GetCueBall(); // Declare and get cueBall pointer
795
796 // Check which player is allowed to interact via mouse click
797 bool canPlayerClickInteract = ((currentPlayer == 1) || (currentPlayer == 2 && !isPlayer2AI));
798 // Define states where interaction is generally allowed
799 bool canInteractState = (currentGameState == PLAYER1_TURN || currentGameState == PLAYER2_TURN ||
800 currentGameState == AIMING || currentGameState == BREAKING ||
801 currentGameState == BALL_IN_HAND_P1 || currentGameState == BALL_IN_HAND_P2 ||
802 currentGameState == PRE_BREAK_PLACEMENT);
803
804 // Check Spin Indicator first (Allow if player's turn/aim phase)
805 if (canPlayerClickInteract && canInteractState) {
806 float spinDistSq = GetDistanceSq((float)ptMouse.x, (float)ptMouse.y, spinIndicatorCenter.x, spinIndicatorCenter.y);
807 if (spinDistSq < spinIndicatorRadius * spinIndicatorRadius * 1.2f) {
808 isSettingEnglish = true;
809 float dx = (float)ptMouse.x - spinIndicatorCenter.x;
810 float dy = (float)ptMouse.y - spinIndicatorCenter.y;
811 float dist = GetDistance(dx, dy, 0, 0);
812 if (dist > spinIndicatorRadius) { dx *= spinIndicatorRadius / dist; dy *= spinIndicatorRadius / dist; }
813 cueSpinX = dx / spinIndicatorRadius;
814 cueSpinY = dy / spinIndicatorRadius;
815 isAiming = false; isDraggingStick = false; isDraggingCueBall = false;
816 return 0;
817 }
818 }
819
820 if (!cueBall) return 0;
821
822 // Check Ball-in-Hand placement/drag
823 bool isPlacingBall = (currentGameState == BALL_IN_HAND_P1 || currentGameState == BALL_IN_HAND_P2 || currentGameState == PRE_BREAK_PLACEMENT);
824 bool isPlayerAllowedToPlace = (isPlacingBall &&
825 ((currentPlayer == 1 && currentGameState == BALL_IN_HAND_P1) ||
826 (currentPlayer == 2 && !isPlayer2AI && currentGameState == BALL_IN_HAND_P2) ||
827 (currentGameState == PRE_BREAK_PLACEMENT))); // Allow current player in break setup
828
829 if (isPlayerAllowedToPlace) {
830 float distSq = GetDistanceSq(cueBall->x, cueBall->y, (float)ptMouse.x, (float)ptMouse.y);
831 if (distSq < BALL_RADIUS * BALL_RADIUS * 9.0f) {
832 isDraggingCueBall = true;
833 isAiming = false; isDraggingStick = false;
834 }
835 else {
836 bool behindHeadstring = (currentGameState == PRE_BREAK_PLACEMENT);
837 if (IsValidCueBallPosition((float)ptMouse.x, (float)ptMouse.y, behindHeadstring)) {
838 cueBall->x = (float)ptMouse.x; cueBall->y = (float)ptMouse.y;
839 cueBall->vx = 0; cueBall->vy = 0;
840 isDraggingCueBall = false;
841 // Transition state
842 if (currentGameState == PRE_BREAK_PLACEMENT) currentGameState = BREAKING;
843 else if (currentGameState == BALL_IN_HAND_P1) currentGameState = PLAYER1_TURN;
844 else if (currentGameState == BALL_IN_HAND_P2) currentGameState = PLAYER2_TURN;
845 cueAngle = 0.0f;
846 }
847 }
848 return 0;
849 }
850
851 // Check for starting Aim (Cue Ball OR Stick)
852 bool canAim = ((currentPlayer == 1 && (currentGameState == PLAYER1_TURN || currentGameState == BREAKING)) ||
853 (currentPlayer == 2 && !isPlayer2AI && (currentGameState == PLAYER2_TURN || currentGameState == BREAKING)));
854
855 if (canAim) {
856 const float stickDrawLength = 150.0f * 1.4f;
857 float currentStickAngle = cueAngle + PI;
858 D2D1_POINT_2F currentStickEnd = D2D1::Point2F(cueBall->x + cosf(currentStickAngle) * stickDrawLength, cueBall->y + sinf(currentStickAngle) * stickDrawLength);
859 D2D1_POINT_2F currentStickTip = D2D1::Point2F(cueBall->x + cosf(currentStickAngle) * 5.0f, cueBall->y + sinf(currentStickAngle) * 5.0f);
860 float distToStickSq = PointToLineSegmentDistanceSq(D2D1::Point2F((float)ptMouse.x, (float)ptMouse.y), currentStickTip, currentStickEnd);
861 float stickClickThresholdSq = 36.0f;
862 float distToCueBallSq = GetDistanceSq(cueBall->x, cueBall->y, (float)ptMouse.x, (float)ptMouse.y);
863 float cueBallClickRadiusSq = BALL_RADIUS * BALL_RADIUS * 25;
864
865 bool clickedStick = (distToStickSq < stickClickThresholdSq);
866 bool clickedCueArea = (distToCueBallSq < cueBallClickRadiusSq);
867
868 if (clickedStick || clickedCueArea) {
869 isDraggingStick = clickedStick && !clickedCueArea;
870 isAiming = clickedCueArea;
871 aimStartPoint = D2D1::Point2F((float)ptMouse.x, (float)ptMouse.y);
872 shotPower = 0;
873 float dx = (float)ptMouse.x - cueBall->x;
874 float dy = (float)ptMouse.y - cueBall->y;
875 if (dx != 0 || dy != 0) cueAngle = atan2f(dy, dx);
876 if (currentGameState != BREAKING) currentGameState = AIMING;
877 }
878 }
879 return 0;
880 } // End WM_LBUTTONDOWN
881
882
883 case WM_LBUTTONUP: {
884 if (cheatModeEnabled && isDraggingCueBall) {
885 isDraggingCueBall = false;
886 if (draggingBallId == 0) {
887 // After dropping CueBall, stay Ball-In-Hand mode if needed
888 if (currentPlayer == 1)
889 currentGameState = BALL_IN_HAND_P1;
890 else if (currentPlayer == 2 && !isPlayer2AI)
891 currentGameState = BALL_IN_HAND_P2;
892 }
893 draggingBallId = -1;
894 return 0;
895 }
896
897 ptMouse.x = LOWORD(lParam);
898 ptMouse.y = HIWORD(lParam);
899
900 Ball* cueBall = GetCueBall(); // Get cueBall pointer
901
902 // Check for releasing aim drag (Stick OR Cue Ball)
903 if ((isAiming || isDraggingStick) &&
904 ((currentPlayer == 1 && (currentGameState == AIMING || currentGameState == BREAKING)) ||
905 (!isPlayer2AI && currentPlayer == 2 && (currentGameState == AIMING || currentGameState == BREAKING))))
906 {
907 bool wasAiming = isAiming;
908 bool wasDraggingStick = isDraggingStick;
909 isAiming = false; isDraggingStick = false;
910
911 if (shotPower > 0.15f) { // Check power threshold
912 if (currentGameState != AI_THINKING) {
913 firstHitBallIdThisShot = -1; cueHitObjectBallThisShot = false; railHitAfterContact = false; // Reset foul flags
914 std::thread([](const TCHAR* soundName) { PlaySound(soundName, NULL, SND_FILENAME | SND_NODEFAULT); }, TEXT("cue.wav")).detach();
915 ApplyShot(shotPower, cueAngle, cueSpinX, cueSpinY);
916 currentGameState = SHOT_IN_PROGRESS;
917 foulCommitted = false; pocketedThisTurn.clear();
918 }
919 }
920 else if (currentGameState != AI_THINKING) { // Revert state if power too low
921 if (currentGameState == BREAKING) { /* Still breaking */ }
922 else {
923 currentGameState = (currentPlayer == 1) ? PLAYER1_TURN : PLAYER2_TURN;
924 if (currentPlayer == 2 && isPlayer2AI) aiTurnPending = false;
925 }
926 }
927 shotPower = 0; // Reset power indicator regardless
928 }
929
930 // Handle releasing cue ball drag (placement)
931 if (isDraggingCueBall) {
932 isDraggingCueBall = false;
933 // Check player allowed to place
934 bool isPlacingState = (currentGameState == BALL_IN_HAND_P1 || currentGameState == BALL_IN_HAND_P2 || currentGameState == PRE_BREAK_PLACEMENT);
935 bool isPlayerAllowed = (isPlacingState &&
936 ((currentPlayer == 1 && currentGameState == BALL_IN_HAND_P1) ||
937 (currentPlayer == 2 && !isPlayer2AI && currentGameState == BALL_IN_HAND_P2) ||
938 (currentGameState == PRE_BREAK_PLACEMENT)));
939
940 if (isPlayerAllowed && cueBall) {
941 bool behindHeadstring = (currentGameState == PRE_BREAK_PLACEMENT);
942 if (IsValidCueBallPosition(cueBall->x, cueBall->y, behindHeadstring)) {
943 // Finalize position already set by mouse move
944 // Transition state
945 if (currentGameState == PRE_BREAK_PLACEMENT) currentGameState = BREAKING;
946 else if (currentGameState == BALL_IN_HAND_P1) currentGameState = PLAYER1_TURN;
947 else if (currentGameState == BALL_IN_HAND_P2) currentGameState = PLAYER2_TURN;
948 cueAngle = 0.0f;
949 }
950 else { /* Stay in BALL_IN_HAND state if final pos invalid */ }
951 }
952 }
953
954 // Handle releasing english setting
955 if (isSettingEnglish) {
956 isSettingEnglish = false;
957 }
958 return 0;
959 } // End WM_LBUTTONUP
960
961 case WM_DESTROY:
962 PostQuitMessage(0);
963 return 0;
964
965 default:
966 return DefWindowProc(hwnd, msg, wParam, lParam);
967 }
968 return 0;
969}
970
971// --- Direct2D Resource Management ---
972
973HRESULT CreateDeviceResources() {
974 HRESULT hr = S_OK;
975
976 // Create Direct2D Factory
977 if (!pFactory) {
978 hr = D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, &pFactory);
979 if (FAILED(hr)) return hr;
980 }
981
982 // Create DirectWrite Factory
983 if (!pDWriteFactory) {
984 hr = DWriteCreateFactory(
985 DWRITE_FACTORY_TYPE_SHARED,
986 __uuidof(IDWriteFactory),
987 reinterpret_cast<IUnknown**>(&pDWriteFactory)
988 );
989 if (FAILED(hr)) return hr;
990 }
991
992 // Create Text Formats
993 if (!pTextFormat && pDWriteFactory) {
994 hr = pDWriteFactory->CreateTextFormat(
995 L"Segoe UI", NULL, DWRITE_FONT_WEIGHT_NORMAL, DWRITE_FONT_STYLE_NORMAL, DWRITE_FONT_STRETCH_NORMAL,
996 16.0f, L"en-us", &pTextFormat
997 );
998 if (FAILED(hr)) return hr;
999 // Center align text
1000 pTextFormat->SetTextAlignment(DWRITE_TEXT_ALIGNMENT_CENTER);
1001 pTextFormat->SetParagraphAlignment(DWRITE_PARAGRAPH_ALIGNMENT_CENTER);
1002 }
1003 if (!pLargeTextFormat && pDWriteFactory) {
1004 hr = pDWriteFactory->CreateTextFormat(
1005 L"Impact", NULL, DWRITE_FONT_WEIGHT_BOLD, DWRITE_FONT_STYLE_NORMAL, DWRITE_FONT_STRETCH_NORMAL,
1006 48.0f, L"en-us", &pLargeTextFormat
1007 );
1008 if (FAILED(hr)) return hr;
1009 pLargeTextFormat->SetTextAlignment(DWRITE_TEXT_ALIGNMENT_LEADING); // Align left
1010 pLargeTextFormat->SetParagraphAlignment(DWRITE_PARAGRAPH_ALIGNMENT_CENTER);
1011 }
1012
1013
1014 // Create Render Target (needs valid hwnd)
1015 if (!pRenderTarget && hwndMain) {
1016 RECT rc;
1017 GetClientRect(hwndMain, &rc);
1018 D2D1_SIZE_U size = D2D1::SizeU(rc.right - rc.left, rc.bottom - rc.top);
1019
1020 hr = pFactory->CreateHwndRenderTarget(
1021 D2D1::RenderTargetProperties(),
1022 D2D1::HwndRenderTargetProperties(hwndMain, size),
1023 &pRenderTarget
1024 );
1025 if (FAILED(hr)) {
1026 // If failed, release factories if they were created in this call
1027 SafeRelease(&pTextFormat);
1028 SafeRelease(&pLargeTextFormat);
1029 SafeRelease(&pDWriteFactory);
1030 SafeRelease(&pFactory);
1031 pRenderTarget = nullptr; // Ensure it's null on failure
1032 return hr;
1033 }
1034 }
1035
1036 return hr;
1037}
1038
1039void DiscardDeviceResources() {
1040 SafeRelease(&pRenderTarget);
1041 SafeRelease(&pTextFormat);
1042 SafeRelease(&pLargeTextFormat);
1043 SafeRelease(&pDWriteFactory);
1044 // Keep pFactory until application exit? Or release here too? Let's release.
1045 SafeRelease(&pFactory);
1046}
1047
1048void OnResize(UINT width, UINT height) {
1049 if (pRenderTarget) {
1050 D2D1_SIZE_U size = D2D1::SizeU(width, height);
1051 pRenderTarget->Resize(size); // Ignore HRESULT for simplicity here
1052 }
1053}
1054
1055// --- Game Initialization ---
1056void InitGame() {
1057 srand((unsigned int)time(NULL)); // Seed random number generator
1058
1059 // --- Ensure pocketed list is clear from the absolute start ---
1060 pocketedThisTurn.clear();
1061
1062 balls.clear(); // Clear existing balls
1063
1064 // Reset Player Info (Names should be set by Dialog/wWinMain/ResetGame)
1065 player1Info.assignedType = BallType::NONE;
1066 player1Info.ballsPocketedCount = 0;
1067 // Player 1 Name usually remains "Player 1"
1068 player2Info.assignedType = BallType::NONE;
1069 player2Info.ballsPocketedCount = 0;
1070 // Player 2 Name is set based on gameMode in ShowNewGameDialog
1071
1072 // Create Cue Ball (ID 0)
1073 // Initial position will be set during PRE_BREAK_PLACEMENT state
1074 balls.push_back({ 0, BallType::CUE_BALL, TABLE_LEFT + TABLE_WIDTH * 0.15f, RACK_POS_Y, 0, 0, CUE_BALL_COLOR, false });
1075
1076 // --- Create Object Balls (Temporary List) ---
1077 std::vector<Ball> objectBalls;
1078 // Solids (1-7, Yellow)
1079 for (int i = 1; i <= 7; ++i) {
1080 objectBalls.push_back({ i, BallType::SOLID, 0, 0, 0, 0, SOLID_COLOR, false });
1081 }
1082 // Stripes (9-15, Red)
1083 for (int i = 9; i <= 15; ++i) {
1084 objectBalls.push_back({ i, BallType::STRIPE, 0, 0, 0, 0, STRIPE_COLOR, false });
1085 }
1086 // 8-Ball (ID 8) - Add it to the list to be placed
1087 objectBalls.push_back({ 8, BallType::EIGHT_BALL, 0, 0, 0, 0, EIGHT_BALL_COLOR, false });
1088
1089
1090 // --- Racking Logic (Improved) ---
1091 float spacingX = BALL_RADIUS * 2.0f * 0.866f; // cos(30) for horizontal spacing
1092 float spacingY = BALL_RADIUS * 2.0f * 1.0f; // Vertical spacing
1093
1094 // Define rack positions (0-14 indices corresponding to triangle spots)
1095 D2D1_POINT_2F rackPositions[15];
1096 int rackIndex = 0;
1097 for (int row = 0; row < 5; ++row) {
1098 for (int col = 0; col <= row; ++col) {
1099 if (rackIndex >= 15) break;
1100 float x = RACK_POS_X + row * spacingX;
1101 float y = RACK_POS_Y + (col - row / 2.0f) * spacingY;
1102 rackPositions[rackIndex++] = D2D1::Point2F(x, y);
1103 }
1104 }
1105
1106 // Separate 8-ball
1107 Ball eightBall;
1108 std::vector<Ball> otherBalls; // Solids and Stripes
1109 bool eightBallFound = false;
1110 for (const auto& ball : objectBalls) {
1111 if (ball.id == 8) {
1112 eightBall = ball;
1113 eightBallFound = true;
1114 }
1115 else {
1116 otherBalls.push_back(ball);
1117 }
1118 }
1119 // Ensure 8 ball was actually created (should always be true)
1120 if (!eightBallFound) {
1121 // Handle error - perhaps recreate it? For now, proceed.
1122 eightBall = { 8, BallType::EIGHT_BALL, 0, 0, 0, 0, EIGHT_BALL_COLOR, false };
1123 }
1124
1125
1126 // Shuffle the other 14 balls
1127 // Use std::shuffle if available (C++11 and later) for better randomness
1128 // std::random_device rd;
1129 // std::mt19937 g(rd());
1130 // std::shuffle(otherBalls.begin(), otherBalls.end(), g);
1131 std::random_shuffle(otherBalls.begin(), otherBalls.end()); // Using deprecated for now
1132
1133 // --- Place balls into the main 'balls' vector in rack order ---
1134 // Important: Add the cue ball (already created) first.
1135 // (Cue ball added at the start of the function now)
1136
1137 // 1. Place the 8-ball in its fixed position (index 4 for the 3rd row center)
1138 int eightBallRackIndex = 4;
1139 eightBall.x = rackPositions[eightBallRackIndex].x;
1140 eightBall.y = rackPositions[eightBallRackIndex].y;
1141 eightBall.vx = 0;
1142 eightBall.vy = 0;
1143 eightBall.isPocketed = false;
1144 balls.push_back(eightBall); // Add 8 ball to the main vector
1145
1146 // 2. Place the shuffled Solids and Stripes in the remaining spots
1147 int otherBallIdx = 0;
1148 for (int i = 0; i < 15; ++i) {
1149 if (i == eightBallRackIndex) continue; // Skip the 8-ball spot
1150
1151 if (otherBallIdx < otherBalls.size()) {
1152 Ball& ballToPlace = otherBalls[otherBallIdx++];
1153 ballToPlace.x = rackPositions[i].x;
1154 ballToPlace.y = rackPositions[i].y;
1155 ballToPlace.vx = 0;
1156 ballToPlace.vy = 0;
1157 ballToPlace.isPocketed = false;
1158 balls.push_back(ballToPlace); // Add to the main game vector
1159 }
1160 }
1161 // --- End Racking Logic ---
1162
1163
1164 // --- Determine Who Breaks and Initial State ---
1165 if (isPlayer2AI) {
1166 // AI Mode: Randomly decide who breaks
1167 if ((rand() % 2) == 0) {
1168 // AI (Player 2) breaks
1169 currentPlayer = 2;
1170 currentGameState = PRE_BREAK_PLACEMENT; // AI needs to place ball first
1171 aiTurnPending = true; // Trigger AI logic
1172 }
1173 else {
1174 // Player 1 (Human) breaks
1175 currentPlayer = 1;
1176 currentGameState = PRE_BREAK_PLACEMENT; // Human places cue ball
1177 aiTurnPending = false;
1178 }
1179 }
1180 else {
1181 // Human vs Human, Player 1 breaks
1182 currentPlayer = 1;
1183 currentGameState = PRE_BREAK_PLACEMENT;
1184 aiTurnPending = false; // No AI involved
1185 }
1186
1187 // Reset other relevant game state variables
1188 foulCommitted = false;
1189 gameOverMessage = L"";
1190 firstBallPocketedAfterBreak = false;
1191 // pocketedThisTurn cleared at start
1192 // Reset shot parameters and input flags
1193 shotPower = 0.0f;
1194 cueSpinX = 0.0f;
1195 cueSpinY = 0.0f;
1196 isAiming = false;
1197 isDraggingCueBall = false;
1198 isSettingEnglish = false;
1199 cueAngle = 0.0f; // Reset aim angle
1200}
1201
1202
1203// --- Game Loop ---
1204void GameUpdate() {
1205 if (currentGameState == SHOT_IN_PROGRESS) {
1206 UpdatePhysics();
1207 CheckCollisions();
1208 bool pocketed = CheckPockets(); // Store if any ball was pocketed
1209
1210 // --- Update pocket flash animation timer ---
1211 if (pocketFlashTimer > 0.0f) {
1212 pocketFlashTimer -= 0.02f;
1213 if (pocketFlashTimer < 0.0f) pocketFlashTimer = 0.0f;
1214
1215 }
1216
1217
1218 if (!AreBallsMoving()) {
1219 ProcessShotResults(); // Determine next state based on what happened
1220 }
1221 }
1222 // --- Check if AI needs to act ---
1223 else if (aiTurnPending && !AreBallsMoving()) {
1224 // --- MODIFIED: Add BALL_IN_HAND_P2 to trigger states ---
1225 if (currentGameState == PLAYER2_TURN || currentGameState == BREAKING ||
1226 currentGameState == PRE_BREAK_PLACEMENT || currentGameState == BALL_IN_HAND_P2)
1227 // --- End Modification ---
1228 {
1229 // Only trigger if AI is P2 and it's their turn/placement phase
1230 if (isPlayer2AI && currentPlayer == 2) {
1231 currentGameState = AI_THINKING;
1232 aiTurnPending = false; // Acknowledge the pending flag
1233
1234 // Trigger AI Decision Making (will handle placement if state is BALL_IN_HAND_P2)
1235 AIMakeDecision();
1236
1237 // AIMakeDecision should end by setting currentGameState = SHOT_IN_PROGRESS (via ApplyShot)
1238 }
1239 else {
1240 aiTurnPending = false; // Clear flag if conditions not met
1241 }
1242 }
1243 else {
1244 aiTurnPending = false; // Clear flag if not in a state where AI should shoot/place
1245 }
1246 }
1247 // Other states are handled by input messages or state transitions
1248}
1249
1250// --- Physics and Collision ---
1251void UpdatePhysics() {
1252 for (size_t i = 0; i < balls.size(); ++i) {
1253 Ball& b = balls[i];
1254 if (!b.isPocketed) {
1255 b.x += b.vx;
1256 b.y += b.vy;
1257
1258 // Apply friction
1259 b.vx *= FRICTION;
1260 b.vy *= FRICTION;
1261
1262 // Stop balls if velocity is very low
1263 if (GetDistanceSq(b.vx, b.vy, 0, 0) < MIN_VELOCITY_SQ) {
1264 b.vx = 0;
1265 b.vy = 0;
1266 }
1267 }
1268 }
1269}
1270
1271void CheckCollisions() {
1272 float left = TABLE_LEFT;
1273 float right = TABLE_RIGHT;
1274 float top = TABLE_TOP;
1275 float bottom = TABLE_BOTTOM;
1276 const float pocketMouthCheckRadiusSq = (POCKET_RADIUS + BALL_RADIUS) * (POCKET_RADIUS + BALL_RADIUS) * 1.1f;
1277
1278 // --- Reset Per-Frame Sound Flags ---
1279 bool playedWallSoundThisFrame = false;
1280 bool playedCollideSoundThisFrame = false;
1281 // ---
1282
1283 for (size_t i = 0; i < balls.size(); ++i) {
1284 Ball& b1 = balls[i];
1285 if (b1.isPocketed) continue;
1286
1287 bool nearPocket[6];
1288 for (int p = 0; p < 6; ++p) {
1289 nearPocket[p] = GetDistanceSq(b1.x, b1.y, pocketPositions[p].x, pocketPositions[p].y) < pocketMouthCheckRadiusSq;
1290 }
1291 bool nearTopLeftPocket = nearPocket[0];
1292 bool nearTopMidPocket = nearPocket[1];
1293 bool nearTopRightPocket = nearPocket[2];
1294 bool nearBottomLeftPocket = nearPocket[3];
1295 bool nearBottomMidPocket = nearPocket[4];
1296 bool nearBottomRightPocket = nearPocket[5];
1297
1298 bool collidedWallThisBall = false;
1299
1300 // --- Ball-Wall Collisions ---
1301 // (Check logic unchanged, added sound calls and railHitAfterContact update)
1302 // Left Wall
1303 if (b1.x - BALL_RADIUS < left) {
1304 if (!nearTopLeftPocket && !nearBottomLeftPocket) {
1305 b1.x = left + BALL_RADIUS; b1.vx *= -1.0f; collidedWallThisBall = true;
1306 if (!playedWallSoundThisFrame) {
1307 std::thread([](const TCHAR* soundName) { PlaySound(soundName, NULL, SND_FILENAME | SND_NODEFAULT); }, TEXT("wall.wav")).detach();
1308 playedWallSoundThisFrame = true;
1309 }
1310 if (cueHitObjectBallThisShot) railHitAfterContact = true; // Track rail hit after contact
1311 }
1312 }
1313 // Right Wall
1314 if (b1.x + BALL_RADIUS > right) {
1315 if (!nearTopRightPocket && !nearBottomRightPocket) {
1316 b1.x = right - BALL_RADIUS; b1.vx *= -1.0f; collidedWallThisBall = true;
1317 if (!playedWallSoundThisFrame) {
1318 std::thread([](const TCHAR* soundName) { PlaySound(soundName, NULL, SND_FILENAME | SND_NODEFAULT); }, TEXT("wall.wav")).detach();
1319 playedWallSoundThisFrame = true;
1320 }
1321 if (cueHitObjectBallThisShot) railHitAfterContact = true; // Track rail hit after contact
1322 }
1323 }
1324 // Top Wall
1325 if (b1.y - BALL_RADIUS < top) {
1326 if (!nearTopLeftPocket && !nearTopMidPocket && !nearTopRightPocket) {
1327 b1.y = top + BALL_RADIUS; b1.vy *= -1.0f; collidedWallThisBall = true;
1328 if (!playedWallSoundThisFrame) {
1329 std::thread([](const TCHAR* soundName) { PlaySound(soundName, NULL, SND_FILENAME | SND_NODEFAULT); }, TEXT("wall.wav")).detach();
1330 playedWallSoundThisFrame = true;
1331 }
1332 if (cueHitObjectBallThisShot) railHitAfterContact = true; // Track rail hit after contact
1333 }
1334 }
1335 // Bottom Wall
1336 if (b1.y + BALL_RADIUS > bottom) {
1337 if (!nearBottomLeftPocket && !nearBottomMidPocket && !nearBottomRightPocket) {
1338 b1.y = bottom - BALL_RADIUS; b1.vy *= -1.0f; collidedWallThisBall = true;
1339 if (!playedWallSoundThisFrame) {
1340 std::thread([](const TCHAR* soundName) { PlaySound(soundName, NULL, SND_FILENAME | SND_NODEFAULT); }, TEXT("wall.wav")).detach();
1341 playedWallSoundThisFrame = true;
1342 }
1343 if (cueHitObjectBallThisShot) railHitAfterContact = true; // Track rail hit after contact
1344 }
1345 }
1346
1347 // Spin effect (Unchanged)
1348 if (collidedWallThisBall) {
1349 if (b1.x <= left + BALL_RADIUS || b1.x >= right - BALL_RADIUS) { b1.vy += cueSpinX * b1.vx * 0.05f; }
1350 if (b1.y <= top + BALL_RADIUS || b1.y >= bottom - BALL_RADIUS) { b1.vx -= cueSpinY * b1.vy * 0.05f; }
1351 cueSpinX *= 0.7f; cueSpinY *= 0.7f;
1352 }
1353
1354
1355 // --- Ball-Ball Collisions ---
1356 for (size_t j = i + 1; j < balls.size(); ++j) {
1357 Ball& b2 = balls[j];
1358 if (b2.isPocketed) continue;
1359
1360 float dx = b2.x - b1.x; float dy = b2.y - b1.y;
1361 float distSq = dx * dx + dy * dy;
1362 float minDist = BALL_RADIUS * 2.0f;
1363
1364 if (distSq > 1e-6 && distSq < minDist * minDist) {
1365 float dist = sqrtf(distSq);
1366 float overlap = minDist - dist;
1367 float nx = dx / dist; float ny = dy / dist;
1368
1369 // Separation (Unchanged)
1370 b1.x -= overlap * 0.5f * nx; b1.y -= overlap * 0.5f * ny;
1371 b2.x += overlap * 0.5f * nx; b2.y += overlap * 0.5f * ny;
1372
1373 float rvx = b1.vx - b2.vx; float rvy = b1.vy - b2.vy;
1374 float velAlongNormal = rvx * nx + rvy * ny;
1375
1376 if (velAlongNormal > 0) { // Colliding
1377 // --- Play Ball Collision Sound ---
1378 if (!playedCollideSoundThisFrame) {
1379 std::thread([](const TCHAR* soundName) { PlaySound(soundName, NULL, SND_FILENAME | SND_NODEFAULT); }, TEXT("poolballhit.wav")).detach();
1380 playedCollideSoundThisFrame = true; // Set flag
1381 }
1382 // --- End Sound ---
1383
1384 // --- NEW: Track First Hit and Cue/Object Collision ---
1385 if (firstHitBallIdThisShot == -1) { // If first hit hasn't been recorded yet
1386 if (b1.id == 0) { // Cue ball hit b2 first
1387 firstHitBallIdThisShot = b2.id;
1388 cueHitObjectBallThisShot = true;
1389 }
1390 else if (b2.id == 0) { // Cue ball hit b1 first
1391 firstHitBallIdThisShot = b1.id;
1392 cueHitObjectBallThisShot = true;
1393 }
1394 // If neither is cue ball, doesn't count as first hit for foul purposes
1395 }
1396 else if (b1.id == 0 || b2.id == 0) {
1397 // Track subsequent cue ball collisions with object balls
1398 cueHitObjectBallThisShot = true;
1399 }
1400 // --- End First Hit Tracking ---
1401
1402
1403 // Impulse (Unchanged)
1404 float impulse = velAlongNormal;
1405 b1.vx -= impulse * nx; b1.vy -= impulse * ny;
1406 b2.vx += impulse * nx; b2.vy += impulse * ny;
1407
1408 // Spin Transfer (Unchanged)
1409 if (b1.id == 0 || b2.id == 0) {
1410 float spinEffectFactor = 0.08f;
1411 b1.vx += (cueSpinY * ny - cueSpinX * nx) * spinEffectFactor;
1412 b1.vy += (cueSpinY * nx + cueSpinX * ny) * spinEffectFactor;
1413 b2.vx -= (cueSpinY * ny - cueSpinX * nx) * spinEffectFactor;
1414 b2.vy -= (cueSpinY * nx + cueSpinX * ny) * spinEffectFactor;
1415 cueSpinX *= 0.85f; cueSpinY *= 0.85f;
1416 }
1417 }
1418 }
1419 } // End ball-ball loop
1420 } // End ball loop
1421} // End CheckCollisions
1422
1423
1424bool CheckPockets() {
1425 bool ballPocketedThisCheck = false; // Local flag for this specific check run
1426 for (size_t i = 0; i < balls.size(); ++i) {
1427 Ball& b = balls[i];
1428 if (!b.isPocketed) { // Only check balls that aren't already flagged as pocketed
1429 for (int p = 0; p < 6; ++p) {
1430 float distSq = GetDistanceSq(b.x, b.y, pocketPositions[p].x, pocketPositions[p].y);
1431 // --- Use updated POCKET_RADIUS ---
1432 if (distSq < POCKET_RADIUS * POCKET_RADIUS) {
1433 b.isPocketed = true;
1434 b.vx = b.vy = 0;
1435 pocketedThisTurn.push_back(b.id);
1436
1437 // --- Play Pocket Sound (Threaded) ---
1438 if (!ballPocketedThisCheck) {
1439 std::thread([](const TCHAR* soundName) { PlaySound(soundName, NULL, SND_FILENAME | SND_NODEFAULT); }, TEXT("pocket.wav")).detach();
1440 ballPocketedThisCheck = true;
1441 }
1442 // --- End Sound ---
1443
1444 break; // Ball is pocketed
1445 }
1446 }
1447 }
1448 }
1449 return ballPocketedThisCheck;
1450}
1451
1452bool AreBallsMoving() {
1453 for (size_t i = 0; i < balls.size(); ++i) {
1454 if (!balls[i].isPocketed && (balls[i].vx != 0 || balls[i].vy != 0)) {
1455 return true;
1456 }
1457 }
1458 return false;
1459}
1460
1461void RespawnCueBall(bool behindHeadstring) { // 'behindHeadstring' only relevant for initial break placement
1462 Ball* cueBall = GetCueBall();
1463 if (cueBall) {
1464 // Reset position to a default
1465 cueBall->x = HEADSTRING_X * 0.5f;
1466 cueBall->y = TABLE_TOP + TABLE_HEIGHT / 2.0f;
1467 cueBall->vx = 0;
1468 cueBall->vy = 0;
1469 cueBall->isPocketed = false;
1470
1471 // Set state based on who gets ball-in-hand
1472 // 'currentPlayer' already reflects who's turn it is NOW (switched before calling this)
1473 if (currentPlayer == 1) { // Player 2 (AI/Human) fouled, Player 1 (Human) gets ball-in-hand
1474 currentGameState = BALL_IN_HAND_P1;
1475 aiTurnPending = false; // Ensure AI flag off
1476 }
1477 else { // Player 1 (Human) fouled, Player 2 gets ball-in-hand
1478 if (isPlayer2AI) {
1479 // --- CONFIRMED FIX: Set correct state for AI Ball-in-Hand ---
1480 currentGameState = BALL_IN_HAND_P2; // AI now needs to place the ball
1481 aiTurnPending = true; // Trigger AI logic (will call AIPlaceCueBall first)
1482 }
1483 else { // Human Player 2
1484 currentGameState = BALL_IN_HAND_P2;
1485 aiTurnPending = false; // Ensure AI flag off
1486 }
1487 }
1488 // Handle initial placement state correctly if called from InitGame
1489 if (behindHeadstring && currentGameState != PRE_BREAK_PLACEMENT) {
1490 // This case might need review depending on exact initial setup flow,
1491 // but the foul logic above should now be correct.
1492 // Let's ensure initial state is PRE_BREAK_PLACEMENT if behindHeadstring is true.
1493 currentGameState = PRE_BREAK_PLACEMENT;
1494 }
1495 }
1496}
1497
1498
1499// --- Game Logic ---
1500
1501void ApplyShot(float power, float angle, float spinX, float spinY) {
1502 Ball* cueBall = GetCueBall();
1503 if (cueBall) {
1504
1505 // --- Play Cue Strike Sound (Threaded) ---
1506 if (power > 0.1f) { // Only play if it's an audible shot
1507 std::thread([](const TCHAR* soundName) { PlaySound(soundName, NULL, SND_FILENAME | SND_NODEFAULT); }, TEXT("cue.wav")).detach();
1508 }
1509 // --- End Sound ---
1510
1511 cueBall->vx = cosf(angle) * power;
1512 cueBall->vy = sinf(angle) * power;
1513
1514 // Apply English (Spin) - Simplified effect (Unchanged)
1515 cueBall->vx += sinf(angle) * spinY * 0.5f;
1516 cueBall->vy -= cosf(angle) * spinY * 0.5f;
1517 cueBall->vx -= cosf(angle) * spinX * 0.5f;
1518 cueBall->vy -= sinf(angle) * spinX * 0.5f;
1519
1520 // Store spin (Unchanged)
1521 cueSpinX = spinX;
1522 cueSpinY = spinY;
1523
1524 // --- Reset Foul Tracking flags for the new shot ---
1525 // (Also reset in LBUTTONUP, but good to ensure here too)
1526 firstHitBallIdThisShot = -1; // No ball hit yet
1527 cueHitObjectBallThisShot = false; // Cue hasn't hit anything yet
1528 railHitAfterContact = false; // No rail hit after contact yet
1529 // --- End Reset ---
1530 }
1531}
1532
1533
1534void ProcessShotResults() {
1535 bool cueBallPocketed = false;
1536 bool eightBallPocketed = false;
1537 bool legalBallPocketed = false;
1538 bool opponentBallPocketed = false;
1539 bool anyNonCueBallPocketed = false; // Includes opponent balls
1540 BallType firstPocketedType = BallType::NONE;
1541 int firstPocketedId = -1;
1542
1543 PlayerInfo& currentPlayerInfo = (currentPlayer == 1) ? player1Info : player2Info;
1544 PlayerInfo& opponentPlayerInfo = (currentPlayer == 1) ? player2Info : player1Info;
1545
1546 // Analyze pocketed balls (Unchanged logic)
1547 for (int pocketedId : pocketedThisTurn) {
1548 Ball* b = GetBallById(pocketedId);
1549 if (!b) continue;
1550 if (!pocketedThisTurn.empty()) {
1551 pocketFlashTimer = 1.0f; // Flash boost when any ball is pocketed
1552 }
1553 if (b->id == 0) { cueBallPocketed = true; }
1554 else if (b->id == 8) { eightBallPocketed = true; }
1555 else {
1556 anyNonCueBallPocketed = true;
1557 if (firstPocketedId == -1) { firstPocketedId = b->id; firstPocketedType = b->type; }
1558 if (currentPlayerInfo.assignedType != BallType::NONE) {
1559 if (b->type == currentPlayerInfo.assignedType) legalBallPocketed = true;
1560 else if (b->type == opponentPlayerInfo.assignedType) opponentBallPocketed = true;
1561 }
1562 }
1563 }
1564
1565 // --- Game Over Checks --- (Unchanged logic)
1566 if (eightBallPocketed) {
1567 CheckGameOverConditions(eightBallPocketed, cueBallPocketed);
1568 if (currentGameState == GAME_OVER) return;
1569 }
1570
1571 // --- MODIFIED: Enhanced Foul Checks ---
1572 bool turnFoul = false;
1573
1574 // Foul 1: Scratch (Cue ball pocketed)
1575 if (cueBallPocketed) {
1576 foulCommitted = true; turnFoul = true;
1577 }
1578
1579 // Foul 2: Hit Nothing (Only if not already a scratch)
1580 // Condition: Cue ball didn't hit *any* object ball during the shot.
1581 if (!turnFoul && !cueHitObjectBallThisShot) {
1582 // Check if the cue ball actually moved significantly to constitute a shot attempt
1583 Ball* cue = GetCueBall();
1584 // Use a small threshold to avoid foul on accidental tiny nudge if needed
1585 // For now, any shot attempt that doesn't hit an object ball is a foul.
1586 // (Could add velocity check from ApplyShot if needed)
1587 if (cue) { // Ensure cue ball exists
1588 foulCommitted = true; turnFoul = true;
1589 }
1590 }
1591
1592 // Foul 3: Wrong Ball First (Check only if not already foul and *something* was hit)
1593 if (!turnFoul && firstHitBallIdThisShot != -1) {
1594 Ball* firstHitBall = GetBallById(firstHitBallIdThisShot);
1595 if (firstHitBall) {
1596 bool isBreakShot = (player1Info.assignedType == BallType::NONE && player2Info.assignedType == BallType::NONE);
1597 bool mustTarget8Ball = (!isBreakShot && currentPlayerInfo.assignedType != BallType::NONE && currentPlayerInfo.ballsPocketedCount >= 7);
1598
1599 if (!isBreakShot) { // Standard play rules
1600 if (mustTarget8Ball) {
1601 if (firstHitBall->id != 8) { foulCommitted = true; turnFoul = true; }
1602 }
1603 else if (currentPlayerInfo.assignedType != BallType::NONE) { // Colors assigned
1604 // Illegal to hit opponent ball OR 8-ball first
1605 if (firstHitBall->type == opponentPlayerInfo.assignedType || firstHitBall->id == 8) {
1606 foulCommitted = true; turnFoul = true;
1607 }
1608 }
1609 // If colors NOT assigned yet (e.g. shot immediately after break), hitting any ball is legal first.
1610 }
1611 // No specific first-hit foul rules applied for the break itself here.
1612 }
1613 }
1614
1615 // Foul 4: No Rail After Contact (Check only if not already foul)
1616 // Condition: Cue hit an object ball, BUT after that first contact,
1617 // NO ball hit a rail AND NO object ball was pocketed (excluding cue/8-ball).
1618 if (!turnFoul && cueHitObjectBallThisShot && !railHitAfterContact && !anyNonCueBallPocketed) {
1619 foulCommitted = true;
1620 turnFoul = true;
1621 }
1622
1623 // Foul 5: Pocketing Opponent's Ball (Optional stricter rule - can uncomment if desired)
1624 // if (!turnFoul && opponentBallPocketed) {
1625 // foulCommitted = true; turnFoul = true;
1626 // }
1627 // --- End Enhanced Foul Checks ---
1628
1629
1630 // --- State Transitions ---
1631 if (turnFoul) {
1632 SwitchTurns();
1633 RespawnCueBall(false); // Ball in hand for opponent (state set in Respawn)
1634 }
1635 // --- Assign Ball Types only AFTER checking for fouls on the break/first shot ---
1636 else if (player1Info.assignedType == BallType::NONE && anyNonCueBallPocketed) {
1637 // (Assign types logic - unchanged)
1638 bool firstTypeVerified = false;
1639 for (int id : pocketedThisTurn) { if (id == firstPocketedId) { firstTypeVerified = true; break; } }
1640
1641 if (firstTypeVerified && (firstPocketedType == BallType::SOLID || firstPocketedType == BallType::STRIPE)) {
1642 AssignPlayerBallTypes(firstPocketedType);
1643 legalBallPocketed = true;
1644 }
1645 // After assignment (or if types already assigned), check if turn continues
1646 if (legalBallPocketed) { // Player legally pocketed their assigned type (newly or existing)
1647 currentGameState = (currentPlayer == 1) ? PLAYER1_TURN : PLAYER2_TURN;
1648 if (currentPlayer == 2 && isPlayer2AI) aiTurnPending = true;
1649 }
1650 else { // Pocketed wrong ball, or only opponent ball, or missed (but no foul committed)
1651 SwitchTurns();
1652 }
1653 }
1654 // --- Normal Play Results (Types Assigned) ---
1655 else if (player1Info.assignedType != BallType::NONE) { // Ensure types assigned before this block
1656 if (legalBallPocketed) { // Legally pocketed own ball
1657 currentGameState = (currentPlayer == 1) ? PLAYER1_TURN : PLAYER2_TURN;
1658 if (currentPlayer == 2 && isPlayer2AI) aiTurnPending = true; // AI continues turn
1659 }
1660 else { // No legal ball pocketed (or no ball pocketed at all) and no foul
1661 SwitchTurns();
1662 }
1663 }
1664 // --- Handle case where shot occurred but no balls pocketed and no foul ---
1665 else if (!anyNonCueBallPocketed && !turnFoul) {
1666 SwitchTurns();
1667 }
1668
1669
1670 // Update pocketed counts AFTER handling turns/fouls/assignment
1671 int p1NewlyPocketed = 0;
1672 int p2NewlyPocketed = 0;
1673 for (int id : pocketedThisTurn) {
1674 if (id == 0 || id == 8) continue; // Skip cue ball and 8-ball
1675 Ball* b = GetBallById(id);
1676 if (!b) continue; // extra safety
1677 if (b->type == player1Info.assignedType) p1NewlyPocketed++;
1678 else if (b->type == player2Info.assignedType) p2NewlyPocketed++;
1679 }
1680 if (currentGameState != GAME_OVER) {
1681 player1Info.ballsPocketedCount += p1NewlyPocketed;
1682 player2Info.ballsPocketedCount += p2NewlyPocketed;
1683 }
1684
1685
1686 // --- Cleanup for next actual shot attempt ---
1687 pocketedThisTurn.clear();
1688 // Reset foul tracking flags (done before next shot applied)
1689 // firstHitBallIdThisShot = -1; // Reset these before next shot call
1690 // cueHitObjectBallThisShot = false;
1691 // railHitAfterContact = false;
1692}
1693
1694void AssignPlayerBallTypes(BallType firstPocketedType) {
1695 if (firstPocketedType == BallType::SOLID || firstPocketedType == BallType::STRIPE) {
1696 if (currentPlayer == 1) {
1697 player1Info.assignedType = firstPocketedType;
1698 player2Info.assignedType = (firstPocketedType == BallType::SOLID) ? BallType::STRIPE : BallType::SOLID;
1699 }
1700 else {
1701 player2Info.assignedType = firstPocketedType;
1702 player1Info.assignedType = (firstPocketedType == BallType::SOLID) ? BallType::STRIPE : BallType::SOLID;
1703 }
1704 }
1705 // If 8-ball was first (illegal on break generally), rules vary.
1706 // Here, we might ignore assignment until a solid/stripe is pocketed legally.
1707 // Or assign based on what *else* was pocketed, if anything.
1708 // Simplification: Assignment only happens on SOLID or STRIPE first pocket.
1709}
1710
1711void CheckGameOverConditions(bool eightBallPocketed, bool cueBallPocketed) {
1712 if (!eightBallPocketed) return; // Only proceed if 8-ball was pocketed
1713
1714 PlayerInfo& currentPlayerInfo = (currentPlayer == 1) ? player1Info : player2Info;
1715 bool playerClearedBalls = (currentPlayerInfo.assignedType != BallType::NONE && currentPlayerInfo.ballsPocketedCount >= 7);
1716
1717 // Loss Conditions:
1718 // 1. Pocket 8-ball AND scratch (pocket cue ball)
1719 // 2. Pocket 8-ball before clearing own color group
1720 if (cueBallPocketed || (!playerClearedBalls && currentPlayerInfo.assignedType != BallType::NONE)) {
1721 gameOverMessage = (currentPlayer == 1) ? L"Player 2 Wins! (Player 1 fouled on 8-ball)" : L"Player 1 Wins! (Player 2 fouled on 8-ball)";
1722 currentGameState = GAME_OVER;
1723 }
1724 // Win Condition:
1725 // 1. Pocket 8-ball legally after clearing own color group
1726 else if (playerClearedBalls) {
1727 gameOverMessage = (currentPlayer == 1) ? L"Player 1 Wins!" : L"Player 2 Wins!";
1728 currentGameState = GAME_OVER;
1729 }
1730 // Special case: 8 ball pocketed on break. Usually re-spot or re-rack.
1731 // Simple: If it happens during assignment phase, treat as foul, respawn 8ball.
1732 else if (player1Info.assignedType == BallType::NONE) {
1733 Ball* eightBall = GetBallById(8);
1734 if (eightBall) {
1735 eightBall->isPocketed = false;
1736 // Place 8-ball on foot spot (approx RACK_POS_X) or center if occupied
1737 eightBall->x = RACK_POS_X;
1738 eightBall->y = RACK_POS_Y;
1739 eightBall->vx = eightBall->vy = 0;
1740 // Check overlap and nudge if necessary (simplified)
1741 }
1742 // Apply foul rules if cue ball was also pocketed
1743 if (cueBallPocketed) {
1744 foulCommitted = true;
1745 // Don't switch turns on break scratch + 8ball pocket? Rules vary.
1746 // Let's make it a foul, switch turns, ball in hand.
1747 SwitchTurns();
1748 RespawnCueBall(false); // Ball in hand for opponent
1749 }
1750 else {
1751 // Just respawned 8ball, continue turn or switch based on other balls pocketed.
1752 // Let ProcessShotResults handle turn logic based on other pocketed balls.
1753 }
1754 // Prevent immediate game over message by returning here
1755 return;
1756 }
1757
1758
1759}
1760
1761
1762void SwitchTurns() {
1763 currentPlayer = (currentPlayer == 1) ? 2 : 1;
1764 // Reset aiming state for the new player
1765 isAiming = false;
1766 shotPower = 0;
1767 // Reset foul flag before new turn *really* starts (AI might take over)
1768 // Foul flag is mainly for display, gets cleared before human/AI shot
1769 // foulCommitted = false; // Probably better to clear before ApplyShot
1770
1771 // Set the correct state based on who's turn it is
1772 if (currentPlayer == 1) {
1773 currentGameState = PLAYER1_TURN;
1774 aiTurnPending = false; // Ensure AI flag is off for P1
1775 }
1776 else { // Player 2's turn
1777 if (isPlayer2AI) {
1778 currentGameState = PLAYER2_TURN; // State indicates it's P2's turn
1779 aiTurnPending = true; // Set flag for GameUpdate to trigger AI
1780 // AI will handle Ball-in-Hand logic if necessary within its decision making
1781 }
1782 else {
1783 currentGameState = PLAYER2_TURN; // Human P2
1784 aiTurnPending = false;
1785 }
1786 }
1787}
1788
1789// --- Helper Functions ---
1790
1791Ball* GetBallById(int id) {
1792 for (size_t i = 0; i < balls.size(); ++i) {
1793 if (balls[i].id == id) {
1794 return &balls[i];
1795 }
1796 }
1797 return nullptr;
1798}
1799
1800Ball* GetCueBall() {
1801 return GetBallById(0);
1802}
1803
1804float GetDistance(float x1, float y1, float x2, float y2) {
1805 return sqrtf(GetDistanceSq(x1, y1, x2, y2));
1806}
1807
1808float GetDistanceSq(float x1, float y1, float x2, float y2) {
1809 float dx = x2 - x1;
1810 float dy = y2 - y1;
1811 return dx * dx + dy * dy;
1812}
1813
1814bool IsValidCueBallPosition(float x, float y, bool checkHeadstring) {
1815 // Basic bounds check (inside cushions)
1816 float left = TABLE_LEFT + CUSHION_THICKNESS + BALL_RADIUS;
1817 float right = TABLE_RIGHT - CUSHION_THICKNESS - BALL_RADIUS;
1818 float top = TABLE_TOP + CUSHION_THICKNESS + BALL_RADIUS;
1819 float bottom = TABLE_BOTTOM - CUSHION_THICKNESS - BALL_RADIUS;
1820
1821 if (x < left || x > right || y < top || y > bottom) {
1822 return false;
1823 }
1824
1825 // Check headstring restriction if needed
1826 if (checkHeadstring && x >= HEADSTRING_X) {
1827 return false;
1828 }
1829
1830 // Check overlap with other balls
1831 for (size_t i = 0; i < balls.size(); ++i) {
1832 if (balls[i].id != 0 && !balls[i].isPocketed) { // Don't check against itself or pocketed balls
1833 if (GetDistanceSq(x, y, balls[i].x, balls[i].y) < (BALL_RADIUS * 2.0f) * (BALL_RADIUS * 2.0f)) {
1834 return false; // Overlapping another ball
1835 }
1836 }
1837 }
1838
1839 return true;
1840}
1841
1842
1843template <typename T>
1844void SafeRelease(T** ppT) {
1845 if (*ppT) {
1846 (*ppT)->Release();
1847 *ppT = nullptr;
1848 }
1849}
1850
1851// --- Helper Function for Line Segment Intersection ---
1852// Finds intersection point of line segment P1->P2 and line segment P3->P4
1853// Returns true if they intersect, false otherwise. Stores intersection point in 'intersection'.
1854bool LineSegmentIntersection(D2D1_POINT_2F p1, D2D1_POINT_2F p2, D2D1_POINT_2F p3, D2D1_POINT_2F p4, D2D1_POINT_2F& intersection)
1855{
1856 float denominator = (p4.y - p3.y) * (p2.x - p1.x) - (p4.x - p3.x) * (p2.y - p1.y);
1857
1858 // Check if lines are parallel or collinear
1859 if (fabs(denominator) < 1e-6) {
1860 return false;
1861 }
1862
1863 float ua = ((p4.x - p3.x) * (p1.y - p3.y) - (p4.y - p3.y) * (p1.x - p3.x)) / denominator;
1864 float ub = ((p2.x - p1.x) * (p1.y - p3.y) - (p2.y - p1.y) * (p1.x - p3.x)) / denominator;
1865
1866 // Check if intersection point lies on both segments
1867 if (ua >= 0.0f && ua <= 1.0f && ub >= 0.0f && ub <= 1.0f) {
1868 intersection.x = p1.x + ua * (p2.x - p1.x);
1869 intersection.y = p1.y + ua * (p2.y - p1.y);
1870 return true;
1871 }
1872
1873 return false;
1874}
1875
1876// --- INSERT NEW HELPER FUNCTION HERE ---
1877// Calculates the squared distance from point P to the line segment AB.
1878float PointToLineSegmentDistanceSq(D2D1_POINT_2F p, D2D1_POINT_2F a, D2D1_POINT_2F b) {
1879 float l2 = GetDistanceSq(a.x, a.y, b.x, b.y);
1880 if (l2 == 0.0f) return GetDistanceSq(p.x, p.y, a.x, a.y); // Segment is a point
1881 // Consider P projecting onto the line AB infinite line
1882 // t = [(P-A) . (B-A)] / |B-A|^2
1883 float t = ((p.x - a.x) * (b.x - a.x) + (p.y - a.y) * (b.y - a.y)) / l2;
1884 t = std::max(0.0f, std::min(1.0f, t)); // Clamp t to the segment [0, 1]
1885 // Projection falls on the segment
1886 D2D1_POINT_2F projection = D2D1::Point2F(a.x + t * (b.x - a.x), a.y + t * (b.y - a.y));
1887 return GetDistanceSq(p.x, p.y, projection.x, projection.y);
1888}
1889// --- End New Helper ---
1890
1891// --- NEW AI Implementation Functions ---
1892
1893// Main entry point for AI turn
1894void AIMakeDecision() {
1895 Ball* cueBall = GetCueBall();
1896 if (!cueBall || !isPlayer2AI || currentPlayer != 2) return;
1897
1898 // Handle Ball-in-Hand placement first if necessary
1899 if (currentGameState == BALL_IN_HAND_P2 || currentGameState == PRE_BREAK_PLACEMENT) {
1900 AIPlaceCueBall();
1901 currentGameState = (player1Info.assignedType == BallType::NONE) ? BREAKING : PLAYER2_TURN;
1902 }
1903
1904 AIShotInfo bestShot = AIFindBestShot();
1905
1906 if (bestShot.possible) {
1907 // --- ACTION: Reset foul flags BEFORE AI applies shot ---
1908 firstHitBallIdThisShot = -1;
1909 cueHitObjectBallThisShot = false;
1910 railHitAfterContact = false;
1911 // --- End Reset ---
1912
1913 // Play sound & Apply Shot (Keep this code)
1914 std::thread([](const TCHAR* soundName) { PlaySound(soundName, NULL, SND_FILENAME | SND_NODEFAULT); }, TEXT("cue.wav")).detach();
1915 ApplyShot(bestShot.power, bestShot.angle, 0.0f, 0.0f); // AI doesn't use spin yet
1916
1917 currentGameState = SHOT_IN_PROGRESS;
1918 foulCommitted = false; // Reset display flag
1919 pocketedThisTurn.clear();
1920 }
1921 else {
1922 // --- ACTION: Reset foul flags even for safety shot ---
1923 firstHitBallIdThisShot = -1;
1924 cueHitObjectBallThisShot = false;
1925 railHitAfterContact = false;
1926 // --- End Reset ---
1927
1928 // AI couldn't find a shot - Safety tap
1929 std::thread([](const TCHAR* soundName) { PlaySound(soundName, NULL, SND_FILENAME | SND_NODEFAULT); }, TEXT("cue.wav")).detach();
1930 ApplyShot(MAX_SHOT_POWER * 0.1f, 0.0f, 0.0f, 0.0f);
1931 currentGameState = SHOT_IN_PROGRESS;
1932 foulCommitted = false;
1933 pocketedThisTurn.clear();
1934 }
1935 aiTurnPending = false;
1936}
1937
1938// AI logic for placing cue ball during ball-in-hand
1939void AIPlaceCueBall() {
1940 Ball* cueBall = GetCueBall();
1941 if (!cueBall) return;
1942
1943 // Simple Strategy: Find the easiest possible shot for the AI's ball type
1944 // Place the cue ball directly behind that target ball, aiming straight at a pocket.
1945 // (More advanced: find spot offering multiple options or safety)
1946
1947 AIShotInfo bestPlacementShot = { false };
1948 D2D1_POINT_2F bestPlacePos = D2D1::Point2F(HEADSTRING_X * 0.5f, RACK_POS_Y); // Default placement
1949
1950 BallType targetType = player2Info.assignedType;
1951 bool canTargetAnyPlacement = false; // Local scope variable for placement logic
1952 if (targetType == BallType::NONE) {
1953 canTargetAnyPlacement = true;
1954 }
1955 bool target8Ball = (!canTargetAnyPlacement && targetType != BallType::NONE && player2Info.ballsPocketedCount >= 7);
1956 if (target8Ball) targetType = BallType::EIGHT_BALL;
1957
1958
1959 for (auto& targetBall : balls) {
1960 if (targetBall.isPocketed || targetBall.id == 0) continue;
1961
1962 // Determine if current ball is a valid target for placement consideration
1963 bool currentBallIsValidTarget = false;
1964 if (target8Ball && targetBall.id == 8) currentBallIsValidTarget = true;
1965 else if (canTargetAnyPlacement && targetBall.id != 8) currentBallIsValidTarget = true;
1966 else if (!canTargetAnyPlacement && !target8Ball && targetBall.type == targetType) currentBallIsValidTarget = true;
1967
1968 if (!currentBallIsValidTarget) continue; // Skip if not a valid target
1969
1970 for (int p = 0; p < 6; ++p) {
1971 // Calculate ideal cue ball position: straight line behind target ball aiming at pocket p
1972 float targetToPocketX = pocketPositions[p].x - targetBall.x;
1973 float targetToPocketY = pocketPositions[p].y - targetBall.y;
1974 float dist = sqrtf(targetToPocketX * targetToPocketX + targetToPocketY * targetToPocketY);
1975 if (dist < 1.0f) continue; // Avoid division by zero
1976
1977 float idealAngle = atan2f(targetToPocketY, targetToPocketX);
1978 // Place cue ball slightly behind target ball along this line
1979 float placeDist = BALL_RADIUS * 3.0f; // Place a bit behind
1980 D2D1_POINT_2F potentialPlacePos = D2D1::Point2F( // Use factory function
1981 targetBall.x - cosf(idealAngle) * placeDist,
1982 targetBall.y - sinf(idealAngle) * placeDist
1983 );
1984
1985 // Check if this placement is valid (on table, behind headstring if break, not overlapping)
1986 bool behindHeadstringRule = (currentGameState == PRE_BREAK_PLACEMENT);
1987 if (IsValidCueBallPosition(potentialPlacePos.x, potentialPlacePos.y, behindHeadstringRule)) {
1988 // Is path from potentialPlacePos to targetBall clear?
1989 // Use D2D1::Point2F() factory function here
1990 if (IsPathClear(potentialPlacePos, D2D1::Point2F(targetBall.x, targetBall.y), 0, targetBall.id)) {
1991 // Is path from targetBall to pocket clear?
1992 // Use D2D1::Point2F() factory function here
1993 if (IsPathClear(D2D1::Point2F(targetBall.x, targetBall.y), pocketPositions[p], targetBall.id, -1)) {
1994 // This seems like a good potential placement. Score it?
1995 // Easy AI: Just take the first valid one found.
1996 bestPlacePos = potentialPlacePos;
1997 goto placement_found; // Use goto for simplicity in non-OOP structure
1998 }
1999 }
2000 }
2001 }
2002 }
2003
2004placement_found:
2005 // Place the cue ball at the best found position (or default if none found)
2006 cueBall->x = bestPlacePos.x;
2007 cueBall->y = bestPlacePos.y;
2008 cueBall->vx = 0;
2009 cueBall->vy = 0;
2010}
2011
2012
2013// AI finds the best shot available on the table
2014AIShotInfo AIFindBestShot() {
2015 AIShotInfo bestShotOverall = { false };
2016 Ball* cueBall = GetCueBall();
2017 if (!cueBall) return bestShotOverall;
2018
2019 // Determine target ball type for AI (Player 2)
2020 BallType targetType = player2Info.assignedType;
2021 bool canTargetAny = false; // Can AI hit any ball (e.g., after break, before assignment)?
2022 if (targetType == BallType::NONE) {
2023 // If colors not assigned, AI aims to pocket *something* (usually lowest numbered ball legally)
2024 // Or, more simply, treat any ball as a potential target to make *a* pocket
2025 canTargetAny = true; // Simplification: allow targeting any non-8 ball.
2026 // A better rule is hit lowest numbered ball first on break follow-up.
2027 }
2028
2029 // Check if AI needs to shoot the 8-ball
2030 bool target8Ball = (!canTargetAny && targetType != BallType::NONE && player2Info.ballsPocketedCount >= 7);
2031
2032
2033 // Iterate through all potential target balls
2034 for (auto& potentialTarget : balls) {
2035 if (potentialTarget.isPocketed || potentialTarget.id == 0) continue; // Skip pocketed and cue ball
2036
2037 // Check if this ball is a valid target
2038 bool isValidTarget = false;
2039 if (target8Ball) {
2040 isValidTarget = (potentialTarget.id == 8);
2041 }
2042 else if (canTargetAny) {
2043 isValidTarget = (potentialTarget.id != 8); // Can hit any non-8 ball
2044 }
2045 else { // Colors assigned, not yet shooting 8-ball
2046 isValidTarget = (potentialTarget.type == targetType);
2047 }
2048
2049 if (!isValidTarget) continue; // Skip if not a valid target for this turn
2050
2051 // Now, check all pockets for this target ball
2052 for (int p = 0; p < 6; ++p) {
2053 AIShotInfo currentShot = EvaluateShot(&potentialTarget, p);
2054 currentShot.involves8Ball = (potentialTarget.id == 8);
2055
2056 if (currentShot.possible) {
2057 // Compare scores to find the best shot
2058 if (!bestShotOverall.possible || currentShot.score > bestShotOverall.score) {
2059 bestShotOverall = currentShot;
2060 }
2061 }
2062 }
2063 } // End loop through potential target balls
2064
2065 // If targeting 8-ball and no shot found, or targeting own balls and no shot found,
2066 // need a safety strategy. Current simple AI just takes best found or taps cue ball.
2067
2068 return bestShotOverall;
2069}
2070
2071
2072// Evaluate a potential shot at a specific target ball towards a specific pocket
2073AIShotInfo EvaluateShot(Ball* targetBall, int pocketIndex) {
2074 AIShotInfo shotInfo;
2075 shotInfo.possible = false; // Assume not possible initially
2076 shotInfo.targetBall = targetBall;
2077 shotInfo.pocketIndex = pocketIndex;
2078
2079 Ball* cueBall = GetCueBall();
2080 if (!cueBall || !targetBall) return shotInfo;
2081
2082 // --- Define local state variables needed for legality checks ---
2083 BallType aiAssignedType = player2Info.assignedType;
2084 bool canTargetAny = (aiAssignedType == BallType::NONE); // Can AI hit any ball?
2085 bool mustTarget8Ball = (!canTargetAny && aiAssignedType != BallType::NONE && player2Info.ballsPocketedCount >= 7);
2086 // ---
2087
2088 // 1. Calculate Ghost Ball position
2089 shotInfo.ghostBallPos = CalculateGhostBallPos(targetBall, pocketIndex);
2090
2091 // 2. Calculate Angle from Cue Ball to Ghost Ball
2092 float dx = shotInfo.ghostBallPos.x - cueBall->x;
2093 float dy = shotInfo.ghostBallPos.y - cueBall->y;
2094 if (fabs(dx) < 0.01f && fabs(dy) < 0.01f) return shotInfo; // Avoid aiming at same spot
2095 shotInfo.angle = atan2f(dy, dx);
2096
2097 // Basic angle validity check (optional)
2098 if (!IsValidAIAimAngle(shotInfo.angle)) {
2099 // Maybe log this or handle edge cases
2100 }
2101
2102 // 3. Check Path: Cue Ball -> Ghost Ball Position
2103 // Use D2D1::Point2F() factory function here
2104 if (!IsPathClear(D2D1::Point2F(cueBall->x, cueBall->y), shotInfo.ghostBallPos, cueBall->id, targetBall->id)) {
2105 return shotInfo; // Path blocked
2106 }
2107
2108 // 4. Check Path: Target Ball -> Pocket
2109 // Use D2D1::Point2F() factory function here
2110 if (!IsPathClear(D2D1::Point2F(targetBall->x, targetBall->y), pocketPositions[pocketIndex], targetBall->id, -1)) {
2111 return shotInfo; // Path blocked
2112 }
2113
2114 // 5. Check First Ball Hit Legality
2115 float firstHitDistSq = -1.0f;
2116 // Use D2D1::Point2F() factory function here
2117 Ball* firstHit = FindFirstHitBall(D2D1::Point2F(cueBall->x, cueBall->y), shotInfo.angle, firstHitDistSq);
2118
2119 if (!firstHit) {
2120 return shotInfo; // AI aims but doesn't hit anything? Impossible shot.
2121 }
2122
2123 // Check if the first ball hit is the intended target ball
2124 if (firstHit->id != targetBall->id) {
2125 // Allow hitting slightly off target if it's very close to ghost ball pos
2126 float ghostDistSq = GetDistanceSq(shotInfo.ghostBallPos.x, shotInfo.ghostBallPos.y, firstHit->x, firstHit->y);
2127 // Allow a tolerance roughly half the ball radius squared
2128 if (ghostDistSq > (BALL_RADIUS * 0.7f) * (BALL_RADIUS * 0.7f)) {
2129 // First hit is significantly different from the target point.
2130 // This shot path leads to hitting the wrong ball first.
2131 return shotInfo; // Foul or unintended shot
2132 }
2133 // If first hit is not target, but very close, allow it for now (might still be foul based on type).
2134 }
2135
2136 // Check legality of the *first ball actually hit* based on game rules
2137 if (!canTargetAny) { // Colors are assigned (or should be)
2138 if (mustTarget8Ball) { // Must hit 8-ball first
2139 if (firstHit->id != 8) {
2140 // return shotInfo; // FOUL - Hitting wrong ball when aiming for 8-ball
2141 // Keep shot possible for now, rely on AIFindBestShot to prioritize legal ones
2142 }
2143 }
2144 else { // Must hit own ball type first
2145 if (firstHit->type != aiAssignedType && firstHit->id != 8) { // Allow hitting 8-ball if own type blocked? No, standard rules usually require hitting own first.
2146 // return shotInfo; // FOUL - Hitting opponent ball or 8-ball when shouldn't
2147 // Keep shot possible for now, rely on AIFindBestShot to prioritize legal ones
2148 }
2149 else if (firstHit->id == 8) {
2150 // return shotInfo; // FOUL - Hitting 8-ball when shouldn't
2151 // Keep shot possible for now
2152 }
2153 }
2154 }
2155 // (If canTargetAny is true, hitting any ball except 8 first is legal - assuming not scratching)
2156
2157
2158 // 6. Calculate Score & Power (Difficulty affects this)
2159 shotInfo.possible = true; // If we got here, the shot is geometrically possible and likely legal enough for AI to consider
2160
2161 float cueToGhostDist = GetDistance(cueBall->x, cueBall->y, shotInfo.ghostBallPos.x, shotInfo.ghostBallPos.y);
2162 float targetToPocketDist = GetDistance(targetBall->x, targetBall->y, pocketPositions[pocketIndex].x, pocketPositions[pocketIndex].y);
2163
2164 // Simple Score: Shorter shots are better, straighter shots are slightly better.
2165 float distanceScore = 1000.0f / (1.0f + cueToGhostDist + targetToPocketDist);
2166
2167 // Angle Score: Calculate cut angle
2168 // Vector Cue -> Ghost
2169 float v1x = shotInfo.ghostBallPos.x - cueBall->x;
2170 float v1y = shotInfo.ghostBallPos.y - cueBall->y;
2171 // Vector Target -> Pocket
2172 float v2x = pocketPositions[pocketIndex].x - targetBall->x;
2173 float v2y = pocketPositions[pocketIndex].y - targetBall->y;
2174 // Normalize vectors
2175 float mag1 = sqrtf(v1x * v1x + v1y * v1y);
2176 float mag2 = sqrtf(v2x * v2x + v2y * v2y);
2177 float angleScoreFactor = 0.5f; // Default if vectors are zero len
2178 if (mag1 > 0.1f && mag2 > 0.1f) {
2179 v1x /= mag1; v1y /= mag1;
2180 v2x /= mag2; v2y /= mag2;
2181 // Dot product gives cosine of angle between cue ball path and target ball path
2182 float dotProduct = v1x * v2x + v1y * v2y;
2183 // Straighter shot (dot product closer to 1) gets higher score
2184 angleScoreFactor = (1.0f + dotProduct) / 2.0f; // Map [-1, 1] to [0, 1]
2185 }
2186 angleScoreFactor = std::max(0.1f, angleScoreFactor); // Ensure some minimum score factor
2187
2188 shotInfo.score = distanceScore * angleScoreFactor;
2189
2190 // Bonus for pocketing 8-ball legally
2191 if (mustTarget8Ball && targetBall->id == 8) {
2192 shotInfo.score *= 10.0; // Strongly prefer the winning shot
2193 }
2194
2195 // Penalty for difficult cuts? Already partially handled by angleScoreFactor.
2196
2197 // 7. Calculate Power
2198 shotInfo.power = CalculateShotPower(cueToGhostDist, targetToPocketDist);
2199
2200 // 8. Add Inaccuracy based on Difficulty (same as before)
2201 float angleError = 0.0f;
2202 float powerErrorFactor = 1.0f;
2203
2204 switch (aiDifficulty) {
2205 case EASY:
2206 angleError = (float)(rand() % 100 - 50) / 1000.0f; // +/- ~3 deg
2207 powerErrorFactor = 0.8f + (float)(rand() % 40) / 100.0f; // 80-120%
2208 shotInfo.power *= 0.8f;
2209 break;
2210 case MEDIUM:
2211 angleError = (float)(rand() % 60 - 30) / 1000.0f; // +/- ~1.7 deg
2212 powerErrorFactor = 0.9f + (float)(rand() % 20) / 100.0f; // 90-110%
2213 break;
2214 case HARD:
2215 angleError = (float)(rand() % 10 - 5) / 1000.0f; // +/- ~0.3 deg
2216 powerErrorFactor = 0.98f + (float)(rand() % 4) / 100.0f; // 98-102%
2217 break;
2218 }
2219 shotInfo.angle += angleError;
2220 shotInfo.power *= powerErrorFactor;
2221 shotInfo.power = std::max(1.0f, std::min(shotInfo.power, MAX_SHOT_POWER)); // Clamp power
2222
2223 return shotInfo;
2224}
2225
2226
2227// Calculates required power (simplified)
2228float CalculateShotPower(float cueToGhostDist, float targetToPocketDist) {
2229 // Basic model: Power needed increases with total distance the balls need to travel.
2230 // Need enough power for cue ball to reach target AND target to reach pocket.
2231 float totalDist = cueToGhostDist + targetToPocketDist;
2232
2233 // Map distance to power (needs tuning)
2234 // Let's say max power is needed for longest possible shot (e.g., corner to corner ~ 1000 units)
2235 float powerRatio = std::min(1.0f, totalDist / 800.0f); // Normalize based on estimated max distance
2236
2237 float basePower = MAX_SHOT_POWER * 0.2f; // Minimum power to move balls reliably
2238 float variablePower = (MAX_SHOT_POWER * 0.8f) * powerRatio; // Scale remaining power range
2239
2240 // Harder AI could adjust based on desired cue ball travel (more power for draw/follow)
2241 return std::min(MAX_SHOT_POWER, basePower + variablePower);
2242}
2243
2244// Calculate the position the cue ball needs to hit for the target ball to go towards the pocket
2245D2D1_POINT_2F CalculateGhostBallPos(Ball* targetBall, int pocketIndex) {
2246 float targetToPocketX = pocketPositions[pocketIndex].x - targetBall->x;
2247 float targetToPocketY = pocketPositions[pocketIndex].y - targetBall->y;
2248 float dist = sqrtf(targetToPocketX * targetToPocketX + targetToPocketY * targetToPocketY);
2249
2250 if (dist < 1.0f) { // Target is basically in the pocket
2251 // Aim slightly off-center to avoid weird physics? Or directly at center?
2252 // For simplicity, return a point slightly behind center along the reverse line.
2253 return D2D1::Point2F(targetBall->x - targetToPocketX * 0.1f, targetBall->y - targetToPocketY * 0.1f);
2254 }
2255
2256 // Normalize direction vector from target to pocket
2257 float nx = targetToPocketX / dist;
2258 float ny = targetToPocketY / dist;
2259
2260 // Ghost ball position is diameter distance *behind* the target ball along this line
2261 float ghostX = targetBall->x - nx * (BALL_RADIUS * 2.0f);
2262 float ghostY = targetBall->y - ny * (BALL_RADIUS * 2.0f);
2263
2264 return D2D1::Point2F(ghostX, ghostY);
2265}
2266
2267// Checks if line segment is clear of obstructing balls
2268bool IsPathClear(D2D1_POINT_2F start, D2D1_POINT_2F end, int ignoredBallId1, int ignoredBallId2) {
2269 float dx = end.x - start.x;
2270 float dy = end.y - start.y;
2271 float segmentLenSq = dx * dx + dy * dy;
2272
2273 if (segmentLenSq < 0.01f) return true; // Start and end are same point
2274
2275 for (const auto& ball : balls) {
2276 if (ball.isPocketed) continue;
2277 if (ball.id == ignoredBallId1) continue;
2278 if (ball.id == ignoredBallId2) continue;
2279
2280 // Check distance from ball center to the line segment
2281 float ballToStartX = ball.x - start.x;
2282 float ballToStartY = ball.y - start.y;
2283
2284 // Project ball center onto the line defined by the segment
2285 float dot = (ballToStartX * dx + ballToStartY * dy) / segmentLenSq;
2286
2287 D2D1_POINT_2F closestPointOnLine;
2288 if (dot < 0) { // Closest point is start point
2289 closestPointOnLine = start;
2290 }
2291 else if (dot > 1) { // Closest point is end point
2292 closestPointOnLine = end;
2293 }
2294 else { // Closest point is along the segment
2295 closestPointOnLine = D2D1::Point2F(start.x + dot * dx, start.y + dot * dy);
2296 }
2297
2298 // Check if the closest point is within collision distance (ball radius + path radius)
2299 if (GetDistanceSq(ball.x, ball.y, closestPointOnLine.x, closestPointOnLine.y) < (BALL_RADIUS * BALL_RADIUS)) {
2300 // Consider slightly wider path check? Maybe BALL_RADIUS * 1.1f?
2301 // if (GetDistanceSq(ball.x, ball.y, closestPointOnLine.x, closestPointOnLine.y) < (BALL_RADIUS * 1.1f)*(BALL_RADIUS*1.1f)) {
2302 return false; // Path is blocked
2303 }
2304 }
2305 return true; // No obstructions found
2306}
2307
2308// Finds the first ball hit along a path (simplified)
2309Ball* FindFirstHitBall(D2D1_POINT_2F start, float angle, float& hitDistSq) {
2310 Ball* hitBall = nullptr;
2311 hitDistSq = -1.0f; // Initialize hit distance squared
2312 float minCollisionDistSq = -1.0f;
2313
2314 float cosA = cosf(angle);
2315 float sinA = sinf(angle);
2316
2317 for (auto& ball : balls) {
2318 if (ball.isPocketed || ball.id == 0) continue; // Skip cue ball and pocketed
2319
2320 float dx = ball.x - start.x;
2321 float dy = ball.y - start.y;
2322
2323 // Project vector from start->ball onto the aim direction vector
2324 float dot = dx * cosA + dy * sinA;
2325
2326 if (dot > 0) { // Ball is generally in front
2327 // Find closest point on aim line to the ball's center
2328 float closestPointX = start.x + dot * cosA;
2329 float closestPointY = start.y + dot * sinA;
2330 float distSq = GetDistanceSq(ball.x, ball.y, closestPointX, closestPointY);
2331
2332 // Check if the aim line passes within the ball's radius
2333 if (distSq < (BALL_RADIUS * BALL_RADIUS)) {
2334 // Calculate distance from start to the collision point on the ball's circumference
2335 float backDist = sqrtf(std::max(0.f, BALL_RADIUS * BALL_RADIUS - distSq));
2336 float collisionDist = dot - backDist; // Distance along aim line to collision
2337
2338 if (collisionDist > 0) { // Ensure collision is in front
2339 float collisionDistSq = collisionDist * collisionDist;
2340 if (hitBall == nullptr || collisionDistSq < minCollisionDistSq) {
2341 minCollisionDistSq = collisionDistSq;
2342 hitBall = &ball; // Found a closer hit ball
2343 }
2344 }
2345 }
2346 }
2347 }
2348 hitDistSq = minCollisionDistSq; // Return distance squared to the first hit
2349 return hitBall;
2350}
2351
2352// Basic check for reasonable AI aim angles (optional)
2353bool IsValidAIAimAngle(float angle) {
2354 // Placeholder - could check for NaN or infinity if calculations go wrong
2355 return isfinite(angle);
2356}
2357
2358// --- Drawing Functions ---
2359
2360void OnPaint() {
2361 HRESULT hr = CreateDeviceResources(); // Ensure resources are valid
2362
2363 if (SUCCEEDED(hr)) {
2364 pRenderTarget->BeginDraw();
2365 DrawScene(pRenderTarget); // Pass render target
2366 hr = pRenderTarget->EndDraw();
2367
2368 if (hr == D2DERR_RECREATE_TARGET) {
2369 DiscardDeviceResources();
2370 // Optionally request another paint message: InvalidateRect(hwndMain, NULL, FALSE);
2371 // But the timer loop will trigger redraw anyway.
2372 }
2373 }
2374 // If CreateDeviceResources failed, EndDraw might not be called.
2375 // Consider handling this more robustly if needed.
2376}
2377
2378void DrawScene(ID2D1RenderTarget* pRT) {
2379 if (!pRT) return;
2380
2381 //pRT->Clear(D2D1::ColorF(D2D1::ColorF::LightGray)); // Background color
2382 // Set background color to #ffffcd (RGB: 255, 255, 205)
2383 pRT->Clear(D2D1::ColorF(1.0f, 1.0f, 0.803f)); // Clear with light yellow background
2384
2385 DrawTable(pRT);
2386 DrawBalls(pRT);
2387 DrawAimingAids(pRT); // Includes cue stick if aiming
2388 DrawUI(pRT);
2389 DrawPowerMeter(pRT);
2390 DrawSpinIndicator(pRT);
2391 DrawPocketedBallsIndicator(pRT);
2392 DrawBallInHandIndicator(pRT); // Draw cue ball ghost if placing
2393
2394 // Draw Game Over Message
2395 if (currentGameState == GAME_OVER && pTextFormat) {
2396 ID2D1SolidColorBrush* pBrush = nullptr;
2397 pRT->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::White), &pBrush);
2398 if (pBrush) {
2399 D2D1_RECT_F layoutRect = D2D1::RectF(TABLE_LEFT, TABLE_TOP + TABLE_HEIGHT / 2 - 30, TABLE_RIGHT, TABLE_TOP + TABLE_HEIGHT / 2 + 30);
2400 pRT->DrawText(
2401 gameOverMessage.c_str(),
2402 (UINT32)gameOverMessage.length(),
2403 pTextFormat, // Use large format maybe?
2404 &layoutRect,
2405 pBrush
2406 );
2407 SafeRelease(&pBrush);
2408 }
2409 }
2410
2411}
2412
2413void DrawTable(ID2D1RenderTarget* pRT) {
2414 ID2D1SolidColorBrush* pBrush = nullptr;
2415
2416 // === Draw Full Orange Frame (Table Border) ===
2417 ID2D1SolidColorBrush* pFrameBrush = nullptr;
2418 pRT->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::Orange), &pFrameBrush);
2419 if (pFrameBrush) {
2420 D2D1_RECT_F outerRect = D2D1::RectF(
2421 TABLE_LEFT - CUSHION_THICKNESS,
2422 TABLE_TOP - CUSHION_THICKNESS,
2423 TABLE_RIGHT + CUSHION_THICKNESS,
2424 TABLE_BOTTOM + CUSHION_THICKNESS
2425 );
2426 pRT->FillRectangle(&outerRect, pFrameBrush);
2427 SafeRelease(&pFrameBrush);
2428 }
2429
2430 // Draw Table Bed (Green Felt)
2431 pRT->CreateSolidColorBrush(TABLE_COLOR, &pBrush);
2432 if (!pBrush) return;
2433 D2D1_RECT_F tableRect = D2D1::RectF(TABLE_LEFT, TABLE_TOP, TABLE_RIGHT, TABLE_BOTTOM);
2434 pRT->FillRectangle(&tableRect, pBrush);
2435 SafeRelease(&pBrush);
2436
2437 // Draw Cushions (Red Border)
2438 pRT->CreateSolidColorBrush(CUSHION_COLOR, &pBrush);
2439 if (!pBrush) return;
2440 // Top Cushion (split by middle pocket)
2441 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);
2442 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);
2443 // Bottom Cushion (split by middle pocket)
2444 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);
2445 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);
2446 // Left Cushion
2447 pRT->FillRectangle(D2D1::RectF(TABLE_LEFT - CUSHION_THICKNESS, TABLE_TOP + HOLE_VISUAL_RADIUS, TABLE_LEFT, TABLE_BOTTOM - HOLE_VISUAL_RADIUS), pBrush);
2448 // Right Cushion
2449 pRT->FillRectangle(D2D1::RectF(TABLE_RIGHT, TABLE_TOP + HOLE_VISUAL_RADIUS, TABLE_RIGHT + CUSHION_THICKNESS, TABLE_BOTTOM - HOLE_VISUAL_RADIUS), pBrush);
2450 SafeRelease(&pBrush);
2451
2452
2453 // Draw Pockets (Black Circles)
2454 pRT->CreateSolidColorBrush(POCKET_COLOR, &pBrush);
2455 if (!pBrush) return;
2456 for (int i = 0; i < 6; ++i) {
2457 D2D1_ELLIPSE ellipse = D2D1::Ellipse(pocketPositions[i], HOLE_VISUAL_RADIUS, HOLE_VISUAL_RADIUS);
2458 pRT->FillEllipse(&ellipse, pBrush);
2459 }
2460 SafeRelease(&pBrush);
2461
2462 // Draw Headstring Line (White)
2463 pRT->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::White, 0.5f), &pBrush);
2464 if (!pBrush) return;
2465 pRT->DrawLine(
2466 D2D1::Point2F(HEADSTRING_X, TABLE_TOP),
2467 D2D1::Point2F(HEADSTRING_X, TABLE_BOTTOM),
2468 pBrush,
2469 1.0f // Line thickness
2470 );
2471 SafeRelease(&pBrush);
2472}
2473
2474
2475void DrawBalls(ID2D1RenderTarget* pRT) {
2476 ID2D1SolidColorBrush* pBrush = nullptr;
2477 ID2D1SolidColorBrush* pStripeBrush = nullptr; // For stripe pattern
2478
2479 pRT->CreateSolidColorBrush(D2D1::ColorF(0, 0, 0), &pBrush); // Placeholder
2480 pRT->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::White), &pStripeBrush);
2481
2482 if (!pBrush || !pStripeBrush) {
2483 SafeRelease(&pBrush);
2484 SafeRelease(&pStripeBrush);
2485 return;
2486 }
2487
2488
2489 for (size_t i = 0; i < balls.size(); ++i) {
2490 const Ball& b = balls[i];
2491 if (!b.isPocketed) {
2492 D2D1_ELLIPSE ellipse = D2D1::Ellipse(D2D1::Point2F(b.x, b.y), BALL_RADIUS, BALL_RADIUS);
2493
2494 // Set main ball color
2495 pBrush->SetColor(b.color);
2496 pRT->FillEllipse(&ellipse, pBrush);
2497
2498 // Draw Stripe if applicable
2499 if (b.type == BallType::STRIPE) {
2500 // Draw a white band across the middle (simplified stripe)
2501 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);
2502 // Need to clip this rectangle to the ellipse bounds - complex!
2503 // Alternative: Draw two colored arcs leaving a white band.
2504 // Simplest: Draw a white circle inside, slightly smaller.
2505 D2D1_ELLIPSE innerEllipse = D2D1::Ellipse(D2D1::Point2F(b.x, b.y), BALL_RADIUS * 0.6f, BALL_RADIUS * 0.6f);
2506 pRT->FillEllipse(innerEllipse, pStripeBrush); // White center part
2507 pBrush->SetColor(b.color); // Set back to stripe color
2508 pRT->FillEllipse(innerEllipse, pBrush); // Fill again, leaving a ring - No, this isn't right.
2509
2510 // Let's try drawing a thick white line across
2511 // This doesn't look great. Just drawing solid red for stripes for now.
2512 }
2513
2514 // Draw Number (Optional - requires more complex text layout or pre-rendered textures)
2515 // if (b.id != 0 && pTextFormat) {
2516 // std::wstring numStr = std::to_wstring(b.id);
2517 // D2D1_RECT_F textRect = D2D1::RectF(b.x - BALL_RADIUS, b.y - BALL_RADIUS, b.x + BALL_RADIUS, b.y + BALL_RADIUS);
2518 // ID2D1SolidColorBrush* pNumBrush = nullptr;
2519 // D2D1_COLOR_F numCol = (b.type == BallType::SOLID || b.id == 8) ? D2D1::ColorF(D2D1::ColorF::Black) : D2D1::ColorF(D2D1::ColorF::White);
2520 // pRT->CreateSolidColorBrush(numCol, &pNumBrush);
2521 // // Create a smaller text format...
2522 // // pRT->DrawText(numStr.c_str(), numStr.length(), pSmallTextFormat, &textRect, pNumBrush);
2523 // SafeRelease(&pNumBrush);
2524 // }
2525 }
2526 }
2527
2528 SafeRelease(&pBrush);
2529 SafeRelease(&pStripeBrush);
2530}
2531
2532
2533void DrawAimingAids(ID2D1RenderTarget* pRT) {
2534 // Condition check at start (Unchanged)
2535 if (currentGameState != PLAYER1_TURN && currentGameState != PLAYER2_TURN &&
2536 currentGameState != BREAKING && currentGameState != AIMING)
2537 {
2538 return;
2539 }
2540
2541 Ball* cueBall = GetCueBall();
2542 if (!cueBall || cueBall->isPocketed) return; // Don't draw if cue ball is gone
2543
2544 ID2D1SolidColorBrush* pBrush = nullptr;
2545 ID2D1SolidColorBrush* pGhostBrush = nullptr;
2546 ID2D1StrokeStyle* pDashedStyle = nullptr;
2547 ID2D1SolidColorBrush* pCueBrush = nullptr;
2548 ID2D1SolidColorBrush* pReflectBrush = nullptr; // Brush for reflection line
2549
2550 // Ensure render target is valid
2551 if (!pRT) return;
2552
2553 // Create Brushes and Styles (check for failures)
2554 HRESULT hr;
2555 hr = pRT->CreateSolidColorBrush(AIM_LINE_COLOR, &pBrush);
2556 if FAILED(hr) { SafeRelease(&pBrush); return; }
2557 hr = pRT->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::White, 0.5f), &pGhostBrush);
2558 if FAILED(hr) { SafeRelease(&pBrush); SafeRelease(&pGhostBrush); return; }
2559 hr = pRT->CreateSolidColorBrush(D2D1::ColorF(0.6f, 0.4f, 0.2f), &pCueBrush);
2560 if FAILED(hr) { SafeRelease(&pBrush); SafeRelease(&pGhostBrush); SafeRelease(&pCueBrush); return; }
2561 // Create reflection brush (e.g., lighter shade or different color)
2562 hr = pRT->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::LightCyan, 0.6f), &pReflectBrush);
2563 if FAILED(hr) { SafeRelease(&pBrush); SafeRelease(&pGhostBrush); SafeRelease(&pCueBrush); SafeRelease(&pReflectBrush); return; }
2564
2565 if (pFactory) {
2566 D2D1_STROKE_STYLE_PROPERTIES strokeProps = D2D1::StrokeStyleProperties();
2567 strokeProps.dashStyle = D2D1_DASH_STYLE_DASH;
2568 hr = pFactory->CreateStrokeStyle(&strokeProps, nullptr, 0, &pDashedStyle);
2569 if FAILED(hr) { pDashedStyle = nullptr; }
2570 }
2571
2572
2573 // --- Cue Stick Drawing (Unchanged from previous fix) ---
2574 const float baseStickLength = 150.0f;
2575 const float baseStickThickness = 4.0f;
2576 float stickLength = baseStickLength * 1.4f;
2577 float stickThickness = baseStickThickness * 1.5f;
2578 float stickAngle = cueAngle + PI;
2579 float powerOffset = 0.0f;
2580 if (isAiming && (currentGameState == AIMING || currentGameState == BREAKING)) {
2581 powerOffset = shotPower * 5.0f;
2582 }
2583 D2D1_POINT_2F cueStickEnd = D2D1::Point2F(cueBall->x + cosf(stickAngle) * (stickLength + powerOffset), cueBall->y + sinf(stickAngle) * (stickLength + powerOffset));
2584 D2D1_POINT_2F cueStickTip = D2D1::Point2F(cueBall->x + cosf(stickAngle) * (powerOffset + 5.0f), cueBall->y + sinf(stickAngle) * (powerOffset + 5.0f));
2585 pRT->DrawLine(cueStickTip, cueStickEnd, pCueBrush, stickThickness);
2586
2587
2588 // --- Projection Line Calculation ---
2589 float cosA = cosf(cueAngle);
2590 float sinA = sinf(cueAngle);
2591 float rayLength = TABLE_WIDTH + TABLE_HEIGHT; // Ensure ray is long enough
2592 D2D1_POINT_2F rayStart = D2D1::Point2F(cueBall->x, cueBall->y);
2593 D2D1_POINT_2F rayEnd = D2D1::Point2F(rayStart.x + cosA * rayLength, rayStart.y + sinA * rayLength);
2594
2595 // Find the first ball hit by the aiming ray
2596 Ball* hitBall = nullptr;
2597 float firstHitDistSq = -1.0f;
2598 D2D1_POINT_2F ballCollisionPoint = { 0, 0 }; // Point on target ball circumference
2599 D2D1_POINT_2F ghostBallPosForHit = { 0, 0 }; // Ghost ball pos for the hit ball
2600
2601 hitBall = FindFirstHitBall(rayStart, cueAngle, firstHitDistSq);
2602 if (hitBall) {
2603 // Calculate the point on the target ball's circumference
2604 float collisionDist = sqrtf(firstHitDistSq);
2605 ballCollisionPoint = D2D1::Point2F(rayStart.x + cosA * collisionDist, rayStart.y + sinA * collisionDist);
2606 // Calculate ghost ball position for this specific hit (used for projection consistency)
2607 ghostBallPosForHit = D2D1::Point2F(hitBall->x - cosA * BALL_RADIUS, hitBall->y - sinA * BALL_RADIUS); // Approx.
2608 }
2609
2610 // Find the first rail hit by the aiming ray
2611 D2D1_POINT_2F railHitPoint = rayEnd; // Default to far end if no rail hit
2612 float minRailDistSq = rayLength * rayLength;
2613 int hitRailIndex = -1; // 0:Left, 1:Right, 2:Top, 3:Bottom
2614
2615 // Define table edge segments for intersection checks
2616 D2D1_POINT_2F topLeft = D2D1::Point2F(TABLE_LEFT, TABLE_TOP);
2617 D2D1_POINT_2F topRight = D2D1::Point2F(TABLE_RIGHT, TABLE_TOP);
2618 D2D1_POINT_2F bottomLeft = D2D1::Point2F(TABLE_LEFT, TABLE_BOTTOM);
2619 D2D1_POINT_2F bottomRight = D2D1::Point2F(TABLE_RIGHT, TABLE_BOTTOM);
2620
2621 D2D1_POINT_2F currentIntersection;
2622
2623 // Check Left Rail
2624 if (LineSegmentIntersection(rayStart, rayEnd, topLeft, bottomLeft, currentIntersection)) {
2625 float distSq = GetDistanceSq(rayStart.x, rayStart.y, currentIntersection.x, currentIntersection.y);
2626 if (distSq < minRailDistSq) { minRailDistSq = distSq; railHitPoint = currentIntersection; hitRailIndex = 0; }
2627 }
2628 // Check Right Rail
2629 if (LineSegmentIntersection(rayStart, rayEnd, topRight, bottomRight, currentIntersection)) {
2630 float distSq = GetDistanceSq(rayStart.x, rayStart.y, currentIntersection.x, currentIntersection.y);
2631 if (distSq < minRailDistSq) { minRailDistSq = distSq; railHitPoint = currentIntersection; hitRailIndex = 1; }
2632 }
2633 // Check Top Rail
2634 if (LineSegmentIntersection(rayStart, rayEnd, topLeft, topRight, currentIntersection)) {
2635 float distSq = GetDistanceSq(rayStart.x, rayStart.y, currentIntersection.x, currentIntersection.y);
2636 if (distSq < minRailDistSq) { minRailDistSq = distSq; railHitPoint = currentIntersection; hitRailIndex = 2; }
2637 }
2638 // Check Bottom Rail
2639 if (LineSegmentIntersection(rayStart, rayEnd, bottomLeft, bottomRight, currentIntersection)) {
2640 float distSq = GetDistanceSq(rayStart.x, rayStart.y, currentIntersection.x, currentIntersection.y);
2641 if (distSq < minRailDistSq) { minRailDistSq = distSq; railHitPoint = currentIntersection; hitRailIndex = 3; }
2642 }
2643
2644
2645 // --- Determine final aim line end point ---
2646 D2D1_POINT_2F finalLineEnd = railHitPoint; // Assume rail hit first
2647 bool aimingAtRail = true;
2648
2649 if (hitBall && firstHitDistSq < minRailDistSq) {
2650 // Ball collision is closer than rail collision
2651 finalLineEnd = ballCollisionPoint; // End line at the point of contact on the ball
2652 aimingAtRail = false;
2653 }
2654
2655 // --- Draw Primary Aiming Line ---
2656 pRT->DrawLine(rayStart, finalLineEnd, pBrush, 1.0f, pDashedStyle ? pDashedStyle : NULL);
2657
2658 // --- Draw Target Circle/Indicator ---
2659 D2D1_ELLIPSE targetCircle = D2D1::Ellipse(finalLineEnd, BALL_RADIUS / 2.0f, BALL_RADIUS / 2.0f);
2660 pRT->DrawEllipse(&targetCircle, pBrush, 1.0f);
2661
2662 // --- Draw Projection/Reflection Lines ---
2663 if (!aimingAtRail && hitBall) {
2664 // Aiming at a ball: Draw Ghost Cue Ball and Target Ball Projection
2665 D2D1_ELLIPSE ghostCue = D2D1::Ellipse(ballCollisionPoint, BALL_RADIUS, BALL_RADIUS); // Ghost ball at contact point
2666 pRT->DrawEllipse(ghostCue, pGhostBrush, 1.0f, pDashedStyle ? pDashedStyle : NULL);
2667
2668 // Calculate target ball projection based on impact line (cue collision point -> target center)
2669 float targetProjectionAngle = atan2f(hitBall->y - ballCollisionPoint.y, hitBall->x - ballCollisionPoint.x);
2670 // Clamp angle calculation if distance is tiny
2671 if (GetDistanceSq(hitBall->x, hitBall->y, ballCollisionPoint.x, ballCollisionPoint.y) < 1.0f) {
2672 targetProjectionAngle = cueAngle; // Fallback if overlapping
2673 }
2674
2675 D2D1_POINT_2F targetStartPoint = D2D1::Point2F(hitBall->x, hitBall->y);
2676 D2D1_POINT_2F targetProjectionEnd = D2D1::Point2F(
2677 hitBall->x + cosf(targetProjectionAngle) * 50.0f, // Projection length 50 units
2678 hitBall->y + sinf(targetProjectionAngle) * 50.0f
2679 );
2680 // Draw solid line for target projection
2681 pRT->DrawLine(targetStartPoint, targetProjectionEnd, pBrush, 1.0f);
2682
2683 // -- Cue Ball Path after collision (Optional, requires physics) --
2684 // Very simplified: Assume cue deflects, angle depends on cut angle.
2685 // float cutAngle = acosf(cosf(cueAngle - targetProjectionAngle)); // Angle between paths
2686 // float cueDeflectionAngle = ? // Depends on cutAngle, spin, etc. Hard to predict accurately.
2687 // D2D1_POINT_2F cueProjectionEnd = ...
2688 // pRT->DrawLine(ballCollisionPoint, cueProjectionEnd, pGhostBrush, 1.0f, pDashedStyle ? pDashedStyle : NULL);
2689
2690 // --- Accuracy Comment ---
2691 // Note: The visual accuracy of this projection, especially for cut shots (hitting the ball off-center)
2692 // or shots with spin, is limited by the simplified physics model. Real pool physics involves
2693 // collision-induced throw, spin transfer, and cue ball deflection not fully simulated here.
2694 // The ghost ball method shows the *ideal* line for a center-cue hit without spin.
2695
2696 }
2697 else if (aimingAtRail && hitRailIndex != -1) {
2698 // Aiming at a rail: Draw reflection line
2699 float reflectAngle = cueAngle;
2700 // Reflect angle based on which rail was hit
2701 if (hitRailIndex == 0 || hitRailIndex == 1) { // Left or Right rail
2702 reflectAngle = PI - cueAngle; // Reflect horizontal component
2703 }
2704 else { // Top or Bottom rail
2705 reflectAngle = -cueAngle; // Reflect vertical component
2706 }
2707 // Normalize angle if needed (atan2 usually handles this)
2708 while (reflectAngle > PI) reflectAngle -= 2 * PI;
2709 while (reflectAngle <= -PI) reflectAngle += 2 * PI;
2710
2711
2712 float reflectionLength = 60.0f; // Length of the reflection line
2713 D2D1_POINT_2F reflectionEnd = D2D1::Point2F(
2714 finalLineEnd.x + cosf(reflectAngle) * reflectionLength,
2715 finalLineEnd.y + sinf(reflectAngle) * reflectionLength
2716 );
2717
2718 // Draw the reflection line (e.g., using a different color/style)
2719 pRT->DrawLine(finalLineEnd, reflectionEnd, pReflectBrush, 1.0f, pDashedStyle ? pDashedStyle : NULL);
2720 }
2721
2722 // Release resources
2723 SafeRelease(&pBrush);
2724 SafeRelease(&pGhostBrush);
2725 SafeRelease(&pCueBrush);
2726 SafeRelease(&pReflectBrush); // Release new brush
2727 SafeRelease(&pDashedStyle);
2728}
2729
2730void DrawUI(ID2D1RenderTarget* pRT) {
2731 if (!pTextFormat || !pLargeTextFormat) return;
2732
2733 ID2D1SolidColorBrush* pBrush = nullptr;
2734 pRT->CreateSolidColorBrush(UI_TEXT_COLOR, &pBrush);
2735 if (!pBrush) return;
2736
2737 // --- Player Info Area (Top Left/Right) --- (Unchanged)
2738 float uiTop = TABLE_TOP - 80;
2739 float uiHeight = 60;
2740 float p1Left = TABLE_LEFT;
2741 float p1Width = 150;
2742 float p2Left = TABLE_RIGHT - p1Width;
2743 D2D1_RECT_F p1Rect = D2D1::RectF(p1Left, uiTop, p1Left + p1Width, uiTop + uiHeight);
2744 D2D1_RECT_F p2Rect = D2D1::RectF(p2Left, uiTop, p2Left + p1Width, uiTop + uiHeight);
2745
2746 // Player 1 Info Text (Unchanged)
2747 std::wostringstream oss1;
2748 oss1 << player1Info.name.c_str() << L"\n";
2749 if (player1Info.assignedType != BallType::NONE) {
2750 oss1 << ((player1Info.assignedType == BallType::SOLID) ? L"Solids (Yellow)" : L"Stripes (Red)");
2751 oss1 << L" [" << player1Info.ballsPocketedCount << L"/7]";
2752 }
2753 else {
2754 oss1 << L"(Undecided)";
2755 }
2756 pRT->DrawText(oss1.str().c_str(), (UINT32)oss1.str().length(), pTextFormat, &p1Rect, pBrush);
2757 // Draw Player 1 Side Ball
2758 if (player1Info.assignedType != BallType::NONE)
2759 {
2760 ID2D1SolidColorBrush* pBallBrush = nullptr;
2761 D2D1_COLOR_F ballColor = (player1Info.assignedType == BallType::SOLID) ?
2762 D2D1::ColorF(1.0f, 1.0f, 0.0f) : D2D1::ColorF(1.0f, 0.0f, 0.0f);
2763 pRT->CreateSolidColorBrush(ballColor, &pBallBrush);
2764 if (pBallBrush)
2765 {
2766 D2D1_POINT_2F ballCenter = D2D1::Point2F(p1Rect.right + 10.0f, p1Rect.top + 20.0f);
2767 float radius = 10.0f;
2768 D2D1_ELLIPSE ball = D2D1::Ellipse(ballCenter, radius, radius);
2769 pRT->FillEllipse(&ball, pBallBrush);
2770 SafeRelease(&pBallBrush);
2771 // Draw border around the ball
2772 ID2D1SolidColorBrush* pBorderBrush = nullptr;
2773 pRT->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::Black), &pBorderBrush);
2774 if (pBorderBrush)
2775 {
2776 pRT->DrawEllipse(&ball, pBorderBrush, 1.5f); // thin border
2777 SafeRelease(&pBorderBrush);
2778 }
2779
2780 // If stripes, draw a stripe band
2781 if (player1Info.assignedType == BallType::STRIPE)
2782 {
2783 ID2D1SolidColorBrush* pStripeBrush = nullptr;
2784 pRT->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::White), &pStripeBrush);
2785 if (pStripeBrush)
2786 {
2787 D2D1_RECT_F stripeRect = D2D1::RectF(
2788 ballCenter.x - radius,
2789 ballCenter.y - 3.0f,
2790 ballCenter.x + radius,
2791 ballCenter.y + 3.0f
2792 );
2793 pRT->FillRectangle(&stripeRect, pStripeBrush);
2794 SafeRelease(&pStripeBrush);
2795 }
2796 }
2797 }
2798 }
2799
2800
2801 // Player 2 Info Text (Unchanged)
2802 std::wostringstream oss2;
2803 oss2 << player2Info.name.c_str() << L"\n";
2804 if (player2Info.assignedType != BallType::NONE) {
2805 oss2 << ((player2Info.assignedType == BallType::SOLID) ? L"Solids (Yellow)" : L"Stripes (Red)");
2806 oss2 << L" [" << player2Info.ballsPocketedCount << L"/7]";
2807 }
2808 else {
2809 oss2 << L"(Undecided)";
2810 }
2811 pRT->DrawText(oss2.str().c_str(), (UINT32)oss2.str().length(), pTextFormat, &p2Rect, pBrush);
2812 // Draw Player 2 Side Ball
2813 if (player2Info.assignedType != BallType::NONE)
2814 {
2815 ID2D1SolidColorBrush* pBallBrush = nullptr;
2816 D2D1_COLOR_F ballColor = (player2Info.assignedType == BallType::SOLID) ?
2817 D2D1::ColorF(1.0f, 1.0f, 0.0f) : D2D1::ColorF(1.0f, 0.0f, 0.0f);
2818 pRT->CreateSolidColorBrush(ballColor, &pBallBrush);
2819 if (pBallBrush)
2820 {
2821 D2D1_POINT_2F ballCenter = D2D1::Point2F(p2Rect.right + 10.0f, p2Rect.top + 20.0f);
2822 float radius = 10.0f;
2823 D2D1_ELLIPSE ball = D2D1::Ellipse(ballCenter, radius, radius);
2824 pRT->FillEllipse(&ball, pBallBrush);
2825 SafeRelease(&pBallBrush);
2826 // Draw border around the ball
2827 ID2D1SolidColorBrush* pBorderBrush = nullptr;
2828 pRT->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::Black), &pBorderBrush);
2829 if (pBorderBrush)
2830 {
2831 pRT->DrawEllipse(&ball, pBorderBrush, 1.5f); // thin border
2832 SafeRelease(&pBorderBrush);
2833 }
2834
2835 // If stripes, draw a stripe band
2836 if (player2Info.assignedType == BallType::STRIPE)
2837 {
2838 ID2D1SolidColorBrush* pStripeBrush = nullptr;
2839 pRT->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::White), &pStripeBrush);
2840 if (pStripeBrush)
2841 {
2842 D2D1_RECT_F stripeRect = D2D1::RectF(
2843 ballCenter.x - radius,
2844 ballCenter.y - 3.0f,
2845 ballCenter.x + radius,
2846 ballCenter.y + 3.0f
2847 );
2848 pRT->FillRectangle(&stripeRect, pStripeBrush);
2849 SafeRelease(&pStripeBrush);
2850 }
2851 }
2852 }
2853 }
2854
2855
2856 // --- MODIFIED: Current Turn Arrow (Blue, Bigger, Beside Name) ---
2857 ID2D1SolidColorBrush* pArrowBrush = nullptr;
2858 pRT->CreateSolidColorBrush(TURN_ARROW_COLOR, &pArrowBrush);
2859 if (pArrowBrush && currentGameState != GAME_OVER && currentGameState != SHOT_IN_PROGRESS && currentGameState != AI_THINKING) {
2860 float arrowSizeBase = 32.0f; // Base size for width/height offsets (4x original ~8)
2861 float arrowCenterY = p1Rect.top + uiHeight / 2.0f; // Center vertically with text box
2862 float arrowTipX, arrowBackX;
2863
2864 if (currentPlayer == 1) {
2865 // Player 1: Arrow left of P1 box, pointing right
2866 arrowBackX = p1Rect.left - 25.0f; // Position left of the box
2867 arrowTipX = arrowBackX + arrowSizeBase * 0.75f; // Pointy end extends right
2868 // Define points for right-pointing arrow
2869 D2D1_POINT_2F pt1 = D2D1::Point2F(arrowTipX, arrowCenterY); // Tip
2870 D2D1_POINT_2F pt2 = D2D1::Point2F(arrowBackX, arrowCenterY - arrowSizeBase / 2.0f); // Top-Back
2871 D2D1_POINT_2F pt3 = D2D1::Point2F(arrowBackX, arrowCenterY + arrowSizeBase / 2.0f); // Bottom-Back
2872
2873 ID2D1PathGeometry* pPath = nullptr;
2874 if (SUCCEEDED(pFactory->CreatePathGeometry(&pPath))) {
2875 ID2D1GeometrySink* pSink = nullptr;
2876 if (SUCCEEDED(pPath->Open(&pSink))) {
2877 pSink->BeginFigure(pt1, D2D1_FIGURE_BEGIN_FILLED);
2878 pSink->AddLine(pt2);
2879 pSink->AddLine(pt3);
2880 pSink->EndFigure(D2D1_FIGURE_END_CLOSED);
2881 pSink->Close();
2882 SafeRelease(&pSink);
2883 pRT->FillGeometry(pPath, pArrowBrush);
2884 }
2885 SafeRelease(&pPath);
2886 }
2887 }
2888 else { // Player 2
2889 // Player 2: Arrow left of P2 box, pointing right (or right of P2 box pointing left?)
2890 // Let's keep it consistent: Arrow left of the active player's box, pointing right.
2891 arrowBackX = p2Rect.left - 25.0f; // Position left of the box
2892 arrowTipX = arrowBackX + arrowSizeBase * 0.75f; // Pointy end extends right
2893 // Define points for right-pointing arrow
2894 D2D1_POINT_2F pt1 = D2D1::Point2F(arrowTipX, arrowCenterY); // Tip
2895 D2D1_POINT_2F pt2 = D2D1::Point2F(arrowBackX, arrowCenterY - arrowSizeBase / 2.0f); // Top-Back
2896 D2D1_POINT_2F pt3 = D2D1::Point2F(arrowBackX, arrowCenterY + arrowSizeBase / 2.0f); // Bottom-Back
2897
2898 ID2D1PathGeometry* pPath = nullptr;
2899 if (SUCCEEDED(pFactory->CreatePathGeometry(&pPath))) {
2900 ID2D1GeometrySink* pSink = nullptr;
2901 if (SUCCEEDED(pPath->Open(&pSink))) {
2902 pSink->BeginFigure(pt1, D2D1_FIGURE_BEGIN_FILLED);
2903 pSink->AddLine(pt2);
2904 pSink->AddLine(pt3);
2905 pSink->EndFigure(D2D1_FIGURE_END_CLOSED);
2906 pSink->Close();
2907 SafeRelease(&pSink);
2908 pRT->FillGeometry(pPath, pArrowBrush);
2909 }
2910 SafeRelease(&pPath);
2911 }
2912 }
2913 SafeRelease(&pArrowBrush);
2914 }
2915
2916
2917 // --- MODIFIED: Foul Text (Large Red, Bottom Center) ---
2918 if (foulCommitted && currentGameState != SHOT_IN_PROGRESS) {
2919 ID2D1SolidColorBrush* pFoulBrush = nullptr;
2920 pRT->CreateSolidColorBrush(FOUL_TEXT_COLOR, &pFoulBrush);
2921 if (pFoulBrush && pLargeTextFormat) {
2922 // Calculate Rect for bottom-middle area
2923 float foulWidth = 200.0f; // Adjust width as needed
2924 float foulHeight = 60.0f;
2925 float foulLeft = TABLE_LEFT + (TABLE_WIDTH / 2.0f) - (foulWidth / 2.0f);
2926 // Position below the pocketed balls bar
2927 float foulTop = pocketedBallsBarRect.bottom + 10.0f;
2928 D2D1_RECT_F foulRect = D2D1::RectF(foulLeft, foulTop, foulLeft + foulWidth, foulTop + foulHeight);
2929
2930 // --- Set text alignment to center for foul text ---
2931 pLargeTextFormat->SetTextAlignment(DWRITE_TEXT_ALIGNMENT_CENTER);
2932 pLargeTextFormat->SetParagraphAlignment(DWRITE_PARAGRAPH_ALIGNMENT_CENTER);
2933
2934 pRT->DrawText(L"FOUL!", 5, pLargeTextFormat, &foulRect, pFoulBrush);
2935
2936 // --- Restore default alignment for large text if needed elsewhere ---
2937 // pLargeTextFormat->SetTextAlignment(DWRITE_TEXT_ALIGNMENT_LEADING);
2938 // pLargeTextFormat->SetParagraphAlignment(DWRITE_PARAGRAPH_ALIGNMENT_CENTER);
2939
2940 SafeRelease(&pFoulBrush);
2941 }
2942 }
2943
2944 // Show AI Thinking State (Unchanged from previous step)
2945 if (currentGameState == AI_THINKING && pTextFormat) {
2946 ID2D1SolidColorBrush* pThinkingBrush = nullptr;
2947 pRT->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::Orange), &pThinkingBrush);
2948 if (pThinkingBrush) {
2949 D2D1_RECT_F thinkingRect = p2Rect;
2950 thinkingRect.top += 20; // Offset within P2 box
2951 // Ensure default text alignment for this
2952 pTextFormat->SetTextAlignment(DWRITE_TEXT_ALIGNMENT_CENTER);
2953 pTextFormat->SetParagraphAlignment(DWRITE_PARAGRAPH_ALIGNMENT_CENTER);
2954 pRT->DrawText(L"Thinking...", 11, pTextFormat, &thinkingRect, pThinkingBrush);
2955 SafeRelease(&pThinkingBrush);
2956 }
2957 }
2958
2959 SafeRelease(&pBrush);
2960
2961 // --- Draw CHEAT MODE label if active ---
2962 if (cheatModeEnabled) {
2963 ID2D1SolidColorBrush* pCheatBrush = nullptr;
2964 pRT->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::Red), &pCheatBrush);
2965 if (pCheatBrush && pTextFormat) {
2966 D2D1_RECT_F cheatTextRect = D2D1::RectF(
2967 TABLE_LEFT + 10.0f,
2968 TABLE_TOP + 10.0f,
2969 TABLE_LEFT + 200.0f,
2970 TABLE_TOP + 40.0f
2971 );
2972 pTextFormat->SetTextAlignment(DWRITE_TEXT_ALIGNMENT_LEADING);
2973 pTextFormat->SetParagraphAlignment(DWRITE_PARAGRAPH_ALIGNMENT_NEAR);
2974 pRT->DrawText(L"CHEAT MODE ON", wcslen(L"CHEAT MODE ON"), pTextFormat, &cheatTextRect, pCheatBrush);
2975 }
2976 SafeRelease(&pCheatBrush);
2977 }
2978}
2979
2980void DrawPowerMeter(ID2D1RenderTarget* pRT) {
2981 // Draw Border
2982 ID2D1SolidColorBrush* pBorderBrush = nullptr;
2983 pRT->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::Black), &pBorderBrush);
2984 if (!pBorderBrush) return;
2985 pRT->DrawRectangle(&powerMeterRect, pBorderBrush, 2.0f);
2986 SafeRelease(&pBorderBrush);
2987
2988 // Create Gradient Fill
2989 ID2D1GradientStopCollection* pGradientStops = nullptr;
2990 ID2D1LinearGradientBrush* pGradientBrush = nullptr;
2991 D2D1_GRADIENT_STOP gradientStops[4];
2992 gradientStops[0].position = 0.0f;
2993 gradientStops[0].color = D2D1::ColorF(D2D1::ColorF::Green);
2994 gradientStops[1].position = 0.45f;
2995 gradientStops[1].color = D2D1::ColorF(D2D1::ColorF::Yellow);
2996 gradientStops[2].position = 0.7f;
2997 gradientStops[2].color = D2D1::ColorF(D2D1::ColorF::Orange);
2998 gradientStops[3].position = 1.0f;
2999 gradientStops[3].color = D2D1::ColorF(D2D1::ColorF::Red);
3000
3001 pRT->CreateGradientStopCollection(gradientStops, 4, &pGradientStops);
3002 if (pGradientStops) {
3003 D2D1_LINEAR_GRADIENT_BRUSH_PROPERTIES props = {};
3004 props.startPoint = D2D1::Point2F(powerMeterRect.left, powerMeterRect.bottom);
3005 props.endPoint = D2D1::Point2F(powerMeterRect.left, powerMeterRect.top);
3006 pRT->CreateLinearGradientBrush(props, pGradientStops, &pGradientBrush);
3007 SafeRelease(&pGradientStops);
3008 }
3009
3010 // Calculate Fill Height
3011 float fillRatio = 0;
3012 if (isAiming && (currentGameState == AIMING || currentGameState == BREAKING)) {
3013 fillRatio = shotPower / MAX_SHOT_POWER;
3014 }
3015 float fillHeight = (powerMeterRect.bottom - powerMeterRect.top) * fillRatio;
3016 D2D1_RECT_F fillRect = D2D1::RectF(
3017 powerMeterRect.left,
3018 powerMeterRect.bottom - fillHeight,
3019 powerMeterRect.right,
3020 powerMeterRect.bottom
3021 );
3022
3023 if (pGradientBrush) {
3024 pRT->FillRectangle(&fillRect, pGradientBrush);
3025 SafeRelease(&pGradientBrush);
3026 }
3027
3028 // Draw scale notches
3029 ID2D1SolidColorBrush* pNotchBrush = nullptr;
3030 pRT->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::Black), &pNotchBrush);
3031 if (pNotchBrush) {
3032 for (int i = 0; i <= 8; ++i) {
3033 float y = powerMeterRect.top + (powerMeterRect.bottom - powerMeterRect.top) * (i / 8.0f);
3034 pRT->DrawLine(
3035 D2D1::Point2F(powerMeterRect.right + 2.0f, y),
3036 D2D1::Point2F(powerMeterRect.right + 8.0f, y),
3037 pNotchBrush,
3038 1.5f
3039 );
3040 }
3041 SafeRelease(&pNotchBrush);
3042 }
3043
3044 // Draw "Power" Label Below Meter
3045 if (pTextFormat) {
3046 ID2D1SolidColorBrush* pTextBrush = nullptr;
3047 pRT->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::Black), &pTextBrush);
3048 if (pTextBrush) {
3049 D2D1_RECT_F textRect = D2D1::RectF(
3050 powerMeterRect.left - 20.0f,
3051 powerMeterRect.bottom + 8.0f,
3052 powerMeterRect.right + 20.0f,
3053 powerMeterRect.bottom + 38.0f
3054 );
3055 pTextFormat->SetTextAlignment(DWRITE_TEXT_ALIGNMENT_CENTER);
3056 pTextFormat->SetParagraphAlignment(DWRITE_PARAGRAPH_ALIGNMENT_NEAR);
3057 pRT->DrawText(L"Power", 5, pTextFormat, &textRect, pTextBrush);
3058 SafeRelease(&pTextBrush);
3059 }
3060 }
3061
3062 // Draw Glow Effect if fully charged or fading out
3063 static float glowPulse = 0.0f;
3064 static bool glowIncreasing = true;
3065 static float glowFadeOut = 0.0f; // NEW: tracks fading out
3066
3067 if (shotPower >= MAX_SHOT_POWER * 0.99f) {
3068 // While fully charged, keep pulsing normally
3069 if (glowIncreasing) {
3070 glowPulse += 0.02f;
3071 if (glowPulse >= 1.0f) glowIncreasing = false;
3072 }
3073 else {
3074 glowPulse -= 0.02f;
3075 if (glowPulse <= 0.0f) glowIncreasing = true;
3076 }
3077 glowFadeOut = 1.0f; // Reset fade out to full
3078 }
3079 else if (glowFadeOut > 0.0f) {
3080 // If shot fired, gradually fade out
3081 glowFadeOut -= 0.02f;
3082 if (glowFadeOut < 0.0f) glowFadeOut = 0.0f;
3083 }
3084
3085 if (glowFadeOut > 0.0f) {
3086 ID2D1SolidColorBrush* pGlowBrush = nullptr;
3087 float effectiveOpacity = (0.3f + 0.7f * glowPulse) * glowFadeOut;
3088 pRT->CreateSolidColorBrush(
3089 D2D1::ColorF(D2D1::ColorF::Red, effectiveOpacity),
3090 &pGlowBrush
3091 );
3092 if (pGlowBrush) {
3093 float glowCenterX = (powerMeterRect.left + powerMeterRect.right) / 2.0f;
3094 float glowCenterY = powerMeterRect.top;
3095 D2D1_ELLIPSE glowEllipse = D2D1::Ellipse(
3096 D2D1::Point2F(glowCenterX, glowCenterY - 10.0f),
3097 12.0f + 3.0f * glowPulse,
3098 6.0f + 2.0f * glowPulse
3099 );
3100 pRT->FillEllipse(&glowEllipse, pGlowBrush);
3101 SafeRelease(&pGlowBrush);
3102 }
3103 }
3104}
3105
3106void DrawSpinIndicator(ID2D1RenderTarget* pRT) {
3107 ID2D1SolidColorBrush* pWhiteBrush = nullptr;
3108 ID2D1SolidColorBrush* pRedBrush = nullptr;
3109
3110 pRT->CreateSolidColorBrush(CUE_BALL_COLOR, &pWhiteBrush);
3111 pRT->CreateSolidColorBrush(ENGLISH_DOT_COLOR, &pRedBrush);
3112
3113 if (!pWhiteBrush || !pRedBrush) {
3114 SafeRelease(&pWhiteBrush);
3115 SafeRelease(&pRedBrush);
3116 return;
3117 }
3118
3119 // Draw White Ball Background
3120 D2D1_ELLIPSE bgEllipse = D2D1::Ellipse(spinIndicatorCenter, spinIndicatorRadius, spinIndicatorRadius);
3121 pRT->FillEllipse(&bgEllipse, pWhiteBrush);
3122 pRT->DrawEllipse(&bgEllipse, pRedBrush, 0.5f); // Thin red border
3123
3124
3125 // Draw Red Dot for Spin Position
3126 float dotRadius = 4.0f;
3127 float dotX = spinIndicatorCenter.x + cueSpinX * (spinIndicatorRadius - dotRadius); // Keep dot inside edge
3128 float dotY = spinIndicatorCenter.y + cueSpinY * (spinIndicatorRadius - dotRadius);
3129 D2D1_ELLIPSE dotEllipse = D2D1::Ellipse(D2D1::Point2F(dotX, dotY), dotRadius, dotRadius);
3130 pRT->FillEllipse(&dotEllipse, pRedBrush);
3131
3132 SafeRelease(&pWhiteBrush);
3133 SafeRelease(&pRedBrush);
3134}
3135
3136
3137void DrawPocketedBallsIndicator(ID2D1RenderTarget* pRT) {
3138 ID2D1SolidColorBrush* pBgBrush = nullptr;
3139 ID2D1SolidColorBrush* pBallBrush = nullptr;
3140
3141 // Ensure render target is valid before proceeding
3142 if (!pRT) return;
3143
3144 HRESULT hr = pRT->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::Black, 0.8f), &pBgBrush); // Semi-transparent black
3145 if (FAILED(hr)) { SafeRelease(&pBgBrush); return; } // Exit if brush creation fails
3146
3147 hr = pRT->CreateSolidColorBrush(D2D1::ColorF(0, 0, 0), &pBallBrush); // Placeholder, color will be set per ball
3148 if (FAILED(hr)) {
3149 SafeRelease(&pBgBrush);
3150 SafeRelease(&pBallBrush);
3151 return; // Exit if brush creation fails
3152 }
3153
3154 // Draw the background bar (rounded rect)
3155 D2D1_ROUNDED_RECT roundedRect = D2D1::RoundedRect(pocketedBallsBarRect, 10.0f, 10.0f); // Corner radius 10
3156 float baseAlpha = 0.8f;
3157 float flashBoost = pocketFlashTimer * 0.5f; // Make flash effect boost alpha slightly
3158 float finalAlpha = std::min(1.0f, baseAlpha + flashBoost);
3159 pBgBrush->SetOpacity(finalAlpha);
3160 pRT->FillRoundedRectangle(&roundedRect, pBgBrush);
3161 pBgBrush->SetOpacity(1.0f); // Reset opacity after drawing
3162
3163 // --- Draw small circles for pocketed balls inside the bar ---
3164
3165 // Calculate dimensions based on the bar's height for better scaling
3166 float barHeight = pocketedBallsBarRect.bottom - pocketedBallsBarRect.top;
3167 float ballDisplayRadius = barHeight * 0.30f; // Make balls slightly smaller relative to bar height
3168 float spacing = ballDisplayRadius * 2.2f; // Adjust spacing slightly
3169 float padding = spacing * 0.75f; // Add padding from the edges
3170 float center_Y = pocketedBallsBarRect.top + barHeight / 2.0f; // Vertical center
3171
3172 // Starting X positions with padding
3173 float currentX_P1 = pocketedBallsBarRect.left + padding;
3174 float currentX_P2 = pocketedBallsBarRect.right - padding; // Start from right edge minus padding
3175
3176 int p1DrawnCount = 0;
3177 int p2DrawnCount = 0;
3178 const int maxBallsToShow = 7; // Max balls per player in the bar
3179
3180 for (const auto& b : balls) {
3181 if (b.isPocketed) {
3182 // Skip cue ball and 8-ball in this indicator
3183 if (b.id == 0 || b.id == 8) continue;
3184
3185 bool isPlayer1Ball = (player1Info.assignedType != BallType::NONE && b.type == player1Info.assignedType);
3186 bool isPlayer2Ball = (player2Info.assignedType != BallType::NONE && b.type == player2Info.assignedType);
3187
3188 if (isPlayer1Ball && p1DrawnCount < maxBallsToShow) {
3189 pBallBrush->SetColor(b.color);
3190 // Draw P1 balls from left to right
3191 D2D1_ELLIPSE ballEllipse = D2D1::Ellipse(D2D1::Point2F(currentX_P1 + p1DrawnCount * spacing, center_Y), ballDisplayRadius, ballDisplayRadius);
3192 pRT->FillEllipse(&ballEllipse, pBallBrush);
3193 p1DrawnCount++;
3194 }
3195 else if (isPlayer2Ball && p2DrawnCount < maxBallsToShow) {
3196 pBallBrush->SetColor(b.color);
3197 // Draw P2 balls from right to left
3198 D2D1_ELLIPSE ballEllipse = D2D1::Ellipse(D2D1::Point2F(currentX_P2 - p2DrawnCount * spacing, center_Y), ballDisplayRadius, ballDisplayRadius);
3199 pRT->FillEllipse(&ballEllipse, pBallBrush);
3200 p2DrawnCount++;
3201 }
3202 // Note: Balls pocketed before assignment or opponent balls are intentionally not shown here.
3203 // You could add logic here to display them differently if needed (e.g., smaller, grayed out).
3204 }
3205 }
3206
3207 SafeRelease(&pBgBrush);
3208 SafeRelease(&pBallBrush);
3209}
3210
3211void DrawBallInHandIndicator(ID2D1RenderTarget* pRT) {
3212 if (!isDraggingCueBall && (currentGameState != BALL_IN_HAND_P1 && currentGameState != BALL_IN_HAND_P2 && currentGameState != PRE_BREAK_PLACEMENT)) {
3213 return; // Only show when placing/dragging
3214 }
3215
3216 Ball* cueBall = GetCueBall();
3217 if (!cueBall) return;
3218
3219 ID2D1SolidColorBrush* pGhostBrush = nullptr;
3220 pRT->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::White, 0.6f), &pGhostBrush); // Semi-transparent white
3221
3222 if (pGhostBrush) {
3223 D2D1_POINT_2F drawPos;
3224 if (isDraggingCueBall) {
3225 drawPos = D2D1::Point2F((float)ptMouse.x, (float)ptMouse.y);
3226 }
3227 else {
3228 // If not dragging but in placement state, show at current ball pos
3229 drawPos = D2D1::Point2F(cueBall->x, cueBall->y);
3230 }
3231
3232 // Check if the placement is valid before drawing differently?
3233 bool behindHeadstring = (currentGameState == PRE_BREAK_PLACEMENT);
3234 bool isValid = IsValidCueBallPosition(drawPos.x, drawPos.y, behindHeadstring);
3235
3236 if (!isValid) {
3237 // Maybe draw red outline if invalid placement?
3238 pGhostBrush->SetColor(D2D1::ColorF(D2D1::ColorF::Red, 0.6f));
3239 }
3240
3241
3242 D2D1_ELLIPSE ghostEllipse = D2D1::Ellipse(drawPos, BALL_RADIUS, BALL_RADIUS);
3243 pRT->FillEllipse(&ghostEllipse, pGhostBrush);
3244 pRT->DrawEllipse(&ghostEllipse, pGhostBrush, 1.0f); // Outline
3245
3246 SafeRelease(&pGhostBrush);
3247 }
3248}
3249```
3250
3251==++ Here's the full source for (file 2/3 (No OOP-based)) "resource.h"::: ++==
3252```resource.h
3253//{{NO_DEPENDENCIES}}
3254// Microsoft Visual C++ generated include file.
3255// Used by Yahoo-8Ball-Pool-Clone.rc
3256//
3257#define IDI_ICON1 101
3258// --- NEW Resource IDs (Define these in your .rc file / resource.h) ---
3259#define IDD_NEWGAMEDLG 106
3260#define IDC_RADIO_2P 1003
3261#define IDC_RADIO_CPU 1005
3262#define IDC_GROUP_AI 1006
3263#define IDC_RADIO_EASY 1007
3264#define IDC_RADIO_MEDIUM 1008
3265#define IDC_RADIO_HARD 1009
3266// Standard IDOK is usually defined, otherwise define it (e.g., #define IDOK 1)
3267
3268// Next default values for new objects
3269//
3270#ifdef APSTUDIO_INVOKED
3271#ifndef APSTUDIO_READONLY_SYMBOLS
3272#define _APS_NEXT_RESOURCE_VALUE 102
3273#define _APS_NEXT_COMMAND_VALUE 40001
3274#define _APS_NEXT_CONTROL_VALUE 1001
3275#define _APS_NEXT_SYMED_VALUE 101
3276#endif
3277#endif
3278
3279```
3280
3281==++ Here's the full source for (file 3/3 (No OOP-based)) "Yahoo-8Ball-Pool-Clone.rc"::: ++==
3282```Yahoo-8Ball-Pool-Clone.rc
3283// Microsoft Visual C++ generated resource script.
3284//
3285#include "resource.h"
3286
3287#define APSTUDIO_READONLY_SYMBOLS
3288/////////////////////////////////////////////////////////////////////////////
3289//
3290// Generated from the TEXTINCLUDE 2 resource.
3291//
3292#include "winres.h"
3293
3294/////////////////////////////////////////////////////////////////////////////
3295#undef APSTUDIO_READONLY_SYMBOLS
3296
3297/////////////////////////////////////////////////////////////////////////////
3298// English (United States) resources
3299
3300#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
3301LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
3302#pragma code_page(1252)
3303
3304#ifdef APSTUDIO_INVOKED
3305/////////////////////////////////////////////////////////////////////////////
3306//
3307// TEXTINCLUDE
3308//
3309
33101 TEXTINCLUDE
3311BEGIN
3312 "resource.h\0"
3313END
3314
33152 TEXTINCLUDE
3316BEGIN
3317 "#include ""winres.h""\r\n"
3318 "\0"
3319END
3320
33213 TEXTINCLUDE
3322BEGIN
3323 "\r\n"
3324 "\0"
3325END
3326
3327#endif // APSTUDIO_INVOKED
3328
3329
3330/////////////////////////////////////////////////////////////////////////////
3331//
3332// Icon
3333//
3334
3335// Icon with lowest ID value placed first to ensure application icon
3336// remains consistent on all systems.
3337IDI_ICON1 ICON "D:\\Download\\cpp-projekt\\FuzenOp_SiloTest\\icons\\shell32_277.ico"
3338
3339#endif // English (United States) resources
3340/////////////////////////////////////////////////////////////////////////////
3341
3342
3343
3344#ifndef APSTUDIO_INVOKED
3345/////////////////////////////////////////////////////////////////////////////
3346//
3347// Generated from the TEXTINCLUDE 3 resource.
3348//
3349
3350
3351/////////////////////////////////////////////////////////////////////////////
3352#endif // not APSTUDIO_INVOKED
3353
3354#include <windows.h> // Needed for control styles like WS_GROUP, BS_AUTORADIOBUTTON etc.
3355
3356/////////////////////////////////////////////////////////////////////////////
3357//
3358// Dialog
3359//
3360
3361IDD_NEWGAMEDLG DIALOGEX 0, 0, 220, 130 // Dialog position (x, y) and size (width, height) in Dialog Units (DLUs)
3362STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU
3363CAPTION "New 8-Ball Game"
3364FONT 8, "MS Shell Dlg", 400, 0, 0x1 // Standard dialog font
3365BEGIN
3366// --- Game Mode Selection ---
3367// Group Box for Game Mode (Optional visually, but helps structure)
3368GROUPBOX "Game Mode", IDC_STATIC, 7, 7, 90, 50
3369
3370// "2 Player" Radio Button (First in this group)
3371CONTROL "&2 Player (Human vs Human)", IDC_RADIO_2P, "Button",
3372BS_AUTORADIOBUTTON | WS_GROUP | WS_TABSTOP, 14, 20, 80, 10
3373
3374// "Human vs CPU" Radio Button
3375CONTROL "Human vs &CPU", IDC_RADIO_CPU, "Button",
3376BS_AUTORADIOBUTTON | WS_TABSTOP, 14, 35, 70, 10
3377
3378
3379// --- AI Difficulty Selection (Inside its own Group Box) ---
3380GROUPBOX "AI Difficulty", IDC_GROUP_AI, 118, 7, 95, 70
3381
3382// "Easy" Radio Button (First in the AI group)
3383CONTROL "&Easy", IDC_RADIO_EASY, "Button",
3384BS_AUTORADIOBUTTON | WS_GROUP | WS_TABSTOP, 125, 20, 60, 10
3385
3386// "Medium" Radio Button
3387CONTROL "&Medium", IDC_RADIO_MEDIUM, "Button",
3388BS_AUTORADIOBUTTON | WS_TABSTOP, 125, 35, 60, 10
3389
3390// "Hard" Radio Button
3391CONTROL "&Hard", IDC_RADIO_HARD, "Button",
3392BS_AUTORADIOBUTTON | WS_TABSTOP, 125, 50, 60, 10
3393
3394
3395// --- Standard Buttons ---
3396DEFPUSHBUTTON "Start", IDOK, 55, 105, 50, 14 // Default button (Enter key)
3397PUSHBUTTON "Cancel", IDCANCEL, 115, 105, 50, 14
3398END
3399```