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

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