Merge branch 'zesterer/item-merging' into 'master'

Zesterer/item merging

See merge request veloren/veloren!3912
This commit is contained in:
Joshua Barretto 2023-05-05 00:03:22 +00:00
commit b6d8182382
9 changed files with 356 additions and 269 deletions

View File

@ -45,6 +45,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Loot that drops multiple items is now distributed fairly between damage contributors. - Loot that drops multiple items is now distributed fairly between damage contributors.
- Added accessibility settings tab. - Added accessibility settings tab.
- Setting to enable subtitles describing sfx. - Setting to enable subtitles describing sfx.
- Item drops that are spatially close and compatible will now merge with one-another to reduce performance problems.
### Changed ### Changed

View File

@ -1158,6 +1158,37 @@ impl Item {
} }
} }
/// Return `true` if `other` can be merged into this item. This is generally
/// only possible if the item has a compatible item ID and is stackable,
/// along with any other similarity checks.
pub fn can_merge(&self, other: &Item) -> bool {
if self.is_stackable()
&& let ItemBase::Simple(other_item_def) = &other.item_base
&& self.is_same_item_def(other_item_def)
&& u32::from(self.amount)
.checked_add(other.amount())
.filter(|&amount| amount <= self.max_amount())
.is_some()
{
true
} else {
false
}
}
/// Try to merge `other` into this item. This is generally only possible if
/// the item has a compatible item ID and is stackable, along with any
/// other similarity checks.
pub fn try_merge(&mut self, other: Item) -> Result<(), Item> {
if self.can_merge(&other) {
self.increase_amount(other.amount())
.expect("`can_merge` succeeded but `increase_amount` did not");
Ok(())
} else {
Err(other)
}
}
pub fn num_slots(&self) -> u16 { self.item_base.num_slots() } pub fn num_slots(&self) -> u16 { self.item_base.num_slots() }
/// NOTE: invariant that amount() ≤ max_amount(), 1 ≤ max_amount(), /// NOTE: invariant that amount() ≤ max_amount(), 1 ≤ max_amount(),

View File

@ -12,7 +12,7 @@ use std::sync::Arc;
use vek::*; use vek::*;
/// Position /// Position
#[derive(Copy, Clone, Default, Debug, PartialEq, Serialize, Deserialize)] #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Pos(pub Vec3<f32>); pub struct Pos(pub Vec3<f32>);
impl Component for Pos { impl Component for Pos {

View File

@ -503,16 +503,16 @@ fn handle_drop_all(
for item in item_to_place { for item in item_to_place {
let vel = Vec3::new(rng.gen_range(-0.1..0.1), rng.gen_range(-0.1..0.1), 0.5); let vel = Vec3::new(rng.gen_range(-0.1..0.1), rng.gen_range(-0.1..0.1), 0.5);
server server.state.create_item_drop(
.state comp::Pos(Vec3::new(
.create_item_drop(Default::default(), item)
.with(comp::Pos(Vec3::new(
pos.0.x + rng.gen_range(5.0..10.0), pos.0.x + rng.gen_range(5.0..10.0),
pos.0.y + rng.gen_range(5.0..10.0), pos.0.y + rng.gen_range(5.0..10.0),
pos.0.z + 5.0, pos.0.z + 5.0,
))) )),
.with(comp::Vel(vel)) comp::Vel(vel),
.build(); item,
None,
);
} }
Ok(()) Ok(())

View File

@ -473,20 +473,17 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, last_change: Healt
let mut spawn_item = |item, loot_owner| { let mut spawn_item = |item, loot_owner| {
let offset = item_offset_spiral.next().unwrap_or_default(); let offset = item_offset_spiral.next().unwrap_or_default();
let item_drop_entity = state state.create_item_drop(
.create_item_drop(Pos(pos.0 + Vec3::unit_z() * 0.25 + offset), item) Pos(pos.0 + Vec3::unit_z() * 0.25 + offset),
.maybe_with(vel) vel.unwrap_or(comp::Vel(Vec3::zero())),
.build(); item,
if let Some(loot_owner) = loot_owner { if let Some(loot_owner) = loot_owner {
debug!("Assigned UID {loot_owner:?} as the winner for the loot drop"); debug!("Assigned UID {loot_owner:?} as the winner for the loot drop");
if let Err(err) = state Some(LootOwner::new(loot_owner))
.ecs() } else {
.write_storage::<LootOwner>() None
.insert(item_drop_entity, LootOwner::new(loot_owner)) },
{ );
error!("Failed to set loot owner on item drop: {err}");
};
}
}; };
let msm = &MaterialStatManifest::load().read(); let msm = &MaterialStatManifest::load().read();
@ -1103,15 +1100,17 @@ pub fn handle_bonk(server: &mut Server, pos: Vec3<f32>, owner: Option<Uid>, targ
for item in flatten_counted_items(&items, ability_map, msm) { for item in flatten_counted_items(&items, ability_map, msm) {
server server
.state .state
.create_object(Default::default(), match block.get_sprite() { .create_object(
// Create different containers depending on the original sprite Pos(pos.map(|e| e as f32) + Vec3::new(0.5, 0.5, 0.0)),
Some(SpriteKind::Apple) => comp::object::Body::Apple, match block.get_sprite() {
Some(SpriteKind::Beehive) => comp::object::Body::Hive, // Create different containers depending on the original sprite
Some(SpriteKind::Coconut) => comp::object::Body::Coconut, Some(SpriteKind::Apple) => comp::object::Body::Apple,
Some(SpriteKind::Bomb) => comp::object::Body::Bomb, Some(SpriteKind::Beehive) => comp::object::Body::Hive,
_ => comp::object::Body::Pouch, Some(SpriteKind::Coconut) => comp::object::Body::Coconut,
}) Some(SpriteKind::Bomb) => comp::object::Body::Bomb,
.with(Pos(pos.map(|e| e as f32) + Vec3::new(0.5, 0.5, 0.0))) _ => comp::object::Body::Pouch,
},
)
.with(item) .with(item)
.maybe_with(match block.get_sprite() { .maybe_with(match block.get_sprite() {
Some(SpriteKind::Bomb) => Some(comp::Object::Bomb { owner }), Some(SpriteKind::Bomb) => Some(comp::Object::Bomb { owner }),

View File

@ -1,4 +1,4 @@
use specs::{world::WorldExt, Builder, Entity as EcsEntity, Join}; use specs::{world::WorldExt, Entity as EcsEntity, Join};
use vek::*; use vek::*;
use common::{ use common::{
@ -253,15 +253,13 @@ pub fn handle_mine_block(
} }
} }
for item in items { for item in items {
let item_drop = state let loot_owner = maybe_uid.map(LootOwnerKind::Player).map(LootOwner::new);
.create_item_drop(Default::default(), item) state.create_item_drop(
.with(Pos(pos.map(|e| e as f32) + Vec3::new(0.5, 0.5, 0.0))); Pos(pos.map(|e| e as f32) + Vec3::new(0.5, 0.5, 0.0)),
if let Some(uid) = maybe_uid { comp::Vel(Vec3::zero()),
item_drop.with(LootOwner::new(LootOwnerKind::Player(uid))) item,
} else { loot_owner,
item_drop );
}
.build();
} }
} }

View File

@ -377,17 +377,18 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
drop(inventory_updates); drop(inventory_updates);
for item in drop_items { for item in drop_items {
state state.create_item_drop(
.create_item_drop(Default::default(), item) comp::Pos(
.with(comp::Pos(
Vec3::new( Vec3::new(
sprite_pos.x as f32, sprite_pos.x as f32,
sprite_pos.y as f32, sprite_pos.y as f32,
sprite_pos.z as f32, sprite_pos.z as f32,
) + Vec3::one().with_z(0.0) * 0.5, ) + Vec3::one().with_z(0.0) * 0.5,
)) ),
.with(comp::Vel(Vec3::zero())) comp::Vel(Vec3::zero()),
.build(); item,
None,
);
} }
}, },
comp::InventoryManip::Use(slot) => { comp::InventoryManip::Use(slot) => {
@ -890,15 +891,15 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
let items_were_crafted = if let Some(crafted_items) = crafted_items { let items_were_crafted = if let Some(crafted_items) = crafted_items {
for item in crafted_items { for item in crafted_items {
if let Err(item) = inventory.push(item) { if let Err(item) = inventory.push(item) {
dropped_items.push(( if let Some(pos) = state.read_component_copied::<comp::Pos>(entity) {
state dropped_items.push((
.read_component_copied::<comp::Pos>(entity) pos,
.unwrap_or_default(), state
state .read_component_copied::<comp::Ori>(entity)
.read_component_copied::<comp::Ori>(entity) .unwrap_or_default(),
.unwrap_or_default(), item.duplicate(ability_map, &msm),
item.duplicate(ability_map, &msm), ));
)); }
} }
} }
true true
@ -940,11 +941,12 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
} }
}); });
state state.create_item_drop(
.create_item_drop(Default::default(), item) comp::Pos(pos.0 + *ori.look_dir() + Vec3::unit_z()),
.with(comp::Pos(pos.0 + *ori.look_dir() + Vec3::unit_z())) comp::Vel(Vec3::zero()),
.with(comp::Vel(Vec3::zero())) item,
.build(); None,
);
} }
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
@ -963,12 +965,11 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
let uid = state.read_component_copied::<Uid>(entity); let uid = state.read_component_copied::<Uid>(entity);
let mut new_entity = state let mut new_entity = state
.create_object(Default::default(), match kind { .create_object(comp::Pos(pos.0 + Vec3::unit_z() * 0.25), match kind {
item::Throwable::Bomb => comp::object::Body::Bomb, item::Throwable::Bomb => comp::object::Body::Bomb,
item::Throwable::Firework(reagent) => comp::object::Body::for_firework(reagent), item::Throwable::Firework(reagent) => comp::object::Body::for_firework(reagent),
item::Throwable::TrainingDummy => comp::object::Body::TrainingDummy, item::Throwable::TrainingDummy => comp::object::Body::TrainingDummy,
}) })
.with(comp::Pos(pos.0 + Vec3::unit_z() * 0.25))
.with(comp::Vel(vel)); .with(comp::Vel(vel));
match kind { match kind {

View File

@ -19,7 +19,7 @@ use common::{
self, self,
item::{ItemKind, MaterialStatManifest}, item::{ItemKind, MaterialStatManifest},
skills::{GeneralSkill, Skill}, skills::{GeneralSkill, Skill},
ChatType, Group, Inventory, Item, Player, Poise, Presence, PresenceKind, ChatType, Group, Inventory, Item, LootOwner, Player, Poise, Presence, PresenceKind,
}, },
effect::Effect, effect::Effect,
link::{Link, LinkHandle}, link::{Link, LinkHandle},
@ -60,7 +60,15 @@ pub trait StateExt {
) -> EcsEntityBuilder; ) -> EcsEntityBuilder;
/// Build a static object entity /// Build a static object entity
fn create_object(&mut self, pos: comp::Pos, object: comp::object::Body) -> EcsEntityBuilder; fn create_object(&mut self, pos: comp::Pos, object: comp::object::Body) -> EcsEntityBuilder;
fn create_item_drop(&mut self, pos: comp::Pos, item: Item) -> EcsEntityBuilder; /// Create an item drop or merge the item with an existing drop, if a
/// suitable candidate exists.
fn create_item_drop(
&mut self,
pos: comp::Pos,
vel: comp::Vel,
item: Item,
loot_owner: Option<LootOwner>,
) -> Option<EcsEntity>;
fn create_ship<F: FnOnce(comp::ship::Body) -> comp::Collider>( fn create_ship<F: FnOnce(comp::ship::Body) -> comp::Collider>(
&mut self, &mut self,
pos: comp::Pos, pos: comp::Pos,
@ -311,7 +319,53 @@ impl StateExt for State {
.with(body) .with(body)
} }
fn create_item_drop(&mut self, pos: comp::Pos, item: Item) -> EcsEntityBuilder { fn create_item_drop(
&mut self,
pos: comp::Pos,
vel: comp::Vel,
item: Item,
loot_owner: Option<LootOwner>,
) -> Option<EcsEntity> {
{
const MAX_MERGE_DIST: f32 = 1.5;
// First, try to identify possible candidates for item merging
// We limit our search to just a few blocks and we prioritise merging with the
// closest
let positions = self.ecs().read_storage::<comp::Pos>();
let loot_owners = self.ecs().read_storage::<LootOwner>();
let mut items = self.ecs().write_storage::<Item>();
let mut nearby_items = self
.ecs()
.read_resource::<common::CachedSpatialGrid>()
.0
.in_circle_aabr(pos.0.xy(), MAX_MERGE_DIST)
.filter(|entity| items.contains(*entity))
.filter_map(|entity| {
Some((entity, positions.get(entity)?.0.distance_squared(pos.0)))
})
.filter(|(_, dist_sqrd)| *dist_sqrd < MAX_MERGE_DIST.powi(2))
.collect::<Vec<_>>();
nearby_items.sort_by_key(|(_, dist_sqrd)| (dist_sqrd * 1000.0) as i32);
for (nearby, _) in nearby_items {
// Only merge if the loot owner is the same
if loot_owners.get(nearby).map(|lo| lo.owner()) == loot_owner.map(|lo| lo.owner())
&& items
.get(nearby)
.map_or(false, |nearby_item| nearby_item.can_merge(&item))
{
// Merging can occur! Perform the merge:
items
.get_mut(nearby)
.expect("we know that the item exists")
.try_merge(item)
.expect("`try_merge` should succeed because `can_merge` returned `true`");
return None;
}
}
// Only if merging items fails do we give up and create a new item
}
let item_drop = comp::item_drop::Body::from(&item); let item_drop = comp::item_drop::Body::from(&item);
let body = comp::Body::ItemDrop(item_drop); let body = comp::Body::ItemDrop(item_drop);
let light_emitter = match &*item.kind() { let light_emitter = match &*item.kind() {
@ -323,17 +377,21 @@ impl StateExt for State {
}), }),
_ => None, _ => None,
}; };
self.ecs_mut() Some(
.create_entity_synced() self.ecs_mut()
.with(item) .create_entity_synced()
.with(pos) .with(item)
.with(comp::Vel(Vec3::zero())) .with(pos)
.with(item_drop.orientation(&mut thread_rng())) .with(vel)
.with(item_drop.mass()) .with(item_drop.orientation(&mut thread_rng()))
.with(item_drop.density()) .with(item_drop.mass())
.with(body.collider()) .with(item_drop.density())
.with(body) .with(body.collider())
.maybe_with(light_emitter) .with(body)
.maybe_with(loot_owner)
.maybe_with(light_emitter)
.build(),
)
} }
fn create_ship<F: FnOnce(comp::ship::Body) -> comp::Collider>( fn create_ship<F: FnOnce(comp::ship::Body) -> comp::Collider>(

View File

@ -56,210 +56,209 @@ impl EventMapper for BlockEventMapper {
let cam_pos = camera.dependents().cam_pos + focus_off; let cam_pos = camera.dependents().cam_pos + focus_off;
// Get the player position and chunk // Get the player position and chunk
let player_pos = state if let Some(player_pos) = state.read_component_copied::<Pos>(player_entity) {
.read_component_copied::<Pos>(player_entity) let player_chunk = player_pos.0.xy().map2(TerrainChunk::RECT_SIZE, |e, sz| {
.unwrap_or_default(); (e.floor() as i32).div_euclid(sz as i32)
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 // For determining if underground/crickets should chirp
let (terrain_alt, temp) = match client.current_chunk() { let (terrain_alt, temp) = match client.current_chunk() {
Some(chunk) => (chunk.meta().alt(), chunk.meta().temp()), Some(chunk) => (chunk.meta().alt(), chunk.meta().temp()),
None => (0.0, 0.0), None => (0.0, 0.0),
}; };
struct BlockSounds<'a> { struct BlockSounds<'a> {
// The function to select the blocks of interest that we should emit from // The function to select the blocks of interest that we should emit from
blocks: fn(&'a BlocksOfInterest) -> &'a [Vec3<i32>], blocks: fn(&'a BlocksOfInterest) -> &'a [Vec3<i32>],
// The range, in chunks, that the particles should be generated in from the player // The range, in chunks, that the particles should be generated in from the player
range: usize, range: usize,
// The sound of the generated particle // The sound of the generated particle
sfx: SfxEvent, sfx: SfxEvent,
// The volume of the sfx // The volume of the sfx
volume: f32, volume: f32,
// Condition that must be true to play // Condition that must be true to play
cond: fn(&State) -> bool, 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.slow_river,
range: 1,
sfx: SfxEvent::RunningWaterSlow,
volume: 1.2,
cond: |_| true,
},
BlockSounds {
blocks: |boi| &boi.fast_river,
range: 1,
sfx: SfxEvent::RunningWaterFast,
volume: 1.5,
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 let sounds: &[BlockSounds] = &[
for offset in Spiral2d::new().take((sounds.range * 2 + 1).pow(2)) { BlockSounds {
let chunk_pos = player_chunk + offset; 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.slow_river,
range: 1,
sfx: SfxEvent::RunningWaterSlow,
volume: 1.2,
cond: |_| true,
},
BlockSounds {
blocks: |boi| &boi.fast_river,
range: 1,
sfx: SfxEvent::RunningWaterFast,
volume: 1.5,
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(),
},
];
// Get all the blocks of interest in this chunk // Iterate through each kind of block of interest
terrain.get(chunk_pos).map(|chunk_data| { for sounds in sounds.iter() {
// Get the positions of the blocks of type sounds // If the timing condition is false, continue
let blocks = (sounds.blocks)(&chunk_data.blocks_of_interest); // 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;
}
let absolute_pos: Vec3<i32> = // For chunks surrounding the player position
Vec3::from(chunk_pos * TerrainChunk::RECT_SIZE.map(|e| e as i32)); for offset in Spiral2d::new().take((sounds.range * 2 + 1).pow(2)) {
let chunk_pos = player_chunk + offset;
// Replace all RunningWater blocks with just one random one per tick // Get all the blocks of interest in this chunk
let blocks = if sounds.sfx == SfxEvent::RunningWaterSlow terrain.get(chunk_pos).map(|chunk_data| {
|| sounds.sfx == SfxEvent::RunningWaterFast // Get the positions of the blocks of type sounds
{ let blocks = (sounds.blocks)(&chunk_data.blocks_of_interest);
blocks
.choose(&mut thread_rng())
.map(std::slice::from_ref)
.unwrap_or(&[])
} else {
blocks
};
// Iterate through each individual block let absolute_pos: Vec3<i32> =
for block in blocks { Vec3::from(chunk_pos * TerrainChunk::RECT_SIZE.map(|e| e as i32));
// TODO Address this hack properly, potentially by making a new
// block of interest type which picks fewer leaf blocks // Replace all RunningWater blocks with just one random one per tick
// Hack to reduce the number of bird, frog, and water sounds let blocks = if sounds.sfx == SfxEvent::RunningWaterSlow
if ((sounds.sfx == SfxEvent::Birdcall || sounds.sfx == SfxEvent::Owl) || sounds.sfx == SfxEvent::RunningWaterFast
&& thread_rng().gen_bool(0.9995))
|| (sounds.sfx == SfxEvent::Frog && thread_rng().gen_bool(0.75))
|| (sounds.sfx == SfxEvent::RunningWaterSlow
&& thread_rng().gen_bool(0.5))
{ {
continue; blocks
} .choose(&mut thread_rng())
let block_pos: Vec3<i32> = absolute_pos + block; .map(std::slice::from_ref)
let internal_state = self.history.entry(block_pos).or_default(); .unwrap_or(&[])
} else {
blocks
};
let block_pos = block_pos.map(|x| x as f32); // Iterate through each individual block
for block in blocks {
if Self::should_emit( // TODO Address this hack properly, potentially by making a new
internal_state, // block of interest type which picks fewer leaf blocks
triggers.get_key_value(&sounds.sfx), // Hack to reduce the number of bird, frog, and water sounds
temp, if ((sounds.sfx == SfxEvent::Birdcall || sounds.sfx == SfxEvent::Owl)
) { && thread_rng().gen_bool(0.9995))
// If the camera is within SFX distance || (sounds.sfx == SfxEvent::Frog && thread_rng().gen_bool(0.75))
if (block_pos.distance_squared(cam_pos)) < SFX_DIST_LIMIT_SQR { || (sounds.sfx == SfxEvent::RunningWaterSlow
let underwater = state && thread_rng().gen_bool(0.5))
.terrain() {
.get(cam_pos.map(|e| e.floor() as i32)) continue;
.map(|b| b.is_liquid()) }
.unwrap_or(false); let block_pos: Vec3<i32> = absolute_pos + block;
let internal_state = self.history.entry(block_pos).or_default();
let sfx_trigger_item = triggers.get_key_value(&sounds.sfx);
if sounds.sfx == SfxEvent::RunningWaterFast { let block_pos = block_pos.map(|x| x as f32);
audio.emit_filtered_sfx(
sfx_trigger_item, if Self::should_emit(
block_pos, internal_state,
Some(sounds.volume), triggers.get_key_value(&sounds.sfx),
Some(8000), temp,
underwater, ) {
); // If the camera is within SFX distance
} else { if (block_pos.distance_squared(cam_pos)) < SFX_DIST_LIMIT_SQR {
audio.emit_sfx( let underwater = state
sfx_trigger_item, .terrain()
block_pos, .get(cam_pos.map(|e| e.floor() as i32))
Some(sounds.volume), .map(|b| b.is_liquid())
underwater, .unwrap_or(false);
);
} let sfx_trigger_item = triggers.get_key_value(&sounds.sfx);
if sounds.sfx == SfxEvent::RunningWaterFast {
audio.emit_filtered_sfx(
sfx_trigger_item,
block_pos,
Some(sounds.volume),
Some(8000),
underwater,
);
} else {
audio.emit_sfx(
sfx_trigger_item,
block_pos,
Some(sounds.volume),
underwater,
);
}
}
internal_state.time = Instant::now();
internal_state.event = sounds.sfx.clone();
} }
internal_state.time = Instant::now();
internal_state.event = sounds.sfx.clone();
} }
} });
}); }
} }
} }
} }