diff --git a/docs/mining_model.puml b/docs/mining_model.puml index b7a9014..a1c4846 100644 --- a/docs/mining_model.puml +++ b/docs/mining_model.puml @@ -1,54 +1,52 @@ @startuml mining_model -!theme plain -skinparam roundcorner 10 +skinparam roundcorner 8 skinparam shadowing false skinparam classAttributeIconSize 0 -skinparam linetype ortho -title Système de Minage Personnalisé (GamingCore) - Modèle de Données & Architecture +title Système de Minage Personnalisé (GamingCore) - Modèle & Architecture -package "Database Schema (SQLite / MySQL)" <> { - entity "ores" as OresTable { - * id : VARCHAR(64) <> +package "Database Schema (SQLite / MySQL)" <> { + class "ores" as OresTable << (T,#5DADE2) >> { + + id : VARCHAR(64) [PK] -- - * block_material : VARCHAR(64) - * hardness : DOUBLE - * rarity : VARCHAR(32) - * stock : INTEGER - * respawnable : BOOLEAN - * respawn_time : INTEGER - * temp_block : VARCHAR(64) - created_at : TIMESTAMP + + block_material : VARCHAR(64) + + hardness : DOUBLE + + rarity : VARCHAR(32) + + stock : INTEGER + + respawnable : BOOLEAN + + respawn_time : INTEGER + + temp_block : VARCHAR(64) + + created_at : TIMESTAMP } - entity "ore_drops" as OreDropsTable { - * id : INTEGER <> + class "ore_drops" as OreDropsTable << (T,#5DADE2) >> { + + id : INTEGER [PK AUTO] -- - * ore_id : VARCHAR(64) <> - * item_material : VARCHAR(64) - * default_amount : INTEGER - * default_chance : DOUBLE - * fortunable : BOOLEAN - * fortune_multiplier : DOUBLE - * smeltable : BOOLEAN - smelted_material : VARCHAR(64) + + ore_id : VARCHAR(64) [FK] + + item_material : VARCHAR(64) + + default_amount : INTEGER + + default_chance : DOUBLE + + fortunable : BOOLEAN + + fortune_multiplier : DOUBLE + + smeltable : BOOLEAN + + smelted_material : VARCHAR(64) } - entity "placed_ores" as PlacedOresTable { - * id : INTEGER <> + class "placed_ores" as PlacedOresTable << (T,#5DADE2) >> { + + id : INTEGER [PK AUTO] -- - * ore_id : VARCHAR(64) <> - * world : VARCHAR(64) - * x : INTEGER - * y : INTEGER - * z : INTEGER - * current_stock : INTEGER - * is_depleted : BOOLEAN - * next_respawn_at : BIGINT + + ore_id : VARCHAR(64) [FK] + + world : VARCHAR(64) + + x : INTEGER + + y : INTEGER + + z : INTEGER + + current_stock : INTEGER + + is_depleted : BOOLEAN + + next_respawn_at : BIGINT } - OresTable ||..o{ OreDropsTable : "1:N (Drops configurés)" - OresTable ||..o{ PlacedOresTable : "1:N (Blocs actifs dans le monde)" + OresTable "1" *-- "0..*" OreDropsTable : "drops configurés" + OresTable "1" *-- "0..*" PlacedOresTable : "blocs actifs" } package "fr.luc.gamingcore.mining.model" { @@ -137,8 +135,8 @@ package "fr.luc.gamingcore.mining.listener" { } } -CustomOre "1" *-- "many" OreDrop -CustomOre "1" -- "many" PlacedOreBlock +CustomOre "1" *-- "0..*" OreDrop +CustomOre "1" -- "0..*" PlacedOreBlock CustomOre --> OreRarity OreManager o-- CustomOre diff --git a/src/main/java/fr/luc/gamingcore/GamingCore.java b/src/main/java/fr/luc/gamingcore/GamingCore.java index eadde50..d3507f5 100644 --- a/src/main/java/fr/luc/gamingcore/GamingCore.java +++ b/src/main/java/fr/luc/gamingcore/GamingCore.java @@ -1,25 +1,73 @@ package fr.luc.gamingcore; +import fr.luc.gamingcore.mining.command.MiningCommand; +import fr.luc.gamingcore.mining.database.DatabaseManager; +import fr.luc.gamingcore.mining.listener.BlockBreakListener; +import fr.luc.gamingcore.mining.listener.BlockProtectionListener; +import fr.luc.gamingcore.mining.manager.OreManager; +import fr.luc.gamingcore.util.TextUtil; +import org.bukkit.command.PluginCommand; +import org.bukkit.plugin.PluginManager; import org.bukkit.plugin.java.JavaPlugin; public final class GamingCore extends JavaPlugin { private static GamingCore instance; + private DatabaseManager databaseManager; + private OreManager oreManager; + @Override public void onEnable() { instance = this; saveDefaultConfig(); - - getLogger().info("GamingCore a ete active avec succes !"); + + // Initialize Text Util prefix from config + TextUtil.setPrefix(getConfig().getString("prefix")); + + // Initialize SQLite Database + this.databaseManager = new DatabaseManager(this); + this.databaseManager.initialize(); + + // Initialize Ore Manager & Load Data + this.oreManager = new OreManager(this, databaseManager); + this.oreManager.loadData(); + + // Register Listeners + PluginManager pm = getServer().getPluginManager(); + pm.registerEvents(new BlockBreakListener(this, oreManager), this); + pm.registerEvents(new BlockProtectionListener(oreManager), this); + + // Register Commands + MiningCommand miningCommand = new MiningCommand(this, oreManager); + PluginCommand gcCmd = getCommand("gamingcore"); + if (gcCmd != null) { + gcCmd.setExecutor(miningCommand); + gcCmd.setTabCompleter(miningCommand); + } + + getLogger().info("GamingCore (Custom Mining Module) a ete active avec succes !"); } @Override public void onDisable() { + if (oreManager != null) { + oreManager.shutdown(); + } + getLogger().info("GamingCore a ete desactive !"); } public static GamingCore getInstance() { return instance; } + + public DatabaseManager getDatabaseManager() { + return databaseManager; + } + + public OreManager getOreManager() { + return oreManager; + } } + diff --git a/src/main/java/fr/luc/gamingcore/mining/command/MiningCommand.java b/src/main/java/fr/luc/gamingcore/mining/command/MiningCommand.java new file mode 100644 index 0000000..a9adb30 --- /dev/null +++ b/src/main/java/fr/luc/gamingcore/mining/command/MiningCommand.java @@ -0,0 +1,242 @@ +package fr.luc.gamingcore.mining.command; + +import fr.luc.gamingcore.GamingCore; +import fr.luc.gamingcore.mining.manager.OreManager; +import fr.luc.gamingcore.mining.model.CustomOre; +import fr.luc.gamingcore.mining.model.OreDrop; +import fr.luc.gamingcore.mining.model.PlacedOreBlock; +import fr.luc.gamingcore.util.TextUtil; +import org.bukkit.Location; +import org.bukkit.block.Block; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; +import org.bukkit.entity.Player; + +import java.util.*; +import java.util.stream.Collectors; + +public class MiningCommand implements CommandExecutor, TabCompleter { + + private final GamingCore plugin; + private final OreManager oreManager; + + public MiningCommand(GamingCore plugin, OreManager oreManager) { + this.plugin = plugin; + this.oreManager = oreManager; + } + + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + if (args.length == 0 || args[0].equalsIgnoreCase("help")) { + sendHelp(sender, label); + return true; + } + + String sub = args[0].toLowerCase(); + + if (sub.equals("reload")) { + if (!sender.hasPermission("gamingcore.admin")) { + TextUtil.sendMessage(sender, "Tu n'as pas la permission d'exécuter cette commande."); + return true; + } + plugin.reloadConfig(); + TextUtil.setPrefix(plugin.getConfig().getString("prefix")); + oreManager.loadData(); + TextUtil.sendMessage(sender, "Configuration et minerais rechargés avec succès !"); + return true; + } + + if (sub.equals("ore")) { + if (args.length < 2) { + sendHelp(sender, label); + return true; + } + + String action = args[1].toLowerCase(); + + switch (action) { + case "list": + handleList(sender); + return true; + + case "set": + case "place": + if (!sender.hasPermission("gamingcore.admin")) { + TextUtil.sendMessage(sender, "Tu n'as pas la permission d'exécuter cette commande."); + return true; + } + if (!(sender instanceof Player)) { + sender.sendMessage("Cette commande doit être exécutée par un joueur."); + return true; + } + if (args.length < 3) { + TextUtil.sendMessage(sender, "Utilisation: /" + label + " ore set "); + return true; + } + handleSetOre((Player) sender, args[2]); + return true; + + case "remove": + case "delete": + if (!sender.hasPermission("gamingcore.admin")) { + TextUtil.sendMessage(sender, "Tu n'as pas la permission d'exécuter cette commande."); + return true; + } + if (!(sender instanceof Player)) { + sender.sendMessage("Cette commande doit être exécutée par un joueur."); + return true; + } + handleRemoveOre((Player) sender); + return true; + + case "info": + if (!(sender instanceof Player)) { + sender.sendMessage("Cette commande doit être exécutée par un joueur."); + return true; + } + handleInfoOre((Player) sender); + return true; + + default: + sendHelp(sender, label); + return true; + } + } + + sendHelp(sender, label); + return true; + } + + private void handleList(CommandSender sender) { + Collection ores = oreManager.getAllOres(); + if (ores.isEmpty()) { + TextUtil.sendMessage(sender, "Aucun minerai personnalisé n'est actuellement configuré."); + return; + } + + TextUtil.sendRawMessage(sender, "═══════════ [ Minerais Personnalisés ] ═══════════"); + for (CustomOre ore : ores) { + TextUtil.sendRawMessage(sender, " " + ore.getId() + " - " + ore.getDisplayName() + + " " + ore.getRarity().getTag() + " (Stock: " + ore.getStock() + ", Respawn: " + ore.getRespawnTimeSeconds() + "s)"); + for (OreDrop drop : ore.getDrops()) { + TextUtil.sendRawMessage(sender, " └─ Drop: " + drop.getItemMaterial().name() + " x" + drop.getDefaultAmount() + + " (" + (int)(drop.getDefaultChance() * 100) + "%)"); + } + } + TextUtil.sendRawMessage(sender, "══════════════════════════════════════════════════"); + } + + private void handleSetOre(Player player, String oreId) { + CustomOre ore = oreManager.getOre(oreId); + if (ore == null) { + TextUtil.sendMessage(player, "Minerai introuvable : " + oreId + ". Tape /gc ore list pour voir la liste."); + return; + } + + Block targetBlock = player.getTargetBlockExact(5); + if (targetBlock == null || targetBlock.isEmpty()) { + TextUtil.sendMessage(player, "Tu dois regarder un bloc valide à moins de 5 blocs de distance."); + return; + } + + Location loc = targetBlock.getLocation(); + PlacedOreBlock placed = oreManager.placeOreBlock(ore.getId(), loc); + if (placed != null) { + TextUtil.sendMessage(player, "Bloc défini avec succès en tant que minerai " + ore.getId() + " (" + ore.getDisplayName() + ") !"); + } else { + TextUtil.sendMessage(player, "Une erreur est survenue lors de la pose du minerai."); + } + } + + private void handleRemoveOre(Player player) { + Block targetBlock = player.getTargetBlockExact(5); + if (targetBlock == null || targetBlock.isEmpty()) { + TextUtil.sendMessage(player, "Tu dois regarder un bloc valide à moins de 5 blocs de distance."); + return; + } + + boolean removed = oreManager.removeOreBlock(targetBlock.getLocation()); + if (removed) { + TextUtil.sendMessage(player, "Minerai personnalisé retiré avec succès de ce bloc."); + } else { + TextUtil.sendMessage(player, "Ce bloc n'est pas un minerai personnalisé actif."); + } + } + + private void handleInfoOre(Player player) { + Block targetBlock = player.getTargetBlockExact(5); + if (targetBlock == null || targetBlock.isEmpty()) { + TextUtil.sendMessage(player, "Tu dois regarder un bloc valide à moins de 5 blocs de distance."); + return; + } + + PlacedOreBlock placed = oreManager.getPlacedBlock(targetBlock.getLocation()); + if (placed == null) { + TextUtil.sendMessage(player, "Ce bloc n'est pas un minerai personnalisé."); + return; + } + + CustomOre ore = placed.getOreType(); + TextUtil.sendRawMessage(player, "═══════════ [ Info Minerai ] ═══════════"); + TextUtil.sendRawMessage(player, " ID : " + placed.getOreId() + ""); + TextUtil.sendRawMessage(player, " Nom : " + ore.getDisplayName() + ""); + TextUtil.sendRawMessage(player, " Rareté : " + ore.getRarity().getTag() + ""); + TextUtil.sendRawMessage(player, " Stock : " + placed.getCurrentStock() + "/" + ore.getStock() + ""); + TextUtil.sendRawMessage(player, " Épuisé : " + (placed.isDepleted() ? "Oui" : "Non") + ""); + if (placed.isDepleted()) { + long remaining = Math.max(0, (placed.getNextRespawnTimestamp() - System.currentTimeMillis()) / 1000); + TextUtil.sendRawMessage(player, " Respawn dans : " + remaining + "s"); + } + TextUtil.sendRawMessage(player, "════════════════════════════════════════"); + } + + private void sendHelp(CommandSender sender, String label) { + TextUtil.sendRawMessage(sender, "═══════════ [ GamingCore Mining ] ═══════════"); + TextUtil.sendRawMessage(sender, " /" + label + " ore list - Liste des minerais disponibles"); + TextUtil.sendRawMessage(sender, " /" + label + " ore info - Voir les infos du bloc ciblé"); + if (sender.hasPermission("gamingcore.admin")) { + TextUtil.sendRawMessage(sender, " /" + label + " ore set - Transformer le bloc ciblé en minerai"); + TextUtil.sendRawMessage(sender, " /" + label + " ore remove - Retirer le minerai ciblé"); + TextUtil.sendRawMessage(sender, " /" + label + " reload - Recharger la configuration et la BDD"); + } + TextUtil.sendRawMessage(sender, "════════════════════════════════════════════"); + } + + @Override + public List onTabComplete(CommandSender sender, Command command, String alias, String[] args) { + List completions = new ArrayList<>(); + + if (args.length == 1) { + List subcommands = new ArrayList<>(Arrays.asList("help", "ore")); + if (sender.hasPermission("gamingcore.admin")) { + subcommands.add("reload"); + } + return subcommands.stream() + .filter(s -> s.toLowerCase().startsWith(args[0].toLowerCase())) + .collect(Collectors.toList()); + } + + if (args.length == 2 && args[0].equalsIgnoreCase("ore")) { + List oreSubcommands = new ArrayList<>(Arrays.asList("list", "info")); + if (sender.hasPermission("gamingcore.admin")) { + oreSubcommands.addAll(Arrays.asList("set", "remove")); + } + return oreSubcommands.stream() + .filter(s -> s.toLowerCase().startsWith(args[1].toLowerCase())) + .collect(Collectors.toList()); + } + + if (args.length == 3 && args[0].equalsIgnoreCase("ore") && args[1].equalsIgnoreCase("set")) { + if (sender.hasPermission("gamingcore.admin")) { + return oreManager.getAllOres().stream() + .map(CustomOre::getId) + .filter(id -> id.toLowerCase().startsWith(args[2].toLowerCase())) + .collect(Collectors.toList()); + } + } + + return completions; + } +} diff --git a/src/main/java/fr/luc/gamingcore/mining/database/DatabaseManager.java b/src/main/java/fr/luc/gamingcore/mining/database/DatabaseManager.java new file mode 100644 index 0000000..d744f26 --- /dev/null +++ b/src/main/java/fr/luc/gamingcore/mining/database/DatabaseManager.java @@ -0,0 +1,328 @@ +package fr.luc.gamingcore.mining.database; + +import com.cryptomorin.xseries.XMaterial; +import fr.luc.gamingcore.GamingCore; +import fr.luc.gamingcore.mining.model.*; +import org.bukkit.Bukkit; + +import java.io.File; +import java.sql.*; +import java.util.*; +import java.util.logging.Level; + +public class DatabaseManager { + + private final GamingCore plugin; + private final File dbFile; + private Connection connection; + + public DatabaseManager(GamingCore plugin) { + this.plugin = plugin; + this.dbFile = new File(plugin.getDataFolder(), "database.db"); + } + + public synchronized void initialize() { + if (!plugin.getDataFolder().exists()) { + plugin.getDataFolder().mkdirs(); + } + + try { + connect(); + createTables(); + seedDefaultOresIfEmpty(); + } catch (SQLException e) { + plugin.getLogger().log(Level.SEVERE, "Impossible d'initialiser la base de donnees SQLite !", e); + } + } + + private synchronized void connect() throws SQLException { + if (connection != null && !connection.isClosed()) { + return; + } + try { + Class.forName("org.sqlite.JDBC"); + } catch (ClassNotFoundException e) { + // Paper runtime includes SQLite JDBC by default + } + String url = "jdbc:sqlite:" + dbFile.getAbsolutePath(); + connection = DriverManager.getConnection(url); + } + + private synchronized Connection getConnection() throws SQLException { + if (connection == null || connection.isClosed()) { + connect(); + } + return connection; + } + + private void createTables() throws SQLException { + try (Statement stmt = getConnection().createStatement()) { + // Ores Table + stmt.executeUpdate("CREATE TABLE IF NOT EXISTS ores (" + + "id VARCHAR(64) PRIMARY KEY, " + + "display_name VARCHAR(64) NOT NULL, " + + "block_material VARCHAR(64) NOT NULL, " + + "hardness DOUBLE NOT NULL DEFAULT 1.0, " + + "rarity VARCHAR(32) NOT NULL DEFAULT 'COMMON', " + + "stock INTEGER NOT NULL DEFAULT 1, " + + "respawnable INTEGER NOT NULL DEFAULT 1, " + + "respawn_time INTEGER NOT NULL DEFAULT 10, " + + "temp_block VARCHAR(64) NOT NULL DEFAULT 'BEDROCK', " + + "created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP" + + ");"); + + // Ore Drops Table + stmt.executeUpdate("CREATE TABLE IF NOT EXISTS ore_drops (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + + "ore_id VARCHAR(64) NOT NULL, " + + "item_material VARCHAR(64) NOT NULL, " + + "default_amount INTEGER NOT NULL DEFAULT 1, " + + "default_chance DOUBLE NOT NULL DEFAULT 1.0, " + + "fortunable INTEGER NOT NULL DEFAULT 1, " + + "fortune_multiplier DOUBLE NOT NULL DEFAULT 1.0, " + + "smeltable INTEGER NOT NULL DEFAULT 0, " + + "smelted_material VARCHAR(64), " + + "FOREIGN KEY (ore_id) REFERENCES ores(id) ON DELETE CASCADE" + + ");"); + + // Placed Ores Table + stmt.executeUpdate("CREATE TABLE IF NOT EXISTS placed_ores (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + + "ore_id VARCHAR(64) NOT NULL, " + + "world VARCHAR(64) NOT NULL, " + + "x INTEGER NOT NULL, " + + "y INTEGER NOT NULL, " + + "z INTEGER NOT NULL, " + + "current_stock INTEGER NOT NULL, " + + "is_depleted INTEGER NOT NULL DEFAULT 0, " + + "next_respawn_at BIGINT NOT NULL DEFAULT 0, " + + "FOREIGN KEY (ore_id) REFERENCES ores(id) ON DELETE CASCADE" + + ");"); + } + } + + private void seedDefaultOresIfEmpty() throws SQLException { + try (Statement stmt = getConnection().createStatement(); + ResultSet rs = stmt.executeQuery("SELECT COUNT(*) FROM ores")) { + if (rs.next() && rs.getInt(1) == 0) { + plugin.getLogger().info("Insertion des minerais par defaut dans la base SQLite..."); + + // 1. Custom Iron + CustomOre iron = new CustomOre("custom_iron", "Fer Renforcé", XMaterial.IRON_ORE, 3.0, + OreRarity.COMMON, 3, true, 15, XMaterial.BEDROCK, new ArrayList<>()); + iron.addDrop(new OreDrop(0, "custom_iron", XMaterial.RAW_IRON, 1, 1.0, true, 1.0, true, XMaterial.IRON_INGOT)); + saveOre(iron); + + // 2. Custom Gold + CustomOre gold = new CustomOre("custom_gold", "Or Pur", XMaterial.GOLD_ORE, 3.5, + OreRarity.UNCOMMON, 4, true, 20, XMaterial.BEDROCK, new ArrayList<>()); + gold.addDrop(new OreDrop(0, "custom_gold", XMaterial.RAW_GOLD, 1, 1.0, true, 1.0, true, XMaterial.GOLD_INGOT)); + saveOre(gold); + + // 3. Custom Diamond + CustomOre diamond = new CustomOre("custom_diamond", "Diamant Cristallin", XMaterial.DIAMOND_ORE, 5.0, + OreRarity.RARE, 5, true, 45, XMaterial.BEDROCK, new ArrayList<>()); + diamond.addDrop(new OreDrop(0, "custom_diamond", XMaterial.DIAMOND, 1, 1.0, true, 1.2, false, null)); + saveOre(diamond); + + // 4. Custom Ancient Debris / Netherite + CustomOre netherite = new CustomOre("custom_netherite", "Débris Ancien Sacré", XMaterial.ANCIENT_DEBRIS, 8.0, + OreRarity.LEGENDARY, 8, true, 120, XMaterial.BEDROCK, new ArrayList<>()); + netherite.addDrop(new OreDrop(0, "custom_netherite", XMaterial.NETHERITE_SCRAP, 1, 0.9, true, 1.3, false, null)); + saveOre(netherite); + } + } + } + + public synchronized void saveOre(CustomOre ore) throws SQLException { + String sql = "INSERT OR REPLACE INTO ores (id, display_name, block_material, hardness, rarity, stock, respawnable, respawn_time, temp_block) " + + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"; + try (PreparedStatement ps = getConnection().prepareStatement(sql)) { + ps.setString(1, ore.getId()); + ps.setString(2, ore.getDisplayName()); + ps.setString(3, ore.getBlockMaterial().name()); + ps.setDouble(4, ore.getHardness()); + ps.setString(5, ore.getRarity().name()); + ps.setInt(6, ore.getStock()); + ps.setInt(7, ore.isRespawnable() ? 1 : 0); + ps.setInt(8, ore.getRespawnTimeSeconds()); + ps.setString(9, ore.getTempBlockMaterial().name()); + ps.executeUpdate(); + } + + // Drops + for (OreDrop drop : ore.getDrops()) { + saveOreDrop(drop); + } + } + + public synchronized void saveOreDrop(OreDrop drop) throws SQLException { + String sql = "INSERT INTO ore_drops (ore_id, item_material, default_amount, default_chance, fortunable, fortune_multiplier, smeltable, smelted_material) " + + "VALUES (?, ?, ?, ?, ?, ?, ?, ?)"; + try (PreparedStatement ps = getConnection().prepareStatement(sql)) { + ps.setString(1, drop.getOreId()); + ps.setString(2, drop.getItemMaterial().name()); + ps.setInt(3, drop.getDefaultAmount()); + ps.setDouble(4, drop.getDefaultChance()); + ps.setInt(5, drop.isFortunable() ? 1 : 0); + ps.setDouble(6, drop.getFortuneMultiplier()); + ps.setInt(7, drop.isSmeltable() ? 1 : 0); + ps.setString(8, drop.getSmeltedMaterial() != null ? drop.getSmeltedMaterial().name() : null); + ps.executeUpdate(); + } + } + + public synchronized Map loadAllOres() { + Map ores = new HashMap<>(); + String sql = "SELECT * FROM ores"; + + try (Statement stmt = getConnection().createStatement(); + ResultSet rs = stmt.executeQuery(sql)) { + + while (rs.next()) { + String id = rs.getString("id"); + String displayName = rs.getString("display_name"); + XMaterial blockMat = XMaterial.matchXMaterial(rs.getString("block_material")).orElse(XMaterial.STONE); + double hardness = rs.getDouble("hardness"); + OreRarity rarity = OreRarity.fromString(rs.getString("rarity")); + int stock = rs.getInt("stock"); + boolean respawnable = rs.getInt("respawnable") == 1; + int respawnTime = rs.getInt("respawn_time"); + XMaterial tempBlock = XMaterial.matchXMaterial(rs.getString("temp_block")).orElse(XMaterial.BEDROCK); + + CustomOre ore = new CustomOre(id, displayName, blockMat, hardness, rarity, stock, respawnable, respawnTime, tempBlock, new ArrayList<>()); + ores.put(id, ore); + } + } catch (SQLException e) { + plugin.getLogger().log(Level.SEVERE, "Erreur lors du chargement des minerais !", e); + } + + // Load drops for all ores + String dropsSql = "SELECT * FROM ore_drops"; + try (Statement stmt = getConnection().createStatement(); + ResultSet rs = stmt.executeQuery(dropsSql)) { + + while (rs.next()) { + int dropId = rs.getInt("id"); + String oreId = rs.getString("ore_id"); + XMaterial itemMat = XMaterial.matchXMaterial(rs.getString("item_material")).orElse(XMaterial.COBBLESTONE); + int amount = rs.getInt("default_amount"); + double chance = rs.getDouble("default_chance"); + boolean fortunable = rs.getInt("fortunable") == 1; + double fortuneMult = rs.getDouble("fortune_multiplier"); + boolean smeltable = rs.getInt("smeltable") == 1; + String smeltedMatStr = rs.getString("smelted_material"); + XMaterial smeltedMat = smeltedMatStr != null ? XMaterial.matchXMaterial(smeltedMatStr).orElse(null) : null; + + CustomOre ore = ores.get(oreId); + if (ore != null) { + OreDrop drop = new OreDrop(dropId, oreId, itemMat, amount, chance, fortunable, fortuneMult, smeltable, smeltedMat); + ore.addDrop(drop); + } + } + } catch (SQLException e) { + plugin.getLogger().log(Level.SEVERE, "Erreur lors du chargement des drops de minerais !", e); + } + + return ores; + } + + public synchronized Map loadPlacedBlocks(Map ores) { + Map map = new HashMap<>(); + String sql = "SELECT * FROM placed_ores"; + + try (Statement stmt = getConnection().createStatement(); + ResultSet rs = stmt.executeQuery(sql)) { + + while (rs.next()) { + int id = rs.getInt("id"); + String oreId = rs.getString("ore_id"); + String world = rs.getString("world"); + int x = rs.getInt("x"); + int y = rs.getInt("y"); + int z = rs.getInt("z"); + int stock = rs.getInt("current_stock"); + boolean depleted = rs.getInt("is_depleted") == 1; + long nextRespawnAt = rs.getLong("next_respawn_at"); + + CustomOre ore = ores.get(oreId); + if (ore != null) { + BlockLocation loc = new BlockLocation(world, x, y, z); + PlacedOreBlock placed = new PlacedOreBlock(id, oreId, ore, loc, stock, depleted, nextRespawnAt); + map.put(loc, placed); + } + } + } catch (SQLException e) { + plugin.getLogger().log(Level.SEVERE, "Erreur lors du chargement des blocs places !", e); + } + + return map; + } + + public synchronized int insertPlacedBlock(PlacedOreBlock block) { + String sql = "INSERT INTO placed_ores (ore_id, world, x, y, z, current_stock, is_depleted, next_respawn_at) " + + "VALUES (?, ?, ?, ?, ?, ?, ?, ?)"; + try (PreparedStatement ps = getConnection().prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { + ps.setString(1, block.getOreId()); + ps.setString(2, block.getLocation().getWorldName()); + ps.setInt(3, block.getLocation().getX()); + ps.setInt(4, block.getLocation().getY()); + ps.setInt(5, block.getLocation().getZ()); + ps.setInt(6, block.getCurrentStock()); + ps.setInt(7, block.isDepleted() ? 1 : 0); + ps.setLong(8, block.getNextRespawnTimestamp()); + ps.executeUpdate(); + + try (ResultSet rs = ps.getGeneratedKeys()) { + if (rs.next()) { + int generatedId = rs.getInt(1); + block.setId(generatedId); + return generatedId; + } + } + } catch (SQLException e) { + plugin.getLogger().log(Level.SEVERE, "Erreur lors de l'enregistrement d'un minerai pose !", e); + } + return -1; + } + + public void updatePlacedBlockAsync(PlacedOreBlock block) { + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + String sql = "UPDATE placed_ores SET current_stock = ?, is_depleted = ?, next_respawn_at = ? WHERE id = ?"; + try (PreparedStatement ps = getConnection().prepareStatement(sql)) { + ps.setInt(1, block.getCurrentStock()); + ps.setInt(2, block.isDepleted() ? 1 : 0); + ps.setLong(3, block.getNextRespawnTimestamp()); + ps.setInt(4, block.getId()); + ps.executeUpdate(); + } catch (SQLException e) { + plugin.getLogger().log(Level.SEVERE, "Erreur lors de la mise a jour de l'etat du minerai pose !", e); + } + }); + } + + public void deletePlacedBlockAsync(BlockLocation loc) { + Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> { + String sql = "DELETE FROM placed_ores WHERE world = ? AND x = ? AND y = ? AND z = ?"; + try (PreparedStatement ps = getConnection().prepareStatement(sql)) { + ps.setString(1, loc.getWorldName()); + ps.setInt(2, loc.getX()); + ps.setInt(3, loc.getY()); + ps.setInt(4, loc.getZ()); + ps.executeUpdate(); + } catch (SQLException e) { + plugin.getLogger().log(Level.SEVERE, "Erreur lors de la suppression d'un minerai pose !", e); + } + }); + } + + public synchronized void close() { + try { + if (connection != null && !connection.isClosed()) { + connection.close(); + } + } catch (SQLException e) { + plugin.getLogger().log(Level.WARNING, "Erreur lors de la fermeture de la connexion SQLite", e); + } + } +} diff --git a/src/main/java/fr/luc/gamingcore/mining/listener/BlockBreakListener.java b/src/main/java/fr/luc/gamingcore/mining/listener/BlockBreakListener.java new file mode 100644 index 0000000..08e282b --- /dev/null +++ b/src/main/java/fr/luc/gamingcore/mining/listener/BlockBreakListener.java @@ -0,0 +1,46 @@ +package fr.luc.gamingcore.mining.listener; + +import fr.luc.gamingcore.GamingCore; +import fr.luc.gamingcore.mining.manager.OreManager; +import fr.luc.gamingcore.mining.model.PlacedOreBlock; +import fr.luc.gamingcore.util.TextUtil; +import org.bukkit.GameMode; +import org.bukkit.block.Block; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.block.BlockBreakEvent; + +public class BlockBreakListener implements Listener { + + private final GamingCore plugin; + private final OreManager oreManager; + + public BlockBreakListener(GamingCore plugin, OreManager oreManager) { + this.plugin = plugin; + this.oreManager = oreManager; + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onBlockBreak(BlockBreakEvent event) { + Block block = event.getBlock(); + PlacedOreBlock placed = oreManager.getPlacedBlock(block.getLocation()); + if (placed == null) { + return; + } + + Player player = event.getPlayer(); + + // If admin in creative mode sneaking breaks the block, remove it permanently + if (player.getGameMode() == GameMode.CREATIVE && player.isSneaking() && player.hasPermission("gamingcore.admin")) { + oreManager.removeOreBlock(block.getLocation()); + TextUtil.sendMessage(player, "Minerai personnalisé retiré avec succès !"); + event.setCancelled(false); + return; + } + + // Delegate to OreManager for custom mining mechanics + oreManager.handleMine(player, event); + } +} diff --git a/src/main/java/fr/luc/gamingcore/mining/listener/BlockProtectionListener.java b/src/main/java/fr/luc/gamingcore/mining/listener/BlockProtectionListener.java new file mode 100644 index 0000000..bb10445 --- /dev/null +++ b/src/main/java/fr/luc/gamingcore/mining/listener/BlockProtectionListener.java @@ -0,0 +1,50 @@ +package fr.luc.gamingcore.mining.listener; + +import fr.luc.gamingcore.mining.manager.OreManager; +import org.bukkit.block.Block; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.block.BlockExplodeEvent; +import org.bukkit.event.block.BlockPistonExtendEvent; +import org.bukkit.event.block.BlockPistonRetractEvent; +import org.bukkit.event.entity.EntityExplodeEvent; + +public class BlockProtectionListener implements Listener { + + private final OreManager oreManager; + + public BlockProtectionListener(OreManager oreManager) { + this.oreManager = oreManager; + } + + @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) + public void onEntityExplode(EntityExplodeEvent event) { + event.blockList().removeIf(block -> oreManager.isPlacedOre(block.getLocation())); + } + + @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) + public void onBlockExplode(BlockExplodeEvent event) { + event.blockList().removeIf(block -> oreManager.isPlacedOre(block.getLocation())); + } + + @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) + public void onPistonExtend(BlockPistonExtendEvent event) { + for (Block block : event.getBlocks()) { + if (oreManager.isPlacedOre(block.getLocation())) { + event.setCancelled(true); + return; + } + } + } + + @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) + public void onPistonRetract(BlockPistonRetractEvent event) { + for (Block block : event.getBlocks()) { + if (oreManager.isPlacedOre(block.getLocation())) { + event.setCancelled(true); + return; + } + } + } +} diff --git a/src/main/java/fr/luc/gamingcore/mining/manager/OreManager.java b/src/main/java/fr/luc/gamingcore/mining/manager/OreManager.java new file mode 100644 index 0000000..183a9a4 --- /dev/null +++ b/src/main/java/fr/luc/gamingcore/mining/manager/OreManager.java @@ -0,0 +1,210 @@ +package fr.luc.gamingcore.mining.manager; + +import com.cryptomorin.xseries.XMaterial; +import com.cryptomorin.xseries.XSound; +import fr.luc.gamingcore.GamingCore; +import fr.luc.gamingcore.mining.database.DatabaseManager; +import fr.luc.gamingcore.mining.model.*; +import fr.luc.gamingcore.util.TextUtil; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.Particle; +import org.bukkit.block.Block; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.entity.Player; +import org.bukkit.event.block.BlockBreakEvent; +import org.bukkit.inventory.ItemStack; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +public class OreManager { + + private final GamingCore plugin; + private final DatabaseManager databaseManager; + private final RespawnScheduler respawnScheduler; + + private final Map registeredOres = new ConcurrentHashMap<>(); + private final Map placedBlocks = new ConcurrentHashMap<>(); + + public OreManager(GamingCore plugin, DatabaseManager databaseManager) { + this.plugin = plugin; + this.databaseManager = databaseManager; + this.respawnScheduler = new RespawnScheduler(plugin, this); + } + + public void loadData() { + registeredOres.clear(); + placedBlocks.clear(); + + // 1. Load Custom Ores from DB + Map ores = databaseManager.loadAllOres(); + registeredOres.putAll(ores); + plugin.getLogger().info("Charges " + registeredOres.size() + " minerais personnalises."); + + // 2. Load Placed Blocks from DB + Map placed = databaseManager.loadPlacedBlocks(registeredOres); + placedBlocks.putAll(placed); + plugin.getLogger().info("Charges " + placedBlocks.size() + " blocs de minerais actifs."); + + // 3. Start Respawn Scheduler + respawnScheduler.start(); + } + + public void shutdown() { + respawnScheduler.stop(); + databaseManager.close(); + } + + public DatabaseManager getDatabaseManager() { + return databaseManager; + } + + public RespawnScheduler getRespawnScheduler() { + return respawnScheduler; + } + + public CustomOre getOre(String id) { + if (id == null) return null; + return registeredOres.get(id.toLowerCase()); + } + + public Collection getAllOres() { + return Collections.unmodifiableCollection(registeredOres.values()); + } + + public void registerOre(CustomOre ore) { + if (ore != null) { + registeredOres.put(ore.getId().toLowerCase(), ore); + try { + databaseManager.saveOre(ore); + } catch (Exception e) { + plugin.getLogger().warning("Erreur lors de la sauvegarde du minerai : " + ore.getId()); + } + } + } + + public PlacedOreBlock getPlacedBlock(Location loc) { + if (loc == null || loc.getWorld() == null) return null; + return placedBlocks.get(BlockLocation.fromLocation(loc)); + } + + public PlacedOreBlock getPlacedBlock(BlockLocation loc) { + if (loc == null) return null; + return placedBlocks.get(loc); + } + + public Collection getAllPlacedBlocks() { + return Collections.unmodifiableCollection(placedBlocks.values()); + } + + public boolean isPlacedOre(Location loc) { + return getPlacedBlock(loc) != null; + } + + public PlacedOreBlock placeOreBlock(String oreId, Location loc) { + CustomOre ore = getOre(oreId); + if (ore == null || loc == null || loc.getWorld() == null) { + return null; + } + + BlockLocation blockLoc = BlockLocation.fromLocation(loc); + PlacedOreBlock placed = new PlacedOreBlock(-1, ore.getId(), ore, blockLoc, ore.getStock(), false, 0); + + // Update block in world + Block block = loc.getBlock(); + block.setType(ore.parseBlockMaterial()); + + // Save in DB and cache + int generatedId = databaseManager.insertPlacedBlock(placed); + placed.setId(generatedId); + placedBlocks.put(blockLoc, placed); + + return placed; + } + + public boolean removeOreBlock(Location loc) { + if (loc == null || loc.getWorld() == null) return false; + BlockLocation blockLoc = BlockLocation.fromLocation(loc); + PlacedOreBlock removed = placedBlocks.remove(blockLoc); + if (removed != null) { + databaseManager.deletePlacedBlockAsync(blockLoc); + loc.getBlock().setType(Material.AIR); + return true; + } + return false; + } + + /** + * Handles custom mining interaction when a player breaks a placed ore block. + */ + public void handleMine(Player player, BlockBreakEvent event) { + Block block = event.getBlock(); + PlacedOreBlock placed = getPlacedBlock(block.getLocation()); + if (placed == null) { + return; + } + + // Cancel the standard vanilla break event + event.setCancelled(true); + + // If block is already depleted and waiting to respawn, do nothing or notify player + if (placed.isDepleted()) { + long remainingSeconds = Math.max(1, (placed.getNextRespawnTimestamp() - System.currentTimeMillis()) / 1000); + TextUtil.sendActionBar(player, "Ce minerai est épuisé ! Réapparition dans " + remainingSeconds + "s."); + XSound.BLOCK_STONE_HIT.play(player, 0.7f, 0.5f); + return; + } + + CustomOre ore = placed.getOreType(); + if (ore == null) return; + + // Tool & Enchantment checks + ItemStack handItem = player.getInventory().getItemInMainHand(); + int fortuneLevel = 0; + if (handItem.hasItemMeta() && handItem.getItemMeta().hasEnchant(Enchantment.LOOT_BONUS_BLOCKS)) { + fortuneLevel = handItem.getEnchantmentLevel(Enchantment.LOOT_BONUS_BLOCKS); + } + + // Calculate and drop items + Location dropLoc = block.getLocation().clone().add(0.5, 0.5, 0.5); + for (OreDrop drop : ore.getDrops()) { + ItemStack dropItem = drop.calculateDrop(fortuneLevel, false); + if (dropItem != null && dropItem.getType() != Material.AIR) { + block.getWorld().dropItemNaturally(dropLoc, dropItem); + } + } + + // Visual & audio feedback for hit + try { + block.getWorld().spawnParticle(Particle.CRIT, dropLoc, 8, 0.2, 0.2, 0.2, 0.05); + } catch (Throwable ignored) {} + XSound.BLOCK_STONE_BREAK.play(player.getLocation(), 0.8f, 1.2f); + XSound.ENTITY_EXPERIENCE_ORB_PICKUP.play(player.getLocation(), 0.5f, 1.5f); + + // Decrement stock + boolean justDepleted = placed.decrementStock(); + + if (justDepleted) { + // Transform block to tempBlock (e.g. Bedrock) + block.setType(ore.parseTempBlockMaterial()); + + // Visual effects for depletion + try { + block.getWorld().spawnParticle(Particle.SMOKE_LARGE, dropLoc, 10, 0.2, 0.2, 0.2, 0.02); + } catch (Throwable ignored) {} + XSound.BLOCK_ANVIL_LAND.play(player.getLocation(), 0.6f, 0.8f); + + TextUtil.sendActionBar(player, "⛏ " + ore.getDisplayName() + " | Épuisé ! (" + ore.getRespawnTimeSeconds() + "s)"); + } else { + // Stock remaining feedback + int current = placed.getCurrentStock(); + int total = ore.getStock(); + String stockColor = current > (total / 2) ? "" : ""; + TextUtil.sendActionBar(player, "⛏ " + ore.getDisplayName() + " | Stock: " + stockColor + current + "/" + total + ""); + } + + // Update database state asynchronously + databaseManager.updatePlacedBlockAsync(placed); + } +} diff --git a/src/main/java/fr/luc/gamingcore/mining/manager/RespawnScheduler.java b/src/main/java/fr/luc/gamingcore/mining/manager/RespawnScheduler.java new file mode 100644 index 0000000..c3b1dc6 --- /dev/null +++ b/src/main/java/fr/luc/gamingcore/mining/manager/RespawnScheduler.java @@ -0,0 +1,83 @@ +package fr.luc.gamingcore.mining.manager; + +import com.cryptomorin.xseries.XSound; +import fr.luc.gamingcore.GamingCore; +import fr.luc.gamingcore.mining.model.BlockLocation; +import fr.luc.gamingcore.mining.model.PlacedOreBlock; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.Particle; +import org.bukkit.block.Block; +import org.bukkit.scheduler.BukkitRunnable; +import org.bukkit.scheduler.BukkitTask; + +public class RespawnScheduler { + + private final GamingCore plugin; + private final OreManager oreManager; + private BukkitTask task; + + public RespawnScheduler(GamingCore plugin, OreManager oreManager) { + this.plugin = plugin; + this.oreManager = oreManager; + } + + public void start() { + stop(); + // Check every second (20 ticks) + this.task = new BukkitRunnable() { + @Override + public void run() { + checkRespawnQueue(); + } + }.runTaskTimer(plugin, 20L, 20L); + } + + public void stop() { + if (task != null && !task.isCancelled()) { + task.cancel(); + task = null; + } + } + + public void checkRespawnQueue() { + long now = System.currentTimeMillis(); + + for (PlacedOreBlock placed : oreManager.getAllPlacedBlocks()) { + if (!placed.isDepleted() || placed.getNextRespawnTimestamp() <= 0) { + continue; + } + + if (now >= placed.getNextRespawnTimestamp()) { + respawnBlock(placed); + } + } + } + + private void respawnBlock(PlacedOreBlock placed) { + BlockLocation blockLoc = placed.getLocation(); + Location loc = blockLoc.toLocation(); + if (loc == null || loc.getWorld() == null || !loc.getWorld().isChunkLoaded(loc.getBlockX() >> 4, loc.getBlockZ() >> 4)) { + // World not loaded or chunk not loaded: reset state anyway so it appears correct when chunk loads + placed.respawn(); + oreManager.getDatabaseManager().updatePlacedBlockAsync(placed); + return; + } + + Block block = loc.getBlock(); + Material oreMat = placed.getOreType().parseBlockMaterial(); + block.setType(oreMat); + + // Respawn state in memory & database + placed.respawn(); + oreManager.getDatabaseManager().updatePlacedBlockAsync(placed); + + // Visual & audio effects + try { + loc.getWorld().spawnParticle(Particle.VILLAGER_HAPPY, loc.clone().add(0.5, 0.5, 0.5), 15, 0.3, 0.3, 0.3, 0.1); + } catch (Throwable ignored) {} + + XSound.BLOCK_STONE_PLACE.play(loc, 1.0f, 1.2f); + } +} diff --git a/src/main/java/fr/luc/gamingcore/mining/model/BlockLocation.java b/src/main/java/fr/luc/gamingcore/mining/model/BlockLocation.java new file mode 100644 index 0000000..a84acff --- /dev/null +++ b/src/main/java/fr/luc/gamingcore/mining/model/BlockLocation.java @@ -0,0 +1,82 @@ +package fr.luc.gamingcore.mining.model; + +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.block.Block; + +import java.util.Objects; + +public final class BlockLocation { + + private final String worldName; + private final int x; + private final int y; + private final int z; + + public BlockLocation(String worldName, int x, int y, int z) { + this.worldName = worldName != null ? worldName : "world"; + this.x = x; + this.y = y; + this.z = z; + } + + public static BlockLocation fromLocation(Location loc) { + if (loc == null || loc.getWorld() == null) { + throw new IllegalArgumentException("Location and World cannot be null"); + } + return new BlockLocation(loc.getWorld().getName(), loc.getBlockX(), loc.getBlockY(), loc.getBlockZ()); + } + + public static BlockLocation fromBlock(Block block) { + if (block == null) { + throw new IllegalArgumentException("Block cannot be null"); + } + return fromLocation(block.getLocation()); + } + + public Location toLocation() { + World world = Bukkit.getWorld(worldName); + if (world == null) return null; + return new Location(world, x, y, z); + } + + public Block getBlock() { + Location loc = toLocation(); + return loc != null ? loc.getBlock() : null; + } + + public String getWorldName() { + return worldName; + } + + public int getX() { + return x; + } + + public int getY() { + return y; + } + + public int getZ() { + return z; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + BlockLocation that = (BlockLocation) o; + return x == that.x && y == that.y && z == that.z && Objects.equals(worldName, that.worldName); + } + + @Override + public int hashCode() { + return Objects.hash(worldName, x, y, z); + } + + @Override + public String toString() { + return worldName + ":" + x + "," + y + "," + z; + } +} diff --git a/src/main/java/fr/luc/gamingcore/mining/model/CustomOre.java b/src/main/java/fr/luc/gamingcore/mining/model/CustomOre.java new file mode 100644 index 0000000..4705162 --- /dev/null +++ b/src/main/java/fr/luc/gamingcore/mining/model/CustomOre.java @@ -0,0 +1,97 @@ +package fr.luc.gamingcore.mining.model; + +import com.cryptomorin.xseries.XMaterial; +import org.bukkit.Material; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class CustomOre { + + private final String id; + private final String displayName; + private final XMaterial blockMaterial; + private final double hardness; + private final OreRarity rarity; + private final int stock; + private final boolean respawnable; + private final int respawnTimeSeconds; + private final XMaterial tempBlockMaterial; + private final List drops; + + public CustomOre(String id, String displayName, XMaterial blockMaterial, double hardness, + OreRarity rarity, int stock, boolean respawnable, int respawnTimeSeconds, + XMaterial tempBlockMaterial, List drops) { + this.id = id; + this.displayName = displayName != null ? displayName : id; + this.blockMaterial = blockMaterial != null ? blockMaterial : XMaterial.STONE; + this.hardness = Math.max(0.1, hardness); + this.rarity = rarity != null ? rarity : OreRarity.COMMON; + this.stock = Math.max(1, stock); + this.respawnable = respawnable; + this.respawnTimeSeconds = Math.max(1, respawnTimeSeconds); + this.tempBlockMaterial = tempBlockMaterial != null ? tempBlockMaterial : XMaterial.BEDROCK; + this.drops = drops != null ? new ArrayList<>(drops) : new ArrayList<>(); + } + + public String getId() { + return id; + } + + public String getDisplayName() { + return displayName; + } + + public XMaterial getBlockMaterial() { + return blockMaterial; + } + + public Material parseBlockMaterial() { + Material mat = blockMaterial.parseMaterial(); + return mat != null ? mat : Material.STONE; + } + + public double getHardness() { + return hardness; + } + + public OreRarity getRarity() { + return rarity; + } + + public int getStock() { + return stock; + } + + public boolean isRespawnable() { + return respawnable; + } + + public int getRespawnTimeSeconds() { + return respawnTimeSeconds; + } + + public XMaterial getTempBlockMaterial() { + return tempBlockMaterial; + } + + public Material parseTempBlockMaterial() { + Material mat = tempBlockMaterial.parseMaterial(); + return mat != null ? mat : Material.BEDROCK; + } + + public List getDrops() { + return Collections.unmodifiableList(drops); + } + + public void addDrop(OreDrop drop) { + if (drop != null) { + this.drops.add(drop); + } + } + + public void clearDrops() { + this.drops.clear(); + } +} diff --git a/src/main/java/fr/luc/gamingcore/mining/model/OreDrop.java b/src/main/java/fr/luc/gamingcore/mining/model/OreDrop.java new file mode 100644 index 0000000..b376044 --- /dev/null +++ b/src/main/java/fr/luc/gamingcore/mining/model/OreDrop.java @@ -0,0 +1,104 @@ +package fr.luc.gamingcore.mining.model; + +import com.cryptomorin.xseries.XMaterial; +import org.bukkit.inventory.ItemStack; + +import java.util.concurrent.ThreadLocalRandom; + +public class OreDrop { + + private final int id; + private final String oreId; + private final XMaterial itemMaterial; + private final int defaultAmount; + private final double defaultChance; + private final boolean fortunable; + private final double fortuneMultiplier; + private final boolean smeltable; + private final XMaterial smeltedMaterial; + + public OreDrop(int id, String oreId, XMaterial itemMaterial, int defaultAmount, double defaultChance, + boolean fortunable, double fortuneMultiplier, boolean smeltable, XMaterial smeltedMaterial) { + this.id = id; + this.oreId = oreId; + this.itemMaterial = itemMaterial != null ? itemMaterial : XMaterial.COBBLESTONE; + this.defaultAmount = Math.max(1, defaultAmount); + this.defaultChance = Math.min(1.0, Math.max(0.0, defaultChance)); + this.fortunable = fortunable; + this.fortuneMultiplier = fortuneMultiplier; + this.smeltable = smeltable; + this.smeltedMaterial = smeltedMaterial; + } + + public int getId() { + return id; + } + + public String getOreId() { + return oreId; + } + + public XMaterial getItemMaterial() { + return itemMaterial; + } + + public int getDefaultAmount() { + return defaultAmount; + } + + public double getDefaultChance() { + return defaultChance; + } + + public boolean isFortunable() { + return fortunable; + } + + public double getFortuneMultiplier() { + return fortuneMultiplier; + } + + public boolean isSmeltable() { + return smeltable; + } + + public XMaterial getSmeltedMaterial() { + return smeltedMaterial; + } + + /** + * Calculates the drop based on chance, fortune level, and auto-smelt status. + * + * @param fortuneLevel Level of Fortune enchantment on tool (0 if none) + * @param autoSmelt Whether auto-smelt is active + * @return ItemStack if drop was successful, or null if chance roll failed + */ + public ItemStack calculateDrop(int fortuneLevel, boolean autoSmelt) { + double roll = ThreadLocalRandom.current().nextDouble(); + if (roll > defaultChance) { + return null; // Failed drop chance roll + } + + int amount = defaultAmount; + if (fortunable && fortuneLevel > 0) { + // Vanilla-like fortune bonus calculation with customizable multiplier + int bonus = ThreadLocalRandom.current().nextInt(fortuneLevel + 2) - 1; + if (bonus > 0) { + amount += (int) Math.round(bonus * Math.max(1.0, fortuneMultiplier)); + } + } + + XMaterial targetMaterial = itemMaterial; + if (smeltable && autoSmelt && smeltedMaterial != null && smeltedMaterial.isSupported()) { + targetMaterial = smeltedMaterial; + } + + ItemStack item = targetMaterial.parseItem(); + if (item == null) { + return null; + } + + item.setAmount(Math.max(1, amount)); + return item; + } +} diff --git a/src/main/java/fr/luc/gamingcore/mining/model/OreRarity.java b/src/main/java/fr/luc/gamingcore/mining/model/OreRarity.java new file mode 100644 index 0000000..a06a2fc --- /dev/null +++ b/src/main/java/fr/luc/gamingcore/mining/model/OreRarity.java @@ -0,0 +1,41 @@ +package fr.luc.gamingcore.mining.model; + +public enum OreRarity { + COMMON("Commun", "Commun", "[Commun]"), + UNCOMMON("Peu Commun", "Peu Commun", "[Peu Commun]"), + RARE("Rare", "Rare", "[Rare]"), + EPIC("Épique", "Épique", "[Épique]"), + LEGENDARY("Légendaire", "Légendaire", "[Légendaire]"), + MYTHIC("Mythique", "Mythique", "[Mythique]"); + + private final String displayName; + private final String formattedName; + private final String tag; + + OreRarity(String displayName, String formattedName, String tag) { + this.displayName = displayName; + this.formattedName = formattedName; + this.tag = tag; + } + + public String getDisplayName() { + return displayName; + } + + public String getFormattedName() { + return formattedName; + } + + public String getTag() { + return tag; + } + + public static OreRarity fromString(String name) { + if (name == null) return COMMON; + try { + return OreRarity.valueOf(name.toUpperCase()); + } catch (IllegalArgumentException e) { + return COMMON; + } + } +} diff --git a/src/main/java/fr/luc/gamingcore/mining/model/PlacedOreBlock.java b/src/main/java/fr/luc/gamingcore/mining/model/PlacedOreBlock.java new file mode 100644 index 0000000..b6ca039 --- /dev/null +++ b/src/main/java/fr/luc/gamingcore/mining/model/PlacedOreBlock.java @@ -0,0 +1,100 @@ +package fr.luc.gamingcore.mining.model; + +public class PlacedOreBlock { + + private int id; + private final String oreId; + private CustomOre oreType; + private final BlockLocation location; + private int currentStock; + private boolean depleted; + private long nextRespawnTimestamp; + + public PlacedOreBlock(int id, String oreId, CustomOre oreType, BlockLocation location, + int currentStock, boolean depleted, long nextRespawnTimestamp) { + this.id = id; + this.oreId = oreId; + this.oreType = oreType; + this.location = location; + this.currentStock = currentStock; + this.depleted = depleted; + this.nextRespawnTimestamp = nextRespawnTimestamp; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getOreId() { + return oreId; + } + + public CustomOre getOreType() { + return oreType; + } + + public void setOreType(CustomOre oreType) { + this.oreType = oreType; + } + + public BlockLocation getLocation() { + return location; + } + + public int getCurrentStock() { + return currentStock; + } + + public void setCurrentStock(int currentStock) { + this.currentStock = currentStock; + } + + public boolean isDepleted() { + return depleted; + } + + public void setDepleted(boolean depleted) { + this.depleted = depleted; + } + + public long getNextRespawnTimestamp() { + return nextRespawnTimestamp; + } + + public void setNextRespawnTimestamp(long nextRespawnTimestamp) { + this.nextRespawnTimestamp = nextRespawnTimestamp; + } + + /** + * Decrements the stock by 1 and checks if it reached 0. + * + * @return true if the ore has just been depleted + */ + public boolean decrementStock() { + this.currentStock--; + if (this.currentStock <= 0) { + this.currentStock = 0; + this.depleted = true; + if (this.oreType != null && this.oreType.isRespawnable()) { + this.nextRespawnTimestamp = System.currentTimeMillis() + (this.oreType.getRespawnTimeSeconds() * 1000L); + } + return true; + } + return false; + } + + /** + * Resets the stock and removes depleted flag on respawn. + */ + public void respawn() { + this.depleted = false; + this.nextRespawnTimestamp = 0L; + if (this.oreType != null) { + this.currentStock = this.oreType.getStock(); + } + } +} diff --git a/src/main/java/fr/luc/gamingcore/util/TextUtil.java b/src/main/java/fr/luc/gamingcore/util/TextUtil.java new file mode 100644 index 0000000..b89cb92 --- /dev/null +++ b/src/main/java/fr/luc/gamingcore/util/TextUtil.java @@ -0,0 +1,65 @@ +package fr.luc.gamingcore.util; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +public final class TextUtil { + + private static final MiniMessage MINI_MESSAGE = MiniMessage.miniMessage(); + private static final LegacyComponentSerializer AMPERSAND_SERIALIZER = LegacyComponentSerializer.legacyAmpersand(); + + private static String prefix = "[GamingCore] » "; + + private TextUtil() {} + + public static void setPrefix(String newPrefix) { + if (newPrefix != null) { + prefix = newPrefix; + } + } + + public static String getPrefix() { + return prefix; + } + + /** + * Parses a string formatted with MiniMessage or legacy & color codes into a Component. + */ + public static Component parse(String input) { + if (input == null || input.isEmpty()) { + return Component.empty(); + } + String formatted = input.replace('§', '&'); + if (formatted.contains("&")) { + return AMPERSAND_SERIALIZER.deserialize(formatted); + } + return MINI_MESSAGE.deserialize(input); + } + + /** + * Sends a parsed message with prefix to a CommandSender (or Player). + */ + public static void sendMessage(CommandSender sender, String message) { + if (sender == null || message == null) return; + sender.sendMessage(parse(prefix + message)); + } + + /** + * Sends a parsed raw message without prefix to a CommandSender. + */ + public static void sendRawMessage(CommandSender sender, String message) { + if (sender == null || message == null) return; + sender.sendMessage(parse(message)); + } + + /** + * Sends an action bar message to a player. + */ + public static void sendActionBar(Player player, String message) { + if (player == null || message == null) return; + player.sendActionBar(parse(message)); + } +} diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 7a7e94a..39bc477 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -3,4 +3,20 @@ version: 1.0.0 main: fr.luc.gamingcore.GamingCore api-version: '1.16' author: Luc -description: GamingCore Plugin +description: GamingCore Plugin - Système de Minage Personnalisé + +commands: + gamingcore: + description: Commande principale de GamingCore + aliases: [gc, mining] + permission: gamingcore.use + usage: / [help|ore|reload] + +permissions: + gamingcore.use: + description: Permet d'utiliser les commandes de base de GamingCore + default: true + gamingcore.admin: + description: Permet d'administrer les minerais et recharger le plugin + default: op + diff --git a/src/test/java/fr/luc/gamingcore/mining/OreModelTest.java b/src/test/java/fr/luc/gamingcore/mining/OreModelTest.java new file mode 100644 index 0000000..0460c0d --- /dev/null +++ b/src/test/java/fr/luc/gamingcore/mining/OreModelTest.java @@ -0,0 +1,58 @@ +package fr.luc.gamingcore.mining; + +import com.cryptomorin.xseries.XMaterial; +import fr.luc.gamingcore.mining.model.*; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; + +import static org.junit.jupiter.api.Assertions.*; + +public class OreModelTest { + + @Test + public void testOreStockAndDepletion() { + CustomOre ore = new CustomOre("test_iron", "Test Iron", XMaterial.IRON_ORE, 3.0, + OreRarity.COMMON, 3, true, 10, XMaterial.BEDROCK, new ArrayList<>()); + + BlockLocation loc = new BlockLocation("world", 100, 64, -200); + PlacedOreBlock placed = new PlacedOreBlock(1, ore.getId(), ore, loc, ore.getStock(), false, 0); + + assertEquals(3, placed.getCurrentStock()); + assertFalse(placed.isDepleted()); + + // First hit + boolean depleted1 = placed.decrementStock(); + assertFalse(depleted1); + assertEquals(2, placed.getCurrentStock()); + + // Second hit + boolean depleted2 = placed.decrementStock(); + assertFalse(depleted2); + assertEquals(1, placed.getCurrentStock()); + + // Third hit -> Depleted + boolean depleted3 = placed.decrementStock(); + assertTrue(depleted3); + assertEquals(0, placed.getCurrentStock()); + assertTrue(placed.isDepleted()); + assertTrue(placed.getNextRespawnTimestamp() > System.currentTimeMillis()); + + // Respawn + placed.respawn(); + assertFalse(placed.isDepleted()); + assertEquals(3, placed.getCurrentStock()); + assertEquals(0L, placed.getNextRespawnTimestamp()); + } + + @Test + public void testBlockLocationEquality() { + BlockLocation loc1 = new BlockLocation("world", 10, 20, 30); + BlockLocation loc2 = new BlockLocation("world", 10, 20, 30); + BlockLocation loc3 = new BlockLocation("nether", 10, 20, 30); + + assertEquals(loc1, loc2); + assertEquals(loc1.hashCode(), loc2.hashCode()); + assertNotEquals(loc1, loc3); + } +}