feat(aura): apply ranked creeper protection
This commit is contained in:
@@ -14,3 +14,4 @@ description: Chronological record of material changes to the Spigot Creeper Fear
|
|||||||
- Replaced lifetime cumulative kill tracking with a persisted rank and current-tier progress model.
|
- Replaced lifetime cumulative kill tracking with a persisted rank and current-tier progress model.
|
||||||
- Added the initial plugin architecture.
|
- Added the initial plugin architecture.
|
||||||
- Completed US-001 with asynchronous SQLite current-tier progress, direct and indirect kill attribution, bounded death deduplication, and automated tests.
|
- Completed US-001 with asynchronous SQLite current-tier progress, direct and indirect kill attribution, bounded death deduplication, and automated tests.
|
||||||
|
- Completed US-002 with per-tier rank advancement, online aura state, creeper block protection, rank damage multipliers, and rank VI cancellation.
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
type: User Story
|
type: User Story
|
||||||
title: "US-002: Unlock Creeper Aura ranks"
|
title: "US-002: Unlock Creeper Aura ranks"
|
||||||
description: Protect blocks and modify creeper damage according to a player's earned aura rank.
|
description: Protect blocks and modify creeper damage according to a player's earned aura rank.
|
||||||
status: backlog
|
status: done
|
||||||
---
|
---
|
||||||
|
|
||||||
# US-002: Unlock Creeper Aura ranks
|
# US-002: Unlock Creeper Aura ranks
|
||||||
@@ -23,17 +23,17 @@ As a **player**, I want Creeper Aura to become stronger as I defeat creepers so
|
|||||||
|
|
||||||
## Acceptance criteria
|
## Acceptance criteria
|
||||||
|
|
||||||
- [ ] A player unlocks the next rank after earning the configured number of kills within their current tier.
|
- [x] A player unlocks the next rank after earning the configured number of kills within their current tier.
|
||||||
- [ ] Unlocking a rank resets current-tier progress to zero.
|
- [x] Unlocking a rank resets current-tier progress to zero.
|
||||||
- [ ] A player below rank I receives normal creeper explosion behavior.
|
- [x] A player below rank I receives normal creeper explosion behavior.
|
||||||
- [ ] 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 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.
|
||||||
- [ ] 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.
|
||||||
- [ ] 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.
|
||||||
- [ ] 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.
|
||||||
- [ ] 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.
|
||||||
- [ ] Charged creepers obey the same aura rules while retaining their vanilla base explosion strength.
|
- [x] Charged creepers obey the same aura rules while retaining their vanilla base explosion strength.
|
||||||
- [ ] Explosions from TNT, beds, respawn anchors, and non-creeper entities are unchanged.
|
- [x] Explosions from TNT, beds, respawn anchors, and non-creeper entities are unchanged.
|
||||||
- [ ] Simultaneous exposure of players with different aura ranks is deterministic and tested.
|
- [x] Simultaneous exposure of players with different aura ranks is deterministic and tested.
|
||||||
|
|
||||||
## Related
|
## Related
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
package games.dmg.creeperfear;
|
package games.dmg.creeperfear;
|
||||||
|
|
||||||
|
import games.dmg.creeperfear.aura.AuraRules;
|
||||||
|
import games.dmg.creeperfear.aura.CreeperAuraListener;
|
||||||
import games.dmg.creeperfear.listener.CreeperDeathListener;
|
import games.dmg.creeperfear.listener.CreeperDeathListener;
|
||||||
|
import games.dmg.creeperfear.listener.PlayerSessionListener;
|
||||||
import games.dmg.creeperfear.progress.ProgressService;
|
import games.dmg.creeperfear.progress.ProgressService;
|
||||||
import games.dmg.creeperfear.progress.SqliteProgressRepository;
|
import games.dmg.creeperfear.progress.SqliteProgressRepository;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
@@ -14,9 +17,20 @@ public final class CreeperFearPlugin extends JavaPlugin {
|
|||||||
public void onEnable() {
|
public void onEnable() {
|
||||||
try {
|
try {
|
||||||
Path databasePath = getDataFolder().toPath().resolve("player-progress.sqlite3");
|
Path databasePath = getDataFolder().toPath().resolve("player-progress.sqlite3");
|
||||||
progressService = new ProgressService(new SqliteProgressRepository(databasePath));
|
AuraRules auraRules = AuraRules.defaults();
|
||||||
|
progressService = new ProgressService(new SqliteProgressRepository(databasePath), auraRules);
|
||||||
getServer().getPluginManager().registerEvents(
|
getServer().getPluginManager().registerEvents(
|
||||||
new CreeperDeathListener(progressService, getLogger()), this);
|
new CreeperDeathListener(progressService, getLogger()), this);
|
||||||
|
getServer().getPluginManager().registerEvents(
|
||||||
|
new PlayerSessionListener(progressService, getLogger()), this);
|
||||||
|
getServer().getPluginManager().registerEvents(
|
||||||
|
new CreeperAuraListener(progressService, auraRules), this);
|
||||||
|
getServer().getOnlinePlayers().forEach(player -> progressService.loadOnline(player.getUniqueId())
|
||||||
|
.exceptionally(failure -> {
|
||||||
|
getLogger().log(Level.SEVERE,
|
||||||
|
"Could not load Creeper Aura progress for " + player.getUniqueId(), failure);
|
||||||
|
return null;
|
||||||
|
}));
|
||||||
getLogger().info("Creeper Fear enabled");
|
getLogger().info("Creeper Fear enabled");
|
||||||
} catch (RuntimeException exception) {
|
} catch (RuntimeException exception) {
|
||||||
getLogger().log(Level.SEVERE, "Creeper Fear could not initialize its progress storage", exception);
|
getLogger().log(Level.SEVERE, "Creeper Fear could not initialize its progress storage", exception);
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package games.dmg.creeperfear.aura;
|
||||||
|
|
||||||
|
import games.dmg.creeperfear.progress.AuraRank;
|
||||||
|
import games.dmg.creeperfear.progress.PlayerProgress;
|
||||||
|
import java.util.EnumMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public final class AuraRules {
|
||||||
|
private final Map<AuraRank, Integer> requirements;
|
||||||
|
private final Map<AuraRank, Double> damageMultipliers;
|
||||||
|
|
||||||
|
public AuraRules(Map<AuraRank, Integer> requirements, Map<AuraRank, Double> damageMultipliers) {
|
||||||
|
this.requirements = new EnumMap<>(requirements);
|
||||||
|
this.damageMultipliers = new EnumMap<>(damageMultipliers);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AuraRules defaults() {
|
||||||
|
Map<AuraRank, Integer> requirements = new EnumMap<>(AuraRank.class);
|
||||||
|
for (AuraRank rank : AuraRank.values()) {
|
||||||
|
if (!rank.isMaximum()) {
|
||||||
|
requirements.put(rank, 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Map<AuraRank, Double> multipliers = new EnumMap<>(AuraRank.class);
|
||||||
|
multipliers.put(AuraRank.LOCKED, 1.0);
|
||||||
|
multipliers.put(AuraRank.I, 3.0);
|
||||||
|
multipliers.put(AuraRank.II, 2.0);
|
||||||
|
multipliers.put(AuraRank.III, 1.5);
|
||||||
|
multipliers.put(AuraRank.IV, 1.0);
|
||||||
|
multipliers.put(AuraRank.V, 0.5);
|
||||||
|
multipliers.put(AuraRank.VI, 0.0);
|
||||||
|
return new AuraRules(requirements, multipliers);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PlayerProgress advanceIfEarned(PlayerProgress progress) {
|
||||||
|
if (progress.rank().isMaximum()) {
|
||||||
|
return progress;
|
||||||
|
}
|
||||||
|
int required = requirements.get(progress.rank());
|
||||||
|
if (progress.tierKills() < required) {
|
||||||
|
return progress;
|
||||||
|
}
|
||||||
|
return new PlayerProgress(
|
||||||
|
progress.playerId(), progress.lastKnownName(), progress.rank().next(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public double damageMultiplier(AuraRank rank) {
|
||||||
|
return damageMultipliers.get(rank);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int requirementForCurrentRank(AuraRank rank) {
|
||||||
|
if (rank.isMaximum()) {
|
||||||
|
throw new IllegalArgumentException("Rank VI has no next requirement");
|
||||||
|
}
|
||||||
|
return requirements.get(rank);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
package games.dmg.creeperfear.aura;
|
||||||
|
|
||||||
|
import games.dmg.creeperfear.progress.AuraRank;
|
||||||
|
import games.dmg.creeperfear.progress.PlayerProgress;
|
||||||
|
import games.dmg.creeperfear.progress.ProgressService;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import org.bukkit.entity.Creeper;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.event.EventHandler;
|
||||||
|
import org.bukkit.event.EventPriority;
|
||||||
|
import org.bukkit.event.Listener;
|
||||||
|
import org.bukkit.event.entity.EntityDamageByEntityEvent;
|
||||||
|
import org.bukkit.event.entity.EntityDamageEvent.DamageCause;
|
||||||
|
import org.bukkit.event.entity.EntityExplodeEvent;
|
||||||
|
|
||||||
|
public final class CreeperAuraListener implements Listener {
|
||||||
|
private static final int RECENT_EXPLOSION_LIMIT = 1024;
|
||||||
|
|
||||||
|
private final Function<UUID, Optional<PlayerProgress>> progressLookup;
|
||||||
|
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) {
|
||||||
|
this(progressService::cached, rules);
|
||||||
|
}
|
||||||
|
|
||||||
|
CreeperAuraListener(
|
||||||
|
Function<UUID, Optional<PlayerProgress>> progressLookup,
|
||||||
|
AuraRules rules) {
|
||||||
|
this.progressLookup = progressLookup;
|
||||||
|
this.rules = rules;
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.LOWEST)
|
||||||
|
public void onCreeperDamage(EntityDamageByEntityEvent event) {
|
||||||
|
if (event.getCause() != DamageCause.ENTITY_EXPLOSION
|
||||||
|
|| !(event.getDamager() instanceof Creeper creeper)
|
||||||
|
|| !(event.getEntity() instanceof Player player)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
PlayerProgress progress = progressLookup.apply(player.getUniqueId()).orElse(null);
|
||||||
|
if (progress == null || !progress.rank().isUnlocked()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
markProtected(creeper.getUniqueId());
|
||||||
|
AuraRank rank = progress.rank();
|
||||||
|
if (rank.isMaximum()) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.setDamage(event.getDamage() * rules.damageMultiplier(rank));
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||||
|
public void onCreeperExplode(EntityExplodeEvent event) {
|
||||||
|
if (event.getEntity() instanceof Creeper creeper && takeProtected(creeper.getUniqueId())) {
|
||||||
|
event.blockList().clear();
|
||||||
|
event.setYield(0.0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void markProtected(UUID creeperId) {
|
||||||
|
protectedExplosions.put(creeperId, Boolean.TRUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized boolean takeProtected(UUID creeperId) {
|
||||||
|
return protectedExplosions.remove(creeperId) != null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package games.dmg.creeperfear.listener;
|
||||||
|
|
||||||
|
import games.dmg.creeperfear.progress.ProgressService;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
import org.bukkit.event.EventHandler;
|
||||||
|
import org.bukkit.event.Listener;
|
||||||
|
import org.bukkit.event.player.PlayerJoinEvent;
|
||||||
|
import org.bukkit.event.player.PlayerQuitEvent;
|
||||||
|
|
||||||
|
public final class PlayerSessionListener implements Listener {
|
||||||
|
private final ProgressService progressService;
|
||||||
|
private final Logger logger;
|
||||||
|
|
||||||
|
public PlayerSessionListener(ProgressService progressService, Logger logger) {
|
||||||
|
this.progressService = progressService;
|
||||||
|
this.logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
public void onPlayerJoin(PlayerJoinEvent event) {
|
||||||
|
UUID playerId = event.getPlayer().getUniqueId();
|
||||||
|
progressService.loadOnline(playerId).whenComplete((progress, failure) -> {
|
||||||
|
if (failure != null) {
|
||||||
|
logger.log(Level.SEVERE, "Could not load Creeper Aura progress for " + playerId, failure);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
public void onPlayerQuit(PlayerQuitEvent event) {
|
||||||
|
progressService.unload(event.getPlayer().getUniqueId());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,7 +9,18 @@ public enum AuraRank {
|
|||||||
V,
|
V,
|
||||||
VI;
|
VI;
|
||||||
|
|
||||||
|
public boolean isUnlocked() {
|
||||||
|
return this != LOCKED;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isMaximum() {
|
public boolean isMaximum() {
|
||||||
return this == VI;
|
return this == VI;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public AuraRank next() {
|
||||||
|
if (isMaximum()) {
|
||||||
|
return VI;
|
||||||
|
}
|
||||||
|
return values()[ordinal() + 1];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,27 @@
|
|||||||
package games.dmg.creeperfear.progress;
|
package games.dmg.creeperfear.progress;
|
||||||
|
|
||||||
|
import games.dmg.creeperfear.aura.AuraRules;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
public final class ProgressService implements AutoCloseable {
|
public final class ProgressService implements AutoCloseable {
|
||||||
private final ProgressRepository repository;
|
private final ProgressRepository repository;
|
||||||
|
private final AuraRules auraRules;
|
||||||
|
private final ConcurrentHashMap<UUID, PlayerProgress> onlineProgress = new ConcurrentHashMap<>();
|
||||||
private final ExecutorService executor;
|
private final ExecutorService executor;
|
||||||
|
|
||||||
public ProgressService(ProgressRepository repository) {
|
public ProgressService(ProgressRepository repository) {
|
||||||
|
this(repository, AuraRules.defaults());
|
||||||
|
}
|
||||||
|
|
||||||
|
public ProgressService(ProgressRepository repository, AuraRules auraRules) {
|
||||||
this.repository = repository;
|
this.repository = repository;
|
||||||
|
this.auraRules = auraRules;
|
||||||
this.executor = Executors.newSingleThreadExecutor(runnable -> {
|
this.executor = Executors.newSingleThreadExecutor(runnable -> {
|
||||||
Thread thread = new Thread(runnable, "creeper-fear-progress");
|
Thread thread = new Thread(runnable, "creeper-fear-progress");
|
||||||
thread.setDaemon(true);
|
thread.setDaemon(true);
|
||||||
@@ -21,8 +30,31 @@ public final class ProgressService implements AutoCloseable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public CompletableFuture<PlayerProgress> recordCreeperKill(UUID playerId, String playerName) {
|
public CompletableFuture<PlayerProgress> recordCreeperKill(UUID playerId, String playerName) {
|
||||||
return CompletableFuture.supplyAsync(
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
() -> repository.recordCreeperKill(playerId, playerName), executor);
|
PlayerProgress recorded = repository.recordCreeperKill(playerId, playerName);
|
||||||
|
PlayerProgress advanced = auraRules.advanceIfEarned(recorded);
|
||||||
|
if (!advanced.equals(recorded)) {
|
||||||
|
repository.save(advanced);
|
||||||
|
}
|
||||||
|
onlineProgress.put(playerId, advanced);
|
||||||
|
return advanced;
|
||||||
|
}, executor);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CompletableFuture<Optional<PlayerProgress>> loadOnline(UUID playerId) {
|
||||||
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
|
Optional<PlayerProgress> progress = repository.find(playerId);
|
||||||
|
progress.ifPresent(value -> onlineProgress.put(playerId, value));
|
||||||
|
return progress;
|
||||||
|
}, executor);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void unload(UUID playerId) {
|
||||||
|
onlineProgress.remove(playerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<PlayerProgress> cached(UUID playerId) {
|
||||||
|
return Optional.ofNullable(onlineProgress.get(playerId));
|
||||||
}
|
}
|
||||||
|
|
||||||
public CompletableFuture<Optional<PlayerProgress>> find(UUID playerId) {
|
public CompletableFuture<Optional<PlayerProgress>> find(UUID playerId) {
|
||||||
@@ -30,7 +62,11 @@ public final class ProgressService implements AutoCloseable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public CompletableFuture<PlayerProgress> save(PlayerProgress progress) {
|
public CompletableFuture<PlayerProgress> save(PlayerProgress progress) {
|
||||||
return CompletableFuture.supplyAsync(() -> repository.save(progress), executor);
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
|
PlayerProgress saved = repository.save(progress);
|
||||||
|
onlineProgress.computeIfPresent(progress.playerId(), (ignored, existing) -> saved);
|
||||||
|
return saved;
|
||||||
|
}, executor);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package games.dmg.creeperfear.aura;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
import games.dmg.creeperfear.progress.AuraRank;
|
||||||
|
import games.dmg.creeperfear.progress.PlayerProgress;
|
||||||
|
import java.util.UUID;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
class AuraRulesTest {
|
||||||
|
private final AuraRules rules = AuraRules.defaults();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void unlocksOneRankAndResetsTierProgressAtOneHundredKills() {
|
||||||
|
PlayerProgress progress = new PlayerProgress(UUID.randomUUID(), "Player", AuraRank.LOCKED, 100);
|
||||||
|
|
||||||
|
PlayerProgress advanced = rules.advanceIfEarned(progress);
|
||||||
|
|
||||||
|
assertEquals(AuraRank.I, advanced.rank());
|
||||||
|
assertEquals(0, advanced.tierKills());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void appliesTheDefaultDamageMultipliers() {
|
||||||
|
assertEquals(3.0, rules.damageMultiplier(AuraRank.I));
|
||||||
|
assertEquals(2.0, rules.damageMultiplier(AuraRank.II));
|
||||||
|
assertEquals(1.5, rules.damageMultiplier(AuraRank.III));
|
||||||
|
assertEquals(1.0, rules.damageMultiplier(AuraRank.IV));
|
||||||
|
assertEquals(0.5, rules.damageMultiplier(AuraRank.V));
|
||||||
|
assertEquals(0.0, rules.damageMultiplier(AuraRank.VI));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package games.dmg.creeperfear.aura;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import games.dmg.creeperfear.progress.AuraRank;
|
||||||
|
import games.dmg.creeperfear.progress.PlayerProgress;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
import org.bukkit.block.Block;
|
||||||
|
import org.bukkit.entity.Creeper;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.event.entity.EntityDamageEvent.DamageCause;
|
||||||
|
import org.bukkit.event.entity.EntityDamageByEntityEvent;
|
||||||
|
import org.bukkit.event.entity.EntityExplodeEvent;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
class CreeperAuraListenerTest {
|
||||||
|
@Test
|
||||||
|
void multipliesPlayerDamageAndClearsBlocksForTheExplosion() {
|
||||||
|
UUID playerId = UUID.randomUUID();
|
||||||
|
UUID creeperId = UUID.randomUUID();
|
||||||
|
Player player = mock(Player.class);
|
||||||
|
Creeper creeper = mock(Creeper.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(creeper.getUniqueId()).thenReturn(creeperId);
|
||||||
|
when(damage.getEntity()).thenReturn(player);
|
||||||
|
when(damage.getDamager()).thenReturn(creeper);
|
||||||
|
when(damage.getCause()).thenReturn(DamageCause.ENTITY_EXPLOSION);
|
||||||
|
when(damage.getDamage()).thenReturn(10.0);
|
||||||
|
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.onCreeperDamage(damage);
|
||||||
|
listener.onCreeperExplode(explosion);
|
||||||
|
|
||||||
|
verify(damage).setDamage(30.0);
|
||||||
|
assertTrue(blocks.isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void cancelsAllDamageAtRankSix() {
|
||||||
|
UUID playerId = UUID.randomUUID();
|
||||||
|
Player player = mock(Player.class);
|
||||||
|
Creeper creeper = mock(Creeper.class);
|
||||||
|
EntityDamageByEntityEvent damage = mock(EntityDamageByEntityEvent.class);
|
||||||
|
when(player.getUniqueId()).thenReturn(playerId);
|
||||||
|
when(damage.getEntity()).thenReturn(player);
|
||||||
|
when(damage.getDamager()).thenReturn(creeper);
|
||||||
|
when(damage.getCause()).thenReturn(DamageCause.ENTITY_EXPLOSION);
|
||||||
|
PlayerProgress progress = new PlayerProgress(playerId, "Player", AuraRank.VI, 0);
|
||||||
|
CreeperAuraListener listener = new CreeperAuraListener(id -> Optional.of(progress), AuraRules.defaults());
|
||||||
|
|
||||||
|
listener.onCreeperDamage(damage);
|
||||||
|
|
||||||
|
verify(damage).setCancelled(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package games.dmg.creeperfear.progress;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
|
class RankProgressionIntegrationTest {
|
||||||
|
@TempDir
|
||||||
|
Path tempDir;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void theHundredthCurrentTierKillUnlocksOneRankAndPersistsTheReset() throws Exception {
|
||||||
|
UUID playerId = UUID.randomUUID();
|
||||||
|
Path database = tempDir.resolve("progress.sqlite3");
|
||||||
|
try (SqliteProgressRepository repository = new SqliteProgressRepository(database)) {
|
||||||
|
repository.save(new PlayerProgress(playerId, "Player", AuraRank.LOCKED, 99));
|
||||||
|
}
|
||||||
|
|
||||||
|
try (ProgressService service = new ProgressService(new SqliteProgressRepository(database))) {
|
||||||
|
PlayerProgress advanced = service.recordCreeperKill(playerId, "Player").get(2, TimeUnit.SECONDS);
|
||||||
|
|
||||||
|
assertEquals(AuraRank.I, advanced.rank());
|
||||||
|
assertEquals(0, advanced.tierKills());
|
||||||
|
}
|
||||||
|
|
||||||
|
try (SqliteProgressRepository repository = new SqliteProgressRepository(database)) {
|
||||||
|
PlayerProgress persisted = repository.find(playerId).orElseThrow();
|
||||||
|
assertEquals(AuraRank.I, persisted.rank());
|
||||||
|
assertEquals(0, persisted.tierKills());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user