feat(progression): reward unique boss kills
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 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.");
|
||||
|
||||
@@ -37,6 +37,7 @@ public final class TriggerSpawnPlugin extends JavaPlugin {
|
||||
this,
|
||||
stateManager,
|
||||
access,
|
||||
settings,
|
||||
new SpawnAreaService(stateManager),
|
||||
new SafeSpawnFinder(),
|
||||
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(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
|
||||
|
||||
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user