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 ae3c036..25143ab 100644 --- a/android/src/main/java/group07/beatbattle/android/firebase/AndroidFirebaseGateway.java +++ b/android/src/main/java/group07/beatbattle/android/firebase/AndroidFirebaseGateway.java @@ -1,5 +1,7 @@ package group07.beatbattle.android.firebase; +import java.util.List; + import group07.beatbattle.firebase.FirebaseGateway; public class AndroidFirebaseGateway implements FirebaseGateway { @@ -24,14 +26,7 @@ public void createSession( int totalRounds, CreateSessionCallback callback ) { - sessionRepository.createSession( - gamePin, - hostId, - state, - currentRound, - totalRounds, - callback - ); + sessionRepository.createSession(gamePin, hostId, state, currentRound, totalRounds, callback); } @Override @@ -64,6 +59,11 @@ public void listenToPlayers(String sessionId, PlayersListenerCallback callback) sessionRepository.listenToPlayers(sessionId, callback); } + @Override + public void fetchPlayers(String sessionId, PlayersListenerCallback callback) { + sessionRepository.fetchPlayers(sessionId, callback); + } + @Override public void removePlayerFromSession( String sessionId, @@ -74,10 +74,32 @@ public void removePlayerFromSession( } @Override - public void deleteSession( - String sessionId, - DeleteSessionCallback callback - ) { + public void deleteSession(String sessionId, DeleteSessionCallback callback) { sessionRepository.deleteSession(sessionId, callback); } + + @Override + public void storeQuestions(String sessionId, List questions, SimpleCallback callback) { + sessionRepository.storeQuestions(sessionId, questions, callback); + } + + @Override + public void getQuestions(String sessionId, GetQuestionsCallback callback) { + sessionRepository.getQuestions(sessionId, callback); + } + + @Override + public void updateSessionState(String sessionId, String state, SimpleCallback callback) { + sessionRepository.updateSessionState(sessionId, state, callback); + } + + @Override + public void listenToSessionState(String sessionId, SessionStateListener listener) { + sessionRepository.listenToSessionState(sessionId, listener); + } + + @Override + public void updatePlayerScore(String sessionId, String playerId, int score, SimpleCallback callback) { + sessionRepository.updatePlayerScore(sessionId, playerId, score, 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 8b575a4..2459fc9 100644 --- a/android/src/main/java/group07/beatbattle/android/firebase/FirestoreSessionRepository.java +++ b/android/src/main/java/group07/beatbattle/android/firebase/FirestoreSessionRepository.java @@ -1,9 +1,12 @@ package group07.beatbattle.android.firebase; import com.google.firebase.Timestamp; +import com.google.firebase.firestore.CollectionReference; +import com.google.firebase.firestore.DocumentSnapshot; import com.google.firebase.firestore.FirebaseFirestore; import com.google.firebase.firestore.ListenerRegistration; import com.google.firebase.firestore.QuerySnapshot; +import com.google.firebase.firestore.WriteBatch; import java.util.ArrayList; import java.util.HashMap; @@ -17,6 +20,7 @@ public class FirestoreSessionRepository { private static final String SESSIONS = "Sessions"; private static final String PLAYERS = "Players"; + private static final String QUESTIONS = "Questions"; private final FirebaseFirestore firestore; @@ -162,6 +166,31 @@ public ListenerRegistration listenToPlayers( }); } + public void fetchPlayers( + String sessionId, + FirebaseGateway.PlayersListenerCallback callback + ) { + firestore.collection(SESSIONS) + .document(sessionId) + .collection(PLAYERS) + .get() + .addOnSuccessListener(snapshot -> { + List players = new ArrayList<>(); + for (DocumentSnapshot document : snapshot.getDocuments()) { + String playerId = document.getId(); + String name = document.getString("name"); + if (name == null) name = "Unknown"; + boolean isHost = Boolean.TRUE.equals(document.getBoolean("isHost")); + Player player = new Player(playerId, name, isHost); + Long scoreValue = document.getLong("score"); + if (scoreValue != null) player.setScore(scoreValue.intValue()); + players.add(player); + } + callback.onPlayersChanged(players); + }) + .addOnFailureListener(callback::onFailure); + } + public void removePlayerFromSession( String sessionId, String playerId, @@ -229,4 +258,114 @@ private void deleteSessionDocument( .addOnSuccessListener(unused -> callback.onSuccess()) .addOnFailureListener(callback::onFailure); } + + public void storeQuestions( + String sessionId, + List questions, + FirebaseGateway.SimpleCallback callback + ) { + WriteBatch batch = firestore.batch(); + CollectionReference questionsRef = firestore + .collection(SESSIONS) + .document(sessionId) + .collection(QUESTIONS); + + for (FirebaseGateway.QuestionData q : questions) { + Map data = new HashMap<>(); + data.put("songId", q.songId); + data.put("songTitle", q.songTitle); + data.put("songArtist", q.songArtist); + data.put("previewUrl", q.previewUrl); + data.put("albumArtUrl", q.albumArtUrl != null ? q.albumArtUrl : ""); + data.put("options", q.options); + data.put("roundIndex", q.roundIndex); + batch.set(questionsRef.document(String.valueOf(q.roundIndex)), data); + } + + batch.commit() + .addOnSuccessListener(unused -> callback.onSuccess()) + .addOnFailureListener(callback::onFailure); + } + + public void getQuestions( + String sessionId, + FirebaseGateway.GetQuestionsCallback callback + ) { + firestore.collection(SESSIONS) + .document(sessionId) + .collection(QUESTIONS) + .orderBy("roundIndex") + .get() + .addOnSuccessListener(snapshot -> { + List questions = new ArrayList<>(); + for (DocumentSnapshot doc : snapshot.getDocuments()) { + String songId = doc.getString("songId"); + String songTitle = doc.getString("songTitle"); + String songArtist = doc.getString("songArtist"); + String previewUrl = doc.getString("previewUrl"); + String albumArtUrl = doc.getString("albumArtUrl"); + List options = (List) doc.get("options"); + Long roundIndexLong = doc.getLong("roundIndex"); + int roundIndex = roundIndexLong != null ? roundIndexLong.intValue() : 0; + + questions.add(new FirebaseGateway.QuestionData( + songId, songTitle, songArtist, previewUrl, albumArtUrl, options, roundIndex + )); + } + callback.onSuccess(questions); + }) + .addOnFailureListener(callback::onFailure); + } + + public void updateSessionState( + String sessionId, + String state, + FirebaseGateway.SimpleCallback callback + ) { + Map update = new HashMap<>(); + update.put("state", state); + + firestore.collection(SESSIONS) + .document(sessionId) + .update(update) + .addOnSuccessListener(unused -> callback.onSuccess()) + .addOnFailureListener(callback::onFailure); + } + + public void listenToSessionState( + String sessionId, + FirebaseGateway.SessionStateListener listener + ) { + firestore.collection(SESSIONS) + .document(sessionId) + .addSnapshotListener((snapshot, error) -> { + if (error != null) { + listener.onFailure(error); + return; + } + if (snapshot == null || !snapshot.exists()) return; + String state = snapshot.getString("state"); + if (state != null) { + listener.onStateChanged(state); + } + }); + } + + public void updatePlayerScore( + String sessionId, + String playerId, + int score, + FirebaseGateway.SimpleCallback callback + ) { + Map update = new HashMap<>(); + update.put("score", score); + + firestore.collection(SESSIONS) + .document(sessionId) + .collection(PLAYERS) + .document(playerId) + .update(update) + .addOnSuccessListener(unused -> callback.onSuccess()) + .addOnFailureListener(callback::onFailure); + } } diff --git a/core/src/main/java/group07/beatbattle/BeatBattle.java b/core/src/main/java/group07/beatbattle/BeatBattle.java index 0421a32..973b6fa 100644 --- a/core/src/main/java/group07/beatbattle/BeatBattle.java +++ b/core/src/main/java/group07/beatbattle/BeatBattle.java @@ -33,6 +33,11 @@ public BeatBattle(FirebaseGateway firebaseGateway) { private MusicService musicService; private AudioPlayer audioPlayer; + private String localPlayerId; + private String localPlayerName; + private String localSessionId; + private boolean localIsHost; + public void setServices(MusicService musicService, AudioPlayer audioPlayer) { this.musicService = musicService; this.audioPlayer = audioPlayer; @@ -41,6 +46,22 @@ public void setServices(MusicService musicService, AudioPlayer audioPlayer) { public MusicService getMusicService() { return musicService; } public AudioPlayer getAudioPlayer() { return audioPlayer; } + public void setLocalPlayer(String id, String name) { + this.localPlayerId = id; + this.localPlayerName = name; + } + + public String getLocalPlayerId() { return localPlayerId; } + public String getLocalPlayerName() { return localPlayerName; } + + public void setLocalSession(String sessionId, boolean isHost) { + this.localSessionId = sessionId; + this.localIsHost = isHost; + } + + public String getLocalSessionId() { return localSessionId; } + public boolean isLocalHost() { return localIsHost; } + @Override public void create() { montserratFont = loadFont("fonts/Montserrat-Regular.ttf", 54); diff --git a/core/src/main/java/group07/beatbattle/controller/LeaderboardController.java b/core/src/main/java/group07/beatbattle/controller/LeaderboardController.java index 68c8937..0ff6167 100644 --- a/core/src/main/java/group07/beatbattle/controller/LeaderboardController.java +++ b/core/src/main/java/group07/beatbattle/controller/LeaderboardController.java @@ -25,10 +25,13 @@ public Leaderboard getLeaderboard() { return leaderboard; } - /** Returns the leaderboard entry for the local player (index 0), or null if session is empty. */ + /** Returns the leaderboard entry for the local player, or null if not found. */ public LeaderboardEntry getLocalPlayerEntry() { - if (session.getPlayers().isEmpty()) return null; - String localId = session.getPlayers().get(0).getId(); + String localId = game.getLocalPlayerId(); + if (localId == null && !session.getPlayers().isEmpty()) { + localId = session.getPlayers().get(0).getId(); + } + if (localId == null) return null; for (LeaderboardEntry entry : leaderboard.getEntries()) { if (entry.getPlayerId().equals(localId)) return entry; } diff --git a/core/src/main/java/group07/beatbattle/controller/LobbyController.java b/core/src/main/java/group07/beatbattle/controller/LobbyController.java index b048566..0d4108a 100644 --- a/core/src/main/java/group07/beatbattle/controller/LobbyController.java +++ b/core/src/main/java/group07/beatbattle/controller/LobbyController.java @@ -2,7 +2,12 @@ import com.badlogic.gdx.Gdx; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + import group07.beatbattle.BeatBattle; +import group07.beatbattle.firebase.FirebaseGateway; import group07.beatbattle.model.GameMode; import group07.beatbattle.model.GameSession; import group07.beatbattle.model.Player; @@ -20,16 +25,12 @@ import group07.beatbattle.view.JoinCreateView; import group07.beatbattle.view.LobbyView; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - public class LobbyController { private static final int OPTIONS_PER_Q = 4; private final BeatBattle game; - private int numRounds = 5; // default, overwritten when host picks rounds + private int numRounds = 5; public LobbyController(BeatBattle game) { this.game = game; @@ -64,8 +65,10 @@ public void onReady( totalRounds, new LobbyService.CreateSessionCallback() { @Override - public void onSuccess(group07.beatbattle.model.SessionCreationResult result) { + public void onSuccess(SessionCreationResult result) { Gdx.app.postRunnable(() -> { + game.setLocalPlayer(result.getHostId(), result.getHostName()); + game.setLocalSession(result.getSessionId(), true); view.setLoadingState(false); view.setStatusMessage(""); StateManager.getInstance().setState( @@ -108,6 +111,8 @@ public void onFailure(Exception exception) { @Override public void onSuccess(SessionJoinResult result) { Gdx.app.postRunnable(() -> { + game.setLocalPlayer(result.getPlayerId(), result.getPlayerName()); + game.setLocalSession(result.getSessionId(), false); view.setLoadingState(false); view.setStatusMessage(""); StateManager.getInstance().setState( @@ -141,10 +146,7 @@ public void onFailure(Exception exception) { || normalizedMessage.contains("game pin must be 4 characters")) { view.setStatusMessage(""); - view.showInfoAlert( - "Unable to join session", - message - ); + view.showInfoAlert("Unable to join session", message); return; } @@ -199,12 +201,13 @@ public void onFailure(Exception exception) { ); } - public void onStartGame() { - int tracksNeeded = numRounds + (OPTIONS_PER_Q - 1) * numRounds; + /** Called by the host when they press Start Game. */ + public void onStartGame(String sessionId, List players) { + int tracksNeeded = numRounds * OPTIONS_PER_Q; game.getMusicService().fetchTracks(tracksNeeded, new MusicServiceCallback() { @Override public void onSuccess(List songs) { - buildAndStartSession(songs); + buildAndStartSession(sessionId, players, songs); } @Override @@ -214,11 +217,51 @@ public void onFailure(String error) { }); } - private void buildAndStartSession(List songs) { - GameSession session = new GameSession("123456", "host1", numRounds); + /** Called by joiners when Firebase signals game has started. */ + public void onGameStarted( + String sessionId, + List questionDataList, + List players + ) { + int totalRounds = questionDataList.size(); + GameSession session = new GameSession(sessionId, game.getLocalPlayerId(), totalRounds); + + for (Player p : players) { + session.addPlayer(p); + } + + for (FirebaseGateway.QuestionData qd : questionDataList) { + Song song = new Song( + qd.songId != null ? qd.songId : "", + qd.songTitle != null ? qd.songTitle : "", + qd.songArtist != null ? qd.songArtist : "", + qd.previewUrl != null ? qd.previewUrl : "", + qd.albumArtUrl != null ? qd.albumArtUrl : "" + ); + Question question = new Question( + "q" + qd.roundIndex, + song, + qd.options != null ? qd.options : new ArrayList<>(), + qd.roundIndex + ); + session.addQuestion(question); + } - // Songs 0..numRounds-1 are correct answers; the rest are the decoy pool - List decoyPool = new ArrayList<>(songs.subList(numRounds, songs.size())); + RoundController roundController = new RoundController(game, session.getCurrentQuestion(), session); + StateManager.getInstance().setState(new InRoundState(game, roundController)); + } + + private void buildAndStartSession(String sessionId, List sessionPlayers, List songs) { + GameSession session = new GameSession(sessionId, game.getLocalPlayerId(), numRounds); + + for (Player p : sessionPlayers) { + session.addPlayer(p); + } + + List decoyPool = new ArrayList<>(songs.subList( + Math.min(numRounds, songs.size()), songs.size() + )); + List questionDataList = new ArrayList<>(); for (int q = 0; q < numRounds && q < songs.size(); q++) { Song correct = songs.get(q); @@ -239,9 +282,56 @@ private void buildAndStartSession(List songs) { Collections.shuffle(options); session.addQuestion(new Question("q" + (q + 1), correct, options, q)); + + questionDataList.add(new FirebaseGateway.QuestionData( + correct.getId(), + correct.getTitle(), + correct.getArtist(), + correct.getPreviewUrl(), + correct.getAlbumArtUrl(), + new ArrayList<>(options), + q + )); } - RoundController roundController = new RoundController(game, session.getCurrentQuestion(), session); - StateManager.getInstance().setState(new InRoundState(game, roundController)); + // Store questions → update state → launch round + game.getFirebaseGateway().storeQuestions( + sessionId, + questionDataList, + new FirebaseGateway.SimpleCallback() { + @Override + public void onSuccess() { + game.getFirebaseGateway().updateSessionState( + sessionId, + "in_round", + new FirebaseGateway.SimpleCallback() { + @Override + public void onSuccess() { + Gdx.app.postRunnable(() -> { + RoundController roundController = new RoundController( + game, session.getCurrentQuestion(), session + ); + StateManager.getInstance().setState( + new InRoundState(game, roundController) + ); + }); + } + + @Override + public void onFailure(Exception exception) { + Gdx.app.error("LobbyController", + "Failed to update session state: " + exception.getMessage()); + } + } + ); + } + + @Override + public void onFailure(Exception exception) { + Gdx.app.error("LobbyController", + "Failed to store questions: " + exception.getMessage()); + } + } + ); } } diff --git a/core/src/main/java/group07/beatbattle/controller/RoundController.java b/core/src/main/java/group07/beatbattle/controller/RoundController.java index 45e65a0..0b12436 100644 --- a/core/src/main/java/group07/beatbattle/controller/RoundController.java +++ b/core/src/main/java/group07/beatbattle/controller/RoundController.java @@ -1,24 +1,24 @@ package group07.beatbattle.controller; +import com.badlogic.gdx.Gdx; + +import java.util.List; + import group07.beatbattle.BeatBattle; import group07.beatbattle.ecs.Engine; import group07.beatbattle.ecs.components.TimerComponent; import group07.beatbattle.ecs.entities.Entity; import group07.beatbattle.ecs.entities.RoundFactory; -import group07.beatbattle.ecs.systems.AudioSystem; +import group07.beatbattle.firebase.FirebaseGateway; import group07.beatbattle.model.GameSession; import group07.beatbattle.model.Leaderboard; import group07.beatbattle.model.Player; import group07.beatbattle.model.Question; import group07.beatbattle.model.score.ScoreCalculator; - import group07.beatbattle.states.LeaderboardState; import group07.beatbattle.states.StartState; import group07.beatbattle.states.StateManager; -import java.util.List; -import java.util.Random; - public class RoundController { private final BeatBattle game; @@ -26,6 +26,7 @@ public class RoundController { private final GameSession session; private final Entity roundEntity; private boolean playerAnswered = false; + public RoundController(BeatBattle game, Question question, GameSession session) { this.game = game; this.question = question; @@ -35,7 +36,6 @@ public RoundController(BeatBattle game, Question question, GameSession session) question.getSong().getPreviewUrl() ); Engine.getInstance().addEntity(roundEntity); - AudioSystem.getInstance().play(roundEntity); } public Question getQuestion() { @@ -57,46 +57,97 @@ public void onAnswerSubmitted(String answer) { boolean correct = question.isCorrect(answer); int points = ScoreCalculator.calculateScore(correct, answerTime); - // TODO: replace player[0] with actual local player once Firebase provides player identity - if (!session.getPlayers().isEmpty()) { - Player localPlayer = session.getPlayers().get(0); + Player localPlayer = findLocalPlayer(); + if (localPlayer != null) { localPlayer.addScore(points); + submitScoreToFirebase(localPlayer.getId(), localPlayer.getScore()); } playerAnswered = true; - // TODO: write submission to Firebase via GameService - // Transition is driven by the view after showing answer reveal } public void onRoundExpired() { - // If the player never answered, record 0 points for this round - if (!playerAnswered && !session.getPlayers().isEmpty()) { - session.getPlayers().get(0).addScore(0); + if (!playerAnswered) { + Player localPlayer = findLocalPlayer(); + if (localPlayer != null) { + submitScoreToFirebase(localPlayer.getId(), localPlayer.getScore()); + } } transitionToLeaderboard(); } public void onLeaveSession() { - AudioSystem.getInstance().stop(roundEntity); Engine.getInstance().removeEntity(roundEntity); StateManager.getInstance().setState(new StartState(game, new LobbyController(game))); } - private void simulateMockPlayers() { - Random rng = new Random(); - List players = session.getPlayers(); - for (int i = 1; i < players.size(); i++) { // skip index 0 (local player) - // 70% chance of answering correctly with a random answer time - boolean correct = rng.nextFloat() < 0.7f; - double answerTime = 2.0 + rng.nextDouble() * 28.0; // 2–30 seconds - int points = ScoreCalculator.calculateScore(correct, answerTime); - players.get(i).addScore(points); + private Player findLocalPlayer() { + String localId = game.getLocalPlayerId(); + if (localId != null) { + for (Player p : session.getPlayers()) { + if (p.getId().equals(localId)) return p; + } + } + if (!session.getPlayers().isEmpty()) { + return session.getPlayers().get(0); } + return null; + } + + private void submitScoreToFirebase(String playerId, int score) { + String sessionId = game.getLocalSessionId(); + if (sessionId == null || sessionId.isBlank()) return; + + game.getFirebaseGateway().updatePlayerScore( + sessionId, + playerId, + score, + new FirebaseGateway.SimpleCallback() { + @Override + public void onSuccess() {} + + @Override + public void onFailure(Exception exception) { + Gdx.app.error("RoundController", + "Failed to submit score: " + exception.getMessage()); + } + } + ); } private void transitionToLeaderboard() { - simulateMockPlayers(); - AudioSystem.getInstance().stop(roundEntity); + String sessionId = game.getLocalSessionId(); + + if (sessionId != null && !sessionId.isBlank()) { + game.getFirebaseGateway().fetchPlayers( + sessionId, + new FirebaseGateway.PlayersListenerCallback() { + @Override + public void onPlayersChanged(List updatedPlayers) { + Gdx.app.postRunnable(() -> { + for (Player updated : updatedPlayers) { + for (Player local : session.getPlayers()) { + if (local.getId().equals(updated.getId())) { + local.setScore(updated.getScore()); + } + } + } + launchLeaderboard(); + }); + } + + @Override + public void onFailure(Exception exception) { + Gdx.app.postRunnable(RoundController.this::launchLeaderboard); + } + } + ); + } else { + launchLeaderboard(); + } + } + + private void launchLeaderboard() { Engine.getInstance().removeEntity(roundEntity); Leaderboard leaderboard = new Leaderboard(session.getPlayers()); LeaderboardController leaderboardController = new LeaderboardController(game, session, leaderboard); diff --git a/core/src/main/java/group07/beatbattle/firebase/FirebaseGateway.java b/core/src/main/java/group07/beatbattle/firebase/FirebaseGateway.java index 0098e4c..ffb6fe9 100644 --- a/core/src/main/java/group07/beatbattle/firebase/FirebaseGateway.java +++ b/core/src/main/java/group07/beatbattle/firebase/FirebaseGateway.java @@ -38,6 +38,11 @@ void listenToPlayers( PlayersListenerCallback callback ); + void fetchPlayers( + String sessionId, + PlayersListenerCallback callback + ); + void removePlayerFromSession( String sessionId, String playerId, @@ -49,6 +54,20 @@ void deleteSession( DeleteSessionCallback callback ); + // --- Multiplayer sync --- + + void storeQuestions(String sessionId, List questions, SimpleCallback callback); + + void getQuestions(String sessionId, GetQuestionsCallback callback); + + void updateSessionState(String sessionId, String state, SimpleCallback callback); + + void listenToSessionState(String sessionId, SessionStateListener listener); + + void updatePlayerScore(String sessionId, String playerId, int score, SimpleCallback callback); + + // --- Callbacks --- + interface GamePinExistsCallback { void onResult(boolean exists); void onFailure(Exception exception); @@ -88,4 +107,48 @@ interface DeleteSessionCallback { void onSuccess(); void onFailure(Exception exception); } + + interface SimpleCallback { + void onSuccess(); + void onFailure(Exception exception); + } + + interface GetQuestionsCallback { + void onSuccess(List questions); + void onFailure(Exception exception); + } + + interface SessionStateListener { + void onStateChanged(String state); + void onFailure(Exception exception); + } + + /** Data transfer object for a question stored in Firebase. */ + class QuestionData { + public final String songId; + public final String songTitle; + public final String songArtist; + public final String previewUrl; + public final String albumArtUrl; + public final List options; + public final int roundIndex; + + public QuestionData( + String songId, + String songTitle, + String songArtist, + String previewUrl, + String albumArtUrl, + List options, + int roundIndex + ) { + this.songId = songId; + this.songTitle = songTitle; + this.songArtist = songArtist; + this.previewUrl = previewUrl; + this.albumArtUrl = albumArtUrl; + this.options = options; + this.roundIndex = roundIndex; + } + } } diff --git a/core/src/main/java/group07/beatbattle/firebase/NoOpFirebaseGateway.java b/core/src/main/java/group07/beatbattle/firebase/NoOpFirebaseGateway.java index 435204d..873cdd4 100644 --- a/core/src/main/java/group07/beatbattle/firebase/NoOpFirebaseGateway.java +++ b/core/src/main/java/group07/beatbattle/firebase/NoOpFirebaseGateway.java @@ -1,5 +1,7 @@ package group07.beatbattle.firebase; +import java.util.List; + public class NoOpFirebaseGateway implements FirebaseGateway { @Override @@ -30,10 +32,7 @@ public void addHostToPlayers( } @Override - public void findSessionIdByGamePin( - String gamePin, - FindSessionCallback callback - ) { + public void findSessionIdByGamePin(String gamePin, FindSessionCallback callback) { callback.onFailure(new UnsupportedOperationException("Firebase is not available on this platform")); } @@ -52,6 +51,11 @@ public void listenToPlayers(String sessionId, PlayersListenerCallback callback) callback.onFailure(new UnsupportedOperationException("Firebase is not available on this platform")); } + @Override + public void fetchPlayers(String sessionId, PlayersListenerCallback callback) { + callback.onFailure(new UnsupportedOperationException("Firebase is not available on this platform")); + } + @Override public void removePlayerFromSession( String sessionId, @@ -68,4 +72,29 @@ public void deleteSession( ) { callback.onFailure(new UnsupportedOperationException("Firebase is not available on this platform")); } + + @Override + public void storeQuestions(String sessionId, List questions, SimpleCallback callback) { + callback.onFailure(new UnsupportedOperationException("Firebase is not available on this platform")); + } + + @Override + public void getQuestions(String sessionId, GetQuestionsCallback callback) { + callback.onFailure(new UnsupportedOperationException("Firebase is not available on this platform")); + } + + @Override + public void updateSessionState(String sessionId, String state, SimpleCallback callback) { + callback.onFailure(new UnsupportedOperationException("Firebase is not available on this platform")); + } + + @Override + public void listenToSessionState(String sessionId, SessionStateListener listener) { + listener.onFailure(new UnsupportedOperationException("Firebase is not available on this platform")); + } + + @Override + public void updatePlayerScore(String sessionId, String playerId, int score, SimpleCallback callback) { + callback.onFailure(new UnsupportedOperationException("Firebase is not available on this platform")); + } } diff --git a/core/src/main/java/group07/beatbattle/states/GameOverState.java b/core/src/main/java/group07/beatbattle/states/GameOverState.java index a7f5cf4..bd4b752 100644 --- a/core/src/main/java/group07/beatbattle/states/GameOverState.java +++ b/core/src/main/java/group07/beatbattle/states/GameOverState.java @@ -2,6 +2,7 @@ import group07.beatbattle.BeatBattle; import group07.beatbattle.controller.LeaderboardController; +import group07.beatbattle.view.GameOverView; public class GameOverState extends State { @@ -14,7 +15,7 @@ public GameOverState(BeatBattle game, LeaderboardController leaderboardControlle @Override public void enter() { - // TODO: game.setScreen(new GameOverView(game, leaderboardController)); + game.setScreen(new GameOverView(game, leaderboardController)); } @Override diff --git a/core/src/main/java/group07/beatbattle/view/GameOverView.java b/core/src/main/java/group07/beatbattle/view/GameOverView.java index b498672..499ca3f 100644 --- a/core/src/main/java/group07/beatbattle/view/GameOverView.java +++ b/core/src/main/java/group07/beatbattle/view/GameOverView.java @@ -1,4 +1,144 @@ package group07.beatbattle.view; -public class GameOverView { +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.ScreenAdapter; +import com.badlogic.gdx.graphics.Color; +import com.badlogic.gdx.graphics.GL20; +import com.badlogic.gdx.graphics.Texture; +import com.badlogic.gdx.scenes.scene2d.Actor; +import com.badlogic.gdx.scenes.scene2d.Stage; +import com.badlogic.gdx.scenes.scene2d.ui.Image; +import com.badlogic.gdx.scenes.scene2d.ui.Label; +import com.badlogic.gdx.scenes.scene2d.ui.Table; +import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener; +import com.badlogic.gdx.utils.Scaling; +import com.badlogic.gdx.utils.viewport.ScreenViewport; + +import java.util.List; + +import group07.beatbattle.BeatBattle; +import group07.beatbattle.controller.LeaderboardController; +import group07.beatbattle.model.LeaderboardEntry; +import group07.beatbattle.ui.components.BackButton; + +public class GameOverView extends ScreenAdapter { + + private static final Color GOLD = new Color(1.00f, 0.84f, 0.00f, 1f); + private static final Color SILVER = new Color(0.75f, 0.75f, 0.75f, 1f); + private static final Color BRONZE = new Color(0.80f, 0.50f, 0.20f, 1f); + private static final Color LOCAL = new Color(0.40f, 1.00f, 0.50f, 1f); + + private final BeatBattle game; + private final LeaderboardController controller; + private final Stage stage; + private final Texture logoTexture; + + public GameOverView(BeatBattle game, LeaderboardController controller) { + this.game = game; + this.controller = controller; + this.stage = new Stage(new ScreenViewport()); + + logoTexture = new Texture(Gdx.files.internal("logo.png")); + Image bgLogo = new Image(logoTexture); + bgLogo.setScaling(Scaling.fit); + bgLogo.setColor(1f, 1f, 1f, 0.12f); + bgLogo.setSize(Gdx.graphics.getWidth() * 0.8f, Gdx.graphics.getWidth() * 0.8f); + bgLogo.setPosition( + (Gdx.graphics.getWidth() - bgLogo.getWidth()) / 2f, + (Gdx.graphics.getHeight() - bgLogo.getHeight()) / 2f + ); + stage.addActor(bgLogo); + + Table root = new Table(); + root.setFillParent(true); + root.pad(60f); + root.top(); + + // Title + root.add(makeLabel("GAME OVER", game.getOrbitronFont(), Color.WHITE)) + .padBottom(10f).row(); + root.add(makeLabel("Final Standings", game.getMontserratFont(), Color.LIGHT_GRAY)) + .padBottom(50f).row(); + + // Header row + Table header = new Table(); + header.add(makeLabel("#", game.getMontserratFont(), Color.LIGHT_GRAY)).width(80f).left(); + header.add(makeLabel("Player", game.getMontserratFont(), Color.LIGHT_GRAY)).expandX().left(); + header.add(makeLabel("Score", game.getMontserratFont(), Color.LIGHT_GRAY)).width(200f).right(); + root.add(header).fillX().padBottom(20f).row(); + + // Player rows (all of them for final screen) + String localId = game.getLocalPlayerId(); + List entries = controller.getLeaderboard().getEntries(); + for (LeaderboardEntry entry : entries) { + boolean isLocal = entry.getPlayerId().equals(localId); + root.add(buildRow(entry, isLocal)).fillX().padBottom(14f).row(); + } + + root.add().expandY().row(); + + // Back to menu button + BackButton backButton = new BackButton("Back to Menu", game.getMontserratFont()); + backButton.addListener(new ChangeListener() { + @Override + public void changed(ChangeEvent event, Actor actor) { + controller.onBackToMenu(); + } + }); + root.add(backButton).width(600f).height(130f).row(); + + stage.addActor(root); + } + + private Table buildRow(LeaderboardEntry entry, boolean isLocal) { + Color nameColor = isLocal ? LOCAL : Color.WHITE; + Color rankColor = rankColor(entry.getRank()); + + Table row = new Table(); + row.add(makeLabel(entry.getRank() + ".", game.getMontserratFont(), rankColor)).width(80f).left(); + row.add(makeLabel(entry.getPlayerName(), game.getMontserratFont(), nameColor)).expandX().left(); + row.add(makeLabel(String.valueOf(entry.getScore()), game.getMontserratFont(), Color.YELLOW)) + .width(200f).right(); + return row; + } + + private Color rankColor(int rank) { + switch (rank) { + case 1: return GOLD; + case 2: return SILVER; + case 3: return BRONZE; + default: return Color.WHITE; + } + } + + private Label makeLabel(String text, com.badlogic.gdx.graphics.g2d.BitmapFont font, Color color) { + Label.LabelStyle style = new Label.LabelStyle(); + style.font = font; + style.fontColor = color; + return new Label(text, style); + } + + @Override + public void show() { + Gdx.input.setInputProcessor(stage); + } + + @Override + public void render(float delta) { + Gdx.gl.glClearColor(0.08f, 0.08f, 0.12f, 1f); + Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT); + stage.act(delta); + stage.draw(); + } + + @Override + public void resize(int width, int height) { + stage.getViewport().update(width, height, true); + } + + @Override + public void dispose() { + stage.dispose(); + logoTexture.dispose(); + } } diff --git a/core/src/main/java/group07/beatbattle/view/LobbyView.java b/core/src/main/java/group07/beatbattle/view/LobbyView.java index 5467ed8..8ae4ecc 100644 --- a/core/src/main/java/group07/beatbattle/view/LobbyView.java +++ b/core/src/main/java/group07/beatbattle/view/LobbyView.java @@ -16,10 +16,12 @@ import com.badlogic.gdx.utils.Align; import com.badlogic.gdx.utils.viewport.ScreenViewport; +import java.util.ArrayList; import java.util.List; import group07.beatbattle.BeatBattle; import group07.beatbattle.controller.LobbyController; +import group07.beatbattle.firebase.FirebaseGateway; import group07.beatbattle.model.GameMode; import group07.beatbattle.model.Player; import group07.beatbattle.model.services.LobbyService; @@ -42,12 +44,14 @@ public class LobbyView extends ScreenAdapter { private final String playerId; private final boolean isHost; private boolean leavingLobby; + private boolean gameStarted; private Label.LabelStyle infoStyle; private Label.LabelStyle titleStyle; private Table playersGrid; private Label statusLabel; + private List currentPlayers = new ArrayList<>(); public LobbyView( BeatBattle game, @@ -70,8 +74,8 @@ public LobbyView( this.hostName = displayName; this.isHost = isHost; this.leavingLobby = false; + this.gameStarted = false; - // Styles titleStyle = new Label.LabelStyle(); titleStyle.font = game.getOrbitronFont(); titleStyle.fontColor = Color.WHITE; @@ -92,6 +96,10 @@ public LobbyView( stage.addActor(root); startListeningToPlayers(); + + if (!isHost) { + startListeningToSessionState(); + } } private void startListeningToPlayers() { @@ -106,16 +114,58 @@ private void startListeningToPlayers() { @Override public void onPlayersChanged(List players) { Gdx.app.postRunnable(() -> { - updatePlayersGrid(players); + currentPlayers = players != null ? players : new ArrayList<>(); + updatePlayersGrid(currentPlayers); setStatusMessage(""); }); } @Override public void onFailure(Exception exception) { - Gdx.app.postRunnable(() -> { - setStatusMessage("Failed to load players"); - }); + Gdx.app.postRunnable(() -> setStatusMessage("Failed to load players")); + } + } + ); + } + + private void startListeningToSessionState() { + if (sessionId == null || sessionId.isBlank()) return; + + game.getFirebaseGateway().listenToSessionState( + sessionId, + new FirebaseGateway.SessionStateListener() { + @Override + public void onStateChanged(String state) { + if ("in_round".equals(state) && !gameStarted && !leavingLobby) { + gameStarted = true; + setStatusMessage("Game starting..."); + game.getFirebaseGateway().getQuestions( + sessionId, + new FirebaseGateway.GetQuestionsCallback() { + @Override + public void onSuccess(List questions) { + Gdx.app.postRunnable(() -> + controller.onGameStarted(sessionId, questions, currentPlayers) + ); + } + + @Override + public void onFailure(Exception exception) { + Gdx.app.postRunnable(() -> { + gameStarted = false; + setStatusMessage("Failed to load questions"); + }); + } + } + ); + } + } + + @Override + public void onFailure(Exception exception) { + Gdx.app.postRunnable(() -> + setStatusMessage("Connection error: " + exception.getMessage()) + ); } } ); @@ -124,17 +174,13 @@ public void onFailure(Exception exception) { private Table createHeader() { Table header = new Table(); - BackButton backButton = - new BackButton("Back", game.getMontserratFont()); - + BackButton backButton = new BackButton("Back", game.getMontserratFont()); SettingsButton settingsButton = new SettingsButton(); backButton.addListener(new ChangeListener() { @Override public void changed(ChangeEvent event, Actor actor) { - if (leavingLobby) { - return; - } + if (leavingLobby) return; showLeaveLobbyConfirmation(); } }); @@ -146,17 +192,8 @@ public void changed(ChangeEvent event, Actor actor) { } }); - header.add(backButton) - .width(200) - .height(100) - .expandX() - .left() - .padLeft(20); - - header.add(settingsButton) - .size(80) - .right() - .padRight(20); + header.add(backButton).width(200).height(100).expandX().left().padLeft(20); + header.add(settingsButton).size(80).right().padRight(20); return header; } @@ -173,14 +210,7 @@ private void showLeaveLobbyConfirmation() { message, () -> { leavingLobby = true; - - controller.onBackFromLobby( - mode, - sessionId, - playerId, - isHost, - LobbyView.this - ); + controller.onBackFromLobby(mode, sessionId, playerId, isHost, LobbyView.this); } ); } @@ -209,9 +239,7 @@ private Table createPartySection() { Table partyBox = createPartyBox(); table.add(partyLabel).padBottom(20).row(); - table.add(partyBox) - .width(850) - .height(620); + table.add(partyBox).width(850).height(620); return table; } @@ -246,22 +274,17 @@ private void updatePlayersGrid(List players) { if (players == null || players.isEmpty()) { playersGrid.add(createPlayerLabel("No players yet")) - .width(cellWidth) - .height(cellHeight) - .pad(15); + .width(cellWidth).height(cellHeight).pad(15); return; } int columnCount = 2; for (int i = 0; i < players.size(); i++) { Player player = players.get(i); - String displayName = player.getName() + (player.isHost() ? " [H]" : ""); playersGrid.add(createPlayerLabel(displayName)) - .width(cellWidth) - .height(cellHeight) - .pad(15); + .width(cellWidth).height(cellHeight).pad(15); if ((i + 1) % columnCount == 0) { playersGrid.row(); @@ -303,19 +326,16 @@ private Table createFooter() { Table footer = new Table(); if (mode == GameMode.CREATE) { - StartGameButton startButton = - new StartGameButton(game.getMontserratFont()); + StartGameButton startButton = new StartGameButton(game.getMontserratFont()); startButton.addListener(new ChangeListener() { @Override public void changed(ChangeEvent event, Actor actor) { - controller.onStartGame(); + controller.onStartGame(sessionId, currentPlayers); } }); - footer.add(startButton) - .width(800) - .height(150); + footer.add(startButton).width(800).height(150); } return footer; @@ -325,17 +345,9 @@ public void setLeavingLobby(boolean leavingLobby) { this.leavingLobby = leavingLobby; } - public String getSessionId() { - return sessionId; - } - - public String getGamePin() { - return gamePin; - } - - public String getHostName() { - return hostName; - } + public String getSessionId() { return sessionId; } + public String getGamePin() { return gamePin; } + public String getHostName() { return hostName; } @Override public void show() {