Refactor DimensionScheduler to support sound settings in message groups and effects
This commit is contained in:
1 parent
23d8fc85de
commit
e558ae3fac
4 files changed
+128
-22
No files matched your search
+65
-10
@@ -3,6 +3,9 @@ package eu.mhsl.craftattack.spawn.common.appliances.tooling.dimensionScheduler;
|
||||
import eu.mhsl.craftattack.spawn.core.Main;
|
||||
import eu.mhsl.craftattack.spawn.core.appliance.Appliance;
|
||||
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand;
|
||||
import net.kyori.adventure.key.InvalidKeyException;
|
||||
import net.kyori.adventure.key.Key;
|
||||
import net.kyori.adventure.sound.Sound;
|
||||
import org.bukkit.configuration.ConfigurationSection;
|
||||
import org.bukkit.configuration.InvalidConfigurationException;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
@@ -26,16 +29,26 @@ import java.util.logging.Level;
|
||||
|
||||
/** Reads named times, message groups and effect definitions; execution belongs to their consumers. */
|
||||
public class DimensionScheduler extends Appliance {
|
||||
public record Effect(String name, int minutesBeforeOpening, String message) {
|
||||
public record SoundSettings(String name, float volume, float pitch) {
|
||||
public Sound asSound() {
|
||||
return Sound.sound(Key.key(this.name), Sound.Source.MASTER, this.volume, this.pitch);
|
||||
}
|
||||
}
|
||||
|
||||
public record MessageGroup(List<String> texts, Optional<SoundSettings> sound) {
|
||||
}
|
||||
|
||||
public record Effect(String name, int minutesBeforeOpening, String message, Optional<SoundSettings> sound) {
|
||||
public Duration leadTime() {
|
||||
return Duration.ofMinutes(this.minutesBeforeOpening);
|
||||
}
|
||||
}
|
||||
|
||||
private static final String FILE_NAME = "dimension-schedule.yml";
|
||||
private static final MessageGroup EMPTY_MESSAGE_GROUP = new MessageGroup(List.of(), Optional.empty());
|
||||
private final File scheduleFile = new File(Main.instance().getDataFolder(), FILE_NAME);
|
||||
private Map<String, Instant> times = Map.of();
|
||||
private Map<String, List<String>> messages = Map.of();
|
||||
private Map<String, MessageGroup> messages = Map.of();
|
||||
private Map<String, List<Effect>> effects = Map.of();
|
||||
private Instant loadedAt = Instant.now();
|
||||
|
||||
@@ -65,7 +78,7 @@ public class DimensionScheduler extends Appliance {
|
||||
}
|
||||
ConfigurationSection messageSection = section(configuration, "messages");
|
||||
ConfigurationSection effectSection = section(configuration, "effects");
|
||||
Map<String, List<String>> loadedMessages = readMessages(messageSection == null ? section(defaults, "messages") : messageSection);
|
||||
Map<String, MessageGroup> loadedMessages = readMessages(messageSection == null ? section(defaults, "messages") : messageSection);
|
||||
Map<String, List<Effect>> loadedEffects = readEffects(effectSection == null ? section(defaults, "effects") : effectSection);
|
||||
|
||||
this.times = Map.copyOf(loadedTimes);
|
||||
@@ -90,12 +103,18 @@ public class DimensionScheduler extends Appliance {
|
||||
throw new IllegalArgumentException(parent.getCurrentPath() + "." + key + " muss ein YAML-Abschnitt sein.");
|
||||
}
|
||||
|
||||
private static Map<String, List<String>> readMessages(ConfigurationSection section) {
|
||||
private static Map<String, MessageGroup> readMessages(ConfigurationSection section) {
|
||||
if(section == null) return Map.of();
|
||||
Map<String, List<String>> messages = new HashMap<>();
|
||||
Map<String, MessageGroup> messages = new HashMap<>();
|
||||
for(Map.Entry<String, Object> entry : section.getValues(false).entrySet()) {
|
||||
if(!(entry.getValue() instanceof List<?> values)) {
|
||||
throw new IllegalArgumentException("messages." + entry.getKey() + " muss eine Liste von Texten sein.");
|
||||
Object configured = entry.getValue();
|
||||
Optional<SoundSettings> sound = Optional.empty();
|
||||
if(configured instanceof ConfigurationSection group) {
|
||||
configured = group.get("texts");
|
||||
sound = readSound(group);
|
||||
}
|
||||
if(!(configured instanceof List<?> values)) {
|
||||
throw new IllegalArgumentException("messages." + entry.getKey() + " muss eine Textliste oder ein Abschnitt mit texts sein.");
|
||||
}
|
||||
List<String> texts = new ArrayList<>();
|
||||
for(Object value : values) {
|
||||
@@ -104,7 +123,7 @@ public class DimensionScheduler extends Appliance {
|
||||
}
|
||||
texts.add(text);
|
||||
}
|
||||
messages.put(entry.getKey(), List.copyOf(texts));
|
||||
messages.put(entry.getKey(), new MessageGroup(List.copyOf(texts), sound));
|
||||
}
|
||||
return Map.copyOf(messages);
|
||||
}
|
||||
@@ -127,13 +146,45 @@ public class DimensionScheduler extends Appliance {
|
||||
if(!(message instanceof String text) || text.isBlank()) {
|
||||
throw new IllegalArgumentException(effect.getCurrentPath() + ".message muss ein nicht leerer Text sein.");
|
||||
}
|
||||
definitions.add(new Effect(name, count, text));
|
||||
definitions.add(new Effect(name, count, text, readSound(effect)));
|
||||
}
|
||||
effects.put(groupName, List.copyOf(definitions));
|
||||
}
|
||||
return Map.copyOf(effects);
|
||||
}
|
||||
|
||||
private static Optional<SoundSettings> readSound(ConfigurationSection section) {
|
||||
Object configured = section.get("sound");
|
||||
if(configured == null) return Optional.empty();
|
||||
if(!(configured instanceof String name)) {
|
||||
throw new IllegalArgumentException(section.getCurrentPath() + ".sound muss ein Sound-Name sein.");
|
||||
}
|
||||
if(name.isBlank()) return Optional.empty();
|
||||
String key;
|
||||
try {
|
||||
key = Key.key(name.trim()).asString();
|
||||
} catch(InvalidKeyException e) {
|
||||
throw new IllegalArgumentException("Ungültiger Sound-Name bei " + section.getCurrentPath() + ".sound: " + name, e);
|
||||
}
|
||||
float volume = readSoundNumber(section, "volume", true);
|
||||
float pitch = readSoundNumber(section, "pitch", false);
|
||||
return Optional.of(new SoundSettings(key, volume, pitch));
|
||||
}
|
||||
|
||||
private static float readSoundNumber(ConfigurationSection section, String key, boolean allowZero) {
|
||||
Object configured = section.get(key);
|
||||
if(configured == null) return 1.0f;
|
||||
if(!(configured instanceof Number number)) {
|
||||
throw new IllegalArgumentException(section.getCurrentPath() + "." + key + " muss eine Zahl sein.");
|
||||
}
|
||||
float value = number.floatValue();
|
||||
if(!Float.isFinite(value) || (allowZero ? value < 0 : value <= 0)) {
|
||||
throw new IllegalArgumentException(section.getCurrentPath() + "." + key
|
||||
+ (allowZero ? " muss endlich und mindestens 0 sein." : " muss endlich und größer als 0 sein."));
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private static Optional<Instant> readTime(String key, Object configured) {
|
||||
if(configured == null) return Optional.empty();
|
||||
if(!(configured instanceof String value)) {
|
||||
@@ -157,7 +208,11 @@ public class DimensionScheduler extends Appliance {
|
||||
}
|
||||
|
||||
public List<String> getMessages(String name) {
|
||||
return this.messages.getOrDefault(name, List.of());
|
||||
return this.getMessageGroup(name).texts();
|
||||
}
|
||||
|
||||
public MessageGroup getMessageGroup(String name) {
|
||||
return this.messages.getOrDefault(name, EMPTY_MESSAGE_GROUP);
|
||||
}
|
||||
|
||||
public List<Effect> getEffects(String name) {
|
||||
|
||||
+23
-3
@@ -22,20 +22,40 @@ times:
|
||||
|
||||
messages:
|
||||
end-activation-denied:
|
||||
- "Die Augen scheinen dich für einen kurzen Moment anzustarren. Dir läuft ein kalter Schauer über den Rücken ..."
|
||||
texts:
|
||||
- "Die Augen scheinen dich für einen kurzen Moment anzustarren. Dir läuft ein kalter Schauer über den Rücken ..."
|
||||
sound: "entity.warden.roar"
|
||||
pitch: 0.2
|
||||
volume: 1.0
|
||||
|
||||
effects:
|
||||
end-opening:
|
||||
five-minutes-before:
|
||||
minutes-before-opening: 5
|
||||
message: "Etwas regt sich in der Leere ..."
|
||||
sound: "ambient.cave"
|
||||
pitch: 0.5
|
||||
volume: 1.0
|
||||
opened:
|
||||
minutes-before-opening: 0
|
||||
message: "Der Weg ins End ist nun offen!"
|
||||
sound: "block.end_portal.spawn"
|
||||
pitch: 1.0
|
||||
volume: 1.0
|
||||
```
|
||||
|
||||
`times` kann beliebige benannte Zeitpunkte enthalten. `messages.end-activation-denied`
|
||||
`times` kann beliebige benannte Zeitpunkte enthalten. `messages.end-activation-denied.texts`
|
||||
enthält beliebig viele Texte für die zufällige private Chat-Nachricht.
|
||||
Die bisherige direkte Textliste wird ebenfalls weiterhin gelesen.
|
||||
`sound`, `pitch` und `volume` stehen direkt bei den zugehörigen Texten beziehungsweise Effekten.
|
||||
Diese Sound-Angaben werden beim Reload mit eingelesen und direkt mit der jeweiligen Nachricht
|
||||
einmal beim Empfänger abgespielt. Der Ablehnungs-Sound ist nur für den auslösenden Spieler hörbar;
|
||||
Ankündigungs- und Öffnungssounds erhalten alle Empfänger der Chat-Nachricht jeweils an ihrer
|
||||
eigenen Position. Es gibt keine zusätzlichen Tasks oder Schleifen für die Sound-Wiedergabe.
|
||||
Ohne `sound` oder mit leerem Sound-Namen bleibt es bei der Textausgabe. `volume` und `pitch`
|
||||
haben den Standardwert `1.0`; Lautstärke muss mindestens `0`, Pitch größer als `0` sein.
|
||||
Ungültige Sound-Schlüssel oder nicht endliche Zahlen lassen den Reload wie andere ungültige
|
||||
Konfigurationswerte scheitern und behalten die bisherige Konfiguration.
|
||||
Unter `effects.end-opening` können frei benannte Effekte ergänzt oder entfernt werden.
|
||||
`minutes-before-opening` ist eine nicht negative ganze Zahl: Ein positiver Wert gibt den
|
||||
Abstand zur End-Öffnung in Minuten an, `0` kennzeichnet einen Effekt beim tatsächlichen Öffnen.
|
||||
@@ -124,7 +144,7 @@ Alle Flavourtexte und zeitgesteuerten Chat-Effekte verwenden einheitlich End-Gr
|
||||
|
||||
## Erweiterungen und Leistung
|
||||
|
||||
Zusätzliche Sounds oder andere Effektarten können später zentral in `EndOpeningEffects.play(effect)`
|
||||
Weitere Effektarten können später zentral in `EndOpeningEffects.play(effect)`
|
||||
ergänzt werden und werden bei manueller sowie zeitgesteuerter Öffnung gleichermaßen ausgeführt.
|
||||
Neue Ankündigungen benötigen nur einen weiteren YAML-Eintrag mit Minutenwert und Text.
|
||||
|
||||
|
||||
+15
-4
@@ -2,16 +2,20 @@ package eu.mhsl.craftattack.spawn.common.appliances.tooling.endPrevent;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.tooling.dimensionScheduler.DimensionScheduler;
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.tooling.dimensionScheduler.DimensionScheduler.Effect;
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.tooling.dimensionScheduler.DimensionScheduler.MessageGroup;
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.tooling.dimensionScheduler.DimensionScheduler.SoundSettings;
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.tooling.dimensionScheduler.DimensionScheduleText;
|
||||
import net.kyori.adventure.sound.Sound;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
/** Presents YAML-defined End effects. Add sounds or other effects in play(). */
|
||||
/** Sends each YAML-defined message together with its optional sound. */
|
||||
final class EndOpeningEffects {
|
||||
private @Nullable DimensionScheduler scheduler;
|
||||
|
||||
@@ -25,10 +29,11 @@ final class EndOpeningEffects {
|
||||
|
||||
void denyActivation(Player player) {
|
||||
if(this.scheduler == null) return;
|
||||
List<String> messages = this.scheduler.getMessages("end-activation-denied");
|
||||
MessageGroup group = this.scheduler.getMessageGroup("end-activation-denied");
|
||||
List<String> messages = group.texts();
|
||||
if(messages.isEmpty()) return;
|
||||
String message = messages.get(ThreadLocalRandom.current().nextInt(messages.size()));
|
||||
player.sendMessage(DimensionScheduleText.message(message));
|
||||
send(player, DimensionScheduleText.message(message), group.sound().map(SoundSettings::asSound));
|
||||
}
|
||||
|
||||
void playOpening() {
|
||||
@@ -39,6 +44,12 @@ final class EndOpeningEffects {
|
||||
|
||||
void play(Effect effect) {
|
||||
Component message = DimensionScheduleText.message(effect.message());
|
||||
Bukkit.getOnlinePlayers().forEach(player -> player.sendMessage(message));
|
||||
Optional<Sound> sound = effect.sound().map(SoundSettings::asSound);
|
||||
Bukkit.getOnlinePlayers().forEach(player -> send(player, message, sound));
|
||||
}
|
||||
|
||||
private static void send(Player player, Component message, Optional<Sound> sound) {
|
||||
player.sendMessage(message);
|
||||
sound.ifPresent(value -> player.playSound(value, Sound.Emitter.self()));
|
||||
}
|
||||
}
|
||||
@@ -7,11 +7,16 @@ times:
|
||||
|
||||
messages:
|
||||
end-activation-denied:
|
||||
- "Die Augen scheinen dich für einen kurzen Moment anzustarren. Dir läuft ein kalter Schauer über den Rücken ..."
|
||||
- "Der Rahmen fühlt sich ungewöhnlich kalt an. Du hast das Gefühl, als würde er dir die Wärme aus den Fingern ziehen …"
|
||||
- "Die Luft über dem leeren Rahmen scheint ungewöhnlich dunkel. Du hast das Gefühl, in einen bodenlosen Abgrund zu blicken ..."
|
||||
- "Für einen kurzen Moment scheint die Umgebung vollkommen still zu werden. Du fragst dich, ob du dir das nur eingebildet hast ..."
|
||||
- "Ein schwaches, violettes Licht flackert zwischen den Augen auf. Etwas scheint dich zu beobachten ..."
|
||||
texts:
|
||||
- "Die Augen scheinen dich für einen kurzen Moment anzustarren. Dir läuft ein kalter Schauer über den Rücken ..."
|
||||
- "Der Rahmen fühlt sich ungewöhnlich kalt an. Du hast das Gefühl, als würde er dir die Wärme aus den Fingern ziehen …"
|
||||
- "Die Luft über dem leeren Rahmen scheint ungewöhnlich dunkel. Du hast das Gefühl, in einen bodenlosen Abgrund zu blicken ..."
|
||||
- "Für einen kurzen Moment scheint die Umgebung vollkommen still zu werden. Du fragst dich, ob du dir das nur eingebildet hast ..."
|
||||
- "Ein schwaches, violettes Licht flackert zwischen den Augen auf. Etwas scheint dich zu beobachten ..."
|
||||
# Ein Sound pro Nachricht, gemeinsam mit dem Nachrichten-Cooldown.
|
||||
sound: "entity.warden.roar"
|
||||
pitch: 0.2
|
||||
volume: 1.0
|
||||
|
||||
effects:
|
||||
end-opening:
|
||||
@@ -20,16 +25,31 @@ effects:
|
||||
four-hours-before:
|
||||
minutes-before-opening: 240
|
||||
message: "Eine unheimliche Stille scheint sich langsam über die Welt auszubreiten. Du hast das seltsame Gefühl, beobachtet zu werden ..."
|
||||
sound: "ambient.cave"
|
||||
pitch: 0.5
|
||||
volume: 1.0
|
||||
one-hour-before:
|
||||
minutes-before-opening: 60
|
||||
message: "Die Luft fühlt sich plötzlich ungewöhnlich schwer an. Du hast das Gefühl, als würde eine unsichtbare Kraft auf dir lasten ..."
|
||||
sound: "ambient.cave"
|
||||
pitch: 0.5
|
||||
volume: 1.0
|
||||
ten-minutes-before:
|
||||
minutes-before-opening: 10
|
||||
message: "Ein seltsames Geräusch hallt durch die Welt. Könnte es das Schlagen gewaltiger Flügel sein ...?"
|
||||
sound: "ambient.cave"
|
||||
pitch: 0.5
|
||||
volume: 1.0
|
||||
one-minute-before:
|
||||
minutes-before-opening: 1
|
||||
message: "Ein gewaltiger Schatten scheint über die Welt zu ziehen. Du blickst zum Himmel, doch dort ist nichts zu sehen ..."
|
||||
sound: "entity.ender_dragon.growl"
|
||||
pitch: 0.5
|
||||
volume: 1.0
|
||||
# 0: Nur beim tatsächlichen Öffnen, gleichermaßen durch Command oder Zeitplan.
|
||||
opened:
|
||||
minutes-before-opening: 0
|
||||
message: "Ein gewaltiger Riss öffnet sich in der Realität! Die Leere erwacht und die letzte Grenze fällt. Der Weg ins End ist nun offen!"
|
||||
sound: "block.end_portal.spawn"
|
||||
pitch: 1.0
|
||||
volume: 1.0
|
||||
Reference in new issue
Block a user