001/*
002 * Copyright (c) 2023. JEFF Media GbR / mfnalex et al.
003 *
004 * This program is free software: you can redistribute it and/or modify
005 * it under the terms of the GNU General Public License as published by
006 * the Free Software Foundation, either version 3 of the License, or
007 * (at your option) any later version.
008 *
009 * This program is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
012 * GNU General Public License for more details.
013 *
014 * You should have received a copy of the GNU General Public License
015 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
016 */
017
018package com.jeff_media.jefflib.data;
019
020import com.google.common.base.Enums;
021import com.jeff_media.jefflib.JeffLib;
022import com.jeff_media.jefflib.NumberUtils;
023import java.util.Locale;
024import java.util.Objects;
025import lombok.AllArgsConstructor;
026import lombok.Builder;
027import lombok.Data;
028import lombok.RequiredArgsConstructor;
029import org.bukkit.Location;
030import org.bukkit.Sound;
031import org.bukkit.SoundCategory;
032import org.bukkit.configuration.ConfigurationSection;
033import org.bukkit.entity.Player;
034import org.jetbrains.annotations.NotNull;
035import org.jetbrains.annotations.Nullable;
036
037// TODO: Test whether something broke after switching from Sound to String
038
039/**
040 * Data class to wrap all information needed to play a sound
041 */
042@Data
043@Builder
044@RequiredArgsConstructor
045@AllArgsConstructor
046public final class SoundData {
047
048    /**
049     *
050     */
051    private final String sound;
052    private float volume = 1;
053    private float pitch = 1;
054    private float pitchVariant;
055    private SoundCategory soundCategory = SoundCategory.MASTER;
056
057    /**
058     * Parses a {@link ConfigurationSection} into a {@link SoundData} object.
059     * <p>
060     * <b>Example:</b>
061     * <pre>
062     * SoundData sound = SoundData.fromConfigurationSection(getConfig().getConfigurationSection("my_sound",null);
063     * </pre>
064     * <p>
065     * <b>Example YAML:</b>
066     * <pre>
067     * my_sound:
068     *  effect: AMBIENT_CAVE    # case insensitive for builtin sounds, CaSe SeNsItIvE for custom sounds
069     *  volume: 0.5            # optional, default 1
070     *  pitch: 1.5             # optional, default 1
071     *  pitch-variant: 0.5     # optional, default 0. If this is set to 0.5, and pitch is set to 1.5, the sound will play with a pitch between 1 and 2
072     *  sound-category: MASTER # optional, case insensitive, default MASTER
073     *  </pre>
074     *
075     * @param config ConfigurationSection to parse
076     * @param prefix Prefix to use when looking up values in the ConfigurationSection. If non-null, this prefix is prefixed to all keys. For example, if this is set to "foo-", then the keys to look up are "foo-sound", "foo-volume", etc.
077     * @throws IllegalArgumentException When no sound is defined, or if the sound category is not valid.
078     */
079    public static SoundData fromConfigurationSection(@NotNull final ConfigurationSection config, @Nullable String prefix) throws IllegalArgumentException {
080        if (prefix == null) prefix = "";
081        String soundName = config.getString(prefix + "effect");
082        if (soundName == null || soundName.isEmpty()) {
083            throw new IllegalArgumentException("No sound effect defined");
084        }
085        final Sound sound = Enums.getIfPresent(Sound.class, soundName.toUpperCase(Locale.ROOT)).orNull();
086        if (sound != null) {
087            soundName = sound.name();
088        }
089        final float volume = (float) config.getDouble(prefix + "volume", 1.0D);
090        final float pitch = (float) config.getDouble(prefix + "pitch", 1.0D);
091        final float pitchVariant = (float) config.getDouble(prefix + "pitch-variant", 1.0D);
092        final String soundCategoryName = config.getString(prefix + "sound-category", SoundCategory.MASTER.name()).toUpperCase(Locale.ROOT);
093        final SoundCategory soundCategory = Enums.getIfPresent(SoundCategory.class, soundCategoryName.toUpperCase()).orNull();
094        if (soundCategory == null) {
095            throw new IllegalArgumentException("Unknown sound category: " + soundCategoryName);
096        }
097        try {
098            Sound tmpSound = Sound.valueOf(soundName);
099            soundName = tmpSound.getKey().getKey();
100        } catch (IllegalArgumentException ignored) {
101            //throw new IllegalArgumentException("Unknown sound: " + soundName);
102        }
103        return new SoundData(soundName.toLowerCase(Locale.ROOT), volume, pitch, pitchVariant, soundCategory);
104    }
105
106    /**
107     * Plays the sound only to the given player
108     *
109     * @param player Player
110     */
111    public void playToPlayer(final Player player) {
112        player.playSound(player.getLocation(), sound, soundCategory, volume, getFinalPitch());
113    }
114
115    private float getFinalPitch() {
116        if (NumberUtils.isZeroOrNegative(pitchVariant)) return pitch;
117        return (float) (pitch - (pitchVariant / 2) + JeffLib.getThreadLocalRandom().nextDouble(0, pitchVariant));
118    }
119
120    /**
121     * Plays the sound only to the given player, at the given location
122     *
123     * @param player   Player
124     * @param location Location
125     */
126    public void playToPlayer(final Player player, final Location location) {
127        player.playSound(location, sound, soundCategory, volume, getFinalPitch());
128    }
129
130    /**
131     * Plays the sound to all players in the world, at the given location
132     *
133     * @param location Location
134     */
135    public void playToWorld(final Location location) {
136        Objects.requireNonNull(location.getWorld()).playSound(location, sound, soundCategory, volume, getFinalPitch());
137    }
138
139}
140