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 @@ -23,6 +23,9 @@
* @param teleportBlocks a single move longer than this is a teleport and not walked distance
* @param minSpacing how close two revealed prints may lie, in blocks
* @param fadeShare the share of the lifetime at its end that shows the faded stage, from 0 to 1
* @param scanStartDelaySeconds how long the tracking item stays locked after the round starts. All
* survivors start at the same spot, so an early scan would show where
* each of them ran off to
* @author Joltra
* @version 1.0.0
* @since 2.16.0
Expand All @@ -42,7 +45,8 @@ public record FootprintConfig(
int scanCooldownSeconds,
double teleportBlocks,
double minSpacing,
double fadeShare
double fadeShare,
int scanStartDelaySeconds
) {

/**
Expand All @@ -52,7 +56,8 @@ public record FootprintConfig(
3.0D, 0.25D, 1000, 2000, 12,
2.0D, 30,
40, 5, 40, 15, 120,
8.0D, 0.7D, 0.33D);
8.0D, 0.7D, 0.33D,
150);

/**
* Checks the values.
Expand Down Expand Up @@ -94,6 +99,9 @@ public record FootprintConfig(
if (fadeShare < 0.0D || fadeShare > 1.0D) {
throw new IllegalArgumentException("fadeShare (" + fadeShare + ") must be between 0.0 and 1.0");
}
if (scanStartDelaySeconds < 0) {
throw new IllegalArgumentException("scanStartDelaySeconds (" + scanStartDelaySeconds + ") must not be negative");
}
}

/**
Expand All @@ -119,7 +127,8 @@ public static FootprintConfig read(ConfigSection section) {
section.getInt("scanCooldownSeconds", defaults.scanCooldownSeconds()),
section.getDouble("teleportBlocks", defaults.teleportBlocks()),
section.getDouble("minSpacing", defaults.minSpacing()),
section.getDouble("fadeShare", defaults.fadeShare())
section.getDouble("fadeShare", defaults.fadeShare()),
section.getInt("scanStartDelaySeconds", defaults.scanStartDelaySeconds())
), defaults);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,24 @@
* @param cooldownSeconds how long the slender waits before he may possess the creek again
* @param sightFactor how much farther survivors notice the creek while it is possessed. 1 keeps
* the normal range
* @param startDelaySeconds how long the slender waits after the round starts before he may possess
* the creek the first time
* @author theEvilReaper
* @version 1.0.0
* @since 2.16.0
*/
public record PossessionConfig(
int maxSeconds,
int cooldownSeconds,
double sightFactor
double sightFactor,
int startDelaySeconds
) {

/**
* The default settings: a short look, a long wait, and half again the normal sight range.
* The default settings: a short look, a long wait, half again the normal sight range, and a
* locked first minute and a half.
*/
public static final PossessionConfig DEFAULT = new PossessionConfig(8, 60, 1.5D);
public static final PossessionConfig DEFAULT = new PossessionConfig(8, 60, 1.5D, 90);

/**
* Checks the values.
Expand All @@ -42,6 +46,9 @@ public record PossessionConfig(
if (sightFactor < 1.0D) {
throw new IllegalArgumentException("sightFactor (" + sightFactor + ") must be at least 1.0");
}
if (startDelaySeconds < 0) {
throw new IllegalArgumentException("startDelaySeconds (" + startDelaySeconds + ") must not be negative");
}
}

/**
Expand All @@ -56,7 +63,8 @@ public static PossessionConfig read(ConfigSection section) {
return section.orDefault("possession", () -> new PossessionConfig(
section.getInt("maxSeconds", defaults.maxSeconds()),
section.getInt("cooldownSeconds", defaults.cooldownSeconds()),
section.getDouble("sightFactor", defaults.sightFactor())
section.getDouble("sightFactor", defaults.sightFactor()),
section.getInt("startDelaySeconds", defaults.startDelaySeconds())
), defaults);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,16 @@ void defaults() {
assertEquals(8.0D, D.teleportBlocks());
assertEquals(0.7D, D.minSpacing());
assertEquals(0.33D, D.fadeShare());
assertEquals(150, D.scanStartDelaySeconds());
}

@Test
@DisplayName("The start delay may be 0 but not negative")
void startDelayRange() {
assertRejected("scanStartDelaySeconds", () -> new FootprintConfig(3.0D, 0.25D, 1000, 2000, 12,
2.0D, 30, 40, 5, 40, 15, 120, 8.0D, 0.7D, 0.33D, -1));
assertDoesNotThrow(() -> new FootprintConfig(3.0D, 0.25D, 1000, 2000, 12,
2.0D, 30, 40, 5, 40, 15, 120, 8.0D, 0.7D, 0.33D, 0));
}

@Test
Expand All @@ -54,46 +64,46 @@ void chanceRange() {
@DisplayName("The longest delay may not be shorter than the shortest")
void delayOrder() {
assertRejected("slenderDelayMaxMillis", () -> new FootprintConfig(3.0D, 0.25D, 2000, 1000, 12,
2.0D, 30, 40, 5, 40, 15, 120, 8.0D, 0.7D, 0.33D));
2.0D, 30, 40, 5, 40, 15, 120, 8.0D, 0.7D, 0.33D, 90));
assertRejected("slenderDelayMinMillis", () -> new FootprintConfig(3.0D, 0.25D, -1, 1000, 12,
2.0D, 30, 40, 5, 40, 15, 120, 8.0D, 0.7D, 0.33D));
2.0D, 30, 40, 5, 40, 15, 120, 8.0D, 0.7D, 0.33D, 90));
}

@Test
@DisplayName("The gap has to be shorter than the history, or a scan never finds anything")
void gapShorterThanHistory() {
assertRejected("scanGapSeconds", () -> new FootprintConfig(3.0D, 0.25D, 1000, 2000, 12,
2.0D, 30, 40, 30, 40, 15, 120, 8.0D, 0.7D, 0.33D));
2.0D, 30, 40, 30, 40, 15, 120, 8.0D, 0.7D, 0.33D, 90));
}

@Test
@DisplayName("A teleport has to be longer than a step and a sample")
void teleportLongerThanSteps() {
assertRejected("teleportBlocks", () -> new FootprintConfig(3.0D, 0.25D, 1000, 2000, 12,
2.0D, 30, 40, 5, 40, 15, 120, 3.0D, 0.7D, 0.33D));
2.0D, 30, 40, 5, 40, 15, 120, 3.0D, 0.7D, 0.33D, 90));
}

@Test
@DisplayName("Counts and durations need at least 1")
void lowerBounds() {
assertRejected("slenderLifetimeSeconds", () -> new FootprintConfig(3.0D, 0.25D, 1000, 2000, 0,
2.0D, 30, 40, 5, 40, 15, 120, 8.0D, 0.7D, 0.33D));
2.0D, 30, 40, 5, 40, 15, 120, 8.0D, 0.7D, 0.33D, 90));
assertRejected("scanRadius", () -> new FootprintConfig(3.0D, 0.25D, 1000, 2000, 12,
2.0D, 30, 0, 5, 40, 15, 120, 8.0D, 0.7D, 0.33D));
2.0D, 30, 0, 5, 40, 15, 120, 8.0D, 0.7D, 0.33D, 90));
assertRejected("scanMaxPrints", () -> new FootprintConfig(3.0D, 0.25D, 1000, 2000, 12,
2.0D, 30, 40, 5, 0, 15, 120, 8.0D, 0.7D, 0.33D));
2.0D, 30, 40, 5, 0, 15, 120, 8.0D, 0.7D, 0.33D, 90));
assertRejected("scanCooldownSeconds", () -> new FootprintConfig(3.0D, 0.25D, 1000, 2000, 12,
2.0D, 30, 40, 5, 40, 15, -1, 8.0D, 0.7D, 0.33D));
2.0D, 30, 40, 5, 40, 15, -1, 8.0D, 0.7D, 0.33D, 90));
assertRejected("slenderStepBlocks", () -> new FootprintConfig(0.0D, 0.25D, 1000, 2000, 12,
2.0D, 30, 40, 5, 40, 15, 120, 8.0D, 0.7D, 0.33D));
2.0D, 30, 40, 5, 40, 15, 120, 8.0D, 0.7D, 0.33D, 90));
}

private static FootprintConfig withChance(double chance) {
return new FootprintConfig(3.0D, chance, 1000, 2000, 12, 2.0D, 30, 40, 5, 40, 15, 120, 8.0D, 0.7D, 0.33D);
return new FootprintConfig(3.0D, chance, 1000, 2000, 12, 2.0D, 30, 40, 5, 40, 15, 120, 8.0D, 0.7D, 0.33D, 90);
}

private static FootprintConfig withFadeShare(double fadeShare) {
return new FootprintConfig(3.0D, 0.25D, 1000, 2000, 12, 2.0D, 30, 40, 5, 40, 15, 120, 8.0D, 0.7D, fadeShare);
return new FootprintConfig(3.0D, 0.25D, 1000, 2000, 12, 2.0D, 30, 40, 5, 40, 15, 120, 8.0D, 0.7D, fadeShare, 90);
}

private static void assertRejected(String field, Executable executable) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -466,13 +466,15 @@ void testPossessionValuesAreRead(@TempDir Path tempDir) throws IOException {
possession.maxSeconds=5
possession.cooldownSeconds=90
possession.sightFactor=2.0
possession.startDelaySeconds=45
""");

PossessionConfig possession = new GameConfigReader(tempDir).getConfig().possession();

assertEquals(5, possession.maxSeconds());
assertEquals(90, possession.cooldownSeconds());
assertEquals(2.0D, possession.sightFactor(), 1.0E-9);
assertEquals(45, possession.startDelaySeconds());
}

@Test
Expand Down Expand Up @@ -514,6 +516,7 @@ void testFootprintValuesAreRead(@TempDir Path tempDir) throws IOException {
footprint.scanRadius=25
footprint.scanCooldownSeconds=90
footprint.fadeShare=0.5
footprint.scanStartDelaySeconds=30
""");

FootprintConfig footprint = new GameConfigReader(tempDir).getConfig().footprint();
Expand All @@ -522,6 +525,7 @@ void testFootprintValuesAreRead(@TempDir Path tempDir) throws IOException {
assertEquals(25, footprint.scanRadius());
assertEquals(90, footprint.scanCooldownSeconds());
assertEquals(0.5D, footprint.fadeShare(), 1.0E-9);
assertEquals(30, footprint.scanStartDelaySeconds());
assertEquals(FootprintConfig.DEFAULT.scanMaxPrints(), footprint.scanMaxPrints());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,25 @@
class PossessionConfigTest {

@Test
@DisplayName("The possession lasts 8 seconds, has a 60 second cooldown and widens the sight by half")
@DisplayName("The possession lasts 8 seconds, has a 60 second cooldown, widens the sight by half and unlocks after 90 seconds")
void defaultsMatchTheDesign() {
assertEquals(8, PossessionConfig.DEFAULT.maxSeconds());
assertEquals(60, PossessionConfig.DEFAULT.cooldownSeconds());
assertEquals(1.5D, PossessionConfig.DEFAULT.sightFactor());
assertEquals(90, PossessionConfig.DEFAULT.startDelaySeconds());
}

@Test
@DisplayName("The possession needs a duration above 0, no negative cooldown and a factor of at least 1")
@DisplayName("The possession needs a duration above 0, no negative cooldown or start delay and a factor of at least 1")
void valuesAreChecked() {
assertTrue(assertThrows(IllegalArgumentException.class, () -> new PossessionConfig(0, 60, 1.5D))
assertTrue(assertThrows(IllegalArgumentException.class, () -> new PossessionConfig(0, 60, 1.5D, 90))
.getMessage().contains("maxSeconds"));
assertTrue(assertThrows(IllegalArgumentException.class, () -> new PossessionConfig(8, -1, 1.5D))
assertTrue(assertThrows(IllegalArgumentException.class, () -> new PossessionConfig(8, -1, 1.5D, 90))
.getMessage().contains("cooldownSeconds"));
assertTrue(assertThrows(IllegalArgumentException.class, () -> new PossessionConfig(8, 60, 0.9D))
assertTrue(assertThrows(IllegalArgumentException.class, () -> new PossessionConfig(8, 60, 0.9D, 90))
.getMessage().contains("sightFactor"));
new PossessionConfig(1, 0, 1.0D);
assertTrue(assertThrows(IllegalArgumentException.class, () -> new PossessionConfig(8, 60, 1.5D, -1))
.getMessage().contains("startDelaySeconds"));
new PossessionConfig(1, 0, 1.0D, 0);
}
}
6 changes: 5 additions & 1 deletion game/src/main/java/net/onelitefeather/cygnus/Cygnus.java
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
import net.onelitefeather.cygnus.phase.RestartPhase;
import net.onelitefeather.cygnus.phase.WaitingPhase;
import net.onelitefeather.cygnus.player.CygnusPlayer;
import net.onelitefeather.cygnus.player.InventoryLock;
import net.onelitefeather.cygnus.possession.PossessionFeature;
import net.onelitefeather.cygnus.resourcepack.ResourcePackService;
import net.onelitefeather.cygnus.sanity.SanityService;
Expand Down Expand Up @@ -233,6 +234,7 @@ sanityService, new HuntedTabWitness(new TracingCreekWitness(sanityService, this.
MinecraftServer.getConnectionManager()::getOnlinePlayerByUuid), this.jumpscareManager, this.staminaService, tickSections);
this.features = Stream.concat(this.resourcePackService.stream(), Stream.of(
new EpilepsyDisclaimer(),
new InventoryLock(),
this.slenderTakeover,
this.spectatorService,
// Not part of the OverlayModule: the sound is the feedback a hit owes the player
Expand All @@ -251,7 +253,9 @@ sanityService, new HuntedTabWitness(new TracingCreekWitness(sanityService, this.
this.gameConfig.adrenaline(),
() -> TeamHelper.survivorsOf(this.teamService),
System::currentTimeMillis),
new FootprintFeature(this.gameConfig.footprint(), System::currentTimeMillis),
new FootprintFeature(this.gameConfig.footprint(),
() -> TeamHelper.slenderOf(this.teamService),
System::currentTimeMillis),
new OverlayModule(this.gameConfig.glitch(), this.gameConfig.pageGlitch(), this.teamService,
this.staminaService, tickSections)
)).toList();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,20 @@
import net.onelitefeather.cygnus.event.SlenderReviveEvent;
import net.onelitefeather.cygnus.team.TeamHelper;
import net.onelitefeather.cygnus.utils.Items;
import org.jetbrains.annotations.Nullable;

import java.util.SplittableRandom;
import java.util.function.LongSupplier;
import java.util.function.Supplier;
import java.util.random.RandomGenerator;

/**
* Footprints for both sides.
* <p>
* The hidden slender now and then leaves a print that only survivors see. Survivors are recorded
* all the time, and the slender reveals their recent tracks around him with his tracker. Moves only
* count while a round runs, and the round end takes every print and every track away.
* count while a round runs, and the round end takes every print and every track away. The tracker
* starts each round locked for a while.
* </p>
*
* @author Joltra
Expand All @@ -41,26 +44,31 @@ public final class FootprintFeature implements GameFeature {
private final SurvivorTrackLog log;
private final SlenderTrail trail;
private final TrackingScan scan;
private final Supplier<@Nullable Player> slender;
private volatile boolean running;

/**
* Creates the feature.
*
* @param config the footprint values
* @param clock supplies the current time in milliseconds
* @param config the footprint values
* @param slender supplies the slender of the round, or {@code null} while there is none
* @param clock supplies the current time in milliseconds
*/
public FootprintFeature(FootprintConfig config, LongSupplier clock) {
this(config, clock, new SplittableRandom());
public FootprintFeature(FootprintConfig config, Supplier<@Nullable Player> slender, LongSupplier clock) {
this(config, slender, clock, new SplittableRandom());
}

/**
* Creates the feature with a given random source, for tests.
*
* @param config the footprint values
* @param clock supplies the current time in milliseconds
* @param random the source for rolls, delays and facings
* @param config the footprint values
* @param slender supplies the slender of the round, or {@code null} while there is none
* @param clock supplies the current time in milliseconds
* @param random the source for rolls, delays and facings
*/
FootprintFeature(FootprintConfig config, LongSupplier clock, RandomGenerator random) {
FootprintFeature(FootprintConfig config, Supplier<@Nullable Player> slender, LongSupplier clock,
RandomGenerator random) {
this.slender = slender;
this.clock = clock;
this.spawner = new FootprintSpawner(random, config.fadeShare());
this.log = new SurvivorTrackLog(config);
Expand All @@ -75,7 +83,7 @@ public EventNode<Event> node() {
}

private void registerListeners() {
this.node.addListener(GameStartEvent.class, _ -> this.running = true);
this.node.addListener(GameStartEvent.class, _ -> this.started());
this.node.addListener(PlayerMoveEvent.class, this::moved);
this.node.addListener(PlayerUseItemEvent.class, this::used);
this.node.addListener(PlayerChunkLoadEvent.class,
Expand All @@ -86,6 +94,16 @@ private void registerListeners() {
this.node.addListener(GameFinishEvent.class, _ -> this.stop());
}

/**
* Starts the round with the tracker locked, and shows the slender how long it stays locked.
*/
private void started() {
this.running = true;
this.scan.start();
Player current = this.slender.get();
if (current != null) this.scan.showCooldown(current);
}

private void moved(PlayerMoveEvent event) {
if (!this.running) return;
Player player = event.getPlayer();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
* The newest seconds are left out, so the prints never lead straight to a survivor. A scan that
* finds nothing still costs the cooldown. That is an answer too, and it keeps the tracker from
* being a free probe. The cooldown belongs to the slender role, so a new slender after a
* takeover inherits what is left of it.
* takeover inherits what is left of it. A round starts with the tracker locked for a while: all
* survivors start at the same spot, and an early scan would show where each of them ran off to.
* </p>
*
* @author Joltra
Expand Down Expand Up @@ -92,6 +93,13 @@ boolean use(Player slender) {
return true;
}

/**
* Locks the tracker for the start delay of a new round.
*/
void start() {
this.readyAt = this.clock.getAsLong() + this.config.scanStartDelaySeconds() * 1000L;
}

/**
* Shows a slender what is left of the cooldown, for example after a takeover.
*
Expand Down
Loading
Loading