added new cheese, glowing remains and mortar and pestle files, updated collar and mushroom curry

This commit is contained in:
DoctorKompot 2021-12-09 23:01:25 +02:00
parent e83446811a
commit 9eedb024fe
12 changed files with 20 additions and 1286 deletions

View File

@ -2480,7 +2480,7 @@
(0.0, 0.0, 0.0), (-50.0, 30.0, 20.0), 0.9,
),
Consumable("common.items.food.cheese"): VoxTrans(
"voxel.object.cheese-0",
"voxel.object.cheese",
(0.0, 0.0, 0.0), (-60.0, -10.0, 0.0), 0.8,
),
Consumable("common.items.food.blue_cheese"): VoxTrans(
@ -2661,8 +2661,9 @@
"voxel.object.ice_shard",
(0.0, 0.0, 0.0), (10.0, -20.0, 30.0), 1.0,
),
Ingredient("FlayerBagDamaged"): Png(
"element.items.item_flayer_soul",
Ingredient("FlayerBagDamaged"): VoxTrans(
"voxel.object.glowing_remains",
(0.0, 0.0, 0.0), (10.0, -20.0, 30.0), 1.0,
),
Ingredient("RaptorFeather"): VoxTrans(
"voxel.object.raptor_feather",
@ -2880,8 +2881,9 @@
"voxel.object.honey",
(1.0, 0.0, 0.0), (-20.0, 20.0, -30.0), 0.9,
),
Ingredient("MortarPestle"): Png(
"element.items.item_mortarpestlecoco",
Ingredient("MortarPestle"): VoxTrans(
"voxel.object.mortar_pestle",
(0.0, 0.0, 0.0), (10.0, -20.0, 30.0), 1.0,
),
Ingredient("EmptyVial"): VoxTrans(
"voxel.object.potion_empty",

BIN
assets/voxygen/voxel/object/cheese.vox (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/voxel/object/collar.vox (Stored with Git LFS)

Binary file not shown.

BIN
assets/voxygen/voxel/object/glowing_remains.vox (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/voxel/object/mortar_pestle.vox (Stored with Git LFS) Normal file

Binary file not shown.

Binary file not shown.

View File

@ -1,297 +0,0 @@
/// EventMapper::Block watches the sound emitting blocks within
/// chunk range of the player and emits ambient sfx
use crate::{
audio::sfx::{SfxEvent, SfxTriggerItem, SfxTriggers, SFX_DIST_LIMIT_SQR},
scene::{terrain::BlocksOfInterest, Camera, Terrain},
AudioFrontend,
};
use super::EventMapper;
use client::Client;
use common::{
comp::Pos,
spiral::Spiral2d,
terrain::TerrainChunk,
vol::{ReadVol, RectRasterableVol},
};
use common_state::State;
use hashbrown::HashMap;
use rand::{thread_rng, Rng};
use std::time::{Duration, Instant};
use vek::*;
#[derive(Clone, PartialEq)]
struct PreviousBlockState {
event: SfxEvent,
time: Instant,
}
impl Default for PreviousBlockState {
fn default() -> Self {
Self {
event: SfxEvent::Idle,
time: Instant::now()
.checked_add(Duration::from_millis(thread_rng().gen_range(0..500)))
.unwrap_or_else(Instant::now),
}
}
}
pub struct BlockEventMapper {
history: HashMap<Vec3<i32>, PreviousBlockState>,
}
impl EventMapper for BlockEventMapper {
fn maintain(
&mut self,
audio: &mut AudioFrontend,
state: &State,
player_entity: specs::Entity,
camera: &Camera,
triggers: &SfxTriggers,
terrain: &Terrain<TerrainChunk>,
client: &Client,
) {
let focus_off = camera.get_focus_pos().map(f32::trunc);
let cam_pos = camera.dependents().cam_pos + focus_off;
// Get the player position and chunk
let player_pos = state
.read_component_copied::<Pos>(player_entity)
.unwrap_or_default();
let player_chunk = player_pos.0.xy().map2(TerrainChunk::RECT_SIZE, |e, sz| {
(e.floor() as i32).div_euclid(sz as i32)
});
// For determining if underground/crickets should chirp
let (terrain_alt, temp) = match client.current_chunk() {
Some(chunk) => (chunk.meta().alt(), chunk.meta().temp()),
None => (0.0, 0.0),
};
struct BlockSounds<'a> {
// The function to select the blocks of interest that we should emit from
blocks: fn(&'a BlocksOfInterest) -> &'a [Vec3<i32>],
// The range, in chunks, that the particles should be generated in from the player
range: usize,
// The sound of the generated particle
sfx: SfxEvent,
// The volume of the sfx
volume: f32,
// Condition that must be true to play
cond: fn(&State) -> bool,
}
let sounds: &[BlockSounds] = &[
BlockSounds {
blocks: |boi| &boi.leaves,
range: 1,
sfx: SfxEvent::Birdcall,
volume: 1.0,
cond: |st| st.get_day_period().is_light(),
},
BlockSounds {
blocks: |boi| &boi.leaves,
range: 1,
sfx: SfxEvent::Owl,
volume: 1.0,
cond: |st| st.get_day_period().is_dark(),
},
// BlockSounds {
// blocks: |boi| &boi.river,
// range: 1,
// sfx: SfxEvent::RunningWater,
// volume: 1.0,
// cond: |_| true,
// },
//BlockSounds {
// blocks: |boi| &boi.embers,
// range: 1,
// sfx: SfxEvent::Embers,
// volume: 0.15,
// //volume: 0.05,
// cond: |_| true,
// //cond: |st| st.get_day_period().is_dark(),
//},
BlockSounds {
blocks: |boi| &boi.frogs,
range: 1,
sfx: SfxEvent::Frog,
volume: 0.8,
cond: |st| st.get_day_period().is_dark(),
},
//BlockSounds {
// blocks: |boi| &boi.flowers,
// range: 4,
// sfx: SfxEvent::LevelUp,
// volume: 1.0,
// cond: |st| st.get_day_period().is_dark(),
//},
BlockSounds {
blocks: |boi| &boi.cricket1,
range: 1,
sfx: SfxEvent::Cricket1,
volume: 0.33,
cond: |st| st.get_day_period().is_dark(),
},
BlockSounds {
blocks: |boi| &boi.cricket2,
range: 1,
sfx: SfxEvent::Cricket2,
volume: 0.33,
cond: |st| st.get_day_period().is_dark(),
},
BlockSounds {
blocks: |boi| &boi.cricket3,
range: 1,
sfx: SfxEvent::Cricket3,
volume: 0.33,
cond: |st| st.get_day_period().is_dark(),
},
BlockSounds {
blocks: |boi| &boi.beehives,
range: 1,
sfx: SfxEvent::Bees,
volume: 0.5,
cond: |st| st.get_day_period().is_light(),
},
];
// Iterate through each kind of block of interest
for sounds in sounds.iter() {
// If the timing condition is false, continue
// or if the player is far enough underground, continue
// TODO Address bird hack properly. See TODO on line 190
if !(sounds.cond)(state)
|| player_pos.0.z < (terrain_alt - 30.0)
|| (sounds.sfx == SfxEvent::Birdcall && thread_rng().gen_bool(0.995))
|| (sounds.sfx == SfxEvent::Owl && thread_rng().gen_bool(0.998))
|| (sounds.sfx == SfxEvent::Frog && thread_rng().gen_bool(0.95))
//Crickets will not chirp below 5 Celsius
|| (sounds.sfx == SfxEvent::Cricket1 && (temp < -0.33))
|| (sounds.sfx == SfxEvent::Cricket2 && (temp < -0.33))
|| (sounds.sfx == SfxEvent::Cricket3 && (temp < -0.33))
{
continue;
}
// For chunks surrounding the player position
for offset in Spiral2d::new().take((sounds.range * 2 + 1).pow(2)) {
let chunk_pos = player_chunk + offset;
// Get all the blocks of interest in this chunk
terrain.get(chunk_pos).map(|chunk_data| {
// Get the positions of the blocks of type sounds
let blocks = (sounds.blocks)(&chunk_data.blocks_of_interest);
let absolute_pos: Vec3<i32> =
Vec3::from(chunk_pos * TerrainChunk::RECT_SIZE.map(|e| e as i32));
// Iterate through each individual block
for block in blocks {
// TODO Address this hack properly, potentially by making a new
// block of interest type which picks fewer leaf blocks
// Hack to reduce the number of bird sounds (too many leaf blocks)
if ((sounds.sfx == SfxEvent::Birdcall || sounds.sfx == SfxEvent::Owl)
&& thread_rng().gen_bool(0.999))
|| (sounds.sfx == SfxEvent::Frog && thread_rng().gen_bool(0.75))
{
continue;
}
let block_pos: Vec3<i32> = absolute_pos + block;
let internal_state = self.history.entry(block_pos).or_default();
let block_pos = block_pos.map(|x| x as f32);
if Self::should_emit(
internal_state,
triggers.get_key_value(&sounds.sfx),
temp,
) {
// If the camera is within SFX distance
if (block_pos.distance_squared(cam_pos)) < SFX_DIST_LIMIT_SQR {
let underwater = state
.terrain()
.get(cam_pos.map(|e| e.floor() as i32))
.map(|b| b.is_liquid())
.unwrap_or(false);
let sfx_trigger_item = triggers.get_key_value(&sounds.sfx);
audio.emit_sfx(
sfx_trigger_item,
block_pos,
Some(sounds.volume),
underwater,
);
}
internal_state.time = Instant::now();
internal_state.event = sounds.sfx.clone();
}
}
});
}
}
}
}
impl BlockEventMapper {
pub fn new() -> Self {
Self {
history: HashMap::new(),
}
}
/// 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
/// Note that with so many blocks to choose from and different blocks being
/// selected each time, this is not perfect, but does reduce the number of
/// plays from blocks that have already emitted sfx and are stored in the
/// BlockEventMapper history.
fn should_emit(
previous_state: &PreviousBlockState,
sfx_trigger_item: Option<(&SfxEvent, &SfxTriggerItem)>,
temp: f32,
) -> bool {
if let Some((event, item)) = sfx_trigger_item {
//The interval between cricket chirps calculated by converting chunk
// temperature to centigrade (we should create a function for this) and applying
// the "cricket formula" to it
let cricket_interval = (25.0 / (3.0 * ((temp * 30.0) + 15.0))).max(0.5);
if &previous_state.event == event {
//In case certain sounds need modification to their threshold,
//use match event
match event {
SfxEvent::Cricket1 => {
previous_state.time.elapsed().as_secs_f32()
>= cricket_interval + thread_rng().gen_range(-0.1..0.1)
},
SfxEvent::Cricket2 => {
//the length and manner of this sound is quite different
if cricket_interval < 0.75 {
previous_state.time.elapsed().as_secs_f32() >= 0.75
} else {
previous_state.time.elapsed().as_secs_f32()
>= cricket_interval + thread_rng().gen_range(-0.1..0.1)
}
},
SfxEvent::Cricket3 => {
previous_state.time.elapsed().as_secs_f32()
>= cricket_interval + thread_rng().gen_range(-0.1..0.1)
},
//Adds random factor to frogs (probably doesn't do anything most of the time)
SfxEvent::Frog => {
previous_state.time.elapsed().as_secs_f32()
>= thread_rng().gen_range(-2.0..2.0)
},
_ => previous_state.time.elapsed().as_secs_f32() >= item.threshold,
}
} else {
true
}
} else {
false
}
}
}

View File

@ -1,129 +0,0 @@
/// EventMapper::Campfire maps sfx to campfires
use crate::{
audio::sfx::{SfxEvent, SfxTriggerItem, SfxTriggers, SFX_DIST_LIMIT_SQR},
scene::{Camera, Terrain},
AudioFrontend,
};
use super::EventMapper;
use client::Client;
use common::{
comp::{object, Body, Pos},
terrain::TerrainChunk,
vol::ReadVol,
};
use common_state::State;
use hashbrown::HashMap;
use specs::{Entity as EcsEntity, Join, WorldExt};
use std::time::{Duration, Instant};
#[derive(Clone)]
struct PreviousEntityState {
event: SfxEvent,
time: Instant,
}
impl Default for PreviousEntityState {
fn default() -> Self {
Self {
event: SfxEvent::Idle,
time: Instant::now(),
}
}
}
pub struct CampfireEventMapper {
event_history: HashMap<EcsEntity, PreviousEntityState>,
}
impl EventMapper for CampfireEventMapper {
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 focus_off = camera.get_focus_pos().map(f32::trunc);
let cam_pos = camera.dependents().cam_pos + focus_off;
for (entity, body, pos) in (
&ecs.entities(),
&ecs.read_storage::<Body>(),
&ecs.read_storage::<Pos>(),
)
.join()
.filter(|(_, _, e_pos)| (e_pos.0.distance_squared(cam_pos)) < SFX_DIST_LIMIT_SQR)
{
if let Body::Object(object::Body::CampfireLit) = body {
let internal_state = self.event_history.entry(entity).or_default();
let mapped_event = SfxEvent::Campfire;
// Check for SFX config entry for this movement
if Self::should_emit(internal_state, triggers.get_key_value(&mapped_event)) {
let underwater = state
.terrain()
.get(cam_pos.map(|e| e.floor() as i32))
.map(|b| b.is_liquid())
.unwrap_or(false);
let sfx_trigger_item = triggers.get_key_value(&mapped_event);
const CAMPFIRE_VOLUME: f32 = 0.8;
audio.emit_sfx(sfx_trigger_item, pos.0, Some(CAMPFIRE_VOLUME), underwater);
internal_state.time = Instant::now();
}
// update state to determine the next event. We only record the time (above) if
// it was dispatched
internal_state.event = mapped_event;
}
}
self.cleanup(player_entity);
}
}
impl CampfireEventMapper {
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
}
}
}

View File

@ -1,182 +0,0 @@
/// 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,
vol::ReadVol,
};
use common_state::State;
use hashbrown::HashMap;
use specs::{Entity as EcsEntity, Join, 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 focus_off = camera.get_focus_pos().map(f32::trunc);
let cam_pos = camera.dependents().cam_pos + focus_off;
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 underwater = state
.terrain()
.get(cam_pos.map(|e| e.floor() as i32))
.map(|b| b.is_liquid())
.unwrap_or(false);
let sfx_trigger_item = triggers.get_key_value(&mapped_event);
audio.emit_sfx(sfx_trigger_item, pos.0, None, underwater);
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 {
if let Some(item) = inventory.equipped(EquipSlot::ActiveMainhand) {
if let ItemKind::Tool(data) = item.kind() {
if character_state.is_attack() {
return SfxEvent::Attack(
CharacterAbilityType::from(character_state),
data.kind,
);
} else if let Some(wield_event) = match (
previous_state.weapon_drawn,
character_state.is_dodge(),
Self::weapon_drawn(character_state),
) {
(false, false, true) => Some(SfxEvent::Wield(data.kind)),
(true, false, 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;

View File

@ -1,239 +0,0 @@
use super::*;
use crate::audio::sfx::SfxEvent;
use common::{
combat::{self, DamageKind},
comp::{
inventory::loadout_builder::LoadoutBuilder, item::tool::ToolKind, CharacterAbilityType,
CharacterState, InputKind, Item,
},
states,
};
use std::time::{Duration, Instant};
#[test]
fn maps_wield_while_equipping() {
let loadout = LoadoutBuilder::empty()
.active_mainhand(Some(Item::new_from_asset_expect(
"common.items.weapons.axe.starter_axe",
)))
.build();
let inventory = Inventory::new_with_loadout(loadout);
let result = CombatEventMapper::map_event(
&CharacterState::Equipping(states::equipping::Data {
static_data: states::equipping::StaticData {
buildup_duration: Duration::from_millis(10),
},
timer: Duration::default(),
is_sneaking: false,
}),
&PreviousEntityState {
event: SfxEvent::Idle,
time: Instant::now(),
weapon_drawn: false,
},
&inventory,
);
assert_eq!(result, SfxEvent::Wield(ToolKind::Axe));
}
#[test]
fn maps_unwield() {
let loadout = LoadoutBuilder::empty()
.active_mainhand(Some(Item::new_from_asset_expect(
"common.items.weapons.bow.starter",
)))
.build();
let inventory = Inventory::new_with_loadout(loadout);
let result = CombatEventMapper::map_event(
&CharacterState::default(),
&PreviousEntityState {
event: SfxEvent::Idle,
time: Instant::now(),
weapon_drawn: true,
},
&inventory,
);
assert_eq!(result, SfxEvent::Unwield(ToolKind::Bow));
}
#[test]
fn maps_basic_melee() {
let loadout = LoadoutBuilder::empty()
.active_mainhand(Some(Item::new_from_asset_expect(
"common.items.weapons.axe.starter_axe",
)))
.build();
let inventory = Inventory::new_with_loadout(loadout);
let result = CombatEventMapper::map_event(
&CharacterState::BasicMelee(states::basic_melee::Data {
static_data: states::basic_melee::StaticData {
buildup_duration: Duration::default(),
swing_duration: Duration::default(),
recover_duration: Duration::default(),
base_damage: 10.0,
base_poise_damage: 10.0,
knockback: combat::Knockback {
strength: 0.0,
direction: combat::KnockbackDir::Away,
},
range: 1.0,
max_angle: 1.0,
ability_info: empty_ability_info(),
damage_effect: None,
damage_kind: DamageKind::Slashing,
},
timer: Duration::default(),
stage_section: states::utils::StageSection::Buildup,
exhausted: false,
}),
&PreviousEntityState {
event: SfxEvent::Idle,
time: Instant::now(),
weapon_drawn: true,
},
&inventory,
);
assert_eq!(
result,
SfxEvent::Attack(CharacterAbilityType::BasicMelee, ToolKind::Axe)
);
}
#[test]
fn matches_ability_stage() {
let loadout = LoadoutBuilder::empty()
.active_mainhand(Some(Item::new_from_asset_expect(
"common.items.weapons.sword.starter",
)))
.build();
let inventory = Inventory::new_with_loadout(loadout);
let result = CombatEventMapper::map_event(
&CharacterState::ComboMelee(states::combo_melee::Data {
static_data: states::combo_melee::StaticData {
num_stages: 1,
stage_data: vec![states::combo_melee::Stage {
stage: 1,
base_damage: 100.0,
base_poise_damage: 100.0,
damage_increase: 10.0,
poise_damage_increase: 10.0,
knockback: 10.0,
range: 4.0,
angle: 30.0,
base_buildup_duration: Duration::from_millis(500),
base_swing_duration: Duration::from_millis(200),
hit_timing: 0.5,
base_recover_duration: Duration::from_millis(400),
forward_movement: 0.5,
damage_kind: DamageKind::Slashing,
damage_effect: None,
}],
initial_energy_gain: 0.0,
max_energy_gain: 100.0,
energy_increase: 20.0,
speed_increase: 0.05,
max_speed_increase: 0.8,
scales_from_combo: 2,
is_interruptible: true,
ori_modifier: 1.0,
ability_info: empty_ability_info(),
},
exhausted: false,
stage: 1,
timer: Duration::default(),
stage_section: states::utils::StageSection::Action,
}),
&PreviousEntityState {
event: SfxEvent::Idle,
time: Instant::now(),
weapon_drawn: true,
},
&inventory,
);
assert_eq!(
result,
SfxEvent::Attack(
CharacterAbilityType::ComboMelee(states::utils::StageSection::Action, 1),
ToolKind::Sword
)
);
}
#[test]
fn ignores_different_ability_stage() {
let loadout = LoadoutBuilder::empty()
.active_mainhand(Some(Item::new_from_asset_expect(
"common.items.weapons.axe.starter_axe",
)))
.build();
let inventory = Inventory::new_with_loadout(loadout);
let result = CombatEventMapper::map_event(
&CharacterState::ComboMelee(states::combo_melee::Data {
static_data: states::combo_melee::StaticData {
num_stages: 1,
stage_data: vec![states::combo_melee::Stage {
stage: 1,
base_damage: 100.0,
base_poise_damage: 100.0,
damage_increase: 100.0,
poise_damage_increase: 10.0,
knockback: 10.0,
range: 4.0,
angle: 30.0,
base_buildup_duration: Duration::from_millis(500),
base_swing_duration: Duration::from_millis(200),
hit_timing: 0.5,
base_recover_duration: Duration::from_millis(400),
forward_movement: 0.5,
damage_kind: DamageKind::Slashing,
damage_effect: None,
}],
initial_energy_gain: 0.0,
max_energy_gain: 100.0,
energy_increase: 20.0,
speed_increase: 0.05,
max_speed_increase: 0.8,
scales_from_combo: 2,
is_interruptible: true,
ori_modifier: 1.0,
ability_info: empty_ability_info(),
},
exhausted: false,
stage: 1,
timer: Duration::default(),
stage_section: states::utils::StageSection::Action,
}),
&PreviousEntityState {
event: SfxEvent::Idle,
time: Instant::now(),
weapon_drawn: true,
},
&inventory,
);
assert_ne!(
result,
SfxEvent::Attack(
CharacterAbilityType::ComboMelee(states::utils::StageSection::Action, 2),
ToolKind::Sword
)
);
}
fn empty_ability_info() -> states::utils::AbilityInfo {
states::utils::AbilityInfo {
tool: None,
hand: None,
input: InputKind::Primary,
input_attr: None,
}
}

View File

@ -1,72 +0,0 @@
mod block;
mod campfire;
mod combat;
mod movement;
use client::Client;
use common::terrain::TerrainChunk;
use common_state::State;
use block::BlockEventMapper;
use campfire::CampfireEventMapper;
use combat::CombatEventMapper;
use movement::MovementEventMapper;
use super::SfxTriggers;
use crate::{
scene::{Camera, Terrain},
AudioFrontend,
};
trait EventMapper {
fn maintain(
&mut self,
audio: &mut AudioFrontend,
state: &State,
player_entity: specs::Entity,
camera: &Camera,
triggers: &SfxTriggers,
terrain: &Terrain<TerrainChunk>,
client: &Client,
);
}
pub struct SfxEventMapper {
mappers: Vec<Box<dyn EventMapper>>,
}
impl SfxEventMapper {
pub fn new() -> Self {
Self {
mappers: vec![
Box::new(CombatEventMapper::new()),
Box::new(MovementEventMapper::new()),
Box::new(BlockEventMapper::new()),
Box::new(CampfireEventMapper::new()),
],
}
}
pub fn maintain(
&mut self,
audio: &mut AudioFrontend,
state: &State,
player_entity: specs::Entity,
camera: &Camera,
triggers: &SfxTriggers,
terrain: &Terrain<TerrainChunk>,
client: &Client,
) {
for mapper in &mut self.mappers {
mapper.maintain(
audio,
state,
player_entity,
camera,
triggers,
terrain,
client,
);
}
}
}

View File

@ -1,358 +0,0 @@
use super::*;
use crate::audio::sfx::SfxEvent;
use common::{
comp::{
bird_large, humanoid, quadruped_medium, quadruped_small, Body, CharacterState, InputKind,
Ori, PhysicsState,
},
states,
terrain::{Block, BlockKind},
};
use std::time::{Duration, Instant};
#[test]
fn no_item_config_no_emit() {
let previous_state = PreviousEntityState::default();
let result = MovementEventMapper::should_emit(&previous_state, None);
assert!(!result);
}
#[test]
fn config_but_played_since_threshold_no_emit() {
let trigger_item = SfxTriggerItem {
files: vec![String::from("some.path.to.sfx.file")],
threshold: 1.0,
};
// Triggered a 'Run' 0 seconds ago
let previous_state = PreviousEntityState {
event: SfxEvent::Run(BlockKind::Grass),
time: Instant::now(),
on_ground: true,
in_water: false,
distance_travelled: 0.0,
};
let result = MovementEventMapper::should_emit(
&previous_state,
Some((&SfxEvent::Run(BlockKind::Grass), &trigger_item)),
);
assert!(!result);
}
#[test]
fn config_and_not_played_since_threshold_emits() {
let trigger_item = SfxTriggerItem {
files: vec![String::from("some.path.to.sfx.file")],
threshold: 0.5,
};
let previous_state = PreviousEntityState {
event: SfxEvent::Idle,
time: Instant::now().checked_add(Duration::from_secs(1)).unwrap(),
on_ground: true,
in_water: false,
distance_travelled: 0.0,
};
let result = MovementEventMapper::should_emit(
&previous_state,
Some((&SfxEvent::Run(BlockKind::Grass), &trigger_item)),
);
assert!(result);
}
#[test]
fn same_previous_event_elapsed_emits() {
let trigger_item = SfxTriggerItem {
files: vec![String::from("some.path.to.sfx.file")],
threshold: 0.5,
};
let previous_state = PreviousEntityState {
event: SfxEvent::Run(BlockKind::Grass),
time: Instant::now()
.checked_sub(Duration::from_millis(1800))
.unwrap(),
on_ground: true,
in_water: false,
distance_travelled: 2.0,
};
let result = MovementEventMapper::should_emit(
&previous_state,
Some((&SfxEvent::Run(BlockKind::Grass), &trigger_item)),
);
assert!(result);
}
#[test]
fn maps_idle() {
let result = MovementEventMapper::map_movement_event(
&CharacterState::Idle(common::states::idle::Data { is_sneaking: false }),
&PhysicsState {
on_ground: Some(Block::empty()),
..Default::default()
},
&PreviousEntityState {
event: SfxEvent::Idle,
time: Instant::now(),
on_ground: true,
in_water: false,
distance_travelled: 0.0,
},
Vec3::zero(),
BlockKind::Grass,
);
assert_eq!(result, SfxEvent::Idle);
}
#[test]
fn maps_run_with_sufficient_velocity() {
let result = MovementEventMapper::map_movement_event(
&CharacterState::Idle(common::states::idle::Data { is_sneaking: false }),
&PhysicsState {
on_ground: Some(Block::empty()),
..Default::default()
},
&PreviousEntityState {
event: SfxEvent::Idle,
time: Instant::now(),
on_ground: true,
in_water: false,
distance_travelled: 0.0,
},
Vec3::new(0.5, 0.8, 0.0),
BlockKind::Grass,
);
assert_eq!(result, SfxEvent::Run(BlockKind::Grass));
}
#[test]
fn does_not_map_run_with_insufficient_velocity() {
let result = MovementEventMapper::map_movement_event(
&CharacterState::Idle(common::states::idle::Data { is_sneaking: false }),
&PhysicsState {
on_ground: Some(Block::empty()),
..Default::default()
},
&PreviousEntityState {
event: SfxEvent::Idle,
time: Instant::now(),
on_ground: true,
in_water: false,
distance_travelled: 0.0,
},
Vec3::new(0.02, 0.0001, 0.0),
BlockKind::Grass,
);
assert_eq!(result, SfxEvent::Idle);
}
#[test]
fn does_not_map_run_with_sufficient_velocity_but_not_on_ground() {
let result = MovementEventMapper::map_movement_event(
&CharacterState::Idle(common::states::idle::Data { is_sneaking: false }),
&Default::default(),
&PreviousEntityState {
event: SfxEvent::Idle,
time: Instant::now(),
on_ground: false,
in_water: false,
distance_travelled: 0.0,
},
Vec3::new(0.5, 0.8, 0.0),
BlockKind::Grass,
);
assert_eq!(result, SfxEvent::Idle);
}
#[test]
fn maps_roll() {
let result = MovementEventMapper::map_movement_event(
&CharacterState::Roll(states::roll::Data {
static_data: states::roll::StaticData {
buildup_duration: Duration::default(),
movement_duration: Duration::default(),
recover_duration: Duration::default(),
roll_strength: 0.0,
immune_melee: false,
ability_info: empty_ability_info(),
},
timer: Duration::default(),
stage_section: states::utils::StageSection::Buildup,
was_wielded: true,
is_sneaking: false,
was_combo: None,
}),
&PhysicsState {
on_ground: Some(Block::empty()),
..Default::default()
},
&PreviousEntityState {
event: SfxEvent::Run(BlockKind::Grass),
time: Instant::now(),
on_ground: true,
in_water: false,
distance_travelled: 0.0,
},
Vec3::new(0.5, 0.5, 0.0),
BlockKind::Grass,
);
assert_eq!(result, SfxEvent::Roll);
}
#[test]
fn maps_land_on_ground_to_run() {
let result = MovementEventMapper::map_movement_event(
&CharacterState::Idle(common::states::idle::Data { is_sneaking: false }),
&PhysicsState {
on_ground: Some(Block::empty()),
..Default::default()
},
&PreviousEntityState {
event: SfxEvent::Idle,
time: Instant::now(),
on_ground: false,
in_water: false,
distance_travelled: 0.0,
},
Vec3::zero(),
BlockKind::Grass,
);
assert_eq!(result, SfxEvent::Run(BlockKind::Grass));
}
#[test]
fn maps_glider_open() {
let result = MovementEventMapper::map_movement_event(
&CharacterState::Glide(states::glide::Data::new(10.0, 1.0, Ori::default())),
&Default::default(),
&PreviousEntityState {
event: SfxEvent::Jump,
time: Instant::now(),
on_ground: false,
in_water: false,
distance_travelled: 0.0,
},
Vec3::zero(),
BlockKind::Grass,
);
assert_eq!(result, SfxEvent::GliderOpen);
}
#[test]
fn maps_glide() {
let result = MovementEventMapper::map_movement_event(
&CharacterState::Glide(states::glide::Data::new(10.0, 1.0, Ori::default())),
&Default::default(),
&PreviousEntityState {
event: SfxEvent::Glide,
time: Instant::now(),
on_ground: false,
in_water: false,
distance_travelled: 0.0,
},
Vec3::zero(),
BlockKind::Grass,
);
assert_eq!(result, SfxEvent::Glide);
}
#[test]
fn maps_glider_close_when_closing_mid_flight() {
let result = MovementEventMapper::map_movement_event(
&CharacterState::Idle(common::states::idle::Data { is_sneaking: false }),
&Default::default(),
&PreviousEntityState {
event: SfxEvent::Glide,
time: Instant::now(),
on_ground: false,
in_water: false,
distance_travelled: 0.0,
},
Vec3::zero(),
BlockKind::Grass,
);
assert_eq!(result, SfxEvent::GliderClose);
}
#[test]
#[ignore]
fn maps_glider_close_when_landing() {
let result = MovementEventMapper::map_movement_event(
&CharacterState::Idle(common::states::idle::Data { is_sneaking: false }),
&PhysicsState {
on_ground: Some(Block::empty()),
..Default::default()
},
&PreviousEntityState {
event: SfxEvent::Glide,
time: Instant::now(),
on_ground: false,
in_water: false,
distance_travelled: 0.0,
},
Vec3::zero(),
BlockKind::Grass,
);
assert_eq!(result, SfxEvent::GliderClose);
}
#[test]
fn maps_quadrupeds_running() {
let result = MovementEventMapper::map_non_humanoid_movement_event(
&PhysicsState {
on_ground: Some(Block::empty()),
..Default::default()
},
Vec3::new(0.5, 0.8, 0.0),
BlockKind::Grass,
);
assert_eq!(result, SfxEvent::Run(BlockKind::Grass));
}
#[test]
fn determines_relative_volumes() {
let human =
MovementEventMapper::get_volume_for_body_type(&Body::Humanoid(humanoid::Body::random()));
let quadruped_medium = MovementEventMapper::get_volume_for_body_type(&Body::QuadrupedMedium(
quadruped_medium::Body::random(),
));
let quadruped_small = MovementEventMapper::get_volume_for_body_type(&Body::QuadrupedSmall(
quadruped_small::Body::random(),
));
let bird_large =
MovementEventMapper::get_volume_for_body_type(&Body::BirdLarge(bird_large::Body::random()));
assert!(quadruped_medium < human);
assert!(quadruped_small < quadruped_medium);
assert!(bird_large < quadruped_small);
}
fn empty_ability_info() -> states::utils::AbilityInfo {
states::utils::AbilityInfo {
tool: None,
hand: None,
input: InputKind::Primary,
input_attr: None,
}
}