Refactored app to include all the ink.js typography.
This commit is contained in:
@@ -0,0 +1,441 @@
|
||||
# AI Interactive Fiction Engine - Technical Documentation
|
||||
|
||||
## 1. Overview
|
||||
|
||||
This document provides a comprehensive technical explanation of the AI Interactive Fiction Engine implemented in `game.js`. The engine leverages inkjs for narrative management and incorporates advanced text rendering features, including custom animations, sophisticated typography using SmartyPants and Hyphenopoly, and optimal line breaking via the Knuth and Plass algorithm, to create rich, dynamic interactive fiction experiences.
|
||||
|
||||
## 2. Core Technologies
|
||||
|
||||
The engine is built upon several key technologies:
|
||||
|
||||
* **inkjs**: Manages the core interactive fiction logic, including story state, branching narrative, choices, and variables.
|
||||
* **Knuth and Plass Line Breaking Algorithm**: Adapted implementation for optimal paragraph justification and line breaking, ensuring high-quality text layout. (Based on Bram Stein's "typeset" library).
|
||||
* **Hyphenopoly**: Provides high-quality, language-aware text hyphenation, crucial for justified text layout.
|
||||
* **SmartyPants**: Enhances typography by converting plain text elements (quotes, dashes) into their typographically correct equivalents.
|
||||
* **Custom Animation System**: A queue-based system handles the precise timing and visual reveal of text elements.
|
||||
* **JavaScript (ES6+)**: The primary implementation language.
|
||||
* **HTML/CSS**: Used for structuring the UI and styling the visual presentation.
|
||||
|
||||
## 3. Engine Architecture and Flow
|
||||
|
||||
The engine follows a well-defined process from initialization to interactive playback.
|
||||
|
||||
### 3.1. Initialization (`window.onload`)
|
||||
|
||||
Execution begins when the window loads:
|
||||
|
||||
1. **Initial State Setup**: Global state variables (`running`, `fastForwardingAll`, `speech`, `speed`, `delay`) are initialized.
|
||||
2. **Background Effects**: Lighting animations for background visuals are configured.
|
||||
3. **Text Libraries**: Hyphenopoly is initialized for hyphenation support.
|
||||
4. **Story Loading**: The compiled Ink story (JSON format) is fetched and loaded.
|
||||
5. **Event Listeners**: Listeners for user interactions (keyboard input, clicks, slider changes) are attached.
|
||||
6. **Playback Start**: The `continueStory()` function is called to begin rendering the narrative.
|
||||
|
||||
### 3.2. The Main Playback Loop (`continueStory()`)
|
||||
|
||||
The `continueStory()` function is the engine's core, orchestrating the display of narrative content and handling user choices.
|
||||
|
||||
**Step-by-step Process:**
|
||||
|
||||
1. **Setup**: Animation state variables (`fade_in`, `running`, `fastForwardingAll`) are reset for the current turn.
|
||||
2. **Story Content Iteration**:
|
||||
```javascript
|
||||
while(story.canContinue) {
|
||||
// Fetch next paragraph & associated tags from inkjs
|
||||
var paragraphText = story.Continue();
|
||||
var tags = story.currentTags;
|
||||
|
||||
// Process tags (Section 3.3)
|
||||
// ...
|
||||
|
||||
// Process and render text (Section 4)
|
||||
// ...
|
||||
}
|
||||
```
|
||||
The loop continues as long as inkjs indicates more content is available for the current narrative sequence.
|
||||
|
||||
3. **Choice Handling**: After exhausting the `story.canContinue` content, the engine checks for available choices:
|
||||
```javascript
|
||||
story.currentChoices.forEach(function(choice) {
|
||||
// Create interactive DOM elements for each choice
|
||||
// Attach event listeners to handle choice selection
|
||||
// Apply appropriate styling
|
||||
});
|
||||
```
|
||||
When a user selects a choice, an event handler calls `story.ChooseChoiceIndex(choice.index)` and then triggers `continueStory()` again to display the subsequent narrative branch.
|
||||
|
||||
4. **End of Story**: If `story.canContinue` is false and there are no choices, the story concludes or reaches a waiting point.
|
||||
|
||||
### 3.3. Tag Processing
|
||||
|
||||
Ink tags are used to control various non-textual aspects of the presentation:
|
||||
|
||||
* `AUDIO <url>`: Triggers playback of an audio file.
|
||||
* `IMAGE <url>`: Displays an image.
|
||||
* `BACKGROUND <url/color>`: Changes the background style.
|
||||
* `CHAPTER`: Applies special formatting for chapter headings (often including drop caps).
|
||||
* `SEPARATOR`: Inserts a decorative visual separator element.
|
||||
* Custom CSS Classes: Tags can directly add CSS classes to paragraph elements for specific styling.
|
||||
|
||||
Tags are processed within the `continueStory` loop before the associated paragraph text is rendered.
|
||||
|
||||
## 4. Advanced Text Rendering Pipeline
|
||||
|
||||
A key feature of this engine is its sophisticated text rendering pipeline, designed to produce professional-quality typography and layout.
|
||||
|
||||
### 4.1. Overview
|
||||
|
||||
Text rendering involves several stages for each paragraph:
|
||||
|
||||
1. **Preprocessing**: Apply SmartyPants for typographic correctness and Hyphenopoly for hyphenation points.
|
||||
2. **Line Breaking Calculation**: Use the Knuth and Plass algorithm (`kap` function) to determine optimal line breaks for the entire paragraph.
|
||||
3. **Typesetting and DOM Creation**: Use the `typesetParagraph` function to generate and position DOM elements based on the line break data.
|
||||
4. **Animation Scheduling**: Schedule the appearance of each text fragment using the custom animation system.
|
||||
|
||||
### 4.2. Knuth and Plass Line Breaking
|
||||
|
||||
The engine employs an adaptation of the Knuth and Plass line breaking algorithm to achieve superior text justification compared to standard browser methods.
|
||||
|
||||
#### 4.2.1. Origin and Adaptation
|
||||
|
||||
* The core logic is based on [Bram Stein's "typeset" library](https://github.com/bramstein/typeset).
|
||||
* **Key Adaptations**:
|
||||
* **HTML Tag Support**: Introduced `linebreak.tag()` nodes to preserve inline HTML formatting (e.g., `<b>`, `<i>`, custom spans) during line breaking.
|
||||
* **Punctuation Handling**: Implemented custom logic for refined spacing around punctuation marks (`.,:;!?`), splitting width into symbolic (25%) and spacing (75%) components.
|
||||
* **Hyphenation Integration**: Tightly integrated with Hyphenopoly, treating hyphenation points as potential breaks with a specific, reduced penalty (25% of normal hyphen width).
|
||||
|
||||
#### 4.2.2. Algorithm Principles
|
||||
|
||||
* **Optimization Problem**: Treats line breaking as minimizing the overall "badness" (demerits) of a paragraph's layout.
|
||||
* **Global View**: Considers the entire paragraph at once, unlike greedy algorithms.
|
||||
* **Box, Glue, Penalty Model**: Represents text as:
|
||||
* **Boxes**: Unbreakable units (words, tagged elements).
|
||||
* **Glue**: Flexible spaces that can stretch or shrink.
|
||||
* **Penalties**: Opportunities for line breaks (e.g., after spaces, hyphens), with associated costs (demerits).
|
||||
* **Demerits System**: Assigns penalties based on line tightness, hyphenation, and relationships between consecutive lines (fitness classes).
|
||||
* **Optimal Path**: Finds the sequence of line breaks with the lowest total demerit score.
|
||||
|
||||
#### 4.2.3. Implementation (`knuth-and-plass.js`, `linebreak.js`)
|
||||
|
||||
* **`knuth-and-plass.js` (Adapter Layer)**:
|
||||
* Provides the `kap(text, measureText, measure, hyphenation)` function.
|
||||
* Parses the input text (already processed by SmartyPants/Hyphenopoly) into Box, Glue, Penalty, and Tag nodes.
|
||||
* Handles special spacing rules for punctuation.
|
||||
* Interfaces with the core `linebreak.js` module.
|
||||
* **`linebreak.js` (Core Algorithm)**:
|
||||
* Contains the `linebreak(nodes, lines, settings)` function.
|
||||
* Implements the main algorithm logic: ratio calculation, demerit calculation, fitness class tracking, active node management (using the LinkedList), and path finding.
|
||||
* Defines node types: `linebreak.box()`, `linebreak.glue()`, `linebreak.penalty()`, `linebreak.tag()`.
|
||||
* **Customization Parameters**:
|
||||
* `tolerance`: Controls acceptable line stretch/shrink (default: 2).
|
||||
* `demerits`: Configurable penalties for lines, flagged breaks (e.g., consecutive hyphens), and fitness class mismatches.
|
||||
|
||||
#### 4.2.4. Supporting Data Structure: LinkedList (`linked-list.js`)
|
||||
|
||||
* A custom, modern ES6 implementation of a doubly linked list, derived from the `typeset` library.
|
||||
* **Purpose**: Efficiently manages the "active nodes" (potential breakpoints being considered) within the `linebreak.js` algorithm. Critical for performance.
|
||||
* **Features**: ES6 class syntax, getter methods (`size`, `first`), method chaining.
|
||||
* **Known Bug**: The `get last()` method incorrectly references `this.last` instead of `this.tail`, potentially causing infinite recursion if called. **This should be corrected to `return this.tail;`**.
|
||||
|
||||
#### 4.2.5. Advantages
|
||||
|
||||
* Superior justification and word spacing.
|
||||
* Reduced and more aesthetically placed hyphenation.
|
||||
* Avoids jarringly tight or loose lines.
|
||||
* Produces professional, book-like typography.
|
||||
* Preserves rich text formatting during layout optimization.
|
||||
|
||||
### 4.3. Typography Enhancements (SmartyPants)
|
||||
|
||||
* Before line breaking, text is processed by `SmartyPants.smartypantsu(text, 1)`.
|
||||
* This converts:
|
||||
* Straight quotes (`"`, `'`) to curly quotes (“ ”, ‘ ’).
|
||||
* Double hyphens (`--`) to em-dashes (—).
|
||||
* Triple hyphens (`---`) to em-dashes (—) (configurable).
|
||||
* Three periods (`...`) to ellipses (…).
|
||||
|
||||
### 4.4. Hyphenation (Hyphenopoly)
|
||||
|
||||
* Applied after SmartyPants using `hyphenator_en(text, '.hyphenatePipe')`.
|
||||
* Inserts soft hyphen characters (`­` or similar markers) at valid break points within words, based on linguistic rules.
|
||||
* These potential hyphenation points are then treated as `Penalty` nodes by the line-breaking algorithm, allowing for more flexible justification.
|
||||
|
||||
### 4.5. Typesetting and DOM Rendering (`typesetParagraph()`)
|
||||
|
||||
This function translates the calculated layout data from `kap` into visible, animated DOM elements.
|
||||
|
||||
```javascript
|
||||
function typesetParagraph(paragraph_data, delay = 0, measure = []) {
|
||||
// ... setup paragraph element <p> ...
|
||||
|
||||
// Iterate through lines defined by paragraph_data.breaks
|
||||
for(let i = 1; i < paragraph_data.breaks.length; i++) {
|
||||
// Iterate through nodes (words, spaces, hyphens, tags) within the line
|
||||
for(let j = paragraph_data.breaks[i-1].position; j <= paragraph_data.breaks[i].position; j++) {
|
||||
const node = paragraph_data.nodes[j];
|
||||
// Create appropriate DOM element (e.g., <span> for words/tags)
|
||||
// Set absolute position (left, top) based on calculated widths and line height
|
||||
// Apply styles (opacity: 0 initially)
|
||||
// Schedule fade-in animation using scheduleTimeout() (Section 5)
|
||||
// Update the running 'delay' total for the next element
|
||||
}
|
||||
// Handle line breaks, adjusting vertical position
|
||||
}
|
||||
|
||||
return [p, delay]; // Return the populated <p> element and the final delay value
|
||||
}
|
||||
```
|
||||
|
||||
* Each word, space, or preserved tag becomes a separate, absolutely positioned DOM element (typically a `<span>`).
|
||||
* Positions are determined precisely based on the widths calculated during the line-breaking phase.
|
||||
* Initial opacity is set to 0, and fade-in animations are scheduled sequentially.
|
||||
|
||||
## 5. Animation System
|
||||
|
||||
The engine features a sophisticated animation system for revealing text dynamically.
|
||||
|
||||
### 5.1. Animation Queue (`timeoutQueue`)
|
||||
|
||||
* A central array `timeoutQueue = []` tracks all pending `setTimeout` calls.
|
||||
* Each entry is an object storing the function to execute, its arguments, and the `timeoutId`.
|
||||
|
||||
### 5.2. Scheduling Animations (`scheduleTimeout`)
|
||||
|
||||
```javascript
|
||||
function scheduleTimeout(func, delay, ...args) {
|
||||
// Creates timeoutObject with execute method and null timeoutId
|
||||
// Uses setTimeout to schedule execution after 'delay'
|
||||
// Inside setTimeout callback: executes func, removes object from timeoutQueue
|
||||
// Pushes timeoutObject onto timeoutQueue
|
||||
// Returns the timeoutId
|
||||
}
|
||||
```
|
||||
* Provides a managed way to schedule functions, ensuring they are tracked.
|
||||
* Enables batch operations like fast-forwarding or cancellation.
|
||||
|
||||
### 5.3. Timing and Delay (`window.delay`, `window.speed`)
|
||||
|
||||
* `window.delay`: Accumulates the total delay time as elements are scheduled. Each new element's animation starts after the previous one finishes.
|
||||
* `window.speed`: Controls the duration of each word's fade-in, typically adjusted by a UI slider. The delay increment for a word is often calculated like `delay += window.speed * word.length;`.
|
||||
* Speed Adjustment: The UI slider uses a non-linear function (`Math.pow(100.0 - value, 3) / 10000 * 10 + 0.01`) for finer control at slower speeds.
|
||||
|
||||
### 5.4. Fast Forwarding (`fastForward`, `fastForwardingAll`)
|
||||
|
||||
* The `fastForward()` function:
|
||||
1. Iterates through `timeoutQueue`.
|
||||
2. Calls `clearTimeout()` for each pending animation.
|
||||
3. Immediately executes the scheduled function (`timeoutObject.execute()`).
|
||||
4. Clears the `timeoutQueue`.
|
||||
5. Resets `window.delay` to 0.
|
||||
* Triggered by user actions (spacebar, click) or programmatically.
|
||||
* `window.fastForwardingAll` flag indicates if fast-forward is continuously active (e.g., holding space).
|
||||
* Visual Feedback: The page border changes color (e.g., red) when `fastForwardingAll` is true.
|
||||
|
||||
### 5.5. Animation Interruption (`stopAllTimeouts`)
|
||||
|
||||
* The `stopAllTimeouts()` function:
|
||||
1. Iterates through `timeoutQueue`.
|
||||
2. Calls `clearTimeout()` for each pending animation.
|
||||
3. Clears the `timeoutQueue`.
|
||||
* Used during navigation, loading saves, or story resets to prevent outdated animations from playing.
|
||||
|
||||
## 6. Additional Features
|
||||
|
||||
### 6.1. Text-to-Speech (TTS)
|
||||
|
||||
* Integrates with an external TTS service (e.g., ElevenLabs API).
|
||||
* A UI button toggles speech playback.
|
||||
* Attempts to synchronize audio playback with text animation reveal (implementation details not fully specified in the source document).
|
||||
* Playback can be interrupted by user interactions or fast-forwarding.
|
||||
|
||||
### 6.2. State Management and Persistence
|
||||
|
||||
* **Ink State**: The core narrative state (current position, variable values, etc.) is managed by inkjs and can be serialized using `story.state.toJson()`.
|
||||
* **Local Storage**: Used to persist the serialized Ink state and potentially other UI/game states (like rendered history) across browser sessions.
|
||||
* **Save/Load**: Functionality allows users to explicitly save the current state and reload it later. This involves restoring the Ink state via `story.state.loadJson()` and potentially re-rendering the story history up to that point.
|
||||
|
||||
## 7. External Libraries & Credits
|
||||
|
||||
The engine relies on the following external libraries:
|
||||
|
||||
* **inkjs**
|
||||
* **Author**: Yannick Lohse (y-lohse)
|
||||
* **Source**: [https://github.com/y-lohse/inkjs](https://github.com/y-lohse/inkjs)
|
||||
* **Description**: JavaScript port of Inkle's Ink narrative scripting language.
|
||||
* **Hyphenopoly**
|
||||
* **Author**: Hermann Monnich (mnater)
|
||||
* **Source**: [https://github.com/mnater/Hyphenopoly](https://github.com/mnater/Hyphenopoly)
|
||||
* **Website**: [https://mnater.github.io/Hyphenopoly/](https://mnater.github.io/Hyphenopoly/)
|
||||
* **Description**: High-quality JavaScript hyphenation library.
|
||||
* **SmartyPants**
|
||||
* **Original Author**: John Gruber
|
||||
* **JavaScript Port Author**: Example: Othree (othree)
|
||||
* **Source (Example Port)**: [https://github.com/othree/smartypants.js](https://github.com/othree/smartypants.js)
|
||||
* **Description**: Typography prettifier for quotes, dashes, etc.
|
||||
* **typeset (Basis for Knuth & Plass / LinkedList)**
|
||||
* **Author**: Bram Stein (bramstein)
|
||||
* **Source**: [https://github.com/bramstein/typeset](https://github.com/bramstein/typeset)
|
||||
* **Description**: Original JavaScript implementation of the Knuth-Plass line breaking algorithm, adapted for this engine.
|
||||
|
||||
## 8. Practical Refactoring Recommendations (Updated)
|
||||
|
||||
Based on the detailed analysis of the engine's functionality and the goal of optimizing for **modularity, separation of concerns, and reusability**, the following refined refactoring strategy is recommended. This approach breaks the system into highly focused, potentially reusable components, moving away from reliance on global state and tightly coupled logic.
|
||||
|
||||
### Core Component Modules:
|
||||
|
||||
1. **`AnimationQueue` (`animation-queue.js`)**
|
||||
* **Responsibility**: Solely manage the timing and execution queue for all scheduled animations (primarily text reveal).
|
||||
* **Core Functions**: `schedule(func, delay, ...args)`, `fastForward()`, `stop()`, `setSpeed(value)`.
|
||||
* **State**: Manages the internal `queue` array, the current `delay` accumulator, and the animation `speed`.
|
||||
* **Reusability**: Highly reusable for any system requiring sequenced, timed execution with speed control and interruption.
|
||||
|
||||
2. **`TextProcessor` (`text-processor.js`)**
|
||||
* **Responsibility**: Encapsulate text pre-processing steps required before layout calculation.
|
||||
* **Core Functions**: `process(text)` method that applies typographic enhancements (SmartyPants) and hyphenation (Hyphenopoly).
|
||||
* **Dependencies**: Takes instances or functions of the SmartyPants and Hyphenopoly libraries during construction.
|
||||
* **Reusability**: Reusable wherever this specific text processing pipeline (SmartyPants + Hyphenopoly) is needed.
|
||||
|
||||
3. **`ParagraphLayout` (`paragraph-layout.js`)**
|
||||
* **Responsibility**: Interface with the Knuth-Plass line breaking algorithm (`kap` function) to calculate optimal line breaks.
|
||||
* **Core Functions**: `calculateLayout(processedText, measures)` method.
|
||||
* **Dependencies**: Takes the `kap` function and a `measureText` function (capable of measuring text widths in the target rendering context) during construction.
|
||||
* **Reusability**: Reusable in any system needing high-quality paragraph line breaking, provided the `kap` algorithm and a text measurement function are supplied.
|
||||
|
||||
4. **`LayoutRenderer` (`layout-renderer.js`)**
|
||||
* **Responsibility**: Translate the abstract layout data (from `ParagraphLayout`) into concrete visual elements (DOM nodes in this case). Handle visual tag rendering.
|
||||
* **Core Functions**: `renderParagraph(layoutData, measures)` creates positioned DOM elements (e.g., `<span>` for words/tags), applies styles (initial opacity 0), and *uses* `AnimationQueue.schedule` to initiate fade-in animations. `renderVisualTag(tagType, tagData)` handles rendering for `IMAGE`, `BACKGROUND`, `CHAPTER`, `SEPARATOR`, and applying CSS classes.
|
||||
* **Dependencies**: Takes an `AnimationQueue` instance and potentially configuration for visual elements.
|
||||
* **Reusability**: Moderately reusable. The core logic of translating layout data is reusable, but the specific DOM creation part is tied to HTML/DOM rendering. Could be adapted for other rendering targets (e.g., Canvas).
|
||||
|
||||
5. **`AudioManager` (`audio-manager.js`)**
|
||||
* **Responsibility**: Manage loading and playback of non-TTS audio effects triggered by tags (e.g., `AUDIO <url>`).
|
||||
* **Core Functions**: `loadSound(id, url)`, `playSound(id)`, `stopSound(id)`, `stopAllSounds()`.
|
||||
* **Reusability**: Highly reusable component for basic audio management in web applications.
|
||||
|
||||
6. **`TtsPlayer` (`tts-player.js`)**
|
||||
* **Responsibility**: Handle interactions with the Text-to-Speech service (e.g., ElevenLabs API), manage playback, and potentially synchronize with the `AnimationQueue`.
|
||||
* **Core Functions**: `speak(text)`, `stopSpeaking()`, `setVoice(voiceId)`, `configure(apiKey, ...)`.
|
||||
* **Dependencies**: May need the `AnimationQueue` for synchronization if required.
|
||||
* **Reusability**: Reusable for adding TTS functionality, specific implementation depends on the chosen TTS provider API.
|
||||
|
||||
7. **`PersistenceManager` (`persistence-manager.js`)**
|
||||
* **Responsibility**: Handle saving and loading the game state.
|
||||
* **Core Functions**: `saveState(stateObject)`, `loadState()`. `stateObject` would contain Ink state JSON, potentially UI state, scroll position, etc.
|
||||
* **Dependencies**: Configurable storage backend (e.g., `localStorage` wrapper).
|
||||
* **Reusability**: Highly reusable for saving/loading application state to various storage mechanisms.
|
||||
|
||||
8. **`InkStoryPlayer` (`ink-story-player.js`)**
|
||||
* **Responsibility**: Orchestrate the narrative flow specific to the Ink story. Manage the `inkjs.Story` instance.
|
||||
* **Core Functions**: `loadStory(storyContentJson)`, `continueStory(containerElement)`, `chooseChoice(index)`. It drives the `story.Continue()` loop, gets text and tags. It *delegates* tasks:
|
||||
* Text processing -> `TextProcessor`
|
||||
* Layout calculation -> `ParagraphLayout`
|
||||
* Rendering paragraphs/visuals -> `LayoutRenderer`
|
||||
* Handling `AUDIO` tags -> `AudioManager`
|
||||
* Handling speech requests (if implemented via tags) -> `TtsPlayer`
|
||||
* Saving/Loading -> `PersistenceManager`
|
||||
* **Dependencies**: `inkjs.Story` class, `TextProcessor`, `ParagraphLayout`, `LayoutRenderer`, `AudioManager`, `TtsPlayer`, `PersistenceManager`.
|
||||
* **Reusability**: Specific to using Ink narratives but uses reusable components for its operations.
|
||||
|
||||
9. **`UiController` (`ui-controller.js`)**
|
||||
* **Responsibility**: Manage user interface interactions (event listeners) and update UI elements (sliders, buttons, status indicators).
|
||||
* **Core Functions**: `setupEventListeners()`, methods to handle specific events (e.g., `handleSpeedChange`, `handleFastForwardToggle`, `handleChoiceClick`, `handleSaveClick`, `handleLoadClick`, `handleTtsToggle`).
|
||||
* **Dependencies**: Interacts primarily with `InkStoryPlayer` (e.g., to choose choice, trigger save/load), `AnimationQueue` (to set speed, trigger fast-forward), `TtsPlayer` (to toggle speech), `AudioManager` (potentially to control master volume).
|
||||
* **Reusability**: Specific to the application's UI structure but follows a standard controller pattern.
|
||||
|
||||
### Main Application Integration (`animated-fiction.js` or `main.js`):
|
||||
|
||||
```javascript
|
||||
// main.js (Example Setup)
|
||||
import { AnimationQueue } from './animation-queue.js';
|
||||
import { TextProcessor } from './text-processor.js';
|
||||
// ... import other modules ...
|
||||
import { Story as InkStory } from './ink.js'; // Assuming inkjs is available
|
||||
|
||||
class AnimatedFictionApp {
|
||||
constructor(config) {
|
||||
this.config = config; // Story content URL, container element, API keys, etc.
|
||||
this.storyContent = null; // Loaded later
|
||||
|
||||
// 1. Instantiate Core Components
|
||||
this.animationQueue = new AnimationQueue();
|
||||
// Provide external libraries/functions to components that need them
|
||||
this.textProcessor = new TextProcessor(SmartyPants, hyphenator_en); // Assuming these are globally available or imported
|
||||
this.paragraphLayout = new ParagraphLayout(kap, this.measureDomText.bind(this)); // `kap` algorithm, bind measure func
|
||||
this.layoutRenderer = new LayoutRenderer(this.animationQueue);
|
||||
this.audioManager = new AudioManager();
|
||||
this.ttsPlayer = new TtsPlayer({ apiKey: config.ttsApiKey /*, optional animationQueue */ });
|
||||
this.persistenceManager = new PersistenceManager({ storage: localStorage }); // Configure storage backend
|
||||
|
||||
// 2. Instantiate Orchestrator & UI
|
||||
this.storyPlayer = new InkStoryPlayer({
|
||||
InkStory: InkStory, // Pass the inkjs Story constructor
|
||||
textProcessor: this.textProcessor,
|
||||
paragraphLayout: this.paragraphLayout,
|
||||
layoutRenderer: this.layoutRenderer,
|
||||
audioManager: this.audioManager,
|
||||
ttsPlayer: this.ttsPlayer,
|
||||
persistenceManager: this.persistenceManager,
|
||||
});
|
||||
this.uiController = new UiController({
|
||||
storyPlayer: this.storyPlayer,
|
||||
animationQueue: this.animationQueue,
|
||||
ttsPlayer: this.ttsPlayer,
|
||||
// Pass references to DOM elements (buttons, sliders)
|
||||
speedSliderElement: document.getElementById('speedSlider'),
|
||||
choiceContainerElement: document.getElementById('choices'),
|
||||
// ... other elements
|
||||
});
|
||||
}
|
||||
|
||||
// Method needed by ParagraphLayout, tied to rendering context
|
||||
measureDomText(text, style = '') {
|
||||
// Implementation to measure text width in the DOM
|
||||
// (e.g., create temporary span, apply style, measure offsetWidth)
|
||||
// ... return width ...
|
||||
}
|
||||
|
||||
async load() {
|
||||
// Fetch story content, etc.
|
||||
const response = await fetch(this.config.storyUrl);
|
||||
this.storyContent = await response.json();
|
||||
}
|
||||
|
||||
start() {
|
||||
if (!this.storyContent) throw new Error("Story not loaded");
|
||||
|
||||
// Setup initial states, load saved game if applicable
|
||||
const savedState = this.persistenceManager.loadState();
|
||||
const initialInkState = savedState ? savedState.inkJson : null;
|
||||
|
||||
this.storyPlayer.loadStory(this.storyContent, initialInkState);
|
||||
this.uiController.setupEventListeners();
|
||||
// Set initial speed from config or saved state
|
||||
this.animationQueue.setSpeed(this.config.initialSpeed || 0.05);
|
||||
|
||||
// Begin the story
|
||||
this.storyPlayer.continueStory(document.getElementById(this.config.storyContainerId));
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize and start the application
|
||||
window.onload = async () => {
|
||||
const app = new AnimatedFictionApp({
|
||||
storyUrl: 'story.json',
|
||||
storyContainerId: 'story',
|
||||
ttsApiKey: 'YOUR_API_KEY' // Example config
|
||||
// ... other configurations
|
||||
});
|
||||
await app.load();
|
||||
app.start();
|
||||
};
|
||||
```
|
||||
|
||||
### Benefits of this Refined Structure:
|
||||
|
||||
* **High Modularity**: Each class has a single, well-defined responsibility.
|
||||
* **Improved Reusability**: Components like `AnimationQueue`, `TextProcessor`, `ParagraphLayout`, `AudioManager`, `TtsPlayer`, `PersistenceManager` have minimal dependencies on the specific interactive fiction context and can be reused elsewhere.
|
||||
* **Clear Separation of Concerns**: Narrative logic (`InkStoryPlayer`) is separated from rendering (`LayoutRenderer`), timing (`AnimationQueue`), text manipulation (`TextProcessor`), UI (`UiController`), and side effects (Audio/TTS/Persistence).
|
||||
* **Testability**: Individual components can be unit-tested more easily by mocking their dependencies.
|
||||
* **Maintainability**: Changes within one module are less likely to impact others. Replacing a component (e.g., swapping the TTS provider) becomes more manageable.
|
||||
|
||||
This refactoring focuses squarely on creating independent, reusable building blocks, aligning perfectly with the goals specified.
|
||||
Reference in New Issue
Block a user