├── .gitignore ├── src ├── main.rs ├── error.rs ├── animate.rs ├── shift.rs ├── edit.rs ├── saveload.rs ├── lib.rs ├── control.rs ├── input.rs ├── draw.rs └── physics.rs ├── notes ├── README.md ├── ideas.md ├── howdoiphysics.md └── howdoiphase.md ├── Cargo.toml ├── README.md ├── LICENSE └── default-storage.ron /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | /target/ 3 | **/*.rs.bk 4 | Cargo.lock 5 | state.json 6 | storage.ron 7 | .idea 8 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | extern crate stacked_worlds; 2 | 3 | fn main() { 4 | stacked_worlds::run().unwrap_or_else(|err| { 5 | eprintln!("Error: {}", err); 6 | }); 7 | } 8 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | pub use failure::{Error, ResultExt}; 2 | 3 | #[derive(Debug, Fail)] 4 | pub enum GameError { 5 | #[fail(display = "cannot create game window: {}", reason)] 6 | WindowError { reason: String } 7 | } 8 | 9 | -------------------------------------------------------------------------------- /notes/README.md: -------------------------------------------------------------------------------- 1 | Scribbled notes 2 | =============== 3 | 4 | Sometimes I have no idea how to do things, and I scribble down notes to help me 5 | remember details. 6 | 7 | Probably a good idea to upload those here, might be fun to remember the 8 | paths I did not take. 9 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "stacked-worlds" 3 | version = "0.1.0" 4 | authors = ["Andrei Vasiliu "] 5 | repository = "https://github.com/andreivasiliu/stacked-worlds" 6 | license = "LGPL-3.0" 7 | readme = "README.md" 8 | 9 | [dependencies] 10 | piston = "0.36" 11 | piston2d-graphics = "0.26" 12 | pistoncore-glutin_window = "0.46" 13 | piston2d-opengl_graphics = "0.53" 14 | failure = "0.1" 15 | serde_derive = "1.0.45" 16 | serde_json = "1.0.17" 17 | serde = "1.0.45" 18 | specs = { version = "0.12", features = ["serde"] } 19 | specs-derive = "0.2" 20 | ron = "0.2" 21 | nphysics2d = "0.8" 22 | nalgebra = "0.14.4" 23 | ncollide2d = "0.15.3" 24 | -------------------------------------------------------------------------------- /notes/ideas.md: -------------------------------------------------------------------------------- 1 | Ideas and plans: 2 | * Objects, inventory, manipulation 3 | * Objects collide with ground and nothing else 4 | * Colored background for each room 5 | * Make an initial seeded random based on the entity 6 | * Just draw it normally, upgrade DrawRoom to use Rectangle 7 | * Background/foreground layers 8 | * Ability to switch layer being drawn upon 9 | * Ditch piston-graphics, pick up ggez 10 | * Move nphysics worlds into a component storage 11 | * Fix drawing so gl_graphics.draw is only ever called once 12 | * Maybe run all draw systems inside gl_graphics.draw 13 | * Maybe change all draw systems to insert drawable shapes into a queue 14 | * Integrate with conrod 15 | * Maybe integrate with specs-hierarchy if InRoom is not enough 16 | -------------------------------------------------------------------------------- /notes/howdoiphysics.md: -------------------------------------------------------------------------------- 1 | ### Components 2 | - Position 3 | - Velocity 4 | - Angle 5 | - Shape 6 | 7 | 8 | ### Object 9 | Check: Shape + InRoom + Position + Velocity + Angle 10 | 11 | Check: Shape + InRoom + Position + Velocity 12 | 13 | Check: Shape + InRoom + Position + Angle 14 | 15 | ### Room 16 | Check: Position + Room + Size 17 | 18 | ### Components 19 | Room has: 20 | - physics group id 21 | - id to internal structure for position+size 22 | 23 | InRoom has: 24 | - physics group id 25 | - maybe position+angle transforms? 26 | 27 | Rooms 28 | ===== 29 | 30 | We have: Room, Position, Size 31 | 32 | 33 | 34 | 35 | Objects 36 | ======= 37 | 38 | We have: Entity, Shape, InRoom, Position, Velocity, Angle 39 | 40 | From InRoom we get entity, from entity and PhysicsSystem we get collision group 41 | 42 | From PhysicsSystem we get entity's PhysicalObject, or initialize one with Shape + others 43 | 44 | We have: PhysicalObject(Handle), Position, Velocity, Angle, collision group 45 | 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Stacked Worlds 2 | 3 | A game prototype written in Rust. 4 | 5 | This project's focus is both as a playground for me to learn Rust, and to implement a game and level-editor for an idea described to me by [lemon24](https://github.com/lemon24). 6 | 7 | What it looks like: [game gif](https://media.giphy.com/media/1jkUVLAMs9hLudoD8P/giphy.gif). 8 | 9 | ## Running 10 | 11 | Download the sources, get [Rust](https://www.rust-lang.org/en-US/), and run `cargo run --release`. 12 | 13 | ## Controls 14 | 15 | Mouse: 16 | * `LMB` *(hold)* - Drag to create rooms, drag inside rooms to draw rectangles 17 | * `RMB` *(hold)* - Hold to create a chain between you and the target, if in range 18 | * `MMB` *(hold)* - Enable edge-panning (will be changed to better panning later) 19 | 20 | Keyboard: 21 | * `a` and `d` - Move left or right 22 | * `Space` - Jump (must be touching a surface) 23 | * `z` *(hold)* - Press to peek into the next room, release to teleport there 24 | * `c` - Change camera mode (toggles between following the player or static) 25 | * `r` - Reset the world (delete all rooms) 26 | * `Esc` - Quit 27 | -------------------------------------------------------------------------------- /src/animate.rs: -------------------------------------------------------------------------------- 1 | extern crate specs; 2 | 3 | use specs::prelude::{VecStorage, System, WriteStorage, Entities, Join}; 4 | use std::marker::PhantomData; 5 | 6 | #[derive(Component, Debug, Serialize, Deserialize, Clone)] 7 | #[storage(VecStorage)] 8 | pub struct Animation 9 | where T: Sync + Send + 'static { 10 | pub current: u32, 11 | pub limit: u32, 12 | 13 | #[serde(skip)] 14 | phantom: PhantomData, 15 | } 16 | 17 | impl Animation 18 | where T: Sync + Send + 'static { 19 | pub fn new(limit: u32) -> Self { 20 | Animation { 21 | current: 0, 22 | limit, 23 | phantom: PhantomData::default(), 24 | } 25 | } 26 | } 27 | 28 | pub struct UpdateAnimations; 29 | 30 | impl <'a> System<'a> for UpdateAnimations { 31 | type SystemData = ( 32 | Entities<'a>, 33 | WriteStorage<'a, Animation> 34 | ); 35 | 36 | fn run(&mut self, (entities, mut room_animations): Self::SystemData) { 37 | for (_entity, animation) in (&*entities, &mut room_animations).join() { 38 | if animation.current < animation.limit { 39 | animation.current += 1; 40 | } else { 41 | // remove animation from entity 42 | } 43 | } 44 | } 45 | } 46 | 47 | #[derive(Debug, Serialize, Deserialize, Clone, Copy)] 48 | pub struct RoomAnimation {} 49 | -------------------------------------------------------------------------------- /notes/howdoiphase.md: -------------------------------------------------------------------------------- 1 | # How do I phase 2 | 3 | I press Z. I hold it. 4 | 5 | Then a world is being drawn on top of the current one. 6 | 7 | Why? Camera trick? Room being drawn in two places at once? 8 | 9 | I release Z. Then what? 10 | 11 | A timer starts on the controller. While it's non-zero, every frame an attempt is made to phase. 12 | 13 | How? Two ways: 14 | * Last frame, a Target entity was created in the target room, with InRoom and Sensor components 15 | * It is destroyed when Z-mode is somehow cancelled 16 | * Or, it lives just 1 frame, and is recreated every frame; but then how can you see its results? 17 | * Or, it has a cooldown to death; no need to worry about it dangling 18 | * Last frame, a shape query against the target room is made, and its result stored 19 | 20 | The InRoom's room_entity changes. Optionally, the Position changes. 21 | 22 | To what does room_entity change? Just the next one for now? How do we find it? 23 | 24 | Where is this information stored? Possibly on: 25 | * The controller; this means only controllable things can shift 26 | * Not a good idea, objects/bullets should be able to shift too 27 | * A Shifter component, it has: 28 | * target_room: Option\, 29 | * target_sensor: Option\, 30 | * sensing: true, 31 | * shifting: true, 32 | * time_left: f64, 33 | * A ShiftBeacon sensor entity that pulls something into its dimension: 34 | * source: Entity, 35 | * .. Shifter 36 | * but how would you give a command to a bullet to shift? 37 | 38 | So: 39 | 1. Shifter figures out where to shift to 40 | 1. On creation or trigger? 41 | 1. Every turn? (allows drawing the target at all times?) 42 | 1. I press Z, controller says 'shift_sense = true' 43 | 1. Controller updates Shifter component with 'sensing = true' 44 | 1. Shifter creates new entity with Sensor, Position, InRoom, and death timer 45 | 1. Shifter updates its target_room and target_sensor 46 | 1. Or only target_sensor if target_room is pre-filled 47 | 1. Sensor entity creates physical_sensor? 48 | 1. Or physical_body with sensor=true? 49 | 1. The entity needs to die; where to store the death timer? 50 | 1. On a ShiftBeacon perhaps? 51 | 1. On a DeathTimer component? 52 | 1. I release Z, controller says 'shift_sense = false' 53 | 1. Controller updates Shifter component with 'shifting = true' 54 | -------------------------------------------------------------------------------- /src/shift.rs: -------------------------------------------------------------------------------- 1 | /// Phase shift between rooms/dimensions 2 | /// 3 | /// If two rooms are interconnected, objects can phase shift from one room to 4 | /// another at the same (or similar) position. 5 | /// 6 | /// Overview: 7 | /// * All entities capable of phase-shifting have a Shifter component 8 | /// * Every update, the TrackShiftTarget figures out the target room, if there is one 9 | /// * ... 10 | 11 | use specs::prelude::{System, DenseVecStorage, Entities, ReadStorage, WriteStorage, Join}; 12 | use specs::world::{Index, EntitiesRes}; 13 | 14 | use physics::{Room, InRoom}; 15 | use input::PlayerController; 16 | 17 | #[derive(Component, Debug, Default, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)] 18 | #[storage(DenseVecStorage)] 19 | pub struct Shifter { 20 | pub target_room: Option, 21 | pub target_entity: Option, 22 | pub shifting: bool, 23 | pub sensing: bool, 24 | } 25 | 26 | 27 | pub struct TrackShiftTarget; 28 | 29 | fn get_next_room<'a>(current_room: Index, entities: &EntitiesRes, rooms: &ReadStorage<'a, Room>) -> Option { 30 | let iteration1 = (entities, rooms).join(); 31 | let iteration2 = (entities, rooms).join(); 32 | 33 | let next_room = iteration1.chain(iteration2) 34 | .map(|(entity, _room)| entity.id()) 35 | .skip_while(|room_index| current_room != *room_index) 36 | .nth(1); 37 | 38 | next_room 39 | } 40 | 41 | impl <'a> System<'a> for TrackShiftTarget { 42 | type SystemData = ( 43 | Entities<'a>, 44 | WriteStorage<'a, Shifter>, 45 | ReadStorage<'a, InRoom>, 46 | ReadStorage<'a, Room>, 47 | ); 48 | 49 | fn run(&mut self, (entities, mut shifters, in_rooms, rooms): Self::SystemData) { 50 | // Figure out the target room. Currently, it's just the next room in the Room storage. 51 | for (_entity, mut shifter, in_room) in (&*entities, &mut shifters, &in_rooms).join() { 52 | shifter.target_room = get_next_room(in_room.room_entity, &*entities, &rooms); 53 | } 54 | } 55 | } 56 | 57 | pub struct StartPhaseShift; 58 | 59 | impl <'a> System<'a> for StartPhaseShift { 60 | type SystemData = ( 61 | Entities<'a>, 62 | ReadStorage<'a, PlayerController>, 63 | WriteStorage<'a, Shifter>, 64 | ); 65 | 66 | fn run(&mut self, (entities, player_controllers, mut shifters): Self::SystemData) { 67 | for (_entity, player_controller, shifter) in (&*entities, &player_controllers, &mut shifters).join() { 68 | if player_controller.shifting && shifter.target_entity.is_none() { 69 | shifter.sensing = true; 70 | // Create Sensor 71 | } else if !player_controller.shifting && shifter.sensing && !shifter.shifting { 72 | shifter.shifting = true; 73 | println!("Shifting to: {:?}", shifter.target_room); 74 | } 75 | } 76 | } 77 | } 78 | 79 | pub struct PhaseShift; 80 | 81 | impl <'a> System<'a> for PhaseShift { 82 | type SystemData = ( 83 | Entities<'a>, 84 | WriteStorage<'a, Shifter>, 85 | WriteStorage<'a, InRoom>, 86 | ); 87 | 88 | fn run(&mut self, (entities, mut shifters, mut in_rooms): Self::SystemData) { 89 | for (_entity, shifter, in_room) in (&*entities, &mut shifters, &mut in_rooms).join() { 90 | if shifter.shifting { 91 | if let Some(target_room) = shifter.target_room { 92 | shifter.shifting = false; 93 | shifter.sensing = false; 94 | in_room.room_entity = target_room; 95 | } 96 | } 97 | } 98 | } 99 | } -------------------------------------------------------------------------------- /src/edit.rs: -------------------------------------------------------------------------------- 1 | use specs::prelude::{System, Entity, Entities, ReadExpect, WriteExpect, LazyUpdate, Builder}; 2 | use specs::world::EntitiesRes; 3 | use specs::saveload::{U64Marker, MarkedBuilder}; 4 | use std::collections::VecDeque; 5 | 6 | use input; 7 | use draw; 8 | use shift; 9 | use physics; 10 | use animate; 11 | use control; 12 | 13 | 14 | pub struct EditorController { 15 | edit_events: VecDeque, 16 | } 17 | 18 | impl EditorController { 19 | pub fn new() -> Self { 20 | EditorController { 21 | edit_events: VecDeque::with_capacity(16), 22 | } 23 | } 24 | 25 | pub fn push_event(&mut self, edit_event: EditEvent) { 26 | self.edit_events.push_back(edit_event); 27 | } 28 | } 29 | 30 | pub enum EditEvent { 31 | CreateRoom { x: f64, y: f64, width: f64, height: f64 }, 32 | CreateTerrainBox { x: f64, y: f64, width: f64, height: f64, room_entity: Entity }, 33 | } 34 | 35 | 36 | pub struct CreateRoom; 37 | 38 | fn create_room( 39 | entities: &EntitiesRes, lazy_update: &LazyUpdate, 40 | x: f64, y: f64, width: f64, height: f64 41 | ) { 42 | let entity = lazy_update.create_entity(entities) 43 | .with(draw::Position { x, y }) 44 | .with(draw::Size { width, height }) 45 | .with(physics::Room) 46 | .with(animate::Animation::::new(32)) 47 | .marked::() 48 | .build(); 49 | 50 | lazy_update.create_entity(entities) 51 | .with(draw::Position { x: width / 2.0 + 5.0, y: height / 2.0 + 10.0 }) 52 | .with(draw::Shape { size: 10.0, class: draw::ShapeClass::Ball }) 53 | .with(physics::Velocity::default()) 54 | .with(physics::InRoom { room_entity: entity.id() }) 55 | .marked::() 56 | .build(); 57 | 58 | lazy_update.create_entity(entities) 59 | .with(draw::Position { x: width / 2.0 - 5.0, y: height / 2.0 - 10.0 }) 60 | .with(draw::Shape { size: 10.0, class: draw::ShapeClass::Ball }) 61 | .with(physics::Velocity::default()) 62 | .with(physics::InRoom { room_entity: entity.id() }) 63 | .marked::() 64 | .build(); 65 | 66 | if entity.id() == 0 { 67 | lazy_update.create_entity(entities) 68 | .with(draw::Position { x: width / 2.0, y: 20.0 }) 69 | .with(draw::Shape { size: 10.0, class: draw::ShapeClass::Ball }) 70 | .with(shift::Shifter::default()) 71 | .with(physics::Velocity::default()) 72 | .with(physics::InRoom { room_entity: entity.id() }) 73 | .with(input::PlayerController::default()) 74 | .with(control::Jump::default()) 75 | .with(physics::Force::default()) 76 | .with(physics::Aim::default()) 77 | .with(physics::CollisionSet::default()) 78 | .marked::() 79 | .build(); 80 | } 81 | } 82 | 83 | impl <'a> System<'a> for CreateRoom { 84 | type SystemData = ( 85 | Entities<'a>, 86 | WriteExpect<'a, EditorController>, 87 | ReadExpect<'a, LazyUpdate>, 88 | ); 89 | 90 | fn run(&mut self, (entities, mut editor_controller, lazy_update): Self::SystemData) { 91 | while let Some(edit_event) = editor_controller.edit_events.pop_front() { 92 | match edit_event { 93 | EditEvent::CreateRoom { x, y, width, height } => { 94 | create_room(&entities, &lazy_update, x, y, width, height); 95 | }, 96 | 97 | EditEvent::CreateTerrainBox { x, y, width, height, room_entity } => { 98 | lazy_update.create_entity(&entities) 99 | .with(draw::Position { x, y }) 100 | .with(draw::Size { width, height }) 101 | .with(physics::InRoom { room_entity: room_entity.id() }) 102 | .with(animate::Animation::::new(32)) 103 | .marked::() 104 | .build(); 105 | }, 106 | }; 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/saveload.rs: -------------------------------------------------------------------------------- 1 | extern crate specs; 2 | extern crate ron; 3 | 4 | use specs::saveload::{DeserializeComponents, SerializeComponents, U64Marker, U64MarkerAllocator}; 5 | use specs::prelude::{System, Entities, ReadStorage, Join, Write, WriteStorage}; 6 | use specs::storage::NullStorage; 7 | 8 | use error::Error; 9 | use draw::{Position, Size, Shape}; 10 | use shift::Shifter; 11 | use animate::{Animation, RoomAnimation}; 12 | use physics::{Room, InRoom, Force, Velocity, CollisionSet, RevoluteJoint, Aim}; 13 | use input::PlayerController; 14 | use control::{Jump, ChainLink}; 15 | 16 | pub struct SaveWorld { 17 | pub file_name: String, 18 | } 19 | 20 | impl <'a> System<'a> for SaveWorld { 21 | type SystemData = ( 22 | Entities<'a>, 23 | ReadStorage<'a, Position>, 24 | ReadStorage<'a, Size>, 25 | ReadStorage<'a, Shape>, 26 | ReadStorage<'a, Room>, 27 | ReadStorage<'a, InRoom>, 28 | ReadStorage<'a, PlayerController>, 29 | ReadStorage<'a, Velocity>, 30 | ReadStorage<'a, Force>, 31 | ReadStorage<'a, Aim>, 32 | ReadStorage<'a, CollisionSet>, 33 | ReadStorage<'a, RevoluteJoint>, 34 | ReadStorage<'a, ChainLink>, 35 | ReadStorage<'a, Shifter>, 36 | ReadStorage<'a, Jump>, 37 | ReadStorage<'a, Animation>, 38 | ReadStorage<'a, U64Marker>, 39 | ); 40 | 41 | fn run(&mut self, (entities, positions, sizes, shapes, rooms, in_rooms, 42 | player_controllers, velocities, forces, aims, collision_sets, 43 | revolute_joints, chain_links, shifters, jumps, animations, markers): Self::SystemData) 44 | { 45 | let mut serializer = ron::ser::Serializer::new(Some(Default::default()), true); 46 | SerializeComponents::::serialize( 47 | &(positions, sizes, shapes, rooms, in_rooms, player_controllers, velocities, 48 | forces, aims, collision_sets, revolute_joints, chain_links, shifters, jumps, animations), 49 | &entities, 50 | &markers, 51 | &mut serializer 52 | ).unwrap_or_else(|e| { 53 | // FIXME: handle this 54 | eprintln!("Error: {}", e); 55 | }); 56 | 57 | let file_contents = serializer.into_output_string(); 58 | 59 | use ::std::fs::File; 60 | use ::std::io::Write; 61 | 62 | let mut file = File::create(&self.file_name) 63 | .expect("Could not create save file."); 64 | file.write_all(file_contents.as_bytes()) 65 | .expect("Could not write save file."); 66 | } 67 | } 68 | 69 | pub struct LoadWorld { 70 | pub file_name: String, 71 | pub default_storage: String, 72 | } 73 | 74 | impl <'a> System<'a> for LoadWorld { 75 | type SystemData = ( 76 | Entities<'a>, 77 | Write<'a, U64MarkerAllocator>, 78 | WriteStorage<'a, Position>, 79 | WriteStorage<'a, Size>, 80 | WriteStorage<'a, Shape>, 81 | WriteStorage<'a, Room>, 82 | WriteStorage<'a, InRoom>, 83 | WriteStorage<'a, PlayerController>, 84 | WriteStorage<'a, Velocity>, 85 | WriteStorage<'a, Force>, 86 | WriteStorage<'a, Aim>, 87 | WriteStorage<'a, CollisionSet>, 88 | WriteStorage<'a, RevoluteJoint>, 89 | WriteStorage<'a, ChainLink>, 90 | WriteStorage<'a, Shifter>, 91 | WriteStorage<'a, Jump>, 92 | WriteStorage<'a, Animation>, 93 | WriteStorage<'a, U64Marker>, 94 | ); 95 | 96 | fn run(&mut self, (entities, mut allocator, positions, sizes, shapes, rooms, in_rooms, player_controllers, 97 | velocities, forces, aims, collision_sets, revolute_joints, chain_links, shifters, jumps, animations, mut markers) 98 | : Self::SystemData) { 99 | use ::std::fs::File; 100 | use ::std::io::Read; 101 | 102 | let file_contents = { 103 | // FIXME: Replace panic! and expect! with actual error handling/recovery 104 | let mut file = match File::open(&self.file_name) { 105 | Ok(file) => file, 106 | Err(error) => { 107 | if error.kind() == ::std::io::ErrorKind::NotFound { 108 | eprintln!("Save file '{}' not found, loading from '{}' instead.", 109 | self.file_name, self.default_storage); 110 | File::open(&self.default_storage) 111 | .expect("Could not open file.") 112 | } else { 113 | panic!("Could not open save file: {} ({})", self.file_name, error); 114 | } 115 | }, 116 | }; 117 | let mut file_contents = Vec::new(); 118 | file.read_to_end(&mut file_contents) 119 | .expect("Could not read file."); 120 | file_contents 121 | }; 122 | 123 | let mut deserializer = ron::de::Deserializer::from_bytes(&file_contents) 124 | .expect("Could not load"); // FIXME: handle error 125 | 126 | DeserializeComponents::::deserialize( 127 | &mut (positions, sizes, shapes, rooms, in_rooms, player_controllers, velocities, 128 | forces, aims, collision_sets, revolute_joints, chain_links, shifters, jumps, animations), 129 | &entities, 130 | &mut markers, 131 | &mut allocator, 132 | &mut deserializer, 133 | ).unwrap_or_else(|e| { 134 | eprintln!("Error: {}", e); // FIXME: handle error 135 | }) 136 | } 137 | } 138 | 139 | #[derive(Component, Debug, Default, Clone, Copy)] 140 | #[storage(NullStorage)] 141 | pub struct DestroyEntity; 142 | 143 | pub struct ResetWorld; 144 | 145 | impl <'a> System<'a> for ResetWorld { 146 | type SystemData = ( 147 | Entities<'a>, 148 | WriteStorage<'a, DestroyEntity>, 149 | ); 150 | 151 | fn run(&mut self, (entities, mut destroy_entities): Self::SystemData) { 152 | for entity in entities.join() { 153 | destroy_entities.insert(entity, DestroyEntity) 154 | .expect("Could not insert DestroyEntity component"); 155 | } 156 | } 157 | } 158 | 159 | pub struct DestroyEntities; 160 | 161 | impl <'a> System<'a> for DestroyEntities { 162 | type SystemData = ( 163 | Entities<'a>, 164 | ReadStorage<'a, DestroyEntity>, 165 | ); 166 | 167 | fn run(&mut self, (entities, destroy_entities): Self::SystemData) { 168 | for (entity, _destroy_entity) in (&*entities, &destroy_entities).join() { 169 | entities.delete(entity) 170 | .expect("Error deleting entity during world reset"); 171 | } 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /default-storage.ron: -------------------------------------------------------------------------------- 1 | [ 2 | EntityData( 3 | marker: U64Marker(11262), 4 | components: (Some(Position( 5 | x: 16, 6 | y: 16, 7 | )), Some(Size( 8 | width: 644, 9 | height: 500, 10 | )), None, Some(Room), None, None, None, None, None, None, None, None, None, None, Some(Animation( 11 | current: 32, 12 | limit: 32, 13 | ))), 14 | ), 15 | EntityData( 16 | marker: U64Marker(11263), 17 | components: (Some(Position( 18 | x: 633.8010000000003, 19 | y: 489.8009311290943, 20 | )), None, Some(Shape( 21 | size: 10, 22 | class: Ball, 23 | )), None, Some(InRoom( 24 | room_entity: 0, 25 | )), None, Some(Velocity( 26 | x: 0, 27 | y: 0, 28 | )), None, None, None, None, None, None, None, None), 29 | ), 30 | EntityData( 31 | marker: U64Marker(11264), 32 | components: (Some(Position( 33 | x: 613.6019947394686, 34 | y: 489.8010000000001, 35 | )), None, Some(Shape( 36 | size: 10, 37 | class: Ball, 38 | )), None, Some(InRoom( 39 | room_entity: 0, 40 | )), None, Some(Velocity( 41 | x: 0, 42 | y: 0, 43 | )), None, None, None, None, None, None, None, None), 44 | ), 45 | EntityData( 46 | marker: U64Marker(11265), 47 | components: (Some(Position( 48 | x: 332.3795648026265, 49 | y: 489.8010000000001, 50 | )), None, Some(Shape( 51 | size: 10, 52 | class: Ball, 53 | )), None, Some(InRoom( 54 | room_entity: 4, 55 | )), Some(PlayerController( 56 | moving: None, 57 | jumping: false, 58 | hooking: false, 59 | hook_established: false, 60 | shifting: false, 61 | )), Some(Velocity( 62 | x: 75.45141474647713, 63 | y: 0, 64 | )), Some(Force( 65 | continuous: (0, 0), 66 | impulse: (0, 0), 67 | )), Some(Aim( 68 | aiming: false, 69 | aiming_toward: (-15.822521877160398, -110.66688375907859), 70 | )), Some(CollisionSet( 71 | colliding: true, 72 | collision_normal: (0, 1), 73 | last_collision_normal: (0, 1), 74 | time_since_collision: 0, 75 | )), None, None, Some(Shifter( 76 | target_room: Some(0), 77 | target_entity: None, 78 | shifting: false, 79 | sensing: false, 80 | )), Some(Jump( 81 | cooldown: 0, 82 | )), None), 83 | ), 84 | EntityData( 85 | marker: U64Marker(11266), 86 | components: (Some(Position( 87 | x: 876, 88 | y: 16, 89 | )), Some(Size( 90 | width: 644, 91 | height: 500, 92 | )), None, Some(Room), None, None, None, None, None, None, None, None, None, None, Some(Animation( 93 | current: 32, 94 | limit: 32, 95 | ))), 96 | ), 97 | EntityData( 98 | marker: U64Marker(11267), 99 | components: (Some(Position( 100 | x: 631.4220877790199, 101 | y: 489.8010000000001, 102 | )), None, Some(Shape( 103 | size: 10, 104 | class: Ball, 105 | )), None, Some(InRoom( 106 | room_entity: 4, 107 | )), None, Some(Velocity( 108 | x: -0.14333043756394853, 109 | y: 0, 110 | )), None, None, None, None, None, None, None, None), 111 | ), 112 | EntityData( 113 | marker: U64Marker(11268), 114 | components: (Some(Position( 115 | x: 10.199000018449578, 116 | y: 489.8010000000001, 117 | )), None, Some(Shape( 118 | size: 10, 119 | class: Ball, 120 | )), None, Some(InRoom( 121 | room_entity: 4, 122 | )), None, Some(Velocity( 123 | x: 0, 124 | y: 0, 125 | )), None, None, None, None, None, None, None, None), 126 | ), 127 | EntityData( 128 | marker: U64Marker(11301), 129 | components: (Some(Position( 130 | x: 64, 131 | y: 176, 132 | )), Some(Size( 133 | width: 96, 134 | height: 64, 135 | )), None, None, Some(InRoom( 136 | room_entity: 0, 137 | )), None, None, None, None, None, None, None, None, None, Some(Animation( 138 | current: 32, 139 | limit: 32, 140 | ))), 141 | ), 142 | EntityData( 143 | marker: U64Marker(11302), 144 | components: (Some(Position( 145 | x: 320, 146 | y: 288, 147 | )), Some(Size( 148 | width: 176, 149 | height: 32, 150 | )), None, None, Some(InRoom( 151 | room_entity: 0, 152 | )), None, None, None, None, None, None, None, None, None, Some(Animation( 153 | current: 32, 154 | limit: 32, 155 | ))), 156 | ), 157 | EntityData( 158 | marker: U64Marker(11303), 159 | components: (Some(Position( 160 | x: 112, 161 | y: 256, 162 | )), Some(Size( 163 | width: 64, 164 | height: 48, 165 | )), None, None, Some(InRoom( 166 | room_entity: 4, 167 | )), None, None, None, None, None, None, None, None, None, Some(Animation( 168 | current: 32, 169 | limit: 32, 170 | ))), 171 | ), 172 | EntityData( 173 | marker: U64Marker(11304), 174 | components: (Some(Position( 175 | x: 208, 176 | y: 224, 177 | )), Some(Size( 178 | width: 64, 179 | height: 48, 180 | )), None, None, Some(InRoom( 181 | room_entity: 4, 182 | )), None, None, None, None, None, None, None, None, None, Some(Animation( 183 | current: 32, 184 | limit: 32, 185 | ))), 186 | ), 187 | EntityData( 188 | marker: U64Marker(11305), 189 | components: (Some(Position( 190 | x: 320, 191 | y: 144, 192 | )), Some(Size( 193 | width: 64, 194 | height: 48, 195 | )), None, None, Some(InRoom( 196 | room_entity: 4, 197 | )), None, None, None, None, None, None, None, None, None, Some(Animation( 198 | current: 32, 199 | limit: 32, 200 | ))), 201 | ), 202 | EntityData( 203 | marker: U64Marker(11306), 204 | components: (Some(Position( 205 | x: 432, 206 | y: 64, 207 | )), Some(Size( 208 | width: 64, 209 | height: 48, 210 | )), None, None, Some(InRoom( 211 | room_entity: 4, 212 | )), None, None, None, None, None, None, None, None, None, Some(Animation( 213 | current: 32, 214 | limit: 32, 215 | ))), 216 | ), 217 | EntityData( 218 | marker: U64Marker(11307), 219 | components: (Some(Position( 220 | x: 500, 221 | y: 272, 222 | )), Some(Size( 223 | width: 32, 224 | height: 144, 225 | )), None, None, Some(InRoom( 226 | room_entity: 4, 227 | )), None, None, None, None, None, None, None, None, None, Some(Animation( 228 | current: 32, 229 | limit: 32, 230 | ))), 231 | ), 232 | EntityData( 233 | marker: U64Marker(11308), 234 | components: (Some(Position( 235 | x: 384, 236 | y: 64, 237 | )), Some(Size( 238 | width: 64, 239 | height: 48, 240 | )), None, None, Some(InRoom( 241 | room_entity: 0, 242 | )), None, None, None, None, None, None, None, None, None, Some(Animation( 243 | current: 32, 244 | limit: 32, 245 | ))), 246 | ), 247 | EntityData( 248 | marker: U64Marker(11309), 249 | components: (Some(Position( 250 | x: 100, 251 | y: 368, 252 | )), Some(Size( 253 | width: 192, 254 | height: 48, 255 | )), None, None, Some(InRoom( 256 | room_entity: 4, 257 | )), None, None, None, None, None, None, None, None, None, Some(Animation( 258 | current: 32, 259 | limit: 32, 260 | ))), 261 | ), 262 | ] -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | /// Systems: 2 | /// saveload: 3 | /// - `LoadWorld` 4 | /// - `SaveWorld` 5 | /// - `ResetWorld` 6 | /// draw.rs: 7 | /// - `ClearScreen` 8 | /// - `DrawRooms` 9 | /// - `DrawSelectionBox` 10 | /// animate.rs: 11 | /// - `UpdateAnimations` 12 | /// physics: 13 | /// - `PhysicsSystem` 14 | /// 15 | /// Components: 16 | /// lib.rs: 17 | /// - `Position` 18 | /// - `Size` 19 | /// - `Room` 20 | /// animate.rs: 21 | /// - `Animation` 22 | /// physics.rs: 23 | /// - `PhysicalObject` 24 | /// - `PhysicalRoom`? 25 | /// saveload.rs: 26 | /// - `DestroyEntity` 27 | 28 | 29 | #[cfg(test)] 30 | mod tests { 31 | #[test] 32 | fn it_works() { 33 | assert_eq!(2 + 2, 4); 34 | } 35 | } 36 | 37 | #[macro_use] 38 | extern crate failure; 39 | extern crate opengl_graphics; 40 | extern crate piston; 41 | extern crate graphics; 42 | extern crate glutin_window; 43 | #[macro_use] 44 | extern crate serde_derive; 45 | extern crate serde_json; 46 | extern crate specs; 47 | #[macro_use] 48 | extern crate specs_derive; 49 | extern crate ron; 50 | extern crate nalgebra; 51 | extern crate nphysics2d; 52 | extern crate ncollide2d; 53 | extern crate core; 54 | 55 | 56 | use opengl_graphics::{GlGraphics, OpenGL}; 57 | use glutin_window::GlutinWindow; 58 | use piston::input::{UpdateEvent, UpdateArgs}; 59 | use piston::input::{RenderEvent, RenderArgs}; 60 | use piston::input::{PressEvent, ReleaseEvent, Key, Button, MouseButton}; 61 | use piston::input::{MouseCursorEvent}; 62 | use piston::window::{WindowSettings, Window}; 63 | use piston::event_loop::{Events, EventSettings}; 64 | use specs::prelude::{World, RunNow}; 65 | use specs::saveload::U64Marker; 66 | use specs::saveload::U64MarkerAllocator; 67 | 68 | mod draw; 69 | mod input; 70 | mod control; 71 | mod shift; 72 | mod edit; 73 | mod animate; 74 | mod physics; 75 | mod saveload; 76 | mod error; 77 | 78 | use error::{GameError, Error}; 79 | use draw::run_draw_systems; 80 | use physics::PhysicsSystem; 81 | use input::{InputEvents, InputEvent}; 82 | 83 | 84 | struct Game { 85 | gl: GlGraphics, 86 | specs_world: World, 87 | physics_system: PhysicsSystem, 88 | } 89 | 90 | pub struct UpdateDeltaTime { 91 | pub dt: f64, 92 | } 93 | 94 | impl Game { 95 | fn render(&mut self, args: &RenderArgs) { 96 | run_draw_systems(&mut self.specs_world, &mut self.gl, *args); 97 | } 98 | 99 | fn update(&mut self, args: &UpdateArgs) { 100 | let () = { 101 | let mut update_delta_time = self.specs_world.write_resource::(); 102 | update_delta_time.dt = args.dt; 103 | }; 104 | 105 | input::InputEventsToState.run_now(&mut self.specs_world.res); 106 | input::MouseInsideRoom.run_now(&mut self.specs_world.res); 107 | input::PlayerControllerInput.run_now(&mut self.specs_world.res); 108 | input::EditorControllerInput.run_now(&mut self.specs_world.res); 109 | input::AimObjects.run_now(&mut self.specs_world.res); 110 | input::GlobalInput.run_now(&mut self.specs_world.res); 111 | input::CameraEdgePan.run_now(&mut self.specs_world.res); 112 | 113 | shift::TrackShiftTarget.run_now(&mut self.specs_world.res); 114 | control::ControlObjects.run_now(&mut self.specs_world.res); 115 | edit::CreateRoom.run_now(&mut self.specs_world.res); 116 | shift::PhaseShift.run_now(&mut self.specs_world.res); 117 | 118 | self.specs_world.maintain(); 119 | self.physics_system.run_now(&mut self.specs_world.res); 120 | 121 | animate::UpdateAnimations.run_now(&mut self.specs_world.res); 122 | control::UpdateCooldowns.run_now(&mut self.specs_world.res); 123 | control::FireHook.run_now(&mut self.specs_world.res); 124 | shift::StartPhaseShift.run_now(&mut self.specs_world.res); 125 | 126 | // Must be left at the end in order to allow every other system to react on destroyed 127 | // entities. 128 | // FIXME: Obsolete, remove the component and system 129 | saveload::DestroyEntities.run_now(&mut self.specs_world.res); 130 | self.specs_world.maintain(); 131 | } 132 | 133 | fn press(&mut self, args: &Button) { 134 | self.specs_world.write_resource::().events 135 | .push_back(InputEvent::PressEvent(*args)); 136 | 137 | // FIXME: Move to edit.rs 138 | if let &Button::Keyboard(Key::R) = args { 139 | saveload::ResetWorld.run_now(&mut self.specs_world.res); 140 | self.specs_world.maintain(); 141 | } 142 | } 143 | 144 | fn release(&mut self, args: &Button) { 145 | self.specs_world.write_resource::().events 146 | .push_back(InputEvent::ReleaseEvent(*args)); 147 | } 148 | 149 | fn mouse_cursor(&mut self, x: f64, y: f64) { 150 | self.specs_world.write_resource::().events 151 | .push_back(InputEvent::MotionEvent(x, y)); 152 | } 153 | } 154 | 155 | pub fn run() -> Result<(), Error> { 156 | let opengl_version = OpenGL::V3_2; 157 | 158 | let mut window: GlutinWindow = WindowSettings::new("stacked-worlds", [640, 480]) 159 | .opengl(opengl_version) 160 | .exit_on_esc(true) 161 | .build() 162 | .map_err(|err| GameError::WindowError { reason: err })?; 163 | 164 | // let game_state = { 165 | // let state_file = std::fs::File::open("state.json"); 166 | // 167 | // match state_file { 168 | // Ok(state_file) => 169 | // serde_json::from_reader::<_, GameState>(state_file) 170 | // .context("Cannot deserialize game state file")?, 171 | // Err(err) => 172 | // if err.kind() == std::io::ErrorKind::NotFound { 173 | // GameState::default() 174 | // } else { 175 | // return Err(Error::from(err).context("Cannot open game state file").into()) 176 | // }, 177 | // } 178 | // }; 179 | 180 | let mut world = World::new(); 181 | 182 | world.register::(); 183 | world.register::(); 184 | world.register::(); 185 | world.register::(); 186 | world.register::(); 187 | world.register::(); 188 | world.register::(); 189 | world.register::(); 190 | world.register::>(); 191 | world.register::(); 192 | world.register::(); 193 | world.register::(); 194 | world.register::(); 195 | world.register::(); 196 | world.register::(); 197 | world.register::(); 198 | world.register::(); 199 | 200 | world.add_resource(U64MarkerAllocator::new()); 201 | world.add_resource(UpdateDeltaTime { dt: 0.0 }); 202 | world.add_resource(input::InputEvents::new()); 203 | world.add_resource(input::InputState::new()); 204 | world.add_resource(edit::EditorController::new()); 205 | world.add_resource(draw::Camera::new()); 206 | world.add_resource(draw::Screen { width: window.draw_size().width as f64, height: window.draw_size().height as f64 }); 207 | 208 | let mut game = Game { 209 | gl: GlGraphics::new(opengl_version), 210 | physics_system: PhysicsSystem::new(), 211 | specs_world: world, 212 | }; 213 | 214 | saveload::LoadWorld { 215 | file_name: "storage.ron".into(), 216 | default_storage: "default-storage.ron".into(), 217 | }.run_now(&mut game.specs_world.res); 218 | 219 | let mut events = Events::new(EventSettings::new()); 220 | 221 | while let Some(event) = events.next(&mut window) { 222 | if let Some(render_args) = event.render_args() { 223 | game.render(&render_args); 224 | } 225 | 226 | if let Some(update_args) = event.update_args() { 227 | game.update(&update_args); 228 | } 229 | 230 | if let Some(press_args) = event.press_args() { 231 | game.press(&press_args); 232 | } 233 | 234 | if let Some(release_args) = event.release_args() { 235 | game.release(&release_args); 236 | } 237 | 238 | if let Some(mouse_cursor_args) = event.mouse_cursor_args() { 239 | game.mouse_cursor(mouse_cursor_args[0], mouse_cursor_args[1]); 240 | } 241 | } 242 | 243 | game.specs_world.maintain(); 244 | saveload::SaveWorld { file_name: "storage.ron".into() }.run_now(&game.specs_world.res); 245 | 246 | // let state_file = std::fs::File::create("state.json") 247 | // .context("Cannot create file to save game state")?; 248 | // serde_json::to_writer(state_file, &game.state) 249 | // .context("Cannot write game state to file")?; 250 | 251 | Ok(()) 252 | } 253 | -------------------------------------------------------------------------------- /src/control.rs: -------------------------------------------------------------------------------- 1 | use specs::prelude::{System, VecStorage, DenseVecStorage, Entities, ReadExpect, ReadStorage, WriteStorage, Join, Builder}; 2 | use specs::saveload::MarkedBuilder; 3 | use nalgebra::Vector2; 4 | 5 | use UpdateDeltaTime; 6 | use input::{PlayerController, Movement}; 7 | use physics::{Velocity, Force, Aim, CollisionSet, InRoom, RevoluteJoint}; 8 | use draw::{Position, Shape, ShapeClass}; 9 | use specs::LazyUpdate; 10 | use specs::world::Index; 11 | use saveload::DestroyEntity; 12 | use specs::saveload::U64Marker; 13 | 14 | #[derive(Component, Debug, Default, Serialize, Deserialize, Copy, Clone, PartialEq)] 15 | #[storage(VecStorage)] 16 | pub struct Jump { 17 | pub cooldown: f64, 18 | } 19 | 20 | pub struct ControlObjects; 21 | 22 | impl <'a> System<'a> for ControlObjects { 23 | type SystemData = ( 24 | Entities<'a>, 25 | ReadStorage<'a, PlayerController>, 26 | ReadStorage<'a, CollisionSet>, 27 | ReadStorage<'a, Velocity>, 28 | WriteStorage<'a, Force>, 29 | WriteStorage<'a, Jump>, 30 | ); 31 | 32 | fn run(&mut self, (entities, player_controller, collision_sets, velocities, mut forces, mut jumps): Self::SystemData) { 33 | let speed = 500000.0; 34 | let jump_speed = 300.0; 35 | 36 | for (_entity, mut force) in (&*entities, &mut forces).join() { 37 | force.continuous = (0.0, 0.0); 38 | force.impulse = (0.0, 0.0); 39 | } 40 | 41 | for (_entity, player_controller, collision_set, velocity, mut force) in (&*entities, &player_controller, &collision_sets, &velocities, &mut forces).join() { 42 | // TODO: Change it to only downward collisions 43 | let touching_ground = collision_set.time_since_collision < 0.1; 44 | 45 | let speed = if touching_ground { speed } else { speed / 5.0 }; 46 | 47 | let (x, y) = match player_controller.moving { 48 | // Move, but only if not exceeding max velocity 49 | Movement::Left if velocity.x > -300.0 => (-1.0 * speed, 0.0), 50 | Movement::Right if velocity.x < 300.0 => (1.0 * speed, 0.0), 51 | 52 | // Break if not moving 53 | Movement::None if touching_ground && velocity.x > 0.0 => (-1.0 * speed, 0.0), 54 | Movement::None if touching_ground && velocity.x < 0.0 => (1.0 * speed, 0.0), 55 | 56 | _ => (0.0, 0.0), 57 | }; 58 | 59 | force.continuous = (force.continuous.0 + x, force.continuous.1 + y); 60 | } 61 | 62 | for (_entity, player_controller, mut jump, collision_set, mut force) in (&*entities, &player_controller, &mut jumps, &collision_sets, &mut forces).join() { 63 | if player_controller.jumping && collision_set.time_since_collision < 0.2 && jump.cooldown <= 0.0 { 64 | let jump_direction = -Vector2::new(collision_set.last_collision_normal.0, 65 | collision_set.last_collision_normal.1).normalize(); 66 | let jump_impulse = jump_direction * jump_speed; 67 | 68 | force.impulse = ( 69 | force.impulse.0 + jump_impulse.x, 70 | force.impulse.1 + jump_impulse.y 71 | ); 72 | 73 | jump.cooldown += 0.25; 74 | } 75 | } 76 | } 77 | } 78 | 79 | #[derive(Component, Debug, Default, Serialize, Deserialize, Clone, Copy, PartialEq)] 80 | #[storage(DenseVecStorage)] 81 | pub struct ChainLink { 82 | // TODO: Maybe figure out how to move these to an Animation component 83 | pub creation_animation: f64, 84 | pub destruction_animation: f64, 85 | pub expire: bool, 86 | pub next_link: Option, 87 | } 88 | 89 | pub struct FireHook; 90 | 91 | impl <'a> System<'a> for FireHook { 92 | type SystemData = ( 93 | Entities<'a>, 94 | WriteStorage<'a, PlayerController>, 95 | WriteStorage<'a, ChainLink>, 96 | ReadStorage<'a, Position>, 97 | ReadStorage<'a, Velocity>, 98 | ReadStorage<'a, InRoom>, 99 | ReadStorage<'a, Aim>, 100 | ReadExpect<'a, LazyUpdate>, 101 | ); 102 | 103 | fn run(&mut self, (entities, mut player_controllers, mut chain_links, 104 | positions, velocities, in_rooms, aims, lazy_update): Self::SystemData) 105 | { 106 | for (entity, mut player_controller, position, velocity, in_room, aim) in (&*entities, &mut player_controllers, &positions, &velocities, &in_rooms, &aims).join() { 107 | if player_controller.hooking && !player_controller.hook_established { 108 | // Create grappling hook chain if possible 109 | let source = Vector2::new(position.x, position.y); 110 | let (target, target_entity) = if let (Some(point), Some(entity)) = (aim.aiming_at_point, aim.aiming_at_entity) { 111 | (Vector2::new(point.0, point.1), entity) 112 | } else { 113 | continue 114 | }; 115 | 116 | let chain_vector = target - source; 117 | let direction = chain_vector.normalize(); 118 | let link_count = (chain_vector / 10.0).norm().floor(); 119 | 120 | if link_count > 15.0 { 121 | continue; 122 | } 123 | 124 | let mut linked_to_entity = target_entity.id(); 125 | let mut next_link = None; 126 | let mut creation_animation = 0.1; 127 | 128 | for i in (2..=link_count as i32).rev() { 129 | let chain_link_position = source + direction * 10.0 * (i as f64); 130 | 131 | let new_entity = lazy_update.create_entity(&entities) 132 | .with(Position { x: chain_link_position.x, y: chain_link_position.y }) 133 | .with(Shape { size: 3.0, class: ShapeClass::ChainLink }) 134 | .with(Velocity { .. *velocity }) 135 | .with(InRoom { .. *in_room }) 136 | .with(ChainLink { next_link, creation_animation, .. ChainLink::default() }) 137 | .with(RevoluteJoint { linked_to_entity, multibody_link: false }) 138 | .marked::() 139 | .build(); 140 | 141 | linked_to_entity = new_entity.id(); 142 | next_link = Some(linked_to_entity); 143 | creation_animation += 0.02; 144 | } 145 | 146 | lazy_update.insert(entity, RevoluteJoint { linked_to_entity, multibody_link: false }); 147 | lazy_update.insert(entity, ChainLink { next_link: Some(linked_to_entity), .. ChainLink::default() }); 148 | 149 | player_controller.hook_established = true; 150 | } else if !player_controller.hooking && player_controller.hook_established { 151 | // Destroy grappling hook chain 152 | 153 | let mut some_next_entity = if let Some(chain_link) = chain_links.get(entity) { 154 | chain_link.next_link 155 | } else { 156 | continue 157 | }; 158 | 159 | let mut destruction_animation = 0.5; 160 | 161 | while let Some(next_entity) = some_next_entity { 162 | let mut chain_link = chain_links.get_mut(entities.entity(next_entity)); 163 | 164 | some_next_entity = chain_link.and_then(|chain_link| { 165 | chain_link.expire = true; 166 | chain_link.destruction_animation = destruction_animation; 167 | destruction_animation += 0.04; 168 | chain_link.next_link 169 | }); 170 | } 171 | 172 | lazy_update.remove::(entity); 173 | lazy_update.remove::(entity); 174 | 175 | player_controller.hook_established = false; 176 | } 177 | } 178 | } 179 | } 180 | 181 | pub struct UpdateCooldowns; 182 | 183 | impl <'a> System<'a> for UpdateCooldowns { 184 | type SystemData = ( 185 | Entities<'a>, 186 | ReadExpect<'a, UpdateDeltaTime>, 187 | WriteStorage<'a, Jump>, 188 | WriteStorage<'a, ChainLink>, 189 | ReadExpect<'a, LazyUpdate>, 190 | ); 191 | 192 | fn run(&mut self, (entities, delta_time, mut jumps, mut chain_links, lazy_update): Self::SystemData) { 193 | for (_entity, mut jump) in (&*entities, &mut jumps).join() { 194 | if jump.cooldown > 0.0 { 195 | jump.cooldown = (jump.cooldown - delta_time.dt).max(0.0); 196 | } 197 | } 198 | 199 | for (entity, mut chain_link) in (&*entities, &mut chain_links).join() { 200 | if chain_link.creation_animation > 0.0 { 201 | chain_link.creation_animation = (chain_link.creation_animation - delta_time.dt).max(0.0); 202 | } 203 | if chain_link.destruction_animation > 0.0 { 204 | chain_link.destruction_animation = (chain_link.destruction_animation - delta_time.dt).max(0.0); 205 | 206 | if chain_link.expire && chain_link.destruction_animation == 0.0 { 207 | lazy_update.insert(entity, DestroyEntity); 208 | } 209 | } 210 | } 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /src/input.rs: -------------------------------------------------------------------------------- 1 | use specs::prelude::{System, Entity, DenseVecStorage, WriteStorage, ReadStorage, ReadExpect, WriteExpect, Entities, Join}; 2 | use std::collections::VecDeque; 3 | use super::{Button, Key, MouseButton}; 4 | use std::collections::HashSet; 5 | use std::collections::HashMap; 6 | use physics::Aim; 7 | use draw::{Position, Size, Camera, Screen}; 8 | use physics::{InRoom, Room}; 9 | use edit::{EditorController, EditEvent}; 10 | 11 | pub enum InputEvent { 12 | PressEvent(Button), 13 | ReleaseEvent(Button), 14 | MotionEvent(f64, f64), 15 | } 16 | 17 | #[derive(Default, Copy, Clone)] // FIXME: derive more 18 | pub struct MouseState { 19 | pub position: (f64, f64), 20 | pub dragging_from: Option<(f64, f64)>, 21 | } 22 | 23 | impl MouseState { 24 | pub fn selection_box(&self) -> Option { 25 | self.dragging_from.map(|dragging_from| { 26 | SelectionBox { 27 | x1: dragging_from.0, 28 | y1: dragging_from.1, 29 | x2: self.position.0, 30 | y2: self.position.1, 31 | } 32 | }) 33 | } 34 | } 35 | 36 | /// The coordinates of the mouse selection box. The first set of coordinates (`x1`, `x2`) are for 37 | /// the point the selection box is being dragged from. 38 | #[derive(Debug)] // FIXME: derive more 39 | pub struct SelectionBox { 40 | pub x1: f64, 41 | pub y1: f64, 42 | pub x2: f64, 43 | pub y2: f64, 44 | } 45 | 46 | impl SelectionBox { 47 | pub fn to_rectangle(&self) -> SelectionRectangle { 48 | let x1 = self.x1.min(self.x2); 49 | let y1 = self.y1.min(self.y2); 50 | let x2 = self.x1.max(self.x2); 51 | let y2 = self.y1.max(self.y2); 52 | 53 | SelectionRectangle { 54 | x: x1, 55 | y: y1, 56 | width: x2 - x1, 57 | height: y2 - y1, 58 | } 59 | } 60 | } 61 | 62 | /// Similar to `SelectionBox`, but with coordinates flipped so that width and height are always 63 | /// positive. Information about where the box is being dragged from is lost. 64 | #[derive(Debug)] // FIXME: derive more 65 | pub struct SelectionRectangle { 66 | pub x: f64, 67 | pub y: f64, 68 | pub width: f64, 69 | pub height: f64, 70 | } 71 | 72 | impl SelectionRectangle { 73 | /// Enlarge the rectangle so that all corners snap to a grid 74 | pub fn snap_to_grid(&self, cell_size: i32) -> Self { 75 | assert_ne!(cell_size, 0); 76 | 77 | // Snap the top-left corner to the grid 78 | let x = (self.x as i32 / cell_size * cell_size) as f64; 79 | let y = (self.y as i32 / cell_size * cell_size) as f64; 80 | 81 | // Bring back the bottom-right corner where it originally was by compensating 82 | let width = self.width + (self.x - x as f64); 83 | let height = self.height + (self.y - y as f64); 84 | 85 | // Snap the bottom-right corner to the grid 86 | let width = ((width as i32 / cell_size + 1) * cell_size) as f64; 87 | let height = ((height as i32 / cell_size + 1) * cell_size) as f64; 88 | 89 | SelectionRectangle { x, y, width, height } 90 | } 91 | 92 | /// Return an array of type [f64; 4] with [x, y, width, height]. 93 | pub fn to_array(&self) -> [f64; 4] { 94 | [self.x, self.y, self.width, self.height] 95 | } 96 | } 97 | 98 | pub struct InputEvents { 99 | pub events: VecDeque, 100 | } 101 | 102 | impl InputEvents { 103 | pub fn new() -> Self { 104 | InputEvents { 105 | events: VecDeque::with_capacity(32), 106 | } 107 | } 108 | } 109 | 110 | pub struct InputState { 111 | pub button_held: HashSet