mirror of
https://github.com/tiennm99/gomoku.git
synced 2026-09-02 16:20:16 +00:00
fix(client): harden scene teardown to prevent PVP→PVE residue
Defensive cleanup to address the reported symptom "can not play PVE after PVP". Server-side flow tests pass in every permutation, so the residue must be in the client scene lifecycle. game-state-service: - Reset isSpectating on GAME_STARTING, not just on CLIENT_EXIT. If a prior watch session ever left the flag set (e.g., the user left via a path that didn't emit CLIENT_EXIT), a fresh game-starting signal would incorrectly treat the new player as a spectator and block clicks via isMyTurn() returning false. game-scene._cleanup: - Explicitly drop Phaser input listeners (pointerdown, pointermove). Phaser's scene shutdown is supposed to tear down the input plugin, but an explicit off() makes the cleanup obvious and robust against any lifecycle quirk in scene restart. - Null out this.board / this.stones / this.lastMarker / this.hoverGraphic / this.hoverPos so the next create() starts from a known-clean state, and late-firing events can't accidentally dereference stale Phaser game objects that the scene already destroyed. No functional change to the happy path — just defensive teardown that makes a scene restart observably idempotent. npm run build green.
This commit is contained in:
@@ -242,7 +242,7 @@ export class GameScene extends Phaser.Scene {
|
||||
showGameHud();
|
||||
}
|
||||
|
||||
/** Unsubscribe all event bus listeners. @private */
|
||||
/** Unsubscribe all event bus listeners and Phaser input handlers. @private */
|
||||
_cleanup() {
|
||||
eventBus.off(ClientEventCode.GAME_MOVE_SUCCESS, this._onMoveSuccess);
|
||||
eventBus.off(ClientEventCode.GAME_MOVE_INVALID, this._onMoveInvalid);
|
||||
@@ -253,6 +253,24 @@ export class GameScene extends Phaser.Scene {
|
||||
eventBus.off(ClientEventCode.GAME_OVER, this._onGameOver);
|
||||
eventBus.off(ClientEventCode.CLIENT_EXIT, this._onClientExit);
|
||||
eventBus.off(ClientEventCode.GAME_STARTING, this._onGameStarting);
|
||||
|
||||
// Defensive: drop Phaser input listeners in case the scene lifecycle
|
||||
// doesn't fully clean them (observed stale pointerdown between PVP→PVE
|
||||
// transitions). Safe to call even if the plugin is already torn down.
|
||||
if (this.input) {
|
||||
this.input.off('pointerdown', this._handleClick, this);
|
||||
this.input.off('pointermove', this._handleHover, this);
|
||||
}
|
||||
|
||||
// Drop references to destroyed Phaser objects so a subsequent create()
|
||||
// gets a fully fresh scene tree. stones[] may hold references that the
|
||||
// scene shutdown already destroyed; clearing here prevents accidental
|
||||
// reuse of stale handles by any late-firing event listener.
|
||||
this.stones = [];
|
||||
this.lastMarker = null;
|
||||
this.board = null;
|
||||
this.hoverGraphic = null;
|
||||
this.hoverPos = null;
|
||||
}
|
||||
|
||||
shutdown() {
|
||||
|
||||
@@ -116,6 +116,11 @@ class GameStateService {
|
||||
|
||||
eventBus.on(ClientEventCode.GAME_STARTING, (data) => {
|
||||
this.resetBoard();
|
||||
// A fresh game-starting signal always means this client is a player,
|
||||
// not a spectator. Clear stale flag in case a previous watch session
|
||||
// left it set (e.g., user transitioned out of watching via a path
|
||||
// that didn't emit CLIENT_EXIT).
|
||||
this.isSpectating = false;
|
||||
this.roomId = data.roomId;
|
||||
this.blackPlayerId = data.blackPlayerId;
|
||||
this.blackPlayerNickname = data.blackPlayerNickname;
|
||||
|
||||
Reference in New Issue
Block a user