From 5a133151c869facf77c6327b9351599142c35c6d Mon Sep 17 00:00:00 2001 From: odaakj Date: Sat, 21 Mar 2026 13:04:10 +0100 Subject: [PATCH 1/5] Add backend for joining existing sessions --- .../firebase/AndroidFirebaseGateway.java | 20 ++++ .../firebase/FirestoreSessionRepository.java | 90 +++++++++++++++ .../controller/LobbyController.java | 62 +++++++++-- .../beatbattle/firebase/FirebaseGateway.java | 33 ++++++ .../firebase/NoOpFirebaseGateway.java | 29 ++++- .../java/group07/beatbattle/model/Player.java | 4 + .../beatbattle/model/SessionJoinResult.java | 31 ++++++ .../model/services/LobbyService.java | 105 +++++++++++++++++- .../beatbattle/view/JoinCreateView.java | 17 ++- .../group07/beatbattle/view/LobbyView.java | 85 ++++++++++++-- 10 files changed, 449 insertions(+), 27 deletions(-) create mode 100644 core/src/main/java/group07/beatbattle/model/SessionJoinResult.java diff --git a/android/src/main/java/group07/beatbattle/android/firebase/AndroidFirebaseGateway.java b/android/src/main/java/group07/beatbattle/android/firebase/AndroidFirebaseGateway.java index c2a1eb1..35cc78b 100644 --- a/android/src/main/java/group07/beatbattle/android/firebase/AndroidFirebaseGateway.java +++ b/android/src/main/java/group07/beatbattle/android/firebase/AndroidFirebaseGateway.java @@ -43,4 +43,24 @@ public void addHostToPlayers( ) { sessionRepository.addHostToPlayers(sessionId, hostId, hostName, callback); } + + @Override + public void findSessionIdByGamePin(String gamePin, FindSessionCallback callback) { + sessionRepository.findSessionIdByGamePin(gamePin, callback); + } + + @Override + public void addPlayerToSession( + String sessionId, + String playerId, + String playerName, + AddPlayerCallback callback + ) { + sessionRepository.addPlayerToSession(sessionId, playerId, playerName, callback); + } + + @Override + public void listenToPlayers(String sessionId, PlayersListenerCallback callback) { + sessionRepository.listenToPlayers(sessionId, callback); + } } diff --git a/android/src/main/java/group07/beatbattle/android/firebase/FirestoreSessionRepository.java b/android/src/main/java/group07/beatbattle/android/firebase/FirestoreSessionRepository.java index b14d368..e654f65 100644 --- a/android/src/main/java/group07/beatbattle/android/firebase/FirestoreSessionRepository.java +++ b/android/src/main/java/group07/beatbattle/android/firebase/FirestoreSessionRepository.java @@ -2,11 +2,16 @@ import com.google.firebase.Timestamp; import com.google.firebase.firestore.FirebaseFirestore; +import com.google.firebase.firestore.ListenerRegistration; +import com.google.firebase.firestore.QuerySnapshot; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import group07.beatbattle.firebase.FirebaseGateway; +import group07.beatbattle.model.Player; public class FirestoreSessionRepository { @@ -70,4 +75,89 @@ public void addHostToPlayers( .addOnSuccessListener(unused -> callback.onSuccess()) .addOnFailureListener(callback::onFailure); } + + public void findSessionIdByGamePin( + String gamePin, + FirebaseGateway.FindSessionCallback callback + ) { + firestore.collection(SESSIONS) + .whereEqualTo("gamePin", gamePin) + .limit(1) + .get() + .addOnSuccessListener(snapshot -> handleSessionLookup(snapshot, callback)) + .addOnFailureListener(callback::onFailure); + } + + private void handleSessionLookup( + QuerySnapshot snapshot, + FirebaseGateway.FindSessionCallback callback + ) { + if (snapshot.isEmpty()) { + callback.onFailure(new IllegalArgumentException("Invalid game pin")); + return; + } + String sessionId = snapshot.getDocuments().get(0).getId(); + callback.onSuccess(sessionId); + } + + public void addPlayerToSession( + String sessionId, + String playerId, + String playerName, + FirebaseGateway.AddPlayerCallback callback + ) { + Map player = new HashMap<>(); + player.put("name", playerName); + player.put("score", 0); + player.put("isHost", false); + + firestore.collection(SESSIONS) + .document(sessionId) + .collection(PLAYERS) + .document(playerId) + .set(player) + .addOnSuccessListener(unused -> callback.onSuccess()) + .addOnFailureListener(callback::onFailure); + } + + public ListenerRegistration listenToPlayers( + String sessionId, + FirebaseGateway.PlayersListenerCallback callback + ) { + return firestore.collection(SESSIONS) + .document(sessionId) + .collection(PLAYERS) + .addSnapshotListener((snapshot, error) -> { + if (error != null) { + callback.onFailure(error); + return; + } + + if (snapshot == null) { + callback.onPlayersChanged(new ArrayList<>()); + return; + } + + List players = new ArrayList<>(); + + snapshot.getDocuments().forEach(document -> { + String playerId = document.getId(); + String name = document.getString("name"); + if (name == null) { + name = "Unknown"; + } + + Player player = new Player(playerId, name); + + Long scoreValue = document.getLong("score"); + if (scoreValue != null) { + player.setScore(scoreValue.intValue()); + } + + players.add(player); + }); + + callback.onPlayersChanged(players); + }); + } } diff --git a/core/src/main/java/group07/beatbattle/controller/LobbyController.java b/core/src/main/java/group07/beatbattle/controller/LobbyController.java index fe3780e..08c2502 100644 --- a/core/src/main/java/group07/beatbattle/controller/LobbyController.java +++ b/core/src/main/java/group07/beatbattle/controller/LobbyController.java @@ -8,6 +8,7 @@ import group07.beatbattle.model.Player; import group07.beatbattle.model.Question; import group07.beatbattle.model.SessionCreationResult; +import group07.beatbattle.model.SessionJoinResult; import group07.beatbattle.model.Song; import group07.beatbattle.model.services.LobbyService; import group07.beatbattle.states.InRoundState; @@ -42,23 +43,60 @@ public void onReady( int totalRounds, JoinCreateView view ) { - if (mode != GameMode.CREATE) { - StateManager.getInstance().setState(new LobbyState(game, mode, this)); - return; - } - view.setLoadingState(true); - view.setStatusMessage("Creating session..."); LobbyService lobbyService = new LobbyService(game.getFirebaseGateway()); - lobbyService.createSession( + if (mode == GameMode.CREATE) { + view.setStatusMessage("Creating session..."); + + lobbyService.createSession( + playerName, + gamePin, + totalRounds, + new LobbyService.CreateSessionCallback() { + @Override + public void onSuccess(SessionCreationResult result) { + Gdx.app.postRunnable(() -> { + view.setLoadingState(false); + view.setStatusMessage(""); + StateManager.getInstance().setState( + new LobbyState( + game, + mode, + result.getSessionId(), + result.getGamePin(), + result.getHostName(), + LobbyController.this + ) + ); + }); + } + + @Override + public void onFailure(Exception exception) { + Gdx.app.postRunnable(() -> { + view.setLoadingState(false); + view.setStatusMessage( + exception.getMessage() != null + ? exception.getMessage() + : "Failed to create session" + ); + }); + } + } + ); + return; + } + + view.setStatusMessage("Joining session..."); + + lobbyService.joinSession( playerName, gamePin, - totalRounds, - new LobbyService.CreateSessionCallback() { + new LobbyService.JoinSessionCallback() { @Override - public void onSuccess(SessionCreationResult result) { + public void onSuccess(SessionJoinResult result) { Gdx.app.postRunnable(() -> { view.setLoadingState(false); view.setStatusMessage(""); @@ -68,7 +106,7 @@ public void onSuccess(SessionCreationResult result) { mode, result.getSessionId(), result.getGamePin(), - result.getHostName(), + result.getPlayerName(), LobbyController.this ) ); @@ -82,7 +120,7 @@ public void onFailure(Exception exception) { view.setStatusMessage( exception.getMessage() != null ? exception.getMessage() - : "Failed to create session" + : "Failed to join session" ); }); } diff --git a/core/src/main/java/group07/beatbattle/firebase/FirebaseGateway.java b/core/src/main/java/group07/beatbattle/firebase/FirebaseGateway.java index 4ed36d1..dcc30e1 100644 --- a/core/src/main/java/group07/beatbattle/firebase/FirebaseGateway.java +++ b/core/src/main/java/group07/beatbattle/firebase/FirebaseGateway.java @@ -1,5 +1,9 @@ package group07.beatbattle.firebase; +import java.util.List; + +import group07.beatbattle.model.Player; + public interface FirebaseGateway { void gamePinExists(String gamePin, GamePinExistsCallback callback); @@ -20,6 +24,20 @@ void addHostToPlayers( AddHostCallback callback ); + void findSessionIdByGamePin(String gamePin, FindSessionCallback callback); + + void addPlayerToSession( + String sessionId, + String playerId, + String playerName, + AddPlayerCallback callback + ); + + void listenToPlayers( + String sessionId, + PlayersListenerCallback callback + ); + interface GamePinExistsCallback { void onResult(boolean exists); void onFailure(Exception exception); @@ -34,4 +52,19 @@ interface AddHostCallback { void onSuccess(); void onFailure(Exception exception); } + + interface FindSessionCallback { + void onSuccess(String sessionId); + void onFailure(Exception exception); + } + + interface AddPlayerCallback { + void onSuccess(); + void onFailure(Exception exception); + } + + interface PlayersListenerCallback { + void onPlayersChanged(List players); + void onFailure(Exception exception); + } } diff --git a/core/src/main/java/group07/beatbattle/firebase/NoOpFirebaseGateway.java b/core/src/main/java/group07/beatbattle/firebase/NoOpFirebaseGateway.java index 324afc9..6af3658 100644 --- a/core/src/main/java/group07/beatbattle/firebase/NoOpFirebaseGateway.java +++ b/core/src/main/java/group07/beatbattle/firebase/NoOpFirebaseGateway.java @@ -4,7 +4,7 @@ public class NoOpFirebaseGateway implements FirebaseGateway { @Override public void gamePinExists(String gamePin, GamePinExistsCallback callback) { - callback.onResult(false); + callback.onFailure(new UnsupportedOperationException("Firebase is not available on this platform")); } @Override @@ -16,7 +16,7 @@ public void createSession( int totalRounds, CreateSessionCallback callback ) { - callback.onSuccess("desktop-session"); + callback.onFailure(new UnsupportedOperationException("Firebase is not available on this platform")); } @Override @@ -26,6 +26,29 @@ public void addHostToPlayers( String hostName, AddHostCallback callback ) { - callback.onSuccess(); + callback.onFailure(new UnsupportedOperationException("Firebase is not available on this platform")); + } + + @Override + public void findSessionIdByGamePin( + String gamePin, + FindSessionCallback callback + ) { + callback.onFailure(new UnsupportedOperationException("Firebase is not available on this platform")); + } + + @Override + public void addPlayerToSession( + String sessionId, + String playerId, + String playerName, + AddPlayerCallback callback + ) { + callback.onFailure(new UnsupportedOperationException("Firebase is not available on this platform")); + } + + @Override + public void listenToPlayers(String sessionId, PlayersListenerCallback callback) { + callback.onFailure(new UnsupportedOperationException("Firebase is not available on this platform")); } } diff --git a/core/src/main/java/group07/beatbattle/model/Player.java b/core/src/main/java/group07/beatbattle/model/Player.java index 03c294b..c70a580 100644 --- a/core/src/main/java/group07/beatbattle/model/Player.java +++ b/core/src/main/java/group07/beatbattle/model/Player.java @@ -23,4 +23,8 @@ public void addScore(int points) { this.lastRoundScore = points; this.score += points; } + + public void setScore(int score) { + this.score = score; + } } diff --git a/core/src/main/java/group07/beatbattle/model/SessionJoinResult.java b/core/src/main/java/group07/beatbattle/model/SessionJoinResult.java new file mode 100644 index 0000000..9ea6bd4 --- /dev/null +++ b/core/src/main/java/group07/beatbattle/model/SessionJoinResult.java @@ -0,0 +1,31 @@ +package group07.beatbattle.model; + +public class SessionJoinResult { + private final String sessionId; + private final String gamePin; + private final String playerId; + private final String playerName; + + public SessionJoinResult(String sessionId, String gamePin, String playerId, String playerName) { + this.sessionId = sessionId; + this.gamePin = gamePin; + this.playerId = playerId; + this.playerName = playerName; + } + + public String getSessionId() { + return sessionId; + } + + public String getGamePin() { + return gamePin; + } + + public String getPlayerId() { + return playerId; + } + + public String getPlayerName() { + return playerName; + } +} diff --git a/core/src/main/java/group07/beatbattle/model/services/LobbyService.java b/core/src/main/java/group07/beatbattle/model/services/LobbyService.java index 2763c71..4827d53 100644 --- a/core/src/main/java/group07/beatbattle/model/services/LobbyService.java +++ b/core/src/main/java/group07/beatbattle/model/services/LobbyService.java @@ -1,9 +1,12 @@ package group07.beatbattle.model.services; import java.util.Random; +import java.util.List; import group07.beatbattle.firebase.FirebaseGateway; import group07.beatbattle.model.SessionCreationResult; +import group07.beatbattle.model.SessionJoinResult; +import group07.beatbattle.model.Player; public class LobbyService { @@ -12,6 +15,11 @@ public interface CreateSessionCallback { void onFailure(Exception exception); } + public interface JoinSessionCallback { + void onSuccess(SessionJoinResult result); + void onFailure(Exception exception); + } + public interface GamePinCallback { void onSuccess(String gamePin); void onFailure(Exception exception); @@ -103,6 +111,61 @@ public void onFailure(Exception exception) { ); } + public void joinSession( + String playerName, + String gamePin, + JoinSessionCallback callback + ) { + try { + validatePlayerName(playerName); + validateGamePin(gamePin); + } catch (IllegalArgumentException exception) { + callback.onFailure(exception); + return; + } + + String trimmedPlayerName = playerName.trim(); + String normalizedGamePin = normalizeGamePin(gamePin); + String playerId = generatePlayerId(); + + firebaseGateway.findSessionIdByGamePin( + normalizedGamePin, + new FirebaseGateway.FindSessionCallback() { + @Override + public void onSuccess(String sessionId) { + firebaseGateway.addPlayerToSession( + sessionId, + playerId, + trimmedPlayerName, + new FirebaseGateway.AddPlayerCallback() { + @Override + public void onSuccess() { + callback.onSuccess( + new SessionJoinResult( + sessionId, + normalizedGamePin, + playerId, + trimmedPlayerName + ) + ); + } + + @Override + public void onFailure(Exception exception) { + callback.onFailure(exception); + } + } + ); + } + + @Override + public void onFailure(Exception exception) { + callback.onFailure(exception); + } + } + ); + } + private void generateUniqueGamePin(GamePinCallback callback) { String gamePin = generateGamePin(); @@ -123,19 +186,51 @@ public void onFailure(Exception exception) { }); } + public interface PlayersCallback { + void onPlayersChanged(List players); + void onFailure(Exception exception); + } + + public void listenToPlayers(String sessionId, PlayersCallback callback) { + firebaseGateway.listenToPlayers( + sessionId, + new FirebaseGateway.PlayersListenerCallback() { + @Override + public void onPlayersChanged(List players) { + callback.onPlayersChanged(players); + } + + @Override + public void onFailure(Exception exception) { + callback.onFailure(exception); + } + } + ); + } + private void validateHostName(String hostName) { if (hostName == null || hostName.trim().isEmpty()) { throw new IllegalArgumentException("Host name cannot be empty"); } } + private void validatePlayerName(String playerName) { + if (playerName == null || playerName.trim().isEmpty()) { + throw new IllegalArgumentException("Player name cannot be empty"); + } + } + private void validateGamePin(String gamePin) { if (gamePin == null || gamePin.isBlank()) { throw new IllegalArgumentException("Game pin is missing"); } + + String normalized = normalizeGamePin(gamePin); + if (normalized.length() != GAME_PIN_LENGTH) { + throw new IllegalArgumentException("Game pin must be 4 characters"); + } } - private void validateTotalRounds(int totalRounds) { if (totalRounds <= 0) { throw new IllegalArgumentException("Number of rounds must be at least 1"); @@ -145,6 +240,10 @@ private void validateTotalRounds(int totalRounds) { } } + private String normalizeGamePin(String gamePin) { + return gamePin.trim().toUpperCase(); + } + private String generateGamePin() { StringBuilder pinBuilder = new StringBuilder(); @@ -159,4 +258,8 @@ private String generateGamePin() { private String generateHostId() { return "host-" + System.currentTimeMillis(); } + + private String generatePlayerId() { + return "player-" + System.currentTimeMillis(); + } } diff --git a/core/src/main/java/group07/beatbattle/view/JoinCreateView.java b/core/src/main/java/group07/beatbattle/view/JoinCreateView.java index a85a758..e83226e 100644 --- a/core/src/main/java/group07/beatbattle/view/JoinCreateView.java +++ b/core/src/main/java/group07/beatbattle/view/JoinCreateView.java @@ -38,6 +38,7 @@ public class JoinCreateView extends ScreenAdapter { private final LobbyService lobbyService; private TextField playerNameField; + private TextField gameCodeField; private Label statusLabel; private Label gameCodeLabel; private JoinCreateButton readyButton; @@ -68,10 +69,14 @@ public JoinCreateView(BeatBattle game, GameMode mode, LobbyController controller readyButton.addListener(new ChangeListener() { @Override public void changed(ChangeEvent event, Actor actor) { + String gamePinValue = mode == GameMode.CREATE + ? generatedGamePin + : gameCodeField.getText(); + controller.onReady( mode, playerNameField.getText(), - generatedGamePin, + gamePinValue, roundSelector.getSelectedRounds(), JoinCreateView.this ); @@ -129,6 +134,10 @@ public void onFailure(Exception exception) { public void setLoadingState(boolean isLoading) { readyButton.setDisabled(isLoading); playerNameField.setDisabled(isLoading); + + if (gameCodeField != null) { + gameCodeField.setDisabled(isLoading); + } } public void setStatusMessage(String message) { @@ -174,9 +183,9 @@ private Label createGeneratedCodeLabel() { } private TextField createCodeInputField() { - TextField codeField = new TextField("", InputFieldStyles.createDefault(game.getMontserratFont())); - codeField.setMessageText("Enter game code"); - return codeField; + gameCodeField = new TextField("", InputFieldStyles.createDefault(game.getMontserratFont())); + gameCodeField.setMessageText("Enter game code"); + return gameCodeField; } @Override diff --git a/core/src/main/java/group07/beatbattle/view/LobbyView.java b/core/src/main/java/group07/beatbattle/view/LobbyView.java index fac418a..45e3125 100644 --- a/core/src/main/java/group07/beatbattle/view/LobbyView.java +++ b/core/src/main/java/group07/beatbattle/view/LobbyView.java @@ -16,9 +16,13 @@ import com.badlogic.gdx.utils.Align; import com.badlogic.gdx.utils.viewport.ScreenViewport; +import java.util.List; + import group07.beatbattle.BeatBattle; import group07.beatbattle.controller.LobbyController; import group07.beatbattle.model.GameMode; +import group07.beatbattle.model.Player; +import group07.beatbattle.model.services.LobbyService; import group07.beatbattle.ui.components.BackButton; import group07.beatbattle.ui.components.SettingsButton; import group07.beatbattle.ui.components.StartGameButton; @@ -29,6 +33,7 @@ public class LobbyView extends ScreenAdapter { private final GameMode mode; private final LobbyController controller; private final Stage stage; + private final LobbyService lobbyService; private final String sessionId; private final String gamePin; @@ -37,6 +42,9 @@ public class LobbyView extends ScreenAdapter { private Label.LabelStyle infoStyle; private Label.LabelStyle titleStyle; + private Table playersGrid; + private Label statusLabel; + public LobbyView(BeatBattle game, GameMode mode, LobbyController controller) { this(game, mode, "", "", "", controller); } @@ -46,6 +54,7 @@ public LobbyView(BeatBattle game, GameMode mode, String sessionId, String gamePi this.mode = mode; this.controller = controller; this.stage = new Stage(new ScreenViewport()); + this.lobbyService = new LobbyService(game.getFirebaseGateway()); this.sessionId = sessionId; this.gamePin = gamePin; this.hostName = hostName; @@ -69,6 +78,35 @@ public LobbyView(BeatBattle game, GameMode mode, String sessionId, String gamePi root.add(createFooter()).expandX().bottom().padBottom(40); stage.addActor(root); + + startListeningToPlayers(); + } + + private void startListeningToPlayers() { + if (sessionId == null || sessionId.isBlank()) { + setStatusMessage("Missing session id"); + return; + } + + lobbyService.listenToPlayers( + sessionId, + new LobbyService.PlayersCallback() { + @Override + public void onPlayersChanged(List players) { + Gdx.app.postRunnable(() -> { + updatePlayersGrid(players); + setStatusMessage(""); + }); + } + + @Override + public void onFailure(Exception exception) { + Gdx.app.postRunnable(() -> { + setStatusMessage("Failed to load players"); + }); + } + } + ); } private Table createHeader() { @@ -147,20 +185,53 @@ private Table createPartyBox() { Table container = new Table(); container.setBackground(createRoundedBackground(boxWidth, boxHeight, radius)); - Table grid = new Table(); + Table content = new Table(); + + playersGrid = new Table(); + statusLabel = new Label("Loading players...", infoStyle); + statusLabel.setAlignment(Align.center); + + content.add(playersGrid).expand().center().row(); + content.add(statusLabel).padTop(20); + + container.add(content).expand().fill().pad(60); + + return container; + } + + private void updatePlayersGrid(List players) { + playersGrid.clear(); float cellWidth = 260f; float cellHeight = 60f; - grid.add(createPlayerLabel("Oda")).width(cellWidth).height(cellHeight).pad(15); - grid.add(createPlayerLabel("Lea")).width(cellWidth).height(cellHeight).pad(15).row(); + if (players == null || players.isEmpty()) { + playersGrid.add(createPlayerLabel("No players yet")) + .width(cellWidth) + .height(cellHeight) + .pad(15); + return; + } - grid.add(createPlayerLabel("Milos")).width(cellWidth).height(cellHeight).pad(15); - grid.add(createPlayerLabel("Karya")).width(cellWidth).height(cellHeight).pad(15); + int columnCount = 2; + for (int i = 0; i < players.size(); i++) { + Player player = players.get(i); - container.add(grid).pad(60); + playersGrid.add(createPlayerLabel(player.getName())) + .width(cellWidth) + .height(cellHeight) + .pad(15); - return container; + if ((i + 1) % columnCount == 0) { + playersGrid.row(); + } + } + } + + private void setStatusMessage(String message) { + if (statusLabel != null) { + statusLabel.setText(message); + } } private Label createPlayerLabel(String name) { From 60adf51e84b5ebea9a707fad0b3d4c6f17ccb7cf Mon Sep 17 00:00:00 2001 From: odaakj Date: Sat, 21 Mar 2026 14:16:16 +0100 Subject: [PATCH 2/5] Ensure session is deleted when host leaves, and that player is removed from session if player leaves --- .../firebase/AndroidFirebaseGateway.java | 17 +++++ .../firebase/FirestoreSessionRepository.java | 71 ++++++++++++++++++- .../controller/LobbyController.java | 67 +++++++++++++---- .../beatbattle/firebase/FirebaseGateway.java | 21 ++++++ .../firebase/NoOpFirebaseGateway.java | 17 +++++ .../java/group07/beatbattle/model/Player.java | 8 ++- .../model/services/LobbyService.java | 56 +++++++++++++++ .../group07/beatbattle/states/LobbyState.java | 16 ++--- .../group07/beatbattle/view/LobbyView.java | 46 +++++++++--- 9 files changed, 286 insertions(+), 33 deletions(-) diff --git a/android/src/main/java/group07/beatbattle/android/firebase/AndroidFirebaseGateway.java b/android/src/main/java/group07/beatbattle/android/firebase/AndroidFirebaseGateway.java index 35cc78b..ae3c036 100644 --- a/android/src/main/java/group07/beatbattle/android/firebase/AndroidFirebaseGateway.java +++ b/android/src/main/java/group07/beatbattle/android/firebase/AndroidFirebaseGateway.java @@ -63,4 +63,21 @@ public void addPlayerToSession( public void listenToPlayers(String sessionId, PlayersListenerCallback callback) { sessionRepository.listenToPlayers(sessionId, callback); } + + @Override + public void removePlayerFromSession( + String sessionId, + String playerId, + RemovePlayerCallback callback + ) { + sessionRepository.removePlayerFromSession(sessionId, playerId, callback); + } + + @Override + public void deleteSession( + String sessionId, + DeleteSessionCallback callback + ) { + sessionRepository.deleteSession(sessionId, callback); + } } diff --git a/android/src/main/java/group07/beatbattle/android/firebase/FirestoreSessionRepository.java b/android/src/main/java/group07/beatbattle/android/firebase/FirestoreSessionRepository.java index e654f65..8b575a4 100644 --- a/android/src/main/java/group07/beatbattle/android/firebase/FirestoreSessionRepository.java +++ b/android/src/main/java/group07/beatbattle/android/firebase/FirestoreSessionRepository.java @@ -147,7 +147,8 @@ public ListenerRegistration listenToPlayers( name = "Unknown"; } - Player player = new Player(playerId, name); + boolean isHost = Boolean.TRUE.equals(document.getBoolean("isHost")); + Player player = new Player(playerId, name, isHost); Long scoreValue = document.getLong("score"); if (scoreValue != null) { @@ -160,4 +161,72 @@ public ListenerRegistration listenToPlayers( callback.onPlayersChanged(players); }); } + + public void removePlayerFromSession( + String sessionId, + String playerId, + FirebaseGateway.RemovePlayerCallback callback + ) { + firestore.collection(SESSIONS) + .document(sessionId) + .collection(PLAYERS) + .document(playerId) + .delete() + .addOnSuccessListener(unused -> callback.onSuccess()) + .addOnFailureListener(callback::onFailure); + } + + public void deleteSession( + String sessionId, + FirebaseGateway.DeleteSessionCallback callback + ) { + firestore.collection(SESSIONS) + .document(sessionId) + .collection(PLAYERS) + .get() + .addOnSuccessListener(snapshot -> { + if (snapshot.isEmpty()) { + deleteSessionDocument(sessionId, callback); + return; + } + + final int totalDocuments = snapshot.size(); + final int[] deletedDocuments = {0}; + final boolean[] failed = {false}; + + snapshot.getDocuments().forEach(document -> { + document.getReference() + .delete() + .addOnSuccessListener(unused -> { + if (failed[0]) { + return; + } + + deletedDocuments[0]++; + + if (deletedDocuments[0] == totalDocuments) { + deleteSessionDocument(sessionId, callback); + } + }) + .addOnFailureListener(exception -> { + if (!failed[0]) { + failed[0] = true; + callback.onFailure(exception); + } + }); + }); + }) + .addOnFailureListener(callback::onFailure); + } + + private void deleteSessionDocument( + String sessionId, + FirebaseGateway.DeleteSessionCallback callback + ) { + firestore.collection(SESSIONS) + .document(sessionId) + .delete() + .addOnSuccessListener(unused -> callback.onSuccess()) + .addOnFailureListener(callback::onFailure); + } } diff --git a/core/src/main/java/group07/beatbattle/controller/LobbyController.java b/core/src/main/java/group07/beatbattle/controller/LobbyController.java index 08c2502..96c5754 100644 --- a/core/src/main/java/group07/beatbattle/controller/LobbyController.java +++ b/core/src/main/java/group07/beatbattle/controller/LobbyController.java @@ -17,6 +17,7 @@ import group07.beatbattle.states.StartState; import group07.beatbattle.states.StateManager; import group07.beatbattle.view.JoinCreateView; +import group07.beatbattle.view.LobbyView; import java.util.Arrays; @@ -66,7 +67,9 @@ public void onSuccess(SessionCreationResult result) { mode, result.getSessionId(), result.getGamePin(), + result.getHostId(), result.getHostName(), + true, LobbyController.this ) ); @@ -106,7 +109,9 @@ public void onSuccess(SessionJoinResult result) { mode, result.getSessionId(), result.getGamePin(), + result.getPlayerId(), result.getPlayerName(), + false, LobbyController.this ) ); @@ -132,25 +137,61 @@ public void onBackFromJoinCreate() { StateManager.getInstance().setState(new StartState(game, this)); } - public void onBackFromLobby(GameMode mode) { - StateManager.getInstance().setState(new JoinCreateState(game, mode, this)); + public void onBackFromLobby( + GameMode mode, + String sessionId, + String playerId, + boolean isHost, + LobbyView view + ) { + view.setStatusMessage("Leaving lobby..."); + + LobbyService lobbyService = new LobbyService(game.getFirebaseGateway()); + + lobbyService.leaveLobby( + sessionId, + playerId, + isHost, + new LobbyService.LeaveLobbyCallback() { + @Override + public void onSuccess() { + Gdx.app.postRunnable(() -> + StateManager.getInstance().setState( + new JoinCreateState(game, mode, LobbyController.this) + ) + ); + } + + @Override + public void onFailure(Exception exception) { + Gdx.app.postRunnable(() -> { + view.setStatusMessage( + exception.getMessage() != null + ? exception.getMessage() + : "Failed to leave lobby" + ); + view.setLeavingLobby(false); + }); + } + } + ); } public void onStartGame() { // TODO: replace mock session with real data from Firebase/Deezer GameSession session = new GameSession("123456", "host1", 2); - session.addPlayer(new Player("p1", "You")); - session.addPlayer(new Player("p2", "Oda")); - session.addPlayer(new Player("p3", "Lea")); - session.addPlayer(new Player("p4", "Milos")); - session.addPlayer(new Player("p5", "Emma")); - session.addPlayer(new Player("p6", "Lucas")); - session.addPlayer(new Player("p7", "Sofia")); - session.addPlayer(new Player("p8", "Noah")); - session.addPlayer(new Player("p9", "Mia")); - session.addPlayer(new Player("p10", "Elias")); - session.addPlayer(new Player("p11", "Nora")); + //session.addPlayer(new Player("p1", "You")); + //session.addPlayer(new Player("p2", "Oda")); + //session.addPlayer(new Player("p3", "Lea")); + //session.addPlayer(new Player("p4", "Milos")); + //session.addPlayer(new Player("p5", "Emma")); + //session.addPlayer(new Player("p6", "Lucas")); + //session.addPlayer(new Player("p7", "Sofia")); + //session.addPlayer(new Player("p8", "Noah")); + //session.addPlayer(new Player("p9", "Mia")); + //session.addPlayer(new Player("p10", "Elias")); + //session.addPlayer(new Player("p11", "Nora")); Song song1 = new Song("1", "Smells Like Teen Spirit", "Nirvana", "", ""); session.addQuestion(new Question("q1", song1, Arrays.asList( diff --git a/core/src/main/java/group07/beatbattle/firebase/FirebaseGateway.java b/core/src/main/java/group07/beatbattle/firebase/FirebaseGateway.java index dcc30e1..0098e4c 100644 --- a/core/src/main/java/group07/beatbattle/firebase/FirebaseGateway.java +++ b/core/src/main/java/group07/beatbattle/firebase/FirebaseGateway.java @@ -38,6 +38,17 @@ void listenToPlayers( PlayersListenerCallback callback ); + void removePlayerFromSession( + String sessionId, + String playerId, + RemovePlayerCallback callback + ); + + void deleteSession( + String sessionId, + DeleteSessionCallback callback + ); + interface GamePinExistsCallback { void onResult(boolean exists); void onFailure(Exception exception); @@ -67,4 +78,14 @@ interface PlayersListenerCallback { void onPlayersChanged(List players); void onFailure(Exception exception); } + + interface RemovePlayerCallback { + void onSuccess(); + void onFailure(Exception exception); + } + + interface DeleteSessionCallback { + void onSuccess(); + void onFailure(Exception exception); + } } diff --git a/core/src/main/java/group07/beatbattle/firebase/NoOpFirebaseGateway.java b/core/src/main/java/group07/beatbattle/firebase/NoOpFirebaseGateway.java index 6af3658..435204d 100644 --- a/core/src/main/java/group07/beatbattle/firebase/NoOpFirebaseGateway.java +++ b/core/src/main/java/group07/beatbattle/firebase/NoOpFirebaseGateway.java @@ -51,4 +51,21 @@ public void addPlayerToSession( public void listenToPlayers(String sessionId, PlayersListenerCallback callback) { callback.onFailure(new UnsupportedOperationException("Firebase is not available on this platform")); } + + @Override + public void removePlayerFromSession( + String sessionId, + String playerId, + RemovePlayerCallback callback + ) { + callback.onFailure(new UnsupportedOperationException("Firebase is not available on this platform")); + } + + @Override + public void deleteSession( + String sessionId, + DeleteSessionCallback callback + ) { + callback.onFailure(new UnsupportedOperationException("Firebase is not available on this platform")); + } } diff --git a/core/src/main/java/group07/beatbattle/model/Player.java b/core/src/main/java/group07/beatbattle/model/Player.java index c70a580..3df88f7 100644 --- a/core/src/main/java/group07/beatbattle/model/Player.java +++ b/core/src/main/java/group07/beatbattle/model/Player.java @@ -4,16 +4,22 @@ public class Player { private final String id; private final String name; + private final boolean isHost; private int score; private int lastRoundScore; - public Player(String id, String name) { + public Player(String id, String name, boolean isHost) { this.id = id; this.name = name; + this.isHost = isHost; this.score = 0; this.lastRoundScore = 0; } + public boolean isHost() { + return isHost; + } + public String getId() { return id; } public String getName() { return name; } public int getScore() { return score; } diff --git a/core/src/main/java/group07/beatbattle/model/services/LobbyService.java b/core/src/main/java/group07/beatbattle/model/services/LobbyService.java index 4827d53..c1cbbf2 100644 --- a/core/src/main/java/group07/beatbattle/model/services/LobbyService.java +++ b/core/src/main/java/group07/beatbattle/model/services/LobbyService.java @@ -25,6 +25,11 @@ public interface GamePinCallback { void onFailure(Exception exception); } + public interface LeaveLobbyCallback { + void onSuccess(); + void onFailure(Exception exception); + } + private static final int GAME_PIN_LENGTH = 4; private static final String PIN_CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; private static final int DEFAULT_TOTAL_ROUNDS = 20; @@ -166,6 +171,57 @@ public void onFailure(Exception exception) { ); } + public void leaveLobby( + String sessionId, + String playerId, + boolean isHost, + LeaveLobbyCallback callback + ) { + if (sessionId == null || sessionId.isBlank()) { + callback.onFailure(new IllegalArgumentException("Session id is missing")); + return; + } + + if (isHost) { + firebaseGateway.deleteSession( + sessionId, + new FirebaseGateway.DeleteSessionCallback() { + @Override + public void onSuccess() { + callback.onSuccess(); + } + + @Override + public void onFailure(Exception exception) { + callback.onFailure(exception); + } + } + ); + return; + } + + if (playerId == null || playerId.isBlank()) { + callback.onFailure(new IllegalArgumentException("Player id is missing")); + return; + } + + firebaseGateway.removePlayerFromSession( + sessionId, + playerId, + new FirebaseGateway.RemovePlayerCallback() { + @Override + public void onSuccess() { + callback.onSuccess(); + } + + @Override + public void onFailure(Exception exception) { + callback.onFailure(exception); + } + } + ); + } + private void generateUniqueGamePin(GamePinCallback callback) { String gamePin = generateGamePin(); diff --git a/core/src/main/java/group07/beatbattle/states/LobbyState.java b/core/src/main/java/group07/beatbattle/states/LobbyState.java index 7763494..7ed79a8 100644 --- a/core/src/main/java/group07/beatbattle/states/LobbyState.java +++ b/core/src/main/java/group07/beatbattle/states/LobbyState.java @@ -9,18 +9,14 @@ public class LobbyState extends State { private final LobbyView view; - // Gammel konstruktør (fallback / join uten data) - public LobbyState(BeatBattle game, GameMode mode, LobbyController controller) { - this(game, mode, "", "", "", controller); - } - - // Ny konstruktør (med Firebase-data) public LobbyState( BeatBattle game, GameMode mode, String sessionId, String gamePin, - String hostName, + String playerId, + String displayName, + boolean isHost, LobbyController controller ) { super(game); @@ -30,7 +26,9 @@ public LobbyState( mode, sessionId, gamePin, - hostName, + playerId, + displayName, + isHost, controller ); } @@ -42,6 +40,6 @@ public void enter() { @Override public void exit() { - // Ingen cleanup nødvendig nå + // Ingen cleanup her - cleanup skjer når bruker forlater lobby aktivt } } diff --git a/core/src/main/java/group07/beatbattle/view/LobbyView.java b/core/src/main/java/group07/beatbattle/view/LobbyView.java index 45e3125..8c4fd18 100644 --- a/core/src/main/java/group07/beatbattle/view/LobbyView.java +++ b/core/src/main/java/group07/beatbattle/view/LobbyView.java @@ -38,6 +38,9 @@ public class LobbyView extends ScreenAdapter { private final String sessionId; private final String gamePin; private final String hostName; + private final String playerId; + private final boolean isHost; + private boolean leavingLobby; private Label.LabelStyle infoStyle; private Label.LabelStyle titleStyle; @@ -45,11 +48,16 @@ public class LobbyView extends ScreenAdapter { private Table playersGrid; private Label statusLabel; - public LobbyView(BeatBattle game, GameMode mode, LobbyController controller) { - this(game, mode, "", "", "", controller); - } - - public LobbyView(BeatBattle game, GameMode mode, String sessionId, String gamePin, String hostName, LobbyController controller) { + public LobbyView( + BeatBattle game, + GameMode mode, + String sessionId, + String gamePin, + String playerId, + String displayName, + boolean isHost, + LobbyController controller + ) { this.game = game; this.mode = mode; this.controller = controller; @@ -57,7 +65,10 @@ public LobbyView(BeatBattle game, GameMode mode, String sessionId, String gamePi this.lobbyService = new LobbyService(game.getFirebaseGateway()); this.sessionId = sessionId; this.gamePin = gamePin; - this.hostName = hostName; + this.playerId = playerId; + this.hostName = displayName; + this.isHost = isHost; + this.leavingLobby = false; // Styles titleStyle = new Label.LabelStyle(); @@ -120,7 +131,18 @@ private Table createHeader() { backButton.addListener(new ChangeListener() { @Override public void changed(ChangeEvent event, Actor actor) { - controller.onBackFromLobby(mode); + if (leavingLobby) { + return; + } + leavingLobby = true; + + controller.onBackFromLobby( + mode, + sessionId, + playerId, + isHost, + LobbyView.this + ); } }); @@ -217,7 +239,9 @@ private void updatePlayersGrid(List players) { for (int i = 0; i < players.size(); i++) { Player player = players.get(i); - playersGrid.add(createPlayerLabel(player.getName())) + String displayName = player.getName() + (player.isHost() ? " [H]" : ""); + + playersGrid.add(createPlayerLabel(displayName)) .width(cellWidth) .height(cellHeight) .pad(15); @@ -228,7 +252,7 @@ private void updatePlayersGrid(List players) { } } - private void setStatusMessage(String message) { + public void setStatusMessage(String message) { if (statusLabel != null) { statusLabel.setText(message); } @@ -280,6 +304,10 @@ public void changed(ChangeEvent event, Actor actor) { return footer; } + public void setLeavingLobby(boolean leavingLobby) { + this.leavingLobby = leavingLobby; + } + public String getSessionId() { return sessionId; } From 19ac282eb82137864a50c724bcdeb125af224aae Mon Sep 17 00:00:00 2001 From: odaakj Date: Sat, 21 Mar 2026 14:47:12 +0100 Subject: [PATCH 3/5] Add alerts for leaving session and invalid game pin --- .../controller/LobbyController.java | 25 +++- .../beatbattle/ui/dialog/AlertDialogs.java | 123 ++++++++++++++++++ .../beatbattle/view/JoinCreateView.java | 5 + .../group07/beatbattle/view/LobbyView.java | 35 +++-- 4 files changed, 174 insertions(+), 14 deletions(-) create mode 100644 core/src/main/java/group07/beatbattle/ui/dialog/AlertDialogs.java diff --git a/core/src/main/java/group07/beatbattle/controller/LobbyController.java b/core/src/main/java/group07/beatbattle/controller/LobbyController.java index 96c5754..b148c96 100644 --- a/core/src/main/java/group07/beatbattle/controller/LobbyController.java +++ b/core/src/main/java/group07/beatbattle/controller/LobbyController.java @@ -122,11 +122,26 @@ public void onSuccess(SessionJoinResult result) { public void onFailure(Exception exception) { Gdx.app.postRunnable(() -> { view.setLoadingState(false); - view.setStatusMessage( - exception.getMessage() != null - ? exception.getMessage() - : "Failed to join session" - ); + + String message = exception.getMessage() != null + ? exception.getMessage() + : "Failed to join session"; + + String normalizedMessage = message.toLowerCase(); + + if (normalizedMessage.contains("invalid game pin") + || normalizedMessage.contains("game pin is missing") + || normalizedMessage.contains("game pin must be 4 characters")) { + + view.setStatusMessage(""); + view.showInfoAlert( + "Unable to join session", + message + ); + return; + } + + view.setStatusMessage(message); }); } } diff --git a/core/src/main/java/group07/beatbattle/ui/dialog/AlertDialogs.java b/core/src/main/java/group07/beatbattle/ui/dialog/AlertDialogs.java new file mode 100644 index 0000000..cf4cbcf --- /dev/null +++ b/core/src/main/java/group07/beatbattle/ui/dialog/AlertDialogs.java @@ -0,0 +1,123 @@ +package group07.beatbattle.ui.dialog; + +import com.badlogic.gdx.graphics.Color; +import com.badlogic.gdx.graphics.Pixmap; +import com.badlogic.gdx.graphics.Texture; +import com.badlogic.gdx.graphics.g2d.BitmapFont; +import com.badlogic.gdx.graphics.g2d.TextureRegion; +import com.badlogic.gdx.scenes.scene2d.Stage; +import com.badlogic.gdx.scenes.scene2d.ui.Dialog; +import com.badlogic.gdx.scenes.scene2d.ui.Label; +import com.badlogic.gdx.scenes.scene2d.ui.Skin; +import com.badlogic.gdx.scenes.scene2d.ui.TextButton; +import com.badlogic.gdx.scenes.scene2d.ui.Window; +import com.badlogic.gdx.scenes.scene2d.utils.Drawable; +import com.badlogic.gdx.scenes.scene2d.utils.TextureRegionDrawable; + +public final class AlertDialogs { + + private AlertDialogs() { + } + + public static void showInfo( + Stage stage, + BitmapFont font, + String title, + String message + ) { + Skin skin = createSkin(font); + + Dialog dialog = new Dialog(title, skin); + dialog.text(new Label(message, skin)); + dialog.button("OK"); + + styleDialog(dialog); + dialog.show(stage); + } + + public static void showConfirmation( + Stage stage, + BitmapFont font, + String title, + String message, + Runnable onConfirm + ) { + Skin skin = createSkin(font); + + Dialog dialog = new Dialog(title, skin) { + @Override + protected void result(Object object) { + boolean confirmed = Boolean.TRUE.equals(object); + if (confirmed && onConfirm != null) { + onConfirm.run(); + } + } + }; + + dialog.text(new Label(message, skin)); + dialog.button("Cancel", false); + dialog.button("Confirm", true); + + styleDialog(dialog); + dialog.show(stage); + } + + private static void styleDialog(Dialog dialog) { + dialog.getContentTable().pad(30); + dialog.getButtonTable().pad(20); + dialog.setModal(true); + dialog.setMovable(false); + dialog.setResizable(false); + } + + private static Skin createSkin(BitmapFont font) { + Skin skin = new Skin(); + + Window.WindowStyle windowStyle = new Window.WindowStyle(); + windowStyle.titleFont = font; + windowStyle.titleFontColor = Color.WHITE; + windowStyle.background = createRoundedBackground(700, 320, 40, 0.18f, 0.18f, 0.28f, 1f); + + Label.LabelStyle labelStyle = new Label.LabelStyle(); + labelStyle.font = font; + labelStyle.fontColor = Color.WHITE; + + TextButton.TextButtonStyle buttonStyle = new TextButton.TextButtonStyle(); + buttonStyle.font = font; + buttonStyle.fontColor = Color.WHITE; + buttonStyle.up = createRoundedBackground(260, 90, 30, 0.28f, 0.28f, 0.40f, 1f); + buttonStyle.down = createRoundedBackground(260, 90, 30, 0.20f, 0.20f, 0.32f, 1f); + + skin.add("default", windowStyle); + skin.add("default", labelStyle); + skin.add("default", buttonStyle); + + return skin; + } + + private static Drawable createRoundedBackground( + int width, + int height, + int radius, + float r, + float g, + float b, + float a + ) { + Pixmap pixmap = new Pixmap(width, height, Pixmap.Format.RGBA8888); + pixmap.setColor(r, g, b, a); + + pixmap.fillRectangle(radius, 0, width - 2 * radius, height); + pixmap.fillRectangle(0, radius, width, height - 2 * radius); + + pixmap.fillCircle(radius, radius, radius); + pixmap.fillCircle(width - radius, radius, radius); + pixmap.fillCircle(radius, height - radius, radius); + pixmap.fillCircle(width - radius, height - radius, radius); + + Texture texture = new Texture(pixmap); + pixmap.dispose(); + + return new TextureRegionDrawable(new TextureRegion(texture)); + } +} diff --git a/core/src/main/java/group07/beatbattle/view/JoinCreateView.java b/core/src/main/java/group07/beatbattle/view/JoinCreateView.java index e83226e..8ab6f85 100644 --- a/core/src/main/java/group07/beatbattle/view/JoinCreateView.java +++ b/core/src/main/java/group07/beatbattle/view/JoinCreateView.java @@ -20,6 +20,7 @@ import group07.beatbattle.ui.components.BackButton; import group07.beatbattle.ui.components.JoinCreateButton; import group07.beatbattle.ui.components.RoundSelector; +import group07.beatbattle.ui.dialog.AlertDialogs; import group07.beatbattle.ui.style.InputFieldStyles; public class JoinCreateView extends ScreenAdapter { @@ -144,6 +145,10 @@ public void setStatusMessage(String message) { statusLabel.setText(message); } + public void showInfoAlert(String title, String message) { + AlertDialogs.showInfo(stage, game.getMontserratFont(), title, message); + } + private Label createTitleLabel() { Label.LabelStyle titleStyle = new Label.LabelStyle(); titleStyle.font = game.getOrbitronFont(); diff --git a/core/src/main/java/group07/beatbattle/view/LobbyView.java b/core/src/main/java/group07/beatbattle/view/LobbyView.java index 8c4fd18..5467ed8 100644 --- a/core/src/main/java/group07/beatbattle/view/LobbyView.java +++ b/core/src/main/java/group07/beatbattle/view/LobbyView.java @@ -26,6 +26,7 @@ import group07.beatbattle.ui.components.BackButton; import group07.beatbattle.ui.components.SettingsButton; import group07.beatbattle.ui.components.StartGameButton; +import group07.beatbattle.ui.dialog.AlertDialogs; public class LobbyView extends ScreenAdapter { @@ -134,15 +135,7 @@ public void changed(ChangeEvent event, Actor actor) { if (leavingLobby) { return; } - leavingLobby = true; - - controller.onBackFromLobby( - mode, - sessionId, - playerId, - isHost, - LobbyView.this - ); + showLeaveLobbyConfirmation(); } }); @@ -168,6 +161,30 @@ public void changed(ChangeEvent event, Actor actor) { return header; } + private void showLeaveLobbyConfirmation() { + String message = isHost + ? "Leaving will delete the session for everyone.\nDo you want to continue?" + : "Are you sure you want to leave the session?"; + + AlertDialogs.showConfirmation( + stage, + game.getMontserratFont(), + "Leave lobby", + message, + () -> { + leavingLobby = true; + + controller.onBackFromLobby( + mode, + sessionId, + playerId, + isHost, + LobbyView.this + ); + } + ); + } + private Table createGameCodeSection() { Table table = new Table(); From 96e63f66469ab68501b58240f97d5d77fda5855b Mon Sep 17 00:00:00 2001 From: odaakj Date: Sat, 21 Mar 2026 15:35:39 +0100 Subject: [PATCH 4/5] Add styling to alerts --- .../beatbattle/ui/dialog/AlertDialogs.java | 223 ++++++++++++++++-- 1 file changed, 201 insertions(+), 22 deletions(-) diff --git a/core/src/main/java/group07/beatbattle/ui/dialog/AlertDialogs.java b/core/src/main/java/group07/beatbattle/ui/dialog/AlertDialogs.java index cf4cbcf..e28221c 100644 --- a/core/src/main/java/group07/beatbattle/ui/dialog/AlertDialogs.java +++ b/core/src/main/java/group07/beatbattle/ui/dialog/AlertDialogs.java @@ -13,9 +13,27 @@ import com.badlogic.gdx.scenes.scene2d.ui.Window; import com.badlogic.gdx.scenes.scene2d.utils.Drawable; import com.badlogic.gdx.scenes.scene2d.utils.TextureRegionDrawable; +import com.badlogic.gdx.utils.Align; public final class AlertDialogs { + private static final float DIALOG_WIDTH_RATIO = 0.88f; + private static final float DIALOG_MAX_WIDTH = 820f; + private static final float DIALOG_MIN_WIDTH = 320f; + private static final float DIALOG_RADIUS = 40f; + + private static final float BUTTON_WIDTH = 220f; + private static final float BUTTON_HEIGHT = 90f; + private static final float BUTTON_RADIUS = 28f; + + private static final Color BACKGROUND_DARK = Color.valueOf("14141F"); + private static final Color PRIMARY = Color.valueOf("594B8F"); + private static final Color SECONDARY = Color.valueOf("DA96CF"); + private static final Color TEXT_LIGHT = Color.valueOf("FFFFFF"); + private static final Color TEXT_DARK = Color.valueOf("000000"); + private static final Color WARNING = Color.valueOf("FFC107"); + private static final Color ERROR = Color.valueOf("B00020"); + private AlertDialogs() { } @@ -25,14 +43,79 @@ public static void showInfo( String title, String message ) { - Skin skin = createSkin(font); + float dialogWidth = getDialogWidth(stage); + float contentWidth = dialogWidth - 140f; + + Skin skin = createSkin(font, BACKGROUND_DARK, BACKGROUND_DARK, dialogWidth); + + Dialog dialog = new Dialog(title, skin); + Label contentLabel = createContentLabel(message, skin); + + dialog.getContentTable() + .add(contentLabel) + .width(contentWidth) + .padTop(10) + .padBottom(10); + + dialog.button("OK", true); + + styleDialog(dialog); + dialog.show(stage); + centerDialog(dialog, stage, dialogWidth); + } + + public static void showWarning( + Stage stage, + BitmapFont font, + String title, + String message + ) { + float dialogWidth = getDialogWidth(stage); + float contentWidth = dialogWidth - 140f; + + Skin skin = createSkin(font, BACKGROUND_DARK, BACKGROUND_DARK, dialogWidth); + + Dialog dialog = new Dialog(title, skin); + Label contentLabel = createContentLabel(message, skin); + + dialog.getContentTable() + .add(contentLabel) + .width(contentWidth) + .padTop(10) + .padBottom(10); + + dialog.button("OK", true); + + styleDialog(dialog); + dialog.show(stage); + centerDialog(dialog, stage, dialogWidth); + } + + public static void showError( + Stage stage, + BitmapFont font, + String title, + String message + ) { + float dialogWidth = getDialogWidth(stage); + float contentWidth = dialogWidth - 140f; + + Skin skin = createSkin(font, BACKGROUND_DARK, BACKGROUND_DARK, dialogWidth); Dialog dialog = new Dialog(title, skin); - dialog.text(new Label(message, skin)); - dialog.button("OK"); + Label contentLabel = createContentLabel(message, skin); + + dialog.getContentTable() + .add(contentLabel) + .width(contentWidth) + .padTop(10) + .padBottom(10); + + dialog.button("OK", true); styleDialog(dialog); dialog.show(stage); + centerDialog(dialog, stage, dialogWidth); } public static void showConfirmation( @@ -42,7 +125,10 @@ public static void showConfirmation( String message, Runnable onConfirm ) { - Skin skin = createSkin(font); + float dialogWidth = getDialogWidth(stage); + float contentWidth = dialogWidth - 140f; + + Skin skin = createSkin(font, BACKGROUND_DARK, PRIMARY, dialogWidth); Dialog dialog = new Dialog(title, skin) { @Override @@ -54,39 +140,125 @@ protected void result(Object object) { } }; - dialog.text(new Label(message, skin)); - dialog.button("Cancel", false); - dialog.button("Confirm", true); + Label contentLabel = createContentLabel(message, skin); + + dialog.getContentTable() + .add(contentLabel) + .width(contentWidth) + .padTop(10) + .padBottom(10); + + dialog.button("Stay", false); + dialog.button("Leave", true); styleDialog(dialog); dialog.show(stage); + centerDialog(dialog, stage, dialogWidth); + } + + private static float getDialogWidth(Stage stage) { + float width = stage.getWidth() * DIALOG_WIDTH_RATIO; + width = Math.min(width, DIALOG_MAX_WIDTH); + width = Math.max(width, DIALOG_MIN_WIDTH); + return width; + } + + private static Label createContentLabel(String message, Skin skin) { + Label contentLabel = new Label(message, skin); + contentLabel.setWrap(true); + contentLabel.setAlignment(Align.center); + return contentLabel; } private static void styleDialog(Dialog dialog) { - dialog.getContentTable().pad(30); - dialog.getButtonTable().pad(20); + dialog.getTitleLabel().setAlignment(Align.center); + + dialog.getTitleTable() + .padTop(22) + .padBottom(16); + + dialog.getContentTable() + .center() + .padLeft(20) + .padRight(20) + .padTop(14) + .padBottom(14); + + dialog.getButtonTable() + .center() + .padTop(22) + .padBottom(30); + + dialog.getButtonTable().getCells().forEach(cell -> + cell.width(BUTTON_WIDTH) + .height(BUTTON_HEIGHT) + .padLeft(12) + .padRight(12) + .padTop(8) + .padBottom(8) + ); + + dialog.getButtonTable().getCells().forEach(cell -> { + TextButton button = (TextButton) cell.getActor(); + button.getLabel().setAlignment(Align.center); + button.pad(12, 20, 12, 20); + }); + dialog.setModal(true); dialog.setMovable(false); dialog.setResizable(false); } - private static Skin createSkin(BitmapFont font) { + private static void centerDialog(Dialog dialog, Stage stage, float dialogWidth) { + dialog.pack(); + + float maxHeight = stage.getHeight() * 0.8f; + float dialogHeight = Math.min(dialog.getPrefHeight(), maxHeight); + + dialog.setSize(dialogWidth, dialogHeight); + dialog.setPosition( + (stage.getWidth() - dialog.getWidth()) / 2f, + (stage.getHeight() - dialog.getHeight()) / 2f + ); + } + + private static Skin createSkin( + BitmapFont font, + Color buttonUpColor, + Color buttonDownColor, + float dialogWidth + ) { Skin skin = new Skin(); Window.WindowStyle windowStyle = new Window.WindowStyle(); windowStyle.titleFont = font; - windowStyle.titleFontColor = Color.WHITE; - windowStyle.background = createRoundedBackground(700, 320, 40, 0.18f, 0.18f, 0.28f, 1f); + windowStyle.titleFontColor = TEXT_DARK; + windowStyle.background = createRoundedBackground( + (int) dialogWidth, + 300, + (int) DIALOG_RADIUS, + SECONDARY + ); Label.LabelStyle labelStyle = new Label.LabelStyle(); labelStyle.font = font; - labelStyle.fontColor = Color.WHITE; + labelStyle.fontColor = TEXT_DARK; TextButton.TextButtonStyle buttonStyle = new TextButton.TextButtonStyle(); buttonStyle.font = font; - buttonStyle.fontColor = Color.WHITE; - buttonStyle.up = createRoundedBackground(260, 90, 30, 0.28f, 0.28f, 0.40f, 1f); - buttonStyle.down = createRoundedBackground(260, 90, 30, 0.20f, 0.20f, 0.32f, 1f); + buttonStyle.fontColor = TEXT_LIGHT; + buttonStyle.up = createRoundedBackground( + (int) BUTTON_WIDTH, + (int) BUTTON_HEIGHT, + (int) BUTTON_RADIUS, + buttonUpColor + ); + buttonStyle.down = createRoundedBackground( + (int) BUTTON_WIDTH, + (int) BUTTON_HEIGHT, + (int) BUTTON_RADIUS, + buttonDownColor + ); skin.add("default", windowStyle); skin.add("default", labelStyle); @@ -99,13 +271,10 @@ private static Drawable createRoundedBackground( int width, int height, int radius, - float r, - float g, - float b, - float a + Color color ) { Pixmap pixmap = new Pixmap(width, height, Pixmap.Format.RGBA8888); - pixmap.setColor(r, g, b, a); + pixmap.setColor(color); pixmap.fillRectangle(radius, 0, width - 2 * radius, height); pixmap.fillRectangle(0, radius, width, height - 2 * radius); @@ -118,6 +287,16 @@ private static Drawable createRoundedBackground( Texture texture = new Texture(pixmap); pixmap.dispose(); - return new TextureRegionDrawable(new TextureRegion(texture)); + TextureRegionDrawable drawable = new TextureRegionDrawable(new TextureRegion(texture)); + + drawable.setLeftWidth(36f); + drawable.setRightWidth(36f); + drawable.setTopHeight(84f); + drawable.setBottomHeight(42f); + + drawable.setMinWidth(0f); + drawable.setMinHeight(0f); + + return drawable; } } From a46d782eb60f5547f31631150b6bd1f3197d4e49 Mon Sep 17 00:00:00 2001 From: odaakj Date: Fri, 10 Apr 2026 12:28:52 +0200 Subject: [PATCH 5/5] Fix bug --- .../java/group07/beatbattle/controller/LobbyController.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/core/src/main/java/group07/beatbattle/controller/LobbyController.java b/core/src/main/java/group07/beatbattle/controller/LobbyController.java index 87e1b87..b048566 100644 --- a/core/src/main/java/group07/beatbattle/controller/LobbyController.java +++ b/core/src/main/java/group07/beatbattle/controller/LobbyController.java @@ -51,12 +51,6 @@ public void onReady( JoinCreateView view ) { numRounds = totalRounds; - - if (mode != GameMode.CREATE) { - StateManager.getInstance().setState(new LobbyState(game, mode, this)); - return; - } - view.setLoadingState(true); LobbyService lobbyService = new LobbyService(game.getFirebaseGateway());