@@ -0,0 +1,372 @@
|
||||
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.
|
||||
* <p>
|
||||
* 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<PlayerMenuPage.UiEvent> {
|
||||
|
||||
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<EntityStore> ref,
|
||||
@Nonnull UICommandBuilder ui,
|
||||
@Nonnull UIEventBuilder events,
|
||||
@Nonnull Store<EntityStore> store) {
|
||||
buildShellRoot(ref, ui, events, store);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleDataEvent(
|
||||
@Nonnull Ref<EntityStore> ref,
|
||||
@Nonnull Store<EntityStore> 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<EntityStore> ref, @Nonnull Store<EntityStore> 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<EntityStore> ref,
|
||||
@Nonnull UICommandBuilder ui,
|
||||
@Nonnull UIEventBuilder events,
|
||||
@Nonnull Store<EntityStore> 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<EntityStore> store,
|
||||
@Nonnull Ref<EntityStore> 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<EntityStore> store,
|
||||
@Nonnull Ref<EntityStore> 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<UiEvent> CODEC =
|
||||
BuilderCodec.builder(UiEvent.class, UiEvent::new).build();
|
||||
|
||||
private UiEvent() {
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user