feat: Add savegame loading (#37)
This commit is contained in:
@@ -19,3 +19,4 @@ pub use pom::PomPlugin;
|
|||||||
pub use savegame::SavegamePlugin;
|
pub use savegame::SavegamePlugin;
|
||||||
pub use start_screen::StartScreenPlugin;
|
pub use start_screen::StartScreenPlugin;
|
||||||
pub use status::StatusPlugin;
|
pub use status::StatusPlugin;
|
||||||
|
pub use ui::UiPlugin;
|
||||||
|
|||||||
@@ -1,11 +1,30 @@
|
|||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
use std::fs;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
#[derive(Resource)]
|
#[derive(Resource, Clone, Debug)]
|
||||||
pub struct SavegamePath(pub PathBuf);
|
pub struct SavegamePath(pub PathBuf);
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct SavegameInfo {
|
||||||
|
pub path: SavegamePath,
|
||||||
|
pub index: u32,
|
||||||
|
pub total_berries: u32,
|
||||||
|
pub completed_focus: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct PartialSaveData {
|
||||||
|
session_tracker: PartialSessionTracker,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct PartialSessionTracker {
|
||||||
|
completed_focus_phases: u32,
|
||||||
|
}
|
||||||
|
|
||||||
impl SavegamePath {
|
impl SavegamePath {
|
||||||
pub fn new(name: &str) -> Self {
|
pub fn new(index: u32) -> Self {
|
||||||
let base_path = get_internal_path().unwrap_or_else(|| {
|
let base_path = get_internal_path().unwrap_or_else(|| {
|
||||||
println!(
|
println!(
|
||||||
"Could not determine platform-specific save directory. Falling back to `./saves/"
|
"Could not determine platform-specific save directory. Falling back to `./saves/"
|
||||||
@@ -17,6 +36,59 @@ impl SavegamePath {
|
|||||||
panic!("Failed to create save directory at {:?}: {}", base_path, e);
|
panic!("Failed to create save directory at {:?}: {}", base_path, e);
|
||||||
}
|
}
|
||||||
|
|
||||||
Self(base_path.join(name))
|
Self(base_path.join(format!("savegame-{}.json", index)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn list() -> Vec<SavegameInfo> {
|
||||||
|
let mut savegames = Vec::new();
|
||||||
|
|
||||||
|
let Some(base_path) = get_internal_path() else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
if !base_path.exists() {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
let Ok(entries) = fs::read_dir(base_path) else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
let path = entry.path();
|
||||||
|
|
||||||
|
if path.extension().and_then(|s| s.to_str()) != Some("json") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Some(file_name) = path.file_stem().and_then(|s| s.to_str()) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if !file_name.starts_with("savegame-") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Ok(index) = file_name.trim_start_matches("savegame-").parse::<u32>() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Ok(content) = fs::read_to_string(&path) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Ok(data) = serde_json::from_str::<PartialSaveData>(&content) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
savegames.push(SavegameInfo {
|
||||||
|
path: SavegamePath(path),
|
||||||
|
index,
|
||||||
|
total_berries: 0, // TODO: add total_berries
|
||||||
|
completed_focus: data.session_tracker.completed_focus_phases,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
savegames.sort_by_key(|s| s.index);
|
||||||
|
savegames
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next() -> Self {
|
||||||
|
let savegames = Self::list();
|
||||||
|
let next_index = savegames.last().map(|s| s.index + 1).unwrap_or(0);
|
||||||
|
Self::new(next_index)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ fn load_savegame(
|
|||||||
mut phase: ResMut<CurrentPhase>,
|
mut phase: ResMut<CurrentPhase>,
|
||||||
mut tracker: ResMut<SessionTracker>,
|
mut tracker: ResMut<SessionTracker>,
|
||||||
mut settings: ResMut<TimerSettings>,
|
mut settings: ResMut<TimerSettings>,
|
||||||
mut pom_query: Query<&mut GridPosition, With<Pom>>,
|
mut pom_query: Query<(&mut GridPosition, &mut Transform), With<Pom>>,
|
||||||
) {
|
) {
|
||||||
for _ in messages.read() {
|
for _ in messages.read() {
|
||||||
if let Ok(mut file) = File::open(&save_path.0) {
|
if let Ok(mut file) = File::open(&save_path.0) {
|
||||||
@@ -114,8 +114,15 @@ fn load_savegame(
|
|||||||
*tracker = save_data.session_tracker;
|
*tracker = save_data.session_tracker;
|
||||||
*settings = save_data.timer_settings;
|
*settings = save_data.timer_settings;
|
||||||
|
|
||||||
if let Ok(mut pom_pos) = pom_query.single_mut() {
|
if let Ok((mut pom_pos, mut pom_transform)) = pom_query.single_mut() {
|
||||||
*pom_pos = save_data.pom_position;
|
*pom_pos = save_data.pom_position;
|
||||||
|
pom_transform.translation = grid_to_world_coords(
|
||||||
|
save_data.pom_position.x,
|
||||||
|
save_data.pom_position.y,
|
||||||
|
Some(1.0),
|
||||||
|
save_data.grid_width,
|
||||||
|
save_data.grid_height,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
for x in 0..save_data.grid_width {
|
for x in 0..save_data.grid_width {
|
||||||
|
|||||||
16
src/features/start_screen/components.rs
Normal file
16
src/features/start_screen/components.rs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub enum RootMarker {
|
||||||
|
MainMenu,
|
||||||
|
PopupSavegameLoad,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub enum ButtonType {
|
||||||
|
LoadGame,
|
||||||
|
NewGame,
|
||||||
|
Settings,
|
||||||
|
PopupSavegameLoad { savegame_path: SavegamePath },
|
||||||
|
PopupClose,
|
||||||
|
}
|
||||||
5
src/features/start_screen/consts.rs
Normal file
5
src/features/start_screen/consts.rs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
pub const NORMAL_BUTTON: Color = Color::srgb(0.15, 0.15, 0.15);
|
||||||
|
pub const HOVERED_BUTTON: Color = Color::srgb(0.25, 0.25, 0.25);
|
||||||
|
pub const PRESSED_BUTTON: Color = Color::srgb(0.35, 0.75, 0.35);
|
||||||
@@ -1,4 +1,9 @@
|
|||||||
use crate::{features::savegame::messages::SavegameLoadMessage, prelude::*};
|
use crate::{features::savegame::messages::SavegameLoadMessage, prelude::*};
|
||||||
|
use components::*;
|
||||||
|
use consts::*;
|
||||||
|
|
||||||
|
pub mod components;
|
||||||
|
pub mod consts;
|
||||||
|
|
||||||
pub struct StartScreenPlugin;
|
pub struct StartScreenPlugin;
|
||||||
|
|
||||||
@@ -10,107 +15,198 @@ impl Plugin for StartScreenPlugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Resource)]
|
|
||||||
struct MenuData {
|
|
||||||
button_entity: Entity,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Component)]
|
|
||||||
enum ButtonType {
|
|
||||||
LoadGame,
|
|
||||||
NewGame,
|
|
||||||
Settings,
|
|
||||||
}
|
|
||||||
|
|
||||||
const NORMAL_BUTTON: Color = Color::srgb(0.15, 0.15, 0.15);
|
|
||||||
const HOVERED_BUTTON: Color = Color::srgb(0.25, 0.25, 0.25);
|
|
||||||
const PRESSED_BUTTON: Color = Color::srgb(0.35, 0.75, 0.35);
|
|
||||||
|
|
||||||
fn setup(mut commands: Commands) {
|
fn setup(mut commands: Commands) {
|
||||||
let button_entity = commands
|
commands.spawn((
|
||||||
|
RootMarker::MainMenu,
|
||||||
|
Node {
|
||||||
|
width: percent(100),
|
||||||
|
height: percent(100),
|
||||||
|
justify_content: JustifyContent::Center,
|
||||||
|
align_items: AlignItems::Center,
|
||||||
|
flex_direction: FlexDirection::Column,
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
children![
|
||||||
|
(
|
||||||
|
Text::new("Pomonon Garten"),
|
||||||
|
TextFont::from_font_size(64.0),
|
||||||
|
TextColor(Color::srgb(0.9, 0.9, 0.9))
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Button,
|
||||||
|
ButtonType::LoadGame,
|
||||||
|
Node {
|
||||||
|
width: px(300),
|
||||||
|
height: px(65),
|
||||||
|
justify_content: JustifyContent::Center,
|
||||||
|
align_items: AlignItems::Center,
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
BackgroundColor(NORMAL_BUTTON),
|
||||||
|
children![(
|
||||||
|
Text::new("Spiel laden"),
|
||||||
|
TextFont::from_font_size(33.0),
|
||||||
|
TextColor(Color::srgb(0.9, 0.9, 0.9))
|
||||||
|
)]
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Button,
|
||||||
|
ButtonType::NewGame,
|
||||||
|
Node {
|
||||||
|
width: px(300),
|
||||||
|
height: px(65),
|
||||||
|
justify_content: JustifyContent::Center,
|
||||||
|
align_items: AlignItems::Center,
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
BackgroundColor(NORMAL_BUTTON),
|
||||||
|
children![(
|
||||||
|
Text::new("Neues Spiel"),
|
||||||
|
TextFont::from_font_size(33.0),
|
||||||
|
TextColor(Color::srgb(0.9, 0.9, 0.9))
|
||||||
|
)]
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Button,
|
||||||
|
ButtonType::Settings,
|
||||||
|
Node {
|
||||||
|
width: px(300),
|
||||||
|
height: px(65),
|
||||||
|
justify_content: JustifyContent::Center,
|
||||||
|
align_items: AlignItems::Center,
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
BackgroundColor(NORMAL_BUTTON),
|
||||||
|
children![(
|
||||||
|
Text::new("Einstellungen"),
|
||||||
|
TextFont::from_font_size(33.0),
|
||||||
|
TextColor(Color::srgb(0.9, 0.9, 0.9))
|
||||||
|
)]
|
||||||
|
)
|
||||||
|
],
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spawn_load_popup(commands: &mut Commands) {
|
||||||
|
commands
|
||||||
.spawn((
|
.spawn((
|
||||||
|
RootMarker::PopupSavegameLoad,
|
||||||
Node {
|
Node {
|
||||||
|
position_type: PositionType::Absolute,
|
||||||
width: percent(100),
|
width: percent(100),
|
||||||
height: percent(100),
|
height: percent(100),
|
||||||
justify_content: JustifyContent::Center,
|
justify_content: JustifyContent::Center,
|
||||||
align_items: AlignItems::Center,
|
align_items: AlignItems::Center,
|
||||||
flex_direction: FlexDirection::Column,
|
|
||||||
..default()
|
..default()
|
||||||
},
|
},
|
||||||
children![
|
ZIndex(1),
|
||||||
(
|
BackgroundColor(Color::srgba(0.0, 0.0, 0.0, 0.8)),
|
||||||
Text::new("Pomonon Garten"),
|
|
||||||
TextFont {
|
|
||||||
font_size: 64.0,
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
TextColor(Color::srgb(0.9, 0.9, 0.9))
|
|
||||||
),
|
|
||||||
(
|
|
||||||
Button,
|
|
||||||
ButtonType::LoadGame,
|
|
||||||
Node {
|
|
||||||
width: px(300),
|
|
||||||
height: px(65),
|
|
||||||
justify_content: JustifyContent::Center,
|
|
||||||
align_items: AlignItems::Center,
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
BackgroundColor(NORMAL_BUTTON),
|
|
||||||
children![(
|
|
||||||
Text::new("Spiel laden"),
|
|
||||||
TextFont {
|
|
||||||
font_size: 33.0,
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
TextColor(Color::srgb(0.9, 0.9, 0.9))
|
|
||||||
)]
|
|
||||||
),
|
|
||||||
(
|
|
||||||
Button,
|
|
||||||
ButtonType::NewGame,
|
|
||||||
Node {
|
|
||||||
width: px(300),
|
|
||||||
height: px(65),
|
|
||||||
justify_content: JustifyContent::Center,
|
|
||||||
align_items: AlignItems::Center,
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
BackgroundColor(NORMAL_BUTTON),
|
|
||||||
children![(
|
|
||||||
Text::new("Neues Spiel"),
|
|
||||||
TextFont {
|
|
||||||
font_size: 33.0,
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
TextColor(Color::srgb(0.9, 0.9, 0.9))
|
|
||||||
)]
|
|
||||||
),
|
|
||||||
(
|
|
||||||
Button,
|
|
||||||
ButtonType::Settings,
|
|
||||||
Node {
|
|
||||||
width: px(300),
|
|
||||||
height: px(65),
|
|
||||||
justify_content: JustifyContent::Center,
|
|
||||||
align_items: AlignItems::Center,
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
BackgroundColor(NORMAL_BUTTON),
|
|
||||||
children![(
|
|
||||||
Text::new("Einstellungen"),
|
|
||||||
TextFont {
|
|
||||||
font_size: 33.0,
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
TextColor(Color::srgb(0.9, 0.9, 0.9))
|
|
||||||
)]
|
|
||||||
)
|
|
||||||
],
|
|
||||||
))
|
))
|
||||||
.id();
|
.with_children(|parent| {
|
||||||
|
parent
|
||||||
|
.spawn((
|
||||||
|
Node {
|
||||||
|
width: px(600.0),
|
||||||
|
height: px(500.0),
|
||||||
|
flex_direction: FlexDirection::Column,
|
||||||
|
align_items: AlignItems::Center,
|
||||||
|
padding: UiRect::all(px(20.0)),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
BackgroundColor(Color::srgb(0.2, 0.2, 0.2)),
|
||||||
|
BorderRadius::all(Val::Px(10.0)),
|
||||||
|
))
|
||||||
|
.with_children(|parent| {
|
||||||
|
parent.spawn((
|
||||||
|
Node {
|
||||||
|
width: percent(100.0),
|
||||||
|
justify_content: JustifyContent::SpaceBetween,
|
||||||
|
align_items: AlignItems::Center,
|
||||||
|
margin: UiRect::bottom(px(20.0)),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
children![
|
||||||
|
(
|
||||||
|
Text::new("Spielstand Auswahl"),
|
||||||
|
TextFont::from_font_size(40.0),
|
||||||
|
TextColor(Color::WHITE),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Button,
|
||||||
|
ButtonType::PopupClose,
|
||||||
|
Node {
|
||||||
|
width: px(40.0),
|
||||||
|
height: px(40.0),
|
||||||
|
justify_content: JustifyContent::Center,
|
||||||
|
align_items: AlignItems::Center,
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
BackgroundColor(Color::srgb(0.8, 0.2, 0.2)),
|
||||||
|
children![(
|
||||||
|
Text::new("X"),
|
||||||
|
TextFont::from_font_size(24.0),
|
||||||
|
TextColor(Color::WHITE),
|
||||||
|
)]
|
||||||
|
)
|
||||||
|
],
|
||||||
|
));
|
||||||
|
|
||||||
commands.insert_resource(MenuData { button_entity });
|
parent
|
||||||
|
.spawn(Node {
|
||||||
|
width: percent(100),
|
||||||
|
flex_direction: FlexDirection::Column,
|
||||||
|
overflow: Overflow::scroll_y(),
|
||||||
|
margin: UiRect::all(px(20.0)),
|
||||||
|
row_gap: px(10.0),
|
||||||
|
..default()
|
||||||
|
})
|
||||||
|
.with_children(|parent| {
|
||||||
|
for savegame in SavegamePath::list() {
|
||||||
|
parent.spawn((
|
||||||
|
Button,
|
||||||
|
ButtonType::PopupSavegameLoad {
|
||||||
|
savegame_path: savegame.path.clone(),
|
||||||
|
},
|
||||||
|
Node {
|
||||||
|
width: percent(100),
|
||||||
|
height: px(80),
|
||||||
|
justify_content: JustifyContent::Center,
|
||||||
|
align_items: AlignItems::Center,
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
BackgroundColor(NORMAL_BUTTON),
|
||||||
|
children![(
|
||||||
|
Node {
|
||||||
|
width: percent(100),
|
||||||
|
height: percent(100),
|
||||||
|
flex_direction: FlexDirection::Column,
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
children![
|
||||||
|
(
|
||||||
|
Text::new(format!(
|
||||||
|
"Spielstand {}",
|
||||||
|
savegame.index + 1
|
||||||
|
)),
|
||||||
|
TextFont::from_font_size(24.0),
|
||||||
|
TextColor(Color::srgb(0.9, 0.9, 0.9))
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Text::new(format!(
|
||||||
|
"Beeren: {}, Fokusphasen abgeschlossen: {}",
|
||||||
|
savegame.total_berries,
|
||||||
|
savegame.completed_focus
|
||||||
|
)),
|
||||||
|
TextFont::from_font_size(18.0),
|
||||||
|
TextColor(Color::srgb(0.9, 0.9, 0.9))
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)],
|
||||||
|
));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn menu(
|
fn menu(
|
||||||
@@ -120,7 +216,8 @@ fn menu(
|
|||||||
(&Interaction, &ButtonType, &mut BackgroundColor),
|
(&Interaction, &ButtonType, &mut BackgroundColor),
|
||||||
(Changed<Interaction>, With<Button>),
|
(Changed<Interaction>, With<Button>),
|
||||||
>,
|
>,
|
||||||
mut load_messages: MessageWriter<SavegameLoadMessage>,
|
root_query: Query<(Entity, &RootMarker)>,
|
||||||
|
mut savegame_messages: MessageWriter<SavegameLoadMessage>,
|
||||||
) {
|
) {
|
||||||
for (interaction, button_type, mut color) in &mut interaction_query {
|
for (interaction, button_type, mut color) in &mut interaction_query {
|
||||||
match *interaction {
|
match *interaction {
|
||||||
@@ -129,14 +226,25 @@ fn menu(
|
|||||||
|
|
||||||
match button_type {
|
match button_type {
|
||||||
ButtonType::LoadGame => {
|
ButtonType::LoadGame => {
|
||||||
commands.insert_resource(SavegamePath::new("savegame.json"));
|
spawn_load_popup(&mut commands);
|
||||||
next_state.set(AppState::GameScreen);
|
|
||||||
load_messages.write(SavegameLoadMessage);
|
|
||||||
}
|
}
|
||||||
ButtonType::NewGame => {
|
ButtonType::NewGame => {
|
||||||
commands.insert_resource(SavegamePath::new("savegame.json"));
|
commands.insert_resource(SavegamePath::next());
|
||||||
next_state.set(AppState::GameScreen);
|
next_state.set(AppState::GameScreen);
|
||||||
}
|
}
|
||||||
|
ButtonType::PopupClose => {
|
||||||
|
for (entity, root) in root_query.iter() {
|
||||||
|
match *root {
|
||||||
|
RootMarker::PopupSavegameLoad => commands.entity(entity).despawn(),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ButtonType::PopupSavegameLoad { savegame_path } => {
|
||||||
|
commands.insert_resource(savegame_path.clone());
|
||||||
|
next_state.set(AppState::GameScreen);
|
||||||
|
savegame_messages.write(SavegameLoadMessage);
|
||||||
|
}
|
||||||
_ => (),
|
_ => (),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -150,6 +258,8 @@ fn menu(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cleanup(mut commands: Commands, menu_data: Res<MenuData>) {
|
fn cleanup(mut commands: Commands, query: Query<Entity, With<RootMarker>>) {
|
||||||
commands.entity(menu_data.button_entity).despawn();
|
for entity in query.iter() {
|
||||||
|
commands.entity(entity).despawn();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
#[derive(EntityEvent, Debug)]
|
||||||
|
#[entity_event(propagate, auto_propagate)]
|
||||||
|
pub struct Scroll {
|
||||||
|
pub entity: Entity,
|
||||||
|
pub delta: Vec2,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Component)]
|
#[derive(Component)]
|
||||||
pub struct UiStatusRootContainer;
|
pub struct UiStatusRootContainer;
|
||||||
|
|
||||||
|
|||||||
1
src/features/ui/consts.rs
Normal file
1
src/features/ui/consts.rs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
pub const LINE_HEIGHT: f32 = 21.0;
|
||||||
@@ -1 +1,86 @@
|
|||||||
|
use crate::prelude::*;
|
||||||
|
use bevy::{input::mouse::*, picking::hover::HoverMap};
|
||||||
|
|
||||||
pub mod components;
|
pub mod components;
|
||||||
|
pub mod consts;
|
||||||
|
|
||||||
|
pub struct UiPlugin;
|
||||||
|
|
||||||
|
impl Plugin for UiPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.add_systems(Update, scroll_events);
|
||||||
|
app.add_observer(on_scroll_handler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scroll_events(
|
||||||
|
mut mouse_wheel_reader: MessageReader<MouseWheel>,
|
||||||
|
hover_map: Res<HoverMap>,
|
||||||
|
keyboard_input: Res<ButtonInput<KeyCode>>,
|
||||||
|
mut commands: Commands,
|
||||||
|
) {
|
||||||
|
for mouse_wheel in mouse_wheel_reader.read() {
|
||||||
|
let mut delta = -Vec2::new(mouse_wheel.x, mouse_wheel.y);
|
||||||
|
|
||||||
|
if mouse_wheel.unit == MouseScrollUnit::Line {
|
||||||
|
delta *= LINE_HEIGHT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if keyboard_input.any_pressed([KeyCode::ControlLeft, KeyCode::ControlRight]) {
|
||||||
|
std::mem::swap(&mut delta.x, &mut delta.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
for pointer_map in hover_map.values() {
|
||||||
|
for entity in pointer_map.keys().copied() {
|
||||||
|
commands.trigger(components::Scroll { entity, delta });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_scroll_handler(
|
||||||
|
mut scroll: On<components::Scroll>,
|
||||||
|
mut query: Query<(&mut ScrollPosition, &Node, &ComputedNode)>,
|
||||||
|
) {
|
||||||
|
let Ok((mut scroll_position, node, computed)) = query.get_mut(scroll.entity) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let max_offset = (computed.content_size() - computed.size()) * computed.inverse_scale_factor();
|
||||||
|
|
||||||
|
let delta = &mut scroll.delta;
|
||||||
|
if node.overflow.x == OverflowAxis::Scroll && delta.x != 0. {
|
||||||
|
// Is this node already scrolled all the way in the direction of the scroll?
|
||||||
|
let max = if delta.x > 0. {
|
||||||
|
scroll_position.x >= max_offset.x
|
||||||
|
} else {
|
||||||
|
scroll_position.x <= 0.
|
||||||
|
};
|
||||||
|
|
||||||
|
if !max {
|
||||||
|
scroll_position.x += delta.x;
|
||||||
|
// Consume the X portion of the scroll delta.
|
||||||
|
delta.x = 0.;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if node.overflow.y == OverflowAxis::Scroll && delta.y != 0. {
|
||||||
|
// Is this node already scrolled all the way in the direction of the scroll?
|
||||||
|
let max = if delta.y > 0. {
|
||||||
|
scroll_position.y >= max_offset.y
|
||||||
|
} else {
|
||||||
|
scroll_position.y <= 0.
|
||||||
|
};
|
||||||
|
|
||||||
|
if !max {
|
||||||
|
scroll_position.y += delta.y;
|
||||||
|
// Consume the Y portion of the scroll delta.
|
||||||
|
delta.y = 0.;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop propagating when the delta is fully consumed.
|
||||||
|
if *delta == Vec2::ZERO {
|
||||||
|
scroll.propagate(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ fn main() {
|
|||||||
features::PhasePlugin,
|
features::PhasePlugin,
|
||||||
features::StatusPlugin,
|
features::StatusPlugin,
|
||||||
features::SavegamePlugin,
|
features::SavegamePlugin,
|
||||||
|
features::UiPlugin,
|
||||||
))
|
))
|
||||||
.insert_resource(config)
|
.insert_resource(config)
|
||||||
.run();
|
.run();
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ pub use crate::features::{
|
|||||||
messages::{InteractStartMessage, MoveMessage},
|
messages::{InteractStartMessage, MoveMessage},
|
||||||
},
|
},
|
||||||
savegame::components::SavegamePath,
|
savegame::components::SavegamePath,
|
||||||
ui::components::*,
|
ui::{components::*, consts::*},
|
||||||
};
|
};
|
||||||
pub use crate::utils::path::get_internal_path;
|
pub use crate::utils::path::get_internal_path;
|
||||||
pub use bevy::prelude::*;
|
pub use bevy::prelude::*;
|
||||||
|
|||||||
Reference in New Issue
Block a user