Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
323d177573 | ||
|
|
ea267ff867 | ||
|
|
24c54a7281 | ||
|
|
fe07c202f9 |
@@ -6,6 +6,41 @@ description: Chronological record of material decisions affecting the Spigot Tyr
|
|||||||
|
|
||||||
# Spigot Tyrant Design Log
|
# Spigot Tyrant Design Log
|
||||||
|
|
||||||
|
## 2026-08-14 — Opt-out proximity glow completed
|
||||||
|
|
||||||
|
- Extended US-011 with a globally visible green outline for opted-out players in the Tyrant's world and configured range while the event is running.
|
||||||
|
- A periodic controller removes the glow when eligibility, proximity, world, lifecycle, or player availability changes and clears managed state on logout and shutdown.
|
||||||
|
- Scoreboard presentation records and restores each player's previous team and glowing state instead of permanently replacing unrelated presentation.
|
||||||
|
- Verified inclusive range behavior, lifecycle and availability gating, green presentation, restoration, and the full Gradle build.
|
||||||
|
|
||||||
|
## 2026-08-14 — Opt-out proximity glow started
|
||||||
|
|
||||||
|
- US-011 is reopened for a test-first green outline on opted-out players near the Tyrant during a running event, using the existing configurable Tyrant range.
|
||||||
|
- Cleanup will cover range, world, participation, lifecycle, availability, shutdown, and restoration of pre-existing scoreboard-team membership.
|
||||||
|
|
||||||
|
## 2026-08-14 — Join messaging refinement completed
|
||||||
|
|
||||||
|
- Updated US-011 so join output no longer announces running, paused, or unstarted lifecycle state.
|
||||||
|
- Eligible unassigned players now see the neutral prompt, `Use /tyrant optout if you do not want to participate in Tyrant events.`
|
||||||
|
- Role, class, ability, cooldown, and existing opt-out details remain unchanged.
|
||||||
|
- Verified both assigned and unassigned join output with automated tests.
|
||||||
|
|
||||||
|
## 2026-08-14 — Join messaging refinement started
|
||||||
|
|
||||||
|
- US-011 is reopened to remove lifecycle announcements from join output and make the opt-out prompt refer generally to Tyrant events.
|
||||||
|
|
||||||
|
## 2026-08-14 — Opt-out combat protection completed
|
||||||
|
|
||||||
|
- Extended US-011 with bidirectional combat isolation between opted-out players and the Tyrant, Assassin, Fixer, or Tamer while the event is running.
|
||||||
|
- Direct player attacks and player-fired projectiles are cancelled at highest event priority, and the attacker is told that the target has chosen not to participate in the Tyrant event.
|
||||||
|
- Paused, unstarted, and unrelated combat remains ordinary Minecraft gameplay.
|
||||||
|
- Verified both attack directions, every Tyrant-side class, projectile attribution, lifecycle gating, and unrelated combat with automated tests.
|
||||||
|
|
||||||
|
## 2026-08-14 — Opt-out combat protection started
|
||||||
|
|
||||||
|
- US-011 is reopened for test-first running-event combat isolation between opted-out players and the Tyrant or Tyrant-side class holders.
|
||||||
|
- Paused and unstarted gameplay remains ordinary Minecraft combat; blocked direct and projectile attackers receive a concise Tyrant event participation message.
|
||||||
|
|
||||||
## 2026-08-14 — Tamer completed
|
## 2026-08-14 — Tamer completed
|
||||||
|
|
||||||
- Completed US-007 with the no-cooldown owner-bound Tamer's Lead, server-generated entity snapshots, durable capture IDs, and one bound Captured Mob item per entity.
|
- Completed US-007 with the no-cooldown owner-bound Tamer's Lead, server-generated entity snapshots, durable capture IDs, and one bound Captured Mob item per entity.
|
||||||
@@ -51,6 +86,14 @@ description: Chronological record of material decisions affecting the Spigot Tyr
|
|||||||
|
|
||||||
- US-005 begins with test-first bound-item invisibility, near-Tyrant duration doubling, and cooldown-gated double-jump effects.
|
- US-005 begins with test-first bound-item invisibility, near-Tyrant duration doubling, and cooldown-gated double-jump effects.
|
||||||
|
|
||||||
|
## 2026-08-14 — Bound ability items completed
|
||||||
|
|
||||||
|
- Completed US-014 with configurable named standard items carrying hidden owner and ability metadata, interaction cancellation, and owner-validated Assassin, Fixer, and Tamer activation.
|
||||||
|
- Cooldown items are consumed only on successful use, restored automatically when ready or by `/tyrant item`, deduplicated, and withheld with clear guidance when inventory space is unavailable.
|
||||||
|
- Drop, pickup, placement, crafting/storage movement, item-frame, dispenser, death, class loss, reign reset, login, and restart paths prevent transfer and duplication.
|
||||||
|
- Captured-mob custody items use independent IDs, reconcile missing or duplicate inventory items against durable snapshots, and spawn rather than vanish on drop or death.
|
||||||
|
- Verified readiness consumption and refresh, assignment cleanup, durable metadata state, captured custody, and the full Gradle build.
|
||||||
|
|
||||||
## 2026-08-14 — Bound ability item implementation started
|
## 2026-08-14 — Bound ability item implementation started
|
||||||
|
|
||||||
- US-014 begins with test-first item readiness and recovery, owner metadata, transfer prevention, cooldown redelivery, and Tamer item permanence.
|
- US-014 begins with test-first item readiness and recovery, owner metadata, transfer prevention, cooldown redelivery, and Tamer item permanence.
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ As a **player**, I want to understand my status and control my eligibility so th
|
|||||||
|
|
||||||
## Acceptance criteria
|
## Acceptance criteria
|
||||||
|
|
||||||
- [x] On join, a player is told the game status and their role, class, subclass, abilities, and relevant cooldowns.
|
- [x] On join, lifecycle state is omitted while role, class, subclass, ability, and relevant cooldown information remains available when applicable.
|
||||||
|
- [x] Eligible players are invited to opt out of Tyrant events without implying that an event is currently running.
|
||||||
- [x] An unassigned opted-in player is told that they are eligible and shown the opt-out command.
|
- [x] An unassigned opted-in player is told that they are eligible and shown the opt-out command.
|
||||||
- [x] An opted-out player is told when they may opt back in.
|
- [x] An opted-out player is told when they may opt back in.
|
||||||
- [x] An unassigned player can opt out through a command.
|
- [x] An unassigned player can opt out through a command.
|
||||||
@@ -20,6 +21,15 @@ As a **player**, I want to understand my status and control my eligibility so th
|
|||||||
- [x] A player cannot opt back in until seven days have elapsed by default.
|
- [x] A player cannot opt back in until seven days have elapsed by default.
|
||||||
- [x] The opt-out duration is configurable, uses unpaused elapsed time, and survives restart.
|
- [x] The opt-out duration is configurable, uses unpaused elapsed time, and survives restart.
|
||||||
- [x] Participation commands are idempotent and clearly report current status and remaining time.
|
- [x] Participation commands are idempotent and clearly report current status and remaining time.
|
||||||
|
- [x] While the event is running, damage is cancelled when the Tyrant or an Assassin, Fixer, or Tamer attacks an opted-out player.
|
||||||
|
- [x] While the event is running, damage is cancelled when an opted-out player attacks the Tyrant or an Assassin, Fixer, or Tamer.
|
||||||
|
- [x] Cancelled attackers are told that the target has chosen not to take part in the Tyrant event.
|
||||||
|
- [x] Protection covers direct player attacks and player-fired projectiles without changing unrelated combat.
|
||||||
|
- [x] Participation combat protection does not apply while the event is paused or not started.
|
||||||
|
- [x] While the event is running, an opted-out player in the same world and within the configured Tyrant range has a green glowing outline visible to everyone.
|
||||||
|
- [x] The opt-out glow is removed when proximity, participation, player availability, Tyrant availability, or running-event requirements stop being satisfied.
|
||||||
|
- [x] Existing scoreboard-team membership is restored after the opt-out glow ends, and plugin shutdown safely clears managed glow state.
|
||||||
|
- [x] Opt-out proximity glow does not apply while the event is paused or not started.
|
||||||
|
|
||||||
## Related
|
## Related
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
type: User Story
|
type: User Story
|
||||||
title: "US-014: Use class ability items"
|
title: "US-014: Use class ability items"
|
||||||
description: Give class holders named bound items that activate abilities without enabling transfer or cooldown bypasses.
|
description: Give class holders named bound items that activate abilities without enabling transfer or cooldown bypasses.
|
||||||
status: in-progress
|
status: done
|
||||||
---
|
---
|
||||||
|
|
||||||
# US-014: Use class ability items
|
# US-014: Use class ability items
|
||||||
@@ -11,17 +11,17 @@ As a **class holder**, I want a recognizable inventory item for my ability so th
|
|||||||
|
|
||||||
## Acceptance criteria
|
## Acceptance criteria
|
||||||
|
|
||||||
- [ ] Each item-triggered class ability uses a uniquely named standard Minecraft item with hidden persistent ownership and ability metadata.
|
- [x] Each item-triggered class ability uses a uniquely named standard Minecraft item with hidden persistent ownership and ability metadata.
|
||||||
- [ ] Right-clicking or swinging the appropriate item activates the ability without also performing an unsafe vanilla action.
|
- [x] Right-clicking or swinging the appropriate item activates the ability without also performing an unsafe vanilla action.
|
||||||
- [ ] Another player cannot activate, retain, craft with, place, store, frame, dispense, or otherwise transfer a bound ability item.
|
- [x] Another player cannot activate, retain, craft with, place, store, frame, dispense, or otherwise transfer a bound ability item.
|
||||||
- [ ] A cooldown ability item disappears only after successful activation and is restored when the cooldown finishes.
|
- [x] A cooldown ability item disappears only after successful activation and is restored when the cooldown finishes.
|
||||||
- [ ] The Tamer's named fishing rod has no cooldown and remains available after successful capture or placement.
|
- [x] The Tamer's named fishing rod has no cooldown and remains available after successful capture or placement.
|
||||||
- [ ] A dropped class ability item vanishes immediately; a dropped captured-mob item instead attempts to spawn its mob safely.
|
- [x] A dropped class ability item vanishes immediately; a dropped captured-mob item instead attempts to spawn its mob safely.
|
||||||
- [ ] `/tyrant item` restores currently available missing class items but cannot bypass cooldowns or create duplicates.
|
- [x] `/tyrant item` restores currently available missing class items but cannot bypass cooldowns or create duplicates.
|
||||||
- [ ] Missing ready items are restored on login and cooldown completion when inventory space is available.
|
- [x] Missing ready items are restored on login and cooldown completion when inventory space is available.
|
||||||
- [ ] When delivery fails because the inventory is full, no item is dropped and the player is told both why and how to use `/tyrant item` after making room.
|
- [x] When delivery fails because the inventory is full, no item is dropped and the player is told both why and how to use `/tyrant item` after making room.
|
||||||
- [ ] Bound class items are removed when a player loses the class or the Tyrant's reign ends.
|
- [x] Bound class items are removed when a player loses the class or the Tyrant's reign ends.
|
||||||
- [ ] Death, inventory movement, server restart, repeated commands, and concurrent events cannot duplicate a bound item or its captured mob.
|
- [x] Death, inventory movement, server restart, repeated commands, and concurrent events cannot duplicate a bound item or its captured mob.
|
||||||
|
|
||||||
## Related
|
## Related
|
||||||
|
|
||||||
|
|||||||
@@ -10,17 +10,20 @@ public final class AbilityItemRefreshTask implements Runnable {
|
|||||||
private final AbilityItemService items;
|
private final AbilityItemService items;
|
||||||
private final Server server;
|
private final Server server;
|
||||||
private final Clock clock;
|
private final Clock clock;
|
||||||
|
private final CapturedMobInventoryService capturedMobInventory;
|
||||||
|
|
||||||
public AbilityItemRefreshTask(
|
public AbilityItemRefreshTask(
|
||||||
TyrantStateManager stateManager,
|
TyrantStateManager stateManager,
|
||||||
AbilityReadinessService readiness,
|
AbilityReadinessService readiness,
|
||||||
AbilityItemService items,
|
AbilityItemService items,
|
||||||
|
CapturedMobInventoryService capturedMobInventory,
|
||||||
Server server,
|
Server server,
|
||||||
Clock clock
|
Clock clock
|
||||||
) {
|
) {
|
||||||
this.stateManager = stateManager;
|
this.stateManager = stateManager;
|
||||||
this.readiness = readiness;
|
this.readiness = readiness;
|
||||||
this.items = items;
|
this.items = items;
|
||||||
|
this.capturedMobInventory = capturedMobInventory;
|
||||||
this.server = server;
|
this.server = server;
|
||||||
this.clock = clock;
|
this.clock = clock;
|
||||||
}
|
}
|
||||||
@@ -40,6 +43,7 @@ public final class AbilityItemRefreshTask implements Runnable {
|
|||||||
}
|
}
|
||||||
items.removeInvalid(player, after);
|
items.removeInvalid(player, after);
|
||||||
items.recover(player, after);
|
items.recover(player, after);
|
||||||
|
capturedMobInventory.reconcile(player, after);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ public final class BoundItemListener implements Listener {
|
|||||||
if (items.isBoundAbilityItem(event.getCurrentItem())
|
if (items.isBoundAbilityItem(event.getCurrentItem())
|
||||||
|| items.isBoundAbilityItem(event.getCursor())) {
|
|| items.isBoundAbilityItem(event.getCursor())) {
|
||||||
if (!(event.getWhoClicked() instanceof Player player)
|
if (!(event.getWhoClicked() instanceof Player player)
|
||||||
|
|| event.isShiftClick()
|
||||||
|| event.getClickedInventory() == null
|
|| event.getClickedInventory() == null
|
||||||
|| !event.getClickedInventory().equals(player.getInventory())) {
|
|| !event.getClickedInventory().equals(player.getInventory())) {
|
||||||
event.setCancelled(true);
|
event.setCancelled(true);
|
||||||
|
|||||||
@@ -81,11 +81,17 @@ public final class BukkitAbilityItemService implements AbilityItemService {
|
|||||||
@Override
|
@Override
|
||||||
public void removeInvalid(Player player, PlayerState state) {
|
public void removeInvalid(Player player, PlayerState state) {
|
||||||
ItemStack[] contents = player.getInventory().getContents();
|
ItemStack[] contents = player.getInventory().getContents();
|
||||||
|
java.util.Set<Ability> retained = java.util.EnumSet.noneOf(Ability.class);
|
||||||
for (int index = 0; index < contents.length; index++) {
|
for (int index = 0; index < contents.length; index++) {
|
||||||
ItemStack item = contents[index];
|
ItemStack item = contents[index];
|
||||||
if (isBoundAbilityItem(item)
|
if (!isBoundAbilityItem(item)) {
|
||||||
&& (owner(item).filter(state.playerId()::equals).isEmpty()
|
continue;
|
||||||
|| ability(item).filter(state.readyAbilityItems()::contains).isEmpty())) {
|
}
|
||||||
|
Optional<Ability> itemAbility = ability(item);
|
||||||
|
boolean valid = owner(item).filter(state.playerId()::equals).isPresent()
|
||||||
|
&& itemAbility.filter(state.readyAbilityItems()::contains).isPresent()
|
||||||
|
&& itemAbility.filter(retained::add).isPresent();
|
||||||
|
if (!valid) {
|
||||||
player.getInventory().setItem(index, null);
|
player.getInventory().setItem(index, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package games.dmg.spigottyrant;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
import org.bukkit.ChatColor;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
|
||||||
|
public final class CapturedMobInventoryService {
|
||||||
|
private final CapturedMobItemService items;
|
||||||
|
private final PluginSettings settings;
|
||||||
|
|
||||||
|
public CapturedMobInventoryService(
|
||||||
|
CapturedMobItemService items,
|
||||||
|
PluginSettings settings
|
||||||
|
) {
|
||||||
|
this.items = items;
|
||||||
|
this.settings = settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reconcile(Player player, PlayerState state) {
|
||||||
|
Map<UUID, CapturedMob> stored = new java.util.HashMap<>();
|
||||||
|
for (CapturedMob mob : state.capturedMobs()) {
|
||||||
|
try {
|
||||||
|
UUID captureId = UUID.fromString(mob.data().get("capture-id"));
|
||||||
|
stored.putIfAbsent(captureId, mob);
|
||||||
|
} catch (IllegalArgumentException | NullPointerException ignored) {
|
||||||
|
// Invalid custody records are not materialized as items.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Set<UUID> retained = new HashSet<>();
|
||||||
|
ItemStack[] contents = player.getInventory().getContents();
|
||||||
|
for (int index = 0; index < contents.length; index++) {
|
||||||
|
ItemStack item = contents[index];
|
||||||
|
if (!items.isCapturedMob(item)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
UUID captureId = items.captureId(item).orElseThrow();
|
||||||
|
boolean valid = items.owner(item).filter(state.playerId()::equals).isPresent()
|
||||||
|
&& stored.containsKey(captureId) && retained.add(captureId);
|
||||||
|
if (!valid) {
|
||||||
|
player.getInventory().setItem(index, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (Map.Entry<UUID, CapturedMob> entry : stored.entrySet()) {
|
||||||
|
if (retained.contains(entry.getKey())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Map<Integer, ItemStack> leftovers = player.getInventory().addItem(
|
||||||
|
items.create(
|
||||||
|
state.playerId(), entry.getKey(), entry.getValue().entityType()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
if (!leftovers.isEmpty()) {
|
||||||
|
player.sendMessage(ChatColor.RED + settings.inventoryFullMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
package games.dmg.spigottyrant;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
import org.bukkit.ChatColor;
|
||||||
|
import org.bukkit.Server;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.event.EventHandler;
|
||||||
|
import org.bukkit.event.EventPriority;
|
||||||
|
import org.bukkit.event.Listener;
|
||||||
|
import org.bukkit.event.player.PlayerQuitEvent;
|
||||||
|
import org.bukkit.scoreboard.Scoreboard;
|
||||||
|
import org.bukkit.scoreboard.ScoreboardManager;
|
||||||
|
import org.bukkit.scoreboard.Team;
|
||||||
|
|
||||||
|
public final class OptOutGlowController implements Runnable, Listener {
|
||||||
|
private static final String TEAM_NAME = "tyrant-optout";
|
||||||
|
private final TyrantStateManager stateManager;
|
||||||
|
private final Server server;
|
||||||
|
private final PluginSettings settings;
|
||||||
|
private final Scoreboard scoreboard;
|
||||||
|
private final Team greenTeam;
|
||||||
|
private final Map<UUID, PreviousPresentation> managed = new HashMap<>();
|
||||||
|
|
||||||
|
public OptOutGlowController(
|
||||||
|
TyrantStateManager stateManager,
|
||||||
|
Server server,
|
||||||
|
PluginSettings settings
|
||||||
|
) {
|
||||||
|
this.stateManager = stateManager;
|
||||||
|
this.server = server;
|
||||||
|
this.settings = settings;
|
||||||
|
ScoreboardManager manager = Objects.requireNonNull(
|
||||||
|
server.getScoreboardManager(), "Scoreboard manager is unavailable"
|
||||||
|
);
|
||||||
|
scoreboard = manager.getMainScoreboard();
|
||||||
|
Team existing = scoreboard.getTeam(TEAM_NAME);
|
||||||
|
greenTeam = existing == null ? scoreboard.registerNewTeam(TEAM_NAME) : existing;
|
||||||
|
greenTeam.setColor(ChatColor.GREEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
GameState game = stateManager.game();
|
||||||
|
Player tyrant = game.tyrantId().map(server::getPlayer).orElse(null);
|
||||||
|
boolean tyrantAvailable = tyrant != null && !tyrant.isDead();
|
||||||
|
Set<UUID> online = new HashSet<>();
|
||||||
|
for (Player player : server.getOnlinePlayers()) {
|
||||||
|
online.add(player.getUniqueId());
|
||||||
|
PlayerState state = stateManager.players().get(player.getUniqueId());
|
||||||
|
boolean sameWorld = tyrantAvailable && !player.isDead()
|
||||||
|
&& player.getWorld().equals(tyrant.getWorld());
|
||||||
|
double distanceSquared = sameWorld
|
||||||
|
? player.getLocation().distanceSquared(tyrant.getLocation())
|
||||||
|
: Double.POSITIVE_INFINITY;
|
||||||
|
boolean shouldGlow = state != null && OptOutGlowPolicy.shouldGlow(
|
||||||
|
game, state, tyrantAvailable, sameWorld, distanceSquared,
|
||||||
|
settings.tyrantRangeBlocks()
|
||||||
|
);
|
||||||
|
if (shouldGlow && !managed.containsKey(player.getUniqueId())) {
|
||||||
|
apply(player);
|
||||||
|
} else if (!shouldGlow && managed.containsKey(player.getUniqueId())) {
|
||||||
|
restore(player.getUniqueId(), player);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (UUID playerId : Set.copyOf(managed.keySet())) {
|
||||||
|
if (!online.contains(playerId) && server.getPlayer(playerId) == null) {
|
||||||
|
restore(playerId, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR)
|
||||||
|
public void onQuit(PlayerQuitEvent event) {
|
||||||
|
if (managed.containsKey(event.getPlayer().getUniqueId())) {
|
||||||
|
restore(event.getPlayer().getUniqueId(), event.getPlayer());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clearAll() {
|
||||||
|
for (UUID playerId : Set.copyOf(managed.keySet())) {
|
||||||
|
restore(playerId, server.getPlayer(playerId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void apply(Player player) {
|
||||||
|
Team previousTeam = scoreboard.getEntryTeam(player.getName());
|
||||||
|
if (previousTeam == greenTeam) {
|
||||||
|
previousTeam = null;
|
||||||
|
}
|
||||||
|
managed.put(
|
||||||
|
player.getUniqueId(),
|
||||||
|
new PreviousPresentation(player.getName(), previousTeam, player.isGlowing())
|
||||||
|
);
|
||||||
|
greenTeam.addEntry(player.getName());
|
||||||
|
player.setGlowing(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void restore(UUID playerId, Player player) {
|
||||||
|
PreviousPresentation previous = managed.remove(playerId);
|
||||||
|
if (previous == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
greenTeam.removeEntry(previous.playerName());
|
||||||
|
if (previous.team() != null) {
|
||||||
|
try {
|
||||||
|
previous.team().addEntry(previous.playerName());
|
||||||
|
} catch (IllegalStateException ignored) {
|
||||||
|
// The prior team was removed while this plugin managed the entry.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (player != null) {
|
||||||
|
player.setGlowing(previous.glowing());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private record PreviousPresentation(
|
||||||
|
String playerName,
|
||||||
|
Team team,
|
||||||
|
boolean glowing
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package games.dmg.spigottyrant;
|
||||||
|
|
||||||
|
public final class OptOutGlowPolicy {
|
||||||
|
private OptOutGlowPolicy() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean shouldGlow(
|
||||||
|
GameState game,
|
||||||
|
PlayerState player,
|
||||||
|
boolean tyrantAvailable,
|
||||||
|
boolean playerAvailableInTyrantWorld,
|
||||||
|
double distanceSquared,
|
||||||
|
double rangeBlocks
|
||||||
|
) {
|
||||||
|
return game.lifecycle() == GameLifecycle.RUNNING
|
||||||
|
&& game.tyrantId().isPresent()
|
||||||
|
&& player.optedOutUntil().isPresent()
|
||||||
|
&& tyrantAvailable
|
||||||
|
&& playerAvailableInTyrantWorld
|
||||||
|
&& distanceSquared <= rangeBlocks * rangeBlocks;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package games.dmg.spigottyrant;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
import org.bukkit.ChatColor;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.entity.Projectile;
|
||||||
|
import org.bukkit.event.EventHandler;
|
||||||
|
import org.bukkit.event.EventPriority;
|
||||||
|
import org.bukkit.event.Listener;
|
||||||
|
import org.bukkit.event.entity.EntityDamageByEntityEvent;
|
||||||
|
|
||||||
|
public final class ParticipationCombatListener implements Listener {
|
||||||
|
public static final String OPTED_OUT_MESSAGE =
|
||||||
|
"That player has chosen not to take part in the Tyrant event.";
|
||||||
|
|
||||||
|
private final TyrantStateManager stateManager;
|
||||||
|
private final ParticipationCombatService combat;
|
||||||
|
|
||||||
|
public ParticipationCombatListener(
|
||||||
|
TyrantStateManager stateManager,
|
||||||
|
ParticipationCombatService combat
|
||||||
|
) {
|
||||||
|
this.stateManager = stateManager;
|
||||||
|
this.combat = combat;
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||||
|
public void onDamage(EntityDamageByEntityEvent event) {
|
||||||
|
if (!(event.getEntity() instanceof Player target)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Optional<Player> attacker = attackingPlayer(event);
|
||||||
|
if (attacker.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Player source = attacker.orElseThrow();
|
||||||
|
Map<UUID, PlayerState> players = stateManager.players();
|
||||||
|
PlayerState attackerState = players.getOrDefault(
|
||||||
|
source.getUniqueId(),
|
||||||
|
PlayerState.newPlayer(source.getUniqueId(), source.getName())
|
||||||
|
);
|
||||||
|
PlayerState targetState = players.getOrDefault(
|
||||||
|
target.getUniqueId(),
|
||||||
|
PlayerState.newPlayer(target.getUniqueId(), target.getName())
|
||||||
|
);
|
||||||
|
if (combat.blocks(stateManager.game(), attackerState, targetState)) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
source.sendMessage(ChatColor.YELLOW + OPTED_OUT_MESSAGE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Optional<Player> attackingPlayer(EntityDamageByEntityEvent event) {
|
||||||
|
if (event.getDamager() instanceof Player player) {
|
||||||
|
return Optional.of(player);
|
||||||
|
}
|
||||||
|
if (event.getDamager() instanceof Projectile projectile
|
||||||
|
&& projectile.getShooter() instanceof Player player) {
|
||||||
|
return Optional.of(player);
|
||||||
|
}
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package games.dmg.spigottyrant;
|
||||||
|
|
||||||
|
public final class ParticipationCombatService {
|
||||||
|
public boolean blocks(
|
||||||
|
GameState game,
|
||||||
|
PlayerState attacker,
|
||||||
|
PlayerState target
|
||||||
|
) {
|
||||||
|
if (game.lifecycle() != GameLifecycle.RUNNING) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
boolean attackerOptedOut = attacker.optedOutUntil().isPresent();
|
||||||
|
boolean targetOptedOut = target.optedOutUntil().isPresent();
|
||||||
|
boolean attackerTyrantSide = isTyrantSide(game, attacker);
|
||||||
|
boolean targetTyrantSide = isTyrantSide(game, target);
|
||||||
|
return (attackerTyrantSide && targetOptedOut)
|
||||||
|
|| (attackerOptedOut && targetTyrantSide);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isTyrantSide(GameState game, PlayerState player) {
|
||||||
|
return game.tyrantId().filter(player.playerId()::equals).isPresent()
|
||||||
|
|| player.tyrantClass() != TyrantClass.NONE;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,7 +15,6 @@ public final class PlayerStatusMessages {
|
|||||||
Instant now
|
Instant now
|
||||||
) {
|
) {
|
||||||
List<String> messages = new ArrayList<>();
|
List<String> messages = new ArrayList<>();
|
||||||
messages.add("Game: " + game.lifecycle());
|
|
||||||
boolean hasRole = false;
|
boolean hasRole = false;
|
||||||
if (game.tyrantId().filter(player.playerId()::equals).isPresent()) {
|
if (game.tyrantId().filter(player.playerId()::equals).isPresent()) {
|
||||||
messages.add("Role: TYRANT. Level " + game.tyrantLevel() + ", choices "
|
messages.add("Role: TYRANT. Level " + game.tyrantLevel() + ", choices "
|
||||||
@@ -45,8 +44,8 @@ public final class PlayerStatusMessages {
|
|||||||
messages.add("Participation: OPTED OUT. You may use /tyrant optin at "
|
messages.add("Participation: OPTED OUT. You may use /tyrant optin at "
|
||||||
+ player.optedOutUntil().orElseThrow() + ".");
|
+ player.optedOutUntil().orElseThrow() + ".");
|
||||||
} else if (!hasRole) {
|
} else if (!hasRole) {
|
||||||
messages.add("You currently have no role or class. Use /tyrant optout "
|
messages.add("Use /tyrant optout if you do not want to participate "
|
||||||
+ "if you do not want to participate.");
|
+ "in Tyrant events.");
|
||||||
}
|
}
|
||||||
return List.copyOf(messages);
|
return List.copyOf(messages);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ public final class SpigotTyrantPlugin extends JavaPlugin {
|
|||||||
private AssassinEffectController assassinEffects;
|
private AssassinEffectController assassinEffects;
|
||||||
private FixerEffectController fixerEffects;
|
private FixerEffectController fixerEffects;
|
||||||
private TyrantEffectController tyrantEffects;
|
private TyrantEffectController tyrantEffects;
|
||||||
|
private OptOutGlowController optOutGlow;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onEnable() {
|
public void onEnable() {
|
||||||
@@ -57,6 +58,9 @@ public final class SpigotTyrantPlugin extends JavaPlugin {
|
|||||||
this, settings
|
this, settings
|
||||||
);
|
);
|
||||||
CapturedMobService capturedMobs = new CapturedMobService();
|
CapturedMobService capturedMobs = new CapturedMobService();
|
||||||
|
CapturedMobInventoryService capturedMobInventory = new CapturedMobInventoryService(
|
||||||
|
capturedMobItems, settings
|
||||||
|
);
|
||||||
AssassinAbilityService assassinAbilities = new AssassinAbilityService(
|
AssassinAbilityService assassinAbilities = new AssassinAbilityService(
|
||||||
readiness, settings
|
readiness, settings
|
||||||
);
|
);
|
||||||
@@ -66,6 +70,7 @@ public final class SpigotTyrantPlugin extends JavaPlugin {
|
|||||||
FixerAbilityService fixerAbilities = new FixerAbilityService(readiness, settings);
|
FixerAbilityService fixerAbilities = new FixerAbilityService(readiness, settings);
|
||||||
fixerEffects = new FixerEffectController(stateManager, getServer(), settings, clock);
|
fixerEffects = new FixerEffectController(stateManager, getServer(), settings, clock);
|
||||||
tyrantEffects = new TyrantEffectController(stateManager, getServer(), settings);
|
tyrantEffects = new TyrantEffectController(stateManager, getServer(), settings);
|
||||||
|
optOutGlow = new OptOutGlowController(stateManager, getServer(), settings);
|
||||||
VigilanteCombatTracker combatTracker = new VigilanteCombatTracker();
|
VigilanteCombatTracker combatTracker = new VigilanteCombatTracker();
|
||||||
vigilanteEffects = new VigilanteEffectController(
|
vigilanteEffects = new VigilanteEffectController(
|
||||||
stateManager, getServer(), combatTracker, settings, clock
|
stateManager, getServer(), combatTracker, settings, clock
|
||||||
@@ -119,6 +124,13 @@ public final class SpigotTyrantPlugin extends JavaPlugin {
|
|||||||
),
|
),
|
||||||
this
|
this
|
||||||
);
|
);
|
||||||
|
getServer().getPluginManager().registerEvents(
|
||||||
|
new ParticipationCombatListener(
|
||||||
|
stateManager, new ParticipationCombatService()
|
||||||
|
),
|
||||||
|
this
|
||||||
|
);
|
||||||
|
getServer().getPluginManager().registerEvents(optOutGlow, this);
|
||||||
getServer().getPluginManager().registerEvents(
|
getServer().getPluginManager().registerEvents(
|
||||||
new VigilanteCombatListener(
|
new VigilanteCombatListener(
|
||||||
stateManager, combatTracker, clock, settings.vigilanteCombatDuration()
|
stateManager, combatTracker, clock, settings.vigilanteCombatDuration()
|
||||||
@@ -167,10 +179,12 @@ public final class SpigotTyrantPlugin extends JavaPlugin {
|
|||||||
getServer().getScheduler().runTaskTimer(this, assassinEffects, 10L, 10L);
|
getServer().getScheduler().runTaskTimer(this, assassinEffects, 10L, 10L);
|
||||||
getServer().getScheduler().runTaskTimer(this, fixerEffects, 10L, 10L);
|
getServer().getScheduler().runTaskTimer(this, fixerEffects, 10L, 10L);
|
||||||
getServer().getScheduler().runTaskTimer(this, tyrantEffects, 10L, 10L);
|
getServer().getScheduler().runTaskTimer(this, tyrantEffects, 10L, 10L);
|
||||||
|
getServer().getScheduler().runTaskTimer(this, optOutGlow, 10L, 10L);
|
||||||
getServer().getScheduler().runTaskTimer(
|
getServer().getScheduler().runTaskTimer(
|
||||||
this,
|
this,
|
||||||
new AbilityItemRefreshTask(
|
new AbilityItemRefreshTask(
|
||||||
stateManager, readiness, abilityItems, getServer(), clock
|
stateManager, readiness, abilityItems, capturedMobInventory,
|
||||||
|
getServer(), clock
|
||||||
),
|
),
|
||||||
20L,
|
20L,
|
||||||
20L
|
20L
|
||||||
@@ -181,6 +195,9 @@ public final class SpigotTyrantPlugin extends JavaPlugin {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDisable() {
|
public void onDisable() {
|
||||||
|
if (optOutGlow != null) {
|
||||||
|
optOutGlow.clearAll();
|
||||||
|
}
|
||||||
if (tyrantEffects != null) {
|
if (tyrantEffects != null) {
|
||||||
tyrantEffects.clearAll();
|
tyrantEffects.clearAll();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import org.bukkit.Location;
|
|||||||
import org.bukkit.Server;
|
import org.bukkit.Server;
|
||||||
import org.bukkit.entity.Entity;
|
import org.bukkit.entity.Entity;
|
||||||
import org.bukkit.entity.EntitySnapshot;
|
import org.bukkit.entity.EntitySnapshot;
|
||||||
|
import org.bukkit.entity.ItemFrame;
|
||||||
import org.bukkit.entity.LivingEntity;
|
import org.bukkit.entity.LivingEntity;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
import org.bukkit.entity.Tameable;
|
import org.bukkit.entity.Tameable;
|
||||||
@@ -159,6 +160,7 @@ public final class TamerListener implements Listener {
|
|||||||
if (capturedItems.isCapturedMob(event.getCurrentItem())
|
if (capturedItems.isCapturedMob(event.getCurrentItem())
|
||||||
|| capturedItems.isCapturedMob(event.getCursor())) {
|
|| capturedItems.isCapturedMob(event.getCursor())) {
|
||||||
if (!(event.getWhoClicked() instanceof Player player)
|
if (!(event.getWhoClicked() instanceof Player player)
|
||||||
|
|| event.isShiftClick()
|
||||||
|| event.getClickedInventory() == null
|
|| event.getClickedInventory() == null
|
||||||
|| !event.getClickedInventory().equals(player.getInventory())) {
|
|| !event.getClickedInventory().equals(player.getInventory())) {
|
||||||
event.setCancelled(true);
|
event.setCancelled(true);
|
||||||
@@ -166,6 +168,16 @@ public final class TamerListener implements Listener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
public void onFrame(PlayerInteractEntityEvent event) {
|
||||||
|
if (event.getRightClicked() instanceof ItemFrame
|
||||||
|
&& capturedItems.isCapturedMob(
|
||||||
|
event.getPlayer().getInventory().getItem(event.getHand())
|
||||||
|
)) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@EventHandler
|
@EventHandler
|
||||||
public void onDispense(BlockDispenseEvent event) {
|
public void onDispense(BlockDispenseEvent event) {
|
||||||
if (capturedItems.isCapturedMob(event.getItem())) {
|
if (capturedItems.isCapturedMob(event.getItem())) {
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
package games.dmg.spigottyrant;
|
||||||
|
|
||||||
|
import static org.mockito.Mockito.doReturn;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
import org.bukkit.ChatColor;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.Server;
|
||||||
|
import org.bukkit.World;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.scoreboard.Scoreboard;
|
||||||
|
import org.bukkit.scoreboard.ScoreboardManager;
|
||||||
|
import org.bukkit.scoreboard.Team;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
final class OptOutGlowControllerTest {
|
||||||
|
private static final UUID TYRANT = UUID.fromString(
|
||||||
|
"11111111-1111-1111-1111-111111111111"
|
||||||
|
);
|
||||||
|
private static final UUID OUT = UUID.fromString(
|
||||||
|
"22222222-2222-2222-2222-222222222222"
|
||||||
|
);
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void appliesGreenGlowAndRestoresPreviousTeamAfterLeavingRange() {
|
||||||
|
TyrantStateManager stateManager = mock(TyrantStateManager.class);
|
||||||
|
Server server = mock(Server.class);
|
||||||
|
PluginSettings settings = mock(PluginSettings.class);
|
||||||
|
ScoreboardManager manager = mock(ScoreboardManager.class);
|
||||||
|
Scoreboard scoreboard = mock(Scoreboard.class);
|
||||||
|
Team green = mock(Team.class);
|
||||||
|
Team previous = mock(Team.class);
|
||||||
|
Player tyrant = player(TYRANT, "Tyrant");
|
||||||
|
Player optedOut = player(OUT, "Out");
|
||||||
|
World world = mock(World.class);
|
||||||
|
Location tyrantLocation = mock(Location.class);
|
||||||
|
Location outLocation = mock(Location.class);
|
||||||
|
when(server.getScoreboardManager()).thenReturn(manager);
|
||||||
|
when(manager.getMainScoreboard()).thenReturn(scoreboard);
|
||||||
|
when(scoreboard.getTeam("tyrant-optout")).thenReturn(null);
|
||||||
|
when(scoreboard.registerNewTeam("tyrant-optout")).thenReturn(green);
|
||||||
|
when(scoreboard.getEntryTeam("Out")).thenReturn(previous);
|
||||||
|
when(server.getPlayer(TYRANT)).thenReturn(tyrant);
|
||||||
|
doReturn(Set.of(tyrant, optedOut)).when(server).getOnlinePlayers();
|
||||||
|
when(settings.tyrantRangeBlocks()).thenReturn(50.0);
|
||||||
|
when(tyrant.getWorld()).thenReturn(world);
|
||||||
|
when(optedOut.getWorld()).thenReturn(world);
|
||||||
|
when(tyrant.getLocation()).thenReturn(tyrantLocation);
|
||||||
|
when(optedOut.getLocation()).thenReturn(outLocation);
|
||||||
|
when(outLocation.distanceSquared(tyrantLocation)).thenReturn(25.0, 2601.0);
|
||||||
|
when(stateManager.game()).thenReturn(running());
|
||||||
|
when(stateManager.players()).thenReturn(Map.of(OUT, optedOut()));
|
||||||
|
OptOutGlowController controller = new OptOutGlowController(
|
||||||
|
stateManager, server, settings
|
||||||
|
);
|
||||||
|
|
||||||
|
controller.run();
|
||||||
|
controller.run();
|
||||||
|
|
||||||
|
verify(green).setColor(ChatColor.GREEN);
|
||||||
|
verify(green).addEntry("Out");
|
||||||
|
verify(optedOut).setGlowing(true);
|
||||||
|
verify(green).removeEntry("Out");
|
||||||
|
verify(previous).addEntry("Out");
|
||||||
|
verify(optedOut).setGlowing(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Player player(UUID id, String name) {
|
||||||
|
Player player = mock(Player.class);
|
||||||
|
when(player.getUniqueId()).thenReturn(id);
|
||||||
|
when(player.getName()).thenReturn(name);
|
||||||
|
return player;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PlayerState optedOut() {
|
||||||
|
return new PlayerState(
|
||||||
|
OUT, "Out", Optional.empty(), Optional.of(Instant.MAX), TyrantClass.NONE,
|
||||||
|
Optional.empty(), Map.of(), Set.of(), java.util.List.of()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GameState running() {
|
||||||
|
return new GameState(
|
||||||
|
GameLifecycle.RUNNING, Optional.of(TYRANT), Optional.empty(),
|
||||||
|
Optional.empty(), Optional.empty(), Optional.empty(), Duration.ZERO,
|
||||||
|
0, 0, Set.of()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package games.dmg.spigottyrant;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
final class OptOutGlowPolicyTest {
|
||||||
|
private static final UUID TYRANT = UUID.fromString(
|
||||||
|
"11111111-1111-1111-1111-111111111111"
|
||||||
|
);
|
||||||
|
private static final UUID PLAYER = UUID.fromString(
|
||||||
|
"22222222-2222-2222-2222-222222222222"
|
||||||
|
);
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void optedOutPlayerGlowsWithinInclusiveRunningRange() {
|
||||||
|
assertTrue(OptOutGlowPolicy.shouldGlow(
|
||||||
|
running(), optedOut(), true, true, 2500.0, 50.0
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void requiresRunningEventAvailablePlayersSameWorldAndRange() {
|
||||||
|
assertFalse(OptOutGlowPolicy.shouldGlow(
|
||||||
|
GameState.empty(), optedOut(), true, true, 1.0, 50.0
|
||||||
|
));
|
||||||
|
assertFalse(OptOutGlowPolicy.shouldGlow(
|
||||||
|
paused(), optedOut(), true, true, 1.0, 50.0
|
||||||
|
));
|
||||||
|
assertFalse(OptOutGlowPolicy.shouldGlow(
|
||||||
|
running(), optedIn(), true, true, 1.0, 50.0
|
||||||
|
));
|
||||||
|
assertFalse(OptOutGlowPolicy.shouldGlow(
|
||||||
|
running(), optedOut(), false, true, 1.0, 50.0
|
||||||
|
));
|
||||||
|
assertFalse(OptOutGlowPolicy.shouldGlow(
|
||||||
|
running(), optedOut(), true, false, 1.0, 50.0
|
||||||
|
));
|
||||||
|
assertFalse(OptOutGlowPolicy.shouldGlow(
|
||||||
|
running(), optedOut(), true, true, 2500.01, 50.0
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PlayerState optedOut() {
|
||||||
|
return new PlayerState(
|
||||||
|
PLAYER, "Player", Optional.empty(), Optional.of(Instant.MAX),
|
||||||
|
TyrantClass.NONE, Optional.empty(), Map.of(), Set.of(), java.util.List.of()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PlayerState optedIn() {
|
||||||
|
return PlayerState.newPlayer(PLAYER, "Player");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GameState running() {
|
||||||
|
return game(GameLifecycle.RUNNING, Optional.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GameState paused() {
|
||||||
|
return game(GameLifecycle.PAUSED, Optional.of(Instant.EPOCH));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GameState game(GameLifecycle lifecycle, Optional<Instant> pausedAt) {
|
||||||
|
return new GameState(
|
||||||
|
lifecycle, Optional.of(TYRANT), Optional.empty(), Optional.empty(),
|
||||||
|
Optional.empty(), pausedAt, Duration.ZERO, 0, 0, Set.of()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package games.dmg.spigottyrant;
|
||||||
|
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
import org.bukkit.ChatColor;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.entity.Projectile;
|
||||||
|
import org.bukkit.event.entity.EntityDamageByEntityEvent;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
final class ParticipationCombatListenerTest {
|
||||||
|
private static final UUID TYRANT = UUID.fromString(
|
||||||
|
"11111111-1111-1111-1111-111111111111"
|
||||||
|
);
|
||||||
|
private static final UUID OUT = UUID.fromString(
|
||||||
|
"22222222-2222-2222-2222-222222222222"
|
||||||
|
);
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void cancelsPlayerProjectileAndMessagesTheAttacker() {
|
||||||
|
TyrantStateManager manager = mock(TyrantStateManager.class);
|
||||||
|
Player attacker = player(TYRANT, "Tyrant");
|
||||||
|
Player target = player(OUT, "Out");
|
||||||
|
Projectile projectile = mock(Projectile.class);
|
||||||
|
EntityDamageByEntityEvent event = mock(EntityDamageByEntityEvent.class);
|
||||||
|
when(projectile.getShooter()).thenReturn(attacker);
|
||||||
|
when(event.getDamager()).thenReturn(projectile);
|
||||||
|
when(event.getEntity()).thenReturn(target);
|
||||||
|
when(manager.game()).thenReturn(running());
|
||||||
|
when(manager.players()).thenReturn(Map.of(
|
||||||
|
TYRANT, state(TYRANT, TyrantClass.NONE, false),
|
||||||
|
OUT, state(OUT, TyrantClass.NONE, true)
|
||||||
|
));
|
||||||
|
ParticipationCombatListener listener = new ParticipationCombatListener(
|
||||||
|
manager, new ParticipationCombatService()
|
||||||
|
);
|
||||||
|
|
||||||
|
listener.onDamage(event);
|
||||||
|
|
||||||
|
verify(event).setCancelled(true);
|
||||||
|
verify(attacker).sendMessage(ChatColor.YELLOW
|
||||||
|
+ ParticipationCombatListener.OPTED_OUT_MESSAGE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Player player(UUID id, String name) {
|
||||||
|
Player player = mock(Player.class);
|
||||||
|
when(player.getUniqueId()).thenReturn(id);
|
||||||
|
when(player.getName()).thenReturn(name);
|
||||||
|
return player;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PlayerState state(UUID id, TyrantClass role, boolean optedOut) {
|
||||||
|
return new PlayerState(
|
||||||
|
id, id.toString(), Optional.empty(),
|
||||||
|
optedOut ? Optional.of(Instant.parse("2026-08-21T00:00:00Z")) : Optional.empty(),
|
||||||
|
role, Optional.empty(), Map.of(), Set.of(), java.util.List.of()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GameState running() {
|
||||||
|
return new GameState(
|
||||||
|
GameLifecycle.RUNNING, Optional.of(TYRANT), Optional.empty(),
|
||||||
|
Optional.empty(), Optional.empty(), Optional.empty(), Duration.ZERO,
|
||||||
|
0, 0, Set.of()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
package games.dmg.spigottyrant;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
final class ParticipationCombatServiceTest {
|
||||||
|
private static final UUID TYRANT = UUID.fromString(
|
||||||
|
"11111111-1111-1111-1111-111111111111"
|
||||||
|
);
|
||||||
|
private static final UUID OUTSIDER = UUID.fromString(
|
||||||
|
"22222222-2222-2222-2222-222222222222"
|
||||||
|
);
|
||||||
|
private static final UUID SUPPORTER = UUID.fromString(
|
||||||
|
"33333333-3333-3333-3333-333333333333"
|
||||||
|
);
|
||||||
|
private final ParticipationCombatService service = new ParticipationCombatService();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void blocksDamageInBothDirectionsBetweenOptedOutPlayerAndTyrantSide() {
|
||||||
|
PlayerState optedOut = player(OUTSIDER, TyrantClass.NONE, true);
|
||||||
|
PlayerState tyrant = player(TYRANT, TyrantClass.NONE, false);
|
||||||
|
PlayerState supporter = player(SUPPORTER, TyrantClass.ASSASSIN, false);
|
||||||
|
|
||||||
|
assertTrue(service.blocks(running(), tyrant, optedOut));
|
||||||
|
assertTrue(service.blocks(running(), optedOut, tyrant));
|
||||||
|
assertTrue(service.blocks(running(), supporter, optedOut));
|
||||||
|
assertTrue(service.blocks(running(), optedOut, supporter));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void recognizesEveryTyrantSideClassAsSupport() {
|
||||||
|
PlayerState optedOut = player(OUTSIDER, TyrantClass.NONE, true);
|
||||||
|
|
||||||
|
assertTrue(service.blocks(
|
||||||
|
running(), player(SUPPORTER, TyrantClass.ASSASSIN, false), optedOut
|
||||||
|
));
|
||||||
|
assertTrue(service.blocks(
|
||||||
|
running(), player(SUPPORTER, TyrantClass.FIXER, false), optedOut
|
||||||
|
));
|
||||||
|
assertTrue(service.blocks(
|
||||||
|
running(), player(SUPPORTER, TyrantClass.TAMER, false), optedOut
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void leavesPausedUnstartedAndUnrelatedCombatAlone() {
|
||||||
|
PlayerState optedOut = player(OUTSIDER, TyrantClass.NONE, true);
|
||||||
|
PlayerState tyrant = player(TYRANT, TyrantClass.NONE, false);
|
||||||
|
PlayerState ordinary = player(SUPPORTER, TyrantClass.NONE, false);
|
||||||
|
|
||||||
|
assertFalse(service.blocks(paused(), tyrant, optedOut));
|
||||||
|
assertFalse(service.blocks(GameState.empty(), tyrant, optedOut));
|
||||||
|
assertFalse(service.blocks(running(), tyrant, ordinary));
|
||||||
|
assertFalse(service.blocks(running(), ordinary, optedOut));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PlayerState player(UUID id, TyrantClass role, boolean optedOut) {
|
||||||
|
return new PlayerState(
|
||||||
|
id, id.toString(), Optional.empty(),
|
||||||
|
optedOut ? Optional.of(Instant.parse("2026-08-21T00:00:00Z")) : Optional.empty(),
|
||||||
|
role, Optional.empty(), Map.of(), Set.of(), java.util.List.of()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GameState running() {
|
||||||
|
return game(GameLifecycle.RUNNING, Optional.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GameState paused() {
|
||||||
|
return game(
|
||||||
|
GameLifecycle.PAUSED,
|
||||||
|
Optional.of(Instant.parse("2026-08-14T00:00:00Z"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GameState game(GameLifecycle lifecycle, Optional<Instant> pausedAt) {
|
||||||
|
return new GameState(
|
||||||
|
lifecycle, Optional.of(TYRANT), Optional.empty(), Optional.empty(),
|
||||||
|
Optional.empty(), pausedAt, Duration.ZERO, 0, 0, Set.of()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package games.dmg.spigottyrant;
|
package games.dmg.spigottyrant;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
@@ -29,7 +30,8 @@ final class PlayerStatusMessagesTest {
|
|||||||
|
|
||||||
List<String> messages = PlayerStatusMessages.forJoin(game, player, NOW);
|
List<String> messages = PlayerStatusMessages.forJoin(game, player, NOW);
|
||||||
|
|
||||||
assertTrue(messages.stream().anyMatch(message -> message.contains("Game: RUNNING")));
|
assertFalse(messages.stream().anyMatch(message -> message.startsWith("Game:")));
|
||||||
|
assertFalse(messages.stream().anyMatch(message -> message.startsWith("Tyrant event:")));
|
||||||
assertTrue(messages.stream().anyMatch(message -> message.contains("Role: VIGILANTE")));
|
assertTrue(messages.stream().anyMatch(message -> message.contains("Role: VIGILANTE")));
|
||||||
assertTrue(messages.stream().anyMatch(message -> message.contains("Class: ASSASSIN")));
|
assertTrue(messages.stream().anyMatch(message -> message.contains("Class: ASSASSIN")));
|
||||||
assertTrue(messages.stream().anyMatch(message -> message.contains("ASSASSIN_INVISIBILITY")
|
assertTrue(messages.stream().anyMatch(message -> message.contains("ASSASSIN_INVISIBILITY")
|
||||||
@@ -42,6 +44,8 @@ final class PlayerStatusMessagesTest {
|
|||||||
GameState.empty(), PlayerState.newPlayer(PLAYER, "Player"), NOW
|
GameState.empty(), PlayerState.newPlayer(PLAYER, "Player"), NOW
|
||||||
);
|
);
|
||||||
|
|
||||||
assertTrue(messages.stream().anyMatch(message -> message.contains("/tyrant optout")));
|
assertTrue(messages.contains(
|
||||||
|
"Use /tyrant optout if you do not want to participate in Tyrant events."
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user