fixes + win/loose visuals

This commit is contained in:
nquidox 2026-04-12 23:34:36 +03:00
parent 9bad37d011
commit a81f9fa8ac
10 changed files with 504 additions and 221 deletions

View file

@ -72,3 +72,13 @@ pub struct Score(pub u32);
#[derive(Component)]
pub struct ScoreTextMarker;
#[derive(Resource, Default)]
pub struct GameOver(pub bool);
#[derive(Component)]
pub struct GameOverMarker;
#[derive(Resource, Default)]
pub struct GameWin(pub bool);
#[derive(Component)]
pub struct GameWinMarker;

View file

@ -46,3 +46,9 @@ pub struct BallProjectile {
pub previous_position: Vec2,
pub ball_type: BallType,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HitboxType {
Circle, // проверка по радиусу от центра слота
Square, // проверка по квадрату спрайта
}

View file

@ -3,9 +3,10 @@ use crate::FACTOR;
pub const SHIFT: f32 = 1.0; // коэффициент пересчета для слотов
pub const SLOT_SIZE: f32 = FACTOR as f32 / SHIFT;
pub const BALL_MOVEMENT_SPEED: f32 = 10.0; // пикселей в секунду, базово 60
pub const INITIAL_BALLS_COUNT: usize = 10;
pub const WARMUP_BALL_MOVEMENT_MULTIPLIER: f32 = 30.0; // во сколько раз больше BALL_MOVEMENT_SPEED, базово 5
pub const BALL_MOVEMENT_SPEED: f32 = 30.0; // пикселей в секунду, базово 60
pub const BUFFER_SLOTS: usize = 20;
pub const INITIAL_BALLS_COUNT: usize = 15;
pub const WARMUP_BALL_MOVEMENT_MULTIPLIER: f32 = 10.0; // во сколько раз больше BALL_MOVEMENT_SPEED, базово 5
pub const HIT_THRESHOLD: f32 = SLOT_SIZE; // для detect_projectile_hit
@ -18,3 +19,5 @@ pub const NEXT_SHOT_Z_INDEX: f32 = 12.0;
pub const SCORE_Z_INDEX: f32 = 100.0;
pub const TRACK_Z_INDEX: f32 = 9.0;
pub const POINTS_TO_WIN: u32 = 1000;

View file

@ -8,7 +8,17 @@ impl Plugin for LevelPlugin {
fn build(&self, app: &mut App) {
app.add_systems(
OnEnter(GameState),
(setup_level, initialize_queue, setup_cannon, setup_score_ui).chain(),
(
(
setup_level,
initialize_queue,
setup_cannon,
setup_score_ui,
setup_ui,
reset_in_game_text,
)
.chain(),
),
)
.add_systems(
Update,
@ -36,6 +46,8 @@ impl Plugin for LevelPlugin {
sync_ball_visuals,
)
.chain(),
update_game_over_ui,
update_game_win_ui,
)
.run_if(in_state(GameState)),
)

View file

@ -19,5 +19,12 @@ pub fn setup_level(mut commands: Commands){
commands.insert_resource(Score::default());
println!("Score is set up");
commands.insert_resource(GameOver::default());
println!("Game over ui is set up");
commands.insert_resource(GameWin::default());
println!("Game win ui is set up");
//TODO подумать как объединить элементы интерфейса, для единой точки оформления
}

View file

@ -1,5 +1,6 @@
use bevy::prelude::*;
use crate::states::level::*;
use bevy::prelude::*;
use std::collections::HashSet;
pub fn spawn_new_ball(
mut commands: Commands,
@ -15,9 +16,9 @@ pub fn spawn_new_ball(
return;
}
let entry_blocked = balls.iter().any(|ball| {
ball.slot_index == track.spawn_index && 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() {
@ -45,8 +46,6 @@ pub fn spawn_new_ball(
}
}
pub fn move_queue_along_track(
track: Res<TrackPath>,
mut balls: Query<(Entity, &mut Ball, &mut Transform)>,
@ -54,15 +53,19 @@ pub fn move_queue_along_track(
wave: Res<WaveState>,
) {
// проверяем, не идет ли прогрев трека
if wave.is_warming_up { return; }
if wave.is_warming_up {
return;
}
//собираем все шары в вектор и сортируем по индексам слотов
// собираем все шары в вектор и сортируем по индексам слотов
let mut balls_sorted: Vec<(Entity, Ball)> = balls.iter().map(|(e, b, _)| (e, *b)).collect();
balls_sorted.sort_by_key(|(_, b)| b.slot_index);
if balls_sorted.is_empty() { return; }
if balls_sorted.is_empty() {
return;
}
//ищем первый пустой слот для определения разрыва
// ищем первый пустой слот для определения разрыва
let active_boundary = {
let mut current_idx = balls_sorted[0].1.slot_index;
@ -77,7 +80,7 @@ pub fn move_queue_along_track(
};
for (entity, mut ball, mut transform) in balls.iter_mut() {
//пропускаем все шары в слотах после слота начала разрыва
// пропускаем все шары в слотах после слота начала разрыва
if ball.slot_index > active_boundary {
// но при этом обнуляем прогресс у оторванных шариков
if ball.slot_progress > 0.01 {
@ -86,22 +89,24 @@ pub fn move_queue_along_track(
continue;
}
// Увеличиваем прогресс
// увеличиваем прогресс
ball.slot_progress += BALL_MOVEMENT_SPEED * time.delta_secs() / SLOT_SIZE;
// Если шарик достиг конца текущего сегмента
// если шарик достиг конца текущего сегмента
if ball.slot_progress >= 1.0 {
// Если это въезд в трек (был в слоте 0) — переходим в слот 1
// если это въезд в трек (был в слоте 0) - переходим в слот 1
// TODO проверить совсместно с переносом спавн-слота в буферную зону
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);
@ -109,30 +114,32 @@ pub fn move_queue_along_track(
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);
}
}
}
pub fn initialize_queue(
mut commands: Commands,
track: Res<TrackPath>,
asset_server: Res<AssetServer>,
mut wave: ResMut<WaveState>,
) {
// заполняем слоты от -START_COUNT до -1
// заполняем слоты от -INITIAL_BALLS_COUNT до -1
let base_idx = track.spawn_index.saturating_sub(INITIAL_BALLS_COUNT);
for i in 0..INITIAL_BALLS_COUNT {
let slot_idx = base_idx + i; // пропускаем индекс 0 (спавн за экраном)
// пропускаем индекс 0 (спавн за экраном), чек тудушку в спавнере шарика (~100 строка)
let slot_idx = base_idx + i;
let Some(&pos) = track.points.get(slot_idx) else {
continue;
};
println!("Slot {}: {:?}", slot_idx, pos); //debug
//инфа о занимаемых буферных слотах
println!("Слот {}: {:?}", slot_idx, pos);
// слот спавна шариков
let target_idx = slot_idx + INITIAL_BALLS_COUNT;
let ball_type = BallType::random();
@ -151,28 +158,31 @@ pub fn initialize_queue(
slot_progress: 0.0,
},
LevelMarker,
WarmupTarget { target_index: target_idx },
WarmupTarget {
target_index: target_idx,
},
));
}
wave.is_warming_up = true;
wave.spawning_allowed = false;
}
pub fn warmup_queue_movement(
track: Res<TrackPath>,
mut wave: ResMut<WaveState>,
mut balls: ParamSet<(
Query<&Ball>,
Query<(&mut Ball, &mut Transform, &WarmupTarget)>
Query<(&mut Ball, &mut Transform, &WarmupTarget)>,
)>,
time: Res<Time>,
) {
if !wave.is_warming_up { return; }
if !wave.is_warming_up {
return;
}
// прогрев идет на повышенной скорости
let speed = BALL_MOVEMENT_SPEED * WARMUP_BALL_MOVEMENT_MULTIPLIER;
let spawn_slot = track.spawn_index.saturating_sub(1);
let spawn_slot_free = !balls.p0().iter().any(|b| b.slot_index == spawn_slot);
@ -190,7 +200,7 @@ pub fn warmup_queue_movement(
}
}
// обновление визуальной позиции
// вычисляем визуальную позицию
let current = track.points.get(ball.slot_index);
let next = track.points.get(ball.slot_index + 1);
@ -205,7 +215,7 @@ pub fn warmup_queue_movement(
if warmup_complete && spawn_slot_free {
wave.is_warming_up = false;
wave.spawning_allowed = true;
println!("Warmup complete: queue ready");
println!("Прогрев окончен, переход в обычный режим");
}
}
@ -213,18 +223,28 @@ pub fn check_wave_completion(
track: Res<TrackPath>,
mut wave: ResMut<WaveState>,
balls: Query<&Ball>,
mut game_over: ResMut<GameOver>,
score: Res<Score>,
mut game_win: ResMut<GameWin>,
) {
if wave.ball_reached_end {
return;
}
// проверка на количество поинтов для победы
if score.0 == POINTS_TO_WIN {
game_win.0 = true;
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!");
game_over.0 = true;
println!("Игра окончена, шарик дошел до финиша!");
break;
}
}
@ -233,49 +253,62 @@ pub fn check_wave_completion(
pub fn check_and_remove_matches(
mut commands: Commands,
mut wave: ResMut<WaveState>,
mut balls: ParamSet<(
Query<(Entity, &Ball)>,
Query<(Entity, &mut Ball)>
)>,
mut balls: ParamSet<(Query<(Entity, &Ball)>, Query<(Entity, &mut Ball)>)>,
mut score: ResMut<Score>,
) {
let Some(insert_idx) = wave.last_insert_index.take() else { return; };
// проверка по "маркеру попадания"
let Some(insert_idx) = wave.last_insert_index.take() else {
return;
};
let mut balls_sorted: Vec<(Entity, Ball)> = balls.p0()
.iter()
.map(|(e, b)| (e, *b))
.collect();
// собираем и сортируем
let mut balls_sorted: Vec<(Entity, Ball)> = balls.p0().iter().map(|(e, b)| (e, *b)).collect();
balls_sorted.sort_by_key(|(_, b)| b.slot_index);
if balls_sorted.is_empty() { return; }
if balls_sorted.is_empty() {
return;
}
let Some(pos) = balls_sorted.iter().position(|(_, b)| b.slot_index == insert_idx) else { return; };
let Some(pos) = balls_sorted
.iter()
.position(|(_, b)| b.slot_index == insert_idx)
else {
return;
};
let inserted_ball = balls_sorted[pos].1;
// Ищем группу того же типа
// ищем группу того же типа
let mut group_start = pos;
while group_start > 0 && balls_sorted[group_start - 1].1.ball_type == inserted_ball.ball_type {
group_start -= 1;
}
let mut group_end = pos + 1;
while group_end < balls_sorted.len() && balls_sorted[group_end].1.ball_type == inserted_ball.ball_type {
while group_end < balls_sorted.len()
&& balls_sorted[group_end].1.ball_type == inserted_ball.ball_type
{
group_end += 1;
}
let group_size = group_end - group_start;
if group_size < 3 { return; }
if group_size < 3 {
return;
}
// Запоминаем диапазон до удаления
// запоминаем диапазон до удаления
let removed_min_idx = balls_sorted[group_start].1.slot_index;
let removed_max_idx = balls_sorted[group_end - 1].1.slot_index;
let removed_count = group_size;
// Проверяем соседей
let has_left_neighbors = balls_sorted.iter().any(|(_, b)| b.slot_index < removed_min_idx);
let has_right_neighbors = balls_sorted.iter().any(|(_, b)| b.slot_index > removed_max_idx);
// проверяем соседей
let has_left_neighbors = balls_sorted
.iter()
.any(|(_, b)| b.slot_index < removed_min_idx);
let has_right_neighbors = balls_sorted
.iter()
.any(|(_, b)| b.slot_index > removed_max_idx);
// --- УДАЛЕНИЕ ---
// собираем на удаление
let to_remove: Vec<Entity> = balls_sorted[group_start..group_end]
.iter()
.map(|(e, _)| *e)
@ -285,55 +318,87 @@ pub fn check_and_remove_matches(
commands.entity(*entity).despawn();
}
// начисляем очки от удаленных
let gained = removed_count * 100;
score.0 += gained as u32;
println!("Match at idx {}! +{} points | total: {}", insert_idx, gained, score.0);
println!(
"Совпадение шаров по индексу: {} | начислено +{} очков | в сумме: {}",
insert_idx, gained, score.0
);
// --- СХЛОПЫВАНИЕ ОЧЕРЕДИ ---
// схлапывание очереди + каскад
if has_left_neighbors && has_right_neighbors {
// Находим ближайшего левого соседа (максимальный индекс среди тех, что левее удаляемых)
let left_neighbor_type = balls_sorted.iter()
// ищем ближайшего левого соседа
let left_neighbor_type = balls_sorted
.iter()
.filter(|(_, b)| b.slot_index < removed_min_idx)
.max_by_key(|(_, b)| b.slot_index)
.map(|(_, b)| b.ball_type);
// Находим ближайшего правого соседа (минимальный индекс среди тех, что правее удаляемых)
let right_neighbor_type = balls_sorted.iter()
// ищем ближайшего правого соседа
let right_neighbor_type = balls_sorted
.iter()
.filter(|(_, b)| b.slot_index > removed_max_idx)
.min_by_key(|(_, b)| b.slot_index)
.map(|(_, b)| b.ball_type);
println!("Closing gap ({}..{}), removed {} balls",
removed_min_idx, removed_max_idx, removed_count);
println!(
"Схлопываем зазор между ({}..{}), удалено {} шариков",
removed_min_idx, removed_max_idx, removed_count
);
// 🔥 КЛЮЧЕВОЕ: вся логика внутри проверки типов
// если в зазоре одинаковые соседи
if left_neighbor_type == right_neighbor_type {
// === ОДИНАКОВЫЕ ТИПЫ: полное схлопывание ===
println!(" → Same type ({:?}): full collapse + reset", left_neighbor_type);
// одинаковые типы шариков, схлопываем с учетом зазоров
println!("Совпадение типов ({:?}): схлопываем", left_neighbor_type);
// Сдвигаем индексы правых шариков влево
// ищем конец НЕПРЕРЫВНОГО блока справа от удалённой группы
// строим набор занятых индексов (после удаления!)
let occupied: HashSet<usize> = balls.p0().iter().map(|(_, b)| b.slot_index).collect();
let mut continuous_block_end = removed_max_idx;
loop {
let next_idx = continuous_block_end + 1;
if occupied.contains(&next_idx) {
continuous_block_end = next_idx;
} else {
// нашли разрыв, останавливаемся.
break;
}
}
// сдвигаем индексы только у шариков в НЕПРЕРЫВНОМ блоке
for (entity, mut ball) in balls.p1().iter_mut() {
if ball.slot_index > removed_max_idx {
if ball.slot_index > removed_max_idx && ball.slot_index <= continuous_block_end {
ball.slot_index -= removed_count;
}
}
// Сбрасываем прогресс ВСЕЙ очереди
// сбрасываем прогресс у сдвинутых шариков
for (entity, mut ball) in balls.p1().iter_mut() {
if ball.slot_index > removed_max_idx && ball.slot_index <= continuous_block_end {
ball.slot_progress = 0.0;
}
}
// записываем ласт хит, чтоб имитировать триггер каскадного уничтожения
// записываем last_insert_index для каскада
// имитируем попадание шарика, чтоб триггернуть каскадную проверку схлапывания
wave.last_insert_index = Some(removed_min_idx - 1);
println!("Cascade armed at idx {}", removed_min_idx - 1);
println!(
"Каскад на: {}, окончание блока: {}",
removed_min_idx - 1,
continuous_block_end
);
} else {
// === РАЗНЫЕ ТИПЫ: НИЧЕГО не делаем ===
// Дыра остаётся, индексы и прогресс не трогаем
println!("Different types ({:?} vs {:?}): gap preserved",
left_neighbor_type, right_neighbor_type);
// сбрасываем прогресс у оторванной части
println!(
"Разные типы ({:?} и {:?}): оставляем зазор",
left_neighbor_type, right_neighbor_type
);
// Сбрасываем прогресс только у шариков ПРАВЕЕ удалённой группы (оторванный хвост)
// сбрасываем прогресс только у шариков правее удалённой группы
for (entity, mut ball) in balls.p1().iter_mut() {
if ball.slot_index > removed_max_idx {
ball.slot_progress = 0.0;
@ -343,27 +408,25 @@ pub fn check_and_remove_matches(
return;
}
// 🔸 Удаление в НАЧАЛЕ или КОНЦЕ: сбрасываем прогресс всем
// сбрасываем прогресс всем
// TODO проверить на баги
for (entity, mut ball) in balls.p1().iter_mut() {
ball.slot_progress = 0.0;
}
}
pub fn sync_ball_visuals(
track: Res<TrackPath>,
mut balls: Query<(&Ball, &mut Transform)>,
) {
pub fn sync_ball_visuals(track: Res<TrackPath>, mut balls: Query<(&Ball, &mut Transform)>) {
for (ball, mut transform) in balls.iter_mut() {
// Получаем точку текущего слота
// получаем точку текущего слота
if let Some(&curr_pos) = track.points.get(ball.slot_index) {
// Если есть следующий слот — интерполируем, иначе — просто центр текущего
// если есть следующий слот - интерполируем, иначе просто центр текущего
let visual_pos = if let Some(&next_pos) = track.points.get(ball.slot_index + 1) {
curr_pos.lerp(next_pos, ball.slot_progress)
} else {
curr_pos
};
// 🔥 ПРИНУДИТЕЛЬНО ставим позицию и правильный Z
// принудительно выставляем позицию и Z-индекс
transform.translation = visual_pos.extend(TRACK_Z_INDEX);
}
}

View file

@ -1,6 +1,7 @@
use bevy::prelude::*;
use crate::states::level::*;
use bevy::window::PrimaryWindow;
use std::collections::HashSet;
pub fn setup_cannon(mut commands: Commands, asset_server: Res<AssetServer>) {
let texture: Handle<Image> = asset_server.load("cannon/cannon.png");
@ -94,120 +95,20 @@ pub fn move_projectiles(
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() > 2000.0 || new_pos.y.abs() > 2000.0 {
commands.entity(entity).despawn();
}
}
}
pub fn detect_projectile_hit(
mut commands: Commands,
track: Res<TrackPath>,
mut wave: ResMut<WaveState>,
mut projectiles: Query<(Entity, &mut BallProjectile)>,
mut balls: Query<(Entity, &mut Ball)>,
) {
// пока идёт структурное изменение, спавн и другие мутации ждут
if wave.is_queue_locked {
return;
}
for (proj_entity, proj) in projectiles.iter_mut() {
let mut min_dist_any = f32::MAX;
let mut nearest_idx_any = 0;
for (idx, &slot_pos) in track.points.iter().enumerate() {
let dist = proj.previous_position.distance(slot_pos);
if dist < min_dist_any {
min_dist_any = dist;
nearest_idx_any = idx;
}
}
// если снаряд далеко от трека - промах
if min_dist_any > HIT_THRESHOLD {
// commands.entity(proj_entity).despawn();
continue;
}
let insert_idx;
let target_progress;
if let Some((_, target_ball)) = balls.iter().find(|(_, b)| b.slot_index == nearest_idx_any) {
// Копируем значения в локальные переменные
target_progress = target_ball.slot_progress;
// Определяем индекс вставки
insert_idx = if target_progress <= 0.5 { //TODO check correction
nearest_idx_any - 1
} else {
nearest_idx_any
};
} 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>();
if let Some(&curr_pos) = track.points.get(insert_idx) {
// Вычисляем позицию: интерполяция или центр слота
let visual_pos = if let Some(&next_pos) = track.points.get(insert_idx + 1) {
curr_pos.lerp(next_pos, target_progress)
} else {
curr_pos
};
// Обновляем Transform: ставим на трек и выравниваем Z (10.0),
// чтобы шарик не висел «над» очередью (PROJECTILE_Z_INDEX)
commands.entity(proj_entity).insert(
Transform::from_translation(visual_pos.extend(10.0))
);
}
// запоминаем индекс слота, куда попали
wave.last_insert_index = Some(insert_idx);
}
// Разблокировка (структурные изменения завершены)
wave.is_queue_locked = false;
}
pub fn cycle_cannon_type(
mut cannon_state: ResMut<CannonState>,
mouse_input: Res<ButtonInput<MouseButton>>,
@ -227,6 +128,7 @@ pub fn update_cannon_preview(
) {
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() {
@ -247,6 +149,7 @@ pub fn update_cannon_preview(
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() {
@ -272,3 +175,195 @@ pub fn build_cannon_state() -> CannonState {
next_type: BallType::random(),
}
}
const HITBOX_TYPE: HitboxType = HitboxType::Circle;
const HIT_THRESHOLD: f32 = SLOT_SIZE * 0.9; // Радиус/половина стороны хитбокса
fn project_point_to_segment(point: Vec2, p1: Vec2, p2: Vec2) -> Vec2 {
// вспомогательная функция для детекта попадания
// проекция точки на отрезок, возвращает ближайшую точку на отрезке [p1, p2] к точке point
let segment = p2 - p1;
let len_sq = segment.length_squared();
if len_sq < 0.0001 { return p1; } // защита от деления на ноль
// проекция: t = 0 -> p1, t = 1 -> p2, t < 0 или > 1 -> за пределами отрезка
let t = ((point - p1).dot(segment)) / len_sq;
let t_clamped = t.clamp(0.0, 1.0);
p1 + segment * t_clamped
}
pub fn detect_projectile_hit(
mut commands: Commands,
track: Res<TrackPath>,
mut wave: ResMut<WaveState>,
mut projectiles: Query<(Entity, &mut BallProjectile)>,
mut balls: Query<(Entity, &mut Ball)>,
) {
if wave.is_queue_locked {
return;
}
for (proj_entity, proj) in projectiles.iter_mut() {
// ищем ближайшую точку на треке
// TODO почистить старые неиспользуемые вещи
let mut nearest_proj_point = Vec2::ZERO;
let mut nearest_segment_idx = 0;
let mut min_dist = f32::MAX;
let mut t_along_segment = 0.0;
for i in 0..track.points.len() - 1 {
let p1 = track.points[i];
let p2 = track.points[i + 1];
let proj_point = project_point_to_segment(proj.previous_position, p1, p2);
let dist = proj.previous_position.distance(proj_point);
if dist < min_dist {
min_dist = dist;
nearest_proj_point = proj_point;
nearest_segment_idx = i;
let segment = p2 - p1;
t_along_segment = if segment.length_squared() > 0.0001 {
((proj_point - p1).dot(segment)) / segment.length_squared()
} else {
0.0
};
}
}
// проверяем попадание
let hit = match HITBOX_TYPE {
HitboxType::Circle => {
min_dist < HIT_THRESHOLD
}
HitboxType::Square => {
let p1 = track.points[nearest_segment_idx];
let p2 = track.points[nearest_segment_idx + 1];
is_point_in_oriented_rect(proj.previous_position, p1, p2, HIT_THRESHOLD)
}
};
if !hit {
continue;
}
// определяем индекс для вставки
let target_slot_idx = if t_along_segment < 0.5 {
nearest_segment_idx
} else {
nearest_segment_idx + 1
};
let insert_idx;
let target_progress;
if let Some((_, target_ball)) = balls.iter().find(|(_, b)| b.slot_index == target_slot_idx) {
target_progress = target_ball.slot_progress;
let proj_before_ball = t_along_segment < target_progress;
insert_idx = if proj_before_ball {
if target_slot_idx > 0 { target_slot_idx - 1 } else { 0 }
} else {
target_slot_idx
};
} else {
continue;
}
// лочим очередь для мутаций
wave.is_queue_locked = true;
// ищем конец непрерывной очереди начиная с insert_idx
// строим набор занятых индексов (обязательно до вставки)
let occupied: HashSet<usize> = balls.iter()
.map(|(_, b)| b.slot_index)
.collect();
let mut continuous_block_end = insert_idx;
loop {
let next_idx = continuous_block_end + 1;
if occupied.contains(&next_idx) {
continuous_block_end = next_idx;
} else {
// разрыв найден, остановка
break;
}
}
// сдвигаем шарики внутри непрерывного блока
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 && ball.slot_index <= continuous_block_end {
if ball.slot_index < max_idx {
ball.slot_index += 1;
} else {
to_despawn.push(entity);
}
}
// шарики за разрывом ball.slot_index > continuous_block_end не двигаем
}
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>();
// сразу визуально синхронизируем
if let Some(&curr_pos) = track.points.get(insert_idx) {
let visual_pos = if let Some(&next_pos) = track.points.get(insert_idx + 1) {
curr_pos.lerp(next_pos, target_progress)
} else {
curr_pos
};
commands.entity(proj_entity).insert(
Transform::from_translation(visual_pos.extend(10.0))
);
}
wave.last_insert_index = Some(insert_idx);
wave.is_queue_locked = false;
}
}
fn is_point_in_oriented_rect(point: Vec2, p1: Vec2, p2: Vec2, half_width: f32) -> bool {
// вспомогательная функция для квадратного хитбокса
// проверяет находится ли point внутри прямоугольника на отрезке [p1, p2] с половинной толщиной
let segment = p2 - p1;
let segment_len = segment.length();
if segment_len < 0.0001 {
return point.distance(p1) < half_width; // вырожденный сегмент
}
// локальная система координат: ось x вдоль сегмента, ось y перпендикуляр
let dir = segment.normalize();
let perp = Vec2::new(-dir.y, dir.x); // перпендикуляр
// вектор от начала сегмента до точки
let to_point = point - p1;
// проекции на локальные оси
let along = to_point.dot(dir); // координата вдоль сегмента
let across = to_point.dot(perp); // координата поперёк сегмента
// проверка границ прямоугольника
let within_length = along >= -half_width && along <= segment_len + half_width;
let within_width = across.abs() <= half_width;
within_length && within_width
}

View file

@ -15,7 +15,7 @@ pub fn debug_draw_grid(mut gizmos: Gizmos, mut commands: Commands) {
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(
@ -25,7 +25,7 @@ pub fn debug_draw_grid(mut gizmos: Gizmos, mut commands: Commands) {
);
}
// Горизонтальные линии сетки
// горизонтальные линии сетки
for row in 0..=GRID_ROWS as u32 {
let y = -half_h + row as f32 * SLOT_SIZE;
gizmos.line_2d(
@ -35,7 +35,7 @@ pub fn debug_draw_grid(mut gizmos: Gizmos, mut commands: Commands) {
);
}
// Точки в центрах слотов (удобно для сверки логики)
// точки в центрах слотов
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;
@ -50,7 +50,7 @@ pub fn debug_draw_grid(mut gizmos: Gizmos, mut commands: Commands) {
..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, чтобы было поверх всего
Transform::from_translation(Vec3::new(cx, cy + 8.0, 100.0)),
DebugSlotNumber,
));
@ -61,7 +61,7 @@ pub fn debug_draw_grid(mut gizmos: Gizmos, mut commands: Commands) {
..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, чтобы было поверх всего
Transform::from_translation(Vec3::new(cx, cy - 8.0, 100.0)),
DebugSlotNumber,
));
}

View file

@ -3,8 +3,6 @@ use bevy::math::Vec2;
use bevy::prelude::*;
use crate::states::level::*;
const BUFFER_SLOTS: usize = 20;
pub fn setup_track_with_buffer() -> TrackPath {
// берем только видимые слоты
let visual_points = make_track();
@ -29,7 +27,7 @@ pub fn setup_track_with_buffer() -> TrackPath {
TrackPath {
points: full_points,
spawn_index: BUFFER_SLOTS-1,
spawn_index: BUFFER_SLOTS-5,
buffer_size: BUFFER_SLOTS,
}
}

View file

@ -1,13 +1,18 @@
use bevy::prelude::*;
use crate::common::ExitToMainMenu;
use crate::states::level::*;
use crate::states::settings_menu::components::SettingsRootMarker;
use crate::ui::{ButtonStyle, spawn_background, spawn_button};
use bevy::asset::ErasedAssetLoader;
use bevy::prelude::*;
pub fn setup_score_ui(
mut commands: Commands,
){
pub fn setup_score_ui(mut commands: Commands) {
commands.spawn((
ScoreTextMarker,
Text2d::new("Score: 0"),
TextFont { font_size: 48.0, ..default() },
TextFont {
font_size: 48.0,
..default()
},
TextColor(Color::WHITE),
Transform::from_xyz(320.0, 300.0, SCORE_Z_INDEX),
Visibility::Visible,
@ -15,13 +20,97 @@ pub fn setup_score_ui(
));
}
pub fn update_score_text(
score: Res<Score>,
mut query: Query<&mut Text2d, With<ScoreTextMarker>>,
) {
pub fn update_score_text(score: Res<Score>, mut query: Query<&mut Text2d, With<ScoreTextMarker>>) {
if score.is_changed() {
if let Ok(mut text) = query.single_mut() {
text.0 = format!("Score: {}", score.0);
}
}
}
pub fn setup_ui(mut commands: Commands) {
//красный текст по центру при проигрыще - когда шарик дошел до конечной точки трека
commands.spawn((
GameOverMarker,
Text2d::new("GAME OVER"),
TextFont {
font_size: 48.0,
..default()
},
TextColor(Color::linear_rgb(1.0, 0.0, 0.0)),
Transform::from_xyz(0.0, 0.0, SCORE_Z_INDEX),
Visibility::Hidden,
LevelMarker,
));
//при победе - когда набрали определенное количество очков
commands.spawn((
GameWinMarker,
Text2d::new("VICTORY!"),
TextFont {
font_size: 48.0,
..default()
},
TextColor(Color::linear_rgb(0.0, 1.0, 0.0)),
Transform::from_xyz(0.0, 0.0, SCORE_Z_INDEX),
Visibility::Hidden,
LevelMarker,
));
}
pub fn update_game_over_ui(
game_over: Option<Res<GameOver>>,
mut query: Query<&mut Visibility, With<GameOverMarker>>,
) {
let Some(game_over) = game_over else {
return;
};
if game_over.is_changed() {
if let Ok(mut vis) = query.single_mut() {
*vis = if game_over.0 {
Visibility::Visible
} else {
Visibility::Hidden
};
}
}
}
pub fn update_game_win_ui(
res: Option<Res<GameWin>>,
mut query: Query<&mut Visibility, With<GameWinMarker>>,
) {
let Some(game_win) = res else {
return;
};
if game_win.is_changed() {
if let Ok(mut vis) = query.single_mut() {
*vis = if game_win.0 {
Visibility::Visible
} else {
Visibility::Hidden
};
}
}
}
pub fn reset_in_game_text(
mut game_over: ResMut<GameOver>,
mut game_win: ResMut<GameWin>,
mut query: ParamSet<(
Query<&mut Visibility, With<GameOverMarker>>,
Query<&mut Visibility, With<GameWinMarker>>,
)>,
) {
game_over.0 = false;
if let Ok(mut vis) = query.p0().single_mut() {
*vis = Visibility::Hidden;
}
game_win.0 = false;
if let Ok(mut vis) = query.p1().single_mut() {
*vis = Visibility::Hidden;
}
}