diff --git a/design/log.md b/design/log.md index 10d5216..093ae09 100644 --- a/design/log.md +++ b/design/log.md @@ -64,3 +64,9 @@ description: Chronological record of significant Trigger Spawn design decisions. - Added a three-second on-screen warm-up cancelled by block movement, jumping, falling, teleportation, world changes, or disconnects. - Safe destinations are sampled within the current world's circle using bounded attempts and a closest-elevation search that rejects environmental hazards. - Only completed teleports consume cooldown, and incomplete progression receives a post-teleport reminder. + +## 2026-08-08 — Progression rewards and reminders + +- Successful `/spawn` teleports now identify each remaining boss and show the next permanent boss-based cooldown tier. +- Fully progressed players receive no unnecessary reminder. +- Verified that newly credited unique kills make `/spawn` immediately available while duplicate kills preserve the existing cooldown. diff --git a/design/user-stories/us-008-reward-spawn-progression.md b/design/user-stories/us-008-reward-spawn-progression.md index 77e57ec..744a2ed 100644 --- a/design/user-stories/us-008-reward-spawn-progression.md +++ b/design/user-stories/us-008-reward-spawn-progression.md @@ -2,7 +2,7 @@ type: User Story title: "US-008: Reward and encourage spawn progression" description: Remind players how to improve spawn access and reward each newly credited unique boss kill with an immediate cooldown reset. -status: backlog +status: done --- # US-008: Reward and encourage spawn progression @@ -11,15 +11,15 @@ As a **player**, I want `/spawn` to explain how I can improve its cooldown and r ## Acceptance criteria -- [ ] After every successful `/spawn`, a player who has not defeated all three unique qualifying enemies receives a reminder that defeating the remaining bosses permanently reduces the cooldown. -- [ ] The reminder identifies the remaining eligible bosses and shows the next cooldown tier. -- [ ] A player who has completed all three unique qualifying kills does not receive the progression reminder. -- [ ] Each newly credited unique Warden, Ender Dragon, or Wither kill immediately clears the player's active `/spawn` cooldown. -- [ ] Clearing the cooldown makes `/spawn` immediately available as a progression reward. -- [ ] The newly credited kill also permanently applies the improved boss-based cooldown tier. -- [ ] Repeated kills of an already credited enemy do not clear the cooldown. -- [ ] The on-screen kill message states that `/spawn` is immediately available and displays the new cooldown tier. -- [ ] Banned players cannot earn kill credit, a cooldown reset, or an improved tier. +- [x] After every successful `/spawn`, a player who has not defeated all three unique qualifying enemies receives a reminder that defeating the remaining bosses permanently reduces the cooldown. +- [x] The reminder identifies the remaining eligible bosses and shows the next cooldown tier. +- [x] A player who has completed all three unique qualifying kills does not receive the progression reminder. +- [x] Each newly credited unique Warden, Ender Dragon, or Wither kill immediately clears the player's active `/spawn` cooldown. +- [x] Clearing the cooldown makes `/spawn` immediately available as a progression reward. +- [x] The newly credited kill also permanently applies the improved boss-based cooldown tier. +- [x] Repeated kills of an already credited enemy do not clear the cooldown. +- [x] The on-screen kill message states that `/spawn` is immediately available and displays the new cooldown tier. +- [x] Banned players cannot earn kill credit, a cooldown reset, or an improved tier. ## Related diff --git a/src/main/java/games/dmg/triggerspawn/ProgressionReminder.java b/src/main/java/games/dmg/triggerspawn/ProgressionReminder.java new file mode 100644 index 0000000..79cfbda --- /dev/null +++ b/src/main/java/games/dmg/triggerspawn/ProgressionReminder.java @@ -0,0 +1,31 @@ +package games.dmg.triggerspawn; + +import java.time.Duration; +import java.util.Arrays; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import org.bukkit.ChatColor; + +final class ProgressionReminder { + private ProgressionReminder() { + } + + static Optional create(Set defeated, PluginSettings settings) { + if (defeated.size() >= BossType.values().length) { + return Optional.empty(); + } + String remaining = Arrays.stream(BossType.values()) + .filter(boss -> !defeated.contains(boss)) + .map(BossType::displayName) + .collect(Collectors.joining(", ")); + Duration nextTier = switch (defeated.size()) { + case 0 -> settings.oneKillCooldown(); + case 1 -> settings.twoKillCooldown(); + default -> settings.threeKillCooldown(); + }; + return Optional.of(ChatColor.GOLD + "Progression reminder: " + ChatColor.YELLOW + + "defeat " + remaining + " to permanently reduce your boss cooldown; " + + "the next tier is " + DurationFormatter.format(nextTier) + "."); + } +} diff --git a/src/main/java/games/dmg/triggerspawn/SpawnCommand.java b/src/main/java/games/dmg/triggerspawn/SpawnCommand.java index 6906951..81585e7 100644 --- a/src/main/java/games/dmg/triggerspawn/SpawnCommand.java +++ b/src/main/java/games/dmg/triggerspawn/SpawnCommand.java @@ -27,6 +27,7 @@ final class SpawnCommand implements CommandExecutor, Listener { private final Plugin plugin; private final SpawnStateManager stateManager; private final AccessService access; + private final PluginSettings settings; private final SpawnAreaService spawnAreas; private final SafeSpawnFinder safeSpawnFinder; private final Clock clock; @@ -36,12 +37,14 @@ final class SpawnCommand implements CommandExecutor, Listener { Plugin plugin, SpawnStateManager stateManager, AccessService access, + PluginSettings settings, SpawnAreaService spawnAreas, SafeSpawnFinder safeSpawnFinder, Clock clock) { this.plugin = plugin; this.stateManager = stateManager; this.access = access; + this.settings = settings; this.spawnAreas = spawnAreas; this.safeSpawnFinder = safeSpawnFinder; this.clock = clock; @@ -140,10 +143,8 @@ final class SpawnCommand implements CommandExecutor, Listener { recordSuccessfulUse(player); player.sendTitle(ChatColor.GREEN + "Welcome to spawn", "", 5, 30, 10); PlayerState completed = stateManager.player(player.getUniqueId(), player.getName()); - if (completed.defeatedBosses().size() < BossType.values().length) { - player.sendMessage(ChatColor.GOLD - + "Defeat each remaining boss to permanently reduce your /spawn cooldown."); - } + ProgressionReminder.create(completed.defeatedBosses(), settings) + .ifPresent(player::sendMessage); } else { player.sendMessage(ChatColor.RED + "The spawn teleport failed. Your cooldown was not used."); diff --git a/src/main/java/games/dmg/triggerspawn/TriggerSpawnPlugin.java b/src/main/java/games/dmg/triggerspawn/TriggerSpawnPlugin.java index e9416d3..494ee4a 100644 --- a/src/main/java/games/dmg/triggerspawn/TriggerSpawnPlugin.java +++ b/src/main/java/games/dmg/triggerspawn/TriggerSpawnPlugin.java @@ -37,6 +37,7 @@ public final class TriggerSpawnPlugin extends JavaPlugin { this, stateManager, access, + settings, new SpawnAreaService(stateManager), new SafeSpawnFinder(), Clock.systemUTC()); diff --git a/src/test/java/games/dmg/triggerspawn/ProgressionReminderTest.java b/src/test/java/games/dmg/triggerspawn/ProgressionReminderTest.java new file mode 100644 index 0000000..046d26e --- /dev/null +++ b/src/test/java/games/dmg/triggerspawn/ProgressionReminderTest.java @@ -0,0 +1,27 @@ +package games.dmg.triggerspawn; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Optional; +import java.util.Set; +import org.bukkit.configuration.MemoryConfiguration; +import org.junit.jupiter.api.Test; + +final class ProgressionReminderTest { + @Test + void identifiesRemainingBossesAndNextPermanentTier() { + PluginSettings settings = PluginSettings.from(new MemoryConfiguration()); + + String reminder = ProgressionReminder.create(Set.of(BossType.WARDEN), settings).orElseThrow(); + + assertTrue(reminder.contains("Dragon")); + assertTrue(reminder.contains("Wither")); + assertTrue(reminder.contains("4h")); + } + + @Test + void completedProgressionNeedsNoReminder() { + PluginSettings settings = PluginSettings.from(new MemoryConfiguration()); + assertTrue(ProgressionReminder.create(Set.of(BossType.values()), settings).isEmpty()); + } +} diff --git a/src/test/java/games/dmg/triggerspawn/ProgressionServiceTest.java b/src/test/java/games/dmg/triggerspawn/ProgressionServiceTest.java index dc35348..8558bf7 100644 --- a/src/test/java/games/dmg/triggerspawn/ProgressionServiceTest.java +++ b/src/test/java/games/dmg/triggerspawn/ProgressionServiceTest.java @@ -44,6 +44,18 @@ final class ProgressionServiceTest { assertEquals(Duration.ofHours(4), second.applicableCooldown()); assertEquals(2, second.defeatedBosses().size()); assertTrue(manager.snapshot().players().get(playerId).lastSpawnUse().isEmpty()); + + Instant laterUse = Instant.parse("2026-08-08T12:00:00Z"); + PlayerState current = manager.snapshot().players().get(playerId); + manager.putPlayer(new PlayerState( + current.playerId(), + current.latestName(), + current.defeatedBosses(), + current.grantedCooldown(), + false, + Optional.of(laterUse))); + progression.creditKill(identity, BossType.WARDEN); + assertEquals(laterUse, manager.snapshot().players().get(playerId).lastSpawnUse().orElseThrow()); } @Test diff --git a/src/test/java/games/dmg/triggerspawn/SpawnCommandTest.java b/src/test/java/games/dmg/triggerspawn/SpawnCommandTest.java index 89a7ba8..5043e9a 100644 --- a/src/test/java/games/dmg/triggerspawn/SpawnCommandTest.java +++ b/src/test/java/games/dmg/triggerspawn/SpawnCommandTest.java @@ -43,6 +43,7 @@ final class SpawnCommandTest { mock(Plugin.class), manager, access, + settings, new SpawnAreaService(manager), new SafeSpawnFinder(), Clock.fixed(Instant.parse("2026-08-08T11:00:00Z"), ZoneOffset.UTC));