Skip to main content

bevy_agent/
game_templates.rs

1//! Game templates and code generation utilities
2
3use crate::error::{BevyAIError, Result};
4use handlebars::Handlebars;
5use serde_json::{json, Value};
6use std::collections::HashMap;
7
8/// Template manager for Bevy AI
9pub struct TemplateManager {
10    handlebars: Handlebars<'static>,
11}
12
13/// Template context for code generation
14#[derive(Debug, Clone)]
15pub struct TemplateContext {
16    /// Name of the project being generated
17    pub project_name: String,
18    /// Description of the project
19    pub description: String,
20    /// List of features to include
21    pub features: Vec<String>,
22    /// List of dependencies to include
23    pub dependencies: Vec<String>,
24    /// Bevy version to use
25    pub bevy_version: String,
26    /// Custom variables for template substitution
27    pub custom_variables: HashMap<String, Value>,
28}
29
30/// Game template definition
31#[derive(Debug, Clone)]
32pub struct GameTemplate {
33    /// Name of the template
34    pub name: String,
35    /// Description of what this template creates
36    pub description: String,
37    /// Category this template belongs to
38    pub category: GameCategory,
39    /// Main Rust code template
40    pub main_template: String,
41    /// Additional files to generate (filename -> content)
42    pub additional_files: HashMap<String, String>,
43    /// Required dependencies
44    pub dependencies: Vec<String>,
45    /// Required Bevy features
46    pub features: Vec<String>,
47}
48
49/// Game categories for templates
50#[derive(Debug, Clone, PartialEq, Eq)]
51pub enum GameCategory {
52    /// 2D/3D platformer games
53    Platformer,
54    /// Shooting games (2D/3D)
55    Shooter,
56    /// Puzzle and logic games
57    Puzzle,
58    /// Strategy and tactical games
59    Strategy,
60    /// Role-playing games
61    Rpg,
62    /// Racing and driving games
63    Racing,
64    /// Simulation games
65    Simulation,
66    /// Arcade-style games
67    Arcade,
68    /// Educational games
69    Educational,
70    /// Experimental and prototype games
71    Experimental,
72}
73
74impl TemplateManager {
75    /// Create a new template manager
76    pub fn new() -> Result<Self> {
77        let mut handlebars = Handlebars::new();
78        
79        // Register built-in templates
80        Self::register_builtin_templates(&mut handlebars)?;
81        
82        Ok(Self { handlebars })
83    }
84    
85    /// Register built-in templates
86    fn register_builtin_templates(handlebars: &mut Handlebars) -> Result<()> {
87        // Basic game template
88        handlebars.register_template_string("basic_game", BASIC_GAME_TEMPLATE)?;
89        
90        // 2D Platformer template
91        handlebars.register_template_string("platformer_2d", PLATFORMER_2D_TEMPLATE)?;
92        
93        // 3D FPS template
94        handlebars.register_template_string("fps_3d", FPS_3D_TEMPLATE)?;
95        
96        // Puzzle game template
97        handlebars.register_template_string("puzzle_game", PUZZLE_GAME_TEMPLATE)?;
98        
99        // Strategy game template
100        handlebars.register_template_string("strategy_game", STRATEGY_GAME_TEMPLATE)?;
101        
102        Ok(())
103    }
104    
105    /// Generate code from template
106    pub fn generate(&self, template_name: &str, context: &TemplateContext) -> Result<String> {
107        let template_context = self.create_handlebars_context(context)?;
108        
109        let result = self.handlebars
110            .render(template_name, &template_context)
111            .map_err(|e| BevyAIError::Template(e))?;
112        
113        Ok(result)
114    }
115    
116    /// Create handlebars context from template context
117    fn create_handlebars_context(&self, context: &TemplateContext) -> Result<Value> {
118        let mut handlebars_context = json!({
119            "project_name": context.project_name,
120            "description": context.description,
121            "features": context.features,
122            "dependencies": context.dependencies,
123            "bevy_version": context.bevy_version,
124        });
125        
126        // Add custom variables
127        if let Some(object) = handlebars_context.as_object_mut() {
128            for (key, value) in &context.custom_variables {
129                object.insert(key.clone(), value.clone());
130            }
131        }
132        
133        Ok(handlebars_context)
134    }
135    
136    /// Get available templates
137    pub fn available_templates(&self) -> Vec<&str> {
138        self.handlebars.get_templates().keys().map(|s| s.as_str()).collect()
139    }
140    
141    /// Register custom template
142    pub fn register_template(&mut self, name: &str, template: &str) -> Result<()> {
143        self.handlebars
144            .register_template_string(name, template)
145            .map_err(|e| BevyAIError::TemplateCreation(e))?;
146        Ok(())
147    }
148    
149    /// Get built-in game templates
150    pub fn builtin_templates() -> Vec<GameTemplate> {
151        vec![
152            GameTemplate {
153                name: "basic_game".to_string(),
154                description: "A basic Bevy game with camera, lighting, and a simple scene".to_string(),
155                category: GameCategory::Educational,
156                main_template: BASIC_GAME_TEMPLATE.to_string(),
157                additional_files: HashMap::new(),
158                dependencies: vec!["bevy".to_string()],
159                features: vec!["3D rendering".to_string(), "Basic input".to_string()],
160            },
161            GameTemplate {
162                name: "platformer_2d".to_string(),
163                description: "A 2D platformer with player movement, physics, and collectibles".to_string(),
164                category: GameCategory::Platformer,
165                main_template: PLATFORMER_2D_TEMPLATE.to_string(),
166                additional_files: HashMap::new(),
167                dependencies: vec!["bevy".to_string()],
168                features: vec!["2D sprites".to_string(), "Physics".to_string(), "Player movement".to_string()],
169            },
170            GameTemplate {
171                name: "fps_3d".to_string(),
172                description: "A 3D first-person shooter with player controller and basic enemies".to_string(),
173                category: GameCategory::Shooter,
174                main_template: FPS_3D_TEMPLATE.to_string(),
175                additional_files: HashMap::new(),
176                dependencies: vec!["bevy".to_string()],
177                features: vec!["3D rendering".to_string(), "FPS controls".to_string(), "Shooting mechanics".to_string()],
178            },
179            GameTemplate {
180                name: "puzzle_game".to_string(),
181                description: "A puzzle game with grid-based mechanics and level progression".to_string(),
182                category: GameCategory::Puzzle,
183                main_template: PUZZLE_GAME_TEMPLATE.to_string(),
184                additional_files: HashMap::new(),
185                dependencies: vec!["bevy".to_string()],
186                features: vec!["Grid system".to_string(), "Puzzle mechanics".to_string(), "Level management".to_string()],
187            },
188            GameTemplate {
189                name: "strategy_game".to_string(),
190                description: "A real-time strategy game with unit management and resource collection".to_string(),
191                category: GameCategory::Strategy,
192                main_template: STRATEGY_GAME_TEMPLATE.to_string(),
193                additional_files: HashMap::new(),
194                dependencies: vec!["bevy".to_string()],
195                features: vec!["Unit management".to_string(), "Resource system".to_string(), "RTS mechanics".to_string()],
196            },
197        ]
198    }
199}
200
201impl Default for TemplateManager {
202    fn default() -> Self {
203        Self::new().expect("Failed to create template manager")
204    }
205}
206
207impl TemplateContext {
208    /// Create a new template context
209    pub fn new(project_name: String, description: String) -> Self {
210        Self {
211            project_name,
212            description,
213            features: Vec::new(),
214            dependencies: vec!["bevy".to_string()],
215            bevy_version: "0.12".to_string(),
216            custom_variables: HashMap::new(),
217        }
218    }
219    
220    /// Add a feature
221    pub fn with_feature(mut self, feature: String) -> Self {
222        self.features.push(feature);
223        self
224    }
225    
226    /// Add a dependency
227    pub fn with_dependency(mut self, dependency: String) -> Self {
228        self.dependencies.push(dependency);
229        self
230    }
231    
232    /// Set Bevy version
233    pub fn with_bevy_version(mut self, version: String) -> Self {
234        self.bevy_version = version;
235        self
236    }
237    
238    /// Add custom variable
239    pub fn with_variable<T: Into<Value>>(mut self, key: String, value: T) -> Self {
240        self.custom_variables.insert(key, value.into());
241        self
242    }
243}
244
245impl std::fmt::Display for GameCategory {
246    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
247        match self {
248            GameCategory::Platformer => write!(f, "Platformer"),
249            GameCategory::Shooter => write!(f, "Shooter"),
250            GameCategory::Puzzle => write!(f, "Puzzle"),
251            GameCategory::Strategy => write!(f, "Strategy"),
252            GameCategory::Rpg => write!(f, "RPG"),
253            GameCategory::Racing => write!(f, "Racing"),
254            GameCategory::Simulation => write!(f, "Simulation"),
255            GameCategory::Arcade => write!(f, "Arcade"),
256            GameCategory::Educational => write!(f, "Educational"),
257            GameCategory::Experimental => write!(f, "Experimental"),
258        }
259    }
260}
261
262// Template constants
263const BASIC_GAME_TEMPLATE: &str = r#"// {{description}}
264// Generated with Bevy AI
265
266use bevy::prelude::*;
267
268fn main() {
269    App::new()
270        .add_plugins(DefaultPlugins.set(WindowPlugin {
271            primary_window: Some(Window {
272                title: "{{project_name}}".to_string(),
273                ..default()
274            }),
275            ..default()
276        }))
277        .add_systems(Startup, setup)
278        .add_systems(Update, (
279            rotate_camera,
280            {{#each features}}
281            // TODO: Implement {{this}}
282            {{/each}}
283        ))
284        .run();
285}
286
287#[derive(Component)]
288struct MainCamera;
289
290fn setup(
291    mut commands: Commands,
292    mut meshes: ResMut<Assets<Mesh>>,
293    mut materials: ResMut<Assets<StandardMaterial>>,
294) {
295    // Camera
296    commands.spawn((
297        Camera3dBundle {
298            transform: Transform::from_xyz(0.0, 6.0, 12.0)
299                .looking_at(Vec3::new(0.0, 1.0, 0.0), Vec3::Y),
300            ..default()
301        },
302        MainCamera,
303    ));
304
305    // Light
306    commands.spawn(DirectionalLightBundle {
307        directional_light: DirectionalLight {
308            shadows_enabled: true,
309            ..default()
310        },
311        transform: Transform {
312            translation: Vec3::new(0.0, 2.0, 0.0),
313            rotation: Quat::from_rotation_x(-std::f32::consts::FRAC_PI_4),
314            ..default()
315        },
316        ..default()
317    });
318
319    // Ground plane
320    commands.spawn(PbrBundle {
321        mesh: meshes.add(Plane3d::default().mesh().size(8.0, 8.0)),
322        material: materials.add(Color::rgb(0.3, 0.5, 0.3)),
323        ..default()
324    });
325
326    // Sample cube
327    commands.spawn(PbrBundle {
328        mesh: meshes.add(Cuboid::new(1.0, 1.0, 1.0)),
329        material: materials.add(Color::rgb(0.8, 0.7, 0.6)),
330        transform: Transform::from_xyz(0.0, 0.5, 0.0),
331        ..default()
332    });
333}
334
335fn rotate_camera(
336    time: Res<Time>,
337    mut camera_query: Query<&mut Transform, (With<MainCamera>, Without<DirectionalLight>)>,
338) {
339    for mut transform in camera_query.iter_mut() {
340        let radius = 12.0;
341        let angle = time.elapsed_seconds() * 0.3;
342        transform.translation.x = angle.cos() * radius;
343        transform.translation.z = angle.sin() * radius;
344        transform.look_at(Vec3::new(0.0, 1.0, 0.0), Vec3::Y);
345    }
346}
347"#;
348
349const PLATFORMER_2D_TEMPLATE: &str = r#"// {{description}}
350// 2D Platformer generated with Bevy AI
351
352use bevy::prelude::*;
353
354const PLAYER_SPEED: f32 = 200.0;
355const JUMP_STRENGTH: f32 = 400.0;
356const GRAVITY: f32 = 800.0;
357
358fn main() {
359    App::new()
360        .add_plugins(DefaultPlugins.set(WindowPlugin {
361            primary_window: Some(Window {
362                title: "{{project_name}}".to_string(),
363                ..default()
364            }),
365            ..default()
366        }))
367        .add_systems(Startup, setup)
368        .add_systems(Update, (
369            player_movement,
370            apply_gravity,
371            camera_follow,
372        ))
373        .run();
374}
375
376#[derive(Component)]
377struct Player {
378    velocity: Vec2,
379    grounded: bool,
380}
381
382#[derive(Component)]
383struct MainCamera;
384
385fn setup(
386    mut commands: Commands,
387    mut meshes: ResMut<Assets<Mesh>>,
388    mut materials: ResMut<Assets<ColorMaterial>>,
389) {
390    // Camera
391    commands.spawn((Camera2dBundle::default(), MainCamera));
392
393    // Player
394    commands.spawn((
395        SpriteBundle {
396            sprite: Sprite {
397                color: Color::BLUE,
398                custom_size: Some(Vec2::new(32.0, 32.0)),
399                ..default()
400            },
401            transform: Transform::from_xyz(0.0, 100.0, 0.0),
402            ..default()
403        },
404        Player {
405            velocity: Vec2::ZERO,
406            grounded: false,
407        },
408    ));
409
410    // Ground platforms
411    let platform_positions = [
412        Vec3::new(0.0, -200.0, 0.0),
413        Vec3::new(200.0, -100.0, 0.0),
414        Vec3::new(-200.0, 0.0, 0.0),
415        Vec3::new(400.0, 50.0, 0.0),
416    ];
417
418    for position in platform_positions {
419        commands.spawn(SpriteBundle {
420            sprite: Sprite {
421                color: Color::GREEN,
422                custom_size: Some(Vec2::new(100.0, 20.0)),
423                ..default()
424            },
425            transform: Transform::from_translation(position),
426            ..default()
427        });
428    }
429}
430
431fn player_movement(
432    keyboard: Res<ButtonInput<KeyCode>>,
433    time: Res<Time>,
434    mut player_query: Query<(&mut Transform, &mut Player)>,
435) {
436    for (mut transform, mut player) in player_query.iter_mut() {
437        let mut movement = 0.0;
438        
439        if keyboard.pressed(KeyCode::ArrowLeft) || keyboard.pressed(KeyCode::KeyA) {
440            movement -= 1.0;
441        }
442        if keyboard.pressed(KeyCode::ArrowRight) || keyboard.pressed(KeyCode::KeyD) {
443            movement += 1.0;
444        }
445        
446        player.velocity.x = movement * PLAYER_SPEED;
447        
448        if (keyboard.just_pressed(KeyCode::Space) || keyboard.just_pressed(KeyCode::ArrowUp)) && player.grounded {
449            player.velocity.y = JUMP_STRENGTH;
450            player.grounded = false;
451        }
452        
453        transform.translation.x += player.velocity.x * time.delta_seconds();
454        transform.translation.y += player.velocity.y * time.delta_seconds();
455    }
456}
457
458fn apply_gravity(
459    time: Res<Time>,
460    mut player_query: Query<(&mut Transform, &mut Player)>,
461) {
462    for (mut transform, mut player) in player_query.iter_mut() {
463        if !player.grounded {
464            player.velocity.y -= GRAVITY * time.delta_seconds();
465        }
466        
467        // Simple ground collision (y = -200 is ground level)
468        if transform.translation.y <= -184.0 {
469            transform.translation.y = -184.0;
470            player.velocity.y = 0.0;
471            player.grounded = true;
472        }
473    }
474}
475
476fn camera_follow(
477    player_query: Query<&Transform, (With<Player>, Without<MainCamera>)>,
478    mut camera_query: Query<&mut Transform, (With<MainCamera>, Without<Player>)>,
479) {
480    if let Ok(player_transform) = player_query.get_single() {
481        for mut camera_transform in camera_query.iter_mut() {
482            camera_transform.translation.x = player_transform.translation.x;
483            camera_transform.translation.y = player_transform.translation.y;
484        }
485    }
486}
487"#;
488
489const FPS_3D_TEMPLATE: &str = r#"// {{description}}
490// 3D FPS generated with Bevy AI
491
492use bevy::prelude::*;
493use bevy::window::CursorGrabMode;
494use bevy::input::mouse::MouseMotion;
495
496const MOVEMENT_SPEED: f32 = 5.0;
497const MOUSE_SENSITIVITY: f32 = 0.002;
498
499fn main() {
500    App::new()
501        .add_plugins(DefaultPlugins.set(WindowPlugin {
502            primary_window: Some(Window {
503                title: "{{project_name}}".to_string(),
504                cursor: bevy::window::Cursor {
505                    grab_mode: CursorGrabMode::Locked,
506                    visible: false,
507                    ..default()
508                },
509                ..default()
510            }),
511            ..default()
512        }))
513        .add_systems(Startup, setup)
514        .add_systems(Update, (
515            player_movement,
516            mouse_look,
517        ))
518        .run();
519}
520
521#[derive(Component)]
522struct Player;
523
524#[derive(Component)]
525struct PlayerCamera {
526    pitch: f32,
527    yaw: f32,
528}
529
530fn setup(
531    mut commands: Commands,
532    mut meshes: ResMut<Assets<Mesh>>,
533    mut materials: ResMut<Assets<StandardMaterial>>,
534) {
535    // Player (invisible, just a transform)
536    commands.spawn((
537        SpatialBundle {
538            transform: Transform::from_xyz(0.0, 1.5, 3.0),
539            ..default()
540        },
541        Player,
542    )).with_children(|parent| {
543        // Camera as child of player
544        parent.spawn((
545            Camera3dBundle {
546                transform: Transform::from_xyz(0.0, 0.0, 0.0),
547                ..default()
548            },
549            PlayerCamera {
550                pitch: 0.0,
551                yaw: 0.0,
552            },
553        ));
554    });
555
556    // Ground
557    commands.spawn(PbrBundle {
558        mesh: meshes.add(Plane3d::default().mesh().size(20.0, 20.0)),
559        material: materials.add(Color::rgb(0.3, 0.5, 0.3)),
560        ..default()
561    });
562
563    // Some cubes to shoot at
564    for i in 0..5 {
565        commands.spawn(PbrBundle {
566            mesh: meshes.add(Cuboid::new(1.0, 1.0, 1.0)),
567            material: materials.add(Color::rgb(0.8, 0.2, 0.2)),
568            transform: Transform::from_xyz(
569                (i as f32 - 2.0) * 3.0,
570                0.5,
571                -5.0,
572            ),
573            ..default()
574        });
575    }
576
577    // Light
578    commands.spawn(DirectionalLightBundle {
579        directional_light: DirectionalLight {
580            shadows_enabled: true,
581            ..default()
582        },
583        transform: Transform {
584            translation: Vec3::new(0.0, 10.0, 0.0),
585            rotation: Quat::from_rotation_x(-std::f32::consts::FRAC_PI_4),
586            ..default()
587        },
588        ..default()
589    });
590}
591
592fn player_movement(
593    keyboard: Res<ButtonInput<KeyCode>>,
594    time: Res<Time>,
595    mut player_query: Query<&mut Transform, With<Player>>,
596) {
597    for mut transform in player_query.iter_mut() {
598        let mut movement = Vec3::ZERO;
599        
600        if keyboard.pressed(KeyCode::KeyW) {
601            movement += transform.forward();
602        }
603        if keyboard.pressed(KeyCode::KeyS) {
604            movement -= transform.forward();
605        }
606        if keyboard.pressed(KeyCode::KeyA) {
607            movement -= transform.right();
608        }
609        if keyboard.pressed(KeyCode::KeyD) {
610            movement += transform.right();
611        }
612        
613        movement.y = 0.0; // Don't move up/down
614        movement = movement.normalize_or_zero();
615        
616        transform.translation += movement * MOVEMENT_SPEED * time.delta_seconds();
617    }
618}
619
620fn mouse_look(
621    mut mouse_motion: EventReader<MouseMotion>,
622    mut camera_query: Query<(&mut Transform, &mut PlayerCamera)>,
623    mut player_query: Query<&mut Transform, (With<Player>, Without<PlayerCamera>)>,
624) {
625    let mut delta = Vec2::ZERO;
626    for motion in mouse_motion.read() {
627        delta += motion.delta;
628    }
629    
630    if delta.length_squared() > 0.0 {
631        for (mut camera_transform, mut camera) in camera_query.iter_mut() {
632            camera.yaw -= delta.x * MOUSE_SENSITIVITY;
633            camera.pitch -= delta.y * MOUSE_SENSITIVITY;
634            camera.pitch = camera.pitch.clamp(-1.5, 1.5);
635            
636            camera_transform.rotation = Quat::from_rotation_y(camera.yaw) * Quat::from_rotation_x(camera.pitch);
637        }
638        
639        // Update player Y rotation to match camera yaw
640        for mut player_transform in player_query.iter_mut() {
641            if let Ok((_, camera)) = camera_query.get_single() {
642                player_transform.rotation = Quat::from_rotation_y(camera.yaw);
643            }
644        }
645    }
646}
647"#;
648
649const PUZZLE_GAME_TEMPLATE: &str = r#"// {{description}}
650// Puzzle Game generated with Bevy AI
651
652use bevy::prelude::*;
653
654const GRID_SIZE: usize = 8;
655const TILE_SIZE: f32 = 32.0;
656
657fn main() {
658    App::new()
659        .add_plugins(DefaultPlugins.set(WindowPlugin {
660            primary_window: Some(Window {
661                title: "{{project_name}}".to_string(),
662                ..default()
663            }),
664            ..default()
665        }))
666        .init_resource::<GameGrid>()
667        .add_systems(Startup, setup)
668        .add_systems(Update, (
669            handle_input,
670            update_visual_grid,
671        ))
672        .run();
673}
674
675#[derive(Resource)]
676struct GameGrid {
677    tiles: [[u8; GRID_SIZE]; GRID_SIZE],
678    selected: Option<(usize, usize)>,
679}
680
681impl Default for GameGrid {
682    fn default() -> Self {
683        let mut tiles = [[0; GRID_SIZE]; GRID_SIZE];
684        
685        // Initialize with a simple pattern
686        for x in 0..GRID_SIZE {
687            for y in 0..GRID_SIZE {
688                tiles[x][y] = ((x + y) % 3) as u8;
689            }
690        }
691        
692        Self {
693            tiles,
694            selected: None,
695        }
696    }
697}
698
699#[derive(Component)]
700struct GridTile {
701    x: usize,
702    y: usize,
703}
704
705#[derive(Component)]
706struct Selected;
707
708fn setup(
709    mut commands: Commands,
710    mut meshes: ResMut<Assets<Mesh>>,
711    mut materials: ResMut<Assets<ColorMaterial>>,
712    grid: Res<GameGrid>,
713) {
714    // Camera
715    commands.spawn(Camera2dBundle::default());
716
717    // Create visual grid
718    for x in 0..GRID_SIZE {
719        for y in 0..GRID_SIZE {
720            let world_x = (x as f32 - GRID_SIZE as f32 / 2.0) * TILE_SIZE;
721            let world_y = (y as f32 - GRID_SIZE as f32 / 2.0) * TILE_SIZE;
722            
723            let color = match grid.tiles[x][y] {
724                0 => Color::RED,
725                1 => Color::GREEN,
726                2 => Color::BLUE,
727                _ => Color::WHITE,
728            };
729            
730            commands.spawn((
731                SpriteBundle {
732                    sprite: Sprite {
733                        color,
734                        custom_size: Some(Vec2::new(TILE_SIZE - 2.0, TILE_SIZE - 2.0)),
735                        ..default()
736                    },
737                    transform: Transform::from_xyz(world_x, world_y, 0.0),
738                    ..default()
739                },
740                GridTile { x, y },
741            ));
742        }
743    }
744}
745
746fn handle_input(
747    keyboard: Res<ButtonInput<KeyCode>>,
748    mut grid: ResMut<GameGrid>,
749) {
750    let mut new_selected = grid.selected;
751    
752    if keyboard.just_pressed(KeyCode::ArrowUp) {
753        if let Some((x, y)) = grid.selected {
754            if y < GRID_SIZE - 1 {
755                new_selected = Some((x, y + 1));
756            }
757        } else {
758            new_selected = Some((GRID_SIZE / 2, GRID_SIZE / 2));
759        }
760    }
761    
762    if keyboard.just_pressed(KeyCode::ArrowDown) {
763        if let Some((x, y)) = grid.selected {
764            if y > 0 {
765                new_selected = Some((x, y - 1));
766            }
767        } else {
768            new_selected = Some((GRID_SIZE / 2, GRID_SIZE / 2));
769        }
770    }
771    
772    if keyboard.just_pressed(KeyCode::ArrowLeft) {
773        if let Some((x, y)) = grid.selected {
774            if x > 0 {
775                new_selected = Some((x - 1, y));
776            }
777        } else {
778            new_selected = Some((GRID_SIZE / 2, GRID_SIZE / 2));
779        }
780    }
781    
782    if keyboard.just_pressed(KeyCode::ArrowRight) {
783        if let Some((x, y)) = grid.selected {
784            if x < GRID_SIZE - 1 {
785                new_selected = Some((x + 1, y));
786            }
787        } else {
788            new_selected = Some((GRID_SIZE / 2, GRID_SIZE / 2));
789        }
790    }
791    
792    if keyboard.just_pressed(KeyCode::Space) {
793        if let Some((x, y)) = grid.selected {
794            // Cycle tile color
795            grid.tiles[x][y] = (grid.tiles[x][y] + 1) % 3;
796        }
797    }
798    
799    grid.selected = new_selected;
800}
801
802fn update_visual_grid(
803    grid: Res<GameGrid>,
804    mut commands: Commands,
805    mut tile_query: Query<(Entity, &mut Sprite, &GridTile), Without<Selected>>,
806    selected_query: Query<Entity, With<Selected>>,
807) {
808    // Remove old selection
809    for entity in selected_query.iter() {
810        commands.entity(entity).remove::<Selected>();
811    }
812    
813    // Update tile colors and add selection
814    for (entity, mut sprite, tile) in tile_query.iter_mut() {
815        let color = match grid.tiles[tile.x][tile.y] {
816            0 => Color::RED,
817            1 => Color::GREEN,
818            2 => Color::BLUE,
819            _ => Color::WHITE,
820        };
821        
822        sprite.color = color;
823        
824        // Add selection highlight
825        if let Some((sel_x, sel_y)) = grid.selected {
826            if tile.x == sel_x && tile.y == sel_y {
827                sprite.color = Color::YELLOW;
828                commands.entity(entity).insert(Selected);
829            }
830        }
831    }
832}
833"#;
834
835const STRATEGY_GAME_TEMPLATE: &str = r#"// {{description}}
836// Strategy Game generated with Bevy AI
837
838use bevy::prelude::*;
839
840fn main() {
841    App::new()
842        .add_plugins(DefaultPlugins.set(WindowPlugin {
843            primary_window: Some(Window {
844                title: "{{project_name}}".to_string(),
845                ..default()
846            }),
847            ..default()
848        }))
849        .init_resource::<GameState>()
850        .init_resource::<SelectedUnits>()
851        .add_systems(Startup, setup)
852        .add_systems(Update, (
853            unit_selection,
854            unit_movement,
855            resource_generation,
856            camera_movement,
857        ))
858        .run();
859}
860
861#[derive(Resource, Default)]
862struct GameState {
863    resources: u32,
864}
865
866#[derive(Resource, Default)]
867struct SelectedUnits {
868    units: Vec<Entity>,
869}
870
871#[derive(Component)]
872struct Unit {
873    health: f32,
874    max_health: f32,
875    unit_type: UnitType,
876}
877
878#[derive(Component)]
879struct Selectable {
880    selected: bool,
881}
882
883#[derive(Component)]
884struct ResourceNode {
885    resource_type: ResourceType,
886    amount: u32,
887}
888
889#[derive(Component)]
890struct MainCamera;
891
892#[derive(Clone)]
893enum UnitType {
894    Worker,
895    Soldier,
896    Scout,
897}
898
899enum ResourceType {
900    Gold,
901    Stone,
902    Wood,
903}
904
905fn setup(
906    mut commands: Commands,
907    mut meshes: ResMut<Assets<Mesh>>,
908    mut materials: ResMut<Assets<StandardMaterial>>,
909) {
910    // Camera
911    commands.spawn((
912        Camera3dBundle {
913            transform: Transform::from_xyz(0.0, 10.0, 10.0)
914                .looking_at(Vec3::ZERO, Vec3::Y),
915            ..default()
916        },
917        MainCamera,
918    ));
919
920    // Light
921    commands.spawn(DirectionalLightBundle {
922        directional_light: DirectionalLight {
923            shadows_enabled: true,
924            ..default()
925        },
926        transform: Transform {
927            translation: Vec3::new(0.0, 10.0, 0.0),
928            rotation: Quat::from_rotation_x(-std::f32::consts::FRAC_PI_4),
929            ..default()
930        },
931        ..default()
932    });
933
934    // Ground
935    commands.spawn(PbrBundle {
936        mesh: meshes.add(Plane3d::default().mesh().size(20.0, 20.0)),
937        material: materials.add(Color::rgb(0.3, 0.5, 0.3)),
938        ..default()
939    });
940
941    // Spawn some units
942    for i in 0..3 {
943        commands.spawn((
944            PbrBundle {
945                mesh: meshes.add(Cuboid::new(0.5, 1.0, 0.5)),
946                material: materials.add(Color::BLUE),
947                transform: Transform::from_xyz(i as f32 * 2.0 - 2.0, 0.5, 0.0),
948                ..default()
949            },
950            Unit {
951                health: 100.0,
952                max_health: 100.0,
953                unit_type: UnitType::Worker,
954            },
955            Selectable { selected: false },
956        ));
957    }
958
959    // Spawn resource nodes
960    let resource_positions = [
961        (Vec3::new(5.0, 0.5, 5.0), ResourceType::Gold),
962        (Vec3::new(-5.0, 0.5, 5.0), ResourceType::Stone),
963        (Vec3::new(0.0, 0.5, -5.0), ResourceType::Wood),
964    ];
965
966    for (position, resource_type) in resource_positions {
967        let color = match resource_type {
968            ResourceType::Gold => Color::YELLOW,
969            ResourceType::Stone => Color::GRAY,
970            ResourceType::Wood => Color::rgb(0.6, 0.3, 0.1),
971        };
972
973        commands.spawn((
974            PbrBundle {
975                mesh: meshes.add(Cylinder::new(0.8, 1.5)),
976                material: materials.add(color),
977                transform: Transform::from_translation(position),
978                ..default()
979            },
980            ResourceNode {
981                resource_type,
982                amount: 100,
983            },
984        ));
985    }
986}
987
988fn unit_selection(
989    mouse_input: Res<ButtonInput<MouseButton>>,
990    mut selectable_query: Query<&mut Selectable>,
991    mut selected_units: ResMut<SelectedUnits>,
992) {
993    if mouse_input.just_pressed(MouseButton::Left) {
994        // Simple selection - select all units for now
995        // In a real game, you'd use raycasting to select specific units
996        selected_units.units.clear();
997        
998        for (entity, mut selectable) in selectable_query.iter_mut().enumerate() {
999            selectable.selected = entity == 0; // Select first unit only
1000            if selectable.selected {
1001                selected_units.units.push(Entity::from_raw(entity as u32));
1002            }
1003        }
1004    }
1005}
1006
1007fn unit_movement(
1008    keyboard: Res<ButtonInput<KeyCode>>,
1009    time: Res<Time>,
1010    selected_units: Res<SelectedUnits>,
1011    mut unit_query: Query<(&mut Transform, &Selectable), With<Unit>>,
1012) {
1013    let mut movement = Vec3::ZERO;
1014    
1015    if keyboard.pressed(KeyCode::KeyW) {
1016        movement.z -= 1.0;
1017    }
1018    if keyboard.pressed(KeyCode::KeyS) {
1019        movement.z += 1.0;
1020    }
1021    if keyboard.pressed(KeyCode::KeyA) {
1022        movement.x -= 1.0;
1023    }
1024    if keyboard.pressed(KeyCode::KeyD) {
1025        movement.x += 1.0;
1026    }
1027    
1028    if movement.length() > 0.0 {
1029        movement = movement.normalize() * 3.0 * time.delta_seconds();
1030        
1031        for (mut transform, selectable) in unit_query.iter_mut() {
1032            if selectable.selected {
1033                transform.translation += movement;
1034            }
1035        }
1036    }
1037}
1038
1039fn resource_generation(
1040    time: Res<Time>,
1041    mut game_state: ResMut<GameState>,
1042    mut last_generation: Local<f32>,
1043) {
1044    *last_generation += time.delta_seconds();
1045    
1046    if *last_generation >= 1.0 {
1047        game_state.resources += 10;
1048        *last_generation = 0.0;
1049        info!("Resources: {}", game_state.resources);
1050    }
1051}
1052
1053fn camera_movement(
1054    keyboard: Res<ButtonInput<KeyCode>>,
1055    time: Res<Time>,
1056    mut camera_query: Query<&mut Transform, With<MainCamera>>,
1057) {
1058    for mut transform in camera_query.iter_mut() {
1059        let mut movement = Vec3::ZERO;
1060        
1061        if keyboard.pressed(KeyCode::ArrowUp) {
1062            movement.z -= 1.0;
1063        }
1064        if keyboard.pressed(KeyCode::ArrowDown) {
1065            movement.z += 1.0;
1066        }
1067        if keyboard.pressed(KeyCode::ArrowLeft) {
1068            movement.x -= 1.0;
1069        }
1070        if keyboard.pressed(KeyCode::ArrowRight) {
1071            movement.x += 1.0;
1072        }
1073        
1074        if movement.length() > 0.0 {
1075            movement = movement.normalize() * 8.0 * time.delta_seconds();
1076            transform.translation += movement;
1077        }
1078    }
1079}
1080"#;