feat(progress): count creeper explosion encounters

This commit is contained in:
dmg
2026-08-08 14:18:45 -04:00
parent c9e0a5d121
commit d1535bbf26
6 changed files with 143 additions and 0 deletions
@@ -5,6 +5,7 @@ import games.dmg.creeperfear.aura.CreeperAuraListener;
import games.dmg.creeperfear.feedback.ProgressDisplay;
import games.dmg.creeperfear.feedback.ProgressFeedback;
import games.dmg.creeperfear.listener.CreeperDeathListener;
import games.dmg.creeperfear.listener.CreeperExplosionProgressListener;
import games.dmg.creeperfear.listener.PlayerSessionListener;
import games.dmg.creeperfear.progress.ProgressService;
import games.dmg.creeperfear.progress.SqliteProgressRepository;
@@ -27,6 +28,8 @@ public final class CreeperFearPlugin extends JavaPlugin {
progressFeedback = new ProgressFeedback(this, new ProgressDisplay(auraRules), displayTicks);
getServer().getPluginManager().registerEvents(
new CreeperDeathListener(progressService, progressFeedback, getLogger()), this);
getServer().getPluginManager().registerEvents(
new CreeperExplosionProgressListener(progressService, progressFeedback, getLogger()), this);
getServer().getPluginManager().registerEvents(
new PlayerSessionListener(progressService, getLogger()), this);
getServer().getPluginManager().registerEvents(
@@ -0,0 +1,85 @@
package games.dmg.creeperfear.listener;
import games.dmg.creeperfear.feedback.KillFeedback;
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.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.logging.Level;
import java.util.logging.Logger;
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;
public final class CreeperExplosionProgressListener implements Listener {
private static final int RECENT_AWARD_LIMIT = 4096;
private final BiFunction<UUID, String, CompletableFuture<PlayerProgress>> recorder;
private final Function<UUID, AuraRank> rankLookup;
private final KillFeedback feedback;
private final Logger logger;
private final Map<ExplosionAward, Boolean> recentAwards = new LinkedHashMap<>(128, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<ExplosionAward, Boolean> eldest) {
return size() > RECENT_AWARD_LIMIT;
}
};
public CreeperExplosionProgressListener(
ProgressService progressService, KillFeedback feedback, Logger logger) {
this(
progressService::recordCreeperKill,
playerId -> progressService.cached(playerId).map(PlayerProgress::rank).orElse(AuraRank.LOCKED),
feedback,
logger);
}
CreeperExplosionProgressListener(
BiFunction<UUID, String, CompletableFuture<PlayerProgress>> recorder,
Function<UUID, AuraRank> rankLookup,
KillFeedback feedback,
Logger logger) {
this.recorder = recorder;
this.rankLookup = rankLookup;
this.feedback = feedback;
this.logger = logger;
}
@EventHandler(priority = EventPriority.LOWEST)
public void onCreeperExplosionDamage(EntityDamageByEntityEvent event) {
if (event.getCause() != DamageCause.ENTITY_EXPLOSION
|| !(event.getDamager() instanceof Creeper creeper)
|| !(event.getEntity() instanceof Player player)) {
return;
}
UUID playerId = player.getUniqueId();
if (!markNew(creeper.getUniqueId(), playerId)) {
return;
}
AuraRank previousRank = rankLookup.apply(playerId);
recorder.apply(playerId, player.getName()).whenComplete((progress, failure) -> {
if (failure != null) {
logger.log(Level.SEVERE,
"Could not persist creeper explosion progress for " + playerId, failure);
return;
}
feedback.show(player, previousRank, progress);
});
}
private synchronized boolean markNew(UUID creeperId, UUID playerId) {
return recentAwards.putIfAbsent(new ExplosionAward(creeperId, playerId), Boolean.TRUE) == null;
}
private record ExplosionAward(UUID creeperId, UUID playerId) {
}
}
@@ -0,0 +1,49 @@
package games.dmg.creeperfear.listener;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import games.dmg.creeperfear.feedback.KillFeedback;
import games.dmg.creeperfear.progress.AuraRank;
import games.dmg.creeperfear.progress.PlayerProgress;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.function.BiFunction;
import java.util.logging.Logger;
import org.bukkit.entity.Creeper;
import org.bukkit.entity.Player;
import org.bukkit.event.entity.EntityDamageByEntityEvent;
import org.bukkit.event.entity.EntityDamageEvent.DamageCause;
import org.junit.jupiter.api.Test;
class CreeperExplosionProgressListenerTest {
@Test
void awardsEachHitPlayerOnlyOnceAndShowsNormalKillFeedback() {
UUID creeperId = UUID.randomUUID();
UUID playerId = UUID.randomUUID();
Creeper creeper = mock(Creeper.class);
Player player = mock(Player.class);
EntityDamageByEntityEvent event = mock(EntityDamageByEntityEvent.class);
@SuppressWarnings("unchecked")
BiFunction<UUID, String, CompletableFuture<PlayerProgress>> recorder = mock(BiFunction.class);
KillFeedback feedback = mock(KillFeedback.class);
PlayerProgress progress = new PlayerProgress(playerId, "Player", AuraRank.I, 0);
when(event.getCause()).thenReturn(DamageCause.ENTITY_EXPLOSION);
when(event.getDamager()).thenReturn(creeper);
when(event.getEntity()).thenReturn(player);
when(creeper.getUniqueId()).thenReturn(creeperId);
when(player.getUniqueId()).thenReturn(playerId);
when(player.getName()).thenReturn("Player");
when(recorder.apply(playerId, "Player")).thenReturn(CompletableFuture.completedFuture(progress));
CreeperExplosionProgressListener listener = new CreeperExplosionProgressListener(
recorder, ignored -> AuraRank.LOCKED, feedback, Logger.getAnonymousLogger());
listener.onCreeperExplosionDamage(event);
listener.onCreeperExplosionDamage(event);
verify(recorder, times(1)).apply(playerId, "Player");
verify(feedback).show(player, AuraRank.LOCKED, progress);
}
}