diff --git a/CHANGELOG.md b/CHANGELOG.md index b2c05e9..1c9cc2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,35 @@ ## Unreleased +## Version 1.1.1 + +### New Features + +#### Border View + ++ Turn on Border View and a short wall of particles appears on town and nation borders as you walk near them, so you + always know when you are about to cross into someone else's land. Only you see the particles. ++ Each side of a border is drawn in the colour of the town it belongs to, the same colours as the map: green for your + town, aqua for your nation, blue for allies, red for enemies, and gold for other towns. A border between two towns + shows both colours side by side. ++ Nation borders are taller and thicker than the borders between towns of one nation, and the plots you own can be + outlined with a low yellow line. ++ Choose how close a border has to be before it appears, from 4 blocks up to the server's limit. ++ Open it from the new Border View button in the map menu, or with `/townymenu borders`; `/townymenu borders on`, `off`, + and `toggle` switch it without opening the menu. Your choices are remembered across restarts. ++ A new tutorial lesson in the Your Profile chapter explains it. + +#### Admin Menus + ++ TownyMenu Settings can turn Border View off for the whole server and set the farthest range players may pick. + +### Technical Details + +#### Misc + ++ New `border-view` and `border-view-max-range` settings in `config.yml`. The new `TownyUtil.relation` gives a town's + relation to a viewer, and now colours both the map and the border particles. + ## Version 1.1.0 ### Improvements diff --git a/CLAUDE.md b/CLAUDE.md index 6457115..d7a85f5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -67,6 +67,7 @@ accepted, before `startServer` works. The server console reads commands from the ``` src/main/kotlin/net/trilleo/mc/plugins/townymenu/ ├── Main.kt # Plugin entry point (Main.instance) +├── borders/ # Border view: per-player settings (BorderView) and the particle task (BorderParticles) ├── commands/ # Sub-commands (auto-registered) │ ├── info/ │ └── moderation/ @@ -77,7 +78,7 @@ src/main/kotlin/net/trilleo/mc/plugins/townymenu/ │ ├── admin/ # Admin menus (/tm admin): Towny config editor, worlds, server, towns, nations │ ├── town/ nation/ plot/ resident/ │ ├── tutorial/ # Tutorial hub, chapter menu, and all lesson content (Tutorial.kt) -│ └── MainMenu.kt, MapMenu.kt, InvitesMenu.kt +│ └── MainMenu.kt, MapMenu.kt, BorderViewMenu.kt, InvitesMenu.kt ├── listeners/ # Event listeners (auto-registered) ├── registration/ # Auto-registration engine (do not modify lightly) └── utils/ # itemStack DSL, Lang, TownyUtil, Prices, TownyConfig, DialogUtil, MessageUtil, LoreUtil diff --git a/README.md b/README.md index eb6b74f..971fe6c 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,9 @@ 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; right-click one to buy it. +- **Border view** — Turn it on and a short wall of particles appears on every town and nation border you walk near, + coloured like the map: your town, your nation, allies, enemies, and other towns. Nation borders stand taller, your + own plots can be outlined too, and you pick how close a border must be before it shows. Only you see the particles. - **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 @@ -89,6 +92,7 @@ All commands are sub-commands of `/townymenu` (alias `/tm`). | `/tm help` | List all available commands | | `/tm tutorial` | Open the tutorial | | `/tm invites` | Answer town, nation, and alliance invitations | +| `/tm borders` | Open border view, or `on` / `off` / `toggle` it | | `/tm reload` | Reload the configuration and translations (OP only) | | `/tm admin` | Open the admin menu (OP only) | @@ -103,6 +107,8 @@ All commands are sub-commands of `/townymenu` (alias `/tm`). | `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 | | `bank-amounts` | `10, 100, 1000, 10000` | Preset amounts the deposit and withdraw menus offer, alongside Everything and a custom amount | +| `border-view` | `true` | Let players turn on particle borders for nearby towns and nations | +| `border-view-max-range` | `16` | The farthest, in blocks (1–64), a player may set border view to | ## Translations diff --git a/docs/DEVELOPER_GUIDE.md b/docs/DEVELOPER_GUIDE.md index 169ae31..46d2052 100644 --- a/docs/DEVELOPER_GUIDE.md +++ b/docs/DEVELOPER_GUIDE.md @@ -420,6 +420,20 @@ and keep the handler a one-liner. Players can turn this off with `live-menu-refr `TownyAlertListener` sends the chat alerts for invitations, bankruptcy, and ruin, each linking to the menu that answers it (`towny-alerts`). +### Border View (`borders`) + +Border view draws particle walls on claim borders near players who turn it on (`BorderViewMenu`, opened from the map +or `/townymenu borders`). + +- `BorderView` holds each player's settings in their persistent data — on/off, whether their own plots are outlined, + and the range — and `available`, the server-wide `border-view` switch. `ranges` lists the distances a player may pick, + capped by `border-view-max-range`. +- `BorderParticles.start` (called from `Main.onEnable`) runs a task every 10 ticks. For each player with border view + on, it looks up the townblocks within their range and draws, just inside each claim, every edge whose neighbour + belongs to another town or the wilderness, coloured by `TownyUtil.relation`. Edges where the nation changes are + taller and thicker; the viewer's own plot edges inside their town are a low yellow line. Particles are sent with + `Player.spawnParticle`, so only that player sees them, and nothing is cached between runs. + ### Admin Menus (`guis/admin`) `AdminMenu` is the server admin hub, opened by `/townymenu admin` or a main-menu button. Both entry points require diff --git a/docs/UTILITY_GUIDE.md b/docs/UTILITY_GUIDE.md index cd0ad05..601734d 100644 --- a/docs/UTILITY_GUIDE.md +++ b/docs/UTILITY_GUIDE.md @@ -119,8 +119,12 @@ putting them into a MiniMessage string — otherwise a town named `, TownBlock?>() + fun block(x: Int, z: Int) = blocks.getOrPut(x to z) { api.getTownBlock(WorldCoord(world.name, x, z)) } + + for (x in floor((location.x - range) / size).toInt()..floor((location.x + range) / size).toInt()) { + for (z in floor((location.z - range) / size).toInt()..floor((location.z + range) / size).toInt()) { + val plot = block(x, z) ?: continue + val town = plot.townOrNull ?: continue + for (side in Side.entries) { + val neighbour = block(x + side.dx, z + side.dz) + val neighbourTown = neighbour?.townOrNull + val kind = when { + neighbourTown != town -> + if (town.hasNation() && neighbourTown?.nationOrNull != town.nationOrNull) Kind.NATION + else Kind.TOWN + + plots && ownedBy(plot, viewer) && !ownedBy(neighbour, viewer) -> Kind.PLOT + else -> continue + } + val color = if (kind == Kind.PLOT) plotColor + else relationColors.getValue(TownyUtil.relation(viewer, town)) + edge(player, x, z, size, side, kind, Particle.DustOptions(color, kind.size), range) + } + } + } + } + + private fun ownedBy(plot: TownBlock?, viewer: Resident?): Boolean = + viewer != null && plot?.residentOrNull == viewer + + /** Draws one block-spaced line along [side] of the townblock at [x], [z], inset into that townblock. */ + private fun edge( + player: Player, x: Int, z: Int, size: Int, side: Side, kind: Kind, dust: Particle.DustOptions, range: Double, + ) { + val location = player.location + val fixed = when (side) { + Side.NORTH -> z * size + INSET + Side.SOUTH -> (z + 1) * size - INSET + Side.WEST -> x * size + INSET + Side.EAST -> (x + 1) * size - INSET + } + val alongX = side == Side.NORTH || side == Side.SOUTH + val start = if (alongX) x * size else z * size + repeat(size) { step -> + val along = start + step + 0.5 + val px = if (alongX) along else fixed + val pz = if (alongX) fixed else along + val dx = px - location.x + val dz = pz - location.z + if (dx * dx + dz * dz > range * range) return@repeat + repeat(kind.rows) { row -> + player.spawnParticle(Particle.DUST, px, location.y + 0.5 + row, pz, 1, 0.0, 0.0, 0.0, 0.0, dust) + } + } + } +} diff --git a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/borders/BorderView.kt b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/borders/BorderView.kt new file mode 100644 index 0000000..c26ee1e --- /dev/null +++ b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/borders/BorderView.kt @@ -0,0 +1,52 @@ +package net.trilleo.mc.plugins.townymenu.borders + +import net.trilleo.mc.plugins.townymenu.Main +import org.bukkit.NamespacedKey +import org.bukkit.entity.Player +import org.bukkit.persistence.PersistentDataType + +/** + * Each player's border view settings, kept in their persistent data so they survive restarts. + * [BorderParticles] reads them every time it draws. + */ +object BorderView { + + /** The distances, in blocks, a player can pick from; [ranges] drops those above the server's limit. */ + private val RANGES = listOf(4, 8, 12, 16, 24, 32) + private const val DEFAULT_RANGE = 8 + + private val enabledKey by lazy { NamespacedKey(Main.instance, "border-view") } + private val plotsKey by lazy { NamespacedKey(Main.instance, "border-view-plots") } + private val rangeKey by lazy { NamespacedKey(Main.instance, "border-view-range") } + + /** Whether the server lets players use border view at all (`border-view` in `config.yml`). */ + val available: Boolean + get() = Main.instance.pluginConfig.borderView + + private val maxRange: Int + get() = Main.instance.pluginConfig.borderViewMaxRange + + fun isOn(player: Player): Boolean = + player.persistentDataContainer.getOrDefault(enabledKey, PersistentDataType.BOOLEAN, false) + + fun setOn(player: Player, on: Boolean) = + player.persistentDataContainer.set(enabledKey, PersistentDataType.BOOLEAN, on) + + /** Whether the edges of the player's own plots are drawn too. */ + fun showsPlots(player: Player): Boolean = + player.persistentDataContainer.getOrDefault(plotsKey, PersistentDataType.BOOLEAN, true) + + fun setShowsPlots(player: Player, show: Boolean) = + player.persistentDataContainer.set(plotsKey, PersistentDataType.BOOLEAN, show) + + /** How close, in blocks, the player must be to a border to see it, capped at the server's limit. */ + fun range(player: Player): Int = + (player.persistentDataContainer.get(rangeKey, PersistentDataType.INTEGER) ?: DEFAULT_RANGE) + .coerceAtMost(maxRange) + + fun setRange(player: Player, range: Int) = + player.persistentDataContainer.set(rangeKey, PersistentDataType.INTEGER, range) + + val ranges: List + get() = RANGES.filter { it <= maxRange }.ifEmpty { listOf(maxRange) } +} diff --git a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/commands/info/BordersCommand.kt b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/commands/info/BordersCommand.kt new file mode 100644 index 0000000..96283a9 --- /dev/null +++ b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/commands/info/BordersCommand.kt @@ -0,0 +1,54 @@ +package net.trilleo.mc.plugins.townymenu.commands.info + +import net.trilleo.mc.plugins.townymenu.borders.BorderView +import net.trilleo.mc.plugins.townymenu.guis.BorderViewMenu +import net.trilleo.mc.plugins.townymenu.registration.PluginCommand +import net.trilleo.mc.plugins.townymenu.utils.sendPrefixed +import net.trilleo.mc.plugins.townymenu.utils.tr +import org.bukkit.command.CommandSender +import org.bukkit.entity.Player + +/** + * Opens the border view menu, or with `on`, `off`, or `toggle` switches border particles straight away. + * Registered as `/townymenu borders`. + */ +class BordersCommand : PluginCommand( + name = "borders", + description = "Show town and nation borders as particles", + usage = "/townymenu borders [on|off|toggle]" +) { + override fun execute(sender: CommandSender, args: Array): Boolean { + if (sender !is Player) { + sender.sendRichMessage(sender.tr("command.borders.players-only")) + return true + } + if (!BorderView.available) { + sender.sendPrefixed(sender.tr("command.borders.unavailable")) + return true + } + val option = args.firstOrNull()?.lowercase() + if (option == null) { + BorderViewMenu(sender, null).open() + return true + } + val on = when (option) { + "on" -> true + "off" -> false + "toggle" -> !BorderView.isOn(sender) + else -> { + sender.sendPrefixed(sender.tr("command.borders.usage")) + return true + } + } + BorderView.setOn(sender, on) + sender.sendPrefixed(sender.tr(if (on) "command.borders.on" else "command.borders.off")) + return true + } + + override fun tabComplete(sender: CommandSender, args: Array): List = + if (args.size == 1) OPTIONS.filter { it.startsWith(args[0].lowercase()) } else emptyList() + + private companion object { + val OPTIONS = listOf("on", "off", "toggle") + } +} 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 414fb5f..cc67e18 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 @@ -56,6 +56,16 @@ class PluginConfig(private val plugin: JavaPlugin) { get() = plugin.config.getBoolean("towny-alerts", true) set(value) = save("towny-alerts", value) + /** Whether players may turn on particle borders for towns and nations (`border-view`). */ + var borderView: Boolean + get() = plugin.config.getBoolean("border-view", true) + set(value) = save("border-view", value) + + /** The largest border view range, in blocks, a player may pick (`border-view-max-range`), kept within 1–64. */ + var borderViewMaxRange: Int + get() = plugin.config.getInt("border-view-max-range", 16).coerceIn(1, 64) + set(value) = save("border-view-max-range", value.coerceIn(1, 64)) + /** * The preset amounts the deposit and withdraw menus offer (`bank-amounts`). * diff --git a/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/BorderViewMenu.kt b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/BorderViewMenu.kt new file mode 100644 index 0000000..703c8b5 --- /dev/null +++ b/src/main/kotlin/net/trilleo/mc/plugins/townymenu/guis/BorderViewMenu.kt @@ -0,0 +1,60 @@ +package net.trilleo.mc.plugins.townymenu.guis + +import net.trilleo.mc.plugins.townymenu.borders.BorderView +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.tr +import org.bukkit.Material +import org.bukkit.entity.Player + +/** The viewer's [BorderView] settings: particle borders on or off, their own plots, the range, and a colour key. */ +class BorderViewMenu(player: Player, back: Menu?) : Menu(player, player.tr("border-view.title"), 3, back) { + + override fun build() { + if (!BorderView.available) { + button(13, Icons.icon(Material.BARRIER, tr("border-view.unavailable"), tr("border-view.unavailable-description"))) + backButton(22) + return + } + + button( + 10, Icons.toggle( + player, Material.ENDER_EYE, tr("border-view.show"), BorderView.isOn(player), + tr("border-view.show-description") + ) + ) { + BorderView.setOn(player, !BorderView.isOn(player)) + render() + } + button( + 12, Icons.toggle( + player, Material.GLOWSTONE_DUST, tr("border-view.plots"), BorderView.showsPlots(player), + tr("border-view.plots-description") + ) + ) { + BorderView.setShowsPlots(player, !BorderView.showsPlots(player)) + render() + } + val range = BorderView.range(player) + button( + 14, Icons.icon( + Material.SPYGLASS, tr("border-view.range"), tr("border-view.range-description"), + tr("border-view.range-current", "range" to range), + actions = hints("click.next", "click.previous") + ) + ) { click -> + BorderView.setRange(player, BorderView.ranges.cycle(range, click)) + render() + } + button( + 16, Icons.icon( + Material.PAPER, tr("map.legend"), tr("border-view.legend-description"), + tr("map.legend-town"), tr("map.legend-plot"), tr("map.legend-nation"), tr("map.legend-ally"), + tr("map.legend-enemy"), tr("map.legend-other"), tr("border-view.legend-nation-border") + ) + ) + backButton(22) + tutorialButton(26, Tutorial.PROFILE) + } +} 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 9a0ff5b..7660ef7 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 @@ -5,11 +5,13 @@ import com.palmergames.bukkit.towny.`object`.Resident 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.borders.BorderView 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.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.TownyUtil.Relation import net.trilleo.mc.plugins.townymenu.utils.itemStack import net.trilleo.mc.plugins.townymenu.utils.tr import org.bukkit.Material @@ -19,7 +21,7 @@ import org.bukkit.inventory.ItemStack /** * A 9×5 chunk map centred on the viewer (north is up). Clicking claimed land * opens that town, right-clicking a plot for sale buys it, and the bottom row - * claims or unclaims the viewer's own chunk. + * claims or unclaims the viewer's own chunk and opens [BorderViewMenu]. */ class MapMenu(player: Player, back: Menu?) : Menu(player, player.tr("map.title"), 6, back) { @@ -82,6 +84,17 @@ class MapMenu(player: Player, back: Menu?) : Menu(player, player.tr("map.title") actions = hints("click.refresh") ) ) { render() } + if (BorderView.available) { + button( + 51, Icons.icon( + Material.ENDER_EYE, tr("map.border-view"), tr("map.border-view-description"), + tr("icon.status", "value" to tr(if (BorderView.isOn(player)) "icon.enabled" else "icon.disabled")), + actions = hints("click.open") + ) + ) { + BorderViewMenu(player, this).open() + } + } tutorialButton(53, Tutorial.GETTING_STARTED) } @@ -93,15 +106,16 @@ class MapMenu(player: Player, back: Menu?) : Menu(player, player.tr("map.title") private fun cell(coord: WorldCoord, plot: TownBlock?, viewer: Resident?, here: Boolean): ItemStack { val town = plot?.townOrNull val nation = town?.nationOrNull - val viewerNation = viewer?.nationOrNull val (material, color) = when { town == null -> Material.WHITE_STAINED_GLASS_PANE to "" viewer != null && plot.residentOrNull == viewer -> Material.YELLOW_STAINED_GLASS_PANE to "" - town == viewer?.townOrNull -> Material.LIME_STAINED_GLASS_PANE to "" - viewerNation != null && nation == viewerNation -> Material.LIGHT_BLUE_STAINED_GLASS_PANE to "" - viewerNation != null && nation != null && viewerNation.hasAlly(nation) -> Material.BLUE_STAINED_GLASS_PANE to "" - viewerNation != null && nation != null && viewerNation.hasEnemy(nation) -> Material.RED_STAINED_GLASS_PANE to "" - else -> Material.ORANGE_STAINED_GLASS_PANE to "" + else -> when (TownyUtil.relation(viewer, town)) { + Relation.TOWN -> Material.LIME_STAINED_GLASS_PANE to "" + Relation.NATION -> Material.LIGHT_BLUE_STAINED_GLASS_PANE to "" + Relation.ALLY -> Material.BLUE_STAINED_GLASS_PANE to "" + Relation.ENEMY -> Material.RED_STAINED_GLASS_PANE to "" + Relation.OTHER -> Material.ORANGE_STAINED_GLASS_PANE to "" + } } return itemStack(if (here) Material.PLAYER_HEAD else material) { if (here) head(player) 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 8575a6f..14fa28d 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 @@ -17,7 +17,7 @@ class PluginSettingsMenu(player: Player, back: Menu) : Menu(player, player.tr("a get() = Main.instance.pluginConfig override fun build() { - val row = layout(10, 11, 12, 13, 14, 15, 16) + val row = layout(10, 11, 12, 13, 14, 15, 16, 19, 20, 21, 22, 23, 24, 25) val language = config.language row.add( Icons.icon( @@ -109,6 +109,31 @@ class PluginSettingsMenu(player: Player, back: Menu) : Menu(player, player.tr("a apply() } } + row.add( + Icons.toggle( + player, Material.ENDER_EYE, tr("admin-plugin.border-view"), config.borderView, + tr("admin-plugin.border-view-description") + ) + ) { + config.borderView = !config.borderView + apply() + } + row.add( + Icons.icon( + Material.SPYGLASS, tr("admin-plugin.border-range"), tr("admin-plugin.border-range-description"), + tr("border-view.range-current", "range" to config.borderViewMaxRange), + actions = listOf(tr("click.change")) + ) + ) { + prompt( + tr("admin-plugin.border-range"), + tr("admin-plugin.border-range-label"), + initial = config.borderViewMaxRange.toString() + ) { input -> + input.trim().toIntOrNull()?.let { config.borderViewMaxRange = it } + apply() + } + } backButton(31) } 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 8bf5516..ac8513f 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,6 +7,7 @@ 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.borders.BorderView 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 @@ -734,6 +735,12 @@ object Tutorial { Lesson( "profile/display", Material.GLOWSTONE_DUST, "tutorial.profile.display", "tutorial.profile.display-body", always { player, back -> ResidentMenu(player, back).toggles() }), + Lesson( + "profile/border-view", Material.ENDER_EYE, "tutorial.profile.border-view", + "tutorial.profile.border-view-body", + always { player, back -> BorderViewMenu(player, back) }, + visible = { BorderView.available }, + ), Lesson( "profile/spawn", Material.RED_BED, "tutorial.profile.spawn", "tutorial.profile.spawn-body", always { player, back -> ResidentMenu(player, back) }), 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 0113b95..f779227 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 @@ -51,6 +51,23 @@ object TownyUtil { fun about(resident: Resident): String? = resident.about.takeUnless { it.isNullOrBlank() || it == TownySettings.getDefaultResidentAbout() } + /** How a town stands towards a viewer, which picks its colour on the map and in border view. */ + enum class Relation { TOWN, NATION, ALLY, ENEMY, OTHER } + + /** [town]'s [Relation] to [viewer]: their own town, a town of their nation, an ally, an enemy, or neither. */ + fun relation(viewer: Resident?, town: Town): Relation { + val nation = town.nationOrNull + val viewerNation = viewer?.nationOrNull + return when { + viewer != null && town == viewer.townOrNull -> Relation.TOWN + viewerNation == null || nation == null -> Relation.OTHER + nation == viewerNation -> Relation.NATION + viewerNation.hasAlly(nation) -> Relation.ALLY + viewerNation.hasEnemy(nation) -> Relation.ENEMY + else -> Relation.OTHER + } + } + /** 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 "-" diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index eb11fa8..3876884 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -24,6 +24,13 @@ live-menu-refresh: true # alliance, or a town goes bankrupt or falls into ruin. Each message opens the menu that answers it. towny-alerts: true +# Let players see town and nation borders as particles (/townymenu borders, or Border View in the map menu). +# Each player turns it on for themselves; nearby borders are drawn in the colour of that town's relation to them. +border-view: true + +# The farthest, in blocks, a player may pick for border view. Larger values draw more particles. +border-view-max-range: 16 + # The preset amounts offered by the deposit and withdraw menus, in the server's currency. # Every player also gets an "All" button and a custom-amount dialog, so this list is only a shortcut. # Towny takes whole numbers only; up to nine amounts fit in the menu row. diff --git a/src/main/resources/lang/en_US.yml b/src/main/resources/lang/en_US.yml index 51aecbd..1895026 100644 --- a/src/main/resources/lang/en_US.yml +++ b/src/main/resources/lang/en_US.yml @@ -34,6 +34,13 @@ command: invites: description: "Answer town, nation, and alliance invitations" players-only: "Only players can open the invites menu." + borders: + description: "Show town and nation borders as particles" + players-only: "Only players can see border particles." + unavailable: "Border view is turned off on this server." + usage: "Usage: /townymenu borders [on|off|toggle]" + "on": "Border view enabled. Walk near a border to see it." + "off": "Border view disabled." category: general: "General" info: "Info" @@ -264,6 +271,22 @@ map: for-sale: "For sale: {price}" click-town: "Click to view the town!" right-buy: "Right-click to buy this plot!" + border-view: "Border View" + border-view-description: "See town and nation borders as coloured particles when you walk near them." + +border-view: + title: "Border View" + show: "Show Borders" + show-description: "Draw a short wall of particles on town and nation borders when you come near one. Only you can see them." + plots: "Your Plot Borders" + plots-description: "Also outline the plots you own inside a town." + range: "Range" + range-description: "How close you need to be to a border before it appears." + range-current: "Range: {range} blocks" + legend-description: "Each side of a border is drawn in the colour of the town it belongs to." + legend-nation-border: "Taller, thicker walls mark a nation border." + unavailable: "Border View Unavailable" + unavailable-description: "The server has turned border view off." invites: title: "Invites" @@ -1286,6 +1309,11 @@ admin-plugin: bank-amounts: "Bank Amounts" bank-amounts-description: "The preset amounts the deposit and withdraw menus offer. Players always get an Everything button and a custom amount too." bank-amounts-label: "Amounts, separated by commas" + border-view: "Border View" + border-view-description: "Let players turn on particle borders for the towns and nations near them." + border-range: "Border View Range" + border-range-description: "The farthest a player can set border view to. Larger ranges draw more particles." + border-range-label: "Range in blocks (1-64)" admin-town: title: "Admin: {town}" @@ -1704,6 +1732,8 @@ tutorial: 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 Modes in your profile turns every mode off at once, or restores the server's defaults." + border-view: "Border View" + border-view-body: "Border View, on the map or with /townymenu borders, draws a short wall of particles along every border near you, coloured like the map: your town, your nation, allies, enemies, and other towns each get their own colour. Nation borders are taller and thicker, and the plots you own get a low yellow line." 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" diff --git a/src/main/resources/lang/zh_CN.yml b/src/main/resources/lang/zh_CN.yml index 38bdc83..338f243 100644 --- a/src/main/resources/lang/zh_CN.yml +++ b/src/main/resources/lang/zh_CN.yml @@ -33,6 +33,13 @@ command: invites: description: "处理城镇、国家与结盟邀请" players-only: "只有玩家可以打开邀请菜单。" + borders: + description: "以粒子显示城镇和国家边界" + players-only: "只有玩家可以查看边界粒子。" + unavailable: "本服务器已关闭边界显示。" + usage: "用法: /townymenu borders [on|off|toggle]" + "on": "边界显示已开启。靠近边界即可看到。" + "off": "边界显示已关闭。" category: general: "通用" info: "信息" @@ -263,6 +270,22 @@ map: for-sale: "出售中: {price}" click-town: "点击查看城镇!" right-buy: "右键购买此地块!" + border-view: "边界显示" + border-view-description: "靠近城镇和国家边界时,以彩色粒子显示它们。" + +border-view: + title: "边界显示" + show: "显示边界" + show-description: "靠近城镇或国家边界时,在边界上显示一小段粒子墙。只有你能看到。" + plots: "你的地块边界" + plots-description: "同时勾勒出你在城镇中拥有的地块。" + range: "范围" + range-description: "你需要离边界多近,边界才会显示。" + range-current: "范围: {range} 格" + legend-description: "边界的每一侧都以其所属城镇的颜色显示。" + legend-nation-border: "更高更粗的粒子墙表示国家边界。" + unavailable: "边界显示不可用" + unavailable-description: "服务器已关闭边界显示。" invites: title: "邀请" @@ -1285,6 +1308,11 @@ admin-plugin: bank-amounts: "银行预设金额" bank-amounts-description: "存款和取款菜单提供的预设金额。玩家始终还能使用“全部”按钮和自定义金额。" bank-amounts-label: "金额,用逗号分隔" + border-view: "边界显示" + border-view-description: "允许玩家为附近的城镇和国家开启粒子边界。" + border-range: "边界显示范围" + border-range-description: "玩家可设置的最大边界显示范围。范围越大,绘制的粒子越多。" + border-range-label: "范围(格,1-64)" admin-town: title: "管理: {town}" @@ -1703,6 +1731,8 @@ tutorial: others-body: "在城镇、国家或好友列表中点击任意居民即可查看其资料。你可以在那里添加好友,镇长还可以邀请、信任、授予职位或踢出对方。" display: "边界、标题与信息工具" display-body: "个人设置会改变你探索时看到的内容:进入城镇时的标题、地块和城镇周围的粒子边界、进入新区块时的聊天地图,以及显示方块归属的信息工具。个人资料中的“显示模式”可以一次关闭所有模式,或恢复服务器默认设置。" + border-view: "边界显示" + border-view-body: "边界显示位于地图菜单中,也可使用 /townymenu borders 打开。它会在你附近的每条边界上显示一小段粒子墙,颜色与地图一致:你的城镇、你的国家、盟友、敌人和其他城镇各有各的颜色。国家边界更高更粗,你拥有的地块则显示一条低矮的黄线。" spawn: "出生点" spawn-body: "前往出生点会把你传送到城镇出生点;开启床出生点时则传送到你的床。视服务器设置而定,传送可能需要费用并有冷却时间。" standing: "地块、税款与处境"