From a79107ad931f9a342774aafe14002417c5777a3a Mon Sep 17 00:00:00 2001 From: benjamls Date: Thu, 16 Apr 2026 19:48:39 +0200 Subject: [PATCH 1/2] Start game upon opponent joining lobby --- .../screens/lobby/LobbyFlowController.java | 30 +++-- .../screens/lobby/LobbyHostUI.java | 91 +++++-------- .../screens/lobby/LobbyScreen.java | 121 +++++++++--------- 3 files changed, 114 insertions(+), 128 deletions(-) diff --git a/core/src/main/java/com/group14/regicidechess/screens/lobby/LobbyFlowController.java b/core/src/main/java/com/group14/regicidechess/screens/lobby/LobbyFlowController.java index 882840a..2bf11af 100644 --- a/core/src/main/java/com/group14/regicidechess/screens/lobby/LobbyFlowController.java +++ b/core/src/main/java/com/group14/regicidechess/screens/lobby/LobbyFlowController.java @@ -7,27 +7,33 @@ /** * Manages Firebase listeners and flow between host and joiner. * Encapsulates all Firebase communication logic. + * + * Auto-start behaviour: the host no longer manually clicks "Start Game". + * As soon as a joiner is detected, the host signals game start and both + * players are navigated to SetupScreen automatically. */ public class LobbyFlowController { - + public interface FlowListener { - void onJoinerArrived(); // Host: someone joined the lobby - void onGameStarted(); // Joiner: host started the game + void onJoinerArrived(); // Host: joiner detected — auto-start is triggered here + void onGameStarted(); // Joiner: host signalled start void onError(String message); } - + private final LobbyState lobbyState; private final FlowListener listener; private String activeGameId; - + public LobbyFlowController(LobbyState lobbyState, FlowListener listener) { this.lobbyState = lobbyState; this.listener = listener; } - + /** * Host: Listens for a joiner to enter the lobby. * When status becomes "joined", calls onJoinerArrived(). + * The host is expected to immediately call signalGameStart() inside + * that callback, then navigate — no manual button press required. */ public void listenForJoiner(String gameId) { this.activeGameId = gameId; @@ -38,17 +44,17 @@ public void listenForJoiner(String gameId) { } })); } - + /** - * Host: Signals the game to start. - * Writes status = "started" to Firebase. + * Host: Writes status = "started" to Firebase so the joiner is notified. + * Called automatically from LobbyScreen once the joiner arrives. */ public void signalGameStart(String gameId) { lobbyState.startGame(gameId); } - + /** - * Joiner: Listens for host to start the game. + * Joiner: Listens for the host's "started" signal. * When status becomes "started", calls onGameStarted(). */ public void listenForGameStart(String gameId) { @@ -60,7 +66,7 @@ public void listenForGameStart(String gameId) { } })); } - + public String getActiveGameId() { return activeGameId; } diff --git a/core/src/main/java/com/group14/regicidechess/screens/lobby/LobbyHostUI.java b/core/src/main/java/com/group14/regicidechess/screens/lobby/LobbyHostUI.java index 22e1f47..22c9407 100644 --- a/core/src/main/java/com/group14/regicidechess/screens/lobby/LobbyHostUI.java +++ b/core/src/main/java/com/group14/regicidechess/screens/lobby/LobbyHostUI.java @@ -12,55 +12,56 @@ /** * Builds and manages the host-specific UI for lobby creation. - * Contains board size slider, budget slider, create button, and start button. + * Contains board size slider, budget slider, and create button. + * + * The "Start Game" button has been removed. The game now starts + * automatically as soon as an opponent joins the lobby. */ public class LobbyHostUI { - + public interface HostUIListener { void onCreateLobby(int boardSize, int budget); - void onStartGame(); void onBack(); } - + private final Skin skin; private final HostUIListener listener; - + private int boardSize = LobbyScreenConfig.BOARD_DEFAULT; private int budget = LobbyScreenConfig.BUDGET_DEFAULT; - + private Label boardSizeValueLabel; private Label budgetValueLabel; private Label statusLabel; private TextButton createBtn; - private TextButton startBtn; private Table sliderSection; - + public LobbyHostUI(Skin skin, HostUIListener listener) { this.skin = skin; this.listener = listener; } - + public Table build() { Table container = new Table(); container.top(); - + // Board size controls Label boardSizeLabel = new Label("Board Size", skin); boardSizeValueLabel = new Label(boardSize + " x " + boardSize, skin); - + Slider boardSlider = createBoardSlider(); - + // Budget controls Label budgetLabel = new Label("Starting Budget", skin); budgetValueLabel = new Label(String.valueOf(budget), skin); - + Slider budgetSlider = createBudgetSlider(); - + // Status label statusLabel = new Label("", skin, "small"); statusLabel.setAlignment(Align.center); - - // Buttons + + // Create button createBtn = new TextButton("Create Lobby", skin, "accent"); createBtn.addListener(new ChangeListener() { @Override @@ -70,19 +71,7 @@ public void changed(ChangeEvent event, Actor actor) { } } }); - - startBtn = new TextButton("Start Game", skin, "accent"); - startBtn.setVisible(false); - startBtn.setDisabled(true); - startBtn.addListener(new ChangeListener() { - @Override - public void changed(ChangeEvent event, Actor actor) { - if (!startBtn.isDisabled()) { - listener.onStartGame(); - } - } - }); - + // Sliders grouped so they can be hidden after lobby is created sliderSection = new Table(); sliderSection.add(buildRow(boardSizeLabel, boardSizeValueLabel)) @@ -97,20 +86,17 @@ public void changed(ChangeEvent event, Actor actor) { .width(LobbyScreenConfig.SLIDER_WIDTH) .height(LobbyScreenConfig.SLIDER_HEIGHT) .padBottom(32).row(); + container.add(sliderSection).expandX().fillX().row(); container.add(createBtn) - .width(LobbyScreenConfig.BUTTON_WIDTH) - .height(LobbyScreenConfig.BUTTON_HEIGHT) - .padBottom(8).row(); - container.add(startBtn) .width(LobbyScreenConfig.BUTTON_WIDTH) .height(LobbyScreenConfig.BUTTON_HEIGHT) .padBottom(16).row(); container.add(statusLabel).expandX().row(); - + return container; } - + private Slider createBoardSlider() { Slider slider = new Slider( LobbyScreenConfig.BOARD_MIN, @@ -128,7 +114,7 @@ public void changed(ChangeEvent event, Actor actor) { }); return slider; } - + private Slider createBudgetSlider() { Slider slider = new Slider( LobbyScreenConfig.BUDGET_MIN, @@ -146,52 +132,45 @@ public void changed(ChangeEvent event, Actor actor) { }); return slider; } - + private Slider.SliderStyle buildSliderStyle() { Slider.SliderStyle style = new Slider.SliderStyle(); style.background = skin.getDrawable("primary-pixel"); style.knob = skin.getDrawable("accent-pixel"); return style; } - + private Table buildRow(Label left, Label right) { Table row = new Table(); row.add(left).expandX().left(); row.add(right).expandX().right(); return row; } - - // Public methods for updating UI state - + + // ── Public methods for updating UI state ────────────────────────────────── + public void showCreatingState() { createBtn.setDisabled(true); setStatus("Creating lobby..."); } - + public void showLobbyCreated(String gameId) { sliderSection.setVisible(false); createBtn.setVisible(false); - startBtn.setVisible(true); - startBtn.setDisabled(true); setStatus("Lobby created!\nGame ID: " + gameId - + "\n\nWaiting for opponent to join..."); + + "\n\nWaiting for opponent to join...\nGame will start automatically."); } - - public void showJoinerArrived() { - setStatus("Opponent joined! Press Start Game when you are ready."); - startBtn.setDisabled(false); - } - - public void showStartingState() { - startBtn.setDisabled(true); - setStatus("Starting..."); + + /** Called when a joiner is detected; game starts immediately after this. */ + public void showOpponentJoined() { + setStatus("Opponent joined! Starting game..."); } - + public void showCreationFailed() { setStatus("Failed to create lobby. Try again."); createBtn.setDisabled(false); } - + public void setStatus(String message) { if (statusLabel != null) { statusLabel.setText(message); diff --git a/core/src/main/java/com/group14/regicidechess/screens/lobby/LobbyScreen.java b/core/src/main/java/com/group14/regicidechess/screens/lobby/LobbyScreen.java index 52dfb98..2ffcde4 100644 --- a/core/src/main/java/com/group14/regicidechess/screens/lobby/LobbyScreen.java +++ b/core/src/main/java/com/group14/regicidechess/screens/lobby/LobbyScreen.java @@ -22,51 +22,52 @@ /** * LobbyScreen — thin coordinator for lobby creation and joining. - * - * Refactored version with improved modularity: - * - UI components extracted to LobbyHostUI and LobbyJoinUI - * - Firebase flow extracted to LobbyFlowController - * - State management extracted to LobbyStateManager - * - Screen now focuses purely on coordination between components + * + * Auto-start flow (Solution 2): + * Host creates lobby → waits with a Game ID shown on screen. + * Joiner enters Game ID and presses "Join Match". + * As soon as the joiner is detected by Firebase, the host signals + * game start and BOTH players are sent to SetupScreen automatically. + * No "Start Game" button exists anymore. */ public class LobbyScreen implements Screen, ScreenInputHandler.ScreenInputObserver { - + // LibGDX private final Game game; private final SpriteBatch batch; private final Stage stage; private final Skin skin; private final ScreenInputHandler inputHandler; - + // Mode and data private final LobbyMode mode; private final String incomingGameId; - + // Components private final LobbyStateManager stateManager; private final LobbyFlowController flowController; private final LobbyHostUI hostUI; private final LobbyJoinUI joinUI; - + // Shared UI private Label titleLabel; private Table root; - + public LobbyScreen(Game game, SpriteBatch batch, LobbyMode mode, Lobby lobby) { this.game = game; this.batch = batch; this.mode = mode; this.incomingGameId = lobby != null ? lobby.getGameId() : null; - + // Initialize components this.stateManager = new LobbyStateManager(); if (lobby != null) { stateManager.setPrefetchedLobby(lobby); } - + this.flowController = new LobbyFlowController(stateManager.getLobbyState(), createFlowListener()); this.stateManager.setListener(createStateListener()); - + // LibGDX setup this.stage = new Stage(new FitViewport( LobbyScreenConfig.VIEWPORT_WIDTH, @@ -75,7 +76,7 @@ public LobbyScreen(Game game, SpriteBatch batch, LobbyMode mode, Lobby lobby) { this.skin = ResourceManager.getInstance().getSkin(); this.inputHandler = new ScreenInputHandler(); inputHandler.addObserver(this); - + // Create mode-specific UI if (mode == LobbyMode.HOST) { this.hostUI = new LobbyHostUI(skin, createHostUIListener()); @@ -84,40 +85,40 @@ public LobbyScreen(Game game, SpriteBatch batch, LobbyMode mode, Lobby lobby) { this.joinUI = new LobbyJoinUI(skin, incomingGameId, createJoinUIListener()); this.hostUI = null; } - + stateManager.enter(); buildUI(); - + // If we have prefetched lobby data for joiner, update UI immediately if (mode == LobbyMode.JOIN && lobby != null) { joinUI.updateLobbyInfo(lobby.getBoardSize(), lobby.getBudget()); } } - + // ───────────────────────────────────────────────────────────────────────── // UI construction // ───────────────────────────────────────────────────────────────────────── - + private void buildUI() { root = new Table(); root.setFillParent(true); root.setBackground(skin.getDrawable("surface-pixel")); root.top().pad(32); stage.addActor(root); - + // Title String titleText = mode == LobbyMode.HOST ? "Create Lobby" : "Join Lobby"; titleLabel = new Label(titleText, skin, "title"); titleLabel.setAlignment(com.badlogic.gdx.utils.Align.center); root.add(titleLabel).expandX().padBottom(32).row(); - + // Mode-specific content if (mode == LobbyMode.HOST) { root.add(hostUI.build()).expandX().fillX().row(); } else { root.add(joinUI.build()).expandX().fillX().row(); } - + // Back button (shared) TextButton backBtn = new TextButton("Back", skin, "default"); backBtn.addListener(new ChangeListener() { @@ -131,11 +132,11 @@ public void changed(ChangeEvent event, Actor actor) { .height(LobbyScreenConfig.BACK_BUTTON_HEIGHT) .padTop(24).row(); } - + // ───────────────────────────────────────────────────────────────────────── // Listeners // ───────────────────────────────────────────────────────────────────────── - + private LobbyHostUI.HostUIListener createHostUIListener() { return new LobbyHostUI.HostUIListener() { @Override @@ -145,22 +146,14 @@ public void onCreateLobby(int boardSize, int budget) { stateManager.setBudget(budget); stateManager.createLobby(); } - - @Override - public void onStartGame() { - hostUI.showStartingState(); - String gameId = stateManager.getGameId(); - flowController.signalGameStart(gameId); - navigateToSetup(gameId, true); - } - + @Override public void onBack() { game.setScreen(new MainMenuScreen(game, batch)); } }; } - + private LobbyJoinUI.JoinUIListener createJoinUIListener() { return new LobbyJoinUI.JoinUIListener() { @Override @@ -168,14 +161,14 @@ public void onJoin(String gameId) { joinUI.showJoiningState(); stateManager.joinLobby(gameId); } - + @Override public void onBack() { game.setScreen(new MainMenuScreen(game, batch)); } }; } - + private LobbyStateManager.StateListener createStateListener() { return new LobbyStateManager.StateListener() { @Override @@ -183,12 +176,12 @@ public void onLobbyCreated(String gameId) { hostUI.showLobbyCreated(gameId); flowController.listenForJoiner(gameId); } - + @Override public void onLobbyCreationFailed() { hostUI.showCreationFailed(); } - + @Override public void onLobbyJoined() { joinUI.updateLobbyInfo( @@ -197,26 +190,34 @@ public void onLobbyJoined() { joinUI.showJoinedState(); flowController.listenForGameStart(stateManager.getGameId()); } - + @Override public void onLobbyJoinFailed() { joinUI.showJoinFailed(); } }; } - + private LobbyFlowController.FlowListener createFlowListener() { return new LobbyFlowController.FlowListener() { @Override public void onJoinerArrived() { - hostUI.showJoinerArrived(); + // Auto-start: no button press needed. + // 1. Update host UI to show the transition message. + hostUI.showOpponentJoined(); + // 2. Signal Firebase so the joiner's listener fires. + String gameId = stateManager.getGameId(); + flowController.signalGameStart(gameId); + // 3. Navigate the host to SetupScreen. + navigateToSetup(gameId, true); } - + @Override public void onGameStarted() { + // Joiner is sent to SetupScreen once the host's signal arrives. navigateToSetup(stateManager.getGameId(), false); } - + @Override public void onError(String message) { if (mode == LobbyMode.HOST) { @@ -227,31 +228,31 @@ public void onError(String message) { } }; } - + // ───────────────────────────────────────────────────────────────────────── // Navigation // ───────────────────────────────────────────────────────────────────────── - + private void navigateToSetup(String gameId, boolean isHost) { int boardSize = stateManager.getBoardSize(); int budget = stateManager.getBudget(); game.setScreen(new SetupScreen(game, batch, gameId, boardSize, budget, isHost)); } - + private void onBack() { game.setScreen(new MainMenuScreen(game, batch)); } - + // ───────────────────────────────────────────────────────────────────────── // Screen lifecycle // ───────────────────────────────────────────────────────────────────────── - + @Override public void show() { Gdx.input.setInputProcessor( new com.badlogic.gdx.InputMultiplexer(stage, inputHandler)); } - + @Override public void render(float delta) { Gdx.gl.glClearColor(0.12f, 0.12f, 0.15f, 1f); @@ -260,42 +261,42 @@ public void render(float delta) { stage.act(delta); stage.draw(); } - + @Override public void resize(int width, int height) { stage.getViewport().update(width, height, true); } - + @Override public void pause() {} - + @Override public void resume() {} - + @Override public void hide() { inputHandler.clearObservers(); stateManager.exit(); } - + @Override public void dispose() { stage.dispose(); } - + // ───────────────────────────────────────────────────────────────────────── - // Input handling (unused but required by interface) + // Input handling // ───────────────────────────────────────────────────────────────────────── - + @Override public void onTap(int x, int y, int pointer, int button) {} - + @Override public void onDrag(int x, int y, int pointer) {} - + @Override public void onRelease(int x, int y, int pointer, int button) {} - + @Override public void onKeyDown(int keycode) {} } \ No newline at end of file From 7e3e1b286a63f03847c11d14d304c6ee0ce4ad5e Mon Sep 17 00:00:00 2001 From: benjamls Date: Thu, 16 Apr 2026 19:48:52 +0200 Subject: [PATCH 2/2] Add an exit button to the main menu --- .../screens/mainmenu/MainMenuConfig.java | 7 ++- .../screens/mainmenu/MainMenuUI.java | 61 ++++++++++++------- 2 files changed, 43 insertions(+), 25 deletions(-) diff --git a/core/src/main/java/com/group14/regicidechess/screens/mainmenu/MainMenuConfig.java b/core/src/main/java/com/group14/regicidechess/screens/mainmenu/MainMenuConfig.java index b755beb..d0c93af 100644 --- a/core/src/main/java/com/group14/regicidechess/screens/mainmenu/MainMenuConfig.java +++ b/core/src/main/java/com/group14/regicidechess/screens/mainmenu/MainMenuConfig.java @@ -20,7 +20,7 @@ private MainMenuConfig() {} // Prevent instantiation public static final int BUTTON_PAD_BOTTOM = 20; public static final int JOIN_BUTTON_PAD_BOTTOM = 16; public static final int JOIN_PANEL_WIDTH = 320; - + // Join panel specific public static final int GAME_ID_FIELD_WIDTH = 200; public static final int GAME_ID_FIELD_HEIGHT = 50; @@ -30,7 +30,10 @@ private MainMenuConfig() {} // Prevent instantiation public static final int BACK_BTN_HEIGHT = 48; public static final int JOIN_PANEL_PAD = 16; public static final int MAX_GAME_ID_LENGTH = 10; - + + // Quit button + public static final int QUIT_BTN_SIZE = 48; + // Colors public static final float BG_R = 0.12f; public static final float BG_G = 0.12f; diff --git a/core/src/main/java/com/group14/regicidechess/screens/mainmenu/MainMenuUI.java b/core/src/main/java/com/group14/regicidechess/screens/mainmenu/MainMenuUI.java index 0811a7a..3337697 100644 --- a/core/src/main/java/com/group14/regicidechess/screens/mainmenu/MainMenuUI.java +++ b/core/src/main/java/com/group14/regicidechess/screens/mainmenu/MainMenuUI.java @@ -1,9 +1,11 @@ // File: core/src/main/java/com/group14/regicidechess/screens/mainmenu/MainMenuUI.java package com.group14.regicidechess.screens.mainmenu; +import com.badlogic.gdx.Gdx; import com.badlogic.gdx.scenes.scene2d.Actor; import com.badlogic.gdx.scenes.scene2d.ui.Label; import com.badlogic.gdx.scenes.scene2d.ui.Skin; +import com.badlogic.gdx.scenes.scene2d.ui.Stack; import com.badlogic.gdx.scenes.scene2d.ui.Table; import com.badlogic.gdx.scenes.scene2d.ui.TextButton; import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener; @@ -11,7 +13,7 @@ /** * Builds and manages the main menu UI. - * Contains title, subtitle, and navigation buttons. + * Contains title, subtitle, navigation buttons, and a quit button. */ public class MainMenuUI { @@ -31,62 +33,54 @@ public MainMenuUI(Skin skin, MainMenuUIListener listener) { } public Table build() { - Table root = new Table(); - root.setFillParent(true); - root.setBackground(skin.getDrawable("surface-pixel")); + Table menu = new Table(); + menu.setFillParent(true); + menu.setBackground(skin.getDrawable("surface-pixel")); - // Title Label titleLabel = new Label("REGICIDE\nCHESS", skin, "title"); titleLabel.setAlignment(Align.center); - // Subtitle Label subtitleLabel = new Label("online strategy chess", skin, "small"); subtitleLabel.setAlignment(Align.center); - // Buttons TextButton createBtn = new TextButton("Create Lobby", skin, "accent"); TextButton joinBtn = new TextButton("Join Lobby", skin, "default"); createBtn.pad(12); joinBtn.pad(12); - // Error label mainErrorLabel = new Label("", skin, "small"); mainErrorLabel.setColor(com.badlogic.gdx.graphics.Color.RED); mainErrorLabel.setAlignment(Align.center); - // Layout - root.add(titleLabel) + menu.add(titleLabel) .expandX() .padTop(MainMenuConfig.TITLE_PAD_TOP) .padBottom(MainMenuConfig.TITLE_PAD_BOTTOM) .row(); - root.add(subtitleLabel) + menu.add(subtitleLabel) .expandX() .padBottom(MainMenuConfig.SUBTITLE_PAD_BOTTOM) .row(); - root.add(createBtn) + menu.add(createBtn) .width(MainMenuConfig.BUTTON_WIDTH) .height(MainMenuConfig.BUTTON_HEIGHT) .padBottom(MainMenuConfig.BUTTON_PAD_BOTTOM) .row(); - root.add(joinBtn) + menu.add(joinBtn) .width(MainMenuConfig.BUTTON_WIDTH) .height(MainMenuConfig.BUTTON_HEIGHT) .padBottom(MainMenuConfig.JOIN_BUTTON_PAD_BOTTOM) .row(); - root.add(mainErrorLabel) + menu.add(mainErrorLabel) .expandX() .padBottom(8) .row(); - // Button listeners createBtn.addListener(new ChangeListener() { @Override public void changed(ChangeEvent event, Actor actor) { clearError(); - if (listener != null) { - listener.onCreateLobby(); - } + if (listener != null) listener.onCreateLobby(); } }); @@ -94,13 +88,34 @@ public void changed(ChangeEvent event, Actor actor) { @Override public void changed(ChangeEvent event, Actor actor) { clearError(); - if (listener != null) { - listener.onJoinLobbyScreen(); - } + if (listener != null) listener.onJoinLobbyScreen(); } }); - return root; + TextButton quitBtn = new TextButton("X", skin, "default"); + quitBtn.addListener(new ChangeListener() { + @Override + public void changed(ChangeEvent event, Actor actor) { + Gdx.app.exit(); + } + }); + + Table overlay = new Table(); + overlay.setFillParent(true); + overlay.top().left().pad(16); + overlay.add(quitBtn) + .width(MainMenuConfig.QUIT_BTN_SIZE) + .height(MainMenuConfig.QUIT_BTN_SIZE); + + Stack root = new Stack(); + root.setFillParent(true); + root.add(menu); + root.add(overlay); + + Table wrapper = new Table(); + wrapper.setFillParent(true); + wrapper.add(root).grow(); + return wrapper; } public void showError(String message) { @@ -110,4 +125,4 @@ public void showError(String message) { public void clearError() { mainErrorLabel.setText(""); } -} +} \ No newline at end of file