diff --git a/examples/games/real_time_strategy.rs b/examples/games/real_time_strategy.rs new file mode 100644 index 0000000000000..961fcfc9c6cac --- /dev/null +++ b/examples/games/real_time_strategy.rs @@ -0,0 +1,1596 @@ +//! RTS Game Proof of Concept +//! Features: Unit selection, movement, resource gathering, unit spawning, basic combat + +use bevy::prelude::*; +use bevy::window::PrimaryWindow; +use rand::Rng; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .init_resource::() + .init_resource::() + .add_systems(Startup, setup) + .add_systems( + Update, + ( + camera_movement, + mouse_button_input, + draw_selection_box, + update_selection_visuals, + update_health_bars, + move_units, + resolve_collisions, + unit_attack, + resource_gathering, + update_ui, + spawn_unit_on_key, + ), + ) + .run(); +} + +// ==================== Components ==================== + +#[derive(Component)] +struct Unit { + max_health: f32, + health: f32, + speed: f32, + attack_damage: f32, + attack_range: f32, + attack_cooldown: Timer, + team: Team, +} + +#[derive(Component, Clone, Copy, PartialEq)] +enum Team { + Player, + Enemy, +} + +#[derive(Component)] +struct Selected; + +#[derive(Component)] +struct SelectionRing { + owner: Entity, +} + +#[derive(Component)] +struct MoveTo { + target: Vec3, +} + +#[derive(Component)] +struct Waypoints { + points: Vec, + current_index: usize, +} + +#[derive(Component)] +struct AttackTarget { + target: Entity, +} + +#[derive(Component)] +struct Resource { + amount: i32, +} + +#[derive(Component)] +struct Gatherer { + gathering_rate: f32, + gathering_timer: Timer, + current_resource: Option, + carrying: i32, + capacity: i32, + assigned_resource: Option, // Manual assignment from player command + gathering_enabled: bool, // Only gather when explicitly enabled by player +} + +#[derive(Component)] +struct MainBase { + team: Team, +} + +#[derive(Component)] +struct HealthBar { + owner: Entity, +} + +#[derive(Component)] +struct HealthBarBackground { + owner: Entity, +} + +#[derive(Component)] +struct UnitMarker; + +// ==================== Resources ==================== + +#[derive(Resource)] +struct GameResources { + player_resources: i32, +} + +impl Default for GameResources { + fn default() -> Self { + Self { + player_resources: 100, + } + } +} + +#[derive(Resource, Default)] +struct SelectionBox { + start: Option, + end: Option, +} + +#[derive(Component)] +struct ResourceUI; + +#[derive(Component)] +struct SelectionInfoUI; + +// ==================== Setup ==================== + +fn setup( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, +) { + // Camera + commands.spawn(( + Camera3d::default(), + Transform::from_xyz(0.0, 15.0, 15.0).looking_at(Vec3::ZERO, Vec3::Y), + )); + + // Lighting - Sun-like directional light + commands.spawn(( + DirectionalLight { + illuminance: 15000.0, + shadows_enabled: true, + ..default() + }, + Transform::from_xyz(4.0, 10.0, 4.0).looking_at(Vec3::ZERO, Vec3::Y), + )); + + // Ambient light for better visibility + commands.insert_resource(AmbientLight { + color: Color::srgb(0.7, 0.7, 0.6), // Slightly warm ambient light + brightness: 300.0, + }); + + // Terrain ground plane - earthy brown/green mix + commands.spawn(( + Mesh3d(meshes.add(Plane3d::default().mesh().size(100.0, 100.0))), + MeshMaterial3d(materials.add(StandardMaterial { + base_color: Color::srgb(0.4, 0.35, 0.25), // Earthy brown + perceptual_roughness: 0.9, + ..default() + })), + Transform::from_xyz(0.0, 0.0, 0.0), + )); + + // Add some grass patches + let grass_material = materials.add(StandardMaterial { + base_color: Color::srgb(0.3, 0.5, 0.2), // Darker grass green + perceptual_roughness: 0.95, + ..default() + }); + + // Create random grass patches for terrain variation + let mut rng = rand::thread_rng(); + for _ in 0..15 { + let x = rng.gen_range(-20.0..20.0); + let z = rng.gen_range(-20.0..20.0); + let size = rng.gen_range(2.0..5.0); + + commands.spawn(( + Mesh3d(meshes.add(Plane3d::default().mesh().size(size, size))), + MeshMaterial3d(grass_material.clone()), + Transform::from_xyz(x, 0.01, z), // Slightly above ground to prevent z-fighting + )); + } + + // Add some dirt patches + let dirt_material = materials.add(StandardMaterial { + base_color: Color::srgb(0.5, 0.4, 0.3), // Lighter dirt + perceptual_roughness: 0.85, + ..default() + }); + + for _ in 0..10 { + let x = rng.gen_range(-20.0..20.0); + let z = rng.gen_range(-20.0..20.0); + let size = rng.gen_range(1.5..4.0); + + commands.spawn(( + Mesh3d(meshes.add(Plane3d::default().mesh().size(size, size))), + MeshMaterial3d(dirt_material.clone()), + Transform::from_xyz(x, 0.02, z), + )); + } + + // Player base + commands.spawn(( + Mesh3d(meshes.add(Cuboid::new(3.0, 2.0, 3.0))), + MeshMaterial3d(materials.add(Color::srgb(0.2, 0.2, 0.8))), + Transform::from_xyz(-10.0, 1.0, -10.0), + MainBase { team: Team::Player }, + )); + + // Enemy base + commands.spawn(( + Mesh3d(meshes.add(Cuboid::new(3.0, 2.0, 3.0))), + MeshMaterial3d(materials.add(Color::srgb(0.8, 0.2, 0.2))), + Transform::from_xyz(10.0, 1.0, 10.0), + MainBase { team: Team::Enemy }, + )); + + // Player units + let player_material = materials.add(Color::srgb(0.3, 0.3, 1.0)); + let unit_mesh = meshes.add(Capsule3d::new(0.3, 1.0)); + + for i in 0..3 { + spawn_unit( + &mut commands, + unit_mesh.clone(), + player_material.clone(), + Vec3::new(-8.0 + i as f32 * 1.5, 0.5, -8.0), + Team::Player, + true, // Workers can gather + ); + } + + // Enemy units + let enemy_material = materials.add(Color::srgb(1.0, 0.3, 0.3)); + + for i in 0..3 { + spawn_unit( + &mut commands, + unit_mesh.clone(), + enemy_material.clone(), + Vec3::new(8.0 + i as f32 * 1.5, 0.5, 8.0), + Team::Enemy, + false, // Enemy units don't gather + ); + } + + // Resources + let resource_material = materials.add(Color::srgb(1.0, 0.84, 0.0)); + let resource_mesh = meshes.add(Sphere::new(0.5)); + + for i in 0..5 { + commands.spawn(( + Mesh3d(resource_mesh.clone()), + MeshMaterial3d(resource_material.clone()), + Transform::from_xyz( + (i as f32 - 2.0) * 3.0, + 0.5, + 0.0, + ), + Resource { amount: 100 }, + )); + } + + // UI - Resource counter + commands.spawn(( + Text::new("Resources: 0"), + TextFont { + font_size: 30.0, + ..default() + }, + TextColor(Color::WHITE), + Node { + position_type: PositionType::Absolute, + top: Val::Px(10.0), + left: Val::Px(10.0), + ..default() + }, + ResourceUI, + )); + + // UI - Selection info + commands.spawn(( + Text::new(""), + TextFont { + font_size: 24.0, + ..default() + }, + TextColor(Color::WHITE), + Node { + position_type: PositionType::Absolute, + top: Val::Px(50.0), + left: Val::Px(10.0), + ..default() + }, + SelectionInfoUI, + )); + + // UI - Controls + commands.spawn(( + Text::new("Controls:\nLeft Click: Select\nRight Click: Move/Attack\nLeft Drag: Box Select\nQ: Spawn Worker\nW: Spawn Warrior\nArrows: Move Camera"), + TextFont { + font_size: 18.0, + ..default() + }, + TextColor(Color::srgba(1.0, 1.0, 1.0, 0.8)), + Node { + position_type: PositionType::Absolute, + bottom: Val::Px(10.0), + left: Val::Px(10.0), + ..default() + }, + )); +} + +fn spawn_unit( + commands: &mut Commands, + mesh: Handle, + material: Handle, + position: Vec3, + team: Team, + is_worker: bool, +) { + let mut entity_commands = commands.spawn(( + Mesh3d(mesh), + MeshMaterial3d(material), + Transform::from_translation(position), + Unit { + max_health: 100.0, + health: 100.0, + speed: 3.0, + attack_damage: 10.0, + attack_range: 2.0, + attack_cooldown: Timer::from_seconds(1.0, TimerMode::Repeating), + team, + }, + UnitMarker, + )); + + if is_worker { + entity_commands.insert(Gatherer { + gathering_rate: 5.0, + gathering_timer: Timer::from_seconds(1.0, TimerMode::Repeating), + current_resource: None, + carrying: 0, + capacity: 10, + assigned_resource: None, + gathering_enabled: false, // Workers don't auto-gather on spawn + }); + } +} + +// ==================== Camera System ==================== + +fn camera_movement( + keyboard: Res>, + time: Res