feat(onboarding): welcome eligible new players
This commit is contained in:
@@ -49,3 +49,10 @@
|
|||||||
- Ambiguous, cancelled, zero-damage, environmental, and self damage do not opt out a player.
|
- Ambiguous, cancelled, zero-damage, environmental, and self damage do not opt out a player.
|
||||||
- Combat opt-out bypasses administrative locks, immediately reconciles effect and prefix state, persists once, and sends one explanatory message.
|
- Combat opt-out bypasses administrative locks, immediately reconciles effect and prefix state, persists once, and sends one explanatory message.
|
||||||
- Verified attribution, victim safety, lock bypass, duplicate suppression, and the full build with `./gradlew clean check jar`.
|
- Verified attribution, victim safety, lock bypass, duplicate suppression, and the full build with `./gradlew clean check jar`.
|
||||||
|
|
||||||
|
### US-004 onboarding completed
|
||||||
|
|
||||||
|
- Join observation now sends the configurable welcome message on each login before the original first-join instant plus the configured calendar-day window.
|
||||||
|
- The exact expiration boundary and globally disabled behavior suppress reminders without resetting first-join state.
|
||||||
|
- The default message names all player commands and explains automatic combat opt-out.
|
||||||
|
- Verified repeated joins, expiration, global suppression, first-join retention, and the full build with `./gradlew clean check jar`.
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
type: User Story
|
type: User Story
|
||||||
title: "US-004: Introduce new players to Leaf"
|
title: "US-004: Introduce new players to Leaf"
|
||||||
description: Remind players how to control Leaf during their first seven calendar days on the server.
|
description: Remind players how to control Leaf during their first seven calendar days on the server.
|
||||||
status: backlog
|
status: done
|
||||||
---
|
---
|
||||||
|
|
||||||
# US-004: Introduce new players to Leaf
|
# US-004: Introduce new players to Leaf
|
||||||
@@ -11,14 +11,14 @@ As a **new player**, I want a brief explanation of Leaf when I join so that I kn
|
|||||||
|
|
||||||
## Acceptance criteria
|
## Acceptance criteria
|
||||||
|
|
||||||
- [ ] Leaf records the first observed join time for each player by UUID as an RFC 3339 UTC timestamp.
|
- [x] Leaf records the first observed join time for each player by UUID as an RFC 3339 UTC timestamp.
|
||||||
- [ ] On every login before the first-join timestamp plus seven calendar days, the player receives a concise welcome message explaining Leaf protection.
|
- [x] On every login before the first-join timestamp plus seven calendar days, the player receives a concise welcome message explaining Leaf protection.
|
||||||
- [ ] The welcome message tells the player to use `/leaf on`, `/leaf off`, and `/leaf status`.
|
- [x] The welcome message tells the player to use `/leaf on`, `/leaf off`, and `/leaf status`.
|
||||||
- [ ] The message makes clear that attacking another player automatically opts the attacker out.
|
- [x] The message makes clear that attacking another player automatically opts the attacker out.
|
||||||
- [ ] Players no longer receive the welcome message once their seven-day onboarding period expires.
|
- [x] Players no longer receive the welcome message once their seven-day onboarding period expires.
|
||||||
- [ ] Existing first-join timestamps are not reset by logout, restart, opt-in changes, or global disablement.
|
- [x] Existing first-join timestamps are not reset by logout, restart, opt-in changes, or global disablement.
|
||||||
- [ ] No onboarding reminders are shown while Leaf is globally disabled.
|
- [x] No onboarding reminders are shown while Leaf is globally disabled.
|
||||||
- [ ] Re-enabling Leaf allows reminders to resume on subsequent logins only for players whose original seven-day period has not expired.
|
- [x] Re-enabling Leaf allows reminders to resume on subsequent logins only for players whose original seven-day period has not expired.
|
||||||
|
|
||||||
## Related
|
## Related
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package games.dmg.leaf;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import org.bukkit.Server;
|
import org.bukkit.Server;
|
||||||
@@ -51,6 +52,11 @@ public final class LeafRuntime {
|
|||||||
);
|
);
|
||||||
stateManager.saveIfDirty();
|
stateManager.saveIfDirty();
|
||||||
reconcile(player);
|
reconcile(player);
|
||||||
|
LeafSettings settings = settingsProvider.current();
|
||||||
|
Instant onboardingEnd = state.firstJoin().plus(settings.onboardingDays(), ChronoUnit.DAYS);
|
||||||
|
if (settings.enabled() && observedAt.isBefore(onboardingEnd)) {
|
||||||
|
player.sendMessage(LeafText.color(settings.welcomeMessage()));
|
||||||
|
}
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
|
|||||||
import static org.mockito.ArgumentMatchers.contains;
|
import static org.mockito.ArgumentMatchers.contains;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.never;
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
@@ -43,6 +44,49 @@ final class LeafRuntimeTest {
|
|||||||
verify(protection, org.mockito.Mockito.times(2)).apply(player, 1);
|
verify(protection, org.mockito.Mockito.times(2)).apply(player, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void welcomesPlayersOnEachJoinOnlyDuringTheConfiguredWindow() throws Exception {
|
||||||
|
UUID playerId = UUID.randomUUID();
|
||||||
|
Player player = player(playerId, "Alex");
|
||||||
|
Server server = mock(Server.class);
|
||||||
|
when(server.getPlayer(playerId)).thenReturn(player);
|
||||||
|
LeafRuntime runtime = runtime(
|
||||||
|
server,
|
||||||
|
mock(LeafProtection.class),
|
||||||
|
temporaryDirectory.resolve("welcome.yml")
|
||||||
|
);
|
||||||
|
Instant firstJoin = Instant.parse("2026-08-01T00:00:00Z");
|
||||||
|
|
||||||
|
runtime.observe(player, firstJoin);
|
||||||
|
runtime.observe(player, firstJoin.plusSeconds(6 * 86_400L));
|
||||||
|
runtime.observe(player, firstJoin.plusSeconds(7 * 86_400L));
|
||||||
|
|
||||||
|
verify(player, times(2)).sendMessage(contains("/leaf on, /leaf off, or /leaf status"));
|
||||||
|
verify(player, times(2)).sendMessage(contains("Attacking another player opts you out"));
|
||||||
|
assertEquals(firstJoin, runtime.status(playerId).firstJoin());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void suppressesWelcomeWhileLeafIsGloballyDisabled() throws Exception {
|
||||||
|
UUID playerId = UUID.randomUUID();
|
||||||
|
Player player = player(playerId, "Alex");
|
||||||
|
Server server = mock(Server.class);
|
||||||
|
when(server.getPlayer(playerId)).thenReturn(player);
|
||||||
|
LeafRuntime runtime = new LeafRuntime(
|
||||||
|
server,
|
||||||
|
new LeafSettingsProvider(LeafSettings.from(Map.of("enabled", false))),
|
||||||
|
new LeafStateManager(new YamlLeafStateRepository(
|
||||||
|
temporaryDirectory.resolve("disabled-welcome.yml")
|
||||||
|
)),
|
||||||
|
mock(LeafProtection.class),
|
||||||
|
mock(LeafIdentity.class)
|
||||||
|
);
|
||||||
|
|
||||||
|
runtime.observe(player, Instant.parse("2026-08-01T00:00:00Z"));
|
||||||
|
|
||||||
|
verify(player, never()).sendMessage(contains("Leaf protection is available"));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void lockedPlayerCannotChangeTheirChoice() throws Exception {
|
void lockedPlayerCannotChangeTheirChoice() throws Exception {
|
||||||
UUID playerId = UUID.randomUUID();
|
UUID playerId = UUID.randomUUID();
|
||||||
|
|||||||
Reference in New Issue
Block a user