about summary refs log tree commit diff
path: root/src/game.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/game.rs')
-rw-r--r--src/game.rs251
1 files changed, 211 insertions, 40 deletions
diff --git a/src/game.rs b/src/game.rs
index c4fc6d2be10a..49068361b5d1 100644
--- a/src/game.rs
+++ b/src/game.rs
@@ -10,6 +10,8 @@ use crate::types::{
     pos, BoundingBox, Collision, Dimensions, Position, Positioned,
     PositionedMut, Ticks,
 };
+use crate::util::promise::Cancelled;
+use crate::util::promise::{promise, Complete, Promise, Promises};
 use crate::util::template::TemplateParams;
 use rand::rngs::SmallRng;
 use rand::SeedableRng;
@@ -36,6 +38,75 @@ impl<'a> PositionedMut for AnEntity<'a> {
     }
 }
 
+enum PromptResolution {
+    Uncancellable(Complete<String>),
+    Cancellable(Complete<Result<String, Cancelled>>),
+}
+
+impl PromptResolution {
+    fn is_cancellable(&self) -> bool {
+        use PromptResolution::*;
+        match self {
+            Uncancellable(_) => false,
+            Cancellable(_) => true,
+        }
+    }
+
+    fn fulfill(&mut self, val: String) {
+        use PromptResolution::*;
+        match self {
+            Cancellable(complete) => complete.ok(val),
+            Uncancellable(complete) => complete.fulfill(val),
+        }
+    }
+
+    fn cancel(&mut self) {
+        use PromptResolution::*;
+        match self {
+            Cancellable(complete) => complete.cancel(),
+            Uncancellable(complete) => {}
+        }
+    }
+}
+
+/// The kind of input the game is waiting to receive
+enum InputState {
+    /// The initial input state of the game - we're currently waiting for direct
+    /// commands.
+    Initial,
+
+    /// A free text prompt has been shown to the user, and every character
+    /// besides "escape" is interpreted as a response to that prompt
+    Prompt {
+        complete: PromptResolution,
+        buffer: String,
+    },
+}
+
+impl InputState {
+    fn uncancellable_prompt(complete: Complete<String>) -> Self {
+        InputState::Prompt {
+            complete: PromptResolution::Uncancellable(complete),
+            buffer: String::new(),
+        }
+    }
+
+    fn cancellable_prompt(
+        complete: Complete<Result<String, Cancelled>>,
+    ) -> Self {
+        InputState::Prompt {
+            complete: PromptResolution::Cancellable(complete),
+            buffer: String::new(),
+        }
+    }
+}
+
+impl Default for InputState {
+    fn default() -> Self {
+        InputState::Initial
+    }
+}
+
 /// The full state of a running Game
 pub struct Game<'a> {
     settings: Settings,
@@ -45,6 +116,9 @@ pub struct Game<'a> {
     /// An iterator on keypresses from the user
     keys: Keys<StdinLock<'a>>,
 
+    /// The kind of input the game is waiting to receive
+    input_state: InputState,
+
     /// The map of all the entities in the game
     entities: EntityMap<AnEntity<'a>>,
 
@@ -60,6 +134,9 @@ pub struct Game<'a> {
 
     /// A global random number generator for the game
     rng: Rng,
+
+    /// A list of promises that are waiting on the game and a result
+    promises: Promises<'a, Self>,
 }
 
 impl<'a> Game<'a> {
@@ -97,9 +174,11 @@ impl<'a> Game<'a> {
                 stdout,
             ),
             keys: stdin.keys(),
+            input_state: Default::default(),
             character_entity_id: entities.insert(Box::new(Character::new())),
             messages: Vec::new(),
             entities,
+            promises: Promises::new(),
         }
     }
 
@@ -131,6 +210,12 @@ impl<'a> Game<'a> {
             .unwrap()
     }
 
+    fn mut_character(&mut self) -> &mut Character {
+        (*self.entities.get_mut(self.character_entity_id).unwrap())
+            .downcast_mut()
+            .unwrap()
+    }
+
     /// Draw all the game entities to the screen
     fn draw_entities(&mut self) -> io::Result<()> {
         for entity in self.entities.entities() {
@@ -168,7 +253,36 @@ impl<'a> Game<'a> {
         let message = self.message(message_name, params);
         self.messages.push(message.to_string());
         self.message_idx = self.messages.len() - 1;
-        self.viewport.write_message(&message)
+        self.viewport.write_message(&message)?;
+        Ok(())
+    }
+
+    /// Prompt the user for input, returning a Future for the result of the
+    /// prompt
+    fn prompt(
+        &mut self,
+        name: &'static str,
+        params: &TemplateParams<'_>,
+    ) -> io::Result<Promise<Self, String>> {
+        let (complete, promise) = promise();
+        self.input_state = InputState::uncancellable_prompt(complete);
+        let message = self.message(name, params);
+        self.viewport.write_prompt(&message)?;
+        self.promises.push(Box::new(promise.clone()));
+        Ok(promise)
+    }
+
+    fn prompt_cancellable(
+        &mut self,
+        name: &'static str,
+        params: &TemplateParams<'_>,
+    ) -> io::Result<Promise<Self, Result<String, Cancelled>>> {
+        let (complete, promise) = promise();
+        self.input_state = InputState::cancellable_prompt(complete);
+        let message = self.message(name, params);
+        self.viewport.write_prompt(&message)?;
+        self.promises.push(Box::new(promise.clone()));
+        Ok(promise)
     }
 
     fn previous_message(&mut self) -> io::Result<()> {
@@ -177,7 +291,8 @@ impl<'a> Game<'a> {
         }
         self.message_idx -= 1;
         let message = &self.messages[self.message_idx];
-        self.viewport.write_message(message)
+        self.viewport.write_message(message)?;
+        Ok(())
     }
 
     fn creature(&self, creature_id: EntityID) -> Option<&Creature> {
@@ -236,60 +351,116 @@ impl<'a> Game<'a> {
         }
     }
 
+    fn flush_promises(&mut self) {
+        unsafe {
+            let game = self as *mut Self;
+            (*game).promises.give_all(&mut *game);
+        }
+    }
+
     /// Run the game
     pub fn run(mut self) -> io::Result<()> {
         info!("Running game");
         self.viewport.init()?;
         self.draw_entities()?;
-        self.say("global.welcome", &template_params!())?;
-        self.flush()?;
+        self.flush().unwrap();
+
+        self.prompt("character.name_prompt", &template_params!())?
+            .on_fulfill(|game, char_name| {
+                game.say(
+                    "global.welcome",
+                    &template_params!({
+                        "character" => {
+                            "name" => char_name,
+                        },
+                    }),
+                )
+                .unwrap();
+                game.flush().unwrap();
+                game.mut_character().set_name(char_name.to_string());
+            });
+
         loop {
             let mut old_position = None;
-            use Command::*;
-            match Command::from_key(self.keys.next().unwrap().unwrap()) {
-                Some(Quit) => {
-                    info!("Quitting game due to user request");
-                    break;
-                }
+            let next_key = self.keys.next().unwrap().unwrap();
+            match &mut self.input_state {
+                InputState::Initial => {
+                    use Command::*;
+                    match Command::from_key(next_key) {
+                        Some(Quit) => {
+                            info!("Quitting game due to user request");
+                            break;
+                        }
 
-                Some(Move(direction)) => {
-                    use Collision::*;
-                    let new_pos = self.character().position + direction;
-                    match self.collision_at(new_pos) {
-                        None => {
-                            old_position = Some(self.character().position);
-                            self.entities.update_position(
-                                self.character_entity_id,
-                                new_pos,
-                            );
+                        Some(Move(direction)) => {
+                            use Collision::*;
+                            let new_pos = self.character().position + direction;
+                            match self.collision_at(new_pos) {
+                                None => {
+                                    old_position =
+                                        Some(self.character().position);
+                                    self.entities.update_position(
+                                        self.character_entity_id,
+                                        new_pos,
+                                    );
+                                }
+                                Some(Combat) => {
+                                    self.attack_at(new_pos)?;
+                                }
+                                Some(Stop) => (),
+                            }
                         }
-                        Some(Combat) => {
-                            self.attack_at(new_pos)?;
+
+                        Some(PreviousMessage) => self.previous_message()?,
+
+                        None => (),
+                    }
+
+                    match old_position {
+                        Some(old_pos) => {
+                            self.tick(
+                                self.character().speed().tiles_to_ticks(
+                                    (old_pos - self.character().position)
+                                        .as_tiles(),
+                                ),
+                            );
+                            self.viewport.clear(old_pos)?;
+                            self.viewport.game_cursor_position =
+                                self.character().position;
+                            self.viewport.draw(
+                                // TODO this clone feels unnecessary.
+                                &self.character().clone(),
+                            )?;
                         }
-                        Some(Stop) => (),
+                        None => (),
                     }
                 }
 
-                Some(PreviousMessage) => self.previous_message()?,
-
-                None => (),
-            }
-
-            match old_position {
-                Some(old_pos) => {
-                    self.tick(self.character().speed().tiles_to_ticks(
-                        (old_pos - self.character().position).as_tiles(),
-                    ));
-                    self.viewport.clear(old_pos)?;
-                    self.viewport.cursor_position = self.character().position;
-                    self.viewport.draw(
-                        // TODO this clone feels unnecessary.
-                        &self.character().clone(),
-                    )?;
+                InputState::Prompt { complete, buffer } => {
+                    use termion::event::Key::*;
+                    match next_key {
+                        Char('\n') => {
+                            info!("Prompt complete: \"{}\"", buffer);
+                            self.viewport.clear_prompt()?;
+                            complete.fulfill(buffer.clone());
+                            self.input_state = InputState::Initial;
+                        }
+                        Char(chr) => {
+                            buffer.push(chr);
+                            self.viewport.push_prompt_chr(chr)?;
+                        }
+                        Esc => complete.cancel(),
+                        Backspace => {
+                            buffer.pop();
+                            self.viewport.pop_prompt_chr()?;
+                        }
+                        _ => {}
+                    }
                 }
-                None => (),
             }
+
             self.flush()?;
+            self.flush_promises();
             debug!("{:?}", self.character());
         }
         Ok(())