package com.disklexar.mmorpg.ui; import com.disklexar.mmorpg.combat.AbilityService; import com.disklexar.mmorpg.player.model.PlayerProfile; import com.disklexar.mmorpg.progression.ProgressionService; import com.disklexar.mmorpg.rpg.clazz.AbilityDefinition; import com.disklexar.mmorpg.rpg.clazz.ClassCatalog; import com.disklexar.mmorpg.rpg.clazz.PlayerClass; import com.disklexar.mmorpg.rpg.job.JobCatalog; import com.disklexar.mmorpg.rpg.job.JobDefinition; import com.disklexar.mmorpg.rpg.power.PowerCatalog; import com.disklexar.mmorpg.rpg.power.PowerDefinition; import com.disklexar.mmorpg.rpg.race.RaceCatalog; import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.hypixel.hytale.codec.builder.BuilderCodec; import com.hypixel.hytale.component.Ref; import com.hypixel.hytale.component.Store; import com.hypixel.hytale.protocol.packets.interface_.CustomPageLifetime; import com.hypixel.hytale.protocol.packets.interface_.CustomUIEventBindingType; import com.hypixel.hytale.server.core.entity.entities.Player; import com.hypixel.hytale.server.core.entity.entities.player.pages.InteractiveCustomUIPage; import com.hypixel.hytale.server.core.ui.builder.EventData; import com.hypixel.hytale.server.core.ui.builder.UICommandBuilder; import com.hypixel.hytale.server.core.ui.builder.UIEventBuilder; import com.hypixel.hytale.server.core.universe.PlayerRef; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.time.Duration; import java.time.Instant; import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.util.StringJoiner; import java.util.UUID; /** * MMORPG player menu with tabs: Personnage, Inventaire, Compétences. *

* Inventaire uses {@code MmorpgInventoryPage.ui} as the root document so {@code ItemGrid.InventorySectionId} * binds correctly. Personnage / Compétences use {@code MmorpgPlayerShell.ui}. */ public final class PlayerMenuPage extends InteractiveCustomUIPage { private static final String SHELL = "Pages/MmorpgPlayerShell.ui"; private static final String CHARACTER_TAB = "Pages/MmorpgCharacterTab.ui"; private static final String SKILLS_TAB = "Pages/MmorpgSkillsTab.ui"; private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm").withZone(ZoneId.systemDefault()); public enum Tab { CHARACTER, INVENTORY, SKILLS } private final PlayerRef playerRef; private final PlayerProfile profile; private final ProgressionService progression; private final AbilityService abilities; private Tab activeTab; public PlayerMenuPage( @Nonnull PlayerRef playerRef, @Nonnull PlayerProfile profile, @Nullable ProgressionService progression, @Nullable AbilityService abilities, @Nonnull Tab initialTab) { super(playerRef, CustomPageLifetime.CanDismiss, UiEvent.CODEC); this.playerRef = playerRef; this.profile = profile; this.progression = progression; this.abilities = abilities; this.activeTab = initialTab; } @Override public void build( @Nonnull Ref ref, @Nonnull UICommandBuilder ui, @Nonnull UIEventBuilder events, @Nonnull Store store) { buildShellRoot(ref, ui, events, store); } @Override public void handleDataEvent( @Nonnull Ref ref, @Nonnull Store store, @Nonnull String raw) { Tab tab = parseTab(raw); if (tab != null && tab != activeTab) { if (tab == Tab.INVENTORY) { openInventoryPage(ref, store); return; } activeTab = tab; UICommandBuilder ui = new UICommandBuilder(); UIEventBuilder events = new UIEventBuilder(); bindTabButtons(events); loadShellTab(ui, events, store, ref, activeTab); sendUpdate(ui, events, false); } } private void openInventoryPage(@Nonnull Ref ref, @Nonnull Store store) { Player player = store.getComponent(ref, Player.getComponentType()); if (player != null) { player.getPageManager().openCustomPage(ref, store, new PlayerInventoryPage( playerRef, profile, progression, abilities)); } } private void buildShellRoot( @Nonnull Ref ref, @Nonnull UICommandBuilder ui, @Nonnull UIEventBuilder events, @Nonnull Store store) { ui.append(SHELL); applyHeader(ui); bindTabButtons(events); loadShellTab(ui, events, store, ref, activeTab); } private void loadShellTab( @Nonnull UICommandBuilder ui, @Nonnull UIEventBuilder events, @Nonnull Store store, @Nonnull Ref ref, @Nonnull Tab tab) { applyTabHighlight(ui, tab); ui.clear("#DynamicTabHost"); switch (tab) { case CHARACTER -> { ui.append("#DynamicTabHost", CHARACTER_TAB); applyCharacterTab(ui, store, ref); ui.set("#FooterHint.Text", escape("Échap pour fermer — onglet Personnage")); } case SKILLS -> { ui.append("#DynamicTabHost", SKILLS_TAB); applySkillsTab(ui); ui.set("#FooterHint.Text", escape( "Échap pour fermer — touches Ability 1/2/3 en jeu")); } case INVENTORY -> { } } ui.set("#HeaderSubtitle.Text", escape(tabSubtitle(tab))); } private void applyTabHighlight(@Nonnull UICommandBuilder ui, @Nonnull Tab tab) { ui.set("#TabCharacterIndicator.Visible", tab == Tab.CHARACTER); ui.set("#TabInventoryIndicator.Visible", tab == Tab.INVENTORY); ui.set("#TabSkillsIndicator.Visible", tab == Tab.SKILLS); ui.set("#TabCharacterWrap.Background", tab == Tab.CHARACTER ? "#3a3f4a" : "#00000000"); ui.set("#TabInventoryWrap.Background", tab == Tab.INVENTORY ? "#3a3f4a" : "#00000000"); ui.set("#TabSkillsWrap.Background", tab == Tab.SKILLS ? "#3a3f4a" : "#00000000"); } private void applyHeader(@Nonnull UICommandBuilder ui) { ui.set("#PlayerName.Text", escape(profile.getDisplayName())); ui.set("#LevelLabel.Text", escape("Nv. " + profile.getLevel())); ui.set("#MoneyLabel.Text", escape(profile.getMoney() + " pièces")); } private void applyCharacterTab( @Nonnull UICommandBuilder ui, @Nonnull Store store, @Nonnull Ref ref) { PlayerClass playerClass = ClassCatalog.byId(profile.getClassId()); String className = playerClass == null ? "Aucune" : playerClass.displayName(); ui.set("#IdentityName.Text", escape(profile.getDisplayName())); ui.set("#RaceLabel.Text", escape("Race : " + RaceCatalog.byId(profile.getRaceId()).displayName())); ui.set("#ClassLabel.Text", escape("Classe : " + className)); long xpMax = progression == null ? 100L : Math.max(1L, progression.xpForLevel(profile.getLevel())); long xpCurrent = profile.getExperience(); float xpRatio = Math.min(1f, (float) xpCurrent / xpMax); ui.set("#XpValue.Text", escape(xpCurrent + " / " + xpMax + " XP")); ui.set("#XpBar.Value", xpRatio); PlayerVitalsReader.Vitals vitals = PlayerVitalsReader.read(store, ref); if (vitals != null) { applyVitalBar(ui, "#HealthBar", "#HealthValue", "Vie", vitals.current(), vitals.max()); applyVitalBar(ui, "#StaminaBar", "#StaminaValue", "Endurance", vitals.staminaCurrent(), vitals.staminaMax()); } else { ui.set("#HealthValue.Text", escape("Vie : —")); ui.set("#StaminaValue.Text", escape("Endurance : —")); } int level = profile.getLevel(); ui.set("#StatStr.Text", escape("FOR " + (10 + level))); ui.set("#StatAgi.Text", escape("AGI " + (8 + level))); ui.set("#StatInt.Text", escape("INT " + (6 + level))); ui.set("#PowersList.Text", escape(formatPowers())); ui.set("#JobsList.Text", escape(formatJobs())); ui.set("#GroupLabel.Text", escape( "Groupe : " + (profile.getGroupId() != null ? "Oui" : "Aucun"))); ui.set("#GuildLabel.Text", escape( "Guilde : " + (profile.getGuildId() != null ? profile.getGuildId() : "Aucune"))); ui.set("#PlayTimeLabel.Text", escape("Temps de jeu : " + formatDuration(profile.getTotalTimePlay()))); ui.set("#LastLoginLabel.Text", escape( "Dernière connexion : " + formatDate(profile.getLastDateConnected()))); ui.set("#CreatedLabel.Text", escape("Création : " + formatDate(profile.getDateCreation()))); } private void applySkillsTab(@Nonnull UICommandBuilder ui) { PlayerClass playerClass = ClassCatalog.byId(profile.getClassId()); if (playerClass == null) { ui.set("#ClassTitle.Text", escape("Aucune classe")); ui.set("#WeaponsLabel.Text", escape("Choisissez une classe : /mmorpg class choisir")); ui.set("#PointsLabel.Text", escape("Points de compétence : —")); for (int slot = 1; slot <= 3; slot++) { ui.set("#Ability" + slot + "Key.Text", escape(AbilitySlotKeys.keyLabel(slot))); ui.set("#Ability" + slot + "Name.Text", escape("—")); ui.set("#Ability" + slot + "Cooldown.Text", escape("—")); ui.set("#Ability" + slot + "Desc.Text", escape("Aucune capacité sans classe.")); } return; } ui.set("#ClassTitle.Text", escape(playerClass.displayName())); ui.set("#WeaponsLabel.Text", escape("Armes : " + String.join(", ", playerClass.weapons()))); ui.set("#PointsLabel.Text", escape("Points de compétence : 0 (arbre M1)")); UUID playerId = profile.getUuid(); for (int slot = 1; slot <= 3; slot++) { ui.set("#Ability" + slot + "Key.Text", escape(AbilitySlotKeys.keyLabel(slot))); } for (AbilityDefinition ability : playerClass.abilities()) { int slot = ability.slot(); ui.set("#Ability" + slot + "Name.Text", escape(ability.displayName())); ui.set("#Ability" + slot + "Cooldown.Text", escape(cooldownLabel(playerId, ability))); ui.set("#Ability" + slot + "Desc.Text", escape(ability.description())); } } private void bindTabButtons(@Nonnull UIEventBuilder events) { events.addEventBinding( CustomUIEventBindingType.Activating, "#TabCharacter", EventData.of("Tab", "character"), false); events.addEventBinding( CustomUIEventBindingType.Activating, "#TabInventory", EventData.of("Tab", "inventory"), false); events.addEventBinding( CustomUIEventBindingType.Activating, "#TabSkills", EventData.of("Tab", "skills"), false); } private static void applyVitalBar( @Nonnull UICommandBuilder ui, @Nonnull String barSelector, @Nonnull String valueSelector, @Nonnull String label, long current, long max) { float ratio = max <= 0 ? 0f : Math.min(1f, (float) current / max); ui.set(barSelector + ".Value", ratio); ui.set(valueSelector + ".Text", escape(label + " : " + current + " / " + max)); } @Nonnull private String cooldownLabel(@Nonnull UUID playerId, @Nonnull AbilityDefinition ability) { if (abilities == null) { return "Prêt"; } long remaining = abilities.cooldownRemaining(playerId, ability.id()); if (remaining <= 0) { return "Prêt"; } return (remaining / 1000 + 1) + "s de recharge"; } @Nonnull private String formatPowers() { if (profile.getPowers().isEmpty()) { return "Aucun"; } StringJoiner joiner = new StringJoiner("\n• ", "• ", ""); for (String id : profile.getPowers()) { PowerDefinition power = PowerCatalog.byId(id); joiner.add(power != null ? power.displayName() : id); } return joiner.toString(); } @Nonnull private String formatJobs() { if (profile.getJobs().isEmpty()) { return "Aucun"; } StringJoiner joiner = new StringJoiner("\n• ", "• ", ""); for (String id : profile.getJobs()) { JobDefinition job = JobCatalog.byId(id); joiner.add(job != null ? job.displayName() : id); } return joiner.toString(); } @Nonnull private static String tabSubtitle(@Nonnull Tab tab) { return switch (tab) { case CHARACTER -> "Personnage"; case INVENTORY -> "Inventaire"; case SKILLS -> "Compétences"; }; } @Nullable private static Tab parseTab(@Nonnull String raw) { try { JsonObject json = JsonParser.parseString(raw).getAsJsonObject(); if (!json.has("Tab")) { return null; } String value = json.get("Tab").getAsString().toLowerCase(); return switch (value) { case "character", "personnage" -> Tab.CHARACTER; case "inventory", "inventaire" -> Tab.INVENTORY; case "skills", "competences", "compétences" -> Tab.SKILLS; default -> null; }; } catch (RuntimeException ignored) { return null; } } @Nonnull private static String formatDuration(long millis) { if (millis <= 0) { return "0 min"; } Duration duration = Duration.ofMillis(millis); long hours = duration.toHours(); long minutes = duration.toMinutesPart(); return hours > 0 ? hours + "h " + minutes + "min" : minutes + "min"; } @Nonnull private static String formatDate(long epochMillis) { if (epochMillis <= 0) { return "—"; } return DATE_FORMAT.format(Instant.ofEpochMilli(epochMillis)); } @Nonnull private static String escape(@Nonnull String text) { return text.replace("\\", "\\\\").replace("\"", "\\\""); } public static final class UiEvent { public static final BuilderCodec CODEC = BuilderCodec.builder(UiEvent.class, UiEvent::new).build(); private UiEvent() { } } }