diff options
Diffstat (limited to 'src/main/kotlin/features/items')
16 files changed, 1489 insertions, 0 deletions
diff --git a/src/main/kotlin/features/items/BlockZapperOverlay.kt b/src/main/kotlin/features/items/BlockZapperOverlay.kt new file mode 100644 index 0000000..cc58f8a --- /dev/null +++ b/src/main/kotlin/features/items/BlockZapperOverlay.kt @@ -0,0 +1,139 @@ +package moe.nea.firmament.features.items + +import io.github.notenoughupdates.moulconfig.ChromaColour +import java.util.LinkedList +import net.minecraft.world.level.block.Block +import net.minecraft.world.level.block.state.BlockState +import net.minecraft.world.level.block.Blocks +import net.minecraft.world.phys.BlockHitResult +import net.minecraft.world.phys.HitResult +import net.minecraft.core.BlockPos +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.WorldKeyboardEvent +import moe.nea.firmament.events.WorldRenderLastEvent +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.data.Config +import moe.nea.firmament.util.data.ManagedConfig +import moe.nea.firmament.util.render.RenderInWorldContext +import moe.nea.firmament.util.skyBlockId +import moe.nea.firmament.util.skyblock.SkyBlockItems + +object BlockZapperOverlay { + val identifier: String + get() = "block-zapper-overlay" + + @Config + object TConfig : ManagedConfig(identifier, Category.ITEMS) { + var blockZapperOverlay by toggle("block-zapper-overlay") { false } + val color by colour("color") { ChromaColour.fromStaticRGB(160, 0, 0, 60) } + var undoKey by keyBindingWithDefaultUnbound("undo-key") + } + + val bannedZapper: List<Block> = listOf<Block>( + Blocks.WHEAT, + Blocks.CARROTS, + Blocks.POTATOES, + Blocks.PUMPKIN, + Blocks.PUMPKIN_STEM, + Blocks.MELON, + Blocks.MELON_STEM, + Blocks.CACTUS, + Blocks.SUGAR_CANE, + Blocks.NETHER_WART, + Blocks.TALL_GRASS, + Blocks.SUNFLOWER, + Blocks.FARMLAND, + Blocks.BREWING_STAND, + Blocks.SNOW, + Blocks.RED_MUSHROOM, + Blocks.BROWN_MUSHROOM, + ) + + private val zapperOffsets: List<BlockPos> = listOf( + BlockPos(0, 0, -1), + BlockPos(0, 0, 1), + BlockPos(-1, 0, 0), + BlockPos(1, 0, 0), + BlockPos(0, 1, 0), + BlockPos(0, -1, 0) + ) + + // Skidded from NEU + // Credit: https://github.com/NotEnoughUpdates/NotEnoughUpdates/blob/9b1fcfebc646e9fb69f99006327faa3e734e5f51/src/main/java/io/github/moulberry/notenoughupdates/miscfeatures/CustomItemEffects.java#L1281-L1355 (Modified) + @Subscribe + fun renderBlockZapperOverlay(event: WorldRenderLastEvent) { + if (!TConfig.blockZapperOverlay) return + val player = MC.player ?: return + val world = player.level ?: return + val heldItem = MC.stackInHand + if (heldItem.skyBlockId != SkyBlockItems.BLOCK_ZAPPER) return + val hitResult = MC.instance.hitResult ?: return + + val zapperBlocks: HashSet<BlockPos> = HashSet() + val returnablePositions = LinkedList<BlockPos>() + + if (hitResult is BlockHitResult && hitResult.type == HitResult.Type.BLOCK) { + var pos: BlockPos = hitResult.blockPos + val firstBlockState: BlockState = world.getBlockState(pos) + val block = firstBlockState.block + + val initialAboveBlock = world.getBlockState(pos.above()).block + if (!bannedZapper.contains(initialAboveBlock) && !bannedZapper.contains(block)) { + var i = 0 + while (i < 164) { + zapperBlocks.add(pos) + returnablePositions.remove(pos) + + val availableNeighbors: MutableList<BlockPos> = ArrayList() + + for (offset in zapperOffsets) { + val newPos = pos.offset(offset) + + if (zapperBlocks.contains(newPos)) continue + + val state: BlockState? = world.getBlockState(newPos) + if (state != null && state.block === block) { + val above = newPos.above() + val aboveBlock = world.getBlockState(above).block + if (!bannedZapper.contains(aboveBlock)) { + availableNeighbors.add(newPos) + } + } + } + + if (availableNeighbors.size >= 2) { + returnablePositions.add(pos) + pos = availableNeighbors[0] + } else if (availableNeighbors.size == 1) { + pos = availableNeighbors[0] + } else if (returnablePositions.isEmpty()) { + break + } else { + i-- + pos = returnablePositions.last() + } + + i++ + } + } + + RenderInWorldContext.renderInWorld(event) { + if (MC.player?.isShiftKeyDown ?: false) { + zapperBlocks.forEach { + block(it, TConfig.color.getEffectiveColourRGB()) + } + } else { + sharedVoxelSurface(zapperBlocks, TConfig.color.getEffectiveColourRGB()) + } + } + } + } + + @Subscribe + fun onWorldKeyboard(it: WorldKeyboardEvent) { + if (!TConfig.undoKey.isBound) return + if (!it.matches(TConfig.undoKey)) return + if (MC.stackInHand.skyBlockId != SkyBlockItems.BLOCK_ZAPPER) return + MC.sendCommand("undozap") + } +} diff --git a/src/main/kotlin/features/items/BonemerangOverlay.kt b/src/main/kotlin/features/items/BonemerangOverlay.kt new file mode 100644 index 0000000..3f16922 --- /dev/null +++ b/src/main/kotlin/features/items/BonemerangOverlay.kt @@ -0,0 +1,94 @@ +package moe.nea.firmament.features.items + +import me.shedaniel.math.Color +import org.joml.Vector2i +import net.minecraft.world.entity.LivingEntity +import net.minecraft.world.entity.decoration.ArmorStand +import net.minecraft.world.entity.player.Player +import net.minecraft.ChatFormatting +import net.minecraft.world.phys.AABB +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.EntityRenderTintEvent +import moe.nea.firmament.events.HudRenderEvent +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.data.Config +import moe.nea.firmament.util.data.ManagedConfig +import moe.nea.firmament.util.render.TintedOverlayTexture +import moe.nea.firmament.util.skyBlockId +import moe.nea.firmament.util.skyblock.SkyBlockItems +import moe.nea.firmament.util.tr + +object BonemerangOverlay { + val identifier: String + get() = "bonemerang-overlay" + + @Config + object TConfig : ManagedConfig(identifier, Category.ITEMS) { + var bonemerangOverlay by toggle("bonemerang-overlay") { false } + val bonemerangOverlayHud by position("bonemerang-overlay-hud", 80, 10) { Vector2i() } + var highlightHitEntities by toggle("highlight-hit-entities") { false } + } + + fun getEntities(): MutableSet<LivingEntity> { + val entities = mutableSetOf<LivingEntity>() + val camera = MC.camera as? Player ?: return entities + val player = MC.player ?: return entities + val world = player.level ?: return entities + + val cameraPos = camera.eyePosition + val rayDirection = camera.lookAngle.normalize() + val endPos = cameraPos.add(rayDirection.scale(15.0)) + val foundEntities = world.getEntities(camera, AABB(cameraPos, endPos).inflate(1.0)) + + for (entity in foundEntities) { + if (entity !is LivingEntity || entity is ArmorStand || entity.isInvisible) continue + val hitResult = entity.boundingBox.inflate(0.35).clip(cameraPos, endPos).orElse(null) + if (hitResult != null) entities.add(entity) + } + + return entities + } + + + val throwableWeapons = listOf( + SkyBlockItems.BONE_BOOMERANG, SkyBlockItems.STARRED_BONE_BOOMERANG, + SkyBlockItems.TRIBAL_SPEAR, + ) + + + @Subscribe + fun onEntityRender(event: EntityRenderTintEvent) { + if (!TConfig.highlightHitEntities) return + if (MC.stackInHand.skyBlockId !in throwableWeapons) return + + val entities = getEntities() + if (entities.isEmpty()) return + if (event.entity !in entities) return + + val tintOverlay by lazy { + TintedOverlayTexture().setColor(Color.ofOpaque(ChatFormatting.BLUE.color!!)) + } + + event.renderState.overlayTexture_firmament = tintOverlay + } + + + @Subscribe + fun onRenderHud(it: HudRenderEvent) { + if (!TConfig.bonemerangOverlay) return + if (MC.stackInHand.skyBlockId !in throwableWeapons) return + + val entities = getEntities() + + it.context.pose().pushMatrix() + TConfig.bonemerangOverlayHud.applyTransformations(it.context.pose()) + it.context.drawString( + MC.font, String.format( + tr( + "firmament.bonemerang-overlay.bonemerang-overlay.display", "Bonemerang Targets: %s" + ).string, entities.size + ), 0, 0, -1, true + ) + it.context.pose().popMatrix() + } +} diff --git a/src/main/kotlin/features/items/EtherwarpOverlay.kt b/src/main/kotlin/features/items/EtherwarpOverlay.kt new file mode 100644 index 0000000..a59fcbd --- /dev/null +++ b/src/main/kotlin/features/items/EtherwarpOverlay.kt @@ -0,0 +1,234 @@ +package moe.nea.firmament.features.items + +import io.github.notenoughupdates.moulconfig.ChromaColour +import net.minecraft.world.level.block.Blocks +import net.minecraft.core.Holder +import net.minecraft.tags.BlockTags +import net.minecraft.tags.TagKey +import net.minecraft.network.chat.Component +import net.minecraft.world.phys.BlockHitResult +import net.minecraft.world.phys.HitResult +import net.minecraft.core.BlockPos +import net.minecraft.world.phys.Vec3 +import net.minecraft.world.phys.shapes.Shapes +import net.minecraft.world.level.BlockGetter +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.WorldRenderLastEvent +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.SBData +import moe.nea.firmament.util.data.Config +import moe.nea.firmament.util.data.ManagedConfig +import moe.nea.firmament.util.extraAttributes +import moe.nea.firmament.util.render.RenderInWorldContext +import moe.nea.firmament.util.skyBlockId +import moe.nea.firmament.util.skyblock.SkyBlockItems +import moe.nea.firmament.util.tr + +object EtherwarpOverlay { + val identifier: String + get() = "etherwarp-overlay" + + @Config + object TConfig : ManagedConfig(identifier, Category.ITEMS) { + var etherwarpOverlay by toggle("etherwarp-overlay") { false } + var onlyShowWhileSneaking by toggle("only-show-while-sneaking") { true } + var cube by toggle("cube") { true } + val cubeColour by colour("cube-colour") { ChromaColour.fromStaticRGB(172, 0, 255, 60) } + val failureCubeColour by colour("cube-colour-fail") { ChromaColour.fromStaticRGB(255, 0, 172, 60) } + val tooCloseCubeColour by colour("cube-colour-tooclose") { ChromaColour.fromStaticRGB(0, 255, 0, 60) } + val tooFarCubeColour by colour("cube-colour-toofar") { ChromaColour.fromStaticRGB(255, 255, 0, 60) } + var wireframe by toggle("wireframe") { false } + var failureText by toggle("failure-text") { false } + } + + enum class EtherwarpResult(val label: Component?, val color: () -> ChromaColour) { + SUCCESS(null, TConfig::cubeColour), + INTERACTION_BLOCKED( + tr("firmament.etherwarp.fail.tooclosetointeractable", "Too close to interactable"), + TConfig::tooCloseCubeColour + ), + TOO_DISTANT(tr("firmament.etherwarp.fail.toofar", "Too far away"), TConfig::tooFarCubeColour), + OCCUPIED(tr("firmament.etherwarp.fail.occupied", "Occupied"), TConfig::failureCubeColour), + } + + val interactionBlocked = Checker( + setOf( + Blocks.HOPPER, + Blocks.CHEST, + Blocks.ENDER_CHEST, + Blocks.FURNACE, + Blocks.CRAFTING_TABLE, + Blocks.CAULDRON, + Blocks.WATER_CAULDRON, + Blocks.ENCHANTING_TABLE, + Blocks.DISPENSER, + Blocks.DROPPER, + Blocks.BREWING_STAND, + Blocks.TRAPPED_CHEST, + Blocks.LEVER, + ), + setOf( + BlockTags.DOORS, + BlockTags.TRAPDOORS, + BlockTags.ANVIL, + BlockTags.FENCE_GATES, + ) + ) + + data class Checker<T>( + val direct: Set<T>, + val byTag: Set<TagKey<T>>, + ) { + fun matches(entry: Holder<T>): Boolean { + return entry.value() in direct || checkTags(entry, byTag) + } + } + + val etherwarpHallpasses = Checker( + setOf( + Blocks.CREEPER_HEAD, + Blocks.CREEPER_WALL_HEAD, + Blocks.DRAGON_HEAD, + Blocks.DRAGON_WALL_HEAD, + Blocks.SKELETON_SKULL, + Blocks.SKELETON_WALL_SKULL, + Blocks.WITHER_SKELETON_SKULL, + Blocks.WITHER_SKELETON_WALL_SKULL, + Blocks.PIGLIN_HEAD, + Blocks.PIGLIN_WALL_HEAD, + Blocks.ZOMBIE_HEAD, + Blocks.ZOMBIE_WALL_HEAD, + Blocks.PLAYER_HEAD, + Blocks.PLAYER_WALL_HEAD, + Blocks.REPEATER, + Blocks.COMPARATOR, + Blocks.BIG_DRIPLEAF_STEM, + Blocks.MOSS_CARPET, + Blocks.PALE_MOSS_CARPET, + Blocks.COCOA, + Blocks.LADDER, + Blocks.SEA_PICKLE, + ), + setOf( + BlockTags.FLOWER_POTS, + BlockTags.WOOL_CARPETS, + ), + ) + val etherwarpConsidersFat = Checker( + setOf(), setOf( + // Wall signs have a hitbox + BlockTags.ALL_SIGNS, BlockTags.ALL_HANGING_SIGNS, + BlockTags.BANNERS, + ) + ) + + + fun <T> checkTags(holder: Holder<out T>, set: Set<TagKey<out T>>) = + holder.tags() + .anyMatch(set::contains) + + + fun isEtherwarpTransparent(world: BlockGetter, blockPos: BlockPos): Boolean { + val blockState = world.getBlockState(blockPos) + val block = blockState.block + if (etherwarpConsidersFat.matches(blockState.blockHolder)) + return false + if (block.defaultBlockState().getCollisionShape(world, blockPos).isEmpty) + return true + if (etherwarpHallpasses.matches(blockState.blockHolder)) + return true + return false + } + + sealed interface EtherwarpBlockHit { + data class BlockHit(val blockPos: BlockPos, val accuratePos: Vec3?) : EtherwarpBlockHit + data object Miss : EtherwarpBlockHit + } + + fun raycastWithEtherwarpTransparency(world: BlockGetter, start: Vec3, end: Vec3): EtherwarpBlockHit { + return BlockGetter.traverseBlocks<EtherwarpBlockHit, Unit>( + start, end, Unit, + { _, blockPos -> + if (isEtherwarpTransparent(world, blockPos)) { + return@traverseBlocks null + } +// val defaultedState = world.getBlockState(blockPos).block.defaultState +// val hitShape = defaultedState.getCollisionShape( +// world, +// blockPos, +// ShapeContext.absent() +// ) +// if (world.raycastBlock(start, end, blockPos, hitShape, defaultedState) == null) { +// return@raycast null +// } + val partialResult = world.clipWithInteractionOverride(start, end, blockPos, Shapes.block(), world.getBlockState(blockPos).block.defaultBlockState()) + return@traverseBlocks EtherwarpBlockHit.BlockHit(blockPos, partialResult?.location) + }, + { EtherwarpBlockHit.Miss }) + } + + enum class EtherwarpItemKind { + MERGED, + RAW + } + + @Subscribe + fun renderEtherwarpOverlay(event: WorldRenderLastEvent) { + if (!TConfig.etherwarpOverlay) return + val player = MC.player ?: return + if (TConfig.onlyShowWhileSneaking && !player.isShiftKeyDown) return + val world = player.level + val heldItem = MC.stackInHand + val etherwarpTyp = run { + if (heldItem.extraAttributes.contains("ethermerge")) + EtherwarpItemKind.MERGED + else if (heldItem.skyBlockId == SkyBlockItems.ETHERWARP_CONDUIT) + EtherwarpItemKind.RAW + else + return + } + val playerEyeHeight = // Sneaking: 1.27 (1.21) 1.54 (1.8.9) / Upright: 1.62 (1.8.9,1.21) + if (player.isShiftKeyDown || etherwarpTyp == EtherwarpItemKind.MERGED) + (if (SBData.skyblockLocation?.isModernServer ?: false) 1.27 else 1.54) + else 1.62 + val playerEyePos = player.position.add(0.0, playerEyeHeight, 0.0) + val start = playerEyePos + val end = player.getViewVector(0F).scale(160.0).add(playerEyePos) + val hitResult = raycastWithEtherwarpTransparency( + world, + start, + end, + ) + if (hitResult !is EtherwarpBlockHit.BlockHit) return + val blockPos = hitResult.blockPos + val success = run { + if (!isEtherwarpTransparent(world, blockPos.above())) + EtherwarpResult.OCCUPIED + else if (!isEtherwarpTransparent(world, blockPos.above(2))) + EtherwarpResult.OCCUPIED + else if (playerEyePos.distanceToSqr(hitResult.accuratePos ?: blockPos.center) > 61 * 61) + EtherwarpResult.TOO_DISTANT + else if ((MC.instance.hitResult as? BlockHitResult) + ?.takeIf { it.type == HitResult.Type.BLOCK } + ?.let { interactionBlocked.matches(world.getBlockState(it.blockPos).blockHolder) } + ?: false + ) + EtherwarpResult.INTERACTION_BLOCKED + else + EtherwarpResult.SUCCESS + } + RenderInWorldContext.renderInWorld(event) { + if (TConfig.cube) + block( + blockPos, + success.color().getEffectiveColourRGB() + ) + if (TConfig.wireframe) wireframeCube(blockPos, 10f) + if (TConfig.failureText && success.label != null) { + withFacingThePlayer(blockPos.center) { + text(success.label) + } + } + } + } +} diff --git a/src/main/kotlin/features/items/recipes/ArrowWidget.kt b/src/main/kotlin/features/items/recipes/ArrowWidget.kt new file mode 100644 index 0000000..db0cf60 --- /dev/null +++ b/src/main/kotlin/features/items/recipes/ArrowWidget.kt @@ -0,0 +1,38 @@ +package moe.nea.firmament.features.items.recipes + +import me.shedaniel.math.Dimension +import me.shedaniel.math.Point +import me.shedaniel.math.Rectangle +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.client.renderer.RenderPipelines +import net.minecraft.resources.ResourceLocation + +class ArrowWidget(override var position: Point) : RecipeWidget() { + override val size: Dimension + get() = Dimension(14, 14) + + companion object { + val arrowSprite = ResourceLocation.withDefaultNamespace("container/furnace/lit_progress") + } + + override fun render( + guiGraphics: GuiGraphics, + mouseX: Int, + mouseY: Int, + partialTick: Float + ) { + guiGraphics.blitSprite( + RenderPipelines.GUI_TEXTURED, + arrowSprite, + 14, + 14, + 0, + 0, + position.x, + position.y, + 14, + 14 + ) + } + +} diff --git a/src/main/kotlin/features/items/recipes/ComponentWidget.kt b/src/main/kotlin/features/items/recipes/ComponentWidget.kt new file mode 100644 index 0000000..08a2aa2 --- /dev/null +++ b/src/main/kotlin/features/items/recipes/ComponentWidget.kt @@ -0,0 +1,27 @@ +package moe.nea.firmament.features.items.recipes + +import me.shedaniel.math.Dimension +import me.shedaniel.math.Point +import me.shedaniel.math.Rectangle +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.network.chat.Component +import moe.nea.firmament.repo.recipes.RecipeLayouter +import moe.nea.firmament.util.MC + +class ComponentWidget(override var position: Point, var text: Component) : RecipeWidget(), RecipeLayouter.Updater<Component> { + override fun update(newValue: Component) { + this.text = newValue + } + + override val size: Dimension + get() = Dimension(MC.font.width(text), MC.font.lineHeight) + + override fun render( + guiGraphics: GuiGraphics, + mouseX: Int, + mouseY: Int, + partialTick: Float + ) { + guiGraphics.drawString(MC.font, text, position.x, position.y, -1) + } +} diff --git a/src/main/kotlin/features/items/recipes/EntityWidget.kt b/src/main/kotlin/features/items/recipes/EntityWidget.kt new file mode 100644 index 0000000..4a087e5 --- /dev/null +++ b/src/main/kotlin/features/items/recipes/EntityWidget.kt @@ -0,0 +1,27 @@ +package moe.nea.firmament.features.items.recipes + +import me.shedaniel.math.Dimension +import me.shedaniel.math.Point +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.world.entity.LivingEntity +import moe.nea.firmament.gui.entity.EntityRenderer + +class EntityWidget( + override var position: Point, + override val size: Dimension, + val entity: LivingEntity +) : RecipeWidget() { + override fun render( + guiGraphics: GuiGraphics, + mouseX: Int, + mouseY: Int, + partialTick: Float + ) { + EntityRenderer.renderEntity( + entity, guiGraphics, + rect.x, rect.y, + rect.width.toDouble(), rect.height.toDouble(), + mouseX.toDouble(), mouseY.toDouble() + ) + } +} diff --git a/src/main/kotlin/features/items/recipes/FireWidget.kt b/src/main/kotlin/features/items/recipes/FireWidget.kt new file mode 100644 index 0000000..565152b --- /dev/null +++ b/src/main/kotlin/features/items/recipes/FireWidget.kt @@ -0,0 +1,19 @@ +package moe.nea.firmament.features.items.recipes + +import me.shedaniel.math.Dimension +import me.shedaniel.math.Point +import net.minecraft.client.gui.GuiGraphics + +class FireWidget(override var position: Point, val animationTicks: Int) : RecipeWidget() { + override val size: Dimension + get() = Dimension(10, 10) + + override fun render( + guiGraphics: GuiGraphics, + mouseX: Int, + mouseY: Int, + partialTick: Float + ) { + TODO("Not yet implemented") + } +} diff --git a/src/main/kotlin/features/items/recipes/ItemList.kt b/src/main/kotlin/features/items/recipes/ItemList.kt new file mode 100644 index 0000000..340e1c3 --- /dev/null +++ b/src/main/kotlin/features/items/recipes/ItemList.kt @@ -0,0 +1,308 @@ +package moe.nea.firmament.features.items.recipes + +import io.github.notenoughupdates.moulconfig.observer.GetSetter +import io.github.notenoughupdates.moulconfig.observer.Property +import java.util.Optional +import me.shedaniel.math.Rectangle +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.client.gui.components.Renderable +import net.minecraft.client.gui.components.events.GuiEventListener +import net.minecraft.client.gui.navigation.ScreenAxis +import net.minecraft.client.gui.navigation.ScreenRectangle +import net.minecraft.client.gui.screens.Screen +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen +import net.minecraft.client.input.MouseButtonEvent +import net.minecraft.client.input.MouseButtonInfo +import net.minecraft.network.chat.Component +import net.minecraft.world.item.ItemStack +import net.minecraft.world.item.Items +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.api.v1.FirmamentAPI +import moe.nea.firmament.events.HandledScreenClickEvent +import moe.nea.firmament.events.HandledScreenForegroundEvent +import moe.nea.firmament.events.ReloadRegistrationEvent +import moe.nea.firmament.repo.RepoManager +import moe.nea.firmament.repo.SBItemStack +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.accessors.castAccessor +import moe.nea.firmament.util.render.drawLine +import moe.nea.firmament.util.skyblockId + +object ItemList { + // TODO: add a global toggle for this and RecipeRegistry + + fun collectExclusions(screen: Screen): Set<ScreenRectangle> { + val exclusions = mutableSetOf<ScreenRectangle>() + if (screen is AbstractContainerScreen<*>) { + val screenHandler = screen.castAccessor() + exclusions.add( + ScreenRectangle( + screenHandler.x_Firmament, + screenHandler.y_Firmament, + screenHandler.backgroundWidth_Firmament, + screenHandler.backgroundHeight_Firmament + ) + ) + } + FirmamentAPI.getInstance().extensions + .forEach { extension -> + for (rectangle in extension.getExclusionZones(screen)) { + if (exclusions.any { it.encompasses(rectangle) }) + continue + exclusions.add(rectangle) + } + } + + return exclusions + } + + var reachableItems = listOf<SBItemStack>() + var pageOffset = 0 + fun recalculateVisibleItems() { + reachableItems = RepoManager.neuRepo.items + .items.values.map { SBItemStack(it.skyblockId) } + } + + @Subscribe + fun onReload(event: ReloadRegistrationEvent) { + event.repo.registerReloadListener { recalculateVisibleItems() } + } + + fun coordinates(outer: ScreenRectangle, exclusions: Collection<ScreenRectangle>): Sequence<ScreenRectangle> { + val entryWidth = 18 + val columns = outer.width / entryWidth + val rows = outer.height / entryWidth + val lowX = outer.right() - columns * entryWidth + val lowY = outer.top() + return generateSequence(0) { it + 1 } + .map { + val xIndex = it % columns + val yIndex = it / columns + ScreenRectangle( + lowX + xIndex * entryWidth, lowY + yIndex * entryWidth, + entryWidth, entryWidth + ) + } + .take(rows * columns) + .filter { candidate -> exclusions.none { it.intersects(candidate) } } + } + + var lastRenderPositions: List<Pair<ScreenRectangle, SBItemStack>> = listOf() + var lastHoveredItemStack: Pair<ScreenRectangle, SBItemStack>? = null + + abstract class ItemListElement( + ) : Renderable, GuiEventListener { + abstract val rectangle: Rectangle + override fun setFocused(focused: Boolean) { + } + + override fun isFocused(): Boolean { + return false + } + + override fun isMouseOver(mouseX: Double, mouseY: Double): Boolean { + return rectangle.contains(mouseX, mouseY) + } + } + + interface HasLabel { + fun component(): Component + } + + + class PopupSettingsElement<T : HasLabel>( + x: Int, + y: Int, + width: Int, + val selected: GetSetter<T>, + val options: List<T>, + ) : ItemListElement() { + override val rectangle: Rectangle = Rectangle(x, y, width, 4 + (MC.font.lineHeight + 2) * options.size) + fun bb(i: Int) = + Rectangle( + rectangle.minX, rectangle.minY + (2 + MC.font.lineHeight) * i + 2, + rectangle.width, MC.font.lineHeight + ) + + override fun render( + guiGraphics: GuiGraphics, + mouseX: Int, + mouseY: Int, + partialTick: Float + ) { + guiGraphics.fill(rectangle.minX, rectangle.minY, rectangle.maxX, rectangle.maxY, 0xFF000000.toInt()) + guiGraphics.submitOutline(rectangle.x, rectangle.y, rectangle.width, rectangle.height, -1) + val sel = selected.get() + for ((index, element) in options.withIndex()) { + val b = bb(index) + val tw = MC.font.width(element.component()) + guiGraphics.drawString( + MC.font, element.component(), b.centerX - tw / 2, + b.y + 1, + if (element == sel) 0xFFA0B000.toInt() else -1 + ) + if (b.contains(mouseX, mouseY)) + guiGraphics.hLine(b.centerX - tw / 2, b.centerX + tw / 2 - 1, b.maxY + 1, -1) + } + } + + override fun mouseClicked(event: MouseButtonEvent, isDoubleClick: Boolean): Boolean { + popupElement = null + for ((index, element) in options.withIndex()) { + val b = bb(index) + if (b.contains(event.x, event.y)) { + selected.set(element) + break + } + } + return true + } + } + + class SettingElement<T : HasLabel>( + x: Int, + y: Int, + val selected: GetSetter<T>, + val options: List<T> + ) : ItemListElement() { + val height = MC.font.lineHeight + 4 + val width = options.maxOf { MC.font.width(it.component()) } + 4 + override val rectangle: Rectangle = Rectangle(x, y, width, height) + + override fun render( + guiGraphics: GuiGraphics, + mouseX: Int, + mouseY: Int, + partialTick: Float + ) { + guiGraphics.drawCenteredString(MC.font, selected.get().component(), rectangle.centerX, rectangle.y + 2, -1) + if (isMouseOver(mouseX.toDouble(), mouseY.toDouble())) { + guiGraphics.hLine(rectangle.minX, rectangle.maxX - 1, rectangle.maxY - 2, -1) + } + } + + override fun mouseClicked( + event: MouseButtonEvent, + isDoubleClick: Boolean + ): Boolean { + popupElement = PopupSettingsElement( + rectangle.x, + rectangle.y - options.size * (MC.font.lineHeight + 2) - 2, + width, + selected, + options + ) + return true + } + } + + var popupElement: ItemListElement? = null + + + fun findStackUnder(mouseX: Int, mouseY: Int): Pair<ScreenRectangle, SBItemStack>? { + val lhis = lastHoveredItemStack + if (lhis != null && lhis.first.containsPoint(mouseX, mouseY)) + return lhis + return lastRenderPositions.firstOrNull { it.first.containsPoint(mouseX, mouseY) } + } + + val isItemListEnabled get() = false + + @Subscribe + fun onClick(event: HandledScreenClickEvent) { + if(!isItemListEnabled)return + val pe = popupElement + val me = MouseButtonEvent( + event.mouseX, event.mouseY, + MouseButtonInfo(event.button, 0) // TODO: missing modifiers + ) + if (pe != null) { + event.cancel() + if (!pe.isMouseOver(event.mouseX, event.mouseY)) { + popupElement = null + return + } + pe.mouseClicked( + me, + false + ) + return + } + listElements.forEach { + if (it.isMouseOver(event.mouseX, event.mouseY)) + it.mouseClicked(me, false) + } + } + + var listElements = listOf<ItemListElement>() + + @Subscribe + fun onRender(event: HandledScreenForegroundEvent) { + if(!isItemListEnabled) return + lastHoveredItemStack = null + lastRenderPositions = listOf() + val exclusions = collectExclusions(event.screen) + val potentiallyVisible = reachableItems.subList(pageOffset, reachableItems.size) + val screenWidth = event.screen.width + val rightThird = ScreenRectangle( + screenWidth - screenWidth / 3, 0, + screenWidth / 3, event.screen.height - MC.font.lineHeight - 4 + ) + val coords = coordinates(rightThird, exclusions) + + lastRenderPositions = coords.zip(potentiallyVisible.asSequence()).toList() + val isPopupHovered = popupElement?.isMouseOver(event.mouseX.toDouble(),event.mouseY.toDouble()) + ?: false + lastRenderPositions.forEach { (pos, stack) -> + val realStack = stack.asLazyImmutableItemStack() + val toRender = realStack ?: ItemStack(Items.PAINTING) + event.context.renderItem(toRender, pos.left() + 1, pos.top() + 1) + if (!isPopupHovered && pos.containsPoint(event.mouseX, event.mouseY)) { + lastHoveredItemStack = pos to stack + event.context.setTooltipForNextFrame( + MC.font, + if (realStack != null) + ItemSlotWidget.getTooltip(realStack) + else + stack.estimateLore(), + Optional.empty(), + event.mouseX, event.mouseY + ) + } + } + event.context.fill( + rightThird.left(), + rightThird.bottom(), + rightThird.right(), + event.screen.height, + 0xFF000000.toInt() + ) + val le = mutableListOf<ItemListElement>() + le.add( + SettingElement( + 0, + rightThird.bottom(), + sortOrder, + SortOrder.entries + ) + ) + val bottomWidth = le.sumOf { it.rectangle.width + 2 } - 2 + var startX = rightThird.getCenterInAxis(ScreenAxis.HORIZONTAL) - bottomWidth / 2 + le.forEach { + it.rectangle.translate(startX, 0) + startX += it.rectangle.width + 2 + } + le.forEach { it.render(event.context, event.mouseX, event.mouseY, event.delta) } + listElements = le + popupElement?.render(event.context, event.mouseX, event.mouseY, event.delta) + } + + enum class SortOrder(val component: Component) : HasLabel { + NAME(Component.literal("Name")), + RARITY(Component.literal("Rarity")); + + override fun component(): Component = component + } + + val sortOrder = Property.of(SortOrder.NAME) +} diff --git a/src/main/kotlin/features/items/recipes/ItemSlotWidget.kt b/src/main/kotlin/features/items/recipes/ItemSlotWidget.kt new file mode 100644 index 0000000..b659643 --- /dev/null +++ b/src/main/kotlin/features/items/recipes/ItemSlotWidget.kt @@ -0,0 +1,141 @@ +package moe.nea.firmament.features.items.recipes + +import java.util.Optional +import me.shedaniel.math.Dimension +import me.shedaniel.math.Point +import me.shedaniel.math.Rectangle +import net.fabricmc.fabric.api.client.item.v1.ItemTooltipCallback +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.network.chat.Component +import net.minecraft.world.item.Item +import net.minecraft.world.item.ItemStack +import net.minecraft.world.item.TooltipFlag +import moe.nea.firmament.api.v1.FirmamentItemWidget +import moe.nea.firmament.events.ItemTooltipEvent +import moe.nea.firmament.keybindings.SavedKeyBinding +import moe.nea.firmament.repo.ExpensiveItemCacheApi +import moe.nea.firmament.repo.SBItemStack +import moe.nea.firmament.repo.recipes.RecipeLayouter +import moe.nea.firmament.util.ErrorUtil +import moe.nea.firmament.util.FirmFormatters.shortFormat +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.darkGrey +import moe.nea.firmament.util.mc.displayNameAccordingToNbt +import moe.nea.firmament.util.mc.loreAccordingToNbt + +class ItemSlotWidget( + point: Point, + var content: List<SBItemStack>, + val slotKind: RecipeLayouter.SlotKind +) : RecipeWidget(), + RecipeLayouter.CyclingItemSlot, + FirmamentItemWidget { + override var position = point + override val size get() = Dimension(16, 16) + val itemRect get() = Rectangle(position, Dimension(16, 16)) + + val backgroundTopLeft + get() = + if (slotKind.isBig) Point(position.x - 4, position.y - 4) + else Point(position.x - 1, position.y - 1) + val backgroundSize = + if (slotKind.isBig) Dimension(16 + 8, 16 + 8) + else Dimension(18, 18) + override val rect: Rectangle + get() = Rectangle(backgroundTopLeft, backgroundSize) + + @OptIn(ExpensiveItemCacheApi::class) + override fun render( + guiGraphics: GuiGraphics, + mouseX: Int, + mouseY: Int, + partialTick: Float + ) { + val stack = current().asImmutableItemStack() + // TODO: draw slot background + if (stack.isEmpty) return + guiGraphics.renderItem(stack, position.x, position.y) + guiGraphics.renderItemDecorations( + MC.font, stack, position.x, position.y, + if (stack.count >= SHORT_NUM_CUTOFF) shortFormat(stack.count.toDouble()) + else null + ) + if (itemRect.contains(mouseX, mouseY) + && guiGraphics.containsPointInScissor(mouseX, mouseY) + ) guiGraphics.setTooltipForNextFrame( + MC.font, getTooltip(stack), Optional.empty(), + mouseX, mouseY + ) + } + + companion object { + val SHORT_NUM_CUTOFF = 1000 + var canUseTooltipEvent = true + + fun getTooltip(itemStack: ItemStack): List<Component> { + val lore = mutableListOf(itemStack.displayNameAccordingToNbt) + lore.addAll(itemStack.loreAccordingToNbt) + if (canUseTooltipEvent) { + try { + ItemTooltipCallback.EVENT.invoker().getTooltip( + itemStack, Item.TooltipContext.EMPTY, + TooltipFlag.NORMAL, lore + ) + } catch (ex: Exception) { + canUseTooltipEvent = false + ErrorUtil.softError("Failed to use vanilla tooltips", ex) + } + } else { + ItemTooltipEvent.publish( + ItemTooltipEvent( + itemStack, + Item.TooltipContext.EMPTY, + TooltipFlag.NORMAL, + lore + ) + ) + } + if (itemStack.count >= SHORT_NUM_CUTOFF && lore.isNotEmpty()) + lore.add(1, Component.literal("${itemStack.count}x").darkGrey()) + return lore + } + } + + + override fun tick() { + if (SavedKeyBinding.isShiftDown()) return + if (content.size <= 1) return + if (MC.currentTick % 5 != 0) return + index = (index + 1) % content.size + } + + var index = 0 + var onUpdate: () -> Unit = {} + + override fun onUpdate(action: () -> Unit) { + this.onUpdate = action + } + + override fun current(): SBItemStack { + return content.getOrElse(index) { SBItemStack.EMPTY } + } + + override fun update(newValue: SBItemStack) { + content = listOf(newValue) + // SAFE: content was just assigned to a non-empty list + index = index.coerceIn(content.indices) + } + + override fun getPlacement(): FirmamentItemWidget.Placement { + return FirmamentItemWidget.Placement.RECIPE_SCREEN + } + + @OptIn(ExpensiveItemCacheApi::class) + override fun getItemStack(): ItemStack { + return current().asImmutableItemStack() + } + + override fun getSkyBlockId(): String { + return current().skyblockId.neuItem + } +} diff --git a/src/main/kotlin/features/items/recipes/MoulConfigWidget.kt b/src/main/kotlin/features/items/recipes/MoulConfigWidget.kt new file mode 100644 index 0000000..aad3bda --- /dev/null +++ b/src/main/kotlin/features/items/recipes/MoulConfigWidget.kt @@ -0,0 +1,46 @@ +package moe.nea.firmament.features.items.recipes + +import io.github.notenoughupdates.moulconfig.gui.GuiComponent +import io.github.notenoughupdates.moulconfig.gui.MouseEvent +import me.shedaniel.math.Dimension +import me.shedaniel.math.Point +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.client.input.MouseButtonEvent +import moe.nea.firmament.util.MoulConfigUtils.createAndTranslateFullContext + +class MoulConfigWidget( + val component: GuiComponent, + override var position: Point, + override val size: Dimension, +) : RecipeWidget() { + override fun render( + guiGraphics: GuiGraphics, + mouseX: Int, + mouseY: Int, + partialTick: Float + ) { + createAndTranslateFullContext( + guiGraphics, mouseX, mouseY, rect, + component::render + ) + } + + override fun mouseClicked(event: MouseButtonEvent, isDoubleClick: Boolean): Boolean { + return createAndTranslateFullContext(null, event.x.toInt(), event.y.toInt(), rect) { + component.mouseEvent(MouseEvent.Click(event.button(), true), it) + } + } + + override fun mouseMoved(mouseX: Double, mouseY: Double) { + createAndTranslateFullContext(null, mouseX, mouseY, rect) { + component.mouseEvent(MouseEvent.Move(0F, 0F), it) + } + } + + override fun mouseReleased(event: MouseButtonEvent): Boolean { + return createAndTranslateFullContext(null, event.x, event.y, rect) { + component.mouseEvent(MouseEvent.Click(event.button(), false), it) + } + } + +} diff --git a/src/main/kotlin/features/items/recipes/RecipeRegistry.kt b/src/main/kotlin/features/items/recipes/RecipeRegistry.kt new file mode 100644 index 0000000..c2df46f --- /dev/null +++ b/src/main/kotlin/features/items/recipes/RecipeRegistry.kt @@ -0,0 +1,116 @@ +package moe.nea.firmament.features.items.recipes + +import com.mojang.blaze3d.platform.InputConstants +import io.github.moulberry.repo.IReloadable +import io.github.moulberry.repo.NEURepository +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.HandledScreenKeyPressedEvent +import moe.nea.firmament.events.ReloadRegistrationEvent +import moe.nea.firmament.keybindings.SavedKeyBinding +import moe.nea.firmament.repo.RepoManager +import moe.nea.firmament.repo.SBItemStack +import moe.nea.firmament.repo.recipes.GenericRecipeRenderer +import moe.nea.firmament.repo.recipes.SBCraftingRecipeRenderer +import moe.nea.firmament.repo.recipes.SBEssenceUpgradeRecipeRenderer +import moe.nea.firmament.repo.recipes.SBForgeRecipeRenderer +import moe.nea.firmament.repo.recipes.SBReforgeRecipeRenderer +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.SkyblockId +import moe.nea.firmament.util.focusedItemStack + +object RecipeRegistry { + val recipeTypes: List<GenericRecipeRenderer<*>> = listOf( + SBCraftingRecipeRenderer, + SBForgeRecipeRenderer, + SBReforgeRecipeRenderer, + SBEssenceUpgradeRecipeRenderer, + ) + + + @Subscribe + fun showUsages(event: HandledScreenKeyPressedEvent) { + val provider = + if (event.matches(SavedKeyBinding.keyWithoutMods(InputConstants.KEY_R))) { + ::getRecipesFor + } else if (event.matches(SavedKeyBinding.keyWithoutMods(InputConstants.KEY_U))) { + ::getUsagesFor + } else { + return + } + val stack = event.screen.focusedItemStack ?: return + val recipes = provider(SBItemStack(stack)) + if (recipes.isEmpty()) return + MC.screen = RecipeScreen(recipes.toList()) + } + + + object RecipeIndexes : IReloadable { + + private fun <T : Any> createIndexFor( + neuRepository: NEURepository, + recipeRenderer: GenericRecipeRenderer<T>, + outputs: Boolean, + ): List<Pair<SkyblockId, RenderableRecipe<T>>> { + val indexer: (T) -> Collection<SBItemStack> = + if (outputs) recipeRenderer::getOutputs + else recipeRenderer::getInputs + return recipeRenderer.findAllRecipes(neuRepository) + .flatMap { + val wrappedRecipe = RenderableRecipe(it, recipeRenderer, null) + indexer(it).map { it.skyblockId to wrappedRecipe } + } + } + + fun createIndex(outputs: Boolean): MutableMap<SkyblockId, List<RenderableRecipe<*>>> { + val m: MutableMap<SkyblockId, List<RenderableRecipe<*>>> = mutableMapOf() + recipeTypes.forEach { renderer -> + createIndexFor(RepoManager.neuRepo, renderer, outputs) + .forEach { (stack, recipe) -> + m.merge(stack, listOf(recipe)) { a, b -> a + b } + } + } + return m + } + + lateinit var recipesForIndex: Map<SkyblockId, List<RenderableRecipe<*>>> + lateinit var usagesForIndex: Map<SkyblockId, List<RenderableRecipe<*>>> + override fun reload(recipe: NEURepository) { + recipesForIndex = createIndex(true) + usagesForIndex = createIndex(false) + } + } + + @Subscribe + fun onRepoBuild(event: ReloadRegistrationEvent) { + event.repo.registerReloadListener(RecipeIndexes) + } + + + fun getRecipesFor(itemStack: SBItemStack): Set<RenderableRecipe<*>> { + val recipes = LinkedHashSet<RenderableRecipe<*>>() + recipeTypes.forEach { injectRecipesFor(it, recipes, itemStack, true) } + recipes.addAll(RecipeIndexes.recipesForIndex[itemStack.skyblockId] ?: emptyList()) + return recipes + } + + fun getUsagesFor(itemStack: SBItemStack): Set<RenderableRecipe<*>> { + val recipes = LinkedHashSet<RenderableRecipe<*>>() + recipeTypes.forEach { injectRecipesFor(it, recipes, itemStack, false) } + recipes.addAll(RecipeIndexes.usagesForIndex[itemStack.skyblockId] ?: emptyList()) + return recipes + } + + private fun <T : Any> injectRecipesFor( + recipeRenderer: GenericRecipeRenderer<T>, + collector: MutableCollection<RenderableRecipe<*>>, + relevantItem: SBItemStack, + mustBeInOutputs: Boolean + ) { + collector.addAll( + recipeRenderer.discoverExtraRecipes(RepoManager.neuRepo, relevantItem, mustBeInOutputs) + .map { RenderableRecipe(it, recipeRenderer, relevantItem) } + ) + } + + +} diff --git a/src/main/kotlin/features/items/recipes/RecipeScreen.kt b/src/main/kotlin/features/items/recipes/RecipeScreen.kt new file mode 100644 index 0000000..9a746f3 --- /dev/null +++ b/src/main/kotlin/features/items/recipes/RecipeScreen.kt @@ -0,0 +1,129 @@ +package moe.nea.firmament.features.items.recipes + +import me.shedaniel.math.Point +import me.shedaniel.math.Rectangle +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.client.gui.screens.Screen +import net.minecraft.client.renderer.RenderPipelines +import moe.nea.firmament.util.mc.CommonTextures +import moe.nea.firmament.util.render.enableScissorWithTranslation +import moe.nea.firmament.util.tr + +class RecipeScreen( + val recipes: List<RenderableRecipe<*>>, +) : Screen(tr("firmament.recipe.screen", "SkyBlock Recipe")) { + + data class PlacedRecipe( + val bounds: Rectangle, + val layoutedRecipe: StandaloneRecipeRenderer, + ) { + fun moveTo(position: Point) { + val Δx = position.x - bounds.x + val Δy = position.y - bounds.y + bounds.translate(Δx, Δy) + layoutedRecipe.widgets.forEach { widget -> + widget.position = widget.position.clone().also { + it.translate(Δx, Δy) + } + } + } + } + + lateinit var placedRecipes: List<PlacedRecipe> + var scrollViewport: Int = 0 + var scrollOffset: Int = 0 + var scrollPortWidth: Int = 0 + var heightEstimate: Int = 0 + val gutter = 10 + override fun init() { + super.init() + scrollViewport = minOf(height - 20, 250) + scrollPortWidth = 0 + heightEstimate = 0 + var offset = height / 2 - scrollViewport / 2 + placedRecipes = recipes.map { + val effectiveWidth = minOf(it.renderer.displayWidth, width - 20) + val bounds = Rectangle( + width / 2 - effectiveWidth / 2, + offset, + effectiveWidth, + it.renderer.displayHeight + ) + if (heightEstimate > 0) + heightEstimate += gutter + heightEstimate += bounds.height + scrollPortWidth = maxOf(effectiveWidth, scrollPortWidth) + offset += bounds.height + gutter + val layoutedRecipe = it.render(bounds) + layoutedRecipe.widgets.forEach(this::addRenderableWidget) + PlacedRecipe(bounds, layoutedRecipe) + } + } + + fun scrollRect() = + Rectangle( + width / 2 - scrollPortWidth / 2, height / 2 - scrollViewport / 2, + scrollPortWidth, scrollViewport + ) + + fun scissorScrollPort(guiGraphics: GuiGraphics) { + guiGraphics.enableScissorWithTranslation(scrollRect()) + } + + override fun mouseScrolled(mouseX: Double, mouseY: Double, scrollX: Double, scrollY: Double): Boolean { + if (!scrollRect().contains(mouseX, mouseY)) + return false + scrollOffset = (scrollOffset + scrollY * -4) + .coerceAtMost(heightEstimate - scrollViewport.toDouble()) + .coerceAtLeast(.0) + .toInt() + var offset = height / 2 - scrollViewport / 2 - scrollOffset + placedRecipes.forEach { + it.moveTo(Point(it.bounds.x, offset)) + offset += it.bounds.height + gutter + } + return true + } + + override fun renderBackground( + guiGraphics: GuiGraphics, + mouseX: Int, + mouseY: Int, + partialTick: Float + ) { + super.renderBackground(guiGraphics, mouseX, mouseY, partialTick) + + val srect = scrollRect() + srect.grow(8, 8) + guiGraphics.blitSprite( + RenderPipelines.GUI_TEXTURED, + CommonTextures.genericWidget(), + srect.x, srect.y, + srect.width, srect.height + ) + + scissorScrollPort(guiGraphics) + placedRecipes.forEach { + guiGraphics.blitSprite( + RenderPipelines.GUI_TEXTURED, + CommonTextures.genericWidget(), + it.bounds.x, it.bounds.y, + it.bounds.width, it.bounds.height + ) + } + guiGraphics.disableScissor() + } + + override fun render(guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float) { + scissorScrollPort(guiGraphics) + super.render(guiGraphics, mouseX, mouseY, partialTick) + guiGraphics.disableScissor() + } + + override fun tick() { + super.tick() + placedRecipes.forEach { + it.layoutedRecipe.tick() + } + } +} diff --git a/src/main/kotlin/features/items/recipes/RecipeWidget.kt b/src/main/kotlin/features/items/recipes/RecipeWidget.kt new file mode 100644 index 0000000..f13707c --- /dev/null +++ b/src/main/kotlin/features/items/recipes/RecipeWidget.kt @@ -0,0 +1,37 @@ +package moe.nea.firmament.features.items.recipes + +import me.shedaniel.math.Dimension +import me.shedaniel.math.Point +import me.shedaniel.math.Rectangle +import net.minecraft.client.gui.components.Renderable +import net.minecraft.client.gui.components.events.GuiEventListener +import net.minecraft.client.gui.narration.NarratableEntry +import net.minecraft.client.gui.narration.NarrationElementOutput +import net.minecraft.client.gui.navigation.ScreenRectangle +import moe.nea.firmament.util.mc.asScreenRectangle + +abstract class RecipeWidget : GuiEventListener, Renderable, NarratableEntry { + override fun narrationPriority(): NarratableEntry.NarrationPriority? { + return NarratableEntry.NarrationPriority.NONE// I am so sorry + } + + override fun updateNarration(narrationElementOutput: NarrationElementOutput) { + } + + open fun tick() {} + private var _focused = false + abstract var position: Point + abstract val size: Dimension + open val rect: Rectangle get() = Rectangle(position, size) + override fun setFocused(focused: Boolean) { + this._focused = focused + } + + override fun isFocused(): Boolean { + return this._focused + } + + override fun isMouseOver(mouseX: Double, mouseY: Double): Boolean { + return rect.contains(mouseX, mouseY) + } +} diff --git a/src/main/kotlin/features/items/recipes/RenderableRecipe.kt b/src/main/kotlin/features/items/recipes/RenderableRecipe.kt new file mode 100644 index 0000000..20ca17e --- /dev/null +++ b/src/main/kotlin/features/items/recipes/RenderableRecipe.kt @@ -0,0 +1,27 @@ +package moe.nea.firmament.features.items.recipes + +import java.util.Objects +import me.shedaniel.math.Rectangle +import moe.nea.firmament.repo.SBItemStack +import moe.nea.firmament.repo.recipes.GenericRecipeRenderer + +class RenderableRecipe<T : Any>( + val recipe: T, + val renderer: GenericRecipeRenderer<T>, + val mainItemStack: SBItemStack?, +) { + fun render(bounds: Rectangle): StandaloneRecipeRenderer { + val layouter = StandaloneRecipeRenderer(bounds) + renderer.render(recipe, bounds, layouter, mainItemStack) + return layouter + } + +// override fun equals(other: Any?): Boolean { +// if (other !is RenderableRecipe<*>) return false +// return renderer == other.renderer && recipe == other.recipe +// } +// +// override fun hashCode(): Int { +// return Objects.hash(recipe, renderer) +// } +} diff --git a/src/main/kotlin/features/items/recipes/StandaloneRecipeRenderer.kt b/src/main/kotlin/features/items/recipes/StandaloneRecipeRenderer.kt new file mode 100644 index 0000000..5a834eb --- /dev/null +++ b/src/main/kotlin/features/items/recipes/StandaloneRecipeRenderer.kt @@ -0,0 +1,77 @@ +package moe.nea.firmament.features.items.recipes + +import io.github.notenoughupdates.moulconfig.gui.GuiComponent +import me.shedaniel.math.Dimension +import me.shedaniel.math.Point +import me.shedaniel.math.Rectangle +import net.minecraft.client.gui.components.events.AbstractContainerEventHandler +import net.minecraft.client.gui.components.events.GuiEventListener +import net.minecraft.network.chat.Component +import net.minecraft.world.entity.LivingEntity +import moe.nea.firmament.repo.SBItemStack +import moe.nea.firmament.repo.recipes.RecipeLayouter + +class StandaloneRecipeRenderer(val bounds: Rectangle) : AbstractContainerEventHandler(), RecipeLayouter { + + fun tick() { + widgets.forEach { it.tick() } + } + + fun <T : RecipeWidget> addWidget(widget: T): T { + this.widgets.add(widget) + return widget + } + + override fun createCyclingItemSlot( + x: Int, + y: Int, + content: List<SBItemStack>, + slotKind: RecipeLayouter.SlotKind + ): RecipeLayouter.CyclingItemSlot { + return addWidget(ItemSlotWidget(Point(x, y), content, slotKind)) + } + + val Rectangle.topLeft get() = Point(x, y) + + override fun createTooltip( + rectangle: Rectangle, + label: List<Component> + ) { + addWidget(TooltipWidget(rectangle.topLeft, rectangle.size, label)) + } + + override fun createLabel( + x: Int, + y: Int, + text: Component + ): RecipeLayouter.Updater<Component> { + return addWidget(ComponentWidget(Point(x, y), text)) + } + + override fun createArrow(x: Int, y: Int): Rectangle { + return addWidget(ArrowWidget(Point(x, y))).rect + } + + override fun createMoulConfig( + x: Int, + y: Int, + w: Int, + h: Int, + component: GuiComponent + ) { + addWidget(MoulConfigWidget(component, Point(x, y), Dimension(w, h))) + } + + override fun createFire(point: Point, animationTicks: Int) { + addWidget(FireWidget(point, animationTicks)) + } + + override fun createEntity(rectangle: Rectangle, entity: LivingEntity) { + addWidget(EntityWidget(rectangle.topLeft, rectangle.size, entity)) + } + + val widgets: MutableList<RecipeWidget> = mutableListOf() + override fun children(): List<GuiEventListener> { + return widgets + } +} diff --git a/src/main/kotlin/features/items/recipes/TooltipWidget.kt b/src/main/kotlin/features/items/recipes/TooltipWidget.kt new file mode 100644 index 0000000..87feb61 --- /dev/null +++ b/src/main/kotlin/features/items/recipes/TooltipWidget.kt @@ -0,0 +1,30 @@ +package moe.nea.firmament.features.items.recipes + +import me.shedaniel.math.Dimension +import me.shedaniel.math.Point +import me.shedaniel.math.Rectangle +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.network.chat.Component +import moe.nea.firmament.repo.recipes.RecipeLayouter + +class TooltipWidget( + override var position: Point, + override val size: Dimension, + label: List<Component> +) : RecipeWidget(), RecipeLayouter.Updater<List<Component>> { + override fun update(newValue: List<Component>) { + this.formattedComponent = newValue.map { it.visualOrderText } + } + + var formattedComponent = label.map { it.visualOrderText } + override fun render( + guiGraphics: GuiGraphics, + mouseX: Int, + mouseY: Int, + partialTick: Float + ) { + if (rect.contains(mouseX, mouseY)) + guiGraphics.setTooltipForNextFrame(formattedComponent, mouseX, mouseY) + } + +} |
