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