#![expect(
clippy::needless_pass_by_value,
reason = "bevy systems take Res/Query by value"
)]
use bevy::log::LogPlugin;
use bevy::prelude::*;
use bevy_brink::{
BrinkBindingsAppExt, BrinkCallBatchResolved, BrinkCallCommandsExt, BrinkCallResolved,
BrinkCommand, BrinkFlow, BrinkFlowRequest, BrinkPlugin, BrinkQueryInput, BrinkStoryAsset,
LineTablesAsset, ProgramAsset, Value, advance_flow, call_ink_function,
};
use brink_runtime::FlowInstance;
const STORY: &str = "\
EXTERNAL enemy_count()
EXTERNAL play_sound(name)
EXTERNAL shout(text)
VAR checks = 0
~ play_sound(\"ambient_hum\")
You scan the clearing. Enemies near: {enemy_count()}.
{shout(\"stay sharp\")}
-> END
=== function can_advance() ===
~ return enemy_count() < 3
=== function bump_checks() ===
~ checks = checks + 1
~ return checks
";
#[derive(Component)]
struct Enemy;
#[derive(Event, BrinkCommand)]
struct PlaySound {
name: String,
}
fn enemy_count(In((_flow, _args)): In<BrinkQueryInput>, enemies: Query<&Enemy>) -> Value {
#[expect(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
Value::Int(enemies.iter().count() as i32)
}
fn on_play_sound(trigger: On<PlaySound>) {
info!("[command] play_sound(\"{}\")", trigger.event().name);
}
fn main() {
let mut app = App::new();
app.add_plugins((
LogPlugin::default(),
AssetPlugin::default(),
BrinkPlugin::<()>::default(),
));
app.bind_brink_query::<(), _, _>("enemy_count", enemy_count)
.bind_brink_fn::<(), _, _>("shout", |args| {
args.first()
.and_then(Value::as_str)
.unwrap_or_default()
.to_uppercase()
})
.bind_brink_command::<(), PlaySound>("play_sound")
.add_observer(on_play_sound)
.add_systems(Update, (request_deferred_check, request_deferred_batch));
let Some(story) = build_story(&mut app, STORY) else {
warn!("failed to compile story");
return;
};
app.world_mut().spawn(Enemy);
app.world_mut().spawn(Enemy);
let flow = app
.world_mut()
.spawn(BrinkFlowRequest::<()>::builder().story(story).build())
.id();
app.update();
info!("--- engine→ink (sync): can_advance() ---");
report_can_advance(&mut app, flow); app.world_mut().spawn(Enemy);
app.world_mut().spawn(Enemy);
report_can_advance(&mut app, flow); despawn_two_enemies(&mut app);
info!("--- engine→ink (deferred): commands.brink_call ---");
info!("--- engine→ink (deferred): commands.brink_call_batch ---");
for _ in 0..3 {
app.update();
}
info!("--- playback (advance_flow): inline {{enemy_count()}} ---");
drive_to_end(&mut app, flow);
info!("done.");
}
fn build_story(app: &mut App, src: &str) -> Option<Handle<BrinkStoryAsset>> {
let owned = src.to_string();
let output = brink_compiler::compile("demo.ink", move |path| {
if path == "demo.ink" {
Ok(owned.clone())
} else {
Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
"no includes",
))
}
})
.ok()?;
let (program, tables) = brink_runtime::link(&output.data).ok()?;
let (_, initial_context) = FlowInstance::new_at_root(&program);
let world = app.world_mut();
let program = world
.resource_mut::<Assets<ProgramAsset>>()
.add(ProgramAsset {
program,
initial_context,
effect_rows: Vec::new(),
});
let line_tables = world
.resource_mut::<Assets<LineTablesAsset>>()
.add(LineTablesAsset { tables });
Some(
world
.resource_mut::<Assets<BrinkStoryAsset>>()
.add(BrinkStoryAsset {
program,
line_tables,
}),
)
}
fn report_can_advance(app: &mut App, flow: Entity) {
match call_ink_function::<()>(app.world_mut(), flow, "can_advance", &[]) {
Ok(value) => info!("can_advance() = {:?}", value.as_bool()),
Err(err) => warn!("can_advance() failed: {err}"),
}
}
fn request_deferred_check(
mut issued: Local<bool>,
mut commands: Commands,
flows: Query<Entity, With<BrinkFlow<()>>>,
) {
if *issued {
return;
}
if let Ok(flow) = flows.single() {
*issued = true;
commands.brink_call::<()>(flow, "can_advance", ()).observe(
|on: On<BrinkCallResolved<()>>| {
info!(
"can_advance() = {:?} (delivered to observer)",
on.event().value.as_bool()
);
},
);
}
}
fn request_deferred_batch(
mut issued: Local<bool>,
mut commands: Commands,
flows: Query<Entity, With<BrinkFlow<()>>>,
) {
if *issued {
return;
}
if let Ok(flow) = flows.single() {
*issued = true;
commands
.brink_call_batch::<()>(
flow,
[
("bump_checks", Vec::<Value>::new()),
("bump_checks", Vec::<Value>::new()),
("can_advance", Vec::<Value>::new()),
],
)
.observe(|on: On<BrinkCallBatchResolved<()>>| {
info!(
"brink_call_batch results (delivered to observer, in order): {:?}",
on.event().results
);
});
}
}
fn drive_to_end(app: &mut App, flow: Entity) {
loop {
match advance_flow::<()>(app.world_mut(), flow) {
Ok(line) => {
let text = line.text().trim_end();
if !text.is_empty() {
info!("[line] {text}");
}
if line.is_terminal() {
break;
}
}
Err(err) => {
warn!("advance_flow failed: {err}");
break;
}
}
}
}
fn despawn_two_enemies(app: &mut App) {
let world = app.world_mut();
let mut query = world.query_filtered::<Entity, With<Enemy>>();
let to_remove: Vec<Entity> = query.iter(world).take(2).collect();
for entity in to_remove {
world.despawn(entity);
}
}