Compare commits

..

2 Commits

Author SHA1 Message Date
84c4252592 feat: コンテナ追加 2025-12-20 01:56:22 +09:00
47bfcfc1e4 feat: 表示安定化 2025-12-20 01:26:09 +09:00
3 changed files with 391 additions and 80 deletions

View File

@ -9,6 +9,14 @@ import org.bukkit.block.sign.Side
import org.bukkit.plugin.java.JavaPlugin import org.bukkit.plugin.java.JavaPlugin
object ShopVisuals { object ShopVisuals {
val VALID_CONTAINERS = setOfNotNull(
org.bukkit.Material.CHEST,
org.bukkit.Material.TRAPPED_CHEST,
org.bukkit.Material.BARREL,
org.bukkit.Material.DECORATED_POT,
org.bukkit.Material.COPPER_CHEST
)
fun updateSign(plugin: JavaPlugin, block: org.bukkit.block.Block, shopData: ShopData, side: Side) { fun updateSign(plugin: JavaPlugin, block: org.bukkit.block.Block, shopData: ShopData, side: Side) {
val state = block.state as? Sign ?: return val state = block.state as? Sign ?: return
val signSide = state.getSide(side) val signSide = state.getSide(side)
@ -46,18 +54,75 @@ object ShopVisuals {
// L3: (Empty) // L3: (Empty)
// L4: <Owner Name> (Gray) // L4: <Owner Name> (Gray)
signSide.line(0, Component.text(itemName, NamedTextColor.AQUA)) signSide.line(0, Component.empty())
signSide.line(1, Component.text("x$amount - $priceStr", NamedTextColor.BLACK)) signSide.line(1, Component.text(itemName, NamedTextColor.AQUA))
signSide.line(2, Component.empty()) signSide.line(2, Component.text("x$amount - $priceStr", NamedTextColor.BLACK))
signSide.line(3, Component.text(ownerName, NamedTextColor.GRAY)) signSide.line(3, Component.text(ownerName, NamedTextColor.GRAY))
signSide.isGlowingText = true
// Update the sign one tick later to ensure persistence compatibility // Update the sign immediately
// Or if called from command, update immediately? state.isWaxed = true // Force lock visually
// Safer to always schedule if in doubt, or check constraints.
// For command, immediate update is fine, but for event, delay is needed.
// Let's us runTask always for safety.
plugin.server.scheduler.runTask(plugin, Runnable {
state.update() state.update()
}) }
fun calculateStock(block: org.bukkit.block.Block, item: org.bukkit.inventory.ItemStack?): Int {
if (item == null) return 0
val downBlock = block.getRelative(org.bukkit.block.BlockFace.DOWN)
if (downBlock.type !in VALID_CONTAINERS) return 0
// Use InventoryHolder to support Chests, Barrels, etc.
val container = downBlock.state as? org.bukkit.inventory.InventoryHolder ?: return 0
val inventory = container.inventory
var count = 0
for (i in 0 until inventory.size) {
val slotItem = inventory.getItem(i)
if (slotItem != null && slotItem.isSimilar(item)) {
count += slotItem.amount
}
}
return count
}
fun removeDisplay(location: org.bukkit.Location, face: String) {
val world = location.world ?: return
val block = location.block
val data = block.blockData as? org.bukkit.block.data.Rotatable ?: return
val rotation = data.rotation
val yaw = yawFromBlockFace(rotation) + 90f
val offset = if (face == "FRONT") org.joml.Vector3f(0.25f, 0.25f, 0.0f) else org.joml.Vector3f(-0.25f, 0.25f, 0.0f)
val rad = Math.toRadians(yaw.toDouble())
offset.rotateY(-rad.toFloat())
val center = location.clone().add(0.5, 0.0, 0.5)
val targetPos = center.clone().add(offset.x.toDouble(), offset.y.toDouble(), offset.z.toDouble())
// Search for entity
val nearby = world.getNearbyEntities(targetPos, 0.2, 0.2, 0.2)
nearby.filterIsInstance<org.bukkit.entity.ItemDisplay>().forEach { it.remove() }
}
fun yawFromBlockFace(face: org.bukkit.block.BlockFace): Float {
return when (face) {
org.bukkit.block.BlockFace.SOUTH -> 0f
org.bukkit.block.BlockFace.SOUTH_SOUTH_WEST -> 22.5f
org.bukkit.block.BlockFace.SOUTH_WEST -> 45f
org.bukkit.block.BlockFace.WEST_SOUTH_WEST -> 67.5f
org.bukkit.block.BlockFace.WEST -> 90f
org.bukkit.block.BlockFace.WEST_NORTH_WEST -> 112.5f
org.bukkit.block.BlockFace.NORTH_WEST -> 135f
org.bukkit.block.BlockFace.NORTH_NORTH_WEST -> 157.5f
org.bukkit.block.BlockFace.NORTH -> 180f
org.bukkit.block.BlockFace.NORTH_NORTH_EAST -> 202.5f
org.bukkit.block.BlockFace.NORTH_EAST -> 225f
org.bukkit.block.BlockFace.EAST_NORTH_EAST -> 247.5f
org.bukkit.block.BlockFace.EAST -> 270f
org.bukkit.block.BlockFace.EAST_SOUTH_EAST -> 292.5f
org.bukkit.block.BlockFace.SOUTH_EAST -> 315f
org.bukkit.block.BlockFace.SOUTH_SOUTH_EAST -> 337.5f
else -> 0f
}
} }
} }

View File

@ -51,14 +51,30 @@ object ShopCommands {
} }
} }
literal("update-signs") { literal("list") {
permission { permission {
description = "Force update all shop signs" description = "List all shops sorted by distance"
defaultValue = org.bukkit.permissions.PermissionDefault.OP defaultValue = org.bukkit.permissions.PermissionDefault.TRUE
} }
executes { executes {
val count = updateAllSigns(plugin) if (sender !is Player) {
sender.sendMessage(Component.text("Updated $count shop signs.", NamedTextColor.GREEN)) sender.sendMessage(Component.text("Only players can use this command.", NamedTextColor.RED))
return@executes
}
displayShopList(plugin, sender as Player)
}
}
literal("delete") {
permission {
description = "Delete a shop by ID"
defaultValue = org.bukkit.permissions.PermissionDefault.OP
}
integer("id") {
executes {
val id: Int = argument("id")
deleteShop(plugin, sender, id)
}
} }
} }
} }
@ -83,20 +99,87 @@ object ShopCommands {
sender.sendMessage(Component.text("Given Shop Sign Tier $tier", NamedTextColor.GREEN)) sender.sendMessage(Component.text("Given Shop Sign Tier $tier", NamedTextColor.GREEN))
} }
private fun updateAllSigns(plugin: ShopPlugin): Int { private fun displayShopList(plugin: ShopPlugin, player: Player) {
val shops = net.hareworks.hcu.shop.database.ShopRepository.findAll() val shops = net.hareworks.hcu.shop.database.ShopRepository.findAll()
var count = 0 val playerLoc = player.location
for (shop in shops) { val playerWorldUid = player.world.uid
val world = org.bukkit.Bukkit.getWorld(shop.worldUid) ?: continue
val block = world.getBlockAt(shop.x, shop.y, shop.z) // Filter by world and sort by distance
// Ideally we check if loaded, but getBlockAt loads chunk if not async? val sortedShops = shops.filter { it.worldUid == playerWorldUid }
// In main thread command, it's fine for now, but beware of lag with thousands of shops. .map { shop ->
val loc = org.bukkit.Location(player.world, shop.x.toDouble(), shop.y.toDouble(), shop.z.toDouble())
shop to playerLoc.distance(loc)
}
.sortedBy { it.second }
player.sendMessage(Component.text("--- Shop List (Nearest) ---", NamedTextColor.GOLD))
if (sortedShops.isEmpty()) {
player.sendMessage(Component.text("No shops found in this world.", NamedTextColor.GRAY))
} else {
sortedShops.forEach { (shop, distance) ->
val distStr = String.format("%.1fm", distance)
val itemStack = shop.item
val itemName = if (itemStack != null) {
itemStack.type.name.lowercase().split("_")
.joinToString(" ") { it.replaceFirstChar { c -> c.uppercase() } }
} else {
"Not Set"
}
val price = net.hareworks.hcu.economy.api.EconomyService.format(shop.price.toInt())
player.sendMessage(
Component.text("#${shop.id}", NamedTextColor.YELLOW)
.append(Component.text(" - ", NamedTextColor.GRAY))
.append(Component.text(itemName, NamedTextColor.AQUA))
.append(Component.text(" ($price)", NamedTextColor.GREEN))
.append(Component.text(" - ", NamedTextColor.GRAY))
.append(Component.text(distStr, NamedTextColor.GRAY))
)
}
}
}
private fun deleteShop(plugin: ShopPlugin, sender: org.bukkit.command.CommandSender, id: Int) {
val shop = net.hareworks.hcu.shop.database.ShopRepository.findById(id)
if (shop == null) {
sender.sendMessage(Component.text("Shop with ID $id not found.", NamedTextColor.RED))
return
}
// Remove Display
val world = org.bukkit.Bukkit.getWorld(shop.worldUid)
if (world != null) {
val location = org.bukkit.Location(world, shop.x.toDouble(), shop.y.toDouble(), shop.z.toDouble())
net.hareworks.hcu.shop.ShopVisuals.removeDisplay(location, shop.face)
val block = location.block
if (block.type == Material.OAK_SIGN) { if (block.type == Material.OAK_SIGN) {
// Check for other shops at this location
val otherFace = if (shop.face == "FRONT") "BACK" else "FRONT"
val otherShop = net.hareworks.hcu.shop.database.ShopRepository.findByLocationAndFace(location, otherFace)
if (otherShop == null) {
// No other shops, remove the block
block.type = Material.AIR
// Create breakage particles/sound? Optional.
} else {
// Clear text for that side
val sign = block.state as? org.bukkit.block.Sign
if (sign != null) {
val side = if (shop.face == "FRONT") org.bukkit.block.sign.Side.FRONT else org.bukkit.block.sign.Side.BACK val side = if (shop.face == "FRONT") org.bukkit.block.sign.Side.FRONT else org.bukkit.block.sign.Side.BACK
net.hareworks.hcu.shop.ShopVisuals.updateSign(plugin, block, shop, side) val signSide = sign.getSide(side)
count++ signSide.line(0, Component.empty())
signSide.line(1, Component.empty())
signSide.line(2, Component.empty())
signSide.line(3, Component.empty())
sign.update()
sender.sendMessage(Component.text("Sign block kept because another shop exists on the other side.", NamedTextColor.YELLOW))
} }
} }
return count }
}
net.hareworks.hcu.shop.database.ShopRepository.delete(id)
sender.sendMessage(Component.text("Shop #$id deleted.", NamedTextColor.GREEN))
} }
} }

View File

@ -16,6 +16,7 @@ import org.bukkit.entity.ItemDisplay
import org.bukkit.event.EventHandler import org.bukkit.event.EventHandler
import org.bukkit.event.Listener import org.bukkit.event.Listener
import org.bukkit.event.block.BlockPlaceEvent import org.bukkit.event.block.BlockPlaceEvent
import org.bukkit.event.block.BlockBreakEvent
import org.bukkit.event.block.SignChangeEvent import org.bukkit.event.block.SignChangeEvent
import org.bukkit.event.player.PlayerInteractEvent import org.bukkit.event.player.PlayerInteractEvent
import org.bukkit.event.block.Action import org.bukkit.event.block.Action
@ -33,7 +34,7 @@ class ShopListener(private val plugin: ShopPlugin) : Listener {
private val tierKey = NamespacedKey(plugin, "shop_tier") private val tierKey = NamespacedKey(plugin, "shop_tier")
@EventHandler @EventHandler(ignoreCancelled = true)
fun onShopSignPlace(event: BlockPlaceEvent) { fun onShopSignPlace(event: BlockPlaceEvent) {
val item = event.itemInHand val item = event.itemInHand
val meta = item.itemMeta ?: return val meta = item.itemMeta ?: return
@ -47,9 +48,9 @@ class ShopListener(private val plugin: ShopPlugin) : Listener {
return return
} }
// Check block against (Chest) // Check block against (Chest/Container)
if (event.blockAgainst.type != Material.CHEST) { if (event.blockAgainst.type !in ShopVisuals.VALID_CONTAINERS) {
// Only valid on top of chests // Only valid on top of supported containers
event.isCancelled = true event.isCancelled = true
return return
} }
@ -64,12 +65,12 @@ class ShopListener(private val plugin: ShopPlugin) : Listener {
// Just ensure color is black initially if needed, but Visuals will overwrite. // Just ensure color is black initially if needed, but Visuals will overwrite.
val frontSide = signState.getSide(Side.FRONT) val frontSide = signState.getSide(Side.FRONT)
frontSide.color = DyeColor.BLACK frontSide.color = DyeColor.BLACK
frontSide.isGlowingText = false frontSide.isGlowingText = true
// Back Text // Back Text
val backSide = signState.getSide(Side.BACK) val backSide = signState.getSide(Side.BACK)
backSide.color = DyeColor.BLACK backSide.color = DyeColor.BLACK
backSide.isGlowingText = false backSide.isGlowingText = true
// Save Tier to Block and Wax (Lock) // Save Tier to Block and Wax (Lock)
signState.persistentDataContainer.set(tierKey, PersistentDataType.INTEGER, tier) signState.persistentDataContainer.set(tierKey, PersistentDataType.INTEGER, tier)
@ -88,15 +89,15 @@ class ShopListener(private val plugin: ShopPlugin) : Listener {
} else { } else {
val actorId = playerEntry.actorId val actorId = playerEntry.actorId
plugin.logger.info("Creating shops for player ${event.player.name} at ${event.blockPlaced.location}")
// Create FRONT shop // Create FRONT shop
val frontShop = ShopRepository.create( val frontShop = ShopRepository.create(
actorId = actorId, actorId = actorId,
location = event.blockPlaced.location, location = event.blockPlaced.location,
face = "FRONT" face = "FRONT"
) )
if (frontShop != null) { plugin.logger.info("Front shop created: ${frontShop != null} (ID: ${frontShop?.id})")
ShopVisuals.updateSign(plugin, event.blockPlaced, frontShop, Side.FRONT)
}
// Create BACK shop // Create BACK shop
val backShop = ShopRepository.create( val backShop = ShopRepository.create(
@ -104,13 +105,35 @@ class ShopListener(private val plugin: ShopPlugin) : Listener {
location = event.blockPlaced.location, location = event.blockPlaced.location,
face = "BACK" face = "BACK"
) )
if (backShop != null) { plugin.logger.info("Back shop created: ${backShop != null} (ID: ${backShop?.id})")
ShopVisuals.updateSign(plugin, event.blockPlaced, backShop, Side.BACK)
} // Close UI and Update Visuals (Delayed to override client-side editor opening)
// Close UI immediately (Next tick)
plugin.server.scheduler.runTask(plugin, Runnable {
event.player.closeInventory()
})
// Update Visuals Delayed to ensure client state is settled
plugin.server.scheduler.runTaskLater(plugin, Runnable {
plugin.logger.info("Executing delayed visual update task.")
if (frontShop != null) {
plugin.logger.info("Updating FRONT sign.")
ShopVisuals.updateSign(plugin, event.blockPlaced, frontShop, Side.FRONT)
} else {
plugin.logger.warning("Front shop was null in delayed task.")
}
if (backShop != null) {
plugin.logger.info("Updating BACK sign.")
ShopVisuals.updateSign(plugin, event.blockPlaced, backShop, Side.BACK)
} else {
plugin.logger.warning("Back shop was null in delayed task.")
} }
// Spawn Displays
spawnDisplays(event.blockPlaced) spawnDisplays(event.blockPlaced)
}, 1L)
}
} }
private fun spawnDisplays(block: org.bukkit.block.Block) { private fun spawnDisplays(block: org.bukkit.block.Block) {
@ -118,7 +141,7 @@ class ShopListener(private val plugin: ShopPlugin) : Listener {
val data = block.blockData as? Rotatable ?: return val data = block.blockData as? Rotatable ?: return
val rotation = data.rotation // BlockFace val rotation = data.rotation // BlockFace
val yaw = yawFromBlockFace(rotation) + 90f val yaw = ShopVisuals.yawFromBlockFace(rotation) + 90f
// Calculate offsets relative to center (0.5, 0, 0.5) // Calculate offsets relative to center (0.5, 0, 0.5)
// Original request assumes 0,0,0 is block origin. // Original request assumes 0,0,0 is block origin.
@ -129,14 +152,6 @@ class ShopListener(private val plugin: ShopPlugin) : Listener {
val backOffset = Vector3f(-0.25f, 0.25f, 0.0f) val backOffset = Vector3f(-0.25f, 0.25f, 0.0f)
// Rotate offsets // Rotate offsets
// yaw is degrees clockwise from South (Z+).
// radians need to be negated for standard CCW rotation logic if we use that,
// or just use rotateY with appropriate sign.
// rotateY in JOML: "Rotates this vector by the given angle in radians around the Y axis"
// Standard geometric Y rotation is CCW.
// Minecraft Yaw 90 (West) should rotate (0,0,1) to (-1,0,0).
// Std Rot 90 CCW: (0,0,1) -> (1,0,0).
// So they are opposite. We should rotate by -yaw.
val rad = Math.toRadians(yaw.toDouble()) val rad = Math.toRadians(yaw.toDouble())
frontOffset.rotateY(-rad.toFloat()) frontOffset.rotateY(-rad.toFloat())
@ -151,7 +166,6 @@ class ShopListener(private val plugin: ShopPlugin) : Listener {
// Spawn Front // Spawn Front
world.spawn(frontPos, ItemDisplay::class.java) { display -> world.spawn(frontPos, ItemDisplay::class.java) { display ->
// display.setItemStack(ItemStack(Material.AIR)) // Default empty
display.setRotation(yaw, 0f) display.setRotation(yaw, 0f)
// Transformation // Transformation
@ -182,28 +196,6 @@ class ShopListener(private val plugin: ShopPlugin) : Listener {
} }
} }
private fun yawFromBlockFace(face: BlockFace): Float {
return when (face) {
BlockFace.SOUTH -> 0f
BlockFace.SOUTH_SOUTH_WEST -> 22.5f
BlockFace.SOUTH_WEST -> 45f
BlockFace.WEST_SOUTH_WEST -> 67.5f
BlockFace.WEST -> 90f
BlockFace.WEST_NORTH_WEST -> 112.5f
BlockFace.NORTH_WEST -> 135f
BlockFace.NORTH_NORTH_WEST -> 157.5f
BlockFace.NORTH -> 180f
BlockFace.NORTH_NORTH_EAST -> 202.5f
BlockFace.NORTH_EAST -> 225f
BlockFace.EAST_NORTH_EAST -> 247.5f
BlockFace.EAST -> 270f
BlockFace.EAST_SOUTH_EAST -> 292.5f
BlockFace.SOUTH_EAST -> 315f
BlockFace.SOUTH_SOUTH_EAST -> 337.5f
else -> 0f
}
}
@EventHandler @EventHandler
fun onSignChange(event: SignChangeEvent) { fun onSignChange(event: SignChangeEvent) {
val player = event.player val player = event.player
@ -230,15 +222,29 @@ class ShopListener(private val plugin: ShopPlugin) : Listener {
player.sendMessage(Component.text("Shop price set to $$price", NamedTextColor.GREEN)) player.sendMessage(Component.text("Shop price set to $$price", NamedTextColor.GREEN))
// Update Actual Sign Visuals // Update Actual Sign Visuals
// Run 1 tick later to override server-side sign handling if needed
plugin.server.scheduler.runTask(plugin, Runnable {
ShopVisuals.updateSign(plugin, event.block, shopData.copy(price = price), event.side) ShopVisuals.updateSign(plugin, event.block, shopData.copy(price = price), event.side)
})
} else { } else {
player.sendMessage(Component.text("Invalid price format. Please enter a valid number.", NamedTextColor.RED)) player.sendMessage(Component.text("Invalid price format. Please enter a valid number.", NamedTextColor.RED))
// Revert lock if invalid? Or keep it unlocked?
// Better to relock to be safe, regardless of valid input or not.
} }
// Clean up Map // Clean up Map
ShopInputMap.pendingInputs.remove(player.uniqueId) ShopInputMap.pendingInputs.remove(player.uniqueId)
event.isCancelled = true event.isCancelled = true // Cancel the actual text update since we handled it virtually
// RELOCK THE SIGN
val signState = event.block.state as? Sign
if (signState != null) {
plugin.server.scheduler.runTask(plugin, Runnable {
signState.isWaxed = true
signState.update()
})
}
} else { } else {
// Normal sign editing protection // Normal sign editing protection
@ -246,6 +252,14 @@ class ShopListener(private val plugin: ShopPlugin) : Listener {
if (state.persistentDataContainer.has(tierKey, PersistentDataType.INTEGER)) { if (state.persistentDataContainer.has(tierKey, PersistentDataType.INTEGER)) {
// If strictly preventing editing of shop signs unless in setup mode // If strictly preventing editing of shop signs unless in setup mode
event.isCancelled = true event.isCancelled = true
val sideStr = if (event.side == Side.FRONT) "FRONT" else "BACK"
val shopData = ShopRepository.findByLocationAndFace(event.block.location, sideStr)
if (shopData != null) {
plugin.server.scheduler.runTask(plugin, Runnable {
ShopVisuals.updateSign(plugin, event.block, shopData, event.side)
})
}
} }
} }
} }
@ -283,15 +297,99 @@ class ShopListener(private val plugin: ShopPlugin) : Listener {
if (shopData.actorId == playerEntry.actorId) { if (shopData.actorId == playerEntry.actorId) {
// Is Owner -> Open Edit GUI // Is Owner -> Open Edit GUI
openEditGui(event.player, shopData) openEditGui(event.player, shopData)
// Also show info to owner
val item = shopData.item
val itemName = if (item != null) {
item.type.name.lowercase().split("_")
.joinToString(" ") { it.replaceFirstChar { c -> c.uppercase() } }
} else { } else {
// Is Customer -> Buy Logic (TODO) "Not Set"
event.player.sendMessage(Component.text("Customer interactions not implemented yet.", NamedTextColor.GRAY)) }
val stock = ShopVisuals.calculateStock(block, item)
val priceStr = net.hareworks.hcu.economy.api.EconomyService.format(shopData.price.toInt())
val amount = item?.amount ?: 0
val ownerName = event.player.name // Use current player name since they are owner
event.player.sendMessage(Component.empty())
event.player.sendMessage(Component.text("Shop Owner: ", NamedTextColor.GRAY).append(Component.text(ownerName, NamedTextColor.WHITE)))
event.player.sendMessage(Component.text("Item: ", NamedTextColor.GRAY).append(Component.text(itemName, NamedTextColor.AQUA)))
event.player.sendMessage(Component.text("Price: ", NamedTextColor.GRAY).append(Component.text("$priceStr for x$amount", NamedTextColor.GREEN)))
event.player.sendMessage(Component.text("Stock: ", NamedTextColor.GRAY).append(Component.text(stock, NamedTextColor.YELLOW)))
event.player.sendMessage(Component.empty())
} else {
// Display Shop Info (Shared Logic)
// We want to show this to everyone, including owner? Or just customers?
// User request implies showing it on click.
// Let's refactor display logic or just copy-paste for safety for now.
val item = shopData.item
val itemName = if (item != null) {
item.type.name.lowercase().split("_")
.joinToString(" ") { it.replaceFirstChar { c -> c.uppercase() } }
} else {
"Not Set"
}
// Calculate Stock
val stock = ShopVisuals.calculateStock(block, item)
val priceStr = net.hareworks.hcu.economy.api.EconomyService.format(shopData.price.toInt())
val amount = item?.amount ?: 0
val ownerName = try {
org.bukkit.Bukkit.getOfflinePlayer(playerEntry.uuid).name ?: playerEntry.uuid.toString()
} catch (e: Exception) {
playerEntry.uuid.toString()
}
event.player.sendMessage(Component.empty())
event.player.sendMessage(Component.text("Shop Owner: ", NamedTextColor.GRAY).append(Component.text(ownerName, NamedTextColor.WHITE)))
event.player.sendMessage(Component.text("Item: ", NamedTextColor.GRAY).append(Component.text(itemName, NamedTextColor.AQUA)))
event.player.sendMessage(Component.text("Price: ", NamedTextColor.GRAY).append(Component.text("$priceStr for x$amount", NamedTextColor.GREEN)))
event.player.sendMessage(Component.text("Stock: ", NamedTextColor.GRAY).append(Component.text(stock, NamedTextColor.YELLOW)))
event.player.sendMessage(Component.empty())
} }
} else { } else {
plugin.logger.warning("Shop data missing for location ${block.location} face $face") plugin.logger.warning("Shop data missing for location ${block.location} face $face")
} }
} }
@EventHandler
fun onContainerInteract(event: PlayerInteractEvent) {
if (event.action != Action.RIGHT_CLICK_BLOCK) return
val block = event.clickedBlock ?: return
if (block.type !in ShopVisuals.VALID_CONTAINERS) return
// Check for Shop Sign above
val signBlock = block.getRelative(BlockFace.UP)
if (signBlock.type != Material.OAK_SIGN) return
val state = signBlock.state as? Sign ?: return
if (!state.persistentDataContainer.has(tierKey, PersistentDataType.INTEGER)) return
// It is a shop chest. Check ownership.
val playerIdService = PlayerIdServiceImpl(plugin.logger)
val playerEntry = playerIdService.find(event.player.uniqueId)
// We need to find at least one shop associated with this sign to determine owner
// Since both front and back shops have same owner, just checking FRONT is enough if it exists, or BACK.
var shopData = ShopRepository.findByLocationAndFace(signBlock.location, "FRONT")
if (shopData == null) {
shopData = ShopRepository.findByLocationAndFace(signBlock.location, "BACK")
}
if (shopData != null && playerEntry != null) {
if (shopData.actorId != playerEntry.actorId) {
// Not owner -> Cancel open
event.isCancelled = true
event.player.sendMessage(Component.text("This chest is locked by the shop.", NamedTextColor.RED))
} else {
// Owner -> Allow open (Default behavior)
}
}
}
private fun openEditGui(player: org.bukkit.entity.Player, shopData: net.hareworks.hcu.shop.database.ShopData) { private fun openEditGui(player: org.bukkit.entity.Player, shopData: net.hareworks.hcu.shop.database.ShopData) {
// 1x5 UI using Hopper // 1x5 UI using Hopper
val inventory = org.bukkit.Bukkit.createInventory(player, InventoryType.HOPPER, Component.text("Shop Setup: ${shopData.face}")) val inventory = org.bukkit.Bukkit.createInventory(player, InventoryType.HOPPER, Component.text("Shop Setup: ${shopData.face}"))
@ -482,9 +580,74 @@ class ShopListener(private val plugin: ShopPlugin) : Listener {
ShopInputMap.pendingInputs[player.uniqueId] = shopId ShopInputMap.pendingInputs[player.uniqueId] = shopId
// Open Editor // Open Editor
// UNLOCK SIGN SO PLAYER CAN EDIT
sign.isWaxed = false
sign.update()
player.openSign(sign, side) player.openSign(sign, side)
} }
@EventHandler(ignoreCancelled = true)
fun onBlockBreak(event: org.bukkit.event.block.BlockBreakEvent) {
val block = event.block
// Handle Container Break Protection
if (block.type in ShopVisuals.VALID_CONTAINERS) {
val signBlock = block.getRelative(BlockFace.UP)
if (signBlock.type == Material.OAK_SIGN) {
val state = signBlock.state as? Sign
if (state != null && state.persistentDataContainer.has(tierKey, PersistentDataType.INTEGER)) {
event.isCancelled = true
event.player.sendMessage(Component.text("Please remove the shop sign first.", NamedTextColor.RED))
}
}
return
}
// Handle Shop Sign Break
if (block.type != Material.OAK_SIGN) return
val state = block.state as? Sign ?: return
if (!state.persistentDataContainer.has(tierKey, PersistentDataType.INTEGER)) return
// It is a shop sign.
val playerIdService = PlayerIdServiceImpl(plugin.logger)
val playerEntry = playerIdService.find(event.player.uniqueId)
// Find Shop Data
val location = block.location
val frontShop = ShopRepository.findByLocationAndFace(location, "FRONT")
val backShop = ShopRepository.findByLocationAndFace(location, "BACK")
// Check ownership (Optional: assume if they can break block they can remove shop)
if (playerEntry != null) {
val ownerId = frontShop?.actorId ?: backShop?.actorId
if (ownerId != null && ownerId != playerEntry.actorId) {
if (!event.player.hasPermission("hcu.shop.admin")) {
event.isCancelled = true
event.player.sendMessage(Component.text("You are not the owner of this shop.", NamedTextColor.RED))
return
}
}
}
// Delete Shops
if (frontShop != null) {
ShopRepository.delete(frontShop.id)
ShopVisuals.removeDisplay(location, "FRONT")
}
if (backShop != null) {
ShopRepository.delete(backShop.id)
ShopVisuals.removeDisplay(location, "BACK")
}
event.player.sendMessage(Component.text("Shop removed.", NamedTextColor.GREEN))
}
private fun updateShopDisplay(shopData: net.hareworks.hcu.shop.database.ShopData) { private fun updateShopDisplay(shopData: net.hareworks.hcu.shop.database.ShopData) {
val world = org.bukkit.Bukkit.getWorld(shopData.worldUid) ?: return val world = org.bukkit.Bukkit.getWorld(shopData.worldUid) ?: return
val block = world.getBlockAt(shopData.x, shopData.y, shopData.z) val block = world.getBlockAt(shopData.x, shopData.y, shopData.z)
@ -493,7 +656,7 @@ class ShopListener(private val plugin: ShopPlugin) : Listener {
val data = block.blockData as? Rotatable ?: return val data = block.blockData as? Rotatable ?: return
val rotation = data.rotation val rotation = data.rotation
val yaw = yawFromBlockFace(rotation) + 90f val yaw = ShopVisuals.yawFromBlockFace(rotation) + 90f
val offset = if (shopData.face == "FRONT") Vector3f(0.25f, 0.25f, 0.0f) else Vector3f(-0.25f, 0.25f, 0.0f) val offset = if (shopData.face == "FRONT") Vector3f(0.25f, 0.25f, 0.0f) else Vector3f(-0.25f, 0.25f, 0.0f)
val rad = Math.toRadians(yaw.toDouble()) val rad = Math.toRadians(yaw.toDouble())