From 2981bdcb07c931f1d50504c6aafffbd5183dc734 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sat, 20 Feb 2021 14:24:40 -0500 Subject: [PATCH] Add request specific amount dialog to terminal GUI --- .../network/block/terminal/TerminalScreen.kt | 263 ++++++++++++++++-- .../block/terminal/TerminalScreenHandler.kt | 2 +- .../phycon/textures/gui/terminal_amount.png | Bin 0 -> 7933 bytes 3 files changed, 240 insertions(+), 25 deletions(-) create mode 100644 src/main/resources/assets/phycon/textures/gui/terminal_amount.png diff --git a/src/main/kotlin/net/shadowfacts/phycon/network/block/terminal/TerminalScreen.kt b/src/main/kotlin/net/shadowfacts/phycon/network/block/terminal/TerminalScreen.kt index 372a17e..077eff2 100644 --- a/src/main/kotlin/net/shadowfacts/phycon/network/block/terminal/TerminalScreen.kt +++ b/src/main/kotlin/net/shadowfacts/phycon/network/block/terminal/TerminalScreen.kt @@ -1,11 +1,15 @@ package net.shadowfacts.phycon.network.block.terminal -import com.mojang.blaze3d.platform.GlStateManager +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.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 @@ -13,6 +17,9 @@ import net.minecraft.text.Text import net.minecraft.util.Identifier import net.shadowfacts.phycon.PhysicalConnectivity import org.lwjgl.glfw.GLFW +import java.lang.NumberFormatException +import kotlin.math.ceil +import kotlin.math.floor /** * @author shadowfacts @@ -20,10 +27,31 @@ import org.lwjgl.glfw.GLFW // todo: translate title class TerminalScreen(handler: TerminalScreenHandler, playerInv: PlayerInventory, title: Text): HandledScreen(handler, playerInv, title) { companion object { - val BACKGROUND = Identifier(PhysicalConnectivity.MODID, "textures/gui/terminal.png") + 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 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 val dialogWidth = 158 + private val dialogHeight = 62 init { backgroundWidth = 252 @@ -33,6 +61,9 @@ class TerminalScreen(handler: TerminalScreenHandler, playerInv: PlayerInventory, override fun init() { super.init() + children.clear() + dialogChildren.clear() + client!!.keyboard.setRepeatEvents(true) searchBox = TextFieldWidget(textRenderer, x + 138, y + 6, 80, 9, LiteralText("Search")) @@ -43,11 +74,68 @@ class TerminalScreen(handler: TerminalScreenHandler, playerInv: PlayerInventory, searchBox.setSelected(true) searchBox.setEditableColor(0xffffff) children.add(searchBox) + + 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) } override fun tick() { super.tick() searchBox.tick() + amountBox.tick() } override fun drawForeground(matrixStack: MatrixStack, mouseX: Int, mouseY: Int) { @@ -58,21 +146,50 @@ class TerminalScreen(handler: TerminalScreenHandler, playerInv: PlayerInventory, } override fun drawBackground(matrixStack: MatrixStack, delta: Float, mouseX: Int, mouseY: Int) { - renderBackground(matrixStack) + // if the dialog is open, the background gradient will be drawn in front of the main terminal gui + if (!showingAmountDialog) { + renderBackground(matrixStack) + } - GlStateManager.color4f(1f, 1f, 1f, 1f) + 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) } + @ExperimentalUnsignedTypes override fun render(matrixStack: MatrixStack, mouseX: Int, mouseY: Int, delta: Float) { - super.render(matrixStack, mouseX, mouseY, delta) + 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) - drawMouseoverTooltip(matrixStack, mouseX, mouseY) + 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 @@ -90,37 +207,107 @@ class TerminalScreen(handler: TerminalScreenHandler, playerInv: PlayerInventory, DrawableHelper.fill(matrixStack, slot.x, slot.y, slot.x + 16, slot.y + 16, color.toInt()) } - override fun onMouseClick(slot: Slot?, i: Int, j: Int, slotActionType: SlotActionType?) { - super.onMouseClick(slot, i, j, slotActionType) + 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 (type == SlotActionType.PICKUP && clickData == 0 && slot != null && handler.isNetworkSlot(slot.id) && !slot.stack.isEmpty) { + dialogStack = slot.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 { + return super.mouseClicked(mouseX, mouseY, button) + } + } + + override fun mouseDragged(d: Double, e: Double, i: Int, f: Double, g: Double): Boolean { + if (showingAmountDialog) { + return false + } else { + return super.mouseDragged(d, e, i, f, g) + } + } + + 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 { + return super.mouseReleased(d, e, i) + } + } + + override fun mouseScrolled(d: Double, e: Double, f: Double): Boolean { + if (showingAmountDialog) { + return false + } else { + return super.mouseScrolled(d, e, f) + } } override fun charTyped(c: Char, i: Int): Boolean { - val oldText = searchBox.text - if (searchBox.charTyped(c, i)) { - if (searchBox.text != oldText) { - search() + if (showingAmountDialog) { + return amountBox.charTyped(c, i) + } else { + val oldText = searchBox.text + if (searchBox.charTyped(c, i)) { + if (searchBox.text != oldText) { + search() + } + return true } - return true - } - return super.charTyped(c, i) + return super.charTyped(c, i) + } } override fun keyPressed(key: Int, j: Int, k: Int): Boolean { - val oldText = searchBox.text - if (searchBox.keyPressed(key, j, k)) { - if (searchBox.text != oldText) { - search() + if (showingAmountDialog) { + return when (key) { + GLFW.GLFW_KEY_ESCAPE -> { + showingAmountDialog = false + true + } + GLFW.GLFW_KEY_ENTER -> { + doDialogRequest() + true + } + else -> { + amountBox.keyPressed(key, j, k) + } } - return true - } - return if (searchBox.isFocused && searchBox.isVisible && key != GLFW.GLFW_KEY_ESCAPE) { - true } else { - super.keyPressed(key, j, k) + val oldText = searchBox.text + if (searchBox.keyPressed(key, j, k)) { + if (searchBox.text != oldText) { + search() + } + return true + } + return if (searchBox.isFocused && searchBox.isVisible && key != GLFW.GLFW_KEY_ESCAPE) { + true + } else { + super.keyPressed(key, j, k) + } } } @@ -128,4 +315,32 @@ class TerminalScreen(handler: TerminalScreenHandler, playerInv: PlayerInventory, screenHandler.search(searchBox.text) } + private fun doDialogRequest() { + showingAmountDialog = false + handler.requestItem(client!!.player!!, dialogStack, amountBox.intValue) + } + + 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()) + } + } + } diff --git a/src/main/kotlin/net/shadowfacts/phycon/network/block/terminal/TerminalScreenHandler.kt b/src/main/kotlin/net/shadowfacts/phycon/network/block/terminal/TerminalScreenHandler.kt index e648f07..bd5b3ff 100644 --- a/src/main/kotlin/net/shadowfacts/phycon/network/block/terminal/TerminalScreenHandler.kt +++ b/src/main/kotlin/net/shadowfacts/phycon/network/block/terminal/TerminalScreenHandler.kt @@ -134,7 +134,7 @@ class TerminalScreenHandler(syncId: Int, playerInv: PlayerInventory, val termina return super.onSlotClick(slotId, clickData, actionType, player) } - private fun requestItem(player: PlayerEntity, stack: ItemStack, amount: Int) { + fun requestItem(player: PlayerEntity, stack: ItemStack, amount: Int) { if (!player.world.isClient) return val handler = (player as ClientPlayerEntity).networkHandler val packet = C2STerminalRequestItem(terminal, stack, amount) diff --git a/src/main/resources/assets/phycon/textures/gui/terminal_amount.png b/src/main/resources/assets/phycon/textures/gui/terminal_amount.png new file mode 100644 index 0000000000000000000000000000000000000000..3999831eb3004243fc25f5b1d7e4e62acd9dddb0 GIT binary patch literal 7933 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