Document markup and improve choice tags

This commit is contained in:
2026-05-17 15:52:41 +02:00
parent c2fb27b6b8
commit 2c54498ee2
52 changed files with 3485 additions and 377 deletions
+279 -2
View File
@@ -48,6 +48,10 @@ class UIDisplayHandlerModule extends BaseModule {
this.lastManualScrollAt = 0;
this.layoutFlowLine = 0;
this.layoutExclusions = [];
this.notificationQueue = [];
this.notificationActive = false;
this.pendingTerminalNotifications = [];
this.latestInputMode = 'text';
// Resources to preload
this.cssPath = '/css/style.css';
@@ -121,7 +125,19 @@ class UIDisplayHandlerModule extends BaseModule {
'measureText',
'loadCSS',
'showChoices',
'preloadImages'
'preloadImages',
'createCreditsDialog',
'openCreditsDialog',
'closeCreditsDialog',
'loadCreditsText',
'createNotificationDialog',
'handleStoryTag',
'getTagMessage',
'showNotification',
'displayNextNotification',
'queueTerminalNotification',
'flushTerminalNotifications',
'closeNotification'
]);
console.log('UIDisplayHandler: Constructor initialized');
@@ -173,6 +189,15 @@ class UIDisplayHandlerModule extends BaseModule {
this.addEventListener(document, 'story:history-updated', (event) => {
this.updateStoryScrollbar(event.detail || {});
});
this.addEventListener(document, 'story:tag', (event) => {
this.handleStoryTag(event.detail);
});
this.addEventListener(document, 'story:turn-start', () => {
this.latestInputMode = 'text';
});
this.addEventListener(document, 'story:input-mode', (event) => {
this.latestInputMode = event.detail || 'text';
});
this.addEventListener(document, 'wheel', this.handleHistoryWheel, { passive: false });
this.addEventListener(document, 'keydown', (event) => {
const tagName = String(event.target?.tagName || '').toLowerCase();
@@ -213,6 +238,9 @@ class UIDisplayHandlerModule extends BaseModule {
? this.t('title.continueHint')
: this.t('title.fastForwardHint');
}
if (state === 'ready' && this.latestInputMode === 'end') {
this.flushTerminalNotifications();
}
});
if (window.ResizeObserver && this.paragraphContainer) {
@@ -472,6 +500,9 @@ class UIDisplayHandlerModule extends BaseModule {
lighting.id = 'lighting';
document.body.appendChild(lighting);
}
this.createCreditsDialog();
this.createNotificationDialog();
console.log('UIDisplayHandler: All containers initialized');
this.applyGameConfig(this.gameConfig?.getConfig?.());
@@ -497,7 +528,27 @@ class UIDisplayHandlerModule extends BaseModule {
metadata.version ? this.t('title.version', { version: metadata.version }) : '',
metadata.copyright || ''
].filter(Boolean);
legalElement.textContent = items.join(' · ');
legalElement.innerHTML = '';
const legalText = document.createElement('span');
legalText.id = 'game_legal_text';
legalText.textContent = items.join(' | ');
legalElement.appendChild(legalText);
const creditsButton = document.createElement('button');
creditsButton.id = 'credits_button';
creditsButton.type = 'button';
creditsButton.textContent = this.t('credits.button');
creditsButton.title = this.t('credits.buttonTitle');
creditsButton.addEventListener('click', (event) => {
event.preventDefault();
event.stopPropagation();
this.openCreditsDialog();
});
if (items.length > 0) {
legalElement.appendChild(document.createTextNode(' | '));
}
legalElement.appendChild(creditsButton);
}
}
@@ -522,6 +573,9 @@ class UIDisplayHandlerModule extends BaseModule {
setText('options', 'topbar.options');
setText('remark_text', 'title.fastForwardHint');
setText('start_prompt', 'title.startPrompt');
setText('credits_dialog_title', 'credits.title');
setText('credits_close', 'credits.close');
setText('story_popup_ok', 'popup.ok');
setTitle('speech', 'topbar.speechTitle');
setTitle('autoplay', 'topbar.autoplayTitle');
setTitle('rewind', 'topbar.newGameTitle');
@@ -533,6 +587,224 @@ class UIDisplayHandlerModule extends BaseModule {
if (input) input.setAttribute('placeholder', this.t('input.placeholder'));
this.applyGameConfig(this.gameConfig?.getConfig?.());
}
createCreditsDialog() {
if (document.getElementById('credits_modal')) {
return;
}
const modal = document.createElement('div');
modal.id = 'credits_modal';
modal.className = 'credits-modal';
modal.setAttribute('aria-hidden', 'true');
modal.innerHTML = `
<div class="credits-dialog" role="dialog" aria-modal="true" aria-labelledby="credits_dialog_title">
<div class="credits-dialog-header">
<h2 id="credits_dialog_title"></h2>
<button type="button" id="credits_close"></button>
</div>
<div class="credits-logo-row" aria-label="Credits links">
<a href="https://openai.com/" target="_blank" rel="noreferrer"><img src="https://cdn.simpleicons.org/openai/2b2218" alt="OpenAI"></a>
<a href="https://www.inklestudios.com/ink/" target="_blank" rel="noreferrer" class="credits-wordmark">ink</a>
<a href="https://mnater.github.io/Hyphenopoly/" target="_blank" rel="noreferrer" class="credits-wordmark">Hyphenopoly</a>
<a href="https://github.com/hexgrad/kokoro" target="_blank" rel="noreferrer" class="credits-wordmark">Kokoro</a>
<a href="https://suno.com/" target="_blank" rel="noreferrer" class="credits-wordmark">Suno</a>
</div>
<pre id="credits_content" class="credits-content"></pre>
</div>
`;
document.body.appendChild(modal);
modal.addEventListener('click', (event) => {
if (event.target === modal) {
this.closeCreditsDialog();
}
});
const closeButton = document.getElementById('credits_close');
if (closeButton) {
closeButton.addEventListener('click', () => this.closeCreditsDialog());
}
}
async openCreditsDialog() {
const modal = document.getElementById('credits_modal');
const content = document.getElementById('credits_content');
if (!modal || !content) {
return;
}
modal.classList.add('visible');
modal.setAttribute('aria-hidden', 'false');
if (!content.dataset.loaded) {
content.textContent = this.t('credits.loading');
content.textContent = await this.loadCreditsText();
content.dataset.loaded = 'true';
}
}
closeCreditsDialog() {
const modal = document.getElementById('credits_modal');
if (!modal) {
return;
}
modal.classList.remove('visible');
modal.setAttribute('aria-hidden', 'true');
}
async loadCreditsText() {
try {
const response = await fetch('/THIRD_PARTY_NOTICES.md', { cache: 'no-cache' });
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.text();
} catch (error) {
console.warn('UIDisplayHandler: Failed to load credits notices', error);
return this.t('credits.loadFailed');
}
}
createNotificationDialog() {
if (document.getElementById('story_popup_modal')) {
return;
}
const modal = document.createElement('div');
modal.id = 'story_popup_modal';
modal.className = 'story-popup-modal';
modal.setAttribute('aria-hidden', 'true');
modal.innerHTML = `
<div class="story-popup-dialog" role="dialog" aria-modal="true" aria-labelledby="story_popup_title">
<h2 id="story_popup_title"></h2>
<div id="story_popup_message"></div>
<button type="button" id="story_popup_ok"></button>
</div>
`;
document.body.appendChild(modal);
modal.addEventListener('click', (event) => {
event.preventDefault();
event.stopPropagation();
if (event.target === modal) {
this.closeNotification();
}
});
modal.addEventListener('pointerdown', (event) => {
event.stopPropagation();
});
const okButton = document.getElementById('story_popup_ok');
if (okButton) {
okButton.addEventListener('click', (event) => {
event.preventDefault();
event.stopPropagation();
this.closeNotification();
});
}
}
handleStoryTag(tag) {
const key = String(tag?.key || '').toLowerCase();
if (!['score', 'error', 'achievement', 'alert'].includes(key)) {
return;
}
const message = this.getTagMessage(tag);
if (key === 'score') {
this.queueTerminalNotification(
'ending',
this.t('popup.endingTitle'),
message || this.t('popup.defaultEnding')
);
} else if (key === 'error') {
this.queueTerminalNotification(
'error',
this.t('popup.errorTitle'),
message || this.t('popup.defaultError')
);
} else if (key === 'achievement') {
this.showNotification(
'achievement',
this.t('popup.achievementTitle'),
message || this.t('popup.defaultAchievement')
);
} else if (key === 'alert') {
this.showNotification(
'alert',
this.t('popup.alertTitle'),
message || this.t('popup.defaultAlert')
);
}
}
getTagMessage(tag) {
return [tag?.value, tag?.param]
.map((part) => String(part || '').trim())
.filter(Boolean)
.join('\n');
}
showNotification(kind, title, message) {
this.notificationQueue.push({ kind, title, message });
this.displayNextNotification();
}
queueTerminalNotification(kind, title, message) {
this.pendingTerminalNotifications.push({ kind, title, message });
if (this.latestInputMode === 'end') {
this.flushTerminalNotifications();
}
}
flushTerminalNotifications() {
if (this.pendingTerminalNotifications.length === 0) {
return;
}
this.pendingTerminalNotifications.splice(0).forEach((notification) => {
this.showNotification(notification.kind, notification.title, notification.message);
});
}
displayNextNotification() {
if (this.notificationActive || this.notificationQueue.length === 0) {
return;
}
const next = this.notificationQueue.shift();
const modal = document.getElementById('story_popup_modal');
const title = document.getElementById('story_popup_title');
const message = document.getElementById('story_popup_message');
const okButton = document.getElementById('story_popup_ok');
if (!modal || !title || !message) {
return;
}
modal.dataset.kind = next.kind;
title.textContent = next.title;
message.textContent = next.message;
if (okButton) {
okButton.textContent = this.t('popup.ok');
setTimeout(() => okButton.focus(), 0);
}
this.notificationActive = true;
modal.classList.add('visible');
modal.setAttribute('aria-hidden', 'false');
}
closeNotification() {
const modal = document.getElementById('story_popup_modal');
if (!modal) {
this.notificationActive = false;
return;
}
modal.classList.remove('visible');
modal.setAttribute('aria-hidden', 'true');
this.notificationActive = false;
setTimeout(() => this.displayNextNotification(), 0);
}
/**
* Measure text width using canvas
@@ -1927,6 +2199,11 @@ class UIDisplayHandlerModule extends BaseModule {
this.container.appendChild(this.paragraphContainer);
}
this.renderedItems = [];
this.notificationQueue = [];
this.pendingTerminalNotifications = [];
this.notificationActive = false;
document.getElementById('story_popup_modal')?.classList.remove('visible');
document.getElementById('story_popup_modal')?.setAttribute('aria-hidden', 'true');
this.historyWindowStartId = 1;
this.historyWindowEndId = 0;
this.storyTopLine = 0;