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.
333 lines
12 KiB
Rust
333 lines
12 KiB
Rust
use bevy::time::Stopwatch;
|
|
|
|
use crate::prelude::*;
|
|
|
|
pub(crate) struct IntroPlugin;
|
|
|
|
impl Plugin for IntroPlugin {
|
|
fn build(&self, app: &mut App) {
|
|
app.init_resource::<CurrentIntroParagraph>()
|
|
.init_resource::<TextAnimationSpeed>()
|
|
.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(just_pressed(KeyCode::Enter))
|
|
.or_else(just_pressed(MouseButton::Left)),
|
|
),
|
|
// 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>),
|
|
set_text_animation_speed
|
|
.run_if(
|
|
just_pressed(KeyCode::Enter)
|
|
.or_else(just_pressed(MouseButton::Left))
|
|
.or_else(just_released(KeyCode::Enter))
|
|
.or_else(just_released(MouseButton::Left))
|
|
)
|
|
)
|
|
.run_if(in_state(GameState::Intro)),
|
|
);
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Component)]
|
|
struct IntroUi;
|
|
|
|
#[derive(Debug, Resource)]
|
|
struct TextAnimationSpeed(f32);
|
|
|
|
impl Default for TextAnimationSpeed {
|
|
fn default() -> Self {
|
|
TextAnimationSpeed(5.0)
|
|
}
|
|
}
|
|
|
|
fn set_text_animation_speed(
|
|
mut animation_speed: ResMut<TextAnimationSpeed>,
|
|
tweaks: Res<Assets<Tweaks>>,
|
|
tweaks_file: Res<tweak::GameTweaks>,
|
|
keys: Res<ButtonInput<KeyCode>>,
|
|
mouse: Res<ButtonInput<MouseButton>>,
|
|
) {
|
|
*animation_speed = if keys.just_pressed(KeyCode::Enter) || mouse.just_pressed(MouseButton::Left) {
|
|
let tweak = tweaks
|
|
.get(tweaks_file.handle.clone())
|
|
.expect("Load tweakfile");
|
|
let speed = tweak.get::<f32>("animation_fast_text_speed").unwrap();
|
|
TextAnimationSpeed(speed)
|
|
} else {
|
|
TextAnimationSpeed::default()
|
|
};
|
|
|
|
debug!("Set animation speeds {:?}", *animation_speed);
|
|
}
|
|
|
|
// 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()
|
|
.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(),
|
|
justify: JustifyText::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,
|
|
) {
|
|
debug!("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,
|
|
) {
|
|
debug!("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>,
|
|
framecount: Res<FrameCount>,
|
|
mut next_state: ResMut<NextState<GameState>>,
|
|
mut curr: ResMut<CurrentIntroParagraph>,
|
|
mut commands: Commands,
|
|
mut speed: ResMut<TextAnimationSpeed>,
|
|
mut keys: ResMut<ButtonInput<KeyCode>>,
|
|
mut mouse: ResMut<ButtonInput<MouseButton>>,
|
|
) {
|
|
debug!("Managing scroll text animation");
|
|
|
|
roots.iter().for_each(|r| {
|
|
debug!("Processing {:?} at frame {:?}", r, framecount.0);
|
|
|
|
// Skip to next paragraph only the current paragraph's animation is complete
|
|
if animated_texts.is_empty() {
|
|
debug!(
|
|
"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);
|
|
|
|
*speed = TextAnimationSpeed::default();
|
|
keys.clear();
|
|
mouse.clear();
|
|
}
|
|
// Locate the last entity we were operating with
|
|
for entity in paragraphs.by_ref() {
|
|
if curr.0 == Some(entity) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
// Operate on the next entity in the list
|
|
curr.0 = paragraphs.next();
|
|
|
|
debug!("Curr: {:?}", curr.0);
|
|
|
|
// Progress to the next paragraph
|
|
if let Some(e) = curr.0 {
|
|
commands.entity(e).insert(ui::TextScroll {
|
|
progress: 0,
|
|
stopwatch: Stopwatch::new(),
|
|
});
|
|
// Continue to the title
|
|
} else {
|
|
commands.entity(r).remove::<ui::TextScrollAnimation>();
|
|
next_state.set(GameState::Title);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
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>,
|
|
parents: Query<&Parent>,
|
|
mut prompt: ResMut<NextState<ui::Prompt>>,
|
|
mut commands: Commands,
|
|
animation_speed: Res<TextAnimationSpeed>,
|
|
) {
|
|
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;
|
|
}
|
|
|
|
text_scroll.stopwatch.tick(Duration::from_secs_f32(time.delta_seconds() * animation_speed.0));
|
|
|
|
text_scroll.progress = {
|
|
// Update animation progress for this paragrpah
|
|
let delay = tweak.get::<f32>("intro_delay").unwrap();
|
|
|
|
(text_scroll.stopwatch.elapsed_secs() / 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>();
|
|
|
|
// Let the user know they can skip this
|
|
prompt.set(ui::Prompt::ClickToContinue);
|
|
}
|
|
});
|
|
}
|