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() {
}
}
}