diff --git a/build.gradle b/build.gradle index 4280130..1066186 100644 --- a/build.gradle +++ b/build.gradle @@ -10,14 +10,15 @@ base { archivesName = project.archives_base_name } - repositories { maven { url 'https://maven.wispforest.io/releases/' } - maven { url 'https://jitpack.io' } // required since owo 0.13.0, for a transitive dependency + maven { url 'https://jitpack.io' } } dependencies { minecraft "com.mojang:minecraft:26.1.2" + // NO mappings block — 26.1+ ships unobfuscated, mappings aren't used + implementation "net.fabricmc:fabric-loader:0.18.4" implementation "net.fabricmc.fabric-api:fabric-api:0.154.0+26.1.2" @@ -40,10 +41,6 @@ processResources { def targetJavaVersion = 25 tasks.withType(JavaCompile).configureEach { - // ensure that the encoding is set to UTF-8, no matter what the system default is - // this fixes some edge cases with special characters not displaying correctly - // see http://yodaconditions.net/blog/fix-for-java-file-encoding-problems-with-gradle.html - // If Javadoc is generated, this must be specified in that task too. it.options.encoding = "UTF-8" if (targetJavaVersion >= 10 || JavaVersion.current().isJava10Compatible()) { it.options.release.set(targetJavaVersion) @@ -55,9 +52,6 @@ java { if (JavaVersion.current() < javaVersion) { toolchain.languageVersion = JavaLanguageVersion.of(targetJavaVersion) } - // Loom will automatically attach sourcesJar to a RemapSourcesJar task and to the "build" task - // if it is present. - // If you remove this line, sources will not be generated. withSourcesJar() } @@ -67,7 +61,6 @@ jar { } } -// configure the maven publication publishing { publications { create("mavenJava", MavenPublication) { @@ -75,12 +68,5 @@ publishing { from components.java } } - - // See https://docs.gradle.org/current/userguide/publishing_maven.html for information on how to set up publishing. - repositories { - // Add repositories to publish to here. - // Notice: This block does NOT have the same function as the block in the top level. - // The repositories here will be used for publishing your artifact, not for - // retrieving dependencies. - } -} + repositories {} +} \ No newline at end of file diff --git a/src/main/java/de/fullrisk/citrusDev/Config.java b/src/main/java/de/fullrisk/citrusDev/Config.java index 78c8c2c..c60dc5c 100644 --- a/src/main/java/de/fullrisk/citrusDev/Config.java +++ b/src/main/java/de/fullrisk/citrusDev/Config.java @@ -13,6 +13,7 @@ import java.nio.file.Path; public class Config { + public int fpsP = 6; public int fpsX = 1; public int fpsY = 1; public String lastModule = "FPS"; diff --git a/src/main/java/de/fullrisk/citrusDev/UI/Module_FPS.java b/src/main/java/de/fullrisk/citrusDev/UI/Module_FPS.java index eea0011..4105296 100644 --- a/src/main/java/de/fullrisk/citrusDev/UI/Module_FPS.java +++ b/src/main/java/de/fullrisk/citrusDev/UI/Module_FPS.java @@ -55,7 +55,7 @@ public class Module_FPS { if (toggle && !shown) { fpsContainer.surface(GLASS_PANEL); - fpsContainer.padding(Insets.of(6)); + fpsContainer.padding(Insets.of(Config.get().fpsP)); fpsContainer.child(fpsLabel); shown = true; } else if (!toggle && shown) { diff --git a/src/main/java/de/fullrisk/citrusDev/UI/MyScreen.java b/src/main/java/de/fullrisk/citrusDev/UI/MyScreen.java index 68df9e1..87eb9d2 100644 --- a/src/main/java/de/fullrisk/citrusDev/UI/MyScreen.java +++ b/src/main/java/de/fullrisk/citrusDev/UI/MyScreen.java @@ -1,6 +1,7 @@ package de.fullrisk.citrusDev.UI; import de.fullrisk.citrusDev.Config; +import de.fullrisk.citrusDev.UIHelper.*; import io.wispforest.owo.ui.base.BaseOwoScreen; import io.wispforest.owo.ui.component.UIComponents; import io.wispforest.owo.ui.container.FlowLayout; @@ -12,20 +13,19 @@ import net.minecraft.network.chat.Component; import net.minecraft.network.chat.FontDescription; import net.minecraft.resources.Identifier; import net.minecraft.sounds.SoundEvents; -import de.fullrisk.citrusDev.UIHelper.ScalableLabel; -import de.fullrisk.citrusDev.UIHelper.ScalableButton; -import de.fullrisk.citrusDev.UIHelper.ScaleValue; +import de.fullrisk.citrusDev.client.PlaytimeTracker; import java.util.List; +import java.util.concurrent.Flow; import static de.fullrisk.citrusDev.UI.Module_FPS.fpsContainer; import static de.fullrisk.citrusDev.UI.Module_FPS.toggle; public class MyScreen extends BaseOwoScreen { - private static final Identifier MC_FIVE = Identifier.fromNamespaceAndPath("citrus-dev", "minecraft_five"); + static final Identifier MC_FIVE = Identifier.fromNamespaceAndPath("citrus-dev", "minecraft_five"); - private static final Surface GLASS_PANEL = Surface.blur(5f, 15f) + static final Surface GLASS_PANEL = Surface.blur(5f, 15f) .and(Surface.flat(0x40FFFFFF)) .and(Surface.outline(0x64de4b)); @@ -47,6 +47,11 @@ public class MyScreen extends BaseOwoScreen { private record TabDef(String label, Runnable onClick) {} private record SettingRow(String label, FlowLayout control) {} + private record QuestData(String title, String description, int requiredPlaytime) { + boolean isUnlocked() { + return PlaytimeTracker.getCurrentPlaytimeSeconds() >= requiredPlaytime; + } + } private FlowLayout content; private final String initialModule; @@ -101,7 +106,7 @@ public class MyScreen extends BaseOwoScreen { row.child(sidebar); content = UIContainers.verticalFlow(Sizing.fixed(CONTENT_WIDTH), Sizing.fixed(ROW_HEIGHT)); - content.gap(6); + content.gap(2); content.padding(Insets.of(4)); content.horizontalAlignment(HorizontalAlignment.CENTER); content.verticalAlignment(VerticalAlignment.TOP); @@ -125,7 +130,6 @@ public class MyScreen extends BaseOwoScreen { rootComponent.child(panel); - // Now that content is attached to the tree, decide what to show. if ("FPS".equals(initialModule)) { openModuleSettings("FPS", fpsModuleSettings()); } else { @@ -154,7 +158,7 @@ public class MyScreen extends BaseOwoScreen { content.clearChildren(); content.horizontalAlignment(HorizontalAlignment.CENTER); content.verticalAlignment(VerticalAlignment.TOP); - buildQuestsPanel(content); + QuestsPanel.build(content, CONTENT_WIDTH, ROW_HEIGHT); }), new TabDef("settings", () -> { content.clearChildren(); @@ -244,7 +248,6 @@ public class MyScreen extends BaseOwoScreen { return button; } - private void buildGeneralPanel(FlowLayout content) { FlowLayout iconButton = UIContainers.horizontalFlow(Sizing.content(), Sizing.content()); iconButton.gap(8); @@ -281,7 +284,7 @@ public class MyScreen extends BaseOwoScreen { iconButton.child(textStack); iconButton.child(UIComponents.spacer()); - FlowLayout settingsButton = makeSmallButton( + FlowLayout settingsButton = SettingsControls.makeSmallButton( FLAT, Identifier.fromNamespaceAndPath("citrus-dev", "textures/icons/settings.png"), () -> { @@ -300,6 +303,7 @@ public class MyScreen extends BaseOwoScreen { toggle = !toggle; fpsLabel.color(Color.ofRgb(toggle ? 0xffffff : 0x797b86)); iconButton.surface(toggle ? GLASS_PANEL : DISABLED); + CitrusToast.show("Module Enabled", "FPS counter is now active"); Minecraft.getInstance().getSoundManager().play( SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK.value(), 2.0F, 2) @@ -313,31 +317,23 @@ public class MyScreen extends BaseOwoScreen { content.child(iconButton); } - private void buildQuestsPanel(FlowLayout content) { - content.child(UIComponents.label(Component.literal("Quests - coming soon"))); - } private void buildSettingsPanel(FlowLayout content) { content.child(UIComponents.label(Component.literal("Settings"))); } - /** - * Opens a per-module settings view inside the "content" container, - * replacing whatever was there before. A back arrow returns to General. - */ private void openModuleSettings(String title, List rows) { content.clearChildren(); - // re-assert alignment explicitly - clearChildren can reset it content.horizontalAlignment(HorizontalAlignment.LEFT); content.verticalAlignment(VerticalAlignment.TOP); FlowLayout header = UIContainers.horizontalFlow(Sizing.fixed(CONTENT_WIDTH - 8), Sizing.content()); header.verticalAlignment(VerticalAlignment.CENTER); header.horizontalAlignment(HorizontalAlignment.LEFT); - header.gap(6); + header.gap(4); - header.child(makeTextButton(FLAT, "<", () -> { + header.child(SettingsControls.makeTextButton(FLAT, "<", () -> { content.clearChildren(); content.horizontalAlignment(HorizontalAlignment.CENTER); content.verticalAlignment(VerticalAlignment.TOP); @@ -350,17 +346,32 @@ public class MyScreen extends BaseOwoScreen { content.child(header); + FlowLayout rowsContainer = UIContainers.verticalFlow(Sizing.fixed(CONTENT_WIDTH - 8), Sizing.content()); + rowsContainer.gap(1); + rowsContainer.verticalAlignment(VerticalAlignment.TOP); + rowsContainer.horizontalAlignment(HorizontalAlignment.LEFT); + rowsContainer.padding(Insets.of(0)); + for (SettingRow row : rows) { - content.child(makeSettingRow(row)); + FlowLayout settingRow = makeSettingRow(row); + settingRow.verticalSizing(Sizing.fixed(16)); // pin each row to a compact fixed height + rowsContainer.child(settingRow); } + + var scrollArea = UIContainers.verticalScroll( + Sizing.fixed(CONTENT_WIDTH - 8), + Sizing.fixed(ROW_HEIGHT - 24), + rowsContainer + ); + content.child(scrollArea); } private FlowLayout makeSettingRow(SettingRow setting) { - FlowLayout row = UIContainers.horizontalFlow(Sizing.fixed(CONTENT_WIDTH - 8), Sizing.content()); - row.verticalAlignment(VerticalAlignment.TOP); + FlowLayout row = UIContainers.horizontalFlow(Sizing.fixed(CONTENT_WIDTH - 8), Sizing.fixed(16)); + row.verticalAlignment(VerticalAlignment.CENTER); row.horizontalAlignment(HorizontalAlignment.LEFT); - row.padding(Insets.of(2)); - row.gap(6); + row.padding(Insets.of(0)); + row.gap(4); row.child(UIComponents.label(Component.literal(setting.label())) .color(Color.ofRgb(0xa3a3ac))); @@ -371,114 +382,28 @@ public class MyScreen extends BaseOwoScreen { } private List fpsModuleSettings() { + List positionStates = List.of( + new SettingsControls.ButtonState("L-TOP", () -> Module_FPS.setPosition(1, 1)), + new SettingsControls.ButtonState("R-TOP", () -> Module_FPS.setPosition(99, 1)), + new SettingsControls.ButtonState("R-BOTTOM", () -> Module_FPS.setPosition(99, 99)), + new SettingsControls.ButtonState("L-BOTTOM", () -> Module_FPS.setPosition(1, 99)) + ); + + FlowLayout sizeSlider = SettingsControls.makeSlider( + FLAT, + 80, + 0.0, 20.0, + 4.0, + value -> (int) value + "px", + value -> Module_FPS.fpsContainer.gap((int) value) + ); + return List.of( - new SettingRow("Position", makeCyclingButton()) + new SettingRow("Position", SettingsControls.makeCyclingButton(GLASS_PANEL, positionStates)), + new SettingRow("Size", sizeSlider) ); } - private record ButtonState(String label, Runnable onSelect) {} - - private FlowLayout makeCyclingButton() { - List states = List.of( - new ButtonState("Off", () -> { - Module_FPS.setPosition(1, 1); - - }), - new ButtonState("Low", () -> { - Module_FPS.setPosition(99, 1); - - }), - new ButtonState("Medium", () -> { - Module_FPS.setPosition(99, 99); - - }), - new ButtonState("High", () -> { - Module_FPS.setPosition(1, 99); - }) - ); - - int[] currentIndex = {0}; - - FlowLayout button = UIContainers.verticalFlow(Sizing.fixed(50), Sizing.fixed(16)); - button.verticalAlignment(VerticalAlignment.CENTER); - button.horizontalAlignment(HorizontalAlignment.CENTER); - button.surface(GLASS_PANEL); - - var label = UIComponents.label(Component.literal(states.get(0).label())) - .color(Color.ofRgb(0xFFFFFF)); - button.child(label); - - button.mouseDown().subscribe((click, doubled) -> { - Minecraft.getInstance().getSoundManager().play( - SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK.value(), 2.0F, 2) - ); - - currentIndex[0] = (currentIndex[0] + 1) % states.size(); - ButtonState newState = states.get(currentIndex[0]); - - label.text(Component.literal(newState.label())); - newState.onSelect().run(); - - return true; - }); - button.mouseEnter().subscribe(() -> { - label.color(Color.ofRgb(0xFFFFFF)); - }); - button.mouseLeave().subscribe(() -> { - label.color(Color.ofRgb(0xCCCCCC)); - }); - return button; - } - - private FlowLayout makeSmallButton(Surface baseSurface, Identifier texture, Runnable onClick) { - FlowLayout button = UIContainers.verticalFlow(Sizing.fixed(16), Sizing.fixed(16)); - button.verticalAlignment(VerticalAlignment.CENTER); - button.horizontalAlignment(HorizontalAlignment.CENTER); - button.surface(baseSurface); - button.child(UIComponents.texture(texture, 0, 0, 12, 12, 12, 12)) - .horizontalAlignment(HorizontalAlignment.CENTER) - .verticalAlignment(VerticalAlignment.CENTER); - - button.mouseEnter().subscribe(() -> button.surface( - Surface.blur(5f, 15f).and(Surface.flat(0x70FFFFFF)).and(Surface.outline(0xFFFFFFFF)) - )); - button.mouseLeave().subscribe(() -> button.surface(baseSurface)); - - button.mouseDown().subscribe((click, doubled) -> { - Minecraft.getInstance().getSoundManager().play( - SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK.value(), 2.0F, 2) - ); - onClick.run(); - return true; - }); - - return button; - } - - private FlowLayout makeTextButton(Surface baseSurface, String text, Runnable onClick) { - FlowLayout button = UIContainers.verticalFlow(Sizing.content(), Sizing.content()); - button.verticalAlignment(VerticalAlignment.CENTER); - button.horizontalAlignment(HorizontalAlignment.CENTER); - button.padding(Insets.of(2, 2, 4, 4)); - button.surface(baseSurface); - button.child(UIComponents.label(Component.literal(text)).color(Color.ofRgb(0xFFFFFF))); - - button.mouseEnter().subscribe(() -> button.surface( - Surface.blur(5f, 15f).and(Surface.flat(0x70FFFFFF)).and(Surface.outline(0xFFFFFFFF)) - )); - button.mouseLeave().subscribe(() -> button.surface(baseSurface)); - - button.mouseDown().subscribe((click, doubled) -> { - Minecraft.getInstance().getSoundManager().play( - SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK.value(), 2.0F, 2) - ); - onClick.run(); - return true; - }); - - return button; - } - @Override public boolean isPauseScreen() { return false; diff --git a/src/main/java/de/fullrisk/citrusDev/UI/OverviewScreen.java b/src/main/java/de/fullrisk/citrusDev/UI/OverviewScreen.java index 6c058d2..0261603 100644 --- a/src/main/java/de/fullrisk/citrusDev/UI/OverviewScreen.java +++ b/src/main/java/de/fullrisk/citrusDev/UI/OverviewScreen.java @@ -1,5 +1,23 @@ package de.fullrisk.citrusDev.UI; -public class OverviewScreen { +import io.wispforest.owo.ui.base.BaseOwoScreen; +import io.wispforest.owo.ui.component.UIComponents; +import io.wispforest.owo.ui.container.FlowLayout; +import io.wispforest.owo.ui.container.UIContainers; +import io.wispforest.owo.ui.core.HorizontalAlignment; +import io.wispforest.owo.ui.core.OwoUIAdapter; +import io.wispforest.owo.ui.core.VerticalAlignment; +import net.minecraft.network.chat.Component; +import io.wispforest.owo.ui.core.*; +import net.minecraft.client.Minecraft; +import net.minecraft.client.resources.sounds.SimpleSoundInstance; +import net.minecraft.network.chat.FontDescription; +import net.minecraft.resources.Identifier; +import net.minecraft.sounds.SoundEvents; +import de.fullrisk.citrusDev.UIHelper.ScalableLabel; +import de.fullrisk.citrusDev.UIHelper.ScalableButton; +import de.fullrisk.citrusDev.UIHelper.ScaleValue; + +public class OverviewScreen{ } \ No newline at end of file diff --git a/src/main/java/de/fullrisk/citrusDev/UI/QuestsPanel.java b/src/main/java/de/fullrisk/citrusDev/UI/QuestsPanel.java new file mode 100644 index 0000000..28daacb --- /dev/null +++ b/src/main/java/de/fullrisk/citrusDev/UI/QuestsPanel.java @@ -0,0 +1,156 @@ +package de.fullrisk.citrusDev.UI; + +import io.wispforest.owo.ui.component.UIComponents; +import io.wispforest.owo.ui.container.FlowLayout; +import io.wispforest.owo.ui.container.UIContainers; +import io.wispforest.owo.ui.core.*; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.FontDescription; +import de.fullrisk.citrusDev.client.PlaytimeTracker; + +import java.util.List; + +public class QuestsPanel { + + private static final int ACCENT = 0xbaff86; + + private record QuestData(String title, String description, int requiredPlaytime) { + boolean isUnlocked() { + return PlaytimeTracker.getCurrentPlaytimeSeconds() >= requiredPlaytime; + } + } + + public static void build(FlowLayout content, int contentWidth, int contentHeight) { + List quests = quests(); + int playtime = PlaytimeTracker.getCurrentPlaytimeSeconds(); + int maxPlaytime = 360; + int progress = Math.min(playtime * 100 / maxPlaytime, 100); + int barWidth = contentWidth - 8; + long unlockedCount = quests.stream().filter(QuestData::isUnlocked).count(); + + // --- status row: playtime + percentage --- + FlowLayout statusBar = UIContainers.horizontalFlow(Sizing.fixed(barWidth), Sizing.fixed(20)); + statusBar.gap(4); + statusBar.padding(Insets.of(4)); + statusBar.verticalAlignment(VerticalAlignment.CENTER); + statusBar.horizontalAlignment(HorizontalAlignment.LEFT); + statusBar.surface(MyScreen.GLASS_PANEL); + + statusBar.child(UIComponents.label(Component.literal("Playtime: " + formatPlaytime(playtime))) + .color(Color.ofRgb(0xFFFFFF))); + statusBar.child(UIComponents.spacer()); + statusBar.child(UIComponents.label(Component.literal(progress + "%")) + .color(Color.ofRgb(ACCENT))); + + content.child(statusBar); + + // --- progress bar --- + FlowLayout barBg = UIContainers.horizontalFlow(Sizing.fixed(barWidth), Sizing.fixed(6)); + barBg.surface(Surface.flat(0x30FFFFFF).and(Surface.outline(0x30FFFFFF))); + barBg.horizontalAlignment(HorizontalAlignment.LEFT); + barBg.gap(0); + barBg.padding(Insets.of(0)); + + int fillWidth = Math.max(barWidth * progress / 100, progress > 0 ? 2 : 0); + FlowLayout barFill = UIContainers.horizontalFlow(Sizing.fixed(fillWidth), Sizing.fixed(6)); + barFill.surface(Surface.flat(0xFF000000 | ACCENT)); + barBg.child(barFill); + + content.child(barBg); + + // --- unlocked count summary --- + FlowLayout summaryRow = UIContainers.horizontalFlow(Sizing.fixed(barWidth), Sizing.content()); + summaryRow.padding(Insets.of(3, 0, 2, 2)); + + summaryRow.child(UIComponents.label( + Component.literal("Quests " + unlockedCount + "/" + quests.size()) + .withStyle(style -> style.withFont(new FontDescription.Resource(MyScreen.MC_FIVE)))) + .color(Color.ofRgb(0xa3a3ac))); + + content.child(summaryRow); + + // --- quest list (scrollable) --- + FlowLayout questList = UIContainers.verticalFlow(Sizing.fixed(barWidth), Sizing.content()); + questList.gap(3); + questList.padding(Insets.of(2, 0, 2, 2)); + + for (QuestData quest : quests) { + questList.child(buildQuestEntry(quest, contentWidth)); + } + + int usedHeight = 20 + 6 + 18 + 6; // statusBar + barBg + summaryRow + gaps + int scrollHeight = contentHeight - usedHeight; + + var scrollArea = UIContainers.verticalScroll( + Sizing.fixed(barWidth), + Sizing.fixed(scrollHeight), + questList + ); + content.child(scrollArea); + } + + private static String formatPlaytime(int seconds) { + int hours = seconds / 3600; + int minutes = (seconds % 3600) / 60; + int secs = seconds % 60; + if (hours > 0) { + return hours + "h " + minutes + "m " + secs + "s"; + } else if (minutes > 0) { + return minutes + "m " + secs + "s"; + } else { + return secs + "s"; + } + } + + private static List quests() { + return List.of( + new QuestData("First Steps", "Play for 5 minutes", 300), + new QuestData("Getting Started", "Play for 30 minutes", 1800), + new QuestData("Dedicated Player", "Play for 1 hour", 3600), + new QuestData("Veteran", "Play for 3 hours", 10800) + ); + } + + private static FlowLayout buildQuestEntry(QuestData quest, int contentWidth) { + boolean unlocked = quest.isUnlocked(); + int entryWidth = contentWidth - 12; + + FlowLayout row = UIContainers.horizontalFlow(Sizing.fixed(entryWidth), Sizing.fixed(32)); + row.gap(6); + row.padding(Insets.of(4)); + row.verticalAlignment(VerticalAlignment.CENTER); + row.horizontalAlignment(HorizontalAlignment.LEFT); + row.surface(unlocked + ? Surface.flat(0x1AFFFFFF).and(Surface.outline(0x30 << 24 | ACCENT)) + : Surface.flat(0x12000000)); + + FlowLayout iconBox = UIContainers.verticalFlow(Sizing.fixed(12), Sizing.fixed(12)); + iconBox.surface(unlocked + ? Surface.flat(0xFF000000 | ACCENT) + : Surface.flat(0x40FFFFFF)); + iconBox.horizontalAlignment(HorizontalAlignment.CENTER); + iconBox.verticalAlignment(VerticalAlignment.CENTER); + iconBox.child(UIComponents.label(Component.literal(unlocked ? "✓" : "")) + .color(Color.ofRgb(0x1c1c1c))); + row.child(iconBox); + + FlowLayout textStack = UIContainers.verticalFlow(Sizing.fixed(entryWidth - 24), Sizing.content()); + textStack.gap(1); + textStack.horizontalAlignment(HorizontalAlignment.LEFT); + textStack.verticalAlignment(VerticalAlignment.TOP); + + var titleLabel = UIComponents.label(Component.literal(quest.title()) + .withStyle(style -> style.withFont(new FontDescription.Resource(MyScreen.MC_FIVE)))) + .color(Color.ofRgb(unlocked ? 0xFFFFFF : 0x797b86)); + textStack.child(titleLabel); + + var descLabel = UIComponents.label(Component.literal(quest.description())) + .color(Color.ofRgb(0xa3a3ac)) + .maxWidth(entryWidth - 24); + textStack.child(descLabel); + + row.child(textStack); + + return row; + } +} \ No newline at end of file diff --git a/src/main/java/de/fullrisk/citrusDev/UIHelper/CitrusToast.java b/src/main/java/de/fullrisk/citrusDev/UIHelper/CitrusToast.java new file mode 100644 index 0000000..5f69ce4 --- /dev/null +++ b/src/main/java/de/fullrisk/citrusDev/UIHelper/CitrusToast.java @@ -0,0 +1,84 @@ +package de.fullrisk.citrusDev.UIHelper; + +import io.wispforest.owo.ui.core.Color; +import io.wispforest.owo.ui.core.OwoUIGraphics; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.components.toasts.Toast; +import net.minecraft.client.gui.components.toasts.ToastManager; +import net.minecraft.network.chat.Component; +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.sounds.SoundEvents; +import org.jspecify.annotations.Nullable; + +public class CitrusToast implements Toast { + + private static final int WIDTH = 180; + private static final int HEIGHT = 32; + private static final long DEFAULT_DURATION_MS = 3000L; + + private final String title; + private final String message; + private final long durationMs; + + private Visibility wantedVisibility = Visibility.SHOW; + + private CitrusToast(String title, String message, long durationMs) { + this.title = title; + this.message = message; + this.durationMs = durationMs; + } + + public static void show(String title, String message) { + show(title, message, DEFAULT_DURATION_MS); + } + + public static void show(String title, String message, long durationMs) { + Minecraft.getInstance().getToastManager().addToast(new CitrusToast(title, message, durationMs)); + } + @Override + public Visibility getWantedVisibility() { + return this.wantedVisibility; + } + + @Override + public void update(ToastManager manager, long fullyVisibleForMs) { + if (fullyVisibleForMs >= this.durationMs) { + this.wantedVisibility = Visibility.HIDE; + } + } + + @Override + public @Nullable SoundEvent getSoundEvent() { + return SoundEvents.UI_TOAST_IN; + } + + @Override + public int width() { + return WIDTH; + } + + @Override + public int height() { + return HEIGHT; + } + + @Override + public void extractRenderState(GuiGraphicsExtractor graphics, Font font, long fullyVisibleForMs) { + OwoUIGraphics owo = OwoUIGraphics.of(graphics); + + int x = 0; + int y = 0; + int w = WIDTH; + int h = HEIGHT; + + // glass background, matching GLASS_PANEL style + owo.fill(RenderPipelines.GUI, x, y, x + w, y + h, 0x40FFFFFF); + owo.drawRectOutline(RenderPipelines.GUI, x, y, w, h, 0x64de4b); + + owo.drawText(Component.literal(this.title), x + 6, y + 6, 1f, 0xFFFFFFFF); + owo.drawText(Component.literal(this.message), x + 6, y + 18, 0.8f, 0xFFa3a3ac); + } +} \ No newline at end of file diff --git a/src/main/java/de/fullrisk/citrusDev/UIHelper/SettingsControls.java b/src/main/java/de/fullrisk/citrusDev/UIHelper/SettingsControls.java new file mode 100644 index 0000000..954c5fe --- /dev/null +++ b/src/main/java/de/fullrisk/citrusDev/UIHelper/SettingsControls.java @@ -0,0 +1,140 @@ +package de.fullrisk.citrusDev.UIHelper; + +import io.wispforest.owo.ui.component.UIComponents; +import io.wispforest.owo.ui.container.FlowLayout; +import io.wispforest.owo.ui.container.UIContainers; +import io.wispforest.owo.ui.core.*; +import net.minecraft.client.Minecraft; +import net.minecraft.client.resources.sounds.SimpleSoundInstance; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; +import net.minecraft.sounds.SoundEvents; + +import java.util.List; +import java.util.function.DoubleConsumer; +import java.util.function.DoubleFunction; + + + +public class SettingsControls { + + private static final Surface GLASS_PANEL = Surface.blur(5f, 15f) + .and(Surface.flat(0x40FFFFFF)) + .and(Surface.outline(0x64de4b)); + + public record ButtonState(String label, Runnable onSelect) {} + + public static FlowLayout makeSmallButton(Surface baseSurface, Identifier texture, Runnable onClick) { + FlowLayout button = UIContainers.verticalFlow(Sizing.fixed(16), Sizing.fixed(16)); + button.verticalAlignment(VerticalAlignment.CENTER); + button.horizontalAlignment(HorizontalAlignment.CENTER); + button.surface(baseSurface); + button.child(UIComponents.texture(texture, 0, 0, 12, 12, 12, 12)) + .horizontalAlignment(HorizontalAlignment.CENTER) + .verticalAlignment(VerticalAlignment.CENTER); + + button.mouseEnter().subscribe(() -> button.surface( + Surface.blur(5f, 15f).and(Surface.flat(0x70FFFFFF)).and(Surface.outline(0xFFFFFFFF)) + )); + button.mouseLeave().subscribe(() -> button.surface(baseSurface)); + + button.mouseDown().subscribe((click, doubled) -> { + Minecraft.getInstance().getSoundManager().play( + SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK.value(), 2.0F, 2) + ); + onClick.run(); + return true; + }); + + return button; + } + + public static FlowLayout makeTextButton(Surface baseSurface, String text, Runnable onClick) { + FlowLayout button = UIContainers.verticalFlow(Sizing.content(), Sizing.content()); + button.verticalAlignment(VerticalAlignment.CENTER); + button.horizontalAlignment(HorizontalAlignment.CENTER); + button.padding(Insets.of(2, 2, 4, 4)); + button.surface(baseSurface); + button.child(UIComponents.label(Component.literal(text)).color(Color.ofRgb(0xFFFFFF))); + + button.mouseEnter().subscribe(() -> button.surface( + Surface.blur(5f, 15f).and(Surface.flat(0x70FFFFFF)).and(Surface.outline(0xFFFFFFFF)) + )); + button.mouseLeave().subscribe(() -> button.surface(baseSurface)); + + button.mouseDown().subscribe((click, doubled) -> { + Minecraft.getInstance().getSoundManager().play( + SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK.value(), 2.0F, 2) + ); + onClick.run(); + return true; + }); + + return button; + } + + public static FlowLayout makeCyclingButton(Surface baseSurface, List states) { + int[] currentIndex = {0}; + + FlowLayout button = UIContainers.verticalFlow(Sizing.fixed(50), Sizing.fixed(16)); + button.verticalAlignment(VerticalAlignment.CENTER); + button.horizontalAlignment(HorizontalAlignment.CENTER); + button.surface(baseSurface); + + var label = UIComponents.label(Component.literal(states.get(0).label())) + .color(Color.ofRgb(0xFFFFFF)); + button.child(label); + + button.mouseDown().subscribe((click, doubled) -> { + Minecraft.getInstance().getSoundManager().play( + SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK.value(), 2.0F, 2) + ); + + currentIndex[0] = (currentIndex[0] + 1) % states.size(); + ButtonState newState = states.get(currentIndex[0]); + + label.text(Component.literal(newState.label())); + newState.onSelect().run(); + + return true; + }); + button.mouseEnter().subscribe(() -> label.color(Color.ofRgb(0xFFFFFF))); + button.mouseLeave().subscribe(() -> label.color(Color.ofRgb(0xCCCCCC))); + + return button; + } + + public static FlowLayout makeSlider(Surface baseSurface, int width, double min, double max, double initial, + DoubleFunction labelFormatter, DoubleConsumer onChange) { + int labelWidth = 34; + int sliderWidth = width - labelWidth - 6; + + FlowLayout wrapper = UIContainers.horizontalFlow(Sizing.fixed(width), Sizing.fixed(16)); + wrapper.verticalAlignment(VerticalAlignment.CENTER); + wrapper.horizontalAlignment(HorizontalAlignment.LEFT); + wrapper.padding(Insets.of(2, 2, 4, 0)); + wrapper.gap(2); + + var valueLabel = UIComponents.label(Component.literal(labelFormatter.apply(initial))); + valueLabel.color(Color.ofRgb(0xFFFFFF)); + valueLabel.horizontalSizing(Sizing.fixed(labelWidth)); + wrapper.child(valueLabel); + + var slider = UIComponents.discreteSlider(Sizing.fixed(sliderWidth), min, max); + slider.horizontalSizing(Sizing.fixed(sliderWidth)); + slider.verticalSizing(Sizing.fixed(10)); + slider.setFromDiscreteValue(initial); + slider.cursorStyle(CursorStyle.POINTER); + slider.mouseDown().subscribe((click, doubled) -> { + return doubled; + }); + slider.onChanged().subscribe(value -> { + valueLabel.text(Component.literal(labelFormatter.apply(value))); + onChange.accept(value); + }); + + wrapper.child(slider); + + return wrapper; + } +} \ No newline at end of file diff --git a/src/main/java/de/fullrisk/citrusDev/client/CitrusDevClient.java b/src/main/java/de/fullrisk/citrusDev/client/CitrusDevClient.java index 0bba02d..d1094cc 100644 --- a/src/main/java/de/fullrisk/citrusDev/client/CitrusDevClient.java +++ b/src/main/java/de/fullrisk/citrusDev/client/CitrusDevClient.java @@ -1,5 +1,6 @@ package de.fullrisk.citrusDev.client; + import de.fullrisk.citrusDev.Config; import de.fullrisk.citrusDev.UI.Module_FPS; import de.fullrisk.citrusDev.UI.MyScreen; @@ -36,6 +37,10 @@ public class CitrusDevClient implements ClientModInitializer { )); ClientTickEvents.END_CLIENT_TICK.register(client -> { + if (client.player != null && !client.isPaused()) { + PlaytimeTracker.tick(); + } + Module_FPS.tick(); // update every tick, unconditionally while (openScreenKey.consumeClick()) { client.setScreen(new MyScreen(null)); // always general tab diff --git a/src/main/java/de/fullrisk/citrusDev/client/PlaytimeTracker.java b/src/main/java/de/fullrisk/citrusDev/client/PlaytimeTracker.java new file mode 100644 index 0000000..f7e9a5b --- /dev/null +++ b/src/main/java/de/fullrisk/citrusDev/client/PlaytimeTracker.java @@ -0,0 +1,160 @@ +// PLACE THIS FILE AT: +// src/client/java/de/fullrisk/citrusDev/client/PlaytimeTracker.java +// +// NOT under src/main/java — MinecraftClient and client.player only exist +// on the client source set. That is the actual cause of the +// "Cannot resolve symbol" errors, not a mistake in this code. + +package de.fullrisk.citrusDev.client; + +import net.minecraft.client.Minecraft; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; +import java.util.UUID; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +public class PlaytimeTracker { + + //ticks zu backend schicken + + private static final String BACKEND_URL = + "http://127.0.0.1:5000/api/heartbeat"; + + private static final HttpClient HTTP = + HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(10)) + .build(); + + private static int ticks = 0; + private static int currentPlaytimeSeconds = 0; + + public static void tick() { + ticks++; + + if (ticks % 100 == 0) { + System.out.println("Ticking: " + ticks); + } + + if (ticks >= 60) { + ticks = 0; + sendHeartbeat(); + getPlaytime(); // <-- add this + + } + } + + private static void sendHeartbeat() { + Minecraft client = + Minecraft.getInstance(); + + if (client.player == null) { + System.out.println("No player yet"); + return; + } + + UUID uuid = client.player.getUUID(); + + String json = """ + { + "uuid": "%s" + } + """.formatted(uuid); + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(BACKEND_URL)) + .timeout(Duration.ofSeconds(10)) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(json)) + .build(); + + HTTP.sendAsync(request, HttpResponse.BodyHandlers.ofString()) + .thenAccept(response -> { + if (response.statusCode() != 200) { + System.out.println("Heartbeat failed: " + response.statusCode()); + } + }) + .exceptionally(error -> { + error.printStackTrace(); + return null; + }); + } + + + //playtime vom server holen + + private static final String PLAYTIME_URL = + "http://localhost:5000/api/player/%s/playtime"; + + + public static void getPlaytime() { + + Minecraft client = Minecraft.getInstance(); + + if (client.player == null) { + return; + } + + UUID uuid = client.player.getUUID(); + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create( + PLAYTIME_URL.formatted(uuid) + )) + .GET() + .build(); + + + HTTP.sendAsync( + request, + HttpResponse.BodyHandlers.ofString() + ) + .thenAccept(response -> { + + if (response.statusCode() == 200) { + + String json = response.body(); + + JsonObject data = + JsonParser.parseString(json) + .getAsJsonObject(); + + + currentPlaytimeSeconds = + data.get("playtime_seconds") + .getAsInt(); + + + // Compare playtime here + if (currentPlaytimeSeconds >= 3600) { + System.out.println( + "Player has more than 1 hour!" + ); + } + else { + System.out.println( + "Player needs more time" + ); + } + + + System.out.println( + "Seconds played: " + currentPlaytimeSeconds + ); + + } else { + System.out.println( + "Failed: " + response.statusCode() + ); + } + + }); + } + + public static int getCurrentPlaytimeSeconds() { + return currentPlaytimeSeconds; + } +} \ No newline at end of file diff --git a/src/main/java/de/fullrisk/citrusDev/mixin/SliderComponentMixin.java b/src/main/java/de/fullrisk/citrusDev/mixin/SliderComponentMixin.java new file mode 100644 index 0000000..b609dc1 --- /dev/null +++ b/src/main/java/de/fullrisk/citrusDev/mixin/SliderComponentMixin.java @@ -0,0 +1,40 @@ +package de.fullrisk.citrusDev.mixin; + +import io.wispforest.owo.ui.component.SliderComponent; +import io.wispforest.owo.ui.core.OwoUIGraphics; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.components.AbstractSliderButton; +import net.minecraft.client.renderer.RenderPipelines; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(AbstractSliderButton.class) +public abstract class SliderComponentMixin { + + @Inject(method = "extractWidgetRenderState", at = @At("HEAD"), cancellable = true) + private void citrusDev$renderGlassSlider(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float a, CallbackInfo ci) { + Object self = this; + if (!(self instanceof SliderComponent slider)) return; + + AbstractSliderButton widget = (AbstractSliderButton) self; + + int x = widget.getX(); + int y = widget.getY(); + int w = widget.getWidth(); + int h = widget.getHeight(); + + OwoUIGraphics owo = OwoUIGraphics.of(graphics); + + // glass track + owo.fill(RenderPipelines.GUI, x, y, x + w, y + h, 0x40FFFFFF); + + // handle + int handleWidth = 4; + int handleX = x + (int) (slider.value() * (w - handleWidth)); + owo.fill(RenderPipelines.GUI, handleX, y, handleX + handleWidth, y + h, 0xFFFFFFFF); + + ci.cancel(); + } +} \ No newline at end of file diff --git a/src/main/resources/citrus-dev.mixins.json b/src/main/resources/citrus-dev.mixins.json index f274764..188ba17 100644 --- a/src/main/resources/citrus-dev.mixins.json +++ b/src/main/resources/citrus-dev.mixins.json @@ -6,6 +6,7 @@ "mixins": [ ], "client": [ + "SliderComponentMixin" ], "injectors": { "defaultRequire": 1