A Quadrilateral Cowboy clone intended to help me learn Game Dev

Working on making this an actual thing, like loading in levels and using ron files for that

+109 -241
+33 -3
Cargo.lock
··· 445 445 "downcast-rs 2.0.2", 446 446 "either", 447 447 "petgraph", 448 - "ron", 448 + "ron 0.12.0", 449 449 "serde", 450 450 "smallvec", 451 451 "thiserror 2.0.18", ··· 541 541 "futures-lite", 542 542 "futures-util", 543 543 "js-sys", 544 - "ron", 544 + "ron 0.12.0", 545 545 "serde", 546 546 "stackfuture", 547 547 "thiserror 2.0.18", ··· 655 655 "serde", 656 656 "thiserror 2.0.18", 657 657 "wgpu-types", 658 + ] 659 + 660 + [[package]] 661 + name = "bevy_common_assets" 662 + version = "0.15.0" 663 + source = "registry+https://github.com/rust-lang/crates.io-index" 664 + checksum = "ad51a6e9def88caadc4ce2a5670382d6ff19fa1e7e7af934326c5c0b376bdd9f" 665 + dependencies = [ 666 + "anyhow", 667 + "bevy_app", 668 + "bevy_asset", 669 + "bevy_reflect", 670 + "ron 0.11.0", 671 + "serde", 672 + "thiserror 2.0.18", 658 673 ] 659 674 660 675 [[package]] ··· 1413 1428 "bevy_transform", 1414 1429 "bevy_utils", 1415 1430 "derive_more", 1416 - "ron", 1431 + "ron 0.12.0", 1417 1432 "serde", 1418 1433 "thiserror 2.0.18", 1419 1434 "uuid", ··· 4386 4401 4387 4402 [[package]] 4388 4403 name = "ron" 4404 + version = "0.11.0" 4405 + source = "registry+https://github.com/rust-lang/crates.io-index" 4406 + checksum = "db09040cc89e461f1a265139777a2bde7f8d8c67c4936f700c63ce3e2904d468" 4407 + dependencies = [ 4408 + "base64", 4409 + "bitflags 2.11.0", 4410 + "serde", 4411 + "serde_derive", 4412 + "unicode-ident", 4413 + ] 4414 + 4415 + [[package]] 4416 + name = "ron" 4389 4417 version = "0.12.0" 4390 4418 source = "registry+https://github.com/rust-lang/crates.io-index" 4391 4419 checksum = "fd490c5b18261893f14449cbd28cb9c0b637aebf161cd77900bfdedaff21ec32" ··· 4998 5026 "bevy", 4999 5027 "bevy-inspector-egui", 5000 5028 "bevy_asset_loader", 5029 + "bevy_common_assets", 5001 5030 "bevy_flycam", 5031 + "serde", 5002 5032 ] 5003 5033 5004 5034 [[package]]
+2
Cargo.toml
··· 7 7 bevy = "0.18.0" 8 8 bevy-inspector-egui = "0.36.0" 9 9 bevy_asset_loader = { version = "0.25.0", features = ["3d"] } 10 + bevy_common_assets = { version = "0.15.0", features = ["ron"] } 10 11 bevy_flycam = "0.18.0" 12 + serde = "1.0.228"
+44 -2
src/game.rs
··· 3 3 #[derive(States, Debug, Clone, PartialEq, Eq, Hash, Default)] 4 4 pub enum GameState { 5 5 #[default] 6 - TestLoading, 7 - Test, 8 6 Loading, 9 7 Main, 10 8 LoadingLevel(String), 11 9 Level(String) 12 10 } 13 11 12 + #[derive(Debug, Clone, PartialEq, Eq, Hash)] 13 + struct LoadingLevelState; 14 + 15 + impl ComputedStates for LoadingLevelState { 16 + type SourceStates = GameState; 17 + 18 + fn compute(sources: Self::SourceStates) -> Option<Self> { 19 + match sources { 20 + GameState::LoadingLevel(_) => Some(LoadingLevelState), 21 + _ => None 22 + } 23 + } 24 + } 25 + 26 + #[derive(Debug, Clone, PartialEq, Eq, Hash)] 27 + struct LevelState; 28 + 29 + impl ComputedStates for LevelState { 30 + type SourceStates = GameState; 31 + 32 + fn compute(sources: Self::SourceStates) -> Option<Self> { 33 + match sources { 34 + GameState::Level(_) => Some(LevelState), 35 + _ => None 36 + } 37 + } 38 + } 39 + 14 40 #[derive(Resource)] 15 41 pub struct GameAssets { 16 42 pub terminal_primitive0: Handle<Mesh>, 17 43 pub terminal_primitive1: Handle<Mesh>, 18 44 pub terminal_chassis_mat: Handle<StandardMaterial> 45 + } 46 + 47 + pub fn plugin(app: &mut App) { 48 + app.add_computed_state::<LoadingLevelState>() 49 + .add_computed_state::<LevelState>() 50 + .add_systems(OnEnter(LoadingLevelState), load_terminal); 51 + } 52 + 53 + fn load_terminal(mut commands: Commands, asset_server: Res<AssetServer>) { 54 + let assets = GameAssets{ 55 + terminal_primitive0: asset_server.load("models/terminal.glb#Mesh0/Primitive0"), 56 + terminal_primitive1: asset_server.load("models/terminal.glb#Mesh0/Primitive1"), 57 + terminal_chassis_mat: asset_server.load("models/terminal.glb#Material0") 58 + }; 59 + 60 + commands.insert_resource(assets); 19 61 }
+29 -235
src/level.rs
··· 1 - use bevy::{asset::RenderAssetUsages, camera::RenderTarget, input::keyboard::{Key, KeyboardInput}, math::Affine2, prelude::*, render::render_resource::{Extent3d, TextureDimension, TextureFormat, TextureUsages}}; 2 - use crate::{game::{GameAssets, GameState}, terminal}; 1 + use bevy::prelude::*; 2 + use bevy_common_assets::ron::RonAssetPlugin; 3 + use crate::game::GameState; 4 + use std::{fs, path}; 3 5 4 6 pub fn plugin(app: &mut App) { 5 - app.add_systems(OnEnter(GameState::Loading), check_levels) 6 - .add_systems(OnEnter(GameState::Test), run_test) 7 - .add_systems(OnEnter(GameState::TestLoading), load_resources) 8 - .add_systems(Update, check_input.run_if( 9 - in_state(GameState::Test) 10 - )); 7 + app.add_plugins(RonAssetPlugin::<Level>::new(&["level.ron"])) 8 + .add_systems(OnEnter(GameState::Loading), load_levels); 11 9 } 12 10 13 - #[derive(Resource, Default)] 14 - pub struct LevelManifest { 15 - pub levels: Vec<(String, String)> 11 + #[derive(serde::Deserialize, bevy::asset::Asset, bevy::reflect::TypePath)] 12 + pub struct Level { 13 + mesh_path: String 16 14 } 17 15 18 - #[derive(Component)] 19 - struct Terminal; 20 - 21 - #[derive(Component, Default)] 22 - struct CurrentInput(String); 16 + #[derive(Resource)] 17 + struct LevelHandle(Handle<Level>); 23 18 24 - enum ConsoleEntry { 25 - Command(String), 26 - Error(String), 27 - Output(String) 19 + #[derive(Resource)] 20 + pub struct Levels { 21 + pub levels: Vec<LevelHandle> 28 22 } 29 23 30 - #[derive(Component, Default)] 31 - struct ConsoleHistory(Vec<ConsoleEntry>); 32 - 33 - #[derive(Component)] 34 - struct TerminalText; 35 - 36 - fn check_levels(mut manifest: ResMut<LevelManifest>, mut next_state: ResMut<NextState<GameState>>) { 37 - let path = "assets/levels"; 38 - 39 - if let Ok(entries) = std::fs::read_dir(path) { 40 - for entry in entries.flatten() { 41 - let path = entry.path(); 42 - 43 - if path.is_dir() { 44 - let scene_file = path.join("scene.scn.ron"); 45 - let name_file = path.join("name.txt"); 46 - 47 - if scene_file.exists() && name_file.exists() { 48 - if let Some(folder_name) = path.file_name().and_then(|n| n.to_str()) { 49 - let scene_name = std::fs::read_to_string(name_file).expect("Was unable to read scene name"); 50 - manifest.levels.push((folder_name.to_string(), scene_name.clone())); 51 - println!("Found Level: {} - {}", folder_name, scene_name); 52 - } 53 - } 24 + fn load_levels(mut commands: Commands, asset_server: Res<AssetServer>, mut next_state: ResMut<NextState<GameState>>) { 25 + commands.insert_resource(Levels{ 26 + levels: fs::read_dir(path::Path::new("assets").join("levels")).unwrap().map(|entry| -> Option<LevelHandle> { 27 + let entry = entry.unwrap(); 28 + 29 + if entry.path().ends_with(".level.ron") { 30 + Some(LevelHandle(asset_server.load(entry.path()))) 31 + } else { 32 + None 54 33 } 55 - } 56 - } 57 - 58 - next_state.set(GameState::Main); 59 - } 60 - 61 - fn load_resources(mut commands: Commands, asset_server: Res<AssetServer>, mut next_state: ResMut<NextState<GameState>>) { 62 - let assets = GameAssets{ 63 - terminal_primitive0: asset_server.load("models/terminal.glb#Mesh0/Primitive0"), 64 - terminal_primitive1: asset_server.load("models/terminal.glb#Mesh0/Primitive1"), 65 - terminal_chassis_mat: asset_server.load("models/terminal.glb#Material0") 66 - }; 67 - commands.insert_resource(assets); 68 - next_state.set(GameState::Test); 69 - } 70 - 71 - fn run_test(mut commands: Commands, game_assets: Res<GameAssets>, mut meshes: ResMut<Assets<Mesh>>, mut images: ResMut<Assets<Image>>, mut materials: ResMut<Assets<StandardMaterial>>, asset_server: Res<AssetServer>) { 72 - if let Some(mesh) = meshes.get_mut(&game_assets.terminal_primitive1) { 73 - // A simple Quad (4 vertices) UV mapping 74 - let uvs = vec![ 75 - [1.0, 1.0], [0.0, 1.0], // Top Left, Top Right 76 - [1.0, 0.0], [0.0, 0.0] // Bottom Left, Bottom Right 77 - ]; 78 - 79 - // This overwrites the existing UV_0 attribute 80 - mesh.insert_attribute(Mesh::ATTRIBUTE_UV_0, uvs); 81 - } 82 - 83 - let size = Extent3d{ 84 - width: 512, 85 - height: 512, 86 - ..default() 87 - }; 88 - 89 - let mut image = Image::new_fill( 90 - size, 91 - TextureDimension::D2, 92 - &[0, 0, 0, 0], 93 - TextureFormat::Bgra8UnormSrgb, 94 - RenderAssetUsages::default() 95 - ); 96 - 97 - image.texture_descriptor.usage = 98 - TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST | TextureUsages::RENDER_ATTACHMENT; 99 - 100 - let image_handle = images.add(image); 101 - 102 - commands.spawn(DirectionalLight::default()); 103 - 104 - let texture_camera = commands.spawn(( 105 - Camera2d, 106 - Camera{ 107 - order: -1, 108 - ..default() 109 - }, 110 - RenderTarget::Image(image_handle.clone().into()), 111 - )).id(); 112 - 113 - commands.spawn(( 114 - Node{ 115 - width: percent(100), 116 - height: percent(100), 117 - align_items: AlignItems::Start, 118 - ..default() 119 - }, 120 - BackgroundColor(bevy::color::palettes::css::BLACK.into()), 121 - UiTargetCamera(texture_camera) 122 - )).with_children(|parent| { 123 - parent.spawn(( 124 - Text::new(""), 125 - TextFont{ 126 - font_size: 14.0, 127 - font: asset_server.load("fonts/NotoMono.ttf"), 128 - ..default() 129 - }, 130 - TextColor::WHITE, 131 - TerminalText 132 - )); 133 - }); 134 - 135 - let material_handle = materials.add(StandardMaterial{ 136 - base_color_texture: Some(image_handle), 137 - reflectance: 0.02, 138 - unlit: false, 139 - uv_transform: Affine2::from_scale_angle_translation( 140 - Vec2::new(1.0, 1.0), // Scale (e.g., 2.0 zooms in) 141 - 0.0, // Rotation in radians 142 - Vec2::new(0.0, 0.0) // Offset (X, Y) 143 - ), 144 - ..default() 145 - }); 146 - 147 - // terminal mesh 148 - commands.spawn(( 149 - Node::default(), 150 - Transform::from_translation(Vec3::ZERO).with_rotation(Quat::from_rotation_y(-std::f32::consts::FRAC_PI_2)), 151 - CurrentInput::default(), 152 - Terminal, 153 - ConsoleHistory::default() 154 - )).with_children(|parent| { 155 - parent.spawn(( 156 - Mesh3d(game_assets.terminal_primitive0.clone()), 157 - MeshMaterial3d(game_assets.terminal_chassis_mat.clone()) 158 - )); 159 - 160 - parent.spawn(( 161 - Mesh3d(game_assets.terminal_primitive1.clone()), 162 - MeshMaterial3d(material_handle) 163 - )); 34 + }).filter(|val| -> bool { 35 + matches!(val, Some(_)) 36 + }).map(|val| -> LevelHandle { 37 + val.unwrap() 38 + }).collect::<Vec<LevelHandle>>() 164 39 }); 165 40 166 - // light 167 - commands.spawn(( 168 - PointLight{ 169 - shadows_enabled: true, 170 - ..default() 171 - }, 172 - Transform::from_xyz(4.0, 8.0, 4.0).looking_at(Vec3::ZERO, Vec3::Y) 173 - )); 174 - 175 - // camera 176 - commands.spawn(( 177 - Camera3d::default(), 178 - Transform::from_xyz(0.5, -1.0, 3.5).looking_at(Vec3::new(0.0, -1.0, 0.0), Vec3::Y) 179 - )); 180 - 181 - commands.spawn(terminal::TerminalSystem); 182 - 183 - commands.spawn(terminal::TerminalObject::new(String::from("door1"), terminal::TObjectType::Door)); 184 - } 185 - 186 - fn check_input(mut keyboard_input_reader: MessageReader<KeyboardInput>, mut query: Query<(&mut CurrentInput, &Terminal, &mut ConsoleHistory)>, mut terminal_query: Query<(&mut Text, &TerminalText)>, mut terminal_system: Query<&mut terminal::TerminalSystem>, mut commands: Commands) { 187 - let mut current_input = query.single_mut().unwrap(); 188 - let mut current_terminal_text = terminal_query.single_mut().unwrap(); 189 - let mut terminal = terminal_system.single_mut().unwrap(); 190 - 191 - for keyboard_input in keyboard_input_reader.read() { 192 - if !keyboard_input.state.is_pressed() { 193 - continue; 194 - } 195 - 196 - current_terminal_text.0.0.clear(); 197 - 198 - match (&keyboard_input.logical_key, &keyboard_input.text) { 199 - (Key::Enter, _) => { 200 - if current_input.0.0.is_empty() { 201 - continue; 202 - } 203 - 204 - current_input.2.0.push(ConsoleEntry::Command(current_input.0.0.to_owned())); 205 - let nodes = terminal.parse(current_input.0.0.clone()); 206 - commands.trigger(terminal::TerminalEvent::new(nodes)); 207 - current_input.0.0.clear(); 208 - }, 209 - (Key::Backspace, _) => { 210 - current_input.0.0.pop(); 211 - }, 212 - (_, Some(inserted_text)) => { 213 - if inserted_text.chars().all(is_printable_char) { 214 - current_input.0.0.push_str(inserted_text); 215 - } 216 - }, 217 - _ => continue, 218 - } 219 - 220 - for entry in current_input.2.0.iter() { 221 - match entry { 222 - ConsoleEntry::Command(cmd) => { 223 - current_terminal_text.0.0 += cmd; 224 - }, 225 - ConsoleEntry::Output(output) => { 226 - current_terminal_text.0.0 += output; 227 - // TODO: Need to figure out how to change color of text line-by-line 228 - }, 229 - ConsoleEntry::Error(error) => { 230 - current_terminal_text.0.0 += error; 231 - // TODO: Need to figure out how to change color of text line-by-line 232 - } 233 - } 234 - 235 - current_terminal_text.0.0 += "\n"; 236 - } 237 - 238 - current_terminal_text.0.0 += &current_input.0.0; 239 - } 240 - } 241 - 242 - fn is_printable_char(chr: char) -> bool { 243 - let is_in_private_use_area = ('\u{e000}'..='\u{f8ff}').contains(&chr) 244 - || ('\u{f0000}'..='\u{ffffd}').contains(&chr) 245 - || ('\u{100000}'..='\u{10fffd}').contains(&chr); 246 - 247 - !is_in_private_use_area && !chr.is_ascii_control() 41 + next_state.set(GameState::Main); 248 42 }
+1 -1
src/main.rs
··· 13 13 .init_state::<game::GameState>() 14 14 .add_plugins(level::plugin) 15 15 .add_plugins(terminal::plugin) 16 - .init_resource::<level::LevelManifest>() 16 + .add_plugins(game::plugin) 17 17 .run(); 18 18 }