From 65564e6f4c69f45508254d2262fbb654b7199c0b Mon Sep 17 00:00:00 2001 From: Llywelwyn Date: Sun, 10 Sep 2023 23:59:09 +0100 Subject: [PATCH 01/50] readme update w/ link to blog --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index c314e4f..9127091 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,8 @@ check out the page in the header for the wasm version, pick [a release of your c this year for roguelikedev does the complete tutorial, i followed along with thebracket's [_roguelike tutorial - in rust_](https://bfnightly.bracketproductions.com). the notes i made during the sprint are being kept below for posterity - further changes since then are noted in [changelog.txt](https://github.com/Llywelwyn/rust-rl/blob/9150ed39e45bee536060cdc769d274e639021012/changelog.txt), and in the release notes. +i'm also working on translating over my progress into blog entries on my site @ [llyw.co.uk](https://llyw.co.uk/), with a larger focus on some of the more interesting implementation details. + ---
From 6d80b80a82d23ea58d5764462798fbb6f89e79fe Mon Sep 17 00:00:00 2001 From: Llywelwyn Date: Mon, 11 Sep 2023 00:01:34 +0100 Subject: [PATCH 02/50] adds some abstractions for readability --- src/ai/energy_system.rs | 77 ++++++++++++++++++++++------------------- 1 file changed, 42 insertions(+), 35 deletions(-) diff --git a/src/ai/energy_system.rs b/src/ai/energy_system.rs index 0a47580..4b79280 100644 --- a/src/ai/energy_system.rs +++ b/src/ai/energy_system.rs @@ -87,16 +87,8 @@ impl<'a> System<'a> for EnergySystem { &positions, !&confusion, ).join() { - let burden_modifier = if let Some(burden) = burdens.get(entity) { - match burden.level { - BurdenLevel::Burdened => SPEED_MOD_BURDENED, - BurdenLevel::Strained => SPEED_MOD_STRAINED, - BurdenLevel::Overloaded => SPEED_MOD_OVERLOADED, - } - } else { - 1.0 - }; - let overmap_mod = if map.overmap { SPEED_MOD_OVERMAP_TRAVEL } else { 1.0 }; + let burden_modifier = get_burden_modifier(&burdens, entity); + let overmap_mod = get_overmap_modifier(&map); // Every entity has a POTENTIAL equal to their speed. let mut energy_potential: i32 = ((energy.speed as f32) * burden_modifier * @@ -121,37 +113,52 @@ impl<'a> System<'a> for EnergySystem { // has enough energy, they take a turn and decrement their energy // by TURN_COST. If the current entity is the player, await input. if energy.current >= TURN_COST { - let mut my_turn = true; energy.current -= TURN_COST; if entity == *player { *runstate = RunState::AwaitingInput; - } else { - let distance = DistanceAlg::Pythagoras.distance2d( - *player_pos, - Point::new(pos.x, pos.y) - ); - if distance > 20.0 { - my_turn = false; - } + } else if cull_turn_by_distance(&player_pos, pos) { + continue; } - if my_turn { - turns.insert(entity, TakingTurn {}).expect("Unable to insert turn."); - if CONFIG.logging.log_ticks { - let name = if let Some(name) = names.get(entity) { - &name.name - } else { - "Unknown entity" - }; - console::log( - format!( - "ENERGY SYSTEM: {} granted a turn. [leftover energy: {}].", - name, - energy.current - ) - ); - } + turns.insert(entity, TakingTurn {}).expect("Unable to insert turn."); + if CONFIG.logging.log_ticks { + let name = if let Some(name) = names.get(entity) { + &name.name + } else { + "Unknown entity" + }; + console::log( + format!( + "ENERGY SYSTEM: {} granted a turn. [leftover energy: {}].", + name, + energy.current + ) + ); } } } } } + +fn get_burden_modifier(burdens: &ReadStorage, entity: Entity) -> f32 { + return if let Some(burden) = burdens.get(entity) { + match burden.level { + BurdenLevel::Burdened => SPEED_MOD_BURDENED, + BurdenLevel::Strained => SPEED_MOD_STRAINED, + BurdenLevel::Overloaded => SPEED_MOD_OVERLOADED, + } + } else { + 1.0 + }; +} + +fn get_overmap_modifier(map: &ReadExpect) -> f32 { + return if map.overmap { SPEED_MOD_OVERMAP_TRAVEL } else { 1.0 }; +} + +fn cull_turn_by_distance(player_pos: &Point, pos: &Position) -> bool { + let distance = DistanceAlg::Pythagoras.distance2d(*player_pos, Point::new(pos.x, pos.y)); + if distance > 20.0 { + return true; + } + return false; +} From 27c1fe9a4875e5909b862f5b0fe2fb328ab030c0 Mon Sep 17 00:00:00 2001 From: Llywelwyn Date: Mon, 18 Sep 2023 21:54:18 +0100 Subject: [PATCH 03/50] cleans up linter warns --- src/ai/energy_system.rs | 2 +- src/ai/turn_status_system.rs | 2 +- src/damage_system.rs | 2 +- src/data/events.rs | 28 ++++++++++++------------ src/data/messages.rs | 16 +++++++------- src/effects/damage.rs | 24 ++++++++++---------- src/effects/triggers.rs | 11 +++++----- src/gamelog/events.rs | 22 +++++++++---------- src/inventory/identification_system.rs | 13 +++++++++-- src/main.rs | 2 +- src/map_builders/area_starting_points.rs | 1 - src/map_builders/mod.rs | 2 +- src/player.rs | 4 ++-- src/states/state.rs | 6 ++--- 14 files changed, 71 insertions(+), 64 deletions(-) diff --git a/src/ai/energy_system.rs b/src/ai/energy_system.rs index 4b79280..8ddbe4b 100644 --- a/src/ai/energy_system.rs +++ b/src/ai/energy_system.rs @@ -68,7 +68,7 @@ impl<'a> System<'a> for EnergySystem { .insert(entity, TakingTurn {}) .expect("Unable to insert turn for turn counter."); energy.current -= TURN_COST; - crate::gamelog::record_event(EVENT::TURN(1)); + crate::gamelog::record_event(EVENT::Turn(1)); // Handle spawning mobs each turn if CONFIG.logging.log_ticks { console::log( diff --git a/src/ai/turn_status_system.rs b/src/ai/turn_status_system.rs index 8a0dfb1..db3acaa 100644 --- a/src/ai/turn_status_system.rs +++ b/src/ai/turn_status_system.rs @@ -99,7 +99,7 @@ impl<'a> System<'a> for TurnStatusSystem { .colour(WHITE) .append("are confused!"); log = true; - gamelog::record_event(EVENT::PLAYER_CONFUSED(1)); + gamelog::record_event(EVENT::PlayerConfused(1)); } else { logger = logger .append("The") diff --git a/src/damage_system.rs b/src/damage_system.rs index 3f32f1c..a4224a7 100644 --- a/src/damage_system.rs +++ b/src/damage_system.rs @@ -81,7 +81,7 @@ pub fn delete_the_dead(ecs: &mut World) { } // For everything that died, increment the event log, and delete. for victim in dead { - gamelog::record_event(events::EVENT::TURN(1)); + gamelog::record_event(events::EVENT::Turn(1)); ecs.delete_entity(victim).expect("Unable to delete."); } } diff --git a/src/data/events.rs b/src/data/events.rs index 9f1aba8..74fa459 100644 --- a/src/data/events.rs +++ b/src/data/events.rs @@ -2,17 +2,17 @@ use serde::{ Deserialize, Serialize }; #[derive(Serialize, Deserialize, Clone)] pub enum EVENT { - TURN(i32), - LEVEL(i32), - CHANGED_FLOOR(String), - PLAYER_CONFUSED(i32), - KICKED_SOMETHING(i32), - BROKE_DOOR(i32), - LOOKED_FOR_HELP(i32), - KILLED(String), - PLAYER_DIED(String), - DISCOVERED(String), - IDENTIFIED(String), + Turn(i32), + Level(i32), + ChangedFloor(String), + PlayerConfused(i32), + KickedSomething(i32), + BrokeDoor(i32), + LookedForHelp(i32), + Killed(String), + PlayerDied(String), + Discovered(String), + Identified(String), } impl EVENT { @@ -20,8 +20,8 @@ impl EVENT { pub const COUNT_KILLED: &str = "killed"; pub const COUNT_LEVEL: &str = "level"; pub const COUNT_CHANGED_FLOOR: &str = "changed_floor"; - pub const COUNT_BROKE_DOOR: &str = "broke_door"; - pub const COUNT_PLAYER_CONFUSED: &str = "player_confused"; + pub const COUNT_BROKE_DOOR: &str = "BrokeDoor"; + pub const COUNT_PLAYER_CONFUSED: &str = "PlayerConfused"; pub const COUNT_KICK: &str = "kick"; - pub const COUNT_LOOKED_FOR_HELP: &str = "looked_for_help"; + pub const COUNT_LOOKED_FOR_HELP: &str = "LookedForHelp"; } diff --git a/src/data/messages.rs b/src/data/messages.rs index 5847c14..78a66d9 100644 --- a/src/data/messages.rs +++ b/src/data/messages.rs @@ -31,15 +31,15 @@ pub const YOU_REMOVE_ITEM: &str = "You unequip your"; pub const YOU_REMOVE_ITEM_CURSED: &str = "You can't remove the"; /// Prefixes death message. -pub const PLAYER_DIED: &str = "You died!"; -/// Death message specifiers. Appended after PLAYER_DIED. -pub const PLAYER_DIED_SUICIDE: &str = "You killed yourself"; -pub const PLAYER_DIED_NAMED_ATTACKER: &str = "You were killed by"; -pub const PLAYER_DIED_UNKNOWN: &str = "You were killed"; // Ultimately, this should never be used. Slowly include specific messages for any death. +pub const PlayerDied: &str = "You died!"; +/// Death message specifiers. Appended after PlayerDied. +pub const PlayerDied_SUICIDE: &str = "You killed yourself"; +pub const PlayerDied_NAMED_ATTACKER: &str = "You were killed by"; +pub const PlayerDied_UNKNOWN: &str = "You were killed"; // Ultimately, this should never be used. Slowly include specific messages for any death. /// Death message addendums. Appended at end of death message. -pub const PLAYER_DIED_ADDENDUM_FIRST: &str = " "; -pub const PLAYER_DIED_ADDENDUM_MID: &str = ", "; -pub const PLAYER_DIED_ADDENDUM_LAST: &str = ", and "; +pub const PlayerDied_ADDENDUM_FIRST: &str = " "; +pub const PlayerDied_ADDENDUM_MID: &str = ", "; +pub const PlayerDied_ADDENDUM_LAST: &str = ", and "; pub const STATUS_CONFUSED_STRING: &str = "confused"; pub const STATUS_BLIND_STRING: &str = "blinded"; // Results in something like: "You died! You were killed by a kobold captain, whilst confused." diff --git a/src/effects/damage.rs b/src/effects/damage.rs index 8285007..2b30e19 100644 --- a/src/effects/damage.rs +++ b/src/effects/damage.rs @@ -170,16 +170,16 @@ fn get_next_level_requirement(level: i32) -> i32 { fn get_death_message(ecs: &World, source: Entity) -> String { let player = ecs.fetch::(); - let mut result: String = format!("{} ", PLAYER_DIED); + let mut result: String = format!("{} ", PlayerDied); // If we killed ourselves, if source == *player { - result.push_str(format!("{}", PLAYER_DIED_SUICIDE).as_str()); + result.push_str(format!("{}", PlayerDied_SUICIDE).as_str()); } else if let Some(name) = ecs.read_storage::().get(source) { result.push_str( - format!("{} {}", PLAYER_DIED_NAMED_ATTACKER, with_article(name.name.clone())).as_str() + format!("{} {}", PlayerDied_NAMED_ATTACKER, with_article(name.name.clone())).as_str() ); } else { - result.push_str(format!("{}", PLAYER_DIED_UNKNOWN).as_str()); + result.push_str(format!("{}", PlayerDied_UNKNOWN).as_str()); } // Status effects { @@ -194,11 +194,11 @@ fn get_death_message(ecs: &World, source: Entity) -> String { result.push_str(" whilst"); for (i, addendum) in addendums.iter().enumerate() { if i == 0 { - result.push_str(format!("{}{}", PLAYER_DIED_ADDENDUM_FIRST, addendum).as_str()); + result.push_str(format!("{}{}", PlayerDied_ADDENDUM_FIRST, addendum).as_str()); } else if i == addendums.len() { - result.push_str(format!("{}{}", PLAYER_DIED_ADDENDUM_LAST, addendum).as_str()); + result.push_str(format!("{}{}", PlayerDied_ADDENDUM_LAST, addendum).as_str()); } else { - result.push_str(format!("{}{}", PLAYER_DIED_ADDENDUM_MID, addendum).as_str()); + result.push_str(format!("{}{}", PlayerDied_ADDENDUM_MID, addendum).as_str()); } } } @@ -221,12 +221,12 @@ pub fn entity_death(ecs: &mut World, effect: &EffectSpawner, target: Entity) { if let Some(source) = effect.source { // If the target was the player, game over, and record source of death. if target == *player { - gamelog::record_event(EVENT::PLAYER_DIED(get_death_message(ecs, source))); + gamelog::record_event(EVENT::PlayerDied(get_death_message(ecs, source))); return; } else { // If the player was the source, record the kill. if let Some(tar_name) = names.get(target) { - gamelog::record_event(EVENT::KILLED(tar_name.name.clone())); + gamelog::record_event(EVENT::Killed(tar_name.name.clone())); } } // Calc XP value of target. @@ -246,7 +246,7 @@ pub fn entity_death(ecs: &mut World, effect: &EffectSpawner, target: Entity) { source_pools.level += 1; // If it was the PLAYER that levelled up: if ecs.read_storage::().get(source).is_some() { - gamelog::record_event(EVENT::LEVEL(1)); + gamelog::record_event(EVENT::Level(1)); gamelog::Logger ::new() .append(LEVELUP_PLAYER) @@ -328,11 +328,11 @@ pub fn entity_death(ecs: &mut World, effect: &EffectSpawner, target: Entity) { if target == *player { if let Some(hc) = ecs.read_storage::().get(target) { if hc.state == HungerState::Starving { - gamelog::record_event(EVENT::PLAYER_DIED("You starved to death!".to_string())); + gamelog::record_event(EVENT::PlayerDied("You starved to death!".to_string())); } } else { gamelog::record_event( - EVENT::PLAYER_DIED("You died from unknown causes!".to_string()) + EVENT::PlayerDied("You died from unknown causes!".to_string()) ); } } diff --git a/src/effects/triggers.rs b/src/effects/triggers.rs index 91273e3..7b98241 100644 --- a/src/effects/triggers.rs +++ b/src/effects/triggers.rs @@ -30,7 +30,6 @@ use crate::{ SingleActivation, BUC, GrantsSpell, - KnownSpell, KnownSpells, Position, Viewshed, @@ -175,9 +174,9 @@ fn handle_grant_spell( event: &mut EventInfo, mut logger: gamelog::Logger ) -> (gamelog::Logger, bool) { - if let Some(granted_spell) = ecs.read_storage::().get(event.entity) { + if let Some(_granted_spell) = ecs.read_storage::().get(event.entity) { if - let Some(known_spells) = ecs + let Some(_known_spells) = ecs .write_storage::() .get_mut(event.source.unwrap()) { @@ -349,10 +348,10 @@ fn handle_identify( .get(event.source.unwrap()) .map(|b| b.known) .unwrap_or(true); - return ( + let result = in_this_backpack && - (has_obfuscated_name || !already_identified || !known_beatitude) - ); + (has_obfuscated_name || !already_identified || !known_beatitude); + return result; }) { to_identify.push((e, name.name.clone())); } diff --git a/src/gamelog/events.rs b/src/gamelog/events.rs index 0ef2d40..69b353c 100644 --- a/src/gamelog/events.rs +++ b/src/gamelog/events.rs @@ -73,12 +73,12 @@ pub fn record_event(event: EVENT) { let mut new_event: String = "unknown event".to_string(); let mut significant_event = true; match event { - EVENT::TURN(n) => { + EVENT::Turn(n) => { modify_event_count(EVENT::COUNT_TURN, n); significant_event = false; } // If de-levelling is ever implemented, this needs refactoring (along with a lot of stuff). - EVENT::LEVEL(n) => { + EVENT::Level(n) => { modify_event_count(EVENT::COUNT_LEVEL, n); let new_lvl = get_event_count(EVENT::COUNT_LEVEL); if new_lvl == 1 { @@ -87,7 +87,7 @@ pub fn record_event(event: EVENT) { new_event = format!("Advanced to level {}", new_lvl); } } - EVENT::CHANGED_FLOOR(n) => { + EVENT::ChangedFloor(n) => { modify_event_count(EVENT::COUNT_CHANGED_FLOOR, 1); if VISITED.lock().unwrap().contains(&n) { significant_event = false; @@ -96,23 +96,23 @@ pub fn record_event(event: EVENT) { new_event = format!("Visited {} for the first time", n); } } - EVENT::KICKED_SOMETHING(n) => { + EVENT::KickedSomething(n) => { modify_event_count(EVENT::COUNT_KICK, n); significant_event = false; } - EVENT::BROKE_DOOR(n) => { + EVENT::BrokeDoor(n) => { modify_event_count(EVENT::COUNT_BROKE_DOOR, n); significant_event = false; } - EVENT::PLAYER_CONFUSED(n) => { + EVENT::PlayerConfused(n) => { modify_event_count(EVENT::COUNT_PLAYER_CONFUSED, n); significant_event = false; } - EVENT::LOOKED_FOR_HELP(n) => { + EVENT::LookedForHelp(n) => { modify_event_count(EVENT::COUNT_LOOKED_FOR_HELP, n); significant_event = false; } - EVENT::KILLED(name) => { + EVENT::Killed(name) => { modify_event_count(EVENT::COUNT_KILLED, 1); if KILLED.lock().unwrap().contains(&name) { significant_event = false; @@ -121,13 +121,13 @@ pub fn record_event(event: EVENT) { new_event = format!("Killed your first {}", name); } } - EVENT::DISCOVERED(name) => { + EVENT::Discovered(name) => { new_event = format!("Discovered {}", name); } - EVENT::IDENTIFIED(name) => { + EVENT::Identified(name) => { new_event = format!("Identified {}", name); } - EVENT::PLAYER_DIED(str) => { + EVENT::PlayerDied(str) => { // Generating the String is handled in the death effect, to avoid passing the ecs here. new_event = format!("{}", str); } diff --git a/src/inventory/identification_system.rs b/src/inventory/identification_system.rs index 3fbe579..ce4ad23 100644 --- a/src/inventory/identification_system.rs +++ b/src/inventory/identification_system.rs @@ -1,4 +1,13 @@ -use crate::{ Beatitude, IdentifiedBeatitude, IdentifiedItem, Item, MasterDungeonMap, Name, ObfuscatedName, Player }; +use crate::{ + Beatitude, + IdentifiedBeatitude, + IdentifiedItem, + Item, + MasterDungeonMap, + Name, + ObfuscatedName, + Player, +}; use specs::prelude::*; use crate::data::events::*; use crate::gamelog; @@ -35,7 +44,7 @@ impl<'a> System<'a> for ItemIdentificationSystem { let tag = crate::raws::get_id_from_name(id.name.clone()); if !dm.identified_items.contains(&id.name) && crate::raws::is_tag_magic(&tag) { if gamelog::get_event_count(EVENT::COUNT_TURN) != 1 { - gamelog::record_event(EVENT::IDENTIFIED(id.name.clone())); + gamelog::record_event(EVENT::Identified(id.name.clone())); } dm.identified_items.insert(id.name.clone()); for (entity, _item, name) in (&entities, &items, &names).join() { diff --git a/src/main.rs b/src/main.rs index 63a90cf..0bf74dc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -129,7 +129,7 @@ fn main() -> BError { gs.ecs.insert(rex_assets::RexAssets::new()); gamelog::setup_log(); - gamelog::record_event(data::events::EVENT::LEVEL(1)); + gamelog::record_event(data::events::EVENT::Level(1)); gs.generate_world_map(1, TileType::Floor); main_loop(context, gs) diff --git a/src/map_builders/area_starting_points.rs b/src/map_builders/area_starting_points.rs index a416ce8..c251164 100644 --- a/src/map_builders/area_starting_points.rs +++ b/src/map_builders/area_starting_points.rs @@ -1,6 +1,5 @@ use super::{ BuilderMap, MetaMapBuilder, Position }; use bracket_lib::prelude::*; -use bracket_lib::prelude::*; #[allow(dead_code)] pub enum XStart { diff --git a/src/map_builders/mod.rs b/src/map_builders/mod.rs index b2cd069..edebe82 100644 --- a/src/map_builders/mod.rs +++ b/src/map_builders/mod.rs @@ -70,7 +70,7 @@ use forest::forest_builder; mod foliage; use foliage::Foliage; mod room_themer; -use room_themer::{ Theme, ThemeRooms }; +use room_themer::ThemeRooms; // Shared data to be passed around build chain pub struct BuilderMap { diff --git a/src/player.rs b/src/player.rs index 019df2e..1849b75 100644 --- a/src/player.rs +++ b/src/player.rs @@ -350,7 +350,7 @@ pub fn kick(i: i32, j: i32, ecs: &mut World) -> RunState { destroyed_pos = Some( Point::new(pos.x + delta_x, pos.y + delta_y) ); - gamelog::record_event(EVENT::BROKE_DOOR(1)); + gamelog::record_event(EVENT::BrokeDoor(1)); return false; // 66% chance of just kicking it. } else { @@ -414,7 +414,7 @@ pub fn kick(i: i32, j: i32, ecs: &mut World) -> RunState { ecs.delete_entity(destroyed_thing).expect("Unable to delete."); } - gamelog::record_event(EVENT::KICKED_SOMETHING(1)); + gamelog::record_event(EVENT::KickedSomething(1)); return RunState::Ticking; } diff --git a/src/states/state.rs b/src/states/state.rs index 2330461..0217cca 100644 --- a/src/states/state.rs +++ b/src/states/state.rs @@ -128,7 +128,7 @@ impl State { .colour(WHITE) .period() .log(); - gamelog::record_event(EVENT::CHANGED_FLOOR(mapname)); + gamelog::record_event(EVENT::ChangedFloor(mapname)); } fn game_over_cleanup(&mut self) { @@ -152,7 +152,7 @@ impl State { self.generate_world_map(1, TileType::Floor); gamelog::setup_log(); - gamelog::record_event(EVENT::LEVEL(1)); + gamelog::record_event(EVENT::Level(1)); } } @@ -513,7 +513,7 @@ impl GameState for State { let result = gui::show_help(ctx); match result { gui::YesNoResult::Yes => { - gamelog::record_event(EVENT::LOOKED_FOR_HELP(1)); + gamelog::record_event(EVENT::LookedForHelp(1)); new_runstate = RunState::AwaitingInput; } _ => {} From c4aa3de640a3e5bf3fd9148e04143decacba319b Mon Sep 17 00:00:00 2001 From: Llywelwyn Date: Mon, 18 Sep 2023 21:54:29 +0100 Subject: [PATCH 04/50] more linter clean-up --- src/ai/approach_ai_system.rs | 2 -- src/data/messages.rs | 14 +++++++------- src/effects/damage.rs | 14 +++++++------- 3 files changed, 14 insertions(+), 16 deletions(-) diff --git a/src/ai/approach_ai_system.rs b/src/ai/approach_ai_system.rs index 30ab222..c3cc2ca 100644 --- a/src/ai/approach_ai_system.rs +++ b/src/ai/approach_ai_system.rs @@ -7,7 +7,6 @@ pub struct ApproachAI {} impl<'a> System<'a> for ApproachAI { #[allow(clippy::type_complexity)] type SystemData = ( - WriteExpect<'a, RandomNumberGenerator>, WriteStorage<'a, TakingTurn>, WriteStorage<'a, WantsToApproach>, WriteStorage<'a, Position>, @@ -20,7 +19,6 @@ impl<'a> System<'a> for ApproachAI { fn run(&mut self, data: Self::SystemData) { let ( - mut rng, mut turns, mut wants_to_approach, mut positions, diff --git a/src/data/messages.rs b/src/data/messages.rs index 78a66d9..89e39c8 100644 --- a/src/data/messages.rs +++ b/src/data/messages.rs @@ -31,15 +31,15 @@ pub const YOU_REMOVE_ITEM: &str = "You unequip your"; pub const YOU_REMOVE_ITEM_CURSED: &str = "You can't remove the"; /// Prefixes death message. -pub const PlayerDied: &str = "You died!"; +pub const PLAYER_DIED: &str = "You died!"; /// Death message specifiers. Appended after PlayerDied. -pub const PlayerDied_SUICIDE: &str = "You killed yourself"; -pub const PlayerDied_NAMED_ATTACKER: &str = "You were killed by"; -pub const PlayerDied_UNKNOWN: &str = "You were killed"; // Ultimately, this should never be used. Slowly include specific messages for any death. +pub const PLAYER_DIED_SUICIDE: &str = "You killed yourself"; +pub const PLAYER_DIED_NAMED_ATTACKER: &str = "You were killed by"; +pub const PLAYER_DIED_UNKNOWN: &str = "You were killed"; // Ultimately, this should never be used. Slowly include specific messages for any death. /// Death message addendums. Appended at end of death message. -pub const PlayerDied_ADDENDUM_FIRST: &str = " "; -pub const PlayerDied_ADDENDUM_MID: &str = ", "; -pub const PlayerDied_ADDENDUM_LAST: &str = ", and "; +pub const PLAYER_DIED_ADDENDUM_FIRST: &str = " "; +pub const PLAYER_DIED_ADDENDUM_MID: &str = ", "; +pub const PLAYER_DIED_ADDENDUM_LAST: &str = ", and "; pub const STATUS_CONFUSED_STRING: &str = "confused"; pub const STATUS_BLIND_STRING: &str = "blinded"; // Results in something like: "You died! You were killed by a kobold captain, whilst confused." diff --git a/src/effects/damage.rs b/src/effects/damage.rs index 2b30e19..74ce8f5 100644 --- a/src/effects/damage.rs +++ b/src/effects/damage.rs @@ -170,16 +170,16 @@ fn get_next_level_requirement(level: i32) -> i32 { fn get_death_message(ecs: &World, source: Entity) -> String { let player = ecs.fetch::(); - let mut result: String = format!("{} ", PlayerDied); + let mut result: String = format!("{} ", PLAYER_DIED); // If we killed ourselves, if source == *player { - result.push_str(format!("{}", PlayerDied_SUICIDE).as_str()); + result.push_str(format!("{}", PLAYER_DIED_SUICIDE).as_str()); } else if let Some(name) = ecs.read_storage::().get(source) { result.push_str( - format!("{} {}", PlayerDied_NAMED_ATTACKER, with_article(name.name.clone())).as_str() + format!("{} {}", PLAYER_DIED_NAMED_ATTACKER, with_article(name.name.clone())).as_str() ); } else { - result.push_str(format!("{}", PlayerDied_UNKNOWN).as_str()); + result.push_str(format!("{}", PLAYER_DIED_UNKNOWN).as_str()); } // Status effects { @@ -194,11 +194,11 @@ fn get_death_message(ecs: &World, source: Entity) -> String { result.push_str(" whilst"); for (i, addendum) in addendums.iter().enumerate() { if i == 0 { - result.push_str(format!("{}{}", PlayerDied_ADDENDUM_FIRST, addendum).as_str()); + result.push_str(format!("{}{}", PLAYER_DIED_ADDENDUM_FIRST, addendum).as_str()); } else if i == addendums.len() { - result.push_str(format!("{}{}", PlayerDied_ADDENDUM_LAST, addendum).as_str()); + result.push_str(format!("{}{}", PLAYER_DIED_ADDENDUM_LAST, addendum).as_str()); } else { - result.push_str(format!("{}{}", PlayerDied_ADDENDUM_MID, addendum).as_str()); + result.push_str(format!("{}{}", PLAYER_DIED_ADDENDUM_MID, addendum).as_str()); } } } From 583afa7078054825ef9d495ddb0354624f7d5844 Mon Sep 17 00:00:00 2001 From: Llywelwyn Date: Wed, 20 Sep 2023 13:14:56 +0100 Subject: [PATCH 05/50] mobs: insects --- docs/ascii_encyclopedia.txt | 2 +- raws/mobs.json | 51 +++++++++++++++++++++++++++++++++++++ raws/spawn_tables.json | 4 +++ 3 files changed, 56 insertions(+), 1 deletion(-) diff --git a/docs/ascii_encyclopedia.txt b/docs/ascii_encyclopedia.txt index db60dbc..2972c1e 100644 --- a/docs/ascii_encyclopedia.txt +++ b/docs/ascii_encyclopedia.txt @@ -1,4 +1,4 @@ -a - A - +a - insects A - b - B - c - chickens C - d - canines D - diff --git a/raws/mobs.json b/raws/mobs.json index 5137ea4..75ee1ed 100644 --- a/raws/mobs.json +++ b/raws/mobs.json @@ -326,6 +326,57 @@ "attacks": [{ "name": "bites", "hit_bonus": 0, "damage": "1d2" }], "loot": { "table": "scrolls", "chance": 0.05 } }, + { + "id": "ant_worker", + "name": "worker ant", + "renderable": { "glyph": "a", "fg": "#ca7631", "bg": "#000000", "order": 1 }, + "flags": ["SMALL_GROUP"], + "level": 2, + "bac": 3, + "speed": 18, + "vision_range": 16, + "attacks": [{ "name": "bites", "hit_bonus": 0, "damage": "1d4" }], + "loot": { "table": "food", "chance": 0.05 } + }, + { + "id": "ant_soldier", + "name": "soldier ant", + "renderable": { "glyph": "a", "fg": "#ca3f26", "bg": "#000000", "order": 1 }, + "flags": ["SMALL_GROUP"], + "level": 3, + "bac": 3, + "speed": 18, + "vision_range": 16, + "attacks": [ + { "name": "bites", "hit_bonus": 0, "damage": "2d4" }, + { "name": "stings", "hit_bonus": 0, "damage": "3d4" } + ], + "loot": { "table": "food", "chance": 0.05 } + }, + { + "id": "caterpillar_cave", + "name": "caterpillar", + "renderable": { "glyph": "a", "fg": "#6b6b6b", "bg": "#000000", "order": 1 }, + "flags": ["SMALL_GROUP"], + "level": 1, + "bac": 3, + "speed": 9, + "vision_range": 16, + "attacks": [{ "name": "bites", "hit_bonus": 0, "damage": "1d3" }], + "loot": { "table": "food", "chance": 0.05 } + }, + { + "id": "caterpillar_giant", + "name": "giant caterpillar", + "renderable": { "glyph": "a", "fg": "#b9aeae", "bg": "#000000", "order": 1 }, + "flags": ["SMALL_GROUP"], + "level": 2, + "bac": 7, + "speed": 9, + "vision_range": 16, + "attacks": [{ "name": "bites", "hit_bonus": 0, "damage": "1d6" }], + "loot": { "table": "food", "chance": 0.10 } + }, { "id": "jackal", "name": "jackal", diff --git a/raws/spawn_tables.json b/raws/spawn_tables.json index a1e8d3b..aeffbcd 100644 --- a/raws/spawn_tables.json +++ b/raws/spawn_tables.json @@ -91,6 +91,8 @@ { "id": "kobold_large", "weight": 1, "difficulty": 2}, { "id": "rat_giant", "weight": 2, "difficulty": 2}, { "id": "coyote", "weight": 4, "difficulty": 2}, + { "id": "caterpillar_cave", "weight": 2, "difficulty": 2}, + { "id": "caterpillar_giant", "weight": 2, "difficulty": 3}, { "id": "zombie_orc", "weight": 1, "difficulty": 3}, { "id": "zombie_dwarf", "weight": 1, "difficulty": 3}, { "id": "gnome", "weight": 1, "difficulty": 3}, @@ -102,8 +104,10 @@ { "id": "dwarf", "weight": 3, "difficulty": 4}, { "id": "orc_hill", "weight": 1, "difficulty": 4}, { "id": "horse_little", "weight": 2, "difficulty": 4}, + { "id": "ant_worker", "weight": 3, "difficulty": 4}, { "id": "dog", "weight": 1, "difficulty": 5}, { "id": "wolf", "weight": 2, "difficulty": 6}, + { "id": "ant_soldier", "weight": 2, "difficulty": 6}, { "id": "orc_captain", "weight": 1, "difficulty": 7}, { "id": "dog_large", "weight": 1, "difficulty": 7}, { "id": "horse", "weight": 2, "difficulty": 7}, From 421c87c97231774c75b1178a3883fdab5f1186a6 Mon Sep 17 00:00:00 2001 From: Llywelwyn Date: Wed, 20 Sep 2023 14:51:16 +0100 Subject: [PATCH 06/50] mobs: wargs, felines --- raws/mobs.json | 60 ++++++++++++++++++++++++++++++++++++++++++ raws/spawn_tables.json | 4 +++ 2 files changed, 64 insertions(+) diff --git a/raws/mobs.json b/raws/mobs.json index 75ee1ed..bc860e3 100644 --- a/raws/mobs.json +++ b/raws/mobs.json @@ -462,6 +462,66 @@ ], "loot": { "table": "equipment", "chance": 0.05 } }, + { + "id": "warg", + "name": "warg", + "renderable": { "glyph": "d", "fg": "#8b7164", "bg": "#000000", "order": 1 }, + "flags": ["SMALL_GROUP"], + "level": 7, + "bac": 4, + "speed": 12, + "vision_range": 16, + "attacks": [{ "name": "bites", "hit_bonus": 0, "damage": "2d6" }], + "loot": { "table": "food", "chance": 0.05 } + }, + { + "id": "jaguar", + "name": "jaguar", + "renderable": { "glyph": "f", "fg": "#d3b947", "bg": "#000000", "order": 1 }, + "flags": [], + "level": 4, + "bac": 6, + "speed": 15, + "vision_range": 16, + "attacks": [ + { "name": "claws", "hit_bonus": 0, "damage": "1d4" }, + { "name": "claws", "hit_bonus": 0, "damage": "1d4" }, + { "name": "bites", "hit_bonus": 0, "damage": "1d8" }, + ], + "loot": { "table": "food", "chance": 0.05 } + }, + { + "id": "lynx", + "name": "lynx", + "renderable": { "glyph": "f", "fg": "#b5d347", "bg": "#000000", "order": 1 }, + "flags": [], + "level": 5, + "bac": 6, + "speed": 15, + "vision_range": 16, + "attacks": [ + { "name": "claws", "hit_bonus": 0, "damage": "1d4" }, + { "name": "claws", "hit_bonus": 0, "damage": "1d4" }, + { "name": "bites", "hit_bonus": 0, "damage": "1d10" }, + ], + "loot": { "table": "food", "chance": 0.05 } + }, + { + "id": "panther", + "name": "panther", + "renderable": { "glyph": "f", "fg": "#58554e", "bg": "#000000", "order": 1 }, + "flags": [], + "level": 5, + "bac": 6, + "speed": 15, + "vision_range": 16, + "attacks": [ + { "name": "claws", "hit_bonus": 0, "damage": "1d6" }, + { "name": "claws", "hit_bonus": 0, "damage": "1d6" }, + { "name": "bites", "hit_bonus": 0, "damage": "1d10" }, + ], + "loot": { "table": "food", "chance": 0.05 } + }, { "id": "ogre", "name": "ogre", diff --git a/raws/spawn_tables.json b/raws/spawn_tables.json index aeffbcd..b624a80 100644 --- a/raws/spawn_tables.json +++ b/raws/spawn_tables.json @@ -107,11 +107,15 @@ { "id": "ant_worker", "weight": 3, "difficulty": 4}, { "id": "dog", "weight": 1, "difficulty": 5}, { "id": "wolf", "weight": 2, "difficulty": 6}, + { "id": "jaguar", "weight": 2, "difficulty": 6}, { "id": "ant_soldier", "weight": 2, "difficulty": 6}, { "id": "orc_captain", "weight": 1, "difficulty": 7}, { "id": "dog_large", "weight": 1, "difficulty": 7}, + { "id": "lynx", "weight": 1, "difficulty": 7}, + { "id": "panther", "weight": 1, "difficulty": 7}, { "id": "horse", "weight": 2, "difficulty": 7}, { "id": "ogre", "weight": 1, "difficulty": 7}, + { "id": "warg", "weight": 2, "difficulty": 8}, { "id": "horse_large", "weight": 2, "difficulty": 9} ] }, From c4a87d98139a7800eab661e9150191926bdc1455 Mon Sep 17 00:00:00 2001 From: Llywelwyn Date: Wed, 20 Sep 2023 20:25:29 +0100 Subject: [PATCH 07/50] removes trailing commas --- raws/mobs.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/raws/mobs.json b/raws/mobs.json index bc860e3..a843058 100644 --- a/raws/mobs.json +++ b/raws/mobs.json @@ -486,7 +486,7 @@ "attacks": [ { "name": "claws", "hit_bonus": 0, "damage": "1d4" }, { "name": "claws", "hit_bonus": 0, "damage": "1d4" }, - { "name": "bites", "hit_bonus": 0, "damage": "1d8" }, + { "name": "bites", "hit_bonus": 0, "damage": "1d8" } ], "loot": { "table": "food", "chance": 0.05 } }, @@ -502,7 +502,7 @@ "attacks": [ { "name": "claws", "hit_bonus": 0, "damage": "1d4" }, { "name": "claws", "hit_bonus": 0, "damage": "1d4" }, - { "name": "bites", "hit_bonus": 0, "damage": "1d10" }, + { "name": "bites", "hit_bonus": 0, "damage": "1d10" } ], "loot": { "table": "food", "chance": 0.05 } }, @@ -518,7 +518,7 @@ "attacks": [ { "name": "claws", "hit_bonus": 0, "damage": "1d6" }, { "name": "claws", "hit_bonus": 0, "damage": "1d6" }, - { "name": "bites", "hit_bonus": 0, "damage": "1d10" }, + { "name": "bites", "hit_bonus": 0, "damage": "1d10" } ], "loot": { "table": "food", "chance": 0.05 } }, From 954991fd9c71df9afea64cedb15afcf8a7489991 Mon Sep 17 00:00:00 2001 From: Llywelwyn Date: Wed, 20 Sep 2023 20:33:05 +0100 Subject: [PATCH 08/50] defaults vision range in raws, only needs specifying if abnormal --- raws/mobs.json | 42 +++-------------------------------------- src/raws/mob_structs.rs | 2 +- src/raws/rawmaster.rs | 7 ++++++- 3 files changed, 10 insertions(+), 41 deletions(-) diff --git a/raws/mobs.json b/raws/mobs.json index a843058..651f4f2 100644 --- a/raws/mobs.json +++ b/raws/mobs.json @@ -62,7 +62,6 @@ "renderable": { "glyph": "@", "fg": "#034efc", "bg": "#000000", "order": 1 }, "flags": ["NEUTRAL", "RANDOM_PATH", "IS_HUMAN"], "level": 2, - "vision_range": 16, "attacks": [{ "name": "hits", "hit_bonus": 0, "damage": "1d8" }], "equipped": ["equip_shortsword", "equip_body_leather"], "quips": ["You wont catch me down the mine.", "Staying out of trouble?"] @@ -73,7 +72,6 @@ "renderable": { "glyph": "r", "fg": "#aa6000", "bg": "#000000", "order": 1 }, "flags": [], "bac": 6, - "vision_range": 16, "attacks": [{ "name": "bites", "hit_bonus": 0, "damage": "1d2" }], "loot": { "table": "food", "chance": 0.1 } }, @@ -83,7 +81,6 @@ "renderable": { "glyph": "c", "fg": "#BB6000", "bg": "#000000", "order": 1 }, "flags": ["HERBIVORE"], "bac": 8, - "vision_range": 16, "attacks": [{ "name": "bites", "hit_bonus": 0, "damage": "1d3" }] }, { @@ -92,7 +89,6 @@ "renderable": { "glyph": "q", "fg": "#a57037", "bg": "#000000", "order": 1 }, "flags": ["HERBIVORE"], "bac": 8, - "vision_range": 16, "attacks": [{ "name": "kicks", "hit_bonus": 0, "damage": "1d2" }] }, { @@ -101,7 +97,6 @@ "renderable": { "glyph": "q", "fg": "#e7e7e7", "bg": "#000000", "order": 1 }, "flags": ["HERBIVORE", "SMALL_GROUP"], "bac": 10, - "vision_range": 16, "attacks": [{ "name": "kicks", "hit_bonus": 0, "damage": "1d2" }] }, { @@ -110,7 +105,6 @@ "renderable": { "glyph": "c", "fg": "#fae478", "bg": "#000000", "order": 1 }, "flags": ["HERBIVORE"], "bac": 10, - "vision_range": 16, "attacks": [{ "name": "bites", "hit_bonus": 0, "damage": "1d2" }] }, { @@ -121,7 +115,6 @@ "level": 3, "bac": 6, "speed": 16, - "vision_range": 16, "attacks": [ { "name": "kicks", "hit_bonus": 0, "damage": "1d6" }, { "name": "bites", "hit_bonus": 0, "damage": "1d2" } @@ -136,7 +129,6 @@ "level": 5, "bac": 5, "speed": 20, - "vision_range": 16, "attacks": [ { "name": "kicks", "hit_bonus": 0, "damage": "1d8" }, { "name": "bites", "hit_bonus": 0, "damage": "1d3" } @@ -150,7 +142,6 @@ "level": 7, "bac": 4, "speed": 24, - "vision_range": 16, "attacks": [ { "name": "kicks", "hit_bonus": 0, "damage": "1d10" }, { "name": "bites", "hit_bonus": 0, "damage": "1d4" } @@ -163,7 +154,6 @@ "flags": ["SMALL_GROUP"], "level": 1, "bac": 7, - "vision_range": 16, "attacks": [{ "name": "bites", "hit_bonus": 0, "damage": "1d3" }], "loot": { "table": "scrolls", "chance": 0.05 } }, @@ -175,7 +165,6 @@ "level": 2, "bac": 6, "speed": 18, - "vision_range": 16, "quips": ["", "", ""], "attacks": [{ "name": "bites", "hit_bonus": 0, "damage": "1d6" }] }, @@ -187,7 +176,6 @@ "level": 4, "bac": 5, "speed": 16, - "vision_range": 16, "attacks": [{ "name": "bites", "hit_bonus": 0, "damage": "1d6" }] }, { @@ -198,7 +186,6 @@ "level": 6, "bac": 4, "speed": 15, - "vision_range": 16, "attacks": [{ "name": "bites", "hit_bonus": 0, "damage": "2d4" }] }, { @@ -208,7 +195,6 @@ "flags": ["SMALL_GROUP", "IS_GNOME"], "level": 1, "speed": 6, - "vision_range": 16, "attacks": [{ "name": "claws", "hit_bonus": 0, "damage": "1d6" }], "loot": { "table": "wands", "chance": 0.05 } }, @@ -230,7 +216,6 @@ "flags": [], "level": 1, "speed": 9, - "vision_range": 16, "attacks": [{ "name": "hits", "hit_bonus": 0, "damage": "1d4" }] }, { @@ -240,7 +225,6 @@ "flags": [], "level": 1, "speed": 6, - "vision_range": 16, "attacks": [{ "name": "hits", "hit_bonus": 0, "damage": "1d4" }], "loot": { "table": "food", "chance": 0.05 } }, @@ -286,7 +270,6 @@ "level": 2, "bac": 10, "speed": 6, - "vision_range": 16, "attacks": [{ "name": "hacks", "hit_bonus": 0, "damage": "1d8" }], "equipped": ["equip_feet_iron"], "loot": { "table": "equipment", "chance": 0.05 } @@ -322,7 +305,6 @@ "level": 1, "bac": 3, "speed": 12, - "vision_range": 16, "attacks": [{ "name": "bites", "hit_bonus": 0, "damage": "1d2" }], "loot": { "table": "scrolls", "chance": 0.05 } }, @@ -334,7 +316,6 @@ "level": 2, "bac": 3, "speed": 18, - "vision_range": 16, "attacks": [{ "name": "bites", "hit_bonus": 0, "damage": "1d4" }], "loot": { "table": "food", "chance": 0.05 } }, @@ -346,7 +327,6 @@ "level": 3, "bac": 3, "speed": 18, - "vision_range": 16, "attacks": [ { "name": "bites", "hit_bonus": 0, "damage": "2d4" }, { "name": "stings", "hit_bonus": 0, "damage": "3d4" } @@ -361,7 +341,6 @@ "level": 1, "bac": 3, "speed": 9, - "vision_range": 16, "attacks": [{ "name": "bites", "hit_bonus": 0, "damage": "1d3" }], "loot": { "table": "food", "chance": 0.05 } }, @@ -373,7 +352,6 @@ "level": 2, "bac": 7, "speed": 9, - "vision_range": 16, "attacks": [{ "name": "bites", "hit_bonus": 0, "damage": "1d6" }], "loot": { "table": "food", "chance": 0.10 } }, @@ -383,7 +361,6 @@ "renderable": { "glyph": "d", "fg": "#AA5500", "bg": "#000000", "order": 1 }, "flags": ["CARNIVORE", "SMALL_GROUP"], "bac": 7, - "vision_range": 16, "attacks": [{ "name": "bites", "hit_bonus": 0, "damage": "1d2" }] }, { @@ -392,7 +369,6 @@ "renderable": { "glyph": "d", "fg": "#FF0000", "bg": "#000000", "order": 1 }, "flags": ["CARNIVORE"], "bac": 7, - "vision_range": 16, "attacks": [{ "name": "bites", "hit_bonus": 0, "damage": "1d3" }] }, { @@ -402,7 +378,6 @@ "flags": ["CARNIVORE", "SMALL_GROUP"], "level": 1, "bac": 7, - "vision_range": 16, "attacks": [{ "name": "bites", "hit_bonus": 0, "damage": "1d4" }] }, { @@ -412,7 +387,6 @@ "flags": ["CARNIVORE"], "level": 5, "bac": 4, - "vision_range": 16, "attacks": [{ "name": "bites", "hit_bonus": 0, "damage": "2d4" }] }, { @@ -422,7 +396,6 @@ "flags": [], "level": 2, "speed": 9, - "vision_range": 16, "attacks": [{ "name": "hits", "hit_bonus": 0, "damage": "1d8" }], "loot": { "table": "wands", "chance": 0.05 } }, @@ -433,7 +406,6 @@ "flags": ["SMALL_GROUP"], "level": 1, "speed": 9, - "vision_range": 16, "attacks": [{ "name": "hits", "hit_bonus": 0, "damage": "1d6" }], "loot": { "table": "equipment", "chance": 0.05 } }, @@ -444,7 +416,6 @@ "flags": ["LARGE_GROUP"], "level": 2, "speed": 9, - "vision_range": 16, "attacks": [{ "name": "hits", "hit_bonus": 0, "damage": "1d6" }], "loot": { "table": "equipment", "chance": 0.05 } }, @@ -455,7 +426,6 @@ "flags": ["MULTIATTACK"], "level": 5, "speed": 5, - "vision_range": 16, "attacks": [ { "name": "hits", "hit_bonus": 0, "damage": "2d4" }, { "name": "hits", "hit_bonus": 0, "damage": "2d4" } @@ -470,7 +440,6 @@ "level": 7, "bac": 4, "speed": 12, - "vision_range": 16, "attacks": [{ "name": "bites", "hit_bonus": 0, "damage": "2d6" }], "loot": { "table": "food", "chance": 0.05 } }, @@ -478,11 +447,10 @@ "id": "jaguar", "name": "jaguar", "renderable": { "glyph": "f", "fg": "#d3b947", "bg": "#000000", "order": 1 }, - "flags": [], + "flags": ["MULTIATTACK"], "level": 4, "bac": 6, "speed": 15, - "vision_range": 16, "attacks": [ { "name": "claws", "hit_bonus": 0, "damage": "1d4" }, { "name": "claws", "hit_bonus": 0, "damage": "1d4" }, @@ -494,11 +462,10 @@ "id": "lynx", "name": "lynx", "renderable": { "glyph": "f", "fg": "#b5d347", "bg": "#000000", "order": 1 }, - "flags": [], + "flags": ["MULTIATTACK"], "level": 5, "bac": 6, "speed": 15, - "vision_range": 16, "attacks": [ { "name": "claws", "hit_bonus": 0, "damage": "1d4" }, { "name": "claws", "hit_bonus": 0, "damage": "1d4" }, @@ -510,11 +477,10 @@ "id": "panther", "name": "panther", "renderable": { "glyph": "f", "fg": "#58554e", "bg": "#000000", "order": 1 }, - "flags": [], + "flags": ["MULTIATTACK"], "level": 5, "bac": 6, "speed": 15, - "vision_range": 16, "attacks": [ { "name": "claws", "hit_bonus": 0, "damage": "1d6" }, { "name": "claws", "hit_bonus": 0, "damage": "1d6" }, @@ -530,7 +496,6 @@ "level": 5, "bac": 5, "speed": 10, - "vision_range": 16, "attacks": [{ "name": "hits", "hit_bonus": 0, "damage": "2d5" }], "loot": { "table": "food", "chance": 0.05 } }, @@ -542,7 +507,6 @@ "level": 2, "bac": 12, "speed": 3, - "vision_range": 16, "attacks": [{ "name": "lashes", "hit_bonus": 4, "damage": "1d8" }], "loot": { "table": "scrolls", "chance": 0.05 } } diff --git a/src/raws/mob_structs.rs b/src/raws/mob_structs.rs index cfd673b..854149b 100644 --- a/src/raws/mob_structs.rs +++ b/src/raws/mob_structs.rs @@ -14,7 +14,7 @@ pub struct Mob { pub attacks: Option>, pub attributes: Option, pub skills: Option>, - pub vision_range: i32, + pub vision_range: Option, pub telepathy_range: Option, pub equipped: Option>, pub loot: Option, diff --git a/src/raws/rawmaster.rs b/src/raws/rawmaster.rs index 66e9929..aaabfd1 100644 --- a/src/raws/rawmaster.rs +++ b/src/raws/rawmaster.rs @@ -5,6 +5,7 @@ use crate::gui::Ancestry; use crate::random_table::RandomTable; use crate::config::CONFIG; use crate::data::visuals::BLOODSTAIN_COLOUR; +use crate::data::entity::DEFAULT_VIEWSHED_STANDARD; use regex::Regex; use bracket_lib::prelude::*; use specs::prelude::*; @@ -387,7 +388,11 @@ pub fn spawn_named_mob( eb = eb.with(Name { name: mob_template.name.clone(), plural: mob_template.name.clone() }); eb = eb.with(Viewshed { visible_tiles: Vec::new(), - range: mob_template.vision_range as i32, + range: if let Some(range) = mob_template.vision_range { + range + } else { + DEFAULT_VIEWSHED_STANDARD + }, dirty: true, }); if let Some(telepath) = &mob_template.telepathy_range { From 335af8ee60d9b2b1a831d4bc8eb575eee5de6c6f Mon Sep 17 00:00:00 2001 From: Llywelwyn Date: Wed, 20 Sep 2023 21:31:59 +0100 Subject: [PATCH 09/50] cl to md + versioned, instead of dated --- changelog.md | 18 ++++++++++++++++++ changelog.txt | 22 ---------------------- 2 files changed, 18 insertions(+), 22 deletions(-) create mode 100644 changelog.md delete mode 100644 changelog.txt diff --git a/changelog.md b/changelog.md new file mode 100644 index 0000000..82b9191 --- /dev/null +++ b/changelog.md @@ -0,0 +1,18 @@ +## v0.1.4 +### added +- **overmap**: bare, but exists. player now starts on the overworld, and can move to local maps (like the old starting town) via >. can leave local maps back to the overmap by walking out of the map boundaries. +- **full keyboard support**: examining and targeting can now be done via keyboard only +- **a config file** read at runtime, unfortunately not compatible with WASM builds yet +- **morgue files**: y/n prompt to write a morgue file on death to /morgue/foo.txt, or to the console for WASM builds +- **dungeon features**: just the basics so far. a grassy, forested treant room, some barracks, etc. +- **named maps**: "Town", "Dungeon" +- **map messages/hints**: "You hear <...>." +### changed +- **colour offsets** are now per-tile (and per-theme) instead of +-% globally. i.e. varying fg/bg offset on a per-tiletype basis +- **chatlog colours** are now consistent +### fixed +- negative starting mana +- status effects only ticking if mob turn aligned with turnclock turn +- map params not being saved on map transition +- mob turns not awaiting the particle queue (mobs moving around mid-animation) +- mobs not re-pathing if their path was blocked, causing traffic jams diff --git a/changelog.txt b/changelog.txt deleted file mode 100644 index 3f8a6e2..0000000 --- a/changelog.txt +++ /dev/null @@ -1,22 +0,0 @@ -2-September-2023 -- fixes: - - negative starting mana - - confusion/status effects only being run if mob turn aligned with turnclock turn - -30-August-2023 -- added dungeon features: grassy forest room, and barracks variants (bunks, squads of mobtypes) -- added support for map messages: i.e. notifications for present dungeon features logged to chat every now and again - -Pre-29-August-2023 -- added overmap: bare, but exists. player now starts on the overworld, and can move to local maps (like the old starting town) via >. can leave local maps back to the overmap by walking out of the map boundaries. -- mouse begone: support still there if wanted, but targeting/e(x)amining can now be done via keyboard only -- added config.toml: non-wasm builds read from config.toml at runtime, or generate a new copy if not present in the exe dir. includes options for logging various details to the console, and visual choices like post-processing effects, all-black bgs vs. full-coloured, etc. -- improved morgue files: y/n prompt to write a morgue file on death (or write to console in the case of wasm), containing a map of the floor the player died on, class/race/attribute/etc. details, a fully identified backpack, and a list of significant events that took place this run w/ turn number -- refactored colour offsets: now per-tile (and per-theme), instead of global. now can include varying fg/bg offset for every type of tile. -- consistent chatlog colours: renderables for mobs, beatitude for items -- dungeon features: framework -- map identifiers (instead of displaying an incorrect depth) on ui: e.g. D1, D2, Town, Woods, etc. -- fixes: - - map params are saved on map transition, instead of only at creation. now bloodstains, vision, etc. will persist when changing between floors - - mob turns await an empty particle queue - no longer will they move mid-fireball animations - - fixed traffic jams - 1. mobs will calc the best path to any tile within range of their target, instead of trying to path directly onto the target tile, and 2. if saved path is blocked to a waypoint, mobs will recalc a new path to the same point \ No newline at end of file From 222c7cc914f0da5f0cd0cc97320b8e15f843ead3 Mon Sep 17 00:00:00 2001 From: Llywelwyn Date: Wed, 20 Sep 2023 21:37:05 +0100 Subject: [PATCH 10/50] dels redunant resources - most of these are built-in with bracket-lib --- resources/terminal10x10_gs_tc.png | Bin 8804 -> 0 bytes resources/terminal8x8.jpg | Bin 26024 -> 0 bytes resources/terminal_10x16.png | Bin 5699 -> 0 bytes resources/vga8x16.png | Bin 35939 -> 0 bytes 4 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 resources/terminal10x10_gs_tc.png delete mode 100644 resources/terminal8x8.jpg delete mode 100644 resources/terminal_10x16.png delete mode 100644 resources/vga8x16.png diff --git a/resources/terminal10x10_gs_tc.png b/resources/terminal10x10_gs_tc.png deleted file mode 100644 index 5e0cdc92fb3881ead60f4b856ee19c00d333855e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8804 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 e+JF5YBm<7<_#hv=|r|I2c$Nr5IQl7#J8C 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 U4>`+Q{`bnzKmQp5*#F-I02uf-3;+NC diff --git a/resources/terminal_10x16.png b/resources/terminal_10x16.png deleted file mode 100644 index e40aa9912a783bb00231c0fbf638aef0f14499f8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5699 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 diff --git a/resources/vga8x16.png b/resources/vga8x16.png deleted file mode 100644 index 913e32c313d8283d4929b05618ca9970bc65f153..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 35939 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 P3=9mOu6{1-oD!M Date: Wed, 20 Sep 2023 21:56:50 +0100 Subject: [PATCH 11/50] defaults placeholder player to human ancestry --- src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index 0bf74dc..53f6943 100644 --- a/src/main.rs +++ b/src/main.rs @@ -121,7 +121,7 @@ fn main() -> BError { gs.ecs.insert(map::MasterDungeonMap::new()); // Master map list gs.ecs.insert(Map::new(true, 1, 64, 64, 0, "New Map", "N", 0)); // Map gs.ecs.insert(Point::new(0, 0)); // Player pos - gs.ecs.insert(gui::Ancestry::Dwarf); // ancestry + gs.ecs.insert(gui::Ancestry::Human); // ancestry let player_entity = spawner::player(&mut gs.ecs, 0, 0); gs.ecs.insert(player_entity); // Player entity gs.ecs.insert(RunState::MapGeneration {}); // RunState From 58e4742f12e7d416660bf0f2979c285b137d8d78 Mon Sep 17 00:00:00 2001 From: Llywelwyn Date: Wed, 20 Sep 2023 21:56:57 +0100 Subject: [PATCH 12/50] more map tests --- tests/map_test.rs | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/tests/map_test.rs b/tests/map_test.rs index 8e8de74..336c4d8 100644 --- a/tests/map_test.rs +++ b/tests/map_test.rs @@ -47,3 +47,39 @@ fn tiletype_with_var_equality() { let tile3 = TileType::ToLocal(3); assert_eq!(tile2, tile3); } + +fn init_maps_for_tests() -> (MasterDungeonMap, Map, Map) { + let dm = MasterDungeonMap::new(); + let (overmap, difficulty, name, short_name, depth) = (false, 0, "Test Map", "Test Map", 0); + let map1 = Map::new(overmap, 1, 64, 64, difficulty, name, short_name, depth); + let map2 = Map::new(overmap, 2, 128, 128, difficulty, name, short_name, depth); + (dm, map1, map2) +} + +#[test] +fn map_saving() { + let (mut dm, map1, map2) = init_maps_for_tests(); + dm.store_map(&map1); + dm.store_map(&map2); +} + +#[test] +fn map_loading() { + let (mut dm, map1, map2) = init_maps_for_tests(); + dm.store_map(&map1); + let loaded_map1 = dm.get_map(map1.id).unwrap(); + assert_eq!(loaded_map1.overmap, map1.overmap); + assert_eq!(loaded_map1.id, map1.id); + assert_eq!(loaded_map1.width, map1.width); + assert_eq!(loaded_map1.height, map1.height); + assert_eq!(loaded_map1.difficulty, map1.difficulty); + assert_eq!(loaded_map1.name, map1.name); + assert_eq!(loaded_map1.short_name, map1.short_name); + assert_eq!(loaded_map1.depth, map1.depth); + assert_eq!(loaded_map1.tiles.len(), map1.tiles.len()); + assert_eq!(loaded_map1.messages, map1.messages); + dm.store_map(&map2); + let loaded_map2 = dm.get_map(map2.id).unwrap(); + assert_eq!(loaded_map2.width, map2.width); + assert_ne!(loaded_map2.width, map1.width); +} From 66013667d8dc0e70f1998e75642329282a50a866 Mon Sep 17 00:00:00 2001 From: Llywelwyn Date: Wed, 20 Sep 2023 23:21:38 +0100 Subject: [PATCH 13/50] gamelog events unit tests --- src/gamelog/events.rs | 7 ++++--- tests/gamelog_test.rs | 43 +++++++++++++++++++++++++++++++++++++++++++ tests/mod.rs | 1 + 3 files changed, 48 insertions(+), 3 deletions(-) create mode 100644 tests/gamelog_test.rs diff --git a/src/gamelog/events.rs b/src/gamelog/events.rs index 69b353c..bef5ff6 100644 --- a/src/gamelog/events.rs +++ b/src/gamelog/events.rs @@ -5,7 +5,7 @@ use crate::data::names::*; lazy_static! { /// A count of each event that has happened over the run. i.e. "turns", "descended", "ascended" - static ref EVENT_COUNTER: Mutex> = Mutex::new(HashMap::new()); + pub static ref EVENT_COUNTER: Mutex> = Mutex::new(HashMap::new()); // A record of events that happened on a given turn. i.e. "Advanced to level 2". pub static ref EVENTS: Mutex>> = Mutex::new(HashMap::new()); // A record of floors visited, and monsters killed. Used to determine if an event is significant. @@ -41,8 +41,9 @@ pub fn restore_events(events: HashMap>) { } /// Wipes all events - for starting a new game. pub fn clear_events() { - EVENT_COUNTER.lock().unwrap().clear(); - EVENTS.lock().unwrap().clear(); + let (mut events, mut event_counts) = (EVENTS.lock().unwrap(), EVENT_COUNTER.lock().unwrap()); + events.clear(); + event_counts.clear(); } #[allow(unused_mut)] diff --git a/tests/gamelog_test.rs b/tests/gamelog_test.rs new file mode 100644 index 0000000..0e18d44 --- /dev/null +++ b/tests/gamelog_test.rs @@ -0,0 +1,43 @@ +// tests/gamelog_test.rs +use rust_rl::gamelog::*; +use rust_rl::data::events::*; +use lazy_static::lazy_static; +use std::sync::Mutex; + +// To ensure this test module uses a single thread. +lazy_static! { + static ref SINGLE_THREAD: Mutex<()> = Mutex::new(()); +} + +#[test] +fn recording_event() { + let _lock = SINGLE_THREAD.lock(); + clear_events(); + record_event(EVENT::Turn(1)); + record_event(EVENT::Turn(0)); + record_event(EVENT::Turn(-1)); + record_event(EVENT::Killed("mob".to_string())); +} + +#[test] +fn getting_event_count() { + let _lock = SINGLE_THREAD.lock(); + clear_events(); + record_event(EVENT::Turn(1)); + assert_eq!(get_event_count(EVENT::COUNT_TURN), 1); + record_event(EVENT::Turn(3)); + assert_eq!(get_event_count(EVENT::COUNT_TURN), 4); + clear_events(); + assert_eq!(get_event_count(EVENT::COUNT_TURN), 0); +} + +#[test] +fn cloning_events() { + let _lock = SINGLE_THREAD.lock(); + clear_events(); + record_event(EVENT::Level(1)); + record_event(EVENT::Turn(5)); + record_event(EVENT::Identified("item".to_string())); + let cloned_events = clone_events(); + assert_eq!(EVENTS.lock().unwrap().clone(), cloned_events); +} diff --git a/tests/mod.rs b/tests/mod.rs index 53cef07..7575647 100644 --- a/tests/mod.rs +++ b/tests/mod.rs @@ -1,2 +1,3 @@ // tests/mod.rs mod map_test; +mod gamelog_test; From 8a44c94272bc701a353e83226dc9cc8e954b8c4b Mon Sep 17 00:00:00 2001 From: Llywelwyn Date: Thu, 21 Sep 2023 00:52:54 +0100 Subject: [PATCH 14/50] adds damage types and mods (weak/resist/immune), for all damage events --- changelog.md | 1 + src/components.rs | 49 ++++++++++++++++++++++++++++++++++++++ src/effects/damage.rs | 12 ++++++++-- src/effects/mod.rs | 2 ++ src/effects/triggers.rs | 6 ++++- src/hunger_system.rs | 3 ++- src/main.rs | 1 + src/melee_combat_system.rs | 5 +++- src/player.rs | 3 ++- src/raws/rawmaster.rs | 38 +++++++++++++++++++++++++---- src/saveload_system.rs | 2 ++ tests/components_test.rs | 19 +++++++++++++++ 12 files changed, 131 insertions(+), 10 deletions(-) create mode 100644 tests/components_test.rs diff --git a/changelog.md b/changelog.md index 82b9191..d4beb21 100644 --- a/changelog.md +++ b/changelog.md @@ -1,6 +1,7 @@ ## v0.1.4 ### added - **overmap**: bare, but exists. player now starts on the overworld, and can move to local maps (like the old starting town) via >. can leave local maps back to the overmap by walking out of the map boundaries. +- **damage types**: immunities, weaknesses, and resistances - **full keyboard support**: examining and targeting can now be done via keyboard only - **a config file** read at runtime, unfortunately not compatible with WASM builds yet - **morgue files**: y/n prompt to write a morgue file on death to /morgue/foo.txt, or to the console for WASM builds diff --git a/src/components.rs b/src/components.rs index ee58646..bb76a93 100644 --- a/src/components.rs +++ b/src/components.rs @@ -316,6 +316,7 @@ pub enum WeaponAttribute { #[derive(Component, Serialize, Deserialize, Clone)] pub struct MeleeWeapon { + pub damage_type: DamageType, pub attribute: WeaponAttribute, pub damage_n_dice: i32, pub damage_die_type: i32, @@ -326,6 +327,7 @@ pub struct MeleeWeapon { #[derive(Serialize, Deserialize, Clone)] pub struct NaturalAttack { pub name: String, + pub damage_type: DamageType, pub damage_n_dice: i32, pub damage_die_type: i32, pub damage_bonus: i32, @@ -365,8 +367,55 @@ pub struct ProvidesHealing { pub modifier: i32, } +#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone, Serialize, Deserialize)] +pub enum DamageType { + Physical, + Magic, + Forced, // Bypasses any immunities. e.g. Hunger ticks. +} + +impl DamageType { + pub fn is_magic(&self) -> bool { + match self { + DamageType::Magic => true, + _ => false, + } + } +} + +#[derive(Debug, PartialEq, Copy, Clone, Serialize, Deserialize)] +pub enum DamageModifier { + None, + Weakness, + Resistance, + Immune, +} + +impl DamageModifier { + pub fn multiplier(&self) -> f32 { + match self { + DamageModifier::None => 1.0, + DamageModifier::Weakness => 10.0, + DamageModifier::Resistance => 0.5, + DamageModifier::Immune => 0.0, + } + } +} + +#[derive(Component, Serialize, Deserialize, Debug, Clone)] +pub struct HasDamageModifiers { + pub modifiers: HashMap, +} + +impl HasDamageModifiers { + pub fn modifier(&self, damage_type: &DamageType) -> &DamageModifier { + self.modifiers.get(damage_type).unwrap_or(&DamageModifier::None) + } +} + #[derive(Component, Debug, ConvertSaveload, Clone)] pub struct InflictsDamage { + pub damage_type: DamageType, pub n_dice: i32, pub sides: i32, pub modifier: i32, diff --git a/src/effects/damage.rs b/src/effects/damage.rs index 74ce8f5..80885e2 100644 --- a/src/effects/damage.rs +++ b/src/effects/damage.rs @@ -14,6 +14,7 @@ use crate::{ HungerClock, HungerState, Bleeds, + HasDamageModifiers, }; use crate::gui::with_article; use crate::data::visuals::{ DEFAULT_PARTICLE_LIFETIME, LONG_PARTICLE_LIFETIME }; @@ -27,8 +28,15 @@ pub fn inflict_damage(ecs: &mut World, damage: &EffectSpawner, target: Entity) { let mut pools = ecs.write_storage::(); if let Some(target_pool) = pools.get_mut(target) { if !target_pool.god { - if let EffectType::Damage { amount } = damage.effect_type { - target_pool.hit_points.current -= amount; + if let EffectType::Damage { amount, damage_type } = damage.effect_type { + let mult = if + let Some(modifiers) = ecs.read_storage::().get(target) + { + modifiers.modifier(&damage_type).multiplier() + } else { + 1.0 + }; + target_pool.hit_points.current -= ((amount as f32) * mult) as i32; let bleeders = ecs.read_storage::(); if let Some(bleeds) = bleeders.get(target) { add_effect( diff --git a/src/effects/mod.rs b/src/effects/mod.rs index 2527e28..c23b52b 100644 --- a/src/effects/mod.rs +++ b/src/effects/mod.rs @@ -4,6 +4,7 @@ use bracket_lib::prelude::*; use specs::prelude::*; use std::collections::VecDeque; use std::sync::Mutex; +use crate::components::DamageType; mod damage; mod hunger; @@ -24,6 +25,7 @@ lazy_static! { pub enum EffectType { Damage { amount: i32, + damage_type: DamageType, }, Healing { amount: i32, diff --git a/src/effects/triggers.rs b/src/effects/triggers.rs index 7b98241..ec64295 100644 --- a/src/effects/triggers.rs +++ b/src/effects/triggers.rs @@ -246,7 +246,11 @@ fn handle_damage( if let Some(damage_item) = ecs.read_storage::().get(event.entity) { let mut rng = ecs.write_resource::(); let roll = rng.roll_dice(damage_item.n_dice, damage_item.sides) + damage_item.modifier; - add_effect(event.source, EffectType::Damage { amount: roll }, event.target.clone()); + add_effect( + event.source, + EffectType::Damage { amount: roll, damage_type: damage_item.damage_type }, + event.target.clone() + ); for target in get_entity_targets(&event.target) { if ecs.read_storage::().get(target).is_some() { continue; diff --git a/src/hunger_system.rs b/src/hunger_system.rs index a0536a4..95d8dc3 100644 --- a/src/hunger_system.rs +++ b/src/hunger_system.rs @@ -5,6 +5,7 @@ use super::{ HungerClock, HungerState, TakingTurn, + DamageType, }; use bracket_lib::prelude::*; use specs::prelude::*; @@ -78,7 +79,7 @@ impl<'a> System<'a> for HungerSystem { if hunger_clock.state == HungerState::Starving { add_effect( None, - EffectType::Damage { amount: 1 }, + EffectType::Damage { amount: 1, damage_type: DamageType::Forced }, Targets::Entity { target: entity } ); } diff --git a/src/main.rs b/src/main.rs index 53f6943..453d1fa 100644 --- a/src/main.rs +++ b/src/main.rs @@ -109,6 +109,7 @@ fn main() -> BError { gs.ecs.register::(); gs.ecs.register::(); gs.ecs.register::(); + gs.ecs.register::(); gs.ecs.register::>(); gs.ecs.register::(); gs.ecs.register::(); diff --git a/src/melee_combat_system.rs b/src/melee_combat_system.rs index 0acd974..cc63807 100644 --- a/src/melee_combat_system.rs +++ b/src/melee_combat_system.rs @@ -123,6 +123,7 @@ impl<'a> System<'a> for MeleeCombatSystem { } else { attacks.push(( MeleeWeapon { + damage_type: crate::DamageType::Physical, attribute: WeaponAttribute::Strength, damage_n_dice: 1, damage_die_type: 4, @@ -301,7 +302,7 @@ impl<'a> System<'a> for MeleeCombatSystem { } add_effect( Some(entity), - EffectType::Damage { amount: damage }, + EffectType::Damage { amount: damage, damage_type: weapon_info.damage_type }, Targets::Entity { target: wants_melee.target } ); if entity == *player_entity { @@ -392,6 +393,7 @@ fn get_natural_attacks( for a in nat.attacks.iter() { attacks.push(( MeleeWeapon { + damage_type: a.damage_type, attribute: WeaponAttribute::Strength, hit_bonus: a.hit_bonus, damage_n_dice: a.damage_n_dice, @@ -409,6 +411,7 @@ fn get_natural_attacks( }; attacks.push(( MeleeWeapon { + damage_type: nat.attacks[attack_index].damage_type, attribute: WeaponAttribute::Strength, hit_bonus: nat.attacks[attack_index].hit_bonus, damage_n_dice: nat.attacks[attack_index].damage_n_dice, diff --git a/src/player.rs b/src/player.rs index 1849b75..dd8ecf3 100644 --- a/src/player.rs +++ b/src/player.rs @@ -32,6 +32,7 @@ use super::{ WantsToPickupItem, get_dest, Destination, + DamageType, }; use bracket_lib::prelude::*; use specs::prelude::*; @@ -290,7 +291,7 @@ pub fn kick(i: i32, j: i32, ecs: &mut World) -> RunState { if rng.roll_dice(1, 20) == 20 { add_effect( None, - EffectType::Damage { amount: 1 }, + EffectType::Damage { amount: 1, damage_type: DamageType::Physical }, Targets::Entity { target: entity } ); gamelog::Logger diff --git a/src/raws/rawmaster.rs b/src/raws/rawmaster.rs index aaabfd1..31a5a1f 100644 --- a/src/raws/rawmaster.rs +++ b/src/raws/rawmaster.rs @@ -25,8 +25,8 @@ macro_rules! apply_effects { } "ranged" => $eb = $eb.with(Ranged { range: effect.1.parse::().unwrap() }), "damage" => { - let (n_dice, sides, modifier) = parse_dice_string(effect.1.as_str()); - $eb = $eb.with(InflictsDamage { n_dice, sides, modifier }) + let (damage_type, dice) = parse_damage_string(effect.1.as_str()); + $eb = $eb.with(InflictsDamage { damage_type, n_dice: dice.0, sides: dice.1, modifier: dice.2 }) } "aoe" => $eb = $eb.with(AOE { radius: effect.1.parse::().unwrap() }), "confusion" => $eb = $eb.with(Confusion { turns: effect.1.parse::().unwrap() }), @@ -45,6 +45,7 @@ macro_rules! apply_effects { /// flags are components that have no parameters to modify. macro_rules! apply_flags { ($flags:expr, $eb:expr) => { + let mut damage_modifiers: HashMap = HashMap::new(); for flag in $flags.iter() { match flag.as_str() { // --- PROP FLAGS BEGIN HERE --- @@ -89,6 +90,13 @@ macro_rules! apply_flags { "NEUTRAL" => $eb = $eb.with(Faction { name: "neutral".to_string() }), "HERBIVORE" => $eb = $eb.with(Faction { name: "herbivore".to_string() }), "CARNIVORE" => $eb = $eb.with(Faction { name: "carnivore".to_string() }), + // --- DAMAGE MODIFIERS --- + "MAGIC_IMMUNITY" => { damage_modifiers.insert(DamageType::Magic, DamageModifier::Immune); } + "MAGIC_WEAKNESS" => { damage_modifiers.insert(DamageType::Magic, DamageModifier::Weakness); } + "MAGIC_RESISTANCE" => { damage_modifiers.insert(DamageType::Magic, DamageModifier::Resistance); } + "PHYSICAL_IMMUNITY" => { damage_modifiers.insert(DamageType::Physical, DamageModifier::Immune); } + "PHYSICAL_WEAKNESS" => { damage_modifiers.insert(DamageType::Physical, DamageModifier::Weakness); } + "PHYSICAL_RESISTANCE" => { damage_modifiers.insert(DamageType::Physical, DamageModifier::Resistance); } // --- MOVEMENT MODES --- ( defaults to WANDER ) "STATIC" => $eb = $eb.with(MoveMode { mode: Movement::Static }), "RANDOM_PATH" => $eb = $eb.with(MoveMode { mode: Movement::RandomWaypoint { path: None } }), @@ -102,6 +110,9 @@ macro_rules! apply_flags { _ => console::log(format!("Unrecognised flag: {}", flag.as_str())), } } + if damage_modifiers.len() > 0 { + $eb = $eb.with(HasDamageModifiers { modifiers: damage_modifiers }); + } }; } @@ -340,13 +351,16 @@ pub fn spawn_named_item( } if let Some(weapon) = &item_template.equip { - let (n_dice, die_type, bonus) = parse_dice_string(weapon.damage.as_str()); + let (damage_type, (n_dice, die_type, bonus)) = parse_damage_string( + weapon.damage.as_str() + ); let weapon_attribute = match weapon.flag.as_str() { "DEXTERITY" => WeaponAttribute::Dexterity, "FINESSE" => WeaponAttribute::Finesse, _ => WeaponAttribute::Strength, }; let wpn = MeleeWeapon { + damage_type, attribute: weapon_attribute, damage_n_dice: n_dice, damage_die_type: die_type, @@ -526,9 +540,10 @@ pub fn spawn_named_mob( if let Some(natural_attacks) = &mob_template.attacks { let mut natural = NaturalAttacks { attacks: Vec::new() }; for na in natural_attacks.iter() { - let (n, d, b) = parse_dice_string(&na.damage); + let (damage_type, (n, d, b)) = parse_damage_string(&na.damage); let attack = NaturalAttack { name: na.name.clone(), + damage_type, hit_bonus: na.hit_bonus, damage_n_dice: n, damage_die_type: d, @@ -1039,3 +1054,18 @@ fn parse_particle_burst(n: &str) -> SpawnParticleBurst { trail_lifetime_ms: tokens[7].parse::().unwrap(), } } + +fn parse_damage_string(n: &str) -> (DamageType, (i32, i32, i32)) { + let tokens: Vec<_> = n.split(';').collect(); + let damage_type = if tokens.len() > 1 { + match tokens[1] { + "physical" => DamageType::Physical, + "magic" => DamageType::Magic, + _ => panic!("Unrecognised damage type in raws: {}", tokens[1]), + } + } else { + DamageType::Physical + }; + let dice = parse_dice_string(tokens[0]); + return (damage_type, dice); +} diff --git a/src/saveload_system.rs b/src/saveload_system.rs index aa6576c..1b4ba57 100644 --- a/src/saveload_system.rs +++ b/src/saveload_system.rs @@ -88,6 +88,7 @@ pub fn save_game(ecs: &mut World) { GrantsXP, HasAncestry, HasClass, + HasDamageModifiers, Hidden, HungerClock, IdentifiedBeatitude, @@ -218,6 +219,7 @@ pub fn load_game(ecs: &mut World) { GrantsXP, HasAncestry, HasClass, + HasDamageModifiers, Hidden, HungerClock, IdentifiedBeatitude, diff --git a/tests/components_test.rs b/tests/components_test.rs new file mode 100644 index 0000000..6c7789e --- /dev/null +++ b/tests/components_test.rs @@ -0,0 +1,19 @@ +// tests/components_test.rs +use rust_rl::components::*; + +#[test] +fn damagetype_equality() { + let dt1 = DamageType::Physical; + let dt2 = DamageType::Physical; + assert_eq!(dt1, dt2); + let dt3 = DamageType::Magic; + assert_ne!(dt1, dt3); +} + +#[test] +fn damagetype_ismagic() { + let dt1 = DamageType::Physical; + let dt2 = DamageType::Magic; + assert!(!dt1.is_magic()); + assert!(dt2.is_magic()); +} From dc4bcbe618c970ba02b6502d87533cac07885a0c Mon Sep 17 00:00:00 2001 From: Llywelwyn Date: Thu, 21 Sep 2023 00:53:04 +0100 Subject: [PATCH 15/50] adds damage types to items --- raws/items.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/raws/items.json b/raws/items.json index b1302a6..7f55d95 100644 --- a/raws/items.json +++ b/raws/items.json @@ -64,7 +64,7 @@ "weight": 0.5, "value": 50, "flags": ["CONSUMABLE", "DESTRUCTIBLE"], - "effects": { "particle_line": "*;-;#00b7ff;75.0;#f4fc83;100.0", "ranged": "12", "damage": "3d4+3" }, + "effects": { "particle_line": "*;-;#00b7ff;75.0;#f4fc83;100.0", "ranged": "12", "damage": "3d4+3;magic" }, "magic": { "class": "uncommon", "naming": "scroll" } }, { @@ -74,7 +74,7 @@ "weight": 0.5, "value": 100, "flags": ["CONSUMABLE", "DESTRUCTIBLE"], - "effects": { "particle": "*;#FFA500;200.0", "ranged": "10", "damage": "4d6", "aoe": "2" }, + "effects": { "particle": "*;#FFA500;200.0", "ranged": "10", "damage": "4d6;magic", "aoe": "2" }, "magic": { "class": "uncommon", "naming": "scroll" } }, { @@ -87,7 +87,7 @@ "effects": { "particle_burst": "▓;*;~;#FFA500;#000000;500.0;#ffd381;60.0", "ranged": "10", - "damage": "8d6", + "damage": "8d6;magic", "aoe": "3" }, "magic": { "class": "rare", "naming": "scroll" } @@ -353,7 +353,7 @@ "weight": 2, "value": 100, "flags": ["CHARGES"], - "effects": { "ranged": "12", "damage": "3d4+3" }, + "effects": { "ranged": "12", "damage": "3d4+3;magic" }, "magic": { "class": "uncommon", "naming": "wand" } }, { @@ -363,7 +363,7 @@ "weight": 2, "value": 300, "flags": ["CHARGES"], - "effects": { "ranged": "10", "damage": "8d6", "aoe": "3" }, + "effects": { "ranged": "10", "damage": "8d6;magic", "aoe": "3" }, "magic": { "class": "rare", "naming": "wand" } }, { From 654aea9a321d5d071be8a29ad167b098e28c2edb Mon Sep 17 00:00:00 2001 From: Llywelwyn Date: Thu, 21 Sep 2023 01:08:01 +0100 Subject: [PATCH 16/50] damage mod multiplier unit tests --- src/components.rs | 13 +++++++++---- tests/components_test.rs | 28 ++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/src/components.rs b/src/components.rs index bb76a93..c2a2bb6 100644 --- a/src/components.rs +++ b/src/components.rs @@ -392,12 +392,17 @@ pub enum DamageModifier { } impl DamageModifier { + const NONE_MOD: f32 = 1.0; + const WEAK_MOD: f32 = 2.0; + const RESIST_MOD: f32 = 0.5; + const IMMUNE_MOD: f32 = 0.0; + pub fn multiplier(&self) -> f32 { match self { - DamageModifier::None => 1.0, - DamageModifier::Weakness => 10.0, - DamageModifier::Resistance => 0.5, - DamageModifier::Immune => 0.0, + DamageModifier::None => Self::NONE_MOD, + DamageModifier::Weakness => Self::WEAK_MOD, + DamageModifier::Resistance => Self::RESIST_MOD, + DamageModifier::Immune => Self::IMMUNE_MOD, } } } diff --git a/tests/components_test.rs b/tests/components_test.rs index 6c7789e..1581f90 100644 --- a/tests/components_test.rs +++ b/tests/components_test.rs @@ -1,5 +1,6 @@ // tests/components_test.rs use rust_rl::components::*; +use std::collections::HashMap; #[test] fn damagetype_equality() { @@ -17,3 +18,30 @@ fn damagetype_ismagic() { assert!(!dt1.is_magic()); assert!(dt2.is_magic()); } + +#[test] +fn get_damage_modifiers() { + let dm = HasDamageModifiers { + modifiers: { + let mut m = HashMap::new(); + m.insert(DamageType::Physical, DamageModifier::Weakness); + m.insert(DamageType::Magic, DamageModifier::Resistance); + m + }, + }; + assert_eq!(dm.modifier(&DamageType::Physical), &DamageModifier::Weakness); + assert_eq!(dm.modifier(&DamageType::Magic), &DamageModifier::Resistance); + assert_ne!(dm.modifier(&DamageType::Forced), &DamageModifier::Immune); +} + +#[test] +fn get_damage_modifier_multiplier() { + let none_mod = &DamageModifier::None.multiplier(); + let weak_mod = &DamageModifier::Weakness.multiplier(); + let res_mod = &DamageModifier::Resistance.multiplier(); + let immune_mod = &DamageModifier::Immune.multiplier(); + assert_eq!(none_mod, &1.0); + assert_eq!(weak_mod, &2.0); + assert_eq!(res_mod, &0.5); + assert_eq!(immune_mod, &0.0); +} From e8b5f6d997e57d11bc949a6527b2f2339fd80c0d Mon Sep 17 00:00:00 2001 From: Llywelwyn Date: Thu, 21 Sep 2023 01:10:02 +0100 Subject: [PATCH 17/50] adds component tests to tests/mod.rs --- tests/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/mod.rs b/tests/mod.rs index 7575647..e1561c3 100644 --- a/tests/mod.rs +++ b/tests/mod.rs @@ -1,3 +1,4 @@ // tests/mod.rs mod map_test; mod gamelog_test; +mod components_test; From 921fee2eccca007cb145400d022c6cb62dbb5a73 Mon Sep 17 00:00:00 2001 From: Llywelwyn Date: Thu, 21 Sep 2023 05:06:52 +0100 Subject: [PATCH 18/50] intrinsic speed + regeneration --- src/ai/energy_system.rs | 16 +++++++++++++++- src/ai/regen_system.rs | 33 +++++++++++++++++++++++++++++---- src/components.rs | 13 ++++++++++++- src/hunger_system.rs | 15 +++++++++++++-- src/main.rs | 1 + src/saveload_system.rs | 2 ++ src/spawner.rs | 8 ++++++-- 7 files changed, 78 insertions(+), 10 deletions(-) diff --git a/src/ai/energy_system.rs b/src/ai/energy_system.rs index 8ddbe4b..da62467 100644 --- a/src/ai/energy_system.rs +++ b/src/ai/energy_system.rs @@ -10,6 +10,7 @@ use crate::{ Map, TakingTurn, Confusion, + Intrinsics, }; use bracket_lib::prelude::*; use specs::prelude::*; @@ -36,6 +37,7 @@ impl<'a> System<'a> for EnergySystem { ReadStorage<'a, Name>, ReadExpect<'a, Point>, ReadStorage<'a, Confusion>, + ReadStorage<'a, Intrinsics>, ); fn run(&mut self, data: Self::SystemData) { @@ -53,6 +55,7 @@ impl<'a> System<'a> for EnergySystem { names, player_pos, confusion, + intrinsics, ) = data; // If not ticking, do nothing. if *runstate != RunState::Ticking { @@ -89,10 +92,12 @@ impl<'a> System<'a> for EnergySystem { ).join() { let burden_modifier = get_burden_modifier(&burdens, entity); let overmap_mod = get_overmap_modifier(&map); + let intrinsic_speed = get_intrinsic_speed(&intrinsics, entity); // Every entity has a POTENTIAL equal to their speed. let mut energy_potential: i32 = ((energy.speed as f32) * burden_modifier * - overmap_mod) as i32; + overmap_mod * + intrinsic_speed) as i32; // Increment current energy by NORMAL_SPEED for every // whole number of NORMAL_SPEEDS in their POTENTIAL. while energy_potential >= NORMAL_SPEED { @@ -162,3 +167,12 @@ fn cull_turn_by_distance(player_pos: &Point, pos: &Position) -> bool { } return false; } + +fn get_intrinsic_speed(intrinsics: &ReadStorage, entity: Entity) -> f32 { + if let Some(intrinsics) = intrinsics.get(entity) { + if intrinsics.list.contains(&crate::Intrinsic::Speed) { + return 4.0 / 3.0; + } + } + return 1.0; +} diff --git a/src/ai/regen_system.rs b/src/ai/regen_system.rs index b74f857..8114c4e 100644 --- a/src/ai/regen_system.rs +++ b/src/ai/regen_system.rs @@ -9,6 +9,7 @@ use crate::{ Position, RandomNumberGenerator, TakingTurn, + Intrinsics, }; use specs::prelude::*; use crate::data::events::*; @@ -36,10 +37,24 @@ impl<'a> System<'a> for RegenSystem { ReadStorage<'a, HasClass>, ReadStorage<'a, Attributes>, WriteExpect<'a, RandomNumberGenerator>, + ReadStorage<'a, Intrinsics>, + ReadExpect<'a, Entity>, ); fn run(&mut self, data: Self::SystemData) { - let (clock, entities, positions, mut pools, turns, player, classes, attributes, mut rng) = data; + let ( + clock, + entities, + positions, + mut pools, + turns, + player, + classes, + attributes, + mut rng, + intrinsics, + player_entity, + ) = data; let mut clock_turn = false; for (_e, _c, _t) in (&entities, &clock, &turns).join() { clock_turn = true; @@ -56,19 +71,29 @@ impl<'a> System<'a> for RegenSystem { } // Player HP regen let level = gamelog::get_event_count(EVENT::COUNT_LEVEL); - if current_turn % get_player_hp_regen_turn(level) == 0 { + if + current_turn % get_player_hp_regen_turn(level) == 0 || + intrinsics.get(*player_entity).unwrap().list.contains(&crate::Intrinsic::Regeneration) + { for (_e, _p, pool, _player) in (&entities, &positions, &mut pools, &player).join() { try_hp_regen_tick(pool, get_player_hp_regen_per_tick(level)); } } // Both MP regen for (e, _p, pool) in (&entities, &positions, &mut pools).join() { - let is_wizard = if let Some(class) = classes.get(e) { class.name == Class::Wizard } else { false }; + let is_wizard = if let Some(class) = classes.get(e) { + class.name == Class::Wizard + } else { + false + }; let numerator = if is_wizard { WIZARD_MP_REGEN_MOD } else { NONWIZARD_MP_REGEN_MOD }; let multiplier: f32 = (numerator as f32) / (MP_REGEN_DIVISOR as f32); let mp_regen_tick = (((MP_REGEN_BASE - pool.level) as f32) * multiplier) as i32; if current_turn % mp_regen_tick == 0 { - try_mana_regen_tick(pool, rng.roll_dice(1, get_mana_regen_per_tick(e, &attributes))); + try_mana_regen_tick( + pool, + rng.roll_dice(1, get_mana_regen_per_tick(e, &attributes)) + ); } } } diff --git a/src/components.rs b/src/components.rs index c2a2bb6..3a7a552 100644 --- a/src/components.rs +++ b/src/components.rs @@ -6,7 +6,7 @@ use specs::error::NoError; use specs::prelude::*; use specs::saveload::{ ConvertSaveload, Marker }; use specs_derive::*; -use std::collections::HashMap; +use std::collections::{ HashMap, HashSet }; // Serialization helper code. We need to implement ConvertSaveload for each type that contains an // Entity. @@ -418,6 +418,17 @@ impl HasDamageModifiers { } } +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum Intrinsic { + Regeneration, // Regenerate 1 HP on every tick + Speed, // 4/3x speed multiplier +} + +#[derive(Component, Serialize, Deserialize, Debug, Clone)] +pub struct Intrinsics { + pub list: HashSet, +} + #[derive(Component, Debug, ConvertSaveload, Clone)] pub struct InflictsDamage { pub damage_type: DamageType, diff --git a/src/hunger_system.rs b/src/hunger_system.rs index 95d8dc3..7a1b0fd 100644 --- a/src/hunger_system.rs +++ b/src/hunger_system.rs @@ -6,6 +6,7 @@ use super::{ HungerState, TakingTurn, DamageType, + Intrinsics, }; use bracket_lib::prelude::*; use specs::prelude::*; @@ -53,10 +54,11 @@ impl<'a> System<'a> for HungerSystem { ReadExpect<'a, Entity>, ReadStorage<'a, Clock>, ReadStorage<'a, TakingTurn>, + ReadStorage<'a, Intrinsics>, ); fn run(&mut self, data: Self::SystemData) { - let (entities, mut hunger_clock, player_entity, turn_clock, turns) = data; + let (entities, mut hunger_clock, player_entity, turn_clock, turns, intrinsics) = data; // If the turn clock isn't taking a turn this tick, don't bother ticking hunger. let mut ticked = false; @@ -72,7 +74,16 @@ impl<'a> System<'a> for HungerSystem { if hunger_clock.duration >= MAX_SATIATION { hunger_clock.duration = MAX_SATIATION; } else { - hunger_clock.duration -= BASE_CLOCK_DECREMENT_PER_TURN; + let mut modifier = 0; + let intrinsic_regen = if let Some(i) = intrinsics.get(entity) { + i.list.contains(&crate::Intrinsic::Regeneration) + } else { + false + }; + if intrinsic_regen { + modifier += 1; + } + hunger_clock.duration -= BASE_CLOCK_DECREMENT_PER_TURN + modifier; } let initial_state = hunger_clock.state; hunger_clock.state = get_hunger_state(hunger_clock.duration); diff --git a/src/main.rs b/src/main.rs index 453d1fa..f82ebbd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -110,6 +110,7 @@ fn main() -> BError { gs.ecs.register::(); gs.ecs.register::(); gs.ecs.register::(); + gs.ecs.register::(); gs.ecs.register::>(); gs.ecs.register::(); gs.ecs.register::(); diff --git a/src/saveload_system.rs b/src/saveload_system.rs index 1b4ba57..894e4ff 100644 --- a/src/saveload_system.rs +++ b/src/saveload_system.rs @@ -95,6 +95,7 @@ pub fn save_game(ecs: &mut World) { IdentifiedItem, InBackpack, InflictsDamage, + Intrinsics, Item, KnownSpells, LootTable, @@ -226,6 +227,7 @@ pub fn load_game(ecs: &mut World) { IdentifiedItem, InBackpack, InflictsDamage, + Intrinsics, Item, KnownSpells, LootTable, diff --git a/src/spawner.rs b/src/spawner.rs index 7b62609..3fa673c 100644 --- a/src/spawner.rs +++ b/src/spawner.rs @@ -25,6 +25,8 @@ use super::{ Viewshed, BlocksTile, Bleeds, + HasDamageModifiers, + Intrinsics, }; use crate::data::entity; use crate::data::visuals::BLOODSTAIN_COLOUR; @@ -32,7 +34,7 @@ use crate::gamesystem::*; use bracket_lib::prelude::*; use specs::prelude::*; use specs::saveload::{ MarkedBuilder, SimpleMarker }; -use std::collections::HashMap; +use std::collections::{ HashMap, HashSet }; /// Spawns the player and returns his/her entity object. pub fn player(ecs: &mut World, player_x: i32, player_y: i32) -> Entity { @@ -86,7 +88,9 @@ pub fn player(ecs: &mut World, player_x: i32, player_y: i32) -> Entity { weight: 0.0, god: false, }) - .with(EquipmentChanged {}) + .with(HasDamageModifiers { modifiers: HashMap::new() }) + .with(Intrinsics { list: HashSet::new() }) + .with(EquipmentChanged {}) // To force re-calc of equipment bonuses. .with(skills) .with(Energy { current: 0, speed: entity::NORMAL_SPEED }) .marked::>() From de0aa331074f446226a78373c24de3bd2db4a9e2 Mon Sep 17 00:00:00 2001 From: Llywelwyn Date: Thu, 21 Sep 2023 05:26:25 +0100 Subject: [PATCH 19/50] swaps over to bracket-lib's parse_dice_string(), and cl --- changelog.md | 1 + src/effects/triggers.rs | 2 +- src/raws/rawmaster.rs | 33 +++++++++++++++------------------ 3 files changed, 17 insertions(+), 19 deletions(-) diff --git a/changelog.md b/changelog.md index d4beb21..4976351 100644 --- a/changelog.md +++ b/changelog.md @@ -1,6 +1,7 @@ ## v0.1.4 ### added - **overmap**: bare, but exists. player now starts on the overworld, and can move to local maps (like the old starting town) via >. can leave local maps back to the overmap by walking out of the map boundaries. +- **intrinsics**: speed, regeneration - **damage types**: immunities, weaknesses, and resistances - **full keyboard support**: examining and targeting can now be done via keyboard only - **a config file** read at runtime, unfortunately not compatible with WASM builds yet diff --git a/src/effects/triggers.rs b/src/effects/triggers.rs index ec64295..ac38ccb 100644 --- a/src/effects/triggers.rs +++ b/src/effects/triggers.rs @@ -172,7 +172,7 @@ fn handle_magic_mapper( fn handle_grant_spell( ecs: &mut World, event: &mut EventInfo, - mut logger: gamelog::Logger + logger: gamelog::Logger ) -> (gamelog::Logger, bool) { if let Some(_granted_spell) = ecs.read_storage::().get(event.entity) { if diff --git a/src/raws/rawmaster.rs b/src/raws/rawmaster.rs index 31a5a1f..1bc355a 100644 --- a/src/raws/rawmaster.rs +++ b/src/raws/rawmaster.rs @@ -6,7 +6,6 @@ use crate::random_table::RandomTable; use crate::config::CONFIG; use crate::data::visuals::BLOODSTAIN_COLOUR; use crate::data::entity::DEFAULT_VIEWSHED_STANDARD; -use regex::Regex; use bracket_lib::prelude::*; use specs::prelude::*; use specs::saveload::{ MarkedBuilder, SimpleMarker }; @@ -20,13 +19,13 @@ macro_rules! apply_effects { let effect_name = effect.0.as_str(); match effect_name { "heal" => { - let (n_dice, sides, modifier) = parse_dice_string(effect.1.as_str()); - $eb = $eb.with(ProvidesHealing { n_dice, sides, modifier }) + let dice = parse_dice_string(effect.1.as_str()).expect("Failed to parse dice string"); + $eb = $eb.with(ProvidesHealing { n_dice: dice.n_dice, sides: dice.die_type, modifier: dice.bonus }) } "ranged" => $eb = $eb.with(Ranged { range: effect.1.parse::().unwrap() }), "damage" => { let (damage_type, dice) = parse_damage_string(effect.1.as_str()); - $eb = $eb.with(InflictsDamage { damage_type, n_dice: dice.0, sides: dice.1, modifier: dice.2 }) + $eb = $eb.with(InflictsDamage { damage_type, n_dice: dice.n_dice, sides: dice.die_type, modifier: dice.bonus }) } "aoe" => $eb = $eb.with(AOE { radius: effect.1.parse::().unwrap() }), "confusion" => $eb = $eb.with(Confusion { turns: effect.1.parse::().unwrap() }), @@ -351,9 +350,7 @@ pub fn spawn_named_item( } if let Some(weapon) = &item_template.equip { - let (damage_type, (n_dice, die_type, bonus)) = parse_damage_string( - weapon.damage.as_str() - ); + let (damage_type, dice) = parse_damage_string(weapon.damage.as_str()); let weapon_attribute = match weapon.flag.as_str() { "DEXTERITY" => WeaponAttribute::Dexterity, "FINESSE" => WeaponAttribute::Finesse, @@ -362,9 +359,9 @@ pub fn spawn_named_item( let wpn = MeleeWeapon { damage_type, attribute: weapon_attribute, - damage_n_dice: n_dice, - damage_die_type: die_type, - damage_bonus: bonus, + damage_n_dice: dice.n_dice, + damage_die_type: dice.die_type, + damage_bonus: dice.bonus, hit_bonus: weapon.to_hit.unwrap_or(0), }; eb = eb.with(wpn); @@ -540,14 +537,14 @@ pub fn spawn_named_mob( if let Some(natural_attacks) = &mob_template.attacks { let mut natural = NaturalAttacks { attacks: Vec::new() }; for na in natural_attacks.iter() { - let (damage_type, (n, d, b)) = parse_damage_string(&na.damage); + let (damage_type, dice) = parse_damage_string(&na.damage); let attack = NaturalAttack { name: na.name.clone(), damage_type, hit_bonus: na.hit_bonus, - damage_n_dice: n, - damage_die_type: d, - damage_bonus: b, + damage_n_dice: dice.n_dice, + damage_die_type: dice.die_type, + damage_bonus: dice.bonus, }; natural.attacks.push(attack); } @@ -733,7 +730,7 @@ pub fn table_by_name(raws: &RawMaster, key: &str, optional_difficulty: Option (i32, i32, i32) { +/*pub fn parse_dice_string(dice: &str) -> (i32, i32, i32) { lazy_static! { static ref DICE_RE: Regex = Regex::new(r"(\d+)d(\d+)([\+\-]\d+)?").unwrap(); } @@ -752,7 +749,7 @@ pub fn parse_dice_string(dice: &str) -> (i32, i32, i32) { } } (n_dice, die_type, die_bonus) -} +}*/ fn find_slot_for_equippable_item(tag: &str, raws: &RawMaster) -> EquipmentSlot { if !raws.item_index.contains_key(tag) { @@ -1055,7 +1052,7 @@ fn parse_particle_burst(n: &str) -> SpawnParticleBurst { } } -fn parse_damage_string(n: &str) -> (DamageType, (i32, i32, i32)) { +fn parse_damage_string(n: &str) -> (DamageType, DiceType) { let tokens: Vec<_> = n.split(';').collect(); let damage_type = if tokens.len() > 1 { match tokens[1] { @@ -1066,6 +1063,6 @@ fn parse_damage_string(n: &str) -> (DamageType, (i32, i32, i32)) { } else { DamageType::Physical }; - let dice = parse_dice_string(tokens[0]); + let dice = parse_dice_string(tokens[0]).expect("Failed to parse dice string"); return (damage_type, dice); } From b6abfbce4a58be7e8193c157e5e0fd267139df68 Mon Sep 17 00:00:00 2001 From: Llywelwyn Date: Thu, 21 Sep 2023 22:46:14 +0100 Subject: [PATCH 20/50] damage types: phys, magic, fire, cold, poison --- raws/items.json | 6 +++--- raws/mobs.json | 6 +++--- src/components.rs | 7 +++++-- src/raws/rawmaster.rs | 22 +++++++++++++++++----- 4 files changed, 28 insertions(+), 13 deletions(-) diff --git a/raws/items.json b/raws/items.json index 7f55d95..2c0c678 100644 --- a/raws/items.json +++ b/raws/items.json @@ -74,7 +74,7 @@ "weight": 0.5, "value": 100, "flags": ["CONSUMABLE", "DESTRUCTIBLE"], - "effects": { "particle": "*;#FFA500;200.0", "ranged": "10", "damage": "4d6;magic", "aoe": "2" }, + "effects": { "particle": "*;#FFA500;200.0", "ranged": "10", "damage": "4d6;fire", "aoe": "2" }, "magic": { "class": "uncommon", "naming": "scroll" } }, { @@ -87,7 +87,7 @@ "effects": { "particle_burst": "▓;*;~;#FFA500;#000000;500.0;#ffd381;60.0", "ranged": "10", - "damage": "8d6;magic", + "damage": "8d6;fire", "aoe": "3" }, "magic": { "class": "rare", "naming": "scroll" } @@ -363,7 +363,7 @@ "weight": 2, "value": 300, "flags": ["CHARGES"], - "effects": { "ranged": "10", "damage": "8d6;magic", "aoe": "3" }, + "effects": { "ranged": "10", "damage": "8d6;fire", "aoe": "3" }, "magic": { "class": "rare", "naming": "wand" } }, { diff --git a/raws/mobs.json b/raws/mobs.json index 651f4f2..ddd6556 100644 --- a/raws/mobs.json +++ b/raws/mobs.json @@ -323,13 +323,13 @@ "id": "ant_soldier", "name": "soldier ant", "renderable": { "glyph": "a", "fg": "#ca3f26", "bg": "#000000", "order": 1 }, - "flags": ["SMALL_GROUP"], + "flags": ["SMALL_GROUP", "POISON_RES"], "level": 3, "bac": 3, "speed": 18, "attacks": [ { "name": "bites", "hit_bonus": 0, "damage": "2d4" }, - { "name": "stings", "hit_bonus": 0, "damage": "3d4" } + { "name": "stings", "hit_bonus": 0, "damage": "3d4;poison" } ], "loot": { "table": "food", "chance": 0.05 } }, @@ -503,7 +503,7 @@ "id": "treant_small", "name": "treant sapling", "renderable": { "glyph": "♠️", "fg": "#10570d", "bg": "#000000", "order": 1 }, - "flags": ["LARGE_GROUP", "GREEN_BLOOD"], + "flags": ["LARGE_GROUP", "GREEN_BLOOD", "FIRE_WEAK"], "level": 2, "bac": 12, "speed": 3, diff --git a/src/components.rs b/src/components.rs index 3a7a552..8c56cce 100644 --- a/src/components.rs +++ b/src/components.rs @@ -370,14 +370,17 @@ pub struct ProvidesHealing { #[derive(Debug, PartialEq, Eq, Hash, Copy, Clone, Serialize, Deserialize)] pub enum DamageType { Physical, - Magic, + Magic, // e.g. magic missiles, silvered weapons + Fire, // e.g. fireball + Cold, // e.g. cone of cold + Poison, // e.g. poison gas Forced, // Bypasses any immunities. e.g. Hunger ticks. } impl DamageType { pub fn is_magic(&self) -> bool { match self { - DamageType::Magic => true, + DamageType::Magic | DamageType::Fire | DamageType::Cold => true, _ => false, } } diff --git a/src/raws/rawmaster.rs b/src/raws/rawmaster.rs index 1bc355a..55753eb 100644 --- a/src/raws/rawmaster.rs +++ b/src/raws/rawmaster.rs @@ -90,12 +90,21 @@ macro_rules! apply_flags { "HERBIVORE" => $eb = $eb.with(Faction { name: "herbivore".to_string() }), "CARNIVORE" => $eb = $eb.with(Faction { name: "carnivore".to_string() }), // --- DAMAGE MODIFIERS --- + "PHYS_IMMUNITY" => { damage_modifiers.insert(DamageType::Physical, DamageModifier::Immune); } + "PHYS_WEAK" => { damage_modifiers.insert(DamageType::Physical, DamageModifier::Weakness); } + "PHYS_RES" => { damage_modifiers.insert(DamageType::Physical, DamageModifier::Resistance); } "MAGIC_IMMUNITY" => { damage_modifiers.insert(DamageType::Magic, DamageModifier::Immune); } - "MAGIC_WEAKNESS" => { damage_modifiers.insert(DamageType::Magic, DamageModifier::Weakness); } - "MAGIC_RESISTANCE" => { damage_modifiers.insert(DamageType::Magic, DamageModifier::Resistance); } - "PHYSICAL_IMMUNITY" => { damage_modifiers.insert(DamageType::Physical, DamageModifier::Immune); } - "PHYSICAL_WEAKNESS" => { damage_modifiers.insert(DamageType::Physical, DamageModifier::Weakness); } - "PHYSICAL_RESISTANCE" => { damage_modifiers.insert(DamageType::Physical, DamageModifier::Resistance); } + "MAGIC_WEAK" => { damage_modifiers.insert(DamageType::Magic, DamageModifier::Weakness); } + "MAGIC_RES" => { damage_modifiers.insert(DamageType::Magic, DamageModifier::Resistance); } + "FIRE_IMMUNITY" => { damage_modifiers.insert(DamageType::Fire, DamageModifier::Immune); } + "FIRE_WEAK" => { damage_modifiers.insert(DamageType::Fire, DamageModifier::Weakness); } + "FIRE_RES" => { damage_modifiers.insert(DamageType::Fire, DamageModifier::Resistance); } + "COLD_IMMUNITY" => { damage_modifiers.insert(DamageType::Cold, DamageModifier::Immune); } + "COLD_WEAK" => { damage_modifiers.insert(DamageType::Cold, DamageModifier::Weakness); } + "COLD_RES" => { damage_modifiers.insert(DamageType::Cold, DamageModifier::Resistance); } + "POISON_IMMUNITY" => { damage_modifiers.insert(DamageType::Poison, DamageModifier::Immune); } + "POISON_WEAK" => { damage_modifiers.insert(DamageType::Poison, DamageModifier::Weakness); } + "POISON_RES" => { damage_modifiers.insert(DamageType::Poison, DamageModifier::Resistance); } // --- MOVEMENT MODES --- ( defaults to WANDER ) "STATIC" => $eb = $eb.with(MoveMode { mode: Movement::Static }), "RANDOM_PATH" => $eb = $eb.with(MoveMode { mode: Movement::RandomWaypoint { path: None } }), @@ -1058,6 +1067,9 @@ fn parse_damage_string(n: &str) -> (DamageType, DiceType) { match tokens[1] { "physical" => DamageType::Physical, "magic" => DamageType::Magic, + "fire" => DamageType::Fire, + "cold" => DamageType::Cold, + "poison" => DamageType::Poison, _ => panic!("Unrecognised damage type in raws: {}", tokens[1]), } } else { From ae8f0d15a03521560ee21891a098e4840e4150c6 Mon Sep 17 00:00:00 2001 From: Llywelwyn Date: Fri, 22 Sep 2023 23:59:15 +0100 Subject: [PATCH 21/50] unreachable! for debug entries --- src/raws/rawmaster.rs | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/raws/rawmaster.rs b/src/raws/rawmaster.rs index 55753eb..5a820ad 100644 --- a/src/raws/rawmaster.rs +++ b/src/raws/rawmaster.rs @@ -727,16 +727,13 @@ pub fn table_by_name(raws: &RawMaster, key: &str, optional_difficulty: Option (i32, i32, i32) { From 23a42bab80573fbdd1556803f13e684d79be4d7b Mon Sep 17 00:00:00 2001 From: Llywelwyn Date: Sat, 23 Sep 2023 00:12:05 +0100 Subject: [PATCH 22/50] Some panics to unreachable!, better error msging --- src/map_builders/area_starting_points.rs | 2 +- src/map_builders/forest.rs | 2 +- src/map_builders/mod.rs | 4 ++-- src/map_builders/room_based_spawner.rs | 2 +- src/map_builders/room_based_stairs.rs | 2 +- src/map_builders/room_based_starting_position.rs | 2 +- src/map_builders/room_corner_rounding.rs | 2 +- src/map_builders/room_draw.rs | 2 +- src/map_builders/room_exploder.rs | 2 +- src/map_builders/room_themer.rs | 2 +- src/map_builders/rooms_corridors_bresenham.rs | 2 +- src/map_builders/rooms_corridors_bsp.rs | 2 +- src/map_builders/rooms_corridors_dogleg.rs | 2 +- src/map_builders/rooms_corridors_nearest.rs | 2 +- src/map_builders/rooms_corridors_spawner.rs | 2 +- src/raws/rawmaster.rs | 6 +++--- 16 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/map_builders/area_starting_points.rs b/src/map_builders/area_starting_points.rs index c251164..9707ed6 100644 --- a/src/map_builders/area_starting_points.rs +++ b/src/map_builders/area_starting_points.rs @@ -75,7 +75,7 @@ impl AreaStartingPosition { } } if available_floors.is_empty() { - panic!("No valid floors to start on"); + unreachable!("No valid floors to start on."); } available_floors.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap()); diff --git a/src/map_builders/forest.rs b/src/map_builders/forest.rs index 86979a1..e30f270 100644 --- a/src/map_builders/forest.rs +++ b/src/map_builders/forest.rs @@ -76,7 +76,7 @@ impl RoadExit { } } if available_floors.is_empty() { - panic!("No valid floors to start on."); + unreachable!("No valid floors to start on."); } available_floors.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap()); let end_x = (available_floors[0].0 as i32) % build_data.map.width; diff --git a/src/map_builders/mod.rs b/src/map_builders/mod.rs index edebe82..81045c6 100644 --- a/src/map_builders/mod.rs +++ b/src/map_builders/mod.rs @@ -137,7 +137,7 @@ impl BuilderChain { None => { self.starter = Some(starter); } - Some(_) => panic!("You can only have one starting builder."), + Some(_) => unreachable!("You can only have one starting builder."), }; } @@ -147,7 +147,7 @@ impl BuilderChain { pub fn build_map(&mut self, rng: &mut RandomNumberGenerator) { match &mut self.starter { - None => panic!("Cannot run a map builder chain without a starting build system"), + None => unreachable!("Cannot run a map builder chain without a starting build system"), Some(starter) => { // Build the starting map starter.build_map(rng, &mut self.build_data); diff --git a/src/map_builders/room_based_spawner.rs b/src/map_builders/room_based_spawner.rs index de9be3a..8b86a7f 100644 --- a/src/map_builders/room_based_spawner.rs +++ b/src/map_builders/room_based_spawner.rs @@ -27,7 +27,7 @@ impl RoomBasedSpawner { ); } } else { - panic!("RoomBasedSpawner only works after rooms have been created"); + unreachable!("RoomBasedSpawner tried to run without any rooms."); } } } diff --git a/src/map_builders/room_based_stairs.rs b/src/map_builders/room_based_stairs.rs index 1e74bd5..c849e00 100644 --- a/src/map_builders/room_based_stairs.rs +++ b/src/map_builders/room_based_stairs.rs @@ -22,7 +22,7 @@ impl RoomBasedStairs { build_data.map.tiles[stairs_idx] = TileType::DownStair; build_data.take_snapshot(); } else { - panic!("RoomBasedStairs only works after rooms have been created"); + unreachable!("RoomBasedStairs tried to run without any rooms."); } } } diff --git a/src/map_builders/room_based_starting_position.rs b/src/map_builders/room_based_starting_position.rs index 95b1f99..16d092b 100644 --- a/src/map_builders/room_based_starting_position.rs +++ b/src/map_builders/room_based_starting_position.rs @@ -20,7 +20,7 @@ impl RoomBasedStartingPosition { let start_pos = rooms[0].center(); build_data.starting_position = Some(Position { x: start_pos.x, y: start_pos.y }); } else { - panic!("RoomBasedStartingPosition only works after rooms have been created"); + unreachable!("RoomBasedStartingPosition tried to run without any rooms."); } } } diff --git a/src/map_builders/room_corner_rounding.rs b/src/map_builders/room_corner_rounding.rs index 60c31c4..516635f 100644 --- a/src/map_builders/room_corner_rounding.rs +++ b/src/map_builders/room_corner_rounding.rs @@ -42,7 +42,7 @@ impl RoomCornerRounder { if let Some(rooms_builder) = &build_data.rooms { rooms = rooms_builder.clone(); } else { - panic!("RoomCornerRounding requires a builder with rooms."); + unreachable!("RoomCornerRounding tried to run without any rooms."); } for room in rooms.iter() { diff --git a/src/map_builders/room_draw.rs b/src/map_builders/room_draw.rs index b562ab5..8169b82 100644 --- a/src/map_builders/room_draw.rs +++ b/src/map_builders/room_draw.rs @@ -50,7 +50,7 @@ impl RoomDrawer { if let Some(rooms_builder) = &build_data.rooms { rooms = rooms_builder.clone(); } else { - panic!("RoomDrawer require a builder with rooms"); + unreachable!("RoomDrawer tried to run without any rooms."); } for room in rooms.iter() { diff --git a/src/map_builders/room_exploder.rs b/src/map_builders/room_exploder.rs index aef4bcb..eea3875 100644 --- a/src/map_builders/room_exploder.rs +++ b/src/map_builders/room_exploder.rs @@ -20,7 +20,7 @@ impl RoomExploder { if let Some(rooms_builder) = &build_data.rooms { rooms = rooms_builder.clone(); } else { - panic!("RoomExploder requires a builder with rooms."); + unreachable!("RoomExploder tried to run without any rooms."); } for room in rooms.iter() { let start = room.center(); diff --git a/src/map_builders/room_themer.rs b/src/map_builders/room_themer.rs index aac6062..2cc4ca1 100644 --- a/src/map_builders/room_themer.rs +++ b/src/map_builders/room_themer.rs @@ -141,7 +141,7 @@ impl ThemeRooms { if let Some(rooms_builder) = &build_data.rooms { rooms = rooms_builder.clone(); } else { - panic!("RoomCornerRounding requires a builder with rooms."); + unreachable!("RoomCornerRounding tried to run without any rooms."); } let count = roll_until_fail(rng, self.percent); diff --git a/src/map_builders/rooms_corridors_bresenham.rs b/src/map_builders/rooms_corridors_bresenham.rs index 3845076..53e7d26 100644 --- a/src/map_builders/rooms_corridors_bresenham.rs +++ b/src/map_builders/rooms_corridors_bresenham.rs @@ -22,7 +22,7 @@ impl BresenhamCorridors { if let Some(rooms_builder) = &build_data.rooms { rooms = rooms_builder.clone(); } else { - panic!("BresenhamCorridors require a builder with room structures"); + unreachable!("BresenhamCorridors tried to run without any rooms."); } let mut connected: HashSet = HashSet::new(); diff --git a/src/map_builders/rooms_corridors_bsp.rs b/src/map_builders/rooms_corridors_bsp.rs index 698b12b..052897c 100644 --- a/src/map_builders/rooms_corridors_bsp.rs +++ b/src/map_builders/rooms_corridors_bsp.rs @@ -21,7 +21,7 @@ impl BspCorridors { if let Some(rooms_builder) = &build_data.rooms { rooms = rooms_builder.clone(); } else { - panic!("BSP Corridors require a builder with room structures"); + unreachable!("BSP Corridors tried to run without any rooms."); } let mut corridors: Vec> = Vec::new(); diff --git a/src/map_builders/rooms_corridors_dogleg.rs b/src/map_builders/rooms_corridors_dogleg.rs index f914347..907a1a4 100644 --- a/src/map_builders/rooms_corridors_dogleg.rs +++ b/src/map_builders/rooms_corridors_dogleg.rs @@ -21,7 +21,7 @@ impl DoglegCorridors { if let Some(rooms_builder) = &build_data.rooms { rooms = rooms_builder.clone(); } else { - panic!("DoglegCorridors require a builder with rooms."); + unreachable!("DoglegCorridors tried to run without any rooms."); } let mut corridors: Vec> = Vec::new(); diff --git a/src/map_builders/rooms_corridors_nearest.rs b/src/map_builders/rooms_corridors_nearest.rs index 7177712..21b0312 100644 --- a/src/map_builders/rooms_corridors_nearest.rs +++ b/src/map_builders/rooms_corridors_nearest.rs @@ -22,7 +22,7 @@ impl NearestCorridors { if let Some(rooms_builder) = &build_data.rooms { rooms = rooms_builder.clone(); } else { - panic!("NearestCorridors requires a builder with rooms"); + unreachable!("NearestCorridors tried to run without any rooms."); } let mut connected: HashSet = HashSet::new(); diff --git a/src/map_builders/rooms_corridors_spawner.rs b/src/map_builders/rooms_corridors_spawner.rs index 88b8b84..a62c87a 100644 --- a/src/map_builders/rooms_corridors_spawner.rs +++ b/src/map_builders/rooms_corridors_spawner.rs @@ -27,7 +27,7 @@ impl CorridorSpawner { ); } } else { - panic!("CorridorSpawner only works after corridors have been created"); + unreachable!("CorridorSpawner tried to run without any corridors."); } } } diff --git a/src/raws/rawmaster.rs b/src/raws/rawmaster.rs index 5a820ad..813dbe2 100644 --- a/src/raws/rawmaster.rs +++ b/src/raws/rawmaster.rs @@ -759,7 +759,7 @@ pub fn table_by_name(raws: &RawMaster, key: &str, optional_difficulty: Option EquipmentSlot { if !raws.item_index.contains_key(tag) { - panic!("Trying to equip an unknown item: {}", tag); + unreachable!("Tried to equip an unknown item: {}", tag); } let item_index = raws.item_index[tag]; let item = &raws.raws.items[item_index]; @@ -794,7 +794,7 @@ fn find_slot_for_equippable_item(tag: &str, raws: &RawMaster) -> EquipmentSlot { } } } - panic!("Trying to equip {}, but it has no slot tag.", tag); + unreachable!("Tried to equip {}, but it has no slot tag.", tag); } pub fn roll_on_loot_table( @@ -1067,7 +1067,7 @@ fn parse_damage_string(n: &str) -> (DamageType, DiceType) { "fire" => DamageType::Fire, "cold" => DamageType::Cold, "poison" => DamageType::Poison, - _ => panic!("Unrecognised damage type in raws: {}", tokens[1]), + _ => unreachable!("Unrecognised damage type in raws: {}", tokens[1]), } } else { DamageType::Physical From 2d33c90af8dbc8329a1b457a53c44f3ea8a32dff Mon Sep 17 00:00:00 2001 From: Llywelwyn Date: Sat, 23 Sep 2023 11:12:28 +0100 Subject: [PATCH 23/50] fixes infini-dungeon difficulty --- src/map_builders/mod.rs | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/src/map_builders/mod.rs b/src/map_builders/mod.rs index 81045c6..7b38efe 100644 --- a/src/map_builders/mod.rs +++ b/src/map_builders/mod.rs @@ -438,21 +438,19 @@ pub fn random_builder( } pub fn level_builder( - new_id: i32, + id: i32, rng: &mut RandomNumberGenerator, width: i32, height: i32, initial_player_level: i32 ) -> BuilderChain { - // TODO: With difficulty and ID/depth decoupled, this can be used for branches later. - let difficulty = new_id; - match new_id { + match id { ID_OVERMAP => overmap_builder(), - ID_TOWN => town_builder(new_id, rng, width, height, 0, initial_player_level), - ID_TOWN2 => forest_builder(new_id, rng, width, height, 1, initial_player_level), + ID_TOWN => town_builder(id, rng, width, height, 0, initial_player_level), + ID_TOWN2 => forest_builder(id, rng, width, height, 1, initial_player_level), ID_TOWN3 => random_builder( - new_id, + id, rng, width, height, @@ -462,25 +460,25 @@ pub fn level_builder( true, BuildType::Room ), - _ if new_id >= ID_INFINITE => + _ if id >= ID_INFINITE => random_builder( - new_id, + id, rng, width, height, - difficulty, - new_id - ID_INFINITE + 1, + 4 + diff(ID_INFINITE, id), + 1 + diff(ID_INFINITE, id), initial_player_level, false, BuildType::Room ), - _ => + _ => // This should be unreachable!() eventually. Right now it's reachable with the debug/cheat menu. It should not be in normal gameplay. random_builder( - new_id, + id, rng, width, height, - difficulty, + 1, 404, initial_player_level, false, @@ -488,3 +486,7 @@ pub fn level_builder( ), } } + +fn diff(branch_id: i32, lvl_id: i32) -> i32 { + return lvl_id - branch_id; +} From fa3b906dce3cc3c3d187de9d23229a53786f2e6e Mon Sep 17 00:00:00 2001 From: Llywelwyn Date: Sat, 23 Sep 2023 11:21:34 +0100 Subject: [PATCH 24/50] adds article to ID morgue msg --- src/gamelog/events.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gamelog/events.rs b/src/gamelog/events.rs index bef5ff6..3e0006f 100644 --- a/src/gamelog/events.rs +++ b/src/gamelog/events.rs @@ -126,7 +126,7 @@ pub fn record_event(event: EVENT) { new_event = format!("Discovered {}", name); } EVENT::Identified(name) => { - new_event = format!("Identified {}", name); + new_event = format!("Identified {}", crate::gui::with_article(name)); } EVENT::PlayerDied(str) => { // Generating the String is handled in the death effect, to avoid passing the ecs here. From 1fa7432dfebd79638ca4212196a9f221e47d0ffc Mon Sep 17 00:00:00 2001 From: Llywelwyn Date: Sun, 1 Oct 2023 20:56:46 +0100 Subject: [PATCH 25/50] room accretion - initial --- src/map_builders/mod.rs | 2 + src/map_builders/room_accretion/consts.rs | 110 +++++++++++++++ src/map_builders/room_accretion/mod.rs | 161 ++++++++++++++++++++++ 3 files changed, 273 insertions(+) create mode 100644 src/map_builders/room_accretion/consts.rs create mode 100644 src/map_builders/room_accretion/mod.rs diff --git a/src/map_builders/mod.rs b/src/map_builders/mod.rs index 7b38efe..70a3c0f 100644 --- a/src/map_builders/mod.rs +++ b/src/map_builders/mod.rs @@ -1,6 +1,8 @@ use super::{ spawner, Map, Position, Rect, TileType }; use bracket_lib::prelude::*; +mod room_accretion; +use room_accretion::RoomAccretionBuilder; mod bsp_dungeon; use bsp_dungeon::BspDungeonBuilder; mod bsp_interior; diff --git a/src/map_builders/room_accretion/consts.rs b/src/map_builders/room_accretion/consts.rs new file mode 100644 index 0000000..f4170df --- /dev/null +++ b/src/map_builders/room_accretion/consts.rs @@ -0,0 +1,110 @@ +use lazy_static::lazy_static; +use bracket_lib::prelude::*; + +pub enum Operator { + LessThan, + GreaterThan, + LessThanEqualTo, + GreaterThanEqualTo, + EqualTo, +} + +impl Operator { + pub fn eval(&self, a: i32, b: i32) -> bool { + match self { + Operator::LessThan => a < b, + Operator::GreaterThan => a > b, + Operator::LessThanEqualTo => a <= b, + Operator::GreaterThanEqualTo => a >= b, + Operator::EqualTo => a == b, + } + } + pub fn string(&self) -> &str { + match self { + Operator::LessThan => "<", + Operator::GreaterThan => ">", + Operator::LessThanEqualTo => "<=", + Operator::GreaterThanEqualTo => ">=", + Operator::EqualTo => "==", + } + } +} + +pub struct CellRules { + pub adjacent_type: i32, + pub into: i32, + pub operator: Operator, + pub n: i32, +} + +impl CellRules { + const fn new(adjacent_type: i32, into: i32, operator: Operator, n: i32) -> CellRules { + CellRules { + adjacent_type, + into, + operator, + n, + } + } +} + +lazy_static! { + pub static ref CA: Vec> = vec![ + vec![ + CellRules::new(1, 1, Operator::GreaterThanEqualTo, 5), + CellRules::new(0, 1, Operator::LessThan, 2) + ], + vec![CellRules::new(1, 1, Operator::GreaterThanEqualTo, 5)] + ]; +} + +#[derive(Debug, Copy, Clone)] +pub enum Direction { + NoDir = -1, + North = 0, + East = 1, + South = 2, + West = 3, +} + +impl Direction { + pub fn transform(&self) -> Point { + match self { + Direction::NoDir => unreachable!("Direction::NoDir should never be transformed"), + Direction::North => Point::new(0, -1), + Direction::East => Point::new(1, 0), + Direction::South => Point::new(0, 1), + Direction::West => Point::new(-1, 0), + } + } +} + +pub struct DirectionIterator { + current: Direction, +} + +impl DirectionIterator { + pub fn new() -> DirectionIterator { + DirectionIterator { + current: Direction::North, + } + } +} + +impl Iterator for DirectionIterator { + type Item = Direction; + fn next(&mut self) -> Option { + use Direction::*; + let next_direction = match self.current { + North => East, + East => South, + South => West, + West => { + return None; + } + NoDir => unreachable!("Direction::NoDir should never be iterated over."), + }; + self.current = next_direction; + Some(next_direction) + } +} diff --git a/src/map_builders/room_accretion/mod.rs b/src/map_builders/room_accretion/mod.rs new file mode 100644 index 0000000..2e003f1 --- /dev/null +++ b/src/map_builders/room_accretion/mod.rs @@ -0,0 +1,161 @@ +use super::{ BuilderMap, Map, InitialMapBuilder, TileType, Point }; +use bracket_lib::prelude::*; + +mod consts; +use consts::*; + +/// Room Accretion map builder. +pub struct RoomAccretionBuilder {} + +impl InitialMapBuilder for RoomAccretionBuilder { + #[allow(dead_code)] + fn build_map(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) { + self.build(rng, build_data); + } +} + +impl RoomAccretionBuilder { + /// Constructor for Room Accretion. + pub fn new() -> Box { + Box::new(RoomAccretionBuilder {}) + } + + fn build(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) { + // + } +} + +fn grid_with_dimensions(h: usize, w: usize, value: i32) -> Vec> { + let mut grid = Vec::with_capacity(h); + for _ in 0..h { + let row = vec![value; w]; + grid.push(row); + } + grid +} + +fn in_bounds(x: i32, y: i32, build_data: &BuilderMap) -> bool { + x > 0 && x < build_data.height && y > 0 && y < build_data.width +} + +fn draw_continuous_shape_on_grid( + room: &Vec>, + top_offset: usize, + left_offset: usize, + grid: &mut Vec> +) { + for row in 0..room.len() { + for col in 0..room[0].len() { + if room[row][col] != 0 { + let target_row = row + top_offset; + let target_col = col + left_offset; + if target_row < grid.len() && target_col < grid[0].len() { + grid[target_row][target_col] = room[row][col]; + } + } + } + } +} + +struct Coordinate { + pub location: Point, + pub value: i32, +} + +fn draw_individual_coordinates_on_grid(coordinates: &Vec, grid: &mut Vec>) { + for c in coordinates { + let x = c.location.x as usize; + let y = c.location.y as usize; + if y < grid.len() && x < grid[0].len() { + grid[y][x] = c.value; + } + } +} + +fn get_cell_neighbours( + cells: &Vec>, + row: usize, + col: usize, + h: usize, + w: usize +) -> Vec { + let mut neighbours = Vec::new(); + for x in row.saturating_sub(1)..=std::cmp::min(row + 1, h - 1) { + for y in col.saturating_sub(1)..=std::cmp::min(col + 1, w - 1) { + if x != row || y != col { + neighbours.push(cells[x][y]); + } + } + } + neighbours +} + +fn make_ca_room(rng: &mut RandomNumberGenerator) -> Vec> { + let width = rng.range(5, 10); + let height = rng.range(5, 10); + let mut cells = grid_with_dimensions(height, width, 0); + cells = cells + .into_iter() + .map(|row| { + row.into_iter() + .map(|_| if rng.roll_dice(1, 2) == 1 { 1 } else { 0 }) + .collect() + }) + .collect(); + + let transform_cell = |state: i32, neighbours: &Vec| -> i32 { + let rules: &[CellRules] = &CA[state as usize]; + let mut new_state = state; + for rule in rules { + let n_neighbours = neighbours + .iter() + .filter(|&&neighbour| neighbour == rule.adjacent_type) + .count(); + if rule.operator.eval(n_neighbours as i32, rule.n) { + new_state = rule.into; + } + } + new_state + }; + + for _ in 0..5 { + let mut new_cells = vec![vec![0; width]; height]; + for row in 0..height { + for col in 0..height { + let neighbours = get_cell_neighbours(&cells, row, col, height, width); + let new_state = transform_cell(cells[row][col], &neighbours); + new_cells[row][col] = new_state; + } + } + cells = new_cells; + } + + cells +} + +fn direction_of_door( + grid: Vec>, + row: usize, + col: usize, + build_data: &BuilderMap +) -> Direction { + if grid[row][col] != 0 { + return Direction::NoDir; + } + let mut solution = Direction::NoDir; + let mut dir_iter = DirectionIterator::new(); + for dir in &mut dir_iter { + let new_col = (col as i32) + dir.transform().x; + let new_row = (row as i32) + dir.transform().y; + let opp_col = (col as i32) - dir.transform().x; + let opp_row = (row as i32) - dir.transform().y; + if + in_bounds(new_col, new_row, &build_data) && + in_bounds(new_col, new_row, &build_data) && + grid[opp_row as usize][opp_col as usize] != 0 + { + solution = dir; + } + } + return solution; +} From 97ca3a25e339495c1a8cc3efc1aba49141203609 Mon Sep 17 00:00:00 2001 From: Llywelwyn Date: Mon, 2 Oct 2023 04:43:01 +0100 Subject: [PATCH 26/50] doors and door directions - RA --- src/map_builders/room_accretion/consts.rs | 5 +- src/map_builders/room_accretion/mod.rs | 66 ++++++++++++++++++++++- 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/src/map_builders/room_accretion/consts.rs b/src/map_builders/room_accretion/consts.rs index f4170df..ae3c434 100644 --- a/src/map_builders/room_accretion/consts.rs +++ b/src/map_builders/room_accretion/consts.rs @@ -1,6 +1,9 @@ use lazy_static::lazy_static; use bracket_lib::prelude::*; +pub const HEIGHT: usize = 64; +pub const WIDTH: usize = 64; + pub enum Operator { LessThan, GreaterThan, @@ -58,7 +61,7 @@ lazy_static! { ]; } -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Copy, Clone, PartialEq)] pub enum Direction { NoDir = -1, North = 0, diff --git a/src/map_builders/room_accretion/mod.rs b/src/map_builders/room_accretion/mod.rs index 2e003f1..0871cb3 100644 --- a/src/map_builders/room_accretion/mod.rs +++ b/src/map_builders/room_accretion/mod.rs @@ -134,7 +134,7 @@ fn make_ca_room(rng: &mut RandomNumberGenerator) -> Vec> { } fn direction_of_door( - grid: Vec>, + grid: &Vec>, row: usize, col: usize, build_data: &BuilderMap @@ -159,3 +159,67 @@ fn direction_of_door( } return solution; } + +#[derive(Copy, Clone, PartialEq)] +pub struct DoorSite { + pub x: i32, + pub y: i32, + pub dir: Direction, +} + +fn choose_random_door_site( + room: Vec>, + rng: &mut RandomNumberGenerator, + build_data: &BuilderMap +) -> Vec { + let mut grid = grid_with_dimensions(HEIGHT, WIDTH, 0); + let mut door_sites: Vec = Vec::new(); + const LEFT_OFFSET: usize = ((WIDTH as f32) / 2.0) as usize; + const TOP_OFFSET: usize = ((HEIGHT as f32) / 2.0) as usize; + draw_continuous_shape_on_grid(&room, TOP_OFFSET, LEFT_OFFSET, &mut grid); + for row in 0..HEIGHT { + for col in 0..WIDTH { + if grid[row][col] == 0 { + let door_dir = direction_of_door(&grid, row, col, &build_data); + if door_dir == Direction::NoDir { + continue; + } + let mut door_failed = false; + let (mut trace_row, mut trace_col) = ( + (row as i32) + door_dir.transform().y, + (col as i32) + door_dir.transform().x, + ); + let mut i = 0; + while i < 10 && in_bounds(trace_row, trace_col, &build_data) && !door_failed { + if grid[trace_row as usize][trace_col as usize] != 0 { + door_failed = true; + } + trace_col += door_dir.transform().x; + trace_row += door_dir.transform().y; + i += 1; + } + if !door_failed { + // May need more information here. + door_sites.push(DoorSite { + x: col as i32, + y: row as i32, + dir: door_dir, + }); + } + } + } + } + let mut chosen_doors: Vec = Vec::new(); + let mut dir_iter = DirectionIterator::new(); + for dir in &mut dir_iter { + let doors_facing_this_dir: Vec<&DoorSite> = door_sites + .iter() + .filter(|&door| door.dir == dir) + .collect(); + if !doors_facing_this_dir.is_empty() { + let index = rng.range(0, doors_facing_this_dir.len()); + chosen_doors.push(*doors_facing_this_dir[index]); + } + } + chosen_doors +} From 7a27321bec72e5401794291ed22e047d36b9aeee Mon Sep 17 00:00:00 2001 From: Llywelwyn Date: Mon, 2 Oct 2023 07:00:28 +0100 Subject: [PATCH 27/50] initial tweaks - starting room w/ corridors + doors --- src/config/mod.rs | 2 +- src/map_builders/mod.rs | 9 +- src/map_builders/room_accretion/consts.rs | 13 +- src/map_builders/room_accretion/mod.rs | 167 +++++++++++++++++++--- 4 files changed, 168 insertions(+), 23 deletions(-) diff --git a/src/config/mod.rs b/src/config/mod.rs index f86c5df..5309151 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -34,7 +34,7 @@ impl Default for Config { fn default() -> Self { Config { logging: LogConfig { - show_mapgen: false, + show_mapgen: true, log_combat: false, log_spawning: false, log_ticks: false, diff --git a/src/map_builders/mod.rs b/src/map_builders/mod.rs index 70a3c0f..b9dcbfe 100644 --- a/src/map_builders/mod.rs +++ b/src/map_builders/mod.rs @@ -447,7 +447,7 @@ pub fn level_builder( initial_player_level: i32 ) -> BuilderChain { match id { - ID_OVERMAP => overmap_builder(), + ID_OVERMAP => room_accretion(), ID_TOWN => town_builder(id, rng, width, height, 0, initial_player_level), ID_TOWN2 => forest_builder(id, rng, width, height, 1, initial_player_level), ID_TOWN3 => @@ -492,3 +492,10 @@ pub fn level_builder( fn diff(branch_id: i32, lvl_id: i32) -> i32 { return lvl_id - branch_id; } + +fn room_accretion() -> BuilderChain { + let mut builder = BuilderChain::new(false, 110, 64, 64, 0, "room_accretion", "accretion", 0, 1); + builder.start_with(RoomAccretionBuilder::new()); + builder.with(AreaStartingPosition::new(XStart::CENTRE, YStart::CENTRE)); + builder +} diff --git a/src/map_builders/room_accretion/consts.rs b/src/map_builders/room_accretion/consts.rs index ae3c434..9fdedb2 100644 --- a/src/map_builders/room_accretion/consts.rs +++ b/src/map_builders/room_accretion/consts.rs @@ -3,6 +3,11 @@ use bracket_lib::prelude::*; pub const HEIGHT: usize = 64; pub const WIDTH: usize = 64; +pub const HALLWAY_CHANCE: f32 = 0.5; +pub const VERTICAL_CORRIDOR_MIN_LENGTH: i32 = 2; +pub const VERTICAL_CORRIDOR_MAX_LENGTH: i32 = 9; +pub const HORIZONTAL_CORRIDOR_MIN_LENGTH: i32 = 5; +pub const HORIZONTAL_CORRIDOR_MAX_LENGTH: i32 = 15; pub enum Operator { LessThan, @@ -53,11 +58,11 @@ impl CellRules { lazy_static! { pub static ref CA: Vec> = vec![ + vec![CellRules::new(1, 1, Operator::GreaterThanEqualTo, 4)], vec![ - CellRules::new(1, 1, Operator::GreaterThanEqualTo, 5), - CellRules::new(0, 1, Operator::LessThan, 2) - ], - vec![CellRules::new(1, 1, Operator::GreaterThanEqualTo, 5)] + CellRules::new(0, 0, Operator::GreaterThanEqualTo, 5), + CellRules::new(1, 0, Operator::LessThan, 2) + ] ]; } diff --git a/src/map_builders/room_accretion/mod.rs b/src/map_builders/room_accretion/mod.rs index 0871cb3..648317f 100644 --- a/src/map_builders/room_accretion/mod.rs +++ b/src/map_builders/room_accretion/mod.rs @@ -21,21 +21,16 @@ impl RoomAccretionBuilder { } fn build(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) { - // + accrete_rooms(rng, build_data); } } fn grid_with_dimensions(h: usize, w: usize, value: i32) -> Vec> { - let mut grid = Vec::with_capacity(h); - for _ in 0..h { - let row = vec![value; w]; - grid.push(row); - } - grid + vec![vec![value; w]; h] } -fn in_bounds(x: i32, y: i32, build_data: &BuilderMap) -> bool { - x > 0 && x < build_data.height && y > 0 && y < build_data.width +fn in_bounds(row: i32, col: i32, build_data: &BuilderMap) -> bool { + row > 0 && row < build_data.height && col > 0 && col < build_data.width } fn draw_continuous_shape_on_grid( @@ -87,10 +82,11 @@ fn get_cell_neighbours( } } } + console::log(&format!("neighbours: {:?}", neighbours)); neighbours } -fn make_ca_room(rng: &mut RandomNumberGenerator) -> Vec> { +fn make_ca_room(rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) -> Vec> { let width = rng.range(5, 10); let height = rng.range(5, 10); let mut cells = grid_with_dimensions(height, width, 0); @@ -121,7 +117,7 @@ fn make_ca_room(rng: &mut RandomNumberGenerator) -> Vec> { for _ in 0..5 { let mut new_cells = vec![vec![0; width]; height]; for row in 0..height { - for col in 0..height { + for col in 0..width { let neighbours = get_cell_neighbours(&cells, row, col, height, width); let new_state = transform_cell(cells[row][col], &neighbours); new_cells[row][col] = new_state; @@ -150,8 +146,8 @@ fn direction_of_door( let opp_col = (col as i32) - dir.transform().x; let opp_row = (row as i32) - dir.transform().y; if - in_bounds(new_col, new_row, &build_data) && - in_bounds(new_col, new_row, &build_data) && + in_bounds(new_row, new_col, &build_data) && + in_bounds(opp_row, opp_col, &build_data) && grid[opp_row as usize][opp_col as usize] != 0 { solution = dir; @@ -160,7 +156,7 @@ fn direction_of_door( return solution; } -#[derive(Copy, Clone, PartialEq)] +#[derive(Debug, Copy, Clone, PartialEq)] pub struct DoorSite { pub x: i32, pub y: i32, @@ -171,7 +167,7 @@ fn choose_random_door_site( room: Vec>, rng: &mut RandomNumberGenerator, build_data: &BuilderMap -) -> Vec { +) -> Vec> { let mut grid = grid_with_dimensions(HEIGHT, WIDTH, 0); let mut door_sites: Vec = Vec::new(); const LEFT_OFFSET: usize = ((WIDTH as f32) / 2.0) as usize; @@ -209,7 +205,7 @@ fn choose_random_door_site( } } } - let mut chosen_doors: Vec = Vec::new(); + let mut chosen_doors: Vec> = vec![None; 4]; let mut dir_iter = DirectionIterator::new(); for dir in &mut dir_iter { let doors_facing_this_dir: Vec<&DoorSite> = door_sites @@ -218,8 +214,145 @@ fn choose_random_door_site( .collect(); if !doors_facing_this_dir.is_empty() { let index = rng.range(0, doors_facing_this_dir.len()); - chosen_doors.push(*doors_facing_this_dir[index]); + chosen_doors[dir as usize] = Some(*doors_facing_this_dir[index]); } } chosen_doors } + +fn shuffle(list: &mut Vec, rng: &mut RandomNumberGenerator) { + let len = list.len(); + for i in (1..len).rev() { + let j = rng.range(0, i + 1); + list.swap(i, j); + } +} + +fn attach_hallway_to( + door_sites: &mut Vec>, + hyperspace: &mut Vec>, + rng: &mut RandomNumberGenerator, + build_data: &BuilderMap +) { + let mut directions = vec![Direction::North, Direction::East, Direction::South, Direction::West]; + shuffle(&mut directions, rng); + let mut hallway_dir: Direction = Direction::NoDir; + for i in 0..4 { + hallway_dir = directions[i]; + console::log( + &format!( + "i: {:?} | hallway_dir: {:?} (as usize: {:?}) | door_sites[hallway_dir]: {:?}", + i, + hallway_dir, + hallway_dir as usize, + door_sites[hallway_dir as usize] + ) + ); + if + door_sites[hallway_dir as usize].is_some() && + in_bounds( + door_sites[hallway_dir as usize].unwrap().y + + hallway_dir.transform().y * VERTICAL_CORRIDOR_MAX_LENGTH, + door_sites[hallway_dir as usize].unwrap().x + + hallway_dir.transform().x * HORIZONTAL_CORRIDOR_MAX_LENGTH, + &build_data + ) + { + break; + } + } + let transform = hallway_dir.transform(); + let hallway_len: i32 = match hallway_dir { + Direction::NoDir => { + console::log("no hallway_dir"); + return; + } + Direction::North | Direction::South => + rng.range(VERTICAL_CORRIDOR_MIN_LENGTH, VERTICAL_CORRIDOR_MAX_LENGTH + 1), + Direction::East | Direction::West => + rng.range(HORIZONTAL_CORRIDOR_MIN_LENGTH, HORIZONTAL_CORRIDOR_MAX_LENGTH + 1), + }; + console::log(&format!("hallway_len: {:?}", hallway_len)); + let mut x = door_sites[hallway_dir as usize].unwrap().x; + let mut y = door_sites[hallway_dir as usize].unwrap().y; + for _i in 0..hallway_len { + if in_bounds(y, x, &build_data) { + hyperspace[y as usize][x as usize] = 1; // Dig out corridor. + } + x += transform.x; + y += transform.y; + } + let new_site = DoorSite { + x, + y, + dir: hallway_dir, + }; + console::log(&format!("new_site: {:?}", new_site)); + door_sites[hallway_dir as usize] = Some(new_site); // Move door to end of corridor. +} + +fn design_room_in_hyperspace( + rng: &mut RandomNumberGenerator, + build_data: &mut BuilderMap +) -> Vec> { + // Project onto hyperspace + let mut hyperspace = grid_with_dimensions(HEIGHT, WIDTH, 0); + let room_type = rng.range(0, 1); + let room = match room_type { + 0 => make_ca_room(rng, build_data), + _ => unreachable!("Invalid room type."), + }; + draw_continuous_shape_on_grid(&room, HEIGHT / 2, WIDTH / 2, &mut hyperspace); + let mut door_sites = choose_random_door_site(room, rng, &build_data); + let roll: f32 = rng.rand(); + if roll < HALLWAY_CHANCE { + attach_hallway_to(&mut door_sites, &mut hyperspace, rng, &build_data); + } + let coords: Vec = door_sites + .iter() + .filter(|&door| door.is_some()) + .map(|&door| Coordinate { + location: Point::new(door.unwrap().x, door.unwrap().y), + value: 2, + }) + .collect(); + draw_individual_coordinates_on_grid(&coords, &mut hyperspace); + hyperspace +} + +fn map_i32_to_tiletype(val: i32, build_data: &mut BuilderMap) -> TileType { + match val { + 0 => TileType::Wall, + 1 => TileType::Floor, + 2 => TileType::Floor, // With door. + _ => unreachable!("Unknown TileType"), + } +} + +fn flatten_hyperspace_into_dungeon( + hyperspace: Vec>, + build_data: &mut BuilderMap +) -> Vec { + let flattened_hyperspace: Vec = hyperspace.into_iter().flatten().collect(); + flattened_hyperspace + .into_iter() + .enumerate() + .map(|(idx, cell)| { + if cell != 0 { + match cell { + 2 => build_data.spawn_list.push((idx, "door".to_string())), + _ => {} + } + map_i32_to_tiletype(cell, build_data) + } else { + build_data.map.tiles[idx % (build_data.map.width as usize)] + } + }) + .collect() +} + +fn accrete_rooms(rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) { + let hyperspace = design_room_in_hyperspace(rng, build_data); + build_data.map.tiles = flatten_hyperspace_into_dungeon(hyperspace, build_data); + build_data.take_snapshot(); +} From 190543a3611c9cfa5b257edacf0ef7f7cea26d7b Mon Sep 17 00:00:00 2001 From: Llywelwyn Date: Mon, 2 Oct 2023 07:39:45 +0100 Subject: [PATCH 28/50] move all doors to the ends of corridors --- src/map_builders/room_accretion/consts.rs | 9 +++ src/map_builders/room_accretion/mod.rs | 81 +++++++++++++++++------ 2 files changed, 69 insertions(+), 21 deletions(-) diff --git a/src/map_builders/room_accretion/consts.rs b/src/map_builders/room_accretion/consts.rs index 9fdedb2..8c10d16 100644 --- a/src/map_builders/room_accretion/consts.rs +++ b/src/map_builders/room_accretion/consts.rs @@ -85,6 +85,15 @@ impl Direction { Direction::West => Point::new(-1, 0), } } + pub fn opposite_dir(&self) -> Direction { + match self { + Direction::NoDir => unreachable!("Direction::NoDir has no opposite."), + Direction::North => Direction::South, + Direction::East => Direction::West, + Direction::South => Direction::North, + Direction::West => Direction::East, + } + } } pub struct DirectionIterator { diff --git a/src/map_builders/room_accretion/mod.rs b/src/map_builders/room_accretion/mod.rs index 648317f..13f729b 100644 --- a/src/map_builders/room_accretion/mod.rs +++ b/src/map_builders/room_accretion/mod.rs @@ -87,8 +87,8 @@ fn get_cell_neighbours( } fn make_ca_room(rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) -> Vec> { - let width = rng.range(5, 10); - let height = rng.range(5, 10); + let width = rng.range(5, 12); + let height = rng.range(5, 12); let mut cells = grid_with_dimensions(height, width, 0); cells = cells .into_iter() @@ -125,10 +125,44 @@ fn make_ca_room(rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) -> } cells = new_cells; } - + // TODO: Floodfill to keep largest contiguous blob cells } +fn room_fits_at( + hyperspace: Vec>, + top_offset: usize, + left_offset: usize, + build_data: &BuilderMap +) -> bool { + let mut x_dungeon: usize; + let mut y_dungeon: usize; + for y in 0..HEIGHT { + for x in 0..WIDTH { + if hyperspace[y][x] != 2 { + y_dungeon = y + top_offset; + x_dungeon = x + left_offset; + for i in y_dungeon.saturating_sub(1)..=std::cmp::min(y_dungeon + 1, WIDTH - 1) { + for j in x_dungeon.saturating_sub(1)..=std::cmp::min( + x_dungeon + 1, + HEIGHT - 1 + ) { + let pt = build_data.map.xy_idx(i as i32, j as i32); + if + !in_bounds(i as i32, j as i32, &build_data) || + !(build_data.map.tiles[pt] == TileType::Wall) || + build_data.spawn_list.contains(&(pt, "door".to_string())) + { + return false; + } + } + } + } + } + } + return true; +} + fn direction_of_door( grid: &Vec>, row: usize, @@ -228,6 +262,10 @@ fn shuffle(list: &mut Vec, rng: &mut RandomNumberGenerator) { } } +fn clamp(x: T, min: T, max: T) -> T { + if x < min { min } else if x > max { max } else { x } +} + fn attach_hallway_to( door_sites: &mut Vec>, hyperspace: &mut Vec>, @@ -239,15 +277,6 @@ fn attach_hallway_to( let mut hallway_dir: Direction = Direction::NoDir; for i in 0..4 { hallway_dir = directions[i]; - console::log( - &format!( - "i: {:?} | hallway_dir: {:?} (as usize: {:?}) | door_sites[hallway_dir]: {:?}", - i, - hallway_dir, - hallway_dir as usize, - door_sites[hallway_dir as usize] - ) - ); if door_sites[hallway_dir as usize].is_some() && in_bounds( @@ -264,7 +293,6 @@ fn attach_hallway_to( let transform = hallway_dir.transform(); let hallway_len: i32 = match hallway_dir { Direction::NoDir => { - console::log("no hallway_dir"); return; } Direction::North | Direction::South => @@ -272,7 +300,6 @@ fn attach_hallway_to( Direction::East | Direction::West => rng.range(HORIZONTAL_CORRIDOR_MIN_LENGTH, HORIZONTAL_CORRIDOR_MAX_LENGTH + 1), }; - console::log(&format!("hallway_len: {:?}", hallway_len)); let mut x = door_sites[hallway_dir as usize].unwrap().x; let mut y = door_sites[hallway_dir as usize].unwrap().y; for _i in 0..hallway_len { @@ -282,13 +309,25 @@ fn attach_hallway_to( x += transform.x; y += transform.y; } - let new_site = DoorSite { - x, - y, - dir: hallway_dir, - }; - console::log(&format!("new_site: {:?}", new_site)); - door_sites[hallway_dir as usize] = Some(new_site); // Move door to end of corridor. + + y = clamp(y - transform.y, 0, (HEIGHT as i32) - 1); + x = clamp(x - transform.x, 0, (WIDTH as i32) - 1); + + let mut dir_iter = DirectionIterator::new(); + for dir in &mut dir_iter { + if dir != hallway_dir.opposite_dir() { + let door_y = y + dir.transform().y; + let door_x = x + dir.transform().x; + door_sites[dir as usize] = Some(DoorSite { + x: door_x, + y: door_y, + dir, + }); + } else { + door_sites[dir as usize] = None; + } + } + console::log(&format!("door_sites: {:?}", door_sites)); } fn design_room_in_hyperspace( From b5743819ece6d614fe00eebfc73a35d07b2ad1ef Mon Sep 17 00:00:00 2001 From: Llywelwyn Date: Mon, 2 Oct 2023 21:11:12 +0100 Subject: [PATCH 29/50] .describe() for Intrinsics, for use in tooltips later --- src/components.rs | 28 ++++++++++++++++++++++++++++ src/gui/tooltip.rs | 6 ++++++ 2 files changed, 34 insertions(+) diff --git a/src/components.rs b/src/components.rs index 8c56cce..522b235 100644 --- a/src/components.rs +++ b/src/components.rs @@ -427,11 +427,39 @@ pub enum Intrinsic { Speed, // 4/3x speed multiplier } +impl Intrinsic { + pub fn describe(&self) -> &str { + match self { + Intrinsic::Regeneration => "regenerates health", + Intrinsic::Speed => "is hasted", + } + } +} + #[derive(Component, Serialize, Deserialize, Debug, Clone)] pub struct Intrinsics { pub list: HashSet, } +impl Intrinsics { + pub fn describe(&self) -> String { + let mut descriptions = Vec::new(); + for intrinsic in &self.list { + descriptions.push(intrinsic.describe()); + } + match descriptions.len() { + 0 => + unreachable!("describe() should never be called on an empty Intrinsics component."), + 1 => format!("It {}.", descriptions[0]), + _ => { + let last = descriptions.pop().unwrap(); + let joined = descriptions.join(", "); + format!("It {}, and {}.", joined, last) + } + } + } +} + #[derive(Component, Debug, ConvertSaveload, Clone)] pub struct InflictsDamage { pub damage_type: DamageType, diff --git a/src/gui/tooltip.rs b/src/gui/tooltip.rs index e11d50f..94c3f97 100644 --- a/src/gui/tooltip.rs +++ b/src/gui/tooltip.rs @@ -111,6 +111,12 @@ pub fn draw_tooltips(ecs: &World, ctx: &mut BTerm, xy: Option<(i32, i32)>) { if position.x == mouse_pos_adjusted.0 && position.y == mouse_pos_adjusted.1 { let mut tip = Tooltip::new(); tip.add(crate::gui::obfuscate_name_ecs(ecs, entity).0, renderable.fg); + let intrinsics = ecs.read_storage::(); + if let Some(intrinsics) = intrinsics.get(entity) { + if !intrinsics.list.is_empty() { + tip.add(intrinsics.describe(), RGB::named(WHITE)); + } + } // Attributes let attr = attributes.get(entity); if let Some(a) = attr { From 4d21bd46d4561f81a0ad7c29115af28584f59d83 Mon Sep 17 00:00:00 2001 From: Llywelwyn Date: Mon, 2 Oct 2023 22:14:00 +0100 Subject: [PATCH 30/50] add_intr!() macro for adding intrinsics to the Player If needed, player can just be replaced by another arg to the macro so this works on every other entity - but right now the player is the only thing to gain/lose intrinsics. --- src/components.rs | 6 ++++++ src/lib.rs | 3 +++ src/macros/mod.rs | 44 ++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 1 + src/saveload_system.rs | 2 ++ 5 files changed, 56 insertions(+) create mode 100644 src/macros/mod.rs diff --git a/src/components.rs b/src/components.rs index 522b235..22f3b9e 100644 --- a/src/components.rs +++ b/src/components.rs @@ -460,6 +460,12 @@ impl Intrinsics { } } +#[derive(Component, Serialize, Deserialize, Debug, Clone)] +pub struct IntrinsicChanged { + pub gained: HashSet, + pub lost: HashSet, +} + #[derive(Component, Debug, ConvertSaveload, Clone)] pub struct InflictsDamage { pub damage_type: DamageType, diff --git a/src/lib.rs b/src/lib.rs index 812c7be..6bbdd04 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,6 +8,9 @@ extern crate serde; #[macro_use] extern crate lazy_static; +#[macro_use] +pub mod macros; + pub mod camera; pub mod components; pub mod raws; diff --git a/src/macros/mod.rs b/src/macros/mod.rs new file mode 100644 index 0000000..6d965c4 --- /dev/null +++ b/src/macros/mod.rs @@ -0,0 +1,44 @@ +// macros/mod.rs + +#[macro_export] +macro_rules! player { + ($ecs:expr, $component:ty) => { + { + let player = $ecs.fetch::(); + let component = $ecs.read_storage::<$component>(); + if let Some(player_component) = component.get(*player) { + true + } else { + false + } + } + }; +} + +#[macro_export] +macro_rules! add_intr { + ($ecs:expr, $intrinsic:expr) => { + let player = $ecs.fetch::(); + let mut intrinsics = $ecs.write_storage::(); + if let Some(player_intrinsics) = intrinsics.get_mut(*player) { + if !player_intrinsics.list.contains(&$intrinsic) { + player_intrinsics.list.insert($intrinsic); + let mut intrinsic_changed = $ecs.write_storage::(); + if let Some(this_intrinsic_changed) = intrinsic_changed.get_mut(*player) { + this_intrinsic_changed.gained.insert($intrinsic); + } else { + intrinsic_changed.insert(*player, crate::IntrinsicChanged { + gained: { + let mut m = std::collections::HashSet::new(); + m.insert($intrinsic); + m + }, + lost: std::collections::HashSet::new() + }).expect("Failed to insert IntrinsicChanged component."); + } + } + } else { + unreachable!("add_intr!(): The player should always have an Intrinsics component."); + } + }; +} diff --git a/src/main.rs b/src/main.rs index f82ebbd..f430376 100644 --- a/src/main.rs +++ b/src/main.rs @@ -111,6 +111,7 @@ fn main() -> BError { gs.ecs.register::(); gs.ecs.register::(); gs.ecs.register::(); + gs.ecs.register::(); gs.ecs.register::>(); gs.ecs.register::(); gs.ecs.register::(); diff --git a/src/saveload_system.rs b/src/saveload_system.rs index 894e4ff..362b35f 100644 --- a/src/saveload_system.rs +++ b/src/saveload_system.rs @@ -95,6 +95,7 @@ pub fn save_game(ecs: &mut World) { IdentifiedItem, InBackpack, InflictsDamage, + IntrinsicChanged, Intrinsics, Item, KnownSpells, @@ -227,6 +228,7 @@ pub fn load_game(ecs: &mut World) { IdentifiedItem, InBackpack, InflictsDamage, + IntrinsicChanged, Intrinsics, Item, KnownSpells, From fa4612cf1ffa6ba3aad3f70122df6b79f51b8c3c Mon Sep 17 00:00:00 2001 From: Llywelwyn Date: Mon, 2 Oct 2023 23:02:34 +0100 Subject: [PATCH 31/50] changed get_noncursed() to helper on BUC struct --- src/components.rs | 9 +++++++++ src/effects/triggers.rs | 4 ++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/components.rs b/src/components.rs index 22f3b9e..696eab4 100644 --- a/src/components.rs +++ b/src/components.rs @@ -243,6 +243,15 @@ pub enum BUC { Blessed, } +impl BUC { + pub fn noncursed(&self) -> bool { + match self { + BUC::Cursed => false, + _ => true, + } + } +} + #[derive(Component, Debug, Serialize, Deserialize, Eq, PartialEq, Hash, Clone)] pub struct Beatitude { pub buc: BUC, diff --git a/src/effects/triggers.rs b/src/effects/triggers.rs index ac38ccb..a82496c 100644 --- a/src/effects/triggers.rs +++ b/src/effects/triggers.rs @@ -1,4 +1,4 @@ -use super::{ add_effect, get_noncursed, particles, spatial, EffectType, Entity, Targets, World }; +use super::{ add_effect, particles, spatial, EffectType, Entity, Targets, World }; use crate::{ gamelog, gui::item_colour_ecs, @@ -205,7 +205,7 @@ fn handle_healing( healing_item.modifier; add_effect( event.source, - EffectType::Healing { amount: roll, increment_max: get_noncursed(&event.buc) }, + EffectType::Healing { amount: roll, increment_max: event.buc.noncursed() }, event.target.clone() ); for target in get_entity_targets(&event.target) { From 46bbe14beac85b515422a6d0812bb6987e5f94f6 Mon Sep 17 00:00:00 2001 From: Llywelwyn Date: Mon, 2 Oct 2023 23:02:46 +0100 Subject: [PATCH 32/50] added effects for adding intrinsics --- src/effects/intrinsics.rs | 11 +++++ src/effects/mod.rs | 8 +++- src/macros/mod.rs | 95 +++++++++++++++++++++++++++++---------- 3 files changed, 90 insertions(+), 24 deletions(-) create mode 100644 src/effects/intrinsics.rs diff --git a/src/effects/intrinsics.rs b/src/effects/intrinsics.rs new file mode 100644 index 0000000..01776e1 --- /dev/null +++ b/src/effects/intrinsics.rs @@ -0,0 +1,11 @@ +use super::{ EffectSpawner, EffectType }; +use specs::prelude::*; + +pub fn add_intrinsic(ecs: &mut World, effect: &EffectSpawner, target: Entity) { + let intrinsic = if let EffectType::AddIntrinsic { intrinsic } = &effect.effect_type { + intrinsic + } else { + unreachable!("add_intrinsic() called with the wrong EffectType") + }; + add_intr!(ecs, target, *intrinsic); +} diff --git a/src/effects/mod.rs b/src/effects/mod.rs index c23b52b..5552f5a 100644 --- a/src/effects/mod.rs +++ b/src/effects/mod.rs @@ -4,13 +4,14 @@ use bracket_lib::prelude::*; use specs::prelude::*; use std::collections::VecDeque; use std::sync::Mutex; -use crate::components::DamageType; +use crate::components::*; mod damage; mod hunger; mod particles; mod targeting; mod triggers; +mod intrinsics; pub use targeting::aoe_tiles; @@ -51,6 +52,9 @@ pub enum EffectType { ModifyNutrition { amount: i32, }, + AddIntrinsic { + intrinsic: Intrinsic, + }, TriggerFire { trigger: Entity, }, @@ -153,6 +157,7 @@ fn tile_effect_hits_entities(effect: &EffectType) -> bool { EffectType::Healing { .. } => true, EffectType::ModifyNutrition { .. } => true, EffectType::Confusion { .. } => true, + EffectType::AddIntrinsic { .. } => true, _ => false, } } @@ -175,6 +180,7 @@ fn affect_entity(ecs: &mut World, effect: &EffectSpawner, target: Entity) { } EffectType::EntityDeath => damage::entity_death(ecs, effect, target), EffectType::ModifyNutrition { .. } => hunger::modify_nutrition(ecs, effect, target), + EffectType::AddIntrinsic { .. } => intrinsics::add_intrinsic(ecs, effect, target), _ => {} } } diff --git a/src/macros/mod.rs b/src/macros/mod.rs index 6d965c4..a064f44 100644 --- a/src/macros/mod.rs +++ b/src/macros/mod.rs @@ -1,7 +1,8 @@ // macros/mod.rs #[macro_export] -macro_rules! player { +/// Used to check if the player has a given component. +macro_rules! player_has_component { ($ecs:expr, $component:ty) => { { let player = $ecs.fetch::(); @@ -16,29 +17,77 @@ macro_rules! player { } #[macro_export] -macro_rules! add_intr { - ($ecs:expr, $intrinsic:expr) => { - let player = $ecs.fetch::(); - let mut intrinsics = $ecs.write_storage::(); - if let Some(player_intrinsics) = intrinsics.get_mut(*player) { - if !player_intrinsics.list.contains(&$intrinsic) { - player_intrinsics.list.insert($intrinsic); - let mut intrinsic_changed = $ecs.write_storage::(); - if let Some(this_intrinsic_changed) = intrinsic_changed.get_mut(*player) { - this_intrinsic_changed.gained.insert($intrinsic); - } else { - intrinsic_changed.insert(*player, crate::IntrinsicChanged { - gained: { - let mut m = std::collections::HashSet::new(); - m.insert($intrinsic); - m - }, - lost: std::collections::HashSet::new() - }).expect("Failed to insert IntrinsicChanged component."); - } +/// Used to check if a given entity has a given Intrinsic. +macro_rules! has { + ($ecs:expr, $entity:expr, $intrinsic:expr) => { + { + let intrinsics = $ecs.read_storage::(); + if let Some(has_intrinsics) = intrinsics.get($entity) { + has_intrinsics.list.contains(&$intrinsic) + } else { + false + } + } + }; +} + +#[macro_export] +/// Used to check if the player has a given Intrinsic. +macro_rules! player_has { + ($ecs:expr, $intrinsic:expr) => { + { + let player = $ecs.fetch::(); + let intrinsics = $ecs.read_storage::(); + if let Some(player_intrinsics) = intrinsics.get(*player) { + player_intrinsics.list.contains(&$intrinsic) + } else { + false + } + } + }; +} + +#[macro_export] +/// Handles adding an Intrinsic to the player, and adding it to the IntrinsicChanged component. +macro_rules! add_intr { + ($ecs:expr, $entity:expr, $intrinsic:expr) => { + { + let mut intrinsics = $ecs.write_storage::(); + if let Some(player_intrinsics) = intrinsics.get_mut($entity) { + if !player_intrinsics.list.contains(&$intrinsic) { + player_intrinsics.list.insert($intrinsic); + let mut intrinsic_changed = $ecs.write_storage::(); + if let Some(this_intrinsic_changed) = intrinsic_changed.get_mut($entity) { + this_intrinsic_changed.gained.insert($intrinsic); + } else { + intrinsic_changed.insert($entity, crate::IntrinsicChanged { + gained: { + let mut m = std::collections::HashSet::new(); + m.insert($intrinsic); + m + }, + lost: std::collections::HashSet::new() + }).expect("Failed to insert IntrinsicChanged component."); + } + } + } else { + intrinsics.insert($entity, crate::Intrinsics { + list: { + let mut m = std::collections::HashSet::new(); + m.insert($intrinsic); + m + } + }).expect("Failed to insert Intrinsics component."); + let mut intrinsic_changed = $ecs.write_storage::(); + intrinsic_changed.insert($entity, crate::IntrinsicChanged { + gained: { + let mut m = std::collections::HashSet::new(); + m.insert($intrinsic); + m + }, + lost: std::collections::HashSet::new() + }).expect("Failed to insert IntrinsicChanged component."); } - } else { - unreachable!("add_intr!(): The player should always have an Intrinsics component."); } }; } From 180532ee3e3bb53565ba8801515e5af8beb9994a Mon Sep 17 00:00:00 2001 From: Llywelwyn Date: Tue, 24 Oct 2023 11:13:43 +0100 Subject: [PATCH 33/50] cherry pick -> serde_json saves to bincode --- Cargo.toml | 1 + src/saveload_system.rs | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 24b6918..e764a60 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ serde = { version = "1.0.93", features = ["derive"]} serde_json = "1.0.39" toml = "0.5" lazy_static = "1.4.0" +bincode = "1.3.3" [dev-dependencies] criterion = { version = "^0.5" } diff --git a/src/saveload_system.rs b/src/saveload_system.rs index 362b35f..a589413 100644 --- a/src/saveload_system.rs +++ b/src/saveload_system.rs @@ -1,6 +1,5 @@ use super::components::*; use bracket_lib::prelude::*; -use specs::error::NoError; use specs::prelude::*; use specs::saveload::{ DeserializeComponents, @@ -12,11 +11,12 @@ use specs::saveload::{ use std::fs; use std::fs::File; use std::path::Path; +use std::convert::Infallible; macro_rules! serialize_individually { ($ecs:expr, $ser:expr, $data:expr, $($type:ty),*) => { $( - SerializeComponents::>::serialize( + SerializeComponents::>::serialize( &( $ecs.read_storage::<$type>(), ), &$data.0, &$data.1, @@ -55,8 +55,8 @@ pub fn save_game(ecs: &mut World) { { let data = (ecs.entities(), ecs.read_storage::>()); - let writer = File::create("./savegame.json").unwrap(); - let mut serializer = serde_json::Serializer::new(writer); + let writer = File::create("./savegame.bin").unwrap(); + let mut serializer = bincode::Serializer::new(writer, bincode::options()); serialize_individually!( ecs, serializer, @@ -150,13 +150,13 @@ pub fn save_game(ecs: &mut World) { } pub fn does_save_exist() -> bool { - Path::new("./savegame.json").exists() + Path::new("./savegame.bin").exists() } macro_rules! deserialize_individually { ($ecs:expr, $de:expr, $data:expr, $($type:ty),*) => { $( - DeserializeComponents::::deserialize( + DeserializeComponents::::deserialize( &mut ( &mut $ecs.write_storage::<$type>(), ), &$data.0, // entities &mut $data.1, // marker @@ -180,8 +180,8 @@ pub fn load_game(ecs: &mut World) { } } - let data = fs::read_to_string("./savegame.json").unwrap(); - let mut de = serde_json::Deserializer::from_str(&data); + let data = fs::read("./savegame.bin").unwrap(); + let mut de = bincode::Deserializer::with_reader(&*data, bincode::options()); { let mut d = ( @@ -311,7 +311,7 @@ pub fn load_game(ecs: &mut World) { } pub fn delete_save() { - if Path::new("./savegame.json").exists() { - std::fs::remove_file("./savegame.json").expect("Unable to delete file"); + if Path::new("./savegame.bin").exists() { + std::fs::remove_file("./savegame.bin").expect("Unable to delete file"); } } From 9c8f3014911730cd402ae2842fbbb4b7a32cb724 Mon Sep 17 00:00:00 2001 From: Llywelwyn Date: Tue, 24 Oct 2023 23:32:42 +0100 Subject: [PATCH 34/50] Infallible -> NoError --- src/saveload_system.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/saveload_system.rs b/src/saveload_system.rs index a589413..5260b82 100644 --- a/src/saveload_system.rs +++ b/src/saveload_system.rs @@ -8,15 +8,15 @@ use specs::saveload::{ SimpleMarker, SimpleMarkerAllocator, }; +use specs::error::NoError; use std::fs; use std::fs::File; use std::path::Path; -use std::convert::Infallible; macro_rules! serialize_individually { ($ecs:expr, $ser:expr, $data:expr, $($type:ty),*) => { $( - SerializeComponents::>::serialize( + SerializeComponents::>::serialize( &( $ecs.read_storage::<$type>(), ), &$data.0, &$data.1, @@ -156,7 +156,7 @@ pub fn does_save_exist() -> bool { macro_rules! deserialize_individually { ($ecs:expr, $de:expr, $data:expr, $($type:ty),*) => { $( - DeserializeComponents::::deserialize( + DeserializeComponents::::deserialize( &mut ( &mut $ecs.write_storage::<$type>(), ), &$data.0, // entities &mut $data.1, // marker From c73f9a545863de84297faf4fe932314724125a2b Mon Sep 17 00:00:00 2001 From: Llywelwyn Date: Sat, 15 Jun 2024 16:42:59 +0100 Subject: [PATCH 35/50] Revert "cherry pick -> serde_json saves to bincode" This reverts commit 180532ee3e3bb53565ba8801515e5af8beb9994a. --- Cargo.toml | 1 - src/saveload_system.rs | 15 ++++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e764a60..24b6918 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,6 @@ serde = { version = "1.0.93", features = ["derive"]} serde_json = "1.0.39" toml = "0.5" lazy_static = "1.4.0" -bincode = "1.3.3" [dev-dependencies] criterion = { version = "^0.5" } diff --git a/src/saveload_system.rs b/src/saveload_system.rs index 5260b82..ccdc1ee 100644 --- a/src/saveload_system.rs +++ b/src/saveload_system.rs @@ -1,5 +1,6 @@ use super::components::*; use bracket_lib::prelude::*; +use specs::error::NoError; use specs::prelude::*; use specs::saveload::{ DeserializeComponents, @@ -55,8 +56,8 @@ pub fn save_game(ecs: &mut World) { { let data = (ecs.entities(), ecs.read_storage::>()); - let writer = File::create("./savegame.bin").unwrap(); - let mut serializer = bincode::Serializer::new(writer, bincode::options()); + let writer = File::create("./savegame.json").unwrap(); + let mut serializer = serde_json::Serializer::new(writer); serialize_individually!( ecs, serializer, @@ -150,7 +151,7 @@ pub fn save_game(ecs: &mut World) { } pub fn does_save_exist() -> bool { - Path::new("./savegame.bin").exists() + Path::new("./savegame.json").exists() } macro_rules! deserialize_individually { @@ -180,8 +181,8 @@ pub fn load_game(ecs: &mut World) { } } - let data = fs::read("./savegame.bin").unwrap(); - let mut de = bincode::Deserializer::with_reader(&*data, bincode::options()); + let data = fs::read_to_string("./savegame.json").unwrap(); + let mut de = serde_json::Deserializer::from_str(&data); { let mut d = ( @@ -311,7 +312,7 @@ pub fn load_game(ecs: &mut World) { } pub fn delete_save() { - if Path::new("./savegame.bin").exists() { - std::fs::remove_file("./savegame.bin").expect("Unable to delete file"); + if Path::new("./savegame.json").exists() { + std::fs::remove_file("./savegame.json").expect("Unable to delete file"); } } From 30697a98bb4b4bbe3188f42ea75206929dbf22ba Mon Sep 17 00:00:00 2001 From: Llywelwyn Date: Sat, 15 Jun 2024 16:43:17 +0100 Subject: [PATCH 36/50] rm room_accretion for now --- src/map_builders/mod.rs | 11 +- src/map_builders/room_accretion/consts.rs | 127 ------- src/map_builders/room_accretion/mod.rs | 397 ---------------------- 3 files changed, 1 insertion(+), 534 deletions(-) delete mode 100644 src/map_builders/room_accretion/consts.rs delete mode 100644 src/map_builders/room_accretion/mod.rs diff --git a/src/map_builders/mod.rs b/src/map_builders/mod.rs index b9dcbfe..7b38efe 100644 --- a/src/map_builders/mod.rs +++ b/src/map_builders/mod.rs @@ -1,8 +1,6 @@ use super::{ spawner, Map, Position, Rect, TileType }; use bracket_lib::prelude::*; -mod room_accretion; -use room_accretion::RoomAccretionBuilder; mod bsp_dungeon; use bsp_dungeon::BspDungeonBuilder; mod bsp_interior; @@ -447,7 +445,7 @@ pub fn level_builder( initial_player_level: i32 ) -> BuilderChain { match id { - ID_OVERMAP => room_accretion(), + ID_OVERMAP => overmap_builder(), ID_TOWN => town_builder(id, rng, width, height, 0, initial_player_level), ID_TOWN2 => forest_builder(id, rng, width, height, 1, initial_player_level), ID_TOWN3 => @@ -492,10 +490,3 @@ pub fn level_builder( fn diff(branch_id: i32, lvl_id: i32) -> i32 { return lvl_id - branch_id; } - -fn room_accretion() -> BuilderChain { - let mut builder = BuilderChain::new(false, 110, 64, 64, 0, "room_accretion", "accretion", 0, 1); - builder.start_with(RoomAccretionBuilder::new()); - builder.with(AreaStartingPosition::new(XStart::CENTRE, YStart::CENTRE)); - builder -} diff --git a/src/map_builders/room_accretion/consts.rs b/src/map_builders/room_accretion/consts.rs deleted file mode 100644 index 8c10d16..0000000 --- a/src/map_builders/room_accretion/consts.rs +++ /dev/null @@ -1,127 +0,0 @@ -use lazy_static::lazy_static; -use bracket_lib::prelude::*; - -pub const HEIGHT: usize = 64; -pub const WIDTH: usize = 64; -pub const HALLWAY_CHANCE: f32 = 0.5; -pub const VERTICAL_CORRIDOR_MIN_LENGTH: i32 = 2; -pub const VERTICAL_CORRIDOR_MAX_LENGTH: i32 = 9; -pub const HORIZONTAL_CORRIDOR_MIN_LENGTH: i32 = 5; -pub const HORIZONTAL_CORRIDOR_MAX_LENGTH: i32 = 15; - -pub enum Operator { - LessThan, - GreaterThan, - LessThanEqualTo, - GreaterThanEqualTo, - EqualTo, -} - -impl Operator { - pub fn eval(&self, a: i32, b: i32) -> bool { - match self { - Operator::LessThan => a < b, - Operator::GreaterThan => a > b, - Operator::LessThanEqualTo => a <= b, - Operator::GreaterThanEqualTo => a >= b, - Operator::EqualTo => a == b, - } - } - pub fn string(&self) -> &str { - match self { - Operator::LessThan => "<", - Operator::GreaterThan => ">", - Operator::LessThanEqualTo => "<=", - Operator::GreaterThanEqualTo => ">=", - Operator::EqualTo => "==", - } - } -} - -pub struct CellRules { - pub adjacent_type: i32, - pub into: i32, - pub operator: Operator, - pub n: i32, -} - -impl CellRules { - const fn new(adjacent_type: i32, into: i32, operator: Operator, n: i32) -> CellRules { - CellRules { - adjacent_type, - into, - operator, - n, - } - } -} - -lazy_static! { - pub static ref CA: Vec> = vec![ - vec![CellRules::new(1, 1, Operator::GreaterThanEqualTo, 4)], - vec![ - CellRules::new(0, 0, Operator::GreaterThanEqualTo, 5), - CellRules::new(1, 0, Operator::LessThan, 2) - ] - ]; -} - -#[derive(Debug, Copy, Clone, PartialEq)] -pub enum Direction { - NoDir = -1, - North = 0, - East = 1, - South = 2, - West = 3, -} - -impl Direction { - pub fn transform(&self) -> Point { - match self { - Direction::NoDir => unreachable!("Direction::NoDir should never be transformed"), - Direction::North => Point::new(0, -1), - Direction::East => Point::new(1, 0), - Direction::South => Point::new(0, 1), - Direction::West => Point::new(-1, 0), - } - } - pub fn opposite_dir(&self) -> Direction { - match self { - Direction::NoDir => unreachable!("Direction::NoDir has no opposite."), - Direction::North => Direction::South, - Direction::East => Direction::West, - Direction::South => Direction::North, - Direction::West => Direction::East, - } - } -} - -pub struct DirectionIterator { - current: Direction, -} - -impl DirectionIterator { - pub fn new() -> DirectionIterator { - DirectionIterator { - current: Direction::North, - } - } -} - -impl Iterator for DirectionIterator { - type Item = Direction; - fn next(&mut self) -> Option { - use Direction::*; - let next_direction = match self.current { - North => East, - East => South, - South => West, - West => { - return None; - } - NoDir => unreachable!("Direction::NoDir should never be iterated over."), - }; - self.current = next_direction; - Some(next_direction) - } -} diff --git a/src/map_builders/room_accretion/mod.rs b/src/map_builders/room_accretion/mod.rs deleted file mode 100644 index 13f729b..0000000 --- a/src/map_builders/room_accretion/mod.rs +++ /dev/null @@ -1,397 +0,0 @@ -use super::{ BuilderMap, Map, InitialMapBuilder, TileType, Point }; -use bracket_lib::prelude::*; - -mod consts; -use consts::*; - -/// Room Accretion map builder. -pub struct RoomAccretionBuilder {} - -impl InitialMapBuilder for RoomAccretionBuilder { - #[allow(dead_code)] - fn build_map(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) { - self.build(rng, build_data); - } -} - -impl RoomAccretionBuilder { - /// Constructor for Room Accretion. - pub fn new() -> Box { - Box::new(RoomAccretionBuilder {}) - } - - fn build(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) { - accrete_rooms(rng, build_data); - } -} - -fn grid_with_dimensions(h: usize, w: usize, value: i32) -> Vec> { - vec![vec![value; w]; h] -} - -fn in_bounds(row: i32, col: i32, build_data: &BuilderMap) -> bool { - row > 0 && row < build_data.height && col > 0 && col < build_data.width -} - -fn draw_continuous_shape_on_grid( - room: &Vec>, - top_offset: usize, - left_offset: usize, - grid: &mut Vec> -) { - for row in 0..room.len() { - for col in 0..room[0].len() { - if room[row][col] != 0 { - let target_row = row + top_offset; - let target_col = col + left_offset; - if target_row < grid.len() && target_col < grid[0].len() { - grid[target_row][target_col] = room[row][col]; - } - } - } - } -} - -struct Coordinate { - pub location: Point, - pub value: i32, -} - -fn draw_individual_coordinates_on_grid(coordinates: &Vec, grid: &mut Vec>) { - for c in coordinates { - let x = c.location.x as usize; - let y = c.location.y as usize; - if y < grid.len() && x < grid[0].len() { - grid[y][x] = c.value; - } - } -} - -fn get_cell_neighbours( - cells: &Vec>, - row: usize, - col: usize, - h: usize, - w: usize -) -> Vec { - let mut neighbours = Vec::new(); - for x in row.saturating_sub(1)..=std::cmp::min(row + 1, h - 1) { - for y in col.saturating_sub(1)..=std::cmp::min(col + 1, w - 1) { - if x != row || y != col { - neighbours.push(cells[x][y]); - } - } - } - console::log(&format!("neighbours: {:?}", neighbours)); - neighbours -} - -fn make_ca_room(rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) -> Vec> { - let width = rng.range(5, 12); - let height = rng.range(5, 12); - let mut cells = grid_with_dimensions(height, width, 0); - cells = cells - .into_iter() - .map(|row| { - row.into_iter() - .map(|_| if rng.roll_dice(1, 2) == 1 { 1 } else { 0 }) - .collect() - }) - .collect(); - - let transform_cell = |state: i32, neighbours: &Vec| -> i32 { - let rules: &[CellRules] = &CA[state as usize]; - let mut new_state = state; - for rule in rules { - let n_neighbours = neighbours - .iter() - .filter(|&&neighbour| neighbour == rule.adjacent_type) - .count(); - if rule.operator.eval(n_neighbours as i32, rule.n) { - new_state = rule.into; - } - } - new_state - }; - - for _ in 0..5 { - let mut new_cells = vec![vec![0; width]; height]; - for row in 0..height { - for col in 0..width { - let neighbours = get_cell_neighbours(&cells, row, col, height, width); - let new_state = transform_cell(cells[row][col], &neighbours); - new_cells[row][col] = new_state; - } - } - cells = new_cells; - } - // TODO: Floodfill to keep largest contiguous blob - cells -} - -fn room_fits_at( - hyperspace: Vec>, - top_offset: usize, - left_offset: usize, - build_data: &BuilderMap -) -> bool { - let mut x_dungeon: usize; - let mut y_dungeon: usize; - for y in 0..HEIGHT { - for x in 0..WIDTH { - if hyperspace[y][x] != 2 { - y_dungeon = y + top_offset; - x_dungeon = x + left_offset; - for i in y_dungeon.saturating_sub(1)..=std::cmp::min(y_dungeon + 1, WIDTH - 1) { - for j in x_dungeon.saturating_sub(1)..=std::cmp::min( - x_dungeon + 1, - HEIGHT - 1 - ) { - let pt = build_data.map.xy_idx(i as i32, j as i32); - if - !in_bounds(i as i32, j as i32, &build_data) || - !(build_data.map.tiles[pt] == TileType::Wall) || - build_data.spawn_list.contains(&(pt, "door".to_string())) - { - return false; - } - } - } - } - } - } - return true; -} - -fn direction_of_door( - grid: &Vec>, - row: usize, - col: usize, - build_data: &BuilderMap -) -> Direction { - if grid[row][col] != 0 { - return Direction::NoDir; - } - let mut solution = Direction::NoDir; - let mut dir_iter = DirectionIterator::new(); - for dir in &mut dir_iter { - let new_col = (col as i32) + dir.transform().x; - let new_row = (row as i32) + dir.transform().y; - let opp_col = (col as i32) - dir.transform().x; - let opp_row = (row as i32) - dir.transform().y; - if - in_bounds(new_row, new_col, &build_data) && - in_bounds(opp_row, opp_col, &build_data) && - grid[opp_row as usize][opp_col as usize] != 0 - { - solution = dir; - } - } - return solution; -} - -#[derive(Debug, Copy, Clone, PartialEq)] -pub struct DoorSite { - pub x: i32, - pub y: i32, - pub dir: Direction, -} - -fn choose_random_door_site( - room: Vec>, - rng: &mut RandomNumberGenerator, - build_data: &BuilderMap -) -> Vec> { - let mut grid = grid_with_dimensions(HEIGHT, WIDTH, 0); - let mut door_sites: Vec = Vec::new(); - const LEFT_OFFSET: usize = ((WIDTH as f32) / 2.0) as usize; - const TOP_OFFSET: usize = ((HEIGHT as f32) / 2.0) as usize; - draw_continuous_shape_on_grid(&room, TOP_OFFSET, LEFT_OFFSET, &mut grid); - for row in 0..HEIGHT { - for col in 0..WIDTH { - if grid[row][col] == 0 { - let door_dir = direction_of_door(&grid, row, col, &build_data); - if door_dir == Direction::NoDir { - continue; - } - let mut door_failed = false; - let (mut trace_row, mut trace_col) = ( - (row as i32) + door_dir.transform().y, - (col as i32) + door_dir.transform().x, - ); - let mut i = 0; - while i < 10 && in_bounds(trace_row, trace_col, &build_data) && !door_failed { - if grid[trace_row as usize][trace_col as usize] != 0 { - door_failed = true; - } - trace_col += door_dir.transform().x; - trace_row += door_dir.transform().y; - i += 1; - } - if !door_failed { - // May need more information here. - door_sites.push(DoorSite { - x: col as i32, - y: row as i32, - dir: door_dir, - }); - } - } - } - } - let mut chosen_doors: Vec> = vec![None; 4]; - let mut dir_iter = DirectionIterator::new(); - for dir in &mut dir_iter { - let doors_facing_this_dir: Vec<&DoorSite> = door_sites - .iter() - .filter(|&door| door.dir == dir) - .collect(); - if !doors_facing_this_dir.is_empty() { - let index = rng.range(0, doors_facing_this_dir.len()); - chosen_doors[dir as usize] = Some(*doors_facing_this_dir[index]); - } - } - chosen_doors -} - -fn shuffle(list: &mut Vec, rng: &mut RandomNumberGenerator) { - let len = list.len(); - for i in (1..len).rev() { - let j = rng.range(0, i + 1); - list.swap(i, j); - } -} - -fn clamp(x: T, min: T, max: T) -> T { - if x < min { min } else if x > max { max } else { x } -} - -fn attach_hallway_to( - door_sites: &mut Vec>, - hyperspace: &mut Vec>, - rng: &mut RandomNumberGenerator, - build_data: &BuilderMap -) { - let mut directions = vec![Direction::North, Direction::East, Direction::South, Direction::West]; - shuffle(&mut directions, rng); - let mut hallway_dir: Direction = Direction::NoDir; - for i in 0..4 { - hallway_dir = directions[i]; - if - door_sites[hallway_dir as usize].is_some() && - in_bounds( - door_sites[hallway_dir as usize].unwrap().y + - hallway_dir.transform().y * VERTICAL_CORRIDOR_MAX_LENGTH, - door_sites[hallway_dir as usize].unwrap().x + - hallway_dir.transform().x * HORIZONTAL_CORRIDOR_MAX_LENGTH, - &build_data - ) - { - break; - } - } - let transform = hallway_dir.transform(); - let hallway_len: i32 = match hallway_dir { - Direction::NoDir => { - return; - } - Direction::North | Direction::South => - rng.range(VERTICAL_CORRIDOR_MIN_LENGTH, VERTICAL_CORRIDOR_MAX_LENGTH + 1), - Direction::East | Direction::West => - rng.range(HORIZONTAL_CORRIDOR_MIN_LENGTH, HORIZONTAL_CORRIDOR_MAX_LENGTH + 1), - }; - let mut x = door_sites[hallway_dir as usize].unwrap().x; - let mut y = door_sites[hallway_dir as usize].unwrap().y; - for _i in 0..hallway_len { - if in_bounds(y, x, &build_data) { - hyperspace[y as usize][x as usize] = 1; // Dig out corridor. - } - x += transform.x; - y += transform.y; - } - - y = clamp(y - transform.y, 0, (HEIGHT as i32) - 1); - x = clamp(x - transform.x, 0, (WIDTH as i32) - 1); - - let mut dir_iter = DirectionIterator::new(); - for dir in &mut dir_iter { - if dir != hallway_dir.opposite_dir() { - let door_y = y + dir.transform().y; - let door_x = x + dir.transform().x; - door_sites[dir as usize] = Some(DoorSite { - x: door_x, - y: door_y, - dir, - }); - } else { - door_sites[dir as usize] = None; - } - } - console::log(&format!("door_sites: {:?}", door_sites)); -} - -fn design_room_in_hyperspace( - rng: &mut RandomNumberGenerator, - build_data: &mut BuilderMap -) -> Vec> { - // Project onto hyperspace - let mut hyperspace = grid_with_dimensions(HEIGHT, WIDTH, 0); - let room_type = rng.range(0, 1); - let room = match room_type { - 0 => make_ca_room(rng, build_data), - _ => unreachable!("Invalid room type."), - }; - draw_continuous_shape_on_grid(&room, HEIGHT / 2, WIDTH / 2, &mut hyperspace); - let mut door_sites = choose_random_door_site(room, rng, &build_data); - let roll: f32 = rng.rand(); - if roll < HALLWAY_CHANCE { - attach_hallway_to(&mut door_sites, &mut hyperspace, rng, &build_data); - } - let coords: Vec = door_sites - .iter() - .filter(|&door| door.is_some()) - .map(|&door| Coordinate { - location: Point::new(door.unwrap().x, door.unwrap().y), - value: 2, - }) - .collect(); - draw_individual_coordinates_on_grid(&coords, &mut hyperspace); - hyperspace -} - -fn map_i32_to_tiletype(val: i32, build_data: &mut BuilderMap) -> TileType { - match val { - 0 => TileType::Wall, - 1 => TileType::Floor, - 2 => TileType::Floor, // With door. - _ => unreachable!("Unknown TileType"), - } -} - -fn flatten_hyperspace_into_dungeon( - hyperspace: Vec>, - build_data: &mut BuilderMap -) -> Vec { - let flattened_hyperspace: Vec = hyperspace.into_iter().flatten().collect(); - flattened_hyperspace - .into_iter() - .enumerate() - .map(|(idx, cell)| { - if cell != 0 { - match cell { - 2 => build_data.spawn_list.push((idx, "door".to_string())), - _ => {} - } - map_i32_to_tiletype(cell, build_data) - } else { - build_data.map.tiles[idx % (build_data.map.width as usize)] - } - }) - .collect() -} - -fn accrete_rooms(rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) { - let hyperspace = design_room_in_hyperspace(rng, build_data); - build_data.map.tiles = flatten_hyperspace_into_dungeon(hyperspace, build_data); - build_data.take_snapshot(); -} From 678636c57dc56960ea3e9fd95cd1020e7d134157 Mon Sep 17 00:00:00 2001 From: Llywelwyn Date: Sat, 15 Jun 2024 16:44:13 +0100 Subject: [PATCH 37/50] Revert "Infallible -> NoError" This reverts commit 9c8f3014911730cd402ae2842fbbb4b7a32cb724. --- src/saveload_system.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/saveload_system.rs b/src/saveload_system.rs index ccdc1ee..3e3debe 100644 --- a/src/saveload_system.rs +++ b/src/saveload_system.rs @@ -9,15 +9,15 @@ use specs::saveload::{ SimpleMarker, SimpleMarkerAllocator, }; -use specs::error::NoError; use std::fs; use std::fs::File; use std::path::Path; +use std::convert::Infallible; macro_rules! serialize_individually { ($ecs:expr, $ser:expr, $data:expr, $($type:ty),*) => { $( - SerializeComponents::>::serialize( + SerializeComponents::>::serialize( &( $ecs.read_storage::<$type>(), ), &$data.0, &$data.1, @@ -157,7 +157,7 @@ pub fn does_save_exist() -> bool { macro_rules! deserialize_individually { ($ecs:expr, $de:expr, $data:expr, $($type:ty),*) => { $( - DeserializeComponents::::deserialize( + DeserializeComponents::::deserialize( &mut ( &mut $ecs.write_storage::<$type>(), ), &$data.0, // entities &mut $data.1, // marker From a7c5d2167cdeda0c852028916da79be3b7412fd1 Mon Sep 17 00:00:00 2001 From: Llywelwyn Date: Sat, 15 Jun 2024 16:46:15 +0100 Subject: [PATCH 38/50] back to serde_json --- src/saveload_system.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/saveload_system.rs b/src/saveload_system.rs index 3e3debe..3a3f733 100644 --- a/src/saveload_system.rs +++ b/src/saveload_system.rs @@ -9,15 +9,15 @@ use specs::saveload::{ SimpleMarker, SimpleMarkerAllocator, }; + use std::fs; use std::fs::File; use std::path::Path; -use std::convert::Infallible; macro_rules! serialize_individually { ($ecs:expr, $ser:expr, $data:expr, $($type:ty),*) => { $( - SerializeComponents::>::serialize( + SerializeComponents::>::serialize( &( $ecs.read_storage::<$type>(), ), &$data.0, &$data.1, @@ -157,7 +157,7 @@ pub fn does_save_exist() -> bool { macro_rules! deserialize_individually { ($ecs:expr, $de:expr, $data:expr, $($type:ty),*) => { $( - DeserializeComponents::::deserialize( + DeserializeComponents::::deserialize( &mut ( &mut $ecs.write_storage::<$type>(), ), &$data.0, // entities &mut $data.1, // marker From 2eaf431942695554a03a086179cf5e7b462c63f7 Mon Sep 17 00:00:00 2001 From: Llywelwyn Date: Sat, 15 Jun 2024 17:24:37 +0100 Subject: [PATCH 39/50] disable show_mapgen for default config --- src/config/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/mod.rs b/src/config/mod.rs index 5309151..f86c5df 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -34,7 +34,7 @@ impl Default for Config { fn default() -> Self { Config { logging: LogConfig { - show_mapgen: true, + show_mapgen: false, log_combat: false, log_spawning: false, log_ticks: false, From 9719ebbe889aecf2dec102d4acc11657eb769f41 Mon Sep 17 00:00:00 2001 From: Llywelwyn Date: Sat, 15 Jun 2024 17:35:40 +0100 Subject: [PATCH 40/50] docs tldr --- docs/combat_system.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/combat_system.txt b/docs/combat_system.txt index 13a780b..44e2f65 100644 --- a/docs/combat_system.txt +++ b/docs/combat_system.txt @@ -49,3 +49,7 @@ Complex example, with negative AC: bloodstains: if starts on bloodied tile, remove blood + heal, gain xp, grow (little dog -> dog), etc. - You have negative AC, so you roll 1d14 for damage reduction, and get an 8. - The total damage is 6 - 8 = -2, but damage can't be negative, so you take 1 point of damage. + +tl;dr +1. Lower AC is better +2. Aim for 0 AC - it's an important breakpoint. Every point of AC before 0 counts for a lot. From c5106a63b59fcc16bc6de146a5845cdcd3cf80fd Mon Sep 17 00:00:00 2001 From: Llywelwyn Date: Sat, 15 Jun 2024 20:14:38 +0100 Subject: [PATCH 41/50] static inventory keys - items remember their slots this is the biggest refactor of my entire life --- raws/items.json | 71 ++++- src/components.rs | 47 ++++ src/damage_system.rs | 15 +- src/data/messages.rs | 1 + src/effects/triggers.rs | 7 +- src/gui/identify_menu.rs | 124 ++++----- src/gui/mod.rs | 404 ++++++++++++++++------------- src/gui/remove_curse_menu.rs | 63 +++-- src/inventory/collection_system.rs | 35 ++- src/inventory/drop_system.rs | 6 + src/inventory/keyhandling.rs | 153 +++++++++++ src/inventory/mod.rs | 2 + src/invkeys.rs | 60 +++++ src/lib.rs | 1 + src/main.rs | 5 + src/player.rs | 3 + src/raws/item_structs.rs | 1 + src/raws/rawmaster.rs | 49 +++- src/saveload_system.rs | 10 + src/states/state.rs | 8 +- 20 files changed, 758 insertions(+), 307 deletions(-) create mode 100644 src/inventory/keyhandling.rs create mode 100644 src/invkeys.rs diff --git a/raws/items.json b/raws/items.json index 2c0c678..7599afe 100644 --- a/raws/items.json +++ b/raws/items.json @@ -3,9 +3,10 @@ "id": "potion_health", "name": { "name": "potion of health", "plural": "potions of health" }, "renderable": { "glyph": "!", "fg": "#FF00FF", "bg": "#000000", "order": 2 }, + "class": "potion", "weight": 1, "value": 50, - "flags": ["CONSUMABLE", "DESTRUCTIBLE"], + "flags": ["CONSUMABLE", "DESTRUCTIBLE", "STACKABLE"], "effects": { "heal": "4d4+2" }, "magic": { "class": "uncommon", "naming": "potion" } }, @@ -13,9 +14,10 @@ "id": "potion_health_weak", "name": { "name": "potion of lesser health", "plural": "potions of lesser health" }, "renderable": { "glyph": "!", "fg": "#FF00FF", "bg": "#000000", "order": 2 }, + "class": "potion", "weight": 1, "value": 25, - "flags": ["CONSUMABLE", "DESTRUCTIBLE"], + "flags": ["CONSUMABLE", "DESTRUCTIBLE", "STACKABLE"], "effects": { "heal": "2d4+2" }, "magic": { "class": "uncommon", "naming": "potion" } }, @@ -23,27 +25,30 @@ "id": "scroll_identify", "name": { "name": "scroll of identify", "plural": "scrolls of identify" }, "renderable": { "glyph": "?", "fg": "#0FFFFF", "bg": "#000000", "order": 2 }, + "class": "scroll", "weight": 0.5, "value": 100, - "flags": ["CONSUMABLE", "DESTRUCTIBLE", "IDENTIFY"], + "flags": ["CONSUMABLE", "DESTRUCTIBLE", "STACKABLE", "IDENTIFY"], "magic": { "class": "uncommon", "naming": "scroll" } }, { "id": "scroll_removecurse", "name": { "name": "scroll of remove curse", "plural": "scrolls of remove curse" }, "renderable": { "glyph": "?", "fg": "#0FFFFF", "bg": "#000000", "order": 2 }, + "class": "scroll", "weight": 0.5, "value": 200, - "flags": ["CONSUMABLE", "DESTRUCTIBLE", "REMOVE_CURSE"], + "flags": ["CONSUMABLE", "DESTRUCTIBLE", "STACKABLE", "REMOVE_CURSE"], "magic": { "class": "rare", "naming": "scroll" } }, { "id": "scroll_health", "name": { "name": "scroll of healing word", "plural": "scrolls of healing word" }, "renderable": { "glyph": "?", "fg": "#00FFFF", "bg": "#000000", "order": 2 }, + "class": "scroll", "weight": 0.5, "value": 50, - "flags": ["CONSUMABLE", "DESTRUCTIBLE"], + "flags": ["CONSUMABLE", "DESTRUCTIBLE", "STACKABLE"], "effects": { "particle_line": "*;-;#53f06d;75.0;#f9ff9f;100.0", "ranged": "12", "heal": "1d4+2" }, "magic": { "class": "uncommon", "naming": "scroll" } }, @@ -51,9 +56,10 @@ "id": "scroll_mass_health", "name": { "name": "scroll of mass healing word", "plural": "scrolls of mass healing word" }, "renderable": { "glyph": "?", "fg": "#00FFFF", "bg": "#000000", "order": 2 }, + "class": "scroll", "weight": 0.5, "value": 200, - "flags": ["CONSUMABLE", "DESTRUCTIBLE"], + "flags": ["CONSUMABLE", "DESTRUCTIBLE", "STACKABLE"], "effects": { "particle": "*;#53f06d;200.0", "ranged": "12", "aoe": "3", "heal": "1d4+2" }, "magic": { "class": "rare", "naming": "scroll" } }, @@ -61,9 +67,10 @@ "id": "scroll_magicmissile", "name": { "name": "scroll of magic missile", "plural": "scrolls of magic missile" }, "renderable": { "glyph": "?", "fg": "#00FFFF", "bg": "#000000", "order": 2 }, + "class": "scroll", "weight": 0.5, "value": 50, - "flags": ["CONSUMABLE", "DESTRUCTIBLE"], + "flags": ["CONSUMABLE", "DESTRUCTIBLE", "STACKABLE"], "effects": { "particle_line": "*;-;#00b7ff;75.0;#f4fc83;100.0", "ranged": "12", "damage": "3d4+3;magic" }, "magic": { "class": "uncommon", "naming": "scroll" } }, @@ -71,9 +78,10 @@ "id": "scroll_embers", "name": { "name": "scroll of embers", "plural": "scrolls of embers" }, "renderable": { "glyph": "?", "fg": "#00FFFF", "bg": "#000000", "order": 2 }, + "class": "scroll", "weight": 0.5, "value": 100, - "flags": ["CONSUMABLE", "DESTRUCTIBLE"], + "flags": ["CONSUMABLE", "DESTRUCTIBLE", "STACKABLE"], "effects": { "particle": "*;#FFA500;200.0", "ranged": "10", "damage": "4d6;fire", "aoe": "2" }, "magic": { "class": "uncommon", "naming": "scroll" } }, @@ -81,9 +89,10 @@ "id": "scroll_fireball", "name": { "name": "scroll of fireball", "plural": "scrolls of fireball" }, "renderable": { "glyph": "?", "fg": "#00FFFF", "bg": "#000000", "order": 2 }, + "class": "scroll", "weight": 0.5, "value": 200, - "flags": ["CONSUMABLE", "DESTRUCTIBLE"], + "flags": ["CONSUMABLE", "DESTRUCTIBLE", "STACKABLE"], "effects": { "particle_burst": "▓;*;~;#FFA500;#000000;500.0;#ffd381;60.0", "ranged": "10", @@ -96,9 +105,10 @@ "id": "scroll_confusion", "name": { "name": "scroll of confusion", "plural": "scrolls of confusion" }, "renderable": { "glyph": "?", "fg": "#00FFFF", "bg": "#000000", "order": 2 }, + "class": "scroll", "weight": 0.5, "value": 100, - "flags": ["CONSUMABLE", "DESTRUCTIBLE"], + "flags": ["CONSUMABLE", "DESTRUCTIBLE", "STACKABLE"], "effects": { "particle_line": "*;-;#ad56a6;75.0;#cacaca;100.0", "ranged": "10", "confusion": "4" }, "magic": { "class": "uncommon", "naming": "scroll" } }, @@ -106,9 +116,10 @@ "id": "scroll_mass_confusion", "name": { "name": "scroll of mass confusion", "plural": "scrolls of mass confusion" }, "renderable": { "glyph": "?", "fg": "#00FFFF", "bg": "#000000", "order": 2 }, + "class": "scroll", "weight": 0.5, "value": 200, - "flags": ["CONSUMABLE", "DESTRUCTIBLE"], + "flags": ["CONSUMABLE", "DESTRUCTIBLE", "STACKABLE"], "effects": { "particle": "*;#ad56a6;200.0", "ranged": "10", "aoe": "3", "confusion": "3" }, "magic": { "class": "veryrare", "naming": "scroll" } }, @@ -116,9 +127,10 @@ "id": "scroll_magicmap", "name": { "name": "scroll of magic mapping", "plural": "scrolls of magic mapping" }, "renderable": { "glyph": "?", "fg": "#00FFFF", "bg": "#000000", "order": 2 }, + "class": "scroll", "weight": 0.5, "value": 50, - "flags": ["CONSUMABLE", "DESTRUCTIBLE", "MAGICMAP"], + "flags": ["CONSUMABLE", "DESTRUCTIBLE", "STACKABLE", "MAGICMAP"], "effects": {}, "magic": { "class": "common", "naming": "scroll" } }, @@ -126,6 +138,7 @@ "id": "equip_dagger", "name": { "name": "dagger", "plural": "daggers" }, "renderable": { "glyph": ")", "fg": "#808080", "bg": "#000000", "order": 2 }, + "class": "weapon", "weight": 1, "value": 2, "flags": ["EQUIP_MELEE"], @@ -135,6 +148,7 @@ "id": "equip_shortsword", "name": { "name": "shortsword", "plural": "shortswords" }, "renderable": { "glyph": ")", "fg": "#C0C0C0", "bg": "#000000", "order": 2 }, + "class": "weapon", "weight": 2, "value": 10, "flags": ["EQUIP_MELEE"], @@ -144,6 +158,7 @@ "id": "equip_rapier", "name": { "name": "rapier", "plural": "rapiers" }, "renderable": { "glyph": ")", "fg": "#C0C0C0", "bg": "#000000", "order": 2 }, + "class": "weapon", "weight": 2, "value": 10, "flags": ["EQUIP_MELEE"], @@ -153,6 +168,7 @@ "id": "equip_pitchfork", "name": { "name": "pitchfork", "plural": "pitchforks" }, "renderable": { "glyph": ")", "fg": "#C0C0C0", "bg": "#000000", "order": 2 }, + "class": "weapon", "weight": 2, "value": 5, "flags": ["EQUIP_MELEE"], @@ -162,6 +178,7 @@ "id": "equip_sickle", "name": { "name": "sickle", "plural": "sickles" }, "renderable": { "glyph": ")", "fg": "#C0C0C0", "bg": "#000000", "order": 2 }, + "class": "weapon", "weight": 2, "value": 5, "flags": ["EQUIP_MELEE"], @@ -171,6 +188,7 @@ "id": "equip_handaxe", "name": { "name": "handaxe", "plural": "handaxes" }, "renderable": { "glyph": ")", "fg": "#C0C0C0", "bg": "#000000", "order": 2 }, + "class": "weapon", "weight": 2, "value": 5, "flags": ["EQUIP_MELEE"], @@ -180,6 +198,7 @@ "id": "equip_longsword", "name": { "name": "longsword", "plural": "longswords" }, "renderable": { "glyph": ")", "fg": "#FFF8DC", "bg": "#000000", "order": 2 }, + "class": "weapon", "weight": 3, "value": 15, "flags": ["EQUIP_MELEE"], @@ -189,6 +208,7 @@ "id": "equip_smallshield", "name": { "name": "buckler", "plural": "bucklers" }, "renderable": { "glyph": "[", "fg": "#808080", "bg": "#000000", "order": 2 }, + "class": "armour", "weight": 2, "value": 5, "flags": ["EQUIP_SHIELD"], @@ -198,6 +218,7 @@ "id": "equip_mediumshield", "name": { "name": "medium shield", "plural": "medium shields" }, "renderable": { "glyph": "[", "fg": "#C0C0C0", "bg": "#000000", "order": 2 }, + "class": "armour", "weight": 6, "value": 10, "flags": ["EQUIP_SHIELD"], @@ -207,6 +228,7 @@ "id": "equip_largeshield", "name": { "name": "large shield", "plural": "large shields" }, "renderable": { "glyph": "[", "fg": "#FFF8DC", "bg": "#000000", "order": 2 }, + "class": "armour", "weight": 12, "value": 35, "flags": ["EQUIP_SHIELD"], @@ -216,6 +238,7 @@ "id": "equip_body_weakleather", "name": { "name": "leather jacket", "plural": "leather jackets" }, "renderable": { "glyph": "[", "fg": "#aa6000", "bg": "#000000", "order": 2 }, + "class": "armour", "weight": 8, "value": 5, "flags": ["EQUIP_BODY"], @@ -225,6 +248,7 @@ "id": "equip_body_leather", "name": { "name": "leather chestpiece", "plural": "leather chestpiece" }, "renderable": { "glyph": "[", "fg": "#aa6000", "bg": "#000000", "order": 2 }, + "class": "armour", "weight": 10, "value": 10, "flags": ["EQUIP_BODY"], @@ -234,6 +258,7 @@ "id": "equip_body_studdedleather", "name": { "name": "studded leather chestpiece", "plural": "studded leather chestpieces" }, "renderable": { "glyph": "[", "fg": "#aa6000", "bg": "#000000", "order": 2 }, + "class": "armour", "weight": 13, "value": 45, "flags": ["EQUIP_BODY"], @@ -243,6 +268,7 @@ "id": "equip_body_ringmail_o", "name": { "name": "orcish ring mail", "plural": "orcish ring mail" }, "renderable": { "glyph": "[", "fg": "#aa6000", "bg": "#000000", "order": 2 }, + "class": "armour", "weight": 45, "value": 50, "flags": ["EQUIP_BODY"], @@ -252,6 +278,7 @@ "id": "equip_body_ringmail", "name": { "name": "ring mail", "plural": "ring mail" }, "renderable": { "glyph": "[", "fg": "#aa6000", "bg": "#000000", "order": 2 }, + "class": "armour", "weight": 45, "value": 70, "flags": ["EQUIP_BODY"], @@ -261,6 +288,7 @@ "id": "equip_head_leather", "name": { "name": "leather cap", "plural": "leather caps" }, "renderable": { "glyph": "[", "fg": "#aa6000", "bg": "#000000", "order": 2 }, + "class": "armour", "weight": 2, "value": 10, "flags": ["EQUIP_HEAD"], @@ -270,6 +298,7 @@ "id": "equip_head_elvish", "name": { "name": "elvish leather helm", "plural": "elvish leather helms" }, "renderable": { "glyph": "[", "fg": "#aa6000", "bg": "#000000", "order": 2 }, + "class": "armour", "weight": 2, "value": 25, "flags": ["EQUIP_HEAD"], @@ -279,6 +308,7 @@ "id": "equip_head_o", "name": { "name": "orcish helm", "plural": "orcish helm" }, "renderable": { "glyph": "[", "fg": "#aa6000", "bg": "#000000", "order": 2 }, + "class": "armour", "weight": 6, "value": 25, "flags": ["EQUIP_HEAD"], @@ -288,6 +318,7 @@ "id": "equip_head_iron", "name": { "name": "iron helm", "plural": "iron helm" }, "renderable": { "glyph": "[", "fg": "#aa6000", "bg": "#000000", "order": 2 }, + "class": "armour", "weight": 10, "value": 45, "flags": ["EQUIP_HEAD"], @@ -297,6 +328,7 @@ "id": "equip_feet_leather", "name": { "name": "leather shoes", "plural": "leather shoes" }, "renderable": { "glyph": "[", "fg": "#aa6000", "bg": "#000000", "order": 2 }, + "class": "armour", "weight": 2, "value": 10, "flags": ["EQUIP_FEET"] @@ -305,6 +337,7 @@ "id": "equip_feet_elvish", "name": { "name": "elvish leather shoes", "plural": "elvish leather shoes" }, "renderable": { "glyph": "[", "fg": "#aa6000", "bg": "#000000", "order": 2 }, + "class": "armour", "weight": 2, "value": 25, "flags": ["EQUIP_FEET"], @@ -314,6 +347,7 @@ "id": "equip_feet_o", "name": { "name": "orcish boots", "plural": "orcish boots" }, "renderable": { "glyph": "[", "fg": "#aa6000", "bg": "#000000", "order": 2 }, + "class": "armour", "weight": 6, "value": 25, "flags": ["EQUIP_FEET"], @@ -323,6 +357,7 @@ "id": "equip_feet_iron", "name": { "name": "iron boots", "plural": "iron boots" }, "renderable": { "glyph": "[", "fg": "#aa6000", "bg": "#000000", "order": 2 }, + "class": "armour", "weight": 10, "value": 45, "flags": ["EQUIP_FEET"], @@ -332,6 +367,7 @@ "id": "equip_neck_protection", "name": { "name": "amulet of protection", "plural": "amulets of protection" }, "renderable": { "glyph": "\"", "fg": "#aa6000", "bg": "#000000", "order": 2 }, + "class": "amulet", "weight": 1, "value": 200, "flags": ["EQUIP_NECK"], @@ -341,6 +377,7 @@ "id": "equip_back_protection", "name": { "name": "cloak of protection", "plural": "cloaks of protection" }, "renderable": { "glyph": "[", "fg": "#aa6000", "bg": "#000000", "order": 2 }, + "class": "armour", "weight": 1, "value": 200, "flags": ["EQUIP_BACK"], @@ -350,6 +387,7 @@ "id": "wand_magicmissile", "name": { "name": "wand of magic missile", "plural": "wands of magic missile" }, "renderable": { "glyph": "/", "fg": "#00FFFF", "bg": "#000000", "order": 2 }, + "class": "wand", "weight": 2, "value": 100, "flags": ["CHARGES"], @@ -360,6 +398,7 @@ "id": "wand_fireball", "name": { "name": "wand of fireball", "plural": "wands of fireball" }, "renderable": { "glyph": "/", "fg": "#00FFFF", "bg": "#000000", "order": 2 }, + "class": "wand", "weight": 2, "value": 300, "flags": ["CHARGES"], @@ -370,6 +409,7 @@ "id": "wand_confusion", "name": { "name": "wand of confusion", "plural": "wands of confusion" }, "renderable": { "glyph": "/", "fg": "#00FFFF", "bg": "#000000", "order": 2 }, + "class": "wand", "weight": 2, "value": 200, "flags": ["CHARGES"], @@ -380,6 +420,7 @@ "id": "wand_digging", "name": { "name": "wand of digging", "plural": "wands of digging" }, "renderable": { "glyph": "/", "fg": "#00FFFF", "bg": "#000000", "order": 2 }, + "class": "wand", "weight": 2, "value": 300, "flags": ["CHARGES", "DIGGER"], @@ -390,16 +431,18 @@ "id": "food_rations", "name": { "name": "rations", "plural": "rations" }, "renderable": { "glyph": "%", "fg": "#FFA07A", "bg": "#000000", "order": 2 }, + "class": "comestible", "weight": 1, "value": 1, - "flags": ["FOOD", "CONSUMABLE"] + "flags": ["FOOD", "CONSUMABLE", "STACKABLE"] }, { "id": "food_apple", "name": { "name": "apple", "plural": "apples" }, "renderable": { "glyph": "%", "fg": "#00FF00", "bg": "#000000", "order": 2 }, + "class": "comestible", "weight": 0.5, "value": 1, - "flags": ["FOOD", "CONSUMABLE"] + "flags": ["FOOD", "CONSUMABLE", "STACKABLE"] } ] diff --git a/src/components.rs b/src/components.rs index 696eab4..45ca1cf 100644 --- a/src/components.rs +++ b/src/components.rs @@ -258,10 +258,40 @@ pub struct Beatitude { pub known: bool, } +#[derive(Debug, Serialize, Deserialize, Copy, Clone, PartialEq, Eq)] +pub enum ItemType { + Amulet, + Weapon, + Armour, + Comestible, + Scroll, + Spellbook, + Potion, + Ring, + Wand, +} + +impl ItemType { + pub fn string(&self) -> &str { + match self { + ItemType::Amulet => "Amulets", + ItemType::Weapon => "Weapons", + ItemType::Armour => "Armour", + ItemType::Comestible => "Comestibles", + ItemType::Scroll => "Scrolls", + ItemType::Spellbook => "Spellbooks", + ItemType::Potion => "Potions", + ItemType::Ring => "Rings", + ItemType::Wand => "Wands", + } + } +} + #[derive(Component, Debug, Serialize, Deserialize, Clone)] pub struct Item { pub weight: f32, // in lbs pub value: f32, // base + pub category: ItemType, } #[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq, Hash)] @@ -618,3 +648,20 @@ pub struct EntityMoved {} #[derive(Component, Debug, Serialize, Deserialize, Clone)] pub struct MultiAttack {} + +#[derive(Component, Debug, Serialize, Deserialize, Clone)] +pub struct Stackable {} + +#[derive(Component, Debug, Serialize, Deserialize, Clone)] +pub struct WantsToRemoveKey {} + +#[derive(Component, Debug, Serialize, Deserialize, Clone)] +pub struct WantsToDelete {} + +#[derive(Component, Debug, Serialize, Deserialize, Clone)] +pub struct Key { + pub idx: usize, +} + +#[derive(Component, Debug, Serialize, Deserialize, Clone)] +pub struct WantsToAssignKey {} diff --git a/src/damage_system.rs b/src/damage_system.rs index a4224a7..b0cc566 100644 --- a/src/damage_system.rs +++ b/src/damage_system.rs @@ -11,6 +11,8 @@ use super::{ Position, Renderable, RunState, + WantsToRemoveKey, + WantsToDelete, }; use bracket_lib::prelude::*; use specs::prelude::*; @@ -65,7 +67,17 @@ pub fn delete_the_dead(ecs: &mut World) { } } } - let (items_to_delete, loot_to_spawn) = handle_dead_entity_items(ecs, &dead); + let (mut items_to_delete, loot_to_spawn) = handle_dead_entity_items(ecs, &dead); + { + let entities = ecs.entities(); + let removekeys = ecs.read_storage::(); + let delete = ecs.read_storage::(); + // Add items marked for deletion to the list, but only if they've already had their + // key assignments handled, to ensurew we don't leave any dangling references behind. + for (e, _d, _r) in (&entities, &delete, !&removekeys).join() { + items_to_delete.push(e); + } + } for loot in loot_to_spawn { crate::raws::spawn_named_entity( &crate::raws::RAWS.lock().unwrap(), @@ -82,6 +94,7 @@ pub fn delete_the_dead(ecs: &mut World) { // For everything that died, increment the event log, and delete. for victim in dead { gamelog::record_event(events::EVENT::Turn(1)); + // TODO: Delete stuff from inventory? This should be handled elsewhere. ecs.delete_entity(victim).expect("Unable to delete."); } } diff --git a/src/data/messages.rs b/src/data/messages.rs index 89e39c8..7175b2a 100644 --- a/src/data/messages.rs +++ b/src/data/messages.rs @@ -25,6 +25,7 @@ pub const NUTRITION_BLESSED: &str = "Delicious"; pub const LEVELUP_PLAYER: &str = "Welcome to experience level"; pub const YOU_PICKUP_ITEM: &str = "You pick up the"; +pub const NO_MORE_KEYS: &str = "Your backpack cannot accomodate any more items"; pub const YOU_DROP_ITEM: &str = "You drop the"; pub const YOU_EQUIP_ITEM: &str = "You equip the"; pub const YOU_REMOVE_ITEM: &str = "You unequip your"; diff --git a/src/effects/triggers.rs b/src/effects/triggers.rs index a82496c..fb54eed 100644 --- a/src/effects/triggers.rs +++ b/src/effects/triggers.rs @@ -33,6 +33,8 @@ use crate::{ KnownSpells, Position, Viewshed, + WantsToRemoveKey, + WantsToDelete, }; use crate::data::messages::*; use bracket_lib::prelude::*; @@ -57,7 +59,10 @@ pub fn item_trigger(source: Option, item: Entity, target: &Targets, ecs: let did_something = event_trigger(source, item, target, ecs); // If it's a consumable, delete it if did_something && ecs.read_storage::().get(item).is_some() { - ecs.entities().delete(item).expect("Failed to delete item"); + let mut removekey = ecs.write_storage::(); + removekey.insert(item, WantsToRemoveKey {}).expect("Unable to insert WantsToRemoveKey"); + let mut delete = ecs.write_storage::(); + delete.insert(item, WantsToDelete {}).expect("Unable to insert WantsToDelete"); } } diff --git a/src/gui/identify_menu.rs b/src/gui/identify_menu.rs index 14e0686..422b2a5 100644 --- a/src/gui/identify_menu.rs +++ b/src/gui/identify_menu.rs @@ -3,7 +3,10 @@ use super::{ item_colour_ecs, obfuscate_name_ecs, print_options, + unique_ecs, + check_key, renderable_colour, + letter_to_option, ItemMenuResult, UniqueInventoryItem, BUC, @@ -19,11 +22,12 @@ use crate::{ Name, ObfuscatedName, Renderable, + Key, states::state::*, }; use bracket_lib::prelude::*; use specs::prelude::*; -use std::collections::BTreeMap; +use std::collections::HashMap; /// Handles the Identify menu. pub fn identify(gs: &mut State, ctx: &mut BTerm) -> (ItemMenuResult, Option) { @@ -37,38 +41,41 @@ pub fn identify(gs: &mut State, ctx: &mut BTerm) -> (ItemMenuResult, Option(); let renderables = gs.ecs.read_storage::(); let beatitudes = gs.ecs.read_storage::(); + let keys = gs.ecs.read_storage::(); let build_identify_iterator = || { - (&entities, &items, &renderables, &names).join().filter(|(item_entity, _i, _r, n)| { - // If not owned by the player, return false. - let mut keep = false; - if let Some(bp) = backpack.get(*item_entity) { - if bp.owner == *player_entity { - keep = true; + (&entities, &items, &renderables, &names, &keys) + .join() + .filter(|(item_entity, _i, _r, n, _k)| { + // If not owned by the player, return false. + let mut keep = false; + if let Some(bp) = backpack.get(*item_entity) { + if bp.owner == *player_entity { + keep = true; + } } - } - // If not equipped by the player, return false. - if let Some(equip) = equipped.get(*item_entity) { - if equip.owner == *player_entity { - keep = true; + // If not equipped by the player, return false. + if let Some(equip) = equipped.get(*item_entity) { + if equip.owner == *player_entity { + keep = true; + } } - } - if !keep { - return false; - } - // If not obfuscated, or already identified, return false. - if - (!obfuscated.get(*item_entity).is_some() || - dm.identified_items.contains(&n.name)) && - beatitudes - .get(*item_entity) - .map(|beatitude| beatitude.known) - .unwrap_or(true) - { - return false; - } - return true; - }) + if !keep { + return false; + } + // If not obfuscated, or already identified, return false. + if + (!obfuscated.get(*item_entity).is_some() || + dm.identified_items.contains(&n.name)) && + beatitudes + .get(*item_entity) + .map(|beatitude| beatitude.known) + .unwrap_or(true) + { + return false; + } + return true; + }) }; // Build list of items to display @@ -91,34 +98,15 @@ pub fn identify(gs: &mut State, ctx: &mut BTerm) -> (ItemMenuResult, Option().get(entity) - { - match beatitude.buc { - BUC::Blessed => 1, - BUC::Uncursed => 2, - BUC::Cursed => 3, - } - } else { - 0 - }; - let unique_item = UniqueInventoryItem { - display_name: super::DisplayName { singular: singular.clone(), plural: plural.clone() }, - rgb: item_colour_ecs(&gs.ecs, entity), - renderables: renderable_colour(&renderables, entity), - glyph: renderable.glyph, - beatitude_status: beatitude_status, - name: name.name.clone(), - }; + let mut player_inventory: super::PlayerInventory = HashMap::new(); + for (entity, _i, renderable, name, key) in build_identify_iterator() { + let unique_item = unique_ecs(&gs.ecs, entity); player_inventory .entry(unique_item) - .and_modify(|(_e, count)| { - *count += 1; + .and_modify(|slot| { + slot.count += 1; }) - .or_insert((entity, 1)); + .or_insert(super::InventorySlot { item: entity, count: 1, idx: key.idx }); } // Get display args let width = get_max_inventory_width(&player_inventory); @@ -133,7 +121,7 @@ pub fn identify(gs: &mut State, ctx: &mut BTerm) -> (ItemMenuResult, Option (ItemMenuResult::NoResponse, None), @@ -141,21 +129,17 @@ pub fn identify(gs: &mut State, ctx: &mut BTerm) -> (ItemMenuResult, Option (ItemMenuResult::Cancel, None), _ => { - let selection = letter_to_option(key); - if selection > -1 && selection < (count as i32) { - let item = player_inventory - .iter() - .nth(selection as usize) - .unwrap().1.0; - gamelog::Logger - ::new() - .append("You identify the") - .colour(item_colour_ecs(&gs.ecs, item)) - .append_n(obfuscate_name_ecs(&gs.ecs, item).0) - .colour(WHITE) - .append("!") - .log(); - return (ItemMenuResult::Selected, Some(item)); + let selection = letter_to_option::letter_to_option(key, ctx.shift); + if selection != -1 && check_key(selection as usize) { + // Get the first entity with a Key {} component that has an idx matching "selection". + let entities = gs.ecs.entities(); + let keyed_items = gs.ecs.read_storage::(); + let backpack = gs.ecs.read_storage::(); + for (e, key, _b) in (&entities, &keyed_items, &backpack).join() { + if key.idx == (selection as usize) { + return (ItemMenuResult::Selected, Some(e)); + } + } } (ItemMenuResult::NoResponse, None) } diff --git a/src/gui/mod.rs b/src/gui/mod.rs index 13e32d0..73c70f5 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -32,6 +32,9 @@ use super::{ Skills, Viewshed, BUC, + Key, + Item, + ItemType, data::ids::get_local_col, }; use crate::data::entity::CARRY_CAPACITY_PER_STRENGTH; @@ -43,7 +46,9 @@ use crate::data::visuals::{ }; use bracket_lib::prelude::*; use specs::prelude::*; -use std::collections::BTreeMap; +use std::collections::{ HashMap, HashSet }; +use crate::invkeys::check_key; + mod character_creation; mod cheat_menu; mod letter_to_option; @@ -271,41 +276,16 @@ pub fn draw_ui(ecs: &World, ctx: &mut BTerm) { ctx.print_color(20, 20, RGB::named(YELLOW), RGB::named(BLACK), "--- GODMODE: ON ---"); } // Draw equipment - let renderables = ecs.read_storage::(); - let mut equipment: Vec<(String, RGB, RGB, FontCharType)> = Vec::new(); - let entities = ecs.entities(); - for (entity, _equipped, renderable) in (&entities, &equipped, &renderables) - .join() - .filter(|item| item.1.owner == *player_entity) { - equipment.push(( - obfuscate_name_ecs(ecs, entity).0, - RGB::named(item_colour_ecs(ecs, entity)), - renderable.fg, - renderable.glyph, - )); - } let mut y = 1; + let equipment = items(&ecs, Filter::Equipped); if !equipment.is_empty() { ctx.print_color(72, y, RGB::named(BLACK), RGB::named(WHITE), "Equipment"); - let mut j = 0; - for item in equipment { - y += 1; - ctx.set(72, y, RGB::named(YELLOW), RGB::named(BLACK), 97 + (j as FontCharType)); - j += 1; - ctx.set(74, y, item.2, RGB::named(BLACK), item.3); - ctx.print_color(76, y, item.1, RGB::named(BLACK), &item.0); - ctx.print_color( - 76 + &item.0.len() + 1, - y, - RGB::named(WHITE), - RGB::named(BLACK), - "(worn)" - ); - } - y += 2; + y += 1; + y = print_options(&ecs, &equipment, 72, y, ctx); + y += 1; } - // Draw consumables + // Draw backpack ctx.print_color(72, y, RGB::named(BLACK), RGB::named(WHITE), "Backpack"); ctx.print_color( 81, @@ -320,8 +300,8 @@ pub fn draw_ui(ecs: &World, ctx: &mut BTerm) { ) ); y += 1; - let player_inventory = get_player_inventory(&ecs); - y = print_options(&player_inventory, 72, y, ctx).0; + let backpack = items(&ecs, Filter::Backpack); + y = print_options(&ecs, &backpack, 72, y, ctx); // Draw spells - if we have any -- NYI! if let Some(known_spells) = ecs.read_storage::().get(*player_entity) { @@ -505,46 +485,46 @@ pub enum ItemMenuResult { } pub fn print_options( + ecs: &World, inventory: &PlayerInventory, mut x: i32, mut y: i32, ctx: &mut BTerm -) -> (i32, i32) { - let mut j = 0; +) -> i32 { let initial_x: i32 = x; - let mut width: i32 = -1; - for (item, (_e, item_count)) in inventory { + let mut sorted: Vec<_> = inventory.iter().collect(); + sorted.sort_by(|a, b| a.1.idx.cmp(&b.1.idx)); + + for (info, slot) in sorted { x = initial_x; // Print the character required to access this item. i.e. (a) - if j < 26 { - ctx.set(x, y, RGB::named(YELLOW), RGB::named(BLACK), 97 + (j as FontCharType)); + if slot.idx < 26 { + ctx.set(x, y, RGB::named(YELLOW), RGB::named(BLACK), 97 + slot.idx); } else { // If we somehow have more than 26, start using capitals - ctx.set(x, y, RGB::named(YELLOW), RGB::named(BLACK), 65 - 26 + (j as FontCharType)); + ctx.set(x, y, RGB::named(YELLOW), RGB::named(BLACK), 65 - 26 + slot.idx); } x += 2; - let fg = RGB::from_u8(item.renderables.0, item.renderables.1, item.renderables.2); - ctx.set(x, y, fg, RGB::named(BLACK), item.glyph); + let fg = RGB::from_u8(info.renderables.0, info.renderables.1, info.renderables.2); + ctx.set(x, y, fg, RGB::named(BLACK), info.glyph); x += 2; - let fg = RGB::from_u8(item.rgb.0, item.rgb.1, item.rgb.2); - if item_count > &1 { + let fg = RGB::from_u8(info.rgb.0, info.rgb.1, info.rgb.2); + if slot.count > 1 { // If more than one, print the number and pluralise // i.e. (a) 3 daggers - ctx.print_color(x, y, fg, RGB::named(BLACK), item_count); + ctx.print_color(x, y, fg, RGB::named(BLACK), slot.count); x += 2; - ctx.print_color(x, y, fg, RGB::named(BLACK), item.display_name.plural.to_string()); - let this_width = x - initial_x + (item.display_name.plural.len() as i32); - width = if width > this_width { width } else { this_width }; + ctx.print_color(x, y, fg, RGB::named(BLACK), info.display_name.plural.to_string()); } else { - if item.display_name.singular.to_lowercase().ends_with("s") { + if info.display_name.singular.to_lowercase().ends_with("s") { ctx.print_color(x, y, fg, RGB::named(BLACK), "some"); x += 5; } else if ['a', 'e', 'i', 'o', 'u'] .iter() - .any(|&v| item.display_name.singular.to_lowercase().starts_with(v)) + .any(|&v| info.display_name.singular.to_lowercase().starts_with(v)) { // If one and starts with a vowel, print 'an' // i.e. (a) an apple @@ -556,40 +536,54 @@ pub fn print_options( ctx.print_color(x, y, fg, RGB::named(BLACK), "a"); x += 2; } - ctx.print_color(x, y, fg, RGB::named(BLACK), item.display_name.singular.to_string()); - let this_width = x - initial_x + (item.display_name.singular.len() as i32); - width = if width > this_width { width } else { this_width }; + /* + let text = if let Some(worn) = ecs.read_storage::().get(slot.item) { + use crate::EquipmentSlot; + let text = match worn.slot { + EquipmentSlot::Melee | EquipmentSlot::Shield => "being held", + _ => "being worn", + }; + format!("{} ({})", info.display_name.singular.to_string(), text) + } else { + info.display_name.singular.to_string() + }; + */ + let text = info.display_name.singular.to_string(); + ctx.print_color(x, y, fg, RGB::named(BLACK), text); } y += 1; - j += 1; } - return (y, width); + return y; } +const PADDING: i32 = 4; +const SOME: i32 = 4; +const AN: i32 = 2; +const A: i32 = 1; + pub fn get_max_inventory_width(inventory: &PlayerInventory) -> i32 { let mut width: i32 = 0; - for (item, (_e, count)) in inventory { + for (item, slot) in inventory { let mut this_width = item.display_name.singular.len() as i32; - // Clean this up. It should use consts. - this_width += 4; // The spaces before and after the character to select this item, etc. - if count <= &1 { + if slot.count <= 1 { if item.display_name.singular == item.display_name.plural { - this_width += 4; // "some".len + this_width += SOME; } else if ['a', 'e', 'i', 'o', 'u'].iter().any(|&v| item.display_name.singular.starts_with(v)) { - this_width += 2; // "an".len + this_width += AN; } else { - this_width += 1; // "a".len + this_width += A; } } else { - this_width += count.to_string().len() as i32; // i.e. "12".len + this_width = + (item.display_name.plural.len() as i32) + (slot.count.to_string().len() as i32); // i.e. "12".len } width = if width > this_width { width } else { this_width }; } - return width; + return width + PADDING; } // Inside the ECS @@ -636,7 +630,7 @@ pub fn obfuscate_name( if has_beatitude.known { let prefix = match has_beatitude.buc { BUC::Cursed => Some("cursed "), - BUC::Uncursed => None, + BUC::Uncursed => Some("uncursed "), BUC::Blessed => Some("blessed "), }; if prefix.is_some() { @@ -831,13 +825,13 @@ pub fn show_help(ctx: &mut BTerm) -> YesNoResult { } } -#[derive(PartialEq, Eq, PartialOrd, Ord)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] struct DisplayName { singular: String, plural: String, } -#[derive(PartialEq, Eq, PartialOrd, Ord)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct UniqueInventoryItem { display_name: DisplayName, rgb: (u8, u8, u8), @@ -847,57 +841,71 @@ pub struct UniqueInventoryItem { name: String, } -pub type PlayerInventory = BTreeMap; +pub struct InventorySlot { + pub item: Entity, + pub count: i32, + pub idx: usize, +} -pub fn get_player_inventory(ecs: &World) -> PlayerInventory { - let player_entity = ecs.fetch::(); - let names = ecs.read_storage::(); - let backpack = ecs.read_storage::(); - let entities = ecs.entities(); - let renderables = ecs.read_storage::(); +pub type PlayerInventory = HashMap; - let mut player_inventory: BTreeMap = BTreeMap::new(); - for (entity, _pack, name, renderable) in (&entities, &backpack, &names, &renderables) - .join() - .filter(|item| item.1.owner == *player_entity) { - // RGB can't be used as a key. This is converting the RGB (tuple of f32) into a tuple of u8s. - let item_colour = item_colour_ecs(ecs, entity); - let renderables = ( - (renderable.fg.r * 255.0) as u8, - (renderable.fg.g * 255.0) as u8, - (renderable.fg.b * 255.0) as u8, - ); - let (singular, plural) = obfuscate_name_ecs(ecs, entity); - let beatitude_status = if let Some(beatitude) = ecs.read_storage::().get(entity) { - match beatitude.buc { - BUC::Blessed => 1, - BUC::Uncursed => 2, - BUC::Cursed => 3, - } - } else { - 0 - }; - let unique_item = UniqueInventoryItem { - display_name: DisplayName { singular: singular.clone(), plural: plural }, - rgb: item_colour, - renderables: renderables, - glyph: renderable.glyph, - beatitude_status: beatitude_status, - name: name.name.clone(), - }; - player_inventory - .entry(unique_item) - .and_modify(|(_e, count)| { - *count += 1; +pub enum Filter { + All, + Backpack, + Equipped, + Category(ItemType), +} + +macro_rules! includeitem { + ($inv:expr, $ecs:expr, $e:expr, $k:expr) => { + $inv.entry(unique_ecs($ecs, $e)) + .and_modify(|slot| { + slot.count += 1; }) - .or_insert((entity, 1)); - } + .or_insert(InventorySlot { + item: $e, + count: 1, + idx: $k.idx, + }); + }; +} - return player_inventory; +pub fn items(ecs: &World, filter: Filter) -> PlayerInventory { + let entities = ecs.entities(); + let keys = ecs.read_storage::(); + let mut inv: PlayerInventory = HashMap::new(); + match filter { + Filter::All => { + for (e, k) in (&entities, &keys).join() { + includeitem!(inv, ecs, e, k); + } + } + Filter::Backpack => { + let backpack = ecs.read_storage::(); + for (e, k, _b) in (&entities, &keys, &backpack).join() { + includeitem!(inv, ecs, e, k); + } + } + Filter::Equipped => { + let equipped = ecs.read_storage::(); + for (e, k, _e) in (&entities, &keys, &equipped).join() { + includeitem!(inv, ecs, e, k); + } + } + Filter::Category(itemtype) => { + let items = ecs.read_storage::(); + for (e, k, _i) in (&entities, &keys, &items) + .join() + .filter(|e| e.2.category == itemtype) { + includeitem!(inv, ecs, e, k); + } + } + } + inv } pub fn show_inventory(gs: &mut State, ctx: &mut BTerm) -> (ItemMenuResult, Option) { - let player_inventory = get_player_inventory(&gs.ecs); + let player_inventory = items(&gs.ecs, Filter::Backpack); let count = player_inventory.len(); let (x_offset, y_offset) = (1, 10); @@ -915,7 +923,7 @@ pub fn show_inventory(gs: &mut State, ctx: &mut BTerm) -> (ItemMenuResult, Optio let y = 3 + y_offset; let width = get_max_inventory_width(&player_inventory); ctx.draw_box(x, y, width + 2, (count + 1) as i32, RGB::named(WHITE), RGB::named(BLACK)); - print_options(&player_inventory, x + 1, y + 1, ctx); + print_options(&gs.ecs, &player_inventory, x + 1, y + 1, ctx); match ctx.key { None => (ItemMenuResult::NoResponse, None), @@ -924,22 +932,23 @@ pub fn show_inventory(gs: &mut State, ctx: &mut BTerm) -> (ItemMenuResult, Optio VirtualKeyCode::Escape => (ItemMenuResult::Cancel, None), _ => { let selection = letter_to_option::letter_to_option(key, ctx.shift); - if selection > -1 && selection < (count as i32) { + if selection != -1 && check_key(selection as usize) { if on_overmap { gamelog::Logger ::new() .append("You can't use items on the overmap.") .log(); } else { - return ( - ItemMenuResult::Selected, - Some( - player_inventory - .iter() - .nth(selection as usize) - .unwrap().1.0 - ), - ); + // Get the first entity with a Key {} component that has idx matching selection + let entities = gs.ecs.entities(); + let keyed_items = gs.ecs.read_storage::(); + let backpack = gs.ecs.read_storage::(); + for (e, key, _b) in (&entities, &keyed_items, &backpack).join() { + if key.idx == (selection as usize) { + return (ItemMenuResult::Selected, Some(e)); + } + } + // TODO: Gamelog about not having selected item? } } (ItemMenuResult::NoResponse, None) @@ -949,7 +958,7 @@ pub fn show_inventory(gs: &mut State, ctx: &mut BTerm) -> (ItemMenuResult, Optio } pub fn drop_item_menu(gs: &mut State, ctx: &mut BTerm) -> (ItemMenuResult, Option) { - let player_inventory = get_player_inventory(&gs.ecs); + let player_inventory = items(&gs.ecs, Filter::Backpack); let count = player_inventory.len(); let (x_offset, y_offset) = (1, 10); @@ -967,7 +976,7 @@ pub fn drop_item_menu(gs: &mut State, ctx: &mut BTerm) -> (ItemMenuResult, Optio let y = 3 + y_offset; let width = get_max_inventory_width(&player_inventory); ctx.draw_box(x, y, width + 2, (count + 1) as i32, RGB::named(WHITE), RGB::named(BLACK)); - print_options(&player_inventory, x + 1, y + 1, ctx); + print_options(&gs.ecs, &player_inventory, x + 1, y + 1, ctx); match ctx.key { None => (ItemMenuResult::NoResponse, None), @@ -975,23 +984,23 @@ pub fn drop_item_menu(gs: &mut State, ctx: &mut BTerm) -> (ItemMenuResult, Optio match key { VirtualKeyCode::Escape => (ItemMenuResult::Cancel, None), _ => { - let selection = letter_to_option(key); - if selection > -1 && selection < (count as i32) { + let selection = letter_to_option::letter_to_option(key, ctx.shift); + if selection != -1 && check_key(selection as usize) { if on_overmap { gamelog::Logger ::new() .append("You can't drop items on the overmap.") .log(); } else { - return ( - ItemMenuResult::Selected, - Some( - player_inventory - .iter() - .nth(selection as usize) - .unwrap().1.0 - ), - ); + // Get the first entity with a Key {} component that has an idx matching "selection". + let entities = gs.ecs.entities(); + let keyed_items = gs.ecs.read_storage::(); + let backpack = gs.ecs.read_storage::(); + for (e, key, _b) in (&entities, &keyed_items, &backpack).join() { + if key.idx == (selection as usize) { + return (ItemMenuResult::Selected, Some(e)); + } + } } } (ItemMenuResult::NoResponse, None) @@ -1001,11 +1010,8 @@ pub fn drop_item_menu(gs: &mut State, ctx: &mut BTerm) -> (ItemMenuResult, Optio } pub fn remove_item_menu(gs: &mut State, ctx: &mut BTerm) -> (ItemMenuResult, Option) { - let player_entity = gs.ecs.fetch::(); - let backpack = gs.ecs.read_storage::(); - let entities = gs.ecs.entities(); - let inventory = (&backpack).join().filter(|item| item.owner == *player_entity); - let count = inventory.count(); + let player_inventory = items(&gs.ecs, Filter::Equipped); + let count = player_inventory.len(); let (x_offset, y_offset) = (1, 10); @@ -1017,38 +1023,11 @@ pub fn remove_item_menu(gs: &mut State, ctx: &mut BTerm) -> (ItemMenuResult, Opt "Unequip what? [aA-zZ][Esc.]" ); - let mut equippable: Vec<(Entity, String)> = Vec::new(); - let mut width = 2; - for (entity, _pack) in (&entities, &backpack) - .join() - .filter(|item| item.1.owner == *player_entity) { - let this_name = &obfuscate_name_ecs(&gs.ecs, entity).0; - let this_width = 5 + this_name.len(); - width = if width > this_width { width } else { this_width }; - equippable.push((entity, this_name.to_string())); - } - let x = 1 + x_offset; - let mut y = 3 + y_offset; - - ctx.draw_box(x, y, width, (count + 1) as i32, RGB::named(WHITE), RGB::named(BLACK)); - y += 1; - - let mut j = 0; - let renderables = gs.ecs.read_storage::(); - for (e, name) in &equippable { - let (mut fg, glyph) = if let Some(renderable) = renderables.get(*e) { - (renderable.fg, renderable.glyph) - } else { - (RGB::named(WHITE), to_cp437('-')) - }; - ctx.set(x + 1, y, RGB::named(YELLOW), RGB::named(BLACK), 97 + (j as FontCharType)); - ctx.set(x + 3, y, fg, RGB::named(BLACK), glyph); - fg = RGB::named(item_colour_ecs(&gs.ecs, *e)); - ctx.print_color(x + 5, y, fg, RGB::named(BLACK), name); - y += 1; - j += 1; - } + let y = 3 + y_offset; + let width = get_max_inventory_width(&player_inventory); + ctx.draw_box(x, y, width + 2, (count + 1) as i32, RGB::named(WHITE), RGB::named(BLACK)); + print_options(&gs.ecs, &player_inventory, x + 1, y + 1, ctx); match ctx.key { None => (ItemMenuResult::NoResponse, None), @@ -1056,9 +1035,17 @@ pub fn remove_item_menu(gs: &mut State, ctx: &mut BTerm) -> (ItemMenuResult, Opt match key { VirtualKeyCode::Escape => (ItemMenuResult::Cancel, None), _ => { - let selection = letter_to_option(key); - if selection > -1 && selection < (count as i32) { - return (ItemMenuResult::Selected, Some(equippable[selection as usize].0)); + let selection = letter_to_option::letter_to_option(key, ctx.shift); + if selection != -1 && check_key(selection as usize) { + // Get the first entity with a Key {} component that has an idx matching "selection". + let entities = gs.ecs.entities(); + let keyed_items = gs.ecs.read_storage::(); + let equipped = gs.ecs.read_storage::(); + for (e, key, _e) in (&entities, &keyed_items, &equipped).join() { + if key.idx == (selection as usize) { + return (ItemMenuResult::Selected, Some(e)); + } + } } (ItemMenuResult::NoResponse, None) } @@ -1458,3 +1445,72 @@ pub fn with_article(name: String) -> String { } format!("a {}", name) } + +pub fn unique( + entity: Entity, + names: &ReadStorage, + obfuscated_names: &ReadStorage, + renderables: &ReadStorage, + beatitudes: &ReadStorage, + magic_items: &ReadStorage, + charges: Option<&ReadStorage>, + dm: &MasterDungeonMap +) -> UniqueInventoryItem { + let item_colour = item_colour(entity, beatitudes); + let (singular, plural) = obfuscate_name( + entity, + names, + magic_items, + obfuscated_names, + beatitudes, + dm, + charges + ); + let (renderables, glyph) = if let Some(renderable) = renderables.get(entity) { + ( + ( + (renderable.fg.r * 255.0) as u8, + (renderable.fg.g * 255.0) as u8, + (renderable.fg.b * 255.0) as u8, + ), + renderable.glyph, + ) + } else { + unreachable!("Item has no renderable component.") + }; + let name = if let Some(name) = names.get(entity) { + name + } else { + unreachable!("Item has no name component.") + }; + let beatitude_status = if let Some(beatitude) = beatitudes.get(entity) { + match beatitude.buc { + BUC::Blessed => 1, + BUC::Uncursed => 2, + BUC::Cursed => 3, + } + } else { + 0 + }; + UniqueInventoryItem { + display_name: DisplayName { singular: singular.clone(), plural }, + rgb: item_colour, + renderables, + glyph, + beatitude_status, + name: name.name.clone(), + } +} + +pub fn unique_ecs(ecs: &World, entity: Entity) -> UniqueInventoryItem { + return unique( + entity, + &ecs.read_storage::(), + &ecs.read_storage::(), + &ecs.read_storage::(), + &ecs.read_storage::(), + &ecs.read_storage::(), + Some(&ecs.read_storage::()), + &ecs.fetch::() + ); +} diff --git a/src/gui/remove_curse_menu.rs b/src/gui/remove_curse_menu.rs index f8d1f14..247fd41 100644 --- a/src/gui/remove_curse_menu.rs +++ b/src/gui/remove_curse_menu.rs @@ -3,9 +3,13 @@ use super::{ item_colour_ecs, obfuscate_name_ecs, print_options, + unique_ecs, renderable_colour, + check_key, + letter_to_option, ItemMenuResult, UniqueInventoryItem, + InventorySlot, }; use crate::{ gamelog, @@ -18,10 +22,11 @@ use crate::{ Renderable, states::state::*, BUC, + Key, }; use bracket_lib::prelude::*; use specs::prelude::*; -use std::collections::BTreeMap; +use std::collections::HashMap; /// Handles the Remove Curse menu. pub fn remove_curse(gs: &mut State, ctx: &mut BTerm) -> (ItemMenuResult, Option) { @@ -33,11 +38,12 @@ pub fn remove_curse(gs: &mut State, ctx: &mut BTerm) -> (ItemMenuResult, Option< let beatitudes = gs.ecs.read_storage::(); let names = gs.ecs.read_storage::(); let renderables = gs.ecs.read_storage::(); + let keys = gs.ecs.read_storage::(); let build_cursed_iterator = || { - (&entities, &items, &beatitudes, &renderables, &names) + (&entities, &items, &beatitudes, &renderables, &names, &keys) .join() - .filter(|(item_entity, _i, b, _r, _n)| { + .filter(|(item_entity, _i, b, _r, _n, _k)| { // Set all items to FALSE initially. let mut keep = false; // If found in the player's backpack, set to TRUE @@ -86,8 +92,8 @@ pub fn remove_curse(gs: &mut State, ctx: &mut BTerm) -> (ItemMenuResult, Option< .log(); return (ItemMenuResult::Selected, Some(item)); } - let mut player_inventory: super::PlayerInventory = BTreeMap::new(); - for (entity, _i, _b, renderable, name) in build_cursed_iterator() { + let mut player_inventory: super::PlayerInventory = HashMap::new(); + for (entity, _i, _b, renderable, name, key) in build_cursed_iterator() { let (singular, plural) = obfuscate_name_ecs(&gs.ecs, entity); let beatitude_status = if let Some(beatitude) = gs.ecs.read_storage::().get(entity) @@ -100,20 +106,17 @@ pub fn remove_curse(gs: &mut State, ctx: &mut BTerm) -> (ItemMenuResult, Option< } else { 0 }; - let unique_item = UniqueInventoryItem { - display_name: super::DisplayName { singular: singular.clone(), plural: plural.clone() }, - rgb: item_colour_ecs(&gs.ecs, entity), - renderables: renderable_colour(&renderables, entity), - glyph: renderable.glyph, - beatitude_status: beatitude_status, - name: name.name.clone(), - }; + let unique_item = unique_ecs(&gs.ecs, entity); player_inventory .entry(unique_item) - .and_modify(|(_e, count)| { - *count += 1; + .and_modify(|slot| { + slot.count += 1; }) - .or_insert((entity, 1)); + .or_insert(InventorySlot { + item: entity, + count: 1, + idx: key.idx, + }); } // Get display args let width = get_max_inventory_width(&player_inventory); @@ -128,7 +131,7 @@ pub fn remove_curse(gs: &mut State, ctx: &mut BTerm) -> (ItemMenuResult, Option< "Decurse which item? [aA-zZ][Esc.]" ); ctx.draw_box(x, y, width + 2, count + 1, RGB::named(WHITE), RGB::named(BLACK)); - print_options(&player_inventory, x + 1, y + 1, ctx); + print_options(&gs.ecs, &player_inventory, x + 1, y + 1, ctx); // Input match ctx.key { None => (ItemMenuResult::NoResponse, None), @@ -136,21 +139,17 @@ pub fn remove_curse(gs: &mut State, ctx: &mut BTerm) -> (ItemMenuResult, Option< match key { VirtualKeyCode::Escape => (ItemMenuResult::Cancel, None), _ => { - let selection = letter_to_option(key); - if selection > -1 && selection < (count as i32) { - let item = player_inventory - .iter() - .nth(selection as usize) - .unwrap().1.0; - gamelog::Logger - ::new() - .append("You decurse the") - .colour(item_colour_ecs(&gs.ecs, item)) - .append_n(obfuscate_name_ecs(&gs.ecs, item).0) - .colour(WHITE) - .append("!") - .log(); - return (ItemMenuResult::Selected, Some(item)); + let selection = letter_to_option::letter_to_option(key, ctx.shift); + if selection != -1 && check_key(selection as usize) { + // Get the first entity with a Key {} component that has an idx matching "selection". + let entities = gs.ecs.entities(); + let keyed_items = gs.ecs.read_storage::(); + let backpack = gs.ecs.read_storage::(); + for (e, key, _b) in (&entities, &keyed_items, &backpack).join() { + if key.idx == (selection as usize) { + return (ItemMenuResult::Selected, Some(e)); + } + } } (ItemMenuResult::NoResponse, None) } diff --git a/src/inventory/collection_system.rs b/src/inventory/collection_system.rs index 2fb7276..5ef9619 100644 --- a/src/inventory/collection_system.rs +++ b/src/inventory/collection_system.rs @@ -12,6 +12,9 @@ use crate::{ ObfuscatedName, Position, WantsToPickupItem, + WantsToAssignKey, + Renderable, + Stackable, }; use specs::prelude::*; use crate::data::messages; @@ -30,9 +33,12 @@ impl<'a> System<'a> for ItemCollectionSystem { WriteStorage<'a, EquipmentChanged>, ReadStorage<'a, MagicItem>, ReadStorage<'a, ObfuscatedName>, + ReadStorage<'a, Renderable>, ReadStorage<'a, Beatitude>, ReadExpect<'a, MasterDungeonMap>, ReadStorage<'a, Charges>, + ReadStorage<'a, WantsToAssignKey>, + ReadStorage<'a, Stackable>, ); fn run(&mut self, data: Self::SystemData) { @@ -45,20 +51,16 @@ impl<'a> System<'a> for ItemCollectionSystem { mut equipment_changed, magic_items, obfuscated_names, + renderables, beatitudes, dm, wands, + wants_key, + stackable, ) = data; - - for pickup in wants_pickup.join() { - positions.remove(pickup.item); - backpack - .insert(pickup.item, InBackpack { owner: pickup.collected_by }) - .expect("Unable to pickup item."); - equipment_changed - .insert(pickup.collected_by, EquipmentChanged {}) - .expect("Unable to insert EquipmentChanged."); - + let mut to_remove: Vec = Vec::new(); + // For every item that wants to be picked up that *isn't* waiting on a key assignment. + for (pickup, _key) in (&wants_pickup, !&wants_key).join() { if pickup.collected_by == *player_entity { gamelog::Logger ::new() @@ -82,8 +84,17 @@ impl<'a> System<'a> for ItemCollectionSystem { .period() .log(); } + positions.remove(pickup.item); + backpack + .insert(pickup.item, InBackpack { owner: pickup.collected_by }) + .expect("Unable to pickup item"); + equipment_changed + .insert(pickup.collected_by, EquipmentChanged {}) + .expect("Unable to insert EquipmentChanged"); + to_remove.push(pickup.collected_by); + } + for item in to_remove.iter() { + wants_pickup.remove(*item); } - - wants_pickup.clear(); } } diff --git a/src/inventory/drop_system.rs b/src/inventory/drop_system.rs index 34084b4..af2d8a2 100644 --- a/src/inventory/drop_system.rs +++ b/src/inventory/drop_system.rs @@ -12,6 +12,7 @@ use crate::{ ObfuscatedName, Position, WantsToDropItem, + WantsToRemoveKey, }; use specs::prelude::*; use crate::data::messages; @@ -34,6 +35,7 @@ impl<'a> System<'a> for ItemDropSystem { ReadStorage<'a, ObfuscatedName>, ReadExpect<'a, MasterDungeonMap>, ReadStorage<'a, Charges>, + WriteStorage<'a, WantsToRemoveKey>, ); fn run(&mut self, data: Self::SystemData) { @@ -50,6 +52,7 @@ impl<'a> System<'a> for ItemDropSystem { obfuscated_names, dm, wands, + mut keys, ) = data; for (entity, to_drop) in (&entities, &wants_drop).join() { @@ -68,6 +71,9 @@ impl<'a> System<'a> for ItemDropSystem { backpack.remove(to_drop.item); if entity == *player_entity { + keys.insert(to_drop.item, WantsToRemoveKey {}).expect( + "Unable to insert WantsToRemoveKey" + ); gamelog::Logger ::new() .append(messages::YOU_DROP_ITEM) diff --git a/src/inventory/keyhandling.rs b/src/inventory/keyhandling.rs new file mode 100644 index 0000000..7194a18 --- /dev/null +++ b/src/inventory/keyhandling.rs @@ -0,0 +1,153 @@ +use crate::{ + gamelog, + gui::unique, + Beatitude, + Charges, + MagicItem, + MasterDungeonMap, + Name, + ObfuscatedName, + Stackable, + Renderable, + WantsToAssignKey, + WantsToRemoveKey, + Key, +}; +use specs::prelude::*; +use crate::data::messages; +use bracket_lib::prelude::*; +use crate::invkeys::*; + +pub struct KeyHandling {} + +const DEBUG_KEYHANDLING: bool = true; + +impl<'a> System<'a> for KeyHandling { + #[allow(clippy::type_complexity)] + type SystemData = ( + Entities<'a>, + WriteStorage<'a, WantsToAssignKey>, + WriteStorage<'a, WantsToRemoveKey>, + WriteStorage<'a, Key>, + ReadStorage<'a, Stackable>, + ReadStorage<'a, Name>, + ReadStorage<'a, ObfuscatedName>, + ReadStorage<'a, Renderable>, + ReadStorage<'a, Beatitude>, + ReadStorage<'a, MagicItem>, + ReadStorage<'a, Charges>, + ReadExpect<'a, MasterDungeonMap>, + ); + + fn run(&mut self, data: Self::SystemData) { + let ( + entities, + mut wants_keys, + mut wants_removekey, + mut keys, + stackable, + names, + obfuscated_names, + renderables, + beatitudes, + magic_items, + wands, + dm, + ) = data; + + // For every entity that wants to be picked up, that still needs a key assigned. + for (e, _wants_key) in (&entities, &wants_keys).join() { + if DEBUG_KEYHANDLING { + console::log(&format!("KEYHANDLING: Assigning key to {:?}", e)); + } + let (stacks, mut handled, unique) = ( + if let Some(_) = stackable.get(e) { true } else { false }, + false, + unique( + e, + &names, + &obfuscated_names, + &renderables, + &beatitudes, + &magic_items, + Some(&wands), + &dm + ), + ); + if stacks { + console::log(&format!("KEYHANDLING: Item is stackable.")); + let maybe_key = item_exists(&unique); + if maybe_key.is_some() { + console::log(&format!("KEYHANDLING: Existing stack found for this item.")); + let key = maybe_key.unwrap(); + keys.insert(e, Key { idx: key }).expect("Unable to insert Key."); + console::log(&format!("KEYHANDLING: Assigned key idx {} to item.", key)); + handled = true; + } + } + if !handled { + console::log( + &format!("KEYHANDLING: Item is not stackable, or no existing stack found.") + ); + if let Some(idx) = assign_next_available() { + console::log( + &format!("KEYHANDLING: Assigned next available index {} to item.", idx) + ); + keys.insert(e, Key { idx }).expect("Unable to insert Key."); + register_stackable(stacks, unique, idx); + } else { + console::log(&format!("KEYHANDLING: No more keys available.")); + gamelog::Logger + ::new() + .append(messages::NO_MORE_KEYS) + .colour(WHITE) + .period() + .log(); + } + } + } + for (e, _wants_key) in (&entities, &wants_removekey).join() { + let idx = keys.get(e).unwrap().idx; + if DEBUG_KEYHANDLING { + console::log(&format!("KEYHANDLING: Removing key from {:?}", e)); + } + // If the item is *not* stackable, then we can just remove the key and clear the index. + if let None = stackable.get(e) { + console::log( + &format!("KEYHANDLING: Item is not stackable, clearing index {}.", idx) + ); + clear_idx(idx); + keys.remove(e); + continue; + } + // If the item *is* stackable, then we need to check if there are any other items that + // share this key assignment, before clearing the index. + console::log( + &format!( + "KEYHANDLING: Item is stackable, checking if any other items share this key." + ) + ); + let mut sole_item_with_key = true; + for (entity, key) in (&entities, &keys).join() { + if entity != e && key.idx == idx { + console::log(&format!("KEYHANDLING: Another item shares index {}", idx)); + sole_item_with_key = false; + break; + } + } + // If no other items shared this key, free up the index. + if sole_item_with_key { + console::log( + &format!("KEYHANDLING: No other items found, clearing index {}.", idx) + ); + clear_idx(idx); + } + // Either way, remove the key component from this item, because we're dropping it. + console::log(&format!("KEYHANDLING: Removing key component from item.")); + keys.remove(e); + } + + wants_removekey.clear(); + wants_keys.clear(); + } +} diff --git a/src/inventory/mod.rs b/src/inventory/mod.rs index eceaccb..76748e0 100644 --- a/src/inventory/mod.rs +++ b/src/inventory/mod.rs @@ -4,6 +4,7 @@ mod equip_system; mod identification_system; mod remove_system; mod use_system; +mod keyhandling; pub use self::{ collection_system::ItemCollectionSystem, @@ -12,4 +13,5 @@ pub use self::{ identification_system::ItemIdentificationSystem, remove_system::ItemRemoveSystem, use_system::ItemUseSystem, + keyhandling::KeyHandling, }; diff --git a/src/invkeys.rs b/src/invkeys.rs new file mode 100644 index 0000000..e52824b --- /dev/null +++ b/src/invkeys.rs @@ -0,0 +1,60 @@ +use std::sync::Mutex; +use std::collections::{ HashMap }; +use specs::prelude::*; +use crate::gui::UniqueInventoryItem; + +lazy_static! { + pub static ref INVKEYS: Mutex> = Mutex::new(HashMap::new()); + pub static ref ASSIGNEDKEYS: Mutex> = Mutex::new(vec![false; 52]); +} + +/// For (de)serialization. +pub fn clone_invkeys() -> HashMap { + let invkeys = INVKEYS.lock().unwrap(); + invkeys.clone() +} +pub fn restore_invkeys(invkeys: HashMap) { + INVKEYS.lock().unwrap().clear(); + INVKEYS.lock().unwrap().extend(invkeys); +} + +pub fn check_key(idx: usize) -> bool { + let lock = ASSIGNEDKEYS.lock().unwrap(); + lock[idx] +} + +pub fn item_exists(item: &UniqueInventoryItem) -> Option { + let invkeys = INVKEYS.lock().unwrap(); + use bracket_lib::prelude::*; + console::log(&format!("{:?}", item)); + if invkeys.contains_key(item) { + Some(*invkeys.get(item).unwrap()) + } else { + None + } +} + +pub fn assign_next_available() -> Option { + let mut lock = ASSIGNEDKEYS.lock().unwrap(); + for (i, key) in lock.iter_mut().enumerate() { + if !*key { + *key = true; + return Some(i); + } + } + None +} + +pub fn register_stackable(stacks: bool, item: UniqueInventoryItem, idx: usize) { + if stacks { + let mut invkeys = INVKEYS.lock().unwrap(); + invkeys.insert(item, idx); + } +} + +pub fn clear_idx(idx: usize) { + let mut lock = ASSIGNEDKEYS.lock().unwrap(); + lock[idx] = false; + let mut invkeys = INVKEYS.lock().unwrap(); + invkeys.retain(|_k, v| *v != idx); +} diff --git a/src/lib.rs b/src/lib.rs index 6bbdd04..e184a58 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -38,6 +38,7 @@ pub mod rex_assets; pub mod spatial; pub mod morgue; pub mod states; +pub mod invkeys; pub use components::*; use particle_system::ParticleBuilder; diff --git a/src/main.rs b/src/main.rs index f430376..fc2c72b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -112,6 +112,11 @@ fn main() -> BError { gs.ecs.register::(); gs.ecs.register::(); gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); gs.ecs.register::>(); gs.ecs.register::(); gs.ecs.register::(); diff --git a/src/player.rs b/src/player.rs index dd8ecf3..36af74b 100644 --- a/src/player.rs +++ b/src/player.rs @@ -30,6 +30,7 @@ use super::{ Viewshed, WantsToMelee, WantsToPickupItem, + WantsToAssignKey, get_dest, Destination, DamageType, @@ -633,7 +634,9 @@ fn get_item(ecs: &mut World) -> RunState { return RunState::AwaitingInput; } Some(item) => { + let mut assignkey = ecs.write_storage::(); let mut pickup = ecs.write_storage::(); + assignkey.insert(item, WantsToAssignKey {}).expect("Unable to insert WantsToAssignKey"); pickup .insert(*player_entity, WantsToPickupItem { collected_by: *player_entity, item }) .expect("Unable to insert want to pickup item."); diff --git a/src/raws/item_structs.rs b/src/raws/item_structs.rs index 897d48d..c670e54 100644 --- a/src/raws/item_structs.rs +++ b/src/raws/item_structs.rs @@ -6,6 +6,7 @@ pub struct Item { pub id: String, pub name: Name, pub renderable: Option, + pub class: String, pub weight: Option, pub value: Option, pub equip: Option, diff --git a/src/raws/rawmaster.rs b/src/raws/rawmaster.rs index 813dbe2..0e2b227 100644 --- a/src/raws/rawmaster.rs +++ b/src/raws/rawmaster.rs @@ -66,6 +66,7 @@ macro_rules! apply_flags { "IDENTIFY" => $eb = $eb.with(ProvidesIdentify {}), "DIGGER" => $eb = $eb.with(Digger {}), "MAGICMAP" => $eb = $eb.with(MagicMapper {}), + "STACKABLE" => $eb = $eb.with(Stackable {}), // CAN BE DESTROYED BY DAMAGE "DESTRUCTIBLE" => $eb = $eb.with(Destructible {}), // --- EQUIP SLOTS --- @@ -281,6 +282,7 @@ pub fn spawn_named_item( if known_beatitude && !identified_items.contains(&item_template.name.name) { dm.identified_items.insert(item_template.name.name.clone()); } + let needs_key = is_player_owned(&player_entity, &pos); std::mem::drop(player_entity); std::mem::drop(dm); // -- DROP EVERYTHING THAT INVOLVES THE ECS BEFORE THIS POINT --- @@ -293,9 +295,23 @@ pub fn spawn_named_item( eb = eb.with(Item { weight: item_template.weight.unwrap_or(0.0), value: item_template.value.unwrap_or(0.0), + category: match item_template.class.as_str() { + "amulet" => ItemType::Amulet, + "weapon" => ItemType::Weapon, + "armour" => ItemType::Armour, + "comestible" => ItemType::Comestible, + "scroll" => ItemType::Scroll, + "spellbook" => ItemType::Spellbook, + "potion" => ItemType::Potion, + "ring" => ItemType::Ring, + "wand" => ItemType::Wand, + _ => unreachable!("Unknown item type."), + }, }); eb = spawn_position(pos, eb, key, raws); - + if needs_key { + eb = eb.with(WantsToAssignKey {}); + } if let Some(renderable) = &item_template.renderable { eb = eb.with(get_renderable_component(renderable)); } @@ -392,6 +408,7 @@ pub fn spawn_named_mob( if raws.mob_index.contains_key(key) { let mob_template = &raws.raws.mobs[raws.mob_index[key]]; let mut player_level = 1; + let needs_key; { let pools = ecs.read_storage::(); let player_entity = ecs.fetch::(); @@ -399,12 +416,15 @@ pub fn spawn_named_mob( if let Some(pool) = player_pool { player_level = pool.level; } + needs_key = is_player_owned(&player_entity, &pos); } - let mut eb; // New entity with a position, name, combatstats, and viewshed eb = ecs.create_entity().marked::>(); eb = spawn_position(pos, eb, key, raws); + if needs_key { + eb = eb.with(WantsToAssignKey {}); + } eb = eb.with(Name { name: mob_template.name.clone(), plural: mob_template.name.clone() }); eb = eb.with(Viewshed { visible_tiles: Vec::new(), @@ -632,10 +652,18 @@ pub fn spawn_named_prop( pos: SpawnType ) -> Option { if raws.prop_index.contains_key(key) { + let needs_key; + { + let player_entity = ecs.fetch::(); + needs_key = is_player_owned(&player_entity, &pos); + } // ENTITY BUILDER PREP let prop_template = &raws.raws.props[raws.prop_index[key]]; let mut eb = ecs.create_entity().marked::>(); eb = spawn_position(pos, eb, key, raws); + if needs_key { + eb = eb.with(WantsToAssignKey {}); + } // APPLY MANDATORY COMPONENTS FOR A PROP: // - Name // - Prop {} @@ -686,6 +714,23 @@ fn spawn_position<'a>( eb } +fn is_player_owned(player: &Entity, pos: &SpawnType) -> bool { + match pos { + SpawnType::Carried { by } => { + if by == player { + return true; + } + } + SpawnType::Equipped { by } => { + if by == player { + return true; + } + } + _ => {} + } + false +} + fn get_renderable_component( renderable: &super::item_structs::Renderable ) -> crate::components::Renderable { diff --git a/src/saveload_system.rs b/src/saveload_system.rs index 3a3f733..f3b284d 100644 --- a/src/saveload_system.rs +++ b/src/saveload_system.rs @@ -99,6 +99,7 @@ pub fn save_game(ecs: &mut World) { IntrinsicChanged, Intrinsics, Item, + Key, KnownSpells, LootTable, MagicItem, @@ -128,17 +129,21 @@ pub fn save_game(ecs: &mut World) { SpawnParticleBurst, SpawnParticleLine, SpawnParticleSimple, + Stackable, TakingTurn, Telepath, ToHitBonus, Viewshed, Charges, WantsToApproach, + WantsToAssignKey, + WantsToDelete, WantsToDropItem, WantsToFlee, WantsToMelee, WantsToPickupItem, WantsToRemoveItem, + WantsToRemoveKey, WantsToUseItem, SerializationHelper, DMSerializationHelper @@ -232,6 +237,7 @@ pub fn load_game(ecs: &mut World) { IntrinsicChanged, Intrinsics, Item, + Key, KnownSpells, LootTable, MagicItem, @@ -261,17 +267,21 @@ pub fn load_game(ecs: &mut World) { SpawnParticleBurst, SpawnParticleLine, SpawnParticleSimple, + Stackable, TakingTurn, Telepath, ToHitBonus, Viewshed, Charges, WantsToApproach, + WantsToAssignKey, + WantsToDelete, WantsToDropItem, WantsToFlee, WantsToMelee, WantsToPickupItem, WantsToRemoveItem, + WantsToRemoveKey, WantsToUseItem, SerializationHelper, DMSerializationHelper diff --git a/src/states/state.rs b/src/states/state.rs index 0217cca..3dde32b 100644 --- a/src/states/state.rs +++ b/src/states/state.rs @@ -64,12 +64,13 @@ impl State { fn resolve_entity_decisions(&mut self) { let mut trigger_system = trigger_system::TriggerSystem {}; - let mut inventory_system = inventory::ItemCollectionSystem {}; let mut item_equip_system = inventory::ItemEquipSystem {}; let mut item_use_system = inventory::ItemUseSystem {}; let mut item_drop_system = inventory::ItemDropSystem {}; let mut item_remove_system = inventory::ItemRemoveSystem {}; + let mut inventory_system = inventory::ItemCollectionSystem {}; let mut item_id_system = inventory::ItemIdentificationSystem {}; + let mut key_system = inventory::KeyHandling {}; let mut melee_system = MeleeCombatSystem {}; trigger_system.run_now(&self.ecs); inventory_system.run_now(&self.ecs); @@ -78,6 +79,7 @@ impl State { item_drop_system.run_now(&self.ecs); item_remove_system.run_now(&self.ecs); item_id_system.run_now(&self.ecs); + key_system.run_now(&self.ecs); melee_system.run_now(&self.ecs); effects::run_effects_queue(&mut self.ecs); @@ -342,7 +344,11 @@ impl GameState for State { gui::ItemMenuResult::NoResponse => {} gui::ItemMenuResult::Selected => { let item_entity = result.1.unwrap(); + let mut removekey = self.ecs.write_storage::(); let mut intent = self.ecs.write_storage::(); + removekey + .insert(item_entity, WantsToRemoveKey {}) + .expect("Unable to insert WantsToRemoveKey"); intent .insert(*self.ecs.fetch::(), WantsToDropItem { item: item_entity, From ba5d120fef177083b7a8bbd56b5b8d62b38882a3 Mon Sep 17 00:00:00 2001 From: Llywelwyn Date: Sat, 15 Jun 2024 20:40:51 +0100 Subject: [PATCH 42/50] fix warns, bump ver --- Cargo.toml | 2 +- src/gui/identify_menu.rs | 5 +---- src/gui/mod.rs | 4 ++-- src/gui/remove_curse_menu.rs | 16 +--------------- src/inventory/collection_system.rs | 6 ------ src/invkeys.rs | 3 +-- 6 files changed, 6 insertions(+), 30 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 24b6918..4ef01ec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rust-rl" -version = "0.1.1" +version = "0.1.2" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/src/gui/identify_menu.rs b/src/gui/identify_menu.rs index 422b2a5..31ce8d7 100644 --- a/src/gui/identify_menu.rs +++ b/src/gui/identify_menu.rs @@ -5,11 +5,8 @@ use super::{ print_options, unique_ecs, check_key, - renderable_colour, letter_to_option, ItemMenuResult, - UniqueInventoryItem, - BUC, }; use crate::{ gamelog, @@ -99,7 +96,7 @@ pub fn identify(gs: &mut State, ctx: &mut BTerm) -> (ItemMenuResult, Option (ItemMenuResult, Option< return (ItemMenuResult::Selected, Some(item)); } let mut player_inventory: super::PlayerInventory = HashMap::new(); - for (entity, _i, _b, renderable, name, key) in build_cursed_iterator() { - let (singular, plural) = obfuscate_name_ecs(&gs.ecs, entity); - let beatitude_status = if - let Some(beatitude) = gs.ecs.read_storage::().get(entity) - { - match beatitude.buc { - BUC::Blessed => 1, - BUC::Uncursed => 2, - BUC::Cursed => 3, - } - } else { - 0 - }; + for (entity, _i, _b, _r, _n, key) in build_cursed_iterator() { let unique_item = unique_ecs(&gs.ecs, entity); player_inventory .entry(unique_item) diff --git a/src/inventory/collection_system.rs b/src/inventory/collection_system.rs index 5ef9619..70fb25c 100644 --- a/src/inventory/collection_system.rs +++ b/src/inventory/collection_system.rs @@ -13,8 +13,6 @@ use crate::{ Position, WantsToPickupItem, WantsToAssignKey, - Renderable, - Stackable, }; use specs::prelude::*; use crate::data::messages; @@ -33,12 +31,10 @@ impl<'a> System<'a> for ItemCollectionSystem { WriteStorage<'a, EquipmentChanged>, ReadStorage<'a, MagicItem>, ReadStorage<'a, ObfuscatedName>, - ReadStorage<'a, Renderable>, ReadStorage<'a, Beatitude>, ReadExpect<'a, MasterDungeonMap>, ReadStorage<'a, Charges>, ReadStorage<'a, WantsToAssignKey>, - ReadStorage<'a, Stackable>, ); fn run(&mut self, data: Self::SystemData) { @@ -51,12 +47,10 @@ impl<'a> System<'a> for ItemCollectionSystem { mut equipment_changed, magic_items, obfuscated_names, - renderables, beatitudes, dm, wands, wants_key, - stackable, ) = data; let mut to_remove: Vec = Vec::new(); // For every item that wants to be picked up that *isn't* waiting on a key assignment. diff --git a/src/invkeys.rs b/src/invkeys.rs index e52824b..2cee2f4 100644 --- a/src/invkeys.rs +++ b/src/invkeys.rs @@ -1,6 +1,5 @@ use std::sync::Mutex; -use std::collections::{ HashMap }; -use specs::prelude::*; +use std::collections::HashMap; use crate::gui::UniqueInventoryItem; lazy_static! { From f494efbf3f82525f592737e70af5f09e7d6711b9 Mon Sep 17 00:00:00 2001 From: Llywelwyn Date: Sat, 15 Jun 2024 20:41:21 +0100 Subject: [PATCH 43/50] bump ver --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 4ef01ec..55d7645 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rust-rl" -version = "0.1.2" +version = "0.1.4" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html From d465592c0fc52dcb96505c2f20d12d10f607b4fd Mon Sep 17 00:00:00 2001 From: Llywelwyn Date: Sun, 16 Jun 2024 10:31:06 +0100 Subject: [PATCH 44/50] abstracts ui drawing --- src/gui/mod.rs | 103 +++++++++++++++++++++++-------------------------- 1 file changed, 48 insertions(+), 55 deletions(-) diff --git a/src/gui/mod.rs b/src/gui/mod.rs index 5a50698..c28ff0b 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -106,6 +106,50 @@ pub fn draw_lerping_bar( ctx.print(sx + width, sy, "]"); } +fn draw_xp(ctx: &mut BTerm, pt: Point, pool: &Pools) { + ctx.print_color( + pt.x, + pt.y, + RGB::named(WHITE), + RGB::named(BLACK), + format!("XP{}/{}", pool.level, pool.xp) + ); +} + +fn calc_ac(ecs: &World, skills: &Skills, stats: &Pools, attr: &Attributes) -> i32 { + let skill_ac_bonus = gamesystem::skill_bonus(Skill::Defence, skills); + let mut armour_ac_bonus = 0; + let equipped = ecs.read_storage::(); + let ac = ecs.read_storage::(); + let player_entity = ecs.fetch::(); + for (wielded, ac) in (&equipped, &ac).join() { + if wielded.owner == *player_entity { + armour_ac_bonus += ac.amount; + } + } + stats.bac - attr.dexterity.bonus / 2 - skill_ac_bonus - armour_ac_bonus +} + +fn draw_ac(ctx: &mut BTerm, pt: Point, ac: i32) { + ctx.print_color(pt.x, pt.y, RGB::named(PINK), RGB::named(BLACK), "AC"); + ctx.print_color(pt.x + 2, pt.y, RGB::named(WHITE), RGB::named(BLACK), ac); +} + +fn draw_attributes(ctx: &mut BTerm, pt: Point, a: &Attributes) { + ctx.print_color(pt.x, pt.y, RGB::named(RED), RGB::named(BLACK), "STR"); + ctx.print_color(pt.x + 3, pt.y, RGB::named(WHITE), RGB::named(BLACK), a.strength.base); + ctx.print_color(pt.x + 7, pt.y, RGB::named(GREEN), RGB::named(BLACK), "DEX"); + ctx.print_color(pt.x + 10, pt.y, RGB::named(WHITE), RGB::named(BLACK), a.dexterity.base); + ctx.print_color(pt.x + 14, pt.y, RGB::named(ORANGE), RGB::named(BLACK), "CON"); + ctx.print_color(pt.x + 17, pt.y, RGB::named(WHITE), RGB::named(BLACK), a.constitution.base); + ctx.print_color(pt.x, 54, RGB::named(CYAN), RGB::named(BLACK), "INT"); + ctx.print_color(pt.x + 3, pt.y + 1, RGB::named(WHITE), RGB::named(BLACK), a.intelligence.base); + ctx.print_color(pt.x + 7, pt.y + 1, RGB::named(YELLOW), RGB::named(BLACK), "WIS"); + ctx.print_color(pt.x + 10, pt.y + 1, RGB::named(WHITE), RGB::named(BLACK), a.wisdom.base); + ctx.print_color(pt.x + 14, pt.y + 1, RGB::named(PURPLE), RGB::named(BLACK), "CHA"); + ctx.print_color(pt.x + 17, pt.y + 1, RGB::named(WHITE), RGB::named(BLACK), a.charisma.base); +} + pub fn draw_ui(ecs: &World, ctx: &mut BTerm) { // Render stats let pools = ecs.read_storage::(); @@ -142,61 +186,9 @@ pub fn draw_ui(ecs: &World, ctx: &mut BTerm) { RGB::named(BLUE), RGB::named(BLACK) ); - // Draw AC - let skill_ac_bonus = gamesystem::skill_bonus(Skill::Defence, &*skills); - let mut armour_ac_bonus = 0; - let equipped = ecs.read_storage::(); - let ac = ecs.read_storage::(); - let player_entity = ecs.fetch::(); - for (wielded, ac) in (&equipped, &ac).join() { - if wielded.owner == *player_entity { - armour_ac_bonus += ac.amount; - } - } - let armour_class = - stats.bac - attributes.dexterity.bonus / 2 - skill_ac_bonus - armour_ac_bonus; - ctx.print_color(26, 53, RGB::named(PINK), RGB::named(BLACK), "AC"); - ctx.print_color(28, 53, RGB::named(WHITE), RGB::named(BLACK), armour_class); - // Draw level - ctx.print_color( - 26, - 54, - RGB::named(WHITE), - RGB::named(BLACK), - format!("XP{}/{}", stats.level, stats.xp) - ); - // Draw attributes - let x = 38; - ctx.print_color(x, 53, RGB::named(RED), RGB::named(BLACK), "STR"); - ctx.print_color(x + 3, 53, RGB::named(WHITE), RGB::named(BLACK), attributes.strength.base); - ctx.print_color(x + 7, 53, RGB::named(GREEN), RGB::named(BLACK), "DEX"); - ctx.print_color( - x + 10, - 53, - RGB::named(WHITE), - RGB::named(BLACK), - attributes.dexterity.base - ); - ctx.print_color(x + 14, 53, RGB::named(ORANGE), RGB::named(BLACK), "CON"); - ctx.print_color( - x + 17, - 53, - RGB::named(WHITE), - RGB::named(BLACK), - attributes.constitution.base - ); - ctx.print_color(x, 54, RGB::named(CYAN), RGB::named(BLACK), "INT"); - ctx.print_color( - x + 3, - 54, - RGB::named(WHITE), - RGB::named(BLACK), - attributes.intelligence.base - ); - ctx.print_color(x + 7, 54, RGB::named(YELLOW), RGB::named(BLACK), "WIS"); - ctx.print_color(x + 10, 54, RGB::named(WHITE), RGB::named(BLACK), attributes.wisdom.base); - ctx.print_color(x + 14, 54, RGB::named(PURPLE), RGB::named(BLACK), "CHA"); - ctx.print_color(x + 17, 54, RGB::named(WHITE), RGB::named(BLACK), attributes.charisma.base); + draw_ac(ctx, Point::new(26, 53), calc_ac(ecs, skills, stats, attributes)); + draw_xp(ctx, Point::new(26, 54), stats); + draw_attributes(ctx, Point::new(38, 53), attributes); // Draw hunger match hunger.state { HungerState::Satiated => { @@ -247,6 +239,7 @@ pub fn draw_ui(ecs: &World, ctx: &mut BTerm) { } } // Burden + let player_entity = ecs.fetch::(); if let Some(burden) = burden.get(*player_entity) { match burden.level { crate::BurdenLevel::Burdened => { From 6324449c1601e85121494bab73cdb88c3368407a Mon Sep 17 00:00:00 2001 From: Llywelwyn Date: Mon, 17 Jun 2024 23:19:20 +0100 Subject: [PATCH 45/50] draw_hunger() --- src/gui/mod.rs | 101 +++++++++++++++++++++++++------------------------ 1 file changed, 52 insertions(+), 49 deletions(-) diff --git a/src/gui/mod.rs b/src/gui/mod.rs index c28ff0b..bde2cf0 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -150,6 +150,57 @@ fn draw_attributes(ctx: &mut BTerm, pt: Point, a: &Attributes) { ctx.print_color(pt.x + 17, pt.y + 1, RGB::named(WHITE), RGB::named(BLACK), a.charisma.base); } +fn draw_hunger(ctx: &mut BTerm, pt: Point, hunger: &HungerClock) { + match hunger.state { + HungerState::Satiated => { + ctx.print_color_right( + 70, + 53, + get_hunger_colour(hunger.state), + RGB::named(BLACK), + "Satiated" + ); + } + HungerState::Normal => {} + HungerState::Hungry => { + ctx.print_color_right( + 70, + 53, + get_hunger_colour(hunger.state), + RGB::named(BLACK), + "Hungry" + ); + } + HungerState::Weak => { + ctx.print_color_right( + 70, + 53, + get_hunger_colour(hunger.state), + RGB::named(BLACK), + "Weak" + ); + } + HungerState::Fainting => { + ctx.print_color_right( + 70, + 53, + get_hunger_colour(hunger.state), + RGB::named(BLACK), + "Fainting" + ); + } + HungerState::Starving => { + ctx.print_color_right( + 70, + 53, + get_hunger_colour(hunger.state), + RGB::named(BLACK), + "Starving" + ); + } + } +} + pub fn draw_ui(ecs: &World, ctx: &mut BTerm) { // Render stats let pools = ecs.read_storage::(); @@ -189,55 +240,7 @@ pub fn draw_ui(ecs: &World, ctx: &mut BTerm) { draw_ac(ctx, Point::new(26, 53), calc_ac(ecs, skills, stats, attributes)); draw_xp(ctx, Point::new(26, 54), stats); draw_attributes(ctx, Point::new(38, 53), attributes); - // Draw hunger - match hunger.state { - HungerState::Satiated => { - ctx.print_color_right( - 70, - 53, - get_hunger_colour(hunger.state), - RGB::named(BLACK), - "Satiated" - ); - } - HungerState::Normal => {} - HungerState::Hungry => { - ctx.print_color_right( - 70, - 53, - get_hunger_colour(hunger.state), - RGB::named(BLACK), - "Hungry" - ); - } - HungerState::Weak => { - ctx.print_color_right( - 70, - 53, - get_hunger_colour(hunger.state), - RGB::named(BLACK), - "Weak" - ); - } - HungerState::Fainting => { - ctx.print_color_right( - 70, - 53, - get_hunger_colour(hunger.state), - RGB::named(BLACK), - "Fainting" - ); - } - HungerState::Starving => { - ctx.print_color_right( - 70, - 53, - get_hunger_colour(hunger.state), - RGB::named(BLACK), - "Starving" - ); - } - } + draw_hunger(ctx, Point::new(70, 53), hunger); // Burden let player_entity = ecs.fetch::(); if let Some(burden) = burden.get(*player_entity) { From 99c17f85213c3f5fbaa8d369bdc6bed75bfab696 Mon Sep 17 00:00:00 2001 From: Llywelwyn Date: Mon, 17 Jun 2024 23:22:30 +0100 Subject: [PATCH 46/50] draw hunger uses Point --- src/gui/mod.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/gui/mod.rs b/src/gui/mod.rs index bde2cf0..7604527 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -154,8 +154,8 @@ fn draw_hunger(ctx: &mut BTerm, pt: Point, hunger: &HungerClock) { match hunger.state { HungerState::Satiated => { ctx.print_color_right( - 70, - 53, + pt.x, + pt.y, get_hunger_colour(hunger.state), RGB::named(BLACK), "Satiated" @@ -164,8 +164,8 @@ fn draw_hunger(ctx: &mut BTerm, pt: Point, hunger: &HungerClock) { HungerState::Normal => {} HungerState::Hungry => { ctx.print_color_right( - 70, - 53, + pt.x, + pt.y, get_hunger_colour(hunger.state), RGB::named(BLACK), "Hungry" @@ -173,8 +173,8 @@ fn draw_hunger(ctx: &mut BTerm, pt: Point, hunger: &HungerClock) { } HungerState::Weak => { ctx.print_color_right( - 70, - 53, + pt.x, + pt.y, get_hunger_colour(hunger.state), RGB::named(BLACK), "Weak" @@ -182,8 +182,8 @@ fn draw_hunger(ctx: &mut BTerm, pt: Point, hunger: &HungerClock) { } HungerState::Fainting => { ctx.print_color_right( - 70, - 53, + pt.x, + pt.y, get_hunger_colour(hunger.state), RGB::named(BLACK), "Fainting" @@ -191,8 +191,8 @@ fn draw_hunger(ctx: &mut BTerm, pt: Point, hunger: &HungerClock) { } HungerState::Starving => { ctx.print_color_right( - 70, - 53, + pt.x, + pt.y, get_hunger_colour(hunger.state), RGB::named(BLACK), "Starving" From bdcd55c8a583422819908a841ef46fe4f978e4bb Mon Sep 17 00:00:00 2001 From: Lewis Wynne Date: Sun, 9 Mar 2025 10:50:35 +0000 Subject: [PATCH 47/50] Fixes miscoloured logs (fixes #26) --- src/ai/turn_status_system.rs | 4 ---- src/effects/triggers.rs | 4 ---- 2 files changed, 8 deletions(-) diff --git a/src/ai/turn_status_system.rs b/src/ai/turn_status_system.rs index db3acaa..e072e45 100644 --- a/src/ai/turn_status_system.rs +++ b/src/ai/turn_status_system.rs @@ -65,9 +65,7 @@ impl<'a> System<'a> for TurnStatusSystem { not_confused.push(entity); if entity == *player_entity { logger = logger - .colour(renderable_colour(&renderables, entity)) .append("You") - .colour(WHITE) .append("snap out of it."); log = true; } else { @@ -94,9 +92,7 @@ impl<'a> System<'a> for TurnStatusSystem { not_my_turn.push(entity); if entity == *player_entity { logger = logger - .colour(renderable_colour(&renderables, entity)) .append("You") - .colour(WHITE) .append("are confused!"); log = true; gamelog::record_event(EVENT::PlayerConfused(1)); diff --git a/src/effects/triggers.rs b/src/effects/triggers.rs index fb54eed..cb4e5d3 100644 --- a/src/effects/triggers.rs +++ b/src/effects/triggers.rs @@ -223,9 +223,7 @@ fn handle_healing( let renderables = ecs.read_storage::(); if ecs.read_storage::().get(target).is_some() { logger = logger - .colour(renderable_colour(&renderables, target)) .append("You") - .colour(WHITE) .append(HEAL_PLAYER_HIT) .buc(event.buc.clone(), None, Some(HEAL_PLAYER_HIT_BLESSED)); } else { @@ -267,9 +265,7 @@ fn handle_damage( let player_viewshed = viewsheds.get(*ecs.fetch::()).unwrap(); if ecs.read_storage::().get(target).is_some() { logger = logger - .colour(renderable_colour(&renderables, target)) .append("You") - .colour(WHITE) .append(DAMAGE_PLAYER_HIT); event.log = true; } else if From 45b9b330396c31cbf1d7fcab8466ba8cb6d005f2 Mon Sep 17 00:00:00 2001 From: Llywelwyn <82828093+Llywelwyn@users.noreply.github.com> Date: Sun, 9 Mar 2025 11:02:16 +0000 Subject: [PATCH 48/50] Update cargo-build-test.yml --- .github/workflows/cargo-build-test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/cargo-build-test.yml b/.github/workflows/cargo-build-test.yml index d54fa2f..ae0ca42 100644 --- a/.github/workflows/cargo-build-test.yml +++ b/.github/workflows/cargo-build-test.yml @@ -16,6 +16,8 @@ jobs: steps: - uses: actions/checkout@v3 + - name: Deps + run: sudo dnf install fontconfig-devel - name: Build run: cargo build --verbose - name: Run tests From 0584d07a1f9bf45d9f591f399a1b22e720a5072f Mon Sep 17 00:00:00 2001 From: Llywelwyn <82828093+Llywelwyn@users.noreply.github.com> Date: Sun, 9 Mar 2025 11:04:32 +0000 Subject: [PATCH 49/50] Update cargo-build-test.yml to use ubuntu-22.04 --- .github/workflows/cargo-build-test.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/cargo-build-test.yml b/.github/workflows/cargo-build-test.yml index ae0ca42..cafaa58 100644 --- a/.github/workflows/cargo-build-test.yml +++ b/.github/workflows/cargo-build-test.yml @@ -12,12 +12,10 @@ env: jobs: build: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - - name: Deps - run: sudo dnf install fontconfig-devel - name: Build run: cargo build --verbose - name: Run tests From c29b93337c5e3a29ebbf286cbd0bb86369faeccd Mon Sep 17 00:00:00 2001 From: Llywelwyn <82828093+Llywelwyn@users.noreply.github.com> Date: Sun, 9 Mar 2025 11:12:11 +0000 Subject: [PATCH 50/50] Update README.md --- README.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 9127091..c1809cf 100644 --- a/README.md +++ b/README.md @@ -2,18 +2,18 @@ #### using _rltk/bracket-lib_, and _specs_ -check out the page in the header for the wasm version, pick [a release of your choice](https://github.com/Llywelwyn/rust-rl/releases), or build manually with: +[![Rust](https://github.com/Llywelwyn/rust-rl/actions/workflows/cargo-build-test.yml/badge.svg)](https://github.com/Llywelwyn/rust-rl/actions/workflows/cargo-build-test.yml) + +check out the page in the header for the wasm version, pick [a release](https://github.com/Llywelwyn/rust-rl/releases), or build manually with: `git clone https://github.com/Llywelwyn/rust-rl/ && cd rust-rl && cargo build --release`, ![image](https://github.com/Llywelwyn/rust-rl/assets/82828093/b05e4f0b-2062-4abe-9fee-c679f9ef420d) -this year for roguelikedev does the complete tutorial, i followed along with thebracket's [_roguelike tutorial - in rust_](https://bfnightly.bracketproductions.com). the notes i made during the sprint are being kept below for posterity - further changes since then are noted in [changelog.txt](https://github.com/Llywelwyn/rust-rl/blob/9150ed39e45bee536060cdc769d274e639021012/changelog.txt), and in the release notes. - -i'm also working on translating over my progress into blog entries on my site @ [llyw.co.uk](https://llyw.co.uk/), with a larger focus on some of the more interesting implementation details. - --- +
+ boring details about the sprint where this project started
week 1 @@ -157,3 +157,4 @@ i'm also working on translating over my progress into blog entries on my site @ ![squares](https://github.com/Llywelwyn/rust-rl/assets/82828093/b752e1cb-340d-475d-84ae-68fdb4977a80)
+