diff --git a/src/main/java/me/alegian/thavma/impl/client/event/T7ClientModEvents.kt b/src/main/java/me/alegian/thavma/impl/client/event/T7ClientModEvents.kt index 0b2be5fb..d3c04729 100644 --- a/src/main/java/me/alegian/thavma/impl/client/event/T7ClientModEvents.kt +++ b/src/main/java/me/alegian/thavma/impl/client/event/T7ClientModEvents.kt @@ -7,8 +7,7 @@ import me.alegian.thavma.impl.client.extension.WandItemExtensions import me.alegian.thavma.impl.client.gui.WorkbenchScreen import me.alegian.thavma.impl.client.gui.book.CraftingPageRenderer import me.alegian.thavma.impl.client.gui.book.TextPageRenderer -import me.alegian.thavma.impl.client.gui.layer.ArcaneLensLayer -import me.alegian.thavma.impl.client.gui.layer.WandLayer +import me.alegian.thavma.impl.client.gui.layer.* import me.alegian.thavma.impl.client.gui.research_table.ResearchScreen import me.alegian.thavma.impl.client.gui.tooltip.AspectClientTooltipComponent import me.alegian.thavma.impl.client.gui.tooltip.AspectTooltipComponent @@ -51,6 +50,9 @@ private fun clientSetup(event: FMLClientSetupEvent) { private fun registerGuiLayers(event: RegisterGuiLayersEvent) { event.registerAboveAll(rl("vis"), WandLayer) event.registerAboveAll(rl("arcane_lens"), ArcaneLensLayer) + event.registerAboveAll(rl("regular_notifications"), RegularNotifLayer::render) + event.registerAboveAll(rl("priority_notifications"), PriorityNotifLayer::render) + event.registerAboveAll(rl("notification_animations"), NotifAnimationLayer::render) } private fun registerEntityRenderers(event: RegisterRenderers) { diff --git a/src/main/java/me/alegian/thavma/impl/client/gui/book/BookScreen.kt b/src/main/java/me/alegian/thavma/impl/client/gui/book/BookScreen.kt index 6099f4bd..dc9e9819 100644 --- a/src/main/java/me/alegian/thavma/impl/client/gui/book/BookScreen.kt +++ b/src/main/java/me/alegian/thavma/impl/client/gui/book/BookScreen.kt @@ -30,7 +30,7 @@ class BookScreen : Screen(Component.literal("book")) { selectorOffset = cornerHeight + selectorGap val categoryRegistry = clientRegistry(T7DatapackRegistries.RESEARCH_CATEGORY) - currentCategory = categoryRegistry?.getOrThrow(ResearchCategories.THAVMA) + currentCategory = categoryRegistry?.getOrThrow(ResearchCategories.STORY) categoryRegistry?.forEach { tabs[it] = addRenderableOnly(TabRenderable(this)) } diff --git a/src/main/java/me/alegian/thavma/impl/client/gui/book/EntryScreen.kt b/src/main/java/me/alegian/thavma/impl/client/gui/book/EntryScreen.kt index 12eb73e2..a1d258bb 100644 --- a/src/main/java/me/alegian/thavma/impl/client/gui/book/EntryScreen.kt +++ b/src/main/java/me/alegian/thavma/impl/client/gui/book/EntryScreen.kt @@ -1,9 +1,11 @@ package me.alegian.thavma.impl.client.gui.book +import me.alegian.thavma.impl.client.gui.layer.PlayerNotifications import me.alegian.thavma.impl.client.gui.layout.* import me.alegian.thavma.impl.client.texture.Texture import me.alegian.thavma.impl.common.book.Page import me.alegian.thavma.impl.common.research.ResearchEntry +import net.minecraft.client.Minecraft import net.minecraft.client.gui.GuiGraphics import net.minecraft.client.gui.screens.Screen import net.minecraft.core.Holder @@ -14,6 +16,7 @@ class EntryScreen(private val entry: Holder) : Screen(Component.l private val BG = Texture("gui/book/background", 510, 282, 512, 512) } + val player = Minecraft.getInstance().player private var currentPage = 0 override fun init() { @@ -47,6 +50,12 @@ class EntryScreen(private val entry: Holder) : Screen(Component.l addRenderableWidget(PageTurningWidget(position, false) { // rerender the screen for the new page(s) turnPage(false) + if (player != null) PlayerNotifications.add( + false, + Component.literal("You turned the page to the left, honestly we've got a bookworm right here!"), + color = 0xFFFFFF, + player = player + ) }) } } @@ -66,6 +75,12 @@ class EntryScreen(private val entry: Holder) : Screen(Component.l addRenderableWidget(PageTurningWidget(position, true) { // rerender the screen for the new page(s) turnPage(true) + if (player != null) PlayerNotifications.add( + false, + Component.literal("You turned the page to the right you are so good!"), + color = 0xFFFFFF, + player = player + ) }) } } diff --git a/src/main/java/me/alegian/thavma/impl/client/gui/layer/NotifAnimationLayer.kt b/src/main/java/me/alegian/thavma/impl/client/gui/layer/NotifAnimationLayer.kt new file mode 100644 index 00000000..5b1fb9b2 --- /dev/null +++ b/src/main/java/me/alegian/thavma/impl/client/gui/layer/NotifAnimationLayer.kt @@ -0,0 +1,248 @@ +package me.alegian.thavma.impl.client.gui.layer + +import com.mojang.blaze3d.systems.RenderSystem +import me.alegian.thavma.impl.client.gui.layer.PriorityNotifLayer.FADE_IN_PRIO +import me.alegian.thavma.impl.client.gui.layer.PriorityNotifLayer.STATIC_DELAY_PRIO +import me.alegian.thavma.impl.client.gui.layer.PriorityNotifLayer.animationOpacity +import me.alegian.thavma.impl.client.gui.layer.PriorityNotifLayer.animationStart +import me.alegian.thavma.impl.client.texture.Texture +import net.minecraft.client.DeltaTracker +import net.minecraft.client.Minecraft +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.client.gui.LayeredDraw +import net.minecraft.resources.ResourceLocation +import net.neoforged.api.distmarker.Dist +import net.neoforged.api.distmarker.OnlyIn +import kotlin.math.abs +import kotlin.math.pow + +@OnlyIn(Dist.CLIENT) +object NotifAnimationLayer : LayeredDraw.Layer { + + val octSheet = Texture("layer/octant_spritesheet", 400, 485, 400, 8245) + val shoeSheet = Texture("layer/horseshoe_spritesheet", 450, 470, 450, 5170) + val fusedSymbol = Texture("layer/combined", 450, 470, 450, 470) + val coloursOnly = Texture("layer/colours_only", 450, 470, 450, 470) + + // ── Position (fractions of screen dimensions — easy to read and tune) ── + // 0 = top of screen, 1 = bottom + private const val SYMBOL_Y_FRAC = 0.625f + + // Distance where each symbol ends up as a fraction of screen width + private const val MAX_SEPARATION_FRAC = 0.10f + + // How fast sprites fade in/out before/after text starts/finishes scrolling + const val ANIMATION_SPEED = 3.0f + + // ── Sprite geometry ──────────────────────────────────────────────────── + // Native texture size of each symbol in its sprite sheet + private val OCT_ORIG_WIDTH = octSheet.width + private val OCT_ORIG_HEIGHT = octSheet.height + private val SHOE_ORIG_WIDTH = shoeSheet.width + private val SHOE_ORIG_HEIGHT = shoeSheet.height + + // How many screen pixels each texture pixel maps to + private const val SYMBOL_RENDER_SCALE = 0.075f + private val OCT_DISPLAY_WIDTH get() = OCT_ORIG_WIDTH * SYMBOL_RENDER_SCALE + private val OCT_DISPLAY_HEIGHT get() = OCT_ORIG_HEIGHT * SYMBOL_RENDER_SCALE + private val SHOE_DISPLAY_WIDTH get() = SHOE_ORIG_WIDTH * SYMBOL_RENDER_SCALE + private val SHOE_DISPLAY_HEIGHT get() = SHOE_ORIG_HEIGHT * SYMBOL_RENDER_SCALE + + val timeLimit = (FADE_IN_PRIO + STATIC_DELAY_PRIO).toFloat() + val fadeInLength = timeLimit / ANIMATION_SPEED // ≈ 33 ticks + val splitLength = timeLimit - fadeInLength + + override fun render( + graphics: GuiGraphics, + deltaTracker: DeltaTracker + ) { + val player = Minecraft.getInstance().player ?: return + val currentTime = player.level().gameTime + + val scaledWidth = graphics.guiWidth() + val scaledHeight = graphics.guiHeight() + val centerXoct = scaledWidth / 2f - OCT_DISPLAY_WIDTH / 2f + val centerYoct = scaledHeight * SYMBOL_Y_FRAC - SHOE_DISPLAY_HEIGHT / 2f + val centerXshoe = scaledWidth / 2f - SHOE_DISPLAY_WIDTH / 2f + val centerYshoe = scaledHeight * SYMBOL_Y_FRAC - SHOE_DISPLAY_HEIGHT / 2f + + // when "a" first priority notification appears, play intro + if (PriorityNotifLayer.shouldPlayIntro) { + if (animationOpacity < 1f) { + renderSystemStart() + RenderSystem.setShaderColor(1f, 1f, 1f, animationOpacity) + + // Symbols fade in as one joined sprite + graphics.blit( + fusedSymbol.location, + centerXshoe.toInt(), centerYshoe.toInt(), + SHOE_DISPLAY_WIDTH.toInt(), SHOE_DISPLAY_HEIGHT.toInt(), + 0f, 0f, + SHOE_ORIG_WIDTH, SHOE_ORIG_HEIGHT, + shoeSheet.width, shoeSheet.height + ) + + RenderSystem.disableBlend() + } else { + renderSystemStart() + + // Raw linear progress through the split phase, 0 → 1 + val rawT = ((currentTime - animationStart).toFloat() - fadeInLength) + .coerceAtLeast(0f) / splitLength + val easedT = easeOutSex(rawT.coerceIn(0f, 1f)) + + val separationOct = (scaledWidth * MAX_SEPARATION_FRAC * (1 - easedT)) + val separationShoe = ((scaledWidth - SHOE_DISPLAY_WIDTH) * (1 - MAX_SEPARATION_FRAC) * (1 - easedT)) + + RenderSystem.setShaderColor(1f, 1f, 1f, 1 - easeInOutCubic(rawT)) + + // Horseshoe moves right from centre + blitSprite( + graphics, + shoeSheet.location, + centerXshoe.toInt() * easedT + separationShoe, + centerYshoe.toInt(), + rawT, false + ) + + // Octant moves left from centre + blitSprite( + graphics, + octSheet.location, + centerXoct.toInt() * easedT + separationOct, + centerYoct.toInt(), + rawT, true + ) + } + + RenderSystem.setShaderColor(1f, 1f, 1f, 1f) + RenderSystem.disableBlend() + } + + // when the last line of priority notifications is gone, play outro + if (PriorityNotifLayer.shouldPlayOutro && !PriorityNotifLayer.shouldPlayIntro) { + renderSystemStart() + + // Raw linear progress through the split phase, 0 → 1 + val rawT = ((currentTime - PriorityNotifLayer.endCheckpoint).toFloat()) + .coerceAtLeast(0f) / splitLength + val easedT = easeOutQuadratic(rawT.coerceIn(0f, 1f)) + + val separationOct = (scaledWidth * MAX_SEPARATION_FRAC * easedT) + val separationShoe = (scaledWidth * (1 - MAX_SEPARATION_FRAC) * easedT) + + RenderSystem.setShaderColor(1f, 1f, 1f, rawT) + + if (currentTime - PriorityNotifLayer.endCheckpoint <= splitLength) { + + // Horseshoe moves left from edge toward centre + blitSprite( + graphics, shoeSheet.location, + centerXshoe.toInt() * (1 - easedT) + separationShoe, + centerYshoe.toInt(), rawT, false + ) + + // Octant moves right from edge toward centre + blitSprite( + graphics, octSheet.location, + centerXoct.toInt() * (1 - easedT) + separationOct, + centerYoct.toInt(), rawT, true + ) + } + RenderSystem.setShaderColor(1f, 1f, 1f, 1f) + RenderSystem.disableBlend() + + if (currentTime - PriorityNotifLayer.endCheckpoint >= splitLength) { + renderSystemStart() + val alpha = (currentTime - PriorityNotifLayer.endCheckpoint - splitLength) / fadeInLength + + RenderSystem.setShaderColor( + 1f, + 1f, + 1f, + 1f - 1.5f * alpha.coerceIn(0f, 1f) + ) + + // Symbols fade out as one joined sprite + graphics.blit( + fusedSymbol.location, + centerXshoe.toInt(), centerYshoe.toInt(), + SHOE_DISPLAY_WIDTH.toInt(), SHOE_DISPLAY_HEIGHT.toInt(), + 0f, 0f, + SHOE_ORIG_WIDTH, SHOE_ORIG_HEIGHT, + shoeSheet.width, shoeSheet.height + ) + + RenderSystem.setShaderColor( + 1f, + 1f, + 1f, + if (alpha <= 0.5f) 2 * alpha.coerceIn(0f, 1f) else 2 * (1 - alpha.coerceIn(0f, 1f)) + ) + + // Coloured highlights blink shortly + graphics.blit( + coloursOnly.location, + centerXshoe.toInt(), centerYshoe.toInt(), + SHOE_DISPLAY_WIDTH.toInt(), SHOE_DISPLAY_HEIGHT.toInt(), + 0f, 0f, + SHOE_ORIG_WIDTH, SHOE_ORIG_HEIGHT, + shoeSheet.width, shoeSheet.height + ) + + RenderSystem.disableBlend() + } + } + RenderSystem.setShaderColor(1f, 1f, 1f, 1f) + RenderSystem.disableBlend() + if (currentTime - PriorityNotifLayer.endCheckpoint > timeLimit) PriorityNotifLayer.shouldPlayOutro = false + } + + private fun blitSprite( + graphics: GuiGraphics, + location: ResourceLocation, + x: Float, + y: Int, + rawT: Float, + isOctant: Boolean + ) { + if (isOctant) { + val easedQuad = easeOutQuadratic(rawT) + val pingPong = 1f - abs((easedQuad * 2f) - 1f) + val currentFrame = (pingPong * 16).toInt() + val spriteSheetOffset = 485f * (1 + currentFrame).toFloat() + + graphics.blit( + location, + x.toInt(), y, // screen position + OCT_DISPLAY_WIDTH.toInt(), OCT_DISPLAY_HEIGHT.toInt(), // how large to draw on screen (2× scale) + 0f, spriteSheetOffset, // UV origin in the texture + OCT_ORIG_WIDTH, OCT_ORIG_HEIGHT, // how many texture pixels to sample + octSheet.width, octSheet.canvasHeight // full texture dimensions + ) + } else { + val frameIndex = (rawT * 40).toInt() % 9 + 1 + val spriteSheetOffset = (SHOE_ORIG_HEIGHT * frameIndex).toFloat() + + graphics.blit( + location, + x.toInt(), y, + SHOE_DISPLAY_WIDTH.toInt(), SHOE_DISPLAY_HEIGHT.toInt(), + 0f, spriteSheetOffset, + SHOE_ORIG_WIDTH, SHOE_ORIG_HEIGHT, + shoeSheet.width, shoeSheet.canvasHeight + ) + } + } + + private fun easeOutQuadratic(t: Float) = (1f - t) * (1f - t) + private fun easeOutSex(t: Float) = (1f - t).pow(6) + private fun easeInOutCubic(t: Float) = + if (t < 0.5f) 4f * t * t * t + else 1f - (-2f * t + 2f).pow(3) / 2f + + private fun renderSystemStart() { + RenderSystem.enableBlend() + RenderSystem.defaultBlendFunc() + } +} \ No newline at end of file diff --git a/src/main/java/me/alegian/thavma/impl/client/gui/layer/PlayerNotifications.kt b/src/main/java/me/alegian/thavma/impl/client/gui/layer/PlayerNotifications.kt new file mode 100644 index 00000000..4d512ab8 --- /dev/null +++ b/src/main/java/me/alegian/thavma/impl/client/gui/layer/PlayerNotifications.kt @@ -0,0 +1,57 @@ +package me.alegian.thavma.impl.client.gui.layer + +import net.minecraft.client.player.LocalPlayer +import net.minecraft.network.chat.Component +import net.minecraft.resources.ResourceLocation +import net.minecraft.world.entity.player.Player +import net.neoforged.api.distmarker.Dist +import net.neoforged.api.distmarker.OnlyIn + +@OnlyIn(Dist.CLIENT) +object PlayerNotifications { + + const val MAX_LISTABLE_REG = Int.MAX_VALUE + const val MAX_LISTABLE_PRIO = Int.MAX_VALUE + const val FONT_SIZE_REG = 0.35f + const val FONT_SIZE_PRIO = 1f + + data class Notification( + val isPriority: Boolean, + val text: Component, + val player: LocalPlayer, + val image: ResourceLocation? = null, + val color: Int = 0xFFFFFF, + val scale: Float = if (isPriority) FONT_SIZE_PRIO else FONT_SIZE_REG, + val addedTime: Long + ) + + private val queue = mutableListOf() + + fun add( + isPriority: Boolean, + text: Component, + player: LocalPlayer, + image: ResourceLocation? = null, + color: Int = 0xFFFFFF, + scale: Float = if (isPriority) FONT_SIZE_PRIO else FONT_SIZE_REG + ) { + queue += Notification( + isPriority = isPriority, + text = text, + player = player, + image = image, + color = color, + scale = scale, + addedTime = player.level().gameTime + ) + } + + /** Non-mutating read — Priority/RegularNotifLayer decides when to clear. */ + internal fun getForPlayer(player: Player): List = + queue.filter { it.player == player } + + /** Called by Priority/RegularNotifLayer once the scroll-out animation completes. */ + internal fun clearForPlayer(player: Player, isPriority: Boolean) { + queue.removeAll { it.player == player && it.isPriority == isPriority } + } +} \ No newline at end of file diff --git a/src/main/java/me/alegian/thavma/impl/client/gui/layer/PriorityNotifLayer.kt b/src/main/java/me/alegian/thavma/impl/client/gui/layer/PriorityNotifLayer.kt new file mode 100644 index 00000000..25dc00c6 --- /dev/null +++ b/src/main/java/me/alegian/thavma/impl/client/gui/layer/PriorityNotifLayer.kt @@ -0,0 +1,146 @@ +package me.alegian.thavma.impl.client.gui.layer + +import com.mojang.blaze3d.systems.RenderSystem +import net.minecraft.client.DeltaTracker +import net.minecraft.client.Minecraft +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.client.gui.LayeredDraw +import net.neoforged.api.distmarker.Dist +import net.neoforged.api.distmarker.OnlyIn + +@OnlyIn(Dist.CLIENT) +object PriorityNotifLayer : LayeredDraw.Layer { + + const val FADE_IN_PRIO = 40L + const val STATIC_DELAY_PRIO = 60L + private const val SCROLL_SPEED_PRIO = 0.4f + private var batchStartTimePrio = -1L + private var globalScrollOffsetPrio = 0f + const val MARGIN_CONST = 2.4f + + var shouldPlayIntro = false + var shouldPlayOutro = false + var endCheckpoint = -1L + var animationOpacity = 1f + var animationStart = -1L + + override fun render(graphics: GuiGraphics, deltaTracker: DeltaTracker) { + val mc = Minecraft.getInstance() + val player = mc.player ?: return + val currentTime = player.level().gameTime + + val notifications = PlayerNotifications.getForPlayer(player).filter { it.isPriority } + + if (notifications.isEmpty()) { + batchStartTimePrio = -1L + globalScrollOffsetPrio = 0f + return + } + + val scaledWidth = mc.window.guiScaledWidth + val scaledHeight = mc.window.guiScaledHeight + val font = mc.font + + // this defines the heights at which lines of text will fade in/out + val bottomMargin = scaledHeight / MARGIN_CONST + val fadeInStart = scaledHeight * 17 / 32f + val fadeInEnd = scaledHeight * 9 / 16f + val fadeOutEnd = scaledHeight * 3 / 4f - scaledHeight / 16f + val fadeOutStart = scaledHeight * 3 / 4f - scaledHeight / 8f + + if (batchStartTimePrio < 0L) { + batchStartTimePrio = currentTime + endCheckpoint = -1L + } + + val elapsedPrio = currentTime - batchStartTimePrio + val inScrollPrio = elapsedPrio >= FADE_IN_PRIO + STATIC_DELAY_PRIO + + shouldPlayIntro = currentTime - batchStartTimePrio < FADE_IN_PRIO + STATIC_DELAY_PRIO + animationOpacity = + (NotifAnimationLayer.ANIMATION_SPEED * (currentTime - batchStartTimePrio) / (FADE_IN_PRIO + STATIC_DELAY_PRIO)).coerceIn( + 0f, + 1f + ) + animationStart = batchStartTimePrio + + val prioNotifs = notifications + .take(PlayerNotifications.MAX_LISTABLE_PRIO) + .map { n -> n to font.split(n.text, (scaledWidth * 6 / 10f / n.scale).toInt()) } + + globalScrollOffsetPrio = if (inScrollPrio) + (elapsedPrio - FADE_IN_PRIO - STATIC_DELAY_PRIO).toFloat() * SCROLL_SPEED_PRIO //* prioNotifs.first().first.scale + else 0f + + val totalHeightPrio = prioNotifs.sumOf { (n, lines) -> + ((lines.size + 1) * n.scale * (font.lineHeight - 1.0)) + }.toFloat() + + // Once the topmost line has drifted past the bottom edge, the batch is done + if (inScrollPrio && globalScrollOffsetPrio > totalHeightPrio)//-prioNotifs.first().first.scale * (font.lineHeight - 1)) + { + PlayerNotifications.clearForPlayer(player, true) + batchStartTimePrio = -1L + globalScrollOffsetPrio = 0f + endCheckpoint = currentTime + shouldPlayOutro = true + return + } + + RenderSystem.enableBlend() + RenderSystem.defaultBlendFunc() + + graphics.enableScissor(0, scaledHeight * 9 / 16 + 1, scaledWidth, scaledHeight * 3 / 4 - scaledHeight / 15) + + var pixelOffsetPrio = 0f + + prioNotifs.forEach { (notif, lines) -> + val rowHeight = notif.scale * (font.lineHeight - 1) + + lines.forEachIndexed { lineIndex, line -> + val baseY = scaledHeight.toFloat() - bottomMargin + + (pixelOffsetPrio + (lineIndex + 0.5f) * rowHeight) + val screenY = baseY - globalScrollOffsetPrio + + var alpha = 255 + when { + screenY < fadeInStart -> alpha = 0 + screenY in fadeInStart..fadeInEnd -> alpha = + (255 - (fadeInEnd - screenY) / (fadeInEnd - fadeInStart) * 255).toInt() + + screenY in fadeInEnd..fadeOutStart -> alpha = 255 + screenY in fadeOutStart..fadeOutEnd -> alpha = + ((fadeOutEnd - screenY) / (fadeOutEnd - fadeOutStart) * 255).toInt() + + screenY > fadeOutEnd -> alpha = 0 + } + + val multiplier = if (currentTime - batchStartTimePrio < FADE_IN_PRIO) + ((currentTime - notif.addedTime).toFloat() / FADE_IN_PRIO).coerceIn(0f, 1f) + else 1f + + alpha = (alpha * multiplier).toInt().coerceIn(0, 255) + + if (alpha == 0) return@forEachIndexed + + val cr = (notif.color shr 16) and 0xFF + val cg = (notif.color shr 8) and 0xFF + val cb = notif.color and 0xFF + val textArgb = ((alpha * 5 / 6) shl 24) or (cr shl 16) or (cg shl 8) or cb + val lineWidth = font.width(line) * notif.scale + + graphics.pose().pushPose() + graphics.pose().translate( + scaledWidth.toFloat() / 2f - lineWidth / 2f, screenY, 0f + ) + graphics.pose().scale(notif.scale, notif.scale, 1f) + graphics.drawString(font, line, 0, 0, textArgb, true) + graphics.pose().popPose() + } + + pixelOffsetPrio += (lines.size + 1) * rowHeight + } + graphics.disableScissor() + RenderSystem.disableBlend() + } +} \ No newline at end of file diff --git a/src/main/java/me/alegian/thavma/impl/client/gui/layer/RegularNotifLayer.kt b/src/main/java/me/alegian/thavma/impl/client/gui/layer/RegularNotifLayer.kt new file mode 100644 index 00000000..b0f38030 --- /dev/null +++ b/src/main/java/me/alegian/thavma/impl/client/gui/layer/RegularNotifLayer.kt @@ -0,0 +1,148 @@ +package me.alegian.thavma.impl.client.gui.layer + +import com.mojang.blaze3d.systems.RenderSystem +import net.minecraft.client.DeltaTracker +import net.minecraft.client.Minecraft +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.client.gui.LayeredDraw +import net.neoforged.api.distmarker.Dist +import net.neoforged.api.distmarker.OnlyIn + +@OnlyIn(Dist.CLIENT) +object RegularNotifLayer : LayeredDraw.Layer { + + //EVERYTHING HERE IS DONE TWICE, ONCE FOR REGULAR NOTIFS (BOTTOM RIGHT) + // AND ONCE FOR PRIORITY NOTIFS (CENTRE OF SCREEN) + + // ── ① Fade-in ───────────────────────────────────────────────────────────── + // How long each notification takes to reach full alpha after being added. + // Per-notification: measured from Notification.addedTime, not batchStartTime. + // Lines are stationary during this phase. + private const val FADE_IN_REG = 20L + + // ── ② Static hold ───────────────────────────────────────────────────────── + // After FADE_IN_DURATION_MS has elapsed from batchStartTime, all lines hold + // at full alpha for this long before anything moves. + // Scroll begins at: batchStartTime + FADE_IN_DURATION_MS + STATIC_DELAY_MS + private const val STATIC_DELAY_REG = 60L + + // ── ③ Scroll-out ────────────────────────────────────────────────────────── + // SCROLL_SPEED_PX_PER_MS: how fast all lines drift downward (px per ms). + // FADE_OUT_DISTANCE_PX: screen pixels before the bottom edge where alpha + // begins dropping. Fade duration in ms ≈ distance / speed. + private const val SCROLL_SPEED_REG = 0.4f + private const val FADE_DISTANCE_REG = 10f + + // How far above the screen's bottom edge lines initially rest (clears the hotbar). + private const val BOTTOM_MARGIN_REG = 15f + + private var batchStartTimeReg = -1L + private var globalScrollOffsetReg = 0f + + override fun render(graphics: GuiGraphics, deltaTracker: DeltaTracker) { + val mc = Minecraft.getInstance() + val player = mc.player ?: return + //val currentTime = Util.getMillis() + val currentTime = player.level().gameTime + + val notifications = PlayerNotifications.getForPlayer(player).filter { !it.isPriority } + + if (notifications.isEmpty()) { + batchStartTimeReg = -1L + globalScrollOffsetReg = 0f + return + } + + if (batchStartTimeReg < 0L) batchStartTimeReg = currentTime + + val scaledWidth = mc.window.guiScaledWidth + val scaledHeight = mc.window.guiScaledHeight + val font = mc.font + val elapsedReg = currentTime - batchStartTimeReg + + val inScrollReg = elapsedReg >= FADE_IN_REG + STATIC_DELAY_REG + + globalScrollOffsetReg = if (inScrollReg) + (elapsedReg - FADE_IN_REG - STATIC_DELAY_REG).toFloat() * SCROLL_SPEED_REG + else 0f + + // Split each notification into wrapped lines + val regNotifs = notifications + .take((PlayerNotifications.MAX_LISTABLE_REG / notifications.first().scale).toInt()) + .map { n -> n to font.split(n.text, (scaledWidth / 3.5f / n.scale).toInt()).reversed() } + + // Total pixel height of all rendered rows including inter-group gaps + val totalHeightReg = regNotifs.sumOf { (n, lines) -> + ((lines.size + 1) * n.scale * (font.lineHeight - 1.0)) + }.toFloat() + + // Once the topmost line has drifted past the bottom edge, the batch is done + if (inScrollReg && globalScrollOffsetReg > BOTTOM_MARGIN_REG + totalHeightReg - 20) { + PlayerNotifications.clearForPlayer(player, false) + batchStartTimeReg = -1L + globalScrollOffsetReg = 0f + return + } + + var pixelOffsetReg = 0f + + RenderSystem.enableBlend() + RenderSystem.defaultBlendFunc() + graphics.enableScissor(scaledWidth * 2 / 3, scaledHeight / 4, scaledWidth, scaledHeight) + + regNotifs.forEach { (notif, lines) -> + val rowHeight = notif.scale * font.lineHeight + + lines.forEachIndexed { lineIndex, line -> + val baseY = scaledHeight.toFloat() - BOTTOM_MARGIN_REG - + (pixelOffsetReg + lineIndex * rowHeight) + val screenY = baseY + globalScrollOffsetReg + + // ── ① Per-notification fade-in (position fixed, only alpha changes) ── + var alpha = (((currentTime - notif.addedTime).toFloat() / FADE_IN_REG).coerceIn(0f, 1f) * 255f).toInt() + + // ── ③ Scroll fade-out: alpha drops as line approaches screen bottom ── + if (inScrollReg) { + val fadeStartY = scaledHeight.toFloat() - FADE_DISTANCE_REG + if (screenY > fadeStartY) { + val fadeProgress = + ((screenY - fadeStartY) / FADE_DISTANCE_REG).coerceIn(0f, 1f) + alpha = (alpha * (1f - fadeProgress)).toInt() + } else alpha = 255 + } + + alpha = alpha.coerceIn(0, 255) + if (alpha == 0) return@forEachIndexed + + val cr = (notif.color shr 16) and 0xFF + val cg = (notif.color shr 8) and 0xFF + val cb = notif.color and 0xFF + val textArgb = ((alpha / 2) shl 24) or (cr shl 16) or (cg shl 8) or cb + val lineWidth = font.width(line) * notif.scale + + graphics.pose().pushPose() + graphics.pose().translate( + scaledWidth.toFloat() - lineWidth - 12f, screenY, 0f + ) + graphics.pose().scale(notif.scale, notif.scale, 1f) + graphics.drawString(font, line, 0, 0, textArgb, true) + graphics.pose().popPose() + + if (lineIndex == 0) { + notif.image?.let { tex -> + RenderSystem.setShaderColor( + cr / 255f, cg / 255f, cb / 255f, alpha / 511f + ) + graphics.blit(tex, scaledWidth - 18, screenY.toInt() - 6, 0, 0, 16, 16) + RenderSystem.setShaderColor(1f, 1f, 1f, 1f) + } + } + } + + pixelOffsetReg += (lines.size + 1) * rowHeight + } + + graphics.disableScissor() + RenderSystem.disableBlend() + } +} \ No newline at end of file diff --git a/src/main/java/me/alegian/thavma/impl/common/item/BookItem.kt b/src/main/java/me/alegian/thavma/impl/common/item/BookItem.kt index 7ca2c8a3..07fa5a76 100644 --- a/src/main/java/me/alegian/thavma/impl/common/item/BookItem.kt +++ b/src/main/java/me/alegian/thavma/impl/common/item/BookItem.kt @@ -2,7 +2,10 @@ package me.alegian.thavma.impl.common.item import me.alegian.thavma.impl.client.clientSound import me.alegian.thavma.impl.client.gui.book.BookScreen +import me.alegian.thavma.impl.client.gui.layer.PlayerNotifications import me.alegian.thavma.impl.client.setScreen +import net.minecraft.client.player.LocalPlayer +import net.minecraft.network.chat.Component import net.minecraft.sounds.SoundEvents import net.minecraft.sounds.SoundSource import net.minecraft.world.InteractionHand @@ -17,6 +20,12 @@ class T7BookItem : Item(Properties().stacksTo(1)) { if (level.isClientSide()) { setScreen(BookScreen()) clientSound(SoundEvents.BOOK_PAGE_TURN, SoundSource.AMBIENT, 1f, 1f) + PlayerNotifications.add( + true, + Component.literal("You have successfully opened the book! I am so proud of you!"), + color = 0xFFFFFF, + player = player as LocalPlayer + ) } return InteractionResultHolder.consume(player.getItemInHand(hand)) diff --git a/src/main/java/me/alegian/thavma/impl/common/scanning/ScanHelper.kt b/src/main/java/me/alegian/thavma/impl/common/scanning/ScanHelper.kt index d63789c5..106c5edf 100644 --- a/src/main/java/me/alegian/thavma/impl/common/scanning/ScanHelper.kt +++ b/src/main/java/me/alegian/thavma/impl/common/scanning/ScanHelper.kt @@ -18,7 +18,6 @@ import net.minecraft.world.entity.EntityType import net.minecraft.world.entity.item.ItemEntity import net.minecraft.world.entity.player.Player import net.minecraft.world.entity.projectile.ProjectileUtil -import net.minecraft.world.item.Item import net.minecraft.world.item.ItemStack import net.minecraft.world.level.ClipContext import net.minecraft.world.level.block.state.BlockState diff --git a/src/main/resources/assets/thavma/textures/layer/colours_only.png b/src/main/resources/assets/thavma/textures/layer/colours_only.png new file mode 100644 index 00000000..7770cfd9 Binary files /dev/null and b/src/main/resources/assets/thavma/textures/layer/colours_only.png differ diff --git a/src/main/resources/assets/thavma/textures/layer/combined.png b/src/main/resources/assets/thavma/textures/layer/combined.png new file mode 100644 index 00000000..6d347f3e Binary files /dev/null and b/src/main/resources/assets/thavma/textures/layer/combined.png differ diff --git a/src/main/resources/assets/thavma/textures/layer/horseshoe_spritesheet.png b/src/main/resources/assets/thavma/textures/layer/horseshoe_spritesheet.png new file mode 100644 index 00000000..90d10e7a Binary files /dev/null and b/src/main/resources/assets/thavma/textures/layer/horseshoe_spritesheet.png differ diff --git a/src/main/resources/assets/thavma/textures/layer/octant_spritesheet.png b/src/main/resources/assets/thavma/textures/layer/octant_spritesheet.png new file mode 100644 index 00000000..f0855ebb Binary files /dev/null and b/src/main/resources/assets/thavma/textures/layer/octant_spritesheet.png differ