├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE ├── README.md ├── assets └── hello_rexpaint.xp ├── examples ├── camera.rs ├── font_change.rs ├── grid_position.rs ├── hello.rs ├── noise.rs ├── resized.rs ├── rexpaint.rs ├── spam.rs └── transform.rs ├── images ├── bevy_roguelike.gif ├── bevy_snake.gif ├── bevy_tetris.gif └── title.png └── src ├── ascii.rs ├── border.rs ├── color.rs ├── lib.rs ├── render ├── built_in_fonts │ ├── jt_curses_12x12.png │ ├── pastiche_8x8.png │ ├── px437_8x16.png │ ├── px437_8x8.png │ ├── rexpaint_8x8.png │ ├── sazarote_curses_12x12.png │ ├── taffer_10x10.png │ ├── taritus_curses_8x12.png │ ├── unscii_8x8.png │ └── zx_evolution_8x8.png ├── camera.rs ├── font.rs ├── material.rs ├── mesh.rs ├── mod.rs ├── terminal.wgsl └── uv_mapping.rs ├── rexpaint ├── mod.rs └── reader.rs ├── string.rs ├── terminal.rs ├── tile.rs └── transform.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.17.0] - 2025/03/09 4 | 5 | ### Changes 6 | - Update for bevy 0.16 7 | 8 | ## [0.16.6] - 2025/03/09 9 | 10 | ### Changes 11 | - Fixed a crash where string iterator would try to split a string between char boundaries. 12 | - Minor tweaks to examples. 13 | 14 | ## [0.16.5] - 2025/03/07 15 | 16 | ### Changes 17 | - Merged a pr to directly us a `Handle` in font switching, will probably replace `TerminalFont::Custom` with that at some point. 18 | - Fixed a bug where `Terminal::put_string` would always panic if negative values were used for the `GridPosition`, even if those values would be in bounds, ie: from a centered pivot. 19 | - Added `Terminal::read_line`. 20 | - Cleanup docs. 21 | - Add credits to readme. 22 | 23 | ## [0.16.4] - 2025/03/03 24 | 25 | ### Changes 26 | - Moved `TerminalCamera`update systems to `First` schedule to fix the camera flicker any time the terminal was resized. 27 | 28 | ## [0.16.3] - 2025/03/03 29 | 30 | ### Changes 31 | - Moved mesh update systems from the `Last` schedule to the `PostUpdate` schedule to fix a one-frame-delay bug. 32 | 33 | ## [0.16.2] - 2025/02/27 34 | 35 | ### Changes 36 | - Removed usage of bevy's `embedded_asset!` macro as it causes crashes in windows wasm builds: https://github.com/bevyengine/bevy/issues/14246. Reverted to manually setting up image handles for built in fonts, no api change. 37 | - Added necessary bevy dependencies so linux/wasm builds should work. 38 | - Cargo update. 39 | 40 | ## [0.16.1] - 2025/02/25 41 | 42 | ### Changes 43 | - Fixed a bug where the `TerminalCamera` update system would panic if the terminal font wasn't loaded yet. 44 | 45 | ## [0.16] - 2025/02/17 46 | 47 | ### Changes 48 | - `put_string` now aligns all strings to the top-left by default. You can manually specify a `Pivot` to override this. In addition to wrapping on newlines, it will now word wrap by default. You can override this with the `dont_word_wrap` function. 49 | - Dependency on `TiledCamera` has been removed and entirely replaced by the internal `TerminalCamera`. 50 | - `ToWorld` has been replaced by `TerminalTransform`, which can be used in combination with `TerminalCamera` to transform positions between world space and terminal grid points. See the "transform" example. 51 | - `TerminalBorder` is now a seperate component. 52 | - `TerminalBundle` has been replaced with by bevy's required components system. `TerminalBorder`, `TerminalMeshPivot`, `TerminalMeshTileScaling`, `SetTerminalGridPosition`, and `SetTerminalLayerPosition` are examples of individual components that can added to customize how the terminal gets displayed. See the examples. 53 | - With the bevy color changes all colors are now represented internally as LinearRgba. The `color` module has a list of const lrgba named colors for convenience. 54 | - Probably more that I can't think of. -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["sark"] 3 | description = "A simple terminal for rendering ascii in bevy." 4 | edition = "2024" 5 | homepage = "https://github.com/sarkahn/bevy_ascii_terminal" 6 | keywords = ["bevy", "ascii", "terminal", "roguelike", "tilemap"] 7 | license = "MIT" 8 | name = "bevy_ascii_terminal" 9 | readme = "README.md" 10 | repository = "https://github.com/sarkahn/bevy_ascii_terminal" 11 | version = "0.17.0" 12 | 13 | [dependencies] 14 | enum-ordinalize = "4.3.0" 15 | thiserror = "1.0.56" 16 | flate2 = "1.0" 17 | byteorder = "1" 18 | sark_grids = "0.6.2" 19 | bevy_platform = "0.16.0" 20 | 21 | [dev-dependencies] 22 | fastnoise-lite = "1.1.1" 23 | rand = "0.8.4" 24 | 25 | [dependencies.bevy] 26 | version = "0.16.0" 27 | default-features = false 28 | features = ["std", "png", "bevy_render", "bevy_asset", "bevy_sprite", "bevy_window", "bevy_color"] 29 | 30 | [dev-dependencies.bevy] 31 | version = "0.16.0" 32 | default-features = false 33 | features = ["bevy_winit"] 34 | 35 | [target.'cfg(unix)'.dependencies.bevy] 36 | version = "0.16.0" 37 | default-features = false 38 | features = ["x11"] 39 | 40 | [target.'cfg(target_arch = "wasm32")'.dependencies.bevy] 41 | version = "0.16.0" 42 | default-features = false 43 | features = ["webgl2"] 44 | 45 | # Enable a small amount of optimization in the dev profile. 46 | [profile.dev] 47 | opt-level = 1 48 | 49 | # Enable a large amount of optimization in the dev profile for dependencies. 50 | [profile.dev.package."*"] 51 | opt-level = 3 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Sark 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 2 | [![Crates.io](https://img.shields.io/crates/v/bevy_ascii_terminal)](https://crates.io/crates/bevy_ascii_terminal/) 3 | [![docs](https://docs.rs/bevy_ascii_terminal/badge.svg)](https://docs.rs/bevy_ascii_terminal/) 4 | 5 | # `Bevy Ascii Terminal` 6 | 7 | A simple ascii terminal integrated into bevy's ecs framework. 8 | 9 | --- 10 | ![](images/title.png) 11 | 12 | --- 13 | 14 | The goal of this crate is to provide a simple, straightforward, and hopefully 15 | fast method for rendering colorful ascii in bevy. It was made with "traditional 16 | roguelikes" in mind, but should serve as a simple UI tool if needed. 17 | 18 | # Code Example 19 | 20 | ```rust 21 | use bevy::prelude::*; 22 | use bevy_ascii_terminal::*; 23 | 24 | fn main() { 25 | App::new() 26 | .add_plugins((DefaultPlugins, TerminalPlugins)) 27 | .add_systems(Startup, setup) 28 | .run(); 29 | } 30 | 31 | fn setup(mut commands: Commands) { 32 | commands.spawn(( 33 | Terminal::new([12, 1]).with_string([0, 0], "Hello world!".fg(color::BLUE)), 34 | TerminalBorder::single_line(), 35 | )); 36 | commands.spawn(TerminalCamera::new()); 37 | } 38 | ``` 39 | 40 | ## Versions 41 | | bevy | bevy_ascii_terminal | 42 | | ----- | ------------------- | 43 | | 0.16 | 0.17.* | 44 | | 0.15 | 0.16.* | 45 | | 0.13 | 0.15.0 | 46 | | 0.12 | 0.14.0 | 47 | | 0.11 | 0.13.0 | 48 | | 0.9 | 0.12.1 | 49 | | 0.8.1 | 0.11.1-4 | 50 | | 0.8 | 0.11 | 51 | | 0.7 | 0.9-0.10 | 52 | 53 | ## Bevy Ascii Terminal Projects 54 | _(Note these were built on earlier versions and haven't been updated in a while)_ 55 | 56 | **Bevy Roguelike** - [Source](https://github.com/sarkahn/bevy_roguelike/) - [WASM](https://sarkahn.github.io/bevy_rust_roguelike_tut_web/) 57 | 58 | **Ascii Snake** - [Source](https://github.com/sarkahn/bevy_ascii_snake/) - [WASM](https://sarkahn.github.io/bevy_ascii_snake/) 59 | 60 | **Ascii Tetris** - [Source](https://github.com/sarkahn/bevy_ascii_tetris/) - [WASM](https://sarkahn.github.io/bevy_ascii_tetris/) 61 | 62 | [![Roguelike](images/bevy_roguelike.gif)](https://github.com/sarkahn/bevy_roguelike/) 63 | [![Snake](images/bevy_snake.gif)](https://github.com/sarkahn/bevy_ascii_snake) 64 | [![Tetris](images/bevy_tetris.gif)](https://github.com/sarkahn/bevy_ascii_tetris/) 65 | 66 | ## Credits 67 | 68 | Built in fonts were put together from various sources and modified only to make them uniform by changing background colors and adding the empty box drawing character from rexpaint (`□`): 69 | - Px437 - https://int10h.org/oldschool-pc-fonts/ 70 | - ZxEvolution - https://www.gridsagegames.com/rexpaint/resources.html 71 | - Pastiche - https://dwarffortresswiki.org/index.php/DF2014:Tileset_repository 72 | - Rexpaint - https://www.gridsagegames.com/rexpaint/resources.html 73 | - Unscii - https://github.com/viznut/unscii 74 | - Taffer - https://dwarffortresswiki.org/index.php/DF2014:Tileset_repository 75 | - TaritusCurses - https://dwarffortresswiki.org/index.php/DF2014:Tileset_repository 76 | - JtCurses - https://dwarffortresswiki.org/index.php/DF2014:Tileset_repository 77 | - SazaroteCurses - Unknown 78 | 79 | Rexpaint loader - https://docs.rs/rexpaint/latest/rexpaint/ -------------------------------------------------------------------------------- /assets/hello_rexpaint.xp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sarkahn/bevy_ascii_terminal/335f453380c051eaeea06ff6525cb97e1527fa15/assets/hello_rexpaint.xp -------------------------------------------------------------------------------- /examples/camera.rs: -------------------------------------------------------------------------------- 1 | //! Demonstrates how the [TerminalCamera] will automatically adjust the viewport 2 | //! to render all visible terminals. 3 | 4 | use bevy::{ 5 | app::AppExit, 6 | color::palettes::css::{BLUE, RED}, 7 | prelude::*, 8 | time::common_conditions::on_timer, 9 | }; 10 | use bevy_ascii_terminal::*; 11 | 12 | const FADED: f32 = 0.65; 13 | const BRIGHT: f32 = 1.0; 14 | 15 | #[derive(Resource, Default)] 16 | struct Current(usize); 17 | 18 | /// It's necessary to store the strings externally since the terminals may be 19 | /// resized. 20 | #[derive(Component)] 21 | pub struct TermString(String, Pivot); 22 | 23 | fn main() { 24 | let key_repeat = std::time::Duration::from_secs_f32(0.1); 25 | App::new() 26 | .add_plugins((DefaultPlugins, TerminalPlugins)) 27 | .init_resource::() 28 | .add_systems(Startup, setup) 29 | .add_systems(PostStartup, put_strings) 30 | .add_systems( 31 | Update, 32 | ( 33 | handle_just_pressed, 34 | handle_pressed.run_if(on_timer(key_repeat)), 35 | ), 36 | ) 37 | .run(); 38 | } 39 | 40 | fn setup(mut commands: Commands) { 41 | commands.spawn(TerminalCamera::new()); 42 | 43 | commands.spawn(( 44 | make_terminal([10, 10], BRIGHT), 45 | TerminalMeshPivot::BottomRight, 46 | TerminalBorder::single_line(), 47 | TermString("WASD to change size".to_string(), Pivot::Center), 48 | )); 49 | commands.spawn(( 50 | make_terminal([10, 10], FADED), 51 | TerminalMeshPivot::BottomLeft, 52 | TerminalBorder::single_line(), 53 | TermString("Tab to change active terminal".to_string(), Pivot::Center), 54 | )); 55 | commands.spawn(( 56 | make_terminal([12, 12], FADED), 57 | TerminalMeshPivot::TopCenter, 58 | TerminalBorder::single_line(), 59 | TermString("Space to toggle border".to_string(), Pivot::TopCenter), 60 | )); 61 | } 62 | 63 | fn make_terminal(size: impl GridSize, lightness: f32) -> Terminal { 64 | let mut term = Terminal::new(size); 65 | draw_grid(&mut term, lightness); 66 | term 67 | } 68 | 69 | fn draw_grid(term: &mut Terminal, lightness: f32) { 70 | for (p, t) in term.iter_xy_mut() { 71 | let grid_color = if (p.x + p.y) % 2 == 0 { 72 | BLUE.with_luminance(lightness - 0.5) 73 | } else { 74 | RED.with_luminance(lightness - 0.5) 75 | }; 76 | t.fg_color = t.fg_color.with_luminance(lightness); 77 | t.bg_color = grid_color.into(); 78 | } 79 | } 80 | 81 | fn put_strings(mut q_term: Query<(&mut Terminal, &TermString)>) { 82 | for (mut term, string) in &mut q_term { 83 | term.put_string([0, 0].pivot(string.1), string.0.as_str().clear_colors()); 84 | } 85 | } 86 | 87 | fn handle_just_pressed( 88 | mut q_term: Query<(Entity, &mut Terminal, &TermString)>, 89 | input: Res>, 90 | q_border: Query<&TerminalBorder>, 91 | mut current: ResMut, 92 | mut evt_quit: EventWriter, 93 | mut commands: Commands, 94 | ) { 95 | // If we're accessing a terminal by index we need to make sure they're 96 | // always in the same order 97 | let mut terminals: Vec<_> = q_term.iter_mut().sort::().collect(); 98 | if input.just_pressed(KeyCode::Tab) { 99 | current.0 = (current.0 + 1) % terminals.len(); 100 | for (i, (_, term, string)) in terminals.iter_mut().enumerate() { 101 | let lightness = if current.0 == i { BRIGHT } else { FADED }; 102 | draw_grid(term, lightness); 103 | term.put_string([0, 0].pivot(string.1), string.0.as_str().clear_colors()); 104 | } 105 | } 106 | 107 | if input.just_pressed(KeyCode::Escape) { 108 | evt_quit.write(AppExit::Success); 109 | } 110 | 111 | if input.just_pressed(KeyCode::Space) { 112 | if q_border.get(terminals[current.0].0).is_ok() { 113 | commands 114 | .entity(terminals[current.0].0) 115 | .remove::(); 116 | } else { 117 | commands 118 | .entity(terminals[current.0].0) 119 | .insert(TerminalBorder::single_line()); 120 | }; 121 | } 122 | } 123 | 124 | fn handle_pressed( 125 | mut q_term: Query<(&mut Terminal, &TermString)>, 126 | input: Res>, 127 | current: Res, 128 | ) { 129 | let hor = input.pressed(KeyCode::KeyD) as i32 - input.pressed(KeyCode::KeyA) as i32; 130 | let ver = input.pressed(KeyCode::KeyW) as i32 - input.pressed(KeyCode::KeyS) as i32; 131 | 132 | let size = IVec2::new(hor, ver); 133 | if !size.cmpeq(IVec2::ZERO).all() { 134 | // You can sort by entity even if Entity isn't explicitly in the query 135 | let mut terminals: Vec<_> = q_term.iter_mut().sort::().collect(); 136 | let string = terminals[current.0].1; 137 | let term = &mut terminals[current.0].0; 138 | 139 | let curr_size = term.size().as_ivec2(); 140 | term.resize((curr_size + size).max(IVec2::ONE).as_uvec2()); 141 | term.clear(); 142 | draw_grid(term, BRIGHT); 143 | term.put_string([0, 0].pivot(string.1), string.0.as_str().clear_colors()); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /examples/font_change.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Sub; 2 | 3 | use bevy::{ 4 | color::palettes::css::{MAROON, MIDNIGHT_BLUE}, 5 | prelude::*, 6 | reflect::{DynamicVariant, Enum}, 7 | }; 8 | use bevy_ascii_terminal::*; 9 | 10 | fn main() { 11 | App::new() 12 | .add_plugins((DefaultPlugins, TerminalPlugins)) 13 | .add_systems(Startup, setup) 14 | .add_systems(Update, (input, update)) 15 | .run(); 16 | } 17 | 18 | fn setup(mut commands: Commands) { 19 | let size = [47, 12]; 20 | let clear_tile = Tile::default().with_bg(MIDNIGHT_BLUE); 21 | let string = String::from_iter(CP437.chars()); 22 | let term = Terminal::new(size) 23 | .with_clear_tile(clear_tile) 24 | // Unlike put_char, put_string defaults to a top left pivot 25 | .with_string([0, 0], "Press spacebar to change fonts") 26 | .with_string([0, 1], "The quick brown fox jumps over the lazy dog.") 27 | .with_string([0, 2], string.fg(color::TAN)); 28 | // .with_string([0, 7], "☺☻♥♦♣♠•'◘'○'◙'♂♀♪♫☼►◄↕‼¶§▬↨↑↓→←∟↔▲▼"); 29 | // .with_string([0, 9], "░▒▓│┤╡╢╖╕╣║╗╝╜╛┐└╒╓╫╪┘┌█▄▌▐▀αßΓπΣσµτΦΘΩδ∞"); 30 | commands.spawn((term, TerminalBorder::single_line())); 31 | commands.spawn(TerminalCamera::new()); 32 | } 33 | 34 | fn input(input: Res>, mut q_term: Query<&mut TerminalFont>) { 35 | if input.just_pressed(KeyCode::Space) { 36 | let mut font = q_term.single_mut().unwrap(); 37 | let info = font 38 | .get_represented_type_info() 39 | .expect("Error getting terminal font enum info"); 40 | let info = match info { 41 | bevy::reflect::TypeInfo::Enum(info) => info, 42 | _ => unreachable!(), 43 | }; 44 | // Exclude custom variant 45 | let max = info.variant_len().sub(2); 46 | let i = font.variant_index(); 47 | let i = (i + 1).rem_euclid(max); 48 | let mut dynamic = font.to_dynamic_enum(); 49 | dynamic.set_variant_with_index(i, info.variant_names()[i], DynamicVariant::Unit); 50 | font.apply(&dynamic); 51 | } 52 | } 53 | 54 | fn update(mut q_term: Query<(&TerminalFont, &mut TerminalBorder), Changed>) { 55 | if let Ok((font, mut border)) = q_term.single_mut() { 56 | border.clear_strings(); 57 | border.put_title(font.variant_name().fg(MAROON).delimiters("[]")); 58 | } 59 | } 60 | 61 | const CP437: &str = r#" 62 | .☺☻♥♦♣♠•◘○◙♂♀♪♫☼ ►◄↕‼¶§▬↨↑↓→←∟↔▲▼ 63 | !\"\#$%&'()*+,-./ 0123456789:;<=>? 64 | @ABCDEFGHIJKLMNO PQRSTUVWXYZ[\]^_ 65 | `abcdefghijklmno pqrstuvwxyz{|}~⌂ 66 | ÇüéâäàåçêëèïîìÄÅ ÉæÆôöòûùÿÖÜ¢£¥₧ƒ 67 | áíóúñѪº¿⌐¬½¼¡«» ░▒▓│┤╡╢╖╕╣║╗╝╜╛┐ 68 | └┴┬├─┼╞╟╚╔╩╦╠═╬╧ ╨╤╥╙╘╒╓╫╪┘┌█▄▌▐▀ 69 | αßΓπΣσµτΦΘΩδ∞φε∩ ≡±≥≤⌠⌡÷≈°∙·√ⁿ²■□ 70 | "#; 71 | -------------------------------------------------------------------------------- /examples/grid_position.rs: -------------------------------------------------------------------------------- 1 | //! Demonstrates how SetTerminalGridPosition and SetTerminalLayerPosition can 2 | //! be used to position terminals on the virtual grid. 3 | 4 | use bevy::prelude::*; 5 | use bevy_ascii_terminal::*; 6 | 7 | fn main() { 8 | App::new() 9 | .add_plugins((DefaultPlugins, TerminalPlugins)) 10 | .add_systems(Startup, setup) 11 | .run(); 12 | } 13 | 14 | fn setup(mut commands: Commands) { 15 | commands.spawn(( 16 | Terminal::new([9, 9]).with_clear_tile(Tile::new( 17 | ascii::Glyph::SmilingFace.into(), 18 | color::WHITE, 19 | color::RED.with_alpha(0.2), 20 | )), 21 | SetTerminalGridPosition::from([3, 3]), 22 | SetTerminalLayerPosition(3), 23 | )); 24 | commands.spawn(( 25 | Terminal::new([9, 9]).with_clear_tile(Tile::new( 26 | ascii::Glyph::FractionQuarter.into(), 27 | color::GREEN, 28 | color::BLUE.with_alpha(0.6), 29 | )), 30 | SetTerminalGridPosition::from([-3, 3]), 31 | SetTerminalLayerPosition(2), 32 | )); 33 | commands.spawn(( 34 | Terminal::new([20, 10]).with_clear_tile(Tile::new( 35 | ascii::Glyph::AtSymbol.into(), 36 | color::ORANGE.with_alpha(0.5), 37 | color::GRAY.with_alpha(0.7), 38 | )), 39 | SetTerminalGridPosition::from([0, -3]), 40 | SetTerminalLayerPosition(1), 41 | )); 42 | commands.spawn(TerminalCamera::new()); 43 | } 44 | -------------------------------------------------------------------------------- /examples/hello.rs: -------------------------------------------------------------------------------- 1 | //! A minimal example with a terminal and camera. 2 | 3 | use bevy::prelude::*; 4 | use bevy_ascii_terminal::*; 5 | 6 | fn main() { 7 | App::new() 8 | .add_plugins((DefaultPlugins, TerminalPlugins)) 9 | .add_systems(Startup, setup) 10 | .run(); 11 | } 12 | 13 | fn setup(mut commands: Commands) { 14 | commands.spawn(( 15 | Terminal::new([12, 1]).with_string([0, 0], "Hello world!".fg(color::BLUE)), 16 | TerminalBorder::single_line(), 17 | )); 18 | commands.spawn(TerminalCamera::new()); 19 | } 20 | -------------------------------------------------------------------------------- /examples/noise.rs: -------------------------------------------------------------------------------- 1 | //! An interactive ui to display noise using the fastnoise-lite crate. 2 | 3 | use bevy::{app::AppExit, prelude::*, time::common_conditions::on_timer}; 4 | use bevy_ascii_terminal::*; 5 | use fastnoise_lite::*; 6 | 7 | fn main() { 8 | let controls = State { 9 | current_control: 0, 10 | noise_type: NoiseType::OpenSimplex2, 11 | fractal_type: FractalType::FBm, 12 | values: vec![ 13 | Control { 14 | name: "Seed".to_string(), 15 | value: 0.0, 16 | step: 1.0, 17 | }, 18 | Control { 19 | name: "Octaves".to_string(), 20 | value: 3.0, 21 | step: 1.0, 22 | }, 23 | Control { 24 | name: "Frequency".to_string(), 25 | value: 0.1, 26 | step: 0.005, 27 | }, 28 | Control { 29 | name: "Lacunarity".to_string(), 30 | value: 2.0, 31 | step: 0.02, 32 | }, 33 | Control { 34 | name: "Gain".to_string(), 35 | value: 0.5, 36 | step: 0.01, 37 | }, 38 | Control { 39 | name: "Weighted Strength".to_string(), 40 | value: 0.0, 41 | step: 0.03, 42 | }, 43 | ], 44 | }; 45 | let key_repeat = std::time::Duration::from_secs_f32(0.1); 46 | App::new() 47 | .insert_resource(controls) 48 | .add_plugins((DefaultPlugins, TerminalPlugins)) 49 | .add_systems(Startup, setup) 50 | .add_systems( 51 | Update, 52 | ( 53 | handle_key_repeat.run_if(on_timer(key_repeat)), 54 | handle_other_input, 55 | draw_controls.run_if(resource_changed::), 56 | make_some_noise.run_if(resource_changed::), 57 | ) 58 | .chain(), 59 | ) 60 | .run(); 61 | } 62 | 63 | #[derive(Component)] 64 | pub struct ControlsTerminal; 65 | 66 | fn setup(mut commands: Commands) { 67 | commands.spawn((Terminal::new([80, 60]), TerminalMeshPivot::TopLeft)); 68 | commands.spawn(( 69 | Terminal::new([30, 30]), 70 | TerminalMeshPivot::TopRight, 71 | ControlsTerminal, 72 | )); 73 | commands.spawn(TerminalCamera::new()); 74 | } 75 | 76 | pub struct Control { 77 | name: String, 78 | value: f32, 79 | step: f32, 80 | } 81 | 82 | #[derive(Resource)] 83 | struct State { 84 | current_control: usize, 85 | noise_type: NoiseType, 86 | fractal_type: FractalType, 87 | values: Vec, 88 | } 89 | 90 | fn handle_key_repeat(input: Res>, mut controls: ResMut) { 91 | let hor = input.pressed(KeyCode::KeyD) as i32 - input.pressed(KeyCode::KeyA) as i32; 92 | if hor != 0 { 93 | let curr = controls.current_control; 94 | let step = controls.values[curr].step; 95 | controls.values[curr].value += step * hor as f32; 96 | } 97 | } 98 | 99 | fn handle_other_input( 100 | input: Res>, 101 | mut controls: ResMut, 102 | mut evt_quit: EventWriter, 103 | ) { 104 | if input.just_pressed(KeyCode::Escape) { 105 | evt_quit.write(AppExit::Success); 106 | } 107 | let ver = input.just_pressed(KeyCode::KeyS) as i32 - input.just_pressed(KeyCode::KeyW) as i32; 108 | if ver != 0 { 109 | let mut value = controls.current_control as i32; 110 | value = (value + ver).rem_euclid(controls.values.len() as i32); 111 | controls.current_control = value as usize; 112 | } 113 | if input.just_pressed(KeyCode::Tab) { 114 | let curr = controls.fractal_type; 115 | controls.fractal_type = match curr { 116 | FractalType::None => FractalType::FBm, 117 | FractalType::FBm => FractalType::Ridged, 118 | FractalType::Ridged => FractalType::PingPong, 119 | FractalType::PingPong => FractalType::None, 120 | _ => FractalType::FBm, 121 | }; 122 | } 123 | 124 | if input.just_pressed(KeyCode::Space) { 125 | let curr = controls.noise_type; 126 | controls.noise_type = match curr { 127 | NoiseType::OpenSimplex2 => NoiseType::OpenSimplex2S, 128 | NoiseType::OpenSimplex2S => NoiseType::Cellular, 129 | NoiseType::Cellular => NoiseType::Perlin, 130 | NoiseType::Perlin => NoiseType::ValueCubic, 131 | NoiseType::ValueCubic => NoiseType::Value, 132 | NoiseType::Value => NoiseType::OpenSimplex2, 133 | }; 134 | } 135 | } 136 | 137 | fn draw_controls(mut q_term: Query<&mut Terminal, With>, controls: Res) { 138 | let mut term = q_term.single_mut().unwrap(); 139 | term.clear(); 140 | term.put_string([0, 0], "WASD to change noise values"); 141 | term.put_string([0, 1], "Space to change noise type"); 142 | term.put_string([0, 2], "Tab to change fractal type"); 143 | term.put_string([0, 3], "Escape to quit"); 144 | term.put_string([0, 4], "-----------------------------"); 145 | for (i, control) in controls.values.iter().enumerate() { 146 | let value = (control.value * 1000.0).round() / 1000.0; 147 | let control_string = format!("{}: {}", control.name, value); 148 | term.put_string([0, i + 5], control_string.as_str()); 149 | 150 | if i == controls.current_control { 151 | term.put_string( 152 | [control_string.len() + 1, i + 5], 153 | "<--".fg(LinearRgba::GREEN), 154 | ); 155 | } 156 | } 157 | } 158 | 159 | fn make_some_noise( 160 | mut q_term: Query<&mut Terminal, Without>, 161 | controls: Res, 162 | ) { 163 | let mut term = q_term.single_mut().unwrap(); 164 | let mut noise = FastNoiseLite::new(); 165 | noise.set_noise_type(Some(controls.noise_type)); 166 | noise.set_fractal_type(Some(controls.fractal_type)); 167 | 168 | noise.set_seed(Some(controls.values[0].value as i32)); 169 | noise.set_fractal_octaves(Some((controls.values[1].value as i32).max(1))); 170 | noise.set_frequency(Some(controls.values[2].value)); 171 | noise.set_fractal_lacunarity(Some(controls.values[3].value)); 172 | noise.set_fractal_gain(Some(controls.values[4].value)); 173 | noise.set_fractal_weighted_strength(Some(controls.values[5].value)); 174 | 175 | for (p, t) in term.iter_xy_mut() { 176 | let noise = noise.get_noise_2d(p.x as f32, p.y as f32); 177 | let noise = (noise + 1.0) / 2.0; 178 | let glyph = if noise < 0.25 { 179 | Glyph::ShadeLight 180 | } else if noise < 0.5 { 181 | Glyph::ShadeMedium 182 | } else if noise < 0.75 { 183 | Glyph::ShadeDark 184 | } else { 185 | Glyph::BlockFull 186 | }; 187 | t.glyph = glyph.to_char(); 188 | t.bg_color = Hsla::from(t.bg_color).with_lightness(noise).into(); 189 | } 190 | term.put_string( 191 | [0, 0], 192 | format!( 193 | "[Noise:{:?} | Fractal:{:?}]", 194 | controls.noise_type, controls.fractal_type 195 | ) 196 | .clear_colors(), 197 | ); 198 | } 199 | -------------------------------------------------------------------------------- /examples/resized.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use bevy::{prelude::*, time::common_conditions::on_timer}; 4 | use bevy_ascii_terminal::*; 5 | 6 | fn main() { 7 | App::new() 8 | .add_plugins((DefaultPlugins, TerminalPlugins)) 9 | .add_systems(Startup, setup) 10 | .add_systems( 11 | Update, 12 | update.run_if(on_timer(Duration::from_secs_f32(0.01))), 13 | ) 14 | .run(); 15 | } 16 | 17 | fn setup(mut commands: Commands) { 18 | commands.spawn(Terminal::new([30, 30])); 19 | commands.spawn(TerminalCamera::new()); 20 | } 21 | 22 | fn update(mut q_term: Query<&mut Terminal>, time: Res