diff --git a/client/src/ui/menu-ui.js b/client/src/ui/menu-ui.js index bc3481a..80b3bc4 100644 --- a/client/src/ui/menu-ui.js +++ b/client/src/ui/menu-ui.js @@ -181,6 +181,16 @@ eventBus.on(ClientEventCode.ROOM_CREATE_SUCCESS, (data) => { eventBus.on(ClientEventCode.CLIENT_EXIT, showLobby); eventBus.on(ClientEventCode.CLIENT_KICK, showLobby); +// Spectator entry: server acknowledges with WatchGameSuccessResponse, then +// immediately follows with a GameStartingResponse + replayed GameMoveSuccess +// events. Flip the spectator flag here so the existing GAME_STARTING handlers +// (menu-scene + game-state-service) transition to GameScene without allowing +// click input. The replayed moves populate gameState.moves before GameScene +// creates, so its create() path will render all existing stones. +eventBus.on(ClientEventCode.GAME_WATCH_SUCCESSFUL, () => { + gameState.isSpectating = true; +}); + // Server emits NICKNAME_SET with invalidLength=0 on first connect to prompt // the user, and with invalidLength>0 as a rejection when a nickname submission // fails length validation. Only treat nonzero invalidLength as an error. diff --git a/server/src/main/java/com/miti99/caro/server/event/handler/WatchGameHandler.java b/server/src/main/java/com/miti99/caro/server/event/handler/WatchGameHandler.java index 1fbf893..c78c5f8 100644 --- a/server/src/main/java/com/miti99/caro/server/event/handler/WatchGameHandler.java +++ b/server/src/main/java/com/miti99/caro/server/event/handler/WatchGameHandler.java @@ -2,15 +2,23 @@ package com.miti99.caro.server.event.handler; import com.miti99.caro.common.channel.ChannelUtils; import com.miti99.caro.common.entity.ClientSide; +import com.miti99.caro.common.entity.GameMove; import com.miti99.caro.common.entity.Room; +import com.miti99.caro.common.enums.RoomStatus; +import com.miti99.caro.protocol.GameMoveSuccessResponse; +import com.miti99.caro.protocol.GameStartingResponse; import com.miti99.caro.protocol.Response; import com.miti99.caro.protocol.RoomJoinFailNotFoundResponse; import com.miti99.caro.protocol.WatchGameSuccessResponse; import com.miti99.caro.server.ServerContains; import com.miti99.caro.server.event.request.WatchGameRequestRecord; +import io.netty.channel.Channel; + public final class WatchGameHandler { + private static final int BOARD_SIZE = 15; + private WatchGameHandler() { } @@ -28,10 +36,49 @@ public final class WatchGameHandler { clientSide.setRoomId(room.getId()); room.getWatcherList().add(clientSide); - ChannelUtils.push(clientSide.getChannel(), Response.newBuilder() + Channel watcherChannel = clientSide.getChannel(); + ChannelUtils.push(watcherChannel, Response.newBuilder() .setWatchGameSuccess(WatchGameSuccessResponse.newBuilder() .setOwner(room.getRoomOwner() == null ? "" : room.getRoomOwner()) .setStatus(room.getStatus() == null ? "" : room.getStatus().toString())) .build()); + + // If the game is already running, bootstrap the watcher with the current + // player pairing and replay every move so their board renders correctly. + // Without this a mid-game watcher joins a blank scene. + if (room.getStatus() == RoomStatus.STARTING) { + bootstrapWatcher(watcherChannel, room); + } + } + + private static void bootstrapWatcher(Channel watcherChannel, Room room) { + ClientSide blackPlayer = room.getClientSideMap().get(room.getBlackPlayerId()); + ClientSide whitePlayer = room.getClientSideMap().get(room.getWhitePlayerId()); + if (blackPlayer == null || whitePlayer == null) { + return; + } + + ChannelUtils.push(watcherChannel, Response.newBuilder() + .setGameStarting(GameStartingResponse.newBuilder() + .setRoomId(room.getId()) + .setBlackPlayerId(blackPlayer.getId()) + .setBlackPlayerNickname(blackPlayer.getNickname() == null ? "" : blackPlayer.getNickname()) + .setWhitePlayerId(whitePlayer.getId()) + .setWhitePlayerNickname(whitePlayer.getNickname() == null ? "" : whitePlayer.getNickname()) + .setBoardSize(BOARD_SIZE)) + .build()); + + for (GameMove move : room.getMoveHistory()) { + ClientSide mover = room.getClientSideMap().get(move.getPlayerId()); + String moverNickname = mover != null && mover.getNickname() != null ? mover.getNickname() : ""; + ChannelUtils.push(watcherChannel, Response.newBuilder() + .setGameMoveSuccess(GameMoveSuccessResponse.newBuilder() + .setRow(move.getRow()) + .setCol(move.getCol()) + .setPiece(move.getPiece() == null ? "" : move.getPiece().name()) + .setPlayerNickname(moverNickname) + .setPlayerId(move.getPlayerId())) + .build()); + } } }