diff --git a/Cargo.toml b/Cargo.toml index 7c6132f..29bff58 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,11 @@ [package] name = "ddnet-map-gen" +authors = ["Edgar "] +description = "A DDraceNetwork map generator." version = "0.1.0" edition = "2021" +license = "AGPL-3-only" +keywords = ["ddnet", "teeworlds", "mapgen"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/LICENSE b/LICENSE index 0ad25db..2dedc5b 100644 --- a/LICENSE +++ b/LICENSE @@ -630,7 +630,7 @@ state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. - Copyright (C) + Copyright (C) 2022 Edgar Luque This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published diff --git a/src/cli.rs b/src/cli.rs index 918bc4e..d977bbb 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -5,12 +5,38 @@ use std::path::Path; pub fn run_cli() -> Result<()> { let matches = command!() - .about("A DDraceNetwork map generator.") + .about("A DDraceNetwork map generator") .arg_required_else_help(true) .subcommand_required(true) - .arg(arg!( "The output file").required(true)) - .subcommand(Command::new("maze").about("Generate a maze-like map.")) - .subcommand(Command::new("fly").about("Generate a map for fly techniques.")) + .arg(arg!( "The output map file").required(true)) + .subcommand( + Command::new("maze") + .about("Generate a maze-like map") + .arg( + arg!(--width "The width of the map") + .default_value("1000") + .required(false), + ) + .arg( + arg!(--height "The height of the map") + .default_value("1000") + .required(false), + ), + ) + .subcommand( + Command::new("fly") + .about("Generate a map for fly techniques") + .arg( + arg!(--width "The width of the map") + .default_value("1000") + .required(false), + ) + .arg( + arg!(--height "The height of the map") + .default_value("1000") + .required(false), + ), + ) .get_matches(); let output = matches.value_of("FILE").expect("output is required"); @@ -19,8 +45,16 @@ pub fn run_cli() -> Result<()> { let mut rng = rand::thread_rng(); match matches.subcommand() { - Some(("maze", _sub_m)) => MazeGenerator::save_file(&mut rng, output)?, - Some(("fly", _sub_m)) => FlyGenerator::save_file(&mut rng, output)?, + Some(("maze", sub_m)) => { + let width: usize = sub_m.value_of_t("width").unwrap_or_else(|e| e.exit()); + let height: usize = sub_m.value_of_t("height").unwrap_or_else(|e| e.exit()); + MazeGenerator::save_file(&mut rng, width, height, output)? + } + Some(("fly", sub_m)) => { + let width: usize = sub_m.value_of_t("width").unwrap_or_else(|e| e.exit()); + let height: usize = sub_m.value_of_t("height").unwrap_or_else(|e| e.exit()); + FlyGenerator::save_file(&mut rng, width, height, output)? + } _ => panic!("invalid command"), } diff --git a/src/generators/fly.rs b/src/generators/fly.rs index 5f7c81a..7e39117 100644 --- a/src/generators/fly.rs +++ b/src/generators/fly.rs @@ -6,52 +6,58 @@ use rand::Rng; pub struct FlyGenerator; impl MapGenerator for FlyGenerator { - fn generate(rng: &mut R) -> Result { + fn generate(rng: &mut R, width: usize, height: usize) -> Result { let mut map = create_initial_map()?; - const HEIGHT: usize = 1000; - const WIDTH: usize = 100; - - let mut tiles = Array2::from_shape_simple_fn((HEIGHT, WIDTH), || { + let mut tiles = Array2::from_shape_simple_fn((height, width), || { GameTile::new(TILE_EMPTY, TileFlags::empty()) }); - let mut front_tiles = Array2::from_shape_simple_fn((HEIGHT, WIDTH), || { + let mut front_tiles = Array2::from_shape_simple_fn((height, width), || { GameTile::new(TILE_EMPTY, TileFlags::empty()) }); let mut unhookable_tiles = - Array2::from_shape_simple_fn((HEIGHT, WIDTH), || Tile::new(0, TileFlags::empty())); + Array2::from_shape_simple_fn((height, width), || Tile::new(0, TileFlags::empty())); let mut freeze_tiles = - Array2::from_shape_simple_fn((HEIGHT, WIDTH), || Tile::new(0, TileFlags::empty())); + Array2::from_shape_simple_fn((height, width), || Tile::new(0, TileFlags::empty())); tiles - .row_mut(HEIGHT - 2) + .row_mut(height - 2) .iter_mut() .for_each(|tile| tile.id = TILE_UNHOOKABLE); unhookable_tiles - .row_mut(HEIGHT - 2) + .row_mut(height - 2) .iter_mut() - .for_each(|tile| tile.id = 1); + .for_each(|tile| tile.id = 2); - tiles[(HEIGHT - 3, WIDTH / 2)].id = TILE_SPAWN; + tiles[(height - 3, width / 2)].id = TILE_SPAWN; - for x in 0..WIDTH { - front_tiles[(HEIGHT - 6, x)].id = TILE_START; + for x in 0..width { + front_tiles[(height - 6, x)].id = TILE_START; front_tiles[(10, x)].id = TILE_FINISH; } - let mut center: i64 = WIDTH as i64 / 2; + let mut center: i64 = width as i64 / 2; let mut fly_width: i64 = 10; - for y in (0..=(HEIGHT - 3)).rev() { - let direction: i64 = rng.gen_range(-1..=1); + let max_steps = (height as f64 * 0.30f64).round() as i64; + + let mut direction_steps: i64 = rng.gen_range((max_steps / 2)..=max_steps); + let mut direction: i64 = rng.gen_range(-1..=1); + + for y in (0..=(height - 3)).rev() { + if direction_steps == 0 { + direction_steps = rng.gen_range(1..=10); + direction = rng.gen_range(-1..=1); + } + let width_change: i64 = rng.gen_range(-1..=1); center += direction; fly_width += width_change; - center = center.clamp(fly_width, WIDTH as i64 - fly_width); - fly_width = fly_width.clamp(2, 12); + fly_width = fly_width.clamp(3, 12); + center = center.clamp(fly_width, width as i64 - fly_width - 1); - for x in ((center + fly_width) as usize)..WIDTH { + for x in ((center + fly_width) as usize)..width { tiles[(y, x)].id = TILE_FREEZE; freeze_tiles[(y, x)].id = 4; } @@ -60,6 +66,8 @@ impl MapGenerator for FlyGenerator { tiles[(y, x)].id = TILE_FREEZE; freeze_tiles[(y, x)].id = 4; } + + direction_steps -= 1; } let game_layer = GameLayer { @@ -70,11 +78,11 @@ impl MapGenerator for FlyGenerator { tiles: CompressedData::Loaded(front_tiles), }; - let mut unhook_tiles_layer = TilesLayer::new((HEIGHT, WIDTH)); + let mut unhook_tiles_layer = TilesLayer::new((height, width)); unhook_tiles_layer.image = Some(0); unhook_tiles_layer.tiles = CompressedData::Loaded(unhookable_tiles); - let mut freeze_tiles_layer = TilesLayer::new((HEIGHT, WIDTH)); + let mut freeze_tiles_layer = TilesLayer::new((height, width)); freeze_tiles_layer.image = Some(1); freeze_tiles_layer.tiles = CompressedData::Loaded(freeze_tiles); freeze_tiles_layer.color = Color { diff --git a/src/generators/maze.rs b/src/generators/maze.rs index 5461a78..9450d36 100644 --- a/src/generators/maze.rs +++ b/src/generators/maze.rs @@ -6,33 +6,55 @@ use ndarray::Array2; pub struct MazeGenerator; impl MapGenerator for MazeGenerator { - fn generate(rng: &mut R) -> Result { - let mut map = create_initial_map()?; - let maze = Maze::new(1001, 1001).unwrap().generate(rng); + fn generate(rng: &mut R, width: usize, height: usize) -> Result { + // Must be odd. + let width = { + if width % 2 == 0 { + width + 1 + } else { + width + } + }; + let height = { + if height % 2 == 0 { + height + 1 + } else { + height + } + }; - let mut tiles = Array2::from_shape_fn((1001, 1001), |(x, y)| { + let mut map = create_initial_map()?; + let maze = Maze::new(width, height).unwrap().generate(rng); + + let hookable_tiles = + Array2::from_shape_fn((height, width), |(y, x)| { + let mut t = 0; + if maze[x][y] == 1 { + t = 9; + } + Tile::new(t, TileFlags::empty()) + }); + + let mut tiles = Array2::from_shape_fn((width, height), |(y, x)| { GameTile::new(maze[x][y], TileFlags::empty()) }); // Put spawn and start tile on top left most tile. - let mut added_spawn = false; - 'outerStart: for y in 0..101 { - for x in 0..101 { - let tile = &mut tiles[(x, y)]; - if tile.id == 0 && !added_spawn { - *tile = GameTile::new(TILE_SPAWN, TileFlags::empty()); - added_spawn = true; - } else if tile.id == 0 && added_spawn { - *tile = GameTile::new(TILE_START, TileFlags::empty()); + 'outerStart: for y in 0..height { + for x in 0..width { + let tile = &mut tiles[(y, x)]; + if tile.id == 0 { + tile.id = TILE_SPAWN; + replace_around_gametile(&mut tiles, x, y, TILE_EMPTY, TILE_START); break 'outerStart; } } } // Put finish tile on bottom right most tile. - 'outerFinish: for y in (0..1001).rev() { - for x in (0..1001).rev() { - let tile = &mut tiles[(x, y)]; + 'outerFinish: for y in (0..height).rev() { + for x in (0..width).rev() { + let tile = &mut tiles[(y, x)]; if tile.id == 0 { *tile = GameTile::new(TILE_FINISH, TileFlags::empty()); break 'outerFinish; @@ -40,12 +62,19 @@ impl MapGenerator for MazeGenerator { } } + let mut hook_tiles_layer = TilesLayer::new((height, width)); + hook_tiles_layer.image = Some(0); + hook_tiles_layer.tiles = CompressedData::Loaded(hookable_tiles); + let game_layer = GameLayer { tiles: CompressedData::Loaded(tiles), }; let mut physics = Group::physics(); physics.layers.push(Layer::Game(game_layer)); + physics.layers.push(Layer::Tiles(hook_tiles_layer)); + + map.groups.push(quads_sky()); map.groups.push(physics); Ok(map) diff --git a/src/generators/mod.rs b/src/generators/mod.rs index 28144af..4850055 100644 --- a/src/generators/mod.rs +++ b/src/generators/mod.rs @@ -1,6 +1,7 @@ use std::path::Path; use eyre::Result; +use ndarray::{Array2}; use rand::Rng; use twmap::*; @@ -17,10 +18,10 @@ pub const TILE_FINISH: u8 = 34; pub const TILE_SPAWN: u8 = 192; pub trait MapGenerator { - fn generate(rng: &mut R) -> Result; + fn generate(rng: &mut R, width: usize, height: usize) -> Result; - fn save_file(rng: &mut R, path: &Path) -> Result<()> { - let mut map = Self::generate(rng)?; + fn save_file(rng: &mut R, width: usize, height: usize, path: &Path) -> Result<()> { + let mut map = Self::generate(rng, width, height)?; map.save_file(path)?; Ok(()) } @@ -81,3 +82,32 @@ pub fn quads_sky() -> Group { quads_group.layers.push(Layer::Quads(quads_layer)); quads_group } + +// Changed the id of the tile if matches oldid. +pub fn replace_gametile(tiles: &mut Array2, x: usize, y: usize, oldid: u8, newid: u8) { + if tiles[(y, x)].id == oldid { + tiles[(y, x)].id = newid; + } +} + +pub fn replace_around_gametile(tiles: &mut Array2, x: usize, y: usize, oldid: u8, newid: u8) { + let width = tiles.ncols(); + let height = tiles.nrows(); + + let directions = [-1, 0, 1]; + + for diry in directions { + for dirx in directions { + if dirx == 0 && diry == 0 { + continue; + } + if (y as i64) + diry < 0 || (y as i64) + diry >= height as i64 { + continue; + } + if (x as i64) + dirx < 0 || (x as i64) + dirx >= width as i64 { + continue; + } + replace_gametile(tiles, ((x as i64) + dirx) as usize, ((y as i64) + diry) as usize, oldid, newid); + } + } +} \ No newline at end of file