Skip to content

Commit

Permalink
Integrate MenuComponent to registry, and make UI a helluva lot cleaner
Browse files Browse the repository at this point in the history
  • Loading branch information
bibi-reden committed Jul 23, 2024
1 parent 5240c97 commit 9f56dc0
Show file tree
Hide file tree
Showing 10 changed files with 275 additions and 129 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.bibireden.playerex.registry;

import com.bibireden.playerex.ui.components.MenuComponent;
import org.jetbrains.annotations.NotNull;

import java.util.ArrayList;
import java.util.List;

public final class AttributesMenuRegistry {
@NotNull
private static final List<Class<? extends MenuComponent>> ENTRIES = new ArrayList<>();

/**
* Registers a {@link MenuComponent} to the registry,
* which will be applied to the {@link com.bibireden.playerex.ui.PlayerEXScreen} as a page.
*/
public static void register(@NotNull Class<? extends MenuComponent> menu) { ENTRIES.add(menu); }

@NotNull
public static List<Class<? extends MenuComponent>> get() {
return ENTRIES;
}
}
14 changes: 12 additions & 2 deletions src/client/kotlin/com/bibireden/playerex/PlayerEXClient.kt
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
package com.bibireden.playerex

import com.bibireden.data_attributes.DataAttributes
import com.bibireden.data_attributes.api.DataAttributesAPI
import com.bibireden.data_attributes.api.event.EntityAttributeModifiedEvents
import com.bibireden.playerex.api.attribute.PlayerEXAttributes
import com.bibireden.playerex.api.event.PlayerEXSoundEvents
import com.bibireden.playerex.networking.NetworkingChannels
import com.bibireden.playerex.networking.NetworkingPackets
import com.bibireden.playerex.networking.registerClientbound
import com.bibireden.playerex.networking.types.NotificationType
import com.bibireden.playerex.registry.AttributesMenuRegistry
import com.bibireden.playerex.ui.PlayerEXScreen
import com.bibireden.playerex.ui.menus.AttributeMenu
import net.fabricmc.api.ClientModInitializer
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents
import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper
Expand All @@ -33,18 +37,24 @@ object PlayerEXClient : ClientModInitializer {

EntityAttributeModifiedEvents.MODIFIED.register { attribute, entity, _, _, _ ->
if (entity is PlayerEntity && entity.world.isClient) {
if (entity != MinecraftClient.getInstance().player) return@register

val screen = MinecraftClient.getInstance().currentScreen
if (screen is PlayerEXScreen) {
if (attribute == PlayerEXAttributes.LEVEL) {
screen.onLevelUpdated()
DataAttributesAPI.getValue(attribute, entity).map(Double::toInt).ifPresent(screen::onLevelUpdated)
}
else {
screen.onAttributesUpdated()
DataAttributesAPI.getValue(attribute, entity).ifPresent { value ->
screen.onAttributesUpdated(attribute, value)
}
}
}
}
}

AttributesMenuRegistry.register(AttributeMenu::class.java)

ClientTickEvents.END_CLIENT_TICK.register { client ->
if (PlayerEX.CONFIG.disableAttributesGui) return@register
while (KEYBINDING_MAIN_SCREEN.wasPressed()) {
Expand Down
120 changes: 24 additions & 96 deletions src/client/kotlin/com/bibireden/playerex/ui/PlayerEXScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,74 +4,68 @@ import com.bibireden.data_attributes.DataAttributes
import com.bibireden.data_attributes.api.DataAttributesAPI
import com.bibireden.data_attributes.api.attribute.EntityAttributeSupplier
import com.bibireden.data_attributes.api.attribute.IEntityAttribute
import com.bibireden.data_attributes.api.attribute.StackingBehavior
import com.bibireden.playerex.PlayerEXClient
import com.bibireden.playerex.api.attribute.PlayerEXAttributes
import com.bibireden.playerex.components.PlayerEXComponents
import com.bibireden.playerex.components.player.IPlayerDataComponent
import com.bibireden.playerex.ext.id
import com.bibireden.playerex.ext.level
import com.bibireden.playerex.networking.NetworkingChannels
import com.bibireden.playerex.networking.NetworkingPackets
import com.bibireden.playerex.networking.types.UpdatePacketType
import com.bibireden.playerex.registry.AttributesMenuRegistry
import com.bibireden.playerex.ui.components.MenuComponent
import com.bibireden.playerex.ui.components.labels.AttributeComponent
import com.bibireden.playerex.ui.components.labels.AttributeLabelComponent
import com.bibireden.playerex.util.PlayerEXUtil
import io.wispforest.owo.ui.base.BaseUIModelScreen
import io.wispforest.owo.ui.component.ButtonComponent
import io.wispforest.owo.ui.component.Components
import io.wispforest.owo.ui.component.LabelComponent
import io.wispforest.owo.ui.component.TextBoxComponent
import io.wispforest.owo.ui.container.Containers
import io.wispforest.owo.ui.container.FlowLayout
import io.wispforest.owo.ui.core.Component
import io.wispforest.owo.ui.core.ParentComponent
import io.wispforest.owo.ui.core.Positioning
import io.wispforest.owo.ui.core.Sizing
import net.minecraft.client.network.ClientPlayerEntity
import net.minecraft.client.resource.language.I18n
import net.minecraft.entity.attribute.EntityAttribute
import net.minecraft.entity.player.PlayerEntity
import net.minecraft.registry.Registries
import net.minecraft.text.Text
import net.minecraft.util.Colors
import net.minecraft.util.Formatting
import kotlin.reflect.KClass

private val StackingBehavior.symbol: String
get() = if (this == StackingBehavior.Add) "+" else "×"

// Transformers
fun <T : Component> ParentComponent.childById(clazz: KClass<T>, id: String) = this.childById(clazz.java, id)

/** Primary screen for the mod that brings everything intended together. */
class PlayerEXScreen : BaseUIModelScreen<FlowLayout>(FlowLayout::class.java, DataSource.asset(PlayerEXClient.MAIN_UI_SCREEN_ID)) {
private var currentPage = 0

private var pages: MutableList<MutableList<Component>> = AttributesMenuRegistry.get()
private val pages: MutableList<MenuComponent> = mutableListOf()

private val playerComponent by lazy { PlayerEXComponents.PLAYER_DATA.get(this.client?.player!!) }

override fun shouldPause(): Boolean = false

/** Whenever the level attribute gets modified, and on initialization of the screen, this will be called. */
fun onLevelUpdated() {
fun onLevelUpdated(level: Int) {
val player = client?.player ?: return

val root = this.uiAdapter.rootComponent

root.childById(LabelComponent::class, "level:current")?.apply {
text(Text.translatable("playerex.ui.current_level", player.level.toInt(), PlayerEXUtil.getRequiredXpForNextLevel(player)))
}

updatePointsAvailable()
updateLevelUpButton(player, root.childById(TextBoxComponent::class, "level:amount")!!.text, root.childById(ButtonComponent::class, "level:button")!!)

this.uiAdapter.rootComponent.forEachDescendant { descendant ->
if (descendant is MenuComponent) descendant.onLevelUpdatedEvents.sink().onLevelUpdated(level)
}
}

/** Whenever any attribute is updated, this will be called. */
// todo: this is subject to change... and needs to be done first
fun onAttributesUpdated() {
PlayerEXAttributes.PRIMARY_ATTRIBUTE_IDS.forEach {
this.uiAdapter.rootComponent.childById(LabelComponent::class, "${it}:current_level")?.apply {
text(EntityAttributeSupplier(it).get()?.let { attribute -> attributeLabel(attribute, client?.player!!) })
}
fun onAttributesUpdated(attribute: EntityAttribute, value: Double) {
this.uiAdapter.rootComponent.forEachDescendant { descendant ->
if (descendant is MenuComponent) descendant.onAttributeUpdatedEvents.sink().onAttributeUpdated(attribute, value)
}
updatePointsAvailable()
}
Expand All @@ -94,7 +88,7 @@ class PlayerEXScreen : BaseUIModelScreen<FlowLayout>(FlowLayout::class.java, Dat

pageCounter.text(Text.of("${currentPage + 1}/${pages.size}"))
content.clearChildren()
content.children(pages[currentPage])
content.child(pages[currentPage])
}

private fun updateLevelUpButton(player: PlayerEntity, text: String, levelUpButton: ButtonComponent) {
Expand All @@ -106,77 +100,6 @@ class PlayerEXScreen : BaseUIModelScreen<FlowLayout>(FlowLayout::class.java, Dat
levelUpButton.tooltip(Text.translatable("playerex.ui.level_button", PlayerEXUtil.getRequiredXpForLevel(player, result), amount, player.experienceLevel))
}

private fun attributeButtonComponent(attribute: EntityAttribute, type: AttributeButtonComponentType): Component {
val player = this.client?.player ?: return Components.label(Text.of("ohno"))
return Components.button(Text.of(type.symbol)) {
it.parent()?.childById(TextBoxComponent::class, "entry:${attribute.id}")?.let { ta ->
val amount = ta.text.toDoubleOrNull() ?: return@let
val points = if (type == AttributeButtonComponentType.Add) playerComponent.skillPoints else playerComponent.refundablePoints

if (points < amount) return@let // invalid, not enough points.

DataAttributesAPI.getValue(attribute, player).ifPresent { NetworkingChannels.MODIFY.clientHandle().send(NetworkingPackets.Update(type.packet, attribute.id, amount.toInt())) }
}
}
.renderer(ButtonComponent.Renderer.flat(Colors.BLACK, Colors.BLACK, Colors.BLACK))
.sizing(Sizing.fixed(12), Sizing.fixed(12))
}

// todo: migrate
private fun createAttributeComponent(attribute: EntityAttribute): Component {
return Containers.horizontalFlow(Sizing.fill(100), Sizing.fixed(18)).also {
it.child(Components.label(Text.translatable(attribute.translationKey)).sizing(Sizing.content(), Sizing.fill(100))
.also { label ->
// todo: allow data_attributes to have API funcs for obtaining this data.
val entries = DataAttributes.FUNCTIONS_CONFIG.functions.data[attribute.id]
if (!entries.isNullOrEmpty()) {
label.tooltip(Text.translatable("playerex.ui.attribute_functions").also { text ->
text.append("\n\n")
entries.forEach { function ->
EntityAttributeSupplier(function.id).get()?.translationKey?.let { key ->
text.append(Text.translatable(key).formatted(Formatting.AQUA))
}
text.append(Text.literal(" ${function.behavior.symbol}").formatted(Formatting.GREEN))
text.append(Text.literal("${function.value}"))
text.append(Text.literal(" (${DataAttributesAPI.getValue(EntityAttributeSupplier(function.id), this.client?.player!!).orElse(0.0)})\n").formatted(Formatting.GRAY))
}
text.formatted(Formatting.ITALIC)
})
}
}.id("${attribute.id}:label")
)
it.child(Components.label(attributeLabel(attribute, this.client?.player!!)).id("${attribute.id}:current_level"))
it.child(
Containers.horizontalFlow(Sizing.fill(50), Sizing.fill(100)).also {
it.child(attributeButtonComponent(attribute, AttributeButtonComponentType.Remove))
it.child(attributeButtonComponent(attribute, AttributeButtonComponentType.Add))
it.child(
Components.textBox(Sizing.fixed(27))
.text("1")
.verticalSizing(Sizing.fixed(12))
.id("entry:${attribute.id}")
)
it.gap(4)
}.positioning(Positioning.relative(100, 0))
)
it.gap(3)
}
}

private fun attributeLabel(attribute: EntityAttribute, player: ClientPlayerEntity): Text? {
return Text.literal("(").append(Text.literal("${DataAttributesAPI.getValue(attribute, player).map(Double::toInt).orElse(0)}").formatted(Formatting.GOLD)).append("/${(attribute as IEntityAttribute).`data_attributes$max`().toInt()})")
}

// todo: migrate to Registry once completed
private fun temporarySupplyAttributePage(): MutableList<Component> = mutableListOf(
Containers.verticalFlow(Sizing.fill(75), Sizing.content()).also {
it.child(Components.label(Text.translatable("playerex.ui.category.primary_attributes")))
it.child(Components.box(Sizing.fill(60), Sizing.fixed(2)))
it.children(PlayerEXAttributes.PRIMARY_ATTRIBUTE_IDS.mapNotNull(Registries.ATTRIBUTE::get).map(::createAttributeComponent))
it.gap(5)
}.positioning(Positioning.relative(10, 25))
)

override fun build(rootComponent: FlowLayout) {
val player = client?.player ?: return

Expand All @@ -193,16 +116,19 @@ class PlayerEXScreen : BaseUIModelScreen<FlowLayout>(FlowLayout::class.java, Dat
val content = rootComponent.childById(FlowLayout::class, "content")!!
val footer = rootComponent.childById(FlowLayout::class, "footer")!!

pages = mutableListOf(temporarySupplyAttributePage())
AttributesMenuRegistry.get().forEach {
val instance = it.getDeclaredConstructor().newInstance()
instance.build(player, this.uiAdapter, playerComponent)
pages.add(instance)
}

this.onLevelUpdated()
this.onAttributesUpdated()
this.onLevelUpdated(player.level.toInt())
this.onPagesUpdated()

pageCounter.text(Text.of("${currentPage + 1}/${pages.size}"))

content.clearChildren()
content.children(pages[currentPage])
content.child(pages[currentPage])

previousPage.onPress {
if (currentPage > 0) {
Expand All @@ -228,6 +154,8 @@ class PlayerEXScreen : BaseUIModelScreen<FlowLayout>(FlowLayout::class.java, Dat
Add,
Remove;

fun getPointsFromComponent(component: IPlayerDataComponent): Int = if (this == Add) component.skillPoints else component.refundablePoints

val symbol: String
get() = if (this == Add) "+" else "-"

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.bibireden.playerex.ui.components

import com.bibireden.playerex.components.player.IPlayerDataComponent
import com.bibireden.playerex.ui.PlayerEXScreen
import io.wispforest.owo.ui.container.FlowLayout
import io.wispforest.owo.ui.core.OwoUIAdapter
import io.wispforest.owo.ui.core.Sizing
import io.wispforest.owo.util.EventSource
import io.wispforest.owo.util.EventStream
import net.minecraft.client.network.ClientPlayerEntity
import net.minecraft.entity.attribute.EntityAttribute
import org.jetbrains.annotations.ApiStatus

/**
* A component meant to be used in the **PlayerEX** screen.
*
* This allows for other mods to create their own custom logic,
* and have the benefits of a unique instance that is attached to the primary mod.
*/
@ApiStatus.OverrideOnly
abstract class MenuComponent(horizontalSizing: Sizing, verticalSizing: Sizing, algorithm: Algorithm) : FlowLayout(horizontalSizing, verticalSizing, algorithm) {
val onLevelUpdatedEvents = OnLevelUpdated.stream
val onAttributeUpdatedEvents = OnAttributeUpdated.stream

val onLevelUpdated: EventSource<OnLevelUpdated> = onLevelUpdatedEvents.source()
val onAttributeUpdated: EventSource<OnAttributeUpdated> = onAttributeUpdatedEvents.source()

/** When the [PlayerEXScreen] is ready to be constructed, this function (if the component is registered) will be called.*/
abstract fun build(player: ClientPlayerEntity, adapter: OwoUIAdapter<FlowLayout>, component: IPlayerDataComponent)

fun interface OnLevelUpdated {
fun onLevelUpdated(level: Int)

companion object {
val stream: EventStream<OnLevelUpdated> get() = EventStream { subscribers ->
OnLevelUpdated { level -> subscribers.forEach { it.onLevelUpdated(level) } }
}
}
}

fun interface OnAttributeUpdated {
fun onAttributeUpdated(attribute: EntityAttribute, level: Double)

companion object {
val stream: EventStream<OnAttributeUpdated> get() = EventStream { subscribers ->
OnAttributeUpdated { attribute, value -> subscribers.forEach { it.onAttributeUpdated(attribute, value) } }
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.bibireden.playerex.ui.components.labels

import com.bibireden.data_attributes.api.DataAttributesAPI
import com.bibireden.playerex.components.player.IPlayerDataComponent
import com.bibireden.playerex.ext.id
import com.bibireden.playerex.networking.NetworkingChannels
import com.bibireden.playerex.networking.NetworkingPackets
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.Sizing
import net.minecraft.entity.attribute.EntityAttribute
import net.minecraft.entity.player.PlayerEntity
import net.minecraft.text.Text
import net.minecraft.util.Colors

class AttributeButtonComponent(attribute: EntityAttribute, player: PlayerEntity, component: IPlayerDataComponent, type: PlayerEXScreen.AttributeButtonComponentType) : ButtonComponent(
Text.literal(type.symbol),
{
// reference text-box to get needed value to send to server
it.parent()?.childById(TextBoxComponent::class, "entry:${attribute.id}")?.let { box ->
val amount = box.text.toDoubleOrNull() ?: return@let
val points = type.getPointsFromComponent(component)

if (points < amount) return@let // invalid, not enough points.

DataAttributesAPI.getValue(attribute, player).ifPresent { NetworkingChannels.MODIFY.clientHandle().send(
NetworkingPackets.Update(type.packet, attribute.id, amount.toInt()))
}
}
}
) {
init {
renderer(Renderer.flat(Colors.BLACK, Colors.BLACK, Colors.BLACK))
sizing(Sizing.fixed(12), Sizing.fixed(12))
}
}
Loading

0 comments on commit 9f56dc0

Please sign in to comment.