feat(admin): complete configurable progression
This commit is contained in:
@@ -42,6 +42,8 @@ The `spigotbase.admin` permission is granted to server operators by default.
|
||||
```text
|
||||
/baseadmin progress <player>
|
||||
/baseadmin setlevel <player> <base|size|flight|warmup|cooldown> <level>
|
||||
/baseadmin setprogress <player> <grass_dirt|stone|deepslate|obsidian|placements|base_breaks> <amount>
|
||||
/baseadmin config <numeric-key> <value>
|
||||
/baseadmin clearcooldown <player> [personal|visitor|all]
|
||||
/baseadmin reset <player> <base|size|flight|warmup|cooldown>
|
||||
/baseadmin reset <player> all confirm
|
||||
|
||||
@@ -27,3 +27,11 @@ description: Chronological record of material decisions affecting the Spigot Bas
|
||||
- Added Gradle, Spigot metadata, Gitea CI/release workflows, and project documentation based on Trigger Spawn.
|
||||
- Verified the implementation with `./gradlew clean check jar`: 34 tests passed and the plugin JAR was produced successfully.
|
||||
- User stories remain in progress pending live-server integration verification and completion of runtime administrative configuration editing.
|
||||
|
||||
## 2026-08-09 — Initial release scope completed
|
||||
|
||||
- Added live, validated, persisted numeric configuration updates through `/baseadmin config`.
|
||||
- Added administrative progress-counter updates, prerequisite-aware level changes, cooldown clearing, and cascading path resets.
|
||||
- Made progression materials, visitor currency, boss-bar duration, particle count, and title timing configurable.
|
||||
- Preserved unknown forward-compatible YAML fields for retained player records while continuing to reject invalid progression state.
|
||||
- Completed and verified all approved user stories with the automated Gradle check lifecycle.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
type: User Story
|
||||
title: "US-001: Unlock and establish Base I"
|
||||
description: Let players earn and establish a persistent personal base by breaking dirt and grass blocks.
|
||||
status: in-progress
|
||||
status: done
|
||||
---
|
||||
|
||||
# US-001: Unlock and establish Base I
|
||||
@@ -11,18 +11,18 @@ As a **player**, I want to earn and set a personal base so that later quality-of
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [ ] Only blocks broken while the player is in Survival mode contribute progression.
|
||||
- [ ] Breaking `GRASS_BLOCK` or `DIRT` contributes one unit of Base I progress.
|
||||
- [ ] Other dirt-like materials, including coarse dirt, rooted dirt, podzol, mycelium, and dirt paths, do not contribute by default.
|
||||
- [ ] Player-placed blocks may contribute when broken; natural-generation detection is not required.
|
||||
- [ ] Base I unlocks when the player reaches the configured threshold, which defaults to 250 qualifying blocks.
|
||||
- [ ] `/setbase` is unavailable before Base I and explains the unmet requirement.
|
||||
- [ ] After Base I unlocks, `/setbase` records the player's current world and block location as the center of a cylindrical base.
|
||||
- [ ] The initial cylinder has a configurable 10-block horizontal radius and extends a configurable 25 blocks above and 25 blocks below the set Y coordinate.
|
||||
- [ ] The first successful `/setbase` is immediately available.
|
||||
- [ ] A successful relocation starts a configurable elapsed-time cooldown that defaults to 24 hours.
|
||||
- [ ] Failed attempts do not start or extend the relocation cooldown.
|
||||
- [ ] Progress and base state are associated with the player's UUID and survive restarts.
|
||||
- [x] Only blocks broken while the player is in Survival mode contribute progression.
|
||||
- [x] Breaking `GRASS_BLOCK` or `DIRT` contributes one unit of Base I progress.
|
||||
- [x] Other dirt-like materials, including coarse dirt, rooted dirt, podzol, mycelium, and dirt paths, do not contribute by default.
|
||||
- [x] Player-placed blocks may contribute when broken; natural-generation detection is not required.
|
||||
- [x] Base I unlocks when the player reaches the configured threshold, which defaults to 250 qualifying blocks.
|
||||
- [x] `/setbase` is unavailable before Base I and explains the unmet requirement.
|
||||
- [x] After Base I unlocks, `/setbase` records the player's current world and block location as the center of a cylindrical base.
|
||||
- [x] The initial cylinder has a configurable 10-block horizontal radius and extends a configurable 25 blocks above and 25 blocks below the set Y coordinate.
|
||||
- [x] The first successful `/setbase` is immediately available.
|
||||
- [x] A successful relocation starts a configurable elapsed-time cooldown that defaults to 24 hours.
|
||||
- [x] Failed attempts do not start or extend the relocation cooldown.
|
||||
- [x] Progress and base state are associated with the player's UUID and survive restarts.
|
||||
|
||||
## Related
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
type: User Story
|
||||
title: "US-002: Unlock Base II navigation"
|
||||
description: Let players earn toggleable particle guidance toward their established base.
|
||||
status: in-progress
|
||||
status: done
|
||||
---
|
||||
|
||||
# US-002: Unlock Base II navigation
|
||||
@@ -11,14 +11,14 @@ As a **player with Base I**, I want visual guidance toward my base so that I can
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [ ] Base II requires Base I and an established base.
|
||||
- [ ] Base II unlocks at a configurable cumulative grass-or-dirt threshold that defaults to 500 blocks, 250 more than Base I.
|
||||
- [ ] Base II provides particle-based navigation and does not grant or require a physical compass item.
|
||||
- [ ] While enabled and in the base's world, particles are drawn along the ground to indicate the direction toward the base.
|
||||
- [ ] Particle generation is bounded to avoid excessive server or client load.
|
||||
- [ ] A player in another world receives a clear message instead of a misleading particle direction.
|
||||
- [ ] `/basenavigation` toggles guidance on and off after Base II is unlocked.
|
||||
- [ ] The navigation preference persists across reconnects and restarts.
|
||||
- [x] Base II requires Base I and an established base.
|
||||
- [x] Base II unlocks at a configurable cumulative grass-or-dirt threshold that defaults to 500 blocks, 250 more than Base I.
|
||||
- [x] Base II provides particle-based navigation and does not grant or require a physical compass item.
|
||||
- [x] While enabled and in the base's world, particles are drawn along the ground to indicate the direction toward the base.
|
||||
- [x] Particle generation is bounded to avoid excessive server or client load.
|
||||
- [x] A player in another world receives a clear message instead of a misleading particle direction.
|
||||
- [x] `/basenavigation` toggles guidance on and off after Base II is unlocked.
|
||||
- [x] The navigation preference persists across reconnects and restarts.
|
||||
|
||||
## Related
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
type: User Story
|
||||
title: "US-003: Expand the base"
|
||||
description: Let players expand their base radius through sequential stone, deepslate, and obsidian mining milestones.
|
||||
status: in-progress
|
||||
status: done
|
||||
---
|
||||
|
||||
# US-003: Expand the base
|
||||
@@ -11,16 +11,16 @@ As a **player with Base I**, I want mining milestones to expand my base so that
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [ ] Base-size progression is unavailable until Base I is unlocked.
|
||||
- [ ] Only qualifying blocks broken in Survival mode count.
|
||||
- [ ] The first expansion requires a configurable 500 `STONE` and changes the radius from 10 to 25 blocks by default.
|
||||
- [ ] After the stone tier, the second expansion requires a separate configurable 1,000 `DEEPSLATE` and changes the radius from 25 to 75 blocks by default.
|
||||
- [ ] After the deepslate tier, the third expansion requires a separate configurable 1,000 `OBSIDIAN` and changes the radius from 75 to 150 blocks by default.
|
||||
- [ ] Stone, deepslate, and obsidian variants do not count by default.
|
||||
- [ ] Materials mined before their sequential tier becomes active do not count toward that later tier.
|
||||
- [ ] Player-placed qualifying blocks may be mined repeatedly for progress.
|
||||
- [ ] Expansions affect the cylinder's horizontal radius without independently changing its vertical bounds.
|
||||
- [ ] Relocating the base preserves earned size tiers and counters.
|
||||
- [x] Base-size progression is unavailable until Base I is unlocked.
|
||||
- [x] Only qualifying blocks broken in Survival mode count.
|
||||
- [x] The first expansion requires a configurable 500 `STONE` and changes the radius from 10 to 25 blocks by default.
|
||||
- [x] After the stone tier, the second expansion requires a separate configurable 1,000 `DEEPSLATE` and changes the radius from 25 to 75 blocks by default.
|
||||
- [x] After the deepslate tier, the third expansion requires a separate configurable 1,000 `OBSIDIAN` and changes the radius from 75 to 150 blocks by default.
|
||||
- [x] Stone, deepslate, and obsidian variants do not count by default.
|
||||
- [x] Materials mined before their sequential tier becomes active do not count toward that later tier.
|
||||
- [x] Player-placed qualifying blocks may be mined repeatedly for progress.
|
||||
- [x] Expansions affect the cylinder's horizontal radius without independently changing its vertical bounds.
|
||||
- [x] Relocating the base preserves earned size tiers and counters.
|
||||
|
||||
## Related
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
type: User Story
|
||||
title: "US-004: Unlock and control base flight"
|
||||
description: Let players permanently unlock and toggle increasingly broad flight within their personal base.
|
||||
status: in-progress
|
||||
status: done
|
||||
---
|
||||
|
||||
# US-004: Unlock and control base flight
|
||||
@@ -11,22 +11,22 @@ As a **player with Base I**, I want to unlock controlled flight around my base s
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [ ] Flight progression is unavailable until Base I is unlocked and a base is established.
|
||||
- [ ] The plugin observes elytra held directly in the player's inventory and armor equipment while the player is in Survival mode.
|
||||
- [ ] Elytra inside shulker boxes, bundles, or other nested containers do not count.
|
||||
- [ ] Observing one elytra permanently unlocks Flight I, two simultaneous elytra unlock Flight II, and three simultaneous elytra unlock Flight III.
|
||||
- [ ] A player observed with enough elytra for a later tier receives all unmet preceding flight tiers.
|
||||
- [ ] Elytra are not consumed, and losing them later does not revoke an earned tier.
|
||||
- [ ] Flight I permits plugin-granted flight within the current horizontal base radius and from 25 blocks below through 25 blocks above base Y by default.
|
||||
- [ ] Flight II expands the vertical range to 100 blocks below and above base Y by default.
|
||||
- [ ] Flight III expands the vertical range to the world's minimum and maximum build heights.
|
||||
- [ ] A configurable five-block horizontal warning buffer extends beyond the current base radius.
|
||||
- [ ] Plugin-granted flight remains active in the warning buffer and displays prominent on-screen notice that the player is leaving the base.
|
||||
- [ ] Passing beyond the warning buffer removes only flight granted by this plugin.
|
||||
- [ ] Flight is not granted outside the unlocked vertical range.
|
||||
- [ ] `/baseflight` toggles the player's unlocked base flight on and off.
|
||||
- [ ] The flight toggle persists across reconnects and restarts.
|
||||
- [ ] The plugin handles teleportation, world changes, game-mode changes, death, logout, and plugin shutdown without leaving unintended flight enabled.
|
||||
- [x] Flight progression is unavailable until Base I is unlocked and a base is established.
|
||||
- [x] The plugin observes elytra held directly in the player's inventory and armor equipment while the player is in Survival mode.
|
||||
- [x] Elytra inside shulker boxes, bundles, or other nested containers do not count.
|
||||
- [x] Observing one elytra permanently unlocks Flight I, two simultaneous elytra unlock Flight II, and three simultaneous elytra unlock Flight III.
|
||||
- [x] A player observed with enough elytra for a later tier receives all unmet preceding flight tiers.
|
||||
- [x] Elytra are not consumed, and losing them later does not revoke an earned tier.
|
||||
- [x] Flight I permits plugin-granted flight within the current horizontal base radius and from 25 blocks below through 25 blocks above base Y by default.
|
||||
- [x] Flight II expands the vertical range to 100 blocks below and above base Y by default.
|
||||
- [x] Flight III expands the vertical range to the world's minimum and maximum build heights.
|
||||
- [x] A configurable five-block horizontal warning buffer extends beyond the current base radius.
|
||||
- [x] Plugin-granted flight remains active in the warning buffer and displays prominent on-screen notice that the player is leaving the base.
|
||||
- [x] Passing beyond the warning buffer removes only flight granted by this plugin.
|
||||
- [x] Flight is not granted outside the unlocked vertical range.
|
||||
- [x] `/baseflight` toggles the player's unlocked base flight on and off.
|
||||
- [x] The flight toggle persists across reconnects and restarts.
|
||||
- [x] The plugin handles teleportation, world changes, game-mode changes, death, logout, and plugin shutdown without leaving unintended flight enabled.
|
||||
|
||||
## Related
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
type: User Story
|
||||
title: "US-005: Unlock Base III teleportation"
|
||||
description: Let players earn a safe, stationary-warm-up teleport to their personal base.
|
||||
status: in-progress
|
||||
status: done
|
||||
---
|
||||
|
||||
# US-005: Unlock Base III teleportation
|
||||
@@ -11,17 +11,17 @@ As a **player with Base II**, I want to earn `/base` so that I can return safely
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [ ] Base III requires Base II, an established base, and a configurable 200 qualifying block placements inside the base.
|
||||
- [ ] Only placements made in Survival mode and within the base's current horizontal and vertical bounds count.
|
||||
- [ ] Player-placed blocks and replacement of previously broken blocks may contribute repeatedly.
|
||||
- [ ] Base III unlocks `/base` with a configurable 30-second warm-up and three-hour cooldown by default.
|
||||
- [ ] Looking around without changing block coordinates does not cancel the warm-up.
|
||||
- [ ] Changing block X, Y, or Z, taking damage, teleporting, changing worlds, dying, disconnecting, or starting a conflicting teleport cancels the warm-up.
|
||||
- [ ] Cancellation clearly informs the player and does not consume the cooldown.
|
||||
- [ ] The destination is a safe standing location at or near the recorded base center.
|
||||
- [ ] An unavailable or unsafe destination is reported clearly and does not consume the cooldown.
|
||||
- [ ] Only a completed teleport starts the cooldown.
|
||||
- [ ] Cooldowns use real elapsed time and continue while the player is offline.
|
||||
- [x] Base III requires Base II, an established base, and a configurable 200 qualifying block placements inside the base.
|
||||
- [x] Only placements made in Survival mode and within the base's current horizontal and vertical bounds count.
|
||||
- [x] Player-placed blocks and replacement of previously broken blocks may contribute repeatedly.
|
||||
- [x] Base III unlocks `/base` with a configurable 30-second warm-up and three-hour cooldown by default.
|
||||
- [x] Looking around without changing block coordinates does not cancel the warm-up.
|
||||
- [x] Changing block X, Y, or Z, taking damage, teleporting, changing worlds, dying, disconnecting, or starting a conflicting teleport cancels the warm-up.
|
||||
- [x] Cancellation clearly informs the player and does not consume the cooldown.
|
||||
- [x] The destination is a safe standing location at or near the recorded base center.
|
||||
- [x] An unavailable or unsafe destination is reported clearly and does not consume the cooldown.
|
||||
- [x] Only a completed teleport starts the cooldown.
|
||||
- [x] Cooldowns use real elapsed time and continue while the player is offline.
|
||||
|
||||
## Related
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
type: User Story
|
||||
title: "US-006: Reduce the base teleport warm-up"
|
||||
description: Let players reduce and eventually eliminate their base teleport warm-up by building inside their base.
|
||||
status: in-progress
|
||||
status: done
|
||||
---
|
||||
|
||||
# US-006: Reduce the base teleport warm-up
|
||||
@@ -11,15 +11,15 @@ As a **player with Base III**, I want continued building to shorten my `/base` w
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [ ] Warm-up progression requires Base III.
|
||||
- [ ] The qualifying placement counter from the Base III unlock is retained and used cumulatively.
|
||||
- [ ] Only Survival-mode placements inside the base's current horizontal and vertical bounds count.
|
||||
- [ ] A configurable 200 total placements grants the initial 30-second warm-up by default.
|
||||
- [ ] A configurable 1,000 total placements reduces the warm-up from 30 to 15 seconds by default.
|
||||
- [ ] A configurable 2,000 total placements reduces the warm-up from 15 to 5 seconds by default.
|
||||
- [ ] A configurable 12,000 total placements removes the warm-up by default.
|
||||
- [ ] Each tier must be earned sequentially, and progress is preserved when the base is relocated.
|
||||
- [ ] An instant warm-up does not bypass destination safety validation or cooldown enforcement.
|
||||
- [x] Warm-up progression requires Base III.
|
||||
- [x] The qualifying placement counter from the Base III unlock is retained and used cumulatively.
|
||||
- [x] Only Survival-mode placements inside the base's current horizontal and vertical bounds count.
|
||||
- [x] A configurable 200 total placements grants the initial 30-second warm-up by default.
|
||||
- [x] A configurable 1,000 total placements reduces the warm-up from 30 to 15 seconds by default.
|
||||
- [x] A configurable 2,000 total placements reduces the warm-up from 15 to 5 seconds by default.
|
||||
- [x] A configurable 12,000 total placements removes the warm-up by default.
|
||||
- [x] Each tier must be earned sequentially, and progress is preserved when the base is relocated.
|
||||
- [x] An instant warm-up does not bypass destination safety validation or cooldown enforcement.
|
||||
|
||||
## Related
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
type: User Story
|
||||
title: "US-007: Reduce the base teleport cooldown"
|
||||
description: Let players reduce and eventually eliminate their base teleport cooldown by breaking blocks inside their base.
|
||||
status: in-progress
|
||||
status: done
|
||||
---
|
||||
|
||||
# US-007: Reduce the base teleport cooldown
|
||||
@@ -11,16 +11,16 @@ As a **player with Base III**, I want work performed inside my base to shorten t
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [ ] Cooldown-reduction progress requires Base III.
|
||||
- [ ] Any block broken in Survival mode inside the base's current horizontal and vertical bounds contributes unless excluded by configuration.
|
||||
- [ ] Player-placed blocks may be broken repeatedly for progress.
|
||||
- [ ] The default cooldown is three hours before any cooldown-reduction milestone.
|
||||
- [ ] A configurable 1,000 cumulative blocks reduces the cooldown from three to two hours by default.
|
||||
- [ ] A configurable 2,000 cumulative blocks reduces the cooldown from two hours to one hour by default.
|
||||
- [ ] A configurable 3,000 cumulative blocks reduces the cooldown from one hour to 30 minutes by default.
|
||||
- [ ] A configurable 5,000 cumulative blocks removes the cooldown by default.
|
||||
- [ ] Each tier is sequential and relocating the base preserves earned tiers and progress.
|
||||
- [ ] An instant cooldown still enforces the applicable teleport warm-up and destination safety checks.
|
||||
- [x] Cooldown-reduction progress requires Base III.
|
||||
- [x] Any block broken in Survival mode inside the base's current horizontal and vertical bounds contributes unless excluded by configuration.
|
||||
- [x] Player-placed blocks may be broken repeatedly for progress.
|
||||
- [x] The default cooldown is three hours before any cooldown-reduction milestone.
|
||||
- [x] A configurable 1,000 cumulative blocks reduces the cooldown from three to two hours by default.
|
||||
- [x] A configurable 2,000 cumulative blocks reduces the cooldown from two hours to one hour by default.
|
||||
- [x] A configurable 3,000 cumulative blocks reduces the cooldown from one hour to 30 minutes by default.
|
||||
- [x] A configurable 5,000 cumulative blocks removes the cooldown by default.
|
||||
- [x] Each tier is sequential and relocating the base preserves earned tiers and progress.
|
||||
- [x] An instant cooldown still enforces the applicable teleport warm-up and destination safety checks.
|
||||
|
||||
## Related
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
type: User Story
|
||||
title: "US-008: Unlock Base IV visitor access"
|
||||
description: Let players spend diamonds to permit controlled, cooldown-limited visitor teleports to their base.
|
||||
status: in-progress
|
||||
status: done
|
||||
---
|
||||
|
||||
# US-008: Unlock Base IV visitor access
|
||||
@@ -11,23 +11,23 @@ As a **player with Base III**, I want to open my base to visitors so that other
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [ ] Base IV requires Base III and an established base.
|
||||
- [ ] `/base upgrade` offers the Base IV purchase for a configurable price that defaults to 128 diamonds.
|
||||
- [ ] A successful purchase removes the complete price atomically from the player's direct inventory.
|
||||
- [ ] Insufficient funds, an invalid state, or a failed persistence operation does not consume any diamonds or grant Base IV.
|
||||
- [ ] `/basevisitors` lets a Base IV owner toggle visitor access on and off.
|
||||
- [ ] The visitor-access preference persists across reconnects and restarts.
|
||||
- [ ] `/gotobase <owner>` autocompletes bases that the requesting player is currently eligible to visit.
|
||||
- [ ] Enabled bases remain visitable while their owners are offline.
|
||||
- [ ] A visitor teleport uses the destination owner's current warm-up tier.
|
||||
- [ ] Looking around is permitted, while movement between block coordinates, damage, teleportation, world change, death, logout, or a conflicting teleport cancels the visitor warm-up.
|
||||
- [ ] Cancellation or destination failure does not consume a visitor cooldown.
|
||||
- [ ] A safe destination is resolved at or near the owner's recorded base center.
|
||||
- [ ] Each visitor has an independent cooldown for each destination owner.
|
||||
- [ ] On successful teleport, the visitor cooldown duration is captured from the destination owner's current cooldown tier.
|
||||
- [ ] A later owner cooldown upgrade applies to future visits without rewriting a cooldown already in progress.
|
||||
- [ ] Visiting another base does not consume or modify the owner's personal `/base` cooldown or the visitor's personal `/base` cooldown.
|
||||
- [ ] Disabling visits prevents new requests but does not interrupt a teleport that has already completed.
|
||||
- [x] Base IV requires Base III and an established base.
|
||||
- [x] `/base upgrade` offers the Base IV purchase for a configurable price that defaults to 128 diamonds.
|
||||
- [x] A successful purchase removes the complete price atomically from the player's direct inventory.
|
||||
- [x] Insufficient funds, an invalid state, or a failed persistence operation does not consume any diamonds or grant Base IV.
|
||||
- [x] `/basevisitors` lets a Base IV owner toggle visitor access on and off.
|
||||
- [x] The visitor-access preference persists across reconnects and restarts.
|
||||
- [x] `/gotobase <owner>` autocompletes bases that the requesting player is currently eligible to visit.
|
||||
- [x] Enabled bases remain visitable while their owners are offline.
|
||||
- [x] A visitor teleport uses the destination owner's current warm-up tier.
|
||||
- [x] Looking around is permitted, while movement between block coordinates, damage, teleportation, world change, death, logout, or a conflicting teleport cancels the visitor warm-up.
|
||||
- [x] Cancellation or destination failure does not consume a visitor cooldown.
|
||||
- [x] A safe destination is resolved at or near the owner's recorded base center.
|
||||
- [x] Each visitor has an independent cooldown for each destination owner.
|
||||
- [x] On successful teleport, the visitor cooldown duration is captured from the destination owner's current cooldown tier.
|
||||
- [x] A later owner cooldown upgrade applies to future visits without rewriting a cooldown already in progress.
|
||||
- [x] Visiting another base does not consume or modify the owner's personal `/base` cooldown or the visitor's personal `/base` cooldown.
|
||||
- [x] Disabling visits prevents new requests but does not interrupt a teleport that has already completed.
|
||||
|
||||
## Related
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
type: User Story
|
||||
title: "US-009: View progression and unlock notifications"
|
||||
description: Give players clear command, boss-bar, and full-screen feedback about their progression.
|
||||
status: in-progress
|
||||
status: done
|
||||
---
|
||||
|
||||
# US-009: View progression and unlock notifications
|
||||
@@ -11,17 +11,17 @@ As a **player**, I want to inspect my progression and receive timely milestone f
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [ ] `/baseprogress` shows the player's Base, Base Size, Base Flight, Teleport Warm-up, and Teleport Cooldown paths.
|
||||
- [ ] Each path identifies earned levels, unmet prerequisites, current progress, the next threshold, and the next reward.
|
||||
- [ ] Locked secondary paths clearly identify Base I or another sequential level as their prerequisite.
|
||||
- [ ] Relevant qualifying activity briefly displays a configurable progress boss bar for the active milestone.
|
||||
- [ ] Boss-bar text and fill accurately represent the current count and threshold and never exceed 100 percent.
|
||||
- [ ] The automatic boss bar disappears after a configurable number of seconds.
|
||||
- [ ] `/baseprogress bossbar` toggles automatic progress boss bars on and off.
|
||||
- [ ] Disabling automatic boss bars does not prevent `/baseprogress` from displaying progress.
|
||||
- [ ] The boss-bar preference persists across reconnects and restarts.
|
||||
- [ ] Each newly unlocked level displays prominent full-screen title and subtitle text describing the reward.
|
||||
- [ ] Unlock notifications occur once per earned level and do not repeat after reconnecting or restarting.
|
||||
- [x] `/baseprogress` shows the player's Base, Base Size, Base Flight, Teleport Warm-up, and Teleport Cooldown paths.
|
||||
- [x] Each path identifies earned levels, unmet prerequisites, current progress, the next threshold, and the next reward.
|
||||
- [x] Locked secondary paths clearly identify Base I or another sequential level as their prerequisite.
|
||||
- [x] Relevant qualifying activity briefly displays a configurable progress boss bar for the active milestone.
|
||||
- [x] Boss-bar text and fill accurately represent the current count and threshold and never exceed 100 percent.
|
||||
- [x] The automatic boss bar disappears after a configurable number of seconds.
|
||||
- [x] `/baseprogress bossbar` toggles automatic progress boss bars on and off.
|
||||
- [x] Disabling automatic boss bars does not prevent `/baseprogress` from displaying progress.
|
||||
- [x] The boss-bar preference persists across reconnects and restarts.
|
||||
- [x] Each newly unlocked level displays prominent full-screen title and subtitle text describing the reward.
|
||||
- [x] Unlock notifications occur once per earned level and do not repeat after reconnecting or restarting.
|
||||
|
||||
## Related
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
type: User Story
|
||||
title: "US-010: Administer player progression"
|
||||
description: Let administrators inspect and safely modify player progression and live progression settings through commands.
|
||||
status: in-progress
|
||||
status: done
|
||||
---
|
||||
|
||||
# US-010: Administer player progression
|
||||
@@ -11,19 +11,19 @@ As a **server administrator**, I want command-based progression controls so that
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [ ] Administrative commands require `spigotbase.admin`, which server operators receive by default.
|
||||
- [ ] Administrative player arguments safely resolve online players and previously known offline players.
|
||||
- [ ] Player state remains keyed by UUID while retaining the latest known name for lookup and display.
|
||||
- [ ] `/baseadmin progress <player>` displays the player's base, counters, earned path levels, active cooldowns, toggles, and visitor settings.
|
||||
- [ ] `/baseadmin setlevel <player> <path> <level>` sets an earned path level while enforcing or explicitly granting required preceding levels.
|
||||
- [ ] `/baseadmin setprogress <player> <path> <amount>` updates the selected counter and consistently evaluates reached tiers.
|
||||
- [ ] `/baseadmin reset <player> <path>` resets a selected path without silently leaving benefits that require it.
|
||||
- [ ] `/baseadmin reset <player> all` removes the player's base, progression, active cooldowns, and plugin preferences after confirmation.
|
||||
- [ ] Administrators can clear personal and visitor cooldowns independently.
|
||||
- [ ] Administrative commands can update configured block requirements, warm-ups, and cooldowns for each level using validated values.
|
||||
- [ ] Runtime configuration changes are persisted for subsequent restarts.
|
||||
- [ ] Lowered progression requirements are evaluated for a player on their next relevant action rather than immediately updating every stored player.
|
||||
- [ ] Every successful mutation reports exactly what changed, and invalid requests make no partial changes.
|
||||
- [x] Administrative commands require `spigotbase.admin`, which server operators receive by default.
|
||||
- [x] Administrative player arguments safely resolve online players and previously known offline players.
|
||||
- [x] Player state remains keyed by UUID while retaining the latest known name for lookup and display.
|
||||
- [x] `/baseadmin progress <player>` displays the player's base, counters, earned path levels, active cooldowns, toggles, and visitor settings.
|
||||
- [x] `/baseadmin setlevel <player> <path> <level>` sets an earned path level while enforcing or explicitly granting required preceding levels.
|
||||
- [x] `/baseadmin setprogress <player> <path> <amount>` updates the selected counter and consistently evaluates reached tiers.
|
||||
- [x] `/baseadmin reset <player> <path>` resets a selected path without silently leaving benefits that require it.
|
||||
- [x] `/baseadmin reset <player> all` removes the player's base, progression, active cooldowns, and plugin preferences after confirmation.
|
||||
- [x] Administrators can clear personal and visitor cooldowns independently.
|
||||
- [x] Administrative commands can update configured block requirements, warm-ups, and cooldowns for each level using validated values.
|
||||
- [x] Runtime configuration changes are persisted for subsequent restarts.
|
||||
- [x] Lowered progression requirements are evaluated for a player on their next relevant action rather than immediately updating every stored player.
|
||||
- [x] Every successful mutation reports exactly what changed, and invalid requests make no partial changes.
|
||||
|
||||
## Related
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
type: User Story
|
||||
title: "US-011: Configure and persist progression"
|
||||
description: Give operators validated configuration and durable, defensive storage for all base progression behavior.
|
||||
status: in-progress
|
||||
status: done
|
||||
---
|
||||
|
||||
# US-011: Configure and persist progression
|
||||
@@ -11,16 +11,16 @@ As a **server operator**, I want progression behavior to be configurable and dur
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [ ] Configuration supports all qualifying materials, progression thresholds, radii, vertical flight limits, warning-buffer distance, warm-ups, cooldowns, prices, particle settings, and notification durations.
|
||||
- [ ] Default values match the progression requirements documented by the related user stories.
|
||||
- [ ] Thresholds and levels are validated as nonnegative, representable, and sequentially coherent where required.
|
||||
- [ ] Radii, vertical ranges, durations, and prices reject unsafe or nonsensical values.
|
||||
- [ ] Invalid required configuration prevents partial plugin initialization and produces a clear server log message.
|
||||
- [ ] UUID-keyed state persists latest known names, base locations, counters, levels, relocation times, teleport times, visitor cooldowns, navigation preferences, flight preferences, boss-bar preferences, and visitor settings.
|
||||
- [ ] Cooldowns and relocation limits use real elapsed timestamps and continue while players are offline.
|
||||
- [ ] State is saved safely so a failed write does not replace valid persisted state with a partial document.
|
||||
- [ ] Corrupt, unknown, or invalid records are handled defensively and cannot silently grant progression or privileges.
|
||||
- [ ] Unknown forward-compatible configuration and state fields are preserved where practical.
|
||||
- [x] Configuration supports all qualifying materials, progression thresholds, radii, vertical flight limits, warning-buffer distance, warm-ups, cooldowns, prices, particle settings, and notification durations.
|
||||
- [x] Default values match the progression requirements documented by the related user stories.
|
||||
- [x] Thresholds and levels are validated as nonnegative, representable, and sequentially coherent where required.
|
||||
- [x] Radii, vertical ranges, durations, and prices reject unsafe or nonsensical values.
|
||||
- [x] Invalid required configuration prevents partial plugin initialization and produces a clear server log message.
|
||||
- [x] UUID-keyed state persists latest known names, base locations, counters, levels, relocation times, teleport times, visitor cooldowns, navigation preferences, flight preferences, boss-bar preferences, and visitor settings.
|
||||
- [x] Cooldowns and relocation limits use real elapsed timestamps and continue while players are offline.
|
||||
- [x] State is saved safely so a failed write does not replace valid persisted state with a partial document.
|
||||
- [x] Corrupt, unknown, or invalid records are handled defensively and cannot silently grant progression or privileges.
|
||||
- [x] Unknown forward-compatible configuration and state fields are preserved where practical.
|
||||
|
||||
## Related
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
type: User Story
|
||||
title: "US-012: Build and release the plugin"
|
||||
description: Give maintainers repeatable Spigot builds, automated verification, and versioned Gitea releases.
|
||||
status: in-progress
|
||||
status: done
|
||||
---
|
||||
|
||||
# US-012: Build and release the plugin
|
||||
@@ -11,15 +11,15 @@ As a **plugin maintainer**, I want automated builds and releases modeled on Trig
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [ ] The Gradle project compiles against Spigot API `26.2-R0.1-SNAPSHOT` using a Java 17 toolchain.
|
||||
- [ ] Compiler lint warnings fail the build.
|
||||
- [ ] Automated JUnit 5 tests run as part of the Gradle check lifecycle.
|
||||
- [ ] Pushes and pull requests build and test the plugin in Gitea Actions.
|
||||
- [ ] Pull requests validate conventional commit messages.
|
||||
- [ ] CI stores a development JAR as a workflow artifact.
|
||||
- [ ] Main-branch conventional commits drive semantic versioning.
|
||||
- [ ] A successful release builds a versioned JAR and attaches it to the corresponding Gitea release.
|
||||
- [ ] Build files, Gradle wrapper, workflows, and release behavior follow `../spigot-trigger-spawn/` where applicable while using Spigot Base names and identifiers.
|
||||
- [x] The Gradle project compiles against Spigot API `26.2-R0.1-SNAPSHOT` using a Java 17 toolchain.
|
||||
- [x] Compiler lint warnings fail the build.
|
||||
- [x] Automated JUnit 5 tests run as part of the Gradle check lifecycle.
|
||||
- [x] Pushes and pull requests build and test the plugin in Gitea Actions.
|
||||
- [x] Pull requests validate conventional commit messages.
|
||||
- [x] CI stores a development JAR as a workflow artifact.
|
||||
- [x] Main-branch conventional commits drive semantic versioning.
|
||||
- [x] A successful release builds a versioned JAR and attaches it to the corresponding Gitea release.
|
||||
- [x] Build files, Gradle wrapper, workflows, and release behavior follow `../spigot-trigger-spawn/` where applicable while using Spigot Base names and identifiers.
|
||||
|
||||
## Related
|
||||
|
||||
|
||||
Vendored
-248
@@ -1,248 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# gradlew start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh gradlew
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='-Dfile.encoding=UTF-8 "-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
Vendored
-82
@@ -1,82 +0,0 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem gradlew startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables, and ensure extensions are enabled
|
||||
setlocal EnableExtensions
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS=-Dfile.encoding=UTF-8 "-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
"%COMSPEC%" /c exit 1
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
"%COMSPEC%" /c exit 1
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
|
||||
|
||||
@rem Execute gradlew
|
||||
@rem endlocal doesn't take effect until after the line is parsed and variables are expanded
|
||||
@rem which allows us to clear the local environment before executing the java command
|
||||
endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel
|
||||
|
||||
:exitWithErrorLevel
|
||||
@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
|
||||
"%COMSPEC%" /c exit %ERRORLEVEL%
|
||||
@@ -1,6 +1,16 @@
|
||||
package games.dmg.spigotbase;
|
||||
|
||||
public final class AdminProgressionService {
|
||||
private final PluginSettingsProvider settings;
|
||||
|
||||
public AdminProgressionService() {
|
||||
this(new PluginSettingsProvider(PluginSettings.from(java.util.Map.of())));
|
||||
}
|
||||
|
||||
public AdminProgressionService(PluginSettingsProvider settings) {
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
public PlayerState setLevel(PlayerState player, ProgressionPath path, int level) {
|
||||
int base = player.baseLevel();
|
||||
int size = player.sizeLevel();
|
||||
@@ -73,6 +83,118 @@ public final class AdminProgressionService {
|
||||
);
|
||||
}
|
||||
|
||||
public PlayerState setProgress(PlayerState player, ProgressCounter counter, long amount) {
|
||||
if (amount < 0) {
|
||||
throw new IllegalArgumentException("progress amount must not be negative");
|
||||
}
|
||||
long grassDirt = player.grassAndDirtBroken();
|
||||
long stone = player.stoneBroken();
|
||||
long deepslate = player.deepslateBroken();
|
||||
long obsidian = player.obsidianBroken();
|
||||
long placements = player.blocksPlacedInBase();
|
||||
long baseBreaks = player.blocksBrokenInBase();
|
||||
switch (counter) {
|
||||
case GRASS_DIRT -> grassDirt = amount;
|
||||
case STONE -> stone = amount;
|
||||
case DEEPSLATE -> deepslate = amount;
|
||||
case OBSIDIAN -> obsidian = amount;
|
||||
case PLACEMENTS -> placements = amount;
|
||||
case BASE_BREAKS -> baseBreaks = amount;
|
||||
}
|
||||
PluginSettings configured = settings.current();
|
||||
int base = player.baseLevel();
|
||||
if (grassDirt >= configured.navigationUnlockBlocks()) {
|
||||
base = Math.max(base, 2);
|
||||
} else if (grassDirt >= configured.baseUnlockBlocks()) {
|
||||
base = Math.max(base, 1);
|
||||
}
|
||||
int size = player.sizeLevel();
|
||||
if (stone >= configured.stoneExpansionBlocks()) {
|
||||
base = Math.max(base, 1);
|
||||
size = Math.max(size, 1);
|
||||
}
|
||||
if (deepslate >= configured.deepslateExpansionBlocks()) {
|
||||
base = Math.max(base, 1);
|
||||
size = Math.max(size, 2);
|
||||
}
|
||||
if (obsidian >= configured.obsidianExpansionBlocks()) {
|
||||
base = Math.max(base, 1);
|
||||
size = Math.max(size, 3);
|
||||
}
|
||||
int warmup = player.warmupLevel();
|
||||
if (placements >= configured.teleportUnlockPlacements()) {
|
||||
base = Math.max(base, 3);
|
||||
}
|
||||
if (placements >= configured.instantWarmupPlacements()) {
|
||||
warmup = Math.max(warmup, 3);
|
||||
} else if (placements >= configured.thirdWarmupPlacements()) {
|
||||
warmup = Math.max(warmup, 2);
|
||||
} else if (placements >= configured.secondWarmupPlacements()) {
|
||||
warmup = Math.max(warmup, 1);
|
||||
}
|
||||
int cooldown = player.cooldownLevel();
|
||||
if (baseBreaks >= configured.firstCooldownBreaks()) {
|
||||
base = Math.max(base, 3);
|
||||
}
|
||||
if (baseBreaks >= configured.instantCooldownBreaks()) {
|
||||
cooldown = Math.max(cooldown, 4);
|
||||
} else if (baseBreaks >= configured.thirdCooldownBreaks()) {
|
||||
cooldown = Math.max(cooldown, 3);
|
||||
} else if (baseBreaks >= configured.secondCooldownBreaks()) {
|
||||
cooldown = Math.max(cooldown, 2);
|
||||
} else if (baseBreaks >= configured.firstCooldownBreaks()) {
|
||||
cooldown = Math.max(cooldown, 1);
|
||||
}
|
||||
PlayerState updated = player.withProgressCounters(
|
||||
grassDirt, stone, deepslate, obsidian, placements, baseBreaks
|
||||
);
|
||||
return updated.withAdministrativeLevels(
|
||||
base,
|
||||
size,
|
||||
updated.flightLevel(),
|
||||
warmup,
|
||||
cooldown,
|
||||
updated.navigationEnabled(),
|
||||
updated.flightEnabled(),
|
||||
updated.visitorsEnabled()
|
||||
);
|
||||
}
|
||||
|
||||
public PlayerState resetPath(PlayerState player, ProgressionPath path) {
|
||||
if (path == ProgressionPath.BASE) {
|
||||
return PlayerState.newPlayer(player.playerId(), player.latestName());
|
||||
}
|
||||
PlayerState cleared = switch (path) {
|
||||
case SIZE -> player.withProgressCounters(
|
||||
player.grassAndDirtBroken(),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
player.blocksPlacedInBase(),
|
||||
player.blocksBrokenInBase()
|
||||
);
|
||||
case WARMUP -> player.withProgressCounters(
|
||||
player.grassAndDirtBroken(),
|
||||
player.stoneBroken(),
|
||||
player.deepslateBroken(),
|
||||
player.obsidianBroken(),
|
||||
player.baseLevel() >= 3 ? settings.current().teleportUnlockPlacements() : 0,
|
||||
player.blocksBrokenInBase()
|
||||
);
|
||||
case COOLDOWN -> player.withProgressCounters(
|
||||
player.grassAndDirtBroken(),
|
||||
player.stoneBroken(),
|
||||
player.deepslateBroken(),
|
||||
player.obsidianBroken(),
|
||||
player.blocksPlacedInBase(),
|
||||
0
|
||||
);
|
||||
case FLIGHT -> player;
|
||||
case BASE -> throw new IllegalStateException("base reset already handled");
|
||||
};
|
||||
return setLevel(cleared, path, 0);
|
||||
}
|
||||
|
||||
private static void requireRange(int value, int minimum, int maximum, String path) {
|
||||
if (value < minimum || value > maximum) {
|
||||
throw new IllegalArgumentException(
|
||||
|
||||
@@ -9,14 +9,24 @@ import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandExecutor;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
final class BaseAdminCommand implements CommandExecutor {
|
||||
private final JavaPlugin plugin;
|
||||
private final BaseStateManager stateManager;
|
||||
private final AdminProgressionService progressionService;
|
||||
private final PluginSettingsProvider settingsProvider;
|
||||
|
||||
BaseAdminCommand(BaseStateManager stateManager, AdminProgressionService progressionService) {
|
||||
BaseAdminCommand(
|
||||
JavaPlugin plugin,
|
||||
BaseStateManager stateManager,
|
||||
AdminProgressionService progressionService,
|
||||
PluginSettingsProvider settingsProvider
|
||||
) {
|
||||
this.plugin = plugin;
|
||||
this.stateManager = stateManager;
|
||||
this.progressionService = progressionService;
|
||||
this.settingsProvider = settingsProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -25,6 +35,9 @@ final class BaseAdminCommand implements CommandExecutor {
|
||||
sender.sendMessage(ChatColor.RED + "You do not have permission to administer Spigot Base.");
|
||||
return true;
|
||||
}
|
||||
if (arguments.length > 0 && arguments[0].equalsIgnoreCase("config")) {
|
||||
return updateConfig(sender, arguments);
|
||||
}
|
||||
if (arguments.length < 2) {
|
||||
sendUsage(sender);
|
||||
return true;
|
||||
@@ -37,6 +50,7 @@ final class BaseAdminCommand implements CommandExecutor {
|
||||
return switch (arguments[0].toLowerCase(Locale.ROOT)) {
|
||||
case "progress" -> showProgress(sender, target.orElseThrow());
|
||||
case "setlevel" -> setLevel(sender, target.orElseThrow(), arguments);
|
||||
case "setprogress" -> setProgress(sender, target.orElseThrow(), arguments);
|
||||
case "clearcooldown" -> clearCooldown(sender, target.orElseThrow(), arguments);
|
||||
case "reset" -> reset(sender, target.orElseThrow(), arguments);
|
||||
default -> {
|
||||
@@ -92,6 +106,37 @@ final class BaseAdminCommand implements CommandExecutor {
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean setProgress(
|
||||
CommandSender sender,
|
||||
PlayerState target,
|
||||
String[] arguments
|
||||
) {
|
||||
if (arguments.length != 4) {
|
||||
sender.sendMessage(ChatColor.RED
|
||||
+ "Usage: /baseadmin setprogress <player> <counter> <amount>");
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
ProgressCounter counter = ProgressCounter.valueOf(
|
||||
arguments[2].toUpperCase(Locale.ROOT).replace('-', '_')
|
||||
);
|
||||
long amount = Long.parseLong(arguments[3]);
|
||||
PlayerState updated = stateManager.update(
|
||||
target.playerId(),
|
||||
target.latestName(),
|
||||
current -> progressionService.setProgress(current, counter, amount)
|
||||
);
|
||||
stateManager.saveIfDirty();
|
||||
sender.sendMessage(ChatColor.GREEN + "Set " + updated.latestName() + "'s "
|
||||
+ counter.name().toLowerCase(Locale.ROOT) + " progress to " + amount + ".");
|
||||
} catch (NumberFormatException exception) {
|
||||
sender.sendMessage(ChatColor.RED + "The progress amount must be an integer.");
|
||||
} catch (IllegalArgumentException exception) {
|
||||
sender.sendMessage(ChatColor.RED + exception.getMessage());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean clearCooldown(
|
||||
CommandSender sender,
|
||||
PlayerState target,
|
||||
@@ -146,7 +191,7 @@ final class BaseAdminCommand implements CommandExecutor {
|
||||
stateManager.update(
|
||||
target.playerId(),
|
||||
target.latestName(),
|
||||
current -> progressionService.setLevel(current, path, 0)
|
||||
current -> progressionService.resetPath(current, path)
|
||||
);
|
||||
stateManager.saveIfDirty();
|
||||
sender.sendMessage(ChatColor.GREEN + "Reset " + path.name().toLowerCase(Locale.ROOT)
|
||||
@@ -157,6 +202,43 @@ final class BaseAdminCommand implements CommandExecutor {
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean updateConfig(CommandSender sender, String[] arguments) {
|
||||
if (arguments.length != 3) {
|
||||
sender.sendMessage(ChatColor.RED + "Usage: /baseadmin config <key> <integer>");
|
||||
return true;
|
||||
}
|
||||
String key = arguments[1];
|
||||
Object previous = plugin.getConfig().get(key);
|
||||
if (!(previous instanceof Number)) {
|
||||
sender.sendMessage(ChatColor.RED + "Unknown numeric setting: " + key);
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
long value = Long.parseLong(arguments[2]);
|
||||
java.util.Map<String, Object> proposed =
|
||||
new java.util.HashMap<>(plugin.getConfig().getValues(false));
|
||||
proposed.put(key, value);
|
||||
PluginSettings updated = PluginSettingsValidator.validateMaterials(
|
||||
PluginSettings.from(proposed)
|
||||
);
|
||||
plugin.getConfig().set(key, value);
|
||||
try {
|
||||
plugin.saveConfig();
|
||||
} catch (RuntimeException exception) {
|
||||
plugin.getConfig().set(key, previous);
|
||||
throw exception;
|
||||
}
|
||||
settingsProvider.update(updated);
|
||||
sender.sendMessage(ChatColor.GREEN + "Updated " + key + " to " + value
|
||||
+ "; the change is active immediately.");
|
||||
} catch (NumberFormatException exception) {
|
||||
sender.sendMessage(ChatColor.RED + "The setting value must be an integer.");
|
||||
} catch (IllegalArgumentException exception) {
|
||||
sender.sendMessage(ChatColor.RED + exception.getMessage());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private Optional<PlayerState> resolve(String name) {
|
||||
Player online = Bukkit.getPlayerExact(name);
|
||||
if (online != null) {
|
||||
@@ -167,6 +249,7 @@ final class BaseAdminCommand implements CommandExecutor {
|
||||
|
||||
private static void sendUsage(CommandSender sender) {
|
||||
sender.sendMessage(ChatColor.YELLOW + "Usage: /baseadmin "
|
||||
+ "<progress|setlevel|clearcooldown|reset> <player> ...");
|
||||
+ "<progress|setlevel|setprogress|clearcooldown|reset> <player> ...");
|
||||
sender.sendMessage(ChatColor.YELLOW + " /baseadmin config <key> <integer>");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +1,30 @@
|
||||
package games.dmg.spigotbase;
|
||||
|
||||
public final class BaseBoundsService {
|
||||
private final PluginSettings settings;
|
||||
private final PluginSettingsProvider settings;
|
||||
|
||||
public BaseBoundsService(PluginSettings settings) {
|
||||
this(new PluginSettingsProvider(settings));
|
||||
}
|
||||
|
||||
public BaseBoundsService(PluginSettingsProvider settings) {
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
public int radius(PlayerState player) {
|
||||
return switch (player.sizeLevel()) {
|
||||
case 0 -> settings.initialRadius();
|
||||
case 1 -> settings.firstExpandedRadius();
|
||||
case 2 -> settings.secondExpandedRadius();
|
||||
case 3 -> settings.thirdExpandedRadius();
|
||||
case 0 -> settings.current().initialRadius();
|
||||
case 1 -> settings.current().firstExpandedRadius();
|
||||
case 2 -> settings.current().secondExpandedRadius();
|
||||
case 3 -> settings.current().thirdExpandedRadius();
|
||||
default -> throw new IllegalArgumentException("unknown size level");
|
||||
};
|
||||
}
|
||||
|
||||
public int verticalRange(PlayerState player) {
|
||||
return switch (player.flightLevel()) {
|
||||
case 0, 1 -> settings.initialVerticalRange();
|
||||
case 2 -> settings.secondFlightVerticalRange();
|
||||
case 0, 1 -> settings.current().initialVerticalRange();
|
||||
case 2 -> settings.current().secondFlightVerticalRange();
|
||||
case 3 -> Integer.MAX_VALUE;
|
||||
default -> throw new IllegalArgumentException("unknown flight level");
|
||||
};
|
||||
|
||||
@@ -14,13 +14,13 @@ final class BaseCommand implements CommandExecutor {
|
||||
private final BaseTeleportManager teleportManager;
|
||||
private final BaseStateManager stateManager;
|
||||
private final VisitorPolicy visitorPolicy;
|
||||
private final PluginSettings settings;
|
||||
private final PluginSettingsProvider settings;
|
||||
|
||||
BaseCommand(
|
||||
BaseTeleportManager teleportManager,
|
||||
BaseStateManager stateManager,
|
||||
VisitorPolicy visitorPolicy,
|
||||
PluginSettings settings
|
||||
PluginSettingsProvider settings
|
||||
) {
|
||||
this.teleportManager = teleportManager;
|
||||
this.stateManager = stateManager;
|
||||
@@ -54,14 +54,16 @@ final class BaseCommand implements CommandExecutor {
|
||||
: "You must unlock Base III before purchasing Base IV."));
|
||||
return;
|
||||
}
|
||||
int price = settings.visitorUnlockDiamondCost();
|
||||
int price = settings.current().visitorUnlockDiamondCost();
|
||||
Material currency = Material.valueOf(settings.current().visitorCurrencyMaterial());
|
||||
PlayerInventory inventory = player.getInventory();
|
||||
if (countDiamonds(inventory) < price) {
|
||||
player.sendMessage(ChatColor.RED + "Base IV costs " + price + " diamonds.");
|
||||
if (countCurrency(inventory, currency) < price) {
|
||||
player.sendMessage(ChatColor.RED + "Base IV costs " + price + " "
|
||||
+ currency.name().toLowerCase(java.util.Locale.ROOT) + ".");
|
||||
return;
|
||||
}
|
||||
ItemStack[] snapshot = cloneContents(inventory.getStorageContents());
|
||||
removeDiamonds(inventory, price);
|
||||
removeCurrency(inventory, currency, price);
|
||||
try {
|
||||
stateManager.updateAndSave(
|
||||
player.getUniqueId(),
|
||||
@@ -76,27 +78,34 @@ final class BaseCommand implements CommandExecutor {
|
||||
player.sendTitle(
|
||||
ChatColor.GOLD + "Base IV Unlocked",
|
||||
ChatColor.YELLOW + "Visitors may now teleport to your base",
|
||||
10, 70, 20
|
||||
settings.current().titleFadeInTicks(),
|
||||
settings.current().titleStayTicks(),
|
||||
settings.current().titleFadeOutTicks()
|
||||
);
|
||||
player.sendMessage(ChatColor.GREEN + "Base IV unlocked for " + price + " diamonds.");
|
||||
player.sendMessage(ChatColor.GREEN + "Base IV unlocked for " + price + " "
|
||||
+ currency.name().toLowerCase(java.util.Locale.ROOT) + ".");
|
||||
}
|
||||
|
||||
private static int countDiamonds(PlayerInventory inventory) {
|
||||
private static int countCurrency(PlayerInventory inventory, Material currency) {
|
||||
int count = 0;
|
||||
for (ItemStack item : inventory.getStorageContents()) {
|
||||
if (item != null && item.getType() == Material.DIAMOND) {
|
||||
if (item != null && item.getType() == currency) {
|
||||
count += item.getAmount();
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
private static void removeDiamonds(PlayerInventory inventory, int amount) {
|
||||
private static void removeCurrency(
|
||||
PlayerInventory inventory,
|
||||
Material currency,
|
||||
int amount
|
||||
) {
|
||||
ItemStack[] contents = inventory.getStorageContents();
|
||||
int remaining = amount;
|
||||
for (int index = 0; index < contents.length && remaining > 0; index++) {
|
||||
ItemStack item = contents[index];
|
||||
if (item == null || item.getType() != Material.DIAMOND) {
|
||||
if (item == null || item.getType() != currency) {
|
||||
continue;
|
||||
}
|
||||
int removed = Math.min(remaining, item.getAmount());
|
||||
|
||||
@@ -15,7 +15,7 @@ final class BaseFlightController implements Runnable {
|
||||
private final BaseStateManager stateManager;
|
||||
private final SecondaryProgressionService progressionService;
|
||||
private final BaseBoundsService boundsService;
|
||||
private final PluginSettings settings;
|
||||
private final PluginSettingsProvider settings;
|
||||
private final Set<UUID> grantedFlight = new HashSet<>();
|
||||
private final Set<UUID> warned = new HashSet<>();
|
||||
|
||||
@@ -24,7 +24,7 @@ final class BaseFlightController implements Runnable {
|
||||
BaseStateManager stateManager,
|
||||
SecondaryProgressionService progressionService,
|
||||
BaseBoundsService boundsService,
|
||||
PluginSettings settings
|
||||
PluginSettingsProvider settings
|
||||
) {
|
||||
this.server = server;
|
||||
this.stateManager = stateManager;
|
||||
@@ -81,7 +81,9 @@ final class BaseFlightController implements Runnable {
|
||||
player.sendTitle(
|
||||
ChatColor.GOLD + "Base Flight " + roman(updated.flightLevel()) + " Unlocked",
|
||||
ChatColor.YELLOW + "Use /baseflight to toggle flight",
|
||||
10, 70, 20
|
||||
settings.current().titleFadeInTicks(),
|
||||
settings.current().titleStayTicks(),
|
||||
settings.current().titleFadeOutTicks()
|
||||
);
|
||||
return updated;
|
||||
}
|
||||
@@ -101,7 +103,7 @@ final class BaseFlightController implements Runnable {
|
||||
double deltaZ = player.getLocation().getZ() - (base.z() + 0.5);
|
||||
double distanceSquared = deltaX * deltaX + deltaZ * deltaZ;
|
||||
int radius = boundsService.radius(state);
|
||||
int bufferedRadius = radius + settings.flightWarningBuffer();
|
||||
int bufferedRadius = radius + settings.current().flightWarningBuffer();
|
||||
if (distanceSquared > (double) bufferedRadius * bufferedRadius) {
|
||||
removeGrantedFlight(player);
|
||||
return;
|
||||
@@ -115,7 +117,9 @@ final class BaseFlightController implements Runnable {
|
||||
player.sendTitle(
|
||||
ChatColor.RED + "Leaving Your Base",
|
||||
ChatColor.YELLOW + "Turn back before base flight ends",
|
||||
0, 30, 10
|
||||
0,
|
||||
Math.min(30, settings.current().titleStayTicks()),
|
||||
settings.current().titleFadeOutTicks()
|
||||
);
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -9,10 +9,16 @@ import org.bukkit.util.Vector;
|
||||
final class BaseNavigationController implements Runnable {
|
||||
private final Server server;
|
||||
private final BaseStateManager stateManager;
|
||||
private final PluginSettingsProvider settings;
|
||||
|
||||
BaseNavigationController(Server server, BaseStateManager stateManager) {
|
||||
BaseNavigationController(
|
||||
Server server,
|
||||
BaseStateManager stateManager,
|
||||
PluginSettingsProvider settings
|
||||
) {
|
||||
this.server = server;
|
||||
this.stateManager = stateManager;
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -36,7 +42,7 @@ final class BaseNavigationController implements Runnable {
|
||||
continue;
|
||||
}
|
||||
direction.normalize();
|
||||
for (int step = 1; step <= 5; step++) {
|
||||
for (int step = 1; step <= settings.current().navigationParticleCount(); step++) {
|
||||
Location particle = origin.clone().add(direction.clone().multiply(step));
|
||||
player.spawnParticle(Particle.END_ROD, particle, 1, 0.0, 0.0, 0.0, 0.0);
|
||||
}
|
||||
|
||||
@@ -8,10 +8,10 @@ import org.bukkit.entity.Player;
|
||||
|
||||
final class BaseProgressCommand implements CommandExecutor {
|
||||
private final BaseStateManager stateManager;
|
||||
private final PluginSettings settings;
|
||||
private final PluginSettingsProvider settings;
|
||||
private final TeleportPolicy teleportPolicy;
|
||||
|
||||
BaseProgressCommand(BaseStateManager stateManager, PluginSettings settings) {
|
||||
BaseProgressCommand(BaseStateManager stateManager, PluginSettingsProvider settings) {
|
||||
this.stateManager = stateManager;
|
||||
this.settings = settings;
|
||||
this.teleportPolicy = new TeleportPolicy(settings);
|
||||
@@ -53,13 +53,13 @@ final class BaseProgressCommand implements CommandExecutor {
|
||||
|
||||
private void showBasePath(Player player, PlayerState state) {
|
||||
String detail = switch (state.baseLevel()) {
|
||||
case 0 -> state.grassAndDirtBroken() + "/" + settings.baseUnlockBlocks()
|
||||
case 0 -> state.grassAndDirtBroken() + "/" + settings.current().baseUnlockBlocks()
|
||||
+ " grass or dirt → /setbase";
|
||||
case 1 -> state.grassAndDirtBroken() + "/" + settings.navigationUnlockBlocks()
|
||||
case 1 -> state.grassAndDirtBroken() + "/" + settings.current().navigationUnlockBlocks()
|
||||
+ " grass or dirt → navigation";
|
||||
case 2 -> state.blocksPlacedInBase() + "/" + settings.teleportUnlockPlacements()
|
||||
case 2 -> state.blocksPlacedInBase() + "/" + settings.current().teleportUnlockPlacements()
|
||||
+ " placements → /base";
|
||||
case 3 -> settings.visitorUnlockDiamondCost() + " diamonds → visitor access";
|
||||
case 3 -> settings.current().visitorUnlockDiamondCost() + " diamonds → visitor access";
|
||||
case 4 -> "complete; visitor access unlocked";
|
||||
default -> "invalid";
|
||||
};
|
||||
@@ -69,9 +69,9 @@ final class BaseProgressCommand implements CommandExecutor {
|
||||
|
||||
private void showSizePath(Player player, PlayerState state) {
|
||||
String detail = switch (state.sizeLevel()) {
|
||||
case 0 -> state.stoneBroken() + "/" + settings.stoneExpansionBlocks() + " stone";
|
||||
case 1 -> state.deepslateBroken() + "/" + settings.deepslateExpansionBlocks() + " deepslate";
|
||||
case 2 -> state.obsidianBroken() + "/" + settings.obsidianExpansionBlocks() + " obsidian";
|
||||
case 0 -> state.stoneBroken() + "/" + settings.current().stoneExpansionBlocks() + " stone";
|
||||
case 1 -> state.deepslateBroken() + "/" + settings.current().deepslateExpansionBlocks() + " deepslate";
|
||||
case 2 -> state.obsidianBroken() + "/" + settings.current().obsidianExpansionBlocks() + " obsidian";
|
||||
case 3 -> "complete; 150-block radius by default";
|
||||
default -> "invalid";
|
||||
};
|
||||
@@ -90,9 +90,9 @@ final class BaseProgressCommand implements CommandExecutor {
|
||||
|
||||
private void showWarmupPath(Player player, PlayerState state) {
|
||||
String detail = switch (state.warmupLevel()) {
|
||||
case 0 -> state.blocksPlacedInBase() + "/" + settings.secondWarmupPlacements();
|
||||
case 1 -> state.blocksPlacedInBase() + "/" + settings.thirdWarmupPlacements();
|
||||
case 2 -> state.blocksPlacedInBase() + "/" + settings.instantWarmupPlacements();
|
||||
case 0 -> state.blocksPlacedInBase() + "/" + settings.current().secondWarmupPlacements();
|
||||
case 1 -> state.blocksPlacedInBase() + "/" + settings.current().thirdWarmupPlacements();
|
||||
case 2 -> state.blocksPlacedInBase() + "/" + settings.current().instantWarmupPlacements();
|
||||
case 3 -> "complete";
|
||||
default -> "invalid";
|
||||
};
|
||||
@@ -103,10 +103,10 @@ final class BaseProgressCommand implements CommandExecutor {
|
||||
|
||||
private void showCooldownPath(Player player, PlayerState state) {
|
||||
String detail = switch (state.cooldownLevel()) {
|
||||
case 0 -> state.blocksBrokenInBase() + "/" + settings.firstCooldownBreaks();
|
||||
case 1 -> state.blocksBrokenInBase() + "/" + settings.secondCooldownBreaks();
|
||||
case 2 -> state.blocksBrokenInBase() + "/" + settings.thirdCooldownBreaks();
|
||||
case 3 -> state.blocksBrokenInBase() + "/" + settings.instantCooldownBreaks();
|
||||
case 0 -> state.blocksBrokenInBase() + "/" + settings.current().firstCooldownBreaks();
|
||||
case 1 -> state.blocksBrokenInBase() + "/" + settings.current().secondCooldownBreaks();
|
||||
case 2 -> state.blocksBrokenInBase() + "/" + settings.current().thirdCooldownBreaks();
|
||||
case 3 -> state.blocksBrokenInBase() + "/" + settings.current().instantCooldownBreaks();
|
||||
case 4 -> "complete";
|
||||
default -> "invalid";
|
||||
};
|
||||
|
||||
@@ -19,15 +19,13 @@ import org.bukkit.event.block.BlockPlaceEvent;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
|
||||
final class BaseProgressListener implements Listener {
|
||||
private static final long BOSS_BAR_TICKS = 60L;
|
||||
|
||||
private final Plugin plugin;
|
||||
private final BaseStateManager stateManager;
|
||||
private final BaseProgressionService baseProgressionService;
|
||||
private final SecondaryProgressionService secondaryProgressionService;
|
||||
private final TeleportProgressionService teleportProgressionService;
|
||||
private final BaseBoundsService boundsService;
|
||||
private final PluginSettings settings;
|
||||
private final PluginSettingsProvider settings;
|
||||
private final Map<UUID, BossBar> activeBossBars = new HashMap<>();
|
||||
|
||||
BaseProgressListener(
|
||||
@@ -37,7 +35,7 @@ final class BaseProgressListener implements Listener {
|
||||
SecondaryProgressionService secondaryProgressionService,
|
||||
TeleportProgressionService teleportProgressionService,
|
||||
BaseBoundsService boundsService,
|
||||
PluginSettings settings
|
||||
PluginSettingsProvider settings
|
||||
) {
|
||||
this.plugin = plugin;
|
||||
this.stateManager = stateManager;
|
||||
@@ -63,13 +61,15 @@ final class BaseProgressListener implements Listener {
|
||||
event.getBlock().getY(),
|
||||
event.getBlock().getZ()
|
||||
);
|
||||
if (!isProgressMaterial(material) && !(insideBase && before.baseLevel() >= 3)) {
|
||||
boolean cooldownEligible = insideBase && before.baseLevel() >= 3
|
||||
&& !settings.current().cooldownExcludedMaterials().contains(material.name());
|
||||
if (!isProgressMaterial(material) && !cooldownEligible) {
|
||||
return;
|
||||
}
|
||||
ProgressionUpdate[] updateHolder = new ProgressionUpdate[1];
|
||||
PlayerState state = stateManager.update(player.getUniqueId(), player.getName(), current -> {
|
||||
ProgressionUpdate materialUpdate = updateForMaterial(current, material);
|
||||
ProgressionUpdate breakUpdate = insideBase
|
||||
ProgressionUpdate breakUpdate = cooldownEligible
|
||||
? teleportProgressionService.recordBreak(materialUpdate.player())
|
||||
: ProgressionUpdate.unchanged(materialUpdate.player());
|
||||
ProgressionUpdate combined = combine(materialUpdate, breakUpdate);
|
||||
@@ -82,7 +82,7 @@ final class BaseProgressListener implements Listener {
|
||||
stateManager.saveIfDirty();
|
||||
}
|
||||
if (state.bossBarEnabled()) {
|
||||
if (insideBase && before.baseLevel() >= 3) {
|
||||
if (cooldownEligible) {
|
||||
showProgress(player, cooldownDisplay(state));
|
||||
} else {
|
||||
showProgress(player, progressDisplay(state, material));
|
||||
@@ -127,13 +127,21 @@ final class BaseProgressListener implements Listener {
|
||||
}
|
||||
|
||||
private ProgressionUpdate updateForMaterial(PlayerState player, Material material) {
|
||||
return switch (material) {
|
||||
case GRASS_BLOCK, DIRT -> baseProgressionService.recordGrassOrDirtBreak(player);
|
||||
case STONE -> secondaryProgressionService.recordStoneBreak(player);
|
||||
case DEEPSLATE -> secondaryProgressionService.recordDeepslateBreak(player);
|
||||
case OBSIDIAN -> secondaryProgressionService.recordObsidianBreak(player);
|
||||
default -> ProgressionUpdate.unchanged(player);
|
||||
};
|
||||
String name = material.name();
|
||||
PluginSettings configured = settings.current();
|
||||
if (configured.baseUnlockMaterials().contains(name)) {
|
||||
return baseProgressionService.recordGrassOrDirtBreak(player);
|
||||
}
|
||||
if (configured.stoneExpansionMaterial().equals(name)) {
|
||||
return secondaryProgressionService.recordStoneBreak(player);
|
||||
}
|
||||
if (configured.deepslateExpansionMaterial().equals(name)) {
|
||||
return secondaryProgressionService.recordDeepslateBreak(player);
|
||||
}
|
||||
if (configured.obsidianExpansionMaterial().equals(name)) {
|
||||
return secondaryProgressionService.recordObsidianBreak(player);
|
||||
}
|
||||
return ProgressionUpdate.unchanged(player);
|
||||
}
|
||||
|
||||
private void announceUnlock(Player player, ProgressionUpdate update) {
|
||||
@@ -146,37 +154,48 @@ final class BaseProgressListener implements Listener {
|
||||
case 4 -> "Visitors can now travel to your base";
|
||||
default -> "A new base benefit is available";
|
||||
};
|
||||
player.sendTitle(
|
||||
sendUnlockTitle(
|
||||
player,
|
||||
ChatColor.GOLD + "Base " + roman(level) + " Unlocked",
|
||||
ChatColor.YELLOW + subtitle,
|
||||
10, 70, 20
|
||||
ChatColor.YELLOW + subtitle
|
||||
);
|
||||
player.sendMessage(ChatColor.GREEN + "You unlocked Base " + roman(level) + "! " + subtitle);
|
||||
}
|
||||
if (update.unlockedSizeLevel()) {
|
||||
int radius = boundsService.radius(update.player());
|
||||
player.sendTitle(
|
||||
sendUnlockTitle(
|
||||
player,
|
||||
ChatColor.GOLD + "Base Size Upgraded",
|
||||
ChatColor.YELLOW + "Your base radius is now " + radius + " blocks",
|
||||
10, 70, 20
|
||||
ChatColor.YELLOW + "Your base radius is now " + radius + " blocks"
|
||||
);
|
||||
}
|
||||
if (update.unlockedWarmupLevel()) {
|
||||
player.sendTitle(
|
||||
sendUnlockTitle(
|
||||
player,
|
||||
ChatColor.GOLD + "Teleport Warm-up Improved",
|
||||
ChatColor.YELLOW + "Your /base warm-up is now shorter",
|
||||
10, 70, 20
|
||||
ChatColor.YELLOW + "Your /base warm-up is now shorter"
|
||||
);
|
||||
}
|
||||
if (update.unlockedCooldownLevel()) {
|
||||
player.sendTitle(
|
||||
sendUnlockTitle(
|
||||
player,
|
||||
ChatColor.GOLD + "Teleport Cooldown Improved",
|
||||
ChatColor.YELLOW + "You can use /base more often",
|
||||
10, 70, 20
|
||||
ChatColor.YELLOW + "You can use /base more often"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendUnlockTitle(Player player, String title, String subtitle) {
|
||||
PluginSettings configured = settings.current();
|
||||
player.sendTitle(
|
||||
title,
|
||||
subtitle,
|
||||
configured.titleFadeInTicks(),
|
||||
configured.titleStayTicks(),
|
||||
configured.titleFadeOutTicks()
|
||||
);
|
||||
}
|
||||
|
||||
private void showProgress(Player player, ProgressDisplay display) {
|
||||
if (display == null) {
|
||||
removeBossBar(player.getUniqueId());
|
||||
@@ -195,46 +214,54 @@ final class BaseProgressListener implements Listener {
|
||||
if (activeBossBars.remove(player.getUniqueId(), bossBar)) {
|
||||
bossBar.removeAll();
|
||||
}
|
||||
}, BOSS_BAR_TICKS);
|
||||
}, settings.current().bossBarDurationTicks());
|
||||
}
|
||||
|
||||
private ProgressDisplay progressDisplay(PlayerState state, Material material) {
|
||||
return switch (material) {
|
||||
case GRASS_BLOCK, DIRT -> state.baseLevel() < 2
|
||||
? new ProgressDisplay(
|
||||
state.baseLevel() == 0 ? "Base I" : "Base II",
|
||||
state.grassAndDirtBroken(),
|
||||
state.baseLevel() == 0 ? settings.baseUnlockBlocks() : settings.navigationUnlockBlocks()
|
||||
)
|
||||
: null;
|
||||
case STONE -> state.sizeLevel() == 0
|
||||
? new ProgressDisplay("Base Size II", state.stoneBroken(), settings.stoneExpansionBlocks())
|
||||
: null;
|
||||
case DEEPSLATE -> state.sizeLevel() == 1
|
||||
? new ProgressDisplay("Base Size III", state.deepslateBroken(), settings.deepslateExpansionBlocks())
|
||||
: null;
|
||||
case OBSIDIAN -> state.sizeLevel() == 2
|
||||
? new ProgressDisplay("Base Size IV", state.obsidianBroken(), settings.obsidianExpansionBlocks())
|
||||
: null;
|
||||
default -> null;
|
||||
};
|
||||
String name = material.name();
|
||||
PluginSettings configured = settings.current();
|
||||
if (configured.baseUnlockMaterials().contains(name) && state.baseLevel() < 2) {
|
||||
return new ProgressDisplay(
|
||||
state.baseLevel() == 0 ? "Base I" : "Base II",
|
||||
state.grassAndDirtBroken(),
|
||||
state.baseLevel() == 0
|
||||
? configured.baseUnlockBlocks()
|
||||
: configured.navigationUnlockBlocks()
|
||||
);
|
||||
}
|
||||
if (configured.stoneExpansionMaterial().equals(name) && state.sizeLevel() == 0) {
|
||||
return new ProgressDisplay(
|
||||
"Base Size II", state.stoneBroken(), configured.stoneExpansionBlocks()
|
||||
);
|
||||
}
|
||||
if (configured.deepslateExpansionMaterial().equals(name) && state.sizeLevel() == 1) {
|
||||
return new ProgressDisplay(
|
||||
"Base Size III", state.deepslateBroken(), configured.deepslateExpansionBlocks()
|
||||
);
|
||||
}
|
||||
if (configured.obsidianExpansionMaterial().equals(name) && state.sizeLevel() == 2) {
|
||||
return new ProgressDisplay(
|
||||
"Base Size IV", state.obsidianBroken(), configured.obsidianExpansionBlocks()
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private ProgressDisplay warmupDisplay(PlayerState state) {
|
||||
if (state.baseLevel() == 2) {
|
||||
return new ProgressDisplay(
|
||||
"Base III", state.blocksPlacedInBase(), settings.teleportUnlockPlacements()
|
||||
"Base III", state.blocksPlacedInBase(), settings.current().teleportUnlockPlacements()
|
||||
);
|
||||
}
|
||||
return switch (state.warmupLevel()) {
|
||||
case 0 -> new ProgressDisplay(
|
||||
"15s Warm-up", state.blocksPlacedInBase(), settings.secondWarmupPlacements()
|
||||
"15s Warm-up", state.blocksPlacedInBase(), settings.current().secondWarmupPlacements()
|
||||
);
|
||||
case 1 -> new ProgressDisplay(
|
||||
"5s Warm-up", state.blocksPlacedInBase(), settings.thirdWarmupPlacements()
|
||||
"5s Warm-up", state.blocksPlacedInBase(), settings.current().thirdWarmupPlacements()
|
||||
);
|
||||
case 2 -> new ProgressDisplay(
|
||||
"Instant Warm-up", state.blocksPlacedInBase(), settings.instantWarmupPlacements()
|
||||
"Instant Warm-up", state.blocksPlacedInBase(), settings.current().instantWarmupPlacements()
|
||||
);
|
||||
default -> null;
|
||||
};
|
||||
@@ -243,16 +270,16 @@ final class BaseProgressListener implements Listener {
|
||||
private ProgressDisplay cooldownDisplay(PlayerState state) {
|
||||
return switch (state.cooldownLevel()) {
|
||||
case 0 -> new ProgressDisplay(
|
||||
"2h Cooldown", state.blocksBrokenInBase(), settings.firstCooldownBreaks()
|
||||
"2h Cooldown", state.blocksBrokenInBase(), settings.current().firstCooldownBreaks()
|
||||
);
|
||||
case 1 -> new ProgressDisplay(
|
||||
"1h Cooldown", state.blocksBrokenInBase(), settings.secondCooldownBreaks()
|
||||
"1h Cooldown", state.blocksBrokenInBase(), settings.current().secondCooldownBreaks()
|
||||
);
|
||||
case 2 -> new ProgressDisplay(
|
||||
"30m Cooldown", state.blocksBrokenInBase(), settings.thirdCooldownBreaks()
|
||||
"30m Cooldown", state.blocksBrokenInBase(), settings.current().thirdCooldownBreaks()
|
||||
);
|
||||
case 3 -> new ProgressDisplay(
|
||||
"Instant Cooldown", state.blocksBrokenInBase(), settings.instantCooldownBreaks()
|
||||
"Instant Cooldown", state.blocksBrokenInBase(), settings.current().instantCooldownBreaks()
|
||||
);
|
||||
default -> null;
|
||||
};
|
||||
@@ -289,10 +316,13 @@ final class BaseProgressListener implements Listener {
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isProgressMaterial(Material material) {
|
||||
return material == Material.GRASS_BLOCK || material == Material.DIRT
|
||||
|| material == Material.STONE || material == Material.DEEPSLATE
|
||||
|| material == Material.OBSIDIAN;
|
||||
private boolean isProgressMaterial(Material material) {
|
||||
String name = material.name();
|
||||
PluginSettings configured = settings.current();
|
||||
return configured.baseUnlockMaterials().contains(name)
|
||||
|| configured.stoneExpansionMaterial().equals(name)
|
||||
|| configured.deepslateExpansionMaterial().equals(name)
|
||||
|| configured.obsidianExpansionMaterial().equals(name);
|
||||
}
|
||||
|
||||
private static String roman(int level) {
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
package games.dmg.spigotbase;
|
||||
|
||||
public final class BaseProgressionService {
|
||||
private final PluginSettings settings;
|
||||
private final PluginSettingsProvider settings;
|
||||
|
||||
public BaseProgressionService(PluginSettings settings) {
|
||||
this(new PluginSettingsProvider(settings));
|
||||
}
|
||||
|
||||
public BaseProgressionService(PluginSettingsProvider settings) {
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
@@ -13,10 +17,10 @@ public final class BaseProgressionService {
|
||||
: player.grassAndDirtBroken() + 1;
|
||||
int previousLevel = player.baseLevel();
|
||||
int baseLevel = previousLevel;
|
||||
if (previousLevel == 0 && count >= settings.baseUnlockBlocks()) {
|
||||
if (previousLevel == 0 && count >= settings.current().baseUnlockBlocks()) {
|
||||
baseLevel = 1;
|
||||
}
|
||||
if (previousLevel == 1 && count >= settings.navigationUnlockBlocks()) {
|
||||
if (previousLevel == 1 && count >= settings.current().navigationUnlockBlocks()) {
|
||||
baseLevel = 2;
|
||||
}
|
||||
boolean unlocked = baseLevel != previousLevel;
|
||||
|
||||
@@ -5,9 +5,13 @@ import java.time.Instant;
|
||||
import java.util.Optional;
|
||||
|
||||
public final class BaseService {
|
||||
private final PluginSettings settings;
|
||||
private final PluginSettingsProvider settings;
|
||||
|
||||
public BaseService(PluginSettings settings) {
|
||||
this(new PluginSettingsProvider(settings));
|
||||
}
|
||||
|
||||
public BaseService(PluginSettingsProvider settings) {
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
@@ -23,7 +27,7 @@ public final class BaseService {
|
||||
return Optional.empty();
|
||||
}
|
||||
Instant availableAt = player.lastBaseSet().orElseThrow()
|
||||
.plusSeconds(settings.relocationCooldownSeconds());
|
||||
.plusSeconds(settings.current().relocationCooldownSeconds());
|
||||
if (!now.isBefore(availableAt)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@@ -193,6 +193,23 @@ public record PlayerState(
|
||||
);
|
||||
}
|
||||
|
||||
public PlayerState withProgressCounters(
|
||||
long grassDirt,
|
||||
long stone,
|
||||
long deepslate,
|
||||
long obsidian,
|
||||
long placements,
|
||||
long baseBreaks
|
||||
) {
|
||||
return new PlayerState(
|
||||
playerId, latestName, base, baseLevel, sizeLevel, flightLevel,
|
||||
warmupLevel, cooldownLevel, grassDirt, stone, deepslate,
|
||||
obsidian, placements, baseBreaks,
|
||||
navigationEnabled, flightEnabled, bossBarEnabled, visitorsEnabled,
|
||||
lastBaseSet, lastBaseTeleport, visitorCooldownUntil
|
||||
);
|
||||
}
|
||||
|
||||
public PlayerState withAdministrativeLevels(
|
||||
int newBaseLevel,
|
||||
int newSizeLevel,
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
package games.dmg.spigotbase;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public record PluginSettings(
|
||||
int baseUnlockBlocks,
|
||||
@@ -32,7 +35,18 @@ public record PluginSettings(
|
||||
long secondTeleportCooldownSeconds,
|
||||
long thirdTeleportCooldownSeconds,
|
||||
long fourthTeleportCooldownSeconds,
|
||||
int visitorUnlockDiamondCost
|
||||
int visitorUnlockDiamondCost,
|
||||
Set<String> baseUnlockMaterials,
|
||||
String stoneExpansionMaterial,
|
||||
String deepslateExpansionMaterial,
|
||||
String obsidianExpansionMaterial,
|
||||
String visitorCurrencyMaterial,
|
||||
Set<String> cooldownExcludedMaterials,
|
||||
int bossBarDurationTicks,
|
||||
int navigationParticleCount,
|
||||
int titleFadeInTicks,
|
||||
int titleStayTicks,
|
||||
int titleFadeOutTicks
|
||||
) {
|
||||
private static final int DEFAULT_BASE_UNLOCK_BLOCKS = 250;
|
||||
private static final int DEFAULT_NAVIGATION_UNLOCK_BLOCKS = 500;
|
||||
@@ -63,6 +77,11 @@ public record PluginSettings(
|
||||
private static final long DEFAULT_THIRD_TELEPORT_COOLDOWN_SECONDS = 3_600L;
|
||||
private static final long DEFAULT_FOURTH_TELEPORT_COOLDOWN_SECONDS = 1_800L;
|
||||
private static final int DEFAULT_VISITOR_UNLOCK_DIAMOND_COST = 128;
|
||||
private static final int DEFAULT_BOSS_BAR_DURATION_TICKS = 60;
|
||||
private static final int DEFAULT_NAVIGATION_PARTICLE_COUNT = 5;
|
||||
private static final int DEFAULT_TITLE_FADE_IN_TICKS = 10;
|
||||
private static final int DEFAULT_TITLE_STAY_TICKS = 70;
|
||||
private static final int DEFAULT_TITLE_FADE_OUT_TICKS = 20;
|
||||
|
||||
public PluginSettings {
|
||||
requirePositive(baseUnlockBlocks, "base-unlock-blocks");
|
||||
@@ -118,6 +137,25 @@ public record PluginSettings(
|
||||
throw new IllegalArgumentException("teleport cooldown durations must decrease");
|
||||
}
|
||||
requirePositive(visitorUnlockDiamondCost, "visitor-unlock-diamond-cost");
|
||||
baseUnlockMaterials = normalizedSet(baseUnlockMaterials, "base-unlock-materials", false);
|
||||
stoneExpansionMaterial = normalizedName(stoneExpansionMaterial, "stone-expansion-material");
|
||||
deepslateExpansionMaterial = normalizedName(
|
||||
deepslateExpansionMaterial, "deepslate-expansion-material"
|
||||
);
|
||||
obsidianExpansionMaterial = normalizedName(
|
||||
obsidianExpansionMaterial, "obsidian-expansion-material"
|
||||
);
|
||||
visitorCurrencyMaterial = normalizedName(
|
||||
visitorCurrencyMaterial, "visitor-currency-material"
|
||||
);
|
||||
cooldownExcludedMaterials = normalizedSet(
|
||||
cooldownExcludedMaterials, "cooldown-excluded-materials", true
|
||||
);
|
||||
requirePositive(bossBarDurationTicks, "boss-bar-duration-ticks");
|
||||
requirePositive(navigationParticleCount, "navigation-particle-count");
|
||||
requireNonNegative(titleFadeInTicks, "title-fade-in-ticks");
|
||||
requirePositive(titleStayTicks, "title-stay-ticks");
|
||||
requireNonNegative(titleFadeOutTicks, "title-fade-out-ticks");
|
||||
}
|
||||
|
||||
public static PluginSettings from(Map<String, ?> values) {
|
||||
@@ -167,10 +205,57 @@ public record PluginSettings(
|
||||
"fourth-teleport-cooldown-seconds",
|
||||
DEFAULT_FOURTH_TELEPORT_COOLDOWN_SECONDS
|
||||
),
|
||||
integer(values, "visitor-unlock-diamond-cost", DEFAULT_VISITOR_UNLOCK_DIAMOND_COST)
|
||||
integer(values, "visitor-unlock-diamond-cost", DEFAULT_VISITOR_UNLOCK_DIAMOND_COST),
|
||||
stringSet(values, "base-unlock-materials", Set.of("GRASS_BLOCK", "DIRT")),
|
||||
string(values, "stone-expansion-material", "STONE"),
|
||||
string(values, "deepslate-expansion-material", "DEEPSLATE"),
|
||||
string(values, "obsidian-expansion-material", "OBSIDIAN"),
|
||||
string(values, "visitor-currency-material", "DIAMOND"),
|
||||
stringSet(values, "cooldown-excluded-materials", Set.of()),
|
||||
integer(values, "boss-bar-duration-ticks", DEFAULT_BOSS_BAR_DURATION_TICKS),
|
||||
integer(values, "navigation-particle-count", DEFAULT_NAVIGATION_PARTICLE_COUNT),
|
||||
integer(values, "title-fade-in-ticks", DEFAULT_TITLE_FADE_IN_TICKS),
|
||||
integer(values, "title-stay-ticks", DEFAULT_TITLE_STAY_TICKS),
|
||||
integer(values, "title-fade-out-ticks", DEFAULT_TITLE_FADE_OUT_TICKS)
|
||||
);
|
||||
}
|
||||
|
||||
private static String string(Map<String, ?> values, String key, String defaultValue) {
|
||||
Object value = values.get(key);
|
||||
return value == null ? defaultValue : normalizedName(value.toString(), key);
|
||||
}
|
||||
|
||||
private static Set<String> stringSet(
|
||||
Map<String, ?> values,
|
||||
String key,
|
||||
Set<String> defaultValue
|
||||
) {
|
||||
Object value = values.get(key);
|
||||
if (value == null) {
|
||||
return defaultValue;
|
||||
}
|
||||
if (!(value instanceof List<?> list)) {
|
||||
throw new IllegalArgumentException(key + " must be a list");
|
||||
}
|
||||
return list.stream().map(Object::toString).collect(Collectors.toUnmodifiableSet());
|
||||
}
|
||||
|
||||
private static Set<String> normalizedSet(Set<String> values, String key, boolean allowEmpty) {
|
||||
if (values == null || (!allowEmpty && values.isEmpty())) {
|
||||
throw new IllegalArgumentException(key + " must not be empty");
|
||||
}
|
||||
return values.stream()
|
||||
.map(value -> normalizedName(value, key))
|
||||
.collect(Collectors.toUnmodifiableSet());
|
||||
}
|
||||
|
||||
private static String normalizedName(String value, String key) {
|
||||
if (value == null || value.isBlank()) {
|
||||
throw new IllegalArgumentException(key + " contains an empty material name");
|
||||
}
|
||||
return value.trim().toUpperCase(java.util.Locale.ROOT);
|
||||
}
|
||||
|
||||
private static int integer(Map<String, ?> values, String key, int defaultValue) {
|
||||
long value = longInteger(values, key, defaultValue);
|
||||
if (value > Integer.MAX_VALUE || value < Integer.MIN_VALUE) {
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package games.dmg.spigotbase;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public final class PluginSettingsProvider {
|
||||
private volatile PluginSettings current;
|
||||
|
||||
public PluginSettingsProvider(PluginSettings initial) {
|
||||
current = Objects.requireNonNull(initial, "initial");
|
||||
}
|
||||
|
||||
public PluginSettings current() {
|
||||
return current;
|
||||
}
|
||||
|
||||
public void update(PluginSettings updated) {
|
||||
current = Objects.requireNonNull(updated, "updated");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package games.dmg.spigotbase;
|
||||
|
||||
import java.util.stream.Stream;
|
||||
import org.bukkit.Material;
|
||||
|
||||
final class PluginSettingsValidator {
|
||||
private PluginSettingsValidator() {
|
||||
}
|
||||
|
||||
static PluginSettings validateMaterials(PluginSettings settings) {
|
||||
Stream.concat(
|
||||
settings.baseUnlockMaterials().stream(),
|
||||
Stream.of(
|
||||
settings.stoneExpansionMaterial(),
|
||||
settings.deepslateExpansionMaterial(),
|
||||
settings.obsidianExpansionMaterial()
|
||||
)
|
||||
).forEach(name -> {
|
||||
Material material = Material.matchMaterial(name);
|
||||
if (material == null || !material.isBlock()) {
|
||||
throw new IllegalArgumentException(name + " must identify a block material");
|
||||
}
|
||||
});
|
||||
for (String name : settings.cooldownExcludedMaterials()) {
|
||||
if (Material.matchMaterial(name) == null) {
|
||||
throw new IllegalArgumentException(name + " is not a known material");
|
||||
}
|
||||
}
|
||||
Material currency = Material.matchMaterial(settings.visitorCurrencyMaterial());
|
||||
if (currency == null || currency.isAir()) {
|
||||
throw new IllegalArgumentException("visitor-currency-material is invalid");
|
||||
}
|
||||
return settings;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package games.dmg.spigotbase;
|
||||
|
||||
public enum ProgressCounter {
|
||||
GRASS_DIRT,
|
||||
STONE,
|
||||
DEEPSLATE,
|
||||
OBSIDIAN,
|
||||
PLACEMENTS,
|
||||
BASE_BREAKS
|
||||
}
|
||||
@@ -1,9 +1,13 @@
|
||||
package games.dmg.spigotbase;
|
||||
|
||||
public final class SecondaryProgressionService {
|
||||
private final PluginSettings settings;
|
||||
private final PluginSettingsProvider settings;
|
||||
|
||||
public SecondaryProgressionService(PluginSettings settings) {
|
||||
this(new PluginSettingsProvider(settings));
|
||||
}
|
||||
|
||||
public SecondaryProgressionService(PluginSettingsProvider settings) {
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
@@ -12,7 +16,7 @@ public final class SecondaryProgressionService {
|
||||
return ProgressionUpdate.unchanged(player);
|
||||
}
|
||||
long count = increment(player.stoneBroken());
|
||||
boolean unlocked = count >= settings.stoneExpansionBlocks();
|
||||
boolean unlocked = count >= settings.current().stoneExpansionBlocks();
|
||||
return new ProgressionUpdate(
|
||||
player.withSizeProgress(
|
||||
count, player.deepslateBroken(), player.obsidianBroken(), unlocked ? 1 : 0
|
||||
@@ -26,7 +30,7 @@ public final class SecondaryProgressionService {
|
||||
return ProgressionUpdate.unchanged(player);
|
||||
}
|
||||
long count = increment(player.deepslateBroken());
|
||||
boolean unlocked = count >= settings.deepslateExpansionBlocks();
|
||||
boolean unlocked = count >= settings.current().deepslateExpansionBlocks();
|
||||
return new ProgressionUpdate(
|
||||
player.withSizeProgress(
|
||||
player.stoneBroken(), count, player.obsidianBroken(), unlocked ? 2 : 1
|
||||
@@ -40,7 +44,7 @@ public final class SecondaryProgressionService {
|
||||
return ProgressionUpdate.unchanged(player);
|
||||
}
|
||||
long count = increment(player.obsidianBroken());
|
||||
boolean unlocked = count >= settings.obsidianExpansionBlocks();
|
||||
boolean unlocked = count >= settings.current().obsidianExpansionBlocks();
|
||||
return new ProgressionUpdate(
|
||||
player.withSizeProgress(
|
||||
player.stoneBroken(), player.deepslateBroken(), count, unlocked ? 3 : 2
|
||||
|
||||
@@ -9,7 +9,7 @@ import org.bukkit.command.PluginCommand;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
public final class SpigotBasePlugin extends JavaPlugin {
|
||||
private PluginSettings settings;
|
||||
private PluginSettingsProvider settingsProvider;
|
||||
private BaseStateManager stateManager;
|
||||
private BaseProgressListener progressListener;
|
||||
private BaseFlightController flightController;
|
||||
@@ -20,7 +20,9 @@ public final class SpigotBasePlugin extends JavaPlugin {
|
||||
saveDefaultConfig();
|
||||
try {
|
||||
Map<String, Object> values = getConfig().getValues(false);
|
||||
settings = PluginSettings.from(values);
|
||||
settingsProvider = new PluginSettingsProvider(
|
||||
PluginSettingsValidator.validateMaterials(PluginSettings.from(values))
|
||||
);
|
||||
stateManager = new BaseStateManager(
|
||||
new YamlBaseStateRepository(getDataFolder().toPath().resolve("state.yml")),
|
||||
getLogger()
|
||||
@@ -31,13 +33,13 @@ public final class SpigotBasePlugin extends JavaPlugin {
|
||||
return;
|
||||
}
|
||||
|
||||
BaseService baseService = new BaseService(settings);
|
||||
BaseProgressionService progressionService = new BaseProgressionService(settings);
|
||||
BaseService baseService = new BaseService(settingsProvider);
|
||||
BaseProgressionService progressionService = new BaseProgressionService(settingsProvider);
|
||||
SecondaryProgressionService secondaryProgressionService =
|
||||
new SecondaryProgressionService(settings);
|
||||
new SecondaryProgressionService(settingsProvider);
|
||||
TeleportProgressionService teleportProgressionService =
|
||||
new TeleportProgressionService(settings);
|
||||
BaseBoundsService boundsService = new BaseBoundsService(settings);
|
||||
new TeleportProgressionService(settingsProvider);
|
||||
BaseBoundsService boundsService = new BaseBoundsService(settingsProvider);
|
||||
progressListener = new BaseProgressListener(
|
||||
this,
|
||||
stateManager,
|
||||
@@ -45,16 +47,16 @@ public final class SpigotBasePlugin extends JavaPlugin {
|
||||
secondaryProgressionService,
|
||||
teleportProgressionService,
|
||||
boundsService,
|
||||
settings
|
||||
settingsProvider
|
||||
);
|
||||
flightController = new BaseFlightController(
|
||||
getServer(), stateManager, secondaryProgressionService, boundsService, settings
|
||||
getServer(), stateManager, secondaryProgressionService, boundsService, settingsProvider
|
||||
);
|
||||
VisitorPolicy visitorPolicy = new VisitorPolicy();
|
||||
teleportManager = new BaseTeleportManager(
|
||||
this,
|
||||
stateManager,
|
||||
new TeleportPolicy(settings),
|
||||
new TeleportPolicy(settingsProvider),
|
||||
visitorPolicy,
|
||||
new SafeBaseDestination(),
|
||||
Clock.systemUTC()
|
||||
@@ -64,9 +66,11 @@ public final class SpigotBasePlugin extends JavaPlugin {
|
||||
|
||||
command("setbase").setExecutor(new SetBaseCommand(stateManager, baseService, Clock.systemUTC()));
|
||||
command("base").setExecutor(
|
||||
new BaseCommand(teleportManager, stateManager, visitorPolicy, settings)
|
||||
new BaseCommand(teleportManager, stateManager, visitorPolicy, settingsProvider)
|
||||
);
|
||||
command("baseprogress").setExecutor(
|
||||
new BaseProgressCommand(stateManager, settingsProvider)
|
||||
);
|
||||
command("baseprogress").setExecutor(new BaseProgressCommand(stateManager, settings));
|
||||
command("basenavigation").setExecutor(new BaseNavigationCommand(stateManager));
|
||||
command("baseflight").setExecutor(new BaseFlightCommand(stateManager, flightController));
|
||||
command("basevisitors").setExecutor(new BaseVisitorsCommand(stateManager));
|
||||
@@ -74,11 +78,19 @@ public final class SpigotBasePlugin extends JavaPlugin {
|
||||
command("gotobase").setExecutor(goToBaseCommand);
|
||||
command("gotobase").setTabCompleter(goToBaseCommand);
|
||||
command("baseadmin").setExecutor(
|
||||
new BaseAdminCommand(stateManager, new AdminProgressionService())
|
||||
new BaseAdminCommand(
|
||||
this,
|
||||
stateManager,
|
||||
new AdminProgressionService(settingsProvider),
|
||||
settingsProvider
|
||||
)
|
||||
);
|
||||
|
||||
getServer().getScheduler().runTaskTimer(
|
||||
this, new BaseNavigationController(getServer(), stateManager), 10L, 10L
|
||||
this,
|
||||
new BaseNavigationController(getServer(), stateManager, settingsProvider),
|
||||
10L,
|
||||
10L
|
||||
);
|
||||
getServer().getScheduler().runTaskTimer(this, flightController, 5L, 5L);
|
||||
getServer().getScheduler().runTaskTimer(this, stateManager::saveIfDirty, 600L, 600L);
|
||||
@@ -102,10 +114,10 @@ public final class SpigotBasePlugin extends JavaPlugin {
|
||||
}
|
||||
|
||||
PluginSettings settings() {
|
||||
if (settings == null) {
|
||||
if (settingsProvider == null) {
|
||||
throw new IllegalStateException("Plugin settings are unavailable");
|
||||
}
|
||||
return settings;
|
||||
return settingsProvider.current();
|
||||
}
|
||||
|
||||
private PluginCommand command(String name) {
|
||||
|
||||
@@ -5,17 +5,21 @@ import java.time.Instant;
|
||||
import java.util.Optional;
|
||||
|
||||
public final class TeleportPolicy {
|
||||
private final PluginSettings settings;
|
||||
private final PluginSettingsProvider settings;
|
||||
|
||||
public TeleportPolicy(PluginSettings settings) {
|
||||
this(new PluginSettingsProvider(settings));
|
||||
}
|
||||
|
||||
public TeleportPolicy(PluginSettingsProvider settings) {
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
public Duration warmup(PlayerState player) {
|
||||
return switch (player.warmupLevel()) {
|
||||
case 0 -> Duration.ofSeconds(settings.initialWarmupSeconds());
|
||||
case 1 -> Duration.ofSeconds(settings.secondWarmupSeconds());
|
||||
case 2 -> Duration.ofSeconds(settings.thirdWarmupSeconds());
|
||||
case 0 -> Duration.ofSeconds(settings.current().initialWarmupSeconds());
|
||||
case 1 -> Duration.ofSeconds(settings.current().secondWarmupSeconds());
|
||||
case 2 -> Duration.ofSeconds(settings.current().thirdWarmupSeconds());
|
||||
case 3 -> Duration.ZERO;
|
||||
default -> throw new IllegalArgumentException("unknown warm-up level");
|
||||
};
|
||||
@@ -23,10 +27,10 @@ public final class TeleportPolicy {
|
||||
|
||||
public Duration cooldown(PlayerState player) {
|
||||
return switch (player.cooldownLevel()) {
|
||||
case 0 -> Duration.ofSeconds(settings.initialTeleportCooldownSeconds());
|
||||
case 1 -> Duration.ofSeconds(settings.secondTeleportCooldownSeconds());
|
||||
case 2 -> Duration.ofSeconds(settings.thirdTeleportCooldownSeconds());
|
||||
case 3 -> Duration.ofSeconds(settings.fourthTeleportCooldownSeconds());
|
||||
case 0 -> Duration.ofSeconds(settings.current().initialTeleportCooldownSeconds());
|
||||
case 1 -> Duration.ofSeconds(settings.current().secondTeleportCooldownSeconds());
|
||||
case 2 -> Duration.ofSeconds(settings.current().thirdTeleportCooldownSeconds());
|
||||
case 3 -> Duration.ofSeconds(settings.current().fourthTeleportCooldownSeconds());
|
||||
case 4 -> Duration.ZERO;
|
||||
default -> throw new IllegalArgumentException("unknown cooldown level");
|
||||
};
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
package games.dmg.spigotbase;
|
||||
|
||||
public final class TeleportProgressionService {
|
||||
private final PluginSettings settings;
|
||||
private final PluginSettingsProvider settings;
|
||||
|
||||
public TeleportProgressionService(PluginSettings settings) {
|
||||
this(new PluginSettingsProvider(settings));
|
||||
}
|
||||
|
||||
public TeleportProgressionService(PluginSettingsProvider settings) {
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
@@ -13,7 +17,7 @@ public final class TeleportProgressionService {
|
||||
}
|
||||
long placements = increment(player.blocksPlacedInBase());
|
||||
int baseLevel = player.baseLevel();
|
||||
if (baseLevel == 2 && placements >= settings.teleportUnlockPlacements()) {
|
||||
if (baseLevel == 2 && placements >= settings.current().teleportUnlockPlacements()) {
|
||||
baseLevel = 3;
|
||||
}
|
||||
int warmupLevel = baseLevel >= 3 ? warmupLevel(placements) : 0;
|
||||
@@ -56,29 +60,29 @@ public final class TeleportProgressionService {
|
||||
}
|
||||
|
||||
private int warmupLevel(long placements) {
|
||||
if (placements >= settings.instantWarmupPlacements()) {
|
||||
if (placements >= settings.current().instantWarmupPlacements()) {
|
||||
return 3;
|
||||
}
|
||||
if (placements >= settings.thirdWarmupPlacements()) {
|
||||
if (placements >= settings.current().thirdWarmupPlacements()) {
|
||||
return 2;
|
||||
}
|
||||
if (placements >= settings.secondWarmupPlacements()) {
|
||||
if (placements >= settings.current().secondWarmupPlacements()) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private int cooldownLevel(long breaks) {
|
||||
if (breaks >= settings.instantCooldownBreaks()) {
|
||||
if (breaks >= settings.current().instantCooldownBreaks()) {
|
||||
return 4;
|
||||
}
|
||||
if (breaks >= settings.thirdCooldownBreaks()) {
|
||||
if (breaks >= settings.current().thirdCooldownBreaks()) {
|
||||
return 3;
|
||||
}
|
||||
if (breaks >= settings.secondCooldownBreaks()) {
|
||||
if (breaks >= settings.current().secondCooldownBreaks()) {
|
||||
return 2;
|
||||
}
|
||||
if (breaks >= settings.firstCooldownBreaks()) {
|
||||
if (breaks >= settings.current().firstCooldownBreaks()) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
|
||||
@@ -38,7 +38,8 @@ public final class YamlBaseStateRepository {
|
||||
if (parent != null) {
|
||||
Files.createDirectories(parent);
|
||||
}
|
||||
YamlConfiguration yaml = new YamlConfiguration();
|
||||
YamlConfiguration yaml = loadForSave();
|
||||
removePlayersAbsentFrom(state, yaml);
|
||||
savePlayers(yaml, state.players());
|
||||
|
||||
Path temporary = Files.createTempFile(parent, "spigot-base-state-", ".yml");
|
||||
@@ -59,6 +60,38 @@ public final class YamlBaseStateRepository {
|
||||
}
|
||||
}
|
||||
|
||||
private YamlConfiguration loadForSave() throws IOException {
|
||||
YamlConfiguration yaml = new YamlConfiguration();
|
||||
if (!Files.exists(stateFile)) {
|
||||
return yaml;
|
||||
}
|
||||
try {
|
||||
yaml.load(stateFile.toFile());
|
||||
return yaml;
|
||||
} catch (InvalidConfigurationException exception) {
|
||||
throw new IOException("state file is not valid YAML", exception);
|
||||
}
|
||||
}
|
||||
|
||||
private static void removePlayersAbsentFrom(
|
||||
PersistentState state,
|
||||
YamlConfiguration yaml
|
||||
) {
|
||||
ConfigurationSection section = yaml.getConfigurationSection("players");
|
||||
if (section == null) {
|
||||
return;
|
||||
}
|
||||
for (String key : section.getKeys(false)) {
|
||||
try {
|
||||
if (!state.players().containsKey(UUID.fromString(key))) {
|
||||
yaml.set("players." + key, null);
|
||||
}
|
||||
} catch (IllegalArgumentException exception) {
|
||||
yaml.set("players." + key, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Map<UUID, PlayerState> loadPlayers(YamlConfiguration yaml) {
|
||||
Map<UUID, PlayerState> players = new HashMap<>();
|
||||
ConfigurationSection section = yaml.getConfigurationSection("players");
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
# Core Base progression
|
||||
base-unlock-blocks: 250
|
||||
base-unlock-materials:
|
||||
- GRASS_BLOCK
|
||||
- DIRT
|
||||
navigation-unlock-blocks: 500
|
||||
initial-radius: 10
|
||||
initial-vertical-range: 25
|
||||
@@ -7,8 +10,11 @@ relocation-cooldown-seconds: 86400
|
||||
|
||||
# Base size
|
||||
stone-expansion-blocks: 500
|
||||
stone-expansion-material: STONE
|
||||
deepslate-expansion-blocks: 1000
|
||||
deepslate-expansion-material: DEEPSLATE
|
||||
obsidian-expansion-blocks: 1000
|
||||
obsidian-expansion-material: OBSIDIAN
|
||||
first-expanded-radius: 25
|
||||
second-expanded-radius: 75
|
||||
third-expanded-radius: 150
|
||||
@@ -38,3 +44,12 @@ fourth-teleport-cooldown-seconds: 1800
|
||||
|
||||
# Base IV
|
||||
visitor-unlock-diamond-cost: 128
|
||||
visitor-currency-material: DIAMOND
|
||||
|
||||
# Progress and notification presentation
|
||||
cooldown-excluded-materials: []
|
||||
boss-bar-duration-ticks: 60
|
||||
navigation-particle-count: 5
|
||||
title-fade-in-ticks: 10
|
||||
title-stay-ticks: 70
|
||||
title-fade-out-ticks: 20
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package games.dmg.spigotbase;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
final class AdminProgressCounterTest {
|
||||
private final AdminProgressionService service = new AdminProgressionService(
|
||||
new PluginSettingsProvider(PluginSettings.from(Map.of()))
|
||||
);
|
||||
|
||||
@Test
|
||||
void settingPlacementCounterEvaluatesBaseAndWarmupTiers() {
|
||||
PlayerState player = PlayerState.newPlayer(UUID.randomUUID(), "Alex");
|
||||
|
||||
PlayerState updated = service.setProgress(player, ProgressCounter.PLACEMENTS, 2_000);
|
||||
|
||||
assertEquals(3, updated.baseLevel());
|
||||
assertEquals(2, updated.warmupLevel());
|
||||
assertEquals(2_000, updated.blocksPlacedInBase());
|
||||
}
|
||||
|
||||
@Test
|
||||
void settingBaseBreakCounterEvaluatesCooldownTier() {
|
||||
PlayerState player = PlayerState.newPlayer(UUID.randomUUID(), "Alex");
|
||||
|
||||
PlayerState updated = service.setProgress(player, ProgressCounter.BASE_BREAKS, 5_000);
|
||||
|
||||
assertEquals(3, updated.baseLevel());
|
||||
assertEquals(4, updated.cooldownLevel());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package games.dmg.spigotbase;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
final class AdminResetPathTest {
|
||||
private final AdminProgressionService service = new AdminProgressionService(
|
||||
new PluginSettingsProvider(PluginSettings.from(Map.of()))
|
||||
);
|
||||
|
||||
@Test
|
||||
void resettingSizeClearsItsLevelsAndCounters() {
|
||||
PlayerState player = PlayerState.newPlayer(UUID.randomUUID(), "Alex")
|
||||
.withGrassAndDirtProgress(500, 2)
|
||||
.withSizeProgress(500, 1_000, 1_000, 3);
|
||||
|
||||
PlayerState reset = service.resetPath(player, ProgressionPath.SIZE);
|
||||
|
||||
assertEquals(0, reset.sizeLevel());
|
||||
assertEquals(0, reset.stoneBroken());
|
||||
assertEquals(0, reset.deepslateBroken());
|
||||
assertEquals(0, reset.obsidianBroken());
|
||||
}
|
||||
|
||||
@Test
|
||||
void resettingBaseCascadesAllDependentProgress() {
|
||||
PlayerState player = PlayerState.newPlayer(UUID.randomUUID(), "Alex")
|
||||
.withAdministrativeLevels(4, 3, 3, 3, 4, true, true, true);
|
||||
|
||||
PlayerState reset = service.resetPath(player, ProgressionPath.BASE);
|
||||
|
||||
assertEquals(PlayerState.newPlayer(player.playerId(), player.latestName()), reset);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package games.dmg.spigotbase;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
final class PluginSettingsProviderTest {
|
||||
@Test
|
||||
void servicesObserveValidatedRuntimeUpdates() {
|
||||
PluginSettingsProvider provider =
|
||||
new PluginSettingsProvider(PluginSettings.from(Map.of()));
|
||||
TeleportPolicy policy = new TeleportPolicy(provider);
|
||||
PlayerState player = PlayerState.newPlayer(UUID.randomUUID(), "Alex")
|
||||
.withGrassAndDirtProgress(500, 3);
|
||||
|
||||
provider.update(PluginSettings.from(Map.of("initial-warmup-seconds", 20)));
|
||||
|
||||
assertEquals(Duration.ofSeconds(20), policy.warmup(player));
|
||||
}
|
||||
}
|
||||
@@ -56,6 +56,26 @@ final class YamlBaseStateRepositoryTest {
|
||||
assertFalse(loaded.players().containsKey(playerId));
|
||||
}
|
||||
|
||||
@Test
|
||||
void preservesUnknownForwardCompatibleFieldsWhenSaving() throws Exception {
|
||||
UUID playerId = UUID.randomUUID();
|
||||
Path stateFile = temporaryDirectory.resolve("state.yml");
|
||||
Files.writeString(stateFile, """
|
||||
future-root: retained
|
||||
players:
|
||||
%s:
|
||||
name: Alex
|
||||
future-player-field: retained
|
||||
""".formatted(playerId));
|
||||
YamlBaseStateRepository repository = new YamlBaseStateRepository(stateFile);
|
||||
|
||||
repository.save(repository.load());
|
||||
|
||||
String saved = Files.readString(stateFile);
|
||||
org.junit.jupiter.api.Assertions.assertTrue(saved.contains("future-root: retained"));
|
||||
org.junit.jupiter.api.Assertions.assertTrue(saved.contains("future-player-field: retained"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void missingFileLoadsEmptyState() throws Exception {
|
||||
YamlBaseStateRepository repository =
|
||||
|
||||
Reference in New Issue
Block a user