check and remove matches + const refactor

This commit is contained in:
nquidox 2026-04-11 19:40:22 +03:00
parent 562b340469
commit 8d673fb3aa
9 changed files with 153 additions and 53 deletions

View file

@ -39,6 +39,7 @@ pub struct WaveState {
pub ball_reached_end: bool, pub ball_reached_end: bool,
pub is_warming_up: bool, pub is_warming_up: bool,
pub is_queue_locked: bool, //отдельно для большей явности pub is_queue_locked: bool, //отдельно для большей явности
pub last_insert_index: Option<usize>, //для системы удаления серий шариков
} }
impl Default for WaveState { impl Default for WaveState {
@ -48,6 +49,7 @@ impl Default for WaveState {
ball_reached_end: false, ball_reached_end: false,
is_warming_up: false, is_warming_up: false,
is_queue_locked: false, is_queue_locked: false,
last_insert_index: None,
} }
} }
} }
@ -65,4 +67,8 @@ pub enum Direction {
Bottom, Bottom,
} }
#[derive(Resource, Default, Debug)]
pub struct Score(pub u32);
#[derive(Component)]
pub struct ScoreTextMarker;

View file

@ -1,6 +1,13 @@
use bevy::prelude::*; use bevy::prelude::*;
use rand::seq::IndexedRandom; use rand::seq::IndexedRandom;
#[derive(Component, Debug, Clone, Copy)]
pub struct Ball {
pub ball_type: BallType,
pub slot_index: usize,
pub slot_progress: f32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum BallType { pub enum BallType {
#[default] #[default]
@ -28,9 +35,3 @@ impl BallType {
} }
} }
#[derive(Component, Debug)]
pub struct Ball {
pub ball_type: BallType,
pub slot_index: usize,
pub slot_progress: f32,
}

View file

@ -0,0 +1,14 @@
use crate::FACTOR;
pub const SHIFT: f32 = 1.0; // коэффициент пересчета для слотов
pub const SLOT_SIZE: f32 = FACTOR as f32 / SHIFT;
pub const BALL_MOVEMENT_SPEED: f32 = 60.0; // пикселей в секунду
pub const INITIAL_BALLS_COUNT: usize = 10;
pub const WARMUP_BALL_MOVEMENT_MULTIPLIER: f32 = 5.0; // во сколько раз больше BALL_MOVEMENT_SPEED
// Z-индексы элементов
pub const CANNON_Z_INDEX: f32 = 10.0;
pub const PROJECTILE_Z_INDEX: f32 = 11.0;
pub const CURRENT_SHOT_Z_INDEX: f32 = 12.0;
pub const NEXT_SHOT_Z_INDEX: f32 = 12.0;

View file

@ -18,3 +18,9 @@ mod components_cannon;
pub use components_cannon::*; pub use components_cannon::*;
mod components_ball; mod components_ball;
pub use components_ball::*; pub use components_ball::*;
mod ui;
pub use ui::*;
mod constants;
pub use constants::*;

View file

@ -2,36 +2,40 @@ use crate::states::AppState::GameState;
use crate::states::level::*; use crate::states::level::*;
use bevy::prelude::*; use bevy::prelude::*;
pub struct LevelPlugin; pub struct LevelPlugin;
impl Plugin for LevelPlugin { impl Plugin for LevelPlugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
app.add_systems(OnEnter(GameState), ( app.add_systems(
setup_level, OnEnter(GameState),
initialize_queue, (setup_level, initialize_queue, setup_cannon).chain(),
setup_cannon).chain()) )
.add_systems( .add_systems(
Update, Update,
(
// debug_draw_grid,
debug_draw_track,
warmup_queue_movement, //до спавна остальных шариков
spawn_new_ball,
check_wave_completion,
//cannon systems
cycle_cannon_type,
update_cannon_preview,
spawn_projectile_from_cannon,
//сохранять порядок верхних трех
rotate_cannon,
move_projectiles,
//сохраняем порядок трех
( (
// debug_draw_grid,
debug_draw_track,
warmup_queue_movement, //до спавна остальных шариков
spawn_new_ball,
move_queue_along_track,
check_wave_completion,
//cannon systems
cycle_cannon_type,
update_cannon_preview,
spawn_projectile_from_cannon,
//сохранять порядок верхних трех
rotate_cannon,
move_projectiles,
detect_projectile_hit, detect_projectile_hit,
check_and_remove_matches,
move_queue_along_track,
) )
.run_if(in_state(GameState)), .chain(),
) )
.add_systems(OnExit(GameState), cleanup_main_menu); .run_if(in_state(GameState)),
)
.add_systems(OnExit(GameState), cleanup_main_menu);
} }
} }

View file

@ -2,8 +2,6 @@ use crate::states::level::*;
use crate::{FACTOR, HEIGHT, WIDTH}; use crate::{FACTOR, HEIGHT, WIDTH};
use bevy::prelude::*; use bevy::prelude::*;
pub const SHIFT: f32 = 1.0;
pub const SLOT_SIZE: f32 = FACTOR as f32 / SHIFT;
pub fn setup_timer(mut commands: Commands){ pub fn setup_timer(mut commands: Commands){
commands.insert_resource(SpawnTimer::default()); commands.insert_resource(SpawnTimer::default());
@ -18,5 +16,8 @@ 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());
println!("Score is set up");
} }

View file

@ -15,11 +15,6 @@ pub fn spawn_new_ball(
return; return;
} }
//до добавления буферных слотов
// let entry_blocked = balls
// .iter()
// .any(|ball| ball.slot_index == 0 && ball.slot_progress < 1.0);
let entry_blocked = balls.iter().any(|ball| { let entry_blocked = balls.iter().any(|ball| {
ball.slot_index == track.spawn_index && ball.slot_progress < 1.0 ball.slot_index == track.spawn_index && ball.slot_progress < 1.0
}); });
@ -50,8 +45,7 @@ pub fn spawn_new_ball(
} }
} }
// FIXME перенести позже в более удобное место/объект левела
const SPEED: f32 = 60.0; // пикселей в секунду
pub fn move_queue_along_track( pub fn move_queue_along_track(
track: Res<TrackPath>, track: Res<TrackPath>,
@ -64,7 +58,7 @@ pub fn move_queue_along_track(
for (mut ball, mut transform) in balls.iter_mut() { for (mut ball, mut transform) in balls.iter_mut() {
// Увеличиваем прогресс // Увеличиваем прогресс
ball.slot_progress += 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 {
@ -92,8 +86,7 @@ pub fn move_queue_along_track(
} }
} }
// FIXME перенести позже в более удобное место/объект левела
const START_COUNT: usize = 10;
pub fn initialize_queue( pub fn initialize_queue(
mut commands: Commands, mut commands: Commands,
@ -102,16 +95,16 @@ pub fn initialize_queue(
mut wave: ResMut<WaveState>, mut wave: ResMut<WaveState>,
) { ) {
// заполняем слоты от -START_COUNT до -1 // заполняем слоты от -START_COUNT до -1
let base_idx = track.spawn_index.saturating_sub(START_COUNT); let base_idx = track.spawn_index.saturating_sub(INITIAL_BALLS_COUNT);
for i in 0..START_COUNT { for i in 0..INITIAL_BALLS_COUNT {
let slot_idx = base_idx + i; // пропускаем индекс 0 (спавн за экраном) let slot_idx = base_idx + i; // пропускаем индекс 0 (спавн за экраном)
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 {}: {:?}", slot_idx, pos); //debug
let target_idx = slot_idx + START_COUNT; 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());
@ -136,8 +129,7 @@ pub fn initialize_queue(
wave.spawning_allowed = false; wave.spawning_allowed = false;
} }
// FIXME перенести позже в более удобное место/объект левела
const WARMUP_MULTIPLIER: f32 = 5.0;
pub fn warmup_queue_movement( pub fn warmup_queue_movement(
track: Res<TrackPath>, track: Res<TrackPath>,
mut wave: ResMut<WaveState>, mut wave: ResMut<WaveState>,
@ -149,7 +141,7 @@ pub fn warmup_queue_movement(
) { ) {
if !wave.is_warming_up { return; } if !wave.is_warming_up { return; }
let speed = SPEED * WARMUP_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);
@ -209,3 +201,82 @@ 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 score: ResMut<Score>,
) {
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();
balls_sorted.sort_by_key(|(_, b)| b.slot_index);
if balls_sorted.is_empty() { 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 {
group_end += 1;
}
let group_size = group_end - group_start;
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 to_remove: Vec<Entity> = balls_sorted[group_start..group_end]
.iter()
.map(|(e, _)| *e)
.collect();
for entity in to_remove.iter() {
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);
// --- СХЛОПЫВАНИЕ ОЧЕРЕДИ ---
if has_left_neighbors && has_right_neighbors {
// 🔸 Удаление ВНУТРИ: сдвигаем все шарики СПРАВА влево
println!("Closing gap ({}..{}), shifting right balls left by {}",
removed_min_idx, removed_max_idx, removed_count);
// Сдвигаем индексы всех шариков с index > removed_max_idx
for (entity, mut ball) in balls.p1().iter_mut() {
if ball.slot_index > removed_max_idx {
ball.slot_index -= removed_count;
ball.slot_progress = 0.0; // "Примагничиваем" к новому слоту
}
}
return;
}
// 🔸 Удаление в НАЧАЛЕ или КОНЦЕ: индексы не меняем
// (опционально: можно сбросить progress у правых соседей для чёткости)
}

View file

@ -2,11 +2,6 @@ use bevy::prelude::*;
use crate::states::level::*; use crate::states::level::*;
use bevy::window::PrimaryWindow; use bevy::window::PrimaryWindow;
const CANNON_Z_INDEX: f32 = 10.0;
const PROJECTILE_Z_INDEX: f32 = 11.0;
const CURRENT_SHOT_Z_INDEX: f32 = 12.0;
const NEXT_SHOT_Z_INDEX: f32 = 12.0;
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");
@ -191,6 +186,8 @@ pub fn detect_projectile_hit(
slot_progress: target_progress, slot_progress: target_progress,
} }
}).remove::<BallProjectile>(); }).remove::<BallProjectile>();
// запоминаем индекс слота, куда попали
wave.last_insert_index = Some(insert_idx);
} }
// Разблокировка (структурные изменения завершены) // Разблокировка (структурные изменения завершены)

View file

@ -29,7 +29,7 @@ pub fn setup_track_with_buffer() -> TrackPath {
TrackPath { TrackPath {
points: full_points, points: full_points,
spawn_index: BUFFER_SLOTS, spawn_index: BUFFER_SLOTS-1,
buffer_size: BUFFER_SLOTS, buffer_size: BUFFER_SLOTS,
} }
} }