pub fn plugin(app: &mut App) { app .add_systems(OnEnter(GameState::Test), run_test) .add_systems(OnEnter(GameState::TestLoading), load_resources) .add_systems(Update, check_input.run_if( in_state(GameState::Test) )); } #[derive(Component)] struct Terminal; #[derive(Component, Default)] struct CurrentInput(String); enum ConsoleEntry { Command(String), Error(String), Output(String) } #[derive(Component, Default)] struct ConsoleHistory(Vec); #[derive(Component)] struct TerminalText; fn load_resources(mut commands: Commands, asset_server: Res, mut next_state: ResMut>) { let assets = GameAssets{ terminal_primitive0: asset_server.load("models/terminal.glb#Mesh0/Primitive0"), terminal_primitive1: asset_server.load("models/terminal.glb#Mesh0/Primitive1"), terminal_chassis_mat: asset_server.load("models/terminal.glb#Material0") }; commands.insert_resource(assets); next_state.set(GameState::Test); } fn run_test(mut commands: Commands, game_assets: Res, mut meshes: ResMut>, mut images: ResMut>, mut materials: ResMut>, asset_server: Res) { if let Some(mesh) = meshes.get_mut(&game_assets.terminal_primitive1) { // A simple Quad (4 vertices) UV mapping let uvs = vec![ [1.0, 1.0], [0.0, 1.0], // Top Left, Top Right [1.0, 0.0], [0.0, 0.0] // Bottom Left, Bottom Right ]; // This overwrites the existing UV_0 attribute mesh.insert_attribute(Mesh::ATTRIBUTE_UV_0, uvs); } let size = Extent3d{ width: 512, height: 512, ..default() }; let mut image = Image::new_fill( size, TextureDimension::D2, &[0, 0, 0, 0], TextureFormat::Bgra8UnormSrgb, RenderAssetUsages::default() ); image.texture_descriptor.usage = TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST | TextureUsages::RENDER_ATTACHMENT; let image_handle = images.add(image); commands.spawn(DirectionalLight::default()); let texture_camera = commands.spawn(( Camera2d, Camera{ order: -1, ..default() }, RenderTarget::Image(image_handle.clone().into()), )).id(); commands.spawn(( Node{ width: percent(100), height: percent(100), align_items: AlignItems::Start, ..default() }, BackgroundColor(bevy::color::palettes::css::BLACK.into()), UiTargetCamera(texture_camera) )).with_children(|parent| { parent.spawn(( Text::new(""), TextFont{ font_size: 14.0, font: asset_server.load("fonts/NotoMono.ttf"), ..default() }, TextColor::WHITE, TerminalText )); }); let material_handle = materials.add(StandardMaterial{ base_color_texture: Some(image_handle), reflectance: 0.02, unlit: false, uv_transform: Affine2::from_scale_angle_translation( Vec2::new(1.0, 1.0), // Scale (e.g., 2.0 zooms in) 0.0, // Rotation in radians Vec2::new(0.0, 0.0) // Offset (X, Y) ), ..default() }); // terminal mesh commands.spawn(( Node::default(), Transform::from_translation(Vec3::ZERO).with_rotation(Quat::from_rotation_y(-std::f32::consts::FRAC_PI_2)), CurrentInput::default(), Terminal, ConsoleHistory::default() )).with_children(|parent| { parent.spawn(( Mesh3d(game_assets.terminal_primitive0.clone()), MeshMaterial3d(game_assets.terminal_chassis_mat.clone()) )); parent.spawn(( Mesh3d(game_assets.terminal_primitive1.clone()), MeshMaterial3d(material_handle) )); }); // light commands.spawn(( PointLight{ shadows_enabled: true, ..default() }, Transform::from_xyz(4.0, 8.0, 4.0).looking_at(Vec3::ZERO, Vec3::Y) )); // camera commands.spawn(( Camera3d::default(), Transform::from_xyz(0.5, -1.0, 3.5).looking_at(Vec3::new(0.0, -1.0, 0.0), Vec3::Y) )); commands.spawn(terminal::TerminalSystem); commands.spawn(terminal::TerminalObject::new(String::from("door1"), terminal::TObjectType::Door)); } fn check_input(mut keyboard_input_reader: MessageReader, 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) { let mut current_input = query.single_mut().unwrap(); let mut current_terminal_text = terminal_query.single_mut().unwrap(); let mut terminal = terminal_system.single_mut().unwrap(); for keyboard_input in keyboard_input_reader.read() { if !keyboard_input.state.is_pressed() { continue; } current_terminal_text.0.0.clear(); match (&keyboard_input.logical_key, &keyboard_input.text) { (Key::Enter, _) => { if current_input.0.0.is_empty() { continue; } current_input.2.0.push(ConsoleEntry::Command(current_input.0.0.to_owned())); let nodes = terminal.parse(current_input.0.0.clone()); commands.trigger(terminal::TerminalEvent::new(nodes)); current_input.0.0.clear(); }, (Key::Backspace, _) => { current_input.0.0.pop(); }, (_, Some(inserted_text)) => { if inserted_text.chars().all(is_printable_char) { current_input.0.0.push_str(inserted_text); } }, _ => continue, } for entry in current_input.2.0.iter() { match entry { ConsoleEntry::Command(cmd) => { current_terminal_text.0.0 += cmd; }, ConsoleEntry::Output(output) => { current_terminal_text.0.0 += output; // TODO: Need to figure out how to change color of text line-by-line }, ConsoleEntry::Error(error) => { current_terminal_text.0.0 += error; // TODO: Need to figure out how to change color of text line-by-line } } current_terminal_text.0.0 += "\n"; } current_terminal_text.0.0 += ¤t_input.0.0; } } fn is_printable_char(chr: char) -> bool { let is_in_private_use_area = ('\u{e000}'..='\u{f8ff}').contains(&chr) || ('\u{f0000}'..='\u{ffffd}').contains(&chr) || ('\u{100000}'..='\u{10fffd}').contains(&chr); !is_in_private_use_area && !chr.is_ascii_control() }