diff --git a/resources/cave_tunnel80x50.xp b/resources/cave_tunnel80x50.xp new file mode 100644 index 0000000..3b0f9cb Binary files /dev/null and b/resources/cave_tunnel80x50.xp differ diff --git a/resources/terminal8x8.jpg b/resources/terminal8x8.jpg index 13e393d..9f4d79d 100644 Binary files a/resources/terminal8x8.jpg and b/resources/terminal8x8.jpg differ diff --git a/src/gui.rs b/src/gui.rs index b36a127..9fada45 100644 --- a/src/gui.rs +++ b/src/gui.rs @@ -1,4 +1,7 @@ -use super::{gamelog::GameLog, CombatStats, InBackpack, Map, Name, Player, Point, Position, State}; +use super::{ + gamelog::GameLog, rex_assets::RexAssets, CombatStats, InBackpack, Map, Name, Player, Point, Position, RunState, + State, +}; use rltk::{Rltk, VirtualKeyCode, RGB}; use specs::prelude::*; @@ -201,3 +204,81 @@ pub fn drop_item_menu(gs: &mut State, ctx: &mut Rltk) -> (ItemMenuResult, Option }, } } + +#[derive(PartialEq, Copy, Clone)] +pub enum MainMenuSelection { + NewGame, + LoadGame, + Quit, +} + +#[derive(PartialEq, Copy, Clone)] +pub enum MainMenuResult { + NoSelection { selected: MainMenuSelection }, + Selected { selected: MainMenuSelection }, +} + +pub fn main_menu(gs: &mut State, ctx: &mut Rltk) -> MainMenuResult { + let runstate = gs.ecs.fetch::(); + let assets = gs.ecs.fetch::(); + + ctx.render_xp_sprite(&assets.menu, 0, 0); + + ctx.print_color(38, 21, RGB::named(rltk::GREEN), RGB::from_f32(0.11, 0.11, 0.11), "RUST-RL"); + + if let RunState::MainMenu { menu_selection: selection } = *runstate { + if selection == MainMenuSelection::NewGame { + ctx.print_color(34, 24, RGB::named(rltk::YELLOW), RGB::from_f32(0.11, 0.11, 0.11), "["); + ctx.print_color(36, 24, RGB::named(rltk::GREEN), RGB::from_f32(0.11, 0.11, 0.11), "new game"); + ctx.print_color(45, 24, RGB::named(rltk::YELLOW), RGB::from_f32(0.11, 0.11, 0.11), "]"); + } else { + ctx.print_color(36, 24, RGB::named(rltk::WHITE), RGB::from_f32(0.11, 0.11, 0.11), "new game"); + } + if selection == MainMenuSelection::LoadGame { + ctx.print_color(38, 26, RGB::named(rltk::YELLOW), RGB::from_f32(0.11, 0.11, 0.11), "["); + ctx.print_color(40, 26, RGB::named(rltk::GREEN), RGB::from_f32(0.11, 0.11, 0.11), "load game"); + ctx.print_color(50, 26, RGB::named(rltk::YELLOW), RGB::from_f32(0.11, 0.11, 0.11), "]"); + } else { + ctx.print_color(40, 26, RGB::named(rltk::WHITE), RGB::from_f32(0.11, 0.11, 0.11), "load game"); + } + if selection == MainMenuSelection::Quit { + ctx.print_color(34, 28, RGB::named(rltk::YELLOW), RGB::from_f32(0.11, 0.11, 0.11), "["); + ctx.print_color(36, 28, RGB::named(rltk::GREEN), RGB::from_f32(0.11, 0.11, 0.11), "goodbye!"); + ctx.print_color(45, 28, RGB::named(rltk::YELLOW), RGB::from_f32(0.11, 0.11, 0.11), "]"); + } else { + ctx.print_color(36, 28, RGB::named(rltk::WHITE), RGB::from_f32(0.11, 0.11, 0.11), "quit"); + } + + match ctx.key { + None => return MainMenuResult::NoSelection { selected: selection }, + Some(key) => match key { + VirtualKeyCode::Escape | VirtualKeyCode::C => { + return MainMenuResult::NoSelection { selected: MainMenuSelection::Quit } + } + VirtualKeyCode::N => return MainMenuResult::NoSelection { selected: MainMenuSelection::NewGame }, + VirtualKeyCode::L => return MainMenuResult::NoSelection { selected: MainMenuSelection::LoadGame }, + VirtualKeyCode::Up => { + let new_selection; + match selection { + MainMenuSelection::NewGame => new_selection = MainMenuSelection::Quit, + MainMenuSelection::LoadGame => new_selection = MainMenuSelection::NewGame, + MainMenuSelection::Quit => new_selection = MainMenuSelection::LoadGame, + } + return MainMenuResult::NoSelection { selected: new_selection }; + } + VirtualKeyCode::Down => { + let new_selection; + match selection { + MainMenuSelection::NewGame => new_selection = MainMenuSelection::LoadGame, + MainMenuSelection::LoadGame => new_selection = MainMenuSelection::Quit, + MainMenuSelection::Quit => new_selection = MainMenuSelection::NewGame, + } + return MainMenuResult::NoSelection { selected: new_selection }; + } + VirtualKeyCode::Return => return MainMenuResult::Selected { selected: selection }, + _ => return MainMenuResult::NoSelection { selected: selection }, + }, + } + } + MainMenuResult::NoSelection { selected: MainMenuSelection::NewGame } +} diff --git a/src/main.rs b/src/main.rs index 1ddd2a9..909d850 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,6 @@ use rltk::{GameState, Point, Rltk, RGB}; use specs::prelude::*; +use std::ops::Add; mod components; pub use components::*; @@ -26,6 +27,7 @@ mod inventory_system; use inventory_system::*; mod particle_system; use particle_system::{ParticleBuilder, DEFAULT_PARTICLE_LIFETIME}; +mod rex_assets; // Embedded resources for use in wasm build rltk::embedded_resource!(TERMINAL8X8, "../resources/terminal8x8.jpg"); @@ -40,6 +42,7 @@ pub enum RunState { MonsterTurn, ShowInventory, ShowDropItem, + MainMenu { menu_selection: gui::MainMenuSelection }, } pub struct State { @@ -72,37 +75,43 @@ impl State { impl GameState for State { fn tick(&mut self, ctx: &mut Rltk) { - // Clear screen - ctx.cls(); - particle_system::cull_dead_particles(&mut self.ecs, ctx); - - // Draw map and ui - draw_map(&self.ecs, ctx); - { - let positions = self.ecs.read_storage::(); - let renderables = self.ecs.read_storage::(); - let map = self.ecs.fetch::(); - - let mut data = (&positions, &renderables).join().collect::>(); - data.sort_by(|&a, &b| b.1.render_order.cmp(&a.1.render_order)); - for (pos, render) in data.iter() { - let idx = map.xy_idx(pos.x, pos.y); - let mut bg = render.bg; - if map.bloodstains.contains(&idx) { - bg = RGB::from_f32(0.4, 0., 0.); - } - if map.visible_tiles[idx] { - ctx.set(pos.x, pos.y, render.fg, bg, render.glyph); - } - } - gui::draw_ui(&self.ecs, ctx); - } - let mut new_runstate; { let runstate = self.ecs.fetch::(); new_runstate = *runstate; } + // Clear screen + ctx.cls(); + particle_system::cull_dead_particles(&mut self.ecs, ctx); + + match new_runstate { + RunState::MainMenu { .. } => {} + _ => { + // Draw map and ui + draw_map(&self.ecs, ctx); + { + let positions = self.ecs.read_storage::(); + let renderables = self.ecs.read_storage::(); + let map = self.ecs.fetch::(); + + let mut data = (&positions, &renderables).join().collect::>(); + data.sort_by(|&a, &b| b.1.render_order.cmp(&a.1.render_order)); + for (pos, render) in data.iter() { + let idx = map.xy_idx(pos.x, pos.y); + let offsets = RGB::from_u8(map.red_offset[idx], map.green_offset[idx], map.blue_offset[idx]); + let mut bg = render.bg.add(RGB::from_u8(26, 45, 45)).add(offsets); + //bg = bg.add(offsets); + if map.bloodstains.contains(&idx) { + bg = RGB::from_f32(0.4, 0., 0.); + } + if map.visible_tiles[idx] { + ctx.set(pos.x, pos.y, render.fg, bg, render.glyph); + } + } + gui::draw_ui(&self.ecs, ctx); + } + } + } match new_runstate { RunState::PreRun => { @@ -153,6 +162,28 @@ impl GameState for State { } } } + RunState::MainMenu { .. } => { + let result = gui::main_menu(self, ctx); + match result { + gui::MainMenuResult::NoSelection { selected } => { + new_runstate = RunState::MainMenu { menu_selection: selected } + } + gui::MainMenuResult::Selected { selected } => { + match selected { + gui::MainMenuSelection::NewGame => new_runstate = RunState::PreRun, + gui::MainMenuSelection::LoadGame => { + new_runstate = RunState::PreRun; + //saveload_system::load_game(&mut self.ecs); + //rew_runstate = RunState::AwaitingInput; + //saveload_system::delete_save(); + } + gui::MainMenuSelection::Quit => { + ::std::process::exit(0); + } + } + } + } + } } { @@ -164,14 +195,22 @@ impl GameState for State { } } +const DISPLAYWIDTH: i32 = 80; +const DISPLAYHEIGHT: i32 = 50; + fn main() -> rltk::BError { use rltk::RltkBuilder; - let mut context = RltkBuilder::simple80x50() - .with_tile_dimensions(16, 16) - //.with_fitscreen(true) + let mut context = RltkBuilder::new() .with_title("rust-rl") + .with_dimensions(DISPLAYWIDTH, DISPLAYHEIGHT) + .with_tile_dimensions(16, 16) + .with_resource_path("resources/") + .with_font("terminal8x8.jpg", 8, 8) + .with_simple_console(DISPLAYWIDTH, DISPLAYHEIGHT, "terminal8x8.jpg") + .with_simple_console_no_bg(DISPLAYWIDTH, DISPLAYHEIGHT, "terminal8x8.jpg") .build()?; - context.with_post_scanlines(true); + context.with_post_scanlines(false); + //context.screen_burn_color(RGB::named((150, 255, 255))); let mut gs = State { ecs: World::new() }; gs.ecs.register::(); @@ -207,8 +246,9 @@ fn main() -> rltk::BError { gs.ecs.insert(Point::new(player_x, player_y)); gs.ecs.insert(player_entity); gs.ecs.insert(gamelog::GameLog { entries: vec!["Here's your welcome message.".to_string()] }); - gs.ecs.insert(RunState::PreRun); + gs.ecs.insert(RunState::MainMenu { menu_selection: gui::MainMenuSelection::NewGame }); gs.ecs.insert(particle_system::ParticleBuilder::new()); + gs.ecs.insert(rex_assets::RexAssets::new()); rltk::main_loop(context, gs) } diff --git a/src/map.rs b/src/map.rs index 289c076..d63f91f 100644 --- a/src/map.rs +++ b/src/map.rs @@ -3,6 +3,7 @@ use rltk::{Algorithm2D, BaseMap, Point, RandomNumberGenerator, Rltk, RGB}; use specs::prelude::*; use std::cmp::{max, min}; use std::collections::HashSet; +use std::ops::Add; #[derive(PartialEq, Copy, Clone)] pub enum TileType { @@ -12,6 +13,7 @@ pub enum TileType { pub const MAPWIDTH: usize = 80; pub const MAPHEIGHT: usize = 43; +const MAX_OFFSET: u8 = 32; const MAPCOUNT: usize = MAPHEIGHT * MAPWIDTH; #[derive(Default)] @@ -22,6 +24,9 @@ pub struct Map { pub height: i32, pub revealed_tiles: Vec, pub visible_tiles: Vec, + pub red_offset: Vec, + pub green_offset: Vec, + pub blue_offset: Vec, pub blocked: Vec, pub tile_content: Vec>, pub bloodstains: HashSet, @@ -89,6 +94,9 @@ impl Map { height: MAPHEIGHT as i32, revealed_tiles: vec![false; MAPCOUNT], visible_tiles: vec![false; MAPCOUNT], + red_offset: vec![0; MAPCOUNT], + green_offset: vec![0; MAPCOUNT], + blue_offset: vec![0; MAPCOUNT], blocked: vec![false; MAPCOUNT], tile_content: vec![Vec::new(); MAPCOUNT], bloodstains: HashSet::new(), @@ -100,6 +108,19 @@ impl Map { let mut rng = RandomNumberGenerator::new(); + for idx in 0..map.red_offset.len() { + let roll = rng.roll_dice(1, MAX_OFFSET as i32); + map.red_offset[idx] = roll as u8; + } + for idx in 0..map.green_offset.len() { + let roll = rng.roll_dice(1, MAX_OFFSET as i32); + map.green_offset[idx] = roll as u8; + } + for idx in 0..map.blue_offset.len() { + let roll = rng.roll_dice(1, MAX_OFFSET as i32); + map.blue_offset[idx] = roll as u8; + } + for _i in 0..MAX_ROOMS { let w = rng.range(MIN_SIZE, MAX_SIZE); let h = rng.range(MIN_SIZE, MAX_SIZE); @@ -198,23 +219,28 @@ pub fn draw_map(ecs: &World, ctx: &mut Rltk) { let mut y = 0; let mut x = 0; for (idx, tile) in map.tiles.iter().enumerate() { + // Get our colour offsets. Credit to Brogue for the inspiration here. + let offsets = RGB::from_u8(map.red_offset[idx], map.green_offset[idx], map.blue_offset[idx]); if map.revealed_tiles[idx] { + let mut fg = offsets; + // Right now, everything always has the same background. It's a + // very dark green, just to distinguish it slightly from the + // black that is tiles we've *never* seen. + let mut bg = offsets.add(RGB::from_u8(26, 45, 45)); let glyph; - let mut fg; - let mut bg = RGB::from_f32(0., 0., 0.); match tile { TileType::Floor => { glyph = rltk::to_cp437('.'); - fg = RGB::from_f32(0.0, 1.0, 0.5); + fg = fg.add(RGB::from_f32(0.1, 0.8, 0.5)); } TileType::Wall => { glyph = wall_glyph(&*map, x, y); - fg = RGB::from_f32(0.0, 1.0, 0.0); + fg = fg.add(RGB::from_f32(0.1, 0.8, 0.1)); } } let mut bloody = false; if map.bloodstains.contains(&idx) { - bg = RGB::from_f32(0.4, 0., 0.); + bg = bg.add(RGB::from_f32(0.4, 0., 0.)); bloody = true; } if !map.visible_tiles[idx] { @@ -223,7 +249,9 @@ pub fn draw_map(ecs: &World, ctx: &mut Rltk) { // since desaturate is an expensive function. If this stops being the case, // will need to switch to using desaturate if bloody { - bg = RGB::from_f32(0.4, 0.4, 0.4) + bg = RGB::from_f32(0.4, 0.4, 0.4); + } else { + bg = bg.to_greyscale(); } } ctx.set(x, y, fg, bg, glyph); diff --git a/src/player.rs b/src/player.rs index 23d7a47..bc6a634 100644 --- a/src/player.rs +++ b/src/player.rs @@ -1,5 +1,5 @@ use super::{ - gamelog::GameLog, CombatStats, Item, Map, Player, Position, RunState, State, Viewshed, WantsToMelee, + gamelog::GameLog, gui, CombatStats, Item, Map, Player, Position, RunState, State, Viewshed, WantsToMelee, WantsToPickupItem, MAPHEIGHT, MAPWIDTH, }; use rltk::{Point, Rltk, VirtualKeyCode}; @@ -98,6 +98,7 @@ pub fn player_input(gs: &mut State, ctx: &mut Rltk) -> RunState { VirtualKeyCode::G => get_item(&mut gs.ecs), VirtualKeyCode::I => return RunState::ShowInventory, VirtualKeyCode::D => return RunState::ShowDropItem, + VirtualKeyCode::Escape => return RunState::MainMenu { menu_selection: gui::MainMenuSelection::NewGame }, _ => { return RunState::AwaitingInput; } @@ -105,3 +106,48 @@ pub fn player_input(gs: &mut State, ctx: &mut Rltk) -> RunState { } RunState::PlayerTurn } + +/* Playing around with autoexplore, without having read how to do it. +pub fn auto_explore(ecs: &mut World) { + let player_pos = ecs.fetch::(); + let positions = ecs.read_storage::(); + let entities = ecs.entities(); + let map = ecs.fetch::(); + let mut viewsheds = ecs.write_storage::(); + + let mut unexplored_tiles: Vec = vec![]; + for (idx, _tile) in map.tiles.iter().enumerate() { + if !map.revealed_tiles[idx] { + unexplored_tiles.push(idx); + } + } + let mut unexplored_tile = (0, 0.0f32); + + let flow_map = DijkstraMap::new_empty(MAPWIDTH, MAPHEIGHT, 200.0); + + DijkstraMap::build(&mut flow_map, &unexplored_tiles, &map); + for (i, tile) in map.tiles.iter().enumerate() { + if *tile == TileType::Floor { + let distance_to_start = flow_map.map[i]; + + if distance_to_start > unexplored_tile.1 { + unexplored_tile.0 = i; + unexplored_tile.1 = distance_to_start; + } + } + } + + let path = rltk::a_star_search(map.xy_idx(player_pos.x, player_pos.y), unexplored_tile.0, &*map); + if path.success && path.steps.len() > 1 { + let mut idx = map.xy_idx(player_pos.x, player_pos.y); + map.blocked[idx] = false; + player_pos.x = (path.steps[1] as i32) % map.width; + player_pos.y = (path.steps[1] as i32) / map.width; + idx = map.xy_idx(player_pos.x, player_pos.y); + map.blocked[idx] = true; + for (ent, viewshed, pos) in (&entities, &mut viewsheds, &positions).join() { + viewshed.dirty = true; + } + } +} +*/ diff --git a/src/rex_assets.rs b/src/rex_assets.rs new file mode 100644 index 0000000..2949a5b --- /dev/null +++ b/src/rex_assets.rs @@ -0,0 +1,16 @@ +use rltk::rex::XpFile; + +rltk::embedded_resource!(CAVE_TUNNEL, "../resources/cave_tunnel80x50.xp"); + +pub struct RexAssets { + pub menu: XpFile, +} + +impl RexAssets { + #[allow(clippy::new_without_default)] + pub fn new() -> RexAssets { + rltk::link_resource!(CAVE_TUNNEL, "../resources/cave_tunnel80x50.xp"); + + RexAssets { menu: XpFile::from_resource("../resources/cave_tunnel80x50.xp").unwrap() } + } +} diff --git a/wasm/rust-llyrl.js b/wasm/rust-llyrl.js index 00e58bb..fb33f48 100644 --- a/wasm/rust-llyrl.js +++ b/wasm/rust-llyrl.js @@ -801,16 +801,16 @@ function __wbg_get_imports() { const ret = wasm.memory; return addHeapObject(ret); }; - imports.wbg.__wbindgen_closure_wrapper194 = function(arg0, arg1, arg2) { - const ret = makeMutClosure(arg0, arg1, 31, __wbg_adapter_20); + imports.wbg.__wbindgen_closure_wrapper187 = function(arg0, arg1, arg2) { + const ret = makeMutClosure(arg0, arg1, 7, __wbg_adapter_20); return addHeapObject(ret); }; - imports.wbg.__wbindgen_closure_wrapper525 = function(arg0, arg1, arg2) { - const ret = makeMutClosure(arg0, arg1, 136, __wbg_adapter_23); + imports.wbg.__wbindgen_closure_wrapper648 = function(arg0, arg1, arg2) { + const ret = makeMutClosure(arg0, arg1, 158, __wbg_adapter_23); return addHeapObject(ret); }; - imports.wbg.__wbindgen_closure_wrapper527 = function(arg0, arg1, arg2) { - const ret = makeMutClosure(arg0, arg1, 136, __wbg_adapter_23); + imports.wbg.__wbindgen_closure_wrapper650 = function(arg0, arg1, arg2) { + const ret = makeMutClosure(arg0, arg1, 158, __wbg_adapter_23); return addHeapObject(ret); }; diff --git a/wasm/rust-llyrl_bg.wasm b/wasm/rust-llyrl_bg.wasm index 73c6d56..90b6ca2 100644 Binary files a/wasm/rust-llyrl_bg.wasm and b/wasm/rust-llyrl_bg.wasm differ