A Quadrilateral Cowboy clone intended to help me learn Game Dev
1pub fn plugin(app: &mut App) {
2 app
3 .add_systems(OnEnter(GameState::Test), run_test)
4 .add_systems(OnEnter(GameState::TestLoading), load_resources)
5 .add_systems(Update, check_input.run_if(
6 in_state(GameState::Test)
7 ));
8}
9
10#[derive(Component)]
11struct Terminal;
12
13#[derive(Component, Default)]
14struct CurrentInput(String);
15
16enum ConsoleEntry {
17 Command(String),
18 Error(String),
19 Output(String)
20}
21
22#[derive(Component, Default)]
23struct ConsoleHistory(Vec<ConsoleEntry>);
24
25#[derive(Component)]
26struct TerminalText;
27
28fn load_resources(mut commands: Commands, asset_server: Res<AssetServer>, mut next_state: ResMut<NextState<GameState>>) {
29 let assets = GameAssets{
30 terminal_primitive0: asset_server.load("models/terminal.glb#Mesh0/Primitive0"),
31 terminal_primitive1: asset_server.load("models/terminal.glb#Mesh0/Primitive1"),
32 terminal_chassis_mat: asset_server.load("models/terminal.glb#Material0")
33 };
34 commands.insert_resource(assets);
35 next_state.set(GameState::Test);
36}
37
38fn 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>) {
39 if let Some(mesh) = meshes.get_mut(&game_assets.terminal_primitive1) {
40 // A simple Quad (4 vertices) UV mapping
41 let uvs = vec![
42 [1.0, 1.0], [0.0, 1.0], // Top Left, Top Right
43 [1.0, 0.0], [0.0, 0.0] // Bottom Left, Bottom Right
44 ];
45
46 // This overwrites the existing UV_0 attribute
47 mesh.insert_attribute(Mesh::ATTRIBUTE_UV_0, uvs);
48 }
49
50 let size = Extent3d{
51 width: 512,
52 height: 512,
53 ..default()
54 };
55
56 let mut image = Image::new_fill(
57 size,
58 TextureDimension::D2,
59 &[0, 0, 0, 0],
60 TextureFormat::Bgra8UnormSrgb,
61 RenderAssetUsages::default()
62 );
63
64 image.texture_descriptor.usage =
65 TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST | TextureUsages::RENDER_ATTACHMENT;
66
67 let image_handle = images.add(image);
68
69 commands.spawn(DirectionalLight::default());
70
71 let texture_camera = commands.spawn((
72 Camera2d,
73 Camera{
74 order: -1,
75 ..default()
76 },
77 RenderTarget::Image(image_handle.clone().into()),
78 )).id();
79
80 commands.spawn((
81 Node{
82 width: percent(100),
83 height: percent(100),
84 align_items: AlignItems::Start,
85 ..default()
86 },
87 BackgroundColor(bevy::color::palettes::css::BLACK.into()),
88 UiTargetCamera(texture_camera)
89 )).with_children(|parent| {
90 parent.spawn((
91 Text::new(""),
92 TextFont{
93 font_size: 14.0,
94 font: asset_server.load("fonts/NotoMono.ttf"),
95 ..default()
96 },
97 TextColor::WHITE,
98 TerminalText
99 ));
100 });
101
102 let material_handle = materials.add(StandardMaterial{
103 base_color_texture: Some(image_handle),
104 reflectance: 0.02,
105 unlit: false,
106 uv_transform: Affine2::from_scale_angle_translation(
107 Vec2::new(1.0, 1.0), // Scale (e.g., 2.0 zooms in)
108 0.0, // Rotation in radians
109 Vec2::new(0.0, 0.0) // Offset (X, Y)
110 ),
111 ..default()
112 });
113
114 // terminal mesh
115 commands.spawn((
116 Node::default(),
117 Transform::from_translation(Vec3::ZERO).with_rotation(Quat::from_rotation_y(-std::f32::consts::FRAC_PI_2)),
118 CurrentInput::default(),
119 Terminal,
120 ConsoleHistory::default()
121 )).with_children(|parent| {
122 parent.spawn((
123 Mesh3d(game_assets.terminal_primitive0.clone()),
124 MeshMaterial3d(game_assets.terminal_chassis_mat.clone())
125 ));
126
127 parent.spawn((
128 Mesh3d(game_assets.terminal_primitive1.clone()),
129 MeshMaterial3d(material_handle)
130 ));
131 });
132
133 // light
134 commands.spawn((
135 PointLight{
136 shadows_enabled: true,
137 ..default()
138 },
139 Transform::from_xyz(4.0, 8.0, 4.0).looking_at(Vec3::ZERO, Vec3::Y)
140 ));
141
142 // camera
143 commands.spawn((
144 Camera3d::default(),
145 Transform::from_xyz(0.5, -1.0, 3.5).looking_at(Vec3::new(0.0, -1.0, 0.0), Vec3::Y)
146 ));
147
148 commands.spawn(terminal::TerminalSystem);
149
150 commands.spawn(terminal::TerminalObject::new(String::from("door1"), terminal::TObjectType::Door));
151}
152
153fn 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) {
154 let mut current_input = query.single_mut().unwrap();
155 let mut current_terminal_text = terminal_query.single_mut().unwrap();
156 let mut terminal = terminal_system.single_mut().unwrap();
157
158 for keyboard_input in keyboard_input_reader.read() {
159 if !keyboard_input.state.is_pressed() {
160 continue;
161 }
162
163 current_terminal_text.0.0.clear();
164
165 match (&keyboard_input.logical_key, &keyboard_input.text) {
166 (Key::Enter, _) => {
167 if current_input.0.0.is_empty() {
168 continue;
169 }
170
171 current_input.2.0.push(ConsoleEntry::Command(current_input.0.0.to_owned()));
172 let nodes = terminal.parse(current_input.0.0.clone());
173 commands.trigger(terminal::TerminalEvent::new(nodes));
174 current_input.0.0.clear();
175 },
176 (Key::Backspace, _) => {
177 current_input.0.0.pop();
178 },
179 (_, Some(inserted_text)) => {
180 if inserted_text.chars().all(is_printable_char) {
181 current_input.0.0.push_str(inserted_text);
182 }
183 },
184 _ => continue,
185 }
186
187 for entry in current_input.2.0.iter() {
188 match entry {
189 ConsoleEntry::Command(cmd) => {
190 current_terminal_text.0.0 += cmd;
191 },
192 ConsoleEntry::Output(output) => {
193 current_terminal_text.0.0 += output;
194 // TODO: Need to figure out how to change color of text line-by-line
195 },
196 ConsoleEntry::Error(error) => {
197 current_terminal_text.0.0 += error;
198 // TODO: Need to figure out how to change color of text line-by-line
199 }
200 }
201
202 current_terminal_text.0.0 += "\n";
203 }
204
205 current_terminal_text.0.0 += ¤t_input.0.0;
206 }
207}
208
209fn is_printable_char(chr: char) -> bool {
210 let is_in_private_use_area = ('\u{e000}'..='\u{f8ff}').contains(&chr)
211 || ('\u{f0000}'..='\u{ffffd}').contains(&chr)
212 || ('\u{100000}'..='\u{10fffd}').contains(&chr);
213
214 !is_in_private_use_area && !chr.is_ascii_control()
215}