diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..1fc0385 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,30 @@ +name: Build Action + +on: + push: + branches: [ main, develop ] + pull_request: + types: [ opened, synchronize, reopened ] + workflow_dispatch: + +permissions: + contents: read + packages: write + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + name: Build and Publish zCrates + uses: GroupeZ-dev/actions/.github/workflows/build.yml@main + with: + project-name: "zCrates" + publish: true + project-to-publish: "api:publish" + discord-avatar-url: "https://groupez.dev/storage/images/325.png" + secrets: + WEBHOOK_URL: ${{ secrets.WEBHOOK_URL }} + MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }} + MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }} \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 7bc07ec..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,10 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Editor-based HTTP Client requests -/httpRequests/ -# Environment-dependent path to Maven home directory -/mavenHomeManager.xml -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml diff --git a/.idea/copilot.data.migration.agent.xml b/.idea/copilot.data.migration.agent.xml deleted file mode 100644 index 4ea72a9..0000000 --- a/.idea/copilot.data.migration.agent.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.idea/copilot.data.migration.ask.xml b/.idea/copilot.data.migration.ask.xml deleted file mode 100644 index 7ef04e2..0000000 --- a/.idea/copilot.data.migration.ask.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.idea/copilot.data.migration.ask2agent.xml b/.idea/copilot.data.migration.ask2agent.xml deleted file mode 100644 index 1f2ea11..0000000 --- a/.idea/copilot.data.migration.ask2agent.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.idea/copilot.data.migration.edit.xml b/.idea/copilot.data.migration.edit.xml deleted file mode 100644 index 8648f94..0000000 --- a/.idea/copilot.data.migration.edit.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.idea/discord.xml b/.idea/discord.xml deleted file mode 100644 index 104c42f..0000000 --- a/.idea/discord.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml deleted file mode 100644 index 8627761..0000000 --- a/.idea/gradle.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/material_theme_project_new.xml b/.idea/material_theme_project_new.xml deleted file mode 100644 index a8bdd19..0000000 --- a/.idea/material_theme_project_new.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index f16dea7..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 94a25f7..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..9e3403f --- /dev/null +++ b/README.md @@ -0,0 +1,264 @@ +# zCrates + +A modern, feature-rich crate/loot box plugin for Minecraft Paper servers with JavaScript-powered animations and algorithms. + +## Features + +- **JavaScript Animations** - Create custom animations using ES6 JavaScript +- **Custom Algorithms** - Define reward selection logic with JavaScript +- **Multiple Key Types** - Virtual (database) and physical (item) keys +- **Flexible Rewards** - Items, commands, or multiple of each +- **Opening Conditions** - Permissions, cooldowns, and custom conditions +- **Reroll System** - Allow players to reroll for a different reward +- **Multi-Display Support** - Blocks, entities, MythicMobs, ItemsAdder, Nexo, Oraxen +- **PlaceholderAPI** - Built-in placeholders for keys and statistics +- **Database Support** - SQLite, MySQL, MariaDB + +## Requirements + +- **Minecraft**: 1.21+ +- **Server**: Paper/Purpur +- **Java**: 21+ +- **Dependencies**: zMenu (bundled) + +## Installation + +1. Download `zCrates.jar` +2. Place in `plugins/` folder +3. Restart server +4. Configure crates in `plugins/zCrates/crates/` + +## Quick Start + +### Creating a Crate + +Create `plugins/zCrates/crates/example.yml`: + +```yaml +id: example +animation: roulette +algorithm: weighted +display-name: "Example Crate" +max-rerolls: 1 + +key: + type: VIRTUAL + name: "example-key" + +related-menu: crate-menu-example + +rewards: + - type: ITEM + id: diamond-reward + weight: 10.0 + display-item: + material: DIAMOND + name: "Diamonds" + item: + material: DIAMOND + amount: 5 + + - type: COMMAND + id: money-reward + weight: 20.0 + display-item: + material: GOLD_INGOT + name: "$1000" + command: "eco give %player% 1000" +``` + +### Placing a Crate + +```bash +/zcrates place example BLOCK CHEST +``` + +### Giving Keys + +```bash +/zcrates give example 5 +``` + +## Commands + +| Command | Description | Permission | +|-------------------------------------------|--------------------------|-------------------------| +| `/zcrates reload` | Reload configurations | `crates.command.reload` | +| `/zcrates open [force]` | Open a crate | `crates.command.open` | +| `/zcrates give ` | Give keys | `crates.command.give` | +| `/zcrates place [value]` | Place a crate | `crates.command.place` | +| `/zcrates remove` | Remove placed crate | `crates.command.remove` | +| `/zcrates purge` | Remove all placed crates | `crates.command.purge` | + +## Key Types + +### Virtual Key +Stored in database, no physical item. +```yaml +key: + type: VIRTUAL + name: "my-key" +``` + +### Physical Key +Item in player inventory. +```yaml +key: + type: PHYSIC + name: "my-key" + item: + material: TRIPWIRE_HOOK + name: "Legendary Key" + glow: true +``` + +## Reward Types + +| Type | Description | +|------------|-----------------------| +| `ITEM` | Single item reward | +| `ITEMS` | Multiple items reward | +| `COMMAND` | Single command | +| `COMMANDS` | Multiple commands | + +## Display Types + +| Type | Description | +|---------------|-----------------------------------| +| `BLOCK` | Placed block (CHEST, ENDER_CHEST) | +| `ENTITY` | Spawned entity (ARMOR_STAND) | +| `MYTHIC_MOB` | MythicMobs entity | +| `ITEMS_ADDER` | ItemsAdder furniture | +| `NEXO` | Nexo furniture | +| `ORAXEN` | Oraxen furniture | + +## JavaScript API + +### Custom Animation + +Create `plugins/zCrates/animations/my-animation.js`: + +```javascript +animations.register("my-animation", { + phases: [ + { + name: "spin", + duration: 3000, + interval: 2, + speedCurve: "EASE_OUT", + onTick: function(context, tickData) { + context.inventory().randomizeSlots(); + } + } + ], + onComplete: function(context) { + context.inventory().close(60); + } +}); +``` + +### Custom Algorithm + +Create `plugins/zCrates/algorithms/my-algorithm.js`: + +```javascript +algorithms.register("my-algorithm", function(context) { + var rewards = context.rewards(); + var history = context.history(); + + // Custom selection logic + return rewards.weightedRandom(); +}); +``` + +## Opening Conditions + +```yaml +conditions: + - type: PERMISSION + permission: "zcrates.open.vip" + - type: COOLDOWN + cooldown: 3600000 # 1 hour in ms +``` + +## PlaceholderAPI + +Numbers are formatted in compact notation (1.2K, 3.5M, etc.). Use `-raw` suffix for raw numbers. + +| Placeholder | Description | Example | +|--------------------------------|---------------------------------------|-----------| +| `%zcrates__keys%` | Key count (formatted) | `1.2K` | +| `%zcrates__keys-raw%` | Key count (raw) | `1234` | +| `%zcrates__opened%` | Crate openings (formatted) | `3.5M` | +| `%zcrates__opened-raw%` | Crate openings (raw) | `3500000` | +| `%zcrates_crates_opened%` | Total openings all crates (formatted) | `15K` | +| `%zcrates_crates_opened_raw%` | Total openings all crates (raw) | `15000` | + +**Examples:** +``` +%zcrates_legendary_keys% → 5 +%zcrates_legendary_opened% → 1.2K +%zcrates_crates_opened% → 3.5M +``` + +## API Usage + +### Maven Dependency + +```xml + + fr.traqueur + zcrates-api + 1.0.0 + provided + +``` + +### Accessing the API + +```java +CratesPlugin plugin = (CratesPlugin) Bukkit.getPluginManager().getPlugin("zCrates"); +CratesManager cratesManager = plugin.getManager(CratesManager.class); + +// Open a crate +Crate crate = Registry.get(CratesRegistry.class).getById("example"); +OpenResult result = cratesManager.tryOpenCrate(player, crate); + +// Give keys +UsersManager usersManager = plugin.getManager(UsersManager.class); +User user = usersManager.getUser(player.getUniqueId()); +user.addKeys("example-key", 5); +``` + +### Listening to Events + +```java +@EventHandler +public void onRewardGiven(RewardGivenEvent event) { + Player player = event.getPlayer(); + Reward reward = event.getReward(); + // Log, broadcast, etc. +} +``` + +## Documentation + +- **[User Guide](docs/USER_GUIDE.md)** - Complete configuration guide +- **[API Reference](docs/API_REFERENCE.md)** - Developer documentation + +## Building + +```bash +./gradlew build +``` + +Output: `target/zCrates.jar` + +## License + +Proprietary - All rights reserved. + +## Support + +- Discord: [Server Link] +- GitHub Issues: [Report bugs] \ No newline at end of file diff --git a/api/build.gradle.kts b/api/build.gradle.kts index 8650400..1e68b52 100644 --- a/api/build.gradle.kts +++ b/api/build.gradle.kts @@ -1,3 +1,7 @@ +plugins { + id("re.alwyn974.groupez.publish") version "1.0.0" +} + rootProject.extra.properties["sha"]?.let { sha -> version = sha } diff --git a/api/src/main/java/fr/traqueur/crates/api/CratesPlugin.java b/api/src/main/java/fr/traqueur/crates/api/CratesPlugin.java index 3eeb2b7..a61ef84 100644 --- a/api/src/main/java/fr/traqueur/crates/api/CratesPlugin.java +++ b/api/src/main/java/fr/traqueur/crates/api/CratesPlugin.java @@ -1,9 +1,25 @@ package fr.traqueur.crates.api; +import fr.maxlego08.menu.api.InventoryManager; import fr.traqueur.crates.api.managers.Manager; +import org.bukkit.event.Listener; import org.bukkit.plugin.ServicePriority; import org.bukkit.plugin.java.JavaPlugin; +/** + * Base class for zItems plugins, providing manager registration and retrieval. + * + *

This class extends {@link JavaPlugin} and offers methods to register + * and retrieve various manager implementations used within the zItems ecosystem. + * Managers are registered as services with Bukkit's {@code ServicesManager}, + * allowing for easy access by other plugins.

+ * + *

Plugins extending this class can leverage the provided methods to + * manage effects, items, and other functionalities through their respective managers.

+ * + * @see Manager + * @see org.bukkit.plugin.ServicesManager + */ public abstract class CratesPlugin extends JavaPlugin { /** @@ -67,4 +83,19 @@ public I getManager(Class clazz) { return rsp.getProvider(); } + /** + * Registers an event listener with the plugin's event system. + * + * @param listener the listener to register + */ + public void registerListener(Listener listener) { + this.getServer().getPluginManager().registerEvents(listener, this); + } + + /** + * Retrieves the InventoryManager associated with this plugin. + * + * @return the InventoryManager instance + */ + public abstract InventoryManager getInventoryManager(); } diff --git a/api/src/main/java/fr/traqueur/crates/api/annotations/AutoHook.java b/api/src/main/java/fr/traqueur/crates/api/annotations/AutoHook.java new file mode 100644 index 0000000..5214702 --- /dev/null +++ b/api/src/main/java/fr/traqueur/crates/api/annotations/AutoHook.java @@ -0,0 +1,24 @@ +package fr.traqueur.crates.api.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation used to mark an {@link fr.traqueur.crates.api.hooks.Hook} for automatic registration. + * When the specified plugin is present, the hook will be enabled automatically. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface AutoHook { + + /** + * The name of the plugin that this hook integrates with. + * This should match the exact plugin name as it appears in the plugin.yml. + * The hook will only be enabled if a plugin with this name is present. + * + * @return the plugin name + */ + String value(); +} \ No newline at end of file diff --git a/api/src/main/java/fr/traqueur/crates/api/events/CrateEvent.java b/api/src/main/java/fr/traqueur/crates/api/events/CrateEvent.java new file mode 100644 index 0000000..6d8a97f --- /dev/null +++ b/api/src/main/java/fr/traqueur/crates/api/events/CrateEvent.java @@ -0,0 +1,46 @@ +package fr.traqueur.crates.api.events; + +import fr.traqueur.crates.api.models.crates.Crate; +import org.bukkit.entity.Player; +import org.bukkit.event.player.PlayerEvent; + +/** + * Base class for all crate-related events. + */ +public abstract class CrateEvent extends PlayerEvent { + + /** The crate involved in this event. */ + protected final Crate crate; + + /** + * Constructs a CrateEvent with the specified player and crate. + * + * @param player the player involved in the event + * @param crate the crate involved in the event + */ + public CrateEvent(Player player, Crate crate) { + super(player); + this.crate = crate; + } + + /** + * Constructs a CrateEvent with the specified player, crate, and async flag. + * + * @param player the player involved in the event + * @param crate the crate involved in the event + * @param async whether the event is asynchronous + */ + public CrateEvent(Player player, Crate crate, boolean async) { + super(player, async); + this.crate = crate; + } + + /** + * Gets the crate involved in this event. + * + * @return the crate + */ + public Crate getCrate() { + return crate; + } +} \ No newline at end of file diff --git a/api/src/main/java/fr/traqueur/crates/api/events/CrateOpenEvent.java b/api/src/main/java/fr/traqueur/crates/api/events/CrateOpenEvent.java new file mode 100644 index 0000000..032a782 --- /dev/null +++ b/api/src/main/java/fr/traqueur/crates/api/events/CrateOpenEvent.java @@ -0,0 +1,59 @@ +package fr.traqueur.crates.api.events; + +import fr.traqueur.crates.api.models.animations.Animation; +import fr.traqueur.crates.api.models.crates.Crate; +import org.bukkit.entity.Player; +import org.bukkit.event.HandlerList; +import org.jetbrains.annotations.NotNull; + +/** + * Called when a player opens a crate (after key is consumed, menu is about to open). + * This event is not cancellable. + */ +public class CrateOpenEvent extends CrateEvent { + + /** The handler list for this event. */ + private static final HandlerList HANDLERS = new HandlerList(); + /** The animation that will be played when the crate is opened. */ + private final Animation animation; + + /** + * Constructs a CrateOpenEvent with the specified player, crate, and animation. + * + * @param player the player opening the crate + * @param crate the crate being opened + * @param animation the animation that will be played + */ + public CrateOpenEvent(Player player, Crate crate, Animation animation) { + super(player, crate); + this.animation = animation; + } + + /** + * Gets the animation that will be played. + * + * @return the animation + */ + public Animation getAnimation() { + return animation; + } + + /** + * Gets the handler list for this event. + * + * @return the handler list + */ + @Override + public @NotNull HandlerList getHandlers() { + return HANDLERS; + } + + /** + * Gets the static handler list for this event. + * + * @return the handler list + */ + public static HandlerList getHandlerList() { + return HANDLERS; + } +} \ No newline at end of file diff --git a/api/src/main/java/fr/traqueur/crates/api/events/CratePreOpenEvent.java b/api/src/main/java/fr/traqueur/crates/api/events/CratePreOpenEvent.java new file mode 100644 index 0000000..975672b --- /dev/null +++ b/api/src/main/java/fr/traqueur/crates/api/events/CratePreOpenEvent.java @@ -0,0 +1,54 @@ +package fr.traqueur.crates.api.events; + +import fr.traqueur.crates.api.models.crates.Crate; +import org.bukkit.entity.Player; +import org.bukkit.event.Cancellable; +import org.bukkit.event.HandlerList; +import org.jetbrains.annotations.NotNull; + +/** + * Called before a player opens a crate. + * This event is cancellable - if cancelled, the crate will not be opened + * and the key will not be consumed. + */ +public class CratePreOpenEvent extends CrateEvent implements Cancellable { + + /** The handler list for this event. */ + private static final HandlerList HANDLERS = new HandlerList(); + /** Indicates whether the event has been cancelled. */ + private boolean cancelled = false; + + /** + * Constructs a CratePreOpenEvent with the specified player and crate. + * + * @param player the player attempting to open the crate + * @param crate the crate being opened + */ + public CratePreOpenEvent(Player player, Crate crate) { + super(player, crate); + } + + @Override + public boolean isCancelled() { + return cancelled; + } + + @Override + public void setCancelled(boolean cancel) { + this.cancelled = cancel; + } + + @Override + public @NotNull HandlerList getHandlers() { + return HANDLERS; + } + + /** + * Gets the static handler list for this event. + * + * @return the handler list + */ + public static HandlerList getHandlerList() { + return HANDLERS; + } +} \ No newline at end of file diff --git a/api/src/main/java/fr/traqueur/crates/api/events/CrateRerollEvent.java b/api/src/main/java/fr/traqueur/crates/api/events/CrateRerollEvent.java new file mode 100644 index 0000000..e5aa85b --- /dev/null +++ b/api/src/main/java/fr/traqueur/crates/api/events/CrateRerollEvent.java @@ -0,0 +1,76 @@ +package fr.traqueur.crates.api.events; + +import fr.traqueur.crates.api.models.crates.Crate; +import fr.traqueur.crates.api.models.crates.Reward; +import org.bukkit.entity.Player; +import org.bukkit.event.Cancellable; +import org.bukkit.event.HandlerList; +import org.jetbrains.annotations.NotNull; + +/** + * Called when a player attempts to reroll their reward. + * This event is cancellable - if cancelled, the reroll will not occur. + */ +public class CrateRerollEvent extends CrateEvent implements Cancellable { + + private static final HandlerList HANDLERS = new HandlerList(); + private boolean cancelled = false; + private final Reward currentReward; + private final int rerollsRemaining; + + /** + * Constructs a CrateRerollEvent with the specified player, crate, current reward, and rerolls remaining. + * + * @param player the player attempting the reroll + * @param crate the crate being rerolled + * @param currentReward the current reward that will be replaced if reroll succeeds + * @param rerollsRemaining the number of rerolls remaining after this one + */ + public CrateRerollEvent(Player player, Crate crate, Reward currentReward, int rerollsRemaining) { + super(player, crate); + this.currentReward = currentReward; + this.rerollsRemaining = rerollsRemaining; + } + + /** + * Gets the current reward that will be replaced if reroll succeeds. + * + * @return the current reward + */ + public Reward getCurrentReward() { + return currentReward; + } + + /** + * Gets the number of rerolls remaining after this reroll (if it succeeds). + * + * @return rerolls remaining after this one + */ + public int getRerollsRemaining() { + return rerollsRemaining; + } + + @Override + public boolean isCancelled() { + return cancelled; + } + + @Override + public void setCancelled(boolean cancel) { + this.cancelled = cancel; + } + + @Override + public @NotNull HandlerList getHandlers() { + return HANDLERS; + } + + /** + * Gets the static handler list for this event. + * + * @return the handler list + */ + public static HandlerList getHandlerList() { + return HANDLERS; + } +} \ No newline at end of file diff --git a/api/src/main/java/fr/traqueur/crates/api/events/RewardGeneratedEvent.java b/api/src/main/java/fr/traqueur/crates/api/events/RewardGeneratedEvent.java new file mode 100644 index 0000000..95e860c --- /dev/null +++ b/api/src/main/java/fr/traqueur/crates/api/events/RewardGeneratedEvent.java @@ -0,0 +1,64 @@ +package fr.traqueur.crates.api.events; + +import fr.traqueur.crates.api.models.crates.Crate; +import fr.traqueur.crates.api.models.crates.Reward; +import org.bukkit.entity.Player; +import org.bukkit.event.HandlerList; +import org.jetbrains.annotations.NotNull; + +/** + * Called when a reward is generated for a player (after animation starts). + * This can be used to track reward distribution or modify logging. + */ +public class RewardGeneratedEvent extends CrateEvent { + + private static final HandlerList HANDLERS = new HandlerList(); + private final Reward reward; + private final boolean isReroll; + + /** + * Constructs a new RewardGeneratedEvent. + * + * @param player the player receiving the reward + * @param crate the crate from which the reward is generated + * @param reward the reward that was generated + * @param isReroll whether this reward was generated as a result of a reroll + */ + public RewardGeneratedEvent(Player player, Crate crate, Reward reward, boolean isReroll) { + super(player, crate); + this.reward = reward; + this.isReroll = isReroll; + } + + /** + * Gets the reward that was generated. + * + * @return the reward + */ + public Reward getReward() { + return reward; + } + + /** + * Whether this reward was generated as a result of a reroll. + * + * @return true if this is a reroll + */ + public boolean isReroll() { + return isReroll; + } + + @Override + public @NotNull HandlerList getHandlers() { + return HANDLERS; + } + + /** + * Gets the handler list for this event. + * + * @return the handler list + */ + public static HandlerList getHandlerList() { + return HANDLERS; + } +} \ No newline at end of file diff --git a/api/src/main/java/fr/traqueur/crates/api/events/RewardGivenEvent.java b/api/src/main/java/fr/traqueur/crates/api/events/RewardGivenEvent.java new file mode 100644 index 0000000..7c94b59 --- /dev/null +++ b/api/src/main/java/fr/traqueur/crates/api/events/RewardGivenEvent.java @@ -0,0 +1,52 @@ +package fr.traqueur.crates.api.events; + +import fr.traqueur.crates.api.models.crates.Crate; +import fr.traqueur.crates.api.models.crates.Reward; +import org.bukkit.entity.Player; +import org.bukkit.event.HandlerList; +import org.jetbrains.annotations.NotNull; + +/** + * Called when a reward is given to a player (when they close the inventory). + * This event fires after the reward has been given. + */ +public class RewardGivenEvent extends CrateEvent { + + private static final HandlerList HANDLERS = new HandlerList(); + private final Reward reward; + + /** + * Constructs a new RewardGivenEvent. + * + * @param player the player receiving the reward + * @param crate the crate from which the reward is given + * @param reward the reward that was given + */ + public RewardGivenEvent(Player player, Crate crate, Reward reward) { + super(player, crate); + this.reward = reward; + } + + /** + * Gets the reward that was given to the player. + * + * @return the reward + */ + public Reward getReward() { + return reward; + } + + @Override + public @NotNull HandlerList getHandlers() { + return HANDLERS; + } + + /** + * Gets the handler list for this event. + * + * @return the handler list + */ + public static HandlerList getHandlerList() { + return HANDLERS; + } +} \ No newline at end of file diff --git a/api/src/main/java/fr/traqueur/crates/api/hooks/Hook.java b/api/src/main/java/fr/traqueur/crates/api/hooks/Hook.java new file mode 100644 index 0000000..8f37532 --- /dev/null +++ b/api/src/main/java/fr/traqueur/crates/api/hooks/Hook.java @@ -0,0 +1,13 @@ +package fr.traqueur.crates.api.hooks; + +/** + * Interface representing a hook that can be enabled. + */ +public interface Hook { + + /** + * Method to be called when the hook is enabled. + */ + void onEnable(); + +} diff --git a/api/src/main/java/fr/traqueur/crates/api/managers/CratesManager.java b/api/src/main/java/fr/traqueur/crates/api/managers/CratesManager.java new file mode 100644 index 0000000..b71cddd --- /dev/null +++ b/api/src/main/java/fr/traqueur/crates/api/managers/CratesManager.java @@ -0,0 +1,263 @@ +package fr.traqueur.crates.api.managers; + +import fr.traqueur.crates.api.models.crates.Crate; +import fr.traqueur.crates.api.models.crates.OpenResult; +import fr.traqueur.crates.api.models.animations.Animation; +import fr.traqueur.crates.api.models.crates.Reward; +import fr.traqueur.crates.api.models.placedcrates.DisplayType; +import fr.traqueur.crates.api.models.placedcrates.PlacedCrate; +import org.bukkit.Chunk; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; +import org.bukkit.inventory.Inventory; + +import java.util.List; +import java.util.Optional; + +/** + * Manager responsible for all crate-related operations including opening crates, + * managing animations, handling rerolls, and managing placed crates in the world. + * + *

This is the main entry point for interacting with the crate system programmatically. + * Obtain an instance via {@code plugin.getManager(CratesManager.class)}.

+ * + * @see OpenResult + * @see Crate + * @see PlacedCrate + */ +public non-sealed interface CratesManager extends Manager { + + // ==================== Crate Opening ==================== + + /** + * Attempts to open a crate for a player with all validation checks. + * + *

This method performs the following checks in order:

+ *
    + *
  1. Verifies the player has the required key
  2. + *
  3. Checks all configured conditions (permissions, cooldowns, etc.)
  4. + *
  5. Fires {@link fr.traqueur.crates.api.events.CratePreOpenEvent} (cancellable)
  6. + *
  7. Consumes the key
  8. + *
  9. Calls {@code onOpen()} on all conditions
  10. + *
  11. Opens the crate menu
  12. + *
+ * + * @param player the player attempting to open the crate + * @param crate the crate to open + * @return an {@link OpenResult} indicating success or the reason for failure + */ + OpenResult tryOpenCrate(Player player, Crate crate); + + /** + * Force opens a crate for a player, bypassing key checks and conditions. + * + *

Use this method for administrative purposes or when you've already + * validated access. This will still fire the {@link fr.traqueur.crates.api.events.CrateOpenEvent}.

+ * + * @param player the player to open the crate for + * @param crate the crate to open + * @param animation the animation to play + */ + void openCrate(Player player, Crate crate, Animation animation); + + /** + * Opens the preview menu for a crate, showing all possible rewards. + * + *

The preview menu allows players to see what rewards they can win + * without consuming a key.

+ * + * @param player the player to show the preview to + * @param crate the crate to preview + */ + void openPreview(Player player, Crate crate); + + /** + * Gets the crate a player is currently previewing, if any. + * + * @param player the player to check + * @return an Optional containing the crate being previewed, or empty if not previewing + */ + Optional getPreviewingCrate(Player player); + + /** + * Closes the preview for a player and cleans up associated state. + * + * @param player the player whose preview to close + */ + void closePreview(Player player); + + /** + * Starts the animation for a player who has an open crate menu. + * + *

This method is typically called by the animation button in the crate menu. + * It generates the reward and begins the animation phases.

+ * + * @param player the player whose animation to start + * @param inventory the inventory to animate in + * @param slots the slots to use for the animation + */ + void startAnimation(Player player, Inventory inventory, List slots); + + // ==================== Reroll System ==================== + + /** + * Checks if a player can reroll their current reward. + * + *

A player can reroll if:

+ *
    + *
  • They have an active crate opening
  • + *
  • The animation has completed
  • + *
  • They have remaining rerolls
  • + *
+ * + * @param player the player to check + * @return true if the player can reroll, false otherwise + */ + boolean canReroll(Player player); + + /** + * Gets the number of rerolls remaining for a player's current crate opening. + * + * @param player the player to check + * @return the number of remaining rerolls, or 0 if not opening a crate + */ + int getRerollsRemaining(Player player); + + /** + * Gets the current reward for a player's active crate opening. + * + * @param player the player to check + * @return an Optional containing the current reward, or empty if not opening + */ + Optional getCurrentReward(Player player); + + /** + * Performs a reroll for a player, generating a new reward and restarting the animation. + * + *

This method fires {@link fr.traqueur.crates.api.events.CrateRerollEvent} which can be cancelled.

+ * + * @param player the player to reroll for + * @return true if the reroll was successful, false if cancelled or not allowed + */ + boolean reroll(Player player); + + /** + * Checks if a player's animation has completed. + * + * @param player the player to check + * @return true if animation is complete, false otherwise + */ + boolean isAnimationCompleted(Player player); + + // ==================== Crate Lifecycle ==================== + + /** + * Stops all active crate openings, cancelling animations and closing inventories. + * + *

This is typically called during plugin shutdown.

+ */ + void stopAllOpening(); + + /** + * Closes a crate opening for a player and gives them their reward. + * + *

If the animation was completed, the current reward is given to the player + * and {@link fr.traqueur.crates.api.events.RewardGivenEvent} is fired.

+ * + * @param player the player whose crate to close + */ + void closeCrate(Player player); + + /** + * Ensures all inventory files exist for registered crates. + * + *

Creates default inventory files if they don't exist.

+ */ + void ensureInventoriesExist(); + + // ==================== Placed Crates Management ==================== + + /** + * Places a crate at a location with the specified display. + * + *

The crate data is persisted in the chunk's PDC and the display + * entity/block is spawned immediately.

+ * + * @param crateId the ID of the crate to place + * @param location the location to place the crate at + * @param displayType the type of display (BLOCK, ENTITY, etc.) + * @param displayValue the display value (material name, entity type, etc.) + * @param yaw the rotation of the display + * @return the created PlacedCrate instance + * @throws IllegalArgumentException if no display factory exists for the type + */ + PlacedCrate placeCrate(String crateId, Location location, DisplayType displayType, String displayValue, float yaw); + + /** + * Removes a placed crate from the world. + * + *

This removes both the display and the persisted data.

+ * + * @param placedCrate the placed crate to remove + */ + void removePlacedCrate(PlacedCrate placedCrate); + + /** + * Finds a placed crate by the block at a location. + * + * @param block the block to search for + * @return an Optional containing the placed crate, or empty if not found + */ + Optional findPlacedCrateByBlock(Block block); + + /** + * Finds a placed crate by its display entity. + * + * @param entity the entity to search for + * @return an Optional containing the placed crate, or empty if not found + */ + Optional findPlacedCrateByEntity(Entity entity); + + /** + * Loads all placed crates from a chunk's PDC and spawns their displays. + * + *

Called automatically on chunk load events.

+ * + * @param chunk the chunk to load from + */ + void loadPlacedCratesFromChunk(Chunk chunk); + + /** + * Unloads placed crates from a chunk, removing their displays. + * + *

Called automatically on chunk unload events. Data is preserved in PDC.

+ * + * @param chunk the chunk to unload + */ + void unloadPlacedCratesFromChunk(Chunk chunk); + + /** + * Loads all placed crates from all loaded chunks in all worlds. + * + *

Called during plugin initialization.

+ */ + void loadAllPlacedCrates(); + + /** + * Unloads all placed crates, removing all displays. + * + *

Called during plugin shutdown.

+ */ + void unloadAllPlacedCrates(); + + /** + * Gets all placed crates in a specific world. + * + * @param world the world to search in + * @return a list of placed crates in the world + */ + List getPlacedCratesInWorld(World world); +} \ No newline at end of file diff --git a/api/src/main/java/fr/traqueur/crates/api/managers/Manager.java b/api/src/main/java/fr/traqueur/crates/api/managers/Manager.java index 7ec79e2..3c33fcf 100644 --- a/api/src/main/java/fr/traqueur/crates/api/managers/Manager.java +++ b/api/src/main/java/fr/traqueur/crates/api/managers/Manager.java @@ -3,8 +3,19 @@ import fr.traqueur.crates.api.CratesPlugin; import org.bukkit.plugin.java.JavaPlugin; -public interface Manager { +/** + * Base interface for all manager classes in the zCrates plugin. + * Managers are responsible for handling specific aspects of the plugin's functionality. + */ +public sealed interface Manager permits CratesManager, UsersManager { + /** Initializes the manager. This method is called during the plugin's startup sequence. */ + void init(); + + /** + * Gets the main CratesPlugin instance. + * @return the CratesPlugin instance + */ default CratesPlugin getPlugin() { return JavaPlugin.getPlugin(CratesPlugin.class); } diff --git a/api/src/main/java/fr/traqueur/crates/api/managers/UsersManager.java b/api/src/main/java/fr/traqueur/crates/api/managers/UsersManager.java new file mode 100644 index 0000000..aa85b9f --- /dev/null +++ b/api/src/main/java/fr/traqueur/crates/api/managers/UsersManager.java @@ -0,0 +1,74 @@ +package fr.traqueur.crates.api.managers; + +import fr.traqueur.crates.api.models.CrateOpening; +import fr.traqueur.crates.api.models.User; + +import java.util.UUID; + +/** + * Manager responsible for player data including virtual keys and opening history. + * + *

This manager handles the lifecycle of user data:

+ *
    + *
  • Loading user data when a player joins
  • + *
  • Caching user data in memory for performance
  • + *
  • Persisting changes to the database
  • + *
  • Unloading user data when a player leaves
  • + *
+ * + *

Usage example:

+ *
{@code
+ * UsersManager usersManager = plugin.getManager(UsersManager.class);
+ * User user = usersManager.getUser(player.getUniqueId());
+ *
+ * // Modify user data
+ * user.addKeys("legendary-key", 5);
+ *
+ * // Changes are automatically persisted
+ * }
+ * + * @see User + * @see CrateOpening + */ +public non-sealed interface UsersManager extends Manager { + + /** + * Loads a user's data from the database into cache. + * + *

This is called automatically when a player joins the server. + * The operation is asynchronous and loads both keys and opening history.

+ * + * @param uuid the player's UUID + */ + void loadUser(UUID uuid); + + /** + * Unloads a user's data from cache and saves any pending changes. + * + *

This is called automatically when a player leaves the server.

+ * + * @param uuid the player's UUID + */ + void unloadUser(UUID uuid); + + /** + * Gets a user from the cache. + * + *

The user must have been loaded first via {@link #loadUser(UUID)}. + * Returns null if the user is not in cache.

+ * + * @param uuid the player's UUID + * @return the cached User object, or null if not loaded + */ + User getUser(UUID uuid); + + /** + * Persists a crate opening record to the database. + * + *

This is called internally when a reward is given to a player. + * The operation is asynchronous.

+ * + * @param opening the crate opening record to persist + */ + void persistCrateOpening(CrateOpening opening); +} diff --git a/api/src/main/java/fr/traqueur/crates/api/models/CrateOpening.java b/api/src/main/java/fr/traqueur/crates/api/models/CrateOpening.java new file mode 100644 index 0000000..d1a0909 --- /dev/null +++ b/api/src/main/java/fr/traqueur/crates/api/models/CrateOpening.java @@ -0,0 +1,21 @@ +package fr.traqueur.crates.api.models; + +import java.util.UUID; + +/** + * Represents a record of a crate opening by a player. + * + * @param id The unique identifier of the crate opening. + * @param playerUuid The UUID of the player who opened the crate. + * @param crateId The identifier of the crate that was opened. + * @param rewardId The identifier of the reward obtained from the crate. + * @param timestamp The timestamp when the crate was opened. + */ +public record CrateOpening( + UUID id, + UUID playerUuid, + String crateId, + String rewardId, + long timestamp +) { +} \ No newline at end of file diff --git a/api/src/main/java/fr/traqueur/crates/api/models/User.java b/api/src/main/java/fr/traqueur/crates/api/models/User.java new file mode 100644 index 0000000..2be292d --- /dev/null +++ b/api/src/main/java/fr/traqueur/crates/api/models/User.java @@ -0,0 +1,101 @@ +package fr.traqueur.crates.api.models; + +import java.util.List; +import java.util.Map; +import java.util.UUID; + +/** + * Represents a player's crate-related data including virtual keys and opening history. + * + *

User data is persisted in the database and cached in memory for performance. + * The {@link fr.traqueur.crates.api.managers.UsersManager} handles loading and saving.

+ * + *

Usage example:

+ *
{@code
+ * UsersManager usersManager = plugin.getManager(UsersManager.class);
+ * User user = usersManager.getUser(player.getUniqueId());
+ *
+ * // Check and use keys
+ * if (user.hasKey("legendary-key")) {
+ *     user.removeKeys("legendary-key", 1);
+ * }
+ *
+ * // Give keys
+ * user.addKeys("common-key", 5);
+ * }
+ * + * @see fr.traqueur.crates.api.managers.UsersManager + * @see CrateOpening + */ +public interface User { + + /** + * Gets the UUID of this user. + * + * @return the player's unique identifier + */ + UUID uuid(); + + /** + * Gets the number of keys a user has for a specific key type. + * + * @param keyName the key name + * @return the number of keys (0 if none) + */ + int getKeyCount(String keyName); + + /** + * Adds keys to this user's balance. + * + * @param keyName the key name + * @param amount the amount to add (must be positive) + */ + void addKeys(String keyName, int amount); + + /** + * Removes keys from this user's balance. + * + *

The balance will not go below 0.

+ * + * @param keyName the key name + * @param amount the amount to remove (must be positive) + */ + void removeKeys(String keyName, int amount); + + /** + * Checks if this user has at least one key of the specified type. + * + * @param keyName the key name + * @return true if the user has at least one key + */ + boolean hasKey(String keyName); + + /** + * Gets all keys and their counts for this user. + * + * @return an unmodifiable map of key names to counts + */ + Map getAllKeys(); + + /** + * Gets all crate openings for this user. + * + *

Used by algorithms that need player history (e.g., pity systems).

+ * + * @return the list of crate openings, most recent first + */ + List getCrateOpenings(); + + /** + * Adds a crate opening to this user's history. + * + *

Called automatically when a reward is given. The opening is persisted + * to the database asynchronously.

+ * + * @param crateId the crate ID + * @param rewardId the reward ID + * @return the created CrateOpening record + */ + CrateOpening addCrateOpening(String crateId, String rewardId); + +} diff --git a/api/src/main/java/fr/traqueur/crates/api/models/Wrapper.java b/api/src/main/java/fr/traqueur/crates/api/models/Wrapper.java new file mode 100644 index 0000000..c4bd2ba --- /dev/null +++ b/api/src/main/java/fr/traqueur/crates/api/models/Wrapper.java @@ -0,0 +1,22 @@ +package fr.traqueur.crates.api.models; + +/** + * A generic wrapper class that holds a delegate object of type T. + * + * @param the type of the delegate object + */ +public abstract class Wrapper { + + /** The delegate object being wrapped */ + protected final T delegate; + + /** + * Constructs a Wrapper with the specified delegate. + * + * @param delegate the object to be wrapped + */ + public Wrapper(T delegate) { + this.delegate = delegate; + } + +} diff --git a/api/src/main/java/fr/traqueur/crates/api/models/algorithms/AlgorithmContext.java b/api/src/main/java/fr/traqueur/crates/api/models/algorithms/AlgorithmContext.java new file mode 100644 index 0000000..0fa384f --- /dev/null +++ b/api/src/main/java/fr/traqueur/crates/api/models/algorithms/AlgorithmContext.java @@ -0,0 +1,26 @@ +package fr.traqueur.crates.api.models.algorithms; + +import fr.traqueur.crates.api.models.CrateOpening; +import fr.traqueur.crates.api.models.Wrapper; +import fr.traqueur.crates.api.models.crates.Reward; + +import java.util.List; + +/** + * Context provided to random algorithms when selecting a reward. + * Contains wrapped objects with helper methods for algorithm development. + * + * This follows the same pattern as AnimationContext, using Wrapper objects + * to expose safe and convenient APIs to JavaScript. + * @param rewards Wrapped list of possible rewards. + * @param history Wrapped list of history of crate openings for the player. + * @param crateId Identifier of the crate being opened. + * @param playerUuid UUID of the player opening the crate. + */ +public record AlgorithmContext( + Wrapper> rewards, + Wrapper> history, + String crateId, + String playerUuid +) { +} diff --git a/api/src/main/java/fr/traqueur/crates/api/models/algorithms/RandomAlgorithm.java b/api/src/main/java/fr/traqueur/crates/api/models/algorithms/RandomAlgorithm.java new file mode 100644 index 0000000..a316267 --- /dev/null +++ b/api/src/main/java/fr/traqueur/crates/api/models/algorithms/RandomAlgorithm.java @@ -0,0 +1,34 @@ +package fr.traqueur.crates.api.models.algorithms; + +import fr.traqueur.crates.api.models.crates.Reward; + +import java.util.List; +import java.util.function.Function; + +/** + * Interface for reward selection algorithms. + * Algorithms can be implemented in JavaScript to provide custom logic + * for selecting rewards from crates (e.g., pity systems, guaranteed rewards). + */ +public interface RandomAlgorithm { + + /** + * Unique identifier for this algorithm + * @return Algorithm ID + */ + String id(); + + /** + * Source file where this algorithm was defined + * @return Source file path + */ + String sourceFile(); + + /** + * Function that selects a reward based on the context + * The function receives an AlgorithmContext and returns the selected Reward + * @return Function that selects a reward + */ + Function selector(); + +} diff --git a/api/src/main/java/fr/traqueur/crates/api/models/animations/Animation.java b/api/src/main/java/fr/traqueur/crates/api/models/animations/Animation.java new file mode 100644 index 0000000..2b6cd59 --- /dev/null +++ b/api/src/main/java/fr/traqueur/crates/api/models/animations/Animation.java @@ -0,0 +1,117 @@ +package fr.traqueur.crates.api.models.animations; + +import java.util.List; +import java.util.function.Consumer; + +/** + * Represents an animation that plays when a crate is opened. + * + *

Animations are defined in JavaScript files in {@code plugins/zCrates/animations/} + * and consist of multiple phases with timing and visual effects.

+ * + *

Example JavaScript animation:

+ *
{@code
+ * animations.register("roulette", {
+ *     phases: [
+ *         {
+ *             name: "spin",
+ *             duration: 3000,
+ *             interval: 2,
+ *             speedCurve: "EASE_OUT",
+ *             onStart: function(context) { },
+ *             onTick: function(context, tickData) { },
+ *             onEnd: function(context) { }
+ *         }
+ *     ],
+ *     onComplete: function(context) { },
+ *     onCancel: function(context) { }
+ * });
+ * }
+ * + * @see AnimationPhase + * @see AnimationContext + */ +public interface Animation { + + /** + * Gets the unique identifier for this animation. + * + *

Referenced by crate configurations in the {@code animation} field.

+ * + * @return the animation ID (e.g., "roulette", "csgo") + */ + String id(); + + /** + * Gets the source JavaScript file path. + * + * @return the relative path to the animation script + */ + String sourceFile(); + + /** + * Gets all phases of this animation. + * + *

Phases execute sequentially, each with its own duration and callbacks.

+ * + * @return the list of animation phases + * @see AnimationPhase + */ + List phases(); + + /** + * Gets a phase by its index. + * + * @param index the phase index (0-based) + * @return the animation phase + * @throws IndexOutOfBoundsException if index is invalid + */ + default AnimationPhase phase(int index) { + if(index < 0 || index >= phases().size()) { + throw new IndexOutOfBoundsException("Invalid phase index: " + index); + } + return phases().get(index); + } + + /** + * Gets a phase by its name. + * + * @param name the phase name + * @return the animation phase + * @throws IllegalArgumentException if no phase with that name exists + */ + default AnimationPhase phase(String name) { + return phases().stream() + .filter(phase -> phase.name().equals(name)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("No phase found with id: " + name)); + } + + /** + * Gets the total duration of all phases combined. + * + * @return the total duration in milliseconds + */ + default long duration() { + return phases().stream().mapToLong(AnimationPhase::duration).sum(); + } + + /** + * Gets the callback executed when the animation completes successfully. + * + *

Called after all phases finish and the reward is ready to be claimed.

+ * + * @return the completion callback + */ + Consumer onComplete(); + + /** + * Gets the callback executed when the animation is cancelled. + * + *

Called when the player closes the menu or the animation is interrupted.

+ * + * @return the cancellation callback + */ + Consumer onCancel(); + +} diff --git a/api/src/main/java/fr/traqueur/crates/api/models/animations/AnimationContext.java b/api/src/main/java/fr/traqueur/crates/api/models/animations/AnimationContext.java new file mode 100644 index 0000000..88a206c --- /dev/null +++ b/api/src/main/java/fr/traqueur/crates/api/models/animations/AnimationContext.java @@ -0,0 +1,15 @@ +package fr.traqueur.crates.api.models.animations; + +import fr.traqueur.crates.api.models.crates.Crate; +import fr.traqueur.crates.api.models.Wrapper; +import org.bukkit.entity.Player; +import org.bukkit.inventory.Inventory; + +/** + * Context information for crate opening animations. + * + * @param player The player involved in the animation. + * @param inventory The inventory associated with the animation. + * @param crate The crate being opened in the animation. + */ +public record AnimationContext(Wrapper player, Wrapper inventory, Wrapper crate) { } diff --git a/api/src/main/java/fr/traqueur/crates/api/models/animations/AnimationPhase.java b/api/src/main/java/fr/traqueur/crates/api/models/animations/AnimationPhase.java new file mode 100644 index 0000000..0f2fee1 --- /dev/null +++ b/api/src/main/java/fr/traqueur/crates/api/models/animations/AnimationPhase.java @@ -0,0 +1,73 @@ +package fr.traqueur.crates.api.models.animations; + +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Function; + +/** + * Represents a phase in an animation sequence. + * + * @param name The name of the animation phase. + * @param duration The total duration of the phase in milliseconds. + * @param interval The interval between ticks in milliseconds. + * @param speedCurve The speed curve to apply during the phase. + * @param onStart A consumer that is called when the phase starts. + * @param onTick A bi-consumer that is called on each tick with the animation context and tick data. + * @param onComplete A consumer that is called when the phase completes. + */ +public record AnimationPhase(String name, long duration, long interval, SpeedCurve speedCurve, + Consumer onStart, + BiConsumer onTick, + Consumer onComplete) { + + /** + * Data provided on each tick of the animation phase. + * + * @param tickNumber The current tick number. + * @param progress The progress of the phase as a value between 0.0 and 1.0. + * @param elapsedTime The elapsed time since the start of the phase in milliseconds. + */ + public record TickData(int tickNumber, double progress, long elapsedTime) { } + + /** + * Represents different speed curves for animation phases. + */ + public enum SpeedCurve { + /** A linear speed curve. */ + LINEAR(progress -> progress), + /** An ease-in speed curve. */ + EASE_IN(progress -> progress * progress), + /** An ease-out speed curve. */ + EASE_OUT(progress -> 1 - Math.pow(1 - progress, 2)), + /** An ease-in-out speed curve. */ + EASE_IN_OUT(progress -> { + if (progress < 0.5) { + return 2 * progress * progress; + } else { + return 1 - Math.pow(-2 * progress + 2, 2) / 2; + } + }); + + /** The function defining the speed curve. */ + private final Function curveFunction; + + /** + * Constructs a SpeedCurve with the given curve function. + * + * @param curveFunction A function that defines the speed curve. + */ + SpeedCurve(Function curveFunction) { + this.curveFunction = curveFunction; + } + + /** + * Applies the speed curve to the given progress value. + * + * @param progress The progress value between 0.0 and 1.0. + * @return The adjusted progress value according to the speed curve. + */ + public double apply(double progress) { + return curveFunction.apply(progress); + } + } +} \ No newline at end of file diff --git a/api/src/main/java/fr/traqueur/crates/api/models/crates/Crate.java b/api/src/main/java/fr/traqueur/crates/api/models/crates/Crate.java new file mode 100644 index 0000000..299cb2c --- /dev/null +++ b/api/src/main/java/fr/traqueur/crates/api/models/crates/Crate.java @@ -0,0 +1,153 @@ +package fr.traqueur.crates.api.models.crates; + +import fr.traqueur.crates.api.models.User; +import fr.traqueur.crates.api.models.algorithms.RandomAlgorithm; +import fr.traqueur.crates.api.models.animations.Animation; +import fr.traqueur.crates.api.settings.models.ItemStackWrapper; + +import java.util.List; + +/** + * Represents a crate configuration that can be opened by players to receive rewards. + * + *

A crate defines all aspects of the loot box experience including:

+ *
    + *
  • The key required to open it
  • + *
  • The animation played during opening
  • + *
  • The algorithm used to select rewards
  • + *
  • The list of possible rewards with their weights
  • + *
  • Opening conditions (permissions, cooldowns, etc.)
  • + *
+ * + *

Crates are loaded from YAML files in the {@code plugins/zCrates/crates/} directory.

+ * + *

Example YAML configuration:

+ *
{@code
+ * id: legendary
+ * animation: roulette
+ * algorithm: weighted
+ * display-name: "Legendary Crate"
+ * max-rerolls: 3
+ * key:
+ *   type: VIRTUAL
+ *   name: "legendary-key"
+ * rewards:
+ *   - type: ITEM
+ *     id: diamond-reward
+ *     weight: 10.0
+ *     display-item:
+ *       material: DIAMOND
+ *     item:
+ *       material: DIAMOND
+ *       amount: 5
+ * }
+ * + * @see Reward + * @see Key + * @see Animation + * @see OpenCondition + */ +public interface Crate { + + /** + * Gets the unique identifier for this crate. + * + *

This ID is used in commands, configurations, and internal references.

+ * + * @return the crate ID (e.g., "legendary", "common") + */ + String id(); + + /** + * Gets the display name of this crate. + * + *

Supports MiniMessage formatting for colors and styles.

+ * + * @return the formatted display name + */ + String displayName(); + + /** + * Gets the key required to open this crate. + * + * @return the key configuration (virtual or physical) + * @see Key + */ + Key key(); + + /** + * Gets the animation to play when opening this crate. + * + * @return the animation configuration + * @see Animation + */ + Animation animation(); + + /** + * Gets the algorithm used to select rewards from this crate. + * + * @return the random selection algorithm + * @see RandomAlgorithm + */ + RandomAlgorithm algorithm(); + + /** + * Gets the zMenu inventory name associated with this crate. + * + *

This refers to an inventory file in {@code plugins/zCrates/inventories/}.

+ * + * @return the menu identifier + */ + String relatedMenu(); + + /** + * Gets all possible rewards from this crate. + * + * @return an unmodifiable list of rewards + * @see Reward + */ + List rewards(); + + /** + * Gets the maximum number of rerolls allowed for this crate. + * + *

A value of 0 disables rerolling. When rerolling is enabled, players + * can re-spin the animation to get a different reward.

+ * + * @return the maximum reroll count + */ + int maxRerolls(); + + /** + * Gets the conditions that must be met to open this crate. + * + *

All conditions must pass before the crate can be opened. + * Common conditions include permissions and cooldowns.

+ * + * @return the list of conditions, empty if no conditions + * @see OpenCondition + */ + List conditions(); + + /** + * Gets a random item to display in menus (preview filler). + * + *

This is typically used in animations to show random reward items + * before the final reward is revealed.

+ * + * @return a random display item from the rewards list + */ + ItemStackWrapper randomDisplay(); + + /** + * Generates a reward for a user using the configured algorithm. + * + *

This method uses the crate's {@link RandomAlgorithm} to select + * a reward based on weights and the user's history.

+ * + * @param user the user opening the crate (for history-based algorithms) + * @return the selected reward + * @see RandomAlgorithm + */ + Reward generateReward(User user); +} diff --git a/api/src/main/java/fr/traqueur/crates/api/models/crates/Key.java b/api/src/main/java/fr/traqueur/crates/api/models/crates/Key.java new file mode 100644 index 0000000..018f1c3 --- /dev/null +++ b/api/src/main/java/fr/traqueur/crates/api/models/crates/Key.java @@ -0,0 +1,93 @@ +package fr.traqueur.crates.api.models.crates; + +import fr.traqueur.structura.annotations.Polymorphic; +import fr.traqueur.structura.api.Loadable; +import org.bukkit.entity.Player; + +/** + * Represents a key required to open a crate. + * + *

Keys use polymorphic deserialization based on the "type" field in YAML. + * Two key types are available:

+ *
    + *
  • {@code VIRTUAL} - Stored in database, no physical item
  • + *
  • {@code PHYSIC} - Physical item in player's inventory
  • + *
+ * + *

Example YAML configurations:

+ *
{@code
+ * # Virtual key (database-stored)
+ * key:
+ *   type: VIRTUAL
+ *   name: "legendary-key"
+ *
+ * # Physical key (item)
+ * key:
+ *   type: PHYSIC
+ *   name: "legendary-key"
+ *   item:
+ *     material: TRIPWIRE_HOOK
+ *     name: "Legendary Key"
+ *     lore:
+ *       - "Right-click on a crate"
+ *     glow: true
+ * }
+ * + * @see Crate + */ +@Polymorphic +public interface Key extends Loadable { + + /** + * Gets the unique name of this key. + * + *

For virtual keys, this is the database key identifier. + * For physical keys, this is used for matching.

+ * + * @return the key name + */ + String name(); + + /** + * Checks if a player has this key. + * + *

For virtual keys, checks the database. + * For physical keys, searches the player's inventory.

+ * + * @param player the player to check + * @return true if the player has at least one key + */ + boolean has(Player player); + + /** + * Removes one key from the player. + * + *

For virtual keys, decrements the database count. + * For physical keys, removes one matching item from inventory.

+ * + * @param player the player to remove the key from + */ + void remove(Player player); + + /** + * Gives one key to the player. + * + *

For virtual keys, increments the database count. + * For physical keys, adds the key item to inventory.

+ * + * @param player the player to give the key to + */ + void give(Player player); + + /** + * Counts how many keys the player has. + * + *

For virtual keys, retrieves the count from the database. + * For physical keys, counts matching items in inventory.

+ * + * @param player the player to count keys for + * @return the number of keys the player has + */ + int count(Player player); + +} diff --git a/api/src/main/java/fr/traqueur/crates/api/models/crates/OpenCondition.java b/api/src/main/java/fr/traqueur/crates/api/models/crates/OpenCondition.java new file mode 100644 index 0000000..57e2115 --- /dev/null +++ b/api/src/main/java/fr/traqueur/crates/api/models/crates/OpenCondition.java @@ -0,0 +1,40 @@ +package fr.traqueur.crates.api.models.crates; + +import fr.traqueur.structura.annotations.Polymorphic; +import fr.traqueur.structura.api.Loadable; +import org.bukkit.entity.Player; + +/** + * Represents a condition that must be met before opening a crate. + * Implementations can check permissions, cooldowns, or any other requirement. + */ +@Polymorphic +public interface OpenCondition extends Loadable { + + /** + * Checks if the player meets this condition. + * + * @param player the player to check + * @param crate the crate being opened + * @return true if the condition is met, false otherwise + */ + boolean check(Player player, Crate crate); + + /** + * Called when the player successfully opens the crate. + * Use this for side effects like setting cooldowns. + * + * @param player the player who opened the crate + * @param crate the crate that was opened + */ + default void onOpen(Player player, Crate crate) { + // Default: no-op + } + + /** + * Gets the error message key to display when the condition is not met. + * + * @return the error message key for messages.yml + */ + String errorMessageKey(); +} \ No newline at end of file diff --git a/api/src/main/java/fr/traqueur/crates/api/models/crates/OpenResult.java b/api/src/main/java/fr/traqueur/crates/api/models/crates/OpenResult.java new file mode 100644 index 0000000..ed1b12e --- /dev/null +++ b/api/src/main/java/fr/traqueur/crates/api/models/crates/OpenResult.java @@ -0,0 +1,74 @@ +package fr.traqueur.crates.api.models.crates; + +import org.jetbrains.annotations.Nullable; + +/** + * Result of attempting to open a crate. + * @param status The status of the open attempt. + * @param failedCondition The condition that failed, if any. + */ +public record OpenResult(Status status, @Nullable OpenCondition failedCondition) { + + /** + * Possible statuses for crate opening attempts. + */ + public enum Status { + /** + * The crate was opened successfully. + */ + SUCCESS, + /** + * The player does not have the required key to open the crate. + */ + NO_KEY, + /** + * A condition required to open the crate was not met. + */ + CONDITION_FAILED, + /** + * The crate opening was cancelled by an event. + */ + EVENT_CANCELLED + } + + /** + * Creates a successful OpenResult. + * @return An OpenResult indicating success. + */ + public static OpenResult success() { + return new OpenResult(Status.SUCCESS, null); + } + + /** + * Creates an OpenResult indicating the player lacks the required key. + * @return An OpenResult indicating no key. + */ + public static OpenResult noKey() { + return new OpenResult(Status.NO_KEY, null); + } + + /** + * Creates an OpenResult indicating a condition failed. + * @param condition The condition that failed. + * @return An OpenResult indicating a condition failure. + */ + public static OpenResult conditionFailed(OpenCondition condition) { + return new OpenResult(Status.CONDITION_FAILED, condition); + } + + /** + * Creates an OpenResult indicating the event was cancelled. + * @return An OpenResult indicating event cancellation. + */ + public static OpenResult eventCancelled() { + return new OpenResult(Status.EVENT_CANCELLED, null); + } + + /** + * Checks if the open attempt resulted in an error. + * @return True if there was an error, false if successful. + */ + public boolean isError() { + return status != Status.SUCCESS; + } +} \ No newline at end of file diff --git a/api/src/main/java/fr/traqueur/crates/api/models/crates/Reward.java b/api/src/main/java/fr/traqueur/crates/api/models/crates/Reward.java new file mode 100644 index 0000000..ffb8de7 --- /dev/null +++ b/api/src/main/java/fr/traqueur/crates/api/models/crates/Reward.java @@ -0,0 +1,98 @@ +package fr.traqueur.crates.api.models.crates; + +import fr.traqueur.crates.api.settings.models.ItemStackWrapper; +import fr.traqueur.structura.annotations.Polymorphic; +import fr.traqueur.structura.api.Loadable; +import org.bukkit.entity.Player; + +/** + * Represents a reward that can be won from a crate. + * + *

Rewards use polymorphic deserialization based on the "type" field in YAML. + * The following reward types are available:

+ *
    + *
  • {@code ITEM} - A single item
  • + *
  • {@code ITEMS} - Multiple items
  • + *
  • {@code COMMAND} - A single console command
  • + *
  • {@code COMMANDS} - Multiple console commands
  • + *
+ * + *

Example YAML configurations:

+ *
{@code
+ * # Single item reward
+ * - type: ITEM
+ *   id: diamond-sword
+ *   weight: 5.0
+ *   display-item:
+ *     material: DIAMOND_SWORD
+ *     name: "Legendary Sword"
+ *   item:
+ *     material: DIAMOND_SWORD
+ *     enchantments:
+ *       SHARPNESS: 5
+ *
+ * # Command reward
+ * - type: COMMAND
+ *   id: money-reward
+ *   weight: 20.0
+ *   display-item:
+ *     material: GOLD_INGOT
+ *     name: "$1000"
+ *   command: "eco give %player% 1000"
+ * }
+ * + * @see Crate + */ +@Polymorphic() +public interface Reward extends Loadable { + + /** + * Gets the unique identifier for this reward within the crate. + * + *

Used for logging, history tracking, and algorithm references.

+ * + * @return the reward ID + */ + String id(); + + /** + * Gets the weight of this reward for random selection. + * + *

Higher weight means higher probability of being selected. + * The probability is calculated as: {@code weight / totalWeights}.

+ * + *

Example: With rewards weighted 10, 20, 70:

+ *
    + *
  • 10 weight = 10% chance
  • + *
  • 20 weight = 20% chance
  • + *
  • 70 weight = 70% chance
  • + *
+ * + * @return the weight value (higher = more common) + */ + double weight(); + + /** + * Gets the item to display in preview menus and animations. + * + *

This is what players see before receiving the reward. + * It may differ from the actual reward item.

+ * + * @return the display item configuration + */ + ItemStackWrapper displayItem(); + + /** + * Gives this reward to a player. + * + *

The implementation depends on the reward type:

+ *
    + *
  • ITEM/ITEMS: Adds items to inventory (drops if full)
  • + *
  • COMMAND/COMMANDS: Executes console commands with %player% placeholder
  • + *
+ * + * @param player the player to give the reward to + */ + void give(Player player); + +} diff --git a/api/src/main/java/fr/traqueur/crates/api/models/placedcrates/CrateDisplay.java b/api/src/main/java/fr/traqueur/crates/api/models/placedcrates/CrateDisplay.java new file mode 100644 index 0000000..7e14106 --- /dev/null +++ b/api/src/main/java/fr/traqueur/crates/api/models/placedcrates/CrateDisplay.java @@ -0,0 +1,32 @@ +package fr.traqueur.crates.api.models.placedcrates; + +import org.bukkit.Location; + +/** + * Represents a display for a crate in the game world. + * + * @param The type of element that the crate display represents. + */ +public interface CrateDisplay { + + /** Spawns the crate display in the game world. */ + void spawn(); + + /** Removes the crate display from the game world. */ + void remove(); + + /** + * Checks if the given element matches the crate display. + * + * @param element The element to check. + * @return true if the element matches, false otherwise. + */ + boolean matches(T element); + + /** + * Gets the location of the crate display in the game world. + * + * @return The location of the crate display. + */ + Location getLocation(); +} \ No newline at end of file diff --git a/api/src/main/java/fr/traqueur/crates/api/models/placedcrates/CrateDisplayFactory.java b/api/src/main/java/fr/traqueur/crates/api/models/placedcrates/CrateDisplayFactory.java new file mode 100644 index 0000000..f3853fe --- /dev/null +++ b/api/src/main/java/fr/traqueur/crates/api/models/placedcrates/CrateDisplayFactory.java @@ -0,0 +1,38 @@ +package fr.traqueur.crates.api.models.placedcrates; + +import org.bukkit.Location; + +import java.util.List; + +/** + * Factory interface for creating crate displays. + * + * @param The type of element that the crate display represents. + */ +public interface CrateDisplayFactory { + + /** + * Creates a crate display at the specified location with the given value and yaw. + * + * @param location the location to create the display + * @param value the value representing the display content + * @param yaw the yaw orientation of the display + * @return the created crate display + */ + CrateDisplay create(Location location, String value, float yaw); + + /** + * Validates if the given value is valid for this display type. + * + * @param value the value to validate + * @return true if valid, false otherwise + */ + boolean isValidValue(String value); + + /** + * Returns a list of suggested values for tab completion. + * + * @return list of suggested values + */ + List getSuggestions(); +} \ No newline at end of file diff --git a/api/src/main/java/fr/traqueur/crates/api/models/placedcrates/DisplayType.java b/api/src/main/java/fr/traqueur/crates/api/models/placedcrates/DisplayType.java new file mode 100644 index 0000000..7f49577 --- /dev/null +++ b/api/src/main/java/fr/traqueur/crates/api/models/placedcrates/DisplayType.java @@ -0,0 +1,19 @@ +package fr.traqueur.crates.api.models.placedcrates; + +/** + * Enum representing different types of displays for placed crates. + */ +public enum DisplayType { + /** Display type for blocks. */ + BLOCK, + /** Display type for entities. */ + ENTITY, + /** Display type for holograms. */ + MYTHIC_MOB, + /** Display type for items adder. */ + ITEMS_ADDER, + /** Display type for oraxen. */ + ORAXEN, + /** Display type for nexo. */ + NEXO; +} \ No newline at end of file diff --git a/api/src/main/java/fr/traqueur/crates/api/models/placedcrates/PlacedCrate.java b/api/src/main/java/fr/traqueur/crates/api/models/placedcrates/PlacedCrate.java new file mode 100644 index 0000000..601a72b --- /dev/null +++ b/api/src/main/java/fr/traqueur/crates/api/models/placedcrates/PlacedCrate.java @@ -0,0 +1,46 @@ +package fr.traqueur.crates.api.models.placedcrates; + +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.World; + +import java.util.UUID; + +/** + * Record representing a placed crate in the game world. + * + * @param id Unique identifier for the placed crate. + * @param crateId Identifier of the crate type. + * @param worldName Name of the world where the crate is placed. + * @param x X coordinate of the crate's location. + * @param y Y coordinate of the crate's location. + * @param z Z coordinate of the crate's location. + * @param displayType Type of display for the crate. + * @param displayValue Value associated with the display type. + * @param yaw Yaw orientation of the crate. + */ +public record PlacedCrate( + UUID id, + String crateId, + String worldName, + int x, + int y, + int z, + DisplayType displayType, + String displayValue, + float yaw +) { + + /** + * Converts the PlacedCrate to a Bukkit Location object. + * + * @return Location object representing the crate's location, or null if the world does not exist. + */ + public Location getLocation() { + World world = Bukkit.getWorld(worldName); + if (world == null) { + return null; + } + return new Location(world, x, y, z, yaw, 0); + } +} \ No newline at end of file diff --git a/api/src/main/java/fr/traqueur/crates/api/providers/ItemsProvider.java b/api/src/main/java/fr/traqueur/crates/api/providers/ItemsProvider.java new file mode 100644 index 0000000..ab5b1bf --- /dev/null +++ b/api/src/main/java/fr/traqueur/crates/api/providers/ItemsProvider.java @@ -0,0 +1,20 @@ +package fr.traqueur.crates.api.providers; + +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +/** + * Functional interface for providing ItemStack instances based on a player and item ID. + */ +@FunctionalInterface +public interface ItemsProvider { + + /** + * Retrieves an ItemStack for the given player and item ID. + * + * @param player the player for whom the item is being retrieved + * @param itemId the identifier of the item + * @return the corresponding ItemStack + */ + ItemStack item(Player player, String itemId); +} diff --git a/api/src/main/java/fr/traqueur/crates/api/placeholders/PlaceholderParser.java b/api/src/main/java/fr/traqueur/crates/api/providers/PlaceholderProvider.java similarity index 86% rename from api/src/main/java/fr/traqueur/crates/api/placeholders/PlaceholderParser.java rename to api/src/main/java/fr/traqueur/crates/api/providers/PlaceholderProvider.java index c76972b..b17ce6a 100644 --- a/api/src/main/java/fr/traqueur/crates/api/placeholders/PlaceholderParser.java +++ b/api/src/main/java/fr/traqueur/crates/api/providers/PlaceholderProvider.java @@ -1,4 +1,4 @@ -package fr.traqueur.crates.api.placeholders; +package fr.traqueur.crates.api.providers; import org.bukkit.entity.Player; @@ -21,7 +21,7 @@ *
  • PAPIHook: Integrates with PlaceholderAPI for full placeholder support
  • * */ -public interface PlaceholderParser { +public interface PlaceholderProvider { /** * The singleton instance holder. @@ -33,14 +33,14 @@ class Holder { */ private Holder() {} - private static PlaceholderParser instance = new EmptyParser(); + private static PlaceholderProvider instance = new EmptyProvider(); /** * Sets the global placeholder parser instance. * * @param parser The parser implementation to use */ - public static void setInstance(PlaceholderParser parser) { + public static void setInstance(PlaceholderProvider parser) { instance = parser; } @@ -49,7 +49,7 @@ public static void setInstance(PlaceholderParser parser) { * * @return The active parser (never null, defaults to EmptyParser) */ - public static PlaceholderParser getInstance() { + public static PlaceholderProvider getInstance() { return instance; } } @@ -81,12 +81,12 @@ static String parsePlaceholders(Player player, String text) { *

    This implementation is used as the default when no placeholder system * (like PlaceholderAPI) is available.

    */ - class EmptyParser implements PlaceholderParser { + class EmptyProvider implements PlaceholderProvider { /** * Constructs an EmptyParser instance. */ - private EmptyParser() {} + private EmptyProvider() {} @Override public String parse(Player player, String text) { diff --git a/api/src/main/java/fr/traqueur/crates/api/registries/AnimationsRegistry.java b/api/src/main/java/fr/traqueur/crates/api/registries/AnimationsRegistry.java new file mode 100644 index 0000000..a8e9bf3 --- /dev/null +++ b/api/src/main/java/fr/traqueur/crates/api/registries/AnimationsRegistry.java @@ -0,0 +1,20 @@ +package fr.traqueur.crates.api.registries; + +import fr.traqueur.crates.api.CratesPlugin; +import fr.traqueur.crates.api.models.animations.Animation; + +/** + * Registry for managing animations loaded from files. + */ +public abstract class AnimationsRegistry extends FileBasedRegistry { + + /** + * Constructs an AnimationsRegistry with the specified plugin and resource folder. + * + * @param plugin The CratesPlugin instance. + * @param resourceFolder The folder where animation files are located. + */ + protected AnimationsRegistry(CratesPlugin plugin, String resourceFolder) { + super(plugin, resourceFolder, "animations", ".js"); + } +} diff --git a/api/src/main/java/fr/traqueur/crates/api/registries/CrateDisplayFactoriesRegistry.java b/api/src/main/java/fr/traqueur/crates/api/registries/CrateDisplayFactoriesRegistry.java new file mode 100644 index 0000000..ffd88ae --- /dev/null +++ b/api/src/main/java/fr/traqueur/crates/api/registries/CrateDisplayFactoriesRegistry.java @@ -0,0 +1,22 @@ +package fr.traqueur.crates.api.registries; + +import fr.traqueur.crates.api.models.placedcrates.CrateDisplayFactory; +import fr.traqueur.crates.api.models.placedcrates.DisplayType; + +/** + * Registry for managing crate display factories. + */ +public interface CrateDisplayFactoriesRegistry extends Registry> { + + /** + * Registers a generic crate display factory for the specified display type. + * + * @param type The display type. + * @param factory The crate display factory. + * @param The type of element that the crate display represents. + */ + default void registerGeneric(DisplayType type, CrateDisplayFactory factory) { + this.register(type, factory); + } + +} \ No newline at end of file diff --git a/api/src/main/java/fr/traqueur/crates/api/registries/CratesRegistry.java b/api/src/main/java/fr/traqueur/crates/api/registries/CratesRegistry.java new file mode 100644 index 0000000..db5acf5 --- /dev/null +++ b/api/src/main/java/fr/traqueur/crates/api/registries/CratesRegistry.java @@ -0,0 +1,30 @@ +package fr.traqueur.crates.api.registries; + +import fr.traqueur.crates.api.CratesPlugin; +import fr.traqueur.crates.api.models.crates.Crate; + +/** + * Abstract registry for managing crate configurations stored in files. + * + *

    This class extends {@link FileBasedRegistry} to provide functionality + * for loading and managing crate definitions from YAML files located in a + * specified resource folder.

    + * + *

    Concrete implementations of this class should specify the resource folder + * where crate configuration files are stored.

    + * + * @see FileBasedRegistry + * @see Crate + */ +public abstract class CratesRegistry extends FileBasedRegistry { + + /** + * Constructs a new {@code CratesRegistry} with the specified plugin and resource folder. + * + * @param plugin the main plugin instance + * @param resourceFolder the folder where crate configuration files are located + */ + protected CratesRegistry(CratesPlugin plugin, String resourceFolder) { + super(plugin, resourceFolder, "crates", ".yml", ".yaml"); + } +} diff --git a/api/src/main/java/fr/traqueur/crates/api/registries/FileBasedRegistry.java b/api/src/main/java/fr/traqueur/crates/api/registries/FileBasedRegistry.java index c7fbff8..af77274 100644 --- a/api/src/main/java/fr/traqueur/crates/api/registries/FileBasedRegistry.java +++ b/api/src/main/java/fr/traqueur/crates/api/registries/FileBasedRegistry.java @@ -11,7 +11,12 @@ import java.util.*; import java.util.stream.Stream; - +/** + * A registry that loads items from files in a specified folder structure. + * + * @param the type of the identifier for registered items + * @param the type of items being registered + */ public abstract class FileBasedRegistry implements Registry { /** The plugin instance */ @@ -23,6 +28,9 @@ public abstract class FileBasedRegistry implements Registry { /** The name used in logging messages */ private final String logName; + /** The supported file types for loading items */ + private final Set supportedFileTypes; + /** The root folder of the loaded folder structure */ private Folder rootFolder; @@ -32,12 +40,14 @@ public abstract class FileBasedRegistry implements Registry { * @param plugin the ItemsPlugin instance * @param resourceFolder the folder in resources containing example files * @param logName the name used in logging messages + * @param supportedFileTypes the supported file extensions (e.g., ".yml", ".json") */ - protected FileBasedRegistry(CratesPlugin plugin, String resourceFolder, String logName) { + protected FileBasedRegistry(CratesPlugin plugin, String resourceFolder, String logName, String... supportedFileTypes) { this.plugin = plugin; this.storage = new HashMap<>(); this.resourceFolder = resourceFolder; this.logName = logName; + this.supportedFileTypes = Set.of(supportedFileTypes); } /** @@ -78,7 +88,7 @@ private Folder buildFolderStructure(Path currentPath, Path rootPath) { if (!sub.isEmpty()) { subFolders.add(sub); } - } else if (isYamlFile(path)) { + } else if (isFile(path.toString().toLowerCase())) { T element = loadFile(path); if (element != null) { elements.add(element); @@ -140,16 +150,6 @@ protected boolean ensureFolderExists(Path folder) { return true; } - /** Checks if the given path points to a YAML file. - * - * @param path the path to check - * @return true if the file is a YAML file, false otherwise - */ - private boolean isYamlFile(Path path) { - String name = path.toString().toLowerCase(); - return name.endsWith(".yml") || name.endsWith(".yaml"); - } - /** Copies example files from the resource folder to the plugin's data folder. */ private void copyExampleFiles() { Logger.info("Copying example " + logName + " files..."); @@ -237,7 +237,12 @@ private List listResourceFiles(String folder) throws IOException { */ private boolean isFile(String fileName) { String lower = fileName.toLowerCase(); - return lower.endsWith(".yml") || lower.endsWith(".yaml") || lower.endsWith(".properties"); + for (String ext : supportedFileTypes) { + if (lower.endsWith(ext)) { + return true; + } + } + return fileName.endsWith(".properties"); } /** Loads an item from the specified file. @@ -258,8 +263,8 @@ public T getById(ID id) { } @Override - public Collection getAll() { - return storage.values(); + public List getAll() { + return new ArrayList<>(storage.values()); } @Override diff --git a/api/src/main/java/fr/traqueur/crates/api/registries/HooksRegistry.java b/api/src/main/java/fr/traqueur/crates/api/registries/HooksRegistry.java new file mode 100644 index 0000000..e950f20 --- /dev/null +++ b/api/src/main/java/fr/traqueur/crates/api/registries/HooksRegistry.java @@ -0,0 +1,24 @@ +package fr.traqueur.crates.api.registries; + +import fr.traqueur.crates.api.hooks.Hook; +import org.bukkit.plugin.java.JavaPlugin; + +/** + * Registry interface for managing hooks within the application. + */ +public interface HooksRegistry extends Registry { + + /** + * Enables all registered hooks. + */ + void enableAll(); + + /** + * Scans the specified package for hook implementations and registers them. + * + * @param plugin the JavaPlugin instance + * @param packageName the package name to scan for hooks + */ + void scanPackage(JavaPlugin plugin, String packageName); + +} diff --git a/api/src/main/java/fr/traqueur/crates/api/registries/ItemsProvidersRegistry.java b/api/src/main/java/fr/traqueur/crates/api/registries/ItemsProvidersRegistry.java new file mode 100644 index 0000000..e252097 --- /dev/null +++ b/api/src/main/java/fr/traqueur/crates/api/registries/ItemsProvidersRegistry.java @@ -0,0 +1,9 @@ +package fr.traqueur.crates.api.registries; + +import fr.traqueur.crates.api.providers.ItemsProvider; + +/** + * Registry interface for managing ItemsProvider instances within the application. + */ +public interface ItemsProvidersRegistry extends Registry { +} diff --git a/api/src/main/java/fr/traqueur/crates/api/registries/RandomAlgorithmsRegistry.java b/api/src/main/java/fr/traqueur/crates/api/registries/RandomAlgorithmsRegistry.java new file mode 100644 index 0000000..c1c412f --- /dev/null +++ b/api/src/main/java/fr/traqueur/crates/api/registries/RandomAlgorithmsRegistry.java @@ -0,0 +1,20 @@ +package fr.traqueur.crates.api.registries; + +import fr.traqueur.crates.api.CratesPlugin; +import fr.traqueur.crates.api.models.algorithms.RandomAlgorithm; + +/** + * Registry for random algorithms used in crates. + */ +public abstract class RandomAlgorithmsRegistry extends FileBasedRegistry { + + /** + * Constructor for RandomAlgorithmsRegistry. + * + * @param plugin the CratesPlugin instance + * @param resourceFolder the resource folder path + */ + protected RandomAlgorithmsRegistry(CratesPlugin plugin, String resourceFolder) { + super(plugin, resourceFolder, "algorithms", ".js"); + } +} diff --git a/api/src/main/java/fr/traqueur/crates/api/registries/Registry.java b/api/src/main/java/fr/traqueur/crates/api/registries/Registry.java index d413096..e65fe33 100644 --- a/api/src/main/java/fr/traqueur/crates/api/registries/Registry.java +++ b/api/src/main/java/fr/traqueur/crates/api/registries/Registry.java @@ -4,6 +4,7 @@ import com.google.common.collect.MutableClassToInstanceMap; import java.util.Collection; +import java.util.List; /** * A generic registry interface for managing items identified by unique IDs. @@ -60,7 +61,7 @@ public interface Registry { * * @return A collection of all registered items. */ - Collection getAll(); + List getAll(); /** * Clear all registered items in this registry. diff --git a/api/src/main/java/fr/traqueur/crates/api/serialization/Keys.java b/api/src/main/java/fr/traqueur/crates/api/serialization/Keys.java new file mode 100644 index 0000000..fc73dcc --- /dev/null +++ b/api/src/main/java/fr/traqueur/crates/api/serialization/Keys.java @@ -0,0 +1,168 @@ +package fr.traqueur.crates.api.serialization; + +import fr.traqueur.crates.api.models.placedcrates.PlacedCrate; +import org.bukkit.NamespacedKey; +import org.bukkit.persistence.PersistentDataContainer; +import org.bukkit.persistence.PersistentDataType; +import org.bukkit.plugin.Plugin; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** + * Registry of all data keys used in the zCrates plugin. + * Each key automatically uses its field name as the key identifier. + */ +public class Keys { + + /** Key for the name of the crate */ + public static final DataKey KEY_NAME = new DataKey<>(PersistentDataType.STRING); + /** Key indicating whether the crate is a placed crate entity */ + public static final DataKey PLACED_CRATE_ENTITY = new DataKey<>(PersistentDataType.BOOLEAN); + /** Key for the list of placed crates */ + public static final DataKey> PLACED_CRATES = new DataKey<>(PersistentDataType.LIST.listTypeFrom(PlacedCrateDataType.INSTANCE)); + + // Internal keys for PlacedCrate PDC serialization + + /** Key for the unique ID of the placed crate */ + public static final DataKey INTERNAL_PLACED_CRATE_ID = new DataKey<>(PersistentDataType.STRING); + /** Key for the crate ID associated with the placed crate */ + public static final DataKey INTERNAL_PLACED_CRATE_CRATE_ID = new DataKey<>(PersistentDataType.STRING); + /** Key for the world name where the placed crate is located */ + public static final DataKey INTERNAL_PLACED_CRATE_WORLD_NAME = new DataKey<>(PersistentDataType.STRING); + /** Key for the X coordinate of the placed crate */ + public static final DataKey INTERNAL_PLACED_CRATE_X = new DataKey<>(PersistentDataType.INTEGER); + /** Key for the Y coordinate of the placed crate */ + public static final DataKey INTERNAL_PLACED_CRATE_Y = new DataKey<>(PersistentDataType.INTEGER); + /** Key for the Z coordinate of the placed crate */ + public static final DataKey INTERNAL_PLACED_CRATE_Z = new DataKey<>(PersistentDataType.INTEGER); + /** Key indicating whether the placed crate is currently active */ + public static final DataKey INTERNAL_PLACED_CRATE_DISPLAY_TYPE = new DataKey<>(PersistentDataType.STRING); + /** Key for the display value of the placed crate */ + public static final DataKey INTERNAL_PLACED_CRATE_DISPLAY_VALUE = new DataKey<>(PersistentDataType.STRING); + /** Key for the yaw rotation of the placed crate */ + public static final DataKey INTERNAL_PLACED_CRATE_YAW = new DataKey<>(PersistentDataType.FLOAT); + + private static Plugin PLUGIN; + + private Keys() { + } + + /** + * Initializes the Keys registry with the given plugin instance. + * This must be called before using any DataKey. + * Initializes dependent data types as well. + * + * @param plugin the plugin instance + */ + public static void initialize(Plugin plugin) { + PLUGIN = plugin; + } + + /** + * Generic typed persistent data key that automatically resolves its name from the static field name. + * + * @param the type of data this key stores + */ + public static class DataKey { + + /** Cache of resolved key names to avoid repeated reflection lookups */ + private static final Map, String> KEY_NAMES = new HashMap<>(); + + /** The PersistentDataType associated with this key */ + private final PersistentDataType type; + /** The NamespacedKey for this DataKey, lazily initialized */ + private NamespacedKey namespacedKey; + + /** + * Creates a new DataKey with the specified {@link PersistentDataType}. + * The key name is automatically derived from the static field name. + * + * @param type the {@link PersistentDataType} for this key + */ + public DataKey(PersistentDataType type) { + this.type = type; + } + + /** + * Gets the NamespacedKey for this DataKey, resolving the field name if needed. + * @return the {@link NamespacedKey} for this DataKey + */ + public NamespacedKey getNamespacedKey() { + if (namespacedKey == null) { + String keyName = resolveFieldName(); + namespacedKey = new NamespacedKey(PLUGIN, keyName.toLowerCase()); + } + return namespacedKey; + } + + /** + * Resolves the field name by scanning the Keys class for this instance. + */ + private String resolveFieldName() { + String cachedName = KEY_NAMES.get(this); + if (cachedName != null) { + return cachedName; + } + + try { + Field[] fields = Keys.class.getDeclaredFields(); + for (Field field : fields) { + if (Modifier.isStatic(field.getModifiers()) && + Modifier.isFinal(field.getModifiers()) && + DataKey.class.isAssignableFrom(field.getType())) { + + field.setAccessible(true); + Object fieldValue = field.get(null); + + if (fieldValue == this) { + String fieldName = field.getName(); + KEY_NAMES.put(this, fieldName); + return fieldName; + } + } + } + } catch (IllegalAccessException e) { + throw new RuntimeException("Failed to resolve field name for DataKey", e); + } + + throw new RuntimeException("Could not resolve field name for DataKey instance"); + } + + /** + * Retrieves the value associated with this key from the given {@link PersistentDataContainer}. + * + * @param container the {@link PersistentDataContainer} from which to retrieve the value + * @return an {@link Optional} containing the value if it exists, or empty if it does not + */ + public Optional get(PersistentDataContainer container) { + return Optional.ofNullable(container.get(getNamespacedKey(), type)); + } + + /** + * Retrieves the value associated with this key from the given {@link PersistentDataContainer}. + * If the value does not exist, the provided default value is returned. + * + * @param container the {@link PersistentDataContainer} from which to retrieve the value + * @param defaultValue the default value to return if the key does not exist + * @return the value associated with this key, or the default value if it does not exist + */ + public T get(PersistentDataContainer container, T defaultValue) { + return container.getOrDefault(getNamespacedKey(), type, defaultValue); + } + + /** + * Sets the value associated with this key in the given {@link PersistentDataContainer}. + * + * @param container the {@link PersistentDataContainer} in which to store the value + * @param value the value to store + */ + public void set(PersistentDataContainer container, T value) { + container.set(getNamespacedKey(), type, value); + } + } +} \ No newline at end of file diff --git a/api/src/main/java/fr/traqueur/crates/api/serialization/PlacedCrateDataType.java b/api/src/main/java/fr/traqueur/crates/api/serialization/PlacedCrateDataType.java new file mode 100644 index 0000000..c66454e --- /dev/null +++ b/api/src/main/java/fr/traqueur/crates/api/serialization/PlacedCrateDataType.java @@ -0,0 +1,35 @@ +package fr.traqueur.crates.api.serialization; + +import fr.traqueur.crates.api.models.placedcrates.DisplayType; +import fr.traqueur.crates.api.models.placedcrates.PlacedCrate; +import org.bukkit.persistence.PersistentDataAdapterContext; +import org.bukkit.persistence.PersistentDataContainer; +import org.bukkit.persistence.PersistentDataType; +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; + +/** + * Abstract class for PlacedCrate data type serialization. + */ +public abstract class PlacedCrateDataType implements PersistentDataType { + + /** Singleton instance of PlacedCrateDataType */ + public static PlacedCrateDataType INSTANCE; + + /** + * Constructor for PlacedCrateDataType. + */ + protected PlacedCrateDataType() { + } + + @Override + public @NotNull Class getPrimitiveType() { + return PersistentDataContainer.class; + } + + @Override + public @NotNull Class getComplexType() { + return PlacedCrate.class; + } +} \ No newline at end of file diff --git a/api/src/main/java/fr/traqueur/crates/api/services/ItemsService.java b/api/src/main/java/fr/traqueur/crates/api/services/ItemsService.java new file mode 100644 index 0000000..0514b9e --- /dev/null +++ b/api/src/main/java/fr/traqueur/crates/api/services/ItemsService.java @@ -0,0 +1,201 @@ +package fr.traqueur.crates.api.services; + +import fr.traqueur.crates.api.PlatformType; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.TextDecoration; + +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; + +import java.util.ArrayList; +import java.util.List; + +/** + * Utility class for creating and modifying ItemStacks with Paper/Spigot compatibility. + * Works with Adventure Components throughout the code and handles conversion only at the final step. + */ +public class ItemsService { + + private static final LegacyComponentSerializer LEGACY_SERIALIZER = LegacyComponentSerializer.legacySection(); + + /* + * Private constructor to prevent instantiation + */ + private ItemsService() { + throw new UnsupportedOperationException("Utility class"); + } + + /** + * Sets the display name of an ItemStack using a Component. + * Handles Paper (native) vs Spigot (legacy conversion) automatically. + * + * @param itemStack The ItemStack to modify + * @param displayName The display name as a Component + */ + public static void setDisplayName(ItemStack itemStack, Component displayName) { + if (itemStack == null || displayName == null) { + return; + } + + ItemMeta meta = itemStack.getItemMeta(); + if (meta == null) { + return; + } + + Component processedDisplayName = displayName.decorationIfAbsent(TextDecoration.ITALIC, TextDecoration.State.FALSE); + + + if (PlatformType.isPaper()) { + // Use Paper's native Adventure API + meta.displayName(processedDisplayName); + } else { + // Convert Component to legacy format for Spigot + String legacy = LEGACY_SERIALIZER.serialize(processedDisplayName); + meta.setDisplayName(legacy); + } + + itemStack.setItemMeta(meta); + } + + /** + * Sets the lore of an ItemStack using a list of Components. + * Handles Paper (native) vs Spigot (legacy conversion) automatically. + * Automatically disables italic decoration for all lore lines. + * + * @param itemStack The ItemStack to modify + * @param lore The lore lines as Components + */ + public static void setLore(ItemStack itemStack, List lore) { + if (itemStack == null || lore == null) { + return; + } + + ItemMeta meta = itemStack.getItemMeta(); + if (meta == null) { + return; + } + + // Disable italic decoration for all lore lines + List processedLore = new ArrayList<>(); + for (Component line : lore) { + processedLore.add(line.decorationIfAbsent(TextDecoration.ITALIC, TextDecoration.State.FALSE)); + } + + if (PlatformType.isPaper()) { + // Use Paper's native Adventure API + meta.lore(processedLore); + } else { + // Convert Components to legacy format for Spigot + List legacyLore = new ArrayList<>(); + for (Component line : processedLore) { + String legacy = LEGACY_SERIALIZER.serialize(line); + legacyLore.add(legacy); + } + meta.setLore(legacyLore); + } + + itemStack.setItemMeta(meta); + } + + /** + * Creates a new ItemStack with the specified material, amount, display name, and lore. + * + * @param material The material of the item + * @param amount The number of items in the stack + * @param displayName The display name as a Component + * @param lore The lore lines as Components + * @param itemName The item name as a Component + * @return The created ItemStack + */ + public static ItemStack createItem(Material material, int amount, Component displayName, List lore, Component itemName) { + ItemStack itemStack = ItemStack.of(material, amount); + + if (displayName != null) { + setDisplayName(itemStack, displayName); + } + + if (lore != null && !lore.isEmpty()) { + setLore(itemStack, lore); + } + + if (itemName != null) { + setItemName(itemStack, itemName); + } + + return itemStack; + } + + /** + * Sets the item name of an ItemStack using a Component. + * Handles Paper (native) vs Spigot (legacy conversion) automatically. + * + * @param itemStack The ItemStack to modify + * @param itemName The item name as a Component + */ + public static void setItemName(ItemStack itemStack, Component itemName) { + if (itemStack == null || itemName == null) { + return; + } + + ItemMeta meta = itemStack.getItemMeta(); + if (meta == null) { + return; + } + + Component processedItemName = itemName.decorationIfAbsent(TextDecoration.ITALIC, TextDecoration.State.FALSE); + + + if (PlatformType.isPaper()) { + // Use Paper's native Adventure API + meta.itemName(processedItemName); + } else { + // Convert Component to legacy format for Spigot + String legacy = LEGACY_SERIALIZER.serialize(processedItemName); + meta.setItemName(legacy); + } + + itemStack.setItemMeta(meta); + } + + /** + * Adds a line to the lore of an ItemStack using a Component. + * Handles Paper (native) vs Spigot (legacy conversion) automatically. + * Automatically disables italic decoration for the added lore line. + * + * @param itemStack The ItemStack to modify + * @param line The lore line as a Component + */ + public static void addLoreLine(ItemStack itemStack, Component line) { + if (itemStack == null || line == null) { + return; + } + + ItemMeta meta = itemStack.getItemMeta(); + if (meta == null) { + return; + } + + List currentLore; + if (PlatformType.isPaper()) { + currentLore = meta.lore(); + } else { + List legacyLore = meta.getLore(); + currentLore = new ArrayList<>(); + if (legacyLore != null) { + for (String legacyLine : legacyLore) { + currentLore.add(LEGACY_SERIALIZER.deserialize(legacyLine)); + } + } + } + + if (currentLore == null) { + currentLore = new ArrayList<>(); + } + + currentLore.add(line.decorationIfAbsent(TextDecoration.ITALIC, TextDecoration.State.FALSE)); + + setLore(itemStack, currentLore); + } +} \ No newline at end of file diff --git a/api/src/main/java/fr/traqueur/crates/api/services/MessagesService.java b/api/src/main/java/fr/traqueur/crates/api/services/MessagesService.java index 16d3eb1..6d72fdc 100644 --- a/api/src/main/java/fr/traqueur/crates/api/services/MessagesService.java +++ b/api/src/main/java/fr/traqueur/crates/api/services/MessagesService.java @@ -2,16 +2,18 @@ import fr.traqueur.crates.api.Logger; import fr.traqueur.crates.api.PlatformType; -import fr.traqueur.crates.api.placeholders.PlaceholderParser; +import fr.traqueur.crates.api.providers.PlaceholderProvider; import net.kyori.adventure.audience.Audience; import net.kyori.adventure.platform.bukkit.BukkitAudiences; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.minimessage.MiniMessage; import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; +import net.kyori.adventure.title.Title; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import org.bukkit.plugin.Plugin; +import java.time.Duration; import java.util.Map; /** @@ -98,7 +100,7 @@ public static void close() { * @param placeholders the placeholder resolvers */ public static void sendMessage(CommandSender sender, String message, TagResolver... placeholders) { - String parsedString = sender instanceof Player player ? PlaceholderParser.parsePlaceholders(player, message) : message; + String parsedString = sender instanceof Player player ? PlaceholderProvider.parsePlaceholders(player, message) : message; Component parsedComponent = parseMessage(parsedString, placeholders); if (PlatformType.isPaper()) { sender.sendMessage(parsedComponent); @@ -108,6 +110,50 @@ public static void sendMessage(CommandSender sender, String message, TagResolver } } + /** + * Sends a title to a player. + * Handles Paper (native) vs Spigot (wrapper) automatically. + ** + * @param player The player to send the title to + * @param title The main title text + * @param subtitle The subtitle text + * @param fadeIn Fade in duration in ticks + * @param stay Stay duration in ticks + * @param fadeOut Fade out duration in ticks + * @param placeholders Optional placeholder resolvers + */ + public static void sendTitle(Player player, String title, String subtitle, int fadeIn, int stay, int fadeOut, TagResolver... placeholders) { + // Parse title and subtitle with PlaceholderAPI + String parsedTitle = PlaceholderProvider.parsePlaceholders(player, title); + String parsedSubtitle = PlaceholderProvider.parsePlaceholders(player, subtitle); + + // Parse MiniMessage and custom placeholders + Component titleComponent = parseMessage(parsedTitle, placeholders); + Component subtitleComponent = parseMessage(parsedSubtitle, placeholders); + + // Create title times + Title.Times times = Title.Times.times( + Duration.ofMillis(fadeIn * 50L), + Duration.ofMillis(stay * 50L), + Duration.ofMillis(fadeOut * 50L) + ); + + Title adventureTitle = Title.title( + titleComponent, + subtitleComponent, + times + ); + + // Send using appropriate backend + if (PlatformType.isPaper()) { + player.showTitle(adventureTitle); + } else { + Audience audience = bukkitAudiences.player(player); + audience.showTitle(adventureTitle); + } + } + + /** * Parses a message with MiniMessage tags, legacy color codes, and custom placeholders. * Converts legacy codes to MiniMessage format first, then parses with placeholder resolution. diff --git a/api/src/main/java/fr/traqueur/crates/api/settings/Settings.java b/api/src/main/java/fr/traqueur/crates/api/settings/Settings.java index 13b38c4..bc8a549 100644 --- a/api/src/main/java/fr/traqueur/crates/api/settings/Settings.java +++ b/api/src/main/java/fr/traqueur/crates/api/settings/Settings.java @@ -4,14 +4,33 @@ import com.google.common.collect.MutableClassToInstanceMap; import fr.traqueur.structura.api.Loadable; +/** + * A generic interface for application settings that can be loaded. + * Provides methods to register and retrieve settings instances. + */ public interface Settings extends Loadable { + /** A map to hold instances of settings classes. */ ClassToInstanceMap INSTANCES = MutableClassToInstanceMap.create(); + /** + * Retrieves the settings instance of the specified class. + * + * @param clazz the class of the settings to retrieve + * @param the type of the settings + * @return the settings instance + */ static T get(Class clazz) { return INSTANCES.getInstance(clazz); } + /** + * Registers a settings instance for the specified class. + * + * @param clazz the class of the settings to register + * @param instance the settings instance to register + * @param the type of the settings + */ static void register(Class clazz, T instance) { INSTANCES.putInstance(clazz, instance); } diff --git a/api/src/main/java/fr/traqueur/crates/api/settings/models/DatabaseSettings.java b/api/src/main/java/fr/traqueur/crates/api/settings/models/DatabaseSettings.java new file mode 100644 index 0000000..9aa3874 --- /dev/null +++ b/api/src/main/java/fr/traqueur/crates/api/settings/models/DatabaseSettings.java @@ -0,0 +1,35 @@ +package fr.traqueur.crates.api.settings.models; + +import fr.maxlego08.sarah.DatabaseConnection; +import fr.traqueur.structura.annotations.Polymorphic; +import fr.traqueur.structura.api.Loadable; + +/** + * Interface representing database settings for the application. + * + *

    This interface extends {@link Loadable}, allowing implementations + * to load database configuration details such as table prefixes and + * connection parameters.

    + * + * @see Loadable + * @see DatabaseConnection + */ +@Polymorphic +public interface DatabaseSettings extends Loadable { + + /** + * Retrieves the table prefix used in the database. + * + * @return the table prefix as a {@code String} + */ + String tablePrefix(); + + /** + * Creates a new database connection based on the settings. + * + * @param debug {@code true} to enable debug mode, {@code false} otherwise + * @return a new {@link DatabaseConnection} instance + */ + DatabaseConnection connection(boolean debug); + +} diff --git a/api/src/main/java/fr/traqueur/crates/api/settings/models/ItemStackWrapper.java b/api/src/main/java/fr/traqueur/crates/api/settings/models/ItemStackWrapper.java new file mode 100644 index 0000000..4a2a14c --- /dev/null +++ b/api/src/main/java/fr/traqueur/crates/api/settings/models/ItemStackWrapper.java @@ -0,0 +1,139 @@ +package fr.traqueur.crates.api.settings.models; + + +import fr.traqueur.crates.api.providers.ItemsProvider; +import fr.traqueur.crates.api.providers.PlaceholderProvider; +import fr.traqueur.crates.api.registries.ItemsProvidersRegistry; +import fr.traqueur.crates.api.registries.Registry; +import fr.traqueur.crates.api.services.ItemsService; +import fr.traqueur.crates.api.services.MessagesService; +import fr.traqueur.structura.annotations.Options; +import fr.traqueur.structura.annotations.defaults.DefaultInt; +import fr.traqueur.structura.api.Loadable; +import net.kyori.adventure.text.Component; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +/** + * Wrapper class for configuring and building ItemStack instances. + * Supports direct material specification or delegation to an ItemsProvider. + * Allows customization of display name, lore, and item name with placeholder support. + * @param material the material of the item (optional if using copyFrom) + * @param amount the quantity of the item (default is 1) + * @param copyFrom delegate to an ItemsProvider to create the item (optional) + * @param displayName the display name of the item (optional) + * @param itemName the custom item name (optional) + * @param lore the lore of the item as a list of strings (optional) + */ +public record ItemStackWrapper( + @Options(optional = true) Material material, + + @Options(optional = true) @DefaultInt(1) int amount, + + @Options(optional = true) Delegate copyFrom, + + @Options(optional = true) String displayName, + + @Options(optional = true) String itemName, + + @Options(optional = true) List lore +) implements Loadable { + + /** + * Delegate class for specifying an item via an ItemsProvider. + * + * @param pluginName the name of the plugin providing the ItemsProvider + * @param itemId the identifier of the item within the provider + */ + public record Delegate(String pluginName, String itemId) implements Loadable { + + /** + * Retrieves the ItemStack from the specified ItemsProvider. + * + * @param player the player for context (used when building custom items) + * @param itemId the identifier of the item to retrieve + * @return the ItemStack provided by the ItemsProvider + * @throws IllegalArgumentException if no provider is found for the given plugin name + */ + public ItemStack item(Player player, String itemId) { + ItemsProvider provider = Registry.get(ItemsProvidersRegistry.class).getById(pluginName); + if (provider == null) { + throw new IllegalArgumentException("No item provider found for plugin: " + pluginName); + } + return provider.item(player, itemId); + } + + } + + /** + * Validates the configuration after loading. + * + * @throws IllegalArgumentException if amount is less than 1 or if neither itemId nor material is specified + */ + public ItemStackWrapper { + if (amount < 1) { + throw new IllegalArgumentException("Amount must be at least 1"); + } + if(copyFrom == null && material == null) { + throw new IllegalArgumentException("Either 'copy-from' or 'material' must be specified"); + } + } + + /** + * Builds an ItemStack from these settings. + * + * @param player the player for context (used when building custom items) + * @return the created ItemStack + * @throws IllegalStateException if neither itemId nor material is specified + */ + public @NotNull ItemStack build(@Nullable Player player) { + Component parsedDisplayName = null; + if (displayName != null && !displayName.isEmpty()) { + String parsedDisplayNameStr = PlaceholderProvider.parsePlaceholders(player, displayName); + parsedDisplayName = MessagesService.parseMessage(parsedDisplayNameStr); + } + + List lore = new ArrayList<>(); + if (this.lore != null) { + for (String loreLine : this.lore) { + String parsedLoreLineStr = PlaceholderProvider.parsePlaceholders(player, loreLine); + Component parsedLoreLine = MessagesService.parseMessage(parsedLoreLineStr); + lore.add(parsedLoreLine); + } + } + + Component parsedItemName = null; + if (itemName != null && !itemName.isEmpty()) { + String parsedItemNameStr = PlaceholderProvider.parsePlaceholders(player, itemName); + parsedItemName = MessagesService.parseMessage(parsedItemNameStr); + } + + if (copyFrom != null) { + ItemStack item = copyFrom.item(player, itemName); + if(item == null) { + throw new IllegalStateException("Item provider returned null for itemId: " + copyFrom.itemId); + } + if(parsedDisplayName != null) { + ItemsService.setDisplayName(item, parsedDisplayName); + } + if(!lore.isEmpty()) { + ItemsService.setLore(item, lore); + } + if (parsedItemName != null) { + ItemsService.setItemName(item, parsedItemName); + } + if (item.getAmount() != amount) { + item.setAmount(amount); + } + return item; + } + + return ItemsService.createItem(material, amount, parsedDisplayName, lore, parsedItemName); + } +} \ No newline at end of file diff --git a/api/src/main/java/fr/traqueur/crates/api/storage/DTO.java b/api/src/main/java/fr/traqueur/crates/api/storage/DTO.java new file mode 100644 index 0000000..4ae0dfd --- /dev/null +++ b/api/src/main/java/fr/traqueur/crates/api/storage/DTO.java @@ -0,0 +1,17 @@ +package fr.traqueur.crates.api.storage; + +/** + * Generic Data Transfer Object (DTO) interface for converting to model objects. + * + * @param the type of the model object + */ +public interface DTO { + + /** + * Converts this DTO to its corresponding model object. + * + * @return the model object of type T + */ + T toModel(); + +} diff --git a/api/src/main/java/fr/traqueur/crates/api/storage/Tables.java b/api/src/main/java/fr/traqueur/crates/api/storage/Tables.java new file mode 100644 index 0000000..099fc44 --- /dev/null +++ b/api/src/main/java/fr/traqueur/crates/api/storage/Tables.java @@ -0,0 +1,15 @@ +package fr.traqueur.crates.api.storage; + +/** + * Defines constants for database table names used in the application. + */ +public interface Tables { + + /** the users table */ + String USERS_TABLE = "users"; + /** the user keys table */ + String USER_KEYS_TABLE = "user_keys"; + /** the crate openings table */ + String CRATE_OPENINGS_TABLE = "crate_openings"; + +} diff --git a/api/src/main/java/fr/traqueur/crates/api/storage/repositories/Repository.java b/api/src/main/java/fr/traqueur/crates/api/storage/repositories/Repository.java new file mode 100644 index 0000000..49c09ec --- /dev/null +++ b/api/src/main/java/fr/traqueur/crates/api/storage/repositories/Repository.java @@ -0,0 +1,56 @@ +package fr.traqueur.crates.api.storage.repositories; + +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.concurrent.CompletableFuture; + +/** + * A generic repository interface for managing entities of type T with an identifier of type ID. + * Provides methods for saving, deleting, and retrieving entities asynchronously. + * + * @param the type of the entity + * @param the type of the entity's identifier + */ +public interface Repository { + + /** + * Initializes the repository asynchronously. + * + * @return a CompletableFuture that completes with true if initialization was successful, false otherwise + */ + CompletableFuture init(); + + /** + * Retrieves all items asynchronously. + * + * @return a CompletableFuture that completes with a list of all items + */ + CompletableFuture> findAll(); + + /** + * Saves the given item asynchronously. + * + * @param item the item to save + * @return a CompletableFuture that completes when the save operation is done + */ + CompletableFuture save(@NotNull T item); + + + /** + * Deletes the item with the given identifier asynchronously. + * + * @param id the identifier of the item to delete + * @return a CompletableFuture that completes when the delete operation is done + */ + CompletableFuture delete(@NotNull ID id); + + /** + * Retrieves the item with the given identifier asynchronously. + * + * @param id the identifier of the item to retrieve + * @return a CompletableFuture that completes with the retrieved item + */ + CompletableFuture get(@NotNull ID id); + +} \ No newline at end of file diff --git a/api/src/main/java/fr/traqueur/crates/api/storage/repositories/SQLRepository.java b/api/src/main/java/fr/traqueur/crates/api/storage/repositories/SQLRepository.java new file mode 100644 index 0000000..00623a5 --- /dev/null +++ b/api/src/main/java/fr/traqueur/crates/api/storage/repositories/SQLRepository.java @@ -0,0 +1,85 @@ +package fr.traqueur.crates.api.storage.repositories; + +import fr.maxlego08.sarah.Column; +import fr.maxlego08.sarah.RequestHelper; +import fr.traqueur.crates.api.storage.Tables; + +import java.lang.reflect.Field; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.RecordComponent; +import java.lang.reflect.Type; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +/** + * Base class for SQL repositories. + * Provides common functionality for repositories that use SQL as a storage source. + * + * @param the type of the entity + * @param the type of the data representation of the entity + * @param the type of the entity's identifier + */ +public abstract class SQLRepository implements Repository { + + /** The request helper for executing SQL queries. */ + protected final RequestHelper requestHelper; + /** The data class type. */ + protected Class dataClass = getDataClass(); + + public CompletableFuture init() { + return createTable(); + } + + @SuppressWarnings("unchecked") + private Class getDataClass() { + // Check generic superclass first (for classes that extend SQLRepository) + Type genericSuperclass = getClass().getGenericSuperclass(); + if (genericSuperclass instanceof ParameterizedType parameterizedType) { + Type rawType = parameterizedType.getRawType(); + if (rawType instanceof Class rawClass && + SQLRepository.class.isAssignableFrom(rawClass)) { + Type[] actualTypeArguments = parameterizedType.getActualTypeArguments(); + if (actualTypeArguments.length > 1) { + return (Class) actualTypeArguments[1]; + } + } + } + throw new IllegalStateException("Could not determine data class for repository: " + getClass().getName()); + } + + /** + * Constructor for SQLRepository. + * + * @param requestHelper the request helper to be used by this repository + */ + public SQLRepository(RequestHelper requestHelper) { + this.requestHelper = requestHelper; + } + + /** + * Retrieves the name of the primary key column from the data class. + * + * @return the name of the primary key column + * @throws IllegalStateException if no primary key column is found + */ + protected String getPrimaryKeyColumn() { + for (RecordComponent recordComponent : dataClass.getRecordComponents()) { + if(recordComponent.isAnnotationPresent(Column.class)) { + Column column = recordComponent.getAnnotation(Column.class); + if(column.primary()) { + return column.value(); + } + } + } + throw new IllegalStateException("No primary key column found in data class: " + dataClass.getName()); + } + + /** + * Creates the table for the entity in the database. + * This method should be implemented by subclasses to define the table structure. + * + * @return a CompletableFuture that completes when the table is created + */ + public abstract CompletableFuture createTable(); + +} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index f02ffc4..a5eef42 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -26,7 +26,6 @@ allprojects { name = "papermc" url = uri("https://repo.papermc.io/repository/maven-public/") } - maven(url = "https://repo.extendedclip.com/content/repositories/placeholderapi/") maven(url = "https://jitpack.io") } @@ -38,16 +37,27 @@ allprojects { dependencies { compileOnly("io.papermc.paper:paper-api:1.21.8-R0.1-SNAPSHOT") - /* Depends */ - compileOnly("me.clip:placeholderapi:2.11.6") - compileOnly("fr.maxlego08.menu:zmenu-api:1.1.0.4") - - /* Adventure for Spigot compatibility */ compileOnly("net.kyori:adventure-platform-bukkit:4.3.4") + compileOnly("org.mozilla:rhino:1.7.14") + compileOnly("org.reflections:reflections:0.10.2") + + compileOnly(files(rootProject.files("libs/zMenu-1.1.0.4.jar"))) /* Libraries */ - implementation("com.github.Traqueur-dev:Structura:1.5.0") + implementation("fr.traqueur:structura:1.6.0") implementation("com.github.Traqueur-dev.CommandsAPI:platform-spigot:4.2.3") + implementation("fr.maxlego08.sarah:sarah:1.21") + + /* Test dependencies */ + testImplementation("org.junit.jupiter:junit-jupiter:5.10.0") + testImplementation("org.mozilla:rhino:1.7.14") + testImplementation("io.papermc.paper:paper-api:1.21.8-R0.1-SNAPSHOT") + testImplementation("org.slf4j:slf4j-simple:2.0.9") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") + } + + tasks.test { + useJUnitPlatform() } tasks.shadowJar { @@ -55,14 +65,19 @@ allprojects { archiveAppendix.set(if (project.path == ":") "" else project.name) archiveClassifier.set("") - relocate("fr.traqueur.structura", "fr.traqueur.items.libs.structura") - relocate("fr.traqueur.commands", "fr.traqueur.items.libs.commands") + relocate("fr.traqueur.structura", "fr.traqueur.crates.libs.structura") + relocate("fr.traqueur.commands", "fr.traqueur.crates.libs.commands") + relocate("fr.maxlego08.sarah", "fr.traqueur.crates.libs.sarah") } } dependencies { api(project(":api")) + api(project(":common")) + rootProject.subprojects.filter { it.path.startsWith(":hooks:") }.forEach { subproject -> + implementation(project(subproject.path)) + } } tasks { diff --git a/common/build.gradle.kts b/common/build.gradle.kts new file mode 100644 index 0000000..6e5faf4 --- /dev/null +++ b/common/build.gradle.kts @@ -0,0 +1,3 @@ +dependencies { + api(project(":api")) +} \ No newline at end of file diff --git a/common/src/main/java/fr/traqueur/crates/models/placedcrates/BlockCrateDisplay.java b/common/src/main/java/fr/traqueur/crates/models/placedcrates/BlockCrateDisplay.java new file mode 100644 index 0000000..5366f56 --- /dev/null +++ b/common/src/main/java/fr/traqueur/crates/models/placedcrates/BlockCrateDisplay.java @@ -0,0 +1,38 @@ +package fr.traqueur.crates.models.placedcrates; + +import fr.traqueur.crates.api.models.placedcrates.CrateDisplay; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.block.Block; + +public class BlockCrateDisplay implements CrateDisplay { + + private final Location location; + private final Material material; + + public BlockCrateDisplay(Location location, String value, float yaw) { + this.location = location; + this.material = Material.valueOf(value.toUpperCase()); + } + + @Override + public void spawn() { + Block block = location.getBlock(); + block.setType(material); + } + + @Override + public void remove() { + location.getBlock().setType(Material.AIR); + } + + @Override + public boolean matches(Block block) { + return block.getLocation().equals(location); + } + + @Override + public Location getLocation() { + return location; + } +} \ No newline at end of file diff --git a/common/src/main/java/fr/traqueur/crates/models/placedcrates/EntityCrateDisplay.java b/common/src/main/java/fr/traqueur/crates/models/placedcrates/EntityCrateDisplay.java new file mode 100644 index 0000000..aaaff92 --- /dev/null +++ b/common/src/main/java/fr/traqueur/crates/models/placedcrates/EntityCrateDisplay.java @@ -0,0 +1,113 @@ +package fr.traqueur.crates.models.placedcrates; + +import fr.traqueur.crates.api.models.placedcrates.CrateDisplay; +import fr.traqueur.crates.api.serialization.Keys; +import org.bukkit.Location; +import org.bukkit.entity.ArmorStand; +import org.bukkit.entity.Entity; +import org.bukkit.entity.EntityType; +import org.bukkit.entity.LivingEntity; + +public class EntityCrateDisplay implements CrateDisplay { + + private final Location location; + protected final String entityType; + private final float yaw; + protected Entity entity; + + public EntityCrateDisplay(Location location, String value, float yaw) { + this.location = location; + this.entityType = value.toUpperCase(); + this.yaw = yaw; + } + + @Override + public void spawn() { + // First, check if an entity already exists at this location (from previous server session) + Location centeredLocation = location.clone().add(0.5, 0, 0.5); + Entity existingEntity = findExistingEntity(centeredLocation); + + if (existingEntity != null) { + this.entity = existingEntity; + // Ensure properties are still correct + this.entity.setRotation(yaw, 0); + this.entity.setInvulnerable(true); + this.entity.setPersistent(true); + this.entity.setGravity(false); + + if (entity instanceof LivingEntity living) { + living.setAI(false); + living.setSilent(true); + living.setCollidable(false); + } + + if (entity instanceof ArmorStand stand) { + stand.setCanMove(false); + stand.setCanTick(false); + } + return; + } + + // Spawn new entity centered on the block + EntityType type = EntityType.valueOf(entityType); + this.entity = location.getWorld().spawnEntity(centeredLocation, type); + this.entity.setRotation(yaw, 0); + this.entity.setInvulnerable(true); + this.entity.setPersistent(true); + this.entity.setGravity(false); + + if (entity instanceof LivingEntity living) { + living.setAI(false); + living.setSilent(true); + living.setCollidable(false); + } + + if (entity instanceof ArmorStand stand) { + stand.setCanMove(false); + stand.setCanTick(false); + } + + Keys.PLACED_CRATE_ENTITY.set(entity.getPersistentDataContainer(), true); + } + + private Entity findExistingEntity(Location centeredLocation) { + // Search for entities with the PDC marker in the vicinity + return centeredLocation.getWorld().getNearbyEntities(centeredLocation, 1, 1, 1).stream() + .filter(e -> { + // Check if entity has our marker + if (!Keys.PLACED_CRATE_ENTITY.get(e.getPersistentDataContainer(), false)) { + return false; + } + // Check if entity type matches + try { + EntityType expectedType = EntityType.valueOf(entityType); + return e.getType() == expectedType; + } catch (IllegalArgumentException ex) { + return false; + } + }) + .findFirst() + .orElse(null); + } + + @Override + public void remove() { + if (entity != null && !entity.isDead()) { + entity.remove(); + } + } + + @Override + public boolean matches(Entity entity) { + return this.entity != null && entity.getUniqueId().equals(this.entity.getUniqueId()); + } + + @Override + public Location getLocation() { + return location; + } + + public Entity getEntity() { + return entity; + } +} \ No newline at end of file diff --git a/docs/API_REFERENCE.md b/docs/API_REFERENCE.md new file mode 100644 index 0000000..ca9f6de --- /dev/null +++ b/docs/API_REFERENCE.md @@ -0,0 +1,635 @@ +# zCrates API Reference + +This document provides comprehensive API documentation for developers who want to integrate with or extend the zCrates plugin. + +## Table of Contents + +- [Getting Started](#getting-started) +- [Core Interfaces](#core-interfaces) +- [Managers](#managers) +- [Models](#models) +- [Events](#events) +- [Registries](#registries) +- [Conditions System](#conditions-system) +- [Examples](#examples) + +--- + +## Getting Started + +### Maven/Gradle Dependency + +```xml + + fr.traqueur + zcrates-api + 1.0.0 + provided + +``` + +### Accessing the API + +```java +// Get the plugin instance +CratesPlugin plugin = (CratesPlugin) JavaPlugin.getPlugin(CratesPlugin.class); + +// Get managers +CratesManager cratesManager = plugin.getManager(CratesManager.class); +UsersManager usersManager = plugin.getManager(UsersManager.class); + +// Get registries +CratesRegistry cratesRegistry = Registry.get(CratesRegistry.class); +AnimationsRegistry animationsRegistry = Registry.get(AnimationsRegistry.class); +``` + +--- + +## Core Interfaces + +### CratesPlugin + +The main plugin class providing access to all plugin services. + +```java +public abstract class CratesPlugin extends JavaPlugin { + + // Get a registered manager + public T getManager(Class clazz); + + // Register an event listener + public void registerListener(Listener listener); + + // Get the zMenu inventory manager + public abstract InventoryManager getInventoryManager(); +} +``` + +--- + +## Managers + +### CratesManager + +Handles crate opening, animations, and placed crates. + +```java +public interface CratesManager extends Manager { + + /** + * Attempts to open a crate for a player. + * Checks key ownership, conditions, and fires events. + * + * @param player the player opening the crate + * @param crate the crate to open + * @return OpenResult indicating success or failure reason + */ + OpenResult tryOpenCrate(Player player, Crate crate); + + /** + * Force opens a crate (bypasses key check and conditions). + * + * @param player the player + * @param crate the crate + * @param animation the animation to play + */ + void openCrate(Player player, Crate crate, Animation animation); + + /** + * Opens the preview menu for a crate. + * + * @param player the player + * @param crate the crate to preview + */ + void openPreview(Player player, Crate crate); + + /** + * Gets the crate a player is currently previewing. + * + * @param player the player + * @return Optional containing the crate, or empty + */ + Optional getPreviewingCrate(Player player); + + /** + * Checks if a player can reroll their reward. + * + * @param player the player + * @return true if reroll is available + */ + boolean canReroll(Player player); + + /** + * Gets remaining rerolls for a player. + * + * @param player the player + * @return number of rerolls remaining + */ + int getRerollsRemaining(Player player); + + /** + * Performs a reroll for a player. + * + * @param player the player + * @return true if reroll succeeded + */ + boolean reroll(Player player); + + /** + * Gets the current reward for a player (during animation). + * + * @param player the player + * @return Optional containing the reward, or empty + */ + Optional getCurrentReward(Player player); + + /** + * Checks if animation has completed for a player. + * + * @param player the player + * @return true if animation is complete + */ + boolean isAnimationCompleted(Player player); + + // Placed Crates Management + + /** + * Places a crate at a location. + * + * @param crateId the crate ID + * @param location the location + * @param displayType the display type (BLOCK, ENTITY, etc.) + * @param displayValue the display value (material/entity type) + * @param yaw the rotation + * @return the created PlacedCrate + */ + PlacedCrate placeCrate(String crateId, Location location, + DisplayType displayType, String displayValue, float yaw); + + /** + * Removes a placed crate. + * + * @param placedCrate the placed crate to remove + */ + void removePlacedCrate(PlacedCrate placedCrate); + + /** + * Finds a placed crate by block. + * + * @param block the block + * @return Optional containing the placed crate, or empty + */ + Optional findPlacedCrateByBlock(Block block); + + /** + * Finds a placed crate by entity. + * + * @param entity the entity + * @return Optional containing the placed crate, or empty + */ + Optional findPlacedCrateByEntity(Entity entity); +} +``` + +### UsersManager + +Handles player data and key management. + +```java +public interface UsersManager extends Manager { + + /** + * Gets a user from cache (synchronous). + * + * @param uuid the player UUID + * @return the User object + */ + User getUser(UUID uuid); + + /** + * Loads a user from database (asynchronous). + * + * @param uuid the player UUID + * @return CompletableFuture with the User + */ + CompletableFuture loadUser(UUID uuid); + + /** + * Saves a user to database (asynchronous). + * + * @param user the user to save + * @return CompletableFuture + */ + CompletableFuture saveUser(User user); + + /** + * Persists a crate opening to database. + * + * @param opening the opening to persist + */ + void persistCrateOpening(CrateOpening opening); +} +``` + +--- + +## Models + +### Crate + +Represents a crate configuration. + +```java +public interface Crate { + String id(); + String displayName(); + Key key(); + Animation animation(); + RandomAlgorithm algorithm(); + String relatedMenu(); + List rewards(); + int maxRerolls(); + List conditions(); + ItemStackWrapper randomDisplay(); + Reward generateReward(User user); +} +``` + +### Key + +Represents a key type (virtual or physical). + +```java +@Polymorphic +public interface Key extends Loadable { + String name(); + boolean has(Player player); + void give(Player player, int amount); + void remove(Player player); +} +``` + +**Implementations:** +- `VIRTUAL` - Stored in database, no physical item +- `PHYSIC` - Physical item in player's inventory + +### Reward + +Represents a reward that can be won from a crate. + +```java +@Polymorphic +public interface Reward extends Loadable { + String id(); + double weight(); + ItemStackWrapper displayItem(); + void give(Player player); +} +``` + +**Implementations:** +- `ITEM` - Single item reward +- `ITEMS` - Multiple items reward +- `COMMAND` - Single command reward +- `COMMANDS` - Multiple commands reward + +### OpenCondition + +Represents a condition to open a crate. + +```java +@Polymorphic +public interface OpenCondition extends Loadable { + + /** + * Checks if the player meets this condition. + * + * @param player the player + * @param crate the crate + * @return true if condition is met + */ + boolean check(Player player, Crate crate); + + /** + * Called when player successfully opens the crate. + * Used for side effects like setting cooldowns. + * + * @param player the player + * @param crate the crate + */ + default void onOpen(Player player, Crate crate) {} + + /** + * Gets the error message key for failed condition. + * + * @return the message key + */ + String errorMessageKey(); +} +``` + +**Implementations:** +- `PERMISSION` - Requires a permission node +- `COOLDOWN` - Requires time since last opening + +### OpenResult + +Result of attempting to open a crate. + +```java +public record OpenResult(Status status, @Nullable OpenCondition failedCondition) { + + public enum Status { + SUCCESS, // Crate opened successfully + NO_KEY, // Player doesn't have the key + CONDITION_FAILED, // A condition was not met + EVENT_CANCELLED // CratePreOpenEvent was cancelled + } + + public static OpenResult success(); + public static OpenResult noKey(); + public static OpenResult conditionFailed(OpenCondition condition); + public static OpenResult eventCancelled(); + + public boolean isSuccess(); +} +``` + +### User + +Represents a player's data. + +```java +public interface User { + UUID uuid(); + Map getKeys(); + int getKeyAmount(String keyName); + void addKey(String keyName, int amount); + void removeKey(String keyName, int amount); + List getCrateOpenings(String crateId); + CrateOpening addCrateOpening(String crateId, String rewardId); +} +``` + +### CrateOpening + +Represents a recorded crate opening. + +```java +public record CrateOpening( + UUID userUuid, + String crateId, + String rewardId, + LocalDateTime openedAt +) {} +``` + +--- + +## Events + +All events extend `CrateEvent` which provides access to player and crate. + +### CratePreOpenEvent (Cancellable) + +Fired before a crate opens. Cancel to prevent opening. + +```java +@EventHandler +public void onPreOpen(CratePreOpenEvent event) { + Player player = event.getPlayer(); + Crate crate = event.getCrate(); + + if (/* some condition */) { + event.setCancelled(true); + } +} +``` + +### CrateOpenEvent + +Fired when a crate opens (after key consumed, menu opening). + +```java +@EventHandler +public void onOpen(CrateOpenEvent event) { + Player player = event.getPlayer(); + Crate crate = event.getCrate(); + Animation animation = event.getAnimation(); +} +``` + +### RewardGeneratedEvent + +Fired when a reward is generated (during animation). + +```java +@EventHandler +public void onRewardGenerated(RewardGeneratedEvent event) { + Player player = event.getPlayer(); + Crate crate = event.getCrate(); + Reward reward = event.getReward(); + boolean isReroll = event.isReroll(); +} +``` + +### CrateRerollEvent (Cancellable) + +Fired before a reroll. Cancel to prevent. + +```java +@EventHandler +public void onReroll(CrateRerollEvent event) { + Player player = event.getPlayer(); + Reward currentReward = event.getCurrentReward(); + int rerollsRemaining = event.getRerollsRemaining(); + + if (/* some condition */) { + event.setCancelled(true); + } +} +``` + +### RewardGivenEvent + +Fired when a reward is given to a player. + +```java +@EventHandler +public void onRewardGiven(RewardGivenEvent event) { + Player player = event.getPlayer(); + Crate crate = event.getCrate(); + Reward reward = event.getReward(); + + // Log, notify, etc. +} +``` + +--- + +## Registries + +### Accessing Registries + +```java +// Get a registry +CratesRegistry cratesRegistry = Registry.get(CratesRegistry.class); +AnimationsRegistry animationsRegistry = Registry.get(AnimationsRegistry.class); +RandomAlgorithmRegistry algorithmRegistry = Registry.get(RandomAlgorithmRegistry.class); + +// Get items from registry +Crate crate = cratesRegistry.getById("example"); +Animation animation = animationsRegistry.getById("roulette"); +Collection allCrates = cratesRegistry.getAll(); +``` + +### Creating Custom Hooks + +```java +@AutoHook("YourPluginName") +public class YourPluginHook implements Hook { + + @Override + public void onEnable() { + // Register custom display factories, item providers, etc. + CrateDisplayFactoriesRegistry registry = + Registry.get(CrateDisplayFactoriesRegistry.class); + registry.registerGeneric(DisplayType.YOUR_TYPE, new YourDisplayFactory()); + } +} +``` + +--- + +## Conditions System + +### Creating Custom Conditions + +1. Create the condition class: + +```java +public record MyCustomCondition(String myParam) implements OpenCondition { + + @Override + public boolean check(Player player, Crate crate) { + // Your logic here + return true; + } + + @Override + public void onOpen(Player player, Crate crate) { + // Called after successful open + } + + @Override + public String errorMessageKey() { + return "my-custom-error"; + } +} +``` + +2. Register in your plugin: + +```java +PolymorphicRegistry.create(OpenCondition.class, registry -> { + registry.register("MY_CUSTOM", MyCustomCondition.class); +}); +``` + +3. Use in crate YAML: + +```yaml +conditions: + - type: MY_CUSTOM + myParam: "value" +``` + +--- + +## Examples + +### Opening a Crate Programmatically + +```java +CratesManager manager = plugin.getManager(CratesManager.class); +CratesRegistry registry = Registry.get(CratesRegistry.class); + +Crate crate = registry.getById("example"); +if (crate != null) { + OpenResult result = manager.tryOpenCrate(player, crate); + + if (!result.isSuccess()) { + switch (result.status()) { + case NO_KEY -> player.sendMessage("You need a key!"); + case CONDITION_FAILED -> { + OpenCondition condition = result.failedCondition(); + // Handle specific condition failure + } + case EVENT_CANCELLED -> player.sendMessage("Opening was cancelled."); + } + } +} +``` + +### Giving Keys to a Player + +```java +UsersManager usersManager = plugin.getManager(UsersManager.class); +User user = usersManager.getUser(player.getUniqueId()); + +// For virtual keys +user.addKey("example-key", 5); + +// For physical keys +Crate crate = registry.getById("example"); +crate.key().give(player, 5); +``` + +### Listening for Rewards + +```java +public class MyRewardListener implements Listener { + + @EventHandler + public void onRewardGiven(RewardGivenEvent event) { + Player player = event.getPlayer(); + Reward reward = event.getReward(); + + // Log to external system + myLogger.log(player.getName() + " won " + reward.id()); + + // Broadcast rare rewards + if (reward.weight() < 5.0) { + Bukkit.broadcastMessage(player.getName() + " won a rare reward!"); + } + } +} +``` + +### Creating a Custom Algorithm + +Create `plugins/zCrates/algorithms/my-algorithm.js`: + +```javascript +algorithms.register("my-algorithm", function(context) { + var rewards = context.rewards(); + var history = context.history(); + + // Get player's recent openings + var recentOpenings = history.getRecent(10); + + // Custom logic + if (recentOpenings.length < 3) { + // New players get better odds + return rewards.weightedRandom(2.0); // 2x weight boost + } + + return rewards.weightedRandom(); +}); +``` + +--- + +## Support + +For issues and feature requests, please use the GitHub issue tracker. \ No newline at end of file diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md new file mode 100644 index 0000000..4f631ef --- /dev/null +++ b/docs/USER_GUIDE.md @@ -0,0 +1,541 @@ +# zCrates - User Guide + +Complete guide for Minecraft server administrators to configure and use the zCrates plugin. + +## Table of Contents + +- [Installation](#installation) +- [Basic Configuration](#basic-configuration) +- [Creating a Crate](#creating-a-crate) +- [Key Types](#key-types) +- [Reward Types](#reward-types) +- [Opening Conditions](#opening-conditions) +- [Animations](#animations) +- [Selection Algorithms](#selection-algorithms) +- [Placing Crates](#placing-crates) +- [Commands](#commands) +- [Permissions](#permissions) +- [Hooks & Integrations](#hooks--integrations) +- [FAQ](#faq) + +--- + +## Installation + +1. Download the `zCrates.jar` file +2. Place it in your server's `plugins/` folder +3. Restart the server +4. Configuration files will be generated automatically + +### Requirements + +- **Minecraft**: 1.21+ +- **Server**: Paper/Purpur +- **Java**: 21+ +- **Dependencies**: zMenu (bundled) + +--- + +## Basic Configuration + +### config.yml + +```yaml +# Enable debug messages +debug: false + +# Database configuration +database: + type: SQLITE # SQLITE, MYSQL, or MARIADB + table-prefix: "zcrates_" + + # For MySQL/MariaDB only + host: "localhost" + port: 3306 + database: "minecraft" + username: "root" + password: "" +``` + +### messages.yml + +Customize all plugin messages: + +```yaml +no-key: "You don't have a key for this crate!" +no-permission: "You don't have permission." +condition-no-permission: "You don't have permission to open this crate!" +condition-cooldown: "You must wait