You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
martian-chess/src/intro.rs

294 lines
10 KiB
Rust

use bevy::core::FrameCount;
use crate::prelude::*;
pub(crate) struct IntroPlugin;
impl Plugin for IntroPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<CurrentIntroParagraph>()
.add_systems(
OnExit(GameState::Loading),
init_intro_text
.run_if(resource_exists::<tweak::GameTweaks>())
.run_if(run_once()),
)
.add_systems(OnEnter(GameState::Intro), manage_intro)
.add_systems(OnExit(GameState::Intro), cleanup_intro)
// All of these run during GameState::Intro
.add_systems(
Update,
(
manage_intro.run_if(any_component_removed::<ui::TextScrollAnimation>()),
// Started when the TextScrollAnimation component is added to the parent entity
// Updated for as long as there is scrolling text
manage_scroll_text_animation.run_if(
any_component_added::<ui::TextScrollAnimation>.or_else(
|keys: Res<Input<KeyCode>>| -> bool {
keys.just_pressed(KeyCode::Return)
},
),
),
// Play intro manages playing the intro of each individual paragraph
// Runs every time the TextScroll component (managed by manage_scroll_text_animation) is updated
scroll_text.run_if(any_with_component::<ui::TextScroll>()),
)
.run_if(in_state(GameState::Intro)),
);
}
}
#[derive(Debug, Resource)]
struct IntroPlayed;
#[derive(Debug, Component)]
struct IntroUi;
// Draw the intro text (invisible) on startup
// Requires the Tweakfile to be loaded
fn init_intro_text(
tweaks_file: Res<tweak::GameTweaks>,
tweaks: Res<Assets<tweak::Tweaks>>,
ui_font: Res<ui::UiFont>,
mut commands: Commands,
) {
let tweak = tweaks.get(tweaks_file.handle.clone()).expect("Load tweaks");
let texts = tweak.get::<Vec<String>>("intro_text").expect("Intro text");
let background_hex = tweak.get::<String>("intro_rgba_background").unwrap();
let text_hidden_hex = tweak.get::<String>("intro_rgba_hidden").unwrap();
commands
.spawn((
IntroUi,
NodeBundle {
style: Style {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
position_type: PositionType::Absolute,
padding: UiRect::all(Val::Px(50.0)),
..default()
},
background_color: Color::NONE.into(),
visibility: Visibility::Hidden,
..default()
},
))
.with_children(|parent| {
texts.iter().for_each(|text| {
parent
.spawn((
IntroUi,
NodeBundle {
style: Style {
position_type: PositionType::Absolute,
padding: UiRect::all(Val::Px(25.0)),
..default()
},
visibility: Visibility::Hidden,
background_color: Color::hex(&background_hex).unwrap().into(),
..default()
},
))
.with_children(|parent| {
parent.spawn((
IntroUi,
TextBundle {
text: Text {
sections: text
.chars()
.into_iter()
.map(|c| TextSection {
value: c.to_string(),
style: TextStyle {
font_size: 16.0,
color: Color::hex(&text_hidden_hex).unwrap(),
font: ui_font.handle.clone(),
},
})
.collect(),
alignment: TextAlignment::Center,
..default()
},
..default()
},
));
});
});
});
}
fn manage_intro(
// Hack, this way of "finding" the root Node containing our paragraphs is precarious
query: Query<Entity, (With<Node>, With<IntroUi>, Without<Parent>)>,
mut commands: Commands,
) {
info!("Managing intro");
query.iter().for_each(|e| {
commands
.entity(e)
.insert(ui::TextScrollAnimation)
.insert(Visibility::Visible);
});
}
fn cleanup_intro(
query: Query<Entity, With<IntroUi>>,
mut texts: Query<&mut Text, With<IntroUi>>,
mut curr: ResMut<CurrentIntroParagraph>,
tweaks_file: Res<tweak::GameTweaks>,
tweaks: Res<Assets<tweak::Tweaks>>,
mut commands: Commands,
) {
info!("Cleaning up intro");
query.iter().for_each(|e| {
commands
.entity(e)
.remove::<ui::TextScrollAnimation>()
.remove::<ui::TextScroll>()
.insert(Visibility::Hidden);
});
{
// Reset text colors
let tweak = tweaks.get(tweaks_file.handle.clone()).expect("Load tweaks");
let text_hidden_hex = tweak.get::<String>("intro_rgba_hidden").unwrap();
let text_hidden_color = Color::hex(text_hidden_hex).unwrap();
texts.iter_mut().for_each(|mut text| {
text.sections.iter_mut().for_each(|s| {
s.style.color = text_hidden_color;
});
});
}
curr.0 = None;
}
#[derive(Debug, Resource, Default)]
struct CurrentIntroParagraph(Option<Entity>);
fn manage_scroll_text_animation(
roots: Query<
Entity,
Or<(
With<ui::TextScrollAnimation>,
Added<ui::TextScrollAnimation>,
)>,
>,
texts: Query<Entity, (With<Text>, With<IntroUi>)>,
animated_texts: Query<&ui::TextScroll>,
parents: Query<&Parent>,
children: Query<&Children>,
time: Res<Time>,
framecount: Res<FrameCount>,
mut next_state: ResMut<NextState<GameState>>,
mut curr: ResMut<CurrentIntroParagraph>,
mut commands: Commands,
) {
info!("Managing scroll text animation");
roots.iter().for_each(|r| {
info!("Processing {:?} at frame {:?}", r, framecount.0);
// Skip to next paragraph only the current paragraph's animation is complete
if animated_texts.is_empty() {
info!(
"No animations playing, moving on to next paragraph {:?}",
curr.0
);
// Create an iterator over all paragraphs
let mut paragraphs = texts.iter_many(children.iter_descendants(r));
if curr.0.is_some() {
// Hide the last active entity
if let Some(e) = curr.0 {
let p = parents.get(e).unwrap();
commands.entity(p.get()).insert(Visibility::Hidden);
commands.entity(e).insert(Visibility::Hidden);
}
// Locate the last entity we were operating with
while let Some(entity) = paragraphs.next() {
if curr.0 == Some(entity) {
break;
}
}
}
// Operate on the next entity in the list
curr.0 = paragraphs.next();
info!("Curr: {:?}", curr.0);
if let Some(e) = curr.0 {
commands.entity(e).insert(ui::TextScroll {
progress: 0,
start: time.elapsed_seconds(),
});
} else {
commands.entity(r).remove::<ui::TextScrollAnimation>();
next_state.set(GameState::Play);
}
}
});
}
fn scroll_text(
mut texts: Query<(Entity, &mut Visibility, &mut Text, &mut ui::TextScroll)>,
tweaks_file: Res<tweak::GameTweaks>,
tweaks: Res<Assets<tweak::Tweaks>>,
time: Res<Time>,
keys: Res<Input<KeyCode>>,
parents: Query<&Parent>,
mut commands: Commands,
) {
let tweak = tweaks.get(tweaks_file.handle.clone()).expect("Load tweaks");
texts
.iter_mut()
.for_each(|(entity, mut vis, mut text, mut text_scroll)| {
debug!("Updating {:?} {:?}", entity, text_scroll);
// If the animation just started, make visible
if text_scroll.progress == 0 {
let p = parents.get(entity).unwrap();
commands.entity(p.get()).insert(Visibility::Inherited);
// Make the text visible as well
*vis = Visibility::Inherited;
}
// If user pressed enter, skip to end of paragraph
text_scroll.progress = if keys.just_pressed(KeyCode::Return) {
usize::MAX
}
// Otherwise, progress by some fixed increment
else {
// Update animation progress for this paragrpah
let delay = tweak.get::<f32>("intro_delay").unwrap();
// Update text_scroll.progress based on frame count
((time.elapsed_seconds() - text_scroll.start) / delay) as usize
};
// Fetch desired color from tweakfile
let text_visible_hex = tweak.get::<String>("intro_rgba_visible").unwrap();
let text_color = Color::hex(text_visible_hex).unwrap();
// Update characters in this paragraph
// PERF: Re-doing a lot of work every loop
text.sections.iter_mut().enumerate().for_each(|(i, s)| {
if i <= text_scroll.progress {
s.style.color = text_color;
}
});
// If the entire paragraph is played out, remove the component
if text.sections.iter().all(|s| s.style.color == text_color) {
commands.entity(entity).remove::<ui::TextScroll>();
}
});
}