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 0f9ee1c3..ddc25fb3 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
@@ -36,6 +36,29 @@ public sealed interface GameConfig permits GameConfigImpl, InternalGameConfig {
int MIN_ACTIVE_PAGE_COUNT = 4 * 2;
+ /**
+ * How many seconds after a round starts before the first pages spawn.
+ *
+ * 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.
+ *
+ *
+ * @since 2.15.0
+ */
+ int PAGE_SPAWN_DELAY = 10;
+
+ /**
+ * How many seconds {@link #PAGE_SPAWN_DELAY} may randomly shift up or down, re-rolled every
+ * round.
+ *
+ * Without this the delay lands on the exact same tick every round, which players learn and
+ * plan around; the jitter keeps the moment the first pages appear unpredictable.
+ *
+ *
+ * @since 2.15.0
+ */
+ int PAGE_SPAWN_DELAY_JITTER = 2;
+
int PAGE_TTL_TIME = 60;
int FORCE_START_TIME = 11;
diff --git a/common/src/main/java/net/onelitefeather/cygnus/common/page/PageCalculation.java b/common/src/main/java/net/onelitefeather/cygnus/common/page/PageCalculation.java
index 8da41b60..aa2578ad 100644
--- a/common/src/main/java/net/onelitefeather/cygnus/common/page/PageCalculation.java
+++ b/common/src/main/java/net/onelitefeather/cygnus/common/page/PageCalculation.java
@@ -3,6 +3,8 @@
import net.minestom.server.MinecraftServer;
import net.onelitefeather.cygnus.common.config.GameConfig;
+import java.util.concurrent.ThreadLocalRandom;
+
/**
* Utility class for calculating the number of pages allocated for the dynamic page system.
* The page count is based on the number of current online players.
@@ -17,23 +19,69 @@ public final class PageCalculation {
private static final int PLAYER_SIZE_FOR_DYNAMIC_PAGE_ALLOCATION = 4;
private static final int PAGE_COUNT_MULTIPLIER = 2;
+ /**
+ * The largest amount {@link #calculatePageAmount()} may add on top of the base amount, so the
+ * total can't be worked out in advance from the player count alone.
+ *
+ * Not exposed through {@link GameConfig}: unlike the other page settings, this one is
+ * deliberately not something a server operator should be able to turn off or tune.
+ *
+ */
+ private static final int PAGE_COUNT_JITTER_MAX = 2;
+
+ private static final int PLAYER_SIZE_FOR_ACTIVE_PAGE_SCALING = 8;
+
+ /**
+ * How many extra survivors above {@value #PLAYER_SIZE_FOR_ACTIVE_PAGE_SCALING} it takes for
+ * {@link #calculateActivePageAmount()} to add one more page.
+ *
+ * Playtesting a steeper step (one active page per extra survivor) reached 16 concurrently
+ * active pages, which felt like clutter; this keeps the top of the range below 10.
+ *
+ */
+ private static final int ACTIVE_PAGE_COUNT_STEP_SIZE = 3;
+
/**
* Calculates the number of pages to allocate for the dynamic page system.
*
* If the number of online players (excluding one) is less than {@value #PLAYER_SIZE_FOR_DYNAMIC_PAGE_ALLOCATION},
- * {@link GameConfig#MIN_PAGE_COUNT} is returned. Otherwise, the page count is determined by
- * multiplying the adjusted player count by {@value #PAGE_COUNT_MULTIPLIER}.
+ * the base amount is {@link GameConfig#MIN_PAGE_COUNT}. Otherwise, it is the adjusted player
+ * count multiplied by {@value #PAGE_COUNT_MULTIPLIER}. A random amount between 0 and
+ * {@value #PAGE_COUNT_JITTER_MAX} is then added on top, re-rolled every call, so the total
+ * can't be derived from the player count alone.
*
* @return the number of pages to allocate, at least {@link GameConfig#MIN_PAGE_COUNT}
*/
public static int calculatePageAmount() {
int currentPlayers = MinecraftServer.getConnectionManager().getOnlinePlayers().size();
- if (currentPlayers - 1 < PLAYER_SIZE_FOR_DYNAMIC_PAGE_ALLOCATION) {
- return GameConfig.MIN_PAGE_COUNT;
- } else {
- return (currentPlayers - 1) * PAGE_COUNT_MULTIPLIER;
+ int baseAmount = currentPlayers - 1 < PLAYER_SIZE_FOR_DYNAMIC_PAGE_ALLOCATION
+ ? GameConfig.MIN_PAGE_COUNT
+ : (currentPlayers - 1) * PAGE_COUNT_MULTIPLIER;
+
+ return baseAmount + ThreadLocalRandom.current().nextInt(PAGE_COUNT_JITTER_MAX + 1);
+ }
+
+ /**
+ * Calculates how many pages should be concurrently active (spawned in the world at once).
+ *
+ * This stays below {@link #calculatePageAmount()} on purpose: it governs how many pages exist
+ * in the world at the same time, not the total pool a round draws from. If the number of online
+ * players (excluding one) is less than {@value #PLAYER_SIZE_FOR_ACTIVE_PAGE_SCALING},
+ * {@link GameConfig#MIN_ACTIVE_PAGE_COUNT} is returned. Otherwise, one page is added for every
+ * {@value #ACTIVE_PAGE_COUNT_STEP_SIZE} survivors past that threshold, so the count stays flat
+ * for most of the range and only rises near the top of a full lobby.
+ *
+ * @return the number of pages to keep active at once, at least {@link GameConfig#MIN_ACTIVE_PAGE_COUNT}
+ */
+ public static int calculateActivePageAmount() {
+ int currentPlayers = MinecraftServer.getConnectionManager().getOnlinePlayers().size();
+ int survivors = currentPlayers - 1;
+
+ if (survivors < PLAYER_SIZE_FOR_ACTIVE_PAGE_SCALING) {
+ return GameConfig.MIN_ACTIVE_PAGE_COUNT;
}
+ return GameConfig.MIN_ACTIVE_PAGE_COUNT + (survivors - PLAYER_SIZE_FOR_ACTIVE_PAGE_SCALING) / ACTIVE_PAGE_COUNT_STEP_SIZE;
}
private PageCalculation() {
diff --git a/common/src/main/java/net/onelitefeather/cygnus/common/page/PageProvider.java b/common/src/main/java/net/onelitefeather/cygnus/common/page/PageProvider.java
index 27050dd9..3014ea9b 100644
--- a/common/src/main/java/net/onelitefeather/cygnus/common/page/PageProvider.java
+++ b/common/src/main/java/net/onelitefeather/cygnus/common/page/PageProvider.java
@@ -30,8 +30,6 @@
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicInteger;
-import static net.onelitefeather.cygnus.common.config.GameConfig.MIN_ACTIVE_PAGE_COUNT;
-
/**
* Handles the logic to manage and spawn pages during the {@link GamePhase}.
*
@@ -77,18 +75,20 @@ public void loadPageData(Set resources) {
this.globalCache.addAll(shuffled);
}
- public void collectStartPages(Instance instance) {
- Check.argCondition(this.globalCache.size() < MIN_ACTIVE_PAGE_COUNT, "Not enough pages to start the game");
+ /**
+ * Collects the pages that should be active in the world when a round starts.
+ *
+ * @param instance the instance to spawn the pages in
+ * @param activePageCount how many pages to keep concurrently active, e.g. from {@link PageCalculation#calculateActivePageAmount()}
+ */
+ public void collectStartPages(Instance instance, int activePageCount) {
+ Check.argCondition(this.globalCache.size() < activePageCount, "Not enough pages to start the game");
var counter = 0;
Set candidateHashes = new HashSet<>();
- while (counter < MIN_ACTIVE_PAGE_COUNT && !this.globalCache.isEmpty()) {
+ while (counter < activePageCount && !this.globalCache.isEmpty()) {
var page = this.globalCache.poll();
- if (page == null) {
- break;
- }
-
if (candidateHashes.add(page.hashCode())) {
Direction direction = page.face();
var position = Helper.updatePosition(page.position().asPos(), direction);
diff --git a/common/src/test/java/net/onelitefeather/cygnus/common/page/PageCalculationTest.java b/common/src/test/java/net/onelitefeather/cygnus/common/page/PageCalculationTest.java
index a9547738..b28493cf 100644
--- a/common/src/test/java/net/onelitefeather/cygnus/common/page/PageCalculationTest.java
+++ b/common/src/test/java/net/onelitefeather/cygnus/common/page/PageCalculationTest.java
@@ -8,11 +8,21 @@
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
+import java.util.HashSet;
+import java.util.Set;
+
import static org.junit.jupiter.api.Assertions.*;
@ExtendWith(MicrotusExtension.class)
class PageCalculationTest {
+ /**
+ * The page count carries an unpredictable +0..+2 on top of the base amount (see
+ * {@link #testPageCalculationVariesAcrossRounds}), so exact-value assertions elsewhere in this
+ * class check a range instead of a single number.
+ */
+ private static final int MAX_JITTER = 2;
+
@Test
void testPageCalculationWithoutScaling(@NotNull Env env) {
Instance instance = env.createFlatInstance();
@@ -22,7 +32,8 @@ void testPageCalculationWithoutScaling(@NotNull Env env) {
}
int pageCount = PageCalculation.calculatePageAmount();
- assertEquals(GameConfig.MIN_PAGE_COUNT, pageCount);
+ assertTrue(pageCount >= GameConfig.MIN_PAGE_COUNT && pageCount <= GameConfig.MIN_PAGE_COUNT + MAX_JITTER,
+ "the page count must be the minimum plus at most the jitter, was " + pageCount);
env.destroyInstance(instance, true);
}
@@ -36,7 +47,75 @@ void testPageCalculationWithScaling(@NotNull Env env) {
}
int pageCount = PageCalculation.calculatePageAmount();
- assertEquals(18, pageCount);
+ assertTrue(pageCount >= 18 && pageCount <= 18 + MAX_JITTER,
+ "the page count must be the scaled amount plus at most the jitter, was " + pageCount);
+
+ env.destroyInstance(instance, true);
+ }
+
+ @Test
+ void testPageCalculationVariesAcrossRounds(@NotNull Env env) {
+ Instance instance = env.createFlatInstance();
+
+ for (int i = 0; i < 10; i++) {
+ env.createPlayer(instance);
+ }
+
+ Set observed = new HashSet<>();
+ for (int i = 0; i < 50; i++) {
+ observed.add(PageCalculation.calculatePageAmount());
+ }
+
+ assertTrue(observed.size() > 1,
+ "the page count must vary across rounds instead of always landing on the same number, observed " + observed);
+ for (int value : observed) {
+ assertTrue(value >= 18 && value <= 18 + MAX_JITTER,
+ "each roll must stay within the base amount plus at most the jitter, was " + value);
+ }
+
+ env.destroyInstance(instance, true);
+ }
+
+ @Test
+ void testActivePageCalculationWithoutScaling(@NotNull Env env) {
+ Instance instance = env.createFlatInstance();
+
+ for (int i = 0; i < 3; i++) {
+ env.createPlayer(instance);
+ }
+
+ int activePageCount = PageCalculation.calculateActivePageAmount();
+ assertEquals(GameConfig.MIN_ACTIVE_PAGE_COUNT, activePageCount);
+
+ env.destroyInstance(instance, true);
+ }
+
+ @Test
+ void testActivePageCalculationStaysFlatUntilNearTheTopOfTheRange(@NotNull Env env) {
+ Instance instance = env.createFlatInstance();
+
+ for (int i = 0; i < 11; i++) {
+ env.createPlayer(instance);
+ }
+
+ int activePageCount = PageCalculation.calculateActivePageAmount();
+ assertEquals(GameConfig.MIN_ACTIVE_PAGE_COUNT, activePageCount,
+ "the active page count must still be at the minimum just below the top of the range");
+
+ env.destroyInstance(instance, true);
+ }
+
+ @Test
+ void testActivePageCalculationWithScaling(@NotNull Env env) {
+ Instance instance = env.createFlatInstance();
+
+ for (int i = 0; i < 13; i++) {
+ env.createPlayer(instance);
+ }
+
+ int activePageCount = PageCalculation.calculateActivePageAmount();
+ assertEquals(9, activePageCount,
+ "the active page count at the top of the range must stay below 10");
env.destroyInstance(instance, true);
}
diff --git a/common/src/test/java/net/onelitefeather/cygnus/common/page/PageProviderTest.java b/common/src/test/java/net/onelitefeather/cygnus/common/page/PageProviderTest.java
index b417835d..d8296260 100644
--- a/common/src/test/java/net/onelitefeather/cygnus/common/page/PageProviderTest.java
+++ b/common/src/test/java/net/onelitefeather/cygnus/common/page/PageProviderTest.java
@@ -272,6 +272,54 @@ void testAClaimOnAnUnknownPageFiresNoEvent(@NotNull Env env) throws Exception {
env.destroyInstance(instance, true);
}
+ @Test
+ void testCollectStartPagesUsesTheGivenActivePageCount(@NotNull Env env) throws Exception {
+ Instance instance = env.createFlatInstance();
+ int activePageCount = 12;
+
+ PageProvider pageProvider = new PageProvider();
+ pageProvider.loadPageData(
+ IntStream.range(0, activePageCount)
+ .mapToObj(i -> new PageResource(new Pos(i, 0, 0), Direction.NORTH))
+ .collect(Collectors.toSet())
+ );
+
+ pageProvider.collectStartPages(instance, activePageCount);
+
+ assertEquals(activePageCount, activePageCount(pageProvider),
+ "collectStartPages must collect exactly the requested active page count");
+
+ env.destroyInstance(instance, true);
+ }
+
+ @Test
+ void testCollectStartPagesRejectsAnActivePageCountAboveTheAvailableData(@NotNull Env env) {
+ Instance instance = env.createFlatInstance();
+
+ PageProvider pageProvider = new PageProvider();
+ pageProvider.loadPageData(
+ IntStream.range(0, 8)
+ .mapToObj(i -> new PageResource(new Pos(i, 0, 0), Direction.NORTH))
+ .collect(Collectors.toSet())
+ );
+
+ IllegalArgumentException exception = assertThrows(
+ IllegalArgumentException.class,
+ () -> pageProvider.collectStartPages(instance, 12)
+ );
+ assertEquals("Not enough pages to start the game", exception.getMessage());
+
+ env.destroyInstance(instance, true);
+ }
+
+ @SuppressWarnings("unchecked")
+ private static int activePageCount(PageProvider pageProvider) throws ReflectiveOperationException {
+ Field field = PageProvider.class.getDeclaredField("activePages");
+ field.setAccessible(true);
+ Map activePages = (Map) field.get(pageProvider);
+ return activePages.size();
+ }
+
private static String plainStatus(PageProvider pageProvider) {
return PlainTextComponentSerializer.plainText().serialize(pageProvider.getPageStatus());
}
diff --git a/game/src/main/java/net/onelitefeather/cygnus/listener/game/GameStartListener.java b/game/src/main/java/net/onelitefeather/cygnus/listener/game/GameStartListener.java
index dea57821..28093fa2 100644
--- a/game/src/main/java/net/onelitefeather/cygnus/listener/game/GameStartListener.java
+++ b/game/src/main/java/net/onelitefeather/cygnus/listener/game/GameStartListener.java
@@ -1,8 +1,10 @@
package net.onelitefeather.cygnus.listener.game;
import net.kyori.adventure.text.Component;
+import net.minestom.server.MinecraftServer;
import net.minestom.server.entity.Player;
import net.minestom.server.event.EventDispatcher;
+import net.minestom.server.timer.TaskSchedule;
import net.onelitefeather.cygnus.ambient.AmbientProvider;
import net.onelitefeather.cygnus.common.Messages;
import net.onelitefeather.cygnus.common.Tags;
@@ -19,10 +21,13 @@
import net.theevilreaper.xerus.api.team.Team;
import net.theevilreaper.xerus.api.team.TeamService;
+import java.util.concurrent.ThreadLocalRandom;
import java.util.function.Consumer;
public final class GameStartListener implements Consumer {
+ private static final int TICKS_PER_SECOND = 20;
+
private final TeamService teamService;
private final AmbientProvider ambientProvider;
private final StaminaService staminaService;
@@ -72,10 +77,23 @@ private void handleSurvivorStart() {
private void startGlobalMechanics() {
this.staminaService.start();
- EventDispatcher.call(new PageSpawnEvent());
this.ambientProvider.startTask();
- // Started after the pages exist: PageSpawnEvent above is what fills the active page map.
- this.pageProximityService.startTask();
+ // Delayed so survivors get a moment to move away from the spawn point before the first
+ // pages appear, instead of one being reachable the instant the round starts. The proximity
+ // task starts alongside it, in the same task, since it depends on pages already existing.
+ // Jittered so the moment doesn't land on the exact same tick every round.
+ MinecraftServer.getSchedulerManager().buildTask(() -> {
+ EventDispatcher.call(new PageSpawnEvent());
+ this.pageProximityService.startTask();
+ }).delay(TaskSchedule.tick(randomizedSpawnDelayTicks())).schedule();
TeamHelper.updateTabList(this.teamService);
}
+
+ private int randomizedSpawnDelayTicks() {
+ int baseTicks = GameConfig.PAGE_SPAWN_DELAY * TICKS_PER_SECOND;
+ int jitterTicks = GameConfig.PAGE_SPAWN_DELAY_JITTER * TICKS_PER_SECOND;
+ ThreadLocalRandom current = ThreadLocalRandom.current();
+ int offset = current.nextInt(2 * jitterTicks + 1) - jitterTicks;
+ return baseTicks + offset;
+ }
}
diff --git a/game/src/main/java/net/onelitefeather/cygnus/listener/page/PageSpawnListener.java b/game/src/main/java/net/onelitefeather/cygnus/listener/page/PageSpawnListener.java
index 6dda0456..76a22f54 100644
--- a/game/src/main/java/net/onelitefeather/cygnus/listener/page/PageSpawnListener.java
+++ b/game/src/main/java/net/onelitefeather/cygnus/listener/page/PageSpawnListener.java
@@ -1,6 +1,7 @@
package net.onelitefeather.cygnus.listener.page;
import net.minestom.server.instance.Instance;
+import net.onelitefeather.cygnus.common.page.PageCalculation;
import net.onelitefeather.cygnus.common.page.PageProvider;
import net.onelitefeather.cygnus.common.page.event.PageSpawnEvent;
@@ -30,7 +31,7 @@ public void accept(PageSpawnEvent event) {
if (activeInstance == null) {
throw new IllegalStateException("Active instance not available for page collection");
}
- this.pageProvider.collectStartPages(activeInstance);
+ this.pageProvider.collectStartPages(activeInstance, PageCalculation.calculateActivePageAmount());
this.pageProvider.spawn();
}
}
diff --git a/game/src/test/java/net/onelitefeather/cygnus/listener/game/GameStartListenerTest.java b/game/src/test/java/net/onelitefeather/cygnus/listener/game/GameStartListenerTest.java
new file mode 100644
index 00000000..d1b18a78
--- /dev/null
+++ b/game/src/test/java/net/onelitefeather/cygnus/listener/game/GameStartListenerTest.java
@@ -0,0 +1,104 @@
+package net.onelitefeather.cygnus.listener.game;
+
+import net.minestom.server.instance.Instance;
+import net.minestom.testing.Env;
+import net.onelitefeather.cygnus.CygnusPlayerTestBase;
+import net.onelitefeather.cygnus.ambient.AmbientProvider;
+import net.onelitefeather.cygnus.common.config.GameConfig;
+import net.onelitefeather.cygnus.common.config.GameConfigReader;
+import net.onelitefeather.cygnus.common.page.PageProvider;
+import net.onelitefeather.cygnus.common.page.event.PageSpawnEvent;
+import net.onelitefeather.cygnus.event.GameStartEvent;
+import net.onelitefeather.cygnus.page.PageProximityService;
+import net.onelitefeather.cygnus.player.CygnusPlayer;
+import net.onelitefeather.cygnus.stamina.StaminaService;
+import net.onelitefeather.cygnus.team.TeamCreator;
+import net.theevilreaper.xerus.api.team.Team;
+import net.theevilreaper.xerus.api.team.TeamService;
+import org.jetbrains.annotations.NotNull;
+import org.junit.jupiter.api.Test;
+
+import java.nio.file.Paths;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Verifies that the first page spawn is delayed rather than happening synchronously with
+ * {@link GameStartEvent}, so survivors get a moment to move away from the spawn point first, and
+ * that the delay is jittered within {@code GameConfig.PAGE_SPAWN_DELAY} ±
+ * {@code GameConfig.PAGE_SPAWN_DELAY_JITTER} rather than landing on the exact same tick every round.
+ */
+class GameStartListenerTest extends CygnusPlayerTestBase {
+
+ private static final int TICKS_PER_SECOND = 20;
+
+ @Test
+ void pageSpawnEventNeverFiresBeforeTheMinimumJitteredDelay(@NotNull Env env) {
+ Instance instance = env.createFlatInstance();
+ GameStartListener listener = createListener(env, instance);
+
+ AtomicBoolean pageSpawnFired = new AtomicBoolean(false);
+ env.process().eventHandler().addListener(PageSpawnEvent.class, event -> pageSpawnFired.set(true));
+
+ listener.accept(new GameStartEvent());
+ // No random source is injectable here, but the jitter can never push the delay below
+ // PAGE_SPAWN_DELAY - PAGE_SPAWN_DELAY_JITTER regardless of what gets rolled.
+ int minDelayTicks = (GameConfig.PAGE_SPAWN_DELAY - GameConfig.PAGE_SPAWN_DELAY_JITTER) * TICKS_PER_SECOND;
+ for (int i = 0; i < minDelayTicks - 1; i++) env.tick();
+ assertFalse(pageSpawnFired.get(), "the page spawn must not fire before the minimum jittered delay");
+
+ // Drain the scheduled task here (whatever it actually rolled), unasserted, so it can't leak
+ // into a later test sharing this Env.
+ int maxDelayTicks = (GameConfig.PAGE_SPAWN_DELAY + GameConfig.PAGE_SPAWN_DELAY_JITTER) * TICKS_PER_SECOND;
+ for (int i = minDelayTicks - 1; i < maxDelayTicks + 5; i++) env.tick();
+
+ env.destroyInstance(instance, true);
+ }
+
+ @Test
+ void pageSpawnEventAlwaysFiresByTheMaximumJitteredDelay(@NotNull Env env) {
+ Instance instance = env.createFlatInstance();
+ GameStartListener listener = createListener(env, instance);
+
+ AtomicBoolean pageSpawnFired = new AtomicBoolean(false);
+ env.process().eventHandler().addListener(PageSpawnEvent.class, event -> pageSpawnFired.set(true));
+
+ listener.accept(new GameStartEvent());
+ // Jitter can only push the delay later, never past PAGE_SPAWN_DELAY + PAGE_SPAWN_DELAY_JITTER.
+ int maxDelayTicks = (GameConfig.PAGE_SPAWN_DELAY + GameConfig.PAGE_SPAWN_DELAY_JITTER) * TICKS_PER_SECOND;
+ for (int i = 0; i < maxDelayTicks + 5; i++) env.tick();
+
+ assertTrue(pageSpawnFired.get(), "the page spawn must fire once the configured delay has passed");
+
+ env.destroyInstance(instance, true);
+ }
+
+ private static GameStartListener createListener(Env env, Instance instance) {
+ CygnusPlayer slender = (CygnusPlayer) env.createPlayer(instance);
+ CygnusPlayer survivor = (CygnusPlayer) env.createPlayer(instance);
+
+ GameConfig gameConfig = new GameConfigReader(Paths.get("")).getConfig();
+ TeamService teamService = TeamService.of();
+ TeamCreator teamCreator = new TeamCreator() {};
+ teamCreator.createTeams(gameConfig, teamService);
+
+ Team slenderTeam = teamService.getTeam(GameConfig.SLENDER_KEY).orElseThrow();
+ Team survivorTeam = teamService.getTeam(GameConfig.SURVIVOR_KEY).orElseThrow();
+ slenderTeam.addPlayer(slender);
+ survivorTeam.addPlayer(survivor);
+
+ AmbientProvider ambientProvider = new AmbientProvider(survivorTeam);
+ StaminaService staminaService = new StaminaService();
+ PageProvider pageProvider = new PageProvider();
+ PageProximityService pageProximityService = new PageProximityService(
+ gameConfig,
+ survivorTeam::getPlayers,
+ List::of
+ );
+
+ return new GameStartListener(teamService, ambientProvider, staminaService, pageProvider, pageProximityService);
+ }
+}