1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
/// EventMapper::Combat watches the combat states of surrounding entities' and
/// emits sfx related to weapons and attacks/abilities
use crate::{
    audio::sfx::{SfxEvent, SfxTriggerItem, SfxTriggers, SFX_DIST_LIMIT_SQR},
    scene::{Camera, Terrain},
    AudioFrontend,
};

use super::EventMapper;

use client::Client;
use common::{
    comp::{
        inventory::slot::EquipSlot, item::ItemKind, CharacterAbilityType, CharacterState,
        Inventory, Pos,
    },
    terrain::TerrainChunk,
};
use common_state::State;
use hashbrown::HashMap;
use specs::{Entity as EcsEntity, Join, LendJoin, WorldExt};
use std::time::{Duration, Instant};

#[derive(Clone)]
struct PreviousEntityState {
    event: SfxEvent,
    time: Instant,
    weapon_drawn: bool,
}

impl Default for PreviousEntityState {
    fn default() -> Self {
        Self {
            event: SfxEvent::Idle,
            time: Instant::now(),
            weapon_drawn: false,
        }
    }
}

pub struct CombatEventMapper {
    event_history: HashMap<EcsEntity, PreviousEntityState>,
}

impl EventMapper for CombatEventMapper {
    fn maintain(
        &mut self,
        audio: &mut AudioFrontend,
        state: &State,
        player_entity: specs::Entity,
        camera: &Camera,
        triggers: &SfxTriggers,
        _terrain: &Terrain<TerrainChunk>,
        _client: &Client,
    ) {
        let ecs = state.ecs();

        let cam_pos = camera.get_pos_with_focus();

        for (entity, pos, inventory, character) in (
            &ecs.entities(),
            &ecs.read_storage::<Pos>(),
            ecs.read_storage::<Inventory>().maybe(),
            ecs.read_storage::<CharacterState>().maybe(),
        )
            .join()
            .filter(|(_, e_pos, ..)| (e_pos.0.distance_squared(cam_pos)) < SFX_DIST_LIMIT_SQR)
        {
            if let Some(character) = character {
                let sfx_state = self.event_history.entry(entity).or_default();

                let mapped_event = inventory.map_or(SfxEvent::Idle, |inv| {
                    Self::map_event(character, sfx_state, inv)
                });

                // Check for SFX config entry for this movement
                if Self::should_emit(sfx_state, triggers.get_key_value(&mapped_event)) {
                    let sfx_trigger_item = triggers.get_key_value(&mapped_event);
                    audio.emit_sfx(sfx_trigger_item, pos.0, None);
                    sfx_state.time = Instant::now();
                }

                // update state to determine the next event. We only record the time (above) if
                // it was dispatched
                sfx_state.event = mapped_event;
                sfx_state.weapon_drawn = Self::weapon_drawn(character);
            }
        }

        self.cleanup(player_entity);
    }
}

impl CombatEventMapper {
    pub fn new() -> Self {
        Self {
            event_history: HashMap::new(),
        }
    }

    /// As the player explores the world, we track the last event of the nearby
    /// entities to determine the correct SFX item to play next based on
    /// their activity. `cleanup` will remove entities from event tracking if
    /// they have not triggered an event for > n seconds. This prevents
    /// stale records from bloating the Map size.
    fn cleanup(&mut self, player: EcsEntity) {
        const TRACKING_TIMEOUT: u64 = 10;

        let now = Instant::now();
        self.event_history.retain(|entity, event| {
            now.duration_since(event.time) < Duration::from_secs(TRACKING_TIMEOUT)
                || entity.id() == player.id()
        });
    }

    /// Ensures that:
    /// 1. An sfx.ron entry exists for an SFX event
    /// 2. The sfx has not been played since it's timeout threshold has elapsed,
    ///    which prevents firing every tick
    fn should_emit(
        previous_state: &PreviousEntityState,
        sfx_trigger_item: Option<(&SfxEvent, &SfxTriggerItem)>,
    ) -> bool {
        if let Some((event, item)) = sfx_trigger_item {
            if &previous_state.event == event {
                previous_state.time.elapsed().as_secs_f32() >= item.threshold
            } else {
                true
            }
        } else {
            false
        }
    }

    fn map_event(
        character_state: &CharacterState,
        previous_state: &PreviousEntityState,
        inventory: &Inventory,
    ) -> SfxEvent {
        let equip_slot = character_state
            .ability_info()
            .and_then(|ability| ability.hand)
            .map_or(EquipSlot::ActiveMainhand, |hand| hand.to_equip_slot());

        if let Some(item) = inventory.equipped(equip_slot) {
            if let ItemKind::Tool(data) = &*item.kind() {
                if character_state.is_attack() {
                    return SfxEvent::Attack(
                        CharacterAbilityType::from(character_state),
                        data.kind,
                    );
                } else if character_state.is_music() {
                    if let Some(ability_spec) = item
                        .ability_spec()
                        .map(|ability_spec| ability_spec.into_owned())
                    {
                        return SfxEvent::Music(data.kind, ability_spec);
                    }
                } else if let Some(wield_event) = match (
                    previous_state.weapon_drawn,
                    Self::weapon_drawn(character_state),
                ) {
                    (false, true) => Some(SfxEvent::Wield(data.kind)),
                    (true, false) => Some(SfxEvent::Unwield(data.kind)),
                    _ => None,
                } {
                    return wield_event;
                }
            }
            // Check for attacking states
        }

        SfxEvent::Idle
    }

    /// This helps us determine whether we should be emitting the Wield/Unwield
    /// events. For now, consider either CharacterState::Wielding or
    /// ::Equipping to mean the weapon is drawn. This will need updating if the
    /// animations change to match the wield_duration associated with the weapon
    fn weapon_drawn(character: &CharacterState) -> bool {
        character.is_wield() || matches!(character, CharacterState::Equipping { .. })
    }
}

#[cfg(test)] mod tests;