veloren_common/states/
interact.rs

1use super::utils::*;
2use crate::{
3    comp::{
4        CharacterState, InventoryManip, StateUpdate, character_state::OutputEvents,
5        controller::InputKind, item::ItemDefinitionIdOwned, slot::InvSlotId,
6    },
7    consts::MAX_INTERACT_RANGE,
8    event::{HelpDownedEvent, InventoryManipEvent, LocalEvent, ToggleSpriteLightEvent},
9    outcome::Outcome,
10    states::behavior::{CharacterBehavior, JoinData},
11    terrain::SpriteKind,
12    uid::Uid,
13    util::Dir,
14};
15use serde::{Deserialize, Serialize};
16use std::time::Duration;
17use vek::Vec3;
18
19/// Separated out to condense update portions of character state
20#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
21pub struct StaticData {
22    /// Buildup to sprite interaction
23    pub buildup_duration: Duration,
24    /// Duration of sprite interaction, `None` means indefinite until cancelled
25    pub use_duration: Option<Duration>,
26    /// Recovery after sprite interaction
27    pub recover_duration: Duration,
28    /// The kind of interaction.
29    pub interact: InteractKind,
30    /// Had weapon wielded
31    pub was_wielded: bool,
32    /// Was sneaking
33    pub was_sneak: bool,
34    /// The item required to interact with the sprite, if one was required
35    ///
36    /// The second field is the slot that the required item was in when this
37    /// state was created. If it isn't in this slot anymore the interaction will
38    /// fail.
39    ///
40    /// If third field is true, item should be consumed on collection
41    pub required_item: Option<(ItemDefinitionIdOwned, InvSlotId, bool)>,
42}
43
44#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
45pub struct Data {
46    /// Struct containing data that does not change over the course of the
47    /// character state
48    pub static_data: StaticData,
49    /// Timer for each stage
50    pub timer: Duration,
51    /// What section the character stage is in
52    pub stage_section: StageSection,
53}
54
55impl CharacterBehavior for Data {
56    fn behavior(&self, data: &JoinData, output_events: &mut OutputEvents) -> StateUpdate {
57        let mut update = StateUpdate::from(data);
58
59        'logic: {
60            let interact_pos = match &self.static_data.interact {
61                InteractKind::Invalid => {
62                    end_ability(data, &mut update);
63                    break 'logic;
64                },
65                InteractKind::Entity { target, .. } => {
66                    if let Some(pos) = data
67                        .id_maps
68                        .uid_entity(*target)
69                        .and_then(|target| data.prev_phys_caches.get(target))
70                        .and_then(|prev| prev.pos)
71                    {
72                        pos.0
73                    } else {
74                        // Not a valid target. We end the state.
75                        end_ability(data, &mut update);
76                        break 'logic;
77                    }
78                },
79                InteractKind::Sprite { pos, .. } => pos.as_() + 0.5,
80            };
81
82            if interact_pos.distance_squared(data.pos.0) > MAX_INTERACT_RANGE.powi(2) {
83                end_ability(data, &mut update);
84                break 'logic;
85            }
86
87            let ori_dir = Dir::from_unnormalized(Vec3::from((interact_pos - data.pos.0).xy()));
88            handle_orientation(data, &mut update, 1.0, ori_dir);
89            handle_move(
90                data,
91                &mut update,
92                self.static_data.interact.movement().unwrap_or(0.0),
93            );
94
95            match self.stage_section {
96                StageSection::Buildup => {
97                    if self.timer < self.static_data.buildup_duration {
98                        // Build up
99                        if let CharacterState::Interact(c) = &mut update.character {
100                            c.timer = tick_attack_or_default(data, self.timer, None);
101                        }
102                    } else {
103                        // Transitions to use section of stage
104                        if let CharacterState::Interact(c) = &mut update.character {
105                            c.timer = Duration::default();
106                            c.stage_section = StageSection::Action;
107                        }
108                    }
109                },
110                StageSection::Action => {
111                    if self
112                        .static_data
113                        .use_duration
114                        .is_none_or(|use_duration| self.timer < use_duration)
115                    {
116                        // sprite interaction
117                        if let CharacterState::Interact(c) = &mut update.character {
118                            c.timer = tick_attack_or_default(data, self.timer, None);
119                        }
120                    } else {
121                        // Transitions to recover section of stage
122                        if let CharacterState::Interact(c) = &mut update.character {
123                            c.timer = Duration::default();
124                            c.stage_section = StageSection::Recover;
125                        }
126                    }
127                },
128                StageSection::Recover => {
129                    if self.timer < self.static_data.recover_duration {
130                        // Recovery
131                        if let CharacterState::Interact(c) = &mut update.character {
132                            c.timer = tick_attack_or_default(
133                                data,
134                                self.timer,
135                                Some(data.stats.recovery_speed_modifier),
136                            );
137                        }
138                    } else {
139                        // Create inventory manipulation event
140                        let (has_required_item, inv_slot) = self
141                            .static_data
142                            .required_item
143                            .as_ref()
144                            .map_or((true, None), |&(ref item_def_id, slot, consume)| {
145                                // Check that required item is still in expected slot
146                                let has_item = data
147                                    .inventory
148                                    .and_then(|inv| inv.get(slot))
149                                    .is_some_and(|item| item.item_definition_id() == *item_def_id);
150
151                                (has_item, has_item.then_some((slot, consume)))
152                            });
153                        if has_required_item {
154                            match self.static_data.interact {
155                                // If the innteract kind is invalid we break out of this block
156                                // above.
157                                InteractKind::Invalid => unreachable!(),
158                                InteractKind::Entity { target, kind, .. } => match kind {
159                                    crate::interaction::InteractionKind::HelpDowned => {
160                                        output_events.emit_server(HelpDownedEvent {
161                                            target,
162                                            helper: Some(*data.uid),
163                                        });
164                                    },
165                                    crate::interaction::InteractionKind::Pet => {},
166                                },
167                                InteractKind::Sprite { pos, kind } => {
168                                    let inv_manip = InventoryManip::Collect {
169                                        sprite_pos: pos,
170                                        required_item: inv_slot,
171                                    };
172                                    match kind {
173                                        SpriteInteractKind::ToggleLight(enable) => output_events
174                                            .emit_server(ToggleSpriteLightEvent {
175                                                entity: data.entity,
176                                                pos,
177                                                enable,
178                                            }),
179                                        _ => output_events.emit_server(InventoryManipEvent(
180                                            data.entity,
181                                            inv_manip,
182                                        )),
183                                    }
184
185                                    if matches!(kind, SpriteInteractKind::Unlock) {
186                                        output_events.emit_local(LocalEvent::CreateOutcome(
187                                            Outcome::SpriteUnlocked { pos },
188                                        ));
189                                    }
190                                },
191                            }
192                        }
193                        // Done
194                        end_ability(data, &mut update);
195                    }
196                },
197                _ => {
198                    // If it somehow ends up in an incorrect stage section
199                    end_ability(data, &mut update);
200                },
201            }
202        }
203
204        // Allow attacks and abilities to interrupt
205        handle_wield(data, &mut update);
206
207        // At end of state logic so an interrupt isn't overwritten
208        if input_is_pressed(data, InputKind::Roll) {
209            handle_input(data, output_events, &mut update, InputKind::Roll);
210        }
211
212        if handle_jump(data, output_events, &mut update, 1.0) {
213            end_ability(data, &mut update);
214        }
215
216        update
217    }
218}
219
220#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
221pub enum InteractKind {
222    Invalid,
223    Entity {
224        target: Uid,
225        kind: crate::interaction::InteractionKind,
226    },
227    Sprite {
228        // TODO: This could be `VolumePos` in the future.
229        pos: Vec3<i32>,
230        kind: SpriteInteractKind,
231    },
232}
233
234impl InteractKind {
235    pub fn movement(&self) -> Option<f32> {
236        match self {
237            Self::Invalid | Self::Sprite { .. } => None,
238            Self::Entity { kind, .. } => kind.movement(),
239        }
240    }
241}
242
243impl crate::interaction::InteractionKind {
244    pub fn movement(&self) -> Option<f32> {
245        match self {
246            Self::HelpDowned => Some(0.1),
247            Self::Pet => Some(0.7),
248        }
249    }
250
251    pub fn durations(&self) -> (Duration, Option<Duration>, Duration) {
252        match self {
253            Self::HelpDowned => (
254                Duration::from_secs_f32(0.5),
255                Some(Duration::from_secs_f32(4.0)),
256                Duration::from_secs_f32(0.5),
257            ),
258            Self::Pet => (
259                Duration::from_secs_f32(0.0),
260                None,
261                Duration::from_secs_f32(0.0),
262            ),
263        }
264    }
265}
266
267/// Used to control effects based off of the type of sprite interacted with
268#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
269pub enum SpriteInteractKind {
270    Chest,
271    Harvestable,
272    Collectible,
273    Unlock,
274    Fallback,
275    ToggleLight(bool),
276}
277
278impl From<SpriteKind> for Option<SpriteInteractKind> {
279    fn from(sprite_kind: SpriteKind) -> Self {
280        match sprite_kind {
281            SpriteKind::Apple
282            | SpriteKind::Mushroom
283            | SpriteKind::RedFlower
284            | SpriteKind::Sunflower
285            | SpriteKind::Coconut
286            | SpriteKind::Beehive
287            | SpriteKind::Cotton
288            | SpriteKind::Moonbell
289            | SpriteKind::Pyrebloom
290            | SpriteKind::WildFlax
291            | SpriteKind::RoundCactus
292            | SpriteKind::ShortFlatCactus
293            | SpriteKind::MedFlatCactus
294            | SpriteKind::Wood
295            | SpriteKind::Bamboo
296            | SpriteKind::Hardwood
297            | SpriteKind::Ironwood
298            | SpriteKind::Frostwood
299            | SpriteKind::Eldwood => Some(SpriteInteractKind::Harvestable),
300            SpriteKind::Stones
301            | SpriteKind::Twigs
302            | SpriteKind::VialEmpty
303            | SpriteKind::Bowl
304            | SpriteKind::PotionMinor
305            | SpriteKind::Seashells
306            | SpriteKind::Bomb => Some(SpriteInteractKind::Collectible),
307            SpriteKind::Keyhole
308            | SpriteKind::BoneKeyhole
309            | SpriteKind::HaniwaKeyhole
310            | SpriteKind::SahaginKeyhole
311            | SpriteKind::VampireKeyhole
312            | SpriteKind::GlassKeyhole
313            | SpriteKind::KeyholeBars
314            | SpriteKind::TerracottaKeyhole
315            | SpriteKind::MyrmidonKeyhole
316            | SpriteKind::MinotaurKeyhole => Some(SpriteInteractKind::Unlock),
317            // Collectible checked in addition to container for case that sprite requires a tool to
318            // collect and cannot be collected by hand, yet still meets the container check
319            _ if sprite_kind.is_container() && sprite_kind.is_collectible() => {
320                Some(SpriteInteractKind::Chest)
321            },
322            _ if sprite_kind.is_collectible() => Some(SpriteInteractKind::Fallback),
323            _ => None,
324        }
325    }
326}
327
328impl SpriteInteractKind {
329    /// Returns (buildup, use, recover)
330    pub fn durations(&self) -> (Duration, Duration, Duration) {
331        match self {
332            Self::Chest => (
333                Duration::from_secs_f32(0.5),
334                Duration::from_secs_f32(2.0),
335                Duration::from_secs_f32(0.5),
336            ),
337            Self::Collectible => (
338                Duration::from_secs_f32(0.1),
339                Duration::from_secs_f32(0.2),
340                Duration::from_secs_f32(0.1),
341            ),
342            Self::Harvestable => (
343                Duration::from_secs_f32(0.3),
344                Duration::from_secs_f32(0.3),
345                Duration::from_secs_f32(0.2),
346            ),
347            Self::Fallback => (
348                Duration::from_secs_f32(5.0),
349                Duration::from_secs_f32(5.0),
350                Duration::from_secs_f32(5.0),
351            ),
352            Self::Unlock => (
353                Duration::from_secs_f32(0.8),
354                Duration::from_secs_f32(1.0),
355                Duration::from_secs_f32(0.3),
356            ),
357            Self::ToggleLight(_) => (
358                Duration::from_secs_f32(0.1),
359                Duration::from_secs_f32(0.2),
360                Duration::from_secs_f32(0.1),
361            ),
362        }
363    }
364}