Initial work on menus
This commit is contained in:
Binary file not shown.
@@ -0,0 +1,47 @@
|
|||||||
|
(
|
||||||
|
button_styles: {
|
||||||
|
MainMenu: (
|
||||||
|
focus_outline_width_px: 4,
|
||||||
|
focus_outline_offset_px: 2,
|
||||||
|
focus_outline_color: Srgba((red: 1.0, green: 1.0, blue: 0.0, alpha: 1.0)),
|
||||||
|
normal: (
|
||||||
|
background_color: Srgba((red: 1.0, green: 0.0, blue: 0.0, alpha: 1.0)),
|
||||||
|
border_color: Srgba((red: 1.0, green: 0.0, blue: 0.0, alpha: 1.0)),
|
||||||
|
text_color: Srgba((red: 1.0, green: 0.0, blue: 0.0, alpha: 1.0)),
|
||||||
|
box_shadow: (),
|
||||||
|
),
|
||||||
|
press: (
|
||||||
|
background_color: Srgba((red: 1.0, green: 0.0, blue: 0.0, alpha: 1.0)),
|
||||||
|
border_color: Srgba((red: 1.0, green: 0.0, blue: 0.0, alpha: 1.0)),
|
||||||
|
text_color: Srgba((red: 1.0, green: 0.0, blue: 0.0, alpha: 1.0)),
|
||||||
|
box_shadow: (),
|
||||||
|
),
|
||||||
|
hover: (
|
||||||
|
background_color: Srgba((red: 0.0, green: 1.0, blue: 0.0, alpha: 1.0)),
|
||||||
|
border_color: Srgba((red: 1.0, green: 0.0, blue: 0.0, alpha: 1.0)),
|
||||||
|
text_color: Srgba((red: 1.0, green: 0.0, blue: 0.0, alpha: 1.0)),
|
||||||
|
box_shadow: (),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
CharacterSelectCharacter: (
|
||||||
|
focus_outline_width_px: 4,
|
||||||
|
focus_outline_offset_px: 2,
|
||||||
|
focus_outline_color: Srgba((red: 1.0, green: 1.0, blue: 0.0, alpha: 1.0)),
|
||||||
|
normal: (
|
||||||
|
background_color: Srgba((red: 1.0, green: 0.0, blue: 0.0, alpha: 1.0)),
|
||||||
|
text_color: Srgba((red: 1.0, green: 0.0, blue: 0.0, alpha: 1.0)),
|
||||||
|
box_shadow: (),
|
||||||
|
),
|
||||||
|
press: (
|
||||||
|
background_color: Srgba((red: 1.0, green: 0.0, blue: 0.0, alpha: 1.0)),
|
||||||
|
text_color: Srgba((red: 1.0, green: 0.0, blue: 0.0, alpha: 1.0)),
|
||||||
|
box_shadow: (),
|
||||||
|
),
|
||||||
|
hover: (
|
||||||
|
background_color: Srgba((red: 0.0, green: 1.0, blue: 0.0, alpha: 1.0)),
|
||||||
|
text_color: Srgba((red: 1.0, green: 0.0, blue: 0.0, alpha: 1.0)),
|
||||||
|
box_shadow: (),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
+4
-2
@@ -11,7 +11,7 @@ use bevy_ggrs::RollbackFrameRate;
|
|||||||
use enum_cycling::EnumCycle;
|
use enum_cycling::EnumCycle;
|
||||||
use enum_cycling_derive::EnumCycle;
|
use enum_cycling_derive::EnumCycle;
|
||||||
|
|
||||||
use crate::{player::Player, replay::StopReplayRecording};
|
use crate::{player::Player, replay::StopReplayRecording, AppState};
|
||||||
|
|
||||||
pub const PLAYER_DEBUG_ROW_COUNT: usize = 6;
|
pub const PLAYER_DEBUG_ROW_COUNT: usize = 6;
|
||||||
|
|
||||||
@@ -365,7 +365,9 @@ impl Plugin for DebugToolsPlugin {
|
|||||||
app.init_resource::<DebugSettings>()
|
app.init_resource::<DebugSettings>()
|
||||||
.add_systems(
|
.add_systems(
|
||||||
Update,
|
Update,
|
||||||
(handle_palette_input, sync_gizmo_depth_bias).chain(),
|
(handle_palette_input, sync_gizmo_depth_bias)
|
||||||
|
.chain()
|
||||||
|
.run_if(in_state(AppState::InMatch)),
|
||||||
)
|
)
|
||||||
.add_systems(EguiPrimaryContextPass, draw_palette);
|
.add_systems(EguiPrimaryContextPass, draw_palette);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,8 +73,8 @@ impl AssetCollection for FighterManifestAssets {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Resource)]
|
#[derive(Resource, Deref)]
|
||||||
pub struct FighterManifestRegistry(HashMap<FighterId, Handle<FighterManifest>>);
|
pub struct FighterManifestRegistry(pub HashMap<FighterId, Handle<FighterManifest>>);
|
||||||
|
|
||||||
impl FighterManifestRegistry {
|
impl FighterManifestRegistry {
|
||||||
pub fn get(&self, fighter: FighterId) -> Option<&Handle<FighterManifest>> {
|
pub fn get(&self, fighter: FighterId) -> Option<&Handle<FighterManifest>> {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use bevy::prelude::*;
|
|||||||
use bevy_asset_loader::asset_collection::AssetCollection;
|
use bevy_asset_loader::asset_collection::AssetCollection;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::math::int::FGi32;
|
use crate::{math::int::FGi32, menus::style::FGUiStyle};
|
||||||
|
|
||||||
#[derive(Reflect, Serialize, Deserialize, Clone)]
|
#[derive(Reflect, Serialize, Deserialize, Clone)]
|
||||||
#[reflect(opaque)]
|
#[reflect(opaque)]
|
||||||
@@ -34,7 +34,9 @@ pub struct GameSettings {
|
|||||||
#[derive(AssetCollection, Resource)]
|
#[derive(AssetCollection, Resource)]
|
||||||
pub struct CommonAssets {
|
pub struct CommonAssets {
|
||||||
#[asset(path = "game_settings.ron")]
|
#[asset(path = "game_settings.ron")]
|
||||||
game_settings: Handle<GameSettings>,
|
pub game_settings: Handle<GameSettings>,
|
||||||
|
#[asset(path = "game.stylesheet.ron")]
|
||||||
|
pub ui_style: Handle<FGUiStyle>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromWorld for GameSettings {
|
impl FromWorld for GameSettings {
|
||||||
|
|||||||
+36
-7
@@ -21,6 +21,27 @@ pub use map::{
|
|||||||
KeyboardInputMapElement,
|
KeyboardInputMapElement,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub enum LocalInputSource {
|
||||||
|
Keyboard,
|
||||||
|
Gamepad(Entity),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Maps an input source to a player index
|
||||||
|
#[derive(Resource, Clone, Debug, Default, Deref, DerefMut)]
|
||||||
|
pub struct LocalInputAssignments(pub Vec<(usize, LocalInputSource)>);
|
||||||
|
|
||||||
|
impl LocalInputAssignments {
|
||||||
|
pub fn get_free_player_index(&self) -> Option<usize> {
|
||||||
|
for i in 0..16 {
|
||||||
|
if let None = self.iter().find(|(idx, _)| *idx == i) {
|
||||||
|
return Some(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
use crate::game_settings::GameSettings;
|
use crate::game_settings::GameSettings;
|
||||||
use crate::math::int::FGi32;
|
use crate::math::int::FGi32;
|
||||||
use crate::netcode::GGRSCfg;
|
use crate::netcode::GGRSCfg;
|
||||||
@@ -102,6 +123,7 @@ impl Plugin for FighterInputPlugin {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
.init_resource::<LocalInputAssignments>()
|
||||||
.add_plugins(gamecube::GamecubeAdapterPlugin)
|
.add_plugins(gamecube::GamecubeAdapterPlugin)
|
||||||
.add_systems(ReadInputs, read_local_inputs)
|
.add_systems(ReadInputs, read_local_inputs)
|
||||||
.add_systems(
|
.add_systems(
|
||||||
@@ -128,22 +150,29 @@ fn read_local_inputs(
|
|||||||
input_map: Res<BaseInputMap>,
|
input_map: Res<BaseInputMap>,
|
||||||
gc_ports: Res<gamecube::GcPorts>,
|
gc_ports: Res<gamecube::GcPorts>,
|
||||||
gamepads: Query<&Gamepad>,
|
gamepads: Query<&Gamepad>,
|
||||||
|
assignments: Res<LocalInputAssignments>,
|
||||||
local_players: Res<LocalPlayers>,
|
local_players: Res<LocalPlayers>,
|
||||||
game_settings: Res<GameSettings>
|
game_settings: Res<GameSettings>
|
||||||
) {
|
) {
|
||||||
let mut local_inputs = HashMap::new();
|
let mut local_inputs = HashMap::new();
|
||||||
|
|
||||||
for handle in &local_players.0 {
|
for handle in &local_players.0 {
|
||||||
let gamepad = gc_ports
|
let assigned = assignments.0.iter().find(|(player_handle, _)| player_handle == handle);
|
||||||
.entities
|
let gamepad = match assigned {
|
||||||
.get(*handle)
|
Some((_, LocalInputSource::Gamepad(entity))) => gamepads.get(*entity).ok(),
|
||||||
.copied()
|
Some((_, LocalInputSource::Keyboard)) => None,
|
||||||
.flatten()
|
None => gc_ports.entities.get(*handle).copied().flatten().and_then(|entity| gamepads.get(entity).ok()),
|
||||||
.and_then(|entity| gamepads.get(entity).ok());
|
};
|
||||||
|
|
||||||
let mut input_frame = match gamepad {
|
let mut input_frame = match assigned {
|
||||||
|
Some((_, LocalInputSource::Keyboard)) => keyboard::sample_keyboard(&key, &input_map),
|
||||||
|
Some((_, LocalInputSource::Gamepad(_))) => gamepad
|
||||||
|
.map(|pad| gamepad::sample_gamepad(pad, &input_map))
|
||||||
|
.unwrap_or_default(),
|
||||||
|
None => match gamepad {
|
||||||
Some(pad) => gamepad::sample_gamepad(pad, &input_map),
|
Some(pad) => gamepad::sample_gamepad(pad, &input_map),
|
||||||
None => keyboard::sample_keyboard(&key, &input_map),
|
None => keyboard::sample_keyboard(&key, &input_map),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// preprocess frame
|
// preprocess frame
|
||||||
|
|||||||
+6
-27
@@ -48,6 +48,7 @@ pub mod replay;
|
|||||||
pub mod schedule;
|
pub mod schedule;
|
||||||
pub mod scripting;
|
pub mod scripting;
|
||||||
pub mod stage;
|
pub mod stage;
|
||||||
|
pub mod menus;
|
||||||
|
|
||||||
#[derive(States, Debug, Clone, Copy, Eq, PartialEq, Hash, Default)]
|
#[derive(States, Debug, Clone, Copy, Eq, PartialEq, Hash, Default)]
|
||||||
pub enum AppState {
|
pub enum AppState {
|
||||||
@@ -59,7 +60,9 @@ pub enum AppState {
|
|||||||
LoadStageManifests,
|
LoadStageManifests,
|
||||||
PrepareStageManifests,
|
PrepareStageManifests,
|
||||||
CommonAssetLoadFailed,
|
CommonAssetLoadFailed,
|
||||||
Idle,
|
MainMenu,
|
||||||
|
CharacterSelect,
|
||||||
|
StageSelect,
|
||||||
LoadingMatch,
|
LoadingMatch,
|
||||||
PreparingMatch,
|
PreparingMatch,
|
||||||
InMatch,
|
InMatch,
|
||||||
@@ -96,6 +99,7 @@ impl Plugin for GamePlugin {
|
|||||||
bevy_common_assets::ron::RonAssetPlugin::<StageManifest>::new(&["stage.ron"]),
|
bevy_common_assets::ron::RonAssetPlugin::<StageManifest>::new(&["stage.ron"]),
|
||||||
bevy_common_assets::ron::RonAssetPlugin::<GameSettings>::new(&["ron"]),
|
bevy_common_assets::ron::RonAssetPlugin::<GameSettings>::new(&["ron"]),
|
||||||
))
|
))
|
||||||
|
.add_plugins(menus::MenuPlugin)
|
||||||
.add_loading_state(
|
.add_loading_state(
|
||||||
LoadingState::new(AppState::LoadingMatch)
|
LoadingState::new(AppState::LoadingMatch)
|
||||||
.continue_to_state(AppState::PreparingMatch)
|
.continue_to_state(AppState::PreparingMatch)
|
||||||
@@ -134,7 +138,7 @@ impl Plugin for GamePlugin {
|
|||||||
stage::manifest::prepare_stage_manifests,
|
stage::manifest::prepare_stage_manifests,
|
||||||
)
|
)
|
||||||
.add_systems(
|
.add_systems(
|
||||||
OnEnter(AppState::Idle),
|
OnEnter(AppState::MainMenu),
|
||||||
match_loading::initiate_default_match,
|
match_loading::initiate_default_match,
|
||||||
)
|
)
|
||||||
.add_systems(
|
.add_systems(
|
||||||
@@ -142,7 +146,6 @@ impl Plugin for GamePlugin {
|
|||||||
match_loading::prepare_match,
|
match_loading::prepare_match,
|
||||||
)
|
)
|
||||||
.add_systems(OnExit(AppState::InMatch), match_loading::cleanup_match)
|
.add_systems(OnExit(AppState::InMatch), match_loading::cleanup_match)
|
||||||
.add_systems(Update, spin_cubes)
|
|
||||||
.insert_resource(args::Args::parse());
|
.insert_resource(args::Args::parse());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -156,27 +159,3 @@ fn wait_for_asset_processor(
|
|||||||
next_state.set(AppState::LoadCommonAssets);
|
next_state.set(AppState::LoadCommonAssets);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Spin-rate in radians per second. Attach it in the inspector; the
|
|
||||||
/// editor sees this component with no registration code because Bevy's
|
|
||||||
/// `reflect_auto_register` picks up the `Reflect` derive. `Default` +
|
|
||||||
/// `#[reflect(Default)]` let the editor add it and the game reconstruct
|
|
||||||
/// it with sensible starting values.
|
|
||||||
#[derive(Component, Reflect, Default)]
|
|
||||||
#[reflect(Component, Default, @EditorCategory::new("Actor"))]
|
|
||||||
pub struct SpinningCube {
|
|
||||||
pub speed: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn spin_cubes(time: Res<Time>, mut cubes: Query<(&SpinningCube, &mut Transform)>) {
|
|
||||||
let dt = time.delta_secs();
|
|
||||||
for (cube, mut transform) in &mut cubes {
|
|
||||||
transform.rotate_y(cube.speed * dt);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Component, Reflect, Default)]
|
|
||||||
#[reflect(Component, Default, @EditorCategory::new("Actor"))]
|
|
||||||
pub struct TestCube {
|
|
||||||
pub frog: f32,
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ fn main() -> AppExit {
|
|||||||
App::new()
|
App::new()
|
||||||
.add_plugins(default_plugins)
|
.add_plugins(default_plugins)
|
||||||
.add_plugins(avian3d::prelude::PhysicsPlugins::default())
|
.add_plugins(avian3d::prelude::PhysicsPlugins::default())
|
||||||
.add_plugins(jackdaw_runtime::JackdawPlugin)
|
|
||||||
.add_plugins(game_test::GamePlugin)
|
.add_plugins(game_test::GamePlugin)
|
||||||
.add_plugins(FreeCameraPlugin)
|
.add_plugins(FreeCameraPlugin)
|
||||||
.add_plugins(EguiPlugin::default())
|
.add_plugins(EguiPlugin::default())
|
||||||
|
|||||||
@@ -169,6 +169,9 @@ pub fn initiate_default_match(
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if !args.synctest {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let players = (0..args.players)
|
let players = (0..args.players)
|
||||||
.map(|handle| MatchPlayer {
|
.map(|handle| MatchPlayer {
|
||||||
handle,
|
handle,
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
use bevy::{input_focus::InputFocus, prelude::*, text::FontSourceTemplate, ui::auto_directional_navigation::AutoDirectionalNavigation};
|
||||||
|
|
||||||
|
use crate::{game_settings::CommonAssets, menus::style::{FGUiButtonType, FGUiStyle}};
|
||||||
|
|
||||||
|
#[derive(Component, Default, Clone)]
|
||||||
|
#[require(Button)]
|
||||||
|
pub struct FGUiButton {
|
||||||
|
pub button_type: FGUiButtonType
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn menu_button(label: &str) -> impl Scene {
|
||||||
|
bsn! {
|
||||||
|
FGUiButton
|
||||||
|
Node {
|
||||||
|
width: px(150),
|
||||||
|
height: px(65),
|
||||||
|
border: px(5),
|
||||||
|
justify_content: JustifyContent::Center,
|
||||||
|
align_items: AlignItems::Center,
|
||||||
|
margin: UiRect { left: px(0), right: px(0), top: px(20), bottom: px(20) }
|
||||||
|
}
|
||||||
|
BoxShadow::new(Color::WHITE, px(0.0), px(0.0), px(10.0), px(10.0))
|
||||||
|
BorderColor::from(Color::BLACK)
|
||||||
|
BackgroundColor(Color::srgb(0.15, 0.15, 0.15))
|
||||||
|
AutoDirectionalNavigation::default()
|
||||||
|
Children [(
|
||||||
|
Text(label)
|
||||||
|
TextFont {
|
||||||
|
font: FontSourceTemplate::Handle("fonts/roboto.ttf"),
|
||||||
|
font_size: px(super::style::H3_SIZE),
|
||||||
|
}
|
||||||
|
TextColor(Color::srgb(0.9, 0.9, 0.9))
|
||||||
|
TextShadow
|
||||||
|
),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn button_setup(query: Query<(Entity, &FGUiButton, &mut Node), Added<FGUiButton>>, common_assets: Res<CommonAssets>, styles: Res<Assets<FGUiStyle>>, mut commands: Commands) {
|
||||||
|
let styles = styles.get(&common_assets.ui_style).expect("UI styles should be loaded");
|
||||||
|
for (entity, button, mut node) in query {
|
||||||
|
styles.button_styles[&button.button_type].normal.apply(&mut commands, entity, &mut node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn button_style_system(query: Query<(Entity, &FGUiButton, &Interaction, &mut Node), Changed<Interaction>>, common_assets: Res<CommonAssets>, styles: Res<Assets<FGUiStyle>>, mut commands: Commands) {
|
||||||
|
let styles = styles.get(&common_assets.ui_style).expect("UI styles should be loaded");
|
||||||
|
|
||||||
|
for (entity, button, interaction, mut node) in query {
|
||||||
|
let button_style_to_use = match interaction {
|
||||||
|
Interaction::Pressed => &styles.button_styles[&button.button_type].press,
|
||||||
|
Interaction::Hovered => &styles.button_styles[&button.button_type].hover,
|
||||||
|
Interaction::None => &styles.button_styles[&button.button_type].normal,
|
||||||
|
};
|
||||||
|
button_style_to_use.apply(&mut commands, entity, &mut node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn button_focus_style_system(query: Query<(Entity, &FGUiButton)>, focus: Res<InputFocus>, mut commands: Commands, common_assets: Res<CommonAssets>, styles: Res<Assets<FGUiStyle>>) {
|
||||||
|
if let Some(focused_entity) = focus.get() {
|
||||||
|
|
||||||
|
if !query.contains(focused_entity) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let styles = styles.get(&common_assets.ui_style).expect("UI styles should be loaded");
|
||||||
|
|
||||||
|
for (entity, button) in query {
|
||||||
|
if focused_entity == entity {
|
||||||
|
let style = &styles.button_styles[&button.button_type];
|
||||||
|
commands.entity(focused_entity).insert(Outline {
|
||||||
|
width: px(style.focus_outline_width_px),
|
||||||
|
offset: px(style.focus_outline_offset_px),
|
||||||
|
color: style.focus_outline_color
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
commands.entity(entity).remove::<Outline>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
use bevy::{prelude::*, text::FontSourceTemplate};
|
||||||
|
|
||||||
|
use crate::{fighter::manifest::FighterManifest, menus::{button::FGUiButton, style::FGUiButtonType}};
|
||||||
|
|
||||||
|
#[derive(Component, Clone, Default, FromTemplate)]
|
||||||
|
pub struct CharacterBox {
|
||||||
|
fighter_manifest: Handle<FighterManifest>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CharacterBox {
|
||||||
|
pub fn scene(manifest_handle: Handle<FighterManifest>, manifests: &Res<Assets<FighterManifest>>) -> impl Scene {
|
||||||
|
let manifest = manifests.get(&manifest_handle).expect("Fighter manifest should be valid");
|
||||||
|
bsn! {
|
||||||
|
Node {
|
||||||
|
width: px(128),
|
||||||
|
height: px(128),
|
||||||
|
flex_direction: FlexDirection::Column,
|
||||||
|
justify_content: JustifyContent::FlexEnd
|
||||||
|
}
|
||||||
|
FGUiButton {
|
||||||
|
button_type: FGUiButtonType::CharacterSelectCharacter
|
||||||
|
}
|
||||||
|
CharacterBox {
|
||||||
|
fighter_manifest: manifest_handle
|
||||||
|
}
|
||||||
|
BackgroundColor(Color::srgb(1.0, 1.0, 1.0))
|
||||||
|
Children [
|
||||||
|
Node {
|
||||||
|
width: percent(100)
|
||||||
|
}
|
||||||
|
Text("El Grande Padre")
|
||||||
|
TextFont {
|
||||||
|
font: FontSourceTemplate::Handle("fonts/roboto.ttf"),
|
||||||
|
font_size: px(crate::menus::style::TEXT_SIZE),
|
||||||
|
}
|
||||||
|
TextColor(Color::srgb(0.9, 0.9, 0.9))
|
||||||
|
BackgroundColor(Color::srgb(0.0, 0.0, 1.0))
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
|
||||||
|
use bevy::prelude::*;
|
||||||
|
pub struct FighterPortrait {
|
||||||
|
pub player_slot: usize
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FighterPortrait {
|
||||||
|
pub fn create(player_slot: usize) -> impl Scene {
|
||||||
|
bsn![
|
||||||
|
Node {
|
||||||
|
max_width: px(300),
|
||||||
|
height: px(400),
|
||||||
|
flex_grow: 1.0
|
||||||
|
}
|
||||||
|
BackgroundColor(bevy::color::palettes::css::ORANGE)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
fighter::manifest::{FighterManifest, FighterManifestRegistry},
|
||||||
|
input::{LocalInputAssignments, LocalInputSource},
|
||||||
|
menus::{MenuMarker, build_menu_with_props, cursor::FGMenuCursor, spawn_menu, wrap_menu},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub mod character_box;
|
||||||
|
pub mod fighter_portrait;
|
||||||
|
|
||||||
|
#[derive(Component, FromTemplate)]
|
||||||
|
pub struct CharacterSelectScreen {
|
||||||
|
pub fighter_portait_container: Entity,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setup_character_select(
|
||||||
|
existing_menu: Option<Single<Entity, With<MenuMarker>>>,
|
||||||
|
commands: Commands,
|
||||||
|
manifest_registry: Res<FighterManifestRegistry>,
|
||||||
|
assignments: Res<LocalInputAssignments>,
|
||||||
|
manifests: Res<Assets<FighterManifest>>,
|
||||||
|
) {
|
||||||
|
let character_boxes = manifest_registry
|
||||||
|
.iter()
|
||||||
|
.map(|(_, v)| character_box::CharacterBox::scene(v.clone(), &manifests))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
// Create existing portraits
|
||||||
|
let fighter_portraits = assignments
|
||||||
|
.iter()
|
||||||
|
.map(|(slot, _)| fighter_portrait::FighterPortrait::create(*slot))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let menu = bsn! {
|
||||||
|
// Characters
|
||||||
|
CharacterSelectScreen {
|
||||||
|
fighter_portait_container: #PortraitContainer
|
||||||
|
}
|
||||||
|
Node {
|
||||||
|
flex_direction: FlexDirection::Column
|
||||||
|
}
|
||||||
|
Children [
|
||||||
|
(Node {
|
||||||
|
flex_direction: FlexDirection::Column,
|
||||||
|
width: percent(100),
|
||||||
|
height: percent(100),
|
||||||
|
justify_content: JustifyContent::Center,
|
||||||
|
align_items: AlignItems::Center
|
||||||
|
}
|
||||||
|
Children [
|
||||||
|
{character_boxes}
|
||||||
|
]
|
||||||
|
),
|
||||||
|
(
|
||||||
|
#PortraitContainer
|
||||||
|
Node {
|
||||||
|
flex_direction: FlexDirection::Row,
|
||||||
|
width: percent(100),
|
||||||
|
justify_content: JustifyContent::Center
|
||||||
|
}
|
||||||
|
Children [
|
||||||
|
{fighter_portraits}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
spawn_menu(existing_menu, commands, menu);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_portrait(
|
||||||
|
player_slot: usize,
|
||||||
|
container: Entity,
|
||||||
|
mut commands: Commands,
|
||||||
|
mut meshes: ResMut<Assets<Mesh>>,
|
||||||
|
mut materials: ResMut<Assets<ColorMaterial>>,
|
||||||
|
mapping: Res<LocalInputAssignments>,
|
||||||
|
) {
|
||||||
|
let entity = commands
|
||||||
|
.spawn_scene(fighter_portrait::FighterPortrait::create(player_slot))
|
||||||
|
.id();
|
||||||
|
commands.entity(container).add_child(entity);
|
||||||
|
|
||||||
|
commands.spawn(
|
||||||
|
FGMenuCursor::create_cursor(player_slot, meshes, materials, mapping)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Event)]
|
||||||
|
pub struct FighterSlotAssigned {
|
||||||
|
pub slot: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_fighter_slot_addition(
|
||||||
|
addition: On<FighterSlotAssigned>,
|
||||||
|
query: Single<&CharacterSelectScreen>,
|
||||||
|
commands: Commands,
|
||||||
|
meshes: ResMut<Assets<Mesh>>,
|
||||||
|
materials: ResMut<Assets<ColorMaterial>>,
|
||||||
|
mapping: Res<LocalInputAssignments>,
|
||||||
|
) {
|
||||||
|
let slot = addition.slot;
|
||||||
|
create_portrait(
|
||||||
|
slot,
|
||||||
|
query.fighter_portait_container,
|
||||||
|
commands,
|
||||||
|
meshes,
|
||||||
|
materials,
|
||||||
|
mapping,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn css_input(
|
||||||
|
key: Res<ButtonInput<KeyCode>>,
|
||||||
|
pads: Query<(Entity, &Gamepad)>,
|
||||||
|
mut assignments: ResMut<LocalInputAssignments>,
|
||||||
|
mut commands: Commands,
|
||||||
|
) {
|
||||||
|
if key.get_pressed().len() != 0 {
|
||||||
|
if let None = assignments
|
||||||
|
.iter()
|
||||||
|
.find(|(_, source)| matches!(source, LocalInputSource::Keyboard))
|
||||||
|
{
|
||||||
|
if let Some(free_idx) = assignments.get_free_player_index() {
|
||||||
|
assignments.push((free_idx, LocalInputSource::Keyboard));
|
||||||
|
info!("New keyboard assignment");
|
||||||
|
commands.trigger(FighterSlotAssigned { slot: free_idx });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (entity, pad) in pads {
|
||||||
|
if pad.any_pressed(GamepadButton::all()) {
|
||||||
|
let existed = assignments
|
||||||
|
.iter()
|
||||||
|
.find(|(_, assignment)| {
|
||||||
|
if let LocalInputSource::Gamepad(assignment_entity) = assignment {
|
||||||
|
return entity == *assignment_entity;
|
||||||
|
}
|
||||||
|
false
|
||||||
|
})
|
||||||
|
.is_some();
|
||||||
|
|
||||||
|
if !existed && let Some(free_idx) = assignments.get_free_player_index() {
|
||||||
|
assignments.push((free_idx, LocalInputSource::Gamepad(entity)));
|
||||||
|
commands.trigger(FighterSlotAssigned { slot: free_idx });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
use bevy::{
|
||||||
|
asset::uuid::Uuid,
|
||||||
|
math::VectorSpace,
|
||||||
|
picking::pointer::{Location, PointerId, PointerInput, PointerLocation},
|
||||||
|
prelude::*,
|
||||||
|
window::{NormalizedWindowRef, WindowRef},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{game_settings::GameSettings, input::LocalInputAssignments, menus::input::MenuInput};
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub enum MenuCursorInputSource {
|
||||||
|
#[default]
|
||||||
|
Any,
|
||||||
|
Player(usize),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component, Default)]
|
||||||
|
pub struct FGMenuCursor {
|
||||||
|
pub input_source: MenuCursorInputSource,
|
||||||
|
pub velocity: Vec2,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FGMenuCursor {
|
||||||
|
pub fn create_cursor(
|
||||||
|
player_slot: usize,
|
||||||
|
mut meshes: ResMut<Assets<Mesh>>,
|
||||||
|
mut materials: ResMut<Assets<ColorMaterial>>,
|
||||||
|
mapping: Res<LocalInputAssignments>,
|
||||||
|
) -> impl Bundle {
|
||||||
|
let (_, mapping) = mapping
|
||||||
|
.iter()
|
||||||
|
.find(|(slot, _)| *slot == player_slot)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let material = materials.add(ColorMaterial::from_color(
|
||||||
|
bevy::color::palettes::tailwind::ROSE_600,
|
||||||
|
));
|
||||||
|
let circle_mesh = meshes.add(Circle::new(25.0));
|
||||||
|
let pointer_id = match mapping {
|
||||||
|
crate::input::LocalInputSource::Keyboard => PointerId::Mouse,
|
||||||
|
crate::input::LocalInputSource::Gamepad(_) => PointerId::Custom(Uuid::new_v4()),
|
||||||
|
};
|
||||||
|
(
|
||||||
|
FGMenuCursor {
|
||||||
|
input_source: super::cursor::MenuCursorInputSource::Player(player_slot),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
pointer_id,
|
||||||
|
MeshMaterial2d(material),
|
||||||
|
Mesh2d(circle_mesh),
|
||||||
|
Transform::IDENTITY,
|
||||||
|
GlobalZIndex(1000),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cursor_input(
|
||||||
|
window: Single<Entity, With<bevy::window::PrimaryWindow>>,
|
||||||
|
query: Query<(
|
||||||
|
&mut FGMenuCursor,
|
||||||
|
&PointerLocation,
|
||||||
|
&PointerId,
|
||||||
|
)>,
|
||||||
|
key: Res<ButtonInput<KeyCode>>,
|
||||||
|
assignments: Res<LocalInputAssignments>,
|
||||||
|
game_settings: Res<GameSettings>,
|
||||||
|
time: Res<Time>,
|
||||||
|
pads: Query<(Entity, &Gamepad)>,
|
||||||
|
mut pointer_writer: MessageWriter<PointerInput>,
|
||||||
|
) {
|
||||||
|
for (mut cursor, location, pointer_id) in query {
|
||||||
|
let input = match cursor.input_source {
|
||||||
|
MenuCursorInputSource::Any => {
|
||||||
|
let mut input = MenuInput::default();
|
||||||
|
for (_, source) in assignments.iter() {
|
||||||
|
input.accumulate(&MenuInput::from_source(
|
||||||
|
source,
|
||||||
|
&key,
|
||||||
|
&pads,
|
||||||
|
&assignments,
|
||||||
|
&game_settings,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
input
|
||||||
|
}
|
||||||
|
MenuCursorInputSource::Player(slot) => {
|
||||||
|
if let Some((_, source)) = assignments.iter().find(|(idx, _)| *idx == slot) {
|
||||||
|
MenuInput::from_source(source, &key, &pads, &assignments, &game_settings)
|
||||||
|
} else {
|
||||||
|
MenuInput::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let movement_screen_space = input.movement * Vec2::new(1.0, -1.0);
|
||||||
|
cursor.velocity = movement_screen_space * 500.0;
|
||||||
|
|
||||||
|
if cursor.velocity == Vec2::ZERO {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let old_cursor_pos = location
|
||||||
|
.location
|
||||||
|
.clone()
|
||||||
|
.map(|loc| loc.position)
|
||||||
|
.unwrap_or(Vec2::ZERO);
|
||||||
|
let cursor_pos = old_cursor_pos + cursor.velocity * time.delta_secs();
|
||||||
|
|
||||||
|
dbg!(cursor_pos);
|
||||||
|
|
||||||
|
pointer_writer.write(PointerInput {
|
||||||
|
pointer_id: *pointer_id,
|
||||||
|
location: Location {
|
||||||
|
target: bevy::camera::NormalizedRenderTarget::Window(
|
||||||
|
WindowRef::Primary
|
||||||
|
.normalize(Some(window.entity()))
|
||||||
|
.expect("Primary window should be valid"),
|
||||||
|
),
|
||||||
|
position: cursor_pos,
|
||||||
|
},
|
||||||
|
action: bevy::picking::pointer::PointerAction::Move {
|
||||||
|
delta: cursor_pos - old_cursor_pos,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
//cursor.position
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_cursor_transform(
|
||||||
|
camera: Single<(&Camera, &GlobalTransform), With<IsDefaultUiCamera>>,
|
||||||
|
query: Query<(&mut Transform, &PointerLocation), With<FGMenuCursor>>,
|
||||||
|
) {
|
||||||
|
let (camera, camera_transform) = camera.into_inner();
|
||||||
|
|
||||||
|
for (mut trf, location) in query {
|
||||||
|
let cursor_pos_viewport = camera
|
||||||
|
.viewport_to_world_2d(
|
||||||
|
camera_transform,
|
||||||
|
location
|
||||||
|
.clone()
|
||||||
|
.location
|
||||||
|
.map(|d| d.position)
|
||||||
|
.unwrap_or_default(),
|
||||||
|
)
|
||||||
|
.ok()
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
trf.translation = Vec3::new(cursor_pos_viewport.x, cursor_pos_viewport.y, 0.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
use crate::{game_settings::GameSettings, input::{LocalInputAssignments, LocalInputSource}};
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct MenuInput {
|
||||||
|
pub movement: Vec2,
|
||||||
|
pub accept: bool
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MenuInput {
|
||||||
|
|
||||||
|
pub fn accumulate(&mut self, other: &Self) {
|
||||||
|
self.movement += other.movement;
|
||||||
|
self.movement = self.movement.normalize();
|
||||||
|
self.accept = self.accept || other.accept;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_gamepad(gamepad: &Gamepad, game_settings: &GameSettings) -> Self {
|
||||||
|
let mut x = gamepad.left_stick().x;
|
||||||
|
let mut y = gamepad.left_stick().y;
|
||||||
|
|
||||||
|
let deadzone = game_settings.input_common.stick_deadzone.to_num::<f32>();
|
||||||
|
|
||||||
|
if x.abs() < deadzone {
|
||||||
|
x = 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if y.abs() < deadzone {
|
||||||
|
y = 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuInput {
|
||||||
|
movement: Vec2::new(x, y),
|
||||||
|
accept: gamepad.pressed(GamepadButton::South)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn from_keyboard(keeb: &ButtonInput<KeyCode>) -> Self {
|
||||||
|
let x = if keeb.pressed(KeyCode::KeyA) { -1.0 } else { 0.0 };
|
||||||
|
let x = x + if keeb.pressed(KeyCode::KeyD) { 1.0 } else { 0.0 };
|
||||||
|
let y = if keeb.pressed(KeyCode::KeyS) { -1.0 } else { 0.0 };
|
||||||
|
let y = y + if keeb.pressed(KeyCode::KeyW) { 1.0 } else { 0.0 };
|
||||||
|
|
||||||
|
let movement = Vec2::new(x, y);
|
||||||
|
MenuInput {
|
||||||
|
movement: movement,
|
||||||
|
accept: keeb.pressed(KeyCode::Enter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_source(source: &LocalInputSource, key: &ButtonInput<KeyCode>, gamepads: &Query<(Entity, &Gamepad)>, assignments: &LocalInputAssignments, game_settings: &GameSettings) -> Self {
|
||||||
|
match source {
|
||||||
|
LocalInputSource::Keyboard => Self::from_keyboard(key),
|
||||||
|
LocalInputSource::Gamepad(entity) => {
|
||||||
|
let gamepad = gamepads.get(*entity);
|
||||||
|
|
||||||
|
if let Ok((_, gamepad)) = gamepad {
|
||||||
|
Self::from_gamepad(gamepad, game_settings)
|
||||||
|
} else {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
use bevy::{
|
||||||
|
prelude::*, text::FontSourceTemplate,
|
||||||
|
ui::auto_directional_navigation::AutoDirectionalNavigation,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{AppState, menus::{MenuMarker, button::menu_button, navigation::NavigationDefaultFocus}};
|
||||||
|
|
||||||
|
#[derive(SceneComponent, Default, Clone)]
|
||||||
|
pub struct MainMenu;
|
||||||
|
|
||||||
|
impl MainMenu {
|
||||||
|
fn scene() -> impl Scene {
|
||||||
|
bsn! {
|
||||||
|
BackgroundColor(Color::srgb(0.15, 0.0, 0.0))
|
||||||
|
Children [
|
||||||
|
(
|
||||||
|
Text("Project Shinespark")
|
||||||
|
TextFont {
|
||||||
|
font: FontSourceTemplate::Handle("fonts/roboto.ttf"),
|
||||||
|
font_size: px(super::style::H1_SIZE),
|
||||||
|
}
|
||||||
|
TextColor(Color::srgb(0.9, 0.9, 0.9))
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Text("This is a beta, kuro owes me 5€")
|
||||||
|
TextFont {
|
||||||
|
font: FontSourceTemplate::Handle("fonts/roboto.ttf"),
|
||||||
|
font_size: px(super::style::H2_SIZE),
|
||||||
|
}
|
||||||
|
TextColor(Color::srgb(0.9, 0.9, 0.9))
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Node {
|
||||||
|
width: percent(100),
|
||||||
|
height: percent(100),
|
||||||
|
align_items: AlignItems::Center,
|
||||||
|
justify_content: JustifyContent::Center,
|
||||||
|
column_gap: px(5),
|
||||||
|
flex_direction: FlexDirection::Column,
|
||||||
|
}
|
||||||
|
Children [
|
||||||
|
(
|
||||||
|
menu_button("Versus")
|
||||||
|
on(|_event: On<Pointer<Press>>, mut next_state: ResMut<NextState<AppState>>,| next_state.set(AppState::CharacterSelect))
|
||||||
|
NavigationDefaultFocus
|
||||||
|
),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setup_main_menu(mut commands: Commands) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
use bevy::{
|
||||||
|
input_focus::{
|
||||||
|
InputFocus, InputFocusVisible,
|
||||||
|
directional_navigation::{AutoNavigationConfig, DirectionalNavigationPlugin},
|
||||||
|
},
|
||||||
|
prelude::*,
|
||||||
|
};
|
||||||
|
|
||||||
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
AppState,
|
||||||
|
game_settings::CommonAssets,
|
||||||
|
menus::{character_select::CharacterSelectScreen, main_menu::MainMenu, style::FGUiStyle},
|
||||||
|
};
|
||||||
|
pub mod button;
|
||||||
|
pub mod character_select;
|
||||||
|
pub mod cursor;
|
||||||
|
pub mod input;
|
||||||
|
pub mod main_menu;
|
||||||
|
pub mod navigation;
|
||||||
|
pub mod scaling;
|
||||||
|
pub mod style;
|
||||||
|
|
||||||
|
#[derive(Component, Clone, Default)]
|
||||||
|
pub struct MenuMarker {}
|
||||||
|
|
||||||
|
pub struct MenuPlugin;
|
||||||
|
|
||||||
|
pub fn build_menu_scene<S>(existing_menu: Option<Single<Entity, With<MenuMarker>>>) -> impl Scene
|
||||||
|
where
|
||||||
|
S: SceneComponent + Default + Clone + Send + Sync,
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn wrap_menu(menu: impl Scene) -> impl Scene {
|
||||||
|
bsn! {
|
||||||
|
#MenuRoot
|
||||||
|
MenuMarker
|
||||||
|
Node {
|
||||||
|
width: percent(100),
|
||||||
|
height: percent(100),
|
||||||
|
align_items: AlignItems::Center,
|
||||||
|
justify_content: JustifyContent::Center,
|
||||||
|
flex_direction: FlexDirection::Column,
|
||||||
|
}
|
||||||
|
Children [
|
||||||
|
(
|
||||||
|
#MenuContainer
|
||||||
|
Node {
|
||||||
|
width: px(1920),
|
||||||
|
height: px(1080),
|
||||||
|
align_items: AlignItems::Center,
|
||||||
|
justify_content: JustifyContent::Center,
|
||||||
|
column_gap: vmin(5.0),
|
||||||
|
flex_direction: FlexDirection::Column,
|
||||||
|
padding: UiRect { left: vmin(5.0), right: vmin(5.0), top: vmin(5.0), bottom: vmin(5.0) }
|
||||||
|
}
|
||||||
|
menu
|
||||||
|
)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn spawn_menu(
|
||||||
|
existing_menu: Option<Single<Entity, With<MenuMarker>>>,
|
||||||
|
mut commands: Commands,
|
||||||
|
menu: impl Scene,
|
||||||
|
) {
|
||||||
|
if let Some(entity) = existing_menu {
|
||||||
|
commands
|
||||||
|
.entity(entity.into_inner())
|
||||||
|
.remove::<Camera2d>()
|
||||||
|
.remove::<Camera>()
|
||||||
|
.despawn();
|
||||||
|
}
|
||||||
|
commands.queue_spawn_scene(wrap_menu(menu));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_menu_with_props<S>(props: S::Props) -> impl Scene
|
||||||
|
where
|
||||||
|
S: SceneComponent + Default + Clone + Send + Sync,
|
||||||
|
{
|
||||||
|
S::scene(props)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_menu<S>(
|
||||||
|
existing_menu: Option<Single<Entity, With<MenuMarker>>>,
|
||||||
|
mut commands: Commands,
|
||||||
|
) where
|
||||||
|
S: SceneComponent + Default + Clone + Send + Sync,
|
||||||
|
{
|
||||||
|
spawn_menu(
|
||||||
|
existing_menu,
|
||||||
|
commands,
|
||||||
|
build_menu_with_props::<S>(S::Props::default()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Plugin for MenuPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.add_systems(
|
||||||
|
OnEnter(AppState::MainMenu),
|
||||||
|
(build_menu::<MainMenu>, |mut commands: Commands| {
|
||||||
|
commands.spawn((Camera2d, IsDefaultUiCamera));
|
||||||
|
commands.spawn((
|
||||||
|
Camera2d,
|
||||||
|
Camera {
|
||||||
|
order: 1,
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.add_systems(
|
||||||
|
OnEnter(AppState::CharacterSelect),
|
||||||
|
character_select::setup_character_select,
|
||||||
|
)
|
||||||
|
.init_resource::<navigation::UINavigationActionState>()
|
||||||
|
.insert_resource(InputFocusVisible(true))
|
||||||
|
.insert_resource(GlobalUiDebugOptions {
|
||||||
|
enabled: true,
|
||||||
|
..default()
|
||||||
|
})
|
||||||
|
// Configure auto-navigation behavior
|
||||||
|
.insert_resource(AutoNavigationConfig {
|
||||||
|
// Require at least 10% overlap in perpendicular axis for cardinal directions
|
||||||
|
min_alignment_factor: 0.1,
|
||||||
|
// Don't connect nodes more than 500 pixels apart between their closest edges
|
||||||
|
max_search_distance: Some(500.0),
|
||||||
|
// Prefer nodes that are well-aligned
|
||||||
|
prefer_aligned: true,
|
||||||
|
})
|
||||||
|
.add_plugins(DirectionalNavigationPlugin)
|
||||||
|
.add_systems(
|
||||||
|
PreUpdate,
|
||||||
|
(
|
||||||
|
navigation::give_default_focus,
|
||||||
|
scaling::ui_scaling_system,
|
||||||
|
button::button_setup,
|
||||||
|
button::button_style_system,
|
||||||
|
button::button_focus_style_system,
|
||||||
|
)
|
||||||
|
.run_if(resource_exists::<CommonAssets>),
|
||||||
|
)
|
||||||
|
.add_systems(Startup, scaling::ui_update_scale_on_startup)
|
||||||
|
.add_systems(
|
||||||
|
PostUpdate,
|
||||||
|
(character_select::css_input, (cursor::cursor_input, cursor::update_cursor_transform).chain())
|
||||||
|
.run_if(in_state(AppState::CharacterSelect))
|
||||||
|
.run_if(any_with_component::<CharacterSelectScreen>),
|
||||||
|
)
|
||||||
|
.add_observer(character_select::handle_fighter_slot_addition)
|
||||||
|
.add_plugins(bevy_common_assets::ron::RonAssetPlugin::<FGUiStyle>::new(
|
||||||
|
&["stylesheet.ron"],
|
||||||
|
))
|
||||||
|
.add_systems(
|
||||||
|
Update,
|
||||||
|
(
|
||||||
|
navigation::process_inputs,
|
||||||
|
navigation::navigate,
|
||||||
|
navigation::interact_with_focused_button,
|
||||||
|
)
|
||||||
|
.chain(),
|
||||||
|
)
|
||||||
|
.init_resource::<InputFocus>();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
use std::{collections::HashSet, time::Duration};
|
||||||
|
use bevy::{camera::NormalizedRenderTarget, input_focus::{InputFocus, InputFocusVisible}, math::CompassOctant, picking::{backend::HitData, pointer::{Location, PointerId}}, prelude::*, ui::auto_directional_navigation::AutoDirectionalNavigator};
|
||||||
|
|
||||||
|
// Action state and input handling
|
||||||
|
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||||
|
enum DirectionalNavigationAction {
|
||||||
|
Up,
|
||||||
|
Down,
|
||||||
|
Left,
|
||||||
|
Right,
|
||||||
|
Select,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DirectionalNavigationAction {
|
||||||
|
fn variants() -> Vec<Self> {
|
||||||
|
vec![
|
||||||
|
DirectionalNavigationAction::Up,
|
||||||
|
DirectionalNavigationAction::Down,
|
||||||
|
DirectionalNavigationAction::Left,
|
||||||
|
DirectionalNavigationAction::Right,
|
||||||
|
DirectionalNavigationAction::Select,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn keycode(&self) -> KeyCode {
|
||||||
|
match self {
|
||||||
|
DirectionalNavigationAction::Up => KeyCode::ArrowUp,
|
||||||
|
DirectionalNavigationAction::Down => KeyCode::ArrowDown,
|
||||||
|
DirectionalNavigationAction::Left => KeyCode::ArrowLeft,
|
||||||
|
DirectionalNavigationAction::Right => KeyCode::ArrowRight,
|
||||||
|
DirectionalNavigationAction::Select => KeyCode::Enter,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn gamepad_button(&self) -> GamepadButton {
|
||||||
|
match self {
|
||||||
|
DirectionalNavigationAction::Up => GamepadButton::DPadUp,
|
||||||
|
DirectionalNavigationAction::Down => GamepadButton::DPadDown,
|
||||||
|
DirectionalNavigationAction::Left => GamepadButton::DPadLeft,
|
||||||
|
DirectionalNavigationAction::Right => GamepadButton::DPadRight,
|
||||||
|
DirectionalNavigationAction::Select => GamepadButton::South,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Resource)]
|
||||||
|
pub struct UINavigationActionState {
|
||||||
|
pressed_actions: HashSet<DirectionalNavigationAction>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Component, Clone)]
|
||||||
|
pub struct NavigationDefaultFocus;
|
||||||
|
|
||||||
|
pub fn process_inputs(
|
||||||
|
mut action_state: ResMut<UINavigationActionState>,
|
||||||
|
keyboard_input: Res<ButtonInput<KeyCode>>,
|
||||||
|
gamepad_input: Query<&Gamepad>,
|
||||||
|
) {
|
||||||
|
action_state.pressed_actions.clear();
|
||||||
|
|
||||||
|
for action in DirectionalNavigationAction::variants() {
|
||||||
|
if keyboard_input.just_pressed(action.keycode()) {
|
||||||
|
action_state.pressed_actions.insert(action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for gamepad in gamepad_input.iter() {
|
||||||
|
for action in DirectionalNavigationAction::variants() {
|
||||||
|
if gamepad.just_pressed(action.gamepad_button()) {
|
||||||
|
action_state.pressed_actions.insert(action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const FOCUSED_BORDER: Srgba = bevy::color::palettes::tailwind::BLUE_50;
|
||||||
|
|
||||||
|
pub fn highlight_focused_element(
|
||||||
|
input_focus: Res<InputFocus>,
|
||||||
|
input_focus_visible: Res<InputFocusVisible>,
|
||||||
|
mut query: Query<(Entity, &mut BorderColor)>,
|
||||||
|
) {
|
||||||
|
for (entity, mut border_color) in query.iter_mut() {
|
||||||
|
if input_focus.get() == Some(entity) && input_focus_visible.0 {
|
||||||
|
*border_color = BorderColor::all(FOCUSED_BORDER);
|
||||||
|
} else {
|
||||||
|
*border_color = BorderColor::DEFAULT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn navigate(
|
||||||
|
action_state: Res<UINavigationActionState>,
|
||||||
|
mut auto_directional_navigator: AutoDirectionalNavigator,
|
||||||
|
) {
|
||||||
|
let net_east_west = action_state
|
||||||
|
.pressed_actions
|
||||||
|
.contains(&DirectionalNavigationAction::Right) as i8
|
||||||
|
- action_state
|
||||||
|
.pressed_actions
|
||||||
|
.contains(&DirectionalNavigationAction::Left) as i8;
|
||||||
|
|
||||||
|
let net_north_south = action_state
|
||||||
|
.pressed_actions
|
||||||
|
.contains(&DirectionalNavigationAction::Up) as i8
|
||||||
|
- action_state
|
||||||
|
.pressed_actions
|
||||||
|
.contains(&DirectionalNavigationAction::Down) as i8;
|
||||||
|
|
||||||
|
// Use Dir2::from_xy to convert input to direction, then convert to CompassOctant
|
||||||
|
let maybe_direction = Dir2::from_xy(net_east_west as f32, net_north_south as f32)
|
||||||
|
.ok()
|
||||||
|
.map(CompassOctant::from);
|
||||||
|
|
||||||
|
if let Some(direction) = maybe_direction {
|
||||||
|
match auto_directional_navigator.navigate(direction) {
|
||||||
|
Ok(_entity) => {
|
||||||
|
// Successfully navigated
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Navigation error: {}", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn give_default_focus(query: Query<Entity, Added<NavigationDefaultFocus>>, mut input_focus: ResMut<InputFocus>) {
|
||||||
|
for entity in query {
|
||||||
|
input_focus.set(entity, bevy::input_focus::FocusCause::Navigated);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn interact_with_focused_button(
|
||||||
|
action_state: Res<UINavigationActionState>,
|
||||||
|
input_focus: Res<InputFocus>,
|
||||||
|
mut commands: Commands,
|
||||||
|
) {
|
||||||
|
if action_state
|
||||||
|
.pressed_actions
|
||||||
|
.contains(&DirectionalNavigationAction::Select)
|
||||||
|
&& let Some(focused_entity) = input_focus.get()
|
||||||
|
{
|
||||||
|
commands.trigger(Pointer::new(
|
||||||
|
PointerId::Mouse,
|
||||||
|
Location {
|
||||||
|
target: NormalizedRenderTarget::None {
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
},
|
||||||
|
position: Vec2::ZERO,
|
||||||
|
},
|
||||||
|
Press {
|
||||||
|
button: PointerButton::Primary,
|
||||||
|
hit: HitData {
|
||||||
|
camera: Entity::PLACEHOLDER,
|
||||||
|
depth: 0.0,
|
||||||
|
position: None,
|
||||||
|
normal: None,
|
||||||
|
extra: None,
|
||||||
|
},
|
||||||
|
count: 1,
|
||||||
|
},
|
||||||
|
focused_entity,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
use bevy::camera::CameraProjection;
|
||||||
|
use bevy::camera::Projection::Orthographic;
|
||||||
|
use bevy::prelude::*;
|
||||||
|
use bevy::window::WindowResized;
|
||||||
|
|
||||||
|
fn compute_ui_scale(window_size: Vec2) -> f32 {
|
||||||
|
const BASE_SIZE: Vec2 = Vec2::new(1920.0, 1080.0);
|
||||||
|
const BASE_ASPECT_RATIO: f32 = BASE_SIZE.x / BASE_SIZE.y;
|
||||||
|
let new_aspect_ratio = window_size.x / window_size.y;
|
||||||
|
if new_aspect_ratio > BASE_ASPECT_RATIO {
|
||||||
|
window_size.y / BASE_SIZE.y
|
||||||
|
} else {
|
||||||
|
window_size.x / BASE_SIZE.x
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ui_update_scale_on_startup(window: Single<&Window>, mut ui_scale: ResMut<UiScale>) {
|
||||||
|
ui_scale.0 = compute_ui_scale(window.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ui_scaling_system(mut resize_reader: MessageReader<WindowResized>, mut ui_scale: ResMut<UiScale>) {
|
||||||
|
|
||||||
|
for event in resize_reader.read() {
|
||||||
|
let dimensions = Vec2::new(event.width, event.height);
|
||||||
|
*ui_scale = UiScale(compute_ui_scale(dimensions));
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use bevy::prelude::*;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
pub const H1_SIZE: i32 = 72;
|
||||||
|
pub const H2_SIZE: i32 = 54;
|
||||||
|
pub const H3_SIZE: i32 = 40;
|
||||||
|
pub const TEXT_SIZE: i32 = 24;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Reflect, Default)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub struct BoxShadowStyle {
|
||||||
|
color: Color,
|
||||||
|
offset_px_x: i32,
|
||||||
|
offset_px_y: i32,
|
||||||
|
spread_radius: f32,
|
||||||
|
blur_radius: f32
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Reflect, Default)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub struct ButtonStyle {
|
||||||
|
background_color: Color,
|
||||||
|
border_color: Color,
|
||||||
|
text_color: Color,
|
||||||
|
box_shadow: BoxShadowStyle,
|
||||||
|
border_size: i32
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Reflect)]
|
||||||
|
pub struct ButtonStyles {
|
||||||
|
pub normal: ButtonStyle,
|
||||||
|
pub hover: ButtonStyle,
|
||||||
|
pub press: ButtonStyle,
|
||||||
|
pub focus_outline_width_px: i32,
|
||||||
|
pub focus_outline_offset_px: i32,
|
||||||
|
pub focus_outline_color: Color,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ButtonStyle {
|
||||||
|
pub fn apply(&self, commands: &mut Commands, entity: Entity, node: &mut Node) {
|
||||||
|
node.border = UiRect::all(px(self.border_size));
|
||||||
|
commands.entity(entity).insert((
|
||||||
|
BackgroundColor(self.background_color),
|
||||||
|
BorderColor::all(self.background_color),
|
||||||
|
BoxShadow::new(self.box_shadow.color, px(self.box_shadow.offset_px_x), px(self.box_shadow.offset_px_y), px(self.box_shadow.spread_radius), px(self.box_shadow.blur_radius))
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Reflect, Eq, PartialEq, Hash, Deserialize, Serialize, Default, Clone)]
|
||||||
|
pub enum FGUiButtonType {
|
||||||
|
#[default]
|
||||||
|
MainMenu,
|
||||||
|
CharacterSelectCharacter
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Asset, Resource, Reflect, Deserialize, Serialize)]
|
||||||
|
pub struct FGUiStyle {
|
||||||
|
pub button_styles: HashMap<FGUiButtonType, ButtonStyles>
|
||||||
|
}
|
||||||
+1
-1
@@ -239,7 +239,7 @@ fn finish_playback(mut commands: Commands, mut next_state: ResMut<NextState<AppS
|
|||||||
commands.remove_resource::<ReplayRuntime>();
|
commands.remove_resource::<ReplayRuntime>();
|
||||||
commands.remove_resource::<LoadedReplay>();
|
commands.remove_resource::<LoadedReplay>();
|
||||||
commands.insert_resource(ReplayComplete);
|
commands.insert_resource(ReplayComplete);
|
||||||
next_state.set(AppState::Idle);
|
next_state.set(AppState::MainMenu);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -173,7 +173,7 @@ pub fn prepare_stage_manifests(
|
|||||||
}
|
}
|
||||||
|
|
||||||
commands.insert_resource(StageManifestRegistry(registry));
|
commands.insert_resource(StageManifestRegistry(registry));
|
||||||
next_state.set(AppState::Idle);
|
next_state.set(AppState::MainMenu);
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StageManifest {
|
impl StageManifest {
|
||||||
|
|||||||
Reference in New Issue
Block a user