diff --git a/src/fighter/manifest.rs b/src/fighter/manifest.rs index 3dff536..d929397 100644 --- a/src/fighter/manifest.rs +++ b/src/fighter/manifest.rs @@ -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, diff --git a/src/lib.rs b/src/lib.rs index 6e76f30..1a1abbb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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::::new(&["fighter.ron"]), - bevy_common_assets::ron::RonAssetPlugin::::new(&["stage.ron"]), - bevy_common_assets::ron::RonAssetPlugin::::new(&["ron"]), + RonAssetPlugin::::create("fighter.ron"), + RonAssetPlugin::::create("stage.ron"), + RonAssetPlugin::::create("ron"), )) .add_plugins(menus::MenuPlugin) .add_loading_state( @@ -158,4 +159,4 @@ fn wait_for_asset_processor( { next_state.set(AppState::LoadCommonAssets); } -} \ No newline at end of file +} diff --git a/src/menus/character_select/character_box.rs b/src/menus/character_select/character_box.rs index 3bd02af..60dc6c1 100644 --- a/src/menus/character_select/character_box.rs +++ b/src/menus/character_select/character_box.rs @@ -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, + pub fighter_manifest: Handle, + pub fighter_id: FighterId } impl CharacterBox { - pub fn scene(manifest_handle: Handle, manifests: &Res>) -> impl Scene { - let manifest = manifests.get(&manifest_handle).expect("Fighter manifest should be valid"); + pub fn scene( + manifest_handle: Handle, + manifests: &Res>, + ) -> 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 [ diff --git a/src/menus/character_select/errors.rs b/src/menus/character_select/errors.rs new file mode 100644 index 0000000..1a11ce9 --- /dev/null +++ b/src/menus/character_select/errors.rs @@ -0,0 +1,3 @@ +pub enum CharacterSelectErrors { + +} \ No newline at end of file diff --git a/src/menus/character_select/fighter_portrait.rs b/src/menus/character_select/fighter_portrait.rs index 2357101..7c00e17 100644 --- a/src/menus/character_select/fighter_portrait.rs +++ b/src/menus/character_select/fighter_portrait.rs @@ -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) ] } -} \ No newline at end of file +} diff --git a/src/menus/character_select/mod.rs b/src/menus/character_select/mod.rs index 9242cbd..94898c3 100644 --- a/src/menus/character_select/mod.rs +++ b/src/menus/character_select/mod.rs @@ -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>>, mut commands: Commands, manifest_registry: Res, assignments: Res, manifests: Res>, - mut meshes: ResMut>, - mut materials: ResMut>, + common_assets: Res, + styles: Res>, ) { let character_boxes = manifest_registry .iter() .map(|(_, v)| character_box::CharacterBox::scene(v.clone(), &manifests)) .collect::>(); - // Create existing portraits - let fighter_portraits = assignments - .iter() - .map(|(slot, _)| fighter_portrait::FighterPortrait::create(*slot)) - .collect::>(); - - 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>, mut next_state: ResMut>| 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::(); 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>, - mut materials: ResMut>, + 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, + addition: On, query: Single<&CharacterSelectScreen>, commands: Commands, - meshes: ResMut>, - materials: ResMut>, + common_assets: Res, + styles: Res>, ) { 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, mut assignments: ResMut, + mut match_config: ResMut, 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>, + character_boxes: Query<&CharacterBox>, + manifests: Res>, + mut match_config: ResMut, + 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, + banner: Single>, + mut commands: Commands, + config: Res, +) { + 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, + cursors: Query<&UiGlobalTransform, With>, + cam: Single<&Camera, With>, + ui_scale: Res, +) -> 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(()) +} diff --git a/src/menus/character_select/token.rs b/src/menus/character_select/token.rs new file mode 100644 index 0000000..ce242b9 --- /dev/null +++ b/src/menus/character_select/token.rs @@ -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() + } + ) + } +} \ No newline at end of file diff --git a/src/menus/cursor.rs b/src/menus/cursor.rs index cb85b48..5dc4d7c 100644 --- a/src/menus/cursor.rs +++ b/src/menus/cursor.rs @@ -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, - materials: &mut Assets, + 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, + mut input_events: MessageReader> +) { + + 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>, - query: Query<( - &mut FGMenuCursor, - &PointerLocation, - &PointerId, - )>, + query: Query<(&mut FGMenuCursor, &PointerLocation, &PointerId)>, assignments: Res, menu_inputs: Res, time: Res