fixes + win/loose visuals
This commit is contained in:
parent
9bad37d011
commit
a81f9fa8ac
10 changed files with 504 additions and 221 deletions
|
|
@ -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;
|
||||||
|
|
@ -46,3 +46,9 @@ pub struct BallProjectile {
|
||||||
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, // проверка по квадрату спрайта
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
@ -18,3 +19,5 @@ 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;
|
||||||
|
|
@ -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)),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -19,5 +19,12 @@ pub fn setup_level(mut commands: Commands){
|
||||||
|
|
||||||
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 подумать как объединить элементы интерфейса, для единой точки оформления
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
@ -86,22 +89,24 @@ 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,30 +114,32 @@ 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
|
|
||||||
|
|
||||||
|
//инфа о занимаемых буферных слотах
|
||||||
|
println!("Слот {}: {:?}", slot_idx, pos);
|
||||||
|
|
||||||
|
// слот спавна шариков
|
||||||
let target_idx = slot_idx + INITIAL_BALLS_COUNT;
|
let target_idx = slot_idx + INITIAL_BALLS_COUNT;
|
||||||
|
|
||||||
let ball_type = BallType::random();
|
let ball_type = BallType::random();
|
||||||
|
|
@ -151,28 +158,31 @@ 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);
|
||||||
|
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
|
@ -205,7 +215,7 @@ pub fn warmup_queue_movement(
|
||||||
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 {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
let mut balls_sorted: Vec<(Entity, Ball)> = balls.p0()
|
// собираем и сортируем
|
||||||
.iter()
|
let mut balls_sorted: Vec<(Entity, Ball)> = balls.p0().iter().map(|(e, b)| (e, *b)).collect();
|
||||||
.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() {
|
||||||
|
if ball.slot_index > removed_max_idx && ball.slot_index <= continuous_block_end {
|
||||||
ball.slot_progress = 0.0;
|
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,27 +408,25 @@ 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>>,
|
||||||
|
|
@ -227,6 +128,7 @@ pub fn update_cannon_preview(
|
||||||
) {
|
) {
|
||||||
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() {
|
||||||
|
|
@ -247,6 +149,7 @@ pub fn update_cannon_preview(
|
||||||
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
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue