A skeleton horse splashes and swims with its own water sounds

SkeletonHorse.getSwimSound is not the generic splash: in the water it swims,
on the ground it splashes and, ridden, it counts the water steps on the same
gallopSoundCounter its hoof steps use, galloping through the water every third
step after the first five. SkeletonHorse.playSwimSound maps the volume
Entity.waterSwimSound worked out: 0.3 on the ground, min(0.1, f * 25.0) in the
water.

Horizon had no skeleton horse entry in swim_sound and the horse is not a
monster, so every one of them swam with entity.generic.swim at the full
speed-based volume.
This commit is contained in:
MSedgeMC committed 2026-11-02 09:58:58 +00:00
1 parent 3476a852df
commit 34ea5ed3ee
4 files changed
+111 -15

No files matched your search

+6 -2
View File
@@ -2743,8 +2743,12 @@ pub fn intern_sound(s: &std::sync::Arc<str>) -> &'static str {
set.insert(leaked);
leaked
}
/// `getSwimSound`.
pub fn swim_sound(m: &Mob) -> &'static str {
/// `getSwimSound`. A skeleton horse's own override counts the steps it takes
/// through the water, so it needs the mob, not just its species.
pub fn swim_sound(m: &mut Mob) -> &'static str {
if m.species() == Species::SkeletonHorse {
return super::equine::skeleton_horse_swim_sound(m);
}
match m.species() {
Species::Drowned => "entity.drowned.swim",
Species::Axolotl => "entity.axolotl.swim",
+94
View File
@@ -478,6 +478,46 @@ pub fn hoof_steps(s: Species) -> bool {
| Species::ZombieHorse
)
}
/// `Entity.isVehicle`: someone on its back, the player steering it
/// (`rider`) or a mob that boarded it (`extra.rider`).
pub fn is_vehicle(m: &Mob) -> bool {
m.rider.is_some() || m.extra.rider.is_some()
}
/// `SkeletonHorse.getSwimSound`: an undead horse splashes its own way. In the
/// water it swims; on the ground a horse with nobody on it splashes, and a
/// ridden one counts on the same `gallopSoundCounter` its hoof steps use, so
/// it splashes for the first five steps, then gallops through the water every
/// third and swims in between.
pub fn skeleton_horse_swim_sound(m: &mut Mob) -> &'static str {
if !m.on_ground {
return "entity.skeleton_horse.swim";
}
if !is_vehicle(m) {
return "entity.skeleton_horse.step_water";
}
m.ai.steps.gallop += 1;
let count = m.ai.steps.gallop;
if count > 5 && count % 3 == 0 {
"entity.skeleton_horse.gallop_water"
} else if count <= 5 {
"entity.skeleton_horse.step_water"
} else {
"entity.skeleton_horse.swim"
}
}
/// `SkeletonHorse.playSwimSound`: a skeleton horse standing on the ground
/// splashes at 0.3, and one in the water at `min(0.1, f * 25.0)` - which
/// saturates at 0.1 for any speed a swim sound plays at.
pub fn swim_volume(m: &Mob, f: f32) -> f32 {
if m.species() != Species::SkeletonHorse {
return f;
}
if m.on_ground {
0.3
} else {
(f * 25.0).min(0.1)
}
}
/// `AbstractHorse.playStepSound` (and `Horse.playGallopSound`) on the block
/// `pos`: silent on liquids; a ridden horse walks on wood for five steps,
/// then gallops every third; unridden, wood or the plain step by age.
@@ -1012,4 +1052,58 @@ mod tests {
assert_eq!(rears(Species::Llama, "minecraft:llama", 60), 0);
assert_eq!(rears(Species::Camel, "minecraft:camel", 60), 0);
}
/// `SkeletonHorse.getSwimSound`: an undead horse has its own water sounds,
/// and one with someone on it counts its water steps on the same
/// `gallopSoundCounter` its hoof steps use - five splashes, then a gallop
/// through the water every third step and a swim sound in between.
#[test]
fn a_skeleton_horse_splashes_and_swims_with_its_own_sounds() {
let sound = |m: &mut Mob| crate::mobs::ai::swim_sound(m);
let mut m = equine(1, Species::SkeletonHorse, "minecraft:skeleton_horse");
// In the water it swims; every other animal's default is the generic
// splash (Entity.getSwimSound).
m.on_ground = false;
assert_eq!(sound(&mut m), "entity.skeleton_horse.swim");
// Standing on the ground with nobody on it, it splashes.
m.on_ground = true;
assert_eq!(sound(&mut m), "entity.skeleton_horse.step_water");
m.extra.rider = Some(9);
let ridden: Vec<&str> = (0..9).map(|_| sound(&mut m)).collect();
assert_eq!(
ridden,
[
"entity.skeleton_horse.step_water",
"entity.skeleton_horse.step_water",
"entity.skeleton_horse.step_water",
"entity.skeleton_horse.step_water",
"entity.skeleton_horse.step_water",
"entity.skeleton_horse.gallop_water",
"entity.skeleton_horse.swim",
"entity.skeleton_horse.swim",
"entity.skeleton_horse.gallop_water",
]
);
assert_eq!(m.ai.steps.gallop, 9);
// A plain horse keeps the generic splash.
let mut horse = equine(2, Species::Horse, "minecraft:horse");
assert_eq!(sound(&mut horse), "entity.generic.swim");
}
/// `SkeletonHorse.playSwimSound`: 0.3 on the ground, and no more than 0.1
/// in the water, where every other animal keeps the volume
/// `waterSwimSound` works out from its speed.
#[test]
fn a_skeleton_horses_splash_is_quiet_in_the_water() {
let mut m = equine(1, Species::SkeletonHorse, "minecraft:skeleton_horse");
m.on_ground = true;
assert_eq!(swim_volume(&m, 0.35), 0.3);
m.on_ground = false;
assert_eq!(swim_volume(&m, 0.35), 0.1);
assert_eq!(swim_volume(&m, 1.0), 0.1);
assert!((swim_volume(&m, 0.001) - 0.025).abs() < 1.0e-6);
let mut horse = equine(2, Species::Horse, "minecraft:horse");
horse.on_ground = false;
assert_eq!(swim_volume(&horse, 0.35), 0.35);
}
}
+9 -11
View File
@@ -746,25 +746,23 @@ impl Mob {
self.ai.steps.next_step = self.next_step();
} else if self.fluids.in_water {
self.ai.steps.next_step = self.next_step();
// Entity.waterSwimSound
// Entity.waterSwimSound, and SkeletonHorse.playSwimSound's
// mapping of the volume it works out.
let v = self.velocity;
let volume = 1.0_f32.min(
let speed =
(v.x * v.x * f64::from(0.2_f32) + v.y * v.y + v.z * v.z * f64::from(0.2_f32))
.sqrt() as f32
* 0.35,
);
.sqrt() as f32;
let volume = super::equine::swim_volume(self, 1.0_f32.min(speed * 0.35));
if !super::ai::movement_sounds(self) {
return;
}
let pitch = 1.0 + (self.random.next_float() - self.random.next_float()) * 0.4;
// Entity.playSwimSound asks getSwimSound for its sound even
// when the mob is silent, and a skeleton horse counts there.
let sound = super::ai::swim_sound(self);
if !self.silent {
let at = [self.position.x, self.position.y, self.position.z];
events.push(MobEvent::Sound(
super::ai::swim_sound(self),
at,
volume,
pitch,
));
events.push(MobEvent::Sound(sound, at, volume, pitch));
}
}
} else if w.facts.is_air(state)
+2 -2
View File
@@ -66,8 +66,8 @@ Llama.LlamaHurtByTargetGoal.canContinueToUse port crates/horizon-client/src/mobs
Mule.playChestEquipsSound inline crates/horizon-client/src/game/mounts.rs::Game::interact_mount gap: minor: a mule uses the donkey's chest sound (entity.donkey.chest) instead of entity.mule.chest
SkeletonHorse.checkSkeletonHorseSpawnRules inline crates/horizon-client/src/mobs/spawn_rules.rs::check_spawn_rules spawner reasons need light or a light ignoring reason, otherwise the animal rules
SkeletonHorse.randomizeAttributes port crates/horizon-client/src/mobs/finalize.rs::Ctx::horse jump strength only
SkeletonHorse.getSwimSound missing minor: a skeleton horse in water uses the generic swim sound, no skeleton_horse.swim, step_water or gallop_water
SkeletonHorse.playSwimSound missing minor: the swim volume rules for a skeleton horse are not applied
SkeletonHorse.getSwimSound port crates/horizon-client/src/mobs/equine.rs::skeleton_horse_swim_sound swim in the water; on the ground step_water unridden, and a ridden one counts on gallopSoundCounter: step_water five times, gallop_water every third, swim in between
SkeletonHorse.playSwimSound port crates/horizon-client/src/mobs/equine.rs::swim_volume 0.3 on the ground, min(0.1, f * 25.0) in the water
SkeletonHorse.playJumpSound inline crates/horizon-client/src/game/mounts.rs::Game::tick_mount_riding the jump sound: skeleton_horse.jump_water in water, the horse jump otherwise
SkeletonHorse.aiStep inline crates/horizon-client/src/game/passengers.rs::Game::tick_skeleton_traps a trap horse that is not persistent is removed after 18000 ticks
SkeletonHorse.addAdditionalSaveData port crates/horizon-client/src/mobs/riders.rs::write_nbt SkeletonTrap and SkeletonTrapTime
1 AbstractHorse.registerGoals port crates/horizon-client/src/mobs/equine.rs::register same goals and priorities; llamas and camels have their own register
66 Mule.playChestEquipsSound inline crates/horizon-client/src/game/mounts.rs::Game::interact_mount gap: minor: a mule uses the donkey's chest sound (entity.donkey.chest) instead of entity.mule.chest
67 SkeletonHorse.checkSkeletonHorseSpawnRules inline crates/horizon-client/src/mobs/spawn_rules.rs::check_spawn_rules spawner reasons need light or a light ignoring reason, otherwise the animal rules
68 SkeletonHorse.randomizeAttributes port crates/horizon-client/src/mobs/finalize.rs::Ctx::horse jump strength only
69 SkeletonHorse.getSwimSound missing port crates/horizon-client/src/mobs/equine.rs::skeleton_horse_swim_sound minor: a skeleton horse in water uses the generic swim sound, no skeleton_horse.swim, step_water or gallop_water swim in the water; on the ground step_water unridden, and a ridden one counts on gallopSoundCounter: step_water five times, gallop_water every third, swim in between
70 SkeletonHorse.playSwimSound missing port crates/horizon-client/src/mobs/equine.rs::swim_volume minor: the swim volume rules for a skeleton horse are not applied 0.3 on the ground, min(0.1, f * 25.0) in the water
71 SkeletonHorse.playJumpSound inline crates/horizon-client/src/game/mounts.rs::Game::tick_mount_riding the jump sound: skeleton_horse.jump_water in water, the horse jump otherwise
72 SkeletonHorse.aiStep inline crates/horizon-client/src/game/passengers.rs::Game::tick_skeleton_traps a trap horse that is not persistent is removed after 18000 ticks
73 SkeletonHorse.addAdditionalSaveData port crates/horizon-client/src/mobs/riders.rs::write_nbt SkeletonTrap and SkeletonTrapTime