1use crate::error::{BevyAIError, Result};
4use handlebars::Handlebars;
5use serde_json::{json, Value};
6use std::collections::HashMap;
7
8pub struct TemplateManager {
10 handlebars: Handlebars<'static>,
11}
12
13#[derive(Debug, Clone)]
15pub struct TemplateContext {
16 pub project_name: String,
18 pub description: String,
20 pub features: Vec<String>,
22 pub dependencies: Vec<String>,
24 pub bevy_version: String,
26 pub custom_variables: HashMap<String, Value>,
28}
29
30#[derive(Debug, Clone)]
32pub struct GameTemplate {
33 pub name: String,
35 pub description: String,
37 pub category: GameCategory,
39 pub main_template: String,
41 pub additional_files: HashMap<String, String>,
43 pub dependencies: Vec<String>,
45 pub features: Vec<String>,
47}
48
49#[derive(Debug, Clone, PartialEq, Eq)]
51pub enum GameCategory {
52 Platformer,
54 Shooter,
56 Puzzle,
58 Strategy,
60 Rpg,
62 Racing,
64 Simulation,
66 Arcade,
68 Educational,
70 Experimental,
72}
73
74impl TemplateManager {
75 pub fn new() -> Result<Self> {
77 let mut handlebars = Handlebars::new();
78
79 Self::register_builtin_templates(&mut handlebars)?;
81
82 Ok(Self { handlebars })
83 }
84
85 fn register_builtin_templates(handlebars: &mut Handlebars) -> Result<()> {
87 handlebars.register_template_string("basic_game", BASIC_GAME_TEMPLATE)?;
89
90 handlebars.register_template_string("platformer_2d", PLATFORMER_2D_TEMPLATE)?;
92
93 handlebars.register_template_string("fps_3d", FPS_3D_TEMPLATE)?;
95
96 handlebars.register_template_string("puzzle_game", PUZZLE_GAME_TEMPLATE)?;
98
99 handlebars.register_template_string("strategy_game", STRATEGY_GAME_TEMPLATE)?;
101
102 Ok(())
103 }
104
105 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 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 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 pub fn available_templates(&self) -> Vec<&str> {
138 self.handlebars.get_templates().keys().map(|s| s.as_str()).collect()
139 }
140
141 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 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 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 pub fn with_feature(mut self, feature: String) -> Self {
222 self.features.push(feature);
223 self
224 }
225
226 pub fn with_dependency(mut self, dependency: String) -> Self {
228 self.dependencies.push(dependency);
229 self
230 }
231
232 pub fn with_bevy_version(mut self, version: String) -> Self {
234 self.bevy_version = version;
235 self
236 }
237
238 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
262const 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"#;