Checkpoint current interactive fiction state

This commit is contained in:
2026-05-14 21:17:43 +02:00
parent c745efd1d2
commit 873049f7e6
183 changed files with 13755 additions and 1459 deletions
+150 -16
View File
@@ -5,7 +5,7 @@ class UIInputHandlerModule extends BaseModule {
super('ui-input-handler', 'UI Input Handler');
// Explicitly declare ui-display-handler as a dependency
this.dependencies = ['ui-display-handler'];
this.dependencies = ['ui-display-handler', 'markup-parser', 'playback-coordinator'];
// Input elements
this.inputArea = null;
@@ -28,7 +28,10 @@ class UIInputHandlerModule extends BaseModule {
'handleKeyboardInput',
'submitCommand',
'addToHistory',
'resetCursorPosition'
'formatCommandHistory',
'resetCursorPosition',
'focusInput',
'setProcessState'
]);
console.log('UIInputHandler: Constructor initialized');
@@ -53,6 +56,9 @@ class UIInputHandlerModule extends BaseModule {
this.reportProgress(60, 'Setting up input elements');
this.setupInputElements();
this.addEventListener(document, 'story:process-state', (event) => {
this.setProcessState(event.detail?.state || 'ready', event.detail || {});
});
this.reportProgress(100, 'UI Input Handler ready');
return true;
@@ -67,14 +73,47 @@ class UIInputHandlerModule extends BaseModule {
* @param {KeyboardEvent} event - The keyboard event
*/
handleKeyboardInput(event) {
// Handle global keyboard shortcuts here
// This is different from the input field's specific key handling
// For example: Escape key to blur the input
if (!this.playerInput) return;
if (event.key === 'Escape') {
if (document.activeElement === this.playerInput) {
this.playerInput.blur();
this.playerInput.blur();
return;
}
const optionsModal = document.getElementById('options-modal');
if (optionsModal && optionsModal.style.display !== 'none') {
return;
}
if (event.key === ' ' && this.isPlaybackActive()) {
document.dispatchEvent(new CustomEvent('ui:command', {
detail: { type: 'continue', source: 'spacebar' }
}));
}
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
if (document.body.dataset.gameRunning !== 'true') {
return;
}
this.submitCommand();
return;
}
if (event.ctrlKey || event.metaKey || event.altKey) {
return;
}
if (event.key.length === 1 && document.activeElement !== this.playerInput) {
if (document.body.dataset.gameRunning !== 'true') {
return;
}
event.preventDefault();
this.focusInput();
const start = this.playerInput.selectionStart ?? this.playerInput.value.length;
const end = this.playerInput.selectionEnd ?? start;
this.playerInput.setRangeText(event.key, start, end, 'end');
this.playerInput.dispatchEvent(new Event('input', { bubbles: true }));
}
}
@@ -144,8 +183,8 @@ class UIInputHandlerModule extends BaseModule {
playerInput.style.whiteSpace = 'pre-wrap';
inputWrapper.appendChild(playerInput);
this.playerInput = playerInput;
}
this.playerInput = playerInput;
// Create the cursor if needed
let cursor = document.getElementById('cursor');
@@ -153,8 +192,8 @@ class UIInputHandlerModule extends BaseModule {
cursor = document.createElement('span');
cursor.id = 'cursor';
inputWrapper.appendChild(cursor);
this.cursor = cursor;
}
this.cursor = cursor;
// Set up input event handlers
if (playerInput) {
@@ -171,16 +210,94 @@ class UIInputHandlerModule extends BaseModule {
// Position the cursor
if (playerInput && cursor) {
this.positionCursor(playerInput, cursor);
// Focus the input to let user start typing immediately
setTimeout(() => {
playerInput.focus();
}, 100);
this.setProcessState('ready', { reason: 'input-initialized' });
this.focusInput();
requestAnimationFrame(() => this.focusInput());
setTimeout(() => this.focusInput(), 250);
}
console.log('UIInputHandler: Input elements setup complete');
}
focusInput() {
if (document.body.dataset.gameRunning !== 'true') {
return;
}
if (!this.playerInput) {
this.playerInput = document.getElementById('player_input');
}
if (this.playerInput && !this.playerInput.disabled) {
this.playerInput.focus();
}
}
setProcessState(state, detail = {}) {
const knownStates = [
'ready',
'command-waiting',
'waiting-generating',
'playing-generating',
'playing-ready'
];
const nextState = knownStates.includes(state) ? state : 'ready';
this.applyMouseCursor(nextState);
if (this.cursor) {
knownStates.forEach(value => this.cursor.classList.remove(`cursor-${value}`));
this.cursor.classList.add(`cursor-${nextState}`);
this.cursor.dataset.processState = nextState;
this.cursor.setAttribute('aria-label', 'text input cursor');
this.cursor.innerHTML = '';
}
console.log(`Cursor process state: ${nextState}`, detail);
}
applyMouseCursor(state) {
const root = document.documentElement;
if (!root) {
return;
}
root.dataset.processState = state;
const cursor = this.getMouseCursor(state);
if (cursor) {
root.style.setProperty('--process-cursor', cursor);
} else {
root.style.removeProperty('--process-cursor');
}
}
getMouseCursor(state) {
if (state === 'ready') {
return '';
}
const svg = this.getMouseCursorSvg(state);
const fallback = state === 'command-waiting' ? 'wait' : 'progress';
return `url("${this.toCursorDataUrl(svg)}") 12 12, ${fallback}`;
}
getMouseCursorSvg(state) {
const stroke = '#222222';
const common = `xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="${stroke}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"`;
const icons = {
'command-waiting': `<svg ${common}><path d="M5 22h14"/><path d="M5 2h14"/><path d="M17 22v-4.172a2 2 0 0 0-.586-1.414L12 12l-4.414 4.414A2 2 0 0 0 7 17.828V22"/><path d="M7 2v4.172a2 2 0 0 0 .586 1.414L12 12l4.414-4.414A2 2 0 0 0 17 6.172V2"/></svg>`,
'waiting-generating': `<svg ${common}><path d="M12 2v4"/><path d="M12 18v4"/><path d="m4.93 4.93 2.83 2.83"/><path d="m16.24 16.24 2.83 2.83"/><path d="M2 12h4"/><path d="M18 12h4"/><path d="m4.93 19.07 2.83-2.83"/><path d="m16.24 7.76 2.83-2.83"/></svg>`,
'playing-generating': `<svg ${common}><path d="M11 5 6 9H2v6h4l5 4z"/><path d="M15.54 8.46a5 5 0 0 1 0 7.07"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14"/><path d="M12 2v3"/><path d="M12 19v3"/></svg>`,
'playing-ready': `<svg ${common}><path d="M11 5 6 9H2v6h4l5 4z"/><path d="M15.54 8.46a5 5 0 0 1 0 7.07"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14"/></svg>`
};
return icons[state] || icons['waiting-generating'];
}
toCursorDataUrl(svg) {
return `data:image/svg+xml,${encodeURIComponent(svg.replace(/\s+/g, ' ').trim())}`;
}
/**
* Handle player input changes
* @param {Event} e - Input event
@@ -271,7 +388,7 @@ class UIInputHandlerModule extends BaseModule {
if (this.commandHistoryElement && this.commandHistoryElement.appendChild) {
const historyItem = document.createElement('div');
historyItem.className = 'history-item';
historyItem.textContent = `> ${command}`;
historyItem.innerHTML = `&gt; ${this.formatCommandHistory(command)}`;
this.commandHistoryElement.appendChild(historyItem);
// Limit visible history items
@@ -284,6 +401,23 @@ class UIInputHandlerModule extends BaseModule {
}
}
formatCommandHistory(command) {
const parser = this.getModule('markup-parser') || window.MarkupParser;
if (parser && typeof parser.markdownToHtml === 'function') {
return parser.markdownToHtml(command);
}
return String(command)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}
isPlaybackActive() {
const playbackCoordinator = this.getModule('playback-coordinator') || window.PlaybackCoordinator;
return Boolean(playbackCoordinator && playbackCoordinator.isPlaying);
}
/**
* Resets the cursor position to the start.
*/