diff --git a/CHANGELOG.md b/CHANGELOG.md index 08051ee..5745726 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,138 @@ ## Unreleased +## Version 1.0.0 + +### New Features + +#### Misc + ++ Open menus now update themselves when Towny changes what they show: residents joining or leaving, land being claimed, + ranks, bank transactions, plot settings, deletions, and the new day. Turn this off with `live-menu-refresh`. ++ TownyMenu now announces in chat when you are invited to a town, your town is invited to a nation, your nation is asked + for an alliance, or your town goes bankrupt or falls into ruin. Each message opens the menu that answers it. Turn this + off with `towny-alerts`. ++ Added `/townymenu invites`, which opens the invites menu. + +#### Main Menu + ++ Added Leaderboards: towns and nations ranked by residents, land, and bank balance, players ranked by the plots they + own, and the richest residents of your town and nation. ++ Added Prices & New Day: a countdown to the next Towny day and what the server charges for founding, claiming, upkeep, + plots, and taxes. + +#### Town Menu + ++ Clicking your town at the top of the town menu opens its status: level and what the next level needs, claim and + outpost limits, the nation zone, and the upkeep due on the next new day. + + Warnings for ruins, bankruptcy, conquest, overclaiming, and upkeep the bank cannot cover are shown there and on + the town icon. + ++ Added a town jail menu: see prisoners and release them, jail online residents with a chosen sentence, bail, jail, and + cell, and list the town's jails. ++ Added Cede This Plot and Take Over Claim to the claims menu, for giving a chunk to another town and taking land from + an overclaimed town. ++ Added Merge into Your Town to other towns' pages, for mayors asking another town to merge into theirs. ++ Added Announce to Town to the residents menu. ++ Added a Nation Zone switch to town settings for towns that have a nation zone. ++ Residents of a ruined town can reclaim it from the town menu, and players without a town can reclaim ruins from Find a + Town when the server allows it. + +#### Nation Menu + ++ Clicking your nation at the top of the nation menu opens its status: level, what the next level needs, the bonuses + member towns receive, and the nation's daily upkeep. ++ Added sanctioned towns, under the nation's Towns menu: sanction towns outside the nation and lift sanctions. ++ Added Announce to Nation to the nation menu. + +#### Plot Menu + ++ Added Join-Day Limits, setting the fewest and most days a buyer must have lived in the town. ++ Added Jail Cells to jail plots, for adding and removing cells where you stand. ++ Added Player Overrides to plot permissions: add or remove players with their own permissions on a plot, and open + Towny's editor to change them. ++ Right-clicking Evict Owner now evicts the owner and puts the plot back up for sale. ++ Added Many Plots at Once: buy, sell, stop selling, or give up every plot in a square or circle around you, or give up + all your plots. ++ Added Make Outpost and Move Outpost Spawn for your town's claims. ++ Added a Plot HUD switch, and clicking the plot icon shows Towny's plot info in chat. ++ Right-clicking Rename Plot removes the plot's name. ++ Putting a plot up for sale now suggests the town's price for that plot type instead of 0. + +#### Map + ++ Right-clicking a plot for sale on the map buys it without walking there. + +#### Resident Menu + ++ Added My Plots, Daily Tax, Outlawed In, and Trusted In to the profile. ++ Added Display Modes to the profile, turning every mode off or restoring the server's defaults. ++ Added Chat Spy and Disable Admin Powers to personal settings for players allowed to use them. + +#### Admin Menus + ++ Added More Town Tools: town level, merging another town in, putting a town up for sale, removing conquered status, + checking outposts, resident ranks, outlaws, trusted residents, and trusted towns. ++ Added More Nation Tools: nation level, merging, moving a town in, rechecking town distances, sanctioned towns, and + resident ranks. ++ Added a plot menu for the chunk you stand in: claim it for a player, move it to another town, trust players, and + unclaim. ++ Added a Towny Permissions editor for `townyperms.yml`: permission groups and their nodes, and the town and nation + ranks mayors and leaders can hand out, including adding, removing, and renaming ranks. ++ Added purging inactive residents, checking a player's permission node, and depositing into every bank to the server + menu. + +#### Tutorial + ++ Added lessons on announcements, merging towns, ceding land, taking over claims, many plots at once, join-day limits, + player overrides, jailing players, nation sanctions, town and nation levels, your plots and taxes, leaderboards, and + prices. + + The ruins, plot selling, plot groups, and display lessons now cover reclaiming, evicting for resale, group-wide + settings, and display modes. + + The map, plot, buying, and outpost lessons now cover buying from the map, the Plot HUD, and making outposts. + +### Improvements + +#### Town Menu + ++ The town directory can be sorted by distance from you, and then shows how far away each town's spawn is. + +#### Misc + ++ Menus now leave an empty row between their buttons and the bottom row holding the back button. + + Lists show four rows of entries per page instead of five. + +### Fixes + +#### Plot Menu + ++ Fixed selling, plot type, settings, permissions, and trust failing on plots in a plot group; on grouped plots they now + change the whole group, and their buttons say so. + +#### Town Menu + ++ Fixed the second row of the claims menu starting one slot too far right. + +#### Misc + ++ Fixed the back button covering the Outsiders: Switch button in the permissions menu. ++ Fixed a menu reopening over, or closing, another menu opened while a command was still finishing. ++ Fixed clicking a button twice quickly running its command twice, such as toggling PvP on and straight back off. ++ Fixed pressing Escape in a text dialog leaving no menu open; the dialog now closes only with Confirm or Cancel. ++ Removed the empty line at the end of the Visit, Found a Town, and Found a Nation icons. ++ Fixed a leading space in a resident's full name when they have a surname but no title. ++ Town and nation boards and resident about texts no longer show Towny's placeholder (`/town set board [msg]`, + `/res set about [msg]`) when none has been set, and the edit dialogs start empty instead of with the placeholder. + +### Technical Details + +#### Misc + ++ `PermissionMenu` takes an optional `overrides` menu, and a `Toggle` with a `null` node is shown unguarded. ++ `TownyUtil` gained `duration` and `secondsUntilNewDay` for Towny's new-day countdown. ++ `ToggleMenu` splits more than 21 toggles into pages instead of asking for an inventory taller than six rows. ++ `Layout.add` logs and skips buttons beyond its slots instead of throwing, so an overfull grid no longer breaks a menu. + ## Version 0.1.1 ### Technical Details diff --git a/CLAUDE.md b/CLAUDE.md index ab1ae2f..ba518d3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -160,10 +160,10 @@ Extend `Menu` (or `PagedMenu` for lists) and implement `build()`, which runs on - [.github/workflows/build.yml](.github/workflows/build.yml) builds every push and pull request. - Releases are published when a commit that changes `plugin_version` lands on `master`: [.github/workflows/release.yml](.github/workflows/release.yml) builds the jar, uses the matching - `## Version X.Y.Z` section of `CHANGELOG.md` as the release notes, creates the `vX.Y.Z` tag, and attaches the jar. - It skips forks, so the release appears in the organization repository the pull request is merged into. It does - nothing if the tag already exists. See [docs/RELEASING.md](docs/RELEASING.md). **Never create tags, and never merge - a version bump into the organization repository, unless explicitly asked** — that publishes a release. + `## Version X.Y.Z` section of `CHANGELOG.md` as the release notes, creates the `vX.Y.Z` tag, and attaches the jar. It + skips forks, so the release appears in the organization repository the pull request is merged into. It does nothing if + the tag already exists. See [docs/RELEASING.md](docs/RELEASING.md). **Never create tags, and never merge a version + bump into the organization repository, unless explicitly asked** — that publishes a release. ## Commit Convention diff --git a/README.md b/README.md index 0cdfae4..7b7fc2b 100644 --- a/README.md +++ b/README.md @@ -18,24 +18,36 @@ lesson opens the menu it explains, every feature menu has a Tutorial button for its chapter, and reading progress is saved. New players without a town are pointed to it when they join. - **Town menu** — Residents and ranks, invites, the town bank, claims (single chunk, area, fill, outposts, unclaim, - bonus claims, auto-claim), PvP/mobs/fire/explosions/open/public/peaceful settings, name, board, tag, taxes, plot - prices, spawn and home block, map colour, selling the town, build permissions, trusted residents and towns, outlaws, - spawn and outpost teleports, and leaving or deleting the town. + bonus claims, auto-claim, ceding a chunk to another town, taking over claims), PvP/mobs/fire/explosions/open/public/ + peaceful/nation zone settings, name, board, tag, taxes, plot prices, spawn and home block, map colour, selling the + town, build permissions, trusted residents and towns, outlaws, the jail (jailing and releasing prisoners, jails and + cells), announcements, merging towns, reclaiming ruins, spawn and outpost teleports, and leaving or deleting the town. - **Nation menu** — Member towns and invitations, residents and nation ranks, the nation bank, allies and enemies, - peaceful/open/public settings, name, board, tag, taxes, capital, leader, spawn, map colour, and leaving or deleting - the nation. -- **Plot menu** — For the plot you stand in: buy, sell, give up, evict, plot type, name, PvP/fire/explosion/mob/tax - settings, build permissions, trusted players, plot groups and districts, and clearing the plot. -- **Profile menu** — Friends, personal plot permissions, a bio, bail, spawn, and every personal toggle (border titles, - auto map, auto claim, plot borders, info tool, and more). + sanctioned towns, announcements, peaceful/open/public settings, name, board, tag, taxes, capital, leader, spawn, map + colour, and leaving or deleting the nation. +- **Plot menu** — For the plot you stand in: buy, sell, give up, evict (and resell), plot type, name, + PvP/fire/explosion/mob/tax settings, build permissions and per-player overrides, trusted players, join-day limits, + jail cells, outposts, plot groups and districts, clearing the plot, the plot HUD, and buying, selling, or giving up + every plot in an area at once. On a plot in a group, these settings change the whole group. +- **Profile menu** — Friends, your plots, daily tax, the towns that outlaw or trust you, personal plot permissions, a + bio, bail, spawn, every personal toggle (border titles, auto map, auto claim, plot borders, info tool, chat spy, admin + bypass, and more), and clearing or resetting all modes at once. - **Map** — A 9×5 chunk map around you, coloured by your town, your plots, your nation, allies, and enemies, with plots - for sale highlighted. -- **Directories** — Browse and rank every town and nation by residents, claims, bank balance, and more; visit, join, - donate, buy a town that is for sale, or propose alliances. -- **Invites** — Accept or decline town invites, nation invites for your town, and alliance requests in one place. + for sale highlighted; right-click one to buy it. +- **Status pages** — Click your town or nation to see its level, what the next level needs, its limits, the daily + upkeep, and warnings for debt, ruins, conquest, and overclaiming. +- **Leaderboards and prices** — Rank towns, nations, and players by residents, land, and money, and see every price on + the server with a countdown to the next Towny day. +- **Directories** — Browse and rank every town and nation by residents, claims, bank balance, distance, and more; visit, + join, donate, buy a town that is for sale, or propose alliances. +- **Invites** — Accept or decline town invites, nation invites for your town, and alliance requests in one place, or + open them from the chat alert TownyMenu sends when one arrives. +- **Live menus** — An open menu updates itself when Towny changes what it shows: a resident joins, land is claimed, the + bank moves, or the new day passes. - **Admin menus** — Server admins can edit every setting in Towny's `config.yml` (grouped by section, searchable, - applied instantly), change per-world settings, run new days, backups, and reloads, manage any town, nation, or - resident, and change TownyMenu's own settings. Open them with `/tm admin` or from the main menu. + applied instantly) and `townyperms.yml` (groups, nodes, and the ranks mayors hand out), change per-world settings, run + new days, backups, reloads, and purges, manage any town, nation, resident, or plot — including levels, merges, sales, + sanctions, and ranks — and change TownyMenu's own settings. Open them with `/tm admin` or from the main menu. - **English and Chinese** — Every menu, dialog, and message follows each player's Minecraft language (English or Simplified Chinese). Server owners can edit the translations or add new languages. - **Respects Towny** — Every action runs the matching Towny command, so Towny's permissions, costs, cooldowns, and @@ -74,6 +86,7 @@ All commands are sub-commands of `/townymenu` (alias `/tm`). | `/tm` | Open the main menu | | `/tm help` | List all available commands | | `/tm tutorial` | Open the tutorial | +| `/tm invites` | Answer town, nation, and alliance invitations | | `/tm reload` | Reload the configuration and translations (OP only) | | `/tm admin` | Open the admin menu (OP only) | @@ -85,6 +98,8 @@ All commands are sub-commands of `/townymenu` (alias `/tm`). | `language` | `auto` | `auto` follows each player's client language; `en_US` or `zh_CN` forces one | | `sneak-swap-hand-shortcut` | `true` | Open the main menu by pressing swap-hand (F) while sneaking | | `tutorial-join-hint` | `true` | Suggest the tutorial to players without a town when they join | +| `live-menu-refresh` | `true` | Update open menus when Towny changes what they show | +| `towny-alerts` | `true` | Announce invites, bankruptcy, and ruin in chat, linked to the right menu | ## Translations diff --git a/docs/DEVELOPER_GUIDE.md b/docs/DEVELOPER_GUIDE.md index 91793af..3e359da 100644 --- a/docs/DEVELOPER_GUIDE.md +++ b/docs/DEVELOPER_GUIDE.md @@ -281,6 +281,9 @@ permissions, bank, ranks, pickers) in `guis/common`, and feature menus in `guis/ can't intercept the command. - **Grey out what the player can't do.** Use `guarded(slot, PermissionNodes.X, item) { … }` (or `Layout.add(node, …)`) with the same node Towny's command checks. +- **Keep an empty row above the bottom row.** The back button, tutorial button, and page controls sit in the bottom row; + leave the row above it empty so they stand apart from the menu's content. Only grids that need every row, such as the + chunk map and the 4×4 permission grid, skip it. - **Escape player-written text.** Pass names, boards, titles, and tags through `TownyUtil.name()` / `TownyUtil.text()` before embedding them in MiniMessage. - **Translate every string.** Icon names, lore, titles, and prompts come from `tr("key")`, never from Kotlin literals. @@ -299,12 +302,12 @@ open inventory's holder — there is no registry and nothing to clean up when a | `render()` | Clears the inventory (grey filler) and calls `build()`. | | `button(slot, item, action?)` | Places an item; `action` receives the `ClickType`. | | `guarded(slot, node, item, action)` | Like `button`, but shows a grey "no permission" icon without `node`. | -| `layout(vararg slots)` | Returns a `Layout` that fills the given slots in order, for optional buttons. | +| `layout(vararg slots)` | Fills slots in order, for optional buttons; extra buttons are logged, skipped. | | `backButton(slot)` | Back arrow to `back`, or a close button when `back` is `null`. | | `tutorialButton(slot, chapter)` | Help button opening the tutorial `Chapter` that explains this menu. | | `run(command, probe?, returnTo?, delay)` | Runs a Towny command as the player (see below). | | `runAndClose(command)` | Closes the menu, then runs the command (teleports, books, chat output). | -| `prompt(title, label, …) { text -> }` | Shows a text-input dialog; Cancel reopens the menu. | +| `prompt(title, label, …) { text -> }` | Shows a text-input dialog; Cancel or empty input reopens the menu. | | `tr(key, "name" to value, …)` | Translates `key` into the viewer's language (see [Translations](#translations)). | | `resident` | The viewer's Towny `Resident`, or `null`. | @@ -317,6 +320,9 @@ lambda reading the state the command should change — and `MenuActions` compare - probe **changed** → `returnTo` (default: this menu) is re-opened, showing the new state; - probe **unchanged**, or no probe → the menu closes so the player can read Towny's chat reply (an error, a cost). +Neither happens if the player has opened a different menu in the meantime. Until the command settles, `Menu` ignores +that player's clicks, so a double click can't run a command twice. + If the command raises a Towny confirmation, `MenuActions` suppresses the chat prompt (`ConfirmationSendEvent`) and shows a native confirmation dialog instead; accepting runs Towny's confirm command and then settles the probe as usual. @@ -325,9 +331,10 @@ town, joining a town). ### PagedMenu and ListMenu -`PagedMenu(player, title, back)` is a six-row menu. Override `entries()` to return `MenuEntry` objects; the top five -rows show one page, slot 45/53 page back and forth, 49 is the back button, and `controls()` may place extra buttons in -46–48 and 50–52. `MenuEntry` takes a **lambda** that builds the icon, so only entries on the visible page are built: +`PagedMenu(player, title, back)` is a six-row menu. Override `entries()` to return `MenuEntry` objects; the top four +rows show one page, the fifth row stays empty, slot 45/53 page back and forth, 49 is the back button, and `controls()` +may place extra buttons in 46–48 and 50–52. `MenuEntry` takes a **lambda** that builds the icon, so only entries on the +visible page are built: ```kotlin override fun entries(): List = @@ -342,13 +349,13 @@ override fun entries(): List = ### Shared Menus (`guis/common`) -| Class | Purpose | -|:-----------------|:----------------------------------------------------------------------------------------------| -| `ToggleMenu` | A grid of `Toggle`s (material, translated name and description, node, command, value reader). | -| `PermissionMenu` | 4×4 build/destroy/switch/item-use grid for any `set perm` command. | -| `BankMenu` | Deposit, withdraw, and bank history for a town or nation. | -| `RankMenu` | Grants or revokes town or nation ranks, checking the per-rank permission node. | -| `Pickers` | Selection menus for online residents, towns, nations, and fixed options. | +| Class | Purpose | +|:-----------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `ToggleMenu` | Pages of `Toggle`s, 21 per page (material, name, description, node, command, value reader). A `null` node leaves the toggle unguarded, for modes Towny checks no node for. | +| `PermissionMenu` | 4×4 build/destroy/switch/item-use grid for any `set perm` command. Pass `overrides` to add a Player Overrides button (used for plots). | +| `BankMenu` | Deposit, withdraw, and bank history for a town or nation. | +| `RankMenu` | Grants or revokes town or nation ranks, checking the per-rank permission node. | +| `Pickers` | Selection menus for online residents, towns, nations, and fixed options. | ### Tutorial (`guis/tutorial`) @@ -381,6 +388,16 @@ When a change adds a Towny feature to a menu, add or update its lesson in `Tutor (`tutorial..` and `tutorial..-body`). New feature menus should place a `tutorialButton` in a free bottom-row slot (the bottom-right corner, or slot 52 in a `PagedMenu`). +### Reacting to Towny (`listeners`) + +Menus are views, so when Towny changes their data from outside — another player, a command, or the new day — +`MenuRefreshListener` calls `OpenMenus.refreshSoon()`, which re-renders every open menu once on the next tick (skipping +players whose own command is still settling). Add the Towny event to that listener when a change should show up live, +and keep the handler a one-liner. Players can turn this off with `live-menu-refresh`. + +`TownyAlertListener` sends the chat alerts for invitations, bankruptcy, and ruin, each linking to the menu that answers +it (`towny-alerts`). + ### Admin Menus (`guis/admin`) `AdminMenu` is the server admin hub, opened by `/townymenu admin` or a main-menu button. Both entry points require @@ -394,6 +411,11 @@ of the `/townyadmin` or `/townyworld` command it runs. | `AdminServerMenu` | New day and hour, backup, database save, reloads, and global `/townyadmin toggle`s. | | `AdminTownMenu` | Any town through `/townyadmin town`: mayor, residents, claims, bank, overrides, deletion. | | `AdminNationMenu` | Any nation through `/townyadmin nation`: leader, capital, towns, bank, toggles, deletion. | +| `AdminTownToolsMenu` | The rest of `/townyadmin town`: level, merging, sale, outlaws, trust, and ranks. | +| `AdminNationToolsMenu` | The rest of `/townyadmin nation`: level, merging, transfers, sanctions, ranks, recheck. | +| `AdminPlotMenu` | `/townyadmin plot` for the plot the admin stands in, plus `set plot` and `unclaim`. | +| `AdminPermsMenu` | `townyperms.yml`: permission groups, and the town and nation ranks mayors may hand out. | +| `AdminNamesMenu` | The names an admin `add`/`remove` subcommand holds (outlaws, trust, sanctions). | | `AdminResidentListMenu` | Every resident, online first, with a name filter. | | `AdminResidentMenu` | Any resident: town, rename, title, surname, NPC flag, unjail, deletion. | | `PluginSettingsMenu` | Edits TownyMenu's own `config.yml` through `PluginConfig`, then calls `Main.reload()`. | @@ -403,6 +425,9 @@ Towny has no command that edits its config, so `TownyConfigMenu` is the one plac gated by `towny.command.townyadmin.reload`, and the probe is `TownySettings.getConfig()`, which a successful reload replaces. +`RankMenu.create` takes an optional `admin` town or nation name. With it the menu runs +`/townyadmin town|nation rank` and checks the admin node, so staff can hand out ranks in towns they are not in. + ### Icons `Icons` builds the standard icons: `icon(material, name, description, extraLines…)` from already-translated text, and @@ -425,8 +450,10 @@ class TownPvpMenu(player: Player, back: Menu) : Menu(player, player.tr("town-pvp override fun build() { val town = resident?.townOrNull ?: return backButton(22) - guarded(13, PermissionNodes.TOWNY_COMMAND_TOWN_TOGGLE_PVP, - Icons.toggle(player, Material.IRON_SWORD, tr("toggle.pvp"), town.isPVP, tr("toggle.town-pvp-description"))) { + guarded( + 13, PermissionNodes.TOWNY_COMMAND_TOWN_TOGGLE_PVP, + Icons.toggle(player, Material.IRON_SWORD, tr("toggle.pvp"), town.isPVP, tr("toggle.town-pvp-description")) + ) { run("towny:town toggle pvp", { town.isPVP }) } backButton(22) @@ -1085,8 +1112,8 @@ To add a setting, add the key to `config.yml` and a matching property to `Plugin ```kotlin var menuSounds: Boolean - get() = plugin.config.getBoolean("menu-sounds", true) - set(value) = save("menu-sounds", value) +get() = plugin.config.getBoolean("menu-sounds", true) +set(value) = save("menu-sounds", value) ``` ### Reloading diff --git a/docs/RELEASING.md b/docs/RELEASING.md index 86e6962..427c75c 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -3,8 +3,8 @@ This guide covers how to maintain [CHANGELOG.md](../CHANGELOG.md) during development and how to publish a release on GitHub. The release itself is automated by [.github/workflows/release.yml](../.github/workflows/release.yml) — when a commit that changes `plugin_version` lands on `master`, the workflow builds the plugin, extracts the matching changelog -section, and creates the `vX.Y.Z` tag and GitHub Release with the jar attached. Every push and pull request is also built -by [.github/workflows/build.yml](../.github/workflows/build.yml). +section, and creates the `vX.Y.Z` tag and GitHub Release with the jar attached. Every push and pull request is also +built by [.github/workflows/build.yml](../.github/workflows/build.yml). Releases are created in the repository the commit is merged into. Development happens on a fork, and the release workflow skips forks entirely, so merging a version-bump pull request into the organization repository publishes the diff --git a/docs/UTILITY_GUIDE.md b/docs/UTILITY_GUIDE.md index f315103..9b8db7f 100644 --- a/docs/UTILITY_GUIDE.md +++ b/docs/UTILITY_GUIDE.md @@ -82,6 +82,8 @@ putting them into a MiniMessage string — otherwise a town named `): Boolean { + if (sender !is Player) { + sender.sendRichMessage(sender.tr("command.invites.players-only")) + return true + } + InvitesMenu(sender, null).open() + return true + } +} diff --git a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/config/PluginConfig.kt b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/config/PluginConfig.kt index 2c5d0a5..e9ddcf2 100644 --- a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/config/PluginConfig.kt +++ b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/config/PluginConfig.kt @@ -46,6 +46,16 @@ class PluginConfig(private val plugin: JavaPlugin) { get() = plugin.config.getBoolean("tutorial-join-hint", true) set(value) = save("tutorial-join-hint", value) + /** Whether open menus are re-rendered when Towny changes what they show (`live-menu-refresh`). */ + var liveMenuRefresh: Boolean + get() = plugin.config.getBoolean("live-menu-refresh", true) + set(value) = save("live-menu-refresh", value) + + /** Whether TownyMenu announces invitations, bankruptcy, and ruin in chat (`towny-alerts`). */ + var townyAlerts: Boolean + get() = plugin.config.getBoolean("towny-alerts", true) + set(value) = save("towny-alerts", value) + private fun save(path: String, value: Any) { plugin.config.set(path, value) plugin.saveConfig() diff --git a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/LeaderboardMenu.kt b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/LeaderboardMenu.kt new file mode 100644 index 0000000..dcd07ec --- /dev/null +++ b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/LeaderboardMenu.kt @@ -0,0 +1,201 @@ +package net.trilleo.mc.plugins.townymenu.guis + +import com.palmergames.bukkit.towny.TownyAPI +import com.palmergames.bukkit.towny.TownySettings +import com.palmergames.bukkit.towny.`object`.Nation +import com.palmergames.bukkit.towny.`object`.Resident +import com.palmergames.bukkit.towny.`object`.Town +import com.palmergames.bukkit.towny.permissions.PermissionNodes +import net.trilleo.mc.plugins.townymenu.guis.framework.Icons +import net.trilleo.mc.plugins.townymenu.guis.framework.ListMenu +import net.trilleo.mc.plugins.townymenu.guis.framework.Menu +import net.trilleo.mc.plugins.townymenu.guis.framework.MenuEntry +import net.trilleo.mc.plugins.townymenu.guis.nation.NationInfoMenu +import net.trilleo.mc.plugins.townymenu.guis.resident.ResidentProfileMenu +import net.trilleo.mc.plugins.townymenu.guis.town.TownInfoMenu +import net.trilleo.mc.plugins.townymenu.guis.tutorial.Tutorial +import net.trilleo.mc.plugins.townymenu.utils.TownyUtil +import net.trilleo.mc.plugins.townymenu.utils.tr +import org.bukkit.Material +import org.bukkit.entity.Player + +/** + * The same rankings as Towny's `/towny top`, `/town baltop`, and `/nation baltop`, as clickable lists. + * Server-wide lists hide towns an admin removed from top lists and show at most Towny's `towny_top_size` entries. + */ +class LeaderboardMenu(player: Player, back: Menu) : Menu(player, player.tr("leaderboard.title"), 6, back) { + + private val topSize: Int + get() = TownySettings.getTownyTopSize() + + private val towns: List + get() = TownyAPI.getInstance().towns.filter { it.isVisibleOnTopLists } + + private val nations: List + get() = TownyAPI.getInstance().nations.toList() + + override fun build() { + button(4, Icons.icon(Material.LECTERN, tr("leaderboard.info"), tr("leaderboard.info-description"))) + + val row = layout(19, 20, 21, 22, 23, 24, 25, 28, 29, 30, 31, 32, 33, 34) + row.add( + PermissionNodes.TOWNY_COMMAND_TOWNY_TOP_RESIDENTS, + Icons.icon(Material.BELL, tr("leaderboard.town-residents"), tr("leaderboard.town-residents-description")) + ) { + townList(tr("leaderboard.town-residents")) { + it.numResidents to tr( + "icon.town.residents", + "count" to it.numResidents + ) + } + } + row.add( + PermissionNodes.TOWNY_COMMAND_TOWNY_TOP_RESIDENTS, + Icons.icon( + Material.BEACON, + tr("leaderboard.nation-residents"), + tr("leaderboard.nation-residents-description") + ) + ) { + nationList(tr("leaderboard.nation-residents")) { + it.numResidents to tr( + "icon.town.residents", + "count" to it.numResidents + ) + } + } + row.add( + PermissionNodes.TOWNY_COMMAND_TOWNY_TOP_LAND, + Icons.icon(Material.GRASS_BLOCK, tr("leaderboard.town-land"), tr("leaderboard.town-land-description")) + ) { + townList(tr("leaderboard.town-land")) { + it.numTownBlocks to tr( + "claims.claimed", + "claims" to it.numTownBlocks, + "max" to it.maxTownBlocksAsAString + ) + } + } + row.add( + PermissionNodes.TOWNY_COMMAND_TOWNY_TOP_LAND, + Icons.icon(Material.OAK_SIGN, tr("leaderboard.resident-land"), tr("leaderboard.resident-land-description")) + ) { + residentList( + tr("leaderboard.resident-land"), + TownyAPI.getInstance().residents.filter { it.townBlocks.isNotEmpty() } + ) { it.townBlocks.size.toDouble() to tr("profile.plots-owned", "count" to it.townBlocks.size) } + } + if (TownyUtil.economy) { + row.add( + PermissionNodes.TOWNY_COMMAND_TOWNY_TOP_BALANCE, + Icons.icon( + Material.GOLD_INGOT, + tr("leaderboard.town-balance"), + tr("leaderboard.town-balance-description") + ) + ) { + townList(tr("leaderboard.town-balance")) { + it.account.cachedBalance to tr( + "icon.bank", + "balance" to TownyUtil.balance(it) + ) + } + } + row.add( + PermissionNodes.TOWNY_COMMAND_TOWNY_TOP_BALANCE, + Icons.icon( + Material.GOLD_BLOCK, + tr("leaderboard.nation-balance"), + tr("leaderboard.nation-balance-description") + ) + ) { + nationList(tr("leaderboard.nation-balance")) { + it.account.cachedBalance to tr( + "icon.bank", + "balance" to TownyUtil.balance(it) + ) + } + } + val town = resident?.townOrNull + if (town != null) { + row.add( + Icons.icon( + Material.EMERALD, tr("leaderboard.town-baltop"), + tr("leaderboard.town-baltop-description", "town" to TownyUtil.name(town.name)) + ) + ) { + residentList(tr("leaderboard.town-baltop"), town.residents, limit = false) + } + } + val nation = resident?.nationOrNull + if (nation != null) { + row.add( + PermissionNodes.TOWNY_COMMAND_NATION_BALTOP, + Icons.icon( + Material.EMERALD_BLOCK, tr("leaderboard.nation-baltop"), + tr("leaderboard.nation-baltop-description", "nation" to TownyUtil.name(nation.name)) + ) + ) { + residentList(tr("leaderboard.nation-baltop"), nation.residents, limit = false) + } + } + } + + tutorialButton(53, Tutorial.GETTING_STARTED) + backButton(49) + } + + private fun rank(index: Int) = tr("leaderboard.rank", "rank" to index + 1) + + /** Opens [towns] ranked by the first value of [score], showing its second value as a lore line. */ + private fun > townList(title: String, score: (Town) -> Pair) = + ListMenu(player, title, this) { menu -> + towns.map { it to score(it) }.sortedByDescending { it.second.first }.take(topSize) + .mapIndexed { index, (town, value) -> + MenuEntry({ Icons.town(player, town, "", rank(index), value.second, tr("common.click-details")) }) { + TownInfoMenu(player, town, menu).open() + } + } + }.open() + + private fun > nationList(title: String, score: (Nation) -> Pair) = + ListMenu(player, title, this) { menu -> + nations.map { it to score(it) }.sortedByDescending { it.second.first }.take(topSize) + .mapIndexed { index, (nation, value) -> + MenuEntry({ + Icons.nation( + player, + nation, + "", + rank(index), + value.second, + tr("common.click-details") + ) + }) { + NationInfoMenu(player, nation, menu).open() + } + } + }.open() + + /** + * Opens [residents] ranked by [score], by bank balance unless given. Town and nation baltops pass `limit = false` + * to list every member, like Towny's, instead of stopping at the top-list size. + */ + private fun residentList( + title: String, + residents: Collection, + limit: Boolean = true, + score: (Resident) -> Pair = { resident -> + val balance = resident.accountOrNull?.cachedBalance ?: 0.0 + balance to tr("bank.balance", "balance" to TownyUtil.money(balance)) + }, + ) = ListMenu(player, title, this) { menu -> + residents.map { it to score(it) }.sortedByDescending { it.second.first } + .let { if (limit) it.take(topSize) else it } + .mapIndexed { index, (target, value) -> + MenuEntry({ Icons.resident(player, target, "", rank(index), value.second, tr("common.click-view")) }) { + ResidentProfileMenu(player, target, menu).open() + } + } + }.open() +} diff --git a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/MainMenu.kt b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/MainMenu.kt index 72a094c..1cedcbc 100644 --- a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/MainMenu.kt +++ b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/MainMenu.kt @@ -19,13 +19,13 @@ import org.bukkit.Material import org.bukkit.entity.Player /** The entry point opened by `/townymenu` or sneak + swap-hand. */ -class MainMenu(player: Player) : Menu(player, player.tr("main.title"), 5) { +class MainMenu(player: Player) : Menu(player, player.tr("main.title"), 6) { override fun build() { val resident = resident if (resident == null) { button(22, Icons.icon(Material.BARRIER, tr("main.unregistered"), tr("main.unregistered-hint"))) - return backButton(40) + return backButton(49) } val town = resident.townOrNull val nation = resident.nationOrNull @@ -60,13 +60,21 @@ class MainMenu(player: Player) : Menu(player, player.tr("main.title"), 5) { val plot = TownyAPI.getInstance().getTownBlock(player) button( 24, Icons.icon( - Material.GRASS_BLOCK, tr("main.plot"), tr("main.plot-description"), - tr( - "main.plot-here", - "town" to (plot?.townOrNull?.let { TownyUtil.name(it.name) } ?: tr("main.wilderness"))))) { + Material.GRASS_BLOCK, tr("main.plot"), tr("main.plot-description"), + tr( + "main.plot-here", + "town" to (plot?.townOrNull?.let { TownyUtil.name(it.name) } ?: tr("main.wilderness"))))) { PlotMenu(player, this).open() } + button( + 28, Icons.icon( + Material.CLOCK, tr("main.prices"), tr("main.prices-description"), + tr("town-status.new-day", "time" to TownyUtil.duration(player, TownyUtil.secondsUntilNewDay())) + ) + ) { + PricesMenu(player, this).open() + } button(29, Icons.icon(Material.FILLED_MAP, tr("main.map"), tr("main.map-description"))) { MapMenu( player, @@ -89,6 +97,10 @@ class MainMenu(player: Player) : Menu(player, player.tr("main.title"), 5) { ).open() } + button(34, Icons.icon(Material.LECTERN, tr("main.leaderboards"), tr("main.leaderboards-description"))) { + LeaderboardMenu(player, this).open() + } + val invites = resident.receivedInvites.size + (town?.receivedInvites?.size ?: 0) + (nation?.receivedInvites?.size ?: 0) button( @@ -101,7 +113,7 @@ class MainMenu(player: Player) : Menu(player, player.tr("main.title"), 5) { } if (player.hasPermission(AdminMenu.PERMISSION)) { - button(44, Icons.icon(Material.COMMAND_BLOCK, tr("main.admin"), tr("main.admin-description"))) { + button(53, Icons.icon(Material.COMMAND_BLOCK, tr("main.admin"), tr("main.admin-description"))) { AdminMenu( player, this @@ -109,6 +121,6 @@ class MainMenu(player: Player) : Menu(player, player.tr("main.title"), 5) { } } - backButton(40) + backButton(49) } } diff --git a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/MapMenu.kt b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/MapMenu.kt index 20a44f0..437fe94 100644 --- a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/MapMenu.kt +++ b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/MapMenu.kt @@ -18,7 +18,8 @@ import org.bukkit.inventory.ItemStack /** * A 9×5 chunk map centred on the viewer (north is up). Clicking claimed land - * opens that town; the bottom row claims or unclaims the viewer's own chunk. + * opens that town, right-clicking a plot for sale buys it, and the bottom row + * claims or unclaims the viewer's own chunk. */ class MapMenu(player: Player, back: Menu?) : Menu(player, player.tr("map.title"), 6, back) { @@ -33,8 +34,12 @@ class MapMenu(player: Player, back: Menu?) : Menu(player, player.tr("map.title") val plot = api.getTownBlock(coord) val here = column == 4 && row == 2 val town = plot?.townOrNull - button(row * 9 + column, cell(coord, plot, viewer, here)) { - if (town != null) TownInfoMenu(player, town, this).open() + button(row * 9 + column, cell(coord, plot, viewer, here)) { click -> + if (click.isRightClick && canBuy(plot, viewer)) { + run("towny:plot claim ${coord.worldName} x${coord.x} z${coord.z}", { plot?.residentOrNull }) + } else if (town != null) { + TownInfoMenu(player, town, this).open() + } } } } @@ -69,6 +74,11 @@ class MapMenu(player: Player, back: Menu?) : Menu(player, player.tr("map.title") tutorialButton(53, Tutorial.GETTING_STARTED) } + /** Towny's `/plot claim x z` buys a plot for sale without standing in it. */ + private fun canBuy(plot: TownBlock?, viewer: Resident?): Boolean = + plot != null && plot.isForSale && viewer != null && plot.residentOrNull != viewer && + TownyUtil.can(player, PermissionNodes.TOWNY_COMMAND_PLOT_CLAIM) + private fun cell(coord: WorldCoord, plot: TownBlock?, viewer: Resident?, here: Boolean): ItemStack { val town = plot?.townOrNull val nation = town?.nationOrNull @@ -100,6 +110,7 @@ class MapMenu(player: Player, back: Menu?) : Menu(player, player.tr("map.title") if (plot.isForSale) add(tr("map.for-sale", "price" to TownyUtil.money(plot.plotPrice))) add("") add(tr("map.click-town")) + if (canBuy(plot, viewer)) add(tr("map.right-buy")) } }) glow(plot?.isForSale == true) diff --git a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/PricesMenu.kt b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/PricesMenu.kt new file mode 100644 index 0000000..810964f --- /dev/null +++ b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/PricesMenu.kt @@ -0,0 +1,127 @@ +package net.trilleo.mc.plugins.townymenu.guis + +import com.palmergames.bukkit.towny.TownySettings +import com.palmergames.bukkit.towny.`object`.TownBlockTypeHandler +import com.palmergames.bukkit.towny.permissions.PermissionNodes +import net.trilleo.mc.plugins.townymenu.guis.framework.Icons +import net.trilleo.mc.plugins.townymenu.guis.framework.Menu +import net.trilleo.mc.plugins.townymenu.guis.tutorial.Tutorial +import net.trilleo.mc.plugins.townymenu.utils.TownyUtil +import net.trilleo.mc.plugins.townymenu.utils.tr +import org.bukkit.Material +import org.bukkit.entity.Player +import kotlin.math.roundToLong + +/** + * The time until Towny's next new day and the server's prices, like `/towny time` and `/towny prices`. Costs that + * depend on a town, such as upkeep and plot prices, use the viewer's town when they have one. + */ +class PricesMenu(player: Player, back: Menu) : Menu(player, player.tr("prices.title"), 6, back) { + + override fun build() { + val day = TownySettings.getNewDayTime() + button( + 4, Icons.icon(Material.CLOCK, tr("prices.new-day"), tr("prices.new-day-description"), *buildList { + if (TownyUtil.can(player, PermissionNodes.TOWNY_COMMAND_TOWNY_TIME)) { + add(tr("town-status.new-day", "time" to TownyUtil.duration(player, TownyUtil.secondsUntilNewDay()))) + } + add(tr("tutorial.fact.new-day", "time" to "%02d:%02d".format(day / 3600 % 24, day / 60 % 60))) + add(tr("tutorial.fact.daily-taxes", "value" to TownyUtil.onOff(player, TownySettings.isTaxingDaily()))) + }.toTypedArray()) + ) + if (!TownyUtil.economy) { + button(22, Icons.icon(Material.BARRIER, tr("prices.no-economy"))) + return backButton(49) + } + + val town = resident?.townOrNull + val nation = town?.nationOrNull + val row = layout(19, 20, 21, 22, 23, 24, 25, 28, 29, 30, 31, 32, 33, 34) + + row.add( + Icons.icon( + Material.BELL, tr("prices.founding"), null, + cost("prices.new-town", TownySettings.getNewTownPrice()), + cost("prices.new-nation", TownySettings.getNewNationPrice()), + cost("prices.reclaim", TownySettings.getEcoPriceReclaimTown()), + cost("prices.merge", TownySettings.getBaseCostForTownMerge().toDouble()), + ) + ) + row.add(Icons.icon(Material.GRASS_BLOCK, tr("prices.claims"), null, *buildList { + add(cost("prices.claim", town?.townBlockCost ?: TownySettings.getClaimPrice())) + val increase = TownySettings.getClaimPriceIncreaseValue() + if (increase > 1) { + add(tr("tutorial.fact.claim-increase", "percent" to ((increase - 1) * 100).roundToLong())) + } + add(cost("prices.claim-refund", TownySettings.getClaimRefundPrice())) + add(cost("prices.bonus-claim", town?.bonusBlockCost ?: TownySettings.getPurchasedBonusBlocksCost())) + if (TownySettings.isAllowingOutposts()) { + add(cost("prices.outpost", TownySettings.getOutpostCost())) + if (TownySettings.getPerOutpostUpkeepCost() > 0) { + add(cost("prices.outpost-upkeep", TownySettings.getPerOutpostUpkeepCost())) + } + } + }.toTypedArray())) + row.add(Icons.icon(Material.HOPPER, tr("prices.upkeep"), tr("prices.upkeep-description"), *buildList { + add( + cost( + "prices.town-upkeep", + town?.let { TownySettings.getTownUpkeepCost(it) } ?: TownySettings.getTownUpkeep())) + add( + cost( + "prices.nation-upkeep", + nation?.let { TownySettings.getNationUpkeepCost(it) } ?: TownySettings.getNationUpkeep())) + if (town != null && town.isOverClaimed) { + add(cost("prices.overclaimed-upkeep", TownySettings.getTownPenaltyUpkeepCost(town))) + } + add( + cost( + "prices.town-peaceful", + town?.let { TownySettings.getTownNeutralityCost(it) } ?: TownySettings.getTownNeutralityCost())) + add( + cost( + "prices.nation-peaceful", + nation?.let { TownySettings.getNationNeutralityCost(it) } + ?: TownySettings.getNationNeutralityCost())) + }.toTypedArray())) + if (town != null) { + row.add( + Icons.icon( + Material.EMERALD, tr("prices.plot-prices", "town" to TownyUtil.name(town.name)), null, + cost("prices.plot", town.plotPrice), + cost("prices.shop", town.commercialPlotPrice), + cost("prices.embassy", town.embassyPlotPrice), + ) + ) + row.add( + Icons.icon( + Material.GOLD_NUGGET, tr("prices.taxes", "town" to TownyUtil.name(town.name)), null, + tr( + "prices.resident-tax", + "cost" to if (town.isTaxPercentage) "${town.taxes}%" else TownyUtil.money(town.taxes) + ), + cost("prices.plot-tax", town.plotTax), + cost("prices.shop-tax", town.commercialPlotTax), + cost("prices.embassy-tax", town.embassyPlotTax), + ) + ) + } + row.add( + Icons.icon( + Material.OAK_SIGN, tr("prices.plot-types"), tr("prices.plot-types-description"), + *TownBlockTypeHandler.getTypes().values.sortedBy { it.name }.map { + tr( + "prices.plot-type", + "type" to TownyUtil.plotType(player, it.name), + "cost" to TownyUtil.money(it.cost) + ) + }.toTypedArray() + ) + ) + + tutorialButton(53, Tutorial.ECONOMY) + backButton(49) + } + + private fun cost(key: String, amount: Double): String = tr(key, "cost" to TownyUtil.money(maxOf(amount, 0.0))) +} diff --git a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/admin/AdminMenu.kt b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/admin/AdminMenu.kt index 3622a69..c9e5aab 100644 --- a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/admin/AdminMenu.kt +++ b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/admin/AdminMenu.kt @@ -18,12 +18,12 @@ import org.bukkit.entity.Player * The server admin hub, opened by `/townymenu admin` or the main menu. Only players with * [PERMISSION] may use it; each action still checks the Towny node of the command it runs. */ -class AdminMenu(player: Player, back: Menu?) : Menu(player, player.tr("admin.title"), 4, back) { +class AdminMenu(player: Player, back: Menu?) : Menu(player, player.tr("admin.title"), 5, back) { override fun build() { if (!player.hasPermission(PERMISSION)) { button(13, Icons.icon(Material.BARRIER, tr("menu.no-permission"))) - return backButton(31) + return backButton(40) } button(10, Icons.icon(Material.COMPARATOR, tr("admin.config"), tr("admin.config-description"))) { @@ -45,6 +45,11 @@ class AdminMenu(player: Player, back: Menu?) : Menu(player, player.tr("admin.tit } val towny = TownyAPI.getInstance() + button( + 18, Icons.icon(Material.GRASS_BLOCK, tr("admin-plot.open"), tr("admin-plot.open-description")) + ) { + AdminPlotMenu(player, this).open() + } button( 20, Icons.icon( Material.BELL, tr("admin.towns"), tr("admin.towns-description"), @@ -86,7 +91,11 @@ class AdminMenu(player: Player, back: Menu?) : Menu(player, player.tr("admin.tit AdminResidentListMenu(player, this).open() } - backButton(31) + button(26, Icons.icon(Material.IRON_DOOR, tr("admin-perms.open"), tr("admin-perms.open-description"))) { + AdminPermsMenu(player, this).open() + } + + backButton(40) } private fun worlds(): Menu = ListMenu(player, tr("admin-world.list-title"), this) { list -> diff --git a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/admin/AdminNamesMenu.kt b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/admin/AdminNamesMenu.kt new file mode 100644 index 0000000..d8fccdd --- /dev/null +++ b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/admin/AdminNamesMenu.kt @@ -0,0 +1,51 @@ +package net.trilleo.mc.plugins.townymenu.guis.admin + +import com.palmergames.bukkit.towny.permissions.PermissionNodes +import net.trilleo.mc.plugins.townymenu.guis.framework.Icons +import net.trilleo.mc.plugins.townymenu.guis.framework.Menu +import net.trilleo.mc.plugins.townymenu.guis.framework.MenuEntry +import net.trilleo.mc.plugins.townymenu.guis.framework.PagedMenu +import net.trilleo.mc.plugins.townymenu.utils.TownyUtil +import org.bukkit.Material +import org.bukkit.entity.Player + +/** + * The names an admin `add`/`remove` subcommand holds — outlaws, trusted residents and towns, sanctioned towns. + * Clicking an entry removes it, and the bottom row adds one by name. + * + * @param command the subcommand to add to and remove from, such as `towny:townyadmin town Springfield outlaw` + * @param held reads the current names, fresh on every render + */ +class AdminNamesMenu( + player: Player, + title: String, + back: Menu, + private val node: PermissionNodes, + private val command: String, + private val material: Material, + private val addTitle: String, + private val addLabel: String, + private val held: () -> List, +) : PagedMenu(player, title, back) { + + private fun count() = held().size + + override fun entries(): List = held().sortedBy { it.lowercase() }.map { name -> + MenuEntry({ + Icons.icon(material, "${TownyUtil.name(name)}", null, tr("admin-tools.click-remove")) + }) { + run("$command remove $name", ::count) + } + } + + override fun controls() { + guarded( + 47, node, + Icons.icon(Material.NAME_TAG, tr("admin-tools.add"), tr("admin-tools.add-description")) + ) { + prompt(addTitle, addLabel) { name -> + run("$command add ${TownyUtil.argument(name)}", ::count) + } + } + } +} diff --git a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/admin/AdminNationMenu.kt b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/admin/AdminNationMenu.kt index 3e62d1a..cd87cad 100644 --- a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/admin/AdminNationMenu.kt +++ b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/admin/AdminNationMenu.kt @@ -18,7 +18,7 @@ import org.bukkit.entity.Player /** Manages any nation as an admin (`/townyadmin nation`). */ class AdminNationMenu(player: Player, private val nation: Nation, back: Menu) : - Menu(player, player.tr("admin-nation.title", "nation" to TownyUtil.name(nation.name)), 5, back) { + Menu(player, player.tr("admin-nation.title", "nation" to TownyUtil.name(nation.name)), 6, back) { private val command: String get() = "towny:townyadmin nation ${nation.name}" @@ -29,7 +29,7 @@ class AdminNationMenu(player: Player, private val nation: Nation, back: Menu) : override fun build() { if (!exists) { button(22, Icons.icon(Material.BARRIER, tr("admin.gone"))) - return backButton(40) + return backButton(49) } button(4, Icons.nation(player, nation)) @@ -140,7 +140,13 @@ class AdminNationMenu(player: Player, private val nation: Nation, back: Menu) : run("$command delete", ::exists, returnTo = back ?: this) } - backButton(40) + grid.add( + Icons.icon(Material.ANVIL, tr("admin-nation-tools.open"), tr("admin-nation-tools.open-description")) + ) { + AdminNationToolsMenu(player, nation, this).open() + } + + backButton(49) } /** Lists the nation's towns other than the capital. */ diff --git a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/admin/AdminNationToolsMenu.kt b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/admin/AdminNationToolsMenu.kt new file mode 100644 index 0000000..eb86723 --- /dev/null +++ b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/admin/AdminNationToolsMenu.kt @@ -0,0 +1,111 @@ +package net.trilleo.mc.plugins.townymenu.guis.admin + +import com.palmergames.bukkit.towny.`object`.Nation +import com.palmergames.bukkit.towny.permissions.PermissionNodes +import net.trilleo.mc.plugins.townymenu.guis.common.Pickers +import net.trilleo.mc.plugins.townymenu.guis.common.RankMenu +import net.trilleo.mc.plugins.townymenu.guis.framework.Icons +import net.trilleo.mc.plugins.townymenu.guis.framework.ListMenu +import net.trilleo.mc.plugins.townymenu.guis.framework.Menu +import net.trilleo.mc.plugins.townymenu.guis.framework.MenuEntry +import net.trilleo.mc.plugins.townymenu.utils.TownyUtil +import net.trilleo.mc.plugins.townymenu.utils.tr +import org.bukkit.Material +import org.bukkit.entity.Player + +/** The rest of `/townyadmin nation`: level, merging, transfers, sanctions, ranks, and the town proximity check. */ +class AdminNationToolsMenu(player: Player, private val nation: Nation, back: Menu) : + Menu(player, player.tr("admin-nation-tools.title", "nation" to TownyUtil.name(nation.name)), 6, back) { + + private val command: String + get() = "towny:townyadmin nation ${nation.name}" + + override fun build() { + if (!nation.exists()) { + button(22, Icons.icon(Material.BARRIER, tr("admin.gone"))) + return backButton(49) + } + button(4, Icons.nation(player, nation)) + + val grid = layout(19, 20, 21, 22, 23, 24, 25) + grid.add( + PermissionNodes.TOWNY_COMMAND_TOWNYADMIN_NATION_SETNATIONLEVEL, + Icons.icon( + Material.EXPERIENCE_BOTTLE, tr("admin-nation-tools.level"), + tr("admin-nation-tools.level-description"), + tr("common.current", "value" to nation.levelNumber) + ) + ) { + prompt( + tr("admin-nation-tools.level-title"), + tr("admin-town-tools.level-label"), + tr("admin-town-tools.level-hint"), + maxLength = 5 + ) { level -> + run("$command setnationlevel ${TownyUtil.argument(level)}", { nation.levelNumber }) + } + } + grid.add( + PermissionNodes.TOWNY_COMMAND_TOWNYADMIN_NATION_MERGE, + Icons.icon( + Material.STRUCTURE_VOID, tr("admin-nation-tools.merge"), + tr("admin-nation-tools.merge-description", "nation" to TownyUtil.name(nation.name)), + tr("admin-town-tools.merge-force") + ) + ) { click -> + val force = click.isRightClick + Pickers.nation(this, tr("admin-nation-tools.merge-title"), { it != nation }) { other -> + run("$command ${if (force) "forcemerge" else "merge"} ${other.name}", { nation.numTowns }) + }.open() + } + grid.add( + PermissionNodes.TOWNY_COMMAND_TOWNYADMIN_NATION_TRANSFER, + Icons.icon( + Material.BELL, tr("admin-nation-tools.transfer"), + tr("admin-nation-tools.transfer-description", "nation" to TownyUtil.name(nation.name)) + ) + ) { + Pickers.town(this, tr("admin-nation-tools.transfer-title"), { !nation.hasTown(it) }) { town -> + run("$command transfer ${town.name}", { nation.numTowns }) + }.open() + } + grid.add( + PermissionNodes.TOWNY_COMMAND_TOWNYADMIN_NATION_RECHECK, + Icons.icon( + Material.RECOVERY_COMPASS, tr("admin-nation-tools.recheck"), + tr("admin-nation-tools.recheck-description") + ) + ) { + runAndClose("$command recheck") + } + grid.add( + PermissionNodes.TOWNY_COMMAND_TOWNYADMIN_NATION_SANCTIONTOWN, + Icons.icon( + Material.RED_BANNER, tr("nation-towns.sanctions"), tr("admin-nation-tools.sanctions-description"), + tr("nation-towns.sanctions-count", "count" to nation.sanctionedTowns.size) + ) + ) { + AdminNamesMenu( + player, tr("nation-sanctions.title", "nation" to TownyUtil.name(nation.name)), this, + PermissionNodes.TOWNY_COMMAND_TOWNYADMIN_NATION_SANCTIONTOWN, "$command sanctiontown", Material.BELL, + tr("nation-sanctions.add-title"), tr("no-town.town-name") + ) { nation.sanctionedTowns.map { it.name } }.open() + } + grid.add( + PermissionNodes.TOWNY_COMMAND_TOWNYADMIN_NATION_RANK, + Icons.icon(Material.NAME_TAG, tr("admin-nation-tools.ranks"), tr("admin-nation-tools.ranks-description")) + ) { + lateinit var list: Menu + list = ListMenu(player, tr("admin-town-tools.ranks-title"), this) { menu -> + nation.residents.sortedBy { it.name.lowercase() }.map { resident -> + MenuEntry({ Icons.resident(player, resident, "", tr("picker.select")) }) { + RankMenu.create(player, resident, nation = true, back = menu, admin = nation.name).open() + } + } + } + list.open() + } + + backButton(49) + } +} diff --git a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/admin/AdminPermGroupMenu.kt b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/admin/AdminPermGroupMenu.kt new file mode 100644 index 0000000..d98b0ec --- /dev/null +++ b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/admin/AdminPermGroupMenu.kt @@ -0,0 +1,44 @@ +package net.trilleo.mc.plugins.townymenu.guis.admin + +import com.palmergames.bukkit.towny.permissions.PermissionNodes +import com.palmergames.bukkit.towny.permissions.TownyPerms +import net.trilleo.mc.plugins.townymenu.guis.framework.Icons +import net.trilleo.mc.plugins.townymenu.guis.framework.Menu +import net.trilleo.mc.plugins.townymenu.guis.framework.MenuEntry +import net.trilleo.mc.plugins.townymenu.guis.framework.PagedMenu +import net.trilleo.mc.plugins.townymenu.utils.TownyUtil +import net.trilleo.mc.plugins.townymenu.utils.itemStack +import net.trilleo.mc.plugins.townymenu.utils.tr +import org.bukkit.Material +import org.bukkit.entity.Player + +/** The permission nodes of one `townyperms.yml` group, such as `towns.mayor` or `towns.ranks.assistant`. */ +class AdminPermGroupMenu(player: Player, private val group: String, back: Menu) : + PagedMenu(player, player.tr("admin-perms.group-title", "group" to TownyUtil.text(group)), back) { + + private val command = "towny:townyadmin townyperms group $group" + + private fun nodes(): List = TownyPerms.getPermsOfGroup(group) + + override fun entries(): List = nodes().sorted().map { node -> + MenuEntry({ + itemStack(Material.PAPER) { + name("${TownyUtil.text(node)}") + lore(tr("admin-perms.click-remove")) + } + }) { + run("$command removeperm $node", { nodes().size }) + } + } + + override fun controls() { + guarded( + 47, PermissionNodes.TOWNY_COMMAND_TOWNYADMIN_TOWNYPERMS, + Icons.icon(Material.NAME_TAG, tr("admin-perms.add-node"), tr("admin-perms.add-node-description")) + ) { + prompt(tr("admin-perms.add-node"), tr("admin-perms.node-label"), maxLength = 128) { node -> + run("$command addperm ${TownyUtil.argument(node)}", { nodes().size }) + } + } + } +} diff --git a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/admin/AdminPermsMenu.kt b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/admin/AdminPermsMenu.kt new file mode 100644 index 0000000..f1ac161 --- /dev/null +++ b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/admin/AdminPermsMenu.kt @@ -0,0 +1,73 @@ +package net.trilleo.mc.plugins.townymenu.guis.admin + +import com.palmergames.bukkit.towny.permissions.PermissionNodes +import com.palmergames.bukkit.towny.permissions.TownyPerms +import net.trilleo.mc.plugins.townymenu.guis.framework.Icons +import net.trilleo.mc.plugins.townymenu.guis.framework.ListMenu +import net.trilleo.mc.plugins.townymenu.guis.framework.Menu +import net.trilleo.mc.plugins.townymenu.guis.framework.MenuEntry +import net.trilleo.mc.plugins.townymenu.utils.TownyUtil +import net.trilleo.mc.plugins.townymenu.utils.itemStack +import net.trilleo.mc.plugins.townymenu.utils.tr +import org.bukkit.Material +import org.bukkit.entity.Player + +/** + * Towny's `townyperms.yml` (`/townyadmin townyperms`): the permission groups every resident, mayor, and nation + * leader gets, and the town and nation ranks a mayor or leader may hand out. + */ +class AdminPermsMenu(player: Player, back: Menu) : Menu(player, player.tr("admin-perms.title"), 6, back) { + + override fun build() { + button(4, Icons.icon(Material.IRON_DOOR, tr("admin-perms.info"), tr("admin-perms.info-description"))) + + val grid = layout(20, 21, 22, 23, 24) + grid.add( + PermissionNodes.TOWNY_COMMAND_TOWNYADMIN_TOWNYPERMS, + Icons.icon( + Material.BOOKSHELF, tr("admin-perms.groups"), tr("admin-perms.groups-description"), + tr("admin-perms.groups-count", "count" to groups().size) + ) + ) { + groupList().open() + } + grid.add( + PermissionNodes.TOWNY_COMMAND_TOWNYADMIN_TOWNYPERMS, + Icons.icon( + Material.NAME_TAG, tr("admin-perms.town-ranks"), tr("admin-perms.town-ranks-description"), + tr("admin-perms.ranks-count", "count" to TownyPerms.getTownRanks().size) + ) + ) { + AdminRanksMenu(player, nation = false, back = this).open() + } + grid.add( + PermissionNodes.TOWNY_COMMAND_TOWNYADMIN_TOWNYPERMS, + Icons.icon( + Material.BEACON, tr("admin-perms.nation-ranks"), tr("admin-perms.nation-ranks-description"), + tr("admin-perms.ranks-count", "count" to TownyPerms.getNationRanks().size) + ) + ) { + AdminRanksMenu(player, nation = true, back = this).open() + } + + backButton(49) + } + + /** Groups that actually hold permissions; `townyperms.yml` also has empty parent sections such as `towns`. */ + private fun groups(): List = + TownyPerms.getGroupList().filter { TownyPerms.getPermsOfGroup(it).isNotEmpty() }.sorted() + + private fun groupList(): Menu = ListMenu(player, tr("admin-perms.groups"), this) { menu -> + groups().map { group -> + MenuEntry({ + itemStack(Material.BOOKSHELF) { + name("${TownyUtil.text(group)}") + lore( + tr("admin-perms.node-count", "count" to TownyPerms.getPermsOfGroup(group).size), + tr("common.click-details"), + ) + } + }) { AdminPermGroupMenu(player, group, menu).open() } + } + } +} diff --git a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/admin/AdminPlotMenu.kt b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/admin/AdminPlotMenu.kt new file mode 100644 index 0000000..e25486b --- /dev/null +++ b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/admin/AdminPlotMenu.kt @@ -0,0 +1,95 @@ +package net.trilleo.mc.plugins.townymenu.guis.admin + +import com.palmergames.bukkit.towny.TownyAPI +import com.palmergames.bukkit.towny.`object`.TownBlock +import com.palmergames.bukkit.towny.`object`.WorldCoord +import com.palmergames.bukkit.towny.permissions.PermissionNodes +import net.trilleo.mc.plugins.townymenu.guis.common.Pickers +import net.trilleo.mc.plugins.townymenu.guis.framework.Icons +import net.trilleo.mc.plugins.townymenu.guis.framework.Menu +import net.trilleo.mc.plugins.townymenu.utils.TownyUtil +import net.trilleo.mc.plugins.townymenu.utils.tr +import org.bukkit.Material +import org.bukkit.entity.Player + +/** `/townyadmin plot` and the plot half of `/townyadmin set`, for the plot the admin is standing in. */ +class AdminPlotMenu(player: Player, back: Menu) : Menu(player, player.tr("admin-plot.title"), 6, back) { + + private val plot: TownBlock? + get() = TownyAPI.getInstance().getTownBlock(player) + + override fun build() { + val plot = plot + val coord = WorldCoord.parseWorldCoord(player) + if (plot == null) { + button( + 4, Icons.icon( + Material.OAK_SAPLING, "${tr("main.wilderness")}", tr("admin-plot.wilderness-description"), + tr("map.chunk", "x" to coord.x, "z" to coord.z) + ) + ) + } else { + val owner = plot.residentOrNull + button( + 4, Icons.icon( + Material.GRASS_BLOCK, + "${if (plot.name.isNullOrBlank()) tr("plot.unnamed") else TownyUtil.text(plot.name)}", + null, + tr("icon.resident.town", "town" to (plot.townOrNull?.let { TownyUtil.name(it.name) } ?: "-")), + tr("map.owner", "owner" to (owner?.let { TownyUtil.name(it.name) } ?: tr("map.owner-town"))), + tr("map.type", "type" to TownyUtil.plotType(player, plot.type.name)), + tr("map.chunk", "x" to plot.x, "z" to plot.z), + tr("plot.claimed", "date" to TownyUtil.date(plot.claimedAt)), + ) + ) + } + + val grid = layout(20, 21, 22, 23, 24) + grid.add( + PermissionNodes.TOWNY_COMMAND_TOWNYADMIN_PLOT_CLAIM, + Icons.icon(Material.PLAYER_HEAD, tr("admin-plot.claim"), tr("admin-plot.claim-description")) + ) { + Pickers.resident(this, tr("admin-plot.claim-title"), { true }) { target -> + run("towny:townyadmin plot claim ${target.name}", { this.plot?.residentOrNull }) + }.open() + } + if (plot != null) { + grid.add( + PermissionNodes.TOWNY_COMMAND_TOWNYADMIN_PLOT_TRUST, + Icons.icon( + Material.TRIPWIRE_HOOK, tr("plot.trusted"), tr("admin-plot.trust-description"), + tr("plot.trusted-count", "count" to plot.trustedResidents.size) + ) + ) { + AdminNamesMenu( + player, tr("plot-trust.title"), this, PermissionNodes.TOWNY_COMMAND_TOWNYADMIN_PLOT_TRUST, + "towny:townyadmin plot trust", Material.PLAYER_HEAD, tr("plot-trust.trust-title"), + tr("common.player-name") + ) { this.plot?.trustedResidents?.map { it.name }.orEmpty() }.open() + } + grid.add( + PermissionNodes.TOWNY_COMMAND_TOWNYADMIN_SET_PLOT, + Icons.icon(Material.BELL, tr("admin-plot.move"), tr("admin-plot.move-description")) + ) { + Pickers.town(this, tr("admin-plot.move-title"), { it != this.plot?.townOrNull }) { town -> + run("towny:townyadmin set plot ${town.name}", { this.plot?.townOrNull }) + }.open() + } + } + grid.add( + PermissionNodes.TOWNY_COMMAND_TOWNYADMIN_UNCLAIM, + Icons.icon(Material.COARSE_DIRT, tr("admin-plot.unclaim"), tr("admin-plot.unclaim-description")) + ) { click -> + if (click.isRightClick) { + prompt(tr("admin-plot.unclaim-title"), tr("claims.radius"), initial = "1", maxLength = 3) { radius -> + run("towny:townyadmin unclaim rect ${TownyUtil.argument(radius)}", { this.plot?.townOrNull }) + } + } else { + run("towny:townyadmin unclaim", { this.plot?.townOrNull }) + } + } + button(50, Icons.icon(Material.CLOCK, tr("map.refresh"), tr("plot.refresh-description"))) { render() } + + backButton(49) + } +} diff --git a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/admin/AdminRanksMenu.kt b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/admin/AdminRanksMenu.kt new file mode 100644 index 0000000..ebab825 --- /dev/null +++ b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/admin/AdminRanksMenu.kt @@ -0,0 +1,74 @@ +package net.trilleo.mc.plugins.townymenu.guis.admin + +import com.palmergames.bukkit.towny.permissions.PermissionNodes +import com.palmergames.bukkit.towny.permissions.TownyPerms +import net.trilleo.mc.plugins.townymenu.guis.common.Pickers +import net.trilleo.mc.plugins.townymenu.guis.framework.Icons +import net.trilleo.mc.plugins.townymenu.guis.framework.Menu +import net.trilleo.mc.plugins.townymenu.guis.framework.MenuEntry +import net.trilleo.mc.plugins.townymenu.guis.framework.PagedMenu +import net.trilleo.mc.plugins.townymenu.utils.TownyUtil +import net.trilleo.mc.plugins.townymenu.utils.itemStack +import net.trilleo.mc.plugins.townymenu.utils.tr +import org.bukkit.Material +import org.bukkit.entity.Player + +/** + * The town or nation ranks a mayor or nation leader can hand out + * (`/townyadmin townyperms townrank|nationrank`), and the permissions each one grants. + */ +class AdminRanksMenu(player: Player, private val nation: Boolean, back: Menu) : PagedMenu( + player, + player.tr(if (nation) "admin-perms.nation-ranks" else "admin-perms.town-ranks"), + back, +) { + + private val command = "towny:townyadmin townyperms ${if (nation) "nationrank" else "townrank"}" + + private val group = if (nation) "nations.ranks" else "towns.ranks" + + private fun ranks(): List = if (nation) TownyPerms.getNationRanks() else TownyPerms.getTownRanks() + + override fun entries(): List = ranks().sorted().map { rank -> + val nodes = + if (nation) TownyPerms.getNationRankPermissions(rank) else TownyPerms.getTownRankPermissions(rank) + MenuEntry({ + itemStack(Material.NAME_TAG) { + name("${TownyUtil.name(rank)}") + lore( + tr("admin-perms.node-count", "count" to nodes.size), + tr("common.left-details"), + tr("admin-perms.right-remove"), + ) + } + }) { click -> + if (click.isRightClick) { + run("$command removerank $rank", { ranks().size }) + } else { + AdminPermGroupMenu(player, "$group.$rank", this).open() + } + } + } + + override fun controls() { + guarded( + 47, PermissionNodes.TOWNY_COMMAND_TOWNYADMIN_TOWNYPERMS, + Icons.icon(Material.PAPER, tr("admin-perms.add-rank"), tr("admin-perms.add-rank-description")) + ) { + prompt(tr("admin-perms.add-rank"), tr("admin-perms.rank-label")) { rank -> + run("$command addrank ${TownyUtil.nameArgument(rank)}", { ranks().size }) + } + } + guarded( + 48, PermissionNodes.TOWNY_COMMAND_TOWNYADMIN_TOWNYPERMS, + Icons.icon(Material.WRITABLE_BOOK, tr("admin-perms.rename-rank"), tr("admin-perms.rename-rank-description")) + ) { + val options = ranks().sorted().map { it to "${TownyUtil.name(it)}" } + Pickers.option(this, tr("admin-perms.rename-rank"), options, null, Material.NAME_TAG) { rank -> + prompt(tr("admin-perms.rename-rank"), tr("common.new-name"), initial = rank) { renamed -> + run("$command renamerank $rank ${TownyUtil.nameArgument(renamed)}", { ranks().sorted() }) + } + }.open() + } + } +} diff --git a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/admin/AdminResidentMenu.kt b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/admin/AdminResidentMenu.kt index e212af9..4ddefef 100644 --- a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/admin/AdminResidentMenu.kt +++ b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/admin/AdminResidentMenu.kt @@ -13,7 +13,7 @@ import org.bukkit.entity.Player /** Manages any resident as an admin (`/townyadmin resident`, `/townyadmin set`). */ class AdminResidentMenu(player: Player, private val target: Resident, back: Menu) : - Menu(player, player.tr("admin-resident.title", "name" to TownyUtil.name(target.name)), 5, back) { + Menu(player, player.tr("admin-resident.title", "name" to TownyUtil.name(target.name)), 6, back) { private val exists: Boolean get() = TownyAPI.getInstance().getResident(target.uuid) != null @@ -21,7 +21,7 @@ class AdminResidentMenu(player: Player, private val target: Resident, back: Menu override fun build() { if (!exists) { button(22, Icons.icon(Material.BARRIER, tr("admin.gone"))) - return backButton(40) + return backButton(49) } button(4, Icons.resident(player, target, *buildList { @@ -126,7 +126,7 @@ class AdminResidentMenu(player: Player, private val target: Resident, back: Menu run("towny:townyadmin resident ${target.name} delete", ::exists, returnTo = back ?: this) } - backButton(40) + backButton(49) } private fun editTitle(field: String, clear: Boolean, title: String, label: String, current: () -> String) { diff --git a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/admin/AdminServerMenu.kt b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/admin/AdminServerMenu.kt index c6d6e5c..e0933c0 100644 --- a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/admin/AdminServerMenu.kt +++ b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/admin/AdminServerMenu.kt @@ -3,15 +3,17 @@ package net.trilleo.mc.plugins.townymenu.guis.admin import com.palmergames.bukkit.towny.TownySettings import com.palmergames.bukkit.towny.permissions.PermissionNodes import net.kyori.adventure.text.minimessage.MiniMessage +import net.trilleo.mc.plugins.townymenu.guis.common.Pickers import net.trilleo.mc.plugins.townymenu.guis.framework.Icons import net.trilleo.mc.plugins.townymenu.guis.framework.Menu import net.trilleo.mc.plugins.townymenu.utils.DialogUtil +import net.trilleo.mc.plugins.townymenu.utils.TownyUtil import net.trilleo.mc.plugins.townymenu.utils.tr import org.bukkit.Material import org.bukkit.entity.Player /** Server-wide Towny actions (`/townyadmin`): new day, backups, reloads, and global toggles. */ -class AdminServerMenu(player: Player, back: Menu) : Menu(player, player.tr("admin-server.title"), 5, back) { +class AdminServerMenu(player: Player, back: Menu) : Menu(player, player.tr("admin-server.title"), 6, back) { override fun build() { guarded( @@ -59,6 +61,30 @@ class AdminServerMenu(player: Player, back: Menu) : Menu(player, player.tr("admi runAndClose("towny:townyadmin checkoutposts") } + guarded( + 15, PermissionNodes.TOWNY_COMMAND_TOWNYADMIN_PURGE, + Icons.icon(Material.BONE, tr("admin-server.purge"), tr("admin-server.purge-description")) + ) { + prompt( + tr("admin-server.purge"), + tr("admin-server.purge-label"), + tr("admin-server.purge-hint"), + initial = "90", + maxLength = 5 + ) { days -> + runAndClose("towny:townyadmin purge ${TownyUtil.argument(days)}") + } + } + guarded( + 16, PermissionNodes.TOWNY_COMMAND_TOWNYADMIN_CHECKPERM, + Icons.icon(Material.SPYGLASS, tr("admin-server.check-perm"), tr("admin-server.check-perm-description")) + ) { + Pickers.resident(this, tr("admin-server.check-perm"), { it.isOnline }) { target -> + prompt(tr("admin-server.check-perm"), tr("admin-perms.node-label"), maxLength = 128) { node -> + runAndClose("towny:townyadmin checkperm ${target.name} ${TownyUtil.argument(node)}") + } + }.open() + } reload( 19, Material.COMPARATOR, @@ -89,6 +115,24 @@ class AdminServerMenu(player: Player, back: Menu) : Menu(player, player.tr("admi tr("admin-server.reload-all-description") ) + if (TownyUtil.economy) { + guarded( + 24, PermissionNodes.TOWNY_COMMAND_TOWNYADMIN_ECO_DEPOSITALL, + Icons.icon( + Material.GOLD_BLOCK, + tr("admin-server.deposit-all"), + tr("admin-server.deposit-all-description") + ) + ) { + prompt( + tr("admin-server.deposit-all"), + tr("common.amount"), + tr("admin-server.deposit-all-hint") + ) { amount -> + runAndClose("towny:townyadmin eco depositall ${TownyUtil.argument(amount)}") + } + } + } toggle( 28, Material.GOLD_INGOT, @@ -138,7 +182,7 @@ class AdminServerMenu(player: Player, back: Menu) : Menu(player, player.tr("admi PermissionNodes.TOWNY_COMMAND_TOWNYADMIN_TOGGLE_REGENERATIONS ) - backButton(40) + backButton(49) } private fun reload(slot: Int, material: Material, target: String, name: String, description: String) { diff --git a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/admin/AdminTownMenu.kt b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/admin/AdminTownMenu.kt index 2a5c149..5656d78 100644 --- a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/admin/AdminTownMenu.kt +++ b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/admin/AdminTownMenu.kt @@ -39,7 +39,7 @@ class AdminTownMenu(player: Player, private val town: Town, back: Menu) : ) ) - val grid = layout(19, 20, 21, 22, 23, 24, 25, 28, 29, 30, 31, 32, 33, 34, 37, 38, 39, 40, 41, 42, 43) + val grid = layout(19, 20, 21, 22, 23, 24, 25, 28, 29, 30, 31, 32, 33, 34) grid.add( PermissionNodes.TOWNY_COMMAND_TOWNYADMIN_TOWN_SPAWN, @@ -183,6 +183,12 @@ class AdminTownMenu(player: Player, private val town: Town, back: Menu) : run("$command delete", ::exists, returnTo = back ?: this) } + grid.add( + Icons.icon(Material.ANVIL, tr("admin-town-tools.open"), tr("admin-town-tools.open-description")) + ) { + AdminTownToolsMenu(player, town, this).open() + } + backButton(49) } diff --git a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/admin/AdminTownToolsMenu.kt b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/admin/AdminTownToolsMenu.kt new file mode 100644 index 0000000..58824d2 --- /dev/null +++ b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/admin/AdminTownToolsMenu.kt @@ -0,0 +1,167 @@ +package net.trilleo.mc.plugins.townymenu.guis.admin + +import com.palmergames.bukkit.towny.`object`.Resident +import com.palmergames.bukkit.towny.`object`.Town +import com.palmergames.bukkit.towny.permissions.PermissionNodes +import net.trilleo.mc.plugins.townymenu.guis.common.Pickers +import net.trilleo.mc.plugins.townymenu.guis.common.RankMenu +import net.trilleo.mc.plugins.townymenu.guis.framework.Icons +import net.trilleo.mc.plugins.townymenu.guis.framework.ListMenu +import net.trilleo.mc.plugins.townymenu.guis.framework.Menu +import net.trilleo.mc.plugins.townymenu.guis.framework.MenuEntry +import net.trilleo.mc.plugins.townymenu.utils.TownyUtil +import net.trilleo.mc.plugins.townymenu.utils.tr +import org.bukkit.Material +import org.bukkit.entity.Player + +/** The rest of `/townyadmin town`: level, merging, sale, outlaws, trust, and ranks. */ +class AdminTownToolsMenu(player: Player, private val town: Town, back: Menu) : + Menu(player, player.tr("admin-town-tools.title", "town" to TownyUtil.name(town.name)), 6, back) { + + private val command: String + get() = "towny:townyadmin town ${town.name}" + + override fun build() { + if (!town.exists()) { + button(22, Icons.icon(Material.BARRIER, tr("admin.gone"))) + return backButton(49) + } + button(4, Icons.town(player, town)) + + val grid = layout(19, 20, 21, 22, 23, 24, 25, 28, 29, 30, 31, 32, 33, 34) + grid.add( + PermissionNodes.TOWNY_COMMAND_TOWNYADMIN_TOWN_SETTOWNLEVEL, + Icons.icon( + Material.EXPERIENCE_BOTTLE, tr("admin-town-tools.level"), tr("admin-town-tools.level-description"), + tr("common.current", "value" to town.levelNumber) + ) + ) { + prompt( + tr("admin-town-tools.level-title"), + tr("admin-town-tools.level-label"), + tr("admin-town-tools.level-hint"), + maxLength = 5 + ) { level -> + run("$command settownlevel ${TownyUtil.argument(level)}", { town.levelNumber }) + } + } + grid.add( + PermissionNodes.TOWNY_COMMAND_TOWNYADMIN_TOWN_MERGE, + Icons.icon( + Material.STRUCTURE_VOID, tr("admin-town-tools.merge"), + tr("admin-town-tools.merge-description", "town" to TownyUtil.name(town.name)), + tr("admin-town-tools.merge-force") + ) + ) { click -> + val force = click.isRightClick + Pickers.town(this, tr("admin-town-tools.merge-title"), { it != town }) { other -> + run("$command ${if (force) "forcemerge" else "merge"} ${other.name}", { town.numResidents }) + }.open() + } + if (TownyUtil.economy) { + if (town.isForSale) { + grid.add( + PermissionNodes.TOWNY_COMMAND_TOWNYADMIN_TOWN_NOTFORSALE, + Icons.icon( + Material.RED_BANNER, tr("admin-town-tools.not-for-sale"), + tr("admin-town-tools.not-for-sale-description"), + tr("common.price", "price" to TownyUtil.money(town.forSalePrice)) + ) + ) { + run("$command notforsale", { town.isForSale }) + } + } else { + grid.add( + PermissionNodes.TOWNY_COMMAND_TOWNYADMIN_TOWN_FORSALE, + Icons.icon( + Material.GREEN_BANNER, tr("admin-town-tools.for-sale"), + tr("admin-town-tools.for-sale-description") + ) + ) { + prompt(tr("admin-town-tools.for-sale-title"), tr("common.price-label"), initial = "0") { price -> + run("$command forsale ${TownyUtil.argument(price)}", { town.isForSale }) + } + } + } + } + if (town.isConquered) { + grid.add( + PermissionNodes.TOWNY_COMMAND_TOWNYADMIN_TOWN_TOGGLE, + Icons.icon( + Material.IRON_SWORD, tr("admin-town-tools.unconquer"), + tr("admin-town-tools.unconquer-description"), + tr("town-status.conquered-days", "days" to town.conqueredDays) + ) + ) { + run("$command toggle conquered off", { town.isConquered }) + } + } + grid.add( + PermissionNodes.TOWNY_COMMAND_TOWNYADMIN_TOWN_CHECKOUTPOSTS, + Icons.icon( + Material.RECOVERY_COMPASS, tr("admin-server.check-outposts"), + tr("admin-town-tools.check-outposts-description") + ) + ) { + runAndClose("$command checkoutposts") + } + grid.add( + PermissionNodes.TOWNY_COMMAND_TOWNYADMIN_TOWN_RANK, + Icons.icon(Material.NAME_TAG, tr("admin-town-tools.ranks"), tr("admin-town-tools.ranks-description")) + ) { + members(town.residents.toList()) { target, list -> + RankMenu.create(player, target, nation = false, back = list, admin = town.name).open() + } + } + grid.add( + PermissionNodes.TOWNY_COMMAND_TOWNYADMIN_TOWN_OUTLAW, + Icons.icon( + Material.IRON_BARS, tr("town.outlaws"), tr("admin-town-tools.outlaws-description"), + tr("town.outlaws-count", "count" to town.outlaws.size) + ) + ) { + AdminNamesMenu( + player, tr("outlaws.title"), this, PermissionNodes.TOWNY_COMMAND_TOWNYADMIN_TOWN_OUTLAW, + "$command outlaw", Material.PLAYER_HEAD, tr("outlaws.declare"), tr("common.player-name") + ) { town.outlaws.map { it.name } }.open() + } + grid.add( + PermissionNodes.TOWNY_COMMAND_TOWNYADMIN_TOWN_TRUST, + Icons.icon( + Material.TRIPWIRE_HOOK, tr("common.trusted"), tr("admin-town-tools.trust-description"), + tr("plot.trusted-count", "count" to town.trustedResidents.size) + ) + ) { + AdminNamesMenu( + player, tr("common.trusted-title"), this, PermissionNodes.TOWNY_COMMAND_TOWNYADMIN_TOWN_TRUST, + "$command trust", Material.PLAYER_HEAD, tr("common.trust-title"), tr("common.player-name") + ) { town.trustedResidents.map { it.name } }.open() + } + grid.add( + PermissionNodes.TOWNY_COMMAND_TOWNYADMIN_TOWN_TRUSTTOWN, + Icons.icon( + Material.BELL, tr("admin-town-tools.trusted-towns"), tr("admin-town-tools.trusted-towns-description"), + tr("nation.towns-count", "count" to town.trustedTowns.size) + ) + ) { + AdminNamesMenu( + player, tr("admin-town-tools.trusted-towns"), this, + PermissionNodes.TOWNY_COMMAND_TOWNYADMIN_TOWN_TRUSTTOWN, "$command trusttown", Material.BELL, + tr("admin-town-tools.trust-town-title"), tr("no-town.town-name") + ) { town.trustedTowns.map { it.name } }.open() + } + + backButton(49) + } + + /** Picks one of [residents], including those offline, and opens [onPick] with the list as its back button. */ + private fun members(residents: List, onPick: (Resident, Menu) -> Unit) { + lateinit var list: Menu + list = ListMenu(player, tr("admin-town-tools.ranks-title"), this) { menu -> + residents.sortedBy { it.name.lowercase() }.map { resident -> + MenuEntry({ Icons.resident(player, resident, "", tr("picker.select")) }) { onPick(resident, menu) } + } + } + list.open() + } +} diff --git a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/admin/AdminWorldMenu.kt b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/admin/AdminWorldMenu.kt index a93f992..488fc6c 100644 --- a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/admin/AdminWorldMenu.kt +++ b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/admin/AdminWorldMenu.kt @@ -14,7 +14,7 @@ import org.bukkit.entity.Player /** One world's Towny settings (`/townyworld`): toggles, wilderness permissions and name, and resetting to defaults. */ class AdminWorldMenu(player: Player, private val world: TownyWorld, back: Menu) : - Menu(player, player.tr("admin-world.title", "world" to TownyUtil.text(world.name)), 4, back) { + Menu(player, player.tr("admin-world.title", "world" to TownyUtil.text(world.name)), 5, back) { private val command = "towny:townyworld ${world.name}" @@ -66,7 +66,7 @@ class AdminWorldMenu(player: Player, private val world: TownyWorld, back: Menu) } } - backButton(31) + backButton(40) } private fun snapshot(): List = toggleList().map { it.value() } + world.unclaimedZoneName diff --git a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/admin/PluginSettingsMenu.kt b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/admin/PluginSettingsMenu.kt index 514403e..f586718 100644 --- a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/admin/PluginSettingsMenu.kt +++ b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/admin/PluginSettingsMenu.kt @@ -11,15 +11,16 @@ import org.bukkit.Material import org.bukkit.entity.Player /** Edits TownyMenu's own `config.yml`; every change is saved and reloaded like `/townymenu reload`. */ -class PluginSettingsMenu(player: Player, back: Menu) : Menu(player, player.tr("admin-plugin.title"), 3, back) { +class PluginSettingsMenu(player: Player, back: Menu) : Menu(player, player.tr("admin-plugin.title"), 4, back) { private val config get() = Main.instance.pluginConfig override fun build() { + val row = layout(10, 11, 12, 13, 14, 15, 16) val language = config.language - button( - 10, Icons.icon( + row.add( + Icons.icon( Material.BOOK, tr("admin-plugin.language"), tr("admin-plugin.language-description"), tr( "common.current", @@ -39,8 +40,8 @@ class PluginSettingsMenu(player: Player, back: Menu) : Menu(player, player.tr("a apply() }.open() } - button( - 12, Icons.icon( + row.add( + Icons.icon( Material.NAME_TAG, tr("admin-plugin.prefix"), tr("admin-plugin.prefix-description"), tr("common.current", "value" to config.messagePrefix), tr("common.click-change") ) @@ -55,8 +56,8 @@ class PluginSettingsMenu(player: Player, back: Menu) : Menu(player, player.tr("a apply() } } - button( - 14, Icons.toggle( + row.add( + Icons.toggle( player, Material.SHIELD, tr("admin-plugin.shortcut"), config.sneakSwapHandShortcut, tr("admin-plugin.shortcut-description") ) @@ -64,8 +65,8 @@ class PluginSettingsMenu(player: Player, back: Menu) : Menu(player, player.tr("a config.sneakSwapHandShortcut = !config.sneakSwapHandShortcut apply() } - button( - 16, Icons.toggle( + row.add( + Icons.toggle( player, Material.KNOWLEDGE_BOOK, tr("admin-plugin.join-hint"), config.tutorialJoinHint, tr("admin-plugin.join-hint-description") ) @@ -73,7 +74,25 @@ class PluginSettingsMenu(player: Player, back: Menu) : Menu(player, player.tr("a config.tutorialJoinHint = !config.tutorialJoinHint apply() } - backButton(22) + row.add( + Icons.toggle( + player, Material.CLOCK, tr("admin-plugin.live-refresh"), config.liveMenuRefresh, + tr("admin-plugin.live-refresh-description") + ) + ) { + config.liveMenuRefresh = !config.liveMenuRefresh + apply() + } + row.add( + Icons.toggle( + player, Material.PAPER, tr("admin-plugin.alerts"), config.townyAlerts, + tr("admin-plugin.alerts-description") + ) + ) { + config.townyAlerts = !config.townyAlerts + apply() + } + backButton(31) } private fun apply() { diff --git a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/common/BankMenu.kt b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/common/BankMenu.kt index 4863c5b..1058c13 100644 --- a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/common/BankMenu.kt +++ b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/common/BankMenu.kt @@ -17,7 +17,7 @@ class BankMenu( player: Player, private val government: Government, back: Menu, -) : Menu(player, player.tr("bank.title", "name" to TownyUtil.name(government.name)), 3, back) { +) : Menu(player, player.tr("bank.title", "name" to TownyUtil.name(government.name)), 4, back) { private val isNation = government is Nation private val command = if (isNation) "towny:nation" else "towny:town" @@ -64,8 +64,8 @@ class BankMenu( ) { runAndClose("$command bankhistory") } - tutorialButton(26, Tutorial.ECONOMY) - backButton(22) + tutorialButton(35, Tutorial.ECONOMY) + backButton(31) } private fun balance(): Double = government.account.holdingBalance diff --git a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/common/PermissionMenu.kt b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/common/PermissionMenu.kt index a13baf5..ba81a03 100644 --- a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/common/PermissionMenu.kt +++ b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/common/PermissionMenu.kt @@ -17,6 +17,7 @@ import org.bukkit.entity.Player * * @param command the Towny set-perm command, e.g. `towny:town set perm` * @param personal label the levels Friends/Town (resident-owned land) instead of Residents/Nation + * @param overrides opens the per-player permission overrides of this land, when it has them * @param permissions reads the current permissions; `null` when the land no longer exists */ class PermissionMenu( @@ -26,6 +27,7 @@ class PermissionMenu( private val command: String, private val node: PermissionNodes, private val personal: Boolean, + private val overrides: ((Menu) -> Menu)? = null, private val permissions: () -> TownyPermission?, ) : Menu(player, title, 6, back) { @@ -39,7 +41,7 @@ class PermissionMenu( ACTIONS.forEachIndexed { column, action -> val allOn = LEVELS.all { perms.getPerm(it, action) } - guarded(11 + column, node, itemStack(ACTION_ICONS.getValue(action)) { + guarded(2 + column, node, itemStack(ACTION_ICONS.getValue(action)) { name("${actionName(action)}") loreWrapped("${actionDescription(action)}") lore("", tr(if (allOn) "perm.everyone-off" else "perm.everyone-on")) @@ -48,7 +50,7 @@ class PermissionMenu( LEVELS.forEachIndexed { row, level -> val allOn = ACTIONS.all { perms.getPerm(level, it) } - guarded(19 + row * 9, node, itemStack(LEVEL_ICONS.getValue(level)) { + guarded(10 + row * 9, node, itemStack(LEVEL_ICONS.getValue(level)) { name("${levelName(level)}") lore(tr(if (allOn) "perm.every-action-off" else "perm.every-action-on")) }) { run("$command ${level.arg} ${if (allOn) "off" else "on"}", ::snapshot) } @@ -62,7 +64,7 @@ class PermissionMenu( tr("icon.click-toggle") ) } - guarded(20 + row * 9 + column, node, cell) { + guarded(11 + row * 9 + column, node, cell) { run("$command ${level.arg} ${action.arg} ${if (allowed) "off" else "on"}", ::snapshot) } } @@ -71,6 +73,11 @@ class PermissionMenu( guarded(25, node, Icons.icon(Material.WATER_BUCKET, tr("perm.reset"), tr("perm.reset-description"))) { run("$command reset", ::snapshot) } + overrides?.let { open -> + button(34, Icons.icon(Material.PLAYER_HEAD, tr("perm.overrides"), tr("perm.overrides-description"))) { + open(this).open() + } + } tutorialButton(53, Tutorial.PROTECTION) backButton(49) } diff --git a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/common/RankMenu.kt b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/common/RankMenu.kt index 2119d23..82f1648 100644 --- a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/common/RankMenu.kt +++ b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/common/RankMenu.kt @@ -1,6 +1,7 @@ package net.trilleo.mc.plugins.townymenu.guis.common import com.palmergames.bukkit.towny.`object`.Resident +import com.palmergames.bukkit.towny.permissions.PermissionNodes import com.palmergames.bukkit.towny.permissions.TownyPerms import net.trilleo.mc.plugins.townymenu.guis.framework.ListMenu import net.trilleo.mc.plugins.townymenu.guis.framework.Menu @@ -14,15 +15,25 @@ import org.bukkit.entity.Player /** Builds the menu that grants or revokes town or nation ranks for [target]. */ object RankMenu { - fun create(player: Player, target: Resident, nation: Boolean, back: Menu): Menu { + /** + * @param admin the town or nation name to change ranks in as an admin, instead of as one of its members + */ + fun create(player: Player, target: Resident, nation: Boolean, back: Menu, admin: String? = null): Menu { val scope = if (nation) "nation" else "town" + val command = + if (admin == null) "towny:$scope rank" else "towny:townyadmin $scope $admin rank" + val adminNode = + if (nation) PermissionNodes.TOWNY_COMMAND_TOWNYADMIN_NATION_RANK + else PermissionNodes.TOWNY_COMMAND_TOWNYADMIN_TOWN_RANK val title = player.tr(if (nation) "rank.nation-title" else "rank.town-title", "name" to TownyUtil.name(target.name)) return ListMenu(player, title, back) { menu -> val ranks = if (nation) TownyPerms.getNationRanks() else TownyPerms.getTownRanks() ranks.map { rank -> val held = if (nation) target.hasNationRank(rank) else target.hasTownRank(rank) - val allowed = TownyUtil.can(player, "towny.command.$scope.rank.$rank") + val allowed = + if (admin == null) TownyUtil.can(player, "towny.command.$scope.rank.$rank") + else TownyUtil.can(player, adminNode) val icon = { itemStack(if (held) Material.NAME_TAG else Material.PAPER) { name("${if (held) "" else ""}${TownyUtil.name(rank)}") @@ -35,7 +46,7 @@ object RankMenu { } MenuEntry(icon) { if (allowed) menu.run( - "towny:$scope rank ${if (held) "remove" else "add"} ${target.name} $rank", + "$command ${if (held) "remove" else "add"} ${target.name} $rank", { if (nation) target.nationRanks.toList() else target.townRanks.toList() }, ) } diff --git a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/common/ToggleMenu.kt b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/common/ToggleMenu.kt index 1027785..a4a446a 100644 --- a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/common/ToggleMenu.kt +++ b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/common/ToggleMenu.kt @@ -3,42 +3,69 @@ package net.trilleo.mc.plugins.townymenu.guis.common import com.palmergames.bukkit.towny.permissions.PermissionNodes import net.trilleo.mc.plugins.townymenu.guis.framework.Icons import net.trilleo.mc.plugins.townymenu.guis.framework.Menu +import net.trilleo.mc.plugins.townymenu.utils.itemStack import org.bukkit.Material import org.bukkit.entity.Player +import org.bukkit.event.inventory.ClickType /** * One on/off setting backed by a Towny toggle command. [name] and [description] are already translated. * + * @param node the permission the toggle needs, or `null` when Towny checks none * @param value reads the current state; `null` when the target no longer exists */ class Toggle( val material: Material, val name: String, val description: String, - val node: PermissionNodes, + val node: PermissionNodes?, val command: String, val value: () -> Boolean?, ) -/** A menu of [Toggle] switches, seven per row. */ +/** A menu of [Toggle] switches, seven per row and up to three rows per page. */ class ToggleMenu( player: Player, title: String, back: Menu, private val toggles: List, -) : Menu(player, title, 3 + (toggles.size + 6) / 7, back) { +) : Menu(player, title, 3 + (minOf(toggles.size, PAGE_SIZE) + 6) / 7, back) { + + private var page = 0 override fun build() { - toggles.forEachIndexed { index, toggle -> + val pages = maxOf(1, (toggles.size + PAGE_SIZE - 1) / PAGE_SIZE) + page = page.coerceIn(0, pages - 1) + toggles.drop(page * PAGE_SIZE).take(PAGE_SIZE).forEachIndexed { index, toggle -> val slot = 10 + index / 7 * 9 + index % 7 - guarded( - slot, - toggle.node, - Icons.toggle(player, toggle.material, toggle.name, toggle.value() ?: false, toggle.description) - ) { - run(toggle.command, toggle.value) + val icon = Icons.toggle(player, toggle.material, toggle.name, toggle.value() ?: false, toggle.description) + val action = { _: ClickType -> run(toggle.command, toggle.value) } + val node = toggle.node + if (node == null) button(slot, icon, action) else guarded(slot, node, icon, action) + } + + val bottom = inventory.size - 9 + if (page > 0) { + button(bottom, pageArrow("menu.previous-page", page, pages)) { + page-- + render() + } + } + if (page < pages - 1) { + button(bottom + 8, pageArrow("menu.next-page", page + 2, pages)) { + page++ + render() } } - backButton(inventory.size - 5) + backButton(bottom + 4) + } + + private fun pageArrow(key: String, target: Int, pages: Int) = itemStack(Material.ARROW) { + name(tr(key)) + lore(tr("menu.page", "page" to target, "pages" to pages)) + } + + private companion object { + const val PAGE_SIZE = 21 } } diff --git a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/framework/Icons.kt b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/framework/Icons.kt index cfc8aa8..9c4d374 100644 --- a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/framework/Icons.kt +++ b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/framework/Icons.kt @@ -43,13 +43,12 @@ object Icons { val town = resident.townOrNull lore(buildList { if (resident.hasTitle() || resident.hasSurname()) { - add( - "${TownyUtil.text(resident.title)} ${TownyUtil.name(resident.name)} ${ - TownyUtil.text( - resident.surname - ) - }".trim() - ) + val fullName = listOf( + TownyUtil.text(resident.title), + TownyUtil.name(resident.name), + TownyUtil.text(resident.surname) + ).filter { it.isNotBlank() } + add("${fullName.joinToString(" ")}") } add( player.tr( @@ -76,7 +75,7 @@ object Icons { itemStack(if (town.isCapital) Material.GOLDEN_HELMET else Material.BELL) { name("${TownyUtil.name(town.name)}") lore(buildList { - town.board.takeIf { it.isNotBlank() }?.let { add("${TownyUtil.text(it).take(60)}") } + TownyUtil.board(town)?.let { add("${TownyUtil.text(it).take(60)}") } add(player.tr("icon.town.mayor", "mayor" to (town.mayor?.let { TownyUtil.name(it.name) } ?: "-"))) add(player.tr("icon.town.residents", "count" to town.numResidents)) add(player.tr("icon.town.claims", "claims" to town.numTownBlocks, "max" to town.maxTownBlocksAsAString)) @@ -104,7 +103,7 @@ object Icons { itemStack(Material.BEACON) { name("${TownyUtil.name(nation.name)}") lore(buildList { - nation.board.takeIf { it.isNotBlank() }?.let { add("${TownyUtil.text(it).take(60)}") } + TownyUtil.board(nation)?.let { add("${TownyUtil.text(it).take(60)}") } add(player.tr("icon.nation.leader", "leader" to (nation.king?.let { TownyUtil.name(it.name) } ?: "-"))) add( player.tr( diff --git a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/framework/Menu.kt b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/framework/Menu.kt index 909ae1b..59f4f68 100644 --- a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/framework/Menu.kt +++ b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/framework/Menu.kt @@ -6,6 +6,7 @@ import com.palmergames.bukkit.towny.permissions.PermissionNodes import net.kyori.adventure.key.Key import net.kyori.adventure.sound.Sound import net.kyori.adventure.text.minimessage.MiniMessage +import net.trilleo.mc.plugins.townymenu.Main import net.trilleo.mc.plugins.townymenu.guis.tutorial.Chapter import net.trilleo.mc.plugins.townymenu.guis.tutorial.TutorialChapterMenu import net.trilleo.mc.plugins.townymenu.utils.DialogUtil @@ -74,6 +75,8 @@ abstract class Menu( internal fun click(slot: Int, click: ClickType) { // A double click also fires as a regular click first; acting on both would run commands twice. if (click == ClickType.DOUBLE_CLICK) return + // The menu shows the old state until the last command settles, so a second click would repeat it. + if (MenuActions.isBusy(player)) return val action = actions[slot] ?: return player.playSound(CLICK_SOUND) action(click) @@ -97,10 +100,20 @@ abstract class Menu( protected inner class Layout(private val slots: IntArray) { private var next = 0 - fun add(item: ItemStack, action: ((ClickType) -> Unit)? = null) = button(slots[next++], item, action) + fun add(item: ItemStack, action: ((ClickType) -> Unit)? = null) { + nextSlot()?.let { button(it, item, action) } + } + + fun add(node: PermissionNodes, item: ItemStack, action: (ClickType) -> Unit) { + nextSlot()?.let { guarded(it, node, item, action) } + } - fun add(node: PermissionNodes, item: ItemStack, action: (ClickType) -> Unit) = - guarded(slots[next++], node, item, action) + /** The next free slot, or `null` (logged) when the layout is full, so an extra button never breaks the menu. */ + private fun nextSlot(): Int? { + if (next < slots.size) return slots[next++] + Main.instance.logger.warning("${this@Menu::class.simpleName} has more buttons than its ${slots.size} layout slots") + return null + } } protected fun layout(vararg slots: Int) = Layout(slots) diff --git a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/framework/MenuActions.kt b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/framework/MenuActions.kt index 726da13..a3ec42a 100644 --- a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/framework/MenuActions.kt +++ b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/framework/MenuActions.kt @@ -20,6 +20,8 @@ import java.util.* * - **unchanged, or no probe** → the menu stays closed so Towny's chat message * (an error, a cost, a teleport notice) is visible. * + * Either way, nothing happens if the player has since opened a different menu. + * * Towny confirmations raised by a menu command are shown as a native dialog * instead of clickable chat text (see [interceptConfirmation]). */ @@ -28,7 +30,14 @@ object MenuActions { /** Ticks to wait before checking the probe; Towny runs some commands asynchronously. */ const val DEFAULT_DELAY = 4L - private class Pending(val menu: Menu, val probe: (() -> Any?)?, val before: Any?, val delay: Long) { + /** [origin] is the menu open when the command ran, or `null` when none was (e.g. after a dialog). */ + private class Pending( + val menu: Menu, + var origin: Menu?, + val probe: (() -> Any?)?, + val before: Any?, + val delay: Long, + ) { var awaitingConfirmation = false } @@ -40,7 +49,7 @@ object MenuActions { */ fun perform(menu: Menu, command: String, probe: (() -> Any?)?, delayTicks: Long) { val player = menu.player - val action = Pending(menu, probe, probe?.invoke(), delayTicks) + val action = Pending(menu, openMenu(player), probe, probe?.invoke(), delayTicks) pending[player.uniqueId] = action player.performCommand(command) later(delayTicks) { settle(player, action) } @@ -56,6 +65,9 @@ object MenuActions { later(1) { showConfirmation(player, action, confirmation) } } + /** `true` while a command [player] started from a menu is still settling, so menu clicks are ignored. */ + fun isBusy(player: Player): Boolean = pending[player.uniqueId]?.awaitingConfirmation == false + fun forget(player: Player) { pending.remove(player.uniqueId) } @@ -65,6 +77,7 @@ object MenuActions { val title = confirmation.title.locale(player).component() val body = if (confirmation.isSerious) player.tr("dialog.cannot-undo") else null val prefix = confirmation.pluginPrefix + action.origin = null DialogUtil.confirm( player, title, body, onYes = { @@ -84,14 +97,17 @@ object MenuActions { if (pending[player.uniqueId] !== action || action.awaitingConfirmation) return pending.remove(player.uniqueId) if (!player.isOnline) return + if (openMenu(player) !== action.origin) return val probe = action.probe if (probe != null && probe() != action.before) { action.menu.open() - } else if (player.openInventory.topInventory.getHolder(false) is Menu) { + } else if (action.origin != null) { player.closeInventory() } } + private fun openMenu(player: Player): Menu? = player.openInventory.topInventory.getHolder(false) as? Menu + private fun later(ticks: Long, block: () -> Unit) { Bukkit.getScheduler().runTaskLater(Main.instance, Runnable(block), ticks) } diff --git a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/framework/OpenMenus.kt b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/framework/OpenMenus.kt new file mode 100644 index 0000000..6145af6 --- /dev/null +++ b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/framework/OpenMenus.kt @@ -0,0 +1,45 @@ +package net.trilleo.mc.plugins.townymenu.guis.framework + +import net.trilleo.mc.plugins.townymenu.Main +import org.bukkit.Bukkit +import org.bukkit.entity.Player +import java.util.logging.Level + +/** + * Keeps the menus players are looking at in step with Towny. + * + * Menus are views of live Towny data, so when something changes them from + * outside — another player, a command, or the new day — every open menu is + * rendered again. Towny fires bursts of events (a mass claim, a new day), so + * [refreshSoon] coalesces them into one refresh on the next tick. + */ +object OpenMenus { + + private var pending = false + + /** The menu [player] has open, or `null` when they have another inventory (or none) open. */ + fun of(player: Player): Menu? = player.openInventory.topInventory.getHolder(false) as? Menu + + /** Re-renders every open menu on the next tick, unless the server turned live refreshing off. */ + fun refreshSoon() { + if (pending || !Main.instance.pluginConfig.liveMenuRefresh) return + pending = true + Bukkit.getScheduler().runTask(Main.instance, Runnable { + pending = false + refreshNow() + }) + } + + private fun refreshNow() { + for (player in Bukkit.getOnlinePlayers()) { + // A command the player started is still settling; MenuActions reopens the menu when it lands. + if (MenuActions.isBusy(player)) continue + val menu = of(player) ?: continue + try { + menu.render() + } catch (error: Exception) { + Main.instance.logger.log(Level.WARNING, "Could not refresh ${menu::class.simpleName}", error) + } + } + } +} diff --git a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/framework/PagedMenu.kt b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/framework/PagedMenu.kt index 02e882c..b456427 100644 --- a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/framework/PagedMenu.kt +++ b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/framework/PagedMenu.kt @@ -15,7 +15,7 @@ class MenuEntry(icon: () -> ItemStack, val action: ((ClickType) -> Unit)? = null } /** - * A six-row menu that pages through [entries]. The top five rows hold entries; + * A six-row menu that pages through [entries]. The top four rows hold entries and the fifth stays empty; * the bottom row holds previous/next arrows, the back button (slot 49), and * any extra buttons placed by [controls] in slots 46–48 and 50–52. */ @@ -78,7 +78,7 @@ abstract class PagedMenu(player: Player, title: String, back: Menu?) : Menu(play } companion object { - private const val PAGE_SIZE = 45 + private const val PAGE_SIZE = 36 } } diff --git a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/nation/NationInfoMenu.kt b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/nation/NationInfoMenu.kt index ab7dca9..f40da26 100644 --- a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/nation/NationInfoMenu.kt +++ b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/nation/NationInfoMenu.kt @@ -10,12 +10,12 @@ import org.bukkit.entity.Player /** Public view of any nation: visit, join, towns, relations, and diplomacy with the viewer's nation. */ class NationInfoMenu(player: Player, private val nation: Nation, back: Menu) : - Menu(player, TownyUtil.name(nation.name), 4, back) { + Menu(player, TownyUtil.name(nation.name), 5, back) { override fun build() { if (!nation.exists()) { button(13, Icons.icon(Material.BARRIER, tr("nation-info.gone"))) - return backButton(31) + return backButton(40) } button(4, Icons.nation(player, nation, tr("common.founded", "date" to TownyUtil.date(nation.registered)))) @@ -37,10 +37,12 @@ class NationInfoMenu(player: Player, private val nation: Nation, back: Menu) : grid.add( Icons.icon( Material.ENDER_PEARL, tr("town-info.visit"), tr("nation-info.visit-description"), - if (TownyUtil.economy && nation.spawnCost > 0) tr( - "common.cost", - "cost" to TownyUtil.money(nation.spawnCost) - ) else "" + *listOfNotNull( + if (TownyUtil.economy && nation.spawnCost > 0) tr( + "common.cost", + "cost" to TownyUtil.money(nation.spawnCost) + ) else null + ).toTypedArray() ) ) { runAndClose("towny:nation spawn ${nation.name}") @@ -107,6 +109,6 @@ class NationInfoMenu(player: Player, private val nation: Nation, back: Menu) : ) { run("towny:nation enemy ${if (own.hasEnemy(nation)) "remove" else "add"} ${nation.name}", snapshot) } } - backButton(31) + backButton(40) } } diff --git a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/nation/NationMenu.kt b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/nation/NationMenu.kt index b1422c0..83be11a 100644 --- a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/nation/NationMenu.kt +++ b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/nation/NationMenu.kt @@ -42,7 +42,11 @@ class NationMenu(player: Player, back: Menu?) : Menu(player, player.tr("nation.t ) ) } - }.toTypedArray())) + add("") + add(tr("nation.click-status")) + }.toTypedArray())) { + NationStatusMenu(player, nation, this).open() + } val grid = layout(19, 20, 21, 22, 23, 24, 25, 28, 29, 30, 31, 32, 33, 34) @@ -88,6 +92,14 @@ class NationMenu(player: Player, back: Menu?) : Menu(player, player.tr("nation.t grid.add(Icons.icon(Material.ENDER_PEARL, tr("nation.spawn"), tr("nation.spawn-description"))) { runAndClose("towny:nation spawn") } + grid.add( + PermissionNodes.TOWNY_COMMAND_NATION_SAY, + Icons.icon(Material.GOAT_HORN, tr("nation.announce"), tr("nation.announce-description")) + ) { + prompt(tr("nation.announce-title"), tr("common.message"), maxLength = 200) { text -> + run("towny:nation say $text") + } + } if (viewer.isKing) { grid.add( PermissionNodes.TOWNY_COMMAND_NATION_DELETE, diff --git a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/nation/NationSanctionsMenu.kt b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/nation/NationSanctionsMenu.kt new file mode 100644 index 0000000..8a48726 --- /dev/null +++ b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/nation/NationSanctionsMenu.kt @@ -0,0 +1,62 @@ +package net.trilleo.mc.plugins.townymenu.guis.nation + +import com.palmergames.bukkit.towny.`object`.Nation +import com.palmergames.bukkit.towny.permissions.PermissionNodes +import net.trilleo.mc.plugins.townymenu.guis.common.Pickers +import net.trilleo.mc.plugins.townymenu.guis.framework.Icons +import net.trilleo.mc.plugins.townymenu.guis.framework.Menu +import net.trilleo.mc.plugins.townymenu.guis.framework.MenuEntry +import net.trilleo.mc.plugins.townymenu.guis.framework.PagedMenu +import net.trilleo.mc.plugins.townymenu.guis.town.TownInfoMenu +import net.trilleo.mc.plugins.townymenu.guis.tutorial.Tutorial +import net.trilleo.mc.plugins.townymenu.utils.TownyUtil +import net.trilleo.mc.plugins.townymenu.utils.tr +import org.bukkit.Material +import org.bukkit.entity.Player + +/** Towns a nation has sanctioned, which keeps their residents from using its nation spawn. */ +class NationSanctionsMenu(player: Player, private val nation: Nation, back: Menu) : + PagedMenu(player, player.tr("nation-sanctions.title", "nation" to TownyUtil.name(nation.name)), back) { + + private val canEdit: Boolean + get() = resident?.nationOrNull == nation && TownyUtil.can( + player, + PermissionNodes.TOWNY_COMMAND_NATION_SANCTIONTOWN + ) + + private fun count() = nation.sanctionedTowns.size + + override fun entries(): List { + val canEdit = canEdit + return nation.sanctionedTowns.sortedBy { it.name.lowercase() }.map { town -> + val lines = listOfNotNull( + "", + tr("common.left-details"), + if (canEdit) tr("nation-sanctions.right-lift") else null + ) + MenuEntry({ Icons.town(player, town, *lines.toTypedArray()) }) { click -> + if (click.isRightClick && canEdit) { + run("towny:nation sanctiontown remove ${town.name}", ::count) + } else { + TownInfoMenu(player, town, this).open() + } + } + } + } + + override fun controls() { + tutorialButton(52, Tutorial.NATIONS) + if (resident?.nationOrNull != nation) return + guarded( + 47, PermissionNodes.TOWNY_COMMAND_NATION_SANCTIONTOWN, + Icons.icon(Material.RED_BANNER, tr("nation-sanctions.add"), tr("nation-sanctions.add-description")) + ) { + Pickers.town( + this, + tr("nation-sanctions.add-title"), + { !nation.hasTown(it) && !nation.hasSanctionedTown(it) }) { picked -> + run("towny:nation sanctiontown add ${picked.name}", ::count) + }.open() + } + } +} diff --git a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/nation/NationSettingsMenu.kt b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/nation/NationSettingsMenu.kt index ead5f91..aa52bdf 100644 --- a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/nation/NationSettingsMenu.kt +++ b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/nation/NationSettingsMenu.kt @@ -22,7 +22,7 @@ class NationSettingsMenu(player: Player, back: Menu) : Menu(player, player.tr("n override fun build() { val nation = nation ?: return backButton(40) - val grid = layout(10, 11, 12, 13, 14, 15, 16, 19, 20, 21, 22, 23, 24, 25, 28, 29, 30, 31, 32, 33, 34) + val grid = layout(10, 11, 12, 13, 14, 15, 16, 19, 20, 21, 22, 23, 24, 25) grid.add( PermissionNodes.TOWNY_COMMAND_NATION_SET_NAME, @@ -47,7 +47,7 @@ class NationSettingsMenu(player: Player, back: Menu) : Menu(player, player.tr("n prompt( tr("nation-details.board-title"), tr("common.message"), - initial = nation.board, + initial = TownyUtil.board(nation).orEmpty(), maxLength = 256, multiline = true ) { board -> diff --git a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/nation/NationStatusMenu.kt b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/nation/NationStatusMenu.kt new file mode 100644 index 0000000..bf05225 --- /dev/null +++ b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/nation/NationStatusMenu.kt @@ -0,0 +1,88 @@ +package net.trilleo.mc.plugins.townymenu.guis.nation + +import com.palmergames.bukkit.towny.TownySettings +import com.palmergames.bukkit.towny.`object`.Nation +import net.trilleo.mc.plugins.townymenu.guis.framework.Icons +import net.trilleo.mc.plugins.townymenu.guis.framework.Menu +import net.trilleo.mc.plugins.townymenu.guis.tutorial.Tutorial +import net.trilleo.mc.plugins.townymenu.utils.TownyUtil +import net.trilleo.mc.plugins.townymenu.utils.tr +import org.bukkit.Material +import org.bukkit.entity.Player +import org.bukkit.inventory.ItemStack + +/** A nation's level, the bonuses it gives member towns, and its daily upkeep. */ +class NationStatusMenu(player: Player, private val nation: Nation, back: Menu) : + Menu(player, player.tr("nation-status.title", "nation" to TownyUtil.name(nation.name)), 6, back) { + + override fun build() { + if (!nation.exists()) { + button(22, Icons.icon(Material.BARRIER, tr("nation-info.gone"))) + return backButton(49) + } + button(4, Icons.nation(player, nation)) + + val row = layout(20, 21, 22, 23, 24) + row.add(level()) + row.add(bonuses()) + if (TownyUtil.economy) row.add(upkeep()) + + tutorialButton(53, Tutorial.NATIONS) + backButton(49) + } + + private fun level(): ItemStack { + val number = TownySettings.getNationLevelNumber(nation) + val thresholds = TownySettings.getConfigNationLevel().keys.sorted() + val byTowns = TownySettings.isNationLevelDeterminedByTownCount() + val progress = if (byTowns) nation.numTowns else nation.numResidents + val next = thresholds.getOrNull(number + 1) + return Icons.icon( + Material.EXPERIENCE_BOTTLE, tr("nation-status.level"), tr("nation-status.level-description"), + tr("town-status.level-number", "level" to number, "max" to TownySettings.getNationLevelMax()), + tr("town-status.level-name", "name" to TownyUtil.name(nation.formattedName)), + when { + nation.manualNationLevel > -1 -> tr("town-status.level-manual") + next == null -> tr("town-status.level-highest") + byTowns -> tr("nation-status.next-level-towns", "current" to progress, "needed" to next) + else -> tr("town-status.next-level-residents", "current" to progress, "needed" to next) + } + ) + } + + private fun bonuses(): ItemStack { + val level = TownySettings.getNationLevel(nation) + return Icons.icon( + Material.GOLDEN_HELMET, tr("nation-status.bonuses"), tr("nation-status.bonuses-description"), *buildList { + add(tr("nation-status.bonus-claims", "count" to level.townBlockLimitBonus())) + add(tr("town-status.nation-zone-size", "size" to level.nationZonesSize())) + if (TownySettings.isAllowingOutposts()) { + add(tr("nation-status.bonus-outposts", "count" to level.nationBonusOutpostLimit())) + add(tr("nation-status.bonus-capital-outposts", "count" to level.nationCapitalBonusOutpostLimit())) + } + }.toTypedArray() + ) + } + + private fun upkeep(): ItemStack { + val upkeep = TownySettings.getNationUpkeepCost(nation) + val peaceful = if (nation.isNeutral) TownySettings.getNationNeutralityCost(nation) else 0.0 + val total = upkeep + peaceful + return Icons.icon( + Material.HOPPER, tr("town-status.upkeep"), tr("nation-status.upkeep-description"), *buildList { + if (!TownySettings.isTaxingDaily()) add( + tr( + "tutorial.fact.daily-taxes", + "value" to TownyUtil.onOff(player, false) + ) + ) + add(tr("town-status.upkeep-daily", "cost" to TownyUtil.money(upkeep))) + if (peaceful > 0) add(tr("town-status.upkeep-peaceful", "cost" to TownyUtil.money(peaceful))) + add(tr("town-status.upkeep-total", "cost" to TownyUtil.money(total))) + add(tr("bank.balance", "balance" to TownyUtil.balance(nation))) + if (TownySettings.isTaxingDaily() && total > nation.account.cachedBalance) add(tr("nation-status.upkeep-short")) + add(tr("town-status.new-day", "time" to TownyUtil.duration(player, TownyUtil.secondsUntilNewDay()))) + }.toTypedArray() + ) + } +} diff --git a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/nation/NationTownsMenu.kt b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/nation/NationTownsMenu.kt index 1a3368f..110ab52 100644 --- a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/nation/NationTownsMenu.kt +++ b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/nation/NationTownsMenu.kt @@ -55,6 +55,14 @@ class NationTownsMenu(player: Player, private val nation: Nation, back: Menu) : run("towny:nation add ${picked.name}", sent) }.open() } + button( + 48, Icons.icon( + Material.RED_BANNER, tr("nation-towns.sanctions"), tr("nation-towns.sanctions-description"), + tr("nation-towns.sanctions-count", "count" to nation.sanctionedTowns.size) + ) + ) { + NationSanctionsMenu(player, nation, this).open() + } val pending = nation.sentInvites button( 51, Icons.icon( diff --git a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/nation/NoNationMenu.kt b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/nation/NoNationMenu.kt index 6d16a82..b1930de 100644 --- a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/nation/NoNationMenu.kt +++ b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/nation/NoNationMenu.kt @@ -13,16 +13,21 @@ import org.bukkit.Material import org.bukkit.entity.Player /** Shown when the viewer's town has no nation: found one, browse nations, or answer invites. */ -class NoNationMenu(player: Player, back: Menu) : Menu(player, player.tr("no-nation.title"), 3, back) { +class NoNationMenu(player: Player, back: Menu) : Menu(player, player.tr("no-nation.title"), 4, back) { override fun build() { val price = if (TownyUtil.economy) tr( "common.cost", "cost" to TownyUtil.money(TownySettings.getNewNationPrice()) - ) else "" + ) else null guarded( 11, PermissionNodes.TOWNY_COMMAND_NATION_NEW, - Icons.icon(Material.BEACON, tr("no-nation.found"), tr("no-nation.found-description"), price) + Icons.icon( + Material.BEACON, + tr("no-nation.found"), + tr("no-nation.found-description"), + *listOfNotNull(price).toTypedArray() + ) ) { prompt(tr("no-nation.found-title"), tr("no-nation.nation-name")) { name -> run( @@ -47,7 +52,7 @@ class NoNationMenu(player: Player, back: Menu) : Menu(player, player.tr("no-nati ) { InvitesMenu(player, this).open() } - tutorialButton(26, Tutorial.NATIONS) - backButton(22) + tutorialButton(35, Tutorial.NATIONS) + backButton(31) } } diff --git a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/plot/PlotAreaMenu.kt b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/plot/PlotAreaMenu.kt new file mode 100644 index 0000000..203f7d2 --- /dev/null +++ b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/plot/PlotAreaMenu.kt @@ -0,0 +1,105 @@ +package net.trilleo.mc.plugins.townymenu.guis.plot + +import com.palmergames.bukkit.towny.TownyAPI +import com.palmergames.bukkit.towny.permissions.PermissionNodes +import net.trilleo.mc.plugins.townymenu.guis.framework.Icons +import net.trilleo.mc.plugins.townymenu.guis.framework.Menu +import net.trilleo.mc.plugins.townymenu.guis.tutorial.Tutorial +import net.trilleo.mc.plugins.townymenu.utils.TownyUtil +import net.trilleo.mc.plugins.townymenu.utils.tr +import org.bukkit.Material +import org.bukkit.entity.Player + +/** + * Buys, sells, and gives up every plot in a square or circle around the viewer, + * using Towny's `rect`/`circle` area selections. The shape and radius live in + * this menu until the player runs an action. + */ +class PlotAreaMenu(player: Player, back: Menu) : Menu(player, player.tr("plot-area.title"), 6, back) { + + private var circle = false + private var radius = "1" + + private val area: String + get() = "${if (circle) "circle" else "rect"} $radius" + + override fun build() { + button(4, Icons.icon(Material.FILLED_MAP, tr("plot-area.info"), tr("plot-area.info-description"))) + + button( + 21, Icons.icon( + if (circle) Material.SNOWBALL else Material.PAINTING, tr("plot-area.shape"), null, + tr("common.current", "value" to tr(if (circle) "plot-area.circle" else "plot-area.square")), + tr("common.click-change") + ) + ) { + circle = !circle + render() + } + button( + 23, Icons.icon( + Material.COMPASS, tr("plot-area.radius"), tr("plot-area.radius-description"), + tr("common.current", "value" to radius), + tr("common.click-change") + ) + ) { + prompt(tr("plot-area.radius-title"), tr("claims.radius"), initial = radius, maxLength = 3) { + radius = TownyUtil.argument(it) + open() + } + } + + val owned = { resident?.townBlocks?.size } + val actions = layout(29, 30, 31, 32, 33) + actions.add( + PermissionNodes.TOWNY_COMMAND_PLOT_CLAIM, + Icons.icon(Material.EMERALD, tr("plot-area.buy"), tr("plot-area.buy-description")) + ) { + run("towny:plot claim $area", owned, delayTicks = CLAIM_DELAY) + } + if (TownyUtil.economy) { + actions.add( + PermissionNodes.TOWNY_COMMAND_PLOT_FORSALE, + Icons.icon(Material.GREEN_BANNER, tr("plot-area.sell"), tr("plot-area.sell-description")) + ) { + val price = TownyAPI.getInstance().getTown(player.location)?.plotPrice?.takeIf { it >= 0 } ?: 0.0 + prompt( + tr("plot-area.sell-title"), + tr("common.price-label"), + tr("plot.for-sale-hint"), + initial = price.toBigDecimal().stripTrailingZeros().toPlainString() + ) { input -> + run("towny:plot forsale ${TownyUtil.argument(input)} within $area") + } + } + } + actions.add( + PermissionNodes.TOWNY_COMMAND_PLOT_NOTFORSALE, + Icons.icon(Material.RED_BANNER, tr("plot-area.not-for-sale"), tr("plot-area.not-for-sale-description")) + ) { + run("towny:plot notforsale $area") + } + actions.add( + PermissionNodes.TOWNY_COMMAND_PLOT_UNCLAIM, + Icons.icon(Material.COARSE_DIRT, tr("plot-area.give-up"), tr("plot-area.give-up-description")) + ) { + run("towny:plot unclaim $area", owned, delayTicks = CLAIM_DELAY) + } + actions.add( + PermissionNodes.TOWNY_COMMAND_PLOT_UNCLAIM, + Icons.icon( + Material.LAVA_BUCKET, tr("plot-area.give-up-all"), tr("plot-area.give-up-all-description"), + tr("profile.plots-owned", "count" to (resident?.townBlocks?.size ?: 0)) + ) + ) { + run("towny:plot unclaim all", owned, delayTicks = CLAIM_DELAY) + } + + tutorialButton(53, Tutorial.PLOTS) + backButton(49) + } + + private companion object { + const val CLAIM_DELAY = 20L + } +} diff --git a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/plot/PlotGroupMenu.kt b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/plot/PlotGroupMenu.kt index 36bebd7..b5f4181 100644 --- a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/plot/PlotGroupMenu.kt +++ b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/plot/PlotGroupMenu.kt @@ -12,7 +12,7 @@ import org.bukkit.Material import org.bukkit.entity.Player /** Plot group and district management for the plot the viewer is standing in. */ -class PlotGroupMenu(player: Player, back: Menu) : Menu(player, player.tr("plot-group.title"), 5, back) { +class PlotGroupMenu(player: Player, back: Menu) : Menu(player, player.tr("plot-group.title"), 6, back) { private val plot: TownBlock? get() = TownyAPI.getInstance().getTownBlock(player) @@ -21,7 +21,7 @@ class PlotGroupMenu(player: Player, back: Menu) : Menu(player, player.tr("plot-g val plot = plot if (plot == null) { button(22, Icons.icon(Material.BARRIER, tr("plot-group.not-in-town"))) - return backButton(40) + return backButton(49) } val group = plot.plotObjectGroup val district = plot.district @@ -31,10 +31,10 @@ class PlotGroupMenu(player: Player, back: Menu) : Menu(player, player.tr("plot-g button( 4, Icons.icon( Material.CHEST, tr("main.plot"), null, - tr("plot.group", "group" to (group?.let { TownyUtil.name(it.name) } ?: tr("common.none"))), - tr("plot.district", "district" to (district?.let { TownyUtil.name(it.name) } ?: tr("common.none"))))) + tr("plot.group", "group" to (group?.let { TownyUtil.name(it.name) } ?: tr("common.none"))), + tr("plot.district", "district" to (district?.let { TownyUtil.name(it.name) } ?: tr("common.none"))))) - val groups = layout(10, 11, 12, 13, 14, 15, 16) + val groups = layout(19, 20, 21, 22, 23, 24, 25) groups.add( PermissionNodes.TOWNY_COMMAND_PLOT_GROUP_ADD, Icons.icon(Material.CHEST, tr("plot-group.add"), tr("plot-group.add-description")) @@ -128,7 +128,7 @@ class PlotGroupMenu(player: Player, back: Menu) : Menu(player, player.tr("plot-g } } - tutorialButton(44, Tutorial.PLOTS) - backButton(40) + tutorialButton(53, Tutorial.PLOTS) + backButton(49) } } diff --git a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/plot/PlotMenu.kt b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/plot/PlotMenu.kt index a0d23e4..c7853f4 100644 --- a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/plot/PlotMenu.kt +++ b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/plot/PlotMenu.kt @@ -1,6 +1,8 @@ package net.trilleo.mc.plugins.townymenu.guis.plot import com.palmergames.bukkit.towny.TownyAPI +import com.palmergames.bukkit.towny.TownySettings +import com.palmergames.bukkit.towny.`object`.PlotGroup import com.palmergames.bukkit.towny.`object`.TownBlock import com.palmergames.bukkit.towny.`object`.TownBlockTypeHandler import com.palmergames.bukkit.towny.`object`.WorldCoord @@ -22,6 +24,10 @@ import org.bukkit.entity.Player /** * The plot the viewer is standing in. Every render re-reads the player's * location, so walking to another chunk and refreshing shows that chunk. + * + * Towny refuses most single-plot commands on a plot in a group, so for grouped + * plots the sale, type, settings, permission, trust, and join-day buttons run + * the matching `/plot group` command and change every plot in the group. */ class PlotMenu(player: Player, back: Menu?) : Menu(player, player.tr("plot.title"), 6, back) { @@ -31,6 +37,15 @@ class PlotMenu(player: Player, back: Menu?) : Menu(player, player.tr("plot.title override fun build() { val plot = plot if (plot == null) wilderness() else claimed(plot) + guarded( + 48, PermissionNodes.TOWNY_COMMAND_PLOT_PERM_HUD, + Icons.icon(Material.SPYGLASS, tr("plot.hud"), tr("plot.hud-description")) + ) { + runAndClose("towny:plot perm hud") + } + button(47, Icons.icon(Material.FILLED_MAP, tr("main.map"), tr("common.map-description"))) { + MapMenu(player, this).open() + } button(50, Icons.icon(Material.CLOCK, tr("map.refresh"), tr("plot.refresh-description"))) { render() } tutorialButton(53, Tutorial.PLOTS) backButton(49) @@ -64,11 +79,8 @@ class PlotMenu(player: Player, back: Menu?) : Menu(player, player.tr("plot.title run("towny:town claim outpost", { town.numTownBlocks }, delayTicks = 20) } } - grid.add(Icons.icon(Material.FILLED_MAP, tr("main.map"), tr("common.map-description"))) { - MapMenu( - player, - this - ).open() + grid.add(Icons.icon(Material.MAP, tr("plot.area"), tr("plot.area-description"))) { + PlotAreaMenu(player, this).open() } } @@ -76,6 +88,14 @@ class PlotMenu(player: Player, back: Menu?) : Menu(player, player.tr("plot.title val town = plot.townOrNull val owner = plot.residentOrNull val viewer = resident + val group = plot.plotObjectGroup + val scope = if (group != null) "towny:plot group" else "towny:plot" + val groupLines = listOfNotNull(group?.let { + tr("plot.group-applies", "group" to TownyUtil.name(it.name), "count" to it.townBlocks.size) + }).toTypedArray() + val forSale = if (group != null) group.price >= 0 else plot.isForSale + val price = group?.price ?: plot.plotPrice + val canInfo = TownyUtil.can(player, PermissionNodes.TOWNY_COMMAND_PLOT_PERM_INFO) button( 4, Icons.icon( @@ -89,17 +109,13 @@ class PlotMenu(player: Player, back: Menu?) : Menu(player, player.tr("plot.title add(tr("map.chunk", "x" to plot.x, "z" to plot.z)) if (plot.isHomeBlock) add(tr("map.home-block")) if (plot.isOutpost) add(tr("map.outpost")) - if (plot.hasPlotObjectGroup()) add( - tr( - "plot.group", - "group" to TownyUtil.name(plot.plotObjectGroup.name) - ) - ) + if (group != null) add(tr("plot.group", "group" to TownyUtil.name(group.name))) if (plot.hasDistrict()) add(tr("plot.district", "district" to TownyUtil.name(plot.district.name))) if (TownyUtil.economy) { - if (plot.isForSale) add(tr("map.for-sale", "price" to TownyUtil.money(plot.plotPrice))) + if (forSale) add(tr("map.for-sale", "price" to TownyUtil.money(price))) if (plot.isTaxed) add(tr("bank.daily-tax", "tax" to TownyUtil.money(plot.plotTax))) } + addAll(joinDayLines(plot, group)) add( tr( "town.pvp-mobs", @@ -114,19 +130,22 @@ class PlotMenu(player: Player, back: Menu?) : Menu(player, player.tr("plot.title ) ) add(tr("plot.claimed", "date" to TownyUtil.date(plot.claimedAt))) + if (canInfo) add(tr("plot.click-info")) }.toTypedArray() ) - ) + ) { + if (canInfo) runAndClose("towny:plot info") + } - val grid = layout(19, 20, 21, 22, 23, 24, 25, 28, 29, 30, 31, 32, 33, 34, 37, 38, 39, 40, 41, 42, 43) + val grid = layout(19, 20, 21, 22, 23, 24, 25, 28, 29, 30, 31, 32, 33, 34) val ownerProbe = { plot.residentOrNull } - val saleProbe = { plot.isForSale to plot.plotPrice } + val saleProbe = { plot.plotObjectGroup?.price to plot.plotPrice } - if (plot.isForSale && owner != viewer) { + if (forSale && owner != viewer) { grid.add( PermissionNodes.TOWNY_COMMAND_PLOT_CLAIM, Icons.icon( Material.EMERALD, tr("plot.buy"), tr("plot.buy-description"), - tr("common.price", "price" to TownyUtil.money(plot.plotPrice)), + tr("common.price", "price" to TownyUtil.money(price)), ) ) { run("towny:plot claim", ownerProbe) } } @@ -138,25 +157,31 @@ class PlotMenu(player: Player, back: Menu?) : Menu(player, player.tr("plot.title run("towny:plot unclaim", ownerProbe) } } - if (plot.isForSale) { + if (forSale) { grid.add( - PermissionNodes.TOWNY_COMMAND_PLOT_NOTFORSALE, - Icons.icon(Material.RED_BANNER, tr("plot.not-for-sale"), tr("plot.not-for-sale-description")) + if (group != null) PermissionNodes.TOWNY_COMMAND_PLOT_GROUP_NOTFORSALE else PermissionNodes.TOWNY_COMMAND_PLOT_NOTFORSALE, + Icons.icon( + Material.RED_BANNER, + tr("plot.not-for-sale"), + tr("plot.not-for-sale-description"), + *groupLines + ) ) { - run("towny:plot notforsale", saleProbe) + run("$scope notforsale", saleProbe) } } else { grid.add( - PermissionNodes.TOWNY_COMMAND_PLOT_FORSALE, - Icons.icon(Material.GREEN_BANNER, tr("plot.for-sale"), tr("plot.for-sale-description")) + if (group != null) PermissionNodes.TOWNY_COMMAND_PLOT_GROUP_FORSALE else PermissionNodes.TOWNY_COMMAND_PLOT_FORSALE, + Icons.icon(Material.GREEN_BANNER, tr("plot.for-sale"), tr("plot.for-sale-description"), *groupLines) ) { prompt( tr("plot.for-sale-title"), tr("common.price-label"), - tr("plot.for-sale-hint"), - initial = "0" - ) { price -> - run("towny:plot forsale ${TownyUtil.argument(price)}", saleProbe) + if (group == null) tr("plot.for-sale-hint") else null, + initial = (town?.getPlotTypePrice(plot.type) ?: 0.0).toBigDecimal().stripTrailingZeros() + .toPlainString() + ) { input -> + run("$scope forsale ${TownyUtil.argument(input)}", saleProbe) } } } @@ -166,20 +191,28 @@ class PlotMenu(player: Player, back: Menu?) : Menu(player, player.tr("plot.title Icons.icon( Material.IRON_BOOTS, tr("plot.evict"), - tr("plot.evict-description", "owner" to TownyUtil.name(owner.name)) + tr("plot.evict-description", "owner" to TownyUtil.name(owner.name)), + *listOfNotNull(if (group == null) tr("plot.evict-resell") else null).toTypedArray(), + *groupLines + ) + ) { click -> + run( + if (click.isRightClick && group == null) "towny:plot evict forsale" else "towny:plot evict", + ownerProbe ) - ) { - run("towny:plot evict", ownerProbe) } } grid.add( Icons.icon( Material.OAK_SIGN, tr("plot.type"), tr("plot.type-description"), - tr("common.current", "value" to TownyUtil.plotType(player, plot.type.name)) + tr("common.current", "value" to TownyUtil.plotType(player, plot.type.name)), + *groupLines ) ) { val types = listOf("reset" to "${TownyUtil.plotType(player, "default")}") + - TownBlockTypeHandler.getTypeNames().filter { it != "default" }.sorted() + TownBlockTypeHandler.getTypeNames() + .filter { it != "default" && (group == null || it != "jail") } + .sorted() .map { it to "${TownyUtil.plotType(player, it)}" } Pickers.option( this, @@ -187,35 +220,99 @@ class PlotMenu(player: Player, back: Menu?) : Menu(player, player.tr("plot.title types, plot.type.name.takeIf { it != "default" } ?: "reset", Material.OAK_SIGN) { type -> - run("towny:plot set $type", { plot.type.name }, returnTo = this) + run("$scope set $type", { plot.type.name }, returnTo = this) }.open() } grid.add( PermissionNodes.TOWNY_COMMAND_PLOT_SET_NAME, Icons.icon(Material.NAME_TAG, tr("plot.rename"), tr("plot.rename-description")) - ) { - prompt(tr("plot.rename-title"), tr("plot.plot-name"), initial = plot.name.orEmpty()) { name -> - run("towny:plot set name ${TownyUtil.nameArgument(name)}", { plot.name }) + ) { click -> + if (click.isRightClick) { + run("towny:plot set name", { plot.name }) + } else { + prompt(tr("plot.rename-title"), tr("plot.plot-name"), initial = plot.name.orEmpty()) { name -> + run("towny:plot set name ${TownyUtil.nameArgument(name)}", { plot.name }) + } } } - grid.add(Icons.icon(Material.LEVER, tr("plot.settings"), tr("plot.settings-description"))) { - toggles(plot).open() + grid.add(Icons.icon(Material.LEVER, tr("plot.settings"), tr("plot.settings-description"), *groupLines)) { + toggles(plot, group != null).open() } + val permNode = + if (group != null) PermissionNodes.TOWNY_COMMAND_PLOT_GROUP_SET else PermissionNodes.TOWNY_COMMAND_PLOT_SET_PERM grid.add( - PermissionNodes.TOWNY_COMMAND_PLOT_SET_PERM, - Icons.icon(Material.IRON_DOOR, tr("common.permissions"), tr("plot.permissions-description")) + permNode, + Icons.icon(Material.IRON_DOOR, tr("common.permissions"), tr("plot.permissions-description"), *groupLines) ) { PermissionMenu( - player, tr("plot.permissions-title"), this, "towny:plot set perm", - PermissionNodes.TOWNY_COMMAND_PLOT_SET_PERM, owner != null - ) { plot.takeIf { it.exists() }?.permissions }.open() + player, tr("plot.permissions-title"), this, "$scope set perm", permNode, owner != null, + overrides = { back -> PlotOverridesMenu(player, plot, back) } + ) { plot.takeIf { it.exists() }?.let { it.plotObjectGroup?.permissions ?: it.permissions } }.open() } grid.add( - PermissionNodes.TOWNY_COMMAND_PLOT_TRUST, Icons.icon( + if (group != null) PermissionNodes.TOWNY_COMMAND_PLOT_GROUP_TRUST else PermissionNodes.TOWNY_COMMAND_PLOT_TRUST, + Icons.icon( Material.TRIPWIRE_HOOK, tr("plot.trusted"), tr("plot.trusted-description"), - tr("plot.trusted-count", "count" to plot.trustedResidents.size), + tr("plot.trusted-count", "count" to (group?.trustedResidents ?: plot.trustedResidents).size), + *groupLines ) ) { PlotTrustMenu(player, plot, this).open() } + grid.add( + if (group != null) PermissionNodes.TOWNY_COMMAND_PLOT_GROUP_SET else PermissionNodes.TOWNY_COMMAND_PLOT_ASMAYOR, + Icons.icon( + Material.CLOCK, tr("plot.join-days"), tr("plot.join-days-description"), + *joinDayLines(plot, group).ifEmpty { listOf(tr("plot.join-days-none")) }.toTypedArray(), + *groupLines + ) + ) { click -> + val key = if (click.isRightClick) "maxjoindays" else "minjoindays" + val probe = { + plot.plotObjectGroup?.let { it.minTownMembershipDays to it.maxTownMembershipDays } + ?: (plot.minTownMembershipDays to plot.maxTownMembershipDays) + } + prompt( + tr(if (click.isRightClick) "plot.max-join-days-title" else "plot.min-join-days-title"), + tr("plot.days"), + tr("plot.join-days-hint"), + maxLength = 5 + ) { input -> + val days = TownyUtil.argument(input) + run("$scope set $key ${if (days == "0") "clear" else days}", probe) + } + } + if (plot.isJail) { + grid.add( + PermissionNodes.TOWNY_COMMAND_PLOT_JAILCELL, + Icons.icon( + Material.IRON_CHAIN, tr("plot.jail-cells"), tr("plot.jail-cells-description"), + tr("plot.jail-cells-count", "count" to (plot.jail?.jailCellCount ?: 0)) + ) + ) { click -> + run( + "towny:plot jailcell ${if (click.isRightClick) "remove" else "add"}", + { plot.jail?.jailCellCount }) + } + } + if (TownySettings.isAllowingOutposts() && group == null && town != null && town == viewer?.townOrNull) { + if (plot.isOutpost) { + grid.add( + PermissionNodes.TOWNY_COMMAND_TOWN_SET_OUTPOST, + Icons.icon(Material.RESPAWN_ANCHOR, tr("plot.outpost-spawn"), tr("plot.outpost-spawn-description")) + ) { + run("towny:plot set outpost spawn", { town.allOutpostSpawns.toList() }) + } + } else if (!plot.isHomeBlock) { + grid.add( + PermissionNodes.TOWNY_COMMAND_TOWN_CLAIM_OUTPOST, + Icons.icon( + Material.COMPASS, tr("plot.make-outpost"), tr("plot.make-outpost-description"), + *outpostCost().toTypedArray() + ) + ) { + run("towny:plot set outpost", { plot.isOutpost to town.allOutpostSpawns.size }) + } + } + } grid.add(Icons.icon(Material.CHEST, tr("plot.groups"), tr("plot.groups-description"))) { PlotGroupMenu(player, this).open() } @@ -225,20 +322,34 @@ class PlotMenu(player: Player, back: Menu?) : Menu(player, player.tr("plot.title ) { runAndClose("towny:plot clear") } + grid.add(Icons.icon(Material.MAP, tr("plot.area"), tr("plot.area-description"))) { + PlotAreaMenu(player, this).open() + } if (town != null) { - grid.add(Icons.icon(Material.BELL, "${TownyUtil.name(town.name)}", tr("plot.town-description"))) { + button(46, Icons.icon(Material.BELL, "${TownyUtil.name(town.name)}", tr("plot.town-description"))) { TownInfoMenu(player, town, this).open() } } - grid.add(Icons.icon(Material.FILLED_MAP, tr("main.map"), tr("common.map-description"))) { - MapMenu( - player, - this - ).open() - } } - private fun toggles(plot: TownBlock): Menu { + private fun outpostCost(): List = + if (TownyUtil.economy && TownySettings.getOutpostCost() > 0) { + listOf(tr("common.cost", "cost" to TownyUtil.money(TownySettings.getOutpostCost()))) + } else emptyList() + + /** The join-day limits Towny places on buying [plot], or on every plot of its [group]. */ + private fun joinDayLines(plot: TownBlock, group: PlotGroup?): List { + val min = group?.minTownMembershipDays ?: plot.minTownMembershipDays + val max = group?.maxTownMembershipDays ?: plot.maxTownMembershipDays + return listOfNotNull( + if (min > 0) tr("plot.min-join-days", "days" to min) else null, + if (max > 0) tr("plot.max-join-days", "days" to max) else null, + ) + } + + private fun toggles(plot: TownBlock, grouped: Boolean): Menu { + val scope = if (grouped) "towny:plot group" else "towny:plot" + fun toggle( material: Material, key: String, @@ -247,7 +358,11 @@ class PlotMenu(player: Player, back: Menu?) : Menu(player, player.tr("plot.title node: PermissionNodes, read: (TownBlock) -> Boolean ) = - Toggle(material, label, description, node, "towny:plot toggle $key") { + Toggle( + material, label, description, + if (grouped) PermissionNodes.TOWNY_COMMAND_PLOT_GROUP_TOGGLE else node, + "$scope toggle $key" + ) { plot.takeIf { it.exists() }?.let(read) } diff --git a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/plot/PlotOverridesMenu.kt b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/plot/PlotOverridesMenu.kt new file mode 100644 index 0000000..e266acb --- /dev/null +++ b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/plot/PlotOverridesMenu.kt @@ -0,0 +1,82 @@ +package net.trilleo.mc.plugins.townymenu.guis.plot + +import com.palmergames.bukkit.towny.`object`.PermissionData +import com.palmergames.bukkit.towny.`object`.Resident +import com.palmergames.bukkit.towny.`object`.TownBlock +import com.palmergames.bukkit.towny.permissions.PermissionNodes +import net.trilleo.mc.plugins.townymenu.guis.common.Pickers +import net.trilleo.mc.plugins.townymenu.guis.framework.Icons +import net.trilleo.mc.plugins.townymenu.guis.framework.Menu +import net.trilleo.mc.plugins.townymenu.guis.framework.MenuEntry +import net.trilleo.mc.plugins.townymenu.guis.framework.PagedMenu +import net.trilleo.mc.plugins.townymenu.guis.tutorial.Tutorial +import net.trilleo.mc.plugins.townymenu.utils.TownyUtil +import net.trilleo.mc.plugins.townymenu.utils.tr +import org.bukkit.Material +import org.bukkit.entity.Player + +/** + * Per-player permission overrides on one plot, or on every plot of its group. + * TownyMenu adds and removes players; the build/destroy/switch/item-use choices + * for each player are edited in Towny's own override editor. + */ +class PlotOverridesMenu(player: Player, private val plot: TownBlock, back: Menu) : + PagedMenu(player, player.tr("plot-overrides.title"), back) { + + private val grouped: Boolean + get() = plot.hasPlotObjectGroup() + + private val command: String + get() = if (grouped) "towny:plot group perm" else "towny:plot perm" + + private fun overrides(): Map = + plot.plotObjectGroup?.permissionOverrides ?: plot.permissionOverrides + + private fun snapshot() = overrides().keys.toSet() + + private fun node(grouped: PermissionNodes, single: PermissionNodes) = if (this.grouped) grouped else single + + override fun entries(): List { + val canRemove = TownyUtil.can( + player, + node(PermissionNodes.TOWNY_COMMAND_PLOT_GROUP_PERM, PermissionNodes.TOWNY_COMMAND_PLOT_PERM_REMOVE) + ) + return overrides().entries.sortedBy { it.key.name.lowercase() }.map { (target, data) -> + val lines = listOfNotNull( + "", + tr("plot-overrides.changed-by", "player" to TownyUtil.name(data.lastChangedBy ?: "-")), + if (canRemove) tr("plot-overrides.click-remove") else null, + ) + MenuEntry({ Icons.resident(player, target, *lines.toTypedArray()) }) { + if (canRemove) run("$command remove ${target.name}", ::snapshot) + } + } + } + + override fun controls() { + tutorialButton(52, Tutorial.PROTECTION) + val addNode = node(PermissionNodes.TOWNY_COMMAND_PLOT_GROUP_PERM, PermissionNodes.TOWNY_COMMAND_PLOT_PERM_ADD) + guarded( + 47, addNode, + Icons.icon(Material.PLAYER_HEAD, tr("plot-overrides.add-online"), tr("plot-overrides.add-description")) + ) { + Pickers.resident(this, tr("plot-overrides.add-title"), { it !in overrides() }) { picked -> + run("$command add ${picked.name}", ::snapshot) + }.open() + } + guarded( + 48, addNode, + Icons.icon(Material.NAME_TAG, tr("plot-overrides.add-name"), tr("plot-overrides.add-description")) + ) { + prompt(tr("plot-overrides.add-title"), tr("common.player-name")) { name -> + run("$command add ${TownyUtil.argument(name)}", ::snapshot) + } + } + guarded( + 51, node(PermissionNodes.TOWNY_COMMAND_PLOT_GROUP_PERM, PermissionNodes.TOWNY_COMMAND_PLOT_PERM_GUI), + Icons.icon(Material.WRITABLE_BOOK, tr("plot-overrides.edit"), tr("plot-overrides.edit-description")) + ) { + runAndClose("$command gui") + } + } +} diff --git a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/plot/PlotTrustMenu.kt b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/plot/PlotTrustMenu.kt index 13a6e22..fa79e99 100644 --- a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/plot/PlotTrustMenu.kt +++ b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/plot/PlotTrustMenu.kt @@ -1,5 +1,6 @@ package net.trilleo.mc.plugins.townymenu.guis.plot +import com.palmergames.bukkit.towny.`object`.Resident import com.palmergames.bukkit.towny.`object`.TownBlock import com.palmergames.bukkit.towny.permissions.PermissionNodes import net.trilleo.mc.plugins.townymenu.guis.common.Pickers @@ -14,39 +15,59 @@ import org.bukkit.Material import org.bukkit.entity.Player /** - * Players trusted on one plot. Towny's `/plot trust` acts on the plot the player - * stands in, so this menu is tied to the plot it was opened for. + * Players trusted on one plot, or on every plot of its group when it has one. + * Towny's `/plot trust` acts on the plot the player stands in, so this menu is + * tied to the plot it was opened for. */ class PlotTrustMenu(player: Player, private val plot: TownBlock, back: Menu) : PagedMenu(player, player.tr("plot-trust.title"), back) { - private fun count() = plot.trustedResidents.size + private val grouped: Boolean + get() = plot.hasPlotObjectGroup() + + private val command: String + get() = if (grouped) "towny:plot group trust" else "towny:plot trust" + + private val node: PermissionNodes + get() = if (grouped) PermissionNodes.TOWNY_COMMAND_PLOT_GROUP_TRUST else PermissionNodes.TOWNY_COMMAND_PLOT_TRUST + + private fun trusted(): Set = plot.plotObjectGroup?.trustedResidents ?: plot.trustedResidents + + private fun count() = trusted().size override fun entries(): List = - plot.trustedResidents.sortedBy { it.name.lowercase() }.map { trusted -> + trusted().sortedBy { it.name.lowercase() }.map { trusted -> MenuEntry({ Icons.resident(player, trusted, "", tr("common.click-untrust")) }) { - run("towny:plot trust remove ${trusted.name}", ::count) + run("$command remove ${trusted.name}", ::count) } } override fun controls() { tutorialButton(52, Tutorial.PLOTS) guarded( - 47, PermissionNodes.TOWNY_COMMAND_PLOT_TRUST, + 47, node, Icons.icon(Material.PLAYER_HEAD, tr("common.trust-online"), tr("plot-trust.trust-online-description")) ) { - Pickers.resident(this, tr("plot-trust.trust-title"), { !plot.hasTrustedResident(it) }) { picked -> - run("towny:plot trust add ${picked.name}", ::count) + Pickers.resident(this, tr("plot-trust.trust-title"), { it !in trusted() }) { picked -> + run("$command add ${picked.name}", ::count) }.open() } guarded( - 48, PermissionNodes.TOWNY_COMMAND_PLOT_TRUST, + 48, node, Icons.icon(Material.NAME_TAG, tr("common.trust-name"), tr("common.trust-name-description")) ) { prompt( tr("plot-trust.trust-title"), tr("common.player-name") - ) { name -> run("towny:plot trust add ${TownyUtil.argument(name)}", ::count) } + ) { name -> run("$command add ${TownyUtil.argument(name)}", ::count) } + } + plot.plotObjectGroup?.let { group -> + button( + 51, Icons.icon( + Material.CHEST, tr("plot-trust.group", "group" to TownyUtil.name(group.name)), + tr("plot-trust.group-description", "count" to group.townBlocks.size) + ) + ) } } } diff --git a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/resident/ResidentMenu.kt b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/resident/ResidentMenu.kt index a8cc1c4..0c9a035 100644 --- a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/resident/ResidentMenu.kt +++ b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/resident/ResidentMenu.kt @@ -2,17 +2,22 @@ package net.trilleo.mc.plugins.townymenu.guis.resident import com.palmergames.bukkit.towny.TownyAPI import com.palmergames.bukkit.towny.`object`.Resident +import com.palmergames.bukkit.towny.`object`.Town import com.palmergames.bukkit.towny.permissions.PermissionNodes import net.trilleo.mc.plugins.townymenu.guis.common.PermissionMenu import net.trilleo.mc.plugins.townymenu.guis.common.Toggle import net.trilleo.mc.plugins.townymenu.guis.common.ToggleMenu import net.trilleo.mc.plugins.townymenu.guis.framework.Icons +import net.trilleo.mc.plugins.townymenu.guis.framework.ListMenu import net.trilleo.mc.plugins.townymenu.guis.framework.Menu +import net.trilleo.mc.plugins.townymenu.guis.framework.MenuEntry +import net.trilleo.mc.plugins.townymenu.guis.town.TownInfoMenu import net.trilleo.mc.plugins.townymenu.guis.tutorial.Tutorial import net.trilleo.mc.plugins.townymenu.utils.TownyUtil import net.trilleo.mc.plugins.townymenu.utils.tr import org.bukkit.Material import org.bukkit.entity.Player +import org.bukkit.inventory.ItemStack /** The viewer's own resident profile: friends, personal toggles, plot permissions, and more. */ class ResidentMenu(player: Player, back: Menu?) : Menu(player, player.tr("profile.title"), 5, back) { @@ -33,7 +38,7 @@ class ResidentMenu(player: Player, back: Menu?) : Menu(player, player.tr("profil ) ) } - if (resident.about.isNotBlank()) add(tr("profile.about-line", "about" to TownyUtil.text(resident.about))) + TownyUtil.about(resident)?.let { add(tr("profile.about-line", "about" to TownyUtil.text(it))) } if (resident.isJailed) { add( tr( @@ -69,7 +74,7 @@ class ResidentMenu(player: Player, back: Menu?) : Menu(player, player.tr("profil prompt( tr("profile.about-title"), tr("profile.bio"), - initial = resident.about, + initial = TownyUtil.about(resident).orEmpty(), maxLength = 159 ) { text -> run("towny:resident set about $text", { resident.about }) @@ -82,6 +87,18 @@ class ResidentMenu(player: Player, back: Menu?) : Menu(player, player.tr("profil ) { runAndClose("towny:resident spawn") } + row.add( + PermissionNodes.TOWNY_COMMAND_RESIDENT_SET_MODE, + Icons.icon( + Material.MILK_BUCKET, tr("profile.modes"), tr("profile.modes-description"), + tr( + "profile.modes-active", + "modes" to resident.modes.ifEmpty { listOf(tr("common.none")) }.joinToString(", ") + ) + ) + ) { click -> + run("towny:resident set mode ${if (click.isRightClick) "reset" else "clear"}", { resident.modes.toSet() }) + } if (resident.isJailed && TownyUtil.economy) { row.add( Icons.icon( @@ -93,10 +110,81 @@ class ResidentMenu(player: Player, back: Menu?) : Menu(player, player.tr("profil } } + standing(resident) tutorialButton(44, Tutorial.PROFILE) backButton(40) } + /** The bottom-row lists of where the viewer stands: their plots, daily tax, and the towns that outlaw or trust them. */ + private fun standing(resident: Resident) { + guarded( + 37, PermissionNodes.TOWNY_COMMAND_RESIDENT_PLOTLIST, + Icons.icon( + Material.GRASS_BLOCK, tr("profile.plots"), tr("profile.plots-description"), + tr("profile.plots-owned", "count" to resident.townBlocks.size) + ) + ) { + ResidentPlotsMenu(player, resident, this).open() + } + if (TownyUtil.economy && TownyUtil.can(player, PermissionNodes.TOWNY_COMMAND_RESIDENT_TAX)) { + button(38, taxIcon(resident)) + } + guarded( + 42, PermissionNodes.TOWNY_COMMAND_RESIDENT_OUTLAWLIST, + Icons.icon( + Material.IRON_BARS, tr("profile.outlawed-in"), tr("profile.outlawed-in-description"), + tr("profile.towns-count", "count" to resident.townsOutlawedIn.size) + ) + ) { + towns(tr("profile.outlawed-in-title")) { it.townsOutlawedIn }.open() + } + guarded( + 43, PermissionNodes.TOWNY_COMMAND_RESIDENT_TRUSTLIST, + Icons.icon( + Material.TRIPWIRE_HOOK, tr("profile.trusted-in"), tr("profile.trusted-in-description"), + tr("profile.towns-count", "count" to resident.townsTrustedIn.size) + ) + ) { + towns(tr("profile.trusted-in-title")) { it.townsTrustedIn }.open() + } + } + + /** The same daily tax breakdown as Towny's `/resident tax`. */ + private fun taxIcon(resident: Resident): ItemStack { + val exempt = resident.hasPermissionNode("towny.tax_exempt") + val town = resident.townOrNull + val townTax = when { + town == null || exempt -> 0.0 + town.isTaxPercentage -> minOf( + (resident.accountOrNull?.cachedBalance ?: 0.0) * town.taxes / 100, + town.maxPercentTaxAmount + ) + + else -> town.taxes + } + val plotTax = resident.townBlocks.sumOf { plot -> + val plotTown = plot.townOrNull + if (plotTown == null || (exempt && plotTown.hasResident(resident))) 0.0 else plot.type.getTax(plotTown) + } + return Icons.icon(Material.GOLD_NUGGET, tr("profile.tax"), tr("profile.tax-description"), *buildList { + if (exempt) add(tr("profile.tax-exempt")) + if (town != null && !exempt) add(tr("profile.tax-town", "tax" to TownyUtil.money(townTax))) + if (resident.townBlocks.isNotEmpty()) add(tr("profile.tax-plots", "tax" to TownyUtil.money(plotTax))) + add(tr("profile.tax-total", "tax" to TownyUtil.money(townTax + plotTax))) + add(tr("town-status.new-day", "time" to TownyUtil.duration(player, TownyUtil.secondsUntilNewDay()))) + }.toTypedArray()) + } + + private fun towns(title: String, source: (Resident) -> List): Menu = ListMenu(player, title, this) { menu -> + TownyAPI.getInstance().getResident(player)?.let(source).orEmpty() + .sortedBy { it.name.lowercase() } + .map { town -> + MenuEntry({ Icons.town(player, town, "", tr("common.click-details")) }) { + TownInfoMenu(player, town, menu).open() + } + } + } + fun permissions(): Menu = PermissionMenu( player, tr("profile.permissions-title"), this, "towny:resident set perm", @@ -109,16 +197,35 @@ class ResidentMenu(player: Player, back: Menu?) : Menu(player, player.tr("profil key: String, label: String, description: String, - node: PermissionNodes, + node: PermissionNodes?, read: (Resident) -> Boolean ) = Toggle(material, label, description, node, "towny:resident toggle $key") { TownyAPI.getInstance().getResident(player)?.let(read) } - fun mode(material: Material, key: String, label: String, description: String, node: PermissionNodes) = + fun mode(material: Material, key: String, label: String, description: String, node: PermissionNodes?) = perm(material, key, label, description, node) { it.hasMode(key) } + val viewer = TownyAPI.getInstance().getResident(player) + val staff = listOfNotNull( + if (TownyUtil.can(player, PermissionNodes.TOWNY_CHAT_SPY)) mode( + Material.SPYGLASS, + "spy", + tr("toggle.spy"), + tr("toggle.spy-description"), + PermissionNodes.TOWNY_CHAT_SPY + ) else null, + // Admin bypass makes Towny deny the admin's own admin checks, so it can't be guarded by one. + if (player.isOp || player.hasPermission(PermissionNodes.TOWNY_ADMIN.node) || viewer?.hasMode("adminbypass") == true) mode( + Material.COMMAND_BLOCK, + "adminbypass", + tr("toggle.admin-bypass"), + tr("toggle.admin-bypass-description"), + null + ) else null, + ) + return ToggleMenu( player, tr("profile.settings-title"), this, listOf( perm( @@ -240,7 +347,7 @@ class ResidentMenu(player: Player, back: Menu?) : Menu(player, player.tr("profil tr("toggle.district-description"), PermissionNodes.TOWNY_COMMAND_RESIDENT_TOGGLE_DISTRICT ), - ) + ) + staff ) } } diff --git a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/resident/ResidentPlotsMenu.kt b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/resident/ResidentPlotsMenu.kt new file mode 100644 index 0000000..0d0ba26 --- /dev/null +++ b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/resident/ResidentPlotsMenu.kt @@ -0,0 +1,70 @@ +package net.trilleo.mc.plugins.townymenu.guis.resident + +import com.palmergames.bukkit.towny.`object`.Resident +import net.trilleo.mc.plugins.townymenu.guis.framework.Icons +import net.trilleo.mc.plugins.townymenu.guis.framework.Menu +import net.trilleo.mc.plugins.townymenu.guis.framework.MenuEntry +import net.trilleo.mc.plugins.townymenu.guis.framework.PagedMenu +import net.trilleo.mc.plugins.townymenu.guis.town.TownInfoMenu +import net.trilleo.mc.plugins.townymenu.guis.tutorial.Tutorial +import net.trilleo.mc.plugins.townymenu.utils.TownyUtil +import net.trilleo.mc.plugins.townymenu.utils.tr +import org.bukkit.Material +import org.bukkit.entity.Player + +/** Every plot [owner] owns, with where it is, its type, and what it costs them each day. */ +class ResidentPlotsMenu(player: Player, private val owner: Resident, back: Menu) : + PagedMenu(player, player.tr("resident-plots.title"), back) { + + override fun entries(): List = + owner.townBlocks + .sortedWith( + compareBy( + { it.townOrNull?.name?.lowercase() }, + { it.worldCoord.worldName }, + { it.x }, + { it.z }) + ) + .map { plot -> + val town = plot.townOrNull + MenuEntry({ + Icons.icon( + Material.GRASS_BLOCK, + "${if (plot.name.isNullOrBlank()) tr("plot.unnamed") else TownyUtil.text(plot.name)}", + null, + *buildList { + add(tr("icon.resident.town", "town" to (town?.let { TownyUtil.name(it.name) } ?: "-"))) + add( + tr( + "resident-plots.location", + "world" to TownyUtil.text(plot.worldCoord.worldName), + "x" to plot.x, + "z" to plot.z + ) + ) + add(tr("map.type", "type" to TownyUtil.plotType(player, plot.type.name))) + if (plot.hasPlotObjectGroup()) { + add(tr("plot.group", "group" to TownyUtil.name(plot.plotObjectGroup.name))) + } + if (TownyUtil.economy && town != null) { + if (plot.isForSale) add(tr("map.for-sale", "price" to TownyUtil.money(plot.plotPrice))) + if (plot.isTaxed) add( + tr( + "bank.daily-tax", + "tax" to TownyUtil.money(plot.type.getTax(town)) + ) + ) + } + if (town != null) { + add("") + add(tr("common.click-details")) + } + }.toTypedArray() + ) + }) { town?.let { TownInfoMenu(player, it, this).open() } } + } + + override fun controls() { + tutorialButton(52, Tutorial.PROFILE) + } +} diff --git a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/resident/ResidentProfileMenu.kt b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/resident/ResidentProfileMenu.kt index 8f43a99..ec39027 100644 --- a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/resident/ResidentProfileMenu.kt +++ b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/resident/ResidentProfileMenu.kt @@ -16,16 +16,16 @@ import org.bukkit.entity.Player * outlawing, and inviting. */ class ResidentProfileMenu(player: Player, private val target: Resident, back: Menu) : - Menu(player, TownyUtil.name(target.name), 5, back) { + Menu(player, TownyUtil.name(target.name), 6, back) { override fun build() { button(4, Icons.resident(player, target, *buildList { add(tr("profile.registered", "date" to TownyUtil.date(target.registered))) target.nationOrNull?.let { add(tr("icon.town.nation", "nation" to TownyUtil.name(it.name))) } - if (target.about.isNotBlank()) add(tr("profile.about-line", "about" to TownyUtil.text(target.about))) + TownyUtil.about(target)?.let { add(tr("profile.about-line", "about" to TownyUtil.text(it))) } }.toTypedArray())) - val viewer = resident ?: return backButton(40) + val viewer = resident ?: return backButton(49) val self = viewer == target val town = viewer.townOrNull val sameTown = town != null && town == target.townOrNull @@ -177,7 +177,7 @@ class ResidentProfileMenu(player: Player, private val target: Resident, back: Me } } - backButton(40) + backButton(49) } private fun editTitle(field: String, clear: Boolean, title: String, label: String, current: () -> String) { diff --git a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/town/JailSentenceMenu.kt b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/town/JailSentenceMenu.kt new file mode 100644 index 0000000..0ae5c6a --- /dev/null +++ b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/town/JailSentenceMenu.kt @@ -0,0 +1,121 @@ +package net.trilleo.mc.plugins.townymenu.guis.town + +import com.palmergames.bukkit.towny.TownySettings +import com.palmergames.bukkit.towny.`object`.Resident +import com.palmergames.bukkit.towny.`object`.Town +import com.palmergames.bukkit.towny.`object`.jail.Jail +import com.palmergames.bukkit.towny.permissions.PermissionNodes +import net.trilleo.mc.plugins.townymenu.guis.common.Pickers +import net.trilleo.mc.plugins.townymenu.guis.framework.Icons +import net.trilleo.mc.plugins.townymenu.guis.framework.Menu +import net.trilleo.mc.plugins.townymenu.utils.TownyUtil +import net.trilleo.mc.plugins.townymenu.utils.tr +import org.bukkit.Material +import org.bukkit.entity.Player + +/** + * Chooses the sentence for jailing [target] — hours, bail, jail, and cell — then + * runs `/town jail`. The choices live in this menu until the player confirms. + */ +class JailSentenceMenu(player: Player, private val town: Town, private val target: Resident, back: Menu) : + Menu(player, player.tr("jail-sentence.title", "player" to TownyUtil.name(target.name)), 5, back) { + + private val jails: List + get() = TownJailMenu.jails(town) + + private var hours = "2" + private var bail = TownySettings.getBailAmount().toBigDecimal().stripTrailingZeros().toPlainString() + private var jailNumber = town.primaryJail?.let { jails.indexOf(it) + 1 }?.takeIf { it > 0 } ?: 1 + + /** Towny's cell index starts at 0; it is shown to players starting at 1. */ + private var cell = 0 + + private val bailEnabled: Boolean + get() = TownySettings.isAllowingBail() && TownyUtil.economy + + override fun build() { + button(4, Icons.resident(player, target)) + val jail = jails.elementAtOrNull(jailNumber - 1) + if (jail == null) { + button(22, Icons.icon(Material.BARRIER, tr("town-jail.no-jails"))) + return backButton(40) + } + if (cell >= jail.jailCellCount) cell = 0 + + val row = layout(20, 21, 22, 23, 24) + row.add( + Icons.icon( + Material.CLOCK, tr("jail-sentence.hours"), tr("jail-sentence.hours-description"), + tr("common.current", "value" to hours), + tr("jail-sentence.max-hours", "hours" to TownySettings.getJailedMaxHours()) + ) + ) { + prompt(tr("jail-sentence.hours-title"), tr("jail-sentence.hours-label"), initial = hours, maxLength = 5) { + hours = TownyUtil.argument(it) + open() + } + } + if (bailEnabled) { + row.add( + Icons.icon( + Material.GOLD_NUGGET, tr("jail-sentence.bail"), tr("jail-sentence.bail-description"), + tr("common.current", "value" to bail), + tr("jail-sentence.max-bail", "bail" to TownyUtil.money(TownySettings.getBailMaxAmount())) + ) + ) { + prompt(tr("jail-sentence.bail-title"), tr("common.amount"), initial = bail, maxLength = 12) { + bail = TownyUtil.argument(it) + open() + } + } + } + if (jails.size > 1) { + row.add( + Icons.icon( + Material.IRON_BARS, tr("jail-sentence.jail"), null, + tr("common.current", "value" to TownJailMenu.jailName(this, town, jail)), + tr("common.click-change") + ) + ) { + val options = jails.mapIndexed { index, option -> + (index + 1).toString() to "${TownJailMenu.jailName(this, town, option)}" + } + Pickers.option(this, tr("jail-sentence.jail"), options, jailNumber.toString(), Material.IRON_BARS) { + jailNumber = it.toInt() + cell = 0 + open() + }.open() + } + } + if (jail.jailCellCount > 1) { + row.add( + Icons.icon( + Material.IRON_CHAIN, tr("jail-sentence.cell"), null, + tr("common.current", "value" to cell + 1), + tr("common.click-change") + ) + ) { + val options = (0 until jail.jailCellCount).map { + it.toString() to tr("jail-sentence.cell-number", "number" to it + 1) + } + Pickers.option(this, tr("jail-sentence.cell"), options, cell.toString(), Material.IRON_CHAIN) { + cell = it.toInt() + open() + }.open() + } + } + row.add( + PermissionNodes.TOWNY_COMMAND_TOWN_JAIL, + Icons.icon(Material.LIME_CONCRETE, tr("jail-sentence.confirm"), tr("jail-sentence.confirm-description")) + ) { + val bailArgument = if (bailEnabled) " $bail" else "" + run( + "towny:town jail ${target.name} $hours$bailArgument $jailNumber $cell", + { target.isJailed }, + returnTo = back ?: this + ) + } + + backButton(40) + } +} diff --git a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/town/NoTownMenu.kt b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/town/NoTownMenu.kt index 54c8883..4e4db35 100644 --- a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/town/NoTownMenu.kt +++ b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/town/NoTownMenu.kt @@ -12,15 +12,26 @@ import net.trilleo.mc.plugins.townymenu.utils.tr import org.bukkit.Material import org.bukkit.entity.Player -/** Shown to players without a town: found one, browse towns to join, or answer invites. */ -class NoTownMenu(player: Player, back: Menu) : Menu(player, player.tr("no-town.title"), 3, back) { +/** Shown to players without a town: found one, browse towns to join, answer invites, or reclaim a ruined town. */ +class NoTownMenu(player: Player, back: Menu) : Menu(player, player.tr("no-town.title"), 4, back) { override fun build() { + val reclaimable = + TownySettings.getTownRuinsReclaimEnabled() && TownySettings.canRuinsBeReclaimedByTownlessPlayers() + val row = if (reclaimable) layout(10, 12, 14, 16) else layout(11, 13, 15) val price = - if (TownyUtil.economy) tr("common.cost", "cost" to TownyUtil.money(TownySettings.getNewTownPrice())) else "" - guarded( - 11, PermissionNodes.TOWNY_COMMAND_TOWN_NEW, - Icons.icon(Material.BELL, tr("no-town.found"), tr("no-town.found-description"), price) + if (TownyUtil.economy) tr( + "common.cost", + "cost" to TownyUtil.money(TownySettings.getNewTownPrice()) + ) else null + row.add( + PermissionNodes.TOWNY_COMMAND_TOWN_NEW, + Icons.icon( + Material.BELL, + tr("no-town.found"), + tr("no-town.found-description"), + *listOfNotNull(price).toTypedArray() + ) ) { prompt(tr("no-town.found-title"), tr("no-town.town-name")) { name -> run( @@ -30,12 +41,11 @@ class NoTownMenu(player: Player, back: Menu) : Menu(player, player.tr("no-town.t ) } } - button(13, Icons.icon(Material.OAK_DOOR, tr("no-town.browse"), tr("no-town.browse-description"))) { + row.add(Icons.icon(Material.OAK_DOOR, tr("no-town.browse"), tr("no-town.browse-description"))) { TownListMenu(player, this).open() } val invites = resident?.receivedInvites?.size ?: 0 - button( - 15, + row.add( Icons.icon( Material.PAPER, tr("main.invites"), @@ -45,7 +55,21 @@ class NoTownMenu(player: Player, back: Menu) : Menu(player, player.tr("no-town.t ) { InvitesMenu(player, this).open() } - tutorialButton(26, Tutorial.FINDING_A_TOWN) - backButton(22) + if (reclaimable) { + val cost = if (TownyUtil.economy) { + tr("common.cost", "cost" to TownyUtil.money(TownySettings.getEcoPriceReclaimTown())) + } else null + row.add( + PermissionNodes.TOWNY_COMMAND_TOWN_RECLAIM, + Icons.icon( + Material.MOSSY_STONE_BRICKS, tr("no-town.reclaim"), tr("no-town.reclaim-description"), + *listOfNotNull(cost).toTypedArray() + ) + ) { + run("towny:town reclaim", { resident?.hasTown() }, returnTo = back ?: MainMenu(player)) + } + } + tutorialButton(35, Tutorial.FINDING_A_TOWN) + backButton(31) } } diff --git a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/town/TownClaimsMenu.kt b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/town/TownClaimsMenu.kt index 9521756..a806e69 100644 --- a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/town/TownClaimsMenu.kt +++ b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/town/TownClaimsMenu.kt @@ -4,6 +4,7 @@ import com.palmergames.bukkit.towny.TownySettings import com.palmergames.bukkit.towny.`object`.Town import com.palmergames.bukkit.towny.permissions.PermissionNodes import net.trilleo.mc.plugins.townymenu.guis.MapMenu +import net.trilleo.mc.plugins.townymenu.guis.common.Pickers import net.trilleo.mc.plugins.townymenu.guis.framework.Icons import net.trilleo.mc.plugins.townymenu.guis.framework.Menu import net.trilleo.mc.plugins.townymenu.guis.tutorial.Tutorial @@ -13,13 +14,13 @@ import org.bukkit.Material import org.bukkit.entity.Player /** Claiming, unclaiming, outposts, and bonus claim purchases for the viewer's town. */ -class TownClaimsMenu(player: Player, back: Menu) : Menu(player, player.tr("claims.title"), 5, back) { +class TownClaimsMenu(player: Player, back: Menu) : Menu(player, player.tr("claims.title"), 6, back) { private val town: Town? get() = resident?.townOrNull override fun build() { - val town = town ?: return backButton(40) + val town = town ?: return backButton(49) button(4, Icons.icon(Material.GRASS_BLOCK, tr("claims.info"), null, *buildList { add(tr("claims.claimed", "claims" to town.numTownBlocks, "max" to town.maxTownBlocksAsAString)) @@ -34,7 +35,7 @@ class TownClaimsMenu(player: Player, back: Menu) : Menu(player, player.tr("claim if (TownyUtil.economy) add(tr("claims.price", "price" to TownyUtil.money(town.townBlockCost))) }.toTypedArray())) - val grid = layout(19, 20, 21, 22, 23, 24, 25, 29, 30, 31, 32, 33) + val grid = layout(19, 20, 21, 22, 23, 24, 25, 28, 29, 30, 31, 32, 33, 34) val claims = { town.numTownBlocks } grid.add( @@ -109,6 +110,22 @@ class TownClaimsMenu(player: Player, back: Menu) : Menu(player, player.tr("claim } } } + grid.add( + PermissionNodes.TOWNY_COMMAND_TOWN_CEDE_PLOT, + Icons.icon(Material.OAK_BOAT, tr("claims.cede"), tr("claims.cede-description")) + ) { + Pickers.town(this, tr("claims.cede-title"), { it != town }) { picked -> + run("towny:town cede plot ${picked.name}") + }.open() + } + if (TownySettings.isOverClaimingAllowingStolenLand()) { + grid.add( + PermissionNodes.TOWNY_COMMAND_TOWN_TAKEOVERCLAIM, + Icons.icon(Material.IRON_SWORD, tr("claims.takeover"), tr("claims.takeover-description")) + ) { + run("towny:town takeoverclaim", claims, delayTicks = CLAIM_DELAY) + } + } val autoClaim = resident?.hasMode("townclaim") == true grid.add( PermissionNodes.TOWNY_COMMAND_RESIDENT_TOGGLE_TOWNCLAIM, @@ -126,8 +143,8 @@ class TownClaimsMenu(player: Player, back: Menu) : Menu(player, player.tr("claim MapMenu(player, this).open() } - tutorialButton(44, Tutorial.CLAIMS) - backButton(40) + tutorialButton(53, Tutorial.CLAIMS) + backButton(49) } private companion object { diff --git a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/town/TownInfoMenu.kt b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/town/TownInfoMenu.kt index 0b56744..4ed88e3 100644 --- a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/town/TownInfoMenu.kt +++ b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/town/TownInfoMenu.kt @@ -12,12 +12,12 @@ import org.bukkit.entity.Player /** Public view of any town: visit, join, donate, buy, or invite it to the viewer's nation. */ class TownInfoMenu(player: Player, private val town: Town, back: Menu) : - Menu(player, TownyUtil.name(town.name), 4, back) { + Menu(player, TownyUtil.name(town.name), 5, back) { override fun build() { if (!town.exists()) { button(13, Icons.icon(Material.BARRIER, tr("town-info.gone"))) - return backButton(31) + return backButton(40) } button(4, Icons.town(player, town, tr("common.founded", "date" to TownyUtil.date(town.registered)))) @@ -32,10 +32,12 @@ class TownInfoMenu(player: Player, private val town: Town, back: Menu) : grid.add( Icons.icon( Material.ENDER_PEARL, tr("town-info.visit"), tr("town-info.visit-description"), - if (TownyUtil.economy && town.spawnCost > 0) tr( - "common.cost", - "cost" to TownyUtil.money(town.spawnCost) - ) else "" + *listOfNotNull( + if (TownyUtil.economy && town.spawnCost > 0) tr( + "common.cost", + "cost" to TownyUtil.money(town.spawnCost) + ) else null + ).toTypedArray() ) ) { runAndClose("towny:town spawn ${town.name}") @@ -92,6 +94,22 @@ class TownInfoMenu(player: Player, private val town: Town, back: Menu) : ) ) { run("towny:town buytown ${town.name}", { town.mayor }, returnTo = MainMenu(player)) } } + val viewerTown = viewer?.townOrNull + if (viewerTown != null && viewerTown != town && viewer.isMayor) { + grid.add( + PermissionNodes.TOWNY_COMMAND_TOWN_MERGE, + Icons.icon( + Material.STRUCTURE_VOID, tr("town-info.merge"), + tr( + "town-info.merge-description", + "town" to TownyUtil.name(town.name), + "own" to TownyUtil.name(viewerTown.name) + ) + ) + ) { + run("towny:town merge ${town.name}") + } + } val nation = viewer?.nationOrNull if (nation != null && !town.hasNation() && viewer.isKing) { grid.add( @@ -106,6 +124,6 @@ class TownInfoMenu(player: Player, private val town: Town, back: Menu) : } } - backButton(31) + backButton(40) } } diff --git a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/town/TownJailMenu.kt b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/town/TownJailMenu.kt new file mode 100644 index 0000000..1860781 --- /dev/null +++ b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/town/TownJailMenu.kt @@ -0,0 +1,100 @@ +package net.trilleo.mc.plugins.townymenu.guis.town + +import com.palmergames.bukkit.towny.TownySettings +import com.palmergames.bukkit.towny.`object`.Resident +import com.palmergames.bukkit.towny.`object`.Town +import com.palmergames.bukkit.towny.`object`.jail.Jail +import com.palmergames.bukkit.towny.permissions.PermissionNodes +import net.trilleo.mc.plugins.townymenu.guis.common.Pickers +import net.trilleo.mc.plugins.townymenu.guis.framework.* +import net.trilleo.mc.plugins.townymenu.guis.tutorial.Tutorial +import net.trilleo.mc.plugins.townymenu.utils.TownyUtil +import net.trilleo.mc.plugins.townymenu.utils.tr +import org.bukkit.Material +import org.bukkit.entity.Player + +/** Prisoners held in the viewer's town jails. Staff can jail online residents and pardon prisoners. */ +class TownJailMenu(player: Player, private val town: Town, back: Menu) : + PagedMenu(player, player.tr("town-jail.title"), back) { + + private fun count() = town.jailedPlayerCount + + override fun entries(): List { + val canUnjail = TownyUtil.can(player, PermissionNodes.TOWNY_COMMAND_TOWN_UNJAIL) + return town.jailedResidents.sortedBy { it.name.lowercase() }.map { prisoner -> + val lines = buildList { + add("") + add(tr("town-jail.hours-left", "hours" to prisoner.jailHours)) + if (TownyUtil.economy && prisoner.jailBailCost > 0) { + add(tr("town-jail.bail", "bail" to TownyUtil.money(prisoner.jailBailCost))) + } + prisoner.jail?.let { jail -> + add( + tr( + "town-jail.held-in", + "jail" to jailName(this@TownJailMenu, town, jail), + "cell" to prisoner.jailCell + 1 + ) + ) + } + if (canUnjail) add(tr("town-jail.click-unjail")) + } + MenuEntry({ Icons.resident(player, prisoner, *lines.toTypedArray()) }) { + if (canUnjail) run("towny:town unjail ${prisoner.name}", ::count) + } + } + } + + override fun controls() { + tutorialButton(52, Tutorial.PROTECTION) + if (town.hasJails()) { + guarded( + 47, PermissionNodes.TOWNY_COMMAND_TOWN_JAIL, + Icons.icon(Material.IRON_BARS, tr("town-jail.jail-player"), tr("town-jail.jail-player-description")) + ) { + Pickers.resident(this, tr("town-jail.jail-player-title"), ::canJail) { picked -> + JailSentenceMenu(player, town, picked, this).open() + }.open() + } + } else { + button(47, Icons.icon(Material.BARRIER, tr("town-jail.no-jails"), tr("town-jail.no-jails-description"))) + } + button( + 51, Icons.icon( + Material.IRON_CHAIN, tr("town-jail.jails"), tr("town-jail.jails-description"), + tr("town-jail.jails-count", "count" to jails(town).size) + ) + ) { jailList().open() } + } + + /** Towny only jails online residents of the town, or of the nation when its capital may jail nation residents. */ + private fun canJail(target: Resident): Boolean = + !target.isJailed && (target.townOrNull == town || + (TownySettings.canNationLeadersJailNationResidents() && town.isCapital && + town.nationOrNull?.hasResident(target) == true)) + + private fun jailList(): Menu = ListMenu(player, tr("town-jail.jails-title"), this) { + jails(town).map { jail -> + MenuEntry({ + val block = jail.townBlock + Icons.icon( + Material.IRON_BARS, "${jailName(this, town, jail)}", null, *listOfNotNull( + if (jail == town.primaryJail) tr("town-jail.primary") else null, + tr("map.chunk", "x" to block.x, "z" to block.z), + tr("town-jail.cells", "count" to jail.jailCellCount), + tr("town-jail.prisoners", "count" to town.jailedResidents.count { it.jail == jail }), + ).toTypedArray() + ) + }) + } + } + + companion object { + fun jails(town: Town): List = town.jails.orEmpty().toList() + + /** The jail plot's name, or its number as used by `/town jail`. */ + fun jailName(menu: Menu, town: Town, jail: Jail): String = + if (jail.hasName()) TownyUtil.text(jail.name) + else menu.tr("town-jail.jail-number", "number" to jails(town).indexOf(jail) + 1) + } +} diff --git a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/town/TownListMenu.kt b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/town/TownListMenu.kt index bd31e86..404dbe7 100644 --- a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/town/TownListMenu.kt +++ b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/town/TownListMenu.kt @@ -2,6 +2,7 @@ package net.trilleo.mc.plugins.townymenu.guis.town import com.palmergames.bukkit.towny.TownyAPI import com.palmergames.bukkit.towny.`object`.Town +import com.palmergames.bukkit.towny.permissions.PermissionNodes import net.trilleo.mc.plugins.townymenu.guis.framework.Icons import net.trilleo.mc.plugins.townymenu.guis.framework.Menu import net.trilleo.mc.plugins.townymenu.guis.framework.MenuEntry @@ -16,34 +17,39 @@ import org.bukkit.entity.Player /** A directory of every town, sortable, doubling as the town leaderboard. */ class TownListMenu(player: Player, back: Menu) : PagedMenu(player, player.tr("town-list.title"), back) { - private enum class Sort(val label: String, val comparator: Comparator) { - RESIDENTS("sort.residents", compareByDescending { it.numResidents }), - CLAIMS("sort.claims", compareByDescending { it.numTownBlocks }), - BALANCE("sort.balance", compareByDescending { it.account.cachedBalance }), - ONLINE("sort.online", compareByDescending { town -> town.residents.count { it.isOnline } }), - OPEN("sort.open", compareByDescending { it.isOpen }.thenByDescending { it.numResidents }), - NAME("sort.name", compareBy { it.name.lowercase() }), + private enum class Sort(val label: String, val comparator: (Player) -> Comparator) { + RESIDENTS("sort.residents", { compareByDescending { it.numResidents } }), + CLAIMS("sort.claims", { compareByDescending { it.numTownBlocks } }), + BALANCE("sort.balance", { compareByDescending { it.account.cachedBalance } }), + ONLINE("sort.online", { compareByDescending { town -> town.residents.count { it.isOnline } } }), + OPEN("sort.open", { compareByDescending { it.isOpen }.thenByDescending { it.numResidents } }), + NEARBY("sort.nearby", { player -> compareBy { distance(player, it) ?: Double.MAX_VALUE } }), + NAME("sort.name", { compareBy { it.name.lowercase() } }), } private var sort = Sort.RESIDENTS override fun entries(): List = TownyAPI.getInstance().towns - .sortedWith(sort.comparator) + .sortedWith(sort.comparator(player)) .map { town -> - MenuEntry({ - Icons.town( - player, - town, - "", - tr("common.click-details") - ) - }) { TownInfoMenu(player, town, this).open() } + val lines = listOfNotNull( + "", + distance(player, town)?.takeIf { sort == Sort.NEARBY } + ?.let { tr("town-list.distance", "blocks" to it.toLong()) }, + tr("common.click-details"), + ) + MenuEntry({ Icons.town(player, town, *lines.toTypedArray()) }) { + TownInfoMenu(player, town, this).open() + } } override fun controls() { tutorialButton(52, Tutorial.FINDING_A_TOWN) - val sorts = Sort.entries.filter { it != Sort.BALANCE || TownyUtil.economy } + val sorts = Sort.entries.filter { + (it != Sort.BALANCE || TownyUtil.economy) && + (it != Sort.NEARBY || TownyUtil.can(player, PermissionNodes.TOWNY_COMMAND_TOWN_NEARBY)) + } button(47, itemStack(Material.HOPPER) { name(tr("sort.current", "sort" to tr(sort.label))) lore(sorts.map { (if (it == sort) "▶ " else " ") + tr(it.label) }) @@ -53,4 +59,11 @@ class TownListMenu(player: Player, back: Menu) : PagedMenu(player, player.tr("to render() } } + + private companion object { + /** Blocks between [player] and [town]'s spawn, or `null` when the town has no spawn in this world. */ + fun distance(player: Player, town: Town): Double? = town.spawnOrNull + ?.takeIf { it.world == player.world } + ?.distance(player.location) + } } diff --git a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/town/TownMembersMenu.kt b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/town/TownMembersMenu.kt index 70ff021..014cd22 100644 --- a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/town/TownMembersMenu.kt +++ b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/town/TownMembersMenu.kt @@ -63,6 +63,14 @@ class TownMembersMenu(player: Player, private val town: Town, back: Menu) : tr("common.player-name") ) { name -> run("towny:town add ${TownyUtil.argument(name)}", sent) } } + guarded( + 50, PermissionNodes.TOWNY_COMMAND_TOWN_SAY, + Icons.icon(Material.BELL, tr("town-members.announce"), tr("town-members.announce-description")) + ) { + prompt(tr("town-members.announce-title"), tr("common.message"), maxLength = 200) { text -> + run("towny:town say $text") + } + } val pending = town.sentInvites guarded( 51, PermissionNodes.TOWNY_COMMAND_TOWN_INVITE_ADD, Icons.icon( diff --git a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/town/TownMenu.kt b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/town/TownMenu.kt index 608a55f..ff6b23a 100644 --- a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/town/TownMenu.kt +++ b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/town/TownMenu.kt @@ -1,6 +1,7 @@ package net.trilleo.mc.plugins.townymenu.guis.town import com.palmergames.bukkit.towny.TownyAPI +import com.palmergames.bukkit.towny.TownySettings import com.palmergames.bukkit.towny.`object`.Town import com.palmergames.bukkit.towny.permissions.PermissionNodes import net.trilleo.mc.plugins.townymenu.guis.MainMenu @@ -59,9 +60,18 @@ class TownMenu(player: Player, back: Menu?) : Menu(player, player.tr("town.title "explosions" to TownyUtil.onOff(player, town.isExplosion) ) ) - }.toTypedArray())) + val warnings = TownStatusMenu.warnings(this@TownMenu, town) + if (warnings.isNotEmpty()) { + add("") + warnings.forEach { add(it.line) } + } + add("") + add(tr("town.click-status")) + }.toTypedArray())) { + TownStatusMenu(player, town, this).open() + } - val grid = layout(19, 20, 21, 22, 23, 24, 25, 28, 29, 30, 31, 32, 33, 34, 37, 38, 39, 40, 41, 42, 43) + val grid = layout(19, 20, 21, 22, 23, 24, 25, 28, 29, 30, 31, 32, 33, 34) grid.add( Icons.icon( @@ -121,6 +131,28 @@ class TownMenu(player: Player, back: Menu?) : Menu(player, player.tr("town.title ) { OutlawsMenu(player, town, this).open() } + if (town.isRuined) { + grid.add( + PermissionNodes.TOWNY_COMMAND_TOWN_RECLAIM, + Icons.icon( + Material.MOSSY_STONE_BRICKS, tr("town.reclaim"), tr("town.reclaim-description"), + *reclaimLines().toTypedArray() + ) + ) { + run("towny:town reclaim", { town.isRuined }) + } + } else { + grid.add( + Icons.icon( + Material.IRON_CHAIN, + tr("town.jail"), + tr("town.jail-description"), + tr("town.jail-count", "count" to town.jailedPlayerCount) + ) + ) { + TownJailMenu(player, town, this).open() + } + } grid.add(Icons.icon(Material.ENDER_PEARL, tr("town.spawn"), tr("town.spawn-description"))) { runAndClose("towny:town spawn") } @@ -168,6 +200,13 @@ class TownMenu(player: Player, back: Menu?) : Menu(player, player.tr("town.title backButton(49) } + /** The cost and waiting time Towny applies before a ruined town can be reclaimed. */ + private fun reclaimLines(): List = buildList { + if (TownyUtil.economy) add(tr("common.cost", "cost" to TownyUtil.money(TownySettings.getEcoPriceReclaimTown()))) + TownySettings.getTownRuinsMinDurationHours().takeIf { it > 0 } + ?.let { add(tr("town.reclaim-wait", "hours" to it)) } + } + fun permissions(): Menu = PermissionMenu( player, tr("town.permissions-title"), this, "towny:town set perm", @@ -243,6 +282,14 @@ class TownMenu(player: Player, back: Menu?) : Menu(player, player.tr("town.title tr("toggle.tax-percent-description"), PermissionNodes.TOWNY_COMMAND_TOWN_TOGGLE_TAXPERCENT ) { it.isTaxPercentage }, + ) + listOfNotNull( + if ((town?.nationZoneSize ?: 0) > 0) toggle( + Material.BEACON, + "nationzone", + tr("toggle.nation-zone"), + tr("toggle.nation-zone-description"), + PermissionNodes.TOWNY_COMMAND_TOWN_TOGGLE_NATIONZONE + ) { it.isNationZoneEnabled } else null ) ) } diff --git a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/town/TownSettingsMenu.kt b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/town/TownSettingsMenu.kt index 7176a43..b0684aa 100644 --- a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/town/TownSettingsMenu.kt +++ b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/town/TownSettingsMenu.kt @@ -20,36 +20,7 @@ class TownSettingsMenu(player: Player, back: Menu) : Menu(player, player.tr("tow override fun build() { val town = town ?: return backButton(49) - val grid = layout( - 10, - 11, - 12, - 13, - 14, - 15, - 16, - 19, - 20, - 21, - 22, - 23, - 24, - 25, - 28, - 29, - 30, - 31, - 32, - 33, - 34, - 37, - 38, - 39, - 40, - 41, - 42, - 43 - ) + val grid = layout(10, 11, 12, 13, 14, 15, 16, 19, 20, 21, 22, 23, 24, 25, 28, 29, 30, 31, 32, 33, 34) grid.add( PermissionNodes.TOWNY_COMMAND_TOWN_SET_NAME, @@ -74,7 +45,7 @@ class TownSettingsMenu(player: Player, back: Menu) : Menu(player, player.tr("tow prompt( tr("town-details.board-title"), tr("common.message"), - initial = town.board, + initial = TownyUtil.board(town).orEmpty(), maxLength = 256, multiline = true ) { board -> diff --git a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/town/TownStatusMenu.kt b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/town/TownStatusMenu.kt new file mode 100644 index 0000000..3de4975 --- /dev/null +++ b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/town/TownStatusMenu.kt @@ -0,0 +1,182 @@ +package net.trilleo.mc.plugins.townymenu.guis.town + +import com.palmergames.bukkit.towny.TownySettings +import com.palmergames.bukkit.towny.`object`.Town +import com.palmergames.bukkit.towny.utils.TownRuinUtil +import net.trilleo.mc.plugins.townymenu.guis.framework.Icons +import net.trilleo.mc.plugins.townymenu.guis.framework.Menu +import net.trilleo.mc.plugins.townymenu.guis.tutorial.Tutorial +import net.trilleo.mc.plugins.townymenu.utils.TownyUtil +import net.trilleo.mc.plugins.townymenu.utils.tr +import org.bukkit.Material +import org.bukkit.entity.Player +import org.bukkit.inventory.ItemStack + +/** A town's level, limits, and daily upkeep, with a warning for every problem Towny would act on. */ +class TownStatusMenu(player: Player, private val town: Town, back: Menu) : + Menu(player, player.tr("town-status.title", "town" to TownyUtil.name(town.name)), 6, back) { + + override fun build() { + if (!town.exists()) { + button(22, Icons.icon(Material.BARRIER, tr("town-info.gone"))) + return backButton(49) + } + button(4, Icons.town(player, town)) + + val info = layout(19, 20, 21, 22, 23, 24, 25) + info.add(level()) + info.add(claims()) + if (TownySettings.isAllowingOutposts()) { + info.add( + Icons.icon( + Material.COMPASS, tr("town-status.outposts"), tr("town-status.outposts-description"), + tr("claims.outposts", "outposts" to town.maxOutpostSpawn, "limit" to town.outpostLimit) + ) + ) + } + if (TownyUtil.economy) info.add(upkeep()) + if (town.hasNation()) { + info.add( + Icons.icon( + Material.BEACON, tr("town-status.nation-zone"), tr("town-status.nation-zone-description"), + tr("town-status.nation-zone-size", "size" to town.nationZoneSize), + tr("icon.currently", "value" to TownyUtil.onOff(player, town.isNationZoneEnabled)) + ) + ) + } + + val problems = warnings(this, town) + if (problems.isEmpty()) { + button( + 31, + Icons.icon(Material.LIME_DYE, tr("town-status.all-good"), tr("town-status.all-good-description")) + ) + } else { + val row = layout(28, 29, 30, 31, 32, 33, 34) + problems.forEach { row.add(it.icon) } + } + + tutorialButton(53, Tutorial.TOWN) + backButton(49) + } + + private fun level(): ItemStack { + val number = TownySettings.getTownLevelNumber(town) + val thresholds = TownySettings.getConfigTownLevel().keys.sorted() + val byClaims = TownySettings.isTownLevelDeterminedByTownBlockCount() + val progress = if (byClaims) town.numTownBlocks else town.numResidents + val next = thresholds.getOrNull(number + 1) + return Icons.icon( + Material.EXPERIENCE_BOTTLE, tr("town-status.level"), tr("town-status.level-description"), *buildList { + add(tr("town-status.level-number", "level" to number, "max" to TownySettings.getTownLevelMax())) + add(tr("town-status.level-name", "name" to TownyUtil.name(town.formattedName))) + add( + when { + town.manualTownLevel > -1 -> tr("town-status.level-manual") + next == null -> tr("town-status.level-highest") + byClaims -> tr("town-status.next-level-claims", "current" to progress, "needed" to next) + else -> tr("town-status.next-level-residents", "current" to progress, "needed" to next) + } + ) + }.toTypedArray() + ) + } + + private fun claims(): ItemStack = Icons.icon( + Material.GRASS_BLOCK, tr("claims.info"), tr("town-status.claims-description"), *buildList { + add(tr("claims.claimed", "claims" to town.numTownBlocks, "max" to town.maxTownBlocksAsAString)) + add( + tr( + "claims.available", + "available" to if (town.hasUnlimitedClaims()) tr("claims.unlimited") else town.availableTownBlocks() + ) + ) + TownySettings.getTownBlockRatio().takeIf { it > 0 } + ?.let { add(tr("tutorial.fact.claims-per-resident", "count" to it)) } + add(tr("claims.bonus", "bonus" to town.bonusBlocks, "purchased" to town.purchasedBlocks)) + if (TownyUtil.economy) { + add(tr("town-status.max-purchased", "count" to TownySettings.getMaxPurchasedBlocks(town))) + } + }.toTypedArray() + ) + + private fun upkeep(): ItemStack { + val upkeep = if (town.hasUpkeep()) TownySettings.getTownUpkeepCost(town) else 0.0 + val penalty = if (town.isOverClaimed) TownySettings.getTownPenaltyUpkeepCost(town) else 0.0 + val peaceful = if (town.isNeutral) TownySettings.getTownNeutralityCost(town) else 0.0 + return Icons.icon( + Material.HOPPER, tr("town-status.upkeep"), tr("town-status.upkeep-description"), *buildList { + if (!TownySettings.isTaxingDaily()) add( + tr( + "tutorial.fact.daily-taxes", + "value" to TownyUtil.onOff(player, false) + ) + ) + if (!town.hasUpkeep()) add(tr("town-status.upkeep-exempt")) + add(tr("town-status.upkeep-daily", "cost" to TownyUtil.money(upkeep))) + if (penalty > 0) add(tr("town-status.upkeep-penalty", "cost" to TownyUtil.money(penalty))) + if (peaceful > 0) add(tr("town-status.upkeep-peaceful", "cost" to TownyUtil.money(peaceful))) + add(tr("town-status.upkeep-total", "cost" to TownyUtil.money(upkeep + penalty + peaceful))) + add(tr("bank.balance", "balance" to TownyUtil.balance(town))) + add(tr("town-status.new-day", "time" to TownyUtil.duration(player, TownyUtil.secondsUntilNewDay()))) + }.toTypedArray() + ) + } + + /** One problem with a town, as a short [line] for summaries and a detailed [icon]. */ + class Warning(val line: String, val icon: ItemStack) + + companion object { + /** Every problem Towny would act on for [town]: ruins, debt, conquest, overclaiming, and unpaid upkeep. */ + fun warnings(menu: Menu, town: Town): List = buildList { + fun warn(material: Material, title: String, vararg lines: String) = + add(Warning(title, Icons.icon(material, title, null, *lines))) + + if (town.isRuined) { + val left = TownySettings.getTownRuinsMaxDurationHours() - TownRuinUtil.getTimeSinceRuining(town) + warn( + Material.CRACKED_STONE_BRICKS, menu.tr("town-status.ruined"), + menu.tr("town-status.ruined-hours", "hours" to left.coerceAtLeast(0)) + ) + } + if (TownyUtil.economy && town.isBankrupt) { + warn( + Material.BARRIER, menu.tr("town-status.bankrupt"), + menu.tr("tutorial.fact.debt", "debt" to TownyUtil.money(town.debtBalance)), + menu.tr("town-status.debt-cap", "cap" to TownyUtil.money(town.account.debtCap)), + menu.tr("town-status.bankrupt-description") + ) + } + if (town.isConquered) { + warn( + Material.IRON_SWORD, menu.tr("town-status.conquered"), + menu.tr("town-status.conquered-days", "days" to town.conqueredDays) + ) + } + if (town.isOverClaimed) { + warn( + Material.TNT, menu.tr("town-status.overclaimed"), + menu.tr("claims.claimed", "claims" to town.numTownBlocks, "max" to town.maxTownBlocksAsAString), + menu.tr( + if (TownySettings.isOverClaimingAllowingStolenLand()) "town-status.overclaimed-stealable" + else "town-status.overclaimed-description" + ) + ) + } + if (TownyUtil.economy && TownySettings.isTaxingDaily() && !town.isRuined && !town.isBankrupt) { + val due = (if (town.hasUpkeep()) TownySettings.getTownUpkeepCost(town) else 0.0) + + (if (town.isOverClaimed) TownySettings.getTownPenaltyUpkeepCost(town) else 0.0) + + (if (town.isNeutral) TownySettings.getTownNeutralityCost(town) else 0.0) + if (due > town.account.cachedBalance) { + warn( + Material.HOPPER, menu.tr("town-status.upkeep-short"), + menu.tr("town-status.upkeep-total", "cost" to TownyUtil.money(due)), + menu.tr("bank.balance", "balance" to TownyUtil.balance(town)), + menu.tr("town-status.upkeep-short-description") + ) + } + } + } + } +} + diff --git a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/tutorial/Tutorial.kt b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/tutorial/Tutorial.kt index 767ac18..8bf5516 100644 --- a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/tutorial/Tutorial.kt +++ b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/tutorial/Tutorial.kt @@ -7,12 +7,11 @@ import com.palmergames.bukkit.towny.`object`.Resident import com.palmergames.bukkit.towny.`object`.Town import com.palmergames.bukkit.towny.`object`.TownyPermission import net.trilleo.mc.plugins.townymenu.Main -import net.trilleo.mc.plugins.townymenu.guis.InvitesMenu -import net.trilleo.mc.plugins.townymenu.guis.MainMenu -import net.trilleo.mc.plugins.townymenu.guis.MapMenu +import net.trilleo.mc.plugins.townymenu.guis.* import net.trilleo.mc.plugins.townymenu.guis.common.BankMenu import net.trilleo.mc.plugins.townymenu.guis.framework.Menu import net.trilleo.mc.plugins.townymenu.guis.nation.* +import net.trilleo.mc.plugins.townymenu.guis.plot.PlotAreaMenu import net.trilleo.mc.plugins.townymenu.guis.plot.PlotGroupMenu import net.trilleo.mc.plugins.townymenu.guis.plot.PlotMenu import net.trilleo.mc.plugins.townymenu.guis.resident.FriendsMenu @@ -200,6 +199,10 @@ object Tutorial { Lesson( "start/invites", Material.PAPER, "tutorial.start.invites", "tutorial.start.invites-body", always { player, back -> InvitesMenu(player, back) }), + Lesson( + "start/leaderboards", Material.LECTERN, "tutorial.start.leaderboards", + "tutorial.start.leaderboards-body", + always { player, back -> LeaderboardMenu(player, back) }), ), ) @@ -267,13 +270,40 @@ object Tutorial { Lesson( "town/details", Material.WRITABLE_BOOK, "tutorial.town.details", "tutorial.town.details-body", withTown { player, _, back -> TownSettingsMenu(player, back) }), + Lesson( + "town/status", Material.EXPERIENCE_BOTTLE, "tutorial.town.status", "tutorial.town.status-body", + withTown { player, town, back -> TownStatusMenu(player, town, back) }) { player -> + town(player)?.let { + listOf( + player.tr( + "town-status.level-number", + "level" to TownySettings.getTownLevelNumber(it), + "max" to TownySettings.getTownLevelMax() + ) + ) + }.orEmpty() + }, + Lesson( + "town/announce", Material.GOAT_HORN, "tutorial.town.announce", "tutorial.town.announce-body", + withTown { player, town, back -> TownMembersMenu(player, town, back) }), + Lesson( + "town/merge", Material.STRUCTURE_VOID, "tutorial.town.merge", "tutorial.town.merge-body", + always { player, back -> TownListMenu(player, back) }) { player -> + buildList { + addAll(player.money(TownySettings.getBaseCostForTownMerge().toDouble(), "tutorial.fact.merge-cost")) + TownySettings.getMaxDistanceForTownMerge().takeIf { it > 0 }?.let { + add(player.tr("tutorial.fact.merge-distance", "distance" to it)) + } + } + }, Lesson( "town/sell", Material.EMERALD, "tutorial.town.sell", "tutorial.town.sell-body", withTown { player, _, back -> TownSettingsMenu(player, back) }, economy ), Lesson( "town/ruins", Material.CRACKED_STONE_BRICKS, "tutorial.town.ruins", "tutorial.town.ruins-body", - withTown { player, _, back -> TownMenu(player, back) }) { player -> + townOrFind + ) { player -> buildList { add( player.tr( @@ -294,6 +324,18 @@ object Tutorial { "value" to TownyUtil.yesNo(player, TownySettings.getTownRuinsReclaimEnabled()) ) ) + if (TownySettings.getTownRuinsReclaimEnabled()) { + addAll(player.money(TownySettings.getEcoPriceReclaimTown(), "tutorial.fact.reclaim-cost")) + add( + player.tr( + "tutorial.fact.reclaim-townless", + "value" to TownyUtil.yesNo( + player, + TownySettings.canRuinsBeReclaimedByTownlessPlayers() + ) + ) + ) + } } } }, @@ -365,6 +407,14 @@ object Tutorial { ) { player -> player.money(TownySettings.getPurchasedBonusBlocksCost(), "tutorial.fact.bonus-cost") }, + Lesson( + "claims/cede", Material.OAK_BOAT, "tutorial.claims.cede", "tutorial.claims.cede-body", + withTown { player, _, back -> TownClaimsMenu(player, back) }), + Lesson( + "claims/takeover", Material.IRON_SWORD, "tutorial.claims.takeover", "tutorial.claims.takeover-body", + withTown { player, _, back -> TownClaimsMenu(player, back) }, + { TownySettings.isOverClaimingAllowingStolenLand() } + ), ), ) @@ -383,6 +433,9 @@ object Tutorial { Lesson( "plots/sell", Material.GREEN_BANNER, "tutorial.plots.sell", "tutorial.plots.sell-body", always { player, back -> PlotMenu(player, back) }), + Lesson( + "plots/area", Material.MAP, "tutorial.plots.area", "tutorial.plots.area-body", + always { player, back -> PlotAreaMenu(player, back) }), Lesson( "plots/types", Material.OAK_SIGN, "tutorial.plots.types", "tutorial.plots.types-body", always { player, back -> PlotMenu(player, back) }), @@ -392,6 +445,9 @@ object Tutorial { Lesson( "plots/trust", Material.TRIPWIRE_HOOK, "tutorial.plots.trust", "tutorial.plots.trust-body", always { player, back -> PlotMenu(player, back) }), + Lesson( + "plots/join-days", Material.CLOCK, "tutorial.plots.join-days", "tutorial.plots.join-days-body", + always { player, back -> PlotMenu(player, back) }), Lesson( "plots/groups", Material.CHEST, "tutorial.plots.groups", "tutorial.plots.groups-body", always { player, back -> PlotGroupMenu(player, back) }), @@ -432,6 +488,12 @@ object Tutorial { "tutorial.protection.trust", "tutorial.protection.trust-body", withTown { player, town, back -> TownTrustMenu(player, town, back) }), + Lesson( + "protection/overrides", + Material.WRITABLE_BOOK, + "tutorial.protection.overrides", + "tutorial.protection.overrides-body", + always { player, back -> PlotMenu(player, back) }), Lesson( "protection/flags", Material.FLINT_AND_STEEL, @@ -449,6 +511,20 @@ object Tutorial { "tutorial.protection.outlaws", "tutorial.protection.outlaws-body", withTown { player, town, back -> OutlawsMenu(player, town, back) }), + Lesson( + "protection/jailing", + Material.IRON_BARS, + "tutorial.protection.jailing", + "tutorial.protection.jailing-body", + withTown { player, town, back -> TownJailMenu(player, town, back) }) { player -> + buildList { + if (TownySettings.isAllowingBail()) { + addAll(player.money(TownySettings.getBailAmount(), "tutorial.fact.bail")) + addAll(player.money(TownySettings.getBailMaxAmount(), "tutorial.fact.max-bail")) + } + town(player)?.let { add(player.tr("town-jail.jails-count", "count" to it.jails.orEmpty().size)) } + } + }, Lesson( "protection/jail", Material.IRON_CHAIN, "tutorial.protection.jail", "tutorial.protection.jail-body", always { player, back -> ResidentMenu(player, back) }) { player -> @@ -492,6 +568,15 @@ object Tutorial { Lesson( "nations/relations", Material.SHIELD, "tutorial.nations.relations", "tutorial.nations.relations-body", withNation { player, nation, back -> NationRelationsMenu(player, nation, back) }), + Lesson( + "nations/level", Material.EXPERIENCE_BOTTLE, "tutorial.nations.level", "tutorial.nations.level-body", + withNation { player, nation, back -> NationStatusMenu(player, nation, back) }), + Lesson( + "nations/sanctions", + Material.RED_BANNER, + "tutorial.nations.sanctions", + "tutorial.nations.sanctions-body", + withNation { player, nation, back -> NationSanctionsMenu(player, nation, back) }), Lesson( "nations/settings", Material.LEVER, "tutorial.nations.settings", "tutorial.nations.settings-body", withNation { player, _, back -> NationMenu(player, back).toggles() }) { player -> @@ -527,6 +612,16 @@ object Tutorial { ), ) }, + Lesson( + "economy/prices", Material.CLOCK, "tutorial.economy.prices", "tutorial.economy.prices-body", + always { player, back -> PricesMenu(player, back) }) { player -> + listOf( + player.tr( + "town-status.new-day", + "time" to TownyUtil.duration(player, TownyUtil.secondsUntilNewDay()) + ) + ) + }, Lesson( "economy/bank", Material.GOLD_INGOT, "tutorial.economy.bank", "tutorial.economy.bank-body", withTown { player, town, back -> BankMenu(player, town, back) }) { player -> @@ -626,6 +721,16 @@ object Tutorial { back ) }), + Lesson( + "profile/standing", Material.GRASS_BLOCK, "tutorial.profile.standing", "tutorial.profile.standing-body", + always { player, back -> ResidentMenu(player, back) }) { player -> + resident(player)?.let { + listOf( + player.tr("profile.plots-owned", "count" to it.townBlocks.size), + player.tr("profile.towns-count", "count" to it.townsOutlawedIn.size), + ) + }.orEmpty() + }, Lesson( "profile/display", Material.GLOWSTONE_DUST, "tutorial.profile.display", "tutorial.profile.display-body", always { player, back -> ResidentMenu(player, back).toggles() }), diff --git a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/tutorial/TutorialChapterMenu.kt b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/tutorial/TutorialChapterMenu.kt index 6790f72..12129bb 100644 --- a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/tutorial/TutorialChapterMenu.kt +++ b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/tutorial/TutorialChapterMenu.kt @@ -27,7 +27,7 @@ class TutorialChapterMenu(player: Player, private val chapter: Chapter, back: Me glow(read == lessons.size) }) { if (chapter.link?.open(player, this) != true) render() } - val grid = layout(19, 20, 21, 22, 23, 24, 25, 28, 29, 30, 31, 32, 33, 34, 37, 38, 39, 40, 41, 42, 43) + val grid = layout(19, 20, 21, 22, 23, 24, 25, 28, 29, 30, 31, 32, 33, 34) lessons.forEach { lesson -> val isRead = Tutorial.isRead(player, lesson) val link = lesson.link?.takeIf { it.available(player) } diff --git a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/tutorial/TutorialMenu.kt b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/tutorial/TutorialMenu.kt index db4f806..1de5541 100644 --- a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/tutorial/TutorialMenu.kt +++ b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/tutorial/TutorialMenu.kt @@ -10,7 +10,7 @@ import org.bukkit.Material import org.bukkit.entity.Player /** The tutorial hub: every chapter with the viewer's reading progress. */ -class TutorialMenu(player: Player, back: Menu?) : Menu(player, player.tr("tutorial.title"), 5, back) { +class TutorialMenu(player: Player, back: Menu?) : Menu(player, player.tr("tutorial.title"), 6, back) { override fun build() { val chapters = Tutorial.visibleChapters @@ -33,7 +33,7 @@ class TutorialMenu(player: Player, back: Menu?) : Menu(player, player.tr("tutori val next = Tutorial.nextUnread(player) if (next != null) { button( - 38, Icons.icon( + 47, Icons.icon( Material.SPECTRAL_ARROW, tr("tutorial.continue"), tr("tutorial.continue-description"), tr("tutorial.chapter-line", "chapter" to tr(next.title)) ) @@ -41,9 +41,9 @@ class TutorialMenu(player: Player, back: Menu?) : Menu(player, player.tr("tutori TutorialChapterMenu(player, next, this).open() } } - backButton(40) + backButton(49) if (read > 0) { - button(42, Icons.icon(Material.WATER_BUCKET, tr("tutorial.reset"), tr("tutorial.reset-description"))) { + button(51, Icons.icon(Material.WATER_BUCKET, tr("tutorial.reset"), tr("tutorial.reset-description"))) { DialogUtil.confirm( player, MiniMessage.miniMessage().deserialize(tr("tutorial.reset-confirm")), null, onYes = { diff --git a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/listeners/MenuRefreshListener.kt b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/listeners/MenuRefreshListener.kt new file mode 100644 index 0000000..5fe36d8 --- /dev/null +++ b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/listeners/MenuRefreshListener.kt @@ -0,0 +1,57 @@ +package net.trilleo.mc.plugins.townymenu.listeners + +import com.palmergames.bukkit.towny.event.* +import com.palmergames.bukkit.towny.event.economy.BankTransactionEvent +import com.palmergames.bukkit.towny.event.town.TownUnclaimEvent +import net.trilleo.mc.plugins.townymenu.guis.framework.OpenMenus +import org.bukkit.event.EventHandler +import org.bukkit.event.EventPriority +import org.bukkit.event.Listener + +/** + * Re-renders open menus when Towny changes something they show, so a player + * watching a list sees residents join, land change hands, or the bank move + * without reopening the menu. Every handler defers to [OpenMenus.refreshSoon], + * which coalesces a burst of events into one refresh. + */ +class MenuRefreshListener : Listener { + + @EventHandler(priority = EventPriority.MONITOR) + fun onResidentJoinTown(event: TownAddResidentEvent) = OpenMenus.refreshSoon() + + @EventHandler(priority = EventPriority.MONITOR) + fun onResidentLeaveTown(event: TownRemoveResidentEvent) = OpenMenus.refreshSoon() + + @EventHandler(priority = EventPriority.MONITOR) + fun onRankAdded(event: TownAddResidentRankEvent) = OpenMenus.refreshSoon() + + @EventHandler(priority = EventPriority.MONITOR) + fun onRankRemoved(event: TownRemoveResidentRankEvent) = OpenMenus.refreshSoon() + + @EventHandler(priority = EventPriority.MONITOR) + fun onTownJoinNation(event: NationAddTownEvent) = OpenMenus.refreshSoon() + + @EventHandler(priority = EventPriority.MONITOR) + fun onTownLeaveNation(event: NationRemoveTownEvent) = OpenMenus.refreshSoon() + + @EventHandler(priority = EventPriority.MONITOR) + fun onClaim(event: TownClaimEvent) = OpenMenus.refreshSoon() + + @EventHandler(priority = EventPriority.MONITOR) + fun onUnclaim(event: TownUnclaimEvent) = OpenMenus.refreshSoon() + + @EventHandler(priority = EventPriority.MONITOR) + fun onPlotSettingsChanged(event: TownBlockSettingsChangedEvent) = OpenMenus.refreshSoon() + + @EventHandler(priority = EventPriority.MONITOR) + fun onBankTransaction(event: BankTransactionEvent) = OpenMenus.refreshSoon() + + @EventHandler(priority = EventPriority.MONITOR) + fun onNewDay(event: NewDayEvent) = OpenMenus.refreshSoon() + + @EventHandler(priority = EventPriority.MONITOR) + fun onTownDeleted(event: DeleteTownEvent) = OpenMenus.refreshSoon() + + @EventHandler(priority = EventPriority.MONITOR) + fun onNationDeleted(event: DeleteNationEvent) = OpenMenus.refreshSoon() +} diff --git a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/listeners/TownyAlertListener.kt b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/listeners/TownyAlertListener.kt new file mode 100644 index 0000000..16dace6 --- /dev/null +++ b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/listeners/TownyAlertListener.kt @@ -0,0 +1,65 @@ +package net.trilleo.mc.plugins.townymenu.listeners + +import com.palmergames.bukkit.towny.TownySettings +import com.palmergames.bukkit.towny.event.NationInviteTownEvent +import com.palmergames.bukkit.towny.event.NationRequestAllyNationEvent +import com.palmergames.bukkit.towny.event.TownInvitePlayerEvent +import com.palmergames.bukkit.towny.event.economy.TownEntersBankruptcyEvent +import com.palmergames.bukkit.towny.event.town.TownRuinedEvent +import com.palmergames.bukkit.towny.`object`.Resident +import net.trilleo.mc.plugins.townymenu.Main +import net.trilleo.mc.plugins.townymenu.utils.TownyUtil +import net.trilleo.mc.plugins.townymenu.utils.sendPrefixed +import net.trilleo.mc.plugins.townymenu.utils.tr +import org.bukkit.event.EventHandler +import org.bukkit.event.EventPriority +import org.bukkit.event.Listener + +/** + * Tells players about things they would otherwise only find by opening a menu: + * an invitation waiting for them, and a town falling into debt or ruin. Each + * message links to the menu that acts on it. + */ +class TownyAlertListener : Listener { + + @EventHandler(priority = EventPriority.MONITOR) + fun onTownInvite(event: TownInvitePlayerEvent) { + val resident = event.invite.receiver + // Towny already hides its own invite messages from residents using this mode. + if (resident.hasMode("ignoreinvites")) return + alert(resident, "alert.town-invite", "town" to TownyUtil.name(event.invite.sender.name)) + } + + @EventHandler(priority = EventPriority.MONITOR) + fun onNationInvite(event: NationInviteTownEvent) { + alert(event.invite.receiver.mayor, "alert.nation-invite", "nation" to TownyUtil.name(event.invite.sender.name)) + } + + @EventHandler(priority = EventPriority.MONITOR) + fun onAllyRequest(event: NationRequestAllyNationEvent) { + alert(event.invite.receiver.king, "alert.ally-request", "nation" to TownyUtil.name(event.invite.sender.name)) + } + + @EventHandler(priority = EventPriority.MONITOR) + fun onBankruptcy(event: TownEntersBankruptcyEvent) { + alert(event.town.mayor, "alert.bankrupt", "town" to TownyUtil.name(event.town.name)) + } + + @EventHandler(priority = EventPriority.MONITOR) + fun onRuined(event: TownRuinedEvent) { + val mayor = event.town.mayor ?: return + val key = if (TownySettings.getTownRuinsReclaimEnabled()) "alert.ruined-reclaim" else "alert.ruined" + alert( + mayor, key, + "town" to TownyUtil.name(event.town.name), + "hours" to TownySettings.getTownRuinsMaxDurationHours(), + "cost" to TownyUtil.money(TownySettings.getEcoPriceReclaimTown()), + ) + } + + private fun alert(resident: Resident?, key: String, vararg args: Pair) { + if (!Main.instance.pluginConfig.townyAlerts) return + val player = resident?.player ?: return + player.sendPrefixed(player.tr(key, *args)) + } +} diff --git a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/utils/DialogUtil.kt b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/utils/DialogUtil.kt index 669a0f7..5f332bc 100644 --- a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/utils/DialogUtil.kt +++ b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/utils/DialogUtil.kt @@ -33,7 +33,8 @@ object DialogUtil { .build() /** - * Asks [player] for a line (or, with [multiline], a paragraph) of text. + * Asks [player] for a line (or, with [multiline], a paragraph) of text. The + * dialog cannot be dismissed with Escape, so exactly one callback always runs. * * @param title MiniMessage dialog title * @param label MiniMessage label above the text field @@ -61,7 +62,7 @@ object DialogUtil { show( player, DialogBase.builder(miniMessage.deserialize(title)) - .canCloseWithEscape(true) + .canCloseWithEscape(false) .afterAction(DialogBase.DialogAfterAction.CLOSE) .body(listOfNotNull(body?.let { DialogBody.plainMessage(miniMessage.deserialize(it)) })) .inputs(listOf(field)) diff --git a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/utils/TownyUtil.kt b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/utils/TownyUtil.kt index 0279b15..0113b95 100644 --- a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/utils/TownyUtil.kt +++ b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/utils/TownyUtil.kt @@ -1,10 +1,14 @@ package net.trilleo.mc.plugins.townymenu.utils import com.palmergames.bukkit.towny.TownyEconomyHandler +import com.palmergames.bukkit.towny.TownySettings import com.palmergames.bukkit.towny.TownyUniverse import com.palmergames.bukkit.towny.`object`.Government +import com.palmergames.bukkit.towny.`object`.Nation import com.palmergames.bukkit.towny.`object`.Resident +import com.palmergames.bukkit.towny.`object`.Town import com.palmergames.bukkit.towny.permissions.PermissionNodes +import com.palmergames.util.TimeMgmt import net.kyori.adventure.text.minimessage.MiniMessage import org.bukkit.entity.Player import java.time.Instant @@ -34,6 +38,19 @@ object TownyUtil { fun plotType(player: Player, type: String): String = Lang.find(player, "plot-type.${type.lowercase()}") ?: name(type) + /** [government]'s board, or `null` when unset or still Towny's `/town set board [msg]` placeholder from `config.yml`. */ + fun board(government: Government): String? = government.board.takeUnless { + it.isNullOrBlank() || it == when (government) { + is Town -> TownySettings.getTownDefaultBoard() + is Nation -> TownySettings.getNationDefaultBoard() + else -> null + } + } + + /** [resident]'s about text, or `null` when unset or still Towny's `/res set about [msg]` placeholder. */ + fun about(resident: Resident): String? = + resident.about.takeUnless { it.isNullOrBlank() || it == TownySettings.getDefaultResidentAbout() } + /** Formats [amount] with the server economy's currency, or `-` when no economy is active. */ fun money(amount: Double): String = if (TownyEconomyHandler.isActive()) text(TownyEconomyHandler.getFormattedBalance(amount)) else "-" @@ -45,6 +62,13 @@ object TownyUtil { fun date(epochMillis: Long): String = if (epochMillis <= 0) "-" else dateFormat.format(Instant.ofEpochMilli(epochMillis)) + /** Seconds until Towny's next new day, when taxes and upkeep are collected. */ + fun secondsUntilNewDay(): Long = TimeMgmt.townyTime(true) + + /** A duration of [seconds] as hours and minutes in [player]'s language. */ + fun duration(player: Player, seconds: Long): String = + player.tr("common.duration", "hours" to seconds / 3600, "minutes" to seconds % 3600 / 60) + /** A coloured On/Off label in [player]'s language. */ fun onOff(player: Player, value: Boolean): String = player.tr(if (value) "common.on" else "common.off") diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index f2c5f95..9db3026 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -15,3 +15,11 @@ sneak-swap-hand-shortcut: true # When a player without a town joins, suggest the tutorial (/townymenu tutorial) in chat. # The hint stops once they have read every lesson or joined a town. tutorial-join-hint: true + +# Re-render an open menu when Towny changes what it shows, such as a resident joining, land being claimed, +# a bank transaction, or the new day. Turn this off if you would rather menus only update when they are reopened. +live-menu-refresh: true + +# Announce in chat when a player is invited to a town, a town is invited to a nation, a nation is asked for an +# alliance, or a town goes bankrupt or falls into ruin. Each message opens the menu that answers it. +towny-alerts: true diff --git a/src/main/resources/lang/en_US.yml b/src/main/resources/lang/en_US.yml index 651a00d..327f681 100644 --- a/src/main/resources/lang/en_US.yml +++ b/src/main/resources/lang/en_US.yml @@ -23,6 +23,9 @@ command: tutorial: description: "Learn how towns, plots, and nations work" players-only: "Only players can open the tutorial." + invites: + description: "Answer town, nation, and alliance invitations" + players-only: "Only players can open the invites menu." category: general: "General" info: "Info" @@ -66,6 +69,7 @@ common: trust-online: "Trust Online Player" trust-name: "Trust by Name" trust-name-description: "Trust any player Towny knows." + duration: "{hours}h {minutes}m" menu: close: "Close" @@ -137,6 +141,18 @@ main: admin-description: "Configure Towny and manage every town, nation, and resident." tutorial: "Tutorial" tutorial-description: "Learn how towns, plots, nations, and these menus work." + prices: "Prices & New Day" + prices-description: "What everything costs on this server, and when the next Towny day starts." + leaderboards: "Leaderboards" + leaderboards-description: "The biggest and richest towns, nations, and players." + +alert: + town-invite: "{town} has invited you to join. /townymenu invites'>Open your invites" + nation-invite: "{nation} has invited your town to join. /townymenu invites'>Open your invites" + ally-request: "{nation} has asked your nation for an alliance. /townymenu invites'>Open your invites" + bankrupt: "{town} has gone bankrupt and is now in debt. Deposit money to pay it off. /townymenu'>Open TownyMenu" + ruined: "{town} has fallen into ruins." + ruined-reclaim: "{town} has fallen into ruins. You can reclaim it for {cost} within {hours} hours. /townymenu'>Open TownyMenu" map: title: "Map" @@ -165,6 +181,7 @@ map: outpost: "Outpost" for-sale: "For sale: {price}" click-town: "Click to view town" + right-buy: "Right-click to buy this plot" invites: title: "Invites" @@ -215,6 +232,8 @@ perm: switch-description: "Use doors, buttons, levers, and containers." item-use: "Item Use" item-use-description: "Use items such as flint and steel or buckets." + overrides: "Player Overrides" + overrides-description: "Give single players their own build, break, and interact permissions on this land." picker: select: "Click to select" @@ -243,7 +262,7 @@ town: claims-description: "Claim and unclaim land, create outposts, and buy bonus claims." settings: "Town Settings" settings-title: "Town Settings" - settings-description: "PvP, mobs, fire, explosions, open, public, and peaceful." + settings-description: "PvP, mobs, fire, explosions, open, public, peaceful, and nation zone." details: "Town Details" details-description: "Name, board, tag, taxes, prices, spawn, and sale." permissions-title: "Town Permissions" @@ -262,6 +281,13 @@ town: delete-description: "Disband your town permanently. You will be asked to confirm." leave: "Leave Town" leave-description: "Leave your town. You will be asked to confirm." + jail: "Jail" + jail-description: "Jail residents, release prisoners, and see your jails." + jail-count: "Prisoners: {count}" + reclaim: "Reclaim Town" + reclaim-description: "Pay to restore your ruined town and become its mayor." + reclaim-wait: "Available after {hours} hours in ruins" + click-status: "Click for level, limits, and upkeep" no-town: title: "Find a Town" @@ -272,6 +298,8 @@ no-town: browse: "Browse Towns" browse-description: "Find an open town to join." invites-description: "Accept an invitation to a town." + reclaim: "Reclaim a Ruined Town" + reclaim-description: "Stand in a ruined town and pay to become its mayor." outlaws: title: "Outlaws" @@ -289,6 +317,43 @@ outposts: world: "World: {world}" location: "Location: {x}, {y}, {z}" +town-jail: + title: "Town Jail" + hours-left: "Hours left: {hours}" + bail: "Bail: {bail}" + held-in: "Held in: {jail}, cell {cell}" + click-unjail: "Click to release" + jail-player: "Jail a Player" + jail-player-title: "Jail Player" + jail-player-description: "Send an online resident to one of your jails." + no-jails: "No Jails" + no-jails-description: "Set a plot's type to Jail to build your town's first jail." + jails: "Jails" + jails-title: "Town Jails" + jails-description: "Every jail plot in your town." + jails-count: "Jails: {count}" + primary: "Primary jail" + cells: "Cells: {count}" + prisoners: "Prisoners: {count}" + jail-number: "Jail #{number}" + +jail-sentence: + title: "Jail {player}" + hours: "Sentence" + hours-description: "How many hours the player stays in jail." + hours-title: "Jail Sentence" + hours-label: "Hours" + max-hours: "Longest sentence: {hours} hours" + bail: "Bail" + bail-description: "What the player can pay to leave early." + bail-title: "Bail" + max-bail: "Highest bail: {bail}" + jail: "Jail" + cell: "Cell" + cell-number: "Cell {number}" + confirm: "Jail Player" + confirm-description: "Send the player to jail with this sentence." + claims: title: "Town Claims" info: "Claims" @@ -323,6 +388,11 @@ claims: bonus-amount: "Number of claims" auto-claim: "Auto Claim" auto-claim-description: "Claim every chunk you walk into." + cede: "Cede This Plot" + cede-title: "Cede Plot to Town" + cede-description: "Give the chunk you stand in to another town. Its mayor must be online to accept." + takeover: "Take Over Claim" + takeover-description: "Take the chunk you stand in from an overclaimed town that borders yours." town-info: gone: "This town no longer exists" @@ -341,9 +411,12 @@ town-info: buy-description: "Become this town's mayor." invite: "Invite to Nation" invite-description: "Invite this town to join {nation}." + merge: "Merge into Your Town" + merge-description: "Ask the mayor of {town} to merge their town into {own}. They must be online to accept." town-list: title: "Towns" + distance: "Distance: {blocks} blocks" town-members: title: "Residents: {town}" @@ -352,6 +425,9 @@ town-members: invite-name: "Invite by Name" invite-name-description: "Invite any player Towny knows, even if they are offline." invite-title: "Invite to Town" + announce: "Announce to Town" + announce-title: "Town Announcement" + announce-description: "Send a message to every online resident." town-details: title: "Town Details" @@ -385,6 +461,46 @@ town-details: for-sale-title: "Sell Town" for-sale-description: "Let another player buy your town." +town-status: + title: "Status: {town}" + level: "Town Level" + level-description: "Your level sets how much land, how many outposts, and how much upkeep your town has." + level-number: "Level: {level}/{max}" + level-name: "Title: {name}" + level-manual: "An admin set this level by hand." + level-highest: "This is the highest level." + next-level-residents: "Next level at {needed} residents (now {current})" + next-level-claims: "Next level at {needed} claims (now {current})" + claims-description: "The land your town holds and how much more it may claim." + max-purchased: "Bonus claims you may buy: {count}" + outposts: "Outposts" + outposts-description: "Outposts let your town claim land away from its borders." + upkeep: "Daily Upkeep" + upkeep-description: "What your town bank pays on the next new day." + upkeep-exempt: "This town pays no upkeep." + upkeep-daily: "Upkeep: {cost}" + upkeep-penalty: "Overclaim penalty: {cost}" + upkeep-peaceful: "Peaceful: {cost}" + upkeep-total: "Total: {cost}" + new-day: "Next new day in: {time}" + nation-zone: "Nation Zone" + nation-zone-description: "Protected wilderness around your town, given by your nation." + nation-zone-size: "Size: {size} chunks" + all-good: "All Good" + all-good-description: "Your town has no warnings." + ruined: "Ruined" + ruined-hours: "Removed for good in: {hours} hours" + bankrupt: "Bankrupt" + bankrupt-description: "Pay off the debt to claim land and recruit again." + debt-cap: "Debt limit: {cap}" + conquered: "Conquered" + conquered-days: "Days left as conquered: {days}" + overclaimed: "Overclaimed" + overclaimed-description: "Unclaim land or grow the town to stop the upkeep penalty." + overclaimed-stealable: "Other towns may take your land while you are overclaimed." + upkeep-short: "Upkeep Unpaid" + upkeep-short-description: "Deposit money before the new day, or the town falls into debt or ruin." + town-trust: resident: "Trusted resident" town: "Trusted town" @@ -402,6 +518,7 @@ sort: online: "Online players" open: "Open to join" name: "Name" + nearby: "Nearby" nation: title: "Your Nation" @@ -427,6 +544,10 @@ nation: leave-description: "Take your town out of the nation. You will be asked to confirm." propose-alliance: "Propose Alliance" declare-enemy: "Declare Enemy" + announce: "Announce to Nation" + announce-title: "Nation Announcement" + announce-description: "Send a message to every online resident of the nation." + click-status: "Click for level, bonuses, and upkeep" no-nation: title: "Find a Nation" @@ -473,6 +594,74 @@ nation-towns: invite: "Invite a Town" invite-title: "Invite Town" invite-description: "Invite a town that isn't in a nation." + sanctions: "Sanctioned Towns" + sanctions-description: "Towns kept away from your nation spawn." + sanctions-count: "Sanctioned: {count}" + +nation-sanctions: + title: "Sanctions: {nation}" + right-lift: "Right-click to lift the sanction" + add: "Sanction a Town" + add-title: "Sanction Town" + add-description: "Keep a town outside your nation away from your nation spawn." + +leaderboard: + title: "Leaderboards" + info: "Leaderboards" + info-description: "The same rankings as Towny's top lists. Click any entry to view it." + rank: "#{rank}" + town-residents: "Towns by Residents" + town-residents-description: "The towns with the most residents." + nation-residents: "Nations by Residents" + nation-residents-description: "The nations with the most residents." + town-land: "Towns by Land" + town-land-description: "The towns holding the most claims." + resident-land: "Players by Plots" + resident-land-description: "The players who own the most plots." + town-balance: "Towns by Bank" + town-balance-description: "The richest town banks." + nation-balance: "Nations by Bank" + nation-balance-description: "The richest nation banks." + town-baltop: "Richest in Your Town" + town-baltop-description: "The residents of {town} with the most money." + nation-baltop: "Richest in Your Nation" + nation-baltop-description: "The residents of {nation} with the most money." + +prices: + title: "Prices & New Day" + new-day: "The Towny Day" + new-day-description: "Taxes and upkeep are collected once a day." + no-economy: "This server has no economy, so nothing costs money." + founding: "Founding" + new-town: "New town: {cost}" + new-nation: "New nation: {cost}" + reclaim: "Reclaiming ruins: {cost}" + merge: "Merging towns: {cost}" + claims: "Land" + claim: "Per claim: {cost}" + claim-refund: "Refund per unclaim: {cost}" + bonus-claim: "Per bonus claim: {cost}" + outpost: "Per outpost: {cost}" + outpost-upkeep: "Daily upkeep per outpost: {cost}" + upkeep: "Daily Upkeep" + upkeep-description: "Paid from the town and nation banks each new day." + town-upkeep: "Town upkeep: {cost}" + nation-upkeep: "Nation upkeep: {cost}" + overclaimed-upkeep: "Overclaim penalty: {cost}" + town-peaceful: "Peaceful town: {cost}" + nation-peaceful: "Peaceful nation: {cost}" + plot-prices: "Plot Prices: {town}" + plot: "Plot: {cost}" + shop: "Shop: {cost}" + embassy: "Embassy: {cost}" + taxes: "Taxes: {town}" + resident-tax: "Resident tax: {cost}" + plot-tax: "Plot tax: {cost}" + shop-tax: "Shop tax: {cost}" + embassy-tax: "Embassy tax: {cost}" + plot-types: "Plot Types" + plot-types-description: "What a mayor pays to set a plot to each type." + plot-type: "{type}: {cost}" nation-details: title: "Nation Details" @@ -496,6 +685,19 @@ nation-details: taxes: "Daily Town Tax" conquered-tax: "Conquered Town Tax" +nation-status: + title: "Status: {nation}" + level: "Nation Level" + level-description: "Your level sets the bonuses member towns receive and the nation's upkeep." + next-level-towns: "Next level at {needed} towns (now {current})" + bonuses: "Town Bonuses" + bonuses-description: "What every member town gains from this nation." + bonus-claims: "Bonus claims per town: {count}" + bonus-outposts: "Bonus outposts per town: {count}" + bonus-capital-outposts: "Bonus outposts for the capital: {count}" + upkeep-description: "What your nation bank pays on the next new day." + upkeep-short: "The nation bank cannot cover this." + plot: title: "Plot" unnamed: "Plot" @@ -525,7 +727,7 @@ plot: type-description: "Shops, embassies, arenas, farms, jails, and more." rename: "Rename Plot" rename-title: "Rename Plot" - rename-description: "Shown to players as they walk in." + rename-description: "Shown to players as they walk in. Right-click to remove the name." plot-name: "Plot name" settings: "Plot Settings" settings-title: "Plot Settings" @@ -540,6 +742,29 @@ plot: clear: "Clear Plot" clear-description: "Remove blocks placed on this plot that the server marks as clearable." town-description: "View the town that owns this land." + group-applies: "Applies to all {count} plots in {group}" + evict-resell: "Right-click to evict and put the plot back up for sale" + join-days: "Join-Day Limits" + join-days-description: "Left-click to set the fewest days a buyer must have lived in town. Right-click to set the most days allowed." + join-days-none: "No limits" + join-days-hint: "Use 0 to remove the limit." + min-join-days: "Fewest days in town: {days}" + max-join-days: "Most days in town: {days}" + min-join-days-title: "Fewest Days in Town" + max-join-days-title: "Most Days in Town" + days: "Days" + jail-cells: "Jail Cells" + jail-cells-description: "Left-click to add a cell where you stand. Right-click to remove the cell where you stand." + jail-cells-count: "Cells: {count}" + hud: "Plot HUD" + hud-description: "Show or hide a sidebar with the plot you are in: owner, type, price, and permissions." + area: "Many Plots at Once" + area-description: "Buy, sell, or give up every plot in an area around you." + click-info: "Click to show Towny's plot info in chat" + make-outpost: "Make Outpost" + make-outpost-description: "Turn this claim into an outpost with a spawn where you stand." + outpost-spawn: "Move Outpost Spawn" + outpost-spawn-description: "Move this outpost's spawn to where you stand." # Towny's built-in plot types. Custom types from Towny's config show their own name unless added here. plot-type: @@ -584,6 +809,41 @@ plot-trust: title: "Plot Trust" trust-title: "Trust on Plot" trust-online-description: "Let an online player build on this plot." + group: "Group: {group}" + group-description: "Trust on this plot applies to all {count} plots in its group." + +plot-area: + title: "Many Plots at Once" + info: "Area Around You" + info-description: "Choose a shape and radius, then act on every plot in that area. A radius of 1 covers the 3×3 chunks around you." + shape: "Shape" + square: "Square" + circle: "Circle" + radius: "Radius" + radius-title: "Area Radius" + radius-description: "How many chunks the area reaches from you." + buy: "Buy Plots" + buy-description: "Buy every plot for sale in the area that you are allowed to buy." + sell: "Sell Plots" + sell-title: "Sell Plots in Area" + sell-description: "Put every plot you may sell in the area up for sale at one price." + not-for-sale: "Stop Selling Plots" + not-for-sale-description: "Take every plot you may manage in the area off sale." + give-up: "Give Up Plots" + give-up-description: "Return the plots you own in the area to the town." + give-up-all: "Give Up All My Plots" + give-up-all-description: "Return every plot you own, anywhere, to its town." + +plot-overrides: + title: "Player Overrides" + changed-by: "Last changed by: {player}" + click-remove: "Click to remove override" + add-online: "Add Online Player" + add-name: "Add by Name" + add-description: "Give a player their own permissions here, then edit them in Towny's editor." + add-title: "Add Player Override" + edit: "Edit Overrides" + edit-description: "Open Towny's editor to choose what each player may build, break, and use." profile: title: "Your Profile" @@ -610,6 +870,28 @@ profile: spawn-description: "Teleport to your bed or your town spawn." bail: "Pay Bail" bail-description: "Pay {cost} to leave jail." + modes: "Display Modes" + modes-description: "Left-click to turn every mode off, such as auto map, borders, and auto claim. Right-click to restore the server's default modes." + modes-active: "Active: {modes}" + plots: "My Plots" + plots-description: "Every plot you own, where it is, and what it costs you daily." + tax: "Daily Tax" + tax-description: "What you pay your town and for your plots on the next new day." + tax-exempt: "You are exempt from taxes." + tax-town: "Town tax: {tax}" + tax-plots: "Plot tax: {tax}" + tax-total: "Total: {tax}" + outlawed-in: "Outlawed In" + outlawed-in-description: "Towns that have declared you an outlaw." + outlawed-in-title: "Outlawed In" + trusted-in: "Trusted In" + trusted-in-description: "Towns that trust you to build anywhere in them." + trusted-in-title: "Trusted In" + towns-count: "Towns: {count}" + +resident-plots: + title: "My Plots" + location: "Where: {world} at {x}, {z}" friends: title: "Friends" @@ -707,6 +989,12 @@ toggle: plot-group-description: "Add each plot you walk into to your selected plot group." district: "District Mode" district-description: "Add each plot you walk into to your selected district." + nation-zone: "Nation Zone" + nation-zone-description: "Protect the land around your town as part of your nation's zone." + spy: "Chat Spy" + spy-description: "See messages from every town and nation chat channel." + admin-bypass: "Disable Admin Powers" + admin-bypass-description: "While on, Towny treats you like a regular player, so you can test protections." admin: title: "Server Admin" @@ -850,6 +1138,20 @@ admin-server: regenerations-description: "Turn repair on unclaim and wilderness explosion repair on or off in every world." left-on: "Left-click to turn on in every world" right-off: "Right-click to turn off in every world" + purge: "Purge Inactive Residents" + purge-description: "Delete residents who have not logged in for a number of days. Towny asks you to confirm." + purge-label: "Days offline" + purge-hint: "Residents offline longer than this are deleted." + check-perm: "Check a Permission" + check-perm-description: "See whether an online player has a permission node." + deposit-all: "Deposit into Every Bank" + deposit-all-description: "Put the same amount into every town and nation bank." + deposit-all-hint: "Paid into every town and nation bank on the server." + +admin-tools: + add: "Add by Name" + add-description: "Add a player or town to this list." + click-remove: "Click to remove" admin-plugin: title: "TownyMenu Settings" @@ -864,6 +1166,10 @@ admin-plugin: shortcut-description: "Open the main menu by pressing swap-hand (F) while sneaking." join-hint: "Tutorial Hint on Join" join-hint-description: "Suggest the tutorial to players without a town when they join, until they have read every lesson." + live-refresh: "Live Menu Refresh" + live-refresh-description: "Update open menus when Towny changes what they show, such as a resident joining or the new day." + alerts: "Chat Alerts" + alerts-description: "Announce invitations, bankruptcy, and ruin in chat, with a link to the menu that answers them." admin-town: title: "Admin: {town}" @@ -906,6 +1212,36 @@ admin-town: top-lists: "Visible on Top Lists" top-lists-description: "Show this town in /towny top rankings." +admin-town-tools: + title: "Tools: {town}" + open: "More Town Tools" + open-description: "Level, merging, sale, outlaws, trust, and ranks." + level: "Town Level" + level-description: "Fix this town at a level, instead of letting its size decide." + level-title: "Set Town Level" + level-label: "Level" + level-hint: "Enter unset to let the town size decide again." + merge: "Merge a Town into This One" + merge-description: "The town you pick is merged into {town} and disappears." + merge-force: "Right-click to merge even when Towny would refuse." + merge-title: "Merge Which Town?" + for-sale: "Put up for Sale" + for-sale-description: "Let a player buy this town and become its mayor." + for-sale-title: "Sell Town" + not-for-sale: "Take off Sale" + not-for-sale-description: "Stop selling this town." + unconquer: "Remove Conquered Status" + unconquer-description: "Free this town from the nation that conquered it." + check-outposts-description: "Remove this town's outpost spawns that are no longer on its land." + ranks: "Resident Ranks" + ranks-description: "Give or take town ranks, ignoring who may normally assign them." + ranks-title: "Whose Ranks?" + outlaws-description: "Players this town has declared outlaws." + trust-description: "Players trusted to build anywhere in this town." + trusted-towns: "Trusted Towns" + trusted-towns-description: "Towns whose residents may build anywhere in this town." + trust-town-title: "Trust Town" + admin-nation: title: "Admin: {nation}" toggles: "Nation Settings" @@ -920,6 +1256,68 @@ admin-nation: delete: "Delete Nation" delete-description: "Delete this nation. Its towns remain, without a nation." +admin-nation-tools: + title: "Tools: {nation}" + open: "More Nation Tools" + open-description: "Level, merging, transfers, sanctions, and ranks." + level: "Nation Level" + level-description: "Fix this nation at a level, instead of letting its size decide." + level-title: "Set Nation Level" + merge: "Merge a Nation into This One" + merge-description: "The nation you pick is merged into {nation} and disappears." + merge-title: "Merge Which Nation?" + transfer: "Move a Town In" + transfer-description: "Take a town out of its nation and place it in {nation}." + transfer-title: "Move Which Town?" + recheck: "Recheck Town Distances" + recheck-description: "Remove member towns that are too far from the capital." + sanctions-description: "Towns kept away from this nation's spawn." + ranks: "Resident Ranks" + ranks-description: "Give or take nation ranks, ignoring who may normally assign them." + +admin-plot: + title: "Admin: Plot" + open: "Plot You Stand In" + open-description: "Claim it for a player, move it to another town, trust players, or unclaim it." + wilderness-description: "Nobody has claimed this chunk." + claim: "Claim for a Player" + claim-description: "Make an online player the owner of this plot." + claim-title: "Claim for Whom?" + trust-description: "Players trusted to build on this plot." + move: "Move to Another Town" + move-description: "Give this plot to a different town." + move-title: "Give to Which Town?" + unclaim: "Unclaim" + unclaim-description: "Left-click to unclaim this chunk. Right-click to unclaim a square around you." + unclaim-title: "Unclaim Area" + +admin-perms: + title: "Towny Permissions" + open: "Towny Permissions" + open-description: "Edit townyperms.yml: the permissions every resident has, and the ranks mayors can hand out." + info: "townyperms.yml" + info-description: "Changes are saved and applied at once, like /townyadmin townyperms." + groups: "Permission Groups" + groups-description: "What players get without a rank: nomads, residents, mayors, and nation leaders." + groups-count: "Groups: {count}" + group-title: "Group: {group}" + town-ranks: "Town Ranks" + town-ranks-description: "The ranks a mayor can give residents, and what each one allows." + nation-ranks: "Nation Ranks" + nation-ranks-description: "The ranks a nation leader can give residents, and what each one allows." + ranks-count: "Ranks: {count}" + node-count: "Permissions: {count}" + node-label: "Permission node" + click-remove: "Click to remove this permission" + right-remove: "Right-click to delete this rank" + add-node: "Add a Permission" + add-node-description: "Give this group another permission node." + add-rank: "Add a Rank" + add-rank-description: "Create a rank that can then be given permissions." + rank-label: "Rank name" + rename-rank: "Rename a Rank" + rename-rank-description: "Rename a rank and keep it on the residents who hold it." + admin-resident: title: "Admin: {name}" jailed: "Jailed" @@ -1007,10 +1405,16 @@ tutorial: nation-upkeep: "Your nation's upkeep per day: {cost}" bankruptcy: "Town bankruptcy: {value}" debt: "Your town is bankrupt and owes {debt}." + merge-cost: "Base cost of merging towns: {cost}" + merge-distance: "Furthest merge distance: {distance} chunks" + reclaim-cost: "Reclaiming ruins costs: {cost}" + reclaim-townless: "Players without a town can reclaim ruins: {value}" + bail: "Default bail: {cost}" + max-bail: "Highest bail: {cost}" start: title: "Getting Started" - description: "What Towny is, how to open and read these menus, the wilderness, the map, and invites." + description: "What Towny is, how to open and read these menus, the wilderness, the map, invites, and leaderboards." towny: "What Is Towny?" towny-body: "Towny lets players protect land by founding towns. A town claims chunks that outsiders can't grief, collects taxes, and can join other towns in a nation. Land nobody has claimed is the wilderness." menus: "Opening the Menus" @@ -1020,9 +1424,11 @@ tutorial: wilderness: "The Wilderness" wilderness-body: "Unclaimed land belongs to no one. Depending on the world, you may be able to build there, but nothing you build is protected. Settle in a town to keep your work safe." map: "The Map" - map-body: "The map shows the chunks around you, coloured by who owns them: your town, your plots, your nation, allies, enemies, and other towns. Glowing tiles are for sale. Click a claimed tile to view its town." + map-body: "The map shows the chunks around you, coloured by who owns them: your town, your plots, your nation, allies, enemies, and other towns. Glowing tiles are for sale. Click a claimed tile to view its town, or right-click a tile for sale to buy it." invites: "Invites" - invites-body: "Towns invite players, nations invite towns, and nations send each other alliance requests. Every pending invitation waits in the Invites menu, where you can accept or decline it." + invites-body: "Towns invite players, nations invite towns, and nations send each other alliance requests. Every pending invitation waits in the Invites menu, where you can accept or decline it. TownyMenu also tells you in chat when one arrives, and that message opens the menu." + leaderboards: "Leaderboards" + leaderboards-body: "The Leaderboards menu ranks towns and nations by residents, land, and bank balance, and players by the plots they own. Click any entry to open it." join: title: "Finding a Town" @@ -1030,7 +1436,7 @@ tutorial: why: "Why Join a Town?" why-body: "Town residents share protected land, can own plots, can teleport to the town spawn, and take part in their town's nation. Builds in the wilderness have none of that protection." browse: "Joining a Town" - browse-body: "Browse the town directory and sort it by residents, claims, or openness. Open towns let anyone join with one click. Other towns need their mayor or an assistant to invite you." + browse-body: "Browse the town directory and sort it by residents, claims, openness, or distance from you. Open towns let anyone join with one click. Other towns need their mayor or an assistant to invite you." invites: "Accepting an Invite" invites-body: "When a town invites you, accept it in the Invites menu. You can only live in one town at a time. Turn on Ignore Invites in your personal settings if you don't want any." found: "Founding a Town" @@ -1040,7 +1446,7 @@ tutorial: town: title: "Running a Town" - description: "Ranks, recruiting, spawn, settings, details, selling, and ruins." + description: "Levels, ranks, recruiting, spawn, settings, details, announcements, merging, selling, and ruins." ranks: "Mayor and Ranks" ranks-body: "The mayor can do everything. Ranks such as assistant, sheriff, or builder share part of that power, and the server decides exactly what each rank may do. Assign ranks from a resident's profile in the Residents menu." grow: "Growing Your Town" @@ -1048,17 +1454,23 @@ tutorial: spawn: "Spawn and Home Block" spawn-body: "The home block is the heart of your town and holds the town spawn. Residents can teleport there from the town menu, and visitors can too while the town is Public. Mayors move the spawn and home block in Town Details." settings: "Town Settings" - settings-body: "PvP, hostile mobs, fire spread, and explosions are switched on or off for the whole town. Open lets anyone join without an invite, Public lets visitors use your spawn, and Peaceful keeps the town out of PvP and war." + settings-body: "PvP, hostile mobs, fire spread, and explosions are switched on or off for the whole town. Open lets anyone join without an invite, Public lets visitors use your spawn, and Peaceful keeps the town out of PvP and war. Towns in a nation can also switch off the nation zone that protects the land around them." details: "Name, Board, and Tag" details-body: "In Town Details, mayors rename the town, write the board message residents see when they log in, set a short chat tag, and choose the town's map colour." sell: "Selling a Town" sell-body: "A mayor can put the town up for sale in Town Details. Whoever buys it pays the price and becomes the new mayor. Towns for sale are marked in the town directory." ruins: "Ruins and Deleting" - ruins-body: "Deleting a town gives all its land back to the wilderness. When ruins are on, a deleted or bankrupt town first becomes ruins that open up to everyone over time and may be reclaimed before they disappear." + ruins-body: "Deleting a town gives all its land back to the wilderness. When ruins are on, a deleted or bankrupt town first becomes ruins that open up to everyone over time. Its residents can reclaim it from the town menu before it disappears, and on some servers players without a town can too, from Find a Town while standing in the ruins." + announce: "Announcements" + announce-body: "Send a message to every online resident with Announce to Town in the Residents menu. Nation leaders can do the same for the whole nation from the nation menu." + merge: "Merging Towns" + merge-body: "A mayor can ask another town to merge into their own from that town's page in the town directory. The other mayor must be online and accept, and then their residents, land, and bank join your town." + status: "Town Level and Warnings" + status-body: "Click your town at the top of the town menu to see its level, how many residents or claims the next level needs, its land and outpost limits, and the upkeep due on the next new day. Anything that needs attention, such as debt, overclaiming, or being conquered, is listed there and on the town icon." claims: title: "Claiming Land" - description: "Chunks and claim limits, claiming and unclaiming, outposts, and bonus claims." + description: "Chunks and claim limits, claiming and unclaiming, outposts, bonus claims, and ceding or taking over land." chunks: "Claims Are Chunks" chunks-body: "Towns claim land one chunk at a time, called a town block. Every claimed chunk is protected by your town's permissions. How many chunks you can claim depends on your residents and any bonus claims." claim: "Claiming Land" @@ -1068,19 +1480,23 @@ tutorial: unclaim: "Unclaiming Land" unclaim-body: "Unclaim chunks you no longer need to free their claim slots. The home block can't be unclaimed. Some servers slowly restore unclaimed land to how it looked before." outposts: "Outposts" - outposts-body: "An outpost is a claim away from your town, such as a distant mine or farm. Each outpost has a spawn that residents can teleport to, and more land can be claimed around it." + outposts-body: "An outpost is a claim away from your town, such as a distant mine or farm. Each outpost has a spawn that residents can teleport to, and more land can be claimed around it. A mayor can also turn an existing claim into an outpost, or move an outpost's spawn, from the Plot menu." bonus: "Bonus Claims" bonus-body: "Reached your claim limit? Buy bonus claims with money from the town bank. The price can rise with each one you buy." + cede: "Ceding Land" + cede-body: "Stand in one of your town's chunks and choose Cede This Plot to give it to another town, for example to settle a border. The other town's mayor must be online to accept. The home block can't be ceded." + takeover: "Taking Over Claims" + takeover-body: "A town with more land than its claim limit is overclaimed. Your town can take over chunks at the edge of an overclaimed town that touch your own land with Take Over Claim. Depending on the server, your nations may have to be enemies." plots: title: "Plots" - description: "Owning, buying, and selling plots, plot types, settings, trust, and groups." + description: "Owning, buying, and selling plots, many plots at once, plot types, settings, trust, join-day limits, and groups." what: "What Is a Plot?" - what-body: "Every chunk a town claims is a plot. The town owns it until a resident buys it, and then the owner decides who may build there. Open the Plot menu while standing in a plot to see who owns it." + what-body: "Every chunk a town claims is a plot. The town owns it until a resident buys it, and then the owner decides who may build there. Open the Plot menu while standing in a plot to see who owns it, click its icon for Towny's full plot info, or turn on the Plot HUD to see it while you walk." buy: "Buying a Plot" - buy-body: "Plots for sale glow on the map. Walk into one, open the Plot menu, and choose Buy Plot to pay the price and become its owner." + buy-body: "Plots for sale glow on the map; right-click one there to buy it from afar. Or walk into it, open the Plot menu, and choose Buy Plot to pay the price and become its owner." sell: "Selling and Giving Up" - sell-body: "Owners and mayors can put a plot up for sale; a price of 0 gives it away. Give Up returns your plot to the town, and mayors can evict an owner." + sell-body: "Owners and mayors can put a plot up for sale; a price of 0 gives it away. Give Up returns your plot to the town. Mayors can evict an owner, and right-clicking Evict Owner puts the plot straight back up for sale." types: "Plot Types" types-body: "Mayors choose each plot's type. Shops and embassies have their own prices and taxes, and embassies can be bought by players from other towns. Arenas always allow PvP, inns let anyone sleep, farms let residents farm, wilds let residents cut trees and mine, and jails hold prisoners." settings: "Plot Settings" @@ -1088,11 +1504,15 @@ tutorial: trust: "Plot Trust" trust-body: "Trust a player on one plot to let them build there without changing the plot's permissions for everyone else." groups: "Groups and Districts" - groups-body: "A plot group bundles several plots so they are sold and managed together, like one big house. A district names a larger area of plots, such as a market quarter." + groups-body: "A plot group bundles several plots so they are sold and managed together, like one big house. While a plot is in a group, its sale, type, settings, permissions, trust, and join-day limits change the whole group. A district names a larger area of plots, such as a market quarter." + join-days: "Join-Day Limits" + join-days-body: "Mayors can keep a plot for newcomers or for long-time residents by setting the fewest days, the most days, or both, that a buyer must have lived in the town." + area: "Many Plots at Once" + area-body: "From the Plot menu, pick a square or circle and a radius around you, then buy every plot for sale in it, put your plots up for sale or take them off sale, or give them up. You can also give up every plot you own at once." protection: title: "Permissions & Protection" - description: "Who may build where, friends and trust, PvP and other switches, outlaws, and jail." + description: "Who may build where, friends, trust, and player overrides, PvP and other switches, outlaws, and jail." actions: "Four Kinds of Actions" actions-body: "Towny protects four actions separately: Build (placing blocks), Destroy (breaking blocks), Switch (doors, buttons, levers, and containers), and Item Use (items such as flint and steel or buckets)." groups: "Who Gets Permission" @@ -1111,10 +1531,14 @@ tutorial: outlaws-body: "Mayors and trusted ranks can declare troublemakers outlaws. Depending on the server, outlaws are warned or teleported away when they enter the town, and can be jailed." jail: "Jail" jail-body: "Towns can send players to a jail plot, for example outlaws or enemies killed in town. Jailed players wait out their sentence, or pay bail from their profile to leave early." + overrides: "Player Overrides" + overrides-body: "Plot owners can give single players their own build, break, switch, and item-use rights on a plot, whatever group they belong to. Add players under Player Overrides in the plot's permissions, then edit their rights in Towny's editor." + jailing: "Jailing Players" + jailing-body: "Mayors and sheriffs jail online residents from the town's Jail menu, choosing the sentence, bail, jail, and cell, and can release prisoners early. A jail is a plot with the Jail type; add cells to it from the Plot menu while standing inside." nations: title: "Nations" - description: "What nations are, joining and founding one, member towns, diplomacy, settings, and spawn." + description: "What nations are, joining and founding one, member towns, diplomacy, sanctions, levels, settings, and spawn." what: "What Is a Nation?" what-body: "A nation is a group of towns. The mayor of its capital town is the nation leader. Nations share a bank, a spawn, allies, and enemies, and member towns can receive extra claims." join: "Joining a Nation" @@ -1129,10 +1553,14 @@ tutorial: settings-body: "Open lets any town join without an invite, Public lets outsiders use the nation spawn, and Peaceful keeps member towns out of PvP and war." spawn: "Nation Spawn" spawn-body: "Members can teleport to the nation spawn, and so can allies and visitors when the nation's settings allow it. The leader moves the spawn in Nation Details." + sanctions: "Sanctions" + sanctions-body: "A nation can sanction towns outside it, and residents of a sanctioned town can't teleport to the nation spawn. Manage sanctioned towns from the nation's Towns menu." + level: "Nation Level" + level-body: "Click your nation at the top of the nation menu to see its level, what the next level needs, the bonus claims, outposts, and nation zone every member town gains, and the nation's daily upkeep." economy: title: "Money & Taxes" - description: "The Towny day, banks, taxes, upkeep, and bankruptcy." + description: "The Towny day, prices, banks, taxes, upkeep, and bankruptcy." day: "The Towny Day" day-body: "Once a day Towny collects taxes and upkeep. Towns and residents that can't pay what they owe face the consequences explained in the lessons below." bank: "Town and Nation Banks" @@ -1146,16 +1574,20 @@ tutorial: nation-tax: "Nation Taxes" nation-tax-body: "Nations charge each member town a daily tax from its bank. A town that can't pay may be removed from the nation." bankruptcy: "Debt and Bankruptcy" - bankruptcy-body: "When bankruptcy is on, a town that can't pay its upkeep goes into debt instead of being lost. A bankrupt town can't claim land or recruit until the debt is paid. Otherwise the town falls into ruin or is deleted." + bankruptcy-body: "When bankruptcy is on, a town that can't pay its upkeep goes into debt instead of being lost. A bankrupt town can't claim land or recruit until the debt is paid. Otherwise the town falls into ruin or is deleted. Either way TownyMenu warns the mayor in chat." + prices: "Prices and the Next Day" + prices-body: "Prices & New Day, on the main menu, counts down to the next Towny day and lists what this server charges for founding, claiming, upkeep, plots, and taxes." profile: title: "Your Profile" - description: "Your profile, other players, what you see while exploring, and spawning." + description: "Your profile, other players, your plots and taxes, what you see while exploring, and spawning." menu: "Your Profile" menu-body: "Your profile shows your town, ranks, friends, plots, and balance. Write a short bio that other players see when they look at your profile." others: "Other Players" others-body: "Click any resident in a town, nation, or friends list to view their profile. From there you can add them as a friend, and mayors can invite, trust, rank, or kick them." display: "Borders, Titles, and the Info Tool" - display-body: "Personal settings change what you see while exploring: a title when you cross into a town, particle borders around plots and towns, a chat map in every new chunk, and an info tool that shows who owns a block." + display-body: "Personal settings change what you see while exploring: a title when you cross into a town, particle borders around plots and towns, a chat map in every new chunk, and an info tool that shows who owns a block. Display Modes in your profile turns every mode off at once, or restores the server's defaults." spawn: "Spawning" spawn-body: "Go to Spawn teleports you to your town spawn, or to your bed while Bed Spawn is on. Spawning may cost money and have a cooldown, depending on the server." + standing: "Plots, Taxes, and Standing" + standing-body: "The bottom of your profile lists every plot you own, what you will pay in tax on the next new day, the towns that outlaw you, and the towns that trust you to build in them." diff --git a/src/main/resources/lang/zh_CN.yml b/src/main/resources/lang/zh_CN.yml index f6f5462..92dcc2c 100644 --- a/src/main/resources/lang/zh_CN.yml +++ b/src/main/resources/lang/zh_CN.yml @@ -23,6 +23,9 @@ command: tutorial: description: "了解城镇、地块和国家的玩法" players-only: "只有玩家可以打开教程。" + invites: + description: "处理城镇、国家与结盟邀请" + players-only: "只有玩家可以打开邀请菜单。" category: general: "通用" info: "信息" @@ -66,6 +69,7 @@ common: trust-online: "信任在线玩家" trust-name: "按名称信任" trust-name-description: "信任 Towny 记录中的任意玩家。" + duration: "{hours} 小时 {minutes} 分钟" menu: close: "关闭" @@ -137,6 +141,18 @@ main: admin-description: "配置 Towny,并管理所有城镇、国家和居民。" tutorial: "教程" tutorial-description: "了解城镇、地块、国家以及这些菜单的用法。" + prices: "价格与新的一天" + prices-description: "本服务器各项费用,以及下一个 Towny 日何时开始。" + leaderboards: "排行榜" + leaderboards-description: "规模最大、最富有的城镇、国家和玩家。" + +alert: + town-invite: "{town} 邀请你加入。/townymenu invites'>打开邀请菜单" + nation-invite: "{nation} 邀请你的城镇加入。/townymenu invites'>打开邀请菜单" + ally-request: "{nation} 请求与你的国家结盟。/townymenu invites'>打开邀请菜单" + bankrupt: "{town} 已破产并开始负债。请存入资金还清债务。/townymenu'>打开 TownyMenu" + ruined: "{town} 已沦为废墟。" + ruined-reclaim: "{town} 已沦为废墟。你可以在 {hours} 小时内花费 {cost} 收复它。/townymenu'>打开 TownyMenu" map: title: "地图" @@ -165,6 +181,7 @@ map: outpost: "前哨" for-sale: "出售中: {price}" click-town: "点击查看城镇" + right-buy: "右键购买此地块" invites: title: "邀请" @@ -215,6 +232,8 @@ perm: switch-description: "使用门、按钮、拉杆和容器。" item-use: "物品使用" item-use-description: "使用打火石、桶等物品。" + overrides: "玩家单独权限" + overrides-description: "为个别玩家单独设置在此土地上的建造、破坏和交互权限。" picker: select: "点击选择" @@ -243,7 +262,7 @@ town: claims-description: "占领和放弃土地、建立前哨以及购买额外领地。" settings: "城镇设置" settings-title: "城镇设置" - settings-description: "PvP、怪物、火焰、爆炸、开放、公开与和平。" + settings-description: "PvP、怪物、火焰、爆炸、开放、公开、和平与国家区域。" details: "城镇详情" details-description: "名称、公告、标签、税款、价格、出生点和出售。" permissions-title: "城镇权限" @@ -262,6 +281,13 @@ town: delete-description: "永久解散你的城镇。执行前会要求确认。" leave: "离开城镇" leave-description: "离开你的城镇。执行前会要求确认。" + jail: "监狱" + jail-description: "关押居民、释放囚犯并查看城镇监狱。" + jail-count: "囚犯:{count}" + reclaim: "收复城镇" + reclaim-description: "付费恢复已成为废墟的城镇,并成为其镇长。" + reclaim-wait: "成为废墟 {hours} 小时后可收复" + click-status: "点击查看等级、上限与维护费" no-town: title: "寻找城镇" @@ -272,6 +298,8 @@ no-town: browse: "浏览城镇" browse-description: "寻找一个可以加入的开放城镇。" invites-description: "接受加入城镇的邀请。" + reclaim: "收复废墟城镇" + reclaim-description: "站在废墟城镇中,付费成为其镇长。" outlaws: title: "通缉犯" @@ -289,6 +317,43 @@ outposts: world: "世界: {world}" location: "坐标: {x}, {y}, {z}" +town-jail: + title: "城镇监狱" + hours-left: "剩余时间:{hours} 小时" + bail: "保释金:{bail}" + held-in: "关押于:{jail},{cell} 号牢房" + click-unjail: "点击释放" + jail-player: "关押玩家" + jail-player-title: "关押玩家" + jail-player-description: "把一名在线居民送进城镇监狱。" + no-jails: "没有监狱" + no-jails-description: "把一个地块的类型设为监狱,建立城镇的第一座监狱。" + jails: "监狱列表" + jails-title: "城镇监狱列表" + jails-description: "城镇中的所有监狱地块。" + jails-count: "监狱:{count}" + primary: "主监狱" + cells: "牢房:{count}" + prisoners: "囚犯:{count}" + jail-number: "{number} 号监狱" + +jail-sentence: + title: "关押 {player}" + hours: "刑期" + hours-description: "玩家在监狱中关押的小时数。" + hours-title: "刑期" + hours-label: "小时" + max-hours: "最长刑期:{hours} 小时" + bail: "保释金" + bail-description: "玩家提前出狱需支付的金额。" + bail-title: "保释金" + max-bail: "最高保释金:{bail}" + jail: "监狱" + cell: "牢房" + cell-number: "{number} 号牢房" + confirm: "确认关押" + confirm-description: "按此刑期将玩家送进监狱。" + claims: title: "城镇领地" info: "领地" @@ -323,6 +388,11 @@ claims: bonus-amount: "领地数量" auto-claim: "自动占领" auto-claim-description: "自动占领你走进的每个区块。" + cede: "割让此地块" + cede-title: "将地块割让给城镇" + cede-description: "把你所站的区块交给另一个城镇。对方镇长必须在线并接受。" + takeover: "夺取领地" + takeover-description: "从与你相邻、领地超额的城镇夺取你所站的区块。" town-info: gone: "该城镇已不存在" @@ -341,9 +411,12 @@ town-info: buy-description: "成为该城镇的镇长。" invite: "邀请加入国家" invite-description: "邀请该城镇加入 {nation}。" + merge: "并入你的城镇" + merge-description: "请求 {town} 的镇长将其城镇并入 {own}。对方必须在线并接受。" town-list: title: "城镇列表" + distance: "距离:{blocks} 格" town-members: title: "居民: {town}" @@ -352,6 +425,9 @@ town-members: invite-name: "按名称邀请" invite-name-description: "邀请 Towny 记录中的任意玩家,即使对方不在线。" invite-title: "邀请加入城镇" + announce: "向城镇公告" + announce-title: "城镇公告" + announce-description: "向所有在线居民发送消息。" town-details: title: "城镇详情" @@ -385,6 +461,46 @@ town-details: for-sale-title: "出售城镇" for-sale-description: "让其他玩家可以购买你的城镇。" +town-status: + title: "状态:{town}" + level: "城镇等级" + level-description: "等级决定你的城镇可拥有的土地、前哨数量以及维护费。" + level-number: "等级:{level}/{max}" + level-name: "称号:{name}" + level-manual: "管理员手动设置了该等级。" + level-highest: "已达到最高等级。" + next-level-residents: "达到 {needed} 名居民可升级(当前 {current})" + next-level-claims: "达到 {needed} 块领地可升级(当前 {current})" + claims-description: "城镇已拥有的土地,以及还能占领多少。" + max-purchased: "可购买的额外领地:{count}" + outposts: "前哨" + outposts-description: "前哨让城镇可以在边界之外占领土地。" + upkeep: "每日维护费" + upkeep-description: "下一个新的一天城镇银行需要支付的金额。" + upkeep-exempt: "此城镇无需支付维护费。" + upkeep-daily: "维护费:{cost}" + upkeep-penalty: "领地超额罚金:{cost}" + upkeep-peaceful: "和平状态:{cost}" + upkeep-total: "合计:{cost}" + new-day: "距离新的一天:{time}" + nation-zone: "国家区域" + nation-zone-description: "由国家赋予的、城镇周围受保护的荒野。" + nation-zone-size: "范围:{size} 个区块" + all-good: "一切正常" + all-good-description: "你的城镇没有任何警告。" + ruined: "已成废墟" + ruined-hours: "将在 {hours} 小时后彻底消失" + bankrupt: "已破产" + bankrupt-description: "还清债务后才能重新占领土地和招募居民。" + debt-cap: "债务上限:{cap}" + conquered: "已被征服" + conquered-days: "剩余被征服天数:{days}" + overclaimed: "领地超额" + overclaimed-description: "放弃部分土地或扩大城镇,以免继续支付超额罚金。" + overclaimed-stealable: "领地超额期间,其他城镇可以夺取你的土地。" + upkeep-short: "维护费不足" + upkeep-short-description: "请在新的一天到来前存入资金,否则城镇将负债或化为废墟。" + town-trust: resident: "受信任的居民" town: "受信任的城镇" @@ -402,6 +518,7 @@ sort: online: "在线玩家" open: "开放加入" name: "名称" + nearby: "距离最近" nation: title: "我的国家" @@ -427,6 +544,10 @@ nation: leave-description: "让你的城镇退出国家。执行前会要求确认。" propose-alliance: "提议结盟" declare-enemy: "宣布为敌国" + announce: "向国家公告" + announce-title: "国家公告" + announce-description: "向国家所有在线居民发送消息。" + click-status: "点击查看等级、加成与维护费" no-nation: title: "寻找国家" @@ -473,6 +594,74 @@ nation-towns: invite: "邀请城镇" invite-title: "邀请城镇" invite-description: "邀请一个未加入国家的城镇。" + sanctions: "受制裁城镇" + sanctions-description: "禁止使用你们国家传送点的城镇。" + sanctions-count: "受制裁:{count}" + +nation-sanctions: + title: "制裁:{nation}" + right-lift: "右键解除制裁" + add: "制裁城镇" + add-title: "制裁城镇" + add-description: "禁止国家外的某个城镇使用你们的国家传送点。" + +leaderboard: + title: "排行榜" + info: "排行榜" + info-description: "与 Towny 排行榜相同的排名。点击任意条目可查看详情。" + rank: "第 {rank} 名" + town-residents: "城镇居民数排行" + town-residents-description: "居民最多的城镇。" + nation-residents: "国家居民数排行" + nation-residents-description: "居民最多的国家。" + town-land: "城镇领地排行" + town-land-description: "占领土地最多的城镇。" + resident-land: "玩家地块排行" + resident-land-description: "拥有地块最多的玩家。" + town-balance: "城镇银行排行" + town-balance-description: "最富有的城镇银行。" + nation-balance: "国家银行排行" + nation-balance-description: "最富有的国家银行。" + town-baltop: "城镇最富居民" + town-baltop-description: "{town} 中最富有的居民。" + nation-baltop: "国家最富居民" + nation-baltop-description: "{nation} 中最富有的居民。" + +prices: + title: "价格与新的一天" + new-day: "Towny 日" + new-day-description: "税款和维护费每天结算一次。" + no-economy: "本服务器未启用经济系统,因此一切都不需要花钱。" + founding: "建立" + new-town: "建立城镇:{cost}" + new-nation: "建立国家:{cost}" + reclaim: "收复废墟:{cost}" + merge: "合并城镇:{cost}" + claims: "土地" + claim: "每块领地:{cost}" + claim-refund: "放弃每块领地退还:{cost}" + bonus-claim: "每块额外领地:{cost}" + outpost: "每个前哨:{cost}" + outpost-upkeep: "每个前哨每日维护费:{cost}" + upkeep: "每日维护费" + upkeep-description: "每个新的一天从城镇和国家银行中扣除。" + town-upkeep: "城镇维护费:{cost}" + nation-upkeep: "国家维护费:{cost}" + overclaimed-upkeep: "领地超额罚金:{cost}" + town-peaceful: "和平城镇:{cost}" + nation-peaceful: "和平国家:{cost}" + plot-prices: "地块价格:{town}" + plot: "普通地块:{cost}" + shop: "商店地块:{cost}" + embassy: "使馆地块:{cost}" + taxes: "税款:{town}" + resident-tax: "居民税:{cost}" + plot-tax: "地块税:{cost}" + shop-tax: "商店税:{cost}" + embassy-tax: "使馆税:{cost}" + plot-types: "地块类型" + plot-types-description: "镇长把地块设为各类型所需的费用。" + plot-type: "{type}:{cost}" nation-details: title: "国家详情" @@ -496,6 +685,19 @@ nation-details: taxes: "每日城镇税" conquered-tax: "被征服城镇税" +nation-status: + title: "状态:{nation}" + level: "国家等级" + level-description: "等级决定成员城镇获得的加成以及国家的维护费。" + next-level-towns: "达到 {needed} 个城镇可升级(当前 {current})" + bonuses: "城镇加成" + bonuses-description: "每个成员城镇从国家获得的加成。" + bonus-claims: "每个城镇的额外领地:{count}" + bonus-outposts: "每个城镇的额外前哨:{count}" + bonus-capital-outposts: "首都的额外前哨:{count}" + upkeep-description: "下一个新的一天国家银行需要支付的金额。" + upkeep-short: "国家银行余额不足以支付。" + plot: title: "地块" unnamed: "地块" @@ -525,7 +727,7 @@ plot: type-description: "商店、大使馆、竞技场、农场、监狱等。" rename: "重命名地块" rename-title: "重命名地块" - rename-description: "玩家走进地块时会显示此名称。" + rename-description: "玩家走进地块时会显示此名称。右键移除名称。" plot-name: "地块名称" settings: "地块设置" settings-title: "地块设置" @@ -540,6 +742,29 @@ plot: clear: "清理地块" clear-description: "移除此地块上被服务器标记为可清理的方块。" town-description: "查看拥有这片土地的城镇。" + group-applies: "作用于 {group} 中的全部 {count} 个地块" + evict-resell: "右键驱逐并重新出售此地块" + join-days: "入镇天数限制" + join-days-description: "左键设置购买者至少需在城镇居住的天数,右键设置最多允许的天数。" + join-days-none: "无限制" + join-days-hint: "输入 0 取消限制。" + min-join-days: "最少入镇天数:{days}" + max-join-days: "最多入镇天数:{days}" + min-join-days-title: "最少入镇天数" + max-join-days-title: "最多入镇天数" + days: "天数" + jail-cells: "牢房" + jail-cells-description: "左键在你所站位置添加牢房,右键移除你所站位置的牢房。" + jail-cells-count: "牢房:{count}" + hud: "地块信息面板" + hud-description: "显示或隐藏侧边栏,展示你所在地块的主人、类型、价格和权限。" + area: "批量地块操作" + area-description: "购买、出售或放弃你周围一片区域内的所有地块。" + click-info: "点击在聊天栏显示 Towny 的地块信息" + make-outpost: "设为前哨" + make-outpost-description: "把此领地设为前哨,并以你所站位置作为前哨出生点。" + outpost-spawn: "移动前哨出生点" + outpost-spawn-description: "把此前哨的出生点移到你所站的位置。" # Towny 内置的地块类型。Towny 配置中的自定义类型会显示其原名,除非在此添加。 plot-type: @@ -584,6 +809,41 @@ plot-trust: title: "地块信任" trust-title: "信任玩家" trust-online-description: "允许一名在线玩家在此地块上建造。" + group: "地块组:{group}" + group-description: "此地块的信任作用于其所在组的全部 {count} 个地块。" + +plot-area: + title: "批量地块操作" + info: "你周围的区域" + info-description: "选择形状和半径,然后对该区域内的所有地块执行操作。半径 1 覆盖你周围 3×3 个区块。" + shape: "形状" + square: "方形" + circle: "圆形" + radius: "半径" + radius-title: "区域半径" + radius-description: "区域从你所在位置向外延伸的区块数。" + buy: "购买地块" + buy-description: "购买区域内所有你可以购买的出售中地块。" + sell: "出售地块" + sell-title: "出售区域内地块" + sell-description: "以同一价格出售区域内所有你可以出售的地块。" + not-for-sale: "停止出售地块" + not-for-sale-description: "停止出售区域内所有你可以管理的地块。" + give-up: "放弃地块" + give-up-description: "把你在区域内拥有的地块归还城镇。" + give-up-all: "放弃我的所有地块" + give-up-all-description: "把你在任何地方拥有的所有地块归还给所属城镇。" + +plot-overrides: + title: "玩家单独权限" + changed-by: "最后修改者:{player}" + click-remove: "点击移除单独权限" + add-online: "添加在线玩家" + add-name: "按名称添加" + add-description: "为玩家单独设置此处的权限,然后在 Towny 的编辑器中修改。" + add-title: "添加玩家单独权限" + edit: "编辑单独权限" + edit-description: "打开 Towny 的编辑器,选择每位玩家可以建造、破坏和使用的内容。" profile: title: "个人资料" @@ -610,6 +870,28 @@ profile: spawn-description: "传送到你的床或城镇出生点。" bail: "支付保释金" bail-description: "支付 {cost} 出狱。" + modes: "显示模式" + modes-description: "左键关闭所有模式,例如自动地图、边界和自动圈地。右键恢复服务器默认模式。" + modes-active: "已开启:{modes}" + plots: "我的地块" + plots-description: "你拥有的所有地块、位置以及每日花费。" + tax: "每日税款" + tax-description: "下一个新的一天你需要向城镇及为地块支付的金额。" + tax-exempt: "你免缴税款。" + tax-town: "城镇税:{tax}" + tax-plots: "地块税:{tax}" + tax-total: "合计:{tax}" + outlawed-in: "被通缉的城镇" + outlawed-in-description: "已将你列为通缉犯的城镇。" + outlawed-in-title: "被通缉的城镇" + trusted-in: "受信任的城镇" + trusted-in-description: "信任你、允许你在其中任意建造的城镇。" + trusted-in-title: "受信任的城镇" + towns-count: "城镇数:{count}" + +resident-plots: + title: "我的地块" + location: "位置:{world} 的 {x}, {z}" friends: title: "好友" @@ -707,6 +989,12 @@ toggle: plot-group-description: "将你走进的每个地块加入所选的地块组。" district: "街区模式" district-description: "将你走进的每个地块加入所选的街区。" + nation-zone: "国家区域" + nation-zone-description: "将城镇周围的土地作为国家区域加以保护。" + spy: "聊天监听" + spy-description: "查看所有城镇和国家聊天频道的消息。" + admin-bypass: "停用管理员权限" + admin-bypass-description: "开启后,Towny 会把你当作普通玩家,便于测试保护设置。" admin: title: "服务器管理" @@ -850,6 +1138,20 @@ admin-server: regenerations-description: "在所有世界开启或关闭放弃领地修复和荒野爆炸修复。" left-on: "左键在所有世界开启" right-off: "右键在所有世界关闭" + purge: "清理不活跃居民" + purge-description: "删除超过指定天数未登录的居民。Towny 会要求你确认。" + purge-label: "离线天数" + purge-hint: "离线时间超过该天数的居民将被删除。" + check-perm: "检查权限" + check-perm-description: "查看某位在线玩家是否拥有某个权限节点。" + deposit-all: "为所有银行存款" + deposit-all-description: "向每个城镇和国家的银行存入相同金额。" + deposit-all-hint: "该金额会存入服务器上每个城镇和国家的银行。" + +admin-tools: + add: "按名称添加" + add-description: "将玩家或城镇加入此列表。" + click-remove: "点击移除" admin-plugin: title: "TownyMenu 设置" @@ -864,6 +1166,10 @@ admin-plugin: shortcut-description: "潜行时按副手切换键(默认 F)打开主菜单。" join-hint: "加入时提示教程" join-hint-description: "没有城镇的玩家加入时向其推荐教程,直到对方读完所有课程。" + live-refresh: "菜单实时刷新" + live-refresh-description: "当 Towny 改变菜单显示的内容时(例如居民加入或新的一天)刷新已打开的菜单。" + alerts: "聊天提醒" + alerts-description: "在聊天中提醒邀请、破产和废墟,并附上可打开对应菜单的链接。" admin-town: title: "管理: {town}" @@ -906,6 +1212,36 @@ admin-town: top-lists: "显示在排行榜" top-lists-description: "在 /towny top 排行中显示该城镇。" +admin-town-tools: + title: "工具:{town}" + open: "更多城镇工具" + open-description: "等级、合并、出售、通缉犯、信任与职位。" + level: "城镇等级" + level-description: "将城镇固定在某个等级,而不由其规模决定。" + level-title: "设置城镇等级" + level-label: "等级" + level-hint: "输入 unset 可恢复由城镇规模决定。" + merge: "合并其他城镇" + merge-description: "你选择的城镇将并入 {town} 并消失。" + merge-force: "右键即使 Towny 拒绝也强制合并。" + merge-title: "合并哪个城镇?" + for-sale: "挂牌出售" + for-sale-description: "允许玩家购买此城镇并成为镇长。" + for-sale-title: "出售城镇" + not-for-sale: "取消出售" + not-for-sale-description: "停止出售此城镇。" + unconquer: "解除被征服状态" + unconquer-description: "解除此城镇被征服的状态。" + check-outposts-description: "移除此城镇已不在其领地上的前哨出生点。" + ranks: "居民职位" + ranks-description: "授予或收回城镇职位,忽略通常的授予限制。" + ranks-title: "修改谁的职位?" + outlaws-description: "此城镇宣布为通缉犯的玩家。" + trust-description: "受信任、可在此城镇任意建造的玩家。" + trusted-towns: "受信任城镇" + trusted-towns-description: "其居民可在此城镇任意建造的城镇。" + trust-town-title: "信任城镇" + admin-nation: title: "管理: {nation}" toggles: "国家设置" @@ -920,6 +1256,68 @@ admin-nation: delete: "删除国家" delete-description: "删除该国家。其城镇会保留,但不再属于任何国家。" +admin-nation-tools: + title: "工具:{nation}" + open: "更多国家工具" + open-description: "等级、合并、转入城镇、制裁与职位。" + level: "国家等级" + level-description: "将国家固定在某个等级,而不由其规模决定。" + level-title: "设置国家等级" + merge: "合并其他国家" + merge-description: "你选择的国家将并入 {nation} 并消失。" + merge-title: "合并哪个国家?" + transfer: "转入城镇" + transfer-description: "将某个城镇从原国家移出,并加入 {nation}。" + transfer-title: "转入哪个城镇?" + recheck: "重新检查城镇距离" + recheck-description: "移除距离首都过远的成员城镇。" + sanctions-description: "禁止使用该国家传送点的城镇。" + ranks: "居民职位" + ranks-description: "授予或收回国家职位,忽略通常的授予限制。" + +admin-plot: + title: "管理:地块" + open: "你所站的地块" + open-description: "为玩家占领、转给其他城镇、添加信任玩家,或放弃该地块。" + wilderness-description: "此区块尚未被占领。" + claim: "为玩家占领" + claim-description: "让一名在线玩家成为此地块的主人。" + claim-title: "为谁占领?" + trust-description: "受信任、可在此地块建造的玩家。" + move: "转给其他城镇" + move-description: "把此地块交给另一个城镇。" + move-title: "交给哪个城镇?" + unclaim: "放弃领地" + unclaim-description: "左键放弃此区块,右键放弃你周围的方形区域。" + unclaim-title: "放弃区域" + +admin-perms: + title: "Towny 权限" + open: "Towny 权限" + open-description: "编辑 townyperms.yml:所有居民拥有的权限,以及镇长可授予的职位。" + info: "townyperms.yml" + info-description: "更改会立即保存并生效,等同于 /townyadmin townyperms。" + groups: "权限组" + groups-description: "没有职位的玩家所拥有的权限:流浪者、居民、镇长与国王。" + groups-count: "权限组:{count}" + group-title: "权限组:{group}" + town-ranks: "城镇职位" + town-ranks-description: "镇长可授予居民的职位,以及每个职位允许的操作。" + nation-ranks: "国家职位" + nation-ranks-description: "国王可授予居民的职位,以及每个职位允许的操作。" + ranks-count: "职位:{count}" + node-count: "权限数:{count}" + node-label: "权限节点" + click-remove: "点击移除此权限" + right-remove: "右键删除此职位" + add-node: "添加权限" + add-node-description: "为此权限组添加一个权限节点。" + add-rank: "添加职位" + add-rank-description: "创建一个职位,随后可为其添加权限。" + rank-label: "职位名称" + rename-rank: "重命名职位" + rename-rank-description: "重命名职位,并保留已持有该职位的居民。" + admin-resident: title: "管理: {name}" jailed: "已被监禁" @@ -1007,10 +1405,16 @@ tutorial: nation-upkeep: "你国家的每日维护费: {cost}" bankruptcy: "城镇破产: {value}" debt: "你的城镇已破产,欠款 {debt}。" + merge-cost: "合并城镇的基础费用:{cost}" + merge-distance: "合并的最远距离:{distance} 个区块" + reclaim-cost: "收复废墟的费用:{cost}" + reclaim-townless: "无城镇玩家可收复废墟:{value}" + bail: "默认保释金:{cost}" + max-bail: "最高保释金:{cost}" start: title: "入门" - description: "Towny 是什么、如何打开和看懂这些菜单、荒野、地图以及邀请。" + description: "Towny 是什么、如何打开和看懂这些菜单、荒野、地图、邀请以及排行榜。" towny: "Towny 是什么?" towny-body: "Towny 让玩家通过建立城镇来保护土地。城镇占领的区块不会被外人破坏,城镇可以收税,还能与其他城镇组成国家。无人占领的土地就是荒野。" menus: "打开菜单" @@ -1020,9 +1424,11 @@ tutorial: wilderness: "荒野" wilderness-body: "未被占领的土地不属于任何人。视世界设置而定,你也许可以在那里建造,但建筑不受任何保护。加入城镇才能保护你的成果。" map: "地图" - map-body: "地图显示你周围的区块,并按归属着色:你的城镇、你的地块、你的国家、盟友、敌人以及其他城镇。发光的格子正在出售。点击已占领的格子可查看所属城镇。" + map-body: "地图显示你周围的区块,并按归属着色:你的城镇、你的地块、你的国家、盟友、敌人以及其他城镇。发光的格子正在出售。点击已占领的格子可查看所属城镇,右键出售中的格子即可购买。" invites: "邀请" - invites-body: "城镇邀请玩家,国家邀请城镇,国家之间还会发送结盟请求。所有待处理的邀请都在邀请菜单中,你可以在那里接受或拒绝。" + invites-body: "城镇邀请玩家,国家邀请城镇,国家之间还会发送结盟请求。所有待处理的邀请都在邀请菜单中,你可以在那里接受或拒绝。收到邀请时 TownyMenu 还会在聊天中提醒你,点击消息即可打开该菜单。" + leaderboards: "排行榜" + leaderboards-body: "排行榜菜单按居民数、领地和银行余额为城镇与国家排名,也按拥有的地块数为玩家排名。点击任意条目即可打开。" join: title: "寻找城镇" @@ -1030,7 +1436,7 @@ tutorial: why: "为什么要加入城镇?" why-body: "城镇居民共享受保护的土地,可以拥有地块、传送到城镇出生点,并参与所在城镇的国家。荒野中的建筑没有这些保护。" browse: "加入城镇" - browse-body: "浏览城镇目录,可按居民数、领地或是否开放排序。开放的城镇一键即可加入,其他城镇需要镇长或助理邀请你。" + browse-body: "浏览城镇目录,可按居民数、领地、是否开放或与你的距离排序。开放的城镇一键即可加入,其他城镇需要镇长或助理邀请你。" invites: "接受邀请" invites-body: "城镇邀请你后,在邀请菜单中接受即可。你同一时间只能属于一个城镇。如果不想收到邀请,可以在个人设置中开启忽略邀请。" found: "建立城镇" @@ -1040,7 +1446,7 @@ tutorial: town: title: "经营城镇" - description: "职位、招募、出生点、设置、详情、出售以及废墟。" + description: "等级、职位、招募、出生点、设置、详情、公告、合并、出售以及废墟。" ranks: "镇长与职位" ranks-body: "镇长拥有全部权力。助理、警长、建筑师等职位分担其中一部分,每个职位的具体权限由服务器决定。在居民菜单中打开居民资料即可授予职位。" grow: "壮大城镇" @@ -1048,17 +1454,23 @@ tutorial: spawn: "出生点与主区块" spawn-body: "主区块是城镇的核心,城镇出生点就在其中。居民可以从城镇菜单传送到出生点;城镇公开时访客也可以。镇长可以在城镇详情中移动出生点和主区块。" settings: "城镇设置" - settings-body: "PvP、敌对生物、火焰蔓延和爆炸对整个城镇统一开关。开放让任何人无需邀请即可加入,公开让访客可以使用你的出生点,和平则让城镇远离 PvP 与战争。" + settings-body: "PvP、敌对生物、火焰蔓延和爆炸对整个城镇统一开关。开放让任何人无需邀请即可加入,公开让访客可以使用你的出生点,和平则让城镇远离 PvP 与战争。国家中的城镇还可以关闭保护其周边土地的国家区域。" details: "名称、公告与标签" details-body: "在城镇详情中,镇长可以重命名城镇、编写居民登录时看到的公告、设置简短的聊天标签,并选择城镇的地图颜色。" sell: "出售城镇" sell-body: "镇长可以在城镇详情中出售城镇。购买者支付价格后成为新镇长。出售中的城镇会在城镇目录中标出。" ruins: "废墟与删除" - ruins-body: "删除城镇会把它的全部土地归还荒野。启用废墟时,被删除或破产的城镇会先变成废墟,随时间逐渐对所有人开放,并可能在消失前被收复。" + ruins-body: "删除城镇会把它的全部土地归还荒野。启用废墟时,被删除或破产的城镇会先变成废墟,随时间逐渐对所有人开放。在废墟消失前,其居民可以在城镇菜单中收复它;在部分服务器上,无城镇的玩家也可以站在废墟中,通过“寻找城镇”菜单收复。" + announce: "公告" + announce-body: "在居民菜单中使用“向城镇公告”,向所有在线居民发送消息。国家领袖也可以在国家菜单中向整个国家发布公告。" + merge: "合并城镇" + merge-body: "镇长可以在城镇目录中打开另一个城镇的页面,请求它并入自己的城镇。对方镇长必须在线并接受,之后对方的居民、土地和银行都会并入你的城镇。" + status: "城镇等级与警告" + status-body: "在城镇菜单中点击顶部的城镇图标,即可查看等级、升级所需的居民或领地数量、土地与前哨上限,以及下一个新的一天需要支付的维护费。负债、领地超额、被征服等需要注意的情况会显示在这里和城镇图标上。" claims: title: "占领土地" - description: "区块与领地上限、占领与放弃、前哨以及额外领地。" + description: "区块与领地上限、占领与放弃、前哨、额外领地以及割让与夺取土地。" chunks: "领地以区块为单位" chunks-body: "城镇以区块为单位占领土地,每个区块称为一块领地。每块已占领的区块都受城镇权限保护。可占领的数量取决于居民人数和额外领地。" claim: "占领土地" @@ -1068,19 +1480,23 @@ tutorial: unclaim: "放弃土地" unclaim-body: "放弃不再需要的区块可以腾出领地名额。主区块不能被放弃。有些服务器会慢慢把被放弃的土地恢复原貌。" outposts: "前哨" - outposts-body: "前哨是远离城镇的领地,例如远处的矿场或农场。每个前哨都有居民可以传送过去的出生点,还可以在其周围继续占领土地。" + outposts-body: "前哨是远离城镇的领地,例如远处的矿场或农场。每个前哨都有居民可以传送过去的出生点,还可以在其周围继续占领土地。镇长也可以在地块菜单中把现有领地设为前哨,或移动前哨的出生点。" bonus: "额外领地" bonus-body: "领地达到上限了?用城镇银行的钱购买额外领地。每次购买后价格可能上涨。" + cede: "割让土地" + cede-body: "站在城镇的某个区块中,选择“割让此地块”即可把它交给另一个城镇,例如用于调整边界。对方镇长必须在线并接受。主区块无法割让。" + takeover: "夺取领地" + takeover-body: "领地超过上限的城镇处于超额状态。你的城镇可以用“夺取领地”占领超额城镇边缘、与你的领地相邻的区块。视服务器设置而定,双方国家可能需要互为敌对。" plots: title: "地块" - description: "拥有、购买与出售地块,地块类型、设置、信任以及地块组。" + description: "拥有、购买与出售地块,批量地块操作,地块类型、设置、信任、入镇天数限制以及地块组。" what: "什么是地块?" - what-body: "城镇占领的每个区块都是一个地块。在居民购买之前,地块归城镇所有;购买后由地块主决定谁可以在上面建造。站在地块中打开地块菜单即可查看其主人。" + what-body: "城镇占领的每个区块都是一个地块。在居民购买之前,地块归城镇所有;购买后由地块主决定谁可以在上面建造。站在地块中打开地块菜单即可查看其主人,点击地块图标可查看 Towny 的完整地块信息,或开启地块信息面板在行走时查看。" buy: "购买地块" - buy-body: "出售中的地块在地图上会发光。走进地块,打开地块菜单并选择购买地块,支付价格后即可成为地块主。" + buy-body: "出售中的地块在地图上会发光,在地图上右键即可远程购买。也可以走进地块,打开地块菜单并选择购买地块,支付价格后即可成为地块主。" sell: "出售与放弃" - sell-body: "地块主和镇长可以出售地块,价格设为 0 即免费赠送。放弃地块会把它归还城镇,镇长也可以驱逐地块主。" + sell-body: "地块主和镇长可以出售地块,价格设为 0 即免费赠送。放弃地块会把它归还城镇。镇长可以驱逐地块主,右键“驱逐所有者”还会立即重新出售该地块。" types: "地块类型" types-body: "镇长可以设置每个地块的类型。商店和大使馆有各自的价格与税率,大使馆还能被其他城镇的玩家购买。竞技场始终允许 PvP,旅馆允许任何人睡觉,农场允许居民耕种,野外允许居民砍树和挖矿,监狱则用来关押犯人。" settings: "地块设置" @@ -1088,11 +1504,15 @@ tutorial: trust: "地块信任" trust-body: "在单个地块上信任某位玩家,即可让其在此建造,而不必为所有人修改地块权限。" groups: "地块组与街区" - groups-body: "地块组把多个地块捆绑在一起,统一出售和管理,就像一座大房子。街区则为一片更大的地块区域命名,例如商业区。" + groups-body: "地块组把多个地块捆绑在一起,统一出售和管理,就像一座大房子。地块属于某个组时,它的出售、类型、设置、权限、信任和入镇天数限制都会作用于整个组。街区则为一片更大的地块区域命名,例如商业区。" + join-days: "入镇天数限制" + join-days-body: "镇长可以把地块留给新居民或老居民:设置购买者至少或最多需要在城镇居住的天数,也可以两者都设。" + area: "批量地块操作" + area-body: "在地块菜单中选择你周围的方形或圆形区域及半径,即可购买其中所有出售中的地块、出售或停止出售你的地块,或放弃这些地块。你也可以一次放弃自己拥有的所有地块。" protection: title: "权限与保护" - description: "谁能在哪里建造、好友与信任、PvP 等开关、通缉犯以及监狱。" + description: "谁能在哪里建造、好友、信任与玩家单独权限、PvP 等开关、通缉犯以及监狱。" actions: "四种操作" actions-body: "Towny 分别保护四种操作:建造(放置方块)、破坏(破坏方块)、开关(门、按钮、拉杆和容器)以及物品使用(打火石、桶等物品)。" groups: "谁能获得权限" @@ -1111,10 +1531,14 @@ tutorial: outlaws-body: "镇长和受信任的职位可以将捣乱者宣布为通缉犯。视服务器设置而定,通缉犯进入城镇时会收到警告或被传送走,还可能被关进监狱。" jail: "监狱" jail-body: "城镇可以把玩家关进监狱地块,例如在城镇中被击杀的通缉犯或敌人。被关押的玩家需要服满刑期,或在个人资料中支付保释金提前出狱。" + overrides: "玩家单独权限" + overrides-body: "地块主可以为个别玩家单独设置在地块上的建造、破坏、开关和物品使用权限,不论其属于哪个群体。在地块权限的“玩家单独权限”中添加玩家,然后在 Towny 的编辑器中修改其权限。" + jailing: "关押玩家" + jailing-body: "镇长和治安官可以在城镇监狱菜单中关押在线居民,选择刑期、保释金、监狱和牢房,也可以提前释放囚犯。监狱是类型为“监狱”的地块,站在其中时可在地块菜单中添加牢房。" nations: title: "国家" - description: "国家是什么、加入与建立国家、成员城镇、外交、设置以及出生点。" + description: "国家是什么、加入与建立国家、成员城镇、外交、制裁、等级、设置以及出生点。" what: "什么是国家?" what-body: "国家由多个城镇组成,首都城镇的镇长就是国王。国家共享银行、出生点、盟友和敌人,成员城镇还可能获得额外领地。" join: "加入国家" @@ -1129,10 +1553,14 @@ tutorial: settings-body: "开放让任何城镇无需邀请即可加入,公开让外人可以使用国家出生点,和平则让成员城镇远离 PvP 与战争。" spawn: "国家出生点" spawn-body: "成员可以传送到国家出生点,国家设置允许时盟友和访客也可以。国王可以在国家详情中移动出生点。" + sanctions: "制裁" + sanctions-body: "国家可以制裁国家外的城镇,受制裁城镇的居民无法传送到国家传送点。可在国家的城镇菜单中管理受制裁城镇。" + level: "国家等级" + level-body: "在国家菜单中点击顶部的国家图标,即可查看等级、升级条件、每个成员城镇获得的额外领地、前哨和国家区域,以及国家的每日维护费。" economy: title: "金钱与税收" - description: "Towny 日、银行、税收、维护费以及破产。" + description: "Towny 日、价格、银行、税收、维护费以及破产。" day: "Towny 日" day-body: "Towny 每天收取一次税款和维护费。无法支付欠款的城镇和居民将承担下面几节课中说明的后果。" bank: "城镇与国家银行" @@ -1146,16 +1574,20 @@ tutorial: nation-tax: "国家税" nation-tax-body: "国家每天从每个成员城镇的银行收取税款。无法缴税的城镇可能会被移出国家。" bankruptcy: "债务与破产" - bankruptcy-body: "启用破产时,无法支付维护费的城镇会负债,而不会直接失去。破产的城镇在还清债务前无法占领土地或招募居民。未启用时,城镇会沦为废墟或被删除。" + bankruptcy-body: "启用破产时,无法支付维护费的城镇会负债,而不会直接失去。破产的城镇在还清债务前无法占领土地或招募居民。未启用时,城镇会沦为废墟或被删除。无论哪种情况,TownyMenu 都会在聊天中提醒镇长。" + prices: "价格与下一天" + prices-body: "主菜单中的“价格与新的一天”会倒计时下一个 Towny 日,并列出本服务器在建立、占领、维护、地块和税款方面的收费。" profile: title: "个人资料" - description: "你的资料、其他玩家、探索时能看到的信息以及出生点。" + description: "你的资料、其他玩家、你的地块与税款、探索时能看到的信息以及出生点。" menu: "你的资料" menu-body: "个人资料显示你的城镇、职位、好友、地块和余额。写一段简短的介绍,其他玩家查看你的资料时就能看到。" others: "其他玩家" others-body: "在城镇、国家或好友列表中点击任意居民即可查看其资料。你可以在那里添加好友,镇长还可以邀请、信任、授予职位或踢出对方。" display: "边界、标题与信息工具" - display-body: "个人设置会改变你探索时看到的内容:进入城镇时的标题、地块和城镇周围的粒子边界、进入新区块时的聊天地图,以及显示方块归属的信息工具。" + display-body: "个人设置会改变你探索时看到的内容:进入城镇时的标题、地块和城镇周围的粒子边界、进入新区块时的聊天地图,以及显示方块归属的信息工具。个人资料中的“显示模式”可以一次关闭所有模式,或恢复服务器默认设置。" spawn: "出生点" spawn-body: "前往出生点会把你传送到城镇出生点;开启床出生点时则传送到你的床。视服务器设置而定,传送可能需要费用并有冷却时间。" + standing: "地块、税款与处境" + standing-body: "个人资料底部列出你拥有的所有地块、下一个新的一天需缴纳的税款、将你列为通缉犯的城镇,以及信任你在其中建造的城镇。"