diff --git a/pom.xml b/pom.xml index 0e10a8e..eb3573b 100644 --- a/pom.xml +++ b/pom.xml @@ -26,17 +26,17 @@ ${javafx.version} - - org.openjfx - javafx-fxml - ${javafx.version} - org.json json 20240303 + + org.openjfx + javafx-fxml + ${javafx.version} + org.openjfx javafx-media diff --git a/src/main/java/com/boredxgames/tictactoeclient/App.java b/src/main/java/com/boredxgames/tictactoeclient/App.java index f22376b..873223a 100644 --- a/src/main/java/com/boredxgames/tictactoeclient/App.java +++ b/src/main/java/com/boredxgames/tictactoeclient/App.java @@ -12,22 +12,21 @@ public class App extends Application { - private static Scene scene; - @Override public void start(Stage stage) throws IOException { - scene = NavigationManager.init(); + Scene scene = NavigationManager.init(); + ThemeManager.init(scene); stage.setScene(scene); stage.show(); - + AudioManager.playDefaultMusic(); - + AudioManager.attachGlobalClickSoundToScene(scene); } public static void main(String[] args) { launch(); } -} +} \ No newline at end of file diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/managers/navigation/NavigationManager.java b/src/main/java/com/boredxgames/tictactoeclient/domain/managers/navigation/NavigationManager.java index 53b9c92..1e27cd6 100644 --- a/src/main/java/com/boredxgames/tictactoeclient/domain/managers/navigation/NavigationManager.java +++ b/src/main/java/com/boredxgames/tictactoeclient/domain/managers/navigation/NavigationManager.java @@ -23,11 +23,7 @@ public static Scene init() throws IOException { current = new ScreenNavigationEntry(Screens.PRIMARY, null, null); - Parent root = initRoot(current.screen().getName()); - scene = new Scene(root, 640, 480); - - - ThemeManager.init(scene); + scene = new Scene(initRoot(current.screen().getName()), 1200, 800); return scene; } @@ -105,7 +101,6 @@ private static Parent initRoot(String fxml) throws IOException { Parent root = loader.load(); - current = new ScreenNavigationEntry(Screens.PRIMARY, null, loader.getController()); return root; } diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/managers/navigation/Screens.java b/src/main/java/com/boredxgames/tictactoeclient/domain/managers/navigation/Screens.java index 930c306..2d39688 100644 --- a/src/main/java/com/boredxgames/tictactoeclient/domain/managers/navigation/Screens.java +++ b/src/main/java/com/boredxgames/tictactoeclient/domain/managers/navigation/Screens.java @@ -5,11 +5,12 @@ public enum Screens { SECONDARY("GameModeScreen"), SETTINGS("SettingsScreen"), GAME_MODE("GameModeScreen"), + OFFLINE_MODE_SELECTION("offline_mode_selection"), SERVER_CONNECTION("server_connection"), AUTHENTICATION("authentication"), + GAME("game_screen"), Home("home"), PVP_SETUP("pvp_setup"), - GAME("game_screen"), DifficultySelection("difficulty_selection"), RECORDINGS("RecordingsListScreen"); diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/model/AuthResponseEntity.java b/src/main/java/com/boredxgames/tictactoeclient/domain/model/AuthResponseEntity.java index 61d6624..873947f 100644 --- a/src/main/java/com/boredxgames/tictactoeclient/domain/model/AuthResponseEntity.java +++ b/src/main/java/com/boredxgames/tictactoeclient/domain/model/AuthResponseEntity.java @@ -11,12 +11,12 @@ public class AuthResponseEntity { private String id ; private String userName ; - private int Score; + private int score; public AuthResponseEntity(String id, String userName, int Score) { this.id = id; this.userName = userName; - this.Score = Score; + this.score = Score; } public AuthResponseEntity() { @@ -31,7 +31,7 @@ public void setUserName(String userName) { } public void setScore(int Score) { - this.Score = Score; + this.score = Score; } public String getId() { @@ -43,7 +43,7 @@ public String getUserName() { } public int getScore() { - return Score; + return score; } public static AuthResponseEntity createAuthEntity(String id ,String username , int score) @@ -53,7 +53,7 @@ public static AuthResponseEntity createAuthEntity(String id ,String username , i @Override public String toString() { - return "AuthRequestEntity{" + "id=" + id + ", userName=" + userName + ", Score=" + Score + '}'; + return "AuthRequestEntity{" + "id=" + id + ", userName=" + userName + ", Score=" + score + '}'; } } diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/model/AvailablePlayersInfo.java b/src/main/java/com/boredxgames/tictactoeclient/domain/model/AvailablePlayersInfo.java new file mode 100644 index 0000000..cbc1abb --- /dev/null +++ b/src/main/java/com/boredxgames/tictactoeclient/domain/model/AvailablePlayersInfo.java @@ -0,0 +1,49 @@ +/* + * Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license + * Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template + */ +package com.boredxgames.tictactoeclient.domain.model; + +import java.util.Vector; + +/** + * + * @author mahmoud + */ +public class AvailablePlayersInfo { + public Vector onlinePlayers; + public Vector inGamePlayers; + public Vector pendingPlayers; + + public AvailablePlayersInfo(Vector online, Vector inGame, Vector pending) { + this.onlinePlayers = online; + this.inGamePlayers = inGame; + this.pendingPlayers = pending; + } + + public Vector getOnlinePlayers() { + return onlinePlayers; + } + + public void setOnlinePlayers(Vector onlinePlayers) { + this.onlinePlayers = onlinePlayers; + } + + public Vector getInGamePlayers() { + return inGamePlayers; + } + + public void setInGamePlayers(Vector inGamePlayers) { + this.inGamePlayers = inGamePlayers; + } + + public Vector getPendingPlayers() { + return pendingPlayers; + } + + public void setPendingPlayers(Vector pendingPlayers) { + this.pendingPlayers = pendingPlayers; + } + + +} \ No newline at end of file diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/model/GameRequestInfo.java b/src/main/java/com/boredxgames/tictactoeclient/domain/model/GameRequestInfo.java new file mode 100644 index 0000000..ad7e617 --- /dev/null +++ b/src/main/java/com/boredxgames/tictactoeclient/domain/model/GameRequestInfo.java @@ -0,0 +1,37 @@ +/* + * Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license + * Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template + */ +package com.boredxgames.tictactoeclient.domain.model; + +/** + * + * @author mahmoud + */ +public class GameRequestInfo { + private final String requesterId; + private final String requesterUserName; + private final String targetId; + + public GameRequestInfo(String requesterId, String requesterUserName, String targetId) { + this.requesterId = requesterId; + this.requesterUserName = requesterUserName; + this.targetId = targetId; + } + + public String getRequesterUserName() { + return requesterUserName; + } + + + + + public String getRequesterId() { + return requesterId; + } + + public String getTargetId() { + return targetId; + } + +} \ No newline at end of file diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/model/GameResponseInfo.java b/src/main/java/com/boredxgames/tictactoeclient/domain/model/GameResponseInfo.java new file mode 100644 index 0000000..5db33b2 --- /dev/null +++ b/src/main/java/com/boredxgames/tictactoeclient/domain/model/GameResponseInfo.java @@ -0,0 +1,58 @@ +/* + * Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license + * Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template + */ +package com.boredxgames.tictactoeclient.domain.model; + +/** + * + * @author mahmoud + */ +public class GameResponseInfo { + private String requesterId; + private String responderId; + private String responderUserName; + private boolean accepted; + + public String getRequesterId() { + return requesterId; + } + + public GameResponseInfo(String requesterId, String responderId, String responderUserName, boolean accepted) { + this.requesterId = requesterId; + this.responderId = responderId; + this.responderUserName = responderUserName; + this.accepted = accepted; + } + + public String getResponderUserName() { + return responderUserName; + } + + public void setResponderUserName(String responderUserName) { + this.responderUserName = responderUserName; + } + + + public void setRequesterId(String requesterId) { + this.requesterId = requesterId; + } + + public String getResponderId() { + return responderId; + } + + public void setResponderId(String responderId) { + this.responderId = responderId; + } + + public boolean isAccepted() { + return accepted; + } + + public void setAccepted(boolean accepted) { + this.accepted = accepted; + } + + +} diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/model/GameStartInfo.java b/src/main/java/com/boredxgames/tictactoeclient/domain/model/GameStartInfo.java new file mode 100644 index 0000000..1a3110f --- /dev/null +++ b/src/main/java/com/boredxgames/tictactoeclient/domain/model/GameStartInfo.java @@ -0,0 +1,34 @@ +/* + * Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license + * Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template + */ +package com.boredxgames.tictactoeclient.domain.model; + +/** + * + * @author moham + */ +public class GameStartInfo { + private final String roomId; + private final String player1; + private final String player2; + + public GameStartInfo(String roomId, String player1, String player2) { + this.roomId = roomId; + this.player1 = player1; + this.player2 = player2; + } + + public String getRoomId() { + return roomId; + } + + public String getPlayer1() { + return player1; + } + + public String getPlayer2() { + return player2; + } + +} diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/model/MoveInfo.java b/src/main/java/com/boredxgames/tictactoeclient/domain/model/MoveInfo.java new file mode 100644 index 0000000..88cc6c3 --- /dev/null +++ b/src/main/java/com/boredxgames/tictactoeclient/domain/model/MoveInfo.java @@ -0,0 +1,51 @@ +/* + * Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license + * Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template + */ +package com.boredxgames.tictactoeclient.domain.model; + +import com.boredxgames.tictactoeclient.domain.services.communication.Action; +import com.boredxgames.tictactoeclient.domain.services.communication.Header; +import com.boredxgames.tictactoeclient.domain.services.communication.Message; +import com.boredxgames.tictactoeclient.domain.services.communication.MessageType; +import com.google.gson.Gson; + +/** + * + * @author mahmoud + */ +public class MoveInfo { + private final String roomId; + private final String playerId; + private final String move; + + public MoveInfo(String playerId, String move, String roomId) { + this.playerId = playerId; + this.move = move; + this.roomId = roomId; + } + + public String getRoomId() { + return roomId; + } + + public String getPlayerId() { + return playerId; + } + + public String getMove() { + return move; + } + + public static MoveInfo createMoveInfo(String roomId, String playerId , Object data) + { + return new MoveInfo(playerId , toJson(data), roomId); + } + + static private String toJson(Object data) + { + Gson gson = new Gson(); + return gson.toJson(data); + + } +} diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/model/OnlineGameState.java b/src/main/java/com/boredxgames/tictactoeclient/domain/model/OnlineGameState.java new file mode 100644 index 0000000..92229e9 --- /dev/null +++ b/src/main/java/com/boredxgames/tictactoeclient/domain/model/OnlineGameState.java @@ -0,0 +1,5 @@ +package com.boredxgames.tictactoeclient.domain.model; + +public class OnlineGameState { + public static GameStartInfo info = null; +} diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/model/Player.java b/src/main/java/com/boredxgames/tictactoeclient/domain/model/PlayerEntity.java similarity index 51% rename from src/main/java/com/boredxgames/tictactoeclient/domain/model/Player.java rename to src/main/java/com/boredxgames/tictactoeclient/domain/model/PlayerEntity.java index 6a634ad..1c769ae 100644 --- a/src/main/java/com/boredxgames/tictactoeclient/domain/model/Player.java +++ b/src/main/java/com/boredxgames/tictactoeclient/domain/model/PlayerEntity.java @@ -1,31 +1,44 @@ +/* + * Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license + * Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template + */ package com.boredxgames.tictactoeclient.domain.model; /** - * @author Tasneem + * + * @author mahmoud */ -public class Player { - private String id; + +public class PlayerEntity { + private String id; private String username; private int score; - public Player(String id, String username, int score) { + public PlayerEntity(String id, String username, int score) { this.id = id; this.username = username; this.score = score; } - public String getUsername() { return username; } - + public void setUsername(String username) { this.username = username; } + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + public int getScore() { return score; } - + public void setScore(int score) { this.score = score; } diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/network/ServerConnectionManager.java b/src/main/java/com/boredxgames/tictactoeclient/domain/network/ServerConnectionManager.java index 77f215a..8f06f41 100644 --- a/src/main/java/com/boredxgames/tictactoeclient/domain/network/ServerConnectionManager.java +++ b/src/main/java/com/boredxgames/tictactoeclient/domain/network/ServerConnectionManager.java @@ -10,6 +10,7 @@ import com.boredxgames.tictactoeclient.domain.model.AuthResponseEntity; import com.boredxgames.tictactoeclient.domain.services.communication.Message; import com.boredxgames.tictactoeclient.domain.services.communication.MessageRouter; +import com.boredxgames.tictactoeclient.presentation.AuthenticationController; import com.google.gson.Gson; import java.io.DataInputStream; import java.io.DataOutputStream; @@ -27,9 +28,9 @@ public class ServerConnectionManager { private Socket socket; private DataInputStream dis; private DataOutputStream dos; - private Thread th; - private Gson gson = new Gson(); + private final Gson gson = new Gson(); private AuthResponseEntity player; + private volatile boolean isIntentionalDisconnect = false; public void setPlayer(AuthResponseEntity player) { this.player = player; @@ -48,14 +49,14 @@ public static ServerConnectionManager getInstance() { return instance; } - public void connect(String host, int port) throws IOException { - InetAddress ip = InetAddress.getByName(host); + public void connect(String host, int port ) throws IOException { + isIntentionalDisconnect = false; + InetAddress ip = InetAddress.getByName("localhost"); socket = new Socket(ip, port); dis = new DataInputStream(socket.getInputStream()); dos = new DataOutputStream(socket.getOutputStream()); - th = new Thread(this::readMessages); + Thread th = new Thread(this::readMessages); th.start(); - } private void readMessages() { @@ -68,20 +69,36 @@ private void readMessages() { MessageRouter router = MessageRouter.getInstance(); router.navigateMessage(response); } catch (IOException ex) { - close(); - + if (isIntentionalDisconnect) { + System.out.println("Disconnected intentionally."); + break; + } + + close(); Platform.runLater(() -> { - NavigationManager.navigate(Screens.SERVER_CONNECTION, NavigationAction.REPLACE_ALL); - }); + AuthenticationController.showUserAlert("Connection to server lost."); + NavigationManager.navigate(Screens.SERVER_CONNECTION, NavigationAction.REPLACE); + }); + break; + } } } + public void disconnect() { + try { + isIntentionalDisconnect = true; + close(); + } catch (Exception e) { + e.printStackTrace(); + } + } public synchronized void sendMessage(Message msg) { try { String jsonMessage = gson.toJson(msg); + System.out.println(jsonMessage + "SENTTTTT"); dos.writeUTF(jsonMessage); dos.flush(); @@ -90,6 +107,9 @@ public synchronized void sendMessage(Message msg) { } } + public AuthResponseEntity getPlayer() { + return player; +} public void close() { try { @@ -108,7 +128,8 @@ public void close() { } + public boolean isConnected() { return !socket.isClosed(); } -} +} \ No newline at end of file diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/services/game/GameService.java b/src/main/java/com/boredxgames/tictactoeclient/domain/services/GameService.java similarity index 75% rename from src/main/java/com/boredxgames/tictactoeclient/domain/services/game/GameService.java rename to src/main/java/com/boredxgames/tictactoeclient/domain/services/GameService.java index 455d07a..c7d52aa 100644 --- a/src/main/java/com/boredxgames/tictactoeclient/domain/services/game/GameService.java +++ b/src/main/java/com/boredxgames/tictactoeclient/domain/services/GameService.java @@ -1,7 +1,8 @@ -package com.boredxgames.tictactoeclient.domain.services.game; +package com.boredxgames.tictactoeclient.domain.services; import com.boredxgames.tictactoeclient.domain.model.GameState; import com.boredxgames.tictactoeclient.domain.model.Move; +import com.boredxgames.tictactoeclient.domain.services.game.GameBoard; /** * @author Tasneem diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/services/communication/Action.java b/src/main/java/com/boredxgames/tictactoeclient/domain/services/communication/Action.java index 91ebf41..72fcdc2 100644 --- a/src/main/java/com/boredxgames/tictactoeclient/domain/services/communication/Action.java +++ b/src/main/java/com/boredxgames/tictactoeclient/domain/services/communication/Action.java @@ -9,8 +9,10 @@ public enum Action { LOGIN(10), REGISTER(20), REQUEST_GAME(30), - GAME_RESPONSE(40), + GAME_RESPONSE(35), + GAME_START(40), SEND_GAME_UPDATE(50), + GAME_END(55), USERNAME_NOT_FOUND(60), REGISTERATION_SUCCESS(70), INTERNAL_SERVER_ERROR(80), @@ -24,7 +26,10 @@ public enum Action { INVALID_OPPONENT(160), NO_PENDING_REQUEST(170), GET_AVAILABLE_PLAYERS(180), - USER_IS_ONLINE(190); + USER_IS_ONLINE(190), + GET_LEADERBOARD(200); + + final int id; Action(int id) { @@ -51,4 +56,4 @@ public String toString() { } -} +} \ No newline at end of file diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/services/communication/MessageRouter.java b/src/main/java/com/boredxgames/tictactoeclient/domain/services/communication/MessageRouter.java index 042d46a..b523275 100644 --- a/src/main/java/com/boredxgames/tictactoeclient/domain/services/communication/MessageRouter.java +++ b/src/main/java/com/boredxgames/tictactoeclient/domain/services/communication/MessageRouter.java @@ -8,25 +8,25 @@ * * @author Hazem */ + import com.boredxgames.tictactoeclient.domain.managers.navigation.NavigationAction; import com.boredxgames.tictactoeclient.domain.managers.navigation.NavigationManager; import com.boredxgames.tictactoeclient.domain.managers.navigation.Screens; -import com.boredxgames.tictactoeclient.domain.model.AuthResponseEntity; +import com.boredxgames.tictactoeclient.domain.model.*; import com.boredxgames.tictactoeclient.domain.network.ServerConnectionManager; -import static com.boredxgames.tictactoeclient.domain.services.communication.Action.INTERNAL_SERVER_ERROR; -import static com.boredxgames.tictactoeclient.domain.services.communication.Action.INVALID_CREDENTIAL; -import static com.boredxgames.tictactoeclient.domain.services.communication.Action.LOGIN_SUCCESS; -import static com.boredxgames.tictactoeclient.domain.services.communication.Action.REGISTERATION_SUCCESS; -import static com.boredxgames.tictactoeclient.domain.services.communication.Action.USERNAME_NOT_FOUND; -import static com.boredxgames.tictactoeclient.domain.services.communication.Action.USER_IS_ONLINE; +import com.boredxgames.tictactoeclient.domain.services.game.OnlinePVPService; import com.boredxgames.tictactoeclient.presentation.AuthenticationController; +import com.boredxgames.tictactoeclient.presentation.HomeController; import com.google.gson.Gson; +import javafx.application.Platform; public class MessageRouter { private static MessageRouter instance; private static ServerConnectionManager connection; private Gson gson = new Gson(); + private static HomeController homeController; + private MessageRouter() { connection = ServerConnectionManager.getInstance(); } @@ -43,17 +43,18 @@ public void navigateMessage(String response) { MessageType messageType = message.getHeader().getMsgType(); assert messageType != null; - Message responseMessage; switch (messageType) { - case REQUEST -> responseMessage = handleRequest(message); + case REQUEST -> handleRequest(message); case RESPONSE -> handleResponse(message); - case EVENT -> { - } + case EVENT -> handleEvent(message); case ERROR -> handleError(message); default -> System.out.println("Unknown MessageType: " + messageType); } - // Handle event messages - } + } + + public static void setHomeController(HomeController controller) { + homeController = controller; + } private Message handleRequest(Message msg) { Action action = msg.getHeader().getAction(); @@ -61,77 +62,177 @@ private Message handleRequest(Message msg) { assert action != null; return switch (action) { - default: { + + default -> { System.out.println("Unknown Action: " + action); yield new Message(); } }; } - + private void handleResponse(Message msg) { Action action = msg.getHeader().getAction(); - System.out.println(action); - + System.out.println(action); + switch (action) { - case LOGIN_SUCCESS -> - { + case LOGIN_SUCCESS -> { System.out.println("Login Success"); - AuthResponseEntity responseData = gson.fromJson(msg.getData(),AuthResponseEntity.class); + AuthResponseEntity responseData = gson.fromJson(msg.getData(), AuthResponseEntity.class); + ServerConnectionManager.getInstance().setPlayer(responseData); System.out.println(responseData); - NavigationManager.navigate(Screens.SECONDARY, NavigationAction.REPLACE); + NavigationManager.navigate(Screens.Home, NavigationAction.REPLACE); } - case REGISTERATION_SUCCESS->{ + case REGISTERATION_SUCCESS -> { System.out.println("Registration success"); - AuthResponseEntity responseData = gson.fromJson(msg.getData(),AuthResponseEntity.class); - AuthenticationController.showUserAlert("Registration success"); - + AuthResponseEntity responseData = gson.fromJson(msg.getData(), AuthResponseEntity.class); + AuthenticationController.showUserAlert("Registration success"); + } - case USERNAME_NOT_FOUND->{ + case USERNAME_NOT_FOUND -> { System.out.println("Username not found"); AuthenticationController.showUserAlert("Username not found"); - + } + case REQUEST_GAME -> { + System.out.println("Server acknowledged Game Request."); + } + case GET_AVAILABLE_PLAYERS -> { + AvailablePlayersInfo info = gson.fromJson(msg.getData(), AvailablePlayersInfo.class); + if (homeController != null) { + Platform.runLater(() -> homeController.updatePlayersList(info)); + } + } + case GAME_RESPONSE -> { + GameResponseInfo info = gson.fromJson(msg.getData(), GameResponseInfo.class); + if (homeController != null) { + Platform.runLater(() -> homeController.handleServerGameResponse(info)); + } + } + case GET_LEADERBOARD -> { + AvailablePlayersInfo info = gson.fromJson(msg.getData(), AvailablePlayersInfo.class); + if (homeController != null) { + Platform.runLater(() -> homeController.updateLeaderboardUI(info)); + } + } + case SEND_GAME_UPDATE -> { + MoveInfo moveInfo = gson.fromJson(msg.getData(), MoveInfo.class); + OnlinePVPService.onIncomingMove(moveInfo); + } + default -> { System.out.println("Unknown Action: " + action); } - }; + } + ; + } + + private void handleEvent(Message msg) { + Action action = msg.getHeader().getAction(); + System.out.println(action); + + switch (action) { + case REQUEST_GAME -> { + GameRequestInfo info = gson.fromJson(msg.getData(), GameRequestInfo.class); + if (homeController != null) { + Platform.runLater(() -> homeController.showIncomingGameRequest(info)); + } + + } + case GAME_START -> { + OnlineGameState.info = gson.fromJson(msg.getData(), GameStartInfo.class); + String player1Name = "You"; + String player2Name = "Opponent"; + NavigationManager.navigate(Screens.GAME, NavigationAction.REPLACE, new GameNavigationParams( + player1Name,player2Name, GameMode.ONLINE_PVP + )); + + } + + default -> { + System.out.println("Unknown Action: " + action); + + } + } + ; } - + private void handleError(Message msg) { System.out.println("my time has come"); Action action = msg.getHeader().getAction(); - System.out.println(action); - + System.out.println(action); + switch (action) { - - case USERNAME_NOT_FOUND->{ + + case USERNAME_NOT_FOUND -> { System.out.println("Username not found"); AuthenticationController.showUserAlert("Username not found"); - + } - - case INTERNAL_SERVER_ERROR->{ + + case INTERNAL_SERVER_ERROR -> { + String errorMessage = "Server error. Please try again."; System.out.println("Internal Server Error"); AuthenticationController.showUserAlert("Internal Server Error"); - + if (homeController != null) { + Platform.runLater(() -> homeController.showErrorAlert(errorMessage)); + } + + } + case PLAYER_BUSY -> { + String errorMsg = "That player is currently in a game."; + System.out.println(errorMsg); + if (homeController != null) { + Platform.runLater(() -> homeController.showErrorAlert(errorMsg)); + } } - case INVALID_CREDENTIAL->{ - System.out.println("INVALID_CREDENTIAL"); + case PENDING_REQUEST_EXISTS -> { + String errorMsg = "You already have a request pending."; + System.out.println(errorMsg); + if (homeController != null) { + Platform.runLater(() -> homeController.showErrorAlert(errorMsg)); + } + } + case TARGET_HAS_PENDING_REQUEST -> { + String errorMsg = "That player is considering another challenge."; + System.out.println(errorMsg); + if (homeController != null) { + Platform.runLater(() -> homeController.showErrorAlert(errorMsg)); + } + } + + case ROOM_NOT_FOUND -> { + String errorMsg = "The game session is no longer available."; + if (homeController != null) { + // Close the "Accept/Decline" popup if open + homeController.dismissIncomingRequest(); + Platform.runLater(() -> homeController.showErrorAlert(errorMsg)); + } + } + + case INVALID_OPPONENT -> { + String errorMsg = "Opponent disconnected."; + if (homeController != null) { + homeController.dismissIncomingRequest(); + Platform.runLater(() -> homeController.showErrorAlert(errorMsg)); + } + } + case INVALID_CREDENTIAL -> { + System.out.println("INVALID_CREDENTIAL"); AuthenticationController.showUserAlert("INVALID CREDENTIAL"); - + } - case USER_IS_ONLINE->{ - System.out.println("User alread logged in"); + case USER_IS_ONLINE -> { + System.out.println("User alread logged in"); AuthenticationController.showUserAlert("User alread logged in"); - + } - case USERNAME_ALREADY_EXIST->{ - System.out.println("USERNAME_ALREADY_EXIST"); + case USERNAME_ALREADY_EXIST -> { + System.out.println("USERNAME_ALREADY_EXIST"); AuthenticationController.showUserAlert("USERNAME ALREADY EXIST"); - + } default -> { System.out.println("Unknown Action: " + action); @@ -140,4 +241,4 @@ private void handleError(Message msg) { } } -} +} \ No newline at end of file diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/services/game/GameBoard.java b/src/main/java/com/boredxgames/tictactoeclient/domain/services/game/GameBoard.java index c0312ce..6a97dd0 100644 --- a/src/main/java/com/boredxgames/tictactoeclient/domain/services/game/GameBoard.java +++ b/src/main/java/com/boredxgames/tictactoeclient/domain/services/game/GameBoard.java @@ -28,6 +28,7 @@ public class GameBoard { private final List moveHistory; + public GameBoard() { this(PLAYER_X); } @@ -66,6 +67,10 @@ public boolean makeMove(int row, int col) { } public boolean makeMove(int row, int col, char player) { + if (player != currentPlayer) { + return false; + } + if (!isValidMove(row, col)) { return false; } @@ -78,7 +83,7 @@ public boolean makeMove(int row, int col, char player) { updateGameState(); if (gameState == GameState.IN_PROGRESS) { - currentPlayer = (player == PLAYER_X) ? PLAYER_O : PLAYER_X; + currentPlayer = (player == PLAYER_X) ? PLAYER_O : PLAYER_X; } return true; diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/services/game/OfflinePVEAIService.java b/src/main/java/com/boredxgames/tictactoeclient/domain/services/game/OfflinePVEAIService.java index 16c299c..2f93b4b 100644 --- a/src/main/java/com/boredxgames/tictactoeclient/domain/services/game/OfflinePVEAIService.java +++ b/src/main/java/com/boredxgames/tictactoeclient/domain/services/game/OfflinePVEAIService.java @@ -7,6 +7,7 @@ import com.boredxgames.tictactoeclient.domain.model.GameState; import com.boredxgames.tictactoeclient.domain.model.Move; import com.boredxgames.tictactoeclient.domain.services.AIService; +import com.boredxgames.tictactoeclient.domain.services.GameService; /** * diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/services/game/OfflinePVPService.java b/src/main/java/com/boredxgames/tictactoeclient/domain/services/game/OfflinePVPService.java index 18c3697..c667efc 100644 --- a/src/main/java/com/boredxgames/tictactoeclient/domain/services/game/OfflinePVPService.java +++ b/src/main/java/com/boredxgames/tictactoeclient/domain/services/game/OfflinePVPService.java @@ -6,12 +6,13 @@ import com.boredxgames.tictactoeclient.domain.model.GameState; import com.boredxgames.tictactoeclient.domain.model.Move; +import com.boredxgames.tictactoeclient.domain.services.GameService; /** * * @author sheri */ -public class OfflinePVPService implements GameService{ +public class OfflinePVPService implements GameService { @Override public void makeMove(Move move, char currentPlayer) { diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/services/game/OnlinGameSession.java b/src/main/java/com/boredxgames/tictactoeclient/domain/services/game/OnlinGameSession.java new file mode 100644 index 0000000..f483e61 --- /dev/null +++ b/src/main/java/com/boredxgames/tictactoeclient/domain/services/game/OnlinGameSession.java @@ -0,0 +1,86 @@ +/* + * Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license + * Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template + */ +package com.boredxgames.tictactoeclient.domain.services.game; + +/** + * + * @author mahmoud + */ + + +import com.boredxgames.tictactoeclient.domain.model.AuthResponseEntity; +import com.boredxgames.tictactoeclient.domain.model.GameRequestInfo; +import com.boredxgames.tictactoeclient.domain.model.GameResponseInfo; +import com.boredxgames.tictactoeclient.domain.network.ServerConnectionManager; +import com.boredxgames.tictactoeclient.domain.services.communication.Action; +import com.boredxgames.tictactoeclient.domain.services.communication.Message; +import com.boredxgames.tictactoeclient.domain.services.communication.MessageType; + +public class OnlinGameSession { + + private static OnlinGameSession instance; + private final ServerConnectionManager connection; + private boolean isWaitingForResponse = false; + + private OnlinGameSession() { + this.connection = ServerConnectionManager.getInstance(); + } + + public static synchronized OnlinGameSession getInstance() { + if (instance == null) { + instance = new OnlinGameSession(); + } + return instance; + } + + public boolean isWaiting() { + return isWaitingForResponse; + } + + public void setWaiting(boolean waiting) { + this.isWaitingForResponse = waiting; + System.out.println("DEBUG: User waiting state set to: " + waiting); + } + + public void requestAvailablePlayers() { + Message msg = Message.createMessage(MessageType.REQUEST, Action.GET_AVAILABLE_PLAYERS, ""); + connection.sendMessage(msg); + } + + public void sendGameRequest(String targetPlayerId) { + if (isWaitingForResponse) { + System.out.println("Blocked: Already waiting for a response."); + return; + } + + AuthResponseEntity me = connection.getPlayer(); + if (me == null) return; + + GameRequestInfo requestInfo = new GameRequestInfo(me.getId(), me.getUserName(), targetPlayerId); + Message msg = Message.createMessage(MessageType.REQUEST, Action.REQUEST_GAME, requestInfo); + + System.out.println("Sending Game Request to: " + targetPlayerId); + connection.sendMessage(msg); + setWaiting(true); + } + + public void sendGameResponse(GameRequestInfo originalRequest, boolean accepted) { + AuthResponseEntity me = connection.getPlayer(); + + GameResponseInfo responseInfo = new GameResponseInfo( + originalRequest.getRequesterId(), + originalRequest.getTargetId(), + me.getUserName(), + accepted + ); + + Message msg = Message.createMessage(MessageType.RESPONSE, Action.GAME_RESPONSE, responseInfo); + connection.sendMessage(msg); + } + public void requestLeaderboard() { + Message msg = Message.createMessage(MessageType.REQUEST, Action.GET_LEADERBOARD, ""); + connection.sendMessage(msg); + } +} \ No newline at end of file diff --git a/src/main/java/com/boredxgames/tictactoeclient/domain/services/game/OnlinePVPService.java b/src/main/java/com/boredxgames/tictactoeclient/domain/services/game/OnlinePVPService.java new file mode 100644 index 0000000..b6eedb7 --- /dev/null +++ b/src/main/java/com/boredxgames/tictactoeclient/domain/services/game/OnlinePVPService.java @@ -0,0 +1,87 @@ +package com.boredxgames.tictactoeclient.domain.services.game; + +import com.boredxgames.tictactoeclient.domain.model.*; +import com.boredxgames.tictactoeclient.domain.network.ServerConnectionManager; +import com.boredxgames.tictactoeclient.domain.services.GameService; +import com.boredxgames.tictactoeclient.domain.services.communication.Action; +import com.boredxgames.tictactoeclient.domain.services.communication.Message; +import com.boredxgames.tictactoeclient.domain.services.communication.MessageType; +import com.google.gson.Gson; +import java.util.function.Consumer; + +public class OnlinePVPService implements GameService { + + private static OnlinePVPService instance; + + public boolean shouldPlayFirst() { + + System.out.println(" Check equality"+OnlineGameState.info.getPlayer1().equalsIgnoreCase(ServerConnectionManager.getInstance().getPlayer().getId())); + + + if (OnlineGameState.info.getPlayer1().equalsIgnoreCase(ServerConnectionManager.getInstance().getPlayer().getId())) { + return true; + } + return false; + } + + public static OnlinePVPService getInstance() { + if (instance == null) { + instance = new OnlinePVPService(); + } + return instance; + } + + private OnlinePVPService() { + } + + // specific listener to bridge Network -> UI + private static Consumer moveListener; + + public OnlinePVPService setMoveListener(Consumer listener) { + moveListener = listener; + return this; + } + + public static void onIncomingMove(MoveInfo moveInfo) { + Gson gson = new Gson(); + Move move = gson.fromJson(moveInfo.getMove(), Move.class); + if (moveListener != null) { + moveListener.accept(move); + } + } + + @Override + public void makeMove(Move move, char currentPlayer) { + + ServerConnectionManager connectionManager = ServerConnectionManager.getInstance(); + AuthResponseEntity player = connectionManager.getPlayer(); + GameStartInfo sessionInfo = OnlineGameState.info; + // Prepare the data + MoveInfo info = MoveInfo.createMoveInfo(sessionInfo.getRoomId(), player.getId(), move); + + // Send SEND_GAME_UPDATE action to server + Message msg = Message.createMessage( + MessageType.RESPONSE, + Action.SEND_GAME_UPDATE, + info + ); + connectionManager.sendMessage(msg); + } + + @Override + public void makeMove(Move move, char currentPlayer, GameBoard board) { + + } + + @Override + public Move getNextMove(GameBoard board, char currentPlayer) { + // In online play, we don't calculate the next move locally. + // We wait for the server event instead. + return null; + } + + @Override + public GameState getOutcome(GameBoard board) { + return board.getGameState(); + } +} diff --git a/src/main/java/com/boredxgames/tictactoeclient/presentation/AuthenticationController.java b/src/main/java/com/boredxgames/tictactoeclient/presentation/AuthenticationController.java index 6d4c9ed..bebe657 100644 --- a/src/main/java/com/boredxgames/tictactoeclient/presentation/AuthenticationController.java +++ b/src/main/java/com/boredxgames/tictactoeclient/presentation/AuthenticationController.java @@ -109,6 +109,7 @@ private String validateInput(String username, String password) { @FXML private void onBackClicked() { + ServerConnectionManager.getInstance().disconnect(); NavigationManager.navigate(Screens.SERVER_CONNECTION, NavigationAction.REPLACE); } @@ -120,10 +121,39 @@ public static void showUserAlert(String message) { alert.setHeaderText(null); alert.setContentText(message); + DialogPane dialogPane = alert.getDialogPane(); + + // 1. Style the main pane + dialogPane.setStyle( + "-fx-background-color: #1e1e1e; " + + "-fx-border-color: #333333; " + + "-fx-border-width: 2px;" + ); + + // 2. Style all labels (Content text) + dialogPane.lookupAll(".label").forEach(node -> + node.setStyle("-fx-text-fill: #e0e0e0; -fx-font-family: 'Segoe UI';") + ); + + // 3. Style the buttons + dialogPane.lookupAll(".button").forEach(node -> { + node.setStyle( + "-fx-background-color: #3c3f41; " + + "-fx-text-fill: white; " + + "-fx-background-radius: 4; " + + "-fx-cursor: hand;" + ); + + // Add simple hover effect via code + node.setOnMouseEntered(e -> node.setStyle("-fx-background-color: #4b4d4d; -fx-text-fill: white; -fx-background-radius: 4;")); + node.setOnMouseExited(e -> node.setStyle("-fx-background-color: #3c3f41; -fx-text-fill: white; -fx-background-radius: 4;")); + }); + + // Set the owner window javafx.stage.Window.getWindows().stream() .filter(javafx.stage.Window::isShowing) .findFirst() - .ifPresent(window -> alert.initOwner(window)); + .ifPresent(alert::initOwner); alert.showAndWait().ifPresent(type -> { if ("Server is out of service".equals(message) || "Internal Server Error".equals(message)) { diff --git a/src/main/java/com/boredxgames/tictactoeclient/presentation/BackgroundHomeAnimation.java b/src/main/java/com/boredxgames/tictactoeclient/presentation/BackgroundHomeAnimation.java new file mode 100644 index 0000000..1b86222 --- /dev/null +++ b/src/main/java/com/boredxgames/tictactoeclient/presentation/BackgroundHomeAnimation.java @@ -0,0 +1,85 @@ +package com.boredxgames.tictactoeclient.presentation; + +/* + * Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license + * Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template + */ + + +/** + * + * @author Hazem + */ + + +import java.util.Random; +import javafx.scene.Node; +import javafx.scene.control.Label; +import javafx.scene.layout.Pane; + + +public abstract class BackgroundHomeAnimation{ + + private static final Random random = new Random(); + + /** + * Animates a container sliding up from the bottom and fading in. + * @param node The container (e.g., VBox) to animate. + */ + public static void animateCardEntry(Node node) { +// node.setOpacity(0); +// node.setTranslateY(500); +// +// TranslateTransition tt = new TranslateTransition(Duration.millis(1500), node); +// tt.setToY(0); +// +// FadeTransition ft = new FadeTransition(Duration.millis(1000), node); +// ft.setToValue(1); +// +// ParallelTransition pt = new ParallelTransition(tt, ft); +// pt.play(); + } + + + public static void startBackgroundAnimation(Pane backgroundPane, double screenWidth, double screenHeight) { +// int particleCount = 50; +// +// for (int i = 0; i < particleCount; i++) { +// Label particle = new Label(random.nextBoolean() ? "X" : "O"); +// particle.getStyleClass().add("background-particle"); +// +// double size = 15 + random.nextInt(50); +// particle.setStyle("-fx-font-size: " + size + "px; -fx-text-fill: rgba(79, 94, 247, " + (0.05 + random.nextDouble() * 0.2) + ");"); +// +// particle.setTranslateX(random.nextInt((int) screenWidth)); +// particle.setTranslateY(random.nextInt((int) screenHeight)); +// +// backgroundPane.getChildren().add(particle); +// animateParticle(particle, screenWidth, screenHeight); +// } + } + + private static void animateParticle(Label particle, double width, double height) { +// TranslateTransition move = new TranslateTransition(Duration.seconds(15 + random.nextInt(15)), particle); +// double endX = particle.getTranslateX() + (random.nextInt(200) - 100); // Drift sideways +// double endY = -100; +// +// move.setToX(endX); +// move.setToY(endY); +// move.setCycleCount(1); +// +// RotateTransition rotate = new RotateTransition(Duration.seconds(5 + random.nextInt(10)), particle); +// rotate.setByAngle(360); +// rotate.setCycleCount(RotateTransition.INDEFINITE); +// +// move.setOnFinished(e -> { +// +// particle.setTranslateY(height + random.nextInt(100)); +// particle.setTranslateX(random.nextInt((int) width)); +// animateParticle(particle, width, height); +// }); +// +// move.play(); +// rotate.play(); + } +} \ No newline at end of file diff --git a/src/main/java/com/boredxgames/tictactoeclient/presentation/GameController.java b/src/main/java/com/boredxgames/tictactoeclient/presentation/GameController.java index 78590ff..7b4e3df 100644 --- a/src/main/java/com/boredxgames/tictactoeclient/presentation/GameController.java +++ b/src/main/java/com/boredxgames/tictactoeclient/presentation/GameController.java @@ -6,20 +6,25 @@ import com.boredxgames.tictactoeclient.domain.managers.navigation.Screens; import com.boredxgames.tictactoeclient.domain.model.GameMode; import com.boredxgames.tictactoeclient.domain.model.GameNavigationParams; -import com.boredxgames.tictactoeclient.domain.model.GameRecord; + import com.boredxgames.tictactoeclient.domain.model.GameState; import com.boredxgames.tictactoeclient.domain.model.Move; +import com.boredxgames.tictactoeclient.domain.services.AIService; +import com.boredxgames.tictactoeclient.domain.services.GameService; import com.boredxgames.tictactoeclient.domain.services.game.GameBoard; -import com.boredxgames.tictactoeclient.domain.services.game.GameService; import com.boredxgames.tictactoeclient.domain.services.game.OfflinePVEAIService; import com.boredxgames.tictactoeclient.domain.services.game.OfflinePVPService; +import com.boredxgames.tictactoeclient.domain.services.game.OnlinePVPService; +import com.boredxgames.tictactoeclient.domain.model.GameRecord; import com.boredxgames.tictactoeclient.domain.services.storage.GameRecordingService; import java.net.URL; import java.util.Objects; import java.util.ResourceBundle; import javafx.animation.KeyFrame; + import javafx.animation.PauseTransition; import javafx.animation.Timeline; +import javafx.application.Platform; import javafx.fxml.FXML; import javafx.fxml.Initializable; import javafx.scene.control.Button; @@ -77,15 +82,17 @@ public class GameController implements Initializable, NavigationParameterAware { private String player2Name = "Player 2"; private GameService gameService; + private char localPlayerId; private final GameRecordingService recordingService = new GameRecordingService(); private GameRecord replayData; @Override public void initialize(URL url, ResourceBundle rb) { + cells = new Button[][]{ - {cell00, cell01, cell02}, - {cell10, cell11, cell12}, - {cell20, cell21, cell22} + {cell00, cell01, cell02}, + {cell10, cell11, cell12}, + {cell20, cell21, cell22} }; gameBoard = new GameBoard(); setupCellHandlers(); @@ -127,6 +134,7 @@ private void applyGameModeSettings() { difficultyBadge.setManaged(false); changeDifficultyButton.setVisible(false); changeDifficultyButton.setManaged(false); + gameService = new OfflinePVPService(); } case OFFLINE_PVE -> { @@ -135,6 +143,7 @@ private void applyGameModeSettings() { difficultyBadge.setManaged(true); changeDifficultyButton.setVisible(true); changeDifficultyButton.setManaged(true); + gameService = new OfflinePVEAIService(); } case ONLINE_PVP -> { @@ -143,6 +152,35 @@ private void applyGameModeSettings() { difficultyBadge.setManaged(false); changeDifficultyButton.setVisible(false); changeDifficultyButton.setManaged(false); + + boolean amIPlayer1 = OnlinePVPService.getInstance().shouldPlayFirst(); + localPlayerId = amIPlayer1 ? GameBoard.PLAYER_X : GameBoard.PLAYER_O; + + System.out.println("I am Player: " + localPlayerId); + + gameService = OnlinePVPService.getInstance().setMoveListener((move) -> { + Platform.runLater(() -> { + char remotePlayer = (localPlayerId == GameBoard.PLAYER_X) ? GameBoard.PLAYER_O : GameBoard.PLAYER_X; + + if (gameBoard.makeMove(move.getRow(), move.getCol(), remotePlayer)) { + updateCell(move.getRow(), move.getCol(), remotePlayer); + + if (gameBoard.getGameState() != GameState.IN_PROGRESS) { + handleGameEnd(); + } else { + updateTurnIndicator(); + enableBoard(); + } + } + }); + }); + + if (amIPlayer1) { + enableBoard(); + } else { + disableBoard(); + } + updateTurnIndicator(); } case REPLAY -> { opponentTypeLabel.setText("REPLAY"); @@ -176,6 +214,10 @@ private void setupButtonHandlers() { NavigationManager.navigate(Screens.PRIMARY, NavigationAction.REPLACE_ALL); }); + backButton.setOnAction(e -> { + NavigationManager.pop(); + }); + backButton.setOnAction(e -> NavigationManager.pop()); settingsButton.setOnAction(e -> NavigationManager.navigate(Screens.SETTINGS, NavigationAction.REPLACE_ALL)); changeDifficultyButton.setOnAction(e -> {}); @@ -211,60 +253,84 @@ private void handleCellClick(int row, int col) { if (!gameBoard.isValidMove(row, col)) return; if (gameMode == GameMode.REPLAY) return; - char currentPlayer = gameBoard.getCurrentPlayer(); + if (gameMode == GameMode.ONLINE_PVP && gameBoard.getCurrentPlayer() != localPlayerId) { + return; + } switch (gameMode) { - case OFFLINE_PVP -> { - gameBoard.makeMove(row, col, currentPlayer); - updateCell(row, col, currentPlayer); - if (!checkGameEnd()) { - updateTurnIndicator(); - } - } - case OFFLINE_PVE -> { - gameBoard.makeMove(row, col, GameBoard.PLAYER_X); - updateCell(row, col, GameBoard.PLAYER_X); - if (checkGameEnd()) return; + case OFFLINE_PVP -> handleOfflinePvp(row, col); + case OFFLINE_PVE -> handleOfflinePve(row, col); + case ONLINE_PVP -> handleOnlinePvp(row, col); + } + } - disableBoard(); - PauseTransition pause = new PauseTransition(Duration.millis(500)); - pause.setOnFinished(e -> { - Move aiMove = gameService.getNextMove(gameBoard, GameBoard.PLAYER_O); - gameService.makeMove(aiMove, GameBoard.PLAYER_O, gameBoard); - if (aiMove != null) { - updateCell(aiMove.getRow(), aiMove.getCol(), GameBoard.PLAYER_O); - } - if (!checkGameEnd()) { - enableBoard(); - gameBoard.switchPlayer(); - updateTurnIndicator(); - } - }); - pause.play(); - } + private void handleOfflinePvp(int row, int col) { + char currentPlayer = gameBoard.getCurrentPlayer(); + + performMove(row, col, currentPlayer); + + if (!checkGameEnd()) { + gameBoard.switchPlayer(); + updateTurnIndicator(); } } - private void startReplay() { + private void handleOfflinePve(int row, int col) { + performMove(row, col, GameBoard.PLAYER_X); + + if (checkGameEnd()) { + return; + } + + scheduleAiTurn(); + } + + private void handleOnlinePvp(int row, int col) { + performMove(row, col, localPlayerId); + + Move move = new Move(row, col); + gameService.makeMove(move, localPlayerId); + disableBoard(); - Timeline timeline = new Timeline(); - int delay = 0; - for (var move : replayData.moves()) { - delay += 1000; - KeyFrame frame = new KeyFrame(Duration.millis(delay), e -> { - gameBoard.forceMove(move.row(), move.col(), move.player()); - updateCell(move.row(), move.col(), move.player()); - - gameBoard.switchPlayer(); - updateTurnIndicator(); - checkGameEnd(); - }); - timeline.getKeyFrames().add(frame); + if (gameBoard.getGameState() != GameState.IN_PROGRESS) { + handleGameEnd(); + } else { + updateTurnIndicator(); } - timeline.play(); } + private void performMove(int row, int col, char player) { + gameBoard.makeMove(row, col, player); + updateCell(row, col, player); + } + + private void scheduleAiTurn() { + disableBoard(); + + PauseTransition pause = new PauseTransition(Duration.millis(500)); + pause.setOnFinished(e -> executeAiMove()); + pause.play(); + } + + private void executeAiMove() { + Move aiMove = gameService.getNextMove(gameBoard, GameBoard.PLAYER_O); + + if (aiMove != null) { + gameService.makeMove(aiMove, GameBoard.PLAYER_O, gameBoard); + performMove(aiMove.getRow(), aiMove.getCol(), GameBoard.PLAYER_O); + } + + if (!checkGameEnd()) { + enableBoard(); + gameBoard.switchPlayer(); + updateTurnIndicator(); + } + } + + + + private boolean checkGameEnd() { GameState state = gameBoard.getGameState(); if (state != GameState.IN_PROGRESS) { @@ -290,11 +356,16 @@ private void updateCell(int row, int col, char player) { } private void updateTurnIndicator() { - char current = gameBoard.getCurrentPlayer(); - boolean isPlayerX = (current == GameBoard.PLAYER_X); - + char currentPlayer = gameBoard.getCurrentPlayer(); + + if (currentPlayer == GameBoard.PLAYER_X) { + setActiveCard(playerCard, opponentCard); + } + else { + setActiveCard(opponentCard, playerCard); + } if (gameMode == GameMode.OFFLINE_PVP || gameMode == GameMode.OFFLINE_PVE || gameMode == GameMode.REPLAY) { - if (isPlayerX) { + if (localPlayerId == GameBoard.PLAYER_X) { setActiveCard(playerCard, opponentCard); } else { setActiveCard(opponentCard, playerCard); @@ -404,8 +475,18 @@ public void resetGame() { } } modalOverlay.setVisible(false); + updateTurnIndicator(); - enableBoard(); + + if (gameMode == GameMode.ONLINE_PVP) { + if (localPlayerId == GameBoard.PLAYER_X) { + enableBoard(); + } else { + disableBoard(); + } + } else { + enableBoard(); + } } private void disableBoard() { @@ -423,4 +504,24 @@ private void enableBoard() { } } } + + private void startReplay() { + disableBoard(); + Timeline timeline = new Timeline(); + int delay = 0; + + for (var move : replayData.moves()) { + delay += 1000; + KeyFrame frame = new KeyFrame(Duration.millis(delay), e -> { + gameBoard.forceMove(move.row(), move.col(), move.player()); + updateCell(move.row(), move.col(), move.player()); + + gameBoard.switchPlayer(); + updateTurnIndicator(); + checkGameEnd(); + }); + timeline.getKeyFrames().add(frame); + } + timeline.play(); + } } \ No newline at end of file diff --git a/src/main/java/com/boredxgames/tictactoeclient/presentation/GameModeScreenController.java b/src/main/java/com/boredxgames/tictactoeclient/presentation/GameModeScreenController.java index 8df99d2..d2f64b4 100644 --- a/src/main/java/com/boredxgames/tictactoeclient/presentation/GameModeScreenController.java +++ b/src/main/java/com/boredxgames/tictactoeclient/presentation/GameModeScreenController.java @@ -35,12 +35,14 @@ public void initialize() { } private void setupActions() { + offlineCard.setOnMouseClicked(e -> { - NavigationManager.navigate(Screens.PVP_SETUP, NavigationAction.REPLACE); + NavigationManager.navigate(Screens.OFFLINE_MODE_SELECTION, NavigationAction.PUSH); }); - + onlineCard.setOnMouseClicked(e -> { NavigationManager.navigate(Screens.SERVER_CONNECTION, NavigationAction.REPLACE_ALL); + }); } diff --git a/src/main/java/com/boredxgames/tictactoeclient/presentation/HomeController.java b/src/main/java/com/boredxgames/tictactoeclient/presentation/HomeController.java new file mode 100644 index 0000000..d7d4851 --- /dev/null +++ b/src/main/java/com/boredxgames/tictactoeclient/presentation/HomeController.java @@ -0,0 +1,143 @@ +package com.boredxgames.tictactoeclient.presentation; + +import com.boredxgames.tictactoeclient.domain.managers.navigation.NavigationAction; +import com.boredxgames.tictactoeclient.domain.managers.navigation.NavigationManager; +import com.boredxgames.tictactoeclient.domain.managers.navigation.Screens; +import com.boredxgames.tictactoeclient.domain.model.*; +import com.boredxgames.tictactoeclient.domain.network.ServerConnectionManager; +import com.boredxgames.tictactoeclient.domain.services.communication.MessageRouter; +import com.boredxgames.tictactoeclient.domain.services.game.OnlinGameSession; + +import java.net.URL; +import java.util.ResourceBundle; +import javafx.application.Platform; +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.fxml.Initializable; +import javafx.scene.control.Alert; +import javafx.scene.control.ButtonType; +import javafx.scene.control.Label; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.Pane; +import javafx.scene.layout.StackPane; + +public class HomeController implements Initializable { + + @FXML private StackPane rootStack; + @FXML private BorderPane mainContent; + @FXML private Pane backgroundPane; + @FXML private Label usernameLabel; + @FXML private Label scoreLabel; + @FXML private PlayersController playersViewController; +@FXML private LeaderboardController leaderboardViewController; + private Alert currentIncomingRequestAlert; + + @Override + public void initialize(URL url, ResourceBundle rb) { + + MessageRouter.setHomeController(this); + + AuthResponseEntity currentUser = ServerConnectionManager.getInstance().getPlayer(); + if (currentUser != null) { + if(usernameLabel != null) usernameLabel.setText(currentUser.getUserName()); + if(scoreLabel != null) scoreLabel.setText("Score: " + currentUser.getScore()); + } + + + OnlinGameSession.getInstance().requestLeaderboard(); + OnlinGameSession.getInstance().requestAvailablePlayers(); + + + } + + public void updatePlayersList(AvailablePlayersInfo info) { + if (playersViewController != null) { + playersViewController.updateList(info); + } + } + + public void handleServerGameResponse(GameResponseInfo info) { + OnlinGameSession.getInstance().setWaiting(false); + + if (info.isAccepted()) { + System.out.println("Opponent Accepted. Waiting for GAME_START event..."); + } else { + Platform.runLater(() -> { + Alert alert = new Alert(Alert.AlertType.INFORMATION); + alert.setTitle("Challenge Declined"); + alert.setHeaderText(null); + alert.setContentText("Player " + info.getResponderUserName()+ " declined your challenge."); + alert.showAndWait(); + + OnlinGameSession.getInstance().requestAvailablePlayers(); + }); + } + } + + public void showIncomingGameRequest(GameRequestInfo info) { + Platform.runLater(() -> { + if (currentIncomingRequestAlert != null && currentIncomingRequestAlert.isShowing()) { + currentIncomingRequestAlert.close(); + } + + Alert alert = new Alert(Alert.AlertType.CONFIRMATION); + alert.setTitle("Incoming Challenge"); + alert.setHeaderText("Game Request"); + alert.setContentText(info.getRequesterUserName()+ " wants to play Tic-Tac-Toe!"); + + ButtonType acceptBtn = new ButtonType("Accept"); + ButtonType declineBtn = new ButtonType("Decline"); + alert.getButtonTypes().setAll(acceptBtn, declineBtn); + + this.currentIncomingRequestAlert = alert; + + alert.showAndWait().ifPresent(type -> { + this.currentIncomingRequestAlert = null; + boolean accepted = (type == acceptBtn); + OnlinGameSession.getInstance().sendGameResponse(info, accepted); + }); + }); + } + + + public void dismissIncomingRequest() { + Platform.runLater(() -> { + if (currentIncomingRequestAlert != null && currentIncomingRequestAlert.isShowing()) { + currentIncomingRequestAlert.setResult(ButtonType.CANCEL); + currentIncomingRequestAlert.close(); + currentIncomingRequestAlert = null; + } + }); + } + + public void showErrorAlert(String message) { + OnlinGameSession.getInstance().setWaiting(false); + Platform.runLater(() -> { + Alert alert = new Alert(Alert.AlertType.ERROR); + alert.setTitle("Error"); + alert.setHeaderText(null); + alert.setContentText(message); + alert.showAndWait(); + + OnlinGameSession.getInstance().requestAvailablePlayers(); + }); + } + + @FXML + private void onSignOut() { + ServerConnectionManager.getInstance().disconnect(); + ServerConnectionManager.getInstance().setPlayer(null); + NavigationManager.navigate(Screens.SERVER_CONNECTION, NavigationAction.REPLACE); + } + + public void updateLeaderboardUI(AvailablePlayersInfo info) { + if (leaderboardViewController != null) { + leaderboardViewController.updateLeaderboard(info); + } +} + @FXML + private void openSettings(ActionEvent event) { + System.out.println("Settings Button Clicked"); + NavigationManager.navigate(Screens.SETTINGS, NavigationAction.PUSH); + } +} \ No newline at end of file diff --git a/src/main/java/com/boredxgames/tictactoeclient/presentation/LeaderboardController.java b/src/main/java/com/boredxgames/tictactoeclient/presentation/LeaderboardController.java new file mode 100644 index 0000000..db87a2d --- /dev/null +++ b/src/main/java/com/boredxgames/tictactoeclient/presentation/LeaderboardController.java @@ -0,0 +1,122 @@ +package com.boredxgames.tictactoeclient.presentation; + +import com.boredxgames.tictactoeclient.domain.model.AuthResponseEntity; +import com.boredxgames.tictactoeclient.domain.model.AvailablePlayersInfo; +import com.boredxgames.tictactoeclient.domain.model.PlayerEntity; +import com.boredxgames.tictactoeclient.domain.network.ServerConnectionManager; +import java.net.URL; +import java.util.ResourceBundle; +import java.util.Vector; +import javafx.application.Platform; +import javafx.fxml.FXML; +import javafx.fxml.Initializable; +import javafx.geometry.Pos; +import javafx.scene.control.Label; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; +import javafx.scene.shape.Circle; + +public class LeaderboardController implements Initializable { + + @FXML private VBox leaderboardContainer; + + @FXML private HBox myRankBar; + @FXML private Label myRankLabel; + @FXML private Label myNameLabel; + @FXML private Label myScoreLabel; + + @Override + public void initialize(URL url, ResourceBundle rb) { + } + + public void updateLeaderboard(AvailablePlayersInfo info) { + Platform.runLater(() -> { + leaderboardContainer.getChildren().clear(); + + Vector allPlayers = new Vector<>(); + if (info.getOnlinePlayers() != null) allPlayers.addAll(info.getOnlinePlayers()); + if (info.getInGamePlayers() != null) allPlayers.addAll(info.getInGamePlayers()); + if (info.getPendingPlayers() != null) allPlayers.addAll(info.getPendingPlayers()); + + allPlayers.sort((p1, p2) -> Integer.compare(p2.getScore(), p1.getScore())); + + AuthResponseEntity me = ServerConnectionManager.getInstance().getPlayer(); + boolean foundMe = false; + + if (me != null && myRankBar != null) { + PlayerEntity myPlayerState = allPlayers.stream() + .filter(p -> p.getId().equals(me.getId())) + .findFirst() + .orElse(null); + + if (myPlayerState != null) { + foundMe = true; + if (myPlayerState.getScore() == 0) { + updateMyFooter("NA", myPlayerState); + } else { + int myRank = allPlayers.indexOf(myPlayerState) + 1; + updateMyFooter(String.valueOf(myRank), myPlayerState); + } + } + myRankBar.setVisible(foundMe); + } + int count = 0; + for (PlayerEntity p : allPlayers) { + if (count >= 5 || p.getScore() <= 0) break; + + leaderboardContainer.getChildren().add(createRow(p, count + 1)); + count++; + } + }); + } + + private void updateMyFooter(String rankText, PlayerEntity p) { + if (myRankLabel != null) myRankLabel.setText(rankText); + if (myNameLabel != null) myNameLabel.setText(p.getUsername()); + if (myScoreLabel != null) myScoreLabel.setText(String.valueOf(p.getScore())); + } + + private HBox createRow(PlayerEntity p, int rank) { + HBox row = new HBox(20); + row.setAlignment(Pos.CENTER_LEFT); + row.getStyleClass().add("leaderboard-row"); + + Label rankLabel = new Label(); + rankLabel.setPrefWidth(60); + + if (rank == 1) { + rankLabel.setText("🏆"); + rankLabel.setStyle("-fx-text-fill: #FFD700; -fx-font-size: 20px;"); + } else if (rank == 2) { + rankLabel.setText("🥈"); + rankLabel.setStyle("-fx-text-fill: #C0C0C0; -fx-font-size: 20px;"); + } else if (rank == 3) { + rankLabel.setText("🥉"); + rankLabel.setStyle("-fx-text-fill: #CD7F32; -fx-font-size: 20px;"); + } else { + rankLabel.setText(String.valueOf(rank)); + rankLabel.setStyle("-fx-text-fill: #94a3b8; -fx-font-size: 14px; -fx-font-weight: bold;"); + } + + HBox nameBox = new HBox(10); + nameBox.setAlignment(Pos.CENTER_LEFT); + HBox.setHgrow(nameBox, Priority.ALWAYS); + + Circle avatar = new Circle(18); + avatar.getStyleClass().add("avatar-profile"); + + Label nameLabel = new Label(p.getUsername()); + nameLabel.setStyle("-fx-text-fill: white; -fx-font-weight: bold; -fx-font-size: 14px;"); + + nameBox.getChildren().addAll(avatar, nameLabel); + + Label scoreLabel = new Label(p.getScore() + " PTS"); + scoreLabel.setPrefWidth(100); + scoreLabel.setAlignment(Pos.CENTER_RIGHT); + scoreLabel.setStyle("-fx-text-fill: #00ff9d; -fx-font-family: 'Monospaced'; -fx-font-weight: bold;"); + + row.getChildren().addAll(rankLabel, nameBox, scoreLabel); + return row; + } +} \ No newline at end of file diff --git a/src/main/java/com/boredxgames/tictactoeclient/presentation/OfflineModeSelectionController.java b/src/main/java/com/boredxgames/tictactoeclient/presentation/OfflineModeSelectionController.java new file mode 100644 index 0000000..99f8601 --- /dev/null +++ b/src/main/java/com/boredxgames/tictactoeclient/presentation/OfflineModeSelectionController.java @@ -0,0 +1,40 @@ +package com.boredxgames.tictactoeclient.presentation; + +import com.boredxgames.tictactoeclient.domain.managers.navigation.NavigationAction; +import com.boredxgames.tictactoeclient.domain.managers.navigation.NavigationManager; +import com.boredxgames.tictactoeclient.domain.managers.navigation.Screens; +import javafx.fxml.Initializable; +import javafx.fxml.FXML; +import javafx.scene.input.MouseEvent; +import javafx.event.ActionEvent; + +import java.net.URL; +import java.util.ResourceBundle; + + +public class OfflineModeSelectionController implements Initializable { + @FXML + @Override + public void initialize(URL url, ResourceBundle resourceBundle) { + + } + @FXML + private void handleSettings(ActionEvent event) { + NavigationManager.navigate(Screens.SETTINGS, NavigationAction.PUSH); + } + @FXML + private void handleBackToMainMenu(ActionEvent event) { + NavigationManager.pop(); + } + + @FXML + private void handlePlayerVsPlayer(MouseEvent event) { + NavigationManager.navigate(Screens.PVP_SETUP, NavigationAction.PUSH); + } + + @FXML + private void handlePlayerVsCPU(MouseEvent event) { + NavigationManager.navigate(Screens.DifficultySelection, NavigationAction.PUSH); + } + +} \ No newline at end of file diff --git a/src/main/java/com/boredxgames/tictactoeclient/presentation/PlayersController.java b/src/main/java/com/boredxgames/tictactoeclient/presentation/PlayersController.java new file mode 100644 index 0000000..a9dddc9 --- /dev/null +++ b/src/main/java/com/boredxgames/tictactoeclient/presentation/PlayersController.java @@ -0,0 +1,119 @@ +package com.boredxgames.tictactoeclient.presentation; + +import com.boredxgames.tictactoeclient.domain.model.AvailablePlayersInfo; +import com.boredxgames.tictactoeclient.domain.model.PlayerEntity; +import com.boredxgames.tictactoeclient.domain.services.game.OnlinGameSession; +import javafx.application.Platform; +import javafx.fxml.FXML; +import javafx.geometry.Pos; +import javafx.scene.control.Alert; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Pane; +import javafx.scene.layout.Priority; +import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; +import javafx.scene.shape.Circle; + +public class PlayersController { + + @FXML + private VBox playersContainer; + + public void updateList(AvailablePlayersInfo info) { + playersContainer.getChildren().clear(); + + if (info.getOnlinePlayers() != null) { + for (PlayerEntity p : info.getOnlinePlayers()) { + playersContainer.getChildren().add(createPlayerCard(p, "ONLINE", "online", true)); + } + } + + if (info.getInGamePlayers() != null) { + for (PlayerEntity p : info.getInGamePlayers()) { + playersContainer.getChildren().add(createPlayerCard(p, "IN GAME", "ingame", false)); + } + } + + if (info.getPendingPlayers() != null) { + for (PlayerEntity p : info.getPendingPlayers()) { + playersContainer.getChildren().add(createPlayerCard(p, "Pending", "pending", false)); + } + } + + if (playersContainer.getChildren().isEmpty()) { + Label placeholder = new Label("No other players connected."); + placeholder.setStyle("-fx-text-fill: #64748b; -fx-font-size: 14px; -fx-padding: 20;"); + playersContainer.getChildren().add(placeholder); + } + } + + private HBox createPlayerCard(PlayerEntity p, String statusText, String type, boolean canChallenge) { + HBox card = new HBox(16); + card.getStyleClass().add("player-card"); + card.setAlignment(Pos.CENTER_LEFT); + + HBox.setHgrow(card, Priority.ALWAYS); + card.setMaxWidth(Double.MAX_VALUE); + + StackPane avatarStack = new StackPane(); + Circle avatarBg = new Circle(24); + avatarBg.setStyle("-fx-fill: #334155;"); + + Circle statusDot = new Circle(6); + statusDot.getStyleClass().add("status-dot-" + type); + StackPane.setAlignment(statusDot, Pos.BOTTOM_RIGHT); + avatarStack.getChildren().addAll(avatarBg, statusDot); + + VBox infoBox = new VBox(2); + infoBox.setAlignment(Pos.CENTER_LEFT); + + Label nameLabel = new Label(p.getUsername()); + nameLabel.getStyleClass().add("player-name"); + + HBox statusLine = new HBox(6); + statusLine.setAlignment(Pos.CENTER_LEFT); + Label statusLabel = new Label(statusText); + statusLabel.getStyleClass().add("status-text-" + type); + Label eloLabel = new Label("• " + p.getScore() + " pts"); + eloLabel.getStyleClass().add("player-subtext"); + + statusLine.getChildren().addAll(statusLabel, eloLabel); + infoBox.getChildren().addAll(nameLabel, statusLine); + + Pane spacer = new Pane(); + HBox.setHgrow(spacer, Priority.ALWAYS); + + Button actionBtn = new Button(); + + if (canChallenge) { + actionBtn.setText("Challenge ⚔"); + actionBtn.getStyleClass().add("btn-challenge"); + + actionBtn.setOnAction(e -> { + System.out.println("response tyepeeeeeeeeeeeeeeeeeee : "+OnlinGameSession.getInstance().isWaiting()); + if (OnlinGameSession.getInstance().isWaiting()) { + Alert alert = new Alert(Alert.AlertType.WARNING); + alert.setTitle("Please Wait"); + alert.setHeaderText(null); + alert.setContentText("You already have a pending request. Please wait for the response."); + alert.showAndWait(); + return; + } + + actionBtn.setDisable(true); + actionBtn.setText("Sent..."); + OnlinGameSession.getInstance().sendGameRequest(p.getId()); + }); + + } else { + actionBtn.setText(statusText); + actionBtn.getStyleClass().add("btn-disabled"); + actionBtn.setDisable(true); + } + + card.getChildren().addAll(avatarStack, infoBox, spacer, actionBtn); + return card; + } +} \ No newline at end of file diff --git a/src/main/java/com/boredxgames/tictactoeclient/presentation/RecordedGamesController.java b/src/main/java/com/boredxgames/tictactoeclient/presentation/RecordedGamesController.java new file mode 100644 index 0000000..1554de2 --- /dev/null +++ b/src/main/java/com/boredxgames/tictactoeclient/presentation/RecordedGamesController.java @@ -0,0 +1,26 @@ +/* + * Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license + * Click nbfs://nbhost/SystemFileSystem/Templates/javafx/FXMLController.java to edit this template + */ +package com.boredxgames.tictactoeclient.presentation; + +import java.net.URL; +import java.util.ResourceBundle; +import javafx.fxml.Initializable; + +/** + * FXML Controller class + * + * @author moham + */ +public class RecordedGamesController implements Initializable { + + /** + * Initializes the controller class. + */ + @Override + public void initialize(URL url, ResourceBundle rb) { + // TODO + } + +} diff --git a/src/main/java/com/boredxgames/tictactoeclient/presentation/Server_connectionController.java b/src/main/java/com/boredxgames/tictactoeclient/presentation/Server_connectionController.java index 4ea4e4f..e4d88b0 100644 --- a/src/main/java/com/boredxgames/tictactoeclient/presentation/Server_connectionController.java +++ b/src/main/java/com/boredxgames/tictactoeclient/presentation/Server_connectionController.java @@ -42,7 +42,7 @@ public void initialize(URL location, ResourceBundle resources) { backgroundPane.getWidth(), backgroundPane.getHeight() ); - + }); serverPresets.put("Local Server", "127.0.0.1"); @@ -52,10 +52,10 @@ public void initialize(URL location, ResourceBundle resources) { ipComboBox.getSelectionModel().select("Local Server"); ipComboBox.setEditable(true); - + } - + @FXML private void connect() { String input = ipComboBox.getValue(); @@ -98,7 +98,7 @@ private void onBackClicked() { stopConnectionThread(); NavigationManager.navigate(Screens.GAME_MODE, NavigationAction.REPLACE_ALL); } - + private void stopConnectionThread() { if (connectionThread != null && connectionThread.isAlive()) { connectionThread.interrupt(); diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index a629653..701ce6e 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -5,16 +5,17 @@ requires java.prefs; requires org.json; requires com.google.gson; - requires java.desktop; +requires java.desktop; requires javafx.media; - opens com.boredxgames.tictactoeclient.domain.managers.audio to javafx.fxml; - opens com.boredxgames.tictactoeclient to javafx.fxml; - opens com.boredxgames.tictactoeclient.domain.services.communication to com.google.gson; - opens com.boredxgames.tictactoeclient.domain.model to com.google.gson; exports com.boredxgames.tictactoeclient; exports com.boredxgames.tictactoeclient.presentation; - opens com.boredxgames.tictactoeclient.presentation to javafx.fxml; exports com.boredxgames.tictactoeclient.domain.managers.state; exports com.boredxgames.tictactoeclient.domain.managers.state.records; + + opens com.boredxgames.tictactoeclient.presentation to javafx.fxml; + opens com.boredxgames.tictactoeclient to com.google.gson, javafx.fxml; + opens com.boredxgames.tictactoeclient.domain.model to com.google.gson; + opens com.boredxgames.tictactoeclient.domain.services.communication to com.google.gson; + } diff --git a/src/main/resources/css/features/authentication_screen.css b/src/main/resources/css/features/authentication_screen.css index adca663..8d82b96 100644 --- a/src/main/resources/css/features/authentication_screen.css +++ b/src/main/resources/css/features/authentication_screen.css @@ -122,3 +122,24 @@ -fx-text-fill: #4f5ef7; -fx-underline: true; } + +.dialog-pane { + -fx-background-color: #2b2b2b; +} + +.dialog-pane .label { + -fx-text-fill: white; +} + +.dialog-pane:header .header-panel { + -fx-background-color: #3c3f41; +} + +.dialog-pane .button { + -fx-background-color: #4a4a4a; + -fx-text-fill: white; +} + +.dialog-pane .button:hover { + -fx-background-color: #5a5a5a; +} \ No newline at end of file diff --git a/src/main/resources/css/features/home.css b/src/main/resources/css/features/home.css new file mode 100644 index 0000000..f264ef9 --- /dev/null +++ b/src/main/resources/css/features/home.css @@ -0,0 +1,239 @@ + +.root { + -fx-background-color: #111022; + -fx-font-family: "Noto Sans", "Segoe UI", sans-serif; + -fx-text-fill: white; + -fx-focus-color: rgba(0,0,0,0); + -fx-faint-focus-color: rgba(0,0,0,0); +} + +.label { + -fx-text-fill: white; +} + + +.scroll-pane, +.scroll-pane .viewport, +.scroll-pane .content { + -fx-background-color: rgba(0,0,0,0) !important; + -fx-background-insets: 0; + -fx-padding: 0; +} + +/* Also target your specific class just in case */ +.edge-to-edge, +.edge-to-edge .viewport, +.edge-to-edge .content { + -fx-background-color: rgba(0,0,0,0) !important; +} + +.flat-tabs { + -fx-background-color: rgba(0,0,0,0); +} + +.flat-tabs .tab-header-area { + -fx-background-color: rgba(0,0,0,0) !important; + -fx-padding: 0; +} + +.flat-tabs .tab-header-background { + -fx-background-color: rgba(0,0,0,0) !important; + -fx-background-insets: 0; +} + +.flat-tabs .headers-region { + -fx-background-color: rgba(0,0,0,0) !important; +} + +.flat-tabs .tab-content-area { + -fx-background-color: rgba(0,0,0,0) !important; + -fx-padding: 0; + -fx-background-insets: 0; +} + +/* 4. TAB BUTTONS */ +.flat-tabs .tab { + -fx-background-color: rgba(0,0,0,0); + -fx-background-insets: 0; + -fx-background-radius: 0; + -fx-padding: 15; + -fx-border-width: 0 0 2 0; + -fx-border-color: rgba(0,0,0,0); +} + +.flat-tabs .tab-label { + -fx-text-fill: #94a3b8; + -fx-font-weight: bold; + -fx-font-size: 14px; +} + +.flat-tabs .tab:hover { + -fx-background-color: rgba(255, 255, 255, 0.05); +} + +.flat-tabs .tab:selected { + -fx-background-color: rgba(0,0,0,0) !important; + -fx-border-color: transparent transparent #463eef transparent; +} + +.flat-tabs .tab:selected .tab-label { + -fx-text-fill: #463eef !important; +} + +.player-card { + -fx-background-color: #1c1b2e; + -fx-background-radius: 16; + -fx-border-color: rgba(255,255,255,0.05); + -fx-border-width: 2px; + -fx-border-radius: 16; + -fx-padding: 10; + -fx-cursor: hand; + -fx-transition: -fx-background-color 0.2s, -fx-border-color 0.2s; +} + +.player-card:hover { + -fx-background-color: #252438 !important; + -fx-border-color: #463eef !important; + -fx-border-width: 2px; /* Must match non-hover width */ +} + +.player-name { -fx-text-fill: white; -fx-font-size: 15px; -fx-font-weight: bold; } +.player-subtext { -fx-text-fill: #94a3b8; -fx-font-size: 11px; } + +/* 6. TOP BAR */ +.top-bar { + -fx-background-color: rgba(28, 27, 46, 0.95); + -fx-padding: 20; + -fx-border-color: rgba(255, 255, 255, 0.1); + -fx-border-width: 0 0 1 0; +} + +.app-icon { -fx-text-fill: #463eef; -fx-font-size: 24px; -fx-font-weight: bold; } +.app-title { -fx-text-fill: white; -fx-font-size: 20px; -fx-font-weight: 700; } +.username { -fx-text-fill: white !important; -fx-font-weight: 700; -fx-font-size: 14px; } +.user-level { -fx-text-fill: #94a3b8; -fx-font-size: 11px; } +.avatar-profile { -fx-stroke: #463eef; -fx-stroke-width: 2; -fx-fill: #2d2d44; } + +.signout-btn { + -fx-background-color: #1c1b2e; + -fx-text-fill: #cbd5e1; + -fx-font-weight: bold; + -fx-background-radius: 8; + -fx-border-color: rgba(255,255,255,0.1); + -fx-border-radius: 8; + -fx-cursor: hand; + -fx-padding: 10; +} +.signout-btn:hover { -fx-background-color: #2a2940; -fx-border-color: rgba(255,255,255,0.2); } + +.quick-match-btn { + -fx-background-color: #463eef; + -fx-text-fill: white; + -fx-font-size: 15px; + -fx-font-weight: bold; + -fx-background-radius: 30; + -fx-padding: 15; + -fx-effect: dropshadow(gaussian, rgba(70, 62, 239, 0.6), 15, 0, 0, 4); + -fx-cursor: hand; +} +.quick-match-btn:hover { -fx-background-color: #6d66f5; } + +.btn-challenge { -fx-background-color: #463eef; -fx-text-fill: white; -fx-font-weight: bold; -fx-background-radius: 8; -fx-cursor: hand; -fx-padding: 8; } +.btn-challenge:hover { -fx-background-color: #6d66f5; } + +.icon-btn { -fx-background-color: rgba(0,0,0,0); -fx-text-fill: #94a3b8; -fx-font-size: 16px; -fx-cursor: hand; } +.icon-btn:hover { -fx-text-fill: white; -fx-background-color: rgba(255,255,255,0.1); -fx-background-radius: 8; } + +.btn-spectate { -fx-background-color: rgba(0,0,0,0); -fx-text-fill: #cbd5e1; -fx-border-color: rgba(255,255,255,0.2); -fx-border-radius: 8; -fx-padding: 8; -fx-font-weight: bold; } +.btn-spectate:hover { -fx-background-color: rgba(255,255,255,0.05); -fx-text-fill: white; } +.btn-disabled { -fx-background-color: rgba(0,0,0,0); -fx-text-fill: #475569; -fx-padding: 8; -fx-border-color: rgba(255,255,255,0.05); -fx-border-radius: 8; } + +/* Status Indicators */ +.status-text-online { -fx-text-fill: #00ff9d; -fx-font-weight: bold; -fx-font-size: 10px; } +.status-text-ingame { -fx-text-fill: #ff9e00; -fx-font-weight: bold; -fx-font-size: 10px; } +.status-text-busy { -fx-text-fill: #64748b; -fx-font-weight: bold; -fx-font-size: 10px; } +.status-dot-online { -fx-fill: #00ff9d; } +.status-dot-ingame { -fx-fill: #ff9e00; } +.status-dot-busy { -fx-fill: #64748b; } + +/* --- SCROLLBAR STYLING --- */ +.modern-scroll .viewport { + -fx-background-color: rgba(0,0,0,0); +} + +.modern-scroll .scroll-bar:vertical { + -fx-background-color: rgba(0,0,0,0); + -fx-pref-width: 12; +} + +.modern-scroll .scroll-bar:vertical .track { + -fx-background-color: rgba(255, 255, 255, 0.05); + -fx-background-radius: 10; +} + +.modern-scroll .scroll-bar:vertical .thumb { + -fx-background-color: #463eef; + -fx-background-radius: 10; + -fx-background-insets: 2; +} + +.modern-scroll .scroll-bar:vertical .thumb:hover { + -fx-background-color: #6d66f5; +} + +.modern-scroll .scroll-bar .increment-button, +.modern-scroll .scroll-bar .decrement-button { + -fx-visible: false; + -fx-padding: 0; + -fx-pref-height: 0; +} + +/* --- RESPONSIVE TWEAKS --- */ +.player-card { + -fx-min-height: 70; + -fx-pref-width: 100%; + -fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.3), 10, 0, 0, 4); +} + +/* --- LEADERBOARD SPECIFIC STYLES --- */ + +.leaderboard-card { + -fx-background-color: #0f0e1e; /* Slightly darker than root */ + -fx-background-radius: 16; + -fx-border-color: rgba(255,255,255,0.05); + -fx-border-radius: 16; + -fx-padding: 0; +} + +.leaderboard-header-row { + -fx-padding: 15 ; + -fx-border-color: rgba(255,255,255,0.05); + -fx-border-width: 0 0 1 0; +} + +.column-header { + -fx-text-fill: #64748b; /* Muted text for headers */ + -fx-font-size: 11px; + -fx-font-weight: bold; + -fx-letter-spacing: 1px; +} + +.leaderboard-row { + -fx-padding: 15 ; + -fx-background-color: transparent; + -fx-border-width: 0 0 1 0; + -fx-border-color: rgba(255,255,255,0.02); + -fx-cursor: hand; +} + +.leaderboard-row:hover { + -fx-background-color: rgba(255,255,255,0.03); +} + +/* The Blue "My Rank" Footer */ +.my-rank-footer { + -fx-background-color: linear-gradient(to right, #463eef, #6d66f5); + -fx-background-radius: 16; + -fx-padding: 15 ; + -fx-effect: dropshadow(three-pass-box, rgba(70, 62, 239, 0.4), 20, 0, 0, 8); +} diff --git a/src/main/resources/css/features/offline_mode_selection.css b/src/main/resources/css/features/offline_mode_selection.css new file mode 100644 index 0000000..870c3ba --- /dev/null +++ b/src/main/resources/css/features/offline_mode_selection.css @@ -0,0 +1,193 @@ +.root { + -offline-card-radius: 24; + -icon-size: 48; + + -text-badge: derive(-text-main, -50%); + + -fx-background-color: -bg-main; + -fx-font-family: 'System'; +} + +.background-effects { + -fx-opacity: 0.15; +} + +.blur-circle-top, .blur-circle-bottom { + -fx-background-color: -primary; + -fx-background-radius: 1000; + -fx-effect: dropshadow(gaussian, -primary, 120, 0.6, 0, 0); +} + +.header { + -fx-border-color: derive(-bg-card, 20%); + -fx-border-width: 0 0 1 0; +} + +.logo-section { + -fx-cursor: hand; + -fx-opacity: 1.0; +} +.logo-section:hover { -fx-opacity: 0.8; } + +.logo-icon-wrapper { + -fx-background-color: -primary-soft; + -fx-background-radius: 12; + -fx-alignment: center; +} + +.logo-icon-wrapper SVGPath { + -fx-fill: -primary; +} + +.logo-text { + -fx-font-size: 18px; + -fx-font-weight: bold; + -fx-text-fill: -text-main; +} + +.icon-button { + -fx-background-color: -bg-card; + -fx-background-radius: 12; + -fx-cursor: hand; + -fx-min-width: 40px; + -fx-min-height: 40px; + -fx-padding: 0; +} +.icon-button:hover { + -fx-border-color: -primary; + -fx-border-width: 1; +} +.icon-button SVGPath { + -fx-fill: -text-main; +} + +.title { + -fx-font-size: 42px; + -fx-font-weight: 800; + -fx-text-fill: -text-main; + -fx-alignment: center; +} +.subtitle { + -fx-font-size: 15px; + -fx-text-fill: -text-muted; + -fx-font-weight: 500; +} + +.game-card { + -fx-background-radius: -offline-card-radius; + -fx-border-radius: -offline-card-radius; + -fx-background-color: -bg-card; + + -fx-cursor: hand; + -fx-effect: dropshadow(three-pass-box, rgba(0, 0, 0, 0.4), 20, 0, 0, 10); + + -fx-border-color: transparent; + -fx-border-width: 2; +} + +.game-card:hover { + -fx-border-color: -primary; + -fx-background-radius: -offline-card-radius; + -fx-border-radius: -offline-card-radius; + -fx-effect: dropshadow(three-pass-box, -primary-soft, 40, 0.2, 0, 0); +} + +.card-bg-image-pvp { + -fx-background-image: url("https://lh3.googleusercontent.com/aida-public/AB6AXuBuw3YJ7m8TLhz3GNAjD8Scf5cQLafn2bp8Qn2o_Ll0YnRvc5Ab_GqZNGey-746vql56GF4b6Sb0UKlABmsHyFQGa2mOfoV1nhTqEWHwqIn3ZPTFy1766m5UtDZBWmPT7H5a4_W3sL8oBT6ZJ74opM1_UsXjR4LR-Hs8VSTtUWD1axZbvQOF6UgXhNW53aKKwV-BU0E91jRE_LkaM9uukR2NIMq35DC3CE2ggLEMvTft7WS62_B5fMAyJ1oTzUDC7Vk-CL4LrZv30EV"); + -fx-background-size: cover; + -fx-background-position: center; + -fx-background-radius: -offline-card-radius; + -fx-background-insets: 0; +} + +.card-bg-image-pve { + -fx-background-image: url("https://lh3.googleusercontent.com/aida-public/AB6AXuD_55qHepZ8p-aFLKxAwRFDeAfTWN8qIelTmbf9QTc6mt5b7JBTox0lLVDujXJT3iQbXoW9F2NKgKgrykD-m9tahZevwoRma9_Ul3pm0Te6Suzje9RtNlAIsJw4o3tIupJ5NfLPGqoFFAs0sg7icvB2sTi4Zdu_4tZAVuCUq6fVujXB6tIYLLGAybHRwF3Y0mPCdWtNYj_TCtu_uOgXabYd_CeW7FP5Uc-ratqZX7ESNF0H6lpNtF91FYYWHaHW9YWDSgPBKw8T2C0t"); + -fx-background-size: cover; + -fx-background-position: center; + -fx-background-radius: -offline-card-radius; + -fx-background-insets: 0; +} + +.card-gradient { + -fx-background-radius: -offline-card-radius; + -fx-background-insets: 0; + -fx-background-color: linear-gradient( + to top, + -bg-main 0%, + derive(-bg-main, 20%) 40%, + derive(-bg-main, 80%) 100% + ); + -fx-opacity: 0.9; +} + +.card-icon-bg { + -fx-background-color: -primary-soft; + -fx-background-radius: 100; + -fx-alignment: center; + + -fx-min-width: -icon-size; + -fx-min-height: -icon-size; + -fx-max-width: -icon-size; + -fx-max-height: -icon-size; +} + +.card-icon-bg SVGPath { + -fx-fill: white; +} + +.game-card:hover .card-icon-bg { + -fx-effect: dropshadow(gaussian, -primary, 10, 0.4, 0, 0); +} + +.card-title { + -fx-font-size: 26px; + -fx-font-weight: bold; + -fx-text-fill: -text-main; +} +.game-card:hover .card-title { + -fx-text-fill: -primary; +} + +.card-description { + -fx-font-size: 14px; + -fx-text-fill: -text-muted; + -fx-font-weight: 500; +} + +.card-footer { + -fx-border-color: derive(-bg-card, 30%); + -fx-border-width: 1 0 0 0; +} + +.card-badge { + -fx-font-size: 11px; + -fx-font-weight: bold; + -fx-text-fill: -text-badge; + -fx-opacity: 0.7; +} + +.action-text { + -fx-font-size: 14px; + -fx-font-weight: bold; + -fx-text-fill: -primary; +} +.card-action SVGPath { + -fx-fill: -primary; +} + +.back-button { + -fx-background-color: transparent; + -fx-padding: 10 20 10 20; + -fx-background-radius: 12; + -fx-cursor: hand; +} +.back-button:hover { + -fx-background-color: -bg-card; +} +.back-button .back-text, .back-button SVGPath { + -fx-text-fill: -text-muted; + -fx-fill: -text-muted; + -fx-font-weight: bold; +} +.back-button:hover .back-text { -fx-text-fill: -primary; } +.back-button:hover SVGPath { -fx-fill: -primary; } \ No newline at end of file diff --git a/src/main/resources/css/themes/green.css b/src/main/resources/css/themes/green.css deleted file mode 100644 index 66c7571..0000000 --- a/src/main/resources/css/themes/green.css +++ /dev/null @@ -1,6 +0,0 @@ -.root { - -color-bg: #0f2a1a; - -color-bg-alt: #163a25; - -color-text: #ffffff; - -color-accent: #4caf50; -} diff --git a/src/main/resources/css/themes/purple.css b/src/main/resources/css/themes/purple.css deleted file mode 100644 index c12155a..0000000 --- a/src/main/resources/css/themes/purple.css +++ /dev/null @@ -1,6 +0,0 @@ -.root { - -color-bg: #2b1e3a; - -color-bg-alt: #3a2752; - -color-text: #ffffff; - -color-accent: #b388ff; -} diff --git a/src/main/resources/css/themes/red.css b/src/main/resources/css/themes/red.css deleted file mode 100644 index 2e18dba..0000000 --- a/src/main/resources/css/themes/red.css +++ /dev/null @@ -1,6 +0,0 @@ -.root { - -color-bg: #2a0f0f; - -color-bg-alt: #3a1515; - -color-text: #ffffff; - -color-accent: #ff5c5c; -} diff --git a/src/main/resources/fxml/home.fxml b/src/main/resources/fxml/home.fxml new file mode 100644 index 0000000..4f1fe7f --- /dev/null +++ b/src/main/resources/fxml/home.fxml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
\ No newline at end of file diff --git a/src/main/resources/fxml/players.fxml b/src/main/resources/fxml/players.fxml new file mode 100644 index 0000000..368f5c6 --- /dev/null +++ b/src/main/resources/fxml/players.fxml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/fxml/recordedgames.fxml b/src/main/resources/fxml/recordedgames.fxml new file mode 100644 index 0000000..dc23334 --- /dev/null +++ b/src/main/resources/fxml/recordedgames.fxml @@ -0,0 +1,12 @@ + + + + + + + \ No newline at end of file