From fd4540c9e7c5a559e606e279a5f2aee91996dd4a Mon Sep 17 00:00:00 2001 From: Caadiq Date: Tue, 30 Dec 2025 19:26:10 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=BB=A4=EC=8A=A4=ED=85=80=20=EC=82=AC?= =?UTF-8?q?=EC=9A=B4=EB=93=9C=20=EC=8B=9C=EC=8A=A4=ED=85=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 클릭 사운드 (custom.click) - GUI 버튼 클릭 시 - 에러 사운드 (custom.error) - 오류 메시지 시 자동 재생 - 알림 사운드 (custom.notification) - 신규 플레이어 환영 시 - 텔레포트 사운드 (custom.teleport) - 스폰, 좌표이동, TPA, back - 메뉴 사운드 (custom.menu) - 메뉴 GUI 열기 시 기타 변경: - 닉네임 변경 시 동일 닉네임 예외처리 추가 - TPA 오류 메시지 빨간색으로 변경 --- .../essentials/command/CoordinateCommand.kt | 29 ++- .../essentials/command/PlayerCommand.kt | 15 ++ .../beemer/essentials/command/SpawnCommand.kt | 18 ++ .../essentials/command/TeleportCommand.kt | 20 +- .../beemer/essentials/event/PlayerEvents.kt | 22 +- .../com/beemer/essentials/gui/AntimobGui.kt | 8 +- .../beemer/essentials/gui/CoordinateGui.kt | 178 ++++++++----- .../essentials/gui/EssentialsMenuGui.kt | 234 +++++++++--------- .../com/beemer/essentials/gui/TeleportGui.kt | 15 +- .../essentials/nickname/NicknameCommand.kt | 7 + .../beemer/essentials/util/MessageUtils.kt | 32 +++ 11 files changed, 384 insertions(+), 194 deletions(-) diff --git a/Essentials/src/main/kotlin/com/beemer/essentials/command/CoordinateCommand.kt b/Essentials/src/main/kotlin/com/beemer/essentials/command/CoordinateCommand.kt index 56a226a..b8d1168 100644 --- a/Essentials/src/main/kotlin/com/beemer/essentials/command/CoordinateCommand.kt +++ b/Essentials/src/main/kotlin/com/beemer/essentials/command/CoordinateCommand.kt @@ -166,12 +166,22 @@ object CoordinateCommand { else (coordinates.size + Menu.ITEMS_PER_PAGE - 1) / Menu.ITEMS_PER_PAGE val container = createPageContainer(player, page) + // 버튼 상태에 따른 이미지 선택 + val buttonImage = + when { + page == 0 && page >= totalPages - 1 -> "\uE010" // 둘 다 비활성 + page == 0 -> "\uE012" // 이전만 비활성 + page >= totalPages - 1 -> "\uE011" // 다음만 비활성 + else -> "\uE013" // 둘 다 활성 + } + + // 커스텀 버튼 이미지만 타이틀에 표시 (타이틀 텍스트는 이미지에 포함) + val customTitle = "\uF808$buttonImage" + player.openMenu( SimpleMenuProvider( { windowId, inv, _ -> Menu(windowId, inv, container, page) }, - Component.literal("저장된 좌표 (${page + 1}/$totalPages)").withStyle { - it.withBold(true) - } + Component.literal(customTitle).withStyle { it.withColor(0xFFFFFF) } ) ) } @@ -208,6 +218,19 @@ object CoordinateCommand { ) player.teleportTo(level, coord.x, coord.y, coord.z, player.yRot, player.xRot) + // 텔레포트 사운드 재생 + val teleportSound = + net.minecraft.sounds.SoundEvent.createVariableRangeEvent( + net.minecraft.resources.ResourceLocation.parse( + "minecraft:custom.teleport" + ) + ) + player.playNotifySound( + teleportSound, + net.minecraft.sounds.SoundSource.MASTER, + 0.2f, + 1.0f + ) MessageUtils.sendSuccess(player, "{$name}(으)로 이동했습니다.") return 1 } diff --git a/Essentials/src/main/kotlin/com/beemer/essentials/command/PlayerCommand.kt b/Essentials/src/main/kotlin/com/beemer/essentials/command/PlayerCommand.kt index 8741cf9..960cdc3 100644 --- a/Essentials/src/main/kotlin/com/beemer/essentials/command/PlayerCommand.kt +++ b/Essentials/src/main/kotlin/com/beemer/essentials/command/PlayerCommand.kt @@ -50,6 +50,21 @@ object PlayerCommand { player.yRot, player.xRot ) + // 텔레포트 사운드 재생 + val teleportSound = + net.minecraft.sounds.SoundEvent + .createVariableRangeEvent( + net.minecraft.resources + .ResourceLocation.parse( + "minecraft:custom.teleport" + ) + ) + player.playNotifySound( + teleportSound, + net.minecraft.sounds.SoundSource.MASTER, + 0.2f, + 1.0f + ) MessageUtils.sendSuccess(player, "이전 위치로 이동했습니다.") 1 } diff --git a/Essentials/src/main/kotlin/com/beemer/essentials/command/SpawnCommand.kt b/Essentials/src/main/kotlin/com/beemer/essentials/command/SpawnCommand.kt index 54cce97..81d3a2e 100644 --- a/Essentials/src/main/kotlin/com/beemer/essentials/command/SpawnCommand.kt +++ b/Essentials/src/main/kotlin/com/beemer/essentials/command/SpawnCommand.kt @@ -105,6 +105,24 @@ object SpawnCommand { player.yRot, player.xRot ) + // 텔레포트 사운드 재생 + val teleportSound = + net.minecraft.sounds.SoundEvent + .createVariableRangeEvent( + net.minecraft + .resources + .ResourceLocation + .parse( + "minecraft:custom.teleport" + ) + ) + player.playNotifySound( + teleportSound, + net.minecraft.sounds.SoundSource + .MASTER, + 0.2f, + 1.0f + ) MessageUtils.sendSuccess( player, "스폰으로 이동했습니다." diff --git a/Essentials/src/main/kotlin/com/beemer/essentials/command/TeleportCommand.kt b/Essentials/src/main/kotlin/com/beemer/essentials/command/TeleportCommand.kt index ce70c81..f417b3d 100644 --- a/Essentials/src/main/kotlin/com/beemer/essentials/command/TeleportCommand.kt +++ b/Essentials/src/main/kotlin/com/beemer/essentials/command/TeleportCommand.kt @@ -38,9 +38,9 @@ object TeleportCommand { it != player } - // 다른 플레이어가 없으면 메시지만 표시 + // 다른 플레이어가 없으면 에러 메시지 표시 if (targetPlayers.isEmpty()) { - MessageUtils.sendInfo( + MessageUtils.sendError( player, "현재 접속 중인 다른 플레이어가 없습니다." ) @@ -56,6 +56,22 @@ object TeleportCommand { container.setItem(idx, head) } + // 플레이어가 있을 때만 클릭 사운드 재생 + val clickSound = + net.minecraft.sounds.SoundEvent + .createVariableRangeEvent( + net.minecraft.resources + .ResourceLocation.parse( + "minecraft:custom.click" + ) + ) + player.playNotifySound( + clickSound, + net.minecraft.sounds.SoundSource.MASTER, + 0.2f, + 1.0f + ) + player.openMenu( SimpleMenuProvider( { windowId, inv, _ -> diff --git a/Essentials/src/main/kotlin/com/beemer/essentials/event/PlayerEvents.kt b/Essentials/src/main/kotlin/com/beemer/essentials/event/PlayerEvents.kt index 93929e6..5b55421 100644 --- a/Essentials/src/main/kotlin/com/beemer/essentials/event/PlayerEvents.kt +++ b/Essentials/src/main/kotlin/com/beemer/essentials/event/PlayerEvents.kt @@ -39,10 +39,30 @@ object PlayerEvents { player.teleportTo(level, spawn.x, spawn.y, spawn.z, player.yRot, player.xRot) // 신규 플레이어 도움말 (약간의 딜레이 후 표시) - player.server.execute { sendWelcomeGuide(player) } + player.server.execute { + sendWelcomeGuide(player) + // 알림 사운드 재생 + playNotificationSound(player) + } } } + /** 알림 사운드 재생 */ + private fun playNotificationSound(player: ServerPlayer) { + val notificationSound = + net.minecraft.sounds.SoundEvent.createVariableRangeEvent( + net.minecraft.resources.ResourceLocation.parse( + "minecraft:custom.notification" + ) + ) + player.playNotifySound( + notificationSound, + net.minecraft.sounds.SoundSource.MASTER, + 0.5f, + 1.0f + ) + } + /** 신규 플레이어 도움말 */ private fun sendWelcomeGuide(player: ServerPlayer) { val separator = diff --git a/Essentials/src/main/kotlin/com/beemer/essentials/gui/AntimobGui.kt b/Essentials/src/main/kotlin/com/beemer/essentials/gui/AntimobGui.kt index 2330703..1219a29 100644 --- a/Essentials/src/main/kotlin/com/beemer/essentials/gui/AntimobGui.kt +++ b/Essentials/src/main/kotlin/com/beemer/essentials/gui/AntimobGui.kt @@ -5,7 +5,6 @@ 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 @@ -98,7 +97,12 @@ class AntimobGui( ): ItemStack = ItemStack.EMPTY private fun playClickSound(player: ServerPlayer) { - player.playNotifySound(SoundEvents.NOTE_BLOCK_BELL.value(), SoundSource.MASTER, 0.5f, 1.0f) + // 커스텀 클릭 사운드 (리소스팩: custom.click) + val customClick = + net.minecraft.sounds.SoundEvent.createVariableRangeEvent( + net.minecraft.resources.ResourceLocation.parse("minecraft:custom.click") + ) + player.playNotifySound(customClick, SoundSource.MASTER, 0.2f, 1.0f) } private fun makeMobHead(mob: String): ItemStack { diff --git a/Essentials/src/main/kotlin/com/beemer/essentials/gui/CoordinateGui.kt b/Essentials/src/main/kotlin/com/beemer/essentials/gui/CoordinateGui.kt index 6cd1a62..129d856 100644 --- a/Essentials/src/main/kotlin/com/beemer/essentials/gui/CoordinateGui.kt +++ b/Essentials/src/main/kotlin/com/beemer/essentials/gui/CoordinateGui.kt @@ -16,7 +16,6 @@ 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 @@ -32,7 +31,7 @@ import net.minecraft.world.item.Items import net.minecraft.world.item.component.CustomData import net.minecraft.world.item.component.ItemLore -/** 좌표 GUI - 페이지 기능 포함 5줄(45개) 표시 + 6번째 줄에 이전/다음 버튼 */ +/** 좌표 GUI - 페이지 기능 포함 4줄(36개) 표시 + 6번째 줄에 이전/다음 버튼 */ class Menu( syncId: Int, playerInv: Inventory, @@ -41,9 +40,9 @@ class Menu( ) : AbstractContainerMenu(MenuType.GENERIC_9x6, syncId) { companion object { - const val ITEMS_PER_PAGE = 45 // 5줄 * 9 - const val PREV_BUTTON_SLOT = 45 // 6번째 줄 첫 번째 - const val NEXT_BUTTON_SLOT = 53 // 6번째 줄 마지막 + const val ITEMS_PER_PAGE = 36 // 4줄 * 9 + val PREV_BUTTON_SLOTS = listOf(45, 46) // 6번째 줄 이전 버튼 2칸 + val NEXT_BUTTON_SLOTS = listOf(52, 53) // 6번째 줄 다음 버튼 2칸 } init { @@ -84,8 +83,8 @@ class Menu( val stack = slots[slotId].item val tag = stack.get(DataComponents.CUSTOM_DATA)?.copyTag() ?: return - // 이전 페이지 버튼 - if (slotId == PREV_BUTTON_SLOT && + // 이전 페이지 버튼 (슬롯 45, 46) + if (slotId in PREV_BUTTON_SLOTS && tag.contains("action") && tag.getString("action") == "prev" ) { @@ -96,8 +95,8 @@ class Menu( return } - // 다음 페이지 버튼 - if (slotId == NEXT_BUTTON_SLOT && + // 다음 페이지 버튼 (슬롯 52, 53) + if (slotId in NEXT_BUTTON_SLOTS && tag.contains("action") && tag.getString("action") == "next" ) { @@ -140,7 +139,14 @@ class Menu( ) player.teleportTo(level, x, y, z, player.yRot, player.xRot) - playClickSound(player) + // 텔레포트 사운드 재생 + val teleportSound = + net.minecraft.sounds.SoundEvent.createVariableRangeEvent( + net.minecraft.resources.ResourceLocation.parse( + "minecraft:custom.teleport" + ) + ) + player.playNotifySound(teleportSound, SoundSource.MASTER, 0.2f, 1.0f) MessageUtils.sendSuccess(player, "{$coordName}(으)로 이동했습니다.") } player.closeContainer() @@ -148,7 +154,12 @@ class Menu( } private fun playClickSound(player: ServerPlayer) { - player.playNotifySound(SoundEvents.NOTE_BLOCK_BELL.value(), SoundSource.MASTER, 0.5f, 1.0f) + // 커스텀 클릭 사운드 (리소스팩: custom.click) + val customClick = + net.minecraft.sounds.SoundEvent.createVariableRangeEvent( + net.minecraft.resources.ResourceLocation.parse("minecraft:custom.click") + ) + player.playNotifySound(customClick, SoundSource.MASTER, 0.2f, 1.0f) } private fun getTotalPages(): Int { @@ -158,12 +169,24 @@ class Menu( private fun openPage(player: ServerPlayer, page: Int) { val container = createPageContainer(player, page) + val totalPages = getTotalPages() + + // 버튼 상태에 따른 이미지 선택 + val buttonImage = + when { + page == 0 && page >= totalPages - 1 -> "\uE010" // 둘 다 비활성 + page == 0 -> "\uE012" // 이전만 비활성 + page >= totalPages - 1 -> "\uE011" // 다음만 비활성 + else -> "\uE013" // 둘 다 활성 + } + + // 커스텀 버튼 이미지만 타이틀에 표시 (타이틀 텍스트는 이미지에 포함) + val customTitle = "\uF808$buttonImage" + player.openMenu( SimpleMenuProvider( { windowId, inv, _ -> Menu(windowId, inv, container, page) }, - Component.literal("저장된 좌표 (${page + 1}/${getTotalPages()})").withStyle { - it.withBold(true) - } + Component.literal(customTitle).withStyle { it.withColor(0xFFFFFF) } ) ) } @@ -182,10 +205,10 @@ fun createPageContainer(player: ServerPlayer, page: Int): SimpleContainer { if (coordinates.isEmpty()) 1 else (coordinates.size + Menu.ITEMS_PER_PAGE - 1) / Menu.ITEMS_PER_PAGE - // 좌표 아이템 추가 (5줄, 45개) + // 좌표 아이템 추가 (2~5번째 줄, 슬롯 9-44) for (i in startIdx until endIdx) { val coord = coordinates[i] - val item = ItemStack(Items.PAPER) + val item = ItemStack(Items.FILLED_MAP) val tag = CompoundTag().apply { putString("name", coord.name) @@ -209,96 +232,131 @@ fun createPageContainer(player: ServerPlayer, page: Int): SimpleContainer { val displayName = if (creatorName != null) { Component.literal(coord.name) - .withStyle { it.withColor(ChatFormatting.GOLD).withBold(true) } + .withStyle { + it.withColor(ChatFormatting.GOLD).withBold(false).withItalic(false) + } .append( Component.literal(" ($creatorName)").withStyle { - it.withColor(ChatFormatting.AQUA).withBold(false) + it.withColor(ChatFormatting.AQUA) + .withBold(false) + .withItalic(false) } ) } else { Component.literal(coord.name).withStyle { - it.withColor(ChatFormatting.GOLD).withBold(true) + it.withColor(ChatFormatting.GOLD).withBold(false).withItalic(false) } } val loreList: List = listOf( Component.literal("디멘션: ") - .withStyle { it.withColor(ChatFormatting.DARK_GREEN) } + .withStyle { + it.withColor(ChatFormatting.DARK_GREEN).withItalic(false) + } .append( Component.literal(translateDimension(coord.dimension)) - .withStyle { it.withColor(ChatFormatting.GRAY) } + .withStyle { + it.withColor(ChatFormatting.GRAY) + .withItalic(false) + } ), Component.literal("바이옴: ") - .withStyle { it.withColor(ChatFormatting.DARK_GREEN) } + .withStyle { + it.withColor(ChatFormatting.DARK_GREEN).withItalic(false) + } .append( Component.literal(translateBiome(coord.biome)).withStyle { - it.withColor(ChatFormatting.GRAY) + it.withColor(ChatFormatting.GRAY).withItalic(false) } ), Component.literal("좌표: ") - .withStyle { it.withColor(ChatFormatting.DARK_GREEN) } + .withStyle { + it.withColor(ChatFormatting.DARK_GREEN).withItalic(false) + } .append( Component.literal( "${coord.x.toInt()}, ${coord.y.toInt()}, ${coord.z.toInt()}" ) - .withStyle { it.withColor(ChatFormatting.GRAY) } + .withStyle { + it.withColor(ChatFormatting.GRAY) + .withItalic(false) + } ) ) item.set(DataComponents.CUSTOM_NAME, displayName) item.set(DataComponents.LORE, ItemLore(loreList)) item.set(DataComponents.CUSTOM_DATA, CustomData.of(tag)) - container.setItem(i - startIdx, item) + item.set( + DataComponents.CUSTOM_MODEL_DATA, + net.minecraft.world.item.component.CustomModelData(1) + ) + container.setItem(9 + (i - startIdx), item) // 슬롯 9부터 시작 (1줄 건너뛰기) } - // 맨 아래줄 빈 공간을 회색 유리판으로 채우기 (슬롯 46-52) - val fillerItem = ItemStack(Items.GRAY_STAINED_GLASS_PANE) + // 1번째 줄 (슬롯 0-8) - 타이틀 영역 투명 처리 + val fillerItem = ItemStack(Items.GLASS_PANE) + fillerItem.set(DataComponents.CUSTOM_NAME, Component.empty()) + fillerItem.set(DataComponents.LORE, ItemLore(listOf())) fillerItem.set( - DataComponents.CUSTOM_NAME, - Component.literal(" ").withStyle { it.withColor(ChatFormatting.DARK_GRAY) } + DataComponents.CUSTOM_MODEL_DATA, + net.minecraft.world.item.component.CustomModelData(1) ) - for (slot in 46..52) { + fillerItem.set(DataComponents.HIDE_TOOLTIP, net.minecraft.util.Unit.INSTANCE) + for (slot in 0..8) { container.setItem(slot, fillerItem.copy()) } - // 이전 페이지 버튼 (슬롯 45) - if (page > 0) { - val prevItem = ItemStack(Items.LIME_STAINED_GLASS_PANE) - val prevTag = CompoundTag().apply { putString("action", "prev") } - prevItem.set( - DataComponents.CUSTOM_NAME, - Component.literal("이전 페이지").withStyle { it.withColor(ChatFormatting.YELLOW) } - ) - prevItem.set(DataComponents.CUSTOM_DATA, CustomData.of(prevTag)) - container.setItem(Menu.PREV_BUTTON_SLOT, prevItem) - } else { - val disabledItem = ItemStack(Items.RED_STAINED_GLASS_PANE) - disabledItem.set( - DataComponents.CUSTOM_NAME, - Component.literal("이전 페이지").withStyle { it.withColor(ChatFormatting.YELLOW) } - ) - container.setItem(Menu.PREV_BUTTON_SLOT, disabledItem) + // 6번째 줄 가운데 슬롯 (47-51) - 투명 처리 + for (slot in 47..51) { + container.setItem(slot, fillerItem.copy()) } - // 다음 페이지 버튼 (슬롯 53) + // 이전 페이지 버튼 (슬롯 45, 46) - 페이지 정보 툴팁 + val prevItem = ItemStack(Items.GLASS_PANE) + val prevTag = CompoundTag().apply { putString("action", if (page > 0) "prev" else "") } + if (page > 0) { + prevItem.set( + DataComponents.CUSTOM_NAME, + Component.literal("이전 페이지 [${page + 1}/$totalPages]").withStyle { + it.withColor(0xA8D8D8).withItalic(false) + } + ) + } else { + prevItem.set(DataComponents.CUSTOM_NAME, Component.empty()) + prevItem.set(DataComponents.HIDE_TOOLTIP, net.minecraft.util.Unit.INSTANCE) + } + prevItem.set(DataComponents.LORE, ItemLore(listOf())) + prevItem.set(DataComponents.CUSTOM_DATA, CustomData.of(prevTag)) + prevItem.set( + DataComponents.CUSTOM_MODEL_DATA, + net.minecraft.world.item.component.CustomModelData(1) + ) + Menu.PREV_BUTTON_SLOTS.forEach { container.setItem(it, prevItem.copy()) } + + // 다음 페이지 버튼 (슬롯 52, 53) - 페이지 정보 툴팁 + val nextItem = ItemStack(Items.GLASS_PANE) + val nextTag = + CompoundTag().apply { putString("action", if (page < totalPages - 1) "next" else "") } if (page < totalPages - 1) { - val nextItem = ItemStack(Items.LIME_STAINED_GLASS_PANE) - val nextTag = CompoundTag().apply { putString("action", "next") } nextItem.set( DataComponents.CUSTOM_NAME, - Component.literal("다음 페이지").withStyle { it.withColor(ChatFormatting.YELLOW) } + Component.literal("다음 페이지 [${page + 1}/$totalPages]").withStyle { + it.withColor(0xA8D8D8).withItalic(false) + } ) - nextItem.set(DataComponents.CUSTOM_DATA, CustomData.of(nextTag)) - container.setItem(Menu.NEXT_BUTTON_SLOT, nextItem) } else { - val disabledItem = ItemStack(Items.RED_STAINED_GLASS_PANE) - disabledItem.set( - DataComponents.CUSTOM_NAME, - Component.literal("다음 페이지").withStyle { it.withColor(ChatFormatting.YELLOW) } - ) - container.setItem(Menu.NEXT_BUTTON_SLOT, disabledItem) + nextItem.set(DataComponents.CUSTOM_NAME, Component.empty()) + nextItem.set(DataComponents.HIDE_TOOLTIP, net.minecraft.util.Unit.INSTANCE) } + nextItem.set(DataComponents.LORE, ItemLore(listOf())) + nextItem.set(DataComponents.CUSTOM_DATA, CustomData.of(nextTag)) + nextItem.set( + DataComponents.CUSTOM_MODEL_DATA, + net.minecraft.world.item.component.CustomModelData(1) + ) + Menu.NEXT_BUTTON_SLOTS.forEach { container.setItem(it, nextItem.copy()) } return container } diff --git a/Essentials/src/main/kotlin/com/beemer/essentials/gui/EssentialsMenuGui.kt b/Essentials/src/main/kotlin/com/beemer/essentials/gui/EssentialsMenuGui.kt index a9475dd..544bf66 100644 --- a/Essentials/src/main/kotlin/com/beemer/essentials/gui/EssentialsMenuGui.kt +++ b/Essentials/src/main/kotlin/com/beemer/essentials/gui/EssentialsMenuGui.kt @@ -5,7 +5,6 @@ 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 @@ -29,16 +28,16 @@ class EssentialsMenuGui( playerInv: Inventory, val container: Container, val viewer: ServerPlayer -) : AbstractContainerMenu(MenuType.GENERIC_9x3, syncId) { +) : AbstractContainerMenu(MenuType.GENERIC_9x6, syncId) { companion object { - const val CONTAINER_SIZE = 27 + const val CONTAINER_SIZE = 54 // 9x6 = 54 - // 메뉴 아이템 슬롯 위치 - const val SLOT_SPAWN = 10 // 스폰 - const val SLOT_COORDINATES = 12 // 좌표 - const val SLOT_WORKBENCH = 14 // 제작대 - const val SLOT_TPA = 16 // TPA + // 메뉴 아이템 슬롯 위치 (9x6 기준) + const val SLOT_SPAWN = 19 // 스폰 + const val SLOT_COORDINATES = 21 // 좌표 + const val SLOT_WORKBENCH = 23 // 제작대 + const val SLOT_TPA = 25 // TPA } init { @@ -71,7 +70,41 @@ class EssentialsMenuGui( } override fun clicked(slotId: Int, button: Int, clickType: ClickType, player: Player) { - if (slotId !in 0 until CONTAINER_SIZE || player !is ServerPlayer) { + // 허용된 버튼 슬롯 목록 + val buttonSlots = + setOf( + // 스폰 + 18, + 19, + 20, + 27, + 28, + 29, + // 제작대 (순서 변경) + 21, + 22, + 23, + 30, + 31, + 32, + // 좌표 (순서 변경) + 24, + 25, + 26, + 33, + 34, + 35, + // TPA + 36, + 37, + 38, + 45, + 46, + 47 + ) + + // 버튼 슬롯이 아니면 기본 처리 + if (slotId !in buttonSlots || player !is ServerPlayer) { super.clicked(slotId, button, clickType, player) return } @@ -79,6 +112,7 @@ class EssentialsMenuGui( val stack = slots[slotId].item val tag = stack.get(DataComponents.CUSTOM_DATA)?.copyTag() ?: return val action = tag.getString("action") + if (action.isEmpty()) return when (action) { "spawn" -> { @@ -87,7 +121,7 @@ class EssentialsMenuGui( player.createCommandSourceStack(), "스폰" ) - playClickSound(player) + // 스폰 명령어에서 이미 사운드 재생하므로 여기서는 제거 } "coordinates" -> { playClickSound(player) @@ -103,7 +137,7 @@ class EssentialsMenuGui( playClickSound(player) } "tpa" -> { - playClickSound(player) + // TPA 명령어에서 사운드 재생하므로 여기서는 제거 player.closeContainer() player.server.commands.performPrefixedCommand( player.createCommandSourceStack(), @@ -114,12 +148,14 @@ class EssentialsMenuGui( } private fun playClickSound(player: ServerPlayer) { - player.playNotifySound( - SoundEvents.NOTE_BLOCK_BELL.value(), - SoundSource.MASTER, - 0.5f, - 1.0f - ) + // 커스텀 클릭 사운드 (리소스팩: custom.click) + val customClick = + net.minecraft.sounds.SoundEvent.createVariableRangeEvent( + net.minecraft.resources.ResourceLocation.parse( + "minecraft:custom.click" + ) + ) + player.playNotifySound(customClick, SoundSource.MASTER, 0.2f, 1.0f) } private fun openWorkbench(player: ServerPlayer) { @@ -160,113 +196,60 @@ class EssentialsMenuGui( /** 메뉴 컨테이너 생성 */ fun createEssentialsMenuContainer(): SimpleContainer { - val container = SimpleContainer(27) + val container = SimpleContainer(54) // 9x6 = 54 - // 배경을 회색 유리판으로 채우기 - 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()) + // 버튼 생성 헬퍼 함수 (툴팁 포함) + fun createButton(action: String, name: String, description: String): ItemStack { + val item = ItemStack(Items.GLASS_PANE) + // 이름 설정 (하늘색) + item.set( + DataComponents.CUSTOM_NAME, + Component.literal(name).withStyle { + it.withColor(0xA8D8D8).withItalic(false) + } + ) + // 설명 설정 (회색) + item.set( + DataComponents.LORE, + ItemLore( + listOf( + Component.literal(description).withStyle { + it.withColor(ChatFormatting.GRAY).withItalic(false) + } + ) + ) + ) + item.set( + DataComponents.CUSTOM_DATA, + CustomData.of(CompoundTag().apply { putString("action", action) }) + ) + // CustomModelData를 1로 설정하여 텍스처 제거 + item.set( + DataComponents.CUSTOM_MODEL_DATA, + net.minecraft.world.item.component.CustomModelData(1) + ) + return item } - // 스폰 버튼 - 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) + // 스폰 버튼 (3x2 영역: 18,19,20,27,28,29) + val spawnSlots = listOf(18, 19, 20, 27, 28, 29) + val spawnItem = createButton("spawn", "스폰", "스폰 지점으로 이동합니다") + spawnSlots.forEach { container.setItem(it, spawnItem.copy()) } - // 좌표 버튼 - 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) + // 제작대 버튼 (3x2 영역: 21,22,23,30,31,32) + val workbenchSlots = listOf(21, 22, 23, 30, 31, 32) + val workbenchItem = createButton("workbench", "제작대", "제작대를 엽니다") + workbenchSlots.forEach { container.setItem(it, workbenchItem.copy()) } - // 제작대 버튼 - 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) + // 좌표 버튼 (3x2 영역: 24,25,26,33,34,35) + val coordSlots = listOf(24, 25, 26, 33, 34, 35) + val coordItem = createButton("coordinates", "좌표", "저장된 좌표 목록을 엽니다") + coordSlots.forEach { container.setItem(it, coordItem.copy()) } - // 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 버튼 (3x2 영역: 36,37,38,45,46,47) + val tpaSlots = listOf(36, 37, 38, 45, 46, 47) + val tpaItem = createButton("tpa", "텔레포트", "다른 플레이어에게 이동합니다") + tpaSlots.forEach { container.setItem(it, tpaItem.copy()) } return container } @@ -274,10 +257,23 @@ fun createEssentialsMenuContainer(): SimpleContainer { /** 메뉴 GUI 열기 */ fun openEssentialsMenu(player: ServerPlayer) { val container = createEssentialsMenuContainer() + + // 메뉴 오픈 사운드 재생 + val menuSound = + net.minecraft.sounds.SoundEvent.createVariableRangeEvent( + net.minecraft.resources.ResourceLocation.parse("minecraft:custom.menu") + ) + player.playNotifySound(menuSound, net.minecraft.sounds.SoundSource.MASTER, 0.2f, 1.0f) + + // 커스텀 GUI 이미지를 위한 유니코드 문자 + // \uF808 = -8 (왼쪽으로 8px 이동) + // \uE000 = 메뉴 GUI 이미지 + val customGuiPrefix = "\uF808\uE000" + player.openMenu( SimpleMenuProvider( { windowId, inv, _ -> EssentialsMenuGui(windowId, inv, container, player) }, - Component.literal("Essentials 메뉴").withStyle { it.withBold(true) } + Component.literal(customGuiPrefix).withStyle { it.withColor(0xFFFFFF) } ) ) } diff --git a/Essentials/src/main/kotlin/com/beemer/essentials/gui/TeleportGui.kt b/Essentials/src/main/kotlin/com/beemer/essentials/gui/TeleportGui.kt index 0dfc1bb..00517f0 100644 --- a/Essentials/src/main/kotlin/com/beemer/essentials/gui/TeleportGui.kt +++ b/Essentials/src/main/kotlin/com/beemer/essentials/gui/TeleportGui.kt @@ -5,7 +5,6 @@ import com.beemer.essentials.data.Location 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 @@ -143,12 +142,14 @@ class TeleportGui( } private fun playClickSound(player: ServerPlayer) { - player.playNotifySound( - SoundEvents.NOTE_BLOCK_BELL.value(), - SoundSource.MASTER, - 0.5f, - 1.0f - ) + // 커스텀 텔레포트 사운드 (리소스팩: custom.teleport) + val teleportSound = + net.minecraft.sounds.SoundEvent.createVariableRangeEvent( + net.minecraft.resources.ResourceLocation.parse( + "minecraft:custom.teleport" + ) + ) + player.playNotifySound(teleportSound, SoundSource.MASTER, 0.2f, 1.0f) } override fun stillValid(player: net.minecraft.world.entity.player.Player): Boolean = true diff --git a/Essentials/src/main/kotlin/com/beemer/essentials/nickname/NicknameCommand.kt b/Essentials/src/main/kotlin/com/beemer/essentials/nickname/NicknameCommand.kt index 5186af2..521957d 100644 --- a/Essentials/src/main/kotlin/com/beemer/essentials/nickname/NicknameCommand.kt +++ b/Essentials/src/main/kotlin/com/beemer/essentials/nickname/NicknameCommand.kt @@ -95,6 +95,13 @@ object NicknameCommand { return 0 } + // 유효성 검사: 현재 닉네임과 동일 + val currentNickname = NicknameDataStore.getNickname(player.uuid) + if (currentNickname == nickname) { + MessageUtils.sendError(player, "현재 사용 중인 닉네임과 동일합니다.") + return 0 + } + // 유효성 검사: 중복 if (NicknameDataStore.isNicknameTaken(nickname, player.uuid)) { MessageUtils.sendError(player, "이미 사용 중인 닉네임입니다.") diff --git a/Essentials/src/main/kotlin/com/beemer/essentials/util/MessageUtils.kt b/Essentials/src/main/kotlin/com/beemer/essentials/util/MessageUtils.kt index 0b06692..e169a6f 100644 --- a/Essentials/src/main/kotlin/com/beemer/essentials/util/MessageUtils.kt +++ b/Essentials/src/main/kotlin/com/beemer/essentials/util/MessageUtils.kt @@ -55,12 +55,44 @@ object MessageUtils { fun sendError(player: ServerPlayer, text: String) { player.sendSystemMessage(error(text)) + playErrorSound(player) } fun sendWarning(player: ServerPlayer, text: String) { player.sendSystemMessage(warning(text)) } + /** 알림 메시지 (사운드 포함) - 서버 접속 시 등 */ + fun sendNotification(player: ServerPlayer, text: String) { + player.sendSystemMessage(info(text)) + playNotificationSound(player) + } + + // === 사운드 재생 === + + private fun playErrorSound(player: ServerPlayer) { + val errorSound = + net.minecraft.sounds.SoundEvent.createVariableRangeEvent( + net.minecraft.resources.ResourceLocation.parse("minecraft:custom.error") + ) + player.playNotifySound(errorSound, net.minecraft.sounds.SoundSource.MASTER, 0.2f, 1.0f) + } + + private fun playNotificationSound(player: ServerPlayer) { + val notificationSound = + net.minecraft.sounds.SoundEvent.createVariableRangeEvent( + net.minecraft.resources.ResourceLocation.parse( + "minecraft:custom.notification" + ) + ) + player.playNotifySound( + notificationSound, + net.minecraft.sounds.SoundSource.MASTER, + 0.5f, + 1.0f + ) + } + /** 스타일 텍스트 파싱 {중괄호} 안의 텍스트는 강조 색상으로 표시 */ private fun parseStyledText(text: String, baseColor: Int, accentColor: Int): MutableComponent { val result: MutableComponent = Component.empty()