├── examples └── loading-maps │ ├── .cargo-ok │ ├── style.css │ ├── src │ ├── systems.rs │ ├── components.rs │ ├── systems │ │ └── treasure.rs │ ├── components │ │ ├── inventory.rs │ │ └── action.rs │ └── lib.rs │ ├── maps │ ├── images │ │ ├── doors.png │ │ ├── uf_items.png │ │ ├── uf_heroes.png │ │ └── uf_terrain.png │ ├── sounds │ │ └── fountain.ogg │ ├── templates │ │ ├── hero.json │ │ └── sound.json │ ├── tiles_test.json │ ├── generic_tiled_test.json │ ├── audio_test.json │ ├── tilesets │ │ └── doors.json │ ├── treasure_chest.json │ └── collision_detection.json │ ├── index.html │ └── Cargo.toml ├── .gitignore ├── old_gods ├── manual │ ├── .gitignore │ ├── src │ │ ├── chapter_1.md │ │ ├── map_creation │ │ │ ├── items.md │ │ │ ├── barriers.md │ │ │ ├── characters.md │ │ │ ├── inventory.md │ │ │ ├── background_tiles.md │ │ │ ├── objects.md │ │ │ ├── sprites.md │ │ │ └── actions.md │ │ ├── img │ │ │ ├── item.png │ │ │ ├── logo.png │ │ │ ├── player.gif │ │ │ ├── drop_items.mp4 │ │ │ ├── inventory.png │ │ │ ├── item_stack.png │ │ │ ├── tile_boundary.gif │ │ │ ├── action_display.png │ │ │ ├── inventory_assoc.png │ │ │ ├── new_sprite_file.mp4 │ │ │ ├── object_boundary.gif │ │ │ └── sprite_variants.mp4 │ │ ├── contributors.md │ │ ├── setup.md │ │ ├── intro.md │ │ ├── SUMMARY.md │ │ └── map_creation.md │ ├── book │ │ └── .nojekyll │ └── book.toml ├── src │ ├── systems │ │ ├── effect │ │ │ └── mod.rs │ │ ├── sprite │ │ │ └── mod.rs │ │ ├── script │ │ │ ├── door.rs │ │ │ └── mod.rs │ │ ├── player │ │ │ └── record.rs │ │ ├── message.rs │ │ ├── zone.rs │ │ ├── map_loader │ │ │ ├── mod.rs │ │ │ └── load.rs │ │ ├── rendering │ │ │ └── text.rs │ │ ├── player.rs │ │ ├── animation.rs │ │ ├── tween.rs │ │ ├── sound.rs │ │ ├── screen.rs │ │ ├── item │ │ │ └── mod.rs │ │ ├── physics │ │ │ └── mod.rs │ │ └── fence.rs │ ├── geom │ │ ├── mod.rs │ │ ├── line.rs │ │ ├── v2.rs │ │ └── shape.rs │ ├── systems.rs │ ├── lib.rs │ ├── utils.rs │ ├── components │ │ ├── font_details.rs │ │ ├── player.rs │ │ ├── exile.rs │ │ ├── cardinal.rs │ │ ├── path.rs │ │ └── rendering.rs │ ├── prelude │ │ └── mod.rs │ ├── fetch.rs │ ├── color.rs │ ├── image.rs │ ├── parser.rs │ ├── sound.rs │ ├── components.rs │ ├── resource_manager.rs │ ├── time.rs │ └── resources.rs ├── test_data │ └── layer_groups.json └── Cargo.toml ├── Cargo.toml ├── rustfmt.toml ├── .ci ├── test.sh └── common.sh ├── .github └── workflows │ ├── clippy.yml │ └── rust.yaml └── README.md /examples/loading-maps/.cargo-ok: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/loading-maps/style.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | -------------------------------------------------------------------------------- /old_gods/manual/.gitignore: -------------------------------------------------------------------------------- 1 | book/* 2 | -------------------------------------------------------------------------------- /old_gods/src/systems/effect/mod.rs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /old_gods/manual/src/chapter_1.md: -------------------------------------------------------------------------------- 1 | # Chapter 1 2 | -------------------------------------------------------------------------------- /old_gods/manual/src/map_creation/items.md: -------------------------------------------------------------------------------- 1 | # Items 2 | -------------------------------------------------------------------------------- /old_gods/manual/src/map_creation/barriers.md: -------------------------------------------------------------------------------- 1 | # Barriers 2 | -------------------------------------------------------------------------------- /old_gods/manual/src/map_creation/characters.md: -------------------------------------------------------------------------------- 1 | # Characters 2 | -------------------------------------------------------------------------------- /old_gods/manual/src/map_creation/inventory.md: -------------------------------------------------------------------------------- 1 | # Inventory 2 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | 3 | members = [ 4 | "old_gods", 5 | "examples/loading-maps" 6 | ] 7 | -------------------------------------------------------------------------------- /examples/loading-maps/src/systems.rs: -------------------------------------------------------------------------------- 1 | pub mod action; 2 | pub mod inventory; 3 | pub mod looting; 4 | -------------------------------------------------------------------------------- /old_gods/manual/book/.nojekyll: -------------------------------------------------------------------------------- 1 | This file makes sure that Github Pages doesn't process mdBook's output. -------------------------------------------------------------------------------- /old_gods/manual/src/img/item.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schell/old-gods/HEAD/old_gods/manual/src/img/item.png -------------------------------------------------------------------------------- /old_gods/manual/src/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schell/old-gods/HEAD/old_gods/manual/src/img/logo.png -------------------------------------------------------------------------------- /old_gods/manual/src/img/player.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schell/old-gods/HEAD/old_gods/manual/src/img/player.gif -------------------------------------------------------------------------------- /examples/loading-maps/src/components.rs: -------------------------------------------------------------------------------- 1 | mod action; 2 | mod inventory; 3 | 4 | pub use action::*; 5 | pub use inventory::*; 6 | -------------------------------------------------------------------------------- /old_gods/manual/src/img/drop_items.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schell/old-gods/HEAD/old_gods/manual/src/img/drop_items.mp4 -------------------------------------------------------------------------------- /old_gods/manual/src/img/inventory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schell/old-gods/HEAD/old_gods/manual/src/img/inventory.png -------------------------------------------------------------------------------- /old_gods/manual/src/img/item_stack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schell/old-gods/HEAD/old_gods/manual/src/img/item_stack.png -------------------------------------------------------------------------------- /old_gods/manual/src/img/tile_boundary.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schell/old-gods/HEAD/old_gods/manual/src/img/tile_boundary.gif -------------------------------------------------------------------------------- /examples/loading-maps/maps/images/doors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schell/old-gods/HEAD/examples/loading-maps/maps/images/doors.png -------------------------------------------------------------------------------- /old_gods/manual/src/img/action_display.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schell/old-gods/HEAD/old_gods/manual/src/img/action_display.png -------------------------------------------------------------------------------- /old_gods/manual/src/img/inventory_assoc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schell/old-gods/HEAD/old_gods/manual/src/img/inventory_assoc.png -------------------------------------------------------------------------------- /old_gods/manual/src/img/new_sprite_file.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schell/old-gods/HEAD/old_gods/manual/src/img/new_sprite_file.mp4 -------------------------------------------------------------------------------- /old_gods/manual/src/img/object_boundary.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schell/old-gods/HEAD/old_gods/manual/src/img/object_boundary.gif -------------------------------------------------------------------------------- /old_gods/manual/src/img/sprite_variants.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schell/old-gods/HEAD/old_gods/manual/src/img/sprite_variants.mp4 -------------------------------------------------------------------------------- /examples/loading-maps/maps/images/uf_items.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schell/old-gods/HEAD/examples/loading-maps/maps/images/uf_items.png -------------------------------------------------------------------------------- /examples/loading-maps/maps/sounds/fountain.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schell/old-gods/HEAD/examples/loading-maps/maps/sounds/fountain.ogg -------------------------------------------------------------------------------- /examples/loading-maps/maps/images/uf_heroes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schell/old-gods/HEAD/examples/loading-maps/maps/images/uf_heroes.png -------------------------------------------------------------------------------- /examples/loading-maps/maps/images/uf_terrain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schell/old-gods/HEAD/examples/loading-maps/maps/images/uf_terrain.png -------------------------------------------------------------------------------- /old_gods/manual/book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | authors = ["Schell Scivally"] 3 | language = "en" 4 | multilingual = false 5 | src = "src" 6 | title = "The Old Gods' Engine" 7 | -------------------------------------------------------------------------------- /old_gods/src/geom/mod.rs: -------------------------------------------------------------------------------- 1 | mod aabb; 2 | mod aabb_tree; 3 | mod line; 4 | mod shape; 5 | mod v2; 6 | 7 | pub use self::{aabb::*, aabb_tree::*, line::*, shape::*, v2::*}; 8 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | blank_lines_upper_bound = 2 2 | combine_control_expr = false 3 | format_code_in_doc_comments = true 4 | format_strings = true 5 | max_width = 100 6 | merge_imports = true -------------------------------------------------------------------------------- /old_gods/manual/src/map_creation/background_tiles.md: -------------------------------------------------------------------------------- 1 | # Background Tiles 2 | 3 | You can build terrain like you would in any Tiled map. Import a tileset and start placing tiles down in layers. As expected, layers are rendered from bottom to top. 4 | -------------------------------------------------------------------------------- /old_gods/manual/src/contributors.md: -------------------------------------------------------------------------------- 1 | # Contributors 2 | 3 | Thanks to all the people involved in making this engine =) 4 | 5 | * Schell Scivally ([schellsan](https://duckduckgo.com/?q=schellsan)) 6 | * Greg Hale ([imalsogreg](https://duckduckgo.com/?q=imalsogreg)) 7 | -------------------------------------------------------------------------------- /.ci/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ROOT="$(git rev-parse --show-toplevel)" 4 | source $ROOT/.ci/common.sh 5 | 6 | section "Test" 7 | 8 | rustup run stable cargo test --verbose 9 | 10 | section "Build WASM" 11 | wasm-pack build --debug --target web examples/loading-maps 12 | 13 | section "done :tada:" 14 | -------------------------------------------------------------------------------- /old_gods/src/systems.rs: -------------------------------------------------------------------------------- 1 | //pub mod action; 2 | pub mod animation; 3 | //pub mod effect; 4 | pub mod fence; 5 | pub mod gamepad; 6 | //pub mod map_loader; 7 | //pub mod message; 8 | pub mod physics; 9 | pub mod player; 10 | pub mod screen; 11 | pub mod sound; 12 | //pub mod sprite; 13 | pub mod tiled; 14 | pub mod tween; 15 | pub mod zone; 16 | -------------------------------------------------------------------------------- /old_gods/manual/src/map_creation/objects.md: -------------------------------------------------------------------------------- 1 | # Objects 2 | 3 | Objects form the one of the most basic building blocks of the engine. Learn to create 4 | objects using the [Working with Objects](https://doc.mapeditor.org/en/stable/manual/objects/) 5 | section of the Tiled documentation. Once created, your game should render your objects as expected. 6 | -------------------------------------------------------------------------------- /old_gods/manual/src/setup.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | The Old Gods' Engine reads Tiled map editor files in JSON format. 4 | [Tiled](https://www.mapeditor.org/) is a free and open source map editor. It's 5 | tops. Please consider donating to Tiled, the developer does good work. 6 | 7 | After downloading Tiled take a moment to read 8 | [the Tiled documentation](https://doc.mapeditor.org/en/stable/). There are also 9 | very good youtube videos explaining its features and how to use them. 10 | -------------------------------------------------------------------------------- /old_gods/manual/src/intro.md: -------------------------------------------------------------------------------- 1 | # Old Gods' Engine 2 | 3 | ![goblin hero](./img/logo.png) 4 | 5 | Welcome, user! Thank you for picking up a copy of the Old Gods' Engine! The OG 6 | Engine is a tool for creating retro style 2d games. It aims to be easy to use and 7 | extend, but its purpose is yet to be set in stone. Indeed, like a troll at 8 | midnight this engine jumps around a lot, but the sun must rise eventually. 9 | 10 | In this book we'll go over how to use the OG Engine for maximum profit or 11 | vengeance, the choice is yours ;) 12 | 13 | [Let's get set up.](./setup.html) 14 | -------------------------------------------------------------------------------- /old_gods/src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate either; 2 | extern crate nom; 3 | extern crate rand; 4 | extern crate serde; 5 | #[macro_use] 6 | extern crate serde_derive; 7 | extern crate serde_json; 8 | extern crate shred; 9 | extern crate shrev; 10 | extern crate spade; 11 | extern crate specs; 12 | 13 | pub mod color; 14 | pub mod components; 15 | pub mod engine; 16 | pub mod fetch; 17 | pub mod geom; 18 | pub mod image; 19 | pub mod parser; 20 | pub mod prelude; 21 | pub mod rendering; 22 | pub mod resources; 23 | pub mod sound; 24 | pub mod systems; 25 | pub mod time; 26 | pub mod utils; 27 | -------------------------------------------------------------------------------- /old_gods/manual/src/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # The Old Gods' Engine 2 | 3 | [Introduction](./intro.md) 4 | - [Getting Started](./setup.md) 5 | - [Map Creation](./map_creation.md) 6 | - [Background Tiles](./map_creation/background_tiles.md) 7 | - [Objects](./map_creation/objects.md) 8 | - [Barriers](./map_creation/barriers.md) 9 | - [Items](./map_creation/items.md) 10 | - [Actions](./map_creation/actions.md) 11 | - [Inventory](./map_creation/inventory.md) 12 | - [Sprites](./map_creation/sprites.md) 13 | - [Characters](./map_creation/characters.md) 14 | [Contributors](./contributors.md) 15 | -------------------------------------------------------------------------------- /old_gods/src/utils.rs: -------------------------------------------------------------------------------- 1 | //! Utilities 2 | 3 | pub trait CanBeEmpty { 4 | /// Return the thing only if it is not empty. 5 | fn non_empty(&self) -> Option<&Self>; 6 | } 7 | 8 | 9 | impl CanBeEmpty for String { 10 | fn non_empty(&self) -> Option<&String> { 11 | if self.is_empty() { 12 | None 13 | } else { 14 | Some(self) 15 | } 16 | } 17 | } 18 | 19 | /// Clamp a number between two numbers 20 | pub fn clamp(mn: N, n: N, mx: N) -> N { 21 | if n < mn { 22 | mn 23 | } else if n > mx { 24 | mx 25 | } else { 26 | n 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /old_gods/src/components/font_details.rs: -------------------------------------------------------------------------------- 1 | //! Information needed to load a Font 2 | 3 | 4 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 5 | pub struct FontDetails { 6 | pub path: String, 7 | pub size: u16, 8 | } 9 | 10 | 11 | impl FontDetails { 12 | pub fn to_css_string(&self) -> String { 13 | let s = format!("{}px {}", self.size, self.path); 14 | s 15 | } 16 | } 17 | 18 | 19 | impl<'a> From<&'a FontDetails> for FontDetails { 20 | fn from(details: &'a FontDetails) -> FontDetails { 21 | FontDetails { 22 | path: details.path.clone(), 23 | size: details.size, 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /old_gods/src/prelude/mod.rs: -------------------------------------------------------------------------------- 1 | pub use super::{ 2 | color::*, 3 | components::{tiled::*, *}, 4 | engine::*, 5 | geom::{AABB, *}, 6 | image::*, 7 | parser::*, 8 | rendering::*, 9 | resources::*, 10 | sound::*, 11 | systems::{ 12 | animation::{Frame, *}, 13 | fence::*, 14 | gamepad::*, 15 | physics::*, 16 | player::*, 17 | screen::*, 18 | sound::*, 19 | tiled::*, 20 | tween::*, 21 | zone::*, 22 | }, 23 | time::*, 24 | utils::*, 25 | }; 26 | pub use either::Either; 27 | pub use serde_json::Value; 28 | pub use shrev::*; 29 | pub use specs::prelude::*; 30 | //pub use super::systems::sprite::*; 31 | -------------------------------------------------------------------------------- /.github/workflows/clippy.yml: -------------------------------------------------------------------------------- 1 | name: clippy 2 | 3 | on: [push] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | 11 | - name: Install nightly toolchain 12 | uses: actions-rs/toolchain@v1 13 | with: 14 | toolchain: nightly 15 | override: true 16 | components: clippy, rustfmt 17 | 18 | - name: Run cargo fmt 19 | uses: actions-rs/cargo@v1 20 | with: 21 | command: fmt 22 | args: --all -- --check 23 | 24 | - name: Run cargo clippy 25 | uses: actions-rs/clippy-check@v1 26 | with: 27 | token: ${{ secrets.GITHUB_TOKEN }} 28 | args: --all-features 29 | -------------------------------------------------------------------------------- /examples/loading-maps/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Test Maps 6 | 7 | 8 | 9 | 10 | 11 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /examples/loading-maps/maps/templates/hero.json: -------------------------------------------------------------------------------- 1 | { "object": 2 | { 3 | "gid":5, 4 | "height":48, 5 | "id":5, 6 | "name":"hero", 7 | "properties":[ 8 | { 9 | "name":"control", 10 | "type":"string", 11 | "value":"player" 12 | }, 13 | { 14 | "name":"inventory_name", 15 | "type":"string", 16 | "value":"hero_inv" 17 | }, 18 | { 19 | "name":"player_index", 20 | "type":"int", 21 | "value":0 22 | }, 23 | { 24 | "name":"toggle_rendering_positions", 25 | "type":"bool", 26 | "value":false 27 | }], 28 | "rotation":0, 29 | "type":"character", 30 | "visible":true, 31 | "width":48 32 | }, 33 | "tileset": 34 | { 35 | "firstgid":1, 36 | "source":"..\/tilesets\/heroes.json" 37 | }, 38 | "type":"template" 39 | } -------------------------------------------------------------------------------- /examples/loading-maps/maps/templates/sound.json: -------------------------------------------------------------------------------- 1 | { "object": 2 | { 3 | "height":0, 4 | "id":2, 5 | "name":"sound", 6 | "point":true, 7 | "properties":[ 8 | { 9 | "name":"autoplay", 10 | "type":"bool", 11 | "value":true 12 | }, 13 | { 14 | "name":"file", 15 | "type":"string", 16 | "value":"sounds\/fountain.ogg" 17 | }, 18 | { 19 | "name":"loop", 20 | "type":"bool", 21 | "value":true 22 | }, 23 | { 24 | "name":"on_map", 25 | "type":"bool", 26 | "value":true 27 | }, 28 | { 29 | "name":"volume", 30 | "type":"float", 31 | "value":1 32 | }], 33 | "rotation":0, 34 | "type":"sound", 35 | "visible":true, 36 | "width":0 37 | }, 38 | "type":"template" 39 | } -------------------------------------------------------------------------------- /old_gods/src/systems/sprite/mod.rs: -------------------------------------------------------------------------------- 1 | use specs::prelude::*; 2 | 3 | use std::collections::HashSet; 4 | 5 | mod record; 6 | //mod warp; 7 | 8 | pub use self::record::*; 9 | //pub use self::warp::*; 10 | 11 | 12 | /// The sprite system controls exiling and domesticating other entities based on 13 | /// an entity's Sprite component's keyframe. 14 | pub struct SpriteSystem; 15 | 16 | 17 | impl<'a> System<'a> for SpriteSystem { 18 | type SystemData = ( 19 | Entities<'a>, 20 | WriteStorage<'a, Exile>, 21 | WriteStorage<'a, Sprite>, 22 | ); 23 | 24 | fn run(&mut self, (entities, mut exiles, mut sprites): Self::SystemData) { 25 | for (ent, sprite) in (&entities, &mut sprites).join() { 26 | let should_skip = 27 | // If this sprite is exiled, skip it 28 | exiles.contains(ent) 29 | // If this sprite does not need its keyframe switched, skip it. 30 | || sprite.keyframe.is_none(); 31 | if should_skip { 32 | continue; 33 | } 34 | let keyframe = sprite.keyframe.take().unwrap(); 35 | // Switch the keyframe of the sprite 36 | sprite.switch_keyframe(&keyframe, &mut exiles); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /old_gods/src/systems/script/door.rs: -------------------------------------------------------------------------------- 1 | use specs::prelude::*; 2 | 3 | use super::super::super::components::{Action, Effect, Sprite}; 4 | 5 | 6 | pub struct Door; 7 | 8 | 9 | impl Door { 10 | /// Run one door. 11 | pub fn run( 12 | actions: &ReadStorage, 13 | entities: &Entities, 14 | ent: Entity, 15 | lazy: &LazyUpdate, 16 | sprite: &Sprite, 17 | ) { 18 | let children: Vec<&Entity> = sprite.current_children(); 19 | 20 | let is_open = sprite.current_keyframe().as_str() == "open"; 21 | let next_keyframe = if is_open { "closed" } else { "open" }; 22 | 23 | 'find_child: for child in children { 24 | // In this simplest of doors script, any action is considered 25 | // a door handle. 26 | if let Some(action) = actions.get(*child) { 27 | // See if it has been taken. 28 | if !action.taken_by.is_empty() { 29 | // The action procs! 30 | lazy 31 | .create_entity(entities) 32 | .with(Effect::ChangeKeyframe { 33 | sprite: ent, 34 | to: next_keyframe.to_string(), 35 | }) 36 | .build(); 37 | break 'find_child; 38 | } 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /old_gods/src/components/player.rs: -------------------------------------------------------------------------------- 1 | use specs::prelude::{Component, Entities, Entity, HashMapStorage, Join, ReadStorage}; 2 | 3 | 4 | /// A component for designating the maximum velocity of an entity. 5 | #[derive(Clone, Debug)] 6 | pub struct MaxSpeed(pub f32); 7 | 8 | 9 | impl MaxSpeed { 10 | pub fn tiled_key() -> String { 11 | "max_speed".to_string() 12 | } 13 | } 14 | 15 | 16 | impl Component for MaxSpeed { 17 | type Storage = HashMapStorage; 18 | } 19 | 20 | 21 | #[derive(Debug, Clone, PartialEq, Hash, Eq, Serialize, Deserialize)] 22 | /// All the AIs in our game. 23 | pub enum AI { 24 | /// An AI that just walks left. 25 | WalksLeft, 26 | } 27 | 28 | 29 | #[derive(Debug, Clone, PartialEq, Hash, Eq)] 30 | /// A player, controlled by an sdl controller. 31 | pub struct Player(pub u32); 32 | 33 | 34 | impl Player { 35 | pub fn tiled_key() -> String { 36 | "control".to_string() 37 | } 38 | 39 | pub fn get_entity<'a>( 40 | &self, 41 | entities: &Entities<'a>, 42 | players: &ReadStorage<'a, Player>, 43 | ) -> Option { 44 | for (entity, player) in (entities, players).join() { 45 | if player == self { 46 | return Some(entity); 47 | } 48 | } 49 | None 50 | } 51 | } 52 | 53 | 54 | impl Component for Player { 55 | type Storage = HashMapStorage; 56 | } 57 | -------------------------------------------------------------------------------- /.ci/common.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | section() { 4 | echo "--- $(TZ=UTC date +%Y%m%d-%H:%M:%S) - $1" 5 | } 6 | 7 | section "Rust Setup" 8 | 9 | if [ -z ${GITHUB_REF+x} ]; then 10 | export GITHUB_REF=`git rev-parse --symbolic-full-name HEAD` 11 | fi 12 | 13 | export PATH=$PATH:$HOME/.cargo/bin 14 | 15 | if hash rustup 2>/dev/null; then 16 | echo "Have rustup, skipping installation..." 17 | else 18 | echo "Installing rustup..." 19 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh 20 | fi 21 | 22 | rustup update 23 | rustup toolchain install nightly 24 | rustup default nightly 25 | 26 | if hash wasm-pack 2>/dev/null; then 27 | echo "Have wasm-pack, skipping installation..." 28 | else 29 | echo "Installing wasm-pack..." 30 | curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh 31 | fi 32 | 33 | if hash wasm-opt 2>/dev/null; then 34 | echo "Have wasm-opt and other wasm tools:" 35 | which wasm-opt 36 | echo "skipping installation..." 37 | else 38 | echo "Installing wasm cli tools..." 39 | git clone https://github.com/WebAssembly/binaryen.git 40 | cd binaryen 41 | cmake . 42 | make 43 | cp bin/* $HOME/.cargo/bin 44 | cd .. 45 | fi 46 | 47 | echo "Building w/ cargo..." 48 | cargo build || exit 1 49 | 50 | echo "Building w/ wasm-pack..." 51 | wasm-pack build --debug --target web examples/loading-maps || exit 1 52 | 53 | echo "Done building on ${GITHUB_REF}" 54 | -------------------------------------------------------------------------------- /old_gods/src/fetch.rs: -------------------------------------------------------------------------------- 1 | use serde::de::DeserializeOwned; 2 | 3 | use std::{future::Future, pin::Pin}; 4 | use wasm_bindgen::JsCast; 5 | use wasm_bindgen_futures::JsFuture; 6 | use web_sys::{window, Request, RequestInit, RequestMode, Response}; 7 | 8 | async fn request_to_text(req: Request) -> Result { 9 | let window = window().ok_or("could not get window")?; 10 | let resp: Response = JsFuture::from(window.fetch_with_request(&req)) 11 | .await 12 | .map_err(|_| "request failed".to_string())? 13 | .dyn_into() 14 | .map_err(|_| "response is malformed")?; 15 | let text: String = JsFuture::from(resp.text().map_err(|_| "could not get response text")?) 16 | .await 17 | .map_err(|_| "getting text failed")? 18 | .as_string() 19 | .ok_or_else(|| "couldn't get text as string".to_string())?; 20 | Ok(text) 21 | } 22 | 23 | pub fn from_url(url: &str) -> Pin>>> { 24 | let mut opts = RequestInit::new(); 25 | opts.method("GET"); 26 | opts.mode(RequestMode::Cors); 27 | 28 | let req = Request::new_with_str_and_init(url, &opts).unwrap(); 29 | 30 | Box::pin(async move { request_to_text(req).await }) 31 | } 32 | 33 | pub async fn _from_json(url: &str) -> Result { 34 | let result: String = from_url(url).await?; 35 | serde_json::from_str(&result).map_err(|e| format!("{}", e)) 36 | } 37 | -------------------------------------------------------------------------------- /examples/loading-maps/maps/tiles_test.json: -------------------------------------------------------------------------------- 1 | { "compressionlevel":0, 2 | "editorsettings": 3 | { 4 | "export": 5 | { 6 | "target":"." 7 | } 8 | }, 9 | "height":3, 10 | "infinite":false, 11 | "layers":[ 12 | { 13 | "data":[22, 42, 62, 102, 122, 142, 162, 182, 202], 14 | "height":3, 15 | "id":1, 16 | "name":"Tile Layer 1", 17 | "opacity":1, 18 | "type":"tilelayer", 19 | "visible":true, 20 | "width":3, 21 | "x":0, 22 | "y":0 23 | }], 24 | "nextlayerid":2, 25 | "nextobjectid":1, 26 | "orientation":"orthogonal", 27 | "properties":[ 28 | { 29 | "name":"toggle_rendering_entity_count", 30 | "type":"bool", 31 | "value":true 32 | }, 33 | { 34 | "name":"toggle_rendering_fps", 35 | "type":"bool", 36 | "value":true 37 | }, 38 | { 39 | "name":"viewport_height_tiles", 40 | "type":"int", 41 | "value":3 42 | }, 43 | { 44 | "name":"viewport_width_tiles", 45 | "type":"int", 46 | "value":3 47 | }], 48 | "renderorder":"right-down", 49 | "tiledversion":"1.3.1", 50 | "tileheight":48, 51 | "tilesets":[ 52 | { 53 | "firstgid":1, 54 | "source":"tilesets\/terrain.json" 55 | }], 56 | "tilewidth":48, 57 | "type":"map", 58 | "version":1.2, 59 | "width":3 60 | } -------------------------------------------------------------------------------- /examples/loading-maps/maps/generic_tiled_test.json: -------------------------------------------------------------------------------- 1 | { "compressionlevel":-1, 2 | "height":3, 3 | "infinite":false, 4 | "layers":[ 5 | { 6 | "data":[126, 127, 129, 126, 128, 126, 126, 126, 127], 7 | "height":3, 8 | "id":1, 9 | "name":"Tile Layer 1", 10 | "opacity":1, 11 | "type":"tilelayer", 12 | "visible":true, 13 | "width":3, 14 | "x":0, 15 | "y":0 16 | }, 17 | { 18 | "draworder":"topdown", 19 | "id":2, 20 | "name":"Object Layer 1", 21 | "objects":[ 22 | { 23 | "gid":569, 24 | "height":48, 25 | "id":1, 26 | "name":"", 27 | "rotation":0, 28 | "type":"", 29 | "visible":true, 30 | "width":48, 31 | "x":34.25, 32 | "y":79.25 33 | }], 34 | "opacity":1, 35 | "type":"objectgroup", 36 | "visible":true, 37 | "x":0, 38 | "y":0 39 | }], 40 | "nextlayerid":3, 41 | "nextobjectid":2, 42 | "orientation":"orthogonal", 43 | "renderorder":"right-down", 44 | "tiledversion":"1.3.1", 45 | "tileheight":48, 46 | "tilesets":[ 47 | { 48 | "firstgid":1, 49 | "source":"tilesets\/terrain.json" 50 | }], 51 | "tilewidth":48, 52 | "type":"map", 53 | "version":1.2, 54 | "width":3 55 | } -------------------------------------------------------------------------------- /old_gods/src/geom/line.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::many_single_char_names)] 2 | 3 | use super::super::geom::V2; 4 | 5 | 6 | #[derive(Debug, Clone, PartialEq, Copy)] 7 | pub struct LineSegment { 8 | pub a: V2, 9 | pub b: V2, 10 | } 11 | 12 | 13 | impl LineSegment { 14 | pub fn new(a: V2, b: V2) -> LineSegment { 15 | LineSegment { a, b } 16 | } 17 | 18 | pub fn intersection_with(&self, l: LineSegment) -> Option { 19 | let r: V2 = self.b - self.a; 20 | let s = l.b - l.a; 21 | let rxs = r.cross(s); 22 | let qp = l.a - self.a; 23 | let qpxr = qp.cross(r); 24 | let rxs_is_zero = rxs.abs() < 1e-10; 25 | 26 | if rxs_is_zero { 27 | None 28 | } else { 29 | let t: f32 = qp.cross(s) / rxs; 30 | let u: f32 = qpxr / rxs; 31 | 32 | // If 0 <= t <= 1 and 0 <= u <= 1 33 | // the two line segments meet at the point p + t r = q + u s. 34 | if (0.0 <= t && t <= 1.0) && (0.0 <= u && u <= 1.0) { 35 | // We can calculate the intersection point using either t or u. 36 | Some(self.a + r.scalar_mul(t)) 37 | } else { 38 | // Otherwise, the two line segments are not parallel but do not intersect. 39 | None 40 | } 41 | } 42 | } 43 | 44 | /// Return the vector difference (b - a). 45 | pub fn vector_difference(&self) -> V2 { 46 | self.b - self.a 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /old_gods/test_data/layer_groups.json: -------------------------------------------------------------------------------- 1 | { "compressionlevel":-1, 2 | "height":3, 3 | "infinite":false, 4 | "layers":[ 5 | { 6 | "id":2, 7 | "layers":[ 8 | { 9 | "data":[0, 0, 0, 0, 0, 0, 0, 0, 0], 10 | "height":3, 11 | "id":1, 12 | "name":"Tile Layer 1", 13 | "opacity":1, 14 | "type":"tilelayer", 15 | "visible":true, 16 | "width":3, 17 | "x":0, 18 | "y":0 19 | }, 20 | { 21 | "draworder":"topdown", 22 | "id":3, 23 | "name":"Object Layer 1", 24 | "objects":[], 25 | "opacity":1, 26 | "type":"objectgroup", 27 | "visible":true, 28 | "x":0, 29 | "y":0 30 | }], 31 | "name":"Group 1", 32 | "opacity":1, 33 | "type":"group", 34 | "visible":true, 35 | "x":0, 36 | "y":0 37 | }], 38 | "nextlayerid":4, 39 | "nextobjectid":1, 40 | "orientation":"orthogonal", 41 | "properties":[ 42 | { 43 | "name":"custom_property", 44 | "type":"int", 45 | "value":666 46 | }], 47 | "renderorder":"right-down", 48 | "tiledversion":"1.3.1", 49 | "tileheight":32, 50 | "tilesets":[], 51 | "tilewidth":32, 52 | "type":"map", 53 | "version":1.2, 54 | "width":3 55 | } -------------------------------------------------------------------------------- /old_gods/src/systems/player/record.rs: -------------------------------------------------------------------------------- 1 | use ::specs::prelude::*; 2 | 3 | use super::super::super::components::{Attributes, Name, ZLevel}; 4 | use super::super::super::geom::V2; 5 | use super::super::super::tiled::json::{Object, Tiledmap}; 6 | use super::super::physics::Velocity; 7 | 8 | 9 | /// All the data needed from a Tiled map in order to create 10 | /// a player. 11 | pub struct ToonRecord { 12 | pub attributes: Attributes, 13 | } 14 | 15 | 16 | impl<'a> ToonRecord { 17 | /// Read a ToonRecord from a tiled Object. 18 | pub fn read(map: &Tiledmap, object: &Object) -> Result { 19 | let attributes = Attributes::read(map, object)?; 20 | let name: Name = attributes.name().ok_or("A player must have a name.")?; 21 | let _position = attributes 22 | .position() 23 | .ok_or("A player must have a position.")?; 24 | let _rendering_or_anime = attributes 25 | .rendering_or_anime() 26 | .ok_or("A player must have a rendering or animation.")?; 27 | let _control = attributes.control().ok_or(format!( 28 | "Player {} must have a 'control' custom property.", 29 | name.0 30 | ))?; 31 | Ok(ToonRecord { attributes }) 32 | } 33 | 34 | /// Decompose an ToonRecord into components and enter them into 35 | /// the ECS. 36 | pub fn into_ecs(self, world: &mut World, z: ZLevel) -> Entity { 37 | let ent = self.attributes.into_ecs(world, z); 38 | world 39 | .write_storage() 40 | .insert(ent, Velocity(V2::new(0.0, 0.0))) 41 | .expect("Could not insert velocity."); 42 | ent 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /old_gods/Cargo.toml: -------------------------------------------------------------------------------- 1 | [lib] 2 | crate-type = ["cdylib", "rlib"] 3 | 4 | [package] 5 | name = "old_gods" 6 | version = "0.0.0" 7 | authors = ["Schell Scivally "] 8 | edition = "2018" 9 | features = ["alloc"] 10 | 11 | [dependencies] 12 | either = "1.5.2" 13 | js-sys = "0.3" 14 | log = "0.4" 15 | nom = "5.0.0-beta2" 16 | rand = "0.7" 17 | serde = { version = "1.0", features = ["derive"] } 18 | serde_derive = "1.0" 19 | serde_json = "1.0" 20 | serde_path_to_error = { version = "0.1", optional = true } 21 | shrev = "1.0" 22 | shred-derive = "0.6" 23 | spade = "1.5.1" 24 | wasm-bindgen = "0.2" 25 | wasm-bindgen-futures = "0.4" 26 | 27 | 28 | [dependencies.web-sys] 29 | version = "0.3" 30 | features = [ 31 | "AudioContext", 32 | "AudioDestinationNode", 33 | "Blob", 34 | "CanvasRenderingContext2d", 35 | "console", 36 | "Document", 37 | "ErrorEvent", 38 | "FileList", 39 | "FileReader", 40 | "Gamepad", 41 | "GamepadButton", 42 | "HtmlAudioElement", 43 | "HtmlCanvasElement", 44 | "HtmlImageElement", 45 | "HtmlMediaElement", 46 | "KeyboardEvent", 47 | "Location", 48 | "MediaElementAudioSourceNode", 49 | "Navigator", 50 | "Performance", 51 | "Request", 52 | "RequestInit", 53 | "RequestMode", 54 | "Response", 55 | "TextMetrics", 56 | "Window" 57 | ] 58 | #[dependencies.sdl2] 59 | #version = "0.32.0" 60 | #default-features = false 61 | #features = [ "mixer", "image", "ttf" ] 62 | 63 | [dependencies.specs] 64 | version = "0.16" 65 | default-features = false 66 | features = ["shred-derive"] 67 | 68 | 69 | [dependencies.shred] 70 | version = "0.9.3" 71 | default-features = false 72 | features = ["nightly"] 73 | 74 | 75 | [features] 76 | default = ["serde_path_to_error"] 77 | -------------------------------------------------------------------------------- /old_gods/src/systems/message.rs: -------------------------------------------------------------------------------- 1 | /// The message system allows text to be shown to the user through a creature 2 | /// talking with a word bubble. 3 | /// 4 | /// A message/wordbubble is transient. It is created by some other system and 5 | /// shows until a certain time has elapsed and then it is removed from the ECS. 6 | use specs::prelude::*; 7 | 8 | use super::super::{ 9 | components::{Exile, Position}, 10 | systems::screen::Screen, 11 | time::FPSCounter, 12 | }; 13 | 14 | 15 | pub struct WordBubble { 16 | _message: String, 17 | time_left: f32, 18 | } 19 | 20 | 21 | impl Component for WordBubble { 22 | type Storage = HashMapStorage; 23 | } 24 | 25 | 26 | pub struct MessageSystem; 27 | 28 | 29 | impl<'a> System<'a> for MessageSystem { 30 | type SystemData = ( 31 | Entities<'a>, 32 | ReadStorage<'a, Exile>, 33 | Read<'a, FPSCounter>, 34 | Read<'a, LazyUpdate>, 35 | ReadStorage<'a, Position>, 36 | Read<'a, Screen>, 37 | WriteStorage<'a, WordBubble>, 38 | ); 39 | 40 | fn run( 41 | &mut self, 42 | (entities, exiles, fps, lazy, positions, screen, mut word_bubbles): Self::SystemData, 43 | ) { 44 | let elements = (&entities, &positions, &mut word_bubbles, !&exiles).join(); 45 | let area = screen.aabb(); 46 | for (ent, &Position(pos), mut word_bubble, ()) in elements { 47 | if !area.contains_point(&pos) { 48 | // this word bubble cannot be seen 49 | continue; 50 | } 51 | 52 | word_bubble.time_left -= fps.last_delta(); 53 | if word_bubble.time_left < 0.0 { 54 | lazy.remove::(ent); 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /old_gods/src/color.rs: -------------------------------------------------------------------------------- 1 | //! Definitions of Color 2 | use wasm_bindgen::JsValue; 3 | 4 | pub mod css; 5 | 6 | /// A color. 7 | #[derive(Debug, Clone, Copy, PartialEq, Hash)] 8 | pub struct Color { 9 | pub r: u8, 10 | pub g: u8, 11 | pub b: u8, 12 | pub a: u8, 13 | } 14 | 15 | 16 | impl Color { 17 | pub fn rgb(r: u8, g: u8, b: u8) -> Color { 18 | Color { r, g, b, a: 255 } 19 | } 20 | 21 | pub fn rgba(r: u8, g: u8, b: u8, a: u8) -> Color { 22 | Color { r, g, b, a } 23 | } 24 | 25 | pub fn into_rgb(self) -> Color { 26 | Color { 27 | r: self.r, 28 | g: self.g, 29 | b: self.g, 30 | a: 255, 31 | } 32 | } 33 | } 34 | 35 | 36 | impl From<&Color> for JsValue { 37 | fn from(color: &Color) -> JsValue { 38 | let s = format!( 39 | "rgba({}, {}, {}, {:.3})", 40 | color.r, 41 | color.g, 42 | color.b, 43 | (color.a as f32 / 255.0) 44 | ); 45 | JsValue::from_str(&s) 46 | } 47 | } 48 | 49 | 50 | impl From for Color { 51 | fn from(n: u32) -> Color { 52 | Color::rgba( 53 | (n >> 24 & 0xff) as u8, 54 | (n >> 16 & 0xff) as u8, 55 | (n >> 8 & 0xff) as u8, 56 | (n & 0xff) as u8, 57 | ) 58 | } 59 | } 60 | 61 | 62 | /// A color used for the background 63 | pub struct BackgroundColor(pub Color); 64 | 65 | 66 | impl Default for BackgroundColor { 67 | fn default() -> Self { 68 | BackgroundColor(Color::rgb(0, 0, 0)) 69 | } 70 | } 71 | 72 | 73 | #[cfg(test)] 74 | mod color_tests { 75 | use super::{css::red, *}; 76 | 77 | #[test] 78 | fn hex() { 79 | let css_red = red(); 80 | let hex_red = Color::from(0xff0000ff); 81 | assert_eq!(css_red, hex_red); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /old_gods/manual/src/map_creation/sprites.md: -------------------------------------------------------------------------------- 1 | # Sprites 2 | 3 | Sprites are complex game objects whose assets and attributes are defined within 4 | a Tiled map file. Sprites are used to create objects that a player can interact 5 | with. Certain Sprites are controlled by special game systems. 6 | 7 | To create a new sprite file simply create a new Tiled map. Name 8 | the file and save it as a json file. 9 | 10 | 11 | 12 | ### Sprite variants 13 | Within a sprite Tiled file each top-level layer should be a group layer. Each 14 | layer is a variant of the sprite. For example, if you were defining a goblin NPC 15 | and there were 3 different goblins - "red_goblin", "blue_goblin" and 16 | "green_goblin" - you would accomplish this by creating 3 layer groups at the top 17 | layer level, each with the name of the variant for that level. 18 | 19 | 20 | 21 | 22 | 23 | Each variant represents a different style of sprite that shares the same logic. 24 | We'll see later how to apply some logic to a sprite when we include one in our map. 25 | 26 | 27 | For the remainder of this example we'll be using `assets/sprites/wooden_door.json` 28 | which describes a wooden door that opens and closes. 29 | 30 | 31 | ### Variant keyframes 32 | 33 | Within a variant we have something called "keyframes". A key frame is one state 34 | of the sprite in time. For example, the `wooden_door` sprite has two such 35 | keyframes: `open` and `closed`. The `wooden_door` sprite's logic determines when 36 | to switch between these two keyframes. Each keyframe is a layer group of tiles 37 | and objects. To define a keyframe for a variant, create a new layer group within 38 | the variant layer group. Add your tiles and objects in new layers within the 39 | keyframe layer group and you're good to go. 40 | 41 | 42 | TODO 43 | -------------------------------------------------------------------------------- /old_gods/src/components/exile.rs: -------------------------------------------------------------------------------- 1 | use specs::prelude::{Component, Entity, HashMapStorage, WriteStorage}; 2 | use std::collections::HashSet; 3 | 4 | 5 | /// ## Exiled entities 6 | 7 | /// Since multiple systems may want to exile or domesticate (un-exile) an entity 8 | /// we use a string to associate an exile with what has exiled it. 9 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 10 | pub struct ExiledBy(pub String); 11 | 12 | 13 | /// An exiled entity is effectively removed from the game, but still exists in 14 | /// the ECS. This maintains the exiled entity's comonents. It's the various other 15 | /// systems' responsibility to check entities for their Exiled compnonents, or 16 | /// lack thereof. 17 | #[derive(Debug, Clone)] 18 | pub struct Exile(pub HashSet); 19 | 20 | 21 | impl Component for Exile { 22 | type Storage = HashMapStorage; 23 | } 24 | 25 | 26 | impl Exile { 27 | pub fn exile(entity: Entity, by: &str, exiles: &mut WriteStorage) { 28 | let by = ExiledBy(by.to_owned()); 29 | if exiles.contains(entity) { 30 | let set = exiles.get_mut(entity).expect("This should never happen."); 31 | set.0.insert(by); 32 | } else { 33 | let mut set = HashSet::new(); 34 | set.insert(by); 35 | exiles 36 | .insert(entity, Exile(set)) 37 | .expect("Could not insert an Exile set."); 38 | } 39 | } 40 | 41 | pub fn domesticate(entity: Entity, by: &str, exiles: &mut WriteStorage) { 42 | let by = ExiledBy(by.to_owned()); 43 | if exiles.contains(entity) { 44 | let set = { 45 | let set = exiles.get_mut(entity).expect("This should never happen."); 46 | set.0.remove(&by); 47 | set.clone() 48 | }; 49 | if set.0.is_empty() { 50 | exiles.remove(entity); 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /.github/workflows/rust.yaml: -------------------------------------------------------------------------------- 1 | name: cicd 2 | 3 | on: push 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v1 12 | 13 | # cacheing 14 | - name: Cache cargo registry 15 | uses: actions/cache@v1 16 | with: 17 | path: ~/.cargo/registry 18 | key: ${{ runner.os }}-cargo-registry-${{ hashFiles('Cargo.lock') }} 19 | restore-keys: | 20 | ${{ runner.os }}-cargo-registry- 21 | 22 | - name: Cache cargo index 23 | uses: actions/cache@v1 24 | with: 25 | path: ~/.cargo/git 26 | key: ${{ runner.os }}-cargo-index-${{ hashFiles('Cargo.lock') }} 27 | restore-keys: | 28 | ${{ runner.os }}-cargo-index- 29 | 30 | - name: Cache local cargo bin 31 | uses: actions/cache@v1 32 | with: 33 | path: ~/.cargo/bin 34 | key: ${{ runner.os }}-cargo-local-bin-${{ hashFiles('Cargo.lock') }} 35 | restore-keys: | 36 | ${{ runner.os }}-cargo-local-bin- 37 | 38 | - name: Cache global cargo bin 39 | uses: actions/cache@v1 40 | with: 41 | path: /usr/share/rust/.cargo/bin 42 | key: ${{ runner.os }}-cargo-global-bin-${{ hashFiles('Cargo.lock') }} 43 | restore-keys: | 44 | ${{ runner.os }}-cargo-global-bin- 45 | 46 | - name: Cache cargo build 47 | uses: actions/cache@v1 48 | with: 49 | path: target 50 | key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('Cargo.lock') }} 51 | restore-keys: | 52 | ${{ runner.os }}-cargo-build-target- 53 | 54 | - name: test_build_lint 55 | run: .ci/lint.sh 56 | 57 | #- name: release 58 | # if: github.ref == 'refs/heads/release' 59 | # env: 60 | # AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 61 | # AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 62 | # AWS_DEFAULT_REGION: ${{ secrets.AWS_DEFAULT_REGION }} 63 | # run: .ci/release.sh 64 | -------------------------------------------------------------------------------- /old_gods/src/systems/zone.rs: -------------------------------------------------------------------------------- 1 | //! Keeps track of any entities that are within the boundaries of a zone. 2 | //! 3 | //! Zones are essentially a cache of entities whose shapes intersect the 4 | //! zone's shape. 5 | use specs::prelude::*; 6 | 7 | use super::super::prelude::{AABBTree, Exile, Position, Shape}; 8 | 9 | 10 | /// A Zone is an area that can hold some entities. In order to work properly 11 | /// an entity with a Zone component should also have a Shape component. 12 | #[derive(Debug, Clone)] 13 | pub struct Zone { 14 | pub inside: Vec, 15 | } 16 | 17 | 18 | impl Component for Zone { 19 | type Storage = HashMapStorage; 20 | } 21 | 22 | 23 | /// The ZoneSystem keeps track of any entities that are within the boundaries of 24 | /// any zone. 25 | /// To be within a zone means that one's shape intersects the zone's shape. 26 | pub struct ZoneSystem; 27 | 28 | 29 | #[derive(SystemData)] 30 | pub struct ZoneSystemData<'a> { 31 | aabb_tree: Read<'a, AABBTree>, 32 | entities: Entities<'a>, 33 | exiles: ReadStorage<'a, Exile>, 34 | positions: ReadStorage<'a, Position>, 35 | shapes: ReadStorage<'a, Shape>, 36 | zones: WriteStorage<'a, Zone>, 37 | } 38 | 39 | 40 | impl<'a> System<'a> for ZoneSystem { 41 | type SystemData = ZoneSystemData<'a>; 42 | 43 | fn run(&mut self, mut data: Self::SystemData) { 44 | // Do some generic zone upkeep 45 | let exiles = &data.exiles; 46 | for (zone_ent, mut zone, ()) in (&data.entities, &mut data.zones, !exiles).join() { 47 | let intersections: Vec = data 48 | .aabb_tree 49 | .query_intersecting_shapes(&data.entities, &zone_ent, &data.shapes, &data.positions) 50 | .into_iter() 51 | .filter_map(|(e, _, _)| { 52 | if e == zone_ent || exiles.contains(e) { 53 | None 54 | } else { 55 | Some(e) 56 | } 57 | }) 58 | .collect(); 59 | zone.inside = intersections; 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /examples/loading-maps/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "loading-maps" 3 | version = "0.0.0" 4 | authors = ["Schell Scivally "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib", "rlib"] 9 | 10 | [features] 11 | default = ["console_error_panic_hook"] 12 | 13 | [dependencies] 14 | console_log = "0.1.2" 15 | old_gods = { path = "../../old_gods" } 16 | log = "0.4" 17 | serde = { version = "1.0", features = ["derive"] } 18 | serde_json = "1.0" 19 | shred-derive = "0.6" 20 | wasm-bindgen = "0.2" 21 | wasm-bindgen-futures = "0.4" 22 | 23 | # The `console_error_panic_hook` crate provides better debugging of panics by 24 | # logging them with `console.error`. This is great for development, but requires 25 | # all the `std::fmt` and `std::panicking` infrastructure, so isn't great for 26 | # code size when deploying. 27 | console_error_panic_hook = { version = "0.1.6", optional = true } 28 | 29 | # `wee_alloc` is a tiny allocator for wasm that is only ~1K in code size 30 | # compared to the default allocator's ~10K. It is slower than the default 31 | # allocator, however. 32 | # 33 | # Unfortunately, `wee_alloc` requires nightly Rust when targeting wasm for now. 34 | wee_alloc = { version = "0.4.2", optional = true } 35 | 36 | [dependencies.mogwai] 37 | #path = "/media/schell/orange/Dropbox/code/mogwai/mogwai" 38 | git = "https://github.com/schell/mogwai.git" 39 | branch = "master" 40 | #rev = "afe175fb3ef0d41ff5963dd28b81ef7e83b02232" 41 | 42 | [dependencies.specs] 43 | version = "0.16" 44 | default-features = false 45 | features = ["shred-derive"] 46 | 47 | [dependencies.shred] 48 | version = "0.9.3" 49 | default-features = false 50 | features = ["nightly"] 51 | 52 | [dependencies.web-sys] 53 | version = "0.3" 54 | # Add more web-sys API's as you need them 55 | features = [ 56 | "Blob", 57 | "CanvasRenderingContext2d", 58 | "DomException", 59 | "HtmlCanvasElement", 60 | "HtmlImageElement", 61 | "HtmlInputElement", 62 | "Request", 63 | "RequestInit", 64 | "RequestMode", 65 | "Response", 66 | "TextMetrics", 67 | "Window" 68 | ] 69 | 70 | [dev-dependencies] 71 | wasm-bindgen-test = "0.2" 72 | 73 | [profile.release] 74 | # Tell `rustc` to optimize for small code size. 75 | opt-level = "s" 76 | -------------------------------------------------------------------------------- /old_gods/src/components/cardinal.rs: -------------------------------------------------------------------------------- 1 | //! Data for cardinal directions. 2 | use specs::prelude::{Component, HashMapStorage}; 3 | 4 | use super::V2; 5 | 6 | 7 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 8 | pub enum Cardinal { 9 | North, 10 | East, 11 | South, 12 | West, 13 | } 14 | 15 | 16 | impl Cardinal { 17 | pub fn try_from_str(s: &str) -> Option { 18 | match s { 19 | "north" => Some(Cardinal::North), 20 | "east" => Some(Cardinal::East), 21 | "south" => Some(Cardinal::South), 22 | "west" => Some(Cardinal::West), 23 | _ => None, 24 | } 25 | } 26 | 27 | //pub fn from_keycode(keycode: &Keycode) -> Option { 28 | // match keycode { 29 | // Keycode::J => {Some(Cardinal::South)} 30 | // Keycode::K => {Some(Cardinal::North)} 31 | // Keycode::H => {Some(Cardinal::West)} 32 | // Keycode::L => {Some(Cardinal::East)} 33 | // _ => None 34 | // } 35 | //} 36 | 37 | /// Returns a cardinal direction from the vector, if possible. 38 | /// Returns None if the x and y components of the vector are equal. 39 | pub fn from_v2(v: &V2) -> Option { 40 | if v.x.abs() > v.y.abs() { 41 | // East or West 42 | if v.x < 0.0 { 43 | Some(Cardinal::West) 44 | } else { 45 | Some(Cardinal::East) 46 | } 47 | } else if v.x.abs() < v.y.abs() { 48 | // North or South 49 | if v.y < 0.0 { 50 | Some(Cardinal::North) 51 | } else { 52 | Some(Cardinal::South) 53 | } 54 | } else { 55 | None 56 | } 57 | } 58 | 59 | 60 | /// Returns the caradinal expressed as a vector. 61 | pub fn as_v2(&self) -> V2 { 62 | match self { 63 | Cardinal::North => V2::new(0.0, -1.0), 64 | Cardinal::East => V2::new(1.0, 0.0), 65 | Cardinal::South => V2::new(0.0, 1.0), 66 | Cardinal::West => V2::new(-1.0, 0.0), 67 | } 68 | } 69 | 70 | 71 | pub fn opposite(&self) -> Cardinal { 72 | match self { 73 | Cardinal::East => Cardinal::West, 74 | Cardinal::West => Cardinal::East, 75 | Cardinal::North => Cardinal::South, 76 | Cardinal::South => Cardinal::North, 77 | } 78 | } 79 | } 80 | 81 | 82 | impl Component for Cardinal { 83 | type Storage = HashMapStorage; 84 | } 85 | -------------------------------------------------------------------------------- /old_gods/src/systems/map_loader/mod.rs: -------------------------------------------------------------------------------- 1 | use shrev::EventChannel; 2 | use specs::prelude::*; 3 | use std::collections::HashSet; 4 | 5 | use super::super::{color::BackgroundColor, geom::V2}; 6 | 7 | pub mod load; 8 | pub use load::*; 9 | 10 | mod map_loader; 11 | pub use self::map_loader::{LoadedLayers, MapLoader}; 12 | 13 | 14 | //////////////////////////////////////////////////////////////////////////////// 15 | /// MapLoadingEvent 16 | //////////////////////////////////////////////////////////////////////////////// 17 | pub struct Tags(pub HashSet); 18 | 19 | 20 | impl Component for Tags { 21 | type Storage = HashMapStorage; 22 | } 23 | //////////////////////////////////////////////////////////////////////////////// 24 | /// MapLoadingEvent 25 | //////////////////////////////////////////////////////////////////////////////// 26 | pub enum MapLoadingEvent { 27 | /// Load a new map into the ECS 28 | LoadMap(String, V2), 29 | LoadSprite { 30 | file: String, 31 | variant: String, 32 | keyframe: Option, 33 | origin: V2, 34 | }, 35 | UnloadEverything, 36 | } 37 | 38 | 39 | //////////////////////////////////////////////////////////////////////////////// 40 | /// MapLoadingSystem 41 | //////////////////////////////////////////////////////////////////////////////// 42 | pub struct MapLoadingSystem { 43 | pub opt_reader: Option>, 44 | } 45 | 46 | 47 | impl<'a> System<'a> for MapLoadingSystem { 48 | type SystemData = ( 49 | Read<'a, EventChannel>, 50 | Write<'a, BackgroundColor>, 51 | Read<'a, LazyUpdate>, 52 | ); 53 | 54 | fn setup(&mut self, world: &mut World) { 55 | Self::SystemData::setup(world); 56 | 57 | self.opt_reader = Some( 58 | world 59 | .fetch_mut::>() 60 | .register_reader(), 61 | ); 62 | } 63 | 64 | fn run(&mut self, (chan, mut background_color, lazy): Self::SystemData) { 65 | if let Some(reader) = self.opt_reader.as_mut() { 66 | for event in chan.read(reader) { 67 | match event { 68 | MapLoadingEvent::LoadMap(file, _global_pos) => { 69 | let file = file.clone(); 70 | MapLoader::load_it(file, &lazy); 71 | } 72 | MapLoadingEvent::LoadSprite{.. /*file, variant, keyframe, origin*/} => { 73 | //let mut loader = MapLoader::new(&entities, &update); 74 | //loader.load_sprite(&file, &variant, keyframe.as_ref(), None, &origin); 75 | } 76 | MapLoadingEvent::UnloadEverything => { 77 | lazy 78 | .exec_mut(|world| world.delete_all()); 79 | background_color.0 = BackgroundColor::default().0; 80 | } 81 | } 82 | } 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | The Old Gods Engine 4 |
5 | 6 |

7 |
8 | 9 | This is an aspirational handmade game engine meant for games targeting the web and SDL2. 10 | It reads Tiled map files into a specs based entity component system. 11 | 12 | Rendering is handled by HtmlCanvasElement or the built in SDL2 renderer. 13 | 14 | A number of base systems handle the core of the engine: 15 | * TiledmapSystem - loads maps 16 | * Physics - collision detection and handling 17 | * AnimationSystem - sprite animation 18 | * GamepadSystem - controller support 19 | 20 | More specific add-ons are available as separate crates. 21 | 22 | ## Warning 23 | This is a WIP and it was also my first Rust project. 24 | 25 | ## Performance 26 | I'm really surprised at the performance. So far without any attention to 27 | performance the engine is running at about 330FPS, with a high of about 500FPS 28 | (in SDL2). On wasm it's running at a pretty steady 60FPS, but this is only 29 | because the frame rate is tied to `requestAnimationFrame`. 30 | 31 | ## Core Features 32 | 33 | * Map creation using the ubiquitous Tiled map editor. 34 | * Animation 35 | * Sprites (nested, keyframed Tiled maps) 36 | * Collision detection and handling (SAT for AABBs) 37 | * Dynamic viewport rendering 38 | * Easily overridable default rendering 39 | 40 | See the [old gods architectural diagram](old_gods/architecture.md) for a quick 41 | overview of the core systems. 42 | 43 | ## Extras / Examples 44 | * Inventory and items 45 | 46 | ## Building 47 | First you'll need new(ish) version of the rust toolchain. For that you can visit 48 | https://rustup.rs/ and follow the installation instructions. 49 | 50 | This project uses the nightly release: 51 | 52 | ``` 53 | rustup default nightly 54 | ``` 55 | 56 | Then you'll need [wasm-pack](https://rustwasm.github.io/wasm-pack/installer/). 57 | 58 | Then, if you don't already have it, `cargo install basic-http-server` or use your 59 | favorite alternative static web server to serve assets. 60 | 61 | After that building is pretty straightforward 62 | 63 | ``` 64 | cargo build 65 | wasm-pack build --debug --target web examples/{some example} 66 | basic-http-server -x -a 127.0.0.1:8888 examples/{some example} 67 | ``` 68 | 69 | Then visit http://localhost:8888/ 70 | 71 | ## Contributing 72 | 73 | If you'd like to contribute check the [issues][issues]. Or look at what 74 | [projects][projects] are kicking around! 75 | 76 | ### Code style 77 | 78 | Formatting is enforced by `rustfmt`. Before you commit do: 79 | 80 | ``` 81 | cargo fmt 82 | ``` 83 | 84 | and your changes will be reformatted to fit our standard. 85 | 86 | [issues]: https://github.com/schell/old-gods/issues 87 | [projects]: https://github.com/schell/old-gods/projects 88 | -------------------------------------------------------------------------------- /old_gods/manual/src/map_creation/actions.md: -------------------------------------------------------------------------------- 1 | # Actions 2 | 3 | Actions are special components that allow characters to interact with objects in 4 | the world. 5 | 6 | ![action display](../img/action_display.png "a player's available action") 7 | 8 | Action "Unlock with white key" 9 | 10 | An action has a description, a fitness strategy. 11 | 12 | A good example of an action is the "Pick up" action automatically created for every 13 | item, which enables a character to pick up an item from the map and place it in 14 | their inventory. Another example is the action on a door that opens or closes the 15 | door. 16 | 17 | ### To place an action 18 | 19 | * Create an object layer if one doesn't already exist 20 | * Use the `Insert Point` tool to add a point object to the layer 21 | * Set the `Type` of the object to `action` 22 | * Add a custom property `text` 23 | * Set the `text` property to the string to display to the player 24 | 25 | ### Properties 26 | 27 | #### Required 28 | 29 | | property | value | description | 30 | |----------|---------------------------------------------------|--------------------------------------------------------------| 31 | | text | any unique string | text displayed to the user when the action appears in the UI | 32 | | fitness | [fitness strategy value](#fitness_strategy_value) | defines when an action can be taken | 33 | | lifespan | [lifespan value](#lifespane_value) | defines how long an action lives | 34 | 35 | #### Fitness Strategy Values 36 | | value | description | 37 | |--------------------------------------|---------------------------------------------------------------------------| 38 | | has_inventory | is fit if a taker has an inventory | 39 | | has_item {string} | is fit if a taker has an inventory containing an item with the given name | 40 | | any [_strategy1_, __strategy2_, ...] | is fit if any of the enclosed fitness strategies are fit | 41 | | all [_strategy1_, __strategy2_, ...] | is fit if all of the enclosed fitness strategies are fit | 42 | 43 | #### Lifespan Values 44 | | value | description | 45 | |---------|----------------------------------------------| 46 | | forever | the action lives forever | 47 | | {int} | the number of times this action may be taken | 48 | | | | 49 | 50 | ## Action Effects 51 | Once an action is taken it is up to a game system to carry out its effects. 52 | TODO: Write more about action effects. 53 | -------------------------------------------------------------------------------- /old_gods/src/image.rs: -------------------------------------------------------------------------------- 1 | //! Loading images/textures. 2 | use log::trace; 3 | use wasm_bindgen::{prelude::*, JsCast}; 4 | use web_sys::{window, EventTarget, HtmlImageElement}; 5 | 6 | use super::prelude::{Callbacks, LoadStatus, LoadableResources, Resources, SharedResource}; 7 | 8 | 9 | pub struct HtmlImageResources(pub LoadableResources); 10 | 11 | 12 | impl Default for HtmlImageResources { 13 | fn default() -> Self { 14 | Self::new() 15 | } 16 | } 17 | 18 | 19 | impl HtmlImageResources { 20 | pub fn new() -> Self { 21 | HtmlImageResources(LoadableResources::new()) 22 | } 23 | } 24 | 25 | 26 | impl Resources for HtmlImageResources { 27 | fn status_of(&self, s: &str) -> LoadStatus { 28 | self.0.status_of(s) 29 | } 30 | 31 | fn load(&mut self, path: &str) { 32 | trace!("loading sprite sheet: {}", path); 33 | 34 | let img = window() 35 | .expect("no window") 36 | .document() 37 | .expect("no document") 38 | .create_element("img") 39 | .expect("can't create img") 40 | .dyn_into::() 41 | .expect("can't coerce img"); 42 | img.set_src(path); 43 | 44 | let rsrc = SharedResource::default(); 45 | rsrc.set_status_and_resource((LoadStatus::Started, Some(img.clone()))); 46 | 47 | let load_rsrc = rsrc.clone(); 48 | let load = Closure::wrap(Box::new(move |_: JsValue| { 49 | load_rsrc.set_status(LoadStatus::Complete); 50 | }) as Box); 51 | 52 | let err_rsrc = rsrc.clone(); 53 | let err_path = path.to_string(); 54 | let err = Closure::wrap(Box::new(move |event: JsValue| { 55 | trace!("error event: {:#?}", event); 56 | let event = event 57 | .dyn_into::() 58 | .expect("Error is not an Event"); 59 | let msg = format!("failed loading {}: {}", &err_path, event.type_()); 60 | trace!(" loading {} erred: {}", &err_path, &msg); 61 | err_rsrc.set_status_and_resource((LoadStatus::Error(msg), None)); 62 | }) as Box); 63 | 64 | let target: &EventTarget = img.dyn_ref().expect("can't coerce img as EventTarget"); 65 | target 66 | .add_event_listener_with_callback("load", load.as_ref().unchecked_ref()) 67 | .unwrap(); 68 | target 69 | .add_event_listener_with_callback("error", err.as_ref().unchecked_ref()) 70 | .unwrap(); 71 | self.0 72 | .callbacks 73 | .insert(path.to_string(), Callbacks::new(load, err)); 74 | self.0.resources.insert(path.to_string(), rsrc); 75 | } 76 | 77 | fn take(&mut self, s: &str) -> Option> { 78 | self.0.take(s) 79 | } 80 | 81 | fn put(&mut self, path: &str, shared_tex: SharedResource) { 82 | self.0.put(path, shared_tex) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /examples/loading-maps/src/systems/treasure.rs: -------------------------------------------------------------------------------- 1 | use specs::prelude::{ 2 | Entity, 3 | Entities, 4 | LazyUpdate, 5 | System, 6 | ReadStorage, 7 | }; 8 | 9 | use old_gods::components::{ 10 | Action, Effect, Inventory, Name, Sprite, 11 | }; 12 | 13 | 14 | pub struct TreasureChestSystem; 15 | 16 | 17 | impl TreasureChestSystem { 18 | /// Get the treasure's inventory 19 | pub fn inventory( 20 | children: &Vec, 21 | inventories: &ReadStorage, 22 | ) -> Entity { 23 | for entity in children { 24 | if inventories.contains(*entity) { 25 | return entity.clone(); 26 | } 27 | } 28 | panic!("Could not find a container's inventory") 29 | } 30 | } 31 | 32 | 33 | //impl<'s> System<'s> for TreasureChestSystem { 34 | // type SystemData = ( 35 | // ReadStorage<'s, Action>, 36 | // Entities<'s>, 37 | // ReadStorage<'s, Inventory>, 38 | // LazyUpdate<'s>, 39 | // ReadSTorage<'s, Name> 40 | // ); 41 | // 42 | // /// Run one container. 43 | // pub fn run( 44 | // actions: &ReadStorage, 45 | // entities: &Entities, 46 | // ent: Entity, 47 | // inventories: &ReadStorage, 48 | // lazy: &LazyUpdate, 49 | // names: &ReadStorage, 50 | // sprite: &Sprite, 51 | // ) { 52 | // let children: Vec = sprite 53 | // .current_children() 54 | // .into_iter() 55 | // .map(|c| c.clone()) 56 | // .collect(); 57 | // 58 | // for child in &children { 59 | // if let Some(action) = actions.get(*child) { 60 | // let name = names.get(*child).expect("A sprite action has no name!"); 61 | // // See if it has been taken. 62 | // if !action.taken_by.is_empty() { 63 | // // The action procs! 64 | // match name.0.as_str() { 65 | // "open" => { 66 | // println!("Opening container {:?}", sprite.keyframe); 67 | // lazy 68 | // .create_entity(entities) 69 | // .with(Effect::ChangeKeyframe { 70 | // sprite: ent, 71 | // to: "open".to_string(), 72 | // }) 73 | // .build(); 74 | // } 75 | // "close" => { 76 | // println!("Closing container {:?}", sprite.keyframe); 77 | // lazy 78 | // .create_entity(entities) 79 | // .with(Effect::ChangeKeyframe { 80 | // sprite: ent, 81 | // to: "close".to_string(), 82 | // }) 83 | // .build(); 84 | // } 85 | // "loot" => { 86 | // let inv: Entity = Container::inventory(&children, inventories); 87 | // for looter in &action.taken_by { 88 | // lazy 89 | // .create_entity(entities) 90 | // .with(Effect::LootInventory { 91 | // inventory: Some(inv), 92 | // looter: *looter, 93 | // }) 94 | // .build(); 95 | // } 96 | // //// Later, exile the action so it doesn't show during the loot process. 97 | // //Exile::exile_later(*child, ExiledBy("container"), &updater); 98 | // } 99 | // s => { 100 | // panic!("Unsupported container action named {:?}", s); 101 | // } 102 | // } 103 | // } 104 | // } 105 | // } 106 | // } 107 | //} 108 | -------------------------------------------------------------------------------- /old_gods/src/parser.rs: -------------------------------------------------------------------------------- 1 | //! Parsing helpers for various Tiled parsing tasks. 2 | #![allow(clippy::many_single_char_names)] 3 | use nom::combinator::map_res; 4 | pub use nom::{ 5 | branch::alt, 6 | bytes::complete::{tag, take_till, take_while_m_n}, 7 | character::complete::{char, digit1, multispace0, multispace1}, 8 | error::ErrorKind, 9 | multi::separated_list, 10 | number::complete::{be_u32, float}, 11 | sequence::tuple, 12 | AsChar, Err, IResult, InputIter, InputTakeAtPosition, Slice, 13 | }; 14 | 15 | use super::color::Color; 16 | 17 | /// Parse a string 18 | pub fn string(i: &str) -> IResult<&str, String> { 19 | let (i, _) = char('"')(i)?; 20 | let (i, n) = take_till(|c| c == '"')(i)?; 21 | let (i, _) = char('"')(i)?; 22 | Ok((i, n.to_string())) 23 | } 24 | 25 | /// Parse a tuple of 2 params. 26 | pub fn params2(item1: X, item2: Y) -> impl Fn(I) -> IResult 27 | where 28 | X: Fn(I) -> IResult, 29 | Y: Fn(I) -> IResult, 30 | I: InputIter + InputTakeAtPosition + Clone + Slice>, 31 | ::Item: AsChar + Clone, 32 | ::Item: AsChar + Clone, 33 | { 34 | move |i: I| { 35 | let comma = tuple((multispace0, char(','), multispace0)); 36 | let (i, _) = char('(')(i)?; 37 | let (i, _) = multispace0(i)?; 38 | let (i, a) = item1(i)?; 39 | let (i, _) = comma(i)?; 40 | let (i, b) = item2(i)?; 41 | let (i, _) = multispace0(i)?; 42 | let (i, _) = char(')')(i)?; 43 | Ok((i, (a, b))) 44 | } 45 | } 46 | 47 | 48 | /// Parse a vec 49 | pub fn vec(parse_item: &'static G) -> impl Fn(I) -> IResult> 50 | where 51 | G: Fn(I) -> IResult, 52 | I: InputIter + InputTakeAtPosition + Clone + PartialEq + Slice>, 53 | ::Item: AsChar + Clone, 54 | ::Item: AsChar + Clone, 55 | { 56 | move |i: I| { 57 | let comma = tuple((multispace0, char(','), multispace0)); 58 | let (i, _) = char('[')(i)?; 59 | let (i, v) = separated_list(comma, parse_item)(i)?; 60 | let (i, _) = char(']')(i)?; 61 | Ok((i, v)) 62 | } 63 | } 64 | 65 | 66 | fn from_hex(input: &str) -> Result { 67 | u8::from_str_radix(input, 16) 68 | } 69 | 70 | fn is_hex_digit(c: char) -> bool { 71 | c.is_digit(16) 72 | } 73 | 74 | fn hex_primary(input: &str) -> IResult<&str, u8> { 75 | map_res(take_while_m_n(2, 2, is_hex_digit), from_hex)(input) 76 | } 77 | 78 | 79 | pub fn hex_color_3(input: &str) -> IResult<&str, (u8, u8, u8)> { 80 | let (input, _) = tag("#")(input)?; 81 | tuple((hex_primary, hex_primary, hex_primary))(input) 82 | } 83 | 84 | 85 | pub fn hex_color_4(input: &str) -> IResult<&str, (u8, u8, u8, u8)> { 86 | let (input, _) = tag("#")(input)?; 87 | tuple((hex_primary, hex_primary, hex_primary, hex_primary))(input) 88 | } 89 | 90 | 91 | pub fn hex_color_rgba(input: &str) -> IResult<&str, Color> { 92 | let (i, (a, r, g, b)) = hex_color_4(input)?; 93 | Ok((i, Color::rgba(r, g, b, a))) 94 | } 95 | 96 | 97 | pub fn hex_color_rgb(input: &str) -> IResult<&str, Color> { 98 | let (i, (r, g, b)) = hex_color_3(input)?; 99 | Ok((i, Color::rgb(r, g, b))) 100 | } 101 | 102 | pub fn hex_color(input: &str) -> IResult<&str, Color> { 103 | alt((hex_color_rgba, hex_color_rgb))(input) 104 | } 105 | -------------------------------------------------------------------------------- /old_gods/src/systems/rendering/text.rs: -------------------------------------------------------------------------------- 1 | //use sdl2::render::*; 2 | //use sdl2::rect::Rect; 3 | use std::path::Path; 4 | 5 | use super::super::super::geom::V2; 6 | use super::super::super::resource_manager::*; 7 | use super::record::*; 8 | use super::render; 9 | 10 | 11 | pub struct RenderText; 12 | 13 | 14 | impl<'ctx, 'res> RenderText { 15 | 16 | /// Texturize some text 17 | pub fn texturize_text( 18 | resources: &'res mut Sdl2Resources<'ctx>, 19 | texture_key: &String, 20 | t: &Text 21 | ) { 22 | // The path to the font is actually inside the 23 | // font directory 24 | let fonts_dir = 25 | resources 26 | .font_directory 27 | .clone(); 28 | let mut descriptor = 29 | t.font.clone(); 30 | descriptor.path = 31 | Path::new(&fonts_dir) 32 | .join(Path::new(&descriptor.path)) 33 | .with_extension("ttf") 34 | .to_str() 35 | .unwrap() 36 | .to_string(); 37 | // Load the font 38 | let font = 39 | resources 40 | .font_manager 41 | .load(&descriptor) 42 | .unwrap(); 43 | // Generate the texture 44 | let surface = 45 | font 46 | .render(&t.text.as_str()) 47 | .blended(t.color) 48 | .map_err(|e| e.to_string()) 49 | .unwrap(); 50 | let mut texture = 51 | resources 52 | .texture_creator 53 | .create_texture_from_surface(&surface) 54 | .map_err(|e| e.to_string()) 55 | .unwrap(); 56 | texture 57 | .set_blend_mode(BlendMode::Blend); 58 | texture 59 | .set_alpha_mod(t.color.a); 60 | // Give the texture to the texture manager 61 | resources 62 | .texture_manager 63 | .put_resource(&texture_key, texture); 64 | } 65 | 66 | 67 | /// Texturize some text if needed 68 | pub fn texturize_text_if_needed( 69 | resources: &'res mut Sdl2Resources<'ctx>, 70 | text: &Text 71 | ) -> &'res Texture<'ctx> { 72 | // Maybe we've already drawn this text before, generate 73 | // the key it would/will live under 74 | let texture_key = 75 | format!("{:?}", text); 76 | // Determine if we've already generated a texture for this 77 | // text rendering 78 | let has_text = 79 | resources 80 | .texture_manager 81 | .get_cache() 82 | .contains_key(&texture_key); 83 | if !has_text { 84 | Self::texturize_text(resources, &texture_key, text); 85 | } 86 | resources 87 | .texture_manager 88 | .get_cache() 89 | .get(&texture_key) 90 | .expect("Impossible") 91 | } 92 | 93 | 94 | /// Get the drawn size of some text 95 | pub fn text_size( 96 | resources: &'res mut Sdl2Resources<'ctx>, 97 | text: &Text 98 | ) -> (u32, u32) { 99 | let tex = 100 | Self::texturize_text_if_needed(resources, text); 101 | let TextureQuery{ width, height, ..} = 102 | tex 103 | .query(); 104 | (width, height) 105 | } 106 | 107 | 108 | /// Draw some text, returns the destination Rect where the 109 | /// text was drawn. 110 | pub fn draw_text( 111 | canvas: &mut WindowCanvas, 112 | resources: &'res mut Sdl2Resources<'ctx>, 113 | pos: &V2, 114 | text: &Text 115 | ) -> Rect { 116 | let tex = 117 | Self::texturize_text_if_needed(resources, text); 118 | let TextureQuery{ width, height, ..} = 119 | tex 120 | .query(); 121 | let dest = 122 | Rect::new( 123 | pos.x as i32, 124 | pos.y as i32, 125 | width, height 126 | ); 127 | render::draw_sprite( 128 | canvas, 129 | Rect::new(0, 0, width, height), 130 | dest, 131 | false, false, false, 132 | &tex 133 | ); 134 | dest 135 | } 136 | 137 | } 138 | -------------------------------------------------------------------------------- /old_gods/src/systems/script/mod.rs: -------------------------------------------------------------------------------- 1 | use serde_json::Value; 2 | use specs::prelude::*; 3 | use std::collections::HashMap; 4 | use std::marker::PhantomData; 5 | 6 | use super::super::components::{Action, Exile, Inventory, Name, Sprite}; 7 | //mod container; 8 | mod door; 9 | 10 | use container::Container; 11 | use door::Door; 12 | 13 | 14 | /// A script component gives the entity special behavior without adding any 15 | /// new data. 16 | #[derive(Debug, Clone)] 17 | pub enum Script { 18 | /// The entity is a sprite that acts like a container, being able to be opened, 19 | /// closed and looted. 20 | Container, 21 | 22 | /// The entity is a sprite that acts like a door, being able to be opened and 23 | /// closed - changing the barriers within it. 24 | Door, 25 | 26 | /// Some other script that will be taken care of by another system 27 | Other { 28 | /// The name of this script 29 | name: String, 30 | 31 | /// Any special properties this script may have 32 | properties: HashMap, 33 | }, 34 | } 35 | 36 | 37 | impl Script { 38 | pub fn tiled_key() -> String { 39 | "script".to_string() 40 | } 41 | 42 | pub fn from_str( 43 | s: &str, 44 | props: Option>, 45 | ) -> Result { 46 | match s { 47 | "container" => Ok(Script::Container), 48 | "door" => Ok(Script::Door), 49 | "" => Err("Object script may not be empty".to_string()), 50 | s => Ok(Script::Other { 51 | name: s.to_string(), 52 | properties: props.unwrap_or(HashMap::new()), 53 | }), 54 | } 55 | } 56 | 57 | /// Return the contained string in the "Other" case, if possible. 58 | pub fn other_string(&self) -> Option<&String> { 59 | self.other().map(|(n, _)| n) 60 | } 61 | 62 | /// Return the other script if possible 63 | pub fn other(&self) -> Option<(&String, &HashMap)> { 64 | match self { 65 | Script::Other { name, properties } => Some((name, properties)), 66 | _ => None, 67 | } 68 | } 69 | } 70 | 71 | 72 | impl Component for Script { 73 | type Storage = HashMapStorage; 74 | } 75 | 76 | 77 | trait ScriptFromKey 78 | where 79 | Self: std::any::Any + Sized 80 | { 81 | type Storage; 82 | 83 | fn insert(key: &str) -> Option; 84 | } 85 | 86 | 87 | pub struct ScriptSystem { 88 | phantom: PhantomData 89 | } 90 | 91 | 92 | impl ScriptSystem { 93 | pub fn new() -> Self { 94 | ScriptSystem { 95 | phantom: PhantomData 96 | } 97 | } 98 | } 99 | 100 | 101 | impl<'a, S:ScriptFromKey> System<'a> for ScriptSystem { 102 | type SystemData = ( 103 | Entities<'a>, 104 | Read<'a, LazyUpdate>, 105 | WriteStorage<'a, Script>, 106 | ); 107 | 108 | fn run( 109 | &mut self, 110 | ( 111 | entities, 112 | lazy, 113 | mut scripts, 114 | ): Self::SystemData, 115 | ) { 116 | for (ent, script, sprite, ()) in 117 | (&entities, &scripts, &sprites, !&exiles).join() 118 | { 119 | match script { 120 | Script::Container => { 121 | Container::run( 122 | &actions, 123 | &entities, 124 | ent, 125 | &inventories, 126 | &lazy, 127 | &names, 128 | sprite, 129 | ); 130 | } 131 | 132 | Script::Door => { 133 | Door::run(&actions, &entities, ent, &lazy, sprite); 134 | } 135 | 136 | Script::Other { name, .. } => { 137 | println!("Seeing sprite with script {:?}", name); 138 | } 139 | } 140 | } 141 | 142 | for (_ent, script, ()) in (&entities, &scripts, !&exiles).join() { 143 | match script { 144 | Script::Other { name, .. } => { 145 | println!("Seeing object with script {:?}", name); 146 | } 147 | _ => {} 148 | } 149 | } 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /old_gods/src/sound.rs: -------------------------------------------------------------------------------- 1 | use super::prelude::{Callbacks, LoadStatus, LoadableResources, Resources, SharedResource}; 2 | use log::trace; 3 | use std::collections::HashMap; 4 | use wasm_bindgen::{ 5 | prelude::{Closure, JsValue}, 6 | JsCast, 7 | }; 8 | use web_sys::{window, AudioContext, EventTarget, HtmlAudioElement, MediaElementAudioSourceNode}; 9 | 10 | 11 | pub struct SoundBlaster { 12 | context: AudioContext, 13 | resources: LoadableResources, 14 | tracks: HashMap, 15 | } 16 | 17 | 18 | impl SoundBlaster { 19 | pub fn new() -> Self { 20 | SoundBlaster { 21 | context: AudioContext::new().expect("Could not create an AudioContext"), 22 | resources: LoadableResources::new(), 23 | tracks: HashMap::new(), 24 | } 25 | } 26 | } 27 | 28 | 29 | impl Default for SoundBlaster { 30 | fn default() -> Self { 31 | Self::new() 32 | } 33 | } 34 | 35 | 36 | impl Resources for SoundBlaster { 37 | fn status_of(&self, s: &str) -> LoadStatus { 38 | self.resources.status_of(s) 39 | } 40 | 41 | fn load(&mut self, path: &str) { 42 | trace!("loading sound: {}", path); 43 | 44 | let audio_element = window() 45 | .expect("no window") 46 | .document() 47 | .expect("no document") 48 | .create_element("audio") 49 | .expect("can't create audio element") 50 | .dyn_into::() 51 | .expect("can't coerce audio element"); 52 | audio_element.set_src(path); 53 | 54 | // https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Using_Web_Audio_API 55 | let track = self 56 | .context 57 | .create_media_element_source(&audio_element) 58 | .unwrap(); 59 | track 60 | .connect_with_audio_node(&self.context.destination()) 61 | .unwrap(); 62 | self.tracks.insert(path.to_string(), track); 63 | 64 | let rsrc = SharedResource::default(); 65 | rsrc.set_status_and_resource((LoadStatus::Started, Some(audio_element.clone()))); 66 | 67 | let load_rsrc = rsrc.clone(); 68 | let load = Closure::wrap(Box::new(move |_: JsValue| { 69 | load_rsrc.set_status(LoadStatus::Complete); 70 | }) as Box); 71 | 72 | let err_rsrc = rsrc.clone(); 73 | let err_path = path.to_string(); 74 | let err = Closure::wrap(Box::new(move |event: JsValue| { 75 | trace!("error event: {:#?}", event); 76 | let event = event 77 | .dyn_into::() 78 | .expect("Error is not an Event"); 79 | let msg = format!("failed loading {}: {}", &err_path, event.type_()); 80 | trace!(" loading {} erred: {}", &err_path, &msg); 81 | err_rsrc.set_status_and_resource((LoadStatus::Error(msg), None)); 82 | }) as Box); 83 | 84 | let target: &EventTarget = audio_element 85 | .dyn_ref() 86 | .expect("can't coerce img as EventTarget"); 87 | target 88 | .add_event_listener_with_callback("load", load.as_ref().unchecked_ref()) 89 | .unwrap(); 90 | target 91 | .add_event_listener_with_callback("error", err.as_ref().unchecked_ref()) 92 | .unwrap(); 93 | self.resources 94 | .callbacks 95 | .insert(path.to_string(), Callbacks::new(load, err)); 96 | self.resources.resources.insert(path.to_string(), rsrc); 97 | } 98 | 99 | fn take(&mut self, s: &str) -> Option> { 100 | self.resources.take(s) 101 | } 102 | 103 | fn put(&mut self, path: &str, sound: SharedResource) { 104 | self.resources.put(path, sound) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /old_gods/src/systems/player.rs: -------------------------------------------------------------------------------- 1 | /// Manages: 2 | /// * moving players based on their controllers' axes 3 | use log::{trace, warn}; 4 | use specs::prelude::*; 5 | 6 | use super::super::prelude::{Exile, MaxSpeed, Object, Player, PlayerControllers, Velocity, V2}; 7 | 8 | 9 | /// Players the movement and actions taken by characters. 10 | pub struct PlayerSystem; 11 | 12 | 13 | #[derive(SystemData)] 14 | pub struct PlayerSystemData<'a> { 15 | entities: Entities<'a>, 16 | player_controllers: Read<'a, PlayerControllers>, 17 | players: WriteStorage<'a, Player>, 18 | exiles: ReadStorage<'a, Exile>, 19 | max_speeds: ReadStorage<'a, MaxSpeed>, 20 | objects: WriteStorage<'a, Object>, 21 | velocities: WriteStorage<'a, Velocity>, 22 | } 23 | 24 | 25 | /// The PlayerSystem carries out motivations on behalf of toons. 26 | impl<'a> System<'a> for PlayerSystem { 27 | type SystemData = PlayerSystemData<'a>; 28 | 29 | fn run(&mut self, mut data: Self::SystemData) { 30 | // Find any objects with character types so we can create player components. 31 | let mut deletes = vec![]; 32 | for (ent, obj) in (&data.entities, &data.objects).join() { 33 | if let "character" = obj.type_is.as_ref() { 34 | let properties = obj.json_properties(); 35 | trace!("character {:#?}", obj); 36 | let scheme = properties 37 | .get("control") 38 | .map(|v| v.as_str().map(|s| s.to_string())) 39 | .flatten(); 40 | match scheme.as_deref() { 41 | Some("player") => { 42 | let ndx = properties 43 | .get("player_index") 44 | .expect( 45 | "Object must have a 'player_index' custom property for control.", 46 | ) 47 | .as_u64() 48 | .map(|u| u as usize) 49 | .expect("'player_index value must be an integer"); 50 | let _ = data.players.insert(ent, Player(ndx as u32)); 51 | } 52 | 53 | Some("npc") => { 54 | panic!("TODO: NPC support"); 55 | } 56 | 57 | None => { 58 | panic!("character object must have a 'control' property"); 59 | } 60 | 61 | Some(scheme) => { 62 | warn!("unsupported character control scheme '{}'", scheme); 63 | } 64 | } 65 | 66 | let _ = data.velocities.insert(ent, Velocity(V2::origin())); 67 | deletes.push(ent); 68 | } 69 | } 70 | deletes.into_iter().for_each(|ent| { 71 | let _ = data.objects.remove(ent); 72 | }); 73 | 74 | // Run over all players and enforce their motivations. 75 | let joints: Vec<_> = (&data.entities, &data.players, !&data.exiles) 76 | .join() 77 | .map(|(ep, p, ())| (ep, p.clone())) 78 | .collect(); 79 | for (ent, player) in joints.into_iter() { 80 | let v = data 81 | .velocities 82 | .get_mut(ent) 83 | .unwrap_or_else(|| panic!("Player {:?} does not have velocity.", player)); 84 | 85 | let max_speed: MaxSpeed = data.max_speeds.get(ent).cloned().unwrap_or(MaxSpeed(100.0)); 86 | 87 | // Get the player's controller on the map 88 | data.player_controllers.with_map_ctrl_at(player.0, |ctrl| { 89 | // Update the velocity of the toon based on the 90 | // player's controller 91 | let ana = ctrl.analog_rate(); 92 | let rate = ana.unitize().unwrap_or_else(|| V2::new(0.0, 0.0)); 93 | let mult = rate.scalar_mul(max_speed.0); 94 | v.0 = mult; 95 | }); 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /old_gods/src/components/path.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Clone)] 2 | pub enum PathComponent { 3 | /// The entity has a Name 4 | HasName(String), 5 | 6 | /// The entity is an Effect 7 | IsEffect 8 | } 9 | 10 | #[derive(Debug, Clone)] 11 | pub enum PathSpace { 12 | /// The entity lives within an inventory. Get the inventory by its Name. 13 | Inventory(String, Vec), 14 | } 15 | 16 | 17 | /// An EntityPathComponent is a type that allows us to locate an entity by some 18 | /// number of components, nestings, etc. It is used to resolve pointers to 19 | /// entities created by map loading. 20 | #[derive(Debug, Clone)] 21 | pub struct EntityPath { 22 | space: PathSpace 23 | } 24 | 25 | 26 | pub struct EntityPathParser; 27 | 28 | 29 | impl EntityPathParser { 30 | pub fn name(i: &str) -> IResult<&str, PathComponent> { 31 | let (i, _) = tag("name")(i)?; 32 | let (i, _) = multispace1(i)?; 33 | let (i, s) = string(i)?; 34 | Ok((i, PathComponent::HasName(s))) 35 | } 36 | 37 | pub fn is_effect(i: &str) -> IResult<&str, PathComponent> { 38 | let (i, _) = tag("is_effect")(i)?; 39 | Ok((i, PathComponent::IsEffect)) 40 | } 41 | 42 | pub fn component(i: &str) -> IResult<&str, PathComponent> { 43 | alt(( 44 | EntityPathParser::name, 45 | EntityPathParser::is_effect 46 | ))(i) 47 | } 48 | 49 | 50 | pub fn parse_path(i: &str) -> IResult<&str, EntityPath> { 51 | let (i, _) = tag("inventory")(i)?; 52 | let (i, _) = multispace0(i)?; 53 | let (i, (inv_name, comps)) = params2( 54 | string, 55 | vec(&EntityPathParser::component) 56 | )(i)?; 57 | Ok( 58 | (i, 59 | EntityPath{ 60 | space: PathSpace::Inventory(inv_name, comps) 61 | } 62 | )) 63 | } 64 | } 65 | 66 | 67 | impl EntityPath { 68 | /// Parses an EntityPath from a string. 69 | pub fn from_str(input: &str) -> Result> { 70 | let result = EntityPathParser::parse_path(input); 71 | result 72 | .map(|(_, e)| e) 73 | } 74 | 75 | /// Resolve an EntityPath, retreiving an Entity, if possible. 76 | pub fn resolve(&self, world: &World) -> Vec { 77 | let get_inventory_items = 78 | |s: &String| -> Vec { 79 | let entities = world.entities(); 80 | let inventories = world.read_storage::(); 81 | let names = world.read_storage::(); 82 | (&entities, &inventories, &names) 83 | .join() 84 | .filter_map(|(_, inventory, name)| { 85 | if name.0 == *s { 86 | Some(inventory.items.clone()) 87 | } else { 88 | None 89 | } 90 | }) 91 | .flatten() 92 | .collect() 93 | }; 94 | match &self.space { 95 | PathSpace::Inventory(s, comps) => { 96 | if comps.is_empty() { 97 | get_inventory_items(s) 98 | } else { 99 | get_inventory_items(s) 100 | .into_iter() 101 | .filter(|ent: &Entity| { 102 | // The ent must have every comp 103 | for comp in comps { 104 | match comp { 105 | PathComponent::IsEffect => { 106 | let effects = world.read_storage::(); 107 | if effects.get(*ent).is_none() { 108 | return false; 109 | } 110 | } 111 | 112 | PathComponent::HasName(name) => { 113 | let names = world.read_storage::(); 114 | if let Some(Name(item_name)) = names.get(*ent) { 115 | if *item_name != *name { 116 | return false; 117 | } 118 | } 119 | } 120 | } 121 | } 122 | true 123 | }) 124 | .collect::>() 125 | } 126 | } 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /old_gods/src/systems/animation.rs: -------------------------------------------------------------------------------- 1 | use specs::prelude::*; 2 | 3 | //use super::rendering::Rendering; 4 | use super::super::{ 5 | components::{Name, Rendering}, 6 | time::FPSCounter, 7 | }; 8 | 9 | 10 | /// One frame's worth of an Animation 11 | #[derive(Debug, Clone, PartialEq)] 12 | pub struct Frame { 13 | pub rendering: Rendering, 14 | pub duration: f32, 15 | } 16 | 17 | 18 | /// A collection of frames, durations and state necessary for 19 | /// animating tiles. 20 | #[derive(Debug, Clone, PartialEq)] 21 | pub struct Animation { 22 | pub frames: Vec, 23 | pub current_frame_index: usize, 24 | pub current_frame_progress: f32, 25 | pub is_playing: bool, 26 | pub should_repeat: bool, 27 | } 28 | 29 | impl Animation { 30 | pub fn step(&mut self, dt: f32) { 31 | // Early exit if the animation is not playing. 32 | if !self.is_playing { 33 | return; 34 | } 35 | 36 | self.current_frame_progress += dt; 37 | 'inc_frame: loop { 38 | if let Some(frame) = self.frames.get(self.current_frame_index) { 39 | if frame.duration <= self.current_frame_progress { 40 | self.current_frame_index += 1; 41 | if self.current_frame_index >= self.frames.len() { 42 | if self.should_repeat { 43 | self.current_frame_index = 0; 44 | } else { 45 | self.is_playing = false; 46 | break 'inc_frame; 47 | } 48 | } 49 | self.current_frame_progress -= frame.duration; 50 | } else { 51 | break 'inc_frame; 52 | } 53 | } 54 | } 55 | } 56 | 57 | 58 | pub fn get_current_frame(&self) -> Option<&Frame> { 59 | self.frames.get(self.current_frame_index) 60 | } 61 | 62 | 63 | pub fn stop(&mut self) { 64 | self.is_playing = false; 65 | } 66 | 67 | 68 | pub fn play(&mut self) { 69 | self.is_playing = true; 70 | } 71 | 72 | 73 | pub fn seek_to(&mut self, ndx: usize) -> bool { 74 | if ndx < self.frames.len() { 75 | self.current_frame_index = ndx; 76 | true 77 | } else { 78 | false 79 | } 80 | } 81 | 82 | pub fn has_ended(&self) -> bool { 83 | self.get_current_frame().is_none() 84 | } 85 | } 86 | 87 | 88 | impl Component for Animation { 89 | type Storage = HashMapStorage; 90 | } 91 | 92 | 93 | /// The animation system controls stepping any tiled animations. 94 | pub struct AnimationSystem; 95 | 96 | impl<'a> System<'a> for AnimationSystem { 97 | type SystemData = ( 98 | Read<'a, FPSCounter>, 99 | Entities<'a>, 100 | WriteStorage<'a, Animation>, 101 | ReadStorage<'a, Name>, 102 | WriteStorage<'a, Rendering>, 103 | ); 104 | 105 | fn run(&mut self, (fps, entities, mut animation, names, mut renderings): Self::SystemData) { 106 | // Find any animations that don't yet have renderings 107 | let mut frameless_animes = vec![]; 108 | for (ent, ani, _) in (&entities, &animation, !&renderings).join() { 109 | if let Some(frame) = ani.get_current_frame() { 110 | // Add the rendering 111 | println!("Adding rendering for animation {:?}", names.get(ent)); 112 | frameless_animes.push((ent, frame)); 113 | } 114 | } 115 | 116 | for (e, f) in frameless_animes { 117 | renderings 118 | .insert(e, f.rendering.clone()) 119 | .expect("Could not insert rendering for a frameless animation."); 120 | } 121 | 122 | // Progress any animations. 123 | for (ani, rndr) in (&mut animation, &mut renderings).join() { 124 | ani.step(fps.last_delta()); 125 | if let Some(frame) = ani.get_current_frame() { 126 | *rndr = frame.rendering.clone(); 127 | } 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /examples/loading-maps/maps/audio_test.json: -------------------------------------------------------------------------------- 1 | { "compressionlevel":-1, 2 | "editorsettings": 3 | { 4 | "export": 5 | { 6 | "target":"." 7 | } 8 | }, 9 | "height":9, 10 | "infinite":false, 11 | "layers":[ 12 | { 13 | "data":[42, 42, 42, 42, 44, 42, 42, 42, 46, 43, 42, 42, 42, 42, 42, 43, 42, 42, 42, 42, 42, 42, 42, 42, 45, 42, 42, 46, 46, 42, 47, 47, 47, 42, 42, 42, 42, 42, 42, 47, 48, 47, 42, 42, 42, 42, 43, 44, 47, 47, 47, 42, 42, 42, 42, 46, 45, 42, 42, 46, 42, 42, 42, 42, 42, 42, 42, 42, 46, 42, 42, 42, 42, 42, 44, 42, 42, 42, 42, 43, 42], 14 | "height":9, 15 | "id":1, 16 | "name":"bg", 17 | "opacity":1, 18 | "type":"tilelayer", 19 | "visible":true, 20 | "width":9, 21 | "x":0, 22 | "y":0 23 | }, 24 | { 25 | "draworder":"topdown", 26 | "id":2, 27 | "name":"objects", 28 | "objects":[ 29 | { 30 | "gid":765, 31 | "height":48, 32 | "id":1, 33 | "name":"hero", 34 | "properties":[ 35 | { 36 | "name":"control", 37 | "type":"string", 38 | "value":"player" 39 | }, 40 | { 41 | "name":"inventory_name", 42 | "type":"string", 43 | "value":"hero_inv" 44 | }, 45 | { 46 | "name":"player_index", 47 | "type":"int", 48 | "value":0 49 | }, 50 | { 51 | "name":"toggle_rendering_positions", 52 | "type":"bool", 53 | "value":false 54 | }], 55 | "rotation":0, 56 | "type":"character", 57 | "visible":true, 58 | "width":48, 59 | "x":102.717887967507, 60 | "y":350.191233711288 61 | }, 62 | { 63 | "height":141.831105093925, 64 | "id":6, 65 | "name":"sound", 66 | "properties":[ 67 | { 68 | "name":"autoplay", 69 | "type":"bool", 70 | "value":true 71 | }, 72 | { 73 | "name":"file", 74 | "type":"string", 75 | "value":"sounds\/fountain.ogg" 76 | }, 77 | { 78 | "name":"loop", 79 | "type":"bool", 80 | "value":true 81 | }, 82 | { 83 | "name":"on_map", 84 | "type":"bool", 85 | "value":true 86 | }, 87 | { 88 | "name":"volume", 89 | "type":"float", 90 | "value":1 91 | }], 92 | "rotation":0, 93 | "type":"sound", 94 | "visible":true, 95 | "width":141.831105093925, 96 | "x":145.121001861567, 97 | "y":145.486545946861 98 | }], 99 | "opacity":1, 100 | "type":"objectgroup", 101 | "visible":true, 102 | "x":0, 103 | "y":0 104 | }], 105 | "nextlayerid":4, 106 | "nextobjectid":7, 107 | "orientation":"orthogonal", 108 | "renderorder":"right-down", 109 | "tiledversion":"1.3.1", 110 | "tileheight":48, 111 | "tilesets":[ 112 | { 113 | "firstgid":1, 114 | "source":"tilesets\/terrain.json" 115 | }, 116 | { 117 | "firstgid":761, 118 | "source":"tilesets\/heroes.json" 119 | }], 120 | "tilewidth":48, 121 | "type":"map", 122 | "version":1.2, 123 | "width":9 124 | } -------------------------------------------------------------------------------- /examples/loading-maps/src/components/inventory.rs: -------------------------------------------------------------------------------- 1 | use super::super::systems::looting::Loot; 2 | use old_gods::prelude::{Component, HashMapStorage, OriginOffset, Rendering, Shape}; 3 | use std::{f32::consts::PI, slice::Iter}; 4 | 5 | 6 | /// An entity with an item component can be kept in an inventory. 7 | #[derive(Debug, Clone, PartialEq)] 8 | pub struct Item { 9 | /// The name of this item. 10 | pub name: String, 11 | 12 | /// Whether or not this item is usable by itself. 13 | pub usable: bool, 14 | 15 | /// If this item can be stacked the Option type holds 16 | /// the count of the stack. 17 | pub stack: Option, 18 | 19 | /// How to render this item. 20 | pub rendering: Rendering, 21 | 22 | /// The shape of the item. 23 | pub shape: Shape, 24 | 25 | /// An origin, if it exists. 26 | pub offset: Option, 27 | 28 | /// Is this a barrier? 29 | pub is_barrier: bool, 30 | } 31 | 32 | 33 | impl Component for Item { 34 | type Storage = HashMapStorage; 35 | } 36 | 37 | 38 | const ITEM_PLACEMENTS: [f32; 16] = [ 39 | 0.0, 40 | PI / 2.0, 41 | PI, 42 | 3.0 * PI / 2.0, 43 | PI / 4.0, 44 | 3.0 * PI / 4.0, 45 | 5.0 * PI / 4.0, 46 | 7.0 * PI / 4.0, 47 | PI / 6.0, 48 | PI / 3.0, 49 | 2.0 * PI / 3.0, 50 | 5.0 * PI / 6.0, 51 | 7.0 * PI / 6.0, 52 | 4.0 * PI / 3.0, 53 | 5.0 * PI / 3.0, 54 | 11.0 * PI / 6.0, 55 | ]; 56 | 57 | /// # Inventory 58 | /// An inventory is a container of items. 59 | #[derive(Debug, Clone)] 60 | pub struct Inventory { 61 | /// The items that are inside this inventory. 62 | /// The inventory is a grid of items. 63 | items: Vec, 64 | 65 | /// A place to store the next angle to use for throwing an item out 66 | /// of the inventory. 67 | next_ejection_angle: u32, 68 | } 69 | 70 | 71 | impl Inventory { 72 | pub fn new(items: Vec) -> Inventory { 73 | Inventory { 74 | items, 75 | next_ejection_angle: 0, 76 | } 77 | } 78 | 79 | /// Dequeue the next item ejection angle. This is nice for 80 | /// a good item dropping effect. 81 | pub fn dequeue_ejection_in_radians(&mut self) -> f32 { 82 | let n = self.next_ejection_angle as usize; 83 | self.next_ejection_angle += 1; 84 | 85 | ITEM_PLACEMENTS[n % ITEM_PLACEMENTS.len()] 86 | } 87 | 88 | /// Add the item, stacking it in an available stack if possible. 89 | pub fn add_item(&mut self, item: Item) { 90 | if item.stack.is_some() { 91 | for prev_item in self.items.iter_mut() { 92 | if prev_item.stack.is_some() && prev_item.name == item.name { 93 | let stack = prev_item.stack.as_mut().unwrap(); 94 | *stack += item.stack.unwrap_or(1); 95 | return; 96 | } 97 | } 98 | } else { 99 | self.items.push(item); 100 | } 101 | } 102 | 103 | /// An iterator over the items. 104 | pub fn item_at_xy(&self, x: i32, y: i32) -> Option<&Item> { 105 | self.items.get(y as usize * Loot::COLS + x as usize) 106 | } 107 | 108 | /// Remove the item at the given index. 109 | pub fn remove(&mut self, ndx: usize) -> Option { 110 | if ndx < self.items.len() { 111 | Some(self.items.remove(ndx)) 112 | } else { 113 | None 114 | } 115 | } 116 | 117 | /// Remove the item at the given x and y 118 | pub fn remove_xy(&mut self, x: usize, y: usize) -> Option { 119 | let ndx = y * Loot::COLS + x; 120 | self.remove(ndx) 121 | } 122 | 123 | /// The number of items in the inventory. 124 | pub fn item_len(&self) -> usize { 125 | self.items.len() 126 | } 127 | 128 | /// Replace all the items in the inventory. 129 | /// Returns the old items. 130 | pub fn replace_items(&mut self, items: Vec) -> Vec { 131 | std::mem::replace(&mut self.items, items) 132 | } 133 | 134 | /// An iterator over all items. 135 | pub fn item_iter(&self) -> Iter { 136 | self.items.iter() 137 | } 138 | } 139 | 140 | 141 | impl Component for Inventory { 142 | type Storage = HashMapStorage; 143 | } 144 | -------------------------------------------------------------------------------- /old_gods/src/components.rs: -------------------------------------------------------------------------------- 1 | use serde_json::Value; 2 | use specs::prelude::{ 3 | Component, Entities, Entity, HashMapStorage, Join, ReadStorage, VecStorage, World, WorldExt, 4 | WriteStorage, 5 | }; 6 | use std::collections::HashMap; 7 | 8 | pub use super::{ 9 | geom::*, 10 | systems::{ 11 | animation::Animation, 12 | fence::{Fence, StepFence}, 13 | physics::{Barrier, Position, Velocity}, 14 | }, 15 | }; 16 | //pub use super::systems::sound::{Music, Sound}; 17 | pub use super::systems::{ 18 | tween::{Easing, Tween, TweenParam}, 19 | zone::Zone, 20 | }; 21 | 22 | mod cardinal; 23 | pub use cardinal::*; 24 | 25 | mod exile; 26 | pub use exile::*; 27 | 28 | mod font_details; 29 | pub use font_details::*; 30 | 31 | mod player; 32 | pub use player::*; 33 | 34 | mod rendering; 35 | pub use rendering::*; 36 | 37 | mod sprite; 38 | pub use sprite::*; 39 | 40 | pub mod tiled; 41 | pub use tiled::{Object, Property, Tiledmap}; 42 | 43 | 44 | /// One of the simplest and most common components. 45 | /// Anything that can be identified by a name. 46 | #[derive(Debug, Clone, PartialEq)] 47 | pub struct Name(pub String); 48 | 49 | 50 | impl Component for Name { 51 | type Storage = VecStorage; 52 | } 53 | 54 | 55 | /// Find a component and entity by another component 56 | pub fn find_by(world: &World, a: &A) -> Option<(Entity, B)> 57 | where 58 | A: Component + PartialEq, 59 | B: Component + Clone, 60 | { 61 | let a_store = world.read_storage::(); 62 | let b_store = world.read_storage::(); 63 | let ents = world.entities(); 64 | for (e, a_ref, b_ref) in (&ents, &a_store, &b_store).join() { 65 | if *a_ref == *a { 66 | let b = (*b_ref).clone(); 67 | return Some((e, b)); 68 | } 69 | } 70 | None 71 | } 72 | 73 | /// Allows `get` and `contains` on read or write storages. 74 | pub trait GetStorage { 75 | fn get(&self, e: Entity) -> Option<&T>; 76 | 77 | fn contains(&self, e: Entity) -> bool; 78 | } 79 | 80 | 81 | impl<'a, T: Component> GetStorage for WriteStorage<'a, T> { 82 | fn get(&self, e: Entity) -> Option<&T> { 83 | self.get(e) 84 | } 85 | 86 | fn contains(&self, e: Entity) -> bool { 87 | self.contains(e) 88 | } 89 | } 90 | 91 | 92 | impl<'a, T: Component> GetStorage for ReadStorage<'a, T> { 93 | fn get(&self, e: Entity) -> Option<&T> { 94 | self.get(e) 95 | } 96 | 97 | fn contains(&self, e: Entity) -> bool { 98 | self.contains(e) 99 | } 100 | } 101 | 102 | 103 | /// Returns the position/current location of the entity, offset by any 104 | /// OriginOffset or barrier center it may also have. 105 | pub fn entity_location(ent: Entity, positions: &P, origins: &O) -> Option 106 | where 107 | P: GetStorage, 108 | O: GetStorage, 109 | { 110 | let pos = positions.get(ent).map(|p| p.0); 111 | let origin = origins.get(ent).map(|o| o.0).unwrap_or_else(V2::origin); 112 | pos.map(|p| p + origin) 113 | } 114 | 115 | 116 | /// Returns the entities origin offset or barrier center. 117 | pub fn entity_local_origin(ent: Entity, shapes: &S, origins: &O) -> V2 118 | where 119 | S: GetStorage, 120 | O: GetStorage, 121 | { 122 | origins 123 | .get(ent) 124 | .map(|o| o.0) 125 | .or_else(|| 126 | // try to locate a shape - if it has a shape we will consider 127 | // the center of its aabb as the origin offset. 128 | shapes.get(ent).map(|s| s.aabb().center())) 129 | .unwrap_or_else(V2::origin) 130 | } 131 | 132 | 133 | /// Used for testing the number of entities before and after a function is run. 134 | pub fn with_ent_counts(entities: &Entities, mut f: F, g: G) { 135 | let before_entity_count = &entities.join().fold(0, |n, _| n + 1); 136 | f(); 137 | let after_entity_count = &entities.join().fold(0, |n, _| n + 1); 138 | g(*before_entity_count, *after_entity_count); 139 | } 140 | 141 | 142 | /// A component that stores any unused JSON propreties left on an object. 143 | #[derive(Debug, Clone)] 144 | pub struct JSON(pub HashMap); 145 | 146 | 147 | impl Component for JSON { 148 | type Storage = HashMapStorage; 149 | } 150 | -------------------------------------------------------------------------------- /old_gods/src/resource_manager.rs: -------------------------------------------------------------------------------- 1 | //use sdl2::image::LoadTexture; 2 | //use sdl2::video::WindowContext; 3 | //use sdl2::render::{TextureCreator, Texture}; 4 | //use sdl2::ttf::{Font, Sdl2TtfContext}; 5 | 6 | use std::borrow::Borrow; 7 | use std::collections::HashMap; 8 | use std::hash::Hash; 9 | use std::fmt::Debug; 10 | 11 | 12 | /// Generic trait to Load any Resource Kind 13 | pub trait ResourceLoader<'l, R> { 14 | type Args: ?Sized; 15 | fn load(&'l self, data: &Self::Args) -> Result; 16 | } 17 | 18 | 19 | /// TextureCreator knows how to load Textures 20 | impl<'l, T> ResourceLoader<'l, Texture<'l>> for TextureCreator { 21 | type Args = str; 22 | fn load(&'l self, path: &str) -> Result { 23 | println!("Loading a texture: {:?}", path); 24 | self.load_texture(path) 25 | } 26 | } 27 | 28 | 29 | /// Font Context knows how to load Fonts 30 | impl<'l> ResourceLoader<'l, Font<'l, 'static>> for Sdl2TtfContext { 31 | type Args = FontDetails; 32 | fn load(&'l self, details: &FontDetails) -> Result, String> { 33 | println!("Loading a font: {:?}", details); 34 | self.load_font(&details.path, details.size) 35 | } 36 | } 37 | 38 | 39 | /// Generic struct to cache any resource loaded by a ResourceLoader 40 | pub struct ResourceManager<'l, K, R, L> 41 | where 42 | K: Hash + Eq, 43 | L: 'l + ResourceLoader<'l, R> 44 | { 45 | loader: &'l L, 46 | cache: HashMap, 47 | } 48 | 49 | impl<'l, K, R, L> ResourceManager<'l, K, R, L> 50 | where 51 | K: Hash + Eq, 52 | L: ResourceLoader<'l, R> 53 | { 54 | pub fn new(loader: &'l L) -> Self { 55 | ResourceManager { 56 | cache: HashMap::new(), 57 | loader: loader, 58 | } 59 | } 60 | 61 | pub fn get_cache(&self) -> &HashMap { 62 | &self.cache 63 | } 64 | 65 | /// Generics magic to allow a HashMap to use String as a key 66 | /// while allowing it to use &str for gets 67 | pub fn load(&mut self, details: &D) -> Result<&R, String> 68 | where 69 | L: ResourceLoader<'l, R, Args = D>, 70 | D: Debug + Eq + Hash + ?Sized, 71 | K: Borrow + for<'a> From<&'a D> 72 | { 73 | let key = 74 | details 75 | .into(); 76 | if !self.cache.contains_key(key) { 77 | let resource = 78 | self 79 | .loader 80 | .load(details)?; 81 | self 82 | .cache 83 | .insert(details.into(), resource); 84 | } 85 | self 86 | .cache 87 | .get(key) 88 | .ok_or(format!("Could not find resource {:?}", details)) 89 | } 90 | 91 | /// Take a texture from the manager. 92 | pub fn take_resource(&mut self, details: &D) -> Result 93 | where 94 | L: ResourceLoader<'l, R, Args = D>, 95 | D: Debug + Eq + Hash + ?Sized, 96 | K: Borrow + for<'a> From<&'a D> 97 | { 98 | self 99 | .cache 100 | .remove(details.into()) 101 | .ok_or(format!("Could not find resource {:?}", details)) 102 | } 103 | 104 | /// Give a texture to the manager. 105 | pub fn put_resource(&mut self, details: &D, resource: R) 106 | where 107 | L: ResourceLoader<'l, R, Args = D>, 108 | D: Debug + Eq + Hash + ?Sized, 109 | K: Borrow + for<'a> From<&'a D> 110 | { 111 | self 112 | .cache 113 | .insert(details.into(), resource); 114 | } 115 | } 116 | 117 | 118 | pub type TextureManager<'l, T> = ResourceManager<'l, String, Texture<'l>, TextureCreator>; 119 | 120 | 121 | pub type FontManager<'l> = ResourceManager<'l, FontDetails, Font<'l, 'static>, Sdl2TtfContext>; 122 | 123 | 124 | pub struct Sdl2Resources<'l> { 125 | pub texture_creator: &'l TextureCreator, 126 | pub texture_manager: TextureManager<'l, WindowContext>, 127 | pub font_manager: FontManager<'l>, 128 | pub font_directory: String 129 | } 130 | 131 | 132 | impl<'l> Sdl2Resources<'l> { 133 | pub fn new( 134 | texture_creator: &'l TextureCreator, 135 | ttf_ctx: &'l Sdl2TtfContext, 136 | font_directory: &str 137 | ) -> Sdl2Resources<'l> { 138 | // Create the texture manager 139 | let texture_manager = 140 | TextureManager::new(texture_creator); 141 | // Create the font manager 142 | let font_manager = 143 | FontManager::new(ttf_ctx); 144 | 145 | Sdl2Resources { 146 | texture_creator, 147 | texture_manager, 148 | font_manager, 149 | font_directory: font_directory.to_string() 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /old_gods/src/systems/tween.rs: -------------------------------------------------------------------------------- 1 | /// The TweenSystem applies affine transformations to entities over time. Be careful, it will 2 | /// overwrite your component values if that's the thing its tweening. 3 | use specs::prelude::*; 4 | 5 | use super::super::{ 6 | prelude::{Position, Velocity, V2}, 7 | time::FPSCounter, 8 | }; 9 | 10 | 11 | /// The thing that's being tweened. 12 | #[derive(Debug, Clone)] 13 | pub enum TweenParam { 14 | Position(V2, V2), 15 | Velocity(V2, V2), 16 | } 17 | 18 | 19 | /// The easing function being used to tween a subject. 20 | #[derive(Debug, Clone)] 21 | pub enum Easing { 22 | Linear, 23 | } 24 | 25 | 26 | impl Easing { 27 | /// t b c d 28 | /// `t` is current time 29 | /// `b` is the start value 30 | /// `c` is the total change in value 31 | /// `d` is the duration 32 | pub fn tween(&self, t: f32, b: f32, c: f32, d: f32) -> f32 { 33 | match self { 34 | Easing::Linear => c * t / d + b, 35 | } 36 | } 37 | } 38 | 39 | 40 | #[derive(Debug, Clone)] 41 | pub struct Tween { 42 | pub subject: Entity, 43 | pub param: TweenParam, 44 | pub easing: Easing, 45 | pub dt: f32, 46 | pub duration: f32, 47 | } 48 | 49 | 50 | impl Tween { 51 | pub fn new(subject: Entity, param: TweenParam, easing: Easing, duration: f32) -> Self { 52 | Tween { 53 | subject, 54 | param, 55 | easing, 56 | duration, 57 | dt: 0.0, 58 | } 59 | } 60 | } 61 | 62 | 63 | impl Component for Tween { 64 | type Storage = HashMapStorage; 65 | } 66 | 67 | 68 | /// Helper function for tweening an entity. 69 | pub fn tween( 70 | entities: &Entities, 71 | subject: Entity, 72 | lazy: &LazyUpdate, 73 | param: TweenParam, 74 | easing: Easing, 75 | duration: f32, // in seconds 76 | ) { 77 | let _ = lazy 78 | .create_entity(&entities) 79 | .with(Tween { 80 | subject, 81 | param, 82 | easing, 83 | duration, 84 | dt: 0.0, 85 | }) 86 | .build(); 87 | } 88 | 89 | 90 | pub struct TweenSystem; 91 | 92 | 93 | #[derive(SystemData)] 94 | pub struct TweenSystemData<'a> { 95 | entities: Entities<'a>, 96 | fps: Read<'a, FPSCounter>, 97 | lazy: Read<'a, LazyUpdate>, 98 | positions: WriteStorage<'a, Position>, 99 | tweens: WriteStorage<'a, Tween>, 100 | velocities: WriteStorage<'a, Velocity>, 101 | } 102 | 103 | 104 | impl<'a> System<'a> for TweenSystem { 105 | type SystemData = TweenSystemData<'a>; 106 | 107 | fn run(&mut self, mut data: TweenSystemData) { 108 | for (ent, mut tween) in (&data.entities, &mut data.tweens).join() { 109 | let delta = data.fps.last_delta(); 110 | tween.dt += delta; 111 | 112 | let tween_is_dead = tween.dt > tween.duration; 113 | if tween_is_dead { 114 | // This tween is done, remove it 115 | data.lazy.remove::(ent); 116 | } 117 | 118 | let tween_v2 = |v: &mut V2, start: V2, end: V2| { 119 | if tween_is_dead { 120 | *v = end; 121 | } else { 122 | v.x = tween 123 | .easing 124 | .tween(tween.dt, start.x, end.x - start.x, tween.duration); 125 | v.y = tween 126 | .easing 127 | .tween(tween.dt, start.y, end.y - start.y, tween.duration); 128 | } 129 | }; 130 | 131 | match tween.param.clone() { 132 | TweenParam::Position(start, end) => { 133 | tween_v2( 134 | &mut data 135 | .positions 136 | .get_mut(tween.subject) 137 | .expect("Trying to tween an entity without a position") 138 | .0, 139 | start, 140 | end, 141 | ); 142 | } 143 | TweenParam::Velocity(start, end) => { 144 | tween_v2( 145 | &mut data 146 | .velocities 147 | .get_mut(tween.subject) 148 | .expect("Trying to tween an entity without a velocity") 149 | .0, 150 | start, 151 | end, 152 | ); 153 | } 154 | } 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /old_gods/src/time.rs: -------------------------------------------------------------------------------- 1 | //! Because Instant::now() doesn't work on arch = wasm32. 2 | pub use std::time::Duration; 3 | #[cfg(not(target_arch = "wasm32"))] 4 | pub use std::time::Instant; 5 | #[cfg(target_arch = "wasm32")] 6 | use web_sys::window; 7 | 8 | #[derive(Clone, Copy, Debug)] 9 | pub struct Millis { 10 | #[cfg(target_arch = "wasm32")] 11 | millis: u32, 12 | 13 | #[cfg(not(target_arch = "wasm32"))] 14 | time: Instant, 15 | } 16 | 17 | 18 | #[cfg(target_arch = "wasm32")] 19 | impl Millis { 20 | pub fn now() -> Self { 21 | Millis { 22 | millis: window().unwrap().performance().unwrap().now() as u32, 23 | } 24 | } 25 | 26 | pub fn millis_since(&self, then: Millis) -> u32 { 27 | self.millis - then.millis 28 | } 29 | } 30 | 31 | #[cfg(not(target_arch = "wasm32"))] 32 | impl Millis { 33 | pub fn now() -> Self { 34 | Millis { 35 | time: Instant::now(), 36 | } 37 | } 38 | 39 | pub fn millis_since(&self, then: Millis) -> u32 { 40 | self.time.duration_since(then.time).as_millis() as u32 41 | } 42 | } 43 | 44 | 45 | pub struct DurationMeasurement { 46 | start: Millis, 47 | } 48 | 49 | 50 | impl DurationMeasurement { 51 | pub fn starting_now() -> DurationMeasurement { 52 | DurationMeasurement { 53 | start: Millis::now(), 54 | } 55 | } 56 | 57 | 58 | pub fn millis_since_start(&self) -> u32 { 59 | Millis::now().millis_since(self.start) 60 | } 61 | } 62 | 63 | 64 | pub fn measure T>(f: F) -> (T, u32) { 65 | let m = DurationMeasurement::starting_now(); 66 | let t = f(); 67 | (t, m.millis_since_start()) 68 | } 69 | 70 | 71 | pub const FPS_COUNTER_BUFFER_SIZE: usize = 60; 72 | 73 | 74 | pub struct CounterBuffer { 75 | buffer: [T; FPS_COUNTER_BUFFER_SIZE], 76 | index: usize, 77 | } 78 | 79 | 80 | impl CounterBuffer { 81 | pub fn new(init: f32) -> Self { 82 | CounterBuffer { 83 | buffer: [init; FPS_COUNTER_BUFFER_SIZE], 84 | index: 0, 85 | } 86 | } 87 | 88 | pub fn write(&mut self, val: f32) { 89 | self.buffer[self.index] = val; 90 | self.index = (self.index + 1) % self.buffer.len(); 91 | } 92 | 93 | pub fn average(&self) -> f32 { 94 | self.buffer.iter().fold(0.0, |sum, dt| sum + dt) / self.buffer.len() as f32 95 | } 96 | 97 | pub fn current(&self) -> f32 { 98 | let last_index = if self.index == 0 { 99 | self.buffer.len() - 1 100 | } else { 101 | self.index - 1 102 | }; 103 | self.buffer[last_index] 104 | } 105 | 106 | pub fn frames(&self) -> &[f32; FPS_COUNTER_BUFFER_SIZE] { 107 | &self.buffer 108 | } 109 | } 110 | 111 | 112 | pub struct FPSCounter { 113 | counter: CounterBuffer, 114 | last_instant: Millis, 115 | last_dt: f32, 116 | averages: CounterBuffer, 117 | } 118 | 119 | 120 | impl FPSCounter { 121 | pub fn new() -> FPSCounter { 122 | FPSCounter { 123 | counter: CounterBuffer::new(0.0), 124 | last_instant: Millis::now(), 125 | last_dt: 0.0, 126 | averages: CounterBuffer::new(0.0), 127 | } 128 | } 129 | 130 | pub fn restart(&mut self) { 131 | self.last_instant = Millis::now(); 132 | } 133 | 134 | pub fn next_frame(&mut self) -> f32 { 135 | let this_instant = Millis::now(); 136 | let delta = this_instant.millis_since(self.last_instant); 137 | let dt_seconds = delta as f32 / 1000.0; 138 | self.last_dt = dt_seconds; 139 | self.last_instant = this_instant; 140 | self.counter.write(dt_seconds); 141 | if self.counter.index + 1 == FPS_COUNTER_BUFFER_SIZE { 142 | let avg = self.counter.average(); 143 | self.averages.write(avg); 144 | } 145 | dt_seconds 146 | } 147 | 148 | pub fn avg_frame_delta(&self) -> f32 { 149 | self.counter.average() 150 | } 151 | 152 | pub fn current_fps(&self) -> f32 { 153 | 1.0 / self.avg_frame_delta() 154 | } 155 | 156 | pub fn current_fps_string(&self) -> String { 157 | let avg = self.averages.current(); 158 | format!("{:.1}", 1.0 / avg) 159 | } 160 | 161 | /// Return the last frame's delta in seconds. 162 | pub fn last_delta(&self) -> f32 { 163 | self.last_dt 164 | } 165 | 166 | pub fn second_averages(&self) -> &[f32; FPS_COUNTER_BUFFER_SIZE] { 167 | self.averages.frames() 168 | } 169 | } 170 | 171 | impl Default for FPSCounter { 172 | fn default() -> FPSCounter { 173 | FPSCounter::new() 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /examples/loading-maps/src/components/action.rs: -------------------------------------------------------------------------------- 1 | use old_gods::{ 2 | parser::*, 3 | prelude::{Component, Entity, FlaggedStorage, HashMapStorage}, 4 | }; 5 | 6 | 7 | /// Encodes the strategies by which we evaluate an entity's elligibility to take 8 | /// an action. 9 | #[derive(Debug, Clone, PartialEq)] 10 | pub enum FitnessStrategy { 11 | /// The target must have an item with the matching name in their inventory. 12 | HasItem(String), 13 | 14 | /// the target must have an inventory. 15 | HasInventory, 16 | 17 | /// The target may pass any fitness test 18 | Any(Vec), 19 | 20 | /// The target must pass all fitness tests 21 | All(Vec), 22 | } 23 | 24 | 25 | impl FitnessStrategy { 26 | pub fn try_from_str(input: &str) -> Result { 27 | let result = FitnessStrategy::parse(input); 28 | result.map(|(_, f)| f).map_err(|e| format!("{}", e)) 29 | } 30 | 31 | /// Parse a HasItem 32 | fn has_item(i: &str) -> IResult<&str, FitnessStrategy> { 33 | let (i, _) = tag("has_item")(i)?; 34 | let (i, _) = multispace1(i)?; 35 | let (i, n) = string(i)?; 36 | Ok((i, FitnessStrategy::HasItem(n))) 37 | } 38 | 39 | /// Parse a HasInventory 40 | fn has_inventory(i: &str) -> IResult<&str, FitnessStrategy> { 41 | let (i, _) = tag("has_inventory")(i)?; 42 | Ok((i, FitnessStrategy::HasInventory)) 43 | } 44 | 45 | /// Parse an Any. 46 | fn any(i: &str) -> IResult<&str, FitnessStrategy> { 47 | let (i, _) = tag("any")(i)?; 48 | let (i, _) = multispace1(i)?; 49 | let (i, v) = vec(&FitnessStrategy::parse)(i)?; 50 | 51 | Ok((i, FitnessStrategy::Any(v))) 52 | } 53 | 54 | /// Parse an All. 55 | fn all(i: &str) -> IResult<&str, FitnessStrategy> { 56 | let (i, _) = tag("all")(i)?; 57 | let (i, _) = multispace1(i)?; 58 | let (i, v) = vec(&FitnessStrategy::parse)(i)?; 59 | 60 | Ok((i, FitnessStrategy::All(v))) 61 | } 62 | 63 | /// Parse a FitnessStrategy 64 | fn parse(i: &str) -> IResult<&str, FitnessStrategy> { 65 | alt(( 66 | FitnessStrategy::has_item, 67 | FitnessStrategy::has_inventory, 68 | FitnessStrategy::any, 69 | FitnessStrategy::all, 70 | ))(i) 71 | } 72 | } 73 | 74 | 75 | #[cfg(test)] 76 | mod fitness_strategy_tests { 77 | use super::*; 78 | 79 | #[test] 80 | fn can_parse_lifespan() { 81 | let my_str = "has_item \"white key\""; 82 | assert_eq!( 83 | FitnessStrategy::try_from_str(my_str), 84 | Ok(FitnessStrategy::HasItem("white key".to_string())) 85 | ); 86 | 87 | let my_str = "has_inventory"; 88 | assert_eq!( 89 | FitnessStrategy::try_from_str(my_str), 90 | Ok(FitnessStrategy::HasInventory) 91 | ); 92 | } 93 | } 94 | 95 | 96 | #[derive(Debug, Clone, PartialEq)] 97 | pub enum Lifespan { 98 | /// This thing has `n` uses. 99 | Many(u32), 100 | 101 | /// This thing never dies. 102 | Forever, 103 | } 104 | 105 | 106 | impl Lifespan { 107 | pub fn _succ(&self) -> Lifespan { 108 | match self { 109 | Lifespan::Many(n) => Lifespan::Many(n + 1), 110 | Lifespan::Forever => Lifespan::Forever, 111 | } 112 | } 113 | 114 | pub fn pred(&self) -> Lifespan { 115 | match self { 116 | Lifespan::Many(0) => Lifespan::Many(0), 117 | Lifespan::Many(n) => Lifespan::Many(n - 1), 118 | Lifespan::Forever => Lifespan::Forever, 119 | } 120 | } 121 | 122 | pub fn is_dead(&self) -> bool { 123 | match self { 124 | Lifespan::Many(0) => true, 125 | _ => false, 126 | } 127 | } 128 | } 129 | 130 | 131 | #[derive(Debug, Clone, PartialEq)] 132 | pub struct Action { 133 | /// Any entities that are elligible to take this action. 134 | pub elligibles: Vec, 135 | 136 | /// All the entities that have taken this action. 137 | pub taken_by: Vec, 138 | 139 | /// Some text about the action to display to the user. 140 | pub text: String, 141 | 142 | /// The method to use for determining whether an entity is elligible to 143 | /// take this action. 144 | pub strategy: FitnessStrategy, 145 | 146 | /// The lifespan of this action. 147 | pub lifespan: Lifespan, 148 | } 149 | 150 | 151 | impl Component for Action { 152 | type Storage = FlaggedStorage>; 153 | } 154 | 155 | 156 | /// Component used to request that an action be taken on behalf of an entity. 157 | pub struct TakeAction; 158 | 159 | 160 | impl Component for TakeAction { 161 | type Storage = HashMapStorage; 162 | } 163 | -------------------------------------------------------------------------------- /old_gods/src/geom/v2.rs: -------------------------------------------------------------------------------- 1 | use spade::PointN; 2 | //use sdl2::rect::Point; 3 | use std::{fmt::Debug, ops::*}; 4 | 5 | 6 | #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] 7 | pub struct V2 { 8 | pub x: f32, 9 | pub y: f32, 10 | } 11 | 12 | 13 | impl Sub for V2 { 14 | type Output = V2; 15 | fn sub(self, other: V2) -> V2 { 16 | V2 { 17 | x: self.x - other.x, 18 | y: self.y - other.y, 19 | } 20 | } 21 | } 22 | 23 | 24 | impl Add for V2 { 25 | type Output = V2; 26 | fn add(self, other: V2) -> V2 { 27 | V2 { 28 | x: self.x + other.x, 29 | y: self.y + other.y, 30 | } 31 | } 32 | } 33 | 34 | 35 | impl AddAssign for V2 { 36 | fn add_assign(&mut self, other: V2) { 37 | self.x += other.x; 38 | self.y += other.y; 39 | } 40 | } 41 | 42 | 43 | impl SubAssign for V2 { 44 | fn sub_assign(&mut self, other: V2) { 45 | self.x -= other.x; 46 | self.y -= other.y; 47 | } 48 | } 49 | 50 | 51 | impl Mul for V2 { 52 | type Output = V2; 53 | fn mul(self, v: V2) -> V2 { 54 | V2 { 55 | x: self.x * v.x, 56 | y: self.y * v.y, 57 | } 58 | } 59 | } 60 | 61 | 62 | impl Div for V2 { 63 | type Output = V2; 64 | fn div(self, v: V2) -> V2 { 65 | V2 { 66 | x: self.x / v.x, 67 | y: self.y / v.y, 68 | } 69 | } 70 | } 71 | 72 | 73 | impl V2 { 74 | pub fn new(x: f32, y: f32) -> V2 { 75 | V2 { x, y } 76 | } 77 | 78 | pub fn origin() -> V2 { 79 | V2::new(0.0, 0.0) 80 | } 81 | 82 | pub fn normal(&self) -> V2 { 83 | V2 { 84 | x: self.y * (-1.0), 85 | y: self.x, 86 | } 87 | } 88 | 89 | pub fn unitize(&self) -> Option { 90 | let m = self.magnitude(); 91 | if m == 0.0 { 92 | None 93 | } else { 94 | Some(V2 { 95 | x: self.x / m, 96 | y: self.y / m, 97 | }) 98 | } 99 | } 100 | 101 | pub fn dot(&self, other: V2) -> f32 { 102 | (self.x * other.x) + (self.y * other.y) 103 | } 104 | 105 | pub fn magnitude(&self) -> f32 { 106 | (self.x.powi(2) + self.y.powi(2)).sqrt() 107 | } 108 | 109 | pub fn distance_to(&self, other: &V2) -> f32 { 110 | (*other - *self).magnitude() 111 | } 112 | 113 | /// Normally the cross product gives you a vector 114 | /// orthogonal to the two param vectors, but since 115 | /// this is all in 2d, you only get the z component, 116 | /// hence this function returns an f32. 117 | pub fn cross(&self, v: V2) -> f32 { 118 | self.x * v.y - self.y * v.x 119 | } 120 | 121 | pub fn scalar_mul(&self, n: f32) -> V2 { 122 | V2 { 123 | x: self.x * n, 124 | y: self.y * n, 125 | } 126 | } 127 | 128 | pub fn translate(&self, v: &V2) -> V2 { 129 | *self + *v 130 | } 131 | 132 | pub fn angle_radians(&self) -> f32 { 133 | f32::atan2(self.y, self.x) 134 | } 135 | 136 | pub fn angle_degrees(&self) -> i16 { 137 | let radians = self.angle_radians(); 138 | (radians * 57.29578) as i16 139 | } 140 | 141 | //pub fn into_point(self) -> Point { 142 | // Point::new( 143 | // f32::round(self.x) as i32, 144 | // f32::round(self.y) as i32, 145 | // ) 146 | //} 147 | } 148 | 149 | 150 | impl PointN for V2 { 151 | type Scalar = f32; 152 | 153 | fn dimensions() -> usize { 154 | 2 155 | } 156 | 157 | fn from_value(value: Self::Scalar) -> Self { 158 | V2::new(value, value) 159 | } 160 | 161 | fn nth(&self, index: usize) -> &Self::Scalar { 162 | match index { 163 | 0 => &self.x, 164 | _ => &self.y, 165 | } 166 | } 167 | 168 | fn nth_mut(&mut self, index: usize) -> &mut Self::Scalar { 169 | match index { 170 | 0 => &mut self.x, 171 | _ => &mut self.y, 172 | } 173 | } 174 | } 175 | 176 | 177 | #[derive(Clone, Debug, PartialEq)] 178 | pub struct KeyVal { 179 | pub key: K, 180 | pub value: Option, 181 | } 182 | 183 | 184 | impl PointN for KeyVal { 185 | type Scalar = K::Scalar; 186 | 187 | fn dimensions() -> usize { 188 | K::dimensions() 189 | } 190 | 191 | fn from_value(value: Self::Scalar) -> Self { 192 | KeyVal { 193 | key: K::from_value(value), 194 | value: None, 195 | } 196 | } 197 | 198 | fn nth(&self, index: usize) -> &Self::Scalar { 199 | self.key.nth(index) 200 | } 201 | 202 | fn nth_mut(&mut self, index: usize) -> &mut Self::Scalar { 203 | self.key.nth_mut(index) 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /old_gods/src/systems/sound.rs: -------------------------------------------------------------------------------- 1 | /// The SoundSystem handles playing music and sound. 2 | use specs::prelude::*; 3 | //use sdl2::mixer::{ 4 | // AUDIO_S16LSB, 5 | // DEFAULT_CHANNELS, 6 | // Chunk, 7 | // Channel, 8 | // Group 9 | //}; 10 | //use sdl2::mixer::InitFlag; 11 | 12 | use super::super::prelude::{ 13 | Exile, OriginOffset, Player, Position, Screen, SoundBlaster, JSON, V2, 14 | }; 15 | 16 | 17 | #[derive(Debug, Clone)] 18 | /// A component for sound effects placed within the map. 19 | pub struct Sound { 20 | /// The sound file for this sound 21 | pub file: String, 22 | 23 | /// The volume this sound should play at. This should be a number between 0.0 24 | /// and 1.0 25 | pub volume: f32, 26 | 27 | /// Whether or not this sound autoplays when loaded. 28 | pub autoplay: bool, 29 | 30 | /// Whether or not this sound is "on the map". 31 | /// Being "on the map" means that it will be positioned spatially around the 32 | /// player and panned across the speakers. 33 | pub on_map: bool, 34 | } 35 | 36 | 37 | impl Component for Sound { 38 | type Storage = HashMapStorage; 39 | } 40 | 41 | 42 | pub struct SoundSystem { 43 | _blaster: SoundBlaster, 44 | } 45 | 46 | 47 | impl SoundSystem { 48 | // /// The channel group used for sound effects. 49 | // fn fx_group(&self) -> Group { 50 | // Group(-1) 51 | // } 52 | 53 | // fn next_fx_channel(&self) -> Channel { 54 | // let channel = self.fx_group().find_available(); 55 | // if let Some(channel) = channel { 56 | // channel 57 | // } else { 58 | // // Increase the number of channels 59 | // let num_channels = self.fx_group().count(); 60 | // sdl2::mixer::allocate_channels(num_channels + 1); 61 | // self.fx_group() 62 | // .find_available() 63 | // .expect("No sound fx channels are available") 64 | // } 65 | // } 66 | } 67 | 68 | 69 | impl Default for SoundSystem { 70 | fn default() -> SoundSystem { 71 | SoundSystem { 72 | _blaster: SoundBlaster::new(), 73 | } 74 | } 75 | } 76 | 77 | 78 | #[derive(SystemData)] 79 | pub struct SoundSystemData<'a> { 80 | entities: Entities<'a>, 81 | _jsons: WriteStorage<'a, JSON>, 82 | players: ReadStorage<'a, Player>, 83 | exiles: ReadStorage<'a, Exile>, 84 | offsets: ReadStorage<'a, OriginOffset>, 85 | positions: ReadStorage<'a, Position>, 86 | screen: Read<'a, Screen>, 87 | sounds: WriteStorage<'a, Sound>, 88 | } 89 | 90 | 91 | impl<'a> System<'a> for SoundSystem { 92 | type SystemData = SoundSystemData<'a>; 93 | 94 | fn run(&mut self, mut data: SoundSystemData) { 95 | // Find the greatest distance a player could see 96 | let max_distance = data.screen.get_size().scalar_mul(0.3).magnitude(); 97 | // Find the zeroeth player 98 | let player_pos: V2 = (&data.entities, &data.players, &data.positions) 99 | .join() 100 | .filter_map(|(e, c, p)| { 101 | if c.0 == 0 { 102 | data.offsets 103 | .get(e) 104 | .map(|&OriginOffset(o)| p.0 + o) 105 | .or(Some(p.0)) 106 | } else { 107 | None 108 | } 109 | }) 110 | .collect::>() 111 | .first() 112 | .cloned() 113 | .unwrap_or_else(|| data.screen.get_focus()); 114 | 115 | // Run through all the sounds that need to be triggered 116 | for (_ent, sound, &Position(p), ()) in ( 117 | &data.entities, 118 | &mut data.sounds, 119 | &data.positions, 120 | !&data.exiles, 121 | ) 122 | .join() 123 | { 124 | // TODO: Only check the sounds that are within a certain range of the 125 | // player position 126 | let (_distance, _angle, _can_hear_sound) = { 127 | // Find the player's proximity to the sound 128 | let proximity = player_pos.distance_to(&p); 129 | // adjust for the max distance of seeing things and the volume 130 | // the volume effectively lowers the distance at which things can be 131 | // heard 132 | let percent = proximity / (max_distance * sound.volume); 133 | // Scale out of 255 134 | let distance = (255.0 * percent) as u8; 135 | 136 | // Get the angle as well 137 | let v = player_pos - p; 138 | let a = v.angle_degrees(); 139 | let angle = (270 + a) % 360; 140 | 141 | (distance, angle, percent < 1.0) 142 | }; 143 | 144 | //if can_hear_sound { 145 | //} else { 146 | //} 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /old_gods/src/systems/map_loader/load.rs: -------------------------------------------------------------------------------- 1 | //! TODO: Retire map_loader::load.rs in favor of TiledSystem. 2 | use std::iter::FromIterator; 3 | use std::iter::Iterator; 4 | 5 | use super::super::{ 6 | super::components::{Rendering, TextureFrame}, 7 | super::geom::{Shape, V2}, 8 | super::tiled::json::AABB as TiledAABB, 9 | super::tiled::json::*, 10 | animation::Frame as AnimeFrame, 11 | animation::*, 12 | physics::*, 13 | }; 14 | 15 | 16 | pub fn get_tile_aabb( 17 | tm: &Tiledmap, 18 | tile_gid: &GlobalId, 19 | ) -> Option> { 20 | let (firstgid, tileset) = tm.get_tileset_by_gid(tile_gid)?; 21 | tileset.aabb(firstgid, tile_gid) 22 | } 23 | 24 | 25 | pub fn get_tile_position( 26 | tm: &Tiledmap, 27 | tile_gid: &GlobalId, 28 | ndx: usize, 29 | ) -> Option { 30 | let (width, height) = (tm.width as u32, tm.height as u32); 31 | let yndx = ndx as u32 / width; 32 | let xndx = ndx as u32 % height; 33 | let aabb = get_tile_aabb(tm, tile_gid)?; 34 | Some(Position(V2::new( 35 | (xndx * aabb.w) as f32, 36 | (yndx * aabb.h) as f32, 37 | ))) 38 | } 39 | 40 | 41 | /// Return a rendering for the tile with the given GlobalId. 42 | pub fn get_tile_rendering( 43 | tm: &Tiledmap, 44 | gid: &GlobalTileIndex, 45 | size: Option<(u32, u32)>, 46 | ) -> Option { 47 | let (firstgid, tileset) = tm.get_tileset_by_gid(&gid.id)?; 48 | let aabb = tileset.aabb(firstgid, &gid.id)?; 49 | Some(Rendering::from_frame(TextureFrame { 50 | sprite_sheet: tileset.image.clone(), 51 | source_aabb: aabb.clone(), 52 | size: size.unwrap_or((aabb.w, aabb.h)), 53 | is_flipped_horizontally: gid.is_flipped_horizontally, 54 | is_flipped_vertically: gid.is_flipped_vertically, 55 | is_flipped_diagonally: gid.is_flipped_diagonally, 56 | })) 57 | } 58 | 59 | 60 | pub fn get_tile_animation( 61 | tm: &Tiledmap, 62 | gid: &GlobalTileIndex, 63 | size: Option<(u32, u32)>, 64 | ) -> Option { 65 | let (firstgid, tileset) = tm.get_tileset_by_gid(&gid.id)?; 66 | let tile = tileset.tile(firstgid, &gid.id)?; 67 | // Get out the animation frames 68 | let frames = tile.clone().animation?; 69 | Some(Animation { 70 | is_playing: true, 71 | frames: Vec::from_iter(frames.iter().filter_map(|frame| { 72 | tileset.aabb_local(&frame.tileid).map(|frame_aabb| { 73 | let size = size.unwrap_or((frame_aabb.w, frame_aabb.h)); 74 | AnimeFrame { 75 | rendering: Rendering::from_frame(TextureFrame { 76 | sprite_sheet: tileset.image.clone(), 77 | source_aabb: frame_aabb.clone(), 78 | size, 79 | is_flipped_horizontally: gid.is_flipped_horizontally, 80 | is_flipped_vertically: gid.is_flipped_vertically, 81 | is_flipped_diagonally: gid.is_flipped_diagonally, 82 | }), 83 | duration: frame.duration as f32 / 1000.0, 84 | } 85 | }) 86 | })), 87 | current_frame_index: 0, 88 | current_frame_progress: 0.0, 89 | should_repeat: true, 90 | }) 91 | } 92 | 93 | 94 | /// Returns the first barrier aabb on the object. 95 | pub fn get_tile_barriers(tm: &Tiledmap, tile_gid: &GlobalId) -> Option { 96 | if let Some(group) = tm.get_tile_object_group(&tile_gid) { 97 | for tile_object in group.objects { 98 | let may_bar = object_barrier(&tile_object); 99 | if may_bar.is_some() { 100 | return may_bar; 101 | } 102 | } 103 | } 104 | None 105 | } 106 | 107 | 108 | pub fn get_z_inc(object: &Object) -> Option { 109 | for prop in object.properties.iter() { 110 | if prop.name == "z_inc" { 111 | if let Some(z_inc) = prop.value.as_i64() { 112 | return Some(z_inc as i32); 113 | } 114 | } 115 | } 116 | None 117 | } 118 | 119 | pub fn object_shape(object: &Object) -> Option { 120 | if let Some(_polyline) = &object.polyline { 121 | // A shape cannot be a polyline 122 | None 123 | } else if let Some(polygon) = &object.polygon { 124 | let vertices: Vec = polygon 125 | .clone() 126 | .into_iter() 127 | .map(|p| V2::new(p.x + object.x, p.y + object.y)) 128 | .collect(); 129 | // TODO: Check polygon for concavity at construction 130 | // ```rust 131 | // pub fn polygon_from_vertices() -> Option 132 | // ``` 133 | // because not all polygons are convex 134 | 135 | // TODO: Support a shape made of many shapes. 136 | // This way we can decompose concave polygons into a number of convex ones. 137 | Some(Shape::Polygon { vertices }) 138 | } else { 139 | // It's a rectangle! 140 | let lower = V2::new(object.x, object.y); 141 | let upper = V2::new(object.x + object.width, object.y + object.height); 142 | Some(Shape::Box { lower, upper }) 143 | } 144 | } 145 | 146 | 147 | pub fn object_barrier(object: &Object) -> Option { 148 | if object.type_is == "barrier" { 149 | object_shape(object) 150 | } else { 151 | None 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /examples/loading-maps/maps/tilesets/doors.json: -------------------------------------------------------------------------------- 1 | { "columns":4, 2 | "editorsettings": 3 | { 4 | "export": 5 | { 6 | "format":"", 7 | "target":"." 8 | } 9 | }, 10 | "image":"..\/images\/doors.png", 11 | "imageheight":864, 12 | "imagewidth":192, 13 | "margin":0, 14 | "name":"doors", 15 | "spacing":0, 16 | "tilecount":72, 17 | "tiledversion":"1.3.1", 18 | "tileheight":48, 19 | "tiles":[ 20 | { 21 | "id":0, 22 | "type":"skip" 23 | }, 24 | { 25 | "id":1, 26 | "type":"skip" 27 | }, 28 | { 29 | "id":2, 30 | "type":"skip" 31 | }, 32 | { 33 | "id":3, 34 | "type":"skip" 35 | }, 36 | { 37 | "id":4, 38 | "properties":[ 39 | { 40 | "name":"file", 41 | "type":"string", 42 | "value":"assets\/sprites\/wooden_door.json" 43 | }, 44 | { 45 | "name":"keyframe", 46 | "type":"string", 47 | "value":"closed" 48 | }, 49 | { 50 | "name":"script", 51 | "type":"string", 52 | "value":"door" 53 | }, 54 | { 55 | "name":"variant", 56 | "type":"string", 57 | "value":"north_south" 58 | }], 59 | "type":"sprite" 60 | }, 61 | { 62 | "id":5, 63 | "properties":[ 64 | { 65 | "name":"file", 66 | "type":"string", 67 | "value":"assets\/sprites\/wooden_door.json" 68 | }, 69 | { 70 | "name":"keyframe", 71 | "type":"string", 72 | "value":"open" 73 | }, 74 | { 75 | "name":"variant", 76 | "type":"string", 77 | "value":"north_south" 78 | }], 79 | "type":"sprite" 80 | }, 81 | { 82 | "id":6, 83 | "properties":[ 84 | { 85 | "name":"file", 86 | "type":"string", 87 | "value":"assets\/sprites\/wooden_door.json" 88 | }, 89 | { 90 | "name":"keyframe", 91 | "type":"string", 92 | "value":"closed" 93 | }, 94 | { 95 | "name":"variant", 96 | "type":"string", 97 | "value":"east_west" 98 | }], 99 | "type":"sprite" 100 | }, 101 | { 102 | "id":7, 103 | "properties":[ 104 | { 105 | "name":"file", 106 | "type":"string", 107 | "value":"assets\/sprites\/wooden_door.json" 108 | }, 109 | { 110 | "name":"keyframe", 111 | "type":"string", 112 | "value":"open" 113 | }, 114 | { 115 | "name":"variant", 116 | "type":"string", 117 | "value":"east_west" 118 | }], 119 | "type":"sprite" 120 | }, 121 | { 122 | "id":8, 123 | "type":"skip" 124 | }, 125 | { 126 | "id":10, 127 | "type":"skip" 128 | }, 129 | { 130 | "id":11, 131 | "type":"skip" 132 | }, 133 | { 134 | "id":61, 135 | "type":"skip" 136 | }, 137 | { 138 | "id":64, 139 | "properties":[ 140 | { 141 | "name":"file", 142 | "type":"string", 143 | "value":"assets\/sprites\/wooden_door.json" 144 | }, 145 | { 146 | "name":"keyframe", 147 | "type":"string", 148 | "value":"closed" 149 | }, 150 | { 151 | "name":"variant", 152 | "type":"string", 153 | "value":"brown_trap" 154 | }], 155 | "type":"sprite" 156 | }, 157 | { 158 | "id":65, 159 | "properties":[ 160 | { 161 | "name":"file", 162 | "type":"string", 163 | "value":"assets\/sprites\/wooden_door.json" 164 | }, 165 | { 166 | "name":"keyframe", 167 | "type":"string", 168 | "value":"open" 169 | }, 170 | { 171 | "name":"variant", 172 | "type":"string", 173 | "value":"brown_trap" 174 | }] 175 | }], 176 | "tilewidth":48, 177 | "type":"tileset", 178 | "version":1.2 179 | } -------------------------------------------------------------------------------- /old_gods/src/systems/screen.rs: -------------------------------------------------------------------------------- 1 | /// The screen system keeps the players within view of the screen. 2 | use specs::prelude::*; 3 | 4 | use super::super::components::{Exile, OriginOffset, Player, Position, AABB, V2}; 5 | use std::f32::{INFINITY, NEG_INFINITY}; 6 | 7 | /// TODO: Rename to Viewport 8 | #[derive(Debug)] 9 | pub struct Screen { 10 | /// The screen's aabb in map coordinates. 11 | viewport: AABB, 12 | 13 | /// Width and height of the focus AABB 14 | tolerance: f32, 15 | 16 | /// Set whether the screen should follow player characters 17 | pub should_follow_players: bool, 18 | } 19 | 20 | 21 | impl Screen { 22 | /// Translate a position to get its relative position within the screen. 23 | pub fn from_map(&self, pos: &V2) -> V2 { 24 | *pos - self.aabb().top_left 25 | } 26 | 27 | pub fn get_size(&self) -> V2 { 28 | self.viewport.extents 29 | } 30 | 31 | 32 | pub fn set_size(&mut self, (w, h): (u32, u32)) { 33 | self.viewport.extents = V2::new(w as f32, h as f32); 34 | } 35 | 36 | 37 | /// Sets the center of the screen to a map coordinate. 38 | pub fn set_focus(&mut self, pos: V2) { 39 | self.viewport.set_center(&pos); 40 | } 41 | 42 | 43 | /// Returns the center of the screen in map coordinates. 44 | pub fn get_focus(&self) -> V2 { 45 | self.viewport.center() 46 | } 47 | 48 | /// Returns a mutable viewport. 49 | pub fn get_mut_viewport(&mut self) -> &mut AABB { 50 | &mut self.viewport 51 | } 52 | 53 | 54 | pub fn get_tolerance(&self) -> f32 { 55 | self.tolerance 56 | } 57 | 58 | 59 | pub fn focus_aabb(&self) -> AABB { 60 | let mut aabb = AABB { 61 | top_left: V2::origin(), 62 | extents: V2::new(self.tolerance, self.tolerance), 63 | }; 64 | aabb.set_center(&self.viewport.center()); 65 | aabb 66 | } 67 | 68 | 69 | pub fn aabb(&self) -> AABB { 70 | self.viewport 71 | } 72 | 73 | 74 | pub fn distance_to_contain_point(&self, p: &V2) -> V2 { 75 | let mut out = V2::origin(); 76 | let aabb = self.focus_aabb(); 77 | if p.x < aabb.left() { 78 | out.x -= aabb.left() - p.x; 79 | } else if p.x > aabb.right() { 80 | out.x += p.x - aabb.right(); 81 | } 82 | if p.y < aabb.top() { 83 | out.y -= aabb.top() - p.y; 84 | } else if p.y > aabb.bottom() { 85 | out.y += p.y - aabb.bottom(); 86 | } 87 | 88 | out 89 | } 90 | } 91 | 92 | 93 | impl Default for Screen { 94 | fn default() -> Screen { 95 | let aabb = AABB { 96 | top_left: V2::origin(), 97 | extents: V2::new(848.0, 648.0), 98 | }; 99 | Screen { 100 | viewport: aabb, 101 | tolerance: 50.0, 102 | should_follow_players: true, 103 | } 104 | } 105 | } 106 | 107 | 108 | pub struct ScreenSystem; 109 | 110 | 111 | #[derive(SystemData)] 112 | pub struct ScreenSystemData<'a> { 113 | entities: Entities<'a>, 114 | exiles: ReadStorage<'a, Exile>, 115 | players: ReadStorage<'a, Player>, 116 | positions: ReadStorage<'a, Position>, 117 | offsets: ReadStorage<'a, OriginOffset>, 118 | screen: Write<'a, Screen>, 119 | } 120 | 121 | 122 | impl<'a> System<'a> for ScreenSystem { 123 | type SystemData = ScreenSystemData<'a>; 124 | 125 | fn run(&mut self, mut data: ScreenSystemData) { 126 | if !data.screen.should_follow_players { 127 | return; 128 | } 129 | 130 | // First get the minimum bounding rectangle that shows all players. 131 | let mut left = INFINITY; 132 | let mut right = NEG_INFINITY; 133 | let mut top = INFINITY; 134 | let mut bottom = NEG_INFINITY; 135 | 136 | for (entity, _player, Position(pos), ()) in ( 137 | &data.entities, 138 | &data.players, 139 | &data.positions, 140 | !&data.exiles, 141 | ) 142 | .join() 143 | { 144 | // TODO: Allow npc players to be counted in screen following. 145 | // It wouldn't be too hard to have a component ScreenFollows and just search through that, 146 | // or use this method if there are no ScreenFollows comps in the ECS. 147 | let offset = data 148 | .offsets 149 | .get(entity) 150 | .map(|o| o.0) 151 | .unwrap_or_else(V2::origin); 152 | 153 | let p = *pos + offset; 154 | 155 | if p.x < left { 156 | left = p.x; 157 | } 158 | if p.x > right { 159 | right = p.x; 160 | } 161 | if p.y < top { 162 | top = p.y; 163 | } 164 | if p.y > bottom { 165 | bottom = p.y; 166 | } 167 | } 168 | 169 | let min_aabb = AABB { 170 | top_left: V2::new(left, top), 171 | extents: V2::new(right - left, bottom - top), 172 | }; 173 | 174 | let distance = data.screen.distance_to_contain_point(&min_aabb.center()); 175 | data.screen.viewport.top_left += distance; 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /old_gods/src/systems/item/mod.rs: -------------------------------------------------------------------------------- 1 | /// Controls picking up items from the map and placing them in inventories. 2 | use specs::prelude::*; 3 | 4 | use super::super::prelude::{ 5 | Action, Effect, Exile, FitnessStrategy, Inventory, Lifespan, Name, Position, 6 | Shape, Sprite, V2, 7 | }; 8 | 9 | 10 | pub struct ItemSystem; 11 | 12 | 13 | impl ItemSystem { 14 | pub fn find_actionless_map_items<'a>( 15 | entities: &Entities<'a>, 16 | items: &ReadStorage<'a, Item>, 17 | positions: &WriteStorage<'a, Position>, 18 | names: &ReadStorage<'a, Name>, 19 | exiles: &WriteStorage<'a, Exile>, 20 | actions: &WriteStorage<'a, Action>, 21 | ) -> Vec<(Entity, Name)> { 22 | // Items that have a position but no action need to have an action created 23 | // for them so they can be picked up. 24 | // Items that don't have a position are assumed to be sitting in an 25 | // inventory, and nothing has to be done. 26 | (entities, items, positions, names, !exiles, !actions) 27 | .join() 28 | .map(|(ent, _, _, name, _, _)| (ent, name.clone())) 29 | .collect() 30 | } 31 | 32 | 33 | /// Creates a new item pickup action 34 | pub fn new_pickup_action( 35 | &self, 36 | entities: &Entities, 37 | lazy: &LazyUpdate, 38 | name: String, 39 | p: V2, 40 | item_shape: Option<&Shape>, 41 | ) -> Entity { 42 | let a = Action { 43 | elligibles: vec![], 44 | taken_by: vec![], 45 | text: format!("Pick up {}", name), 46 | strategy: FitnessStrategy::HasInventory, 47 | lifespan: Lifespan::Many(1), 48 | }; 49 | let s = item_shape 50 | .map(|s| { 51 | let aabb = s.aabb(); 52 | let mut new_aabb = aabb.clone(); 53 | new_aabb.extents += V2::new(4.0, 4.0); 54 | new_aabb.set_center(&aabb.center()); 55 | new_aabb.to_shape() 56 | }) 57 | .unwrap_or(Shape::Box { 58 | lower: V2::origin(), 59 | upper: V2::new(15.0, 15.0), 60 | }); 61 | 62 | println!("Creating an action {:?}", a.text); 63 | 64 | lazy 65 | .create_entity(&entities) 66 | .with(a) 67 | .with(Position(p)) 68 | .with(s) 69 | .with(Name("pickup item".to_string())) 70 | .build() 71 | } 72 | } 73 | 74 | 75 | impl<'a> System<'a> for ItemSystem { 76 | type SystemData = ( 77 | Entities<'a>, 78 | WriteStorage<'a, Action>, 79 | WriteStorage<'a, Exile>, 80 | ReadStorage<'a, Item>, 81 | WriteStorage<'a, Inventory>, 82 | Read<'a, LazyUpdate>, 83 | ReadStorage<'a, Name>, 84 | WriteStorage<'a, Position>, 85 | ReadStorage<'a, Shape>, 86 | ReadStorage<'a, Sprite>, 87 | ); 88 | fn run( 89 | &mut self, 90 | ( 91 | entities, 92 | actions, 93 | exiles, 94 | items, 95 | inventories, 96 | lazy, 97 | names, 98 | mut positions, 99 | shapes, 100 | sprites, 101 | ): Self::SystemData, 102 | ) { 103 | for (ent, Item { .. }, name, ()) in 104 | (&entities, &items, &names, !&exiles).join() 105 | { 106 | // Determine if this is a map item or an inventory item 107 | let may_pos = positions.get(ent).cloned(); 108 | let item_on_map = may_pos.is_some(); 109 | if item_on_map { 110 | let position = may_pos.unwrap(); 111 | // Determine if this item has a pickup action (we'll store it using a sprite) 112 | if let Some(sprite) = sprites.get(ent) { 113 | let action_ent = sprite 114 | .top_level_children 115 | .first() 116 | .cloned() 117 | .expect("Item sprite doesn't have any entities"); 118 | let action = actions 119 | .get(action_ent) 120 | .expect("Item sprite doesn't have a pickup action component"); 121 | // Give the action to the first elligible taker. 122 | 'action_taken: for taker in &action.taken_by { 123 | let taker_has_inventory = inventories.contains(*taker); 124 | if taker_has_inventory { 125 | // It has been taken, so put a pickup effect in the ECS. 126 | let pickup_effect = Effect::InsertItem { 127 | item: ent, 128 | into: Some(*taker), 129 | from: None, 130 | }; 131 | lazy.create_entity(&entities).with(pickup_effect).build(); 132 | // Delete the pickup action and sprite 133 | entities.delete(action_ent).unwrap(); 134 | lazy.remove::(ent); 135 | break 'action_taken; 136 | } 137 | } 138 | 139 | // Make sure the action position stays up to date with the item 140 | positions 141 | .insert(action_ent, position) 142 | .expect("Could not insert item pickup action position"); 143 | } else { 144 | // It does not! 145 | // Create a sprite component and a pickup action for it. 146 | let action_ent = self.new_pickup_action( 147 | &entities, 148 | &lazy, 149 | name.0.clone(), 150 | position.0, 151 | shapes.get(ent), 152 | ); 153 | let sprite = Sprite::with_top_level_children(vec![action_ent]); 154 | lazy.insert(ent, sprite); 155 | } 156 | } else { 157 | // This is an inventory item and there's nothing to do 158 | } 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /old_gods/src/resources.rs: -------------------------------------------------------------------------------- 1 | //! Traits and types for loading shared resources. 2 | use log::warn; 3 | use std::{ 4 | collections::HashMap, 5 | sync::{Arc, Mutex}, 6 | }; 7 | use wasm_bindgen::{closure::Closure, JsValue}; 8 | 9 | 10 | #[derive(Clone)] 11 | /// The loading status of a resource. 12 | pub enum LoadStatus { 13 | None, 14 | Started, 15 | Complete, 16 | Error(String), 17 | } 18 | 19 | 20 | /// A shared resource. 21 | /// All clones of this resource refer to the same underlying memory. 22 | #[derive(Clone)] 23 | pub struct SharedResource { 24 | payload: Arc)>>, 25 | } 26 | 27 | impl Default for SharedResource { 28 | fn default() -> Self { 29 | SharedResource { 30 | payload: Arc::new(Mutex::new((LoadStatus::None, None))), 31 | } 32 | } 33 | } 34 | 35 | 36 | impl SharedResource { 37 | pub fn with_payload(&self, f: impl FnOnce(&(LoadStatus, Option)) -> A) -> A { 38 | let payload = self 39 | .payload 40 | .try_lock() 41 | .expect("Could not acquire lock - load_sprite_sheet::load"); 42 | f(&payload) 43 | } 44 | 45 | pub fn with_status(&self, f: impl FnOnce(&LoadStatus) -> A) -> A { 46 | self.with_payload(|p| f(&p.0)) 47 | } 48 | 49 | pub fn with_resource(&self, f: impl FnOnce(&T) -> A) -> Option { 50 | self.with_payload(|p| p.1.as_ref().map(f)) 51 | } 52 | 53 | pub fn set_status(&self, status: LoadStatus) { 54 | let mut payload = self 55 | .payload 56 | .try_lock() 57 | .expect("Could not acquire lock - load_sprite_sheet::load"); 58 | payload.0 = status; 59 | } 60 | 61 | pub fn set_resource(&self, may_rsrc: Option) { 62 | let mut payload = self 63 | .payload 64 | .try_lock() 65 | .expect("Could not acquire lock - load_sprite_sheet::load"); 66 | payload.1 = may_rsrc; 67 | } 68 | 69 | pub fn set_status_and_resource(&self, new_payload: (LoadStatus, Option)) { 70 | let mut payload = self 71 | .payload 72 | .try_lock() 73 | .expect("Could not acquire lock - load_sprite_sheet::load"); 74 | *payload = new_payload; 75 | } 76 | } 77 | 78 | 79 | /// A generic way to load resources. 80 | /// Resources loaded this way can be polled in subsystems. 81 | pub trait Resources { 82 | fn status_of(&self, key: &str) -> LoadStatus; 83 | fn load(&mut self, key: &str); 84 | fn take(&mut self, key: &str) -> Option>; 85 | fn put(&mut self, key: &str, rsrc: SharedResource); 86 | } 87 | 88 | 89 | /// Poll the load status of a resource: 90 | /// * if it has not yet started a load, start a new loading process, return nothing 91 | /// * if it is loading, do nothing, return nothing 92 | /// * if it is complete call the closure with the resource and return some answer 93 | /// * if it has erred, return the error message 94 | pub fn when_loaded(rs: &mut Rs, key: &str, f: F) -> Result, String> 95 | where 96 | Rs: Resources, 97 | F: FnOnce(&R) -> T, 98 | { 99 | match rs.status_of(&key) { 100 | LoadStatus::None => { 101 | // Load it and come back later 102 | rs.load(&key); 103 | return Ok(None); 104 | } 105 | LoadStatus::Started => { 106 | // Come back later because it's loading etc. 107 | return Ok(None); 108 | } 109 | LoadStatus::Complete => {} 110 | LoadStatus::Error(msg) => { 111 | warn!("sprite sheet loading error: {}", msg); 112 | return Err(msg); 113 | } 114 | } 115 | 116 | if let Some(shared) = rs.take(key) { 117 | let may_t: Option = shared.with_resource(|r| f(r)); 118 | rs.put(key, shared); 119 | Ok(may_t) 120 | } else { 121 | Err("No shared resource - this should not happen".to_string()) 122 | } 123 | } 124 | 125 | 126 | /// Helps hold JS closures while loading resources. 127 | pub struct Callbacks { 128 | pub loading: Arc>, 129 | pub error: Arc>, 130 | } 131 | 132 | 133 | impl Callbacks { 134 | pub fn new(loading: Closure, error: Closure) -> Self { 135 | Callbacks { 136 | loading: Arc::new(loading), 137 | error: Arc::new(error), 138 | } 139 | } 140 | } 141 | 142 | 143 | /// Helper function for writing [Resources::status_of] for types that contain 144 | /// a hashmap of shared resources. 145 | pub fn status_of_sharedmap( 146 | resources: &HashMap>, 147 | s: &str, 148 | ) -> LoadStatus { 149 | resources 150 | .get(s) 151 | .map(|rsrc| rsrc.with_status(|s| s.clone())) 152 | .unwrap_or(LoadStatus::None) 153 | } 154 | 155 | 156 | /// An inner type for loadable resources. 157 | /// 158 | /// This struct's implementation of [Resources::load] does nothing, but 159 | /// a wrapper type can proxy the other trait functions to an inner 160 | /// LoadableResources and implement its own [Resources::load] function. 161 | /// This cuts down on the boilerplate of making new resources. 162 | /// 163 | /// See [old_gods::image::HtmlImageResources] for an example of this. 164 | pub struct LoadableResources { 165 | pub resources: HashMap>, 166 | pub callbacks: HashMap, 167 | } 168 | 169 | 170 | impl Default for LoadableResources { 171 | fn default() -> Self { 172 | Self::new() 173 | } 174 | } 175 | 176 | 177 | impl LoadableResources { 178 | pub fn new() -> Self { 179 | LoadableResources { 180 | resources: HashMap::new(), 181 | callbacks: HashMap::new(), 182 | } 183 | } 184 | } 185 | 186 | 187 | impl Resources for LoadableResources { 188 | fn status_of(&self, s: &str) -> LoadStatus { 189 | status_of_sharedmap(&self.resources, s) 190 | } 191 | 192 | fn load(&mut self, _path: &str) { 193 | // TODO: Think about making [Resources::load] return a Result<(), String> 194 | panic!("Attempting to use LoadableResources::load when this type is meant to be wrapped!"); 195 | } 196 | 197 | fn take(&mut self, s: &str) -> Option> { 198 | if self.callbacks.contains_key(s) { 199 | let _ = self.callbacks.remove(s); 200 | } 201 | self.resources.remove(s) 202 | } 203 | 204 | fn put(&mut self, path: &str, shared: SharedResource) { 205 | self.resources.insert(path.to_string(), shared); 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /old_gods/src/systems/physics/mod.rs: -------------------------------------------------------------------------------- 1 | /// Manages: 2 | /// * maintaining the cardinal direction an object is/was last moving in 3 | use specs::prelude::*; 4 | 5 | use super::super::prelude::{AABBTree, Cardinal, Exile, FPSCounter, Shape, ZLevel, AABB, V2}; 6 | 7 | 8 | // TODO: Mass and acceleration for physical bodies. 9 | 10 | #[derive(Debug, Clone, PartialEq)] 11 | pub struct Position(pub V2); 12 | 13 | 14 | impl Component for Position { 15 | type Storage = FlaggedStorage>; 16 | } 17 | 18 | 19 | #[derive(Debug, Clone, PartialEq)] 20 | pub struct Velocity(pub V2); 21 | 22 | 23 | impl Component for Velocity { 24 | type Storage = HashMapStorage; 25 | } 26 | 27 | 28 | #[derive(Debug, Clone)] 29 | pub struct Barrier; 30 | 31 | 32 | impl Barrier { 33 | pub fn tiled_type() -> String { 34 | "barrier".to_string() 35 | } 36 | } 37 | 38 | 39 | impl Component for Barrier { 40 | type Storage = HashMapStorage; 41 | } 42 | 43 | 44 | pub struct Physics { 45 | pub shape_reader: Option>, 46 | pub position_reader: Option>, 47 | } 48 | 49 | 50 | impl Default for Physics { 51 | fn default() -> Physics { 52 | Physics { 53 | shape_reader: None, 54 | position_reader: None, 55 | } 56 | } 57 | } 58 | 59 | 60 | #[derive(SystemData)] 61 | pub struct PhysicsSystemData<'a> { 62 | aabb_tree: Write<'a, AABBTree>, 63 | barriers: ReadStorage<'a, Barrier>, 64 | cardinals: WriteStorage<'a, Cardinal>, 65 | entities: Entities<'a>, 66 | exiles: ReadStorage<'a, Exile>, 67 | fps: Read<'a, FPSCounter>, 68 | positions: WriteStorage<'a, Position>, 69 | shapes: ReadStorage<'a, Shape>, 70 | velocities: ReadStorage<'a, Velocity>, 71 | zlevels: ReadStorage<'a, ZLevel>, 72 | } 73 | 74 | 75 | impl<'a> PhysicsSystemData<'a> { 76 | /// Move all the things that can move. 77 | pub fn move_things(&mut self) { 78 | let dt = self.fps.last_delta(); 79 | for (ent, vel, ()) in (&self.entities, &self.velocities, !&self.exiles).join() { 80 | let v = vel.0; 81 | let dxy = v.scalar_mul(dt); 82 | if dxy.magnitude() > 0.0 { 83 | let pos = self 84 | .positions 85 | .get_mut(ent) 86 | .expect("Entity must have a position to add velocity to."); 87 | pos.0 += dxy; 88 | // Update the direction the thing is moving in 89 | if let Some(c) = Cardinal::from_v2(&v) { 90 | self.cardinals 91 | .insert(ent, c) 92 | .expect("Could not insert a Cardinal dir"); 93 | } 94 | } 95 | } 96 | } 97 | 98 | /// For each entity that has a position, barrier, shape, zlevel and velocity - 99 | /// find any collisions and deal with them. 100 | /// Only adjust the positions of entities that have a velocity, that way tiles 101 | /// with overlapping borders will not be moved around. 102 | pub fn collide_things(&mut self) { 103 | for (ent, _, _, shape, &ZLevel(z), ()) in ( 104 | &self.entities, 105 | &self.velocities, 106 | &self.barriers, 107 | &self.shapes, 108 | &self.zlevels, 109 | !&self.exiles, 110 | ) 111 | .join() 112 | { 113 | let may_pos = self.positions.get(ent); 114 | if may_pos.is_none() { 115 | continue; 116 | } 117 | let pos = may_pos.expect("Impossible").0; 118 | // Query all collisions with this entity's shape. 119 | // Find the new position using the minimum translation vector that pushes 120 | // it out of intersection. 121 | // 122 | // If the resulting position is different from the previous, update the 123 | // position. 124 | let new_position = self 125 | .aabb_tree 126 | .query_intersecting_barriers( 127 | &self.entities, 128 | &ent, 129 | &self.shapes, 130 | &self.positions, 131 | &self.barriers, 132 | ) 133 | .into_iter() 134 | .fold(pos, |new_pos, (other_ent, _, mtv)| { 135 | let other_z = self.zlevels.get(other_ent); 136 | let should_include = 137 | // The other thing must have a zlevel 138 | other_z.is_some() 139 | // The two things must be on the same zlevel. 140 | && (z - other_z.unwrap().0).abs() < f32::EPSILON 141 | // The other thing must not be exiled. 142 | && !self.exiles.contains(other_ent); 143 | if !should_include { 144 | return new_pos; 145 | } 146 | 147 | new_pos - mtv 148 | }); 149 | 150 | if pos != new_position { 151 | let pos = self.positions.get_mut(ent).expect("Impossible"); 152 | pos.0 = new_position; 153 | // TODO: Check if this is necessary 154 | self.aabb_tree 155 | .insert(ent, shape.aabb().translate(&new_position)); 156 | } 157 | } 158 | } 159 | } 160 | 161 | 162 | impl<'a> System<'a> for Physics { 163 | type SystemData = PhysicsSystemData<'a>; 164 | 165 | fn setup(&mut self, world: &mut World) { 166 | Self::SystemData::setup(world); 167 | // Get the Barrier and Position storages and start watching them for changes. 168 | let mut shapes: WriteStorage = SystemData::fetch(&world); 169 | self.shape_reader = Some(shapes.register_reader()); 170 | let mut positions: WriteStorage = SystemData::fetch(&world); 171 | self.position_reader = Some(positions.register_reader()); 172 | } 173 | 174 | fn run(&mut self, mut data: PhysicsSystemData) { 175 | data.move_things(); 176 | data.collide_things(); 177 | 178 | // Maintain our aabb_tree with new positions and shapes 179 | let shape_reader = self 180 | .shape_reader 181 | .as_mut() 182 | .expect("Could not unwrap shape reader"); 183 | let position_reader = self 184 | .position_reader 185 | .as_mut() 186 | .expect("Could not unwrap position reader"); 187 | let positions = &data.positions; 188 | let shapes = &data.shapes; 189 | let mut events: Vec<&ComponentEvent> = shapes.channel().read(shape_reader).collect(); 190 | events.extend( 191 | positions 192 | .channel() 193 | .read(position_reader) 194 | .collect::>(), 195 | ); 196 | data.aabb_tree.update_tree( 197 | &data.entities, 198 | events, 199 | |ent: Entity| -> Option<(Entity, AABB)> { 200 | let position = positions.get(ent).map(|p| p.0)?; 201 | shapes 202 | .get(ent) 203 | .map(|shape| (ent, shape.aabb().translate(&position))) 204 | }, 205 | ); 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /old_gods/src/systems/fence.rs: -------------------------------------------------------------------------------- 1 | //! A fence is a line of demarcation. 2 | //! 3 | //! The fence system tracks what entities have crossed a fence from one frame to 4 | //! another. 5 | //! 6 | //! Step fences alter the ZLevel of entities that have crossed them. 7 | use log::trace; 8 | use specs::prelude::*; 9 | 10 | use std::collections::HashMap; 11 | 12 | use super::super::{ 13 | components::{Position, Velocity, ZLevel}, 14 | geom::{AABBTree, EntityBounds, LineSegment, AABB, V2}, 15 | }; 16 | 17 | 18 | /// A fence is used to track entities that cross it, and at what angle. 19 | #[derive(Debug, Clone)] 20 | pub struct Fence { 21 | /// The points in this fence. 22 | pub points: Vec, 23 | 24 | /// The entities being watched and their last known positions. 25 | pub watching: HashMap, 26 | 27 | /// the entities that have crossed the fence and whether or not the cross 28 | /// product of the intersection was positive. 29 | /// This determines if the fence was crossed one way or another. 30 | pub crossed: HashMap, 31 | } 32 | 33 | 34 | impl Fence { 35 | pub fn new(points: Vec) -> Fence { 36 | Fence { 37 | points, 38 | watching: HashMap::new(), 39 | crossed: HashMap::new(), 40 | } 41 | } 42 | 43 | pub fn segments(&self) -> Vec<(&V2, &V2)> { 44 | let line1: Vec<&V2> = self.points.iter().collect(); 45 | let line2: Vec<&V2> = self.points.iter().collect::>().drain(1..).collect(); 46 | let segments: Vec<(&V2, &V2)> = line1.into_iter().zip(line2.into_iter()).collect(); 47 | segments 48 | } 49 | } 50 | 51 | 52 | impl Component for Fence { 53 | type Storage = HashMapStorage; 54 | } 55 | 56 | 57 | /// A special fence that when crossed either increments or decrements an entity's 58 | /// ZLevel. This is a bit of a hack to allow creatures to move up stairs and still 59 | /// render properly. 60 | #[derive(Debug, Clone)] 61 | pub struct StepFence { 62 | pub step: f32, 63 | pub fence: Fence, 64 | } 65 | 66 | 67 | impl Component for StepFence { 68 | type Storage = HashMapStorage; 69 | } 70 | 71 | 72 | pub fn run_fence( 73 | aabb_tree: &Read, 74 | entities: &Entities, 75 | velocities: &ReadStorage, 76 | fence_ent: Entity, 77 | fence: &mut Fence, 78 | pos: V2, 79 | ) { 80 | // Clear out our entities this frame 81 | let last_watching: HashMap = fence.watching.drain().collect(); 82 | fence.watching = HashMap::new(); 83 | fence.crossed = HashMap::new(); 84 | let segments: Vec<(V2, V2)> = fence 85 | .segments() 86 | .iter() 87 | .map(|tup| (*tup.0, *tup.1)) 88 | .collect::>() 89 | .drain(..) 90 | .collect(); 91 | // Maintain a list of entities we've already known have crossed 92 | for (p1, p2) in segments { 93 | // The fence's points are relative to the fence's position. 94 | let point1 = p1 + pos; 95 | let point2 = p2 + pos; 96 | // Find the radius^2 of our query 97 | // (length of the segment)^2 98 | let radius = p1.distance_to(&p2); 99 | let radius2 = radius * radius; 100 | // Use the circle that includes the whole segment to query for interesting 101 | // subjects 102 | let ebs = aabb_tree.rtree.lookup_in_circle(&point1, &radius2); 103 | // Insert all the entities we're watching 104 | for EntityBounds { entity_id, bounds } in ebs { 105 | let entity = entities.entity(*entity_id); 106 | if fence_ent == entity { 107 | continue; 108 | } 109 | // Add this thing so we can check it next frame 110 | let entity_center = AABB::from_mbr(bounds).center(); 111 | fence.watching.insert(entity, entity_center); 112 | // Continue on to the next entity if we already know this one crossed 113 | if fence.crossed.contains_key(&entity) { 114 | continue; 115 | } 116 | let entity_velocity = velocities.get(entity); 117 | // In order to cross a fence a thing must be moving 118 | if entity_velocity.is_none() { 119 | continue; 120 | } 121 | if let Some(prev_center) = last_watching.get(&entity) { 122 | // We were watching this entity previously, so check to see if the 123 | // line made by its previous position and new position intersects with 124 | // our segment. 125 | let fence_segment = LineSegment::new(point1, point2); 126 | let ent_segment = LineSegment::new(*prev_center, entity_center); 127 | let intersection_point = fence_segment.intersection_with(ent_segment); 128 | if intersection_point.is_some() { 129 | // It intersects, so now figure out the cross product 130 | let vector_moved = ent_segment.vector_difference(); 131 | let vector_fence = fence_segment.vector_difference(); 132 | let cross = vector_fence.cross(vector_moved); 133 | fence.crossed.insert(entity, cross < 0.0); 134 | } 135 | } 136 | } 137 | } 138 | } 139 | 140 | 141 | pub struct FenceSystem; 142 | 143 | 144 | impl FenceSystem {} 145 | 146 | 147 | #[derive(SystemData)] 148 | pub struct FenceSystemData<'a> { 149 | aabb_tree: Read<'a, AABBTree>, 150 | entities: Entities<'a>, 151 | fences: WriteStorage<'a, Fence>, 152 | positions: ReadStorage<'a, Position>, 153 | step_fences: WriteStorage<'a, StepFence>, 154 | velocities: ReadStorage<'a, Velocity>, 155 | zlevels: WriteStorage<'a, ZLevel>, 156 | } 157 | 158 | 159 | impl<'a> FenceSystemData<'a> { 160 | pub fn run_fences(&mut self) { 161 | // Run regular fences 162 | for (fence_ent, mut fence, &Position(pos)) in 163 | (&self.entities, &mut self.fences, &self.positions).join() 164 | { 165 | run_fence( 166 | &self.aabb_tree, 167 | &self.entities, 168 | &self.velocities, 169 | fence_ent, 170 | &mut fence, 171 | pos, 172 | ); 173 | } 174 | } 175 | 176 | pub fn run_step_fences(&mut self) { 177 | for (fence_ent, step_fence, &Position(pos)) in 178 | (&self.entities, &mut self.step_fences, &self.positions).join() 179 | { 180 | run_fence( 181 | &self.aabb_tree, 182 | &self.entities, 183 | &self.velocities, 184 | fence_ent, 185 | &mut step_fence.fence, 186 | pos, 187 | ); 188 | 189 | // run through all crossings and adjust their zlevel 190 | for (entity, is_positive) in step_fence.fence.crossed.iter() { 191 | if let Some(z) = self.zlevels.get_mut(*entity) { 192 | let inc = if *is_positive { 193 | step_fence.step 194 | } else { 195 | -step_fence.step 196 | }; 197 | 198 | z.0 += inc; 199 | trace!("Stepping z {:?} to {:?}", inc, z.0); 200 | } 201 | } 202 | } 203 | } 204 | } 205 | 206 | 207 | impl<'a> System<'a> for FenceSystem { 208 | type SystemData = FenceSystemData<'a>; 209 | 210 | fn run(&mut self, mut data: FenceSystemData) { 211 | data.run_fences(); 212 | data.run_step_fences(); 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /old_gods/manual/src/map_creation.md: -------------------------------------------------------------------------------- 1 | # Map Creation 2 | 3 | 4 | 5 | Terrain and Background 6 | ---------------------- 7 | You can build terrain like you would in any Tiled map. Import a terrain tileset 8 | and start placing tiles down in layers. As expected, layers are rendered from 9 | bottom to top. 10 | 11 | 12 | Animation 13 | --------- 14 | Animations are created using the [Tile Animation Editor][animation_editor]. 15 | Simply placing an animated tile on the map introduces its animation into the ECS. 16 | 17 | 18 | Boundaries 19 | ---------- 20 | Boundaries can be placed on the map as a top level object or can be placed within 21 | tiles in the [Tile Collision Editor][collision_editor]. They then percolate into 22 | the map as component objects of each instance of the tile. This makes it easy to 23 | set the boundary for all instances of tile like "wall left" or "bush" or "rock". 24 | 25 | ### To place a top level object boundary 26 | 27 | * Select the layer the boundary should live in 28 | * Insert a new rectangle object into the layer 29 | * Set the recangle object's `Type` property to `barrier` 30 | 31 | ![object boundary](./img/object_boundary.gif) 32 | 33 | ### To add and place tile boundaries 34 | 35 | * Edit the tileset that includes the tile 36 | * Select the tile 37 | * Open the [Tile Collision Editor][collision_editor] 38 | * Insert a new rectangle object 39 | * Set the recangle object's `Type` property to `barrier` 40 | 41 | ![tile boundary](./img/tile_boundary.gif) 42 | 43 | ### Debug rendering boundaries 44 | 45 | Boundaries can be rendered for debugging using debug toggles in the 46 | `RenderingSystem`: 47 | 48 | ```rust,ignore 49 | let mut rendering_system = RenderingSystem::new(&mut canvas, &texture_creator); 50 | rendering_system.toggles.insert(RenderingToggles::Barriers); 51 | ``` 52 | 53 | 54 | Items 55 | ----- 56 | Items are simply objects with their `Type` property set to `item`. This allows 57 | them to be placed into inventories. 58 | 59 | ![item](./img/item.png) 60 | 61 | ### Optional properties 62 | Items have two optional properties - `usable` and `stack_count`. 63 | 64 | | property name | value | description | 65 | |---------------|-------------------|-------------------------------------------------------------------------------| 66 | | usable | `true`, `false` | whether or not this item is usable from an inventory | 67 | | stack | `0`, `1`, `2` ... | tells the engine this item is stackable, and how many there are in this stack | 68 | 69 | ![item stack](./img/item_stack.png "A stack of raft wood") 70 | 71 | A 10 stack of raft wood. 72 | 73 | Once an item is on the map, it can be picked up by any player. Once an item is 74 | in a player's inventory it can be viewed by hitting the `Y` button and dropped 75 | by hitting the `B` button while the inventory is open and the item is selected. 76 | 77 | 78 | 79 | Inventories 80 | ----------- 81 | An inventory is a collection of items that live on a special `inventories` layer. 82 | Objects can then reference an inventory using a custom parameter `inventory_name` 83 | with its value set to the value of the inventory's `Name` property. 84 | 85 | ### Creating an inventory 86 | 87 | * Create a layer named `inventories` if one doesn't already exist 88 | * In the `inventories` layer, place a new rectangle object and make sure that it 89 | is big enough to hold all the items you plan on keeping within it 90 | * Set the object's `Type` property to `inventory` 91 | * Set the object's `Name` property to something unique 92 | * Place your items within the rectangle object. These items are now "stored" in 93 | the inventory 94 | 95 | ![new inventory](./img/inventory.png) 96 | 97 | ### Referencing an inventory 98 | 99 | To associate an inventory with an object (such as a player, npc or container), 100 | add a custom property `inventory_name` on the object and set its value to the 101 | value of the `name` property of the inventory object. 102 | 103 | ![inventory](./img/inventory_assoc.png) 104 | 105 | 106 | [Actions](./map_creation/actions.md) 107 | ------- 108 | Actions are special components that allow characters to interact with the world. 109 | 110 | ![action display](./img/action_display.png "a player's available action") 111 | 112 | A player's available action 113 | 114 | 115 | [Sprites](./map_creation/sprites.md) 116 | ------- 117 | Sprites are complex game objects whose assets and attributes are defined within 118 | a Tiled map file. Sprites are used to create objects that a player can interact 119 | with. Certain Sprites are controlled by special game systems. 120 | 121 | 122 | Characters 123 | ---------- 124 | A character is a hero, creature, monster, enchanted object, moldy flapjack or 125 | anything else that is controlled by either a player or an automatic inspiration AKA 126 | an AI ;). 127 | 128 | ### To place a character 129 | 130 | * Create an object layer if one doesn't already exist 131 | * Use the `Insert Tile` tool to add a tile object to the layer 132 | * Set the `Name` of the object (can be anything) 133 | * Set the `Type` of the object to `character` 134 | * Add a custom property `control` 135 | * Follow the below steps for player or non-player characters 136 | 137 | ### Player Characters 138 | 139 | * Set the value of the `control` property to `player` 140 | * Add a custom property `player_index` 141 | * Set the value of the `player_index` property to an integer, eg. `0`, `1`, `2` ... 142 | This will represent the controller used to control the character 143 | 144 | ### Non-player Characters 145 | 146 | * Set the value of the `control` property to `npc` 147 | * Add a custom property `ai` 148 | * Set the value of the `ai` property to one of the supported AI types (TODO) 149 | 150 | ![creating a character](./img/player.gif) 151 | 152 | ### Properties 153 | 154 | #### Required 155 | 156 | | property name | value | description | 157 | |---------------|---------------------------|----------------------------------------| 158 | | name | any unique string | name of the character, can be anything | 159 | | type | `character` | tells the engine that this is a toon | 160 | | control | must be `player` or `npc` | determines the control scheme | 161 | 162 | #### conditional 163 | 164 | | property name | value | description | 165 | |---------------|---------------|------------------------------------------| 166 | | player_index | `0`, `1`, ... | the controller used to control this toon | 167 | | ai | ?_ | the ai used to control this toon | 168 | 169 | #### Optional 170 | 171 | | property name | value | description | 172 | |----------------|----------------------------------------------------|-----------------------------------------| 173 | | inventory_name | any unique value, must match some inventory's name | | 174 | | z_inc | an integer, eg. `-1`, `1`, `2` | adds an integer to the object's z_level | 175 | | max_speed | some float value | sets the max speed | 176 | 177 | ### Optional Objects 178 | Barrier and offset objects can be placed on the character's tile/animation and 179 | they will be recognized. 180 | 181 | 182 | [animation_editor]: https://doc.mapeditor.org/en/stable/manual/editing-tilesets/#tile-animation-editor 183 | [collision_editor]: https://doc.mapeditor.org/en/stable/manual/editing-tilesets/#tile-collision-editor 184 | -------------------------------------------------------------------------------- /examples/loading-maps/maps/treasure_chest.json: -------------------------------------------------------------------------------- 1 | { "compressionlevel":0, 2 | "editorsettings": 3 | { 4 | "export": 5 | { 6 | "target":"." 7 | } 8 | }, 9 | "height":1, 10 | "infinite":false, 11 | "layers":[ 12 | { 13 | "id":1, 14 | "layers":[ 15 | { 16 | "id":11, 17 | "layers":[ 18 | { 19 | "data":[32], 20 | "height":1, 21 | "id":12, 22 | "name":"tiles", 23 | "opacity":1, 24 | "type":"tilelayer", 25 | "visible":true, 26 | "width":1, 27 | "x":0, 28 | "y":0 29 | }], 30 | "name":"empty", 31 | "opacity":1, 32 | "type":"group", 33 | "visible":false, 34 | "x":0, 35 | "y":0 36 | }, 37 | { 38 | "id":2, 39 | "layers":[ 40 | { 41 | "data":[30], 42 | "height":1, 43 | "id":4, 44 | "name":"tiles", 45 | "opacity":1, 46 | "type":"tilelayer", 47 | "visible":true, 48 | "width":1, 49 | "x":0, 50 | "y":0 51 | }, 52 | { 53 | "draworder":"topdown", 54 | "id":5, 55 | "name":"actions", 56 | "objects":[ 57 | { 58 | "height":0, 59 | "id":31, 60 | "name":"open", 61 | "point":true, 62 | "properties":[ 63 | { 64 | "name":"fitness", 65 | "type":"string", 66 | "value":"all [has_item \"white key\"]" 67 | }, 68 | { 69 | "name":"lifespan", 70 | "type":"string", 71 | "value":"1" 72 | }, 73 | { 74 | "name":"text", 75 | "type":"string", 76 | "value":"Unlock with white key" 77 | }], 78 | "rotation":0, 79 | "type":"action", 80 | "visible":true, 81 | "width":0, 82 | "x":23.5, 83 | "y":37.5 84 | }], 85 | "opacity":1, 86 | "type":"objectgroup", 87 | "visible":true, 88 | "x":0, 89 | "y":0 90 | }], 91 | "name":"closed", 92 | "opacity":1, 93 | "type":"group", 94 | "visible":false, 95 | "x":0, 96 | "y":0 97 | }, 98 | { 99 | "id":6, 100 | "layers":[ 101 | { 102 | "data":[30], 103 | "height":1, 104 | "id":8, 105 | "name":"tiles", 106 | "opacity":1, 107 | "type":"tilelayer", 108 | "visible":true, 109 | "width":1, 110 | "x":0, 111 | "y":0 112 | }, 113 | { 114 | "draworder":"topdown", 115 | "id":9, 116 | "name":"actions", 117 | "objects":[ 118 | { 119 | "height":0, 120 | "id":30, 121 | "name":"loot", 122 | "point":true, 123 | "properties":[ 124 | { 125 | "name":"fitness", 126 | "type":"string", 127 | "value":"has_inventory" 128 | }, 129 | { 130 | "name":"lifespan", 131 | "type":"string", 132 | "value":"forever" 133 | }, 134 | { 135 | "name":"text", 136 | "type":"string", 137 | "value":"Loot the treasure" 138 | }], 139 | "rotation":0, 140 | "type":"action", 141 | "visible":true, 142 | "width":0, 143 | "x":24, 144 | "y":40 145 | }], 146 | "opacity":1, 147 | "type":"objectgroup", 148 | "visible":true, 149 | "x":0, 150 | "y":0 151 | }], 152 | "name":"open", 153 | "opacity":1, 154 | "type":"group", 155 | "visible":true, 156 | "x":0, 157 | "y":0 158 | }, 159 | { 160 | "draworder":"topdown", 161 | "id":10, 162 | "name":"barrier", 163 | "objects":[ 164 | { 165 | "height":13, 166 | "id":26, 167 | "name":"chest barrier", 168 | "rotation":0, 169 | "type":"barrier", 170 | "visible":true, 171 | "width":23.5, 172 | "x":12.25, 173 | "y":21.25 174 | }], 175 | "opacity":1, 176 | "type":"objectgroup", 177 | "visible":false, 178 | "x":0, 179 | "y":0 180 | }], 181 | "name":"gold", 182 | "opacity":1, 183 | "type":"group", 184 | "visible":true, 185 | "x":0, 186 | "y":0 187 | }], 188 | "nextlayerid":14, 189 | "nextobjectid":32, 190 | "orientation":"orthogonal", 191 | "renderorder":"right-down", 192 | "tiledversion":"1.3.1", 193 | "tileheight":48, 194 | "tilesets":[ 195 | { 196 | "firstgid":1, 197 | "source":"tilesets\/items.json" 198 | }], 199 | "tilewidth":48, 200 | "type":"map", 201 | "version":1.2, 202 | "width":1 203 | } -------------------------------------------------------------------------------- /examples/loading-maps/src/lib.rs: -------------------------------------------------------------------------------- 1 | use log::{trace, Level}; 2 | use mogwai::prelude::*; 3 | use old_gods::{fetch, prelude::*}; 4 | use std::{ 5 | collections::HashSet, 6 | panic, 7 | sync::{Arc, Mutex}, 8 | }; 9 | use wasm_bindgen::prelude::*; 10 | use web_sys::HtmlElement; 11 | 12 | mod components; 13 | mod systems; 14 | 15 | mod render; 16 | use render::WebRenderingContext; 17 | 18 | // When the `wee_alloc` feature is enabled, use `wee_alloc` as the global 19 | // allocator. 20 | #[cfg(feature = "wee_alloc")] 21 | #[global_allocator] 22 | static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; 23 | 24 | fn maps() -> Vec { 25 | vec![ 26 | "maps/tiles_test.json".into(), 27 | "maps/collision_detection.json".into(), 28 | "maps/audio_test.json".into(), 29 | "maps/full_test.json".into(), 30 | ] 31 | } 32 | 33 | 34 | #[derive(Clone)] 35 | enum InMsg { 36 | Load(String), 37 | LoadError(String), 38 | Loaded(Tiledmap), 39 | } 40 | 41 | 42 | #[derive(Clone)] 43 | enum OutMsg { 44 | Status(String), 45 | } 46 | 47 | 48 | impl OutMsg { 49 | fn status_msg(&self) -> Option { 50 | match self { 51 | OutMsg::Status(msg) => Some(msg.clone()), //_ => { None } 52 | } 53 | } 54 | } 55 | 56 | 57 | pub type WebEngine = Engine<'static, 'static, WebRenderingContext, HtmlImageResources>; 58 | 59 | 60 | struct App { 61 | ecs: Arc>, 62 | current_map_path: Option, 63 | } 64 | 65 | 66 | impl App { 67 | fn new(ecs: Arc>) -> App { 68 | App { 69 | ecs, 70 | current_map_path: None, 71 | } 72 | } 73 | } 74 | 75 | 76 | impl mogwai::prelude::Component for App { 77 | type ModelMsg = InMsg; 78 | type ViewMsg = OutMsg; 79 | type DomNode = HtmlElement; 80 | 81 | fn update(&mut self, msg: &InMsg, tx_view: &Transmitter, sub: &Subscriber) { 82 | match msg { 83 | InMsg::Load(path) => { 84 | let ecs = self.ecs.try_lock().expect("no lock on ecs"); 85 | 86 | self.current_map_path = Some(format!("{}/{}", ecs.base_url, path)); 87 | tx_view.send(&OutMsg::Status(format!("starting load of {}", path))); 88 | let path = path.clone(); 89 | let base_url = ecs.base_url.clone(); 90 | sub.send_async(async move { 91 | let tiledmap = Tiledmap::from_url(&base_url, &path, fetch::from_url).await; 92 | match tiledmap { 93 | Err(msg) => InMsg::LoadError(msg), 94 | Ok(map) => InMsg::Loaded(map), 95 | } 96 | }); 97 | } 98 | InMsg::LoadError(msg) => { 99 | self.current_map_path = None; 100 | tx_view.send(&OutMsg::Status(format!("Loading error:\n{:#?}", msg))); 101 | } 102 | InMsg::Loaded(map) => { 103 | let mut ecs = self.ecs.try_lock().expect("no lock on ecs"); 104 | ecs.world.delete_all(); 105 | 106 | if let Some((width, height)) = map.get_suggested_viewport_size() { 107 | trace!("got map viewport size: {} {}", width, height); 108 | ecs.set_map_viewport_size(width, height); 109 | trace!("resetting viewport position to (0, 0)"); 110 | ecs.set_map_viewport_top_left(0, 0); 111 | } 112 | let num_entities = { 113 | let entities = ecs.world.system_data::(); 114 | (&entities).join().count() 115 | }; 116 | tx_view.send(&OutMsg::Status(format!( 117 | "Successfully loaded {} entities from {}", 118 | num_entities, 119 | self.current_map_path.as_ref().unwrap(), 120 | ))); 121 | if ecs.is_debug() { 122 | let mut ecs_toggles: Write> = ecs.world.system_data(); 123 | let map_toggles = RenderingToggles::from_properties(&map.properties); 124 | *ecs_toggles = map_toggles; 125 | } 126 | { 127 | let mut data: old_gods::systems::tiled::InsertMapData = ecs.world.system_data(); 128 | old_gods::systems::tiled::insert_map(map, &mut data); 129 | } 130 | 131 | ecs.restart_time(); 132 | } 133 | } 134 | } 135 | 136 | fn view(&self, tx: Transmitter, rx: Receiver) -> Gizmo { 137 | div().class("container-fluid").with( 138 | maps() 139 | .into_iter() 140 | .fold( 141 | fieldset().with(legend().text("Old Gods Map Loader")), 142 | |fieldset, map| { 143 | fieldset.with( 144 | div().with( 145 | a().attribute("href", &format!("#{}", &map)) 146 | .text(&map) 147 | .tx_on( 148 | "click", 149 | tx.contra_map(move |_| InMsg::Load(map.to_string())), 150 | ), 151 | ), 152 | ) 153 | }, 154 | ) 155 | .with(pre().rx_text("", rx.branch_filter_map(|msg| msg.status_msg()))) 156 | .with({ 157 | let ecs = self.ecs.try_lock().expect("no lock on ecs in view"); 158 | let canvas = ecs.rendering_context.canvas().expect("no canvas in view"); 159 | Gizmo::wrapping(canvas).class("embed-responsive embed-responsive-16by9") 160 | }), 161 | ) 162 | } 163 | } 164 | 165 | 166 | #[wasm_bindgen] 167 | pub fn main() -> Result<(), JsValue> { 168 | panic::set_hook(Box::new(console_error_panic_hook::hook)); 169 | console_log::init_with_level(Level::Trace).unwrap(); 170 | 171 | let app_ecs = { 172 | let dispatcher = DispatcherBuilder::new() 173 | .with_thread_local(systems::action::ActionSystem) 174 | .with_thread_local(systems::inventory::InventorySystem::new()) 175 | .with_thread_local(systems::looting::LootingSystem); 176 | let mut ecs = Engine::new_with( 177 | "http://localhost:8888", 178 | dispatcher, 179 | WebRenderingContext::default, 180 | ); 181 | ecs.set_window_size(1600, 900); 182 | ecs.rendering_context 183 | .0 184 | .context 185 | .set_image_smoothing_enabled(false); 186 | ecs.map_rendering_context 187 | .0 188 | .context 189 | .set_image_smoothing_enabled(false); 190 | if cfg!(debug_assertions) { 191 | ecs.set_debug_mode(true); 192 | } 193 | Arc::new(Mutex::new(ecs)) 194 | }; 195 | 196 | // Set up the game loop 197 | let ecs = app_ecs.clone(); 198 | request_animation_frame(move || { 199 | let mut ecs = ecs 200 | .try_lock() 201 | .expect("no lock on ecs - request animation loop"); 202 | ecs.maintain(); 203 | ecs.render().unwrap(); 204 | // We always want to reschedule this animation frame 205 | true 206 | }); 207 | 208 | App::new(app_ecs).into_component().run_init({ 209 | let hash = window().location().hash().expect("no hash object"); 210 | let ndx = hash.find('#').unwrap_or(0); 211 | let (_, hash) = hash.split_at(ndx); 212 | let mut msgs = vec![]; 213 | for map in maps().into_iter() { 214 | if hash.ends_with(&map) { 215 | msgs.push(InMsg::Load(map.clone())); 216 | } 217 | } 218 | msgs 219 | }) 220 | } 221 | -------------------------------------------------------------------------------- /old_gods/src/geom/shape.rs: -------------------------------------------------------------------------------- 1 | use specs::prelude::*; 2 | 3 | use super::super::prelude::{AABB, V2}; 4 | 5 | // TODO: Real SAT for polygons, AABB and circles. 6 | // See http://www.metanetsoftware.com/2016/n-tutorial-a-collision-detection-and-response 7 | 8 | #[derive(Debug, Clone, PartialEq)] 9 | /// A number of different shapes. A shape itself doesn't have a position, it 10 | /// only describes the dimensions and separating axes. 11 | pub enum Shape { 12 | /// A box defined by two points. 13 | Box { 14 | lower: V2, // top left 15 | upper: V2, // bottom right 16 | }, 17 | 18 | /// An assumed to be convex polygon 19 | Polygon { vertices: Vec }, 20 | } 21 | 22 | 23 | impl Shape { 24 | pub fn box_with_size(w: f32, h: f32) -> Shape { 25 | Shape::Box { 26 | upper: V2::origin(), 27 | lower: V2::new(w, h), 28 | } 29 | } 30 | 31 | /// The axis aligned box needed to contain the shape 32 | pub fn aabb(&self) -> AABB { 33 | match &self { 34 | Shape::Box { lower, upper } => AABB::from_points(*lower, *upper), 35 | Shape::Polygon { vertices } => { 36 | let mut left = std::f32::INFINITY; 37 | let mut right = std::f32::NEG_INFINITY; 38 | let mut top = std::f32::INFINITY; 39 | let mut bottom = std::f32::NEG_INFINITY; 40 | 41 | for v in vertices { 42 | if v.x < left { 43 | left = v.x; 44 | } 45 | 46 | if v.x > right { 47 | right = v.x; 48 | } 49 | 50 | if v.y < top { 51 | top = v.y; 52 | } 53 | 54 | if v.y > bottom { 55 | bottom = v.y; 56 | } 57 | } 58 | 59 | AABB::from_points(V2::new(left, top), V2::new(right, bottom)) 60 | } 61 | } 62 | } 63 | 64 | /// The width and height of the box needed to fully contain the shape. 65 | pub fn extents(&self) -> V2 { 66 | self.aabb().extents 67 | } 68 | 69 | /// Scale the shape in x and y. 70 | pub fn into_scaled(self, scale: &V2) -> Shape { 71 | match self { 72 | Shape::Box { upper, lower } => Shape::Box { 73 | upper: upper * *scale, 74 | lower: lower * *scale, 75 | }, 76 | Shape::Polygon { vertices } => { 77 | let vertices: Vec = vertices.into_iter().map(|v| v * *scale).collect(); 78 | Shape::Polygon { vertices } 79 | } 80 | } 81 | } 82 | 83 | /// Return a new shape translated by a vector 84 | pub fn translated(&self, v: &V2) -> Shape { 85 | match &self { 86 | Shape::Box { upper, lower } => Shape::Box { 87 | upper: *upper + *v, 88 | lower: *lower + *v, 89 | }, 90 | Shape::Polygon { vertices } => Shape::Polygon { 91 | vertices: vertices.iter().map(|p| *p + *v).collect(), 92 | }, 93 | } 94 | } 95 | 96 | 97 | /// A list of all the vertices in this shape. 98 | pub fn vertices(&self) -> Vec { 99 | match self { 100 | Shape::Box { lower, upper } => vec![ 101 | *lower, 102 | V2::new(upper.x, lower.y), 103 | *upper, 104 | V2::new(lower.x, upper.y), 105 | ], 106 | Shape::Polygon { vertices } => vertices.clone(), 107 | } 108 | } 109 | 110 | /// A list of all the vertices in this shape with the first vertex cloned and 111 | /// appended to the end, closing the polygon. 112 | pub fn vertices_closed(&self) -> Vec { 113 | let mut vertices = self.vertices(); 114 | let first_vertex = vertices.first().cloned(); 115 | 116 | if first_vertex.is_none() { 117 | // This is an empty polygon, return an empty list 118 | return vec![]; 119 | } 120 | 121 | let first_vertex = first_vertex.unwrap(); 122 | 123 | vertices.push(first_vertex); 124 | 125 | vertices 126 | } 127 | 128 | /// A list of potential separating axes as unit vectors. 129 | /// See https://www.metanetsoftware.com/2016/n-tutorial-a-collision-detection-and-response#section1 130 | pub fn potential_separating_axes(&self) -> Vec { 131 | let vertices = self.vertices_closed(); 132 | let mut out_vertices = vec![]; 133 | 134 | for i in 1..vertices.len() { 135 | let p1 = vertices[i - 1]; 136 | let p2 = vertices[i]; 137 | let v = p1 - p2; 138 | // get the perpendicular vector 139 | let pv = V2::new(-v.y, v.x); 140 | out_vertices.push(pv.unitize()); 141 | } 142 | 143 | out_vertices.into_iter().filter_map(|v| v).collect() 144 | } 145 | 146 | 147 | /// Returns the midpoints of each edge in the shape. 148 | /// Used for (at least) debug drawing info. 149 | pub fn midpoints(&self) -> Vec { 150 | let vs = self.vertices_closed(); 151 | 152 | (1..vs.len()) 153 | .map(|i| { 154 | let p2 = vs[i]; 155 | let p1 = vs[i - 1]; 156 | p1 + (p2 - p1).scalar_mul(0.5) 157 | }) 158 | .collect() 159 | } 160 | 161 | 162 | /// Returns a simple range of the vec. 163 | pub fn range(v: &[f32]) -> (f32, f32) { 164 | let start = (std::f32::INFINITY, std::f32::NEG_INFINITY); 165 | v.iter().fold(start, |(min, max), n| { 166 | (f32::min(min, *n), f32::max(max, *n)) 167 | }) 168 | } 169 | 170 | 171 | /// Return the ranged projection of this shape on an axis. 172 | pub fn ranged_projection_on( 173 | &self, 174 | p: V2, // the world position of this shape 175 | axis: V2, // the axis we're projecting onto 176 | ) -> (f32, f32) { 177 | let points1d = self 178 | .vertices() 179 | .into_iter() 180 | .map(|v| { 181 | let loc = p + v; 182 | axis.dot(loc) 183 | }) 184 | .collect::>(); 185 | Self::range(&points1d) 186 | } 187 | 188 | 189 | /// Returns the minimum translation vector needed to push an intersecting 190 | /// shape out of intersection. If the two are not intersecting it returns 191 | /// `None`. This should be called after the broadphase of detection. 192 | pub fn mtv_apart( 193 | &self, 194 | this_position: V2, // This shape's world location 195 | other_shape: &Shape, // The other shape 196 | other_position: V2, // The other shape's world location 197 | ) -> Option { 198 | // Maintain the smallest axis overlap that we'll later use as the mtv 199 | let mut overlap: Option<(f32, V2)> = None; 200 | 201 | let mut axes: Vec = self.potential_separating_axes(); 202 | axes.extend(other_shape.potential_separating_axes()); 203 | 204 | for axis in axes { 205 | let (my_start, my_end) = self.ranged_projection_on(this_position, axis); 206 | let (their_start, their_end) = other_shape.ranged_projection_on(other_position, axis); 207 | let does_collide = my_end > their_start && my_start < their_end; 208 | if !does_collide { 209 | // Early exit! These shapes don't overlap 210 | return None; 211 | } 212 | 213 | let this_overlap = my_end - their_start; 214 | let last_overlap = overlap.map(|(o, _)| o).unwrap_or(std::f32::INFINITY); 215 | if this_overlap.abs() < last_overlap.abs() { 216 | overlap = Some((this_overlap, axis)); 217 | } 218 | } 219 | 220 | overlap.map(|(o, v)| v.scalar_mul(o)) 221 | } 222 | } 223 | 224 | 225 | /// Lots of things have shapes in OG games and they change often. 226 | /// We need to sync shapes with an rtree resource so shape's storage is a flagged 227 | /// storage type. 228 | /// @See https://slide-rs.github.io/specs/12_tracked.html 229 | impl Component for Shape { 230 | type Storage = FlaggedStorage>; 231 | } 232 | -------------------------------------------------------------------------------- /old_gods/src/components/rendering.rs: -------------------------------------------------------------------------------- 1 | //! Rendering data. 2 | use specs::prelude::*; 3 | use std::collections::{HashMap, HashSet}; 4 | 5 | use super::super::{ 6 | components::tiled::{Property, AABB as TiledAABB}, 7 | prelude::{Color, FontDetails, V2}, 8 | }; 9 | 10 | 11 | /// ## ZLevel 12 | /// Determines rendering order. 13 | #[derive(Debug, Clone, Copy, PartialEq)] 14 | pub struct ZLevel(pub f32); 15 | 16 | 17 | impl Component for ZLevel { 18 | type Storage = VecStorage; 19 | } 20 | 21 | 22 | impl ZLevel { 23 | /// Adds an amount and returns a new zlevel. 24 | pub fn add(&self, n: f32) -> ZLevel { 25 | ZLevel(self.0 + n) 26 | } 27 | } 28 | 29 | 30 | /// Helps render tiles by allowing an origin offset during z-sorting and 31 | /// rendering. 32 | #[derive(Debug, Clone, Copy, PartialEq)] 33 | pub struct OriginOffset(pub V2); 34 | 35 | 36 | impl OriginOffset { 37 | pub fn tiled_type() -> String { 38 | "origin_offset".to_string() 39 | } 40 | } 41 | 42 | 43 | impl Component for OriginOffset { 44 | type Storage = DenseVecStorage; 45 | } 46 | 47 | 48 | #[derive(Debug, Clone, PartialEq, Hash)] 49 | /// A frame within a texture. 50 | pub struct TextureFrame { 51 | /// The name of the sprite sheet. 52 | pub sprite_sheet: String, 53 | 54 | /// The source rectangle within the spritesheet. 55 | pub source_aabb: TiledAABB, 56 | 57 | /// The destination size 58 | pub size: (u32, u32), 59 | 60 | pub is_flipped_horizontally: bool, 61 | 62 | pub is_flipped_vertically: bool, 63 | 64 | pub is_flipped_diagonally: bool, 65 | } 66 | 67 | 68 | impl TextureFrame { 69 | pub fn scale(&self) -> V2 { 70 | let sx = self.size.0 as f32 / self.source_aabb.w as f32; 71 | let sy = self.size.1 as f32 / self.source_aabb.h as f32; 72 | V2::new(sx, sy) 73 | } 74 | } 75 | 76 | 77 | #[derive(Debug, Clone, PartialEq, Hash)] 78 | /// Drawn text. 79 | pub struct Text { 80 | pub text: String, 81 | pub font: FontDetails, 82 | pub color: Color, 83 | pub size: (u32, u32), 84 | } 85 | 86 | 87 | impl Text { 88 | pub fn as_key(&self) -> String { 89 | format!("{:?}", self) 90 | } 91 | } 92 | 93 | 94 | /// The base types that can be rendered 95 | #[derive(Debug, Clone, PartialEq, Hash)] 96 | pub enum RenderingPrimitive { 97 | TextureFrame(TextureFrame), 98 | Text(Text), 99 | } 100 | 101 | 102 | /// A composite rendering type representing a display list 103 | #[derive(Debug, Clone, PartialEq, Hash)] 104 | pub struct Rendering { 105 | /// The alpha mod of this rendering 106 | pub alpha: u8, 107 | 108 | /// The primitive of this rendering 109 | pub primitive: RenderingPrimitive, 110 | } 111 | 112 | 113 | impl Rendering { 114 | pub fn from_frame(frame: TextureFrame) -> Rendering { 115 | Rendering { 116 | alpha: 255, 117 | primitive: RenderingPrimitive::TextureFrame(frame), 118 | } 119 | } 120 | 121 | pub fn from_text(text: Text) -> Rendering { 122 | Rendering { 123 | alpha: 255, 124 | primitive: RenderingPrimitive::Text(text), 125 | } 126 | } 127 | 128 | pub fn as_frame(&self) -> Option<&TextureFrame> { 129 | match &self.primitive { 130 | RenderingPrimitive::TextureFrame(t) => Some(t), 131 | _ => None, 132 | } 133 | } 134 | 135 | 136 | pub fn size(&self) -> (u32, u32) { 137 | match &self.primitive { 138 | RenderingPrimitive::TextureFrame(t) => t.size, 139 | RenderingPrimitive::Text(t) => t.size, 140 | } 141 | } 142 | } 143 | 144 | 145 | impl Component for Rendering { 146 | type Storage = DenseVecStorage; 147 | } 148 | 149 | 150 | #[derive(Debug, Clone, Hash, PartialEq, Eq)] 151 | /// Various toggles to display or hide things during rendering. 152 | /// Toggling the rendering of various debug infos can be done by adding a custom 153 | /// property to your map file or individual objects. 154 | pub enum RenderingToggles { 155 | /// Toggle marking actions. 156 | Actions, 157 | 158 | /// Toggle rendering positions. 159 | Positions, 160 | 161 | /// Toggle rendering barriers. 162 | Barriers, 163 | 164 | /// Toggle rendering the AABBTree. 165 | AABBTree, 166 | 167 | /// Toggle rendering velocities. 168 | Velocities, 169 | 170 | /// Toggle rendering zlevels. 171 | ZLevels, 172 | 173 | /// Toggle marking players. 174 | Players, 175 | 176 | /// Toggle marking the screen 177 | Screen, 178 | 179 | /// Toggle displaying the FPS. 180 | FPS, 181 | 182 | /// Render zones 183 | Zones, 184 | 185 | /// Fences 186 | Fences, 187 | 188 | /// Display the apparent entity count 189 | EntityCount, 190 | 191 | /// Display collision system information 192 | CollisionInfo, 193 | 194 | /// Display shapes 195 | Shapes, 196 | 197 | /// Display something else. 198 | /// Used for extension. 199 | Other(String), 200 | } 201 | 202 | 203 | /// Used for on-screen debugging of specific objects. 204 | #[derive(Clone, Debug)] 205 | pub struct ObjectRenderingToggles(pub HashSet); 206 | 207 | 208 | impl Component for ObjectRenderingToggles { 209 | type Storage = HashMapStorage; 210 | } 211 | 212 | 213 | impl RenderingToggles { 214 | pub fn property_map() -> HashMap { 215 | use RenderingToggles::*; 216 | let props = vec![ 217 | Actions, 218 | Positions, 219 | Barriers, 220 | AABBTree, 221 | Velocities, 222 | ZLevels, 223 | Players, 224 | Screen, 225 | FPS, 226 | Zones, 227 | Fences, 228 | EntityCount, 229 | CollisionInfo, 230 | Shapes, 231 | ]; 232 | props 233 | .into_iter() 234 | .map(|t| (t.property_str().to_string(), t)) 235 | .collect() 236 | } 237 | 238 | 239 | pub fn property_str(&self) -> &str { 240 | use RenderingToggles::*; 241 | match self { 242 | Actions => "toggle_rendering_actions", 243 | Positions => "toggle_rendering_positions", 244 | Barriers => "toggle_rendering_barriers", 245 | AABBTree => "toggle_rendering_aabb_tree", 246 | Velocities => "toggle_rendering_velocities", 247 | ZLevels => "toggle_rendering_z_levels", 248 | Players => "toggle_rendering_players", 249 | Screen => "toggle_rendering_screen", 250 | FPS => "toggle_rendering_fps", 251 | Zones => "toggle_rendering_zones", 252 | Fences => "toggle_rendering_fences", 253 | EntityCount => "toggle_rendering_entity_count", 254 | CollisionInfo => "toggle_rendering_collision_info", 255 | Shapes => "toggle_rendering_shapes", 256 | Other(s) => s, 257 | } 258 | } 259 | 260 | pub fn from_properties(props: &[Property]) -> HashSet { 261 | let toggles = Self::property_map(); 262 | let mut set = HashSet::new(); 263 | for prop in props { 264 | if !prop.name.starts_with("toggle_rendering_") { 265 | continue; 266 | } 267 | let toggle = toggles 268 | .get(&prop.name) 269 | .cloned() 270 | .unwrap_or_else(|| RenderingToggles::Other(prop.name.clone())); 271 | let should_set = prop.value.as_bool().unwrap_or(false); 272 | if should_set { 273 | set.insert(toggle.clone()); 274 | } 275 | } 276 | set 277 | } 278 | 279 | pub fn remove_from_properties( 280 | props: &mut HashMap, 281 | ) -> Option { 282 | let props_vec: Vec = props.iter().map(|(_, p)| p.clone()).collect(); 283 | let toggles = Self::from_properties(&props_vec); 284 | if !toggles.is_empty() { 285 | for toggle in toggles.iter() { 286 | let _ = props.remove(toggle.property_str()); 287 | } 288 | Some(ObjectRenderingToggles(toggles)) 289 | } else { 290 | None 291 | } 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /examples/loading-maps/maps/collision_detection.json: -------------------------------------------------------------------------------- 1 | { "compressionlevel":0, 2 | "editorsettings": 3 | { 4 | "export": 5 | { 6 | "target":"." 7 | } 8 | }, 9 | "height":6, 10 | "infinite":false, 11 | "layers":[ 12 | { 13 | "draworder":"topdown", 14 | "id":1, 15 | "name":"shapes", 16 | "objects":[ 17 | { 18 | "height":36.5, 19 | "id":1, 20 | "name":"box_a", 21 | "rotation":0, 22 | "type":"barrier", 23 | "visible":true, 24 | "width":62.5, 25 | "x":11, 26 | "y":15 27 | }, 28 | { 29 | "height":42, 30 | "id":2, 31 | "name":"box_b", 32 | "rotation":0, 33 | "type":"barrier", 34 | "visible":true, 35 | "width":57.5, 36 | "x":51.5, 37 | "y":34.5 38 | }, 39 | { 40 | "height":67.9720812182741, 41 | "id":6, 42 | "name":"box_d", 43 | "rotation":0, 44 | "type":"barrier", 45 | "visible":true, 46 | "width":103.46192893401, 47 | "x":51.9162436548223, 48 | "y":200.027918781726 49 | }, 50 | { 51 | "height":0, 52 | "id":7, 53 | "name":"poly_a", 54 | "polygon":[ 55 | { 56 | "x":0, 57 | "y":0 58 | }, 59 | { 60 | "x":83.010152284264, 61 | "y":-42.1065989847716 62 | }, 63 | { 64 | "x":98.6497461928934, 65 | "y":36.6928934010152 66 | }, 67 | { 68 | "x":52.9340101522843, 69 | "y":42.1065989847716 70 | }], 71 | "rotation":0, 72 | "type":"barrier", 73 | "visible":true, 74 | "width":0, 75 | "x":14.6218274111675, 76 | "y":191.606598984772 77 | }, 78 | { 79 | "height":0, 80 | "id":8, 81 | "name":"tri_a", 82 | "polygon":[ 83 | { 84 | "x":0, 85 | "y":0 86 | }, 87 | { 88 | "x":93.9096385542169, 89 | "y":30.7771084337349 90 | }, 91 | { 92 | "x":43.4036144578313, 93 | "y":106.536144578313 94 | }], 95 | "rotation":0, 96 | "type":"barrier", 97 | "visible":true, 98 | "width":0, 99 | "x":169.795180722892, 100 | "y":11 101 | }, 102 | { 103 | "height":0, 104 | "id":9, 105 | "name":"tri_b", 106 | "polygon":[ 107 | { 108 | "x":0, 109 | "y":0 110 | }, 111 | { 112 | "x":36.3012048192771, 113 | "y":-78.9156626506024 114 | }, 115 | { 116 | "x":75.7590361445783, 117 | "y":-1.57831325301205 118 | }], 119 | "rotation":0, 120 | "type":"barrier", 121 | "visible":true, 122 | "width":0, 123 | "x":176.10843373494, 124 | "y":142 125 | }, 126 | { 127 | "height":0, 128 | "id":11, 129 | "name":"tri_c", 130 | "polygon":[ 131 | { 132 | "x":0, 133 | "y":0 134 | }, 135 | { 136 | "x":87.3846153846154, 137 | "y":87.3846153846154 138 | }, 139 | { 140 | "x":12.7435897435897, 141 | "y":59.1666666666667 142 | }], 143 | "rotation":0, 144 | "type":"barrier", 145 | "visible":true, 146 | "width":0, 147 | "x":164.98717948718, 148 | "y":192.115384615385 149 | }, 150 | { 151 | "height":0, 152 | "id":12, 153 | "name":"tri_d", 154 | "polygon":[ 155 | { 156 | "x":0, 157 | "y":0 158 | }, 159 | { 160 | "x":67.3589743589744, 161 | "y":-41.8717948717949 162 | }, 163 | { 164 | "x":57.3461538461538, 165 | "y":32.7692307692308 166 | }], 167 | "rotation":0, 168 | "type":"barrier", 169 | "visible":true, 170 | "width":0, 171 | "x":211.153846153846, 172 | "y":222.871794871795 173 | }, 174 | { 175 | "height":0, 176 | "id":13, 177 | "name":"outside", 178 | "polygon":[ 179 | { 180 | "x":0, 181 | "y":0 182 | }, 183 | { 184 | "x":42.8148148148148, 185 | "y":-34.8148148148148 186 | }, 187 | { 188 | "x":68, 189 | "y":-4.35185185185185 190 | }, 191 | { 192 | "x":53.7283950617284, 193 | "y":30.462962962963 194 | }, 195 | { 196 | "x":12.5925925925926, 197 | "y":35.6851851851852 198 | }], 199 | "rotation":0, 200 | "type":"barrier", 201 | "visible":true, 202 | "width":0, 203 | "x":8.5, 204 | "y":121.314814814815 205 | }, 206 | { 207 | "height":33, 208 | "id":14, 209 | "name":"inside", 210 | "rotation":0, 211 | "type":"barrier", 212 | "visible":true, 213 | "width":32.5, 214 | "x":24, 215 | "y":114 216 | }], 217 | "opacity":1, 218 | "type":"objectgroup", 219 | "visible":true, 220 | "x":0, 221 | "y":0 222 | }], 223 | "nextlayerid":2, 224 | "nextobjectid":16, 225 | "orientation":"orthogonal", 226 | "properties":[ 227 | { 228 | "name":"toggle_rendering_collision_info", 229 | "type":"bool", 230 | "value":true 231 | }, 232 | { 233 | "name":"toggle_rendering_fps", 234 | "type":"bool", 235 | "value":true 236 | }, 237 | { 238 | "name":"viewport_height_tiles", 239 | "type":"int", 240 | "value":6 241 | }, 242 | { 243 | "name":"viewport_width_tiles", 244 | "type":"int", 245 | "value":6 246 | }], 247 | "renderorder":"right-down", 248 | "tiledversion":"1.3.1", 249 | "tileheight":48, 250 | "tilesets":[], 251 | "tilewidth":48, 252 | "type":"map", 253 | "version":1.2, 254 | "width":6 255 | } --------------------------------------------------------------------------------