Stabilize playback state and cursor feedback
This commit is contained in:
@@ -4,6 +4,9 @@
|
||||
*/
|
||||
import { BaseModule } from './base-module.js';
|
||||
|
||||
const GAME_API_TIMEOUT_MS = 60000;
|
||||
const PLAYER_COMMAND_TIMEOUT_MS = 60000;
|
||||
|
||||
class SocketClientModule extends BaseModule {
|
||||
constructor() {
|
||||
super('socket-client', 'Socket Client');
|
||||
@@ -23,6 +26,10 @@ class SocketClientModule extends BaseModule {
|
||||
this.defaultHost = 'localhost:3000';
|
||||
this.receivedBlockCounter = 0;
|
||||
this.receivedParagraphCounter = 0;
|
||||
this.pendingCommandTimer = null;
|
||||
this.pendingCommand = null;
|
||||
this.gameApiTimeoutMs = GAME_API_TIMEOUT_MS;
|
||||
this.playerCommandTimeoutMs = PLAYER_COMMAND_TIMEOUT_MS;
|
||||
|
||||
// Bind methods using parent's bindMethods utility
|
||||
this.bindMethods([
|
||||
@@ -54,6 +61,9 @@ class SocketClientModule extends BaseModule {
|
||||
'cueMarkersFromTags',
|
||||
'dispatchChoices',
|
||||
'dispatchInputMode',
|
||||
'handleServerError',
|
||||
'clearPendingCommand',
|
||||
'translate',
|
||||
'isStructuralTag',
|
||||
'blocksFromTags',
|
||||
'enqueueStructuredBlock',
|
||||
@@ -191,8 +201,13 @@ class SocketClientModule extends BaseModule {
|
||||
|
||||
// Special handling for narrative text
|
||||
this.socket.on('narrativeResponse', (data) => {
|
||||
this.clearPendingCommand('narrative-response');
|
||||
this.processTurnResult(data);
|
||||
});
|
||||
|
||||
this.socket.on('error', (error) => {
|
||||
this.handleServerError(error);
|
||||
});
|
||||
|
||||
this.socket.on('gameConfig', (data) => {
|
||||
document.dispatchEvent(new CustomEvent('game:config', {
|
||||
@@ -300,6 +315,45 @@ class SocketClientModule extends BaseModule {
|
||||
}));
|
||||
}
|
||||
|
||||
handleServerError(error) {
|
||||
const message = String(error?.message || error?.error || error || 'The game server reported an error.');
|
||||
console.error('Socket Client: Server error event:', error);
|
||||
this.clearPendingCommand('server-error');
|
||||
document.dispatchEvent(new CustomEvent('story:tag', {
|
||||
detail: {
|
||||
key: 'error',
|
||||
value: message,
|
||||
source: 'server'
|
||||
}
|
||||
}));
|
||||
document.dispatchEvent(new CustomEvent('story:process-state', {
|
||||
detail: { state: 'ready', reason: 'server-error', message }
|
||||
}));
|
||||
}
|
||||
|
||||
clearPendingCommand(reason = 'cleared') {
|
||||
if (this.pendingCommandTimer) {
|
||||
clearTimeout(this.pendingCommandTimer);
|
||||
this.pendingCommandTimer = null;
|
||||
}
|
||||
if (this.pendingCommand) {
|
||||
console.log('Socket Client: Command wait cleared', {
|
||||
reason,
|
||||
command: this.pendingCommand
|
||||
});
|
||||
}
|
||||
this.pendingCommand = null;
|
||||
}
|
||||
|
||||
translate(key, fallback, params = {}) {
|
||||
const localization = this.getModule('localization');
|
||||
if (localization && typeof localization.translate === 'function') {
|
||||
const translated = localization.translate(key, params);
|
||||
if (translated && translated !== key) return translated;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
processParagraphResult(paragraph, turnId, pendingParagraph = null) {
|
||||
const pending = pendingParagraph && typeof pendingParagraph === 'object'
|
||||
? pendingParagraph
|
||||
@@ -652,7 +706,30 @@ class SocketClientModule extends BaseModule {
|
||||
}
|
||||
|
||||
try {
|
||||
this.clearPendingCommand('new-command');
|
||||
this.socket.emit('playerCommand', { command });
|
||||
this.pendingCommand = command;
|
||||
this.pendingCommandTimer = setTimeout(() => {
|
||||
const timedOutCommand = this.pendingCommand;
|
||||
this.clearPendingCommand('timeout');
|
||||
console.warn('Socket Client: Player command timed out', {
|
||||
timeoutMs: this.playerCommandTimeoutMs,
|
||||
command: timedOutCommand
|
||||
});
|
||||
document.dispatchEvent(new CustomEvent('story:tag', {
|
||||
detail: {
|
||||
key: 'alert',
|
||||
value: this.translate(
|
||||
'popup.commandTimeout',
|
||||
'The game server did not answer in time. You can try again.'
|
||||
),
|
||||
source: 'client-timeout'
|
||||
}
|
||||
}));
|
||||
document.dispatchEvent(new CustomEvent('story:process-state', {
|
||||
detail: { state: 'ready', reason: 'command-timeout', command: timedOutCommand }
|
||||
}));
|
||||
}, this.playerCommandTimeoutMs);
|
||||
document.dispatchEvent(new CustomEvent('story:process-state', {
|
||||
detail: { state: 'command-waiting', reason: 'command-sent', command }
|
||||
}));
|
||||
@@ -677,8 +754,26 @@ class SocketClientModule extends BaseModule {
|
||||
return;
|
||||
}
|
||||
|
||||
this.socket.emit('gameApi', { method, args }, (response) => {
|
||||
let settled = false;
|
||||
const finish = (response) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
clearTimeout(timeoutId);
|
||||
resolve(response || { success: false, error: 'empty_response' });
|
||||
};
|
||||
const timeoutId = setTimeout(() => {
|
||||
console.warn('Socket Client: gameApi call timed out', {
|
||||
method,
|
||||
timeoutMs: this.gameApiTimeoutMs
|
||||
});
|
||||
document.dispatchEvent(new CustomEvent('story:process-state', {
|
||||
detail: { state: 'ready', reason: 'game-api-timeout', method }
|
||||
}));
|
||||
finish({ success: false, error: 'timeout', method });
|
||||
}, this.gameApiTimeoutMs);
|
||||
|
||||
this.socket.emit('gameApi', { method, args }, (response) => {
|
||||
finish(response);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user