Compare commits

..

4 commits

Author SHA1 Message Date
nquidox
67ddcb0c65 factor ou projectile components and systems 2026-04-19 22:18:55 +03:00
nquidox
eeebdbd272 moved critical systems to fixed update schedule 2026-04-18 21:55:27 +03:00
nquidox
f502cdb0ce debug logger 2026-04-18 21:55:00 +03:00
nquidox
8520323cca format + todo 2026-04-18 21:54:43 +03:00
8 changed files with 486 additions and 337 deletions

View file

@ -2,36 +2,44 @@ use crate::common::messages::ExitRequestMessage;
use crate::common::plugin::CommonUiPlugin; use crate::common::plugin::CommonUiPlugin;
use crate::common::systems::exit_system; use crate::common::systems::exit_system;
use bevy::camera::ScalingMode; use bevy::camera::ScalingMode;
use bevy::log::LogPlugin;
use bevy::prelude::*; use bevy::prelude::*;
mod common; mod common;
mod gameplay; mod gameplay;
mod states; mod states;
mod ui; mod ui;
use crate::states::game::state::MainGameState; use crate::states::game::state::MainGameState;
use crate::states::linear::plugin::LinearPlayPlugin;
use crate::states::main_menu::state::MainMenuState; use crate::states::main_menu::state::MainMenuState;
use crate::states::settings_menu::state::SettingsMenuState; use crate::states::settings_menu::state::SettingsMenuState;
use crate::states::linear::plugin::LinearPlayPlugin;
const FACTOR: u32 = 40; const FACTOR: u32 = 40;
const WIDTH: u32 = 32; const WIDTH: u32 = 32;
const HEIGHT: u32 = 18; const HEIGHT: u32 = 18;
fn main() { fn main() {
App::new() App::new()
.add_message::<ExitRequestMessage>() .add_message::<ExitRequestMessage>()
.add_plugins(DefaultPlugins.set(WindowPlugin { .add_plugins(
primary_window: Some(Window { DefaultPlugins
title: "alpha v0.2".into(), .set(WindowPlugin {
resolution: (WIDTH * FACTOR, HEIGHT * FACTOR).into(), primary_window: Some(Window {
resizable: true, title: "alpha v0.2".into(),
..default() resolution: (WIDTH * FACTOR, HEIGHT * FACTOR).into(),
}), resizable: true,
..default() ..default()
})) }),
..default()
})
// выводит много инфы, раскоментить по необходимости
// .set(LogPlugin {
// level: bevy::log::Level::DEBUG,
// filter: "bevy_ecs=debug".to_string(),
// ..default()
// }),
)
.add_plugins(( .add_plugins((
CommonUiPlugin, CommonUiPlugin,
MainMenuState, MainMenuState,

View file

@ -31,33 +31,9 @@ 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<ProjectileHit>,
}
pub struct ProjectileHit {
pub hit_progress: f32,
pub hit_position: Vec2,
pub segment_index: usize,
}
#[derive(Component)] #[derive(Component)]
pub struct ShotMarker; pub struct ShotMarker;
#[derive(Component)] #[derive(Component)]
pub struct SwapMarker; pub struct SwapMarker;
#[derive(Component)]
pub struct IntersectMarker;
pub struct Intersection {
pub t: f32,
pub world_pos: Vec2,
pub segment_index: usize,
}

View file

@ -0,0 +1,33 @@
use bevy::math::Vec2;
use bevy::prelude::Component;
use crate::states::linear::RoundBallType;
#[derive(Component)]
pub struct RoundBallProjectile {
pub velocity: Vec2,
pub direction: Vec2,
pub previous_position: Vec2,
pub ball_type: RoundBallType,
pub hit_result: Option<ProjectileHit>,
pub target_position: Option<Vec2>,
}
#[derive(Copy, Clone)]
pub struct ProjectileHit {
pub hit_progress: f32,
pub hit_position: Vec2,
pub segment_index: usize,
}
#[derive(Component)]
pub struct IntersectMarker;
pub struct Intersection {
pub t: f32,
pub world_pos: Vec2,
pub segment_index: usize,
}
#[derive(Component)]
pub struct InsertMarker;

View file

@ -25,6 +25,11 @@ mod components_cannon;
pub use components_cannon::*; pub use components_cannon::*;
mod systems_cannon; mod systems_cannon;
pub use systems_cannon::*; pub use systems_cannon::*;
mod systems_projectile;
pub use systems_projectile::*;
mod components_projectile;
pub use components_projectile::*;

View file

@ -13,34 +13,43 @@ pub enum LinearUpdateSet {
impl Plugin for LinearPlayPlugin { impl Plugin for LinearPlayPlugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
app app.add_systems(
.add_systems(OnEnter(LinearPlayState), (setup, spawn_linear_cannon).chain()) OnEnter(LinearPlayState),
.add_systems(OnEnter(LinearGameRestart), linear_restart) (setup, spawn_linear_cannon).chain(),
.add_plugins(LinearUIPlugin) )
.configure_sets( .add_systems(OnEnter(LinearGameRestart), linear_restart)
Update, .add_plugins(LinearUIPlugin)
LinearUpdateSet::Track.run_if(in_state(LinearPlayState)), .configure_sets(
) Update,
.add_systems( LinearUpdateSet::Track.run_if(in_state(LinearPlayState)),
Update, ).configure_sets(
( FixedUpdate,
toggle_debug_systems, LinearUpdateSet::Track.run_if(in_state(LinearPlayState)),
draw_track_gizmos, )
draw_grid, .add_systems(
// draw_cannon_laser, Update,
//несколько систем с соблюдением порядка (
calculate_projectile_hits, toggle_debug_systems,
linear_move_projectiles, draw_track_gizmos,
cycle_cannon_balls, draw_grid,
update_linear_cannon_preview, // draw_cannon_laser,
spawn_projectile_from_cannon, cycle_cannon_balls,
spawn_round_ball, update_linear_cannon_preview,
move_round_balls, spawn_projectile_from_cannon,
rotate_linear_cannon, rotate_linear_cannon,
) ).in_set(LinearUpdateSet::Track),
.in_set(LinearUpdateSet::Track), )
) .add_systems(
.add_systems(OnExit(LinearPlayState), cleanup); FixedUpdate,
(
spawn_round_ball,
move_round_balls,
calculate_projectile_hits,
linear_move_projectiles,
insert_projectile_into_track,
).in_set(LinearUpdateSet::Track),
)
.add_systems(OnExit(LinearPlayState), cleanup);
} }
} }
@ -52,9 +61,9 @@ fn setup(mut commands: Commands) {
commands.insert_resource(precalculated_track); commands.insert_resource(precalculated_track);
commands.insert_resource(build_linear_cannon_state()); commands.insert_resource(build_linear_cannon_state());
let debug_config = LinearDebugConfig{ let debug_config = LinearDebugConfig {
toggle_track: true, toggle_track: false,
toggle_grid: true, toggle_grid: false,
toggle_laser: false, toggle_laser: false,
}; };
commands.insert_resource(debug_config); commands.insert_resource(debug_config);

View file

@ -96,6 +96,7 @@ pub fn spawn_projectile_from_cannon(
previous_position: spawn_pos_2d, previous_position: spawn_pos_2d,
ball_type, ball_type,
hit_result: None, hit_result: None,
target_position: None,
}, },
IntersectMarker, // маркер для фильтрации, что шарик-снаряд может иметь пересечения с треком IntersectMarker, // маркер для фильтрации, что шарик-снаряд может иметь пересечения с треком
LinearStateMarker, LinearStateMarker,
@ -180,269 +181,3 @@ pub fn cycle_cannon_balls(
cannon_state.cycle(); cannon_state.cycle();
} }
} }
pub fn linear_move_projectiles(
mut commands: Commands,
mut projectiles: Query<(Entity, &mut RoundBallProjectile, &mut Transform)>,
time: Res<Time>,
) {
for (entity, mut proj, mut transform) in projectiles.iter_mut() {
let delta = proj.velocity * time.delta_secs();
let new_pos = proj.previous_position + delta;
transform.translation = new_pos.extend(transform.translation.z);
proj.previous_position = new_pos;
// удаляем снаряды, улетевшие за экран
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 calculate_projectile_hits(
mut commands: Commands,
mut projectiles: Query<
(Entity, &mut RoundBallProjectile, &mut Transform),
With<IntersectMarker>,
>,
track: Res<PrecalculatedTrack>,
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<f32> = 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::<IntersectMarker>();
}
} else {
// Пересечений нет -> убираем маркер, снаряд летит дальше
commands.entity(entity).remove::<IntersectMarker>();
}
}
}
fn raycast_track(origin: Vec2, direction: Vec2, track: &PrecalculatedTrack) -> Vec<Intersection> {
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<f32> {
// вектор самого отрезка трека
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<f32> {
// возвращает список дистанций 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)
}

View file

@ -0,0 +1,384 @@
use crate::states::linear::*;
use crate::{FACTOR, HEIGHT, WIDTH};
use bevy::asset::AssetServer;
use bevy::math::Vec2;
use bevy::prelude::*;
use std::f32::consts::{FRAC_PI_2, PI};
pub fn linear_move_projectiles(
mut commands: Commands,
mut projectiles: Query<(Entity, &mut RoundBallProjectile, &mut Transform)>,
time: Res<Time>,
) {
for (entity, mut proj, mut transform) in projectiles.iter_mut() {
let delta = proj.velocity * time.delta_secs();
let new_pos = proj.previous_position + delta;
transform.translation = new_pos.extend(transform.translation.z);
proj.previous_position = new_pos;
// удаляем снаряды, улетевшие за экран
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 calculate_projectile_hits(
mut commands: Commands,
mut projectiles: Query<
(Entity, &mut RoundBallProjectile, &mut Transform),
With<IntersectMarker>,
>,
track: Res<PrecalculatedTrack>,
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<f32> = 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::<IntersectMarker>()
// .insert(InsertMarker);
// }
let mut occupied: Vec<f32> = balls_on_track.iter().map(|b| b.track_progress).collect();
occupied.sort_by(|a, b| a.partial_cmp(b).unwrap());
let radius_norm = BALL_RADIUS / track.total_length;
let mut target_hit = None;
for inter in &intersections {
let hit_progress = calculate_progress(inter, &track);
if is_occupied(hit_progress, &occupied, radius_norm) {
target_hit = Some((inter, hit_progress));
break; // Нашли цель — выходим из цикла
}
}
if let Some((target_inter, hit_progress)) = target_hit {
// ПОПАДАНИЕ: снэпим именно к той точке, которая занята!
proj.hit_result = Some(ProjectileHit {
hit_progress,
hit_position: target_inter.world_pos,
segment_index: target_inter.segment_index,
});
// записываем координаты точки попадания, до которой летит шарик
// вероятен момент, когда шарик летит, а в точке пересечения появится шарики на треке
// если вылезет такое, пофиксить через проверку всех точек пересечения, а не только
// конкретной, которая тут записана
proj.target_position = Some(target_inter.world_pos);
// Переключаем маркер: больше не ищем, готовы к вставке
commands.entity(entity)
.remove::<IntersectMarker>()
.insert(InsertMarker);
} else {
// Пересечений нет -> убираем маркер, снаряд летит дальше
commands.entity(entity).remove::<IntersectMarker>();
}
}
}
fn raycast_track(origin: Vec2, direction: Vec2, track: &PrecalculatedTrack) -> Vec<Intersection> {
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<f32> {
// вектор самого отрезка трека
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<f32> {
// возвращает список дистанций 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)
}
pub fn insert_projectile_into_track(
mut commands: Commands,
mut projectiles: Query<(Entity, &RoundBallProjectile), With<InsertMarker>>,
mut balls_on_track: Query<(Entity, &mut RoundBall)>,
track: Res<PrecalculatedTrack>,
asset_server: Res<AssetServer>,
) {
for (proj_entity, proj) in projectiles.iter_mut() {
let Some(hit) = proj.hit_result else {
continue;
};
let Some(target_pos) = proj.target_position else { continue };
let distance = proj.previous_position.distance(target_pos);
// расстояние, при котором встраивается шарик
const ARRIVAL_DIST: f32 = 5.0;
if distance > ARRIVAL_DIST {
continue;
}
// ищем шарик, в который попали (цель)
// собираем данные, чтобы не конфликтовать с мутабельным доступом ниже
let mut balls_data: Vec<_> = balls_on_track
.iter()
.map(|(entity, ball)| (entity, ball.track_progress))
.collect();
// сортируем по близости к точке удара
balls_data.sort_by(|a, b| {
let diff_a = (a.1 - hit.hit_progress).abs();
let diff_b = (b.1 - hit.hit_progress).abs();
diff_a.partial_cmp(&diff_b).unwrap()
});
let Some((target_entity, original_target_progress)) = balls_data.first() else {
continue;
};
// копируем значение
let original_progress = *original_target_progress;
// размер сдвига посчитан в треке
let shift = track.min_spawn_gap;
// ставим на место шарика-цели, а сам шарик-цель сдвигаем вперед по треку
for (entity, mut ball) in balls_on_track.iter_mut() {
if entity == *target_entity {
ball.track_progress = (original_progress + shift).clamp(0.0, 1.0);
}
}
for (entity, mut ball) in balls_on_track.iter_mut() {
ball.track_progress = (ball.track_progress + shift).clamp(0.0, 1.0);
}
// спавним новый шарик на месте цели
commands.spawn((
Sprite {
image: asset_server.load(proj.ball_type.asset_path()),
custom_size: Some(Vec2::splat(ROUND_BALL_SIZE)),
..default()
},
Transform::from_translation(hit.hit_position.extend(ROUND_BALL_Z)),
RoundBall {
ball_type: proj.ball_type,
track_progress: original_progress,
},
LinearStateMarker,
));
// деспавн шарика-снаряда
commands.entity(proj_entity).despawn();
}
}

View file

@ -4,7 +4,7 @@ use crate::states::linear::plugin::LinearUpdateSet;
use crate::states::linear::{DebugToggle, LinearRestartMarker, LinearStateMarker}; use crate::states::linear::{DebugToggle, LinearRestartMarker, LinearStateMarker};
use crate::ui::button_click::ButtonClickMessage; use crate::ui::button_click::ButtonClickMessage;
use crate::ui::click::handle_click_system; use crate::ui::click::handle_click_system;
use crate::ui::{ButtonStyle, spawn_button, spawn_background}; use crate::ui::{ButtonStyle, spawn_button};
use bevy::prelude::*; use bevy::prelude::*;
use crate::{FACTOR, HEIGHT, WIDTH}; use crate::{FACTOR, HEIGHT, WIDTH};
@ -67,6 +67,7 @@ fn setup_ui(mut commands: Commands, asset_server: Res<AssetServer>) {
Transform::from_xyz(0.0, 0.0, -10.0), Transform::from_xyz(0.0, 0.0, -10.0),
LinearStateMarker, LinearStateMarker,
)); ));
// TODO - изменить функцию спавна бгшки, чтоб можно было задавать параметры окна
// spawn_background(&mut commands, bg_image, LinearStateMarker); // spawn_background(&mut commands, bg_image, LinearStateMarker);
spawn_button( spawn_button(
@ -100,8 +101,6 @@ fn setup_ui(mut commands: Commands, asset_server: Res<AssetServer>) {
&button_style, &button_style,
DebugToggle::Laser, DebugToggle::Laser,
); );
} }
pub fn restart_game_button_system( pub fn restart_game_button_system(