zuma-like/src/states/level/system_cannon.rs
2026-04-12 23:34:36 +03:00

369 lines
No EOL
13 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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");
commands.spawn((
LevelMarker,
Cannon,
Transform::from_xyz(579.0 - (1280.0 / 2.0), (768.0 / 2.0) - 150.0, CANNON_Z_INDEX),
Sprite::from_image(texture),
Visibility::Visible,
));
println!("Cannon visuals is set up");
}
pub fn rotate_cannon(
mut query: Query<&mut Transform, With<Cannon>>,
window_query: Query<&Window, With<PrimaryWindow>>,
camera_query: Query<(&Camera, &GlobalTransform)>,
) {
let Ok(mut cannon_tf) = query.single_mut() else {
return;
};
let Ok(window) = window_query.single() else {
return;
};
let Some(cursor_pos) = window.cursor_position() else {
return;
};
let Ok((camera, camera_tf)) = camera_query.single() else {
return;
};
// Конвертируем экранные координаты курсора в мировые
let Ok(world_cursor_pos) = camera.viewport_to_world_2d(camera_tf, cursor_pos) else {
return;
};
// Вектор направления от пушки к курсору
let cannon_pos = Vec2::new(cannon_tf.translation.x, cannon_tf.translation.y);
let direction = world_cursor_pos - cannon_pos;
// Применяем вращение по оси Z
let angle = direction.to_angle() - std::f32::consts::FRAC_PI_2;
cannon_tf.rotation = Quat::from_rotation_z(angle);
}
pub fn spawn_projectile_from_cannon(
mut commands: Commands,
cannon_query: Query<&Transform, With<Cannon>>,
mouse_input: Res<ButtonInput<MouseButton>>,
asset_server: Res<AssetServer>,
mut cannon_state: ResMut<CannonState>
) {
if !mouse_input.just_pressed(MouseButton::Left) {
return;
}
let Ok(cannon_tf) = cannon_query.single() else {
return;
};
let ball_type = cannon_state.current_type;
let image = asset_server.load(ball_type.asset_path());
let offset = 100.0; // сдвиг от центра к дулу пушки
let direction = cannon_tf.rotation.mul_vec3(Vec3::Y).truncate().normalize();
let spawn_pos_2d = cannon_tf.translation.truncate() + direction * offset;
let spawn_pos = spawn_pos_2d.extend(PROJECTILE_Z_INDEX);
commands.spawn((
LevelMarker,
Transform::from_translation(spawn_pos),
Sprite {
image,
custom_size: Some(Vec2::splat(SLOT_SIZE)),
..default()
},
Visibility::Visible,
BallProjectile {
velocity: direction * 800.0,
previous_position: spawn_pos_2d,
ball_type,
},
));
cannon_state.fire();
}
pub fn move_projectiles(
mut commands: Commands,
mut projectiles: Query<(Entity, &mut BallProjectile, &mut Transform)>,
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 cycle_cannon_type(
mut cannon_state: ResMut<CannonState>,
mouse_input: Res<ButtonInput<MouseButton>>,
) {
if mouse_input.just_pressed(MouseButton::Right) {
cannon_state.cycle_next();
}
}
pub fn update_cannon_preview(
mut commands: Commands,
cannon_state: Res<CannonState>,
asset_server: Res<AssetServer>,
cannon_query: Query<Entity, With<Cannon>>,
muzzle_query: Query<Entity, With<CurrentPreviewMarker>>,
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() {
// усли шарик у дула есть, то обновляем спрайт
commands.entity(muzzle_entity).insert(Sprite::from_image(muzzle_image));
} else {
//иначе создаем
let preview_entity = commands.spawn((
CurrentPreviewMarker,
Sprite{
image: muzzle_image,
custom_size: Some(Vec2::splat(SLOT_SIZE)),
..default()
},
Transform::from_xyz(0.0, 100.0, CURRENT_SHOT_Z_INDEX),
Visibility::Visible,
)).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() {
commands.entity(next_entity).insert(Sprite::from_image(next_image));
} else {
let preview_entity = commands.spawn((
NextPreviewMarker,
Sprite{
image: next_image,
custom_size: Some(Vec2::splat(SLOT_SIZE)),
..default()
},
Transform::from_xyz(0.0, -50.0, NEXT_SHOT_Z_INDEX),
Visibility::Visible,
)).id();
commands.entity(cannon_entity).add_child(preview_entity);
}
}
pub fn build_cannon_state() -> CannonState {
CannonState {
current_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
}