package net.shadowfacts.phycon.block.terminal import com.mojang.blaze3d.systems.RenderSystem import net.minecraft.client.MinecraftClient import net.minecraft.client.gui.DrawableHelper import net.minecraft.client.gui.screen.ingame.HandledScreen import net.minecraft.client.gui.widget.AbstractButtonWidget import net.minecraft.client.gui.widget.AbstractPressableButtonWidget import net.minecraft.client.gui.widget.ButtonWidget import net.minecraft.client.gui.widget.TextFieldWidget import net.minecraft.client.util.math.MatrixStack import net.minecraft.entity.player.PlayerInventory import net.minecraft.item.ItemStack import net.minecraft.screen.slot.Slot import net.minecraft.screen.slot.SlotActionType import net.minecraft.text.LiteralText import net.minecraft.text.Text import net.minecraft.text.TranslatableText import net.minecraft.util.Identifier import net.minecraft.util.math.MathHelper import net.shadowfacts.phycon.PhysicalConnectivity import net.shadowfacts.phycon.networking.C2STerminalRequestItem import net.shadowfacts.phycon.networking.C2STerminalUpdateDisplayedItems import net.shadowfacts.phycon.util.SortMode import net.shadowfacts.phycon.util.next import net.shadowfacts.phycon.util.prev import org.lwjgl.glfw.GLFW import java.lang.NumberFormatException import kotlin.math.ceil import kotlin.math.floor import kotlin.math.min import kotlin.math.roundToInt /** * @author shadowfacts */ // todo: translate title class TerminalScreen(handler: TerminalScreenHandler, playerInv: PlayerInventory, title: Text): HandledScreen(handler, playerInv, title) { companion object { private val BACKGROUND = Identifier(PhysicalConnectivity.MODID, "textures/gui/terminal.png") private val DIALOG = Identifier(PhysicalConnectivity.MODID, "textures/gui/terminal_amount.png") } private lateinit var searchBox: TextFieldWidget private lateinit var sortButton: SortButton private lateinit var amountBox: TextFieldWidget private var dialogStack = ItemStack.EMPTY private var showingAmountDialog = false set(value) { val oldValue = field field = value for (e in dialogChildren) { e.visible = value } amountBox.isVisible searchBox.setSelected(!value) amountBox.setSelected(value) if (value && !oldValue) { amountBox.text = "1" } } private var dialogChildren = mutableListOf() private var scrollPosition = 0f private var isDraggingScrollThumb = false private val trackMinY = 18 private val trackHeight = 106 private val thumbHeight = 15 private val thumbWidth = 12 private val scrollThumbTop: Int get() = trackMinY + (scrollPosition * (trackHeight - thumbHeight)).roundToInt() private val dialogWidth = 158 private val dialogHeight = 62 init { backgroundWidth = 252 backgroundHeight = 222 } override fun init() { super.init() children.clear() dialogChildren.clear() client!!.keyboard.setRepeatEvents(true) searchBox = TextFieldWidget(textRenderer, x + 138, y + 6, 80, 9, LiteralText("Search")) searchBox.setMaxLength(50) // setHasBorder is actually setDrawsBackground searchBox.setHasBorder(false) searchBox.isVisible = true searchBox.setSelected(true) searchBox.setEditableColor(0xffffff) addChild(searchBox) sortButton = SortButton(x + 256, y + 0, handler.sortMode, { requestUpdatedItems() }, ::renderTooltip) addButton(sortButton) val dialogMinX = width / 2 - dialogWidth / 2 val dialogMinY = height / 2 - dialogHeight / 2 amountBox = TextFieldWidget(textRenderer, dialogMinX + 8, dialogMinY + 27, 80, 9, LiteralText("Amount")) amountBox.setHasBorder(false) amountBox.isVisible = false amountBox.setSelected(false) amountBox.setEditableColor(0xffffff) amountBox.setTextPredicate { if (it.isEmpty()) { true } else { try { Integer.parseInt(it) > 0 } catch (e: NumberFormatException) { false } } } dialogChildren.add(amountBox) val plusOne = SmallButton(dialogMinX + 7, dialogMinY + 7, 28, LiteralText("+1")) { amountBox.intValue += 1 } dialogChildren.add(plusOne) val plusTen = SmallButton(dialogMinX + 7 + 28 + 3, dialogMinY + 7, 28, LiteralText("+10")) { amountBox.intValue = ceil((amountBox.intValue + 1) / 10.0).toInt() * 10 } dialogChildren.add(plusTen) val plusHundred = SmallButton(dialogMinX + 7 + (28 + 3) * 2, dialogMinY + 7, 28, LiteralText("+100")) { amountBox.intValue = ceil((amountBox.intValue + 1) / 100.0).toInt() * 100 } dialogChildren.add(plusHundred) val minusOne = SmallButton(dialogMinX + 7, dialogMinY + 39, 28, LiteralText("-1")) { amountBox.intValue -= 1 } dialogChildren.add(minusOne) val minusTen = SmallButton(dialogMinX + 7 + 28 + 3, dialogMinY + 39, 28, LiteralText("-10")) { amountBox.intValue = floor((amountBox.intValue - 1) / 10.0).toInt() * 10 } dialogChildren.add(minusTen) val minusHundred = SmallButton(dialogMinX + 7 + (28 + 3) * 2, dialogMinY + 39, 28, LiteralText("-100")) { amountBox.intValue = floor((amountBox.intValue - 1) / 100.0).toInt() * 100 } dialogChildren.add(minusHundred) // 101,25 val request = ButtonWidget(dialogMinX + 101, dialogMinY + 21, 50, 20, LiteralText("Request")) { doDialogRequest() } dialogChildren.add(request) requestUpdatedItems() } private fun requestUpdatedItems() { val player = MinecraftClient.getInstance().player!! player.networkHandler.sendPacket(C2STerminalUpdateDisplayedItems(handler.terminal, searchBox.text, sortButton.mode, scrollPosition)) } override fun tick() { super.tick() searchBox.tick() amountBox.tick() } override fun drawForeground(matrixStack: MatrixStack, mouseX: Int, mouseY: Int) { textRenderer.draw(matrixStack, title, 65f, 6f, 0x404040) textRenderer.draw(matrixStack, playerInventory.displayName, 65f, backgroundHeight - 94f, 0x404040) textRenderer.draw(matrixStack, TranslatableText("gui.phycon.terminal_buffer"), 7f, 6f, 0x404040) } override fun drawBackground(matrixStack: MatrixStack, delta: Float, mouseX: Int, mouseY: Int) { // if the dialog is open, the background gradient will be drawn in front of the main terminal gui if (!showingAmountDialog) { renderBackground(matrixStack) } RenderSystem.color4f(1f, 1f, 1f, 1f) client!!.textureManager.bindTexture(BACKGROUND) val x = (width - backgroundWidth) / 2 val y = (height - backgroundHeight) / 2 drawTexture(matrixStack, x, y, 0, 0, backgroundWidth, backgroundHeight) // scroll thumb drawTexture(matrixStack, x + 232, y + scrollThumbTop, 52, 230, thumbWidth, thumbHeight) } @ExperimentalUnsignedTypes override fun render(matrixStack: MatrixStack, mouseX: Int, mouseY: Int, delta: Float) { if (showingAmountDialog) { RenderSystem.pushMatrix() // items are rendered at some stupidly high z offset. item amounts at an even higher one RenderSystem.translatef(0f, 0f, -350f) // fake the mouse x/y while showing a dialog so slot mouseover highlights aren't drawn super.render(matrixStack, -1, -1, delta) RenderSystem.popMatrix() } else { super.render(matrixStack, mouseX, mouseY, delta) } searchBox.render(matrixStack, mouseX, mouseY, delta) if (showingAmountDialog) { renderBackground(matrixStack) RenderSystem.color4f(1f, 1f, 1f, 1f) client!!.textureManager.bindTexture(DIALOG) val dialogMinX = width / 2 - dialogWidth / 2 val dialogMinY = height / 2 - dialogHeight / 2 drawTexture(matrixStack, dialogMinX, dialogMinY, 0, 0, dialogWidth, dialogHeight) for (e in dialogChildren) { e.render(matrixStack, mouseX, mouseY, delta) } } else { drawMouseoverTooltip(matrixStack, mouseX, mouseY) } } @ExperimentalUnsignedTypes fun drawSlotUnderlay(matrixStack: MatrixStack, slot: Slot) { if (!handler.isBufferSlot(slot.id)) { return } val mode = handler.terminal.internalBuffer.getMode(slot.id - handler.bufferSlotsStart) val color: UInt = when (mode) { TerminalBufferInventory.Mode.TO_NETWORK -> 0xFFFF0000u TerminalBufferInventory.Mode.FROM_NETWORK -> 0xFF00FF00u else -> return } DrawableHelper.fill(matrixStack, slot.x, slot.y, slot.x + 16, slot.y + 16, color.toInt()) } private fun isPointInsScrollThumb(mouseX: Double, mouseY: Double): Boolean { val x = (width - backgroundWidth) / 2 val y = (height - backgroundHeight) / 2 val thumbMinX = x + 232 val thumbMaxX = thumbMinX + thumbWidth val thumbMinY = y + scrollThumbTop val thumbMaxY = thumbMinY + thumbHeight return mouseX >= thumbMinX && mouseX < thumbMaxX && mouseY >= thumbMinY && mouseY < thumbMaxY } override fun onMouseClick(slot: Slot?, invSlot: Int, clickData: Int, type: SlotActionType?) { super.onMouseClick(slot, invSlot, clickData, type) // don't unfocus the search box on mouse click searchBox.setSelected(true) if (slot != null && !slot.stack.isEmpty && handler.isNetworkSlot(slot.id)) { val stack = slot.stack if (type == SlotActionType.QUICK_MOVE) { // shift click, request full stack requestItem(stack, min(stack.count, stack.maxCount)) } else if (type == SlotActionType.PICKUP) { if (clickData == 1) { // right click, request half stack requestItem(stack, ceil(min(stack.count, stack.maxCount) / 2f).toInt()) } else { dialogStack = stack showingAmountDialog = true searchBox.setSelected(false) } } } } override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean { if (showingAmountDialog) { for (e in dialogChildren) { if (e.mouseClicked(mouseX, mouseY, button)) { return true } } return false } else { if (isPointInsScrollThumb(mouseX, mouseY)) { isDraggingScrollThumb = true return true } return super.mouseClicked(mouseX, mouseY, button) } } override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, deltaX: Double, deltaY: Double): Boolean { if (showingAmountDialog) { return false } else if (isDraggingScrollThumb) { scrollPosition = (mouseY.toFloat() - (y + trackMinY) - 7.5f) / (trackHeight - 15) scrollPosition = MathHelper.clamp(scrollPosition, 0f, 1f) requestUpdatedItems() return true } else { return super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY) } } override fun mouseMoved(d: Double, e: Double) { if (showingAmountDialog) { } else { super.mouseMoved(d, e) } } override fun mouseReleased(d: Double, e: Double, i: Int): Boolean { if (showingAmountDialog) { return false } else { isDraggingScrollThumb = false return super.mouseReleased(d, e, i) } } override fun mouseScrolled(mouseX: Double, mouseY: Double, amount: Double): Boolean { if (showingAmountDialog) { return false } else { var newOffsetInRows = handler.currentScrollOffsetInRows() - amount.toInt() newOffsetInRows = MathHelper.clamp(newOffsetInRows, 0, handler.maxScrollOffsetInRows()) val newScrollPosition = newOffsetInRows / handler.maxScrollOffsetInRows().toFloat() scrollPosition = newScrollPosition requestUpdatedItems() return super.mouseScrolled(mouseX, mouseY, amount) } } override fun charTyped(c: Char, i: Int): Boolean { if (showingAmountDialog) { return amountBox.charTyped(c, i) } else { val oldText = searchBox.text if (searchBox.charTyped(c, i)) { if (searchBox.text != oldText) { scrollPosition = 0f requestUpdatedItems() } return true } return super.charTyped(c, i) } } override fun keyPressed(key: Int, j: Int, k: Int): Boolean { if (showingAmountDialog) { return when (key) { GLFW.GLFW_KEY_ESCAPE -> { showingAmountDialog = false true } GLFW.GLFW_KEY_ENTER -> { doDialogRequest() true } else -> { amountBox.keyPressed(key, j, k) } } } else { val oldText = searchBox.text if (searchBox.keyPressed(key, j, k)) { if (searchBox.text != oldText) { scrollPosition = 0f requestUpdatedItems() } return true } return if (searchBox.isFocused && searchBox.isVisible && key != GLFW.GLFW_KEY_ESCAPE) { true } else { super.keyPressed(key, j, k) } } } private fun doDialogRequest() { showingAmountDialog = false requestItem(dialogStack, amountBox.intValue) } private fun requestItem(stack: ItemStack, amount: Int) { val netHandler = MinecraftClient.getInstance().player!!.networkHandler val packet = C2STerminalRequestItem(handler.terminal, stack, amount) netHandler.sendPacket(packet) } private var TextFieldWidget.intValue: Int get() = if (text.isEmpty()) 0 else Integer.parseInt(text) set(value) { text = value.toString() setSelected(true) } class SmallButton(x: Int, y: Int, width: Int, title: Text, action: PressAction): ButtonWidget(x, y, width, 14, title, action) { @ExperimentalUnsignedTypes override fun renderButton(matrixStack: MatrixStack, mouseX: Int, mouseY: Int, delta: Float) { val client = MinecraftClient.getInstance() client.textureManager.bindTexture(DIALOG) RenderSystem.color4f(1f, 1f, 1f, 1f) val v = if (isHovered) 142 else 128 RenderSystem.enableBlend() RenderSystem.defaultBlendFunc() RenderSystem.enableDepthTest() drawTexture(matrixStack, x, y, 0, v, width / 2, height) drawTexture(matrixStack, x + width / 2, y, 200 - width / 2, v, width / 2, height) drawCenteredText(matrixStack, client.textRenderer, message, x + width / 2, y + (height - 8) / 2, 0xffffffffu.toInt()) } } class SortButton( x: Int, y: Int, var mode: SortMode, val onChange: (SortMode) -> Unit, val doRenderTooltip: (MatrixStack, Text, Int, Int) -> Unit ): AbstractPressableButtonWidget(x, y, 20, 20, LiteralText("")) { override fun onPress() {} override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean { if ((button == 0 || button == 1) && clicked(mouseX, mouseY)) { val newVal = if (button == 0) mode.next else mode.prev mode = newVal onChange(mode) playDownSound(MinecraftClient.getInstance().soundManager) return true } return false } override fun renderButton(matrixStack: MatrixStack, mouseX: Int, mouseY: Int, delta: Float) { val client = MinecraftClient.getInstance() RenderSystem.color4f(1f, 1f, 1f, 1f) RenderSystem.enableBlend() RenderSystem.defaultBlendFunc() RenderSystem.enableDepthTest() client.textureManager.bindTexture(WIDGETS_LOCATION) val k = getYImage(isHovered) drawTexture(matrixStack, x, y, 0, 46 + k * 20, width / 2, height) drawTexture(matrixStack, x + width / 2, y, 200 - width / 2, 46 + k * 20, width / 2, height) client.textureManager.bindTexture(BACKGROUND) val u: Int = when (mode) { SortMode.COUNT_HIGH_FIRST -> 0 SortMode.COUNT_LOW_FIRST -> 16 SortMode.ALPHABETICAL -> 32 } drawTexture(matrixStack, x + 2, y + 2, u, 230, 16, 16) if (isHovered) { renderToolTip(matrixStack, mouseX, mouseY) } } override fun renderToolTip(matrixStack: MatrixStack, mouseX: Int, mouseY: Int) { val text = LiteralText("") text.append("Sort by: ") text.append(mode.tooltip) doRenderTooltip(matrixStack, text, mouseX, mouseY) } } }