diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/client/render/RenderHelper.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/client/render/RenderHelper.kt index 85a5f538c..ece72d1f2 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/client/render/RenderHelper.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/client/render/RenderHelper.kt @@ -9,7 +9,10 @@ import org.lwjgl.opengl.GL11 import ru.dbotthepony.mc.otm.OverdriveThatMatters import ru.dbotthepony.mc.otm.client.minecraft import ru.dbotthepony.mc.otm.core.RGBAColor +import java.lang.Math.pow +import kotlin.math.acos import kotlin.math.cos +import kotlin.math.pow import kotlin.math.sin private val identity = Matrix4f().also { it.setIdentity() } @@ -41,7 +44,7 @@ fun drawTexturedRect( u1: Float = 1f, v1: Float = 1f ) { - RenderSystem.setShader { GameRenderer.getPositionTexShader() } + RenderSystem.setShader(GameRenderer::getPositionTexShader) RenderSystem.enableTexture() RenderSystem.enableBlend() RenderSystem.defaultBlendFunc() @@ -328,6 +331,89 @@ fun drawRect( height: Float ) = drawRect(pose.last().pose(), x, y, width, height) +fun drawRect( + x: Float, + y: Float, + width: Float, + height: Float +) = drawRect(identity, x, y, width, height) + +fun drawLine( + matrix: Matrix4f, + startX: Float, + startY: Float, + endX: Float, + endY: Float, + width: Float +) { + RenderSystem.disableTexture() + RenderSystem.enableBlend() + RenderSystem.defaultBlendFunc() + RenderSystem.setShader(GameRenderer::getPositionColorShader) + RenderSystem.depthFunc(GL11.GL_ALWAYS) + + val tess = Tesselator.getInstance() + val builder = tess.builder + + builder.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_COLOR) + + val length = ((startX - endX).pow(2f) + (startY - endY).pow(2f)).pow(0.5f) + val angle = acos((endX - startX) / length) + + val cos = cos(angle) + val sin = sin(angle) + + val y0 = -width + + val y1 = width + + val x2 = length + val y2 = width + + val x3 = length + val y3 = -width + + builder.vertex(matrix, + startX + y0 * sin, + startY + y0 * cos, + zLevel).color(drawColor).endVertex() + + builder.vertex(matrix, + startX + y1 * sin, + startY + y1 * cos, + zLevel).color(drawColor).endVertex() + + builder.vertex(matrix, + startX + x2 * cos - y2 * sin, + startY + x2 * sin + y2 * cos, + zLevel).color(drawColor).endVertex() + + builder.vertex(matrix, + startX + x3 * cos - y3 * sin, + startY + x3 * sin + y3 * cos, + zLevel).color(drawColor).endVertex() + + tess.end() + RenderSystem.enableTexture() +} + +fun drawLine( + pose: PoseStack, + startX: Float, + startY: Float, + endX: Float, + endY: Float, + width: Float +) = drawLine(pose.last().pose(), startX, startY, endX, endY, width) + +fun drawLine( + startX: Float, + startY: Float, + endX: Float, + endY: Float, + width: Float +) = drawLine(identity, startX, startY, endX, endY, width) + private data class ScissorRect(val x: Int, val y: Int, val width: Int, val height: Int) private val scissorStack = ArrayDeque() diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/client/screen/AndroidStationScreen.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/client/screen/AndroidStationScreen.kt index 631759cf3..5ef46ae56 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/client/screen/AndroidStationScreen.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/client/screen/AndroidStationScreen.kt @@ -1,8 +1,14 @@ package ru.dbotthepony.mc.otm.client.screen import com.mojang.blaze3d.platform.InputConstants +import com.mojang.blaze3d.systems.RenderSystem import com.mojang.blaze3d.vertex.PoseStack +import it.unimi.dsi.fastutil.floats.FloatArrayList +import it.unimi.dsi.fastutil.ints.Int2FloatAVLTreeMap +import it.unimi.dsi.fastutil.ints.Int2ObjectAVLTreeMap +import it.unimi.dsi.fastutil.ints.Int2ObjectFunction import net.minecraft.ChatFormatting +import net.minecraft.Util import net.minecraft.client.Minecraft import net.minecraft.client.resources.sounds.SimpleSoundInstance import net.minecraft.network.chat.Component @@ -13,7 +19,10 @@ import ru.dbotthepony.mc.otm.android.AndroidResearch import ru.dbotthepony.mc.otm.android.AndroidResearchType import ru.dbotthepony.mc.otm.capability.AndroidCapability import ru.dbotthepony.mc.otm.capability.MatteryCapability -import ru.dbotthepony.mc.otm.client.render.RenderHelper +import ru.dbotthepony.mc.otm.client.minecraft +import ru.dbotthepony.mc.otm.client.render.drawColor +import ru.dbotthepony.mc.otm.client.render.drawLine +import ru.dbotthepony.mc.otm.client.render.drawRect import ru.dbotthepony.mc.otm.client.screen.panels.* import ru.dbotthepony.mc.otm.client.screen.widget.PowerGaugePanel import ru.dbotthepony.mc.otm.core.RGBAColor @@ -23,121 +32,279 @@ import ru.dbotthepony.mc.otm.network.AndroidNetworkChannel import ru.dbotthepony.mc.otm.network.AndroidResearchRequestPacket import ru.dbotthepony.mc.otm.registry.MRegistry import java.util.* +import kotlin.collections.ArrayList + +private fun exploreTree(research: AndroidResearchType<*>, seen: MutableSet>, result: MutableList>) { + if (!seen.add(research)) { + return + } + + result.add(research) + + for (children in research.flatUnlocks) { + exploreTree(children, seen, result) + } +} + +private fun findGraphs(): List, List>>> { + val seen = HashSet>() + val list = ArrayList, ArrayList>>>() + + for (research in MRegistry.ANDROID_RESEARCH) { + if (research.flatPrerequisites.isEmpty()) { + val tree = ArrayList>() + + exploreTree(research, seen, tree) + + if (tree.isNotEmpty()) { + list.add(research to tree) + } + } + } + + return list +} + +private fun isTree(root: AndroidResearchType<*>): Boolean { + if (root.flatPrerequisites.size > 1) { + return false + } + + for (child in root.flatUnlocks) { + if (!isTree(child)) { + return false + } + } + + return true +} + +private class Tree(val node: AndroidResearchType<*>) : Iterable { + val subtrees = ArrayList() + + val height: Int + val width: Int + + init { + for (child in node.flatUnlocks) { + val tree = Tree(child) + subtrees.add(tree) + } + + if (subtrees.isEmpty()) { + height = 0 + width = 1 + } else { + var height = 0 + var width = 0 + + for (child in subtrees) { + height = height.coerceAtLeast(child.height) + width += child.width + } + + this.height = height + 1 + this.width = width + } + + if (subtrees.isNotEmpty()) { + subtrees.sortWith { a, b -> + return@sortWith b.height.compareTo(a.height) + } + } + } + + override fun iterator(): Iterator { + if (subtrees.size < 2) { + return subtrees.iterator() + } else { + return object : Iterator { + private var index = subtrees.size - 1 + private var backward = true + + override fun hasNext(): Boolean { + return backward || index < subtrees.size - 1 + } + + override fun next(): Tree { + val index = index + + if (backward) { + this.index -= 2 + + if (this.index == -1) { + backward = false + this.index = 0 + } else if (this.index == 0) { + backward = false + } + } else { + if (this.index == 0) { + this.index = if (subtrees.size % 2 == 0) 2 else 1 + } else { + this.index += 2 + } + } + + return subtrees[index] + } + } + } + } + + fun put( + rows: Int2ObjectFunction, + left: Float, + capability: AndroidCapability + ): Pair { + val totalWidth = width * 24f + + val lines = ArrayList, Pair>>() + + val button = object : AndroidResearchButton(rows[node.researchTreeDepth], capability.getResearch(node)) { + override fun innerRender(stack: PoseStack, mouse_x: Float, mouse_y: Float, flag: Float) { + super.innerRender(stack, mouse_x, mouse_y, flag) + + drawColor = RGBAColor.WHITE + RGBAColor.WHITE.setShaderColor() + + for (line in lines) { + val (pos1, pos2) = line + val (x1, y1) = pos1 + val (x2, y2) = pos2 + + drawLine(stack, x1, y1, x2, y2, 0.5f) + } + } + } + + button.x = left + totalWidth / 2f - button.width / 2f + + @Suppress("name_shadowing") + var left = left + + val widths = FloatArrayList() + + if (subtrees.size > 1) { + lines.add( + ((button.width / 2f) to button.y + button.height) to + ((button.width / 2f) to button.y + button.height + 3f) + ) + } + + var minX = Float.MAX_VALUE + var maxX = -Float.MAX_VALUE + + for (subtree in this) { + val (btn, move) = subtree.put(rows, left, capability) + widths.add(move) + left += move + + if (subtrees.size == 1) { + lines.add( + ((button.width / 2f) to button.y + button.height) to + ((btn.width / 2f) to button.y + button.height + 6f) + ) + } else { + val x = btn.x + btn.width / 2f - button.x + val y = button.y + button.height + 3f + + lines.add( + (x to y) to + (x to y + 3f) + ) + + minX = minX.coerceAtMost(x) + maxX = maxX.coerceAtLeast(x) + } + } + + if (subtrees.size > 1) { + val y = button.y + button.height + 3f + + lines.add( + (minX - 0.5f to y) to + (maxX to y) + ) + } + + return button to totalWidth + } +} + +private open class AndroidResearchButton(parent: EditablePanel, private val node: AndroidResearch) : + EditablePanel( + parent.screen, + parent, + 0f, + 0f, + AndroidStationScreen.BUTTON_SIZE.toFloat(), + AndroidStationScreen.BUTTON_SIZE.toFloat() + ) { + init { + setDockMargin(2f, 2f, 2f, 2f) + } + + override fun innerRender(stack: PoseStack, mouse_x: Float, mouse_y: Float, flag: Float) { + minecraft.player?.getCapability(MatteryCapability.ANDROID)?.ifPresentK { + if (node.isResearched) { + AndroidStationScreen.RESEARCHED.setSystemColor() + } else if (node.canResearch) { + AndroidStationScreen.CAN_BE_RESEARCHED.setSystemColor() + } else { + AndroidStationScreen.CAN_NOT_BE_RESEARCHED.setSystemColor() + } + + val icon = node.skinIcon + + if (icon != null) { + icon.render(stack, 0f, 0f, width, height) + } else { + drawRect(stack, 0f, 0f, width, height) + } + + val text = node.iconText + + if (text != null) { + font.drawShadow(stack, text, width - font.width(text), height - font.lineHeight, -0x1) + } + } + } + + override fun mouseClickedInner(mouse_x: Double, mouse_y: Double, mouse_click_type: Int): Boolean { + if (mouse_click_type == InputConstants.MOUSE_BUTTON_LEFT) { + if (node.canResearch && !node.isResearched) { + AndroidNetworkChannel.sendToServer(AndroidResearchRequestPacket(node.type)) + } + + minecraft.soundManager.play(SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK, 1.0f)) + } + + return true + } + + override fun innerRenderTooltips(stack: PoseStack, mouse_x: Float, mouse_y: Float, flag: Float): Boolean { + if (isHovered) { + val list = ArrayList().also { it.addAll(node.screenTooltipLines) } + + if (node.isResearched) { + list.add(TranslatableComponent("otm.android_station.research.researched").withStyle(ChatFormatting.DARK_AQUA)) + } else if (node.canResearch) { + list.add(TranslatableComponent("otm.android_station.research.can_be_researched").withStyle(ChatFormatting.DARK_GREEN)) + } else { + list.add(TranslatableComponent("otm.android_station.research.can_not_be_researched").withStyle(ChatFormatting.DARK_RED)) + } + + screen.renderComponentTooltip(stack, list, mouse_x.toInt(), mouse_y.toInt()) + } + + return isHovered + } +} class AndroidStationScreen constructor(p_97741_: AndroidStationMenu, p_97742_: Inventory, p_97743_: Component) : MatteryScreen(p_97741_, p_97742_, p_97743_) { - internal inner class AndroidResearchButton(parent: EditablePanel?, private val node: AndroidResearch) : - EditablePanel( - this@AndroidStationScreen, - parent, - 0f, - 0f, - BUTTON_SIZE.toFloat(), - BUTTON_SIZE.toFloat() - ) { - - init { - setDockMargin(2f, 2f, 2f, 2f) - } - - override fun innerRender(stack: PoseStack, mouse_x: Float, mouse_y: Float, flag: Float) { - ru.dbotthepony.mc.otm.client.minecraft.player!!.getCapability(MatteryCapability.ANDROID) - .ifPresentK { - if (node.isResearched) { - RESEARCHED.setSystemColor() - } else if (node.canResearch) { - CAN_BE_RESEARCHED.setSystemColor() - } else { - CAN_NOT_BE_RESEARCHED.setSystemColor() - } - - val icon = node.skinIcon - - if (icon != null) { - icon.render(stack, 0f, 0f, width, height) - } else { - RenderHelper.drawRect(stack, 0f, 0f, width, height) - } - - val text = node.iconText - - if (text != null) { - font.drawShadow(stack, text, width - font.width(text), height - font.lineHeight, -0x1) - } - } - } - - override fun mouseClickedInner(mouse_x: Double, mouse_y: Double, mouse_click_type: Int): Boolean { - if (mouse_click_type == InputConstants.MOUSE_BUTTON_LEFT) { - if (node.canResearch && !node.isResearched) { - AndroidNetworkChannel.sendToServer(AndroidResearchRequestPacket(node.type)) - } - - ru.dbotthepony.mc.otm.client.minecraft.soundManager.play(SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK, 1.0f)) - } - - return true - } - - override fun innerRenderTooltips(stack: PoseStack, mouse_x: Float, mouse_y: Float, flag: Float): Boolean { - if (isHovered) { - val list = ArrayList().also { it.addAll(node.screenTooltipLines) } - - if (node.isResearched) { - list.add(TranslatableComponent("otm.android_station.research.researched").withStyle(ChatFormatting.DARK_AQUA)) - } else if (node.canResearch) { - list.add(TranslatableComponent("otm.android_station.research.can_be_researched").withStyle(ChatFormatting.DARK_GREEN)) - } else { - list.add(TranslatableComponent("otm.android_station.research.can_not_be_researched").withStyle(ChatFormatting.DARK_RED)) - } - - renderComponentTooltip(stack, list, mouse_x.toInt(), mouse_y.toInt()) - } - - return isHovered - } - } - - private val rows = arrayOfNulls(100) - private val seen: MutableSet> = HashSet() - private var nextX = 0f - private val rowsWidth = FloatArray(100) - private val createdButtons = Array(100) { arrayOfNulls(1000) } - private val createdButtonsIdx = IntArray(100) - - private fun dive(cap: AndroidCapability, research: AndroidResearchType<*>, level: Int) { - if (seen.contains(research)) return - seen.add(research) - - if (rows[level] == null) { - rows[level] = object : EditablePanel(this@AndroidStationScreen, canvas, 0f, (level * 22).toFloat(), 10000f, 22f) { - override fun mouseClickedInner(mouse_x: Double, mouse_y: Double, flag: Int) = false - override fun mouseReleasedInner(mouse_x: Double, mouse_y: Double, flag: Int) = false - override fun mouseDraggedInner(mouse_x: Double, mouse_y: Double, flag: Int, drag_x: Double, drag_y: Double) = false - } - } - - val row = rows[level] - val button = AndroidResearchButton(row, cap.getResearch(research)) - - button.setPos(nextX + rowsWidth[level], 2f) - - createdButtons[level][createdButtonsIdx[level]] = button - createdButtonsIdx[level]++ - rowsWidth[level] += 22f - - for (_research in research.flatUnlocks) { - dive(cap, _research, level + 1) - } - - if (level > 0) { - for (_research in research.definedPrerequisites) { - dive(cap, _research, level - 1) - } - } - } - private var canvas: DraggableCanvasPanel? = null private var research: FramePanel? = null @@ -146,56 +313,60 @@ class AndroidStationScreen constructor(p_97741_: AndroidStationMenu, p_97742_: I research = FramePanel(this, null, 0f, 0f, window.guiScaledWidth * 0.8f, window.guiScaledHeight * 0.8f, TranslatableComponent("otm.gui.android_research")) + val rows = Int2ObjectAVLTreeMap() + canvas = object : DraggableCanvasPanel(this@AndroidStationScreen, research, width = (GRID_WIDTH * 22).toFloat(), height = 0f) { override fun innerRender(stack: PoseStack, mouse_x: Float, mouse_y: Float, flag: Float) { - RenderHelper.setDrawColor(RGBAColor.BLACK) - RenderHelper.drawRect(stack, 0f, 0f, width, height) + drawColor = RGBAColor.BLACK + drawRect(stack, 0f, 0f, width, height) } } - minecraft?.player?.getCapability(MatteryCapability.ANDROID)?.ifPresent { - Arrays.fill(rows, null) - nextX = 0f + minecraft?.player?.getCapability(MatteryCapability.ANDROID)?.ifPresentK { + var totalWidth = 0f - for (research in MRegistry.ANDROID_RESEARCH.values) { - if (research.definedPrerequisites.isEmpty()) { - dive(it, research, 0) + for (graph in findGraphs()) { + if (isTree(graph.first)) { + val tree = Tree(graph.first) - var max = 0f - - for (v in rowsWidth) - max = Math.max(max, v) - - for (button_list in createdButtons) { - var count = 0 - - for (i in button_list.indices) { - if (button_list[i] == null) { - count = i - break + for (i in 0 .. tree.height) { + rows.computeIfAbsent(i, Int2ObjectFunction { + object : EditablePanel(this@AndroidStationScreen, canvas, 0f, it * 24f, 10000f, 22f) { + override fun mouseClickedInner(mouse_x: Double, mouse_y: Double, mouse_click_type: Int) = false + override fun mouseReleasedInner(mouse_x: Double, mouse_y: Double, flag: Int) = false + override fun mouseDraggedInner(mouse_x: Double, mouse_y: Double, flag: Int, drag_x: Double, drag_y: Double) = false } - } - - if (count > 0) { - var thisX = nextX + max / 2f - count * 22f / 2f - - for (i in 0 until count) { - button_list[i]!!.setPos(thisX, 2f) - thisX += 22f - } - } + }) } - for (v in createdButtons) - Arrays.fill(v, null) + totalWidth += tree.put(rows, totalWidth, it).second + } else { + val rowWidths = Int2FloatAVLTreeMap() - nextX += max - Arrays.fill(rowsWidth, 0f) - Arrays.fill(createdButtonsIdx, 0) + for (research in graph.second) { + val row = rows.computeIfAbsent(research.researchTreeDepth, Int2ObjectFunction { + object : EditablePanel(this@AndroidStationScreen, canvas, 0f, it * 24f, 10000f, 22f) { + override fun mouseClickedInner(mouse_x: Double, mouse_y: Double, mouse_click_type: Int) = false + override fun mouseReleasedInner(mouse_x: Double, mouse_y: Double, flag: Int) = false + override fun mouseDraggedInner(mouse_x: Double, mouse_y: Double, flag: Int, drag_x: Double, drag_y: Double) = false + } + }) + + val button = AndroidResearchButton(row, it.getResearch(research)) + val width = rowWidths[research.researchTreeDepth] + button.setPos(totalWidth + width, 0f) + rowWidths[research.researchTreeDepth] += 22f + } + + var maximal = 0f + + for (value in rowWidths.values) { + maximal = maximal.coerceAtLeast(value) + } + + totalWidth += maximal } } - - seen.clear() } canvas!!.dock = Dock.FILL