restore after broken git

This commit is contained in:
nquidox 2026-04-10 20:28:55 +03:00
commit 138c62ac34
51 changed files with 7559 additions and 0 deletions

7
src/common/components.rs Normal file
View 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
View file

@ -0,0 +1,4 @@
use bevy::prelude::Message;
#[derive(Message)]
pub struct ExitRequestMessage;

8
src/common/mod.rs Normal file
View 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
View 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
View 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
View file

56
src/main.rs Normal file
View 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
View 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,
}

View 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
View 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
View 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
View 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);
}

View 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);
}
}
}

View 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
View 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;

View 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();
}
}

View file

View 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());
}

View 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;
}
}
}

View 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);
}
}
}
}

View 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,
));
}
}
}

View 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 });
// }

View 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;

View file

@ -0,0 +1,5 @@
pub mod state;
pub mod components;
pub use components::*;
pub mod systems;
pub mod plugin;

View 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)
);
}
}

View 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();
}
}

View 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
View 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;

View file

@ -0,0 +1,4 @@
use bevy::prelude::Component;
#[derive(Component)]
pub struct SettingsRootMarker;

View file

@ -0,0 +1,4 @@
pub mod state;
pub(crate) mod components;
mod systems;
mod plugin;

View 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)
// );
}
}

View 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();
}
}

View 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);
// }
// }
// }

View file

@ -0,0 +1,8 @@
use bevy::prelude::Component;
#[derive(Component, Clone, Copy)]
#[derive(PartialEq)]
pub enum AudioAction {
Mute,
Unmute,
}

View 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
View file

@ -0,0 +1,2 @@
pub mod hover_style;
pub mod audio;

View 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
View file

@ -0,0 +1 @@
pub mod button_click;

13
src/ui/mod.rs Normal file
View 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;

View 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
View 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
View file

@ -0,0 +1,4 @@
pub mod background;
pub mod button;
pub mod slider;

35
src/ui/spawners/slider.rs Normal file
View 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);
}

View 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
View 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
View file

@ -0,0 +1,2 @@
pub mod button_hover;
pub mod click;

View 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
View file

@ -0,0 +1 @@
pub mod button_style;