restore after broken git
This commit is contained in:
commit
138c62ac34
51 changed files with 7559 additions and 0 deletions
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
/target
|
||||||
|
/assets
|
||||||
|
.idea
|
||||||
|
.directory
|
||||||
5995
Cargo.lock
generated
Normal file
5995
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
30
Cargo.toml
Normal file
30
Cargo.toml
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
[package]
|
||||||
|
name = "rgg-zuma"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
bevy = "0.18.1"
|
||||||
|
rand = "0.10.0"
|
||||||
|
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "linux")'.dependencies.bevy]
|
||||||
|
version = "0.18.1"
|
||||||
|
default-features = false
|
||||||
|
features = [
|
||||||
|
"wayland",
|
||||||
|
"x11",
|
||||||
|
]
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "windows")'.dependencies.bevy]
|
||||||
|
version = "0.18.1"
|
||||||
|
default-features = false
|
||||||
|
features = []
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
opt-level = 3
|
||||||
|
lto = "fat"
|
||||||
|
codegen-units = 1
|
||||||
|
panic = "abort"
|
||||||
|
strip = true
|
||||||
|
debug = false
|
||||||
7
src/common/components.rs
Normal file
7
src/common/components.rs
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
#[derive(Component, Clone, Copy)]
|
||||||
|
pub struct CommonCloseApp;
|
||||||
|
|
||||||
|
#[derive(Component, Clone, Copy)]
|
||||||
|
pub struct ExitToMainMenu;
|
||||||
4
src/common/messages.rs
Normal file
4
src/common/messages.rs
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
use bevy::prelude::Message;
|
||||||
|
|
||||||
|
#[derive(Message)]
|
||||||
|
pub struct ExitRequestMessage;
|
||||||
8
src/common/mod.rs
Normal file
8
src/common/mod.rs
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
pub mod systems;
|
||||||
|
|
||||||
|
pub mod components;
|
||||||
|
pub mod plugin;
|
||||||
|
pub mod messages;
|
||||||
|
|
||||||
|
pub use components::*;
|
||||||
|
|
||||||
35
src/common/plugin.rs
Normal file
35
src/common/plugin.rs
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
use crate::common::systems::*;
|
||||||
|
use crate::common::*;
|
||||||
|
use crate::states::AppState;
|
||||||
|
use crate::ui::button_click::ButtonClickMessage;
|
||||||
|
use crate::ui::button_hover::button_hover;
|
||||||
|
use crate::ui::click::handle_click_system;
|
||||||
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub struct CommonButtonSet;
|
||||||
|
|
||||||
|
pub struct CommonUiPlugin;
|
||||||
|
|
||||||
|
impl Plugin for CommonUiPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.add_message::<ButtonClickMessage<CommonCloseApp>>()
|
||||||
|
.add_message::<ButtonClickMessage<ExitToMainMenu>>()
|
||||||
|
.add_systems(
|
||||||
|
Update,
|
||||||
|
(
|
||||||
|
button_hover,
|
||||||
|
handle_click_system::<CommonCloseApp>,
|
||||||
|
handle_click_system::<ExitToMainMenu>,
|
||||||
|
exit_to_main_menu_system,
|
||||||
|
close_app_system,
|
||||||
|
)
|
||||||
|
.in_set(CommonButtonSet),
|
||||||
|
)
|
||||||
|
.configure_sets(
|
||||||
|
Update,
|
||||||
|
CommonButtonSet
|
||||||
|
.run_if(in_state(AppState::MainMenu).or(in_state(AppState::SettingsMenu))),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
38
src/common/systems.rs
Normal file
38
src/common/systems.rs
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
use crate::common::{CommonCloseApp, ExitToMainMenu};
|
||||||
|
use crate::common::messages::ExitRequestMessage;
|
||||||
|
use crate::states::AppState;
|
||||||
|
use crate::ui::button_click::ButtonClickMessage;
|
||||||
|
|
||||||
|
pub fn exit_to_main_menu_system(
|
||||||
|
interaction_query: Query<
|
||||||
|
&Interaction,
|
||||||
|
(Changed<Interaction>, With<Button>, With<ExitToMainMenu>),
|
||||||
|
>,
|
||||||
|
mut next_state: ResMut<NextState<AppState>>,
|
||||||
|
) {
|
||||||
|
for interaction in &interaction_query {
|
||||||
|
if matches!(interaction, Interaction::Pressed) {
|
||||||
|
next_state.set(AppState::MainMenu);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn close_app_system(
|
||||||
|
mut messages: MessageReader<ButtonClickMessage<CommonCloseApp>>,
|
||||||
|
mut exit_request: MessageWriter<ExitRequestMessage>,
|
||||||
|
) {
|
||||||
|
for _msg in messages.read() {
|
||||||
|
exit_request.write(ExitRequestMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn exit_system(
|
||||||
|
mut requests: MessageReader<ExitRequestMessage>,
|
||||||
|
mut app_exit: MessageWriter<AppExit>,
|
||||||
|
) {
|
||||||
|
for _ in requests.read() {
|
||||||
|
app_exit.write(AppExit::Success);
|
||||||
|
}
|
||||||
|
}
|
||||||
0
src/gameplay/mod.rs
Normal file
0
src/gameplay/mod.rs
Normal file
56
src/main.rs
Normal file
56
src/main.rs
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
use crate::common::messages::ExitRequestMessage;
|
||||||
|
use crate::common::plugin::CommonUiPlugin;
|
||||||
|
use crate::common::systems::exit_system;
|
||||||
|
use bevy::camera::ScalingMode;
|
||||||
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
|
||||||
|
mod common;
|
||||||
|
mod gameplay;
|
||||||
|
mod states;
|
||||||
|
mod ui;
|
||||||
|
|
||||||
|
use crate::states::game::state::MainGameState;
|
||||||
|
use crate::states::main_menu::state::MainMenuState;
|
||||||
|
use crate::states::settings_menu::state::SettingsMenuState;
|
||||||
|
|
||||||
|
const FACTOR: u32 = 80;
|
||||||
|
const WIDTH: u32 = 16;
|
||||||
|
const HEIGHT: u32 = 9;
|
||||||
|
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
App::new()
|
||||||
|
.add_message::<ExitRequestMessage>()
|
||||||
|
.add_plugins(DefaultPlugins.set(WindowPlugin {
|
||||||
|
primary_window: Some(Window {
|
||||||
|
title: "alpha".into(),
|
||||||
|
resolution: (WIDTH * FACTOR, HEIGHT * FACTOR).into(),
|
||||||
|
resizable: true,
|
||||||
|
..default()
|
||||||
|
}),
|
||||||
|
..default()
|
||||||
|
}))
|
||||||
|
.add_plugins((
|
||||||
|
CommonUiPlugin,
|
||||||
|
MainMenuState,
|
||||||
|
SettingsMenuState,
|
||||||
|
MainGameState,
|
||||||
|
))
|
||||||
|
.add_systems(Startup, setup)
|
||||||
|
.add_systems(Update, exit_system)
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup(mut commands: Commands) {
|
||||||
|
let projection = Projection::Orthographic(OrthographicProjection {
|
||||||
|
scaling_mode: ScalingMode::Fixed {
|
||||||
|
width: (WIDTH * FACTOR) as f32,
|
||||||
|
height: (HEIGHT * FACTOR) as f32,
|
||||||
|
},
|
||||||
|
scale: 1.00,
|
||||||
|
..OrthographicProjection::default_2d()
|
||||||
|
});
|
||||||
|
|
||||||
|
commands.spawn((Camera2d, projection));
|
||||||
|
}
|
||||||
11
src/states/app_states.rs
Normal file
11
src/states/app_states.rs
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
use bevy::prelude::States;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Hash, States)]
|
||||||
|
pub enum AppState {
|
||||||
|
#[default]
|
||||||
|
MainMenu,
|
||||||
|
SettingsMenu,
|
||||||
|
GameState,
|
||||||
|
GameRestart,
|
||||||
|
LevelState,
|
||||||
|
}
|
||||||
14
src/states/game/components.rs
Normal file
14
src/states/game/components.rs
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
use bevy::prelude::Component;
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct GameStateRootMarker;
|
||||||
|
|
||||||
|
//FIXME распилить на отдельные структуры-маркеры
|
||||||
|
#[derive(Component, Clone, Copy)]
|
||||||
|
#[derive(PartialEq)]
|
||||||
|
pub enum InGameButtonAction {
|
||||||
|
Restart,
|
||||||
|
ExitToMainMenu,
|
||||||
|
}
|
||||||
|
#[derive(Component, Copy, Clone)]
|
||||||
|
pub struct GameRestart;
|
||||||
4
src/states/game/mod.rs
Normal file
4
src/states/game/mod.rs
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
pub mod state;
|
||||||
|
pub mod components;
|
||||||
|
pub mod systems;
|
||||||
|
pub mod plugin;
|
||||||
23
src/states/game/plugin.rs
Normal file
23
src/states/game/plugin.rs
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
use bevy::prelude::*;
|
||||||
|
use crate::states::game::components::GameRestart;
|
||||||
|
use crate::states::level::plugin::LevelPlugin;
|
||||||
|
use crate::ui::button_click::ButtonClickMessage;
|
||||||
|
use crate::ui::click::handle_click_system;
|
||||||
|
|
||||||
|
#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub struct MainGameStateButtonSet;
|
||||||
|
|
||||||
|
pub struct InGameUiPlugin;
|
||||||
|
|
||||||
|
impl Plugin for InGameUiPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.add_message::<ButtonClickMessage<GameRestart>>()
|
||||||
|
.add_plugins(LevelPlugin)
|
||||||
|
.add_systems(
|
||||||
|
Update,
|
||||||
|
(
|
||||||
|
handle_click_system::<GameRestart>,
|
||||||
|
).in_set(MainGameStateButtonSet)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
109
src/states/game/state.rs
Normal file
109
src/states/game/state.rs
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
use bevy::prelude::*;
|
||||||
|
use crate::states::AppState;
|
||||||
|
use crate::states::game::components::{GameRestart, GameStateRootMarker};
|
||||||
|
use crate::states::game::systems::restart_game_button_system;
|
||||||
|
use crate::ui::{ButtonStyle, spawn_background, spawn_button};
|
||||||
|
use crate::states::game::plugin::InGameUiPlugin;
|
||||||
|
use crate::ui::button_hover::button_hover;
|
||||||
|
|
||||||
|
pub struct MainGameState;
|
||||||
|
|
||||||
|
impl Plugin for MainGameState {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.init_state::<AppState>()
|
||||||
|
.add_plugins(InGameUiPlugin)
|
||||||
|
.add_plugins(())
|
||||||
|
// .add_systems(OnEnter(AppState::GameRestart), auto_restart)
|
||||||
|
.add_systems(
|
||||||
|
OnEnter(AppState::GameState),
|
||||||
|
(
|
||||||
|
setup_game,
|
||||||
|
// (setup_track, spawn_ball).chain(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.add_systems(
|
||||||
|
Update,
|
||||||
|
(
|
||||||
|
button_hover,
|
||||||
|
restart_game_button_system
|
||||||
|
).run_if(in_state(AppState::GameState)),
|
||||||
|
)
|
||||||
|
.add_systems(OnExit(AppState::GameState), cleanup_game)
|
||||||
|
.add_systems(OnEnter(AppState::GameRestart), auto_restart);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_game(mut commands: Commands, asset_server: Res<AssetServer>) {
|
||||||
|
//всё окно
|
||||||
|
let root = commands
|
||||||
|
.spawn((
|
||||||
|
Node {
|
||||||
|
display: Display::Flex,
|
||||||
|
width: Val::Percent(100.0),
|
||||||
|
height: Val::Percent(100.0),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
BackgroundColor(Color::NONE),
|
||||||
|
Name::new("Game state background node"),
|
||||||
|
GameStateRootMarker,
|
||||||
|
))
|
||||||
|
.id();
|
||||||
|
|
||||||
|
// область меню
|
||||||
|
let menu_root = commands
|
||||||
|
.spawn((
|
||||||
|
Node {
|
||||||
|
display: Display::Flex,
|
||||||
|
width: Val::Auto,
|
||||||
|
height: Val::Auto,
|
||||||
|
padding: UiRect::all(Val::Px(10.0)),
|
||||||
|
row_gap: Val::Px(5.0),
|
||||||
|
position_type: PositionType::Absolute,
|
||||||
|
left: Val::Percent(5.0),
|
||||||
|
top: Val::Percent(5.0),
|
||||||
|
align_items: AlignItems::Center,
|
||||||
|
flex_direction: FlexDirection::Column,
|
||||||
|
justify_content: JustifyContent::FlexEnd,
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
BackgroundColor(Color::linear_rgba(0.15, 0.15, 0.15, 0.5)),
|
||||||
|
Name::new("Game menu buttons node"),
|
||||||
|
))
|
||||||
|
.id();
|
||||||
|
commands.entity(root).add_child(menu_root);
|
||||||
|
|
||||||
|
//бэкграунд
|
||||||
|
let bg: Handle<Image> = asset_server.load("bg/game.png");
|
||||||
|
let bg_entity = spawn_background(&mut commands, bg, GameStateRootMarker);
|
||||||
|
|
||||||
|
let mm_button_style = ButtonStyle {
|
||||||
|
font: asset_server.load("fonts/QR Ames Beta.otf"),
|
||||||
|
font_size: 24.0,
|
||||||
|
text_color: Color::WHITE,
|
||||||
|
normal_bg: Color::linear_rgba(0.15, 0.15, 0.15, 1.0),
|
||||||
|
hovered_bg: Color::linear_rgb(0.25, 0.25, 0.25),
|
||||||
|
pressed_bg: Color::linear_rgb(0.3, 0.3, 0.3),
|
||||||
|
width: Val::Px(200.0),
|
||||||
|
height: Val::Px(50.0),
|
||||||
|
margin: UiRect::all(Val::Px(10.0)),
|
||||||
|
};
|
||||||
|
|
||||||
|
spawn_button(
|
||||||
|
&mut commands,
|
||||||
|
menu_root,
|
||||||
|
"Restart",
|
||||||
|
&mm_button_style,
|
||||||
|
GameRestart,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cleanup_game(mut commands: Commands, query: Query<Entity, With<GameStateRootMarker>>) {
|
||||||
|
for entity in &query {
|
||||||
|
commands.entity(entity).despawn_children();
|
||||||
|
commands.entity(entity).despawn();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn auto_restart(mut next_state: ResMut<NextState<AppState>>) {
|
||||||
|
next_state.set(AppState::GameState);
|
||||||
|
}
|
||||||
16
src/states/game/systems.rs
Normal file
16
src/states/game/systems.rs
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
use bevy::prelude::*;
|
||||||
|
use crate::states::AppState;
|
||||||
|
use crate::states::game::components::GameRestart;
|
||||||
|
|
||||||
|
|
||||||
|
pub fn restart_game_button_system(
|
||||||
|
interaction_query: Query<&Interaction, (Changed<Interaction>, With<GameRestart>)>,
|
||||||
|
mut next_state: ResMut<NextState<AppState>>,
|
||||||
|
) {
|
||||||
|
for interaction in &interaction_query {
|
||||||
|
if matches!(interaction, Interaction::Pressed) {
|
||||||
|
println!("🔄 Кнопка Restart нажата! Переход в Restarting...");
|
||||||
|
next_state.set(AppState::GameRestart);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
99
src/states/level/components.rs
Normal file
99
src/states/level/components.rs
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
use bevy::prelude::*;
|
||||||
|
use rand::seq::{IndexedRandom, SliceRandom};
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct Level {
|
||||||
|
track: TrackPath,
|
||||||
|
target_score: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct LevelMarker;
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct DebugSlotNumber;
|
||||||
|
|
||||||
|
#[derive(Resource)]
|
||||||
|
pub struct TrackPath {
|
||||||
|
pub points: Vec<Vec2>,
|
||||||
|
pub spawn_index: usize,
|
||||||
|
pub buffer_size: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
|
||||||
|
pub enum BallType {
|
||||||
|
#[default]
|
||||||
|
First,
|
||||||
|
Second,
|
||||||
|
Third,
|
||||||
|
Forth,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BallType {
|
||||||
|
pub const fn asset_path(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
BallType::First => "balls/1.png",
|
||||||
|
BallType::Second => "balls/2.png",
|
||||||
|
BallType::Third => "balls/3.png",
|
||||||
|
BallType::Forth => "balls/4.png",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn random() -> Self {
|
||||||
|
[Self::First, Self::Second, Self::Third, Self::Forth]
|
||||||
|
.choose(&mut rand::rng())
|
||||||
|
.copied()
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component, Debug)]
|
||||||
|
pub struct Ball {
|
||||||
|
pub ball_type: BallType,
|
||||||
|
pub slot_index: usize,
|
||||||
|
pub slot_progress: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Resource)]
|
||||||
|
pub struct SpawnTimer {
|
||||||
|
pub timer: Timer,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for SpawnTimer {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
timer: Timer::from_seconds(2.0, TimerMode::Repeating),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Resource)]
|
||||||
|
pub struct WaveState {
|
||||||
|
pub spawning_allowed: bool,
|
||||||
|
pub ball_reached_end: bool,
|
||||||
|
pub is_warming_up: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for WaveState {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self{
|
||||||
|
spawning_allowed: false,
|
||||||
|
ball_reached_end: false,
|
||||||
|
is_warming_up: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct WarmupTarget {
|
||||||
|
pub target_index: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct Cannon;
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct BallProjectile {
|
||||||
|
pub velocity: Vec2,
|
||||||
|
pub previous_position: Vec2,
|
||||||
|
pub ball_type: BallType,
|
||||||
|
}
|
||||||
8
src/states/level/mod.rs
Normal file
8
src/states/level/mod.rs
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
pub mod plugin;
|
||||||
|
pub mod components;
|
||||||
|
pub mod system;
|
||||||
|
pub mod state;
|
||||||
|
pub mod system_ball;
|
||||||
|
pub mod system_cannon;
|
||||||
|
pub mod system_track;
|
||||||
|
pub mod system_grid;
|
||||||
42
src/states/level/plugin.rs
Normal file
42
src/states/level/plugin.rs
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
use crate::states::AppState::GameState;
|
||||||
|
use crate::states::level::system_ball::*;
|
||||||
|
use crate::states::level::system::*;
|
||||||
|
use bevy::prelude::*;
|
||||||
|
use crate::states::level::system_cannon::*;
|
||||||
|
use crate::states::level::components::LevelMarker;
|
||||||
|
use crate::states::level::system_track::{debug_draw_track};
|
||||||
|
|
||||||
|
pub struct LevelPlugin;
|
||||||
|
|
||||||
|
impl Plugin for LevelPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.add_systems(OnEnter(GameState), (
|
||||||
|
setup_level,
|
||||||
|
initialize_queue,
|
||||||
|
setup_cannon).chain())
|
||||||
|
.add_systems(
|
||||||
|
Update,
|
||||||
|
(
|
||||||
|
// debug_draw_grid,
|
||||||
|
debug_draw_track,
|
||||||
|
warmup_queue_movement, //до спавна остальных шариков
|
||||||
|
spawn_new_ball,
|
||||||
|
move_queue_along_track,
|
||||||
|
check_wave_completion,
|
||||||
|
rotate_cannon,
|
||||||
|
spawn_projectile_from_cannon,
|
||||||
|
move_projectiles,
|
||||||
|
detect_projectile_hit,
|
||||||
|
)
|
||||||
|
.run_if(in_state(GameState)),
|
||||||
|
)
|
||||||
|
.add_systems(OnExit(GameState), cleanup_main_menu);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cleanup_main_menu(mut commands: Commands, query: Query<Entity, With<LevelMarker>>) {
|
||||||
|
for entity in &query {
|
||||||
|
commands.entity(entity).despawn_children();
|
||||||
|
commands.entity(entity).despawn();
|
||||||
|
}
|
||||||
|
}
|
||||||
0
src/states/level/state.rs
Normal file
0
src/states/level/state.rs
Normal file
18
src/states/level/system.rs
Normal file
18
src/states/level/system.rs
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
|
||||||
|
use crate::states::level::components::*;
|
||||||
|
use crate::{FACTOR, HEIGHT, WIDTH};
|
||||||
|
use bevy::prelude::*;
|
||||||
|
use crate::states::level::system_track::setup_track_with_buffer;
|
||||||
|
|
||||||
|
pub const SHIFT: f32 = 2.0;
|
||||||
|
pub const SLOT_SIZE: f32 = FACTOR as f32 / SHIFT;
|
||||||
|
|
||||||
|
pub fn setup_timer(mut commands: Commands){
|
||||||
|
commands.insert_resource(SpawnTimer::default());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setup_level(mut commands: Commands){
|
||||||
|
commands.insert_resource(setup_track_with_buffer());
|
||||||
|
commands.insert_resource(WaveState::default());
|
||||||
|
}
|
||||||
|
|
||||||
211
src/states/level/system_ball.rs
Normal file
211
src/states/level/system_ball.rs
Normal file
|
|
@ -0,0 +1,211 @@
|
||||||
|
use crate::states::level::components::{Ball, BallType, LevelMarker, TrackPath, WarmupTarget, WaveState};
|
||||||
|
use crate::states::level::system::SLOT_SIZE;
|
||||||
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
pub fn spawn_new_ball(
|
||||||
|
mut commands: Commands,
|
||||||
|
track: Res<TrackPath>,
|
||||||
|
asset_server: Res<AssetServer>,
|
||||||
|
time: Res<Time>,
|
||||||
|
mut timer: Local<Timer>,
|
||||||
|
balls: Query<&Ball>,
|
||||||
|
wave: Res<WaveState>,
|
||||||
|
) {
|
||||||
|
if !wave.spawning_allowed {
|
||||||
|
timer.reset();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//до добавления буферных слотов
|
||||||
|
// let entry_blocked = balls
|
||||||
|
// .iter()
|
||||||
|
// .any(|ball| ball.slot_index == 0 && ball.slot_progress < 1.0);
|
||||||
|
|
||||||
|
let entry_blocked = balls.iter().any(|ball| {
|
||||||
|
ball.slot_index == track.spawn_index && ball.slot_progress < 1.0
|
||||||
|
});
|
||||||
|
|
||||||
|
if timer.is_finished() && !entry_blocked {
|
||||||
|
if let Some(&spawn_pos) = track.points.first() {
|
||||||
|
let ball_type = BallType::random();
|
||||||
|
let image: Handle<Image> = asset_server.load(ball_type.asset_path());
|
||||||
|
|
||||||
|
commands.spawn((
|
||||||
|
Sprite {
|
||||||
|
image,
|
||||||
|
custom_size: Some(Vec2::splat(SLOT_SIZE)),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
Transform::from_translation(spawn_pos.extend(10.0)),
|
||||||
|
Ball {
|
||||||
|
ball_type,
|
||||||
|
slot_index: track.spawn_index,
|
||||||
|
slot_progress: 0.0,
|
||||||
|
},
|
||||||
|
LevelMarker,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
timer.reset();
|
||||||
|
} else {
|
||||||
|
timer.tick(time.delta());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME перенести позже в более удобное место/объект левела
|
||||||
|
const SPEED: f32 = 60.0; // пикселей в секунду
|
||||||
|
|
||||||
|
pub fn move_queue_along_track(
|
||||||
|
track: Res<TrackPath>,
|
||||||
|
mut balls: Query<(&mut Ball, &mut Transform)>,
|
||||||
|
time: Res<Time>,
|
||||||
|
wave: Res<WaveState>,
|
||||||
|
) {
|
||||||
|
// проверяем, не идет ли прогрев трека
|
||||||
|
if wave.is_warming_up { return; }
|
||||||
|
|
||||||
|
for (mut ball, mut transform) in balls.iter_mut() {
|
||||||
|
// Увеличиваем прогресс
|
||||||
|
ball.slot_progress += SPEED * time.delta_secs() / SLOT_SIZE;
|
||||||
|
|
||||||
|
// Если шарик достиг конца текущего сегмента
|
||||||
|
if ball.slot_progress >= 1.0 {
|
||||||
|
// Если это въезд в трек (был в слоте 0) — переходим в слот 1
|
||||||
|
if ball.slot_index == 0 {
|
||||||
|
ball.slot_index = 1;
|
||||||
|
} else {
|
||||||
|
// Обычное движение: переходим к следующему слоту
|
||||||
|
ball.slot_index += 1;
|
||||||
|
}
|
||||||
|
ball.slot_progress = 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Вычисляем визуальную позицию
|
||||||
|
let current = track.points.get(ball.slot_index);
|
||||||
|
let next = track.points.get(ball.slot_index + 1);
|
||||||
|
|
||||||
|
if let (Some(&curr_pos), Some(&next_pos)) = (current, next) {
|
||||||
|
let pos = curr_pos.lerp(next_pos, ball.slot_progress);
|
||||||
|
transform.translation = pos.extend(transform.translation.z);
|
||||||
|
} else if let Some(&pos) = current {
|
||||||
|
// Последний слот — просто ставим в него
|
||||||
|
transform.translation = pos.extend(transform.translation.z);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME перенести позже в более удобное место/объект левела
|
||||||
|
const START_COUNT: usize = 10;
|
||||||
|
|
||||||
|
pub fn initialize_queue(
|
||||||
|
mut commands: Commands,
|
||||||
|
track: Res<TrackPath>,
|
||||||
|
asset_server: Res<AssetServer>,
|
||||||
|
mut wave: ResMut<WaveState>,
|
||||||
|
) {
|
||||||
|
// заполняем слоты от -START_COUNT до -1
|
||||||
|
let base_idx = track.spawn_index.saturating_sub(START_COUNT);
|
||||||
|
|
||||||
|
for i in 0..START_COUNT {
|
||||||
|
let slot_idx = base_idx + i; // пропускаем индекс 0 (спавн за экраном)
|
||||||
|
let Some(&pos) = track.points.get(slot_idx) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
println!("Slot {}: {:?}", slot_idx, pos); //debug
|
||||||
|
|
||||||
|
let target_idx = slot_idx + START_COUNT;
|
||||||
|
|
||||||
|
let ball_type = BallType::random();
|
||||||
|
let image: Handle<Image> = asset_server.load(ball_type.asset_path());
|
||||||
|
|
||||||
|
commands.spawn((
|
||||||
|
Sprite {
|
||||||
|
image,
|
||||||
|
custom_size: Some(Vec2::splat(SLOT_SIZE)),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
Transform::from_translation(pos.extend(10.0)),
|
||||||
|
Ball {
|
||||||
|
ball_type,
|
||||||
|
slot_index: slot_idx,
|
||||||
|
slot_progress: 0.0,
|
||||||
|
},
|
||||||
|
LevelMarker,
|
||||||
|
WarmupTarget { target_index: target_idx },
|
||||||
|
));
|
||||||
|
}
|
||||||
|
wave.is_warming_up = true;
|
||||||
|
wave.spawning_allowed = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME перенести позже в более удобное место/объект левела
|
||||||
|
const WARMUP_MULTIPLIER: f32 = 5.0;
|
||||||
|
pub fn warmup_queue_movement(
|
||||||
|
track: Res<TrackPath>,
|
||||||
|
mut wave: ResMut<WaveState>,
|
||||||
|
mut balls: ParamSet<(
|
||||||
|
Query<&Ball>,
|
||||||
|
Query<(&mut Ball, &mut Transform, &WarmupTarget)>
|
||||||
|
)>,
|
||||||
|
time: Res<Time>,
|
||||||
|
) {
|
||||||
|
if !wave.is_warming_up { return; }
|
||||||
|
|
||||||
|
let speed = SPEED * WARMUP_MULTIPLIER;
|
||||||
|
|
||||||
|
|
||||||
|
let spawn_slot = track.spawn_index.saturating_sub(1);
|
||||||
|
let spawn_slot_free = !balls.p0().iter().any(|b| b.slot_index == spawn_slot);
|
||||||
|
|
||||||
|
let mut warmup_complete = true;
|
||||||
|
|
||||||
|
for (mut ball, mut transform, target) in balls.p1().iter_mut() {
|
||||||
|
if ball.slot_index < target.target_index {
|
||||||
|
warmup_complete = false;
|
||||||
|
|
||||||
|
ball.slot_progress += speed * time.delta_secs() / SLOT_SIZE;
|
||||||
|
|
||||||
|
if ball.slot_progress >= 1.0 {
|
||||||
|
ball.slot_index += 1;
|
||||||
|
ball.slot_progress = 0.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// обновление визуальной позиции
|
||||||
|
let current = track.points.get(ball.slot_index);
|
||||||
|
let next = track.points.get(ball.slot_index + 1);
|
||||||
|
|
||||||
|
if let (Some(&curr_pos), Some(&next_pos)) = (current, next) {
|
||||||
|
let pos = curr_pos.lerp(next_pos, ball.slot_progress);
|
||||||
|
transform.translation = pos.extend(transform.translation.z);
|
||||||
|
} else if let Some(&pos) = current {
|
||||||
|
transform.translation = pos.extend(transform.translation.z);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if warmup_complete && spawn_slot_free {
|
||||||
|
wave.is_warming_up = false;
|
||||||
|
wave.spawning_allowed = true;
|
||||||
|
println!("Warmup complete: queue ready");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn check_wave_completion(
|
||||||
|
track: Res<TrackPath>,
|
||||||
|
mut wave: ResMut<WaveState>,
|
||||||
|
balls: Query<&Ball>,
|
||||||
|
) {
|
||||||
|
if wave.ball_reached_end {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let last_index = track.points.len() - 1;
|
||||||
|
|
||||||
|
for ball in &balls {
|
||||||
|
if ball.slot_index >= last_index {
|
||||||
|
wave.ball_reached_end = true;
|
||||||
|
wave.spawning_allowed = false;
|
||||||
|
println!("Wave completed: ball reached the end!");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
156
src/states/level/system_cannon.rs
Normal file
156
src/states/level/system_cannon.rs
Normal file
|
|
@ -0,0 +1,156 @@
|
||||||
|
use bevy::prelude::*;
|
||||||
|
use bevy::window::PrimaryWindow;
|
||||||
|
use crate::states::level::components::{Ball, BallProjectile, BallType, Cannon, LevelMarker, TrackPath};
|
||||||
|
use crate::states::level::system::SLOT_SIZE;
|
||||||
|
|
||||||
|
pub fn setup_cannon(
|
||||||
|
mut commands: Commands,
|
||||||
|
asset_server: Res<AssetServer>,
|
||||||
|
) {
|
||||||
|
let texture: Handle<Image> = asset_server.load("cannon/cannon.png");
|
||||||
|
|
||||||
|
commands.spawn((
|
||||||
|
LevelMarker,
|
||||||
|
Cannon,
|
||||||
|
Transform::from_xyz(579.0-(1280.0/2.0), (768.0/2.0)-150.0, 1.0),
|
||||||
|
Sprite::from_image(texture),
|
||||||
|
Visibility::Visible,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn rotate_cannon(
|
||||||
|
mut query: Query<&mut Transform, With<Cannon>>,
|
||||||
|
window_query: Query<&Window, With<PrimaryWindow>>,
|
||||||
|
camera_query: Query<(&Camera, &GlobalTransform)>,
|
||||||
|
) {
|
||||||
|
let Ok(mut cannon_tf) = query.single_mut() else { return };
|
||||||
|
let Ok(window) = window_query.single() else { return };
|
||||||
|
let Some(cursor_pos) = window.cursor_position() else { return };
|
||||||
|
let Ok((camera, camera_tf)) = camera_query.single() else { return };
|
||||||
|
|
||||||
|
// Конвертируем экранные координаты курсора в мировые
|
||||||
|
let Ok(world_cursor_pos) = camera.viewport_to_world_2d(camera_tf, cursor_pos) else { return };
|
||||||
|
|
||||||
|
// Вектор направления от пушки к курсору
|
||||||
|
let cannon_pos = Vec2::new(cannon_tf.translation.x, cannon_tf.translation.y);
|
||||||
|
let direction = world_cursor_pos - cannon_pos;
|
||||||
|
|
||||||
|
// Применяем вращение по оси Z
|
||||||
|
let angle = direction.to_angle() - std::f32::consts::FRAC_PI_2;
|
||||||
|
cannon_tf.rotation = Quat::from_rotation_z(angle);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn spawn_projectile_from_cannon(
|
||||||
|
mut commands: Commands,
|
||||||
|
cannon_query: Query<&Transform, With<Cannon>>,
|
||||||
|
mouse_input: Res<ButtonInput<MouseButton>>,
|
||||||
|
asset_server: Res<AssetServer>,
|
||||||
|
) {
|
||||||
|
if !mouse_input.just_pressed(MouseButton::Left) { return; }
|
||||||
|
|
||||||
|
let Ok(cannon_tf) = cannon_query.single() else { return; };
|
||||||
|
|
||||||
|
let ball_type = BallType::First; // TODO: рандом/цикл
|
||||||
|
let image = asset_server.load(ball_type.asset_path());
|
||||||
|
|
||||||
|
let offset = 100.0;
|
||||||
|
let direction = cannon_tf.rotation.mul_vec3(Vec3::Y).truncate().normalize();
|
||||||
|
let spawn_pos_2d = cannon_tf.translation.truncate() + direction * offset;
|
||||||
|
let spawn_pos = spawn_pos_2d.extend(10.0); // z=10, чтобы было поверх трека
|
||||||
|
|
||||||
|
commands.spawn((
|
||||||
|
LevelMarker,
|
||||||
|
Transform::from_translation(spawn_pos),
|
||||||
|
Sprite {
|
||||||
|
image,
|
||||||
|
custom_size: Some(Vec2::splat(SLOT_SIZE)),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
Visibility::Visible,
|
||||||
|
BallProjectile {
|
||||||
|
velocity: direction * 800.0,
|
||||||
|
previous_position: spawn_pos_2d,
|
||||||
|
ball_type,
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn move_projectiles(
|
||||||
|
mut commands: Commands,
|
||||||
|
mut projectiles: Query<(Entity, &mut BallProjectile, &mut Transform)>,
|
||||||
|
time: Res<Time>,
|
||||||
|
) {
|
||||||
|
for (entity, mut proj, mut transform) in projectiles.iter_mut() {
|
||||||
|
// Обновляем позицию
|
||||||
|
let new_pos = proj.previous_position + proj.velocity * time.delta_secs();
|
||||||
|
transform.translation = new_pos.extend(transform.translation.z);
|
||||||
|
proj.previous_position = new_pos;
|
||||||
|
|
||||||
|
// Опционально: удаляем снаряды, улетевшие за экран
|
||||||
|
if new_pos.x.abs() > 2000.0 || new_pos.y.abs() > 2000.0 {
|
||||||
|
commands.entity(entity).despawn();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn detect_projectile_hit(
|
||||||
|
mut commands: Commands,
|
||||||
|
track: Res<TrackPath>,
|
||||||
|
mut projectiles: Query<(Entity, &mut BallProjectile)>,
|
||||||
|
mut balls: Query<&mut Ball>, // <-- нужен мутабельный доступ для сдвига
|
||||||
|
) {
|
||||||
|
for (proj_entity, proj) in projectiles.iter_mut() {
|
||||||
|
let mut nearest_idx = 0;
|
||||||
|
let mut min_dist = f32::MAX;
|
||||||
|
|
||||||
|
for (idx, &slot_pos) in track.points.iter().enumerate() {
|
||||||
|
let dist = proj.previous_position.distance(slot_pos);
|
||||||
|
if dist < min_dist {
|
||||||
|
min_dist = dist;
|
||||||
|
nearest_idx = idx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const HIT_THRESHOLD: f32 = SLOT_SIZE;
|
||||||
|
if min_dist < HIT_THRESHOLD {
|
||||||
|
|
||||||
|
let occupied_slots: Vec<usize> = balls.iter()
|
||||||
|
.map(|ball| ball.slot_index)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if occupied_slots.contains(&nearest_idx) {
|
||||||
|
|
||||||
|
let left_progress = if nearest_idx > 0 {
|
||||||
|
balls.iter()
|
||||||
|
.find(|b| b.slot_index == nearest_idx - 1)
|
||||||
|
.map(|b| b.slot_progress)
|
||||||
|
.unwrap_or(0.0)
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
|
||||||
|
// 2. Сдвигаем правую часть очереди вправо
|
||||||
|
for mut ball in balls.iter_mut() {
|
||||||
|
if ball.slot_index >= nearest_idx {
|
||||||
|
ball.slot_index += 1;
|
||||||
|
ball.slot_progress = 0.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
commands.entity(proj_entity)
|
||||||
|
.insert(Ball {
|
||||||
|
ball_type: proj.ball_type,
|
||||||
|
slot_index: nearest_idx,
|
||||||
|
slot_progress: left_progress,
|
||||||
|
})
|
||||||
|
.remove::<BallProjectile>();
|
||||||
|
|
||||||
|
println!("Hit! Inserted at slot {}", nearest_idx);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
commands.entity(proj_entity).despawn();
|
||||||
|
println!("Miss: slot {} is empty", nearest_idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
70
src/states/level/system_grid.rs
Normal file
70
src/states/level/system_grid.rs
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
use bevy::color::Color;
|
||||||
|
use bevy::math::{Vec2, Vec3};
|
||||||
|
use bevy::prelude::{default, Commands, Gizmos, Text2d, TextColor, TextFont, Transform};
|
||||||
|
use crate::states::level::components::DebugSlotNumber;
|
||||||
|
use crate::states::level::system::{SHIFT, SLOT_SIZE};
|
||||||
|
use crate::{HEIGHT, WIDTH};
|
||||||
|
|
||||||
|
const GRID_COLS: f32 = WIDTH as f32 * SHIFT;
|
||||||
|
const GRID_ROWS: f32 = HEIGHT as f32 * SHIFT;
|
||||||
|
const SLOTS_IN_ROW: u32 = WIDTH * SHIFT as u32;
|
||||||
|
const SLOTS_IN_COL: u32 = HEIGHT * SHIFT as u32;
|
||||||
|
|
||||||
|
pub fn debug_draw_grid(mut gizmos: Gizmos, mut commands: Commands) {
|
||||||
|
let width = GRID_COLS * SLOT_SIZE;
|
||||||
|
let height = GRID_ROWS * SLOT_SIZE;
|
||||||
|
let half_w = width / 2.0;
|
||||||
|
let half_h = height / 2.0;
|
||||||
|
|
||||||
|
// Вертикальные линии сетки
|
||||||
|
for col in 0..=GRID_COLS as u32 {
|
||||||
|
let x = -half_w + col as f32 * SLOT_SIZE;
|
||||||
|
gizmos.line_2d(
|
||||||
|
Vec2::new(x, -half_h),
|
||||||
|
Vec2::new(x, half_h),
|
||||||
|
Color::srgba(0.4, 0.4, 0.4, 0.4),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Горизонтальные линии сетки
|
||||||
|
for row in 0..=GRID_ROWS as u32 {
|
||||||
|
let y = -half_h + row as f32 * SLOT_SIZE;
|
||||||
|
gizmos.line_2d(
|
||||||
|
Vec2::new(-half_w, y),
|
||||||
|
Vec2::new(half_w, y),
|
||||||
|
Color::srgba(0.4, 0.4, 0.4, 0.4),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Точки в центрах слотов (удобно для сверки логики)
|
||||||
|
for row in 0..GRID_ROWS as u32 {
|
||||||
|
for col in 0..GRID_COLS as u32 {
|
||||||
|
let cx = -half_w + col as f32 * SLOT_SIZE + SLOT_SIZE / 2.0;
|
||||||
|
let cy = -half_h + row as f32 * SLOT_SIZE + SLOT_SIZE / 2.0;
|
||||||
|
gizmos.circle_2d(Vec2::new(cx, cy), 4.0, Color::srgba(1.0, 0.9, 0.0, 0.7));
|
||||||
|
|
||||||
|
let coord_text = format!("{:.0}, {:.0}", cx, cy);
|
||||||
|
commands.spawn((
|
||||||
|
Text2d::new(cx.to_string()),
|
||||||
|
TextFont {
|
||||||
|
font_size: 14.0,
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
TextColor(Color::srgb(237.0 / 255.0, 21.0 / 255.0, 127.0 / 255.0)),
|
||||||
|
Transform::from_translation(Vec3::new(cx, cy + 8.0, 100.0)), // z=100, чтобы было поверх всего
|
||||||
|
DebugSlotNumber,
|
||||||
|
));
|
||||||
|
|
||||||
|
commands.spawn((
|
||||||
|
Text2d::new(cy.to_string()),
|
||||||
|
TextFont {
|
||||||
|
font_size: 14.0,
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
TextColor(Color::srgb(237.0 / 255.0, 21.0 / 255.0, 127.0 / 255.0)),
|
||||||
|
Transform::from_translation(Vec3::new(cx, cy - 8.0, 100.0)), // z=100, чтобы было поверх всего
|
||||||
|
DebugSlotNumber,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
102
src/states/level/system_track.rs
Normal file
102
src/states/level/system_track.rs
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
use bevy::color::Color;
|
||||||
|
use bevy::math::Vec2;
|
||||||
|
use bevy::prelude::{Gizmos, Res};
|
||||||
|
use crate::states::level::components::TrackPath;
|
||||||
|
use crate::states::level::system::SLOT_SIZE;
|
||||||
|
|
||||||
|
const BUFFER_SLOTS: usize = 20;
|
||||||
|
|
||||||
|
pub fn setup_track_with_buffer() -> TrackPath {
|
||||||
|
// берем только видимые слоты
|
||||||
|
let visual_points = make_track();
|
||||||
|
|
||||||
|
// вычисляем обратное направление от начала трека
|
||||||
|
let direction = if visual_points.len() > 1 {
|
||||||
|
(visual_points[0] - visual_points[1]).normalize()
|
||||||
|
} else {
|
||||||
|
Vec2::NEG_X // если трек из 1 точки
|
||||||
|
};
|
||||||
|
|
||||||
|
// создаем невидимые буферные слоты (от дальнего к ближнему)
|
||||||
|
let mut buffer_points = Vec::with_capacity(BUFFER_SLOTS);
|
||||||
|
for i in (1..=BUFFER_SLOTS).rev() {
|
||||||
|
let pos = visual_points[0] + direction * (i as f32 * SLOT_SIZE);
|
||||||
|
buffer_points.push(pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
// цепляем буферные слоты к видимой части трека
|
||||||
|
let mut full_points = buffer_points;
|
||||||
|
full_points.extend(visual_points);
|
||||||
|
|
||||||
|
TrackPath {
|
||||||
|
points: full_points,
|
||||||
|
spawn_index: BUFFER_SLOTS,
|
||||||
|
buffer_size: BUFFER_SLOTS,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_track() -> Vec<Vec2> {
|
||||||
|
vec![
|
||||||
|
//спавн точка за видимой областью
|
||||||
|
// Vec2::new(-660.0, -140.0),
|
||||||
|
Vec2::new(-620.0, -140.0),
|
||||||
|
Vec2::new(-580.0, -140.0),
|
||||||
|
Vec2::new(-540.0, -140.0),
|
||||||
|
Vec2::new(-540.0, -100.0),
|
||||||
|
Vec2::new(-500.0, -100.0),
|
||||||
|
Vec2::new(-460.0, -100.0),
|
||||||
|
Vec2::new(-420.0, -100.0),
|
||||||
|
Vec2::new(-380.0, -100.0),
|
||||||
|
Vec2::new(-380.0, -60.0),
|
||||||
|
Vec2::new(-340.0, -60.0),
|
||||||
|
Vec2::new(-300.0, -60.0),
|
||||||
|
Vec2::new(-260.0, -60.0),
|
||||||
|
Vec2::new(-220.0, -60.0),
|
||||||
|
Vec2::new(-180.0, -60.0),
|
||||||
|
Vec2::new(-140.0, -60.0),
|
||||||
|
Vec2::new(-100.0, -60.0),
|
||||||
|
Vec2::new(-60.0, -60.0),
|
||||||
|
Vec2::new(-20.0, -60.0),
|
||||||
|
Vec2::new(20.0, -60.0),
|
||||||
|
Vec2::new(60.0, -60.0),
|
||||||
|
Vec2::new(60.0, -20.0),
|
||||||
|
Vec2::new(60.0, 20.0),
|
||||||
|
Vec2::new(60.0, 60.0),
|
||||||
|
Vec2::new(100.0, 60.0),
|
||||||
|
Vec2::new(140.0, 60.0),
|
||||||
|
Vec2::new(180.0, 60.0),
|
||||||
|
Vec2::new(180.0, 100.0),
|
||||||
|
Vec2::new(180.0, 140.0),
|
||||||
|
Vec2::new(220.0, 140.0),
|
||||||
|
Vec2::new(260.0, 140.0),
|
||||||
|
Vec2::new(300.0, 140.0),
|
||||||
|
Vec2::new(340.0, 140.0),
|
||||||
|
Vec2::new(380.0, 140.0),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub fn debug_draw_track(mut gizmos: Gizmos, track: Res<TrackPath>) {
|
||||||
|
for slot in &track.points {
|
||||||
|
gizmos.rect_2d(
|
||||||
|
*slot, // центр
|
||||||
|
Vec2::splat(SLOT_SIZE), // ширина и высота
|
||||||
|
Color::srgb(237.0 / 255.0, 21.0 / 255.0, 127.0 / 255.0),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fn generate_track(mut commands: Commands) {
|
||||||
|
// let half_w = crate::states::level::system::GRID_COLS as f32 * SLOT_SIZE / 2.0;
|
||||||
|
// let half_h = crate::states::level::system::GRID_ROWS as f32 * SLOT_SIZE / 2.0;
|
||||||
|
//
|
||||||
|
// let mut points = Vec::with_capacity(crate::states::level::system::GRID_COLS as usize);
|
||||||
|
//
|
||||||
|
// for col in (0..crate::states::level::system::GRID_COLS as u32).rev() {
|
||||||
|
// let x = -half_w + col as f32 * SLOT_SIZE + SLOT_SIZE / 2.0;
|
||||||
|
// let y = 0.0;
|
||||||
|
// points.push(Vec2::new(x, y));
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// commands.insert_resource(TrackPath { points, spawn_index: 0, buffer_size: 0 });
|
||||||
|
// }
|
||||||
10
src/states/main_menu/components.rs
Normal file
10
src/states/main_menu/components.rs
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct MainMenuRootMarker;
|
||||||
|
|
||||||
|
#[derive(Component, Copy, Clone)]
|
||||||
|
pub struct MainMenuNewGameButton;
|
||||||
|
|
||||||
|
#[derive(Component, Copy, Clone)]
|
||||||
|
pub struct MainMenuSettingsButton;
|
||||||
5
src/states/main_menu/mod.rs
Normal file
5
src/states/main_menu/mod.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
pub mod state;
|
||||||
|
pub mod components;
|
||||||
|
pub use components::*;
|
||||||
|
pub mod systems;
|
||||||
|
pub mod plugin;
|
||||||
23
src/states/main_menu/plugin.rs
Normal file
23
src/states/main_menu/plugin.rs
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
use bevy::prelude::*;
|
||||||
|
use crate::states::main_menu::{MainMenuNewGameButton, MainMenuSettingsButton};
|
||||||
|
use crate::ui::button_click::ButtonClickMessage;
|
||||||
|
use crate::ui::click::handle_click_system;
|
||||||
|
|
||||||
|
#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub struct MainMenuButtonSet;
|
||||||
|
|
||||||
|
pub struct MainMenuUiPlugin;
|
||||||
|
|
||||||
|
impl Plugin for MainMenuUiPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.add_message::<ButtonClickMessage<MainMenuNewGameButton>>()
|
||||||
|
.add_message::<ButtonClickMessage<MainMenuSettingsButton>>()
|
||||||
|
.add_systems(
|
||||||
|
Update,
|
||||||
|
(
|
||||||
|
handle_click_system::<MainMenuNewGameButton>,
|
||||||
|
handle_click_system::<MainMenuSettingsButton>,
|
||||||
|
).in_set(MainMenuButtonSet)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
117
src/states/main_menu/state.rs
Normal file
117
src/states/main_menu/state.rs
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
use crate::states::AppState;
|
||||||
|
use crate::states::main_menu::{MainMenuNewGameButton, MainMenuRootMarker, MainMenuSettingsButton};
|
||||||
|
use crate::states::main_menu::systems::*;
|
||||||
|
use crate::ui::button_hover::button_hover;
|
||||||
|
use crate::ui::{ButtonStyle, spawn_background, spawn_button};
|
||||||
|
use bevy::prelude::*;
|
||||||
|
use crate::common::CommonCloseApp;
|
||||||
|
use crate::states::main_menu::plugin::MainMenuUiPlugin;
|
||||||
|
|
||||||
|
pub struct MainMenuState;
|
||||||
|
|
||||||
|
impl Plugin for MainMenuState {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.init_state::<AppState>()
|
||||||
|
.add_plugins(MainMenuUiPlugin)
|
||||||
|
.add_systems(OnEnter(AppState::MainMenu), setup_main_menu)
|
||||||
|
.add_systems(
|
||||||
|
Update,
|
||||||
|
(
|
||||||
|
button_hover,
|
||||||
|
new_game_button_system,
|
||||||
|
settings_button_system,
|
||||||
|
)
|
||||||
|
.run_if(in_state(AppState::MainMenu)),
|
||||||
|
)
|
||||||
|
.add_systems(OnExit(AppState::MainMenu), cleanup_main_menu);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_main_menu(mut commands: Commands, asset_server: Res<AssetServer>) {
|
||||||
|
//всё окно
|
||||||
|
let root = commands
|
||||||
|
.spawn((
|
||||||
|
Node {
|
||||||
|
display: Display::Flex,
|
||||||
|
width: Val::Percent(100.0),
|
||||||
|
height: Val::Percent(100.0),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
BackgroundColor(Color::NONE),
|
||||||
|
Name::new("Main menu background node"),
|
||||||
|
MainMenuRootMarker,
|
||||||
|
))
|
||||||
|
.id();
|
||||||
|
|
||||||
|
// область меню
|
||||||
|
let menu_root = commands
|
||||||
|
.spawn((
|
||||||
|
Node {
|
||||||
|
display: Display::Flex,
|
||||||
|
width: Val::Percent(25.0),
|
||||||
|
height: Val::Auto,
|
||||||
|
padding: UiRect::all(Val::Px(10.0)),
|
||||||
|
row_gap: Val::Px(5.0),
|
||||||
|
position_type: PositionType::Absolute,
|
||||||
|
left: Val::Percent(5.0),
|
||||||
|
bottom: Val::Percent(5.0),
|
||||||
|
align_items: AlignItems::Center,
|
||||||
|
flex_direction: FlexDirection::Column,
|
||||||
|
justify_content: JustifyContent::FlexEnd,
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
BackgroundColor(Color::linear_rgba(0.15, 0.15, 0.15, 0.5)),
|
||||||
|
Name::new("Main menu buttons node"),
|
||||||
|
))
|
||||||
|
.id();
|
||||||
|
commands.entity(root).add_child(menu_root);
|
||||||
|
|
||||||
|
//бэкграунд
|
||||||
|
let bg: Handle<Image> = asset_server.load("bg/main_menu.png");
|
||||||
|
spawn_background(&mut commands, bg, MainMenuRootMarker);
|
||||||
|
|
||||||
|
//задаем стиль кнопкам главного меню
|
||||||
|
let mm_button_style = ButtonStyle {
|
||||||
|
font: asset_server.load("fonts/QR Ames Beta.otf"),
|
||||||
|
font_size: 24.0,
|
||||||
|
text_color: Color::WHITE,
|
||||||
|
normal_bg: Color::linear_rgba(0.15, 0.15, 0.15, 1.0),
|
||||||
|
hovered_bg: Color::linear_rgb(0.25, 0.25, 0.25),
|
||||||
|
pressed_bg: Color::linear_rgb(0.3, 0.3, 0.3),
|
||||||
|
width: Val::Percent(90.0),
|
||||||
|
height: Val::Percent(20.0),
|
||||||
|
margin: UiRect::all(Val::Px(10.0)),
|
||||||
|
};
|
||||||
|
|
||||||
|
//кнопки главного меню
|
||||||
|
spawn_button(
|
||||||
|
&mut commands,
|
||||||
|
menu_root,
|
||||||
|
"Новая игра",
|
||||||
|
&mm_button_style,
|
||||||
|
MainMenuNewGameButton,
|
||||||
|
);
|
||||||
|
|
||||||
|
spawn_button(
|
||||||
|
&mut commands,
|
||||||
|
menu_root,
|
||||||
|
"Настройки",
|
||||||
|
&mm_button_style,
|
||||||
|
MainMenuSettingsButton,
|
||||||
|
);
|
||||||
|
|
||||||
|
spawn_button(
|
||||||
|
&mut commands,
|
||||||
|
menu_root,
|
||||||
|
"Выход",
|
||||||
|
&mm_button_style,
|
||||||
|
CommonCloseApp,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cleanup_main_menu(mut commands: Commands, query: Query<Entity, With<MainMenuRootMarker>>) {
|
||||||
|
for entity in &query {
|
||||||
|
commands.entity(entity).despawn_children();
|
||||||
|
commands.entity(entity).despawn();
|
||||||
|
}
|
||||||
|
}
|
||||||
25
src/states/main_menu/systems.rs
Normal file
25
src/states/main_menu/systems.rs
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
use crate::states::AppState;
|
||||||
|
use crate::states::main_menu::{MainMenuNewGameButton, MainMenuSettingsButton};
|
||||||
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
pub fn new_game_button_system(
|
||||||
|
interaction_query: Query<&Interaction, (Changed<Interaction>, With<MainMenuNewGameButton>)>,
|
||||||
|
mut next_state: ResMut<NextState<AppState>>,
|
||||||
|
) {
|
||||||
|
for interaction in &interaction_query {
|
||||||
|
if matches!(interaction, Interaction::Pressed) {
|
||||||
|
next_state.set(AppState::GameState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn settings_button_system(
|
||||||
|
interaction_query: Query<&Interaction, (Changed<Interaction>, With<MainMenuSettingsButton>)>,
|
||||||
|
mut next_state: ResMut<NextState<AppState>>,
|
||||||
|
) {
|
||||||
|
for interaction in &interaction_query {
|
||||||
|
if matches!(interaction, Interaction::Pressed) {
|
||||||
|
next_state.set(AppState::SettingsMenu);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
8
src/states/mod.rs
Normal file
8
src/states/mod.rs
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
pub mod app_states;
|
||||||
|
pub use app_states::AppState;
|
||||||
|
|
||||||
|
pub mod main_menu;
|
||||||
|
|
||||||
|
pub mod settings_menu;
|
||||||
|
pub mod game;
|
||||||
|
mod level;
|
||||||
4
src/states/settings_menu/components.rs
Normal file
4
src/states/settings_menu/components.rs
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
use bevy::prelude::Component;
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct SettingsRootMarker;
|
||||||
4
src/states/settings_menu/mod.rs
Normal file
4
src/states/settings_menu/mod.rs
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
pub mod state;
|
||||||
|
pub(crate) mod components;
|
||||||
|
mod systems;
|
||||||
|
mod plugin;
|
||||||
23
src/states/settings_menu/plugin.rs
Normal file
23
src/states/settings_menu/plugin.rs
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
use bevy::prelude::*;
|
||||||
|
use crate::states::main_menu::{MainMenuNewGameButton, MainMenuSettingsButton};
|
||||||
|
use crate::ui::button_click::ButtonClickMessage;
|
||||||
|
use crate::ui::click::handle_click_system;
|
||||||
|
|
||||||
|
#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub struct SettingsButtonSet;
|
||||||
|
|
||||||
|
pub struct SettingsUiPlugin;
|
||||||
|
|
||||||
|
impl Plugin for SettingsUiPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
// app.add_message::<ButtonClickMessage<MainMenuNewGame>>()
|
||||||
|
// .add_message::<ButtonClickMessage<MainMenuSettingsButton>>()
|
||||||
|
// .add_systems(
|
||||||
|
// Update,
|
||||||
|
// (
|
||||||
|
// handle_click_system::<MainMenuNewGame>,
|
||||||
|
// handle_click_system::<MainMenuSettingsButton>,
|
||||||
|
// ).in_set(SettingsButtonSet)
|
||||||
|
// );
|
||||||
|
}
|
||||||
|
}
|
||||||
63
src/states/settings_menu/state.rs
Normal file
63
src/states/settings_menu/state.rs
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
use bevy::prelude::*;
|
||||||
|
use crate::common::ExitToMainMenu;
|
||||||
|
use crate::states::app_states::AppState;
|
||||||
|
use crate::states::settings_menu::components::SettingsRootMarker;
|
||||||
|
use crate::ui::{spawn_background, spawn_button, ButtonStyle};
|
||||||
|
|
||||||
|
pub struct SettingsMenuState;
|
||||||
|
|
||||||
|
impl Plugin for SettingsMenuState {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.init_state::<AppState>()
|
||||||
|
.add_systems(OnEnter(AppState::SettingsMenu), setup_settings_menu)
|
||||||
|
.add_systems(OnExit(AppState::SettingsMenu), cleanup_settings_menu);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_settings_menu(mut commands: Commands, asset_server: Res<AssetServer>) {
|
||||||
|
let root = commands
|
||||||
|
.spawn((
|
||||||
|
Node {
|
||||||
|
width: Val::Percent(100.0),
|
||||||
|
height: Val::Percent(100.0),
|
||||||
|
align_items: AlignItems::Center,
|
||||||
|
justify_content: JustifyContent::Center,
|
||||||
|
flex_direction: FlexDirection::Column,
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
BackgroundColor(Color::NONE),
|
||||||
|
Name::new("Settings menu"),
|
||||||
|
SettingsRootMarker,
|
||||||
|
))
|
||||||
|
.id();
|
||||||
|
|
||||||
|
let bg: Handle<Image> = asset_server.load("bg/settings.png");
|
||||||
|
spawn_background(&mut commands, bg, SettingsRootMarker);
|
||||||
|
|
||||||
|
let mm_button_style = ButtonStyle {
|
||||||
|
font: asset_server.load("fonts/QR Ames Beta.otf"),
|
||||||
|
font_size: 24.0,
|
||||||
|
text_color: Color::WHITE,
|
||||||
|
normal_bg: Color::linear_rgba(0.15, 0.15, 0.15, 1.0),
|
||||||
|
hovered_bg: Color::linear_rgb(0.25, 0.25, 0.25),
|
||||||
|
pressed_bg: Color::linear_rgb(0.3, 0.3, 0.3),
|
||||||
|
width: Val::Percent(90.0),
|
||||||
|
height: Val::Percent(20.0),
|
||||||
|
margin: UiRect::all(Val::Px(10.0)),
|
||||||
|
};
|
||||||
|
|
||||||
|
spawn_button(
|
||||||
|
&mut commands,
|
||||||
|
root,
|
||||||
|
"В главное меню",
|
||||||
|
&mm_button_style,
|
||||||
|
ExitToMainMenu,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cleanup_settings_menu(mut commands: Commands, query: Query<Entity, With<SettingsRootMarker>>) {
|
||||||
|
for entity in &query {
|
||||||
|
commands.entity(entity).despawn_children();
|
||||||
|
commands.entity(entity).despawn();
|
||||||
|
}
|
||||||
|
}
|
||||||
14
src/states/settings_menu/systems.rs
Normal file
14
src/states/settings_menu/systems.rs
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
// use bevy::prelude::{Button, Changed, Interaction, NextState, Query, ResMut, With};
|
||||||
|
// use crate::states::AppState;
|
||||||
|
// use crate::states::settings_menu::components::BackToMainMenuButton;
|
||||||
|
//
|
||||||
|
// pub fn back_to_main_menu_button_system(
|
||||||
|
// interaction_query: Query<&Interaction, (Changed<Interaction>, With<Button>, With<BackToMainMenuButton>)>,
|
||||||
|
// mut next_state: ResMut<NextState<AppState>>,
|
||||||
|
// ){
|
||||||
|
// for interaction in &interaction_query {
|
||||||
|
// if matches!(interaction, Interaction::Pressed) {
|
||||||
|
// next_state.set(AppState::MainMenu);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
8
src/ui/components/audio.rs
Normal file
8
src/ui/components/audio.rs
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
use bevy::prelude::Component;
|
||||||
|
|
||||||
|
#[derive(Component, Clone, Copy)]
|
||||||
|
#[derive(PartialEq)]
|
||||||
|
pub enum AudioAction {
|
||||||
|
Mute,
|
||||||
|
Unmute,
|
||||||
|
}
|
||||||
8
src/ui/components/hover_style.rs
Normal file
8
src/ui/components/hover_style.rs
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct HoverStyle {
|
||||||
|
pub normal: Color,
|
||||||
|
pub hovered: Color,
|
||||||
|
pub pressed: Color,
|
||||||
|
}
|
||||||
2
src/ui/components/mod.rs
Normal file
2
src/ui/components/mod.rs
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
pub mod hover_style;
|
||||||
|
pub mod audio;
|
||||||
6
src/ui/messages/button_click.rs
Normal file
6
src/ui/messages/button_click.rs
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
use bevy::prelude::Message;
|
||||||
|
|
||||||
|
#[derive(Message)]
|
||||||
|
pub struct ButtonClickMessage<T: Send + Sync + 'static> {
|
||||||
|
pub action: T,
|
||||||
|
}
|
||||||
1
src/ui/messages/mod.rs
Normal file
1
src/ui/messages/mod.rs
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
pub mod button_click;
|
||||||
13
src/ui/mod.rs
Normal file
13
src/ui/mod.rs
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
pub mod components;pub mod messages;
|
||||||
|
pub use crate::ui::messages::*;
|
||||||
|
|
||||||
|
pub mod spawners;
|
||||||
|
pub use crate::ui::spawners::background::spawn_background;
|
||||||
|
pub use crate::ui::spawners::button::spawn_button;
|
||||||
|
|
||||||
|
pub mod systems;
|
||||||
|
pub use crate::ui::systems::*;
|
||||||
|
|
||||||
|
pub mod types;
|
||||||
|
pub use crate::ui::types::button_style::ButtonStyle;
|
||||||
|
|
||||||
37
src/ui/spawners/background.rs
Normal file
37
src/ui/spawners/background.rs
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
pub fn spawn_background_ui(
|
||||||
|
commands: &mut Commands,
|
||||||
|
parent: Entity,
|
||||||
|
image: Handle<Image>,
|
||||||
|
) {
|
||||||
|
commands.entity(parent).with_children(|builder| {
|
||||||
|
builder.spawn((
|
||||||
|
Node {
|
||||||
|
position_type: PositionType::Absolute,
|
||||||
|
width: Val::Percent(100.0),
|
||||||
|
height: Val::Percent(100.0),
|
||||||
|
justify_content: JustifyContent::Center,
|
||||||
|
align_items: AlignItems::Center,
|
||||||
|
left: Val::Px(0.0),
|
||||||
|
top: Val::Px(0.0),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
ImageNode::new(image),
|
||||||
|
ZIndex(-1),
|
||||||
|
Name::new("Background"),
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn spawn_background<B: Bundle>(
|
||||||
|
commands: &mut Commands,
|
||||||
|
image: Handle<Image>,
|
||||||
|
extra: B,
|
||||||
|
) -> Entity {
|
||||||
|
commands.spawn((
|
||||||
|
Sprite::from_image(image),
|
||||||
|
Transform::from_xyz(0.0, 0.0, -10.0),
|
||||||
|
extra,
|
||||||
|
)).id()
|
||||||
|
}
|
||||||
36
src/ui/spawners/button.rs
Normal file
36
src/ui/spawners/button.rs
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
use bevy::prelude::*;
|
||||||
|
use crate::ui::ButtonStyle;
|
||||||
|
|
||||||
|
pub fn spawn_button<T: Component>(
|
||||||
|
commands: &mut Commands,
|
||||||
|
parent: Entity,
|
||||||
|
label: &str,
|
||||||
|
style: &ButtonStyle,
|
||||||
|
action: T,
|
||||||
|
) {
|
||||||
|
commands.entity(parent).with_children(|builder| {
|
||||||
|
builder.spawn((
|
||||||
|
Button,
|
||||||
|
Node {
|
||||||
|
width: style.width,
|
||||||
|
height: style.height,
|
||||||
|
margin: style.margin,
|
||||||
|
justify_content: JustifyContent::Center,
|
||||||
|
align_items: AlignItems::Center,
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
BackgroundColor(style.normal_bg),
|
||||||
|
action,
|
||||||
|
)).with_children(|b| {
|
||||||
|
b.spawn((
|
||||||
|
Text(label.to_string()),
|
||||||
|
TextFont {
|
||||||
|
font: style.font.clone(),
|
||||||
|
font_size: style.font_size,
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
TextColor(style.text_color),
|
||||||
|
));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
4
src/ui/spawners/mod.rs
Normal file
4
src/ui/spawners/mod.rs
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
pub mod background;
|
||||||
|
pub mod button;
|
||||||
|
pub mod slider;
|
||||||
|
|
||||||
35
src/ui/spawners/slider.rs
Normal file
35
src/ui/spawners/slider.rs
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
pub fn spawn_slider(
|
||||||
|
commands: &mut Commands,
|
||||||
|
root: Entity,
|
||||||
|
){
|
||||||
|
let slider = commands
|
||||||
|
.spawn((
|
||||||
|
Node {
|
||||||
|
display: Display::Flex,
|
||||||
|
width: Val::Percent(30.0),
|
||||||
|
height: Val::Percent(3.0),
|
||||||
|
border: UiRect{
|
||||||
|
left: Val::Px(2.0),
|
||||||
|
right: Val::Px(2.0),
|
||||||
|
top: Val::Px(2.0),
|
||||||
|
bottom: Val::Px(2.0),
|
||||||
|
},
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
BackgroundColor(Color::srgb_u8(216, 223, 233)),
|
||||||
|
)).id();
|
||||||
|
|
||||||
|
let thumb = commands.spawn((
|
||||||
|
Node{
|
||||||
|
height: Val::Percent(100.0),
|
||||||
|
aspect_ratio: Some(1.0),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
BackgroundColor(Color::srgb_u8(1, 226, 106)),
|
||||||
|
)).id();
|
||||||
|
|
||||||
|
commands.entity(slider).add_child(thumb);
|
||||||
|
commands.entity(root).add_child(slider);
|
||||||
|
}
|
||||||
22
src/ui/systems/button_hover.rs
Normal file
22
src/ui/systems/button_hover.rs
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
pub fn button_hover(
|
||||||
|
mut interaction_query: Query<
|
||||||
|
(&Interaction, &mut BackgroundColor),
|
||||||
|
(Changed<Interaction>, With<Button>),
|
||||||
|
>,
|
||||||
|
) {
|
||||||
|
for (interaction, mut color) in &mut interaction_query {
|
||||||
|
match interaction {
|
||||||
|
Interaction::Pressed => {
|
||||||
|
*color = BackgroundColor(Color::linear_rgb(0.3, 0.3, 0.3));
|
||||||
|
}
|
||||||
|
Interaction::Hovered => {
|
||||||
|
*color = BackgroundColor(Color::linear_rgb(0.25, 0.25, 0.25));
|
||||||
|
}
|
||||||
|
Interaction::None => {
|
||||||
|
*color = BackgroundColor(Color::linear_rgb(0.15, 0.15, 0.15));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
14
src/ui/systems/click.rs
Normal file
14
src/ui/systems/click.rs
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
use bevy::prelude::*;
|
||||||
|
use crate::ui::messages::button_click::ButtonClickMessage;
|
||||||
|
|
||||||
|
|
||||||
|
pub fn handle_click_system<T: Component + Copy + Send + Sync + 'static>(
|
||||||
|
query: Query<(&Interaction, &T), Changed<Interaction>>,
|
||||||
|
mut writer: MessageWriter<ButtonClickMessage<T>>,
|
||||||
|
) {
|
||||||
|
for (interaction, action) in &query {
|
||||||
|
if *interaction == Interaction::Pressed {
|
||||||
|
writer.write(ButtonClickMessage { action: *action });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
src/ui/systems/mod.rs
Normal file
2
src/ui/systems/mod.rs
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
pub mod button_hover;
|
||||||
|
pub mod click;
|
||||||
14
src/ui/types/button_style.rs
Normal file
14
src/ui/types/button_style.rs
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct ButtonStyle {
|
||||||
|
pub width: Val,
|
||||||
|
pub height: Val,
|
||||||
|
pub font: Handle<Font>,
|
||||||
|
pub font_size: f32,
|
||||||
|
pub text_color: Color,
|
||||||
|
pub normal_bg: Color,
|
||||||
|
pub hovered_bg: Color,
|
||||||
|
pub pressed_bg: Color,
|
||||||
|
pub margin: UiRect,
|
||||||
|
}
|
||||||
1
src/ui/types/mod.rs
Normal file
1
src/ui/types/mod.rs
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
pub mod button_style;
|
||||||
Loading…
Add table
Add a link
Reference in a new issue