feat: 커스텀 사운드 시스템 추가
- 클릭 사운드 (custom.click) - GUI 버튼 클릭 시 - 에러 사운드 (custom.error) - 오류 메시지 시 자동 재생 - 알림 사운드 (custom.notification) - 신규 플레이어 환영 시 - 텔레포트 사운드 (custom.teleport) - 스폰, 좌표이동, TPA, back - 메뉴 사운드 (custom.menu) - 메뉴 GUI 열기 시 기타 변경: - 닉네임 변경 시 동일 닉네임 예외처리 추가 - TPA 오류 메시지 빨간색으로 변경
This commit is contained in:
parent
4dba2a4803
commit
fd4540c9e7
11 changed files with 384 additions and 194 deletions
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
"스폰으로 이동했습니다."
|
||||
|
|
|
|||
|
|
@ -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, _ ->
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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, "이미 사용 중인 닉네임입니다.")
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue