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
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
}

View file

@ -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
}

View file

@ -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,
"스폰으로 이동했습니다."

View file

@ -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, _ ->

View file

@ -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 =

View file

@ -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 {

View file

@ -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<Component> =
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
}

View file

@ -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) }
)
)
}

View file

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

View file

@ -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, "이미 사용 중인 닉네임입니다.")

View file

@ -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()