From eb4e1a1e6c745e4ebe4ff12ba4cbc5ae0adaff62 Mon Sep 17 00:00:00 2001 From: nquidox Date: Sat, 18 Apr 2026 20:49:53 +0300 Subject: [PATCH 1/5] update --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index f35fcc8..b9b0926 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /target /assets .idea -.directory \ No newline at end of file +.directory +/tiled/ From a10cc0712bccb274334aee979a2a1cacfb78b49c Mon Sep 17 00:00:00 2001 From: nquidox Date: Sat, 18 Apr 2026 20:51:09 +0300 Subject: [PATCH 2/5] debug systems toggle --- src/states/linear/components.rs | 16 +++++- src/states/linear/mod.rs | 4 +- .../linear/{systems.rs => systems_debug.rs} | 52 ++++++++++++++---- src/states/linear/ui.rs | 53 ++++++++++++++++--- 4 files changed, 105 insertions(+), 20 deletions(-) rename src/states/linear/{systems.rs => systems_debug.rs} (59%) diff --git a/src/states/linear/components.rs b/src/states/linear/components.rs index 6c0a133..06548c8 100644 --- a/src/states/linear/components.rs +++ b/src/states/linear/components.rs @@ -4,4 +4,18 @@ use bevy::prelude::*; pub struct LinearStateMarker; #[derive(Component, Copy, Clone)] -pub struct LinearRestartMarker; \ No newline at end of file +pub struct LinearRestartMarker; + +#[derive(Resource)] +pub struct LinearDebugConfig { + pub toggle_track: bool, + pub toggle_grid: bool, + pub toggle_laser: bool, +} + +#[derive(Component, Copy, Clone)] +pub enum DebugToggle{ + Track, + Grid, + Laser, +} \ No newline at end of file diff --git a/src/states/linear/mod.rs b/src/states/linear/mod.rs index 1bb90fd..bd0d3da 100644 --- a/src/states/linear/mod.rs +++ b/src/states/linear/mod.rs @@ -3,8 +3,8 @@ pub mod plugin; mod components; pub use components::*; -mod systems; -pub use systems::*; +mod systems_debug; +pub use systems_debug::*; mod components_track; pub use components_track::*; diff --git a/src/states/linear/systems.rs b/src/states/linear/systems_debug.rs similarity index 59% rename from src/states/linear/systems.rs rename to src/states/linear/systems_debug.rs index 16d63b9..bac5622 100644 --- a/src/states/linear/systems.rs +++ b/src/states/linear/systems_debug.rs @@ -1,9 +1,13 @@ -use crate::{HEIGHT, WIDTH, states, FACTOR}; +use crate::{FACTOR, HEIGHT, WIDTH, states}; use bevy::prelude::*; use states::linear::*; use std::f32::consts::FRAC_PI_2; -pub fn draw_track_gizmos(track: Res, mut gizmos: Gizmos) { +pub fn draw_track_gizmos(track: Res, mut gizmos: Gizmos, config: ResMut) { + if !config.toggle_track { + return; + }; + let mut current_pos = track.start_point; let mut current_dir = track.start_direction.normalize(); @@ -53,23 +57,51 @@ pub fn draw_track_gizmos(track: Res, mut gizmos: Gizmos) { gizmos.circle_2d(current_pos, 5.0, Color::WHITE); } -pub fn draw_grid(mut gizmos: Gizmos) { - let half_x = WIDTH as f32 / 2.0 * FACTOR as f32; - let half_y = HEIGHT as f32 / 2.0 * FACTOR as f32; +pub fn draw_grid(mut gizmos: Gizmos, config: ResMut) { + if !config.toggle_grid { + return; + }; + const FF: f32 = FACTOR as f32; + + const HALF_X: f32 = WIDTH as f32 / 2.0; + const HALF_Y: f32 = HEIGHT as f32 / 2.0; + const STEP_X: f32 = HALF_X * FF; + const STEP_Y: f32 = HALF_Y * FF; for i in 0..WIDTH { gizmos.line_2d( - Vec2::new((i as f32 - 8.0) * FACTOR as f32, half_y), - Vec2::new((i as f32 - 8.0) * FACTOR as f32, -half_y), + Vec2::new((i as f32 - HALF_X) * FF, STEP_Y), + Vec2::new((i as f32 - HALF_X) * FF, -STEP_Y), DARK_GREEN, ); } - + for i in 0..HEIGHT { gizmos.line_2d( - Vec2::new(-half_x, (i as f32 - 4.0) * FACTOR as f32), - Vec2::new(half_x, (i as f32 - 4.0) * FACTOR as f32), + Vec2::new(-STEP_X, (i as f32 - HALF_Y) * FF), + Vec2::new(STEP_X, (i as f32 - HALF_Y) * FF), DARK_GREEN, ) } } + +// pub fn draw_cannon_laser(mut gizmos: Gizmos, config: ResMut){ +// if !config.toggle_laser { +// return; +// }; +// } + +pub fn toggle_debug_systems( + buttons: Query<(&Interaction, &DebugToggle), Changed>, + mut config: ResMut, +) { + for (interaction, toggle_type) in &buttons { + if matches!(interaction, Interaction::Pressed) { + match toggle_type { + DebugToggle::Track => config.toggle_track = !config.toggle_track, + DebugToggle::Grid => config.toggle_grid = !config.toggle_grid, + DebugToggle::Laser => config.toggle_laser = !config.toggle_laser, + } + } + } +} diff --git a/src/states/linear/ui.rs b/src/states/linear/ui.rs index 18e4c93..9ceb97c 100644 --- a/src/states/linear/ui.rs +++ b/src/states/linear/ui.rs @@ -1,11 +1,12 @@ use crate::states::AppState; use crate::states::AppState::LinearPlayState; use crate::states::linear::plugin::LinearUpdateSet; -use crate::states::linear::{LinearRestartMarker, LinearStateMarker}; +use crate::states::linear::{DebugToggle, LinearRestartMarker, LinearStateMarker}; use crate::ui::button_click::ButtonClickMessage; use crate::ui::click::handle_click_system; -use crate::ui::{ButtonStyle, spawn_button}; +use crate::ui::{ButtonStyle, spawn_button, spawn_background}; use bevy::prelude::*; +use crate::{FACTOR, HEIGHT, WIDTH}; pub struct LinearUIPlugin; @@ -44,25 +45,63 @@ fn setup_ui(mut commands: Commands, asset_server: Res) { )) .id(); - let restart_button_style = ButtonStyle { + let button_style = ButtonStyle { font: asset_server.load("fonts/QR Ames Beta.otf"), - font_size: 24.0, + font_size: 16.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), + width: Val::Px(100.0), + height: Val::Px(25.0), margin: UiRect::all(Val::Px(10.0)), }; + + let bg_image: Handle = asset_server.load("sprites/bg/linear_bg.png"); + commands.spawn(( + Sprite{ + image: bg_image, + custom_size: Some(Vec2::new((WIDTH*FACTOR) as f32, (HEIGHT*FACTOR) as f32)), + ..default() + }, + Transform::from_xyz(0.0, 0.0, -10.0), + LinearStateMarker, + )); + // spawn_background(&mut commands, bg_image, LinearStateMarker); spawn_button( &mut commands, root, "Restart", - &restart_button_style, + &button_style, LinearRestartMarker, ); + + spawn_button( + &mut commands, + root, + "Track", + &button_style, + DebugToggle::Track, + ); + + spawn_button( + &mut commands, + root, + "Grid", + &button_style, + DebugToggle::Grid, + ); + + spawn_button( + &mut commands, + root, + "Laser", + &button_style, + DebugToggle::Laser, + ); + + } pub fn restart_game_button_system( From 5749e0a9d0f9fc59ea4b871ee04b766059f32dda Mon Sep 17 00:00:00 2001 From: nquidox Date: Sat, 18 Apr 2026 20:51:42 +0300 Subject: [PATCH 3/5] app scaling change --- src/main.rs | 6 +++--- src/states/linear/systems_track.rs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main.rs b/src/main.rs index 7851732..2066e59 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,9 +15,9 @@ use crate::states::main_menu::state::MainMenuState; use crate::states::settings_menu::state::SettingsMenuState; use crate::states::linear::plugin::LinearPlayPlugin; -const FACTOR: u32 = 80; -const WIDTH: u32 = 16; -const HEIGHT: u32 = 9; +const FACTOR: u32 = 40; +const WIDTH: u32 = 32; +const HEIGHT: u32 = 18; fn main() { diff --git a/src/states/linear/systems_track.rs b/src/states/linear/systems_track.rs index 0bedbe9..bf2a264 100644 --- a/src/states/linear/systems_track.rs +++ b/src/states/linear/systems_track.rs @@ -7,8 +7,8 @@ pub fn setup_linear_track() -> Track { println!("Построение трека начато"); Track { start_point: Vec2 { - x: CENTER_X - FACTOR as f32 * 7.0, - y: CENTER_Y - FACTOR as f32 * 4.0, + x: CENTER_X - STEP * 7.0 * 2.0, + y: CENTER_Y - STEP * 4.0 * 2.0, }, start_direction: Vec2::X, segments: vec![ From 97cf8adb4f898d020dc22840a8bcafe1eaed4ead Mon Sep 17 00:00:00 2001 From: nquidox Date: Sat, 18 Apr 2026 20:51:55 +0300 Subject: [PATCH 4/5] const for balls --- src/states/linear/constants.rs | 7 ++++++- src/states/linear/system_ball.rs | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/states/linear/constants.rs b/src/states/linear/constants.rs index ae03297..3099699 100644 --- a/src/states/linear/constants.rs +++ b/src/states/linear/constants.rs @@ -6,8 +6,13 @@ pub const CENTER_Y: f32 = 0.0; // pub const FACTOR_RADIUS: f32 = FACTOR as f32 / 2.0; +// основная размерность и масштаб +pub const SCALE: f32 = 1.0; pub const STEP: f32 = FACTOR as f32 / SCALE; -pub const SCALE: f32 = 2.0; + +// зависимые размеры +pub const ROUND_BALL_SIZE: f32 = STEP; +pub const BALL_RADIUS: f32 = ROUND_BALL_SIZE / 2.0; // Z-INDEXES pub const ROUND_BALL_Z: f32 = 10.0; diff --git a/src/states/linear/system_ball.rs b/src/states/linear/system_ball.rs index b74b174..be907ed 100644 --- a/src/states/linear/system_ball.rs +++ b/src/states/linear/system_ball.rs @@ -26,7 +26,7 @@ pub fn spawn_round_ball( commands.spawn(( Sprite { image, - custom_size: Some(Vec2::splat(STEP)), + custom_size: Some(Vec2::splat(ROUND_BALL_SIZE)), ..default() }, Transform::from_translation((track.segments[0].start_pos).extend(ROUND_BALL_Z)), From f297f249dfef05bc234ac2725cbfa48135fa4a0e Mon Sep 17 00:00:00 2001 From: nquidox Date: Sat, 18 Apr 2026 20:52:44 +0300 Subject: [PATCH 5/5] calculate projectile hits --- src/states/linear/components_cannon.rs | 19 +- src/states/linear/plugin.rs | 13 +- src/states/linear/systems_cannon.rs | 333 ++++++++++++++++++++++--- 3 files changed, 326 insertions(+), 39 deletions(-) diff --git a/src/states/linear/components_cannon.rs b/src/states/linear/components_cannon.rs index bc631a4..89d3f11 100644 --- a/src/states/linear/components_cannon.rs +++ b/src/states/linear/components_cannon.rs @@ -34,8 +34,16 @@ impl LinearCannonState { #[derive(Component)] pub struct RoundBallProjectile { pub velocity: Vec2, + pub direction: Vec2, pub previous_position: Vec2, pub ball_type: RoundBallType, + pub hit_result: Option, +} + +pub struct ProjectileHit { + pub hit_progress: f32, + pub hit_position: Vec2, + pub segment_index: usize, } @@ -43,4 +51,13 @@ pub struct RoundBallProjectile { pub struct ShotMarker; #[derive(Component)] -pub struct SwapMarker; \ No newline at end of file +pub struct SwapMarker; + +#[derive(Component)] +pub struct IntersectMarker; + +pub struct Intersection { + pub t: f32, + pub world_pos: Vec2, + pub segment_index: usize, +} \ No newline at end of file diff --git a/src/states/linear/plugin.rs b/src/states/linear/plugin.rs index 3b82147..b02d144 100644 --- a/src/states/linear/plugin.rs +++ b/src/states/linear/plugin.rs @@ -24,9 +24,12 @@ impl Plugin for LinearPlayPlugin { .add_systems( Update, ( - draw_track_gizmos, + toggle_debug_systems, + draw_track_gizmos, draw_grid, + // draw_cannon_laser, //несколько систем с соблюдением порядка + calculate_projectile_hits, linear_move_projectiles, cycle_cannon_balls, update_linear_cannon_preview, @@ -48,6 +51,13 @@ fn setup(mut commands: Commands) { commands.insert_resource(build_track); commands.insert_resource(precalculated_track); commands.insert_resource(build_linear_cannon_state()); + + let debug_config = LinearDebugConfig{ + toggle_track: true, + toggle_grid: true, + toggle_laser: false, + }; + commands.insert_resource(debug_config); } fn cleanup(mut commands: Commands, query: Query>) { @@ -60,6 +70,7 @@ fn cleanup(mut commands: Commands, query: Query> commands.remove_resource::(); //зачистить сразу после калькуляции commands.remove_resource::(); commands.remove_resource::(); + commands.remove_resource::(); } fn linear_restart(mut next_state: ResMut>) { diff --git a/src/states/linear/systems_cannon.rs b/src/states/linear/systems_cannon.rs index 023aa28..16865a5 100644 --- a/src/states/linear/systems_cannon.rs +++ b/src/states/linear/systems_cannon.rs @@ -1,10 +1,8 @@ use crate::states::linear::*; +use crate::{FACTOR, HEIGHT, WIDTH}; use bevy::prelude::*; use bevy::window::PrimaryWindow; -use std::f32::consts::FRAC_PI_2; -use bevy::asset::ErasedAssetLoader; -use crate::{FACTOR, HEIGHT, WIDTH}; -use crate::states::level::{BallProjectile, BallType, Cannon, CannonState, CurrentPreviewMarker, NextPreviewMarker, CURRENT_SHOT_Z_INDEX, NEXT_SHOT_Z_INDEX, SLOT_SIZE}; +use std::f32::consts::{FRAC_PI_2, PI}; pub fn spawn_linear_cannon(mut commands: Commands, asset_server: Res) { let image: Handle = asset_server.load("sprites/cannon/cannon.png"); @@ -25,6 +23,8 @@ pub fn rotate_linear_cannon( mut query: Query<&mut Transform, With>, window_query: Query<&Window, With>, camera_query: Query<(&Camera, &GlobalTransform)>, + config: ResMut, //FIXME debug + mut gizmos: Gizmos, ) { let Ok(mut cannon_tf) = query.single_mut() else { return; @@ -51,6 +51,13 @@ pub fn rotate_linear_cannon( let angle = direction.to_angle() - FRAC_PI_2; cannon_tf.rotation = Quat::from_rotation_z(angle); + + if !config.toggle_laser { + return; + } else { + const OUT_OF_SCREEN: f32 = (WIDTH * FACTOR) as f32 * 1.5; + gizmos.line_2d(cannon_pos, direction * OUT_OF_SCREEN, PINK); + }; } pub fn spawn_projectile_from_cannon( @@ -58,7 +65,7 @@ pub fn spawn_projectile_from_cannon( cannon_query: Query<&Transform, With>, mouse_input: Res>, asset_server: Res, - mut cannon_state: ResMut + mut cannon_state: ResMut, ) { if !mouse_input.just_pressed(MouseButton::Left) { return; @@ -79,15 +86,18 @@ pub fn spawn_projectile_from_cannon( commands.spawn(( Sprite { image, - custom_size: Some(Vec2::splat(STEP)), + custom_size: Some(Vec2::splat(ROUND_BALL_SIZE)), ..default() }, Transform::from_translation(spawn_pos), RoundBallProjectile { velocity: direction * 800.0, + direction, // задаем отдельным полем, чтоб сэкономить на вычислениях в рейкастинге previous_position: spawn_pos_2d, ball_type, + hit_result: None, }, + IntersectMarker, // маркер для фильтрации, что шарик-снаряд может иметь пересечения с треком LinearStateMarker, )); cannon_state.fire(); @@ -108,30 +118,32 @@ pub fn update_linear_cannon_preview( shot_query: Query>, swap_query: Query>, ) { - let Ok(cannon_entity) = cannon_query.single() else { return }; - + let Ok(cannon_entity) = cannon_query.single() else { + return; + }; + let shot_image: Handle = asset_server.load(cannon_state.shot.asset_path()); if let Ok(shot_entity) = shot_query.single() { // усли шарик у дула есть, то обновляем спрайт - commands.entity(shot_entity).insert( - Sprite{ - image: shot_image, - custom_size: Some(Vec2::splat(STEP)), - ..default() - } - ); + commands.entity(shot_entity).insert(Sprite { + image: shot_image, + custom_size: Some(Vec2::splat(ROUND_BALL_SIZE)), + ..default() + }); } else { //иначе создаем - let preview_entity = commands.spawn(( - Sprite{ - image: shot_image, - custom_size: Some(Vec2::splat(STEP)), - ..default() - }, - Transform::from_xyz(0.0, STEP * 2.2, LINEAR_CANNON_BALL_Z), - ShotMarker - )).id(); + let preview_entity = commands + .spawn(( + Sprite { + image: shot_image, + custom_size: Some(Vec2::splat(ROUND_BALL_SIZE)), + ..default() + }, + Transform::from_xyz(0.0, STEP * 2.2, LINEAR_CANNON_BALL_Z), + ShotMarker, + )) + .id(); commands.entity(cannon_entity).add_child(preview_entity); } @@ -139,21 +151,23 @@ pub fn update_linear_cannon_preview( let swap_image = asset_server.load(cannon_state.swap.asset_path()); if let Ok(swap_entity) = swap_query.single() { - commands.entity(swap_entity).insert(Sprite{ + commands.entity(swap_entity).insert(Sprite { image: swap_image, - custom_size: Some(Vec2::splat(STEP)), + custom_size: Some(Vec2::splat(ROUND_BALL_SIZE)), ..default() }); } else { - let preview_entity = commands.spawn(( - Sprite{ - image: swap_image, - custom_size: Some(Vec2::splat(STEP)), - ..default() - }, - Transform::from_xyz(0.0, -STEP * 1.5, LINEAR_CANNON_BALL_Z), - SwapMarker, - )).id(); + let preview_entity = commands + .spawn(( + Sprite { + image: swap_image, + custom_size: Some(Vec2::splat(ROUND_BALL_SIZE)), + ..default() + }, + Transform::from_xyz(0.0, -STEP * 1.5, LINEAR_CANNON_BALL_Z), + SwapMarker, + )) + .id(); commands.entity(cannon_entity).add_child(preview_entity); } } @@ -180,10 +194,255 @@ pub fn linear_move_projectiles( proj.previous_position = new_pos; // удаляем снаряды, улетевшие за экран - if new_pos.x.abs() > 1.5 * (WIDTH * FACTOR) as f32 || new_pos.y.abs() > 1.5 * (HEIGHT * FACTOR) as f32 { + if new_pos.x.abs() > 2.0 * (WIDTH * FACTOR) as f32 + || new_pos.y.abs() > 2.0 * (HEIGHT * FACTOR) as f32 + { commands.entity(entity).despawn(); } } } -// pub fn detect_track_hit(){} //TODO \ No newline at end of file +pub fn calculate_projectile_hits( + mut commands: Commands, + mut projectiles: Query< + (Entity, &mut RoundBallProjectile, &mut Transform), + With, + >, + track: Res, + balls_on_track: Query<&RoundBall>, +) { + for (entity, mut proj, mut transform) in projectiles.iter_mut() { + // если уже посчитано место попадания, пропускаем + if proj.hit_result.is_some() { + continue; + } + + // ищем все точки пересечений с треком + let mut intersections = raycast_track(proj.previous_position, proj.direction, &track); + + // сортируем от ближайшего t к дальнему + intersections.sort_by(|a, b| a.t.partial_cmp(&b.t).unwrap()); + + // если есть точки пересечения + if let Some(closest_hit) = intersections.first() { + // собираем шарики + let mut occupied: Vec = balls_on_track.iter().map(|b| b.track_progress).collect(); + // сортируем прогресс для дальнейших вычислений + occupied.sort_by(|a, b| a.partial_cmp(b).unwrap()); + + //считаем прогресс для точки попадания через мировые координаты и индекс сегмента на треке + let hit_progress = calculate_progress(closest_hit, &track); + + let radius_norm = BALL_RADIUS / track.total_length; + if is_occupied(hit_progress, &occupied, radius_norm) { + proj.hit_result = Some(ProjectileHit { + hit_progress, + hit_position: closest_hit.world_pos, + segment_index: closest_hit.segment_index, + }); + println!("Попадание в трек: {}", hit_progress); + transform.translation = closest_hit.world_pos.extend(transform.translation.z); + // убираем маркер поиска + commands.entity(entity).remove::(); + } + } else { + // Пересечений нет -> убираем маркер, снаряд летит дальше + commands.entity(entity).remove::(); + } + } +} + +fn raycast_track(origin: Vec2, direction: Vec2, track: &PrecalculatedTrack) -> Vec { + let mut intersections = vec![]; + + for (i, seg) in track.segments.iter().enumerate() { + match seg.segment_type { + SegementType::Line => { + // ищем пересечение + if let Some(t) = + intersect_line(origin, direction, seg.start_pos, seg.direction, seg.length) + { + let pos = origin + direction * t; + intersections.push(Intersection { + t, + world_pos: pos, + segment_index: i, + }); + } + } + + SegementType::Turn => { + let hits = intersect_arc( + origin, + direction, + seg.center, + seg.radius, + seg.start_angle, + seg.sweep_sign, + ); + + for t in hits { + let pos = origin + direction * t; + intersections.push(Intersection { + t, + world_pos: pos, + segment_index: i, + }); + } + } + } + } + + intersections +} + +fn intersect_line( + origin: Vec2, + direction: Vec2, + seg_start: Vec2, + seg_dir: Vec2, + seg_length: f32, +) -> Option { + // вектор самого отрезка трека + let seg_vec = seg_dir * seg_length; + + // знаменатель: векторное произведение направления луча и вектора отрезка + // x1*y2 - x2*y1 + let denom = direction.x * seg_vec.y - direction.y * seg_vec.x; + + // если знаменатель близок к нулю - луч параллелен отрезку, пересечения нет + if denom.abs() < 1e-6 { + return None; + } + + let w = seg_start - origin; + + // вычисляем параметры t и u + // t - расстояние по лучу + // u - позиция на отрезке (0.0 = начало, 1.0 = конец) + let t = (w.x * seg_vec.y - w.y * seg_vec.x) / denom; + let u = (w.x * direction.y - w.y * direction.x) / denom; + + // проверяем условия попадания + // t > 0 - точка пересечения находится перед пушкой (а не за спиной) + // u >= 0 && u <= 1 - точка пересечения находится внутри отрезка (не на бесконечной линии) + if t > 0.0 && u >= 0.0 && u <= 1.0 { + return Some(t); + } + + None +} + +fn intersect_arc( + origin: Vec2, + direction: Vec2, + center: Vec2, + radius: f32, + start_angle: f32, + sweep_sign: f32, +) -> Vec { + // возвращает список дистанций t до точек пересечения с дугой. + // может вернуть 0, 1 или 2 точки (вход и выход), если луч проходит сквозь дугу. + + let mut hits = Vec::new(); + + // считаем пересечения луча и полной окружности через квадратное уравнение + + let l = origin - center; + let b = 2.0 * l.dot(direction); // поскольку направление нормализовано, a = 1 + let c = l.dot(l) - radius * radius; + + let discriminant = b * b - 4.0 * c; + + // если дискриминант меньше нуля, луч пролетает мимо окружности + if discriminant < 0.0 { + return hits; + } + + let sqrt_discriminant = discriminant.sqrt(); + + // находим корни + let roots = vec![ + (-b - sqrt_discriminant) / 2.0, + (-b + sqrt_discriminant) / 2.0, + ]; + + for t in roots { + // ищем только положительные точки, потому что они будут перед пушкой + if t > 0.0 { + let hit_point = origin + direction * t; + + // попали ли в сектор дуги + let hit_angle = (hit_point - center).to_angle(); + + // находим разницу между углом попадания и стартовым углом + let mut angle_diff = hit_angle - start_angle; + + // нормализуем разницу в диапазон [-PI, PI] + // это нужно, чтобы корректно обрабатывать переход через плюс/минус пи + while angle_diff > PI { + angle_diff -= 2.0 * PI; + } + while angle_diff < -PI { + angle_diff += 2.0 * PI; + } + + // проверяем, лежит ли разница в пределах 90 градусов (пи пополам) с учетом направления + let is_valid_angle = if sweep_sign > 0.0 { + // против часовой + angle_diff >= 0.0 && angle_diff <= FRAC_PI_2 + } else { + // по часовой + angle_diff >= -FRAC_PI_2 && angle_diff <= 0.0 + }; + + if is_valid_angle { + hits.push(t); + } + } + } + + hits +} + +fn calculate_progress(inter: &Intersection, track: &PrecalculatedTrack) -> f32 { + // берем только нужный сегмент трека, где есть пересечение + let track_seg = &track.segments[inter.segment_index]; + + // по типу сегмента переводим точку из мировых координат Vec2 в прогресс трека 0.0 ... 1.0 + match track_seg.segment_type { + SegementType::Line => { + let diff = inter.world_pos - track_seg.start_pos; + let local_t = (diff.x * track_seg.direction.x + diff.y * track_seg.direction.y) + / track_seg.length; + // возвращаем прогресс + track_seg.t_start + (track_seg.t_end - track_seg.t_start) * local_t + } + + SegementType::Turn => { + let hit_angle = (inter.world_pos - track_seg.center).to_angle(); + let mut angle_diff = hit_angle - track_seg.start_angle; + + // критичный момент - важно нормализовать разницу в диапазон от -пи до +пи, чтоб он не улетел + while angle_diff > PI { + angle_diff -= 2.0 * PI + } + while angle_diff < -PI { + angle_diff += 2.0 * PI + } + + // учитываем направление обхода + let local_t = angle_diff / (track_seg.sweep_sign * FRAC_PI_2); + + // возвращаем прогресс + track_seg.t_start + (track_seg.t_end - track_seg.t_start) * local_t + } + } +} + +fn is_occupied(target: f32, occupied: &[f32], radius_norm: f32) -> bool { + let min = target - radius_norm; + let max = target + radius_norm; + + occupied.iter().any(|&p| p >= min && p <= max) +}