Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/
Expand All @@ -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
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) |

Expand All @@ -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

Expand Down
14 changes: 14 additions & 0 deletions docs/DEVELOPER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions docs/UTILITY_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,12 @@ putting them into a MiniMessage string — otherwise a town named `<click:run_co
| `onOff(Player, Boolean)` | A coloured On / Off label in the player's language |
| `yesNo(Player, Boolean)` | A coloured Yes / No label in the player's language |
| `plotType(Player, String)` | A plot type in the player's language (`plot-type.*`), or the raw name |
| `relation(Resident?, Town)` | A town's `Relation` to the viewer: `TOWN`, `NATION`, `ALLY`, `ENEMY`, `OTHER` |
| `economy` | `true` when Towny's economy is active |

`relation` is what colours a town on the map and its borders in border view, so both always agree; use it for any
new view that colours towns by how they stand towards the viewer.

### Permissions and Input

| Method | Description |
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
kotlin.code.style=official

# Plugin Properties
plugin_version=1.1.0
plugin_version=1.1.1

# Dependency Versions
towny_version=0.103.2.7
2 changes: 2 additions & 0 deletions src/main/kotlin/net/trilleo/mc/plugins/townymenu/Main.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package net.trilleo.mc.plugins.townymenu

import net.trilleo.mc.plugins.townymenu.borders.BorderParticles
import net.trilleo.mc.plugins.townymenu.config.PluginConfig
import net.trilleo.mc.plugins.townymenu.guis.framework.Menu
import net.trilleo.mc.plugins.townymenu.registration.CommandRegistrar
Expand All @@ -23,6 +24,7 @@ class Main : JavaPlugin() {
CommandRegistrar.registerAll(this)
PermissionRegistrar.registerAll(this)
ListenerRegistrar.registerAll(this)
BorderParticles.start(this)
}

/** Re-reads `config.yml` and the language files, applying the message prefix and language. */
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package net.trilleo.mc.plugins.townymenu.borders

import com.palmergames.bukkit.towny.TownyAPI
import com.palmergames.bukkit.towny.TownySettings
import com.palmergames.bukkit.towny.`object`.Resident
import com.palmergames.bukkit.towny.`object`.TownBlock
import com.palmergames.bukkit.towny.`object`.WorldCoord
import net.trilleo.mc.plugins.townymenu.utils.TownyUtil
import net.trilleo.mc.plugins.townymenu.utils.TownyUtil.Relation
import org.bukkit.Color
import org.bukkit.Particle
import org.bukkit.entity.Player
import org.bukkit.plugin.java.JavaPlugin
import kotlin.math.floor

/**
* Draws a short wall of dust particles along every claim border near a player with [BorderView] on.
*
* Each side of a border is drawn just inside its own claim, in the colour of that town's relation to the viewer
* (the same colours as the map), so a border between two towns shows both colours side by side. Nation borders are
* taller and thicker than borders between towns of one nation, and the viewer's own plots get a low yellow line.
* Only the viewer sees the particles, and only the part of a border within their chosen range.
*/
object BorderParticles {

private const val INTERVAL_TICKS = 10L
private const val INSET = 0.2

private enum class Kind(val size: Float, val rows: Int) {
NATION(1.4f, 3),
TOWN(1.0f, 2),
PLOT(0.7f, 1),
}

private enum class Side(val dx: Int, val dz: Int) { NORTH(0, -1), SOUTH(0, 1), WEST(-1, 0), EAST(1, 0) }

private val relationColors = mapOf(
Relation.TOWN to Color.fromRGB(0x55FF55),
Relation.NATION to Color.fromRGB(0x55FFFF),
Relation.ALLY to Color.fromRGB(0x5555FF),
Relation.ENEMY to Color.fromRGB(0xFF5555),
Relation.OTHER to Color.fromRGB(0xFFAA00),
)
private val plotColor = Color.fromRGB(0xFFFF55)

fun start(plugin: JavaPlugin) {
plugin.server.scheduler.runTaskTimer(plugin, Runnable {
if (!BorderView.available) return@Runnable
plugin.server.onlinePlayers.filter(BorderView::isOn).forEach(::draw)
}, INTERVAL_TICKS, INTERVAL_TICKS)
}

private fun draw(player: Player) {
val api = TownyAPI.getInstance()
val world = player.world
if (!api.isTownyWorld(world)) return
val viewer = api.getResident(player)
val plots = viewer != null && BorderView.showsPlots(player)
val range = BorderView.range(player).toDouble()
val size = TownySettings.getTownBlockSize()
val location = player.location

val blocks = HashMap<Pair<Int, Int>, 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)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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<Int>
get() = RANGES.filter { it <= maxRange }.ifEmpty { listOf(maxRange) }
}
Original file line number Diff line number Diff line change
@@ -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<out String>): 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<out String>): List<String> =
if (args.size == 1) OPTIONS.filter { it.startsWith(args[0].lowercase()) } else emptyList()

private companion object {
val OPTIONS = listOf("on", "off", "toggle")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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`).
*
Expand Down
Loading