zuma-like/src/states/level/system_cannon.rs

275 lines
9.1 KiB
Rust
Raw Normal View History

2026-04-10 20:28:55 +03:00
use bevy::prelude::*;
2026-04-11 18:09:30 +03:00
use crate::states::level::*;
2026-04-10 20:28:55 +03:00
use bevy::window::PrimaryWindow;
2026-04-11 15:12:11 +03:00
pub fn setup_cannon(mut commands: Commands, asset_server: Res<AssetServer>) {
2026-04-10 20:28:55 +03:00
let texture: Handle<Image> = asset_server.load("cannon/cannon.png");
commands.spawn((
LevelMarker,
Cannon,
2026-04-11 18:09:30 +03:00
Transform::from_xyz(579.0 - (1280.0 / 2.0), (768.0 / 2.0) - 150.0, CANNON_Z_INDEX),
2026-04-10 20:28:55 +03:00
Sprite::from_image(texture),
Visibility::Visible,
));
2026-04-11 18:09:30 +03:00
println!("Cannon visuals is set up");
2026-04-10 20:28:55 +03:00
}
pub fn rotate_cannon(
mut query: Query<&mut Transform, With<Cannon>>,
window_query: Query<&Window, With<PrimaryWindow>>,
camera_query: Query<(&Camera, &GlobalTransform)>,
) {
2026-04-11 15:12:11 +03:00
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;
};
2026-04-10 20:28:55 +03:00
// Конвертируем экранные координаты курсора в мировые
2026-04-11 15:12:11 +03:00
let Ok(world_cursor_pos) = camera.viewport_to_world_2d(camera_tf, cursor_pos) else {
return;
};
2026-04-10 20:28:55 +03:00
// Вектор направления от пушки к курсору
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>,
2026-04-11 18:09:30 +03:00
mut cannon_state: ResMut<CannonState>
2026-04-10 20:28:55 +03:00
) {
2026-04-11 15:12:11 +03:00
if !mouse_input.just_pressed(MouseButton::Left) {
return;
}
2026-04-10 20:28:55 +03:00
2026-04-11 15:12:11 +03:00
let Ok(cannon_tf) = cannon_query.single() else {
return;
};
2026-04-10 20:28:55 +03:00
2026-04-11 18:09:30 +03:00
let ball_type = cannon_state.current_type;
2026-04-10 20:28:55 +03:00
let image = asset_server.load(ball_type.asset_path());
2026-04-11 15:12:11 +03:00
let offset = 100.0; // сдвиг от центра к дулу пушки
2026-04-10 20:28:55 +03:00
let direction = cannon_tf.rotation.mul_vec3(Vec3::Y).truncate().normalize();
let spawn_pos_2d = cannon_tf.translation.truncate() + direction * offset;
2026-04-11 18:09:30 +03:00
let spawn_pos = spawn_pos_2d.extend(PROJECTILE_Z_INDEX);
2026-04-10 20:28:55 +03:00
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,
},
));
2026-04-11 18:09:30 +03:00
cannon_state.fire();
2026-04-10 20:28:55 +03:00
}
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() {
// Обновляем позицию
2026-04-11 15:12:11 +03:00
let delta = proj.velocity * time.delta_secs();
let new_pos = proj.previous_position + delta;
2026-04-11 18:09:30 +03:00
2026-04-10 20:28:55 +03:00
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>,
2026-04-11 15:12:11 +03:00
mut wave: ResMut<WaveState>,
2026-04-10 20:28:55 +03:00
mut projectiles: Query<(Entity, &mut BallProjectile)>,
2026-04-11 15:12:11 +03:00
mut balls: Query<(Entity, &mut Ball)>,
2026-04-10 20:28:55 +03:00
) {
2026-04-11 15:12:11 +03:00
// пока идёт структурное изменение, спавн и другие мутации ждут
if wave.is_queue_locked {
return;
}
2026-04-11 15:14:27 +03:00
for (proj_entity, proj) in projectiles.iter_mut() {
2026-04-11 15:12:11 +03:00
let mut min_dist_any = f32::MAX;
let mut nearest_idx_any = 0;
2026-04-10 20:28:55 +03:00
for (idx, &slot_pos) in track.points.iter().enumerate() {
let dist = proj.previous_position.distance(slot_pos);
2026-04-11 15:12:11 +03:00
if dist < min_dist_any {
min_dist_any = dist;
nearest_idx_any = idx;
2026-04-10 20:28:55 +03:00
}
}
2026-04-11 15:12:11 +03:00
// если снаряд далеко от трека - промах
if min_dist_any > HIT_THRESHOLD {
// commands.entity(proj_entity).despawn();
continue;
}
2026-04-10 20:28:55 +03:00
2026-04-11 15:12:11 +03:00
let insert_idx;
let target_progress;
2026-04-10 20:28:55 +03:00
2026-04-11 15:12:11 +03:00
if let Some((_, target_ball)) = balls.iter().find(|(_, b)| b.slot_index == nearest_idx_any) {
// Копируем значения в локальные переменные
target_progress = target_ball.slot_progress;
2026-04-10 20:28:55 +03:00
2026-04-11 15:12:11 +03:00
// Определяем индекс вставки
2026-04-11 18:09:30 +03:00
insert_idx = if target_progress <= 0.5 { //TODO check correction
nearest_idx_any - 1
2026-04-10 20:28:55 +03:00
} else {
2026-04-11 18:09:30 +03:00
nearest_idx_any
2026-04-11 15:12:11 +03:00
};
} else {
// Слот пуст — снаряд пролетает мимо
2026-04-11 18:09:30 +03:00
// commands.entity(proj_entity).despawn();
2026-04-11 15:12:11 +03:00
continue;
}
wave.is_queue_locked = true;
// сдвигаем очередь
let max_idx = track.points.len() - 1;
let mut to_despawn = Vec::new();
2026-04-11 18:09:30 +03:00
2026-04-11 15:12:11 +03:00
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);
};
2026-04-10 20:28:55 +03:00
}
}
2026-04-11 18:09:30 +03:00
2026-04-11 15:12:11 +03:00
for entity in to_despawn {
commands.entity(entity).despawn();
}
2026-04-11 18:09:30 +03:00
2026-04-11 15:12:11 +03:00
commands.entity(proj_entity).insert({
Ball {
ball_type: proj.ball_type,
slot_index: insert_idx,
slot_progress: target_progress,
}
}).remove::<BallProjectile>();
2026-04-12 16:39:23 +03:00
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);
2026-04-10 20:28:55 +03:00
}
2026-04-11 15:12:11 +03:00
// Разблокировка (структурные изменения завершены)
wave.is_queue_locked = false;
}
2026-04-11 18:09:30 +03:00
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(),
}
}