diff --git a/src/main/java/com/example/ExampleMod.java b/src/main/java/com/example/ExampleMod.java deleted file mode 100644 index 215cfbb..0000000 --- a/src/main/java/com/example/ExampleMod.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.example; - -import net.fabricmc.api.ModInitializer; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class ExampleMod implements ModInitializer { - public static final String MOD_ID = "modid"; - - // This logger is used to write text to the console and the log file. - // It is considered best practice to use your mod id as the logger's name. - // That way, it's clear which mod wrote info, warnings, and errors. - public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID); - - @Override - public void onInitialize() { - // This code runs as soon as Minecraft is in a mod-load-ready state. - // However, some things (like resources) may still be uninitialized. - // Proceed with mild caution. - - LOGGER.info("Hello Fabric world!"); - } -} \ No newline at end of file diff --git a/src/main/java/com/example/GolemsExtra.java b/src/main/java/com/example/GolemsExtra.java new file mode 100644 index 0000000..cf03c66 --- /dev/null +++ b/src/main/java/com/example/GolemsExtra.java @@ -0,0 +1,20 @@ +package com.example; + +import com.example.block.ModBlocks; +import com.example.entity.ModEntities; +import net.fabricmc.api.ModInitializer; + +public class GolemsExtra implements ModInitializer { + public static final String MOD_ID = "golemsextra"; + + @Override + public void onInitialize() { + System.out.println("GolemsExtra: Initializing Mod Components..."); + + // ** CALL REGISTRATION METHODS ** + ModBlocks.registerModBlocks(); + ModEntities.registerModEntities(); + + System.out.println("GolemsExtra: Initialization Complete."); + } +} diff --git a/src/main/java/com/example/block/ModBlocks.java b/src/main/java/com/example/block/ModBlocks.java new file mode 100644 index 0000000..08030f4 --- /dev/null +++ b/src/main/java/com/example/block/ModBlocks.java @@ -0,0 +1,40 @@ +package com.example.block; + +import com.example.GolemsExtra; +import com.example.block.entity.StrawChestBlockEntity; +import net.fabricmc.fabric.api.object.builder.v1.block.FabricBlockSettings; +import net.minecraft.block.Block; +import net.minecraft.block.Blocks; +import net.minecraft.block.entity.BlockEntityType; +import net.minecraft.util.Identifier; +import net.minecraft.util.registry.Registry; + +public class ModBlocks { + + // 1. Declare Block & BlockEntityType (updated to use final) + public static final Block STRAW_CHEST = registerBlock("straw_chest", + // Copy settings from Hay Block (e.g., strength, sound) + new StrawChestBlock(FabricBlockSettings.copy(Blocks.HAY_BLOCK)) + ); + + // We can use a wildcard type here for simplicity + public static BlockEntityType STRAW_CHEST_ENTITY; + + public static void registerModBlocks() { + // 2. Register the Block Entity Type + STRAW_CHEST_ENTITY = Registry.register( + Registry.BLOCK_ENTITY_TYPE, + new Identifier(GolemsExtra.MOD_ID, "straw_chest_entity"), + // Create the Block Entity Type and associate it with the STRAW_CHEST block + BlockEntityType.Builder.create(StrawChestBlockEntity::new, STRAW_CHEST).build(null) + ); + + // This log confirms the registration methods were called. + System.out.println("Registered Mod Blocks and Entities for " + GolemsExtra.MOD_ID); + } + + // Helper method to simplify block registration + private static Block registerBlock(String name, Block block) { + return Registry.register(Registry.BLOCK, new Identifier(GolemsExtra.MOD_ID, name), block); + } +} diff --git a/src/main/java/com/example/block/StrawChestBlock.java b/src/main/java/com/example/block/StrawChestBlock.java new file mode 100644 index 0000000..e1dc648 --- /dev/null +++ b/src/main/java/com/example/block/StrawChestBlock.java @@ -0,0 +1,32 @@ +package com.example.block; + +import com.example.block.entity.StrawChestBlockEntity; +import net.fabricmc.fabric.api.object.builder.v1.block.FabricBlockSettings; +import net.minecraft.block.BlockRenderType; +import net.minecraft.block.BlockState; +import net.minecraft.block.BlockWithEntity; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.util.math.BlockPos; + +public class StrawChestBlock extends BlockWithEntity { + + // Constructor: Uses settings copied from HAY_BLOCK (or your preferred base) + public StrawChestBlock(FabricBlockSettings settings) { + super(settings); + } + + // Required method: Creates the specific Block Entity when placed + @Override + public BlockEntity createBlockEntity(BlockPos pos, BlockState state) { + return new StrawChestBlockEntity(pos, state); + } + + // Tells Minecraft that this block uses a Block Entity for rendering + @Override + public BlockRenderType getRenderType(BlockState state) { + return BlockRenderType.MODEL; // Use a standard block model + } + + // NOTE: You would add player interaction (onUse) logic here later + // to allow players to open the inventory. +} diff --git a/src/main/java/com/example/block/entity/StrawChestBlockEntity.java b/src/main/java/com/example/block/entity/StrawChestBlockEntity.java new file mode 100644 index 0000000..a13b93c --- /dev/null +++ b/src/main/java/com/example/block/entity/StrawChestBlockEntity.java @@ -0,0 +1,13 @@ +package com.example.block.entity; + +import com.example.block.ModBlocks; // Used for the BlockEntityType reference +import net.minecraft.block.BlockState; +import net.minecraft.util.math.BlockPos; + +public class StrawChestBlockEntity extends UtilityStorageBlockEntity { + + public StrawChestBlockEntity(BlockPos pos, BlockState state) { + // We pass the specific BlockEntityType defined in ModBlocks to the generic constructor + super(ModBlocks.STRAW_CHEST_ENTITY, pos, state); + } +} diff --git a/src/main/java/com/example/block/entity/UtilityStorageBlockEntity.java b/src/main/java/com/example/block/entity/UtilityStorageBlockEntity.java new file mode 100644 index 0000000..2130a1c --- /dev/null +++ b/src/main/java/com/example/block/entity/UtilityStorageBlockEntity.java @@ -0,0 +1,114 @@ +package com.example.block.entity; + +import net.minecraft.block.BlockState; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.block.entity.BlockEntityType; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.inventory.Inventories; +import net.minecraft.inventory.SidedInventory; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.util.collection.DefaultedList; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Direction; + +public abstract class UtilityStorageBlockEntity extends BlockEntity implements SidedInventory { + + // All specific chests will have 9 slots for Golem-deposited items + private static final int INVENTORY_SIZE = 9; + protected DefaultedList inventory; + + // Constructor (requires the specific BlockEntityType from ModBlocks) + public UtilityStorageBlockEntity(BlockEntityType type, BlockPos pos, BlockState state) { + super(type, pos, state); + this.inventory = DefaultedList.ofSize(INVENTORY_SIZE, ItemStack.EMPTY); + } + + // --- Inventory Read/Write (NBT) --- + + @Override + public void readNbt(NbtCompound nbt) { + super.readNbt(nbt); + Inventories.readNbt(nbt, this.inventory); + } + + @Override + public void writeNbt(NbtCompound nbt) { + super.writeNbt(nbt); + Inventories.writeNbt(nbt, this.inventory); + } + + // --- Standard Inventory Interface Implementation --- + + @Override + public int size() { + return INVENTORY_SIZE; + } + + @Override + public boolean isEmpty() { + return this.inventory.stream().allMatch(ItemStack::isEmpty); + } + + @Override + public ItemStack getStack(int slot) { + return this.inventory.get(slot); + } + + @Override + public ItemStack removeStack(int slot, int amount) { + return Inventories.splitStack(this.inventory, slot, amount); + } + + @Override + public ItemStack removeStack(int slot) { + return Inventories.removeStack(this.inventory, slot); + } + + @Override + public void setStack(int slot, ItemStack stack) { + this.inventory.set(slot, stack); + if (stack.getCount() > this.getMaxItemCount()) { + stack.setCount(this.getMaxItemCount()); + } + this.markDirty(); + } + + @Override + public boolean canPlayerUse(PlayerEntity player) { + if (this.world.getBlockEntity(this.pos) != this) { + return false; + } + return player.squaredDistanceTo((double)this.pos.getX() + 0.5, (double)this.pos.getY() + 0.5, (double)this.pos.getZ() + 0.5) <= 64.0; + } + + @Override + public void clear() { + this.inventory.clear(); + } + + // --- SidedInventory (For Golem/Hopper Transfer) --- + + @Override + public int[] getAvailableSlots(Direction side) { + // All 9 slots are available for I/O + int[] slots = new int[INVENTORY_SIZE]; + for (int i = 0; i < INVENTORY_SIZE; i++) { + slots[i] = i; + } + return slots; + } + + // Items can be inserted by Golems or Hoppers from any side + @Override + public boolean canInsert(int slot, ItemStack stack, @org.jetbrains.annotations.Nullable Direction dir) { + return true; + } + + // By default, prevent auto-extraction (e.g., by Hoppers below) to keep it simple. + // You can override this in specific subclasses if needed. + @Override + public boolean canExtract(int slot, ItemStack stack, Direction dir) { + return false; + } +} diff --git a/src/main/java/com/example/entity/ModEntities.java b/src/main/java/com/example/entity/ModEntities.java new file mode 100644 index 0000000..a96f557 --- /dev/null +++ b/src/main/java/com/example/entity/ModEntities.java @@ -0,0 +1,42 @@ +package com.example.entity; + +import com.example.GolemsExtra; +import net.fabricmc.fabric.api.object.builder.v1.entity.FabricDefaultAttributeRegistry; +import net.fabricmc.fabric.api.object.builder.v1.entity.FabricEntityTypeBuilder; +import net.minecraft.entity.EntityDimensions; +import net.minecraft.entity.EntityType; +import net.minecraft.entity.SpawnGroup; +import net.minecraft.util.Identifier; +import net.minecraft.util.registry.Registry; + +public class ModEntities { + + // 1. Declare the Straw Golem Entity Type + public static final EntityType STRAW_GOLEM_ENTITY_TYPE = registerEntityType( + "straw_golem", + FabricEntityTypeBuilder.create(SpawnGroup.MISC, StrawGolemEntity::new) + // Define the Golem's size (Adjust as needed for Copper Golem size) + .dimensions(EntityDimensions.fixed(0.6f, 1.2f)) + .build() + ); + + public static void registerModEntities() { + // 2. Register the Golem's Attributes (Health, Speed, etc.) + FabricDefaultAttributeRegistry.register( + STRAW_GOLEM_ENTITY_TYPE, + StrawGolemEntity.createStrawGolemAttributes() + ); + + System.out.println("Registered Mod Entities for " + GolemsExtra.MOD_ID); + } + + // Helper method to simplify entity type registration + private static EntityType registerEntityType(String name, EntityType entityType) { + // NOTE: We cast the return type here to match the specific Golem class for easier use. + return (EntityType)Registry.register( + Registry.ENTITY_TYPE, + new Identifier(GolemsExtra.MOD_ID, name), + entityType + ); + } +} diff --git a/src/main/java/com/example/entity/StrawGolemEntity.java b/src/main/java/com/example/entity/StrawGolemEntity.java new file mode 100644 index 0000000..6c3b9e2 --- /dev/null +++ b/src/main/java/com/example/entity/StrawGolemEntity.java @@ -0,0 +1,93 @@ +package com.example.entity; + +import com.example.block.ModBlocks; +import net.minecraft.entity.EntityType; +import net.minecraft.entity.ai.goal.LookAroundGoal; +import net.minecraft.entity.ai.goal.LookAtEntityGoal; +import net.minecraft.entity.ai.goal.WanderAroundFarGoal; +import net.minecraft.entity.attribute.DefaultAttributeContainer; +import net.minecraft.entity.attribute.EntityAttributes; +import net.minecraft.entity.mob.MobEntity; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.world.World; +import net.minecraft.block.Block; +import net.minecraft.block.BlockState; +import net.minecraft.block.Blocks; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; +import net.minecraft.util.math.random.Random; + +public class StrawGolemEntity extends UtilityGolemEntity { + + public StrawGolemEntity(EntityType entityType, World world) { + super(entityType, world); + } + + // --- Placeholder Entity Attributes --- + // Defines health, speed, etc. + public static DefaultAttributeContainer.Builder createStrawGolemAttributes() { + // Placeholder stats (Low Health, Moderate Speed) + return MobEntity.createMobAttributes() + .add(EntityAttributes.GENERIC_MAX_HEALTH, 10.0) + .add(EntityAttributes.GENERIC_MOVEMENT_SPEED, 0.28); + } + + // --- Placeholder AI Goals (Basic Movement) --- + // We will replace goals 1 and 2 later with farming/item transfer logic + @Override + protected void initGoals() { + // Placeholder Goals: Golem simply wanders and looks around + this.goalSelector.add(5, new WanderAroundFarGoal(this, 0.6)); + this.goalSelector.add(6, new LookAtEntityGoal(this, PlayerEntity.class, 6.0f)); + this.goalSelector.add(7, new LookAroundGoal(this)); + } + + /** + * Checks for the two-block pattern (Hay Bale + Pumpkin) and spawns the Golem and Chest. + * @param world The world. + * @param pos The position where the Carved Pumpkin was placed. + */ + public static boolean checkAndSpawn(World world, BlockPos pos) { + if (world.isClient) return false; + + BlockPos basePos = pos.down(); + BlockState baseState = world.getBlockState(basePos); + + // 1. Check if the base is a Hay Bale + if (baseState.isOf(Blocks.HAY_BLOCK)) { + + // --- PATTERN MATCHED --- + + // 2. Clear the Pumpkin + // Use flag 3: notify neighbors, re-render, don't update lighting + world.setBlockState(pos, Blocks.AIR.getDefaultState(), 3); + world.syncWorldEvent(2001, pos, Block.getRawIdFromState(world.getBlockState(pos))); + + // 3. Replace the Hay Bale with the custom Straw Chest Block + world.setBlockState(basePos, ModBlocks.STRAW_CHEST.getDefaultState(), 3); + + // 4. Spawn the Straw Golem Entity + StrawGolemEntity golem = ModEntities.STRAW_GOLEM_ENTITY_TYPE.create(world); + + if (golem != null) { + BlockPos spawnPos = basePos.up(); + Random random = world.random; + + // Position and angle setup + golem.refreshPositionAndAngles( + (double)spawnPos.getX() + 0.5, + (double)spawnPos.getY() + 0.05, + (double)spawnPos.getZ() + 0.5, + random.nextFloat() * 360.0F, 0.0F + ); + + // ** CRUCIAL STEP: Set the Golem's storage location to the new chest ** + golem.setStoragePos(basePos); + + world.spawnEntity(golem); + return true; + } + } + return false; + } +} diff --git a/src/main/java/com/example/entity/UtilityGolemEntity.java b/src/main/java/com/example/entity/UtilityGolemEntity.java new file mode 100644 index 0000000..ff61e6d --- /dev/null +++ b/src/main/java/com/example/entity/UtilityGolemEntity.java @@ -0,0 +1,82 @@ +package com.example.entity; + +import net.minecraft.entity.EntityType; +import net.minecraft.entity.mob.PathAwareEntity; +import net.minecraft.inventory.Inventories; +import net.minecraft.inventory.SidedInventory; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.collection.DefaultedList; +import net.minecraft.item.ItemStack; +import net.minecraft.world.World; + +public abstract class UtilityGolemEntity extends PathAwareEntity { + + // The Golem's internal inventory for carrying collected items (9 slots) + private final SidedInventory inventory = Inventories.ofSize(9); + + // The position of the Golem's designated home chest (e.g., Straw Chest) + protected BlockPos storagePos = null; + + // Constructor is abstract, implemented by subclasses (e.g., StrawGolemEntity) + public UtilityGolemEntity(EntityType entityType, World world) { + super(entityType, world); + } + + // --- Inventory Access --- + + public SidedInventory getInventory() { + return this.inventory; + } + + public boolean isInventoryEmpty() { + return this.inventory.isEmpty(); + } + + // Checks if the internal inventory is full + public boolean isInventoryFull() { + DefaultedList stacks = ((Inventories)this.inventory).get.get(); + for (ItemStack stack : stacks) { + if (stack.isEmpty() || stack.getCount() < stack.getMaxCount()) { + return false; + } + } + return true; + } + + // --- Storage Management --- + + public void setStoragePos(BlockPos pos) { + this.storagePos = pos; + } + + public BlockPos getStoragePos() { + return this.storagePos; + } + + // --- NBT (Saving Data) --- + + @Override + public void writeCustomDataToNbt(NbtCompound nbt) { + super.writeCustomDataToNbt(nbt); + // Save inventory contents + Inventories.writeNbt(nbt, (DefaultedList) ((Inventories)this.inventory).get.get()); + + // Save storage position using the BlockPos helper method + if (this.storagePos != null) { + nbt.putLong("StoragePos", this.storagePos.asLong()); + } + } + + @Override + public void readCustomDataFromNbt(NbtCompound nbt) { + super.readCustomDataFromNbt(nbt); + // Read inventory contents + Inventories.readNbt(nbt, (DefaultedList) ((Inventories)this.inventory).get.get()); + + // Read storage position + if (nbt.contains("StoragePos")) { + this.storagePos = BlockPos.ofLong(nbt.getLong("StoragePos")); + } + } +} diff --git a/src/main/java/com/example/entity/ai/StrawFarmingGoal.java b/src/main/java/com/example/entity/ai/StrawFarmingGoal.java new file mode 100644 index 0000000..5d30dde --- /dev/null +++ b/src/main/java/com/example/entity/ai/StrawFarmingGoal.java @@ -0,0 +1,270 @@ +package com.example.entity.ai; + +import com.example.entity.StrawGolemEntity; +import com.example.util.InventoryUtil; // You need to create this helper class +import net.minecraft.block.*; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.entity.ai.goal.Goal; +import net.minecraft.inventory.SidedInventory; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Direction; +import net.minecraft.world.World; +import net.minecraft.world.WorldView; +import java.util.EnumSet; +import java.util.Optional; +import java.util.function.Predicate; + +public class StrawFarmingGoal extends Goal { + + private final StrawGolemEntity golem; + private final double speed; + private BlockPos targetBlock; + private int searchAttempts; + + // Radius for searching for work (e.g., 8 blocks) + private static final int WORK_RANGE = 8; + + // Predicate to identify blocks that can be hoed (Dirt/Grass) + private static final Predicate CAN_BE_HOED = (state) -> + state.isOf(Blocks.DIRT) || state.isOf(Blocks.GRASS_BLOCK); + + public StrawFarmingGoal(StrawGolemEntity golem, double speed) { + this.golem = golem; + this.speed = speed; + this.setControls(EnumSet.of(Control.MOVE)); + } + + // --- Core Goal Control Methods --- + + /** + * Should run when the Golem's inventory is NOT full and there is work to do. + */ + @Override + public boolean canStart() { + if (this.golem.isInventoryFull()) { + return false; // Prioritize moving items if full + } + + // Find work to start + this.targetBlock = this.findWork(); + + return this.targetBlock != null; + } + + /** + * Should continue running if a target is set and the Golem is not full. + */ + @Override + public boolean shouldContinue() { + return this.targetBlock != null && !this.golem.isInventoryFull() && !this.golem.getNavigation().isIdle(); + } + + @Override + public void start() { + if (this.targetBlock != null) { + this.golem.getNavigation().startMovingTo( + this.targetBlock.getX() + 0.5, + this.targetBlock.getY(), + this.targetBlock.getZ() + 0.5, + this.speed + ); + } + this.searchAttempts = 0; + } + + @Override + public void stop() { + this.targetBlock = null; + this.golem.getNavigation().stop(); + } + + // --- Work Execution (The Heart of the AI) --- + + @Override + public void tick() { + if (this.targetBlock == null) { + return; + } + + // Maintain path to the target + this.golem.getNavigation().startMovingTo( + this.targetBlock.getX() + 0.5, + this.targetBlock.getY(), + this.targetBlock.getZ() + 0.5, + this.speed + ); + + // Check if the Golem is close enough to interact (1.5 blocks is enough) + if (this.golem.squaredDistanceTo(this.targetBlock.getX() + 0.5, this.targetBlock.getY(), this.targetBlock.getZ() + 0.5) < 2.25) { + + BlockState state = this.golem.world.getBlockState(this.targetBlock); + World world = this.golem.world; + + if (isMatureCrop(state)) { + // 1. HARVEST (breaks the crop, items drop) + world.breakBlock(this.targetBlock, true, this.golem); + // After harvesting, the Golem should prioritize picking up the drops + // (this is usually handled by the vanilla pathfinding/collision but + // a dedicated Item collection goal might be needed for reliability). + + // Immediately attempt to replant + this.replantCrop(world, this.targetBlock); + } + else if (state.isOf(Blocks.FARMLAND) && world.getBlockState(this.targetBlock.up()).isAir()) { + // 2. REPLANT (Found bare farmland) + this.replantCrop(world, this.targetBlock); + } + else if (CAN_BE_HOED.test(state)) { + // 3. HOE & PLANT (Found dirt/grass block) + this.hoeAndPlant(world, this.targetBlock); + } + else if (isPlantableGround(state)) { + // 4. TALL PLANT (Found suitable ground for cane/bamboo) + this.plantTallCrop(world, this.targetBlock); + } + + // Interaction complete, stop and search for new work + this.targetBlock = null; + } + } + + // --- Work Finding Helpers --- + + private BlockPos findWork() { + BlockPos currentPos = this.golem.getBlockPos(); + + // 1. PRIORITIZE: Mature crops ready for harvest + Optional matureCrop = BlockPos.stream( + currentPos.add(-WORK_RANGE, -1, -WORK_RANGE), + currentPos.add(WORK_RANGE, 1, WORK_RANGE) + ) + .filter(pos -> isMatureCrop(this.golem.world.getBlockState(pos))) + .min((pos1, pos2) -> (int) pos1.getSquaredDistance(currentPos) - (int) pos2.getSquaredDistance(currentPos)); + + if (matureCrop.isPresent()) { + return matureCrop.get(); + } + + // 2. SECONDARY: Bare farmland or dirt/grass to expand the farm + Optional plantSpot = BlockPos.stream( + currentPos.add(-WORK_RANGE, -1, -WORK_RANGE), + currentPos.add(WORK_RANGE, 1, WORK_RANGE) + ) + .filter(pos -> canPlantAt(this.golem.world, pos)) + .min((pos1, pos2) -> (int) pos1.getSquaredDistance(currentPos) - (int) pos2.getSquaredDistance(currentPos)); + + return plantSpot.orElse(null); + } + + // --- Interaction Logic Helpers --- + + private boolean isMatureCrop(BlockState state) { + // Check for common crops at max age + if (state.getBlock() instanceof CropBlock cropBlock) { + return cropBlock.isMature(state); + } + // Check for cane/bamboo at max height (optional: you may prefer not to auto-harvest tall crops) + return false; + } + + private boolean canPlantAt(World world, BlockPos pos) { + BlockState groundState = world.getBlockState(pos); + BlockState airState = world.getBlockState(pos.up()); + + if (!airState.isAir()) return false; // Must have air space above + + // 1. Can we replant on bare farmland? (Requires seeds in inventory) + if (groundState.isOf(Blocks.FARMLAND) && InventoryUtil.hasSeeds(this.golem.getInventory())) { + return true; + } + + // 2. Can we hoe and plant on dirt/grass? (Requires seeds and proximity to water) + if (CAN_BE_HOED.test(groundState) && InventoryUtil.hasSeeds(this.golem.getInventory()) + /* && isNearWater(world, pos) */) { // **IMPORTANT**: Need water check helper + return true; + } + + // 3. Can we plant tall crops? (Requires tall item and correct ground) + if (isPlantableGround(groundState) && InventoryUtil.hasTallCrops(this.golem.getInventory())) { + return true; + } + + return false; + } + + // Only includes basic planting blocks for tall plants (no water requirement check here) + private boolean isPlantableGround(BlockState state) { + return state.isOf(Blocks.DIRT) || state.isOf(Blocks.GRASS_BLOCK) || state.isOf(Blocks.SAND); + } + + private void hoeAndPlant(World world, BlockPos pos) { + // 1. Hoe the block + world.setBlockState(pos, Blocks.FARMLAND.getDefaultState(), 3); + + // 2. Plant (same as replant logic) + this.replantCrop(world, pos); + } + + // --- Farming Logic Implementations --- + + /** + * Logic: Consumes a standard seed (Wheat, Potato, etc.) and plants it on Farmland. + * Target pos is the Farmland block; we plant on pos.up(). + */ + private void replantCrop(World world, BlockPos pos) { + // 1. Check inventory and consume 1 seed + Block blockToPlant = InventoryUtil.consumeSeedAndGetBlock(this.golem.getInventory()); + + if (blockToPlant != null) { + // 2. Place the crop + BlockPos plantPos = pos.up(); + // Ensure the space is actually empty before placing (safety check) + if (world.getBlockState(plantPos).isAir()) { + world.setBlockState(plantPos, blockToPlant.getDefaultState()); + } + } + } + + /** + * Logic: Consumes a tall crop item (Cane, Bamboo) and plants it. + * Checks specific requirements like Water for Sugarcane. + */ + private void plantTallCrop(World world, BlockPos pos) { + // 1. Peek at what we might plant (to check water requirements) + // Note: For simplicity, we just consume and try to place. + Block blockToPlant = InventoryUtil.consumeTallCropAndGetBlock(this.golem.getInventory()); + + if (blockToPlant != null) { + BlockPos plantPos = pos.up(); + + // Special Check: Sugarcane requires water adjacent to the soil (pos) + if (blockToPlant == Blocks.SUGAR_CANE) { + if (!isNearWater(world, pos)) { + // Invalid spot for cane. (Ideally return item, but for now we abort) + return; + } + } + + // 2. Place the crop + if (world.getBlockState(plantPos).isAir()) { + world.setBlockState(plantPos, blockToPlant.getDefaultState()); + } + } + } + + /** + * Helper: Checks if the specified block position has water directly adjacent to it. + * Required for: Sugarcane logic and farmland hydration checks. + */ + private boolean isNearWater(World world, BlockPos pos) { + for (BlockPos neighbor : BlockPos.iterate(pos.add(-1, 0, -1), pos.add(1, 0, 1))) { + if (world.getFluidState(neighbor).isIn(net.minecraft.tag.FluidTags.WATER)) { + return true; + } + } + return false; + } + +} diff --git a/src/main/java/com/example/entity/ai/StrawMoveItemsGoal.java b/src/main/java/com/example/entity/ai/StrawMoveItemsGoal.java new file mode 100644 index 0000000..09011ba --- /dev/null +++ b/src/main/java/com/example/entity/ai/StrawMoveItemsGoal.java @@ -0,0 +1,171 @@ +package com.example.entity.ai; + +import com.example.entity.StrawGolemEntity; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.entity.ai.goal.Goal; +import net.minecraft.inventory.Inventory; +import net.minecraft.inventory.SidedInventory; +import net.minecraft.item.HoeItem; +import net.minecraft.item.ItemStack; +import net.minecraft.util.math.BlockPos; + +import java.util.EnumSet; + +public class StrawMoveItemsGoal extends Goal { + + private final StrawGolemEntity golem; + private final double speed; + private int failedPathfindingAttempts = 0; + + public StrawMoveItemsGoal(StrawGolemEntity golem, double speed) { + this.golem = golem; + this.speed = speed; + this.setControls(EnumSet.of(Control.MOVE)); + } + + @Override + public boolean canStart() { + // Run if inventory is full OR if it has items and can't find farming work + // For simplicity: Run if inventory is not empty. + // You might want to tweak this to "Only run if full" to keep it farming longer. + return !this.golem.isInventoryEmpty() && this.golem.getStoragePos() != null; + } + + @Override + public boolean shouldContinue() { + // Continue until inventory is empty or we lose the storage position + return !this.golem.isInventoryEmpty() && this.golem.getStoragePos() != null; + } + + @Override + public void start() { + this.moveToStorage(); + this.failedPathfindingAttempts = 0; + } + + @Override + public void tick() { + BlockPos storagePos = this.golem.getStoragePos(); + + if (storagePos == null) { + this.golem.getNavigation().stop(); + return; + } + + // Keep moving towards the chest + if (this.golem.getNavigation().isIdle()) { + this.moveToStorage(); + } + + // Check distance to interact (approx 2 blocks) + if (this.golem.squaredDistanceTo(storagePos.getX() + 0.5, storagePos.getY(), storagePos.getZ() + 0.5) < 4.0) { + this.transferItems(storagePos); + } + } + + private void moveToStorage() { + BlockPos pos = this.golem.getStoragePos(); + if (pos != null) { + this.golem.getNavigation().startMovingTo(pos.getX() + 0.5, pos.getY(), pos.getZ() + 0.5, this.speed); + } + } + + /** + * The core logic for moving items with the specific "Hoe in Slot 1" rule. + */ + private void transferItems(BlockPos pos) { + BlockEntity be = this.golem.world.getBlockEntity(pos); + + // Ensure the target is actually an inventory (Chest/Barrel) + if (!(be instanceof Inventory)) { + return; // Chest might have been broken + } + + Inventory chestInv = (Inventory) be; + SidedInventory golemInv = this.golem.getInventory(); + + // --- STEP 1: Handle the Hoe (Put in Chest Slot 0) --- + for (int i = 0; i < golemInv.size(); i++) { + ItemStack stack = golemInv.getStack(i); + + if (stack.getItem() instanceof HoeItem) { + // We found a hoe! Check Chest Slot 0 (Index 0 is "Slot 1") + ItemStack chestSlot0 = chestInv.getStack(0); + + if (chestSlot0.isEmpty()) { + // Slot is empty, put the hoe there + chestInv.setStack(0, stack.copy()); + golemInv.setStack(i, ItemStack.EMPTY); + } + else if (chestSlot0.getItem() instanceof HoeItem) { + // Slot already has a hoe? + // Optional: Swap them if the Golem's hoe is better/newer? + // For now, we skip moving it if the slot is occupied. + } + // If Chest Slot 0 has a non-hoe item, we skip putting the hoe there + // (or you could code it to swap the items). + } + } + + // --- STEP 2: Dump the Rest (Crops, Seeds, etc.) --- + for (int i = 0; i < golemInv.size(); i++) { + ItemStack stack = golemInv.getStack(i); + + if (stack.isEmpty()) continue; + + // Try to add this stack to the chest + ItemStack remaining = this.addItemToInventory(chestInv, stack); + + // Update Golem's inventory with what's left (or empty if all moved) + golemInv.setStack(i, remaining); + } + + // Mark both inventories as dirty so the game saves the changes + be.markDirty(); + // Golem inventory dirty is handled by setStack + } + + /** + * Helper to insert stacks into a target inventory. + * Returns the remainder (ItemStack) that couldn't fit. + */ + private ItemStack addItemToInventory(Inventory target, ItemStack stack) { + ItemStack remaining = stack.copy(); + + // 1. Try to merge with existing stacks + for (int i = 0; i < target.size(); i++) { + if (remaining.isEmpty()) break; + + // Skip Slot 0 if we want to strictly reserve it for the hoe + // (Remove this check if you only care about putting the hoe IN slot 0, but allow other items there too) + if (i == 0) continue; + + ItemStack targetStack = target.getStack(i); + + if (ItemStack.canCombine(targetStack, remaining)) { + int availableSpace = targetStack.getMaxCount() - targetStack.getCount(); + + if (availableSpace > 0) { + int transferAmount = Math.min(remaining.getCount(), availableSpace); + targetStack.increment(transferAmount); + remaining.decrement(transferAmount); + } + } + } + + // 2. Try to fill empty slots + if (!remaining.isEmpty()) { + for (int i = 0; i < target.size(); i++) { + if (remaining.isEmpty()) break; + if (i == 0) continue; // Skip Slot 0 (Reserve rule) + + if (target.getStack(i).isEmpty()) { + target.setStack(i, remaining.copy()); + remaining.setCount(0); + } + } + } + + return remaining; + } +} diff --git a/src/main/java/com/example/mixin/CarvedPumpkinBlockMixin.java b/src/main/java/com/example/mixin/CarvedPumpkinBlockMixin.java new file mode 100644 index 0000000..ef409d6 --- /dev/null +++ b/src/main/java/com/example/mixin/CarvedPumpkinBlockMixin.java @@ -0,0 +1,39 @@ +package com.example.mixin; + +import com.example.entity.StrawGolemEntity; +import net.minecraft.block.CarvedPumpkinBlock; +import net.minecraft.block.BlockState; +import net.minecraft.entity.LivingEntity; +import net.minecraft.item.ItemStack; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(CarvedPumpkinBlock.class) +public class CarvedPumpkinBlockMixin { + + /** + * Injects logic immediately after the Carved Pumpkin is placed, + * checking if it completes the Straw Golem pattern. + */ + @Inject( + method = "onPlaced", + at = @At("RETURN") + ) + private void golemsextra$checkStrawGolemPattern( + World world, + BlockPos pos, + BlockState state, + LivingEntity placer, + ItemStack itemStack, + CallbackInfo ci + ) { + if (!world.isClient) { + // Call our static check method. It handles block replacement and spawning. + StrawGolemEntity.checkAndSpawn(world, pos); + } + } +} diff --git a/src/main/java/com/example/mixin/ExampleMixin.java b/src/main/java/com/example/mixin/ExampleMixin.java deleted file mode 100644 index 7a67d1f..0000000 --- a/src/main/java/com/example/mixin/ExampleMixin.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.example.mixin; - -import net.minecraft.server.MinecraftServer; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; - -@Mixin(MinecraftServer.class) -public class ExampleMixin { - @Inject(at = @At("HEAD"), method = "loadLevel") - private void init(CallbackInfo info) { - // This code is injected into the start of MinecraftServer.loadLevel()V - } -} \ No newline at end of file diff --git a/src/main/java/com/example/util/InventoryUtil.java b/src/main/java/com/example/util/InventoryUtil.java new file mode 100644 index 0000000..d2cf6ef --- /dev/null +++ b/src/main/java/com/example/util/InventoryUtil.java @@ -0,0 +1,117 @@ +package com.example.util; + +import net.minecraft.block.Block; +import net.minecraft.block.Blocks; +import net.minecraft.inventory.SidedInventory; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import net.minecraft.tag.ItemTags; // Useful for mod compatibility if using tags +import java.util.HashMap; +import java.util.Map; + +public class InventoryUtil { + + // Map defining which seeds plant which blocks + private static final Map SEED_TO_BLOCK_MAP = new HashMap<>(); + + static { + SEED_TO_BLOCK_MAP.put(Items.WHEAT_SEEDS, Blocks.WHEAT); + SEED_TO_BLOCK_MAP.put(Items.POTATO, Blocks.POTATOES); + SEED_TO_BLOCK_MAP.put(Items.CARROT, Blocks.CARROTS); + SEED_TO_BLOCK_MAP.put(Items.BEETROOT_SEEDS, Blocks.BEETROOTS); + SEED_TO_BLOCK_MAP.put(Items.MELON_SEEDS, Blocks.MELON_STEM); + SEED_TO_BLOCK_MAP.put(Items.PUMPKIN_SEEDS, Blocks.PUMPKIN_STEM); + // Add modded seeds here if needed + } + + private static final Item[] TALL_CROPS = { + Items.SUGAR_CANE, + Items.BAMBOO, + Items.CACTUS + }; + + /** + * Checks if the inventory contains any valid crop seeds (Wheat, Carrot, Potato, etc.). + */ + public static boolean hasSeeds(SidedInventory inventory) { + return getFirstSeedSlot(inventory) != -1; + } + + /** + * Checks if the inventory contains valid tall crops (Cane, Bamboo, Cactus). + */ + public static boolean hasTallCrops(SidedInventory inventory) { + return getFirstTallCropSlot(inventory) != -1; + } + + /** + * Finds, removes one seed from the inventory, and returns the Block corresponding to that seed. + * Returns null if no seeds are found. + */ + public static Block consumeSeedAndGetBlock(SidedInventory inventory) { + int slot = getFirstSeedSlot(inventory); + if (slot != -1) { + ItemStack stack = inventory.getStack(slot); + Item item = stack.getItem(); + + // Get the block this seed corresponds to + Block blockToPlant = SEED_TO_BLOCK_MAP.get(item); + + // Decrease stack size by 1 (consume the seed) + stack.decrement(1); + inventory.setStack(slot, stack.isEmpty() ? ItemStack.EMPTY : stack); + + return blockToPlant; + } + return null; + } + + /** + * Finds, removes one tall crop item, and returns the Block to plant. + * Returns null if no tall crops are found. + */ + public static Block consumeTallCropAndGetBlock(SidedInventory inventory) { + int slot = getFirstTallCropSlot(inventory); + if (slot != -1) { + ItemStack stack = inventory.getStack(slot); + Item item = stack.getItem(); + + // For tall crops, the Item usually corresponds directly to the Block + Block blockToPlant = Block.getBlockFromItem(item); + + // Decrease stack size + stack.decrement(1); + inventory.setStack(slot, stack.isEmpty() ? ItemStack.EMPTY : stack); + + return blockToPlant; + } + return null; + } + + // --- Private Helper Methods --- + + private static int getFirstSeedSlot(SidedInventory inventory) { + for (int i = 0; i < inventory.size(); i++) { + ItemStack stack = inventory.getStack(i); + if (!stack.isEmpty() && SEED_TO_BLOCK_MAP.containsKey(stack.getItem())) { + return i; + } + } + return -1; + } + + private static int getFirstTallCropSlot(SidedInventory inventory) { + for (int i = 0; i < inventory.size(); i++) { + ItemStack stack = inventory.getStack(i); + if (!stack.isEmpty()) { + for (Item tallCrop : TALL_CROPS) { + if (stack.getItem() == tallCrop) { + return i; + } + } + } + } + return -1; + } +} diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index 02033e2..9164a90 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -1,9 +1,9 @@ { "schemaVersion": 1, - "id": "modid", + "id": "golemsextra", "version": "${version}", - "name": "Example mod", - "description": "This is an example description! Tell everyone what your mod is about!", + "name": "Golems Extra", + "description": "This mod adds extra golems with jobs", "authors": [ "Me!" ], @@ -12,11 +12,11 @@ "sources": "https://github.com/FabricMC/fabric-example-mod" }, "license": "CC0-1.0", - "icon": "assets/modid/icon.png", + "icon": "assets/golemsextra/icon.png", "environment": "*", "entrypoints": { "main": [ - "com.example.ExampleMod" + "com.example.GolemsExtra" ], "client": [ "com.example.ExampleModClient" @@ -38,4 +38,4 @@ "suggests": { "another-mod": "*" } -} \ No newline at end of file +} diff --git a/src/main/resources/modid.mixins.json b/src/main/resources/golemsextra.mixins.json similarity index 86% rename from src/main/resources/modid.mixins.json rename to src/main/resources/golemsextra.mixins.json index f48035e..0c5cfe3 100644 --- a/src/main/resources/modid.mixins.json +++ b/src/main/resources/golemsextra.mixins.json @@ -3,7 +3,7 @@ "package": "com.example.mixin", "compatibilityLevel": "JAVA_21", "mixins": [ - "ExampleMixin" + "CarvedPumpkinBlockMixin" ], "injectors": { "defaultRequire": 1 @@ -11,4 +11,4 @@ "overwrites": { "requireAnnotations": true } -} \ No newline at end of file +}