feat: 커스텀 사운드 시스템 추가

- 클릭 사운드 (custom.click) - GUI 버튼 클릭 시
- 에러 사운드 (custom.error) - 오류 메시지 시 자동 재생
- 알림 사운드 (custom.notification) - 신규 플레이어 환영 시
- 텔레포트 사운드 (custom.teleport) - 스폰, 좌표이동, TPA, back
- 메뉴 사운드 (custom.menu) - 메뉴 GUI 열기 시

기타 변경:
- 닉네임 변경 시 동일 닉네임 예외처리 추가
- TPA 오류 메시지 빨간색으로 변경
This commit is contained in:
Caadiq 2025-12-30 19:26:10 +09:00
parent 4dba2a4803
commit fd4540c9e7
11 changed files with 384 additions and 194 deletions

View file

@ -166,12 +166,22 @@ object CoordinateCommand {
else (coordinates.size + Menu.ITEMS_PER_PAGE - 1) / Menu.ITEMS_PER_PAGE else (coordinates.size + Menu.ITEMS_PER_PAGE - 1) / Menu.ITEMS_PER_PAGE
val container = createPageContainer(player, 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( player.openMenu(
SimpleMenuProvider( SimpleMenuProvider(
{ windowId, inv, _ -> Menu(windowId, inv, container, page) }, { windowId, inv, _ -> Menu(windowId, inv, container, page) },
Component.literal("저장된 좌표 (${page + 1}/$totalPages)").withStyle { Component.literal(customTitle).withStyle { it.withColor(0xFFFFFF) }
it.withBold(true)
}
) )
) )
} }
@ -208,6 +218,19 @@ object CoordinateCommand {
) )
player.teleportTo(level, coord.x, coord.y, coord.z, player.yRot, player.xRot) 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}(으)로 이동했습니다.") MessageUtils.sendSuccess(player, "{$name}(으)로 이동했습니다.")
return 1 return 1
} }

View file

@ -50,6 +50,21 @@ object PlayerCommand {
player.yRot, player.yRot,
player.xRot 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, "이전 위치로 이동했습니다.") MessageUtils.sendSuccess(player, "이전 위치로 이동했습니다.")
1 1
} }

View file

@ -105,6 +105,24 @@ object SpawnCommand {
player.yRot, player.yRot,
player.xRot 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( MessageUtils.sendSuccess(
player, player,
"스폰으로 이동했습니다." "스폰으로 이동했습니다."

View file

@ -38,9 +38,9 @@ object TeleportCommand {
it != player it != player
} }
// 다른 플레이어가 없으면 메시지 표시 // 다른 플레이어가 없으면 에러 메시지 표시
if (targetPlayers.isEmpty()) { if (targetPlayers.isEmpty()) {
MessageUtils.sendInfo( MessageUtils.sendError(
player, player,
"현재 접속 중인 다른 플레이어가 없습니다." "현재 접속 중인 다른 플레이어가 없습니다."
) )
@ -56,6 +56,22 @@ object TeleportCommand {
container.setItem(idx, head) 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( player.openMenu(
SimpleMenuProvider( SimpleMenuProvider(
{ windowId, inv, _ -> { windowId, inv, _ ->

View file

@ -39,9 +39,29 @@ object PlayerEvents {
player.teleportTo(level, spawn.x, spawn.y, spawn.z, player.yRot, player.xRot) 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) { private fun sendWelcomeGuide(player: ServerPlayer) {

View file

@ -5,7 +5,6 @@ import net.minecraft.ChatFormatting
import net.minecraft.core.component.DataComponents import net.minecraft.core.component.DataComponents
import net.minecraft.network.chat.Component import net.minecraft.network.chat.Component
import net.minecraft.server.level.ServerPlayer import net.minecraft.server.level.ServerPlayer
import net.minecraft.sounds.SoundEvents
import net.minecraft.sounds.SoundSource import net.minecraft.sounds.SoundSource
import net.minecraft.world.Container import net.minecraft.world.Container
import net.minecraft.world.entity.player.Inventory import net.minecraft.world.entity.player.Inventory
@ -98,7 +97,12 @@ class AntimobGui(
): ItemStack = ItemStack.EMPTY ): ItemStack = ItemStack.EMPTY
private fun playClickSound(player: ServerPlayer) { 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 { private fun makeMobHead(mob: String): ItemStack {

View file

@ -16,7 +16,6 @@ import net.minecraft.network.chat.Component
import net.minecraft.resources.ResourceKey import net.minecraft.resources.ResourceKey
import net.minecraft.resources.ResourceLocation import net.minecraft.resources.ResourceLocation
import net.minecraft.server.level.ServerPlayer import net.minecraft.server.level.ServerPlayer
import net.minecraft.sounds.SoundEvents
import net.minecraft.sounds.SoundSource import net.minecraft.sounds.SoundSource
import net.minecraft.world.Container import net.minecraft.world.Container
import net.minecraft.world.SimpleContainer 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.CustomData
import net.minecraft.world.item.component.ItemLore import net.minecraft.world.item.component.ItemLore
/** 좌표 GUI - 페이지 기능 포함 5줄(45개) 표시 + 6번째 줄에 이전/다음 버튼 */ /** 좌표 GUI - 페이지 기능 포함 4줄(36개) 표시 + 6번째 줄에 이전/다음 버튼 */
class Menu( class Menu(
syncId: Int, syncId: Int,
playerInv: Inventory, playerInv: Inventory,
@ -41,9 +40,9 @@ class Menu(
) : AbstractContainerMenu(MenuType.GENERIC_9x6, syncId) { ) : AbstractContainerMenu(MenuType.GENERIC_9x6, syncId) {
companion object { companion object {
const val ITEMS_PER_PAGE = 45 // 5줄 * 9 const val ITEMS_PER_PAGE = 36 // 4줄 * 9
const val PREV_BUTTON_SLOT = 45 // 6번째 줄 첫 번째 val PREV_BUTTON_SLOTS = listOf(45, 46) // 6번째 줄 이전 버튼 2칸
const val NEXT_BUTTON_SLOT = 53 // 6번째 줄 마지막 val NEXT_BUTTON_SLOTS = listOf(52, 53) // 6번째 줄 다음 버튼 2칸
} }
init { init {
@ -84,8 +83,8 @@ class Menu(
val stack = slots[slotId].item val stack = slots[slotId].item
val tag = stack.get(DataComponents.CUSTOM_DATA)?.copyTag() ?: return val tag = stack.get(DataComponents.CUSTOM_DATA)?.copyTag() ?: return
// 이전 페이지 버튼 // 이전 페이지 버튼 (슬롯 45, 46)
if (slotId == PREV_BUTTON_SLOT && if (slotId in PREV_BUTTON_SLOTS &&
tag.contains("action") && tag.contains("action") &&
tag.getString("action") == "prev" tag.getString("action") == "prev"
) { ) {
@ -96,8 +95,8 @@ class Menu(
return return
} }
// 다음 페이지 버튼 // 다음 페이지 버튼 (슬롯 52, 53)
if (slotId == NEXT_BUTTON_SLOT && if (slotId in NEXT_BUTTON_SLOTS &&
tag.contains("action") && tag.contains("action") &&
tag.getString("action") == "next" tag.getString("action") == "next"
) { ) {
@ -140,7 +139,14 @@ class Menu(
) )
player.teleportTo(level, x, y, z, player.yRot, player.xRot) 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}(으)로 이동했습니다.") MessageUtils.sendSuccess(player, "{$coordName}(으)로 이동했습니다.")
} }
player.closeContainer() player.closeContainer()
@ -148,7 +154,12 @@ class Menu(
} }
private fun playClickSound(player: ServerPlayer) { 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 { private fun getTotalPages(): Int {
@ -158,12 +169,24 @@ class Menu(
private fun openPage(player: ServerPlayer, page: Int) { private fun openPage(player: ServerPlayer, page: Int) {
val container = createPageContainer(player, page) 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( player.openMenu(
SimpleMenuProvider( SimpleMenuProvider(
{ windowId, inv, _ -> Menu(windowId, inv, container, page) }, { windowId, inv, _ -> Menu(windowId, inv, container, page) },
Component.literal("저장된 좌표 (${page + 1}/${getTotalPages()})").withStyle { Component.literal(customTitle).withStyle { it.withColor(0xFFFFFF) }
it.withBold(true)
}
) )
) )
} }
@ -182,10 +205,10 @@ fun createPageContainer(player: ServerPlayer, page: Int): SimpleContainer {
if (coordinates.isEmpty()) 1 if (coordinates.isEmpty()) 1
else (coordinates.size + Menu.ITEMS_PER_PAGE - 1) / Menu.ITEMS_PER_PAGE else (coordinates.size + Menu.ITEMS_PER_PAGE - 1) / Menu.ITEMS_PER_PAGE
// 좌표 아이템 추가 (5줄, 45개) // 좌표 아이템 추가 (2~5번째 줄, 슬롯 9-44)
for (i in startIdx until endIdx) { for (i in startIdx until endIdx) {
val coord = coordinates[i] val coord = coordinates[i]
val item = ItemStack(Items.PAPER) val item = ItemStack(Items.FILLED_MAP)
val tag = val tag =
CompoundTag().apply { CompoundTag().apply {
putString("name", coord.name) putString("name", coord.name)
@ -209,96 +232,131 @@ fun createPageContainer(player: ServerPlayer, page: Int): SimpleContainer {
val displayName = val displayName =
if (creatorName != null) { if (creatorName != null) {
Component.literal(coord.name) Component.literal(coord.name)
.withStyle { it.withColor(ChatFormatting.GOLD).withBold(true) } .withStyle {
it.withColor(ChatFormatting.GOLD).withBold(false).withItalic(false)
}
.append( .append(
Component.literal(" ($creatorName)").withStyle { Component.literal(" ($creatorName)").withStyle {
it.withColor(ChatFormatting.AQUA).withBold(false) it.withColor(ChatFormatting.AQUA)
.withBold(false)
.withItalic(false)
} }
) )
} else { } else {
Component.literal(coord.name).withStyle { Component.literal(coord.name).withStyle {
it.withColor(ChatFormatting.GOLD).withBold(true) it.withColor(ChatFormatting.GOLD).withBold(false).withItalic(false)
} }
} }
val loreList: List<Component> = val loreList: List<Component> =
listOf( listOf(
Component.literal("디멘션: ") Component.literal("디멘션: ")
.withStyle { it.withColor(ChatFormatting.DARK_GREEN) } .withStyle {
it.withColor(ChatFormatting.DARK_GREEN).withItalic(false)
}
.append( .append(
Component.literal(translateDimension(coord.dimension)) Component.literal(translateDimension(coord.dimension))
.withStyle { it.withColor(ChatFormatting.GRAY) } .withStyle {
it.withColor(ChatFormatting.GRAY)
.withItalic(false)
}
), ),
Component.literal("바이옴: ") Component.literal("바이옴: ")
.withStyle { it.withColor(ChatFormatting.DARK_GREEN) } .withStyle {
it.withColor(ChatFormatting.DARK_GREEN).withItalic(false)
}
.append( .append(
Component.literal(translateBiome(coord.biome)).withStyle { Component.literal(translateBiome(coord.biome)).withStyle {
it.withColor(ChatFormatting.GRAY) it.withColor(ChatFormatting.GRAY).withItalic(false)
} }
), ),
Component.literal("좌표: ") Component.literal("좌표: ")
.withStyle { it.withColor(ChatFormatting.DARK_GREEN) } .withStyle {
it.withColor(ChatFormatting.DARK_GREEN).withItalic(false)
}
.append( .append(
Component.literal( Component.literal(
"${coord.x.toInt()}, ${coord.y.toInt()}, ${coord.z.toInt()}" "${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.CUSTOM_NAME, displayName)
item.set(DataComponents.LORE, ItemLore(loreList)) item.set(DataComponents.LORE, ItemLore(loreList))
item.set(DataComponents.CUSTOM_DATA, CustomData.of(tag)) 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) // 1번째 줄 (슬롯 0-8) - 타이틀 영역 투명 처리
val fillerItem = ItemStack(Items.GRAY_STAINED_GLASS_PANE) val fillerItem = ItemStack(Items.GLASS_PANE)
fillerItem.set(DataComponents.CUSTOM_NAME, Component.empty())
fillerItem.set(DataComponents.LORE, ItemLore(listOf()))
fillerItem.set( fillerItem.set(
DataComponents.CUSTOM_NAME, DataComponents.CUSTOM_MODEL_DATA,
Component.literal(" ").withStyle { it.withColor(ChatFormatting.DARK_GRAY) } 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()) container.setItem(slot, fillerItem.copy())
} }
// 이전 페이지 버튼 (슬롯 45) // 6번째 줄 가운데 슬롯 (47-51) - 투명 처리
if (page > 0) { for (slot in 47..51) {
val prevItem = ItemStack(Items.LIME_STAINED_GLASS_PANE) container.setItem(slot, fillerItem.copy())
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)
} }
// 다음 페이지 버튼 (슬롯 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) { if (page < totalPages - 1) {
val nextItem = ItemStack(Items.LIME_STAINED_GLASS_PANE)
val nextTag = CompoundTag().apply { putString("action", "next") }
nextItem.set( nextItem.set(
DataComponents.CUSTOM_NAME, 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)
} }
)
} else {
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 return container
} }

View file

@ -5,7 +5,6 @@ import net.minecraft.core.component.DataComponents
import net.minecraft.nbt.CompoundTag import net.minecraft.nbt.CompoundTag
import net.minecraft.network.chat.Component import net.minecraft.network.chat.Component
import net.minecraft.server.level.ServerPlayer import net.minecraft.server.level.ServerPlayer
import net.minecraft.sounds.SoundEvents
import net.minecraft.sounds.SoundSource import net.minecraft.sounds.SoundSource
import net.minecraft.world.Container import net.minecraft.world.Container
import net.minecraft.world.SimpleContainer import net.minecraft.world.SimpleContainer
@ -29,16 +28,16 @@ class EssentialsMenuGui(
playerInv: Inventory, playerInv: Inventory,
val container: Container, val container: Container,
val viewer: ServerPlayer val viewer: ServerPlayer
) : AbstractContainerMenu(MenuType.GENERIC_9x3, syncId) { ) : AbstractContainerMenu(MenuType.GENERIC_9x6, syncId) {
companion object { companion object {
const val CONTAINER_SIZE = 27 const val CONTAINER_SIZE = 54 // 9x6 = 54
// 메뉴 아이템 슬롯 위치 // 메뉴 아이템 슬롯 위치 (9x6 기준)
const val SLOT_SPAWN = 10 // 스폰 const val SLOT_SPAWN = 19 // 스폰
const val SLOT_COORDINATES = 12 // 좌표 const val SLOT_COORDINATES = 21 // 좌표
const val SLOT_WORKBENCH = 14 // 제작대 const val SLOT_WORKBENCH = 23 // 제작대
const val SLOT_TPA = 16 // TPA const val SLOT_TPA = 25 // TPA
} }
init { init {
@ -71,7 +70,41 @@ class EssentialsMenuGui(
} }
override fun clicked(slotId: Int, button: Int, clickType: ClickType, player: Player) { 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) super.clicked(slotId, button, clickType, player)
return return
} }
@ -79,6 +112,7 @@ class EssentialsMenuGui(
val stack = slots[slotId].item val stack = slots[slotId].item
val tag = stack.get(DataComponents.CUSTOM_DATA)?.copyTag() ?: return val tag = stack.get(DataComponents.CUSTOM_DATA)?.copyTag() ?: return
val action = tag.getString("action") val action = tag.getString("action")
if (action.isEmpty()) return
when (action) { when (action) {
"spawn" -> { "spawn" -> {
@ -87,7 +121,7 @@ class EssentialsMenuGui(
player.createCommandSourceStack(), player.createCommandSourceStack(),
"스폰" "스폰"
) )
playClickSound(player) // 스폰 명령어에서 이미 사운드 재생하므로 여기서는 제거
} }
"coordinates" -> { "coordinates" -> {
playClickSound(player) playClickSound(player)
@ -103,7 +137,7 @@ class EssentialsMenuGui(
playClickSound(player) playClickSound(player)
} }
"tpa" -> { "tpa" -> {
playClickSound(player) // TPA 명령어에서 사운드 재생하므로 여기서는 제거
player.closeContainer() player.closeContainer()
player.server.commands.performPrefixedCommand( player.server.commands.performPrefixedCommand(
player.createCommandSourceStack(), player.createCommandSourceStack(),
@ -114,12 +148,14 @@ class EssentialsMenuGui(
} }
private fun playClickSound(player: ServerPlayer) { private fun playClickSound(player: ServerPlayer) {
player.playNotifySound( // 커스텀 클릭 사운드 (리소스팩: custom.click)
SoundEvents.NOTE_BLOCK_BELL.value(), val customClick =
SoundSource.MASTER, net.minecraft.sounds.SoundEvent.createVariableRangeEvent(
0.5f, net.minecraft.resources.ResourceLocation.parse(
1.0f "minecraft:custom.click"
) )
)
player.playNotifySound(customClick, SoundSource.MASTER, 0.2f, 1.0f)
} }
private fun openWorkbench(player: ServerPlayer) { private fun openWorkbench(player: ServerPlayer) {
@ -160,113 +196,60 @@ class EssentialsMenuGui(
/** 메뉴 컨테이너 생성 */ /** 메뉴 컨테이너 생성 */
fun createEssentialsMenuContainer(): SimpleContainer { fun createEssentialsMenuContainer(): SimpleContainer {
val container = SimpleContainer(27) val container = SimpleContainer(54) // 9x6 = 54
// 배경을 회색 유리판으로 채우기 // 버튼 생성 헬퍼 함수 (툴팁 포함)
val fillerItem = ItemStack(Items.GRAY_STAINED_GLASS_PANE) fun createButton(action: String, name: String, description: String): ItemStack {
fillerItem.set( val item = ItemStack(Items.GLASS_PANE)
// 이름 설정 (하늘색)
item.set(
DataComponents.CUSTOM_NAME, DataComponents.CUSTOM_NAME,
Component.literal(" ").withStyle { it.withColor(ChatFormatting.DARK_GRAY) } Component.literal(name).withStyle {
) it.withColor(0xA8D8D8).withItalic(false)
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( // 설명 설정 (회색)
item.set(
DataComponents.LORE, DataComponents.LORE,
ItemLore( ItemLore(
listOf( listOf(
Component.literal("스폰으로 이동합니다.").withStyle { Component.literal(description).withStyle {
it.withColor(ChatFormatting.GRAY) it.withColor(ChatFormatting.GRAY).withItalic(false)
} }
) )
) )
) )
spawnItem.set( item.set(
DataComponents.CUSTOM_DATA, DataComponents.CUSTOM_DATA,
CustomData.of(CompoundTag().apply { putString("action", "spawn") }) CustomData.of(CompoundTag().apply { putString("action", action) })
) )
container.setItem(EssentialsMenuGui.SLOT_SPAWN, spawnItem) // CustomModelData를 1로 설정하여 텍스처 제거
item.set(
DataComponents.CUSTOM_MODEL_DATA,
net.minecraft.world.item.component.CustomModelData(1)
)
return item
}
// 좌표 버튼 // 스폰 버튼 (3x2 영역: 18,19,20,27,28,29)
val coordItem = ItemStack(Items.COMPASS) val spawnSlots = listOf(18, 19, 20, 27, 28, 29)
coordItem.set( val spawnItem = createButton("spawn", "스폰", "스폰 지점으로 이동합니다")
DataComponents.CUSTOM_NAME, spawnSlots.forEach { container.setItem(it, spawnItem.copy()) }
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 workbenchItem = ItemStack(Items.CRAFTING_TABLE) val workbenchSlots = listOf(21, 22, 23, 30, 31, 32)
workbenchItem.set( val workbenchItem = createButton("workbench", "제작대", "제작대를 엽니다")
DataComponents.CUSTOM_NAME, workbenchSlots.forEach { container.setItem(it, workbenchItem.copy()) }
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 버튼 // 좌표 버튼 (3x2 영역: 24,25,26,33,34,35)
val tpaItem = ItemStack(Items.ENDER_PEARL) val coordSlots = listOf(24, 25, 26, 33, 34, 35)
tpaItem.set( val coordItem = createButton("coordinates", "좌표", "저장된 좌표 목록을 엽니다")
DataComponents.CUSTOM_NAME, coordSlots.forEach { container.setItem(it, coordItem.copy()) }
Component.literal("텔레포트").withStyle {
it.withColor(ChatFormatting.AQUA).withBold(true) // TPA 버튼 (3x2 영역: 36,37,38,45,46,47)
} val tpaSlots = listOf(36, 37, 38, 45, 46, 47)
) val tpaItem = createButton("tpa", "텔레포트", "다른 플레이어에게 이동합니다")
tpaItem.set( tpaSlots.forEach { container.setItem(it, tpaItem.copy()) }
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
} }
@ -274,10 +257,23 @@ fun createEssentialsMenuContainer(): SimpleContainer {
/** 메뉴 GUI 열기 */ /** 메뉴 GUI 열기 */
fun openEssentialsMenu(player: ServerPlayer) { fun openEssentialsMenu(player: ServerPlayer) {
val container = createEssentialsMenuContainer() 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( player.openMenu(
SimpleMenuProvider( SimpleMenuProvider(
{ windowId, inv, _ -> EssentialsMenuGui(windowId, inv, container, player) }, { windowId, inv, _ -> EssentialsMenuGui(windowId, inv, container, player) },
Component.literal("Essentials 메뉴").withStyle { it.withBold(true) } Component.literal(customGuiPrefix).withStyle { it.withColor(0xFFFFFF) }
) )
) )
} }

View file

@ -5,7 +5,6 @@ import com.beemer.essentials.data.Location
import com.beemer.essentials.nickname.NicknameDataStore import com.beemer.essentials.nickname.NicknameDataStore
import com.beemer.essentials.util.MessageUtils import com.beemer.essentials.util.MessageUtils
import net.minecraft.server.level.ServerPlayer import net.minecraft.server.level.ServerPlayer
import net.minecraft.sounds.SoundEvents
import net.minecraft.sounds.SoundSource import net.minecraft.sounds.SoundSource
import net.minecraft.world.Container import net.minecraft.world.Container
import net.minecraft.world.entity.player.Inventory import net.minecraft.world.entity.player.Inventory
@ -143,12 +142,14 @@ class TeleportGui(
} }
private fun playClickSound(player: ServerPlayer) { private fun playClickSound(player: ServerPlayer) {
player.playNotifySound( // 커스텀 텔레포트 사운드 (리소스팩: custom.teleport)
SoundEvents.NOTE_BLOCK_BELL.value(), val teleportSound =
SoundSource.MASTER, net.minecraft.sounds.SoundEvent.createVariableRangeEvent(
0.5f, net.minecraft.resources.ResourceLocation.parse(
1.0f "minecraft:custom.teleport"
) )
)
player.playNotifySound(teleportSound, SoundSource.MASTER, 0.2f, 1.0f)
} }
override fun stillValid(player: net.minecraft.world.entity.player.Player): Boolean = true override fun stillValid(player: net.minecraft.world.entity.player.Player): Boolean = true

View file

@ -95,6 +95,13 @@ object NicknameCommand {
return 0 return 0
} }
// 유효성 검사: 현재 닉네임과 동일
val currentNickname = NicknameDataStore.getNickname(player.uuid)
if (currentNickname == nickname) {
MessageUtils.sendError(player, "현재 사용 중인 닉네임과 동일합니다.")
return 0
}
// 유효성 검사: 중복 // 유효성 검사: 중복
if (NicknameDataStore.isNicknameTaken(nickname, player.uuid)) { if (NicknameDataStore.isNicknameTaken(nickname, player.uuid)) {
MessageUtils.sendError(player, "이미 사용 중인 닉네임입니다.") MessageUtils.sendError(player, "이미 사용 중인 닉네임입니다.")

View file

@ -55,12 +55,44 @@ object MessageUtils {
fun sendError(player: ServerPlayer, text: String) { fun sendError(player: ServerPlayer, text: String) {
player.sendSystemMessage(error(text)) player.sendSystemMessage(error(text))
playErrorSound(player)
} }
fun sendWarning(player: ServerPlayer, text: String) { fun sendWarning(player: ServerPlayer, text: String) {
player.sendSystemMessage(warning(text)) 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 { private fun parseStyledText(text: String, baseColor: Int, accentColor: Int): MutableComponent {
val result: MutableComponent = Component.empty() val result: MutableComponent = Component.empty()