Pumpkin/pumpkin/src/command/commands/playsound.rs
Alexander Medvedev 995edfb3c2 Add Effects
2025-02-22 14:17:06 +01:00

201 lines
7.6 KiB
Rust

use async_trait::async_trait;
use pumpkin_data::sound::SoundCategory;
use pumpkin_util::text::TextComponent;
use rand::{Rng, thread_rng};
use crate::command::{
CommandError, CommandExecutor, CommandSender,
args::{
Arg, ConsumedArgs, FindArg, bounded_num::BoundedNumArgumentConsumer,
players::PlayersArgumentConsumer, position_3d::Position3DArgumentConsumer,
sound::SoundArgumentConsumer, sound_category::SoundCategoryArgumentConsumer,
},
tree::CommandTree,
tree::builder::argument,
};
/// Command: playsound <sound> [<source>] [<targets>] [<pos>] [<volume>] [<pitch>] [<minVolume>]
///
/// Plays a sound at specified position for target players.
/// - sound: The sound identifier to play
/// - source: Sound category (master, music, record, etc.)
/// - targets: Players who will hear the sound
/// - pos: Position to play the sound from
/// - volume: Sound volume (>=0, default: 1.0)
/// - pitch: Sound pitch (0.5-2.0, default: 1.0)
/// - minVolume: Minimum volume for distant players (0.0-1.0, default: 0.0)
const NAMES: [&str; 1] = ["playsound"];
const DESCRIPTION: &str = "Plays a sound at a position.";
const ARG_SOUND: &str = "sound";
const ARG_SOURCE: &str = "source";
const ARG_TARGETS: &str = "targets";
const ARG_POS: &str = "pos";
const ARG_VOLUME: &str = "volume";
const ARG_PITCH: &str = "pitch";
const ARG_MIN_VOLUME: &str = "minVolume";
// Volume must be >= 0, no upper limit
fn volume_consumer() -> BoundedNumArgumentConsumer<f32> {
BoundedNumArgumentConsumer::new().name(ARG_VOLUME).min(0.0)
}
// Pitch must be between 0.0 and 2.0
// Values below 0.5 are treated as 0.5
fn pitch_consumer() -> BoundedNumArgumentConsumer<f32> {
BoundedNumArgumentConsumer::new()
.name(ARG_PITCH)
.min(0.0)
.max(2.0)
}
// Minimum volume must be between 0.0 and 1.0
// Controls the volume for players outside normal hearing range
fn min_volume_consumer() -> BoundedNumArgumentConsumer<f32> {
BoundedNumArgumentConsumer::new()
.name(ARG_MIN_VOLUME)
.min(0.0)
.max(1.0)
}
struct Executor;
#[async_trait]
impl CommandExecutor for Executor {
async fn execute<'a>(
&self,
sender: &mut CommandSender<'a>,
_server: &crate::server::Server,
args: &ConsumedArgs<'a>,
) -> Result<(), CommandError> {
// Get required sound argument
let sound = SoundArgumentConsumer::find_arg(args, ARG_SOUND)?;
// Get optional sound category, defaults to Master
let source = args
.get(ARG_SOURCE)
.map_or(SoundCategory::Master, |arg| match arg {
Arg::SoundCategory(category) => *category,
_ => SoundCategory::Master,
});
// Get target players, defaults to sender if not specified
let targets = if let Ok(players) = PlayersArgumentConsumer::find_arg(args, ARG_TARGETS) {
players
} else if let Some(player) = sender.as_player() {
&[player.clone()]
} else {
return Ok(());
};
// Get optional position, defaults to target's position
let position = Position3DArgumentConsumer::find_arg(args, ARG_POS).ok();
// Get optional volume parameter
let volume = match BoundedNumArgumentConsumer::<f32>::find_arg(args, ARG_VOLUME) {
Ok(Ok(v)) => v,
_ => 1.0, // Default volume
};
// Get optional pitch parameter
let pitch = match BoundedNumArgumentConsumer::<f32>::find_arg(args, ARG_PITCH) {
Ok(Ok(p)) => p.max(0.5), // Values below 0.5 are clamped
_ => 1.0, // Default pitch
};
// Get optional minimum volume (currently unused in implementation)
let _min_volume = match BoundedNumArgumentConsumer::<f32>::find_arg(args, ARG_MIN_VOLUME) {
Ok(Ok(v)) => v,
_ => 0.0, // Default minimum volume
};
// Use same random seed for all targets to ensure sound synchronization
let seed = thread_rng().r#gen::<f64>();
// Track how many players actually received the sound
let mut players_who_heard = 0;
// Play sound for each target player
for target in targets {
let pos = position.unwrap_or(target.living_entity.entity.pos.load());
// Check if player can hear the sound based on volume and distance
let player_pos = target.living_entity.entity.pos.load();
let distance = player_pos.squared_distance_to_vec(pos);
let max_distance = 16.0 * volume; // 16 blocks is base distance at volume 1.0
if distance <= max_distance.into() || _min_volume > 0.0 {
target
.play_sound(sound as u16, source, &pos, volume, pitch, seed)
.await;
players_who_heard += 1;
}
}
// Send appropriate message based on results
if players_who_heard == 0 {
sender
.send_message(TextComponent::translate("commands.playsound.failed", []))
.await;
} else {
let sound_name = sound.to_name();
if players_who_heard == 1 {
sender
.send_message(TextComponent::translate(
"commands.playsound.success.single",
[
TextComponent::text(sound_name),
TextComponent::text(targets[0].gameprofile.name.clone()),
],
))
.await;
} else {
sender
.send_message(TextComponent::translate(
"commands.playsound.success.multiple",
[
TextComponent::text(sound_name),
TextComponent::text(players_who_heard.to_string()),
],
))
.await;
}
}
Ok(())
}
}
pub fn init_command_tree() -> CommandTree {
CommandTree::new(NAMES, DESCRIPTION).then(
argument(ARG_SOUND, SoundArgumentConsumer)
.then(
argument(ARG_SOURCE, SoundCategoryArgumentConsumer)
.then(
argument(ARG_TARGETS, PlayersArgumentConsumer)
.then(
argument(ARG_POS, Position3DArgumentConsumer)
.then(
argument(ARG_VOLUME, volume_consumer())
.then(
argument(ARG_PITCH, pitch_consumer())
.then(
argument(
ARG_MIN_VOLUME,
min_volume_consumer(),
)
.execute(Executor),
)
.execute(Executor),
)
.execute(Executor),
)
.execute(Executor),
)
.execute(Executor),
)
.execute(Executor),
)
.execute(Executor),
)
}