use bevy::prelude::*;
use bevy_replique::prelude::*;
const START_NODE: &str = "start";
const PORTRAIT: f32 = 96.0;
fn main() {
App::new()
.add_plugins(DefaultPlugins.set(AssetPlugin {
file_path: "assets".to_string(),
..default()
}))
.add_plugins(RepliquePlugin)
.add_dialogue_command(add_scene)
.add_dialogue_command(remove_scene)
.init_resource::<Waiting>()
.add_systems(Startup, setup)
.add_systems(
Update,
(
show_line,
show_finished,
handle_input,
resume_on_awaited_all,
),
)
.add_systems(PostUpdate, handle_tween)
.add_observer(despawn_on_tween_end)
.add_observer(remove_await_on_tween_end)
.run();
}
#[derive(RepliqueValue, Component, Debug, Clone, Copy, PartialEq, Eq)]
enum Character {
Alice,
Bob,
Caroline,
}
impl Character {
fn from_name(name: &str) -> Option<Self> {
match name {
"Alice" => Some(Self::Alice),
"Bob" => Some(Self::Bob),
"Caroline" => Some(Self::Caroline),
_ => None,
}
}
fn name(self) -> &'static str {
match self {
Self::Alice => "Alice",
Self::Bob => "Bob",
Self::Caroline => "Caroline",
}
}
fn color(self) -> Color {
match self {
Self::Alice => Color::srgb(0.92, 0.68, 0.72),
Self::Bob => Color::srgb(0.62, 0.75, 0.94),
Self::Caroline => Color::srgb(0.70, 0.88, 0.70),
}
}
}
#[derive(RepliqueArgs)]
struct CharactersParams(#[variadic] Vec<Character>);
#[replique_command]
fn add_scene(
In(CharactersParams(characters)): In<CharactersParams>,
mut commands: Commands,
call: DialogueCall,
stage: Single<Entity, With<Stage>>,
on_stage: Query<&Character>,
) -> CommandFlow {
let mut awaiting = None;
for character in characters {
if on_stage.iter().any(|on_stage| *on_stage == character) {
continue;
}
if awaiting.is_none() {
awaiting = Some(commands.spawn(DialogueAwait(call.token())).id());
}
commands.entity(*stage).with_child((
portrait(character),
TweenMargin::new(-PORTRAIT - 32.0, 0.0),
AwaitedBy(awaiting.unwrap()),
));
}
if awaiting.is_some() {
CommandFlow::Blocking
} else {
CommandFlow::Immediate
}
}
#[replique_command]
fn remove_scene(
In(CharactersParams(characters)): In<CharactersParams>,
mut commands: Commands,
on_stage: Query<(Entity, &Character)>,
) {
for (entity, character) in &on_stage {
if characters.contains(character) {
commands
.entity(entity)
.insert((TweenMargin::new(0.0, -PORTRAIT - 32.0), DespawnOnTweenEnd));
}
}
}
#[derive(Resource, Default)]
enum Waiting {
#[default]
Nothing,
Line(DialogueToken),
}
#[derive(Component)]
struct Stage;
#[derive(Component)]
struct LineText(Character);
#[derive(Component)]
struct HintText;
#[derive(Component)]
struct DialogueAwait(DialogueToken);
#[derive(Component)]
#[relationship(relationship_target = Awaiting)]
struct AwaitedBy(Entity);
#[derive(Component)]
#[relationship_target(relationship = AwaitedBy)]
struct Awaiting(Vec<Entity>);
#[derive(Component)]
struct TweenMargin {
from: f32,
to: f32,
timer: Timer,
}
impl TweenMargin {
fn new(from: f32, to: f32) -> Self {
TweenMargin {
from,
to,
timer: Timer::from_seconds(0.25, TimerMode::Once),
}
}
fn margin(&self) -> f32 {
self.from.lerp(self.to, self.timer.fraction())
}
}
#[derive(Component)]
struct DespawnOnTweenEnd;
fn portrait(character: Character) -> impl Bundle {
(
character,
Node {
align_items: AlignItems::Center,
column_gap: px(16),
..default()
},
children![
(
Node {
width: px(PORTRAIT),
height: px(PORTRAIT),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
..default()
},
BackgroundColor(character.color()),
children![(
Text::new(character.name()),
TextFont::from_font_size(16.0),
TextColor(Color::BLACK),
)],
),
(
Text::new(""),
TextFont::from_font_size(24.0),
LineText(character),
),
],
)
}
fn setup(
mut commands: Commands,
asset_server: Res<AssetServer>,
mut start: MessageWriter<StartDialogue>,
) {
commands.spawn(Camera2d);
let dialogue: Handle<RepliqueDialogue> = asset_server.load("dialogue/custom_command.rep");
let runner = commands.spawn(DialogueRunner::new(dialogue)).id();
start.write(StartDialogue {
runner,
node: START_NODE.into(),
});
commands.spawn((
Node {
width: percent(100),
height: percent(100),
flex_direction: FlexDirection::Column,
justify_content: JustifyContent::SpaceBetween,
padding: UiRect::all(px(32)),
..default()
},
children![
(
Node {
flex_direction: FlexDirection::Column,
row_gap: px(16),
..default()
},
Stage,
),
(
Text::new("[Space] continue"),
TextFont::from_font_size(20.0),
TextColor(Color::srgb(0.7, 0.7, 0.8)),
HintText,
),
],
));
}
fn show_line(
mut lines: MessageReader<DialogueLine>,
mut waiting: ResMut<Waiting>,
mut texts: Query<(&LineText, &mut Text)>,
) {
for line in lines.read() {
let speaking = line.speaker.as_deref().and_then(Character::from_name);
for (LineText(character), mut text) in &mut texts {
text.0 = match speaking {
Some(speaking) if speaking == *character => line.text.clone(),
_ => String::new(),
};
}
*waiting = Waiting::Line(line.token);
}
}
fn show_finished(
mut finished: MessageReader<DialogueFinished>,
mut waiting: ResMut<Waiting>,
mut hint: Single<&mut Text, With<HintText>>,
) {
for _ in finished.read() {
hint.0 = "End of dialogue.".to_string();
*waiting = Waiting::Nothing;
}
}
fn handle_input(
keys: Res<ButtonInput<KeyCode>>,
mut waiting: ResMut<Waiting>,
mut resume: MessageWriter<ResumeDialogue>,
) {
if let Waiting::Line(token) = *waiting
&& keys.just_pressed(KeyCode::Space)
{
resume.write(ResumeDialogue {
token,
input: ResumeInput::Advance,
});
*waiting = Waiting::Nothing;
}
}
fn handle_tween(
mut commands: Commands,
time: Res<Time>,
tweens: Query<(Entity, &mut Node, &mut TweenMargin)>,
) {
for (entity, mut node, mut tween) in tweens {
if tween.timer.tick(time.delta()).is_finished() {
node.margin.left = px(tween.to);
commands.entity(entity).remove::<TweenMargin>();
continue;
}
node.margin.left = px(tween.margin())
}
}
fn despawn_on_tween_end(
tween_ended: On<Remove, TweenMargin>,
mut commands: Commands,
tweens: Query<(), With<DespawnOnTweenEnd>>,
) {
if tweens.contains(tween_ended.entity) {
commands.entity(tween_ended.entity).despawn();
}
}
fn remove_await_on_tween_end(tween_ended: On<Remove, TweenMargin>, mut commands: Commands) {
commands.entity(tween_ended.entity).remove::<AwaitedBy>();
}
fn resume_on_awaited_all(
mut commands: Commands,
mut resume: MessageWriter<ResumeDialogue>,
dialogues: Query<(Entity, &DialogueAwait), Without<Awaiting>>,
) {
for (entity, dialogue) in dialogues {
resume.write(ResumeDialogue {
token: dialogue.0,
input: ResumeInput::Advance,
});
commands.entity(entity).despawn();
}
}