inventory update
Build / build (push) Has been cancelled

This commit is contained in:
gpatruno
2026-06-05 20:33:23 +02:00
parent 7e81dccb0e
commit 02d2af0993
28 changed files with 3281 additions and 91 deletions
+12 -2
View File
@@ -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 <choisir\|quitter\|info> [classe]` | Gérer votre classe |
| `/mmorpg power <recevoir\|retirer\|info> [pouvoir]` | Gérer vos pouvoirs |
| `/mmorpg job <rejoindre\|quitter\|info> [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).
+52 -23
View File
@@ -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)
@@ -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<EntityStore> 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<EntityStore> 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 <classe>.";
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(
@@ -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<EntityStore> store,
@Nonnull Ref<EntityStore> 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()));
}
});
}
}
@@ -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)));
}
@@ -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));
@@ -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 <choisir|quitter|info> [classe]
/mmorpg power <recevoir|retirer|info> [pouvoir]
@@ -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<EntityStore> entityRef = playerRef.getReference();
if (entityRef == null || !entityRef.isValid()) {
playerRef.sendMessage(Message.raw(
"[Capacité " + slot + "] Impossible d'utiliser la capacité pour le moment."));
return;
}
Store<EntityStore> 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));
}
}
@@ -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);
@@ -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<EntityStore> 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<EntityStore> 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;
}
}
@@ -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());
@@ -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] + "]";
}
}
@@ -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<PlayerInventoryPage.UiEvent> {
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<EntityStore> ref,
@Nonnull UICommandBuilder ui,
@Nonnull UIEventBuilder events,
@Nonnull Store<EntityStore> 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<EntityStore> ref,
@Nonnull Store<EntityStore> 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<UiEvent> CODEC =
BuilderCodec.builder(UiEvent.class, UiEvent::new).build();
private UiEvent() {
}
}
}
@@ -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<EntityStore> store, @Nonnull Ref<EntityStore> 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<EntityStore> store,
@Nonnull Ref<EntityStore> 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<EntityStore> store,
@Nonnull Ref<EntityStore> 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<EntityStore> store,
@Nonnull Ref<EntityStore> 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) {
}
}
@@ -0,0 +1,372 @@
package com.disklexar.mmorpg.ui;
import com.disklexar.mmorpg.combat.AbilityService;
import com.disklexar.mmorpg.player.model.PlayerProfile;
import com.disklexar.mmorpg.progression.ProgressionService;
import com.disklexar.mmorpg.rpg.clazz.AbilityDefinition;
import com.disklexar.mmorpg.rpg.clazz.ClassCatalog;
import com.disklexar.mmorpg.rpg.clazz.PlayerClass;
import com.disklexar.mmorpg.rpg.job.JobCatalog;
import com.disklexar.mmorpg.rpg.job.JobDefinition;
import com.disklexar.mmorpg.rpg.power.PowerCatalog;
import com.disklexar.mmorpg.rpg.power.PowerDefinition;
import com.disklexar.mmorpg.rpg.race.RaceCatalog;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.hypixel.hytale.codec.builder.BuilderCodec;
import com.hypixel.hytale.component.Ref;
import com.hypixel.hytale.component.Store;
import com.hypixel.hytale.protocol.packets.interface_.CustomPageLifetime;
import com.hypixel.hytale.protocol.packets.interface_.CustomUIEventBindingType;
import com.hypixel.hytale.server.core.entity.entities.Player;
import com.hypixel.hytale.server.core.entity.entities.player.pages.InteractiveCustomUIPage;
import com.hypixel.hytale.server.core.ui.builder.EventData;
import com.hypixel.hytale.server.core.ui.builder.UICommandBuilder;
import com.hypixel.hytale.server.core.ui.builder.UIEventBuilder;
import com.hypixel.hytale.server.core.universe.PlayerRef;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.StringJoiner;
import java.util.UUID;
/**
* MMORPG player menu with tabs: Personnage, Inventaire, Compétences.
* <p>
* Inventaire uses {@code MmorpgInventoryPage.ui} as the root document so {@code ItemGrid.InventorySectionId}
* binds correctly. Personnage / Compétences use {@code MmorpgPlayerShell.ui}.
*/
public final class PlayerMenuPage extends InteractiveCustomUIPage<PlayerMenuPage.UiEvent> {
private static final String SHELL = "Pages/MmorpgPlayerShell.ui";
private static final String CHARACTER_TAB = "Pages/MmorpgCharacterTab.ui";
private static final String SKILLS_TAB = "Pages/MmorpgSkillsTab.ui";
private static final DateTimeFormatter DATE_FORMAT =
DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm").withZone(ZoneId.systemDefault());
public enum Tab {
CHARACTER,
INVENTORY,
SKILLS
}
private final PlayerRef playerRef;
private final PlayerProfile profile;
private final ProgressionService progression;
private final AbilityService abilities;
private Tab activeTab;
public PlayerMenuPage(
@Nonnull PlayerRef playerRef,
@Nonnull PlayerProfile profile,
@Nullable ProgressionService progression,
@Nullable AbilityService abilities,
@Nonnull Tab initialTab) {
super(playerRef, CustomPageLifetime.CanDismiss, UiEvent.CODEC);
this.playerRef = playerRef;
this.profile = profile;
this.progression = progression;
this.abilities = abilities;
this.activeTab = initialTab;
}
@Override
public void build(
@Nonnull Ref<EntityStore> ref,
@Nonnull UICommandBuilder ui,
@Nonnull UIEventBuilder events,
@Nonnull Store<EntityStore> store) {
buildShellRoot(ref, ui, events, store);
}
@Override
public void handleDataEvent(
@Nonnull Ref<EntityStore> ref,
@Nonnull Store<EntityStore> store,
@Nonnull String raw) {
Tab tab = parseTab(raw);
if (tab != null && tab != activeTab) {
if (tab == Tab.INVENTORY) {
openInventoryPage(ref, store);
return;
}
activeTab = tab;
UICommandBuilder ui = new UICommandBuilder();
UIEventBuilder events = new UIEventBuilder();
bindTabButtons(events);
loadShellTab(ui, events, store, ref, activeTab);
sendUpdate(ui, events, false);
}
}
private void openInventoryPage(@Nonnull Ref<EntityStore> ref, @Nonnull Store<EntityStore> store) {
Player player = store.getComponent(ref, Player.getComponentType());
if (player != null) {
player.getPageManager().openCustomPage(ref, store, new PlayerInventoryPage(
playerRef,
profile,
progression,
abilities));
}
}
private void buildShellRoot(
@Nonnull Ref<EntityStore> ref,
@Nonnull UICommandBuilder ui,
@Nonnull UIEventBuilder events,
@Nonnull Store<EntityStore> store) {
ui.append(SHELL);
applyHeader(ui);
bindTabButtons(events);
loadShellTab(ui, events, store, ref, activeTab);
}
private void loadShellTab(
@Nonnull UICommandBuilder ui,
@Nonnull UIEventBuilder events,
@Nonnull Store<EntityStore> store,
@Nonnull Ref<EntityStore> ref,
@Nonnull Tab tab) {
applyTabHighlight(ui, tab);
ui.clear("#DynamicTabHost");
switch (tab) {
case CHARACTER -> {
ui.append("#DynamicTabHost", CHARACTER_TAB);
applyCharacterTab(ui, store, ref);
ui.set("#FooterHint.Text", escape("Échap pour fermer — onglet Personnage"));
}
case SKILLS -> {
ui.append("#DynamicTabHost", SKILLS_TAB);
applySkillsTab(ui);
ui.set("#FooterHint.Text", escape(
"Échap pour fermer — touches Ability 1/2/3 en jeu"));
}
case INVENTORY -> { }
}
ui.set("#HeaderSubtitle.Text", escape(tabSubtitle(tab)));
}
private void applyTabHighlight(@Nonnull UICommandBuilder ui, @Nonnull Tab tab) {
ui.set("#TabCharacterIndicator.Visible", tab == Tab.CHARACTER);
ui.set("#TabInventoryIndicator.Visible", tab == Tab.INVENTORY);
ui.set("#TabSkillsIndicator.Visible", tab == Tab.SKILLS);
ui.set("#TabCharacterWrap.Background", tab == Tab.CHARACTER ? "#3a3f4a" : "#00000000");
ui.set("#TabInventoryWrap.Background", tab == Tab.INVENTORY ? "#3a3f4a" : "#00000000");
ui.set("#TabSkillsWrap.Background", tab == Tab.SKILLS ? "#3a3f4a" : "#00000000");
}
private void applyHeader(@Nonnull UICommandBuilder ui) {
ui.set("#PlayerName.Text", escape(profile.getDisplayName()));
ui.set("#LevelLabel.Text", escape("Nv. " + profile.getLevel()));
ui.set("#MoneyLabel.Text", escape(profile.getMoney() + " pièces"));
}
private void applyCharacterTab(
@Nonnull UICommandBuilder ui,
@Nonnull Store<EntityStore> store,
@Nonnull Ref<EntityStore> ref) {
PlayerClass playerClass = ClassCatalog.byId(profile.getClassId());
String className = playerClass == null ? "Aucune" : playerClass.displayName();
ui.set("#IdentityName.Text", escape(profile.getDisplayName()));
ui.set("#RaceLabel.Text", escape("Race : " + RaceCatalog.byId(profile.getRaceId()).displayName()));
ui.set("#ClassLabel.Text", escape("Classe : " + className));
long xpMax = progression == null ? 100L : Math.max(1L, progression.xpForLevel(profile.getLevel()));
long xpCurrent = profile.getExperience();
float xpRatio = Math.min(1f, (float) xpCurrent / xpMax);
ui.set("#XpValue.Text", escape(xpCurrent + " / " + xpMax + " XP"));
ui.set("#XpBar.Value", xpRatio);
PlayerVitalsReader.Vitals vitals = PlayerVitalsReader.read(store, ref);
if (vitals != null) {
applyVitalBar(ui, "#HealthBar", "#HealthValue", "Vie", vitals.current(), vitals.max());
applyVitalBar(ui, "#StaminaBar", "#StaminaValue", "Endurance",
vitals.staminaCurrent(), vitals.staminaMax());
} else {
ui.set("#HealthValue.Text", escape("Vie : —"));
ui.set("#StaminaValue.Text", escape("Endurance : —"));
}
int level = profile.getLevel();
ui.set("#StatStr.Text", escape("FOR " + (10 + level)));
ui.set("#StatAgi.Text", escape("AGI " + (8 + level)));
ui.set("#StatInt.Text", escape("INT " + (6 + level)));
ui.set("#PowersList.Text", escape(formatPowers()));
ui.set("#JobsList.Text", escape(formatJobs()));
ui.set("#GroupLabel.Text", escape(
"Groupe : " + (profile.getGroupId() != null ? "Oui" : "Aucun")));
ui.set("#GuildLabel.Text", escape(
"Guilde : " + (profile.getGuildId() != null ? profile.getGuildId() : "Aucune")));
ui.set("#PlayTimeLabel.Text", escape("Temps de jeu : " + formatDuration(profile.getTotalTimePlay())));
ui.set("#LastLoginLabel.Text", escape(
"Dernière connexion : " + formatDate(profile.getLastDateConnected())));
ui.set("#CreatedLabel.Text", escape("Création : " + formatDate(profile.getDateCreation())));
}
private void applySkillsTab(@Nonnull UICommandBuilder ui) {
PlayerClass playerClass = ClassCatalog.byId(profile.getClassId());
if (playerClass == null) {
ui.set("#ClassTitle.Text", escape("Aucune classe"));
ui.set("#WeaponsLabel.Text", escape("Choisissez une classe : /mmorpg class choisir"));
ui.set("#PointsLabel.Text", escape("Points de compétence : —"));
for (int slot = 1; slot <= 3; slot++) {
ui.set("#Ability" + slot + "Key.Text", escape(AbilitySlotKeys.keyLabel(slot)));
ui.set("#Ability" + slot + "Name.Text", escape(""));
ui.set("#Ability" + slot + "Cooldown.Text", escape(""));
ui.set("#Ability" + slot + "Desc.Text", escape("Aucune capacité sans classe."));
}
return;
}
ui.set("#ClassTitle.Text", escape(playerClass.displayName()));
ui.set("#WeaponsLabel.Text", escape("Armes : " + String.join(", ", playerClass.weapons())));
ui.set("#PointsLabel.Text", escape("Points de compétence : 0 (arbre M1)"));
UUID playerId = profile.getUuid();
for (int slot = 1; slot <= 3; slot++) {
ui.set("#Ability" + slot + "Key.Text", escape(AbilitySlotKeys.keyLabel(slot)));
}
for (AbilityDefinition ability : playerClass.abilities()) {
int slot = ability.slot();
ui.set("#Ability" + slot + "Name.Text", escape(ability.displayName()));
ui.set("#Ability" + slot + "Cooldown.Text", escape(cooldownLabel(playerId, ability)));
ui.set("#Ability" + slot + "Desc.Text", escape(ability.description()));
}
}
private void bindTabButtons(@Nonnull UIEventBuilder events) {
events.addEventBinding(
CustomUIEventBindingType.Activating,
"#TabCharacter",
EventData.of("Tab", "character"),
false);
events.addEventBinding(
CustomUIEventBindingType.Activating,
"#TabInventory",
EventData.of("Tab", "inventory"),
false);
events.addEventBinding(
CustomUIEventBindingType.Activating,
"#TabSkills",
EventData.of("Tab", "skills"),
false);
}
private static void applyVitalBar(
@Nonnull UICommandBuilder ui,
@Nonnull String barSelector,
@Nonnull String valueSelector,
@Nonnull String label,
long current,
long max) {
float ratio = max <= 0 ? 0f : Math.min(1f, (float) current / max);
ui.set(barSelector + ".Value", ratio);
ui.set(valueSelector + ".Text", escape(label + " : " + current + " / " + max));
}
@Nonnull
private String cooldownLabel(@Nonnull UUID playerId, @Nonnull AbilityDefinition ability) {
if (abilities == null) {
return "Prêt";
}
long remaining = abilities.cooldownRemaining(playerId, ability.id());
if (remaining <= 0) {
return "Prêt";
}
return (remaining / 1000 + 1) + "s de recharge";
}
@Nonnull
private String formatPowers() {
if (profile.getPowers().isEmpty()) {
return "Aucun";
}
StringJoiner joiner = new StringJoiner("\n• ", "", "");
for (String id : profile.getPowers()) {
PowerDefinition power = PowerCatalog.byId(id);
joiner.add(power != null ? power.displayName() : id);
}
return joiner.toString();
}
@Nonnull
private String formatJobs() {
if (profile.getJobs().isEmpty()) {
return "Aucun";
}
StringJoiner joiner = new StringJoiner("\n• ", "", "");
for (String id : profile.getJobs()) {
JobDefinition job = JobCatalog.byId(id);
joiner.add(job != null ? job.displayName() : id);
}
return joiner.toString();
}
@Nonnull
private static String tabSubtitle(@Nonnull Tab tab) {
return switch (tab) {
case CHARACTER -> "Personnage";
case INVENTORY -> "Inventaire";
case SKILLS -> "Compétences";
};
}
@Nullable
private static Tab parseTab(@Nonnull String raw) {
try {
JsonObject json = JsonParser.parseString(raw).getAsJsonObject();
if (!json.has("Tab")) {
return null;
}
String value = json.get("Tab").getAsString().toLowerCase();
return switch (value) {
case "character", "personnage" -> Tab.CHARACTER;
case "inventory", "inventaire" -> Tab.INVENTORY;
case "skills", "competences", "compétences" -> Tab.SKILLS;
default -> null;
};
} catch (RuntimeException ignored) {
return null;
}
}
@Nonnull
private static String formatDuration(long millis) {
if (millis <= 0) {
return "0 min";
}
Duration duration = Duration.ofMillis(millis);
long hours = duration.toHours();
long minutes = duration.toMinutesPart();
return hours > 0 ? hours + "h " + minutes + "min" : minutes + "min";
}
@Nonnull
private static String formatDate(long epochMillis) {
if (epochMillis <= 0) {
return "";
}
return DATE_FORMAT.format(Instant.ofEpochMilli(epochMillis));
}
@Nonnull
private static String escape(@Nonnull String text) {
return text.replace("\\", "\\\\").replace("\"", "\\\"");
}
public static final class UiEvent {
public static final BuilderCodec<UiEvent> CODEC =
BuilderCodec.builder(UiEvent.class, UiEvent::new).build();
private UiEvent() {
}
}
}
@@ -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);
}
@@ -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 : —";
}
}
}
@@ -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);
}
}
}
@@ -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);
}
}
}
}
@@ -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);
}
}
}
@@ -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.";
}
}
@@ -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
}
]
}
}
@@ -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
}
]
}
}
@@ -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
}
]
}
}
@@ -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"
}
@@ -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
}
]
}
}
@@ -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"
}
+1 -1
View File
@@ -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": [
{