HexCasting/Common/src/main/java/at/petrak/hexcasting/client/gui/GuiSpellcasting.kt

505 lines
20 KiB
Kotlin

package at.petrak.hexcasting.client.gui
import at.petrak.hexcasting.api.casting.eval.ExecutionClientView
import at.petrak.hexcasting.api.casting.eval.ResolvedPattern
import at.petrak.hexcasting.api.casting.eval.ResolvedPatternType
import at.petrak.hexcasting.api.casting.iota.IotaType
import at.petrak.hexcasting.api.casting.math.HexAngle
import at.petrak.hexcasting.api.casting.math.HexCoord
import at.petrak.hexcasting.api.casting.math.HexDir
import at.petrak.hexcasting.api.casting.math.HexPattern
import at.petrak.hexcasting.api.mod.HexConfig
import at.petrak.hexcasting.api.mod.HexTags
import at.petrak.hexcasting.api.utils.asTranslatedComponent
import at.petrak.hexcasting.client.ClientTickCounter
import at.petrak.hexcasting.client.ShiftScrollListener
import at.petrak.hexcasting.client.ktxt.accumulatedScroll
import at.petrak.hexcasting.client.render.*
import at.petrak.hexcasting.client.sound.GridSoundInstance
import at.petrak.hexcasting.common.lib.HexAttributes
import at.petrak.hexcasting.common.lib.HexSounds
import at.petrak.hexcasting.common.msgs.MsgNewSpellPatternC2S
import at.petrak.hexcasting.xplat.IClientXplatAbstractions
import com.mojang.blaze3d.systems.RenderSystem
import com.mojang.blaze3d.vertex.PoseStack
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.screens.Screen
import net.minecraft.client.renderer.GameRenderer
import net.minecraft.client.resources.sounds.SimpleSoundInstance
import net.minecraft.client.resources.sounds.SoundInstance
import net.minecraft.nbt.CompoundTag
import net.minecraft.sounds.SoundSource
import net.minecraft.util.FormattedCharSequence
import net.minecraft.util.Mth
import net.minecraft.world.InteractionHand
import net.minecraft.world.phys.Vec2
import kotlin.math.*
// TODO winfy: fix this class to use ExecutionClientView
class GuiSpellcasting constructor(
private val handOpenedWith: InteractionHand,
private var patterns: MutableList<ResolvedPattern>,
private var cachedStack: List<CompoundTag>,
private var cachedRavenmind: CompoundTag?,
private var parenCount: Int,
) : Screen("gui.hexcasting.spellcasting".asTranslatedComponent) {
private var stackDescs: List<FormattedCharSequence> = listOf()
private var parenDescs: List<FormattedCharSequence> = listOf()
private var ravenmind: FormattedCharSequence? = null
private var drawState: PatternDrawState = PatternDrawState.BetweenPatterns
private val usedSpots: MutableSet<HexCoord> = HashSet()
private var ambianceSoundInstance: GridSoundInstance? = null
private val randSrc = SoundInstance.createUnseededRandom()
init {
for ((pattern, origin) in patterns) {
this.usedSpots.addAll(pattern.positions(origin))
}
this.calculateIotaDisplays()
}
fun recvServerUpdate(info: ExecutionClientView, index: Int) {
if (info.isStackClear) {
this.minecraft?.setScreen(null)
return
}
// TODO this is the kinda hacky bit
if (info.resolutionType == ResolvedPatternType.UNDONE) {
this.patterns.reversed().drop(1).firstOrNull { it.type == ResolvedPatternType.ESCAPED }?.let { it.type = ResolvedPatternType.UNDONE }
this.patterns.getOrNull(index)?.let { it.type = ResolvedPatternType.EVALUATED }
} else this.patterns.getOrNull(index)?.let {
it.type = info.resolutionType
}
this.cachedStack = info.stackDescs
this.cachedRavenmind = info.ravenmind
this.calculateIotaDisplays()
}
fun calculateIotaDisplays() {
val mc = Minecraft.getInstance()
val width = (this.width * LHS_IOTAS_ALLOCATION).toInt()
this.stackDescs =
this.cachedStack.map { IotaType.getDisplayWithMaxWidth(it, width, mc.font) }
.asReversed()
// this.parenDescs = if (this.cachedParens.isNotEmpty())
// this.cachedParens.flatMap { HexIotaTypes.getDisplayWithMaxWidth(it, width, mc.font) }
// else if (this.parenCount > 0)
// listOf("...".gold.visualOrderText)
// else
// emptyList()
this.parenDescs = emptyList()
this.ravenmind =
this.cachedRavenmind?.let {
IotaType.getDisplayWithMaxWidth(
it,
(this.width * RHS_IOTAS_ALLOCATION).toInt(),
mc.font
)
}
}
override fun init() {
val minecraft = Minecraft.getInstance()
val soundManager = minecraft.soundManager
soundManager.stop(HexSounds.CASTING_AMBIANCE.location, null)
val player = minecraft.player
if (player != null) {
this.ambianceSoundInstance = GridSoundInstance(player)
soundManager.play(this.ambianceSoundInstance!!)
}
this.calculateIotaDisplays()
}
override fun tick() {
val minecraft = Minecraft.getInstance()
val player = minecraft.player
if (player != null) {
val heldItem = player.getItemInHand(handOpenedWith)
if (heldItem.isEmpty || !heldItem.`is`(HexTags.Items.STAVES))
closeForReal()
}
}
override fun mouseClicked(mxOut: Double, myOut: Double, pButton: Int): Boolean {
if (super.mouseClicked(mxOut, myOut, pButton)) {
return true
}
val mx = Mth.clamp(mxOut, 0.0, this.width.toDouble())
val my = Mth.clamp(myOut, 0.0, this.height.toDouble())
if (this.drawState is PatternDrawState.BetweenPatterns) {
val coord = this.pxToCoord(Vec2(mx.toFloat(), my.toFloat()))
if (!this.usedSpots.contains(coord)) {
this.drawState = PatternDrawState.JustStarted(coord)
Minecraft.getInstance().soundManager.play(
SimpleSoundInstance(
HexSounds.START_PATTERN,
SoundSource.PLAYERS,
0.25f,
1f,
randSrc,
this.ambianceSoundInstance!!.x,
this.ambianceSoundInstance!!.y,
this.ambianceSoundInstance!!.z,
)
)
}
}
return false
}
override fun mouseDragged(mxOut: Double, myOut: Double, pButton: Int, pDragX: Double, pDragY: Double): Boolean {
if (super.mouseDragged(mxOut, myOut, pButton, pDragX, pDragY)) {
return true
}
val mx = Mth.clamp(mxOut, 0.0, this.width.toDouble())
val my = Mth.clamp(myOut, 0.0, this.height.toDouble())
val anchorCoord = when (this.drawState) {
PatternDrawState.BetweenPatterns -> null
is PatternDrawState.JustStarted -> (this.drawState as PatternDrawState.JustStarted).start
is PatternDrawState.Drawing -> (this.drawState as PatternDrawState.Drawing).current
}
if (anchorCoord != null) {
val anchor = this.coordToPx(anchorCoord)
val mouse = Vec2(mx.toFloat(), my.toFloat())
val snapDist =
this.hexSize() * this.hexSize() * 2.0 * Mth.clamp(HexConfig.client().gridSnapThreshold(), 0.5, 1.0)
if (anchor.distanceToSqr(mouse) >= snapDist) {
val delta = mouse.add(anchor.negated())
val angle = atan2(delta.y, delta.x)
// 0 is right, increases clockwise(?)
val snappedAngle = angle.div(Mth.TWO_PI).mod(6.0f)
val newdir = HexDir.values()[(snappedAngle.times(6).roundToInt() + 1).mod(6)]
// The player might have a lousy aim, so set the new anchor point to the "ideal"
// location as if they had hit it exactly on the nose.
val idealNextLoc = anchorCoord + newdir
var playSound = false
if (!this.usedSpots.contains(idealNextLoc)) {
if (this.drawState is PatternDrawState.JustStarted) {
val pat = HexPattern(newdir)
this.drawState = PatternDrawState.Drawing(anchorCoord, idealNextLoc, pat)
playSound = true
} else if (this.drawState is PatternDrawState.Drawing) {
// how anyone gets around without a borrowck is beyond me
val ds = (this.drawState as PatternDrawState.Drawing)
val lastDir = ds.wipPattern.finalDir()
if (newdir == lastDir.rotatedBy(HexAngle.BACK)) {
// We're diametrically opposite! Do a backtrack
if (ds.wipPattern.angles.isEmpty()) {
this.drawState = PatternDrawState.JustStarted(ds.current + newdir)
} else {
ds.current += newdir
ds.wipPattern.angles.removeLast()
}
playSound = true
} else {
val success = ds.wipPattern.tryAppendDir(newdir)
if (success) {
ds.current = idealNextLoc
}
playSound = success
}
}
}
if (playSound) {
Minecraft.getInstance().soundManager.play(
SimpleSoundInstance(
HexSounds.ADD_TO_PATTERN,
SoundSource.PLAYERS,
0.25f,
1f + (Math.random().toFloat() - 0.5f) * 0.1f,
randSrc,
this.ambianceSoundInstance!!.x,
this.ambianceSoundInstance!!.y,
this.ambianceSoundInstance!!.z,
)
)
}
}
}
return false
}
override fun mouseReleased(mx: Double, my: Double, pButton: Int): Boolean {
if (super.mouseReleased(mx, my, pButton)) {
return true
}
when (this.drawState) {
PatternDrawState.BetweenPatterns -> {}
is PatternDrawState.JustStarted -> {
// Well, we never managed to get anything on the stack this go-around.
this.drawState = PatternDrawState.BetweenPatterns
}
is PatternDrawState.Drawing -> {
val (start, _, pat) = this.drawState as PatternDrawState.Drawing
this.drawState = PatternDrawState.BetweenPatterns
this.patterns.add(ResolvedPattern(pat, start, ResolvedPatternType.UNRESOLVED))
this.usedSpots.addAll(pat.positions(start))
IClientXplatAbstractions.INSTANCE.sendPacketToServer(
MsgNewSpellPatternC2S(
this.handOpenedWith,
pat,
this.patterns
)
)
}
}
return false
}
override fun mouseScrolled(pMouseX: Double, pMouseY: Double, pDelta: Double): Boolean {
super.mouseScrolled(pMouseX, pMouseY, pDelta)
val mouseHandler = Minecraft.getInstance().mouseHandler
if (mouseHandler.accumulatedScroll != 0.0 && sign(pDelta) != sign(mouseHandler.accumulatedScroll)) {
mouseHandler.accumulatedScroll = 0.0
}
mouseHandler.accumulatedScroll += pDelta
val accumulation: Int = mouseHandler.accumulatedScroll.toInt()
if (accumulation == 0) {
return true
}
mouseHandler.accumulatedScroll -= accumulation.toDouble()
ShiftScrollListener.onScroll(pDelta, false)
return true
}
override fun onClose() {
if (drawState == PatternDrawState.BetweenPatterns)
closeForReal()
else
drawState = PatternDrawState.BetweenPatterns
}
fun closeForReal() {
Minecraft.getInstance().soundManager.stop(HexSounds.CASTING_AMBIANCE.location, null)
super.onClose()
}
override fun render(ps: PoseStack, pMouseX: Int, pMouseY: Int, pPartialTick: Float) {
super.render(ps, pMouseX, pMouseY, pPartialTick)
this.ambianceSoundInstance?.mousePosX = pMouseX / this.width.toDouble()
this.ambianceSoundInstance?.mousePosY = pMouseX / this.width.toDouble()
val mat = ps.last().pose()
val prevShader = RenderSystem.getShader()
RenderSystem.setShader(GameRenderer::getPositionColorShader)
RenderSystem.disableDepthTest()
RenderSystem.disableCull()
// Draw guide dots around the cursor
val mousePos = Vec2(pMouseX.toFloat(), pMouseY.toFloat())
// snap it to the center
val mouseCoord = this.pxToCoord(mousePos)
val radius = 3
for (dotCoord in mouseCoord.rangeAround(radius)) {
if (!this.usedSpots.contains(dotCoord)) {
val dotPx = this.coordToPx(dotCoord)
val delta = dotPx.add(mousePos.negated()).length()
// when right on top of the cursor, 1.0
// when at the full radius, 0! this is so we don't have dots suddenly appear/disappear.
// we subtract size from delta so there's a little "island" of 100% bright points by the mouse
val scaledDist = Mth.clamp(
1.0f - ((delta - this.hexSize()) / (radius.toFloat() * this.hexSize())),
0f,
1f
)
drawSpot(
mat,
dotPx,
scaledDist * 2f,
Mth.lerp(scaledDist, 0.4f, 0.5f),
Mth.lerp(scaledDist, 0.8f, 1.0f),
Mth.lerp(scaledDist, 0.7f, 0.9f),
scaledDist
)
}
}
RenderSystem.defaultBlendFunc()
for ((idx, elts) in this.patterns.withIndex()) {
val (pat, origin, valid) = elts
drawPatternFromPoints(
mat,
pat.toLines(
this.hexSize(),
this.coordToPx(origin)
),
findDupIndices(pat.positions()),
true,
valid.color or (0xC8 shl 24),
valid.fadeColor or (0xC8 shl 24),
if (valid.success) 0.2f else 0.9f,
DEFAULT_READABILITY_OFFSET,
1f,
idx.toDouble()
)
}
// Now draw the currently WIP pattern
if (this.drawState !is PatternDrawState.BetweenPatterns) {
val points = mutableListOf<Vec2>()
var dupIndices: Set<Int>? = null
if (this.drawState is PatternDrawState.JustStarted) {
val ds = this.drawState as PatternDrawState.JustStarted
points.add(this.coordToPx(ds.start))
} else if (this.drawState is PatternDrawState.Drawing) {
val ds = this.drawState as PatternDrawState.Drawing
dupIndices = findDupIndices(ds.wipPattern.positions())
for (pos in ds.wipPattern.positions()) {
val pix = this.coordToPx(pos + ds.start)
points.add(pix)
}
}
points.add(mousePos)
// Use the size of the patterns as the seed so that way when this one is added the zappies don't jump
drawPatternFromPoints(mat,
points,
dupIndices,
false,
0xff_64c8ff_u.toInt(),
0xff_fecbe6_u.toInt(),
0.1f,
DEFAULT_READABILITY_OFFSET,
1f,
this.patterns.size.toDouble())
}
RenderSystem.enableDepthTest()
val mc = Minecraft.getInstance()
val font = mc.font
ps.pushPose()
ps.translate(10.0, 10.0, 0.0)
// if (this.parenCount > 0) {
// val boxHeight = (this.parenDescs.size + 1f) * 10f
// RenderSystem.setShader(GameRenderer::getPositionColorShader)
// RenderSystem.defaultBlendFunc()
// drawBox(ps, 0f, 0f, (this.width * LHS_IOTAS_ALLOCATION + 5).toFloat(), boxHeight, 7.5f)
// ps.translate(0.0, 0.0, 1.0)
//
// val time = ClientTickCounter.getTotal() * 0.16f
// val opacity = (Mth.map(cos(time), -1f, 1f, 200f, 255f)).toInt()
// val color = 0x00_ffffff or (opacity shl 24)
// RenderSystem.setShader { prevShader }
// for (desc in this.parenDescs) {
// font.draw(ps, desc, 10f, 7f, color)
// ps.translate(0.0, 10.0, 0.0)
// }
// ps.translate(0.0, 15.0, 0.0)
// }
if (this.stackDescs.isNotEmpty()) {
val boxHeight = (this.stackDescs.size + 1f) * 10f
RenderSystem.setShader(GameRenderer::getPositionColorShader)
RenderSystem.enableBlend()
drawBox(ps, 0f, 0f, (this.width * LHS_IOTAS_ALLOCATION + 5).toFloat(), boxHeight)
ps.translate(0.0, 0.0, 1.0)
RenderSystem.setShader { prevShader }
for (desc in this.stackDescs) {
font.draw(ps, desc, 5f, 7f, -1)
ps.translate(0.0, 10.0, 0.0)
}
}
ps.popPose()
if (this.ravenmind != null) {
val kotlinBad = this.ravenmind!!
ps.pushPose()
val boxHeight = 15f
val addlScale = 1.5f
ps.translate(this.width * (1.0 - RHS_IOTAS_ALLOCATION * addlScale) - 10, 10.0, 0.0)
RenderSystem.setShader(GameRenderer::getPositionColorShader)
RenderSystem.enableBlend()
drawBox(
ps, 0f, 0f,
(this.width * RHS_IOTAS_ALLOCATION * addlScale).toFloat(), boxHeight * addlScale,
)
ps.translate(5.0, 5.0, 1.0)
ps.scale(addlScale, addlScale, 1f)
val time = ClientTickCounter.getTotal() * 0.2f
val opacity = (Mth.map(sin(time), -1f, 1f, 150f, 255f)).toInt()
val color = 0x00_ffffff or (opacity shl 24)
RenderSystem.setShader { prevShader }
font.draw(ps, kotlinBad, 0f, 0f, color)
ps.popPose()
}
RenderSystem.setShader { prevShader }
}
// why the hell is this default true
override fun isPauseScreen(): Boolean = false
/** Distance between adjacent hex centers */
fun hexSize(): Float {
val scaleModifier = Minecraft.getInstance().player!!.getAttributeValue(HexAttributes.GRID_ZOOM)
// Originally, we allowed 32 dots across. Assuming a 1920x1080 screen this allowed like 500-odd area.
// Let's be generous and give them 512.
val baseScale = sqrt(this.width.toDouble() * this.height / 512.0)
return (baseScale / scaleModifier).toFloat()
}
fun coordsOffset(): Vec2 = Vec2(this.width.toFloat() * 0.5f, this.height.toFloat() * 0.5f)
fun coordToPx(coord: HexCoord) =
at.petrak.hexcasting.api.utils.coordToPx(coord, this.hexSize(), this.coordsOffset())
fun pxToCoord(px: Vec2) = at.petrak.hexcasting.api.utils.pxToCoord(px, this.hexSize(), this.coordsOffset())
private sealed class PatternDrawState {
/** We're waiting on the player to right-click again */
object BetweenPatterns : PatternDrawState()
/** We just started drawing and haven't drawn the first line yet. */
data class JustStarted(val start: HexCoord) : PatternDrawState()
/** We've started drawing a pattern for real. */
data class Drawing(val start: HexCoord, var current: HexCoord, val wipPattern: HexPattern) : PatternDrawState()
}
companion object {
const val LHS_IOTAS_ALLOCATION = 0.7
const val RHS_IOTAS_ALLOCATION = 0.15
fun drawBox(ps: PoseStack, x: Float, y: Float, w: Float, h: Float, leftMargin: Float = 2.5f) {
RenderSystem.setShader(GameRenderer::getPositionColorShader)
RenderSystem.enableBlend()
renderQuad(ps, x, y, w, h, 0x50_303030)
renderQuad(ps, x + leftMargin, y + 2.5f, w - leftMargin - 2.5f, h - 5f, 0x50_303030)
}
}
}