Skip to main content

eldiron_shared/
text_session.rs

1use 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}