1 use crate::color;
2 use crate::engine::{Display, TextMetrics};
3 use crate::formula;
4 use crate::player::CauseOfDeath;
5 use crate::point::Point;
6 use crate::rect::Rectangle;
7 use crate::state::{Side, State};
8 use crate::ui::{self, Button};
9 
10 pub enum Action {
11     NewGame,
12     Help,
13     Menu,
14 }
15 
16 struct Layout {
17     window_rect: Rectangle,
18     rect: Rectangle,
19     action_under_mouse: Option<Action>,
20     rect_under_mouse: Option<Rectangle>,
21     new_game_button: Button,
22     help_button: Button,
23     menu_button: Button,
24 }
25 
26 pub struct Window;
27 
28 impl Window {
layout(&self, state: &State, metrics: &dyn TextMetrics) -> Layout29     fn layout(&self, state: &State, metrics: &dyn TextMetrics) -> Layout {
30         let mut action_under_mouse = None;
31         let mut rect_under_mouse = None;
32 
33         let padding = Point::from_i32(1);
34         let size = Point::new(37, 17) + (padding * 2);
35         let top_left = Point {
36             x: (state.display_size.x - size.x) / 2,
37             y: 7,
38         };
39 
40         let window_rect = Rectangle::from_point_and_size(top_left, size);
41 
42         let rect = Rectangle::new(
43             window_rect.top_left() + padding,
44             window_rect.bottom_right() - padding,
45         );
46 
47         let new_game_button = Button::new(rect.bottom_left(), "[N]ew Game").align_left();
48 
49         let help_button = Button::new(rect.bottom_left(), "[?] Help").align_center(rect.width());
50 
51         let menu_button = Button::new(rect.bottom_right(), "[Esc] Main Menu").align_right();
52 
53         let text_rect = metrics.button_rect(&new_game_button);
54         if text_rect.contains(state.mouse.tile_pos) {
55             action_under_mouse = Some(Action::NewGame);
56             rect_under_mouse = Some(text_rect);
57         }
58 
59         let text_rect = metrics.button_rect(&help_button);
60         // NOTE(shadower): This is a fixup for the discrepancy between
61         // the text width in pixels and how it maps to the tile
62         // coordinates. It just looks better 1 tile wider.
63         let text_rect = Rectangle::new(text_rect.top_left(), text_rect.bottom_right() + (1, 0));
64         if text_rect.contains(state.mouse.tile_pos) {
65             action_under_mouse = Some(Action::Help);
66             rect_under_mouse = Some(text_rect);
67         }
68 
69         let text_rect = metrics.button_rect(&menu_button);
70         if text_rect.contains(state.mouse.tile_pos) {
71             action_under_mouse = Some(Action::Menu);
72             rect_under_mouse = Some(text_rect);
73         }
74 
75         Layout {
76             window_rect,
77             rect,
78             action_under_mouse,
79             rect_under_mouse,
80             new_game_button,
81             help_button,
82             menu_button,
83         }
84     }
85 
render(&self, state: &State, metrics: &dyn TextMetrics, display: &mut Display)86     pub fn render(&self, state: &State, metrics: &dyn TextMetrics, display: &mut Display) {
87         use self::CauseOfDeath::*;
88         use crate::ui::Text::*;
89 
90         let layout = self.layout(state, metrics);
91 
92         let cause_of_death = formula::cause_of_death(&state.player);
93 
94         let endgame_reason_text = if state.side == Side::Victory {
95             if !state.player.alive() {
96                 log::warn!("The player appears to be dead on victory screen.");
97             }
98             if cause_of_death.is_some() {
99                 log::warn!("The player has active cause of dead on victory screen.");
100             }
101             "You won!"
102         } else {
103             "You lost:"
104         };
105 
106         let perpetrator = state.player.perpetrator.as_ref();
107 
108         let endgame_description = match (cause_of_death, perpetrator) {
109             (Some(Exhausted), None) => "Exhausted".into(),
110             (Some(Exhausted), Some(monster)) => format!(
111                 "Exhausted because of {} ({})",
112                 monster.name(),
113                 monster.glyph()
114             ),
115             (Some(Overdosed), _) => "Overdosed".into(),
116             (Some(LostWill), Some(monster)) => format!(
117                 "Lost all Will due to {} ({})",
118                 monster.name(),
119                 monster.glyph()
120             ),
121             (Some(LostWill), None) => unreachable!(),
122             (Some(Killed), Some(monster)) => {
123                 format!("Defeated by {} ({})", monster.name(), monster.glyph())
124             }
125             (Some(Killed), None) => unreachable!(),
126             (None, _) => "".into(), // Victory
127         };
128 
129         let doses_in_inventory = state
130             .player
131             .inventory
132             .iter()
133             .filter(|item| item.is_dose())
134             .count();
135 
136         let turns_text = format!("Turns: {}", state.turn);
137         let carrying_doses_text = if state.player_picked_up_a_dose {
138             format!("Carrying {} doses", doses_in_inventory)
139         } else {
140             "You've never managed to save a dose for a later fix.".to_string()
141         };
142         let high_streak_text = format!(
143             "Longest High streak: {} turns",
144             state.player.longest_high_streak
145         );
146         let tip_text = format!("Tip: {}", endgame_tip(state));
147 
148         let lines = vec![
149             Centered(endgame_reason_text),
150             Centered(&endgame_description),
151             EmptySpace(2),
152             Centered(&turns_text),
153             Empty,
154             Centered(&high_streak_text),
155             Empty,
156             Centered(&carrying_doses_text),
157             EmptySpace(2),
158             Paragraph(&tip_text),
159             EmptySpace(2),
160         ];
161 
162         display.draw_rectangle(layout.window_rect, color::window_background);
163 
164         ui::render_text_flow(&lines, layout.rect, metrics, display);
165 
166         if let Some(rect) = layout.rect_under_mouse {
167             display.draw_rectangle(rect, color::menu_highlight);
168         }
169 
170         display.draw_button(&layout.new_game_button);
171         display.draw_button(&layout.help_button);
172         display.draw_button(&layout.menu_button);
173     }
174 
hovered(&self, state: &State, metrics: &dyn TextMetrics) -> Option<Action>175     pub fn hovered(&self, state: &State, metrics: &dyn TextMetrics) -> Option<Action> {
176         self.layout(state, metrics).action_under_mouse
177     }
178 }
179 
endgame_tip(state: &State) -> String180 fn endgame_tip(state: &State) -> String {
181     use self::CauseOfDeath::*;
182     let throwavay_rng = &mut state.rng.clone();
183 
184     let overdosed_tips = &[
185         "Using another dose when High will likely cause overdose early on.",
186         "When you get too close to a dose, it will be impossible to resist.",
187         "The `+`, `x` and `I` doses are much stronger. Early on, you'll likely overdose on them.",
188     ];
189 
190     let food_tips = &["Eat food (by pressing [1]) or use a dose to stave off withdrawal."];
191 
192     let hunger_tips = &[
193         "Being hit by `h` will quickly get you into a withdrawal.",
194         "The `h` monsters can swarm you.",
195     ];
196 
197     let anxiety_tips = &["Being hit by `a` reduces your Will. You lose when it reaches zero."];
198 
199     let unsorted_tips = &[
200         "As you use doses, you slowly build up tolerance.",
201         "Even the doses of the same kind can have different strength. Their purity varies.",
202         "Directly confronting `a` will slowly increase your Will.",
203         "The other characters won't talk to you while you're High.",
204         "Bumping to another person sober will give you a bonus.",
205         "The `D` monsters move twice as fast as you. Be careful.",
206     ];
207 
208     let all_tips = overdosed_tips
209         .iter()
210         .chain(food_tips)
211         .chain(hunger_tips)
212         .chain(anxiety_tips)
213         .chain(unsorted_tips)
214         .collect::<Vec<_>>();
215 
216     let cause_of_death = formula::cause_of_death(&state.player);
217     let perpetrator = state.player.perpetrator.as_ref();
218     let selected_tip = match (cause_of_death, perpetrator) {
219         (Some(Overdosed), _) => *throwavay_rng.choose(overdosed_tips).unwrap(),
220         (Some(Exhausted), Some(_monster)) => *throwavay_rng.choose(hunger_tips).unwrap(),
221         (Some(Exhausted), None) => *throwavay_rng.choose(food_tips).unwrap(),
222         (Some(LostWill), Some(_monster)) => *throwavay_rng.choose(anxiety_tips).unwrap(),
223         _ => *throwavay_rng.choose(&all_tips).unwrap(),
224     };
225 
226     String::from(selected_tip)
227 }
228