Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
34f32cd32d | ||
|
|
0b202f588d | ||
|
|
ec7d50c2b5 | ||
|
|
62f6352633 |
@@ -6,6 +6,75 @@ description: Chronological record of material decisions affecting the Spigot Tyr
|
|||||||
|
|
||||||
# Spigot Tyrant Design Log
|
# Spigot Tyrant Design Log
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
- The active Tyrant can open the panel with `/tyrant` or `/tyrant menu`; existing command alternatives remain available and provide the authoritative mutations behind menu actions.
|
||||||
|
- Added a 100-tick login delay with online, lifecycle, role, and already-opened session checks so only the current Tyrant receives one automatic panel per login.
|
||||||
|
- Verified command routing, authorization, overview state, delayed opening safeguards, compiler warnings, tests, and packaging with `./gradlew clean check jar`.
|
||||||
|
|
||||||
|
## 2026-08-21 — Tyrant control panel implementation started
|
||||||
|
|
||||||
|
- US-003, US-004, US-009, and US-015 begin a test-first implementation of the Tyrant control panel and its delayed login opening.
|
||||||
|
- The installed Spigot API does not expose a native dialog API, so the control panel will use a server-rendered inventory menu while preserving command alternatives.
|
||||||
|
|
||||||
|
## 2026-08-21 — Tyrant control panel planned
|
||||||
|
|
||||||
|
- Added US-015 for a graphical Tyrant control panel covering reign overview, unlock purchases, class assignments, and roster intelligence while retaining command alternatives.
|
||||||
|
- Planned automatic control-panel opening five seconds after the active Tyrant joins a running event, with lifecycle, role, disconnect, and duplicate-opening safeguards.
|
||||||
|
- Reopened US-003, US-004, and US-009 in the backlog to track their approved control-panel interactions.
|
||||||
|
|
||||||
## 2026-08-18 — Preserve ordinary potion effects
|
## 2026-08-18 — Preserve ordinary potion effects
|
||||||
|
|
||||||
- Corrected Assassin effect maintenance so it refreshes active Tyrant abilities without removing invisibility, Speed, or Weakness supplied by potions, commands, or other plugins.
|
- Corrected Assassin effect maintenance so it refreshes active Tyrant abilities without removing invisibility, Speed, or Weakness supplied by potions, commands, or other plugins.
|
||||||
|
|||||||
@@ -14,3 +14,6 @@
|
|||||||
12. [US-012: Configure and persist game state](us-012-configure-and-persist-state.md)
|
12. [US-012: Configure and persist game state](us-012-configure-and-persist-state.md)
|
||||||
13. [US-013: Build, test, and release the plugin](us-013-build-test-and-release.md)
|
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)
|
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)
|
||||||
|
|||||||
@@ -19,9 +19,13 @@ As the **Tyrant**, I want to earn one meaningful choice for defeating the Vigila
|
|||||||
- [x] A purchase consumes exactly one unspent choice and cannot be duplicated by retries or concurrent input.
|
- [x] A purchase consumes exactly one unspent choice and cannot be duplicated by retries or concurrent input.
|
||||||
- [x] Progress cannot be earned or spent while the game is paused.
|
- [x] Progress cannot be earned or spent while the game is paused.
|
||||||
- [x] Levels, purchases, and unspent choices survive logout and restart but reset when the reign ends.
|
- [x] Levels, purchases, and unspent choices survive logout and restart but reset when the reign ends.
|
||||||
|
- [x] The Tyrant can review and purchase unlocks through the Tyrant control panel.
|
||||||
|
- [x] A confirmation displays the selected unlock and choice cost before purchase.
|
||||||
|
- [x] The control panel refreshes the available choices immediately after a successful purchase.
|
||||||
|
|
||||||
## Related
|
## Related
|
||||||
|
|
||||||
- [Role and class model](../concepts/role-and-class-model.md)
|
- [Role and class model](../concepts/role-and-class-model.md)
|
||||||
- [Assign unlocked classes](us-004-assign-unlocked-classes.md)
|
- [Assign unlocked classes](us-004-assign-unlocked-classes.md)
|
||||||
- [Purchase and use Tyrant abilities](us-009-use-tyrant-abilities.md)
|
- [Purchase and use Tyrant abilities](us-009-use-tyrant-abilities.md)
|
||||||
|
- [Manage a reign through the Tyrant control panel](us-015-manage-tyrant-control-panel.md)
|
||||||
|
|||||||
@@ -19,8 +19,13 @@ As the **Tyrant**, I want to assign unlocked specialist classes so that I can bu
|
|||||||
- [x] Assignment and removal are stored by UUID and communicated clearly to affected players.
|
- [x] Assignment and removal are stored by UUID and communicated clearly to affected players.
|
||||||
- [x] A class holder is reminded of the assignment and its controls on login.
|
- [x] A class holder is reminded of the assignment and its controls on login.
|
||||||
- [x] All class assignments end when the Tyrant's reign ends.
|
- [x] All class assignments end when the Tyrant's reign ends.
|
||||||
|
- [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
|
## Related
|
||||||
|
|
||||||
- [Role and class model](../concepts/role-and-class-model.md)
|
- [Role and class model](../concepts/role-and-class-model.md)
|
||||||
- [Use class ability items](us-014-use-class-ability-items.md)
|
- [Use class ability items](us-014-use-class-ability-items.md)
|
||||||
|
- [Manage a reign through the Tyrant control panel](us-015-manage-tyrant-control-panel.md)
|
||||||
|
|||||||
@@ -21,6 +21,17 @@ 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] 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] 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] 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.
|
||||||
|
|
||||||
|
## 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. The complete `./gradlew clean check jar` lifecycle passes.
|
||||||
|
|
||||||
## Related
|
## 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] 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] 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 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
|
## Related
|
||||||
|
|
||||||
- [Role and class model](../concepts/role-and-class-model.md)
|
- [Role and class model](../concepts/role-and-class-model.md)
|
||||||
- [Handle inactivity and pending selections](us-010-handle-inactivity-and-selection.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)
|
||||||
|
|||||||
@@ -19,8 +19,11 @@ As the **Tyrant**, I want permanent upgrades and limited roster intelligence so
|
|||||||
- [x] Intelligence counts the current Vigilante and living assigned Followers, whether online or offline, according to configurable counting rules.
|
- [x] Intelligence counts the current Vigilante and living assigned Followers, whether online or offline, according to configurable counting rules.
|
||||||
- [x] The Tyrant can inspect ability ownership and the remaining intelligence cooldown.
|
- [x] The Tyrant can inspect ability ownership and the remaining intelligence cooldown.
|
||||||
- [x] Purchases and cooldowns survive restart and end with the Tyrant's reign.
|
- [x] Purchases and cooldowns survive restart and end with the Tyrant's reign.
|
||||||
|
- [x] The Tyrant control panel shows purchased passive upgrades and the roster-intelligence readiness or remaining cooldown.
|
||||||
|
- [x] The Tyrant can activate ready roster intelligence from the control panel and receive the result without revealing identities or locations.
|
||||||
|
|
||||||
## Related
|
## Related
|
||||||
|
|
||||||
- [Progress and purchase Tyrant unlocks](us-003-progress-and-purchase-unlocks.md)
|
- [Progress and purchase Tyrant unlocks](us-003-progress-and-purchase-unlocks.md)
|
||||||
- [Configure and persist game state](us-012-configure-and-persist-state.md)
|
- [Configure and persist game state](us-012-configure-and-persist-state.md)
|
||||||
|
- [Manage a reign through the Tyrant control panel](us-015-manage-tyrant-control-panel.md)
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
---
|
||||||
|
type: User Story
|
||||||
|
title: "US-015: Manage a reign through the Tyrant control panel"
|
||||||
|
description: Give the active Tyrant a graphical control panel for inspecting and managing the current reign.
|
||||||
|
status: done
|
||||||
|
---
|
||||||
|
|
||||||
|
# US-015: Manage a reign through the Tyrant control panel
|
||||||
|
|
||||||
|
As the **Tyrant**, I want a control panel for my reign so that I can inspect progress, purchase unlocks, assign classes, and use abilities without memorizing commands.
|
||||||
|
|
||||||
|
## Acceptance criteria
|
||||||
|
|
||||||
|
- [x] The active Tyrant can open the control panel with `/tyrant menu`, and `/tyrant` with no arguments opens the same panel.
|
||||||
|
- [x] A player who is not the active Tyrant cannot open or operate the control panel.
|
||||||
|
- [x] When the active Tyrant joins while the event is running, the control panel opens automatically five seconds after login.
|
||||||
|
- [x] Automatic opening is cancelled if the Tyrant disconnects, loses the Tyrant role, or the event pauses or stops during the five-second delay.
|
||||||
|
- [x] Other players and pending Tyrant candidates do not receive the automatically opened control panel.
|
||||||
|
- [x] Closing an automatically opened control panel does not cause it to reopen during the same login session.
|
||||||
|
- [x] The Tyrant may open the control panel manually during the five-second delay without causing a second panel to open automatically.
|
||||||
|
- [x] The main screen shows the Tyrant's level, unspent choices, purchased unlocks, class assignments, and roster-intelligence cooldown.
|
||||||
|
- [x] Purchased, available, unavailable, and cooldown-gated actions are visually distinguishable and explain why they cannot currently be used.
|
||||||
|
- [x] The Tyrant can navigate between overview, unlock, class-assignment, and ability screens and can close the panel without changing gameplay state.
|
||||||
|
- [x] The Tyrant can review and purchase an available unlock after confirming the selected unlock and its choice cost.
|
||||||
|
- [x] The Tyrant can select a purchased class and assign or reassign it to an eligible online player.
|
||||||
|
- [x] The class player selector excludes the Tyrant and clearly identifies players who are opted out or otherwise ineligible.
|
||||||
|
- [x] Class reassignment identifies the current holder and requires confirmation before replacing that holder.
|
||||||
|
- [x] The Tyrant can activate roster intelligence from the panel when the ability is purchased and ready.
|
||||||
|
- [x] A successful or rejected action refreshes the panel from authoritative current state and communicates the result clearly.
|
||||||
|
- [x] Panel actions use the same domain rules and persistence behavior as their command equivalents.
|
||||||
|
- [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
|
||||||
|
|
||||||
|
Automated tests verify command entry points, active-Tyrant authorization, authoritative overview state, the exact 100-tick login delay, lifecycle cancellation, and duplicate-opening prevention. The complete `./gradlew clean check jar` lifecycle passes.
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- [Progress and purchase Tyrant unlocks](us-003-progress-and-purchase-unlocks.md)
|
||||||
|
- [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,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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,436 @@
|
|||||||
|
package games.dmg.spigottyrant;
|
||||||
|
|
||||||
|
import java.time.Clock;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
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 BukkitTyrantControlPanelRenderer
|
||||||
|
implements TyrantControlPanelRenderer, Listener {
|
||||||
|
private static final String TITLE = ChatColor.DARK_RED + "Tyrant Control Panel";
|
||||||
|
private final Server server;
|
||||||
|
private final TyrantStateManager stateManager;
|
||||||
|
private final Clock clock;
|
||||||
|
|
||||||
|
public BukkitTyrantControlPanelRenderer(
|
||||||
|
Server server,
|
||||||
|
TyrantStateManager stateManager,
|
||||||
|
Clock clock
|
||||||
|
) {
|
||||||
|
this.server = server;
|
||||||
|
this.stateManager = stateManager;
|
||||||
|
this.clock = clock;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void openOverview(Player player, TyrantControlPanelModel model) {
|
||||||
|
MenuHolder holder = new MenuHolder(MenuView.OVERVIEW, null, null);
|
||||||
|
Inventory inventory = create(holder, 27, TITLE);
|
||||||
|
inventory.setItem(10, item(Material.NETHER_STAR, ChatColor.GOLD + "Reign Overview", List.of(
|
||||||
|
"Level: " + model.level(),
|
||||||
|
"Unspent choices: " + model.unspentChoices(),
|
||||||
|
"Purchased: " + (model.purchasedUnlocks().isEmpty()
|
||||||
|
? "none" : model.purchasedUnlocks())
|
||||||
|
)));
|
||||||
|
inventory.setItem(12, item(Material.ENCHANTED_BOOK, ChatColor.YELLOW + "Unlocks",
|
||||||
|
List.of("Review and purchase reign unlocks.")));
|
||||||
|
inventory.setItem(14, item(Material.ARMOR_STAND, ChatColor.AQUA + "Class Assignments",
|
||||||
|
classSummary(model)));
|
||||||
|
String intelligence = intelligenceText(model);
|
||||||
|
inventory.setItem(16, item(Material.SPYGLASS, ChatColor.LIGHT_PURPLE + "Roster Intelligence",
|
||||||
|
List.of(intelligence, "Click to activate when ready.")));
|
||||||
|
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 (!isActiveTyrant(player)) {
|
||||||
|
player.closeInventory();
|
||||||
|
player.sendMessage(ChatColor.RED + "This Tyrant control panel is no longer active.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
switch (holder.view()) {
|
||||||
|
case OVERVIEW -> clickOverview(player, event.getRawSlot());
|
||||||
|
case UNLOCKS -> clickUnlocks(player, event.getRawSlot());
|
||||||
|
case PURCHASE_CONFIRMATION -> clickPurchaseConfirmation(
|
||||||
|
player, event.getRawSlot(), holder.unlock()
|
||||||
|
);
|
||||||
|
case CLASSES -> clickClasses(player, event.getRawSlot());
|
||||||
|
case ABILITIES -> clickAbilities(player, event.getRawSlot());
|
||||||
|
case PLAYERS -> clickPlayers(player, event.getRawSlot(), holder.tyrantClass());
|
||||||
|
case ASSIGN_CONFIRMATION -> clickAssignmentConfirmation(
|
||||||
|
player, event.getRawSlot(), holder.tyrantClass(), holder.targetId()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void clickOverview(Player player, int slot) {
|
||||||
|
if (slot == 12) {
|
||||||
|
openUnlocks(player);
|
||||||
|
} else if (slot == 14) {
|
||||||
|
openClasses(player);
|
||||||
|
} else if (slot == 16) {
|
||||||
|
openAbilities(player);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openAbilities(Player player) {
|
||||||
|
TyrantControlPanelModel model = model(player);
|
||||||
|
MenuHolder holder = new MenuHolder(MenuView.ABILITIES, null, null);
|
||||||
|
Inventory inventory = create(holder, 27, TITLE + " — Abilities");
|
||||||
|
inventory.setItem(11, item(Material.BLAZE_POWDER, ChatColor.RED + "Permanent Strength",
|
||||||
|
List.of(model.purchasedUnlocks().contains(TyrantUnlock.STRENGTH)
|
||||||
|
? "Purchased and active." : "Not purchased.")));
|
||||||
|
inventory.setItem(13, item(Material.IRON_CHESTPLATE,
|
||||||
|
ChatColor.BLUE + "Permanent Resistance",
|
||||||
|
List.of(model.purchasedUnlocks().contains(TyrantUnlock.RESISTANCE)
|
||||||
|
? "Purchased and active." : "Not purchased.")));
|
||||||
|
inventory.setItem(15, item(Material.SPYGLASS, ChatColor.LIGHT_PURPLE
|
||||||
|
+ "Roster Intelligence", List.of(
|
||||||
|
intelligenceText(model), "Click to activate when ready."
|
||||||
|
)));
|
||||||
|
inventory.setItem(22, backItem());
|
||||||
|
player.openInventory(inventory);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void clickAbilities(Player player, int slot) {
|
||||||
|
if (slot == 22) {
|
||||||
|
refresh(player);
|
||||||
|
} else if (slot == 15) {
|
||||||
|
player.performCommand("tyrant intelligence");
|
||||||
|
openAbilities(player);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openUnlocks(Player player) {
|
||||||
|
TyrantControlPanelModel model = model(player);
|
||||||
|
MenuHolder holder = new MenuHolder(MenuView.UNLOCKS, null, null);
|
||||||
|
Inventory inventory = create(holder, 27, TITLE + " — Unlocks");
|
||||||
|
int[] slots = {10, 11, 12, 14, 15, 16};
|
||||||
|
TyrantUnlock[] unlocks = TyrantUnlock.values();
|
||||||
|
for (int index = 0; index < unlocks.length; index++) {
|
||||||
|
TyrantUnlock unlock = unlocks[index];
|
||||||
|
UnlockAvailability availability = model.unlocks().get(unlock);
|
||||||
|
Material material = switch (availability) {
|
||||||
|
case PURCHASED -> Material.LIME_DYE;
|
||||||
|
case AVAILABLE -> Material.GOLD_INGOT;
|
||||||
|
case UNAVAILABLE -> Material.GRAY_DYE;
|
||||||
|
};
|
||||||
|
inventory.setItem(slots[index], item(material,
|
||||||
|
ChatColor.YELLOW + readable(unlock.name()),
|
||||||
|
List.of("Status: " + availability, "Cost: 1 choice")));
|
||||||
|
}
|
||||||
|
inventory.setItem(22, backItem());
|
||||||
|
player.openInventory(inventory);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void clickUnlocks(Player player, int slot) {
|
||||||
|
if (slot == 22) {
|
||||||
|
refresh(player);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int[] slots = {10, 11, 12, 14, 15, 16};
|
||||||
|
TyrantUnlock[] unlocks = TyrantUnlock.values();
|
||||||
|
for (int index = 0; index < slots.length; index++) {
|
||||||
|
if (slot == slots[index]
|
||||||
|
&& model(player).unlocks().get(unlocks[index])
|
||||||
|
== UnlockAvailability.AVAILABLE) {
|
||||||
|
openPurchaseConfirmation(player, unlocks[index]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openPurchaseConfirmation(Player player, TyrantUnlock unlock) {
|
||||||
|
MenuHolder holder = new MenuHolder(MenuView.PURCHASE_CONFIRMATION, unlock, null);
|
||||||
|
Inventory inventory = create(holder, 27, TITLE + " — Confirm Purchase");
|
||||||
|
inventory.setItem(11, item(Material.RED_WOOL, ChatColor.RED + "Cancel", List.of()));
|
||||||
|
inventory.setItem(13, item(Material.ENCHANTED_BOOK, ChatColor.YELLOW + readable(unlock.name()),
|
||||||
|
List.of("Cost: 1 choice")));
|
||||||
|
inventory.setItem(15, item(Material.LIME_WOOL, ChatColor.GREEN + "Confirm", List.of()));
|
||||||
|
player.openInventory(inventory);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void clickPurchaseConfirmation(Player player, int slot, TyrantUnlock unlock) {
|
||||||
|
if (slot == 11) {
|
||||||
|
openUnlocks(player);
|
||||||
|
} else if (slot == 15 && unlock != null) {
|
||||||
|
player.performCommand("tyrant buy " + unlock.name().toLowerCase(Locale.ROOT));
|
||||||
|
openUnlocks(player);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openClasses(Player player) {
|
||||||
|
TyrantControlPanelModel model = model(player);
|
||||||
|
MenuHolder holder = new MenuHolder(MenuView.CLASSES, null, null);
|
||||||
|
Inventory inventory = create(holder, 27, TITLE + " — Classes");
|
||||||
|
TyrantClass[] classes = {TyrantClass.ASSASSIN, TyrantClass.FIXER, TyrantClass.TAMER};
|
||||||
|
int[] slots = {11, 13, 15};
|
||||||
|
for (int index = 0; index < classes.length; index++) {
|
||||||
|
TyrantClass tyrantClass = classes[index];
|
||||||
|
boolean purchased = stateManager.game().purchases().contains(unlockFor(tyrantClass));
|
||||||
|
inventory.setItem(slots[index], item(
|
||||||
|
purchased ? Material.ARMOR_STAND : Material.BARRIER,
|
||||||
|
ChatColor.AQUA + readable(tyrantClass.name()),
|
||||||
|
List.of("Holder: " + model.classHolders().get(tyrantClass),
|
||||||
|
purchased ? "Click to assign." : "Unlock not purchased.")));
|
||||||
|
}
|
||||||
|
inventory.setItem(22, backItem());
|
||||||
|
player.openInventory(inventory);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void clickClasses(Player player, int slot) {
|
||||||
|
if (slot == 22) {
|
||||||
|
refresh(player);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
TyrantClass tyrantClass = switch (slot) {
|
||||||
|
case 11 -> TyrantClass.ASSASSIN;
|
||||||
|
case 13 -> TyrantClass.FIXER;
|
||||||
|
case 15 -> TyrantClass.TAMER;
|
||||||
|
default -> null;
|
||||||
|
};
|
||||||
|
if (tyrantClass != null
|
||||||
|
&& stateManager.game().purchases().contains(unlockFor(tyrantClass))) {
|
||||||
|
openPlayers(player, tyrantClass);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openPlayers(Player player, TyrantClass tyrantClass) {
|
||||||
|
MenuHolder holder = new MenuHolder(MenuView.PLAYERS, null, tyrantClass);
|
||||||
|
Inventory inventory = create(holder, 54, TITLE + " — Select Player");
|
||||||
|
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 = state.optedOutUntil().isPresent()
|
||||||
|
? ChatColor.RED + "Ineligible: opted out"
|
||||||
|
: state.tyrantClass() != TyrantClass.NONE
|
||||||
|
&& state.tyrantClass() != tyrantClass
|
||||||
|
? ChatColor.RED + "Ineligible: already " + state.tyrantClass()
|
||||||
|
: ChatColor.GREEN + "Eligible";
|
||||||
|
inventory.setItem(slot, item(Material.PLAYER_HEAD, candidate.getName(),
|
||||||
|
List.of(eligibility, candidate.getUniqueId().toString())));
|
||||||
|
slot++;
|
||||||
|
}
|
||||||
|
inventory.setItem(49, backItem());
|
||||||
|
player.openInventory(inventory);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void clickPlayers(Player player, int slot, TyrantClass tyrantClass) {
|
||||||
|
if (slot == 49) {
|
||||||
|
openClasses(player);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ItemStack clicked = player.getOpenInventory().getTopInventory().getItem(slot);
|
||||||
|
if (clicked == null || clicked.getType() != Material.PLAYER_HEAD) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<String> lore = clicked.getItemMeta().getLore();
|
||||||
|
if (lore == null || lore.size() < 2 || !ChatColor.stripColor(lore.get(0)).equals("Eligible")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
openAssignmentConfirmation(
|
||||||
|
player, tyrantClass, UUID.fromString(ChatColor.stripColor(lore.get(1)))
|
||||||
|
);
|
||||||
|
} catch (IllegalArgumentException ignored) {
|
||||||
|
player.sendMessage(ChatColor.RED + "That player selection is no longer valid.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openAssignmentConfirmation(
|
||||||
|
Player player,
|
||||||
|
TyrantClass tyrantClass,
|
||||||
|
UUID targetId
|
||||||
|
) {
|
||||||
|
Player target = server.getPlayer(targetId);
|
||||||
|
if (target == null) {
|
||||||
|
player.sendMessage(ChatColor.RED + "That player is no longer online.");
|
||||||
|
openPlayers(player, tyrantClass);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
MenuHolder holder = new MenuHolder(
|
||||||
|
MenuView.ASSIGN_CONFIRMATION, null, tyrantClass, targetId
|
||||||
|
);
|
||||||
|
Inventory inventory = create(holder, 27, TITLE + " — Confirm Assignment");
|
||||||
|
String current = model(player).classHolders().get(tyrantClass);
|
||||||
|
inventory.setItem(11, item(Material.RED_WOOL, ChatColor.RED + "Cancel", List.of()));
|
||||||
|
inventory.setItem(13, item(Material.ARMOR_STAND,
|
||||||
|
ChatColor.YELLOW + target.getName(),
|
||||||
|
List.of("Class: " + tyrantClass, "Current holder: " + current)));
|
||||||
|
inventory.setItem(15, item(Material.LIME_WOOL, ChatColor.GREEN + "Confirm", List.of()));
|
||||||
|
player.openInventory(inventory);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void clickAssignmentConfirmation(
|
||||||
|
Player player,
|
||||||
|
int slot,
|
||||||
|
TyrantClass tyrantClass,
|
||||||
|
UUID targetId
|
||||||
|
) {
|
||||||
|
if (slot == 11) {
|
||||||
|
openPlayers(player, tyrantClass);
|
||||||
|
} else if (slot == 15 && tyrantClass != null && targetId != null) {
|
||||||
|
Player target = server.getPlayer(targetId);
|
||||||
|
if (target == null) {
|
||||||
|
player.sendMessage(ChatColor.RED + "That player is no longer online.");
|
||||||
|
} else {
|
||||||
|
player.performCommand("tyrant assign "
|
||||||
|
+ tyrantClass.name().toLowerCase(Locale.ROOT) + " " + target.getName());
|
||||||
|
}
|
||||||
|
openClasses(player);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isActiveTyrant(Player player) {
|
||||||
|
GameState game = stateManager.game();
|
||||||
|
return game.lifecycle() == GameLifecycle.RUNNING
|
||||||
|
&& game.tyrantId().filter(player.getUniqueId()::equals).isPresent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private TyrantControlPanelModel model(Player player) {
|
||||||
|
return TyrantControlPanelModel.create(
|
||||||
|
stateManager.game(),
|
||||||
|
stateManager.player(player.getUniqueId(), player.getName()),
|
||||||
|
stateManager.players(),
|
||||||
|
clock.instant()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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 List<String> classSummary(TyrantControlPanelModel model) {
|
||||||
|
List<String> summary = new ArrayList<>();
|
||||||
|
for (TyrantClass tyrantClass : new TyrantClass[] {
|
||||||
|
TyrantClass.ASSASSIN, TyrantClass.FIXER, TyrantClass.TAMER
|
||||||
|
}) {
|
||||||
|
summary.add(readable(tyrantClass.name()) + ": "
|
||||||
|
+ model.classHolders().get(tyrantClass));
|
||||||
|
}
|
||||||
|
return summary;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String intelligenceText(TyrantControlPanelModel model) {
|
||||||
|
if (model.unlocks().get(TyrantUnlock.ROSTER_INTELLIGENCE)
|
||||||
|
!= UnlockAvailability.PURCHASED) {
|
||||||
|
return "Unavailable: unlock not purchased.";
|
||||||
|
}
|
||||||
|
Duration cooldown = model.intelligenceCooldown();
|
||||||
|
return cooldown.isZero() ? "Ready" : "Cooldown: " + cooldown.toSeconds() + "s";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TyrantUnlock unlockFor(TyrantClass tyrantClass) {
|
||||||
|
return switch (tyrantClass) {
|
||||||
|
case ASSASSIN -> TyrantUnlock.ASSASSIN;
|
||||||
|
case FIXER -> TyrantUnlock.FIXER;
|
||||||
|
case TAMER -> TyrantUnlock.TAMER;
|
||||||
|
case NONE -> throw new IllegalArgumentException("NONE is not assignable");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String readable(String value) {
|
||||||
|
String lower = value.toLowerCase(Locale.ROOT).replace('_', ' ');
|
||||||
|
return Character.toUpperCase(lower.charAt(0)) + lower.substring(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum MenuView {
|
||||||
|
OVERVIEW,
|
||||||
|
UNLOCKS,
|
||||||
|
PURCHASE_CONFIRMATION,
|
||||||
|
CLASSES,
|
||||||
|
ABILITIES,
|
||||||
|
PLAYERS,
|
||||||
|
ASSIGN_CONFIRMATION
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class MenuHolder implements InventoryHolder {
|
||||||
|
private final MenuView view;
|
||||||
|
private final TyrantUnlock unlock;
|
||||||
|
private final TyrantClass tyrantClass;
|
||||||
|
private final UUID targetId;
|
||||||
|
private Inventory inventory;
|
||||||
|
|
||||||
|
private MenuHolder(MenuView view, TyrantUnlock unlock, TyrantClass tyrantClass) {
|
||||||
|
this(view, unlock, tyrantClass, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private MenuHolder(
|
||||||
|
MenuView view,
|
||||||
|
TyrantUnlock unlock,
|
||||||
|
TyrantClass tyrantClass,
|
||||||
|
UUID targetId
|
||||||
|
) {
|
||||||
|
this.view = view;
|
||||||
|
this.unlock = unlock;
|
||||||
|
this.tyrantClass = tyrantClass;
|
||||||
|
this.targetId = targetId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private MenuView view() {
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TyrantUnlock unlock() {
|
||||||
|
return unlock;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TyrantClass tyrantClass() {
|
||||||
|
return tyrantClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
private UUID targetId() {
|
||||||
|
return targetId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Inventory getInventory() {
|
||||||
|
return inventory;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,8 @@
|
|||||||
|
package games.dmg.spigottyrant;
|
||||||
|
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface CapturedMobCustodyRelease {
|
||||||
|
PlayerState releaseAll(Player player, PlayerState state);
|
||||||
|
}
|
||||||
@@ -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,31 @@
|
|||||||
|
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) {
|
||||||
|
List<CapturedMob> retained = new ArrayList<>();
|
||||||
|
for (CapturedMob mob : state.capturedMobs()) {
|
||||||
|
if (!spawner.spawn(player, mob)) {
|
||||||
|
retained.add(mob);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (retained.size() == state.capturedMobs().size()) {
|
||||||
|
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,57 @@
|
|||||||
|
package games.dmg.spigottyrant;
|
||||||
|
|
||||||
|
import java.time.Clock;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
import org.bukkit.ChatColor;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
public final class DefaultTyrantControlPanel implements TyrantControlPanel {
|
||||||
|
private final TyrantStateManager stateManager;
|
||||||
|
private final TyrantControlPanelRenderer renderer;
|
||||||
|
private final Clock clock;
|
||||||
|
private final Set<UUID> openedSessions = new HashSet<>();
|
||||||
|
|
||||||
|
public DefaultTyrantControlPanel(
|
||||||
|
TyrantStateManager stateManager,
|
||||||
|
TyrantControlPanelRenderer renderer,
|
||||||
|
Clock clock
|
||||||
|
) {
|
||||||
|
this.stateManager = stateManager;
|
||||||
|
this.renderer = renderer;
|
||||||
|
this.clock = clock;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void open(Player player) {
|
||||||
|
GameState game = stateManager.game();
|
||||||
|
if (game.lifecycle() != GameLifecycle.RUNNING
|
||||||
|
|| game.tyrantId().filter(player.getUniqueId()::equals).isEmpty()) {
|
||||||
|
player.sendMessage(ChatColor.RED
|
||||||
|
+ "Only the active Tyrant can open the control panel while the event is running.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
PlayerState tyrant = stateManager.player(player.getUniqueId(), player.getName());
|
||||||
|
renderer.openOverview(
|
||||||
|
player,
|
||||||
|
TyrantControlPanelModel.create(game, tyrant, stateManager.players(), clock.instant())
|
||||||
|
);
|
||||||
|
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,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,6 @@
|
|||||||
|
package games.dmg.spigottyrant;
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface DelayedTaskScheduler {
|
||||||
|
void schedule(Runnable task, long delayTicks);
|
||||||
|
}
|
||||||
@@ -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);
|
PlayerState target = players.get(targetId);
|
||||||
if (target == null || target.optedOutUntil().isPresent()
|
if (target == null || target.optedOutUntil().isPresent()
|
||||||
|
|| target.followerOf().isPresent()
|
||||||
|| game.tyrantId().filter(targetId::equals).isPresent()
|
|| game.tyrantId().filter(targetId::equals).isPresent()
|
||||||
|| game.vigilanteId().filter(targetId::equals).isPresent()) {
|
|| game.vigilanteId().filter(targetId::equals).isPresent()) {
|
||||||
return result(players, FollowerStatus.INELIGIBLE);
|
return result(players, FollowerStatus.INELIGIBLE);
|
||||||
}
|
}
|
||||||
|
if (vigilanteId.equals(invitations.get(targetId))) {
|
||||||
|
return result(players, FollowerStatus.ALREADY_INVITED);
|
||||||
|
}
|
||||||
invitations.put(targetId, vigilanteId);
|
invitations.put(targetId, vigilanteId);
|
||||||
return result(players, FollowerStatus.INVITED);
|
return result(players, FollowerStatus.INVITED);
|
||||||
}
|
}
|
||||||
@@ -77,6 +81,13 @@ public final class FollowerService {
|
|||||||
return updated(players, withFollower(target, Optional.empty()), FollowerStatus.LEFT);
|
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() {
|
public void clearInvitations() {
|
||||||
invitations.clear();
|
invitations.clear();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package games.dmg.spigottyrant;
|
|||||||
|
|
||||||
public enum FollowerStatus {
|
public enum FollowerStatus {
|
||||||
INVITED,
|
INVITED,
|
||||||
|
ALREADY_INVITED,
|
||||||
JOINED,
|
JOINED,
|
||||||
DISMISSED,
|
DISMISSED,
|
||||||
LEFT,
|
LEFT,
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
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.tyrantClass() == TyrantClass.TAMER || state.capturedMobs().isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Player player = online.get(state.playerId());
|
||||||
|
if (player == null || player.isDead()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
PlayerState released = 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 fixerItem,
|
||||||
AbilityItemSettings tamerItem,
|
AbilityItemSettings tamerItem,
|
||||||
AbilityItemSettings capturedMobItem,
|
AbilityItemSettings capturedMobItem,
|
||||||
|
AbilityItemSettings tyrantControlItem,
|
||||||
|
AbilityItemSettings vigilanteControlItem,
|
||||||
String recoveryCommand,
|
String recoveryCommand,
|
||||||
String inventoryFullMessage,
|
String inventoryFullMessage,
|
||||||
boolean freezeTimersWhilePaused
|
boolean freezeTimersWhilePaused
|
||||||
@@ -78,6 +80,10 @@ public record PluginSettings(
|
|||||||
fixerItem = Objects.requireNonNull(fixerItem, "fixerItem");
|
fixerItem = Objects.requireNonNull(fixerItem, "fixerItem");
|
||||||
tamerItem = Objects.requireNonNull(tamerItem, "tamerItem");
|
tamerItem = Objects.requireNonNull(tamerItem, "tamerItem");
|
||||||
capturedMobItem = Objects.requireNonNull(capturedMobItem, "capturedMobItem");
|
capturedMobItem = Objects.requireNonNull(capturedMobItem, "capturedMobItem");
|
||||||
|
tyrantControlItem = Objects.requireNonNull(tyrantControlItem, "tyrantControlItem");
|
||||||
|
vigilanteControlItem = Objects.requireNonNull(
|
||||||
|
vigilanteControlItem, "vigilanteControlItem"
|
||||||
|
);
|
||||||
recoveryCommand = requireText(recoveryCommand, "recovery-command");
|
recoveryCommand = requireText(recoveryCommand, "recovery-command");
|
||||||
inventoryFullMessage = requireText(inventoryFullMessage, "inventory-full-message");
|
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, "fixer-item", new AbilityItemSettings("STICK", "Fixer's Wrench")),
|
||||||
item(values, "tamer-item", new AbilityItemSettings("FISHING_ROD", "Tamer's Lead")),
|
item(values, "tamer-item", new AbilityItemSettings("FISHING_ROD", "Tamer's Lead")),
|
||||||
item(values, "captured-mob-item", new AbilityItemSettings("PAPER", "Captured Mob")),
|
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, "recovery-command", "/tyrant item"),
|
||||||
string(
|
string(
|
||||||
values,
|
values,
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ public final class PluginSettingsValidator {
|
|||||||
validateItem(settings.fixerItem(), "fixer-item");
|
validateItem(settings.fixerItem(), "fixer-item");
|
||||||
validateItem(settings.tamerItem(), "tamer-item");
|
validateItem(settings.tamerItem(), "tamer-item");
|
||||||
validateItem(settings.capturedMobItem(), "captured-mob-item");
|
validateItem(settings.capturedMobItem(), "captured-mob-item");
|
||||||
|
validateItem(settings.tyrantControlItem(), "tyrant-control-item");
|
||||||
|
validateItem(settings.vigilanteControlItem(), "vigilante-control-item");
|
||||||
for (String entityType : settings.deniedMobTypes()) {
|
for (String entityType : settings.deniedMobTypes()) {
|
||||||
try {
|
try {
|
||||||
EntityType.valueOf(entityType);
|
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();
|
FollowerService followers = new FollowerService();
|
||||||
AbilityReadinessService readiness = new AbilityReadinessService();
|
AbilityReadinessService readiness = new AbilityReadinessService();
|
||||||
AbilityItemService abilityItems = new BukkitAbilityItemService(this, settings);
|
AbilityItemService abilityItems = new BukkitAbilityItemService(this, settings);
|
||||||
|
RoleControlItemService roleControlItems = new BukkitRoleControlItemService(
|
||||||
|
this, settings
|
||||||
|
);
|
||||||
CapturedMobItemService capturedMobItems = new BukkitCapturedMobItemService(
|
CapturedMobItemService capturedMobItems = new BukkitCapturedMobItemService(
|
||||||
this, settings
|
this, settings
|
||||||
);
|
);
|
||||||
@@ -61,6 +64,10 @@ public final class SpigotTyrantPlugin extends JavaPlugin {
|
|||||||
CapturedMobInventoryService capturedMobInventory = new CapturedMobInventoryService(
|
CapturedMobInventoryService capturedMobInventory = new CapturedMobInventoryService(
|
||||||
capturedMobItems, settings
|
capturedMobItems, settings
|
||||||
);
|
);
|
||||||
|
CapturedMobCustodyRelease formerTamerRelease =
|
||||||
|
new DefaultCapturedMobCustodyRelease(
|
||||||
|
new BukkitCapturedMobSpawner(getServer(), settings)
|
||||||
|
);
|
||||||
AssassinAbilityService assassinAbilities = new AssassinAbilityService(
|
AssassinAbilityService assassinAbilities = new AssassinAbilityService(
|
||||||
readiness, settings
|
readiness, settings
|
||||||
);
|
);
|
||||||
@@ -80,6 +87,23 @@ public final class SpigotTyrantPlugin extends JavaPlugin {
|
|||||||
stateManager, tyrantPresentation
|
stateManager, tyrantPresentation
|
||||||
);
|
);
|
||||||
managedEffects.restoreAll();
|
managedEffects.restoreAll();
|
||||||
|
BukkitTyrantControlPanelRenderer controlPanelRenderer =
|
||||||
|
new BukkitTyrantControlPanelRenderer(getServer(), stateManager, clock);
|
||||||
|
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")
|
Objects.requireNonNull(getCommand("tyrant"), "Missing tyrant command metadata")
|
||||||
.setExecutor(new TyrantCommand(
|
.setExecutor(new TyrantCommand(
|
||||||
stateManager,
|
stateManager,
|
||||||
@@ -91,10 +115,15 @@ public final class SpigotTyrantPlugin extends JavaPlugin {
|
|||||||
new RoleRelinquishmentService(succession, maintenance),
|
new RoleRelinquishmentService(succession, maintenance),
|
||||||
tyrantPresentation,
|
tyrantPresentation,
|
||||||
abilityItems,
|
abilityItems,
|
||||||
new TyrantAbilityService(settings.rosterIntelligenceCooldown())
|
new TyrantAbilityService(settings.rosterIntelligenceCooldown()),
|
||||||
|
controlPanel,
|
||||||
|
roleControlItems
|
||||||
));
|
));
|
||||||
Objects.requireNonNull(getCommand("vigilante"), "Missing vigilante metadata")
|
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")
|
Objects.requireNonNull(getCommand("tyrantadmin"), "Missing tyrantadmin metadata")
|
||||||
.setExecutor(new TyrantAdminCommand(
|
.setExecutor(new TyrantAdminCommand(
|
||||||
stateManager,
|
stateManager,
|
||||||
@@ -111,6 +140,26 @@ public final class SpigotTyrantPlugin extends JavaPlugin {
|
|||||||
new PlayerJoinListener(stateManager, clock, tyrantPresentation),
|
new PlayerJoinListener(stateManager, clock, tyrantPresentation),
|
||||||
this
|
this
|
||||||
);
|
);
|
||||||
|
getServer().getPluginManager().registerEvents(controlPanelRenderer, this);
|
||||||
|
getServer().getPluginManager().registerEvents(vigilanteControlPanelRenderer, this);
|
||||||
|
getServer().getPluginManager().registerEvents(
|
||||||
|
new TyrantControlPanelJoinListener(
|
||||||
|
stateManager,
|
||||||
|
controlPanel,
|
||||||
|
(task, delayTicks) -> getServer().getScheduler()
|
||||||
|
.runTaskLater(this, task, delayTicks)
|
||||||
|
),
|
||||||
|
this
|
||||||
|
);
|
||||||
|
getServer().getPluginManager().registerEvents(
|
||||||
|
new VigilanteControlPanelJoinListener(
|
||||||
|
stateManager,
|
||||||
|
vigilanteControlPanel,
|
||||||
|
(task, delayTicks) -> getServer().getScheduler()
|
||||||
|
.runTaskLater(this, task, delayTicks)
|
||||||
|
),
|
||||||
|
this
|
||||||
|
);
|
||||||
getServer().getPluginManager().registerEvents(
|
getServer().getPluginManager().registerEvents(
|
||||||
new TyrantDeathListener(
|
new TyrantDeathListener(
|
||||||
stateManager, succession, tyrantPresentation, getServer(), clock
|
stateManager, succession, tyrantPresentation, getServer(), clock
|
||||||
@@ -141,6 +190,12 @@ public final class SpigotTyrantPlugin extends JavaPlugin {
|
|||||||
new BoundItemListener(abilityItems),
|
new BoundItemListener(abilityItems),
|
||||||
this
|
this
|
||||||
);
|
);
|
||||||
|
getServer().getPluginManager().registerEvents(
|
||||||
|
new RoleControlItemListener(
|
||||||
|
stateManager, roleControlItems, controlPanel, vigilanteControlPanel
|
||||||
|
),
|
||||||
|
this
|
||||||
|
);
|
||||||
getServer().getPluginManager().registerEvents(
|
getServer().getPluginManager().registerEvents(
|
||||||
new TamerListener(
|
new TamerListener(
|
||||||
stateManager, abilityItems, capturedMobItems, capturedMobs,
|
stateManager, abilityItems, capturedMobItems, capturedMobs,
|
||||||
@@ -189,6 +244,20 @@ public final class SpigotTyrantPlugin extends JavaPlugin {
|
|||||||
20L,
|
20L,
|
||||||
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);
|
getServer().getScheduler().runTaskTimer(this, stateManager::saveIfDirty, 600L, 600L);
|
||||||
getLogger().info("Spigot Tyrant enabled.");
|
getLogger().info("Spigot Tyrant enabled.");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ public final class TyrantCommand implements CommandExecutor {
|
|||||||
private final TyrantPresentation presentation;
|
private final TyrantPresentation presentation;
|
||||||
private final AbilityItemService abilityItems;
|
private final AbilityItemService abilityItems;
|
||||||
private final TyrantAbilityService tyrantAbilities;
|
private final TyrantAbilityService tyrantAbilities;
|
||||||
|
private final TyrantControlPanel controlPanel;
|
||||||
|
private final RoleControlItemService roleControlItems;
|
||||||
|
|
||||||
public TyrantCommand(
|
public TyrantCommand(
|
||||||
TyrantStateManager stateManager,
|
TyrantStateManager stateManager,
|
||||||
@@ -34,6 +36,33 @@ public final class TyrantCommand implements CommandExecutor {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public TyrantCommand(
|
||||||
|
TyrantStateManager stateManager,
|
||||||
|
TyrantProgressionService progression,
|
||||||
|
TyrantControlPanel controlPanel
|
||||||
|
) {
|
||||||
|
this(
|
||||||
|
stateManager, progression, new ClassAssignmentService(),
|
||||||
|
new BukkitOnlinePlayerDirectory(), Clock.systemUTC(),
|
||||||
|
new ParticipationService(Duration.ofDays(7)), null, null, null,
|
||||||
|
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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
TyrantCommand(
|
TyrantCommand(
|
||||||
TyrantStateManager stateManager,
|
TyrantStateManager stateManager,
|
||||||
TyrantProgressionService progression,
|
TyrantProgressionService progression,
|
||||||
@@ -44,7 +73,8 @@ public final class TyrantCommand implements CommandExecutor {
|
|||||||
this(
|
this(
|
||||||
stateManager, progression, assignments, onlinePlayers, clock,
|
stateManager, progression, assignments, onlinePlayers, clock,
|
||||||
new ParticipationService(Duration.ofDays(7)), null, null, null,
|
new ParticipationService(Duration.ofDays(7)), null, null, null,
|
||||||
new TyrantAbilityService(Duration.ofHours(24))
|
new TyrantAbilityService(Duration.ofHours(24)), TyrantCommand::unavailablePanel,
|
||||||
|
null
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,6 +89,46 @@ public final class TyrantCommand implements CommandExecutor {
|
|||||||
TyrantPresentation presentation,
|
TyrantPresentation presentation,
|
||||||
AbilityItemService abilityItems,
|
AbilityItemService abilityItems,
|
||||||
TyrantAbilityService tyrantAbilities
|
TyrantAbilityService tyrantAbilities
|
||||||
|
) {
|
||||||
|
this(
|
||||||
|
stateManager, progression, assignments, onlinePlayers, clock, participation,
|
||||||
|
relinquishment, presentation, abilityItems, tyrantAbilities,
|
||||||
|
TyrantCommand::unavailablePanel, 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
|
||||||
|
) {
|
||||||
|
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.stateManager = stateManager;
|
||||||
this.progression = progression;
|
this.progression = progression;
|
||||||
@@ -70,6 +140,8 @@ public final class TyrantCommand implements CommandExecutor {
|
|||||||
this.presentation = presentation;
|
this.presentation = presentation;
|
||||||
this.abilityItems = abilityItems;
|
this.abilityItems = abilityItems;
|
||||||
this.tyrantAbilities = tyrantAbilities;
|
this.tyrantAbilities = tyrantAbilities;
|
||||||
|
this.controlPanel = controlPanel;
|
||||||
|
this.roleControlItems = roleControlItems;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -83,7 +155,12 @@ public final class TyrantCommand implements CommandExecutor {
|
|||||||
sender.sendMessage("This command must be used by a player.");
|
sender.sendMessage("This command must be used by a player.");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (arguments.length == 0 || arguments[0].equalsIgnoreCase("choices")) {
|
if (arguments.length == 0
|
||||||
|
|| arguments.length == 1 && arguments[0].equalsIgnoreCase("menu")) {
|
||||||
|
controlPanel.open(player);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (arguments.length == 1 && arguments[0].equalsIgnoreCase("choices")) {
|
||||||
showChoices(player);
|
showChoices(player);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -121,7 +198,7 @@ public final class TyrantCommand implements CommandExecutor {
|
|||||||
}
|
}
|
||||||
player.sendMessage(
|
player.sendMessage(
|
||||||
ChatColor.YELLOW
|
ChatColor.YELLOW
|
||||||
+ "Usage: /tyrant <status|choices|buy|assign|item|intelligence|optout|optin|"
|
+ "Usage: /tyrant <menu|status|choices|buy|assign|item|intelligence|optout|optin|"
|
||||||
+ "relinquish confirm>"
|
+ "relinquish confirm>"
|
||||||
);
|
);
|
||||||
return true;
|
return true;
|
||||||
@@ -268,14 +345,21 @@ public final class TyrantCommand implements CommandExecutor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void recoverItems(Player player) {
|
private void recoverItems(Player player) {
|
||||||
if (abilityItems == null) {
|
if (abilityItems == null && roleControlItems == null) {
|
||||||
player.sendMessage(ChatColor.RED + "Ability item recovery is unavailable.");
|
player.sendMessage(ChatColor.RED + "Item recovery is unavailable.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
PlayerState state = stateManager.player(player.getUniqueId(), player.getName());
|
if (abilityItems != null) {
|
||||||
abilityItems.recover(player, state);
|
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
|
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) {
|
private void relinquish(Player player, String[] arguments) {
|
||||||
@@ -303,6 +387,10 @@ public final class TyrantCommand implements CommandExecutor {
|
|||||||
+ readable(result.status()) + ".");
|
+ readable(result.status()) + ".");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void unavailablePanel(Player player) {
|
||||||
|
player.sendMessage(ChatColor.RED + "The Tyrant control panel is unavailable.");
|
||||||
|
}
|
||||||
|
|
||||||
private static String readable(Enum<?> status) {
|
private static String readable(Enum<?> status) {
|
||||||
return status.name().toLowerCase(Locale.ROOT).replace('_', ' ');
|
return status.name().toLowerCase(Locale.ROOT).replace('_', ' ');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package games.dmg.spigottyrant;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface TyrantControlPanel {
|
||||||
|
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 TyrantControlPanelJoinListener implements Listener {
|
||||||
|
private static final long OPEN_DELAY_TICKS = 100L;
|
||||||
|
private final TyrantStateManager stateManager;
|
||||||
|
private final TyrantControlPanel controlPanel;
|
||||||
|
private final DelayedTaskScheduler scheduler;
|
||||||
|
|
||||||
|
public TyrantControlPanelJoinListener(
|
||||||
|
TyrantStateManager stateManager,
|
||||||
|
TyrantControlPanel 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(() -> openIfStillTyrant(player), OPEN_DELAY_TICKS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
public void onQuit(PlayerQuitEvent event) {
|
||||||
|
controlPanel.endSession(event.getPlayer().getUniqueId());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openIfStillTyrant(Player player) {
|
||||||
|
GameState game = stateManager.game();
|
||||||
|
if (player.isOnline()
|
||||||
|
&& game.lifecycle() == GameLifecycle.RUNNING
|
||||||
|
&& game.tyrantId().filter(player.getUniqueId()::equals).isPresent()
|
||||||
|
&& !controlPanel.openedThisSession(player.getUniqueId())) {
|
||||||
|
controlPanel.open(player);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package games.dmg.spigottyrant;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.EnumMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public record TyrantControlPanelModel(
|
||||||
|
int level,
|
||||||
|
int unspentChoices,
|
||||||
|
Map<TyrantUnlock, UnlockAvailability> unlocks,
|
||||||
|
Map<TyrantClass, String> classHolders,
|
||||||
|
Duration intelligenceCooldown
|
||||||
|
) {
|
||||||
|
public TyrantControlPanelModel {
|
||||||
|
unlocks = Map.copyOf(unlocks);
|
||||||
|
classHolders = Map.copyOf(classHolders);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<TyrantUnlock> purchasedUnlocks() {
|
||||||
|
return unlocks.entrySet().stream()
|
||||||
|
.filter(entry -> entry.getValue() == UnlockAvailability.PURCHASED)
|
||||||
|
.map(Map.Entry::getKey)
|
||||||
|
.collect(java.util.stream.Collectors.toUnmodifiableSet());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TyrantControlPanelModel create(
|
||||||
|
GameState game,
|
||||||
|
PlayerState tyrant,
|
||||||
|
Map<java.util.UUID, PlayerState> players,
|
||||||
|
Instant now
|
||||||
|
) {
|
||||||
|
Map<TyrantUnlock, UnlockAvailability> unlocks = new EnumMap<>(TyrantUnlock.class);
|
||||||
|
for (TyrantUnlock unlock : TyrantUnlock.values()) {
|
||||||
|
UnlockAvailability availability;
|
||||||
|
if (game.purchases().contains(unlock)) {
|
||||||
|
availability = UnlockAvailability.PURCHASED;
|
||||||
|
} else if (game.lifecycle() == GameLifecycle.RUNNING
|
||||||
|
&& game.unspentChoices() > 0) {
|
||||||
|
availability = UnlockAvailability.AVAILABLE;
|
||||||
|
} else {
|
||||||
|
availability = UnlockAvailability.UNAVAILABLE;
|
||||||
|
}
|
||||||
|
unlocks.put(unlock, availability);
|
||||||
|
}
|
||||||
|
Map<TyrantClass, String> holders = new EnumMap<>(TyrantClass.class);
|
||||||
|
for (TyrantClass tyrantClass : new TyrantClass[] {
|
||||||
|
TyrantClass.ASSASSIN, TyrantClass.FIXER, TyrantClass.TAMER
|
||||||
|
}) {
|
||||||
|
String holder = players.values().stream()
|
||||||
|
.filter(player -> player.tyrantClass() == tyrantClass)
|
||||||
|
.map(PlayerState::latestName)
|
||||||
|
.findFirst()
|
||||||
|
.orElse("Unassigned");
|
||||||
|
holders.put(tyrantClass, holder);
|
||||||
|
}
|
||||||
|
Duration cooldown = tyrant.cooldownEnds().getOrDefault(
|
||||||
|
Ability.ROSTER_INTELLIGENCE, now
|
||||||
|
).isAfter(now)
|
||||||
|
? Duration.between(now, tyrant.cooldownEnds().get(Ability.ROSTER_INTELLIGENCE))
|
||||||
|
: Duration.ZERO;
|
||||||
|
return new TyrantControlPanelModel(
|
||||||
|
game.tyrantLevel(), game.unspentChoices(), unlocks, holders, cooldown
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package games.dmg.spigottyrant;
|
||||||
|
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface TyrantControlPanelRenderer {
|
||||||
|
void openOverview(Player player, TyrantControlPanelModel model);
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package games.dmg.spigottyrant;
|
||||||
|
|
||||||
|
public enum UnlockAvailability {
|
||||||
|
PURCHASED,
|
||||||
|
AVAILABLE,
|
||||||
|
UNAVAILABLE
|
||||||
|
}
|
||||||
@@ -11,15 +11,43 @@ public final class VigilanteCommand implements CommandExecutor {
|
|||||||
private final TyrantStateManager stateManager;
|
private final TyrantStateManager stateManager;
|
||||||
private final FollowerService followers;
|
private final FollowerService followers;
|
||||||
private final OnlinePlayerDirectory onlinePlayers;
|
private final OnlinePlayerDirectory onlinePlayers;
|
||||||
|
private final VigilanteControlPanel controlPanel;
|
||||||
|
private final RoleControlItemService roleControlItems;
|
||||||
|
|
||||||
public VigilanteCommand(
|
public VigilanteCommand(
|
||||||
TyrantStateManager stateManager,
|
TyrantStateManager stateManager,
|
||||||
FollowerService followers,
|
FollowerService followers,
|
||||||
OnlinePlayerDirectory onlinePlayers
|
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.stateManager = stateManager;
|
||||||
this.followers = followers;
|
this.followers = followers;
|
||||||
this.onlinePlayers = onlinePlayers;
|
this.onlinePlayers = onlinePlayers;
|
||||||
|
this.controlPanel = controlPanel;
|
||||||
|
this.roleControlItems = roleControlItems;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -33,6 +61,21 @@ public final class VigilanteCommand implements CommandExecutor {
|
|||||||
sender.sendMessage("This command must be used by a player.");
|
sender.sendMessage("This command must be used by a player.");
|
||||||
return true;
|
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;
|
FollowerResult result;
|
||||||
Player target = null;
|
Player target = null;
|
||||||
if (arguments.length == 1 && arguments[0].equalsIgnoreCase("accept")) {
|
if (arguments.length == 1 && arguments[0].equalsIgnoreCase("accept")) {
|
||||||
@@ -61,7 +104,7 @@ public final class VigilanteCommand implements CommandExecutor {
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
player.sendMessage(ChatColor.YELLOW
|
player.sendMessage(ChatColor.YELLOW
|
||||||
+ "Usage: /vigilante <invite <player>|accept|dismiss <player>|leave>");
|
+ "Usage: /vigilante <menu|item|invite <player>|accept|dismiss <player>|leave>");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (result.status() == FollowerStatus.JOINED
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -50,6 +50,14 @@ tyrant-resistance-level: 1
|
|||||||
follower-strength-cap: 5
|
follower-strength-cap: 5
|
||||||
follower-resistance-cap: 4
|
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
|
# Bound item recovery and delivery messages
|
||||||
recovery-command: /tyrant item
|
recovery-command: /tyrant item
|
||||||
inventory-full-message: Your inventory is full. Make room and use /tyrant item.
|
inventory-full-message: Your inventory is full. Make room and use /tyrant item.
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ author: dmg.games
|
|||||||
commands:
|
commands:
|
||||||
tyrant:
|
tyrant:
|
||||||
description: View and use Spigot Tyrant game features.
|
description: View and use Spigot Tyrant game features.
|
||||||
usage: /tyrant <status|choices|buy|assign|item|intelligence|optout|optin|relinquish confirm>
|
usage: /tyrant <menu|status|choices|buy|assign|item|intelligence|optout|optin|relinquish confirm>
|
||||||
vigilante:
|
vigilante:
|
||||||
description: Manage Vigilante Followers.
|
description: Manage Vigilante Followers.
|
||||||
usage: /vigilante <invite <player>|accept|dismiss <player>|leave>
|
usage: /vigilante <menu|item|invite <player>|accept|dismiss <player>|leave>
|
||||||
tyrantadmin:
|
tyrantadmin:
|
||||||
description: Administer the Spigot Tyrant game.
|
description: Administer the Spigot Tyrant game.
|
||||||
usage: /tyrantadmin <status|start|pause|resume|reset confirm>
|
usage: /tyrantadmin <status|start|pause|resume|reset confirm>
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
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 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,72 @@
|
|||||||
|
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.Clock;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.ZoneOffset;
|
||||||
|
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 DefaultTyrantControlPanelTest {
|
||||||
|
@Test
|
||||||
|
void nonTyrantCannotRenderControlPanel() {
|
||||||
|
UUID tyrantId = UUID.fromString("11111111-1111-1111-1111-111111111111");
|
||||||
|
UUID otherId = UUID.fromString("22222222-2222-2222-2222-222222222222");
|
||||||
|
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 other = mock(Player.class);
|
||||||
|
when(other.getUniqueId()).thenReturn(otherId);
|
||||||
|
TyrantControlPanelRenderer renderer = mock(TyrantControlPanelRenderer.class);
|
||||||
|
DefaultTyrantControlPanel panel = new DefaultTyrantControlPanel(
|
||||||
|
manager, renderer,
|
||||||
|
Clock.fixed(Instant.parse("2026-08-21T12:00:00Z"), ZoneOffset.UTC)
|
||||||
|
);
|
||||||
|
|
||||||
|
panel.open(other);
|
||||||
|
|
||||||
|
verify(renderer, never()).openOverview(any(), any());
|
||||||
|
verify(other).sendMessage(org.mockito.ArgumentMatchers.contains("active Tyrant"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void openingAsActiveTyrantRendersAuthoritativeOverview() {
|
||||||
|
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,
|
||||||
|
2, 1, Set.of(TyrantUnlock.STRENGTH)
|
||||||
|
);
|
||||||
|
PlayerState tyrantState = PlayerState.newPlayer(tyrantId, "Tyrant");
|
||||||
|
TyrantStateManager manager = mock(TyrantStateManager.class);
|
||||||
|
when(manager.game()).thenReturn(game);
|
||||||
|
when(manager.player(tyrantId, "Tyrant")).thenReturn(tyrantState);
|
||||||
|
when(manager.players()).thenReturn(Map.of(tyrantId, tyrantState));
|
||||||
|
Player tyrant = mock(Player.class);
|
||||||
|
when(tyrant.getUniqueId()).thenReturn(tyrantId);
|
||||||
|
when(tyrant.getName()).thenReturn("Tyrant");
|
||||||
|
TyrantControlPanelRenderer renderer = mock(TyrantControlPanelRenderer.class);
|
||||||
|
DefaultTyrantControlPanel panel = new DefaultTyrantControlPanel(
|
||||||
|
manager, renderer,
|
||||||
|
Clock.fixed(Instant.parse("2026-08-21T12:00:00Z"), ZoneOffset.UTC)
|
||||||
|
);
|
||||||
|
|
||||||
|
panel.open(tyrant);
|
||||||
|
|
||||||
|
verify(renderer).openOverview(org.mockito.ArgumentMatchers.eq(tyrant), any());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
package games.dmg.spigottyrant;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -34,6 +35,32 @@ final class FollowerServiceTest {
|
|||||||
assertEquals(TyrantClass.FIXER, accepted.players().get(FOLLOWER).tyrantClass());
|
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
|
@Test
|
||||||
void vigilanteCanDismissAndFollowerCanLeave() {
|
void vigilanteCanDismissAndFollowerCanLeave() {
|
||||||
GameState game = game();
|
GameState game = game();
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
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 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"
|
||||||
|
));
|
||||||
|
return new PlayerState(
|
||||||
|
player.playerId(), player.latestName(), player.lastLogin(), player.optedOutUntil(),
|
||||||
|
player.tyrantClass(), player.followerOf(), player.cooldownEnds(),
|
||||||
|
player.readyAbilityItems(), java.util.List.of(mob)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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("STICK", settings.assassinItem().material());
|
||||||
assertEquals("Assassin Cloak", settings.assassinItem().name());
|
assertEquals("Assassin Cloak", settings.assassinItem().name());
|
||||||
assertEquals("FISHING_ROD", settings.tamerItem().material());
|
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("/tyrant item", settings.recoveryCommand());
|
||||||
assertEquals("Your inventory is full. Make room and use /tyrant item.",
|
assertEquals("Your inventory is full. Make room and use /tyrant item.",
|
||||||
settings.inventoryFullMessage());
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,6 +20,51 @@ import org.junit.jupiter.api.Test;
|
|||||||
import org.mockito.ArgumentCaptor;
|
import org.mockito.ArgumentCaptor;
|
||||||
|
|
||||||
final class TyrantCommandTest {
|
final class TyrantCommandTest {
|
||||||
|
@Test
|
||||||
|
void activeTyrantCanOpenControlPanelWithNoArgumentsOrMenuSubcommand() {
|
||||||
|
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, 1, Set.of()
|
||||||
|
);
|
||||||
|
TyrantStateManager manager = mock(TyrantStateManager.class);
|
||||||
|
when(manager.game()).thenReturn(game);
|
||||||
|
Player tyrant = mock(Player.class);
|
||||||
|
when(tyrant.getUniqueId()).thenReturn(tyrantId);
|
||||||
|
TyrantControlPanel controlPanel = mock(TyrantControlPanel.class);
|
||||||
|
TyrantCommand command = new TyrantCommand(
|
||||||
|
manager, new TyrantProgressionService(), controlPanel
|
||||||
|
);
|
||||||
|
|
||||||
|
command.onCommand(tyrant, mock(Command.class), "tyrant", new String[0]);
|
||||||
|
command.onCommand(tyrant, mock(Command.class), "tyrant", new String[] {"menu"});
|
||||||
|
|
||||||
|
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
|
@Test
|
||||||
void choicesClearlyIdentifyPurchasedAvailableAndUnavailableUnlocks() {
|
void choicesClearlyIdentifyPurchasedAvailableAndUnavailableUnlocks() {
|
||||||
UUID tyrantId = UUID.fromString("11111111-1111-1111-1111-111111111111");
|
UUID tyrantId = UUID.fromString("11111111-1111-1111-1111-111111111111");
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
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 TyrantControlPanelJoinListenerTest {
|
||||||
|
@Test
|
||||||
|
void delayedTaskDoesNotOpenAfterEventPauses() {
|
||||||
|
UUID tyrantId = UUID.fromString("11111111-1111-1111-1111-111111111111");
|
||||||
|
GameState paused = new GameState(
|
||||||
|
GameLifecycle.PAUSED, Optional.of(tyrantId), Optional.empty(),
|
||||||
|
Optional.empty(), Optional.empty(), Optional.of(java.time.Instant.EPOCH),
|
||||||
|
Duration.ZERO, 0, 1, Set.of()
|
||||||
|
);
|
||||||
|
TyrantStateManager manager = mock(TyrantStateManager.class);
|
||||||
|
when(manager.game()).thenReturn(paused);
|
||||||
|
Player tyrant = mock(Player.class);
|
||||||
|
when(tyrant.getUniqueId()).thenReturn(tyrantId);
|
||||||
|
when(tyrant.isOnline()).thenReturn(true);
|
||||||
|
PlayerJoinEvent event = mock(PlayerJoinEvent.class);
|
||||||
|
when(event.getPlayer()).thenReturn(tyrant);
|
||||||
|
DelayedTaskScheduler scheduler = mock(DelayedTaskScheduler.class);
|
||||||
|
TyrantControlPanel panel = mock(TyrantControlPanel.class);
|
||||||
|
TyrantControlPanelJoinListener listener = new TyrantControlPanelJoinListener(
|
||||||
|
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(tyrant);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void delayedTaskDoesNotReopenPanelOpenedManually() {
|
||||||
|
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, 1, Set.of()
|
||||||
|
);
|
||||||
|
TyrantStateManager manager = mock(TyrantStateManager.class);
|
||||||
|
when(manager.game()).thenReturn(game);
|
||||||
|
Player tyrant = mock(Player.class);
|
||||||
|
when(tyrant.getUniqueId()).thenReturn(tyrantId);
|
||||||
|
when(tyrant.isOnline()).thenReturn(true);
|
||||||
|
PlayerJoinEvent event = mock(PlayerJoinEvent.class);
|
||||||
|
when(event.getPlayer()).thenReturn(tyrant);
|
||||||
|
DelayedTaskScheduler scheduler = mock(DelayedTaskScheduler.class);
|
||||||
|
TyrantControlPanel panel = mock(TyrantControlPanel.class);
|
||||||
|
when(panel.openedThisSession(tyrantId)).thenReturn(true);
|
||||||
|
TyrantControlPanelJoinListener listener = new TyrantControlPanelJoinListener(
|
||||||
|
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(tyrant);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void opensPanelOneHundredTicksAfterActiveTyrantJoins() {
|
||||||
|
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, 1, Set.of()
|
||||||
|
);
|
||||||
|
TyrantStateManager manager = mock(TyrantStateManager.class);
|
||||||
|
when(manager.game()).thenReturn(game);
|
||||||
|
Player tyrant = mock(Player.class);
|
||||||
|
when(tyrant.getUniqueId()).thenReturn(tyrantId);
|
||||||
|
when(tyrant.isOnline()).thenReturn(true);
|
||||||
|
PlayerJoinEvent event = mock(PlayerJoinEvent.class);
|
||||||
|
when(event.getPlayer()).thenReturn(tyrant);
|
||||||
|
DelayedTaskScheduler scheduler = mock(DelayedTaskScheduler.class);
|
||||||
|
TyrantControlPanel panel = mock(TyrantControlPanel.class);
|
||||||
|
TyrantControlPanelJoinListener listener = new TyrantControlPanelJoinListener(
|
||||||
|
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(tyrant);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package games.dmg.spigottyrant;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
final class TyrantControlPanelModelTest {
|
||||||
|
@Test
|
||||||
|
void overviewShowsProgressPurchasesAssignmentsAndIntelligenceCooldown() {
|
||||||
|
UUID tyrantId = UUID.fromString("11111111-1111-1111-1111-111111111111");
|
||||||
|
UUID assassinId = UUID.fromString("22222222-2222-2222-2222-222222222222");
|
||||||
|
Instant now = Instant.parse("2026-08-21T12:00:00Z");
|
||||||
|
GameState game = new GameState(
|
||||||
|
GameLifecycle.RUNNING, Optional.of(tyrantId), Optional.empty(),
|
||||||
|
Optional.empty(), Optional.empty(), Optional.empty(), Duration.ZERO,
|
||||||
|
3, 2, Set.of(TyrantUnlock.ASSASSIN, TyrantUnlock.ROSTER_INTELLIGENCE)
|
||||||
|
);
|
||||||
|
PlayerState tyrant = new PlayerState(
|
||||||
|
tyrantId, "Tyrant", Optional.of(now), Optional.empty(), TyrantClass.NONE,
|
||||||
|
Optional.empty(), Map.of(Ability.ROSTER_INTELLIGENCE, now.plusSeconds(90)),
|
||||||
|
Set.of(), java.util.List.of()
|
||||||
|
);
|
||||||
|
PlayerState assassin = new PlayerState(
|
||||||
|
assassinId, "Blade", Optional.of(now), Optional.empty(), TyrantClass.ASSASSIN,
|
||||||
|
Optional.empty(), Map.of(), Set.of(), java.util.List.of()
|
||||||
|
);
|
||||||
|
|
||||||
|
TyrantControlPanelModel model = TyrantControlPanelModel.create(
|
||||||
|
game, tyrant, Map.of(tyrantId, tyrant, assassinId, assassin), now
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals(3, model.level());
|
||||||
|
assertEquals(2, model.unspentChoices());
|
||||||
|
assertEquals(UnlockAvailability.PURCHASED,
|
||||||
|
model.unlocks().get(TyrantUnlock.ASSASSIN));
|
||||||
|
assertEquals(
|
||||||
|
Set.of(TyrantUnlock.ASSASSIN, TyrantUnlock.ROSTER_INTELLIGENCE),
|
||||||
|
model.purchasedUnlocks()
|
||||||
|
);
|
||||||
|
assertEquals("Blade", model.classHolders().get(TyrantClass.ASSASSIN));
|
||||||
|
assertEquals(Duration.ofSeconds(90), model.intelligenceCooldown());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user