use bevy::prelude::*;
use bevy::window::PrimaryWindow;
use crate::level_format::{INDESTRUCTIBLE_BRICK, SIMPLE_BRICK};
use crate::systems::textures::loader::ObjectClass;
use crate::systems::textures::TypeVariantRegistry;
use crate::{
Brick, BrickTypeId, CountsTowardsCompletion, CELL_HEIGHT, CELL_WIDTH, GRID_HEIGHT, GRID_WIDTH,
PLANE_H, PLANE_W,
};
use bevy_rapier3d::prelude::*;
#[derive(Resource, Default)]
pub struct PaletteState {
pub open: bool,
}
#[derive(Resource, Default)]
pub struct SelectedBrick {
pub type_id: Option<u8>,
}
#[derive(Component)]
pub struct PaletteRoot;
#[derive(Component, Debug)]
pub struct PalettePreview {
pub type_id: u8,
pub material: Option<Handle<StandardMaterial>>,
}
#[derive(Component)]
pub struct GhostPreview;
#[derive(Component, Debug)]
pub struct PreviewViewport {
pub type_id: u8,
pub mesh: Handle<Mesh>,
pub material: Option<Handle<StandardMaterial>>,
}
pub fn toggle_palette(keyboard: Res<ButtonInput<KeyCode>>, mut state: ResMut<PaletteState>) {
if keyboard.just_pressed(KeyCode::KeyP) {
state.open = !state.open;
}
}
pub fn ensure_palette_ui(
state: Res<PaletteState>,
mut commands: Commands,
existing: Query<Entity, With<PaletteRoot>>,
registry: Option<Res<'_, TypeVariantRegistry>>,
materials_res: Option<Res<'_, Assets<StandardMaterial>>>,
mut meshes_res: Option<ResMut<'_, Assets<Mesh>>>,
) {
if !state.is_changed() {
return;
}
if state.open {
if !existing.is_empty() {
return;
}
let material_20 = registry
.as_ref()
.and_then(|r| r.get(ObjectClass::Brick, SIMPLE_BRICK));
let base_color_20 = material_20.as_ref().and_then(|h| {
materials_res
.as_ref()
.and_then(|m| m.get(h).map(|mat| mat.base_color))
});
let material_90 = registry
.as_ref()
.and_then(|r| r.get(ObjectClass::Brick, INDESTRUCTIBLE_BRICK));
let base_color_90 = material_90.as_ref().and_then(|h| {
materials_res
.as_ref()
.and_then(|m| m.get(h).map(|mat| mat.base_color))
});
commands
.spawn((Node { ..default() }, PaletteRoot))
.with_children(|parent| {
parent.spawn((
Text::new("Designer Palette"),
TextFont {
font_size: 16.0,
..default()
},
TextColor(Color::WHITE),
));
parent.spawn((
Text::new(format!("{} — Simple Brick", SIMPLE_BRICK)),
TextFont {
font_size: 14.0,
..default()
},
TextColor(Color::WHITE),
));
parent.spawn((
Node {
width: Val::Px(48.0),
height: Val::Px(24.0),
margin: UiRect::all(Val::Px(4.0)),
..default()
},
BackgroundColor(base_color_20.unwrap_or(Color::srgba(0.5, 0.5, 0.5, 1.0))),
PalettePreview {
type_id: SIMPLE_BRICK,
material: material_20.clone(),
},
Button,
));
parent.spawn((
Text::new(format!(
"{} — Indestructible (won't count toward completion)",
INDESTRUCTIBLE_BRICK
)),
TextFont {
font_size: 14.0,
..default()
},
TextColor(Color::srgba(1.0, 0.84, 0.0, 1.0)),
));
parent.spawn((
Node {
width: Val::Px(48.0),
height: Val::Px(24.0),
margin: UiRect::all(Val::Px(4.0)),
..default()
},
BackgroundColor(base_color_90.unwrap_or(Color::srgba(0.5, 0.5, 0.5, 1.0))),
PalettePreview {
type_id: INDESTRUCTIBLE_BRICK,
material: material_90.clone(),
},
Button,
));
});
if let Some(meshes) = meshes_res.as_mut() {
let mesh_20 = meshes.add(Cuboid::new(0.5, 0.2, 0.5));
if let Some(mat) = material_20.clone() {
commands.spawn((
Mesh3d(mesh_20.clone()),
MeshMaterial3d(mat.clone()),
Transform::from_xyz(0.0, 0.0, 0.0),
PreviewViewport {
type_id: SIMPLE_BRICK,
mesh: mesh_20.clone(),
material: Some(mat.clone()),
},
));
} else {
commands.spawn((
Mesh3d(mesh_20.clone()),
PreviewViewport {
type_id: SIMPLE_BRICK,
mesh: mesh_20.clone(),
material: None,
},
));
}
let mesh_90 = meshes.add(Cuboid::new(0.5, 0.2, 0.5));
if let Some(mat) = material_90.clone() {
commands.spawn((
Mesh3d(mesh_90.clone()),
MeshMaterial3d(mat.clone()),
Transform::from_xyz(0.0, 0.0, 0.0),
PreviewViewport {
type_id: INDESTRUCTIBLE_BRICK,
mesh: mesh_90.clone(),
material: Some(mat.clone()),
},
));
} else {
commands.spawn((
Mesh3d(mesh_90.clone()),
PreviewViewport {
type_id: INDESTRUCTIBLE_BRICK,
mesh: mesh_90.clone(),
material: None,
},
));
}
}
} else {
for e in existing.iter() {
commands.entity(e).despawn();
}
}
}
pub fn handle_palette_selection(
interactions: Query<(&Interaction, &PalettePreview), Changed<Interaction>>,
mut selected: ResMut<SelectedBrick>,
) {
for (interaction, preview) in interactions.iter() {
if *interaction == Interaction::Pressed {
selected.type_id = Some(preview.type_id);
info!("Selected brick type {}", preview.type_id);
}
}
}
pub fn update_palette_selection_feedback(
selected: Res<SelectedBrick>,
mut previews: Query<(&PalettePreview, &mut BackgroundColor)>,
materials_res: Option<Res<Assets<StandardMaterial>>>,
) {
for (preview, mut bg_color) in previews.iter_mut() {
if Some(preview.type_id) == selected.type_id {
*bg_color = BackgroundColor(Color::srgba(1.0, 1.0, 0.0, 1.0));
} else {
let base_color = preview.material.as_ref().and_then(|h| {
materials_res
.as_ref()
.and_then(|m| m.get(h).map(|mat| mat.base_color))
});
*bg_color = BackgroundColor(base_color.unwrap_or(Color::srgba(0.5, 0.5, 0.5, 1.0)));
}
}
}
fn cursor_to_grid(
cursor_pos: Vec2,
_window: &Window,
camera_transform: &GlobalTransform,
camera: &Camera,
) -> Option<(usize, usize)> {
let ray = camera
.viewport_to_world(camera_transform, cursor_pos)
.ok()?;
let ray_direction = *ray.direction;
if ray_direction.y.abs() < 0.001 {
return None; }
let t = -ray.origin.y / ray_direction.y;
if t < 0.0 {
return None; }
let intersection = ray.origin + ray_direction * t;
let x_normalized = (intersection.x + PLANE_H / 2.0) / PLANE_H;
let z_normalized = (intersection.z + PLANE_W / 2.0) / PLANE_W;
if !(0.0..1.0).contains(&x_normalized) || !(0.0..1.0).contains(&z_normalized) {
return None; }
let grid_x = (x_normalized * GRID_HEIGHT as f32).floor() as usize;
let grid_z = (z_normalized * GRID_WIDTH as f32).floor() as usize;
Some((grid_x, grid_z))
}
pub fn update_ghost_preview(
mut commands: Commands,
selected: Res<SelectedBrick>,
window: Query<&Window, With<PrimaryWindow>>,
camera_query: Query<(&GlobalTransform, &Camera), With<Camera3d>>,
ghost: Query<Entity, With<GhostPreview>>,
registry: Option<Res<TypeVariantRegistry>>,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
let Ok(window) = window.single() else {
return;
};
let Ok((camera_transform, camera)) = camera_query.single() else {
return;
};
let Some(cursor_pos) = window.cursor_position() else {
for entity in ghost.iter() {
commands.entity(entity).despawn();
}
return;
};
let Some(type_id) = selected.type_id else {
for entity in ghost.iter() {
commands.entity(entity).despawn();
}
return;
};
let Some((grid_x, grid_z)) = cursor_to_grid(cursor_pos, window, camera_transform, camera)
else {
for entity in ghost.iter() {
commands.entity(entity).despawn();
}
return;
};
let world_x = -PLANE_H / 2.0 + (grid_x as f32 + 0.5) * CELL_HEIGHT;
let world_z = -PLANE_W / 2.0 + (grid_z as f32 + 0.5) * CELL_WIDTH;
let world_pos = Vec3::new(world_x, 0.5, world_z);
let material = registry
.as_ref()
.and_then(|r| r.get(ObjectClass::Brick, type_id))
.unwrap_or_else(|| {
materials.add(StandardMaterial {
base_color: Color::srgba(0.5, 0.5, 0.5, 0.5),
alpha_mode: AlphaMode::Blend,
..default()
})
});
if let Some(ghost_entity) = ghost.iter().next() {
commands
.entity(ghost_entity)
.insert(Transform::from_translation(world_pos));
} else {
commands.spawn((
Mesh3d(meshes.add(Cuboid::new(CELL_HEIGHT * 0.9, 0.4, CELL_WIDTH * 0.9))),
MeshMaterial3d(material),
Transform::from_translation(world_pos),
GhostPreview,
));
}
}
pub fn place_bricks_on_drag(
mut commands: Commands,
selected: Res<SelectedBrick>,
mouse: Res<ButtonInput<MouseButton>>,
window: Query<&Window, With<PrimaryWindow>>,
camera_query: Query<(&GlobalTransform, &Camera), With<Camera3d>>,
registry: Option<Res<TypeVariantRegistry>>,
mut meshes: ResMut<Assets<Mesh>>,
existing_bricks: Query<&Transform, With<Brick>>,
) {
if !mouse.pressed(MouseButton::Left) {
return;
}
let Ok(window) = window.single() else {
return;
};
let Ok((camera_transform, camera)) = camera_query.single() else {
return;
};
let Some(type_id) = selected.type_id else {
return;
};
let Some(cursor_pos) = window.cursor_position() else {
return;
};
let Some((grid_x, grid_z)) = cursor_to_grid(cursor_pos, window, camera_transform, camera)
else {
return;
};
let world_x = -PLANE_H / 2.0 + (grid_x as f32 + 0.5) * CELL_HEIGHT;
let world_z = -PLANE_W / 2.0 + (grid_z as f32 + 0.5) * CELL_WIDTH;
let world_pos = Vec3::new(world_x, 0.5, world_z);
const POSITION_TOLERANCE: f32 = 0.1;
for existing_transform in existing_bricks.iter() {
if existing_transform.translation.distance(world_pos) < POSITION_TOLERANCE {
return; }
}
let material = registry
.as_ref()
.and_then(|r| r.get(ObjectClass::Brick, type_id));
let mut brick_entity = commands.spawn((
Mesh3d(meshes.add(Cuboid::new(CELL_HEIGHT * 0.9, 1.0, CELL_WIDTH * 0.9))),
Transform::from_translation(world_pos),
Collider::cuboid(CELL_HEIGHT * 0.45, 0.5, CELL_WIDTH * 0.45),
Brick,
BrickTypeId(type_id),
));
if let Some(mat) = material {
brick_entity.insert(MeshMaterial3d(mat));
}
if type_id != 90 {
brick_entity.insert(CountsTowardsCompletion);
}
info!(
"Placed brick type {} at grid ({}, {})",
type_id, grid_x, grid_z
);
}