diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 3247f16..e31e644 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -11,112 +11,71 @@ on: jobs: # Run the `rustfmt` code formatter - rustfmt: - name: Rustfmt [Formatter] + rust: + name: cargo fmt & cargo clippy runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 - - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable - components: rustfmt - override: true - - run: rustup component add rustfmt - - uses: actions-rs/cargo@v1 - with: - command: fmt - args: --all -- --check + - uses: actions/checkout@v2 - # Run the `clippy` linting tool - clippy: - name: Clippy [Linter] - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v1 - - name: Install dependencies - run: sudo apt-get install -y xorg-dev libasound2-dev - - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable - components: clippy - override: true - - uses: actions-rs/clippy-check@v1 - with: - token: ${{ secrets.GITHUB_TOKEN }} - args: --all-targets --all-features -- -D clippy::all + - name: install dependencies + # run: sudo apt-get install -y libasound2-dev libwayland-cursor0 libxkbcommon-dev libwayland-dev + run: sudo apt-get install -y libwayland-cursor0 libxkbcommon-dev libwayland-dev - # Ensure that the project could be successfully compiled - cargo_check: - name: Compile - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v1 - - name: Install dependencies - run: sudo apt-get install -y xorg-dev libasound2-dev - - uses: actions-rs/toolchain@v1 + - name: install rust + uses: actions-rs/toolchain@v1 with: - profile: minimal - toolchain: stable + profile: default + toolchain: nightly override: true - - uses: actions-rs/cargo@v1 + + - name: cargo check + uses: actions-rs/cargo@v1 with: command: check args: --all - # Run tests on Linux, macOS, and Windows - # On both Rust stable and Rust nightly - test: - name: Test Suite - needs: [cargo_check] - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest, macOS-latest, windows-latest] - rust: [stable, nightly] + - name: cargo fmt + uses: actions-rs/cargo@v1 + with: + command: fmt + args: --all -- --check + + wasm: + name: wasm compile & deploy + runs-on: ubuntu-latest steps: - # Checkout the branch being tested - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 - # Install all the required dependencies for testing - - uses: actions-rs/toolchain@v1 + - name: install rust + uses: actions-rs/toolchain@v1 with: profile: minimal - toolchain: stable + toolchain: nightly override: true + target: wasm32-unknown-unknown - # Install Node.js at a fixed version - - uses: actions/setup-node@v1 - with: - node-version: "12.0" - - # Install Ruby at a fixed version - - uses: actions/setup-ruby@v1 - with: - ruby-version: "2.6" + - name: install wasm-bindgen + uses: jetli/wasm-bindgen-action@v0.1.0 - # Install Python at a fixed version - - uses: actions/setup-python@v1 - with: - python-version: "3.7" - - # Install dotnet at a fixed version - - uses: actions/setup-dotnet@v1 + - name: cargo build --release + uses: actions-rs/cargo@v1 with: - dotnet-version: "2.2.402" + command: build + args: --release --target wasm32-unknown-unknown - - name: Install dependencies - if: matrix.os == 'ubuntu-latest' - run: sudo apt-get install -y xorg-dev libasound2-dev + - name: run wasm-bindgen and copy assets + run: | + wasm-bindgen --out-name castle-game --out-dir web --target web target/wasm32-unknown-unknown/release/castle-game.wasm + rm -rf web/assets + cp -R assets web/assets - # Run the ignored tests that expect the above setup - - name: Run all tests - uses: actions-rs/cargo@v1 - with: - command: test - args: -- -Z unstable-options --include-ignored + - name: deploy to github pages + uses: s0/git-publish-subdir-action@master + env: + REPO: self + BRANCH: gh-pages + FOLDER: web + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Build sources for every OS github_build: @@ -132,13 +91,13 @@ jobs: include: - target: x86_64-unknown-linux-gnu os: ubuntu-latest - name: castle-game-x86_64-unknown-linux-gnu.tar.gz + name: castle_game-x86_64-unknown-linux-gnu - target: x86_64-apple-darwin os: macOS-latest - name: castle-game-x86_64-apple-darwin.tar.gz + name: castle_game-x86_64-apple-darwin - target: x86_64-pc-windows-msvc os: windows-latest - name: castle-game-x86_64-pc-windows-msvc.zip + name: castle_game-x86_64-pc-windows-msvc.exe runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v1 @@ -147,13 +106,13 @@ jobs: uses: actions-rs/toolchain@v1 with: profile: minimal - toolchain: stable + toolchain: nightly override: true target: ${{ matrix.target }} - name: Install dependencies if: matrix.os == 'ubuntu-latest' - run: sudo apt-get install -y xorg-dev libasound2-dev + run: sudo apt-get install -y libx11-dev libxi-dev libgl1-mesa-dev gcc-mingw-w64 - name: Build target uses: actions-rs/cargo@v1 @@ -165,16 +124,16 @@ jobs: if: matrix.os == 'windows-latest' run: | cd target/${{ matrix.target }}/release - strip castle-game.exe - 7z a ../../../${{ matrix.name }} castle-game.exe + strip castle_game.exe + mv castle_game.exe ../../../${{ matrix.name }} cd - - name: Prepare build artifacts [-nix] if: matrix.os != 'windows-latest' run: | cd target/${{ matrix.target }}/release - strip castle-game - tar czvf ../../../${{ matrix.name }} castle-game + strip castle_game + mv castle_game ../../../${{ matrix.name }} cd - - name: Upload build artifact @@ -196,26 +155,28 @@ jobs: - name: Download releases from github_build uses: actions/download-artifact@v1 with: - name: castle-game-x86_64-unknown-linux-gnu.tar.gz + name: castle_game-x86_64-unknown-linux-gnu path: . + - name: Download releases from github_build uses: actions/download-artifact@v1 with: - name: castle-game-x86_64-apple-darwin.tar.gz + name: castle_game-x86_64-apple-darwin path: . + - name: Download releases from github_build uses: actions/download-artifact@v1 with: - name: castle-game-x86_64-pc-windows-msvc.zip + name: castle_game-x86_64-pc-windows-msvc.exe path: . - name: Generate checksums - run: for file in castle-game-*; do openssl dgst -sha256 -r "$file" | awk '{print $1}' > "${file}.sha256"; done + run: for file in castle_game-*; do openssl dgst -sha256 -r "$file" | awk '{print $1}' > "${file}.sha256"; done - name: Create GitHub release ${{ matrix.target }} uses: softprops/action-gh-release@v1 with: files: | - castle-game-* + castle_game-* env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index ce83e8c..0f247a5 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,7 @@ Cargo.lock **/*.rs.bk resources/ -assets/ + +*.js +*.wasm +*.ts diff --git a/Cargo.toml b/Cargo.toml index 4c86b25..34b377b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "castle-game" version = "0.1.37-alpha.0" -edition = "2018" +edition = "2021" authors = ["Thomas Versteeg "] license = "GPL-3.0" homepage = "https://github.com/tversteeg/castle-game" @@ -13,37 +13,48 @@ repository = "https://github.com/tversteeg/castle-game.git" keywords = ["game", "2d", "destructible"] categories = ["games"] -build = "build.rs" - [badges] travis-ci = {repository = "tversteeg/castle-game"} is-it-maintained-issue-resolution = { repository = "tversteeg/castle-game" } +[features] +default = [] +inspector = ["bevy-inspector-egui"] + [dependencies] -cgmath = "0.17.0" -collision = "0.20.1" -const-tweaker = "0.3.1" -cpal = "0.11.0" -direct-gui = "0.1.25" -line_drawing = "0.8.0" -minifb = "0.19.0" -rand = "0.8.0" -sfxr = "0.1.4" -specs = { version = "0.16.1", features = ["shred-derive"] } -specs-derive = "0.4.1" - -[dependencies.rust-embed] -version = "5.5.1" -features = ["interpolate-folder-path"] - -[dependencies.blit] -version = "0.5.12" -default-features = false -features = ["aseprite"] - -[build-dependencies] -git2 = "0.13.6" -blit = "0.5.12" -image = "0.23.6" -aseprite = "0.1.3" -serde_json = "1.0.56" +anyhow = "1.0.56" +bevy = { version = "0.7.0", default-features = false, features = ["trace"] } +bevy-inspector-egui = { version = "0.10.0", optional = true } +bevy_easings = "0.6.0" +bevy_egui = "0.13.0" +bevy-inspector-egui-rapier = { version = "0.1.1", features = ["rapier2d"] } +bevy_rapier2d = { version = "0.12.1", features = ["simd-stable", "wasm-bindgen", "render"] } +earcutr = "0.2.0" +fastrand = "1.7.0" +geo = "0.20.0" +geo-booleanop = { git = "https://github.com/21re/rust-geo-booleanop.git" } +geo-types = "0.7.4" +itertools = "0.10.3" +lyon_path = "0.17.7" +lyon_tessellation = "0.17.10" +nalgebra = "0.30.1" +noise = { version = "0.7.0", default-features = false } +mock-inspector-derive = { version = "0.0.1", path = "mock-inspector-derive" } +rand = "0.8.5" +tracing-subscriber = "0.3.11" +usvg = { version = "0.22.0", default-features = false } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +tracing-wasm = "0.2.1" +console_error_panic_hook = "0.1.7" + +[workspace] +members = [".", "mock-inspector-derive"] + +# Don't make debug builds painfully slow +[profile.dev] +opt-level = 1 + +# Always run release versions of slow crates +[profile.dev.package."*"] +opt-level = 3 diff --git a/README.md b/README.md index 6839e74..fe5e72c 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,19 @@ To build the project you need to have [Rust](https://www.rustup.rs/) installed. You need to install [CMake](https://cmake.org/) and make sure it's in your path. +### WASM + +```sh +rustup target add wasm32-unknown-unknown +cargo install wasm-bindgen-cli + +cargo build --release --target wasm32-unknown-unknown +wasm-bindgen --out-name castle-game --out-dir web --target web target/wasm32-unknown-unknown/release/castle-game.wasm + +cargo install basic-http-server +(cd web && basic-http-server) +``` + ## Run Check out the repository with git and build: diff --git a/assets/fonts/Pixerif.ttf b/assets/fonts/Pixerif.ttf new file mode 100644 index 0000000..5ced489 Binary files /dev/null and b/assets/fonts/Pixerif.ttf differ diff --git a/assets/projectiles/arrow.svg b/assets/projectiles/arrow.svg new file mode 100644 index 0000000..57f90c9 --- /dev/null +++ b/assets/projectiles/arrow.svg @@ -0,0 +1,70 @@ + + + + + + + + + + + + diff --git a/assets/units/allies/character.svg b/assets/units/allies/character.svg new file mode 100644 index 0000000..b2ae40f --- /dev/null +++ b/assets/units/allies/character.svg @@ -0,0 +1,158 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/units/enemies/character.svg b/assets/units/enemies/character.svg new file mode 100644 index 0000000..457de5f --- /dev/null +++ b/assets/units/enemies/character.svg @@ -0,0 +1,158 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/weapons/bow.svg b/assets/weapons/bow.svg new file mode 100644 index 0000000..cdbc0d7 --- /dev/null +++ b/assets/weapons/bow.svg @@ -0,0 +1,52 @@ + + + + + + + + + diff --git a/assets/weapons/spear.svg b/assets/weapons/spear.svg new file mode 100644 index 0000000..7063c83 --- /dev/null +++ b/assets/weapons/spear.svg @@ -0,0 +1,48 @@ + + + + + + + diff --git a/build.rs b/build.rs deleted file mode 100644 index a831a04..0000000 --- a/build.rs +++ /dev/null @@ -1,105 +0,0 @@ -use blit::*; -use git2::Repository; -use std::env; -use std::fs; - -fn get_blit_buffer(path: &str, mask_color: u32) -> Option { - let img = image::open(path).unwrap(); - Some(blit_buffer(&img, Color::from_u32(mask_color))) -} - -fn save_blit_buffer_from_image( - assets_dir: &str, - folder: &str, - name: &str, - output: &str, - mask_color: u32, -) { - let path = format!("{}/{}/{}", assets_dir, folder, name); - - let blit_buf = get_blit_buffer(&path, mask_color).unwrap(); - blit_buf - .save(format!( - "{}/{}/{}.blit", - env::var("OUT_DIR").unwrap(), - folder, - output - )) - .unwrap(); -} - -fn save_anim_buffer(assets_dir: &str, folder: &str, name: &str, output: &str, mask_color: u32) { - let path = format!("{}/{}/{}", assets_dir, folder, name); - - // Open the spritesheet info - let file = fs::File::open(path).unwrap(); - let info: aseprite::SpritesheetData = serde_json::from_reader(file).unwrap(); - - let blit_buf = { - let image = info.meta.image.as_ref(); - - get_blit_buffer( - &format!("{}/{}", env::var("OUT_DIR").unwrap(), &image.unwrap()), - mask_color, - ) - .unwrap() - }; - let anim_buffer = AnimationBlitBuffer::new(blit_buf, info); - anim_buffer - .save(format!( - "{}/{}/{}.anim", - env::var("OUT_DIR").unwrap(), - folder, - output - )) - .unwrap(); -} - -fn parse_folder(assets_dir: &str, folder: &str, mask_color: u32) { - fs::create_dir_all(format!("{}/{}", env::var("OUT_DIR").unwrap(), folder)).unwrap(); - - let asset_paths = fs::read_dir(format!("{}/{}", assets_dir, folder)).unwrap(); - - for path in asset_paths { - let filepath = path.unwrap().path(); - let filename = filepath.file_name().unwrap(); - let filestem = filepath.file_stem().unwrap(); - let extension = filepath.extension().unwrap(); - - // Rerun the build script if any of the assets changed - println!("cargo:rerun-if-changed={:?}", filepath); - - match extension.to_str().unwrap() { - "png" => save_blit_buffer_from_image( - assets_dir, - folder, - filename.to_str().unwrap(), - filestem.to_str().unwrap(), - mask_color, - ), - "json" => save_anim_buffer( - assets_dir, - folder, - filename.to_str().unwrap(), - filestem.to_str().unwrap(), - mask_color, - ), - other => panic!("Filetype not recognized: {}", other), - } - } -} - -fn main() { - let assets_dir = format!("{}/assets", env::var("OUT_DIR").unwrap()); - if !std::path::Path::new(&assets_dir).exists() { - let url = "https://github.com/tversteeg/castle-game-assets.git"; - if let Err(e) = Repository::clone(url, &assets_dir) { - panic!("Failed to clone repository: {}", e); - } - } - - parse_folder(&assets_dir, "sprites", 0xFF_FF_00_FF); - parse_folder(&assets_dir, "masks", 0xFF_00_00_00); - - parse_folder(&assets_dir, "gui", 0xFF_FF_00_FF); -} diff --git a/mock-inspector-derive/Cargo.toml b/mock-inspector-derive/Cargo.toml new file mode 100644 index 0000000..c733a8e --- /dev/null +++ b/mock-inspector-derive/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "mock-inspector-derive" +version = "0.0.1" +edition = "2021" + +[lib] +proc-macro = true diff --git a/mock-inspector-derive/src/lib.rs b/mock-inspector-derive/src/lib.rs new file mode 100644 index 0000000..3bb78a5 --- /dev/null +++ b/mock-inspector-derive/src/lib.rs @@ -0,0 +1,9 @@ +use proc_macro::TokenStream; + +/// Add a proc macro for [`Inspectable`] that doesn't do anything. +/// +/// Used when the "inspector" feature flag is disabled. +#[proc_macro_derive(Inspectable, attributes(inspectable))] +pub fn derive_helper_attr(_item: TokenStream) -> TokenStream { + TokenStream::new() +} diff --git a/src/ai.rs b/src/ai.rs deleted file mode 100644 index dd8a64f..0000000 --- a/src/ai.rs +++ /dev/null @@ -1,162 +0,0 @@ -use collision::Discrete; -use specs::prelude::*; -use specs_derive::Component; - -use super::*; - -const BLOOD_COLOR: u32 = 0xAC_32_33; - -#[derive(Component, Debug, Copy, Clone)] -pub struct Destination(pub f64); - -#[derive(Component, Debug)] -pub struct Ally; - -#[derive(Component, Debug)] -pub struct Enemy; - -#[derive(Component, Debug)] -pub struct Melee { - dmg: f64, - hitrate: f64, - - cooldown: f64, -} - -impl Melee { - pub fn new(dmg: f64, hitrate: f64) -> Self { - Melee { - dmg, - hitrate, - - cooldown: 0.0, - } - } -} - -#[derive(SystemData)] -pub struct MeleeSystemData<'a> { - entities: Entities<'a>, - dt: Read<'a, DeltaTime>, - ally: ReadStorage<'a, Ally>, - enemy: ReadStorage<'a, Enemy>, - pos: ReadStorage<'a, WorldPosition>, - bb: ReadStorage<'a, BoundingBox>, - state: ReadStorage<'a, UnitState>, - melee: WriteStorage<'a, Melee>, - health: WriteStorage<'a, Health>, - updater: Read<'a, LazyUpdate>, -} - -pub struct MeleeSystem; -impl<'a> System<'a> for MeleeSystem { - type SystemData = MeleeSystemData<'a>; - - fn run(&mut self, mut system_data: Self::SystemData) { - let dt = system_data.dt.to_seconds(); - - for (a, _, a_pos, a_bb, a_state) in ( - &*system_data.entities, - &system_data.ally, - &system_data.pos, - &system_data.bb, - &system_data.state, - ) - .join() - { - // Only fight between units with the melee state - if *a_state != UnitState::Melee { - continue; - } - - let a_aabb = *a_bb + *a_pos.0; - for (e, _, e_pos, e_bb) in ( - &*system_data.entities, - &system_data.enemy, - &system_data.pos, - &system_data.bb, - ) - .join() - { - // Only fight between units with the melee state - if *a_state != UnitState::Melee { - continue; - } - - let e_aabb = *e_bb + *e_pos.0; - if a_aabb.intersects(&*e_aabb) { - { - let a_melee: Option<&mut Melee> = system_data.melee.get_mut(a); - if let Some(melee) = a_melee { - melee.cooldown -= dt; - if melee.cooldown <= 0.0 { - if reduce_unit_health( - &system_data.entities, - e, - system_data.health.get_mut(e).unwrap(), - melee.dmg, - ) { - // The enemy died - system_data.updater.insert( - system_data.entities.create(), - FloatingText { - text: "x".to_string(), - pos: e_pos.0, - time_alive: 2.0, - }, - ); - } - - melee.cooldown = melee.hitrate; - - let blood = system_data.entities.create(); - system_data - .updater - .insert(blood, PixelParticle::new(BLOOD_COLOR, 10.0)); - system_data.updater.insert(blood, *e_pos); - system_data - .updater - .insert(blood, Velocity::new(-10.0, -10.0)); - } - } - } - { - let e_melee: Option<&mut Melee> = system_data.melee.get_mut(e); - if let Some(melee) = e_melee { - melee.cooldown -= dt; - if melee.cooldown <= 0.0 { - if reduce_unit_health( - &system_data.entities, - a, - system_data.health.get_mut(a).unwrap(), - melee.dmg, - ) { - // The ally died - system_data.updater.insert( - system_data.entities.create(), - FloatingText { - text: "x".to_string(), - pos: a_pos.0, - time_alive: 2.0, - }, - ); - } - - melee.cooldown = melee.hitrate; - - let blood = system_data.entities.create(); - system_data - .updater - .insert(blood, PixelParticle::new(BLOOD_COLOR, 10.0)); - system_data.updater.insert(blood, *a_pos); - system_data - .updater - .insert(blood, Velocity::new(-10.0, -10.0)); - } - } - } - } - } - } - } -} diff --git a/src/audio.rs b/src/audio.rs deleted file mode 100644 index 0ef6a8b..0000000 --- a/src/audio.rs +++ /dev/null @@ -1,159 +0,0 @@ -use cpal::{ - traits::{EventLoopTrait, HostTrait}, - Format, SampleFormat, SampleRate, StreamData, UnknownTypeOutputBuffer, -}; -use sfxr::{Generator, Sample, WaveType}; -use std::{ - sync::{Arc, Mutex}, - thread, -}; - -#[const_tweaker::tweak(min = 0.0, max = 1.0, step = 0.001)] -const LIGHT_PROJECTILE_VOLUME: f32 = 0.25; -#[const_tweaker::tweak(min = 0.0, max = 1.0, step = 0.001)] -const LIGHT_PROJECTILE_BASE_FREQ: f64 = 0.12; -#[const_tweaker::tweak(min = 0.0, max = 1.0, step = 0.001)] -const LIGHT_PROJECTILE_ATTACK_DURATION: f32 = 0.01; -#[const_tweaker::tweak(min = 0.0, max = 1.0, step = 0.001)] -const LIGHT_PROJECTILE_SUSTAIN_DURATION: f32 = 0.005; -#[const_tweaker::tweak(min = 0.0, max = 1.0, step = 0.001)] -const LIGHT_PROJECTILE_DECAY_DURATION: f32 = 0.14; - -#[const_tweaker::tweak(min = 0.0, max = 1.0, step = 0.001)] -const HEAVY_PROJECTILE_VOLUME: f32 = 1.0; -#[const_tweaker::tweak(min = 0.0, max = 1.0, step = 0.001)] -const HEAVY_PROJECTILE_BASE_FREQ: f64 = 0.15; -#[const_tweaker::tweak(min = 0.0, max = 1.0, step = 0.001)] -const HEAVY_PROJECTILE_ATTACK_DURATION: f32 = 0.01; -#[const_tweaker::tweak(min = 0.0, max = 1.0, step = 0.001)] -const HEAVY_PROJECTILE_SUSTAIN_DURATION: f32 = 0.005; -#[const_tweaker::tweak(min = 0.0, max = 1.0, step = 0.001)] -const HEAVY_PROJECTILE_DECAY_DURATION: f32 = 0.14; - -#[const_tweaker::tweak(min = 0.0, max = 1.0, step = 0.001)] -const UNIT_HIT_VOLUME: f32 = 0.8; -#[const_tweaker::tweak(min = 0.0, max = 1.0, step = 0.001)] -const UNIT_HIT_BASE_FREQ: f64 = 0.12; -#[const_tweaker::tweak(min = 0.0, max = 1.0, step = 0.001)] -const UNIT_HIT_ATTACK_DURATION: f32 = 0.01; -#[const_tweaker::tweak(min = 0.0, max = 1.0, step = 0.001)] -const UNIT_HIT_SUSTAIN_DURATION: f32 = 0.005; -#[const_tweaker::tweak(min = 0.0, max = 1.0, step = 0.001)] -const UNIT_HIT_DECAY_DURATION: f32 = 0.14; - -/// Manages the audio. -#[derive(Default)] -pub struct Audio { - generator: Arc>>, -} - -impl Audio { - /// Instantiate a new audio object without a generator. - pub fn new() -> Self { - Self { - generator: Arc::new(Mutex::new(None)), - } - } - - /// Play a sound for a light projectile hitting the ground. - pub fn play_light_projectile(&self) { - let mut sample = Sample::new(); - - sample.wave_type = WaveType::Sine; - sample.base_freq = *LIGHT_PROJECTILE_BASE_FREQ; - sample.env_attack = *LIGHT_PROJECTILE_ATTACK_DURATION; - sample.env_sustain = *LIGHT_PROJECTILE_SUSTAIN_DURATION; - sample.env_decay = *LIGHT_PROJECTILE_DECAY_DURATION; - - self.play(sample, *LIGHT_PROJECTILE_VOLUME); - } - - /// Play a sound for a heavy projectile hitting the ground. - pub fn play_heavy_projectile(&self) { - let mut sample = Sample::new(); - - sample.wave_type = WaveType::Sine; - sample.base_freq = *HEAVY_PROJECTILE_BASE_FREQ; - sample.env_attack = *HEAVY_PROJECTILE_ATTACK_DURATION; - sample.env_sustain = *HEAVY_PROJECTILE_SUSTAIN_DURATION; - sample.env_decay = *HEAVY_PROJECTILE_DECAY_DURATION; - - self.play(sample, *HEAVY_PROJECTILE_VOLUME); - } - - /// Play a sound when a unit is hit. - pub fn play_unit_hit(&self) { - let mut sample = Sample::new(); - - sample.wave_type = WaveType::Sine; - sample.base_freq = *UNIT_HIT_BASE_FREQ; - sample.env_attack = *UNIT_HIT_ATTACK_DURATION; - sample.env_sustain = *UNIT_HIT_SUSTAIN_DURATION; - sample.env_decay = *UNIT_HIT_DECAY_DURATION; - - self.play(sample, *UNIT_HIT_VOLUME); - } - - /// Play a sample. - pub fn play(&self, sample: Sample, volume: f32) { - let mut new_generator = Generator::new(sample); - new_generator.volume = volume; - - let mut generator = self.generator.lock().unwrap(); - *generator = Some(new_generator); - } - - /// Start a thread which will emit the audio. - pub fn run(&mut self) { - let generator = self.generator.clone(); - - thread::spawn(|| { - // Setup the audio system - let host = cpal::default_host(); - let event_loop = host.event_loop(); - - let device = host - .default_output_device() - .expect("no output device available"); - - // This is the only format sfxr supports - let format = Format { - channels: 1, - sample_rate: SampleRate(44_100), - data_type: SampleFormat::F32, - }; - - let stream_id = event_loop - .build_output_stream(&device, &format) - .expect("could not build output stream"); - - event_loop - .play_stream(stream_id) - .expect("could not play stream"); - - event_loop.run(move |stream_id, stream_result| { - let stream_data = match stream_result { - Ok(data) => data, - Err(err) => { - eprintln!("an error occurred on stream {:?}: {:?}", stream_id, err); - return; - } - }; - - match stream_data { - StreamData::Output { - buffer: UnknownTypeOutputBuffer::F32(mut buffer), - } => match *generator.lock().unwrap() { - Some(ref mut generator) => generator.generate(&mut buffer), - None => { - for elem in buffer.iter_mut() { - *elem = 0.0; - } - } - }, - _ => panic!("output type buffer can not be used"), - } - }); - }); - } -} diff --git a/src/camera.rs b/src/camera.rs new file mode 100644 index 0000000..5883009 --- /dev/null +++ b/src/camera.rs @@ -0,0 +1,79 @@ +use bevy::{ + core::Name, + math::Vec3, + prelude::{ + App, Commands, Component, EventReader, GlobalTransform, OrthographicCameraBundle, Plugin, + Query, Res, Transform, UiCameraBundle, With, + }, + render::camera::WindowOrigin, + window::{CursorMoved, Windows}, +}; + +use crate::constants::Constants; + +/// The plugin to handle camera movements. +pub struct CameraPlugin; + +impl Plugin for CameraPlugin { + fn build(&self, app: &mut App) { + app.add_startup_system(setup); + app.add_system(system); + } +} + +/// The component we can attach to the camera. +#[derive(Component)] +pub struct Camera; + +/// Initial setup for the camera. +fn setup(mut commands: Commands, constants: Res) { + // Setup the cameras + let mut camera = OrthographicCameraBundle::new_2d(); + + camera.transform = Transform { + scale: Vec3::splat(constants.camera.scale), + ..Default::default() + }; + + camera.orthographic_projection.window_origin = WindowOrigin::BottomLeft; + + // Draw everything with z-index 0.0..100.0 + camera.orthographic_projection.near = -1000.0; + + commands + .spawn_bundle(camera) + .insert(Camera) + .insert(Name::new("Camera")); + commands + .spawn_bundle(UiCameraBundle::default()) + .insert(Name::new("UI Camera")); +} + +/// The system for following the mouse with the camera. +pub fn system( + mut events: EventReader, + windows: Res, + mut query: Query<&mut GlobalTransform, With>, + constants: Res, +) { + events.iter().for_each(|event| { + // The camera should always be in the query + let mut transform = query.iter_mut().next().unwrap(); + + // Get the window size so we can calculate the max camera position + let window_size = windows.get(event.id).unwrap(); + + // The maximum position of the camera to the right + let max_position = constants.terrain.width - window_size.width() * constants.camera.scale; + + // The position of the mouse as a fraction + // Keep a zone on the edges in which moving the mouse won't move the camera + let mouse_x = ((event.position.x / window_size.width()) + * (1.0 + constants.camera.border_size * 2.0) + - constants.camera.border_size) + .clamp(0.0, 1.0); + + // Position the camera at the mouse + transform.translation = Vec3::new(mouse_x * max_position, 0.0, 0.0); + }); +} diff --git a/src/color.rs b/src/color.rs new file mode 100644 index 0000000..3957636 --- /dev/null +++ b/src/color.rs @@ -0,0 +1,296 @@ +//! Define the colors using the DB32 color pallete. +//! [https://lospec.com/palette-list/dawnbringer-32] +//! +//! Converting colors in NeoVim: +//! +//! ```vim +//! :%s#.*#\=printf("\t/// `%s`.\n\tC%d,", submatch(0), line('.') / 2 + 1) +//! ``` +//! +//! ```lua +//! :luado hex = line:gsub("#",""); return string.format("Palette::C%d => Color::Rgba {red: %f, green: %f, blue: %f, alpha:1.0},", linenr, tonumber("0x"..hex:sub(1,2))/255, tonumber("0x"..hex:sub(3,4))/255, tonumber("0x"..hex:sub(5,6))/255) +//! ``` + +use bevy::prelude::Color; +use bevy_egui::egui::Color32; + +pub enum Palette { + /// `#000000`. + C1, + /// `#222034`. + C2, + /// `#45283c`. + C3, + /// `#663931`. + C4, + /// `#8f563b`. + C5, + /// `#df7126`. + C6, + /// `#d9a066`. + C7, + /// `#eec39a`. + C8, + /// `#fbf236`. + C9, + /// `#99e550`. + C10, + /// `#6abe30`. + C11, + /// `#37946e`. + C12, + /// `#4b692f`. + C13, + /// `#524b24`. + C14, + /// `#323c39`. + C15, + /// `#3f3f74`. + C16, + /// `#306082`. + C17, + /// `#5b6ee1`. + C18, + /// `#639bff`. + C19, + /// `#5fcde4`. + C20, + /// `#cbdbfc`. + C21, + /// `#ffffff`. + C22, + /// `#9badb7`. + C23, + /// `#847e87`. + C24, + /// `#696a6a`. + C25, + /// `#595652`. + C26, + /// `#76428a`. + C27, + /// `#ac3232`. + C28, + /// `#d95763`. + C29, + /// `#d77bba`. + C30, + /// `#8f974a`. + C31, + /// `#8a6f30`. + C32, +} + +impl From for Color { + /// Create a bevy color. + fn from(color: Palette) -> Color { + match color { + Palette::C1 => Color::Rgba { + red: 0.000000, + green: 0.000000, + blue: 0.000000, + alpha: 1.0, + }, + Palette::C2 => Color::Rgba { + red: 0.133333, + green: 0.125490, + blue: 0.203922, + alpha: 1.0, + }, + Palette::C3 => Color::Rgba { + red: 0.270588, + green: 0.156863, + blue: 0.235294, + alpha: 1.0, + }, + Palette::C4 => Color::Rgba { + red: 0.400000, + green: 0.223529, + blue: 0.192157, + alpha: 1.0, + }, + Palette::C5 => Color::Rgba { + red: 0.560784, + green: 0.337255, + blue: 0.231373, + alpha: 1.0, + }, + Palette::C6 => Color::Rgba { + red: 0.874510, + green: 0.443137, + blue: 0.149020, + alpha: 1.0, + }, + Palette::C7 => Color::Rgba { + red: 0.850980, + green: 0.627451, + blue: 0.400000, + alpha: 1.0, + }, + Palette::C8 => Color::Rgba { + red: 0.933333, + green: 0.764706, + blue: 0.603922, + alpha: 1.0, + }, + Palette::C9 => Color::Rgba { + red: 0.984314, + green: 0.949020, + blue: 0.211765, + alpha: 1.0, + }, + Palette::C10 => Color::Rgba { + red: 0.600000, + green: 0.898039, + blue: 0.313725, + alpha: 1.0, + }, + Palette::C11 => Color::Rgba { + red: 0.415686, + green: 0.745098, + blue: 0.188235, + alpha: 1.0, + }, + Palette::C12 => Color::Rgba { + red: 0.215686, + green: 0.580392, + blue: 0.431373, + alpha: 1.0, + }, + Palette::C13 => Color::Rgba { + red: 0.294118, + green: 0.411765, + blue: 0.184314, + alpha: 1.0, + }, + Palette::C14 => Color::Rgba { + red: 0.321569, + green: 0.294118, + blue: 0.141176, + alpha: 1.0, + }, + Palette::C15 => Color::Rgba { + red: 0.196078, + green: 0.235294, + blue: 0.223529, + alpha: 1.0, + }, + Palette::C16 => Color::Rgba { + red: 0.247059, + green: 0.247059, + blue: 0.454902, + alpha: 1.0, + }, + Palette::C17 => Color::Rgba { + red: 0.188235, + green: 0.376471, + blue: 0.509804, + alpha: 1.0, + }, + Palette::C18 => Color::Rgba { + red: 0.356863, + green: 0.431373, + blue: 0.882353, + alpha: 1.0, + }, + Palette::C19 => Color::Rgba { + red: 0.388235, + green: 0.607843, + blue: 1.000000, + alpha: 1.0, + }, + Palette::C20 => Color::Rgba { + red: 0.372549, + green: 0.803922, + blue: 0.894118, + alpha: 1.0, + }, + Palette::C21 => Color::Rgba { + red: 0.796078, + green: 0.858824, + blue: 0.988235, + alpha: 1.0, + }, + Palette::C22 => Color::Rgba { + red: 1.000000, + green: 1.000000, + blue: 1.000000, + alpha: 1.0, + }, + Palette::C23 => Color::Rgba { + red: 0.607843, + green: 0.678431, + blue: 0.717647, + alpha: 1.0, + }, + Palette::C24 => Color::Rgba { + red: 0.517647, + green: 0.494118, + blue: 0.529412, + alpha: 1.0, + }, + Palette::C25 => Color::Rgba { + red: 0.411765, + green: 0.415686, + blue: 0.415686, + alpha: 1.0, + }, + Palette::C26 => Color::Rgba { + red: 0.349020, + green: 0.337255, + blue: 0.321569, + alpha: 1.0, + }, + Palette::C27 => Color::Rgba { + red: 0.462745, + green: 0.258824, + blue: 0.541176, + alpha: 1.0, + }, + Palette::C28 => Color::Rgba { + red: 0.674510, + green: 0.196078, + blue: 0.196078, + alpha: 1.0, + }, + Palette::C29 => Color::Rgba { + red: 0.850980, + green: 0.341176, + blue: 0.388235, + alpha: 1.0, + }, + Palette::C30 => Color::Rgba { + red: 0.843137, + green: 0.482353, + blue: 0.729412, + alpha: 1.0, + }, + Palette::C31 => Color::Rgba { + red: 0.560784, + green: 0.592157, + blue: 0.290196, + alpha: 1.0, + }, + Palette::C32 => Color::Rgba { + red: 0.541176, + green: 0.435294, + blue: 0.188235, + alpha: 1.0, + }, + } + } +} + +impl From for Color32 { + fn from(color: Palette) -> Color32 { + let bevy_color: Color = color.into(); + let rgba = bevy_color.as_rgba_f32(); + + Color32::from_rgba_unmultiplied( + (rgba[0] * 255.0) as u8, + (rgba[1] * 255.0) as u8, + (rgba[2] * 255.0) as u8, + (rgba[3] * 255.0) as u8, + ) + } +} diff --git a/src/constants.rs b/src/constants.rs new file mode 100644 index 0000000..10b1d4c --- /dev/null +++ b/src/constants.rs @@ -0,0 +1,260 @@ +use crate::{ + inspector::Inspectable, + random::RandomRange, + unit::{faction::Faction, unit_type::UnitType}, +}; +use bevy::math::Vec2; + +/// The constants. +/// +/// Not made actually constant so it can be changed in the inspector. +#[derive(Debug, Inspectable)] +pub struct Constants { + /// World constants. + #[inspectable(label = "World", collapse)] + pub world: WorldConstants, + /// Camera constants. + #[inspectable(label = "Camera", collapse)] + pub camera: CameraConstants, + /// Terrain constants. + #[inspectable(label = "Terrain", collapse)] + pub terrain: TerrainConstants, + /// Enemy spawning constants. + #[inspectable(label = "Spawning", collapse)] + pub spawning: SpawningConstants, + /// Ally soldier constants. + #[inspectable(label = "Ally Soldier", collapse)] + pub ally_soldier: UnitConstants, + /// Enemy soldier constants. + #[inspectable(label = "Enemy Soldier", collapse)] + pub enemy_soldier: UnitConstants, + /// Ally archer constants. + #[inspectable(label = "Ally Archer", collapse)] + pub ally_archer: UnitConstants, + /// Enemy archer constants. + #[inspectable(label = "Enemy Archer", collapse)] + pub enemy_archer: UnitConstants, + /// Arrow constants. + #[inspectable(label = "Arrow", collapse)] + pub arrow: ProjectileConstants, + /// UI constants. + #[inspectable(label = "UI", collapse)] + pub ui: UiConstants, +} + +impl Constants { + /// Get the unit constants. + pub fn unit(&'_ self, unit_type: UnitType, faction: Faction) -> &'_ UnitConstants { + match (unit_type, faction) { + (UnitType::Soldier, Faction::Ally) => &self.ally_soldier, + (UnitType::Soldier, Faction::Enemy) => &self.enemy_soldier, + (UnitType::Archer, Faction::Ally) => &self.ally_archer, + (UnitType::Archer, Faction::Enemy) => &self.enemy_archer, + } + } +} + +impl Default for Constants { + fn default() -> Self { + Self { + ally_soldier: UnitConstants { + hp: 100.0, + walking_speed: 1.5, + minimum_weapon_distance: 2.0, + weapon_delay: 1.0, + stop_distance: 2.0, + }, + enemy_soldier: UnitConstants { + hp: 100.0, + walking_speed: -1.7, + minimum_weapon_distance: 2.0, + weapon_delay: 1.0, + stop_distance: 2.0, + }, + ally_archer: UnitConstants { + hp: 100.0, + walking_speed: 1.2, + minimum_weapon_distance: 100.0, + weapon_delay: 5.0, + stop_distance: 50.0, + }, + enemy_archer: UnitConstants { + hp: 100.0, + walking_speed: -1.3, + minimum_weapon_distance: 100.0, + weapon_delay: 5.0, + stop_distance: 50.0, + }, + arrow: ProjectileConstants { + remove_after_resting_for: 0.5, + flight_time: RandomRange { min: 4.0, max: 6.0 }, + rotation_offset: -std::f32::consts::PI / 2.0, + damage: 20.0, + min_velocity_for_damage: 1.0, + }, + spawning: SpawningConstants::default(), + terrain: TerrainConstants::default(), + camera: CameraConstants::default(), + world: WorldConstants::default(), + ui: UiConstants::default(), + } + } +} + +/// Constants for a specific unit. +#[derive(Debug, Clone, Inspectable)] +pub struct UnitConstants { + /// Health. + #[inspectable(min = 1.0, max = 1000.0)] + pub hp: f32, + /// Walking speed. + #[inspectable(min = -100.0, max = 100.0, suffix = "m/s")] + pub walking_speed: f32, + /// The minimum distance at which the weapon will be used. + #[inspectable(min = 0.0, max = 1000.0, suffix = "m")] + pub minimum_weapon_distance: f32, + /// The amount of seconds at which the weapon will be used. + #[inspectable(min = 0.2, max = 1000.0, suffix = "s")] + pub weapon_delay: f32, + /// Distance at which to stop before the next unit. + #[inspectable(min = 0.2, max = 1000.0, suffix = "m")] + pub stop_distance: f32, +} + +/// Constants for the terrain. +#[derive(Debug, Clone, Inspectable)] +pub struct TerrainConstants { + /// Total width of the terrain. + #[inspectable(min = 10.0, max = 1000.0, suffix = "m")] + pub width: f32, + /// How many height points should be calculated for the terrain. + pub height_points: usize, + /// Random height of the terrain between the bounds. + #[inspectable(min = 1.0, max = 50.0, suffix = "m")] + pub height: RandomRange, + /// The scale of the noise, will influence which X points will be get as sample. + #[inspectable(min = 0.0, max = 1.0)] + pub noise_scale: f64, + /// Where allies are spawned. + #[inspectable(min = 0.0, max = 1000.0, suffix = "m")] + pub ally_starting_position: f32, + /// Where enemies are spawned. + #[inspectable(min = 0.0, max = 1000.0, suffix = "m")] + pub enemy_starting_position: f32, +} + +impl Default for TerrainConstants { + fn default() -> Self { + let width = 200.0; + + Self { + width, + height_points: 100, + height: RandomRange { + min: 6.0, + max: 14.0, + }, + noise_scale: 0.01, + ally_starting_position: 20.0, + enemy_starting_position: width - 20.0, + } + } +} + +/// Constants for the camera. +#[derive(Debug, Clone, Copy, Inspectable)] +pub struct CameraConstants { + /// How far the camera is zoomed in. + pub scale: f32, + /// Camera border on the each on which it won't move. + pub border_size: f32, +} + +impl Default for CameraConstants { + fn default() -> Self { + Self { + scale: 1.0 / 10.0, + border_size: 0.2, + } + } +} + +/// Constants for projectiles. +#[derive(Debug, Clone, Copy, Inspectable)] +pub struct ProjectileConstants { + /// How long until an arrow is removed when laying on the ground. + #[inspectable(min = 0.0, max = 1000.0, suffix = "s")] + pub remove_after_resting_for: f32, + /// Seconds until the arrow will hit the target. + #[inspectable(min = 0.0, max = 1000.0, suffix = "s")] + pub flight_time: RandomRange, + /// How much the rotation of the arrow will be offset. + #[inspectable(min = -std::f32::consts::PI, max = std::f32::consts::PI, suffix = "r")] + pub rotation_offset: f32, + /// How much damage this does on collision. + #[inspectable(min = 0.0, max = 10000.0, suffix = "hp")] + pub damage: f32, + /// The minimum velocity to inflict damage. + #[inspectable(min = 0.0, max = 10000.0, suffix = "hp")] + pub min_velocity_for_damage: f32, +} + +/// Constants for the world. +#[derive(Debug, Clone, Copy, Inspectable)] +pub struct WorldConstants { + /// How fast objects fall in m/s. + #[inspectable(min = -1000.0, max = 1000.0, suffix = "m/s")] + pub gravity: f32, +} + +impl Default for WorldConstants { + fn default() -> Self { + Self { gravity: -9.81 } + } +} + +/// Constants for the spawning of enemies and allies. +#[derive(Debug, Clone, Copy, Inspectable)] +pub struct SpawningConstants { + /// When the recruit button for an allied soldier is available again. + #[inspectable(min = 0.1, max = 1000.0, suffix = "s")] + pub ally_soldier_interval: f32, + /// When the recruit button for an allied archer is available again. + #[inspectable(min = 0.1, max = 1000.0, suffix = "s")] + pub ally_archer_interval: f32, + /// How long it takes for an enemy soldier to spawn. + #[inspectable(min = 0.1, max = 1000.0, suffix = "s")] + pub enemy_soldier_interval: f32, + /// How long it takes for an enemy archer to spawn. + #[inspectable(min = 0.1, max = 1000.0, suffix = "s")] + pub enemy_archer_interval: f32, +} + +impl Default for SpawningConstants { + fn default() -> Self { + Self { + ally_soldier_interval: 5.0, + ally_archer_interval: 10.0, + enemy_soldier_interval: 10.0, + enemy_archer_interval: 21.0, + } + } +} + +/// Constants for the UI. +#[derive(Debug, Clone, Copy, Inspectable)] +pub struct UiConstants { + /// How far the main bar is removed from the top half of the screen. + pub main_bar_offset: Vec2, + /// The size of the recruit button and the progress bar. + pub recruit_button_size: Vec2, +} + +impl Default for UiConstants { + fn default() -> Self { + Self { + main_bar_offset: [0.0, 5.0].into(), + recruit_button_size: [80.0, 20.0].into(), + } + } +} diff --git a/src/draw.rs b/src/draw.rs deleted file mode 100644 index 09b75df..0000000 --- a/src/draw.rs +++ /dev/null @@ -1,305 +0,0 @@ -use blit::*; -use cgmath::Point2; -use line_drawing::Bresenham; -use specs::*; -use specs_derive::Component; -use std::collections::HashMap; -use std::error::Error; -use std::time::Duration; - -use crate::geom::*; -use crate::terrain::*; - -const GREEN_BAR_COLOR: u32 = 0xFF_6A_BE_30; -const RED_BAR_COLOR: u32 = 0xFF_AC_32_33; - -#[derive(Component, Debug, Copy, Clone)] -pub struct PixelParticle { - pub color: u32, - pub life: f64, - - pub pos: Point2, -} - -impl PixelParticle { - pub fn new(color: u32, life: f64) -> Self { - PixelParticle { - color, - life, - pos: Point2::new(0, 0), - } - } -} - -#[derive(Component, Debug, Copy, Clone)] -pub struct MaskId { - pub id: usize, - pub size: (usize, usize), -} - -#[derive(Component, Debug, Copy, Clone)] -pub struct Sprite { - pub pos: Point, - img_ref: usize, -} - -impl Sprite { - pub fn new(img_ref: usize) -> Self { - Sprite { - img_ref, - pos: Point::new(0.0, 0.0), - } - } - - pub fn img_ref(&self) -> usize { - self.img_ref - } -} - -#[derive(Component, Debug, Copy, Clone)] -pub struct Anim { - pub pos: Point, - pub info: Animation, - img_ref: usize, -} - -impl Anim { - pub fn new(img_ref: usize, info: Animation) -> Self { - Anim { - img_ref, - info, - pos: Point::new(0.0, 0.0), - } - } - - pub fn img_ref(&self) -> usize { - self.img_ref - } -} - -#[derive(Component, Debug, Copy, Clone)] -pub struct Line { - pub p1: Point2, - pub p2: Point2, - pub color: u32, -} - -impl Line { - pub fn new(color: u32) -> Self { - Line { - color, - p1: Point2 { x: 0, y: 0 }, - p2: Point2 { x: 0, y: 0 }, - } - } -} - -pub struct Images(pub HashMap); - -pub struct SpriteSystem; -impl<'a> System<'a> for SpriteSystem { - type SystemData = (ReadStorage<'a, WorldPosition>, WriteStorage<'a, Sprite>); - - fn run(&mut self, (pos, mut sprite): Self::SystemData) { - for (pos, sprite) in (&pos, &mut sprite).join() { - sprite.pos = pos.0; - } - } -} - -pub struct AnimSystem; -impl<'a> System<'a> for AnimSystem { - type SystemData = (ReadStorage<'a, WorldPosition>, WriteStorage<'a, Anim>); - - fn run(&mut self, (pos, mut anim): Self::SystemData) { - for (pos, anim) in (&pos, &mut anim).join() { - anim.pos = pos.0; - } - } -} - -pub struct Render { - background: Vec, - - blit_buffers: Vec<(String, BlitBuffer)>, - anim_buffers: Vec<(String, AnimationBlitBuffer)>, - - width: usize, - height: usize, -} - -impl Render { - pub fn new(size: (usize, usize)) -> Self { - Render { - background: vec![0; (size.0 * size.1) as usize], - - width: size.0, - height: size.1, - - blit_buffers: Vec::new(), - anim_buffers: Vec::new(), - } - } - - pub fn draw_terrain_and_background(&mut self, buffer: &mut Vec, terrain: &Terrain) { - for (output, (bg, terrain)) in buffer - .iter_mut() - .zip(self.background.iter().zip(&terrain.buffer)) - { - if (*terrain & 0xFF_FF_FF) != 0xFF_00_FF { - // The terrain doesn't needs to be cleared - *output = *terrain; - continue; - } - *output = *bg; - } - } - - pub fn draw_healthbar( - &mut self, - buffer: &mut Vec, - pos: Point2, - health_ratio: f64, - width: usize, - ) { - if pos.x >= self.width || pos.y >= self.height { - return; - } - - let y = pos.y * self.width; - - let width = if pos.x + width >= self.width { - self.width - pos.x - } else { - width - }; - let health = pos.x + (health_ratio * width as f64) as usize; - - // Draw the green bar - for x in pos.x..health { - buffer[x + y] = GREEN_BAR_COLOR; - } - - // Draw the red bar - let max = pos.x + width; - for x in health..max { - buffer[x + y] = RED_BAR_COLOR; - } - } - - pub fn draw_foreground( - &mut self, - buffer: &mut Vec, - sprite: &Sprite, - ) -> Result<(), Box> { - let buf = &self.blit_buffers[sprite.img_ref()].1; - - let size = self.size(); - buf.blit(buffer, size.0, sprite.pos.as_i32()); - - Ok(()) - } - - pub fn draw_foreground_anim( - &mut self, - buffer: &mut Vec, - anim: &Anim, - ) -> Result<(), Box> { - let buf = &self.anim_buffers[anim.img_ref()].1; - - let size = self.size(); - buf.blit(buffer, size.0, anim.pos.as_i32(), &anim.info)?; - - Ok(()) - } - - pub fn draw_foreground_pixel(&mut self, buffer: &mut Vec, pos: Point2, color: u32) { - if pos.x >= self.width || pos.y >= self.height { - return; - } - - buffer[pos.x + pos.y * self.width] = color; - } - - pub fn draw_foreground_line( - &mut self, - buffer: &mut Vec, - p1: Point2, - p2: Point2, - color: u32, - ) { - if p2.y >= self.height || p1.x >= self.width && p2.x >= self.width { - return; - } - - for (x, y) in Bresenham::new((p1.x as i32, p1.y as i32), (p2.x as i32, p2.y as i32)) { - if x >= self.width as i32 || y >= self.height as i32 { - continue; - } - - buffer[x as usize + y as usize * self.width] = color; - } - } - - pub fn draw_mask_terrain( - &mut self, - terrain: &mut Terrain, - mask: &TerrainMask, - ) -> Result<(), Box> { - let buf = &self.blit_buffers[mask.id].1; - - // Center the mask - let mut pos = mask.pos; - pos.0 -= buf.size().0 / 2; - pos.1 -= buf.size().1 / 2; - - let size = self.size(); - buf.blit(&mut terrain.buffer, size.0, pos); - - Ok(()) - } - - pub fn draw_terrain_from_memory(&mut self, terrain: &mut Terrain, bytes: &[u8]) { - let buf = BlitBuffer::from_memory(bytes).unwrap(); - - let size = self.size(); - buf.blit(&mut terrain.buffer, size.0, (0, 0)); - } - - pub fn draw_background_from_memory(&mut self, bytes: &[u8]) { - let buf = BlitBuffer::from_memory(bytes).unwrap(); - - let size = self.size(); - buf.blit(&mut self.background, size.0, (0, 0)); - } - - /// Update the animation with the buffer, this is needed here because the timings are described - /// inside the AnimationBlitBuffer object. - pub fn update_anim(&self, anim: &mut Anim, dt: Duration) -> Result<(), Box> { - let buf = &self.anim_buffers[anim.img_ref()].1; - - anim.info.update(buf, dt)?; - - Ok(()) - } - - pub fn size(&self) -> (usize, usize) { - (self.width, self.height) - } - - pub fn add_buf_from_memory(&mut self, name: &str, bytes: &[u8]) -> usize { - let buf = BlitBuffer::from_memory(bytes).unwrap(); - - self.blit_buffers.push((String::from(name), buf)); - - self.blit_buffers.len() - 1 - } - - pub fn add_anim_buf_from_memory(&mut self, name: &str, bytes: &[u8]) -> usize { - let buf = AnimationBlitBuffer::from_memory(bytes).unwrap(); - - self.anim_buffers.push((String::from(name), buf)); - - self.anim_buffers.len() - 1 - } -} diff --git a/src/draw/colored_mesh.rs b/src/draw/colored_mesh.rs new file mode 100644 index 0000000..7772df4 --- /dev/null +++ b/src/draw/colored_mesh.rs @@ -0,0 +1,265 @@ +use crate::inspector::Inspectable; +use bevy::{ + core::FloatOrd, + core_pipeline::Transparent2d, + prelude::{ + App, Assets, Bundle, Commands, Component, ComputedVisibility, Entity, FromWorld, + GlobalTransform, Handle, HandleUntyped, Local, Mesh, Msaa, Plugin, Query, Res, ResMut, + Shader, Transform, Visibility, With, World, + }, + reflect::TypeUuid, + render::{ + render_asset::RenderAssets, + render_phase::{AddRenderCommand, DrawFunctions, RenderPhase, SetItemPipeline}, + render_resource::{ + BlendState, ColorTargetState, ColorWrites, FragmentState, FrontFace, MultisampleState, + PolygonMode, PrimitiveState, RenderPipelineCache, RenderPipelineDescriptor, + SpecializedPipeline, SpecializedPipelines, TextureFormat, VertexAttribute, + VertexBufferLayout, VertexFormat, VertexState, VertexStepMode, + }, + texture::BevyDefault, + view::VisibleEntities, + RenderApp, RenderStage, + }, + sprite::{ + DrawMesh2d, Mesh2dHandle, Mesh2dPipeline, Mesh2dPipelineKey, Mesh2dUniform, + SetMesh2dBindGroup, SetMesh2dViewBindGroup, + }, +}; + +use crate::geometry::transform::TransformBuilder; + +/// Handle to the custom shader with a unique random ID. +pub const COLORED_MESH_SHADER_HANDLE: HandleUntyped = + HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 13518715925425351754); + +/// A marker component for colored 2d meshes. +#[derive(Debug, Default, Component, Inspectable)] +pub struct ColoredMesh; + +/// Bundle for easy construction of colored meshes. +#[derive(Default, Bundle, Inspectable)] +pub struct ColoredMeshBundle { + pub colored_mesh: ColoredMesh, + #[inspectable(ignore)] + pub handle: Mesh2dHandle, + pub transform: Transform, + pub global_transform: GlobalTransform, + #[inspectable(ignore)] + pub visibility: Visibility, + #[inspectable(ignore)] + pub computed_visibility: ComputedVisibility, +} + +impl ColoredMeshBundle { + /// Create a new bundle. + pub fn new(mesh: Handle) -> Self { + Self { + handle: Mesh2dHandle(mesh), + transform: Transform::from_xyz(0.0, 0.0, 0.0), + ..Default::default() + } + } +} + +impl TransformBuilder for ColoredMeshBundle { + fn transform_mut_ref(&'_ mut self) -> &'_ mut Transform { + &mut self.transform + } +} + +/// Custom pipeline for 2d meshes with vertex colors. +pub struct ColoredMeshPipeline { + /// This pipeline wraps the standard [`Mesh2dPipeline`]. + mesh2d_pipeline: Mesh2dPipeline, +} + +impl FromWorld for ColoredMeshPipeline { + fn from_world(world: &mut World) -> Self { + Self { + mesh2d_pipeline: Mesh2dPipeline::from_world(world), + } + } +} + +// We implement `SpecializedPipeline` to customize the default rendering from `Mesh2dPipeline`. +impl SpecializedPipeline for ColoredMeshPipeline { + type Key = Mesh2dPipelineKey; + + fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor { + // Customize how to store the meshes' vertex attributes in the vertex buffer + // Our meshes only have position and color + let vertex_attributes = vec![ + // Position (GOTCHA! Vertex_Position isn't first in the buffer due to how Mesh sorts attributes (alphabetically)) + VertexAttribute { + format: VertexFormat::Float32x3, + // this offset is the size of the color attribute, which is stored first + offset: 16, + // position is available at location 0 in the shader + shader_location: 0, + }, + // Color + VertexAttribute { + format: VertexFormat::Float32x4, + offset: 0, + shader_location: 1, + }, + ]; + // This is the sum of the size of position and color attributes (12 + 16 = 28) + let vertex_array_stride = 28; + + RenderPipelineDescriptor { + vertex: VertexState { + // Use our custom shader + shader: COLORED_MESH_SHADER_HANDLE.typed::(), + entry_point: "vertex".into(), + shader_defs: Vec::new(), + // Use our custom vertex buffer + buffers: vec![VertexBufferLayout { + array_stride: vertex_array_stride, + step_mode: VertexStepMode::Vertex, + attributes: vertex_attributes, + }], + }, + fragment: Some(FragmentState { + // Use our custom shader + shader: COLORED_MESH_SHADER_HANDLE.typed::(), + shader_defs: Vec::new(), + entry_point: "fragment".into(), + targets: vec![ColorTargetState { + format: TextureFormat::bevy_default(), + blend: Some(BlendState::ALPHA_BLENDING), + write_mask: ColorWrites::ALL, + }], + }), + // Use the two standard uniforms for 2d meshes + layout: Some(vec![ + // Bind group 0 is the view uniform + self.mesh2d_pipeline.view_layout.clone(), + // Bind group 1 is the mesh uniform + self.mesh2d_pipeline.mesh_layout.clone(), + ]), + primitive: PrimitiveState { + front_face: FrontFace::Cw, + cull_mode: None, + unclipped_depth: false, + polygon_mode: PolygonMode::Fill, + conservative: false, + topology: key.primitive_topology(), + strip_index_format: None, + }, + depth_stencil: None, + multisample: MultisampleState { + count: key.msaa_samples(), + mask: !0, + alpha_to_coverage_enabled: false, + }, + label: Some("colored_mesh_pipeline".into()), + } + } +} + +/// Specify how to render a colored 2d mesh. +type DrawColoredMesh = ( + // Set the pipeline + SetItemPipeline, + // Set the view uniform as bind group 0 + SetMesh2dViewBindGroup<0>, + // Set the mesh uniform as bind group 1 + SetMesh2dBindGroup<1>, + // Draw the mesh + DrawMesh2d, +); + +/// Plugin that renders [`ColoredMesh`]s. +pub struct ColoredMeshPlugin; + +impl Plugin for ColoredMeshPlugin { + fn build(&self, app: &mut App) { + // Load our custom shader + let mut shaders = app.world.get_resource_mut::>().unwrap(); + shaders.set_untracked( + COLORED_MESH_SHADER_HANDLE, + Shader::from_wgsl(include_str!("colored_mesh.wgsl")), + ); + + // Register our custom draw function and pipeline, and add our render systems + let render_app = app.get_sub_app_mut(RenderApp).unwrap(); + render_app + .add_render_command::() + .init_resource::() + .init_resource::>() + .add_system_to_stage(RenderStage::Extract, extract_colored_mesh) + .add_system_to_stage(RenderStage::Queue, queue_colored_mesh); + } +} + +/// Extract the [`ColoredMesh`] marker component into the render app +pub fn extract_colored_mesh( + mut commands: Commands, + mut previous_len: Local, + query: Query<(Entity, &ComputedVisibility), With>, +) { + let mut values = Vec::with_capacity(*previous_len); + for (entity, computed_visibility) in query.iter() { + if !computed_visibility.is_visible { + continue; + } + values.push((entity, (ColoredMesh,))); + } + *previous_len = values.len(); + commands.insert_or_spawn_batch(values); +} + +/// Queue the 2d meshes marked with [`ColoredMesh`] using our custom pipeline and draw function. +#[allow(clippy::too_many_arguments)] +pub fn queue_colored_mesh( + transparent_draw_functions: Res>, + colored_mesh_pipeline: Res, + mut pipelines: ResMut>, + mut pipeline_cache: ResMut, + msaa: Res, + render_meshes: Res>, + colored_mesh: Query<(&Mesh2dHandle, &Mesh2dUniform), With>, + mut views: Query<(&VisibleEntities, &mut RenderPhase)>, +) { + if colored_mesh.is_empty() { + return; + } + // Iterate each view (a camera is a view) + for (visible_entities, mut transparent_phase) in views.iter_mut() { + let draw_colored_mesh = transparent_draw_functions + .read() + .get_id::() + .unwrap(); + + let mesh_key = Mesh2dPipelineKey::from_msaa_samples(msaa.samples); + + // Queue all entities visible to that view + for visible_entity in &visible_entities.entities { + if let Ok((mesh2d_handle, mesh2d_uniform)) = colored_mesh.get(*visible_entity) { + // Get our specialized pipeline + let mut mesh2d_key = mesh_key; + if let Some(mesh) = render_meshes.get(&mesh2d_handle.0) { + mesh2d_key |= + Mesh2dPipelineKey::from_primitive_topology(mesh.primitive_topology); + } + + let pipeline_id = + pipelines.specialize(&mut pipeline_cache, &colored_mesh_pipeline, mesh2d_key); + + let mesh_z = mesh2d_uniform.transform.w_axis.z; + transparent_phase.add(Transparent2d { + entity: *visible_entity, + draw_function: draw_colored_mesh, + pipeline: pipeline_id, + // The 2d render items are sorted according to their z value before rendering, + // in order to get correct transparency + sort_key: FloatOrd(mesh_z), + // This material is not batched + batch_range: None, + }); + } + } + } +} diff --git a/src/draw/colored_mesh.wgsl b/src/draw/colored_mesh.wgsl new file mode 100644 index 0000000..18ced47 --- /dev/null +++ b/src/draw/colored_mesh.wgsl @@ -0,0 +1,44 @@ +// Import the standard 2d mesh uniforms and set their bind groups +#import bevy_sprite::mesh2d_view_bind_group +[[group(0), binding(0)]] +var view: View; + +#import bevy_sprite::mesh2d_struct +[[group(1), binding(0)]] +var mesh: Mesh2d; + +// The structure of the vertex buffer is as specified in `specialize()` +struct Vertex { + [[location(0)]] position: vec3; + [[location(1)]] color: vec4; +}; + +struct VertexOutput { + // The vertex shader must set the on-screen position of the vertex + [[builtin(position)]] clip_position: vec4; + // We pass the vertex color to the framgent shader in location 0 + [[location(0)]] color: vec4; +}; + +/// Entry point for the vertex shader. +[[stage(vertex)]] +fn vertex(vertex: Vertex) -> VertexOutput { + var out: VertexOutput; + // Project the world position of the mesh into screen position + out.clip_position = view.view_proj * mesh.model * vec4(vertex.position, 1.0); + out.color = vertex.color; + + return out; +} + +// The input of the fragment shader must correspond to the output of the vertex shader for all `location`s +struct FragmentInput { + // The color is interpolated between vertices by default + [[location(0)]] color: vec4; +}; + +/// Entry point for the fragment shader. +[[stage(fragment)]] +fn fragment(in: FragmentInput) -> [[location(0)]] vec4 { + return in.color; +} diff --git a/src/draw/mesh.rs b/src/draw/mesh.rs new file mode 100644 index 0000000..19cd9da --- /dev/null +++ b/src/draw/mesh.rs @@ -0,0 +1,257 @@ +use bevy::{ + prelude::Mesh, + render::{mesh::Indices, render_resource::PrimitiveTopology}, +}; + +use lyon_tessellation::math::Transform; +use lyon_tessellation::{ + path::PathEvent, BuffersBuilder, FillOptions, FillTessellator, FillVertex, + FillVertexConstructor, StrokeOptions, StrokeTessellator, StrokeVertex, StrokeVertexConstructor, + VertexBuffers, +}; + +/// Convert a geo polygon to a mesh. +pub trait ToMesh { + /// Get the vertices, indices and colors. + fn buffers(&self) -> (Vec<[f32; 3]>, Vec, Vec<[f32; 4]>); + + /// Convert the object to a mesh. + fn to_mesh(&self) -> Mesh { + bevy::log::trace!("Creating mesh"); + + let (vertices, indices, colors) = self.buffers(); + let triangles = indices.len() / 3; + + // Create the mesh + let mut mesh = Mesh::new(PrimitiveTopology::TriangleList); + + // Set the indices + mesh.set_indices(Some(Indices::U32(indices))); + + // Set the vertices + mesh.set_attribute(Mesh::ATTRIBUTE_POSITION, vertices); + + // Set the colors + mesh.set_attribute(Mesh::ATTRIBUTE_COLOR, colors); + + bevy::log::debug!("Mesh created with {triangles} triangles"); + + mesh + } +} + +/// Buffers for creating a mesh. +#[derive(Debug, Default)] +pub struct MeshBuffers { + vertices: Vec<[f32; 3]>, + indices: Vec, + colors: Vec<[f32; 4]>, +} + +impl MeshBuffers { + /// Construct a new buffers object with empty buffers. + pub fn new() -> Self { + Self::default() + } + + /// Convert a path fill to vertex and index buffers. + pub fn append_fill( + &mut self, + path: impl IntoIterator, + transform: Transform, + color: C, + ) where + C: Into<[f32; 4]>, + { + bevy::log::trace!("Converting path fill to vertex buffers"); + + // The resulting vertex and index buffers + let mut buffers: VertexBuffers<[f32; 3], u32> = VertexBuffers::new(); + + // Use our custom vertex constructor to create a bevy vertex buffer + let mut vertex_builder = + BuffersBuilder::new(&mut buffers, BevyVertexConstructor { transform }); + + // Tesselate the fill + let mut tessellator = FillTessellator::new(); + let result = tessellator.tessellate(path, &FillOptions::default(), &mut vertex_builder); + assert!(result.is_ok()); + + self.merge_buffers(buffers, color.into()); + } + + /// Convert a path stroke to vertex and index buffers. + pub fn append_stroke( + &mut self, + path: impl IntoIterator, + stroke_options: &StrokeOptions, + transform: Transform, + color: C, + ) where + C: Into<[f32; 4]>, + { + bevy::log::trace!("Converting path stroke to vertex buffers"); + + // The resulting vertex and index buffers + let mut buffers: VertexBuffers<[f32; 3], u32> = VertexBuffers::new(); + + // Use our custom vertex constructor to create a bevy vertex buffer + let mut vertex_builder = + BuffersBuilder::new(&mut buffers, BevyVertexConstructor { transform }); + + // Tesselate the fill + let mut tessellator = StrokeTessellator::new(); + let result = tessellator.tessellate(path, stroke_options, &mut vertex_builder); + assert!(result.is_ok()); + + self.merge_buffers(buffers, color.into()); + } + + /// Merge the buffers. + fn merge_buffers(&mut self, mut buffers: VertexBuffers<[f32; 3], u32>, color: [f32; 4]) { + // Add the offset so multiple items can be merged + let indices_offset = self.vertices.len() as u32; + if indices_offset != 0 { + buffers + .indices + .iter_mut() + .for_each(|index| *index += indices_offset); + } + + self.vertices.append(&mut buffers.vertices); + self.indices.append(&mut buffers.indices); + + // Fill the buffer with the same size as the vertices with colors + self.colors.resize(self.vertices.len(), color); + } +} + +impl ToMesh for MeshBuffers { + fn buffers(&self) -> (Vec<[f32; 3]>, Vec, Vec<[f32; 4]>) { + ( + self.vertices.clone(), + self.indices.clone(), + self.colors.clone(), + ) + } +} + +/// A custom vertex constructor for lyon, creates bevy vertices. +struct BevyVertexConstructor { + /// The transform to apply to all vertices. + transform: Transform, +} + +impl FillVertexConstructor<[f32; 3]> for BevyVertexConstructor { + fn new_vertex(&mut self, vertex: FillVertex) -> [f32; 3] { + // Transform the 2D point + let transformed = self.transform.transform_point(vertex.position()); + + [transformed.x, transformed.y, 0.0] + } +} + +impl StrokeVertexConstructor<[f32; 3]> for BevyVertexConstructor { + fn new_vertex(&mut self, vertex: StrokeVertex) -> [f32; 3] { + // Transform the 2D point + let transformed = self.transform.transform_point(vertex.position()); + + [transformed.x, transformed.y, 0.0] + } +} + +#[cfg(feature = "inspector")] +pub mod inspector { + use bevy::{ + prelude::{Assets, Mesh}, + render::mesh::{Indices, VertexAttributeValues}, + sprite::Mesh2dHandle, + }; + use bevy_inspector_egui::{ + egui::{ + plot::{Legend, Plot, Polygon, Value, Values}, + Color32, Grid, Ui, Vec2, + }, + Context, + }; + + /// Draw the inspectable view for a bevy [`Mesh`]. + pub fn mesh_inspectable(handle: &mut Mesh2dHandle, ui: &mut Ui, context: &mut Context) -> bool { + if let Some(mesh) = context + .world() + .map(|world| world.get_resource::>()) + .flatten() + .map(|meshes| meshes.get(&handle.0)) + .flatten() + { + let indices = mesh.indices(); + let vertices = mesh.attribute(Mesh::ATTRIBUTE_POSITION); + let colors = mesh.attribute(Mesh::ATTRIBUTE_COLOR); + + if let Some(((indices, vertices), colors)) = indices.zip(vertices).zip(colors) { + // Convert the mesh into colored triangles + if let Indices::U32(indices) = indices { + if let VertexAttributeValues::Float32x3(vertices) = vertices { + if let VertexAttributeValues::Float32x4(colors) = colors { + // Convert the mesh data into plot data + let vertices_and_colors = indices + .iter() + .map(|index| { + let pos = vertices[*index as usize]; + let color = colors[*index as usize]; + ( + Value::new(pos[0], pos[1]), + Color32::from_rgba_unmultiplied( + (color[0] * 255.0) as u8, + (color[1] * 255.0) as u8, + (color[2] * 255.0) as u8, + (color[3] * 255.0) as u8, + ), + ) + }) + .collect::>(); + + // Draw a grid with all the triangles + Grid::new(context.id()).show(ui, |ui| { + let plot = Plot::new("triangles") + .legend(Legend::default()) + .data_aspect(0.8) + .min_size(Vec2::new(250.0, 250.0)) + .show_x(true) + .show_y(true); + plot.show(ui, |plot_ui| { + vertices_and_colors.chunks_exact(3).for_each(|triangle| { + plot_ui.polygon( + Polygon::new(Values::from_values_iter( + triangle + .iter() + .map(|vertex_and_color| vertex_and_color.0), + )) + // Add thicker strokes and reduce the fill + // transparency + .highlight(true) + .color(triangle[0].1) + // Use the color as the name so everything + // with the same color will be grouped + .name( + format!( + "#{:02X?}{:02X?}{:02X?}{:02X?}", + triangle[0].1.r(), + triangle[0].1.g(), + triangle[0].1.b(), + triangle[0].1.a() + ), + ), + ); + }); + }); + }); + } + } + } + } + } + + true + } +} diff --git a/src/draw/mod.rs b/src/draw/mod.rs new file mode 100644 index 0000000..4221456 --- /dev/null +++ b/src/draw/mod.rs @@ -0,0 +1,19 @@ +pub mod colored_mesh; +pub mod mesh; +pub mod svg; + +use self::colored_mesh::ColoredMeshPlugin; +use self::svg::SvgAssetLoader; +use bevy::prelude::{AddAsset, App, Msaa, Plugin}; + +/// The plugin to manage rendering. +pub struct DrawPlugin; + +impl Plugin for DrawPlugin { + fn build(&self, app: &mut App) { + // Smooth anti aliasing + app.insert_resource(Msaa { samples: 4 }) + .init_asset_loader::() + .add_plugin(ColoredMeshPlugin); + } +} diff --git a/src/draw/svg.rs b/src/draw/svg.rs new file mode 100644 index 0000000..0e33ec1 --- /dev/null +++ b/src/draw/svg.rs @@ -0,0 +1,260 @@ +use anyhow::{Context, Error}; +use bevy::asset::{AssetLoader, BoxedFuture, LoadContext, LoadedAsset}; +use bevy::prelude::{Color, Mesh}; + +use lyon_tessellation::geom::euclid::default::Transform2D; +use lyon_tessellation::{ + math::Point, path::PathEvent, FillVertex, FillVertexConstructor, LineCap, LineJoin, + StrokeOptions, StrokeVertex, StrokeVertexConstructor, +}; +use std::slice::Iter; +use usvg::{NodeKind, Options, Paint, Path, PathSegment, Transform, Tree}; + +use crate::draw::mesh::{MeshBuffers, ToMesh}; +use crate::geometry::polygon::STROKE_TOLERANCE; + +/// Bevy SVG asset loader. +#[derive(Debug, Default)] +pub struct SvgAssetLoader; + +impl AssetLoader for SvgAssetLoader { + fn load<'a>( + &'a self, + bytes: &'a [u8], + load_context: &'a mut LoadContext, + ) -> BoxedFuture<'a, Result<(), Error>> { + Box::pin(async move { + bevy::log::debug!("Loading SVG {:?}", load_context.path()); + + // Parse and simplify the SVG file + let svg_tree = Tree::from_data(bytes, &Options::default().to_ref()) + .with_context(|| format!("Could not parse SVG file {:?}", load_context.path()))?; + + // Generate the mesh + let mesh = svg_to_mesh(&svg_tree); + + // Upload the mesh + load_context.set_default_asset(LoadedAsset::new(mesh)); + + bevy::log::trace!("SVG loaded"); + + Ok(()) + }) + } + + fn extensions(&self) -> &[&str] { + &["svg"] + } +} + +/// A custom vertex constructor for lyon, creates bevy vertices. +struct BevyVertexConstructor { + /// The transform to apply to all vertices. + transform: Transform, +} + +impl FillVertexConstructor<[f32; 3]> for BevyVertexConstructor { + fn new_vertex(&mut self, vertex: FillVertex) -> [f32; 3] { + let pos = vertex.position(); + + // Transform the 2D point + // TODO: remove ugly casts + let (x, y) = self.transform.apply(pos.x as f64, pos.y as f64); + + [x as f32, y as f32, 0.0] + } +} + +impl StrokeVertexConstructor<[f32; 3]> for BevyVertexConstructor { + fn new_vertex(&mut self, vertex: StrokeVertex) -> [f32; 3] { + let pos = vertex.position(); + + // Transform the 2D point + // TODO: remove ugly casts + let (x, y) = self.transform.apply(pos.x as f64, pos.y as f64); + + [x as f32, y as f32, 0.0] + } +} + +// Taken from https://github.com/nical/lyon/blob/74e6b137fea70d71d3b537babae22c6652f8843e/examples/wgpu_svg/src/main.rs +struct PathConvIter<'a> { + iter: Iter<'a, PathSegment>, + prev: Point, + first: Point, + needs_end: bool, + deferred: Option, +} + +impl<'a> PathConvIter<'a> { + /// Convert a SVG path to the iterator for the tessellator. + pub fn from_svg_path(path: &'a Path) -> Self { + Self { + iter: path.data.0.iter(), + first: Point::zero(), + prev: Point::zero(), + deferred: None, + needs_end: false, + } + } +} + +impl<'l> Iterator for PathConvIter<'l> { + type Item = PathEvent; + + fn next(&mut self) -> Option { + if self.deferred.is_some() { + return self.deferred.take(); + } + + let next = self.iter.next(); + match next { + Some(PathSegment::MoveTo { x, y }) => { + if self.needs_end { + let last = self.prev; + let first = self.first; + self.needs_end = false; + self.prev = Point::new((*x) as f32, -(*y) as f32); + self.deferred = Some(PathEvent::Begin { at: self.prev }); + self.first = self.prev; + + Some(PathEvent::End { + last, + first, + close: false, + }) + } else { + self.first = Point::new((*x) as f32, -(*y) as f32); + + Some(PathEvent::Begin { at: self.first }) + } + } + Some(PathSegment::LineTo { x, y }) => { + self.needs_end = true; + let from = self.prev; + self.prev = Point::new((*x) as f32, -(*y) as f32); + + Some(PathEvent::Line { + from, + to: self.prev, + }) + } + Some(PathSegment::CurveTo { + x1, + y1, + x2, + y2, + x, + y, + }) => { + self.needs_end = true; + let from = self.prev; + self.prev = Point::new((*x) as f32, -(*y) as f32); + + Some(PathEvent::Cubic { + from, + ctrl1: Point::new((*x1) as f32, -(*y1) as f32), + ctrl2: Point::new((*x2) as f32, -(*y2) as f32), + to: self.prev, + }) + } + Some(PathSegment::ClosePath) => { + self.needs_end = false; + self.prev = self.first; + Some(PathEvent::End { + last: self.prev, + first: self.first, + close: true, + }) + } + None => { + if self.needs_end { + self.needs_end = false; + let last = self.prev; + let first = self.first; + Some(PathEvent::End { + last, + first, + close: false, + }) + } else { + None + } + } + } + } +} + +/// Convert a SVG to a mesh. +fn svg_to_mesh(svg: &Tree) -> Mesh { + bevy::log::trace!("Converting SVG paths to mesh"); + + // The resulting vertex and index buffers + let mut buffers = MeshBuffers::new(); + + for node in svg.root().descendants() { + if let NodeKind::Path(ref path) = *node.borrow() { + bevy::log::trace!("Parsing SVG path node"); + + // Convert the fill to a polygon + if let Some(ref fill) = path.fill { + buffers.append_fill( + PathConvIter::from_svg_path(path), + svg_transform_to_lyon(&path.transform), + svg_color_to_bevy(&fill.paint, fill.opacity.to_u8()), + ); + } + + // Convert the stroke to a polygon + if let Some(ref stroke) = path.stroke { + // Convert the usvg stroke options to lyon stroke options + let linecap = match stroke.linecap { + usvg::LineCap::Butt => LineCap::Butt, + usvg::LineCap::Round => LineCap::Round, + usvg::LineCap::Square => LineCap::Square, + }; + let linejoin = match stroke.linejoin { + usvg::LineJoin::Miter => LineJoin::Miter, + usvg::LineJoin::Round => LineJoin::Round, + usvg::LineJoin::Bevel => LineJoin::Bevel, + }; + + let stroke_options = StrokeOptions::tolerance(STROKE_TOLERANCE) + .with_line_width(stroke.width.value() as f32) + .with_line_cap(linecap) + .with_line_join(linejoin); + + buffers.append_stroke( + PathConvIter::from_svg_path(path), + &stroke_options, + svg_transform_to_lyon(&path.transform), + svg_color_to_bevy(&stroke.paint, stroke.opacity.to_u8()), + ); + } + } + } + + buffers.to_mesh() +} + +/// Convert an SVG color to a Bevy color. +fn svg_color_to_bevy(paint: &Paint, opacity: u8) -> [f32; 4] { + match paint { + Paint::Color(color) => Color::rgba_u8(color.red, color.green, color.blue, opacity), + // We only support plain colors + _ => Color::default(), + } + .as_linear_rgba_f32() +} + +/// Convert an SVG transform to a Lyon transform. +fn svg_transform_to_lyon(transform: &Transform) -> Transform2D { + Transform2D::new( + transform.a as f32, + transform.b as f32, + transform.c as f32, + transform.d as f32, + transform.e as f32, + transform.f as f32, + ) +} diff --git a/src/geom.rs b/src/geom.rs deleted file mode 100644 index ddbee24..0000000 --- a/src/geom.rs +++ /dev/null @@ -1,114 +0,0 @@ -use cgmath::{EuclideanSpace, Point2}; -use collision::Aabb2; -use specs::{Component, VecStorage}; -use specs_derive::Component; -use std::ops::{Add, Deref, DerefMut}; - -#[derive(Component, Debug, Copy, Clone)] -#[storage(VecStorage)] -pub struct WorldPosition(pub Point); - -#[derive(Component, Debug, Copy, Clone)] -#[storage(VecStorage)] -pub struct Point(pub Point2); - -impl Point { - pub fn new(x: f64, y: f64) -> Self { - Point(Point2::new(x, y)) - } - - pub fn as_i32(self) -> (i32, i32) { - (self.0.x as i32, self.0.y as i32) - } - - pub fn as_usize(self) -> Point2 { - Point2::new(self.0.x as usize, self.0.y as usize) - } -} - -impl Deref for Point { - type Target = Point2; - - fn deref(&self) -> &Point2 { - &self.0 - } -} - -impl DerefMut for Point { - fn deref_mut(&mut self) -> &mut Point2 { - &mut self.0 - } -} - -#[derive(Component, Debug, Copy, Clone)] -#[storage(VecStorage)] -pub struct BoundingBox(Aabb2); - -impl BoundingBox { - pub fn new(p1: Point, p2: Point) -> Self { - BoundingBox(Aabb2::new(*p1, *p2)) - } - - pub fn to_i32(self) -> (i32, i32, i32, i32) { - ( - self.min.x as i32, - self.min.y as i32, - (self.max.x - self.min.x) as i32, - (self.max.y - self.min.y) as i32, - ) - } - - pub fn width(self) -> f64 { - self.max.x - self.min.x - } - - pub fn height(self) -> f64 { - self.max.y - self.min.y - } - - pub fn to_half_width(self) -> BoundingBox { - let quart_width = self.width() / 4.0; - - let mut copy = self; - copy.min.x += quart_width; - copy.max.x -= quart_width; - - copy - } -} - -impl Deref for BoundingBox { - type Target = Aabb2; - - fn deref(&self) -> &Aabb2 { - &self.0 - } -} - -impl DerefMut for BoundingBox { - fn deref_mut(&mut self) -> &mut Aabb2 { - &mut self.0 - } -} - -impl Add> for BoundingBox { - type Output = Self; - - fn add(self, pos: Point2) -> Self { - BoundingBox::new( - Point(self.min + pos.to_vec()), - Point(self.max + pos.to_vec()), - ) - } -} - -impl Add for BoundingBox { - type Output = Self; - - fn add(self, pos: Point) -> Self { - BoundingBox::new( - Point(self.min + pos.to_vec()), - Point(self.max + pos.to_vec()), - ) - } -} diff --git a/src/geometry/breakable.rs b/src/geometry/breakable.rs new file mode 100644 index 0000000..a7fb77c --- /dev/null +++ b/src/geometry/breakable.rs @@ -0,0 +1,60 @@ +use crate::inspector::Inspectable; +use bevy::prelude::{Component, Entity, EventReader, EventWriter, Query}; +use bevy_rapier2d::{ + physics::IntoEntity, + prelude::{ContactEvent, RigidBodyVelocityComponent}, +}; + +/// Allow a polygon to break into multiple pieces when force is applied. +#[derive(Component, Inspectable)] +pub struct Breakable { + /// The velocity on impact on which it breaks. + impact_velocity: f32, +} + +impl Default for Breakable { + fn default() -> Self { + Self { + impact_velocity: 3.0, + } + } +} + +/// The event that's fired when an entity needs to break. +#[derive(Debug)] +pub struct BreakEvent { + /// The speed with which the collision occurs. + pub impact_velocity: f32, + /// The entity which collides. + pub entity: Entity, +} + +/// Check collision events for when enough force is applied. +pub fn system( + mut events: EventReader, + query: Query<(Entity, &RigidBodyVelocityComponent, &Breakable)>, + mut event_writer: EventWriter, +) { + for event in events.iter() { + if let ContactEvent::Started(collision_object_1, collision_object_2) = event { + // Try to get the breakable entity from both sides of the collision + if let Ok((entity, velocity, breakable)) = query + .get(collision_object_1.entity()) + .or_else(|_| query.get(collision_object_2.entity())) + { + // "Calculate" the impact velocity + // TODO: use better velocity calculation + let impact_velocity = -velocity.0.linvel.y; + + // Check the velocity to see if we need to split it + if impact_velocity >= breakable.impact_velocity { + // We need to split the object, trigger an event + event_writer.send(BreakEvent { + impact_velocity, + entity, + }); + } + } + } + } +} diff --git a/src/geometry/mod.rs b/src/geometry/mod.rs new file mode 100644 index 0000000..5a38c6a --- /dev/null +++ b/src/geometry/mod.rs @@ -0,0 +1,30 @@ +pub mod breakable; +pub mod polygon; +pub mod split; +pub mod transform; + +use self::{ + breakable::{BreakEvent, Breakable}, + polygon::{Polygon, PolygonShapeBundle}, +}; +use crate::inspector::RegisterInspectable; +use bevy::prelude::{App, ParallelSystemDescriptorCoercion, Plugin, SystemLabel}; + +/// For prioritizing systems in relation to our systems. +#[derive(Debug, Clone, Hash, PartialEq, Eq, SystemLabel)] +pub enum GeometrySystem { + BreakEvent, +} + +/// The plugin to register geometry types. +pub struct GeometryPlugin; + +impl Plugin for GeometryPlugin { + fn build(&self, app: &mut App) { + app.register_inspectable::() + .register_inspectable::() + .register_inspectable::() + .add_event::() + .add_system(breakable::system.label(GeometrySystem::BreakEvent)); + } +} diff --git a/src/geometry/polygon.rs b/src/geometry/polygon.rs new file mode 100644 index 0000000..24cd68b --- /dev/null +++ b/src/geometry/polygon.rs @@ -0,0 +1,256 @@ +use super::transform::TransformBuilder; +use crate::{ + draw::{ + colored_mesh::ColoredMeshBundle, + mesh::{MeshBuffers, ToMesh}, + }, + inspector::Inspectable, +}; +use bevy::{ + prelude::{Assets, Bundle, Color, Component, Mesh}, + utils::tracing, +}; +use bevy_rapier2d::prelude::ColliderShape; +use geo::{prelude::IsConvex, LineString, Polygon as GeoPolygon}; +use lyon_path::{geom::euclid::Point2D, math::Transform, Path}; +use lyon_tessellation::{LineCap, StrokeOptions}; +use std::ops::{Deref, DerefMut}; + +/// Lyon tolerance for generating a mesh from the stroke. +pub const STROKE_TOLERANCE: f32 = 0.1; + +/// Convert a geo polygon to a collision shape. +pub trait ToColliderShape { + /// Convert the polygon to a collision shape by taking the outline. + fn to_collider_shape(&self) -> ColliderShape; +} + +/// Mark the entity as a polygon. +#[derive(Debug, Clone, Component)] +pub struct Polygon { + /// The color to render the fill. + pub fill: Option, + /// The color to render the stroke and the size. + pub stroke: Option<(Color, f32)>, + /// The polygon geometry. + polygon: GeoPolygon, +} + +impl Polygon { + /// Construct a new polygon. + pub fn new(exterior: LineString, interiors: Vec>) -> Self { + Self { + polygon: GeoPolygon::new(exterior, interiors), + fill: None, + stroke: None, + } + } + + /// Triangulate the polygon. + /// + /// Uses earcutr instead of lyon, generates better collider meshes. + pub fn triangulate(&self) -> (Vec, Vec) { + // Convert the polygon points to coordinates + let coordinates = self + .exterior() + .points() + .map(|point| vec![point.x(), point.y()]) + .collect::>>(); + + // Convert the coordinates to indices and holes + let (vertices, hole_indices, dimensions) = earcutr::flatten(&vec![coordinates]); + + // Triangulate the polygon + let indices = earcutr::earcut(&vertices, &hole_indices, dimensions); + + (vertices, indices) + } +} + +impl From> for Polygon { + fn from(polygon: GeoPolygon) -> Self { + Self { + polygon, + fill: None, + stroke: None, + } + } +} + +impl From<&Polygon> for Path { + /// Build a lyon path from the polygon. + fn from(polygon: &Polygon) -> Self { + let mut builder = Path::builder(); + + let mut exterior = polygon.exterior().points(); + + // Get the first to begin + let first = exterior.next().expect("Polygon is empty"); + builder.begin(Point2D::new(first.x(), first.y())); + + for point in exterior { + builder.line_to(Point2D::new(point.x(), point.y())); + } + + builder.end(true); + + builder.build() + } +} + +impl Deref for Polygon { + type Target = GeoPolygon; + + fn deref(&self) -> &Self::Target { + &self.polygon + } +} + +impl DerefMut for Polygon { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.polygon + } +} + +impl ToMesh for Polygon { + fn buffers(&self) -> (Vec<[f32; 3]>, Vec, Vec<[f32; 4]>) { + let mut buffers = MeshBuffers::new(); + + // Add the fill first, so the stroke will be placed on top over it + if let Some(fill_color) = self.fill { + buffers.append_fill( + Path::from(self).into_iter(), + Transform::default(), + fill_color, + ); + } + + // Add the stroke + if let Some((stroke_color, stroke_size)) = self.stroke { + let stroke_options = StrokeOptions::tolerance(STROKE_TOLERANCE) + .with_line_width(stroke_size) + .with_line_cap(LineCap::Square); + + buffers.append_stroke( + Path::from(self).into_iter(), + &stroke_options, + Transform::default(), + stroke_color, + ); + } + + buffers.buffers() + } +} + +impl ToColliderShape for Polygon { + #[tracing::instrument(name = "converting polygon to collision shape")] + fn to_collider_shape(&self) -> ColliderShape { + // If the polygon is convex just create a convex hull for it, which is better performance-wise + if self.exterior().is_convex() { + // Convert the polygon points to coordinates + let points = self + .exterior() + .points() + .map(|point| nalgebra::point![point.x(), point.y()]) + .collect::>(); + + // TODO: handle error + ColliderShape::convex_hull(&points).unwrap() + } else { + // Triangulate the polygon first with a simple mesh without a stroke + let (vertices, indices) = self.triangulate(); + + // Convert the vertices to rapier vertices + assert!(vertices.len() % 2 == 0); + let vertices = vertices + .chunks_exact(2) + .map(|xy| nalgebra::point![xy[0], xy[1]]) + .collect::>(); + + // Convert the indices to rapier indices + assert!(indices.len() % 3 == 0); + let indices = indices + .chunks_exact(3) + .map(|indices| [indices[0] as u32, indices[1] as u32, indices[2] as u32]) + .collect::>(); + + ColliderShape::trimesh(vertices, indices) + } + } +} + +/// The polygon component filled with a single color. +#[derive(Bundle, Inspectable)] +pub struct PolygonShapeBundle { + /// The polygon. + polygon: Polygon, + /// The mesh and the material. + #[bundle] + #[inspectable(ignore)] + mesh: ColoredMeshBundle, +} + +impl PolygonShapeBundle { + /// Construct a new polygon with a single color and position. + pub fn new( + mut polygon: Polygon, + fill: Option, + stroke: Option<(Color, f32)>, + meshes: &mut Assets, + ) -> Self { + polygon.fill = fill; + polygon.stroke = stroke; + + let mesh = ColoredMeshBundle::new(meshes.add(polygon.to_mesh())); + + Self { mesh, polygon } + } +} + +impl TransformBuilder for PolygonShapeBundle { + fn transform_mut_ref(&'_ mut self) -> &'_ mut bevy::prelude::Transform { + self.mesh.transform_mut_ref() + } +} + +#[cfg(feature = "inspector")] +mod inspector { + use super::*; + use bevy_inspector_egui::{ + egui::{ + plot::{Legend, Line, Plot, Value, Values}, + Grid, Ui, + }, + Context, Inspectable, + }; + + impl Inspectable for Polygon { + type Attributes = (); + + fn ui(&mut self, ui: &mut Ui, _: Self::Attributes, context: &mut Context) -> bool { + Grid::new(context.id()).show(ui, |ui| { + // Plot the polygon + let plot = Plot::new("polygon") + .legend(Legend::default()) + .data_aspect(0.8) + .min_size(bevy_inspector_egui::egui::Vec2::new(250.0, 250.0)) + .show_x(true) + .show_y(true); + plot.show(ui, |plot_ui| { + // TODO: plot interior + plot_ui.line( + Line::new(Values::from_values_iter( + self.exterior() + .coords() + .map(|coord| Value::new(coord.x, coord.y)), + )) + .name("exterior"), + ) + }); + }); + + false + } + } +} diff --git a/src/geometry/split.rs b/src/geometry/split.rs new file mode 100644 index 0000000..859853e --- /dev/null +++ b/src/geometry/split.rs @@ -0,0 +1,60 @@ +use bevy::utils::tracing; +use geo::{prelude::BoundingRect, LineString}; +use geo_booleanop::boolean::BooleanOp; +use geo_types::{Coordinate, Polygon, Rect}; +use rand::Rng; + +/// Split a polygon into multiple parts. +pub trait Split { + // TODO: https://github.com/rust-lang/rust/issues/63063 + // type Iter: Iterator; + + /// Split the polygon into multiple parts by creating a random shape and using boolean + /// operations. + fn split(&self) -> Box>; +} + +impl Split> for Polygon { + // TODO: https://github.com/rust-lang/rust/issues/63063 + // type Iter = impl Iterator; + + #[tracing::instrument(name = "splitting polygon", level = "info")] + fn split(&self) -> Box> { + // Setup the random generator + let mut rng = rand::thread_rng(); + + // Create a random polygon shape through the center + let bounding_rect = self + .bounding_rect() + // Use a small rectangle when the bounding rectangle can't be calculated, this shouldn't + // happen much + .unwrap_or_else(|| { + Rect::new(Coordinate { x: 0.0, y: 0.0 }, Coordinate { x: 1.0, y: 1.0 }) + }); + + // Add a new point to the at a random point + let min = bounding_rect.min(); + let max = bounding_rect.max(); + + // Create the random polygon + let random_shape = Polygon::new( + LineString::from(vec![ + ( + rng.gen_range::(min.x..max.x), + rng.gen_range::(min.y..max.y), + ), + (max.x, min.y), + (max.x, max.y), + (min.x, max.y), + ]), + vec![], + ); + + // Get both sides of the object through boolean operations + let side1 = self.intersection(&random_shape); + let side2 = self.difference(&random_shape); + + // Get all polygons from both splits + Box::new(side1.into_iter().chain(side2.into_iter())) + } +} diff --git a/src/geometry/transform.rs b/src/geometry/transform.rs new file mode 100644 index 0000000..1cf08b5 --- /dev/null +++ b/src/geometry/transform.rs @@ -0,0 +1,40 @@ +use bevy::{math::Quat, prelude::Transform}; + +/// Add transformation builder functions to a type. +pub trait TransformBuilder { + /// Implementation for types for getting mutable access to the transform. + fn transform_mut_ref(&'_ mut self) -> &'_ mut Transform; + + /// Set the X & Y position for the mesh. + fn with_position(mut self, x: f32, y: f32) -> Self + where + Self: Sized, + { + self.transform_mut_ref().translation.x = x; + self.transform_mut_ref().translation.y = y; + + self + } + + /// Set the rotation in degrees. + fn with_rotation(mut self, rotation: f32) -> Self + where + Self: Sized, + { + self.transform_mut_ref().rotation = Quat::from_rotation_z(rotation); + + self + } + + /// Set the Z-index of the mesh. + /// + /// Z-indices range from `0.0..100.0`. + fn with_z_index(mut self, z_index: f32) -> Self + where + Self: Sized, + { + self.transform_mut_ref().translation.z = z_index; + + self + } +} diff --git a/src/gui.rs b/src/gui.rs deleted file mode 100644 index 8d31b90..0000000 --- a/src/gui.rs +++ /dev/null @@ -1,137 +0,0 @@ -use blit::*; -use direct_gui::controls::*; -use direct_gui::*; -use rust_embed::RustEmbed; -use specs::*; -use specs_derive::Component; - -use super::*; - -#[derive(RustEmbed)] -#[folder = "$OUT_DIR/gui/"] -struct GuiFolder; - -#[derive(Component, Debug)] -pub struct FloatingText { - pub text: String, - pub pos: Point, - pub time_alive: f64, -} - -pub struct FloatingTextSystem; -impl<'a> System<'a> for FloatingTextSystem { - type SystemData = ( - Entities<'a>, - Read<'a, DeltaTime>, - WriteStorage<'a, FloatingText>, - ); - - fn run(&mut self, (entities, dt, mut text): Self::SystemData) { - let dt = dt.to_seconds(); - - for (entity, text) in (&*entities, &mut text).join() { - // Kill the text if it's time alive is up - text.time_alive -= dt; - if text.time_alive <= 0.0 { - let _ = entities.delete(entity); - continue; - } - - // Float the text up - text.pos.0.y -= dt * 20.0; - } - } -} - -#[derive(Debug, Copy, Clone, Eq, PartialEq)] -pub enum GuiEvent { - None, - BuyArcherButton, - BuySoldierButton, -} - -pub struct IngameGui { - gui: Gui, - cs: ControlState, - size: (i32, i32), - bg_pos: (i32, i32), - - menu_bg: BlitBuffer, - archer_button: ControlRef, - soldier_button: ControlRef, -} - -impl IngameGui { - pub fn new(size: (i32, i32)) -> Self { - // Setup the GUI system - let mut gui = Gui::new(size); - - let menu_bg = BlitBuffer::from_memory(&GuiFolder::get("iconbar.blit").unwrap()).unwrap(); - - let bg_x = (size.0 - menu_bg.size().0) / 2; - let bg_y = size.1 - menu_bg.size().1; - - let archer_button_img = gui - .load_sprite_from_memory(&GuiFolder::get("archer-button.blit").unwrap()) - .unwrap(); - let archer_button = - gui.register(Button::new_with_sprite(archer_button_img).with_pos(bg_x + 8, bg_y + 12)); - - let soldier_button_img = gui - .load_sprite_from_memory(&GuiFolder::get("soldier-button.blit").unwrap()) - .unwrap(); - let soldier_button = gui - .register(Button::new_with_sprite(soldier_button_img).with_pos(bg_x + 40, bg_y + 12)); - - IngameGui { - gui, - size, - menu_bg, - archer_button, - soldier_button, - - cs: ControlState::default(), - bg_pos: (bg_x, bg_y), - } - } - - pub fn handle_mouse(&mut self, pos: (i32, i32), left_is_down: bool) { - self.cs.mouse_pos = pos; - self.cs.mouse_down = left_is_down; - } - - pub fn update(&mut self) -> GuiEvent { - let mut result = GuiEvent::None; - - // Set the state to the buttons pressed - { - // If the mouse is not down anymore but the button state is still pressed means that - // the mouse was just released - let archer_button: &Button = self.gui.get(self.archer_button).unwrap(); - if !self.cs.mouse_down && archer_button.pressed() { - result = GuiEvent::BuyArcherButton; - } - - let soldier_button: &Button = self.gui.get(self.soldier_button).unwrap(); - if !self.cs.mouse_down && soldier_button.pressed() { - result = GuiEvent::BuySoldierButton; - } - } - - self.gui.update(&self.cs); - - result - } - - pub fn draw_label(&mut self, buffer: &mut Vec, text: &str, pos: (i32, i32)) { - let default_font = self.gui.default_font(); - self.gui - .draw_label(buffer, default_font, &text.to_string(), pos); - } - - pub fn render(&mut self, buffer: &mut Vec) { - self.menu_bg.blit(buffer, self.size.0 as usize, self.bg_pos); - - self.gui.draw_to_buffer(buffer); - } -} diff --git a/src/inspector.rs b/src/inspector.rs new file mode 100644 index 0000000..a1aff56 --- /dev/null +++ b/src/inspector.rs @@ -0,0 +1,105 @@ +#[cfg(feature = "inspector")] +mod inspector { + pub use bevy_inspector_egui::{Inspectable, RegisterInspectable}; + + use crate::{ + constants::Constants, + map::terrain::Terrain, + projectile::Projectile, + unit::{ + closest::{ClosestAlly, ClosestEnemy}, + unit_type::UnitType, + }, + weapon::Weapon, + }; + use bevy::{ + prelude::{App, Entity, Plugin, With}, + sprite::Mesh2dHandle, + }; + use bevy_inspector_egui::{ + widgets::{InspectorQuery, ResourceInspector}, + InspectableRegistry, InspectorPlugin as BevyInspectorEguiPlugin, + }; + use bevy_inspector_egui_rapier::InspectableRapierPlugin; + + /// The inspector with all the subwindows. + #[derive(Default, Inspectable)] + pub struct Inspector { + #[inspectable(label = "Constants", collapse)] + constants: ResourceInspector, + #[inspectable(label = "Resources", collapse)] + resources: Resources, + #[inspectable(label = "Units", collapse)] + units: InspectorQuery>, + #[inspectable(label = "Weapons", collapse)] + weapons: InspectorQuery>, + #[inspectable(label = "Projectiles", collapse)] + projectiles: InspectorQuery>, + } + + /// Show these resources. + #[derive(Default, Inspectable)] + pub struct Resources { + #[inspectable(label = "Closest Ally")] + closest_ally: ResourceInspector, + #[inspectable(label = "Closest Enemy")] + closest_enemy: ResourceInspector, + #[inspectable(label = "Terrain", collapse)] + terrain: ResourceInspector, + } + + /// The plugin to the inspection of ECS items. + pub struct InspectorPlugin; + + impl Plugin for InspectorPlugin { + fn build(&self, app: &mut App) { + app + // Rapier structs + .add_plugin(InspectableRapierPlugin) + // The debug view + .add_plugin(BevyInspectorEguiPlugin::::new()); + + // Get the registry for inspectables to add our own implementation for custom types + let mut inspectable_registry = app + .world + .get_resource_or_insert_with(InspectableRegistry::default); + + // "Implement" inspectable trait for mesh + inspectable_registry + .register_raw::(crate::draw::mesh::inspector::mesh_inspectable); + } + } +} + +#[cfg(not(feature = "inspector"))] +mod inspector { + pub use mock_inspector_derive::Inspectable; + + use bevy::prelude::{App, Plugin}; + use bevy_egui::EguiPlugin; + + /// The inspectable trait to not do anything. + pub trait Inspectable {} + + /// The register trait to not do anything. + pub trait RegisterInspectable { + fn register_inspectable(&mut self) -> &mut Self { + self + } + } + + impl RegisterInspectable for App {} + + /// The plugin to the inspection of ECS items. + /// + /// Doesn't do anything now. + pub struct InspectorPlugin; + + impl Plugin for InspectorPlugin { + fn build(&self, app: &mut App) { + app.add_plugin(EguiPlugin); + } + } +} + +pub use inspector::*; diff --git a/src/level.rs b/src/level.rs deleted file mode 100644 index e13700f..0000000 --- a/src/level.rs +++ /dev/null @@ -1,235 +0,0 @@ -use blit::Animation; -use cgmath::Point2; -use specs::*; - -use crate::*; - -const WOOD_COLOR: u32 = 0x66_39_31; - -pub fn buy_archer(world: &mut World) { - let archer_sprite = { - let images = &*world.read_resource::(); - - *images.0.get("ally-archer1").unwrap() - }; - - let health = 20.0; - - world - .create_entity() - .with(Ally) - .with(Anim::new(archer_sprite, Animation::start(0, 2, true))) - .with(WorldPosition(Point::new(1.0, 340.0))) - .with(Walk::new( - BoundingBox::new(Point::new(1.0, 5.0), Point::new(4.0, 10.0)), - 20.0, - )) - .with(BoundingBox::new( - Point::new(0.0, 0.0), - Point::new(5.0, 10.0), - )) - .with(Destination(1280.0)) - .with(Health(20.0)) - .with(HealthBar { - health, - max_health: health, - width: 5, - pos: Point2::new(0, 0), - offset: (1, -3), - }) - .with(Melee::new(5.0, 1.0)) - .with(Turret { - delay: 3.0, - min_distance: 20.0, - max_strength: 150.0, - flight_time: 2.0, - strength_variation: 0.1, - ..Turret::default() - }) - .with(TurretOffset((2.0, 2.0))) - .with(Point::new(0.0, 0.0)) - .with(Arrow(3.0)) - .with(Line::new(WOOD_COLOR)) - .with(Damage(5.0)) - .with(ProjectileBoundingBox(BoundingBox::new( - Point::new(0.0, 0.0), - Point::new(1.0, 1.0), - ))) - .with(IgnoreCollision::Ally) - .with(UnitState::Walk) - .build(); -} - -pub fn buy_soldier(world: &mut World) { - let soldier_sprite = { - let images = &*world.read_resource::(); - - *images.0.get("ally-melee1").unwrap() - }; - - let health = 50.0; - - world - .create_entity() - .with(Ally) - .with(Sprite::new(soldier_sprite)) - .with(WorldPosition(Point::new(1.0, 340.0))) - .with(Walk::new( - BoundingBox::new(Point::new(1.0, 5.0), Point::new(4.0, 10.0)), - 15.0, - )) - .with(BoundingBox::new( - Point::new(0.0, 0.0), - Point::new(5.0, 10.0), - )) - .with(Destination(1280.0)) - .with(Health(health)) - .with(HealthBar { - health, - max_health: health, - width: 10, - pos: Point2::new(0, 0), - offset: (-2, -3), - }) - .with(Melee::new(10.0, 1.0)) - .with(UnitState::Walk) - .build(); -} - -pub fn place_turrets(world: &mut World, level: u8) { - let (projectile1, bighole1, enemy_soldier1, enemy_archer1) = { - let images = &*world.read_resource::(); - - ( - *images.0.get("projectile1").unwrap(), - *images.0.get("bighole1").unwrap(), - *images.0.get("enemy-melee1").unwrap(), - *images.0.get("enemy-archer1").unwrap(), - ) - }; - - if level == 1 { - world - .create_entity() - .with(Enemy) - .with(Turret { - delay: 3.0, - min_distance: 50.0, - max_strength: 310.0, - flight_time: 5.0, - strength_variation: 0.05, - ..Turret::default() - }) - .with(Point::new(1270.0, 295.0)) - .with(ProjectileSprite(Sprite::new(projectile1))) - .with(MaskId { - id: bighole1, - size: (5, 5), - }) - .with(ProjectileBoundingBox(BoundingBox::new( - Point::new(0.0, 0.0), - Point::new(5.0, 5.0), - ))) - .with(Damage(30.0)) - .build(); - - world - .create_entity() - .with(Enemy) - .with(Turret { - delay: 1.0, - min_distance: 50.0, - max_strength: 290.0, - flight_time: 4.0, - strength_variation: 0.05, - ..Turret::default() - }) - .with(Point::new(1255.0, 315.0)) - .with(Arrow(7.0)) - .with(Line::new(WOOD_COLOR)) - .with(ProjectileBoundingBox(BoundingBox::new( - Point::new(0.0, 0.0), - Point::new(1.0, 1.0), - ))) - .with(Damage(10.0)) - .build(); - - for i in 0..5 { - let health = 50.0; - - world - .create_entity() - .with(Enemy) - .with(Sprite::new(enemy_soldier1)) - .with(WorldPosition(Point::new(1130.0 - 20.0 * i as f64, 320.0))) - .with(Walk::new( - BoundingBox::new(Point::new(2.0, 5.0), Point::new(5.0, 10.0)), - 15.0, - )) - .with(BoundingBox::new( - Point::new(1.0, 0.0), - Point::new(6.0, 10.0), - )) - .with(Destination(10.0)) - .with(Health(health)) - .with(HealthBar { - health, - max_health: health, - width: 10, - pos: Point2::new(0, 0), - offset: (-2, -3), - }) - .with(Melee::new(10.0, 1.0)) - .with(UnitState::Walk) - .build(); - } - - for i in 0..20 { - let health = 20.0; - - world - .create_entity() - .with(Enemy) - .with(Sprite::new(enemy_archer1)) - .with(WorldPosition(Point::new(1140.0 - 20.0 * i as f64, 320.0))) - .with(Walk::new( - BoundingBox::new(Point::new(1.0, 5.0), Point::new(4.0, 10.0)), - 20.0, - )) - .with(BoundingBox::new( - Point::new(1.0, 0.0), - Point::new(5.0, 10.0), - )) - .with(Destination(10.0)) - .with(Health(health)) - .with(HealthBar { - health, - max_health: health, - width: 5, - pos: Point2::new(0, 0), - offset: (1, -3), - }) - .with(Melee::new(5.0, 1.0)) - .with(Turret { - delay: 3.0, - min_distance: 20.0, - max_strength: 150.0, - flight_time: 2.0, - strength_variation: 0.1, - ..Turret::default() - }) - .with(TurretOffset((2.0, 2.0))) - .with(Point::new(0.0, 0.0)) - .with(Arrow(3.0)) - .with(Line::new(WOOD_COLOR)) - .with(Damage(5.0)) - .with(ProjectileBoundingBox(BoundingBox::new( - Point::new(0.0, 0.0), - Point::new(1.0, 1.0), - ))) - .with(IgnoreCollision::Enemy) - .with(UnitState::Walk) - .build(); - } - } -} diff --git a/src/log.rs b/src/log.rs new file mode 100644 index 0000000..e8fa4f6 --- /dev/null +++ b/src/log.rs @@ -0,0 +1,33 @@ +use bevy::{ + log::LogSettings, + prelude::{App, Plugin}, +}; +use tracing_subscriber::{fmt::Layer, prelude::*, registry::Registry, EnvFilter}; + +/// Define our own log plugin so we can configure tracing. +pub struct CustomLogPlugin; + +impl Plugin for CustomLogPlugin { + #[cfg(target_arch = "wasm32")] + fn build(&self, app: &mut App) { + // Add traces to the console output + tracing_wasm::set_as_global_default(); + } + + #[cfg(not(target_arch = "wasm32"))] + fn build(&self, app: &mut App) { + let default_filter = { + let settings = app.world.get_resource_or_insert_with(LogSettings::default); + format!("{},{}", settings.level, settings.filter) + }; + let filter_layer = EnvFilter::try_from_default_env() + .or_else(|_| EnvFilter::try_new(&default_filter)) + .unwrap(); + let subscriber = Registry::default().with(filter_layer); + + let subscriber = subscriber.with(Layer::default()); + + bevy::utils::tracing::subscriber::set_global_default(subscriber) + .expect("Could not set global default tracing subscriber. If you've already set up a tracing subscriber, please disable LogPlugin from Bevy's DefaultPlugins"); + } +} diff --git a/src/main.rs b/src/main.rs index fc2a689..db44fc1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,310 +1,86 @@ -mod ai; -mod audio; +mod camera; +mod color; +mod constants; mod draw; -mod geom; -mod gui; -mod level; +mod geometry; +mod inspector; +mod log; +mod map; mod physics; mod projectile; -mod terrain; -mod turret; +mod random; +mod ui; mod unit; - -use minifb::*; -use rust_embed::RustEmbed; -use specs::{DispatcherBuilder, Join, World, WorldExt}; -use std::{ - collections::HashMap, - thread, - time::{Duration, SystemTime}, +mod weapon; + +use crate::color::Palette; +use crate::geometry::GeometryPlugin; +use crate::inspector::InspectorPlugin; +use crate::log::CustomLogPlugin; +use crate::{ + camera::CameraPlugin, draw::DrawPlugin, map::MapPlugin, physics::PhysicsPlugin, + projectile::ProjectilePlugin, ui::UiPlugin, unit::UnitPlugin, weapon::WeaponPlugin, }; - -use ai::*; -use audio::Audio; -use draw::*; -use geom::*; -use gui::*; -use level::*; -use physics::*; -use projectile::*; -use terrain::*; -use turret::*; -use unit::*; - -const WIDTH: usize = 1280; -const HEIGHT: usize = 540; - -const GRAVITY: f64 = 98.1; - -#[derive(RustEmbed)] -#[folder = "$OUT_DIR/sprites/"] -struct SpriteFolder; - -impl SpriteFolder { - fn load_sprite(render: &mut Render, resources: &mut HashMap, name: &str) { - let mut file = name.to_owned(); - file.push_str(".blit"); - - let buf = Self::get(&*file).unwrap(); - - resources.insert(name.to_string(), render.add_buf_from_memory(name, &buf)); - } - - fn load_anim(render: &mut Render, resources: &mut HashMap, name: &str) { - let mut file = name.to_owned(); - file.push_str(".anim"); - - let buf = Self::get(&*file).unwrap(); - - resources.insert( - name.to_string(), - render.add_anim_buf_from_memory(name, &buf), - ); - } -} - -#[derive(RustEmbed)] -#[folder = "$OUT_DIR/masks/"] -struct MaskFolder; - -impl MaskFolder { - fn load_sprite(render: &mut Render, resources: &mut HashMap, name: &str) { - let mut file = name.to_owned(); - file.push_str(".blit"); - - let buf = Self::get(&*file).unwrap(); - - resources.insert(name.to_string(), render.add_buf_from_memory(name, &buf)); - } -} +use bevy::{ + log::{Level, LogPlugin, LogSettings}, + prelude::*, +}; +use bevy_easings::EasingsPlugin; +use constants::Constants; fn main() { - let mut buffer: Vec = vec![0; (WIDTH * HEIGHT) as usize]; - - let mut render = Render::new((WIDTH, HEIGHT)); - - let mut resources = HashMap::new(); - - SpriteFolder::load_anim(&mut render, &mut resources, "ally-archer1"); - SpriteFolder::load_sprite(&mut render, &mut resources, "ally-melee1"); - SpriteFolder::load_sprite(&mut render, &mut resources, "enemy-melee1"); - SpriteFolder::load_sprite(&mut render, &mut resources, "enemy-archer1"); - SpriteFolder::load_sprite(&mut render, &mut resources, "projectile1"); - - MaskFolder::load_sprite(&mut render, &mut resources, "bighole1"); - - // Setup game related things - let mut world = World::new(); - - // draw.rs - world.register::(); - world.register::(); - world.register::(); - world.register::(); - world.register::(); - - // terrain.rs - world.register::(); - world.register::(); - - // physics.rs - world.register::(); - world.register::(); - world.register::(); - world.register::(); - - // ai.rs - world.register::(); - world.register::(); - world.register::(); - world.register::(); - - // unit.rs - world.register::(); - world.register::(); - world.register::(); - world.register::(); - - // turret.rs - world.register::(); - world.register::(); - - // projectile.rs - world.register::(); - world.register::(); - world.register::(); - world.register::(); - world.register::(); - world.register::(); - - // gui.rs - world.register::(); - - // Resources to `Fetch` - world.insert(Terrain::new((WIDTH, HEIGHT))); - world.insert(Gravity(GRAVITY)); - world.insert(DeltaTime::new(1.0 / 60.0)); - world.insert(Images(resources)); - world.insert(Audio::new()); - - render.draw_background_from_memory(&SpriteFolder::get("background.blit").unwrap()); - render.draw_terrain_from_memory( - &mut *world.write_resource::(), - &SpriteFolder::get("level.blit").unwrap(), - ); - - place_turrets(&mut world, 1); - - let mut dispatcher = DispatcherBuilder::new() - .with(ProjectileSystem, "projectile", &[]) - .with(ArrowSystem, "arrow", &["projectile"]) - .with( - ProjectileCollisionSystem, - "projectile_collision", - &["projectile"], - ) - .with( - ProjectileRemovalFromMaskSystem, - "projectile_removal_from_mask", - &["projectile"], - ) - .with(TerrainCollapseSystem, "terrain_collapse", &["projectile"]) - .with(WalkSystem, "walk", &[]) - .with(UnitFallSystem, "unit_fall", &["walk"]) - .with(UnitResumeWalkingSystem, "unit_resume_walking", &["walk"]) - .with(UnitCollideSystem, "unit_collide", &["walk"]) - .with(MeleeSystem, "melee", &["walk"]) - .with(HealthBarSystem, "health_bar", &["walk"]) - .with(TurretUnitSystem, "turret_unit", &["walk"]) - .with(TurretSystem, "turret", &["turret_unit"]) - .with(SpriteSystem, "sprite", &["projectile", "walk"]) - .with(AnimSystem, "anim", &["projectile", "walk"]) - .with(ParticleSystem, "particle", &[]) - .with(FloatingTextSystem, "floating_text", &[]) - .build(); - - // Setup minifb window related things - let title = format!( - "Castle Game {} - Press ESC to exit.", - env!("CARGO_PKG_VERSION") - ); - let options = WindowOptions { - borderless: false, - title: true, - scale: Scale::X2, - scale_mode: ScaleMode::AspectRatioStretch, - ..Default::default() - }; - let mut window = Window::new(&title, WIDTH, HEIGHT, options).expect("Unable to open window"); - - // Setup the GUI system - let mut gui = IngameGui::new((WIDTH as i32, HEIGHT as i32)); - - { - // Start the audio - let mut audio = world.write_resource::