|
|
|
@ -1,11 +1,7 @@
|
|
|
|
use std::collections::VecDeque;
|
|
|
|
use bevy::core::FrameCount;
|
|
|
|
|
|
|
|
|
|
|
|
use bevy::{core::FrameCount, utils::HashMap};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
use crate::prelude::*;
|
|
|
|
use crate::prelude::*;
|
|
|
|
|
|
|
|
|
|
|
|
use self::ui::TextScroll;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
pub(crate) struct IntroPlugin;
|
|
|
|
pub(crate) struct IntroPlugin;
|
|
|
|
|
|
|
|
|
|
|
|
impl Plugin for IntroPlugin {
|
|
|
|
impl Plugin for IntroPlugin {
|
|
|
|
@ -20,18 +16,21 @@ impl Plugin for IntroPlugin {
|
|
|
|
.add_systems(OnExit(GameState::Intro), deactivate::<Intro>)
|
|
|
|
.add_systems(OnExit(GameState::Intro), deactivate::<Intro>)
|
|
|
|
.add_systems(
|
|
|
|
.add_systems(
|
|
|
|
Update,
|
|
|
|
Update,
|
|
|
|
play_text_scroll
|
|
|
|
// 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(in_state(GameState::Intro))
|
|
|
|
.run_if(in_state(GameState::Intro))
|
|
|
|
.run_if(any_component_changed::<ui::TextScroll>),
|
|
|
|
.run_if(any_component_added::<ui::TextScrollAnimation>.or_else(
|
|
|
|
|
|
|
|
|keys: Res<Input<KeyCode>>| -> bool { keys.just_pressed(KeyCode::Return) },
|
|
|
|
|
|
|
|
)),
|
|
|
|
)
|
|
|
|
)
|
|
|
|
.add_systems(
|
|
|
|
.add_systems(
|
|
|
|
Update,
|
|
|
|
Update,
|
|
|
|
// Play intro manages playing the intro of each individual paragraph
|
|
|
|
// Play intro manages playing the intro of each individual paragraph
|
|
|
|
play_intro.run_if(
|
|
|
|
// Runs every time the TextScroll component (managed by manage_scroll_text_animation) is updated
|
|
|
|
any_component_added::<TextScroll>
|
|
|
|
scroll_text
|
|
|
|
.or_else(any_component_removed::<TextScroll>())
|
|
|
|
.run_if(in_state(GameState::Intro))
|
|
|
|
.or_else(|keys: Res<Input<KeyCode>>| -> bool { keys.just_pressed(KeyCode::Return) })
|
|
|
|
.run_if(any_with_component::<ui::TextScroll>()),
|
|
|
|
)
|
|
|
|
|
|
|
|
);
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@ -68,7 +67,6 @@ fn init_intro_text(
|
|
|
|
align_items: AlignItems::Center,
|
|
|
|
align_items: AlignItems::Center,
|
|
|
|
flex_direction: FlexDirection::Column,
|
|
|
|
flex_direction: FlexDirection::Column,
|
|
|
|
position_type: PositionType::Absolute,
|
|
|
|
position_type: PositionType::Absolute,
|
|
|
|
padding: UiRect::all(Val::Px(50.0)),
|
|
|
|
|
|
|
|
..default()
|
|
|
|
..default()
|
|
|
|
},
|
|
|
|
},
|
|
|
|
background_color: Color::NONE.into(),
|
|
|
|
background_color: Color::NONE.into(),
|
|
|
|
@ -77,132 +75,169 @@ fn init_intro_text(
|
|
|
|
},
|
|
|
|
},
|
|
|
|
))
|
|
|
|
))
|
|
|
|
.with_children(|parent| {
|
|
|
|
.with_children(|parent| {
|
|
|
|
parent
|
|
|
|
texts.iter().for_each(|text| {
|
|
|
|
.spawn((
|
|
|
|
parent
|
|
|
|
Intro,
|
|
|
|
.spawn((
|
|
|
|
NodeBundle {
|
|
|
|
Intro,
|
|
|
|
style: Style {
|
|
|
|
NodeBundle {
|
|
|
|
padding: UiRect::all(Val::Px(25.0)),
|
|
|
|
style: Style {
|
|
|
|
|
|
|
|
width: Val::Percent(100.0),
|
|
|
|
|
|
|
|
justify_content: JustifyContent::Center,
|
|
|
|
|
|
|
|
align_content: AlignContent::Center,
|
|
|
|
|
|
|
|
flex_direction: FlexDirection::Column,
|
|
|
|
|
|
|
|
align_items: AlignItems::Center,
|
|
|
|
|
|
|
|
justify_items: JustifyItems::Center,
|
|
|
|
|
|
|
|
..default()
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
background_color: Color::hex(&background_hex).unwrap().into(),
|
|
|
|
..default()
|
|
|
|
..default()
|
|
|
|
},
|
|
|
|
},
|
|
|
|
background_color: Color::hex(&background_hex).unwrap().into(),
|
|
|
|
))
|
|
|
|
..default()
|
|
|
|
.with_children(|parent| {
|
|
|
|
},
|
|
|
|
|
|
|
|
))
|
|
|
|
|
|
|
|
.with_children(|parent| {
|
|
|
|
|
|
|
|
texts.iter().for_each(|text| {
|
|
|
|
|
|
|
|
parent.spawn((
|
|
|
|
parent.spawn((
|
|
|
|
Intro,
|
|
|
|
Intro,
|
|
|
|
TextBundle::from_sections(text.chars().into_iter().map(|c| TextSection {
|
|
|
|
TextBundle {
|
|
|
|
value: c.to_string(),
|
|
|
|
style: Style {
|
|
|
|
style: TextStyle {
|
|
|
|
// position_type: PositionType::Absolute,
|
|
|
|
font_size: 16.0,
|
|
|
|
top: Val::Px(0.0),
|
|
|
|
color: Color::hex(&text_hidden_hex).unwrap(),
|
|
|
|
left: Val::Px(0.0),
|
|
|
|
font: ui_font.handle.clone(),
|
|
|
|
justify_self: JustifySelf::Center,
|
|
|
|
|
|
|
|
align_self: AlignSelf::Center,
|
|
|
|
|
|
|
|
..default()
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
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()
|
|
|
|
.with_text_alignment(TextAlignment::Center),
|
|
|
|
},
|
|
|
|
));
|
|
|
|
));
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn start_intro(
|
|
|
|
fn start_intro(
|
|
|
|
query: Query<Entity, (With<Node>, With<Intro>)>,
|
|
|
|
// Hack, this way of "finding" the Node with our animated Text is precarious
|
|
|
|
|
|
|
|
query: Query<Entity, (With<Node>, With<Intro>, With<Children>, Without<Parent>)>,
|
|
|
|
mut commands: Commands,
|
|
|
|
mut commands: Commands,
|
|
|
|
) {
|
|
|
|
) {
|
|
|
|
query.iter().for_each(|e| {
|
|
|
|
query.iter().for_each(|e| {
|
|
|
|
commands.entity(e).insert(TextScroll { progress: 0, start: 0 });
|
|
|
|
commands.entity(e).insert(ui::TextScrollAnimation);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Manages playing the intro animation
|
|
|
|
fn manage_scroll_text_animation(
|
|
|
|
// Each paragrpah types out letter by letter in a separate animation handled by play_text_scroll
|
|
|
|
roots: Query<Entity, With<ui::TextScrollAnimation>>,
|
|
|
|
// This simply marks each paragraph for play and (should) stay dormant while those animations are playing
|
|
|
|
texts: Query<Entity, (With<Text>, With<Intro>)>,
|
|
|
|
fn play_intro(
|
|
|
|
animated_texts: Query<&ui::TextScroll>,
|
|
|
|
parents: Query<Entity, (With<Node>, With<Intro>, With<TextScroll>)>,
|
|
|
|
|
|
|
|
mut texts: Query<(Entity, Option<&mut TextScroll>), (With<Text>, With<Intro>)>,
|
|
|
|
|
|
|
|
children: Query<&Children>,
|
|
|
|
children: Query<&Children>,
|
|
|
|
mut queues: Local<HashMap<Entity, VecDeque<Entity>>>,
|
|
|
|
time: Res<Time>,
|
|
|
|
mut commands: Commands,
|
|
|
|
|
|
|
|
tweaks_file: Res<tweak::GameTweaks>,
|
|
|
|
|
|
|
|
tweaks: Res<Assets<tweak::Tweaks>>,
|
|
|
|
|
|
|
|
framecount: Res<FrameCount>,
|
|
|
|
framecount: Res<FrameCount>,
|
|
|
|
|
|
|
|
mut curr: Local<Option<Entity>>,
|
|
|
|
|
|
|
|
mut commands: Commands,
|
|
|
|
) {
|
|
|
|
) {
|
|
|
|
info!("Play intro...");
|
|
|
|
roots.iter().for_each(|r| {
|
|
|
|
|
|
|
|
info!("Processing {:?} at frame {:?}", r, framecount.0);
|
|
|
|
// Iterate over all UI nodes which are marked for animation
|
|
|
|
|
|
|
|
parents.iter().for_each(|p| {
|
|
|
|
// Skip to next paragraph only the current paragraph's animation is complete
|
|
|
|
// If the hash does not contain this parent key, add the VecDeque of child entities
|
|
|
|
if animated_texts.is_empty() {
|
|
|
|
if !(*queues).contains_key(&p) {
|
|
|
|
info!(
|
|
|
|
(*queues).insert(p, texts.iter_many(children.iter_descendants(p)).map(|(e, _)| e).collect());
|
|
|
|
"No animations playing, moving on to next paragraph {:?}",
|
|
|
|
}
|
|
|
|
*curr
|
|
|
|
// Fetch the VecDeque so we can play with it
|
|
|
|
);
|
|
|
|
let queue = (*queues).get_mut(&p).unwrap();
|
|
|
|
|
|
|
|
|
|
|
|
// Create an iterator over all paragraphs
|
|
|
|
// Pop the front of the line, this is the next entity to animate
|
|
|
|
let mut paragraphs = texts.iter_many(children.iter_descendants(r));
|
|
|
|
if let Some(e) = queue.pop_front() {
|
|
|
|
if curr.is_some() {
|
|
|
|
if let Ok((e, opt_ts)) = texts.get_mut(e) {
|
|
|
|
// Hide the last active entity
|
|
|
|
// If this entity has a TextScroll already, update it
|
|
|
|
if let Some(e) = *curr {
|
|
|
|
if let Some(mut ts) = opt_ts {
|
|
|
|
commands.entity(e).insert(Visibility::Hidden);
|
|
|
|
// Increment the progress counter by some variable amount
|
|
|
|
|
|
|
|
let tweak = tweaks.get(tweaks_file.handle.clone()).expect("Load tweaks");
|
|
|
|
|
|
|
|
let delay = tweak.get::<f32>("intro_delay").expect("[intro] delay = float");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ts.progress = ((framecount.0 - ts.start) as f32 / delay) as usize;
|
|
|
|
|
|
|
|
// Otherwise, initialize it with one
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
// Insert a fresh TextScroll into this paragraph
|
|
|
|
|
|
|
|
commands.entity(e).insert(TextScroll { start: framecount.0, progress: 0 });
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Locate the last entity we were operating with
|
|
|
|
|
|
|
|
while let Some(entity) = paragraphs.next() {
|
|
|
|
|
|
|
|
if *curr == Some(entity) {
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
// Operate on the next entity in the list
|
|
|
|
|
|
|
|
*curr = paragraphs.next();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
info!("Curr: {:?}", *curr);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// TEMP: Just testing if the paragraph animation works
|
|
|
|
|
|
|
|
if let Some(e) = *curr {
|
|
|
|
|
|
|
|
commands.entity(e).insert(ui::TextScroll {
|
|
|
|
|
|
|
|
progress: 0,
|
|
|
|
|
|
|
|
start: time.elapsed_seconds(),
|
|
|
|
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
panic!("This shouldn't happen!");
|
|
|
|
commands.entity(r).remove::<ui::TextScrollAnimation>();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
|
|
|
|
// We have played all child entities, so remove the animate marker
|
|
|
|
|
|
|
|
commands.entity(p).remove::<TextScroll>();
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Upon entering the Intro state, start the intro "animation"
|
|
|
|
fn scroll_text(
|
|
|
|
fn play_text_scroll(
|
|
|
|
mut texts: Query<(Entity, &mut Visibility, &mut Text, &mut ui::TextScroll)>,
|
|
|
|
tweaks_file: Res<tweak::GameTweaks>,
|
|
|
|
tweaks_file: Res<tweak::GameTweaks>,
|
|
|
|
tweaks: Res<Assets<tweak::Tweaks>>,
|
|
|
|
tweaks: Res<Assets<tweak::Tweaks>>,
|
|
|
|
mut text: Query<(Entity, &mut Text, &mut Visibility, &ui::TextScroll)>,
|
|
|
|
time: Res<Time>,
|
|
|
|
|
|
|
|
keys: Res<Input<KeyCode>>,
|
|
|
|
mut commands: Commands,
|
|
|
|
mut commands: Commands,
|
|
|
|
) {
|
|
|
|
) {
|
|
|
|
info!("Play text scroll");
|
|
|
|
let tweak = tweaks.get(tweaks_file.handle.clone()).expect("Load tweaks");
|
|
|
|
|
|
|
|
texts
|
|
|
|
// Iterate over all (one) text
|
|
|
|
.iter_mut()
|
|
|
|
text.iter_mut().for_each(|(e, mut t, mut v, ui::TextScroll { progress, .. })| {
|
|
|
|
.for_each(|(entity, mut vis, mut text, mut text_scroll)| {
|
|
|
|
// If this is the first frame of the animation, make the object visibility
|
|
|
|
debug!("Updating {:?} {:?}", entity, text_scroll);
|
|
|
|
if *progress == 0 {
|
|
|
|
|
|
|
|
*v = Visibility::Inherited;
|
|
|
|
// If the animation just started, make visible
|
|
|
|
}
|
|
|
|
if text_scroll.progress == 0 {
|
|
|
|
|
|
|
|
*vis = Visibility::Inherited;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let tweak = tweaks.get(tweaks_file.handle.clone()).expect("Load tweaks");
|
|
|
|
// If user pressed enter, skip to end of paragraph
|
|
|
|
let text_visible_hex = tweak.get::<String>("intro_rgba_visible").unwrap();
|
|
|
|
text_scroll.progress = if keys.just_pressed(KeyCode::Return) {
|
|
|
|
let text_color = Color::hex(&text_visible_hex).unwrap();
|
|
|
|
usize::MAX
|
|
|
|
|
|
|
|
}
|
|
|
|
// Iterate over all characters in the intro text
|
|
|
|
// Otherwise, progress by some fixed increment
|
|
|
|
t.sections
|
|
|
|
else {
|
|
|
|
.iter_mut()
|
|
|
|
// Update animation progress for this paragrpah
|
|
|
|
.enumerate()
|
|
|
|
let delay = tweak.get::<f32>("intro_delay").unwrap();
|
|
|
|
// Only operate on sections up to this point
|
|
|
|
|
|
|
|
.filter_map(|(i, s)| (i <= *progress).then_some(s))
|
|
|
|
// Update text_scroll.progress based on frame count
|
|
|
|
// Set the alpha to 1.0 making it visible
|
|
|
|
((time.elapsed_seconds() - text_scroll.start) / delay) as usize
|
|
|
|
.for_each(|s| {
|
|
|
|
};
|
|
|
|
s.style.color = text_color.clone();
|
|
|
|
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Once the animation is done, remove the marker component
|
|
|
|
// If the entire paragraph is played out, remove the component
|
|
|
|
if t.sections.iter().all(|s| s.style.color == text_color) {
|
|
|
|
if text.sections.iter().all(|s| s.style.color == text_color) {
|
|
|
|
commands.entity(e).remove::<ui::TextScroll>();
|
|
|
|
commands.entity(entity).remove::<ui::TextScroll>();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Intro animation reveals one character every nth frame
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Hit enter to skip the "animation"
|
|
|
|
|