Compare commits

...
17 Commits
Author SHA1 Message Date
YouHaveTrouble 54eadb13c2 adjust the mining visualisation when shifting 2026-10-01 16:10:57 +02:00
YouHaveTrouble 07d086b26f fix tunnelling breaking visualisation when gamerule advance_time is set to false 2026-10-01 16:00:56 +02:00
YouHaveTrouble 6335252b8a update docs once more 2026-09-29 19:37:37 +02:00
YouHaveTrouble fd69600b3e fix disabling visualiser breaking the functionality 2026-09-29 19:30:39 +02:00
YouHaveTrouble 17e183669f cancel the visualise task for the player if they left 2026-09-29 19:30:05 +02:00
YouHaveTrouble e0f5b3a0d7 update documentation with information about Tunnelling 2026-09-29 19:19:57 +02:00
YouHaveTrouble 6ab3d47699 bump version 2026-09-29 19:12:48 +02:00
YouHaveTrouble 21946bfe3b avoid pdc since there's already a map of data about multimining 2026-09-29 19:02:36 +02:00
YouHaveTrouble 0f628fb3fc visualize breaking blocks with tunnelling 2026-09-29 18:42:33 +02:00
YouHaveTrouble c769a0905e switch to less intensive event for block face check 2026-09-29 15:51:14 +02:00
YouHaveTrouble b93718cce2 fix minor bugs related to tools enchanted with tunnelling breaking 2026-09-28 21:52:09 +02:00
YouHaveTrouble b3998f8937 more defaults for tunnelling 2026-09-28 20:27:48 +02:00
YouHaveTrouble 7f530a1e87 update workflows 2026-09-28 17:40:57 +02:00
YouHaveTrouble e62bff7c73 update package lock 2026-09-28 17:30:39 +02:00
YouHaveTrouble 9050004dae Revert "don't track package-lock"
This reverts commit 0f20ce8520.
2026-09-28 17:29:28 +02:00
YouHaveTrouble 3f76e0a431 update the docs 2026-09-28 17:28:36 +02:00
YouHaveTrouble 0f20ce8520 don't track package-lock 2026-09-28 17:17:20 +02:00
11 changed files with 2522 additions and 3627 deletions
+3 -3
View File
@@ -10,13 +10,13 @@ jobs:
build-commit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Set up JDK
uses: actions/setup-java@v4
uses: actions/setup-java@v6
with:
java-version: 21
java-version: '21'
distribution: 'temurin'
- name: Set short commit hash
run: fullCommit=${{ github.sha }} && echo "SHORT_SHA=${fullCommit:0:8}" >> $GITHUB_ENV
+3 -3
View File
@@ -16,9 +16,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v4
uses: actions/checkout@v7
- name: Build Astro page
uses: withastro/action@v4
uses: withastro/action@v6
with:
path: ./docs
node-version: '24'
@@ -31,4 +31,4 @@ jobs:
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
uses: actions/deploy-pages@v5
+1 -1
View File
@@ -11,7 +11,7 @@ export default defineConfig({
sidebar: [
{
label: 'Documentation',
autogenerate: { directory: 'documentation' },
items: [{ autogenerate: { "directory": "documentation" } }]
},
{
label: 'Configuration',
+2317 -3546
View File
File diff suppressed because it is too large Load Diff
+2 -3
View File
@@ -10,8 +10,7 @@
"astro": "astro"
},
"dependencies": {
"@astrojs/starlight": "^0.34.3",
"astro": "^5.6.1",
"sharp": "^0.32.5"
"@astrojs/starlight": "^0.42.4",
"astro": "^7.2.10"
}
}
@@ -4,11 +4,41 @@ description: How to specify common input types in configuration
---
## Tags
There are 2 types of tags that can be used in the configuration:
- `minecraft:item_name` - For specific items, use namespaced vanilla item id. If namespace is not provided, `minecraft` is assumed.
- `#minecraft:item_tag_name` - For <a href="https://minecraft.wiki/w/Item_tag_(Java_Edition)" target="_blank">item tags</a>,
use namespaced tag id. If namespace is not provided, `minecraft` is assumed. Custom tags from other plugins and
datapacks are also supported.
### Items
#### Item reference
Example: `minecraft:stick`
For specific items, use namespaced vanilla item id. If namespace is not provided, `minecraft` is assumed.
#### Item tag
Example: `#minecraft:axes`
For <a href="https://minecraft.wiki/w/Item_tag_(Java_Edition)" target="_blank">item tags</a>, use namespaced tag id. If
namespace is not provided, `minecraft` is assumed. Custom tags from other plugins and datapacks are also supported.
### Blocks
#### Block reference
Example: `minecraft:dirt`
For specific items, use namespaced vanilla item id. If namespace is not provided, `minecraft` is assumed.
#### Block tag
Example: `#minecraft:planks`
For <a href="https://minecraft.wiki/w/Block_tag_(Java_Edition)" target="_blank">block tags</a>, use namespaced tag id. If
namespace is not provided, `minecraft` is assumed. Custom tags from other plugins and datapacks are also supported.
### Enchantments
#### Enchantment tag
Example: `#minecraft:in_enchanting_table`
For <a href="https://minecraft.wiki/w/Enchantment_tag_(Java_Edition)" target="_blank">enchantment tags</a>, use namespaced tag id. If
namespace is not provided, `minecraft` is assumed. Custom tags from other plugins and datapacks are also supported.
## Slot types
- `ANY` - Any slot.
@@ -18,8 +18,8 @@ Cost that will be added to the item when adding the enchantment to it in anvil.
- **Type**: `int`
Weight of the enchantment. Used if `canGetFromEnchantingTable` is set to true. Higher numbers mean it will come up more
frequently in the enchanting table.
Weight of the enchantment. Higher numbers mean it will come up more frequently in the enchanting table. Used if
`#minecraft:in_enchanting_table` is present in enchantment tags.
### minimumCost
@@ -29,7 +29,7 @@ Minimum cost that will be required for the enchantment to show up in the enchant
- **Type**: `int`
Base cost of the enchant.
Base cost of the enchantment.
#### perLevel
@@ -45,7 +45,7 @@ Maximum cost that will be required for the enchantment to show up in the enchant
- **Type**: `int`
Base cost of the enchant.
Base cost of the enchantment.
#### perLevel
@@ -55,23 +55,22 @@ Cost to add per level of the enchantment.
### enchantmentTags
- **Type**: `key[]`
- **Type**: [`Enchantment Tags`](/configuration/input-types/#enchantment-tag)
This is list of [tags](/configuration/input-types#tags) that the enchantment will be tagged with. This can be used to put the enchantment in enchanting table,
mark it as a curse, etc. <a href="https://minecraft.wiki/w/Enchantment_tag_(Java_Edition)" target="_blank">All vanilla</a>
and custom tags are supported.
This is list of tags that the enchantment will be tagged with. This can be used to put the enchantment in enchanting table,
mark it as a curse, etc.
### supportedItemTags
- **Type**: `key[]`
- **Type**: [`Item Tags`](/configuration/input-types/#items)
[Items and/or item tags](/configuration/input-types#tags) that the enchantment can be applied to.
Items and/or item tags that the enchantment can be applied to.
### activeSlots
- **Type**: `string[]`
- **Type**: [`Slot types`](/configuration/input-types/#slot-types)
[Slots](/configuration/input-types#slot-types) that the enchantment will be active in.
Inventory slots that the enchantment will be active in.
### enabled
@@ -96,9 +95,11 @@ of the enchantment does not support multiple levels.
## Telepathy
### onlyUserCanPickupItems
- **Type**: `boolean`
If set to true, items teleported by the enchant will only be able to be picked up by the player that broke the block.
If set to true, items teleported by the enchantment will only be able to be picked up by the player that broke the block.
## Executioner
@@ -155,6 +156,26 @@ Cooldown in ticks that will be applied to the activating items cooldown group.
Namespaced key of the sound that will play when enchantment blocks a hit.
## Tunnelling
### affectedBlockTags
- **Type**: [`Block Tags`](/configuration/input-types/#blocks)
List of blocks and block tags that will be affected by the enchantment's mining effect.
### blocksPerLevel
- **Type**: `int`
How many blocks in each direction enchantment should mine per enchantment level.
### visualizeBreaking
- **Type**: `boolean`
Display visualisation of blocks being broken by the enchantment along with breaking the base block.
## Panic
### panicChancePerLevel
@@ -73,6 +73,14 @@ Automatically blocks hits, but triggers a configurable cooldown every time it do
the enchanted items cooldown group, so if you make this enchantment support more than just 1 item, you're going to have
to handle applying the cooldown group to them yourself.
### Tunnelling
**Translation key**: `enchantio.enchantment.tunnelling`
**Description**:
Mines multiple blocks around initially destroyed block. Activates when mining affected block and breaks all the affected
blocks in a `range x range x 1` pattern.
## Curses
### Curse of Panic
+1 -1
View File
@@ -15,7 +15,7 @@
<properties>
<java.version>21</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<revision>1.15.2</revision>
<revision>1.16.1</revision>
</properties>
<url>https://youhavetrouble.me</url>
@@ -25,6 +25,7 @@ public class TunnellingEnchant implements EnchantioEnchant {
public static final Key AFFECTED_BLOCKS_KEY = Key.key("enchantio", "affected_by_tunnelling");
private final int anvilCost, weight, maxLevel, blocksPerLevel;
private final boolean visualizeBreaking;
private final EnchantmentRegistryEntry.EnchantmentCost minimumCost;
private final EnchantmentRegistryEntry.EnchantmentCost maximumCost;
private final Set<TagEntry<ItemType>> supportedItemTags = new HashSet<>();
@@ -40,7 +41,8 @@ public class TunnellingEnchant implements EnchantioEnchant {
Collection<TagEntry<ItemType>> supportedItemTags,
Map<Key, Set<TagEntry<BlockType>>> affectedBlockTags,
int maxLevel,
int blocksPerLevel
int blocksPerLevel,
boolean visualizeBreaking
) {
this.anvilCost = anvilCost;
this.weight = weight;
@@ -48,6 +50,7 @@ public class TunnellingEnchant implements EnchantioEnchant {
this.maximumCost = maximumCost;
this.maxLevel = maxLevel;
this.blocksPerLevel = blocksPerLevel;
this.visualizeBreaking = visualizeBreaking;
this.supportedItemTags.addAll(supportedItemTags);
this.enchantTagKeys.addAll(enchantTagKeys);
this.affectedBlockTags.putAll(affectedBlockTags);
@@ -112,6 +115,10 @@ public class TunnellingEnchant implements EnchantioEnchant {
return blocksPerLevel;
}
public boolean shouldVisualizeBreaking() {
return visualizeBreaking;
}
public static TunnellingEnchant create(ConfigurationSection configurationSection) {
TunnellingEnchant tunnellingEnchant = new TunnellingEnchant(
EnchantioConfig.getInt(configurationSection, "anvilCost", 1),
@@ -149,12 +156,15 @@ public class TunnellingEnchant implements EnchantioEnchant {
"#minecraft:glazed_terracotta",
"#minecraft:concrete",
"#minecraft:stone_bricks",
"minecraft:end_stone"
"minecraft:end_stone",
"minecraft:cobblestone",
"minecraft:cobbled_deepslate"
)
))
),
EnchantioConfig.getInt(configurationSection, "maxLevel", 1),
EnchantioConfig.getInt(configurationSection, "blocksPerLevel", 1)
EnchantioConfig.getInt(configurationSection, "blocksPerLevel", 1),
EnchantioConfig.getBoolean(configurationSection, "visualizeBreaking", true)
);
if (EnchantioConfig.getBoolean(configurationSection, "enabled", true)) {
@@ -5,7 +5,6 @@ import io.papermc.paper.registry.RegistryKey;
import io.papermc.paper.registry.keys.tags.BlockTypeTagKeys;
import me.youhavetrouble.enchantio.Enchantio;
import me.youhavetrouble.enchantio.EnchantioConfig;
import me.youhavetrouble.enchantio.enchants.EnchantioEnchant;
import me.youhavetrouble.enchantio.enchants.TunnellingEnchant;
import org.bukkit.*;
import org.bukkit.block.Block;
@@ -16,89 +15,144 @@ 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.Action;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.inventory.EquipmentSlot;
import org.bukkit.event.block.BlockDamageAbortEvent;
import org.bukkit.event.block.BlockDamageEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.event.player.PlayerTeleportEvent;
import org.bukkit.inventory.ItemStack;
import org.bukkit.persistence.PersistentDataType;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
@SuppressWarnings("UnstableApiUsage")
public class TunnellingListener implements Listener {
private final static Set<Block> blockBreakSkips = new HashSet<>();
private final static Map<UUID, BlockBreakData> blockBreakData = new ConcurrentHashMap<>();
private final Enchantment tunnelling = RegistryAccess.registryAccess().getRegistry(RegistryKey.ENCHANTMENT).get(TunnellingEnchant.KEY);
private final NamespacedKey tunnellingBlockFaceKey = new NamespacedKey("enchantio", "tunellingblockface");
private final TunnellingEnchant tunnellingEnchant;
public TunnellingListener() {
if (EnchantioConfig.ENCHANTS.get(TunnellingEnchant.KEY) instanceof TunnellingEnchant enchant) {
this.tunnellingEnchant = enchant;
} else {
throw new RuntimeException("Tunnelling enchantment is not registered, but something tried to create a listener for it. Don't do that.");
}
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onTunnelBlockSideCheck(PlayerInteractEvent event) {
public void onPlayerJoin(PlayerJoinEvent event) {
UUID uuid = event.getPlayer().getUniqueId();
if (!tunnellingEnchant.shouldVisualizeBreaking()) return;
event.getPlayer().getScheduler().runAtFixedRate(Enchantio.getPlugin(Enchantio.class), (task) -> {
Player player = Bukkit.getPlayer(uuid);
if (player == null || !player.isOnline()) {
task.cancel();
return;
}
BlockBreakData data = blockBreakData.get(uuid);
if (data == null) return;
if (player.isSneaking()) {
updateBlockBreakProgress(player, data, 0);
return;
}
long elapsed = System.currentTimeMillis() - data.startedBreakingAt();
long elapsedTicks = elapsed / 50;
float breakSpeed = data.block.getBreakSpeed(player);
float ticksNeeded = 1.0f / breakSpeed;
float progress = Math.min(elapsedTicks / ticksNeeded, 1.0f);
if (progress >= 1.0f) return;
updateBlockBreakProgress(player, data, progress);
}, null, 1, 1);
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onTunnelBlockStartBreak(BlockDamageEvent event) {
if (tunnelling == null) return;
if (event.getHand() != EquipmentSlot.HAND) return;
if (event.getAction() != Action.LEFT_CLICK_BLOCK) return;
Player player = event.getPlayer();
if (player.isSneaking() || GameMode.CREATIVE.equals(player.getGameMode())) return;
ItemStack item = event.getItem();
if (item == null) return;
if (!item.containsEnchantment(tunnelling)) return;
event.getPlayer().getPersistentDataContainer().set(tunnellingBlockFaceKey, PersistentDataType.STRING, event.getBlockFace().toString());
if (GameMode.CREATIVE.equals(player.getGameMode())) return;
ItemStack item = player.getInventory().getItemInMainHand();
if (item.isEmpty()) return;
int enchantLevel = item.getEnchantmentLevel(tunnelling);
if (enchantLevel == -1) return;
if (blockBreakData.containsKey(player.getUniqueId())) return;
Block block = event.getBlock();
Set<Block> blocksToBreak = getSquare(block, event.getBlockFace(), tunnellingEnchant.getBlocksPerLevel() * enchantLevel);
blockBreakData.put(player.getUniqueId(), new BlockBreakData(block, System.currentTimeMillis(), event.getBlockFace(), blocksToBreak));
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onTunnelBlockStopBreak(BlockDamageAbortEvent event) {
BlockBreakData data = blockBreakData.remove(event.getPlayer().getUniqueId());
if (data == null) return;
updateBlockBreakProgress(event.getPlayer(), data, 0f);
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onPlayerQuit(PlayerQuitEvent event) {
blockBreakData.remove(event.getPlayer().getUniqueId());
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onPlayerTpToAnotherWorld(PlayerTeleportEvent event) {
Player player = event.getPlayer();
BlockBreakData data = blockBreakData.get(player.getUniqueId());
if (data == null) return;
if (data.block.getWorld().getUID().equals(player.getWorld().getUID())) return;
blockBreakData.remove(player.getUniqueId());
}
@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
public void onTunnelBlockBreak(BlockBreakEvent event) {
if (tunnelling == null) return;
Player player = event.getPlayer();
Block block = event.getBlock();
BlockBreakData data = blockBreakData.remove(player.getUniqueId());
if (blockBreakSkips.contains(block)) return;
if (data != null) updateBlockBreakProgress(player, data, 0f);
if (GameMode.CREATIVE.equals(player.getGameMode())) return;
if (player.isSneaking()) return;
String rawFace = player.getPersistentDataContainer().get(tunnellingBlockFaceKey, PersistentDataType.STRING);
if (rawFace == null) return;
EnchantioEnchant enchant = EnchantioConfig.ENCHANTS.get(TunnellingEnchant.KEY);
if (!(enchant instanceof TunnellingEnchant tunnellingEnchant)) return;
ItemStack item = event.getPlayer().getInventory().getItemInMainHand();
int enchantLevel = item.getEnchantmentLevel(tunnelling);
if (enchantLevel <= 0) return;
Block block = event.getBlock();
BlockType blockType = block.getType().asBlockType();
if (!Registry.BLOCK.getTagValues(BlockTypeTagKeys.create(TunnellingEnchant.AFFECTED_BLOCKS_KEY)).contains(blockType)) return;
if (blockBreakSkips.contains(block)) return;
BlockFace blockFace;
try {
blockFace = BlockFace.valueOf(rawFace);
} catch (IllegalArgumentException e) {
return;
}
Set<Block> blocksToBreak = getSquare(block, blockFace, tunnellingEnchant.getBlocksPerLevel() * enchantLevel);
if (data == null) return;
Set<Block> blocksToBreak = getSquare(block, data.blockFace, tunnellingEnchant.getBlocksPerLevel() * enchantLevel);
blockBreakSkips.addAll(blocksToBreak);
for (Block b : blocksToBreak) {
if (player.getInventory().getItemInMainHand().isEmpty()) {
event.setCancelled(true);
blockBreakSkips.removeAll(blocksToBreak);
break;
}
event.getPlayer().breakBlock(b);
blockBreakSkips.remove(b);
}
}
private void updateBlockBreakProgress(Player player, BlockBreakData data, float progress) {
if (!tunnellingEnchant.shouldVisualizeBreaking()) return;
int id = Integer.MIN_VALUE;
for (Block block : data.blocksToSyncDamage) {
player.sendBlockDamage(block.getLocation(), progress, id++);
}
}
private Set<Block> getSquare(Block block, BlockFace face, int range) {
Set<Block> blocks = new HashSet<>();
switch (face) {
case DOWN:
case UP:
blocks.addAll(getBlocksToTunnel(block.getLocation(), range, 0, range));
break;
case EAST:
case WEST:
blocks.addAll(getBlocksToTunnel(block.getLocation(), 0, range, range));
break;
case NORTH:
case SOUTH:
blocks.addAll(getBlocksToTunnel(block.getLocation(), range, range, 0));
break;
default:
break;
case DOWN, UP -> {
return getBlocksToTunnel(block.getLocation(), range, 0, range);
}
case EAST, WEST -> {
return getBlocksToTunnel(block.getLocation(), 0, range, range);
}
case NORTH, SOUTH -> {
return getBlocksToTunnel(block.getLocation(), range, range, 0);
}
}
return blocks;
return new HashSet<>();
}
/**
@@ -106,8 +160,8 @@ public class TunnellingListener implements Listener {
* @param base location at the centre of selection
* @return All blocks that will get broken
*/
private List<Block> getBlocksToTunnel(Location base, int changeX, int changeY, int changeZ) {
List<Block> blocks = new ArrayList<>();
private Set<Block> getBlocksToTunnel(Location base, int changeX, int changeY, int changeZ) {
Set<Block> blocks = new HashSet<>();
for (int x = (base.getBlockX() - changeX); x <= (base.getBlockX() + changeX); x++) {
for (int y = (base.getBlockY() - changeY); y <= (base.getBlockY() + changeY); y++) {
for (int z = (base.getBlockZ() - changeZ); z <= (base.getBlockZ() + changeZ); z++) {
@@ -124,4 +178,6 @@ public class TunnellingListener implements Listener {
return blocks;
}
private record BlockBreakData(Block block, long startedBreakingAt, BlockFace blockFace, Set<Block> blocksToSyncDamage) {}
}