diff --git a/common/src/main/java/net/onelitefeather/cygnus/common/config/GameConfig.java b/common/src/main/java/net/onelitefeather/cygnus/common/config/GameConfig.java index eb9d4ca3..8c48f2dc 100644 --- a/common/src/main/java/net/onelitefeather/cygnus/common/config/GameConfig.java +++ b/common/src/main/java/net/onelitefeather/cygnus/common/config/GameConfig.java @@ -88,39 +88,20 @@ public sealed interface GameConfig permits GameConfigImpl, InternalGameConfig { Key DEFAULT_DAMAGE_SOUND = Key.key("entity.player.hurt"); /** - * The static the slender hears while the survivors take his pages away. + * The {@link #pageGlitchPulseSeconds()} a configuration gets when it says nothing. *

- * A resource pack sound rather than a vanilla one: three 2.2 second takes of tape hiss the - * client picks between, high-passed at 520 Hz so the effect's own pitch drop to 0.7 leaves - * it hissing rather than humming. Nothing in vanilla comes close - rain is the nearest, and - * it reads as weather. - *

- *

- * A server running without the Cygnus pack therefore hears nothing here. That is the right - * way round: the static is a horror cue, and half of one played through the wrong sample is - * worse than none. + * Long enough that a find registers as its own event on screen, short enough that the slender + * is not looking through it while he chases somebody. *

*/ - Key DEFAULT_SLENDER_STATIC_SOUND = Key.key("cygnus", "vhs_static"); - - /** The {@link #slenderStaticQuietInterval()} a configuration gets when it says nothing. */ - int DEFAULT_SLENDER_STATIC_QUIET_INTERVAL = 12; - - /** The {@link #slenderStaticFranticInterval()} a configuration gets when it says nothing. */ - int DEFAULT_SLENDER_STATIC_FRANTIC_INTERVAL = 3; + int DEFAULT_PAGE_GLITCH_PULSE_SECONDS = 3; /** - * The longest {@link #slenderStaticQuietInterval()} a configuration may ask for. Past this a - * round could end before the slender has heard the static twice, which makes it noise rather - * than a clock. + * The longest {@link #pageGlitchPulseSeconds()} a configuration may ask for. Past this a find + * late in the round would still be on his screen when the next one lands, and the pulse stops + * being a pulse. */ - int MAX_SLENDER_STATIC_INTERVAL = 120; - - /** The {@link #slenderStaticMinVolume()} a configuration gets when it says nothing. */ - float DEFAULT_SLENDER_STATIC_MIN_VOLUME = 0.15F; - - /** The {@link #slenderStaticMaxVolume()} a configuration gets when it says nothing. */ - float DEFAULT_SLENDER_STATIC_MAX_VOLUME = 0.8F; + int MAX_PAGE_GLITCH_PULSE_SECONDS = 30; /** * The largest {@link #glitchRange()} a configuration may ask for. Beyond this the slender would @@ -334,60 +315,24 @@ static Builder builder() { Key damageSound(); /** - * Returns whether the slender hears static as the survivors collect his pages. - * - * @return {@code true} while the static is on - * @since 2.14.0 - */ - boolean slenderStaticEnabled(); - - /** - * Returns the sound the static is built from. + * Returns whether the slender's own screen tears as the survivors collect his pages. *

- * The key is not resolved against the sound registry here: a resource pack sound is a perfectly - * good answer and would not be found in it. It is sent as named. + * The effect rides the same channel as the gaze glitch and is therefore drawn by the resource + * pack, which is why it is gated by the overlays as well as by this setting. *

* - * @return the sound key, never {@code null} - * @since 2.14.0 - */ - Key slenderStaticSound(); - - /** - * Returns how many seconds lie between two bursts while no page has been found. - * - * @return the interval in seconds, at most {@link #MAX_SLENDER_STATIC_INTERVAL} - * @since 2.14.0 - */ - int slenderStaticQuietInterval(); - - /** - * Returns how many seconds lie between two bursts once every page is gone. - *

- * The gap shrinks from {@link #slenderStaticQuietInterval()} towards this value as the pages - * disappear, which is what tells the slender how late in the round he is. - *

- * - * @return the interval in seconds, below {@link #slenderStaticQuietInterval()} - * @since 2.14.0 - */ - int slenderStaticFranticInterval(); - - /** - * Returns how loud the static is while no page has been found. - * - * @return the volume, between 0 and {@link #slenderStaticMaxVolume()} - * @since 2.14.0 + * @return {@code true} while the page glitch is on + * @since 2.15.0 */ - float slenderStaticMinVolume(); + boolean pageGlitchEnabled(); /** - * Returns how loud the static is once every page is gone. + * Returns how long a single find holds the glitch one level above where the round stands. * - * @return the volume, at most 1 - * @since 2.14.0 + * @return the pulse in seconds, at most {@link #MAX_PAGE_GLITCH_PULSE_SECONDS} + * @since 2.15.0 */ - float slenderStaticMaxVolume(); + int pageGlitchPulseSeconds(); /** * Returns how close the slender has to be before the sight of him tears a survivor's view. @@ -596,63 +541,24 @@ sealed interface Builder permits GameConfigBuilder { Builder damageSound(Key damageSound); /** - * Sets whether the slender hears static as the survivors collect his pages. - * - * @param slenderStaticEnabled {@code true} to keep the static on - * @return the builder instance - * @since 2.14.0 - */ - Builder slenderStaticEnabled(boolean slenderStaticEnabled); - - /** - * Sets the sound the static is built from. - * - * @param slenderStaticSound the sound key - * @return the builder instance - * @since 2.14.0 - */ - Builder slenderStaticSound(Key slenderStaticSound); - - /** - * Sets how many seconds lie between two bursts while no page has been found. - * - * @param slenderStaticQuietInterval the interval in seconds - * @return the builder instance - * @throws IllegalArgumentException if the interval is below 1 or above - * {@link GameConfig#MAX_SLENDER_STATIC_INTERVAL} - * @since 2.14.0 - */ - Builder slenderStaticQuietInterval(int slenderStaticQuietInterval); - - /** - * Sets how many seconds lie between two bursts once every page is gone. - * - * @param slenderStaticFranticInterval the interval in seconds - * @return the builder instance - * @throws IllegalArgumentException if the interval is below 1 - * @since 2.14.0 - */ - Builder slenderStaticFranticInterval(int slenderStaticFranticInterval); - - /** - * Sets how loud the static is while no page has been found. + * Sets whether the slender's own screen tears as the survivors collect his pages. * - * @param slenderStaticMinVolume the volume + * @param pageGlitchEnabled {@code true} to keep the page glitch on * @return the builder instance - * @throws IllegalArgumentException if the volume is below 0 or above 1 - * @since 2.14.0 + * @since 2.15.0 */ - Builder slenderStaticMinVolume(float slenderStaticMinVolume); + Builder pageGlitchEnabled(boolean pageGlitchEnabled); /** - * Sets how loud the static is once every page is gone. + * Sets how long a single find holds the glitch above where the round stands. * - * @param slenderStaticMaxVolume the volume + * @param pageGlitchPulseSeconds the pulse in seconds * @return the builder instance - * @throws IllegalArgumentException if the volume is below 0 or above 1 - * @since 2.14.0 + * @throws IllegalArgumentException if the pulse is below 1 or above + * {@link GameConfig#MAX_PAGE_GLITCH_PULSE_SECONDS} + * @since 2.15.0 */ - Builder slenderStaticMaxVolume(float slenderStaticMaxVolume); + Builder pageGlitchPulseSeconds(int pageGlitchPulseSeconds); /** * Sets how close the slender has to be before the sight of him tears a survivor's view. diff --git a/common/src/main/java/net/onelitefeather/cygnus/common/config/GameConfigBuilder.java b/common/src/main/java/net/onelitefeather/cygnus/common/config/GameConfigBuilder.java index 8771919f..b0b9686d 100644 --- a/common/src/main/java/net/onelitefeather/cygnus/common/config/GameConfigBuilder.java +++ b/common/src/main/java/net/onelitefeather/cygnus/common/config/GameConfigBuilder.java @@ -38,15 +38,8 @@ public final class GameConfigBuilder implements GameConfig.Builder { private int glitchRange = GameConfig.DEFAULT_GLITCH_RANGE; private int glitchCloseRange = GameConfig.DEFAULT_GLITCH_CLOSE_RANGE; private int glitchViewAngle = GameConfig.DEFAULT_GLITCH_VIEW_ANGLE; - private boolean slenderStaticEnabled; - private Key slenderStaticSound = GameConfig.DEFAULT_SLENDER_STATIC_SOUND; - // Pre-set for the same reason as the glitch distances above: build() checks the two intervals - // and the two volumes against each other, and a builder that was never told about the static - // would trip those checks on a pair of zeroes. - private int slenderStaticQuietInterval = GameConfig.DEFAULT_SLENDER_STATIC_QUIET_INTERVAL; - private int slenderStaticFranticInterval = GameConfig.DEFAULT_SLENDER_STATIC_FRANTIC_INTERVAL; - private float slenderStaticMinVolume = GameConfig.DEFAULT_SLENDER_STATIC_MIN_VOLUME; - private float slenderStaticMaxVolume = GameConfig.DEFAULT_SLENDER_STATIC_MAX_VOLUME; + private boolean pageGlitchEnabled; + private int pageGlitchPulseSeconds = GameConfig.DEFAULT_PAGE_GLITCH_PULSE_SECONDS; @Override public GameConfig.Builder minPlayers(int minPlayers) { @@ -207,66 +200,22 @@ public GameConfig.Builder glitchViewAngle(int glitchViewAngle) { } @Override - public GameConfig.Builder slenderStaticEnabled(boolean slenderStaticEnabled) { - this.slenderStaticEnabled = slenderStaticEnabled; + public GameConfig.Builder pageGlitchEnabled(boolean pageGlitchEnabled) { + this.pageGlitchEnabled = pageGlitchEnabled; return this; } @Override - public GameConfig.Builder slenderStaticSound(Key slenderStaticSound) { - this.slenderStaticSound = slenderStaticSound; - return this; - } - - @Override - public GameConfig.Builder slenderStaticQuietInterval(int slenderStaticQuietInterval) { - if (slenderStaticQuietInterval < 1 - || slenderStaticQuietInterval > GameConfig.MAX_SLENDER_STATIC_INTERVAL) { + public GameConfig.Builder pageGlitchPulseSeconds(int pageGlitchPulseSeconds) { + if (pageGlitchPulseSeconds < 1 || pageGlitchPulseSeconds > GameConfig.MAX_PAGE_GLITCH_PULSE_SECONDS) { throw new IllegalArgumentException( - "Slender static quiet interval must be between 1 and " - + GameConfig.MAX_SLENDER_STATIC_INTERVAL + " seconds"); - } - this.slenderStaticQuietInterval = slenderStaticQuietInterval; - return this; - } - - @Override - public GameConfig.Builder slenderStaticFranticInterval(int slenderStaticFranticInterval) { - if (slenderStaticFranticInterval < 1) { - throw new IllegalArgumentException("Slender static frantic interval must be at least 1 second"); + "Page glitch pulse must be between 1 and " + + GameConfig.MAX_PAGE_GLITCH_PULSE_SECONDS + " seconds"); } - this.slenderStaticFranticInterval = slenderStaticFranticInterval; - return this; - } - - @Override - public GameConfig.Builder slenderStaticMinVolume(float slenderStaticMinVolume) { - this.slenderStaticMinVolume = checkVolume(slenderStaticMinVolume, "minimum"); - return this; - } - - @Override - public GameConfig.Builder slenderStaticMaxVolume(float slenderStaticMaxVolume) { - this.slenderStaticMaxVolume = checkVolume(slenderStaticMaxVolume, "maximum"); + this.pageGlitchPulseSeconds = pageGlitchPulseSeconds; return this; } - /** - * Checks a static volume against the range a sound can carry. - * - * @param volume the volume to check - * @param name how the volume is named in the message of a failure - * @return the volume - * @throws IllegalArgumentException if the volume is outside 0 to 1 - */ - private static float checkVolume(float volume, String name) { - if (volume < 0.0F || volume > 1.0F) { - throw new IllegalArgumentException( - "Slender static " + name + " volume must be between 0 and 1"); - } - return volume; - } - /** * {@inheritDoc} *

@@ -285,16 +234,6 @@ public GameConfig build() { "Glitch close range (" + glitchCloseRange + ") must be below the glitch range (" + glitchRange + ")"); } - if (slenderStaticFranticInterval >= slenderStaticQuietInterval) { - throw new IllegalArgumentException( - "Slender static frantic interval (" + slenderStaticFranticInterval - + ") must be below the quiet interval (" + slenderStaticQuietInterval + ")"); - } - if (slenderStaticMinVolume > slenderStaticMaxVolume) { - throw new IllegalArgumentException( - "Slender static minimum volume (" + slenderStaticMinVolume - + ") must not be above the maximum volume (" + slenderStaticMaxVolume + ")"); - } return new GameConfigImpl( minPlayers, maxPlayers, @@ -316,12 +255,8 @@ public GameConfig build() { glitchRange, glitchCloseRange, glitchViewAngle, - slenderStaticEnabled, - slenderStaticSound, - slenderStaticQuietInterval, - slenderStaticFranticInterval, - slenderStaticMinVolume, - slenderStaticMaxVolume + pageGlitchEnabled, + pageGlitchPulseSeconds ); } } diff --git a/common/src/main/java/net/onelitefeather/cygnus/common/config/GameConfigImpl.java b/common/src/main/java/net/onelitefeather/cygnus/common/config/GameConfigImpl.java index 19ff84a4..e147215d 100644 --- a/common/src/main/java/net/onelitefeather/cygnus/common/config/GameConfigImpl.java +++ b/common/src/main/java/net/onelitefeather/cygnus/common/config/GameConfigImpl.java @@ -33,12 +33,8 @@ * @param glitchCloseRange the distance in blocks at which the tearing is at its worst * @param glitchViewAngle how far off the centre of their view he may stand and still count * as seen, in degrees - * @param slenderStaticEnabled whether the slender hears static as his pages are collected - * @param slenderStaticSound the sound the static is built from - * @param slenderStaticQuietInterval the seconds between two bursts while no page has been found - * @param slenderStaticFranticInterval the seconds between two bursts once every page is gone - * @param slenderStaticMinVolume how loud the static is while no page has been found - * @param slenderStaticMaxVolume how loud the static is once every page is gone + * @param pageGlitchEnabled whether the slender's screen tears as his pages are collected + * @param pageGlitchPulseSeconds how long a single find holds the glitch above the round's level * @author theEvilReaper * @version 1.4.0 * @since 1.0.0 @@ -64,12 +60,8 @@ public record GameConfigImpl( int glitchRange, int glitchCloseRange, int glitchViewAngle, - boolean slenderStaticEnabled, - Key slenderStaticSound, - int slenderStaticQuietInterval, - int slenderStaticFranticInterval, - float slenderStaticMinVolume, - float slenderStaticMaxVolume + boolean pageGlitchEnabled, + int pageGlitchPulseSeconds ) implements GameConfig { } diff --git a/common/src/main/java/net/onelitefeather/cygnus/common/config/GameConfigReader.java b/common/src/main/java/net/onelitefeather/cygnus/common/config/GameConfigReader.java index 0edd1b8a..d371f461 100644 --- a/common/src/main/java/net/onelitefeather/cygnus/common/config/GameConfigReader.java +++ b/common/src/main/java/net/onelitefeather/cygnus/common/config/GameConfigReader.java @@ -40,12 +40,8 @@ *

  • glitchRange
  • *
  • glitchCloseRange
  • *
  • glitchViewAngle
  • - *
  • slenderStaticEnabled
  • - *
  • slenderStaticSound
  • - *
  • slenderStaticQuietInterval
  • - *
  • slenderStaticFranticInterval
  • - *
  • slenderStaticMinVolume
  • - *
  • slenderStaticMaxVolume
  • + *
  • pageGlitchEnabled
  • + *
  • pageGlitchPulseSeconds
  • * *

    * If a property can not be found in the file, the default value will be used. @@ -65,7 +61,6 @@ public final class GameConfigReader { private static final Pattern SHA1_PATTERN = Pattern.compile("[0-9a-fA-F]{40}"); private static final String PAGE_PROXIMITY_SOUND_KEY = "pageProximitySound"; private static final String DAMAGE_SOUND_KEY = "damageSound"; - private static final String SLENDER_STATIC_SOUND_KEY = "slenderStaticSound"; private final Path path; @@ -128,12 +123,8 @@ public GameConfig getConfig() { .glitchRange(getInt(properties, "glitchRange", internal.glitchRange())) .glitchCloseRange(getInt(properties, "glitchCloseRange", internal.glitchCloseRange())) .glitchViewAngle(getInt(properties, "glitchViewAngle", internal.glitchViewAngle())) - .slenderStaticEnabled(getBoolean(properties, "slenderStaticEnabled", internal.slenderStaticEnabled())) - .slenderStaticSound(getSound(properties, SLENDER_STATIC_SOUND_KEY, internal.slenderStaticSound())) - .slenderStaticQuietInterval(getInt(properties, "slenderStaticQuietInterval", internal.slenderStaticQuietInterval())) - .slenderStaticFranticInterval(getInt(properties, "slenderStaticFranticInterval", internal.slenderStaticFranticInterval())) - .slenderStaticMinVolume(getFloat(properties, "slenderStaticMinVolume", internal.slenderStaticMinVolume())) - .slenderStaticMaxVolume(getFloat(properties, "slenderStaticMaxVolume", internal.slenderStaticMaxVolume())); + .pageGlitchEnabled(getBoolean(properties, "pageGlitchEnabled", internal.pageGlitchEnabled())) + .pageGlitchPulseSeconds(getInt(properties, "pageGlitchPulseSeconds", internal.pageGlitchPulseSeconds())); return configBuilder.build(); } diff --git a/common/src/main/java/net/onelitefeather/cygnus/common/config/InternalGameConfig.java b/common/src/main/java/net/onelitefeather/cygnus/common/config/InternalGameConfig.java index 121c0d5f..66c60d2a 100644 --- a/common/src/main/java/net/onelitefeather/cygnus/common/config/InternalGameConfig.java +++ b/common/src/main/java/net/onelitefeather/cygnus/common/config/InternalGameConfig.java @@ -33,12 +33,8 @@ * @param glitchCloseRange the distance in blocks at which the tearing is at its worst * @param glitchViewAngle how far off the centre of their view he may stand and still count * as seen, in degrees - * @param slenderStaticEnabled whether the slender hears static as his pages are collected - * @param slenderStaticSound the sound the static is built from - * @param slenderStaticQuietInterval the seconds between two bursts while no page has been found - * @param slenderStaticFranticInterval the seconds between two bursts once every page is gone - * @param slenderStaticMinVolume how loud the static is while no page has been found - * @param slenderStaticMaxVolume how loud the static is once every page is gone + * @param pageGlitchEnabled whether the slender's screen tears as his pages are collected + * @param pageGlitchPulseSeconds how long a single find holds the glitch above the round's level * @author theEvilReaper * @version 1.4.0 * @since 1.0.0 @@ -64,12 +60,8 @@ record InternalGameConfig( int glitchRange, int glitchCloseRange, int glitchViewAngle, - boolean slenderStaticEnabled, - Key slenderStaticSound, - int slenderStaticQuietInterval, - int slenderStaticFranticInterval, - float slenderStaticMinVolume, - float slenderStaticMaxVolume + boolean pageGlitchEnabled, + int pageGlitchPulseSeconds ) implements GameConfig { // Sentry and the ResourcePack are opt-in: a service that says nothing about them reports to @@ -79,8 +71,8 @@ record InternalGameConfig( // The damage feedback is on by default for the same reason: taking a hit in silence is a bug, // not a setting. The cooldown of 20 ticks lets through every second damage tick of a draining // slender, which is enough to notice and not enough to grate. - // The slender's static is on by default too: it is the only thing that tells him how far the - // survivors have got without putting the page counter in front of him. + // The page glitch is on by default too: it is the only thing that tells the slender how far + // the survivors have got without putting their page counter in front of him. private static final GameConfig DEFAULT = new InternalGameConfig( 2, 13, 30, 900, 1, 12, null, null, null, true, 20, 20, GameConfig.DEFAULT_PAGE_PROXIMITY_SOUND, @@ -89,11 +81,7 @@ record InternalGameConfig( GameConfig.DEFAULT_GLITCH_RANGE, GameConfig.DEFAULT_GLITCH_CLOSE_RANGE, GameConfig.DEFAULT_GLITCH_VIEW_ANGLE, - true, GameConfig.DEFAULT_SLENDER_STATIC_SOUND, - GameConfig.DEFAULT_SLENDER_STATIC_QUIET_INTERVAL, - GameConfig.DEFAULT_SLENDER_STATIC_FRANTIC_INTERVAL, - GameConfig.DEFAULT_SLENDER_STATIC_MIN_VOLUME, - GameConfig.DEFAULT_SLENDER_STATIC_MAX_VOLUME); + true, GameConfig.DEFAULT_PAGE_GLITCH_PULSE_SECONDS); /** * Returns the default configuration for the game. diff --git a/common/src/test/java/net/onelitefeather/cygnus/common/config/GameConfigReaderTest.java b/common/src/test/java/net/onelitefeather/cygnus/common/config/GameConfigReaderTest.java index 5468246e..9b6befab 100644 --- a/common/src/test/java/net/onelitefeather/cygnus/common/config/GameConfigReaderTest.java +++ b/common/src/test/java/net/onelitefeather/cygnus/common/config/GameConfigReaderTest.java @@ -188,57 +188,43 @@ void testARangeBeyondTheMaximumIsRejected(@TempDir Path tempDir) throws IOExcept } @Test - void testSlenderStaticDefaultsWhenNothingIsConfigured() { + void testPageGlitchDefaultsWhenNothingIsConfigured() { GameConfig config = new GameConfigReader(Paths.get("src", "test", "resources")).getConfig(); - assertTrue(config.slenderStaticEnabled()); - assertEquals(GameConfig.DEFAULT_SLENDER_STATIC_SOUND, config.slenderStaticSound()); - assertEquals(GameConfig.DEFAULT_SLENDER_STATIC_QUIET_INTERVAL, config.slenderStaticQuietInterval()); - assertEquals(GameConfig.DEFAULT_SLENDER_STATIC_FRANTIC_INTERVAL, config.slenderStaticFranticInterval()); - assertEquals(GameConfig.DEFAULT_SLENDER_STATIC_MIN_VOLUME, config.slenderStaticMinVolume()); - assertEquals(GameConfig.DEFAULT_SLENDER_STATIC_MAX_VOLUME, config.slenderStaticMaxVolume()); + assertTrue(config.pageGlitchEnabled()); + assertEquals(GameConfig.DEFAULT_PAGE_GLITCH_PULSE_SECONDS, config.pageGlitchPulseSeconds()); } @Test - void testSlenderStaticValuesAreReadWhenTheyAreConfigured(@TempDir Path tempDir) throws IOException { + void testPageGlitchValuesAreReadWhenTheyAreConfigured(@TempDir Path tempDir) throws IOException { Files.writeString(tempDir.resolve("config.properties"), """ minPlayers=4 - slenderStaticSound=cygnus:vhs_static - slenderStaticQuietInterval=20 - slenderStaticFranticInterval=2 - slenderStaticMinVolume=0.1 - slenderStaticMaxVolume=1.0 + pageGlitchPulseSeconds=6 """); GameConfig config = new GameConfigReader(tempDir).getConfig(); - assertEquals(Key.key("cygnus", "vhs_static"), config.slenderStaticSound(), - "a resource pack sound has to survive the reader, it is the point of the setting"); - assertEquals(20, config.slenderStaticQuietInterval()); - assertEquals(2, config.slenderStaticFranticInterval()); - assertEquals(0.1F, config.slenderStaticMinVolume()); - assertEquals(1.0F, config.slenderStaticMaxVolume()); + assertEquals(6, config.pageGlitchPulseSeconds()); } @Test - void testSlenderStaticCanBeTurnedOff(@TempDir Path tempDir) throws IOException { + void testPageGlitchCanBeTurnedOff(@TempDir Path tempDir) throws IOException { Files.writeString(tempDir.resolve("config.properties"), """ minPlayers=4 - slenderStaticEnabled=false + pageGlitchEnabled=false """); GameConfig config = new GameConfigReader(tempDir).getConfig(); - assertFalse(config.slenderStaticEnabled()); + assertFalse(config.pageGlitchEnabled()); } @Test - void testAFranticIntervalAtOrAboveTheQuietOneIsRejected(@TempDir Path tempDir) throws IOException { + void testAPulseBeyondTheMaximumIsRejected(@TempDir Path tempDir) throws IOException { Files.writeString(tempDir.resolve("config.properties"), """ minPlayers=4 - slenderStaticQuietInterval=5 - slenderStaticFranticInterval=5 - """); + pageGlitchPulseSeconds=%d + """.formatted(GameConfig.MAX_PAGE_GLITCH_PULSE_SECONDS + 1)); GameConfigReader reader = new GameConfigReader(tempDir); diff --git a/config.properties.example b/config.properties.example index 14fd558e..4a87ae10 100644 --- a/config.properties.example +++ b/config.properties.example @@ -149,38 +149,21 @@ survivorTeamSize=12 # means he triggers the effect while barely in frame. #glitchViewAngle=30 -# --- The slender's static ----------------------------------------------------- -# The survivors watch their page counter go up; the slender has none and only -# learns the score when the round ends. The static is that counter told as sound: -# a hiss that sits closer together and louder the fewer pages are left, plus a -# burst of its own the moment a page goes. Only he hears it, and it carries no -# direction - neither side may place the other by it. - -# Whether the slender hears the static at all. -#slenderStaticEnabled=true - -# The sound the static is built from. +# --- The slender's page glitch ----------------------------------------------- +# The survivors watch their page counter climb; the slender has none and only +# learns the score when the round ends. The glitch is that counter, told as his +# own picture coming apart: a baseline that climbs a level at a time as the pages +# disappear, and a harder pulse the moment one goes. # -# Ships in the Cygnus resource pack as three takes of tape hiss the client picks -# between. Unlike the other sound settings, a key that names no vanilla sound is -# not replaced by a default here but sent to the client as named, which is what -# lets a pack sound be used at all. -# -# A server that hands out no pack, or an older one, hears nothing under this key. -# weather.rain is the closest vanilla stand-in if that is the situation. -#slenderStaticSound=cygnus:vhs_static - -# Seconds between two bursts while no page has been found. Between 1 and 120. -#slenderStaticQuietInterval=12 - -# Seconds between two bursts once every page is gone. At least 1 and below -# slenderStaticQuietInterval - the gap shrinks from the one towards the other as -# the pages disappear, so a configuration where they meet or cross is rejected. -#slenderStaticFranticInterval=3 - -# How loud the static is while no page has been found. Between 0 and 1. -#slenderStaticMinVolume=0.15 - -# How loud it is once every page is gone. Between 0 and 1 and not below -# slenderStaticMinVolume. -#slenderStaticMaxVolume=0.8 +# It rides the same channel as the gaze glitch, so the resource pack draws it and +# the cygnus.overlays gate switches it off along with the other full-screen +# effects. Unlike the survivors' version it never darkens the world - that would +# take the sight the slender hunts with. + +# Whether the slender's screen tears as his pages are collected. +#pageGlitchEnabled=true + +# How long a single find holds the glitch one level above where the round stands, +# in seconds. Between 1 and 30. Past that a find late in the round would still be +# on screen when the next one lands, and the pulse stops reading as an event. +#pageGlitchPulseSeconds=3 diff --git a/game/src/main/java/net/onelitefeather/cygnus/Cygnus.java b/game/src/main/java/net/onelitefeather/cygnus/Cygnus.java index 347cf7c0..102f0e78 100644 --- a/game/src/main/java/net/onelitefeather/cygnus/Cygnus.java +++ b/game/src/main/java/net/onelitefeather/cygnus/Cygnus.java @@ -44,7 +44,7 @@ import net.onelitefeather.cygnus.page.PageProximityService; import net.onelitefeather.cygnus.blood.BloodSplatterService; import net.onelitefeather.cygnus.damage.DamageSoundService; -import net.onelitefeather.cygnus.noise.SlenderStaticService; +import net.onelitefeather.cygnus.glitch.PageGlitchService; import net.onelitefeather.cygnus.command.GlitchCommand; import net.onelitefeather.cygnus.command.StartCommand; import net.onelitefeather.cygnus.common.ListenerHandling; @@ -133,7 +133,7 @@ public final class Cygnus implements TeamCreator, ListenerHandling { private final DamageSoundService damageSoundService; private final TunnelVisionRenderer tunnelVisionRenderer; private final TunnelVisionService tunnelVisionService; - private final SlenderStaticService slenderStaticService; + private final PageGlitchService pageGlitchService; public Cygnus() { Path path = ServiceBootstrap.resolveWorkingDirectory(); @@ -188,8 +188,12 @@ public Cygnus() { bound -> ThreadLocalRandom.current().nextInt(bound) ); this.damageSoundService = new DamageSoundService(this.gameConfig, System::currentTimeMillis); - this.slenderStaticService = new SlenderStaticService( + // The gaze's signal, shared rather than a second one: a player has one carrier, and two + // would draw two full-screen quads over each other. Which of the two services addresses a + // player follows their team, and a hand-over moves them from one to the other. + this.pageGlitchService = new PageGlitchService( this.gameConfig, + this.gazeSignal, () -> TeamHelper.slenderOf(this.teamService)); this.tunnelVisionRenderer = new OverlayTunnelVisionRenderer(this.screenOverlay); this.tunnelVisionService = new TunnelVisionService(this.tunnelVisionRenderer, player -> StaminaHelper.remainingShare(this.staminaService, player)); @@ -269,9 +273,6 @@ private void registerGameListener() { // Not part of registerOverlayListeners: the sound is the feedback a hit owes the player // either way, and it needs neither the resource pack nor the overlay gate to be heard. this.damageSoundService.registerListener(handler); - // Outside registerOverlayListeners for the same reason as the damage sound: the static is - // heard, not drawn, so neither the resource pack nor the overlay gate has a say in it. - this.slenderStaticService.registerListener(handler); this.registerOverlayListeners(handler); } @@ -291,6 +292,7 @@ private void registerOverlayListeners(GlobalEventHandler handler) { this.slenderGazeService.registerListener(handler, () -> TeamHelper.survivorsOf(this.teamService)); this.bloodSplatterService.registerListener(handler); this.tunnelVisionService.registerListener(handler, () -> TeamHelper.survivorsOf(this.teamService)); + this.pageGlitchService.registerListener(handler); } private void initPhases() { diff --git a/game/src/main/java/net/onelitefeather/cygnus/gaze/BossBarGazeSignal.java b/game/src/main/java/net/onelitefeather/cygnus/gaze/BossBarGazeSignal.java index 90cf82cb..6d588d2e 100644 --- a/game/src/main/java/net/onelitefeather/cygnus/gaze/BossBarGazeSignal.java +++ b/game/src/main/java/net/onelitefeather/cygnus/gaze/BossBarGazeSignal.java @@ -73,6 +73,9 @@ public final class BossBarGazeSignal implements GazeSink { */ private final PlayerState bars = new PlayerState<>(); + /** Who may have the world darkened along with their veil. */ + private final PlayerState worldTints = new PlayerState<>(); + /** * Builds the component that carries a level. * @@ -100,10 +103,19 @@ static Component signalFor(int level) { */ @Override public void attach(Player survivor) { + this.attach(survivor, true); + } + + /** + * {@inheritDoc} + */ + @Override + public void attach(Player player, boolean worldTint) { BossBar bar = BossBar.bossBar( signalFor(SlenderGaze.NONE), 1f, CARRIER_COLOR, BossBar.Overlay.PROGRESS); - this.bars.put(survivor, bar); - survivor.showBossBar(bar); + this.bars.put(player, bar); + this.worldTints.put(player, worldTint); + player.showBossBar(bar); } /** @@ -112,6 +124,7 @@ public void attach(Player survivor) { @Override public void detach(Player survivor) { BossBar bar = this.bars.remove(survivor); + this.worldTints.remove(survivor); if (bar == null) return; survivor.hideBossBar(bar); } @@ -133,6 +146,11 @@ public void level(Player survivor, int level) { // pass and cannot tell the lightmap anything, which is why the level has to travel twice. bar.name(signalFor(level)); + if (!Boolean.TRUE.equals(this.worldTints.get(survivor))) { + // Attached without the world tint: the veil above is the whole effect for this player. + return; + } + if (level == SlenderGaze.NONE) { bar.removeFlag(BossBar.Flag.DARKEN_SCREEN); return; diff --git a/game/src/main/java/net/onelitefeather/cygnus/gaze/GazeSink.java b/game/src/main/java/net/onelitefeather/cygnus/gaze/GazeSink.java index 7ffb33dc..34428e63 100644 --- a/game/src/main/java/net/onelitefeather/cygnus/gaze/GazeSink.java +++ b/game/src/main/java/net/onelitefeather/cygnus/gaze/GazeSink.java @@ -47,6 +47,23 @@ public void level(Player survivor, int level) { */ void attach(Player survivor); + /** + * Starts signalling for a player who must not have the world darkened along with the veil. + *

    + * The two halves of the effect are separable and the slender needs only one of them: the veil + * says how far the round has got, while darkening the world would take his sight, which is the + * one thing he has over the survivors. A sink that has no world tint to leave out ignores the + * distinction, which is what the default here does. + *

    + * + * @param player the player to start signalling for + * @param worldTint whether the world may be darkened along with the veil + * @since 2.15.0 + */ + default void attach(Player player, boolean worldTint) { + this.attach(player); + } + /** * Stops signalling for a survivor and takes back whatever was set for them. * diff --git a/game/src/main/java/net/onelitefeather/cygnus/gaze/SlenderGazeService.java b/game/src/main/java/net/onelitefeather/cygnus/gaze/SlenderGazeService.java index de0f488b..5db5f8fd 100644 --- a/game/src/main/java/net/onelitefeather/cygnus/gaze/SlenderGazeService.java +++ b/game/src/main/java/net/onelitefeather/cygnus/gaze/SlenderGazeService.java @@ -8,6 +8,7 @@ import net.minestom.server.instance.Instance; import net.onelitefeather.cygnus.event.GameFinishEvent; import net.onelitefeather.cygnus.event.GameStartEvent; +import net.onelitefeather.cygnus.event.SlenderReviveEvent; import net.onelitefeather.cygnus.utils.PlayerState; import net.onelitefeather.cygnus.utils.RepeatingTask; import org.jetbrains.annotations.Nullable; @@ -83,6 +84,10 @@ public void registerListener(EventNode node, Supplier> surviv } }); node.addListener(PlayerDeathEvent.class, event -> this.remove(event.getPlayer())); + // A survivor promoted to slender when the old one quits is alive, so neither of the two + // listeners above covers him. Left tracked, he would be measured against himself: distance + // zero, which reads as the worst level, for the rest of the round. + node.addListener(SlenderReviveEvent.class, event -> this.remove(event.getPlayer())); node.addListener(PlayerDisconnectEvent.class, event -> this.remove(event.getPlayer())); node.addListener(GameFinishEvent.class, event -> { this.cleanUp(); diff --git a/game/src/main/java/net/onelitefeather/cygnus/glitch/PageGlitchService.java b/game/src/main/java/net/onelitefeather/cygnus/glitch/PageGlitchService.java new file mode 100644 index 00000000..56c8470f --- /dev/null +++ b/game/src/main/java/net/onelitefeather/cygnus/glitch/PageGlitchService.java @@ -0,0 +1,215 @@ +package net.onelitefeather.cygnus.glitch; + +import net.minestom.server.entity.Player; +import net.minestom.server.event.Event; +import net.minestom.server.event.EventNode; +import net.onelitefeather.cygnus.common.config.GameConfig; +import net.onelitefeather.cygnus.common.page.event.PageFoundEvent; +import net.onelitefeather.cygnus.event.GameFinishEvent; +import net.onelitefeather.cygnus.event.GameStartEvent; +import net.onelitefeather.cygnus.gaze.GazeSink; +import net.onelitefeather.cygnus.gaze.SlenderGaze; +import net.onelitefeather.cygnus.utils.RepeatingTask; +import org.jetbrains.annotations.Nullable; + +import java.time.temporal.ChronoUnit; +import java.util.function.Supplier; + +/** + * Tears the slender's own screen apart as the survivors take his pages. + * + *

    He is the only player in a round without a page counter: the survivors watch theirs climb, he + * only learns the score when it ends. This is that counter, told as a picture coming apart rather + * than as a number - the same worn-tape look the survivors get from seeing him, turned back on him + * and driven by the round instead of by a line of sight.

    + * + *

    Two things carry it. A baseline sits on his screen for the rest of the round and climbs a + * level at a time as the pages disappear, and every single find pulses one level above that for + * {@link GameConfig#pageGlitchPulseSeconds()} before falling back. Until the first page goes there + * is nothing at all: {@link SlenderGaze#NONE} rather than the weakest level, because an untouched + * round has nothing to say yet.

    + * + *

    The level travels through a {@link GazeSink}, the same seam {@code SlenderGazeService} uses, + * so what reaches the client is decided in one place. The sink is deliberately shared with the + * gaze rather than a second one of its own: a player has one signal carrier, and two would draw + * two full-screen quads over each other. Slender and survivors are disjoint, so the two services + * never address the same player - except across a hand-over, which is why {@link #tick()} watches + * for the slender changing and moves the effect over.

    + * + *

    Usage:

    + *
    {@code
    + * PageGlitchService service = new PageGlitchService(config, signal, () -> currentSlender);
    + * service.registerListener(eventNode);
    + * }
    + * + * @author TheMeinerLP + * @version 1.0.0 + * @since 2.15.0 + */ +public final class PageGlitchService { + + /** How often the service looks at the round, in seconds. */ + private static final long TICK_SECONDS = 1L; + + private final GameConfig config; + private final GazeSink sink; + private final Supplier<@Nullable Player> slender; + private final RepeatingTask task = new RepeatingTask(this::tick); + + /** How far the round has got, between {@code 0} and {@code 1}. */ + private float progress; + + /** Seconds left of the pulse a find started, {@code 0} while none is running. */ + private int pulseSecondsLeft; + + /** The slender the effect currently sits on, {@code null} while it sits on nobody. */ + private @Nullable Player attached; + + /** The level last reported to the sink, so an unchanged one is never sent twice. */ + private int reported = SlenderGaze.NONE; + + /** + * Creates a new instance of the {@link PageGlitchService}. + * + * @param config the configuration holding the switch and the pulse + * @param sink where the level is signalled to + * @param slender supplies the current slender, or {@code null} while there is none + */ + public PageGlitchService(GameConfig config, GazeSink sink, Supplier<@Nullable Player> slender) { + this.config = config; + this.sink = sink; + this.slender = slender; + } + + /** + * Hooks the service into the round's lifecycle. + * + * @param node the node to register on + */ + public void registerListener(EventNode node) { + node.addListener(GameStartEvent.class, event -> this.start()); + node.addListener(PageFoundEvent.class, this::onPageFound); + node.addListener(GameFinishEvent.class, event -> this.stop()); + } + + /** + * Starts a round with an untouched screen. Does nothing while the glitch is turned off. + */ + public void start() { + if (!this.config.pageGlitchEnabled()) return; + this.progress = 0.0F; + this.pulseSecondsLeft = 0; + this.task.start(TICK_SECONDS, ChronoUnit.SECONDS); + this.apply(); + } + + /** + * Stops the effect, takes it off whoever is wearing it and forgets the round. + */ + public void stop() { + this.task.stop(); + this.release(); + this.progress = 0.0F; + this.pulseSecondsLeft = 0; + } + + /** + * @return {@code true} while the service is watching a round + */ + public boolean isRunning() { + return this.task.isRunning(); + } + + /** + * Answers a find with a pulse and raises the baseline behind it. + * + * @param event the find + */ + void onPageFound(PageFoundEvent event) { + if (!this.config.pageGlitchEnabled()) return; + this.progress = shareOf(event.foundCount(), event.maxPages()); + this.pulseSecondsLeft = this.config.pageGlitchPulseSeconds(); + // apply() rather than tick(): a find is not a second passing, and running the clock here + // would cut the pulse a second short of what it was configured for. + this.apply(); + } + + /** + * Runs one second of the round: lets a running pulse decay, then draws. + */ + void tick() { + if (!this.config.pageGlitchEnabled()) return; + if (this.pulseSecondsLeft > 0) { + this.pulseSecondsLeft--; + } + this.apply(); + } + + /** + * Puts the current level on the current slender's screen, moving the effect across if the + * slender has changed. + */ + private void apply() { + if (!this.config.pageGlitchEnabled()) return; + + Player currentSlender = this.slender.get(); + if (currentSlender == null) { + this.release(); + return; + } + + if (!currentSlender.equals(this.attached)) { + // A hand-over, from SlenderReviveEvent. The old slender is a survivor's problem now and + // must not keep a torn screen; the round's state follows whoever took his place. + this.release(); + // Without the world tint: the veil tells him how far the round has got, while darkening + // the world on top of it would take the sight he hunts with. + this.sink.attach(currentSlender, false); + this.attached = currentSlender; + } + + int level = this.level(); + if (level == this.reported) return; + this.reported = level; + this.sink.level(currentSlender, level); + } + + /** + * Takes the effect off whoever is wearing it, if anybody is. + */ + private void release() { + if (this.attached == null) return; + this.sink.detach(this.attached); + this.attached = null; + this.reported = SlenderGaze.NONE; + } + + /** + * Works out the level his screen sits at right now. + * + * @return a level between {@code 0} and {@link SlenderGaze#LEVELS} minus one, or + * {@link SlenderGaze#NONE} while no page has been found + */ + private int level() { + if (this.progress <= 0.0F) { + // A pulse cannot fire before a find, so there is no baseline to raise here. + return SlenderGaze.NONE; + } + + int baseline = Math.round(this.progress * (SlenderGaze.LEVELS - 1)); + if (this.pulseSecondsLeft <= 0) return baseline; + return Math.min(baseline + 1, SlenderGaze.LEVELS - 1); + } + + /** + * Works out how far along a round is. + * + * @param foundCount how many pages are gone + * @param maxPages how many the round needs, {@code 0} or less counting as done + * @return the share, between {@code 0} and {@code 1} + */ + private static float shareOf(int foundCount, int maxPages) { + if (maxPages <= 0) return 1.0F; + return Math.clamp((float) foundCount / maxPages, 0.0F, 1.0F); + } +} diff --git a/game/src/main/java/net/onelitefeather/cygnus/glitch/package-info.java b/game/src/main/java/net/onelitefeather/cygnus/glitch/package-info.java new file mode 100644 index 00000000..90f3bfb4 --- /dev/null +++ b/game/src/main/java/net/onelitefeather/cygnus/glitch/package-info.java @@ -0,0 +1,6 @@ +/** + * Holds the glitch the slender's own screen picks up as his pages are taken. + * + * @since 2.15.0 + */ +package net.onelitefeather.cygnus.glitch; diff --git a/game/src/main/java/net/onelitefeather/cygnus/noise/SlenderStaticService.java b/game/src/main/java/net/onelitefeather/cygnus/noise/SlenderStaticService.java deleted file mode 100644 index ec07ede6..00000000 --- a/game/src/main/java/net/onelitefeather/cygnus/noise/SlenderStaticService.java +++ /dev/null @@ -1,208 +0,0 @@ -package net.onelitefeather.cygnus.noise; - -import net.kyori.adventure.sound.Sound; -import net.minestom.server.entity.Player; -import net.minestom.server.event.Event; -import net.minestom.server.event.EventNode; -import net.minestom.server.sound.SoundEvent; -import net.onelitefeather.cygnus.common.config.GameConfig; -import net.onelitefeather.cygnus.common.page.event.PageFoundEvent; -import net.onelitefeather.cygnus.event.GameFinishEvent; -import net.onelitefeather.cygnus.event.GameStartEvent; -import net.onelitefeather.cygnus.utils.RepeatingTask; -import org.jetbrains.annotations.Nullable; - -import java.time.temporal.ChronoUnit; -import java.util.function.Supplier; - -/** - * Lets the slender hear the round slipping away from him. - * - *

    He is the only player with no page counter in front of him: the survivors watch theirs go up, - * he only ever learns the score when the round ends. This service is that counter, told as sound - * rather than as a number - a tape hiss that sits closer together and louder the fewer pages are - * left, the way the static in Slender: The Eight Pages tightens with every page.

    - * - *

    Two things carry it. A carpet runs for as long as the round does and closes the gap between - * its bursts from {@link GameConfig#slenderStaticQuietInterval()} down to - * {@link GameConfig#slenderStaticFranticInterval()} as the pages disappear. On top of it, every - * single find lands as a burst of its own, so he knows a page went the moment it went instead of - * on the next beat of the carpet.

    - * - *

    Everything is played with {@link Sound.Emitter#self()} on the slender's own entity, so it - * reaches nobody else and carries no direction - a survivor must not be able to hear how close the - * slender is to a page, and the slender must not be able to place himself by it either.

    - * - *

    Usage:

    - *
    {@code
    - * SlenderStaticService service = new SlenderStaticService(config, () -> currentSlender);
    - * service.registerListener(eventNode);
    - * }
    - * - * @author TheMeinerLP - * @version 1.0.0 - * @since 2.14.0 - */ -public final class SlenderStaticService { - - /** How often the carpet is looked at, in seconds. */ - private static final long TICK_SECONDS = 1L; - - /** The pitch the static plays at while no page has been found. */ - private static final float FRESH_PITCH = 1.0F; - - /** The pitch it has dropped to once every page is gone: a tape that has been running too long. */ - private static final float WORN_PITCH = 0.7F; - - private final GameConfig config; - private final Supplier<@Nullable Player> slender; - private final SoundEvent sound; - private final RepeatingTask task = new RepeatingTask(this::tick); - - /** How far the round has got, between {@code 0} and {@code 1}. */ - private float progress; - - /** Seconds left until the carpet plays again. */ - private int secondsUntilBurst; - - /** - * Creates a new instance of the {@link SlenderStaticService}. - * - * @param config the configuration holding the sound, the intervals and the volumes - * @param slender supplies the current slender, or {@code null} while there is none - */ - public SlenderStaticService(GameConfig config, Supplier<@Nullable Player> slender) { - this.config = config; - this.slender = slender; - this.sound = SoundEvent.of(config.slenderStaticSound(), null); - this.secondsUntilBurst = this.interval(); - } - - /** - * Hooks the service into the round's lifecycle. - * - * @param node the node to register on - */ - public void registerListener(EventNode node) { - node.addListener(GameStartEvent.class, event -> this.start()); - node.addListener(PageFoundEvent.class, this::onPageFound); - node.addListener(GameFinishEvent.class, event -> this.stop()); - } - - /** - * Starts the carpet from the top. Does nothing while the static is turned off. - */ - public void start() { - if (!this.config.slenderStaticEnabled()) return; - this.reset(); - this.task.start(TICK_SECONDS, ChronoUnit.SECONDS); - } - - /** - * Stops the carpet and forgets how far the round had got. - */ - public void stop() { - this.task.stop(); - this.reset(); - } - - /** - * @return {@code true} while the carpet is running - */ - public boolean isRunning() { - return this.task.isRunning(); - } - - /** - * Answers a find with a burst and tightens the carpet behind it. - * - * @param event the find - */ - void onPageFound(PageFoundEvent event) { - if (!this.config.slenderStaticEnabled()) return; - this.progress = shareOf(event.foundCount(), event.maxPages()); - // The burst restarts the gap so it does not land on top of the carpet's next beat, which - // would read as one long noise rather than as two separate things happening. - this.secondsUntilBurst = this.interval(); - this.play(); - } - - /** - * Runs one second of the carpet. - */ - void tick() { - if (--this.secondsUntilBurst > 0) return; - this.secondsUntilBurst = this.interval(); - this.play(); - } - - /** - * Plays the static to the current slender, unless the feature is off or there is no slender to - * play it to. - */ - private void play() { - if (!this.config.slenderStaticEnabled()) return; - Player currentSlender = this.slender.get(); - if (currentSlender == null) return; - currentSlender.playSound( - Sound.sound(this.sound, Sound.Source.MASTER, this.volume(), this.pitch()), - Sound.Emitter.self() - ); - } - - /** - * Puts the carpet back to how a round starts. - */ - private void reset() { - this.progress = 0.0F; - this.secondsUntilBurst = this.interval(); - } - - /** - * @return the seconds between two bursts at the current progress - */ - private int interval() { - return Math.round(lerp( - this.config.slenderStaticQuietInterval(), - this.config.slenderStaticFranticInterval(), - this.progress)); - } - - /** - * @return the volume of a burst at the current progress - */ - private float volume() { - return lerp(this.config.slenderStaticMinVolume(), this.config.slenderStaticMaxVolume(), this.progress); - } - - /** - * @return the pitch of a burst at the current progress - */ - private float pitch() { - return lerp(FRESH_PITCH, WORN_PITCH, this.progress); - } - - /** - * Works out how far along a round is. - * - * @param foundCount how many pages are gone - * @param maxPages how many the round needs, {@code 0} or less counting as done - * @return the share, between {@code 0} and {@code 1} - */ - private static float shareOf(int foundCount, int maxPages) { - if (maxPages <= 0) return 1.0F; - return Math.clamp((float) foundCount / maxPages, 0.0F, 1.0F); - } - - /** - * Reads a value off the line between two ends. - * - * @param start the value at {@code share} 0 - * @param end the value at {@code share} 1 - * @param share where between the two to read - * @return the value at that point - */ - private static float lerp(float start, float end, float share) { - return start + (end - start) * share; - } -} diff --git a/game/src/main/java/net/onelitefeather/cygnus/noise/package-info.java b/game/src/main/java/net/onelitefeather/cygnus/noise/package-info.java deleted file mode 100644 index eb839730..00000000 --- a/game/src/main/java/net/onelitefeather/cygnus/noise/package-info.java +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Holds the static the slender hears while the survivors take his pages away. - * - * @since 2.14.0 - */ -package net.onelitefeather.cygnus.noise; diff --git a/game/src/test/java/net/onelitefeather/cygnus/gaze/BossBarGazeSignalTest.java b/game/src/test/java/net/onelitefeather/cygnus/gaze/BossBarGazeSignalTest.java index 36f9c1c9..841f0fff 100644 --- a/game/src/test/java/net/onelitefeather/cygnus/gaze/BossBarGazeSignalTest.java +++ b/game/src/test/java/net/onelitefeather/cygnus/gaze/BossBarGazeSignalTest.java @@ -135,6 +135,21 @@ void levelCarriesBothChannels(Env env) { assertFalse(signal.barOf(survivor).hasFlag(BossBar.Flag.DARKEN_SCREEN)); } + @Test + @DisplayName("A player attached without the world tint gets the veil and nothing else") + void withoutTheWorldTintOnlyTheVeilIsSent(Env env) { + BossBarGazeSignal signal = new BossBarGazeSignal(); + Player slender = connect(env, env.createFlatInstance()); + signal.attach(slender, false); + + signal.level(slender, 2); + + assertEquals(BossBarGazeSignal.SIGNAL_BASE + 3, colourOf(signal.barOf(slender)), + "the veil is the effect and has to arrive either way"); + assertFalse(signal.barOf(slender).hasFlag(BossBar.Flag.DARKEN_SCREEN), + "darkening the world would take the slender's sight, which is his whole advantage"); + } + /** * Reads the encoded colour back out of a bar's title. * diff --git a/game/src/test/java/net/onelitefeather/cygnus/gaze/SlenderGazeServiceTest.java b/game/src/test/java/net/onelitefeather/cygnus/gaze/SlenderGazeServiceTest.java index 38a8ad6a..4256856b 100644 --- a/game/src/test/java/net/onelitefeather/cygnus/gaze/SlenderGazeServiceTest.java +++ b/game/src/test/java/net/onelitefeather/cygnus/gaze/SlenderGazeServiceTest.java @@ -10,6 +10,7 @@ import net.onelitefeather.cygnus.CygnusPlayerTestBase; import net.onelitefeather.cygnus.event.GameFinishEvent; import net.onelitefeather.cygnus.event.GameStartEvent; +import net.onelitefeather.cygnus.event.SlenderReviveEvent; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -272,6 +273,23 @@ public void level(Player survivor, int level) { } } + @Test + @DisplayName("A survivor promoted to slender stops being tracked") + void aPromotedSurvivorIsDropped(Env env) { + Instance instance = env.createFlatInstance(); + Player survivor = connect(env, instance, new Pos(0, 40, 0, 0, 0)); + Player slender = connect(env, instance, new Pos(0, 40, 5)); + SlenderGazeService service = new SlenderGazeService(GazeSink.NONE, GAZE, () -> slender); + service.registerListener(env.process().eventHandler(), () -> Set.of(survivor)); + EventDispatcher.call(new GameStartEvent()); + + EventDispatcher.call(new SlenderReviveEvent(survivor)); + service.tick(); + + assertEquals(SlenderGaze.NONE, service.levelOf(survivor), + "the new slender would otherwise measure himself, land at distance zero and sit at the worst level for the rest of the round"); + } + /** * Connects a player at the given position. * diff --git a/game/src/test/java/net/onelitefeather/cygnus/glitch/PageGlitchServiceTest.java b/game/src/test/java/net/onelitefeather/cygnus/glitch/PageGlitchServiceTest.java new file mode 100644 index 00000000..bcb0e03a --- /dev/null +++ b/game/src/test/java/net/onelitefeather/cygnus/glitch/PageGlitchServiceTest.java @@ -0,0 +1,326 @@ +package net.onelitefeather.cygnus.glitch; + +import net.minestom.server.coordinate.Pos; +import net.minestom.server.entity.Player; +import net.minestom.server.event.EventDispatcher; +import net.minestom.server.instance.Instance; +import net.minestom.testing.Env; +import net.minestom.testing.TestConnection; +import net.onelitefeather.cygnus.CygnusPlayerTestBase; +import net.onelitefeather.cygnus.common.config.GameConfig; +import net.onelitefeather.cygnus.common.page.event.PageFoundEvent; +import net.onelitefeather.cygnus.event.GameFinishEvent; +import net.onelitefeather.cygnus.event.GameStartEvent; +import net.onelitefeather.cygnus.gaze.GazeSink; +import net.onelitefeather.cygnus.gaze.SlenderGaze; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Verifies the glitch the slender's own screen picks up as his pages are taken. + * + * @author TheMeinerLP + * @version 1.0.0 + * @since 2.15.0 + */ +class PageGlitchServiceTest extends CygnusPlayerTestBase { + + /** How many pages a round needs in every test that does not say otherwise. */ + private static final int PAGES = 4; + + /** How long a find's pulse holds, in seconds. */ + private static final int PULSE_SECONDS = 2; + + /** The strongest level there is. */ + private static final int WORST = SlenderGaze.LEVELS - 1; + + @Test + @DisplayName("The slender's screen is clean until the first page goes") + void beforeTheFirstPageTheScreenIsClean(@NotNull Env env) { + Instance instance = env.createFlatInstance(); + Player slender = env.createPlayer(instance); + RecordingSink sink = new RecordingSink(); + PageGlitchService service = service(config(true), sink, slender); + service.registerListener(env.process().eventHandler()); + + EventDispatcher.call(new GameStartEvent()); + service.tick(); + + assertEquals(List.of(slender), sink.attached, "he has to be attached, ready for a level"); + assertEquals(List.of(), sink.levels, "an untouched round is not worth a single frame of tearing"); + + env.destroyInstance(instance, true); + } + + @Test + @DisplayName("The last page leaves his screen at its worst") + void theLastPageIsTheWorstItGets(@NotNull Env env) { + Instance instance = env.createFlatInstance(); + Player slender = env.createPlayer(instance); + RecordingSink sink = new RecordingSink(); + PageGlitchService service = service(config(true), sink, slender); + service.registerListener(env.process().eventHandler()); + EventDispatcher.call(new GameStartEvent()); + + EventDispatcher.call(new PageFoundEvent(slender, PAGES, PAGES)); + tick(service, PULSE_SECONDS); + + assertEquals(WORST, sink.lastLevel(), "every page gone has to read as the worst level there is"); + + env.destroyInstance(instance, true); + } + + @Test + @DisplayName("A find pulses one level above where the round stands") + void aFindPulsesAboveTheBaseline(@NotNull Env env) { + Instance instance = env.createFlatInstance(); + Player slender = env.createPlayer(instance); + RecordingSink sink = new RecordingSink(); + PageGlitchService service = service(config(true), sink, slender); + service.registerListener(env.process().eventHandler()); + EventDispatcher.call(new GameStartEvent()); + + EventDispatcher.call(new PageFoundEvent(slender, 1, PAGES)); + + int pulse = sink.lastLevel(); + tick(service, PULSE_SECONDS); + int baseline = sink.lastLevel(); + + assertTrue(pulse > baseline, "the moment a page goes has to hit harder than the state it leaves behind"); + + env.destroyInstance(instance, true); + } + + @Test + @DisplayName("The pulse holds for the configured seconds") + void thePulseHoldsForItsSeconds(@NotNull Env env) { + Instance instance = env.createFlatInstance(); + Player slender = env.createPlayer(instance); + RecordingSink sink = new RecordingSink(); + PageGlitchService service = service(config(true), sink, slender); + service.registerListener(env.process().eventHandler()); + EventDispatcher.call(new GameStartEvent()); + EventDispatcher.call(new PageFoundEvent(slender, 1, PAGES)); + int pulse = sink.lastLevel(); + + tick(service, PULSE_SECONDS - 1); + assertEquals(pulse, sink.lastLevel(), "the pulse must not fall off early"); + tick(service, 1); + + assertTrue(sink.lastLevel() < pulse, "once its seconds are up the pulse has to fall back"); + + env.destroyInstance(instance, true); + } + + @Test + @DisplayName("An unchanged level is never sent twice") + void anUnchangedLevelIsNeverRepeated(@NotNull Env env) { + Instance instance = env.createFlatInstance(); + Player slender = env.createPlayer(instance); + RecordingSink sink = new RecordingSink(); + PageGlitchService service = service(config(true), sink, slender); + service.registerListener(env.process().eventHandler()); + EventDispatcher.call(new GameStartEvent()); + EventDispatcher.call(new PageFoundEvent(slender, PAGES, PAGES)); + + tick(service, PULSE_SECONDS + 5); + + assertEquals(sink.levels.stream().distinct().toList(), sink.levels, + "a sink is only ever told about a change, repeating one costs the client for nothing"); + + env.destroyInstance(instance, true); + } + + @Test + @DisplayName("A new slender takes the glitch over from the old one") + void aNewSlenderTakesItOver(@NotNull Env env) { + Instance instance = env.createFlatInstance(); + Player first = env.createPlayer(instance); + Player second = env.createPlayer(instance, new Pos(4, 40, 0)); + AtomicReference slender = new AtomicReference<>(first); + RecordingSink sink = new RecordingSink(); + PageGlitchService service = new PageGlitchService(config(true), sink, slender::get); + service.registerListener(env.process().eventHandler()); + EventDispatcher.call(new GameStartEvent()); + EventDispatcher.call(new PageFoundEvent(first, PAGES, PAGES)); + + slender.set(second); + service.tick(); + + assertEquals(List.of(first), sink.detached, "the old slender must not keep a screen full of tearing"); + assertEquals(List.of(first, second), sink.attached); + assertSame(second, sink.lastPlayer(), "the round's state belongs to whoever is the slender now"); + assertEquals(WORST, sink.lastLevel(), "the pages stay found across a hand-over"); + + env.destroyInstance(instance, true); + } + + @Test + @DisplayName("The end of the round takes the glitch off his screen") + void theEndOfTheRoundClearsHisScreen(@NotNull Env env) { + Instance instance = env.createFlatInstance(); + Player slender = env.createPlayer(instance); + RecordingSink sink = new RecordingSink(); + PageGlitchService service = service(config(true), sink, slender); + service.registerListener(env.process().eventHandler()); + EventDispatcher.call(new GameStartEvent()); + EventDispatcher.call(new PageFoundEvent(slender, PAGES, PAGES)); + + EventDispatcher.call(new GameFinishEvent(GameFinishEvent.Reason.ALL_PAGES_FOUND)); + + assertEquals(List.of(slender), sink.detached); + sink.levels.clear(); + EventDispatcher.call(new GameStartEvent()); + service.tick(); + assertEquals(List.of(), sink.levels, "a fresh round starts with the pages unfound again"); + + env.destroyInstance(instance, true); + } + + @Test + @DisplayName("A turned off feature never touches his screen") + void aTurnedOffFeatureStaysAway(@NotNull Env env) { + Instance instance = env.createFlatInstance(); + Player slender = env.createPlayer(instance); + RecordingSink sink = new RecordingSink(); + PageGlitchService service = service(config(false), sink, slender); + service.registerListener(env.process().eventHandler()); + + EventDispatcher.call(new GameStartEvent()); + EventDispatcher.call(new PageFoundEvent(slender, PAGES, PAGES)); + tick(service, PULSE_SECONDS + 1); + + assertEquals(List.of(), sink.attached); + assertEquals(List.of(), sink.levels); + + env.destroyInstance(instance, true); + } + + @Test + @DisplayName("Without a slender there is no screen to tear") + void withoutASlenderNothingHappens(@NotNull Env env) { + Instance instance = env.createFlatInstance(); + RecordingSink sink = new RecordingSink(); + PageGlitchService service = new PageGlitchService(config(true), sink, () -> null); + service.registerListener(env.process().eventHandler()); + + EventDispatcher.call(new GameStartEvent()); + tick(service, PULSE_SECONDS + 1); + + assertEquals(List.of(), sink.attached); + assertEquals(List.of(), sink.levels); + + env.destroyInstance(instance, true); + } + + @Test + @DisplayName("The slender keeps his sight: the veil comes without the world tint") + void theWorldIsNeverDarkenedForHim(@NotNull Env env) { + Instance instance = env.createFlatInstance(); + Player slender = env.createPlayer(instance); + RecordingSink sink = new RecordingSink(); + PageGlitchService service = service(config(true), sink, slender); + service.registerListener(env.process().eventHandler()); + + EventDispatcher.call(new GameStartEvent()); + + assertEquals(List.of(false), sink.worldTints, + "darkening the world would blind the hunter, which is the one thing he must not lose"); + + env.destroyInstance(instance, true); + } + + /** + * Runs the service's second by hand, so a test does not have to wait for the scheduler. + * + * @param service the service under test + * @param seconds how many seconds to run + */ + private static void tick(PageGlitchService service, int seconds) { + for (int i = 0; i < seconds; i++) { + service.tick(); + } + } + + /** + * Builds a service for a slender who never changes. + * + * @param config the configuration to run with + * @param sink where the levels go + * @param slender the slender + * @return the service under test + */ + private static PageGlitchService service(GameConfig config, GazeSink sink, Player slender) { + return new PageGlitchService(config, sink, () -> slender); + } + + /** + * Builds a configuration that only says something about the glitch. + * + * @param enabled whether the glitch is on + * @return the configuration + */ + private static GameConfig config(boolean enabled) { + return GameConfig.builder() + .pageGlitchEnabled(enabled) + .pageGlitchPulseSeconds(PULSE_SECONDS) + .build(); + } + + /** + * A sink that writes down what it was told instead of drawing anything. + */ + private static final class RecordingSink implements GazeSink { + + private final List attached = new ArrayList<>(); + private final List worldTints = new ArrayList<>(); + private final List detached = new ArrayList<>(); + private final List levels = new ArrayList<>(); + private final List players = new ArrayList<>(); + + @Override + public void attach(Player player) { + this.attach(player, true); + } + + @Override + public void attach(Player player, boolean worldTint) { + this.attached.add(player); + this.worldTints.add(worldTint); + } + + @Override + public void detach(Player player) { + this.detached.add(player); + } + + @Override + public void level(Player player, int level) { + this.players.add(player); + this.levels.add(level); + } + + /** + * @return the level reported last, or {@link SlenderGaze#NONE} if there was none + */ + private int lastLevel() { + return this.levels.isEmpty() ? SlenderGaze.NONE : this.levels.getLast(); + } + + /** + * @return the player the last level was reported for + */ + private Player lastPlayer() { + return this.players.getLast(); + } + } +} diff --git a/game/src/test/java/net/onelitefeather/cygnus/noise/SlenderStaticServiceTest.java b/game/src/test/java/net/onelitefeather/cygnus/noise/SlenderStaticServiceTest.java deleted file mode 100644 index 7f11ee4d..00000000 --- a/game/src/test/java/net/onelitefeather/cygnus/noise/SlenderStaticServiceTest.java +++ /dev/null @@ -1,280 +0,0 @@ -package net.onelitefeather.cygnus.noise; - -import net.kyori.adventure.key.Key; -import net.minestom.server.entity.Player; -import net.minestom.server.event.EventDispatcher; -import net.minestom.server.instance.Instance; -import net.minestom.server.network.packet.server.play.EntitySoundEffectPacket; -import net.minestom.testing.Collector; -import net.minestom.testing.Env; -import net.minestom.testing.TestConnection; -import net.onelitefeather.cygnus.CygnusPlayerTestBase; -import net.onelitefeather.cygnus.common.config.GameConfig; -import net.onelitefeather.cygnus.common.page.event.PageFoundEvent; -import net.onelitefeather.cygnus.event.GameFinishEvent; -import net.onelitefeather.cygnus.event.GameStartEvent; -import org.jetbrains.annotations.NotNull; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -import java.util.function.Supplier; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -/** - * Verifies the static the slender hears while the survivors take his pages away. - * - * @author TheMeinerLP - * @version 1.0.0 - * @since 2.14.0 - */ -class SlenderStaticServiceTest extends CygnusPlayerTestBase { - - /** How many pages a round needs in every test that does not say otherwise. */ - private static final int PAGES = 4; - - /** Seconds between two bursts while no page has been found yet. */ - private static final int QUIET_INTERVAL = 8; - - /** Seconds between two bursts once every page is gone. */ - private static final int FRANTIC_INTERVAL = 2; - - private static final float MIN_VOLUME = 0.2F; - private static final float MAX_VOLUME = 0.8F; - - @Test - @DisplayName("Before a page is found the static comes in the long interval") - void beforeAPageIsFoundTheStaticComesSlowly(@NotNull Env env) { - Instance instance = env.createFlatInstance(); - TestConnection connection = env.createConnection(); - Player slender = connection.connect(instance); - SlenderStaticService service = service(config(true), () -> slender); - service.registerListener(env.process().eventHandler()); - EventDispatcher.call(new GameStartEvent()); - Collector beforeTheGapIsUp = connection.trackIncoming(EntitySoundEffectPacket.class); - - tick(service, QUIET_INTERVAL - 1); - // A collector stops tracking the moment it is read, so the second half of the test needs - // one of its own. - beforeTheGapIsUp.assertEmpty(); - Collector onTheLastSecond = connection.trackIncoming(EntitySoundEffectPacket.class); - tick(service, 1); - - onTheLastSecond.assertSingle(packet -> - assertEquals(slender.getEntityId(), packet.entityId(), "the static sits in the slender's own head")); - - env.destroyInstance(instance, true); - } - - @Test - @DisplayName("Every found page shortens the gap between two bursts") - void everyFoundPageShortensTheGap(@NotNull Env env) { - Instance instance = env.createFlatInstance(); - TestConnection connection = env.createConnection(); - Player slender = connection.connect(instance); - SlenderStaticService service = service(config(true), () -> slender); - service.registerListener(env.process().eventHandler()); - EventDispatcher.call(new GameStartEvent()); - EventDispatcher.call(new PageFoundEvent(slender, PAGES, PAGES)); - Collector sounds = connection.trackIncoming(EntitySoundEffectPacket.class); - - tick(service, FRANTIC_INTERVAL); - - sounds.assertSingle(packet -> - assertEquals(MAX_VOLUME, packet.volume(), "the last page has to be as loud as it gets")); - - env.destroyInstance(instance, true); - } - - @Test - @DisplayName("A found page hits the slender with a burst right away") - void aFoundPageBurstsImmediately(@NotNull Env env) { - Instance instance = env.createFlatInstance(); - TestConnection connection = env.createConnection(); - Player slender = connection.connect(instance); - SlenderStaticService service = service(config(true), () -> slender); - service.registerListener(env.process().eventHandler()); - EventDispatcher.call(new GameStartEvent()); - Collector sounds = connection.trackIncoming(EntitySoundEffectPacket.class); - - EventDispatcher.call(new PageFoundEvent(slender, 1, PAGES)); - - sounds.assertSingle(packet -> assertTrue(packet.volume() > MIN_VOLUME, - "the burst has to stand out against the carpet it interrupts")); - - env.destroyInstance(instance, true); - } - - @Test - @DisplayName("A burst restarts the gap, so it never lands on top of the carpet") - void aBurstRestartsTheGap(@NotNull Env env) { - Instance instance = env.createFlatInstance(); - TestConnection connection = env.createConnection(); - Player slender = connection.connect(instance); - SlenderStaticService service = service(config(true), () -> slender); - service.registerListener(env.process().eventHandler()); - EventDispatcher.call(new GameStartEvent()); - tick(service, QUIET_INTERVAL - 1); - EventDispatcher.call(new PageFoundEvent(slender, 1, PAGES)); - Collector sounds = connection.trackIncoming(EntitySoundEffectPacket.class); - - tick(service, 1); - - sounds.assertEmpty(); - - env.destroyInstance(instance, true); - } - - @Test - @DisplayName("Nobody but the slender hears the static") - void nobodyElseHearsTheStatic(@NotNull Env env) { - Instance instance = env.createFlatInstance(); - TestConnection slenderConnection = env.createConnection(); - Player slender = slenderConnection.connect(instance); - TestConnection survivorConnection = env.createConnection(); - survivorConnection.connect(instance); - SlenderStaticService service = service(config(true), () -> slender); - service.registerListener(env.process().eventHandler()); - EventDispatcher.call(new GameStartEvent()); - Collector survivorSounds = - survivorConnection.trackIncoming(EntitySoundEffectPacket.class); - - EventDispatcher.call(new PageFoundEvent(slender, 1, PAGES)); - tick(service, QUIET_INTERVAL); - - survivorSounds.assertEmpty(); - - env.destroyInstance(instance, true); - } - - @Test - @DisplayName("A turned off feature stays silent") - void aTurnedOffFeatureStaysSilent(@NotNull Env env) { - Instance instance = env.createFlatInstance(); - TestConnection connection = env.createConnection(); - Player slender = connection.connect(instance); - SlenderStaticService service = service(config(false), () -> slender); - service.registerListener(env.process().eventHandler()); - EventDispatcher.call(new GameStartEvent()); - Collector sounds = connection.trackIncoming(EntitySoundEffectPacket.class); - - EventDispatcher.call(new PageFoundEvent(slender, PAGES, PAGES)); - tick(service, QUIET_INTERVAL); - - sounds.assertEmpty(); - assertFalse(service.isRunning(), "a turned off feature must not even schedule a task"); - - env.destroyInstance(instance, true); - } - - @Test - @DisplayName("The end of the round stops the static and forgets the progress") - void theEndOfTheRoundStopsTheStatic(@NotNull Env env) { - Instance instance = env.createFlatInstance(); - TestConnection connection = env.createConnection(); - Player slender = connection.connect(instance); - SlenderStaticService service = service(config(true), () -> slender); - service.registerListener(env.process().eventHandler()); - EventDispatcher.call(new GameStartEvent()); - EventDispatcher.call(new PageFoundEvent(slender, PAGES, PAGES)); - - EventDispatcher.call(new GameFinishEvent(GameFinishEvent.Reason.ALL_PAGES_FOUND)); - assertFalse(service.isRunning(), "the task has to be gone once the round is over"); - - EventDispatcher.call(new GameStartEvent()); - Collector sounds = connection.trackIncoming(EntitySoundEffectPacket.class); - tick(service, FRANTIC_INTERVAL); - - sounds.assertEmpty(); - - env.destroyInstance(instance, true); - } - - @Test - @DisplayName("Without a slender there is nobody to play to") - void withoutASlenderNothingIsPlayed(@NotNull Env env) { - Instance instance = env.createFlatInstance(); - TestConnection connection = env.createConnection(); - connection.connect(instance); - SlenderStaticService service = service(config(true), () -> null); - service.registerListener(env.process().eventHandler()); - EventDispatcher.call(new GameStartEvent()); - Collector sounds = connection.trackIncoming(EntitySoundEffectPacket.class); - - tick(service, QUIET_INTERVAL); - - sounds.assertEmpty(); - - env.destroyInstance(instance, true); - } - - @Test - @DisplayName("A resource pack sound is sent as named instead of being dropped") - void aResourcePackSoundIsSentAsNamed(@NotNull Env env) { - Instance instance = env.createFlatInstance(); - TestConnection connection = env.createConnection(); - Player slender = connection.connect(instance); - Key packSound = Key.key("cygnus", "vhs_static"); - GameConfig config = GameConfig.builder() - .slenderStaticEnabled(true) - .slenderStaticSound(packSound) - .slenderStaticQuietInterval(QUIET_INTERVAL) - .slenderStaticFranticInterval(FRANTIC_INTERVAL) - .slenderStaticMinVolume(MIN_VOLUME) - .slenderStaticMaxVolume(MAX_VOLUME) - .build(); - SlenderStaticService service = service(config, () -> slender); - service.registerListener(env.process().eventHandler()); - EventDispatcher.call(new GameStartEvent()); - Collector sounds = connection.trackIncoming(EntitySoundEffectPacket.class); - - EventDispatcher.call(new PageFoundEvent(slender, 1, PAGES)); - - sounds.assertSingle(packet -> assertEquals(packSound, packet.soundEvent().key(), - "a sound that only exists in the resource pack must reach the client under its own name")); - - env.destroyInstance(instance, true); - } - - /** - * Runs the service's second by hand, so a test does not have to wait for the scheduler. - * - * @param service the service under test - * @param seconds how many seconds to run - */ - private static void tick(SlenderStaticService service, int seconds) { - for (int i = 0; i < seconds; i++) { - service.tick(); - } - } - - /** - * Builds a service for the given slender. - * - * @param config the configuration to run with - * @param slender supplies the current slender - * @return the service under test - */ - private static SlenderStaticService service(GameConfig config, Supplier slender) { - return new SlenderStaticService(config, slender); - } - - /** - * Builds a configuration that only says something about the static. - * - * @param enabled whether the static is on - * @return the configuration - */ - private static GameConfig config(boolean enabled) { - return GameConfig.builder() - .slenderStaticEnabled(enabled) - .slenderStaticSound(Key.key("weather.rain")) - .slenderStaticQuietInterval(QUIET_INTERVAL) - .slenderStaticFranticInterval(FRANTIC_INTERVAL) - .slenderStaticMinVolume(MIN_VOLUME) - .slenderStaticMaxVolume(MAX_VOLUME) - .build(); - } -}