More CSS work

This commit is contained in:
2026-09-11 19:03:05 +02:00
parent cc850debf2
commit 1be4b470c6
17 changed files with 732 additions and 159 deletions
+13 -7
View File
@@ -1,20 +1,22 @@
use std::collections::HashMap;
use bevy::{asset::UntypedHandle, prelude::*};
use bevy::{
asset::UntypedHandle,
prelude::*,
};
use bevy_asset_loader::asset_collection::AssetCollection;
use ron_asset_manager::prelude::RonAsset;
use serde::{Deserialize, Serialize};
use strum::IntoEnumIterator;
use strum_macros::EnumIter;
use crate::{
AppState,
fighter::{FighterAttributes, FighterCameraProfile, animation::AnimKind, attack::AttackKind},
game_settings::GameSettings,
math::{int::FGi32, vec3::FGVec3},
AppState, fighter::{FighterAttributes, FighterCameraProfile, animation::AnimKind, attack::AttackKind}, game_settings::GameSettings, math::{int::FGi32, vec3::FGVec3}, texture_reference::TextureReference,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Reflect, EnumIter)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Reflect, EnumIter, Default)]
pub enum FighterId {
#[default]
TestFighter,
}
@@ -29,11 +31,15 @@ pub struct FighterHurtbox {
pub radius: FGi32,
}
#[derive(Serialize, Deserialize, Clone, Asset, Reflect)]
#[derive(Clone, Asset, Deserialize, Reflect, RonAsset)]
pub struct FighterManifest {
pub id: FighterId,
pub model_path: String,
pub baked_animation_path: String,
#[asset]
#[dependency]
#[reflect(ignore)]
pub icon: TextureReference,
pub attributes: FighterAttributes,
pub camera: FighterCameraProfile,
pub animations: HashMap<AnimKind, String>,
+6 -5
View File
@@ -25,9 +25,9 @@ use bevy::{
asset::processor::{AssetProcessor, ProcessorState}, prelude::*, settings::SettingsPlugin, tasks::block_on,
};
use bevy_asset_loader::prelude::*;
use ron_asset_manager::RonAssetPlugin;
use bevy_wind_waker_shader::prelude::*;
use clap::Parser;
use jackdaw_runtime::EditorCategory;
use crate::{
camera::MatchCameraPlugin, fighter::manifest::FighterManifest, game_settings::GameSettings,
@@ -49,6 +49,7 @@ pub mod schedule;
pub mod scripting;
pub mod stage;
pub mod menus;
pub mod texture_reference;
#[derive(States, Debug, Clone, Copy, Eq, PartialEq, Hash, Default)]
pub enum AppState {
@@ -95,9 +96,9 @@ impl Plugin for GamePlugin {
scripting::importer::FighterScriptImportPlugin,
stage::StagePlugin,
MatchCameraPlugin,
bevy_common_assets::ron::RonAssetPlugin::<FighterManifest>::new(&["fighter.ron"]),
bevy_common_assets::ron::RonAssetPlugin::<StageManifest>::new(&["stage.ron"]),
bevy_common_assets::ron::RonAssetPlugin::<GameSettings>::new(&["ron"]),
RonAssetPlugin::<FighterManifest>::create("fighter.ron"),
RonAssetPlugin::<StageManifest>::create("stage.ron"),
RonAssetPlugin::<GameSettings>::create("ron"),
))
.add_plugins(menus::MenuPlugin)
.add_loading_state(
@@ -158,4 +159,4 @@ fn wait_for_asset_processor(
{
next_state.set(AppState::LoadCommonAssets);
}
}
}
+21 -7
View File
@@ -1,15 +1,25 @@
use bevy::{picking::Pickable, prelude::*, text::FontSourceTemplate};
use bevy::{picking::Pickable, prelude::*, text::FontSourceTemplate, ui::widget::ImageNodeSize};
use crate::{fighter::manifest::FighterManifest, menus::{button::FGUiButton, style::FGUiButtonType}};
use crate::{
fighter::{FighterId, manifest::FighterManifest}, menus::{button::FGUiButton, style::FGUiButtonType},
};
#[derive(Component, Clone, Default, FromTemplate)]
#[derive(Component, Clone, FromTemplate)]
pub struct CharacterBox {
fighter_manifest: Handle<FighterManifest>,
pub fighter_manifest: Handle<FighterManifest>,
pub fighter_id: FighterId
}
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");
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");
let fighter_icon = manifest.icon.handle().clone();
let fighter_id = manifest.id;
bsn! {
Node {
width: px(128),
@@ -17,11 +27,15 @@ impl CharacterBox {
flex_direction: FlexDirection::Column,
justify_content: JustifyContent::FlexEnd
}
ImageNode {
image: fighter_icon
}
FGUiButton {
button_type: FGUiButtonType::CharacterSelectCharacter
}
CharacterBox {
fighter_manifest: manifest_handle
fighter_manifest: manifest_handle,
fighter_id: fighter_id
}
BackgroundColor(Color::srgb(1.0, 1.0, 1.0))
Children [
+3
View File
@@ -0,0 +1,3 @@
pub enum CharacterSelectErrors {
}
@@ -1,7 +1,6 @@
use bevy::prelude::*;
pub struct FighterPortrait {
pub player_slot: usize
pub player_slot: usize,
}
impl FighterPortrait {
@@ -15,4 +14,4 @@ impl FighterPortrait {
BackgroundColor(bevy::color::palettes::css::ORANGE)
]
}
}
}
+172 -34
View File
@@ -1,43 +1,53 @@
use bevy::prelude::*;
use bevy::{
picking::pointer::{PointerId, PointerLocation},
prelude::*,
};
use bevy_egui::egui::accesskit::TextAlign;
use crate::{
fighter::manifest::{FighterManifest, FighterManifestRegistry},
input::LocalInputAssignments,
menus::{MenuMarker, build_menu_with_props, cursor::FGMenuCursor, spawn_menu, wrap_menu},
AppState, fighter::manifest::{FighterManifest, FighterManifestRegistry}, game_settings::CommonAssets, input::{LocalInputAssignments, LocalInputSource}, menus::{
MenuMarker, build_menu_with_props,
button::FGUiButton,
character_select::character_box::CharacterBox,
cursor::{FGMenuCursor, MenuCursorInputSource},
errors::MenuErrors::{self, FighterManifestNotFound},
match_config::{MenuMatchConfig, MenuMatchPlayerSlot},
scaling, spawn_menu,
style::{FGUiButtonType, FGUiStyle},
wrap_menu,
},
};
pub mod character_box;
pub mod errors;
pub mod fighter_portrait;
pub mod token;
#[derive(Component, Clone, Default)]
pub struct StartText;
#[derive(Component, FromTemplate)]
pub struct CharacterSelectScreen {
pub fighter_portait_container: Entity,
}
#[derive(Component, FromTemplate)]
pub struct StartButtonBanner;
pub fn setup_character_select(
existing_menu: Option<Single<Entity, With<MenuMarker>>>,
mut commands: Commands,
manifest_registry: Res<FighterManifestRegistry>,
assignments: Res<LocalInputAssignments>,
manifests: Res<Assets<FighterManifest>>,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<ColorMaterial>>,
common_assets: Res<CommonAssets>,
styles: Res<Assets<FGUiStyle>>,
) {
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<_>>();
for (slot, _) in assignments.iter() {
commands.spawn(FGMenuCursor::create_cursor(*slot, &mut meshes, &mut materials));
}
let menu = bsn! {
// Characters
CharacterSelectScreen {
@@ -47,7 +57,8 @@ pub fn setup_character_select(
flex_direction: FlexDirection::Column
}
Children [
(Node {
(
Node {
flex_direction: FlexDirection::Column,
width: percent(100),
height: percent(100),
@@ -55,7 +66,32 @@ pub fn setup_character_select(
align_items: AlignItems::Center
}
Children [
{character_boxes}
{character_boxes},
(
// Start banner thingy
StartButtonBanner
Node {
position_type: PositionType::Absolute,
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
width: percent(100),
}
FGUiButton {
button_type: FGUiButtonType::CharacterSelectStartBanner
}
Text::new("Peenids")
Visibility::Hidden
TextLayout {
justify: Justify::Center
}
TextFont {
font_size: FontSize::Px(48.0),
}
TextColor(Color::WHITE)
BackgroundColor(Color::BLACK)
GlobalZIndex(500)
on(|_ev: On<Pointer<Press>>, mut next_state: ResMut<NextState<AppState>>| next_state.set(AppState::StageSelect))
)
]
),
(
@@ -63,15 +99,14 @@ pub fn setup_character_select(
Node {
flex_direction: FlexDirection::Row,
width: percent(100),
height: px(400),
justify_content: JustifyContent::Center
}
Children [
{fighter_portraits}
]
)
]
};
commands.init_resource::<MenuMatchConfig>();
spawn_menu(existing_menu, commands, menu);
}
@@ -79,44 +114,58 @@ pub fn create_portrait(
player_slot: usize,
container: Entity,
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<ColorMaterial>>,
style: &FGUiStyle,
input_source: &LocalInputSource,
) {
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, &mut meshes, &mut materials)
);
let cursor = commands
.spawn(FGMenuCursor::create_cursor(
player_slot,
style,
input_source,
))
.id();
commands.spawn(token::CharacterSelectToken::create(
cursor,
player_slot,
style,
));
commands.entity(container).add_child(entity);
}
#[derive(Event)]
pub struct FighterSlotAssigned {
pub struct PlayerSlotAssigned {
pub slot: usize,
pub input_source: LocalInputSource,
}
pub fn handle_fighter_slot_addition(
addition: On<FighterSlotAssigned>,
addition: On<PlayerSlotAssigned>,
query: Single<&CharacterSelectScreen>,
commands: Commands,
meshes: ResMut<Assets<Mesh>>,
materials: ResMut<Assets<ColorMaterial>>,
common_assets: Res<CommonAssets>,
styles: Res<Assets<FGUiStyle>>,
) {
let slot = addition.slot;
create_portrait(
slot,
query.fighter_portait_container,
commands,
meshes,
materials,
styles
.get(&common_assets.ui_style)
.expect("UI Style should be loaded by now"),
&addition.input_source,
);
}
pub fn css_input(
pub fn css_auto_assign_slots(
menu_inputs: Res<crate::menus::input::MenuInputState>,
mut assignments: ResMut<LocalInputAssignments>,
mut match_config: ResMut<MenuMatchConfig>,
mut commands: Commands,
) {
for (source, input) in &menu_inputs.inputs {
@@ -124,9 +173,98 @@ pub fn css_input(
let existed = assignments.iter().any(|(_, assigned)| assigned == source);
if !existed && let Some(free_idx) = assignments.get_free_player_index() {
assignments.push((free_idx, *source));
match_config.players.push(MenuMatchPlayerSlot {
slot: free_idx,
fighter: None,
});
info!("New input assignment");
commands.trigger(FighterSlotAssigned { slot: free_idx });
commands.trigger(PlayerSlotAssigned {
slot: free_idx,
input_source: source.clone(),
});
}
}
}
}
#[derive(Event)]
pub struct FighterSelectedEvent {
slot: usize,
}
pub fn select_fighter(
event: On<Pointer<Press>>,
character_boxes: Query<&CharacterBox>,
manifests: Res<Assets<FighterManifest>>,
mut match_config: ResMut<MenuMatchConfig>,
cursors: Query<(&FGMenuCursor, &PointerId)>,
mut commands: Commands,
) -> Result {
if let Some(character_box) = character_boxes.get(event.event_target()).ok() {
let cursor = cursors.iter().find(|(_, id)| **id == event.pointer_id);
if let Some((cursor, _)) = cursor {
if let MenuCursorInputSource::Player(slot) = cursor.input_source {
let manifest = manifests
.get(&character_box.fighter_manifest)
.ok_or(FighterManifestNotFound)?;
match_config.set_player_fighter(slot, Some(manifest.id))?;
commands.trigger(FighterSelectedEvent { slot });
}
}
}
Ok(())
}
pub fn update_start_banner_visibility(
_addition: On<FighterSelectedEvent>,
banner: Single<Entity, With<StartButtonBanner>>,
mut commands: Commands,
config: Res<MenuMatchConfig>,
) {
let match_ready = config.all_players_ready();
commands.entity(banner.into_inner()).insert(if match_ready {Visibility::Visible} else {Visibility::Hidden});
}
pub fn copy_cursor_transform_to_token(
tokens: Query<(
&token::CharacterSelectToken,
&mut UiTransform,
&ComputedNode,
)>,
character_boxes: Query<(&CharacterBox, &ComputedNode, &UiGlobalTransform)>,
match_config: Res<MenuMatchConfig>,
cursors: Query<&UiGlobalTransform, With<FGMenuCursor>>,
cam: Single<&Camera, With<IsDefaultUiCamera>>,
ui_scale: Res<UiScale>,
) -> Result {
let camera = cam.into_inner();
for (token, mut trf, computed) in tokens {
let fighter = match_config.get_player_fighter(token.slot)?;
if let Some(fighter_id) = fighter {
// Has selected fighter, move the token to the fighter portrait
let character_box = character_boxes
.iter()
.find(|(character_box, _, _)| character_box.fighter_id == fighter_id);
if let Some((_, _, box_trf)) = character_box {
let offset = computed.size * computed.inverse_scale_factor;
let translation =
scaling::logical_to_ui_position(box_trf.translation, camera, &ui_scale)
- offset * 0.5;
trf.translation = Val2::new(px(translation.x), px(translation.y));
}
} else {
if let Ok(cursor_trf) = cursors.get(token.cursor) {
let offset = computed.size * computed.inverse_scale_factor;
let translation =
scaling::logical_to_ui_position(cursor_trf.translation, camera, &ui_scale)
- offset;
trf.translation.x = px(translation.x);
trf.translation.y = px(translation.y);
}
}
}
Ok(())
}
+33
View File
@@ -0,0 +1,33 @@
//! Token that is used to select characters in the CSS
use bevy::prelude::*;
use crate::menus::style::FGUiStyle;
#[derive(Component, Clone)]
pub struct CharacterSelectToken {
pub slot: usize,
pub cursor: Entity
}
impl CharacterSelectToken {
pub fn create(cursor: Entity, slot: usize, style: &FGUiStyle) -> impl Bundle {
(
CharacterSelectToken {
slot,
cursor
},
Pickable::IGNORE,
Node {
position_type: PositionType::Absolute,
width: px(64),
height: px(64),
..default()
},
ImageNode {
image: style.css_token.handle.handle.clone(),
..default()
}
)
}
}
+59 -36
View File
@@ -1,11 +1,16 @@
use bevy::{
asset::uuid::Uuid,
picking::{pointer::{Location, PointerAction, PointerId, PointerInput, PointerLocation}, Pickable},
picking::{
Pickable,
pointer::{Location, PointerAction, PointerId, PointerInput, PointerLocation},
},
prelude::*,
window::WindowRef,
};
use crate::{input::LocalInputAssignments, menus::input::{MenuInput, MenuInputState}};
use crate::{
input::{LocalInputAssignments, LocalInputSource}, menus::{input::{MenuInput, MenuInputState}, scaling, style::FGUiStyle},
};
#[derive(Default)]
pub enum MenuCursorInputSource {
@@ -24,22 +29,27 @@ pub struct FGMenuCursor {
impl FGMenuCursor {
pub fn create_cursor(
player_slot: usize,
meshes: &mut Assets<Mesh>,
materials: &mut Assets<ColorMaterial>,
style: &FGUiStyle,
input_source: &LocalInputSource,
) -> impl Bundle {
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 = PointerId::Custom(Uuid::new_v4());
let pointer_id = if matches!(input_source, LocalInputSource::Keyboard) {PointerId::Mouse} else {PointerId::Custom(Uuid::new_v4())};
let circle_icon = style.cursor.handle().clone();
(
FGMenuCursor {
input_source: super::cursor::MenuCursorInputSource::Player(player_slot),
..default()
},
Node {
width: px(64),
height: px(64),
position_type: PositionType::Absolute,
..default()
},
ImageNode {
image: circle_icon,
..default()
},
pointer_id,
MeshMaterial2d(material),
Mesh2d(circle_mesh),
Transform::IDENTITY,
GlobalZIndex(1000),
Pickable::IGNORE,
@@ -47,13 +57,36 @@ impl FGMenuCursor {
}
}
/// The player using the keyboard can input things with the mouse, so we should copy their movement input
pub fn copy_mouse_input(
mut query: Query<(&FGMenuCursor, &mut PointerLocation, &PointerId)>,
assignments: Res<LocalInputAssignments>,
mut input_events: MessageReader<Pointer<Move>>
) {
for event in input_events.read() {
if let PointerId::Mouse = event.pointer_id {
for (cursor, mut location, _) in query.iter_mut() {
let is_mouse = match cursor.input_source {
MenuCursorInputSource::Any => true,
MenuCursorInputSource::Player(slot) => {
assignments.get(slot).map(|(_, source)| {
matches!(source, LocalInputSource::Keyboard)
}).unwrap_or_default()
},
};
if is_mouse {
location.location = Some(event.pointer_location.clone());
}
}
}
}
}
pub fn cursor_input(
window: Single<(Entity, &Window), With<bevy::window::PrimaryWindow>>,
query: Query<(
&mut FGMenuCursor,
&PointerLocation,
&PointerId,
)>,
query: Query<(&mut FGMenuCursor, &PointerLocation, &PointerId)>,
assignments: Res<LocalInputAssignments>,
menu_inputs: Res<MenuInputState>,
time: Res<Time>,
@@ -109,12 +142,12 @@ pub fn cursor_input(
});
}
if input.accept != cursor.accepting {
cursor.accepting = input.accept;
if input.accept.is_pressed() != cursor.accepting {
cursor.accepting = input.accept.is_pressed();
pointer_writer.write(PointerInput {
pointer_id: *pointer_id,
location: pointer_location,
action: if input.accept {
action: if input.accept.is_pressed() {
PointerAction::Press(PointerButton::Primary)
} else {
PointerAction::Release(PointerButton::Primary)
@@ -125,25 +158,15 @@ pub fn cursor_input(
}
pub fn update_cursor_transform(
camera: Single<(&Camera, &GlobalTransform), With<IsDefaultUiCamera>>,
query: Query<(&mut Transform, &PointerLocation), With<FGMenuCursor>>,
camera: Single<&Camera, With<IsDefaultUiCamera>>,
query: Query<(&mut UiTransform, &PointerLocation), With<FGMenuCursor>>,
ui_scale: Res<UiScale>,
) {
let (camera, camera_transform) = camera.into_inner();
let camera = 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);
for (mut ui_trf, location) in query {
let ui_pos = scaling::logical_to_ui_position(location.location.as_ref().map(|d| d.position).unwrap_or_default(), camera, &ui_scale);
ui_trf.translation = Val2::new(px(ui_pos.x), px(ui_pos.y));
}
}
+11
View File
@@ -0,0 +1,11 @@
use bevy::{ecs::query::QueryEntityError, reflect::Reflect};
use crate::{fighter::FighterId, menus::match_config::MenuMatchConfigErrors};
#[derive(thiserror::Error, Debug, Reflect)]
pub enum MenuErrors {
#[error("Fighter manifest not found")]
FighterManifestNotFound,
#[error("Match config error: {0:?}")]
MatchConfigError(#[from] MenuMatchConfigErrors),
}
+204 -38
View File
@@ -1,92 +1,237 @@
use bevy::prelude::*;
use std::collections::HashMap;
use crate::input::{
GamepadBinding, LocalInputSource, MenuInputMap, MenuInputMapAction,
};
use bevy::{input::gamepad::GamepadInput, prelude::*};
use crate::input::{GamepadBinding, LocalInputSource, MenuInputMap, MenuInputMapAction};
#[derive(Clone, Default, PartialEq, Eq, PartialOrd, Ord, Copy)]
pub enum MenuInputActionState {
#[default]
Released,
Pressed,
JustReleased,
JustPressed,
}
#[derive(Default, Clone)]
pub struct MenuJoystickState(HashMap<GamepadInput, f32>);
impl MenuJoystickState {
pub fn get(&self, input: impl Into<GamepadInput>) -> f32 {
self.0.get(&input.into()).map(|d| *d).unwrap_or_default()
}
pub fn from_gamepad(gamepad: &Gamepad) -> Self {
let d = gamepad
.analog()
.all_axes_and_values()
.map(|(axis, val)| (*axis, val))
.collect::<HashMap<_, _>>();
Self(d)
}
}
impl MenuInputActionState {
pub fn is_pressed(&self) -> bool {
matches!(self, MenuInputActionState::JustPressed)
|| matches!(self, MenuInputActionState::Pressed)
}
pub fn is_just_pressed(&self) -> bool {
matches!(self, MenuInputActionState::JustPressed)
}
pub fn accumulate(&mut self, other: &Self) {
if *self < *other {
*self = *other;
}
}
}
#[derive(Default, Clone)]
pub struct MenuInput {
pub movement: Vec2,
pub accept: bool,
pub back: bool,
pub start: bool,
pub movement_digital_up: MenuInputActionState,
pub movement_digital_down: MenuInputActionState,
pub movement_digital_left: MenuInputActionState,
pub movement_digital_right: MenuInputActionState,
pub accept: MenuInputActionState,
pub back: MenuInputActionState,
pub start: MenuInputActionState,
}
#[derive(Resource, Default)]
pub struct MenuInputState {
pub inputs: Vec<(LocalInputSource, MenuInput)>,
pub prev_frame_joystick_state: Vec<(LocalInputSource, MenuJoystickState)>,
}
impl MenuInputState {
pub fn get(&self, source: &LocalInputSource) -> MenuInput {
self.inputs.iter().find(|(candidate, _)| candidate == source).map(|(_, input)| input.clone()).unwrap_or_default()
self.inputs
.iter()
.find(|(candidate, _)| candidate == source)
.map(|(_, input)| input.clone())
.unwrap_or_default()
}
pub fn aggregate(&self) -> MenuInput {
self.inputs.iter().fold(MenuInput::default(), |mut result, (_, input)| {
result.movement += input.movement;
result.accept |= input.accept;
result.back |= input.back;
result.start |= input.start;
result
})
self.inputs
.iter()
.fold(MenuInput::default(), |mut result, (_, input)| {
result.accumulate(input);
result
})
}
}
impl MenuInput {
pub fn accumulate(&mut self, other: &Self) {
self.movement += other.movement;
self.accept |= other.accept;
self.back |= other.back;
self.start |= other.start;
self.accept.accumulate(&other.accept);
self.back.accumulate(&other.back);
self.start.accumulate(&other.start);
self.movement_digital_up.accumulate(&other.movement_digital_up);
self.movement_digital_down.accumulate(&other.movement_digital_down);
self.movement_digital_left.accumulate(&other.movement_digital_left);
self.movement_digital_right.accumulate(&other.movement_digital_right);
self.movement = self.movement.clamp_length_max(1.0);
}
}
fn keyboard_input(key: &ButtonInput<KeyCode>, map: &MenuInputMap) -> MenuInput {
let mut input = MenuInput::default();
let key_to_state = |key_code| {
if key.just_pressed(key_code) {
MenuInputActionState::JustPressed
} else if key.pressed(key_code) {
MenuInputActionState::Pressed
} else if key.just_released(key_code) {
MenuInputActionState::JustReleased
} else {
MenuInputActionState::Released
}
};
for binding in &map.keyboard {
let pressed = key.pressed(binding.key);
match binding.action {
MenuInputMapAction::MovementXDir(sign) => {
input.movement.x += if pressed { sign as f32 } else { 0.0 };
let state = key_to_state(binding.key);
input.movement.x += if state.is_pressed() { sign as f32 } else { 0.0 };
if sign.is_positive() {
input.movement_digital_right.accumulate(&state);
} else if sign.is_negative() {
input.movement_digital_left.accumulate(&state);
}
}
MenuInputMapAction::MovementYDir(sign) => {
input.movement.y += if pressed { sign as f32 } else { 0.0 };
let state = key_to_state(binding.key);
input.movement.y += if state.is_pressed() { sign as f32 } else { 0.0 };
if sign.is_positive() {
input.movement_digital_up.accumulate(&state);
} else if sign.is_negative() {
input.movement_digital_down.accumulate(&state);
}
}
MenuInputMapAction::Accept => input.accept |= pressed,
MenuInputMapAction::Back => input.back |= pressed,
MenuInputMapAction::Start => input.start |= pressed,
MenuInputMapAction::Accept => input.accept.accumulate(&key_to_state(binding.key)),
MenuInputMapAction::Back => input.back.accumulate(&key_to_state(binding.key)),
MenuInputMapAction::Start => input.start.accumulate(&key_to_state(binding.key)),
}
}
input.accept = map.keyboard.iter().any(|binding| matches!(binding.action, MenuInputMapAction::Accept) && key.pressed(binding.key));
input.back = map.keyboard.iter().any(|binding| matches!(binding.action, MenuInputMapAction::Back) && key.pressed(binding.key));
input.start = map.keyboard.iter().any(|binding| matches!(binding.action, MenuInputMapAction::Start) && key.pressed(binding.key));
input.movement = input.movement.normalize_or_zero();
input
}
fn gamepad_input(pad: &Gamepad, map: &MenuInputMap, deadzone: f32) -> MenuInput {
fn gamepad_input(
pad: &Gamepad,
map: &MenuInputMap,
deadzone: f32,
prev_frame: &MenuJoystickState,
) -> MenuInput {
let mut input = MenuInput::default();
let button_to_state = |button| {
if pad.just_pressed(button) {
MenuInputActionState::JustPressed
} else if pad.pressed(button) {
MenuInputActionState::Pressed
} else if pad.just_released(button) {
MenuInputActionState::JustReleased
} else {
MenuInputActionState::Released
}
};
for binding in &map.gamepad {
let (value, just_pressed) = match binding.binding {
GamepadBinding::Button(button) => (if pad.pressed(button) { 1.0 } else { 0.0 }, pad.just_pressed(button)),
GamepadBinding::Axis(axis, sign) => (pad.get(axis).unwrap_or_default() * sign as f32, false),
let (value, status) = match binding.binding {
GamepadBinding::Button(button) => {
let state = button_to_state(button);
(if state.is_pressed() { 1.0 } else { 0.0 }, state)
}
GamepadBinding::Axis(axis, sign) => {
let prev_frame = prev_frame.get(axis);
let curr_frame = pad.get(axis).unwrap_or_default();
let prev_frame_in_deadzone = prev_frame.abs() >= deadzone;
let curr_frame_in_deadzone = curr_frame.abs() >= deadzone;
let status = if prev_frame_in_deadzone && curr_frame_in_deadzone {
if prev_frame.signum() != curr_frame.signum() {
MenuInputActionState::JustPressed
} else {
MenuInputActionState::Pressed
}
} else if curr_frame_in_deadzone && !prev_frame_in_deadzone {
MenuInputActionState::JustPressed
} else if !curr_frame_in_deadzone && prev_frame_in_deadzone {
MenuInputActionState::JustReleased
} else {
MenuInputActionState::Released
};
(curr_frame * sign as f32, status)
}
};
match binding.action {
MenuInputMapAction::MovementXDir(sign) => {
input.movement.x += value * sign as f32;
if sign > 0 {
input.movement_digital_right.accumulate(&status);
} else {
input.movement_digital_left.accumulate(&status);
}
}
MenuInputMapAction::MovementYDir(sign) => {
input.movement.y += value * sign as f32;
if sign > 0 {
input.movement_digital_up.accumulate(&status);
} else {
input.movement_digital_down.accumulate(&status);
}
}
MenuInputMapAction::Accept => {
input.accept.accumulate(&status);
}
MenuInputMapAction::Back => {
input.accept.accumulate(&status);
}
MenuInputMapAction::Start => {
input.accept.accumulate(&status);
}
MenuInputMapAction::Accept => { input.accept |= value > 0.5; input.accept |= just_pressed; }
MenuInputMapAction::Back => { input.back |= value > 0.5; input.back |= just_pressed; }
MenuInputMapAction::Start => { input.start |= value > 0.5; input.start |= just_pressed; }
}
}
if input.movement.x.abs() < deadzone { input.movement.x = 0.0; }
if input.movement.y.abs() < deadzone { input.movement.y = 0.0; }
if input.movement.x.abs() < deadzone {
input.movement.x = 0.0;
}
if input.movement.y.abs() < deadzone {
input.movement.y = 0.0;
}
input.movement = input.movement.clamp_length_max(1.0);
input
}
@@ -99,8 +244,29 @@ pub fn sample_menu_inputs(
) {
state.inputs.clear();
let deadzone = 0.275;
state.inputs.push((LocalInputSource::Keyboard, keyboard_input(&key, &map)));
state
.inputs
.push((LocalInputSource::Keyboard, keyboard_input(&key, &map)));
let mut prev_joystick_states: Vec<(LocalInputSource, MenuJoystickState)> = vec![];
std::mem::swap(&mut prev_joystick_states, &mut state.prev_frame_joystick_state);
for (entity, pad) in pads.iter() {
state.inputs.push((LocalInputSource::Gamepad(entity), gamepad_input(pad, &map, deadzone)));
let local_source = LocalInputSource::Gamepad(entity);
let prev_frame_inputs = state
.prev_frame_joystick_state
.iter()
.find(|(source, _)| *source == local_source)
.map(|(_, gp)| gp.clone())
.unwrap_or(MenuJoystickState::default());
state.inputs.push((
local_source,
gamepad_input(pad, &map, deadzone, &prev_frame_inputs),
));
state
.prev_frame_joystick_state
.push((local_source, MenuJoystickState::from_gamepad(pad)));
}
}
+9 -3
View File
@@ -3,7 +3,10 @@ use bevy::{
ui::auto_directional_navigation::AutoDirectionalNavigation,
};
use crate::{AppState, menus::{MenuMarker, button::menu_button, navigation::NavigationDefaultFocus}};
use crate::{
AppState,
menus::{MenuMarker, button::menu_button, navigation::NavigationDefaultFocus},
};
#[derive(SceneComponent, Default, Clone)]
pub struct MainMenu;
@@ -44,6 +47,10 @@ impl MainMenu {
on(|_event: On<Pointer<Press>>, mut next_state: ResMut<NextState<AppState>>,| next_state.set(AppState::CharacterSelect))
NavigationDefaultFocus
),
(
menu_button("Versus2")
on(|_event: On<Pointer<Press>>, mut next_state: ResMut<NextState<AppState>>,| next_state.set(AppState::CharacterSelect))
),
]
),
]
@@ -51,5 +58,4 @@ impl MainMenu {
}
}
pub fn setup_main_menu(mut commands: Commands) {
}
pub fn setup_main_menu(mut commands: Commands) {}
+37
View File
@@ -0,0 +1,37 @@
use bevy::prelude::*;
use thiserror::Error;
use crate::fighter::FighterId;
pub struct MenuMatchPlayerSlot {
pub slot: usize,
pub fighter: Option<FighterId>
}
#[derive(Resource, Default)]
pub struct MenuMatchConfig {
pub players: Vec<MenuMatchPlayerSlot>
}
#[derive(Error, Debug, Reflect)]
pub enum MenuMatchConfigErrors {
#[error("Fighter slot {0} was invalid!")]
PlayerSlotInvalid(usize)
}
impl MenuMatchConfig {
pub fn set_player_fighter(&mut self, player_slot: usize, fighter_id: Option<FighterId>) -> Result<(), MenuMatchConfigErrors> {
let slot = self.players.get_mut(player_slot).ok_or(MenuMatchConfigErrors::PlayerSlotInvalid(player_slot))?;
slot.fighter = fighter_id;
Ok(())
}
pub fn get_player_fighter(&self, player_slot: usize) -> Result<Option<FighterId>, MenuMatchConfigErrors> {
let slot = self.players.get(player_slot).ok_or(MenuMatchConfigErrors::PlayerSlotInvalid(player_slot))?;
Ok(slot.fighter)
}
pub fn all_players_ready(&self) -> bool {
let has_unselected_fighter = self.players.iter().any(|player_slot| player_slot.fighter.is_none());
!has_unselected_fighter
}
}
+15 -5
View File
@@ -7,6 +7,7 @@ use bevy::{
};
use bevy::prelude::*;
use ron_asset_manager::RonAssetPlugin;
use crate::{
AppState,
@@ -21,6 +22,9 @@ pub mod main_menu;
pub mod navigation;
pub mod scaling;
pub mod style;
pub mod match_config;
pub mod errors;
pub mod stage_select;
#[derive(Component, Clone, Default)]
pub struct MenuMarker {}
@@ -54,7 +58,7 @@ pub fn wrap_menu(menu: impl Scene) -> impl Scene {
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) }
//padding: UiRect { left: vmin(5.0), right: vmin(5.0), top: vmin(5.0), bottom: vmin(5.0) }
}
menu
)
@@ -161,15 +165,21 @@ impl Plugin for MenuPlugin {
)
.add_systems(
PostUpdate,
(character_select::css_input, cursor::update_cursor_transform)
(
character_select::css_auto_assign_slots,
cursor::copy_mouse_input,
cursor::update_cursor_transform,
character_select::copy_cursor_transform_to_token,
)
.chain()
.run_if(in_state(AppState::CharacterSelect))
.run_if(any_with_component::<CharacterSelectScreen>),
)
.add_systems(OnExit(AppState::CharacterSelect), cursor::despawn_cursors)
.add_observer(character_select::handle_fighter_slot_addition)
.add_plugins(bevy_common_assets::ron::RonAssetPlugin::<FGUiStyle>::new(
&["stylesheet.ron"],
))
.add_observer(character_select::select_fighter.run_if(in_state(AppState::CharacterSelect)))
.add_observer(character_select::update_start_banner_visibility.run_if(in_state(AppState::CharacterSelect)))
.add_plugins(RonAssetPlugin::<FGUiStyle>::create("stylesheet.ron"))
.add_systems(
Update,
(
+36 -9
View File
@@ -1,6 +1,17 @@
use std::collections::HashSet;
use bevy::{camera::NormalizedRenderTarget, input_focus::{InputFocus, InputFocusVisible}, math::CompassOctant, picking::{backend::HitData, pointer::{Location, PointerId}}, prelude::*, ui::auto_directional_navigation::AutoDirectionalNavigator};
use super::input::MenuInputState;
use bevy::{
camera::NormalizedRenderTarget,
input_focus::{InputFocus, InputFocusVisible},
math::CompassOctant,
picking::{
backend::HitData,
pointer::{Location, PointerId},
},
prelude::*,
ui::auto_directional_navigation::AutoDirectionalNavigator,
};
use bevy_egui::egui::IntoAtoms;
use std::collections::HashSet;
// Action state and input handling
#[derive(Debug, PartialEq, Eq, Hash)]
@@ -12,8 +23,7 @@ enum DirectionalNavigationAction {
Select,
}
impl DirectionalNavigationAction {
}
impl DirectionalNavigationAction {}
#[derive(Default, Resource)]
pub struct UINavigationActionState {
@@ -37,7 +47,21 @@ pub fn process_inputs(
) {
action_state.pressed_actions.clear();
let input = menu_inputs.aggregate();
/*let repeat_press = if has_direction && !initial_press {
let has_direction = input.movement.length() != 0.0;
let mut dir_distances = [
(input.movement_digital_up, input.movement.dot(Vec2::Y)),
(input.movement_digital_down, input.movement.dot(Vec2::NEG_Y)),
(input.movement_digital_left, input.movement.dot(Vec2::NEG_X)),
(input.movement_digital_right, input.movement.dot(Vec2::X)),
];
let dir_distances_slice = dir_distances.as_mut_slice();
dir_distances_slice.sort_by(|a, b| a.1.total_cmp(&b.1));
let (curr_dir, _) = dir_distances_slice.last().copied().unwrap();
let repeat_press = if has_direction && !curr_dir.is_just_pressed() {
repeat.held_for += time.delta_secs();
if !repeat.repeating && repeat.held_for >= 0.3 {
repeat.repeating = true;
@@ -51,19 +75,19 @@ pub fn process_inputs(
if !has_direction {
repeat.held_for = 0.0;
repeat.repeating = false;
} else if initial_press {
} else if curr_dir.is_just_pressed() {
repeat.held_for = 0.0;
repeat.repeating = false;
}
false
};
if initial_press || repeat_press {
if curr_dir.is_just_pressed() || repeat_press {
if input.movement.y > 0.0 { action_state.pressed_actions.insert(DirectionalNavigationAction::Up); }
if input.movement.y < 0.0 { action_state.pressed_actions.insert(DirectionalNavigationAction::Down); }
if input.movement.x < 0.0 { action_state.pressed_actions.insert(DirectionalNavigationAction::Left); }
if input.movement.x > 0.0 { action_state.pressed_actions.insert(DirectionalNavigationAction::Right); }
}
if input.accept { action_state.pressed_actions.insert(DirectionalNavigationAction::Select); }*/
if input.accept.is_pressed() { action_state.pressed_actions.insert(DirectionalNavigationAction::Select); }
}
const FOCUSED_BORDER: Srgba = bevy::color::palettes::tailwind::BLUE_50;
@@ -117,7 +141,10 @@ pub fn navigate(
}
}
pub fn give_default_focus(query: Query<Entity, Added<NavigationDefaultFocus>>, mut input_focus: ResMut<InputFocus>) {
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);
}
+25 -5
View File
@@ -15,15 +15,35 @@ fn compute_ui_scale(window_size: Vec2) -> f32 {
}
pub fn ui_update_scale_on_startup(window: Single<&Window>, mut ui_scale: ResMut<UiScale>) {
ui_scale.0 = compute_ui_scale(window.size());
ui_scale.0 = compute_ui_scale(window.size());
}
pub fn ui_scaling_system(mut resize_reader: MessageReader<WindowResized>, mut ui_scale: ResMut<UiScale>) {
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
break;
}
}
pub fn world_to_ui_position(pos: Vec2, camera: &Camera, camera_transform: &GlobalTransform, ui_scale: &UiScale) -> Vec2 {
let viewport_position = camera.world_to_viewport(camera_transform, Vec3::new(pos.x, pos.y, 0.0)).unwrap();
let viewport_rect = camera.logical_viewport_rect().unwrap();
let ui_position = (viewport_position - viewport_rect.min) / ui_scale.0;
ui_position
}
pub fn logical_to_ui_position(pos: Vec2, camera: &Camera, ui_scale: &UiScale) -> Vec2 {
let viewport_rect = camera.logical_viewport_rect().unwrap();
let ui_position = (pos - viewport_rect.min) / ui_scale.0;
ui_position
}
+25 -7
View File
@@ -2,6 +2,9 @@ use std::collections::HashMap;
use bevy::prelude::*;
use serde::{Deserialize, Serialize};
use ron_asset_manager::prelude::*;
use crate::texture_reference::TextureReference;
pub const H1_SIZE: i32 = 72;
pub const H2_SIZE: i32 = 54;
@@ -15,7 +18,7 @@ pub struct BoxShadowStyle {
offset_px_x: i32,
offset_px_y: i32,
spread_radius: f32,
blur_radius: f32
blur_radius: f32,
}
#[derive(Serialize, Deserialize, Reflect, Default)]
@@ -25,7 +28,7 @@ pub struct ButtonStyle {
border_color: Color,
text_color: Color,
box_shadow: BoxShadowStyle,
border_size: i32
border_size: i32,
}
#[derive(Serialize, Deserialize, Reflect)]
@@ -44,7 +47,13 @@ impl ButtonStyle {
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))
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),
),
));
}
}
@@ -53,10 +62,19 @@ impl ButtonStyle {
pub enum FGUiButtonType {
#[default]
MainMenu,
CharacterSelectCharacter
CharacterSelectCharacter,
CharacterSelectStartBanner,
}
#[derive(Asset, Resource, Reflect, Deserialize, Serialize)]
#[derive(Asset, Resource, Reflect, Deserialize, RonAsset)]
pub struct FGUiStyle {
pub button_styles: HashMap<FGUiButtonType, ButtonStyles>
}
pub button_styles: HashMap<FGUiButtonType, ButtonStyles>,
#[asset]
#[dependency]
#[reflect(ignore)]
pub cursor: TextureReference,
#[asset]
#[dependency]
#[reflect(ignore)]
pub css_token: TextureReference,
}
+61
View File
@@ -0,0 +1,61 @@
use bevy::{
asset::{LoadContext, UntypedAssetId, VisitAssetDependencies},
image::{ImageSampler, ImageSamplerDescriptor},
prelude::*,
};
use ron_asset_manager::{Shandle, prelude::RonAsset};
use serde::Deserialize;
#[derive(Clone, Deserialize, Default)]
pub struct TextureReference {
pub path: String,
#[serde(default)]
pub sampling: TextureSampling,
#[serde(skip)]
pub handle: Shandle<Image>,
}
#[derive(Clone, Copy, Default, Deserialize)]
pub enum TextureSampling {
#[default]
Default,
Linear,
Nearest,
}
impl TextureReference {
pub fn handle(&self) -> &Handle<Image> {
&self.handle.handle
}
fn load(&mut self, load_context: &mut LoadContext<'_>) {
let sampling = self.sampling;
self.handle.handle = load_context
.load_builder()
.with_settings(move |settings: &mut bevy::image::ImageLoaderSettings| {
settings.sampler = match sampling {
TextureSampling::Default => ImageSampler::Default,
TextureSampling::Linear => {
ImageSampler::Descriptor(ImageSamplerDescriptor::linear())
}
TextureSampling::Nearest => {
ImageSampler::Descriptor(ImageSamplerDescriptor::nearest())
}
};
})
.load(self.path.clone())
}
}
impl RonAsset for TextureReference {
fn load_assets(&mut self, load_context: &mut LoadContext<'_>) {
self.load(load_context);
self.handle.path = self.path.clone();
}
}
impl VisitAssetDependencies for TextureReference {
fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) {
self.handle.handle.visit_dependencies(visit);
}
}