overlord_event_system/logic/
auto_chest.rs

1use crate::{
2    event::OverlordEvent, game_config_helpers::GameConfigLookup, logic::handler::OverlordLogic,
3    state::OverlordState,
4};
5
6use essences::autochest::{AutoChestFilter, AutoChestFilterId};
7use event_system::{event::EventPluginized, system::EventHandleResult};
8
9impl OverlordLogic {
10    pub fn handle_enable_auto_chest(
11        &self,
12        mut state: OverlordState,
13    ) -> EventHandleResult<OverlordEvent, OverlordState> {
14        let game_config = self.game_config.get();
15
16        // Gate auto-chest on character LEVEL (not just chapter): a player must
17        // manually open chests to reach the level threshold before auto-open
18        // unlocks — rewards hands-on early play (playtester request).
19        let level_gate = game_config
20            .gatings
21            .autochest
22            .autochest_button_unlock_character_level;
23        if state.character_state.character.character_level < level_gate {
24            tracing::warn!(
25                "EnableAutoChest rejected: character_level {} < gate {}",
26                state.character_state.character.character_level,
27                level_gate
28            );
29            return EventHandleResult::fail(state);
30        }
31
32        let events = if !state.character_state.character.auto_chest_enabled {
33            state.character_state.character.auto_chest_enabled = true;
34
35            vec![EventPluginized::delayed(
36                OverlordEvent::AutoChestOpenItemCase {
37                    batch_size: state.auto_chest.batch_size,
38                },
39                game_config.game_settings.auto_chest_pause_duration_ticks,
40            )]
41        } else {
42            vec![]
43        };
44
45        EventHandleResult::ok_events(state, events)
46    }
47
48    pub fn handle_disable_auto_chest(
49        &self,
50        mut state: OverlordState,
51    ) -> EventHandleResult<OverlordEvent, OverlordState> {
52        state.character_state.character.auto_chest_enabled = false;
53
54        EventHandleResult::ok(state)
55    }
56
57    pub fn handle_enable_auto_chest_filter(
58        &self,
59        filter_id: AutoChestFilterId,
60        mut state: OverlordState,
61    ) -> EventHandleResult<OverlordEvent, OverlordState> {
62        let Some(filter) = state.auto_chest.filters.iter().find(|f| f.id == filter_id) else {
63            tracing::error!(
64                "Tried enabling auto_chest filter with id = {}, but didnt find it in state",
65                filter_id
66            );
67            return EventHandleResult::fail(state);
68        };
69
70        state.character_state.character.last_filter_used_id = Some(filter.id);
71
72        state.auto_chest.active_filter = Some(filter.clone());
73
74        EventHandleResult::ok(state)
75    }
76
77    pub fn handle_disable_auto_chest_filter(
78        &self,
79        mut state: OverlordState,
80    ) -> EventHandleResult<OverlordEvent, OverlordState> {
81        state.auto_chest.active_filter = None;
82
83        EventHandleResult::ok(state)
84    }
85
86    pub fn handle_enable_auto_chest_power_compare(
87        &self,
88        mut state: OverlordState,
89    ) -> EventHandleResult<OverlordEvent, OverlordState> {
90        state.auto_chest.power_compare_enabled = true;
91
92        EventHandleResult::ok(state)
93    }
94
95    pub fn handle_disable_auto_chest_power_compare(
96        &self,
97        mut state: OverlordState,
98    ) -> EventHandleResult<OverlordEvent, OverlordState> {
99        state.auto_chest.power_compare_enabled = false;
100
101        EventHandleResult::ok(state)
102    }
103
104    pub fn handle_update_auto_chest_batch_size(
105        &self,
106        batch_size: i64,
107        mut state: OverlordState,
108    ) -> EventHandleResult<OverlordEvent, OverlordState> {
109        let game_config = self.game_config.get();
110        let Some(item_case_settings) = game_config
111            .item_case_settings_by_level(state.character_state.character.item_case_level)
112        else {
113            tracing::error!(
114                "No item case settings for level = {}",
115                state.character_state.character.item_case_level
116            );
117            return EventHandleResult::fail(state);
118        };
119
120        if batch_size
121            > item_case_settings
122                .auto_chest_settings
123                .auto_only_max_batch_size
124        {
125            tracing::error!(
126                "Batch size = {}, is bigger than auto max = {}",
127                batch_size,
128                item_case_settings
129                    .auto_chest_settings
130                    .auto_only_max_batch_size,
131            );
132            return EventHandleResult::fail(state);
133        }
134        state.auto_chest.batch_size = batch_size;
135
136        EventHandleResult::ok(state)
137    }
138
139    pub fn handle_new_auto_chest_filter(
140        &self,
141        filter: AutoChestFilter,
142        mut state: OverlordState,
143    ) -> EventHandleResult<OverlordEvent, OverlordState> {
144        let game_config = self.game_config.get();
145
146        if let Err(e) = filter.validate(
147            state.character_state.character.current_chapter_level,
148            state.character_state.character.character_level,
149            &game_config.attributes,
150            &game_config.gatings.autochest,
151        ) {
152            tracing::error!("Filter {:?},\n Didn't pass validation: {:?}", filter, e);
153            return EventHandleResult::fail(state);
154        }
155
156        // Upsert: the client re-sends NewAutoChestFilter when its synced view
157        // lags behind the round trip (its exists-in-state check races the
158        // patch). Hard-failing here silently discarded the user's edit.
159        if let Some(existing) = state
160            .auto_chest
161            .filters
162            .iter_mut()
163            .find(|f| f.id == filter.id)
164        {
165            *existing = filter.clone();
166        } else {
167            state.auto_chest.filters.push(filter.clone());
168        }
169
170        Self::refresh_active_filter(&mut state, &filter);
171
172        EventHandleResult::ok(state)
173    }
174
175    /// The active filter is a denormalized clone of a `filters` entry — keep it
176    /// in step whenever that entry changes, or filtering silently runs with
177    /// stale settings until the next EnableAutoChestFilter.
178    fn refresh_active_filter(state: &mut OverlordState, updated: &AutoChestFilter) {
179        if let Some(active) = &mut state.auto_chest.active_filter
180            && active.id == updated.id
181        {
182            *active = updated.clone();
183        }
184    }
185
186    pub fn handle_update_auto_chest_filter(
187        &self,
188        updated_filter: AutoChestFilter,
189        mut state: OverlordState,
190    ) -> EventHandleResult<OverlordEvent, OverlordState> {
191        let game_config = self.game_config.get();
192
193        let Some(old_filter) = state
194            .auto_chest
195            .filters
196            .iter_mut()
197            .find(|f| f.id == updated_filter.id)
198        else {
199            tracing::error!(
200                "Tried updating auto_chest filter with id = {}, but didn't find it in state",
201                updated_filter.id
202            );
203            return EventHandleResult::fail(state);
204        };
205
206        if let Err(e) = updated_filter.validate(
207            state.character_state.character.current_chapter_level,
208            state.character_state.character.character_level,
209            &game_config.attributes,
210            &game_config.gatings.autochest,
211        ) {
212            tracing::error!(
213                "Filter {:?}, didn't pass validation = {:?}",
214                updated_filter,
215                e
216            );
217            return EventHandleResult::fail(state);
218        }
219
220        *old_filter = updated_filter.clone();
221
222        Self::refresh_active_filter(&mut state, &updated_filter);
223
224        EventHandleResult::ok(state)
225    }
226
227    pub fn handle_remove_auto_chest_filter(
228        &self,
229        filter_id: AutoChestFilterId,
230        mut state: OverlordState,
231    ) -> EventHandleResult<OverlordEvent, OverlordState> {
232        let Some(filter_position) = state
233            .auto_chest
234            .filters
235            .iter()
236            .position(|filter| filter.id == filter_id)
237        else {
238            tracing::error!(
239                "Tried removing filter with id = {}, but didn't find it in state",
240                filter_id
241            );
242            return EventHandleResult::fail(state);
243        };
244
245        if state.character_state.character.last_filter_used_id == Some(filter_id) {
246            state.character_state.character.last_filter_used_id = None;
247        }
248
249        // Removing the ACTIVE filter must also stop filtering — otherwise a
250        // ghost clone of the deleted preset keeps selling drops.
251        if state
252            .auto_chest
253            .active_filter
254            .as_ref()
255            .is_some_and(|f| f.id == filter_id)
256        {
257            state.auto_chest.active_filter = None;
258        }
259
260        state.auto_chest.filters.remove(filter_position);
261
262        EventHandleResult::ok(state)
263    }
264}