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)] #[derive(Component)]
pub struct ScoreTextMarker; 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

@ -45,4 +45,10 @@ pub struct BallProjectile {
pub velocity: Vec2, pub velocity: Vec2,
pub previous_position: Vec2, pub previous_position: Vec2,
pub ball_type: BallType, 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 SHIFT: f32 = 1.0; // коэффициент пересчета для слотов
pub const SLOT_SIZE: f32 = FACTOR as f32 / SHIFT; pub const SLOT_SIZE: f32 = FACTOR as f32 / SHIFT;
pub const BALL_MOVEMENT_SPEED: f32 = 10.0; // пикселей в секунду, базово 60 pub const BALL_MOVEMENT_SPEED: f32 = 30.0; // пикселей в секунду, базово 60
pub const INITIAL_BALLS_COUNT: usize = 10; pub const BUFFER_SLOTS: usize = 20;
pub const WARMUP_BALL_MOVEMENT_MULTIPLIER: f32 = 30.0; // во сколько раз больше BALL_MOVEMENT_SPEED, базово 5 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 pub const HIT_THRESHOLD: f32 = SLOT_SIZE; // для detect_projectile_hit
@ -17,4 +18,6 @@ pub const NEXT_SHOT_Z_INDEX: f32 = 12.0;
pub const SCORE_Z_INDEX: f32 = 100.0; pub const SCORE_Z_INDEX: f32 = 100.0;
pub const TRACK_Z_INDEX: f32 = 9.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) { fn build(&self, app: &mut App) {
app.add_systems( app.add_systems(
OnEnter(GameState), 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( .add_systems(
Update, Update,
@ -36,6 +46,8 @@ impl Plugin for LevelPlugin {
sync_ball_visuals, sync_ball_visuals,
) )
.chain(), .chain(),
update_game_over_ui,
update_game_win_ui,
) )
.run_if(in_state(GameState)), .run_if(in_state(GameState)),
) )

View file

@ -16,8 +16,15 @@ pub fn setup_level(mut commands: Commands){
commands.insert_resource(build_cannon_state()); commands.insert_resource(build_cannon_state());
println!("CannonState is set up"); println!("CannonState is set up");
commands.insert_resource(Score::default()); commands.insert_resource(Score::default());
println!("Score is set up"); 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 crate::states::level::*;
use bevy::prelude::*;
use std::collections::HashSet;
pub fn spawn_new_ball( pub fn spawn_new_ball(
mut commands: Commands, mut commands: Commands,
@ -15,9 +16,9 @@ pub fn spawn_new_ball(
return; return;
} }
let entry_blocked = balls.iter().any(|ball| { let entry_blocked = balls
ball.slot_index == track.spawn_index && ball.slot_progress < 1.0 .iter()
}); .any(|ball| ball.slot_index == track.spawn_index && ball.slot_progress < 1.0);
if timer.is_finished() && !entry_blocked { if timer.is_finished() && !entry_blocked {
if let Some(&spawn_pos) = track.points.first() { if let Some(&spawn_pos) = track.points.first() {
@ -45,8 +46,6 @@ pub fn spawn_new_ball(
} }
} }
pub fn move_queue_along_track( pub fn move_queue_along_track(
track: Res<TrackPath>, track: Res<TrackPath>,
mut balls: Query<(Entity, &mut Ball, &mut Transform)>, mut balls: Query<(Entity, &mut Ball, &mut Transform)>,
@ -54,15 +53,19 @@ pub fn move_queue_along_track(
wave: Res<WaveState>, 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(); let mut balls_sorted: Vec<(Entity, Ball)> = balls.iter().map(|(e, b, _)| (e, *b)).collect();
balls_sorted.sort_by_key(|(_, b)| b.slot_index); 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 active_boundary = {
let mut current_idx = balls_sorted[0].1.slot_index; 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() { for (entity, mut ball, mut transform) in balls.iter_mut() {
//пропускаем все шары в слотах после слота начала разрыва // пропускаем все шары в слотах после слота начала разрыва
if ball.slot_index > active_boundary { if ball.slot_index > active_boundary {
// но при этом обнуляем прогресс у оторванных шариков // но при этом обнуляем прогресс у оторванных шариков
if ball.slot_progress > 0.01 { if ball.slot_progress > 0.01 {
@ -85,23 +88,25 @@ pub fn move_queue_along_track(
} }
continue; continue;
} }
// Увеличиваем прогресс // увеличиваем прогресс
ball.slot_progress += BALL_MOVEMENT_SPEED * time.delta_secs() / SLOT_SIZE; ball.slot_progress += BALL_MOVEMENT_SPEED * time.delta_secs() / SLOT_SIZE;
// Если шарик достиг конца текущего сегмента // если шарик достиг конца текущего сегмента
if ball.slot_progress >= 1.0 { if ball.slot_progress >= 1.0 {
// Если это въезд в трек (был в слоте 0) — переходим в слот 1 // если это въезд в трек (был в слоте 0) - переходим в слот 1
// TODO проверить совсместно с переносом спавн-слота в буферную зону
if ball.slot_index == 0 { if ball.slot_index == 0 {
ball.slot_index = 1; ball.slot_index = 1;
} else { } else {
// Обычное движение: переходим к следующему слоту // обычное движение, переходим к следующему слоту
ball.slot_index += 1; ball.slot_index += 1;
} }
// начинаем прогресс с нуля в новом слоте
ball.slot_progress = 0.0; ball.slot_progress = 0.0;
} }
// Вычисляем визуальную позицию // вычисляем визуальную позицию
let current = track.points.get(ball.slot_index); let current = track.points.get(ball.slot_index);
let next = track.points.get(ball.slot_index + 1); let next = track.points.get(ball.slot_index + 1);
@ -109,31 +114,33 @@ pub fn move_queue_along_track(
let pos = curr_pos.lerp(next_pos, ball.slot_progress); let pos = curr_pos.lerp(next_pos, ball.slot_progress);
transform.translation = pos.extend(transform.translation.z); transform.translation = pos.extend(transform.translation.z);
} else if let Some(&pos) = current { } else if let Some(&pos) = current {
// Последний слот — просто ставим в него // последний слот - просто ставим в него
transform.translation = pos.extend(transform.translation.z); transform.translation = pos.extend(transform.translation.z);
} }
} }
} }
pub fn initialize_queue( pub fn initialize_queue(
mut commands: Commands, mut commands: Commands,
track: Res<TrackPath>, track: Res<TrackPath>,
asset_server: Res<AssetServer>, asset_server: Res<AssetServer>,
mut wave: ResMut<WaveState>, mut wave: ResMut<WaveState>,
) { ) {
// заполняем слоты от -START_COUNT до -1 // заполняем слоты от -INITIAL_BALLS_COUNT до -1
let base_idx = track.spawn_index.saturating_sub(INITIAL_BALLS_COUNT); let base_idx = track.spawn_index.saturating_sub(INITIAL_BALLS_COUNT);
for i in 0..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 { let Some(&pos) = track.points.get(slot_idx) else {
continue; continue;
}; };
println!("Slot {}: {:?}", slot_idx, pos); //debug
let target_idx = slot_idx + INITIAL_BALLS_COUNT; //инфа о занимаемых буферных слотах
println!("Слот {}: {:?}", slot_idx, pos);
// слот спавна шариков
let target_idx = slot_idx + INITIAL_BALLS_COUNT;
let ball_type = BallType::random(); let ball_type = BallType::random();
let image: Handle<Image> = asset_server.load(ball_type.asset_path()); let image: Handle<Image> = asset_server.load(ball_type.asset_path());
@ -151,31 +158,34 @@ pub fn initialize_queue(
slot_progress: 0.0, slot_progress: 0.0,
}, },
LevelMarker, LevelMarker,
WarmupTarget { target_index: target_idx }, WarmupTarget {
target_index: target_idx,
},
)); ));
} }
wave.is_warming_up = true; wave.is_warming_up = true;
wave.spawning_allowed = false; wave.spawning_allowed = false;
} }
pub fn warmup_queue_movement( pub fn warmup_queue_movement(
track: Res<TrackPath>, track: Res<TrackPath>,
mut wave: ResMut<WaveState>, mut wave: ResMut<WaveState>,
mut balls: ParamSet<( mut balls: ParamSet<(
Query<&Ball>, Query<&Ball>,
Query<(&mut Ball, &mut Transform, &WarmupTarget)> Query<(&mut Ball, &mut Transform, &WarmupTarget)>,
)>, )>,
time: Res<Time>, 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 speed = BALL_MOVEMENT_SPEED * WARMUP_BALL_MOVEMENT_MULTIPLIER;
let spawn_slot = track.spawn_index.saturating_sub(1); let spawn_slot = track.spawn_index.saturating_sub(1);
let spawn_slot_free = !balls.p0().iter().any(|b| b.slot_index == spawn_slot); let spawn_slot_free = !balls.p0().iter().any(|b| b.slot_index == spawn_slot);
let mut warmup_complete = true; let mut warmup_complete = true;
for (mut ball, mut transform, target) in balls.p1().iter_mut() { for (mut ball, mut transform, target) in balls.p1().iter_mut() {
@ -190,7 +200,7 @@ pub fn warmup_queue_movement(
} }
} }
// обновление визуальной позиции // вычисляем визуальную позицию
let current = track.points.get(ball.slot_index); let current = track.points.get(ball.slot_index);
let next = track.points.get(ball.slot_index + 1); let next = track.points.get(ball.slot_index + 1);
@ -201,11 +211,11 @@ pub fn warmup_queue_movement(
transform.translation = pos.extend(transform.translation.z); transform.translation = pos.extend(transform.translation.z);
} }
} }
if warmup_complete && spawn_slot_free { if warmup_complete && spawn_slot_free {
wave.is_warming_up = false; wave.is_warming_up = false;
wave.spawning_allowed = true; wave.spawning_allowed = true;
println!("Warmup complete: queue ready"); println!("Прогрев окончен, переход в обычный режим");
} }
} }
@ -213,18 +223,28 @@ pub fn check_wave_completion(
track: Res<TrackPath>, track: Res<TrackPath>,
mut wave: ResMut<WaveState>, mut wave: ResMut<WaveState>,
balls: Query<&Ball>, balls: Query<&Ball>,
mut game_over: ResMut<GameOver>,
score: Res<Score>,
mut game_win: ResMut<GameWin>,
) { ) {
if wave.ball_reached_end { if wave.ball_reached_end {
return; return;
} }
// проверка на количество поинтов для победы
if score.0 == POINTS_TO_WIN {
game_win.0 = true;
return;
}
let last_index = track.points.len() - 1; let last_index = track.points.len() - 1;
for ball in &balls { for ball in &balls {
if ball.slot_index >= last_index { if ball.slot_index >= last_index {
wave.ball_reached_end = true; wave.ball_reached_end = true;
wave.spawning_allowed = false; wave.spawning_allowed = false;
println!("Wave completed: ball reached the end!"); game_over.0 = true;
println!("Игра окончена, шарик дошел до финиша!");
break; break;
} }
} }
@ -233,49 +253,62 @@ pub fn check_wave_completion(
pub fn check_and_remove_matches( pub fn check_and_remove_matches(
mut commands: Commands, mut commands: Commands,
mut wave: ResMut<WaveState>, mut wave: ResMut<WaveState>,
mut balls: ParamSet<( mut balls: ParamSet<(Query<(Entity, &Ball)>, Query<(Entity, &mut Ball)>)>,
Query<(Entity, &Ball)>,
Query<(Entity, &mut Ball)>
)>,
mut score: ResMut<Score>, 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 {
let mut balls_sorted: Vec<(Entity, Ball)> = balls.p0() return;
.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); 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 inserted_ball = balls_sorted[pos].1;
// Ищем группу того же типа // ищем группу того же типа
let mut group_start = pos; let mut group_start = pos;
while group_start > 0 && balls_sorted[group_start - 1].1.ball_type == inserted_ball.ball_type { while group_start > 0 && balls_sorted[group_start - 1].1.ball_type == inserted_ball.ball_type {
group_start -= 1; group_start -= 1;
} }
let mut group_end = pos + 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; group_end += 1;
} }
let group_size = group_end - group_start; 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_min_idx = balls_sorted[group_start].1.slot_index;
let removed_max_idx = balls_sorted[group_end - 1].1.slot_index; let removed_max_idx = balls_sorted[group_end - 1].1.slot_index;
let removed_count = group_size; let removed_count = group_size;
// Проверяем соседей // проверяем соседей
let has_left_neighbors = balls_sorted.iter().any(|(_, b)| b.slot_index < removed_min_idx); let has_left_neighbors = balls_sorted
let has_right_neighbors = balls_sorted.iter().any(|(_, b)| b.slot_index > removed_max_idx); .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] let to_remove: Vec<Entity> = balls_sorted[group_start..group_end]
.iter() .iter()
.map(|(e, _)| *e) .map(|(e, _)| *e)
@ -285,55 +318,87 @@ pub fn check_and_remove_matches(
commands.entity(*entity).despawn(); commands.entity(*entity).despawn();
} }
// начисляем очки от удаленных
let gained = removed_count * 100; let gained = removed_count * 100;
score.0 += gained as u32; 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 { 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) .filter(|(_, b)| b.slot_index < removed_min_idx)
.max_by_key(|(_, b)| b.slot_index) .max_by_key(|(_, b)| b.slot_index)
.map(|(_, b)| b.ball_type); .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) .filter(|(_, b)| b.slot_index > removed_max_idx)
.min_by_key(|(_, b)| b.slot_index) .min_by_key(|(_, b)| b.slot_index)
.map(|(_, b)| b.ball_type); .map(|(_, b)| b.ball_type);
println!("Closing gap ({}..{}), removed {} balls", println!(
removed_min_idx, removed_max_idx, removed_count); "Схлопываем зазор между ({}..{}), удалено {} шариков",
removed_min_idx, removed_max_idx, removed_count
);
// 🔥 КЛЮЧЕВОЕ: вся логика внутри проверки типов // если в зазоре одинаковые соседи
if left_neighbor_type == right_neighbor_type { 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() { 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; ball.slot_index -= removed_count;
} }
} }
// Сбрасываем прогресс ВСЕЙ очереди
// сбрасываем прогресс у сдвинутых шариков
for (entity, mut ball) in balls.p1().iter_mut() { for (entity, mut ball) in balls.p1().iter_mut() {
ball.slot_progress = 0.0; 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); 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 { } else {
// === РАЗНЫЕ ТИПЫ: НИЧЕГО не делаем === // сбрасываем прогресс у оторванной части
// Дыра остаётся, индексы и прогресс не трогаем println!(
println!("Different types ({:?} vs {:?}): gap preserved", "Разные типы ({:?} и {:?}): оставляем зазор",
left_neighbor_type, right_neighbor_type); left_neighbor_type, right_neighbor_type
);
// Сбрасываем прогресс только у шариков ПРАВЕЕ удалённой группы (оторванный хвост)
// сбрасываем прогресс только у шариков правее удалённой группы
for (entity, mut ball) in balls.p1().iter_mut() { 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_progress = 0.0; ball.slot_progress = 0.0;
@ -343,28 +408,26 @@ pub fn check_and_remove_matches(
return; return;
} }
// 🔸 Удаление в НАЧАЛЕ или КОНЦЕ: сбрасываем прогресс всем // сбрасываем прогресс всем
// TODO проверить на баги
for (entity, mut ball) in balls.p1().iter_mut() { for (entity, mut ball) in balls.p1().iter_mut() {
ball.slot_progress = 0.0; ball.slot_progress = 0.0;
} }
} }
pub fn sync_ball_visuals( pub fn sync_ball_visuals(track: Res<TrackPath>, mut balls: Query<(&Ball, &mut Transform)>) {
track: Res<TrackPath>,
mut balls: Query<(&Ball, &mut Transform)>,
) {
for (ball, mut transform) in balls.iter_mut() { for (ball, mut transform) in balls.iter_mut() {
// Получаем точку текущего слота // получаем точку текущего слота
if let Some(&curr_pos) = track.points.get(ball.slot_index) { 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) { let visual_pos = if let Some(&next_pos) = track.points.get(ball.slot_index + 1) {
curr_pos.lerp(next_pos, ball.slot_progress) curr_pos.lerp(next_pos, ball.slot_progress)
} else { } else {
curr_pos curr_pos
}; };
// 🔥 ПРИНУДИТЕЛЬНО ставим позицию и правильный Z // принудительно выставляем позицию и Z-индекс
transform.translation = visual_pos.extend(TRACK_Z_INDEX); transform.translation = visual_pos.extend(TRACK_Z_INDEX);
} }
} }
} }

View file

@ -1,6 +1,7 @@
use bevy::prelude::*; use bevy::prelude::*;
use crate::states::level::*; use crate::states::level::*;
use bevy::window::PrimaryWindow; use bevy::window::PrimaryWindow;
use std::collections::HashSet;
pub fn setup_cannon(mut commands: Commands, asset_server: Res<AssetServer>) { 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");
@ -94,120 +95,20 @@ pub fn move_projectiles(
time: Res<Time>, time: Res<Time>,
) { ) {
for (entity, mut proj, mut transform) in projectiles.iter_mut() { for (entity, mut proj, mut transform) in projectiles.iter_mut() {
// Обновляем позицию // обновляем позицию
let delta = proj.velocity * time.delta_secs(); let delta = proj.velocity * time.delta_secs();
let new_pos = proj.previous_position + delta; 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;
// Опционально: удаляем снаряды, улетевшие за экран // удаляем снаряды, улетевшие за экран
if new_pos.x.abs() > 2000.0 || new_pos.y.abs() > 2000.0 { if new_pos.x.abs() > 2000.0 || new_pos.y.abs() > 2000.0 {
commands.entity(entity).despawn(); 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( pub fn cycle_cannon_type(
mut cannon_state: ResMut<CannonState>, mut cannon_state: ResMut<CannonState>,
mouse_input: Res<ButtonInput<MouseButton>>, mouse_input: Res<ButtonInput<MouseButton>>,
@ -226,7 +127,8 @@ pub fn update_cannon_preview(
next_query: Query<Entity, With<NextPreviewMarker>>, next_query: Query<Entity, With<NextPreviewMarker>>,
) { ) {
let Ok(cannon_entity) = cannon_query.single() else { return }; let Ok(cannon_entity) = cannon_query.single() else { return };
// шарик у дула
let muzzle_image = asset_server.load(cannon_state.current_type.asset_path()); let muzzle_image = asset_server.load(cannon_state.current_type.asset_path());
if let Ok(muzzle_entity) = muzzle_query.single() { if let Ok(muzzle_entity) = muzzle_query.single() {
@ -246,7 +148,8 @@ pub fn update_cannon_preview(
)).id(); )).id();
commands.entity(cannon_entity).add_child(preview_entity); commands.entity(cannon_entity).add_child(preview_entity);
} }
// следующий шарик в хвосте
let next_image = asset_server.load(cannon_state.next_type.asset_path()); let next_image = asset_server.load(cannon_state.next_type.asset_path());
if let Ok(next_entity) = next_query.single() { if let Ok(next_entity) = next_query.single() {
@ -272,3 +175,195 @@ pub fn build_cannon_state() -> CannonState {
next_type: BallType::random(), 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_w = width / 2.0;
let half_h = height / 2.0; let half_h = height / 2.0;
// Вертикальные линии сетки // вертикальные линии сетки
for col in 0..=GRID_COLS as u32 { for col in 0..=GRID_COLS as u32 {
let x = -half_w + col as f32 * SLOT_SIZE; let x = -half_w + col as f32 * SLOT_SIZE;
gizmos.line_2d( 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 { for row in 0..=GRID_ROWS as u32 {
let y = -half_h + row as f32 * SLOT_SIZE; let y = -half_h + row as f32 * SLOT_SIZE;
gizmos.line_2d( 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 row in 0..GRID_ROWS as u32 {
for col in 0..GRID_COLS as u32 { for col in 0..GRID_COLS as u32 {
let cx = -half_w + col as f32 * SLOT_SIZE + SLOT_SIZE / 2.0; 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() ..default()
}, },
TextColor(Color::srgb(237.0 / 255.0, 21.0 / 255.0, 127.0 / 255.0)), 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, DebugSlotNumber,
)); ));
@ -61,7 +61,7 @@ pub fn debug_draw_grid(mut gizmos: Gizmos, mut commands: Commands) {
..default() ..default()
}, },
TextColor(Color::srgb(237.0 / 255.0, 21.0 / 255.0, 127.0 / 255.0)), 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, DebugSlotNumber,
)); ));
} }

View file

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

View file

@ -1,13 +1,18 @@
use bevy::prelude::*; use crate::common::ExitToMainMenu;
use crate::states::level::*; 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( pub fn setup_score_ui(mut commands: Commands) {
mut commands: Commands,
){
commands.spawn(( commands.spawn((
ScoreTextMarker, ScoreTextMarker,
Text2d::new("Score: 0"), Text2d::new("Score: 0"),
TextFont { font_size: 48.0, ..default() }, TextFont {
font_size: 48.0,
..default()
},
TextColor(Color::WHITE), TextColor(Color::WHITE),
Transform::from_xyz(320.0, 300.0, SCORE_Z_INDEX), Transform::from_xyz(320.0, 300.0, SCORE_Z_INDEX),
Visibility::Visible, Visibility::Visible,
@ -15,13 +20,97 @@ pub fn setup_score_ui(
)); ));
} }
pub fn update_score_text( pub fn update_score_text(score: Res<Score>, mut query: Query<&mut Text2d, With<ScoreTextMarker>>) {
score: Res<Score>,
mut query: Query<&mut Text2d, With<ScoreTextMarker>>,
) {
if score.is_changed() { if score.is_changed() {
if let Ok(mut text) = query.single_mut() { if let Ok(mut text) = query.single_mut() {
text.0 = format!("Score: {}", score.0); 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;
}
}