// === STORY READER GAME ===
// Prototype for reading long stories with sentence chunking and word-by-word translation
class StoryReader {
constructor(options) {
this.container = options.container;
this.content = options.content;
this.onScoreUpdate = options.onScoreUpdate || (() => {});
this.onGameEnd = options.onGameEnd || (() => {});
// Reading state
this.currentChapter = 0;
this.currentSentence = 0;
this.totalSentences = 0;
this.readingSessions = 0;
this.wordsRead = 0;
this.comprehensionScore = 0;
// Story data
this.story = null;
this.vocabulary = {};
// UI state
this.showTranslations = false;
this.showPronunciations = false;
this.readingMode = 'sentence'; // 'sentence' or 'paragraph'
this.fontSize = 'medium';
// Reading time tracking
this.startTime = Date.now();
this.totalReadingTime = 0;
this.readingTimer = null;
this.init();
}
init() {
logSh(`π Story Reader content received:`, this.content, 'DEBUG');
logSh(`π Story field exists: ${!!this.content.story}`, 'DEBUG');
logSh(`π RawContent exists: ${!!this.content.rawContent}`, 'DEBUG');
// VΓ©rifier d'abord le contenu brut (rawContent) puis le contenu adaptΓ©
const storyData = this.content.rawContent?.story || this.content.story;
if (!storyData) {
logSh('No story content found in content or rawContent', 'ERROR');
this.showError('This content does not contain a story for reading.');
return;
}
this.story = storyData;
this.vocabulary = this.content.rawContent?.vocabulary || this.content.vocabulary || {};
this.calculateTotalSentences();
logSh(`π Story Reader initialized: "${this.story.title}" (${this.totalSentences} sentences)`, 'INFO');
this.createInterface();
this.loadProgress();
this.renderCurrentSentence();
}
showError(message) {
this.container.innerHTML = `
Chapter 1: Loading...
Loading story...
Translation will appear here...
Words Read:
0
Reading Time:
00:00
Progress:
0%
`;
this.addStyles();
this.setupEventListeners();
}
addStyles() {
const style = document.createElement('style');
style.textContent = `
.story-reader-wrapper {
max-width: 800px;
margin: 0 auto;
padding: 20px;
font-family: 'Georgia', serif;
line-height: 1.6;
}
.story-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 2px solid #e2e8f0;
}
.story-title h2 {
margin: 0 0 10px 0;
color: #2d3748;
font-size: 1.8em;
}
.reading-progress {
display: flex;
align-items: center;
gap: 10px;
}
.progress-bar {
width: 200px;
height: 8px;
background: #e2e8f0;
border-radius: 4px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #3b82f6, #10b981);
width: 0%;
transition: width 0.3s ease;
}
.story-controls {
display: flex;
gap: 10px;
}
.settings-panel {
background: #f7fafc;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 15px;
margin-bottom: 20px;
}
.setting-group {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 10px;
}
.setting-group label {
font-weight: 600;
min-width: 100px;
}
.chapter-info {
background: #edf2f7;
padding: 10px 15px;
border-radius: 6px;
margin-bottom: 20px;
font-style: italic;
color: #4a5568;
}
.reading-area {
position: relative;
background: white;
border: 2px solid #e2e8f0;
border-radius: 12px;
padding: 30px;
margin-bottom: 20px;
min-height: 200px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
}
.sentence-display {
text-align: center;
}
.original-text {
font-size: 1.2em;
color: #2d3748;
margin-bottom: 15px;
cursor: pointer;
padding: 15px;
border-radius: 8px;
transition: background-color 0.2s;
}
.original-text:hover {
background-color: #f7fafc;
}
.original-text.small { font-size: 1em; }
.original-text.medium { font-size: 1.2em; }
.original-text.large { font-size: 1.4em; }
.original-text.extra-large { font-size: 1.6em; }
.translation-text {
font-style: italic;
color: #718096;
font-size: 1em;
padding: 10px;
background: #f0fff4;
border-radius: 6px;
border-left: 4px solid #10b981;
}
.clickable-word {
cursor: pointer;
padding: 2px 4px;
border-radius: 3px;
transition: background-color 0.2s;
position: relative;
display: inline-block;
}
.clickable-word:hover {
background-color: #fef5e7;
color: #d69e2e;
}
.word-with-pronunciation {
position: relative;
display: inline-block;
margin: 0 2px;
vertical-align: top;
line-height: 1.8;
}
.pronunciation-text {
position: absolute;
top: -16px;
left: 50%;
transform: translateX(-50%);
font-size: 0.7em;
color: #718096;
font-style: italic;
white-space: nowrap;
pointer-events: none;
z-index: 10;
display: none;
}
.pronunciation-text.show {
display: block;
}
.reading-area {
position: relative;
background: white;
border: 2px solid #e2e8f0;
border-radius: 12px;
padding: 40px 30px 30px 30px;
margin-bottom: 20px;
min-height: 200px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
line-height: 2.2;
}
.word-popup {
position: fixed;
background: white;
border: 2px solid #3b82f6;
border-radius: 6px;
padding: 8px 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
z-index: 9999;
max-width: 200px;
min-width: 120px;
font-size: 0.9em;
line-height: 1.3;
}
.word-original {
font-weight: bold;
color: #2d3748;
font-size: 1em;
margin-bottom: 3px;
}
.word-translation {
color: #10b981;
font-size: 0.9em;
margin-bottom: 2px;
}
.word-type {
font-size: 0.75em;
color: #718096;
font-style: italic;
}
.story-navigation {
display: flex;
justify-content: center;
gap: 15px;
margin-bottom: 20px;
}
.nav-btn {
padding: 12px 24px;
border: 2px solid #e2e8f0;
background: white;
border-radius: 8px;
cursor: pointer;
font-size: 1em;
transition: all 0.2s;
}
.nav-btn:hover:not(:disabled) {
background: #f7fafc;
border-color: #cbd5e0;
}
.nav-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.nav-btn.primary {
background: #3b82f6;
color: white;
border-color: #3b82f6;
}
.nav-btn.primary:hover {
background: #2563eb;
}
.reading-stats {
display: flex;
justify-content: space-around;
background: #f7fafc;
padding: 15px;
border-radius: 8px;
border: 1px solid #e2e8f0;
}
.stat {
text-align: center;
}
.stat-label {
display: block;
font-size: 0.9em;
color: #718096;
margin-bottom: 5px;
}
.stat-value {
display: block;
font-weight: bold;
font-size: 1.1em;
color: #2d3748;
}
@media (max-width: 768px) {
.story-reader-wrapper {
padding: 10px;
}
.story-header {
flex-direction: column;
gap: 15px;
}
.reading-stats {
flex-direction: column;
gap: 10px;
}
}
`;
document.head.appendChild(style);
}
setupEventListeners() {
// Navigation
document.getElementById('prev-btn').addEventListener('click', () => this.previousSentence());
document.getElementById('next-btn').addEventListener('click', () => this.nextSentence());
document.getElementById('bookmark-btn').addEventListener('click', () => this.saveBookmark());
// Controls
document.getElementById('settings-btn').addEventListener('click', () => this.toggleSettings());
document.getElementById('toggle-translation-btn').addEventListener('click', () => this.toggleTranslations());
document.getElementById('pronunciation-toggle-btn').addEventListener('click', () => this.togglePronunciations());
// Settings
document.getElementById('font-size-select').addEventListener('change', (e) => this.changeFontSize(e.target.value));
document.getElementById('reading-mode-select').addEventListener('change', (e) => this.changeReadingMode(e.target.value));
// Keyboard shortcuts
document.addEventListener('keydown', (e) => {
if (e.key === 'ArrowLeft') this.previousSentence();
if (e.key === 'ArrowRight') this.nextSentence();
if (e.key === 'Space') {
e.preventDefault();
this.nextSentence();
}
if (e.key === 't' || e.key === 'T') this.toggleTranslations();
});
// Click outside to close word popup
document.addEventListener('click', (e) => {
if (!e.target.closest('.word-popup') && !e.target.closest('.clickable-word')) {
this.hideWordPopup();
}
});
}
getCurrentSentenceData() {
let sentenceCount = 0;
for (let chapterIndex = 0; chapterIndex < this.story.chapters.length; chapterIndex++) {
const chapter = this.story.chapters[chapterIndex];
if (sentenceCount + chapter.sentences.length > this.currentSentence) {
const sentenceInChapter = this.currentSentence - sentenceCount;
return {
chapter: chapterIndex,
sentence: sentenceInChapter,
data: chapter.sentences[sentenceInChapter],
chapterTitle: chapter.title
};
}
sentenceCount += chapter.sentences.length;
}
return null;
}
// Match words from sentence with centralized vocabulary
matchWordsWithVocabulary(sentence) {
const words = sentence.split(/(\s+|[.,!?;:"'()[\]{}\-ββ])/);
const matchedWords = [];
words.forEach(token => {
// Skip whitespace and punctuation tokens
if (/^\s+$/.test(token) || /^[.,!?;:"'()[\]{}\-ββ]+$/.test(token)) {
matchedWords.push({
original: token,
hasVocab: false,
isWhitespace: true
});
return;
}
// Clean word (remove punctuation for matching)
const cleanWord = token.toLowerCase().replace(/[.,!?;:"'()[\]{}\-ββ]/g, '');
// Skip empty tokens
if (!cleanWord) return;
// Check if word exists in vocabulary (try exact match first, then stems)
let vocabEntry = this.content.vocabulary[cleanWord];
// Try common variations if exact match not found
if (!vocabEntry) {
// Try without 's' for plurals
if (cleanWord.endsWith('s')) {
vocabEntry = this.content.vocabulary[cleanWord.slice(0, -1)];
}
// Try without 'ed' for past tense
if (!vocabEntry && cleanWord.endsWith('ed')) {
vocabEntry = this.content.vocabulary[cleanWord.slice(0, -2)];
}
// Try without 'ing' for present participle
if (!vocabEntry && cleanWord.endsWith('ing')) {
vocabEntry = this.content.vocabulary[cleanWord.slice(0, -3)];
}
}
if (vocabEntry) {
// Word found in vocabulary
matchedWords.push({
original: token,
hasVocab: true,
word: cleanWord,
translation: vocabEntry.translation || vocabEntry.user_language,
pronunciation: vocabEntry.pronunciation,
type: vocabEntry.type || 'unknown'
});
} else {
// Word not in vocabulary - render as plain text
matchedWords.push({
original: token,
hasVocab: false
});
}
});
return matchedWords;
}
renderCurrentSentence() {
const sentenceData = this.getCurrentSentenceData();
if (!sentenceData) return;
const { data, chapterTitle } = sentenceData;
// Update chapter info
document.getElementById('chapter-info').innerHTML = `
π Story Complete!
You've finished reading "${this.story.title}"
Words read: ${this.wordsRead}
Total sentences: ${this.totalSentences}
`;
document.getElementById('reading-area').innerHTML = completionMessage;
}
start() {
logSh('π Story Reader: Starting', 'INFO');
this.startReadingTimer();
}
startReadingTimer() {
this.startTime = Date.now();
this.readingTimer = setInterval(() => {
this.updateReadingTime();
}, 1000);
}
updateReadingTime() {
const currentTime = Date.now();
this.totalReadingTime = Math.floor((currentTime - this.startTime) / 1000);
const minutes = Math.floor(this.totalReadingTime / 60);
const seconds = this.totalReadingTime % 60;
const timeString = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
document.getElementById('reading-time').textContent = timeString;
}
restart() {
this.currentSentence = 0;
this.wordsRead = 0;
// Restart reading timer
if (this.readingTimer) {
clearInterval(this.readingTimer);
}
this.startReadingTimer();
this.renderCurrentSentence();
this.saveProgress();
}
destroy() {
// Clean up timer
if (this.readingTimer) {
clearInterval(this.readingTimer);
}
this.container.innerHTML = '';
}
}
// Module registration
window.GameModules = window.GameModules || {};
window.GameModules.StoryReader = StoryReader;