feat(progression): reward unique boss kills
Release / release (push) Successful in 1m52s
CI / build (push) Successful in 54s

This commit is contained in:
dmg
2026-08-08 13:35:23 -04:00
parent 87dec73f8f
commit 64ba0e2af4
8 changed files with 93 additions and 14 deletions
+6
View File
@@ -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. - 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. - 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. - 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.
@@ -2,7 +2,7 @@
type: User Story type: User Story
title: "US-008: Reward and encourage spawn progression" 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. 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 # 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 ## 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. - [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.
- [ ] The reminder identifies the remaining eligible bosses and shows the next cooldown tier. - [x] 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. - [x] 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. - [x] 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. - [x] Clearing the cooldown makes `/spawn` immediately available as a progression reward.
- [ ] The newly credited kill also permanently applies the improved boss-based cooldown tier. - [x] 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. - [x] 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. - [x] 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] Banned players cannot earn kill credit, a cooldown reset, or an improved tier.
## Related ## Related
@@ -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<String> create(Set<BossType> 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) + ".");
}
}
@@ -27,6 +27,7 @@ final class SpawnCommand implements CommandExecutor, Listener {
private final Plugin plugin; private final Plugin plugin;
private final SpawnStateManager stateManager; private final SpawnStateManager stateManager;
private final AccessService access; private final AccessService access;
private final PluginSettings settings;
private final SpawnAreaService spawnAreas; private final SpawnAreaService spawnAreas;
private final SafeSpawnFinder safeSpawnFinder; private final SafeSpawnFinder safeSpawnFinder;
private final Clock clock; private final Clock clock;
@@ -36,12 +37,14 @@ final class SpawnCommand implements CommandExecutor, Listener {
Plugin plugin, Plugin plugin,
SpawnStateManager stateManager, SpawnStateManager stateManager,
AccessService access, AccessService access,
PluginSettings settings,
SpawnAreaService spawnAreas, SpawnAreaService spawnAreas,
SafeSpawnFinder safeSpawnFinder, SafeSpawnFinder safeSpawnFinder,
Clock clock) { Clock clock) {
this.plugin = plugin; this.plugin = plugin;
this.stateManager = stateManager; this.stateManager = stateManager;
this.access = access; this.access = access;
this.settings = settings;
this.spawnAreas = spawnAreas; this.spawnAreas = spawnAreas;
this.safeSpawnFinder = safeSpawnFinder; this.safeSpawnFinder = safeSpawnFinder;
this.clock = clock; this.clock = clock;
@@ -140,10 +143,8 @@ final class SpawnCommand implements CommandExecutor, Listener {
recordSuccessfulUse(player); recordSuccessfulUse(player);
player.sendTitle(ChatColor.GREEN + "Welcome to spawn", "", 5, 30, 10); player.sendTitle(ChatColor.GREEN + "Welcome to spawn", "", 5, 30, 10);
PlayerState completed = stateManager.player(player.getUniqueId(), player.getName()); PlayerState completed = stateManager.player(player.getUniqueId(), player.getName());
if (completed.defeatedBosses().size() < BossType.values().length) { ProgressionReminder.create(completed.defeatedBosses(), settings)
player.sendMessage(ChatColor.GOLD .ifPresent(player::sendMessage);
+ "Defeat each remaining boss to permanently reduce your /spawn cooldown.");
}
} else { } else {
player.sendMessage(ChatColor.RED player.sendMessage(ChatColor.RED
+ "The spawn teleport failed. Your cooldown was not used."); + "The spawn teleport failed. Your cooldown was not used.");
@@ -37,6 +37,7 @@ public final class TriggerSpawnPlugin extends JavaPlugin {
this, this,
stateManager, stateManager,
access, access,
settings,
new SpawnAreaService(stateManager), new SpawnAreaService(stateManager),
new SafeSpawnFinder(), new SafeSpawnFinder(),
Clock.systemUTC()); Clock.systemUTC());
@@ -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());
}
}
@@ -44,6 +44,18 @@ final class ProgressionServiceTest {
assertEquals(Duration.ofHours(4), second.applicableCooldown()); assertEquals(Duration.ofHours(4), second.applicableCooldown());
assertEquals(2, second.defeatedBosses().size()); assertEquals(2, second.defeatedBosses().size());
assertTrue(manager.snapshot().players().get(playerId).lastSpawnUse().isEmpty()); 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 @Test
@@ -43,6 +43,7 @@ final class SpawnCommandTest {
mock(Plugin.class), mock(Plugin.class),
manager, manager,
access, access,
settings,
new SpawnAreaService(manager), new SpawnAreaService(manager),
new SafeSpawnFinder(), new SafeSpawnFinder(),
Clock.fixed(Instant.parse("2026-08-08T11:00:00Z"), ZoneOffset.UTC)); Clock.fixed(Instant.parse("2026-08-08T11:00:00Z"), ZoneOffset.UTC));