feat(tyrant): add arena boss succession
This commit is contained in:
@@ -8,31 +8,32 @@ description: Defines lifecycle, succession, selection, and pause behavior for a
|
|||||||
|
|
||||||
## Lifecycle
|
## Lifecycle
|
||||||
|
|
||||||
The game begins unstarted. An administrator starts it by assigning the first Tyrant; the administrator may explicitly assign the first Vigilante or allow selection from eligible recently active players. A running game may be paused without deleting its state and later resumed.
|
The game begins unstarted. An administrator starts it by assigning the first Tyrant; the administrator may explicitly assign the first Vigilante or leave that role vacant for the blue arena challenge. A running game may be paused without deleting its state and later resumed.
|
||||||
|
|
||||||
Pausing freezes gameplay progression, role inactivity checks, pending-selection deadlines, ability cooldowns, and daily-use timers. Role and class effects are suppressed while paused and restored on resume.
|
Pausing freezes gameplay progression, role inactivity checks, pending-selection deadlines, ability cooldowns, and daily-use timers. Role and class effects are suppressed while paused and restored on resume.
|
||||||
|
|
||||||
## Tyrant succession
|
## Tyrant succession
|
||||||
|
|
||||||
When a player directly kills the Tyrant, that killer becomes the new Tyrant. When no player is credited with the death, an eligible opted-in player active during the preceding 24 hours is selected randomly. The former Tyrant is excluded when another candidate exists.
|
When a player directly kills the Tyrant, that killer becomes the new Tyrant immediately. When no player is credited with the death, or when the Tyrant becomes inactive or relinquishes, the reign ends and the Tyrant role remains vacant for the shared arena challenge. The former Tyrant may compete again under the same rules as other opted-in participants.
|
||||||
|
|
||||||
Every Tyrant death ends the reign, clears all role-side assignments, resets the former Tyrant's reign progression, and leaves the Vigilante role open for the arena challenge. The new Tyrant receives one starting unlock choice.
|
Every ended Tyrant reign clears all role-side assignments and resets reign progression. The shared arena presents the Tyrant challenge first with a red particle boundary. Its winner becomes Tyrant at level zero with one starting unlock choice; only then may a vacant Vigilante role open the blue challenge at the same location.
|
||||||
|
|
||||||
## Vigilante succession
|
## Vigilante succession
|
||||||
|
|
||||||
Every Vigilante death ends that assignment regardless of cause. Vigilante inactivity and relinquishment also end the assignment, and every new Tyrant reign clears the previous assignment. Followers are cleared rather than transferred.
|
Every Vigilante death ends that assignment regardless of cause. Vigilante inactivity and relinquishment also end the assignment, and every new Tyrant reign clears the previous assignment. Followers are cleared rather than transferred.
|
||||||
|
|
||||||
While the game is running with an active Tyrant and no Vigilante, an administrator-configured arena opens. One eligible opted-in non-Tyrant player at a time may challenge the arena boss. Defeating it assigns that player as Vigilante. Leaving, disconnecting, or dying resets the fight without preventing an immediate retry. Without an active Tyrant or configured arena, the role remains vacant and no boss exists.
|
While the game is running with an active Tyrant, no open Tyrant challenge, and no Vigilante, the administrator-configured arena opens its Vigilante challenge. One eligible opted-in non-Tyrant player at a time may challenge the arena boss. Defeating it assigns that player as Vigilante. Leaving, disconnecting, or dying resets the fight without preventing an immediate retry. Without an active Tyrant or configured arena, the role remains vacant and no boss exists.
|
||||||
|
|
||||||
A Tyrant earns progression from a Vigilante death only when the Tyrant personally killed the Vigilante.
|
A Tyrant earns progression from a Vigilante death only when the Tyrant personally killed the Vigilante.
|
||||||
|
|
||||||
## Pending selections
|
## Pending selections
|
||||||
|
|
||||||
An offline randomly selected Tyrant has 24 hours of unpaused time to log in and confirm the assignment. Failure causes another Tyrant selection. When no eligible Tyrant candidate exists, the position remains vacant and selection retries periodically. Vigilante vacancies use the arena rather than pending random selection.
|
New random and pending role selections are not created. Legacy pending Tyrant or Vigilante selections are cleared into the applicable arena vacancy without granting a role. A Tyrant vacancy has priority, so the two role challenges cannot run concurrently.
|
||||||
|
|
||||||
## Related stories
|
## Related stories
|
||||||
|
|
||||||
- [Start, pause, and administer the game](../user-stories/us-001-start-pause-and-administer.md)
|
- [Start, pause, and administer the game](../user-stories/us-001-start-pause-and-administer.md)
|
||||||
- [Transfer and reset the Tyrant role](../user-stories/us-002-transfer-and-reset-tyrant.md)
|
- [Transfer and reset the Tyrant role](../user-stories/us-002-transfer-and-reset-tyrant.md)
|
||||||
- [Handle inactivity and pending selections](../user-stories/us-010-handle-inactivity-and-selection.md)
|
- [Handle inactivity and arena vacancies](../user-stories/us-010-handle-inactivity-and-selection.md)
|
||||||
- [Claim the Vigilante role in the arena](../user-stories/us-019-claim-vigilante-in-arena.md)
|
- [Claim the Vigilante role in the arena](../user-stories/us-019-claim-vigilante-in-arena.md)
|
||||||
|
- [Claim the Tyrant role in the arena](../user-stories/us-020-claim-tyrant-in-arena.md)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ description: Defines the durable state and timekeeping needed to restore a Tyran
|
|||||||
|
|
||||||
# Persistence Model
|
# Persistence Model
|
||||||
|
|
||||||
State is keyed by player UUID while retaining the latest known player name for display. Durable global state includes lifecycle status, current and pending role holders, selection deadlines, paused durations, the current Tyrant reign, and the administrator-configured Vigilante arena center. Active boss and challenger entities are transient and are reconstructed or reset from authoritative role and arena state after restart.
|
State is keyed by player UUID while retaining the latest known player name for display. Durable global state includes lifecycle status, current and pending role holders, selection deadlines, paused durations, the current Tyrant reign, and the administrator-configured Vigilante arena center. Active Tyrant or Vigilante challenge bosses and challenger entities are transient and are reconstructed or reset from authoritative role and shared-arena state after restart.
|
||||||
|
|
||||||
Durable player state includes participation status, opt-out deadline, login history, role or class, Tyrant level, purchased unlocks, unspent choices, Follower relationship, ability cooldowns, daily intelligence use, bound-item delivery state, and captured mobs.
|
Durable player state includes participation status, opt-out deadline, login history, role or class, Tyrant level, purchased unlocks, unspent choices, Follower relationship, ability cooldowns, daily intelligence use, bound-item delivery state, and captured mobs.
|
||||||
|
|
||||||
@@ -18,3 +18,4 @@ Elapsed-time rules use UTC instants. Paused time is excluded from timers. Saves
|
|||||||
- [Configure and persist game state](../user-stories/us-012-configure-and-persist-state.md)
|
- [Configure and persist game state](../user-stories/us-012-configure-and-persist-state.md)
|
||||||
- [Use class ability items](../user-stories/us-014-use-class-ability-items.md)
|
- [Use class ability items](../user-stories/us-014-use-class-ability-items.md)
|
||||||
- [Claim the Vigilante role in the arena](../user-stories/us-019-claim-vigilante-in-arena.md)
|
- [Claim the Vigilante role in the arena](../user-stories/us-019-claim-vigilante-in-arena.md)
|
||||||
|
- [Claim the Tyrant role in the arena](../user-stories/us-020-claim-tyrant-in-arena.md)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ description: Defines Tyrant-side classes, Vigilante-side membership, progression
|
|||||||
|
|
||||||
## Roles and affiliations
|
## Roles and affiliations
|
||||||
|
|
||||||
A running game has one active or pending Tyrant. It has at most one Vigilante, and a vacant Vigilante role remains open until an eligible player wins the configured arena challenge. The Vigilante may recruit Followers through invitations that require acceptance. The Tyrant may assign the Assassin, Fixer, and Tamer classes after unlocking them.
|
A running game has at most one Tyrant and at most one Vigilante. A vacant Tyrant role takes priority in the shared arena; after its winner becomes Tyrant, a vacant Vigilante role may use the same arena. The Vigilante may recruit Followers through invitations that require acceptance. The Tyrant may assign the Assassin, Fixer, and Tamer classes after unlocking them.
|
||||||
|
|
||||||
Role alignment and Tyrant-side class are independent. A Vigilante or Follower may simultaneously hold one Tyrant-side class, allowing divided loyalties. A player cannot simultaneously be the Tyrant, Vigilante, and/or a Follower, and cannot hold more than one Tyrant-side class. Each Tyrant-side class may be assigned to only one player at a time. Opted-out players are ineligible for every assignment.
|
Role alignment and Tyrant-side class are independent. A Vigilante or Follower may simultaneously hold one Tyrant-side class, allowing divided loyalties. A player cannot simultaneously be the Tyrant, Vigilante, and/or a Follower, and cannot hold more than one Tyrant-side class. Each Tyrant-side class may be assigned to only one player at a time. Opted-out players are ineligible for every assignment.
|
||||||
|
|
||||||
@@ -37,3 +37,4 @@ Vigilante Follower scaling is capped by default at Strength V and Resistance IV,
|
|||||||
- [Assign unlocked classes](../user-stories/us-004-assign-unlocked-classes.md)
|
- [Assign unlocked classes](../user-stories/us-004-assign-unlocked-classes.md)
|
||||||
- [Support the Vigilante and Followers](../user-stories/us-008-support-vigilante-and-followers.md)
|
- [Support the Vigilante and Followers](../user-stories/us-008-support-vigilante-and-followers.md)
|
||||||
- [Claim the Vigilante role in the arena](../user-stories/us-019-claim-vigilante-in-arena.md)
|
- [Claim the Vigilante role in the arena](../user-stories/us-019-claim-vigilante-in-arena.md)
|
||||||
|
- [Claim the Tyrant role in the arena](../user-stories/us-020-claim-tyrant-in-arena.md)
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ This bundle documents the asymmetric Tyrant game, its role succession, class abi
|
|||||||
|
|
||||||
- [User stories](user-stories/index.md)
|
- [User stories](user-stories/index.md)
|
||||||
- [Vigilante arena challenge](user-stories/us-019-claim-vigilante-in-arena.md)
|
- [Vigilante arena challenge](user-stories/us-019-claim-vigilante-in-arena.md)
|
||||||
|
- [Tyrant arena challenge](user-stories/us-020-claim-tyrant-in-arena.md)
|
||||||
- [Game state machine](concepts/game-state-machine.md)
|
- [Game state machine](concepts/game-state-machine.md)
|
||||||
- [Role and class model](concepts/role-and-class-model.md)
|
- [Role and class model](concepts/role-and-class-model.md)
|
||||||
- [Persistence model](concepts/persistence-model.md)
|
- [Persistence model](concepts/persistence-model.md)
|
||||||
|
|||||||
@@ -6,6 +6,14 @@ description: Chronological record of material decisions affecting the Spigot Tyr
|
|||||||
|
|
||||||
# Spigot Tyrant Design Log
|
# Spigot Tyrant Design Log
|
||||||
|
|
||||||
|
## 2026-08-23 — Shared Tyrant and Vigilante arena succession completed
|
||||||
|
|
||||||
|
- Completed US-020 and reopened related succession, inactivity, administration, configuration, and Vigilante-arena stories so non-player Tyrant replacement is earned rather than random.
|
||||||
|
- Environmental Tyrant death, inactivity, and relinquishment now clear the reign and open the shared one-player Tyrant boss challenge; a directly credited player killer still succeeds immediately.
|
||||||
|
- The shared arena prioritizes a vacant Tyrant with a red particle circle, then opens a vacant Vigilante challenge with a blue circle after a Tyrant becomes active; neutral particles remain while neither challenge is open.
|
||||||
|
- Both roles share the configured location, boss balance, barriers, containment, mob exclusion, reset behavior, notifications, and restart protections, while an active Tyrant remains unable to enter.
|
||||||
|
- Removed new random and pending role selection, retained safe migration of legacy pending state and the legacy maintenance-interval configuration key, and verified 115 automated tests plus `./gradlew clean check jar`.
|
||||||
|
|
||||||
## 2026-08-23 — Vigilante arena succession completed
|
## 2026-08-23 — Vigilante arena succession completed
|
||||||
|
|
||||||
- Completed US-019 and reopened related administration, Vigilante, inactivity, participation, persistence, control-panel, and completion stories to replace automatic Vigilante selection with an earned arena challenge.
|
- Completed US-019 and reopened related administration, Vigilante, inactivity, participation, persistence, control-panel, and completion stories to replace automatic Vigilante selection with an earned arena challenge.
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
7. [US-007: Capture and place mobs as a Tamer](us-007-capture-and-place-mobs.md)
|
7. [US-007: Capture and place mobs as a Tamer](us-007-capture-and-place-mobs.md)
|
||||||
8. [US-008: Support the Vigilante and Followers](us-008-support-vigilante-and-followers.md)
|
8. [US-008: Support the Vigilante and Followers](us-008-support-vigilante-and-followers.md)
|
||||||
9. [US-009: Purchase and use Tyrant abilities](us-009-use-tyrant-abilities.md)
|
9. [US-009: Purchase and use Tyrant abilities](us-009-use-tyrant-abilities.md)
|
||||||
10. [US-010: Handle inactivity and pending selections](us-010-handle-inactivity-and-selection.md)
|
10. [US-010: Handle inactivity and arena vacancies](us-010-handle-inactivity-and-selection.md)
|
||||||
11. [US-011: Inform players and manage participation](us-011-inform-and-manage-participation.md)
|
11. [US-011: Inform players and manage participation](us-011-inform-and-manage-participation.md)
|
||||||
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)
|
||||||
@@ -19,3 +19,4 @@
|
|||||||
17. [US-017: Use bound role control items](us-017-use-bound-role-control-items.md)
|
17. [US-017: Use bound role control items](us-017-use-bound-role-control-items.md)
|
||||||
18. [US-018: Complete Tyrant commands contextually](us-018-complete-commands-contextually.md)
|
18. [US-018: Complete Tyrant commands contextually](us-018-complete-commands-contextually.md)
|
||||||
19. [US-019: Claim the Vigilante role in the arena](us-019-claim-vigilante-in-arena.md)
|
19. [US-019: Claim the Vigilante role in the arena](us-019-claim-vigilante-in-arena.md)
|
||||||
|
20. [US-020: Claim the Tyrant role in the arena](us-020-claim-tyrant-in-arena.md)
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ As a **server operator**, I want to start, pause, resume, and inspect the game s
|
|||||||
- [x] Pausing freezes progression, cooldowns, daily-use timers, inactivity checks, and pending-selection deadlines.
|
- [x] Pausing freezes progression, cooldowns, daily-use timers, inactivity checks, and pending-selection deadlines.
|
||||||
- [x] Role and class effects are suppressed while paused and restored when play resumes.
|
- [x] Role and class effects are suppressed while paused and restored when play resumes.
|
||||||
- [x] Administrative commands require the `spigottyrant.admin` permission, granted to server operators by default.
|
- [x] Administrative commands require the `spigottyrant.admin` permission, granted to server operators by default.
|
||||||
- [x] Administrators can inspect lifecycle state, roles, classes, Followers, purchases, levels, cooldowns, participation, pending selections, and the configured Vigilante arena location.
|
- [x] Administrators can inspect lifecycle state, roles, classes, Followers, purchases, levels, cooldowns, participation, legacy pending selections, and the configured shared role arena location.
|
||||||
- [x] Destructive administrative operations require explicit confirmation.
|
- [x] Destructive administrative operations require explicit confirmation.
|
||||||
|
|
||||||
## Related
|
## Related
|
||||||
|
|||||||
@@ -12,11 +12,11 @@ As a **player**, I want defeating the Tyrant to transfer power predictably so th
|
|||||||
## Acceptance criteria
|
## Acceptance criteria
|
||||||
|
|
||||||
- [x] A player directly credited with killing the Tyrant becomes the new Tyrant.
|
- [x] A player directly credited with killing the Tyrant becomes the new Tyrant.
|
||||||
- [x] A Tyrant death without a player killer selects an eligible opted-in player active within the preceding 24 hours at random.
|
- [x] A Tyrant death without a player killer leaves the role vacant and opens the shared Tyrant arena challenge.
|
||||||
- [x] The former Tyrant is excluded from random succession when another eligible candidate exists.
|
- [x] The former Tyrant may compete again under the same arena eligibility and reset rules as every other opted-in participant.
|
||||||
- [x] The former Tyrant's level, unlocks, unspent choices, and Tyrant ability cooldowns reset when the reign ends.
|
- [x] The former Tyrant's level, unlocks, unspent choices, and Tyrant ability cooldowns reset when the reign ends.
|
||||||
- [x] Every Assassin, Fixer, Tamer, Vigilante, and Follower assignment is cleared when the Tyrant dies.
|
- [x] Every Assassin, Fixer, Tamer, Vigilante, and Follower assignment is cleared when the Tyrant dies.
|
||||||
- [x] A new Vigilante is selected under the normal candidate rules after succession.
|
- [x] After Tyrant succession, a vacant Vigilante role opens the shared arena's Vigilante challenge.
|
||||||
- [x] The new Tyrant receives exactly one starting unlock choice.
|
- [x] The new Tyrant receives exactly one starting unlock choice.
|
||||||
- [x] The Tyrant's displayed name is red and the Tyrant has a red glowing outline while the game is running.
|
- [x] The Tyrant's displayed name is red and the Tyrant has a red glowing outline while the game is running.
|
||||||
- [x] Succession cannot assign the same player as both Tyrant and Vigilante.
|
- [x] Succession cannot assign the same player as both Tyrant and Vigilante.
|
||||||
@@ -25,3 +25,4 @@ As a **player**, I want defeating the Tyrant to transfer power predictably so th
|
|||||||
|
|
||||||
- [Tyrant game state machine](../concepts/game-state-machine.md)
|
- [Tyrant game state machine](../concepts/game-state-machine.md)
|
||||||
- [Progress and purchase Tyrant unlocks](us-003-progress-and-purchase-unlocks.md)
|
- [Progress and purchase Tyrant unlocks](us-003-progress-and-purchase-unlocks.md)
|
||||||
|
- [Claim the Tyrant role in the arena](us-020-claim-tyrant-in-arena.md)
|
||||||
|
|||||||
@@ -31,6 +31,6 @@ As the **Vigilante**, I want nearby living Followers to strengthen me against th
|
|||||||
## 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 arena vacancies](us-010-handle-inactivity-and-selection.md)
|
||||||
- [Manage Followers through the Vigilante control panel](us-016-manage-vigilante-control-panel.md)
|
- [Manage Followers through the Vigilante control panel](us-016-manage-vigilante-control-panel.md)
|
||||||
- [Claim the Vigilante role in the arena](us-019-claim-vigilante-in-arena.md)
|
- [Claim the Vigilante role in the arena](us-019-claim-vigilante-in-arena.md)
|
||||||
|
|||||||
@@ -1,25 +1,23 @@
|
|||||||
---
|
---
|
||||||
type: User Story
|
type: User Story
|
||||||
title: "US-010: Handle inactivity and pending selections"
|
title: "US-010: Handle inactivity and arena vacancies"
|
||||||
description: Replace absent or unconfirmed role holders using recently active eligible players.
|
description: Replace inactive role holders through the shared arena without granting legacy pending selections.
|
||||||
status: done
|
status: done
|
||||||
---
|
---
|
||||||
|
|
||||||
# US-010: Handle inactivity and pending selections
|
# US-010: Handle inactivity and arena vacancies
|
||||||
|
|
||||||
As a **participant**, I want inactive central roles to be replaced fairly so that the game does not stall around absent players.
|
As a **participant**, I want inactive central roles to be replaced fairly so that the game does not stall around absent players.
|
||||||
|
|
||||||
## Acceptance criteria
|
## Acceptance criteria
|
||||||
|
|
||||||
- [x] A Tyrant or Vigilante who has not logged in for 48 unpaused hours loses the position.
|
- [x] A Tyrant or Vigilante who has not logged in for 48 unpaused hours loses the position.
|
||||||
- [x] Random candidates are drawn from eligible opted-in players who logged in during the preceding 24 hours.
|
- [x] Tyrant and Vigilante vacancies use their shared arena challenges rather than random candidate selection.
|
||||||
- [x] The Tyrant is always excluded from Vigilante selection.
|
- [x] The Tyrant is always excluded from Vigilante selection.
|
||||||
- [x] A previous role holder is excluded when another eligible candidate exists and may be reused only as a fallback.
|
- [x] Previous role holders may compete for their former role under the same arena eligibility and reset rules as other opted-in participants.
|
||||||
- [x] Tyrant inactivity ends the reign, clears all assignments, invokes normal random Tyrant succession, and opens the Vigilante arena challenge once a Tyrant is active.
|
- [x] Tyrant inactivity ends the reign, clears all assignments, and opens the Tyrant arena challenge; the Vigilante challenge can open only after a Tyrant becomes active.
|
||||||
- [x] Vigilante inactivity clears Followers and opens the Vigilante arena challenge without changing the Tyrant.
|
- [x] Vigilante inactivity clears Followers and opens the Vigilante arena challenge without changing the Tyrant.
|
||||||
- [x] An offline randomly selected candidate must log in within 24 unpaused hours to confirm the assignment.
|
- [x] Legacy pending role selections are cleared safely into the applicable arena vacancy without granting a role.
|
||||||
- [x] An unconfirmed Tyrant selection expires and rerolls automatically under the same eligibility rules.
|
|
||||||
- [x] Tyrant selection remains vacant and retries periodically when no valid candidate exists.
|
|
||||||
- [x] Candidate selection cannot assign conflicting roles or choose an opted-out player, including during concurrent joins and deaths.
|
- [x] Candidate selection cannot assign conflicting roles or choose an opted-out player, including during concurrent joins and deaths.
|
||||||
|
|
||||||
## Related
|
## Related
|
||||||
@@ -27,3 +25,4 @@ As a **participant**, I want inactive central roles to be replaced fairly so tha
|
|||||||
- [Tyrant game state machine](../concepts/game-state-machine.md)
|
- [Tyrant game state machine](../concepts/game-state-machine.md)
|
||||||
- [Inform players and manage participation](us-011-inform-and-manage-participation.md)
|
- [Inform players and manage participation](us-011-inform-and-manage-participation.md)
|
||||||
- [Claim the Vigilante role in the arena](us-019-claim-vigilante-in-arena.md)
|
- [Claim the Vigilante role in the arena](us-019-claim-vigilante-in-arena.md)
|
||||||
|
- [Claim the Tyrant role in the arena](us-020-claim-tyrant-in-arena.md)
|
||||||
|
|||||||
@@ -34,6 +34,6 @@ As a **player**, I want to understand my status and control my eligibility so th
|
|||||||
|
|
||||||
## Related
|
## Related
|
||||||
|
|
||||||
- [Handle inactivity and pending selections](us-010-handle-inactivity-and-selection.md)
|
- [Handle inactivity and arena vacancies](us-010-handle-inactivity-and-selection.md)
|
||||||
- [Persistence model](../concepts/persistence-model.md)
|
- [Persistence model](../concepts/persistence-model.md)
|
||||||
- [Claim the Vigilante role in the arena](us-019-claim-vigilante-in-arena.md)
|
- [Claim the Vigilante role in the arena](us-019-claim-vigilante-in-arena.md)
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ As a **server operator**, I want configurable and durable game behavior so that
|
|||||||
|
|
||||||
## Acceptance criteria
|
## Acceptance criteria
|
||||||
|
|
||||||
- [x] Configuration covers ranges, durations, cooldowns, inactivity periods, candidate windows, retry intervals, effect levels and caps, mob restrictions, item materials and names, messages, timer behavior, and Vigilante arena boss balance.
|
- [x] Configuration covers ranges, durations, cooldowns, inactivity and maintenance intervals, effect levels and caps, mob restrictions, item materials and names, messages, timer behavior, and shared-role-arena boss balance.
|
||||||
- [x] Defaults match the approved user stories, including a 50-block Tyrant range and Follower range, seven-day opt-out, 48-hour inactivity, and 24-hour candidate windows.
|
- [x] Defaults match the approved user stories, including a 50-block Tyrant range and Follower range, seven-day opt-out, 48-hour inactivity, and one-minute role maintenance.
|
||||||
- [x] Invalid required configuration prevents partial plugin initialization and produces a clear server log message.
|
- [x] Invalid required configuration prevents partial plugin initialization and produces a clear server log message.
|
||||||
- [x] UUID-keyed state stores lifecycle, current and pending roles, assignments, login history, participation, reign progression, purchases, choices, cooldowns, paused time, item delivery, and captured mobs; the administrator-set arena center is durable across restarts.
|
- [x] UUID-keyed state stores lifecycle, current and pending roles, assignments, login history, participation, reign progression, purchases, choices, cooldowns, paused time, item delivery, and captured mobs; the administrator-set arena center is durable across restarts.
|
||||||
- [x] Cooldowns and deadlines use UTC instants and exclude administratively paused time.
|
- [x] Cooldowns and deadlines use UTC instants and exclude administratively paused time.
|
||||||
@@ -26,3 +26,4 @@ As a **server operator**, I want configurable and durable game behavior so that
|
|||||||
- [Persistence model](../concepts/persistence-model.md)
|
- [Persistence model](../concepts/persistence-model.md)
|
||||||
- [Build, test, and release the plugin](us-013-build-test-and-release.md)
|
- [Build, test, and release the plugin](us-013-build-test-and-release.md)
|
||||||
- [Claim the Vigilante role in the arena](us-019-claim-vigilante-in-arena.md)
|
- [Claim the Vigilante role in the arena](us-019-claim-vigilante-in-arena.md)
|
||||||
|
- [Claim the Tyrant role in the arena](us-020-claim-tyrant-in-arena.md)
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ Automated tests verify default and explicit command entry points, active-Vigilan
|
|||||||
## Related
|
## Related
|
||||||
|
|
||||||
- [Support the Vigilante and Followers](us-008-support-vigilante-and-followers.md)
|
- [Support the Vigilante and Followers](us-008-support-vigilante-and-followers.md)
|
||||||
- [Handle inactivity and pending selections](us-010-handle-inactivity-and-selection.md)
|
- [Handle inactivity and arena vacancies](us-010-handle-inactivity-and-selection.md)
|
||||||
- [Manage a reign through the Tyrant control panel](us-015-manage-tyrant-control-panel.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)
|
- [Role and class model](../concepts/role-and-class-model.md)
|
||||||
- [Use bound role control items](us-017-use-bound-role-control-items.md)
|
- [Use bound role control items](us-017-use-bound-role-control-items.md)
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ As an **opted-in participant**, I want a visible one-player boss challenge for a
|
|||||||
## Acceptance criteria
|
## Acceptance criteria
|
||||||
|
|
||||||
- [x] An administrator can set and inspect a durable arena center, and configuration validates the arena radius and boss strength.
|
- [x] An administrator can set and inspect a durable arena center, and configuration validates the arena radius and boss strength.
|
||||||
- [x] The configured arena has a continuously visible particle boundary, including when no fight is active.
|
- [x] The configured arena has a continuously visible neutral boundary when no challenge is open and a blue particle boundary while the Vigilante challenge is open.
|
||||||
- [x] A vacant Vigilante role opens the challenge after death, inactivity, relinquishment, or a new Tyrant reign, but a boss exists only while a Tyrant is active and the game is running.
|
- [x] A vacant Vigilante role opens the challenge after death, inactivity, relinquishment, or a new Tyrant reign, but only while a Tyrant is active, no Tyrant challenge is open, and the game is running.
|
||||||
- [x] The challenge boss defaults to an armored, 80-health enhanced zombie, does not burn in sunlight, and remains within a default 10-block-radius arena.
|
- [x] The challenge boss defaults to an armored, 80-health enhanced zombie, does not burn in sunlight, and remains within a default 10-block-radius arena.
|
||||||
- [x] All online opted-in players are told when the role opens and receive the arena world and coordinates; opted-in players receive the same notice when they log in while it remains open, and opted-out players do not.
|
- [x] All online opted-in players are told when the role opens and receive the arena world and coordinates; opted-in players receive the same notice when they log in while it remains open, and opted-out players do not.
|
||||||
- [x] Exactly one eligible opted-in non-Tyrant player may enter as challenger, including when entry is attempted by walking or teleportation.
|
- [x] Exactly one eligible opted-in non-Tyrant player may enter as challenger, including when entry is attempted by walking or teleportation.
|
||||||
@@ -30,6 +30,7 @@ As an **opted-in participant**, I want a visible one-player boss challenge for a
|
|||||||
|
|
||||||
- [Start, pause, and administer the game](us-001-start-pause-and-administer.md)
|
- [Start, pause, and administer the game](us-001-start-pause-and-administer.md)
|
||||||
- [Support the Vigilante and Followers](us-008-support-vigilante-and-followers.md)
|
- [Support the Vigilante and Followers](us-008-support-vigilante-and-followers.md)
|
||||||
- [Handle inactivity and pending selections](us-010-handle-inactivity-and-selection.md)
|
- [Handle inactivity and arena vacancies](us-010-handle-inactivity-and-selection.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)
|
||||||
- [Tyrant game state machine](../concepts/game-state-machine.md)
|
- [Tyrant game state machine](../concepts/game-state-machine.md)
|
||||||
|
- [Claim the Tyrant role in the arena](us-020-claim-tyrant-in-arena.md)
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
---
|
||||||
|
type: User Story
|
||||||
|
title: "US-020: Claim the Tyrant role in the arena"
|
||||||
|
description: Use the shared one-player arena to replace a Tyrant who was not defeated by another player.
|
||||||
|
status: done
|
||||||
|
---
|
||||||
|
|
||||||
|
# US-020: Claim the Tyrant role in the arena
|
||||||
|
|
||||||
|
As an **opted-in participant**, I want to earn a vacant Tyrant role through the shared boss arena so that environmental death, inactivity, and relinquishment do not assign power randomly.
|
||||||
|
|
||||||
|
## Acceptance criteria
|
||||||
|
|
||||||
|
- [x] A player directly credited with killing the Tyrant still becomes the new Tyrant immediately without an arena fight.
|
||||||
|
- [x] Tyrant death without a player killer, Tyrant inactivity, and Tyrant relinquishment end the reign, clear all role-side assignments, and open the shared arena with no active or pending Tyrant.
|
||||||
|
- [x] The shared arena opens only one role challenge at a time: a vacant Tyrant takes priority, and the Vigilante challenge can open only after a Tyrant becomes active.
|
||||||
|
- [x] The passive arena boundary uses red particles for an open Tyrant challenge, blue particles for an open Vigilante challenge, and a neutral marker while neither role challenge is open.
|
||||||
|
- [x] The Tyrant challenge uses the existing configured arena, boss balance, mob exclusion, one-challenger barrier, containment, reset, notification, and missing-location rules.
|
||||||
|
- [x] An eligible opted-in participant, including the former Tyrant, may challenge for the vacant Tyrant role; an active Tyrant cannot enter any arena fight.
|
||||||
|
- [x] Defeating the Tyrant challenge boss assigns the challenger as Tyrant with level zero, no purchases, and exactly one starting unlock choice.
|
||||||
|
- [x] Completing the Tyrant challenge immediately permits the blue Vigilante challenge to open at the same location when the Vigilante role is vacant.
|
||||||
|
- [x] Legacy pending Tyrant replacement state is cleared into an arena vacancy without granting a role.
|
||||||
|
- [x] Pausing, resetting, restarting, reloading, and repeated or concurrent events cannot duplicate either role boss, challenger, barrier, or assignment.
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- [Transfer and reset the Tyrant role](us-002-transfer-and-reset-tyrant.md)
|
||||||
|
- [Handle inactivity and arena vacancies](us-010-handle-inactivity-and-selection.md)
|
||||||
|
- [Claim the Vigilante role in the arena](us-019-claim-vigilante-in-arena.md)
|
||||||
|
- [Tyrant game state machine](../concepts/game-state-machine.md)
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package games.dmg.spigottyrant;
|
||||||
|
|
||||||
|
public enum ArenaRole {
|
||||||
|
TYRANT,
|
||||||
|
VIGILANTE
|
||||||
|
}
|
||||||
@@ -18,9 +18,7 @@ public record PluginSettings(
|
|||||||
double vigilanteBossArmor,
|
double vigilanteBossArmor,
|
||||||
Duration optOutDuration,
|
Duration optOutDuration,
|
||||||
Duration roleInactivity,
|
Duration roleInactivity,
|
||||||
Duration candidateActivityWindow,
|
Duration roleMaintenanceInterval,
|
||||||
Duration pendingSelectionTimeout,
|
|
||||||
Duration selectionRetryInterval,
|
|
||||||
Duration assassinCooldown,
|
Duration assassinCooldown,
|
||||||
Duration assassinInvisibilityDuration,
|
Duration assassinInvisibilityDuration,
|
||||||
Duration assassinDoubleJumpCooldown,
|
Duration assassinDoubleJumpCooldown,
|
||||||
@@ -60,9 +58,7 @@ public record PluginSettings(
|
|||||||
requireFiniteRange(vigilanteBossArmor, 0.0, 30.0, "vigilante-boss-armor");
|
requireFiniteRange(vigilanteBossArmor, 0.0, 30.0, "vigilante-boss-armor");
|
||||||
requirePositive(optOutDuration, "opt-out-seconds");
|
requirePositive(optOutDuration, "opt-out-seconds");
|
||||||
requirePositive(roleInactivity, "role-inactivity-seconds");
|
requirePositive(roleInactivity, "role-inactivity-seconds");
|
||||||
requirePositive(candidateActivityWindow, "candidate-activity-window-seconds");
|
requirePositive(roleMaintenanceInterval, "role-maintenance-seconds");
|
||||||
requirePositive(pendingSelectionTimeout, "pending-selection-timeout-seconds");
|
|
||||||
requirePositive(selectionRetryInterval, "selection-retry-seconds");
|
|
||||||
requirePositive(assassinCooldown, "assassin-cooldown-seconds");
|
requirePositive(assassinCooldown, "assassin-cooldown-seconds");
|
||||||
requirePositive(assassinInvisibilityDuration, "assassin-invisibility-seconds");
|
requirePositive(assassinInvisibilityDuration, "assassin-invisibility-seconds");
|
||||||
requirePositive(assassinDoubleJumpCooldown, "assassin-double-jump-cooldown-seconds");
|
requirePositive(assassinDoubleJumpCooldown, "assassin-double-jump-cooldown-seconds");
|
||||||
@@ -109,9 +105,10 @@ public record PluginSettings(
|
|||||||
decimal(values, "vigilante-boss-armor", 10.0),
|
decimal(values, "vigilante-boss-armor", 10.0),
|
||||||
duration(values, "opt-out-seconds", Duration.ofDays(7)),
|
duration(values, "opt-out-seconds", Duration.ofDays(7)),
|
||||||
duration(values, "role-inactivity-seconds", Duration.ofHours(48)),
|
duration(values, "role-inactivity-seconds", Duration.ofHours(48)),
|
||||||
duration(values, "candidate-activity-window-seconds", Duration.ofHours(24)),
|
durationAlias(
|
||||||
duration(values, "pending-selection-timeout-seconds", Duration.ofHours(24)),
|
values, "role-maintenance-seconds", "selection-retry-seconds",
|
||||||
duration(values, "selection-retry-seconds", Duration.ofMinutes(1)),
|
Duration.ofMinutes(1)
|
||||||
|
),
|
||||||
duration(values, "assassin-cooldown-seconds", Duration.ofHours(1)),
|
duration(values, "assassin-cooldown-seconds", Duration.ofHours(1)),
|
||||||
duration(values, "assassin-invisibility-seconds", Duration.ofMinutes(10)),
|
duration(values, "assassin-invisibility-seconds", Duration.ofMinutes(10)),
|
||||||
duration(values, "assassin-double-jump-cooldown-seconds", Duration.ofSeconds(60)),
|
duration(values, "assassin-double-jump-cooldown-seconds", Duration.ofSeconds(60)),
|
||||||
@@ -289,6 +286,18 @@ public record PluginSettings(
|
|||||||
return value.trim();
|
return value.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Duration durationAlias(
|
||||||
|
Map<String, ?> values,
|
||||||
|
String key,
|
||||||
|
String legacyKey,
|
||||||
|
Duration defaultValue
|
||||||
|
) {
|
||||||
|
if (values.containsKey(key)) {
|
||||||
|
return duration(values, key, defaultValue);
|
||||||
|
}
|
||||||
|
return duration(values, legacyKey, defaultValue);
|
||||||
|
}
|
||||||
|
|
||||||
private static Duration duration(Map<String, ?> values, String key, Duration defaultValue) {
|
private static Duration duration(Map<String, ?> values, String key, Duration defaultValue) {
|
||||||
Object raw = values.get(key);
|
Object raw = values.get(key);
|
||||||
if (raw == null) {
|
if (raw == null) {
|
||||||
|
|||||||
+98
-50
@@ -5,6 +5,7 @@ import java.util.Optional;
|
|||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import org.bukkit.ChatColor;
|
import org.bukkit.ChatColor;
|
||||||
|
import org.bukkit.Color;
|
||||||
import org.bukkit.Location;
|
import org.bukkit.Location;
|
||||||
import org.bukkit.Particle;
|
import org.bukkit.Particle;
|
||||||
import org.bukkit.Server;
|
import org.bukkit.Server;
|
||||||
@@ -31,24 +32,27 @@ import org.bukkit.event.player.PlayerQuitEvent;
|
|||||||
import org.bukkit.event.player.PlayerRespawnEvent;
|
import org.bukkit.event.player.PlayerRespawnEvent;
|
||||||
import org.bukkit.event.player.PlayerTeleportEvent;
|
import org.bukkit.event.player.PlayerTeleportEvent;
|
||||||
|
|
||||||
public final class VigilanteArenaController implements Listener, Runnable {
|
public final class RoleArenaController implements Listener, Runnable {
|
||||||
private static final String BOSS_TAG = "spigottyrant-vigilante-boss";
|
private static final String BOSS_TAG = "spigottyrant-role-arena-boss";
|
||||||
private final TyrantStateManager stateManager;
|
private final TyrantStateManager stateManager;
|
||||||
private final Server server;
|
private final Server server;
|
||||||
private final ArenaLocationStore locations;
|
private final ArenaLocationStore locations;
|
||||||
private final PluginSettings settings;
|
private final PluginSettings settings;
|
||||||
private final VigilanteArenaSuccessionService succession =
|
private final VigilanteArenaSuccessionService vigilanteSuccession =
|
||||||
new VigilanteArenaSuccessionService();
|
new VigilanteArenaSuccessionService();
|
||||||
|
private final TyrantArenaSuccessionService tyrantSuccession =
|
||||||
|
new TyrantArenaSuccessionService();
|
||||||
private final VigilanteArenaBarrier barrier = new VigilanteArenaBarrier();
|
private final VigilanteArenaBarrier barrier = new VigilanteArenaBarrier();
|
||||||
private final Set<UUID> relocating = new HashSet<>();
|
private final Set<UUID> relocating = new HashSet<>();
|
||||||
private UUID bossId;
|
private UUID bossId;
|
||||||
private UUID challengerId;
|
private UUID challengerId;
|
||||||
private boolean spawningBoss;
|
private boolean spawningBoss;
|
||||||
private boolean wasOpen;
|
private ArenaRole previousOpenRole;
|
||||||
private boolean warnedMissingLocation;
|
private ArenaRole bossRole;
|
||||||
|
private ArenaRole warnedMissingRole;
|
||||||
private ArenaLocation observedLocation;
|
private ArenaLocation observedLocation;
|
||||||
|
|
||||||
public VigilanteArenaController(
|
public RoleArenaController(
|
||||||
TyrantStateManager stateManager,
|
TyrantStateManager stateManager,
|
||||||
Server server,
|
Server server,
|
||||||
ArenaLocationStore locations,
|
ArenaLocationStore locations,
|
||||||
@@ -62,12 +66,13 @@ public final class VigilanteArenaController implements Listener, Runnable {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
|
Optional<ArenaRole> openRole = RoleArenaPolicy.openRole(stateManager.game());
|
||||||
Optional<ArenaLocation> configured = locations.location();
|
Optional<ArenaLocation> configured = locations.location();
|
||||||
if (configured.isEmpty()) {
|
if (configured.isEmpty()) {
|
||||||
closeFight();
|
closeFight();
|
||||||
observedLocation = null;
|
observedLocation = null;
|
||||||
warnIfOpenWithoutLocation();
|
warnIfOpenWithoutLocation(openRole);
|
||||||
wasOpen = false;
|
previousOpenRole = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ArenaLocation arena = configured.orElseThrow();
|
ArenaLocation arena = configured.orElseThrow();
|
||||||
@@ -78,27 +83,31 @@ public final class VigilanteArenaController implements Listener, Runnable {
|
|||||||
World world = server.getWorld(arena.worldName());
|
World world = server.getWorld(arena.worldName());
|
||||||
if (world == null) {
|
if (world == null) {
|
||||||
closeFight();
|
closeFight();
|
||||||
warnIfOpenWithoutLocation();
|
warnIfOpenWithoutLocation(openRole);
|
||||||
wasOpen = false;
|
previousOpenRole = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
warnedMissingLocation = false;
|
warnedMissingRole = null;
|
||||||
drawBoundary(world, arena);
|
ArenaRole role = openRole.orElse(null);
|
||||||
removeOtherMobs(world, arena);
|
if (role != previousOpenRole) {
|
||||||
boolean open = VigilanteArenaPolicy.isOpen(stateManager.game());
|
closeFight();
|
||||||
if (open && !wasOpen) {
|
if (role != null) {
|
||||||
announceVacancy(arena);
|
announceVacancy(arena, role);
|
||||||
}
|
}
|
||||||
wasOpen = open;
|
}
|
||||||
if (!open) {
|
previousOpenRole = role;
|
||||||
|
drawBoundary(world, arena, role);
|
||||||
|
removeOtherMobs(world, arena);
|
||||||
|
if (role == null) {
|
||||||
closeFight();
|
closeFight();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Zombie boss = boss(world);
|
Zombie boss = boss(world);
|
||||||
if (boss == null) {
|
if (boss == null || bossRole != role) {
|
||||||
boss = spawnBoss(world, arena);
|
closeFight();
|
||||||
|
boss = spawnBoss(world, arena, role);
|
||||||
}
|
}
|
||||||
boss.setFireTicks(0);
|
boss.setFireTicks(0);
|
||||||
if (!contains(arena, boss.getLocation())) {
|
if (!contains(arena, boss.getLocation())) {
|
||||||
@@ -107,8 +116,8 @@ public final class VigilanteArenaController implements Listener, Runnable {
|
|||||||
Player challenger = challengerId == null ? null : server.getPlayer(challengerId);
|
Player challenger = challengerId == null ? null : server.getPlayer(challengerId);
|
||||||
if (challenger == null || challenger.isDead()
|
if (challenger == null || challenger.isDead()
|
||||||
|| !contains(arena, challenger.getLocation())
|
|| !contains(arena, challenger.getLocation())
|
||||||
|| !VigilanteArenaPolicy.isEligible(
|
|| !RoleArenaPolicy.isEligible(
|
||||||
stateManager.game(), playerState(challenger)
|
stateManager.game(), role, playerState(challenger)
|
||||||
)) {
|
)) {
|
||||||
if (challengerId != null) {
|
if (challengerId != null) {
|
||||||
resetFight();
|
resetFight();
|
||||||
@@ -163,13 +172,16 @@ public final class VigilanteArenaController implements Listener, Runnable {
|
|||||||
|
|
||||||
@EventHandler(priority = EventPriority.MONITOR)
|
@EventHandler(priority = EventPriority.MONITOR)
|
||||||
public void onJoin(PlayerJoinEvent event) {
|
public void onJoin(PlayerJoinEvent event) {
|
||||||
if (!VigilanteArenaPolicy.isOpen(stateManager.game())) {
|
Optional<ArenaRole> role = RoleArenaPolicy.openRole(stateManager.game());
|
||||||
|
if (role.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
PlayerState state = playerState(event.getPlayer());
|
PlayerState state = playerState(event.getPlayer());
|
||||||
locations.location().filter(location -> VigilanteArenaPolicy.isEligible(
|
locations.location().filter(location -> RoleArenaPolicy.isEligible(
|
||||||
stateManager.game(), state
|
stateManager.game(), role.orElseThrow(), state
|
||||||
)).ifPresent(location -> event.getPlayer().sendMessage(vacancyMessage(location)));
|
)).ifPresent(location -> event.getPlayer().sendMessage(
|
||||||
|
vacancyMessage(location, role.orElseThrow())
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||||
@@ -218,17 +230,20 @@ public final class VigilanteArenaController implements Listener, Runnable {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
UUID defeatedBoss = bossId;
|
UUID defeatedBoss = bossId;
|
||||||
|
ArenaRole defeatedRole = bossRole;
|
||||||
bossId = null;
|
bossId = null;
|
||||||
|
bossRole = null;
|
||||||
Player killer = event.getEntity().getKiller();
|
Player killer = event.getEntity().getKiller();
|
||||||
if (killer != null && challengerId != null
|
if (killer != null && challengerId != null && defeatedRole != null
|
||||||
&& killer.getUniqueId().equals(challengerId)
|
&& killer.getUniqueId().equals(challengerId)
|
||||||
&& VigilanteArenaPolicy.isEligible(
|
&& RoleArenaPolicy.isEligible(
|
||||||
stateManager.game(), playerState(killer)
|
stateManager.game(), defeatedRole, playerState(killer)
|
||||||
)) {
|
)) {
|
||||||
assignVigilante(challengerId);
|
assignRole(challengerId, defeatedRole);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
bossId = defeatedBoss;
|
bossId = defeatedBoss;
|
||||||
|
bossRole = defeatedRole;
|
||||||
resetFight();
|
resetFight();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,8 +268,9 @@ public final class VigilanteArenaController implements Listener, Runnable {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
PlayerState state = playerState(player);
|
PlayerState state = playerState(player);
|
||||||
if (challengerId == null && bossId != null
|
ArenaRole role = bossRole;
|
||||||
&& VigilanteArenaPolicy.isEligible(stateManager.game(), state)) {
|
if (challengerId == null && bossId != null && role != null
|
||||||
|
&& RoleArenaPolicy.isEligible(stateManager.game(), role, state)) {
|
||||||
challengerId = player.getUniqueId();
|
challengerId = player.getUniqueId();
|
||||||
World world = server.getWorld(arena.worldName());
|
World world = server.getWorld(arena.worldName());
|
||||||
if (world != null) {
|
if (world != null) {
|
||||||
@@ -266,7 +282,8 @@ public final class VigilanteArenaController implements Listener, Runnable {
|
|||||||
boss.setTarget(player);
|
boss.setTarget(player);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
player.sendMessage(ChatColor.GOLD + "Defeat the arena boss to become Vigilante!");
|
player.sendMessage(ChatColor.GOLD + "Defeat the arena boss to become "
|
||||||
|
+ readable(role) + "!");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
bounce(player, from, to, arena);
|
bounce(player, from, to, arena);
|
||||||
@@ -313,9 +330,10 @@ public final class VigilanteArenaController implements Listener, Runnable {
|
|||||||
}
|
}
|
||||||
bossId = null;
|
bossId = null;
|
||||||
}
|
}
|
||||||
|
bossRole = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Zombie spawnBoss(World world, ArenaLocation arena) {
|
private Zombie spawnBoss(World world, ArenaLocation arena, ArenaRole role) {
|
||||||
for (Entity entity : world.getEntities()) {
|
for (Entity entity : world.getEntities()) {
|
||||||
if (entity.getScoreboardTags().contains(BOSS_TAG)) {
|
if (entity.getScoreboardTags().contains(BOSS_TAG)) {
|
||||||
entity.remove();
|
entity.remove();
|
||||||
@@ -325,7 +343,9 @@ public final class VigilanteArenaController implements Listener, Runnable {
|
|||||||
try {
|
try {
|
||||||
Zombie boss = world.spawn(toBukkit(world, arena), Zombie.class, zombie -> {
|
Zombie boss = world.spawn(toBukkit(world, arena), Zombie.class, zombie -> {
|
||||||
zombie.setAdult();
|
zombie.setAdult();
|
||||||
zombie.setCustomName(ChatColor.DARK_RED + "Vigilante's Trial");
|
zombie.setCustomName(role == ArenaRole.TYRANT
|
||||||
|
? ChatColor.RED + "Tyrant's Trial"
|
||||||
|
: ChatColor.BLUE + "Vigilante's Trial");
|
||||||
zombie.setCustomNameVisible(true);
|
zombie.setCustomNameVisible(true);
|
||||||
zombie.setRemoveWhenFarAway(false);
|
zombie.setRemoveWhenFarAway(false);
|
||||||
zombie.setCanPickupItems(false);
|
zombie.setCanPickupItems(false);
|
||||||
@@ -336,6 +356,7 @@ public final class VigilanteArenaController implements Listener, Runnable {
|
|||||||
zombie.setHealth(settings.vigilanteBossHealth());
|
zombie.setHealth(settings.vigilanteBossHealth());
|
||||||
});
|
});
|
||||||
bossId = boss.getUniqueId();
|
bossId = boss.getUniqueId();
|
||||||
|
bossRole = role;
|
||||||
return boss;
|
return boss;
|
||||||
} finally {
|
} finally {
|
||||||
spawningBoss = false;
|
spawningBoss = false;
|
||||||
@@ -351,10 +372,12 @@ public final class VigilanteArenaController implements Listener, Runnable {
|
|||||||
&& !zombie.isDead() ? zombie : null;
|
&& !zombie.isDead() ? zombie : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void assignVigilante(UUID playerId) {
|
private void assignRole(UUID playerId, ArenaRole role) {
|
||||||
LifecycleState assigned;
|
LifecycleState assigned;
|
||||||
try {
|
try {
|
||||||
assigned = succession.claim(stateManager.snapshot(), playerId);
|
assigned = role == ArenaRole.TYRANT
|
||||||
|
? tyrantSuccession.claim(stateManager.snapshot(), playerId)
|
||||||
|
: vigilanteSuccession.claim(stateManager.snapshot(), playerId);
|
||||||
} catch (IllegalStateException exception) {
|
} catch (IllegalStateException exception) {
|
||||||
resetFight();
|
resetFight();
|
||||||
return;
|
return;
|
||||||
@@ -365,39 +388,46 @@ public final class VigilanteArenaController implements Listener, Runnable {
|
|||||||
barrier.clear();
|
barrier.clear();
|
||||||
String name = Optional.ofNullable(server.getPlayer(playerId))
|
String name = Optional.ofNullable(server.getPlayer(playerId))
|
||||||
.map(Player::getName).orElse(playerId.toString());
|
.map(Player::getName).orElse(playerId.toString());
|
||||||
String message = ChatColor.YELLOW + name + " defeated the arena boss and is the Vigilante!";
|
String message = ChatColor.YELLOW + name + " defeated the arena boss and is the "
|
||||||
|
+ readable(role) + "!";
|
||||||
server.getOnlinePlayers().forEach(player -> player.sendMessage(message));
|
server.getOnlinePlayers().forEach(player -> player.sendMessage(message));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void announceVacancy(ArenaLocation arena) {
|
private void announceVacancy(ArenaLocation arena, ArenaRole role) {
|
||||||
String message = vacancyMessage(arena);
|
String message = vacancyMessage(arena, role);
|
||||||
for (Player player : server.getOnlinePlayers()) {
|
for (Player player : server.getOnlinePlayers()) {
|
||||||
PlayerState state = playerState(player);
|
PlayerState state = playerState(player);
|
||||||
if (VigilanteArenaPolicy.isEligible(stateManager.game(), state)) {
|
if (RoleArenaPolicy.isEligible(stateManager.game(), role, state)) {
|
||||||
player.sendMessage(message);
|
player.sendMessage(message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void warnIfOpenWithoutLocation() {
|
private void warnIfOpenWithoutLocation(Optional<ArenaRole> openRole) {
|
||||||
if (!VigilanteArenaPolicy.isOpen(stateManager.game()) || warnedMissingLocation) {
|
if (openRole.isEmpty()) {
|
||||||
|
warnedMissingRole = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
warnedMissingLocation = true;
|
ArenaRole role = openRole.orElseThrow();
|
||||||
|
if (role == warnedMissingRole) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
warnedMissingRole = role;
|
||||||
server.getConsoleSender().sendMessage(ChatColor.RED
|
server.getConsoleSender().sendMessage(ChatColor.RED
|
||||||
+ "The Vigilante role is vacant, but no valid arena location is configured. "
|
+ "The " + readable(role)
|
||||||
|
+ " role is vacant, but no valid arena location is configured. "
|
||||||
+ "Use /tyrantadmin arena set.");
|
+ "Use /tyrantadmin arena set.");
|
||||||
for (Player player : server.getOnlinePlayers()) {
|
for (Player player : server.getOnlinePlayers()) {
|
||||||
if (player.hasPermission("spigottyrant.admin")) {
|
if (player.hasPermission("spigottyrant.admin")) {
|
||||||
player.sendMessage(ChatColor.RED
|
player.sendMessage(ChatColor.RED
|
||||||
+ "The Vigilante arena location is not configured.");
|
+ "The shared role arena location is not configured.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private String vacancyMessage(ArenaLocation arena) {
|
private String vacancyMessage(ArenaLocation arena, ArenaRole role) {
|
||||||
return ChatColor.YELLOW + "The Vigilante position is open! Challenge the boss in "
|
return ChatColor.YELLOW + "The " + readable(role)
|
||||||
+ arena.display() + ".";
|
+ " position is open! Challenge the boss in " + arena.display() + ".";
|
||||||
}
|
}
|
||||||
|
|
||||||
private void removeOtherMobs(World world, ArenaLocation arena) {
|
private void removeOtherMobs(World world, ArenaLocation arena) {
|
||||||
@@ -411,11 +441,14 @@ public final class VigilanteArenaController implements Listener, Runnable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void drawBoundary(World world, ArenaLocation arena) {
|
private void drawBoundary(World world, ArenaLocation arena, ArenaRole role) {
|
||||||
if (barrier.active()) {
|
if (barrier.active()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
double radius = settings.vigilanteArenaRadiusBlocks();
|
double radius = settings.vigilanteArenaRadiusBlocks();
|
||||||
|
Color color = boundaryColor(role);
|
||||||
|
Particle.DustOptions dust = color == null
|
||||||
|
? null : new Particle.DustOptions(color, 1.25F);
|
||||||
for (int index = 0; index < 48; index++) {
|
for (int index = 0; index < 48; index++) {
|
||||||
double angle = 2.0 * Math.PI * index / 48.0;
|
double angle = 2.0 * Math.PI * index / 48.0;
|
||||||
Location point = new Location(
|
Location point = new Location(
|
||||||
@@ -424,7 +457,11 @@ public final class VigilanteArenaController implements Listener, Runnable {
|
|||||||
arena.y() + 1.0,
|
arena.y() + 1.0,
|
||||||
arena.z() + Math.sin(angle) * radius
|
arena.z() + Math.sin(angle) * radius
|
||||||
);
|
);
|
||||||
|
if (dust == null) {
|
||||||
world.spawnParticle(Particle.END_ROD, point, 1, 0.0, 0.0, 0.0, 0.0);
|
world.spawnParticle(Particle.END_ROD, point, 1, 0.0, 0.0, 0.0, 0.0);
|
||||||
|
} else {
|
||||||
|
world.spawnParticle(Particle.DUST, point, 1, 0.0, 0.0, 0.0, 0.0, dust);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -498,6 +535,17 @@ public final class VigilanteArenaController implements Listener, Runnable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Color boundaryColor(ArenaRole role) {
|
||||||
|
if (role == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return role == ArenaRole.TYRANT ? Color.RED : Color.BLUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String readable(ArenaRole role) {
|
||||||
|
return role == ArenaRole.TYRANT ? "Tyrant" : "Vigilante";
|
||||||
|
}
|
||||||
|
|
||||||
private static boolean movedHorizontally(Location from, Location to) {
|
private static boolean movedHorizontally(Location from, Location to) {
|
||||||
return from.getWorld() != to.getWorld()
|
return from.getWorld() != to.getWorld()
|
||||||
|| from.getX() != to.getX() || from.getZ() != to.getZ();
|
|| from.getX() != to.getX() || from.getZ() != to.getZ();
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package games.dmg.spigottyrant;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public final class RoleArenaPolicy {
|
||||||
|
private RoleArenaPolicy() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Optional<ArenaRole> openRole(GameState game) {
|
||||||
|
if (game.lifecycle() != GameLifecycle.RUNNING) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
if (game.tyrantId().isEmpty()) {
|
||||||
|
return game.pendingTyrant().isEmpty()
|
||||||
|
? Optional.of(ArenaRole.TYRANT) : Optional.empty();
|
||||||
|
}
|
||||||
|
return game.vigilanteId().isEmpty()
|
||||||
|
? Optional.of(ArenaRole.VIGILANTE) : Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isEligible(
|
||||||
|
GameState game,
|
||||||
|
ArenaRole role,
|
||||||
|
PlayerState player
|
||||||
|
) {
|
||||||
|
return player != null
|
||||||
|
&& openRole(game).filter(role::equals).isPresent()
|
||||||
|
&& player.optedOutUntil().isEmpty()
|
||||||
|
&& game.tyrantId().filter(player.playerId()::equals).isEmpty();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
package games.dmg.spigottyrant;
|
|
||||||
|
|
||||||
import java.time.Duration;
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.UUID;
|
|
||||||
import java.util.random.RandomGenerator;
|
|
||||||
|
|
||||||
public final class RoleCandidateSelector {
|
|
||||||
public Optional<UUID> select(
|
|
||||||
Map<UUID, PlayerState> players,
|
|
||||||
Set<UUID> excluded,
|
|
||||||
Optional<UUID> previousHolder,
|
|
||||||
Instant now,
|
|
||||||
Duration activityWindow,
|
|
||||||
RandomGenerator random
|
|
||||||
) {
|
|
||||||
Instant cutoff = now.minus(activityWindow);
|
|
||||||
List<UUID> eligible = new ArrayList<>();
|
|
||||||
for (PlayerState player : players.values()) {
|
|
||||||
if (excluded.contains(player.playerId())) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (player.lastLogin().isEmpty()
|
|
||||||
|| player.lastLogin().orElseThrow().isBefore(cutoff)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (player.optedOutUntil().isPresent()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
eligible.add(player.playerId());
|
|
||||||
}
|
|
||||||
if (eligible.size() > 1) {
|
|
||||||
previousHolder.ifPresent(eligible::remove);
|
|
||||||
}
|
|
||||||
if (eligible.isEmpty()) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
eligible.sort(Comparator.comparing(UUID::toString));
|
|
||||||
return Optional.of(eligible.get(random.nextInt(eligible.size())));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,65 +5,39 @@ import java.time.Instant;
|
|||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.random.RandomGenerator;
|
|
||||||
|
|
||||||
public final class RoleMaintenanceService {
|
public final class RoleMaintenanceService {
|
||||||
private final RoleCandidateSelector candidates;
|
|
||||||
private final TyrantSuccessionService succession;
|
private final TyrantSuccessionService succession;
|
||||||
private final Duration inactivity;
|
private final Duration inactivity;
|
||||||
private final Duration activityWindow;
|
|
||||||
private final Duration pendingTimeout;
|
|
||||||
private final RandomGenerator random;
|
|
||||||
|
|
||||||
public RoleMaintenanceService(
|
public RoleMaintenanceService(
|
||||||
RoleCandidateSelector candidates,
|
|
||||||
TyrantSuccessionService succession,
|
TyrantSuccessionService succession,
|
||||||
Duration inactivity,
|
Duration inactivity
|
||||||
Duration activityWindow,
|
|
||||||
Duration pendingTimeout,
|
|
||||||
RandomGenerator random
|
|
||||||
) {
|
) {
|
||||||
this.candidates = candidates;
|
|
||||||
this.succession = succession;
|
this.succession = succession;
|
||||||
this.inactivity = inactivity;
|
this.inactivity = inactivity;
|
||||||
this.activityWindow = activityWindow;
|
|
||||||
this.pendingTimeout = pendingTimeout;
|
|
||||||
this.random = random;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public LifecycleState maintain(
|
public LifecycleState maintain(PersistentState state, Instant now) {
|
||||||
PersistentState state,
|
|
||||||
Set<UUID> onlinePlayerIds,
|
|
||||||
Instant now
|
|
||||||
) {
|
|
||||||
if (state.game().lifecycle() != GameLifecycle.RUNNING) {
|
if (state.game().lifecycle() != GameLifecycle.RUNNING) {
|
||||||
return new LifecycleState(state.game(), state.players());
|
return new LifecycleState(state.game(), state.players());
|
||||||
}
|
}
|
||||||
LifecycleState current = confirmPending(state, onlinePlayerIds, now);
|
LifecycleState current = clearLegacyPending(state);
|
||||||
if (isInactive(current.game().tyrantId(), current.players(), now)) {
|
if (isInactive(current.game().tyrantId(), current.players(), now)) {
|
||||||
return succession.succeed(
|
return succession.succeed(current.asPersistentState(), Optional.empty());
|
||||||
current.asPersistentState(), Optional.empty(), onlinePlayerIds, now
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (isInactive(current.game().vigilanteId(), current.players(), now)) {
|
if (isInactive(current.game().vigilanteId(), current.players(), now)) {
|
||||||
current = rerollVigilante(
|
current = rerollVigilante(
|
||||||
current.asPersistentState(), current.game().vigilanteId(), onlinePlayerIds, now
|
current.asPersistentState(), current.game().vigilanteId()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
current = rerollExpiredTyrant(current, onlinePlayerIds, now);
|
|
||||||
if (current.game().tyrantId().isEmpty() && current.game().pendingTyrant().isEmpty()) {
|
|
||||||
current = selectTyrant(current, Optional.empty(), onlinePlayerIds, now);
|
|
||||||
}
|
|
||||||
return current;
|
return current;
|
||||||
}
|
}
|
||||||
|
|
||||||
public LifecycleState rerollVigilante(
|
public LifecycleState rerollVigilante(
|
||||||
PersistentState state,
|
PersistentState state,
|
||||||
Optional<UUID> previousVigilante,
|
Optional<UUID> previousVigilante
|
||||||
Set<UUID> onlinePlayerIds,
|
|
||||||
Instant now
|
|
||||||
) {
|
) {
|
||||||
Map<UUID, PlayerState> players = clearFollowers(state.players(), previousVigilante);
|
Map<UUID, PlayerState> players = clearFollowers(state.players(), previousVigilante);
|
||||||
GameState game = copyRoles(
|
GameState game = copyRoles(
|
||||||
@@ -73,70 +47,12 @@ public final class RoleMaintenanceService {
|
|||||||
return new LifecycleState(game, players);
|
return new LifecycleState(game, players);
|
||||||
}
|
}
|
||||||
|
|
||||||
private LifecycleState confirmPending(
|
private static LifecycleState clearLegacyPending(PersistentState state) {
|
||||||
PersistentState state,
|
|
||||||
Set<UUID> onlinePlayerIds,
|
|
||||||
Instant now
|
|
||||||
) {
|
|
||||||
GameState game = state.game();
|
GameState game = state.game();
|
||||||
Optional<UUID> tyrant = game.tyrantId();
|
GameState cleared = copyRoles(
|
||||||
Optional<UUID> vigilante = game.vigilanteId();
|
game, game.tyrantId(), game.vigilanteId(), Optional.empty(), Optional.empty()
|
||||||
Optional<PendingSelection> pendingTyrant = game.pendingTyrant();
|
|
||||||
Optional<PendingSelection> pendingVigilante = game.pendingVigilante();
|
|
||||||
if (pendingTyrant.isPresent()) {
|
|
||||||
PendingSelection pending = pendingTyrant.orElseThrow();
|
|
||||||
if (!now.isAfter(pending.expiresAt())
|
|
||||||
&& onlinePlayerIds.contains(pending.candidateId())
|
|
||||||
&& eligibleAtConfirmation(state.players().get(pending.candidateId()), now)
|
|
||||||
&& vigilante.filter(pending.candidateId()::equals).isEmpty()) {
|
|
||||||
tyrant = Optional.of(pending.candidateId());
|
|
||||||
pendingTyrant = Optional.empty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Legacy pending Vigilante selections become arena vacancies.
|
|
||||||
pendingVigilante = Optional.empty();
|
|
||||||
return new LifecycleState(
|
|
||||||
copyRoles(game, tyrant, vigilante, pendingTyrant, pendingVigilante),
|
|
||||||
state.players()
|
|
||||||
);
|
);
|
||||||
}
|
return new LifecycleState(cleared, state.players());
|
||||||
|
|
||||||
private LifecycleState rerollExpiredTyrant(
|
|
||||||
LifecycleState state,
|
|
||||||
Set<UUID> onlinePlayerIds,
|
|
||||||
Instant now
|
|
||||||
) {
|
|
||||||
Optional<PendingSelection> pending = state.game().pendingTyrant();
|
|
||||||
if (pending.isEmpty() || !now.isAfter(pending.orElseThrow().expiresAt())) {
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
return selectTyrant(
|
|
||||||
state,
|
|
||||||
Optional.of(pending.orElseThrow().candidateId()),
|
|
||||||
onlinePlayerIds,
|
|
||||||
now
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private LifecycleState selectTyrant(
|
|
||||||
LifecycleState state,
|
|
||||||
Optional<UUID> previous,
|
|
||||||
Set<UUID> onlinePlayerIds,
|
|
||||||
Instant now
|
|
||||||
) {
|
|
||||||
Set<UUID> excluded = state.game().vigilanteId().map(Set::of).orElseGet(Set::of);
|
|
||||||
Optional<UUID> selected = candidates.select(
|
|
||||||
state.players(), excluded, previous, now, activityWindow, random
|
|
||||||
);
|
|
||||||
Optional<UUID> active = selected.filter(onlinePlayerIds::contains);
|
|
||||||
Optional<PendingSelection> pending = selected
|
|
||||||
.filter(playerId -> active.isEmpty())
|
|
||||||
.map(playerId -> new PendingSelection(playerId, now.plus(pendingTimeout)));
|
|
||||||
GameState game = copyRoles(
|
|
||||||
state.game(), active, state.game().vigilanteId(), pending,
|
|
||||||
state.game().pendingVigilante()
|
|
||||||
);
|
|
||||||
return new LifecycleState(game, state.players());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isInactive(
|
private boolean isInactive(
|
||||||
@@ -153,10 +69,6 @@ public final class RoleMaintenanceService {
|
|||||||
&& !player.lastLogin().orElseThrow().plus(inactivity).isAfter(now);
|
&& !player.lastLogin().orElseThrow().plus(inactivity).isAfter(now);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean eligibleAtConfirmation(PlayerState player, Instant now) {
|
|
||||||
return player != null && player.optedOutUntil().isEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Map<UUID, PlayerState> clearFollowers(
|
private static Map<UUID, PlayerState> clearFollowers(
|
||||||
Map<UUID, PlayerState> players,
|
Map<UUID, PlayerState> players,
|
||||||
Optional<UUID> previousVigilante
|
Optional<UUID> previousVigilante
|
||||||
|
|||||||
@@ -1,40 +1,29 @@
|
|||||||
package games.dmg.spigottyrant;
|
package games.dmg.spigottyrant;
|
||||||
|
|
||||||
import java.time.Clock;
|
import java.time.Clock;
|
||||||
import java.util.Set;
|
|
||||||
import java.util.UUID;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import org.bukkit.Server;
|
|
||||||
import org.bukkit.entity.Player;
|
|
||||||
|
|
||||||
public final class RoleMaintenanceTask implements Runnable {
|
public final class RoleMaintenanceTask implements Runnable {
|
||||||
private final TyrantStateManager stateManager;
|
private final TyrantStateManager stateManager;
|
||||||
private final RoleMaintenanceService maintenance;
|
private final RoleMaintenanceService maintenance;
|
||||||
private final TyrantPresentation presentation;
|
private final TyrantPresentation presentation;
|
||||||
private final Server server;
|
|
||||||
private final Clock clock;
|
private final Clock clock;
|
||||||
|
|
||||||
public RoleMaintenanceTask(
|
public RoleMaintenanceTask(
|
||||||
TyrantStateManager stateManager,
|
TyrantStateManager stateManager,
|
||||||
RoleMaintenanceService maintenance,
|
RoleMaintenanceService maintenance,
|
||||||
TyrantPresentation presentation,
|
TyrantPresentation presentation,
|
||||||
Server server,
|
|
||||||
Clock clock
|
Clock clock
|
||||||
) {
|
) {
|
||||||
this.stateManager = stateManager;
|
this.stateManager = stateManager;
|
||||||
this.maintenance = maintenance;
|
this.maintenance = maintenance;
|
||||||
this.presentation = presentation;
|
this.presentation = presentation;
|
||||||
this.server = server;
|
|
||||||
this.clock = clock;
|
this.clock = clock;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
PersistentState before = stateManager.snapshot();
|
PersistentState before = stateManager.snapshot();
|
||||||
Set<UUID> online = server.getOnlinePlayers().stream()
|
LifecycleState after = maintenance.maintain(before, clock.instant());
|
||||||
.map(Player::getUniqueId)
|
|
||||||
.collect(Collectors.toUnmodifiableSet());
|
|
||||||
LifecycleState after = maintenance.maintain(before, online, clock.instant());
|
|
||||||
if (!after.asPersistentState().equals(before)) {
|
if (!after.asPersistentState().equals(before)) {
|
||||||
stateManager.replaceState(after);
|
stateManager.replaceState(after);
|
||||||
stateManager.saveIfDirty();
|
stateManager.saveIfDirty();
|
||||||
|
|||||||
@@ -31,15 +31,13 @@ public final class RoleRelinquishmentService {
|
|||||||
}
|
}
|
||||||
if (state.game().tyrantId().filter(playerId::equals).isPresent()) {
|
if (state.game().tyrantId().filter(playerId::equals).isPresent()) {
|
||||||
return new RelinquishmentResult(
|
return new RelinquishmentResult(
|
||||||
succession.succeed(state, Optional.empty(), onlinePlayerIds, now),
|
succession.succeed(state, Optional.empty()),
|
||||||
RelinquishmentStatus.RELINQUISHED
|
RelinquishmentStatus.RELINQUISHED
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (state.game().vigilanteId().filter(playerId::equals).isPresent()) {
|
if (state.game().vigilanteId().filter(playerId::equals).isPresent()) {
|
||||||
return new RelinquishmentResult(
|
return new RelinquishmentResult(
|
||||||
maintenance.rerollVigilante(
|
maintenance.rerollVigilante(state, Optional.of(playerId)),
|
||||||
state, Optional.of(playerId), onlinePlayerIds, now
|
|
||||||
),
|
|
||||||
RelinquishmentStatus.RELINQUISHED
|
RelinquishmentStatus.RELINQUISHED
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ public final class SpigotTyrantPlugin extends JavaPlugin {
|
|||||||
private FixerEffectController fixerEffects;
|
private FixerEffectController fixerEffects;
|
||||||
private TyrantEffectController tyrantEffects;
|
private TyrantEffectController tyrantEffects;
|
||||||
private OptOutGlowController optOutGlow;
|
private OptOutGlowController optOutGlow;
|
||||||
private VigilanteArenaController vigilanteArena;
|
private RoleArenaController roleArena;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onEnable() {
|
public void onEnable() {
|
||||||
@@ -38,26 +38,14 @@ public final class SpigotTyrantPlugin extends JavaPlugin {
|
|||||||
try {
|
try {
|
||||||
arenaLocations.location();
|
arenaLocations.location();
|
||||||
} catch (IllegalArgumentException exception) {
|
} catch (IllegalArgumentException exception) {
|
||||||
getLogger().log(Level.SEVERE, "Invalid Vigilante arena location", exception);
|
getLogger().log(Level.SEVERE, "Invalid shared role arena location", exception);
|
||||||
getServer().getPluginManager().disablePlugin(this);
|
getServer().getPluginManager().disablePlugin(this);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Clock clock = Clock.systemUTC();
|
Clock clock = Clock.systemUTC();
|
||||||
RoleCandidateSelector candidateSelector = new RoleCandidateSelector();
|
TyrantSuccessionService succession = new TyrantSuccessionService();
|
||||||
java.util.random.RandomGenerator random = java.util.random.RandomGenerator.getDefault();
|
|
||||||
TyrantSuccessionService succession = new TyrantSuccessionService(
|
|
||||||
candidateSelector,
|
|
||||||
settings.candidateActivityWindow(),
|
|
||||||
settings.pendingSelectionTimeout(),
|
|
||||||
random
|
|
||||||
);
|
|
||||||
RoleMaintenanceService maintenance = new RoleMaintenanceService(
|
RoleMaintenanceService maintenance = new RoleMaintenanceService(
|
||||||
candidateSelector,
|
succession, settings.roleInactivity()
|
||||||
succession,
|
|
||||||
settings.roleInactivity(),
|
|
||||||
settings.candidateActivityWindow(),
|
|
||||||
settings.pendingSelectionTimeout(),
|
|
||||||
random
|
|
||||||
);
|
);
|
||||||
BukkitOnlinePlayerDirectory onlinePlayers = new BukkitOnlinePlayerDirectory();
|
BukkitOnlinePlayerDirectory onlinePlayers = new BukkitOnlinePlayerDirectory();
|
||||||
FollowerService followers = new FollowerService();
|
FollowerService followers = new FollowerService();
|
||||||
@@ -154,10 +142,10 @@ public final class SpigotTyrantPlugin extends JavaPlugin {
|
|||||||
new PlayerJoinListener(stateManager, clock, tyrantPresentation),
|
new PlayerJoinListener(stateManager, clock, tyrantPresentation),
|
||||||
this
|
this
|
||||||
);
|
);
|
||||||
vigilanteArena = new VigilanteArenaController(
|
roleArena = new RoleArenaController(
|
||||||
stateManager, getServer(), arenaLocations, settings
|
stateManager, getServer(), arenaLocations, settings
|
||||||
);
|
);
|
||||||
getServer().getPluginManager().registerEvents(vigilanteArena, this);
|
getServer().getPluginManager().registerEvents(roleArena, this);
|
||||||
getServer().getPluginManager().registerEvents(controlPanelRenderer, this);
|
getServer().getPluginManager().registerEvents(controlPanelRenderer, this);
|
||||||
getServer().getPluginManager().registerEvents(vigilanteControlPanelRenderer, this);
|
getServer().getPluginManager().registerEvents(vigilanteControlPanelRenderer, this);
|
||||||
getServer().getPluginManager().registerEvents(
|
getServer().getPluginManager().registerEvents(
|
||||||
@@ -180,14 +168,14 @@ public final class SpigotTyrantPlugin extends JavaPlugin {
|
|||||||
);
|
);
|
||||||
getServer().getPluginManager().registerEvents(
|
getServer().getPluginManager().registerEvents(
|
||||||
new TyrantDeathListener(
|
new TyrantDeathListener(
|
||||||
stateManager, succession, tyrantPresentation, getServer(), clock
|
stateManager, succession, tyrantPresentation, getServer()
|
||||||
),
|
),
|
||||||
this
|
this
|
||||||
);
|
);
|
||||||
getServer().getPluginManager().registerEvents(
|
getServer().getPluginManager().registerEvents(
|
||||||
new VigilanteDeathListener(
|
new VigilanteDeathListener(
|
||||||
stateManager, new TyrantProgressionService(), maintenance,
|
stateManager, new TyrantProgressionService(), maintenance,
|
||||||
getServer(), clock
|
getServer()
|
||||||
),
|
),
|
||||||
this
|
this
|
||||||
);
|
);
|
||||||
@@ -238,12 +226,12 @@ public final class SpigotTyrantPlugin extends JavaPlugin {
|
|||||||
this
|
this
|
||||||
);
|
);
|
||||||
long maintenanceTicks = Math.max(
|
long maintenanceTicks = Math.max(
|
||||||
1L, Math.multiplyExact(settings.selectionRetryInterval().toSeconds(), 20L)
|
1L, Math.multiplyExact(settings.roleMaintenanceInterval().toSeconds(), 20L)
|
||||||
);
|
);
|
||||||
getServer().getScheduler().runTaskTimer(
|
getServer().getScheduler().runTaskTimer(
|
||||||
this,
|
this,
|
||||||
new RoleMaintenanceTask(
|
new RoleMaintenanceTask(
|
||||||
stateManager, maintenance, tyrantPresentation, getServer(), clock
|
stateManager, maintenance, tyrantPresentation, clock
|
||||||
),
|
),
|
||||||
maintenanceTicks,
|
maintenanceTicks,
|
||||||
maintenanceTicks
|
maintenanceTicks
|
||||||
@@ -253,7 +241,7 @@ public final class SpigotTyrantPlugin extends JavaPlugin {
|
|||||||
getServer().getScheduler().runTaskTimer(this, fixerEffects, 10L, 10L);
|
getServer().getScheduler().runTaskTimer(this, fixerEffects, 10L, 10L);
|
||||||
getServer().getScheduler().runTaskTimer(this, tyrantEffects, 10L, 10L);
|
getServer().getScheduler().runTaskTimer(this, tyrantEffects, 10L, 10L);
|
||||||
getServer().getScheduler().runTaskTimer(this, optOutGlow, 10L, 10L);
|
getServer().getScheduler().runTaskTimer(this, optOutGlow, 10L, 10L);
|
||||||
getServer().getScheduler().runTaskTimer(this, vigilanteArena, 1L, 10L);
|
getServer().getScheduler().runTaskTimer(this, roleArena, 1L, 10L);
|
||||||
getServer().getScheduler().runTaskTimer(
|
getServer().getScheduler().runTaskTimer(
|
||||||
this,
|
this,
|
||||||
new AbilityItemRefreshTask(
|
new AbilityItemRefreshTask(
|
||||||
@@ -283,8 +271,8 @@ public final class SpigotTyrantPlugin extends JavaPlugin {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDisable() {
|
public void onDisable() {
|
||||||
if (vigilanteArena != null) {
|
if (roleArena != null) {
|
||||||
vigilanteArena.close();
|
roleArena.close();
|
||||||
}
|
}
|
||||||
if (optOutGlow != null) {
|
if (optOutGlow != null) {
|
||||||
optOutGlow.clearAll();
|
optOutGlow.clearAll();
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ public final class TyrantAdminCommand implements CommandExecutor {
|
|||||||
|
|
||||||
private void arena(CommandSender sender, String[] arguments) {
|
private void arena(CommandSender sender, String[] arguments) {
|
||||||
if (arguments.length == 1 || arguments[1].equalsIgnoreCase("status")) {
|
if (arguments.length == 1 || arguments[1].equalsIgnoreCase("status")) {
|
||||||
sender.sendMessage("Vigilante arena: " + arenaLocations.location()
|
sender.sendMessage("Shared role arena: " + arenaLocations.location()
|
||||||
.map(ArenaLocation::display).orElse("not configured"));
|
.map(ArenaLocation::display).orElse("not configured"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -162,7 +162,7 @@ public final class TyrantAdminCommand implements CommandExecutor {
|
|||||||
location.getYaw(), location.getPitch()
|
location.getYaw(), location.getPitch()
|
||||||
);
|
);
|
||||||
arenaLocations.set(arena);
|
arenaLocations.set(arena);
|
||||||
sender.sendMessage(ChatColor.GREEN + "Vigilante arena set to " + arena.display() + ".");
|
sender.sendMessage(ChatColor.GREEN + "Shared role arena set to " + arena.display() + ".");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showStatus(CommandSender sender) {
|
private void showStatus(CommandSender sender) {
|
||||||
@@ -176,7 +176,7 @@ public final class TyrantAdminCommand implements CommandExecutor {
|
|||||||
+ ", purchases: " + game.purchases());
|
+ ", purchases: " + game.purchases());
|
||||||
sender.sendMessage("Pending Tyrant: " + game.pendingTyrant());
|
sender.sendMessage("Pending Tyrant: " + game.pendingTyrant());
|
||||||
sender.sendMessage("Pending Vigilante: " + game.pendingVigilante());
|
sender.sendMessage("Pending Vigilante: " + game.pendingVigilante());
|
||||||
sender.sendMessage("Vigilante arena: " + arenaLocations.location()
|
sender.sendMessage("Shared role arena: " + arenaLocations.location()
|
||||||
.map(ArenaLocation::display).orElse("not configured"));
|
.map(ArenaLocation::display).orElse("not configured"));
|
||||||
for (PlayerState player : stateManager.players().values()) {
|
for (PlayerState player : stateManager.players().values()) {
|
||||||
boolean optedOut = player.optedOutUntil().isPresent();
|
boolean optedOut = player.optedOutUntil().isPresent();
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package games.dmg.spigottyrant;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public final class TyrantArenaSuccessionService {
|
||||||
|
public LifecycleState claim(PersistentState state, UUID winnerId) {
|
||||||
|
PlayerState winner = state.players().get(winnerId);
|
||||||
|
if (!RoleArenaPolicy.isEligible(state.game(), ArenaRole.TYRANT, winner)) {
|
||||||
|
throw new IllegalStateException("player is not eligible to claim the Tyrant role");
|
||||||
|
}
|
||||||
|
GameState game = state.game();
|
||||||
|
GameState assigned = new GameState(
|
||||||
|
game.lifecycle(), Optional.of(winnerId), Optional.empty(), Optional.empty(),
|
||||||
|
Optional.empty(), game.pausedAt(), game.accumulatedPausedTime(),
|
||||||
|
0, 1, Set.of()
|
||||||
|
);
|
||||||
|
return new LifecycleState(assigned, state.players());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,7 @@
|
|||||||
package games.dmg.spigottyrant;
|
package games.dmg.spigottyrant;
|
||||||
|
|
||||||
import java.time.Clock;
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import org.bukkit.ChatColor;
|
import org.bukkit.ChatColor;
|
||||||
import org.bukkit.Server;
|
import org.bukkit.Server;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
@@ -18,20 +15,17 @@ public final class TyrantDeathListener implements Listener {
|
|||||||
private final TyrantSuccessionService succession;
|
private final TyrantSuccessionService succession;
|
||||||
private final TyrantPresentation presentation;
|
private final TyrantPresentation presentation;
|
||||||
private final Server server;
|
private final Server server;
|
||||||
private final Clock clock;
|
|
||||||
|
|
||||||
public TyrantDeathListener(
|
public TyrantDeathListener(
|
||||||
TyrantStateManager stateManager,
|
TyrantStateManager stateManager,
|
||||||
TyrantSuccessionService succession,
|
TyrantSuccessionService succession,
|
||||||
TyrantPresentation presentation,
|
TyrantPresentation presentation,
|
||||||
Server server,
|
Server server
|
||||||
Clock clock
|
|
||||||
) {
|
) {
|
||||||
this.stateManager = stateManager;
|
this.stateManager = stateManager;
|
||||||
this.succession = succession;
|
this.succession = succession;
|
||||||
this.presentation = presentation;
|
this.presentation = presentation;
|
||||||
this.server = server;
|
this.server = server;
|
||||||
this.clock = clock;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@EventHandler(priority = EventPriority.MONITOR)
|
@EventHandler(priority = EventPriority.MONITOR)
|
||||||
@@ -44,22 +38,18 @@ public final class TyrantDeathListener implements Listener {
|
|||||||
}
|
}
|
||||||
Optional<UUID> killerId = Optional.ofNullable(event.getEntity().getKiller())
|
Optional<UUID> killerId = Optional.ofNullable(event.getEntity().getKiller())
|
||||||
.map(Player::getUniqueId);
|
.map(Player::getUniqueId);
|
||||||
Set<UUID> online = server.getOnlinePlayers().stream()
|
|
||||||
.map(Player::getUniqueId)
|
|
||||||
.collect(Collectors.toUnmodifiableSet());
|
|
||||||
LifecycleState succeeded = succession.succeed(
|
LifecycleState succeeded = succession.succeed(
|
||||||
stateManager.snapshot(), killerId, online, clock.instant()
|
stateManager.snapshot(), killerId
|
||||||
);
|
);
|
||||||
stateManager.replaceState(succeeded);
|
stateManager.replaceState(succeeded);
|
||||||
stateManager.saveIfDirty();
|
stateManager.saveIfDirty();
|
||||||
presentation.reconcile(succeeded.game().tyrantId());
|
presentation.reconcile(succeeded.game().tyrantId());
|
||||||
String successor = succeeded.game().tyrantId()
|
String message = succeeded.game().tyrantId().map(id -> {
|
||||||
.map(id -> {
|
|
||||||
Player player = server.getPlayer(id);
|
Player player = server.getPlayer(id);
|
||||||
return player == null ? id.toString() : player.getName();
|
String successor = player == null ? id.toString() : player.getName();
|
||||||
})
|
return ChatColor.RED + successor + " is the new Tyrant!";
|
||||||
.orElse("pending selection");
|
}).orElse(ChatColor.RED
|
||||||
String message = ChatColor.RED + successor + " is the new Tyrant!";
|
+ "The Tyrant role is vacant; the red arena challenge is now open.");
|
||||||
server.getOnlinePlayers().forEach(player -> player.sendMessage(message));
|
server.getOnlinePlayers().forEach(player -> player.sendMessage(message));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,65 +1,26 @@
|
|||||||
package games.dmg.spigottyrant;
|
package games.dmg.spigottyrant;
|
||||||
|
|
||||||
import java.time.Duration;
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.random.RandomGenerator;
|
|
||||||
|
|
||||||
public final class TyrantSuccessionService {
|
public final class TyrantSuccessionService {
|
||||||
private final RoleCandidateSelector candidates;
|
|
||||||
private final Duration activityWindow;
|
|
||||||
private final Duration pendingTimeout;
|
|
||||||
private final RandomGenerator random;
|
|
||||||
|
|
||||||
public TyrantSuccessionService(
|
|
||||||
RoleCandidateSelector candidates,
|
|
||||||
Duration activityWindow,
|
|
||||||
Duration pendingTimeout,
|
|
||||||
RandomGenerator random
|
|
||||||
) {
|
|
||||||
this.candidates = candidates;
|
|
||||||
this.activityWindow = activityWindow;
|
|
||||||
this.pendingTimeout = pendingTimeout;
|
|
||||||
this.random = random;
|
|
||||||
}
|
|
||||||
|
|
||||||
public LifecycleState succeed(
|
public LifecycleState succeed(
|
||||||
PersistentState state,
|
PersistentState state,
|
||||||
Optional<UUID> killerId,
|
Optional<UUID> killerId
|
||||||
Set<UUID> onlinePlayerIds,
|
|
||||||
Instant now
|
|
||||||
) {
|
) {
|
||||||
if (state.game().lifecycle() != GameLifecycle.RUNNING
|
if (state.game().lifecycle() != GameLifecycle.RUNNING
|
||||||
|| state.game().tyrantId().isEmpty()) {
|
|| state.game().tyrantId().isEmpty()) {
|
||||||
throw new IllegalStateException("a running Tyrant reign is required");
|
throw new IllegalStateException("a running Tyrant reign is required");
|
||||||
}
|
}
|
||||||
UUID formerTyrant = state.game().tyrantId().orElseThrow();
|
|
||||||
Map<UUID, PlayerState> clearedPlayers = clearAssignments(state.players());
|
Map<UUID, PlayerState> clearedPlayers = clearAssignments(state.players());
|
||||||
Optional<UUID> selectedTyrant = killerId.isPresent()
|
|
||||||
? killerId
|
|
||||||
: candidates.select(
|
|
||||||
clearedPlayers,
|
|
||||||
Set.of(),
|
|
||||||
Optional.of(formerTyrant),
|
|
||||||
now,
|
|
||||||
activityWindow,
|
|
||||||
random
|
|
||||||
);
|
|
||||||
Optional<UUID> activeTyrant = selectedTyrant
|
|
||||||
.filter(playerId -> killerId.isPresent() || onlinePlayerIds.contains(playerId));
|
|
||||||
Optional<PendingSelection> pendingTyrant = selectedTyrant
|
|
||||||
.filter(playerId -> activeTyrant.isEmpty())
|
|
||||||
.map(playerId -> new PendingSelection(playerId, now.plus(pendingTimeout)));
|
|
||||||
|
|
||||||
GameState game = new GameState(
|
GameState game = new GameState(
|
||||||
GameLifecycle.RUNNING,
|
GameLifecycle.RUNNING,
|
||||||
activeTyrant,
|
killerId,
|
||||||
|
Optional.empty(),
|
||||||
Optional.empty(),
|
Optional.empty(),
|
||||||
pendingTyrant,
|
|
||||||
Optional.empty(),
|
Optional.empty(),
|
||||||
Optional.empty(),
|
Optional.empty(),
|
||||||
state.game().accumulatedPausedTime(),
|
state.game().accumulatedPausedTime(),
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
package games.dmg.spigottyrant;
|
|
||||||
|
|
||||||
public final class VigilanteArenaPolicy {
|
|
||||||
private VigilanteArenaPolicy() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isOpen(GameState game) {
|
|
||||||
return game.lifecycle() == GameLifecycle.RUNNING
|
|
||||||
&& game.tyrantId().isPresent()
|
|
||||||
&& game.vigilanteId().isEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isEligible(GameState game, PlayerState player) {
|
|
||||||
return player != null
|
|
||||||
&& isOpen(game)
|
|
||||||
&& player.optedOutUntil().isEmpty()
|
|
||||||
&& game.tyrantId().filter(player.playerId()::equals).isEmpty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,9 @@ import java.util.UUID;
|
|||||||
public final class VigilanteArenaSuccessionService {
|
public final class VigilanteArenaSuccessionService {
|
||||||
public LifecycleState claim(PersistentState state, UUID winnerId) {
|
public LifecycleState claim(PersistentState state, UUID winnerId) {
|
||||||
PlayerState winner = state.players().get(winnerId);
|
PlayerState winner = state.players().get(winnerId);
|
||||||
if (!VigilanteArenaPolicy.isEligible(state.game(), winner)) {
|
if (!RoleArenaPolicy.isEligible(
|
||||||
|
state.game(), ArenaRole.VIGILANTE, winner
|
||||||
|
)) {
|
||||||
throw new IllegalStateException("player is not eligible to claim the Vigilante role");
|
throw new IllegalStateException("player is not eligible to claim the Vigilante role");
|
||||||
}
|
}
|
||||||
GameState game = state.game();
|
GameState game = state.game();
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
package games.dmg.spigottyrant;
|
package games.dmg.spigottyrant;
|
||||||
|
|
||||||
import java.time.Clock;
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import org.bukkit.ChatColor;
|
import org.bukkit.ChatColor;
|
||||||
import org.bukkit.Server;
|
import org.bukkit.Server;
|
||||||
@@ -17,20 +15,17 @@ public final class VigilanteDeathListener implements Listener {
|
|||||||
private final TyrantProgressionService progression;
|
private final TyrantProgressionService progression;
|
||||||
private final RoleMaintenanceService maintenance;
|
private final RoleMaintenanceService maintenance;
|
||||||
private final Server server;
|
private final Server server;
|
||||||
private final Clock clock;
|
|
||||||
|
|
||||||
public VigilanteDeathListener(
|
public VigilanteDeathListener(
|
||||||
TyrantStateManager stateManager,
|
TyrantStateManager stateManager,
|
||||||
TyrantProgressionService progression,
|
TyrantProgressionService progression,
|
||||||
RoleMaintenanceService maintenance,
|
RoleMaintenanceService maintenance,
|
||||||
Server server,
|
Server server
|
||||||
Clock clock
|
|
||||||
) {
|
) {
|
||||||
this.stateManager = stateManager;
|
this.stateManager = stateManager;
|
||||||
this.progression = progression;
|
this.progression = progression;
|
||||||
this.maintenance = maintenance;
|
this.maintenance = maintenance;
|
||||||
this.server = server;
|
this.server = server;
|
||||||
this.clock = clock;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@EventHandler(priority = EventPriority.MONITOR)
|
@EventHandler(priority = EventPriority.MONITOR)
|
||||||
@@ -47,11 +42,8 @@ public final class VigilanteDeathListener implements Listener {
|
|||||||
PersistentState withProgress = new PersistentState(
|
PersistentState withProgress = new PersistentState(
|
||||||
progressed, stateManager.players()
|
progressed, stateManager.players()
|
||||||
);
|
);
|
||||||
Set<UUID> online = server.getOnlinePlayers().stream()
|
|
||||||
.map(Player::getUniqueId)
|
|
||||||
.collect(java.util.stream.Collectors.toUnmodifiableSet());
|
|
||||||
LifecycleState rerolled = maintenance.rerollVigilante(
|
LifecycleState rerolled = maintenance.rerollVigilante(
|
||||||
withProgress, Optional.of(victimId), online, clock.instant()
|
withProgress, Optional.of(victimId)
|
||||||
);
|
);
|
||||||
stateManager.replaceState(rerolled);
|
stateManager.replaceState(rerolled);
|
||||||
stateManager.saveIfDirty();
|
stateManager.saveIfDirty();
|
||||||
|
|||||||
@@ -2,19 +2,17 @@
|
|||||||
tyrant-range-blocks: 50
|
tyrant-range-blocks: 50
|
||||||
follower-range-blocks: 50
|
follower-range-blocks: 50
|
||||||
|
|
||||||
# Vigilante arena and boss
|
# Shared Tyrant and Vigilante arena and boss
|
||||||
vigilante-arena-radius-blocks: 10
|
vigilante-arena-radius-blocks: 10
|
||||||
vigilante-boss-health: 80
|
vigilante-boss-health: 80
|
||||||
vigilante-boss-damage: 8
|
vigilante-boss-damage: 8
|
||||||
vigilante-boss-armor: 10
|
vigilante-boss-armor: 10
|
||||||
# Set with /tyrantadmin arena set
|
# Set with /tyrantadmin arena set
|
||||||
|
|
||||||
# Participation, inactivity, and selection
|
# Participation and role inactivity
|
||||||
opt-out-seconds: 604800
|
opt-out-seconds: 604800
|
||||||
role-inactivity-seconds: 172800
|
role-inactivity-seconds: 172800
|
||||||
candidate-activity-window-seconds: 86400
|
role-maintenance-seconds: 60
|
||||||
pending-selection-timeout-seconds: 86400
|
|
||||||
selection-retry-seconds: 60
|
|
||||||
freeze-timers-while-paused: true
|
freeze-timers-while-paused: true
|
||||||
|
|
||||||
# Assassin
|
# Assassin
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ final class DefaultConfigurationTest {
|
|||||||
PluginSettings settings = PluginSettings.from(values);
|
PluginSettings settings = PluginSettings.from(values);
|
||||||
assertEquals(50.0, settings.tyrantRangeBlocks());
|
assertEquals(50.0, settings.tyrantRangeBlocks());
|
||||||
assertEquals(Duration.ofDays(7), settings.optOutDuration());
|
assertEquals(Duration.ofDays(7), settings.optOutDuration());
|
||||||
|
assertEquals(Duration.ofMinutes(1), settings.roleMaintenanceInterval());
|
||||||
assertEquals("FISHING_ROD", settings.tamerItem().material());
|
assertEquals("FISHING_ROD", settings.tamerItem().material());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,8 +17,7 @@ final class PluginSettingsTest {
|
|||||||
assertEquals(50.0, settings.followerRangeBlocks());
|
assertEquals(50.0, settings.followerRangeBlocks());
|
||||||
assertEquals(Duration.ofDays(7), settings.optOutDuration());
|
assertEquals(Duration.ofDays(7), settings.optOutDuration());
|
||||||
assertEquals(Duration.ofHours(48), settings.roleInactivity());
|
assertEquals(Duration.ofHours(48), settings.roleInactivity());
|
||||||
assertEquals(Duration.ofHours(24), settings.candidateActivityWindow());
|
assertEquals(Duration.ofMinutes(1), settings.roleMaintenanceInterval());
|
||||||
assertEquals(Duration.ofHours(24), settings.pendingSelectionTimeout());
|
|
||||||
assertEquals(Duration.ofHours(1), settings.assassinCooldown());
|
assertEquals(Duration.ofHours(1), settings.assassinCooldown());
|
||||||
assertEquals(Duration.ofMinutes(10), settings.assassinInvisibilityDuration());
|
assertEquals(Duration.ofMinutes(10), settings.assassinInvisibilityDuration());
|
||||||
assertEquals(Duration.ofHours(1), settings.fixerCooldown());
|
assertEquals(Duration.ofHours(1), settings.fixerCooldown());
|
||||||
@@ -30,11 +29,17 @@ final class PluginSettingsTest {
|
|||||||
assertEquals(10.0, settings.vigilanteBossArmor());
|
assertEquals(10.0, settings.vigilanteBossArmor());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void acceptsLegacySelectionRetryAsMaintenanceInterval() {
|
||||||
|
PluginSettings settings = PluginSettings.from(Map.of("selection-retry-seconds", 30));
|
||||||
|
|
||||||
|
assertEquals(Duration.ofSeconds(30), settings.roleMaintenanceInterval());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void approvedDefaultsCoverAbilitiesItemsMobsAndMessages() {
|
void approvedDefaultsCoverAbilitiesItemsMobsAndMessages() {
|
||||||
PluginSettings settings = PluginSettings.from(Map.of());
|
PluginSettings settings = PluginSettings.from(Map.of());
|
||||||
|
|
||||||
assertEquals(Duration.ofMinutes(1), settings.selectionRetryInterval());
|
|
||||||
assertEquals(Duration.ofSeconds(60), settings.assassinDoubleJumpCooldown());
|
assertEquals(Duration.ofSeconds(60), settings.assassinDoubleJumpCooldown());
|
||||||
assertEquals(Duration.ofSeconds(15), settings.assassinSpeedDuration());
|
assertEquals(Duration.ofSeconds(15), settings.assassinSpeedDuration());
|
||||||
assertEquals(Duration.ofSeconds(20), settings.assassinWeaknessDuration());
|
assertEquals(Duration.ofSeconds(20), settings.assassinWeaknessDuration());
|
||||||
@@ -71,6 +76,10 @@ final class PluginSettingsTest {
|
|||||||
IllegalArgumentException.class,
|
IllegalArgumentException.class,
|
||||||
() -> PluginSettings.from(Map.of("role-inactivity-seconds", -1))
|
() -> PluginSettings.from(Map.of("role-inactivity-seconds", -1))
|
||||||
);
|
);
|
||||||
|
assertThrows(
|
||||||
|
IllegalArgumentException.class,
|
||||||
|
() -> PluginSettings.from(Map.of("role-maintenance-seconds", 0))
|
||||||
|
);
|
||||||
assertThrows(
|
assertThrows(
|
||||||
IllegalArgumentException.class,
|
IllegalArgumentException.class,
|
||||||
() -> PluginSettings.from(Map.of("vigilante-arena-radius-blocks", 0))
|
() -> PluginSettings.from(Map.of("vigilante-arena-radius-blocks", 0))
|
||||||
|
|||||||
+40
-3
@@ -1,5 +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.assertNull;
|
||||||
import static org.mockito.ArgumentMatchers.contains;
|
import static org.mockito.ArgumentMatchers.contains;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.never;
|
import static org.mockito.Mockito.never;
|
||||||
@@ -12,15 +14,23 @@ import java.util.Map;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
import org.bukkit.Color;
|
||||||
import org.bukkit.Server;
|
import org.bukkit.Server;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
import org.bukkit.event.player.PlayerJoinEvent;
|
import org.bukkit.event.player.PlayerJoinEvent;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
final class VigilanteArenaControllerTest {
|
final class RoleArenaControllerTest {
|
||||||
private static final UUID TYRANT = UUID.fromString("11111111-1111-1111-1111-111111111111");
|
private static final UUID TYRANT = UUID.fromString("11111111-1111-1111-1111-111111111111");
|
||||||
private static final UUID PLAYER = UUID.fromString("22222222-2222-2222-2222-222222222222");
|
private static final UUID PLAYER = UUID.fromString("22222222-2222-2222-2222-222222222222");
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void boundaryColorsIdentifyTheOpenRole() {
|
||||||
|
assertEquals(Color.RED, RoleArenaController.boundaryColor(ArenaRole.TYRANT));
|
||||||
|
assertEquals(Color.BLUE, RoleArenaController.boundaryColor(ArenaRole.VIGILANTE));
|
||||||
|
assertNull(RoleArenaController.boundaryColor(null));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void optedInPlayerJoiningDuringVacancyReceivesArenaCoordinates() {
|
void optedInPlayerJoiningDuringVacancyReceivesArenaCoordinates() {
|
||||||
Player player = mock(Player.class);
|
Player player = mock(Player.class);
|
||||||
@@ -32,7 +42,7 @@ final class VigilanteArenaControllerTest {
|
|||||||
when(locations.location()).thenReturn(Optional.of(
|
when(locations.location()).thenReturn(Optional.of(
|
||||||
new ArenaLocation("world", 10.0, 64.0, -5.0, 0.0F, 0.0F)
|
new ArenaLocation("world", 10.0, 64.0, -5.0, 0.0F, 0.0F)
|
||||||
));
|
));
|
||||||
VigilanteArenaController controller = new VigilanteArenaController(
|
RoleArenaController controller = new RoleArenaController(
|
||||||
manager, mock(Server.class), locations, PluginSettings.from(Map.of())
|
manager, mock(Server.class), locations, PluginSettings.from(Map.of())
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -41,6 +51,26 @@ final class VigilanteArenaControllerTest {
|
|||||||
verify(player).sendMessage(contains("world at 10, 64, -5"));
|
verify(player).sendMessage(contains("world at 10, 64, -5"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void optedInPlayerIsNotifiedOfTyrantVacancy() {
|
||||||
|
Player player = mock(Player.class);
|
||||||
|
when(player.getUniqueId()).thenReturn(PLAYER);
|
||||||
|
TyrantStateManager manager = mock(TyrantStateManager.class);
|
||||||
|
when(manager.game()).thenReturn(tyrantVacancy());
|
||||||
|
when(manager.players()).thenReturn(Map.of(PLAYER, PlayerState.newPlayer(PLAYER, "Player")));
|
||||||
|
ArenaLocationStore locations = mock(ArenaLocationStore.class);
|
||||||
|
when(locations.location()).thenReturn(Optional.of(
|
||||||
|
new ArenaLocation("world", 10.0, 64.0, -5.0, 0.0F, 0.0F)
|
||||||
|
));
|
||||||
|
RoleArenaController controller = new RoleArenaController(
|
||||||
|
manager, mock(Server.class), locations, PluginSettings.from(Map.of())
|
||||||
|
);
|
||||||
|
|
||||||
|
controller.onJoin(mockJoin(player));
|
||||||
|
|
||||||
|
verify(player).sendMessage(contains("Tyrant position is open"));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void optedOutPlayerDoesNotReceiveVacancyNotice() {
|
void optedOutPlayerDoesNotReceiveVacancyNotice() {
|
||||||
Player player = mock(Player.class);
|
Player player = mock(Player.class);
|
||||||
@@ -56,7 +86,7 @@ final class VigilanteArenaControllerTest {
|
|||||||
when(locations.location()).thenReturn(Optional.of(
|
when(locations.location()).thenReturn(Optional.of(
|
||||||
new ArenaLocation("world", 10.0, 64.0, -5.0, 0.0F, 0.0F)
|
new ArenaLocation("world", 10.0, 64.0, -5.0, 0.0F, 0.0F)
|
||||||
));
|
));
|
||||||
VigilanteArenaController controller = new VigilanteArenaController(
|
RoleArenaController controller = new RoleArenaController(
|
||||||
manager, mock(Server.class), locations, PluginSettings.from(Map.of())
|
manager, mock(Server.class), locations, PluginSettings.from(Map.of())
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -71,6 +101,13 @@ final class VigilanteArenaControllerTest {
|
|||||||
return event;
|
return event;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static GameState tyrantVacancy() {
|
||||||
|
return new GameState(
|
||||||
|
GameLifecycle.RUNNING, Optional.empty(), Optional.empty(), Optional.empty(),
|
||||||
|
Optional.empty(), Optional.empty(), Duration.ZERO, 0, 1, Set.of()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private static GameState openGame() {
|
private static GameState openGame() {
|
||||||
return new GameState(
|
return new GameState(
|
||||||
GameLifecycle.RUNNING, Optional.of(TYRANT), Optional.empty(), Optional.empty(),
|
GameLifecycle.RUNNING, Optional.of(TYRANT), Optional.empty(), Optional.empty(),
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package games.dmg.spigottyrant;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
import 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 RoleArenaPolicyTest {
|
||||||
|
private static final UUID TYRANT = UUID.fromString("11111111-1111-1111-1111-111111111111");
|
||||||
|
private static final UUID PLAYER = UUID.fromString("22222222-2222-2222-2222-222222222222");
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void tyrantVacancyHasPriorityOverVigilanteVacancy() {
|
||||||
|
assertEquals(Optional.of(ArenaRole.TYRANT), RoleArenaPolicy.openRole(game(
|
||||||
|
GameLifecycle.RUNNING, Optional.empty(), Optional.empty(), Optional.empty()
|
||||||
|
)));
|
||||||
|
assertEquals(Optional.of(ArenaRole.VIGILANTE), RoleArenaPolicy.openRole(game(
|
||||||
|
GameLifecycle.RUNNING, Optional.of(TYRANT), Optional.empty(), Optional.empty()
|
||||||
|
)));
|
||||||
|
assertEquals(Optional.empty(), RoleArenaPolicy.openRole(game(
|
||||||
|
GameLifecycle.RUNNING, Optional.of(TYRANT), Optional.of(PLAYER), Optional.empty()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void pausedGameAndLegacyPendingTyrantDoNotOpenFight() {
|
||||||
|
assertEquals(Optional.empty(), RoleArenaPolicy.openRole(game(
|
||||||
|
GameLifecycle.PAUSED, Optional.empty(), Optional.empty(), Optional.empty()
|
||||||
|
)));
|
||||||
|
assertEquals(Optional.empty(), RoleArenaPolicy.openRole(game(
|
||||||
|
GameLifecycle.RUNNING, Optional.empty(), Optional.empty(),
|
||||||
|
Optional.of(new PendingSelection(PLAYER, Instant.parse("2026-08-24T00:00:00Z")))
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void activeTyrantCannotChallengeAndOptedOutPlayersRemainIneligible() {
|
||||||
|
GameState vigilanteFight = game(
|
||||||
|
GameLifecycle.RUNNING, Optional.of(TYRANT), Optional.empty(), Optional.empty()
|
||||||
|
);
|
||||||
|
GameState tyrantFight = game(
|
||||||
|
GameLifecycle.RUNNING, Optional.empty(), Optional.empty(), Optional.empty()
|
||||||
|
);
|
||||||
|
PlayerState eligible = PlayerState.newPlayer(PLAYER, "Player");
|
||||||
|
PlayerState optedOut = new PlayerState(
|
||||||
|
PLAYER, "Player", Optional.empty(), Optional.of(Instant.parse("2026-08-24T12:00:00Z")),
|
||||||
|
TyrantClass.NONE, Optional.empty(), Map.of(), Set.of(), java.util.List.of()
|
||||||
|
);
|
||||||
|
|
||||||
|
assertTrue(RoleArenaPolicy.isEligible(vigilanteFight, ArenaRole.VIGILANTE, eligible));
|
||||||
|
assertTrue(RoleArenaPolicy.isEligible(tyrantFight, ArenaRole.TYRANT, eligible));
|
||||||
|
assertFalse(RoleArenaPolicy.isEligible(vigilanteFight, ArenaRole.VIGILANTE, optedOut));
|
||||||
|
assertFalse(RoleArenaPolicy.isEligible(
|
||||||
|
vigilanteFight, ArenaRole.VIGILANTE, PlayerState.newPlayer(TYRANT, "Tyrant")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GameState game(
|
||||||
|
GameLifecycle lifecycle,
|
||||||
|
Optional<UUID> tyrant,
|
||||||
|
Optional<UUID> vigilante,
|
||||||
|
Optional<PendingSelection> pendingTyrant
|
||||||
|
) {
|
||||||
|
return new GameState(
|
||||||
|
lifecycle, tyrant, vigilante, pendingTyrant, Optional.empty(),
|
||||||
|
lifecycle == GameLifecycle.PAUSED
|
||||||
|
? Optional.of(Instant.parse("2026-08-23T12:00:00Z")) : Optional.empty(),
|
||||||
|
Duration.ZERO, 0, 0, Set.of()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
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 java.util.random.RandomGenerator;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
|
|
||||||
final class RoleCandidateSelectorTest {
|
|
||||||
@Test
|
|
||||||
void selectsOnlyRecentlyActiveEligibleNonExcludedPlayer() {
|
|
||||||
Instant now = Instant.parse("2026-08-14T12:00:00Z");
|
|
||||||
UUID tyrant = UUID.fromString("11111111-1111-1111-1111-111111111111");
|
|
||||||
UUID candidate = UUID.fromString("22222222-2222-2222-2222-222222222222");
|
|
||||||
UUID stale = UUID.fromString("33333333-3333-3333-3333-333333333333");
|
|
||||||
Map<UUID, PlayerState> players = Map.of(
|
|
||||||
tyrant, withLogin(PlayerState.newPlayer(tyrant, "Tyrant"), now),
|
|
||||||
candidate, withLogin(PlayerState.newPlayer(candidate, "Candidate"), now.minusSeconds(60)),
|
|
||||||
stale, withLogin(PlayerState.newPlayer(stale, "Stale"), now.minus(Duration.ofHours(25)))
|
|
||||||
);
|
|
||||||
|
|
||||||
Optional<UUID> selected = new RoleCandidateSelector().select(
|
|
||||||
players,
|
|
||||||
Set.of(tyrant),
|
|
||||||
Optional.empty(),
|
|
||||||
now,
|
|
||||||
Duration.ofHours(24),
|
|
||||||
RandomGenerator.of("L64X128MixRandom")
|
|
||||||
);
|
|
||||||
|
|
||||||
assertEquals(Optional.of(candidate), selected);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static PlayerState withLogin(PlayerState player, Instant login) {
|
|
||||||
return new PlayerState(
|
|
||||||
player.playerId(), player.latestName(), Optional.of(login), player.optedOutUntil(),
|
|
||||||
player.tyrantClass(), player.followerOf(), player.cooldownEnds(),
|
|
||||||
player.readyAbilityItems(), player.capturedMobs()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,7 +8,6 @@ import java.util.Map;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.random.RandomGenerator;
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
final class RoleMaintenanceServiceTest {
|
final class RoleMaintenanceServiceTest {
|
||||||
@@ -18,19 +17,25 @@ final class RoleMaintenanceServiceTest {
|
|||||||
private static final UUID OTHER = UUID.fromString("44444444-4444-4444-4444-444444444444");
|
private static final UUID OTHER = UUID.fromString("44444444-4444-4444-4444-444444444444");
|
||||||
private static final UUID FOLLOWER = UUID.fromString("55555555-5555-5555-5555-555555555555");
|
private static final UUID FOLLOWER = UUID.fromString("55555555-5555-5555-5555-555555555555");
|
||||||
private static final Instant NOW = Instant.parse("2026-08-14T12:00:00Z");
|
private static final Instant NOW = Instant.parse("2026-08-14T12:00:00Z");
|
||||||
private final RandomGenerator random = RandomGenerator.of("L64X128MixRandom");
|
|
||||||
private final RoleCandidateSelector selector = new RoleCandidateSelector();
|
|
||||||
private final RoleMaintenanceService service = new RoleMaintenanceService(
|
private final RoleMaintenanceService service = new RoleMaintenanceService(
|
||||||
selector,
|
new TyrantSuccessionService(), Duration.ofHours(48)
|
||||||
new TyrantSuccessionService(
|
|
||||||
selector, Duration.ofHours(24), Duration.ofHours(24), random
|
|
||||||
),
|
|
||||||
Duration.ofHours(48),
|
|
||||||
Duration.ofHours(24),
|
|
||||||
Duration.ofHours(24),
|
|
||||||
random
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void legacyPendingTyrantIsClearedIntoArenaVacancy() {
|
||||||
|
GameState game = game(
|
||||||
|
Optional.empty(), Optional.empty(),
|
||||||
|
Optional.of(new PendingSelection(CANDIDATE, NOW.plusSeconds(60))),
|
||||||
|
Optional.empty()
|
||||||
|
);
|
||||||
|
PersistentState state = new PersistentState(game, players(active(CANDIDATE)));
|
||||||
|
|
||||||
|
LifecycleState maintained = service.maintain(state, NOW);
|
||||||
|
|
||||||
|
assertEquals(Optional.empty(), maintained.game().tyrantId());
|
||||||
|
assertEquals(Optional.empty(), maintained.game().pendingTyrant());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void legacyPendingVigilanteIsClearedForArenaSuccession() {
|
void legacyPendingVigilanteIsClearedForArenaSuccession() {
|
||||||
GameState game = game(
|
GameState game = game(
|
||||||
@@ -41,7 +46,7 @@ final class RoleMaintenanceServiceTest {
|
|||||||
active(TYRANT), active(CANDIDATE)
|
active(TYRANT), active(CANDIDATE)
|
||||||
));
|
));
|
||||||
|
|
||||||
LifecycleState maintained = service.maintain(state, Set.of(TYRANT, CANDIDATE), NOW);
|
LifecycleState maintained = service.maintain(state, NOW);
|
||||||
|
|
||||||
assertEquals(Optional.empty(), maintained.game().vigilanteId());
|
assertEquals(Optional.empty(), maintained.game().vigilanteId());
|
||||||
assertEquals(Optional.empty(), maintained.game().pendingVigilante());
|
assertEquals(Optional.empty(), maintained.game().pendingVigilante());
|
||||||
@@ -57,7 +62,7 @@ final class RoleMaintenanceServiceTest {
|
|||||||
active(TYRANT), active(CANDIDATE), active(OTHER)
|
active(TYRANT), active(CANDIDATE), active(OTHER)
|
||||||
));
|
));
|
||||||
|
|
||||||
LifecycleState maintained = service.maintain(state, Set.of(TYRANT), NOW);
|
LifecycleState maintained = service.maintain(state, NOW);
|
||||||
|
|
||||||
assertEquals(Optional.empty(), maintained.game().vigilanteId());
|
assertEquals(Optional.empty(), maintained.game().vigilanteId());
|
||||||
assertEquals(Optional.empty(), maintained.game().pendingVigilante());
|
assertEquals(Optional.empty(), maintained.game().pendingVigilante());
|
||||||
@@ -78,7 +83,7 @@ final class RoleMaintenanceServiceTest {
|
|||||||
FOLLOWER, follower, OTHER, active(OTHER)
|
FOLLOWER, follower, OTHER, active(OTHER)
|
||||||
));
|
));
|
||||||
|
|
||||||
LifecycleState maintained = service.maintain(state, Set.of(TYRANT, OTHER), NOW);
|
LifecycleState maintained = service.maintain(state, NOW);
|
||||||
|
|
||||||
assertEquals(Optional.empty(), maintained.game().vigilanteId());
|
assertEquals(Optional.empty(), maintained.game().vigilanteId());
|
||||||
assertEquals(Optional.empty(), maintained.players().get(FOLLOWER).followerOf());
|
assertEquals(Optional.empty(), maintained.players().get(FOLLOWER).followerOf());
|
||||||
@@ -86,7 +91,7 @@ final class RoleMaintenanceServiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void inactiveTyrantEndsReignAndUsesNormalSuccession() {
|
void inactiveTyrantEndsReignAndOpensArena() {
|
||||||
PlayerState staleTyrant = withLogin(
|
PlayerState staleTyrant = withLogin(
|
||||||
active(TYRANT), NOW.minus(Duration.ofHours(48))
|
active(TYRANT), NOW.minus(Duration.ofHours(48))
|
||||||
);
|
);
|
||||||
@@ -104,8 +109,10 @@ final class RoleMaintenanceServiceTest {
|
|||||||
TYRANT, staleTyrant, VIGILANTE, active(VIGILANTE), OTHER, successor
|
TYRANT, staleTyrant, VIGILANTE, active(VIGILANTE), OTHER, successor
|
||||||
));
|
));
|
||||||
|
|
||||||
LifecycleState maintained = service.maintain(state, Set.of(OTHER), NOW);
|
LifecycleState maintained = service.maintain(state, NOW);
|
||||||
|
|
||||||
|
assertEquals(Optional.empty(), maintained.game().tyrantId());
|
||||||
|
assertEquals(Optional.empty(), maintained.game().pendingTyrant());
|
||||||
assertEquals(0, maintained.game().tyrantLevel());
|
assertEquals(0, maintained.game().tyrantLevel());
|
||||||
assertEquals(1, maintained.game().unspentChoices());
|
assertEquals(1, maintained.game().unspentChoices());
|
||||||
assertEquals(Set.of(), maintained.game().purchases());
|
assertEquals(Set.of(), maintained.game().purchases());
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import java.util.Map;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.random.RandomGenerator;
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
final class RoleRelinquishmentServiceTest {
|
final class RoleRelinquishmentServiceTest {
|
||||||
@@ -18,14 +17,9 @@ final class RoleRelinquishmentServiceTest {
|
|||||||
UUID vigilante = UUID.fromString("22222222-2222-2222-2222-222222222222");
|
UUID vigilante = UUID.fromString("22222222-2222-2222-2222-222222222222");
|
||||||
UUID candidate = UUID.fromString("33333333-3333-3333-3333-333333333333");
|
UUID candidate = UUID.fromString("33333333-3333-3333-3333-333333333333");
|
||||||
Instant now = Instant.parse("2026-08-14T12:00:00Z");
|
Instant now = Instant.parse("2026-08-14T12:00:00Z");
|
||||||
RoleCandidateSelector selector = new RoleCandidateSelector();
|
TyrantSuccessionService succession = new TyrantSuccessionService();
|
||||||
RandomGenerator random = RandomGenerator.of("L64X128MixRandom");
|
|
||||||
TyrantSuccessionService succession = new TyrantSuccessionService(
|
|
||||||
selector, Duration.ofHours(24), Duration.ofHours(24), random
|
|
||||||
);
|
|
||||||
RoleMaintenanceService maintenance = new RoleMaintenanceService(
|
RoleMaintenanceService maintenance = new RoleMaintenanceService(
|
||||||
selector, succession, Duration.ofHours(48), Duration.ofHours(24),
|
succession, Duration.ofHours(48)
|
||||||
Duration.ofHours(24), random
|
|
||||||
);
|
);
|
||||||
RoleRelinquishmentService service = new RoleRelinquishmentService(
|
RoleRelinquishmentService service = new RoleRelinquishmentService(
|
||||||
succession, maintenance
|
succession, maintenance
|
||||||
@@ -53,6 +47,38 @@ final class RoleRelinquishmentServiceTest {
|
|||||||
assertEquals(Optional.empty(), result.state().game().pendingVigilante());
|
assertEquals(Optional.empty(), result.state().game().pendingVigilante());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void tyrantRelinquishmentOpensTyrantArenaAndClearsReign() {
|
||||||
|
UUID tyrant = UUID.fromString("11111111-1111-1111-1111-111111111111");
|
||||||
|
UUID vigilante = UUID.fromString("22222222-2222-2222-2222-222222222222");
|
||||||
|
Instant now = Instant.parse("2026-08-14T12:00:00Z");
|
||||||
|
TyrantSuccessionService succession = new TyrantSuccessionService();
|
||||||
|
RoleRelinquishmentService service = new RoleRelinquishmentService(
|
||||||
|
succession, new RoleMaintenanceService(succession, Duration.ofHours(48))
|
||||||
|
);
|
||||||
|
GameState game = new GameState(
|
||||||
|
GameLifecycle.RUNNING, Optional.of(tyrant), Optional.of(vigilante),
|
||||||
|
Optional.empty(), Optional.empty(), Optional.empty(), Duration.ZERO,
|
||||||
|
4, 0, Set.of(TyrantUnlock.STRENGTH)
|
||||||
|
);
|
||||||
|
|
||||||
|
RelinquishmentResult result = service.relinquish(
|
||||||
|
new PersistentState(game, Map.of(
|
||||||
|
tyrant, active(tyrant, now), vigilante, active(vigilante, now)
|
||||||
|
)),
|
||||||
|
tyrant,
|
||||||
|
Set.of(tyrant, vigilante),
|
||||||
|
now
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals(RelinquishmentStatus.RELINQUISHED, result.status());
|
||||||
|
assertEquals(Optional.empty(), result.state().game().tyrantId());
|
||||||
|
assertEquals(Optional.empty(), result.state().game().pendingTyrant());
|
||||||
|
assertEquals(Optional.empty(), result.state().game().vigilanteId());
|
||||||
|
assertEquals(0, result.state().game().tyrantLevel());
|
||||||
|
assertEquals(1, result.state().game().unspentChoices());
|
||||||
|
}
|
||||||
|
|
||||||
private static PlayerState active(UUID id, Instant now) {
|
private static PlayerState active(UUID id, Instant now) {
|
||||||
PlayerState player = PlayerState.newPlayer(id, id.toString());
|
PlayerState player = PlayerState.newPlayer(id, id.toString());
|
||||||
return new PlayerState(
|
return new PlayerState(
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ final class TyrantAdminCommandTest {
|
|||||||
location.worldName().equals("arena_world")
|
location.worldName().equals("arena_world")
|
||||||
&& location.x() == 12.5 && location.y() == 70.0 && location.z() == -4.5
|
&& location.x() == 12.5 && location.y() == 70.0 && location.z() == -4.5
|
||||||
));
|
));
|
||||||
verify(sender).sendMessage(contains("Vigilante arena set"));
|
verify(sender).sendMessage(contains("Shared role arena set"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
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 TyrantArenaSuccessionServiceTest {
|
||||||
|
@Test
|
||||||
|
void eligibleBossWinnerClaimsVacantTyrantRoleWithFreshReign() {
|
||||||
|
UUID winner = UUID.fromString("22222222-2222-2222-2222-222222222222");
|
||||||
|
GameState game = new GameState(
|
||||||
|
GameLifecycle.RUNNING, Optional.empty(), Optional.empty(), Optional.empty(),
|
||||||
|
Optional.empty(), Optional.empty(), Duration.ZERO, 0, 1, Set.of()
|
||||||
|
);
|
||||||
|
PersistentState before = new PersistentState(
|
||||||
|
game, Map.of(winner, PlayerState.newPlayer(winner, "Winner"))
|
||||||
|
);
|
||||||
|
|
||||||
|
LifecycleState claimed = new TyrantArenaSuccessionService().claim(before, winner);
|
||||||
|
|
||||||
|
assertEquals(Optional.of(winner), claimed.game().tyrantId());
|
||||||
|
assertEquals(Optional.empty(), claimed.game().vigilanteId());
|
||||||
|
assertEquals(0, claimed.game().tyrantLevel());
|
||||||
|
assertEquals(1, claimed.game().unspentChoices());
|
||||||
|
assertEquals(Set.of(), claimed.game().purchases());
|
||||||
|
assertEquals(
|
||||||
|
Optional.of(ArenaRole.VIGILANTE),
|
||||||
|
RoleArenaPolicy.openRole(claimed.game())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +1,12 @@
|
|||||||
package games.dmg.spigottyrant;
|
package games.dmg.spigottyrant;
|
||||||
|
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
import static org.mockito.Mockito.doReturn;
|
import static org.mockito.Mockito.doReturn;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
import java.time.Clock;
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.Instant;
|
|
||||||
import java.time.ZoneOffset;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
@@ -42,7 +38,7 @@ final class TyrantDeathListenerTest {
|
|||||||
when(manager.game()).thenReturn(game);
|
when(manager.game()).thenReturn(game);
|
||||||
when(manager.snapshot()).thenReturn(before);
|
when(manager.snapshot()).thenReturn(before);
|
||||||
TyrantSuccessionService succession = mock(TyrantSuccessionService.class);
|
TyrantSuccessionService succession = mock(TyrantSuccessionService.class);
|
||||||
when(succession.succeed(eq(before), eq(Optional.of(killerId)), any(), any()))
|
when(succession.succeed(eq(before), eq(Optional.of(killerId))))
|
||||||
.thenReturn(succeeded);
|
.thenReturn(succeeded);
|
||||||
TyrantPresentation presentation = mock(TyrantPresentation.class);
|
TyrantPresentation presentation = mock(TyrantPresentation.class);
|
||||||
Server server = mock(Server.class);
|
Server server = mock(Server.class);
|
||||||
@@ -58,8 +54,7 @@ final class TyrantDeathListenerTest {
|
|||||||
manager,
|
manager,
|
||||||
succession,
|
succession,
|
||||||
presentation,
|
presentation,
|
||||||
server,
|
server
|
||||||
Clock.fixed(Instant.parse("2026-08-14T12:00:00Z"), ZoneOffset.UTC)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
listener.onDeath(event);
|
listener.onDeath(event);
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
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.assertNotEquals;
|
|
||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
@@ -9,7 +8,6 @@ import java.util.Map;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.random.RandomGenerator;
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
final class TyrantSuccessionServiceTest {
|
final class TyrantSuccessionServiceTest {
|
||||||
@@ -18,19 +16,14 @@ final class TyrantSuccessionServiceTest {
|
|||||||
private static final UUID KILLER = UUID.fromString("33333333-3333-3333-3333-333333333333");
|
private static final UUID KILLER = UUID.fromString("33333333-3333-3333-3333-333333333333");
|
||||||
private static final UUID OTHER = UUID.fromString("44444444-4444-4444-4444-444444444444");
|
private static final UUID OTHER = UUID.fromString("44444444-4444-4444-4444-444444444444");
|
||||||
private static final Instant NOW = Instant.parse("2026-08-14T12:00:00Z");
|
private static final Instant NOW = Instant.parse("2026-08-14T12:00:00Z");
|
||||||
private final TyrantSuccessionService service = new TyrantSuccessionService(
|
private final TyrantSuccessionService service = new TyrantSuccessionService();
|
||||||
new RoleCandidateSelector(),
|
|
||||||
Duration.ofHours(24),
|
|
||||||
Duration.ofHours(24),
|
|
||||||
RandomGenerator.of("L64X128MixRandom")
|
|
||||||
);
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void playerKillerBecomesTyrantAndEntireReignIsReset() {
|
void playerKillerBecomesTyrantAndEntireReignIsReset() {
|
||||||
PersistentState before = progressedState();
|
PersistentState before = progressedState();
|
||||||
|
|
||||||
LifecycleState after = service.succeed(
|
LifecycleState after = service.succeed(
|
||||||
before, Optional.of(KILLER), Set.of(KILLER, OTHER), NOW
|
before, Optional.of(KILLER)
|
||||||
);
|
);
|
||||||
|
|
||||||
assertEquals(Optional.of(KILLER), after.game().tyrantId());
|
assertEquals(Optional.of(KILLER), after.game().tyrantId());
|
||||||
@@ -39,6 +32,9 @@ final class TyrantSuccessionServiceTest {
|
|||||||
assertEquals(Set.of(), after.game().purchases());
|
assertEquals(Set.of(), after.game().purchases());
|
||||||
assertEquals(Optional.empty(), after.game().vigilanteId());
|
assertEquals(Optional.empty(), after.game().vigilanteId());
|
||||||
assertEquals(Optional.empty(), after.game().pendingVigilante());
|
assertEquals(Optional.empty(), after.game().pendingVigilante());
|
||||||
|
assertEquals(
|
||||||
|
Optional.of(ArenaRole.VIGILANTE), RoleArenaPolicy.openRole(after.game())
|
||||||
|
);
|
||||||
for (PlayerState player : after.players().values()) {
|
for (PlayerState player : after.players().values()) {
|
||||||
assertEquals(TyrantClass.NONE, player.tyrantClass());
|
assertEquals(TyrantClass.NONE, player.tyrantClass());
|
||||||
assertEquals(Optional.empty(), player.followerOf());
|
assertEquals(Optional.empty(), player.followerOf());
|
||||||
@@ -47,15 +43,14 @@ final class TyrantSuccessionServiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void environmentalDeathRandomlySelectsRecentPlayerInsteadOfFormerTyrant() {
|
void environmentalDeathOpensTyrantArenaWithoutPendingSelection() {
|
||||||
PersistentState before = progressedState();
|
PersistentState before = progressedState();
|
||||||
|
|
||||||
LifecycleState after = service.succeed(before, Optional.empty(), Set.of(OTHER), NOW);
|
LifecycleState after = service.succeed(before, Optional.empty());
|
||||||
|
|
||||||
UUID selected = after.game().tyrantId().orElseGet(() ->
|
assertEquals(Optional.empty(), after.game().tyrantId());
|
||||||
after.game().pendingTyrant().orElseThrow().candidateId()
|
assertEquals(Optional.empty(), after.game().pendingTyrant());
|
||||||
);
|
assertEquals(Optional.empty(), after.game().vigilanteId());
|
||||||
assertNotEquals(OLD_TYRANT, selected);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static PersistentState progressedState() {
|
private static PersistentState progressedState() {
|
||||||
|
|||||||
@@ -1,61 +0,0 @@
|
|||||||
package games.dmg.spigottyrant;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
||||||
|
|
||||||
import java.time.Duration;
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.UUID;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
|
|
||||||
final class VigilanteArenaPolicyTest {
|
|
||||||
private static final UUID TYRANT = UUID.fromString("11111111-1111-1111-1111-111111111111");
|
|
||||||
private static final UUID PLAYER = UUID.fromString("22222222-2222-2222-2222-222222222222");
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void opensOnlyForRunningGameWithTyrantAndVacantVigilante() {
|
|
||||||
assertTrue(VigilanteArenaPolicy.isOpen(game(
|
|
||||||
GameLifecycle.RUNNING, Optional.of(TYRANT), Optional.empty()
|
|
||||||
)));
|
|
||||||
assertFalse(VigilanteArenaPolicy.isOpen(game(
|
|
||||||
GameLifecycle.PAUSED, Optional.of(TYRANT), Optional.empty()
|
|
||||||
)));
|
|
||||||
assertFalse(VigilanteArenaPolicy.isOpen(game(
|
|
||||||
GameLifecycle.RUNNING, Optional.empty(), Optional.empty()
|
|
||||||
)));
|
|
||||||
assertFalse(VigilanteArenaPolicy.isOpen(game(
|
|
||||||
GameLifecycle.RUNNING, Optional.of(TYRANT), Optional.of(PLAYER)
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void onlyOptedInNonTyrantMayChallenge() {
|
|
||||||
GameState game = game(GameLifecycle.RUNNING, Optional.of(TYRANT), Optional.empty());
|
|
||||||
PlayerState eligible = PlayerState.newPlayer(PLAYER, "Player");
|
|
||||||
PlayerState optedOut = new PlayerState(
|
|
||||||
PLAYER, "Player", Optional.empty(), Optional.of(Instant.parse("2026-08-24T12:00:00Z")),
|
|
||||||
TyrantClass.NONE, Optional.empty(), Map.of(), Set.of(), java.util.List.of()
|
|
||||||
);
|
|
||||||
|
|
||||||
assertTrue(VigilanteArenaPolicy.isEligible(game, eligible));
|
|
||||||
assertFalse(VigilanteArenaPolicy.isEligible(game, optedOut));
|
|
||||||
assertFalse(VigilanteArenaPolicy.isEligible(game,
|
|
||||||
PlayerState.newPlayer(TYRANT, "Tyrant")));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static GameState game(
|
|
||||||
GameLifecycle lifecycle,
|
|
||||||
Optional<UUID> tyrant,
|
|
||||||
Optional<UUID> vigilante
|
|
||||||
) {
|
|
||||||
return new GameState(
|
|
||||||
lifecycle, tyrant, vigilante, Optional.empty(), Optional.empty(),
|
|
||||||
lifecycle == GameLifecycle.PAUSED
|
|
||||||
? Optional.of(Instant.parse("2026-08-23T12:00:00Z")) : Optional.empty(),
|
|
||||||
Duration.ZERO, 0, 0, Set.of()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user