Skip to main content

ripplefx_engine 1.5.0 copy "ripplefx_engine: ^1.5.0" to clipboard
ripplefx_engine: ^1.5.0 copied to clipboard

Play a campaign written in the Ripple Effect editor from Dart or Flutter — a Flame game, an app, a server — with the engine's own story, dice and soundscape scheduling, over its C ABI.

example/ripplefx_engine_example.dart

// A campaign in a terminal — the Player, the soundscape driver, and a speaker
// that prints what it would play instead of playing it: the seam a real
// speaker (flame_audio, audioplayers, just_audio, SoLoud, a game engine's
// own) is plugged into. doc/speaker.md is the contract; example/flame_player
// is the same in a Flame game.
//
//   dart run example/ripplefx_engine_example.dart <bundle folder> [locale]
import 'dart:io';

import 'package:ripplefx_engine/ripplefx_engine.dart';

Future<void> main(List<String> args) async {
  if (args.isEmpty) {
    stderr.writeln(
      'usage: dart run example/ripplefx_engine_example.dart <bundle folder> '
      '[locale]',
    );
    exitCode = 64;
    return;
  }
  try {
    final campaign = await Campaign.openDirectory(args.first);
    final manifest = campaign.manifest;
    stdout.writeln(
      '${manifest.project.title} — engine ${RippleEngine.version}',
    );

    final audio = SoundscapeDriver(campaign, _PrintingSpeaker());
    final player = Player(
      Session.create(
        campaign,
        SessionConfig(
          board: manifest.entries.isEmpty ? 0 : manifest.entries.first,
          seed: DateTime.now().microsecondsSinceEpoch,
          locale: args.length > 1 ? args[1] : null,
        ),
      ),
      audio: audio,
      onError: (error, _) => stderr.writeln(error),
    );
    player.onAnyCallback(
      (fired) =>
          stdout.writeln('  [callback] ${fired.callback} (${fired.eventName})'),
    );

    player.start();
    // The host's loop: a frame every 25 ms, the driver and the player told
    // the time has moved, a turn answered when one is showing.
    for (
      var turn = player.turn;
      turn != null && !turn.isOver;
      turn = player.turn
    ) {
      audio.update();
      player.update();
      switch (turn) {
        case Show(:final passage):
          if (_ask(passage) case final int picked) {
            player.choose(picked);
          } else {
            player.leave();
          }
        case Ask():
          player.answer(null);
        default:
          await Future<void>.delayed(const Duration(milliseconds: 25));
      }
    }
    switch (player.turn) {
      case Ended(:final title):
        stdout.writeln('\n— The end${title == null ? '' : ': $title'} —');
      case Failed(:final message):
        stdout.writeln('\nThe story could not go on: $message');
      default:
    }
    player.dispose();
    audio.dispose();
    campaign.dispose();
  } on RippleException catch (error) {
    stderr.writeln('ripple: ${error.message}');
    exitCode = 1;
  }
}

// The index of the choice taken, or null when the reader leaves.
int? _ask(Passage passage) {
  stdout.writeln('\n== ${passage.title} ==\n${passage.text}\n');
  if (passage.choices.isEmpty) return null;
  for (final (i, choice) in passage.choices.indexed) {
    stdout.writeln(
      '  ${i + 1}. ${choice.text}${choice.available ? '' : ' (spent)'}',
    );
  }
  while (true) {
    stdout.write('> ');
    final line = stdin.readLineSync();
    if (line == null) return null;
    final picked = int.tryParse(line.trim());
    if (picked != null &&
        picked >= 1 &&
        picked <= passage.choices.length &&
        passage.choices[picked - 1].available) {
      return picked - 1;
    }
  }
}

// Prints every voice command it is given. A real one opens the file and plays
// it; this one says what it would have done.
final class _PrintingSpeaker implements Speaker {
  @override
  Voice? open(VoiceSource source, VoiceRole role, VoiceEvents events) {
    stdout.writeln(
      '  [sound] open ${source.file ?? source.path} (${role.name})',
    );
    return _PrintingVoice(source.path);
  }

  @override
  bool resolves(VoiceSource source) => true;
}

final class _PrintingVoice implements Voice {
  _PrintingVoice(this.path);

  final String path;

  @override
  void play(VoicePlay request) => stdout.writeln(
    '  [sound] play $path from ${request.fromMs.toStringAsFixed(0)} ms at '
    'gain ${request.gain.toStringAsFixed(2)}${request.looping ? ', looping' : ''}',
  );

  @override
  void setGain(double gain) {}

  @override
  void pause() => stdout.writeln('  [sound] pause $path');

  @override
  void resume() => stdout.writeln('  [sound] resume $path');

  @override
  void stop() => stdout.writeln('  [sound] stop $path');
}
1
likes
130
points
34
downloads

Documentation

Documentation
API reference

Publisher

verified publisherconnecting-the-dots.dev

Weekly Downloads

Play a campaign written in the Ripple Effect editor from Dart or Flutter — a Flame game, an app, a server — with the engine's own story, dice and soundscape scheduling, over its C ABI.

Homepage
Repository (GitHub)
View/report issues

Topics

#interactive-fiction #narrative #game-development #ffi #flame

License

unknown (license)

Dependencies

archive, code_assets, crypto, ffi, hooks, web

More

Packages that depend on ripplefx_engine