eldiron_shared/
text_session.rs1use crate::text_game as sg;
2use rusterix::Map;
3use std::collections::BTreeSet;
4
5pub type TextGameMessage = (Option<u32>, Option<u32>, u32, String, String);
6pub type TextGameSay = (Option<u32>, Option<u32>, String, String);
7
8#[derive(Clone, Debug, PartialEq, Eq)]
9pub enum TextSessionOutput {
10 RenderRoom,
11 Plain(String),
12 Message { text: String, category: String },
13}
14
15#[derive(Default, Clone)]
16pub struct TextSession {
17 initialized: bool,
18 last_sector_id: Option<u32>,
19 last_nearby_attackers: BTreeSet<String>,
20 last_room_items: BTreeSet<String>,
21 suppress_next_sector_render: Option<u32>,
22 suppress_next_description_for_sector: Option<u32>,
23 last_announced_hour: Option<u8>,
24 auto_attack_target: Option<u32>,
25}
26
27impl TextSession {
28 pub fn new() -> Self {
29 Self::default()
30 }
31
32 pub fn reset(&mut self) {
33 *self = Self::default();
34 }
35
36 pub fn auto_attack_target(&self) -> Option<u32> {
37 self.auto_attack_target
38 }
39
40 pub fn clear_auto_attack_target(&mut self) {
41 self.auto_attack_target = None;
42 }
43
44 pub fn set_current_hour(&mut self, hour: Option<u8>) {
45 self.last_announced_hour = hour;
46 }
47
48 pub fn startup(
49 &mut self,
50 map: &Map,
51 authoring: &str,
52 current_hour: Option<u8>,
53 ) -> Vec<TextSessionOutput> {
54 self.initialized = true;
55 self.last_sector_id = sg::current_player_and_sector(map).map(|(_, sector)| sector.id);
56 self.last_nearby_attackers = current_nearby_attackers(map, authoring);
57 self.last_room_items = current_room_items(map, authoring);
58 self.last_announced_hour = current_hour;
59
60 match sg::authoring_startup_display(authoring) {
61 sg::StartupDisplay::Description => sg::render_current_sector_description(map)
62 .map(TextSessionOutput::Plain)
63 .into_iter()
64 .collect(),
65 sg::StartupDisplay::Room => vec![TextSessionOutput::RenderRoom],
66 sg::StartupDisplay::None => Vec::new(),
67 }
68 }
69
70 pub fn after_movement(&mut self, map: &Map, authoring: &str) -> Vec<TextSessionOutput> {
71 self.last_sector_id = sg::current_player_and_sector(map).map(|(_, sector)| sector.id);
72 self.suppress_next_sector_render = self.last_sector_id;
73 self.suppress_next_description_for_sector = self.last_sector_id;
74 self.last_nearby_attackers = current_nearby_attackers(map, authoring);
75 self.last_room_items = current_room_items(map, authoring);
76 vec![TextSessionOutput::RenderRoom]
77 }
78
79 pub fn collect(
80 &mut self,
81 map: &Map,
82 authoring: &str,
83 mut messages: Vec<TextGameMessage>,
84 mut says: Vec<TextGameSay>,
85 current_hour: Option<u8>,
86 current_hour_label: Option<String>,
87 auto_attack_on_attack: bool,
88 ) -> Vec<TextSessionOutput> {
89 let mut output = Vec::new();
90 let mut rendered_room_this_update = false;
91 let current_sector_id = sg::current_player_and_sector(map).map(|(_, sector)| sector.id);
92
93 if !self.initialized {
94 output.extend(self.startup(map, authoring, current_hour));
95 rendered_room_this_update = output
96 .iter()
97 .any(|entry| matches!(entry, TextSessionOutput::RenderRoom));
98 } else if let Some(current_sector_id) = current_sector_id
99 && Some(current_sector_id) != self.last_sector_id
100 {
101 self.last_sector_id = Some(current_sector_id);
102 if self.suppress_next_sector_render == Some(current_sector_id) {
103 self.suppress_next_sector_render = None;
104 } else {
105 output.push(TextSessionOutput::RenderRoom);
106 rendered_room_this_update = true;
107 }
108 self.last_nearby_attackers = current_nearby_attackers(map, authoring);
109 }
110
111 let player_id = sg::current_player_and_sector(map).map(|(player, _)| player.id);
112 let current_description = sg::render_current_sector_description(map);
113 let mut saw_death = false;
114
115 for (sender_entity, _sender_item, receiver_id, message, category) in messages.drain(..) {
116 if saw_death {
117 break;
118 }
119 if let Some(player_id) = player_id
120 && receiver_id != player_id
121 {
122 continue;
123 }
124 if auto_attack_on_attack
125 && is_under_attack_message(&message)
126 && let (Some(sender_id), Some(player_id)) = (sender_entity, player_id)
127 && sender_id != player_id
128 {
129 self.auto_attack_target = Some(sender_id);
130 }
131 if current_sector_id.is_some()
132 && self.suppress_next_description_for_sector == current_sector_id
133 && current_description
134 .as_deref()
135 .map(|text| text.trim() == message.trim())
136 .unwrap_or(false)
137 {
138 self.suppress_next_description_for_sector = None;
139 continue;
140 }
141 if !should_print_text_message(&message, &category) {
142 continue;
143 }
144 if current_description
145 .as_deref()
146 .map(|text| text.trim() == message.trim())
147 .unwrap_or(false)
148 {
149 if rendered_room_this_update {
150 continue;
151 }
152 output.push(TextSessionOutput::RenderRoom);
153 rendered_room_this_update = true;
154 continue;
155 }
156
157 output.push(TextSessionOutput::Message {
158 text: message.clone(),
159 category: category.clone(),
160 });
161 if message.trim() == "You died. Try again!" {
162 saw_death = true;
163 }
164 }
165
166 for (_sender_entity, _sender_item, message, _category) in says.drain(..) {
167 if saw_death {
168 break;
169 }
170 output.push(TextSessionOutput::Plain(message));
171 }
172
173 if !saw_death && let (Some(hour), Some(label)) = (current_hour, current_hour_label) {
174 let previous_hour = self.last_announced_hour.replace(hour);
175 if previous_hour != Some(hour) {
176 output.push(TextSessionOutput::Plain(format!("It is {}.", label)));
177 }
178 } else if let Some(hour) = current_hour {
179 self.last_announced_hour = Some(hour);
180 }
181
182 let current_nearby_attackers = current_nearby_attackers(map, authoring);
183 let current_room_items = current_room_items(map, authoring);
184 if !rendered_room_this_update && !saw_death {
185 let new_attackers: Vec<String> = current_nearby_attackers
186 .difference(&self.last_nearby_attackers)
187 .cloned()
188 .collect();
189 if !new_attackers.is_empty() {
190 output.push(TextSessionOutput::Plain(
191 sg::render_nearby_attacker_appearance_sentence(&new_attackers),
192 ));
193 }
194
195 let new_items: Vec<String> = current_room_items
196 .difference(&self.last_room_items)
197 .cloned()
198 .collect();
199 let already_announced_drop = output.iter().any(|entry| match entry {
200 TextSessionOutput::Plain(text) => text.contains("falls to the floor"),
201 TextSessionOutput::Message { text, .. } => text.contains("falls to the floor"),
202 TextSessionOutput::RenderRoom => false,
203 });
204 if !already_announced_drop && !new_items.is_empty() {
205 output.push(TextSessionOutput::Plain(if new_items.len() == 1 {
206 "Something falls to the floor.".to_string()
207 } else {
208 "Several things fall to the floor.".to_string()
209 }));
210 }
211 }
212
213 if saw_death {
214 self.auto_attack_target = None;
215 self.last_nearby_attackers.clear();
216 self.last_room_items.clear();
217 self.suppress_next_description_for_sector = None;
218 } else {
219 self.last_nearby_attackers = current_nearby_attackers;
220 self.last_room_items = current_room_items;
221 }
222
223 output
224 }
225}
226
227fn current_nearby_attackers(map: &Map, authoring: &str) -> BTreeSet<String> {
228 sg::build_text_room(map, authoring)
229 .map(|room| room.nearby_attackers.into_iter().collect())
230 .unwrap_or_default()
231}
232
233fn current_room_items(map: &Map, authoring: &str) -> BTreeSet<String> {
234 sg::build_text_room(map, authoring)
235 .map(|room| room.items.into_iter().collect())
236 .unwrap_or_default()
237}
238
239fn should_print_text_message(message: &str, category: &str) -> bool {
240 !(category == "warning" && message.trim() == "{system.cant_do_that_yet}")
241}
242
243fn is_under_attack_message(message: &str) -> bool {
244 message.trim_start().starts_with("You are under attack by ")
245}