From b9b98b1e7a1134b5e6bc63ca5a11c5c805a39246 Mon Sep 17 00:00:00 2001 From: Bibi Reden Date: Fri, 13 Dec 2024 13:44:03 -0600 Subject: [PATCH] [feat] Finalize PlayerEX UI and stage release --- CHANGELOG.md | 8 +- .../playerex/mixin/LocalPlayerMixin.java | 34 ++- .../bibireden/playerex/ui/PlayerEXScreen.kt | 249 ++++++++++-------- .../bibireden/playerex/ui/PlayerEXTextures.kt | 10 + .../ui/components/AttributeComponent.kt | 23 +- .../ui/components/AttributeListComponent.kt | 7 +- .../ui/components/ProgressBarComponent.kt | 58 ++++ .../buttons/AttributeButtonComponent.kt | 12 +- .../labels/AttributeLabelComponent.kt | 7 +- .../ui/menus/PlayerEXAttributesMenu.kt | 199 ++++++++++---- .../assets/playerex/owo_ui/main_ui_model.xml | 143 ++++++---- .../assets/playerex/textures/gui/rhombus.png | Bin 0 -> 727 bytes .../playerex/mixin/LivingEntityMixin.java | 4 + .../kotlin/com/bibireden/playerex/PlayerEX.kt | 5 +- .../bibireden/playerex/compat/CompatUtils.kt | 9 - .../resources/assets/playerex/lang/en_us.json | 12 +- 16 files changed, 533 insertions(+), 247 deletions(-) create mode 100644 src/client/kotlin/com/bibireden/playerex/ui/PlayerEXTextures.kt create mode 100644 src/client/kotlin/com/bibireden/playerex/ui/components/ProgressBarComponent.kt create mode 100644 src/client/resources/assets/playerex/textures/gui/rhombus.png delete mode 100644 src/main/kotlin/com/bibireden/playerex/compat/CompatUtils.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 00590ceb..9339ee4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,5 @@ -## Fixes 🌽 -- Resolved a mixin target for Forge users (on Sinytra) that caused items to break immediately. - - Thank you **`AndyNoob` https://github.com/AndyNoob** for this contribution. \ No newline at end of file +**This slates the first official release of the new PlayerEX. It had been stable for a while, although the UI was not fully completed.** + +## Additions 🍎 +- Finished the PlayerEX UI to the point where it is ready for release. + - There is now a component set on the right of the UI that has a list of current pages. Use it in order to navigate between multiple mods who use the registry. diff --git a/src/client/java/com/bibireden/playerex/mixin/LocalPlayerMixin.java b/src/client/java/com/bibireden/playerex/mixin/LocalPlayerMixin.java index 1d52d384..2e5deb94 100644 --- a/src/client/java/com/bibireden/playerex/mixin/LocalPlayerMixin.java +++ b/src/client/java/com/bibireden/playerex/mixin/LocalPlayerMixin.java @@ -1,8 +1,18 @@ package com.bibireden.playerex.mixin; +import com.bibireden.data_attributes.api.DataAttributesAPI; import com.bibireden.playerex.ui.PlayerEXScreen; +import com.mojang.authlib.GameProfile; +import de.dafuqs.additionalentityattributes.AdditionalEntityAttributes; import net.minecraft.client.Minecraft; import net.minecraft.client.player.LocalPlayer; +import net.minecraft.core.BlockPos; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.Level; import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; @@ -11,11 +21,33 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; @Mixin(LocalPlayer.class) -abstract class LocalPlayerMixin { +abstract class LocalPlayerMixin extends LivingEntity { @Shadow @Final protected Minecraft minecraft; + protected LocalPlayerMixin(EntityType entityType, Level level) { + super(entityType, level); + } + @Inject(method = "setExperienceValues", at = @At("TAIL")) private void playerex$setExperienceValues(CallbackInfo ci) { if (minecraft.screen instanceof PlayerEXScreen screen) screen.onExperienceUpdated(); } + + @Override + public void setHealth(float health) { + super.setHealth(health); + if (minecraft == null) return; + if (minecraft.screen instanceof PlayerEXScreen screen) { + screen.onAttributeUpdated(Attributes.MAX_HEALTH, getMaxHealth()); + } + } + + @Override + public void setAirSupply(int air) { + super.setAirSupply(air); + if (minecraft == null) return; + if (minecraft.screen instanceof PlayerEXScreen screen) { + screen.onAttributeUpdated(AdditionalEntityAttributes.LUNG_CAPACITY, DataAttributesAPI.getValue(AdditionalEntityAttributes.LUNG_CAPACITY, this).orElse(0.0)); + } + } } diff --git a/src/client/kotlin/com/bibireden/playerex/ui/PlayerEXScreen.kt b/src/client/kotlin/com/bibireden/playerex/ui/PlayerEXScreen.kt index 7046805a..6347c51b 100644 --- a/src/client/kotlin/com/bibireden/playerex/ui/PlayerEXScreen.kt +++ b/src/client/kotlin/com/bibireden/playerex/ui/PlayerEXScreen.kt @@ -1,5 +1,6 @@ package com.bibireden.playerex.ui +import com.bibireden.data_attributes.ui.renderers.ButtonRenderers import com.bibireden.playerex.PlayerEXClient import com.bibireden.playerex.components.player.IPlayerDataComponent import com.bibireden.playerex.ext.component @@ -14,18 +15,19 @@ import com.bibireden.playerex.ui.components.buttons.AttributeButtonComponent import com.bibireden.playerex.ui.helper.InputHelper import com.bibireden.playerex.ui.util.Colors import com.bibireden.playerex.util.PlayerEXUtil +import io.wispforest.endec.impl.StructEndecBuilder import io.wispforest.owo.ui.base.BaseUIModelScreen import io.wispforest.owo.ui.component.* +import io.wispforest.owo.ui.container.Containers import io.wispforest.owo.ui.container.FlowLayout +import io.wispforest.owo.ui.core.* import io.wispforest.owo.ui.core.Component as OwoComponent -import io.wispforest.owo.ui.core.Easing -import io.wispforest.owo.ui.core.OwoUIDrawContext -import io.wispforest.owo.ui.core.ParentComponent -import io.wispforest.owo.ui.core.Sizing import io.wispforest.owo.util.EventSource +import net.minecraft.network.chat.Component +import net.minecraft.network.chat.Style +import net.minecraft.resources.ResourceLocation import net.minecraft.util.Mth import net.minecraft.world.entity.ai.attributes.Attribute -import net.minecraft.network.chat.Component import kotlin.reflect.KClass // Transformers @@ -33,12 +35,22 @@ fun ParentComponent.childById(clazz: KClass, id: String) = /** Primary screen for the mod that brings everything intended together. */ class PlayerEXScreen : BaseUIModelScreen(FlowLayout::class.java, DataSource.asset(PlayerEXClient.MAIN_UI_SCREEN_ID)) { + private var listCollapsed = false + private var currentPage = 0 - private val pages: MutableList = mutableListOf() + private val pages: MutableList> = mutableListOf() private val player by lazy { this.minecraft!!.player!! } + private val content by lazy { uiAdapter.rootComponent.childById(FlowLayout::class, "content")!! } + private val footer by lazy { uiAdapter.rootComponent.childById(FlowLayout::class, "footer")!! } + + private val currentLevel by lazy { uiAdapter.rootComponent.childById(LabelComponent::class, "level:current")!! } + private val levelAmount by lazy { uiAdapter.rootComponent.childById(TextBoxComponent::class, "level:amount")!! } + private val levelButton by lazy { uiAdapter.rootComponent.childById(ButtonComponent::class, "level:button")!! } + + private val onLevelUpdatedEvents = OnLevelUpdated.stream private val onLevelUpdated: EventSource = onLevelUpdatedEvents.source() @@ -46,132 +58,153 @@ class PlayerEXScreen : BaseUIModelScreen(FlowLayout::class.java, Dat /** Whenever the level attribute gets modified, and on initialization of the screen, this will be called. */ fun onLevelUpdated(level: Int) { -// currentLevel.apply { -// text(Component.translatable("playerex.ui.current_level", player.level.toInt(), PlayerEXUtil.getRequiredXpForNextLevel(player))) -// } -// -// updatePointsAvailable() -// updateLevelUpButton() -// updateProgressBar() -// -// this.uiAdapter.rootComponent.forEachDescendant { descendant -> -// if (descendant is MenuComponent) descendant.onLevelUpdatedEvents.sink().onLevelUpdated(level) -// if (descendant is AttributeButtonComponent) descendant.refresh() -// } + currentLevel.apply { + text(Component.translatable("playerex.ui.current_level", player.level.toInt(), PlayerEXUtil.getRequiredXpForNextLevel(player))) + } + + updatePointsAvailable() + updateLevelUpButton() + updateProgressBar() + + this.uiAdapter.rootComponent.forEachDescendant { descendant -> + if (descendant is MenuComponent) descendant.onLevelUpdatedEvents.sink().onLevelUpdated(level) + if (descendant is AttributeButtonComponent) descendant.refresh() + } } /** Whenever any attribute is updated, this will be called. */ fun onAttributeUpdated(attribute: Attribute, value: Double) { -// this.uiAdapter.rootComponent.forEachDescendant { descendant -> -// if (descendant is MenuComponent) descendant.onAttributeUpdatedEvents.sink().onAttributeUpdated(attribute, value) -// if (descendant is AttributeButtonComponent) descendant.refresh() -// } -// updatePointsAvailable() + this.uiAdapter.rootComponent.forEachDescendant { descendant -> + if (descendant is MenuComponent) descendant.onAttributeUpdatedEvents.sink().onAttributeUpdated(attribute, value) + if (descendant is AttributeButtonComponent) descendant.refresh() + } + updatePointsAvailable() } private fun updatePointsAvailable() { -// this.uiAdapter.rootComponent.childById(LabelComponent::class, "points_available")?.apply { -// text(Component.translatable("playerex.ui.main.skill_points_header").append(": [").append( -// Component.literal("${player.component.skillPoints}").withStyle { -// it.withColor( -// when (player.component.skillPoints) { -// 0 -> Colors.GRAY -// else -> Colors.SATURATED_BLUE -// } -// ) -// }).append("]") -// ) -// } + this.uiAdapter.rootComponent.childById(LabelComponent::class, "points_available")?.apply { + text(Component.translatable("playerex.ui.main.skill_points_header").append(": ").append( + Component.literal("${player.component.skillPoints}").withStyle { + it.withColor( + when (player.component.skillPoints) { + 0 -> Colors.GRAY + else -> Colors.SATURATED_BLUE + } + ) + }) + ) + } } - private fun onPagesUpdated() { -// val root = this.uiAdapter.rootComponent -// val pageCounter = root.childById(LabelComponent::class, "counter")!! -// val content = root.childById(FlowLayout::class, "content")!! -// -// pageCounter.text(Component.nullToEmpty("${currentPage + 1}/${pages.size}")) -// content.clearChildren() -// content.child(pages[currentPage]) + /** changes the page based on the index. */ + private fun switchPage(to: Int) { + val root = this.uiAdapter.rootComponent + + val content = root.childById(FlowLayout::class, "content")!! + + content.clearChildren() + content.child(pages[to].second) } private fun updateLevelUpButton() { -// val amount = levelAmount.value.toIntOrNull() ?: return -// val result = player.level + amount -// -// levelButton -// .active(player.experienceLevel >= PlayerEXUtil.getRequiredXpForLevel(player, result)) -// .tooltip(Component.translatable("playerex.ui.level_button", PlayerEXUtil.getRequiredXpForLevel(player, result), amount, player.experienceLevel)) + val amount = levelAmount.value.toIntOrNull() ?: return + val result = player.level + amount + + levelButton + .active(player.experienceLevel >= PlayerEXUtil.getRequiredXpForLevel(player, result)) + .tooltip(Component.translatable("playerex.ui.level_button", PlayerEXUtil.getRequiredXpForLevel(player, result), amount, player.experienceLevel)) } private fun updateProgressBar() { -// var result = 0.0 -// if (player.experienceLevel > 0) { -// val required = PlayerEXUtil.getRequiredXpForNextLevel(player) -// result = Mth.clamp((player.experienceLevel.toDouble() / required) * 100, 0.0, 100.0) -// } -// footer.childById(BoxComponent::class, "progress")!! -// .horizontalSizing().animate(250, Easing.CUBIC, Sizing.fill(result.toInt())).forwards() + var result = 0.0 + if (player.experienceLevel > 0) { + val required = PlayerEXUtil.getRequiredXpForNextLevel(player) + result = Mth.clamp((player.experienceLevel.toDouble() / required) * 100, 0.0, 100.0) + } + footer.childById(BoxComponent::class, "progress")?.horizontalSizing()?.animate(250, Easing.CUBIC, Sizing.fill(result.toInt()))?.forwards() } - override fun build(rootComponent: FlowLayout) { + private fun updateCollapseState(content: FlowLayout, listLayout: FlowLayout, button: ButtonComponent) { + button.message = Component.literal(if (listCollapsed) "<" else ">").withStyle(Style.EMPTY.withBold(true)) + content.horizontalSizing().animate(500, Easing.SINE, if (listCollapsed) Sizing.fill(97) else Sizing.fill(85)).forwards() + button.horizontalSizing().animate(500, Easing.SINE, if (listCollapsed) Sizing.fixed(10) else Sizing.fill(10)).forwards() + listLayout.horizontalSizing().animate(500, Easing.SINE, Sizing.fill(if (listCollapsed) 3 else 15)).forwards() + } + override fun build(rootComponent: FlowLayout) { + val listLayout = rootComponent.childById(FlowLayout::class, "list")!! + val contentLayout = rootComponent.childById(FlowLayout::class, "content")!! + + PlayerEXMenuRegistry.get().forEach { (resource, clazz) -> + val instance = clazz.getDeclaredConstructor().newInstance() + instance.init(minecraft!!, this, player.component) + instance.build(contentLayout) + pages.add(Pair(resource, instance)) + } -// val player = minecraft?.player ?: return -// -// val levelUpButton = rootComponent.childById(ButtonComponent::class, "level:button")!! -// -// updateLevelUpButton() -// -// levelAmount.setFilter(InputHelper::isUIntInput) -// levelAmount.onChanged().subscribe { updateLevelUpButton() } -// -// val previousPage = rootComponent.childById(ButtonComponent::class, "previous")!! -// val pageCounter = rootComponent.childById(LabelComponent::class, "counter")!! -// val nextPage = rootComponent.childById(ButtonComponent::class, "next")!! -// val exit = rootComponent.childById(ButtonComponent::class, "exit")!! -// -// PlayerEXMenuRegistry.get().forEach { (_, clazz) -> -// val instance = clazz.getDeclaredConstructor().newInstance() -// instance.init(minecraft!!, this, player.component) -// instance.build(content) -// pages.add(instance) -// } -// -// this.onLevelUpdated(player.level.toInt()) -// this.onPagesUpdated() -// -// pageCounter.text(Component.nullToEmpty("${currentPage + 1}/${pages.size}")) -// -// content.clearChildren() -// content.child(pages[currentPage]) -// -// previousPage.onPress { -// if (currentPage > 0) { -// currentPage-- -// this.onPagesUpdated() -// } -// } -// nextPage.onPress { -// if (currentPage < pages.lastIndex) { -// currentPage++ -// this.onPagesUpdated() -// } -// } -// -// levelUpButton.onPress { -// levelAmount.value.toIntOrNull()?.let { NetworkingChannels.MODIFY.clientHandle().send(NetworkingPackets.Level(it)) } -// } -// -// onLevelUpdated.subscribe { this.updateLevelUpButton() } -// -// exit.onPress { this.onClose() } + listLayout.child( + Components.button(Component.empty()) { + listCollapsed = !listCollapsed + updateCollapseState(contentLayout, listLayout, it) + } + .apply { + renderer(ButtonRenderers.STANDARD) + textShadow(false) + verticalSizing(Sizing.fill(100)) + + updateCollapseState(contentLayout, listLayout, this) + } + ) + + listLayout.child( + Containers.verticalScroll(Sizing.fill(90), Sizing.fill(100), Containers.verticalFlow(Sizing.content(), Sizing.content()) + .apply { + gap(2) + padding(Insets.of(4)) + verticalAlignment(VerticalAlignment.TOP) + horizontalAlignment(HorizontalAlignment.CENTER) + } + .also { vf -> + pages.forEachIndexed { index, (resource) -> + vf.child( + Components.button(Component.translatable("playerex.ui.menu.${resource.toLanguageKey()}")) { this.switchPage(index) } + .renderer(ButtonRenderers.STANDARD) + ) + } + } + ) + .apply { + positioning(Positioning.relative(100, 0)) + } + .id("scroll") + ) + + val levelUpButton = rootComponent.childById(ButtonComponent::class, "level:button")!! + val exit = rootComponent.childById(ButtonComponent::class, "exit")!! + + + levelAmount.setFilter(InputHelper::isUIntInput) + levelAmount.onChanged().subscribe { updateLevelUpButton() } + + onLevelUpdated(player.level.toInt()) + onExperienceUpdated() + + switchPage(0) + + onLevelUpdated.subscribe { updateLevelUpButton() } + + levelUpButton.onPress { levelAmount.value.toIntOrNull()?.let { NetworkingChannels.MODIFY.clientHandle().send(NetworkingPackets.Level(it)) } } + exit.onPress { this.onClose() } } /** Whenever the player's experience is changed, refreshing the current status of experience-tied ui elements. */ fun onExperienceUpdated() { -// updateLevelUpButton() -// updateProgressBar() + this.uiAdapter.rootComponent.childById(LabelComponent::class.java, "experience")!!.apply { + text(Component.literal(player.experienceLevel.toString())) + } + updateLevelUpButton() + updateProgressBar() } override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { diff --git a/src/client/kotlin/com/bibireden/playerex/ui/PlayerEXTextures.kt b/src/client/kotlin/com/bibireden/playerex/ui/PlayerEXTextures.kt new file mode 100644 index 00000000..b2997355 --- /dev/null +++ b/src/client/kotlin/com/bibireden/playerex/ui/PlayerEXTextures.kt @@ -0,0 +1,10 @@ +package com.bibireden.playerex.ui + +import com.bibireden.playerex.PlayerEX +import org.jetbrains.annotations.ApiStatus + +@ApiStatus.Internal +object PlayerEXTextures { + @JvmField + val RHOMBUS = PlayerEX.id("textures/ui/rhombus.png") +} \ No newline at end of file diff --git a/src/client/kotlin/com/bibireden/playerex/ui/components/AttributeComponent.kt b/src/client/kotlin/com/bibireden/playerex/ui/components/AttributeComponent.kt index 274fd5df..49fa3efa 100644 --- a/src/client/kotlin/com/bibireden/playerex/ui/components/AttributeComponent.kt +++ b/src/client/kotlin/com/bibireden/playerex/ui/components/AttributeComponent.kt @@ -12,6 +12,7 @@ import com.bibireden.playerex.ui.components.buttons.AttributeButtonComponent import com.bibireden.playerex.ui.components.labels.AttributeLabelComponent import com.bibireden.playerex.ui.util.Colors import io.wispforest.owo.ui.component.Components +import io.wispforest.owo.ui.container.Containers import io.wispforest.owo.ui.container.FlowLayout import io.wispforest.owo.ui.core.* import net.minecraft.ChatFormatting @@ -25,7 +26,7 @@ private val StackingBehavior.symbol: String get() = if (this == StackingBehavior.Add) "+" else "×" class AttributeComponent(private val attribute: Attribute, private val player: Player, val component: IPlayerDataComponent) : FlowLayout(Sizing.fill(100), Sizing.fixed(18), Algorithm.HORIZONTAL) { - val label: AttributeLabelComponent + private val label: AttributeLabelComponent fun refresh() { val entries = DataAttributesAPI.clientManager.functions[attribute.id] @@ -60,17 +61,21 @@ class AttributeComponent(private val attribute: Attribute, private val player: P child( Components.label(Component.translatable(attribute.descriptionId)) .verticalTextAlignment(VerticalAlignment.CENTER) - .sizing(Sizing.content(), Sizing.fill(100)) - .positioning(Positioning.relative(0, 50)) + .horizontalTextAlignment(HorizontalAlignment.LEFT) + .sizing(Sizing.fill(40), Sizing.content()) .id("${attribute.id}:label") ) - child(AttributeButtonComponent(attribute, player, component, ButtonType.Remove)) - child( - AttributeLabelComponent(attribute, player).also { label = it } - .horizontalSizing(Sizing.fill(34)) - ) - child(AttributeButtonComponent(attribute, player, component, ButtonType.Add)) + child(Containers.horizontalFlow(Sizing.fill(60), Sizing.content()).apply { + horizontalAlignment(HorizontalAlignment.RIGHT) + verticalAlignment(VerticalAlignment.CENTER) + + gap(6) + + child(AttributeButtonComponent(attribute, player, component, ButtonType.Remove)) + child(AttributeLabelComponent(attribute, player).also { label = it }) + child(AttributeButtonComponent(attribute, player, component, ButtonType.Add)) + }) horizontalAlignment(HorizontalAlignment.RIGHT) verticalAlignment(VerticalAlignment.CENTER) diff --git a/src/client/kotlin/com/bibireden/playerex/ui/components/AttributeListComponent.kt b/src/client/kotlin/com/bibireden/playerex/ui/components/AttributeListComponent.kt index 943e0537..a89b6c8d 100644 --- a/src/client/kotlin/com/bibireden/playerex/ui/components/AttributeListComponent.kt +++ b/src/client/kotlin/com/bibireden/playerex/ui/components/AttributeListComponent.kt @@ -4,19 +4,20 @@ import com.bibireden.data_attributes.api.attribute.EntityAttributeSupplier import io.wispforest.owo.ui.component.Components import io.wispforest.owo.ui.container.Containers import io.wispforest.owo.ui.container.FlowLayout +import io.wispforest.owo.ui.core.Color import io.wispforest.owo.ui.core.Sizing import net.minecraft.world.entity.player.Player import net.minecraft.network.chat.Component import kotlin.jvm.optionals.getOrNull class AttributeListComponent(translationKey: String, private val player: Player, val attributes: List) : FlowLayout(Sizing.fill(25), Sizing.content(), Algorithm.VERTICAL) { - val entriesSection: FlowLayout + private val entriesSection: FlowLayout init { child(Components.label(Component.translatable(translationKey)).horizontalSizing(Sizing.fill(100))) - child(Components.box(Sizing.fill(100), Sizing.fixed(2))) + child(Components.box(Sizing.fill(100), Sizing.fixed(1)).color(Color.ofArgb(0x10FFFFFF))) entriesSection = Containers.verticalFlow(Sizing.fill(100), Sizing.content()) - .apply { gap(4) }.also(::child) + .apply { gap(10) }.also(::child) gap(4) refresh() diff --git a/src/client/kotlin/com/bibireden/playerex/ui/components/ProgressBarComponent.kt b/src/client/kotlin/com/bibireden/playerex/ui/components/ProgressBarComponent.kt new file mode 100644 index 00000000..8e8c9f50 --- /dev/null +++ b/src/client/kotlin/com/bibireden/playerex/ui/components/ProgressBarComponent.kt @@ -0,0 +1,58 @@ +package com.bibireden.playerex.ui.components + +import io.wispforest.owo.ui.component.BoxComponent +import io.wispforest.owo.ui.component.Components +import io.wispforest.owo.ui.container.FlowLayout +import io.wispforest.owo.ui.core.Color +import io.wispforest.owo.ui.core.Easing +import io.wispforest.owo.ui.core.Sizing +import org.jetbrains.annotations.ApiStatus +import kotlin.jvm.Throws + +@ApiStatus.Internal +class ProgressBarComponent(horizontalSizing: Sizing, verticalSizing: Sizing, private val easing: Easing = Easing.SINE, private val fill: Boolean = true) : FlowLayout(horizontalSizing, verticalSizing, Algorithm.HORIZONTAL) { + /** The current percentage of the progress bar. */ + var percentage: Double = 0.0 + set(amount) { + progress((amount * 100).toInt()) + field = amount + } + + private val progressBar = Components.box(Sizing.fill(0), verticalSizing) + .fill(fill) + .direction(BoxComponent.GradientDirection.LEFT_TO_RIGHT) + + private val progressBarBackground = Components.box(Sizing.fill(100), verticalSizing) + .apply { + color(Color.WHITE) + fill(fill) + zIndex(-1) + } + + init { + child(progressBar) + child(progressBarBackground) + } + + fun color(start: Color, end: Color? = null): ProgressBarComponent { + if (end == null) { + progressBar.color(start) + } + else { + progressBar.startColor(start) + progressBar.endColor(end) + } + return this + } + + fun backgroundColor(color: Color): ProgressBarComponent { + progressBarBackground.color(color) + return this + } + + @Throws(IllegalArgumentException::class) + private fun progress(percentage: Int) { + if (percentage < 0 || percentage > 100) throw IllegalArgumentException("Percentage must be between 0 and 100") + progressBar.horizontalSizing(Sizing.fill(percentage)) + } +} \ No newline at end of file diff --git a/src/client/kotlin/com/bibireden/playerex/ui/components/buttons/AttributeButtonComponent.kt b/src/client/kotlin/com/bibireden/playerex/ui/components/buttons/AttributeButtonComponent.kt index 0b1a724f..84b67b8f 100644 --- a/src/client/kotlin/com/bibireden/playerex/ui/components/buttons/AttributeButtonComponent.kt +++ b/src/client/kotlin/com/bibireden/playerex/ui/components/buttons/AttributeButtonComponent.kt @@ -2,6 +2,7 @@ package com.bibireden.playerex.ui.components.buttons import com.bibireden.data_attributes.api.DataAttributesAPI import com.bibireden.data_attributes.api.attribute.IEntityAttribute +import com.bibireden.data_attributes.ui.renderers.ButtonRenderers import com.bibireden.playerex.components.player.IPlayerDataComponent import com.bibireden.playerex.ext.id import com.bibireden.playerex.networking.NetworkingChannels @@ -10,6 +11,7 @@ import com.bibireden.playerex.ui.PlayerEXScreen import com.bibireden.playerex.ui.childById import io.wispforest.owo.ui.component.ButtonComponent import io.wispforest.owo.ui.component.TextBoxComponent +import io.wispforest.owo.ui.core.Insets import io.wispforest.owo.ui.core.Sizing import net.minecraft.world.entity.ai.attributes.Attribute import net.minecraft.world.entity.player.Player @@ -18,8 +20,8 @@ import net.minecraft.network.chat.Component class AttributeButtonComponent(val attribute: Attribute, private val player: Player, private val component: IPlayerDataComponent, val type: PlayerEXScreen.ButtonType) : ButtonComponent( Component.literal(type.symbol), { - // reference text-box to get the necessary value to send to server - it.parent()?.parent()?.childById(TextBoxComponent::class, "input")?.let { box -> + // reference text-box to get the necessary value to send to server (ik it's bad, we fix later) + it.parent()?.parent()?.parent()?.childById(TextBoxComponent::class, "input")?.let { box -> val amount = box.value.toDoubleOrNull() ?: return@let val points = type.getPointsFromComponent(component) @@ -32,14 +34,14 @@ class AttributeButtonComponent(val attribute: Attribute, private val player: Pla } ) { init { - sizing(Sizing.fixed(12), Sizing.fixed(12)) - + sizing(Sizing.content(), Sizing.fixed(9)) + renderer(ButtonRenderers.STANDARD) refresh() } fun refresh() { DataAttributesAPI.getValue(attribute, player).ifPresent { value -> - val max = (attribute as IEntityAttribute).`data_attributes$max`(); + val max = (attribute as IEntityAttribute).`data_attributes$max`() this.active(when (type) { PlayerEXScreen.ButtonType.Add -> component.skillPoints > 0 && max > value PlayerEXScreen.ButtonType.Remove -> component.refundablePoints > 0 && value > 0 diff --git a/src/client/kotlin/com/bibireden/playerex/ui/components/labels/AttributeLabelComponent.kt b/src/client/kotlin/com/bibireden/playerex/ui/components/labels/AttributeLabelComponent.kt index 1bdb60ad..6627571e 100644 --- a/src/client/kotlin/com/bibireden/playerex/ui/components/labels/AttributeLabelComponent.kt +++ b/src/client/kotlin/com/bibireden/playerex/ui/components/labels/AttributeLabelComponent.kt @@ -7,6 +7,7 @@ import com.bibireden.playerex.ext.id import com.bibireden.playerex.ui.util.Colors import io.wispforest.owo.ui.component.LabelComponent import io.wispforest.owo.ui.core.HorizontalAlignment +import io.wispforest.owo.ui.core.Sizing import io.wispforest.owo.ui.core.VerticalAlignment import net.minecraft.world.entity.ai.attributes.Attribute import net.minecraft.world.entity.player.Player @@ -16,11 +17,11 @@ private fun createTextFromAttribute(attribute: Attribute, player: Player): Compo val allocatedPoints = player.component.get(attribute).toInt() val actual = DataAttributesAPI.getValue(attribute, player).map(Double::toInt).orElse(0) - val text = Component.literal("(") + val text = Component.literal("[ ") .append(Component.literal("$allocatedPoints").withStyle { it.withColor(Colors.GOLD) }) - .append("/${(attribute as IEntityAttribute).`data_attributes$max`().toInt()})") + .append("/${(attribute as IEntityAttribute).`data_attributes$max`().toInt()} ]") val difference = actual - allocatedPoints if (difference > 0) { @@ -37,6 +38,8 @@ open class AttributeLabelComponent(private val attribute: Attribute, private val this.horizontalTextAlignment(HorizontalAlignment.CENTER) this.verticalTextAlignment(VerticalAlignment.CENTER) + this.horizontalSizing(Sizing.content()) + this.id("${attribute.id}:current_level") this.refresh() diff --git a/src/client/kotlin/com/bibireden/playerex/ui/menus/PlayerEXAttributesMenu.kt b/src/client/kotlin/com/bibireden/playerex/ui/menus/PlayerEXAttributesMenu.kt index 8eeec712..51e0706f 100644 --- a/src/client/kotlin/com/bibireden/playerex/ui/menus/PlayerEXAttributesMenu.kt +++ b/src/client/kotlin/com/bibireden/playerex/ui/menus/PlayerEXAttributesMenu.kt @@ -1,6 +1,8 @@ package com.bibireden.playerex.ui.menus +import com.bibireden.data_attributes.api.DataAttributesAPI import com.bibireden.data_attributes.api.attribute.EntityAttributeSupplier +import com.bibireden.data_attributes.ext.round import com.bibireden.playerex.api.attribute.PlayerEXAttributes import com.bibireden.playerex.ext.component import com.bibireden.playerex.ext.id @@ -11,6 +13,7 @@ import com.bibireden.playerex.ui.components.buttons.AttributeButtonComponent import com.bibireden.playerex.ui.components.labels.AttributeLabelComponent import com.bibireden.playerex.ui.helper.InputHelper import de.dafuqs.additionalentityattributes.AdditionalEntityAttributes +import io.wispforest.owo.ui.component.BoxComponent import io.wispforest.owo.ui.component.Components import io.wispforest.owo.ui.component.TextBoxComponent import io.wispforest.owo.ui.container.Containers @@ -21,10 +24,31 @@ import net.minecraft.core.registries.BuiltInRegistries import net.minecraft.world.entity.ai.attributes.Attributes import net.minecraft.world.entity.player.Player import net.minecraft.network.chat.Component +import net.minecraft.util.Mth import org.jetbrains.annotations.ApiStatus @ApiStatus.Internal class PlayerEXAttributesMenu : MenuComponent(algorithm = Algorithm.HORIZONTAL) { + private val lungProgressBar = ProgressBarComponent(Sizing.fill(65), Sizing.fixed(4)) + .color(Color.ofRgb(0x399ce9), Color.ofRgb(0x68a8d9)) + .apply { positioning(Positioning.relative(50, 100)).id("progress:lung") } + + private val progressHealthBar = ProgressBarComponent(Sizing.fill(65), Sizing.fixed(4)) + .color(Color.ofRgb(0xea233a), Color.ofRgb(0xf3394f)) + .apply { positioning(Positioning.relative(50, 80)).id("progress:health") } + + private val labelHealth = Components.label(Component.literal("")) + .apply { + color(Color.ofArgb(0x32FFFFFF)) + positioning(Positioning.relative(50, 74)) + } + + private val labelLung = Components.label(Component.literal("")) + .apply { + color(Color.ofArgb(0x32FFFFFF)) + positioning(Positioning.relative(50, 94)) + } + private val MELEE_COMBAT_STATS: List = listOf( EntityAttributeSupplier(Attributes.ATTACK_DAMAGE.id), EntityAttributeSupplier(Attributes.ATTACK_SPEED.id), @@ -59,6 +83,7 @@ class PlayerEXAttributesMenu : MenuComponent(algorithm = Algorithm.HORIZONTAL) { EntityAttributeSupplier(PlayerEXAttributes.FREEZE_RESISTANCE.id), EntityAttributeSupplier(PlayerEXAttributes.LIGHTNING_RESISTANCE.id), EntityAttributeSupplier(PlayerEXAttributes.POISON_RESISTANCE.id), + EntityAttributeSupplier(PlayerEXAttributes.WITHER_RESISTANCE.id), ) /** Whenever ANY attribute gets updated. */ @@ -91,59 +116,131 @@ class PlayerEXAttributesMenu : MenuComponent(algorithm = Algorithm.HORIZONTAL) { } + private fun onHealthUpdated() { + val player = client!!.player!! + val percentage = Mth.clamp((player.health / player.maxHealth).toDouble(), 0.0, 100.0) + progressHealthBar.percentage = percentage + labelHealth.text(Component.translatable("playerex.ui.main.labels.health").append(" - ${player.health.toDouble().round(1)}")) + } + + private fun onLungCapacityUpdated() { + val player = client!!.player!! + val percentage = Mth.clamp(player.airSupply.toDouble() / player.maxAirSupply, 0.0, 100.0) + lungProgressBar.percentage = percentage + labelLung.text(Component.translatable("attribute.name.additionalentityattributes.generic.lung_capacity").append(" - ${player.airSupply}")) + } + override fun build(screenRoot: FlowLayout) { val player = client?.player ?: return val component = playerComponent ?: return - child(Containers.verticalScroll( - Sizing.fill(45), - Sizing.fill(100), - Containers.verticalFlow(Sizing.fill(100), Sizing.content()).apply { - child(Containers.horizontalFlow(Sizing.fill(100), Sizing.content(2)).apply { - child(Components.label(Component.translatable("playerex.ui.category.attributes"))) - child( - Components.textBox(Sizing.fixed(27)) - .text("1") - .also { - it.setMaxLength(4) - it.setFilter(InputHelper::isUIntInput) - it.onChanged().subscribe { onInputFieldUpdated(player) } - } - .verticalSizing(Sizing.fixed(10)) - .positioning(Positioning.relative(100, 0)) - .id("input") + // -- divider line -- // + + child(Components.box(Sizing.fixed(2), Sizing.fill(100)).color(Color.ofArgb(0x32FFFFFF)).positioning(Positioning.relative(45, 50))) + child(Components.box(Sizing.fill(55), Sizing.fixed(2)).color(Color.ofArgb(0x32FFFFFF)).positioning(Positioning.relative(100, 50))) + + child(Containers.verticalScroll(Sizing.fill(40), Sizing.fill(70), Containers.verticalFlow(Sizing.fill(100), Sizing.content(6)).apply { + verticalAlignment(VerticalAlignment.CENTER) + gap(10) + padding(Insets.right(5)) + + child(Containers.horizontalFlow(Sizing.fill(100), Sizing.content(4)).apply { + verticalAlignment(VerticalAlignment.CENTER) + + child(Components.label(Component.translatable("playerex.ui.category.attributes")).color(Color.ofArgb(0x32FFFFFF))) + child( + Components.textBox(Sizing.fixed(27)) + .text("1") + .also { + it.setMaxLength(3) + it.setFilter(InputHelper::isUIntInput) + it.onChanged().subscribe { onInputFieldUpdated(player) } + } + .verticalSizing(Sizing.fixed(15)) + .positioning(Positioning.relative(100, 50)) + .id("input") + ) + }) + + child( + Components.box(Sizing.fill(100), Sizing.fixed(2)) + .color(Color.ofArgb(0x36FFFFFF)) + ) + + for (id in PlayerEXAttributes.PRIMARY_ATTRIBUTE_IDS) { + val attribute = BuiltInRegistries.ATTRIBUTE.get(id) ?: continue + child(AttributeComponent(attribute, player, component)) + child( + Components.box(Sizing.fill(100), Sizing.fixed(2)).fill(true) + .color(Color.ofArgb(0x10FFFFFF)) + ) + } + } + .verticalAlignment(VerticalAlignment.CENTER) + .id("attributes") + ) + .positioning(Positioning.relative(5, 50)) + ) + + // VIGOR + + child( + Containers.verticalFlow(Sizing.fill(54), Sizing.fill(46)) + .apply { + child(Components.label(Component.translatable("playerex.ui.main.sections.vigor")).color(Color.ofArgb(0x32FFFFFF)) + .horizontalSizing(Sizing.content())) + + // health progress + child(labelHealth) + child(progressHealthBar) + + // lung progress + child(labelLung) + child(lungProgressBar) + + child(Containers.verticalScroll( + Sizing.fill(100), + Sizing.fill(50), + Containers.horizontalFlow(Sizing.fill(100), Sizing.content()).apply { + child(AttributeListComponent("playerex.ui.main.categories.vitality", player, VITALITY_STATS).horizontalSizing(Sizing.fill(45))) + child(AttributeListComponent("playerex.ui.main.categories.resistance", player, RESISTANCE_STATS).horizontalSizing(Sizing.fill(45))) + gap(8) + alignment(HorizontalAlignment.CENTER, VerticalAlignment.TOP) + } + ) + .positioning(Positioning.relative(50, 20)) ) - }) - child(Components.box(Sizing.fill(100), Sizing.fixed(2))) - verticalAlignment(VerticalAlignment.TOP) - gap(5) - padding(Insets.right(5)) - children(PlayerEXAttributes.PRIMARY_ATTRIBUTE_IDS.mapNotNull(BuiltInRegistries.ATTRIBUTE::get).map { AttributeComponent(it, player, component) }) - }.id("attributes")) + } + .horizontalAlignment(HorizontalAlignment.CENTER) + .positioning(Positioning.relative(100, 100)) ) - child(Containers.verticalScroll( - Sizing.content(), - Sizing.fill(100), - Containers.verticalFlow(Sizing.content(), Sizing.content()).apply { - child(AttributeListComponent("playerex.ui.main.categories.vitality", player, VITALITY_STATS)) - child(AttributeListComponent("playerex.ui.main.categories.resistance", player, RESISTANCE_STATS)) - padding(Insets.right(5)) - gap(8) - } - )) + // COMBAT STATS child( - Containers.verticalScroll( - Sizing.content(), - Sizing.fill(100), - Containers.verticalFlow(Sizing.content(), Sizing.content()).apply { - child(AttributeListComponent("playerex.ui.main.categories.melee_combat", player, MELEE_COMBAT_STATS)) - child(AttributeListComponent("playerex.ui.main.categories.ranged_combat", player, RANGED_COMBAT_STATS)) - child(AttributeListComponent("playerex.ui.main.categories.defense_combat", player, DEFENSE_COMBAT_STATS)) - padding(Insets.right(5)) - gap(10) - }.id("combat_stats") - ) + Containers.verticalFlow(Sizing.fill(54), Sizing.fill(46)) + .apply { + child(Components.label(Component.translatable("playerex.ui.main.sections.combat_stats")).color(Color.ofArgb(0x32FFFFFF)) + .horizontalSizing(Sizing.content())) + + child( + Containers.verticalScroll( + Sizing.fill(100), + Sizing.fill(90), + Containers.horizontalFlow(Sizing.fill(100), Sizing.content()).apply { + child(AttributeListComponent("playerex.ui.main.categories.melee_combat", player, MELEE_COMBAT_STATS).horizontalSizing(Sizing.fill(30))) + child(AttributeListComponent("playerex.ui.main.categories.ranged_combat", player, RANGED_COMBAT_STATS).horizontalSizing(Sizing.fill(30))) + child(AttributeListComponent("playerex.ui.main.categories.defense_combat", player, DEFENSE_COMBAT_STATS).horizontalSizing(Sizing.fill(30))) + padding(Insets.vertical(15)) + gap(10) + alignment(HorizontalAlignment.CENTER, VerticalAlignment.TOP) + } + .positioning(Positioning.relative(50, 100)) + .id("combat_stats") + ) + ) + } + .horizontalAlignment(HorizontalAlignment.CENTER) + .positioning(Positioning.relative(100, 0)) ) gap(10) @@ -153,9 +250,19 @@ class PlayerEXAttributesMenu : MenuComponent(algorithm = Algorithm.HORIZONTAL) { onAttributeUpdate() onInputFieldUpdated(player) - onAttributeUpdated.subscribe { _, _ -> + onLungCapacityUpdated() + onHealthUpdated() + + onAttributeUpdated.subscribe { attribute, _ -> onAttributeUpdate() onInputFieldUpdated(player) + + if (attribute == AdditionalEntityAttributes.LUNG_CAPACITY) { + onLungCapacityUpdated() + } + else if (attribute == Attributes.MAX_HEALTH) { + onHealthUpdated() + } } } } \ No newline at end of file diff --git a/src/client/resources/assets/playerex/owo_ui/main_ui_model.xml b/src/client/resources/assets/playerex/owo_ui/main_ui_model.xml index 3380907f..10748f1e 100644 --- a/src/client/resources/assets/playerex/owo_ui/main_ui_model.xml +++ b/src/client/resources/assets/playerex/owo_ui/main_ui_model.xml @@ -44,29 +44,11 @@ - - - - - - - @@ -90,62 +72,111 @@ - 85100 + 8585 0,40 10 center + + + + + 8515 + 100,40 + + 10 + center + + + + + + + 100100 + + + + + + + 6464 + 4,95 + + - - 1002 - true - 0,10 - + + + + + + 1 - - 02 + + 20 + 27 + - true + 3 + bottom + + + - 0,10 - 1 - + 00 - - - - - 1 + 100,50 + - - 10 - 27 - + + 802 + true + white + - 3 - bottom - - - + true - 65100 - bottom + left-to-right + gold + red - 2 + 0,0 + + + + + + + + 40 + + 50,80 diff --git a/src/client/resources/assets/playerex/textures/gui/rhombus.png b/src/client/resources/assets/playerex/textures/gui/rhombus.png new file mode 100644 index 0000000000000000000000000000000000000000..a065c452cb6979376c24c047731f7a90fbe78f19 GIT binary patch literal 727 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=jKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1qucL9r6oh?3y^w370~qEv=}#LT=BJwMkF1yeo4 z@6F#Q166EEjqptK^weVD0CHFvq!?Kl7=bJ=AeM%*L2l7tWCn{f0ojI(ObmQLItqw0 z+gZTk89+7&Bmgl;Ka57Pl7X3lVFEh?3sBy`$k>2!0mMv@de#LHb0z`VAixAPg$b-O z$kGDHg6c9fFaXIWFHt)Zr*{6pXx2W^Y*7;&8h!&`!>rx4BS_APTAz?YyAg3?k}e7uTFn^ zPpqe_@7*?spa7%IDzlPi-BjHmvgwUa=kkv9axMW@0Vz#|X63CKE-ZZ)codB?mYA`) zIkdHNrWq`o-qGVAIWN|zIi)K8IuE0f@e;}M1?dZ=xfY}@l9c2)FP6ukpp!hKfZ@ZM z0!O9|XABmy+_`y|NjTwMLfjmIQt_{cm^!RG*~Oo1z5V(CTg}@aUz41xV(V`$RDQYF z$9l8Xdf9k(F}FLuHai4f`MqlGG%PpN_iNeR9IYT2Q8q(Bup(_of+3c;TsWvlmR^Y*lcf-DPeLHt+@7fn4cFSv) qR<>+D6)x9b5ckvTFMr1QnSU5xoi^NG-!GmAiXl%|KbLh*2~7atbMA8h literal 0 HcmV?d00001 diff --git a/src/main/java/com/bibireden/playerex/mixin/LivingEntityMixin.java b/src/main/java/com/bibireden/playerex/mixin/LivingEntityMixin.java index 21c39417..9e0765f7 100644 --- a/src/main/java/com/bibireden/playerex/mixin/LivingEntityMixin.java +++ b/src/main/java/com/bibireden/playerex/mixin/LivingEntityMixin.java @@ -7,6 +7,8 @@ import net.minecraft.world.InteractionHand; import net.minecraft.world.damagesource.DamageSource; import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.Level; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Unique; import org.spongepowered.asm.mixin.injection.At; @@ -15,6 +17,8 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import org.spongepowered.asm.mixin.injection.callback.LocalCapture; +import java.io.IOException; + @Mixin(LivingEntity.class) public abstract class LivingEntityMixin { diff --git a/src/main/kotlin/com/bibireden/playerex/PlayerEX.kt b/src/main/kotlin/com/bibireden/playerex/PlayerEX.kt index 5f8a2e9e..793338bf 100644 --- a/src/main/kotlin/com/bibireden/playerex/PlayerEX.kt +++ b/src/main/kotlin/com/bibireden/playerex/PlayerEX.kt @@ -44,10 +44,7 @@ object PlayerEX : ModInitializer { fun id(path: String) = ResourceLocation.tryBuild(MOD_ID, path)!! - private val gimmick = listOf( - "Let's do it right this time...", - "We test in production (not really).", - ).random() + private val gimmick = listOf("Let's do it right this time...", "We test in production (not really).", "Extraordinary solutions to extraordinary problems.").random() override fun onInitialize() { NetworkingChannels.NOTIFICATIONS.registerClientboundDeferred(NetworkingPackets.Notify::class.java) diff --git a/src/main/kotlin/com/bibireden/playerex/compat/CompatUtils.kt b/src/main/kotlin/com/bibireden/playerex/compat/CompatUtils.kt deleted file mode 100644 index 70636681..00000000 --- a/src/main/kotlin/com/bibireden/playerex/compat/CompatUtils.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.bibireden.playerex.compat - -import net.fabricmc.loader.api.FabricLoader -import org.jetbrains.annotations.ApiStatus - -@ApiStatus.Internal -object CompatUtils { - fun isModLoaded(id: String): Boolean = FabricLoader.getInstance().isModLoaded(id) -} \ No newline at end of file diff --git a/src/main/resources/assets/playerex/lang/en_us.json b/src/main/resources/assets/playerex/lang/en_us.json index 9f103e90..99e4a61a 100644 --- a/src/main/resources/assets/playerex/lang/en_us.json +++ b/src/main/resources/assets/playerex/lang/en_us.json @@ -13,7 +13,7 @@ "playerex.ui.main.skill_points_header": "SKILL POINTS", "playerex.ui.main.modified_attributes": [ {"text": "❤ ", "color": "#48D19B"}, {"text": "Modified Attributes", "color": "white"} ], - "playerex.ui.category.attributes": "Attributes", + "playerex.ui.category.attributes": "ATTRIBUTES", "playerex.ui.current_level": [ {"text": "❤ ", "color": "#F0C25E"}, @@ -37,6 +37,16 @@ "playerex.ui.nameplate.level": "", + "playerex.ui.menu.playerex.attributes": [ + {"text": "✎ ", "color": "#F0C25E"}, + {"text": "PlayerEX Attributes", "color": "white"} + ], + + "playerex.ui.main.labels.health": "Health", + + "playerex.ui.main.sections.vigor": "VIGOR", + "playerex.ui.main.sections.combat_stats": "COMBAT STATS", + "playerex.ui.main.categories.melee_combat": "⚔ MELEE", "playerex.ui.main.categories.ranged_combat": "🏹 RANGED", "playerex.ui.main.categories.defense_combat": "🛡 DEFENSE",