From 02d2af0993fb6ce1eb4d6f38e79cfc71ece769dd Mon Sep 17 00:00:00 2001 From: gpatruno Date: Fri, 5 Jun 2026 20:33:23 +0200 Subject: [PATCH] inventory update --- README.md | 14 +- docs/README-UI.md | 75 ++- .../mmorpg/combat/ClassAbilityController.java | 135 +++++- .../mmorpg/command/InventoryCommand.java | 66 +++ .../disklexar/mmorpg/command/MenuCommand.java | 13 +- .../mmorpg/command/MmorpgCommand.java | 1 + .../mmorpg/command/MmorpgHelpCommand.java | 3 +- .../mmorpg/events/AbilityPacketFilter.java | 7 +- .../events/PlayerConnectionHandler.java | 2 +- .../MmorpgCastAbilityInteraction.java | 17 +- .../disklexar/mmorpg/ui/AbilityBarHud.java | 4 + .../disklexar/mmorpg/ui/AbilitySlotKeys.java | 23 + .../mmorpg/ui/PlayerInventoryPage.java | 267 +++++++++++ .../mmorpg/ui/PlayerInventoryReader.java | 167 +++++++ .../disklexar/mmorpg/ui/PlayerMenuPage.java | 372 +++++++++++++++ .../Common/UI/Custom/MmorpgAbilityBar.ui | 6 +- .../UI/Custom/Pages/MmorpgCharacterTab.ui | 206 ++++++++ .../UI/Custom/Pages/MmorpgInventoryPage.ui | 277 +++++++++++ .../UI/Custom/Pages/MmorpgInventoryTab.ui | 161 +++++++ .../UI/Custom/Pages/MmorpgPlayerShell.ui | 131 +++++ .../Common/UI/Custom/Pages/MmorpgSkillsTab.ui | 198 ++++++++ .../Item/Items/Weapon_Battleaxe_Mithril.json | 155 +++++- .../Items/Weapon_Crossbow_Ancient_Steel.json | 76 ++- .../Item/Items/Weapon_Daggers_Mithril.json | 451 +++++++++++++++++- .../Item/Items/Weapon_Longsword_Mithril.json | 157 +++++- .../Item/Items/Weapon_Shortbow_Cobalt.json | 302 +++++++++++- .../Item/Items/Weapon_Staff_Mithril.json | 84 +++- src/main/resources/manifest.json | 2 +- 28 files changed, 3281 insertions(+), 91 deletions(-) create mode 100644 src/main/java/com/disklexar/mmorpg/command/InventoryCommand.java create mode 100644 src/main/java/com/disklexar/mmorpg/ui/AbilitySlotKeys.java create mode 100644 src/main/java/com/disklexar/mmorpg/ui/PlayerInventoryPage.java create mode 100644 src/main/java/com/disklexar/mmorpg/ui/PlayerInventoryReader.java create mode 100644 src/main/java/com/disklexar/mmorpg/ui/PlayerMenuPage.java create mode 100644 src/main/resources/Common/UI/Custom/Pages/MmorpgCharacterTab.ui create mode 100644 src/main/resources/Common/UI/Custom/Pages/MmorpgInventoryPage.ui create mode 100644 src/main/resources/Common/UI/Custom/Pages/MmorpgInventoryTab.ui create mode 100644 src/main/resources/Common/UI/Custom/Pages/MmorpgPlayerShell.ui create mode 100644 src/main/resources/Common/UI/Custom/Pages/MmorpgSkillsTab.ui diff --git a/README.md b/README.md index bac6026..706434b 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,8 @@ Le JAR produit se trouve dans `build/libs/hytale-mmorpg-0.1.0.jar` (nom selon ve | `/mmorpg help` | Affiche l'aide | | `/mmorpg profile` | Résumé court (niveau, XP) | | `/mmorpg info` | Toutes vos informations (chat) | -| `/mmorpg menu` | Ouvre l'interface d'informations du joueur (page UI custom) | +| `/mmorpg menu` | Menu joueur MMORPG — onglets Personnage, Inventaire, Compétences | +| `/mmorpg inventory` | Même menu, ouvert directement sur l'onglet Inventaire | | `/mmorpg class [classe]` | Gérer votre classe | | `/mmorpg power [pouvoir]` | Gérer vos pouvoirs | | `/mmorpg job [metier]` | Gérer vos métiers | @@ -135,7 +136,7 @@ src/main/java/com/disklexar/mmorpg/ ├── rpg/ # clazz (classes) · power · job · race · group ├── combat/ # Capacités, état de combat, effets passifs, planificateur │ └── system/ # Systèmes ECS : dégâts, morts/kills, casse de bloc -├── ui/ # AbilityBarHud, AbilityBarService, PlayerInfoPage +├── ui/ # AbilityBarHud, PlayerMenuPage, PlayerInfoPage ├── quest/ # Stub (M3) └── social/ # Stub (M4) ``` @@ -148,6 +149,15 @@ src/main/java/com/disklexar/mmorpg/ - [Guide de développement](docs/DEVELOPMENT.md) - [Contribution](CONTRIBUTING.md) +### Custom UI (Hytale) + +Documentation officielle et guides communautaires utiles pour les assets `.ui` et les pages serveur : + +- [Custom UI — vue d'ensemble](https://hytalemodding.dev/en/docs/official-documentation/custom-ui) (architecture `UICommandBuilder`, HUD vs pages, sélecteurs `#Id.Propriété`) +- [Layout](https://hytalemodding.dev/en/docs/official-documentation/custom-ui/layout) (`Anchor`, `Padding`, `LayoutMode`, `FlexWeight`) +- [Common Styling](https://hytalemodding.dev/en/docs/official-documentation/custom-ui/common-styling) (`Common.ui`, styles partagés) +- [Guide plugin UI](https://hytalemodding.dev/en/docs/guides/plugin/ui) (emplacement des fichiers, `IncludesAssetPack`, diagnostic client) + ## Licence MIT — voir [LICENSE](LICENSE). diff --git a/docs/README-UI.md b/docs/README-UI.md index 96780ca..fdea3a9 100644 --- a/docs/README-UI.md +++ b/docs/README-UI.md @@ -78,7 +78,8 @@ src/main/resources/Common/UI/Custom/ ├── Pages/ │ ├── MmorpgPlayerShell.ui # Fenêtre à onglets (coquille) │ ├── MmorpgCharacterTab.ui # Onglet Personnage -│ ├── MmorpgInventoryTab.ui # Onglet Inventaire +│ ├── MmorpgInventoryPage.ui # 🟡 Début — inventaire MMORPG (coquille + grille) +│ ├── MmorpgInventoryTab.ui # Onglet Inventaire (futur, dans coquille à onglets) │ └── MmorpgSkillTreeTab.ui # Onglet Arbre de compétences └── Widgets/ ├── MmorpgProgressBar.ui # Barre réutilisable (vie, mana, XP) @@ -91,8 +92,11 @@ src/main/resources/Common/UI/Custom/ | Composant UI | Classe Java | Statut | |--------------|-------------|--------| | Barre capacités | `AbilityBarHud` + `AbilityBarService` | ✅ Actif | -| Profil joueur (liste) | `PlayerInfoPage` | ✅ Actif (layout base-game) | -| Coquille à onglets | `PlayerMenuPage` | 🔲 M1 | +| Profil joueur (fallback) | `PlayerInfoPage` | ✅ Actif (`/mmorpg menucustom`) | +| Menu à onglets | `PlayerMenuPage` | ✅ Actif (`/mmorpg menu`) | +| Onglet Personnage | `MmorpgCharacterTab.ui` | ✅ Actif | +| Onglet Inventaire | `MmorpgInventoryTab.ui` + `PlayerInventoryReader` | ✅ Actif | +| Onglet Compétences | `MmorpgSkillsTab.ui` | ✅ Actif (capacités classe + branches M1) | | Barres vitales HUD | `VitalsHud` + `VitalsHudService` | 🔲 M1 | | Buff tray | `BuffTrayHud` | 🔲 M1 | | Hotbar étendue | `HotbarHud` | 🔲 M2 | @@ -193,9 +197,12 @@ l'inventaire natif ou des interactions custom futures. | Slot | ID | Touche | Mise à jour | |------|-----|--------|-------------| -| 1 | `#Slot1Name`, `#Slot1Key` | Ability 1 | Nom + cooldown (`Charge (3s)`) | -| 2 | `#Slot2Name`, `#Slot2Key` | Ability 2 | idem | -| 3 | `#Slot3Name`, `#Slot3Key` | Ability 3 | idem | +| 1 | `#Slot1Name`, `#Slot1Key` | Q (Use Ability 1) | Nom + cooldown (`Charge (3s)`) | +| 2 | `#Slot2Name`, `#Slot2Key` | E (Use Ability 2) | idem | +| 3 | `#Slot3Name`, `#Slot3Key` | R (Use Ability 3) | idem | + +Les labels `#Slot*Key` / `#Ability*Key` sont remplis par `AbilitySlotKeys` avec les raccourcis +Hytale par défaut (`[Q]`, `[E]`, `[R]`). Le serveur ne connaît pas les rebinding client. **Commande debug :** `/mmorpg hud` — force la synchronisation. @@ -203,8 +210,8 @@ l'inventaire natif ou des interactions custom futures. ## 2. Menus joueur -Ouverture cible : **`C`** ou `/mmorpg menu` → coquille à onglets. -État actuel : `/mmorpg menu` ouvre `PlayerInfoPage` (liste verticale, layout `WarpListPage.ui`). +Ouverture : **`/mmorpg menu`** → coquille à onglets (`PlayerMenuPage`). +Fallback chat : `/mmorpg info` si la page UI échoue. ### 2.1 Coquille — `MmorpgPlayerShell.ui` @@ -247,18 +254,24 @@ pouvoirs / métiers / social. ### 2.3 Onglet Inventaire -**Fichier cible :** `Pages/MmorpgInventoryTab.ui` +**Fichier inventaire :** `Pages/MmorpgInventoryPage.ui` — document **racine** pour l'onglet Inventaire (`/mmorpg inventory`) +**Fichier coquille :** `Pages/MmorpgPlayerShell.ui` — Personnage / Compétences uniquement -> Un plugin serveur **ne peut pas modifier l'inventaire natif**. Cet onglet est une **vue MMORPG** -> complémentaire (items tagués, équipement RPG, filtres). +> Relié à l'inventaire **vanilla** via `ItemGrid.InventorySectionId` (sac, barre rapide, armure, utilitaire). +> **`InventorySectionId` ne fonctionne pas** dans une coquille imbriquée : l'inventaire doit être le document +> racine (`MmorpgInventoryPage.ui`). Changement d'onglet vers/depuis Inventaire → `rebuild()` complet. +> Ne pas appeler `#Grid.Slots` depuis le serveur — le client rejette cette commande et déconnecte. +> Glisser-déposer synchronisé ; panneau détails au clic. -| Zone | Description | -|------|-------------| -| `#EquipmentPaperDoll` | Silhouette : tête, torse, mains, jambes, arme principale / secondaire | -| `#InventoryGrid` | Grille 8×5 (40 slots) — items avec rareté (bordure couleur) | -| `#ItemDetails` | Panneau droit : nom, stats, description, boutons Utiliser / Jeter | -| `#CategoryTabs` | Filtres : Tout, Équipement, Consommables, Matériaux, Quête | -| `#WeightBar` | Encumbrance (futur) | +| Zone | ID | Section vanilla | +|------|-----|-----------------| +| Armure | `#ArmorGrid` | `ARMOR_SECTION_ID` (-3) | +| Utilitaire | `#UtilityGrid` | `UTILITY_SECTION_ID` (-5) | +| Sac | `#StorageGrid` | `STORAGE_SECTION_ID` (-2), 9×4 | +| Barre rapide | `#HotbarGrid` | `HOTBAR_SECTION_ID` (-1) | +| Détails | `#ItemDetails` | Clic / survol slot → `PlayerInventoryReader` | + +**À venir :** filtres par catégorie, stats MMORPG sur items tagués, boutons Utiliser / Jeter. **Ergonomie :** @@ -378,7 +391,7 @@ sequenceDiagram |-------|--------------|----------------------| | **M0** ✅ | `MmorpgAbilityBar.ui`, `PlayerInfoPage`, `/mmorpg hud` | Classes, capacités | | **M1** | `MmorpgVitalsHud`, `MmorpgBuffTray`, `MmorpgPlayerShell` + onglet Personnage | Stats vie/mana, buffs | -| **M2** | Onglet Inventaire MMORPG, `MmorpgHotbarHud` | Items tagués, équipement | +| **M2** | Onglet Inventaire MMORPG, `MmorpgHotbarHud` | Items tagués, équipement — **début** : `MmorpgInventoryPage.ui` | | **M3** | Onglet Arbre de compétences | Points de compétence, déblocages | | **M4** | Quêtes dans HUD, tooltips riches | Module quêtes | @@ -399,10 +412,26 @@ sequenceDiagram --- -## 8. Références projet +## 8. Références -- Code HUD actif : `src/main/java/com/disklexar/mmorpg/ui/` -- Asset barre capacités : `src/main/resources/Common/UI/Custom/MmorpgAbilityBar.ui` -- Scaffold pages : `assetpack/ui/Pages/MmorpgPlayerInfo.ui` +### Documentation Custom UI (officielle / guides) + +- [Custom UI — vue d'ensemble](https://hytalemodding.dev/en/docs/official-documentation/custom-ui) — HUD vs pages, `append` / `set`, sélecteurs +- [Layout](https://hytalemodding.dev/en/docs/official-documentation/custom-ui/layout) — `Anchor`, `LayoutMode`, `FlexWeight` (utilisé pour le panneau inventaire 3 colonnes) +- [Common Styling](https://hytalemodding.dev/en/docs/official-documentation/custom-ui/common-styling) — `Common.ui`, styles vanilla réutilisables +- [Guide plugin UI](https://hytalemodding.dev/en/docs/guides/plugin/ui) — chemin `Common/UI/Custom`, `IncludesAssetPack`, mode diagnostic client + +**Rappels appris en production :** + +- `CustomUIHud` : uniquement `append(document)` + `set()` — pas de `appendInline` / `clear` (crash client). +- Propriétés dynamiques : préférer `ProgressBar.Value` plutôt que `Group.Width` (rejeté par le client). +- Ancrage HUD : le groupe racine doit avoir `Anchor: (Left: …, Width: …)` sinon centrage écran. + +### Fichiers projet + +- Code UI : `src/main/java/com/disklexar/mmorpg/ui/` +- HUD vitals + capacités : `src/main/resources/Common/UI/Custom/MmorpgAbilityBar.ui` +- Inventaire (début) : `src/main/resources/Common/UI/Custom/Pages/MmorpgInventoryPage.ui` +- Profil (scaffold) : `src/main/resources/Common/UI/Custom/Pages/MmorpgPlayerInfo.ui` - Guide asset pack : [assetpack/README.md](../assetpack/README.md) - Architecture : [ARCHITECTURE.md](ARCHITECTURE.md) diff --git a/src/main/java/com/disklexar/mmorpg/combat/ClassAbilityController.java b/src/main/java/com/disklexar/mmorpg/combat/ClassAbilityController.java index b5ab98e..fdaec9f 100644 --- a/src/main/java/com/disklexar/mmorpg/combat/ClassAbilityController.java +++ b/src/main/java/com/disklexar/mmorpg/combat/ClassAbilityController.java @@ -8,6 +8,7 @@ import com.disklexar.mmorpg.rpg.clazz.AbilityDefinition; import com.disklexar.mmorpg.rpg.clazz.ClassService; import com.disklexar.mmorpg.rpg.clazz.PlayerClass; import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.logger.HytaleLogger; import com.hypixel.hytale.protocol.InteractionType; import com.hypixel.hytale.server.core.Message; import com.hypixel.hytale.server.core.universe.PlayerRef; @@ -16,6 +17,7 @@ import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; import javax.annotation.Nonnull; import javax.annotation.Nullable; +import java.util.logging.Level; /** * Central entry point for routing Ability 1/2/3 inputs to MMORPG class abilities. @@ -24,9 +26,11 @@ import javax.annotation.Nullable; public final class ClassAbilityController { private final MmorpgPlugin plugin; + private final HytaleLogger logger; public ClassAbilityController(@Nonnull MmorpgPlugin plugin) { this.plugin = plugin; + this.logger = plugin.getLogger(); } /** @@ -34,11 +38,12 @@ public final class ClassAbilityController { * should be suppressed for this slot (even if the cast fails, e.g. wrong weapon). */ public boolean shouldSuppressVanilla(@Nonnull PlayerRef playerRef, int slot) { - return resolveAbility(playerRef, slot) != null; + return resolve(playerRef, slot).resolved(); } /** - * Attempts to cast the class ability bound to {@code slot}. + * Attempts to cast the class ability bound to {@code slot} (triggered by in-game input: + * Ability 1/2/3). Sends chat feedback only for failures — cooldown is shown on the HUD bar. * * @return {@code true} if vanilla should be blocked (player has a class with this slot). */ @@ -47,25 +52,31 @@ public final class ClassAbilityController { @Nonnull World world, int slot, @Nullable Ref aimedTarget) { - ResolvedAbility resolved = resolveAbility(playerRef, slot); - if (resolved == null) { + return tryCast(playerRef, world, slot, aimedTarget, true); + } + + /** + * @param fromInput {@code true} when triggered by Ability 1/2/3 keys (errors only in chat); + * {@code false} for {@code /mmorpg ability} (also confirms a successful cast). + */ + public boolean tryCast( + @Nonnull PlayerRef playerRef, + @Nonnull World world, + int slot, + @Nullable Ref aimedTarget, + boolean fromInput) { + ResolveOutcome outcome = resolve(playerRef, slot); + if (!outcome.resolved()) { + if (outcome.failure() != ResolveFailure.SERVER_NOT_READY) { + notifyResolveFailure(playerRef, slot, outcome.failure()); + } return false; } + ResolvedAbility resolved = outcome.ability(); AbilityService.CastResult result = resolved.abilities().cast( playerRef, world, resolved.playerClass(), resolved.ability(), aimedTarget); - - switch (result) { - case OK -> playerRef.sendMessage(Message.raw("Capacité : " + resolved.ability().displayName())); - case ON_COOLDOWN -> { - long remaining = resolved.abilities().cooldownRemaining( - playerRef.getUuid(), resolved.ability().id()); - playerRef.sendMessage(Message.raw("En recharge (" + (remaining / 1000 + 1) + "s).")); - } - case WRONG_WEAPON -> playerRef.sendMessage(Message.raw( - "Mauvaise arme. Équipez : " + String.join(", ", resolved.playerClass().weapons()))); - case UNKNOWN -> playerRef.sendMessage(Message.raw("Capacité inconnue.")); - } + notifyCastResult(playerRef, resolved, result, fromInput); return true; } @@ -82,11 +93,11 @@ public final class ClassAbilityController { return slotFor(type) != 0; } - @Nullable - private ResolvedAbility resolveAbility(@Nonnull PlayerRef playerRef, int slot) { + @Nonnull + private ResolveOutcome resolve(@Nonnull PlayerRef playerRef, int slot) { Bootstrap bootstrap = plugin.getBootstrap(); if (bootstrap == null) { - return null; + return ResolveOutcome.failed(ResolveFailure.SERVER_NOT_READY); } PlayerSessionService sessions = bootstrap.getRegistry().require(PlayerSessionService.class); @@ -95,20 +106,98 @@ public final class ClassAbilityController { PlayerProfile profile = sessions.getSession(playerRef.getUuid()); if (profile == null) { - return null; + return ResolveOutcome.failed(ResolveFailure.NO_PROFILE); } PlayerClass playerClass = classService.getClass(profile); if (playerClass == null) { - return null; + return ResolveOutcome.failed(ResolveFailure.NO_CLASS); } AbilityDefinition ability = playerClass.abilities().stream() .filter(a -> a.slot() == slot) .findFirst() .orElse(null); if (ability == null) { - return null; + return ResolveOutcome.failed(ResolveFailure.NO_ABILITY_SLOT); + } + return ResolveOutcome.ok(new ResolvedAbility(playerClass, ability, abilities)); + } + + private void notifyResolveFailure( + @Nonnull PlayerRef playerRef, + int slot, + @Nonnull ResolveFailure failure) { + String message = switch (failure) { + case NO_PROFILE -> "[Capacité " + slot + "] Profil indisponible. Reconnectez-vous."; + case NO_CLASS -> "[Capacité " + slot + "] Aucune classe active. " + + "Utilisez /mmorpg class choisir ."; + case NO_ABILITY_SLOT -> "[Capacité " + slot + "] Aucune capacité dans ce slot."; + case SERVER_NOT_READY -> null; + }; + if (message != null) { + playerRef.sendMessage(Message.raw(message)); + logAbilityFeedback(playerRef, "resolve:" + failure, message); + } + } + + private void notifyCastResult( + @Nonnull PlayerRef playerRef, + @Nonnull ResolvedAbility resolved, + @Nonnull AbilityService.CastResult result, + boolean fromInput) { + String message = formatCastFeedback(resolved, result, fromInput); + if (message == null) { + return; + } + playerRef.sendMessage(Message.raw(message)); + logAbilityFeedback(playerRef, result.name(), message); + } + + @Nullable + private String formatCastFeedback( + @Nonnull ResolvedAbility resolved, + @Nonnull AbilityService.CastResult result, + boolean fromInput) { + String abilityName = resolved.ability().displayName(); + return switch (result) { + case OK -> fromInput ? null : "Capacité utilisée : " + abilityName + "."; + // Cooldown is already shown on the ability-bar HUD. + case ON_COOLDOWN -> null; + case WRONG_WEAPON -> "[" + abilityName + "] Arme requise : " + + String.join(", ", resolved.playerClass().weapons()) + "."; + case UNKNOWN -> "[" + abilityName + "] Capacité indisponible pour le moment."; + }; + } + + private void logAbilityFeedback( + @Nonnull PlayerRef playerRef, + @Nonnull String outcome, + @Nonnull String message) { + logger.at(Level.INFO).log( + "Ability input [%s] %s — %s", + outcome, + playerRef.getUsername(), + message); + } + + private enum ResolveFailure { + SERVER_NOT_READY, + NO_PROFILE, + NO_CLASS, + NO_ABILITY_SLOT + } + + private record ResolveOutcome(@Nullable ResolvedAbility ability, @Nullable ResolveFailure failure) { + static ResolveOutcome ok(@Nonnull ResolvedAbility ability) { + return new ResolveOutcome(ability, null); + } + + static ResolveOutcome failed(@Nonnull ResolveFailure failure) { + return new ResolveOutcome(null, failure); + } + + boolean resolved() { + return ability != null; } - return new ResolvedAbility(playerClass, ability, abilities); } private record ResolvedAbility( diff --git a/src/main/java/com/disklexar/mmorpg/command/InventoryCommand.java b/src/main/java/com/disklexar/mmorpg/command/InventoryCommand.java new file mode 100644 index 0000000..b5adb80 --- /dev/null +++ b/src/main/java/com/disklexar/mmorpg/command/InventoryCommand.java @@ -0,0 +1,66 @@ +package com.disklexar.mmorpg.command; + +import com.disklexar.mmorpg.MmorpgPlugin; +import com.disklexar.mmorpg.player.PlayerInfoView; +import com.disklexar.mmorpg.player.model.PlayerProfile; +import com.disklexar.mmorpg.combat.AbilityService; +import com.disklexar.mmorpg.progression.ProgressionService; +import com.disklexar.mmorpg.ui.PlayerInventoryPage; +import com.disklexar.mmorpg.ui.PlayerMenuPage; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.Message; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand; +import com.hypixel.hytale.server.core.entity.entities.Player; +import com.hypixel.hytale.server.core.entity.entities.player.pages.PageManager; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; + +import javax.annotation.Nonnull; + +/** + * {@code /mmorpg inventory} — opens the MMORPG inventory shell (custom UI). + */ +public final class InventoryCommand extends AbstractPlayerCommand { + + private final MmorpgPlugin plugin; + + public InventoryCommand(@Nonnull MmorpgPlugin plugin) { + super("inventory", "Ouvrir le menu joueur sur l'onglet Inventaire"); + this.plugin = plugin; + } + + @Override + protected void execute( + @Nonnull CommandContext context, + @Nonnull Store store, + @Nonnull Ref ref, + @Nonnull PlayerRef playerRef, + @Nonnull World world) { + PlayerProfile profile = CommandSupport.profile(plugin, store, ref); + if (profile == null) { + context.sendMessage(Message.raw("Aucun profil chargé. Reconnectez-vous.")); + return; + } + Player player = store.getComponent(ref, Player.getComponentType()); + if (player == null) { + context.sendMessage(Message.raw(PlayerInfoView.asText(profile))); + return; + } + PageManager pageManager = player.getPageManager(); + world.execute(() -> { + try { + pageManager.openCustomPage(ref, store, new PlayerInventoryPage( + playerRef, + profile, + CommandSupport.service(plugin, ProgressionService.class), + CommandSupport.service(plugin, AbilityService.class))); + } catch (Throwable t) { + playerRef.sendMessage(Message.raw( + "Impossible d'ouvrir l'inventaire MMORPG : " + t.getMessage())); + } + }); + } +} diff --git a/src/main/java/com/disklexar/mmorpg/command/MenuCommand.java b/src/main/java/com/disklexar/mmorpg/command/MenuCommand.java index 4f0a130..9ed3f6b 100644 --- a/src/main/java/com/disklexar/mmorpg/command/MenuCommand.java +++ b/src/main/java/com/disklexar/mmorpg/command/MenuCommand.java @@ -3,7 +3,9 @@ package com.disklexar.mmorpg.command; import com.disklexar.mmorpg.MmorpgPlugin; import com.disklexar.mmorpg.player.PlayerInfoView; import com.disklexar.mmorpg.player.model.PlayerProfile; -import com.disklexar.mmorpg.ui.PlayerInfoPage; +import com.disklexar.mmorpg.combat.AbilityService; +import com.disklexar.mmorpg.progression.ProgressionService; +import com.disklexar.mmorpg.ui.PlayerMenuPage; import com.hypixel.hytale.component.Ref; import com.hypixel.hytale.component.Store; import com.hypixel.hytale.server.core.Message; @@ -26,7 +28,7 @@ public final class MenuCommand extends AbstractPlayerCommand { private final MmorpgPlugin plugin; public MenuCommand(@Nonnull MmorpgPlugin plugin) { - super("menu", "Ouvrir l'interface d'informations du joueur"); + super("menu", "Ouvrir le menu joueur MMORPG (Personnage / Inventaire / Compétences)"); this.plugin = plugin; } @@ -46,7 +48,12 @@ public final class MenuCommand extends AbstractPlayerCommand { PageManager pageManager = player.getPageManager(); world.execute(() -> { try { - pageManager.openCustomPage(ref, store, new PlayerInfoPage(playerRef, profile, false)); + pageManager.openCustomPage(ref, store, new PlayerMenuPage( + playerRef, + profile, + CommandSupport.service(plugin, ProgressionService.class), + CommandSupport.service(plugin, AbilityService.class), + PlayerMenuPage.Tab.CHARACTER)); } catch (Throwable t) { playerRef.sendMessage(Message.raw(PlayerInfoView.asText(profile))); } diff --git a/src/main/java/com/disklexar/mmorpg/command/MmorpgCommand.java b/src/main/java/com/disklexar/mmorpg/command/MmorpgCommand.java index 0df31e2..7565519 100644 --- a/src/main/java/com/disklexar/mmorpg/command/MmorpgCommand.java +++ b/src/main/java/com/disklexar/mmorpg/command/MmorpgCommand.java @@ -16,6 +16,7 @@ public final class MmorpgCommand extends AbstractCommandCollection { addSubCommand(new MmorpgProfileCommand(plugin)); addSubCommand(new InfoCommand(plugin)); addSubCommand(new MenuCommand(plugin)); + addSubCommand(new InventoryCommand(plugin)); addSubCommand(new HudCommand(plugin)); addSubCommand(new ClassCommand(plugin)); addSubCommand(new PowerCommand(plugin)); diff --git a/src/main/java/com/disklexar/mmorpg/command/MmorpgHelpCommand.java b/src/main/java/com/disklexar/mmorpg/command/MmorpgHelpCommand.java index d06fd4b..e0df676 100644 --- a/src/main/java/com/disklexar/mmorpg/command/MmorpgHelpCommand.java +++ b/src/main/java/com/disklexar/mmorpg/command/MmorpgHelpCommand.java @@ -25,7 +25,8 @@ public final class MmorpgHelpCommand extends AbstractAsyncCommand { /mmorpg help — cette aide /mmorpg profile — résumé court de votre profil /mmorpg info — toutes vos informations (chat) - /mmorpg menu — interface des informations du joueur + /mmorpg menu — menu joueur (Personnage / Inventaire / Compétences) + /mmorpg inventory — menu joueur, onglet Inventaire /mmorpg menucustom — interface personnalisée (asset pack, test) /mmorpg class [classe] /mmorpg power [pouvoir] diff --git a/src/main/java/com/disklexar/mmorpg/events/AbilityPacketFilter.java b/src/main/java/com/disklexar/mmorpg/events/AbilityPacketFilter.java index c8f4857..4912992 100644 --- a/src/main/java/com/disklexar/mmorpg/events/AbilityPacketFilter.java +++ b/src/main/java/com/disklexar/mmorpg/events/AbilityPacketFilter.java @@ -9,6 +9,7 @@ import com.hypixel.hytale.protocol.Packet; import com.hypixel.hytale.protocol.packets.interaction.CancelInteractionChain; import com.hypixel.hytale.protocol.packets.interaction.SyncInteractionChain; import com.hypixel.hytale.protocol.packets.interaction.SyncInteractionChains; +import com.hypixel.hytale.server.core.Message; import com.hypixel.hytale.server.core.entity.entities.Player; import com.hypixel.hytale.server.core.io.adapter.PlayerPacketFilter; import com.hypixel.hytale.server.core.universe.PlayerRef; @@ -73,16 +74,20 @@ public final class AbilityPacketFilter implements PlayerPacketFilter { Ref entityRef = playerRef.getReference(); if (entityRef == null || !entityRef.isValid()) { + playerRef.sendMessage(Message.raw( + "[Capacité " + slot + "] Impossible d'utiliser la capacité pour le moment.")); return; } Store store = entityRef.getStore(); Player player = store.getComponent(entityRef, Player.getComponentType()); if (player == null) { + playerRef.sendMessage(Message.raw( + "[Capacité " + slot + "] Impossible d'utiliser la capacité pour le moment.")); return; } World world = player.getWorld(); - world.execute(() -> controller.tryCast(playerRef, world, slot, null)); + world.execute(() -> controller.tryCast(playerRef, world, slot, null, true)); } } diff --git a/src/main/java/com/disklexar/mmorpg/events/PlayerConnectionHandler.java b/src/main/java/com/disklexar/mmorpg/events/PlayerConnectionHandler.java index bb82741..bede16f 100644 --- a/src/main/java/com/disklexar/mmorpg/events/PlayerConnectionHandler.java +++ b/src/main/java/com/disklexar/mmorpg/events/PlayerConnectionHandler.java @@ -71,7 +71,7 @@ public final class PlayerConnectionHandler { profile.getLevel(), com.disklexar.mmorpg.rpg.race.RaceCatalog.byId(profile.getRaceId()).displayName()))); universePlayerRef.sendMessage(Message.raw( - "Commandes : /mmorpg menu (profil) | /mmorpg class info")); + "Commandes : /mmorpg menu (profil) | /mmorpg inventory | /mmorpg class info")); if (classService.hasClass(profile)) { scheduler.runLater(() -> abilityBar.sync(uuid, profile), 5_000L); diff --git a/src/main/java/com/disklexar/mmorpg/interaction/MmorpgCastAbilityInteraction.java b/src/main/java/com/disklexar/mmorpg/interaction/MmorpgCastAbilityInteraction.java index 2adb930..1b38e05 100644 --- a/src/main/java/com/disklexar/mmorpg/interaction/MmorpgCastAbilityInteraction.java +++ b/src/main/java/com/disklexar/mmorpg/interaction/MmorpgCastAbilityInteraction.java @@ -9,6 +9,7 @@ import com.hypixel.hytale.component.Ref; import com.hypixel.hytale.component.Store; import com.hypixel.hytale.protocol.InteractionState; import com.hypixel.hytale.protocol.InteractionType; +import com.hypixel.hytale.server.core.Message; import com.hypixel.hytale.server.core.entity.InteractionContext; import com.hypixel.hytale.server.core.entity.entities.Player; import com.hypixel.hytale.server.core.modules.interaction.interaction.CooldownHandler; @@ -55,10 +56,6 @@ public final class MmorpgCastAbilityInteraction extends SimpleInstantInteraction if (abilitySlot == 0) { abilitySlot = ClassAbilityController.slotFor(interactionType); } - if (abilitySlot == 0) { - interactionContext.getState().state = InteractionState.Failed; - return; - } Ref entityRef = interactionContext.getEntity(); if (entityRef == null || !entityRef.isValid()) { @@ -74,8 +71,15 @@ public final class MmorpgCastAbilityInteraction extends SimpleInstantInteraction return; } + if (abilitySlot == 0) { + playerRef.sendMessage(Message.raw("Capacité invalide (slot inconnu).")); + interactionContext.getState().state = InteractionState.Failed; + return; + } + MmorpgPlugin plugin = MmorpgPlugin.get(); if (plugin == null) { + playerRef.sendMessage(Message.raw("Plugin MMORPG indisponible.")); interactionContext.getState().state = InteractionState.Failed; return; } @@ -83,8 +87,7 @@ public final class MmorpgCastAbilityInteraction extends SimpleInstantInteraction World world = player.getWorld(); Ref target = interactionContext.getTargetEntity(); ClassAbilityController controller = plugin.getClassAbilityController(); - if (!controller.tryCast(playerRef, world, abilitySlot, target)) { - interactionContext.getState().state = InteractionState.Failed; - } + controller.tryCast(playerRef, world, abilitySlot, target); + interactionContext.getState().state = InteractionState.Failed; } } diff --git a/src/main/java/com/disklexar/mmorpg/ui/AbilityBarHud.java b/src/main/java/com/disklexar/mmorpg/ui/AbilityBarHud.java index 6c26eb6..7955b10 100644 --- a/src/main/java/com/disklexar/mmorpg/ui/AbilityBarHud.java +++ b/src/main/java/com/disklexar/mmorpg/ui/AbilityBarHud.java @@ -55,6 +55,10 @@ public final class AbilityBarHud extends CustomUIHud { applyBar(ui, "#XpBar", xpCurrent, xpMax); ui.set("#XpValue.Text", escape(xpCurrent + " / " + xpMax)); + for (int slot = 1; slot <= 3; slot++) { + ui.set("#Slot" + slot + "Key.Text", escape(AbilitySlotKeys.keyLabel(slot))); + } + for (AbilityDefinition ability : playerClass.abilities()) { int slot = ability.slot(); long remaining = abilities.cooldownRemaining(playerId, ability.id()); diff --git a/src/main/java/com/disklexar/mmorpg/ui/AbilitySlotKeys.java b/src/main/java/com/disklexar/mmorpg/ui/AbilitySlotKeys.java new file mode 100644 index 0000000..fe4ef6c --- /dev/null +++ b/src/main/java/com/disklexar/mmorpg/ui/AbilitySlotKeys.java @@ -0,0 +1,23 @@ +package com.disklexar.mmorpg.ui; + +import javax.annotation.Nonnull; + +/** + * Default Hytale key labels for class ability slots (Use Ability 1/2/3). + * Server-side only — players may rebind these in client settings. + */ +public final class AbilitySlotKeys { + + private static final String[] DEFAULT_KEYS = {"", "Q", "E", "R"}; + + private AbilitySlotKeys() { + } + + @Nonnull + public static String keyLabel(int slot) { + if (slot < 1 || slot > 3) { + return "—"; + } + return "[" + DEFAULT_KEYS[slot] + "]"; + } +} diff --git a/src/main/java/com/disklexar/mmorpg/ui/PlayerInventoryPage.java b/src/main/java/com/disklexar/mmorpg/ui/PlayerInventoryPage.java new file mode 100644 index 0000000..d4fa040 --- /dev/null +++ b/src/main/java/com/disklexar/mmorpg/ui/PlayerInventoryPage.java @@ -0,0 +1,267 @@ +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.ClassCatalog; +import com.disklexar.mmorpg.rpg.clazz.PlayerClass; +import com.google.gson.JsonElement; +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.inventory.InventoryComponent; +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; + +/** + * Standalone inventory page ({@code MmorpgInventoryPage.ui} root document). + * Does not reference {@code MmorpgPlayerShell.ui}. + */ +public final class PlayerInventoryPage extends InteractiveCustomUIPage { + + private static final String DOCUMENT = "Pages/MmorpgInventoryPage.ui"; + + private final PlayerRef playerRef; + private final PlayerProfile profile; + private final ProgressionService progression; + private final AbilityService abilities; + + public PlayerInventoryPage( + @Nonnull PlayerRef playerRef, + @Nonnull PlayerProfile profile, + @Nullable ProgressionService progression, + @Nullable AbilityService abilities) { + super(playerRef, CustomPageLifetime.CanDismiss, UiEvent.CODEC); + this.playerRef = playerRef; + this.profile = profile; + this.progression = progression; + this.abilities = abilities; + } + + @Override + public void build( + @Nonnull Ref ref, + @Nonnull UICommandBuilder ui, + @Nonnull UIEventBuilder events, + @Nonnull Store store) { + ui.append(DOCUMENT); + applyHeader(ui); + applyInventorySummary(ui, PlayerInventoryReader.snapshot(store, ref)); + applyEmptyItemDetails(ui); + applyTabHighlight(ui); + bindTabButtons(events); + bindInventoryGridEvents(events); + } + + @Override + public void handleDataEvent( + @Nonnull Ref ref, + @Nonnull Store store, + @Nonnull String raw) { + PlayerMenuPage.Tab tab = parseTab(raw); + if (tab != null && tab != PlayerMenuPage.Tab.INVENTORY) { + Player player = store.getComponent(ref, Player.getComponentType()); + if (player != null) { + player.getPageManager().openCustomPage(ref, store, new PlayerMenuPage( + playerRef, + profile, + progression, + abilities, + tab)); + } + return; + } + + SlotSelection selection = parseSlotSelection(raw); + if (selection == null) { + return; + } + + UICommandBuilder ui = new UICommandBuilder(); + PlayerInventoryReader.ItemView item = PlayerInventoryReader.itemAt( + store, ref, selection.sectionId(), selection.slotIndex()); + if (item != null) { + applyItemDetails(ui, item); + } else { + applyEmptyItemDetails(ui); + } + applyInventorySummary(ui, PlayerInventoryReader.snapshot(store, ref)); + sendUpdate(ui, false); + } + + private void applyHeader(@Nonnull UICommandBuilder ui) { + ui.set("#PlayerName.Text", escape(profile.getDisplayName())); + ui.set("#ClassLabel.Text", escape(classLabel())); + ui.set("#LevelLabel.Text", escape("Nv. " + profile.getLevel())); + ui.set("#MoneyLabel.Text", escape(profile.getMoney() + " pièces")); + } + + private void applyTabHighlight(@Nonnull UICommandBuilder ui) { + ui.set("#TabCharacterIndicator.Visible", false); + ui.set("#TabInventoryIndicator.Visible", true); + ui.set("#TabSkillsIndicator.Visible", false); + ui.set("#TabCharacterWrap.Background", "#00000000"); + ui.set("#TabInventoryWrap.Background", "#3a3f4a"); + ui.set("#TabSkillsWrap.Background", "#00000000"); + } + + private void applyInventorySummary( + @Nonnull UICommandBuilder ui, + @Nonnull PlayerInventoryReader.Snapshot snapshot) { + ui.set("#StorageSummary.Text", escape( + "Sac : " + snapshot.storageUsed() + " / " + snapshot.storageCapacity())); + ui.set("#HotbarSummary.Text", escape( + "Barre rapide : " + snapshot.hotbarUsed() + " / " + snapshot.hotbarCapacity())); + ui.set("#ArmorSummary.Text", escape( + "Armure : " + snapshot.armorUsed() + " / " + snapshot.armorCapacity())); + ui.set("#UtilitySummary.Text", escape( + "Utilitaire : " + snapshot.utilityUsed() + " / " + snapshot.utilityCapacity())); + + float fill = snapshot.storageCapacity() <= 0 + ? 0f + : Math.min(1f, (float) snapshot.storageUsed() / snapshot.storageCapacity()); + ui.set("#WeightBar.Value", fill); + ui.set("#WeightValue.Text", escape(snapshot.totalItemCount() + " piles d'objets")); + } + + private void applyEmptyItemDetails(@Nonnull UICommandBuilder ui) { + ui.set("#ItemName.Text", escape("Aucun objet sélectionné")); + ui.set("#ItemRarity.Text", escape("Cliquez un emplacement")); + ui.set("#ItemDescription.Text", escape( + "Les grilles sont reliées à votre inventaire vanilla.")); + } + + private void applyItemDetails(@Nonnull UICommandBuilder ui, @Nonnull PlayerInventoryReader.ItemView item) { + ui.set("#ItemName.Text", escape(item.name())); + ui.set("#ItemRarity.Text", escape(item.rarity())); + ui.set("#ItemDescription.Text", escape(item.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 void bindInventoryGridEvents(@Nonnull UIEventBuilder events) { + bindGridEvents(events, "#ArmorGrid", InventoryComponent.ARMOR_SECTION_ID); + bindGridEvents(events, "#StorageGrid", InventoryComponent.STORAGE_SECTION_ID); + bindGridEvents(events, "#HotbarGrid", InventoryComponent.HOTBAR_SECTION_ID); + bindGridEvents(events, "#UtilityGrid", InventoryComponent.UTILITY_SECTION_ID); + } + + private static void bindGridEvents( + @Nonnull UIEventBuilder events, + @Nonnull String selector, + int sectionId) { + EventData data = EventData.of("Section", String.valueOf(sectionId)); + events.addEventBinding(CustomUIEventBindingType.SlotClicking, selector, data, false); + events.addEventBinding(CustomUIEventBindingType.SlotMouseEntered, selector, data, false); + events.addEventBinding(CustomUIEventBindingType.Dropped, selector, data, false); + events.addEventBinding(CustomUIEventBindingType.SlotMouseDragCompleted, selector, data, false); + } + + @Nonnull + private String classLabel() { + PlayerClass playerClass = ClassCatalog.byId(profile.getClassId()); + return playerClass == null ? "Sans classe" : playerClass.displayName(); + } + + @Nullable + private static PlayerMenuPage.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" -> PlayerMenuPage.Tab.CHARACTER; + case "inventory", "inventaire" -> PlayerMenuPage.Tab.INVENTORY; + case "skills", "competences", "compétences" -> PlayerMenuPage.Tab.SKILLS; + default -> null; + }; + } catch (RuntimeException ignored) { + return null; + } + } + + @Nullable + private static SlotSelection parseSlotSelection(@Nonnull String raw) { + try { + JsonObject json = JsonParser.parseString(raw).getAsJsonObject(); + Integer slotIndex = readInt(json, "SlotIndex", "slotIndex", "Index"); + if (slotIndex == null) { + return null; + } + Integer sectionId = readInt(json, "Section", "InventorySectionId", "SourceInventorySectionId"); + if (sectionId == null) { + sectionId = InventoryComponent.STORAGE_SECTION_ID; + } + return new SlotSelection(sectionId, slotIndex); + } catch (RuntimeException ignored) { + return null; + } + } + + @Nullable + private static Integer readInt(@Nonnull JsonObject json, @Nonnull String... keys) { + for (String key : keys) { + if (!json.has(key)) { + continue; + } + JsonElement element = json.get(key); + if (element.isJsonPrimitive()) { + try { + return element.getAsInt(); + } catch (NumberFormatException ignored) { + String asString = element.getAsString(); + if (!asString.isBlank()) { + return Integer.parseInt(asString); + } + } + } + } + return null; + } + + @Nonnull + private static String escape(@Nonnull String text) { + return text.replace("\\", "\\\\").replace("\"", "\\\""); + } + + private record SlotSelection(int sectionId, int slotIndex) { + } + + public static final class UiEvent { + public static final BuilderCodec CODEC = + BuilderCodec.builder(UiEvent.class, UiEvent::new).build(); + + private UiEvent() { + } + } +} diff --git a/src/main/java/com/disklexar/mmorpg/ui/PlayerInventoryReader.java b/src/main/java/com/disklexar/mmorpg/ui/PlayerInventoryReader.java new file mode 100644 index 0000000..25c01c7 --- /dev/null +++ b/src/main/java/com/disklexar/mmorpg/ui/PlayerInventoryReader.java @@ -0,0 +1,167 @@ +package com.disklexar.mmorpg.ui; + +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.inventory.InventoryComponent; +import com.hypixel.hytale.server.core.inventory.ItemStack; +import com.hypixel.hytale.server.core.inventory.container.ItemContainer; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Reads live player inventory sections for the MMORPG inventory page. + */ +public final class PlayerInventoryReader { + + public record Snapshot( + int storageUsed, + int storageCapacity, + int hotbarUsed, + int hotbarCapacity, + int armorUsed, + int armorCapacity, + int utilityUsed, + int utilityCapacity, + int totalItemCount) { + } + + public record ItemView( + @Nonnull String name, + @Nonnull String rarity, + @Nonnull String description, + @Nonnull String itemId, + int quantity) { + } + + private PlayerInventoryReader() { + } + + @Nonnull + public static Snapshot snapshot(@Nonnull Store store, @Nonnull Ref ref) { + SectionCounts storage = countSection(store, ref, InventoryComponent.STORAGE_SECTION_ID); + SectionCounts hotbar = countSection(store, ref, InventoryComponent.HOTBAR_SECTION_ID); + SectionCounts armor = countSection(store, ref, InventoryComponent.ARMOR_SECTION_ID); + SectionCounts utility = countSection(store, ref, InventoryComponent.UTILITY_SECTION_ID); + + return new Snapshot( + storage.used(), + storage.capacity(), + hotbar.used(), + hotbar.capacity(), + armor.used(), + armor.capacity(), + utility.used(), + utility.capacity(), + storage.used() + hotbar.used() + armor.used() + utility.used()); + } + + @Nullable + public static ItemView itemAt( + @Nonnull Store store, + @Nonnull Ref ref, + int sectionId, + int slotIndex) { + ItemContainer container = container(store, ref, sectionId); + if (container == null || slotIndex < 0 || slotIndex >= container.getCapacity()) { + return null; + } + ItemStack stack = container.getItemStack((short) slotIndex); + if (ItemStack.isEmpty(stack)) { + return null; + } + return toView(stack, sectionId, slotIndex); + } + + @Nullable + private static ItemContainer container( + @Nonnull Store store, + @Nonnull Ref ref, + int sectionId) { + var type = InventoryComponent.getComponentTypeById(sectionId); + if (type == null) { + return null; + } + InventoryComponent component = store.getComponent(ref, type); + return component == null ? null : component.getInventory(); + } + + @Nonnull + private static SectionCounts countSection( + @Nonnull Store store, + @Nonnull Ref ref, + int sectionId) { + ItemContainer container = container(store, ref, sectionId); + if (container == null) { + return new SectionCounts(0, 0); + } + int used = 0; + for (short slot = 0; slot < container.getCapacity(); slot++) { + if (!ItemStack.isEmpty(container.getItemStack(slot))) { + used++; + } + } + return new SectionCounts(used, container.getCapacity()); + } + + @Nonnull + private static ItemView toView( + @Nonnull ItemStack stack, + int sectionId, + int slotIndex) { + String itemId = stack.getItemId() == null ? "inconnu" : stack.getItemId(); + String name = formatItemId(itemId); + int quantity = stack.getQuantity(); + + StringBuilder description = new StringBuilder(); + description.append("ID : ").append(itemId); + description.append("\nSection : ").append(sectionLabel(sectionId)); + description.append("\nEmplacement : ").append(slotIndex + 1); + description.append("\nQuantité : ").append(quantity); + + if (stack.getMaxDurability() > 0) { + description.append("\nDurabilité : ") + .append((int) stack.getDurability()) + .append(" / ") + .append((int) stack.getMaxDurability()); + } + + String rarity = stack.isBroken() ? "Endommagé" : "Objet vanilla"; + + return new ItemView(name, rarity, description.toString(), itemId, quantity); + } + + @Nonnull + private static String sectionLabel(int sectionId) { + if (sectionId == InventoryComponent.STORAGE_SECTION_ID) { + return "Sac"; + } + if (sectionId == InventoryComponent.HOTBAR_SECTION_ID) { + return "Barre rapide"; + } + if (sectionId == InventoryComponent.ARMOR_SECTION_ID) { + return "Armure"; + } + if (sectionId == InventoryComponent.UTILITY_SECTION_ID) { + return "Utilitaire"; + } + return "Section " + sectionId; + } + + @Nonnull + private static String formatItemId(@Nonnull String itemId) { + String trimmed = itemId; + if (trimmed.startsWith("Weapon_")) { + trimmed = trimmed.substring("Weapon_".length()); + } else if (trimmed.startsWith("Tool_")) { + trimmed = trimmed.substring("Tool_".length()); + } else if (trimmed.startsWith("Armor_")) { + trimmed = trimmed.substring("Armor_".length()); + } + return trimmed.replace('_', ' '); + } + + private record SectionCounts(int used, int capacity) { + } +} diff --git a/src/main/java/com/disklexar/mmorpg/ui/PlayerMenuPage.java b/src/main/java/com/disklexar/mmorpg/ui/PlayerMenuPage.java new file mode 100644 index 0000000..7485205 --- /dev/null +++ b/src/main/java/com/disklexar/mmorpg/ui/PlayerMenuPage.java @@ -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. + *

+ * 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() { + } + } +} diff --git a/src/main/resources/Common/UI/Custom/MmorpgAbilityBar.ui b/src/main/resources/Common/UI/Custom/MmorpgAbilityBar.ui index 6d7dba9..83bb470 100644 --- a/src/main/resources/Common/UI/Custom/MmorpgAbilityBar.ui +++ b/src/main/resources/Common/UI/Custom/MmorpgAbilityBar.ui @@ -149,7 +149,7 @@ Group { Label #Slot1Key { Style: (FontSize: 12, RenderBold: true, TextColor: #f5c842); - Text: "[1]"; + Text: "[Q]"; Anchor: (Bottom: 2); } @@ -167,7 +167,7 @@ Group { Label #Slot2Key { Style: (FontSize: 12, RenderBold: true, TextColor: #f5c842); - Text: "[2]"; + Text: "[E]"; Anchor: (Bottom: 2); } @@ -185,7 +185,7 @@ Group { Label #Slot3Key { Style: (FontSize: 12, RenderBold: true, TextColor: #f5c842); - Text: "[3]"; + Text: "[R]"; Anchor: (Bottom: 2); } diff --git a/src/main/resources/Common/UI/Custom/Pages/MmorpgCharacterTab.ui b/src/main/resources/Common/UI/Custom/Pages/MmorpgCharacterTab.ui new file mode 100644 index 0000000..312ec55 --- /dev/null +++ b/src/main/resources/Common/UI/Custom/Pages/MmorpgCharacterTab.ui @@ -0,0 +1,206 @@ +@MmorpgAccent = #f5c842; +@MmorpgText = #e8e8ec; +@MmorpgMuted = #878e9c; +@MmorpgBorder = #3a3f4a; +@HealthFill = PatchStyle(Color: #e74c3c); +@HealthBg = PatchStyle(Color: #2a1515); +@StaminaFill = PatchStyle(Color: #2ecc71); +@StaminaBg = PatchStyle(Color: #152a1c); +@XpFill = PatchStyle(Color: #4a9eff); +@XpBg = PatchStyle(Color: #152030); + +Group { + LayoutMode: Left; + + Group #LeftColumn { + LayoutMode: Top; + Anchor: (Width: 400, Right: 16); + + Label { + Style: (FontSize: 11, RenderBold: true, TextColor: @MmorpgMuted); + Text: "IDENTITÉ"; + Anchor: (Bottom: 6); + } + + Label #IdentityName { + Style: (FontSize: 18, RenderBold: true, TextColor: @MmorpgAccent); + Text: "Joueur"; + Anchor: (Bottom: 4); + } + + Label #RaceLabel { + Style: (FontSize: 13, TextColor: @MmorpgText); + Text: "Race : Humain"; + Anchor: (Bottom: 2); + } + + Label #ClassLabel { + Style: (FontSize: 13, TextColor: @MmorpgText); + Text: "Classe : —"; + Anchor: (Bottom: 10); + } + + Label { + Style: (FontSize: 11, RenderBold: true, TextColor: @MmorpgMuted); + Text: "PROGRESSION"; + Anchor: (Bottom: 6); + } + + Label #XpValue { + Style: (FontSize: 11, TextColor: @MmorpgText); + Text: "0 / 100 XP"; + Anchor: (Bottom: 4); + } + + ProgressBar #XpBar { + Anchor: (Width: 360, Height: 14, Bottom: 10); + Bar: @XpFill; + Background: @XpBg; + Value: 0.0; + Direction: End; + } + + Label { + Style: (FontSize: 11, RenderBold: true, TextColor: @MmorpgMuted); + Text: "VITALITÉ"; + Anchor: (Bottom: 6); + } + + Label #HealthValue { + Style: (FontSize: 10, TextColor: @MmorpgText); + Text: "Vie : —"; + Anchor: (Bottom: 2); + } + + ProgressBar #HealthBar { + Anchor: (Width: 360, Height: 12, Bottom: 6); + Bar: @HealthFill; + Background: @HealthBg; + Value: 1.0; + Direction: End; + } + + Label #StaminaValue { + Style: (FontSize: 10, TextColor: @MmorpgText); + Text: "Endurance : —"; + Anchor: (Bottom: 2); + } + + ProgressBar #StaminaBar { + Anchor: (Width: 360, Height: 12, Bottom: 10); + Bar: @StaminaFill; + Background: @StaminaBg; + Value: 1.0; + Direction: End; + } + + Label { + Style: (FontSize: 11, RenderBold: true, TextColor: @MmorpgMuted); + Text: "ATTRIBUTS"; + Anchor: (Bottom: 6); + } + + Group { + LayoutMode: Left; + Anchor: (Bottom: 4); + + Label #StatStr { + Style: (FontSize: 12, TextColor: @MmorpgText); + Text: "FOR 10"; + Padding: (Horizontal: 10, Vertical: 4); + Background: #2a3140; + Anchor: (Right: 6); + } + + Label #StatAgi { + Style: (FontSize: 12, TextColor: @MmorpgText); + Text: "AGI 10"; + Padding: (Horizontal: 10, Vertical: 4); + Background: #2a3140; + Anchor: (Right: 6); + } + + Label #StatInt { + Style: (FontSize: 12, TextColor: @MmorpgText); + Text: "INT 10"; + Padding: (Horizontal: 10, Vertical: 4); + Background: #2a3140; + } + } + + Label #StatsHint { + Style: (FontSize: 9, TextColor: @MmorpgMuted); + Text: "Attributs dérivés du niveau (système M1 à venir)"; + } + } + + Group #RightColumn { + LayoutMode: Top; + FlexWeight: 1; + + Label { + Style: (FontSize: 11, RenderBold: true, TextColor: @MmorpgMuted); + Text: "POUVOIRS PASSIFS"; + Anchor: (Bottom: 4); + } + + Label #PowersList { + Style: (FontSize: 12, TextColor: @MmorpgText, Wrap: true); + Text: "Aucun"; + Anchor: (Bottom: 10); + } + + Label { + Style: (FontSize: 11, RenderBold: true, TextColor: @MmorpgMuted); + Text: "MÉTIERS"; + Anchor: (Bottom: 4); + } + + Label #JobsList { + Style: (FontSize: 12, TextColor: @MmorpgText, Wrap: true); + Text: "Aucun"; + Anchor: (Bottom: 10); + } + + Label { + Style: (FontSize: 11, RenderBold: true, TextColor: @MmorpgMuted); + Text: "SOCIAL"; + Anchor: (Bottom: 4); + } + + Label #GroupLabel { + Style: (FontSize: 12, TextColor: @MmorpgText); + Text: "Groupe : Aucun"; + Anchor: (Bottom: 2); + } + + Label #GuildLabel { + Style: (FontSize: 12, TextColor: @MmorpgText); + Text: "Guilde : Aucune"; + Anchor: (Bottom: 10); + } + + Label { + Style: (FontSize: 11, RenderBold: true, TextColor: @MmorpgMuted); + Text: "STATISTIQUES"; + Anchor: (Bottom: 4); + } + + Label #PlayTimeLabel { + Style: (FontSize: 12, TextColor: @MmorpgText); + Text: "Temps de jeu : 0 min"; + Anchor: (Bottom: 2); + } + + Label #LastLoginLabel { + Style: (FontSize: 12, TextColor: @MmorpgText); + Text: "Dernière connexion : —"; + Anchor: (Bottom: 2); + } + + Label #CreatedLabel { + Style: (FontSize: 12, TextColor: @MmorpgText); + Text: "Création : —"; + } + } +} diff --git a/src/main/resources/Common/UI/Custom/Pages/MmorpgInventoryPage.ui b/src/main/resources/Common/UI/Custom/Pages/MmorpgInventoryPage.ui new file mode 100644 index 0000000..01dc255 --- /dev/null +++ b/src/main/resources/Common/UI/Custom/Pages/MmorpgInventoryPage.ui @@ -0,0 +1,277 @@ +@MmorpgBg = #0d0f14(0.96); +@MmorpgPanel = #1a1c20; +@MmorpgAccent = #f5c842; +@MmorpgText = #e8e8ec; +@MmorpgMuted = #878e9c; +@MmorpgBorder = #3a3f4a; + +@ShellWidth = 920; +@ShellHeight = 660; + +@GridStyle = ItemGridStyle( + SlotSpacing: 4, + SlotSize: 52, + SlotIconSize: 44 +); + +Group { + LayoutMode: Center; + + Group #MmorpgShell { + LayoutMode: Top; + Anchor: (Width: @ShellWidth, Height: @ShellHeight); + Background: @MmorpgBg; + Padding: (Full: 16); + + Group #Header { + LayoutMode: Left; + Anchor: (Bottom: 12); + + Group { + LayoutMode: Top; + FlexWeight: 1; + + Label #PlayerName { + Style: (FontSize: 20, RenderBold: true, TextColor: @MmorpgAccent); + Text: "Joueur"; + Anchor: (Bottom: 2); + } + + Label #ClassLabel { + Style: (FontSize: 13, TextColor: @MmorpgText); + Text: "Classe"; + } + } + + Group { + LayoutMode: Top; + Anchor: (Right: 24); + + Label #LevelLabel { + Style: (FontSize: 14, RenderBold: true, TextColor: @MmorpgText); + Text: "Nv. 1"; + Anchor: (Bottom: 2); + } + + Label #MoneyLabel { + Style: (FontSize: 13, TextColor: @MmorpgAccent); + Text: "0 pièces"; + } + } + } + + Group #TabBar { + LayoutMode: Left; + Padding: (Bottom: 10); + Anchor: (Bottom: 8); + + Group #TabCharacterWrap { + LayoutMode: Top; + Padding: (Horizontal: 12, Vertical: 4); + Anchor: (Right: 6); + Background: PatchStyle(Color: #00000000); + + TextButton #TabCharacter { + Text: "Personnage"; + } + + Group #TabCharacterIndicator { + Anchor: (Height: 2, Top: 4); + Background: PatchStyle(Color: @MmorpgAccent); + Visible: false; + } + } + + Group #TabInventoryWrap { + LayoutMode: Top; + Padding: (Horizontal: 12, Vertical: 4); + Anchor: (Right: 6); + Background: PatchStyle(Color: #00000000); + + TextButton #TabInventory { + Text: "Inventaire"; + } + + Group #TabInventoryIndicator { + Anchor: (Height: 2, Top: 4); + Background: PatchStyle(Color: @MmorpgAccent); + Visible: false; + } + } + + Group #TabSkillsWrap { + LayoutMode: Top; + Padding: (Horizontal: 12, Vertical: 4); + Anchor: (Right: 6); + Background: PatchStyle(Color: #00000000); + + TextButton #TabSkills { + Text: "Compétences"; + } + + Group #TabSkillsIndicator { + Anchor: (Height: 2, Top: 4); + Background: PatchStyle(Color: @MmorpgAccent); + Visible: false; + } + } + } + + Group #TabContent { + LayoutMode: Left; + FlexWeight: 1; + + Group #EquipmentPanel { + LayoutMode: Top; + Anchor: (Width: 88, Right: 12); + Padding: (Full: 8); + Background: @MmorpgPanel; + + Label { + Style: (FontSize: 11, RenderBold: true, TextColor: @MmorpgMuted); + Text: "ARMURE"; + Anchor: (Bottom: 4); + } + + Label #ArmorSummary { + Style: (FontSize: 9, TextColor: @MmorpgMuted); + Text: "Armure : 0 / 4"; + Anchor: (Bottom: 6); + } + + ItemGrid #ArmorGrid { + Anchor: (Width: 56, Height: 232, Bottom: 10); + SlotsPerRow: 1; + InventorySectionId: -3; + AreItemsDraggable: true; + DisplayItemQuantity: true; + RenderItemQualityBackground: true; + Style: @GridStyle; + } + + Label { + Style: (FontSize: 11, RenderBold: true, TextColor: @MmorpgMuted); + Text: "UTILITAIRE"; + Anchor: (Bottom: 4); + } + + Label #UtilitySummary { + Style: (FontSize: 9, TextColor: @MmorpgMuted); + Text: "Utilitaire : 0 / 4"; + Anchor: (Bottom: 6); + } + + ItemGrid #UtilityGrid { + Anchor: (Width: 56, Height: 232); + SlotsPerRow: 1; + InventorySectionId: -5; + AreItemsDraggable: true; + DisplayItemQuantity: true; + RenderItemQualityBackground: true; + Style: @GridStyle; + } + } + + Group #InventoryCenter { + LayoutMode: Top; + FlexWeight: 1; + Padding: (Full: 8); + Background: @MmorpgPanel; + + Label #StorageSummary { + Style: (FontSize: 11, RenderBold: true, TextColor: @MmorpgAccent); + Text: "Sac : 0 / 36"; + Anchor: (Bottom: 6); + } + + ItemGrid #StorageGrid { + Anchor: (Width: 500, Height: 232, Bottom: 10); + SlotsPerRow: 9; + InventorySectionId: -2; + AreItemsDraggable: true; + DisplayItemQuantity: true; + RenderItemQualityBackground: true; + Style: @GridStyle; + } + + Label #HotbarSummary { + Style: (FontSize: 11, RenderBold: true, TextColor: @MmorpgAccent); + Text: "Barre rapide : 0 / 9"; + Anchor: (Bottom: 6); + } + + ItemGrid #HotbarGrid { + Anchor: (Width: 500, Height: 60); + SlotsPerRow: 9; + InventorySectionId: -1; + AreItemsDraggable: true; + DisplayItemQuantity: true; + RenderItemQualityBackground: true; + Style: @GridStyle; + } + } + + Group #ItemDetails { + LayoutMode: Top; + Anchor: (Width: 260, Left: 12); + Padding: (Full: 10); + Background: @MmorpgPanel; + + Label { + Style: (FontSize: 11, RenderBold: true, TextColor: @MmorpgMuted); + Text: "DÉTAILS"; + Anchor: (Bottom: 8); + } + + Label #ItemName { + Style: (FontSize: 15, RenderBold: true, TextColor: @MmorpgAccent); + Text: "Aucun objet"; + Anchor: (Bottom: 6); + } + + Label #ItemRarity { + Style: (FontSize: 11, TextColor: @MmorpgMuted); + Text: "Sélectionnez un emplacement"; + Anchor: (Bottom: 8); + } + + Label #ItemDescription { + Style: (FontSize: 12, TextColor: @MmorpgText, Wrap: true); + Text: "Cliquez un objet pour afficher ses informations."; + Anchor: (Bottom: 12); + } + + Group #WeightTrack { + LayoutMode: Top; + Anchor: (Bottom: 4); + + Label { + Style: (FontSize: 10, TextColor: @MmorpgMuted); + Text: "Remplissage du sac"; + Anchor: (Bottom: 4); + } + + ProgressBar #WeightBar { + Anchor: (Width: 220, Height: 10); + Bar: PatchStyle(Color: @MmorpgAccent); + Background: PatchStyle(Color: @MmorpgBorder); + Value: 0.0; + Direction: End; + } + + Label #WeightValue { + Style: (FontSize: 9, TextColor: @MmorpgMuted); + Text: "0 piles"; + Anchor: (Top: 4); + } + } + } + } + + Label #FooterHint { + Style: (FontSize: 11, TextColor: @MmorpgMuted); + Text: "Échap pour fermer — glisser-déposer actif (inventaire vanilla synchronisé)"; + Anchor: (Top: 10); + } + } +} diff --git a/src/main/resources/Common/UI/Custom/Pages/MmorpgInventoryTab.ui b/src/main/resources/Common/UI/Custom/Pages/MmorpgInventoryTab.ui new file mode 100644 index 0000000..79fcdf9 --- /dev/null +++ b/src/main/resources/Common/UI/Custom/Pages/MmorpgInventoryTab.ui @@ -0,0 +1,161 @@ +@MmorpgPanel = #1a1c20; +@MmorpgAccent = #f5c842; +@MmorpgText = #e8e8ec; +@MmorpgMuted = #878e9c; +@MmorpgBorder = #3a3f4a; + +@GridStyle = ItemGridStyle( + SlotSpacing: 4, + SlotSize: 52, + SlotIconSize: 44 +); + +Group { + LayoutMode: Left; + + Group #EquipmentPanel { + LayoutMode: Top; + Anchor: (Width: 88, Right: 12); + Padding: (Full: 8); + Background: @MmorpgPanel; + + Label { + Style: (FontSize: 11, RenderBold: true, TextColor: @MmorpgMuted); + Text: "ARMURE"; + Anchor: (Bottom: 4); + } + + Label #ArmorSummary { + Style: (FontSize: 9, TextColor: @MmorpgMuted); + Text: "Armure : 0 / 4"; + Anchor: (Bottom: 6); + } + + ItemGrid #ArmorGrid { + Anchor: (Width: 56, Height: 232, Bottom: 10); + SlotsPerRow: 1; + InventorySectionId: -3; + AreItemsDraggable: true; + DisplayItemQuantity: true; + RenderItemQualityBackground: true; + Style: @GridStyle; + } + + Label { + Style: (FontSize: 11, RenderBold: true, TextColor: @MmorpgMuted); + Text: "UTILITAIRE"; + Anchor: (Bottom: 4); + } + + Label #UtilitySummary { + Style: (FontSize: 9, TextColor: @MmorpgMuted); + Text: "Utilitaire : 0 / 4"; + Anchor: (Bottom: 6); + } + + ItemGrid #UtilityGrid { + Anchor: (Width: 56, Height: 232); + SlotsPerRow: 1; + InventorySectionId: -5; + AreItemsDraggable: true; + DisplayItemQuantity: true; + RenderItemQualityBackground: true; + Style: @GridStyle; + } + } + + Group #InventoryCenter { + LayoutMode: Top; + FlexWeight: 1; + Padding: (Full: 8); + Background: @MmorpgPanel; + + Label #StorageSummary { + Style: (FontSize: 11, RenderBold: true, TextColor: @MmorpgAccent); + Text: "Sac : 0 / 36"; + Anchor: (Bottom: 6); + } + + ItemGrid #StorageGrid { + Anchor: (Width: 500, Height: 232, Bottom: 10); + SlotsPerRow: 9; + InventorySectionId: -2; + AreItemsDraggable: true; + DisplayItemQuantity: true; + RenderItemQualityBackground: true; + Style: @GridStyle; + } + + Label #HotbarSummary { + Style: (FontSize: 11, RenderBold: true, TextColor: @MmorpgAccent); + Text: "Barre rapide : 0 / 9"; + Anchor: (Bottom: 6); + } + + ItemGrid #HotbarGrid { + Anchor: (Width: 500, Height: 60); + SlotsPerRow: 9; + InventorySectionId: -1; + AreItemsDraggable: true; + DisplayItemQuantity: true; + RenderItemQualityBackground: true; + Style: @GridStyle; + } + } + + Group #ItemDetails { + LayoutMode: Top; + Anchor: (Width: 260, Left: 12); + Padding: (Full: 10); + Background: @MmorpgPanel; + + Label { + Style: (FontSize: 11, RenderBold: true, TextColor: @MmorpgMuted); + Text: "DÉTAILS"; + Anchor: (Bottom: 8); + } + + Label #ItemName { + Style: (FontSize: 15, RenderBold: true, TextColor: @MmorpgAccent); + Text: "Aucun objet"; + Anchor: (Bottom: 6); + } + + Label #ItemRarity { + Style: (FontSize: 11, TextColor: @MmorpgMuted); + Text: "Sélectionnez un emplacement"; + Anchor: (Bottom: 8); + } + + Label #ItemDescription { + Style: (FontSize: 12, TextColor: @MmorpgText, Wrap: true); + Text: "Cliquez un objet pour afficher ses informations."; + Anchor: (Bottom: 12); + } + + Group #WeightTrack { + LayoutMode: Top; + Anchor: (Bottom: 4); + + Label { + Style: (FontSize: 10, TextColor: @MmorpgMuted); + Text: "Remplissage du sac"; + Anchor: (Bottom: 4); + } + + ProgressBar #WeightBar { + Anchor: (Width: 220, Height: 10); + Bar: PatchStyle(Color: @MmorpgAccent); + Background: PatchStyle(Color: @MmorpgBorder); + Value: 0.0; + Direction: End; + } + + Label #WeightValue { + Style: (FontSize: 9, TextColor: @MmorpgMuted); + Text: "0 piles"; + Anchor: (Top: 4); + } + } + } +} diff --git a/src/main/resources/Common/UI/Custom/Pages/MmorpgPlayerShell.ui b/src/main/resources/Common/UI/Custom/Pages/MmorpgPlayerShell.ui new file mode 100644 index 0000000..60a21e0 --- /dev/null +++ b/src/main/resources/Common/UI/Custom/Pages/MmorpgPlayerShell.ui @@ -0,0 +1,131 @@ +@MmorpgBg = #0d0f14(0.96); +@MmorpgPanel = #1a1c20; +@MmorpgAccent = #f5c842; +@MmorpgText = #e8e8ec; +@MmorpgMuted = #878e9c; + +@ShellWidth = 920; +@ShellHeight = 660; + +Group { + LayoutMode: Center; + + Group #MmorpgShell { + LayoutMode: Top; + Anchor: (Width: @ShellWidth, Height: @ShellHeight); + Background: @MmorpgBg; + Padding: (Full: 16); + + Group #Header { + LayoutMode: Left; + Anchor: (Bottom: 12); + + Group { + LayoutMode: Top; + FlexWeight: 1; + + Label #PlayerName { + Style: (FontSize: 20, RenderBold: true, TextColor: @MmorpgAccent); + Text: "Joueur"; + Anchor: (Bottom: 2); + } + + Label #HeaderSubtitle { + Style: (FontSize: 13, TextColor: @MmorpgText); + Text: "Profil MMORPG"; + } + } + + Group { + LayoutMode: Top; + Anchor: (Right: 24); + + Label #LevelLabel { + Style: (FontSize: 14, RenderBold: true, TextColor: @MmorpgText); + Text: "Nv. 1"; + Anchor: (Bottom: 2); + } + + Label #MoneyLabel { + Style: (FontSize: 13, TextColor: @MmorpgAccent); + Text: "0 pièces"; + } + } + } + + Group #TabBar { + LayoutMode: Left; + Padding: (Bottom: 10); + Anchor: (Bottom: 8); + + Group #TabCharacterWrap { + LayoutMode: Top; + Padding: (Horizontal: 12, Vertical: 4); + Anchor: (Right: 6); + Background: PatchStyle(Color: #00000000); + + TextButton #TabCharacter { + Text: "Personnage"; + } + + Group #TabCharacterIndicator { + Anchor: (Height: 2, Top: 4); + Background: PatchStyle(Color: @MmorpgAccent); + Visible: false; + } + } + + Group #TabInventoryWrap { + LayoutMode: Top; + Padding: (Horizontal: 12, Vertical: 4); + Anchor: (Right: 6); + Background: PatchStyle(Color: #00000000); + + TextButton #TabInventory { + Text: "Inventaire"; + } + + Group #TabInventoryIndicator { + Anchor: (Height: 2, Top: 4); + Background: PatchStyle(Color: @MmorpgAccent); + Visible: false; + } + } + + Group #TabSkillsWrap { + LayoutMode: Top; + Padding: (Horizontal: 12, Vertical: 4); + Anchor: (Right: 6); + Background: PatchStyle(Color: #00000000); + + TextButton #TabSkills { + Text: "Compétences"; + } + + Group #TabSkillsIndicator { + Anchor: (Height: 2, Top: 4); + Background: PatchStyle(Color: @MmorpgAccent); + Visible: false; + } + } + } + + Group #TabContent { + LayoutMode: Top; + FlexWeight: 1; + + Group #DynamicTabHost { + LayoutMode: Top; + FlexWeight: 1; + Background: @MmorpgPanel; + Padding: (Full: 10); + } + } + + Label #FooterHint { + Style: (FontSize: 11, TextColor: @MmorpgMuted); + Text: "Échap pour fermer"; + Anchor: (Top: 10); + } + } +} diff --git a/src/main/resources/Common/UI/Custom/Pages/MmorpgSkillsTab.ui b/src/main/resources/Common/UI/Custom/Pages/MmorpgSkillsTab.ui new file mode 100644 index 0000000..c374f8e --- /dev/null +++ b/src/main/resources/Common/UI/Custom/Pages/MmorpgSkillsTab.ui @@ -0,0 +1,198 @@ +@MmorpgAccent = #f5c842; +@MmorpgText = #e8e8ec; +@MmorpgMuted = #878e9c; +@MmorpgSlot = #2a3140; +@MmorpgLocked = #1e2228; + +Group { + LayoutMode: Top; + + Label #ClassTitle { + Style: (FontSize: 16, RenderBold: true, TextColor: @MmorpgAccent); + Text: "Classe"; + Anchor: (Bottom: 2); + } + + Label #WeaponsLabel { + Style: (FontSize: 11, TextColor: @MmorpgMuted); + Text: "Armes : —"; + Anchor: (Bottom: 6); + } + + Label #PointsLabel { + Style: (FontSize: 12, TextColor: @MmorpgText); + Text: "Points de compétence : 0"; + Anchor: (Bottom: 10); + } + + Label { + Style: (FontSize: 11, RenderBold: true, TextColor: @MmorpgMuted); + Text: "CAPACITÉS DE CLASSE"; + Anchor: (Bottom: 6); + } + + Group { + LayoutMode: Left; + Anchor: (Bottom: 14); + + Group #AbilityPanel1 { + LayoutMode: Top; + Padding: (Full: 8); + Background: @MmorpgSlot; + Anchor: (Width: 250, Right: 10); + + Label #Ability1Key { + Style: (FontSize: 12, RenderBold: true, TextColor: @MmorpgAccent); + Text: "[Q]"; + Anchor: (Bottom: 4); + } + + Label #Ability1Name { + Style: (FontSize: 14, RenderBold: true, TextColor: @MmorpgText); + Text: "Capacité 1"; + Anchor: (Bottom: 4); + } + + Label #Ability1Cooldown { + Style: (FontSize: 10, TextColor: @MmorpgMuted); + Text: "Prêt"; + Anchor: (Bottom: 6); + } + + Label #Ability1Desc { + Style: (FontSize: 11, TextColor: @MmorpgMuted, Wrap: true); + Text: "—"; + } + } + + Group #AbilityPanel2 { + LayoutMode: Top; + Padding: (Full: 8); + Background: @MmorpgSlot; + Anchor: (Width: 250, Right: 10); + + Label #Ability2Key { + Style: (FontSize: 12, RenderBold: true, TextColor: @MmorpgAccent); + Text: "[E]"; + Anchor: (Bottom: 4); + } + + Label #Ability2Name { + Style: (FontSize: 14, RenderBold: true, TextColor: @MmorpgText); + Text: "Capacité 2"; + Anchor: (Bottom: 4); + } + + Label #Ability2Cooldown { + Style: (FontSize: 10, TextColor: @MmorpgMuted); + Text: "Prêt"; + Anchor: (Bottom: 6); + } + + Label #Ability2Desc { + Style: (FontSize: 11, TextColor: @MmorpgMuted, Wrap: true); + Text: "—"; + } + } + + Group #AbilityPanel3 { + LayoutMode: Top; + Padding: (Full: 8); + Background: @MmorpgSlot; + Anchor: (Width: 250); + + Label #Ability3Key { + Style: (FontSize: 12, RenderBold: true, TextColor: @MmorpgAccent); + Text: "[R]"; + Anchor: (Bottom: 4); + } + + Label #Ability3Name { + Style: (FontSize: 14, RenderBold: true, TextColor: @MmorpgText); + Text: "Capacité 3"; + Anchor: (Bottom: 4); + } + + Label #Ability3Cooldown { + Style: (FontSize: 10, TextColor: @MmorpgMuted); + Text: "Prêt"; + Anchor: (Bottom: 6); + } + + Label #Ability3Desc { + Style: (FontSize: 11, TextColor: @MmorpgMuted, Wrap: true); + Text: "—"; + } + } + } + + Label { + Style: (FontSize: 11, RenderBold: true, TextColor: @MmorpgMuted); + Text: "BRANCHES DE COMPÉTENCES"; + Anchor: (Bottom: 6); + } + + Group { + LayoutMode: Left; + Anchor: (Bottom: 8); + + Group #Branch1 { + LayoutMode: Top; + Padding: (Full: 8); + Background: @MmorpgLocked; + Anchor: (Width: 180, Right: 10); + + Label #Branch1Title { + Style: (FontSize: 12, RenderBold: true, TextColor: @MmorpgMuted); + Text: "Offensive"; + Anchor: (Bottom: 4); + } + + Label #Branch1Status { + Style: (FontSize: 10, TextColor: @MmorpgMuted); + Text: "Verrouillé — M1"; + } + } + + Group #Branch2 { + LayoutMode: Top; + Padding: (Full: 8); + Background: @MmorpgLocked; + Anchor: (Width: 180, Right: 10); + + Label #Branch2Title { + Style: (FontSize: 12, RenderBold: true, TextColor: @MmorpgMuted); + Text: "Défensive"; + Anchor: (Bottom: 4); + } + + Label #Branch2Status { + Style: (FontSize: 10, TextColor: @MmorpgMuted); + Text: "Verrouillé — M1"; + } + } + + Group #Branch3 { + LayoutMode: Top; + Padding: (Full: 8); + Background: @MmorpgLocked; + Anchor: (Width: 180); + + Label #Branch3Title { + Style: (FontSize: 12, RenderBold: true, TextColor: @MmorpgMuted); + Text: "Utilitaire"; + Anchor: (Bottom: 4); + } + + Label #Branch3Status { + Style: (FontSize: 10, TextColor: @MmorpgMuted); + Text: "Verrouillé — M1"; + } + } + } + + Label #SkillsHint { + Style: (FontSize: 10, TextColor: @MmorpgMuted, Wrap: true); + Text: "Utilisez [Q] [E] [R] (Use Ability 1/2/3) en jeu. L'arbre de talents complet arrive en M1."; + } +} diff --git a/src/main/resources/Server/Item/Items/Weapon_Battleaxe_Mithril.json b/src/main/resources/Server/Item/Items/Weapon_Battleaxe_Mithril.json index 9cb88e5..65887d5 100644 --- a/src/main/resources/Server/Item/Items/Weapon_Battleaxe_Mithril.json +++ b/src/main/resources/Server/Item/Items/Weapon_Battleaxe_Mithril.json @@ -1,19 +1,166 @@ { - "Id": "Weapon_Battleaxe_Mithril", + "Parent": "Template_Weapon_Battleaxe", + "TranslationProperties": { + "Name": "server.items.Weapon_Battleaxe_Mithril.name" + }, + "Model": "Items/Weapons/Battleaxe/Mithril.blockymodel", + "Texture": "Items/Weapons/Battleaxe/Mithril_Texture.png", + "Icon": "Icons/ItemsGenerated/Weapon_Battleaxe_Mithril.png", + "Recipe": { + "TimeSeconds": 5, + "KnowledgeRequired": false, + "Input": [ + { + "ItemId": "Ingredient_Bar_Mithril", + "Quantity": 10 + }, + { + "ItemId": "Ingredient_Leather_Storm", + "Quantity": 4 + }, + { + "ItemId": "Ingredient_Voidheart", + "Quantity": 1 + } + ], + "BenchRequirement": [ + { + "Type": "Crafting", + "Categories": [ + "Weapon_Battleaxe" + ], + "Id": "Weapon_Bench", + "RequiredTierLevel": 3 + } + ] + }, + "Quality": "Epic", + "ItemLevel": 50, + "InteractionVars": { + "Swing_Down_Damage": { + "Interactions": [ + { + "Parent": "Weapon_Battleaxe_Primary_Swing_Down_Damage", + "DamageCalculator": { + "BaseDamage": { + "Physical": 73 + } + }, + "DamageEffects": { + "WorldSoundEventId": "SFX_Battleaxe_T2_Impact", + "LocalSoundEventId": "SFX_Battleaxe_T2_Impact" + } + } + ] + }, + "Swing_Down_Left_Damage": { + "Interactions": [ + { + "Parent": "Weapon_Battleaxe_Primary_Swing_Down_Left_Damage", + "DamageCalculator": { + "BaseDamage": { + "Physical": 36 + } + }, + "DamageEffects": { + "WorldSoundEventId": "SFX_Battleaxe_T2_Impact", + "LocalSoundEventId": "SFX_Battleaxe_T2_Impact" + } + } + ] + }, + "Swing_Down_Right_Damage": { + "Interactions": [ + { + "Parent": "Weapon_Battleaxe_Primary_Swing_Down_Right_Damage", + "DamageCalculator": { + "BaseDamage": { + "Physical": 44 + } + }, + "DamageEffects": { + "WorldSoundEventId": "SFX_Battleaxe_T2_Impact", + "LocalSoundEventId": "SFX_Battleaxe_T2_Impact" + } + } + ] + }, + "Downstrike_Damage": { + "Interactions": [ + { + "Parent": "Weapon_Battleaxe_Primary_Downstrike_Damage", + "DamageCalculator": { + "BaseDamage": { + "Physical": 57 + } + }, + "EntityStatsOnHit": [ + { + "EntityStatId": "SignatureEnergy", + "Amount": 2 + } + ], + "DamageEffects": { + "WorldSoundEventId": "SFX_Battleaxe_T2_Impact", + "LocalSoundEventId": "SFX_Battleaxe_T2_Impact" + } + } + ] + }, + "Signature_Whirlwind_Damage": { + "Interactions": [ + { + "Parent": "Weapon_Battleaxe_Signature_Whirlwind_Damage", + "DamageCalculator": { + "BaseDamage": { + "Physical": 29 + } + }, + "EntityStatsOnHit": [], + "DamageEffects": { + "WorldSoundEventId": "SFX_Battleaxe_T2_Impact", + "LocalSoundEventId": "SFX_Battleaxe_T2_Impact" + } + } + ] + }, + "Guard_Wield": { + "Interactions": [ + { + "Parent": "Weapon_Battleaxe_Secondary_Guard_Wield", + "StaminaCost": { + "Value": 20, + "CostType": "Damage" + } + } + ] + } + }, + "MaxDurability": 220, + "DurabilityLossOnHit": 0.45, "Interactions": { "Ability1": { "Interactions": [ - { "Type": "mmorpg_cast_ability", "Slot": 1 } + { + "Type": "mmorpg_cast_ability", + "Slot": 1 + } ] }, "Ability2": { "Interactions": [ - { "Type": "mmorpg_cast_ability", "Slot": 2 } + { + "Type": "mmorpg_cast_ability", + "Slot": 2 + } ] }, "Ability3": { "Interactions": [ - { "Type": "mmorpg_cast_ability", "Slot": 3 } + { + "Type": "mmorpg_cast_ability", + "Slot": 3 + } ] } } diff --git a/src/main/resources/Server/Item/Items/Weapon_Crossbow_Ancient_Steel.json b/src/main/resources/Server/Item/Items/Weapon_Crossbow_Ancient_Steel.json index 846cad5..ab3a34e 100644 --- a/src/main/resources/Server/Item/Items/Weapon_Crossbow_Ancient_Steel.json +++ b/src/main/resources/Server/Item/Items/Weapon_Crossbow_Ancient_Steel.json @@ -1,19 +1,87 @@ { - "Id": "Weapon_Crossbow_Ancient_Steel", + "Parent": "Template_Weapon_Crossbow", + "TranslationProperties": { + "Name": "server.items.Weapon_Crossbow_Ancient_Steel.name" + }, + "Model": "Items/Weapons/Crossbow/Iron.blockymodel", + "Texture": "Items/Weapons/Crossbow/Iron_Rusty_Texture.png", + "Icon": "Icons/ItemsGenerated/Weapon_Crossbow_Iron_Rusty.png", + "Quality": "Rare", + "ItemLevel": 30, + "InteractionVars": { + "Standard_Projectile_Damage": { + "Interactions": [ + { + "Parent": "Weapon_Crossbow_Damage_Standard_Projectile", + "DamageCalculator": { + "BaseDamage": { + "Projectile": 16 + } + } + } + ] + }, + "Combo_Projectile_Damage": { + "Interactions": [ + { + "Parent": "Weapon_Crossbow_Damage_Combo_Projectile", + "DamageCalculator": { + "BaseDamage": { + "Projectile": 42 + } + } + } + ] + }, + "Signature_BigArrow_Damage": { + "Interactions": [ + { + "Parent": "Weapon_Crossbow_Damage_Signature_Projectile", + "DamageCalculator": { + "BaseDamage": { + "Projectile": 123 + } + } + } + ] + }, + "Guard_Wield": { + "Interactions": [ + { + "Parent": "Weapon_Crossbow_Secondary_Guard_Wield", + "StaminaCost": { + "Value": 15.56, + "CostType": "Damage" + } + } + ] + } + }, + "MaxDurability": 180, + "DurabilityLossOnHit": 0.28, "Interactions": { "Ability1": { "Interactions": [ - { "Type": "mmorpg_cast_ability", "Slot": 1 } + { + "Type": "mmorpg_cast_ability", + "Slot": 1 + } ] }, "Ability2": { "Interactions": [ - { "Type": "mmorpg_cast_ability", "Slot": 2 } + { + "Type": "mmorpg_cast_ability", + "Slot": 2 + } ] }, "Ability3": { "Interactions": [ - { "Type": "mmorpg_cast_ability", "Slot": 3 } + { + "Type": "mmorpg_cast_ability", + "Slot": 3 + } ] } } diff --git a/src/main/resources/Server/Item/Items/Weapon_Daggers_Mithril.json b/src/main/resources/Server/Item/Items/Weapon_Daggers_Mithril.json index a053fed..a1ebb75 100644 --- a/src/main/resources/Server/Item/Items/Weapon_Daggers_Mithril.json +++ b/src/main/resources/Server/Item/Items/Weapon_Daggers_Mithril.json @@ -1,19 +1,462 @@ { - "Id": "Weapon_Daggers_Mithril", + "Parent": "Template_Weapon_Daggers", + "TranslationProperties": { + "Name": "server.items.Weapon_Daggers_Mithril.name" + }, + "Model": "Items/Weapons/Dagger/Mithril.blockymodel", + "Texture": "Items/Weapons/Dagger/Mithril_Texture.png", + "Icon": "Icons/ItemsGenerated/Weapon_Daggers_Mithril.png", + "Recipe": { + "TimeSeconds": 5, + "KnowledgeRequired": false, + "Input": [ + { + "ItemId": "Ingredient_Bar_Mithril", + "Quantity": 10 + }, + { + "ItemId": "Ingredient_Leather_Storm", + "Quantity": 3 + }, + { + "ItemId": "Ingredient_Voidheart", + "Quantity": 1 + } + ], + "BenchRequirement": [ + { + "Type": "Crafting", + "Categories": [ + "Weapon_Daggers" + ], + "Id": "Weapon_Bench", + "RequiredTierLevel": 3 + } + ] + }, + "Quality": "Epic", + "ItemLevel": 50, + "InteractionVars": { + "Swing_Left_Damage": { + "Interactions": [ + { + "Parent": "Weapon_Daggers_Primary_Swing_Left_Damage", + "DamageCalculator": { + "BaseDamage": { + "Physical": 8 + } + }, + "DamageEffects": { + "WorldSoundEventId": "SFX_Daggers_T2_Slash_Impact", + "LocalSoundEventId": "SFX_Daggers_T2_Slash_Impact" + }, + "AngledDamage": [] + } + ] + }, + "Swing_Right_Damage": { + "Interactions": [ + { + "Parent": "Weapon_Daggers_Primary_Swing_Right_Damage", + "DamageCalculator": { + "BaseDamage": { + "Physical": 8 + } + }, + "DamageEffects": { + "WorldSoundEventId": "SFX_Daggers_T2_Slash_Impact", + "LocalSoundEventId": "SFX_Daggers_T2_Slash_Impact" + }, + "AngledDamage": [] + } + ] + }, + "Stab_Left_Damage": { + "Interactions": [ + { + "Parent": "Weapon_Daggers_Primary_Stab_Left_Damage", + "DamageCalculator": { + "BaseDamage": { + "Physical": 15 + } + }, + "DamageEffects": { + "WorldSoundEventId": "SFX_Daggers_T2_Stab_Impact", + "LocalSoundEventId": "SFX_Daggers_T2_Stab_Impact" + }, + "AngledDamage": [] + } + ] + }, + "Stab_Right_Damage": { + "Interactions": [ + { + "Parent": "Weapon_Daggers_Primary_Stab_Right_Damage", + "DamageCalculator": { + "BaseDamage": { + "Physical": 25 + } + }, + "DamageEffects": { + "WorldSoundEventId": "SFX_Daggers_T2_Stab_Impact", + "LocalSoundEventId": "SFX_Daggers_T2_Stab_Impact" + }, + "AngledDamage": [] + } + ] + }, + "Pounce_Sweep_Damage": { + "Interactions": [ + { + "Parent": "Weapon_Daggers_Primary_Pounce_Sweep_Damage", + "DamageCalculator": { + "BaseDamage": { + "Physical": 61 + } + }, + "EntityStatsOnHit": [ + { + "EntityStatId": "SignatureEnergy", + "Amount": 1 + } + ], + "DamageEffects": { + "WorldSoundEventId": "SFX_Daggers_T2_Slash_Impact", + "LocalSoundEventId": "SFX_Daggers_T2_Slash_Impact" + }, + "AngledDamage": [ + { + "Angle": 180, + "AngleDistance": 80, + "DamageEffects": { + "Knockback": { + "Direction": { + "X": 0, + "Y": 2, + "Z": -3 + }, + "Type": "Force", + "Force": 6, + "VelocityType": "Set", + "VelocityConfig": { + "AirResistance": 0.99, + "AirResistanceMax": 0.98, + "GroundResistance": 0.94, + "GroundResistanceMax": 0.3, + "Threshold": 3.0, + "Style": "Linear" + } + }, + "WorldParticles": [ + { + "SystemId": "Impact_Critical", + "Scale": 1, + "RotationOffset": { + "Yaw": 180, + "Pitch": 0, + "Roll": 0 + } + } + ], + "LocalSoundEventId": "SFX_Daggers_T2_Slash_Impact", + "WorldSoundEventId": "SFX_Daggers_T2_Slash_Impact" + }, + "DamageCalculator": { + "BaseDamage": { + "Physical": 92 + } + } + } + ] + } + ] + }, + "Pounce_Stab_Damage": { + "Interactions": [ + { + "Parent": "Weapon_Daggers_Primary_Pounce_Stab_Damage", + "DamageCalculator": { + "BaseDamage": { + "Physical": 76 + } + }, + "EntityStatsOnHit": [ + { + "EntityStatId": "SignatureEnergy", + "Amount": 6 + } + ], + "DamageEffects": { + "WorldSoundEventId": "SFX_Daggers_T2_Stab_Impact", + "LocalSoundEventId": "SFX_Daggers_T2_Stab_Impact" + }, + "AngledDamage": [ + { + "Angle": 180, + "AngleDistance": 80, + "DamageEffects": { + "Knockback": { + "Direction": { + "X": 0, + "Y": 1, + "Z": -2 + }, + "Type": "Force", + "Force": 5, + "VelocityType": "Set", + "VelocityConfig": { + "AirResistance": 0.99, + "AirResistanceMax": 0.98, + "GroundResistance": 0.94, + "GroundResistanceMax": 0.3, + "Threshold": 3.0, + "Style": "Linear" + } + }, + "WorldParticles": [ + { + "SystemId": "Impact_Critical", + "Scale": 1, + "RotationOffset": { + "Yaw": 180, + "Pitch": 0, + "Roll": 0 + } + } + ], + "LocalSoundEventId": "SFX_Daggers_T2_Stab_Impact", + "WorldSoundEventId": "SFX_Daggers_T2_Stab_Impact" + }, + "DamageCalculator": { + "BaseDamage": { + "Physical": 114 + } + } + } + ] + } + ] + }, + "Razorstrike_Slash_Damage": { + "Interactions": [ + { + "Parent": "Weapon_Daggers_Signature_Razorstrike_Slash_Damage", + "DamageCalculator": { + "BaseDamage": { + "Physical": 49 + } + }, + "EntityStatsOnHit": [], + "DamageEffects": { + "WorldSoundEventId": "SFX_Daggers_T2_Slash_Impact", + "LocalSoundEventId": "SFX_Daggers_T2_Slash_Impact" + }, + "AngledDamage": [ + { + "Angle": 180, + "AngleDistance": 80, + "DamageEffects": { + "Knockback": { + "Direction": { + "X": 0, + "Y": 1, + "Z": -2 + }, + "Type": "Force", + "Force": 7, + "VelocityType": "Add", + "VelocityConfig": { + "AirResistance": 0.99, + "AirResistanceMax": 0.98, + "GroundResistance": 0.94, + "GroundResistanceMax": 0.3, + "Threshold": 3.0, + "Style": "Linear" + } + }, + "WorldParticles": [ + { + "SystemId": "Impact_Critical", + "Scale": 1, + "RotationOffset": { + "Yaw": 180, + "Pitch": 0, + "Roll": 0 + } + } + ], + "LocalSoundEventId": "SFX_Daggers_T2_Slash_Impact", + "WorldSoundEventId": "SFX_Daggers_T2_Slash_Impact" + }, + "DamageCalculator": { + "BaseDamage": { + "Physical": 74 + } + } + } + ] + } + ] + }, + "Razorstrike_Sweep_Damage": { + "Interactions": [ + { + "Parent": "Weapon_Daggers_Signature_Razorstrike_Sweep_Damage", + "DamageCalculator": { + "BaseDamage": { + "Physical": 61 + } + }, + "EntityStatsOnHit": [], + "DamageEffects": { + "WorldSoundEventId": "SFX_Daggers_T2_Slash_Impact", + "LocalSoundEventId": "SFX_Daggers_T2_Slash_Impact" + }, + "AngledDamage": [ + { + "Angle": 180, + "AngleDistance": 80, + "DamageEffects": { + "Knockback": { + "Direction": { + "X": 0, + "Y": 1, + "Z": -1 + }, + "Type": "Force", + "Force": 7, + "VelocityType": "Add", + "VelocityConfig": { + "AirResistance": 0.99, + "AirResistanceMax": 0.98, + "GroundResistance": 0.94, + "GroundResistanceMax": 0.3, + "Threshold": 3.0, + "Style": "Linear" + } + }, + "WorldParticles": [ + { + "SystemId": "Impact_Critical", + "Scale": 1, + "RotationOffset": { + "Yaw": 180, + "Pitch": 0, + "Roll": 0 + } + } + ], + "LocalSoundEventId": "SFX_Daggers_T2_Slash_Impact", + "WorldSoundEventId": "SFX_Daggers_T2_Slash_Impact" + }, + "DamageCalculator": { + "BaseDamage": { + "Physical": 92 + } + } + } + ] + } + ] + }, + "Razorstrike_Lunge_Damage": { + "Interactions": [ + { + "Parent": "Weapon_Daggers_Signature_Razorstrike_Lunge_Damage", + "DamageCalculator": { + "BaseDamage": { + "Physical": 73 + } + }, + "EntityStatsOnHit": [], + "DamageEffects": { + "WorldSoundEventId": "SFX_Daggers_T2_Slash_Impact", + "LocalSoundEventId": "SFX_Daggers_T2_Slash_Impact" + }, + "AngledDamage": [ + { + "Angle": 180, + "AngleDistance": 80, + "DamageEffects": { + "Knockback": { + "Direction": { + "X": 0, + "Y": 2, + "Z": -3 + }, + "Type": "Force", + "Force": 12, + "VelocityType": "Set", + "VelocityConfig": { + "AirResistance": 0.99, + "AirResistanceMax": 0.98, + "GroundResistance": 0.94, + "GroundResistanceMax": 0.3, + "Threshold": 3.0, + "Style": "Linear" + } + }, + "WorldParticles": [ + { + "SystemId": "Impact_Critical", + "Scale": 1, + "RotationOffset": { + "Yaw": 180, + "Pitch": 0, + "Roll": 0 + } + } + ], + "LocalSoundEventId": "SFX_Daggers_T2_Slash_Impact", + "WorldSoundEventId": "SFX_Daggers_T2_Slash_Impact" + }, + "DamageCalculator": { + "BaseDamage": { + "Physical": 110 + } + } + } + ] + } + ] + }, + "Guard_Wield": { + "Interactions": [ + { + "Parent": "Weapon_Daggers_Secondary_Guard_Wield", + "StaminaCost": { + "Value": 20, + "CostType": "Damage" + } + } + ] + } + }, + "MaxDurability": 220, + "DurabilityLossOnHit": 0.1, "Interactions": { "Ability1": { "Interactions": [ - { "Type": "mmorpg_cast_ability", "Slot": 1 } + { + "Type": "mmorpg_cast_ability", + "Slot": 1 + } ] }, "Ability2": { "Interactions": [ - { "Type": "mmorpg_cast_ability", "Slot": 2 } + { + "Type": "mmorpg_cast_ability", + "Slot": 2 + } ] }, "Ability3": { "Interactions": [ - { "Type": "mmorpg_cast_ability", "Slot": 3 } + { + "Type": "mmorpg_cast_ability", + "Slot": 3 + } ] } } diff --git a/src/main/resources/Server/Item/Items/Weapon_Longsword_Mithril.json b/src/main/resources/Server/Item/Items/Weapon_Longsword_Mithril.json index 2fe9e6a..255dcec 100644 --- a/src/main/resources/Server/Item/Items/Weapon_Longsword_Mithril.json +++ b/src/main/resources/Server/Item/Items/Weapon_Longsword_Mithril.json @@ -1,20 +1,167 @@ { - "Id": "Weapon_Longsword_Mithril", + "TranslationProperties": { + "Name": "server.items.Weapon_Longsword_Mithril.name" + }, + "Categories": [ + "Items.Weapons" + ], + "Quality": "Epic", + "ItemLevel": 50, + "Model": "Items/Weapons/Longsword/Mithril.blockymodel", + "Texture": "Items/Weapons/Longsword/Mithril_Texture.png", + "PlayerAnimationsId": "Longsword", "Interactions": { + "Primary": "Longsword_Attack", "Ability1": { "Interactions": [ - { "Type": "mmorpg_cast_ability", "Slot": 1 } + { + "Type": "mmorpg_cast_ability", + "Slot": 1 + } ] }, "Ability2": { "Interactions": [ - { "Type": "mmorpg_cast_ability", "Slot": 2 } + { + "Type": "mmorpg_cast_ability", + "Slot": 2 + } ] }, "Ability3": { "Interactions": [ - { "Type": "mmorpg_cast_ability", "Slot": 3 } + { + "Type": "mmorpg_cast_ability", + "Slot": 3 + } ] } - } + }, + "InteractionVars": { + "Longsword_Swing_Left_Effect": { + "Interactions": [ + { + "Parent": "Longsword_Swing_Left_Effect", + "Effects": { + "WorldSoundEventId": "SFX_Longsword_Special_Swing" + } + } + ] + }, + "Longsword_Swing_Left_Damage": { + "Interactions": [ + { + "Parent": "Longsword_Swing_Left_Damage", + "DamageCalculator": { + "Type": "Absolute", + "BaseDamage": { + "Physical": 70 + }, + "RandomPercentageModifier": 0.15 + }, + "DamageEffects": { + "WorldSoundEventId": "SFX_Longsword_Special_Impact" + } + } + ] + }, + "Longsword_Swing_Right_Effect": { + "Interactions": [ + { + "Parent": "Longsword_Swing_Left_Effect", + "Effects": { + "WorldSoundEventId": "SFX_Longsword_Special_Swing" + } + } + ] + }, + "Longsword_Swing_Right_Damage": { + "Interactions": [ + { + "Parent": "Longsword_Swing_Right_Damage", + "DamageCalculator": { + "Type": "Absolute", + "BaseDamage": { + "Physical": 70 + }, + "RandomPercentageModifier": 0.15 + }, + "DamageEffects": { + "WorldSoundEventId": "SFX_Longsword_Special_Impact" + } + } + ] + }, + "Longsword_Swing_Up_Left_Effect": { + "Interactions": [ + { + "Parent": "Longsword_Swing_Left_Effect", + "Effects": { + "WorldSoundEventId": "SFX_Longsword_Special_Swing" + } + } + ] + }, + "Longsword_Swing_Up_Left_Damage": { + "Interactions": [ + { + "Parent": "Longsword_Swing_Up_Left_Damage", + "DamageCalculator": { + "Type": "Absolute", + "BaseDamage": { + "Physical": 70 + }, + "RandomPercentageModifier": 0.15 + }, + "DamageEffects": { + "WorldSoundEventId": "SFX_Longsword_Special_Impact" + } + } + ] + }, + "Longsword_Stab_Charged_Effect": "Longsword_Stab_Charged_Effect", + "Longsword_Stab_Charged_Damage": { + "Interactions": [ + { + "Parent": "Longsword_Stab_Charged_Damage", + "DamageCalculator": { + "BaseDamage": { + "Physical": 210 + }, + "Type": "Absolute", + "RandomPercentageModifier": 0.15 + }, + "DamageEffects": { + "WorldSoundEventId": "SFX_Longsword_Special_Impact" + } + } + ] + } + }, + "DroppedItemAnimation": "Items/Animations/Dropped/Dropped_Diagonal_Left.blockyanim", + "IconProperties": { + "Scale": 0.31, + "Translation": [ + -47, + -47 + ], + "Rotation": [ + 45, + 90, + 0 + ] + }, + "Icon": "Icons/ItemsGenerated/Weapon_Longsword_Mithril.png", + "Tags": { + "Type": [ + "Weapon" + ], + "Family": [ + "Longsword" + ] + }, + "MaxDurability": 160, + "DurabilityLossOnHit": 0.52, + "Weapon": {}, + "ItemSoundSetId": "ISS_Weapons_Blade_Large" } diff --git a/src/main/resources/Server/Item/Items/Weapon_Shortbow_Cobalt.json b/src/main/resources/Server/Item/Items/Weapon_Shortbow_Cobalt.json index 1021f6c..c16e48d 100644 --- a/src/main/resources/Server/Item/Items/Weapon_Shortbow_Cobalt.json +++ b/src/main/resources/Server/Item/Items/Weapon_Shortbow_Cobalt.json @@ -1,19 +1,313 @@ { - "Id": "Weapon_Shortbow_Cobalt", + "TranslationProperties": { + "Name": "server.items.Weapon_Shortbow_Cobalt.name" + }, + "Parent": "Template_Weapon_Shortbow", + "Quality": "Rare", + "Model": "Items/Weapons/Bow/Cobalt.blockymodel", + "Texture": "Items/Weapons/Bow/Cobalt_Texture.png", + "Icon": "Icons/ItemsGenerated/Weapon_Bow_Cobalt.png", + "ItemLevel": 35, + "Recipe": { + "TimeSeconds": 4, + "KnowledgeRequired": false, + "Input": [ + { + "ItemId": "Ingredient_Bar_Cobalt", + "Quantity": 10 + }, + { + "ItemId": "Ingredient_Leather_Heavy", + "Quantity": 2 + }, + { + "ItemId": "Ingredient_Fabric_Scrap_Shadoweave", + "Quantity": 3 + } + ], + "BenchRequirement": [ + { + "Type": "Crafting", + "Categories": [ + "Weapon_Bow" + ], + "Id": "Weapon_Bench", + "RequiredTierLevel": 2 + }, + { + "Type": "DiagramCrafting", + "Categories": [ + "Weapons.Bow" + ], + "Id": "Armory" + } + ] + }, + "InteractionVars": { + "Primary_Shoot_Damage_Strength_0": { + "Interactions": [ + { + "Parent": "Weapon_Shortbow_Primary_Shoot_Damage_Strength_0", + "DamageCalculator": { + "BaseDamage": { + "Projectile": 7 + } + } + } + ] + }, + "Primary_Shoot_Damage_Strength_1": { + "Interactions": [ + { + "Parent": "Weapon_Shortbow_Primary_Shoot_Damage_Strength_1", + "DamageCalculator": { + "BaseDamage": { + "Projectile": 11 + } + } + } + ] + }, + "Primary_Shoot_Damage_Strength_2": { + "Interactions": [ + { + "Parent": "Weapon_Shortbow_Primary_Shoot_Damage_Strength_2", + "DamageCalculator": { + "BaseDamage": { + "Projectile": 15 + } + } + } + ] + }, + "Primary_Shoot_Damage_Strength_3": { + "Interactions": [ + { + "Parent": "Weapon_Shortbow_Primary_Shoot_Damage_Strength_3", + "DamageCalculator": { + "BaseDamage": { + "Projectile": 20 + } + } + } + ] + }, + "Primary_Shoot_Damage_Strength_4": { + "Interactions": [ + { + "Parent": "Weapon_Shortbow_Primary_Shoot_Damage_Strength_4", + "DamageCalculator": { + "BaseDamage": { + "Projectile": 23 + } + }, + "TargetedDamage": { + "Head": { + "DamageCalculator": { + "BaseDamage": { + "Projectile": 30 + } + }, + "DamageEffects": { + "Knockback": { + "Type": "Force", + "VelocityConfig": { + "AirResistance": 0.99, + "AirResistanceMax": 0.98, + "GroundResistance": 0.94, + "GroundResistanceMax": 0.3, + "Threshold": 3.0, + "Style": "Linear" + }, + "Direction": { + "X": 0.0, + "Y": 0.5, + "Z": -1 + }, + "Force": 8, + "VelocityType": "Set" + }, + "WorldParticles": [ + { + "SystemId": "Impact_Critical", + "Scale": 1, + "RotationOffset": { + "Yaw": 180, + "Pitch": 0, + "Roll": 0 + } + } + ] + } + } + } + } + ] + }, + "Signature_Volley_Damage": { + "Interactions": [ + { + "Parent": "Weapon_Shortbow_Signature_Volley_Damage", + "DamageCalculator": { + "BaseDamage": { + "Projectile": 17 + } + } + } + ] + }, + "Guard_Wield": { + "Interactions": [ + { + "Parent": "Weapon_Shortbow_Secondary_Guard_Wield", + "StaminaCost": { + "Value": 12.22, + "CostType": "Damage" + } + } + ] + } + }, + "ItemAppearanceConditions": { + "SignatureCharges": [ + { + "Condition": [ + 1, + 100 + ], + "ConditionValueType": "Percent", + "Model": "Items/Weapons/Bow/Cobalt_Triple.blockymodel", + "Texture": "Items/Weapons/Bow/Cobalt_Texture.png", + "LocalSoundEventId": "SFX_Bow_T2_Signature_Loop_Local", + "WorldSoundEventId": "SFX_Bow_T2_Signature_Loop", + "Particles": [ + { + "SystemId": "Bow_Signature_Charge", + "TargetNodeName": "ARROW-PLACEHOLDER", + "PositionOffset": { + "X": 0.5, + "Y": 0.25, + "Z": 0 + }, + "Scale": 0.5, + "TargetEntityPart": "PrimaryItem" + }, + { + "SystemId": "Bow_Signature_Charge", + "TargetNodeName": "ARROW-PLACEHOLDER", + "PositionOffset": { + "X": 0.5, + "Y": 0, + "Z": 0 + }, + "Scale": 0.5, + "TargetEntityPart": "PrimaryItem" + }, + { + "SystemId": "Bow_Signature_Charge", + "TargetNodeName": "ARROW-PLACEHOLDER", + "PositionOffset": { + "X": 0.5, + "Y": -0.25, + "Z": 0 + }, + "Scale": 0.5, + "TargetEntityPart": "PrimaryItem" + } + ], + "ModelVFXId": "Bow_Signature_Status" + } + ], + "SignatureEnergy": [ + { + "Condition": [ + 100, + 100 + ], + "ConditionValueType": "Percent", + "Particles": [ + { + "SystemId": "Sword_Signature_Ready", + "TargetNodeName": "Handle", + "PositionOffset": { + "X": 0.8 + }, + "TargetEntityPart": "PrimaryItem" + }, + { + "SystemId": "Sword_Signature_Status_Spawn", + "TargetEntityPart": "PrimaryItem" + }, + { + "SystemId": "Bow_Signature_Status", + "TargetNodeName": "Bow-Top", + "PositionOffset": { + "X": 0.55 + }, + "TargetEntityPart": "PrimaryItem", + "Scale": 1.3 + }, + { + "SystemId": "Bow_Signature_Status", + "TargetNodeName": "Bow-Bot", + "TargetEntityPart": "PrimaryItem", + "Scale": 1.3 + } + ], + "FirstPersonParticles": [ + { + "SystemId": "Sword_Signature_Ready", + "TargetNodeName": "Handle", + "PositionOffset": { + "X": 0.8 + }, + "TargetEntityPart": "PrimaryItem" + }, + { + "SystemId": "Sword_Signature_Status_Spawn", + "TargetNodeName": "Handle", + "TargetEntityPart": "PrimaryItem", + "PositionOffset": { + "Y": 0 + } + }, + { + "SystemId": "Bow_Signature_Status_FP", + "TargetNodeName": "Bow-Top", + "PositionOffset": { + "X": 0.5 + }, + "TargetEntityPart": "PrimaryItem" + } + ] + } + ] + }, + "MaxDurability": 150, + "DurabilityLossOnHit": 0.58, "Interactions": { "Ability1": { "Interactions": [ - { "Type": "mmorpg_cast_ability", "Slot": 1 } + { + "Type": "mmorpg_cast_ability", + "Slot": 1 + } ] }, "Ability2": { "Interactions": [ - { "Type": "mmorpg_cast_ability", "Slot": 2 } + { + "Type": "mmorpg_cast_ability", + "Slot": 2 + } ] }, "Ability3": { "Interactions": [ - { "Type": "mmorpg_cast_ability", "Slot": 3 } + { + "Type": "mmorpg_cast_ability", + "Slot": 3 + } ] } } diff --git a/src/main/resources/Server/Item/Items/Weapon_Staff_Mithril.json b/src/main/resources/Server/Item/Items/Weapon_Staff_Mithril.json index 42f0b42..f185406 100644 --- a/src/main/resources/Server/Item/Items/Weapon_Staff_Mithril.json +++ b/src/main/resources/Server/Item/Items/Weapon_Staff_Mithril.json @@ -1,20 +1,94 @@ { - "Id": "Weapon_Staff_Mithril", + "TranslationProperties": { + "Name": "server.items.Weapon_Staff_Mithril.name" + }, + "Categories": [ + "Items.Weapons" + ], + "Quality": "Epic", + "ItemLevel": 50, + "Model": "Items/Weapons/Staff/Mithril.blockymodel", + "Texture": "Items/Weapons/Staff/Mithril_Texture.png", + "PlayerAnimationsId": "Staff", "Interactions": { + "Primary": "Staff_Primary", + "Secondary": "Staff_Primary", "Ability1": { "Interactions": [ - { "Type": "mmorpg_cast_ability", "Slot": 1 } + { + "Type": "mmorpg_cast_ability", + "Slot": 1 + } ] }, "Ability2": { "Interactions": [ - { "Type": "mmorpg_cast_ability", "Slot": 2 } + { + "Type": "mmorpg_cast_ability", + "Slot": 2 + } ] }, "Ability3": { "Interactions": [ - { "Type": "mmorpg_cast_ability", "Slot": 3 } + { + "Type": "mmorpg_cast_ability", + "Slot": 3 + } ] } - } + }, + "InteractionVars": { + "Staff_Cast_Summon_Charged": { + "Interactions": [ + { + "Parent": "Staff_Cast_Summon_Charged", + "Costs": { + "Mana": 50 + } + } + ] + }, + "Staff_Cast_Summon_Cost": { + "Interactions": [ + { + "Parent": "Staff_Cast_Cost", + "StatModifiers": { + "Mana": -50 + } + } + ] + }, + "Staff_Cast_Summon_Launch": "Staff_Cast_Launch", + "Staff_Cast_Summon_Effect": "Staff_Cast_Effect", + "Staff_Cast_Summon_Fail": "Staff_Cast_Fail", + "Spear_Swing_Left_Damage": "Spear_Swing_Left_Damage", + "Spear_Swing_Right_Damage": "Spear_Swing_Right_Damage", + "Spear_Swing_Left_Effect": "Spear_Swing_Left_Effect", + "Spear_Swing_Right_Effect": "Spear_Swing_Right_Effect" + }, + "IconProperties": { + "Scale": 0.28, + "Translation": [ + -52, + -52 + ], + "Rotation": [ + 22.5, + 45, + 315 + ] + }, + "DroppedItemAnimation": "Items/Animations/Dropped/Dropped_Diagonal_Left.blockyanim", + "Icon": "Icons/ItemsGenerated/Weapon_Staff_Mithril.png", + "Tags": { + "Type": [ + "Weapon" + ], + "Family": [ + "Staff" + ] + }, + "Weapon": {}, + "ItemSoundSetId": "ISS_Weapons_Wood" } diff --git a/src/main/resources/manifest.json b/src/main/resources/manifest.json index 000e7bb..d096676 100644 --- a/src/main/resources/manifest.json +++ b/src/main/resources/manifest.json @@ -1,7 +1,7 @@ { "Group": "com.disklexar", "Name": "MMORPG", - "Version": "0.2.7", + "Version": "0.3.5", "Description": "Serveur MMORPG Hytale — progression, persistance et systèmes sociaux", "Authors": [ {