feat: GUI 클릭 소리 및 개선

- 모든 GUI에 bell 클릭 소리 추가 (playNotifySound로 해당 플레이어만 듣도록)
- TeleportGui: 다른 플레이어 없으면 메시지만 표시, 빈 공간 클릭 무시
- EssentialsMenuGui: 아이템 이름 간소화 (스폰/좌표/제작대/텔레포트)
- 설명 텍스트에서 '클릭하여' 제거, 좌표에 '저장된' 추가
This commit is contained in:
Caadiq 2025-12-29 17:27:49 +09:00
parent 791d6f2e9f
commit 4dba2a4803
5 changed files with 319 additions and 265 deletions

View file

@ -4,6 +4,7 @@ import com.beemer.essentials.gui.TeleportGui
import com.beemer.essentials.gui.TeleportGui.Companion.CONTAINER_SIZE
import com.beemer.essentials.nickname.NicknameDataStore
import com.beemer.essentials.util.CommandUtils
import com.beemer.essentials.util.MessageUtils
import com.beemer.essentials.util.TranslationUtils
import com.mojang.authlib.GameProfile
import net.minecraft.ChatFormatting
@ -32,13 +33,22 @@ object TeleportCommand {
CommandUtils.getPlayerOrSendFailure(context.source)
?: return@executes 0
val container = SimpleContainer(9 * 3)
val targetPlayers =
player.server.playerList.players.filter {
it != player
}
// 다른 플레이어가 없으면 메시지만 표시
if (targetPlayers.isEmpty()) {
MessageUtils.sendInfo(
player,
"현재 접속 중인 다른 플레이어가 없습니다."
)
return@executes 0
}
val container = SimpleContainer(9 * 3)
targetPlayers.take(CONTAINER_SIZE).forEachIndexed {
idx,
target ->

View file

@ -5,6 +5,8 @@ import net.minecraft.ChatFormatting
import net.minecraft.core.component.DataComponents
import net.minecraft.network.chat.Component
import net.minecraft.server.level.ServerPlayer
import net.minecraft.sounds.SoundEvents
import net.minecraft.sounds.SoundSource
import net.minecraft.world.Container
import net.minecraft.world.entity.player.Inventory
import net.minecraft.world.inventory.AbstractContainerMenu
@ -77,6 +79,7 @@ class AntimobGui(
) {
if (player is ServerPlayer && slotId in 0 until CONTAINER_SIZE) {
if (glassSlotToMob.containsKey(slotId)) {
playClickSound(player)
val mob = glassSlotToMob[slotId] ?: return
AntimobConfig.toggle(mob)
container.setItem(slotId, makeToggleGlass(mob))
@ -94,6 +97,10 @@ class AntimobGui(
index: Int
): ItemStack = ItemStack.EMPTY
private fun playClickSound(player: ServerPlayer) {
player.playNotifySound(SoundEvents.NOTE_BLOCK_BELL.value(), SoundSource.MASTER, 0.5f, 1.0f)
}
private fun makeMobHead(mob: String): ItemStack {
val headItem = ItemStack(Items.PLAYER_HEAD)

View file

@ -16,6 +16,8 @@ import net.minecraft.network.chat.Component
import net.minecraft.resources.ResourceKey
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.level.ServerPlayer
import net.minecraft.sounds.SoundEvents
import net.minecraft.sounds.SoundSource
import net.minecraft.world.Container
import net.minecraft.world.SimpleContainer
import net.minecraft.world.SimpleMenuProvider
@ -88,6 +90,7 @@ class Menu(
tag.getString("action") == "prev"
) {
if (currentPage > 0) {
playClickSound(player)
openPage(player, currentPage - 1)
}
return
@ -100,6 +103,7 @@ class Menu(
) {
val totalPages = getTotalPages()
if (currentPage < totalPages - 1) {
playClickSound(player)
openPage(player, currentPage + 1)
}
return
@ -118,7 +122,6 @@ class Menu(
val level = dimension?.let { player.server.getLevel(it) }
if (level != null) {
// 이전 위치 저장
val prevPos = player.blockPosition()
PlayerConfig.recordLastLocation(
player,
@ -137,12 +140,17 @@ class Menu(
)
player.teleportTo(level, x, y, z, player.yRot, player.xRot)
playClickSound(player)
MessageUtils.sendSuccess(player, "{$coordName}(으)로 이동했습니다.")
}
player.closeContainer()
}
}
private fun playClickSound(player: ServerPlayer) {
player.playNotifySound(SoundEvents.NOTE_BLOCK_BELL.value(), SoundSource.MASTER, 0.5f, 1.0f)
}
private fun getTotalPages(): Int {
val totalItems = CoordinateConfig.getCoordinates().size
return if (totalItems == 0) 1 else (totalItems + ITEMS_PER_PAGE - 1) / ITEMS_PER_PAGE

View file

@ -5,6 +5,8 @@ import net.minecraft.core.component.DataComponents
import net.minecraft.nbt.CompoundTag
import net.minecraft.network.chat.Component
import net.minecraft.server.level.ServerPlayer
import net.minecraft.sounds.SoundEvents
import net.minecraft.sounds.SoundSource
import net.minecraft.world.Container
import net.minecraft.world.SimpleContainer
import net.minecraft.world.SimpleMenuProvider
@ -29,240 +31,253 @@ class EssentialsMenuGui(
val viewer: ServerPlayer
) : AbstractContainerMenu(MenuType.GENERIC_9x3, syncId) {
companion object {
const val CONTAINER_SIZE = 27
companion object {
const val CONTAINER_SIZE = 27
// 메뉴 아이템 슬롯 위치
const val SLOT_SPAWN = 10 // 스폰
const val SLOT_COORDINATES = 12 // 좌표
const val SLOT_WORKBENCH = 14 // 제작대
const val SLOT_TPA = 16 // TPA
}
init {
// 컨테이너 슬롯 추가
for (i in 0 until CONTAINER_SIZE) {
val x = 8 + (i % 9) * 18
val y = 18 + (i / 9) * 18
addSlot(
object : Slot(container, i, x, y) {
override fun mayPickup(player: Player): Boolean = false
override fun mayPlace(stack: ItemStack): Boolean = false
}
)
// 메뉴 아이템 슬롯 위치
const val SLOT_SPAWN = 10 // 스폰
const val SLOT_COORDINATES = 12 // 좌표
const val SLOT_WORKBENCH = 14 // 제작대
const val SLOT_TPA = 16 // TPA
}
// 플레이어 인벤토리 슬롯
for (row in 0 until 3) {
for (col in 0 until 9) {
val index = col + row * 9 + 9
val x = 8 + col * 18
val y = 86 + row * 18
addSlot(Slot(playerInv, index, x, y))
}
}
for (col in 0 until 9) {
val x = 8 + col * 18
val y = 144
addSlot(Slot(playerInv, col, x, y))
}
}
override fun clicked(slotId: Int, button: Int, clickType: ClickType, player: Player) {
if (slotId !in 0 until CONTAINER_SIZE || player !is ServerPlayer) {
super.clicked(slotId, button, clickType, player)
return
}
val stack = slots[slotId].item
val tag = stack.get(DataComponents.CUSTOM_DATA)?.copyTag() ?: return
val action = tag.getString("action")
when (action) {
"spawn" -> {
player.closeContainer()
// 스폰 명령어 실행
player.server.commands.performPrefixedCommand(
player.createCommandSourceStack(),
"스폰"
)
}
"coordinates" -> {
player.closeContainer()
// 좌표 GUI 열기
player.server.commands.performPrefixedCommand(
player.createCommandSourceStack(),
"좌표"
)
}
"workbench" -> {
player.closeContainer()
// 제작대 열기
openWorkbench(player)
}
"tpa" -> {
player.closeContainer()
// TPA GUI 열기
player.server.commands.performPrefixedCommand(
player.createCommandSourceStack(),
"tpa"
)
}
}
}
private fun openWorkbench(player: ServerPlayer) {
val access =
object : ContainerLevelAccess {
override fun <T : Any> evaluate(
function:
java.util.function.BiFunction<
net.minecraft.world.level.Level,
net.minecraft.core.BlockPos,
T>
): java.util.Optional<T> {
return java.util.Optional.ofNullable(
function.apply(player.level(), player.blockPosition())
init {
// 컨테이너 슬롯 추가
for (i in 0 until CONTAINER_SIZE) {
val x = 8 + (i % 9) * 18
val y = 18 + (i / 9) * 18
addSlot(
object : Slot(container, i, x, y) {
override fun mayPickup(player: Player): Boolean = false
override fun mayPlace(stack: ItemStack): Boolean = false
}
)
}
}
player.openMenu(
SimpleMenuProvider(
{ containerId, inventory, _ ->
object : CraftingMenu(containerId, inventory, access) {
override fun stillValid(player: Player): Boolean = true
}
},
Component.translatable("container.crafting")
)
)
}
// 플레이어 인벤토리 슬롯
for (row in 0 until 3) {
for (col in 0 until 9) {
val index = col + row * 9 + 9
val x = 8 + col * 18
val y = 86 + row * 18
addSlot(Slot(playerInv, index, x, y))
}
}
for (col in 0 until 9) {
val x = 8 + col * 18
val y = 144
addSlot(Slot(playerInv, col, x, y))
}
}
override fun stillValid(player: Player): Boolean = true
override fun quickMoveStack(player: Player, index: Int): ItemStack = ItemStack.EMPTY
override fun clicked(slotId: Int, button: Int, clickType: ClickType, player: Player) {
if (slotId !in 0 until CONTAINER_SIZE || player !is ServerPlayer) {
super.clicked(slotId, button, clickType, player)
return
}
val stack = slots[slotId].item
val tag = stack.get(DataComponents.CUSTOM_DATA)?.copyTag() ?: return
val action = tag.getString("action")
when (action) {
"spawn" -> {
player.closeContainer()
player.server.commands.performPrefixedCommand(
player.createCommandSourceStack(),
"스폰"
)
playClickSound(player)
}
"coordinates" -> {
playClickSound(player)
player.closeContainer()
player.server.commands.performPrefixedCommand(
player.createCommandSourceStack(),
"좌표"
)
}
"workbench" -> {
player.closeContainer()
openWorkbench(player)
playClickSound(player)
}
"tpa" -> {
playClickSound(player)
player.closeContainer()
player.server.commands.performPrefixedCommand(
player.createCommandSourceStack(),
"tpa"
)
}
}
}
private fun playClickSound(player: ServerPlayer) {
player.playNotifySound(
SoundEvents.NOTE_BLOCK_BELL.value(),
SoundSource.MASTER,
0.5f,
1.0f
)
}
private fun openWorkbench(player: ServerPlayer) {
val access =
object : ContainerLevelAccess {
override fun <T : Any> evaluate(
function:
java.util.function.BiFunction<
net.minecraft.world.level.Level,
net.minecraft.core.BlockPos,
T>
): java.util.Optional<T> {
return java.util.Optional.ofNullable(
function.apply(
player.level(),
player.blockPosition()
)
)
}
}
player.openMenu(
SimpleMenuProvider(
{ containerId, inventory, _ ->
object : CraftingMenu(containerId, inventory, access) {
override fun stillValid(player: Player): Boolean =
true
}
},
Component.translatable("container.crafting")
)
)
}
override fun stillValid(player: Player): Boolean = true
override fun quickMoveStack(player: Player, index: Int): ItemStack = ItemStack.EMPTY
}
/** 메뉴 컨테이너 생성 */
fun createEssentialsMenuContainer(): SimpleContainer {
val container = SimpleContainer(27)
val container = SimpleContainer(27)
// 배경을 회색 유리판으로 채우기
val fillerItem = ItemStack(Items.GRAY_STAINED_GLASS_PANE)
fillerItem.set(
DataComponents.CUSTOM_NAME,
Component.literal(" ").withStyle { it.withColor(ChatFormatting.DARK_GRAY) }
)
for (i in 0 until 27) {
container.setItem(i, fillerItem.copy())
}
// 배경을 회색 유리판으로 채우기
val fillerItem = ItemStack(Items.GRAY_STAINED_GLASS_PANE)
fillerItem.set(
DataComponents.CUSTOM_NAME,
Component.literal(" ").withStyle { it.withColor(ChatFormatting.DARK_GRAY) }
)
for (i in 0 until 27) {
container.setItem(i, fillerItem.copy())
}
// 스폰 버튼
val spawnItem = ItemStack(Items.GRASS_BLOCK)
spawnItem.set(
DataComponents.CUSTOM_NAME,
Component.literal("스폰으로 이동").withStyle {
it.withColor(ChatFormatting.GREEN).withBold(true)
}
)
spawnItem.set(
DataComponents.LORE,
ItemLore(
listOf(
Component.literal("클릭하여 스폰으로 이동합니다.").withStyle {
it.withColor(ChatFormatting.GRAY)
}
)
)
)
spawnItem.set(
DataComponents.CUSTOM_DATA,
CustomData.of(CompoundTag().apply { putString("action", "spawn") })
)
container.setItem(EssentialsMenuGui.SLOT_SPAWN, spawnItem)
// 스폰 버튼
val spawnItem = ItemStack(Items.GRASS_BLOCK)
spawnItem.set(
DataComponents.CUSTOM_NAME,
Component.literal("스폰").withStyle {
it.withColor(ChatFormatting.GREEN).withBold(true)
}
)
spawnItem.set(
DataComponents.LORE,
ItemLore(
listOf(
Component.literal("스폰으로 이동합니다.").withStyle {
it.withColor(ChatFormatting.GRAY)
}
)
)
)
spawnItem.set(
DataComponents.CUSTOM_DATA,
CustomData.of(CompoundTag().apply { putString("action", "spawn") })
)
container.setItem(EssentialsMenuGui.SLOT_SPAWN, spawnItem)
// 좌표 버튼
val coordItem = ItemStack(Items.COMPASS)
coordItem.set(
DataComponents.CUSTOM_NAME,
Component.literal("저장된 좌표").withStyle {
it.withColor(ChatFormatting.YELLOW).withBold(true)
}
)
coordItem.set(
DataComponents.LORE,
ItemLore(
listOf(
Component.literal("클릭하여 좌표 목록을 엽니다.").withStyle {
it.withColor(ChatFormatting.GRAY)
}
)
)
)
coordItem.set(
DataComponents.CUSTOM_DATA,
CustomData.of(CompoundTag().apply { putString("action", "coordinates") })
)
container.setItem(EssentialsMenuGui.SLOT_COORDINATES, coordItem)
// 좌표 버튼
val coordItem = ItemStack(Items.COMPASS)
coordItem.set(
DataComponents.CUSTOM_NAME,
Component.literal("좌표").withStyle {
it.withColor(ChatFormatting.YELLOW).withBold(true)
}
)
coordItem.set(
DataComponents.LORE,
ItemLore(
listOf(
Component.literal("저장된 좌표 목록을 엽니다.").withStyle {
it.withColor(ChatFormatting.GRAY)
}
)
)
)
coordItem.set(
DataComponents.CUSTOM_DATA,
CustomData.of(CompoundTag().apply { putString("action", "coordinates") })
)
container.setItem(EssentialsMenuGui.SLOT_COORDINATES, coordItem)
// 제작대 버튼
val workbenchItem = ItemStack(Items.CRAFTING_TABLE)
workbenchItem.set(
DataComponents.CUSTOM_NAME,
Component.literal("제작대 열기").withStyle {
it.withColor(ChatFormatting.GOLD).withBold(true)
}
)
workbenchItem.set(
DataComponents.LORE,
ItemLore(
listOf(
Component.literal("클릭하여 제작대를 엽니다.").withStyle {
it.withColor(ChatFormatting.GRAY)
}
)
)
)
workbenchItem.set(
DataComponents.CUSTOM_DATA,
CustomData.of(CompoundTag().apply { putString("action", "workbench") })
)
container.setItem(EssentialsMenuGui.SLOT_WORKBENCH, workbenchItem)
// 제작대 버튼
val workbenchItem = ItemStack(Items.CRAFTING_TABLE)
workbenchItem.set(
DataComponents.CUSTOM_NAME,
Component.literal("제작대").withStyle {
it.withColor(ChatFormatting.GOLD).withBold(true)
}
)
workbenchItem.set(
DataComponents.LORE,
ItemLore(
listOf(
Component.literal("제작대를 엽니다.").withStyle {
it.withColor(ChatFormatting.GRAY)
}
)
)
)
workbenchItem.set(
DataComponents.CUSTOM_DATA,
CustomData.of(CompoundTag().apply { putString("action", "workbench") })
)
container.setItem(EssentialsMenuGui.SLOT_WORKBENCH, workbenchItem)
// TPA 버튼
val tpaItem = ItemStack(Items.ENDER_PEARL)
tpaItem.set(
DataComponents.CUSTOM_NAME,
Component.literal("플레이어 텔레포트").withStyle {
it.withColor(ChatFormatting.AQUA).withBold(true)
}
)
tpaItem.set(
DataComponents.LORE,
ItemLore(
listOf(
Component.literal("클릭하여 다른 플레이어에게 이동합니다.").withStyle {
it.withColor(ChatFormatting.GRAY)
}
)
)
)
tpaItem.set(
DataComponents.CUSTOM_DATA,
CustomData.of(CompoundTag().apply { putString("action", "tpa") })
)
container.setItem(EssentialsMenuGui.SLOT_TPA, tpaItem)
// TPA 버튼
val tpaItem = ItemStack(Items.ENDER_PEARL)
tpaItem.set(
DataComponents.CUSTOM_NAME,
Component.literal("텔레포트").withStyle {
it.withColor(ChatFormatting.AQUA).withBold(true)
}
)
tpaItem.set(
DataComponents.LORE,
ItemLore(
listOf(
Component.literal("다른 플레이어에게 이동합니다.").withStyle {
it.withColor(ChatFormatting.GRAY)
}
)
)
)
tpaItem.set(
DataComponents.CUSTOM_DATA,
CustomData.of(CompoundTag().apply { putString("action", "tpa") })
)
container.setItem(EssentialsMenuGui.SLOT_TPA, tpaItem)
return container
return container
}
/** 메뉴 GUI 열기 */
fun openEssentialsMenu(player: ServerPlayer) {
val container = createEssentialsMenuContainer()
player.openMenu(
SimpleMenuProvider(
{ windowId, inv, _ -> EssentialsMenuGui(windowId, inv, container, player) },
Component.literal("Essentials 메뉴").withStyle { it.withBold(true) }
)
)
val container = createEssentialsMenuContainer()
player.openMenu(
SimpleMenuProvider(
{ windowId, inv, _ -> EssentialsMenuGui(windowId, inv, container, player) },
Component.literal("Essentials 메뉴").withStyle { it.withBold(true) }
)
)
}

View file

@ -2,10 +2,11 @@ package com.beemer.essentials.gui
import com.beemer.essentials.config.PlayerConfig
import com.beemer.essentials.data.Location
import com.beemer.essentials.data.Player
import com.beemer.essentials.nickname.NicknameDataStore
import com.beemer.essentials.util.MessageUtils
import net.minecraft.server.level.ServerPlayer
import net.minecraft.sounds.SoundEvents
import net.minecraft.sounds.SoundSource
import net.minecraft.world.Container
import net.minecraft.world.entity.player.Inventory
import net.minecraft.world.inventory.AbstractContainerMenu
@ -78,65 +79,78 @@ class TeleportGui(
player: net.minecraft.world.entity.player.Player
) {
if (slotId in 0 until CONTAINER_SIZE && player is ServerPlayer) {
// 해당 슬롯에 플레이어가 있는지 확인
val currentTargetPlayer =
targetPlayers.getOrNull(slotId)?.let {
player.server.playerList.getPlayer(it)
}
if (currentTargetPlayer != null && player.uuid != currentTargetPlayer.uuid
) {
val prevPos = player.blockPosition()
val prevDimension = player.level().dimension().location().toString()
val prevBiome =
player.level()
.getBiome(prevPos)
.unwrapKey()
.map { it.location().toString() }
.orElse("minecraft:plains")
val previousLocation =
Location(
dimension = prevDimension,
biome = prevBiome,
x = prevPos.x.toDouble(),
y = prevPos.y.toDouble(),
z = prevPos.z.toDouble()
)
PlayerConfig.recordLastLocation(player, previousLocation)
val targetLevel = currentTargetPlayer.serverLevel()
player.teleportTo(
targetLevel,
currentTargetPlayer.x,
currentTargetPlayer.y,
currentTargetPlayer.z,
currentTargetPlayer.yRot,
currentTargetPlayer.xRot
)
// 닉네임이 있으면 닉네임 사용
val targetName =
NicknameDataStore.getNickname(currentTargetPlayer.uuid)
?: currentTargetPlayer.gameProfile.name
val playerName =
NicknameDataStore.getNickname(player.uuid)
?: player.gameProfile.name
MessageUtils.sendSuccess(player, "{$targetName}님에게 텔레포트 했습니다.")
MessageUtils.sendInfo(
currentTargetPlayer,
"{$playerName}님이 당신에게 텔레포트 했습니다."
)
} else {
MessageUtils.sendError(player, "해당 플레이어는 현재 오프라인입니다.")
// 플레이어가 없으면 무시 (빈 공간 클릭)
if (currentTargetPlayer == null) {
return
}
val prevPos = player.blockPosition()
val prevDimension = player.level().dimension().location().toString()
val prevBiome =
player.level()
.getBiome(prevPos)
.unwrapKey()
.map { it.location().toString() }
.orElse("minecraft:plains")
val previousLocation =
Location(
dimension = prevDimension,
biome = prevBiome,
x = prevPos.x.toDouble(),
y = prevPos.y.toDouble(),
z = prevPos.z.toDouble()
)
PlayerConfig.recordLastLocation(player, previousLocation)
val targetLevel = currentTargetPlayer.serverLevel()
player.teleportTo(
targetLevel,
currentTargetPlayer.x,
currentTargetPlayer.y,
currentTargetPlayer.z,
currentTargetPlayer.yRot,
currentTargetPlayer.xRot
)
// 소리 재생 (텔레포트 후)
playClickSound(player)
// 닉네임이 있으면 닉네임 사용
val targetName =
NicknameDataStore.getNickname(currentTargetPlayer.uuid)
?: currentTargetPlayer.gameProfile.name
val playerName =
NicknameDataStore.getNickname(player.uuid)
?: player.gameProfile.name
MessageUtils.sendSuccess(player, "{$targetName}님에게 텔레포트 했습니다.")
MessageUtils.sendInfo(
currentTargetPlayer,
"{$playerName}님이 당신에게 텔레포트 했습니다."
)
player.closeContainer()
return
}
super.clicked(slotId, button, clickType, player)
}
private fun playClickSound(player: ServerPlayer) {
player.playNotifySound(
SoundEvents.NOTE_BLOCK_BELL.value(),
SoundSource.MASTER,
0.5f,
1.0f
)
}
override fun stillValid(player: net.minecraft.world.entity.player.Player): Boolean = true
override fun quickMoveStack(