use super::{ effects::{ add_effect, EffectType, Targets }, gamelog, gui::obfuscate_name_ecs, gui::renderable_colour_ecs, gui::item_colour_ecs, camera::get_screen_bounds, raws::Reaction, Attributes, BlocksTile, BlocksVisibility, Door, EntityMoved, Faction, HasAncestry, Hidden, HungerClock, HungerState, Item, Map, Name, Player, Pools, Position, Renderable, RunState, states::state::*, Telepath, TileType, Viewshed, WantsToMelee, WantsToPickupItem, get_dest, Destination, DamageType, }; use bracket_lib::prelude::*; use specs::prelude::*; use std::cmp::{ max, min }; use crate::consts::events::*; use crate::consts::ids::*; use crate::gui::with_article; use notan::prelude::*; use std::collections::HashMap; pub fn try_door(i: i32, j: i32, ecs: &mut World) -> RunState { let mut positions = ecs.write_storage::(); let mut players = ecs.write_storage::(); let mut viewsheds = ecs.write_storage::(); let attributes = ecs.read_storage::(); let map = ecs.fetch::(); let entities = ecs.entities(); let mut doors = ecs.write_storage::(); let mut blocks_visibility = ecs.write_storage::(); let mut blocks_movement = ecs.write_storage::(); let names = ecs.read_storage::(); let mut rng = ecs.write_resource::(); let mut result = RunState::AwaitingInput; let mut door_pos: Option = None; for (_entity, _player, pos, attributes) in ( &entities, &mut players, &mut positions, &attributes, ).join() { let delta_x = i; let delta_y = j; if !( pos.x + delta_x < 0 || pos.x + delta_x > map.width - 1 || pos.y + delta_y < 0 || pos.y + delta_y > map.height - 1 ) { let destination_idx = map.xy_idx(pos.x + delta_x, pos.y + delta_y); if !crate::spatial::has_tile_content(destination_idx) { gamelog::Logger::new().append("You see no door there.").log(); } let mut multiple_tile_content = false; if crate::spatial::length(destination_idx) > 1 { multiple_tile_content = true; } crate::spatial::for_each_tile_content(destination_idx, |potential_target| { let door = doors.get_mut(potential_target); if let Some(door) = door { if door.open == true { let renderables = ecs.read_storage::(); if multiple_tile_content { if let Some(name) = names.get(potential_target) { gamelog::Logger ::new() .append("The") .colour(renderable_colour_ecs(ecs, potential_target)) .append(&name.name) .colour(WHITE) .append("is blocked.") .log(); } } else if rng.roll_dice(1, 6) + attributes.strength.bonus < 2 { if let Some(name) = names.get(potential_target) { gamelog::Logger ::new() .append("The") .colour(renderable_colour_ecs(ecs, potential_target)) .append(&name.name) .colour(WHITE) .append("resists!") .log(); } } else { door.open = false; if door.blocks_vis { blocks_visibility .insert(potential_target, BlocksVisibility {}) .expect("Unable to insert BlocksVisibility."); } if door.blocks_move { blocks_movement .insert(potential_target, BlocksTile {}) .expect("Unable to insert BlocksTile."); } if let Some(name) = names.get(potential_target) { gamelog::Logger ::new() .append("You close the") .colour(renderable_colour_ecs(ecs, potential_target)) .append_n(&name.name) .colour(WHITE) .period() .log(); } //Re-get renderables as mutable std::mem::drop(renderables); let mut renderables = ecs.write_storage::(); let render_data = renderables.get_mut(potential_target).unwrap(); render_data.swap(); door_pos = Some(Point::new(pos.x + delta_x, pos.y + delta_y)); } result = RunState::Ticking; } else { gamelog::Logger::new().append("It's already closed.").log(); } } }); } } // If a door was interacted with, update every viewshed that could // see that door. for viewshed in (&mut viewsheds).join() { if let Some(door_pos) = door_pos { if viewshed.visible_tiles.contains(&door_pos) { viewshed.dirty = true; } } } return result; } pub fn open(i: i32, j: i32, ecs: &mut World) -> RunState { let mut positions = ecs.write_storage::(); let mut players = ecs.write_storage::(); let mut viewsheds = ecs.write_storage::(); let attributes = ecs.read_storage::(); let map = ecs.fetch::(); let entities = ecs.entities(); let mut doors = ecs.write_storage::(); let mut blocks_visibility = ecs.write_storage::(); let mut blocks_movement = ecs.write_storage::(); let names = ecs.read_storage::(); let mut rng = ecs.write_resource::(); let mut result = RunState::AwaitingInput; let mut door_pos: Option = None; for (_entity, _player, pos, attributes) in ( &entities, &mut players, &mut positions, &attributes, ).join() { let delta_x = i; let delta_y = j; if !( pos.x + delta_x < 0 || pos.x + delta_x > map.width - 1 || pos.y + delta_y < 0 || pos.y + delta_y > map.height - 1 ) { let destination_idx = map.xy_idx(pos.x + delta_x, pos.y + delta_y); if !crate::spatial::has_tile_content(destination_idx) { gamelog::Logger::new().append("You see no door there.").log(); } crate::spatial::for_each_tile_content(destination_idx, |potential_target| { let door = doors.get_mut(potential_target); if let Some(door) = door { if door.open == false { let renderables = ecs.read_storage::(); if rng.roll_dice(1, 6) + attributes.strength.bonus < 2 { if let Some(name) = names.get(potential_target) { gamelog::Logger ::new() .append("The") .colour(renderable_colour_ecs(ecs, potential_target)) .append(&name.name) .colour(WHITE) .append("resists!") .log(); } } else { door.open = true; blocks_visibility.remove(potential_target); blocks_movement.remove(potential_target); if let Some(name) = names.get(potential_target) { gamelog::Logger ::new() .append("You open the") .colour(renderable_colour_ecs(ecs, potential_target)) .append_n(&name.name) .colour(WHITE) .period() .log(); } std::mem::drop(renderables); let mut renderables = ecs.write_storage::(); let render_data = renderables.get_mut(potential_target).unwrap(); render_data.swap(); door_pos = Some(Point::new(pos.x + delta_x, pos.y + delta_y)); } result = RunState::Ticking; } else { gamelog::Logger::new().append("It's already open.").log(); } } }); } } // If a door was interacted with, update every viewshed that could // see that door. for viewshed in (&mut viewsheds).join() { if let Some(door_pos) = door_pos { if viewshed.visible_tiles.contains(&door_pos) { viewshed.dirty = true; } } } return result; } pub fn kick(i: i32, j: i32, ecs: &mut World) -> RunState { let mut something_was_destroyed: Option = None; let mut destroyed_pos: Option = None; { let mut positions = ecs.write_storage::(); let mut players = ecs.write_storage::(); let mut viewsheds = ecs.write_storage::(); let attributes = ecs.read_storage::(); let map = ecs.fetch::(); let entities = ecs.entities(); let mut doors = ecs.write_storage::(); let names = ecs.read_storage::(); let mut rng = ecs.write_resource::(); for (entity, _player, pos, attributes) in ( &entities, &mut players, &mut positions, &attributes, ).join() { let delta_x = i; let delta_y = j; if !( pos.x + delta_x < 0 || pos.x + delta_x > map.width - 1 || pos.y + delta_y < 0 || pos.y + delta_y > map.height - 1 ) { let destination_idx = map.xy_idx(pos.x + delta_x, pos.y + delta_y); if !crate::spatial::has_tile_content(destination_idx) { if rng.roll_dice(1, 20) == 20 { add_effect( None, EffectType::Damage { amount: 1, damage_type: DamageType::Physical }, Targets::Entity { target: entity } ); gamelog::Logger ::new() .append("Ouch! You kick the open air, and pull something.") .log(); break; } else { // If there's nothing at all, just kick the air and waste a turn. gamelog::Logger::new().append("You kick the open air.").log(); break; } } else { let mut last_non_door_target: Option = None; let mut target_name = "thing"; let mut colour = WHITE; crate::spatial::for_each_tile_content_with_bool( destination_idx, |potential_target| { if let Some(name) = names.get(potential_target) { target_name = &name.name; } let items = ecs.read_storage::(); colour = if let Some(_) = items.get(potential_target) { item_colour_ecs(ecs, potential_target) } else { renderable_colour_ecs(ecs, potential_target) }; // If it's a door, let door = doors.get_mut(potential_target); if let Some(door) = door { // If the door is closed, if door.open == false { add_effect( None, EffectType::Particle { glyph: to_cp437('‼'), fg: RGB::named(CHOCOLATE), bg: RGB::named(BLACK), lifespan: 150.0, delay: 0.0, }, Targets::Entity { target: potential_target } ); // ~33% chance of breaking it down + str if rng.roll_dice(1, 10) + attributes.strength.bonus > 6 { gamelog::Logger ::new() .append("As you kick the") .colour(colour) .append_n(obfuscate_name_ecs(ecs, potential_target).0) .colour(WHITE) .append(", it crashes open!") .log(); something_was_destroyed = Some(potential_target); destroyed_pos = Some( Point::new(pos.x + delta_x, pos.y + delta_y) ); gamelog::record_event(EVENT::BrokeDoor(1)); return false; // 66% chance of just kicking it. } else { gamelog::Logger ::new() .append("You kick the") .colour(colour) .append_n(obfuscate_name_ecs(ecs, potential_target).0) .colour(WHITE) .period() .log(); return false; } // If the door is open and there's nothing else on the tile, } else if crate::spatial::length(destination_idx) == 1 { // Just kick the air. gamelog::Logger::new().append("You kick the open air.").log(); return false; } } else { last_non_door_target = Some(potential_target); } return true; } ); if let Some(e) = last_non_door_target { gamelog::Logger ::new() .append("You kick the") .colour(colour) .append_n(obfuscate_name_ecs(ecs, e).0) .colour(WHITE) .period() .log(); add_effect( None, EffectType::Particle { glyph: to_cp437('‼'), fg: RGB::named(CHOCOLATE), bg: RGB::named(BLACK), lifespan: 150.0, delay: 0.0, }, Targets::Entity { target: e } ); // Do something here if it's anything other than a door. break; } } } } if let Some(destroyed_pos) = destroyed_pos { for viewshed in (&mut viewsheds).join() { if viewshed.visible_tiles.contains(&destroyed_pos) { viewshed.dirty = true; } } } } if let Some(destroyed_thing) = something_was_destroyed { ecs.delete_entity(destroyed_thing).expect("Unable to delete."); } gamelog::record_event(EVENT::KickedSomething(1)); return RunState::Ticking; } pub fn try_move_player(delta_x: i32, delta_y: i32, ecs: &mut World) -> RunState { let mut positions = ecs.write_storage::(); let mut players = ecs.write_storage::(); let mut viewsheds = ecs.write_storage::(); let mut telepaths = ecs.write_storage::(); let mut entity_moved = ecs.write_storage::(); let factions = ecs.read_storage::(); let ancestries = ecs.read_storage::(); let pools = ecs.read_storage::(); let map = ecs.fetch::(); let entities = ecs.entities(); let mut wants_to_melee = ecs.write_storage::(); let mut doors = ecs.write_storage::(); let names = ecs.read_storage::(); let mut swap_entities: Vec<(Entity, i32, i32)> = Vec::new(); let mut result: Option; for (entity, _player, pos, viewshed) in ( &entities, &mut players, &mut positions, &mut viewsheds, ).join() { if pos.x + delta_x < 0 || pos.x + delta_x > map.width - 1 || pos.y + delta_y < 0 || pos.y + delta_y > map.height - 1 { return RunState::AwaitingInput; } let destination_idx = map.xy_idx(pos.x + delta_x, pos.y + delta_y); result = crate::spatial::for_each_tile_content_with_runstate( destination_idx, |potential_target| { let mut hostile = true; if pools.get(potential_target).is_some() { // We get the reaction of the target to this entity -- // i.e. in reverse to usual. We want to know if the target // is hostile to us. If it isn't, we can swap places. let result = crate::raws::get_reactions( potential_target, entity, &factions, &ancestries, &crate::raws::RAWS.lock().unwrap() ); if result != Reaction::Attack { hostile = false; } } if !hostile { swap_entities.push((potential_target, pos.x, pos.y)); pos.x = min(map.width - 1, max(0, pos.x + delta_x)); pos.y = min(map.height - 1, max(0, pos.y + delta_y)); entity_moved.insert(entity, EntityMoved {}).expect("Unable to insert marker"); viewshed.dirty = true; let mut ppos = ecs.write_resource::(); ppos.x = pos.x; ppos.y = pos.y; } else { let target = pools.get(potential_target); if let Some(_target) = target { wants_to_melee .insert(entity, WantsToMelee { target: potential_target }) .expect("Add target failed."); return Some(RunState::Ticking); } } let door = doors.get_mut(potential_target); if let Some(door) = door { if door.open == false && door.blocks_move { if let Some(name) = names.get(potential_target) { let colour = if let Some(_) = ecs.read_storage::().get(potential_target) { item_colour_ecs(ecs, potential_target) } else { renderable_colour_ecs(ecs, potential_target) }; gamelog::Logger ::new() .append("The") .colour(colour) .append(&name.name) .colour(WHITE) .append("is in your way.") .log(); } return Some(RunState::AwaitingInput); } } return None; } ); if result.is_some() { return result.unwrap(); } if swap_entities.len() <= 0 { if crate::spatial::is_blocked(destination_idx) { gamelog::Logger::new().append("You can't move there.").log(); return RunState::AwaitingInput; } let hidden = ecs.read_storage::(); // Push every entity name in the pile to a vector of strings let mut seen_items: Vec<(String, (u8, u8, u8))> = Vec::new(); let mut some = false; crate::spatial::for_each_tile_content(destination_idx, |entity| { if !hidden.get(entity).is_some() && names.get(entity).is_some() { let item_name = obfuscate_name_ecs(ecs, entity).0; let item_colour = item_colour_ecs(ecs, entity); seen_items.push((item_name, item_colour)); some = true; } }); match map.tiles[destination_idx] { TileType::ToLocal(id) => { let name = get_local_desc(id); let colour = rgb_to_u8(get_local_col(id)); gamelog::Logger ::new() .append("You see") .colour(colour) .append_n(&name) .colour(WHITE) .period() .log(); } _ => {} } // If some names were found, append. Logger = logger is necessary // makes logger called a mutable self. It's not the most efficient // but it happens infrequently enough (once per player turn at most) // that it shouldn't matter. if some { let mut logger = gamelog::Logger::new().append("You see"); for i in 0..seen_items.len() { if i > 0 && i < seen_items.len() { logger = logger.append(", "); } logger = logger .colour(seen_items[i].1) .append_n(with_article(&seen_items[i].0)) .colour(WHITE); } logger.period().log(); } let old_idx = map.xy_idx(pos.x, pos.y); pos.x = min(map.width - 1, max(0, pos.x + delta_x)); pos.y = min(map.height - 1, max(0, pos.y + delta_y)); let new_idx = map.xy_idx(pos.x, pos.y); entity_moved.insert(entity, EntityMoved {}).expect("Unable to insert marker"); crate::spatial::move_entity(entity, old_idx, new_idx); // Dirty viewsheds, and check only now if telepath viewshed exists viewshed.dirty = true; if let Some(telepathy) = telepaths.get_mut(entity) { telepathy.dirty = true; } let mut ppos = ecs.write_resource::(); ppos.x = pos.x; ppos.y = pos.y; if map.tiles[new_idx] == TileType::ToOvermap(map.id) { return RunState::GoToLevel(ID_OVERMAP, TileType::ToLocal(map.id)); } return RunState::Ticking; } } for m in swap_entities.iter() { if let Some(name) = names.get(m.0) { gamelog::Logger ::new() .append("You swap places with the") .colour(renderable_colour_ecs(ecs, m.0)) .append_n(&name.name) .colour(WHITE) .period() .log(); } if let Some(their_pos) = positions.get_mut(m.0) { let old_idx = map.xy_idx(their_pos.x, their_pos.y); their_pos.x = m.1; their_pos.y = m.2; let new_idx = map.xy_idx(their_pos.x, their_pos.y); crate::spatial::move_entity(m.0, old_idx, new_idx); return RunState::Ticking; } } return RunState::AwaitingInput; } fn get_item(ecs: &mut World) -> RunState { let player_pos = ecs.fetch::(); let player_entity = ecs.fetch::(); let entities = ecs.entities(); let items = ecs.read_storage::(); let positions = ecs.read_storage::(); let mut target_item: Option = None; for (item_entity, _item, position) in (&entities, &items, &positions).join() { if position.x == player_pos.x && position.y == player_pos.y { target_item = Some(item_entity); } } match target_item { None => { gamelog::Logger::new().append("There is nothing to pick up.").log(); return RunState::AwaitingInput; } Some(item) => { let mut pickup = ecs.write_storage::(); pickup .insert(*player_entity, WantsToPickupItem { collected_by: *player_entity, item }) .expect("Unable to insert want to pickup item."); return RunState::Ticking; } } } fn try_descend(ecs: &mut World) -> RunState { let dest = try_change_level(ecs, false); let curr_map_id = ecs.fetch::().id; return match dest { Destination::None => RunState::AwaitingInput, Destination::NextLevel => RunState::GoToLevel(curr_map_id + 1, TileType::UpStair), Destination::PreviousLevel => RunState::GoToLevel(curr_map_id - 1, TileType::DownStair), Destination::ToLocal(id) => RunState::GoToLevel(ID_OVERMAP, TileType::ToLocal(id)), Destination::ToOvermap(id) => RunState::GoToLevel(id, TileType::ToOvermap(id)), }; } fn try_ascend(ecs: &mut World) -> RunState { let dest = try_change_level(ecs, true); let curr_map_id = ecs.fetch::().id; return match dest { Destination::None => RunState::AwaitingInput, Destination::NextLevel => RunState::GoToLevel(curr_map_id + 1, TileType::UpStair), Destination::PreviousLevel => RunState::GoToLevel(curr_map_id - 1, TileType::DownStair), Destination::ToLocal(id) => RunState::GoToLevel(ID_OVERMAP, TileType::ToLocal(id)), Destination::ToOvermap(id) => RunState::GoToLevel(id, TileType::ToOvermap(id)), }; } pub fn player_input(gs: &mut State, ctx: &mut App, on_overmap: bool) -> RunState { let key = &ctx.keyboard; // Movement for keycode in key.pressed.iter() { match *keycode { KeyCode::Numpad1 | KeyCode::B => { return try_move_player(-1, 1, &mut gs.ecs); } KeyCode::Numpad2 | KeyCode::Down | KeyCode::J => { return try_move_player(0, 1, &mut gs.ecs); } KeyCode::Numpad3 | KeyCode::N => { return try_move_player(1, 1, &mut gs.ecs); } KeyCode::Numpad4 | KeyCode::Left | KeyCode::H => { return try_move_player(-1, 0, &mut gs.ecs); } KeyCode::Numpad6 | KeyCode::Right | KeyCode::L => { return try_move_player(1, 0, &mut gs.ecs); } KeyCode::Numpad7 | KeyCode::Y => { return try_move_player(-1, -1, &mut gs.ecs); } KeyCode::Numpad8 | KeyCode::Up | KeyCode::K => { return try_move_player(0, -1, &mut gs.ecs); } KeyCode::Numpad9 | KeyCode::U => { return try_move_player(1, -1, &mut gs.ecs); } KeyCode::Period => { if key.shift() { return try_descend(&mut gs.ecs); } return skip_turn(&mut gs.ecs); } KeyCode::Comma => { if key.shift() { return try_ascend(&mut gs.ecs); } } KeyCode::Slash => { if key.shift() { return RunState::HelpScreen; } } KeyCode::C => { if !on_overmap { return RunState::ActionWithDirection { function: try_door }; } } KeyCode::O => { if !on_overmap { return RunState::ActionWithDirection { function: open }; } } KeyCode::F => { if !on_overmap { return RunState::ActionWithDirection { function: kick }; } } KeyCode::G => { return get_item(&mut gs.ecs); } KeyCode::I => { return RunState::ShowInventory; } KeyCode::D => { return RunState::ShowDropItem; } KeyCode::R => { return RunState::ShowRemoveItem; } KeyCode::Minus => { return RunState::ShowCheatMenu; } KeyCode::Escape => { return RunState::SaveGame; } KeyCode::X => { let bounds = get_screen_bounds(&gs.ecs, false); let ppos = gs.ecs.fetch::(); let (x, y) = ( ppos.x + bounds.x_offset - bounds.min_x, ppos.y + bounds.y_offset - bounds.min_y, ); return RunState::Farlook { x, y }; } _ => {} } } return RunState::AwaitingInput; } fn try_change_level(ecs: &mut World, backtracking: bool) -> Destination { let player_pos = ecs.fetch::(); let map = ecs.fetch::(); let player_idx = map.xy_idx(player_pos.x, player_pos.y); let this_tile = map.tiles[player_idx]; let mut blocked = false; crate::spatial::for_each_tile_content(player_idx, |potential| { if let Some(is_door) = ecs.read_storage::().get(potential) { if is_door.open == false { blocked = true; gamelog::Logger::new().append("The way is blocked.").log(); } } }); if blocked { return Destination::None; } return get_dest(this_tile, backtracking); } fn skip_turn(ecs: &mut World) -> RunState { let player_entity = ecs.fetch::(); let mut viewsheds = ecs.write_storage::(); let worldmap_resource = ecs.fetch::(); let hunger_clocks = ecs.read_storage::(); // Default to being able to heal by waiting. let mut can_heal = true; let factions = ecs.read_storage::(); let ancestries = ecs.read_storage::(); // Check viewshed for monsters nearby. If we can see a monster, we can't heal. let viewshed = viewsheds.get_mut(*player_entity).unwrap(); for tile in viewshed.visible_tiles.iter() { let idx = worldmap_resource.xy_idx(tile.x, tile.y); crate::spatial::for_each_tile_content(idx, |entity_id| { let result = crate::raws::get_reactions( *player_entity, entity_id, &factions, &ancestries, &crate::raws::RAWS.lock().unwrap() ); if result == Reaction::Attack { can_heal = false; } }); } // Dirty viewshed (so we search for hidden tiles whenever we wait) viewshed.dirty = true; // Check player's hunger state - if we're hungry or worse, we can't heal. let player_hunger_clock = hunger_clocks.get(*player_entity); if let Some(clock) = player_hunger_clock { match clock.state { HungerState::Hungry => { can_heal = false; } HungerState::Weak => { can_heal = false; } HungerState::Fainting => { can_heal = false; } _ => {} } } if can_heal { let mut health_components = ecs.write_storage::(); let pools = health_components.get_mut(*player_entity).unwrap(); let mut rng = ecs.write_resource::(); let roll = rng.roll_dice(1, 6); if roll == 6 && pools.hit_points.current < pools.hit_points.max { pools.hit_points.current += 1; } } gamelog::Logger::new().append("You wait a turn.").log(); return RunState::Ticking; } /* 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 = 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; } } } */