diff --git a/crates/horizon-client/src/game.rs b/crates/horizon-client/src/game.rs index 5b9a14da8..2217d5c65 100644 --- a/crates/horizon-client/src/game.rs +++ b/crates/horizon-client/src/game.rs @@ -4740,6 +4740,14 @@ impl Game<'_> { self.audio .play("minecraft:entity.player.swim", Some(feet), volume, pitch); } + MotionEvent::Chime { volume, pitch } => { + self.audio.play( + "minecraft:block.amethyst_block.chime", + Some(feet), + volume, + pitch, + ); + } MotionEvent::Drown => { // LivingEntity.makeDrownParticles: eight bubbles moving // with the player. diff --git a/crates/horizon-client/src/game/remote.rs b/crates/horizon-client/src/game/remote.rs index 663983443..5ebb6276e 100644 --- a/crates/horizon-client/src/game/remote.rs +++ b/crates/horizon-client/src/game/remote.rs @@ -3403,6 +3403,14 @@ impl Game<'_> { self.audio .play("minecraft:entity.player.swim", Some(feet), volume, pitch); } + horizon_client::motion::MotionEvent::Chime { volume, pitch } => { + self.audio.play( + "minecraft:block.amethyst_block.chime", + Some(feet), + volume, + pitch, + ); + } // NetherPortalBlock.entityInside on the client's own level: // the portal overlay and its trigger sound (the server moves // the player). diff --git a/crates/horizon-client/src/motion.rs b/crates/horizon-client/src/motion.rs index 540a7e819..a3df4ab4b 100644 --- a/crates/horizon-client/src/motion.rs +++ b/crates/horizon-client/src/motion.rs @@ -55,6 +55,8 @@ pub struct MotionResources { /// `BlockTags.SOUL_SPEED_BLOCKS`: what Soul Speed's effects look for. pub soul_speed_blocks: BTreeSet, pub fall_reset: BTreeSet, + /// `BlockTags.CRYSTAL_SOUND_BLOCKS`: blocks that chime under footsteps. + pub crystal_sound: BTreeSet, pub landing: BlockLandingRegistry, pub health: crate::health::HealthResources, pub fluids: FluidResources, @@ -115,6 +117,7 @@ impl MotionResources { glide_through: tag("minecraft:can_glide_through")?, soul_speed_blocks: tag("minecraft:soul_speed_blocks")?, fall_reset: tag("minecraft:fall_damage_resetting")?, + crystal_sound: tag("minecraft:crystal_sound_blocks")?, }) } } @@ -165,6 +168,11 @@ pub enum MotionEvent { state: BlockStateId, in_water: bool, }, + /// `Entity.playAmethystStepSound`: `block.amethyst_block.chime`. + Chime { + volume: f32, + pitch: f32, + }, /// `Entity.waterSwimSound`: `entity.player.swim`. Swim { volume: f32, @@ -309,6 +317,10 @@ pub struct PlayerMotion { /// `Entity.moveDist`, `flyDist` and `nextStep`: footsteps come once per /// metre walked, not once per move. pub steps: MovementDistances, + /// `Entity.crystalSoundIntensity` and `lastCrystalSoundPlayTick`: the + /// amethyst chime grows with steps taken close together. + pub crystal_sound_intensity: f32, + pub last_crystal_sound_play_tick: i32, pub needs_sync: bool, pub sync_position: bool, /// Moved by an ender pearl (`ServerPlayer.teleport`): a server sends the @@ -501,6 +513,8 @@ impl PlayerMotion { next_step: 1.0, ..MovementDistances::default() }, + crystal_sound_intensity: 0.0, + last_crystal_sound_play_tick: 0, needs_sync: false, sync_position: false, teleported: false, @@ -2164,6 +2178,30 @@ impl MovementEmissionWorld for MotionWorld<'_, '_, '_> { state, in_water, }); + // Entity.walkingStepSound: the amethyst chime after the step. + let name = &self + .assets + .registry + .state(state) + .ok_or("missing scene state")? + .name; + let tick = self.player.health.tick_count; + if self.resources.crystal_sound.contains(name) + && tick >= self.player.last_crystal_sound_play_tick + 20 + { + // Entity.playAmethystStepSound (the level random stands in for + // the entity's). + let elapsed = f64::from(tick - self.player.last_crystal_sound_play_tick); + let decayed = self.player.crystal_sound_intensity * 0.997_f64.powf(elapsed) as f32; + let intensity = 1.0_f32.min(decayed + 0.07); + self.player.crystal_sound_intensity = intensity; + let pitch = 0.5 + intensity * self.scene.random.next_float() * 1.2; + let volume = 0.1 + intensity * 1.2; + self.player + .events + .push(MotionEvent::Chime { volume, pitch }); + self.player.last_crystal_sound_play_tick = tick; + } Ok(()) } fn step_event(&mut self, _: BlockStateId) -> Result<()> {