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.
294 lines
10 KiB
Rust
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>();
|
|
}
|
|
});
|
|
}
|