Skip to main content

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(data, self.timer, None);
133                        }
134                    } else {
135                        // Create inventory manipulation event
136                        let (has_required_item, inv_slot) = self
137                            .static_data
138                            .required_item
139                            .as_ref()
140                            .map_or((true, None), |&(ref item_def_id, slot, consume)| {
141                                // Check that required item is still in expected slot
142                                let has_item = data
143                                    .inventory
144                                    .and_then(|inv| inv.get(slot))
145                                    .is_some_and(|item| item.item_definition_id() == *item_def_id);
146
147                                (has_item, has_item.then_some((slot, consume)))
148                            });
149                        if has_required_item {
150                            match self.static_data.interact {
151                                // If the innteract kind is invalid we break out of this block
152                                // above.
153                                InteractKind::Invalid => unreachable!(),
154                                InteractKind::Entity { target, kind, .. } => match kind {
155                                    crate::interaction::InteractionKind::HelpDowned => {
156                                        output_events.emit_server(HelpDownedEvent {
157                                            target,
158                                            helper: Some(*data.uid),
159                                        });
160                                    },
161                                    crate::interaction::InteractionKind::Pet => {},
162                                },
163                                InteractKind::Sprite { pos, kind } => {
164                                    let inv_manip = InventoryManip::Collect {
165                                        sprite_pos: pos,
166                                        required_item: inv_slot,
167                                    };
168                                    match kind {
169                                        SpriteInteractKind::ToggleLight(enable) => output_events
170                                            .emit_server(ToggleSpriteLightEvent {
171                                                entity: data.entity,
172                                                pos,
173                                                enable,
174                                            }),
175                                        _ => output_events.emit_server(InventoryManipEvent(
176                                            data.entity,
177                                            inv_manip,
178                                        )),
179                                    }
180
181                                    if matches!(kind, SpriteInteractKind::Unlock) {
182                                        output_events.emit_local(LocalEvent::CreateOutcome(
183                                            Outcome::SpriteUnlocked { pos },
184                                        ));
185                                    }
186                                },
187                            }
188                        }
189                        // Done
190                        end_ability(data, &mut update);
191                    }
192                },
193                _ => {
194                    // If it somehow ends up in an incorrect stage section
195                    end_ability(data, &mut update);
196                },
197            }
198        }
199
200        // Allow attacks and abilities to interrupt
201        handle_wield(data, &mut update);
202
203        // At end of state logic so an interrupt isn't overwritten
204        if input_is_pressed(data, InputKind::Roll) {
205            handle_input(data, output_events, &mut update, InputKind::Roll);
206        }
207
208        if handle_jump(data, output_events, &mut update, 1.0) {
209            end_ability(data, &mut update);
210        }
211
212        update
213    }
214}
215
216#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
217pub enum InteractKind {
218    Invalid,
219    Entity {
220        target: Uid,
221        kind: crate::interaction::InteractionKind,
222    },
223    Sprite {
224        // TODO: This could be `VolumePos` in the future.
225        pos: Vec3<i32>,
226        kind: SpriteInteractKind,
227    },
228}
229
230impl InteractKind {
231    pub fn movement(&self) -> Option<f32> {
232        match self {
233            Self::Invalid | Self::Sprite { .. } => None,
234            Self::Entity { kind, .. } => kind.movement(),
235        }
236    }
237}
238
239impl crate::interaction::InteractionKind {
240    pub fn movement(&self) -> Option<f32> {
241        match self {
242            Self::HelpDowned => Some(0.1),
243            Self::Pet => Some(0.7),
244        }
245    }
246
247    pub fn durations(&self) -> (Duration, Option<Duration>, Duration) {
248        match self {
249            Self::HelpDowned => (
250                Duration::from_secs_f32(0.5),
251                Some(Duration::from_secs_f32(4.0)),
252                Duration::from_secs_f32(0.5),
253            ),
254            Self::Pet => (
255                Duration::from_secs_f32(0.0),
256                None,
257                Duration::from_secs_f32(0.0),
258            ),
259        }
260    }
261}
262
263/// Used to control effects based off of the type of sprite interacted with
264#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
265pub enum SpriteInteractKind {
266    Chest,
267    Harvestable,
268    Collectible,
269    Unlock,
270    Fallback,
271    ToggleLight(bool),
272}
273
274impl From<SpriteKind> for Option<SpriteInteractKind> {
275    fn from(sprite_kind: SpriteKind) -> Self {
276        match sprite_kind {
277            SpriteKind::Apple
278            | SpriteKind::Mushroom
279            | SpriteKind::RedFlower
280            | SpriteKind::Sunflower
281            | SpriteKind::Coconut
282            | SpriteKind::Beehive
283            | SpriteKind::Cotton
284            | SpriteKind::Moonbell
285            | SpriteKind::Pyrebloom
286            | SpriteKind::WildFlax
287            | SpriteKind::RoundCactus
288            | SpriteKind::ShortFlatCactus
289            | SpriteKind::MedFlatCactus
290            | SpriteKind::Wood
291            | SpriteKind::Bamboo
292            | SpriteKind::Hardwood
293            | SpriteKind::Ironwood
294            | SpriteKind::Frostwood
295            | SpriteKind::Eldwood
296            | SpriteKind::Lettuce => Some(SpriteInteractKind::Harvestable),
297            SpriteKind::Stones
298            | SpriteKind::Twigs
299            | SpriteKind::VialEmpty
300            | SpriteKind::Bowl
301            | SpriteKind::PotionMinor
302            | SpriteKind::Seashells
303            | SpriteKind::Bomb => Some(SpriteInteractKind::Collectible),
304            SpriteKind::Keyhole
305            | SpriteKind::BoneKeyhole
306            | SpriteKind::HaniwaKeyhole
307            | SpriteKind::SahaginKeyhole
308            | SpriteKind::VampireKeyhole
309            | SpriteKind::GlassKeyhole
310            | SpriteKind::KeyholeBars
311            | SpriteKind::TerracottaKeyhole
312            | SpriteKind::MyrmidonKeyhole
313            | SpriteKind::MinotaurKeyhole => Some(SpriteInteractKind::Unlock),
314            // Collectible checked in addition to container for case that sprite requires a tool to
315            // collect and cannot be collected by hand, yet still meets the container check
316            _ if sprite_kind.is_defined_as_container()
317                && sprite_kind.collectible_info() == Some(None) =>
318            {
319                Some(SpriteInteractKind::Chest)
320            },
321            _ if sprite_kind.collectible_info() == Some(None) => Some(SpriteInteractKind::Fallback),
322            _ => None,
323        }
324    }
325}
326
327impl SpriteInteractKind {
328    /// Returns (buildup, use, recover)
329    pub fn durations(&self) -> (Duration, Duration, Duration) {
330        match self {
331            Self::Chest => (
332                Duration::from_secs_f32(0.5),
333                Duration::from_secs_f32(2.0),
334                Duration::from_secs_f32(0.5),
335            ),
336            Self::Collectible => (
337                Duration::from_secs_f32(0.1),
338                Duration::from_secs_f32(0.2),
339                Duration::from_secs_f32(0.1),
340            ),
341            Self::Harvestable => (
342                Duration::from_secs_f32(0.3),
343                Duration::from_secs_f32(0.3),
344                Duration::from_secs_f32(0.2),
345            ),
346            Self::Fallback => (
347                Duration::from_secs_f32(5.0),
348                Duration::from_secs_f32(5.0),
349                Duration::from_secs_f32(5.0),
350            ),
351            Self::Unlock => (
352                Duration::from_secs_f32(0.8),
353                Duration::from_secs_f32(1.0),
354                Duration::from_secs_f32(0.3),
355            ),
356            Self::ToggleLight(_) => (
357                Duration::from_secs_f32(0.1),
358                Duration::from_secs_f32(0.2),
359                Duration::from_secs_f32(0.1),
360            ),
361        }
362    }
363}