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) +}