Compare commits

...
2 Commits
Author SHA1 Message Date
dmg 3d65576010 fix(assassin): preserve external potion effects
Release / release (push) Successful in 2m46s
CI / build (push) Successful in 1m8s
2026-08-18 23:50:15 -04:00
dmg 323d177573 feat(participation): highlight opted-out players
Release / release (push) Successful in 2m40s
CI / build (push) Successful in 1m8s
2026-08-15 09:13:34 -04:00
10 changed files with 555 additions and 16 deletions
+18
View File
@@ -6,6 +6,24 @@ description: Chronological record of material decisions affecting the Spigot Tyr
# Spigot Tyrant Design Log # Spigot Tyrant Design Log
## 2026-08-18 — Preserve ordinary potion effects
- Corrected Assassin effect maintenance so it refreshes active Tyrant abilities without removing invisibility, Speed, or Weakness supplied by potions, commands, or other plugins.
- Expired or inapplicable Tyrant-managed effects stop refreshing and expire naturally within their short 30-tick maintenance duration, while double-jump flight cleanup remains immediate.
- Reopened and completed US-005 with regression coverage; verified `./gradlew clean check jar`.
## 2026-08-14 — Opt-out proximity glow completed
- Extended US-011 with a globally visible green outline for opted-out players in the Tyrant's world and configured range while the event is running.
- A periodic controller removes the glow when eligibility, proximity, world, lifecycle, or player availability changes and clears managed state on logout and shutdown.
- Scoreboard presentation records and restores each player's previous team and glowing state instead of permanently replacing unrelated presentation.
- Verified inclusive range behavior, lifecycle and availability gating, green presentation, restoration, and the full Gradle build.
## 2026-08-14 — Opt-out proximity glow started
- US-011 is reopened for a test-first green outline on opted-out players near the Tyrant during a running event, using the existing configurable Tyrant range.
- Cleanup will cover range, world, participation, lifecycle, availability, shutdown, and restoration of pre-existing scoreboard-team membership.
## 2026-08-14 — Join messaging refinement completed ## 2026-08-14 — Join messaging refinement completed
- Updated US-011 so join output no longer announces running, paused, or unstarted lifecycle state. - Updated US-011 so join output no longer announces running, paused, or unstarted lifecycle state.
@@ -20,6 +20,13 @@ As the **Assassin**, I want stealth and burst mobility so that I can ambush oppo
- [x] Grounding and supported movement reset jump availability only after the 60-second cooldown has elapsed. - [x] Grounding and supported movement reset jump availability only after the 60-second cooldown has elapsed.
- [x] Ability cooldowns use unpaused elapsed time and survive death, logout, and restart. - [x] Ability cooldowns use unpaused elapsed time and survive death, logout, and restart.
- [x] The player can inspect remaining cooldowns and receives clear feedback when an activation is unavailable. - [x] The player can inspect remaining cooldowns and receives clear feedback when an activation is unavailable.
- [x] Tyrant removes only Assassin potion effects that Tyrant itself applied.
- [x] Invisibility from potions, commands, or other plugins remains active for Assassins and non-Assassins.
- [x] Tyrant-managed Assassin effects are still removed when abilities expire, roles change, games stop, or the plugin disables.
## Validation
Automated controller tests verify that inactive Tyrant state does not alter ordinary potion effects, active Assassin invisibility is refreshed, and refresh stops at expiry so the short managed effect expires naturally. The complete `./gradlew clean check jar` lifecycle passes.
## Related ## Related
@@ -26,6 +26,10 @@ As a **player**, I want to understand my status and control my eligibility so th
- [x] Cancelled attackers are told that the target has chosen not to take part in the Tyrant event. - [x] Cancelled attackers are told that the target has chosen not to take part in the Tyrant event.
- [x] Protection covers direct player attacks and player-fired projectiles without changing unrelated combat. - [x] Protection covers direct player attacks and player-fired projectiles without changing unrelated combat.
- [x] Participation combat protection does not apply while the event is paused or not started. - [x] Participation combat protection does not apply while the event is paused or not started.
- [x] While the event is running, an opted-out player in the same world and within the configured Tyrant range has a green glowing outline visible to everyone.
- [x] The opt-out glow is removed when proximity, participation, player availability, Tyrant availability, or running-event requirements stop being satisfied.
- [x] Existing scoreboard-team membership is restored after the opt-out glow ends, and plugin shutdown safely clears managed glow state.
- [x] Opt-out proximity glow does not apply while the event is paused or not started.
## Related ## Related
@@ -3,6 +3,7 @@ package games.dmg.spigottyrant;
import java.time.Clock; import java.time.Clock;
import java.time.Instant; import java.time.Instant;
import java.util.HashSet; import java.util.HashSet;
import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
import org.bukkit.GameMode; import org.bukkit.GameMode;
@@ -17,6 +18,7 @@ public final class AssassinEffectController implements Runnable {
private final Server server; private final Server server;
private final PluginSettings settings; private final PluginSettings settings;
private final Clock clock; private final Clock clock;
private final EffectApplier effectApplier;
private final Set<UUID> grantedFlight = new HashSet<>(); private final Set<UUID> grantedFlight = new HashSet<>();
public AssassinEffectController( public AssassinEffectController(
@@ -25,10 +27,21 @@ public final class AssassinEffectController implements Runnable {
PluginSettings settings, PluginSettings settings,
Clock clock Clock clock
) { ) {
this.stateManager = stateManager; this(stateManager, server, settings, clock, new BukkitEffectApplier());
this.server = server; }
this.settings = settings;
this.clock = clock; AssassinEffectController(
TyrantStateManager stateManager,
Server server,
PluginSettings settings,
Clock clock,
EffectApplier effectApplier
) {
this.stateManager = Objects.requireNonNull(stateManager, "stateManager");
this.server = Objects.requireNonNull(server, "server");
this.settings = Objects.requireNonNull(settings, "settings");
this.clock = Objects.requireNonNull(clock, "clock");
this.effectApplier = Objects.requireNonNull(effectApplier, "effectApplier");
} }
@Override @Override
@@ -43,15 +56,15 @@ public final class AssassinEffectController implements Runnable {
} }
applyWhileActive( applyWhileActive(
player, state, Ability.ASSASSIN_INVISIBILITY_ACTIVE, player, state, Ability.ASSASSIN_INVISIBILITY_ACTIVE,
PotionEffectType.INVISIBILITY, 0, false EffectKind.INVISIBILITY, 0, false
); );
applyWhileActive( applyWhileActive(
player, state, Ability.ASSASSIN_SPEED_ACTIVE, player, state, Ability.ASSASSIN_SPEED_ACTIVE,
PotionEffectType.SPEED, 0, true EffectKind.SPEED, 0, true
); );
applyWhileActive( applyWhileActive(
player, state, Ability.ASSASSIN_WEAKNESS_ACTIVE, player, state, Ability.ASSASSIN_WEAKNESS_ACTIVE,
PotionEffectType.WEAKNESS, settings.assassinWeaknessLevel() - 1, true EffectKind.WEAKNESS, settings.assassinWeaknessLevel() - 1, true
); );
boolean jumpReady = !state.cooldownEnds().getOrDefault( boolean jumpReady = !state.cooldownEnds().getOrDefault(
Ability.ASSASSIN_DOUBLE_JUMP, Instant.MIN Ability.ASSASSIN_DOUBLE_JUMP, Instant.MIN
@@ -71,29 +84,52 @@ public final class AssassinEffectController implements Runnable {
Player player, Player player,
PlayerState state, PlayerState state,
Ability ability, Ability ability,
PotionEffectType type, EffectKind effect,
int amplifier, int amplifier,
boolean particles boolean particles
) { ) {
if (state.cooldownEnds().getOrDefault(ability, Instant.MIN).isAfter(clock.instant())) { if (state.cooldownEnds().getOrDefault(ability, Instant.MIN).isAfter(clock.instant())) {
player.addPotionEffect(new PotionEffect( effectApplier.apply(player, effect, amplifier, particles);
type, EFFECT_TICKS, amplifier, false, particles, true
));
} else {
player.removePotionEffect(type);
} }
} }
private void clear(Player player) { private void clear(Player player) {
player.removePotionEffect(PotionEffectType.INVISIBILITY);
player.removePotionEffect(PotionEffectType.SPEED);
player.removePotionEffect(PotionEffectType.WEAKNESS);
if (grantedFlight.remove(player.getUniqueId()) && isSurvivalLike(player)) { if (grantedFlight.remove(player.getUniqueId()) && isSurvivalLike(player)) {
player.setAllowFlight(false); player.setAllowFlight(false);
player.setFlying(false); player.setFlying(false);
} }
} }
enum EffectKind {
INVISIBILITY,
SPEED,
WEAKNESS
}
@FunctionalInterface
interface EffectApplier {
void apply(Player player, EffectKind effect, int amplifier, boolean particles);
}
private static final class BukkitEffectApplier implements EffectApplier {
@Override
public void apply(
Player player,
EffectKind effect,
int amplifier,
boolean particles
) {
PotionEffectType type = switch (effect) {
case INVISIBILITY -> PotionEffectType.INVISIBILITY;
case SPEED -> PotionEffectType.SPEED;
case WEAKNESS -> PotionEffectType.WEAKNESS;
};
player.addPotionEffect(new PotionEffect(
type, EFFECT_TICKS, amplifier, false, particles, true
));
}
}
private static boolean isGrounded(Player player) { private static boolean isGrounded(Player player) {
return !player.getLocation().clone().subtract(0.0, 0.1, 0.0) return !player.getLocation().clone().subtract(0.0, 0.1, 0.0)
.getBlock().isPassable(); .getBlock().isPassable();
@@ -0,0 +1,127 @@
package games.dmg.spigottyrant;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import org.bukkit.ChatColor;
import org.bukkit.Server;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.scoreboard.Scoreboard;
import org.bukkit.scoreboard.ScoreboardManager;
import org.bukkit.scoreboard.Team;
public final class OptOutGlowController implements Runnable, Listener {
private static final String TEAM_NAME = "tyrant-optout";
private final TyrantStateManager stateManager;
private final Server server;
private final PluginSettings settings;
private final Scoreboard scoreboard;
private final Team greenTeam;
private final Map<UUID, PreviousPresentation> managed = new HashMap<>();
public OptOutGlowController(
TyrantStateManager stateManager,
Server server,
PluginSettings settings
) {
this.stateManager = stateManager;
this.server = server;
this.settings = settings;
ScoreboardManager manager = Objects.requireNonNull(
server.getScoreboardManager(), "Scoreboard manager is unavailable"
);
scoreboard = manager.getMainScoreboard();
Team existing = scoreboard.getTeam(TEAM_NAME);
greenTeam = existing == null ? scoreboard.registerNewTeam(TEAM_NAME) : existing;
greenTeam.setColor(ChatColor.GREEN);
}
@Override
public void run() {
GameState game = stateManager.game();
Player tyrant = game.tyrantId().map(server::getPlayer).orElse(null);
boolean tyrantAvailable = tyrant != null && !tyrant.isDead();
Set<UUID> online = new HashSet<>();
for (Player player : server.getOnlinePlayers()) {
online.add(player.getUniqueId());
PlayerState state = stateManager.players().get(player.getUniqueId());
boolean sameWorld = tyrantAvailable && !player.isDead()
&& player.getWorld().equals(tyrant.getWorld());
double distanceSquared = sameWorld
? player.getLocation().distanceSquared(tyrant.getLocation())
: Double.POSITIVE_INFINITY;
boolean shouldGlow = state != null && OptOutGlowPolicy.shouldGlow(
game, state, tyrantAvailable, sameWorld, distanceSquared,
settings.tyrantRangeBlocks()
);
if (shouldGlow && !managed.containsKey(player.getUniqueId())) {
apply(player);
} else if (!shouldGlow && managed.containsKey(player.getUniqueId())) {
restore(player.getUniqueId(), player);
}
}
for (UUID playerId : Set.copyOf(managed.keySet())) {
if (!online.contains(playerId) && server.getPlayer(playerId) == null) {
restore(playerId, null);
}
}
}
@EventHandler(priority = EventPriority.MONITOR)
public void onQuit(PlayerQuitEvent event) {
if (managed.containsKey(event.getPlayer().getUniqueId())) {
restore(event.getPlayer().getUniqueId(), event.getPlayer());
}
}
public void clearAll() {
for (UUID playerId : Set.copyOf(managed.keySet())) {
restore(playerId, server.getPlayer(playerId));
}
}
private void apply(Player player) {
Team previousTeam = scoreboard.getEntryTeam(player.getName());
if (previousTeam == greenTeam) {
previousTeam = null;
}
managed.put(
player.getUniqueId(),
new PreviousPresentation(player.getName(), previousTeam, player.isGlowing())
);
greenTeam.addEntry(player.getName());
player.setGlowing(true);
}
private void restore(UUID playerId, Player player) {
PreviousPresentation previous = managed.remove(playerId);
if (previous == null) {
return;
}
greenTeam.removeEntry(previous.playerName());
if (previous.team() != null) {
try {
previous.team().addEntry(previous.playerName());
} catch (IllegalStateException ignored) {
// The prior team was removed while this plugin managed the entry.
}
}
if (player != null) {
player.setGlowing(previous.glowing());
}
}
private record PreviousPresentation(
String playerName,
Team team,
boolean glowing
) {
}
}
@@ -0,0 +1,22 @@
package games.dmg.spigottyrant;
public final class OptOutGlowPolicy {
private OptOutGlowPolicy() {
}
public static boolean shouldGlow(
GameState game,
PlayerState player,
boolean tyrantAvailable,
boolean playerAvailableInTyrantWorld,
double distanceSquared,
double rangeBlocks
) {
return game.lifecycle() == GameLifecycle.RUNNING
&& game.tyrantId().isPresent()
&& player.optedOutUntil().isPresent()
&& tyrantAvailable
&& playerAvailableInTyrantWorld
&& distanceSquared <= rangeBlocks * rangeBlocks;
}
}
@@ -15,6 +15,7 @@ public final class SpigotTyrantPlugin extends JavaPlugin {
private AssassinEffectController assassinEffects; private AssassinEffectController assassinEffects;
private FixerEffectController fixerEffects; private FixerEffectController fixerEffects;
private TyrantEffectController tyrantEffects; private TyrantEffectController tyrantEffects;
private OptOutGlowController optOutGlow;
@Override @Override
public void onEnable() { public void onEnable() {
@@ -69,6 +70,7 @@ public final class SpigotTyrantPlugin extends JavaPlugin {
FixerAbilityService fixerAbilities = new FixerAbilityService(readiness, settings); FixerAbilityService fixerAbilities = new FixerAbilityService(readiness, settings);
fixerEffects = new FixerEffectController(stateManager, getServer(), settings, clock); fixerEffects = new FixerEffectController(stateManager, getServer(), settings, clock);
tyrantEffects = new TyrantEffectController(stateManager, getServer(), settings); tyrantEffects = new TyrantEffectController(stateManager, getServer(), settings);
optOutGlow = new OptOutGlowController(stateManager, getServer(), settings);
VigilanteCombatTracker combatTracker = new VigilanteCombatTracker(); VigilanteCombatTracker combatTracker = new VigilanteCombatTracker();
vigilanteEffects = new VigilanteEffectController( vigilanteEffects = new VigilanteEffectController(
stateManager, getServer(), combatTracker, settings, clock stateManager, getServer(), combatTracker, settings, clock
@@ -128,6 +130,7 @@ public final class SpigotTyrantPlugin extends JavaPlugin {
), ),
this this
); );
getServer().getPluginManager().registerEvents(optOutGlow, this);
getServer().getPluginManager().registerEvents( getServer().getPluginManager().registerEvents(
new VigilanteCombatListener( new VigilanteCombatListener(
stateManager, combatTracker, clock, settings.vigilanteCombatDuration() stateManager, combatTracker, clock, settings.vigilanteCombatDuration()
@@ -176,6 +179,7 @@ public final class SpigotTyrantPlugin extends JavaPlugin {
getServer().getScheduler().runTaskTimer(this, assassinEffects, 10L, 10L); getServer().getScheduler().runTaskTimer(this, assassinEffects, 10L, 10L);
getServer().getScheduler().runTaskTimer(this, fixerEffects, 10L, 10L); getServer().getScheduler().runTaskTimer(this, fixerEffects, 10L, 10L);
getServer().getScheduler().runTaskTimer(this, tyrantEffects, 10L, 10L); getServer().getScheduler().runTaskTimer(this, tyrantEffects, 10L, 10L);
getServer().getScheduler().runTaskTimer(this, optOutGlow, 10L, 10L);
getServer().getScheduler().runTaskTimer( getServer().getScheduler().runTaskTimer(
this, this,
new AbilityItemRefreshTask( new AbilityItemRefreshTask(
@@ -191,6 +195,9 @@ public final class SpigotTyrantPlugin extends JavaPlugin {
@Override @Override
public void onDisable() { public void onDisable() {
if (optOutGlow != null) {
optOutGlow.clearAll();
}
if (tyrantEffects != null) { if (tyrantEffects != null) {
tyrantEffects.clearAll(); tyrantEffects.clearAll();
} }
@@ -0,0 +1,145 @@
package games.dmg.spigottyrant;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
import java.nio.file.Path;
import java.time.Clock;
import java.time.Instant;
import java.time.ZoneOffset;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.logging.Logger;
import org.bukkit.Server;
import org.bukkit.entity.Player;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
final class AssassinEffectControllerTest {
private static final Instant NOW = Instant.parse("2026-08-18T12:00:00Z");
private static final UUID PLAYER_ID = UUID.fromString(
"11111111-1111-1111-1111-111111111111"
);
@TempDir
Path temporaryDirectory;
@Test
void doesNotRemovePotionEffectsItDidNotApply() throws Exception {
TyrantStateManager stateManager = manager();
Player player = player();
Server server = server(player);
AssassinEffectController.EffectApplier effects = mock(
AssassinEffectController.EffectApplier.class
);
AssassinEffectController controller = controller(stateManager, server, effects);
controller.run();
controller.clearAll();
verifyNoInteractions(effects);
}
@Test
void stopsRefreshingManagedInvisibilityAfterItsDeadline() throws Exception {
TyrantStateManager stateManager = manager();
stateManager.updateGame(ignored -> runningGame());
stateManager.updatePlayer(PLAYER_ID, "Assassin", ignored -> assassin(Map.of(
Ability.ASSASSIN_INVISIBILITY_ACTIVE, NOW.plusSeconds(60),
Ability.ASSASSIN_DOUBLE_JUMP, NOW.plusSeconds(60)
)));
Player player = player();
Server server = server(player);
AssassinEffectController.EffectApplier effects = mock(
AssassinEffectController.EffectApplier.class
);
AssassinEffectController controller = controller(stateManager, server, effects);
controller.run();
verify(effects).apply(
player,
AssassinEffectController.EffectKind.INVISIBILITY,
0,
false
);
stateManager.updatePlayer(PLAYER_ID, "Assassin", ignored -> assassin(Map.of(
Ability.ASSASSIN_DOUBLE_JUMP, NOW.plusSeconds(60)
)));
clearInvocations(player, effects);
controller.run();
verifyNoInteractions(effects);
}
private TyrantStateManager manager() throws Exception {
return new TyrantStateManager(
new YamlTyrantStateRepository(temporaryDirectory.resolve("state.yml")),
Logger.getLogger("test")
);
}
private static AssassinEffectController controller(
TyrantStateManager stateManager,
Server server,
AssassinEffectController.EffectApplier effects
) {
return new AssassinEffectController(
stateManager,
server,
PluginSettings.from(Map.of()),
Clock.fixed(NOW, ZoneOffset.UTC),
effects
);
}
private static Server server(Player player) {
Server server = mock(Server.class);
doReturn(List.of(player)).when(server).getOnlinePlayers();
return server;
}
private static Player player() {
Player player = mock(Player.class);
when(player.getUniqueId()).thenReturn(PLAYER_ID);
when(player.getName()).thenReturn("Assassin");
return player;
}
private static PlayerState assassin(Map<Ability, Instant> cooldowns) {
return new PlayerState(
PLAYER_ID,
"Assassin",
Optional.empty(),
Optional.empty(),
TyrantClass.ASSASSIN,
Optional.empty(),
cooldowns,
Set.of(),
List.of()
);
}
private static GameState runningGame() {
return new GameState(
GameLifecycle.RUNNING,
Optional.empty(),
Optional.empty(),
Optional.empty(),
Optional.empty(),
Optional.empty(),
java.time.Duration.ZERO,
0,
0,
Set.of()
);
}
}
@@ -0,0 +1,97 @@
package games.dmg.spigottyrant;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.time.Duration;
import java.time.Instant;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import org.bukkit.ChatColor;
import org.bukkit.Location;
import org.bukkit.Server;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.bukkit.scoreboard.Scoreboard;
import org.bukkit.scoreboard.ScoreboardManager;
import org.bukkit.scoreboard.Team;
import org.junit.jupiter.api.Test;
final class OptOutGlowControllerTest {
private static final UUID TYRANT = UUID.fromString(
"11111111-1111-1111-1111-111111111111"
);
private static final UUID OUT = UUID.fromString(
"22222222-2222-2222-2222-222222222222"
);
@Test
void appliesGreenGlowAndRestoresPreviousTeamAfterLeavingRange() {
TyrantStateManager stateManager = mock(TyrantStateManager.class);
Server server = mock(Server.class);
PluginSettings settings = mock(PluginSettings.class);
ScoreboardManager manager = mock(ScoreboardManager.class);
Scoreboard scoreboard = mock(Scoreboard.class);
Team green = mock(Team.class);
Team previous = mock(Team.class);
Player tyrant = player(TYRANT, "Tyrant");
Player optedOut = player(OUT, "Out");
World world = mock(World.class);
Location tyrantLocation = mock(Location.class);
Location outLocation = mock(Location.class);
when(server.getScoreboardManager()).thenReturn(manager);
when(manager.getMainScoreboard()).thenReturn(scoreboard);
when(scoreboard.getTeam("tyrant-optout")).thenReturn(null);
when(scoreboard.registerNewTeam("tyrant-optout")).thenReturn(green);
when(scoreboard.getEntryTeam("Out")).thenReturn(previous);
when(server.getPlayer(TYRANT)).thenReturn(tyrant);
doReturn(Set.of(tyrant, optedOut)).when(server).getOnlinePlayers();
when(settings.tyrantRangeBlocks()).thenReturn(50.0);
when(tyrant.getWorld()).thenReturn(world);
when(optedOut.getWorld()).thenReturn(world);
when(tyrant.getLocation()).thenReturn(tyrantLocation);
when(optedOut.getLocation()).thenReturn(outLocation);
when(outLocation.distanceSquared(tyrantLocation)).thenReturn(25.0, 2601.0);
when(stateManager.game()).thenReturn(running());
when(stateManager.players()).thenReturn(Map.of(OUT, optedOut()));
OptOutGlowController controller = new OptOutGlowController(
stateManager, server, settings
);
controller.run();
controller.run();
verify(green).setColor(ChatColor.GREEN);
verify(green).addEntry("Out");
verify(optedOut).setGlowing(true);
verify(green).removeEntry("Out");
verify(previous).addEntry("Out");
verify(optedOut).setGlowing(false);
}
private static Player player(UUID id, String name) {
Player player = mock(Player.class);
when(player.getUniqueId()).thenReturn(id);
when(player.getName()).thenReturn(name);
return player;
}
private static PlayerState optedOut() {
return new PlayerState(
OUT, "Out", Optional.empty(), Optional.of(Instant.MAX), TyrantClass.NONE,
Optional.empty(), Map.of(), Set.of(), java.util.List.of()
);
}
private static GameState running() {
return new GameState(
GameLifecycle.RUNNING, Optional.of(TYRANT), Optional.empty(),
Optional.empty(), Optional.empty(), Optional.empty(), Duration.ZERO,
0, 0, Set.of()
);
}
}
@@ -0,0 +1,76 @@
package games.dmg.spigottyrant;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.time.Duration;
import java.time.Instant;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import org.junit.jupiter.api.Test;
final class OptOutGlowPolicyTest {
private static final UUID TYRANT = UUID.fromString(
"11111111-1111-1111-1111-111111111111"
);
private static final UUID PLAYER = UUID.fromString(
"22222222-2222-2222-2222-222222222222"
);
@Test
void optedOutPlayerGlowsWithinInclusiveRunningRange() {
assertTrue(OptOutGlowPolicy.shouldGlow(
running(), optedOut(), true, true, 2500.0, 50.0
));
}
@Test
void requiresRunningEventAvailablePlayersSameWorldAndRange() {
assertFalse(OptOutGlowPolicy.shouldGlow(
GameState.empty(), optedOut(), true, true, 1.0, 50.0
));
assertFalse(OptOutGlowPolicy.shouldGlow(
paused(), optedOut(), true, true, 1.0, 50.0
));
assertFalse(OptOutGlowPolicy.shouldGlow(
running(), optedIn(), true, true, 1.0, 50.0
));
assertFalse(OptOutGlowPolicy.shouldGlow(
running(), optedOut(), false, true, 1.0, 50.0
));
assertFalse(OptOutGlowPolicy.shouldGlow(
running(), optedOut(), true, false, 1.0, 50.0
));
assertFalse(OptOutGlowPolicy.shouldGlow(
running(), optedOut(), true, true, 2500.01, 50.0
));
}
private static PlayerState optedOut() {
return new PlayerState(
PLAYER, "Player", Optional.empty(), Optional.of(Instant.MAX),
TyrantClass.NONE, Optional.empty(), Map.of(), Set.of(), java.util.List.of()
);
}
private static PlayerState optedIn() {
return PlayerState.newPlayer(PLAYER, "Player");
}
private static GameState running() {
return game(GameLifecycle.RUNNING, Optional.empty());
}
private static GameState paused() {
return game(GameLifecycle.PAUSED, Optional.of(Instant.EPOCH));
}
private static GameState game(GameLifecycle lifecycle, Optional<Instant> pausedAt) {
return new GameState(
lifecycle, Optional.of(TYRANT), Optional.empty(), Optional.empty(),
Optional.empty(), pausedAt, Duration.ZERO, 0, 0, Set.of()
);
}
}