mirror of
https://github.com/tiennm99/caro.git
synced 2026-09-04 16:16:45 +00:00
fix: wire up spectator mode end-to-end
Watch feature was half-implemented since day one and stayed broken through the typed-protobuf migration: 1. Server (WatchGameHandler): only pushed the WatchGameSuccessResponse ack. A mid-game watcher joined a blank scene because the server never told them who the players were or what moves had been played. 2. Client: had a Watch button that sent the request but no event bus handler for GAME_WATCH_SUCCESSFUL, so nothing happened visually. Server fix: - On successful watch, if room.status == STARTING, bootstrap the watcher with a synthesised GameStartingResponse (player ids, nicknames, board size) and replay every move in room.getMoveHistory() as individual GameMoveSuccessResponse messages on that channel. - Black/white player lookup uses room.getBlackPlayerId/getWhitePlayerId against the clientSideMap so we don't reassign roles. - Move replay resolves playerNickname from the same map. Client fix: - menu-ui.js: new GAME_WATCH_SUCCESSFUL handler flips gameState.isSpectating = true. The subsequent GameStartingResponse flows through the existing menu-scene handler (transitions to GameScene) and the existing game-state-service handler (populates room state, resets moves). - Move replay events propagate through the global GAME_MOVE_SUCCESS handler in game-state-service before GameScene.create() runs, so GameScene's existing rejoin/spectate loop at create() renders every stone.
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user