diff --git a/android/src/main/java/com/group14/regicidechess/android/AndroidFirebase.java b/android/src/main/java/com/group14/regicidechess/android/AndroidFirebase.java index efd296d..b57bf72 100644 --- a/android/src/main/java/com/group14/regicidechess/android/AndroidFirebase.java +++ b/android/src/main/java/com/group14/regicidechess/android/AndroidFirebase.java @@ -355,4 +355,19 @@ private int getInt(DataSnapshot snapshot, String key) { if (val instanceof Integer) return (Integer) val; return 0; } + + @Override + public void unconfirmSetup(String gameId, boolean isWhite, Runnable onSuccess, Callback onError) { + String player = isWhite ? "white" : "black"; + DatabaseReference gameRef = db.child("games").child(gameId); + + // Clear the ready flag for this player and reset bothReady + gameRef.child("setup").child(player).removeValue() + .addOnSuccessListener(v -> { + gameRef.child("bothReady").removeValue() + .addOnSuccessListener(v2 -> onSuccess.run()) + .addOnFailureListener(e -> onError.call(e.getMessage())); + }) + .addOnFailureListener(e -> onError.call(e.getMessage())); + } } \ No newline at end of file diff --git a/core/src/main/java/com/group14/regicidechess/database/FirebaseAPI.java b/core/src/main/java/com/group14/regicidechess/database/FirebaseAPI.java index 140946c..c88bd7b 100644 --- a/core/src/main/java/com/group14/regicidechess/database/FirebaseAPI.java +++ b/core/src/main/java/com/group14/regicidechess/database/FirebaseAPI.java @@ -1,3 +1,4 @@ +// FirebaseAPI.java - Fix unconfirmSetup signature package com.group14.regicidechess.database; import com.group14.regicidechess.model.Lobby; @@ -39,6 +40,11 @@ public interface FirebaseAPI { */ void confirmSetup(String gameId, boolean isWhite, int[][] board, Runnable onSuccess); + /** + * Unconfirms the setup, marking this player as not ready. + */ + void unconfirmSetup(String gameId, boolean isWhite, Runnable onSuccess, Callback onError); + /** Fetches the opponent's stored board layout. */ void getOpponentBoard(String gameId, boolean localIsWhite, Callback onBoard); @@ -79,4 +85,4 @@ void listenForHeartbeat(String gameId, boolean listenForWhite, long timeoutMs, interface Callback { void call(T value); } -} +} \ No newline at end of file diff --git a/core/src/main/java/com/group14/regicidechess/screens/game/GameScreen.java b/core/src/main/java/com/group14/regicidechess/screens/game/GameScreen.java index eecfd4e..14cd5e5 100644 --- a/core/src/main/java/com/group14/regicidechess/screens/game/GameScreen.java +++ b/core/src/main/java/com/group14/regicidechess/screens/game/GameScreen.java @@ -205,7 +205,6 @@ public void render(float delta) { stage.act(delta); stage.draw(); - // Draw a semi-transparent dimmer behind any visible overlay if (overlayManager.isAnyOverlayVisible()) { Gdx.gl.glEnable(GL20.GL_BLEND); Gdx.gl.glBlendFunc(GL20.GL_SRC_ALPHA, GL20.GL_ONE_MINUS_SRC_ALPHA); @@ -217,7 +216,7 @@ public void render(float delta) { sr.end(); sr.dispose(); Gdx.gl.glDisable(GL20.GL_BLEND); - stage.draw(); // redraw so overlay card appears above dimmer + stage.draw(); } } @@ -295,7 +294,6 @@ private void executeMove(Vector2 from, Vector2 to) { ChessPiece movingPiece = inMatchState.getBoard().getPieceAt(from); if (movingPiece == null) { deselect(); return; } - // Execute locally — this also switches the turn inside InMatchState ChessPiece captured = inMatchState.executeMove(from, to); deselect(); @@ -305,7 +303,6 @@ private void executeMove(Vector2 from, Vector2 to) { overlayManager.showGameOver(true); } else if (isPawnPromotion(movingPiece, to)) { - // Do NOT save yet — wait for the player to pick a promotion piece pendingPromotionMove = new Move(from, to, movingPiece, localPlayer); overlayManager.showPromotion(); 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 24fb3d6..22e1f47 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 @@ -33,6 +33,7 @@ public interface HostUIListener { private Label statusLabel; private TextButton createBtn; private TextButton startBtn; + private Table sliderSection; public LobbyHostUI(Skin skin, HostUIListener listener) { this.skin = skin; @@ -82,19 +83,21 @@ public void changed(ChangeEvent event, Actor actor) { } }); - // Layout - container.add(buildRow(boardSizeLabel, boardSizeValueLabel)) + // Sliders grouped so they can be hidden after lobby is created + sliderSection = new Table(); + sliderSection.add(buildRow(boardSizeLabel, boardSizeValueLabel)) .expandX().fillX().padBottom(8).row(); - container.add(boardSlider) + sliderSection.add(boardSlider) .width(LobbyScreenConfig.SLIDER_WIDTH) .height(LobbyScreenConfig.SLIDER_HEIGHT) .padBottom(24).row(); - container.add(buildRow(budgetLabel, budgetValueLabel)) + sliderSection.add(buildRow(budgetLabel, budgetValueLabel)) .expandX().fillX().padBottom(8).row(); - container.add(budgetSlider) + sliderSection.add(budgetSlider) .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) @@ -166,6 +169,7 @@ public void showCreatingState() { } public void showLobbyCreated(String gameId) { + sliderSection.setVisible(false); createBtn.setVisible(false); startBtn.setVisible(true); startBtn.setDisabled(true); diff --git a/core/src/main/java/com/group14/regicidechess/screens/setup/SetupBoardInputHandler.java b/core/src/main/java/com/group14/regicidechess/screens/setup/SetupBoardInputHandler.java index bb8b67a..c6aa1a6 100644 --- a/core/src/main/java/com/group14/regicidechess/screens/setup/SetupBoardInputHandler.java +++ b/core/src/main/java/com/group14/regicidechess/screens/setup/SetupBoardInputHandler.java @@ -1,4 +1,4 @@ -// File: core/src/main/java/com/group14/regicidechess/screens/setup/SetupBoardInputHandler.java +// SetupBoardInputHandler.java - Updated version package com.group14.regicidechess.screens.setup; import com.group14.regicidechess.model.Player; @@ -14,6 +14,7 @@ public class SetupBoardInputHandler { public interface BoardActionListener { void onPiecePlaced(ChessPiece piece, int col, int row); void onPieceRemoved(int col, int row); + void onPieceReplaced(ChessPiece oldPiece, ChessPiece newPiece, int col, int row); void onInvalidPlacement(String reason); void onStateChanged(); } @@ -23,18 +24,32 @@ public interface BoardActionListener { private final SetupPaletteWidget palette; private final Player localPlayer; private final BoardActionListener listener; + private final boolean isLocked; public SetupBoardInputHandler(SetupState setupState, SetupBoardRenderer renderer, SetupPaletteWidget palette, Player localPlayer, BoardActionListener listener) { + this(setupState, renderer, palette, localPlayer, listener, false); + } + + public SetupBoardInputHandler(SetupState setupState, SetupBoardRenderer renderer, + SetupPaletteWidget palette, Player localPlayer, + BoardActionListener listener, boolean isLocked) { this.setupState = setupState; this.renderer = renderer; this.palette = palette; this.localPlayer = localPlayer; this.listener = listener; + this.isLocked = isLocked; } public boolean handleTap(float worldX, float worldY) { + // If locked, ignore all board interactions + if (isLocked) { + listener.onInvalidPlacement("Setup is locked. Unconfirm to make changes."); + return false; + } + float cellSize = renderer.getCellSize(); float boardLeft = renderer.getBoardLeft(); float boardBottom = renderer.getBoardBottom(); @@ -49,26 +64,62 @@ public boolean handleTap(float worldX, float worldY) { ChessPiece existing = setupState.getBoard().getPieceAt(col, row); - if (existing != null) { - // Remove existing piece + if (palette.hasSelection()) { + ChessPiece newPiece = palette.createSelectedPiece(localPlayer); + + if (existing != null) { + // Check if it's the same piece type + boolean isSameType = existing.getClass().equals(newPiece.getClass()); + + if (isSameType) { + // Same piece type = remove + setupState.removePiece(col, row); + listener.onPieceRemoved(col, row); + // Keep selection for future placements + listener.onStateChanged(); + } else { + // Different piece type = replace + // Check if we can afford the new piece after refunding the old one + int costDiff = newPiece.getPointCost() - existing.getPointCost(); + + if (costDiff <= setupState.getPlayer().getBudgetRemaining()) { + // First remove old piece (this refunds its cost) + setupState.removePiece(col, row); + // Then place new piece (this spends its cost) + boolean success = setupState.placePiece(newPiece, col, row); + + if (success) { + listener.onPieceReplaced(existing, newPiece, col, row); + palette.afterPiecePlaced(); + listener.onStateChanged(); + } else { + // If placement fails, put the old piece back + setupState.placePiece(existing, col, row); + listener.onInvalidPlacement("Cannot replace piece - check budget and placement rules."); + } + } else { + listener.onInvalidPlacement("Not enough budget to replace with " + newPiece.getTypeName() + + " (needs " + costDiff + " more)"); + } + } + } else { + // Empty square - normal placement + boolean success = setupState.placePiece(newPiece, col, row); + + if (success) { + listener.onPiecePlaced(newPiece, col, row); + palette.afterPiecePlaced(); + listener.onStateChanged(); + } else { + String reason = getPlacementFailureReason(newPiece); + listener.onInvalidPlacement(reason); + } + } + } else if (existing != null) { + // No piece selected, clicking on existing piece = remove setupState.removePiece(col, row); listener.onPieceRemoved(col, row); listener.onStateChanged(); - } else if (palette.hasSelection()) { - ChessPiece piece = palette.createSelectedPiece(localPlayer); - boolean success = setupState.placePiece(piece, col, row); - - if (success) { - // IMPORTANT: Do NOT clear selection - keep it so player can place more - // palette.afterPiecePlaced() is called but we don't clear selection - // The palette's keepSelectionAfterPlace flag is true by default - palette.afterPiecePlaced(); // This won't clear if keepSelectionAfterPlace is true - listener.onPiecePlaced(piece, col, row); - listener.onStateChanged(); - } else { - String reason = getPlacementFailureReason(piece); - listener.onInvalidPlacement(reason); - } } return true; @@ -87,4 +138,7 @@ private boolean kingIsOnBoard() { } return false; } + + public void setLocked(boolean locked) { + } } \ No newline at end of file diff --git a/core/src/main/java/com/group14/regicidechess/screens/setup/SetupFlowController.java b/core/src/main/java/com/group14/regicidechess/screens/setup/SetupFlowController.java index 860bcfb..99dc3f5 100644 --- a/core/src/main/java/com/group14/regicidechess/screens/setup/SetupFlowController.java +++ b/core/src/main/java/com/group14/regicidechess/screens/setup/SetupFlowController.java @@ -1,4 +1,4 @@ -// File: core/src/main/java/com/group14/regicidechess/screens/setup/SetupFlowController.java +// SetupFlowController.java - Updated version package com.group14.regicidechess.screens.setup; import com.badlogic.gdx.Gdx; @@ -17,6 +17,8 @@ public interface FlowListener { void onBothReady(); void onOpponentBoardFetched(Board finalBoard, Player opponent); void onError(String message); + void onUnconfirmSuccess(); + void onUnconfirmError(String message); } private final String gameId; @@ -26,6 +28,7 @@ public interface FlowListener { private final BoardFetchRetryPolicy retryPolicy; private int boardFetchRetries = 0; + private boolean isConfirmed = false; public SetupFlowController(String gameId, Player localPlayer, SetupState setupState, FlowListener listener) { this(gameId, localPlayer, setupState, listener, new BoardFetchRetryPolicy()); @@ -41,6 +44,11 @@ public SetupFlowController(String gameId, Player localPlayer, SetupState setupSt } public void confirm() { + if (isConfirmed) { + listener.onError("Setup already confirmed!"); + return; + } + // Check UI readiness (King placed) if (!setupState.isReadyForConfirm()) { listener.onError("Place your King before confirming."); @@ -53,6 +61,7 @@ public void confirm() { return; } + isConfirmed = true; listener.onUploadComplete(); int[][] encoded = SetupBoardCodec.encode(setupState.getBoard()); @@ -63,6 +72,31 @@ public void confirm() { this::listenForBothReady ); } + + public void unconfirm() { + if (!isConfirmed) { + listener.onError("Setup not confirmed yet!"); + return; + } + + // Reset the player's ready flag + setupState.getPlayer().resetReady(); + isConfirmed = false; + + DatabaseManager.getInstance().getApi().unconfirmSetup( + gameId, + localPlayer.isWhite(), + () -> Gdx.app.postRunnable(() -> { + listener.onUnconfirmSuccess(); + }), + error -> Gdx.app.postRunnable(() -> { + listener.onUnconfirmError(error); + // Revert flag if unconfirm fails + isConfirmed = true; + setupState.setReady(); + }) + ); + } private void listenForBothReady() { DatabaseManager.getInstance().getApi().listenForBothSetupReady( @@ -106,4 +140,8 @@ private void fetchOpponentBoard() { }) ); } + + public boolean isConfirmed() { + return isConfirmed; + } } \ No newline at end of file diff --git a/core/src/main/java/com/group14/regicidechess/screens/setup/SetupFooterWidget.java b/core/src/main/java/com/group14/regicidechess/screens/setup/SetupFooterWidget.java index b46d36e..f106234 100644 --- a/core/src/main/java/com/group14/regicidechess/screens/setup/SetupFooterWidget.java +++ b/core/src/main/java/com/group14/regicidechess/screens/setup/SetupFooterWidget.java @@ -1,4 +1,3 @@ -// File: core/src/main/java/com/group14/regicidechess/screens/setup/SetupFooterWidget.java package com.group14.regicidechess.screens.setup; import com.badlogic.gdx.scenes.scene2d.Actor; @@ -10,83 +9,126 @@ import com.badlogic.gdx.utils.Align; /** - * SetupFooterWidget — builds and manages the footer UI. + * SetupFooterWidget — three visual states: + * + * SETUP: [Clear] | "Place King..." or "Ready!" | [Confirm] + * WAITING: [Unconfirm] | "Waiting for opponent..." | (empty) + * LOCKED: same as WAITING but via setWaitingMode(true) */ public class SetupFooterWidget { public interface FooterListener { void onClear(); void onConfirm(); + void onUnconfirm(); } - private final Skin skin; + private final Skin skin; private final FooterListener listener; + private TextButton clearBtn; private TextButton confirmBtn; - private Label statusLabel; - private Label waitingLabel; + private TextButton unconfirmBtn; + private Label statusLabel; + private Label waitingLabel; + private Table footer; public SetupFooterWidget(Skin skin, FooterListener listener) { - this.skin = skin; + this.skin = skin; this.listener = listener; } public Table build() { - Table footer = new Table(); + footer = new Table(); footer.setBackground(skin.getDrawable("surface-pixel")); footer.pad(10); - TextButton clearBtn = new TextButton("Clear", skin, "danger"); - confirmBtn = new TextButton("Confirm", skin, "accent"); + clearBtn = new TextButton("Clear", skin, "danger"); + confirmBtn = new TextButton("Confirm", skin, "accent"); + unconfirmBtn = new TextButton("Unconfirm", skin, "default"); confirmBtn.setDisabled(true); statusLabel = new Label("Place your King to continue", skin, "small"); statusLabel.setAlignment(Align.center); + statusLabel.setWrap(false); // prevent vertical expansion waitingLabel = new Label("Waiting for opponent...", skin, "small"); waitingLabel.setAlignment(Align.center); - waitingLabel.setVisible(false); + waitingLabel.setWrap(false); clearBtn.addListener(new ChangeListener() { - @Override - public void changed(ChangeEvent event, Actor actor) { + @Override public void changed(ChangeEvent event, Actor actor) { listener.onClear(); } }); - confirmBtn.addListener(new ChangeListener() { - @Override - public void changed(ChangeEvent event, Actor actor) { - if (!confirmBtn.isDisabled()) { - listener.onConfirm(); - } + @Override public void changed(ChangeEvent event, Actor actor) { + if (!confirmBtn.isDisabled()) listener.onConfirm(); + } + }); + unconfirmBtn.addListener(new ChangeListener() { + @Override public void changed(ChangeEvent event, Actor actor) { + listener.onUnconfirm(); } }); - footer.add(clearBtn).width(140).height(50).expandX().left(); - footer.add(statusLabel).expandX(); - footer.add(confirmBtn).width(140).height(50).expandX().right(); - + showSetupMode(); // build initial layout return footer; } + // ── Mode transitions ────────────────────────────────────────────────────── + + /** + * Normal editing state: + * [Clear] | status text | [Confirm] + */ + public void showSetupMode() { + if (footer == null) return; + footer.clearChildren(); + footer.add(clearBtn).width(110).height(48).left(); + footer.add(statusLabel).expandX().center().padLeft(8).padRight(8); + footer.add(confirmBtn).width(110).height(48).right(); + footer.layout(); + } + + /** + * Waiting / confirmed state: + * [Unconfirm] | "Waiting for opponent..." | (spacer) + */ + public void showWaitingMode() { + if (footer == null) return; + footer.clearChildren(); + footer.add(unconfirmBtn).width(130).height(48).left(); + footer.add(waitingLabel).expandX().center().padLeft(8).padRight(8); + footer.add().width(130); // mirror spacer so waiting label stays centred + footer.layout(); + } + + // ── Individual updates ──────────────────────────────────────────────────── + public void setConfirmEnabled(boolean enabled) { - confirmBtn.setDisabled(!enabled); + if (confirmBtn != null) confirmBtn.setDisabled(!enabled); } - public void setStatusMessage(String message) { - statusLabel.setText(message); + public void setStatusMessage(String msg) { + if (statusLabel != null) statusLabel.setText(msg); } + // ── Legacy compatibility ────────────────────────────────────────────────── + + /** @deprecated Use showWaitingMode() / showSetupMode() directly. */ + @Deprecated public void setWaitingMode(boolean waiting) { - confirmBtn.setVisible(!waiting); - waitingLabel.setVisible(waiting); - if (waiting) { - setStatusMessage("Waiting for opponent to finish setup..."); - } + if (waiting) showWaitingMode(); else showSetupMode(); } - public Label getWaitingLabel() { - return waitingLabel; + /** @deprecated Use showWaitingMode() / showSetupMode() directly. */ + @Deprecated + public void setConfirmed(boolean confirmed) { + if (confirmed) showWaitingMode(); else showSetupMode(); } + + /** @deprecated No longer needed — waiting label is embedded. */ + @Deprecated + public Label getWaitingLabel() { return waitingLabel; } } \ No newline at end of file diff --git a/core/src/main/java/com/group14/regicidechess/screens/setup/SetupScreen.java b/core/src/main/java/com/group14/regicidechess/screens/setup/SetupScreen.java index 3ec4893..d64d1f3 100644 --- a/core/src/main/java/com/group14/regicidechess/screens/setup/SetupScreen.java +++ b/core/src/main/java/com/group14/regicidechess/screens/setup/SetupScreen.java @@ -1,4 +1,4 @@ -// File: core/src/main/java/com/group14/regicidechess/screens/setup/SetupScreen.java +// SetupScreen.java - Updated with exception handling package com.group14.regicidechess.screens.setup; import com.badlogic.gdx.Game; @@ -14,21 +14,11 @@ import com.group14.regicidechess.input.ScreenInputHandler; import com.group14.regicidechess.model.Player; import com.group14.regicidechess.model.pieces.ChessPiece; -import com.group14.regicidechess.model.pieces.King; import com.group14.regicidechess.screens.game.GameScreen; import com.group14.regicidechess.states.SetupState; import com.group14.regicidechess.utils.ResourceManager; -/** - * SetupScreen — thin coordinator for the piece-placement phase. - * - * Refactored version with improved modularity: - * - UI components extracted to separate widgets - * - Firebase flow extracted to SetupFlowController - * - Board input logic extracted to SetupBoardInputHandler - * - Coordinate mapping extracted to BoardCoordinateMapper - * - Palette keeps selection after placing pieces for faster placement - */ + public class SetupScreen implements Screen, ScreenInputHandler.ScreenInputObserver { // LibGDX @@ -44,6 +34,7 @@ public class SetupScreen implements Screen, ScreenInputHandler.ScreenInputObserv private final String gameId; @SuppressWarnings("unused") private final boolean isHost; + private boolean isConfirmed = false; // UI Widgets private final SetupHeaderWidget headerWidget; @@ -52,8 +43,8 @@ public class SetupScreen implements Screen, ScreenInputHandler.ScreenInputObserv // Helpers private final SetupBoardRenderer boardRenderer; - private final SetupBoardInputHandler boardInputHandler; - private final SetupFlowController flowController; + private SetupBoardInputHandler boardInputHandler; + private SetupFlowController flowController; @SuppressWarnings("unused") private Table root; @@ -65,52 +56,68 @@ public SetupScreen(Game game, SpriteBatch batch, this.gameId = gameId; this.isHost = isHost; - // Host = white (player1), joiner = black (player2) - this.localPlayer = new Player(isHost ? "player1" : "player2", isHost, budget); - - // Setup state - this.setupState = new SetupState(); - setupState.setBoardSize(boardSize); - setupState.setBudget(budget); - setupState.setPlayer(localPlayer); - setupState.enter(); - - // LibGDX setup - this.stage = new Stage(new FitViewport( - SetupScreenConfig.VIEWPORT_WIDTH, - SetupScreenConfig.VIEWPORT_HEIGHT), - batch); - this.skin = ResourceManager.getInstance().getSkin(); - this.inputHandler = new ScreenInputHandler(); - inputHandler.addObserver(this); - - // Create widgets and helpers - // IMPORTANT: Pass true to keep selection after placing pieces - this.palette = new SetupPaletteWidget(skin, this::onPaletteSelectionChanged, true); - - BoardCoordinateMapper coordinateMapper = new BoardCoordinateMapper(localPlayer, boardSize); - - this.boardRenderer = new SetupBoardRenderer(batch, coordinateMapper); - this.headerWidget = new SetupHeaderWidget(skin, localPlayer); - this.footerWidget = new SetupFooterWidget(skin, createFooterListener()); - - this.boardInputHandler = new SetupBoardInputHandler( - setupState, boardRenderer, palette, localPlayer, createBoardActionListener()); - - this.flowController = new SetupFlowController( - gameId, localPlayer, setupState, createFlowListener()); - - buildUI(); - boardRenderer.computeGeometry( - SetupScreenConfig.VIEWPORT_WIDTH, - SetupScreenConfig.VIEWPORT_HEIGHT, - SetupScreenConfig.HEADER_HEIGHT, - SetupScreenConfig.PALETTE_HEIGHT, - SetupScreenConfig.FOOTER_HEIGHT, - boardSize); - - // Log initial state for debugging - Gdx.app.log("SetupScreen", "Initialized with board size: " + boardSize + ", budget: " + budget); + try { + // Host = white (player1), joiner = black (player2) + this.localPlayer = new Player(isHost ? "player1" : "player2", isHost, budget); + + // Setup state + this.setupState = new SetupState(); + setupState.setBoardSize(boardSize); + setupState.setBudget(budget); + setupState.setPlayer(localPlayer); + setupState.enter(); + + // LibGDX setup + this.stage = new Stage(new FitViewport( + SetupScreenConfig.VIEWPORT_WIDTH, + SetupScreenConfig.VIEWPORT_HEIGHT), + batch); + this.skin = ResourceManager.getInstance().getSkin(); + this.inputHandler = new ScreenInputHandler(); + inputHandler.addObserver(this); + + // Create widgets and helpers + this.palette = new SetupPaletteWidget(skin, this::onPaletteSelectionChanged, true); + + BoardCoordinateMapper coordinateMapper = new BoardCoordinateMapper(localPlayer, boardSize); + + this.boardRenderer = new SetupBoardRenderer(batch, coordinateMapper); + this.headerWidget = new SetupHeaderWidget(skin, localPlayer); + this.footerWidget = new SetupFooterWidget(skin, createFooterListener()); + + this.boardInputHandler = new SetupBoardInputHandler( + setupState, boardRenderer, palette, localPlayer, createBoardActionListener(), false); + + this.flowController = new SetupFlowController( + gameId, localPlayer, setupState, createFlowListener()); + + buildUI(); + boardRenderer.computeGeometry( + SetupScreenConfig.VIEWPORT_WIDTH, + SetupScreenConfig.VIEWPORT_HEIGHT, + SetupScreenConfig.HEADER_HEIGHT, + SetupScreenConfig.PALETTE_HEIGHT, + SetupScreenConfig.FOOTER_HEIGHT, + boardSize); + + Gdx.app.log("SetupScreen", "Initialized with board size: " + boardSize + ", budget: " + budget); + } catch (Exception e) { + Gdx.app.error("SetupScreen", "Error initializing SetupScreen: " + e.getMessage(), e); + showErrorAndExit("Failed to initialize setup: " + e.getMessage()); + throw new RuntimeException("SetupScreen initialization failed", e); + } + } + + private void showErrorAndExit(String message) { + Gdx.app.error("SetupScreen", message); + // Try to go back to main menu + try { + com.group14.regicidechess.screens.mainmenu.MainMenuScreen mainMenu = + new com.group14.regicidechess.screens.mainmenu.MainMenuScreen(game, batch); + game.setScreen(mainMenu); + } catch (Exception e) { + Gdx.app.error("SetupScreen", "Could not return to main menu", e); + } } // ───────────────────────────────────────────────────────────────────────── @@ -118,35 +125,39 @@ public SetupScreen(Game game, SpriteBatch batch, // ───────────────────────────────────────────────────────────────────────── private void buildUI() { - root = new Table(); - root.setFillParent(true); - root.top(); - stage.addActor(root); - - // Header - root.add(headerWidget.build()) - .expandX().fillX() - .height(SetupScreenConfig.HEADER_HEIGHT) - .row(); - - // Spacer for board area (drawn by boardRenderer) - root.add().expandX().expandY().row(); - - // Palette - root.add(palette.buildWidget()) - .expandX().fillX() - .height(SetupScreenConfig.PALETTE_HEIGHT) - .row(); - - // Footer - root.add(footerWidget.build()) - .expandX().fillX() - .height(SetupScreenConfig.FOOTER_HEIGHT) - .row(); - - // Waiting label (initially hidden) - root.add(footerWidget.getWaitingLabel()) - .expandX().padTop(8).row(); + try { + root = new Table(); + root.setFillParent(true); + root.top(); + stage.addActor(root); + + // Header + root.add(headerWidget.build()) + .expandX().fillX() + .height(SetupScreenConfig.HEADER_HEIGHT) + .row(); + + // Spacer for board area (drawn by boardRenderer) + root.add().expandX().expandY().row(); + + // Palette + root.add(palette.buildWidget()) + .expandX().fillX() + .height(SetupScreenConfig.PALETTE_HEIGHT) + .row(); + + // Footer + root.add(footerWidget.build()) + .expandX().fillX() + .height(SetupScreenConfig.FOOTER_HEIGHT) + .row(); + + // Waiting label (initially hidden) + root.add(footerWidget.getWaitingLabel()) + .expandX().padTop(8).row(); + } catch (Exception e) { + Gdx.app.error("SetupScreen", "Error building UI: " + e.getMessage(), e); + } } // ───────────────────────────────────────────────────────────────────────── @@ -157,16 +168,41 @@ private SetupFooterWidget.FooterListener createFooterListener() { return new SetupFooterWidget.FooterListener() { @Override public void onClear() { - Gdx.app.log("SetupScreen", "Clear button pressed"); - setupState.clearBoard(); - palette.clearSelection(); - refreshUI(); + try { + if (isConfirmed) { + showStatus("Cannot clear board while confirmed. Press Unconfirm first."); + return; + } + Gdx.app.log("SetupScreen", "Clear button pressed"); + setupState.clearBoard(); + palette.clearSelection(); + refreshUI(); + } catch (Exception e) { + Gdx.app.error("SetupScreen", "Error clearing board: " + e.getMessage(), e); + showStatus("Error clearing board: " + e.getMessage()); + } } @Override public void onConfirm() { - Gdx.app.log("SetupScreen", "Confirm button pressed"); - flowController.confirm(); + try { + Gdx.app.log("SetupScreen", "Confirm button pressed"); + flowController.confirm(); + } catch (Exception e) { + Gdx.app.error("SetupScreen", "Error confirming setup: " + e.getMessage(), e); + showStatus("Error confirming: " + e.getMessage()); + } + } + + @Override + public void onUnconfirm() { + try { + Gdx.app.log("SetupScreen", "Unconfirm button pressed"); + flowController.unconfirm(); + } catch (Exception e) { + Gdx.app.error("SetupScreen", "Error unconfirming setup: " + e.getMessage(), e); + showStatus("Error unconfirming: " + e.getMessage()); + } } }; } @@ -185,6 +221,13 @@ public void onPieceRemoved(int col, int row) { refreshUI(); } + @Override + public void onPieceReplaced(ChessPiece oldPiece, ChessPiece newPiece, int col, int row) { + Gdx.app.log("SetupScreen", "Piece replaced: " + oldPiece.getTypeName() + + " -> " + newPiece.getTypeName() + " at (" + col + ", " + row + ")"); + refreshUI(); + } + @Override public void onInvalidPlacement(String reason) { Gdx.app.log("SetupScreen", "Invalid placement: " + reason); @@ -203,49 +246,104 @@ private SetupFlowController.FlowListener createFlowListener() { @Override public void onUploadComplete() { Gdx.app.postRunnable(() -> { - Gdx.app.log("SetupScreen", "Upload complete, waiting for opponent"); - footerWidget.setConfirmEnabled(false); - footerWidget.setWaitingMode(true); + try { + Gdx.app.log("SetupScreen", "Upload complete, waiting for opponent"); + isConfirmed = true; + footerWidget.setConfirmed(true); + footerWidget.setConfirmEnabled(true); + footerWidget.setWaitingMode(true); + updateBoardInputLock(true); + } catch (Exception e) { + Gdx.app.error("SetupScreen", "Error in onUploadComplete: " + e.getMessage(), e); + } + }); + } + + @Override + public void onUnconfirmSuccess() { + Gdx.app.postRunnable(() -> { + try { + Gdx.app.log("SetupScreen", "Unconfirm successful"); + isConfirmed = false; + footerWidget.setConfirmed(false); + footerWidget.setWaitingMode(false); + footerWidget.setConfirmEnabled(setupState.isReadyForConfirm()); + updateBoardInputLock(false); + showStatus("Setup unlocked. You can now make changes."); + } catch (Exception e) { + Gdx.app.error("SetupScreen", "Error in onUnconfirmSuccess: " + e.getMessage(), e); + } + }); + } + + @Override + public void onUnconfirmError(String message) { + Gdx.app.postRunnable(() -> { + try { + Gdx.app.log("SetupScreen", "Unconfirm error: " + message); + showStatus("Failed to unconfirm: " + message); + } catch (Exception e) { + Gdx.app.error("SetupScreen", "Error in onUnconfirmError: " + e.getMessage(), e); + } }); } @Override public void onBothReady() { Gdx.app.log("SetupScreen", "Both players ready"); - // Nothing to do here, waiting for board fetch } @Override public void onOpponentBoardFetched(com.group14.regicidechess.model.Board finalBoard, Player opponent) { Gdx.app.postRunnable(() -> { - Gdx.app.log("SetupScreen", "Opponent board fetched, navigating to GameScreen"); - game.setScreen(new GameScreen( - game, batch, - finalBoard, - localPlayer, - setupState.getBoardSize(), - gameId)); + try { + Gdx.app.log("SetupScreen", "Opponent board fetched, navigating to GameScreen"); + game.setScreen(new GameScreen( + game, batch, + finalBoard, + localPlayer, + setupState.getBoardSize(), + gameId)); + } catch (Exception e) { + Gdx.app.error("SetupScreen", "Error navigating to GameScreen: " + e.getMessage(), e); + showStatus("Error starting game: " + e.getMessage()); + } }); } @Override public void onError(String message) { Gdx.app.postRunnable(() -> { - Gdx.app.log("SetupScreen", "Error: " + message); - showStatus(message); - footerWidget.setConfirmEnabled(true); - footerWidget.setWaitingMode(false); + try { + Gdx.app.log("SetupScreen", "Error: " + message); + showStatus(message); + isConfirmed = false; + footerWidget.setConfirmed(false); + footerWidget.setConfirmEnabled(true); + footerWidget.setWaitingMode(false); + updateBoardInputLock(false); + } catch (Exception e) { + Gdx.app.error("SetupScreen", "Error in onError: " + e.getMessage(), e); + } }); } }; } + + private void updateBoardInputLock(boolean locked) { + try { + this.boardInputHandler = new SetupBoardInputHandler( + setupState, boardRenderer, palette, localPlayer, createBoardActionListener(), locked); + } catch (Exception e) { + Gdx.app.error("SetupScreen", "Error updating board input lock: " + e.getMessage(), e); + } + } private void onPaletteSelectionChanged(int selectedIndex) { if (selectedIndex >= 0) { Gdx.app.log("SetupScreen", "Selected piece: " + SetupPaletteWidget.PIECE_NAMES[selectedIndex]); } - // Widget handles its own button styles, no action needed } // ───────────────────────────────────────────────────────────────────────── @@ -254,18 +352,20 @@ private void onPaletteSelectionChanged(int selectedIndex) { @Override public void render(float delta) { - Gdx.gl.glClearColor(0.12f, 0.12f, 0.15f, 1f); - Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT); - setupState.update(delta); - - // Board is drawn before the stage so pieces sit below Scene2D actors - boardRenderer.drawBoard(stage.getCamera().combined, setupState); - stage.act(delta); - stage.draw(); - - // Palette sprites are drawn after the stage so they appear on top of buttons - boardRenderer.drawPaletteSprites(stage.getCamera().combined, - palette.getButtons(), SetupPaletteWidget.PIECE_NAMES); + try { + Gdx.gl.glClearColor(0.12f, 0.12f, 0.15f, 1f); + Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT); + setupState.update(delta); + + boardRenderer.drawBoard(stage.getCamera().combined, setupState); + stage.act(delta); + stage.draw(); + + boardRenderer.drawPaletteSprites(stage.getCamera().combined, + palette.getButtons(), SetupPaletteWidget.PIECE_NAMES); + } catch (Exception e) { + Gdx.app.error("SetupScreen", "Error in render: " + e.getMessage(), e); + } } // ───────────────────────────────────────────────────────────────────────── @@ -274,8 +374,12 @@ public void render(float delta) { @Override public void onTap(int screenX, int screenY, int pointer, int button) { - Vector2 world = stage.getViewport().unproject(new Vector2(screenX, screenY)); - boardInputHandler.handleTap(world.x, world.y); + try { + Vector2 world = stage.getViewport().unproject(new Vector2(screenX, screenY)); + boardInputHandler.handleTap(world.x, world.y); + } catch (Exception e) { + Gdx.app.error("SetupScreen", "Error handling tap: " + e.getMessage(), e); + } } @Override @@ -291,28 +395,37 @@ public void onKeyDown(int keycode) {} // UI refresh // ───────────────────────────────────────────────────────────────────────── - private void refreshUI() { - headerWidget.refreshBudget(); - - // Use the correct method for UI readiness - boolean kingPlaced = setupState.isReadyForConfirm(); - footerWidget.setConfirmEnabled(kingPlaced); - - // Debug logging - int pieceCount = setupState.getBoard().getPieces().size(); - int playerPieces = setupState.getBoard().getPieces(setupState.getPlayer()).size(); - Gdx.app.log("SetupScreen", "Refresh UI - King placed: " + kingPlaced + - ", Total pieces: " + pieceCount + - ", Player pieces: " + playerPieces + - ", Budget remaining: " + localPlayer.getBudgetRemaining()); - - showStatus(kingPlaced - ? "Ready! Press Confirm when done." - : "Place your King to continue."); + private void refreshUI() { + try { + headerWidget.refreshBudget(); + + boolean kingPlaced = setupState.isReadyForConfirm(); + if (!isConfirmed) { + footerWidget.setConfirmEnabled(kingPlaced); + } + + int pieceCount = setupState.getBoard().getPieces().size(); + int playerPieces = setupState.getBoard().getPieces(setupState.getPlayer()).size(); + Gdx.app.log("SetupScreen", "Refresh UI - King placed: " + kingPlaced + + ", Confirmed: " + isConfirmed + + ", Total pieces: " + pieceCount + + ", Player pieces: " + playerPieces + + ", Budget remaining: " + localPlayer.getBudgetRemaining()); + + showStatus(kingPlaced + ? (isConfirmed ? "Setup confirmed! Press Unconfirm to make changes." : "Ready! Press Confirm when done.") + : "Place your King to continue."); + } catch (Exception e) { + Gdx.app.error("SetupScreen", "Error refreshing UI: " + e.getMessage(), e); + } } private void showStatus(String msg) { - footerWidget.setStatusMessage(msg); + try { + footerWidget.setStatusMessage(msg); + } catch (Exception e) { + Gdx.app.error("SetupScreen", "Error showing status: " + e.getMessage(), e); + } } // ───────────────────────────────────────────────────────────────────────── @@ -321,21 +434,29 @@ private void showStatus(String msg) { @Override public void show() { - Gdx.app.log("SetupScreen", "Screen shown"); - Gdx.input.setInputProcessor( - new com.badlogic.gdx.InputMultiplexer(stage, inputHandler)); + try { + Gdx.app.log("SetupScreen", "Screen shown"); + Gdx.input.setInputProcessor( + new com.badlogic.gdx.InputMultiplexer(stage, inputHandler)); + } catch (Exception e) { + Gdx.app.error("SetupScreen", "Error in show: " + e.getMessage(), e); + } } @Override public void resize(int width, int height) { - stage.getViewport().update(width, height, true); - boardRenderer.computeGeometry( - SetupScreenConfig.VIEWPORT_WIDTH, - SetupScreenConfig.VIEWPORT_HEIGHT, - SetupScreenConfig.HEADER_HEIGHT, - SetupScreenConfig.PALETTE_HEIGHT, - SetupScreenConfig.FOOTER_HEIGHT, - setupState.getBoardSize()); + try { + stage.getViewport().update(width, height, true); + boardRenderer.computeGeometry( + SetupScreenConfig.VIEWPORT_WIDTH, + SetupScreenConfig.VIEWPORT_HEIGHT, + SetupScreenConfig.HEADER_HEIGHT, + SetupScreenConfig.PALETTE_HEIGHT, + SetupScreenConfig.FOOTER_HEIGHT, + setupState.getBoardSize()); + } catch (Exception e) { + Gdx.app.error("SetupScreen", "Error in resize: " + e.getMessage(), e); + } } @Override @@ -346,15 +467,23 @@ public void resume() {} @Override public void hide() { - Gdx.app.log("SetupScreen", "Screen hidden"); - inputHandler.clearObservers(); - setupState.exit(); + try { + Gdx.app.log("SetupScreen", "Screen hidden"); + inputHandler.clearObservers(); + setupState.exit(); + } catch (Exception e) { + Gdx.app.error("SetupScreen", "Error in hide: " + e.getMessage(), e); + } } @Override public void dispose() { - Gdx.app.log("SetupScreen", "Screen disposed"); - stage.dispose(); - boardRenderer.dispose(); + try { + Gdx.app.log("SetupScreen", "Screen disposed"); + stage.dispose(); + boardRenderer.dispose(); + } catch (Exception e) { + Gdx.app.error("SetupScreen", "Error in dispose: " + e.getMessage(), e); + } } } \ No newline at end of file diff --git a/core/src/main/java/com/group14/regicidechess/utils/ResourceManager.java b/core/src/main/java/com/group14/regicidechess/utils/ResourceManager.java index 0ec4046..764f29b 100644 --- a/core/src/main/java/com/group14/regicidechess/utils/ResourceManager.java +++ b/core/src/main/java/com/group14/regicidechess/utils/ResourceManager.java @@ -266,7 +266,6 @@ public Texture getPieceTexture(String color, String type) { if (Gdx.files.internal(path).exists()) { return getTexture(path); } - // Placeholder: tinted 1×1 pixel (will look like a coloured square) return getSolidTexture(color.equals("white") ? Color.WHITE : Color.DARK_GRAY); }