Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
92 commits
Select commit Hold shift + click to select a range
285ff9e
feat(creek): add config record
theEvilReaper Sep 24, 2026
40ead28
chore(config): add new config usage
theEvilReaper Sep 24, 2026
afc86ea
chore(page): expose page count
theEvilReaper Sep 24, 2026
f7ce872
feat(creek): add entity structure class
theEvilReaper Sep 24, 2026
f5afd13
feat(creek): add body package
theEvilReaper Sep 24, 2026
f8c9f46
feat(creek): add catch logic
theEvilReaper Sep 24, 2026
3d45635
feat(creek): add dread count handling
theEvilReaper Sep 24, 2026
e1ea0da
feat(creek): add state handling
theEvilReaper Sep 24, 2026
ef13848
feat(creek): add world integration
theEvilReaper Sep 24, 2026
1405f19
feat(creek): add debug part
theEvilReaper Sep 24, 2026
7756db2
feat(creek): add management layer
theEvilReaper Sep 24, 2026
d2a1672
test(creek): add a bunch of cases
theEvilReaper Sep 24, 2026
48aeb9b
feat(doc): improve documentation
theEvilReaper Sep 24, 2026
47d8857
chore(stamina): add drain method
theEvilReaper Sep 24, 2026
41f53fe
feat(creek): add debug command
theEvilReaper Sep 24, 2026
74ea9eb
chore(game): wire creek usage
theEvilReaper Sep 24, 2026
d3bc5db
chore(config): add creek configuration example
theEvilReaper Sep 24, 2026
8cf566d
chore(config): update Creek config usage
theEvilReaper Sep 25, 2026
9e062c2
feat(map): add CreekRoute structure
theEvilReaper Sep 25, 2026
ce6ae9b
chore(page): update map creation context
theEvilReaper Sep 25, 2026
11b32b9
feat(setup): add preview for creek routes
theEvilReaper Sep 25, 2026
a956f9d
feat(setup): add CreekRouteDialogs
theEvilReaper Sep 25, 2026
8eb392a
chore(setup): add new setup part item
theEvilReaper Sep 25, 2026
35cff2d
feat(data): wire new inventory
theEvilReaper Sep 25, 2026
df4a436
feat(setup): add creek route listener
theEvilReaper Sep 25, 2026
4ea7b80
chore(setup): add creek dialog handling
theEvilReaper Sep 25, 2026
cfbbf99
chore(reader): update creek config reader handling
theEvilReaper Sep 25, 2026
de4f16c
chore(creek): add new config value
theEvilReaper Sep 25, 2026
7eca655
chore(creek): update state handling
theEvilReaper Sep 25, 2026
101cbdb
chore(map): add file loading for routes
theEvilReaper Sep 25, 2026
5e95745
chore(creek): update service and debug part
theEvilReaper Sep 25, 2026
490974b
test(creek): add context class
theEvilReaper Sep 25, 2026
5e3a085
chore(setup): new constants
theEvilReaper Sep 25, 2026
95088d7
chore(setup): register new listener
theEvilReaper Sep 25, 2026
9c2e62c
chore(creek): improve path route logic
theEvilReaper Sep 25, 2026
6864992
chore(game): add new method
theEvilReaper Sep 25, 2026
05c10ab
test(creek): add more route tests
theEvilReaper Sep 25, 2026
272c5ec
chore(setup): add route dialog handler and slot implementation
theEvilReaper Sep 25, 2026
a769eed
test(path): add small path test
theEvilReaper Sep 25, 2026
b14affb
chore(config): update example
theEvilReaper Sep 25, 2026
4b3dcd0
chore(message): update component creation
theEvilReaper Sep 25, 2026
c3a8f39
chore: improve variable naming
theEvilReaper Sep 25, 2026
b560fc3
chore(item): update used items during the creek setup
theEvilReaper Sep 25, 2026
fd89cdb
chore(config): add new creek values
theEvilReaper Sep 25, 2026
ae9f96a
chore(message): add new message
theEvilReaper Sep 25, 2026
e95e7ca
chore(creek): add preview particles
theEvilReaper Sep 25, 2026
ce1daae
chore(setup): add new dialog things
theEvilReaper Sep 25, 2026
bd2898f
chore(config): update reader cases
theEvilReaper Sep 25, 2026
0902b1e
chore(map): improve map creation
theEvilReaper Sep 25, 2026
7e5fb36
chore(creek): improve route listener
theEvilReaper Sep 25, 2026
e67b0d2
chore(setup): improve route usage
theEvilReaper Sep 25, 2026
f72d1c9
chore(creek): improve route path, route and file handling
theEvilReaper Sep 25, 2026
e3d6d3e
chore: update test cases
theEvilReaper Sep 25, 2026
b0d1dfa
chore(creek): add step handling
theEvilReaper Sep 25, 2026
67d275f
chore(game): improve creek usage during startup
theEvilReaper Sep 25, 2026
31d0d0a
fix(dialog): improve atmosphere dialog
theEvilReaper Sep 25, 2026
5e52308
chore(creek): remove old random route and update provider
theEvilReaper Sep 25, 2026
5526492
chore(config): update example config
theEvilReaper Sep 25, 2026
2540bae
test(creek): update to RouteStep
theEvilReaper Sep 25, 2026
f1607ff
feat(route): add step record
theEvilReaper Sep 25, 2026
34c7db5
feat(creek): add waypoint and pause handler
theEvilReaper Sep 25, 2026
f2c3a61
fix(setup): remove bossbar on reset
Joltras Sep 26, 2026
62519f9
chore(setup): improve item range
Joltras Sep 26, 2026
abe96da
chore(game): improve entity pathfinding
Joltras Sep 26, 2026
634514b
chore(creek): improve documentation
Joltras Sep 26, 2026
4c0b58a
refactor(creek): overhaul state usage to have multiple instances
Joltras Sep 26, 2026
58d192c
chore(creek): improve path documentation
Joltras Sep 26, 2026
5aec375
chore(creek): update creek service and clock part
Joltras Sep 26, 2026
002b10d
test(creek): update test cases
Joltras Sep 26, 2026
13fd7b1
docs: update documentation
Joltras Sep 26, 2026
27ca0fe
chore(creek): remove WanderState class
Joltras Sep 26, 2026
69a72d8
chore(creek): move count logic
Joltras Sep 27, 2026
85a6aba
chore(creek): remove config usage from the state handling
Joltras Sep 27, 2026
74464fa
chore(creek): add new method to handle vanish changes
Joltras Sep 27, 2026
23fb098
test(creek): combine test cases and refactor others
Joltras Sep 27, 2026
e30f8b8
fix(creek): give every creek its own route walker
Joltras Sep 27, 2026
d535c57
chore(creek): compute route points once
Joltras Sep 27, 2026
7a91da3
chore(creek): reuse stalk start in variants
Joltras Sep 27, 2026
8c1acfe
refactor(creek): share one survivor snapshot per step
Joltras Sep 27, 2026
e8dbb85
fix(creek): only take back effects the creek handed out
Joltras Sep 27, 2026
a4711d8
refactor(creek): replace catch and select callbacks with CreekActions
Joltras Sep 27, 2026
bd50e19
feat(creek): vanish at dead ends to leave a route island
Joltras Sep 27, 2026
83c1bf9
feat(creek): link route ends to the middle of other routes
Joltras Sep 27, 2026
dd55023
refactor(creek): simplify after the patrol rework
Joltras Sep 27, 2026
7593918
feat(creek): fling selected survivors away instead of teleporting them
Joltras Sep 27, 2026
c44e04a
test(setup): pass a player to the setup data under test
Joltras Sep 28, 2026
1cf4ae2
refactor(map): collect creek routes with the stream api
Joltras Sep 28, 2026
314a649
chore(creek): read the step height from the entity attribute
Joltras Sep 28, 2026
151dc7f
chore(creek): stop at the first hidden point when coming back
Joltras Sep 28, 2026
63cc534
refactor(creek): rename PatrolConsequence to PatrolHelper
Joltras Sep 28, 2026
1bed567
refactor(creek): store a waypoint's position as a Vec
Joltras Sep 28, 2026
729a5a5
chore(creek): let the creek step up whole blocks like a vanilla creaking
Joltras Sep 28, 2026
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
@@ -0,0 +1,183 @@
package net.onelitefeather.cygnus.common.config;

/**
* Settings for the creek, the second figure next to the slender.
* <p>
* All numbers that control the creek's behavior live here, so they can be tuned in a playtest
* without changing code. The constructor rejects combinations that would break the behavior,
* for example a stalk distance range that runs backwards, or a hiding spot inside the view cone.
* </p>
*
* @param enabled whether the creek takes part in a round
* @param activeWithLastSurvivor whether the creek keeps going when only one survivor is left
* @param sightRange how far away a survivor can notice the creek, in blocks
* @param sightViewAngle the maximum angle from a survivor's view center at which the
* creek counts as seen, in degrees
* @param wanderPauseMillis how long the creek stops when spotted while wandering
* @param wanderSpeed movement speed while wandering, in blocks per tick
* @param huntSpeed movement speed while hunting, in blocks per tick; must be above
* a walking survivor's 0.216
* @param stalkThreshold the dread at which the creek starts stalking a survivor
* @param huntThreshold the dread at which a stalk turns into a hunt
* @param stalkMinDistance minimum distance to the stalked survivor, in blocks
* @param stalkMaxDistance maximum distance to the stalked survivor, in blocks
* @param stalkMinAngle minimum angle from the stalked survivor's view direction, in degrees
* @param stalkMaxAngle maximum angle from the stalked survivor's view direction, in degrees
* @param stalkRevealMillis how long the creek stays visible before it teleports away
* @param stalkMinSeconds minimum length of a stalk
* @param stalkMaxSeconds maximum length of a stalk
* @param huntMaxSeconds maximum length of a hunt
* @param catchDistance distance at which the creek catches a survivor, in blocks
* @param vanishMinSeconds shortest vanish time, used at full dread
* @param vanishMaxSeconds longest vanish time, used at no dread
* @param respawnMinDistance minimum distance to every survivor when reappearing, in blocks
* @param personalSpace how close a survivor may come outside a hunt, in blocks
* @param stuckMillis how long the creek may make no progress before taking a shortcut
* @param dreadPageWeight dread share from found pages
* @param dreadTimeWeight dread share from elapsed round time
* @param dreadIsolationWeight dread share from being alone
* @param isolationRadius distance to the nearest survivor at which someone counts as alone
* @param betrayalCatchCount from this catch on, a survivor is always revealed to the slender
* @param betrayalChance chance of a reveal on earlier catches
* @param betrayalGlowSeconds how long a revealed survivor glows for the slender
* @param slownessSeconds how long a caught survivor is slowed
* @param routeLinkDistance how close two route ends have to be to count as linked, in blocks
* @param randomStopChance chance that the creek stops for a moment after reaching a waypoint
* @param randomStopMinMillis shortest random stop, in milliseconds
* @param randomStopMaxMillis longest random stop, in milliseconds
* @author theEvilReaper
* @version 1.0.0
* @since 2.15.0
*/
public record CreekConfig(
boolean enabled,
boolean activeWithLastSurvivor,
int sightRange,
int sightViewAngle,
int wanderPauseMillis,
double wanderSpeed,
double huntSpeed,
double stalkThreshold,
double huntThreshold,
int stalkMinDistance,
int stalkMaxDistance,
int stalkMinAngle,
int stalkMaxAngle,
int stalkRevealMillis,
int stalkMinSeconds,
int stalkMaxSeconds,
int huntMaxSeconds,
double catchDistance,
int vanishMinSeconds,
int vanishMaxSeconds,
int respawnMinDistance,
int personalSpace,
int stuckMillis,
double dreadPageWeight,
double dreadTimeWeight,
double dreadIsolationWeight,
int isolationRadius,
int betrayalCatchCount,
double betrayalChance,
int betrayalGlowSeconds,
int slownessSeconds,
double routeLinkDistance,
double randomStopChance,
int randomStopMinMillis,
int randomStopMaxMillis
) {

/**
* The default settings: enabled, and tuned for a round of about fifteen minutes.
*/
public static final CreekConfig DEFAULT = new CreekConfig(
true, true,
48, 35,
1500, 0.07D, 0.25D,
0.25D, 0.6D,
20, 35, 40, 70, 700, 45, 90,
30, 1.5D,
20, 40, 30, 15, 3000,
0.6D, 0.3D, 0.1D, 25,
2, 0.15D, 6, 4,
3.0D,
0.15D, 1500, 4000
);

/**
* Checks the values.
*
* @throws IllegalArgumentException if a value is out of range or two values contradict each other
*/
public CreekConfig {
atLeast("sightRange", sightRange, 1);
between("sightViewAngle", sightViewAngle, 1, GameConfig.Glitch.MAX_VIEW_ANGLE);
atLeast("wanderPauseMillis", wanderPauseMillis, 0);
positive("wanderSpeed", wanderSpeed);
positive("huntSpeed", huntSpeed);
positive("stalkThreshold", stalkThreshold);
between("huntThreshold", huntThreshold, 0.0D, 1.0D);
below("stalkThreshold", stalkThreshold, "huntThreshold", huntThreshold);
atLeast("personalSpace", personalSpace, 1);
below("personalSpace", personalSpace, "stalkMinDistance", stalkMinDistance);
below("stalkMinDistance", stalkMinDistance, "stalkMaxDistance", stalkMaxDistance);
// A hiding spot inside the view cone would count as seen right away.
below("sightViewAngle", sightViewAngle, "stalkMinAngle", stalkMinAngle);
below("stalkMinAngle", stalkMinAngle, "stalkMaxAngle", stalkMaxAngle);
between("stalkMaxAngle", stalkMaxAngle, 1, 180);
atLeast("stalkRevealMillis", stalkRevealMillis, 0);
atLeast("stalkMinSeconds", stalkMinSeconds, 1);
notAbove("stalkMinSeconds", stalkMinSeconds, "stalkMaxSeconds", stalkMaxSeconds);
atLeast("huntMaxSeconds", huntMaxSeconds, 1);
positive("catchDistance", catchDistance);
atLeast("vanishMinSeconds", vanishMinSeconds, 0);
notAbove("vanishMinSeconds", vanishMinSeconds, "vanishMaxSeconds", vanishMaxSeconds);
atLeast("respawnMinDistance", respawnMinDistance, 1);
atLeast("stuckMillis", stuckMillis, 1);
atLeast("dreadPageWeight", dreadPageWeight, 0.0D);
atLeast("dreadTimeWeight", dreadTimeWeight, 0.0D);
atLeast("dreadIsolationWeight", dreadIsolationWeight, 0.0D);
atLeast("isolationRadius", isolationRadius, 1);
atLeast("betrayalCatchCount", betrayalCatchCount, 1);
between("betrayalChance", betrayalChance, 0.0D, 1.0D);
atLeast("betrayalGlowSeconds", betrayalGlowSeconds, 1);
atLeast("slownessSeconds", slownessSeconds, 0);
positive("routeLinkDistance", routeLinkDistance);
between("randomStopChance", randomStopChance, 0.0D, 1.0D);
atLeast("randomStopMinMillis", randomStopMinMillis, 0);
notAbove("randomStopMinMillis", randomStopMinMillis, "randomStopMaxMillis", randomStopMaxMillis);
}

private static void atLeast(String name, double value, double minimum) {
if (value < minimum) {
throw new IllegalArgumentException(name + " (" + value + ") must be at least " + minimum);
}
}

private static void positive(String name, double value) {
if (value <= 0.0D) {
throw new IllegalArgumentException(name + " (" + value + ") must be above 0");
}
}

private static void between(String name, double value, double minimum, double maximum) {
if (value < minimum || value > maximum) {
throw new IllegalArgumentException(
name + " (" + value + ") must be between " + minimum + " and " + maximum);
}
}

private static void below(String lowerName, double lower, String upperName, double upper) {
if (lower >= upper) {
throw new IllegalArgumentException(
lowerName + " (" + lower + ") must be below " + upperName + " (" + upper + ")");
}
}

private static void notAbove(String lowerName, double lower, String upperName, double upper) {
if (lower > upper) {
throw new IllegalArgumentException(
lowerName + " (" + lower + ") must not be above " + upperName + " (" + upper + ")");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
* @param damageSound the sound a player hears when hit
* @param glitch how the sight of the slender tears a survivor's view
* @param slenderStatic the static the slender hears while pages are found
* @param creek the creek, the second figure next to the slender
* @param lobbyAtmosphereShare how far the lobby's atmosphere is taken towards the map's own: {@code 0}
* leaves the vanilla overworld, {@code 1} is exactly the map's atmosphere
* @author theEvilReaper
Expand All @@ -36,6 +37,7 @@ public record GameConfig(
DamageSound damageSound,
Glitch glitch,
SlenderStatic slenderStatic,
CreekConfig creek,
float lobbyAtmosphereShare
) {

Expand All @@ -53,16 +55,12 @@ public record GameConfig(
* Spawning immediately at {@code GameStartEvent} let survivors grab a page before they had even
* moved from the spawn point. The delay gives them time to spread across the map first.
* </p>
*
* @since 2.15.0
*/
public static final int PAGE_SPAWN_DELAY = 10;

/**
* How many seconds {@link #PAGE_SPAWN_DELAY} may randomly shift up or down, re-rolled every
* round, so the moment the first pages appear stays unpredictable.
*
* @since 2.15.0
*/
public static final int PAGE_SPAWN_DELAY_JITTER = 2;

Expand All @@ -72,16 +70,12 @@ public record GameConfig(
* The page then has to come back on the spot it was found on. Without the delay it would be
* collectible again right away, letting a survivor pick up page after page on the same spot.
* </p>
*
* @since 2.15.0
*/
public static final int PAGE_RESPAWN_DELAY = 15;

/**
* How many seconds {@link #PAGE_RESPAWN_DELAY} may randomly shift up or down, re-rolled for every
* hidden page, so waiting next to the spot does not pay off.
*
* @since 2.15.0
*/
public static final int PAGE_RESPAWN_DELAY_JITTER = 5;

Expand Down Expand Up @@ -112,6 +106,7 @@ public record GameConfig(
DamageSound.DEFAULT,
Glitch.DEFAULT,
SlenderStatic.DEFAULT,
CreekConfig.DEFAULT,
DEFAULT_LOBBY_ATMOSPHERE_SHARE
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
* <li>slenderStaticFranticInterval</li>
* <li>slenderStaticMinVolume</li>
* <li>slenderStaticMaxVolume</li>
* <li>creek.* (see {@link CreekConfig})</li>
* </ul>
* <p>
* If a property can not be found in the file, the default value will be used.
Expand All @@ -66,6 +67,7 @@ public final class GameConfigReader {
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 static final String CREEK_PREFIX = "creek.";

private final Path path;

Expand Down Expand Up @@ -147,6 +149,7 @@ public GameConfig getConfig() {
getFloat(properties, "slenderStaticMinVolume", slenderStatic.minVolume()),
getFloat(properties, "slenderStaticMaxVolume", slenderStatic.maxVolume())
),
getCreek(properties),
getFloat(properties, "lobbyAtmosphereShare", GameConfig.DEFAULT.lobbyAtmosphereShare())
);
}
Expand Down Expand Up @@ -186,6 +189,80 @@ private float getFloat(Properties properties, String key, float defaultValue) {
}
}

/**
* Reads a decimal with double precision. The creek's values are compared with each other,
* and float rounding would turn 0.6 into 0.6000000238.
*
* @param properties the loaded properties
* @param key the key to read
* @param defaultValue the value to use when the key is absent or unreadable
* @return the parsed value
*/
private double getDouble(Properties properties, String key, double defaultValue) {
String value = properties.getProperty(key);
if (value == null) {
return defaultValue;
}
try {
return Double.parseDouble(value.trim());
} catch (NumberFormatException _) {
CONFIG_LOGGER.warn("Failed to parse decimal config value for key '{}': '{}'. Falling back to default: {}", key, value, defaultValue);
return defaultValue;
}
}

/**
* Reads the creek settings. All keys start with {@value #CREEK_PREFIX}.
* <p>
* Like every other group, an unreadable value falls back to its default, while values that
* contradict each other are rejected by {@link CreekConfig}.
* </p>
*
* @param properties the loaded properties
* @return the creek settings, never {@code null}
* @throws IllegalArgumentException if the values do not fit together
*/
private CreekConfig getCreek(Properties properties) {
CreekConfig d = CreekConfig.DEFAULT;
return new CreekConfig(
getBoolean(properties, CREEK_PREFIX + "enabled", d.enabled()),
getBoolean(properties, CREEK_PREFIX + "activeWithLastSurvivor", d.activeWithLastSurvivor()),
getInt(properties, CREEK_PREFIX + "sightRange", d.sightRange()),
getInt(properties, CREEK_PREFIX + "sightViewAngle", d.sightViewAngle()),
getInt(properties, CREEK_PREFIX + "wanderPauseMillis", d.wanderPauseMillis()),
getDouble(properties, CREEK_PREFIX + "wanderSpeed", d.wanderSpeed()),
getDouble(properties, CREEK_PREFIX + "huntSpeed", d.huntSpeed()),
getDouble(properties, CREEK_PREFIX + "stalkThreshold", d.stalkThreshold()),
getDouble(properties, CREEK_PREFIX + "huntThreshold", d.huntThreshold()),
getInt(properties, CREEK_PREFIX + "stalkMinDistance", d.stalkMinDistance()),
getInt(properties, CREEK_PREFIX + "stalkMaxDistance", d.stalkMaxDistance()),
getInt(properties, CREEK_PREFIX + "stalkMinAngle", d.stalkMinAngle()),
getInt(properties, CREEK_PREFIX + "stalkMaxAngle", d.stalkMaxAngle()),
getInt(properties, CREEK_PREFIX + "stalkRevealMillis", d.stalkRevealMillis()),
getInt(properties, CREEK_PREFIX + "stalkMinSeconds", d.stalkMinSeconds()),
getInt(properties, CREEK_PREFIX + "stalkMaxSeconds", d.stalkMaxSeconds()),
getInt(properties, CREEK_PREFIX + "huntMaxSeconds", d.huntMaxSeconds()),
getDouble(properties, CREEK_PREFIX + "catchDistance", d.catchDistance()),
getInt(properties, CREEK_PREFIX + "vanishMinSeconds", d.vanishMinSeconds()),
getInt(properties, CREEK_PREFIX + "vanishMaxSeconds", d.vanishMaxSeconds()),
getInt(properties, CREEK_PREFIX + "respawnMinDistance", d.respawnMinDistance()),
getInt(properties, CREEK_PREFIX + "personalSpace", d.personalSpace()),
getInt(properties, CREEK_PREFIX + "stuckMillis", d.stuckMillis()),
getDouble(properties, CREEK_PREFIX + "dreadPageWeight", d.dreadPageWeight()),
getDouble(properties, CREEK_PREFIX + "dreadTimeWeight", d.dreadTimeWeight()),
getDouble(properties, CREEK_PREFIX + "dreadIsolationWeight", d.dreadIsolationWeight()),
getInt(properties, CREEK_PREFIX + "isolationRadius", d.isolationRadius()),
getInt(properties, CREEK_PREFIX + "betrayalCatchCount", d.betrayalCatchCount()),
getDouble(properties, CREEK_PREFIX + "betrayalChance", d.betrayalChance()),
getInt(properties, CREEK_PREFIX + "betrayalGlowSeconds", d.betrayalGlowSeconds()),
getInt(properties, CREEK_PREFIX + "slownessSeconds", d.slownessSeconds()),
getDouble(properties, CREEK_PREFIX + "routeLinkDistance", d.routeLinkDistance()),
getDouble(properties, CREEK_PREFIX + "randomStopChance", d.randomStopChance()),
getInt(properties, CREEK_PREFIX + "randomStopMinMillis", d.randomStopMinMillis()),
getInt(properties, CREEK_PREFIX + "randomStopMaxMillis", d.randomStopMaxMillis())
);
}

/**
* Reads a flag from the properties. Anything other than {@code true} or {@code false} is not a
* decision the operator made on purpose, so it falls back to the default instead of silently
Expand Down
Loading
Loading