restore after broken git
This commit is contained in:
commit
138c62ac34
51 changed files with 7559 additions and 0 deletions
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