3 Commits
Author SHA1 Message Date
dmg 85356c5c8e feat(progression): lower default rank I threshold
Release / release (push) Successful in 2m26s
CI / build (push) Successful in 1m12s
2026-08-11 15:15:39 -04:00
dmg 55e4f04f16 feat(aura): protect blocks within player radius
Release / release (push) Successful in 2m32s
CI / build (push) Successful in 1m11s
2026-08-11 14:46:54 -04:00
dmg b4a8094874 feat(commands): add permission-aware tab completion
Release / release (push) Successful in 2m30s
CI / build (push) Successful in 1m18s
2026-08-10 22:22:08 -04:00
18 changed files with 368 additions and 49 deletions
+5 -3
View File
@@ -1,6 +1,6 @@
# Spigot Creeper Fear # Spigot Creeper Fear
A Spigot 26.2 plugin that lets players earn six Creeper Aura ranks. Once unlocked, an aura prevents creeper explosions that hit the player from breaking blocks, trading that protection for rank-dependent player damage. A Spigot 26.2 plugin that lets players earn six Creeper Aura ranks. Once unlocked, an aura prevents a creeper explosion within 25 blocks of the player from breaking blocks, whether or not the explosion hits the player. Players hit by the explosion receive rank-dependent damage.
## Requirements ## Requirements
@@ -9,7 +9,7 @@ A Spigot 26.2 plugin that lets players earn six Creeper Aura ranks. Once unlocke
## Progression ## Progression
Players earn one current-tier point when they kill a creeper directly, through a projectile, or through an owned tameable. A self-destructing creeper also awards one point to every player its explosion hits. Each tier defaults to 100 points and resets progress to zero when unlocked. Players earn one current-tier point when they kill a creeper directly, through a projectile, or through an owned tameable. A self-destructing creeper also awards one point to every player its explosion hits. Rank I unlocks at 25 points by default; subsequent ranks require 100 points each. Progress resets to zero when a rank is unlocked.
| Rank | Creeper damage | Block damage | | Rank | Creeper damage | Block damage |
| --- | ---: | --- | | --- | ---: | --- |
@@ -48,12 +48,14 @@ Restart the server after copying the JAR. The plugin creates its configuration a
The personal progress permission defaults to everyone. Administrative permissions default to server operators. Administrative changes are recorded in the server log. The personal progress permission defaults to everyone. Administrative permissions default to server operators. Administrative changes are recorded in the server log.
Commands provide permission-aware tab completion for subcommands, online player names, and valid ranks. Known offline players remain valid command targets but are not suggested.
## Configuration ## Configuration
```yaml ```yaml
progression: progression:
requirements: requirements:
I: 100 I: 25
II: 100 II: 100
III: 100 III: 100
IV: 100 IV: 100
+1 -1
View File
@@ -36,7 +36,7 @@ For explosions, player damage events establish whether an unlocked player would
## Commands and configuration ## Commands and configuration
`/creeperaura` exposes player progress and permission-protected offline administration. Rank requirements, multipliers, feedback duration, and messages are loaded from YAML. Valid command-based changes are written back to YAML and survive restart. `/creeperaura` exposes player progress and permission-protected offline administration. Permission-aware tab completion suggests available subcommands, online players, and valid ranks without querying SQLite on the server thread. Rank requirements, multipliers, feedback duration, and messages are loaded from YAML. Valid command-based changes are written back to YAML and survive restart.
## Verification ## Verification
+9
View File
@@ -6,6 +6,15 @@ description: Chronological record of material changes to the Spigot Creeper Fear
# Design Log # Design Log
## 2026-08-11
- Updated US-002 so an unlocked player's aura protects blocks from any creeper explosion within 25 blocks, without requiring the explosion to hit the player.
- Updated US-002 and US-006 so rank I requires 25 creeper kills by default while later ranks continue to require 100.
## 2026-08-10
- Completed US-008 with permission-aware command, online-player, and rank tab completion.
## 2026-08-08 ## 2026-08-08
- Established the OKF v0.1 design bundle. - Established the OKF v0.1 design bundle.
+1
View File
@@ -13,3 +13,4 @@ description: Catalog of user stories for the Spigot Creeper Fear plugin.
- [US-005: Administer player progression](us-005-administer-player-progression.md) - [US-005: Administer player progression](us-005-administer-player-progression.md)
- [US-006: Configure aura progression](us-006-configure-aura-progression.md) - [US-006: Configure aura progression](us-006-configure-aura-progression.md)
- [US-007: Build and release the plugin](us-007-build-and-release-plugin.md) - [US-007: Build and release the plugin](us-007-build-and-release-plugin.md)
- [US-008: Autocomplete commands](us-008-autocomplete-commands.md)
@@ -13,7 +13,7 @@ As a **player**, I want Creeper Aura to become stronger as I defeat creepers so
| Current state | Kills needed to unlock next rank | Damage to protected player | Creeper block damage | | Current state | Kills needed to unlock next rank | Damage to protected player | Creeper block damage |
| --- | ---: | ---: | --- | | --- | ---: | ---: | --- |
| Locked | 100 to unlock I | 1× | Normal | | Locked | 25 to unlock I | 1× | Normal |
| Creeper Aura I | 100 to unlock II | 3× | Prevented | | Creeper Aura I | 100 to unlock II | 3× | Prevented |
| Creeper Aura II | 100 to unlock III | 2× | Prevented | | Creeper Aura II | 100 to unlock III | 2× | Prevented |
| Creeper Aura III | 100 to unlock IV | 1.5× | Prevented | | Creeper Aura III | 100 to unlock IV | 1.5× | Prevented |
@@ -23,11 +23,13 @@ As a **player**, I want Creeper Aura to become stronger as I defeat creepers so
## Acceptance criteria ## Acceptance criteria
- [x] A player unlocks the next rank after earning the configured number of kills within their current tier. - [x] A player unlocks rank I after 25 kills by default; subsequent ranks require 100 kills each by default.
- [x] Unlocking a rank resets current-tier progress to zero. - [x] Unlocking a rank resets current-tier progress to zero.
- [x] A player below rank I receives normal creeper explosion behavior. - [x] A player below rank I receives normal creeper explosion behavior.
- [x] An aura activates when an unlocked player would have been hit by the creeper explosion, even when armor or another modifier would reduce the eventual damage to zero. - [x] An aura protects blocks when a player with rank I or higher is within 25 blocks of a creeper at explosion time, including at exactly 25 blocks.
- [x] Block protection does not require the aura player to be hit by or have line of sight to the explosion.
- [x] An activated aura prevents that creeper explosion from breaking or removing blocks for everyone affected by the explosion. - [x] An activated aura prevents that creeper explosion from breaking or removing blocks for everyone affected by the explosion.
- [x] Players beyond 25 blocks and players whose aura is locked do not activate block protection.
- [x] Other nearby players and entities continue to receive their normal creeper explosion effects unless they have their own aura damage modifier. - [x] Other nearby players and entities continue to receive their normal creeper explosion effects unless they have their own aura damage modifier.
- [x] The protected player's rank multiplier is applied to vanilla creeper explosion damage before armor, enchantments, resistance, and difficulty mitigation. - [x] The protected player's rank multiplier is applied to vanilla creeper explosion damage before armor, enchantments, resistance, and difficulty mitigation.
- [x] At rank VI, the protected player's creeper explosion damage event is cancelled so that damage and knockback are nullified. - [x] At rank VI, the protected player's creeper explosion damage event is cancelled so that damage and knockback are nullified.
@@ -11,7 +11,7 @@ As a **server administrator**, I want to configure progression and aura behavior
## Acceptance criteria ## Acceptance criteria
- [x] Configuration provides documented defaults for the six per-rank kill requirements and six unlocked-rank damage multipliers. - [x] Configuration defaults to 25 kills for rank I, 100 kills for each subsequent rank, and the documented six unlocked-rank damage multipliers.
- [x] Rank requirements are positive integers. - [x] Rank requirements are positive integers.
- [x] Damage multipliers are finite and non-negative. - [x] Damage multipliers are finite and non-negative.
- [x] Progress boss-bar duration and player-facing rank messages are configurable. - [x] Progress boss-bar duration and player-facing rank messages are configurable.
@@ -0,0 +1,31 @@
---
type: User Story
title: "US-008: Autocomplete commands"
description: Help players and administrators discover valid Creeper Aura command arguments with permission-aware tab completion.
status: done
---
# US-008: Autocomplete commands
As a **command sender**, I want Creeper Aura commands to offer relevant tab completions so that I can enter valid commands quickly and accurately.
## Acceptance criteria
- [x] `/creeperaura` suggests only subcommands allowed by the sender's permissions.
- [x] `progress`, `set`, `add`, and `rank` suggest matching online player names where appropriate.
- [x] `rank` suggests `locked`, `I`, `II`, `III`, `IV`, `V`, and `VI`.
- [x] `threshold` suggests configurable ranks `I` through `VI`.
- [x] Suggestions are filtered case-insensitively by the current input.
- [x] Unauthorized administrative subcommands and arguments are not suggested.
- [x] Completion works for both players and the server console.
- [x] Automated tests cover completion behavior.
- [x] Command metadata and project documentation describe autocomplete support.
Offline stored players remain valid command targets but are not suggested, avoiding synchronous database access during completion.
## Related
- [Check personal progress](us-004-check-personal-progress.md)
- [Administer player progression](us-005-administer-player-progression.md)
- [Configure aura progression](us-006-configure-aura-progression.md)
- [User-story catalog](index.md)
@@ -3,6 +3,7 @@ package games.dmg.creeperfear;
import games.dmg.creeperfear.aura.AuraRules; import games.dmg.creeperfear.aura.AuraRules;
import games.dmg.creeperfear.aura.CreeperAuraListener; import games.dmg.creeperfear.aura.CreeperAuraListener;
import games.dmg.creeperfear.command.CreeperAuraCommand; import games.dmg.creeperfear.command.CreeperAuraCommand;
import games.dmg.creeperfear.command.CreeperAuraTabCompleter;
import games.dmg.creeperfear.command.ProgressMessages; import games.dmg.creeperfear.command.ProgressMessages;
import games.dmg.creeperfear.command.ProgressMutations; import games.dmg.creeperfear.command.ProgressMutations;
import games.dmg.creeperfear.config.AuraConfigLoader; import games.dmg.creeperfear.config.AuraConfigLoader;
@@ -17,6 +18,7 @@ import games.dmg.creeperfear.progress.SqliteProgressRepository;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Objects; import java.util.Objects;
import java.util.logging.Level; import java.util.logging.Level;
import org.bukkit.command.PluginCommand;
import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.plugin.java.JavaPlugin;
public final class CreeperFearPlugin extends JavaPlugin { public final class CreeperFearPlugin extends JavaPlugin {
@@ -42,14 +44,16 @@ public final class CreeperFearPlugin extends JavaPlugin {
new PlayerSessionListener(progressService, getLogger()), this); new PlayerSessionListener(progressService, getLogger()), this);
getServer().getPluginManager().registerEvents( getServer().getPluginManager().registerEvents(
new CreeperAuraListener(progressService, auraRules), this); new CreeperAuraListener(progressService, auraRules), this);
Objects.requireNonNull(getCommand("creeperaura"), "creeperaura command") PluginCommand creeperAuraCommand = Objects.requireNonNull(
.setExecutor(new CreeperAuraCommand( getCommand("creeperaura"), "creeperaura command");
this, creeperAuraCommand.setExecutor(new CreeperAuraCommand(
progressService, this,
new ProgressMessages(auraRules), progressService,
new ProgressMutations(), new ProgressMessages(auraRules),
progressFeedback, new ProgressMutations(),
configurationManager)); progressFeedback,
configurationManager));
creeperAuraCommand.setTabCompleter(new CreeperAuraTabCompleter(getServer()));
getServer().getOnlinePlayers().forEach(player -> progressService.loadOnline(player.getUniqueId()) getServer().getOnlinePlayers().forEach(player -> progressService.loadOnline(player.getUniqueId())
.exceptionally(failure -> { .exceptionally(failure -> {
getLogger().log(Level.SEVERE, getLogger().log(Level.SEVERE,
@@ -17,7 +17,7 @@ public final class AuraRules {
public static AuraRules defaults() { public static AuraRules defaults() {
Map<AuraRank, Integer> requirements = new EnumMap<>(AuraRank.class); Map<AuraRank, Integer> requirements = new EnumMap<>(AuraRank.class);
for (AuraRank rank : AuraRank.values()) { for (AuraRank rank : AuraRank.values()) {
if (!rank.isMaximum()) requirements.put(rank, 100); if (!rank.isMaximum()) requirements.put(rank, rank == AuraRank.LOCKED ? 25 : 100);
} }
Map<AuraRank, Double> multipliers = new EnumMap<>(AuraRank.class); Map<AuraRank, Double> multipliers = new EnumMap<>(AuraRank.class);
multipliers.put(AuraRank.LOCKED, 1.0); multipliers.put(AuraRank.LOCKED, 1.0);
@@ -3,11 +3,10 @@ package games.dmg.creeperfear.aura;
import games.dmg.creeperfear.progress.AuraRank; import games.dmg.creeperfear.progress.AuraRank;
import games.dmg.creeperfear.progress.PlayerProgress; import games.dmg.creeperfear.progress.PlayerProgress;
import games.dmg.creeperfear.progress.ProgressService; import games.dmg.creeperfear.progress.ProgressService;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import java.util.function.Function; import java.util.function.Function;
import org.bukkit.Location;
import org.bukkit.entity.Creeper; import org.bukkit.entity.Creeper;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
@@ -18,16 +17,10 @@ import org.bukkit.event.entity.EntityDamageEvent.DamageCause;
import org.bukkit.event.entity.EntityExplodeEvent; import org.bukkit.event.entity.EntityExplodeEvent;
public final class CreeperAuraListener implements Listener { public final class CreeperAuraListener implements Listener {
private static final int RECENT_EXPLOSION_LIMIT = 1024; private static final double BLOCK_PROTECTION_RADIUS_SQUARED = 25.0 * 25.0;
private final Function<UUID, Optional<PlayerProgress>> progressLookup; private final Function<UUID, Optional<PlayerProgress>> progressLookup;
private final AuraRules rules; private final AuraRules rules;
private final Map<UUID, Boolean> protectedExplosions = new LinkedHashMap<>(64, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<UUID, Boolean> eldest) {
return size() > RECENT_EXPLOSION_LIMIT;
}
};
public CreeperAuraListener(ProgressService progressService, AuraRules rules) { public CreeperAuraListener(ProgressService progressService, AuraRules rules) {
this(progressService::cached, rules); this(progressService::cached, rules);
@@ -52,7 +45,6 @@ public final class CreeperAuraListener implements Listener {
return; return;
} }
markProtected(creeper.getUniqueId());
AuraRank rank = progress.rank(); AuraRank rank = progress.rank();
if (rank.isMaximum()) { if (rank.isMaximum()) {
event.setCancelled(true); event.setCancelled(true);
@@ -63,17 +55,23 @@ public final class CreeperAuraListener implements Listener {
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onCreeperExplode(EntityExplodeEvent event) { public void onCreeperExplode(EntityExplodeEvent event) {
if (event.getEntity() instanceof Creeper creeper && takeProtected(creeper.getUniqueId())) { if (event.getEntity() instanceof Creeper creeper && hasNearbyAuraPlayer(creeper)) {
event.blockList().clear(); event.blockList().clear();
event.setYield(0.0f); event.setYield(0.0f);
} }
} }
private synchronized void markProtected(UUID creeperId) { private boolean hasNearbyAuraPlayer(Creeper creeper) {
protectedExplosions.put(creeperId, Boolean.TRUE); Location creeperLocation = creeper.getLocation();
} for (Player player : creeper.getWorld().getPlayers()) {
if (player.getLocation().distanceSquared(creeperLocation) <= BLOCK_PROTECTION_RADIUS_SQUARED
private synchronized boolean takeProtected(UUID creeperId) { && progressLookup.apply(player.getUniqueId())
return protectedExplosions.remove(creeperId) != null; .map(PlayerProgress::rank)
.filter(AuraRank::isUnlocked)
.isPresent()) {
return true;
}
}
return false;
} }
} }
@@ -0,0 +1,79 @@
package games.dmg.creeperfear.command;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import org.bukkit.Server;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.jetbrains.annotations.NotNull;
public final class CreeperAuraTabCompleter implements TabCompleter {
private static final List<String> ALL_RANKS = List.of("locked", "I", "II", "III", "IV", "V", "VI");
private static final List<String> CONFIGURABLE_RANKS = ALL_RANKS.subList(1, ALL_RANKS.size());
private final Server server;
public CreeperAuraTabCompleter(Server server) {
this.server = server;
}
@Override
public List<String> onTabComplete(
@NotNull CommandSender sender,
@NotNull Command command,
@NotNull String label,
@NotNull String[] args) {
if (args.length == 3
&& args[0].equalsIgnoreCase("rank")
&& sender.hasPermission("creeperfear.admin.modify")) {
return matching(ALL_RANKS, args[2]);
}
if (args.length == 2
&& args[0].equalsIgnoreCase("threshold")
&& sender.hasPermission("creeperfear.admin.configure")) {
return matching(CONFIGURABLE_RANKS, args[1]);
}
if (args.length == 2 && completesPlayer(sender, args[0])) {
List<String> playerNames = server.getOnlinePlayers().stream()
.map(player -> player.getName())
.toList();
return matching(playerNames, args[1]);
}
if (args.length != 1) return List.of();
List<String> suggestions = new ArrayList<>();
if (sender.hasPermission("creeperfear.progress")
|| sender.hasPermission("creeperfear.admin.inspect")) {
suggestions.add("progress");
}
if (sender.hasPermission("creeperfear.admin.modify")) {
suggestions.add("set");
suggestions.add("add");
suggestions.add("rank");
}
if (sender.hasPermission("creeperfear.admin.configure")) {
suggestions.add("threshold");
suggestions.add("reload");
}
return matching(suggestions, args[0]);
}
private boolean completesPlayer(CommandSender sender, String subcommand) {
if (subcommand.equalsIgnoreCase("progress")) {
return sender.hasPermission("creeperfear.admin.inspect");
}
return (subcommand.equalsIgnoreCase("set")
|| subcommand.equalsIgnoreCase("add")
|| subcommand.equalsIgnoreCase("rank"))
&& sender.hasPermission("creeperfear.admin.modify");
}
private List<String> matching(List<String> candidates, String input) {
String prefix = input.toLowerCase(Locale.ROOT);
return candidates.stream()
.filter(candidate -> candidate.toLowerCase(Locale.ROOT).startsWith(prefix))
.toList();
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
progression: progression:
requirements: requirements:
I: 100 I: 25
II: 100 II: 100
III: 100 III: 100
IV: 100 IV: 100
+1 -1
View File
@@ -6,7 +6,7 @@ author: dmg.games
description: Unlock Creeper Aura ranks by defeating creepers. description: Unlock Creeper Aura ranks by defeating creepers.
commands: commands:
creeperaura: creeperaura:
description: Check and administer Creeper Aura progression. description: Check and administer Creeper Aura progression with permission-aware tab completion.
usage: /<command> <progress [player]|set <player> <progress>|add <player> <progress>|rank <player> <rank>|threshold <rank> <points>|reload> usage: /<command> <progress [player]|set <player> <progress>|add <player> <progress>|rank <player> <rank>|threshold <rank> <points>|reload>
permissions: permissions:
creeperfear.progress: creeperfear.progress:
@@ -11,8 +11,8 @@ class AuraRulesTest {
private final AuraRules rules = AuraRules.defaults(); private final AuraRules rules = AuraRules.defaults();
@Test @Test
void unlocksOneRankAndResetsTierProgressAtOneHundredKills() { void unlocksFirstRankAndResetsTierProgressAtTwentyFiveKills() {
PlayerProgress progress = new PlayerProgress(UUID.randomUUID(), "Player", AuraRank.LOCKED, 100); PlayerProgress progress = new PlayerProgress(UUID.randomUUID(), "Player", AuraRank.LOCKED, 25);
PlayerProgress advanced = rules.advanceIfEarned(progress); PlayerProgress advanced = rules.advanceIfEarned(progress);
@@ -20,6 +20,12 @@ class AuraRulesTest {
assertEquals(0, advanced.tierKills()); assertEquals(0, advanced.tierKills());
} }
@Test
void usesOneHundredKillsForLaterRanksByDefault() {
assertEquals(100, rules.requirementForCurrentRank(AuraRank.I));
assertEquals(100, rules.requirementForCurrentRank(AuraRank.V));
}
@Test @Test
void appliesTheDefaultDamageMultipliers() { void appliesTheDefaultDamageMultipliers() {
assertEquals(3.0, rules.damageMultiplier(AuraRank.I)); assertEquals(3.0, rules.damageMultiplier(AuraRank.I));
@@ -1,15 +1,20 @@
package games.dmg.creeperfear.aura; package games.dmg.creeperfear.aura;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import games.dmg.creeperfear.progress.AuraRank; import games.dmg.creeperfear.progress.AuraRank;
import games.dmg.creeperfear.progress.PlayerProgress; import games.dmg.creeperfear.progress.PlayerProgress;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.block.Block; import org.bukkit.block.Block;
import org.bukkit.entity.Creeper; import org.bukkit.entity.Creeper;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
@@ -20,31 +25,94 @@ import org.junit.jupiter.api.Test;
class CreeperAuraListenerTest { class CreeperAuraListenerTest {
@Test @Test
void multipliesPlayerDamageAndClearsBlocksForTheExplosion() { void multipliesDamageForAnAuraPlayerHitByACreeperExplosion() {
UUID playerId = UUID.randomUUID(); UUID playerId = UUID.randomUUID();
UUID creeperId = UUID.randomUUID();
Player player = mock(Player.class); Player player = mock(Player.class);
Creeper creeper = mock(Creeper.class); Creeper creeper = mock(Creeper.class);
EntityDamageByEntityEvent damage = mock(EntityDamageByEntityEvent.class); EntityDamageByEntityEvent damage = mock(EntityDamageByEntityEvent.class);
EntityExplodeEvent explosion = mock(EntityExplodeEvent.class);
ArrayList<Block> blocks = new ArrayList<>();
blocks.add(mock(Block.class));
when(player.getUniqueId()).thenReturn(playerId); when(player.getUniqueId()).thenReturn(playerId);
when(creeper.getUniqueId()).thenReturn(creeperId);
when(damage.getEntity()).thenReturn(player); when(damage.getEntity()).thenReturn(player);
when(damage.getDamager()).thenReturn(creeper); when(damage.getDamager()).thenReturn(creeper);
when(damage.getCause()).thenReturn(DamageCause.ENTITY_EXPLOSION); when(damage.getCause()).thenReturn(DamageCause.ENTITY_EXPLOSION);
when(damage.getDamage()).thenReturn(10.0); when(damage.getDamage()).thenReturn(10.0);
PlayerProgress progress = new PlayerProgress(playerId, "Player", AuraRank.I, 0);
CreeperAuraListener listener = new CreeperAuraListener(id -> Optional.of(progress), AuraRules.defaults());
listener.onCreeperDamage(damage);
verify(damage).setDamage(30.0);
}
@Test
void protectsBlocksWhenAuraPlayerIsExactly25BlocksAwayWithoutBeingHit() {
UUID playerId = UUID.randomUUID();
Player player = mock(Player.class);
Creeper creeper = mock(Creeper.class);
World world = mock(World.class);
EntityExplodeEvent explosion = mock(EntityExplodeEvent.class);
ArrayList<Block> blocks = blocksToBreak();
when(player.getUniqueId()).thenReturn(playerId);
when(player.getLocation()).thenReturn(new Location(world, 25.0, 64.0, 0.0));
when(creeper.getWorld()).thenReturn(world);
when(creeper.getLocation()).thenReturn(new Location(world, 0.0, 64.0, 0.0));
when(world.getPlayers()).thenReturn(List.of(player));
when(explosion.getEntity()).thenReturn(creeper); when(explosion.getEntity()).thenReturn(creeper);
when(explosion.blockList()).thenReturn(blocks); when(explosion.blockList()).thenReturn(blocks);
PlayerProgress progress = new PlayerProgress(playerId, "Player", AuraRank.I, 0); PlayerProgress progress = new PlayerProgress(playerId, "Player", AuraRank.I, 0);
CreeperAuraListener listener = new CreeperAuraListener(id -> Optional.of(progress), AuraRules.defaults()); CreeperAuraListener listener = new CreeperAuraListener(id -> Optional.of(progress), AuraRules.defaults());
listener.onCreeperDamage(damage);
listener.onCreeperExplode(explosion); listener.onCreeperExplode(explosion);
verify(damage).setDamage(30.0);
assertTrue(blocks.isEmpty()); assertTrue(blocks.isEmpty());
verify(explosion).setYield(0.0f);
}
@Test
void leavesBlocksWhenAuraPlayerIsBeyond25Blocks() {
UUID playerId = UUID.randomUUID();
Player player = mock(Player.class);
Creeper creeper = mock(Creeper.class);
World world = mock(World.class);
EntityExplodeEvent explosion = mock(EntityExplodeEvent.class);
ArrayList<Block> blocks = blocksToBreak();
when(player.getUniqueId()).thenReturn(playerId);
when(player.getLocation()).thenReturn(new Location(world, 25.01, 64.0, 0.0));
when(creeper.getWorld()).thenReturn(world);
when(creeper.getLocation()).thenReturn(new Location(world, 0.0, 64.0, 0.0));
when(world.getPlayers()).thenReturn(List.of(player));
when(explosion.getEntity()).thenReturn(creeper);
when(explosion.blockList()).thenReturn(blocks);
PlayerProgress progress = new PlayerProgress(playerId, "Player", AuraRank.I, 0);
CreeperAuraListener listener = new CreeperAuraListener(id -> Optional.of(progress), AuraRules.defaults());
listener.onCreeperExplode(explosion);
assertFalse(blocks.isEmpty());
verify(explosion, never()).setYield(0.0f);
}
@Test
void leavesBlocksWhenNearbyPlayerHasNotUnlockedAura() {
UUID playerId = UUID.randomUUID();
Player player = mock(Player.class);
Creeper creeper = mock(Creeper.class);
World world = mock(World.class);
EntityExplodeEvent explosion = mock(EntityExplodeEvent.class);
ArrayList<Block> blocks = blocksToBreak();
when(player.getUniqueId()).thenReturn(playerId);
when(player.getLocation()).thenReturn(new Location(world, 1.0, 64.0, 0.0));
when(creeper.getWorld()).thenReturn(world);
when(creeper.getLocation()).thenReturn(new Location(world, 0.0, 64.0, 0.0));
when(world.getPlayers()).thenReturn(List.of(player));
when(explosion.getEntity()).thenReturn(creeper);
when(explosion.blockList()).thenReturn(blocks);
PlayerProgress progress = new PlayerProgress(playerId, "Player", AuraRank.LOCKED, 0);
CreeperAuraListener listener = new CreeperAuraListener(id -> Optional.of(progress), AuraRules.defaults());
listener.onCreeperExplode(explosion);
assertFalse(blocks.isEmpty());
verify(explosion, never()).setYield(0.0f);
} }
@Test @Test
@@ -64,4 +132,10 @@ class CreeperAuraListenerTest {
verify(damage).setCancelled(true); verify(damage).setCancelled(true);
} }
private static ArrayList<Block> blocksToBreak() {
ArrayList<Block> blocks = new ArrayList<>();
blocks.add(mock(Block.class));
return blocks;
}
} }
@@ -0,0 +1,98 @@
package games.dmg.creeperfear.command;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.util.List;
import org.bukkit.Server;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.ConsoleCommandSender;
import org.bukkit.entity.Player;
import org.junit.jupiter.api.Test;
class CreeperAuraTabCompleterTest {
private final Server server = mock(Server.class);
private final Command command = mock(Command.class);
private final CommandSender sender = mock(CommandSender.class);
private final CreeperAuraTabCompleter completer = new CreeperAuraTabCompleter(server);
@Test
void suggestsOnlySubcommandsAllowedByTheSendersPermissions() {
when(sender.hasPermission("creeperfear.progress")).thenReturn(true);
when(sender.hasPermission("creeperfear.admin.modify")).thenReturn(true);
List<String> suggestions = completer.onTabComplete(sender, command, "creeperaura", new String[] {""});
assertEquals(List.of("progress", "set", "add", "rank"), suggestions);
}
@Test
void filtersSubcommandsCaseInsensitivelyByPartialInput() {
when(sender.hasPermission("creeperfear.admin.configure")).thenReturn(true);
List<String> suggestions = completer.onTabComplete(sender, command, "creeperaura", new String[] {"TH"});
assertEquals(List.of("threshold"), suggestions);
}
@Test
void suggestsMatchingOnlinePlayersForPermittedPlayerArguments() {
Player alice = mock(Player.class);
Player bob = mock(Player.class);
when(alice.getName()).thenReturn("Alice");
when(bob.getName()).thenReturn("Bob");
doReturn(List.of(alice, bob)).when(server).getOnlinePlayers();
when(sender.hasPermission("creeperfear.admin.modify")).thenReturn(true);
List<String> suggestions = completer.onTabComplete(
sender, command, "creeperaura", new String[] {"rank", "aL"});
assertEquals(List.of("Alice"), suggestions);
}
@Test
void suggestsAllRanksForTheRankCommand() {
when(sender.hasPermission("creeperfear.admin.modify")).thenReturn(true);
List<String> suggestions = completer.onTabComplete(
sender, command, "creeperaura", new String[] {"rank", "Alice", ""});
assertEquals(List.of("locked", "I", "II", "III", "IV", "V", "VI"), suggestions);
}
@Test
void suggestsOnlyConfigurableRanksForThresholds() {
when(sender.hasPermission("creeperfear.admin.configure")).thenReturn(true);
List<String> suggestions = completer.onTabComplete(
sender, command, "creeperaura", new String[] {"threshold", ""});
assertEquals(List.of("I", "II", "III", "IV", "V", "VI"), suggestions);
}
@Test
void doesNotSuggestAdministrativeArgumentsWithoutPermission() {
assertEquals(List.of(), completer.onTabComplete(
sender, command, "creeperaura", new String[] {"progress", ""}));
assertEquals(List.of(), completer.onTabComplete(
sender, command, "creeperaura", new String[] {"rank", "Alice", ""}));
assertEquals(List.of(), completer.onTabComplete(
sender, command, "creeperaura", new String[] {"threshold", ""}));
}
@Test
void completesCommandsForPlayersAndTheServerConsole() {
Player player = mock(Player.class);
ConsoleCommandSender console = mock(ConsoleCommandSender.class);
when(player.hasPermission("creeperfear.progress")).thenReturn(true);
when(console.hasPermission("creeperfear.progress")).thenReturn(true);
assertEquals(List.of("progress"), completer.onTabComplete(
player, command, "creeperaura", new String[] {""}));
assertEquals(List.of("progress"), completer.onTabComplete(
console, command, "creeperaura", new String[] {""}));
}
}
@@ -4,10 +4,25 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertThrows;
import games.dmg.creeperfear.progress.AuraRank; import games.dmg.creeperfear.progress.AuraRank;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.configuration.file.YamlConfiguration;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
class AuraConfigLoaderTest { class AuraConfigLoaderTest {
@Test
void bundledConfigurationDefaultsFirstRankToTwentyFiveKills() throws Exception {
try (InputStream stream = getClass().getResourceAsStream("/config.yml")) {
YamlConfiguration config = YamlConfiguration.loadConfiguration(
new InputStreamReader(stream, StandardCharsets.UTF_8));
AuraConfiguration configuration = new AuraConfigLoader().load(config);
assertEquals(25, configuration.rules().requirementForCurrentRank(AuraRank.LOCKED));
assertEquals(100, configuration.rules().requirementForCurrentRank(AuraRank.I));
}
}
@Test @Test
void loadsPerRankRequirementsMultipliersAndFeedback() throws Exception { void loadsPerRankRequirementsMultipliersAndFeedback() throws Exception {
AuraConfiguration configuration = new AuraConfigLoader().load(validConfiguration()); AuraConfiguration configuration = new AuraConfigLoader().load(validConfiguration());
@@ -15,12 +15,12 @@ class ProgressDisplayTest {
@Test @Test
void describesLockedCurrentTierProgress() { void describesLockedCurrentTierProgress() {
PlayerProgress progress = new PlayerProgress(UUID.randomUUID(), "Player", AuraRank.LOCKED, 42); PlayerProgress progress = new PlayerProgress(UUID.randomUUID(), "Player", AuraRank.LOCKED, 12);
ProgressDisplay.State state = display.forProgress(progress); ProgressDisplay.State state = display.forProgress(progress);
assertEquals("Creeper Aura: Locked — 42 / 100", state.text()); assertEquals("Creeper Aura: Locked — 12 / 25", state.text());
assertEquals(0.42, state.fraction(), 0.0001); assertEquals(0.48, state.fraction(), 0.0001);
assertTrue(state.visible()); assertTrue(state.visible());
} }