Add ink integration UI and media playback

This commit is contained in:
2026-05-15 21:23:46 +02:00
parent 44dc64f830
commit f2e786d5bc
89 changed files with 6561 additions and 556 deletions
+77 -5
View File
@@ -18,6 +18,7 @@ class UIInputHandlerModule extends BaseModule {
this.historyIndex = -1;
this.commandHistory = [];
this.inputBuffer = '';
this.inputMode = 'text';
// Bind methods using the parent class bindMethods utility
this.bindMethods([
@@ -28,11 +29,14 @@ class UIInputHandlerModule extends BaseModule {
'handleKeyboardInput',
'submitCommand',
'addToHistory',
'bindHistoryToTurn',
'highlightHistoryTurn',
'formatCommandHistory',
'resetCursorPosition',
'focusInput',
'setProcessState',
'setInputAvailability',
'setMode',
'clearHistory'
]);
@@ -61,6 +65,15 @@ class UIInputHandlerModule extends BaseModule {
this.addEventListener(document, 'story:process-state', (event) => {
this.setProcessState(event.detail?.state || 'ready', event.detail || {});
});
this.addEventListener(document, 'story:input-mode', (event) => {
this.setMode(event.detail || 'text');
});
this.addEventListener(document, 'story:turn-start', (event) => {
this.bindHistoryToTurn(event.detail?.turnId);
});
this.addEventListener(document, 'story:visible-turn', (event) => {
this.highlightHistoryTurn(event.detail?.turnId);
});
this.reportProgress(100, 'UI Input Handler ready');
return true;
@@ -87,7 +100,7 @@ class UIInputHandlerModule extends BaseModule {
return;
}
if (event.key === ' ' && this.isPlaybackActive()) {
if (event.key === ' ' && (this.isPlaybackActive() || this.isSkippablePauseActive())) {
document.dispatchEvent(new CustomEvent('ui:command', {
detail: { type: 'continue', source: 'spacebar' }
}));
@@ -95,7 +108,7 @@ class UIInputHandlerModule extends BaseModule {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
if (document.body.dataset.gameRunning !== 'true') {
if (document.body.dataset.gameRunning !== 'true' || this.inputMode !== 'text') {
return;
}
this.submitCommand();
@@ -110,6 +123,9 @@ class UIInputHandlerModule extends BaseModule {
if (document.body.dataset.gameRunning !== 'true') {
return;
}
if (this.inputMode !== 'text') {
return;
}
event.preventDefault();
this.focusInput();
const start = this.playerInput.selectionStart ?? this.playerInput.value.length;
@@ -176,8 +192,6 @@ class UIInputHandlerModule extends BaseModule {
playerInput.id = 'player_input';
playerInput.rows = 1;
playerInput.placeholder = 'What will you do?';
playerInput.setAttribute('autocomplete', 'off');
playerInput.setAttribute('spellcheck', 'true');
// Fix horizontal scrolling by ensuring the textbox wraps text
playerInput.style.overflowX = 'hidden';
@@ -187,6 +201,7 @@ class UIInputHandlerModule extends BaseModule {
inputWrapper.appendChild(playerInput);
}
this.playerInput = playerInput;
this.applyTextInputAttributes(playerInput);
// Create the cursor if needed
let cursor = document.getElementById('cursor');
@@ -260,7 +275,7 @@ class UIInputHandlerModule extends BaseModule {
}
setInputAvailability(enabled) {
this.inputEnabled = Boolean(enabled);
this.inputEnabled = Boolean(enabled) && this.inputMode === 'text';
const commandInput = document.getElementById('command_input');
if (commandInput) {
commandInput.classList.toggle('fading', !this.inputEnabled);
@@ -276,6 +291,31 @@ class UIInputHandlerModule extends BaseModule {
}
}
applyTextInputAttributes(playerInput) {
if (!playerInput) return;
const attributes = {
autocomplete: 'off',
autocorrect: 'off',
autocapitalize: 'sentences',
spellcheck: 'true',
'aria-autocomplete': 'none',
'data-form-type': 'other',
'data-1p-ignore': 'true',
'data-lpignore': 'true',
'data-bwignore': 'true'
};
Object.entries(attributes).forEach(([name, value]) => {
playerInput.setAttribute(name, value);
});
}
setMode(mode) {
this.inputMode = ['text', 'choice', 'end'].includes(mode) ? mode : 'text';
this.setInputAvailability(this.inputMode === 'text');
}
applyMouseCursor(state) {
const root = document.documentElement;
if (!root) {
@@ -365,6 +405,7 @@ class UIInputHandlerModule extends BaseModule {
submitCommand() {
if (!this.playerInput || !this.playerInput.value.trim()) return;
if (document.body.dataset.gameRunning !== 'true' || !this.inputEnabled) return;
if (this.inputMode !== 'text') return;
const command = this.playerInput.value.trim();
console.log(`UIInputHandler: Submitting command: "${command}"`);
@@ -420,7 +461,15 @@ class UIInputHandlerModule extends BaseModule {
if (this.commandHistoryElement && this.commandHistoryElement.appendChild) {
const historyItem = document.createElement('div');
historyItem.className = 'history-item';
historyItem.dataset.turnId = 'pending';
historyItem.innerHTML = `> ${this.formatCommandHistory(command)}`;
historyItem.addEventListener('click', () => {
const turnId = historyItem.dataset.turnId;
if (!turnId || turnId === 'pending') return;
document.dispatchEvent(new CustomEvent('story:scroll-to-turn', {
detail: { turnId: Number(turnId) }
}));
});
this.commandHistoryElement.appendChild(historyItem);
// Limit visible history items
@@ -433,6 +482,25 @@ class UIInputHandlerModule extends BaseModule {
}
}
bindHistoryToTurn(turnId) {
if (!Number.isInteger(Number(turnId))) return;
if (!this.commandHistoryElement) {
this.commandHistoryElement = document.getElementById('command_history');
}
const pending = this.commandHistoryElement?.querySelector('.history-item[data-turn-id="pending"]');
if (!pending) return;
pending.dataset.turnId = String(turnId);
pending.classList.remove('history-pending');
}
highlightHistoryTurn(turnId) {
if (!this.commandHistoryElement || turnId == null) return;
const id = String(turnId);
this.commandHistoryElement.querySelectorAll('.history-item').forEach((item) => {
item.classList.toggle('active', item.dataset.turnId === id);
});
}
formatCommandHistory(command) {
const parser = this.getModule('markup-parser') || window.MarkupParser;
if (parser && typeof parser.markdownToHtml === 'function') {
@@ -450,6 +518,10 @@ class UIInputHandlerModule extends BaseModule {
return Boolean(playbackCoordinator && playbackCoordinator.isPlaying);
}
isSkippablePauseActive() {
return document.documentElement.dataset.skippablePause === 'true';
}
/**
* Resets the cursor position to the start.
*/