mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
added new cheese, glowing remains and mortar and pestle files, updated collar and mushroom curry
This commit is contained in:
parent
e83446811a
commit
9eedb024fe
@ -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
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)
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
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
BIN
assets/voxygen/voxel/object/mortar_pestle.vox
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
assets/voxygen/voxel/object/mushroom_curry.vox
(Stored with Git LFS)
BIN
assets/voxygen/voxel/object/mushroom_curry.vox
(Stored with Git LFS)
Binary file not shown.
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
@ -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,
|
||||
}
|
||||
}
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -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,
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user