Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,15 @@ public sealed interface GameConfig permits GameConfigImpl, InternalGameConfig {
*/
Key DEFAULT_DAMAGE_SOUND = Key.key("entity.player.hurt");

/**
* The {@link #lobbyAtmosphereShare()} a configuration gets when it says nothing.
* <p>
* Enough of the map's own haze and colour to be recognised in the distance, far enough from it
* that the lobby still reads as the lit room players wait in rather than as the map itself.
* </p>
*/
float DEFAULT_LOBBY_ATMOSPHERE_SHARE = 0.3F;

/**
* The static the slender hears while the survivors take his pages away.
* <p>
Expand Down Expand Up @@ -333,6 +342,20 @@ static Builder builder() {
*/
Key damageSound();

/**
* Returns how far the lobby's atmosphere is taken from the open end towards the game map's own.
* <p>
* {@code 0} leaves the lobby on the vanilla overworld, which is where it was. {@code 1} gives it
* exactly the map's atmosphere, which makes the start of a round invisible - the point of the
* setting is the distance between the two, so that walking into the round reads as the world
* closing in rather than as a cut.
* </p>
*
* @return the share, between 0 and 1
* @since 2.14.0
*/
float lobbyAtmosphereShare();

/**
* Returns whether the slender hears static as the survivors collect his pages.
*
Expand Down Expand Up @@ -595,6 +618,16 @@ sealed interface Builder permits GameConfigBuilder {
*/
Builder damageSound(Key damageSound);

/**
* Sets how far the lobby's atmosphere is taken towards the game map's own.
*
* @param lobbyAtmosphereShare the share
* @return the builder instance
* @throws IllegalArgumentException if the share is below 0 or above 1
* @since 2.14.0
*/
Builder lobbyAtmosphereShare(float lobbyAtmosphereShare);

/**
* Sets whether the slender hears static as the survivors collect his pages.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ 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 float lobbyAtmosphereShare = GameConfig.DEFAULT_LOBBY_ATMOSPHERE_SHARE;
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
Expand Down Expand Up @@ -206,6 +207,15 @@ public GameConfig.Builder glitchViewAngle(int glitchViewAngle) {
return this;
}

@Override
public GameConfig.Builder lobbyAtmosphereShare(float lobbyAtmosphereShare) {
if (lobbyAtmosphereShare < 0.0F || lobbyAtmosphereShare > 1.0F) {
throw new IllegalArgumentException("Lobby atmosphere share must be between 0 and 1");
}
this.lobbyAtmosphereShare = lobbyAtmosphereShare;
return this;
}

@Override
public GameConfig.Builder slenderStaticEnabled(boolean slenderStaticEnabled) {
this.slenderStaticEnabled = slenderStaticEnabled;
Expand Down Expand Up @@ -321,7 +331,8 @@ public GameConfig build() {
slenderStaticQuietInterval,
slenderStaticFranticInterval,
slenderStaticMinVolume,
slenderStaticMaxVolume
slenderStaticMaxVolume,
lobbyAtmosphereShare
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
* @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 lobbyAtmosphereShare how far the lobby's atmosphere is taken towards the map's own
* @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
Expand Down Expand Up @@ -69,7 +70,8 @@ public record GameConfigImpl(
int slenderStaticQuietInterval,
int slenderStaticFranticInterval,
float slenderStaticMinVolume,
float slenderStaticMaxVolume
float slenderStaticMaxVolume,
float lobbyAtmosphereShare
) implements GameConfig {

}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
* <li>glitchRange</li>
* <li>glitchCloseRange</li>
* <li>glitchViewAngle</li>
* <li>lobbyAtmosphereShare</li>
* <li>slenderStaticEnabled</li>
* <li>slenderStaticSound</li>
* <li>slenderStaticQuietInterval</li>
Expand Down Expand Up @@ -133,7 +134,8 @@ public GameConfig getConfig() {
.slenderStaticQuietInterval(getInt(properties, "slenderStaticQuietInterval", internal.slenderStaticQuietInterval()))
.slenderStaticFranticInterval(getInt(properties, "slenderStaticFranticInterval", internal.slenderStaticFranticInterval()))
.slenderStaticMinVolume(getFloat(properties, "slenderStaticMinVolume", internal.slenderStaticMinVolume()))
.slenderStaticMaxVolume(getFloat(properties, "slenderStaticMaxVolume", internal.slenderStaticMaxVolume()));
.slenderStaticMaxVolume(getFloat(properties, "slenderStaticMaxVolume", internal.slenderStaticMaxVolume()))
.lobbyAtmosphereShare(getFloat(properties, "lobbyAtmosphereShare", internal.lobbyAtmosphereShare()));

return configBuilder.build();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
* @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 lobbyAtmosphereShare how far the lobby's atmosphere is taken towards the map's own
* @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
Expand Down Expand Up @@ -69,7 +70,8 @@ record InternalGameConfig(
int slenderStaticQuietInterval,
int slenderStaticFranticInterval,
float slenderStaticMinVolume,
float slenderStaticMaxVolume
float slenderStaticMaxVolume,
float lobbyAtmosphereShare
) implements GameConfig {

// Sentry and the ResourcePack are opt-in: a service that says nothing about them reports to
Expand All @@ -93,7 +95,8 @@ record InternalGameConfig(
GameConfig.DEFAULT_SLENDER_STATIC_QUIET_INTERVAL,
GameConfig.DEFAULT_SLENDER_STATIC_FRANTIC_INTERVAL,
GameConfig.DEFAULT_SLENDER_STATIC_MIN_VOLUME,
GameConfig.DEFAULT_SLENDER_STATIC_MAX_VOLUME);
GameConfig.DEFAULT_SLENDER_STATIC_MAX_VOLUME,
GameConfig.DEFAULT_LOBBY_ATMOSPHERE_SHARE);

/**
* Returns the default configuration for the game.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package net.onelitefeather.cygnus.common.dimension;

import net.kyori.adventure.util.RGBLike;
import net.minestom.server.color.Color;

/**
* One atmosphere read part of the way towards another.
*
* <p>This exists for the lobby. A player who walks straight from a vanilla sky into a map that
* closes in at forty blocks meets the whole atmosphere at once, at the same moment the round
* starts. Giving the lobby a weakened version of the map's own atmosphere turns that into a
* build-up: the same colours and the same haze, only far enough away to still see across the
* lobby, so the start of the round reads as the world tightening rather than as a cut.</p>
*
* <p>Every value is read off the straight line between the two ends, colours channel by channel.
* A share of {@code 0} is the open end untouched, {@code 1} is the other atmosphere exactly, and
* anything between is proportionally near each.</p>
*
* <p>The result is a {@link MapAtmosphere}, which means its own corrections apply on the way out:
* a blend that would leave the fog no span to fade over is repaired there rather than here.</p>
*
* <p>Usage:</p>
* <pre>{@code
* DimensionAtmosphere lobby = BlendedAtmosphere.between(StaticDimensionPreset.BRIGHT, mapAtmosphere, 0.3f);
* }</pre>
*
* @author TheMeinerLP
* @version 1.0.0
* @since 2.15.0
*/
public final class BlendedAtmosphere {

private BlendedAtmosphere() {
throw new UnsupportedOperationException();
}

/**
* Reads an atmosphere off the line between two others.
*
* @param open the atmosphere a share of {@code 0} yields
* @param target the atmosphere a share of {@code 1} yields
* @param share how far to travel from {@code open} towards {@code target}, clamped to
* {@code [0, 1]}
* @return the blended atmosphere
*/
public static DimensionAtmosphere between(DimensionAtmosphere open, DimensionAtmosphere target, float share) {
float amount = Math.clamp(share, 0.0f, 1.0f);
return new MapAtmosphere(
mix(open.fogColor(), target.fogColor(), amount),
mix(open.skyLightColor(), target.skyLightColor(), amount),
mix(open.skyColor(), target.skyColor(), amount),
mix(open.ambientLightColor(), target.ambientLightColor(), amount),
lerp(open.skyLightFactor(), target.skyLightFactor(), amount),
lerp(open.fogStartDistance(), target.fogStartDistance(), amount),
lerp(open.fogEndDistance(), target.fogEndDistance(), amount),
lerp(open.skyFogEndDistance(), target.skyFogEndDistance(), amount)
);
}

/**
* Mixes two colours channel by channel.
*
* @param open the colour at {@code amount} 0
* @param target the colour at {@code amount} 1
* @param amount where between the two to read
* @return the mixed colour
*/
private static Color mix(RGBLike open, RGBLike target, float amount) {
return new Color(
Math.round(lerp(open.red(), target.red(), amount)),
Math.round(lerp(open.green(), target.green(), amount)),
Math.round(lerp(open.blue(), target.blue(), amount))
);
}

/**
* Reads a value off the line between two ends.
*
* @param open the value at {@code amount} 0
* @param target the value at {@code amount} 1
* @param amount where between the two to read
* @return the value at that point
*/
private static float lerp(float open, float target, float amount) {
return open + (target - open) * amount;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,37 @@ void testAFranticIntervalAtOrAboveTheQuietOneIsRejected(@TempDir Path tempDir) t
assertThrows(IllegalArgumentException.class, reader::getConfig);
}

@Test
void testLobbyAtmosphereShareDefaultsWhenNothingIsConfigured() {
GameConfig config = new GameConfigReader(Paths.get("src", "test", "resources")).getConfig();

assertEquals(GameConfig.DEFAULT_LOBBY_ATMOSPHERE_SHARE, config.lobbyAtmosphereShare());
}

@Test
void testLobbyAtmosphereShareIsReadWhenItIsConfigured(@TempDir Path tempDir) throws IOException {
Files.writeString(tempDir.resolve("config.properties"), """
minPlayers=4
lobbyAtmosphereShare=0.6
""");

GameConfig config = new GameConfigReader(tempDir).getConfig();

assertEquals(0.6F, config.lobbyAtmosphereShare());
}

@Test
void testALobbyAtmosphereShareOutsideItsRangeIsRejected(@TempDir Path tempDir) throws IOException {
Files.writeString(tempDir.resolve("config.properties"), """
minPlayers=4
lobbyAtmosphereShare=1.5
""");

GameConfigReader reader = new GameConfigReader(tempDir);

assertThrows(IllegalArgumentException.class, reader::getConfig);
}

@Test
void testDamageSoundDefaultsWhenNothingIsConfigured() {
GameConfig config = new GameConfigReader(Paths.get("src", "test", "resources")).getConfig();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package net.onelitefeather.cygnus.common.dimension;

import net.minestom.server.color.Color;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertSame;

/**
* Verifies the atmosphere the lobby is given: the map's own, pulled back towards the open end.
*
* @author TheMeinerLP
* @version 1.0.0
* @since 2.15.0
*/
class BlendedAtmosphereTest {

/** The open end a lobby is pulled towards. */
private static final DimensionAtmosphere OPEN = new MapAtmosphere(
new Color(100, 100, 100),
new Color(200, 200, 200),
new Color(150, 150, 150),
new Color(10, 10, 10),
1.0f, 20f, 200f, 100f
);

/** A map that closes in on the player. */
private static final DimensionAtmosphere CLOSED = new MapAtmosphere(
new Color(0, 40, 20),
new Color(0, 80, 40),
new Color(0, 60, 30),
new Color(0, 4, 2),
0.0f, 0f, 40f, 20f
);

@Test
@DisplayName("A share of zero leaves the open end untouched")
void zeroKeepsTheOpenEnd() {
DimensionAtmosphere blended = BlendedAtmosphere.between(OPEN, CLOSED, 0f);

assertEquals(20f, blended.fogStartDistance());
assertEquals(200f, blended.fogEndDistance());
assertEquals(1.0f, blended.skyLightFactor());
assertEquals(OPEN.fogColor(), blended.fogColor());
}

@Test
@DisplayName("A share of one is the map itself")
void oneIsTheMapItself() {
DimensionAtmosphere blended = BlendedAtmosphere.between(OPEN, CLOSED, 1f);

assertEquals(0f, blended.fogStartDistance());
assertEquals(40f, blended.fogEndDistance());
assertEquals(0.0f, blended.skyLightFactor());
assertEquals(CLOSED.fogColor(), blended.fogColor());
}

@Test
@DisplayName("A share in between lands in between, on every value")
void aShareLandsInBetween() {
DimensionAtmosphere blended = BlendedAtmosphere.between(OPEN, CLOSED, 0.5f);

assertEquals(10f, blended.fogStartDistance());
assertEquals(120f, blended.fogEndDistance());
assertEquals(60f, blended.skyFogEndDistance());
assertEquals(0.5f, blended.skyLightFactor());
}

@Test
@DisplayName("Colours are mixed channel by channel")
void coloursAreMixedPerChannel() {
DimensionAtmosphere blended = BlendedAtmosphere.between(OPEN, CLOSED, 0.5f);

assertEquals(new Color(50, 70, 60), blended.fogColor());
assertEquals(new Color(100, 140, 120), blended.skyLightColor());
assertEquals(new Color(75, 105, 90), blended.skyColor());
}

@Test
@DisplayName("A share outside its range is pulled back into it")
void aShareOutsideItsRangeIsClamped() {
assertEquals(OPEN.fogEndDistance(), BlendedAtmosphere.between(OPEN, CLOSED, -1f).fogEndDistance());
assertEquals(CLOSED.fogEndDistance(), BlendedAtmosphere.between(OPEN, CLOSED, 2f).fogEndDistance());
}

@Test
@DisplayName("The fog keeps a span the client can fade over")
void theFogKeepsAFadeableSpan() {
DimensionAtmosphere flat = new MapAtmosphere(
new Color(0, 0, 0), new Color(0, 0, 0), new Color(0, 0, 0), new Color(1, 1, 1),
0f, 30f, 31f, 20f
);

DimensionAtmosphere blended = BlendedAtmosphere.between(flat, flat, 0.5f);

assertEquals(30f, blended.fogStartDistance());
assertEquals(31f, blended.fogEndDistance(),
"a blend must not collapse the gap the fog fades over");
}

@Test
@DisplayName("Blending an atmosphere with itself changes nothing")
void blendingWithItselfChangesNothing() {
DimensionAtmosphere blended = BlendedAtmosphere.between(CLOSED, CLOSED, 0.3f);

assertEquals(CLOSED.fogColor(), blended.fogColor());
assertEquals(CLOSED.fogEndDistance(), blended.fogEndDistance());
assertEquals(CLOSED.skyLightFactor(), blended.skyLightFactor());
}
}
Loading
Loading