Android research *tree*

*epic music plays*
This commit is contained in:
DBotThePony 2022-08-23 20:20:00 +07:00
parent b4f1e7fd89
commit 14e57cd4d1
Signed by: DBot
GPG Key ID: DCC23B5715498507
2 changed files with 407 additions and 150 deletions

View File

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

View File

@ -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<AndroidResearchType<*>>, result: MutableList<AndroidResearchType<*>>) {
if (!seen.add(research)) {
return
}
result.add(research)
for (children in research.flatUnlocks) {
exploreTree(children, seen, result)
}
}
private fun findGraphs(): List<Pair<AndroidResearchType<*>, List<AndroidResearchType<*>>>> {
val seen = HashSet<AndroidResearchType<*>>()
val list = ArrayList<Pair<AndroidResearchType<*>, ArrayList<AndroidResearchType<*>>>>()
for (research in MRegistry.ANDROID_RESEARCH) {
if (research.flatPrerequisites.isEmpty()) {
val tree = ArrayList<AndroidResearchType<*>>()
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<Tree> {
val subtrees = ArrayList<Tree>()
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<Tree> {
if (subtrees.size < 2) {
return subtrees.iterator()
} else {
return object : Iterator<Tree> {
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<EditablePanel>,
left: Float,
capability: AndroidCapability
): Pair<AndroidResearchButton, Float> {
val totalWidth = width * 24f
val lines = ArrayList<Pair<Pair<Float, Float>, Pair<Float, Float>>>()
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<Component>().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<AndroidStationMenu>(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<Component>().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<EditablePanel>(100)
private val seen: MutableSet<AndroidResearchType<*>> = HashSet()
private var nextX = 0f
private val rowsWidth = FloatArray(100)
private val createdButtons = Array(100) { arrayOfNulls<AndroidResearchButton>(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<EditablePanel>()
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