diff --git a/src/client/java/com/tcm/MineTale/MineTaleDataGen.java b/src/client/java/com/tcm/MineTale/MineTaleDataGen.java index e4bfe19..bc4ace5 100644 --- a/src/client/java/com/tcm/MineTale/MineTaleDataGen.java +++ b/src/client/java/com/tcm/MineTale/MineTaleDataGen.java @@ -1,9 +1,6 @@ package com.tcm.MineTale; -import com.tcm.MineTale.datagen.ModBlockTagProvider; -import com.tcm.MineTale.datagen.ModLangProvider; -import com.tcm.MineTale.datagen.ModModelProvider; -import com.tcm.MineTale.datagen.ModRecipeProvider; +import com.tcm.MineTale.datagen.*; import net.fabricmc.fabric.api.datagen.v1.DataGeneratorEntrypoint; import net.fabricmc.fabric.api.datagen.v1.FabricDataGenerator; @@ -11,10 +8,10 @@ public class MineTaleDataGen implements DataGeneratorEntrypoint { /** - * Initialize a data pack and register the mod's data providers. + * Initialize a data pack and register the mod's data providers for data generation. * - * Creates a data pack from the given Fabric data generator and adds the language - * and model providers so they will run during data generation. + * Registers language, model, recipe, block tag, and loot table providers so they + * will run as part of the Fabric data generation pack created from the given generator. * * @param fabricDataGenerator the Fabric data generator used to create the data pack */ @@ -26,5 +23,6 @@ public void onInitializeDataGenerator(FabricDataGenerator fabricDataGenerator) { pack.addProvider(ModModelProvider::new); pack.addProvider(ModRecipeProvider::new); pack.addProvider(ModBlockTagProvider::new); + pack.addProvider(ModLootTableProvider::new); } } \ No newline at end of file diff --git a/src/client/java/com/tcm/MineTale/datagen/ModBlockTagProvider.java b/src/client/java/com/tcm/MineTale/datagen/ModBlockTagProvider.java index 21e9aec..6c306aa 100644 --- a/src/client/java/com/tcm/MineTale/datagen/ModBlockTagProvider.java +++ b/src/client/java/com/tcm/MineTale/datagen/ModBlockTagProvider.java @@ -1,5 +1,6 @@ package com.tcm.MineTale.datagen; +import com.jcraft.jorbis.Block; import com.tcm.MineTale.registry.ModBlocks; import net.fabricmc.fabric.api.datagen.v1.FabricDataOutput; import net.fabricmc.fabric.api.datagen.v1.provider.FabricTagProvider; @@ -20,14 +21,24 @@ public ModBlockTagProvider(FabricDataOutput output, CompletableFuture registryLookup) { + super(dataOutput, registryLookup); + } + + /** + * Registers loot tables for the mod's workbench blocks. + * + * Each added loot table causes the block to drop itself (one item) only when the block's state + * matches the required DoubleBlockHalf (LOWER) and ChestType (LEFT or SINGLE) for that block, + * and the drop is subject to explosion survival/decay. + */ + @Override + public void generate() { + ///Block Drops Itself + this.add(ModBlocks.ARMORERS_WORKBENCH_BLOCK, + LootTable.lootTable() // Use the static factory method to start the builder + .withPool(LootPool.lootPool() + .setRolls(ConstantValue.exactly(1.0F)) + .add(LootItem.lootTableItem(ModBlocks.ARMORERS_WORKBENCH_BLOCK)) + .when(LootItemBlockStatePropertyCondition.hasBlockStateProperties(ModBlocks.ARMORERS_WORKBENCH_BLOCK) + .setProperties(StatePropertiesPredicate.Builder.properties() + .hasProperty(AbstractWorkbench.HALF, DoubleBlockHalf.LOWER) + .hasProperty(AbstractWorkbench.TYPE, ChestType.LEFT) + ) + ) + .when(ExplosionCondition.survivesExplosion()) + ) + ); + + this.add(ModBlocks.CAMPFIRE_WORKBENCH_BLOCK, + LootTable.lootTable() // Use the static factory method to start the builder + .withPool(LootPool.lootPool() + .setRolls(ConstantValue.exactly(1.0F)) + .add(LootItem.lootTableItem(ModBlocks.CAMPFIRE_WORKBENCH_BLOCK)) + .when(LootItemBlockStatePropertyCondition.hasBlockStateProperties(ModBlocks.CAMPFIRE_WORKBENCH_BLOCK) + .setProperties(StatePropertiesPredicate.Builder.properties() + .hasProperty(AbstractWorkbench.HALF, DoubleBlockHalf.LOWER) + .hasProperty(AbstractWorkbench.TYPE, ChestType.SINGLE) + ) + ) + .when(ExplosionCondition.survivesExplosion()) + ) + ); + + this.add(ModBlocks.WORKBENCH_WORKBENCH_BLOCK, + LootTable.lootTable() // Use the static factory method to start the builder + .withPool(LootPool.lootPool() + .setRolls(ConstantValue.exactly(1.0F)) + .add(LootItem.lootTableItem(ModBlocks.WORKBENCH_WORKBENCH_BLOCK)) + .when(LootItemBlockStatePropertyCondition.hasBlockStateProperties(ModBlocks.WORKBENCH_WORKBENCH_BLOCK) + .setProperties(StatePropertiesPredicate.Builder.properties() + .hasProperty(AbstractWorkbench.HALF, DoubleBlockHalf.LOWER) + .hasProperty(AbstractWorkbench.TYPE, ChestType.LEFT) + ) + ) + .when(ExplosionCondition.survivesExplosion()) + ) + ); + + this.add(ModBlocks.FURNACE_WORKBENCH_BLOCK_T1, + LootTable.lootTable() // Use the static factory method to start the builder + .withPool(LootPool.lootPool() + .setRolls(ConstantValue.exactly(1.0F)) + .add(LootItem.lootTableItem(ModBlocks.FURNACE_WORKBENCH_BLOCK_T1)) + .when(LootItemBlockStatePropertyCondition.hasBlockStateProperties(ModBlocks.FURNACE_WORKBENCH_BLOCK_T1) + .setProperties(StatePropertiesPredicate.Builder.properties() + .hasProperty(AbstractWorkbench.HALF, DoubleBlockHalf.LOWER) + .hasProperty(AbstractWorkbench.TYPE, ChestType.LEFT) + ) + ) + .when(ExplosionCondition.survivesExplosion()) + ) + ); + + this.add(ModBlocks.FURNACE_WORKBENCH_BLOCK_T2, + LootTable.lootTable() // Use the static factory method to start the builder + .withPool(LootPool.lootPool() + .setRolls(ConstantValue.exactly(1.0F)) + .add(LootItem.lootTableItem(ModBlocks.FURNACE_WORKBENCH_BLOCK_T2)) + .when(LootItemBlockStatePropertyCondition.hasBlockStateProperties(ModBlocks.FURNACE_WORKBENCH_BLOCK_T2) + .setProperties(StatePropertiesPredicate.Builder.properties() + .hasProperty(AbstractWorkbench.HALF, DoubleBlockHalf.LOWER) + .hasProperty(AbstractWorkbench.TYPE, ChestType.LEFT) + ) + ) + .when(ExplosionCondition.survivesExplosion()) + ) + ); + } + + +/// For Ore Drops +/** + * Create a loot table builder that drops the specified item in multiple quantities with Silk Touch, Fortune bonus, and explosion decay applied. + * + * @param drop the source block used for Silk Touch dispatch and explosion-decay context + * @param item the item to drop from the ore + * @return a LootTable.Builder that drops `item` in a base count between 2 and 5, augmented by the Fortune enchantment, with Silk Touch handling and explosion decay applied + */ + public LootTable.Builder AverageOreDrops(Block drop, Item item) { + HolderLookup.RegistryLookup impl = this.registries.lookupOrThrow(Registries.ENCHANTMENT); + return this.createSilkTouchDispatchTable(drop, this.applyExplosionDecay(drop, ((LootPoolSingletonContainer.Builder) + LootItem.lootTableItem(item).apply(SetItemCountFunction.setCount(UniformGenerator.between(2, 5)))) + .apply(ApplyBonusCount.addOreBonusCount(impl.getOrThrow(Enchantments.FORTUNE))))); + } + + /** + * Creates a loot table builder for an ore that yields the specified item with Silk Touch and Fortune handling. + * + * The table gives a base drop count of exactly 1 (before Fortune), increases the count with Fortune, returns the + * ore block when mined with Silk Touch, and applies explosion decay to the drop. + * + * @param drop the ore block (returned when Silk Touch is used) + * @param item the item to drop when the ore is mined without Silk Touch + * @return a LootTable.Builder configured to drop the specified item with Fortune bonuses, Silk Touch dispatch, + * and explosion decay + */ + public LootTable.Builder SingleOreDrops(Block drop, Item item) { + HolderLookup.RegistryLookup impl = this.registries.lookupOrThrow(Registries.ENCHANTMENT); + return this.createSilkTouchDispatchTable(drop, this.applyExplosionDecay(drop, ((LootPoolSingletonContainer.Builder) + LootItem.lootTableItem(item).apply(SetItemCountFunction.setCount(ConstantValue.exactly(1)))) + .apply(ApplyBonusCount.addOreBonusCount(impl.getOrThrow(Enchantments.FORTUNE))))); + } + + /** + * Builds a loot table for a "light" ore block that supports Silk Touch, Fortune bonuses, and explosion decay. + * + * @param drop the ore block whose loot table is being created + * @param item the item to drop from the ore when not Silk Touched + * @return a LootTable.Builder that drops {@code item} in quantities of 1–2 (before Fortune), applies Fortune bonus, + * dispatches to Silk Touch drops when applicable, and respects explosion decay + */ + public LootTable.Builder LightOreDrops(Block drop, Item item) { + HolderLookup.RegistryLookup impl = this.registries.lookupOrThrow(Registries.ENCHANTMENT); + return this.createSilkTouchDispatchTable(drop, this.applyExplosionDecay(drop, ((LootPoolSingletonContainer.Builder) + LootItem.lootTableItem(item).apply(SetItemCountFunction.setCount(UniformGenerator.between(1, 2)))) + .apply(ApplyBonusCount.addOreBonusCount(impl.getOrThrow(Enchantments.FORTUNE))))); + } +} \ No newline at end of file diff --git a/src/main/java/com/tcm/MineTale/MineTale.java b/src/main/java/com/tcm/MineTale/MineTale.java index 6d3686d..dac500e 100644 --- a/src/main/java/com/tcm/MineTale/MineTale.java +++ b/src/main/java/com/tcm/MineTale/MineTale.java @@ -1,5 +1,6 @@ package com.tcm.MineTale; +import com.tcm.MineTale.util.ModLootTableModifiers; import net.fabricmc.api.ModInitializer; import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry; import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; @@ -72,6 +73,8 @@ public void onInitialize() { RecipeSynchronization.synchronizeRecipeSerializer(ModRecipes.FURNACE_SERIALIZER); + ModLootTableModifiers.modifyLootTables(); + // Register the payload type and codec so the game knows how to handle it PayloadTypeRegistry.playC2S().register(CraftRequestPayload.TYPE, CraftRequestPayload.CODEC); @@ -129,10 +132,19 @@ public void onInitialize() { }); }); - LOGGER.info("Hello Fabric world!"); + LOGGER.info("MineTale Loaded!"); } - private boolean hasIngredients(ServerPlayer player, WorkbenchRecipe recipe) { + /** + * Determines whether the player (and nearby pullable chests, if the workbench allows) collectively contain all item ingredients required by the given workbench recipe. + * + * This check requires the player's open container to be an AbstractWorkbenchContainerMenu; if it is not, the method returns `false`. It examines the player's non-equipment inventory and, when the workbench permits pulling, the contents of nearby inventories. Each recipe ingredient must be satisfied by a distinct matching item instance from those inventories. + * + * @param player the server player whose inventories are checked + * @param recipe the workbench recipe whose ingredient requirements are being validated + * @return `true` if all ingredients of the recipe can be satisfied from the player and allowed nearby inventories, `false` otherwise + */ + private boolean hasIngredients(ServerPlayer player, WorkbenchRecipe recipe) { if (!(player.containerMenu instanceof AbstractWorkbenchContainerMenu menu)) return false; AbstractWorkbenchEntity be = menu.getBlockEntity(); diff --git a/src/main/java/com/tcm/MineTale/block/workbenches/AbstractWorkbench.java b/src/main/java/com/tcm/MineTale/block/workbenches/AbstractWorkbench.java index fc9e7d6..6c6de09 100644 --- a/src/main/java/com/tcm/MineTale/block/workbenches/AbstractWorkbench.java +++ b/src/main/java/com/tcm/MineTale/block/workbenches/AbstractWorkbench.java @@ -23,6 +23,7 @@ import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.block.state.StateDefinition; import net.minecraft.world.level.block.state.properties.*; +import net.minecraft.world.level.gameevent.GameEvent; import net.minecraft.world.phys.BlockHitResult; import net.minecraft.world.phys.shapes.Shapes; import net.minecraft.world.phys.shapes.VoxelShape; @@ -33,6 +34,8 @@ import java.util.function.Supplier; +// DoorBlock + public abstract class AbstractWorkbench extends BaseEntityBlock { public static final EnumProperty FACING = HorizontalDirectionalBlock.FACING; public static final EnumProperty HALF = BlockStateProperties.DOUBLE_BLOCK_HALF; @@ -351,4 +354,70 @@ protected float getShadeBrightness(BlockState state, BlockGetter level, BlockPos // We want 1.0F to ensure the block doesn't cast a pitch-black shadow on itself. return 1.0F; } + + /** + * Handle teardown of a multi-block workbench when a player destroys one of its parts, + * ensuring the master/slave parts are removed consistently and loot is produced exactly once. + * + *

Server-side behavior: + * - Computes the master (bottom-left) position for the workbench and whether the broken part is the master. + * - Iterates the workbench footprint (2x2 if wide and tall, or the corresponding subset) and removes other parts: + * - If the master is broken, other parts are removed silently (no drops). + * - If a slave is broken, the master is destroyed (producing drops unless the player is in creative) and other slaves are removed silently. + * - Emits GameEvent.BLOCK_DESTROY for each part that is removed. + * - If a slave was broken by a non-creative player, prevents the slave part itself from dropping to avoid duplicate loot. + * + * @param level the world where the destruction occurs + * @param pos the position of the part being destroyed + * @param state the block state of the part being destroyed (may be modified to suppress drops) + * @param player the player performing the destruction + * @return the BlockState returned by the superclass implementation after custom teardown handling + */ + @Override + public BlockState playerWillDestroy(Level level, BlockPos pos, BlockState state, Player player) { + if (!level.isClientSide()) { + BlockPos masterPos = getMasterPos(state, pos); + boolean isMaster = pos.equals(masterPos); + + // Iterate through the entire 2x2 or 1x2 structure + for (int y = 0; y <= (isTall ? 1 : 0); y++) { + for (int x = 0; x <= (isWide ? 1 : 0); x++) { + Direction facing = state.getValue(FACING); + BlockPos targetPos = masterPos.above(y).relative(facing.getClockWise(), x); + + // Skip the block the player is currently mining + if (targetPos.equals(pos)) continue; + + BlockState targetState = level.getBlockState(targetPos); + if (targetState.is(this) && getMasterPos(targetState, targetPos).equals(masterPos)) { + if (isMaster) { + // If we are mining the Master, destroy others SILENTLY + level.setBlock(targetPos, Blocks.AIR.defaultBlockState(), 35); + } else { + // If we are mining a Slave block, we need to handle the Master carefully. + // If the Master is at this targetPos, destroy it WITH drops. + if (targetPos.equals(masterPos)) { + level.destroyBlock(targetPos, !player.isCreative()); + } else { + // Otherwise, it's just another slave block, remove silently. + level.setBlock(targetPos, Blocks.AIR.defaultBlockState(), 35); + } + } + level.gameEvent(GameEvent.BLOCK_DESTROY, targetPos, GameEvent.Context.of(player, targetState)); + } + } + } + + // Final tweak: If the player is mining a SLAVE block, + // we must prevent the slave block itself from dropping. + if (!isMaster && !player.isCreative()) { + // This prevents the current block from dropping its loot table + // because we already triggered the Master's drop above. + state = state.setValue(BlockStateProperties.LIT, false); // Optional: change state to desync loot + level.setBlock(pos, Blocks.AIR.defaultBlockState(), 35); + } + } + + return super.playerWillDestroy(level, pos, state, player); + } } \ No newline at end of file diff --git a/src/main/java/com/tcm/MineTale/registry/ModBlocks.java b/src/main/java/com/tcm/MineTale/registry/ModBlocks.java index 22ac439..f2096e6 100644 --- a/src/main/java/com/tcm/MineTale/registry/ModBlocks.java +++ b/src/main/java/com/tcm/MineTale/registry/ModBlocks.java @@ -28,6 +28,7 @@ import net.minecraft.world.level.material.MapColor; import net.minecraft.world.level.block.Blocks; +import static net.minecraft.world.level.block.Blocks.litBlockEmission; import static net.minecraft.world.level.block.Blocks.logProperties; public class ModBlocks { @@ -59,14 +60,18 @@ public class ModBlocks { public static final Block FURNACE_WORKBENCH_BLOCK_T1 = register( "furnace_workbench_block_t1", (props) -> new FurnaceWorkbench(props, ModTiers.TIER_1), - BlockBehaviour.Properties.of().sound(SoundType.STONE).noOcclusion(), - true + BlockBehaviour.Properties.of().sound(SoundType.STONE).noOcclusion() + .requiresCorrectToolForDrops().strength(3.5F) + .lightLevel(litBlockEmission(13)), + true ); public static final Block FURNACE_WORKBENCH_BLOCK_T2 = register( "furnace_workbench_block_t2", (props) -> new FurnaceWorkbench(props, ModTiers.TIER_2), - BlockBehaviour.Properties.of().sound(SoundType.STONE).noOcclusion(), + BlockBehaviour.Properties.of().sound(SoundType.STONE).noOcclusion() + .requiresCorrectToolForDrops().strength(3.5F) + .lightLevel(litBlockEmission(13)), true ); diff --git a/src/main/java/com/tcm/MineTale/registry/ModItems.java b/src/main/java/com/tcm/MineTale/registry/ModItems.java index 47aef66..7d7a95b 100644 --- a/src/main/java/com/tcm/MineTale/registry/ModItems.java +++ b/src/main/java/com/tcm/MineTale/registry/ModItems.java @@ -3,18 +3,26 @@ import java.util.ArrayList; import java.util.List; import java.util.function.Function; +import java.util.function.UnaryOperator; import com.tcm.MineTale.MineTale; import com.tcm.MineTale.item.ModCreativeTab; import net.fabricmc.fabric.api.itemgroup.v1.ItemGroupEvents; import net.minecraft.core.Registry; +import net.minecraft.core.component.DataComponents; import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.core.registries.Registries; import net.minecraft.resources.Identifier; import net.minecraft.resources.ResourceKey; import net.minecraft.world.food.FoodProperties; +import net.minecraft.world.item.BlockItem; import net.minecraft.world.item.Item; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.component.ItemContainerContents; +import net.minecraft.world.level.block.Block; + +import static net.minecraft.world.item.Items.registerBlock; public class ModItems { diff --git a/src/main/java/com/tcm/MineTale/util/ModLootTableModifiers.java b/src/main/java/com/tcm/MineTale/util/ModLootTableModifiers.java new file mode 100644 index 0000000..80557e4 --- /dev/null +++ b/src/main/java/com/tcm/MineTale/util/ModLootTableModifiers.java @@ -0,0 +1,47 @@ +package com.tcm.MineTale.util; + +import net.fabricmc.fabric.api.loot.v3.LootTableEvents; +import net.minecraft.world.item.Items; +import net.minecraft.resources.Identifier; +import net.minecraft.world.level.storage.loot.LootPool; +import net.minecraft.world.level.storage.loot.entries.LootItem; +import net.minecraft.world.level.storage.loot.functions.SetItemCountFunction; +import net.minecraft.world.level.storage.loot.providers.number.ConstantValue; +import net.minecraft.world.level.storage.loot.providers.number.UniformGenerator; + +public class ModLootTableModifiers { + private static final Identifier SHORT_GRASS_ID = Identifier.fromNamespaceAndPath("minecraft", "blocks/short_grass"); + private static final Identifier TALL_GRASS_ID = Identifier.fromNamespaceAndPath("minecraft", "blocks/tall_grass"); + + /** + * Registers a listener that modifies the loot tables for short and tall grass. + * + *

When the listener sees the short grass loot table, it replaces its pools with a single-roll + * pool that can produce AIR (count exactly 1) and STICK (count between 1 and 3). When the listener + * sees the tall grass loot table, it replaces its pools with a single-roll pool that can produce + * STICK (weight 1, count between 1 and 4). + * + *

This method registers the modification via LootTableEvents.MODIFY. + */ + public static void modifyLootTables() { + LootTableEvents.MODIFY.register((key, tableBuilder, sources, registry) -> { + if (SHORT_GRASS_ID.equals(key.identifier())) { + LootPool.Builder poolBuilder = LootPool.lootPool() + .setRolls(ConstantValue.exactly(1)) + .add(LootItem.lootTableItem(Items.AIR) + .apply(SetItemCountFunction.setCount(UniformGenerator.between(1f, 1f)))) + .add(LootItem.lootTableItem(Items.STICK) + .apply(SetItemCountFunction.setCount(UniformGenerator.between(1f, 3f)))); + tableBuilder.pool(poolBuilder.build()); + } + + if (TALL_GRASS_ID.equals(key.identifier())) { + LootPool.Builder poolBuilder = LootPool.lootPool() + .setRolls(ConstantValue.exactly(1)) + .add(LootItem.lootTableItem(Items.STICK).setWeight(1) + .apply(SetItemCountFunction.setCount(UniformGenerator.between(1f, 4f)))); + tableBuilder.pool(poolBuilder.build()); + } + }); + } +} \ No newline at end of file