Fifty Narrative Engine

pub package License: MIT

Sequential sentence execution for narrative games -- you write the handlers, the engine manages the flow.

A complete sentence processing pipeline for visual novels, interactive fiction, and narrative games. NarrativeQueue handles ordering, NarrativeEngine drives the state machine, and NarrativeInterpreter delegates each instruction (read/write/ask/wait/navigate) to your callbacks. Pure Dart with no state management dependency. Part of Fifty Flutter Kit.

Sentence Queue Dialogue Choices Narration

Why fifty_narrative_engine

  • Sequential execution, not manual state -- NarrativeQueue handles sentence ordering and NarrativeEngine controls the processing state machine; you write handlers, not flow logic.
  • Pluggable instruction set -- NarrativeInterpreter delegates read/write/ask/wait/navigate to your callbacks; add TTS, custom dialogs, or screen transitions in a few lines.
  • Idempotent rendering -- SafeNarrativeWriter deduplicates sentences so re-processing a queue never shows the same sentence twice.
  • Drop into any architecture -- No GetX, no Flutter widgets required; pure Dart callbacks work with any state management system.

Installation

dependencies:
  fifty_narrative_engine: ^0.1.1

For Contributors

dependencies:
  fifty_narrative_engine:
    path: ../fifty_narrative_engine

Dependencies: web, plugin_platform_interface


Quick Start

1. Create a Sentence Model

Implement BaseNarrativeModel for your application:

class GameSentence implements BaseNarrativeModel {
  @override
  final int? order;

  @override
  final String text;

  @override
  final String instruction;

  @override
  final bool waitForUserInput;

  @override
  final String? phase;

  @override
  final List<dynamic> choices;

  GameSentence({
    this.order,
    required this.text,
    this.instruction = 'write',
    this.waitForUserInput = false,
    this.phase,
    this.choices = const [],
  });
}

2. Initialize the Engine

final engine = NarrativeEngine(
  onStatusChange: (status) {
    print('Engine status: $status');
  },
  onSentencesChanged: (sentences) {
    // Update UI with processed sentences
    setState(() {
      _displayedSentences = sentences;
    });
  },
);

3. Create an Interpreter

final interpreter = NarrativeInterpreter(
  engine: engine,
  onRead: (text) async {
    // Text-to-speech
    await ttsService.speak(text);
  },
  onWrite: (sentence) async {
    // Add to chat display
    engine.addSentenceToWritten(sentence);
  },
  onAsk: (sentence) async {
    // Show question with choices
    await showChoiceDialog(sentence);
  },
  onWait: (sentence) async {
    // Wait for user tap
    await engine.pauseUntilUserContinues();
  },
  onNavigate: (sentence) async {
    // Navigate to new screen/phase
    await navigateToPhase(sentence.phase!);
  },
);

engine.registerInterpreter(interpreter);

4. Process Sentences

// Create sentences
final sentences = [
  GameSentence(
    order: 1,
    text: 'Welcome to the adventure!',
    instruction: 'read + write',
  ),
  GameSentence(
    order: 2,
    text: 'Choose your path:',
    instruction: 'write',
    choices: ['Forest', 'Mountain', 'Sea'],
  ),
];

// Enqueue and process
engine.enqueue(sentences);
await engine.process(
  onComplete: () => print('All sentences processed!'),
  onInterrupted: () => print('Processing was interrupted'),
);

Architecture

NarrativeEngine
    |
    +-- NarrativeQueue
    |       Order-based sorting, push/pop operations
    |
    +-- NarrativeInterpreter
    |       Instruction parsing, handler delegation
    |
    +-- SafeNarrativeWriter
    |       Deduplication for idempotent rendering
    |
    +-- ProcessingStatus
            State machine: idle → processing → completed

Core Components

Component Description
NarrativeEngine Core processor managing sentence execution flow
NarrativeInterpreter Instruction parser and handler delegator
NarrativeQueue Optimized queue with order-based sorting
SafeNarrativeWriter Deduplication for idempotent sentence rendering
BaseNarrativeModel Abstract interface for custom sentence models

Customization

All behavior is controlled through NarrativeInterpreter callbacks. Each instruction type maps to a handler you provide -- add TTS, custom dialogs, audio cues, or screen transitions without modifying the engine.

Interpreter Callbacks

final interpreter = NarrativeInterpreter(
  engine: engine,

  // read: Text-to-speech or audio playback
  onRead: (text) async {
    await ttsService.speak(text);
  },

  // write: Display text on screen
  onWrite: (sentence) async {
    engine.addSentenceToWritten(sentence);
  },

  // ask: Show choices, wait for player selection
  onAsk: (sentence) async {
    engine.pause();
    final choice = await showChoiceDialog(sentence.choices);
    handleChoice(choice);
    engine.resume();
  },

  // wait: Pause until player taps to continue
  onWait: (sentence) async {
    await engine.pauseUntilUserContinues();
  },

  // navigate: Transition to a new phase or screen
  onNavigate: (sentence) async {
    await Navigator.pushNamed(context, '/game/${sentence.phase}');
  },

  // Catch-all for unrecognized instructions
  onUnhandled: (sentence) async {
    debugPrint('Unknown instruction: ${sentence.instruction}');
  },
);

engine.registerInterpreter(interpreter);

Instructions can be combined (e.g. read + write) and the interpreter handles both in sequence.

Custom Sentence Models

Implement BaseNarrativeModel to attach domain-specific data to your sentences:

class GameSentence implements BaseNarrativeModel {
  @override final int? order;
  @override final String text;
  @override final String instruction;
  @override final bool waitForUserInput;
  @override final String? phase;
  @override final List<dynamic> choices;

  // Add your own fields
  final String? speakerName;
  final String? portraitAsset;

  GameSentence({
    this.order,
    required this.text,
    this.instruction = 'write',
    this.waitForUserInput = false,
    this.phase,
    this.choices = const [],
    this.speakerName,
    this.portraitAsset,
  });
}

API Reference

NarrativeEngine

Core processor managing sentence execution flow.

class NarrativeEngine {
  // Constructor
  NarrativeEngine({
    ValueChanged<ProcessingStatus>? onStatusChange,
    ValueChanged<List<BaseNarrativeModel>>? onSentencesChanged,
    ValueChanged<int>? onProcessingIndexChanged,
    NarrativeInterpreter? interpreter,
  });

  // Getters
  List<BaseNarrativeModel> get sentences;
  int get processingIndex;
  ProcessingStatus get status;
  Stream<ProcessingStatus> get onStatusChanged;

  // Core Methods
  void enqueue(List<BaseNarrativeModel> newSentences);
  Future<void> process({VoidCallback? onComplete, VoidCallback? onInterrupted});

  // Flow Control
  void pause();
  void resume();
  void cancel();
  void reset();

  // User Interaction
  Future<void> pauseUntilUserContinues();
  void continueAfterUserInput();

  // Sentence Management
  void addSentenceToWritten(BaseNarrativeModel sentence);
  void clearProcessedSentences();

  // Registration
  void registerInterpreter(NarrativeInterpreter interpreter);

  // Cleanup
  void dispose();
}

ProcessingStatus

Engine state enumeration:

enum ProcessingStatus {
  idle,       // Not processing
  processing, // Actively processing
  paused,     // Temporarily paused
  cancelled,  // Cancelled before completion
  completed,  // All sentences processed
}

NarrativeInterpreter

Instruction parser and handler delegator:

class NarrativeInterpreter {
  NarrativeInterpreter({
    required NarrativeEngine engine,
    Future<void> Function(String text)? onRead,
    Future<void> Function(BaseNarrativeModel sentence)? onWrite,
    Future<void> Function(BaseNarrativeModel sentence)? onAsk,
    Future<void> Function(BaseNarrativeModel sentence)? onNavigate,
    Future<void> Function(BaseNarrativeModel sentence)? onWait,
    Future<void> Function(BaseNarrativeModel sentence)? onUnhandled,
  });

  Future<void> interpret(BaseNarrativeModel sentence);
}

Supported Instructions:

Instruction Handler Purpose
read onRead Text-to-speech output
write onWrite Display text on screen
ask onAsk Show choices, wait for selection
wait onWait Pause until user tap
navigate onNavigate Phase-based navigation

Instructions can be combined: read + write

NarrativeQueue

Optimized queue for large-scale sentence processing:

class NarrativeQueue {
  // Push operations
  void pushBack(BaseNarrativeModel item);
  void pushBackAll(Iterable<BaseNarrativeModel> items);
  void pushFront(BaseNarrativeModel item);
  void pushFrontAll(Iterable<BaseNarrativeModel> items);
  void pushOrdered(BaseNarrativeModel item);
  void pushOrderedAll(Iterable<BaseNarrativeModel> items);

  // Access
  BaseNarrativeModel pop();
  BaseNarrativeModel peek();
  List<BaseNarrativeModel> toList();

  // Management
  void clear();
  void sortByOrder();
  void remove(BaseNarrativeModel item);
  void removeWhere(bool Function(BaseNarrativeModel) test);

  // Properties
  int get length;
  bool get isEmpty;
  bool get isNotEmpty;
  bool contains(BaseNarrativeModel item);
}

SafeNarrativeWriter

Prevents duplicate sentence rendering:

class SafeNarrativeWriter {
  SafeNarrativeWriter(Future<void> Function(BaseNarrativeModel) delegate);

  Future<void> write(BaseNarrativeModel sentence);
  void reset();
}

BaseNarrativeModel

Abstract interface for sentence models:

abstract class BaseNarrativeModel {
  int? get order;           // Queue sorting order
  String get text;          // Sentence content
  String get instruction;   // Processing directive
  bool get waitForUserInput;// Pause after processing
  String? get phase;        // Navigation phase
  List<dynamic> get choices;// Choice options
}

Usage Patterns

Tap to Continue

final interpreter = NarrativeInterpreter(
  engine: engine,
  onWrite: (sentence) async {
    engine.addSentenceToWritten(sentence);
  },
  onWait: (sentence) async {
    // Engine will pause until user taps
    await engine.pauseUntilUserContinues();
  },
);

// In UI
GestureDetector(
  onTap: () => engine.continueAfterUserInput(),
  child: ChatDisplay(sentences: engine.sentences),
)

Choice System

final interpreter = NarrativeInterpreter(
  engine: engine,
  onAsk: (sentence) async {
    engine.pause();

    final choice = await showDialog<String>(
      context: context,
      builder: (_) => ChoiceDialog(choices: sentence.choices),
    );

    handleChoice(choice);
    engine.resume();
  },
);

Phase Navigation

final interpreter = NarrativeInterpreter(
  engine: engine,
  onNavigate: (sentence) async {
    await Navigator.pushNamed(
      context,
      '/game/${sentence.phase}',
    );
  },
);

// Sentences with phases
GameSentence(
  text: 'You enter the forest...',
  instruction: 'write',
  phase: 'forest',  // Will trigger navigation
)

With TTS Integration

// Using fifty_speech_engine
final speechEngine = FiftySpeechEngine();

final interpreter = NarrativeInterpreter(
  engine: engine,
  onRead: (text) async {
    await speechEngine.speak(text);
  },
  onWrite: (sentence) async {
    engine.addSentenceToWritten(sentence);
  },
);

With Audio Engine

import 'package:fifty_audio_engine/fifty_audio_engine.dart';

final interpreter = NarrativeInterpreter(
  engine: engine,
  onNavigate: (sentence) async {
    // Play transition sound
    await audioEngine.playSfx('transition.mp3');

    // Change BGM for new phase
    await audioEngine.playBgm('${sentence.phase}_theme.mp3');
  },
);

Order-Based Processing

final queue = NarrativeQueue();

// Sentences added out of order
queue.pushOrdered(GameSentence(text: 'Third', order: 3));
queue.pushOrdered(GameSentence(text: 'First', order: 1));
queue.pushOrdered(GameSentence(text: 'Second', order: 2));

// Will process: First, Second, Third
while (queue.isNotEmpty) {
  final sentence = queue.pop();
  print(sentence.text);
}

Platform Support

Platform Support Notes
Android Yes
iOS Yes
macOS Yes
Linux Yes
Windows Yes
Web Yes

Fifty Design Language Integration

This package is part of Fifty Flutter Kit:

  • Consistent naming - All classes follow Fifty Flutter Kit patterns
  • fifty_speech_engine - TTS integration for read instructions
  • fifty_audio_engine - Sound effects for navigation transitions
  • Compatible packages - Works alongside all Fifty Flutter Kit packages

Migration from erune_narrative_engine

If migrating from the original package:

  1. Update import:

    // Before
    import 'package:erune_narrative_engine/erune_narrative_engine.dart';
    
    // After
    import 'package:fifty_narrative_engine/fifty_narrative_engine.dart';
    
  2. Replace class names:

    • EruneSentencesEngine -> FiftyNarrativeEngine
  3. GetX Removal: The NarrativeEngine no longer extends GetxController. Use the callback parameters instead:

    // Before (with GetX)
    final engine = Get.put(NarrativeEngine());
    engine.sentences; // RxList
    
    // After (callbacks)
    final engine = NarrativeEngine(
      onSentencesChanged: (sentences) => setState(() {}),
    );
    engine.sentences; // Regular List
    

Version

Current: 0.1.2


License

MIT License - see LICENSE for details.

Part of Fifty Flutter Kit.