Compare commits

...

3 commits

Author SHA1 Message Date
nquidox
4da000f1c8 components and systems refactor 2026-04-11 18:09:30 +03:00
nquidox
04754db916 simplified 2026-04-11 15:14:27 +03:00
nquidox
c4d547520e ball insert into track queue 2026-04-11 15:12:11 +03:00
10 changed files with 368 additions and 143 deletions

View file

@ -19,39 +19,6 @@ pub struct TrackPath {
pub buffer_size: 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)] #[derive(Resource)]
pub struct SpawnTimer { pub struct SpawnTimer {
@ -71,6 +38,7 @@ pub struct WaveState {
pub spawning_allowed: bool, pub spawning_allowed: bool,
pub ball_reached_end: bool, pub ball_reached_end: bool,
pub is_warming_up: bool, pub is_warming_up: bool,
pub is_queue_locked: bool, //отдельно для большей явности
} }
impl Default for WaveState { impl Default for WaveState {
@ -79,6 +47,7 @@ impl Default for WaveState {
spawning_allowed: false, spawning_allowed: false,
ball_reached_end: false, ball_reached_end: false,
is_warming_up: false, is_warming_up: false,
is_queue_locked: false,
} }
} }
} }
@ -88,12 +57,12 @@ pub struct WarmupTarget {
pub target_index: usize, pub target_index: usize,
} }
#[derive(Component)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Cannon; pub enum Direction {
Left,
Right,
Top,
Bottom,
}
#[derive(Component)]
pub struct BallProjectile {
pub velocity: Vec2,
pub previous_position: Vec2,
pub ball_type: BallType,
}

View file

@ -0,0 +1,36 @@
use bevy::prelude::*;
use rand::seq::IndexedRandom;
#[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,
}

View file

@ -0,0 +1,54 @@
use bevy::math::Vec2;
use bevy::prelude::{Component, Resource};
use crate::states::level::*;
#[derive(Component)]
pub struct Cannon;
#[derive(Resource, Debug, Clone)]
pub struct CannonState {
pub current_type: BallType,
pub next_type: BallType,
}
impl Default for CannonState {
fn default() -> Self {
Self {
current_type: BallType::random(),
next_type: BallType::random(),
}
}
}
impl CannonState {
pub fn fire(&mut self) {
self.current_type = self.next_type;
self.next_type = BallType::random();
}
pub fn cycle_next(&mut self) {
// self.next_type = match self.next_type {
// BallType::First => BallType::Second,
// BallType::Second => BallType::Third,
// BallType::Third => BallType::Forth,
// BallType::Forth => BallType::First,
// };
let cur = self.current_type;
self.current_type = self.next_type;
self.next_type = cur;
}
}
#[derive(Component)]
pub struct CurrentPreviewMarker;
#[derive(Component)]
pub struct NextPreviewMarker;
#[derive(Component)]
pub struct BallProjectile {
pub velocity: Vec2,
pub previous_position: Vec2,
pub ball_type: BallType,
}

View file

@ -1,8 +1,20 @@
pub mod plugin; pub mod plugin;
pub mod components; mod components;
pub mod system; pub use components::*;
pub mod state; mod system;
pub mod system_ball; pub use system::*;
pub mod system_cannon; mod state;
pub mod system_track; pub use state::*;
pub mod system_grid; mod system_ball;
pub use system_ball::*;
mod system_cannon;
pub use system_cannon::*;
mod system_track;
pub use system_track::*;
pub mod system_grid;
pub use system_grid::*;
mod components_cannon;
pub use components_cannon::*;
mod components_ball;
pub use components_ball::*;

View file

@ -1,10 +1,7 @@
use crate::states::AppState::GameState; use crate::states::AppState::GameState;
use crate::states::level::system_ball::*; use crate::states::level::*;
use crate::states::level::system::*;
use bevy::prelude::*; 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; pub struct LevelPlugin;
@ -23,8 +20,12 @@ impl Plugin for LevelPlugin {
spawn_new_ball, spawn_new_ball,
move_queue_along_track, move_queue_along_track,
check_wave_completion, check_wave_completion,
rotate_cannon, //cannon systems
cycle_cannon_type,
update_cannon_preview,
spawn_projectile_from_cannon, spawn_projectile_from_cannon,
//сохранять порядок верхних трех
rotate_cannon,
move_projectiles, move_projectiles,
detect_projectile_hit, detect_projectile_hit,
) )

View file

@ -1,10 +1,8 @@
use crate::states::level::*;
use crate::states::level::components::*;
use crate::{FACTOR, HEIGHT, WIDTH}; use crate::{FACTOR, HEIGHT, WIDTH};
use bevy::prelude::*; use bevy::prelude::*;
use crate::states::level::system_track::setup_track_with_buffer;
pub const SHIFT: f32 = 2.0; pub const SHIFT: f32 = 1.0;
pub const SLOT_SIZE: f32 = FACTOR as f32 / SHIFT; pub const SLOT_SIZE: f32 = FACTOR as f32 / SHIFT;
pub fn setup_timer(mut commands: Commands){ pub fn setup_timer(mut commands: Commands){
@ -13,6 +11,12 @@ pub fn setup_timer(mut commands: Commands){
pub fn setup_level(mut commands: Commands){ pub fn setup_level(mut commands: Commands){
commands.insert_resource(setup_track_with_buffer()); commands.insert_resource(setup_track_with_buffer());
println!("Track is set up");
commands.insert_resource(WaveState::default()); commands.insert_resource(WaveState::default());
println!("WaveState is set up");
commands.insert_resource(build_cannon_state());
println!("CannonState is set up");
} }

View file

@ -1,6 +1,5 @@
use crate::states::level::components::{Ball, BallType, LevelMarker, TrackPath, WarmupTarget, WaveState};
use crate::states::level::system::SLOT_SIZE;
use bevy::prelude::*; use bevy::prelude::*;
use crate::states::level::*;
pub fn spawn_new_ball( pub fn spawn_new_ball(
mut commands: Commands, mut commands: Commands,
@ -11,7 +10,7 @@ pub fn spawn_new_ball(
balls: Query<&Ball>, balls: Query<&Ball>,
wave: Res<WaveState>, wave: Res<WaveState>,
) { ) {
if !wave.spawning_allowed { if !wave.spawning_allowed || wave.is_queue_locked {
timer.reset(); timer.reset();
return; return;
} }
@ -209,3 +208,4 @@ pub fn check_wave_completion(
} }
} }
} }

View file

@ -1,21 +1,23 @@
use bevy::prelude::*; use bevy::prelude::*;
use crate::states::level::*;
use bevy::window::PrimaryWindow; 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( const CANNON_Z_INDEX: f32 = 10.0;
mut commands: Commands, const PROJECTILE_Z_INDEX: f32 = 11.0;
asset_server: Res<AssetServer>, const CURRENT_SHOT_Z_INDEX: f32 = 12.0;
) { const NEXT_SHOT_Z_INDEX: f32 = 12.0;
pub fn setup_cannon(mut commands: Commands, asset_server: Res<AssetServer>) {
let texture: Handle<Image> = asset_server.load("cannon/cannon.png"); let texture: Handle<Image> = asset_server.load("cannon/cannon.png");
commands.spawn(( commands.spawn((
LevelMarker, LevelMarker,
Cannon, Cannon,
Transform::from_xyz(579.0-(1280.0/2.0), (768.0/2.0)-150.0, 1.0), Transform::from_xyz(579.0 - (1280.0 / 2.0), (768.0 / 2.0) - 150.0, CANNON_Z_INDEX),
Sprite::from_image(texture), Sprite::from_image(texture),
Visibility::Visible, Visibility::Visible,
)); ));
println!("Cannon visuals is set up");
} }
pub fn rotate_cannon( pub fn rotate_cannon(
@ -23,13 +25,23 @@ pub fn rotate_cannon(
window_query: Query<&Window, With<PrimaryWindow>>, window_query: Query<&Window, With<PrimaryWindow>>,
camera_query: Query<(&Camera, &GlobalTransform)>, camera_query: Query<(&Camera, &GlobalTransform)>,
) { ) {
let Ok(mut cannon_tf) = query.single_mut() else { return }; let Ok(mut cannon_tf) = query.single_mut() else {
let Ok(window) = window_query.single() else { return }; return;
let Some(cursor_pos) = window.cursor_position() else { return }; };
let Ok((camera, camera_tf)) = camera_query.single() 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 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 cannon_pos = Vec2::new(cannon_tf.translation.x, cannon_tf.translation.y);
@ -45,18 +57,23 @@ pub fn spawn_projectile_from_cannon(
cannon_query: Query<&Transform, With<Cannon>>, cannon_query: Query<&Transform, With<Cannon>>,
mouse_input: Res<ButtonInput<MouseButton>>, mouse_input: Res<ButtonInput<MouseButton>>,
asset_server: Res<AssetServer>, asset_server: Res<AssetServer>,
mut cannon_state: ResMut<CannonState>
) { ) {
if !mouse_input.just_pressed(MouseButton::Left) { return; } if !mouse_input.just_pressed(MouseButton::Left) {
return;
}
let Ok(cannon_tf) = cannon_query.single() else { return; }; let Ok(cannon_tf) = cannon_query.single() else {
return;
};
let ball_type = BallType::First; // TODO: рандом/цикл let ball_type = cannon_state.current_type;
let image = asset_server.load(ball_type.asset_path()); let image = asset_server.load(ball_type.asset_path());
let offset = 100.0; let offset = 100.0; // сдвиг от центра к дулу пушки
let direction = cannon_tf.rotation.mul_vec3(Vec3::Y).truncate().normalize(); let direction = cannon_tf.rotation.mul_vec3(Vec3::Y).truncate().normalize();
let spawn_pos_2d = cannon_tf.translation.truncate() + direction * offset; let spawn_pos_2d = cannon_tf.translation.truncate() + direction * offset;
let spawn_pos = spawn_pos_2d.extend(10.0); // z=10, чтобы было поверх трека let spawn_pos = spawn_pos_2d.extend(PROJECTILE_Z_INDEX);
commands.spawn(( commands.spawn((
LevelMarker, LevelMarker,
@ -73,6 +90,7 @@ pub fn spawn_projectile_from_cannon(
ball_type, ball_type,
}, },
)); ));
cannon_state.fire();
} }
pub fn move_projectiles( pub fn move_projectiles(
@ -82,7 +100,9 @@ pub fn move_projectiles(
) { ) {
for (entity, mut proj, mut transform) in projectiles.iter_mut() { for (entity, mut proj, mut transform) in projectiles.iter_mut() {
// Обновляем позицию // Обновляем позицию
let new_pos = proj.previous_position + proj.velocity * time.delta_secs(); let delta = proj.velocity * time.delta_secs();
let new_pos = proj.previous_position + delta;
transform.translation = new_pos.extend(transform.translation.z); transform.translation = new_pos.extend(transform.translation.z);
proj.previous_position = new_pos; proj.previous_position = new_pos;
@ -93,64 +113,151 @@ pub fn move_projectiles(
} }
} }
const HIT_THRESHOLD: f32 = SLOT_SIZE;
pub fn detect_projectile_hit( pub fn detect_projectile_hit(
mut commands: Commands, mut commands: Commands,
track: Res<TrackPath>, track: Res<TrackPath>,
mut wave: ResMut<WaveState>,
mut projectiles: Query<(Entity, &mut BallProjectile)>, mut projectiles: Query<(Entity, &mut BallProjectile)>,
mut balls: Query<&mut Ball>, // <-- нужен мутабельный доступ для сдвига mut balls: Query<(Entity, &mut Ball)>,
) { ) {
// пока идёт структурное изменение, спавн и другие мутации ждут
if wave.is_queue_locked {
return;
}
for (proj_entity, proj) in projectiles.iter_mut() { for (proj_entity, proj) in projectiles.iter_mut() {
let mut nearest_idx = 0; let mut min_dist_any = f32::MAX;
let mut min_dist = f32::MAX; let mut nearest_idx_any = 0;
for (idx, &slot_pos) in track.points.iter().enumerate() { for (idx, &slot_pos) in track.points.iter().enumerate() {
let dist = proj.previous_position.distance(slot_pos); let dist = proj.previous_position.distance(slot_pos);
if dist < min_dist { if dist < min_dist_any {
min_dist = dist; min_dist_any = dist;
nearest_idx = idx; nearest_idx_any = 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() if min_dist_any > HIT_THRESHOLD {
.find(|b| b.slot_index == nearest_idx - 1) // commands.entity(proj_entity).despawn();
.map(|b| b.slot_progress) continue;
.unwrap_or(0.0) }
} else {
0.0
};
// 2. Сдвигаем правую часть очереди вправо let insert_idx;
for mut ball in balls.iter_mut() { let target_progress;
if ball.slot_index >= nearest_idx {
ball.slot_index += 1;
ball.slot_progress = 0.0;
}
}
commands.entity(proj_entity) if let Some((_, target_ball)) = balls.iter().find(|(_, b)| b.slot_index == nearest_idx_any) {
.insert(Ball { // Копируем значения в локальные переменные
ball_type: proj.ball_type, target_progress = target_ball.slot_progress;
slot_index: nearest_idx,
slot_progress: left_progress,
})
.remove::<BallProjectile>();
println!("Hit! Inserted at slot {}", nearest_idx);
// Определяем индекс вставки
insert_idx = if target_progress <= 0.5 { //TODO check correction
nearest_idx_any - 1
} else { } else {
commands.entity(proj_entity).despawn(); nearest_idx_any
println!("Miss: slot {} is empty", nearest_idx); };
} else {
// Слот пуст — снаряд пролетает мимо
// commands.entity(proj_entity).despawn();
continue;
}
wave.is_queue_locked = true;
// сдвигаем очередь
let max_idx = track.points.len() - 1;
let mut to_despawn = Vec::new();
for (entity, mut ball) in balls.iter_mut() {
if ball.slot_index >= insert_idx {
if ball.slot_index < max_idx {
ball.slot_index += 1;
} else {
to_despawn.push(entity);
};
} }
} }
for entity in to_despawn {
commands.entity(entity).despawn();
}
commands.entity(proj_entity).insert({
Ball {
ball_type: proj.ball_type,
slot_index: insert_idx,
slot_progress: target_progress,
}
}).remove::<BallProjectile>();
} }
}
// Разблокировка (структурные изменения завершены)
wave.is_queue_locked = false;
}
pub fn cycle_cannon_type(
mut cannon_state: ResMut<CannonState>,
mouse_input: Res<ButtonInput<MouseButton>>,
) {
if mouse_input.just_pressed(MouseButton::Right) {
cannon_state.cycle_next();
}
}
pub fn update_cannon_preview(
mut commands: Commands,
cannon_state: Res<CannonState>,
asset_server: Res<AssetServer>,
cannon_query: Query<Entity, With<Cannon>>,
muzzle_query: Query<Entity, With<CurrentPreviewMarker>>,
next_query: Query<Entity, With<NextPreviewMarker>>,
) {
let Ok(cannon_entity) = cannon_query.single() else { return };
let muzzle_image = asset_server.load(cannon_state.current_type.asset_path());
if let Ok(muzzle_entity) = muzzle_query.single() {
// усли шарик у дула есть, то обновляем спрайт
commands.entity(muzzle_entity).insert(Sprite::from_image(muzzle_image));
} else {
//иначе создаем
let preview_entity = commands.spawn((
CurrentPreviewMarker,
Sprite{
image: muzzle_image,
custom_size: Some(Vec2::splat(SLOT_SIZE)),
..default()
},
Transform::from_xyz(0.0, 100.0, CURRENT_SHOT_Z_INDEX),
Visibility::Visible,
)).id();
commands.entity(cannon_entity).add_child(preview_entity);
}
let next_image = asset_server.load(cannon_state.next_type.asset_path());
if let Ok(next_entity) = next_query.single() {
commands.entity(next_entity).insert(Sprite::from_image(next_image));
} else {
let preview_entity = commands.spawn((
NextPreviewMarker,
Sprite{
image: next_image,
custom_size: Some(Vec2::splat(SLOT_SIZE)),
..default()
},
Transform::from_xyz(0.0, -50.0, NEXT_SHOT_Z_INDEX),
Visibility::Visible,
)).id();
commands.entity(cannon_entity).add_child(preview_entity);
}
}
pub fn build_cannon_state() -> CannonState {
CannonState {
current_type: BallType::random(),
next_type: BallType::random(),
}
}

View file

@ -1,8 +1,7 @@
use bevy::color::Color; use bevy::color::Color;
use bevy::math::{Vec2, Vec3}; use bevy::math::{Vec2, Vec3};
use bevy::prelude::{default, Commands, Gizmos, Text2d, TextColor, TextFont, Transform}; use bevy::prelude::*;
use crate::states::level::components::DebugSlotNumber; use crate::states::level::*;
use crate::states::level::system::{SHIFT, SLOT_SIZE};
use crate::{HEIGHT, WIDTH}; use crate::{HEIGHT, WIDTH};
const GRID_COLS: f32 = WIDTH as f32 * SHIFT; const GRID_COLS: f32 = WIDTH as f32 * SHIFT;

View file

@ -1,8 +1,7 @@
use bevy::color::Color; use bevy::color::Color;
use bevy::math::Vec2; use bevy::math::Vec2;
use bevy::prelude::{Gizmos, Res}; use bevy::prelude::*;
use crate::states::level::components::TrackPath; use crate::states::level::*;
use crate::states::level::system::SLOT_SIZE;
const BUFFER_SLOTS: usize = 20; const BUFFER_SLOTS: usize = 20;
@ -35,8 +34,38 @@ pub fn setup_track_with_buffer() -> TrackPath {
} }
} }
// for debug
fn make_track() -> Vec<Vec2> { fn make_track() -> Vec<Vec2> {
vec![ make_track_80()
}
fn make_track_80() -> Vec<Vec2> {
// let track = generate_track_line(Vec2::new(-600.0, -80.0), Vec2::new(600.0, -80.0), Direction::Right);
let track: Vec<Vec2> = vec![
Vec2::new(-600.0, -80.0),
Vec2::new(-520.0, -80.0),
Vec2::new(-440.0, -80.0),
Vec2::new(-360.0, -80.0),
Vec2::new(-280.0, -80.0),
Vec2::new(-200.0, -80.0),
Vec2::new(-120.0, -80.0),
Vec2::new(-40.0, -80.0),
Vec2::new(40.0, -80.0),
Vec2::new(120.0, -80.0),
Vec2::new(200.0, -80.0),
Vec2::new(280.0, -80.0),
Vec2::new(360.0, -80.0),
Vec2::new(440.0, -80.0),
Vec2::new(520.0, -80.0),
Vec2::new(600.0, -80.0),
];
track
}
fn make_track_40() -> Vec<Vec2> {
let mut track = vec![
//спавн точка за видимой областью //спавн точка за видимой областью
// Vec2::new(-660.0, -140.0), // Vec2::new(-660.0, -140.0),
Vec2::new(-620.0, -140.0), Vec2::new(-620.0, -140.0),
@ -72,7 +101,8 @@ fn make_track() -> Vec<Vec2> {
Vec2::new(300.0, 140.0), Vec2::new(300.0, 140.0),
Vec2::new(340.0, 140.0), Vec2::new(340.0, 140.0),
Vec2::new(380.0, 140.0), Vec2::new(380.0, 140.0),
] ];
track
} }
@ -86,17 +116,30 @@ pub fn debug_draw_track(mut gizmos: Gizmos, track: Res<TrackPath>) {
} }
} }
// fn generate_track(mut commands: Commands) { fn generate_track_line(start: Vec2, end: Vec2, direction: Direction) -> Vec<Vec2> {
// 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 result = vec![end];
//
// let mut points = Vec::with_capacity(crate::states::level::system::GRID_COLS as usize); let step = match direction {
// Direction::Left => Vec2::new(-SLOT_SIZE, 0.0),
// for col in (0..crate::states::level::system::GRID_COLS as u32).rev() { Direction::Right => Vec2::new( SLOT_SIZE, 0.0),
// let x = -half_w + col as f32 * SLOT_SIZE + SLOT_SIZE / 2.0; Direction::Top => Vec2::new(0.0, SLOT_SIZE),
// let y = 0.0; Direction::Bottom => Vec2::new(0.0, -SLOT_SIZE),
// points.push(Vec2::new(x, y)); };
// }
// let steps = match direction {
// commands.insert_resource(TrackPath { points, spawn_index: 0, buffer_size: 0 }); Direction::Left | Direction::Right => {
// } ((end.x - start.x).abs() / SLOT_SIZE) as usize
}
Direction::Top | Direction::Bottom => {
((end.y - start.y).abs() / SLOT_SIZE) as usize
}
};
for i in 0..=steps {
let pos = start + step * i as f32;
result.push(pos);
}
result
}