diff --git a/.gitignore b/.gitignore index 1910c368..2252919b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ target /out template/Cargo.lock -agb*/Cargo.lock \ No newline at end of file +agb*/Cargo.lock +*.tiled-session \ No newline at end of file diff --git a/examples/the-dungeon-puzzlers-lament/build.rs b/examples/the-dungeon-puzzlers-lament/build.rs index ae98913d..f3eec135 100644 --- a/examples/the-dungeon-puzzlers-lament/build.rs +++ b/examples/the-dungeon-puzzlers-lament/build.rs @@ -5,6 +5,7 @@ use std::{ io::{BufWriter, Write}, str::FromStr, }; +use tiled::{Map, ObjectLayer, TileLayer}; use proc_macro2::TokenStream; @@ -28,6 +29,24 @@ const LEVEL_NAMES: &[&str] = &[ "level_spikes3", "level_around", "level_squidprogramming", + "a_familiar_sight", + "block_push_1", + "just_rocks", + "squid_rock", + "ice_ice", + "block_push_2", + "glove_key", + "block_push_3", + "teleporter_1", + "squid_teleport", + "teleporter_2", + "slime_teleporter", + "another_ice", + "another_ice_2", + "another_ice_3", + "another_ice_4", + "hole_introduction", + "rotator_1", ]; fn main() { @@ -38,7 +57,18 @@ fn main() { let ui_map = load_tmx(&mut tile_loader, "maps/UI.tmx"); let ui_tiles = export_ui_tiles(&ui_map, quote!(ui)); - let levels = LEVEL_NAMES + const DPL_LEVELS_ENVIRONMENT_VARIABLE: &str = "DPL_LEVELS"; + + println!( + "cargo:rerun-if-env-changed={}", + DPL_LEVELS_ENVIRONMENT_VARIABLE + ); + + let levels: Vec = env::var(DPL_LEVELS_ENVIRONMENT_VARIABLE) + .map(|x| x.split(',').map(|x| x.trim().to_string()).collect()) + .unwrap_or(LEVEL_NAMES.iter().map(|x| x.to_string()).collect()); + + let levels = levels .iter() .map(|level| load_level(&mut tile_loader, &format!("maps/levels/{level}.tmx"))) .collect::>(); @@ -100,6 +130,15 @@ enum Entity { SpikesDown, SquidUp, SquidDown, + Ice, + MovableBlock, + Glove, + Teleporter, + Hole, + RotatorUp, + RotatorDown, + RotatorLeft, + RotatorRight, } impl FromStr for Entity { @@ -123,6 +162,15 @@ impl FromStr for Entity { "SPIKES_DOWN" => SpikesDown, "SQUID_UP" => SquidUp, "SQUID_DOWN" => SquidDown, + "ICE" => Ice, + "BLOCK" => MovableBlock, + "GLOVE" => Glove, + "TELEPORTER" => Teleporter, + "HOLE" => Hole, + "ROTATOR_LEFT" => RotatorLeft, + "ROTATOR_RIGHT" => RotatorRight, + "ROTATOR_UP" => RotatorUp, + "ROTATOR_DOWN" => RotatorDown, _ => return Err(()), }) } @@ -147,6 +195,15 @@ impl quote::ToTokens for Entity { SpikesDown => quote!(Item::SpikesDown), SquidUp => quote!(Item::SquidUp), SquidDown => quote!(Item::SquidDown), + Ice => quote!(Item::Ice), + MovableBlock => quote!(Item::MovableBlock), + Glove => quote!(Item::Glove), + Teleporter => quote!(Item::Teleporter), + Hole => quote!(Item::Hole), + RotatorUp => quote!(Item::RotatorUp), + RotatorDown => quote!(Item::RotatorDown), + RotatorLeft => quote!(Item::RotatorLeft), + RotatorRight => quote!(Item::RotatorRight), }) } } @@ -203,6 +260,7 @@ impl quote::ToTokens for EntityWithPosition { struct Level { starting_items: Vec, fixed_positions: Vec, + solution_positions: Vec, directions: Vec, wall_bitmap: Vec, name: String, @@ -212,6 +270,7 @@ impl quote::ToTokens for Level { fn to_tokens(&self, tokens: &mut TokenStream) { let wall_bitmap = &self.wall_bitmap; let fixed_positions = &self.fixed_positions; + let solution_positions = &self.solution_positions; let directions = &self.directions; let starting_items = &self.starting_items; let name = &self.name; @@ -220,6 +279,7 @@ impl quote::ToTokens for Level { Level::new( Map::new(11, 10, &[#(#wall_bitmap),*]), &[#(#fixed_positions),*], + &[#(#solution_positions),*], &[#(#directions),*], &[#(#starting_items),*], #name, @@ -228,10 +288,10 @@ impl quote::ToTokens for Level { } } -fn export_level(map: &tiled::Map) -> Level { - let objects = map.get_layer(1).unwrap().as_object_layer().unwrap(); - - let fixed_positions = objects.objects().map(|obj| { +fn extract_objects_from_layer( + objects: ObjectLayer<'_>, +) -> impl Iterator + '_ { + objects.objects().map(|obj| { let entity: Entity = obj .name .parse() @@ -241,7 +301,20 @@ fn export_level(map: &tiled::Map) -> Level { let y = (obj.y / 16.0) as i32; EntityWithPosition(entity, (x, y)) - }); + }) +} + +fn export_level(map: &tiled::Map) -> Level { + let objects = map + .get_object_layer("Puzzle") + .expect("The puzzle object layer should exist"); + + let fixed_positions = extract_objects_from_layer(objects); + + let solution_positions = extract_objects_from_layer( + map.get_object_layer("Solution") + .expect("Should have an object layer called 'Solution'"), + ); let Some(tiled::PropertyValue::StringValue(starting_items)) = map.properties.get("ITEMS") else { @@ -296,6 +369,7 @@ fn export_level(map: &tiled::Map) -> Level { Level { starting_items: starting_items.collect(), fixed_positions: fixed_positions.collect(), + solution_positions: solution_positions.collect(), directions: directions.collect(), wall_bitmap: bool_to_bit(&are_walls.collect::>()), name: level_name.clone(), @@ -303,7 +377,9 @@ fn export_level(map: &tiled::Map) -> Level { } fn export_tiles(map: &tiled::Map, background: TokenStream) -> TokenStream { - let map_tiles = map.get_layer(0).unwrap().as_tile_layer().unwrap(); + let map_tiles = map + .get_tile_layer("Ground") + .expect("The ground layer should exist"); let width = map_tiles.width().unwrap() * 2; let height = map_tiles.height().unwrap() * 2; @@ -387,3 +463,22 @@ fn check_bool_to_bit() { let bools = [true, false, false, false, true, true, true, true]; assert_eq!(bool_to_bit(&bools), [0b11110001]); } + +trait TiledMapExtensions { + fn get_object_layer(&self, name: &str) -> Option; + fn get_tile_layer(&self, name: &str) -> Option; +} + +impl TiledMapExtensions for Map { + fn get_object_layer(&self, name: &str) -> Option { + self.layers() + .find(|x| x.name == name) + .and_then(|x| x.as_object_layer()) + } + + fn get_tile_layer(&self, name: &str) -> Option { + self.layers() + .find(|x| x.name == name) + .and_then(|x| x.as_tile_layer()) + } +} diff --git a/examples/the-dungeon-puzzlers-lament/gfx/concept.aseprite b/examples/the-dungeon-puzzlers-lament/gfx/concept.aseprite index 69259a09..78cf8352 100644 Binary files a/examples/the-dungeon-puzzlers-lament/gfx/concept.aseprite and b/examples/the-dungeon-puzzlers-lament/gfx/concept.aseprite differ diff --git a/examples/the-dungeon-puzzlers-lament/gfx/sprites16x16.aseprite b/examples/the-dungeon-puzzlers-lament/gfx/sprites16x16.aseprite index 5c818a91..56a76ea9 100644 Binary files a/examples/the-dungeon-puzzlers-lament/gfx/sprites16x16.aseprite and b/examples/the-dungeon-puzzlers-lament/gfx/sprites16x16.aseprite differ diff --git a/examples/the-dungeon-puzzlers-lament/maps/levels/a_familiar_sight.tmx b/examples/the-dungeon-puzzlers-lament/maps/levels/a_familiar_sight.tmx new file mode 100644 index 00000000..e6583806 --- /dev/null +++ b/examples/the-dungeon-puzzlers-lament/maps/levels/a_familiar_sight.tmx @@ -0,0 +1,36 @@ + + + + + + + + + + +0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0, +0,1,6,5,2,2,6,4,8,9,0, +0,10,17,17,12,15,13,17,17,38,0, +0,19,20,20,20,25,20,22,21,27,0, +0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0 + + + + + + + + + + + + + + + + diff --git a/examples/the-dungeon-puzzlers-lament/maps/levels/another_ice.tmx b/examples/the-dungeon-puzzlers-lament/maps/levels/another_ice.tmx new file mode 100644 index 00000000..5b79dfd8 --- /dev/null +++ b/examples/the-dungeon-puzzlers-lament/maps/levels/another_ice.tmx @@ -0,0 +1,63 @@ + + + + + + + + + + +0,0,0,1,2,9,0,1,8,6,9, +0,0,0,10,17,18,1,1073741852,17,12,18, +0,1,5,1073741852,17,3221225500,1073741852,14,13,15,47, +0,46,16,17,15,12,16,12,2147483676,26,27, +0,10,12,3221225505,1073741857,3221225505,1073741857,16,18,0,0, +0,37,15,1073741863,2147483689,41,1073741863,13,3221225500,9,0, +0,46,13,17,1073741864,3221225512,14,11,15,18,0, +0,19,28,11,17,16,12,13,11,47,0, +0,0,19,20,22,20,28,15,14,18,0, +0,0,0,0,0,0,19,26,23,27,0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/the-dungeon-puzzlers-lament/maps/levels/another_ice_2.tmx b/examples/the-dungeon-puzzlers-lament/maps/levels/another_ice_2.tmx new file mode 100644 index 00000000..5d1d638b --- /dev/null +++ b/examples/the-dungeon-puzzlers-lament/maps/levels/another_ice_2.tmx @@ -0,0 +1,132 @@ + + + + + + + + + + +1,8,2,7,8,8,3,7,4,2,9, +46,17,13,17,13,12,12,15,11,16,38, +10,14,12,11,16,15,16,13,14,17,47, +37,13,16,15,16,13,16,14,16,13,38, +46,17,17,17,17,17,11,17,15,11,38, +46,17,13,12,17,15,15,16,17,15,38, +10,15,17,11,15,12,11,12,17,16,18, +10,13,13,17,15,17,16,13,2147483676,20,27, +19,28,15,13,17,12,2147483676,22,27,0,0, +0,19,24,20,25,20,27,0,0,0,0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/the-dungeon-puzzlers-lament/maps/levels/another_ice_3.tmx b/examples/the-dungeon-puzzlers-lament/maps/levels/another_ice_3.tmx new file mode 100644 index 00000000..8004709d --- /dev/null +++ b/examples/the-dungeon-puzzlers-lament/maps/levels/another_ice_3.tmx @@ -0,0 +1,72 @@ + + + + + + + + + + +1,8,2,7,8,8,3,9,1,2,9, +46,2147483676,22,23,28,12,12,38,19,26,27, +19,27,1,9,46,15,16,38,1,5,9, +1,8,32,2147483680,1073741852,13,16,18,37,13,38, +46,17,3221225504,1073741856,17,17,17,38,19,23,27, +19,22,27,19,22,28,16,18,1,7,9, +0,0,0,1,2,32,15,18,19,26,27, +1,4,7,1073741852,15,1073741863,16,3221225500,5,3,9, +37,13,16,13,17,11,13,12,12,11,38, +19,22,22,20,25,22,26,25,26,24,27 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/the-dungeon-puzzlers-lament/maps/levels/another_ice_4.tmx b/examples/the-dungeon-puzzlers-lament/maps/levels/another_ice_4.tmx new file mode 100644 index 00000000..8069e5f1 --- /dev/null +++ b/examples/the-dungeon-puzzlers-lament/maps/levels/another_ice_4.tmx @@ -0,0 +1,54 @@ + + + + + + + + + + +0,0,0,1,7,9,0,0,0,0,0, +0,0,1,1073741852,13,3221225500,8,9,0,0,0, +0,0,37,12,17,16,15,3221225500,9,0,0, +0,0,10,15,11,16,13,15,47,0,0, +0,0,37,16,14,17,14,11,18,0,0, +0,0,10,17,13,17,11,12,38,0,0, +0,0,46,17,16,12,13,11,47,0,0, +0,0,19,28,16,15,17,14,38,0,0, +0,0,0,19,22,28,11,2147483676,27,0,0, +0,0,0,0,0,19,24,27,0,0,0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/the-dungeon-puzzlers-lament/maps/levels/block_push_1.tmx b/examples/the-dungeon-puzzlers-lament/maps/levels/block_push_1.tmx new file mode 100644 index 00000000..883df163 --- /dev/null +++ b/examples/the-dungeon-puzzlers-lament/maps/levels/block_push_1.tmx @@ -0,0 +1,45 @@ + + + + + + + + + + +0,1,3,2,6,9,0,0,0,0,0, +0,46,15,14,13,3221225500,4,6,7,7,9, +0,46,12,11,15,16,11,13,15,14,38, +0,46,12,13,14,16,17,14,11,12,18, +0,10,13,16,15,14,12,14,13,15,18, +0,46,16,17,17,13,15,14,16,15,18, +0,19,28,17,11,16,12,15,17,15,38, +0,0,19,20,28,17,17,15,17,2147483676,27, +0,0,0,0,19,21,25,20,23,27,0, +0,0,0,0,0,0,0,0,0,0,0 + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/the-dungeon-puzzlers-lament/maps/levels/block_push_2.tmx b/examples/the-dungeon-puzzlers-lament/maps/levels/block_push_2.tmx new file mode 100644 index 00000000..2aaef9c0 --- /dev/null +++ b/examples/the-dungeon-puzzlers-lament/maps/levels/block_push_2.tmx @@ -0,0 +1,45 @@ + + + + + + + + + + +0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,1,4,3,9,0,0,0, +0,0,0,1,1073741852,16,15,3221225500,8,9,0, +0,0,0,46,12,15,13,17,11,38,0, +0,0,0,19,24,28,17,2147483676,25,27,0, +0,0,0,0,0,10,13,47,0,0,0, +0,0,0,0,0,19,20,27,0,0,0, +0,0,0,0,0,0,0,0,0,0,0 + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/the-dungeon-puzzlers-lament/maps/levels/block_push_3.tmx b/examples/the-dungeon-puzzlers-lament/maps/levels/block_push_3.tmx new file mode 100644 index 00000000..48c2c3ec --- /dev/null +++ b/examples/the-dungeon-puzzlers-lament/maps/levels/block_push_3.tmx @@ -0,0 +1,48 @@ + + + + + + + + + + +0,0,0,0,0,0,0,0,0,0,0, +0,0,1,6,2,8,8,9,0,0,0, +0,1,1073741852,17,12,16,12,3221225500,9,0,0, +0,46,11,16,12,17,17,12,38,0,0, +0,46,17,16,12,16,11,14,38,0,0, +0,19,26,28,14,12,14,15,38,0,0, +0,0,0,19,24,28,11,2147483676,27,0,0, +0,0,0,0,0,19,23,27,0,0,0, +0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/the-dungeon-puzzlers-lament/maps/levels/glove_key.tmx b/examples/the-dungeon-puzzlers-lament/maps/levels/glove_key.tmx new file mode 100644 index 00000000..ff6161be --- /dev/null +++ b/examples/the-dungeon-puzzlers-lament/maps/levels/glove_key.tmx @@ -0,0 +1,48 @@ + + + + + + + + + + +0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0, +0,1,8,3,2,2,8,5,4,9,0, +0,10,17,15,17,11,13,15,16,38,0, +1,1073741852,11,12,11,13,17,13,16,47,0, +10,12,17,14,15,13,13,11,13,18,0, +19,28,12,15,13,13,14,15,13,38,0, +0,19,25,26,24,22,25,24,24,27,0, +0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/the-dungeon-puzzlers-lament/maps/levels/hole_introduction.tmx b/examples/the-dungeon-puzzlers-lament/maps/levels/hole_introduction.tmx new file mode 100644 index 00000000..434cc987 --- /dev/null +++ b/examples/the-dungeon-puzzlers-lament/maps/levels/hole_introduction.tmx @@ -0,0 +1,39 @@ + + + + + + + + + + +0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0, +0,1,6,5,2,2,6,4,8,9,0, +0,10,17,17,12,15,13,17,17,38,0, +0,19,20,20,20,25,20,22,21,27,0, +0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0 + + + + + + + + + + + + + + + + + + + diff --git a/examples/the-dungeon-puzzlers-lament/maps/levels/ice_ice.tmx b/examples/the-dungeon-puzzlers-lament/maps/levels/ice_ice.tmx new file mode 100644 index 00000000..616beb28 --- /dev/null +++ b/examples/the-dungeon-puzzlers-lament/maps/levels/ice_ice.tmx @@ -0,0 +1,60 @@ + + + + + + + + + + +0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0, +0,1,6,9,0,0,0,1,6,9,0, +0,10,16,3221225500,9,0,1,1073741852,15,47,0, +0,46,13,17,47,0,37,11,11,18,0, +0,37,13,14,3221225500,7,1073741852,11,11,18,0, +0,10,17,14,11,15,14,17,15,18,0, +0,10,16,13,11,17,16,14,11,18,0, +0,19,21,21,23,28,16,2147483676,26,27,0, +0,0,0,0,0,19,26,27,0,0,0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/the-dungeon-puzzlers-lament/maps/levels/just_rocks.tmx b/examples/the-dungeon-puzzlers-lament/maps/levels/just_rocks.tmx new file mode 100644 index 00000000..d0fc2bcc --- /dev/null +++ b/examples/the-dungeon-puzzlers-lament/maps/levels/just_rocks.tmx @@ -0,0 +1,48 @@ + + + + + + + + + + +0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0, +0,0,1,3,5,4,5,9,0,0,0, +0,0,46,17,13,15,11,3221225500,9,0,0, +0,1,1073741852,12,17,15,13,15,3221225500,9,0, +0,10,13,13,13,15,17,11,17,18,0, +0,10,14,13,11,15,12,16,15,47,0, +0,19,20,28,17,16,14,2147483676,21,27,0, +0,0,0,19,21,23,26,27,0,0,0, +0,0,0,0,0,0,0,0,0,0,0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/the-dungeon-puzzlers-lament/maps/levels/level1.tmx b/examples/the-dungeon-puzzlers-lament/maps/levels/level1.tmx index 2db372a0..047ea962 100644 --- a/examples/the-dungeon-puzzlers-lament/maps/levels/level1.tmx +++ b/examples/the-dungeon-puzzlers-lament/maps/levels/level1.tmx @@ -1,12 +1,12 @@ - + - + 0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0, @@ -20,7 +20,7 @@ 0,0,0,0,0,0,0,0,0,0,0 - + @@ -31,4 +31,9 @@ + + + + + diff --git a/examples/the-dungeon-puzzlers-lament/maps/levels/level2.tmx b/examples/the-dungeon-puzzlers-lament/maps/levels/level2.tmx index 39e6acef..d8464078 100644 --- a/examples/the-dungeon-puzzlers-lament/maps/levels/level2.tmx +++ b/examples/the-dungeon-puzzlers-lament/maps/levels/level2.tmx @@ -1,12 +1,12 @@ - + - + 0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,1,3,9,0,0,0,0, @@ -20,7 +20,7 @@ 0,0,0,0,0,0,0,0,0,0,0 - + @@ -28,4 +28,9 @@ + + + + + diff --git a/examples/the-dungeon-puzzlers-lament/maps/levels/level3.tmx b/examples/the-dungeon-puzzlers-lament/maps/levels/level3.tmx index 28abd513..d4d8e227 100644 --- a/examples/the-dungeon-puzzlers-lament/maps/levels/level3.tmx +++ b/examples/the-dungeon-puzzlers-lament/maps/levels/level3.tmx @@ -1,12 +1,12 @@ - + - + 0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0, @@ -20,7 +20,7 @@ 0,0,0,0,0,0,0,0,0,0,0 - + @@ -34,4 +34,9 @@ + + + + + diff --git a/examples/the-dungeon-puzzlers-lament/maps/levels/level4.tmx b/examples/the-dungeon-puzzlers-lament/maps/levels/level4.tmx index f306fbfc..e6337c49 100644 --- a/examples/the-dungeon-puzzlers-lament/maps/levels/level4.tmx +++ b/examples/the-dungeon-puzzlers-lament/maps/levels/level4.tmx @@ -1,12 +1,12 @@ - + - + 0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0, @@ -20,7 +20,7 @@ 0,0,0,0,0,0,0,0,0,0,0 - + @@ -31,4 +31,9 @@ + + + + + diff --git a/examples/the-dungeon-puzzlers-lament/maps/levels/level5.tmx b/examples/the-dungeon-puzzlers-lament/maps/levels/level5.tmx index c1fe7171..35b5a899 100644 --- a/examples/the-dungeon-puzzlers-lament/maps/levels/level5.tmx +++ b/examples/the-dungeon-puzzlers-lament/maps/levels/level5.tmx @@ -1,12 +1,12 @@ - + - + 0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0, @@ -20,7 +20,7 @@ 0,0,0,0,0,0,0,0,0,0,0 - + @@ -28,4 +28,12 @@ + + + + + + + + diff --git a/examples/the-dungeon-puzzlers-lament/maps/levels/level6.tmx b/examples/the-dungeon-puzzlers-lament/maps/levels/level6.tmx index 5e1bc8cd..5d433dcc 100644 --- a/examples/the-dungeon-puzzlers-lament/maps/levels/level6.tmx +++ b/examples/the-dungeon-puzzlers-lament/maps/levels/level6.tmx @@ -1,12 +1,12 @@ - + - + 0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0, @@ -20,7 +20,7 @@ 0,0,0,0,0,0,0,0,0,0,0 - + @@ -37,4 +37,9 @@ + + + + + diff --git a/examples/the-dungeon-puzzlers-lament/maps/levels/level_around.tmx b/examples/the-dungeon-puzzlers-lament/maps/levels/level_around.tmx index a2e0cee9..ce43a108 100644 --- a/examples/the-dungeon-puzzlers-lament/maps/levels/level_around.tmx +++ b/examples/the-dungeon-puzzlers-lament/maps/levels/level_around.tmx @@ -1,26 +1,26 @@ - + - + -0,0,1,7,4,7,5,2,2,2,9, -0,0,46,11,11,17,13,12,14,16,18, -1,6,1073741852,13,16,11,12,12,13,12,18, -46,16,13,13,15,14,13,16,15,13,47, -37,17,12,14,12,13,17,11,2147483676,26,27, -19,28,11,13,14,11,16,16,18,0,0, -0,37,16,12,13,17,17,15,18,0,0, -0,19,28,16,15,17,15,11,47,0,0, -0,0,46,12,15,12,16,2147483676,27,0,0, -0,0,19,21,25,23,22,27,0,0,0 +0,0,0,0,1,7,5,6,9,0,0, +0,0,1,4,1073741852,17,13,13,3221225500,9,0, +0,1,1073741852,13,16,11,12,16,16,18,0, +0,37,13,13,15,14,13,11,11,47,0, +0,46,12,14,12,13,17,17,2147483676,27,0, +0,46,11,13,14,11,16,13,47,0,0, +0,19,28,12,13,17,17,16,47,0,0, +0,0,19,28,15,17,15,2147483676,27,0,0, +0,0,0,19,22,25,20,27,0,0,0, +0,0,0,0,0,0,0,0,0,0,0 - + @@ -28,4 +28,18 @@ + + + + + + + + + + + + + + diff --git a/examples/the-dungeon-puzzlers-lament/maps/levels/level_spikes.tmx b/examples/the-dungeon-puzzlers-lament/maps/levels/level_spikes.tmx index d50168d9..845fa321 100644 --- a/examples/the-dungeon-puzzlers-lament/maps/levels/level_spikes.tmx +++ b/examples/the-dungeon-puzzlers-lament/maps/levels/level_spikes.tmx @@ -1,12 +1,12 @@ - + - + 0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0, @@ -20,7 +20,7 @@ 0,0,0,0,0,0,0,0,0,0,0 - + @@ -37,4 +37,12 @@ + + + + + + + + diff --git a/examples/the-dungeon-puzzlers-lament/maps/levels/level_spikes2.tmx b/examples/the-dungeon-puzzlers-lament/maps/levels/level_spikes2.tmx index f1e28c2f..1f3fbde5 100644 --- a/examples/the-dungeon-puzzlers-lament/maps/levels/level_spikes2.tmx +++ b/examples/the-dungeon-puzzlers-lament/maps/levels/level_spikes2.tmx @@ -1,12 +1,12 @@ - + - + 0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0, @@ -20,7 +20,7 @@ 0,0,0,0,0,0,0,0,0,0,0 - + @@ -40,4 +40,18 @@ + + + + + + + + + + + + + + diff --git a/examples/the-dungeon-puzzlers-lament/maps/levels/level_spikes3.tmx b/examples/the-dungeon-puzzlers-lament/maps/levels/level_spikes3.tmx index e8e67c6e..fe1fefb3 100644 --- a/examples/the-dungeon-puzzlers-lament/maps/levels/level_spikes3.tmx +++ b/examples/the-dungeon-puzzlers-lament/maps/levels/level_spikes3.tmx @@ -1,12 +1,12 @@ - + - + 0,0,1,8,4,1073741872,2,7,9,0,0, 0,0,37,15,15,1073741863,15,13,3221225500,3,9, @@ -20,7 +20,7 @@ 0,0,0,0,0,19,21,22,23,27,0 - + @@ -43,4 +43,15 @@ + + + + + + + + + + + diff --git a/examples/the-dungeon-puzzlers-lament/maps/levels/level_squid1.tmx b/examples/the-dungeon-puzzlers-lament/maps/levels/level_squid1.tmx index 79dcfd30..0e062937 100644 --- a/examples/the-dungeon-puzzlers-lament/maps/levels/level_squid1.tmx +++ b/examples/the-dungeon-puzzlers-lament/maps/levels/level_squid1.tmx @@ -1,12 +1,12 @@ - + - + 0,0,0,0,1,8,2,6,9,0,0, 0,0,0,0,46,11,11,11,38,0,0, @@ -20,7 +20,7 @@ 0,0,0,0,0,19,23,27,0,0,0 - + @@ -34,4 +34,15 @@ + + + + + + + + + + + diff --git a/examples/the-dungeon-puzzlers-lament/maps/levels/level_squid2.tmx b/examples/the-dungeon-puzzlers-lament/maps/levels/level_squid2.tmx index a740e0fc..327ce03b 100644 --- a/examples/the-dungeon-puzzlers-lament/maps/levels/level_squid2.tmx +++ b/examples/the-dungeon-puzzlers-lament/maps/levels/level_squid2.tmx @@ -1,12 +1,12 @@ - + - + 0,0,0,0,0,0,0,0,0,0,0, 0,0,0,1,5,9,0,0,0,0,0, @@ -20,7 +20,7 @@ 0,0,0,0,0,0,0,0,0,0,0 - + @@ -37,4 +37,15 @@ + + + + + + + + + + + diff --git a/examples/the-dungeon-puzzlers-lament/maps/levels/level_squid_button.tmx b/examples/the-dungeon-puzzlers-lament/maps/levels/level_squid_button.tmx index a6d7a077..05481924 100644 --- a/examples/the-dungeon-puzzlers-lament/maps/levels/level_squid_button.tmx +++ b/examples/the-dungeon-puzzlers-lament/maps/levels/level_squid_button.tmx @@ -1,12 +1,12 @@ - + - + 0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,1,2,9,0,0,0, @@ -20,7 +20,7 @@ 0,0,0,0,0,0,0,0,0,0,0 - + @@ -40,4 +40,21 @@ + + + + + + + + + + + + + + + + + diff --git a/examples/the-dungeon-puzzlers-lament/maps/levels/level_squid_drop.tmx b/examples/the-dungeon-puzzlers-lament/maps/levels/level_squid_drop.tmx index 1dc5cabd..f00d08fc 100644 --- a/examples/the-dungeon-puzzlers-lament/maps/levels/level_squid_drop.tmx +++ b/examples/the-dungeon-puzzlers-lament/maps/levels/level_squid_drop.tmx @@ -1,12 +1,12 @@ - + - + 0,0,0,1,4,8,3,2,3,9,0, 0,0,0,46,11,12,14,16,14,38,0, @@ -20,7 +20,7 @@ 0,0,0,0,0,0,0,0,0,0,0 - + @@ -34,4 +34,21 @@ + + + + + + + + + + + + + + + + + diff --git a/examples/the-dungeon-puzzlers-lament/maps/levels/level_squid_force_button.tmx b/examples/the-dungeon-puzzlers-lament/maps/levels/level_squid_force_button.tmx index 918dd3c5..b962a0ca 100644 --- a/examples/the-dungeon-puzzlers-lament/maps/levels/level_squid_force_button.tmx +++ b/examples/the-dungeon-puzzlers-lament/maps/levels/level_squid_force_button.tmx @@ -1,12 +1,12 @@ - + - + 0,0,0,0,0,0,0,0,0,0,0, 0,1,7,9,0,0,0,0,0,0,0, @@ -20,7 +20,7 @@ 0,0,0,0,0,0,0,0,0,0,0 - + @@ -37,4 +37,12 @@ + + + + + + + + diff --git a/examples/the-dungeon-puzzlers-lament/maps/levels/level_squid_intro.tmx b/examples/the-dungeon-puzzlers-lament/maps/levels/level_squid_intro.tmx index f16222ce..d121b081 100644 --- a/examples/the-dungeon-puzzlers-lament/maps/levels/level_squid_intro.tmx +++ b/examples/the-dungeon-puzzlers-lament/maps/levels/level_squid_intro.tmx @@ -1,12 +1,12 @@ - + - + 0,0,0,1,8,9,0,0,0,0,0, 1,7,2,32,16,3221225500,9,0,0,0,0, @@ -20,7 +20,7 @@ 0,0,0,19,23,25,48,22,27,0,0 - + @@ -40,4 +40,12 @@ + + + + + + + + diff --git a/examples/the-dungeon-puzzlers-lament/maps/levels/level_squid_item.tmx b/examples/the-dungeon-puzzlers-lament/maps/levels/level_squid_item.tmx index 8c250cdb..d60fe025 100644 --- a/examples/the-dungeon-puzzlers-lament/maps/levels/level_squid_item.tmx +++ b/examples/the-dungeon-puzzlers-lament/maps/levels/level_squid_item.tmx @@ -1,12 +1,12 @@ - + - + 0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,1,3,7,6,9,0,0, @@ -20,7 +20,7 @@ 0,0,0,0,0,0,0,0,0,0,0 - + @@ -40,4 +40,15 @@ + + + + + + + + + + + diff --git a/examples/the-dungeon-puzzlers-lament/maps/levels/level_squidprogramming.tmx b/examples/the-dungeon-puzzlers-lament/maps/levels/level_squidprogramming.tmx index b7c12c53..c422f7d3 100644 --- a/examples/the-dungeon-puzzlers-lament/maps/levels/level_squidprogramming.tmx +++ b/examples/the-dungeon-puzzlers-lament/maps/levels/level_squidprogramming.tmx @@ -1,12 +1,12 @@ - + - + 0,1,4,3221225492,7,2147483655,3,1073741844,1073741850,2147483649,0, 0,37,2147483659,11,2147483659,2147483665,3221225486,3221225484,1073741839,3221225518,0, @@ -20,7 +20,7 @@ 19,2147483672,2147483673,1073741831,1073741832,1073741829,21,1073741826,22,22,27 - + @@ -49,4 +49,15 @@ + + + + + + + + + + + diff --git a/examples/the-dungeon-puzzlers-lament/maps/levels/level_switch.tmx b/examples/the-dungeon-puzzlers-lament/maps/levels/level_switch.tmx index 333cba05..f81b4dbb 100644 --- a/examples/the-dungeon-puzzlers-lament/maps/levels/level_switch.tmx +++ b/examples/the-dungeon-puzzlers-lament/maps/levels/level_switch.tmx @@ -1,12 +1,12 @@ - + - + 0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0, @@ -20,7 +20,7 @@ 0,0,0,0,0,0,0,0,0,0,0 - + @@ -34,4 +34,9 @@ + + + + + diff --git a/examples/the-dungeon-puzzlers-lament/maps/levels/rotator_1.tmx b/examples/the-dungeon-puzzlers-lament/maps/levels/rotator_1.tmx new file mode 100644 index 00000000..a07675ac --- /dev/null +++ b/examples/the-dungeon-puzzlers-lament/maps/levels/rotator_1.tmx @@ -0,0 +1,57 @@ + + + + + + + + + + +1,6,7,7,4,5,3,2,4,7,9, +37,12,14,13,12,15,12,16,14,13,47, +19,22,26,26,26,25,23,22,24,26,27, +1,7,4,7,2,2,8,2,2,5,9, +10,11,14,13,15,11,14,14,12,17,38, +46,12,12,14,12,16,17,15,17,13,18, +10,14,17,14,17,14,16,15,16,11,38, +10,15,16,16,11,11,11,13,16,15,18, +10,12,14,16,14,12,14,13,12,17,47, +19,23,24,26,22,20,24,25,26,25,27 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/the-dungeon-puzzlers-lament/maps/levels/rotator_2.tmx b/examples/the-dungeon-puzzlers-lament/maps/levels/rotator_2.tmx new file mode 100644 index 00000000..4783ebeb --- /dev/null +++ b/examples/the-dungeon-puzzlers-lament/maps/levels/rotator_2.tmx @@ -0,0 +1,48 @@ + + + + + + + + + + +0,0,0,0,0,1,6,8,5,4,9, +0,0,1,7,8,32,11,17,12,17,38, +0,0,10,14,17,1073741863,14,2147483676,28,13,38, +0,0,37,11,11,13,13,38,46,15,18, +1,8,1073741852,16,16,11,13,18,10,11,38, +37,16,17,12,13,11,17,18,46,11,47, +10,12,2147483676,22,26,23,23,27,10,17,38, +46,16,3221225500,5,6,7,5,3,1073741852,15,47, +10,13,16,15,13,13,12,14,11,12,47, +19,26,20,21,25,20,22,24,21,24,27 + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/the-dungeon-puzzlers-lament/maps/levels/rotator_demonstration.tmx b/examples/the-dungeon-puzzlers-lament/maps/levels/rotator_demonstration.tmx new file mode 100644 index 00000000..896034e4 --- /dev/null +++ b/examples/the-dungeon-puzzlers-lament/maps/levels/rotator_demonstration.tmx @@ -0,0 +1,45 @@ + + + + + + + + + + +0,1,8,3,9,1,7,9,0,0,0, +1,1073741852,11,11,3221225500,1073741852,13,38,0,0,0, +37,17,16,14,16,14,15,3221225500,7,9,0, +10,11,39,16,2147483676,28,16,16,11,47,0, +37,13,3221225504,24,27,10,15,14,29,30,0, +37,12,3221225500,9,0,37,15,12,15,38,0, +37,11,13,3221225500,6,1073741852,12,15,14,47,0, +19,28,12,11,13,17,11,39,15,38,0, +0,19,22,28,14,15,12,3221225504,26,27,0, +0,0,0,19,20,24,21,27,0,0,0 + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/the-dungeon-puzzlers-lament/maps/levels/slime_teleporter.tmx b/examples/the-dungeon-puzzlers-lament/maps/levels/slime_teleporter.tmx new file mode 100644 index 00000000..1dab8d70 --- /dev/null +++ b/examples/the-dungeon-puzzlers-lament/maps/levels/slime_teleporter.tmx @@ -0,0 +1,57 @@ + + + + + + + + + + +0,0,0,0,0,0,0,0,0,0,0, +0,0,0,1,6,4,3,9,0,0,0, +0,0,1,1073741852,14,15,15,3221225500,8,9,0, +0,1,1073741852,17,15,12,14,17,16,38,0, +0,10,11,39,14,39,15,17,11,18,0, +0,37,16,1073741863,12,1073741863,15,14,12,38,0, +0,46,17,29,2147483677,15,15,11,12,18,0, +0,19,20,28,17,13,11,11,13,47,0, +0,0,0,19,24,28,14,15,2147483676,27,0, +0,0,0,0,0,19,22,22,27,0,0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/the-dungeon-puzzlers-lament/maps/levels/squid_rock.tmx b/examples/the-dungeon-puzzlers-lament/maps/levels/squid_rock.tmx new file mode 100644 index 00000000..5c5f5ba6 --- /dev/null +++ b/examples/the-dungeon-puzzlers-lament/maps/levels/squid_rock.tmx @@ -0,0 +1,48 @@ + + + + + + + + + + +0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,1,6,2,7,5,9,0, +0,0,1,8,1073741852,17,15,13,17,38,0, +0,0,46,15,15,11,11,17,15,47,0, +0,0,10,15,12,17,17,15,11,38,0, +0,0,19,21,28,14,17,14,16,18,0, +0,0,0,0,19,24,28,16,13,38,0, +0,0,0,0,0,0,19,22,23,27,0, +0,0,0,0,0,0,0,0,0,0,0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/the-dungeon-puzzlers-lament/maps/levels/squid_teleport.tmx b/examples/the-dungeon-puzzlers-lament/maps/levels/squid_teleport.tmx new file mode 100644 index 00000000..5807282d --- /dev/null +++ b/examples/the-dungeon-puzzlers-lament/maps/levels/squid_teleport.tmx @@ -0,0 +1,57 @@ + + + + + + + + + + +0,0,0,0,0,0,0,0,0,0,0, +1,7,4,2,6,9,0,1,8,9,0, +10,15,11,17,15,18,0,46,11,18,0, +46,16,14,39,14,47,0,37,14,18,0, +46,11,13,1073741863,17,18,0,46,17,18,0, +10,16,17,11,2147483676,27,0,46,13,38,0, +19,28,11,14,47,0,0,19,22,27,0, +0,37,15,2147483676,27,0,0,0,0,0,0, +0,19,25,27,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/the-dungeon-puzzlers-lament/maps/levels/teleporter_1.tmx b/examples/the-dungeon-puzzlers-lament/maps/levels/teleporter_1.tmx new file mode 100644 index 00000000..1d30a67a --- /dev/null +++ b/examples/the-dungeon-puzzlers-lament/maps/levels/teleporter_1.tmx @@ -0,0 +1,39 @@ + + + + + + + + + + +0,0,0,0,0,0,0,0,0,0,0, +0,1,4,5,4,3,7,2,8,9,0, +0,37,11,17,13,16,13,12,17,38,0, +0,19,21,22,20,26,26,20,25,27,0, +0,0,0,1,7,3,3,9,0,0,0, +0,0,1,1073741852,13,16,17,3221225500,9,0,0, +0,0,37,13,11,12,14,13,18,0,0, +0,0,19,21,28,16,2147483676,25,27,0,0, +0,0,0,0,19,25,27,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0 + + + + + + + + + + + + + + + + + + + diff --git a/examples/the-dungeon-puzzlers-lament/maps/levels/teleporter_2.tmx b/examples/the-dungeon-puzzlers-lament/maps/levels/teleporter_2.tmx new file mode 100644 index 00000000..f3ee9d48 --- /dev/null +++ b/examples/the-dungeon-puzzlers-lament/maps/levels/teleporter_2.tmx @@ -0,0 +1,66 @@ + + + + + + + + + + +0,0,1,8,7,7,5,9,0,0,0, +1,6,1073741852,15,16,15,13,3221225500,9,0,0, +46,15,11,17,17,12,11,14,47,0,0, +46,13,14,13,14,13,12,2147483676,27,0,0, +46,15,16,17,13,12,17,3221225500,8,9,0, +19,28,13,12,17,12,11,16,13,47,0, +0,19,25,28,15,11,16,12,2147483676,27,0, +0,0,0,19,28,17,2147483676,22,27,0,0, +0,0,0,0,46,15,38,0,0,0,0, +0,0,0,0,19,23,27,0,0,0,0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/the-dungeon-puzzlers-lament/src/game/numbers.rs b/examples/the-dungeon-puzzlers-lament/src/game/numbers.rs index 346e42bd..c2fc371e 100644 --- a/examples/the-dungeon-puzzlers-lament/src/game/numbers.rs +++ b/examples/the-dungeon-puzzlers-lament/src/game/numbers.rs @@ -19,4 +19,184 @@ pub const NUMBERS: &[&str] = &[ "Eighteen", "Ninteen", "Twenty", + "Twenty One", + "Twenty Two", + "Twenty Three", + "Twenty Four", + "Twenty Five", + "Twenty Six", + "Twenty Seven", + "Twenty Eight", + "Twenty Nine", + "Thirty", + "Thirty One", + "Thirty Two", + "Thirty Three", + "Thirty Four", + "Thirty Five", + "Thirty Six", + "Thirty Seven", + "Thirty Eight", + "Thirty Nine", + "Forty", + "Forty One", + "Forty Two", + "Forty Three", + "Forty Four", + "Forty Five", + "Forty Six", + "Forty Seven", + "Forty Eight", + "Forty Nine", + "Fifty", + "Fifty One", + "Fifty Two", + "Fifty Three", + "Fifty Four", + "Fifty Five", + "Fifty Six", + "Fifty Seven", + "Fifty Eight", + "Fifty Nine", + "Sixty", + "Sixty One", + "Sixty Two", + "Sixty Three", + "Sixty Four", + "Sixty Five", + "Sixty Six", + "Sixty Seven", + "Sixty Eight", + "Sixty Nine", + "Seventy", + "Seventy One", + "Seventy Two", + "Seventy Three", + "Seventy Four", + "Seventy Five", + "Seventy Six", + "Seventy Seven", + "Seventy Eight", + "Seventy Nine", + "Eighty", + "Eighty One", + "Eighty Two", + "Eighty Three", + "Eighty Four", + "Eighty Five", + "Eighty Six", + "Eighty Seven", + "Eighty Eight", + "Eighty Nine", + "Ninety", + "Ninety One", + "Ninety Two", + "Ninety Three", + "Ninety Four", + "Ninety Five", + "Ninety Six", + "Ninety Seven", + "Ninety Eight", + "Ninety Nine", + "One Hundred", + "One Hundred and One", + "One Hundred and Two", + "One Hundred and Three", + "One Hundred and Four", + "One Hundred and Five", + "One Hundred and Six", + "One Hundred and Seven", + "One Hundred and Eight", + "One Hundred and Nine", + "One Hundred and Ten", + "One Hundred and Eleven", + "One Hundred and Twelve", + "One Hundred and Thirteen", + "One Hundred and Fourteen", + "One Hundred and Fifteen", + "One Hundred and Sixteen", + "One Hundred and Seventeen", + "One Hundred and Eighteen", + "One Hundred and Nineteen", + "One Hundred and Twenty", + "One Hundred and Twenty One", + "One Hundred and Twenty Two", + "One Hundred and Twenty Three", + "One Hundred and Twenty Four", + "One Hundred and Twenty Five", + "One Hundred and Twenty Six", + "One Hundred and Twenty Seven", + "One Hundred and Twenty Eight", + "One Hundred and Twenty Nine", + "One Hundred and Thirty", + "One Hundred and Thirty One", + "One Hundred and Thirty Two", + "One Hundred and Thirty Three", + "One Hundred and Thirty Four", + "One Hundred and Thirty Five", + "One Hundred and Thirty Six", + "One Hundred and Thirty Seven", + "One Hundred and Thirty Eight", + "One Hundred and Thirty Nine", + "One Hundred and Forty", + "One Hundred and Forty One", + "One Hundred and Forty Two", + "One Hundred and Forty Three", + "One Hundred and Forty Four", + "One Hundred and Forty Five", + "One Hundred and Forty Six", + "One Hundred and Forty Seven", + "One Hundred and Forty Eight", + "One Hundred and Forty Nine", + "One Hundred and Fifty", + "One Hundred and Fifty One", + "One Hundred and Fifty Two", + "One Hundred and Fifty Three", + "One Hundred and Fifty Four", + "One Hundred and Fifty Five", + "One Hundred and Fifty Six", + "One Hundred and Fifty Seven", + "One Hundred and Fifty Eight", + "One Hundred and Fifty Nine", + "One Hundred and Sixty", + "One Hundred and Sixty One", + "One Hundred and Sixty Two", + "One Hundred and Sixty Three", + "One Hundred and Sixty Four", + "One Hundred and Sixty Five", + "One Hundred and Sixty Six", + "One Hundred and Sixty Seven", + "One Hundred and Sixty Eight", + "One Hundred and Sixty Nine", + "One Hundred and Seventy", + "One Hundred and Seventy One", + "One Hundred and Seventy Two", + "One Hundred and Seventy Three", + "One Hundred and Seventy Four", + "One Hundred and Seventy Five", + "One Hundred and Seventy Six", + "One Hundred and Seventy Seven", + "One Hundred and Seventy Eight", + "One Hundred and Seventy Nine", + "One Hundred and Eighty", + "One Hundred and Eighty One", + "One Hundred and Eighty Two", + "One Hundred and Eighty Three", + "One Hundred and Eighty Four", + "One Hundred and Eighty Five", + "One Hundred and Eighty Six", + "One Hundred and Eighty Seven", + "One Hundred and Eighty Eight", + "One Hundred and Eighty Nine", + "One Hundred and Ninety", + "One Hundred and Ninety One", + "One Hundred and Ninety Two", + "One Hundred and Ninety Three", + "One Hundred and Ninety Four", + "One Hundred and Ninety Five", + "One Hundred and Ninety Six", + "One Hundred and Ninety Seven", + "One Hundred and Ninety Eight", + "One Hundred and Ninety Nine", + "Two Hundred", ]; diff --git a/examples/the-dungeon-puzzlers-lament/src/game/simulation.rs b/examples/the-dungeon-puzzlers-lament/src/game/simulation.rs index ce42c8ec..997c332a 100644 --- a/examples/the-dungeon-puzzlers-lament/src/game/simulation.rs +++ b/examples/the-dungeon-puzzlers-lament/src/game/simulation.rs @@ -11,7 +11,7 @@ use crate::{ use self::{ animation::{Animation, RenderCache}, - entity::{Action, EntityMap}, + entity::{Action, EntityMap, EntityMapMaker}, }; mod animation; @@ -33,16 +33,21 @@ pub struct Simulation { impl Simulation { pub fn generate( - a: impl Iterator)>, + entities_to_add: impl Iterator)>, level: &'static Level, sfx: &mut Sfx, loader: &mut SpriteLoader, ) -> Simulation { - let mut entities = EntityMap::default(); + let mut entities = EntityMapMaker::new(); let mut animation = Animation::default(); - for (item, location) in a { - animation.populate(entities.add(item, location), sfx); + for (item, location) in entities_to_add { + entities.add(item, location); + } + + let (entities, animations) = entities.make_entity_map(); + for ani in animations { + animation.populate(ani, sfx); } let mut simulation = Simulation { diff --git a/examples/the-dungeon-puzzlers-lament/src/game/simulation/animation.rs b/examples/the-dungeon-puzzlers-lament/src/game/simulation/animation.rs index b4a2b4a0..e6df07e6 100644 --- a/examples/the-dungeon-puzzlers-lament/src/game/simulation/animation.rs +++ b/examples/the-dungeon-puzzlers-lament/src/game/simulation/animation.rs @@ -8,6 +8,7 @@ use agb::{ display::object::{OamIterator, ObjectUnmanaged, SpriteLoader}, fixnum::{Num, Vector2D}, }; +use alloc::vec; use alloc::vec::Vec; use slotmap::SecondaryMap; @@ -26,11 +27,15 @@ struct AnimationEntity { attached: Option<(Item, Num)>, } +struct MovePoints { + sound_effect: Option, + points: Vec>>, +} + #[derive(Default)] struct ToPlay { - moves: Vec, + move_points: SecondaryMap, attach_progress: Vec, - fakeout: Vec, detatch: Vec, attach: Vec, change: Vec, @@ -50,9 +55,45 @@ impl ToPlay { ) { match instruction { AnimationInstruction::Move(e, p, s) => { - self.moves.push(Move(e, convert_to_real_space(p), s)); + let move_points = + self.move_points + .entry(e) + .unwrap() + .or_insert_with(|| MovePoints { + sound_effect: s, + points: map + .get(e) + .map(|x| vec![x.start_position]) + .unwrap_or_default(), + }); + move_points.points.push(convert_to_real_space(p)); + if let Some(sound_effect) = s { + move_points.sound_effect.get_or_insert(sound_effect); + } + } + AnimationInstruction::FakeOutMove(e, d, s) => { + let move_points = + self.move_points + .entry(e) + .unwrap() + .or_insert_with(|| MovePoints { + sound_effect: s, + points: map + .get(e) + .map(|x| vec![x.start_position]) + .unwrap_or_default(), + }); + + if let Some(sound_effect) = s { + move_points.sound_effect.get_or_insert(sound_effect); + } + + let &most_recent_position = move_points.points.last().unwrap(); + move_points + .points + .push(most_recent_position + convert_to_real_space(d.into()) / 2); + move_points.points.push(most_recent_position); } - AnimationInstruction::FakeOutMove(e, d, s) => self.fakeout.push(FakeOutMove(e, d, s)), AnimationInstruction::Detatch(e, nk, s) => self.detatch.push(Detatch(e, nk, s)), AnimationInstruction::Attach(e, o, s) => { if let Some(entity_to_attach) = map.get(o) { @@ -118,7 +159,13 @@ impl RenderCache { let mut score = 0; if matches!( self.item, - Item::Stairs | Item::Switch | Item::SwitchPressed | Item::SpikesDown | Item::SpikesUp + Item::Stairs + | Item::Switch + | Item::SwitchPressed + | Item::SpikesDown + | Item::SpikesUp + | Item::Ice + | Item::Teleporter ) { score += 100000; } @@ -148,12 +195,6 @@ impl Map { entity.start_position = destination; } } - - fn set_entity_to_start_location(&mut self, entity: EntityKey) { - if let Some(entity) = self.map.get_mut(entity) { - entity.rendered_position = entity.start_position; - } - } } impl Deref for Map { @@ -170,6 +211,31 @@ impl DerefMut for Map { } } +fn lerp_points, Output = N> + core::ops::Add>( + points: &[N], + t: Num, +) -> N { + let number_of_points = points.len() as i32; + let slope_for_spike_fn = number_of_points - 1; + + let relevant_points_pair_idx = (t * slope_for_spike_fn).floor(); + + let spike_function_for_first = t * -slope_for_spike_fn + relevant_points_pair_idx + 1; + let spike_function_for_second = t * slope_for_spike_fn - relevant_points_pair_idx; + let first_point_idx = relevant_points_pair_idx as usize; + + let &first = points + .get(first_point_idx) + .expect("Maybe input to lerp is out of range?"); + let second = points.get(first_point_idx + 1); + + if let Some(&second) = second { + first * spike_function_for_first + second * spike_function_for_second + } else { + first + } +} + impl Animation { pub fn populate(&mut self, instruction: AnimationInstruction, sfx: &mut Sfx) { self.to_play.populate(instruction, &mut self.map, sfx); @@ -234,25 +300,16 @@ impl Animation { } pub fn update(&mut self, sfx: &mut Sfx) -> bool { - if !self.to_play.moves.is_empty() - || !self.to_play.fakeout.is_empty() - || !self.to_play.attach_progress.is_empty() - { + if !self.to_play.move_points.is_empty() || !self.to_play.attach_progress.is_empty() { if self.time >= 1.into() { // finalise animations - for m in self.to_play.moves.drain(0..) { + for m in self.to_play.move_points.drain() { let entity = m.0; - let destination = m.1; + let &destination = m.1.points.last().unwrap(); self.map.set_entity_start_location(entity, destination); } - for m in self.to_play.fakeout.drain(0..) { - let entity = m.0; - - self.map.set_entity_to_start_location(entity); - } - for m in self.to_play.attach_progress.drain(0..) { if let Some(ease) = self .map @@ -265,34 +322,11 @@ impl Animation { } } else { // play moves and fakeouts - for m in self.to_play.moves.iter_mut() { - let entity = m.0; - let destination = m.1; - - sfx.play_sound_effect(m.2.take()); + for (entity, move_points) in self.to_play.move_points.iter_mut() { + sfx.play_sound_effect(move_points.sound_effect.take()); if let Some(entity) = self.map.get_mut(entity) { - let location = entity.start_position * (Num::::new(1) - self.ease) - + destination * self.ease; - - entity.rendered_position = location; - } - } - - for m in self.to_play.fakeout.iter_mut() { - let entity = m.0; - let direction = m.1; - let direction = convert_to_real_space(direction.into()); - - sfx.play_sound_effect(m.2.take()); - - let go_to = direction / 2; - - let start = (self.ease * 2 - 1).abs(); - let end_multiplier = -start + 1; - - if let Some(entity) = self.map.get_mut(entity) { - let location = entity.start_position + go_to * end_multiplier; + let location = lerp_points(&move_points.points, self.ease); entity.rendered_position = location; } @@ -334,9 +368,13 @@ impl Animation { attached: None, }, ); - self.to_play - .moves - .push(Move(new_key, destination_position, None)); + self.to_play.move_points.insert( + new_key, + MovePoints { + sound_effect: None, + points: vec![position, destination_position], + }, + ); } } } else if !self.to_play.attach.is_empty() { @@ -385,8 +423,6 @@ impl Animation { } } -struct Move(EntityKey, Vector2D>, Option); -struct FakeOutMove(EntityKey, Direction, Option); struct Detatch(EntityKey, EntityKey, Option); struct Attach(EntityKey, Item, EntityKey, Option); struct AttachProgress(EntityKey, Option); diff --git a/examples/the-dungeon-puzzlers-lament/src/game/simulation/entity.rs b/examples/the-dungeon-puzzlers-lament/src/game/simulation/entity.rs index 95a1474b..c8248890 100644 --- a/examples/the-dungeon-puzzlers-lament/src/game/simulation/entity.rs +++ b/examples/the-dungeon-puzzlers-lament/src/game/simulation/entity.rs @@ -18,11 +18,39 @@ use super::animation::AnimationInstruction; new_key_type! { pub struct EntityKey; } -#[derive(Default)] pub struct EntityMap { map: SlotMap, } +pub struct EntityMapMaker { + map: Vec<(crate::level::Item, Vector2D)>, +} + +impl EntityMapMaker { + pub fn new() -> Self { + Self { + map: Default::default(), + } + } + + pub fn add(&mut self, entity: crate::level::Item, location: Vector2D) { + self.map.push((entity, location)); + } + + pub fn make_entity_map(mut self) -> (EntityMap, Vec) { + self.map + .sort_unstable_by_key(|(_, location)| location.x + location.y * 100); + let mut entity_map = EntityMap { + map: Default::default(), + }; + let mut animations = Vec::new(); + for (entity, location) in self.map { + animations.push(entity_map.add(entity, location)); + } + (entity_map, animations) + } +} + #[derive(Clone, Copy, PartialEq, PartialOrd, Debug)] pub enum Outcome { Continue, @@ -30,6 +58,23 @@ pub enum Outcome { Win, } +struct ActionResult { + hero_has_died: bool, + win_has_triggered: bool, +} + +impl ActionResult { + fn new(hero_has_died: bool, win_has_triggered: bool) -> Self { + Self { + hero_has_died, + win_has_triggered, + } + } +} + +struct HasMoved(bool); +struct WantsToMoveAgain(bool); + impl EntityMap { fn whats_at(&self, location: Vector2D) -> impl Iterator { self.map @@ -85,198 +130,392 @@ impl EntityMap { AnimationInstruction::Add(idx, entity, location, None) } + // allow because while it's a lot of arguments, it's not confusing because they are all of different types + #[allow(clippy::too_many_arguments)] + fn attempt_move_in_direction( + &mut self, + map: &Map, + animations: &mut Vec, + entity_to_update_key: EntityKey, + direction: Direction, + can_turn_around: bool, + push_depth: i32, + entities_that_have_moved: &mut Vec<(EntityKey, Direction)>, + ) -> (HasMoved, ActionResult) { + let mut hero_has_died = false; + let mut win_has_triggered = false; + + let Some(entity_to_update) = self.map.get(entity_to_update_key) else { + return ( + HasMoved(false), + ActionResult::new(hero_has_died, win_has_triggered), + ); + }; + + let entity_location = entity_to_update.location; + + let desired_location = entity_location + direction.into(); + let surface = map.get(desired_location); + + let mut should_die_later = false; + + let (can_move, explicit_stay_put, fake_out_effect) = if surface == MapElement::Wall { + (false, true, None) + } else { + let mut can_move = true; + let mut explicit_stay_put = false; + let mut fake_out_effect = None; + + let move_attempt_resolutions: Vec<_> = self + .whats_at(desired_location) + .filter(|(k, _)| *k != entity_to_update_key) + .map(|(key, other_entity)| { + (key, resolve_move(entity_to_update, other_entity, direction)) + }) + .collect(); + + for (other_entity_key, move_resolution) in move_attempt_resolutions { + match move_resolution { + MoveAttemptResolution::KillDie => { + hero_has_died |= self.kill_entity(other_entity_key, animations); + hero_has_died |= self.kill_entity(entity_to_update_key, animations); + can_move = false; + } + MoveAttemptResolution::Kill => { + hero_has_died |= self.kill_entity(other_entity_key, animations); + fake_out_effect = self + .map + .get(entity_to_update_key) + .and_then(|x| x.kill_sound_effect()); + can_move = false; + } + MoveAttemptResolution::Die => { + hero_has_died |= self.kill_entity(entity_to_update_key, animations); + can_move = false; + } + MoveAttemptResolution::CoExist => {} + MoveAttemptResolution::StayPut => { + can_move = false; + explicit_stay_put = true; + } + MoveAttemptResolution::AttemptPush => { + let depth = push_depth - 1; + if depth >= 0 { + let (can_move_result, action_result) = self.attempt_move_in_direction( + map, + animations, + other_entity_key, + direction, + true, + depth, + entities_that_have_moved, + ); + + if !can_move_result.0 { + can_move = false; + explicit_stay_put = true; + } + hero_has_died |= action_result.hero_has_died; + win_has_triggered |= action_result.win_has_triggered; + } else { + can_move = false; + explicit_stay_put = true; + } + } + MoveAttemptResolution::DieLater => { + should_die_later = true; + } + } + } + + (can_move, explicit_stay_put, fake_out_effect) + }; + + if can_move { + if let Some(e) = self.map.get_mut(entity_to_update_key) { + e.location = desired_location; + } + entities_that_have_moved.push((entity_to_update_key, direction)); + + let Some(entity_to_update) = self.map.get(entity_to_update_key) else { + return ( + HasMoved(can_move), + ActionResult::new(hero_has_died, win_has_triggered), + ); + }; + + let move_effect = entity_to_update.move_effect(); + + animations.push(AnimationInstruction::Move( + entity_to_update_key, + desired_location, + move_effect, + )); + } else if !should_die_later + && explicit_stay_put + && can_turn_around + && self.map.get(entity_to_update_key).map(|e| e.turns_around()) == Some(true) + { + if let Some(directions_to_attempt) = self + .map + .get(entity_to_update_key) + .and_then(|e| e.directions_to_attempt()) + { + #[allow(clippy::indexing_slicing)] + for &direction_to_attempt in directions_to_attempt { + let (can_move, action) = self.attempt_move_in_direction( + map, + animations, + entity_to_update_key, + direction_to_attempt, + false, + push_depth, + entities_that_have_moved, + ); + + if can_move.0 { + if let Some((Some(change), change_effect)) = self + .map + .get_mut(entity_to_update_key) + .map(|e| (e.change_direction(direction_to_attempt), e.change_effect())) + { + animations.push(AnimationInstruction::PriorityChange( + entity_to_update_key, + change, + change_effect, + )); + } + + return ( + can_move, + ActionResult::new( + hero_has_died || action.hero_has_died, + win_has_triggered || action.win_has_triggered, + ), + ); + } + } + + let last_direction_attempt = *directions_to_attempt.last().unwrap(); + + animations.push(AnimationInstruction::FakeOutMove( + entity_to_update_key, + last_direction_attempt, + self.map + .get(entity_to_update_key) + .and_then(|e| e.fake_out_wall_effect()), + )); + + if let Some((Some(change), change_effect)) = + self.map.get_mut(entity_to_update_key).map(|e| { + ( + e.change_direction(last_direction_attempt), + e.change_effect(), + ) + }) + { + animations.push(AnimationInstruction::PriorityChange( + entity_to_update_key, + change, + change_effect, + )); + } + + return ( + HasMoved(false), + ActionResult::new(hero_has_died, win_has_triggered), + ); + } + } else if can_turn_around { + animations.push(AnimationInstruction::FakeOutMove( + entity_to_update_key, + direction, + if explicit_stay_put { + self.map + .get(entity_to_update_key) + .and_then(|e| e.fake_out_wall_effect()) + } else { + fake_out_effect.or_else(|| { + self.map + .get(entity_to_update_key) + .and_then(|e| e.fake_out_effect()) + }) + }, + )); + } + + if should_die_later { + hero_has_died |= self.kill_entity(entity_to_update_key, animations); + } + + ( + HasMoved(can_move), + ActionResult::new(hero_has_died, win_has_triggered), + ) + } + + fn resolve_overlap_from_move( + &mut self, + animations: &mut Vec, + entity_to_update_key: EntityKey, + ) -> (WantsToMoveAgain, ActionResult) { + let mut win_has_triggered = false; + let mut hero_has_died = false; + let mut should_move_again = false; + + let Some(entity_to_update) = self.map.get(entity_to_update_key) else { + return ( + WantsToMoveAgain(should_move_again), + ActionResult::new(hero_has_died, win_has_triggered), + ); + }; + + let location = entity_to_update.location; + + let overlap_resolutions: Vec<_> = self + .whats_at(location) + .filter(|(k, _)| *k != entity_to_update_key) + .map(|(key, other_entity)| (key, resolve_overlap(entity_to_update, other_entity))) + .collect(); + + for (other_entity_key, move_resolution) in overlap_resolutions { + match move_resolution { + OverlapResolution::Pickup => { + animations.push(AnimationInstruction::Attach( + entity_to_update_key, + other_entity_key, + self.map + .get(other_entity_key) + .and_then(|x| x.pickup_sound_effect()), + )); + let other = self.map.remove(other_entity_key).unwrap(); + + if let Some((location, dropped)) = self + .map + .get_mut(entity_to_update_key) + .and_then(|x| x.pickup(other.entity).map(|y| (x.location, y))) + { + let new_key = self.map.insert(Entity { + location, + entity: dropped, + }); + + animations.push(AnimationInstruction::Detatch( + entity_to_update_key, + new_key, + self.map.get(new_key).and_then(|x| x.drop_effect()), + )); + } + } + OverlapResolution::CoExist => {} + OverlapResolution::Win => { + win_has_triggered = true; + } + OverlapResolution::ToggleSystem(system) => { + for (k, e) in self.map.iter_mut() { + if let Some(change) = e.switch(system) { + animations.push(AnimationInstruction::Change( + k, + change, + e.change_effect(), + )); + } + } + } + OverlapResolution::Die => { + hero_has_died |= self.kill_entity(entity_to_update_key, animations); + break; + } + OverlapResolution::KillDie => { + hero_has_died |= self.kill_entity(other_entity_key, animations); + hero_has_died |= self.kill_entity(entity_to_update_key, animations); + break; + } + OverlapResolution::MoveAgain => { + should_move_again = true; + } + OverlapResolution::Teleport => { + // find other teleporter + let other_teleporter = self.map.iter().find(|(entity_key, entity)| { + *entity_key != other_entity_key + && matches!(entity.entity, EntityType::Teleporter) + }); + + if let Some((_other_teleporter_key, other_teleporter)) = other_teleporter { + let location_to_teleport_to = other_teleporter.location; + if self.whats_at(location_to_teleport_to).count() == 1 { + //ok, we can teleport + animations.push(AnimationInstruction::Move( + entity_to_update_key, + location_to_teleport_to, + Some(SoundEffect::TeleportEffect), + )); + if let Some(entity) = self.map.get_mut(entity_to_update_key) { + entity.location = location_to_teleport_to; + } + } + } + } + } + } + ( + WantsToMoveAgain(should_move_again), + ActionResult::new(hero_has_died, win_has_triggered), + ) + } + pub fn tick(&mut self, map: &Map, hero: Action) -> (Outcome, Vec) { let mut hero_has_died = false; let mut win_has_triggered = false; let mut animations = Vec::new(); - let desired_actions: Vec<(EntityKey, Action)> = self + let mut entities_to_try_update = self .map .iter() - .map(|(key, entity)| (key, entity.desired_action(map, self, hero))) - .collect(); + .map(|(key, entity)| (key, entity.desired_action(hero))) + .filter_map(|(key, action)| match action { + Action::Nothing => None, + Action::Direction(direction) => Some((key, direction)), + }) + .collect::>(); - for (entity_key, action) in desired_actions { - if !self.map.contains_key(entity_key) { - continue; + let mut first_loop = true; + + while !entities_to_try_update.is_empty() { + let mut entities_that_have_moved = Vec::new(); + + for (entity_to_update_key, direction) in entities_to_try_update.drain(..) { + let (_, action_result) = self.attempt_move_in_direction( + map, + &mut animations, + entity_to_update_key, + direction, + first_loop, + self.map + .get(entity_to_update_key) + .and_then(|e| e.push_depth()) + .unwrap_or(0), + &mut entities_that_have_moved, + ); + + hero_has_died |= action_result.hero_has_died; + win_has_triggered |= action_result.win_has_triggered; } - match action { - Action::Nothing => { - // nothing does nothing and causes nothing to happen + + for (entity_to_update_key, direction) in entities_that_have_moved { + let (should_move_again, action_result) = + self.resolve_overlap_from_move(&mut animations, entity_to_update_key); + + if should_move_again.0 { + entities_to_try_update.push((entity_to_update_key, direction)); } - Action::Direction(direction) | Action::ChangeDirection(direction) => { - if matches!(action, Action::ChangeDirection(_)) { - if let Some(change) = self - .map - .get_mut(entity_key) - .and_then(|e| e.change_direction()) - { - animations.push(AnimationInstruction::PriorityChange( - entity_key, - change, - self.map.get(entity_key).and_then(|e| e.change_effect()), - )); - } - } - - let Some(entity) = &self.map.get(entity_key) else { - continue; - }; - - let desired_location = entity.location + direction.into(); - let surface = map.get(desired_location); - if surface == MapElement::Wall { - let wall_resolution = resolve_wall_move(&entity.entity); - match wall_resolution { - WallResolution::StayPut => { - animations.push(AnimationInstruction::FakeOutMove( - entity_key, - direction, - entity.fake_out_wall_effect(), - )); - } - } - } else { - // what is at that location - let resolutions: Vec<_> = self - .whats_at(desired_location) - .filter(|(k, _)| *k != entity_key) - .map(|(key, other_entity)| (key, resolve_move(entity, other_entity))) - .collect(); - - let mut can_move = true; - let mut explicit_stay_put = false; - let mut fake_out_effect = None; - - for (other, resolution) in resolutions { - match resolution { - MoveAttemptResolution::KillDie => { - hero_has_died |= self.kill_entity(other, &mut animations); - hero_has_died |= self.kill_entity(entity_key, &mut animations); - can_move = false; - } - MoveAttemptResolution::Kill => { - hero_has_died |= self.kill_entity(other, &mut animations); - fake_out_effect = self - .map - .get(entity_key) - .and_then(|x| x.kill_sound_effect()); - can_move = false; - } - MoveAttemptResolution::Die => { - hero_has_died |= self.kill_entity(entity_key, &mut animations); - can_move = false; - } - MoveAttemptResolution::CoExist => {} - MoveAttemptResolution::StayPut => { - can_move = false; - explicit_stay_put = true; - } - } - } - - if can_move { - if let Some(e) = self.map.get_mut(entity_key) { - e.location = desired_location; - } - let Some(entity) = &self.map.get(entity_key) else { - continue; - }; - - animations.push(AnimationInstruction::Move( - entity_key, - desired_location, - entity.move_effect(), - )); - - let overlap_resolutions: Vec<_> = self - .whats_at(desired_location) - .filter(|(k, _)| *k != entity_key) - .map(|(key, other_entity)| { - (key, resolve_overlap(entity, other_entity)) - }) - .collect(); - - if overlap_resolutions - .iter() - .filter(|(_, r)| *r == OverlapResolution::Die) - .count() - != 0 - { - hero_has_died |= self.kill_entity(entity_key, &mut animations); - } else { - for (other, resolution) in overlap_resolutions { - match resolution { - OverlapResolution::Pickup => { - animations.push(AnimationInstruction::Attach( - entity_key, - other, - self.map - .get(other) - .and_then(|x| x.pickup_sound_effect()), - )); - let other = self.map.remove(other).unwrap(); - - if let Some((location, dropped)) = - self.map.get_mut(entity_key).and_then(|x| { - x.pickup(other.entity).map(|y| (x.location, y)) - }) - { - let new_key = self.map.insert(Entity { - location, - entity: dropped, - }); - - animations.push(AnimationInstruction::Detatch( - entity_key, - new_key, - self.map - .get(new_key) - .and_then(|x| x.drop_effect()), - )); - } - } - OverlapResolution::CoExist => {} - OverlapResolution::Win => { - win_has_triggered = true; - } - OverlapResolution::ToggleSystem(system) => { - for (k, e) in self.map.iter_mut() { - if let Some(change) = e.switch(system) { - animations.push(AnimationInstruction::Change( - k, - change, - e.change_effect(), - )); - } - } - } - OverlapResolution::Die => { - // already handled - } - } - } - } - } else { - animations.push(AnimationInstruction::FakeOutMove( - entity_key, - direction, - if explicit_stay_put { - self.map - .get(entity_key) - .and_then(|e| e.fake_out_wall_effect()) - } else { - fake_out_effect.or_else(|| { - self.map.get(entity_key).and_then(|e| e.fake_out_effect()) - }) - }, - )); - } - } - } + hero_has_died |= action_result.hero_has_died; + win_has_triggered |= action_result.win_has_triggered; } + + first_loop = false; } ( @@ -297,23 +536,24 @@ enum MoveAttemptResolution { Die, KillDie, CoExist, + DieLater, StayPut, + AttemptPush, } #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)] pub struct SwitchSystem(usize); -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)] +#[derive(Debug, PartialEq, Eq, Clone, Copy)] enum OverlapResolution { Pickup, CoExist, Win, ToggleSystem(SwitchSystem), Die, -} - -enum WallResolution { - StayPut, + KillDie, + MoveAgain, + Teleport, } fn resolve_spikes(switable: &Switchable) -> OverlapResolution { @@ -327,31 +567,41 @@ fn resolve_spikes(switable: &Switchable) -> OverlapResolution { fn resolve_overlap(me: &Entity, other: &Entity) -> OverlapResolution { match (&me.entity, &other.entity) { (EntityType::Hero(_), EntityType::Stairs) => OverlapResolution::Win, - (_, EntityType::Item(_)) => OverlapResolution::Pickup, + (EntityType::Hero(_) | EntityType::Enemy(Enemy::Moving(_)), EntityType::Item(_)) => { + OverlapResolution::Pickup + } + (EntityType::MovableBlock, EntityType::Spikes(_)) => OverlapResolution::CoExist, (_, EntityType::Spikes(switch)) => resolve_spikes(switch), (_, EntityType::Switch(switch)) => OverlapResolution::ToggleSystem(switch.system), - (_, EntityType::Enemy(_) | EntityType::Hero(_)) => OverlapResolution::Die, + (_, EntityType::Enemy(_)) => OverlapResolution::Die, + (_, EntityType::Ice) => OverlapResolution::MoveAgain, + (_, EntityType::Teleporter) => OverlapResolution::Teleport, + (EntityType::MovableBlock, EntityType::Hole) => OverlapResolution::KillDie, + (_, EntityType::Hole) => OverlapResolution::Die, _ => OverlapResolution::CoExist, } } -fn resolve_wall_move(_entity: &EntityType) -> WallResolution { - WallResolution::StayPut -} - -fn holding_attack_resolve(holding: Option<&EntityType>) -> MoveAttemptResolution { - match holding { - Some(&EntityType::Item(Item::Sword)) => MoveAttemptResolution::Kill, +fn holding_attack_resolve( + holding: Option<&EntityType>, + other: &Entity, + direction: Direction, +) -> MoveAttemptResolution { + match (holding, &other.entity) { + (Some(&EntityType::Item(Item::Sword)), _) => MoveAttemptResolution::Kill, + (_, EntityType::Enemy(Enemy::Moving(squid))) => { + hero_walk_into_squid_interaction(squid, direction) + } _ => MoveAttemptResolution::CoExist, } } -fn squid_holding_attack_resolve(me: &Squid, other: &Entity) -> MoveAttemptResolution { +fn squid_holding_attack_resolve(me: &Moving, other: &Entity) -> MoveAttemptResolution { match (me.holding.as_deref(), &other.entity, other.holding()) { ( Some(&EntityType::Item(Item::Sword)), - EntityType::Enemy(Enemy::Squid(squid)), + EntityType::Enemy(Enemy::Moving(squid)), Some(&EntityType::Item(Item::Sword)), ) => { if squid.direction == -me.direction { @@ -363,7 +613,7 @@ fn squid_holding_attack_resolve(me: &Squid, other: &Entity) -> MoveAttemptResolu (Some(&EntityType::Item(Item::Sword)), EntityType::Enemy(_), None) => { MoveAttemptResolution::Kill } - (_, EntityType::Enemy(Enemy::Squid(squid)), Some(&EntityType::Item(Item::Sword))) => { + (_, EntityType::Enemy(Enemy::Moving(squid)), Some(&EntityType::Item(Item::Sword))) => { if squid.direction == -me.direction { MoveAttemptResolution::Die } else { @@ -391,23 +641,35 @@ fn switch_door_resolve(door: &Switchable) -> MoveAttemptResolution { } } -fn resolve_move(mover: &Entity, into: &Entity) -> MoveAttemptResolution { +fn hero_walk_into_squid_interaction(squid: &Moving, direction: Direction) -> MoveAttemptResolution { + if direction == -squid.direction { + MoveAttemptResolution::DieLater + } else { + MoveAttemptResolution::CoExist + } +} + +fn resolve_move(mover: &Entity, into: &Entity, direction: Direction) -> MoveAttemptResolution { match (&mover.entity, &into.entity) { (EntityType::Hero(hero), EntityType::Hero(_) | EntityType::Enemy(_)) => { - holding_attack_resolve(hero.holding.as_deref()) + holding_attack_resolve(hero.holding.as_deref(), into, direction) } (EntityType::Hero(hero), EntityType::Door) => holding_door_resolve(hero.holding.as_deref()), - (EntityType::Enemy(Enemy::Squid(squid)), EntityType::Hero(_) | EntityType::Enemy(_)) => { + (EntityType::Enemy(Enemy::Moving(squid)), EntityType::Hero(_) | EntityType::Enemy(_)) => { squid_holding_attack_resolve(squid, into) } (EntityType::Enemy(_), EntityType::Hero(_) | EntityType::Enemy(_)) => { MoveAttemptResolution::Kill } (_, EntityType::SwitchedDoor(door)) => switch_door_resolve(door), - (EntityType::Enemy(Enemy::Squid(squid)), EntityType::Door) => { + (EntityType::Enemy(Enemy::Moving(squid)), EntityType::Door) => { holding_door_resolve(squid.holding.as_deref()) } (_, EntityType::Door) => MoveAttemptResolution::StayPut, + (_, EntityType::MovableBlock) => MoveAttemptResolution::AttemptPush, + (EntityType::MovableBlock, EntityType::Hero(_) | EntityType::Enemy(_)) => { + MoveAttemptResolution::StayPut + } (_, _) => MoveAttemptResolution::CoExist, } } @@ -435,27 +697,39 @@ pub enum EntityType { Enemy(Enemy), Stairs, Door, + Hole, SwitchedDoor(Switchable), Switch(Switchable), Spikes(Switchable), + Ice, + MovableBlock, + Teleporter, } #[derive(Debug)] -pub struct Squid { +pub struct Moving { direction: Direction, holding: Option>, + movable_enemy_type: MovableEnemyType, +} + +#[derive(Debug, PartialEq, Eq)] +enum MovableEnemyType { + Squid, + Rotator, } #[derive(Debug)] pub enum Enemy { Slime, - Squid(Squid), + Moving(Moving), } #[derive(Debug)] pub enum Item { Sword, Key, + Glove, } #[derive(PartialEq, Eq, Clone, Copy, Debug)] @@ -500,42 +774,25 @@ impl From<&Direction> for Vector2D { pub enum Action { Nothing, Direction(Direction), - ChangeDirection(Direction), } impl Entity { - fn desired_action(&self, walls: &Map, entities: &EntityMap, hero_action: Action) -> Action { + fn desired_action(&self, hero_action: Action) -> Action { match &self.entity { EntityType::Hero(_) => hero_action, - EntityType::Enemy(Enemy::Squid(squid)) => { - let desired_location = self.location + squid.direction.into(); - let wall = walls.get(desired_location); - - if matches!(wall, MapElement::Wall) { - Action::ChangeDirection(-squid.direction) - } else { - let can_move = entities - .whats_at(desired_location) - .map(|(_, other_entity)| resolve_move(self, other_entity)) - .filter(|resolution| matches!(resolution, MoveAttemptResolution::StayPut)) - .count() - == 0; - - if can_move { - Action::Direction(squid.direction) - } else { - Action::ChangeDirection(-squid.direction) - } - } - } + EntityType::Enemy(Enemy::Moving(squid)) => Action::Direction(squid.direction), _ => Action::Nothing, } } + fn turns_around(&self) -> bool { + matches!(self.entity, EntityType::Enemy(Enemy::Moving(_))) + } + fn pickup(&mut self, item: EntityType) -> Option { let holding = match &mut self.entity { EntityType::Hero(hero) => &mut hero.holding, - EntityType::Enemy(Enemy::Squid(squid)) => &mut squid.holding, + EntityType::Enemy(Enemy::Moving(squid)) => &mut squid.holding, _ => panic!("this entity can't pick up things"), }; @@ -546,15 +803,28 @@ impl Entity { fn take_holding(&mut self) -> Option { match &mut self.entity { EntityType::Hero(hero) => hero.holding.take().map(|x| *x), - EntityType::Enemy(Enemy::Squid(squid)) => squid.holding.take().map(|x| *x), + EntityType::Enemy(Enemy::Moving(squid)) => squid.holding.take().map(|x| *x), _ => None, } } + fn push_depth(&self) -> Option { + if matches!(self.holding(), Some(&EntityType::Item(Item::Glove))) { + Some(i32::MAX) + } else if matches!( + self.entity, + EntityType::Hero(_) | EntityType::Enemy(Enemy::Moving(_)) + ) { + Some(1) + } else { + None + } + } + fn holding(&self) -> Option<&EntityType> { match &self.entity { EntityType::Hero(hero) => hero.holding.as_deref(), - EntityType::Enemy(Enemy::Squid(squid)) => squid.holding.as_deref(), + EntityType::Enemy(Enemy::Moving(squid)) => squid.holding.as_deref(), _ => None, } } @@ -564,7 +834,11 @@ impl Entity { EntityType::Hero(_) => Some(SoundEffect::HeroDie), EntityType::Door => Some(SoundEffect::DoorOpen), EntityType::Enemy(Enemy::Slime) => Some(SoundEffect::SlimeDie), - EntityType::Enemy(Enemy::Squid(_)) => Some(SoundEffect::SquidDie), + EntityType::Enemy(Enemy::Moving(e)) + if e.movable_enemy_type == MovableEnemyType::Squid => + { + Some(SoundEffect::SquidDie) + } _ => None, } } @@ -616,15 +890,46 @@ impl Entity { None } - fn change_direction(&mut self) -> Option { - match &mut self.entity { - EntityType::Enemy(Enemy::Squid(squid)) => { - squid.direction = -squid.direction; + fn directions_to_attempt(&self) -> Option<&'static [Direction]> { + match &self.entity { + EntityType::Enemy(Enemy::Moving(moving_type)) => { + Some(match moving_type.movable_enemy_type { + MovableEnemyType::Squid => match moving_type.direction { + Direction::Up => &[Direction::Down], + Direction::Down => &[Direction::Up], + _ => panic!("Left and right movements are not valid for a squid"), + }, + MovableEnemyType::Rotator => match moving_type.direction { + Direction::Up => &[Direction::Right, Direction::Left, Direction::Down], + Direction::Down => &[Direction::Left, Direction::Right, Direction::Up], + Direction::Left => &[Direction::Up, Direction::Down, Direction::Right], + Direction::Right => &[Direction::Down, Direction::Up, Direction::Left], + }, + }) + } + _ => None, + } + } - if squid.direction == Direction::Up { - Some(level::Item::SquidUp) - } else { - Some(level::Item::SquidDown) + fn change_direction(&mut self, direction: Direction) -> Option { + match &mut self.entity { + EntityType::Enemy(Enemy::Moving(moving)) => { + moving.direction = direction; + + match moving.movable_enemy_type { + MovableEnemyType::Squid => { + if direction == Direction::Up { + Some(level::Item::SquidUp) + } else { + Some(level::Item::SquidDown) + } + } + MovableEnemyType::Rotator => Some(match direction { + Direction::Up => level::Item::RotatorUp, + Direction::Down => level::Item::RotatorDown, + Direction::Left => level::Item::RotatorLeft, + Direction::Right => level::Item::RotatorRight, + }), } } _ => None, @@ -711,14 +1016,159 @@ impl From for EntityType { system: SwitchSystem(0), active: false, }), - level::Item::SquidUp => EntityType::Enemy(Enemy::Squid(Squid { + level::Item::SquidUp => EntityType::Enemy(Enemy::Moving(Moving { direction: Direction::Up, holding: None, + movable_enemy_type: MovableEnemyType::Squid, })), - level::Item::SquidDown => EntityType::Enemy(Enemy::Squid(Squid { + level::Item::SquidDown => EntityType::Enemy(Enemy::Moving(Moving { direction: Direction::Down, holding: None, + movable_enemy_type: MovableEnemyType::Squid, + })), + level::Item::Ice => EntityType::Ice, + level::Item::MovableBlock => EntityType::MovableBlock, + level::Item::Glove => EntityType::Item(Item::Glove), + level::Item::Teleporter => EntityType::Teleporter, + level::Item::Hole => EntityType::Hole, + level::Item::RotatorRight => EntityType::Enemy(Enemy::Moving(Moving { + direction: Direction::Right, + holding: None, + movable_enemy_type: MovableEnemyType::Rotator, + })), + level::Item::RotatorLeft => EntityType::Enemy(Enemy::Moving(Moving { + direction: Direction::Left, + holding: None, + movable_enemy_type: MovableEnemyType::Rotator, + })), + level::Item::RotatorUp => EntityType::Enemy(Enemy::Moving(Moving { + direction: Direction::Up, + holding: None, + movable_enemy_type: MovableEnemyType::Rotator, + })), + level::Item::RotatorDown => EntityType::Enemy(Enemy::Moving(Moving { + direction: Direction::Down, + holding: None, + movable_enemy_type: MovableEnemyType::Rotator, })), } } } + +#[cfg(test)] +mod tests { + + use super::*; + use agb::hash_map::HashMap; + + #[test_case] + fn check_all_puzzle_solutions_work(_gba: &mut agb::Gba) { + let number_of_levels = crate::level::Level::num_levels(); + let mut failed_levels = Vec::new(); + + #[derive(Debug)] + #[allow(dead_code)] + struct MismatchCount { + given: i32, + used: i32, + } + + #[derive(Debug)] + enum CompleteSimulationResult { + Success, + ExplicitLoss, + InputSequenceOver, + MismatchedItems(HashMap), + } + + fn check_level_has_valid_items(level: usize) -> HashMap { + let level = crate::level::Level::get_level(level); + + let mut given_items = HashMap::new(); + + for &item in level.items.iter() { + *given_items.entry(item).or_insert(0) += 1; + } + + let mut solution_items = HashMap::new(); + + for entity in level.solution.iter() { + *solution_items.entry(entity.0).or_insert(0) += 1; + } + + let mut mismatched = HashMap::new(); + + for (&item, &count) in solution_items.iter() { + let given_count = given_items.get(&item).copied().unwrap_or(0); + if given_count < count { + mismatched.insert( + item, + MismatchCount { + given: given_count, + used: count, + }, + ); + } + } + + mismatched + } + + fn check_level_works(level: usize) -> CompleteSimulationResult { + let level = crate::level::Level::get_level(level); + + let mut simulator = EntityMapMaker::new(); + for entity in level.entities { + simulator.add(entity.0, entity.1); + } + for solution_entity in level.solution { + simulator.add(solution_entity.0, solution_entity.1); + } + + let (mut simulator, _) = simulator.make_entity_map(); + + for &direction in level.directions { + let (outcome, _) = simulator.tick(&level.map, Action::Direction(direction)); + match outcome { + Outcome::Continue => {} + Outcome::Loss => return CompleteSimulationResult::ExplicitLoss, + Outcome::Win => return CompleteSimulationResult::Success, + } + } + + CompleteSimulationResult::InputSequenceOver + } + + for level_idx in 0..number_of_levels { + let mismatched_items = check_level_has_valid_items(level_idx); + if !mismatched_items.is_empty() { + failed_levels.push(( + level_idx, + CompleteSimulationResult::MismatchedItems(mismatched_items), + )) + } + let outcome = check_level_works(level_idx); + match outcome { + CompleteSimulationResult::ExplicitLoss + | CompleteSimulationResult::InputSequenceOver => { + failed_levels.push((level_idx, outcome)) + } + _ => {} + } + } + + if !failed_levels.is_empty() { + agb::println!("Levels that failed were:"); + for (level, outcome) in failed_levels { + agb::println!( + "Level: {}, reason {:?}, lament: {}", + level, + outcome, + crate::level::Level::get_level(level).name + ); + } + + panic!("Level check failed"); + } + } +} diff --git a/examples/the-dungeon-puzzlers-lament/src/level.rs b/examples/the-dungeon-puzzlers-lament/src/level.rs index 3452f30a..ff779e0b 100644 --- a/examples/the-dungeon-puzzlers-lament/src/level.rs +++ b/examples/the-dungeon-puzzlers-lament/src/level.rs @@ -2,7 +2,7 @@ use agb::{display::object::Tag, fixnum::Vector2D}; use crate::{game::Direction, map::Map, resources}; -#[derive(Clone, Copy, PartialEq, Eq, Debug)] +#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)] pub enum Item { Sword, Slime, @@ -18,6 +18,15 @@ pub enum Item { SpikesDown, SquidUp, SquidDown, + Ice, + MovableBlock, + Glove, + Teleporter, + Hole, + RotatorRight, + RotatorLeft, + RotatorUp, + RotatorDown, } impl Item { @@ -37,6 +46,15 @@ impl Item { Item::SpikesDown => resources::SPIKES_OFF, Item::SquidUp => resources::SQUID_UP_SHADOW, Item::SquidDown => resources::SQUID_DOWN_SHADOW, + Item::Ice => resources::ICE, + Item::MovableBlock => resources::ROCK_SHADOW, + Item::Glove => resources::POW_GLOVE_SHADOW, + Item::Teleporter => resources::TELEPORTER_SHADOW, + Item::Hole => resources::HOLE, + Item::RotatorRight => resources::ROTATOR_RIGHT_SHADOW, + Item::RotatorLeft => resources::ROTATOR_LEFT_SHADOW, + Item::RotatorUp => resources::ROTATOR_UP_SHADOW, + Item::RotatorDown => resources::ROTATOR_DOWN_SHADOW, } } @@ -56,6 +74,15 @@ impl Item { Item::SpikesDown => resources::SPIKES_OFF, Item::SquidUp => resources::SQUID_UP, Item::SquidDown => resources::SQUID_DOWN, + Item::Ice => resources::ICE, + Item::MovableBlock => resources::ROCK, + Item::Glove => resources::POW_GLOVE, + Item::Teleporter => resources::TELEPORTER, + Item::Hole => resources::HOLE, + Item::RotatorRight => resources::ROTATOR_RIGHT, + Item::RotatorLeft => resources::ROTATOR_LEFT, + Item::RotatorUp => resources::ROTATOR_UP, + Item::RotatorDown => resources::ROTATOR_DOWN, } } @@ -78,6 +105,15 @@ impl Item { Item::SpikesDown => ZERO, Item::SquidUp => STANDARD, Item::SquidDown => STANDARD, + Item::Ice => ZERO, + Item::MovableBlock => ZERO, + Item::Glove => STANDARD, + Item::Teleporter => ZERO, + Item::Hole => ZERO, + Item::RotatorRight => STANDARD, + Item::RotatorLeft => STANDARD, + Item::RotatorUp => STANDARD, + Item::RotatorDown => STANDARD, } } } @@ -87,15 +123,19 @@ pub struct Entity(pub Item, pub Vector2D); pub struct Level { pub map: Map<'static>, pub entities: &'static [Entity], + #[cfg(test)] + pub solution: &'static [Entity], pub directions: &'static [Direction], pub items: &'static [Item], pub name: &'static str, } impl Level { + #[allow(unused_variables)] const fn new( map: Map<'static>, entities: &'static [Entity], + solution: &'static [Entity], directions: &'static [Direction], items: &'static [Item], name: &'static str, @@ -103,6 +143,8 @@ impl Level { Self { map, entities, + #[cfg(test)] + solution, directions, items, name, diff --git a/examples/the-dungeon-puzzlers-lament/src/map.rs b/examples/the-dungeon-puzzlers-lament/src/map.rs index c0d9c6a3..558d2779 100644 --- a/examples/the-dungeon-puzzlers-lament/src/map.rs +++ b/examples/the-dungeon-puzzlers-lament/src/map.rs @@ -22,7 +22,7 @@ impl<'map> Map<'map> { pub const fn get(&self, index: Vector2D) -> MapElement { let (x, y) = (index.x, index.y); - if x > self.width as i32 || y > self.height as i32 { + if x > self.width as i32 || x < 0 || y > self.height as i32 || y < 0 { MapElement::Wall } else { let position = x as usize + y as usize * self.width; diff --git a/examples/the-dungeon-puzzlers-lament/src/resources.rs b/examples/the-dungeon-puzzlers-lament/src/resources.rs index c4af40f7..a65b9961 100644 --- a/examples/the-dungeon-puzzlers-lament/src/resources.rs +++ b/examples/the-dungeon-puzzlers-lament/src/resources.rs @@ -49,6 +49,22 @@ named_tag!( SQUID_DOWN, SQUID_UP_SHADOW, SQUID_DOWN_SHADOW, + ICE, + ROCK, + ROCK_SHADOW, + POW_GLOVE, + POW_GLOVE_SHADOW, + TELEPORTER, + TELEPORTER_SHADOW, + HOLE, + ROTATOR_RIGHT, + ROTATOR_UP, + ROTATOR_LEFT, + ROTATOR_DOWN, + ROTATOR_RIGHT_SHADOW, + ROTATOR_UP_SHADOW, + ROTATOR_LEFT_SHADOW, + ROTATOR_DOWN_SHADOW, ] ); diff --git a/examples/the-dungeon-puzzlers-lament/src/sfx.rs b/examples/the-dungeon-puzzlers-lament/src/sfx.rs index f73d8e62..0bce34e8 100644 --- a/examples/the-dungeon-puzzlers-lament/src/sfx.rs +++ b/examples/the-dungeon-puzzlers-lament/src/sfx.rs @@ -75,6 +75,7 @@ impl<'a> Sfx<'a> { SoundEffect::SwordDrop => {} SoundEffect::SwitchedDoorToggle => {} SoundEffect::SpikesToggle => {} + SoundEffect::TeleportEffect => {} } } } @@ -101,4 +102,5 @@ pub enum SoundEffect { SwitchedDoorToggle, SpikesToggle, WallHit, + TeleportEffect, } diff --git a/justfile b/justfile index 1cbabf36..bfac29b9 100644 --- a/justfile +++ b/justfile @@ -68,7 +68,7 @@ check-linker-script-consistency: find -type f -name gba.ld -print0 | xargs -0 -n1 cmp -- agb/gba.ld find -type f -name gba_mb.ld -print0 | xargs -0 -n1 cmp -- agb/gba_mb.ld -ci: check-linker-script-consistency build-debug clippy fmt-check test miri build-release test-release doctest-agb build-roms build-book check-docs +ci: check-linker-script-consistency build-debug clippy fmt-check test miri build-release test-release doctest-agb test-games build-roms build-book check-docs build-roms: just _build-rom "examples/the-purple-night" "PURPLENIGHT" @@ -103,6 +103,12 @@ _run-tool +tool: (cd tools && cargo build) "$CARGO_TARGET_DIR/debug/tools" {{tool}} +test-games: + just test-game the-dungeon-puzzlers-lament + +test-game game: + (cd "examples/{{game}}" && CARGO_TARGET_THUMBV4T_NONE_EABI_RUNNER=mgba-test-runner cargo test) + _build-rom folder name: #!/usr/bin/env bash set -euxo pipefail