Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7318d4afa6 | ||
|
|
80810b3ba1 | ||
|
|
34f32cd32d | ||
|
|
0b202f588d | ||
|
|
ec7d50c2b5 |
@@ -6,6 +6,81 @@ description: Chronological record of material decisions affecting the Spigot Tyr
|
||||
|
||||
# Spigot Tyrant Design Log
|
||||
|
||||
## 2026-08-21 — Contextual command completion completed
|
||||
|
||||
- Completed US-018 with registered completers for `/tyrant`, `/vigilante`, and `/tyrantadmin`, replacing Bukkit's unconditional online-player fallback.
|
||||
- Suggestions now follow argument position, filter case-insensitively, expose eligible assignment and recruitment targets, identify current Followers for dismissal, and gate administration by permission.
|
||||
- Administrative completion remains available to permitted console senders, while player-only commands return no console suggestions.
|
||||
- Verified syntax, eligibility, membership, permissions, prefix filtering, compiler warnings, tests, and packaging with `./gradlew clean check jar`.
|
||||
|
||||
## 2026-08-21 — Contextual command completion started
|
||||
|
||||
- US-018 begins a test-first replacement of Bukkit's player-name fallback with syntax-aware Tyrant, Vigilante, and administrative command suggestions.
|
||||
- Player arguments will be filtered by the applicable assignment or membership rules, while administrative suggestions remain permission-gated.
|
||||
|
||||
## 2026-08-21 — Single-mob Tamer custody completed
|
||||
|
||||
- Completed US-007 with an authoritative one-mob custody limit that rejects a second capture before inventory, entity, or state mutation and tells the Tamer to release the held mob first.
|
||||
- Releasing or dropping custody permits another capture immediately, while durable state preserves the limit across logout, restart, recovery, and repeated interaction.
|
||||
- Legacy excess custody retains the oldest mob and safely releases extras; offline or failed releases remain durable for periodic retry.
|
||||
- Verified domain rejection, listener ordering, legacy reconciliation, compiler warnings, tests, and packaging with `./gradlew clean check jar`.
|
||||
|
||||
## 2026-08-21 — Single-mob Tamer custody started
|
||||
|
||||
- US-007 begins a test-first one-mob custody limit for active Tamers.
|
||||
- New captures will be rejected before mutation while custody exists, and legacy excess custody will be released safely with failed releases retained for retry.
|
||||
|
||||
## 2026-08-21 — Former Tamer automatic release completed
|
||||
|
||||
- Completed US-004 and US-007 so every captured mob is released automatically after its holder loses the Tamer class, regardless of the class-removal path.
|
||||
- Online former Tamers release at a validated safe location; offline, dead, unsafe, malformed, or otherwise unspawnable custody remains durable and retries periodically after login or movement.
|
||||
- Custody records and captured-mob items are removed only for successful spawns, while ordinary inventory remains untouched.
|
||||
- Verified former-Tamer filtering, offline deferral, partial success, custody preservation, compiler warnings, tests, and packaging with `./gradlew clean check jar`.
|
||||
|
||||
## 2026-08-21 — Former Tamer automatic release started
|
||||
|
||||
- US-004 and US-007 begin a test-first implementation that automatically releases captured mobs when a player loses the Tamer class.
|
||||
- Release will preserve ordinary inventory, remove custody only after successful spawning, and defer safely for offline players or unsafe locations before retrying.
|
||||
|
||||
## 2026-08-21 — Bound role control items completed
|
||||
|
||||
- Completed US-017 and the reopened US-015 and US-016 criteria with configurable owner-bound Tyrant and Vigilante control items that open their respective panels.
|
||||
- Role items are reconciled on login and periodically after role transitions, deduplicated, removed after role loss, and recoverable through `/tyrant item` or `/vigilante item` without dropping overflow.
|
||||
- Drop, pickup, container, drag, hopper, placement, dispenser, item-frame, death, stale-owner, and stale-role paths prevent transfer or remove invalid items.
|
||||
- Added default `Tyrant Control` Nether Star and `Vigilante Control` Compass settings and validation.
|
||||
- Verified role policy, delayed reconciliation, panel interaction, command recovery, settings defaults, compiler warnings, tests, and packaging with `./gradlew clean check jar`.
|
||||
|
||||
## 2026-08-21 — Bound role control item implementation started
|
||||
|
||||
- US-015, US-016, and US-017 begin a test-first implementation of configurable owner-bound Tyrant and Vigilante control items.
|
||||
- Delivery, command recovery, interaction, role validation, transfer prevention, role-loss cleanup, and duplicate resistance will build on the existing bound-item patterns.
|
||||
|
||||
## 2026-08-21 — Bound role control items planned
|
||||
|
||||
- Added US-017 for secure owner-bound Tyrant and Vigilante inventory items that open the corresponding control panels.
|
||||
- The Tyrant item defaults to a Nether Star named `Tyrant Control`, and the Vigilante item defaults to a Compass named `Vigilante Control`; both material and name are configurable.
|
||||
- Planned automatic role-assignment and login delivery, command recovery, full transfer prevention, removal on role loss or inventory departure, and duplicate resistance.
|
||||
- Reopened US-015 and US-016 in the backlog to track control-panel access through the new role items.
|
||||
|
||||
## 2026-08-21 — Vigilante control panel completed
|
||||
|
||||
- Completed US-016 and the reopened US-008 criteria with resistance overview, recruitment, Follower management, and confirmed role-relinquishment screens.
|
||||
- `/vigilante` and `/vigilante menu` open the panel, while existing invitation, acceptance, dismissal, leave, and relinquishment commands remain available.
|
||||
- Added a 100-tick login delay with online, lifecycle, role, and already-opened session checks so only the current Vigilante receives one automatic panel per login.
|
||||
- Pending invitations are visible and idempotent, existing Followers cannot be reinvited, and all menu mutations delegate to authoritative command and domain rules.
|
||||
- Verified command routing, authorization, overview state, invitation rules, delayed opening safeguards, compiler warnings, tests, and packaging with `./gradlew clean check jar`.
|
||||
|
||||
## 2026-08-21 — Vigilante control panel implementation started
|
||||
|
||||
- US-008 and US-016 begin a test-first implementation of the Vigilante control panel, delayed login opening, resistance overview, recruitment, dismissal, and role relinquishment.
|
||||
- The panel will reuse authoritative Follower and relinquishment rules while preserving all existing command alternatives.
|
||||
|
||||
## 2026-08-21 — Vigilante control panel planned
|
||||
|
||||
- Added US-016 for a graphical Vigilante control panel covering resistance overview, recruitment, Follower management, and confirmed role relinquishment while preserving command alternatives.
|
||||
- Planned automatic control-panel opening five seconds after the active Vigilante joins a running event, with lifecycle, role, disconnect, and duplicate-opening safeguards.
|
||||
- Reopened US-008 in the backlog to track the approved control-panel interactions and nearby-Follower combat contribution display.
|
||||
|
||||
## 2026-08-21 — Tyrant control panel completed
|
||||
|
||||
- Completed US-015 and the reopened US-003, US-004, and US-009 control-panel criteria with overview, unlock purchase confirmation, class assignment, and ability screens.
|
||||
|
||||
@@ -15,3 +15,6 @@
|
||||
13. [US-013: Build, test, and release the plugin](us-013-build-test-and-release.md)
|
||||
14. [US-014: Use class ability items](us-014-use-class-ability-items.md)
|
||||
15. [US-015: Manage a reign through the Tyrant control panel](us-015-manage-tyrant-control-panel.md)
|
||||
16. [US-016: Manage Followers through the Vigilante control panel](us-016-manage-vigilante-control-panel.md)
|
||||
17. [US-017: Use bound role control items](us-017-use-bound-role-control-items.md)
|
||||
18. [US-018: Complete Tyrant commands contextually](us-018-complete-commands-contextually.md)
|
||||
|
||||
@@ -22,6 +22,7 @@ As the **Tyrant**, I want to assign unlocked specialist classes so that I can bu
|
||||
- [x] The Tyrant can assign and reassign purchased classes through the Tyrant control panel.
|
||||
- [x] The control panel's player selector communicates eligibility and identifies the current class holder before confirmation.
|
||||
- [x] Control-panel and command-based assignments enforce identical rules.
|
||||
- [x] Removing or reassigning the Tamer class automatically releases the former holder's captured mobs while leaving ordinary inventory untouched.
|
||||
|
||||
## Related
|
||||
|
||||
|
||||
@@ -21,6 +21,23 @@ As the **Tamer**, I want to capture a mob and release it elsewhere so that I can
|
||||
- [x] Capture restrictions are configurable without permitting Ender Dragons.
|
||||
- [x] Invalid placement, full inventory, death, logout, restart, plugin disable, containers, and concurrent interaction cannot lose or duplicate a mob.
|
||||
- [x] Stored mob data is validated defensively before spawning and cannot execute untrusted serialized behavior.
|
||||
- [x] When a player loses the Tamer class, every captured mob in their custody is automatically released at the former Tamer's location.
|
||||
- [x] Ordinary inventory items are not dropped or changed during automatic release.
|
||||
- [x] Each captured-mob item and custody record is removed only after its mob spawns successfully.
|
||||
- [x] Multiple captured mobs are released without duplicating entities or custody records.
|
||||
- [x] If the former Tamer is offline or no safe release location is available, release is deferred without losing the captured mob.
|
||||
- [x] A deferred release is retried when the former Tamer next logs in or reaches a safe location.
|
||||
- [x] Tyrant death, class reassignment, opt-out, administration, and other Tamer-removal paths use the same release behavior.
|
||||
- [x] A Tamer can hold custody of at most one captured mob at a time.
|
||||
- [x] Attempting to capture another mob while one is held is rejected before changing either mob.
|
||||
- [x] The Tamer is told to release the currently captured mob before capturing another.
|
||||
- [x] Releasing or dropping the held mob immediately allows another capture.
|
||||
- [x] The one-mob limit survives logout, restart, item recovery, and repeated or concurrent interaction.
|
||||
- [x] If legacy state contains multiple captured mobs, extras are safely released when possible; custody is retained until release succeeds so no mob is lost.
|
||||
|
||||
## Validation
|
||||
|
||||
Automated tests verify that only former Tamers are processed, offline custody remains deferred, successful releases remove only their corresponding custody records, and failed releases remain available for retry. Additional tests verify pre-mutation rejection of a second capture, clear player guidance, and safe reduction of legacy excess custody. The complete `./gradlew clean check jar` lifecycle passes.
|
||||
|
||||
## Related
|
||||
|
||||
|
||||
@@ -23,8 +23,13 @@ As the **Vigilante**, I want nearby living Followers to strengthen me against th
|
||||
- [x] The deceased Vigilante is excluded when another eligible non-Tyrant candidate exists and is reused only when no such candidate exists.
|
||||
- [x] Followers are cleared rather than transferred when their Vigilante dies or loses the position.
|
||||
- [x] The Tyrant is never eligible to be selected as Vigilante.
|
||||
- [x] The Vigilante can review, invite, and dismiss Followers through the Vigilante control panel.
|
||||
- [x] Control-panel and command-based Follower actions enforce identical eligibility and membership rules.
|
||||
- [x] The control panel communicates the current nearby-Follower combat contribution and configured effect caps.
|
||||
- [x] Relinquishing through the control panel follows normal Vigilante succession and Follower cleanup.
|
||||
|
||||
## Related
|
||||
|
||||
- [Role and class model](../concepts/role-and-class-model.md)
|
||||
- [Handle inactivity and pending selections](us-010-handle-inactivity-and-selection.md)
|
||||
- [Manage Followers through the Vigilante control panel](us-016-manage-vigilante-control-panel.md)
|
||||
|
||||
@@ -31,6 +31,7 @@ As the **Tyrant**, I want a control panel for my reign so that I can inspect pro
|
||||
- [x] Stale, repeated, or concurrent panel interactions cannot duplicate purchases, bypass cooldowns, or create conflicting assignments.
|
||||
- [x] Panel actions become unavailable immediately if the game pauses, the reign ends, or the player is no longer the Tyrant.
|
||||
- [x] Existing `/tyrant choices`, `/tyrant buy`, `/tyrant assign`, and `/tyrant intelligence` commands remain available as alternatives.
|
||||
- [x] The active Tyrant can open the control panel by using the bound Tyrant control item.
|
||||
|
||||
## Validation
|
||||
|
||||
@@ -42,3 +43,4 @@ Automated tests verify command entry points, active-Tyrant authorization, author
|
||||
- [Assign unlocked classes](us-004-assign-unlocked-classes.md)
|
||||
- [Purchase and use Tyrant abilities](us-009-use-tyrant-abilities.md)
|
||||
- [Inform players and manage participation](us-011-inform-and-manage-participation.md)
|
||||
- [Use bound role control items](us-017-use-bound-role-control-items.md)
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
---
|
||||
type: User Story
|
||||
title: "US-016: Manage Followers through the Vigilante control panel"
|
||||
description: Give the active Vigilante a graphical control panel for reviewing and managing the resistance.
|
||||
status: done
|
||||
---
|
||||
|
||||
# US-016: Manage Followers through the Vigilante control panel
|
||||
|
||||
As the **Vigilante**, I want a control panel for managing Followers so that I can coordinate the resistance without memorizing commands.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [x] The active Vigilante can open the control panel with `/vigilante menu`, and `/vigilante` with no arguments opens the same panel.
|
||||
- [x] A player who is not the active Vigilante cannot open or operate the control panel.
|
||||
- [x] The control panel opens automatically five seconds after the active Vigilante joins a running event.
|
||||
- [x] Automatic opening is cancelled if the player disconnects, loses the Vigilante role, or the event pauses or stops during the five-second delay.
|
||||
- [x] Pending Vigilante candidates and other players do not receive the automatically opened control panel.
|
||||
- [x] Manually opening the control panel during the delay prevents a duplicate automatic opening.
|
||||
- [x] Closing the automatically opened control panel does not cause it to reopen during the same login session.
|
||||
- [x] The overview shows current Followers, pending invitations, and the number of nearby Followers currently eligible to contribute combat effects.
|
||||
- [x] Each Follower's online, living, same-world, proximity, and participation status is communicated without exposing information unavailable to the Vigilante.
|
||||
- [x] The control panel shows the current Strength and Resistance contribution and configured caps.
|
||||
- [x] The Vigilante can navigate between overview, recruitment, Follower-management, and role-control screens.
|
||||
- [x] The recruitment screen lists eligible online players and explains why opted-out, Tyrant, Vigilante, or otherwise ineligible players cannot be invited.
|
||||
- [x] The Vigilante can invite an eligible player after confirmation.
|
||||
- [x] Pending outgoing invitations are identified and cannot be duplicated through repeated or concurrent interaction.
|
||||
- [x] Invited players receive the existing invitation notice and may continue using `/vigilante accept`.
|
||||
- [x] The Follower-management screen identifies current Followers and requires confirmation before dismissal.
|
||||
- [x] Successful invitations and dismissals refresh the control panel from authoritative current state and notify affected players.
|
||||
- [x] The role-control screen allows the Vigilante to relinquish the role only after explicit confirmation.
|
||||
- [x] Relinquishing through the control panel uses normal Vigilante replacement and Follower-cleanup rules.
|
||||
- [x] Every action revalidates the running lifecycle and active Vigilante before changing state.
|
||||
- [x] Stale or repeated control-panel interactions cannot invite an ineligible player, duplicate an invitation, or dismiss the wrong Follower.
|
||||
- [x] Existing `/vigilante invite`, `/vigilante accept`, `/vigilante dismiss`, `/vigilante leave`, and `/tyrant relinquish confirm` commands remain available.
|
||||
- [x] The active Vigilante can open the control panel by using the bound Vigilante control item.
|
||||
|
||||
## Validation
|
||||
|
||||
Automated tests verify default and explicit command entry points, active-Vigilante authorization, overview state, invitation deduplication and eligibility, the exact 100-tick login delay, lifecycle cancellation, and duplicate-opening prevention. The complete `./gradlew clean check jar` lifecycle passes.
|
||||
|
||||
## Related
|
||||
|
||||
- [Support the Vigilante and Followers](us-008-support-vigilante-and-followers.md)
|
||||
- [Handle inactivity and pending selections](us-010-handle-inactivity-and-selection.md)
|
||||
- [Manage a reign through the Tyrant control panel](us-015-manage-tyrant-control-panel.md)
|
||||
- [Role and class model](../concepts/role-and-class-model.md)
|
||||
- [Use bound role control items](us-017-use-bound-role-control-items.md)
|
||||
@@ -0,0 +1,43 @@
|
||||
---
|
||||
type: User Story
|
||||
title: "US-017: Use bound role control items"
|
||||
description: Give the active Tyrant and Vigilante secure inventory items that open their control panels.
|
||||
status: done
|
||||
---
|
||||
|
||||
# US-017: Use bound role control items
|
||||
|
||||
As a **Tyrant or Vigilante**, I want a bound role item that opens my control panel so that I can access role controls without typing a command.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [x] A player receives the appropriate control item when becoming the active Tyrant or Vigilante.
|
||||
- [x] The Tyrant control item opens the Tyrant control panel when used.
|
||||
- [x] The Vigilante control item opens the Vigilante control panel when used.
|
||||
- [x] The Tyrant control item defaults to a Nether Star named `Tyrant Control`.
|
||||
- [x] The Vigilante control item defaults to a Compass named `Vigilante Control`.
|
||||
- [x] Each item's material and name are configurable, and hidden metadata identifies its owner, associated role, and the plugin.
|
||||
- [x] Only the current role holder identified by the item's metadata can use the item.
|
||||
- [x] A former role holder or another player cannot use or retain the item.
|
||||
- [x] `/tyrant item` restores a missing Tyrant control item in addition to existing eligible class items.
|
||||
- [x] `/vigilante item` restores a missing Vigilante control item.
|
||||
- [x] Recovery commands cannot create duplicate control items.
|
||||
- [x] Using a recovery command while the inventory is full does not drop the item and explains how to retry.
|
||||
- [x] A role control item cannot be placed, crafted with, stored, framed, dispensed, transferred, or moved into another inventory.
|
||||
- [x] Dropping a role control item removes it immediately rather than creating a transferable dropped item.
|
||||
- [x] If a role control item leaves its owner's inventory during death or another inventory event, it is removed and may be recovered after respawn.
|
||||
- [x] A missing role control item is restored automatically on login or role assignment when inventory space is available.
|
||||
- [x] Every copy is removed immediately when the player loses the associated role.
|
||||
- [x] Repeated commands, role transitions, death, logout, restart, and concurrent inventory events cannot duplicate role control items.
|
||||
- [x] Existing `/tyrant`, `/tyrant menu`, `/vigilante`, and `/vigilante menu` control-panel access remains available.
|
||||
|
||||
## Validation
|
||||
|
||||
Automated tests verify role eligibility, paused-role retention, reconciliation and recovery, panel activation, role-specific commands, and configuration defaults. Inventory listeners cover transfer and departure paths, and the complete `./gradlew clean check jar` lifecycle passes.
|
||||
|
||||
## Related
|
||||
|
||||
- [Manage a reign through the Tyrant control panel](us-015-manage-tyrant-control-panel.md)
|
||||
- [Manage Followers through the Vigilante control panel](us-016-manage-vigilante-control-panel.md)
|
||||
- [Use class ability items](us-014-use-class-ability-items.md)
|
||||
- [Configure and persist game state](us-012-configure-and-persist-state.md)
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
type: User Story
|
||||
title: "US-018: Complete Tyrant commands contextually"
|
||||
description: Replace Bukkit's player-name fallback with valid context-aware command suggestions.
|
||||
status: done
|
||||
---
|
||||
|
||||
# US-018: Complete Tyrant commands contextually
|
||||
|
||||
As a **player or administrator**, I want contextual command suggestions so that I can discover valid Tyrant commands and arguments.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [x] `/tyrant` suggests its available subcommands instead of arbitrary player names.
|
||||
- [x] `/tyrant buy` suggests Tyrant unlock names.
|
||||
- [x] `/tyrant assign` suggests `assassin`, `fixer`, and `tamer`, followed by eligible online players.
|
||||
- [x] `/tyrant relinquish` suggests `confirm`.
|
||||
- [x] `/vigilante` suggests its available subcommands.
|
||||
- [x] `/vigilante invite` suggests eligible online recruitment candidates.
|
||||
- [x] `/vigilante dismiss` suggests current online Followers.
|
||||
- [x] `/tyrantadmin` suggestions are visible only to senders with `spigottyrant.admin`.
|
||||
- [x] `/tyrantadmin start` suggests online Tyrant and Vigilante candidates.
|
||||
- [x] `/tyrantadmin reset` suggests `confirm`.
|
||||
- [x] Suggestions are filtered case-insensitively by the partially typed argument.
|
||||
- [x] Suggestions never include syntactically invalid options for the current argument position.
|
||||
- [x] Console completion works for administrative commands without exposing player-only commands as executable console actions.
|
||||
- [x] Existing command execution behavior remains unchanged.
|
||||
|
||||
## Validation
|
||||
|
||||
Automated tests verify root syntax, unlocks, classes, confirmations, eligible recruits, current Followers, permission gating, online administrative candidates, argument positions, and case-insensitive prefix filtering. The complete `./gradlew clean check jar` lifecycle passes.
|
||||
|
||||
## Related
|
||||
|
||||
- [Start, pause, and administer the game](us-001-start-pause-and-administer.md)
|
||||
- [Assign unlocked classes](us-004-assign-unlocked-classes.md)
|
||||
- [Support the Vigilante and Followers](us-008-support-vigilante-and-followers.md)
|
||||
- [Manage a reign through the Tyrant control panel](us-015-manage-tyrant-control-panel.md)
|
||||
- [Manage Followers through the Vigilante control panel](us-016-manage-vigilante-control-panel.md)
|
||||
@@ -0,0 +1,47 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Server;
|
||||
import org.bukkit.entity.EntitySnapshot;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
public final class BukkitCapturedMobSpawner implements CapturedMobSpawner {
|
||||
private final Server server;
|
||||
private final PluginSettings settings;
|
||||
|
||||
public BukkitCapturedMobSpawner(Server server, PluginSettings settings) {
|
||||
this.server = server;
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean spawn(Player player, CapturedMob mob) {
|
||||
Location location = player.getLocation();
|
||||
if (!safe(location)
|
||||
|| "ENDER_DRAGON".equals(mob.entityType())
|
||||
|| settings.deniedMobTypes().contains(mob.entityType())) {
|
||||
return false;
|
||||
}
|
||||
String serialized = mob.data().get("snapshot");
|
||||
if (serialized == null || serialized.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
EntitySnapshot snapshot = server.getEntityFactory().createEntitySnapshot(serialized);
|
||||
if (!snapshot.getEntityType().name().equals(mob.entityType())) {
|
||||
return false;
|
||||
}
|
||||
snapshot.createEntity(location);
|
||||
return true;
|
||||
} catch (IllegalArgumentException | IllegalStateException exception) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean safe(Location location) {
|
||||
return location.getWorld() != null
|
||||
&& location.getBlock().isPassable()
|
||||
&& location.clone().add(0.0, 1.0, 0.0).getBlock().isPassable()
|
||||
&& location.getWorld().getWorldBorder().isInside(location);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import org.bukkit.Server;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
public final class BukkitFollowerPresenceProvider implements FollowerPresenceProvider {
|
||||
private final Server server;
|
||||
private final double rangeSquared;
|
||||
|
||||
public BukkitFollowerPresenceProvider(Server server, double rangeBlocks) {
|
||||
this.server = server;
|
||||
this.rangeSquared = rangeBlocks * rangeBlocks;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<UUID, FollowerPresence> inspect(
|
||||
Player vigilante,
|
||||
Map<UUID, PlayerState> players
|
||||
) {
|
||||
Map<UUID, FollowerPresence> result = new HashMap<>();
|
||||
for (PlayerState state : players.values()) {
|
||||
Player follower = server.getPlayer(state.playerId());
|
||||
boolean online = follower != null && follower.isOnline();
|
||||
boolean living = online && !follower.isDead();
|
||||
boolean sameWorld = living && follower.getWorld().equals(vigilante.getWorld());
|
||||
boolean withinRange = sameWorld && follower.getLocation()
|
||||
.distanceSquared(vigilante.getLocation()) <= rangeSquared;
|
||||
result.put(
|
||||
state.playerId(),
|
||||
new FollowerPresence(online, living, sameWorld, withinRange)
|
||||
);
|
||||
}
|
||||
return Map.copyOf(result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
import org.bukkit.persistence.PersistentDataType;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
|
||||
public final class BukkitRoleControlItemService implements RoleControlItemService {
|
||||
private final NamespacedKey roleKey;
|
||||
private final NamespacedKey ownerKey;
|
||||
private final PluginSettings settings;
|
||||
|
||||
public BukkitRoleControlItemService(Plugin plugin, PluginSettings settings) {
|
||||
roleKey = new NamespacedKey(plugin, "role-control");
|
||||
ownerKey = new NamespacedKey(plugin, "role-control-owner");
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void recover(Player player, GameState game) {
|
||||
Optional<RoleControl> required = RoleControlItemPolicy.requiredRole(
|
||||
game, player.getUniqueId()
|
||||
);
|
||||
if (required.isEmpty() || contains(player, required.orElseThrow())) {
|
||||
return;
|
||||
}
|
||||
Map<Integer, ItemStack> leftovers = player.getInventory().addItem(
|
||||
create(player.getUniqueId(), required.orElseThrow())
|
||||
);
|
||||
if (!leftovers.isEmpty()) {
|
||||
String command = required.orElseThrow() == RoleControl.TYRANT
|
||||
? "/tyrant item" : "/vigilante item";
|
||||
player.sendMessage(ChatColor.RED
|
||||
+ "Your inventory is full. Make room and use " + command + ".");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isBoundRoleControlItem(ItemStack item) {
|
||||
return role(item).isPresent() && owner(item).isPresent();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<RoleControl> role(ItemStack item) {
|
||||
String value = metadata(item, roleKey);
|
||||
if (value == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
try {
|
||||
return Optional.of(RoleControl.valueOf(value));
|
||||
} catch (IllegalArgumentException exception) {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<UUID> owner(ItemStack item) {
|
||||
String value = metadata(item, ownerKey);
|
||||
if (value == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
try {
|
||||
return Optional.of(UUID.fromString(value));
|
||||
} catch (IllegalArgumentException exception) {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeInvalid(Player player, GameState game) {
|
||||
Optional<RoleControl> required = RoleControlItemPolicy.requiredRole(
|
||||
game, player.getUniqueId()
|
||||
);
|
||||
boolean retained = false;
|
||||
ItemStack[] contents = player.getInventory().getContents();
|
||||
for (int index = 0; index < contents.length; index++) {
|
||||
ItemStack item = contents[index];
|
||||
if (!isBoundRoleControlItem(item)) {
|
||||
continue;
|
||||
}
|
||||
boolean valid = !retained
|
||||
&& owner(item).filter(player.getUniqueId()::equals).isPresent()
|
||||
&& role(item).equals(required);
|
||||
if (valid) {
|
||||
retained = true;
|
||||
} else {
|
||||
player.getInventory().setItem(index, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeAll(Player player) {
|
||||
ItemStack[] contents = player.getInventory().getContents();
|
||||
for (int index = 0; index < contents.length; index++) {
|
||||
if (isBoundRoleControlItem(contents[index])) {
|
||||
player.getInventory().setItem(index, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private ItemStack create(UUID ownerId, RoleControl role) {
|
||||
PluginSettings.AbilityItemSettings configured = role == RoleControl.TYRANT
|
||||
? settings.tyrantControlItem() : settings.vigilanteControlItem();
|
||||
Material material = Material.matchMaterial(configured.material());
|
||||
if (material == null) {
|
||||
throw new IllegalStateException("Configured role control material is unavailable");
|
||||
}
|
||||
ItemStack item = new ItemStack(material);
|
||||
ItemMeta meta = item.getItemMeta();
|
||||
meta.setDisplayName(ChatColor.GOLD + configured.name());
|
||||
meta.getPersistentDataContainer().set(
|
||||
roleKey, PersistentDataType.STRING, role.name()
|
||||
);
|
||||
meta.getPersistentDataContainer().set(
|
||||
ownerKey, PersistentDataType.STRING, ownerId.toString()
|
||||
);
|
||||
item.setItemMeta(meta);
|
||||
return item;
|
||||
}
|
||||
|
||||
private boolean contains(Player player, RoleControl role) {
|
||||
for (ItemStack item : player.getInventory().getContents()) {
|
||||
if (owner(item).filter(player.getUniqueId()::equals).isPresent()
|
||||
&& role(item).filter(role::equals).isPresent()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static String metadata(ItemStack item, NamespacedKey key) {
|
||||
if (item == null || !item.hasItemMeta()) {
|
||||
return null;
|
||||
}
|
||||
return item.getItemMeta().getPersistentDataContainer()
|
||||
.get(key, PersistentDataType.STRING);
|
||||
}
|
||||
}
|
||||
@@ -255,7 +255,9 @@ public final class BukkitTyrantControlPanelRenderer
|
||||
return;
|
||||
}
|
||||
try {
|
||||
openAssignmentConfirmation(player, tyrantClass, UUID.fromString(lore.get(1)));
|
||||
openAssignmentConfirmation(
|
||||
player, tyrantClass, UUID.fromString(ChatColor.stripColor(lore.get(1)))
|
||||
);
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
player.sendMessage(ChatColor.RED + "That player selection is no longer valid.");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,409 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.Server;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.InventoryHolder;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
|
||||
public final class BukkitVigilanteControlPanelRenderer
|
||||
implements VigilanteControlPanelRenderer, Listener {
|
||||
private static final String TITLE = ChatColor.DARK_BLUE + "Vigilante Control Panel";
|
||||
private final Server server;
|
||||
private final TyrantStateManager stateManager;
|
||||
private final FollowerService followers;
|
||||
private final FollowerPresenceProvider presenceProvider;
|
||||
private final int strengthCap;
|
||||
private final int resistanceCap;
|
||||
|
||||
public BukkitVigilanteControlPanelRenderer(
|
||||
Server server,
|
||||
TyrantStateManager stateManager,
|
||||
FollowerService followers,
|
||||
FollowerPresenceProvider presenceProvider,
|
||||
int strengthCap,
|
||||
int resistanceCap
|
||||
) {
|
||||
this.server = server;
|
||||
this.stateManager = stateManager;
|
||||
this.followers = followers;
|
||||
this.presenceProvider = presenceProvider;
|
||||
this.strengthCap = strengthCap;
|
||||
this.resistanceCap = resistanceCap;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void openOverview(Player player, VigilanteControlPanelModel model) {
|
||||
MenuHolder holder = new MenuHolder(MenuView.OVERVIEW, null);
|
||||
Inventory inventory = create(holder, 27, TITLE);
|
||||
inventory.setItem(10, item(Material.SHIELD, ChatColor.BLUE + "Resistance Overview",
|
||||
List.of(
|
||||
"Followers: " + model.followers().size(),
|
||||
"Pending invitations: " + model.pendingInvitations().size(),
|
||||
"Nearby contributors: " + model.nearbyEligibleFollowers(),
|
||||
"Strength: " + model.strengthLevel() + "/" + model.strengthCap(),
|
||||
"Resistance: " + model.resistanceLevel() + "/" + model.resistanceCap()
|
||||
)));
|
||||
inventory.setItem(12, item(Material.WRITABLE_BOOK, ChatColor.GREEN + "Recruitment",
|
||||
List.of("Invite eligible online players.")));
|
||||
inventory.setItem(14, item(Material.PLAYER_HEAD, ChatColor.AQUA + "Manage Followers",
|
||||
followerSummary(model)));
|
||||
inventory.setItem(16, item(Material.REDSTONE_TORCH, ChatColor.RED + "Role Control",
|
||||
List.of("Relinquish the Vigilante role.")));
|
||||
player.openInventory(inventory);
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onClick(InventoryClickEvent event) {
|
||||
if (!(event.getWhoClicked() instanceof Player player)
|
||||
|| !(event.getView().getTopInventory().getHolder() instanceof MenuHolder holder)) {
|
||||
return;
|
||||
}
|
||||
event.setCancelled(true);
|
||||
if (event.getRawSlot() < 0
|
||||
|| event.getRawSlot() >= event.getView().getTopInventory().getSize()) {
|
||||
return;
|
||||
}
|
||||
if (!isActiveVigilante(player)) {
|
||||
player.closeInventory();
|
||||
player.sendMessage(ChatColor.RED
|
||||
+ "This Vigilante control panel is no longer active.");
|
||||
return;
|
||||
}
|
||||
switch (holder.view()) {
|
||||
case OVERVIEW -> clickOverview(player, event.getRawSlot());
|
||||
case RECRUITMENT -> clickRecruitment(player, event.getRawSlot());
|
||||
case INVITE_CONFIRMATION -> clickInviteConfirmation(
|
||||
player, event.getRawSlot(), holder.targetId()
|
||||
);
|
||||
case FOLLOWERS -> clickFollowers(player, event.getRawSlot());
|
||||
case DISMISS_CONFIRMATION -> clickDismissConfirmation(
|
||||
player, event.getRawSlot(), holder.targetId()
|
||||
);
|
||||
case ROLE -> clickRole(player, event.getRawSlot());
|
||||
case RELINQUISH_CONFIRMATION -> clickRelinquishConfirmation(
|
||||
player, event.getRawSlot()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private void clickOverview(Player player, int slot) {
|
||||
if (slot == 12) {
|
||||
openRecruitment(player);
|
||||
} else if (slot == 14) {
|
||||
openFollowers(player);
|
||||
} else if (slot == 16) {
|
||||
openRole(player);
|
||||
}
|
||||
}
|
||||
|
||||
private void openRecruitment(Player player) {
|
||||
MenuHolder holder = new MenuHolder(MenuView.RECRUITMENT, null);
|
||||
Inventory inventory = create(holder, 54, TITLE + " — Recruitment");
|
||||
Set<UUID> pending = followers.invitedPlayerIds(player.getUniqueId());
|
||||
int slot = 0;
|
||||
for (Player candidate : server.getOnlinePlayers()) {
|
||||
if (slot >= 45 || candidate.getUniqueId().equals(player.getUniqueId())) {
|
||||
continue;
|
||||
}
|
||||
PlayerState state = stateManager.player(candidate.getUniqueId(), candidate.getName());
|
||||
String eligibility = recruitmentEligibility(candidate.getUniqueId(), state, pending);
|
||||
inventory.setItem(slot, playerItem(candidate.getName(), candidate.getUniqueId(),
|
||||
eligibility));
|
||||
slot++;
|
||||
}
|
||||
inventory.setItem(49, backItem());
|
||||
player.openInventory(inventory);
|
||||
}
|
||||
|
||||
private String recruitmentEligibility(
|
||||
UUID candidateId,
|
||||
PlayerState state,
|
||||
Set<UUID> pending
|
||||
) {
|
||||
GameState game = stateManager.game();
|
||||
if (pending.contains(candidateId)) {
|
||||
return ChatColor.YELLOW + "Pending invitation";
|
||||
}
|
||||
if (state.optedOutUntil().isPresent()) {
|
||||
return ChatColor.RED + "Ineligible: opted out";
|
||||
}
|
||||
if (game.tyrantId().filter(candidateId::equals).isPresent()) {
|
||||
return ChatColor.RED + "Ineligible: Tyrant";
|
||||
}
|
||||
if (game.vigilanteId().filter(candidateId::equals).isPresent()) {
|
||||
return ChatColor.RED + "Ineligible: Vigilante";
|
||||
}
|
||||
if (state.followerOf().isPresent()) {
|
||||
return ChatColor.RED + "Ineligible: already a Follower";
|
||||
}
|
||||
return ChatColor.GREEN + "Eligible";
|
||||
}
|
||||
|
||||
private void clickRecruitment(Player player, int slot) {
|
||||
if (slot == 49) {
|
||||
refresh(player);
|
||||
return;
|
||||
}
|
||||
selectedPlayer(player, slot, "Eligible")
|
||||
.ifPresent(targetId -> openInviteConfirmation(player, targetId));
|
||||
}
|
||||
|
||||
private void openInviteConfirmation(Player player, UUID targetId) {
|
||||
Player target = server.getPlayer(targetId);
|
||||
if (target == null) {
|
||||
player.sendMessage(ChatColor.RED + "That player is no longer online.");
|
||||
openRecruitment(player);
|
||||
return;
|
||||
}
|
||||
MenuHolder holder = new MenuHolder(MenuView.INVITE_CONFIRMATION, targetId);
|
||||
Inventory inventory = create(holder, 27, TITLE + " — Confirm Invitation");
|
||||
inventory.setItem(11, cancelItem());
|
||||
inventory.setItem(13, item(Material.WRITABLE_BOOK, ChatColor.YELLOW + target.getName(),
|
||||
List.of("Invite this player to become a Follower?")));
|
||||
inventory.setItem(15, confirmItem());
|
||||
player.openInventory(inventory);
|
||||
}
|
||||
|
||||
private void clickInviteConfirmation(Player player, int slot, UUID targetId) {
|
||||
if (slot == 11) {
|
||||
openRecruitment(player);
|
||||
} else if (slot == 15 && targetId != null) {
|
||||
Player target = server.getPlayer(targetId);
|
||||
if (target == null) {
|
||||
player.sendMessage(ChatColor.RED + "That player is no longer online.");
|
||||
} else {
|
||||
player.performCommand("vigilante invite " + target.getName());
|
||||
}
|
||||
openRecruitment(player);
|
||||
}
|
||||
}
|
||||
|
||||
private void openFollowers(Player player) {
|
||||
VigilanteControlPanelModel model = model(player);
|
||||
MenuHolder holder = new MenuHolder(MenuView.FOLLOWERS, null);
|
||||
Inventory inventory = create(holder, 54, TITLE + " — Followers");
|
||||
int slot = 0;
|
||||
for (VigilanteFollowerView follower : model.followers()) {
|
||||
if (slot >= 45) {
|
||||
break;
|
||||
}
|
||||
FollowerPresence presence = follower.presence();
|
||||
List<String> status = List.of(
|
||||
presence.online() ? "Online" : "Offline",
|
||||
presence.living() ? "Living" : "Dead or unavailable",
|
||||
presence.sameWorld() ? "Same world" : "Different world or unavailable",
|
||||
presence.withinRange() ? "Within range" : "Outside range or unavailable",
|
||||
follower.optedOut() ? "Opted out" : "Participating",
|
||||
presence.online() ? ChatColor.GREEN + "Click to dismiss."
|
||||
: ChatColor.RED + "Must be online to dismiss."
|
||||
);
|
||||
inventory.setItem(slot, playerItem(
|
||||
follower.name(), follower.playerId(), status.toString()
|
||||
));
|
||||
slot++;
|
||||
}
|
||||
inventory.setItem(49, backItem());
|
||||
player.openInventory(inventory);
|
||||
}
|
||||
|
||||
private void clickFollowers(Player player, int slot) {
|
||||
if (slot == 49) {
|
||||
refresh(player);
|
||||
return;
|
||||
}
|
||||
selectedPlayer(player, slot, "Online")
|
||||
.filter(targetId -> server.getPlayer(targetId) != null)
|
||||
.ifPresent(targetId -> openDismissConfirmation(player, targetId));
|
||||
}
|
||||
|
||||
private void openDismissConfirmation(Player player, UUID targetId) {
|
||||
Player target = server.getPlayer(targetId);
|
||||
if (target == null) {
|
||||
player.sendMessage(ChatColor.RED + "That Follower is no longer online.");
|
||||
openFollowers(player);
|
||||
return;
|
||||
}
|
||||
MenuHolder holder = new MenuHolder(MenuView.DISMISS_CONFIRMATION, targetId);
|
||||
Inventory inventory = create(holder, 27, TITLE + " — Confirm Dismissal");
|
||||
inventory.setItem(11, cancelItem());
|
||||
inventory.setItem(13, item(Material.PLAYER_HEAD, ChatColor.YELLOW + target.getName(),
|
||||
List.of("Dismiss this Follower?")));
|
||||
inventory.setItem(15, confirmItem());
|
||||
player.openInventory(inventory);
|
||||
}
|
||||
|
||||
private void clickDismissConfirmation(Player player, int slot, UUID targetId) {
|
||||
if (slot == 11) {
|
||||
openFollowers(player);
|
||||
} else if (slot == 15 && targetId != null) {
|
||||
Player target = server.getPlayer(targetId);
|
||||
if (target == null) {
|
||||
player.sendMessage(ChatColor.RED + "That Follower is no longer online.");
|
||||
} else {
|
||||
player.performCommand("vigilante dismiss " + target.getName());
|
||||
}
|
||||
openFollowers(player);
|
||||
}
|
||||
}
|
||||
|
||||
private void openRole(Player player) {
|
||||
MenuHolder holder = new MenuHolder(MenuView.ROLE, null);
|
||||
Inventory inventory = create(holder, 27, TITLE + " — Role Control");
|
||||
inventory.setItem(13, item(Material.REDSTONE_TORCH,
|
||||
ChatColor.RED + "Relinquish Vigilante Role",
|
||||
List.of("Followers will be cleared and a replacement selected.",
|
||||
"Click to review confirmation.")));
|
||||
inventory.setItem(22, backItem());
|
||||
player.openInventory(inventory);
|
||||
}
|
||||
|
||||
private void clickRole(Player player, int slot) {
|
||||
if (slot == 22) {
|
||||
refresh(player);
|
||||
} else if (slot == 13) {
|
||||
MenuHolder holder = new MenuHolder(MenuView.RELINQUISH_CONFIRMATION, null);
|
||||
Inventory inventory = create(holder, 27, TITLE + " — Confirm Relinquishment");
|
||||
inventory.setItem(11, cancelItem());
|
||||
inventory.setItem(13, item(Material.BARRIER, ChatColor.RED + "Relinquish Role",
|
||||
List.of("This clears all Followers and rerolls the Vigilante.")));
|
||||
inventory.setItem(15, confirmItem());
|
||||
player.openInventory(inventory);
|
||||
}
|
||||
}
|
||||
|
||||
private void clickRelinquishConfirmation(Player player, int slot) {
|
||||
if (slot == 11) {
|
||||
openRole(player);
|
||||
} else if (slot == 15) {
|
||||
player.closeInventory();
|
||||
player.performCommand("tyrant relinquish confirm");
|
||||
}
|
||||
}
|
||||
|
||||
private java.util.Optional<UUID> selectedPlayer(
|
||||
Player player,
|
||||
int slot,
|
||||
String requiredStatus
|
||||
) {
|
||||
ItemStack clicked = player.getOpenInventory().getTopInventory().getItem(slot);
|
||||
if (clicked == null || clicked.getType() != Material.PLAYER_HEAD) {
|
||||
return java.util.Optional.empty();
|
||||
}
|
||||
List<String> lore = clicked.getItemMeta().getLore();
|
||||
if (lore == null || lore.size() < 2
|
||||
|| !ChatColor.stripColor(lore.get(0)).contains(requiredStatus)) {
|
||||
return java.util.Optional.empty();
|
||||
}
|
||||
try {
|
||||
return java.util.Optional.of(UUID.fromString(
|
||||
ChatColor.stripColor(lore.get(lore.size() - 1))
|
||||
));
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
player.sendMessage(ChatColor.RED + "That player selection is no longer valid.");
|
||||
return java.util.Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isActiveVigilante(Player player) {
|
||||
GameState game = stateManager.game();
|
||||
return game.lifecycle() == GameLifecycle.RUNNING
|
||||
&& game.vigilanteId().filter(player.getUniqueId()::equals).isPresent();
|
||||
}
|
||||
|
||||
private VigilanteControlPanelModel model(Player player) {
|
||||
Map<UUID, PlayerState> players = stateManager.players();
|
||||
return VigilanteControlPanelModel.create(
|
||||
stateManager.game(), players, followers.invitedPlayerIds(player.getUniqueId()),
|
||||
presenceProvider.inspect(player, players), strengthCap, resistanceCap
|
||||
);
|
||||
}
|
||||
|
||||
private void refresh(Player player) {
|
||||
openOverview(player, model(player));
|
||||
}
|
||||
|
||||
private Inventory create(MenuHolder holder, int size, String title) {
|
||||
Inventory inventory = server.createInventory(holder, size, title);
|
||||
holder.inventory = inventory;
|
||||
return inventory;
|
||||
}
|
||||
|
||||
private static ItemStack playerItem(String name, UUID playerId, String status) {
|
||||
return item(Material.PLAYER_HEAD, name, List.of(status, playerId.toString()));
|
||||
}
|
||||
|
||||
private static ItemStack item(Material material, String name, List<String> lore) {
|
||||
ItemStack item = new ItemStack(material);
|
||||
ItemMeta meta = item.getItemMeta();
|
||||
meta.setDisplayName(name);
|
||||
meta.setLore(lore.stream().map(line -> ChatColor.GRAY + line).toList());
|
||||
item.setItemMeta(meta);
|
||||
return item;
|
||||
}
|
||||
|
||||
private static ItemStack backItem() {
|
||||
return item(Material.ARROW, ChatColor.YELLOW + "Back", List.of());
|
||||
}
|
||||
|
||||
private static ItemStack cancelItem() {
|
||||
return item(Material.RED_WOOL, ChatColor.RED + "Cancel", List.of());
|
||||
}
|
||||
|
||||
private static ItemStack confirmItem() {
|
||||
return item(Material.LIME_WOOL, ChatColor.GREEN + "Confirm", List.of());
|
||||
}
|
||||
|
||||
private static List<String> followerSummary(VigilanteControlPanelModel model) {
|
||||
if (model.followers().isEmpty()) {
|
||||
return List.of("No Followers assigned.");
|
||||
}
|
||||
return model.followers().stream()
|
||||
.map(follower -> follower.name() + (follower.contributes() ? " — contributing" : ""))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private enum MenuView {
|
||||
OVERVIEW,
|
||||
RECRUITMENT,
|
||||
INVITE_CONFIRMATION,
|
||||
FOLLOWERS,
|
||||
DISMISS_CONFIRMATION,
|
||||
ROLE,
|
||||
RELINQUISH_CONFIRMATION
|
||||
}
|
||||
|
||||
private static final class MenuHolder implements InventoryHolder {
|
||||
private final MenuView view;
|
||||
private final UUID targetId;
|
||||
private Inventory inventory;
|
||||
|
||||
private MenuHolder(MenuView view, UUID targetId) {
|
||||
this.view = view;
|
||||
this.targetId = targetId;
|
||||
}
|
||||
|
||||
private MenuView view() {
|
||||
return view;
|
||||
}
|
||||
|
||||
private UUID targetId() {
|
||||
return targetId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Inventory getInventory() {
|
||||
return inventory;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
public interface CapturedMobCustodyRelease {
|
||||
PlayerState releaseAll(Player player, PlayerState state);
|
||||
|
||||
PlayerState releaseExcess(Player player, PlayerState state, int custodyLimit);
|
||||
}
|
||||
@@ -16,6 +16,11 @@ public final class CapturedMobService {
|
||||
if (player.tyrantClass() != TyrantClass.TAMER) {
|
||||
throw new IllegalStateException("only a Tamer can capture mobs");
|
||||
}
|
||||
if (!player.capturedMobs().isEmpty()) {
|
||||
throw new IllegalStateException(
|
||||
"release the currently captured mob before capturing another"
|
||||
);
|
||||
}
|
||||
if ("ENDER_DRAGON".equalsIgnoreCase(entityType)) {
|
||||
throw new IllegalArgumentException("Ender Dragons cannot be captured");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface CapturedMobSpawner {
|
||||
boolean spawn(Player player, CapturedMob mob);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
public final class DefaultCapturedMobCustodyRelease implements CapturedMobCustodyRelease {
|
||||
private final CapturedMobSpawner spawner;
|
||||
|
||||
public DefaultCapturedMobCustodyRelease(CapturedMobSpawner spawner) {
|
||||
this.spawner = spawner;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlayerState releaseAll(Player player, PlayerState state) {
|
||||
return releaseExcess(player, state, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlayerState releaseExcess(
|
||||
Player player,
|
||||
PlayerState state,
|
||||
int custodyLimit
|
||||
) {
|
||||
if (custodyLimit < 0) {
|
||||
throw new IllegalArgumentException("custodyLimit must not be negative");
|
||||
}
|
||||
List<CapturedMob> retained = new ArrayList<>();
|
||||
List<CapturedMob> custody = state.capturedMobs();
|
||||
for (int index = 0; index < custody.size(); index++) {
|
||||
CapturedMob mob = custody.get(index);
|
||||
if (index < custodyLimit || !spawner.spawn(player, mob)) {
|
||||
retained.add(mob);
|
||||
}
|
||||
}
|
||||
if (retained.equals(custody)) {
|
||||
return state;
|
||||
}
|
||||
return new PlayerState(
|
||||
state.playerId(), state.latestName(), state.lastLogin(), state.optedOutUntil(),
|
||||
state.tyrantClass(), state.followerOf(), state.cooldownEnds(),
|
||||
state.readyAbilityItems(), retained
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
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;
|
||||
|
||||
public final class DefaultVigilanteControlPanel implements VigilanteControlPanel {
|
||||
private final TyrantStateManager stateManager;
|
||||
private final FollowerService followers;
|
||||
private final VigilanteControlPanelRenderer renderer;
|
||||
private final FollowerPresenceProvider presenceProvider;
|
||||
private final int strengthCap;
|
||||
private final int resistanceCap;
|
||||
private final Set<UUID> openedSessions = new HashSet<>();
|
||||
|
||||
public DefaultVigilanteControlPanel(
|
||||
TyrantStateManager stateManager,
|
||||
FollowerService followers,
|
||||
VigilanteControlPanelRenderer renderer,
|
||||
FollowerPresenceProvider presenceProvider,
|
||||
int strengthCap,
|
||||
int resistanceCap
|
||||
) {
|
||||
this.stateManager = stateManager;
|
||||
this.followers = followers;
|
||||
this.renderer = renderer;
|
||||
this.presenceProvider = presenceProvider;
|
||||
this.strengthCap = strengthCap;
|
||||
this.resistanceCap = resistanceCap;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void open(Player player) {
|
||||
GameState game = stateManager.game();
|
||||
if (game.lifecycle() != GameLifecycle.RUNNING
|
||||
|| game.vigilanteId().filter(player.getUniqueId()::equals).isEmpty()) {
|
||||
player.sendMessage(ChatColor.RED
|
||||
+ "Only the active Vigilante can open the control panel while the event is running.");
|
||||
return;
|
||||
}
|
||||
Map<UUID, PlayerState> players = stateManager.players();
|
||||
renderer.openOverview(
|
||||
player,
|
||||
VigilanteControlPanelModel.create(
|
||||
game,
|
||||
players,
|
||||
followers.invitedPlayerIds(player.getUniqueId()),
|
||||
presenceProvider.inspect(player, players),
|
||||
strengthCap,
|
||||
resistanceCap
|
||||
)
|
||||
);
|
||||
openedSessions.add(player.getUniqueId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beginSession(UUID playerId) {
|
||||
openedSessions.remove(playerId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void endSession(UUID playerId) {
|
||||
openedSessions.remove(playerId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean openedThisSession(UUID playerId) {
|
||||
return openedSessions.contains(playerId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
public record FollowerPresence(
|
||||
boolean online,
|
||||
boolean living,
|
||||
boolean sameWorld,
|
||||
boolean withinRange
|
||||
) {
|
||||
public boolean contributes() {
|
||||
return online && living && sameWorld && withinRange;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface FollowerPresenceProvider {
|
||||
Map<UUID, FollowerPresence> inspect(Player vigilante, Map<UUID, PlayerState> players);
|
||||
}
|
||||
@@ -22,10 +22,14 @@ public final class FollowerService {
|
||||
}
|
||||
PlayerState target = players.get(targetId);
|
||||
if (target == null || target.optedOutUntil().isPresent()
|
||||
|| target.followerOf().isPresent()
|
||||
|| game.tyrantId().filter(targetId::equals).isPresent()
|
||||
|| game.vigilanteId().filter(targetId::equals).isPresent()) {
|
||||
return result(players, FollowerStatus.INELIGIBLE);
|
||||
}
|
||||
if (vigilanteId.equals(invitations.get(targetId))) {
|
||||
return result(players, FollowerStatus.ALREADY_INVITED);
|
||||
}
|
||||
invitations.put(targetId, vigilanteId);
|
||||
return result(players, FollowerStatus.INVITED);
|
||||
}
|
||||
@@ -77,6 +81,13 @@ public final class FollowerService {
|
||||
return updated(players, withFollower(target, Optional.empty()), FollowerStatus.LEFT);
|
||||
}
|
||||
|
||||
public java.util.Set<UUID> invitedPlayerIds(UUID vigilanteId) {
|
||||
return invitations.entrySet().stream()
|
||||
.filter(entry -> entry.getValue().equals(vigilanteId))
|
||||
.map(Map.Entry::getKey)
|
||||
.collect(java.util.stream.Collectors.toUnmodifiableSet());
|
||||
}
|
||||
|
||||
public void clearInvitations() {
|
||||
invitations.clear();
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package games.dmg.spigottyrant;
|
||||
|
||||
public enum FollowerStatus {
|
||||
INVITED,
|
||||
ALREADY_INVITED,
|
||||
JOINED,
|
||||
DISMISSED,
|
||||
LEFT,
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
import org.bukkit.Server;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
public final class FormerTamerReleaseTask implements Runnable {
|
||||
private final TyrantStateManager stateManager;
|
||||
private final Server server;
|
||||
private final CapturedMobCustodyRelease release;
|
||||
private final CapturedMobInventoryService inventory;
|
||||
|
||||
public FormerTamerReleaseTask(
|
||||
TyrantStateManager stateManager,
|
||||
Server server,
|
||||
CapturedMobCustodyRelease release
|
||||
) {
|
||||
this(stateManager, server, release, null);
|
||||
}
|
||||
|
||||
public FormerTamerReleaseTask(
|
||||
TyrantStateManager stateManager,
|
||||
Server server,
|
||||
CapturedMobCustodyRelease release,
|
||||
CapturedMobInventoryService inventory
|
||||
) {
|
||||
this.stateManager = stateManager;
|
||||
this.server = server;
|
||||
this.release = release;
|
||||
this.inventory = inventory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Map<UUID, Player> online = server.getOnlinePlayers().stream()
|
||||
.collect(Collectors.toMap(Player::getUniqueId, player -> player));
|
||||
for (PlayerState state : stateManager.players().values()) {
|
||||
if (state.capturedMobs().isEmpty()
|
||||
|| state.tyrantClass() == TyrantClass.TAMER
|
||||
&& state.capturedMobs().size() == 1) {
|
||||
continue;
|
||||
}
|
||||
Player player = online.get(state.playerId());
|
||||
if (player == null || player.isDead()) {
|
||||
continue;
|
||||
}
|
||||
PlayerState released = state.tyrantClass() == TyrantClass.TAMER
|
||||
? release.releaseExcess(player, state, 1)
|
||||
: release.releaseAll(player, state);
|
||||
if (released.equals(state)) {
|
||||
continue;
|
||||
}
|
||||
stateManager.updatePlayer(
|
||||
state.playerId(), state.latestName(), current -> released
|
||||
);
|
||||
stateManager.saveIfDirty();
|
||||
if (inventory != null) {
|
||||
inventory.reconcile(player, released);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,8 @@ public record PluginSettings(
|
||||
AbilityItemSettings fixerItem,
|
||||
AbilityItemSettings tamerItem,
|
||||
AbilityItemSettings capturedMobItem,
|
||||
AbilityItemSettings tyrantControlItem,
|
||||
AbilityItemSettings vigilanteControlItem,
|
||||
String recoveryCommand,
|
||||
String inventoryFullMessage,
|
||||
boolean freezeTimersWhilePaused
|
||||
@@ -78,6 +80,10 @@ public record PluginSettings(
|
||||
fixerItem = Objects.requireNonNull(fixerItem, "fixerItem");
|
||||
tamerItem = Objects.requireNonNull(tamerItem, "tamerItem");
|
||||
capturedMobItem = Objects.requireNonNull(capturedMobItem, "capturedMobItem");
|
||||
tyrantControlItem = Objects.requireNonNull(tyrantControlItem, "tyrantControlItem");
|
||||
vigilanteControlItem = Objects.requireNonNull(
|
||||
vigilanteControlItem, "vigilanteControlItem"
|
||||
);
|
||||
recoveryCommand = requireText(recoveryCommand, "recovery-command");
|
||||
inventoryFullMessage = requireText(inventoryFullMessage, "inventory-full-message");
|
||||
}
|
||||
@@ -114,6 +120,14 @@ public record PluginSettings(
|
||||
item(values, "fixer-item", new AbilityItemSettings("STICK", "Fixer's Wrench")),
|
||||
item(values, "tamer-item", new AbilityItemSettings("FISHING_ROD", "Tamer's Lead")),
|
||||
item(values, "captured-mob-item", new AbilityItemSettings("PAPER", "Captured Mob")),
|
||||
item(
|
||||
values, "tyrant-control-item",
|
||||
new AbilityItemSettings("NETHER_STAR", "Tyrant Control")
|
||||
),
|
||||
item(
|
||||
values, "vigilante-control-item",
|
||||
new AbilityItemSettings("COMPASS", "Vigilante Control")
|
||||
),
|
||||
string(values, "recovery-command", "/tyrant item"),
|
||||
string(
|
||||
values,
|
||||
|
||||
@@ -12,6 +12,8 @@ public final class PluginSettingsValidator {
|
||||
validateItem(settings.fixerItem(), "fixer-item");
|
||||
validateItem(settings.tamerItem(), "tamer-item");
|
||||
validateItem(settings.capturedMobItem(), "captured-mob-item");
|
||||
validateItem(settings.tyrantControlItem(), "tyrant-control-item");
|
||||
validateItem(settings.vigilanteControlItem(), "vigilante-control-item");
|
||||
for (String entityType : settings.deniedMobTypes()) {
|
||||
try {
|
||||
EntityType.valueOf(entityType);
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
public enum RoleControl {
|
||||
TYRANT,
|
||||
VIGILANTE
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import org.bukkit.entity.ItemFrame;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.block.Action;
|
||||
import org.bukkit.event.block.BlockDispenseEvent;
|
||||
import org.bukkit.event.block.BlockPlaceEvent;
|
||||
import org.bukkit.event.entity.EntityPickupItemEvent;
|
||||
import org.bukkit.event.entity.PlayerDeathEvent;
|
||||
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||
import org.bukkit.event.inventory.InventoryDragEvent;
|
||||
import org.bukkit.event.inventory.InventoryMoveItemEvent;
|
||||
import org.bukkit.event.player.PlayerDropItemEvent;
|
||||
import org.bukkit.event.player.PlayerInteractEntityEvent;
|
||||
import org.bukkit.event.player.PlayerInteractEvent;
|
||||
import org.bukkit.event.player.PlayerJoinEvent;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
|
||||
public final class RoleControlItemListener implements Listener {
|
||||
private final TyrantStateManager stateManager;
|
||||
private final RoleControlItemService items;
|
||||
private final TyrantControlPanel tyrantPanel;
|
||||
private final VigilanteControlPanel vigilantePanel;
|
||||
|
||||
public RoleControlItemListener(
|
||||
TyrantStateManager stateManager,
|
||||
RoleControlItemService items,
|
||||
TyrantControlPanel tyrantPanel,
|
||||
VigilanteControlPanel vigilantePanel
|
||||
) {
|
||||
this.stateManager = stateManager;
|
||||
this.items = items;
|
||||
this.tyrantPanel = tyrantPanel;
|
||||
this.vigilantePanel = vigilantePanel;
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onInteract(PlayerInteractEvent event) {
|
||||
ItemStack item = event.getItem();
|
||||
if (!items.isBoundRoleControlItem(item)
|
||||
|| event.getAction() != Action.RIGHT_CLICK_AIR
|
||||
&& event.getAction() != Action.RIGHT_CLICK_BLOCK) {
|
||||
return;
|
||||
}
|
||||
event.setCancelled(true);
|
||||
Player player = event.getPlayer();
|
||||
GameState game = stateManager.game();
|
||||
java.util.Optional<RoleControl> required = RoleControlItemPolicy.requiredRole(
|
||||
game, player.getUniqueId()
|
||||
);
|
||||
boolean valid = items.owner(item).filter(player.getUniqueId()::equals).isPresent()
|
||||
&& items.role(item).equals(required)
|
||||
&& game.lifecycle() == GameLifecycle.RUNNING;
|
||||
if (!valid) {
|
||||
items.removeInvalid(player, game);
|
||||
player.sendMessage(org.bukkit.ChatColor.RED
|
||||
+ "That role control item is no longer valid.");
|
||||
return;
|
||||
}
|
||||
if (required.orElseThrow() == RoleControl.TYRANT) {
|
||||
tyrantPanel.open(player);
|
||||
} else {
|
||||
vigilantePanel.open(player);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onJoin(PlayerJoinEvent event) {
|
||||
items.removeInvalid(event.getPlayer(), stateManager.game());
|
||||
items.recover(event.getPlayer(), stateManager.game());
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onDrop(PlayerDropItemEvent event) {
|
||||
if (items.isBoundRoleControlItem(event.getItemDrop().getItemStack())) {
|
||||
event.getItemDrop().remove();
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onPickup(EntityPickupItemEvent event) {
|
||||
if (items.isBoundRoleControlItem(event.getItem().getItemStack())) {
|
||||
event.setCancelled(true);
|
||||
event.getItem().remove();
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onInventoryClick(InventoryClickEvent event) {
|
||||
if (items.isBoundRoleControlItem(event.getCurrentItem())
|
||||
|| items.isBoundRoleControlItem(event.getCursor())) {
|
||||
if (!(event.getWhoClicked() instanceof Player player)
|
||||
|| event.isShiftClick()
|
||||
|| event.getClickedInventory() == null
|
||||
|| !event.getClickedInventory().equals(player.getInventory())) {
|
||||
event.setCancelled(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onInventoryDrag(InventoryDragEvent event) {
|
||||
if (items.isBoundRoleControlItem(event.getOldCursor())
|
||||
|| event.getNewItems().values().stream()
|
||||
.anyMatch(items::isBoundRoleControlItem)) {
|
||||
event.setCancelled(true);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onInventoryMove(InventoryMoveItemEvent event) {
|
||||
if (items.isBoundRoleControlItem(event.getItem())) {
|
||||
event.setCancelled(true);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onPlace(BlockPlaceEvent event) {
|
||||
if (items.isBoundRoleControlItem(event.getItemInHand())) {
|
||||
event.setCancelled(true);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onDispense(BlockDispenseEvent event) {
|
||||
if (items.isBoundRoleControlItem(event.getItem())) {
|
||||
event.setCancelled(true);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onFrame(PlayerInteractEntityEvent event) {
|
||||
if (event.getRightClicked() instanceof ItemFrame
|
||||
&& items.isBoundRoleControlItem(event.getPlayer().getInventory()
|
||||
.getItem(event.getHand()))) {
|
||||
event.setCancelled(true);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onDeath(PlayerDeathEvent event) {
|
||||
event.getDrops().removeIf(items::isBoundRoleControlItem);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public final class RoleControlItemPolicy {
|
||||
private RoleControlItemPolicy() {
|
||||
}
|
||||
|
||||
public static Optional<RoleControl> requiredRole(GameState game, UUID playerId) {
|
||||
if (game.tyrantId().filter(playerId::equals).isPresent()) {
|
||||
return Optional.of(RoleControl.TYRANT);
|
||||
}
|
||||
if (game.vigilanteId().filter(playerId::equals).isPresent()) {
|
||||
return Optional.of(RoleControl.VIGILANTE);
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import org.bukkit.Server;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
public final class RoleControlItemRefreshTask implements Runnable {
|
||||
private final TyrantStateManager stateManager;
|
||||
private final RoleControlItemService items;
|
||||
private final Server server;
|
||||
|
||||
public RoleControlItemRefreshTask(
|
||||
TyrantStateManager stateManager,
|
||||
RoleControlItemService items,
|
||||
Server server
|
||||
) {
|
||||
this.stateManager = stateManager;
|
||||
this.items = items;
|
||||
this.server = server;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
GameState game = stateManager.game();
|
||||
for (Player player : server.getOnlinePlayers()) {
|
||||
items.removeInvalid(player, game);
|
||||
if (!player.isDead()) {
|
||||
items.recover(player, game);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
|
||||
public interface RoleControlItemService {
|
||||
void recover(Player player, GameState game);
|
||||
|
||||
boolean isBoundRoleControlItem(ItemStack item);
|
||||
|
||||
Optional<RoleControl> role(ItemStack item);
|
||||
|
||||
Optional<UUID> owner(ItemStack item);
|
||||
|
||||
void removeInvalid(Player player, GameState game);
|
||||
|
||||
void removeAll(Player player);
|
||||
}
|
||||
@@ -54,6 +54,9 @@ public final class SpigotTyrantPlugin extends JavaPlugin {
|
||||
FollowerService followers = new FollowerService();
|
||||
AbilityReadinessService readiness = new AbilityReadinessService();
|
||||
AbilityItemService abilityItems = new BukkitAbilityItemService(this, settings);
|
||||
RoleControlItemService roleControlItems = new BukkitRoleControlItemService(
|
||||
this, settings
|
||||
);
|
||||
CapturedMobItemService capturedMobItems = new BukkitCapturedMobItemService(
|
||||
this, settings
|
||||
);
|
||||
@@ -61,6 +64,10 @@ public final class SpigotTyrantPlugin extends JavaPlugin {
|
||||
CapturedMobInventoryService capturedMobInventory = new CapturedMobInventoryService(
|
||||
capturedMobItems, settings
|
||||
);
|
||||
CapturedMobCustodyRelease formerTamerRelease =
|
||||
new DefaultCapturedMobCustodyRelease(
|
||||
new BukkitCapturedMobSpawner(getServer(), settings)
|
||||
);
|
||||
AssassinAbilityService assassinAbilities = new AssassinAbilityService(
|
||||
readiness, settings
|
||||
);
|
||||
@@ -85,6 +92,18 @@ public final class SpigotTyrantPlugin extends JavaPlugin {
|
||||
TyrantControlPanel controlPanel = new DefaultTyrantControlPanel(
|
||||
stateManager, controlPanelRenderer, clock
|
||||
);
|
||||
FollowerPresenceProvider followerPresence = new BukkitFollowerPresenceProvider(
|
||||
getServer(), settings.followerRangeBlocks()
|
||||
);
|
||||
BukkitVigilanteControlPanelRenderer vigilanteControlPanelRenderer =
|
||||
new BukkitVigilanteControlPanelRenderer(
|
||||
getServer(), stateManager, followers, followerPresence,
|
||||
settings.followerStrengthCap(), settings.followerResistanceCap()
|
||||
);
|
||||
VigilanteControlPanel vigilanteControlPanel = new DefaultVigilanteControlPanel(
|
||||
stateManager, followers, vigilanteControlPanelRenderer, followerPresence,
|
||||
settings.followerStrengthCap(), settings.followerResistanceCap()
|
||||
);
|
||||
Objects.requireNonNull(getCommand("tyrant"), "Missing tyrant command metadata")
|
||||
.setExecutor(new TyrantCommand(
|
||||
stateManager,
|
||||
@@ -97,10 +116,14 @@ public final class SpigotTyrantPlugin extends JavaPlugin {
|
||||
tyrantPresentation,
|
||||
abilityItems,
|
||||
new TyrantAbilityService(settings.rosterIntelligenceCooldown()),
|
||||
controlPanel
|
||||
controlPanel,
|
||||
roleControlItems
|
||||
));
|
||||
Objects.requireNonNull(getCommand("vigilante"), "Missing vigilante metadata")
|
||||
.setExecutor(new VigilanteCommand(stateManager, followers, onlinePlayers));
|
||||
.setExecutor(new VigilanteCommand(
|
||||
stateManager, followers, onlinePlayers, vigilanteControlPanel,
|
||||
roleControlItems
|
||||
));
|
||||
Objects.requireNonNull(getCommand("tyrantadmin"), "Missing tyrantadmin metadata")
|
||||
.setExecutor(new TyrantAdminCommand(
|
||||
stateManager,
|
||||
@@ -113,11 +136,20 @@ public final class SpigotTyrantPlugin extends JavaPlugin {
|
||||
settings.pendingSelectionTimeout(),
|
||||
random
|
||||
));
|
||||
Objects.requireNonNull(getCommand("tyrant"), "Missing tyrant command metadata")
|
||||
.setTabCompleter(new TyrantTabCompleter(stateManager, onlinePlayers));
|
||||
Objects.requireNonNull(getCommand("vigilante"), "Missing vigilante metadata")
|
||||
.setTabCompleter(new VigilanteTabCompleter(
|
||||
stateManager, followers, onlinePlayers
|
||||
));
|
||||
Objects.requireNonNull(getCommand("tyrantadmin"), "Missing tyrantadmin metadata")
|
||||
.setTabCompleter(new TyrantAdminTabCompleter(onlinePlayers));
|
||||
getServer().getPluginManager().registerEvents(
|
||||
new PlayerJoinListener(stateManager, clock, tyrantPresentation),
|
||||
this
|
||||
);
|
||||
getServer().getPluginManager().registerEvents(controlPanelRenderer, this);
|
||||
getServer().getPluginManager().registerEvents(vigilanteControlPanelRenderer, this);
|
||||
getServer().getPluginManager().registerEvents(
|
||||
new TyrantControlPanelJoinListener(
|
||||
stateManager,
|
||||
@@ -127,6 +159,15 @@ public final class SpigotTyrantPlugin extends JavaPlugin {
|
||||
),
|
||||
this
|
||||
);
|
||||
getServer().getPluginManager().registerEvents(
|
||||
new VigilanteControlPanelJoinListener(
|
||||
stateManager,
|
||||
vigilanteControlPanel,
|
||||
(task, delayTicks) -> getServer().getScheduler()
|
||||
.runTaskLater(this, task, delayTicks)
|
||||
),
|
||||
this
|
||||
);
|
||||
getServer().getPluginManager().registerEvents(
|
||||
new TyrantDeathListener(
|
||||
stateManager, succession, tyrantPresentation, getServer(), clock
|
||||
@@ -157,6 +198,12 @@ public final class SpigotTyrantPlugin extends JavaPlugin {
|
||||
new BoundItemListener(abilityItems),
|
||||
this
|
||||
);
|
||||
getServer().getPluginManager().registerEvents(
|
||||
new RoleControlItemListener(
|
||||
stateManager, roleControlItems, controlPanel, vigilanteControlPanel
|
||||
),
|
||||
this
|
||||
);
|
||||
getServer().getPluginManager().registerEvents(
|
||||
new TamerListener(
|
||||
stateManager, abilityItems, capturedMobItems, capturedMobs,
|
||||
@@ -205,6 +252,20 @@ public final class SpigotTyrantPlugin extends JavaPlugin {
|
||||
20L,
|
||||
20L
|
||||
);
|
||||
getServer().getScheduler().runTaskTimer(
|
||||
this,
|
||||
new RoleControlItemRefreshTask(stateManager, roleControlItems, getServer()),
|
||||
1L,
|
||||
10L
|
||||
);
|
||||
getServer().getScheduler().runTaskTimer(
|
||||
this,
|
||||
new FormerTamerReleaseTask(
|
||||
stateManager, getServer(), formerTamerRelease, capturedMobInventory
|
||||
),
|
||||
1L,
|
||||
10L
|
||||
);
|
||||
getServer().getScheduler().runTaskTimer(this, stateManager::saveIfDirty, 600L, 600L);
|
||||
getLogger().info("Spigot Tyrant enabled.");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
public final class TabSuggestions {
|
||||
private TabSuggestions() {
|
||||
}
|
||||
|
||||
public static List<String> matching(String prefix, Collection<String> candidates) {
|
||||
String normalized = prefix == null ? "" : prefix.toLowerCase(Locale.ROOT);
|
||||
return candidates.stream()
|
||||
.filter(candidate -> candidate.toLowerCase(Locale.ROOT).startsWith(normalized))
|
||||
.distinct()
|
||||
.sorted(String.CASE_INSENSITIVE_ORDER)
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
@@ -63,6 +63,11 @@ public final class TamerListener implements Listener {
|
||||
return;
|
||||
}
|
||||
PlayerState state = stateManager.player(player.getUniqueId(), player.getName());
|
||||
if (!state.capturedMobs().isEmpty()) {
|
||||
player.sendMessage(ChatColor.RED
|
||||
+ "Release the currently captured mob before capturing another.");
|
||||
return;
|
||||
}
|
||||
Entity target = event.getRightClicked();
|
||||
if (!canCapture(state, target)) {
|
||||
player.sendMessage(ChatColor.RED + "That mob cannot be captured.");
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.command.TabCompleter;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
public final class TyrantAdminTabCompleter implements TabCompleter {
|
||||
private static final String PERMISSION = "spigottyrant.admin";
|
||||
private static final List<String> SUBCOMMANDS = List.of(
|
||||
"status", "start", "pause", "resume", "reset"
|
||||
);
|
||||
private final OnlinePlayerDirectory onlinePlayers;
|
||||
|
||||
public TyrantAdminTabCompleter(OnlinePlayerDirectory onlinePlayers) {
|
||||
this.onlinePlayers = onlinePlayers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> onTabComplete(
|
||||
CommandSender sender,
|
||||
Command command,
|
||||
String alias,
|
||||
String[] arguments
|
||||
) {
|
||||
if (!sender.hasPermission(PERMISSION) || arguments.length == 0) {
|
||||
return List.of();
|
||||
}
|
||||
if (arguments.length == 1) {
|
||||
return TabSuggestions.matching(arguments[0], SUBCOMMANDS);
|
||||
}
|
||||
String subcommand = arguments[0].toLowerCase(java.util.Locale.ROOT);
|
||||
if (arguments.length == 2) {
|
||||
return switch (subcommand) {
|
||||
case "start" -> TabSuggestions.matching(arguments[1], onlineNames(null));
|
||||
case "reset" -> TabSuggestions.matching(arguments[1], List.of("confirm"));
|
||||
default -> List.of();
|
||||
};
|
||||
}
|
||||
if (arguments.length == 3 && subcommand.equals("start")) {
|
||||
return TabSuggestions.matching(arguments[2], onlineNames(arguments[1]));
|
||||
}
|
||||
return List.of();
|
||||
}
|
||||
|
||||
private List<String> onlineNames(String excludedName) {
|
||||
List<String> names = new ArrayList<>();
|
||||
for (UUID playerId : onlinePlayers.onlinePlayerIds()) {
|
||||
Player player = onlinePlayers.findById(playerId);
|
||||
if (player != null && (excludedName == null
|
||||
|| !player.getName().equalsIgnoreCase(excludedName))) {
|
||||
names.add(player.getName());
|
||||
}
|
||||
}
|
||||
return List.copyOf(names);
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ public final class TyrantCommand implements CommandExecutor {
|
||||
private final AbilityItemService abilityItems;
|
||||
private final TyrantAbilityService tyrantAbilities;
|
||||
private final TyrantControlPanel controlPanel;
|
||||
private final RoleControlItemService roleControlItems;
|
||||
|
||||
public TyrantCommand(
|
||||
TyrantStateManager stateManager,
|
||||
@@ -44,7 +45,21 @@ public final class TyrantCommand implements CommandExecutor {
|
||||
stateManager, progression, new ClassAssignmentService(),
|
||||
new BukkitOnlinePlayerDirectory(), Clock.systemUTC(),
|
||||
new ParticipationService(Duration.ofDays(7)), null, null, null,
|
||||
new TyrantAbilityService(Duration.ofHours(24)), controlPanel
|
||||
new TyrantAbilityService(Duration.ofHours(24)), controlPanel, null
|
||||
);
|
||||
}
|
||||
|
||||
public TyrantCommand(
|
||||
TyrantStateManager stateManager,
|
||||
TyrantProgressionService progression,
|
||||
TyrantControlPanel controlPanel,
|
||||
RoleControlItemService roleControlItems
|
||||
) {
|
||||
this(
|
||||
stateManager, progression, new ClassAssignmentService(),
|
||||
new BukkitOnlinePlayerDirectory(), Clock.systemUTC(),
|
||||
new ParticipationService(Duration.ofDays(7)), null, null, null,
|
||||
new TyrantAbilityService(Duration.ofHours(24)), controlPanel, roleControlItems
|
||||
);
|
||||
}
|
||||
|
||||
@@ -58,7 +73,8 @@ public final class TyrantCommand implements CommandExecutor {
|
||||
this(
|
||||
stateManager, progression, assignments, onlinePlayers, clock,
|
||||
new ParticipationService(Duration.ofDays(7)), null, null, null,
|
||||
new TyrantAbilityService(Duration.ofHours(24)), TyrantCommand::unavailablePanel
|
||||
new TyrantAbilityService(Duration.ofHours(24)), TyrantCommand::unavailablePanel,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
@@ -77,7 +93,7 @@ public final class TyrantCommand implements CommandExecutor {
|
||||
this(
|
||||
stateManager, progression, assignments, onlinePlayers, clock, participation,
|
||||
relinquishment, presentation, abilityItems, tyrantAbilities,
|
||||
TyrantCommand::unavailablePanel
|
||||
TyrantCommand::unavailablePanel, null
|
||||
);
|
||||
}
|
||||
|
||||
@@ -93,6 +109,26 @@ public final class TyrantCommand implements CommandExecutor {
|
||||
AbilityItemService abilityItems,
|
||||
TyrantAbilityService tyrantAbilities,
|
||||
TyrantControlPanel controlPanel
|
||||
) {
|
||||
this(
|
||||
stateManager, progression, assignments, onlinePlayers, clock, participation,
|
||||
relinquishment, presentation, abilityItems, tyrantAbilities, controlPanel, null
|
||||
);
|
||||
}
|
||||
|
||||
public TyrantCommand(
|
||||
TyrantStateManager stateManager,
|
||||
TyrantProgressionService progression,
|
||||
ClassAssignmentService assignments,
|
||||
OnlinePlayerDirectory onlinePlayers,
|
||||
Clock clock,
|
||||
ParticipationService participation,
|
||||
RoleRelinquishmentService relinquishment,
|
||||
TyrantPresentation presentation,
|
||||
AbilityItemService abilityItems,
|
||||
TyrantAbilityService tyrantAbilities,
|
||||
TyrantControlPanel controlPanel,
|
||||
RoleControlItemService roleControlItems
|
||||
) {
|
||||
this.stateManager = stateManager;
|
||||
this.progression = progression;
|
||||
@@ -105,6 +141,7 @@ public final class TyrantCommand implements CommandExecutor {
|
||||
this.abilityItems = abilityItems;
|
||||
this.tyrantAbilities = tyrantAbilities;
|
||||
this.controlPanel = controlPanel;
|
||||
this.roleControlItems = roleControlItems;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -308,14 +345,21 @@ public final class TyrantCommand implements CommandExecutor {
|
||||
}
|
||||
|
||||
private void recoverItems(Player player) {
|
||||
if (abilityItems == null) {
|
||||
player.sendMessage(ChatColor.RED + "Ability item recovery is unavailable.");
|
||||
if (abilityItems == null && roleControlItems == null) {
|
||||
player.sendMessage(ChatColor.RED + "Item recovery is unavailable.");
|
||||
return;
|
||||
}
|
||||
PlayerState state = stateManager.player(player.getUniqueId(), player.getName());
|
||||
abilityItems.recover(player, state);
|
||||
if (abilityItems != null) {
|
||||
PlayerState state = stateManager.player(player.getUniqueId(), player.getName());
|
||||
abilityItems.recover(player, state);
|
||||
}
|
||||
GameState game = stateManager.game();
|
||||
if (roleControlItems != null
|
||||
&& game.tyrantId().filter(player.getUniqueId()::equals).isPresent()) {
|
||||
roleControlItems.recover(player, game);
|
||||
}
|
||||
player.sendMessage(ChatColor.YELLOW
|
||||
+ "Recovered every currently ready missing class item that could fit.");
|
||||
+ "Recovered every currently available missing Tyrant item that could fit.");
|
||||
}
|
||||
|
||||
private void relinquish(Player player, String[] arguments) {
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.command.TabCompleter;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
public final class TyrantTabCompleter implements TabCompleter {
|
||||
private static final List<String> SUBCOMMANDS = List.of(
|
||||
"menu", "status", "choices", "buy", "assign", "item", "intelligence",
|
||||
"optout", "optin", "relinquish"
|
||||
);
|
||||
private static final List<String> UNLOCKS = java.util.Arrays.stream(TyrantUnlock.values())
|
||||
.map(value -> value.name().toLowerCase(java.util.Locale.ROOT))
|
||||
.toList();
|
||||
private static final List<String> CLASSES = List.of("assassin", "fixer", "tamer");
|
||||
private final TyrantStateManager stateManager;
|
||||
private final OnlinePlayerDirectory onlinePlayers;
|
||||
|
||||
public TyrantTabCompleter(
|
||||
TyrantStateManager stateManager,
|
||||
OnlinePlayerDirectory onlinePlayers
|
||||
) {
|
||||
this.stateManager = stateManager;
|
||||
this.onlinePlayers = onlinePlayers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> onTabComplete(
|
||||
CommandSender sender,
|
||||
Command command,
|
||||
String alias,
|
||||
String[] arguments
|
||||
) {
|
||||
if (!(sender instanceof Player player) || arguments.length == 0) {
|
||||
return List.of();
|
||||
}
|
||||
if (arguments.length == 1) {
|
||||
return TabSuggestions.matching(arguments[0], SUBCOMMANDS);
|
||||
}
|
||||
String subcommand = arguments[0].toLowerCase(java.util.Locale.ROOT);
|
||||
if (arguments.length == 2) {
|
||||
return switch (subcommand) {
|
||||
case "buy" -> TabSuggestions.matching(arguments[1], UNLOCKS);
|
||||
case "assign" -> TabSuggestions.matching(arguments[1], CLASSES);
|
||||
case "relinquish" -> TabSuggestions.matching(
|
||||
arguments[1], List.of("confirm")
|
||||
);
|
||||
default -> List.of();
|
||||
};
|
||||
}
|
||||
if (arguments.length == 3 && subcommand.equals("assign")) {
|
||||
return TabSuggestions.matching(arguments[2], eligibleAssignees(player));
|
||||
}
|
||||
return List.of();
|
||||
}
|
||||
|
||||
private List<String> eligibleAssignees(Player tyrant) {
|
||||
Map<UUID, PlayerState> states = stateManager.players();
|
||||
GameState game = stateManager.game();
|
||||
Set<UUID> online = onlinePlayers.onlinePlayerIds();
|
||||
List<String> names = new ArrayList<>();
|
||||
for (UUID playerId : online) {
|
||||
if (playerId.equals(tyrant.getUniqueId())
|
||||
|| game != null && game.tyrantId().filter(playerId::equals).isPresent()) {
|
||||
continue;
|
||||
}
|
||||
PlayerState state = states.get(playerId);
|
||||
if (state != null && state.optedOutUntil().isPresent()) {
|
||||
continue;
|
||||
}
|
||||
Player candidate = onlinePlayers.findById(playerId);
|
||||
if (candidate != null) {
|
||||
names.add(candidate.getName());
|
||||
}
|
||||
}
|
||||
return List.copyOf(names);
|
||||
}
|
||||
}
|
||||
@@ -11,15 +11,43 @@ public final class VigilanteCommand implements CommandExecutor {
|
||||
private final TyrantStateManager stateManager;
|
||||
private final FollowerService followers;
|
||||
private final OnlinePlayerDirectory onlinePlayers;
|
||||
private final VigilanteControlPanel controlPanel;
|
||||
private final RoleControlItemService roleControlItems;
|
||||
|
||||
public VigilanteCommand(
|
||||
TyrantStateManager stateManager,
|
||||
FollowerService followers,
|
||||
OnlinePlayerDirectory onlinePlayers
|
||||
) {
|
||||
this(
|
||||
stateManager, followers, onlinePlayers,
|
||||
player -> player.sendMessage(ChatColor.RED
|
||||
+ "The Vigilante control panel is unavailable."),
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
public VigilanteCommand(
|
||||
TyrantStateManager stateManager,
|
||||
FollowerService followers,
|
||||
OnlinePlayerDirectory onlinePlayers,
|
||||
VigilanteControlPanel controlPanel
|
||||
) {
|
||||
this(stateManager, followers, onlinePlayers, controlPanel, null);
|
||||
}
|
||||
|
||||
public VigilanteCommand(
|
||||
TyrantStateManager stateManager,
|
||||
FollowerService followers,
|
||||
OnlinePlayerDirectory onlinePlayers,
|
||||
VigilanteControlPanel controlPanel,
|
||||
RoleControlItemService roleControlItems
|
||||
) {
|
||||
this.stateManager = stateManager;
|
||||
this.followers = followers;
|
||||
this.onlinePlayers = onlinePlayers;
|
||||
this.controlPanel = controlPanel;
|
||||
this.roleControlItems = roleControlItems;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -33,6 +61,21 @@ public final class VigilanteCommand implements CommandExecutor {
|
||||
sender.sendMessage("This command must be used by a player.");
|
||||
return true;
|
||||
}
|
||||
if (arguments.length == 0
|
||||
|| arguments.length == 1 && arguments[0].equalsIgnoreCase("menu")) {
|
||||
controlPanel.open(player);
|
||||
return true;
|
||||
}
|
||||
if (arguments.length == 1 && arguments[0].equalsIgnoreCase("item")) {
|
||||
if (roleControlItems == null) {
|
||||
player.sendMessage(ChatColor.RED + "Role item recovery is unavailable.");
|
||||
} else {
|
||||
roleControlItems.recover(player, stateManager.game());
|
||||
player.sendMessage(ChatColor.YELLOW
|
||||
+ "Recovered the missing Vigilante control item if it could fit.");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
FollowerResult result;
|
||||
Player target = null;
|
||||
if (arguments.length == 1 && arguments[0].equalsIgnoreCase("accept")) {
|
||||
@@ -61,7 +104,7 @@ public final class VigilanteCommand implements CommandExecutor {
|
||||
);
|
||||
} else {
|
||||
player.sendMessage(ChatColor.YELLOW
|
||||
+ "Usage: /vigilante <invite <player>|accept|dismiss <player>|leave>");
|
||||
+ "Usage: /vigilante <menu|item|invite <player>|accept|dismiss <player>|leave>");
|
||||
return true;
|
||||
}
|
||||
if (result.status() == FollowerStatus.JOINED
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import java.util.UUID;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface VigilanteControlPanel {
|
||||
void open(Player player);
|
||||
|
||||
default void beginSession(UUID playerId) {
|
||||
}
|
||||
|
||||
default void endSession(UUID playerId) {
|
||||
}
|
||||
|
||||
default boolean openedThisSession(UUID playerId) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import java.util.UUID;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.player.PlayerJoinEvent;
|
||||
import org.bukkit.event.player.PlayerQuitEvent;
|
||||
|
||||
public final class VigilanteControlPanelJoinListener implements Listener {
|
||||
private static final long OPEN_DELAY_TICKS = 100L;
|
||||
private final TyrantStateManager stateManager;
|
||||
private final VigilanteControlPanel controlPanel;
|
||||
private final DelayedTaskScheduler scheduler;
|
||||
|
||||
public VigilanteControlPanelJoinListener(
|
||||
TyrantStateManager stateManager,
|
||||
VigilanteControlPanel controlPanel,
|
||||
DelayedTaskScheduler scheduler
|
||||
) {
|
||||
this.stateManager = stateManager;
|
||||
this.controlPanel = controlPanel;
|
||||
this.scheduler = scheduler;
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onJoin(PlayerJoinEvent event) {
|
||||
Player player = event.getPlayer();
|
||||
UUID playerId = player.getUniqueId();
|
||||
controlPanel.beginSession(playerId);
|
||||
scheduler.schedule(() -> openIfStillVigilante(player), OPEN_DELAY_TICKS);
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onQuit(PlayerQuitEvent event) {
|
||||
controlPanel.endSession(event.getPlayer().getUniqueId());
|
||||
}
|
||||
|
||||
private void openIfStillVigilante(Player player) {
|
||||
GameState game = stateManager.game();
|
||||
if (player.isOnline()
|
||||
&& game.lifecycle() == GameLifecycle.RUNNING
|
||||
&& game.vigilanteId().filter(player.getUniqueId()::equals).isPresent()
|
||||
&& !controlPanel.openedThisSession(player.getUniqueId())) {
|
||||
controlPanel.open(player);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public record VigilanteControlPanelModel(
|
||||
List<VigilanteFollowerView> followers,
|
||||
Set<String> pendingInvitations,
|
||||
int nearbyEligibleFollowers,
|
||||
int strengthLevel,
|
||||
int resistanceLevel,
|
||||
int strengthCap,
|
||||
int resistanceCap
|
||||
) {
|
||||
public VigilanteControlPanelModel {
|
||||
followers = List.copyOf(followers);
|
||||
pendingInvitations = Set.copyOf(pendingInvitations);
|
||||
}
|
||||
|
||||
public static VigilanteControlPanelModel create(
|
||||
GameState game,
|
||||
Map<UUID, PlayerState> players,
|
||||
Set<UUID> pendingInvitationIds,
|
||||
Map<UUID, FollowerPresence> presence,
|
||||
int strengthCap,
|
||||
int resistanceCap
|
||||
) {
|
||||
UUID vigilanteId = game.vigilanteId().orElseThrow();
|
||||
List<VigilanteFollowerView> followers = players.values().stream()
|
||||
.filter(player -> player.followerOf().filter(vigilanteId::equals).isPresent())
|
||||
.map(player -> new VigilanteFollowerView(
|
||||
player.playerId(), player.latestName(), player.optedOutUntil().isPresent(),
|
||||
presence.getOrDefault(
|
||||
player.playerId(), new FollowerPresence(false, false, false, false)
|
||||
)
|
||||
))
|
||||
.sorted(Comparator.comparing(VigilanteFollowerView::name))
|
||||
.toList();
|
||||
Set<String> pending = pendingInvitationIds.stream()
|
||||
.map(players::get)
|
||||
.filter(java.util.Objects::nonNull)
|
||||
.map(PlayerState::latestName)
|
||||
.collect(Collectors.toUnmodifiableSet());
|
||||
int nearby = (int) followers.stream()
|
||||
.filter(VigilanteFollowerView::contributes)
|
||||
.count();
|
||||
VigilantePower.Levels levels = VigilantePower.forFollowers(
|
||||
nearby, strengthCap, resistanceCap
|
||||
);
|
||||
return new VigilanteControlPanelModel(
|
||||
followers, pending, nearby, levels.strength(), levels.resistance(),
|
||||
strengthCap, resistanceCap
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface VigilanteControlPanelRenderer {
|
||||
void openOverview(Player player, VigilanteControlPanelModel model);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public record VigilanteFollowerView(
|
||||
UUID playerId,
|
||||
String name,
|
||||
boolean optedOut,
|
||||
FollowerPresence presence
|
||||
) {
|
||||
public boolean contributes() {
|
||||
return !optedOut && presence.contributes();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.command.TabCompleter;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
public final class VigilanteTabCompleter implements TabCompleter {
|
||||
private static final List<String> SUBCOMMANDS = List.of(
|
||||
"menu", "item", "invite", "accept", "dismiss", "leave"
|
||||
);
|
||||
private final TyrantStateManager stateManager;
|
||||
private final FollowerService followers;
|
||||
private final OnlinePlayerDirectory onlinePlayers;
|
||||
|
||||
public VigilanteTabCompleter(
|
||||
TyrantStateManager stateManager,
|
||||
FollowerService followers,
|
||||
OnlinePlayerDirectory onlinePlayers
|
||||
) {
|
||||
this.stateManager = stateManager;
|
||||
this.followers = followers;
|
||||
this.onlinePlayers = onlinePlayers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> onTabComplete(
|
||||
CommandSender sender,
|
||||
Command command,
|
||||
String alias,
|
||||
String[] arguments
|
||||
) {
|
||||
if (!(sender instanceof Player player) || arguments.length == 0) {
|
||||
return List.of();
|
||||
}
|
||||
if (arguments.length == 1) {
|
||||
return TabSuggestions.matching(arguments[0], SUBCOMMANDS);
|
||||
}
|
||||
if (arguments.length != 2) {
|
||||
return List.of();
|
||||
}
|
||||
return switch (arguments[0].toLowerCase(java.util.Locale.ROOT)) {
|
||||
case "invite" -> TabSuggestions.matching(
|
||||
arguments[1], eligibleInvitees(player)
|
||||
);
|
||||
case "dismiss" -> TabSuggestions.matching(
|
||||
arguments[1], currentFollowers(player)
|
||||
);
|
||||
default -> List.of();
|
||||
};
|
||||
}
|
||||
|
||||
private List<String> eligibleInvitees(Player vigilante) {
|
||||
GameState game = stateManager.game();
|
||||
Map<UUID, PlayerState> states = stateManager.players();
|
||||
Set<UUID> pending = followers.invitedPlayerIds(vigilante.getUniqueId());
|
||||
List<String> names = new ArrayList<>();
|
||||
for (UUID playerId : onlinePlayers.onlinePlayerIds()) {
|
||||
PlayerState state = states.get(playerId);
|
||||
boolean assignedRole = game.tyrantId().filter(playerId::equals).isPresent()
|
||||
|| game.vigilanteId().filter(playerId::equals).isPresent();
|
||||
boolean ineligibleState = state != null
|
||||
&& (state.optedOutUntil().isPresent() || state.followerOf().isPresent());
|
||||
if (assignedRole || ineligibleState || pending.contains(playerId)) {
|
||||
continue;
|
||||
}
|
||||
Player candidate = onlinePlayers.findById(playerId);
|
||||
if (candidate != null) {
|
||||
names.add(candidate.getName());
|
||||
}
|
||||
}
|
||||
return List.copyOf(names);
|
||||
}
|
||||
|
||||
private List<String> currentFollowers(Player vigilante) {
|
||||
Map<UUID, PlayerState> states = stateManager.players();
|
||||
List<String> names = new ArrayList<>();
|
||||
for (UUID playerId : onlinePlayers.onlinePlayerIds()) {
|
||||
PlayerState state = states.get(playerId);
|
||||
if (state == null
|
||||
|| state.followerOf().filter(vigilante.getUniqueId()::equals).isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
Player candidate = onlinePlayers.findById(playerId);
|
||||
if (candidate != null) {
|
||||
names.add(candidate.getName());
|
||||
}
|
||||
}
|
||||
return List.copyOf(names);
|
||||
}
|
||||
}
|
||||
@@ -50,6 +50,14 @@ tyrant-resistance-level: 1
|
||||
follower-strength-cap: 5
|
||||
follower-resistance-cap: 4
|
||||
|
||||
# Role control items
|
||||
tyrant-control-item:
|
||||
material: NETHER_STAR
|
||||
name: Tyrant Control
|
||||
vigilante-control-item:
|
||||
material: COMPASS
|
||||
name: Vigilante Control
|
||||
|
||||
# Bound item recovery and delivery messages
|
||||
recovery-command: /tyrant item
|
||||
inventory-full-message: Your inventory is full. Make room and use /tyrant item.
|
||||
|
||||
@@ -10,7 +10,7 @@ commands:
|
||||
usage: /tyrant <menu|status|choices|buy|assign|item|intelligence|optout|optin|relinquish confirm>
|
||||
vigilante:
|
||||
description: Manage Vigilante Followers.
|
||||
usage: /vigilante <invite <player>|accept|dismiss <player>|leave>
|
||||
usage: /vigilante <menu|item|invite <player>|accept|dismiss <player>|leave>
|
||||
tyrantadmin:
|
||||
description: Administer the Spigot Tyrant game.
|
||||
usage: /tyrantadmin <status|start|pause|resume|reset confirm>
|
||||
|
||||
@@ -28,6 +28,21 @@ final class CapturedMobServiceTest {
|
||||
assertEquals(java.util.List.of(), released.capturedMobs());
|
||||
}
|
||||
|
||||
@Test
|
||||
void tamerMustReleaseHeldMobBeforeCapturingAnother() {
|
||||
PlayerState holding = service.capture(
|
||||
tamer(), UUID.randomUUID(), "COW", "cow-snapshot"
|
||||
);
|
||||
|
||||
IllegalStateException exception = assertThrows(IllegalStateException.class, () ->
|
||||
service.capture(holding, UUID.randomUUID(), "SHEEP", "sheep-snapshot")
|
||||
);
|
||||
|
||||
assertEquals("release the currently captured mob before capturing another",
|
||||
exception.getMessage());
|
||||
assertEquals(1, holding.capturedMobs().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void enderDragonCanNeverBeCapturedAndMissingReleaseDoesNotMutate() {
|
||||
assertThrows(IllegalArgumentException.class, () -> service.capture(
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
final class DefaultCapturedMobCustodyReleaseTest {
|
||||
@Test
|
||||
void legacyExcessReleaseRetainsOneAndAnyFailedExtra() {
|
||||
UUID playerId = UUID.fromString("33333333-3333-3333-3333-333333333333");
|
||||
CapturedMob retained = mob("COW");
|
||||
CapturedMob releasedExtra = mob("SHEEP");
|
||||
CapturedMob deferredExtra = mob("PIG");
|
||||
PlayerState state = new PlayerState(
|
||||
playerId, "Tamer", Optional.empty(), Optional.empty(), TyrantClass.TAMER,
|
||||
Optional.empty(), Map.of(), java.util.Set.of(),
|
||||
List.of(retained, releasedExtra, deferredExtra)
|
||||
);
|
||||
CapturedMobSpawner spawner = mock(CapturedMobSpawner.class);
|
||||
Player player = mock(Player.class);
|
||||
when(spawner.spawn(player, releasedExtra)).thenReturn(true);
|
||||
when(spawner.spawn(player, deferredExtra)).thenReturn(false);
|
||||
DefaultCapturedMobCustodyRelease service = new DefaultCapturedMobCustodyRelease(spawner);
|
||||
|
||||
PlayerState result = service.releaseExcess(player, state, 1);
|
||||
|
||||
assertEquals(List.of(retained, deferredExtra), result.capturedMobs());
|
||||
}
|
||||
|
||||
@Test
|
||||
void removesOnlyCustodyThatSpawnedSuccessfully() {
|
||||
UUID playerId = UUID.fromString("33333333-3333-3333-3333-333333333333");
|
||||
CapturedMob released = mob("COW");
|
||||
CapturedMob deferred = mob("SHEEP");
|
||||
PlayerState state = new PlayerState(
|
||||
playerId, "Former", Optional.empty(), Optional.empty(), TyrantClass.NONE,
|
||||
Optional.empty(), Map.of(), java.util.Set.of(), List.of(released, deferred)
|
||||
);
|
||||
CapturedMobSpawner spawner = mock(CapturedMobSpawner.class);
|
||||
Player player = mock(Player.class);
|
||||
when(spawner.spawn(player, released)).thenReturn(true);
|
||||
when(spawner.spawn(player, deferred)).thenReturn(false);
|
||||
DefaultCapturedMobCustodyRelease service = new DefaultCapturedMobCustodyRelease(spawner);
|
||||
|
||||
PlayerState result = service.releaseAll(player, state);
|
||||
|
||||
assertEquals(List.of(deferred), result.capturedMobs());
|
||||
assertEquals(TyrantClass.NONE, result.tyrantClass());
|
||||
}
|
||||
|
||||
private static CapturedMob mob(String type) {
|
||||
return new CapturedMob(type, Map.of(
|
||||
"capture-id", UUID.randomUUID().toString(), "snapshot", "snapshot-" + type
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
final class DefaultVigilanteControlPanelTest {
|
||||
@Test
|
||||
void activeVigilanteOpensAuthoritativeOverview() {
|
||||
UUID tyrantId = UUID.fromString("11111111-1111-1111-1111-111111111111");
|
||||
UUID vigilanteId = UUID.fromString("22222222-2222-2222-2222-222222222222");
|
||||
GameState game = game(tyrantId, vigilanteId);
|
||||
PlayerState vigilanteState = PlayerState.newPlayer(vigilanteId, "Vigilante");
|
||||
TyrantStateManager manager = mock(TyrantStateManager.class);
|
||||
when(manager.game()).thenReturn(game);
|
||||
when(manager.players()).thenReturn(Map.of(vigilanteId, vigilanteState));
|
||||
Player vigilante = mock(Player.class);
|
||||
when(vigilante.getUniqueId()).thenReturn(vigilanteId);
|
||||
VigilanteControlPanelRenderer renderer = mock(VigilanteControlPanelRenderer.class);
|
||||
FollowerPresenceProvider presence = mock(FollowerPresenceProvider.class);
|
||||
when(presence.inspect(any(), any())).thenReturn(Map.of());
|
||||
DefaultVigilanteControlPanel panel = new DefaultVigilanteControlPanel(
|
||||
manager, new FollowerService(), renderer, presence, 5, 4
|
||||
);
|
||||
|
||||
panel.open(vigilante);
|
||||
|
||||
verify(renderer).openOverview(org.mockito.ArgumentMatchers.eq(vigilante), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void nonVigilanteCannotRenderControlPanel() {
|
||||
UUID tyrantId = UUID.fromString("11111111-1111-1111-1111-111111111111");
|
||||
UUID vigilanteId = UUID.fromString("22222222-2222-2222-2222-222222222222");
|
||||
Player other = mock(Player.class);
|
||||
when(other.getUniqueId()).thenReturn(UUID.randomUUID());
|
||||
TyrantStateManager manager = mock(TyrantStateManager.class);
|
||||
when(manager.game()).thenReturn(game(tyrantId, vigilanteId));
|
||||
VigilanteControlPanelRenderer renderer = mock(VigilanteControlPanelRenderer.class);
|
||||
DefaultVigilanteControlPanel panel = new DefaultVigilanteControlPanel(
|
||||
manager, new FollowerService(), renderer, mock(FollowerPresenceProvider.class), 5, 4
|
||||
);
|
||||
|
||||
panel.open(other);
|
||||
|
||||
verify(renderer, never()).openOverview(any(), any());
|
||||
verify(other).sendMessage(org.mockito.ArgumentMatchers.contains("active Vigilante"));
|
||||
}
|
||||
|
||||
private static GameState game(UUID tyrantId, UUID vigilanteId) {
|
||||
return new GameState(
|
||||
GameLifecycle.RUNNING, Optional.of(tyrantId), Optional.of(vigilanteId),
|
||||
Optional.empty(), Optional.empty(), Optional.empty(), Duration.ZERO,
|
||||
0, 0, Set.of()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
@@ -34,6 +35,32 @@ final class FollowerServiceTest {
|
||||
assertEquals(TyrantClass.FIXER, accepted.players().get(FOLLOWER).tyrantClass());
|
||||
}
|
||||
|
||||
@Test
|
||||
void repeatedInvitationIsRejectedAndPendingInvitationCanBeInspected() {
|
||||
GameState game = game();
|
||||
PlayerState target = PlayerState.newPlayer(FOLLOWER, "Follower");
|
||||
Map<UUID, PlayerState> players = Map.of(FOLLOWER, target);
|
||||
|
||||
FollowerResult first = service.invite(game, players, VIGILANTE, FOLLOWER);
|
||||
FollowerResult repeated = service.invite(game, players, VIGILANTE, FOLLOWER);
|
||||
|
||||
assertEquals(FollowerStatus.INVITED, first.status());
|
||||
assertEquals(FollowerStatus.ALREADY_INVITED, repeated.status());
|
||||
assertTrue(service.invitedPlayerIds(VIGILANTE).contains(FOLLOWER));
|
||||
}
|
||||
|
||||
@Test
|
||||
void existingFollowerCannotReceiveAnotherInvitation() {
|
||||
GameState game = game();
|
||||
PlayerState follower = withFollower(PlayerState.newPlayer(FOLLOWER, "Follower"));
|
||||
|
||||
FollowerResult result = service.invite(
|
||||
game, Map.of(FOLLOWER, follower), VIGILANTE, FOLLOWER
|
||||
);
|
||||
|
||||
assertEquals(FollowerStatus.INELIGIBLE, result.status());
|
||||
}
|
||||
|
||||
@Test
|
||||
void vigilanteCanDismissAndFollowerCanLeave() {
|
||||
GameState game = game();
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import org.bukkit.Server;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
final class FormerTamerReleaseTaskTest {
|
||||
@Test
|
||||
void offlineFormerTamerCustodyRemainsDeferred() {
|
||||
UUID formerId = UUID.fromString("33333333-3333-3333-3333-333333333333");
|
||||
PlayerState former = withMob(PlayerState.newPlayer(formerId, "Former"));
|
||||
TyrantStateManager manager = mock(TyrantStateManager.class);
|
||||
when(manager.players()).thenReturn(Map.of(formerId, former));
|
||||
Server server = mock(Server.class);
|
||||
doReturn(Set.of()).when(server).getOnlinePlayers();
|
||||
CapturedMobCustodyRelease release = mock(CapturedMobCustodyRelease.class);
|
||||
|
||||
new FormerTamerReleaseTask(manager, server, release).run();
|
||||
|
||||
verify(release, never()).releaseAll(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void safelyReleasesLegacyCustodyAboveOneForActiveTamer() {
|
||||
UUID tamerId = UUID.fromString("44444444-4444-4444-4444-444444444444");
|
||||
PlayerState once = withMob(withClass(
|
||||
PlayerState.newPlayer(tamerId, "Tamer"), TyrantClass.TAMER
|
||||
));
|
||||
PlayerState legacy = withMob(once);
|
||||
TyrantStateManager manager = mock(TyrantStateManager.class);
|
||||
when(manager.players()).thenReturn(Map.of(tamerId, legacy));
|
||||
Player tamer = mock(Player.class);
|
||||
when(tamer.getUniqueId()).thenReturn(tamerId);
|
||||
Server server = mock(Server.class);
|
||||
doReturn(Set.of(tamer)).when(server).getOnlinePlayers();
|
||||
CapturedMobCustodyRelease release = mock(CapturedMobCustodyRelease.class);
|
||||
when(release.releaseExcess(tamer, legacy, 1)).thenReturn(legacy);
|
||||
|
||||
new FormerTamerReleaseTask(manager, server, release).run();
|
||||
|
||||
verify(release).releaseExcess(tamer, legacy, 1);
|
||||
verify(release, never()).releaseAll(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void releasesCustodyOnlyAfterPlayerLosesTamerClass() {
|
||||
UUID formerId = UUID.fromString("33333333-3333-3333-3333-333333333333");
|
||||
UUID tamerId = UUID.fromString("44444444-4444-4444-4444-444444444444");
|
||||
PlayerState former = withMob(PlayerState.newPlayer(formerId, "Former"));
|
||||
PlayerState tamer = withMob(withClass(
|
||||
PlayerState.newPlayer(tamerId, "Tamer"), TyrantClass.TAMER
|
||||
));
|
||||
TyrantStateManager manager = mock(TyrantStateManager.class);
|
||||
when(manager.players()).thenReturn(Map.of(formerId, former, tamerId, tamer));
|
||||
Player formerPlayer = mock(Player.class);
|
||||
when(formerPlayer.getUniqueId()).thenReturn(formerId);
|
||||
Player tamerPlayer = mock(Player.class);
|
||||
when(tamerPlayer.getUniqueId()).thenReturn(tamerId);
|
||||
Server server = mock(Server.class);
|
||||
doReturn(Set.of(formerPlayer, tamerPlayer)).when(server).getOnlinePlayers();
|
||||
CapturedMobCustodyRelease release = mock(CapturedMobCustodyRelease.class);
|
||||
when(release.releaseAll(formerPlayer, former)).thenReturn(former);
|
||||
FormerTamerReleaseTask task = new FormerTamerReleaseTask(manager, server, release);
|
||||
|
||||
task.run();
|
||||
|
||||
verify(release).releaseAll(formerPlayer, former);
|
||||
verify(release, never()).releaseAll(tamerPlayer, tamer);
|
||||
}
|
||||
|
||||
private static PlayerState withMob(PlayerState player) {
|
||||
CapturedMob mob = new CapturedMob("COW", Map.of(
|
||||
"capture-id", UUID.randomUUID().toString(), "snapshot", "snapshot"
|
||||
));
|
||||
java.util.List<CapturedMob> custody = new java.util.ArrayList<>(player.capturedMobs());
|
||||
custody.add(mob);
|
||||
return new PlayerState(
|
||||
player.playerId(), player.latestName(), player.lastLogin(), player.optedOutUntil(),
|
||||
player.tyrantClass(), player.followerOf(), player.cooldownEnds(),
|
||||
player.readyAbilityItems(), custody
|
||||
);
|
||||
}
|
||||
|
||||
private static PlayerState withClass(PlayerState player, TyrantClass tyrantClass) {
|
||||
return new PlayerState(
|
||||
player.playerId(), player.latestName(), player.lastLogin(), player.optedOutUntil(),
|
||||
tyrantClass, player.followerOf(), player.cooldownEnds(),
|
||||
player.readyAbilityItems(), player.capturedMobs()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -43,6 +43,10 @@ final class PluginSettingsTest {
|
||||
assertEquals("STICK", settings.assassinItem().material());
|
||||
assertEquals("Assassin Cloak", settings.assassinItem().name());
|
||||
assertEquals("FISHING_ROD", settings.tamerItem().material());
|
||||
assertEquals("NETHER_STAR", settings.tyrantControlItem().material());
|
||||
assertEquals("Tyrant Control", settings.tyrantControlItem().name());
|
||||
assertEquals("COMPASS", settings.vigilanteControlItem().material());
|
||||
assertEquals("Vigilante Control", settings.vigilanteControlItem().name());
|
||||
assertEquals("/tyrant item", settings.recoveryCommand());
|
||||
assertEquals("Your inventory is full. Make room and use /tyrant item.",
|
||||
settings.inventoryFullMessage());
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
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.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import org.bukkit.entity.Item;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.block.Action;
|
||||
import org.bukkit.event.inventory.InventoryMoveItemEvent;
|
||||
import org.bukkit.event.player.PlayerDropItemEvent;
|
||||
import org.bukkit.event.player.PlayerInteractEvent;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
final class RoleControlItemListenerTest {
|
||||
@Test
|
||||
void droppedRoleControlItemVanishes() {
|
||||
ItemStack item = mock(ItemStack.class);
|
||||
Item dropped = mock(Item.class);
|
||||
when(dropped.getItemStack()).thenReturn(item);
|
||||
PlayerDropItemEvent event = mock(PlayerDropItemEvent.class);
|
||||
when(event.getItemDrop()).thenReturn(dropped);
|
||||
RoleControlItemService items = mock(RoleControlItemService.class);
|
||||
when(items.isBoundRoleControlItem(item)).thenReturn(true);
|
||||
RoleControlItemListener listener = listener(items);
|
||||
|
||||
listener.onDrop(event);
|
||||
|
||||
verify(dropped).remove();
|
||||
}
|
||||
|
||||
@Test
|
||||
void automatedInventoryTransferIsBlocked() {
|
||||
ItemStack item = mock(ItemStack.class);
|
||||
InventoryMoveItemEvent event = mock(InventoryMoveItemEvent.class);
|
||||
when(event.getItem()).thenReturn(item);
|
||||
RoleControlItemService items = mock(RoleControlItemService.class);
|
||||
when(items.isBoundRoleControlItem(item)).thenReturn(true);
|
||||
RoleControlItemListener listener = listener(items);
|
||||
|
||||
listener.onInventoryMove(event);
|
||||
|
||||
verify(event).setCancelled(true);
|
||||
}
|
||||
|
||||
@Test
|
||||
void currentTyrantUsesBoundItemToOpenControlPanel() {
|
||||
UUID tyrantId = UUID.fromString("11111111-1111-1111-1111-111111111111");
|
||||
GameState game = new GameState(
|
||||
GameLifecycle.RUNNING, Optional.of(tyrantId), Optional.empty(),
|
||||
Optional.empty(), Optional.empty(), Optional.empty(), Duration.ZERO,
|
||||
0, 0, Set.of()
|
||||
);
|
||||
TyrantStateManager manager = mock(TyrantStateManager.class);
|
||||
when(manager.game()).thenReturn(game);
|
||||
Player tyrant = mock(Player.class);
|
||||
when(tyrant.getUniqueId()).thenReturn(tyrantId);
|
||||
ItemStack item = mock(ItemStack.class);
|
||||
PlayerInteractEvent event = mock(PlayerInteractEvent.class);
|
||||
when(event.getPlayer()).thenReturn(tyrant);
|
||||
when(event.getItem()).thenReturn(item);
|
||||
when(event.getAction()).thenReturn(Action.RIGHT_CLICK_AIR);
|
||||
RoleControlItemService items = mock(RoleControlItemService.class);
|
||||
when(items.isBoundRoleControlItem(item)).thenReturn(true);
|
||||
when(items.owner(item)).thenReturn(Optional.of(tyrantId));
|
||||
when(items.role(item)).thenReturn(Optional.of(RoleControl.TYRANT));
|
||||
TyrantControlPanel tyrantPanel = mock(TyrantControlPanel.class);
|
||||
RoleControlItemListener listener = new RoleControlItemListener(
|
||||
manager, items, tyrantPanel, mock(VigilanteControlPanel.class)
|
||||
);
|
||||
|
||||
listener.onInteract(event);
|
||||
|
||||
verify(event).setCancelled(true);
|
||||
verify(tyrantPanel).open(tyrant);
|
||||
}
|
||||
|
||||
private static RoleControlItemListener listener(RoleControlItemService items) {
|
||||
return new RoleControlItemListener(
|
||||
mock(TyrantStateManager.class), items, mock(TyrantControlPanel.class),
|
||||
mock(VigilanteControlPanel.class)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
final class RoleControlItemPolicyTest {
|
||||
@Test
|
||||
void runningRoleHoldersRequireOnlyTheirOwnControlItem() {
|
||||
UUID tyrantId = UUID.fromString("11111111-1111-1111-1111-111111111111");
|
||||
UUID vigilanteId = UUID.fromString("22222222-2222-2222-2222-222222222222");
|
||||
GameState game = new GameState(
|
||||
GameLifecycle.RUNNING, Optional.of(tyrantId), Optional.of(vigilanteId),
|
||||
Optional.empty(), Optional.empty(), Optional.empty(), Duration.ZERO,
|
||||
0, 0, Set.of()
|
||||
);
|
||||
|
||||
assertEquals(Optional.of(RoleControl.TYRANT),
|
||||
RoleControlItemPolicy.requiredRole(game, tyrantId));
|
||||
assertEquals(Optional.of(RoleControl.VIGILANTE),
|
||||
RoleControlItemPolicy.requiredRole(game, vigilanteId));
|
||||
assertEquals(Optional.empty(), RoleControlItemPolicy.requiredRole(game, UUID.randomUUID()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void pausedRoleHolderRetainsControlItem() {
|
||||
UUID tyrantId = UUID.fromString("11111111-1111-1111-1111-111111111111");
|
||||
GameState game = new GameState(
|
||||
GameLifecycle.PAUSED, Optional.of(tyrantId), Optional.empty(),
|
||||
Optional.empty(), Optional.empty(), Optional.of(java.time.Instant.EPOCH),
|
||||
Duration.ZERO, 0, 0, Set.of()
|
||||
);
|
||||
|
||||
assertEquals(Optional.of(RoleControl.TYRANT),
|
||||
RoleControlItemPolicy.requiredRole(game, tyrantId));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
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.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import org.bukkit.Server;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
final class RoleControlItemRefreshTaskTest {
|
||||
@Test
|
||||
void reconciliationRemovesInvalidCopiesAndRecoversRequiredRoleItem() {
|
||||
UUID tyrantId = UUID.fromString("11111111-1111-1111-1111-111111111111");
|
||||
GameState game = new GameState(
|
||||
GameLifecycle.RUNNING, Optional.of(tyrantId), Optional.empty(),
|
||||
Optional.empty(), Optional.empty(), Optional.empty(), Duration.ZERO,
|
||||
0, 0, Set.of()
|
||||
);
|
||||
TyrantStateManager manager = mock(TyrantStateManager.class);
|
||||
when(manager.game()).thenReturn(game);
|
||||
Player tyrant = mock(Player.class);
|
||||
Server server = mock(Server.class);
|
||||
doReturn(Set.of(tyrant)).when(server).getOnlinePlayers();
|
||||
RoleControlItemService items = mock(RoleControlItemService.class);
|
||||
|
||||
new RoleControlItemRefreshTask(manager, items, server).run();
|
||||
|
||||
verify(items).removeInvalid(tyrant, game);
|
||||
verify(items).recover(tyrant, game);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import org.bukkit.Server;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.player.PlayerInteractEntityEvent;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.PlayerInventory;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
final class TamerListenerTest {
|
||||
@Test
|
||||
void heldMobRejectsAnotherCaptureBeforeTargetOrInventoryChanges() {
|
||||
UUID playerId = UUID.fromString("33333333-3333-3333-3333-333333333333");
|
||||
CapturedMob heldMob = new CapturedMob("COW", Map.of(
|
||||
"capture-id", UUID.randomUUID().toString(), "snapshot", "cow"
|
||||
));
|
||||
PlayerState state = new PlayerState(
|
||||
playerId, "Tamer", Optional.empty(), Optional.empty(), TyrantClass.TAMER,
|
||||
Optional.empty(), Map.of(), Set.of(Ability.TAMER_CAPTURE),
|
||||
java.util.List.of(heldMob)
|
||||
);
|
||||
Player player = mock(Player.class);
|
||||
when(player.getUniqueId()).thenReturn(playerId);
|
||||
when(player.getName()).thenReturn("Tamer");
|
||||
PlayerInventory inventory = mock(PlayerInventory.class);
|
||||
when(player.getInventory()).thenReturn(inventory);
|
||||
ItemStack lead = mock(ItemStack.class);
|
||||
when(inventory.getItem(org.bukkit.inventory.EquipmentSlot.HAND)).thenReturn(lead);
|
||||
Entity target = mock(Entity.class);
|
||||
PlayerInteractEntityEvent event = mock(PlayerInteractEntityEvent.class);
|
||||
when(event.getPlayer()).thenReturn(player);
|
||||
when(event.getHand()).thenReturn(org.bukkit.inventory.EquipmentSlot.HAND);
|
||||
when(event.getRightClicked()).thenReturn(target);
|
||||
AbilityItemService abilityItems = mock(AbilityItemService.class);
|
||||
when(abilityItems.ability(lead)).thenReturn(Optional.of(Ability.TAMER_CAPTURE));
|
||||
when(abilityItems.owner(lead)).thenReturn(Optional.of(playerId));
|
||||
TyrantStateManager manager = mock(TyrantStateManager.class);
|
||||
when(manager.player(playerId, "Tamer")).thenReturn(state);
|
||||
CapturedMobItemService capturedItems = mock(CapturedMobItemService.class);
|
||||
TamerListener listener = new TamerListener(
|
||||
manager, abilityItems, capturedItems, new CapturedMobService(),
|
||||
PluginSettings.from(Map.of()), mock(Server.class)
|
||||
);
|
||||
|
||||
listener.onCapture(event);
|
||||
|
||||
verify(player).sendMessage(org.mockito.ArgumentMatchers.contains(
|
||||
"Release the currently captured mob"
|
||||
));
|
||||
verify(target, never()).createSnapshot();
|
||||
verify(capturedItems, never()).create(
|
||||
org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.any(),
|
||||
org.mockito.ArgumentMatchers.anyString()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
final class TyrantAdminTabCompleterTest {
|
||||
@Test
|
||||
void permissionGatesAdministrativeSyntaxAndOnlineCandidates() {
|
||||
UUID alphaId = UUID.fromString("11111111-1111-1111-1111-111111111111");
|
||||
UUID betaId = UUID.fromString("22222222-2222-2222-2222-222222222222");
|
||||
Player alpha = player(alphaId, "Alpha");
|
||||
Player beta = player(betaId, "Beta");
|
||||
OnlinePlayerDirectory directory = mock(OnlinePlayerDirectory.class);
|
||||
when(directory.onlinePlayerIds()).thenReturn(Set.of(alphaId, betaId));
|
||||
when(directory.findById(alphaId)).thenReturn(alpha);
|
||||
when(directory.findById(betaId)).thenReturn(beta);
|
||||
TyrantAdminTabCompleter completer = new TyrantAdminTabCompleter(directory);
|
||||
Command command = mock(Command.class);
|
||||
CommandSender denied = mock(CommandSender.class);
|
||||
CommandSender admin = mock(CommandSender.class);
|
||||
when(admin.hasPermission("spigottyrant.admin")).thenReturn(true);
|
||||
|
||||
assertEquals(java.util.List.of(), completer.onTabComplete(
|
||||
denied, command, "tyrantadmin", new String[] {""}
|
||||
));
|
||||
assertTrue(completer.onTabComplete(
|
||||
admin, command, "tyrantadmin", new String[] {""}
|
||||
).containsAll(java.util.List.of("status", "start", "pause", "resume", "reset")));
|
||||
assertEquals(java.util.List.of("Alpha", "Beta"), completer.onTabComplete(
|
||||
admin, command, "tyrantadmin", new String[] {"start", ""}
|
||||
));
|
||||
assertEquals(java.util.List.of("Beta"), completer.onTabComplete(
|
||||
admin, command, "tyrantadmin", new String[] {"start", "Alpha", ""}
|
||||
));
|
||||
assertEquals(java.util.List.of("confirm"), completer.onTabComplete(
|
||||
admin, command, "tyrantadmin", new String[] {"reset", "c"}
|
||||
));
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -43,6 +43,28 @@ final class TyrantCommandTest {
|
||||
verify(controlPanel, org.mockito.Mockito.times(2)).open(tyrant);
|
||||
}
|
||||
|
||||
@Test
|
||||
void itemCommandRecoversTyrantRoleControlItem() {
|
||||
UUID tyrantId = UUID.fromString("11111111-1111-1111-1111-111111111111");
|
||||
GameState game = new GameState(
|
||||
GameLifecycle.RUNNING, Optional.of(tyrantId), Optional.empty(),
|
||||
Optional.empty(), Optional.empty(), Optional.empty(), Duration.ZERO,
|
||||
0, 0, Set.of()
|
||||
);
|
||||
TyrantStateManager manager = mock(TyrantStateManager.class);
|
||||
when(manager.game()).thenReturn(game);
|
||||
Player tyrant = mock(Player.class);
|
||||
when(tyrant.getUniqueId()).thenReturn(tyrantId);
|
||||
RoleControlItemService roleItems = mock(RoleControlItemService.class);
|
||||
TyrantCommand command = new TyrantCommand(
|
||||
manager, new TyrantProgressionService(), mock(TyrantControlPanel.class), roleItems
|
||||
);
|
||||
|
||||
command.onCommand(tyrant, mock(Command.class), "tyrant", new String[] {"item"});
|
||||
|
||||
verify(roleItems).recover(tyrant, game);
|
||||
}
|
||||
|
||||
@Test
|
||||
void choicesClearlyIdentifyPurchasedAvailableAndUnavailableUnlocks() {
|
||||
UUID tyrantId = UUID.fromString("11111111-1111-1111-1111-111111111111");
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
import java.util.List;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
final class TyrantTabCompleterTest {
|
||||
@Test
|
||||
void rootBuyAssignAndConfirmationSuggestionsFollowSyntaxAndPrefix() {
|
||||
TyrantTabCompleter completer = new TyrantTabCompleter(
|
||||
mock(TyrantStateManager.class), mock(OnlinePlayerDirectory.class)
|
||||
);
|
||||
Player player = mock(Player.class);
|
||||
Command command = mock(Command.class);
|
||||
|
||||
List<String> root = completer.onTabComplete(
|
||||
player, command, "tyrant", new String[] {""}
|
||||
);
|
||||
List<String> buy = completer.onTabComplete(
|
||||
player, command, "tyrant", new String[] {"buy", "ro"}
|
||||
);
|
||||
List<String> assign = completer.onTabComplete(
|
||||
player, command, "tyrant", new String[] {"assign", "f"}
|
||||
);
|
||||
List<String> confirm = completer.onTabComplete(
|
||||
player, command, "tyrant", new String[] {"relinquish", "c"}
|
||||
);
|
||||
|
||||
assertTrue(root.containsAll(List.of("menu", "status", "buy", "assign", "item")));
|
||||
assertFalse(root.contains("SomePlayer"));
|
||||
assertEquals(List.of("roster_intelligence"), buy);
|
||||
assertEquals(List.of("fixer"), assign);
|
||||
assertEquals(List.of("confirm"), confirm);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
final class VigilanteCommandTest {
|
||||
@Test
|
||||
void itemCommandRecoversVigilanteRoleControlItem() {
|
||||
UUID vigilanteId = UUID.fromString("22222222-2222-2222-2222-222222222222");
|
||||
GameState game = new GameState(
|
||||
GameLifecycle.RUNNING,
|
||||
Optional.of(UUID.fromString("11111111-1111-1111-1111-111111111111")),
|
||||
Optional.of(vigilanteId), Optional.empty(), Optional.empty(), Optional.empty(),
|
||||
Duration.ZERO, 0, 0, Set.of()
|
||||
);
|
||||
TyrantStateManager manager = mock(TyrantStateManager.class);
|
||||
when(manager.game()).thenReturn(game);
|
||||
Player vigilante = mock(Player.class);
|
||||
when(vigilante.getUniqueId()).thenReturn(vigilanteId);
|
||||
RoleControlItemService roleItems = mock(RoleControlItemService.class);
|
||||
VigilanteCommand command = new VigilanteCommand(
|
||||
manager, new FollowerService(), mock(OnlinePlayerDirectory.class),
|
||||
mock(VigilanteControlPanel.class), roleItems
|
||||
);
|
||||
|
||||
command.onCommand(vigilante, mock(Command.class), "vigilante", new String[] {"item"});
|
||||
|
||||
verify(roleItems).recover(vigilante, game);
|
||||
}
|
||||
|
||||
@Test
|
||||
void playerCanOpenControlPanelWithNoArgumentsOrMenuSubcommand() {
|
||||
UUID playerId = UUID.fromString("22222222-2222-2222-2222-222222222222");
|
||||
Player player = mock(Player.class);
|
||||
when(player.getUniqueId()).thenReturn(playerId);
|
||||
VigilanteControlPanel controlPanel = mock(VigilanteControlPanel.class);
|
||||
VigilanteCommand command = new VigilanteCommand(
|
||||
mock(TyrantStateManager.class),
|
||||
new FollowerService(),
|
||||
mock(OnlinePlayerDirectory.class),
|
||||
controlPanel
|
||||
);
|
||||
|
||||
command.onCommand(player, mock(Command.class), "vigilante", new String[0]);
|
||||
command.onCommand(player, mock(Command.class), "vigilante", new String[] {"menu"});
|
||||
|
||||
verify(controlPanel, times(2)).open(player);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.player.PlayerJoinEvent;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
final class VigilanteControlPanelJoinListenerTest {
|
||||
@Test
|
||||
void opensPanelOneHundredTicksAfterActiveVigilanteJoins() {
|
||||
UUID vigilanteId = UUID.fromString("22222222-2222-2222-2222-222222222222");
|
||||
TyrantStateManager manager = mock(TyrantStateManager.class);
|
||||
when(manager.game()).thenReturn(game(GameLifecycle.RUNNING, vigilanteId));
|
||||
Player vigilante = player(vigilanteId);
|
||||
PlayerJoinEvent event = mock(PlayerJoinEvent.class);
|
||||
when(event.getPlayer()).thenReturn(vigilante);
|
||||
DelayedTaskScheduler scheduler = mock(DelayedTaskScheduler.class);
|
||||
VigilanteControlPanel panel = mock(VigilanteControlPanel.class);
|
||||
VigilanteControlPanelJoinListener listener = new VigilanteControlPanelJoinListener(
|
||||
manager, panel, scheduler
|
||||
);
|
||||
|
||||
listener.onJoin(event);
|
||||
|
||||
ArgumentCaptor<Runnable> task = ArgumentCaptor.forClass(Runnable.class);
|
||||
verify(scheduler).schedule(task.capture(), org.mockito.ArgumentMatchers.eq(100L));
|
||||
task.getValue().run();
|
||||
verify(panel).open(vigilante);
|
||||
}
|
||||
|
||||
@Test
|
||||
void doesNotOpenAfterPauseOrWhenAlreadyOpenedManually() {
|
||||
UUID vigilanteId = UUID.fromString("22222222-2222-2222-2222-222222222222");
|
||||
Player vigilante = player(vigilanteId);
|
||||
PlayerJoinEvent event = mock(PlayerJoinEvent.class);
|
||||
when(event.getPlayer()).thenReturn(vigilante);
|
||||
DelayedTaskScheduler scheduler = mock(DelayedTaskScheduler.class);
|
||||
VigilanteControlPanel panel = mock(VigilanteControlPanel.class);
|
||||
when(panel.openedThisSession(vigilanteId)).thenReturn(true);
|
||||
TyrantStateManager manager = mock(TyrantStateManager.class);
|
||||
when(manager.game()).thenReturn(game(GameLifecycle.PAUSED, vigilanteId));
|
||||
VigilanteControlPanelJoinListener listener = new VigilanteControlPanelJoinListener(
|
||||
manager, panel, scheduler
|
||||
);
|
||||
|
||||
listener.onJoin(event);
|
||||
ArgumentCaptor<Runnable> task = ArgumentCaptor.forClass(Runnable.class);
|
||||
verify(scheduler).schedule(task.capture(), org.mockito.ArgumentMatchers.eq(100L));
|
||||
task.getValue().run();
|
||||
|
||||
verify(panel, never()).open(vigilante);
|
||||
}
|
||||
|
||||
private static Player player(UUID id) {
|
||||
Player player = mock(Player.class);
|
||||
when(player.getUniqueId()).thenReturn(id);
|
||||
when(player.isOnline()).thenReturn(true);
|
||||
return player;
|
||||
}
|
||||
|
||||
private static GameState game(GameLifecycle lifecycle, UUID vigilanteId) {
|
||||
return new GameState(
|
||||
lifecycle,
|
||||
Optional.of(UUID.fromString("11111111-1111-1111-1111-111111111111")),
|
||||
Optional.of(vigilanteId), Optional.empty(), Optional.empty(),
|
||||
lifecycle == GameLifecycle.PAUSED
|
||||
? Optional.of(java.time.Instant.EPOCH) : Optional.empty(),
|
||||
Duration.ZERO, 0, 0, Set.of()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
final class VigilanteControlPanelModelTest {
|
||||
@Test
|
||||
void overviewReportsFollowersInvitationsAndCappedNearbyPower() {
|
||||
UUID tyrantId = UUID.fromString("11111111-1111-1111-1111-111111111111");
|
||||
UUID vigilanteId = UUID.fromString("22222222-2222-2222-2222-222222222222");
|
||||
UUID nearbyId = UUID.fromString("33333333-3333-3333-3333-333333333333");
|
||||
UUID offlineId = UUID.fromString("44444444-4444-4444-4444-444444444444");
|
||||
UUID invitedId = UUID.fromString("55555555-5555-5555-5555-555555555555");
|
||||
GameState game = new GameState(
|
||||
GameLifecycle.RUNNING, Optional.of(tyrantId), Optional.of(vigilanteId),
|
||||
Optional.empty(), Optional.empty(), Optional.empty(), Duration.ZERO,
|
||||
0, 0, Set.of()
|
||||
);
|
||||
PlayerState nearby = follower(nearbyId, "Nearby", vigilanteId);
|
||||
PlayerState offline = follower(offlineId, "Offline", vigilanteId);
|
||||
PlayerState invited = PlayerState.newPlayer(invitedId, "Invited");
|
||||
|
||||
VigilanteControlPanelModel model = VigilanteControlPanelModel.create(
|
||||
game,
|
||||
Map.of(nearbyId, nearby, offlineId, offline, invitedId, invited),
|
||||
Set.of(invitedId),
|
||||
Map.of(
|
||||
nearbyId, new FollowerPresence(true, true, true, true),
|
||||
offlineId, new FollowerPresence(false, false, false, false)
|
||||
),
|
||||
5,
|
||||
4
|
||||
);
|
||||
|
||||
assertEquals(2, model.followers().size());
|
||||
assertEquals(Set.of("Invited"), model.pendingInvitations());
|
||||
assertEquals(1, model.nearbyEligibleFollowers());
|
||||
assertEquals(1, model.strengthLevel());
|
||||
assertEquals(1, model.resistanceLevel());
|
||||
assertEquals(5, model.strengthCap());
|
||||
assertEquals(4, model.resistanceCap());
|
||||
}
|
||||
|
||||
private static PlayerState follower(UUID id, String name, UUID vigilanteId) {
|
||||
PlayerState player = PlayerState.newPlayer(id, name);
|
||||
return new PlayerState(
|
||||
player.playerId(), player.latestName(), player.lastLogin(), player.optedOutUntil(),
|
||||
player.tyrantClass(), Optional.of(vigilanteId), player.cooldownEnds(),
|
||||
player.readyAbilityItems(), player.capturedMobs()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
final class VigilanteTabCompleterTest {
|
||||
@Test
|
||||
void rootAndPlayerArgumentsUseMembershipEligibility() {
|
||||
UUID tyrantId = UUID.fromString("11111111-1111-1111-1111-111111111111");
|
||||
UUID vigilanteId = UUID.fromString("22222222-2222-2222-2222-222222222222");
|
||||
UUID eligibleId = UUID.fromString("33333333-3333-3333-3333-333333333333");
|
||||
UUID followerId = UUID.fromString("44444444-4444-4444-4444-444444444444");
|
||||
PlayerState eligible = PlayerState.newPlayer(eligibleId, "Eligible");
|
||||
PlayerState follower = follower(followerId, "Follower", vigilanteId);
|
||||
TyrantStateManager manager = mock(TyrantStateManager.class);
|
||||
when(manager.game()).thenReturn(new GameState(
|
||||
GameLifecycle.RUNNING, Optional.of(tyrantId), Optional.of(vigilanteId),
|
||||
Optional.empty(), Optional.empty(), Optional.empty(), Duration.ZERO,
|
||||
0, 0, Set.of()
|
||||
));
|
||||
when(manager.players()).thenReturn(Map.of(eligibleId, eligible, followerId, follower));
|
||||
OnlinePlayerDirectory directory = mock(OnlinePlayerDirectory.class);
|
||||
when(directory.onlinePlayerIds()).thenReturn(Set.of(eligibleId, followerId, tyrantId));
|
||||
Player eligiblePlayer = player(eligibleId, "Eligible");
|
||||
Player followerPlayer = player(followerId, "Follower");
|
||||
Player tyrantPlayer = player(tyrantId, "Tyrant");
|
||||
when(directory.findById(eligibleId)).thenReturn(eligiblePlayer);
|
||||
when(directory.findById(followerId)).thenReturn(followerPlayer);
|
||||
when(directory.findById(tyrantId)).thenReturn(tyrantPlayer);
|
||||
VigilanteTabCompleter completer = new VigilanteTabCompleter(
|
||||
manager, new FollowerService(), directory
|
||||
);
|
||||
Player vigilante = player(vigilanteId, "Vigilante");
|
||||
Command command = mock(Command.class);
|
||||
|
||||
assertTrue(completer.onTabComplete(
|
||||
vigilante, command, "vigilante", new String[] {""}
|
||||
).containsAll(java.util.List.of("menu", "item", "invite", "dismiss")));
|
||||
assertEquals(java.util.List.of("Eligible"), completer.onTabComplete(
|
||||
vigilante, command, "vigilante", new String[] {"invite", ""}
|
||||
));
|
||||
assertEquals(java.util.List.of("Follower"), completer.onTabComplete(
|
||||
vigilante, command, "vigilante", new String[] {"dismiss", ""}
|
||||
));
|
||||
}
|
||||
|
||||
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 follower(UUID id, String name, UUID vigilanteId) {
|
||||
PlayerState state = PlayerState.newPlayer(id, name);
|
||||
return new PlayerState(
|
||||
id, name, state.lastLogin(), state.optedOutUntil(), state.tyrantClass(),
|
||||
Optional.of(vigilanteId), state.cooldownEnds(), state.readyAbilityItems(),
|
||||
state.capturedMobs()
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user