mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-12 13:03:44 -10:00
Keep level-up sounds on each player clock
Co-Authored-By: Codex GPT-6 <[email protected]>
This commit is contained in:
1 parent
591ce12184
commit
f4c4ab173c
7 files changed
+136
-43
No files matched your search
@@ -1482,40 +1482,69 @@ impl Experience {
|
||||
}
|
||||
sound_level
|
||||
}
|
||||
/// `Player.giveExperienceLevels`: play `entity.player.levelup` when
|
||||
/// `amount` is positive, the level landed on is a multiple of 5, and
|
||||
/// 100 ticks have passed since the last one (`lastLevelUpTime < tickCount
|
||||
/// - 100`). The volume is `level / 30` (1 above 30) times 0.75.
|
||||
pub fn level_up_volume(amount: i32, level: i32, ticks: u64, last: u64) -> Option<f32> {
|
||||
if amount > 0 && level % 5 == 0 && last < ticks.saturating_sub(100) {
|
||||
let vol = if level > 30 { 1.0 } else { level as f32 / 30.0 };
|
||||
Some(vol * 0.75)
|
||||
}
|
||||
|
||||
/// A player's `Entity.tickCount` and `Player.lastLevelUpTime`. This state belongs
|
||||
/// to the player entity, not the level or the server's clock.
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct LevelUpSound {
|
||||
ticks: i32,
|
||||
last: i32,
|
||||
}
|
||||
|
||||
impl LevelUpSound {
|
||||
/// `Entity.commonTick`: called once for this player, including spectators.
|
||||
pub fn tick(&mut self) {
|
||||
self.ticks = self.ticks.wrapping_add(1);
|
||||
}
|
||||
|
||||
/// `Player.giveExperienceLevels`: return the sound's volume and start its
|
||||
/// cooldown when a positive change lands on a fifth level.
|
||||
pub fn give_levels(&mut self, amount: i32, level: i32) -> Option<f32> {
|
||||
if amount > 0 && level % 5 == 0 && (self.last as f32) < self.ticks as f32 - 100.0 {
|
||||
self.last = self.ticks;
|
||||
Some((if level > 30 { 1.0 } else { level as f32 / 30.0 }) * 0.75)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod level_up_sound {
|
||||
use super::Experience;
|
||||
use super::LevelUpSound;
|
||||
|
||||
#[test]
|
||||
fn give_experience_levels_sounds_on_every_fifth_level_after_a_hundred_ticks() {
|
||||
// 5/30 * 0.75 = 0.125, 10/30 * 0.75 = 0.25, and 30/30 and anything
|
||||
// above 30 are both 0.75. A level that is not a multiple of 5, a
|
||||
// loss of levels, and a cooldown that has not elapsed stay quiet.
|
||||
assert_eq!(Experience::level_up_volume(1, 5, 200, 0), Some(0.125));
|
||||
assert_eq!(Experience::level_up_volume(1, 10, 200, 0), Some(0.25));
|
||||
assert_eq!(Experience::level_up_volume(1, 30, 101, 0), Some(0.75));
|
||||
assert_eq!(Experience::level_up_volume(1, 35, 200, 0), Some(0.75));
|
||||
assert_eq!(Experience::level_up_volume(1, 6, 200, 0), None);
|
||||
assert_eq!(Experience::level_up_volume(-1, 5, 200, 0), None);
|
||||
assert_eq!(Experience::level_up_volume(1, 5, 100, 0), None);
|
||||
assert_eq!(Experience::level_up_volume(1, 5, 101, 0), Some(0.125));
|
||||
assert_eq!(Experience::level_up_volume(1, 5, 250, 150), None);
|
||||
assert_eq!(Experience::level_up_volume(1, 5, 251, 150), Some(0.125));
|
||||
fn players_have_independent_ages_and_cooldowns() {
|
||||
let mut a = LevelUpSound::default();
|
||||
let mut b = LevelUpSound::default();
|
||||
for _ in 0..100 {
|
||||
a.tick();
|
||||
b.tick();
|
||||
}
|
||||
assert_eq!(a.give_levels(1, 5), None);
|
||||
a.tick();
|
||||
b.tick();
|
||||
assert_eq!(a.give_levels(1, 5), Some(0.125));
|
||||
assert_eq!(b.give_levels(1, 10), Some(0.25));
|
||||
assert_eq!(a.give_levels(1, 10), None);
|
||||
for _ in 0..100 {
|
||||
a.tick();
|
||||
}
|
||||
assert_eq!(a.give_levels(1, 30), None);
|
||||
a.tick();
|
||||
assert_eq!(a.give_levels(1, 30), Some(0.75));
|
||||
let mut respawned = LevelUpSound::default();
|
||||
assert_eq!(respawned.give_levels(1, 35), None);
|
||||
for _ in 0..101 {
|
||||
respawned.tick();
|
||||
}
|
||||
assert_eq!(respawned.give_levels(-1, 35), None);
|
||||
assert_eq!(respawned.give_levels(1, 34), None);
|
||||
assert_eq!(respawned.give_levels(1, 35), Some(0.75));
|
||||
}
|
||||
}
|
||||
|
||||
/// A painting (`Painting`, a `HangingEntity`): the block it hangs in, the
|
||||
/// 2D data value of the direction it faces, and its variant.
|
||||
#[derive(Clone, Debug)]
|
||||
|
||||
@@ -2759,7 +2759,7 @@ struct Game<'a> {
|
||||
orbs: horizon_client::join_seq::JoinList<ExperienceOrb>,
|
||||
xp_delay: u32,
|
||||
ordered: ordered_tick::OrderedTicks,
|
||||
last_level_up: u64,
|
||||
level_up_sound: horizon_client::entities::LevelUpSound,
|
||||
/// The arm swing (`LivingEntity.SwingState`) and how many swings started.
|
||||
swing: horizon_client::first_person::SwingState,
|
||||
swings_started: u32,
|
||||
@@ -14194,19 +14194,13 @@ impl Game<'_> {
|
||||
return;
|
||||
};
|
||||
let gained = gained.max(0);
|
||||
let at = self.eye_position();
|
||||
let old_level = self.experience.level;
|
||||
let gave = self.experience.give(gained);
|
||||
self.native_player_level_changed(old_level);
|
||||
// Player.giveExperienceLevels runs per level, so the first multiple of 5
|
||||
// crossed plays the sound (and starts the cooldown for the rest).
|
||||
if let Some(level) = gave
|
||||
&& self.ticks > self.last_level_up + 100
|
||||
{
|
||||
self.last_level_up = self.ticks;
|
||||
let volume = if level > 30 { 1.0 } else { level as f32 / 30.0 };
|
||||
self.audio
|
||||
.play("minecraft:entity.player.levelup", at, volume * 0.75, 1.0);
|
||||
if let Some(level) = gave {
|
||||
self.play_level_up(1, level);
|
||||
}
|
||||
}
|
||||
/// BowItem.releaseUsing: power from charge time, arrows at power * 3.
|
||||
@@ -19476,7 +19470,7 @@ impl<'a> Game<'a> {
|
||||
orbs: Default::default(),
|
||||
xp_delay: 0,
|
||||
ordered: Default::default(),
|
||||
last_level_up: 0,
|
||||
level_up_sound: Default::default(),
|
||||
swing: Default::default(),
|
||||
swings_started: 0,
|
||||
heights: Default::default(),
|
||||
|
||||
@@ -883,7 +883,10 @@ impl Game<'_> {
|
||||
if a.rewards.experience > 0 {
|
||||
let old_level = self.experience.level;
|
||||
let points = self.xp_change(a.rewards.experience).map_or(0, |p| p.max(0));
|
||||
self.experience.give(points);
|
||||
let gained_level = self.experience.give(points);
|
||||
if let Some(level) = gained_level {
|
||||
self.play_level_up(1, level);
|
||||
}
|
||||
self.native_player_level_changed(old_level);
|
||||
}
|
||||
// ServerPlayer.awardRecipesByKey: the rewards that name a recipe.
|
||||
|
||||
@@ -3055,6 +3055,7 @@ impl Game<'_> {
|
||||
/// The caller reports the level change to the mods.
|
||||
fn give_points(&mut self, amount: i32) {
|
||||
let e = &mut self.experience;
|
||||
let mut sound_level = None;
|
||||
e.progress += amount as f32 / horizon_client::entities::Experience::needed(e.level) as f32;
|
||||
e.total = e.total.saturating_add(amount).clamp(0, i32::MAX);
|
||||
while e.progress < 0.0 {
|
||||
@@ -3071,8 +3072,14 @@ impl Game<'_> {
|
||||
e.progress =
|
||||
(e.progress - 1.0) * horizon_client::entities::Experience::needed(e.level) as f32;
|
||||
e.level += 1;
|
||||
if e.level % 5 == 0 {
|
||||
sound_level.get_or_insert(e.level);
|
||||
}
|
||||
e.progress /= horizon_client::entities::Experience::needed(e.level) as f32;
|
||||
}
|
||||
if let Some(level) = sound_level {
|
||||
self.play_level_up(1, level);
|
||||
}
|
||||
}
|
||||
/// AdvancementCommands.perform / performCriterion.
|
||||
fn cmd_advancement(&mut self, ctx: &Context) -> R<i32> {
|
||||
|
||||
@@ -118,6 +118,8 @@ pub(super) struct GuestPlay {
|
||||
pub(super) respawn_requested: bool,
|
||||
/// `Player.takeXpDelay`: ticks until the next orb can be taken.
|
||||
xp_delay: u32,
|
||||
/// This player entity's age and level-up sound cooldown.
|
||||
level_up_sound: horizon_client::entities::LevelUpSound,
|
||||
/// What last killed the guest outright (`/kill`): the death message's key.
|
||||
death_cause: Option<String>,
|
||||
/// The equipment modifiers on the guest's attributes (`apply_equipment`)
|
||||
@@ -166,6 +168,7 @@ pub(super) struct GuestSwap {
|
||||
/// The host's command permission, swapped for the guest's for its turn.
|
||||
permission: u8,
|
||||
xp: Experience,
|
||||
level_up_sound: horizon_client::entities::LevelUpSound,
|
||||
host_keys: horizon_client::held::HeldKeys,
|
||||
screen: Screen,
|
||||
host_sign: Option<super::sign_edit::SignEditor>,
|
||||
@@ -486,6 +489,7 @@ impl Game<'_> {
|
||||
total: g.xp_total,
|
||||
},
|
||||
);
|
||||
let level_up_sound = std::mem::replace(&mut self.level_up_sound, play.level_up_sound);
|
||||
// The host's held keys stay theirs: the guest holds only sneak, and a
|
||||
// screen the guest opens (`keys.clear()`) leaves the host walking.
|
||||
let host_keys = std::mem::take(&mut self.keys);
|
||||
@@ -617,6 +621,7 @@ impl Game<'_> {
|
||||
block_reach,
|
||||
permission,
|
||||
xp,
|
||||
level_up_sound,
|
||||
host_keys,
|
||||
screen,
|
||||
host_sign,
|
||||
@@ -717,6 +722,7 @@ impl Game<'_> {
|
||||
block_reach,
|
||||
permission,
|
||||
xp,
|
||||
level_up_sound,
|
||||
host_keys,
|
||||
screen,
|
||||
host_sign,
|
||||
@@ -966,6 +972,7 @@ impl Game<'_> {
|
||||
.insert(key, BlockEntity::Chest(self.ender_items.clone()));
|
||||
self.ender_open = Some(key);
|
||||
}
|
||||
play.level_up_sound = std::mem::replace(&mut self.level_up_sound, level_up_sound);
|
||||
let new_xp = std::mem::replace(&mut self.experience, xp);
|
||||
g.xp_level = new_xp.level;
|
||||
g.xp_progress = new_xp.progress;
|
||||
@@ -3539,6 +3546,7 @@ impl Game<'_> {
|
||||
// empty: the mods hear each worn item again from empty (`native_players_equipment`).
|
||||
if let Some(play) = host.play.get_mut(&conn) {
|
||||
play.equipment_heard = None;
|
||||
play.level_up_sound = Default::default();
|
||||
}
|
||||
// PlayerList.respawn: a server's player comes back in its respawn
|
||||
// point's level: the Overworld, where the world spawn and beds are,
|
||||
@@ -3702,6 +3710,13 @@ impl Game<'_> {
|
||||
self.tick_player_menus(host);
|
||||
self.native_players_equipment(host);
|
||||
self.announce_guest_advancements(host);
|
||||
// ServerLevel.tickNonPassenger calls Entity.commonTick for each player,
|
||||
// including spectators and players whose client has not loaded yet.
|
||||
for g in &host.hub.guests {
|
||||
if host.hub.here(g) && !self.on_server_trip(g.conn) {
|
||||
host.play.entry(g.conn).or_default().level_up_sound.tick();
|
||||
}
|
||||
}
|
||||
let conns: Vec<u32> = host
|
||||
.hub
|
||||
.guests
|
||||
|
||||
@@ -1000,14 +1000,12 @@ impl Game<'_> {
|
||||
xp.total = 0;
|
||||
}
|
||||
let level = xp.level;
|
||||
if let Some(volume) = horizon_client::entities::Experience::level_up_volume(
|
||||
levels,
|
||||
level,
|
||||
self.ticks,
|
||||
self.last_level_up,
|
||||
) {
|
||||
self.last_level_up = self.ticks;
|
||||
// playSound(null, feet): the player hears their own level-up.
|
||||
self.play_level_up(levels, level);
|
||||
self.native_player_level_changed(old_level);
|
||||
}
|
||||
/// `Player.giveExperienceLevels`: all players near its feet hear the sound.
|
||||
pub(super) fn play_level_up(&mut self, amount: i32, level: i32) {
|
||||
if let Some(volume) = self.level_up_sound.give_levels(amount, level) {
|
||||
let at = self
|
||||
.player
|
||||
.as_ref()
|
||||
@@ -1015,7 +1013,6 @@ impl Game<'_> {
|
||||
self.audio
|
||||
.play("minecraft:entity.player.levelup", at, volume, 1.0);
|
||||
}
|
||||
self.native_player_level_changed(old_level);
|
||||
}
|
||||
/// Whether the result slot may be taken (`mayPickup`).
|
||||
fn may_take_result(&self) -> bool {
|
||||
|
||||
@@ -13089,5 +13089,53 @@ CHECKS += ['pig-saddle-shears']
|
||||
CHECK_FNS['pig-saddle-shears'] = check_pig_saddle_shears
|
||||
|
||||
|
||||
def check_levelup_sounds(host, guest, out):
|
||||
"""Each server player has its own age and level-up cooldown, including point gains."""
|
||||
sound = 'minecraft:entity.player.levelup'
|
||||
guest('command', command=f'tp @s {host.player}')
|
||||
for player in (host, guest):
|
||||
player('command', command='xp set @s 0 levels')
|
||||
player('command', command='xp set @s 0 points')
|
||||
host.wait(120)
|
||||
before = {p.name: heard(p, sound) for p in (host, guest)}
|
||||
for player in (host, guest):
|
||||
command_says(player, 'xp add @s 5 levels')
|
||||
host.wait(10)
|
||||
first = {p.name: heard(p, sound) - before[p.name] for p in (host, guest)}
|
||||
assert first == {p.name: 2 for p in (host, guest)}, first
|
||||
command_says(host, 'xp add @s 5 levels')
|
||||
host.wait(5)
|
||||
cooldown = {p.name: heard(p, sound) - before[p.name] for p in (host, guest)}
|
||||
assert cooldown == first, cooldown
|
||||
host.wait(110)
|
||||
command_says(host, 'xp set @s 4 levels')
|
||||
command_says(host, 'xp set @s 0 points')
|
||||
command_says(host, 'xp add @s 15 points')
|
||||
host.wait(10)
|
||||
points = {p.name: heard(p, sound) - before[p.name] for p in (host, guest)}
|
||||
assert points == {p.name: 3 for p in (host, guest)}, points
|
||||
command_says(guest, 'spawnpoint @s ~ ~ ~')
|
||||
guest('command', command='kill @s')
|
||||
guest.wait(30)
|
||||
guest.raw('respawn')
|
||||
guest.wait(5)
|
||||
command_says(host, 'xp set Guest 0 levels')
|
||||
command_says(host, 'xp add Guest 5 levels')
|
||||
guest.wait(5)
|
||||
after_respawn = {p.name: heard(p, sound) - before[p.name] for p in (host, guest)}
|
||||
assert after_respawn == points, after_respawn
|
||||
guest.wait(110)
|
||||
command_says(host, 'xp add Guest 5 levels')
|
||||
guest.wait(10)
|
||||
aged = {p.name: heard(p, sound) - before[p.name] for p in (host, guest)}
|
||||
assert aged == {p.name: 4 for p in (host, guest)}, aged
|
||||
return {'independent': first, 'cooldown': cooldown, 'points': points,
|
||||
'respawn': after_respawn, 'respawn_aged': aged}
|
||||
|
||||
|
||||
CHECKS += ['levelup-sounds']
|
||||
CHECK_FNS['levelup-sounds'] = check_levelup_sounds
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in new issue
Block a user