diff --git a/README.md b/README.md index 79a0639..8eb3de1 100644 --- a/README.md +++ b/README.md @@ -74,11 +74,10 @@ is mod is in a VERY early stage of development. Expect bugs, missing features, a - Pull the repository to your local machine. - Allow Gradle to sync completely (usually takes ~2 minutes). -2. **Select Version (Code Editing Only)** - - If you intend to modify the source code, use the included run configurations to set your workspace and have Java 21 installed and available: +2. **Select Version** + - Use the included run configurations to set your workspace and have Java 21 installed and available: - `Switch to 1.20.1` - `Switch to 1.21.1` - - *Note: You can skip this step if you just want to launch the game.* - *Note: With IntelliJ idea, you can install the Stonecutter plugin and it becomes a dropdown at the top left* 3. **Launch the Game** @@ -86,3 +85,4 @@ is mod is in a VERY early stage of development. Expect bugs, missing features, a - **Fabric:** 1.20.1 & 1.21.1 - **NeoForge:** 1.21.1 - **Forge:** 1.20.1 + - *Note: Be sure to have the proper stonecutter version seleted* diff --git a/common/build.gradle.kts b/common/build.gradle.kts index ac84d5d..b5736f5 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -31,6 +31,7 @@ loom { repositories { mavenCentral() + maven("https://api.modrinth.com/maven") { name = "Modrinth" } maven("https://maven.architectury.dev/") maven("https://maven.terraformersmc.com/releases/") maven("https://dl.cloudsmith.io/public/geckolib3/geckolib/maven/") @@ -59,6 +60,13 @@ dependencies { // JEI (Common API) // We use "modCompileOnly" because we only need the API to write code, not the full mod modCompileOnly("mezz.jei:jei-${commonMod.minecraft_version}-common-api:${commonMod.prop("jei_version")}") + + // Jade (Common API) + val jadeVersion = if (is120) commonMod.prop("jade_version_1_20_1") else commonMod.prop("jade_version_1_21_1") + val jadeClassifier = if (is120) "forge" else "fabric" + + // Use modCompileOnly to provide the API for compilation + modCompileOnly("maven.modrinth:jade:$jadeVersion+$jadeClassifier") } val commonJava: Configuration by configurations.creating { diff --git a/common/src/main/java/net/cmr/jurassicrevived/CommonClass.java b/common/src/main/java/net/cmr/jurassicrevived/CommonClass.java index db4a3ff..3cc00cf 100644 --- a/common/src/main/java/net/cmr/jurassicrevived/CommonClass.java +++ b/common/src/main/java/net/cmr/jurassicrevived/CommonClass.java @@ -1,11 +1,19 @@ package net.cmr.jurassicrevived; +import dev.architectury.event.events.common.LifecycleEvent; import net.cmr.jurassicrevived.block.ModBlocks; +import net.cmr.jurassicrevived.block.entity.ModBlockEntities; import net.cmr.jurassicrevived.config.JRConfigManager; import net.cmr.jurassicrevived.entity.ModEntities; import net.cmr.jurassicrevived.item.ModCreativeTabs; import net.cmr.jurassicrevived.item.ModItems; import net.cmr.jurassicrevived.platform.Services; +import net.cmr.jurassicrevived.recipe.ModRecipes; +import net.cmr.jurassicrevived.screen.ModMenuTypes; +import net.cmr.jurassicrevived.sound.ModSounds; +import net.cmr.jurassicrevived.util.FenceClimbHandler; +import net.cmr.jurassicrevived.util.FenceDiagonalHandler; +import net.cmr.jurassicrevived.util.ModEvents; import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.world.item.Items; @@ -38,8 +46,27 @@ public class CommonClass JRConfigManager.load(Services.PLATFORM.getConfigDir()); ModBlocks.register(); + ModEntities.register(); ModItems.register(); ModCreativeTabs.register(); - ModEntities.register(); + ModEntities.registerAttributes(); + + ModMenuTypes.register(); + + ModRecipes.register(); + + LifecycleEvent.SETUP.register(() -> { + ModBlocks.setupPots(); + //ModEntities.registerSpawnPlacements(); + }); + + ModBlockEntities.register(); + + FenceClimbHandler.register(); + FenceDiagonalHandler.init(); + + ModEvents.init(); + + ModSounds.register(); } } \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/CommonClientClass.java b/common/src/main/java/net/cmr/jurassicrevived/CommonClientClass.java new file mode 100644 index 0000000..732f92f --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/CommonClientClass.java @@ -0,0 +1,124 @@ +package net.cmr.jurassicrevived; + +import dev.architectury.registry.client.level.entity.EntityRendererRegistry; +import dev.architectury.registry.menu.MenuRegistry; +import dev.architectury.registry.client.rendering.BlockEntityRendererRegistry; +import net.cmr.jurassicrevived.block.entity.ModBlockEntities; +import net.cmr.jurassicrevived.block.renderer.TankBlockEntityRenderer; +import net.cmr.jurassicrevived.entity.ModEntities; +import net.cmr.jurassicrevived.entity.client.*; +import net.cmr.jurassicrevived.screen.ModMenuTypes; +import net.cmr.jurassicrevived.screen.custom.*; +import net.cmr.jurassicrevived.sound.MachineHumSoundHandler; +import net.cmr.jurassicrevived.util.FenceClimbClientHandler; +import net.minecraft.client.renderer.entity.NoopRenderer; +import dev.architectury.event.events.common.LifecycleEvent; + +public class CommonClientClass { + public static void init() { + // Initialize client-only systems + MachineHumSoundHandler.init(); + FenceClimbClientHandler.register(); + + // Register Entity Renderers (Architectury handles suppliers here) + EntityRendererRegistry.register(ModEntities.SEAT, NoopRenderer::new); + EntityRendererRegistry.register(ModEntities.APATOSAURUS, ApatosaurusRenderer::new); + EntityRendererRegistry.register(ModEntities.ALBERTOSAURUS, AlbertosaurusRenderer::new); + EntityRendererRegistry.register(ModEntities.BARYONYX, BaryonyxRenderer::new); + EntityRendererRegistry.register(ModEntities.BRACHIOSAURUS, BrachiosaurusRenderer::new); + EntityRendererRegistry.register(ModEntities.CARNOTAURUS, CarnotaurusRenderer::new); + EntityRendererRegistry.register(ModEntities.CERATOSAURUS, CeratosaurusRenderer::new); + EntityRendererRegistry.register(ModEntities.COMPSOGNATHUS, CompsognathusRenderer::new); + EntityRendererRegistry.register(ModEntities.CONCAVENATOR, ConcavenatorRenderer::new); + EntityRendererRegistry.register(ModEntities.DEINONYCHUS, DeinonychusRenderer::new); + EntityRendererRegistry.register(ModEntities.DILOPHOSAURUS, DilophosaurusRenderer::new); + EntityRendererRegistry.register(ModEntities.DIPLODOCUS, DiplodocusRenderer::new); + EntityRendererRegistry.register(ModEntities.DISTORTUS_REX, DistortusRexRenderer::new); + EntityRendererRegistry.register(ModEntities.EDMONTOSAURUS, EdmontosaurusRenderer::new); + EntityRendererRegistry.register(ModEntities.FDUCK, FDuckRenderer::new); + EntityRendererRegistry.register(ModEntities.GALLIMIMUS, GallimimusRenderer::new); + EntityRendererRegistry.register(ModEntities.GIGANOTOSAURUS, GiganotosaurusRenderer::new); + EntityRendererRegistry.register(ModEntities.GUANLONG, GuanlongRenderer::new); + EntityRendererRegistry.register(ModEntities.HERRERASAURUS, HerrerasaurusRenderer::new); + EntityRendererRegistry.register(ModEntities.INDOMINUS_REX, IndominusRexRenderer::new); + EntityRendererRegistry.register(ModEntities.MAJUNGASAURUS, MajungasaurusRenderer::new); + EntityRendererRegistry.register(ModEntities.OURANOSAURUS, OuranosaurusRenderer::new); + EntityRendererRegistry.register(ModEntities.PARASAUROLOPHUS, ParasaurolophusRenderer::new); + EntityRendererRegistry.register(ModEntities.PROCOMPSOGNATHUS, ProcompsognathusRenderer::new); + EntityRendererRegistry.register(ModEntities.PROTOCERATOPS, ProtoceratopsRenderer::new); + EntityRendererRegistry.register(ModEntities.ARAMBOURGIANIA, ArambourgianiaRenderer::new); + EntityRendererRegistry.register(ModEntities.CEARADACTYLUS, CearadactylusRenderer::new); + EntityRendererRegistry.register(ModEntities.DIMORPHODON, DimorphodonRenderer::new); + EntityRendererRegistry.register(ModEntities.GEOSTERNBERGIA, GeosternbergiaRenderer::new); + EntityRendererRegistry.register(ModEntities.GUIDRACO, GuidracoRenderer::new); + EntityRendererRegistry.register(ModEntities.LUDODACTYLUS, LudodactylusRenderer::new); + EntityRendererRegistry.register(ModEntities.MOGANOPTERUS, MoganopterusRenderer::new); + EntityRendererRegistry.register(ModEntities.NYCTOSAURUS, NyctosaurusRenderer::new); + EntityRendererRegistry.register(ModEntities.PTERANODON, PteranodonRenderer::new); + EntityRendererRegistry.register(ModEntities.PTERODAUSTRO, PterodaustroRenderer::new); + EntityRendererRegistry.register(ModEntities.QUETZALCOATLUS, QuetzalcoatlusRenderer::new); + EntityRendererRegistry.register(ModEntities.TAPEJARA, TapejaraRenderer::new); + EntityRendererRegistry.register(ModEntities.TROPEOGNATHUS, TropeognathusRenderer::new); + EntityRendererRegistry.register(ModEntities.TUPUXUARA, TupuxuaraRenderer::new); + EntityRendererRegistry.register(ModEntities.ZHENYUANOPTERUS, ZhenyuanopterusRenderer::new); + EntityRendererRegistry.register(ModEntities.RUGOPS, RugopsRenderer::new); + EntityRendererRegistry.register(ModEntities.SHANTUNGOSAURUS, ShantungosaurusRenderer::new); + EntityRendererRegistry.register(ModEntities.SPINOSAURUS, SpinosaurusRenderer::new); + EntityRendererRegistry.register(ModEntities.STEGOSAURUS, StegosaurusRenderer::new); + EntityRendererRegistry.register(ModEntities.STYRACOSAURUS, StyracosaurusRenderer::new); + EntityRendererRegistry.register(ModEntities.THERIZINOSAURUS, TherizinosaurusRenderer::new); + EntityRendererRegistry.register(ModEntities.TRICERATOPS, TriceratopsRenderer::new); + EntityRendererRegistry.register(ModEntities.TYRANNOSAURUS_REX, TyrannosaurusRexRenderer::new); + EntityRendererRegistry.register(ModEntities.VELOCIRAPTOR, VelociraptorRenderer::new); + EntityRendererRegistry.register(ModEntities.CHICKENOSAURUS, ChickenosaurusRenderer::new); + EntityRendererRegistry.register(ModEntities.ALLOSAURUS, AllosaurusRenderer::new); + EntityRendererRegistry.register(ModEntities.ALVAREZSAURUS, AlvarezsaurusRenderer::new); + EntityRendererRegistry.register(ModEntities.ANKYLOSAURUS, AnkylosaurusRenderer::new); + EntityRendererRegistry.register(ModEntities.CARCHARODONTOSAURUS, CarcharodontosaurusRenderer::new); + EntityRendererRegistry.register(ModEntities.CHASMOSAURUS, ChasmosaurusRenderer::new); + EntityRendererRegistry.register(ModEntities.COELOPHYSIS, CoelophysisRenderer::new); + EntityRendererRegistry.register(ModEntities.COELURUS, CoelurusRenderer::new); + EntityRendererRegistry.register(ModEntities.CORYTHOSAURUS, CorythosaurusRenderer::new); + EntityRendererRegistry.register(ModEntities.DRYOSAURUS, DryosaurusRenderer::new); + EntityRendererRegistry.register(ModEntities.HADROSAURUS, HadrosaurusRenderer::new); + EntityRendererRegistry.register(ModEntities.HYPSILOPHODON, HypsilophodonRenderer::new); + EntityRendererRegistry.register(ModEntities.INDORAPTOR, IndoraptorRenderer::new); + EntityRendererRegistry.register(ModEntities.INOSTRANCEVIA, InostranceviaRenderer::new); + EntityRendererRegistry.register(ModEntities.LAMBEOSAURUS, LambeosaurusRenderer::new); + EntityRendererRegistry.register(ModEntities.MAMENCHISAURUS, MamenchisaurusRenderer::new); + EntityRendererRegistry.register(ModEntities.METRIACANTHOSAURUS, MetriacanthosaurusRenderer::new); + EntityRendererRegistry.register(ModEntities.ORNITHOLESTES, OrnitholestesRenderer::new); + EntityRendererRegistry.register(ModEntities.ORNITHOMIMUS, OrnithomimusRenderer::new); + EntityRendererRegistry.register(ModEntities.OVIRAPTOR, OviraptorRenderer::new); + EntityRendererRegistry.register(ModEntities.PACHYCEPHALOSAURUS, PachycephalosaurusRenderer::new); + EntityRendererRegistry.register(ModEntities.PROCERATOSAURUS, ProceratosaurusRenderer::new); + EntityRendererRegistry.register(ModEntities.RAJASAURUS, RajasaurusRenderer::new); + EntityRendererRegistry.register(ModEntities.SEGISAURUS, SegisaurusRenderer::new); + EntityRendererRegistry.register(ModEntities.TITANOSAURUS, TitanosaurusRenderer::new); + EntityRendererRegistry.register(ModEntities.TROODON, TroodonRenderer::new); + EntityRendererRegistry.register(ModEntities.UTAHRAPTOR, UtahraptorRenderer::new); + + //? if <=1.20.1 { + LifecycleEvent.SETUP.register(() -> { + MenuRegistry.registerScreenFactory(ModMenuTypes.GENERATOR_MENU.get(), GeneratorScreen::new); + MenuRegistry.registerScreenFactory(ModMenuTypes.DNA_EXTRACTOR_MENU.get(), DNAExtractorScreen::new); + MenuRegistry.registerScreenFactory(ModMenuTypes.DNA_ANALYZER_MENU.get(), DNAAnalyzerScreen::new); + MenuRegistry.registerScreenFactory(ModMenuTypes.FOSSIL_GRINDER_MENU.get(), FossilGrinderScreen::new); + MenuRegistry.registerScreenFactory(ModMenuTypes.FOSSIL_CLEANER_MENU.get(), FossilCleanerScreen::new); + MenuRegistry.registerScreenFactory(ModMenuTypes.DNA_HYBRIDIZER_MENU.get(), DNAHybridizerScreen::new); + MenuRegistry.registerScreenFactory(ModMenuTypes.EMBRYONIC_MACHINE_MENU.get(), EmbryonicMachineScreen::new); + MenuRegistry.registerScreenFactory(ModMenuTypes.EMBRYO_CALCIFICATION_MACHINE_MENU.get(), EmbryoCalcificationMachineScreen::new); + MenuRegistry.registerScreenFactory(ModMenuTypes.INCUBATOR_MENU.get(), IncubatorScreen::new); + MenuRegistry.registerScreenFactory(ModMenuTypes.TANK_MENU.get(), TankScreen::new); + MenuRegistry.registerScreenFactory(ModMenuTypes.POWER_CELL_MENU.get(), PowerCellScreen::new); + MenuRegistry.registerScreenFactory(ModMenuTypes.WOOD_CRATE_MENU.get(), CrateScreen::new); + MenuRegistry.registerScreenFactory(ModMenuTypes.IRON_CRATE_MENU.get(), CrateScreen::new); + }); + //?} + + LifecycleEvent.SETUP.register(() -> { + // Register Block Entity Renderers (SETUP is fine for these) + BlockEntityRendererRegistry.register(ModBlockEntities.TANK_BE.get(), TankBlockEntityRenderer::new); + }); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/Constants.java b/common/src/main/java/net/cmr/jurassicrevived/Constants.java index 94d32ec..ef1d2af 100644 --- a/common/src/main/java/net/cmr/jurassicrevived/Constants.java +++ b/common/src/main/java/net/cmr/jurassicrevived/Constants.java @@ -1,5 +1,6 @@ package net.cmr.jurassicrevived; +import net.minecraft.resources.ResourceLocation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -9,4 +10,12 @@ public class Constants public static final String MOD_ID = "jurassicrevived"; public static final String MOD_NAME = "JurassicRevived"; public static final Logger LOG = LoggerFactory.getLogger(MOD_NAME); + + public static ResourceLocation rl(String path) { + //? if >1.20.1 { + /*return ResourceLocation.fromNamespaceAndPath(MOD_ID, path); + *///?} else { + return new ResourceLocation(MOD_ID, path); + //?} + } } \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/block/ModBlocks.java b/common/src/main/java/net/cmr/jurassicrevived/block/ModBlocks.java index e689e25..e090838 100644 --- a/common/src/main/java/net/cmr/jurassicrevived/block/ModBlocks.java +++ b/common/src/main/java/net/cmr/jurassicrevived/block/ModBlocks.java @@ -3,13 +3,19 @@ package net.cmr.jurassicrevived.block; import dev.architectury.registry.registries.DeferredRegister; import dev.architectury.registry.registries.RegistrySupplier; import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.block.custom.*; +import net.cmr.jurassicrevived.block.custom.PipeBlock; +import net.cmr.jurassicrevived.entity.ModEntities; import net.cmr.jurassicrevived.item.ModItems; +import net.cmr.jurassicrevived.mixin.FlowerPotBlockAccessor; import net.minecraft.core.registries.Registries; +import net.minecraft.world.effect.MobEffects; import net.minecraft.world.item.BlockItem; import net.minecraft.world.item.Item; -import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.*; import net.minecraft.world.level.block.state.BlockBehaviour; +import java.util.Map; import java.util.function.Supplier; public class ModBlocks { @@ -20,6 +26,647 @@ public class ModBlocks { public static final RegistrySupplier FOSSIL_ORE = registerBlock("fossil_ore", () -> new Block(BlockBehaviour.Properties.of().strength(3.0f))); + public static final RegistrySupplier CAT_PLUSHIE = registerBlock("cat_plushie", + () -> new DecoBlock(BlockBehaviour.Properties.of().noOcclusion().sound(SoundType.WOOL))); + + public static final RegistrySupplier TRASH_CAN = registerBlock("trash_can", + () -> new TrashBlock(BlockBehaviour.Properties.of().noOcclusion())); + + public static final RegistrySupplier BENCH = registerBlock("bench", + () -> new BenchBlock(BlockBehaviour.Properties.of().noOcclusion())); + + public static final RegistrySupplier CHARRED_TERRACOTTA = registerBlock("charred_terracotta", + () -> new Block(BlockBehaviour.Properties.of().noOcclusion())); + + public static final RegistrySupplier LIGHT_POST = registerBlock("light_post", + () -> new LightPostBlock(BlockBehaviour.Properties.of().noOcclusion().lightLevel(state -> 15))); + + public static final RegistrySupplier ITEM_PIPE = registerBlock("item_pipe", + () -> new PipeBlock(BlockBehaviour.Properties.of().strength(1.0F).noOcclusion(), PipeBlock.Transport.ITEMS)); + + public static final RegistrySupplier FLUID_PIPE = registerBlock("fluid_pipe", + () -> new PipeBlock(BlockBehaviour.Properties.of().strength(1.0F).noOcclusion(), PipeBlock.Transport.FLUIDS)); + + public static final RegistrySupplier POWER_PIPE = registerBlock("power_pipe", + () -> new PipeBlock(BlockBehaviour.Properties.of().strength(1.0F).noOcclusion(), PipeBlock.Transport.ENERGY)); + + public static final RegistrySupplier TANK = registerBlock("tank", + () -> new TankBlock(BlockBehaviour.Properties.of().strength(3f).requiresCorrectToolForDrops().noOcclusion().noLootTable())); + + public static final RegistrySupplier POWER_CELL = registerBlock("power_cell", + () -> new PowerCellBlock(BlockBehaviour.Properties.of().strength(3f).requiresCorrectToolForDrops().noOcclusion().noLootTable())); + + public static final RegistrySupplier WOOD_CRATE = registerBlock("wood_crate", + () -> new CrateBlock(BlockBehaviour.Properties.of().strength(2.0f).noOcclusion().noLootTable().sound(SoundType.WOOD), 9)); + public static final RegistrySupplier IRON_CRATE = registerBlock("iron_crate", + () -> new CrateBlock(BlockBehaviour.Properties.of().strength(2.0f).requiresCorrectToolForDrops().noOcclusion().noLootTable(), 18)); + + public static final RegistrySupplier GENERATOR = registerBlock("generator", + () -> new GeneratorBlock(BlockBehaviour.Properties.of().noOcclusion().requiresCorrectToolForDrops().strength(4f).noLootTable())); + public static final RegistrySupplier DNA_EXTRACTOR = registerBlock("dna_extractor", + () -> new DNAExtractorBlock(BlockBehaviour.Properties.of().noOcclusion().requiresCorrectToolForDrops().strength(4f).noLootTable())); + public static final RegistrySupplier DNA_ANALYZER = registerBlock("dna_analyzer", + () -> new DNAAnalyzerBlock(BlockBehaviour.Properties.of().noOcclusion().requiresCorrectToolForDrops().strength(4f).noLootTable())); + public static final RegistrySupplier FOSSIL_GRINDER = registerBlock("fossil_grinder", + () -> new FossilGrinderBlock(BlockBehaviour.Properties.of().noOcclusion().requiresCorrectToolForDrops().strength(4f).noLootTable())); + public static final RegistrySupplier FOSSIL_CLEANER = registerBlock("fossil_cleaner", + () -> new FossilCleanerBlock(BlockBehaviour.Properties.of().noOcclusion().requiresCorrectToolForDrops().strength(4f).noLootTable())); + public static final RegistrySupplier DNA_HYBRIDIZER = registerBlock("dna_hybridizer", + () -> new DNAHybridizerBlock(BlockBehaviour.Properties.of().noOcclusion().requiresCorrectToolForDrops().strength(4f).noLootTable())); + public static final RegistrySupplier EMBRYONIC_MACHINE = registerBlock("embryonic_machine", + () -> new EmbryonicMachineBlock(BlockBehaviour.Properties.of().noOcclusion().requiresCorrectToolForDrops().strength(4f).noLootTable())); + public static final RegistrySupplier EMBRYO_CALCIFICATION_MACHINE = registerBlock("embryo_calcification_machine", + () -> new EmbryoCalcificationMachineBlock(BlockBehaviour.Properties.of().noOcclusion().requiresCorrectToolForDrops().strength(4f).noLootTable())); + public static final RegistrySupplier INCUBATOR = registerBlock("incubator", + () -> new IncubatorBlock(BlockBehaviour.Properties.of().noOcclusion().requiresCorrectToolForDrops().strength(4f).noLootTable())); + public static final RegistrySupplier WHITE_GENERATOR = registerBlock("white_generator", + () -> new GeneratorBlock(BlockBehaviour.Properties.of().noOcclusion().requiresCorrectToolForDrops().strength(4f).noLootTable())); + public static final RegistrySupplier WHITE_DNA_EXTRACTOR = registerBlock("white_dna_extractor", + () -> new DNAExtractorBlock(BlockBehaviour.Properties.of().noOcclusion().requiresCorrectToolForDrops().strength(4f).noLootTable())); + public static final RegistrySupplier WHITE_DNA_ANALYZER = registerBlock("white_dna_analyzer", + () -> new DNAAnalyzerBlock(BlockBehaviour.Properties.of().noOcclusion().requiresCorrectToolForDrops().strength(4f).noLootTable())); + public static final RegistrySupplier WHITE_FOSSIL_GRINDER = registerBlock("white_fossil_grinder", + () -> new FossilGrinderBlock(BlockBehaviour.Properties.of().noOcclusion().requiresCorrectToolForDrops().strength(4f).noLootTable())); + public static final RegistrySupplier WHITE_FOSSIL_CLEANER = registerBlock("white_fossil_cleaner", + () -> new FossilCleanerBlock(BlockBehaviour.Properties.of().noOcclusion().requiresCorrectToolForDrops().strength(4f).noLootTable())); + public static final RegistrySupplier WHITE_DNA_HYBRIDIZER = registerBlock("white_dna_hybridizer", + () -> new DNAHybridizerBlock(BlockBehaviour.Properties.of().noOcclusion().requiresCorrectToolForDrops().strength(4f).noLootTable())); + public static final RegistrySupplier WHITE_EMBRYONIC_MACHINE = registerBlock("white_embryonic_machine", + () -> new EmbryonicMachineBlock(BlockBehaviour.Properties.of().noOcclusion().requiresCorrectToolForDrops().strength(4f).noLootTable())); + public static final RegistrySupplier WHITE_EMBRYO_CALCIFICATION_MACHINE = registerBlock("white_embryo_calcification_machine", + () -> new EmbryoCalcificationMachineBlock(BlockBehaviour.Properties.of().noOcclusion().requiresCorrectToolForDrops().strength(4f).noLootTable())); + public static final RegistrySupplier WHITE_INCUBATOR = registerBlock("white_incubator", + () -> new IncubatorBlock(BlockBehaviour.Properties.of().noOcclusion().requiresCorrectToolForDrops().strength(4f).noLootTable())); + + //? if >1.20.1 { + /*public static final RegistrySupplier ROYAL_FERN = registerBlock("royal_fern", + () -> new FlowerBlock(MobEffects.UNLUCK, 0, BlockBehaviour.Properties.ofFullCopy(Blocks.ALLIUM))); + public static final RegistrySupplier POTTED_ROYAL_FERN = BLOCKS.register("potted_royal_fern", + () -> new FlowerPotBlock(ROYAL_FERN.get(), BlockBehaviour.Properties.ofFullCopy(Blocks.POTTED_ALLIUM))); + + public static final RegistrySupplier HORSETAIL_FERN = registerBlock("horsetail_fern", + () -> new FlowerBlock(MobEffects.UNLUCK, 0, BlockBehaviour.Properties.ofFullCopy(Blocks.ALLIUM))); + public static final RegistrySupplier POTTED_HORSETAIL_FERN = BLOCKS.register("potted_horsetail_fern", + () -> new FlowerPotBlock(HORSETAIL_FERN.get(), BlockBehaviour.Properties.ofFullCopy(Blocks.POTTED_ALLIUM))); + + public static final RegistrySupplier WESTERN_SWORD_FERN = registerBlock("western_sword_fern", + () -> new FlowerBlock(MobEffects.UNLUCK, 0, BlockBehaviour.Properties.ofFullCopy(Blocks.ALLIUM))); + public static final RegistrySupplier POTTED_WESTERN_SWORD_FERN = BLOCKS.register("potted_western_sword_fern", + () -> new FlowerPotBlock(WESTERN_SWORD_FERN.get(), BlockBehaviour.Properties.ofFullCopy(Blocks.POTTED_ALLIUM))); + + public static final RegistrySupplier ONYCHIOPSIS = registerBlock("onychiopsis", + () -> new FlowerBlock(MobEffects.UNLUCK, 0, BlockBehaviour.Properties.ofFullCopy(Blocks.ALLIUM))); + public static final RegistrySupplier POTTED_ONYCHIOPSIS = BLOCKS.register("potted_onychiopsis", + () -> new FlowerPotBlock(ONYCHIOPSIS.get(), BlockBehaviour.Properties.ofFullCopy(Blocks.POTTED_ALLIUM))); + *///?} else { + public static final RegistrySupplier ROYAL_FERN = registerBlock("royal_fern", + () -> new FlowerBlock(MobEffects.UNLUCK, 0, BlockBehaviour.Properties.copy(Blocks.ALLIUM))); + public static final RegistrySupplier POTTED_ROYAL_FERN = BLOCKS.register("potted_royal_fern", + () -> new FlowerPotBlock(ROYAL_FERN.get(), BlockBehaviour.Properties.copy(Blocks.POTTED_ALLIUM))); + + public static final RegistrySupplier HORSETAIL_FERN = registerBlock("horsetail_fern", + () -> new FlowerBlock(MobEffects.UNLUCK, 0, BlockBehaviour.Properties.copy(Blocks.ALLIUM))); + public static final RegistrySupplier POTTED_HORSETAIL_FERN = BLOCKS.register("potted_horsetail_fern", + () -> new FlowerPotBlock(HORSETAIL_FERN.get(), BlockBehaviour.Properties.copy(Blocks.POTTED_ALLIUM))); + + public static final RegistrySupplier WESTERN_SWORD_FERN = registerBlock("western_sword_fern", + () -> new FlowerBlock(MobEffects.UNLUCK, 0, BlockBehaviour.Properties.copy(Blocks.ALLIUM))); + public static final RegistrySupplier POTTED_WESTERN_SWORD_FERN = BLOCKS.register("potted_western_sword_fern", + () -> new FlowerPotBlock(WESTERN_SWORD_FERN.get(), BlockBehaviour.Properties.copy(Blocks.POTTED_ALLIUM))); + + public static final RegistrySupplier ONYCHIOPSIS = registerBlock("onychiopsis", + () -> new FlowerBlock(MobEffects.UNLUCK, 0, BlockBehaviour.Properties.copy(Blocks.ALLIUM))); + public static final RegistrySupplier POTTED_ONYCHIOPSIS = BLOCKS.register("potted_onychiopsis", + () -> new FlowerPotBlock(ONYCHIOPSIS.get(), BlockBehaviour.Properties.copy(Blocks.POTTED_ALLIUM))); + //?} + + public static void setupPots() { + Map map = null; + + // Attempt Mixin cast safely + try { + Object pot = Blocks.FLOWER_POT; + if (pot instanceof FlowerPotBlockAccessor accessor) { + map = accessor.getPottedByContent(); + } + } catch (Throwable ignored) {} + + // Fallback to Reflection if Mixin didn't apply (common on early Fabric loading) + if (map == null) { + try { + java.lang.reflect.Field field = FlowerPotBlock.class.getDeclaredField("POTTED_BY_CONTENT"); + field.setAccessible(true); + map = (Map) field.get(null); + } catch (Exception e) { + Constants.LOG.error("Could not access FlowerPotBlock.POTTED_BY_CONTENT", e); + } + } + + if (map != null) { + if (ROYAL_FERN.isPresent()) map.put(ROYAL_FERN.get(), POTTED_ROYAL_FERN.get()); + if (HORSETAIL_FERN.isPresent()) map.put(HORSETAIL_FERN.get(), POTTED_HORSETAIL_FERN.get()); + if (WESTERN_SWORD_FERN.isPresent()) map.put(WESTERN_SWORD_FERN.get(), POTTED_WESTERN_SWORD_FERN.get()); + if (ONYCHIOPSIS.isPresent()) map.put(ONYCHIOPSIS.get(), POTTED_ONYCHIOPSIS.get()); + } + } + + public static final RegistrySupplier GYPSUM_STONE = registerBlock("gypsum_stone", + () -> new Block(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops())); + public static final RegistrySupplier GYPSUM_COBBLESTONE = registerBlock("gypsum_cobblestone", + () -> new Block(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops())); + public static final RegistrySupplier GYPSUM_STONE_BRICKS = registerBlock("gypsum_stone_bricks", + () -> new Block(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops())); + public static final RegistrySupplier SMOOTH_GYPSUM_STONE = registerBlock("smooth_gypsum_stone", + () -> new Block(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops())); + public static final RegistrySupplier CHISELED_GYPSUM_STONE = registerBlock("chiseled_gypsum_stone", + () -> new Block(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops())); + + public static final RegistrySupplier GYPSUM_BRICK_STAIRS = registerBlock("gypsum_brick_stairs", + () -> new StairBlock(ModBlocks.GYPSUM_STONE_BRICKS.get().defaultBlockState(), + BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops())); + public static final RegistrySupplier GYPSUM_BRICK_SLAB = registerBlock("gypsum_brick_slab", + () -> new SlabBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops())); + public static final RegistrySupplier GYPSUM_BRICK_WALL = registerBlock("gypsum_brick_wall", + () -> new WallBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops())); + + public static final RegistrySupplier FENCE_LIGHT = registerBlock("fence_light", + () -> new FenceLightBlock(BlockBehaviour.Properties.of().strength(1.0F).noOcclusion().lightLevel(state -> 15))); + + public static final RegistrySupplier LOW_SECURITY_FENCE_POLE = registerBlock("low_security_fence_pole", + () -> new FencePoleBlock(BlockBehaviour.Properties.of().strength(1.0F).noOcclusion().requiresCorrectToolForDrops(), FencePoleBlock.Tier.LOW)); + public static final RegistrySupplier LOW_SECURITY_FENCE_WIRE = registerBlock("low_security_fence_wire", + () -> new FenceWireBlock(BlockBehaviour.Properties.of().strength(0.5F).noOcclusion().requiresCorrectToolForDrops(), FenceWireBlock.Tier.LOW)); + public static final RegistrySupplier MEDIUM_SECURITY_FENCE_POLE = registerBlock("medium_security_fence_pole", + () -> new FencePoleBlock(BlockBehaviour.Properties.of().strength(1.0F).noOcclusion().requiresCorrectToolForDrops(), FencePoleBlock.Tier.MEDIUM)); + public static final RegistrySupplier MEDIUM_SECURITY_FENCE_WIRE = registerBlock("medium_security_fence_wire", + () -> new FenceWireBlock(BlockBehaviour.Properties.of().strength(0.5F).noOcclusion().requiresCorrectToolForDrops(), FenceWireBlock.Tier.MEDIUM)); + + + + public static final RegistrySupplier STONE_FOSSIL = registerBlock("stone_fossil", + () -> new Block(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops())); + public static final RegistrySupplier DEEPSLATE_FOSSIL = registerBlock("deepslate_fossil", + () -> new Block(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops())); + public static final RegistrySupplier AMBER_ORE = registerBlock("amber_ore", + () -> new Block(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops())); + public static final RegistrySupplier DEEPSLATE_ICE_SHARD_ORE = registerBlock("deepslate_ice_shard_ore", + () -> new Block(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops())); + + public static final RegistrySupplier REINFORCED_STONE = registerBlock("reinforced_stone", + () -> new Block(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops())); + public static final RegistrySupplier REINFORCED_STONE_BRICKS = registerBlock("reinforced_stone_bricks", + () -> new Block(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops())); + public static final RegistrySupplier CHISELED_REINFORCED_STONE = registerBlock("chiseled_reinforced_stone", + () -> new Block(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops())); + + public static final RegistrySupplier REINFORCED_BRICK_STAIRS = registerBlock("reinforced_brick_stairs", + () -> new StairBlock(ModBlocks.REINFORCED_STONE_BRICKS.get().defaultBlockState(), + BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops())); + public static final RegistrySupplier REINFORCED_BRICK_SLAB = registerBlock("reinforced_brick_slab", + () -> new SlabBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops())); + public static final RegistrySupplier REINFORCED_BRICK_WALL = registerBlock("reinforced_brick_wall", + () -> new WallBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops())); + + public static final RegistrySupplier ALBERTOSAURUS_EGG = registerBlock("albertosaurus_egg", + () -> new EggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.ALBERTOSAURUS)); + + public static final RegistrySupplier APATOSAURUS_EGG = registerBlock("apatosaurus_egg", + () -> new EggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.APATOSAURUS)); + + public static final RegistrySupplier BRACHIOSAURUS_EGG = registerBlock("brachiosaurus_egg", + () -> new EggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.BRACHIOSAURUS)); + + public static final RegistrySupplier CERATOSAURUS_EGG = registerBlock("ceratosaurus_egg", + () -> new EggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.CERATOSAURUS)); + + public static final RegistrySupplier COMPSOGNATHUS_EGG = registerBlock("compsognathus_egg", + () -> new EggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.COMPSOGNATHUS)); + + public static final RegistrySupplier DILOPHOSAURUS_EGG = registerBlock("dilophosaurus_egg", + () -> new EggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.DILOPHOSAURUS)); + + public static final RegistrySupplier DIPLODOCUS_EGG = registerBlock("diplodocus_egg", + () -> new EggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.DIPLODOCUS)); + + public static final RegistrySupplier GALLIMIMUS_EGG = registerBlock("gallimimus_egg", + () -> new EggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.GALLIMIMUS)); + + public static final RegistrySupplier INDOMINUS_REX_EGG = registerBlock("indominus_rex_egg", + () -> new EggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.INDOMINUS_REX)); + + public static final RegistrySupplier OURANOSAURUS_EGG = registerBlock("ouranosaurus_egg", + () -> new EggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.OURANOSAURUS)); + + public static final RegistrySupplier PARASAUROLOPHUS_EGG = registerBlock("parasaurolophus_egg", + () -> new EggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.PARASAUROLOPHUS)); + + public static final RegistrySupplier SPINOSAURUS_EGG = registerBlock("spinosaurus_egg", + () -> new EggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.SPINOSAURUS)); + + public static final RegistrySupplier TRICERATOPS_EGG = registerBlock("triceratops_egg", + () -> new EggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.TRICERATOPS)); + + public static final RegistrySupplier TYRANNOSAURUS_REX_EGG = registerBlock("tyrannosaurus_rex_egg", + () -> new EggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.TYRANNOSAURUS_REX)); + + public static final RegistrySupplier VELOCIRAPTOR_EGG = registerBlock("velociraptor_egg", + () -> new EggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.VELOCIRAPTOR)); + + public static final RegistrySupplier BARYONYX_EGG = registerBlock("baryonyx_egg", + () -> new EggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.BARYONYX)); + + public static final RegistrySupplier CARNOTAURUS_EGG = registerBlock("carnotaurus_egg", + () -> new EggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.CARNOTAURUS)); + + public static final RegistrySupplier CONCAVENATOR_EGG = registerBlock("concavenator_egg", + () -> new EggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.CONCAVENATOR)); + + public static final RegistrySupplier DEINONYCHUS_EGG = registerBlock("deinonychus_egg", + () -> new EggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.DEINONYCHUS)); + + public static final RegistrySupplier EDMONTOSAURUS_EGG = registerBlock("edmontosaurus_egg", + () -> new EggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.EDMONTOSAURUS)); + + public static final RegistrySupplier GIGANOTOSAURUS_EGG = registerBlock("giganotosaurus_egg", + () -> new EggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.GIGANOTOSAURUS)); + + public static final RegistrySupplier GUANLONG_EGG = registerBlock("guanlong_egg", + () -> new EggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.GUANLONG)); + + public static final RegistrySupplier HERRERASAURUS_EGG = registerBlock("herrerasaurus_egg", + () -> new EggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.HERRERASAURUS)); + + public static final RegistrySupplier MAJUNGASAURUS_EGG = registerBlock("majungasaurus_egg", + () -> new EggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.MAJUNGASAURUS)); + + public static final RegistrySupplier PROCOMPSOGNATHUS_EGG = registerBlock("procompsognathus_egg", + () -> new EggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.PROCOMPSOGNATHUS)); + + public static final RegistrySupplier PROTOCERATOPS_EGG = registerBlock("protoceratops_egg", + () -> new EggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.PROTOCERATOPS)); + + public static final RegistrySupplier RUGOPS_EGG = registerBlock("rugops_egg", + () -> new EggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.RUGOPS)); + + public static final RegistrySupplier SHANTUNGOSAURUS_EGG = registerBlock("shantungosaurus_egg", + () -> new EggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.SHANTUNGOSAURUS)); + + public static final RegistrySupplier STEGOSAURUS_EGG = registerBlock("stegosaurus_egg", + () -> new EggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.STEGOSAURUS)); + + public static final RegistrySupplier STYRACOSAURUS_EGG = registerBlock("styracosaurus_egg", + () -> new EggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.STYRACOSAURUS)); + + public static final RegistrySupplier THERIZINOSAURUS_EGG = registerBlock("therizinosaurus_egg", + () -> new EggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.THERIZINOSAURUS)); + + public static final RegistrySupplier DISTORTUS_REX_EGG = registerBlock("distortus_rex_egg", + () -> new EggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.DISTORTUS_REX)); + + public static final RegistrySupplier ALLOSAURUS_EGG = registerBlock("allosaurus_egg", + () -> new EggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.ALLOSAURUS)); + + public static final RegistrySupplier ALVAREZSAURUS_EGG = registerBlock("alvarezsaurus_egg", + () -> new EggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.ALVAREZSAURUS)); + + public static final RegistrySupplier ANKYLOSAURUS_EGG = registerBlock("ankylosaurus_egg", + () -> new EggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.ANKYLOSAURUS)); + + public static final RegistrySupplier ARAMBOURGIANIA_EGG = registerBlock("arambourgiania_egg", + () -> new EggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.ARAMBOURGIANIA)); + + public static final RegistrySupplier CARCHARODONTOSAURUS_EGG = registerBlock("carcharodontosaurus_egg", + () -> new EggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.CARCHARODONTOSAURUS)); + + public static final RegistrySupplier CEARADACTYLUS_EGG = registerBlock("cearadactylus_egg", + () -> new EggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.CEARADACTYLUS)); + + public static final RegistrySupplier CHASMOSAURUS_EGG = registerBlock("chasmosaurus_egg", + () -> new EggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.CHASMOSAURUS)); + + public static final RegistrySupplier COELOPHYSIS_EGG = registerBlock("coelophysis_egg", + () -> new EggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.COELOPHYSIS)); + + public static final RegistrySupplier COELURUS_EGG = registerBlock("coelurus_egg", + () -> new EggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.COELURUS)); + + public static final RegistrySupplier CORYTHOSAURUS_EGG = registerBlock("corythosaurus_egg", + () -> new EggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.CORYTHOSAURUS)); + + public static final RegistrySupplier DIMORPHODON_EGG = registerBlock("dimorphodon_egg", + () -> new EggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.DIMORPHODON)); + + public static final RegistrySupplier DRYOSAURUS_EGG = registerBlock("dryosaurus_egg", + () -> new EggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.DRYOSAURUS)); + + public static final RegistrySupplier GEOSTERNBERGIA_EGG = registerBlock("geosternbergia_egg", + () -> new EggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.GEOSTERNBERGIA)); + + public static final RegistrySupplier GUIDRACO_EGG = registerBlock("guidraco_egg", + () -> new EggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.GUIDRACO)); + + public static final RegistrySupplier HADROSAURUS_EGG = registerBlock("hadrosaurus_egg", + () -> new EggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.HADROSAURUS)); + + public static final RegistrySupplier HYPSILOPHODON_EGG = registerBlock("hypsilophodon_egg", + () -> new EggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.HYPSILOPHODON)); + + public static final RegistrySupplier INDORAPTOR_EGG = registerBlock("indoraptor_egg", + () -> new EggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.INDORAPTOR)); + + public static final RegistrySupplier INOSTRANCEVIA_EGG = registerBlock("inostrancevia_egg", + () -> new EggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.INOSTRANCEVIA)); + + public static final RegistrySupplier LAMBEOSAURUS_EGG = registerBlock("lambeosaurus_egg", + () -> new EggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.LAMBEOSAURUS)); + + public static final RegistrySupplier LUDODACTYLUS_EGG = registerBlock("ludodactylus_egg", + () -> new EggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.LUDODACTYLUS)); + + public static final RegistrySupplier MAMENCHISAURUS_EGG = registerBlock("mamenchisaurus_egg", + () -> new EggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.MAMENCHISAURUS)); + + public static final RegistrySupplier METRIACANTHOSAURUS_EGG = registerBlock("metriacanthosaurus_egg", + () -> new EggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.METRIACANTHOSAURUS)); + + public static final RegistrySupplier MOGANOPTERUS_EGG = registerBlock("moganopterus_egg", + () -> new EggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.MOGANOPTERUS)); + + public static final RegistrySupplier NYCTOSAURUS_EGG = registerBlock("nyctosaurus_egg", + () -> new EggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.NYCTOSAURUS)); + + public static final RegistrySupplier ORNITHOLESTES_EGG = registerBlock("ornitholestes_egg", + () -> new EggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.ORNITHOLESTES)); + + public static final RegistrySupplier ORNITHOMIMUS_EGG = registerBlock("ornithomimus_egg", + () -> new EggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.ORNITHOMIMUS)); + + public static final RegistrySupplier OVIRAPTOR_EGG = registerBlock("oviraptor_egg", + () -> new EggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.OVIRAPTOR)); + + public static final RegistrySupplier PACHYCEPHALOSAURUS_EGG = registerBlock("pachycephalosaurus_egg", + () -> new EggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.PACHYCEPHALOSAURUS)); + + public static final RegistrySupplier PROCERATOSAURUS_EGG = registerBlock("proceratosaurus_egg", + () -> new EggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.PROCERATOSAURUS)); + + public static final RegistrySupplier PTERANODON_EGG = registerBlock("pteranodon_egg", + () -> new EggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.PTERANODON)); + + public static final RegistrySupplier PTERODAUSTRO_EGG = registerBlock("pterodaustro_egg", + () -> new EggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.PTERODAUSTRO)); + + public static final RegistrySupplier QUETZALCOATLUS_EGG = registerBlock("quetzalcoatlus_egg", + () -> new EggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.QUETZALCOATLUS)); + + public static final RegistrySupplier RAJASAURUS_EGG = registerBlock("rajasaurus_egg", + () -> new EggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.RAJASAURUS)); + + public static final RegistrySupplier SEGISAURUS_EGG = registerBlock("segisaurus_egg", + () -> new EggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.SEGISAURUS)); + + public static final RegistrySupplier TAPEJARA_EGG = registerBlock("tapejara_egg", + () -> new EggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.TAPEJARA)); + + public static final RegistrySupplier TITANOSAURUS_EGG = registerBlock("titanosaurus_egg", + () -> new EggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.TITANOSAURUS)); + + public static final RegistrySupplier TROODON_EGG = registerBlock("troodon_egg", + () -> new EggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.TROODON)); + + public static final RegistrySupplier TROPEOGNATHUS_EGG = registerBlock("tropeognathus_egg", + () -> new EggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.TROPEOGNATHUS)); + + public static final RegistrySupplier TUPUXUARA_EGG = registerBlock("tupuxuara_egg", + () -> new EggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.TUPUXUARA)); + + public static final RegistrySupplier UTAHRAPTOR_EGG = registerBlock("utahraptor_egg", + () -> new EggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.UTAHRAPTOR)); + + public static final RegistrySupplier ZHENYUANOPTERUS_EGG = registerBlock("zhenyuanopterus_egg", + () -> new EggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.ZHENYUANOPTERUS)); + + + + public static final RegistrySupplier INCUBATED_APATOSAURUS_EGG = registerBlock("incubated_apatosaurus_egg", + () -> new IncubatedEggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.APATOSAURUS)); + + public static final RegistrySupplier INCUBATED_ALBERTOSAURUS_EGG = registerBlock("incubated_albertosaurus_egg", + () -> new IncubatedEggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.ALBERTOSAURUS)); + + public static final RegistrySupplier INCUBATED_VELOCIRAPTOR_EGG = registerBlock("incubated_velociraptor_egg", + () -> new IncubatedEggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.VELOCIRAPTOR)); + + public static final RegistrySupplier INCUBATED_TYRANNOSAURUS_REX_EGG = registerBlock("incubated_tyrannosaurus_rex_egg", + () -> new IncubatedEggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.TYRANNOSAURUS_REX)); + + public static final RegistrySupplier INCUBATED_TRICERATOPS_EGG = registerBlock("incubated_triceratops_egg", + () -> new IncubatedEggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.TRICERATOPS)); + + public static final RegistrySupplier INCUBATED_SPINOSAURUS_EGG = registerBlock("incubated_spinosaurus_egg", + () -> new IncubatedEggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.SPINOSAURUS)); + + public static final RegistrySupplier INCUBATED_PARASAUROLOPHUS_EGG = registerBlock("incubated_parasaurolophus_egg", + () -> new IncubatedEggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.PARASAUROLOPHUS)); + + public static final RegistrySupplier INCUBATED_INDOMINUS_REX_EGG = registerBlock("incubated_indominus_rex_egg", + () -> new IncubatedEggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.INDOMINUS_REX)); + + public static final RegistrySupplier INCUBATED_GALLIMIMUS_EGG = registerBlock("incubated_gallimimus_egg", + () -> new IncubatedEggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.GALLIMIMUS)); + + public static final RegistrySupplier INCUBATED_DIPLODOCUS_EGG = registerBlock("incubated_diplodocus_egg", + () -> new IncubatedEggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.DIPLODOCUS)); + + public static final RegistrySupplier INCUBATED_OURANOSAURUS_EGG = registerBlock("incubated_ouranosaurus_egg", + () -> new IncubatedEggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.OURANOSAURUS)); + + public static final RegistrySupplier INCUBATED_DILOPHOSAURUS_EGG = registerBlock("incubated_dilophosaurus_egg", + () -> new IncubatedEggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.DILOPHOSAURUS)); + + public static final RegistrySupplier INCUBATED_COMPSOGNATHUS_EGG = registerBlock("incubated_compsognathus_egg", + () -> new IncubatedEggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.COMPSOGNATHUS)); + + public static final RegistrySupplier INCUBATED_CERATOSAURUS_EGG = registerBlock("incubated_ceratosaurus_egg", + () -> new IncubatedEggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.CERATOSAURUS)); + + public static final RegistrySupplier INCUBATED_BRACHIOSAURUS_EGG = registerBlock("incubated_brachiosaurus_egg", + () -> new IncubatedEggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.BRACHIOSAURUS)); + + public static final RegistrySupplier INCUBATED_BARYONYX_EGG = registerBlock("incubated_baryonyx_egg", + () -> new IncubatedEggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.BARYONYX)); + + public static final RegistrySupplier INCUBATED_CARNOTAURUS_EGG = registerBlock("incubated_carnotaurus_egg", + () -> new IncubatedEggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.CARNOTAURUS)); + + public static final RegistrySupplier INCUBATED_CONCAVENATOR_EGG = registerBlock("incubated_concavenator_egg", + () -> new IncubatedEggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.CONCAVENATOR)); + + public static final RegistrySupplier INCUBATED_DEINONYCHUS_EGG = registerBlock("incubated_deinonychus_egg", + () -> new IncubatedEggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.DEINONYCHUS)); + + public static final RegistrySupplier INCUBATED_EDMONTOSAURUS_EGG = registerBlock("incubated_edmontosaurus_egg", + () -> new IncubatedEggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.EDMONTOSAURUS)); + + public static final RegistrySupplier INCUBATED_GIGANOTOSAURUS_EGG = registerBlock("incubated_giganotosaurus_egg", + () -> new IncubatedEggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.GIGANOTOSAURUS)); + + public static final RegistrySupplier INCUBATED_GUANLONG_EGG = registerBlock("incubated_guanlong_egg", + () -> new IncubatedEggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.GUANLONG)); + + public static final RegistrySupplier INCUBATED_HERRERASAURUS_EGG = registerBlock("incubated_herrerasaurus_egg", + () -> new IncubatedEggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.HERRERASAURUS)); + + public static final RegistrySupplier INCUBATED_MAJUNGASAURUS_EGG = registerBlock("incubated_majungasaurus_egg", + () -> new IncubatedEggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.MAJUNGASAURUS)); + + public static final RegistrySupplier INCUBATED_PROCOMPSOGNATHUS_EGG = registerBlock("incubated_procompsognathus_egg", + () -> new IncubatedEggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.PROCOMPSOGNATHUS)); + + public static final RegistrySupplier INCUBATED_PROTOCERATOPS_EGG = registerBlock("incubated_protoceratops_egg", + () -> new IncubatedEggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.PROTOCERATOPS)); + + public static final RegistrySupplier INCUBATED_RUGOPS_EGG = registerBlock("incubated_rugops_egg", + () -> new IncubatedEggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.RUGOPS)); + + public static final RegistrySupplier INCUBATED_SHANTUNGOSAURUS_EGG = registerBlock("incubated_shantungosaurus_egg", + () -> new IncubatedEggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.SHANTUNGOSAURUS)); + + public static final RegistrySupplier INCUBATED_STEGOSAURUS_EGG = registerBlock("incubated_stegosaurus_egg", + () -> new IncubatedEggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.STEGOSAURUS)); + + public static final RegistrySupplier INCUBATED_STYRACOSAURUS_EGG = registerBlock("incubated_styracosaurus_egg", + () -> new IncubatedEggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.STYRACOSAURUS)); + + public static final RegistrySupplier INCUBATED_THERIZINOSAURUS_EGG = registerBlock("incubated_therizinosaurus_egg", + () -> new IncubatedEggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.THERIZINOSAURUS)); + + public static final RegistrySupplier INCUBATED_DISTORTUS_REX_EGG = registerBlock("incubated_distortus_rex_egg", + () -> new IncubatedEggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.DISTORTUS_REX)); + + public static final RegistrySupplier INCUBATED_ALLOSAURUS_EGG = registerBlock("incubated_allosaurus_egg", + () -> new IncubatedEggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.ALLOSAURUS)); + + public static final RegistrySupplier INCUBATED_ALVAREZSAURUS_EGG = registerBlock("incubated_alvarezsaurus_egg", + () -> new IncubatedEggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.ALVAREZSAURUS)); + + public static final RegistrySupplier INCUBATED_ANKYLOSAURUS_EGG = registerBlock("incubated_ankylosaurus_egg", + () -> new IncubatedEggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.ANKYLOSAURUS)); + + public static final RegistrySupplier INCUBATED_ARAMBOURGIANIA_EGG = registerBlock("incubated_arambourgiania_egg", + () -> new IncubatedEggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.ARAMBOURGIANIA)); + + public static final RegistrySupplier INCUBATED_CARCHARODONTOSAURUS_EGG = registerBlock("incubated_carcharodontosaurus_egg", + () -> new IncubatedEggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.CARCHARODONTOSAURUS)); + + public static final RegistrySupplier INCUBATED_CEARADACTYLUS_EGG = registerBlock("incubated_cearadactylus_egg", + () -> new IncubatedEggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.CEARADACTYLUS)); + + public static final RegistrySupplier INCUBATED_CHASMOSAURUS_EGG = registerBlock("incubated_chasmosaurus_egg", + () -> new IncubatedEggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.CHASMOSAURUS)); + + public static final RegistrySupplier INCUBATED_COELOPHYSIS_EGG = registerBlock("incubated_coelophysis_egg", + () -> new IncubatedEggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.COELOPHYSIS)); + + public static final RegistrySupplier INCUBATED_COELURUS_EGG = registerBlock("incubated_coelurus_egg", + () -> new IncubatedEggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.COELURUS)); + + public static final RegistrySupplier INCUBATED_CORYTHOSAURUS_EGG = registerBlock("incubated_corythosaurus_egg", + () -> new IncubatedEggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.CORYTHOSAURUS)); + + public static final RegistrySupplier INCUBATED_DIMORPHODON_EGG = registerBlock("incubated_dimorphodon_egg", + () -> new IncubatedEggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.DIMORPHODON)); + + public static final RegistrySupplier INCUBATED_DRYOSAURUS_EGG = registerBlock("incubated_dryosaurus_egg", + () -> new IncubatedEggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.DRYOSAURUS)); + + public static final RegistrySupplier INCUBATED_GEOSTERNBERGIA_EGG = registerBlock("incubated_geosternbergia_egg", + () -> new IncubatedEggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.GEOSTERNBERGIA)); + + public static final RegistrySupplier INCUBATED_GUIDRACO_EGG = registerBlock("incubated_guidraco_egg", + () -> new IncubatedEggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.GUIDRACO)); + + public static final RegistrySupplier INCUBATED_HADROSAURUS_EGG = registerBlock("incubated_hadrosaurus_egg", + () -> new IncubatedEggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.HADROSAURUS)); + + public static final RegistrySupplier INCUBATED_HYPSILOPHODON_EGG = registerBlock("incubated_hypsilophodon_egg", + () -> new IncubatedEggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.HYPSILOPHODON)); + + public static final RegistrySupplier INCUBATED_INDORAPTOR_EGG = registerBlock("incubated_indoraptor_egg", + () -> new IncubatedEggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.INDORAPTOR)); + + public static final RegistrySupplier INCUBATED_INOSTRANCEVIA_EGG = registerBlock("incubated_inostrancevia_egg", + () -> new IncubatedEggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.INOSTRANCEVIA)); + + public static final RegistrySupplier INCUBATED_LAMBEOSAURUS_EGG = registerBlock("incubated_lambeosaurus_egg", + () -> new IncubatedEggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.LAMBEOSAURUS)); + + public static final RegistrySupplier INCUBATED_LUDODACTYLUS_EGG = registerBlock("incubated_ludodactylus_egg", + () -> new IncubatedEggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.LUDODACTYLUS)); + + public static final RegistrySupplier INCUBATED_MAMENCHISAURUS_EGG = registerBlock("incubated_mamenchisaurus_egg", + () -> new IncubatedEggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.MAMENCHISAURUS)); + + public static final RegistrySupplier INCUBATED_METRIACANTHOSAURUS_EGG = registerBlock("incubated_metriacanthosaurus_egg", + () -> new IncubatedEggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.METRIACANTHOSAURUS)); + + public static final RegistrySupplier INCUBATED_MOGANOPTERUS_EGG = registerBlock("incubated_moganopterus_egg", + () -> new IncubatedEggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.MOGANOPTERUS)); + + public static final RegistrySupplier INCUBATED_NYCTOSAURUS_EGG = registerBlock("incubated_nyctosaurus_egg", + () -> new IncubatedEggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.NYCTOSAURUS)); + + public static final RegistrySupplier INCUBATED_ORNITHOLESTES_EGG = registerBlock("incubated_ornitholestes_egg", + () -> new IncubatedEggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.ORNITHOLESTES)); + + public static final RegistrySupplier INCUBATED_ORNITHOMIMUS_EGG = registerBlock("incubated_ornithomimus_egg", + () -> new IncubatedEggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.ORNITHOMIMUS)); + + public static final RegistrySupplier INCUBATED_OVIRAPTOR_EGG = registerBlock("incubated_oviraptor_egg", + () -> new IncubatedEggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.OVIRAPTOR)); + + public static final RegistrySupplier INCUBATED_PACHYCEPHALOSAURUS_EGG = registerBlock("incubated_pachycephalosaurus_egg", + () -> new IncubatedEggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.PACHYCEPHALOSAURUS)); + + public static final RegistrySupplier INCUBATED_PROCERATOSAURUS_EGG = registerBlock("incubated_proceratosaurus_egg", + () -> new IncubatedEggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.PROCERATOSAURUS)); + + public static final RegistrySupplier INCUBATED_PTERANODON_EGG = registerBlock("incubated_pteranodon_egg", + () -> new IncubatedEggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.PTERANODON)); + + public static final RegistrySupplier INCUBATED_PTERODAUSTRO_EGG = registerBlock("incubated_pterodaustro_egg", + () -> new IncubatedEggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.PTERODAUSTRO)); + + public static final RegistrySupplier INCUBATED_QUETZALCOATLUS_EGG = registerBlock("incubated_quetzalcoatlus_egg", + () -> new IncubatedEggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.QUETZALCOATLUS)); + + public static final RegistrySupplier INCUBATED_RAJASAURUS_EGG = registerBlock("incubated_rajasaurus_egg", + () -> new IncubatedEggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.RAJASAURUS)); + + public static final RegistrySupplier INCUBATED_SEGISAURUS_EGG = registerBlock("incubated_segisaurus_egg", + () -> new IncubatedEggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.SEGISAURUS)); + + public static final RegistrySupplier INCUBATED_TAPEJARA_EGG = registerBlock("incubated_tapejara_egg", + () -> new IncubatedEggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.TAPEJARA)); + + public static final RegistrySupplier INCUBATED_TITANOSAURUS_EGG = registerBlock("incubated_titanosaurus_egg", + () -> new IncubatedEggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.TITANOSAURUS)); + + public static final RegistrySupplier INCUBATED_TROODON_EGG = registerBlock("incubated_troodon_egg", + () -> new IncubatedEggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.TROODON)); + + public static final RegistrySupplier INCUBATED_TROPEOGNATHUS_EGG = registerBlock("incubated_tropeognathus_egg", + () -> new IncubatedEggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.TROPEOGNATHUS)); + + public static final RegistrySupplier INCUBATED_TUPUXUARA_EGG = registerBlock("incubated_tupuxuara_egg", + () -> new IncubatedEggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.TUPUXUARA)); + + public static final RegistrySupplier INCUBATED_UTAHRAPTOR_EGG = registerBlock("incubated_utahraptor_egg", + () -> new IncubatedEggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.UTAHRAPTOR)); + + public static final RegistrySupplier INCUBATED_ZHENYUANOPTERUS_EGG = registerBlock("incubated_zhenyuanopterus_egg", + () -> new IncubatedEggBlock(BlockBehaviour.Properties.of().strength(4f).requiresCorrectToolForDrops(), ModEntities.ZHENYUANOPTERUS)); + // --- Helper Methods --- diff --git a/common/src/main/java/net/cmr/jurassicrevived/block/custom/BenchBlock.java b/common/src/main/java/net/cmr/jurassicrevived/block/custom/BenchBlock.java new file mode 100644 index 0000000..6c7e427 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/block/custom/BenchBlock.java @@ -0,0 +1,223 @@ +package net.cmr.jurassicrevived.block.custom; + +import com.mojang.serialization.MapCodec; +import net.cmr.jurassicrevived.entity.ModEntities; +import net.cmr.jurassicrevived.entity.custom.SeatEntity; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.context.BlockPlaceContext; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.HorizontalDirectionalBlock; +import net.minecraft.world.level.block.Mirror; +import net.minecraft.world.level.block.Rotation; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.StateDefinition; +import net.minecraft.world.level.block.state.properties.Property; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.Vec3; +import net.minecraft.world.phys.shapes.CollisionContext; +import net.minecraft.world.phys.shapes.Shapes; +import net.minecraft.world.phys.shapes.VoxelShape; +// ... existing code ... + +public class BenchBlock extends HorizontalDirectionalBlock { + + public static final VoxelShape SHAPE = Block.box(0.0D, 0.0D, 0.0D, 16.0D, 19.0D, 19.0D); + + public BenchBlock(Properties properties) { + super(properties); + this.registerDefaultState((BlockState)((BlockState)((BlockState)this.stateDefinition.any()).setValue(FACING, Direction.SOUTH))); + } + + protected MapCodec codec() { + return null; + } + + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + builder.add(new Property[]{FACING}); + } + + public BlockState mirror(BlockState state, Mirror mirror) { + return state.rotate(mirror.getRotation((Direction)state.getValue(FACING))); + } + + // Ensure rotation of FACING property when the block is rotated (e.g., structure placement, commands, etc.) + @Override + public BlockState rotate(BlockState state, Rotation rotation) { + return state.setValue(FACING, rotation.rotate(state.getValue(FACING))); + } + + public BlockState getStateForPlacement(BlockPlaceContext context) { + // Ensure we always use a horizontal facing (N/E/S/W), even when looking up or down. + return (BlockState)this.defaultBlockState().setValue(FACING, context.getHorizontalDirection().getOpposite()); + } + + @Override + public VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) { + Direction facing = state.getValue(FACING); + // The SHAPE is defined for NORTH. Rotate it for other facings. + return switch (facing) { + case NORTH -> SHAPE; + case EAST -> rotateShape90Y(SHAPE); + case SOUTH -> rotateShape90Y(rotateShape90Y(SHAPE)); + case WEST -> rotateShape90Y(rotateShape90Y(rotateShape90Y(SHAPE))); + default -> SHAPE; + }; + } + + // Utility: rotate a VoxelShape 90 degrees around Y (from NORTH to EAST) + private static VoxelShape rotateShape90Y(VoxelShape shape) { + VoxelShape[] buffer = new VoxelShape[]{shape, Shapes.empty()}; + // Iterate all boxes in the shape and rotate their coordinates + shape.forAllBoxes((minX, minY, minZ, maxX, maxY, maxZ) -> { + // Convert to block-space rotation around Y: (x,z) -> (1 - z, x) + double rMinX = 1.0D - maxZ; + double rMinZ = minX; + double rMaxX = 1.0D - minZ; + double rMaxZ = maxX; + VoxelShape rotated = Block.box(rMinX * 16.0D, minY * 16.0D, rMinZ * 16.0D, rMaxX * 16.0D, maxY * 16.0D, rMaxZ * 16.0D); + buffer[1] = Shapes.or(buffer[1], rotated); + }); + return buffer[1].optimize(); + } + + // --- Sitting logic --- + + //? if >1.20.1 { + /*@Override + protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, + Player player, BlockHitResult hit) { + if (player.isShiftKeyDown()) return InteractionResult.PASS; + if (level.isClientSide) return InteractionResult.SUCCESS; + + // Reuse existing seat if present + AABB check = new AABB(pos).inflate(0.2); + for (SeatEntity seat : level.getEntitiesOfClass(SeatEntity.class, check)) { + if (seat.getPassengers().isEmpty()) { + // Snap the player to target pos before scheduling mount + Direction facing = state.getValue(FACING); + Vec3 base = Vec3.atBottomCenterOf(pos).add(0, 0.45, 0); + double forward = -0.2; + Vec3 offset = switch (facing) { + case NORTH -> new Vec3(0, 0, forward); + case SOUTH -> new Vec3(0, 0, -forward); + case WEST -> new Vec3(forward, 0, 0); + case EAST -> new Vec3(-forward, 0, 0); + default -> Vec3.ZERO; + }; + Vec3 seatPos = base.add(offset); + + // Use moveTo (sets x/y/z + yaw/pitch) so both server and tracking clients get the same transform + seat.moveTo(seatPos.x, seatPos.y, seatPos.z, facing.toYRot(), 0.0F); + level.sendBlockUpdated(pos, state, state, Block.UPDATE_NEIGHBORS | Block.UPDATE_CLIENTS); + + seat.scheduleMount(player.getUUID()); + return InteractionResult.CONSUME; + } + return InteractionResult.CONSUME; + } + + Direction facing = state.getValue(FACING); + Vec3 base = Vec3.atBottomCenterOf(pos).add(0, 0.45, 0); + double forward = -0.2; + Vec3 offset = switch (facing) { + case NORTH -> new Vec3(0, 0, forward); + case SOUTH -> new Vec3(0, 0, -forward); + case WEST -> new Vec3(forward, 0, 0); + case EAST -> new Vec3(-forward, 0, 0); + default -> Vec3.ZERO; + }; + Vec3 seatPos = base.add(offset); + + SeatEntity seat = ModEntities.SEAT.get().create(level); + if (seat == null) return InteractionResult.PASS; + + // Prefer moveTo over setPos for initial spawn pose + seat.moveTo(seatPos.x, seatPos.y, seatPos.z, facing.toYRot(), 0.0F); + level.addFreshEntity(seat); + + // Immediately reassert pose with moveTo to ensure tracking sees the same coords this tick + seat.moveTo(seatPos.x, seatPos.y, seatPos.z, facing.toYRot(), 0.0F); + + // Nudge a block update to flush to clients in edge cases + level.sendBlockUpdated(pos, state, state, Block.UPDATE_NEIGHBORS | Block.UPDATE_CLIENTS); + + // Mount next tick from the entity + seat.scheduleMount(player.getUUID()); + return InteractionResult.CONSUME; + } + *///?} else { + @Override + public InteractionResult use(BlockState state, Level level, BlockPos pos, + Player player, InteractionHand hand, BlockHitResult hit) { + if (player.isShiftKeyDown()) return InteractionResult.PASS; + if (level.isClientSide) return InteractionResult.SUCCESS; + + // Reuse existing seat if present + AABB check = new AABB(pos).inflate(0.2); + for (SeatEntity seat : level.getEntitiesOfClass(SeatEntity.class, check)) { + if (seat.getPassengers().isEmpty()) { + Direction facing = state.getValue(FACING); + Vec3 base = Vec3.atBottomCenterOf(pos).add(0, 0.45, 0); + double forward = -0.2; + Vec3 offset = switch (facing) { + case NORTH -> new Vec3(0, 0, forward); + case SOUTH -> new Vec3(0, 0, -forward); + case WEST -> new Vec3(forward, 0, 0); + case EAST -> new Vec3(-forward, 0, 0); + default -> Vec3.ZERO; + }; + Vec3 seatPos = base.add(offset); + + seat.moveTo(seatPos.x, seatPos.y, seatPos.z, facing.toYRot(), 0.0F); + level.sendBlockUpdated(pos, state, state, Block.UPDATE_NEIGHBORS | Block.UPDATE_CLIENTS); + + seat.scheduleMount(player.getUUID()); + return InteractionResult.CONSUME; + } + return InteractionResult.CONSUME; + } + + Direction facing = state.getValue(FACING); + Vec3 base = Vec3.atBottomCenterOf(pos).add(0, 0.45, 0); + double forward = -0.2; + Vec3 offset = switch (facing) { + case NORTH -> new Vec3(0, 0, forward); + case SOUTH -> new Vec3(0, 0, -forward); + case WEST -> new Vec3(forward, 0, 0); + case EAST -> new Vec3(-forward, 0, 0); + default -> Vec3.ZERO; + }; + Vec3 seatPos = base.add(offset); + + SeatEntity seat = ModEntities.SEAT.get().create(level); + if (seat == null) return InteractionResult.PASS; + + seat.moveTo(seatPos.x, seatPos.y, seatPos.z, facing.toYRot(), 0.0F); + level.addFreshEntity(seat); + seat.moveTo(seatPos.x, seatPos.y, seatPos.z, facing.toYRot(), 0.0F); + + level.sendBlockUpdated(pos, state, state, Block.UPDATE_NEIGHBORS | Block.UPDATE_CLIENTS); + + seat.scheduleMount(player.getUUID()); + return InteractionResult.CONSUME; + } + //?} + + @Override + public void onRemove(BlockState state, Level level, BlockPos pos, BlockState newState, boolean isMoving) { + if (!state.is(newState.getBlock())) { + // Clean up seat if bench is broken/replaced + for (SeatEntity seat : level.getEntitiesOfClass(SeatEntity.class, new AABB(pos).inflate(0.5))) { + seat.discard(); + } + } + super.onRemove(state, level, pos, newState, isMoving); + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/block/custom/CrateBlock.java b/common/src/main/java/net/cmr/jurassicrevived/block/custom/CrateBlock.java new file mode 100644 index 0000000..fb9c093 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/block/custom/CrateBlock.java @@ -0,0 +1,166 @@ +package net.cmr.jurassicrevived.block.custom; + +//? if >1.20.1 { +/*import com.mojang.serialization.MapCodec; +import net.minecraft.core.component.DataComponents; +import net.minecraft.world.item.component.CustomData; +import net.minecraft.world.ItemInteractionResult; +*///?} + +import dev.architectury.registry.menu.ExtendedMenuProvider; +import dev.architectury.registry.menu.MenuRegistry; +import net.cmr.jurassicrevived.block.entity.custom.CrateBlockEntity; +import net.minecraft.core.BlockPos; +import net.minecraft.core.registries.Registries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.BaseEntityBlock; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.RenderShape; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityTicker; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.BlockHitResult; +import org.jetbrains.annotations.Nullable; + +public class CrateBlock extends BaseEntityBlock { + private final int slots; + + //? if >1.20.1 { + /*@Override protected MapCodec codec() { return null; } + *///?} + + public CrateBlock(Properties properties, int slots) { + super(properties); + this.slots = slots; + } + + @Override + public RenderShape getRenderShape(BlockState state) { + return RenderShape.MODEL; + } + + @Nullable + @Override + public BlockEntity newBlockEntity(BlockPos pos, BlockState state) { + return new CrateBlockEntity(pos, state, this.slots); + } + + @Nullable + @Override + public BlockEntityTicker getTicker(Level level, BlockState state, BlockEntityType type) { + return null; + } + + @Override + //? if >1.20.1 { + /*public BlockState playerWillDestroy(Level level, BlockPos pos, BlockState state, Player player) { + *///?} else { + public void playerWillDestroy(Level level, BlockPos pos, BlockState state, Player player) { + //?} + if (!level.isClientSide) { + if (player.getAbilities().instabuild) { + level.removeBlockEntity(pos); + level.setBlock(pos, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL); + //? if >1.20.1 { + /*return state; + *///?} else { + return; + //?} + } + + BlockEntity be = level.getBlockEntity(pos); + if (be instanceof CrateBlockEntity fbe) { + ItemStack stack = new ItemStack(this.asItem()); + + if (!fbe.isEmptyForDrop()) { + //? if >1.20.1 { + /*CompoundTag tag = fbe.saveWithoutMetadata(level.registryAccess()); + var beTypeKey = level.registryAccess() + .registryOrThrow(Registries.BLOCK_ENTITY_TYPE) + .getKey(fbe.getType()); + if (beTypeKey != null) { + tag.putString("id", beTypeKey.toString()); + } + stack.set(DataComponents.BLOCK_ENTITY_DATA, CustomData.of(tag)); + *///?} else { + CompoundTag tag = fbe.saveWithoutMetadata(); + stack.getOrCreateTagElement("BlockEntityTag").merge(tag); + //?} + } + + popResource(level, pos, stack); + level.removeBlockEntity(pos); + level.setBlock(pos, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL); + //? if >1.20.1 { + /*return state; + *///?} else { + return; + //?} + } + } + super.playerWillDestroy(level, pos, state, player); + //? if >1.20.1 { + /*return state; + *///?} else { + return; + //?} + } + + @Override + public void onRemove(BlockState pState, Level pLevel, BlockPos pPos, BlockState pNewState, boolean pIsMoving) { + super.onRemove(pState, pLevel, pPos, pNewState, pIsMoving); + } + + @Override + public boolean hasAnalogOutputSignal(BlockState state) { + return true; + } + + @Override + public int getAnalogOutputSignal(BlockState state, Level level, BlockPos pos) { + BlockEntity be = level.getBlockEntity(pos); + if (be instanceof CrateBlockEntity crate) { + return crate.redstoneSignal(); + } + return 0; + } + + @Override + public boolean triggerEvent(BlockState state, Level level, BlockPos pos, int id, int param) { + super.triggerEvent(state, level, pos, id, param); + BlockEntity be = level.getBlockEntity(pos); + return be != null && be.triggerEvent(id, param); + } + + //? if >1.20.1 { + /*@Override + protected ItemInteractionResult useItemOn(ItemStack stack, BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, BlockHitResult hit) { + if (!level.isClientSide()) { + BlockEntity be = level.getBlockEntity(pos); + if (be instanceof CrateBlockEntity crate) { + MenuRegistry.openExtendedMenu((ServerPlayer) player, crate); + } + } + return ItemInteractionResult.sidedSuccess(level.isClientSide()); + } + *///?} else { + @Override + public InteractionResult use(BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, BlockHitResult hit) { + if (!level.isClientSide()) { + BlockEntity be = level.getBlockEntity(pos); + if (be instanceof CrateBlockEntity crate) { + MenuRegistry.openExtendedMenu((ServerPlayer) player, crate); + } + } + return InteractionResult.sidedSuccess(level.isClientSide()); + } + //?} +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/block/custom/DNAAnalyzerBlock.java b/common/src/main/java/net/cmr/jurassicrevived/block/custom/DNAAnalyzerBlock.java new file mode 100644 index 0000000..bfb2290 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/block/custom/DNAAnalyzerBlock.java @@ -0,0 +1,240 @@ +package net.cmr.jurassicrevived.block.custom; + +//? if >1.20.1 { +/*import com.mojang.serialization.MapCodec; +import net.minecraft.core.component.DataComponents; +import net.minecraft.world.item.component.CustomData; +import net.minecraft.world.ItemInteractionResult; +*///?} + +import dev.architectury.registry.menu.MenuRegistry; +import net.cmr.jurassicrevived.block.entity.ModBlockEntities; +import net.cmr.jurassicrevived.block.entity.custom.DNAAnalyzerBlockEntity; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.particles.ItemParticleOption; +import net.minecraft.core.particles.ParticleTypes; +import net.minecraft.core.registries.Registries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.util.RandomSource; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.context.BlockPlaceContext; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.*; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityTicker; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.StateDefinition; +import net.minecraft.world.level.block.state.properties.BlockStateProperties; +import net.minecraft.world.level.block.state.properties.BooleanProperty; +import net.minecraft.world.level.block.state.properties.DirectionProperty; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.shapes.CollisionContext; +import net.minecraft.world.phys.shapes.Shapes; +import net.minecraft.world.phys.shapes.VoxelShape; +import org.jetbrains.annotations.Nullable; + +public class DNAAnalyzerBlock extends BaseEntityBlock { + public static final DirectionProperty FACING = BlockStateProperties.HORIZONTAL_FACING; + public static final BooleanProperty LIT = BlockStateProperties.LIT; + + //? if >1.20.1 { + /*public static final MapCodec CODEC = simpleCodec(DNAAnalyzerBlock::new); + @Override protected MapCodec codec() { return CODEC; } + *///?} + + public DNAAnalyzerBlock(Properties properties) { + super(properties); + } + + private static final VoxelShape SHAPE_NORTH = Shapes.box( + 1.0 / 16.0, 0.0 / 16.0, 3.0 / 16.0, + 15.0 / 16.0, 16.0 / 16.0, 12.5 / 16.0 + ); + + private static final VoxelShape SHAPE_SOUTH = rotateShapeY(SHAPE_NORTH, 180); + private static final VoxelShape SHAPE_WEST = rotateShapeY(SHAPE_NORTH, 90); + private static final VoxelShape SHAPE_EAST = rotateShapeY(SHAPE_NORTH, 90); + + private static VoxelShape rotateShapeY(VoxelShape shape, int degrees) { + double rad = Math.toRadians(((degrees % 360) + 360) % 360); + int turns = (int) Math.round(rad / (Math.PI / 2)); + turns = ((turns % 4) + 4) % 4; + + VoxelShape current = shape; + for (int i = 0; i < turns; i++) { + VoxelShape[] buffer = new VoxelShape[]{Shapes.empty()}; + current.forAllBoxes((minX, minY, minZ, maxX, maxY, maxZ) -> { + double nMinX = 1.0 - maxZ; + double nMinZ = minX; + double nMaxX = 1.0 - minZ; + double nMaxZ = maxX; + buffer[0] = Shapes.or(buffer[0], Shapes.box(nMinX, minY, nMinZ, nMaxX, maxY, nMaxZ)); + }); + current = buffer[0]; + } + return current; + } + + @Override + public BlockState rotate(BlockState state, Rotation rotation) { + return state.setValue(FACING, rotation.rotate(state.getValue(FACING))); + } + + @Override + public BlockState mirror(BlockState state, Mirror mirror) { + return state.rotate(mirror.getRotation(state.getValue(FACING))); + } + + @Override + public @Nullable BlockState getStateForPlacement(BlockPlaceContext context) { + return this.defaultBlockState().setValue(FACING, context.getHorizontalDirection().getOpposite()).setValue(LIT, false); + } + + @Override + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + builder.add(FACING, LIT); + } + + @Override + public VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) { + Direction dir = state.getValue(FACING); + return switch (dir) { + case NORTH -> SHAPE_NORTH; + case SOUTH -> SHAPE_SOUTH; + case WEST -> SHAPE_WEST; + case EAST -> SHAPE_EAST; + default -> SHAPE_NORTH; + }; + } + + @Override + public @Nullable BlockEntity newBlockEntity(BlockPos blockPos, BlockState blockState) { + return new DNAAnalyzerBlockEntity(blockPos, blockState); + } + + @Override + public RenderShape getRenderShape(BlockState state) { + return RenderShape.MODEL; + } + + @Override + //? if >1.20.1 { + /*public BlockState playerWillDestroy(Level level, BlockPos pos, BlockState state, Player player) { + *///?} else { + public void playerWillDestroy(Level level, BlockPos pos, BlockState state, Player player) { + //?} + if (!level.isClientSide) { + if (player.getAbilities().instabuild) { + level.removeBlockEntity(pos); + level.setBlock(pos, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL); + //? if >1.20.1 { + /*return state; + *///?} else { + return; + //?} + } + + BlockEntity be = level.getBlockEntity(pos); + if (be instanceof DNAAnalyzerBlockEntity fbe) { + ItemStack stack = new ItemStack(this.asItem()); + + if (!fbe.isEmptyForDrop()) { + //? if >1.20.1 { + /*CompoundTag tag = fbe.saveWithoutMetadata(level.registryAccess()); + var beTypeKey = level.registryAccess().registryOrThrow(Registries.BLOCK_ENTITY_TYPE).getKey(fbe.getType()); + if (beTypeKey != null) tag.putString("id", beTypeKey.toString()); + stack.set(DataComponents.BLOCK_ENTITY_DATA, CustomData.of(tag)); + *///?} else { + CompoundTag tag = fbe.saveWithoutMetadata(); + stack.getOrCreateTagElement("BlockEntityTag").merge(tag); + //?} + } + + popResource(level, pos, stack); + level.removeBlockEntity(pos); + level.setBlock(pos, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL); + //? if >1.20.1 { + /*return state; + *///?} else { + return; + //?} + } + } + super.playerWillDestroy(level, pos, state, player); + //? if >1.20.1 { + /*return state; + *///?} else { + return; + //?} + } + + //? if >1.20.1 { + /*@Override + protected ItemInteractionResult useItemOn(ItemStack pStack, BlockState pState, Level pLevel, BlockPos pPos, Player pPlayer, InteractionHand pHand, BlockHitResult pHitResult) { + if (!pLevel.isClientSide()) { + BlockEntity entity = pLevel.getBlockEntity(pPos); + if(entity instanceof DNAAnalyzerBlockEntity dnaAnalyzerBlockEntity) { + MenuRegistry.openExtendedMenu((ServerPlayer) pPlayer, dnaAnalyzerBlockEntity); + } + } + return ItemInteractionResult.sidedSuccess(pLevel.isClientSide()); + } + *///?} else { + @Override + public InteractionResult use(BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, BlockHitResult hit) { + if (!level.isClientSide()) { + BlockEntity entity = level.getBlockEntity(pos); + if (entity instanceof DNAAnalyzerBlockEntity dnaAnalyzerBlockEntity) { + MenuRegistry.openExtendedMenu((ServerPlayer) player, dnaAnalyzerBlockEntity); + } + } + return InteractionResult.sidedSuccess(level.isClientSide()); + } + //?} + + @Override + public @Nullable BlockEntityTicker getTicker(Level level, BlockState state, BlockEntityType blockEntityType) { + if (blockEntityType != ModBlockEntities.DNA_ANALYZER_BE.get()) return null; + + if (level.isClientSide) { + return null; + } else { + return createTickerHelper(blockEntityType, ModBlockEntities.DNA_ANALYZER_BE.get(), + (level1, blockPos, blockState, dnaAnalyzerBlockEntity) -> dnaAnalyzerBlockEntity.tick(level1, blockPos, blockState)); + } + } + + @Override + public void animateTick(BlockState state, Level level, BlockPos pos, RandomSource random) { + if (!state.getValue(LIT)) { + return; + } + + double xPos = (double)pos.getX() + 0.5; + double yPos = pos.getY(); + double zPos = (double)pos.getZ() + 0.5; + + Direction direction = state.getValue(FACING).getOpposite(); + Direction.Axis axis = direction.getAxis(); + + double defaultOffset = random.nextDouble() * 0.6 - 0.3; + double xOffsets = axis == Direction.Axis.X ? (double)direction.getStepX() * 0.52 : defaultOffset; + double yOffset = random.nextDouble() * 6.0 / 8.0; + double zOffset = axis == Direction.Axis.Z ? (double)direction.getStepZ() * 0.52 : defaultOffset; + + level.addParticle(ParticleTypes.SMOKE, xPos + xOffsets, yPos + yOffset, zPos + zOffset, 0.0, 0.0, 0.0); + + BlockEntity be = level.getBlockEntity(pos); + if(be instanceof DNAAnalyzerBlockEntity dnaAnalyzerBlockEntity && !dnaAnalyzerBlockEntity.itemHandler.getItem(1).isEmpty()) { + level.addParticle(new ItemParticleOption(ParticleTypes.ITEM, dnaAnalyzerBlockEntity.itemHandler.getItem(1)), + xPos + xOffsets, yPos + yOffset, zPos + zOffset, 0.0, 0.0, 0.0); + } + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/block/custom/DNAExtractorBlock.java b/common/src/main/java/net/cmr/jurassicrevived/block/custom/DNAExtractorBlock.java new file mode 100644 index 0000000..02a9fcd --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/block/custom/DNAExtractorBlock.java @@ -0,0 +1,245 @@ +package net.cmr.jurassicrevived.block.custom; + +//? if >1.20.1 { +/*import com.mojang.serialization.MapCodec; +import net.minecraft.core.component.DataComponents; +import net.minecraft.world.item.component.CustomData; +import net.minecraft.world.ItemInteractionResult; +*///?} + +import dev.architectury.registry.menu.MenuRegistry; +import net.cmr.jurassicrevived.block.entity.ModBlockEntities; +import net.cmr.jurassicrevived.block.entity.custom.DNAExtractorBlockEntity; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.particles.ItemParticleOption; +import net.minecraft.core.particles.ParticleTypes; +import net.minecraft.core.registries.Registries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.util.RandomSource; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.context.BlockPlaceContext; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.*; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityTicker; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.StateDefinition; +import net.minecraft.world.level.block.state.properties.BlockStateProperties; +import net.minecraft.world.level.block.state.properties.BooleanProperty; +import net.minecraft.world.level.block.state.properties.DirectionProperty; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.shapes.CollisionContext; +import net.minecraft.world.phys.shapes.Shapes; +import net.minecraft.world.phys.shapes.VoxelShape; +import org.jetbrains.annotations.Nullable; + +public class DNAExtractorBlock extends BaseEntityBlock { + public static final DirectionProperty FACING = BlockStateProperties.HORIZONTAL_FACING; + public static final BooleanProperty LIT = BlockStateProperties.LIT; + + //? if >1.20.1 { + /*public static final MapCodec CODEC = simpleCodec(DNAExtractorBlock::new); + @Override protected MapCodec codec() { return CODEC; } + *///?} + + public DNAExtractorBlock(Properties properties) { + super(properties); + } + + private static final VoxelShape SHAPE_NORTH = Shapes.box( + 0.0 / 16.0, 0.0 / 16.0, 0.5 / 16.0, + 16.0 / 16.0, 17.0 / 16.0, 15.5 / 16.0 + ); + + private static final VoxelShape SHAPE_SOUTH = rotateShapeY(SHAPE_NORTH, 180); + private static final VoxelShape SHAPE_WEST = rotateShapeY(SHAPE_NORTH, 90); + private static final VoxelShape SHAPE_EAST = rotateShapeY(SHAPE_NORTH, 90); + + private static VoxelShape rotateShapeY(VoxelShape shape, int degrees) { + double rad = Math.toRadians(((degrees % 360) + 360) % 360); + int turns = (int) Math.round(rad / (Math.PI / 2)); + turns = ((turns % 4) + 4) % 4; + + VoxelShape current = shape; + for (int i = 0; i < turns; i++) { + VoxelShape[] buffer = new VoxelShape[]{Shapes.empty()}; + current.forAllBoxes((minX, minY, minZ, maxX, maxY, maxZ) -> { + double nMinX = 1.0 - maxZ; + double nMinZ = minX; + double nMaxX = 1.0 - minZ; + double nMaxZ = maxX; + buffer[0] = Shapes.or(buffer[0], Shapes.box(nMinX, minY, nMinZ, nMaxX, maxY, nMaxZ)); + }); + current = buffer[0]; + } + return current; + } + + @Override + public BlockState rotate(BlockState state, Rotation rotation) { + return state.setValue(FACING, rotation.rotate(state.getValue(FACING))); + } + + @Override + public BlockState mirror(BlockState state, Mirror mirror) { + return state.rotate(mirror.getRotation(state.getValue(FACING))); + } + + @Override + public @Nullable BlockState getStateForPlacement(BlockPlaceContext context) { + return this.defaultBlockState().setValue(FACING, context.getHorizontalDirection().getOpposite().getOpposite()).setValue(LIT, false); + } + + @Override + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + builder.add(FACING, LIT); + } + + @Override + public VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) { + Direction dir = state.getValue(FACING); + return switch (dir) { + case NORTH -> SHAPE_NORTH; + case SOUTH -> SHAPE_SOUTH; + case WEST -> SHAPE_WEST; + case EAST -> SHAPE_EAST; + default -> SHAPE_NORTH; + }; + } + + @Override + public @Nullable BlockEntity newBlockEntity(BlockPos blockPos, BlockState blockState) { + return new DNAExtractorBlockEntity(blockPos, blockState); + } + + @Override + public RenderShape getRenderShape(BlockState state) { + return RenderShape.MODEL; + } + + @Override + //? if >1.20.1 { + /*public BlockState playerWillDestroy(Level level, BlockPos pos, BlockState state, Player player) { + *///?} else { + public void playerWillDestroy(Level level, BlockPos pos, BlockState state, Player player) { + //?} + if (!level.isClientSide) { + if (player.getAbilities().instabuild) { + level.removeBlockEntity(pos); + level.setBlock(pos, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL); + //? if >1.20.1 { + /*return state; + *///?} else { + return; + //?} + } + + BlockEntity be = level.getBlockEntity(pos); + if (be instanceof DNAExtractorBlockEntity fbe) { + ItemStack stack = new ItemStack(this.asItem()); + + if (!fbe.isEmptyForDrop()) { + //? if >1.20.1 { + /*CompoundTag tag = fbe.saveWithoutMetadata(level.registryAccess()); + var beTypeKey = level.registryAccess().registryOrThrow(Registries.BLOCK_ENTITY_TYPE).getKey(fbe.getType()); + if (beTypeKey != null) tag.putString("id", beTypeKey.toString()); + stack.set(DataComponents.BLOCK_ENTITY_DATA, CustomData.of(tag)); + *///?} else { + CompoundTag tag = fbe.saveWithoutMetadata(); + stack.getOrCreateTagElement("BlockEntityTag").merge(tag); + //?} + } + + popResource(level, pos, stack); + level.removeBlockEntity(pos); + level.setBlock(pos, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL); + //? if >1.20.1 { + /*return state; + *///?} else { + return; + //?} + } + } + super.playerWillDestroy(level, pos, state, player); + //? if >1.20.1 { + /*return state; + *///?} else { + return; + //?} + } + + @Override + public void onRemove(BlockState pState, Level pLevel, BlockPos pPos, BlockState pNewState, boolean pIsMoving) { + super.onRemove(pState, pLevel, pPos, pNewState, pIsMoving); + } + + //? if >1.20.1 { + /*@Override + protected ItemInteractionResult useItemOn(ItemStack pStack, BlockState pState, Level pLevel, BlockPos pPos, Player pPlayer, InteractionHand pHand, BlockHitResult pHitResult) { + if (!pLevel.isClientSide()) { + BlockEntity entity = pLevel.getBlockEntity(pPos); + if(entity instanceof DNAExtractorBlockEntity dnaExtractorBlockEntity) { + MenuRegistry.openExtendedMenu((ServerPlayer) pPlayer, dnaExtractorBlockEntity); + } + } + return ItemInteractionResult.sidedSuccess(pLevel.isClientSide()); + } + *///?} else { + @Override + public InteractionResult use(BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, BlockHitResult hit) { + if (!level.isClientSide()) { + BlockEntity entity = level.getBlockEntity(pos); + if (entity instanceof DNAExtractorBlockEntity dnaExtractorBlockEntity) { + MenuRegistry.openExtendedMenu((ServerPlayer) player, dnaExtractorBlockEntity); + } + } + return InteractionResult.sidedSuccess(level.isClientSide()); + } + //?} + + @Override + public @Nullable BlockEntityTicker getTicker(Level level, BlockState state, BlockEntityType blockEntityType) { + if (blockEntityType != ModBlockEntities.DNA_EXTRACTOR_BE.get()) return null; + + if (level.isClientSide) { + return null; + } else { + return createTickerHelper(blockEntityType, ModBlockEntities.DNA_EXTRACTOR_BE.get(), + (level1, blockPos, blockState, dnaExtractorBlockEntity) -> dnaExtractorBlockEntity.tick(level1, blockPos, blockState)); + } + } + + @Override + public void animateTick(BlockState state, Level level, BlockPos pos, RandomSource random) { + if (!state.getValue(LIT)) { + return; + } + + double xPos = (double)pos.getX() + 0.5; + double yPos = pos.getY(); + double zPos = (double)pos.getZ() + 0.5; + + Direction direction = state.getValue(FACING).getOpposite(); + Direction.Axis axis = direction.getAxis(); + + double defaultOffset = random.nextDouble() * 0.6 - 0.3; + double xOffsets = axis == Direction.Axis.X ? (double)direction.getStepX() * 0.52 : defaultOffset; + double yOffset = random.nextDouble() * 6.0 / 8.0; + double zOffset = axis == Direction.Axis.Z ? (double)direction.getStepZ() * 0.52 : defaultOffset; + + level.addParticle(ParticleTypes.SMOKE, xPos + xOffsets, yPos + yOffset, zPos + zOffset, 0.0, 0.0, 0.0); + + BlockEntity be = level.getBlockEntity(pos); + if(be instanceof DNAExtractorBlockEntity dnaExtractorBlockEntity && !dnaExtractorBlockEntity.itemHandler.getItem(1).isEmpty()) { + level.addParticle(new ItemParticleOption(ParticleTypes.ITEM, dnaExtractorBlockEntity.itemHandler.getItem(1)), + xPos + xOffsets, yPos + yOffset, zPos + zOffset, 0.0, 0.0, 0.0); + } + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/block/custom/DNAHybridizerBlock.java b/common/src/main/java/net/cmr/jurassicrevived/block/custom/DNAHybridizerBlock.java new file mode 100644 index 0000000..4927509 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/block/custom/DNAHybridizerBlock.java @@ -0,0 +1,239 @@ +package net.cmr.jurassicrevived.block.custom; + +//? if >1.20.1 { +/*import com.mojang.serialization.MapCodec; +import net.minecraft.core.component.DataComponents; +import net.minecraft.core.registries.Registries; +import net.minecraft.world.item.component.CustomData; +import net.minecraft.world.ItemInteractionResult; +*///?} + +import dev.architectury.registry.menu.MenuRegistry; +import net.cmr.jurassicrevived.block.entity.ModBlockEntities; +import net.cmr.jurassicrevived.block.entity.custom.DNAHybridizerBlockEntity; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.particles.ItemParticleOption; +import net.minecraft.core.particles.ParticleTypes; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.util.RandomSource; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult;import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.context.BlockPlaceContext; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.*; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityTicker; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.StateDefinition; +import net.minecraft.world.level.block.state.properties.BlockStateProperties; +import net.minecraft.world.level.block.state.properties.BooleanProperty; +import net.minecraft.world.level.block.state.properties.DirectionProperty; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.shapes.CollisionContext; +import net.minecraft.world.phys.shapes.Shapes; +import net.minecraft.world.phys.shapes.VoxelShape; +import org.jetbrains.annotations.Nullable; + +public class DNAHybridizerBlock extends BaseEntityBlock { + public static final DirectionProperty FACING = BlockStateProperties.HORIZONTAL_FACING; + public static final BooleanProperty LIT = BlockStateProperties.LIT; + + //? if >1.20.1 { + /*public static final MapCodec CODEC = simpleCodec(DNAHybridizerBlock::new); + @Override protected MapCodec codec() { return CODEC; } + *///?} + + public DNAHybridizerBlock(Properties properties) { + super(properties); + } + + private static final VoxelShape SHAPE_NORTH = Shapes.box( + 0.0 / 16.0, 0.0 / 16.0, 0.0 / 16.0, + 16.0 / 16.0, 16.0 / 16.0, 13.5 / 16.0 + ); + + private static final VoxelShape SHAPE_SOUTH = rotateShapeY(SHAPE_NORTH, 180); + private static final VoxelShape SHAPE_WEST = rotateShapeY(SHAPE_NORTH, 90); + private static final VoxelShape SHAPE_EAST = rotateShapeY(SHAPE_NORTH, 90); + + private static VoxelShape rotateShapeY(VoxelShape shape, int degrees) { + double rad = Math.toRadians(((degrees % 360) + 360) % 360); + int turns = (int) Math.round(rad / (Math.PI / 2)); + turns = ((turns % 4) + 4) % 4; + + VoxelShape current = shape; + for (int i = 0; i < turns; i++) { + VoxelShape[] buffer = new VoxelShape[]{Shapes.empty()}; + current.forAllBoxes((minX, minY, minZ, maxX, maxY, maxZ) -> { + double nMinX = 1.0 - maxZ; + double nMinZ = minX; + double nMaxX = 1.0 - minZ; + double nMaxZ = maxX; + buffer[0] = Shapes.or(buffer[0], Shapes.box(nMinX, minY, nMinZ, nMaxX, maxY, nMaxZ)); + }); + current = buffer[0]; + } + return current; + } + + @Override + public BlockState rotate(BlockState state, Rotation rotation) { + return state.setValue(FACING, rotation.rotate(state.getValue(FACING))); + } + + @Override + public BlockState mirror(BlockState state, Mirror mirror) { + return state.rotate(mirror.getRotation(state.getValue(FACING))); + } + + @Override + public @Nullable BlockState getStateForPlacement(BlockPlaceContext context) { + return this.defaultBlockState().setValue(FACING, context.getHorizontalDirection().getOpposite().getOpposite()).setValue(LIT, false); + } + + @Override + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + builder.add(FACING, LIT); + } + + @Override + public VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) { + Direction dir = state.getValue(FACING); + return switch (dir) { + case NORTH -> SHAPE_NORTH; + case SOUTH -> SHAPE_SOUTH; + case WEST -> SHAPE_WEST; + case EAST -> SHAPE_EAST; + default -> SHAPE_NORTH; + }; + } + + @Override + public @Nullable BlockEntity newBlockEntity(BlockPos blockPos, BlockState blockState) { + return new DNAHybridizerBlockEntity(blockPos, blockState); + } + + @Override + public RenderShape getRenderShape(BlockState state) { + return RenderShape.MODEL; + } + + @Override + //? if >1.20.1 { + /*public BlockState playerWillDestroy(Level level, BlockPos pos, BlockState state, Player player) { + *///?} else { + public void playerWillDestroy(Level level, BlockPos pos, BlockState state, Player player) { + //?} + if (!level.isClientSide) { + if (player.getAbilities().instabuild) { + level.removeBlockEntity(pos); + level.setBlock(pos, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL); + //? if >1.20.1 { + /*return state; + *///?} else { + return; + //?} + } + + BlockEntity be = level.getBlockEntity(pos); + if (be instanceof DNAHybridizerBlockEntity fbe) { + ItemStack stack = new ItemStack(this.asItem()); + + if (!fbe.isEmptyForDrop()) { + //? if >1.20.1 { + /*CompoundTag tag = fbe.saveWithoutMetadata(level.registryAccess()); + var beTypeKey = level.registryAccess().registryOrThrow(Registries.BLOCK_ENTITY_TYPE).getKey(fbe.getType()); + if (beTypeKey != null) tag.putString("id", beTypeKey.toString()); + stack.set(DataComponents.BLOCK_ENTITY_DATA, CustomData.of(tag)); + *///?} else { + CompoundTag tag = fbe.saveWithoutMetadata(); + stack.getOrCreateTagElement("BlockEntityTag").merge(tag); + //?} + } + + popResource(level, pos, stack); + level.removeBlockEntity(pos); + level.setBlock(pos, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL); + //? if >1.20.1 { + /*return state; + *///?} else { + return; + //?} + } + } + super.playerWillDestroy(level, pos, state, player); + //? if >1.20.1 { + /*return state; + *///?} else { + return; + //?} + } + + //? if >1.20.1 { + /*@Override + protected ItemInteractionResult useItemOn(ItemStack pStack, BlockState pState, Level pLevel, BlockPos pPos, Player pPlayer, InteractionHand pHand, BlockHitResult pHitResult) { + if (!pLevel.isClientSide()) { + BlockEntity entity = pLevel.getBlockEntity(pPos); + if(entity instanceof DNAHybridizerBlockEntity dnaHybridizerBlockEntity) { + MenuRegistry.openExtendedMenu((ServerPlayer) pPlayer, dnaHybridizerBlockEntity); + } + } + return ItemInteractionResult.sidedSuccess(pLevel.isClientSide()); + } + *///?} else { + @Override + public InteractionResult use(BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, BlockHitResult hit) { + if (!level.isClientSide()) { + BlockEntity entity = level.getBlockEntity(pos); + if (entity instanceof DNAHybridizerBlockEntity dnaHybridizerBlockEntity) { + MenuRegistry.openExtendedMenu((ServerPlayer) player, dnaHybridizerBlockEntity); + } + } + return InteractionResult.sidedSuccess(level.isClientSide()); + } + //?} + + @Override + public @Nullable BlockEntityTicker getTicker(Level level, BlockState state, BlockEntityType blockEntityType) { + if (blockEntityType != ModBlockEntities.DNA_HYBRIDIZER_BE.get()) return null; + + if (level.isClientSide) { + return null; + } else { + return createTickerHelper(blockEntityType, ModBlockEntities.DNA_HYBRIDIZER_BE.get(), + (level1, blockPos, blockState, dnaHybridizerBlockEntity) -> dnaHybridizerBlockEntity.tick(level1, blockPos, blockState)); + } + } + + @Override + public void animateTick(BlockState state, Level level, BlockPos pos, RandomSource random) { + if (!state.getValue(LIT)) { + return; + } + + double xPos = (double)pos.getX() + 0.5; + double yPos = pos.getY(); + double zPos = (double)pos.getZ() + 0.5; + + Direction direction = state.getValue(FACING).getOpposite(); + Direction.Axis axis = direction.getAxis(); + + double defaultOffset = random.nextDouble() * 0.6 - 0.3; + double xOffsets = axis == Direction.Axis.X ? (double)direction.getStepX() * 0.52 : defaultOffset; + double yOffset = random.nextDouble() * 6.0 / 8.0; + double zOffset = axis == Direction.Axis.Z ? (double)direction.getStepZ() * 0.52 : defaultOffset; + + level.addParticle(ParticleTypes.SMOKE, xPos + xOffsets, yPos + yOffset, zPos + zOffset, 0.0, 0.0, 0.0); + + BlockEntity be = level.getBlockEntity(pos); + if(be instanceof DNAHybridizerBlockEntity dnaHybridizerBlockEntity && !dnaHybridizerBlockEntity.itemHandler.getItem(1).isEmpty()) { + level.addParticle(new ItemParticleOption(ParticleTypes.ITEM, dnaHybridizerBlockEntity.itemHandler.getItem(1)), + xPos + xOffsets, yPos + yOffset, zPos + zOffset, 0.0, 0.0, 0.0); + } + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/block/custom/DecoBlock.java b/common/src/main/java/net/cmr/jurassicrevived/block/custom/DecoBlock.java new file mode 100644 index 0000000..629eac4 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/block/custom/DecoBlock.java @@ -0,0 +1,55 @@ +package net.cmr.jurassicrevived.block.custom; + +import com.mojang.serialization.MapCodec; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.item.context.BlockPlaceContext; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.HorizontalDirectionalBlock; +import net.minecraft.world.level.block.Mirror; +import net.minecraft.world.level.block.Rotation; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.StateDefinition; +import net.minecraft.world.level.block.state.properties.Property; +import net.minecraft.world.phys.shapes.CollisionContext; +import net.minecraft.world.phys.shapes.VoxelShape; + +public class DecoBlock extends HorizontalDirectionalBlock { + + public static final VoxelShape SHAPE = Block.box(4.0D, 0.0D, 4.0D, 12.0D, 5.0D, 12.0D); + + public DecoBlock(Properties properties) { + super(properties); + this.registerDefaultState((BlockState)((BlockState)((BlockState)this.stateDefinition.any()).setValue(FACING, Direction.SOUTH))); + } + + protected MapCodec codec() { + return null; + } + + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + builder.add(new Property[]{FACING}); + } + + public BlockState mirror(BlockState state, Mirror mirror) { + return state.rotate(mirror.getRotation((Direction)state.getValue(FACING))); + } + + // Ensure rotation of FACING property when the block is rotated (e.g., structure placement, commands, etc.) + @Override + public BlockState rotate(BlockState state, Rotation rotation) { + return state.setValue(FACING, rotation.rotate(state.getValue(FACING))); + } + + public BlockState getStateForPlacement(BlockPlaceContext context) { + // Ensure we always use a horizontal facing (N/E/S/W), even when looking up or down. + return (BlockState)this.defaultBlockState().setValue(FACING, context.getHorizontalDirection().getOpposite()); + } + + @Override + public VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) { + Direction facing = state.getValue(FACING); + return SHAPE; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/block/custom/EggBlock.java b/common/src/main/java/net/cmr/jurassicrevived/block/custom/EggBlock.java new file mode 100644 index 0000000..0cb4d08 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/block/custom/EggBlock.java @@ -0,0 +1,127 @@ +package net.cmr.jurassicrevived.block.custom; + +import net.cmr.jurassicrevived.block.entity.custom.EggBlockEntity; +import net.minecraft.core.BlockPos; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.util.RandomSource; +import net.minecraft.world.entity.AgeableMob; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.Mob; +import net.minecraft.world.entity.MobSpawnType; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.TooltipFlag; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.EntityBlock; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.Vec3; +import net.minecraft.world.phys.shapes.CollisionContext; +import net.minecraft.world.phys.shapes.VoxelShape; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.function.Supplier; + +public class EggBlock extends Block implements EntityBlock { + + private final Supplier> toSpawn; + private final int hatchSeconds = 1200; + + public EggBlock(Properties pProperties, Supplier> toSpawn) { + super(pProperties); + this.toSpawn = toSpawn; + } + + private static final VoxelShape EGG_SHAPE = Block.box(6.5D, 0.0D, 6.5D, 9.5D, 4.0D, 9.5D); + + @Override + public VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) { + return EGG_SHAPE; + } + + @Override + public VoxelShape getCollisionShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) { + return EGG_SHAPE; + } + + @Override + public void onPlace(BlockState state, Level level, BlockPos pos, BlockState oldState, boolean isMoving) { + super.onPlace(state, level, pos, oldState, isMoving); + if (!level.isClientSide) { + BlockEntity be = level.getBlockEntity(pos); + if (be instanceof EggBlockEntity eggBE) { + eggBE.resetForNewPlacement(level, hatchSeconds); + } + level.scheduleTick(pos, this, hatchSeconds * 20); + } + } + + @Override + public void tick(BlockState state, ServerLevel level, BlockPos pos, RandomSource random) { + BlockEntity be = level.getBlockEntity(pos); + if (!(be instanceof EggBlockEntity eggBE)) return; + if (eggBE.getSecondsRemaining(level) <= 0) { + super.tick(state, level, pos, random); + } else { + level.scheduleTick(pos, this, eggBE.getSecondsRemaining(level) * 20); + return; + } + EntityType type = toSpawn.get(); + if (type != null) { + Mob mob = type.create(level); + if (mob != null) { + Vec3 spawn = Vec3.atCenterOf(pos); + mob.moveTo(spawn.x, spawn.y + 0.1, spawn.z, level.random.nextFloat() * 360F, 0.0F); + + //? if >1.20.1 { + /*mob.finalizeSpawn(level, level.getCurrentDifficultyAt(pos), MobSpawnType.TRIGGERED, null); + *///?} else { + mob.finalizeSpawn(level, level.getCurrentDifficultyAt(pos), MobSpawnType.TRIGGERED, null, null); + //?} + + if (mob instanceof AgeableMob ageable) { + ageable.setBaby(true); + } + + level.addFreshEntity(mob); + } + } + level.levelEvent(2001, pos, Block.getId(state)); + level.removeBlock(pos, false); + } + + @Override + public void onRemove(BlockState state, Level level, BlockPos pos, BlockState newState, boolean isMoving) { + if (!level.isClientSide && state.getBlock() != newState.getBlock()) { + BlockEntity be = level.getBlockEntity(pos); + if (be instanceof EggBlockEntity eggBE) { + eggBE.invalidateTimer(); + } + } + super.onRemove(state, level, pos, newState, isMoving); + } + + @Nullable + @Override + public BlockEntity newBlockEntity(BlockPos pos, BlockState state) { + return new EggBlockEntity(pos, state); + } + + //? if >1.20.1 { + /*@Override + public void appendHoverText(ItemStack pStack, Item.TooltipContext pContext, List pTootipComponents, TooltipFlag pTooltipFlag) { + pTootipComponents.add(Component.translatable("tooltip.jurassicrevived.egg.hatches_in", hatchSeconds)); + super.appendHoverText(pStack, pContext, pTootipComponents, pTooltipFlag); + } + *///?} else { + @Override + public void appendHoverText(ItemStack pStack, @Nullable BlockGetter level, List pTootipComponents, TooltipFlag pTooltipFlag) { + pTootipComponents.add(Component.translatable("tooltip.jurassicrevived.egg.hatches_in", hatchSeconds)); + super.appendHoverText(pStack, level, pTootipComponents, pTooltipFlag); + } + //?} +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/block/custom/EmbryoCalcificationMachineBlock.java b/common/src/main/java/net/cmr/jurassicrevived/block/custom/EmbryoCalcificationMachineBlock.java new file mode 100644 index 0000000..5e25a29 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/block/custom/EmbryoCalcificationMachineBlock.java @@ -0,0 +1,240 @@ +package net.cmr.jurassicrevived.block.custom; + +//? if >1.20.1 { +/*import com.mojang.serialization.MapCodec; +import net.minecraft.core.component.DataComponents; +import net.minecraft.world.item.component.CustomData; +import net.minecraft.world.ItemInteractionResult; +*///?} + +import dev.architectury.registry.menu.MenuRegistry; +import net.cmr.jurassicrevived.block.entity.ModBlockEntities; +import net.cmr.jurassicrevived.block.entity.custom.EmbryoCalcificationMachineBlockEntity; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.particles.ItemParticleOption; +import net.minecraft.core.particles.ParticleTypes; +import net.minecraft.core.registries.Registries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.util.RandomSource; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.context.BlockPlaceContext; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.*; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityTicker; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.StateDefinition; +import net.minecraft.world.level.block.state.properties.BlockStateProperties; +import net.minecraft.world.level.block.state.properties.BooleanProperty; +import net.minecraft.world.level.block.state.properties.DirectionProperty; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.shapes.CollisionContext; +import net.minecraft.world.phys.shapes.Shapes; +import net.minecraft.world.phys.shapes.VoxelShape; +import org.jetbrains.annotations.Nullable; + +public class EmbryoCalcificationMachineBlock extends BaseEntityBlock { + public static final DirectionProperty FACING = BlockStateProperties.HORIZONTAL_FACING; + public static final BooleanProperty LIT = BlockStateProperties.LIT; + + //? if >1.20.1 { + /*public static final MapCodec CODEC = simpleCodec(EmbryoCalcificationMachineBlock::new); + @Override protected MapCodec codec() { return CODEC; } + *///?} + + public EmbryoCalcificationMachineBlock(Properties properties) { + super(properties); + } + + private static final VoxelShape SHAPE_NORTH = Shapes.box( + 1.0 / 16.0, 0.0 / 16.0, 4.0 / 16.0, + 15.0 / 16.0, 17.0 / 16.0, 14.0 / 16.0 + ); + + private static final VoxelShape SHAPE_SOUTH = rotateShapeY(SHAPE_NORTH, 180); + private static final VoxelShape SHAPE_WEST = rotateShapeY(SHAPE_NORTH, 90); + private static final VoxelShape SHAPE_EAST = rotateShapeY(SHAPE_NORTH, 90); + + private static VoxelShape rotateShapeY(VoxelShape shape, int degrees) { + double rad = Math.toRadians(((degrees % 360) + 360) % 360); + int turns = (int) Math.round(rad / (Math.PI / 2)); + turns = ((turns % 4) + 4) % 4; + + VoxelShape current = shape; + for (int i = 0; i < turns; i++) { + VoxelShape[] buffer = new VoxelShape[]{Shapes.empty()}; + current.forAllBoxes((minX, minY, minZ, maxX, maxY, maxZ) -> { + double nMinX = 1.0 - maxZ; + double nMinZ = minX; + double nMaxX = 1.0 - minZ; + double nMaxZ = maxX; + buffer[0] = Shapes.or(buffer[0], Shapes.box(nMinX, minY, nMinZ, nMaxX, maxY, nMaxZ)); + }); + current = buffer[0]; + } + return current; + } + + @Override + public BlockState rotate(BlockState state, Rotation rotation) { + return state.setValue(FACING, rotation.rotate(state.getValue(FACING))); + } + + @Override + public BlockState mirror(BlockState state, Mirror mirror) { + return state.rotate(mirror.getRotation(state.getValue(FACING))); + } + + @Override + public @Nullable BlockState getStateForPlacement(BlockPlaceContext context) { + return this.defaultBlockState().setValue(FACING, context.getHorizontalDirection().getOpposite().getOpposite()).setValue(LIT, false); + } + + @Override + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + builder.add(FACING, LIT); + } + + @Override + public VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) { + Direction dir = state.getValue(FACING); + return switch (dir) { + case NORTH -> SHAPE_NORTH; + case SOUTH -> SHAPE_SOUTH; + case WEST -> SHAPE_WEST; + case EAST -> SHAPE_EAST; + default -> SHAPE_NORTH; + }; + } + + @Override + public @Nullable BlockEntity newBlockEntity(BlockPos blockPos, BlockState blockState) { + return new EmbryoCalcificationMachineBlockEntity(blockPos, blockState); + } + + @Override + public RenderShape getRenderShape(BlockState state) { + return RenderShape.MODEL; + } + + @Override + //? if >1.20.1 { + /*public BlockState playerWillDestroy(Level level, BlockPos pos, BlockState state, Player player) { + *///?} else { + public void playerWillDestroy(Level level, BlockPos pos, BlockState state, Player player) { + //?} + if (!level.isClientSide) { + if (player.getAbilities().instabuild) { + level.removeBlockEntity(pos); + level.setBlock(pos, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL); + //? if >1.20.1 { + /*return state; + *///?} else { + return; + //?} + } + + BlockEntity be = level.getBlockEntity(pos); + if (be instanceof EmbryoCalcificationMachineBlockEntity fbe) { + ItemStack stack = new ItemStack(this.asItem()); + + if (!fbe.isEmptyForDrop()) { + //? if >1.20.1 { + /*CompoundTag tag = fbe.saveWithoutMetadata(level.registryAccess()); + var beTypeKey = level.registryAccess().registryOrThrow(Registries.BLOCK_ENTITY_TYPE).getKey(fbe.getType()); + if (beTypeKey != null) tag.putString("id", beTypeKey.toString()); + stack.set(DataComponents.BLOCK_ENTITY_DATA, CustomData.of(tag)); + *///?} else { + CompoundTag tag = fbe.saveWithoutMetadata(); + stack.getOrCreateTagElement("BlockEntityTag").merge(tag); + //?} + } + + popResource(level, pos, stack); + level.removeBlockEntity(pos); + level.setBlock(pos, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL); + //? if >1.20.1 { + /*return state; + *///?} else { + return; + //?} + } + } + super.playerWillDestroy(level, pos, state, player); + //? if >1.20.1 { + /*return state; + *///?} else { + return; + //?} + } + + //? if >1.20.1 { + /*@Override + protected ItemInteractionResult useItemOn(ItemStack pStack, BlockState pState, Level pLevel, BlockPos pPos, Player pPlayer, InteractionHand pHand, BlockHitResult pHitResult) { + if (!pLevel.isClientSide()) { + BlockEntity entity = pLevel.getBlockEntity(pPos); + if(entity instanceof EmbryoCalcificationMachineBlockEntity embryoCalcificationMachineBlockEntity) { + MenuRegistry.openExtendedMenu((ServerPlayer) pPlayer, embryoCalcificationMachineBlockEntity); + } + } + return ItemInteractionResult.sidedSuccess(pLevel.isClientSide()); + } + *///?} else { + @Override + public InteractionResult use(BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, BlockHitResult hit) { + if (!level.isClientSide()) { + BlockEntity entity = level.getBlockEntity(pos); + if (entity instanceof EmbryoCalcificationMachineBlockEntity embryoCalcificationMachineBlockEntity) { + MenuRegistry.openExtendedMenu((ServerPlayer) player, embryoCalcificationMachineBlockEntity); + } + } + return InteractionResult.sidedSuccess(level.isClientSide()); + } + //?} + + @Override + public @Nullable BlockEntityTicker getTicker(Level level, BlockState state, BlockEntityType blockEntityType) { + if (blockEntityType != ModBlockEntities.EMBRYO_CALCIFICATION_MACHINE_BE.get()) return null; + + if (level.isClientSide) { + return null; + } else { + return createTickerHelper(blockEntityType, ModBlockEntities.EMBRYO_CALCIFICATION_MACHINE_BE.get(), + (level1, blockPos, blockState, embryoCalcificationMachineBlockEntity) -> embryoCalcificationMachineBlockEntity.tick(level1, blockPos, blockState)); + } + } + + @Override + public void animateTick(BlockState state, Level level, BlockPos pos, RandomSource random) { + if (!state.getValue(LIT)) { + return; + } + + double xPos = (double)pos.getX() + 0.5; + double yPos = pos.getY(); + double zPos = (double)pos.getZ() + 0.5; + + Direction direction = state.getValue(FACING).getOpposite(); + Direction.Axis axis = direction.getAxis(); + + double defaultOffset = random.nextDouble() * 0.6 - 0.3; + double xOffsets = axis == Direction.Axis.X ? (double)direction.getStepX() * 0.52 : defaultOffset; + double yOffset = random.nextDouble() * 6.0 / 8.0; + double zOffset = axis == Direction.Axis.Z ? (double)direction.getStepZ() * 0.52 : defaultOffset; + + level.addParticle(ParticleTypes.SMOKE, xPos + xOffsets, yPos + yOffset, zPos + zOffset, 0.0, 0.0, 0.0); + + BlockEntity be = level.getBlockEntity(pos); + if(be instanceof EmbryoCalcificationMachineBlockEntity embryoCalcificationMachineBlockEntity && !embryoCalcificationMachineBlockEntity.itemHandler.getItem(1).isEmpty()) { + level.addParticle(new ItemParticleOption(ParticleTypes.ITEM, embryoCalcificationMachineBlockEntity.itemHandler.getItem(1)), + xPos + xOffsets, yPos + yOffset, zPos + zOffset, 0.0, 0.0, 0.0); + } + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/block/custom/EmbryonicMachineBlock.java b/common/src/main/java/net/cmr/jurassicrevived/block/custom/EmbryonicMachineBlock.java new file mode 100644 index 0000000..8388482 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/block/custom/EmbryonicMachineBlock.java @@ -0,0 +1,240 @@ +package net.cmr.jurassicrevived.block.custom; + +//? if >1.20.1 { +/*import com.mojang.serialization.MapCodec; +import net.minecraft.core.component.DataComponents; +import net.minecraft.world.item.component.CustomData; +import net.minecraft.world.ItemInteractionResult; +*///?} + +import dev.architectury.registry.menu.MenuRegistry; +import net.cmr.jurassicrevived.block.entity.ModBlockEntities; +import net.cmr.jurassicrevived.block.entity.custom.EmbryonicMachineBlockEntity; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.particles.ItemParticleOption; +import net.minecraft.core.particles.ParticleTypes; +import net.minecraft.core.registries.Registries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.util.RandomSource; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.context.BlockPlaceContext; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.*; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityTicker; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.StateDefinition; +import net.minecraft.world.level.block.state.properties.BlockStateProperties; +import net.minecraft.world.level.block.state.properties.BooleanProperty; +import net.minecraft.world.level.block.state.properties.DirectionProperty; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.shapes.CollisionContext; +import net.minecraft.world.phys.shapes.Shapes; +import net.minecraft.world.phys.shapes.VoxelShape; +import org.jetbrains.annotations.Nullable; + +public class EmbryonicMachineBlock extends BaseEntityBlock { + public static final DirectionProperty FACING = BlockStateProperties.HORIZONTAL_FACING; + public static final BooleanProperty LIT = BlockStateProperties.LIT; + + //? if >1.20.1 { + /*public static final MapCodec CODEC = simpleCodec(EmbryonicMachineBlock::new); + @Override protected MapCodec codec() { return CODEC; } + *///?} + + public EmbryonicMachineBlock(Properties properties) { + super(properties); + } + + private static final VoxelShape SHAPE_NORTH = Shapes.box( + 1.0 / 16.0, 0.0 / 16.0, 0.0 / 16.0, + 15.0 / 16.0, 5.0 / 16.0, 15.0 / 16.0 + ); + + private static final VoxelShape SHAPE_SOUTH = rotateShapeY(SHAPE_NORTH, 180); + private static final VoxelShape SHAPE_WEST = rotateShapeY(SHAPE_NORTH, 90); + private static final VoxelShape SHAPE_EAST = rotateShapeY(SHAPE_NORTH, 90); + + private static VoxelShape rotateShapeY(VoxelShape shape, int degrees) { + double rad = Math.toRadians(((degrees % 360) + 360) % 360); + int turns = (int) Math.round(rad / (Math.PI / 2)); + turns = ((turns % 4) + 4) % 4; + + VoxelShape current = shape; + for (int i = 0; i < turns; i++) { + VoxelShape[] buffer = new VoxelShape[]{Shapes.empty()}; + current.forAllBoxes((minX, minY, minZ, maxX, maxY, maxZ) -> { + double nMinX = 1.0 - maxZ; + double nMinZ = minX; + double nMaxX = 1.0 - minZ; + double nMaxZ = maxX; + buffer[0] = Shapes.or(buffer[0], Shapes.box(nMinX, minY, nMinZ, nMaxX, maxY, nMaxZ)); + }); + current = buffer[0]; + } + return current; + } + + @Override + public BlockState rotate(BlockState state, Rotation rotation) { + return state.setValue(FACING, rotation.rotate(state.getValue(FACING))); + } + + @Override + public BlockState mirror(BlockState state, Mirror mirror) { + return state.rotate(mirror.getRotation(state.getValue(FACING))); + } + + @Override + public @Nullable BlockState getStateForPlacement(BlockPlaceContext context) { + return this.defaultBlockState().setValue(FACING, context.getHorizontalDirection().getOpposite().getOpposite()).setValue(LIT, false); + } + + @Override + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + builder.add(FACING, LIT); + } + + @Override + public VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) { + Direction dir = state.getValue(FACING); + return switch (dir) { + case NORTH -> SHAPE_NORTH; + case SOUTH -> SHAPE_SOUTH; + case WEST -> SHAPE_WEST; + case EAST -> SHAPE_EAST; + default -> SHAPE_NORTH; + }; + } + + @Override + public @Nullable BlockEntity newBlockEntity(BlockPos blockPos, BlockState blockState) { + return new EmbryonicMachineBlockEntity(blockPos, blockState); + } + + @Override + public RenderShape getRenderShape(BlockState state) { + return RenderShape.MODEL; + } + + @Override + //? if >1.20.1 { + /*public BlockState playerWillDestroy(Level level, BlockPos pos, BlockState state, Player player) { + *///?} else { + public void playerWillDestroy(Level level, BlockPos pos, BlockState state, Player player) { + //?} + if (!level.isClientSide) { + if (player.getAbilities().instabuild) { + level.removeBlockEntity(pos); + level.setBlock(pos, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL); + //? if >1.20.1 { + /*return state; + *///?} else { + return; + //?} + } + + BlockEntity be = level.getBlockEntity(pos); + if (be instanceof EmbryonicMachineBlockEntity fbe) { + ItemStack stack = new ItemStack(this.asItem()); + + if (!fbe.isEmptyForDrop()) { + //? if >1.20.1 { + /*CompoundTag tag = fbe.saveWithoutMetadata(level.registryAccess()); + var beTypeKey = level.registryAccess().registryOrThrow(Registries.BLOCK_ENTITY_TYPE).getKey(fbe.getType()); + if (beTypeKey != null) tag.putString("id", beTypeKey.toString()); + stack.set(DataComponents.BLOCK_ENTITY_DATA, CustomData.of(tag)); + *///?} else { + CompoundTag tag = fbe.saveWithoutMetadata(); + stack.getOrCreateTagElement("BlockEntityTag").merge(tag); + //?} + } + + popResource(level, pos, stack); + level.removeBlockEntity(pos); + level.setBlock(pos, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL); + //? if >1.20.1 { + /*return state; + *///?} else { + return; + //?} + } + } + super.playerWillDestroy(level, pos, state, player); + //? if >1.20.1 { + /*return state; + *///?} else { + return; + //?} + } + + //? if >1.20.1 { + /*@Override + protected ItemInteractionResult useItemOn(ItemStack pStack, BlockState pState, Level pLevel, BlockPos pPos, Player pPlayer, InteractionHand pHand, BlockHitResult pHitResult) { + if (!pLevel.isClientSide()) { + BlockEntity entity = pLevel.getBlockEntity(pPos); + if(entity instanceof EmbryonicMachineBlockEntity embryonicMachineBlockEntity) { + MenuRegistry.openExtendedMenu((ServerPlayer) pPlayer, embryonicMachineBlockEntity); + } + } + return ItemInteractionResult.sidedSuccess(pLevel.isClientSide()); + } + *///?} else { + @Override + public InteractionResult use(BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, BlockHitResult hit) { + if (!level.isClientSide()) { + BlockEntity entity = level.getBlockEntity(pos); + if (entity instanceof EmbryonicMachineBlockEntity embryonicMachineBlockEntity) { + MenuRegistry.openExtendedMenu((ServerPlayer) player, embryonicMachineBlockEntity); + } + } + return InteractionResult.sidedSuccess(level.isClientSide()); + } + //?} + + @Override + public @Nullable BlockEntityTicker getTicker(Level level, BlockState state, BlockEntityType blockEntityType) { + if (blockEntityType != ModBlockEntities.EMBRYONIC_MACHINE_BE.get()) return null; + + if (level.isClientSide) { + return null; + } else { + return createTickerHelper(blockEntityType, ModBlockEntities.EMBRYONIC_MACHINE_BE.get(), + (level1, blockPos, blockState, embryonicMachineBlockEntity) -> embryonicMachineBlockEntity.tick(level1, blockPos, blockState)); + } + } + + @Override + public void animateTick(BlockState state, Level level, BlockPos pos, RandomSource random) { + if (!state.getValue(LIT)) { + return; + } + + double xPos = (double)pos.getX() + 0.5; + double yPos = pos.getY(); + double zPos = (double)pos.getZ() + 0.5; + + Direction direction = state.getValue(FACING).getOpposite(); + Direction.Axis axis = direction.getAxis(); + + double defaultOffset = random.nextDouble() * 0.6 - 0.3; + double xOffsets = axis == Direction.Axis.X ? (double)direction.getStepX() * 0.52 : defaultOffset; + double yOffset = random.nextDouble() * 6.0 / 8.0; + double zOffset = axis == Direction.Axis.Z ? (double)direction.getStepZ() * 0.52 : defaultOffset; + + level.addParticle(ParticleTypes.SMOKE, xPos + xOffsets, yPos + yOffset, zPos + zOffset, 0.0, 0.0, 0.0); + + BlockEntity be = level.getBlockEntity(pos); + if(be instanceof EmbryonicMachineBlockEntity embryonicMachineBlockEntity && !embryonicMachineBlockEntity.itemHandler.getItem(1).isEmpty()) { + level.addParticle(new ItemParticleOption(ParticleTypes.ITEM, embryonicMachineBlockEntity.itemHandler.getItem(1)), + xPos + xOffsets, yPos + yOffset, zPos + zOffset, 0.0, 0.0, 0.0); + } + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/block/custom/FenceLightBlock.java b/common/src/main/java/net/cmr/jurassicrevived/block/custom/FenceLightBlock.java new file mode 100644 index 0000000..bebcd36 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/block/custom/FenceLightBlock.java @@ -0,0 +1,55 @@ +package net.cmr.jurassicrevived.block.custom; + +import com.mojang.serialization.MapCodec; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.item.context.BlockPlaceContext; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.HorizontalDirectionalBlock; +import net.minecraft.world.level.block.Mirror; +import net.minecraft.world.level.block.Rotation; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.StateDefinition; +import net.minecraft.world.level.block.state.properties.Property; +import net.minecraft.world.phys.shapes.CollisionContext; +import net.minecraft.world.phys.shapes.VoxelShape; + +public class FenceLightBlock extends HorizontalDirectionalBlock { + + public static final VoxelShape SHAPE = Block.box(6.0D, 0.0D, 6.0D, 10.0D, 6.0D, 10.0D); + + public FenceLightBlock(Properties properties) { + super(properties); + this.registerDefaultState((BlockState)((BlockState)((BlockState)this.stateDefinition.any()).setValue(FACING, Direction.SOUTH))); + } + + protected MapCodec codec() { + return null; + } + + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + builder.add(new Property[]{FACING}); + } + + public BlockState mirror(BlockState state, Mirror mirror) { + return state.rotate(mirror.getRotation((Direction)state.getValue(FACING))); + } + + // Ensure rotation of FACING property when the block is rotated (e.g., structure placement, commands, etc.) + @Override + public BlockState rotate(BlockState state, Rotation rotation) { + return state.setValue(FACING, rotation.rotate(state.getValue(FACING))); + } + + public BlockState getStateForPlacement(BlockPlaceContext context) { + // Ensure we always use a horizontal facing (N/E/S/W), even when looking up or down. + return (BlockState)this.defaultBlockState().setValue(FACING, context.getHorizontalDirection().getOpposite()); + } + + @Override + public VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) { + Direction facing = state.getValue(FACING); + return SHAPE; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/block/custom/FencePoleBlock.java b/common/src/main/java/net/cmr/jurassicrevived/block/custom/FencePoleBlock.java new file mode 100644 index 0000000..fbad5f8 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/block/custom/FencePoleBlock.java @@ -0,0 +1,275 @@ +package net.cmr.jurassicrevived.block.custom; + +import net.cmr.jurassicrevived.util.FenceUpdateGuard; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.item.context.BlockPlaceContext; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.LevelAccessor; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.SimpleWaterloggedBlock; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.StateDefinition; +import net.minecraft.world.level.block.state.properties.BlockStateProperties; +import net.minecraft.world.level.block.state.properties.BooleanProperty; +import net.minecraft.world.level.block.state.properties.IntegerProperty; +import net.minecraft.world.level.material.FluidState; +import net.minecraft.world.level.material.Fluids; +import net.minecraft.world.phys.shapes.CollisionContext; +import net.minecraft.world.phys.shapes.Shapes; +import net.minecraft.world.phys.shapes.VoxelShape; + +public class FencePoleBlock extends Block implements SimpleWaterloggedBlock { + public static final BooleanProperty WATERLOGGED = BlockStateProperties.WATERLOGGED; + public static final BooleanProperty NORTH = BooleanProperty.create("north"); + public static final BooleanProperty EAST = BooleanProperty.create("east"); + public static final BooleanProperty SOUTH = BooleanProperty.create("south"); + public static final BooleanProperty WEST = BooleanProperty.create("west"); + public static final BooleanProperty NE = BooleanProperty.create("ne"); + public static final BooleanProperty SE = BooleanProperty.create("se"); + public static final BooleanProperty SW = BooleanProperty.create("sw"); + public static final BooleanProperty NW = BooleanProperty.create("nw"); + public static final IntegerProperty TIER = IntegerProperty.create("tier", 0, 2); + + public enum Tier { + LOW(0), MEDIUM(1), HIGH(2); + public final int id; + Tier(int id) { this.id = id; } + } + + private final Tier tierConfig; + + public FencePoleBlock(Properties properties, Tier tier) { + super(properties); + this.tierConfig = tier; + this.registerDefaultState(this.stateDefinition.any() + .setValue(NORTH, false) + .setValue(EAST, false) + .setValue(SOUTH, false) + .setValue(WEST, false) + .setValue(NE, false) + .setValue(SE, false) + .setValue(SW, false) + .setValue(NW, false) + .setValue(TIER, tier.id) + .setValue(WATERLOGGED, false) + ); + } + + @Override + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + builder.add(WATERLOGGED, NORTH, EAST, SOUTH, WEST, NE, SE, SW, NW, TIER); + } + + @Override + public BlockState getStateForPlacement(BlockPlaceContext ctx) { + Level level = ctx.getLevel(); + BlockPos pos = ctx.getClickedPos(); + FluidState fluid = level.getFluidState(pos); + + return this.defaultBlockState() + .setValue(NORTH, connectsTo(level, pos, Direction.NORTH)) + .setValue(EAST, connectsTo(level, pos, Direction.EAST)) + .setValue(SOUTH, connectsTo(level, pos, Direction.SOUTH)) + .setValue(WEST, connectsTo(level, pos, Direction.WEST)) + .setValue(NE, FenceWireBlock.canConnectDiagonally(level, pos, Direction.NORTH, Direction.EAST)) + .setValue(SE, FenceWireBlock.canConnectDiagonally(level, pos, Direction.SOUTH, Direction.EAST)) + .setValue(SW, FenceWireBlock.canConnectDiagonally(level, pos, Direction.SOUTH, Direction.WEST)) + .setValue(NW, FenceWireBlock.canConnectDiagonally(level, pos, Direction.NORTH, Direction.WEST)) + .setValue(TIER, tierConfig.id) + .setValue(WATERLOGGED, fluid.getType() == Fluids.WATER); + } + + @Override + @SuppressWarnings("deprecation") + public BlockState updateShape(BlockState state, Direction dir, BlockState neighborState, LevelAccessor level, BlockPos pos, BlockPos neighborPos) { + if (state.getValue(WATERLOGGED)) { + level.scheduleTick(pos, Fluids.WATER, Fluids.WATER.getTickDelay(level)); + } + if (dir.getAxis().isHorizontal()) { + boolean connect = connectsTo(level, pos, dir); + state = state.setValue(propertyFor(dir), connect); + } + return state; + } + + @Override + @SuppressWarnings("deprecation") + public void neighborChanged(BlockState state, Level level, BlockPos pos, Block neighborBlock, BlockPos neighborPos, boolean movedByPiston) { + BlockState updated = state + .setValue(NE, FenceWireBlock.canConnectDiagonally(level, pos, Direction.NORTH, Direction.EAST)) + .setValue(SE, FenceWireBlock.canConnectDiagonally(level, pos, Direction.SOUTH, Direction.EAST)) + .setValue(SW, FenceWireBlock.canConnectDiagonally(level, pos, Direction.SOUTH, Direction.WEST)) + .setValue(NW, FenceWireBlock.canConnectDiagonally(level, pos, Direction.NORTH, Direction.WEST)); + if (updated != state) { + level.setBlock(pos, updated, Block.UPDATE_CLIENTS); + } + + if (beginGuard()) { + try { + updateDiagonalsAround(level, pos); + } finally { + endGuard(); + } + } + + super.neighborChanged(state, level, pos, neighborBlock, neighborPos, movedByPiston); + } + + private static boolean beginGuard() { + return FenceUpdateGuard.begin(); + } + + private static void endGuard() { + FenceUpdateGuard.end(); + } + + private void updateDiagonalsAround(Level level, BlockPos pos) { + BlockPos[] diags = new BlockPos[] { + pos.north().east(), pos.south().east(), pos.south().west(), pos.north().west() + }; + for (BlockPos p : diags) { + BlockState bs = level.getBlockState(p); + Block b = bs.getBlock(); + if (b instanceof FencePoleBlock) { + boolean ne = FenceWireBlock.canConnectDiagonally(level, p, Direction.NORTH, Direction.EAST); + boolean se = FenceWireBlock.canConnectDiagonally(level, p, Direction.SOUTH, Direction.EAST); + boolean sw = FenceWireBlock.canConnectDiagonally(level, p, Direction.SOUTH, Direction.WEST); + boolean nw = FenceWireBlock.canConnectDiagonally(level, p, Direction.NORTH, Direction.WEST); + BlockState updated = bs + .setValue(FencePoleBlock.NE, ne) + .setValue(FencePoleBlock.SE, se) + .setValue(FencePoleBlock.SW, sw) + .setValue(FencePoleBlock.NW, nw); + if (updated != bs) { + level.setBlock(p, updated, Block.UPDATE_CLIENTS); + } + } else if (b instanceof FenceWireBlock) { + boolean ne = FenceWireBlock.canConnectDiagonally(level, p, Direction.NORTH, Direction.EAST); + boolean se = FenceWireBlock.canConnectDiagonally(level, p, Direction.SOUTH, Direction.EAST); + boolean sw = FenceWireBlock.canConnectDiagonally(level, p, Direction.SOUTH, Direction.WEST); + boolean nw = FenceWireBlock.canConnectDiagonally(level, p, Direction.NORTH, Direction.WEST); + BlockState updated = bs + .setValue(FenceWireBlock.NE, ne) + .setValue(FenceWireBlock.SE, se) + .setValue(FenceWireBlock.SW, sw) + .setValue(FenceWireBlock.NW, nw); + if (updated != bs) { + level.setBlock(p, updated, Block.UPDATE_CLIENTS); + } + } + } + } + + private boolean connectsTo(LevelAccessor level, BlockPos pos, Direction dir) { + BlockPos neighborPos = pos.relative(dir); + BlockState neighbor = level.getBlockState(neighborPos); + Block nb = neighbor.getBlock(); + + if (nb instanceof FenceWireBlock || nb instanceof FencePoleBlock) { + return true; + } + + return neighbor.isFaceSturdy(level, neighborPos, dir.getOpposite()); + } + + private static BooleanProperty propertyFor(Direction dir) { + return switch (dir) { + case NORTH -> NORTH; + case EAST -> EAST; + case SOUTH -> SOUTH; + case WEST -> WEST; + default -> throw new IllegalArgumentException("Only horizontal"); + }; + } + + @Override + public FluidState getFluidState(BlockState state) { + return state.getValue(WATERLOGGED) ? Fluids.WATER.getSource(false) : super.getFluidState(state); + } + + @Override + public boolean propagatesSkylightDown(BlockState state, BlockGetter level, BlockPos pos) { + return true; + } + + @Override + public int getLightBlock(BlockState state, BlockGetter level, BlockPos pos) { + return 0; + } + + @Override + public float getShadeBrightness(BlockState state, BlockGetter level, BlockPos pos) { + return 1.0F; + } + + private static final VoxelShape POST = Block.box(6.5, 0.0, 6.5, 9.5, 16.0, 9.5); + + private static final VoxelShape ARM_NORTH = Block.box(7.5, 2.5, 0.0, 8.5, 13.5, 8.0); + private static final VoxelShape ARM_SOUTH = Block.box(7.5, 2.5, 8.0, 8.5, 13.5, 16.0); + private static final VoxelShape ARM_WEST = Block.box(0.0, 2.5, 7.5, 8.0, 13.5, 8.5); + private static final VoxelShape ARM_EAST = Block.box(8.0, 2.5, 7.5, 16.0, 13.5, 8.5); + + private static VoxelShape buildDiagonal(boolean east, boolean south) { + VoxelShape shape = Shapes.empty(); + double y1 = 2.5, y2 = 13.5; + for (int i = 0; i < 8; i++) { + double off = i; + double x1 = east ? 16 - (off + 1) : 0 + off; + double x2 = x1 + 1; + double z1 = south ? 16 - (off + 1) : 0 + off; + double z2 = z1 + 1; + shape = Shapes.or(shape, Block.box(x1, y1, z1, x2, y2, z2)); + } + return shape; + } + + private static final VoxelShape DIAG_NE = buildDiagonal(true, false); + private static final VoxelShape DIAG_SE = buildDiagonal(true, true); + private static final VoxelShape DIAG_SW = buildDiagonal(false, true); + private static final VoxelShape DIAG_NW = buildDiagonal(false, false); + + // Precomputed shapes for all 256 combinations: + // bit 0..3: NORTH, EAST, SOUTH, WEST; bit 4..7: NE, SE, SW, NW + private static final VoxelShape[] SHAPES = new VoxelShape[256]; + + static { + for (int mask = 0; mask < SHAPES.length; mask++) { + VoxelShape s = POST; + + if ((mask & (1 << 0)) != 0) s = Shapes.or(s, ARM_NORTH); + if ((mask & (1 << 1)) != 0) s = Shapes.or(s, ARM_EAST); + if ((mask & (1 << 2)) != 0) s = Shapes.or(s, ARM_SOUTH); + if ((mask & (1 << 3)) != 0) s = Shapes.or(s, ARM_WEST); + + if ((mask & (1 << 4)) != 0) s = Shapes.or(s, DIAG_NE); + if ((mask & (1 << 5)) != 0) s = Shapes.or(s, DIAG_SE); + if ((mask & (1 << 6)) != 0) s = Shapes.or(s, DIAG_SW); + if ((mask & (1 << 7)) != 0) s = Shapes.or(s, DIAG_NW); + + SHAPES[mask] = s; + } + } + + @Override + public VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext ctx) { + int mask = 0; + if (state.getValue(NORTH)) mask |= (1 << 0); + if (state.getValue(EAST)) mask |= (1 << 1); + if (state.getValue(SOUTH)) mask |= (1 << 2); + if (state.getValue(WEST)) mask |= (1 << 3); + + if (state.getValue(NE)) mask |= (1 << 4); + if (state.getValue(SE)) mask |= (1 << 5); + if (state.getValue(SW)) mask |= (1 << 6); + if (state.getValue(NW)) mask |= (1 << 7); + + return SHAPES[mask]; + } + + @Override + public VoxelShape getCollisionShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext ctx) { + return getShape(state, level, pos, ctx); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/block/custom/FenceWireBlock.java b/common/src/main/java/net/cmr/jurassicrevived/block/custom/FenceWireBlock.java new file mode 100644 index 0000000..749f032 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/block/custom/FenceWireBlock.java @@ -0,0 +1,413 @@ +package net.cmr.jurassicrevived.block.custom; + +import net.cmr.jurassicrevived.util.FenceUpdateGuard; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.item.context.BlockPlaceContext; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.LevelAccessor; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.SimpleWaterloggedBlock; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.StateDefinition; +import net.minecraft.world.level.block.state.properties.BlockStateProperties; +import net.minecraft.world.level.block.state.properties.BooleanProperty; +import net.minecraft.world.level.block.state.properties.IntegerProperty; +import net.minecraft.world.level.material.FluidState; +import net.minecraft.world.level.material.Fluids; +import net.minecraft.world.phys.shapes.CollisionContext; +import net.minecraft.world.phys.shapes.Shapes; +import net.minecraft.world.phys.shapes.VoxelShape; + +public class FenceWireBlock extends Block implements SimpleWaterloggedBlock { + + public static final BooleanProperty WATERLOGGED = BlockStateProperties.WATERLOGGED; + + private static boolean beginGuard() { + return FenceUpdateGuard.begin(); + } + + private static void endGuard() { + FenceUpdateGuard.end(); + } + + public static final BooleanProperty NORTH = BlockStateProperties.NORTH; + public static final BooleanProperty EAST = BlockStateProperties.EAST; + public static final BooleanProperty SOUTH = BlockStateProperties.SOUTH; + public static final BooleanProperty WEST = BlockStateProperties.WEST; + public static final BooleanProperty POWERED = BlockStateProperties.POWERED; + public static final BooleanProperty NE = BooleanProperty.create("ne"); + public static final BooleanProperty SE = BooleanProperty.create("se"); + public static final BooleanProperty SW = BooleanProperty.create("sw"); + public static final BooleanProperty NW = BooleanProperty.create("nw"); + public static final IntegerProperty TIER = IntegerProperty.create("tier", 0, 2); + + public enum Tier { + LOW(0), MEDIUM(1), HIGH(2); + public final int id; + + Tier(int id) { + this.id = id; + } + } + + private final Tier tierConfig; + + public FenceWireBlock(Properties properties, Tier tier) { + super(properties); + this.tierConfig = tier; + this.registerDefaultState(this.stateDefinition.any() + .setValue(NORTH, false) + .setValue(EAST, false) + .setValue(SOUTH, false) + .setValue(WEST, false) + .setValue(POWERED, false) + .setValue(NE, false) + .setValue(SE, false) + .setValue(SW, false) + .setValue(NW, false) + .setValue(TIER, tier.id) + .setValue(WATERLOGGED, false) + ); + } + + @Override + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + builder.add(NORTH, EAST, SOUTH, WEST, POWERED, NE, SE, SW, NW, TIER, WATERLOGGED); + } + + @Override + public BlockState getStateForPlacement(BlockPlaceContext ctx) { + Level level = ctx.getLevel(); + BlockPos pos = ctx.getClickedPos(); + boolean powered = level.hasNeighborSignal(pos); + boolean waterlogged = level.getFluidState(pos).getType() == Fluids.WATER; + + return this.defaultBlockState() + .setValue(NORTH, connectsCardinalTo(level, pos, Direction.NORTH)) + .setValue(EAST, connectsCardinalTo(level, pos, Direction.EAST)) + .setValue(SOUTH, connectsCardinalTo(level, pos, Direction.SOUTH)) + .setValue(WEST, connectsCardinalTo(level, pos, Direction.WEST)) + .setValue(NE, canConnectDiagonally(level, pos, Direction.NORTH, Direction.EAST)) + .setValue(SE, canConnectDiagonally(level, pos, Direction.SOUTH, Direction.EAST)) + .setValue(SW, canConnectDiagonally(level, pos, Direction.SOUTH, Direction.WEST)) + .setValue(NW, canConnectDiagonally(level, pos, Direction.NORTH, Direction.WEST)) + .setValue(POWERED, powered) + .setValue(TIER, tierConfig.id) + .setValue(WATERLOGGED, waterlogged); + } + + @Override + @SuppressWarnings("deprecation") + public BlockState updateShape(BlockState state, Direction dir, BlockState neighborState, LevelAccessor level, BlockPos pos, BlockPos neighborPos) { + if (state.getValue(WATERLOGGED)) { + level.scheduleTick(pos, Fluids.WATER, Fluids.WATER.getTickDelay(level)); + } + if (dir.getAxis().isHorizontal()) { + boolean connect = connectsCardinalTo(level, pos, dir); + state = state.setValue(propertyFor(dir), connect); + } + state = state + .setValue(NE, canConnectDiagonally(level, pos, Direction.NORTH, Direction.EAST)) + .setValue(SE, canConnectDiagonally(level, pos, Direction.SOUTH, Direction.EAST)) + .setValue(SW, canConnectDiagonally(level, pos, Direction.SOUTH, Direction.WEST)) + .setValue(NW, canConnectDiagonally(level, pos, Direction.NORTH, Direction.WEST)); + return state; + } + + @Override + public FluidState getFluidState(BlockState state) { + return state.getValue(WATERLOGGED) ? Fluids.WATER.getSource(false) : super.getFluidState(state); + } + + @Override + @SuppressWarnings("deprecation") + public void neighborChanged(BlockState state, Level level, BlockPos pos, Block neighborBlock, BlockPos neighborPos, boolean movedByPiston) { + boolean poweredNow = level.hasNeighborSignal(pos); + if (poweredNow != state.getValue(POWERED)) { + if (beginGuard()) { + try { + level.setBlock(pos, state.setValue(POWERED, poweredNow), Block.UPDATE_ALL); + } finally { + endGuard(); + } + } else { + level.setBlock(pos, state.setValue(POWERED, poweredNow), Block.UPDATE_ALL); + } + } + + if (beginGuard()) { + try { + BlockState updated = state + .setValue(NE, canConnectDiagonally(level, pos, Direction.NORTH, Direction.EAST)) + .setValue(SE, canConnectDiagonally(level, pos, Direction.SOUTH, Direction.EAST)) + .setValue(SW, canConnectDiagonally(level, pos, Direction.SOUTH, Direction.WEST)) + .setValue(NW, canConnectDiagonally(level, pos, Direction.NORTH, Direction.WEST)); + if (updated != state) { + level.setBlock(pos, updated, Block.UPDATE_CLIENTS); + } + updateDiagonalsAround(level, pos); + } finally { + endGuard(); + } + } + } + + private void recomputeSelfDiagonals(Level level, BlockPos pos, BlockState state) { + BlockState updated = state + .setValue(NE, canConnectDiagonally(level, pos, Direction.NORTH, Direction.EAST)) + .setValue(SE, canConnectDiagonally(level, pos, Direction.SOUTH, Direction.EAST)) + .setValue(SW, canConnectDiagonally(level, pos, Direction.SOUTH, Direction.WEST)) + .setValue(NW, canConnectDiagonally(level, pos, Direction.NORTH, Direction.WEST)); + if (updated != state) { + // Diagonal-only change: client update only + level.setBlock(pos, updated, Block.UPDATE_CLIENTS); + } + } + + private void updateDiagonalsAround(Level level, BlockPos pos) { + BlockPos nePos = pos.north().east(); + BlockPos sePos = pos.south().east(); + BlockPos swPos = pos.south().west(); + BlockPos nwPos = pos.north().west(); + + BlockPos[] diags = new BlockPos[]{nePos, sePos, swPos, nwPos}; + + for (BlockPos p : diags) { + BlockState bs = level.getBlockState(p); + Block b = bs.getBlock(); + + if (b instanceof FencePoleBlock) { + BooleanProperty backFlag; + Direction backA; + Direction backB; + + if (p.equals(nePos)) { + backFlag = FencePoleBlock.SW; + backA = Direction.SOUTH; + backB = Direction.WEST; + } else if (p.equals(sePos)) { + backFlag = FencePoleBlock.NW; + backA = Direction.NORTH; + backB = Direction.WEST; + } else if (p.equals(swPos)) { + backFlag = FencePoleBlock.NE; + backA = Direction.NORTH; + backB = Direction.EAST; + } else { + backFlag = FencePoleBlock.SE; + backA = Direction.SOUTH; + backB = Direction.EAST; + } + + boolean allow = FenceWireBlock.canConnectDiagonally(level, p, backA, backB); + + BlockState updated = bs.setValue(backFlag, allow); + if (updated != bs) { + level.setBlock(p, updated, Block.UPDATE_CLIENTS); + } + + } else if (b instanceof FenceWireBlock) { + boolean ne = canConnectDiagonally(level, p, Direction.NORTH, Direction.EAST); + boolean se = canConnectDiagonally(level, p, Direction.SOUTH, Direction.EAST); + boolean sw = canConnectDiagonally(level, p, Direction.SOUTH, Direction.WEST); + boolean nw = canConnectDiagonally(level, p, Direction.NORTH, Direction.WEST); + BlockState updated = bs + .setValue(FenceWireBlock.NE, ne) + .setValue(FenceWireBlock.SE, se) + .setValue(FenceWireBlock.SW, sw) + .setValue(FenceWireBlock.NW, nw); + if (updated != bs) { + level.setBlock(p, updated, Block.UPDATE_CLIENTS); + } + } + } + } + + public static boolean canConnectDiagonally(LevelAccessor level, BlockPos pos, Direction a, Direction b) { + BlockState srcState = level.getBlockState(pos); + Block srcBlock = srcState.getBlock(); + + BlockPos diag = pos.relative(a).relative(b); + BlockState diagState = level.getBlockState(diag); + Block diagBlock = diagState.getBlock(); + + boolean srcIsPole = srcBlock instanceof FencePoleBlock; + boolean diagIsWire = diagBlock instanceof FenceWireBlock; + boolean diagIsPole = diagBlock instanceof FencePoleBlock; + + boolean diagIsSolidCorner = + diagState.isFaceSturdy(level, diag, a.getOpposite()) + || diagState.isFaceSturdy(level, diag, b.getOpposite()); + + if (!diagIsWire && !diagIsPole && !diagIsSolidCorner) return false; + + if (srcIsPole || diagIsPole) { + return true; + } + + BlockPos stepA = pos.relative(a); + BlockPos stepB = pos.relative(b); + + BlockState stateA = level.getBlockState(stepA); + BlockState stateB = level.getBlockState(stepB); + + boolean stepABlocks = + stateA.getBlock() instanceof FenceWireBlock + || stateA.getBlock() instanceof FencePoleBlock + || stateA.isFaceSturdy(level, stepA, a.getOpposite()); + + boolean stepBBlocks = + stateB.getBlock() instanceof FenceWireBlock + || stateB.getBlock() instanceof FencePoleBlock + || stateB.isFaceSturdy(level, stepB, b.getOpposite()); + + if (stepABlocks || stepBBlocks) return false; + + return true; + } + + private static boolean canConnectDiagonallyToFence(LevelAccessor level, BlockPos pos, Direction a, Direction b) { + BlockPos diag = pos.relative(a).relative(b); + BlockState diagState = level.getBlockState(diag); + Block db = diagState.getBlock(); + if (!(db instanceof FenceWireBlock) && !(db instanceof FencePoleBlock)) { + return false; + } + return canConnectDiagonally(level, pos, a, b); + } + + private boolean connectsTo(BlockState neighbor) { + Block b = neighbor.getBlock(); + return (b instanceof FenceWireBlock) || (b instanceof FencePoleBlock); + } + + private static BooleanProperty propertyFor(Direction dir) { + return switch (dir) { + case NORTH -> NORTH; + case EAST -> EAST; + case SOUTH -> SOUTH; + case WEST -> WEST; + default -> throw new IllegalArgumentException("Only horizontal"); + }; + } + + private boolean connectsCardinalTo(LevelAccessor level, BlockPos pos, Direction dir) { + BlockPos neighborPos = pos.relative(dir); + BlockState neighbor = level.getBlockState(neighborPos); + Block nb = neighbor.getBlock(); + + if (nb instanceof FencePoleBlock) { + return true; + } + + if (nb instanceof FenceWireBlock) { + Direction right = dir.getClockWise(); + Direction left = dir.getCounterClockWise(); + + boolean thisDiagRight = canConnectDiagonallyToFence(level, pos, dir, right); + boolean thisDiagLeft = canConnectDiagonallyToFence(level, pos, dir, left); + + Direction back = dir.getOpposite(); + boolean neighDiagRight = canConnectDiagonallyToFence(level, neighborPos, back, back.getClockWise()); + boolean neighDiagLeft = canConnectDiagonallyToFence(level, neighborPos, back, back.getCounterClockWise()); + + if (thisDiagRight || thisDiagLeft || neighDiagRight || neighDiagLeft) { + return false; + } + + return true; + } + + if (neighbor.isFaceSturdy(level, neighborPos, dir.getOpposite())) { + return true; + } + + return false; + } + + @Override + public boolean propagatesSkylightDown(BlockState state, BlockGetter level, BlockPos pos) { + return true; + } + + @Override + public int getLightBlock(BlockState state, BlockGetter level, BlockPos pos) { + return 0; + } + + @Override + public float getShadeBrightness(BlockState state, BlockGetter level, BlockPos pos) { + return 1.0F; + } + + private static final VoxelShape CENTER = Block.box(7.5, 2.5, 7.5, 8.5, 13.5, 8.5); + + private static final VoxelShape ARM_NORTH = Block.box(7.5, 2.5, 0.0, 8.5, 13.5, 8.0); + private static final VoxelShape ARM_SOUTH = Block.box(7.5, 2.5, 8.0, 8.5, 13.5, 16.0); + private static final VoxelShape ARM_WEST = Block.box(0.0, 2.5, 7.5, 8.0, 13.5, 8.5); + private static final VoxelShape ARM_EAST = Block.box(8.0, 2.5, 7.5, 16.0, 13.5, 8.5); + + private static VoxelShape buildDiagonal(boolean east, boolean south) { + VoxelShape shape = Shapes.empty(); + double y1 = 2.5, y2 = 13.5; + for (int i = 0; i < 8; i++) { + double off = i; // 0..7 + double x1 = east ? 16 - (off + 1) : 0 + off; + double x2 = x1 + 1; + double z1 = south ? 16 - (off + 1) : 0 + off; + double z2 = z1 + 1; + shape = Shapes.or(shape, Block.box(x1, y1, z1, x2, y2, z2)); + } + return shape; + } + + private static final VoxelShape DIAG_NE = buildDiagonal(true, false); + private static final VoxelShape DIAG_SE = buildDiagonal(true, true); + private static final VoxelShape DIAG_SW = buildDiagonal(false, true); + private static final VoxelShape DIAG_NW = buildDiagonal(false, false); + + // Precomputed shapes for all 256 combinations: + // bit 0..3: NORTH, EAST, SOUTH, WEST; bit 4..7: NE, SE, SW, NW + private static final VoxelShape[] SHAPES = new VoxelShape[256]; + + static { + for (int mask = 0; mask < SHAPES.length; mask++) { + VoxelShape s = CENTER; + + if ((mask & (1 << 0)) != 0) s = Shapes.or(s, ARM_NORTH); + if ((mask & (1 << 1)) != 0) s = Shapes.or(s, ARM_EAST); + if ((mask & (1 << 2)) != 0) s = Shapes.or(s, ARM_SOUTH); + if ((mask & (1 << 3)) != 0) s = Shapes.or(s, ARM_WEST); + + if ((mask & (1 << 4)) != 0) s = Shapes.or(s, DIAG_NE); + if ((mask & (1 << 5)) != 0) s = Shapes.or(s, DIAG_SE); + if ((mask & (1 << 6)) != 0) s = Shapes.or(s, DIAG_SW); + if ((mask & (1 << 7)) != 0) s = Shapes.or(s, DIAG_NW); + + SHAPES[mask] = s; + } + } + + @Override + public VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext ctx) { + int mask = 0; + if (state.getValue(NORTH)) mask |= (1 << 0); + if (state.getValue(EAST)) mask |= (1 << 1); + if (state.getValue(SOUTH)) mask |= (1 << 2); + if (state.getValue(WEST)) mask |= (1 << 3); + + if (state.getValue(NE)) mask |= (1 << 4); + if (state.getValue(SE)) mask |= (1 << 5); + if (state.getValue(SW)) mask |= (1 << 6); + if (state.getValue(NW)) mask |= (1 << 7); + + return SHAPES[mask]; + } + + @Override + public VoxelShape getCollisionShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext ctx) { + return getShape(state, level, pos, ctx); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/block/custom/FossilCleanerBlock.java b/common/src/main/java/net/cmr/jurassicrevived/block/custom/FossilCleanerBlock.java new file mode 100644 index 0000000..8462abf --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/block/custom/FossilCleanerBlock.java @@ -0,0 +1,263 @@ +package net.cmr.jurassicrevived.block.custom; + +//? if >1.20.1 { +/*import com.mojang.serialization.MapCodec; +import net.minecraft.core.component.DataComponents; +import net.minecraft.world.item.component.CustomData; +import net.minecraft.world.ItemInteractionResult; +*///?} + +import dev.architectury.registry.menu.MenuRegistry; +import net.cmr.jurassicrevived.block.entity.ModBlockEntities; +import net.cmr.jurassicrevived.block.entity.custom.FossilCleanerBlockEntity; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.particles.ItemParticleOption; +import net.minecraft.core.particles.ParticleTypes; +import net.minecraft.core.registries.Registries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.util.RandomSource; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.context.BlockPlaceContext; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.*; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityTicker; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.StateDefinition; +import net.minecraft.world.level.block.state.properties.BlockStateProperties; +import net.minecraft.world.level.block.state.properties.BooleanProperty; +import net.minecraft.world.level.block.state.properties.DirectionProperty; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.shapes.CollisionContext; +import net.minecraft.world.phys.shapes.Shapes; +import net.minecraft.world.phys.shapes.VoxelShape; +import org.jetbrains.annotations.Nullable; + +public class FossilCleanerBlock extends BaseEntityBlock { + public static final DirectionProperty FACING = BlockStateProperties.HORIZONTAL_FACING; + public static final BooleanProperty LIT = BlockStateProperties.LIT; + + //? if >1.20.1 { + /*public static final MapCodec CODEC = simpleCodec(FossilCleanerBlock::new); + @Override protected MapCodec codec() { return CODEC; } + *///?} + + public FossilCleanerBlock(Properties properties) { + super(properties); + } + + private static final VoxelShape SHAPE_NORTH = Shapes.box( + 2.0 / 16.0, 0.0 / 16.0, 2.0 / 16.0, + 14.0 / 16.0, 14.0 / 16.0, 15.0 / 16.0 + ); + + private static final VoxelShape SHAPE_SOUTH = rotateShapeY(SHAPE_NORTH, 180); + private static final VoxelShape SHAPE_WEST = rotateShapeY(SHAPE_NORTH, -90); + private static final VoxelShape SHAPE_EAST = rotateShapeY(SHAPE_NORTH, 90); + + private static VoxelShape rotateShapeY(VoxelShape shape, int degrees) { + double rad = Math.toRadians(((degrees % 360) + 360) % 360); + int turns = (int) Math.round(rad / (Math.PI / 2)); + turns = ((turns % 4) + 4) % 4; + + VoxelShape current = shape; + for (int i = 0; i < turns; i++) { + VoxelShape[] buffer = new VoxelShape[]{Shapes.empty()}; + current.forAllBoxes((minX, minY, minZ, maxX, maxY, maxZ) -> { + double nMinX = 1.0 - maxZ; + double nMinZ = minX; + double nMaxX = 1.0 - minZ; + double nMaxZ = maxX; + buffer[0] = Shapes.or(buffer[0], Shapes.box(nMinX, minY, nMinZ, nMaxX, maxY, nMaxZ)); + }); + current = buffer[0]; + } + return current; + } + + @Override + public BlockState rotate(BlockState state, Rotation rotation) { + return state.setValue(FACING, rotation.rotate(state.getValue(FACING))); + } + + @Override + public BlockState mirror(BlockState state, Mirror mirror) { + return state.rotate(mirror.getRotation(state.getValue(FACING))); + } + + @Override + public @Nullable BlockState getStateForPlacement(BlockPlaceContext context) { + return this.defaultBlockState().setValue(FACING, context.getHorizontalDirection().getOpposite().getOpposite()).setValue(LIT, false); + } + + @Override + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + builder.add(FACING, LIT); + } + + @Override + public VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) { + Direction dir = state.getValue(FACING); + return switch (dir) { + case NORTH -> SHAPE_NORTH; + case SOUTH -> SHAPE_SOUTH; + case WEST -> SHAPE_WEST; + case EAST -> SHAPE_EAST; + default -> SHAPE_NORTH; + }; + } + + @Override + public @Nullable BlockEntity newBlockEntity(BlockPos blockPos, BlockState blockState) { + return new FossilCleanerBlockEntity(blockPos, blockState); + } + + @Override + public RenderShape getRenderShape(BlockState state) { + return RenderShape.MODEL; + } + + @Override + //? if >1.20.1 { + /*public BlockState playerWillDestroy(Level level, BlockPos pos, BlockState state, Player player) { + *///?} else { + public void playerWillDestroy(Level level, BlockPos pos, BlockState state, Player player) { + //?} + if (!level.isClientSide) { + if (player.getAbilities().instabuild) { + level.removeBlockEntity(pos); + level.setBlock(pos, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL); + //? if >1.20.1 { + /*return state; + *///?} else { + return; + //?} + } + + BlockEntity be = level.getBlockEntity(pos); + if (be instanceof FossilCleanerBlockEntity fbe) { + ItemStack stack = new ItemStack(this.asItem()); + + if (!fbe.isEmptyForDrop()) { + //? if >1.20.1 { + /*CompoundTag tag = fbe.saveWithoutMetadata(level.registryAccess()); + var beTypeKey = level.registryAccess().registryOrThrow(Registries.BLOCK_ENTITY_TYPE).getKey(fbe.getType()); + if (beTypeKey != null) tag.putString("id", beTypeKey.toString()); + stack.set(DataComponents.BLOCK_ENTITY_DATA, CustomData.of(tag)); + *///?} else { + CompoundTag tag = fbe.saveWithoutMetadata(); + stack.getOrCreateTagElement("BlockEntityTag").merge(tag); + //?} + } + + popResource(level, pos, stack); + level.removeBlockEntity(pos); + level.setBlock(pos, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL); + //? if >1.20.1 { + /*return state; + *///?} else { + return; + //?} + } + } + super.playerWillDestroy(level, pos, state, player); + //? if >1.20.1 { + /*return state; + *///?} else { + return; + //?} + } + + //? if >1.20.1 { + /*@Override + protected ItemInteractionResult useItemOn(ItemStack pStack, BlockState pState, Level pLevel, BlockPos pPos, Player pPlayer, InteractionHand pHand, BlockHitResult pHitResult) { + BlockEntity entity = pLevel.getBlockEntity(pPos); + if (entity instanceof FossilCleanerBlockEntity fossilCleanerBlockEntity) { + if (!pLevel.isClientSide()) { + if (pStack.is(Items.WATER_BUCKET)) { + long currentAmount = fossilCleanerBlockEntity.getFluid().getAmount(); + if (16000 - currentAmount >= 1000) { + if (!pPlayer.getAbilities().instabuild) { + pPlayer.setItemInHand(pHand, new ItemStack(Items.BUCKET)); + } + // Actual addition is handled in BE tick for simplicity across loaders + return ItemInteractionResult.SUCCESS; + } + } + MenuRegistry.openExtendedMenu((ServerPlayer) pPlayer, fossilCleanerBlockEntity); + } + return ItemInteractionResult.sidedSuccess(pLevel.isClientSide()); + } + return ItemInteractionResult.sidedSuccess(pLevel.isClientSide()); + } + *///?} else { + @Override + public InteractionResult use(BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, BlockHitResult hit) { + BlockEntity entity = level.getBlockEntity(pos); + if (entity instanceof FossilCleanerBlockEntity fossilCleanerBlockEntity) { + if (!level.isClientSide()) { + ItemStack stack = player.getItemInHand(hand); + if (stack.is(Items.WATER_BUCKET)) { + long currentAmount = fossilCleanerBlockEntity.getFluid().getAmount(); + if (16000 - currentAmount >= 1000) { + if (!player.getAbilities().instabuild) { + player.setItemInHand(hand, new ItemStack(Items.BUCKET)); + } + return InteractionResult.SUCCESS; + } + } + MenuRegistry.openExtendedMenu((ServerPlayer) player, fossilCleanerBlockEntity); + } + return InteractionResult.sidedSuccess(level.isClientSide()); + } + return InteractionResult.sidedSuccess(level.isClientSide()); + } + //?} + + @Override + public @Nullable BlockEntityTicker getTicker(Level level, BlockState state, BlockEntityType blockEntityType) { + if (blockEntityType != ModBlockEntities.FOSSIL_CLEANER_BE.get()) return null; + + if (level.isClientSide) { + return null; + } else { + return createTickerHelper(blockEntityType, ModBlockEntities.FOSSIL_CLEANER_BE.get(), + (level1, blockPos, blockState, dnaExtractorBlockEntity) -> dnaExtractorBlockEntity.tick(level1, blockPos, blockState)); + } + } + + @Override + public void animateTick(BlockState state, Level level, BlockPos pos, RandomSource random) { + if (!state.getValue(LIT)) { + return; + } + + double xPos = (double)pos.getX() + 0.5; + double yPos = pos.getY(); + double zPos = (double)pos.getZ() + 0.5; + + Direction direction = state.getValue(FACING).getOpposite(); + Direction.Axis axis = direction.getAxis(); + + double defaultOffset = random.nextDouble() * 0.6 - 0.3; + double xOffsets = axis == Direction.Axis.X ? (double)direction.getStepX() * 0.52 : defaultOffset; + double yOffset = random.nextDouble() * 6.0 / 8.0; + double zOffset = axis == Direction.Axis.Z ? (double)direction.getStepZ() * 0.52 : defaultOffset; + + level.addParticle(ParticleTypes.SMOKE, xPos + xOffsets, yPos + yOffset, zPos + zOffset, 0.0, 0.0, 0.0); + + BlockEntity be = level.getBlockEntity(pos); + if(be instanceof FossilCleanerBlockEntity fossilCleanerBlockEntity && !fossilCleanerBlockEntity.itemHandler.getItem(1).isEmpty()) { + level.addParticle(new ItemParticleOption(ParticleTypes.ITEM, fossilCleanerBlockEntity.itemHandler.getItem(1)), + xPos + xOffsets, yPos + yOffset, zPos + zOffset, 0.0, 0.0, 0.0); + } + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/block/custom/FossilGrinderBlock.java b/common/src/main/java/net/cmr/jurassicrevived/block/custom/FossilGrinderBlock.java new file mode 100644 index 0000000..0f6ee7f --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/block/custom/FossilGrinderBlock.java @@ -0,0 +1,245 @@ +package net.cmr.jurassicrevived.block.custom; + +//? if >1.20.1 { +/*import com.mojang.serialization.MapCodec; +import net.minecraft.core.component.DataComponents; +import net.minecraft.world.item.component.CustomData; +import net.minecraft.world.ItemInteractionResult; +*///?} + +import dev.architectury.registry.menu.MenuRegistry; +import net.cmr.jurassicrevived.block.entity.ModBlockEntities; +import net.cmr.jurassicrevived.block.entity.custom.FossilGrinderBlockEntity; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.particles.ItemParticleOption; +import net.minecraft.core.particles.ParticleTypes; +import net.minecraft.core.registries.Registries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.util.RandomSource; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.context.BlockPlaceContext; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.*; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityTicker; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.StateDefinition; +import net.minecraft.world.level.block.state.properties.BlockStateProperties; +import net.minecraft.world.level.block.state.properties.BooleanProperty; +import net.minecraft.world.level.block.state.properties.DirectionProperty; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.shapes.CollisionContext; +import net.minecraft.world.phys.shapes.Shapes; +import net.minecraft.world.phys.shapes.VoxelShape; +import org.jetbrains.annotations.Nullable; + +public class FossilGrinderBlock extends BaseEntityBlock { + public static final DirectionProperty FACING = BlockStateProperties.HORIZONTAL_FACING; + public static final BooleanProperty LIT = BlockStateProperties.LIT; + + //? if >1.20.1 { + /*public static final MapCodec CODEC = simpleCodec(FossilGrinderBlock::new); + @Override protected MapCodec codec() { return CODEC; } + *///?} + + public FossilGrinderBlock(Properties properties) { + super(properties); + } + + private static final VoxelShape SHAPE_NORTH = Shapes.box( + 1.0 / 16.0, 0.0 / 16.0, 1.0 / 16.0, + 16.0 / 16.0, 15.0 / 16.0, 16.5 / 16.0 + ); + + private static final VoxelShape SHAPE_SOUTH = rotateShapeY(SHAPE_NORTH, 180); + private static final VoxelShape SHAPE_WEST = rotateShapeY(SHAPE_NORTH, -90); + private static final VoxelShape SHAPE_EAST = rotateShapeY(SHAPE_NORTH, 90); + + private static VoxelShape rotateShapeY(VoxelShape shape, int degrees) { + double rad = Math.toRadians(((degrees % 360) + 360) % 360); + int turns = (int) Math.round(rad / (Math.PI / 2)); + turns = ((turns % 4) + 4) % 4; + + VoxelShape current = shape; + for (int i = 0; i < turns; i++) { + VoxelShape[] buffer = new VoxelShape[]{Shapes.empty()}; + current.forAllBoxes((minX, minY, minZ, maxX, maxY, maxZ) -> { + double nMinX = 1.0 - maxZ; + double nMinZ = minX; + double nMaxX = 1.0 - minZ; + double nMaxZ = maxX; + buffer[0] = Shapes.or(buffer[0], Shapes.box(nMinX, minY, nMinZ, nMaxX, maxY, nMaxZ)); + }); + current = buffer[0]; + } + return current; + } + + @Override + public BlockState rotate(BlockState state, Rotation rotation) { + return state.setValue(FACING, rotation.rotate(state.getValue(FACING))); + } + + @Override + public BlockState mirror(BlockState state, Mirror mirror) { + return state.rotate(mirror.getRotation(state.getValue(FACING))); + } + + @Override + public @Nullable BlockState getStateForPlacement(BlockPlaceContext context) { + return this.defaultBlockState().setValue(FACING, context.getHorizontalDirection().getOpposite().getOpposite()).setValue(LIT, false); + } + + @Override + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + builder.add(FACING, LIT); + } + + @Override + public VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) { + Direction dir = state.getValue(FACING); + return switch (dir) { + case NORTH -> SHAPE_NORTH; + case SOUTH -> SHAPE_SOUTH; + case WEST -> SHAPE_WEST; + case EAST -> SHAPE_EAST; + default -> SHAPE_NORTH; + }; + } + + @Override + public @Nullable BlockEntity newBlockEntity(BlockPos blockPos, BlockState blockState) { + return new FossilGrinderBlockEntity(blockPos, blockState); + } + + @Override + public RenderShape getRenderShape(BlockState state) { + return RenderShape.MODEL; + } + + @Override + //? if >1.20.1 { + /*public BlockState playerWillDestroy(Level level, BlockPos pos, BlockState state, Player player) { + *///?} else { + public void playerWillDestroy(Level level, BlockPos pos, BlockState state, Player player) { + //?} + if (!level.isClientSide) { + if (player.getAbilities().instabuild) { + level.removeBlockEntity(pos); + level.setBlock(pos, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL); + //? if >1.20.1 { + /*return state; + *///?} else { + return; + //?} + } + + BlockEntity be = level.getBlockEntity(pos); + if (be instanceof FossilGrinderBlockEntity fbe) { + ItemStack stack = new ItemStack(this.asItem()); + + if (!fbe.isEmptyForDrop()) { + //? if >1.20.1 { + /*CompoundTag tag = fbe.saveWithoutMetadata(level.registryAccess()); + var beTypeKey = level.registryAccess().registryOrThrow(Registries.BLOCK_ENTITY_TYPE).getKey(fbe.getType()); + if (beTypeKey != null) tag.putString("id", beTypeKey.toString()); + stack.set(DataComponents.BLOCK_ENTITY_DATA, CustomData.of(tag)); + *///?} else { + CompoundTag tag = fbe.saveWithoutMetadata(); + stack.getOrCreateTagElement("BlockEntityTag").merge(tag); + //?} + } + + popResource(level, pos, stack); + level.removeBlockEntity(pos); + level.setBlock(pos, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL); + //? if >1.20.1 { + /*return state; + *///?} else { + return; + //?} + } + } + super.playerWillDestroy(level, pos, state, player); + //? if >1.20.1 { + /*return state; + *///?} else { + return; + //?} + } + + @Override + public void onRemove(BlockState pState, Level pLevel, BlockPos pPos, BlockState pNewState, boolean pIsMoving) { + super.onRemove(pState, pLevel, pPos, pNewState, pIsMoving); + } + + //? if >1.20.1 { + /*@Override + protected ItemInteractionResult useItemOn(ItemStack pStack, BlockState pState, Level pLevel, BlockPos pPos, Player pPlayer, InteractionHand pHand, BlockHitResult pHitResult) { + if (!pLevel.isClientSide()) { + BlockEntity entity = pLevel.getBlockEntity(pPos); + if(entity instanceof FossilGrinderBlockEntity fossilGrinderBlockEntity) { + MenuRegistry.openExtendedMenu((ServerPlayer) pPlayer, fossilGrinderBlockEntity); + } + } + return ItemInteractionResult.sidedSuccess(pLevel.isClientSide()); + } + *///?} else { + @Override + public InteractionResult use(BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, BlockHitResult hit) { + if (!level.isClientSide()) { + BlockEntity entity = level.getBlockEntity(pos); + if (entity instanceof FossilGrinderBlockEntity fossilGrinderBlockEntity) { + MenuRegistry.openExtendedMenu((ServerPlayer) player, fossilGrinderBlockEntity); + } + } + return InteractionResult.sidedSuccess(level.isClientSide()); + } + //?} + + @Override + public @Nullable BlockEntityTicker getTicker(Level level, BlockState state, BlockEntityType blockEntityType) { + if (blockEntityType != ModBlockEntities.FOSSIL_GRINDER_BE.get()) return null; + + if (level.isClientSide) { + return null; + } else { + return createTickerHelper(blockEntityType, ModBlockEntities.FOSSIL_GRINDER_BE.get(), + (level1, blockPos, blockState, fossilGrinderBlockEntity) -> fossilGrinderBlockEntity.tick(level1, blockPos, blockState)); + } + } + + @Override + public void animateTick(BlockState state, Level level, BlockPos pos, RandomSource random) { + if (!state.getValue(LIT)) { + return; + } + + double xPos = (double)pos.getX() + 0.5; + double yPos = pos.getY(); + double zPos = (double)pos.getZ() + 0.5; + + Direction direction = state.getValue(FACING).getOpposite(); + Direction.Axis axis = direction.getAxis(); + + double defaultOffset = random.nextDouble() * 0.6 - 0.3; + double xOffsets = axis == Direction.Axis.X ? (double)direction.getStepX() * 0.52 : defaultOffset; + double yOffset = random.nextDouble() * 6.0 / 8.0; + double zOffset = axis == Direction.Axis.Z ? (double)direction.getStepZ() * 0.52 : defaultOffset; + + level.addParticle(ParticleTypes.SMOKE, xPos + xOffsets, yPos + yOffset, zPos + zOffset, 0.0, 0.0, 0.0); + + BlockEntity be = level.getBlockEntity(pos); + if(be instanceof FossilGrinderBlockEntity fossilGrinderBlockEntity && !fossilGrinderBlockEntity.itemHandler.getItem(1).isEmpty()) { + level.addParticle(new ItemParticleOption(ParticleTypes.ITEM, fossilGrinderBlockEntity.itemHandler.getItem(1)), + xPos + xOffsets, yPos + yOffset, zPos + zOffset, 0.0, 0.0, 0.0); + } + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/block/custom/GeneratorBlock.java b/common/src/main/java/net/cmr/jurassicrevived/block/custom/GeneratorBlock.java new file mode 100644 index 0000000..1f5bfbf --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/block/custom/GeneratorBlock.java @@ -0,0 +1,238 @@ +package net.cmr.jurassicrevived.block.custom; + +//? if >1.20.1 { +/*import com.mojang.serialization.MapCodec; +import net.minecraft.core.component.DataComponents; +import net.minecraft.world.item.component.CustomData; +import net.minecraft.world.ItemInteractionResult; +*///?} + +import dev.architectury.registry.menu.MenuRegistry; +import net.cmr.jurassicrevived.block.entity.ModBlockEntities; +import net.cmr.jurassicrevived.block.entity.custom.GeneratorBlockEntity; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.particles.ParticleTypes; +import net.minecraft.core.registries.Registries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.util.RandomSource; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.context.BlockPlaceContext; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.*; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityTicker; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.StateDefinition; +import net.minecraft.world.level.block.state.properties.BlockStateProperties; +import net.minecraft.world.level.block.state.properties.BooleanProperty; +import net.minecraft.world.level.block.state.properties.DirectionProperty; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.shapes.CollisionContext; +import net.minecraft.world.phys.shapes.Shapes; +import net.minecraft.world.phys.shapes.VoxelShape; +import org.jetbrains.annotations.Nullable; + +public class GeneratorBlock extends BaseEntityBlock { + public static final DirectionProperty FACING = BlockStateProperties.HORIZONTAL_FACING; + public static final BooleanProperty LIT = BlockStateProperties.LIT; + + //? if >1.20.1 { + /*public static final MapCodec CODEC = simpleCodec(GeneratorBlock::new); + @Override protected MapCodec codec() { return CODEC; } + *///?} + + public GeneratorBlock(Properties properties) { + super(properties); + } + + private static final VoxelShape SHAPE_NORTH = Shapes.box( + 1.0 / 16.0, 0.0 / 16.0, 1.0 / 16.0, + 15.0 / 16.0, 20.5 / 16.0, 15.0 / 16.0 + ); + + private static final VoxelShape SHAPE_SOUTH = rotateShapeY(SHAPE_NORTH, 180); + private static final VoxelShape SHAPE_WEST = rotateShapeY(SHAPE_NORTH, 90); + private static final VoxelShape SHAPE_EAST = rotateShapeY(SHAPE_NORTH, 90); + + private static VoxelShape rotateShapeY(VoxelShape shape, int degrees) { + double rad = Math.toRadians(((degrees % 360) + 360) % 360); + int turns = (int) Math.round(rad / (Math.PI / 2)); + turns = ((turns % 4) + 4) % 4; + + VoxelShape current = shape; + for (int i = 0; i < turns; i++) { + VoxelShape[] buffer = new VoxelShape[]{Shapes.empty()}; + current.forAllBoxes((minX, minY, minZ, maxX, maxY, maxZ) -> { + double nMinX = 1.0 - maxZ; + double nMinZ = minX; + double nMaxX = 1.0 - minZ; + double nMaxZ = maxX; + buffer[0] = Shapes.or(buffer[0], Shapes.box(nMinX, minY, nMinZ, nMaxX, maxY, nMaxZ)); + }); + current = buffer[0]; + } + return current; + } + + @Override + public BlockState rotate(BlockState state, Rotation rotation) { + return state.setValue(FACING, rotation.rotate(state.getValue(FACING))); + } + + @Override + public BlockState mirror(BlockState state, Mirror mirror) { + return state.rotate(mirror.getRotation(state.getValue(FACING))); + } + + @Override + public @Nullable BlockState getStateForPlacement(BlockPlaceContext context) { + return this.defaultBlockState().setValue(FACING, context.getHorizontalDirection().getOpposite()).setValue(LIT, false); + } + + @Override + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + builder.add(FACING, LIT); + } + + @Override + public VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) { + Direction dir = state.getValue(FACING); + return switch (dir) { + case NORTH -> SHAPE_NORTH; + case SOUTH -> SHAPE_SOUTH; + case WEST -> SHAPE_WEST; + case EAST -> SHAPE_EAST; + default -> SHAPE_NORTH; + }; + } + + @Override + public @Nullable BlockEntity newBlockEntity(BlockPos blockPos, BlockState blockState) { + return new GeneratorBlockEntity(blockPos, blockState); + } + + @Override + public RenderShape getRenderShape(BlockState state) { + return RenderShape.MODEL; + } + + @Override + //? if >1.20.1 { + /*public BlockState playerWillDestroy(Level level, BlockPos pos, BlockState state, Player player) { + *///?} else { + public void playerWillDestroy(Level level, BlockPos pos, BlockState state, Player player) { + //?} + if (!level.isClientSide) { + if (player.getAbilities().instabuild) { + level.removeBlockEntity(pos); + level.setBlock(pos, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL); + //? if >1.20.1 { + /*return state; + *///?} else { + return; + //?} + } + + BlockEntity be = level.getBlockEntity(pos); + if (be instanceof GeneratorBlockEntity fbe) { + ItemStack stack = new ItemStack(this.asItem()); + + if (!fbe.isEmptyForDrop()) { + //? if >1.20.1 { + /*CompoundTag tag = fbe.saveWithoutMetadata(level.registryAccess()); + var beTypeKey = level.registryAccess().registryOrThrow(Registries.BLOCK_ENTITY_TYPE).getKey(fbe.getType()); + if (beTypeKey != null) tag.putString("id", beTypeKey.toString()); + stack.set(DataComponents.BLOCK_ENTITY_DATA, CustomData.of(tag)); + *///?} else { + CompoundTag tag = fbe.saveWithoutMetadata(); + stack.getOrCreateTagElement("BlockEntityTag").merge(tag); + //?} + } + + popResource(level, pos, stack); + level.removeBlockEntity(pos); + level.setBlock(pos, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL); + //? if >1.20.1 { + /*return state; + *///?} else { + return; + //?} + } + } + super.playerWillDestroy(level, pos, state, player); + //? if >1.20.1 { + /*return state; + *///?} else { + return; + //?} + } + + @Override + public void onRemove(BlockState pState, Level pLevel, BlockPos pPos, BlockState pNewState, boolean pIsMoving) { + super.onRemove(pState, pLevel, pPos, pNewState, pIsMoving); + } + + //? if >1.20.1 { + /*@Override + protected ItemInteractionResult useItemOn(ItemStack pStack, BlockState pState, Level pLevel, BlockPos pPos, Player pPlayer, InteractionHand pHand, BlockHitResult pHitResult) { + if (!pLevel.isClientSide()) { + BlockEntity entity = pLevel.getBlockEntity(pPos); + if(entity instanceof GeneratorBlockEntity generatorBlockEntity) { + MenuRegistry.openExtendedMenu((ServerPlayer) pPlayer, generatorBlockEntity); + } + } + return ItemInteractionResult.sidedSuccess(pLevel.isClientSide()); + } + *///?} else { + @Override + public InteractionResult use(BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, BlockHitResult hit) { + if (!level.isClientSide()) { + BlockEntity entity = level.getBlockEntity(pos); + if (entity instanceof GeneratorBlockEntity generatorBlockEntity) { + MenuRegistry.openExtendedMenu((ServerPlayer) player, generatorBlockEntity); + } + } + return InteractionResult.sidedSuccess(level.isClientSide()); + } + //?} + + @Override + public @Nullable BlockEntityTicker getTicker(Level level, BlockState state, BlockEntityType blockEntityType) { + if (blockEntityType != ModBlockEntities.GENERATOR_BE.get()) return null; + + if (level.isClientSide) { + return null; + } else { + return createTickerHelper(blockEntityType, ModBlockEntities.GENERATOR_BE.get(), + (level1, blockPos, blockState, generatorBlockEntity) -> generatorBlockEntity.tick(level1, blockPos, blockState)); + } + } + + @Override + public void animateTick(BlockState state, Level level, BlockPos pos, RandomSource random) { + if (!state.getValue(LIT)) { + return; + } + + double xPos = (double)pos.getX() + 0.5; + double yPos = pos.getY(); + double zPos = (double)pos.getZ() + 0.5; + + Direction direction = state.getValue(FACING).getOpposite(); + Direction.Axis axis = direction.getAxis(); + + double defaultOffset = random.nextDouble() * 0.6 - 0.3; + double xOffsets = axis == Direction.Axis.X ? (double)direction.getStepX() * 0.52 : defaultOffset; + double yOffset = random.nextDouble() * 6.0 / 8.0; + double zOffset = axis == Direction.Axis.Z ? (double)direction.getStepZ() * 0.52 : defaultOffset; + + level.addParticle(ParticleTypes.SMOKE, xPos + xOffsets, yPos + yOffset, zPos + zOffset, 0.0, 0.0, 0.0); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/block/custom/IncubatedEggBlock.java b/common/src/main/java/net/cmr/jurassicrevived/block/custom/IncubatedEggBlock.java new file mode 100644 index 0000000..0436c8e --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/block/custom/IncubatedEggBlock.java @@ -0,0 +1,128 @@ +package net.cmr.jurassicrevived.block.custom; + +import net.cmr.jurassicrevived.block.entity.custom.EggBlockEntity; +import net.minecraft.core.BlockPos; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.util.RandomSource; +import net.minecraft.world.entity.AgeableMob; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.Mob; +import net.minecraft.world.entity.MobSpawnType; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.TooltipFlag; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.EntityBlock; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.Vec3; +import net.minecraft.world.phys.shapes.CollisionContext; +import net.minecraft.world.phys.shapes.VoxelShape; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.function.Supplier; + +public class IncubatedEggBlock extends Block implements EntityBlock { + + private final Supplier> toSpawn; + private final int hatchSeconds = 60; + + public IncubatedEggBlock(Properties pProperties, Supplier> toSpawn) { + super(pProperties); + this.toSpawn = toSpawn; + } + + private static final VoxelShape EGG_SHAPE = Block.box(6.5D, 0.0D, 6.5D, 9.5D, 4.0D, 9.5D); + + @Override + public VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) { + return EGG_SHAPE; + } + + @Override + public VoxelShape getCollisionShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) { + return EGG_SHAPE; + } + + @Override + public void onPlace(BlockState state, Level level, BlockPos pos, BlockState oldState, boolean isMoving) { + super.onPlace(state, level, pos, oldState, isMoving); + if (!level.isClientSide) { + BlockEntity be = level.getBlockEntity(pos); + if (be instanceof EggBlockEntity eggBE) { + eggBE.resetForNewPlacement(level, hatchSeconds); + } + level.scheduleTick(pos, this, hatchSeconds * 20); + } + } + + @Override + public void tick(BlockState state, ServerLevel level, BlockPos pos, RandomSource random) { + BlockEntity be = level.getBlockEntity(pos); + if (!(be instanceof EggBlockEntity eggBE)) return; + if (eggBE.getSecondsRemaining(level) <= 0) { + super.tick(state, level, pos, random); + } else { + level.scheduleTick(pos, this, eggBE.getSecondsRemaining(level) * 20); + return; + } + + EntityType type = toSpawn.get(); + if (type != null) { + Mob mob = type.create(level); + if (mob != null) { + Vec3 spawn = Vec3.atCenterOf(pos); + mob.moveTo(spawn.x, spawn.y + 0.1, spawn.z, level.random.nextFloat() * 360F, 0.0F); + + //? if >1.20.1 { + /*mob.finalizeSpawn(level, level.getCurrentDifficultyAt(pos), MobSpawnType.TRIGGERED, null); + *///?} else { + mob.finalizeSpawn(level, level.getCurrentDifficultyAt(pos), MobSpawnType.TRIGGERED, null, null); + //?} + + if (mob instanceof AgeableMob ageable) { + ageable.setBaby(true); + } + + level.addFreshEntity(mob); + } + } + level.levelEvent(2001, pos, Block.getId(state)); + level.removeBlock(pos, false); + } + + @Override + public void onRemove(BlockState state, Level level, BlockPos pos, BlockState newState, boolean isMoving) { + if (!level.isClientSide && state.getBlock() != newState.getBlock()) { + BlockEntity be = level.getBlockEntity(pos); + if (be instanceof EggBlockEntity eggBE) { + eggBE.invalidateTimer(); + } + } + super.onRemove(state, level, pos, newState, isMoving); + } + + @Nullable + @Override + public BlockEntity newBlockEntity(BlockPos pos, BlockState state) { + return new EggBlockEntity(pos, state); + } + + //? if >1.20.1 { + /*@Override + public void appendHoverText(ItemStack pStack, Item.TooltipContext pContext, List pTootipComponents, TooltipFlag pTooltipFlag) { + pTootipComponents.add(Component.translatable("tooltip.jurassicrevived.egg.hatches_in", hatchSeconds)); + super.appendHoverText(pStack, pContext, pTootipComponents, pTooltipFlag); + } + *///?} else { + @Override + public void appendHoverText(ItemStack pStack, @Nullable BlockGetter level, List pTootipComponents, TooltipFlag pTooltipFlag) { + pTootipComponents.add(Component.translatable("tooltip.jurassicrevived.egg.hatches_in", hatchSeconds)); + super.appendHoverText(pStack, level, pTootipComponents, pTooltipFlag); + } + //?} +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/block/custom/IncubatorBlock.java b/common/src/main/java/net/cmr/jurassicrevived/block/custom/IncubatorBlock.java new file mode 100644 index 0000000..a889b69 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/block/custom/IncubatorBlock.java @@ -0,0 +1,213 @@ +package net.cmr.jurassicrevived.block.custom; + +//? if >1.20.1 { +/*import com.mojang.serialization.MapCodec; +import net.minecraft.core.component.DataComponents; +import net.minecraft.world.item.component.CustomData; +import net.minecraft.world.ItemInteractionResult; +*///?} + +import dev.architectury.registry.menu.MenuRegistry; +import net.cmr.jurassicrevived.block.entity.ModBlockEntities; +import net.cmr.jurassicrevived.block.entity.custom.IncubatorBlockEntity; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.particles.ParticleTypes; +import net.minecraft.core.registries.Registries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.util.RandomSource; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.context.BlockPlaceContext; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.*; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityTicker; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.StateDefinition; +import net.minecraft.world.level.block.state.properties.BlockStateProperties; +import net.minecraft.world.level.block.state.properties.BooleanProperty; +import net.minecraft.world.level.block.state.properties.DirectionProperty; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.shapes.CollisionContext; +import net.minecraft.world.phys.shapes.Shapes; +import net.minecraft.world.phys.shapes.VoxelShape; +import org.jetbrains.annotations.Nullable; + +public class IncubatorBlock extends BaseEntityBlock { + public static final DirectionProperty FACING = BlockStateProperties.HORIZONTAL_FACING; + public static final BooleanProperty LIT = BlockStateProperties.LIT; + + //? if >1.20.1 { + /*public static final MapCodec CODEC = simpleCodec(IncubatorBlock::new); + @Override protected MapCodec codec() { return CODEC; } + *///?} + + public IncubatorBlock(Properties properties) { + super(properties); + } + + private static final VoxelShape SHAPE = Shapes.box( + 0.0 / 16.0, 0.0 / 16.0, 0.0 / 16.0, + 16.0 / 16.0, 23.0 / 16.0, 16.0 / 16.0 + ); + + private static final VoxelShape SHAPE_LIT = Shapes.box( + 0.0 / 16.0, 0.0 / 16.0, 0.0 / 16.0, + 16.0 / 16.0, 18.0 / 16.0, 16.0 / 16.0 + ); + + @Override + public BlockState rotate(BlockState state, Rotation rotation) { + return state.setValue(FACING, rotation.rotate(state.getValue(FACING))); + } + + @Override + public BlockState mirror(BlockState state, Mirror mirror) { + return state.rotate(mirror.getRotation(state.getValue(FACING))); + } + + @Override + public @Nullable BlockState getStateForPlacement(BlockPlaceContext context) { + return this.defaultBlockState().setValue(FACING, context.getHorizontalDirection().getOpposite()).setValue(LIT, false); + } + + @Override + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + builder.add(FACING, LIT); + } + + @Override + public VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) { + boolean lit = state.getValue(LIT); + return lit ? SHAPE_LIT : SHAPE; + } + + @Override + public @Nullable BlockEntity newBlockEntity(BlockPos blockPos, BlockState blockState) { + return new IncubatorBlockEntity(blockPos, blockState); + } + + @Override + public RenderShape getRenderShape(BlockState state) { + return RenderShape.MODEL; + } + + @Override + //? if >1.20.1 { + /*public BlockState playerWillDestroy(Level level, BlockPos pos, BlockState state, Player player) { + *///?} else { + public void playerWillDestroy(Level level, BlockPos pos, BlockState state, Player player) { + //?} + if (!level.isClientSide) { + if (player.getAbilities().instabuild) { + level.removeBlockEntity(pos); + level.setBlock(pos, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL); + //? if >1.20.1 { + /*return state; + *///?} else { + return; + //?} + } + + BlockEntity be = level.getBlockEntity(pos); + if (be instanceof IncubatorBlockEntity fbe) { + ItemStack stack = new ItemStack(this.asItem()); + + if (!fbe.isEmptyForDrop()) { + //? if >1.20.1 { + /*CompoundTag tag = fbe.saveWithoutMetadata(level.registryAccess()); + var beTypeKey = level.registryAccess().registryOrThrow(Registries.BLOCK_ENTITY_TYPE).getKey(fbe.getType()); + if (beTypeKey != null) tag.putString("id", beTypeKey.toString()); + stack.set(DataComponents.BLOCK_ENTITY_DATA, CustomData.of(tag)); + *///?} else { + CompoundTag tag = fbe.saveWithoutMetadata(); + stack.getOrCreateTagElement("BlockEntityTag").merge(tag); + //?} + } + + popResource(level, pos, stack); + level.removeBlockEntity(pos); + level.setBlock(pos, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL); + //? if >1.20.1 { + /*return state; + *///?} else { + return; + //?} + } + } + super.playerWillDestroy(level, pos, state, player); + //? if >1.20.1 { + /*return state; + *///?} else { + return; + //?} + } + + @Override + public void onRemove(BlockState pState, Level pLevel, BlockPos pPos, BlockState pNewState, boolean pIsMoving) { + super.onRemove(pState, pLevel, pPos, pNewState, pIsMoving); + } + + //? if >1.20.1 { + /*@Override + protected ItemInteractionResult useItemOn(ItemStack pStack, BlockState pState, Level pLevel, BlockPos pPos, Player pPlayer, InteractionHand pHand, BlockHitResult pHitResult) { + if (!pLevel.isClientSide()) { + BlockEntity entity = pLevel.getBlockEntity(pPos); + if(entity instanceof IncubatorBlockEntity incubatorBlockEntity) { + MenuRegistry.openExtendedMenu((ServerPlayer) pPlayer, incubatorBlockEntity); + } + } + return ItemInteractionResult.sidedSuccess(pLevel.isClientSide()); + } + *///?} else { + @Override + public InteractionResult use(BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, BlockHitResult hit) { + if (!level.isClientSide()) { + BlockEntity entity = level.getBlockEntity(pos); + if (entity instanceof IncubatorBlockEntity incubatorBlockEntity) { + MenuRegistry.openExtendedMenu((ServerPlayer) player, incubatorBlockEntity); + } + } + return InteractionResult.sidedSuccess(level.isClientSide()); + } + //?} + + @Override + public @Nullable BlockEntityTicker getTicker(Level level, BlockState state, BlockEntityType blockEntityType) { + if (blockEntityType != ModBlockEntities.INCUBATOR_BE.get()) return null; + + if (level.isClientSide) { + return null; + } else { + return createTickerHelper(blockEntityType, ModBlockEntities.INCUBATOR_BE.get(), + (level1, blockPos, blockState, incubatorBlockEntity) -> incubatorBlockEntity.tick(level1, blockPos, blockState)); + } + } + + @Override + public void animateTick(BlockState state, Level level, BlockPos pos, RandomSource random) { + if (!state.getValue(LIT)) { + return; + } + + double xPos = (double)pos.getX() + 0.5; + double yPos = pos.getY(); + double zPos = (double)pos.getZ() + 0.5; + + Direction direction = state.getValue(FACING).getOpposite(); + Direction.Axis axis = direction.getAxis(); + + double defaultOffset = random.nextDouble() * 0.6 - 0.3; + double xOffsets = axis == Direction.Axis.X ? (double)direction.getStepX() * 0.52 : defaultOffset; + double yOffset = random.nextDouble() * 6.0 / 8.0; + double zOffset = axis == Direction.Axis.Z ? (double)direction.getStepZ() * 0.52 : defaultOffset; + + level.addParticle(ParticleTypes.SMOKE, xPos + xOffsets, yPos + yOffset, zPos + zOffset, 0.0, 0.0, 0.0); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/block/custom/LightPostBlock.java b/common/src/main/java/net/cmr/jurassicrevived/block/custom/LightPostBlock.java new file mode 100644 index 0000000..da71798 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/block/custom/LightPostBlock.java @@ -0,0 +1,55 @@ +package net.cmr.jurassicrevived.block.custom; + +import com.mojang.serialization.MapCodec; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.item.context.BlockPlaceContext; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.HorizontalDirectionalBlock; +import net.minecraft.world.level.block.Mirror; +import net.minecraft.world.level.block.Rotation; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.StateDefinition; +import net.minecraft.world.level.block.state.properties.Property; +import net.minecraft.world.phys.shapes.CollisionContext; +import net.minecraft.world.phys.shapes.VoxelShape; + +public class LightPostBlock extends HorizontalDirectionalBlock { + + public static final VoxelShape SHAPE = Block.box(3.5D, 0.0D, 3.5D, 12.5D, 16.0D, 12.5D); + + public LightPostBlock(Properties properties) { + super(properties); + this.registerDefaultState((BlockState)((BlockState)((BlockState)this.stateDefinition.any()).setValue(FACING, Direction.SOUTH))); + } + + protected MapCodec codec() { + return null; + } + + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + builder.add(new Property[]{FACING}); + } + + public BlockState mirror(BlockState state, Mirror mirror) { + return state.rotate(mirror.getRotation((Direction)state.getValue(FACING))); + } + + // Ensure rotation of FACING property when the block is rotated (e.g., structure placement, commands, etc.) + @Override + public BlockState rotate(BlockState state, Rotation rotation) { + return state.setValue(FACING, rotation.rotate(state.getValue(FACING))); + } + + public BlockState getStateForPlacement(BlockPlaceContext context) { + // Ensure we always use a horizontal facing (N/E/S/W), even when looking up or down. + return (BlockState)this.defaultBlockState().setValue(FACING, context.getHorizontalDirection().getOpposite()); + } + + @Override + public VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) { + Direction facing = state.getValue(FACING); + return SHAPE; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/block/custom/PipeBlock.java b/common/src/main/java/net/cmr/jurassicrevived/block/custom/PipeBlock.java new file mode 100644 index 0000000..26ad7a4 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/block/custom/PipeBlock.java @@ -0,0 +1,365 @@ +package net.cmr.jurassicrevived.block.custom; + +import net.cmr.jurassicrevived.block.entity.ModBlockEntities; +import net.cmr.jurassicrevived.block.entity.custom.PipeBlockEntity; +import net.cmr.jurassicrevived.block.entity.energy.ModEnergyUtil; +import net.cmr.jurassicrevived.config.JRConfigManager; +import net.cmr.jurassicrevived.item.ModItems; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.util.Mth; +import net.minecraft.util.StringRepresentable; +import net.minecraft.world.Container; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +//? if >1.20.1 { +/*import net.minecraft.world.ItemInteractionResult; + *///?} +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.context.BlockPlaceContext; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.LevelAccessor; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.EntityBlock; +import net.minecraft.world.level.block.RenderShape; +import net.minecraft.world.level.block.SimpleWaterloggedBlock; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityTicker; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.StateDefinition; +import net.minecraft.world.level.block.state.properties.BlockStateProperties; +import net.minecraft.world.level.block.state.properties.BooleanProperty; +import net.minecraft.world.level.block.state.properties.EnumProperty; +import net.minecraft.world.level.material.FluidState; +import net.minecraft.world.level.material.Fluids; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.Vec3; +import net.minecraft.world.phys.shapes.CollisionContext; +import net.minecraft.world.phys.shapes.Shapes; +import net.minecraft.world.phys.shapes.VoxelShape; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public class PipeBlock extends Block implements EntityBlock, SimpleWaterloggedBlock { + + public enum Transport { + ITEMS, FLUIDS, ENERGY + } + + public enum ConnectionType implements StringRepresentable { + NONE, PIPE, CONNECTOR, CONNECTOR_PULL; + + @Override + public String getSerializedName() { + return switch (this) { + case NONE -> "none"; + case PIPE -> "pipe"; + case CONNECTOR -> "connector"; + case CONNECTOR_PULL -> "connector_pull"; + }; + } + } + + public static final EnumProperty DOWN = EnumProperty.create("down", ConnectionType.class); + public static final EnumProperty UP = EnumProperty.create("up", ConnectionType.class); + public static final EnumProperty NORTH = EnumProperty.create("north", ConnectionType.class); + public static final EnumProperty SOUTH = EnumProperty.create("south", ConnectionType.class); + public static final EnumProperty WEST = EnumProperty.create("west", ConnectionType.class); + public static final EnumProperty EAST = EnumProperty.create("east", ConnectionType.class); + public static final BooleanProperty WATERLOGGED = BlockStateProperties.WATERLOGGED; + + private final Transport transport; + + private static final VoxelShape CORE = box(6, 6, 6, 10, 10, 10); + private static final VoxelShape ARM_DOWN = box(6, 0, 6, 10, 6, 10); + private static final VoxelShape ARM_UP = box(6, 10, 6, 10, 16, 10); + private static final VoxelShape ARM_NORTH = box(6, 6, 0, 10, 10, 6); + private static final VoxelShape ARM_SOUTH = box(6, 6, 10, 10, 10, 16); + private static final VoxelShape ARM_WEST = box(0, 6, 6, 6, 10, 10); + private static final VoxelShape ARM_EAST = box(10, 6, 6, 16, 10, 10); + private static final VoxelShape CAP_DOWN = box(6, 0, 6, 10, 1, 10); + private static final VoxelShape CAP_UP = box(6, 15, 6, 10, 16, 10); + private static final VoxelShape CAP_NORTH = box(6, 6, 0, 10, 10, 1); + private static final VoxelShape CAP_SOUTH = box(6, 6, 15, 10, 10, 16); + private static final VoxelShape CAP_WEST = box(0, 6, 6, 1, 10, 10); + private static final VoxelShape CAP_EAST = box(15, 6, 6, 16, 10, 10); + + private static final VoxelShape[] SHAPES = new VoxelShape[64]; + + static { + for (int mask = 0; mask < SHAPES.length; mask++) { + VoxelShape s = CORE; + if ((mask & (1 << 0)) != 0) s = Shapes.or(s, ARM_DOWN, CAP_DOWN); + if ((mask & (1 << 1)) != 0) s = Shapes.or(s, ARM_UP, CAP_UP); + if ((mask & (1 << 2)) != 0) s = Shapes.or(s, ARM_NORTH, CAP_NORTH); + if ((mask & (1 << 3)) != 0) s = Shapes.or(s, ARM_SOUTH, CAP_SOUTH); + if ((mask & (1 << 4)) != 0) s = Shapes.or(s, ARM_WEST, CAP_WEST); + if ((mask & (1 << 5)) != 0) s = Shapes.or(s, ARM_EAST, CAP_EAST); + SHAPES[mask] = s; + } + } + + public PipeBlock(Properties properties, Transport transport) { + super(properties.noOcclusion()); + this.transport = transport; + this.registerDefaultState( + this.stateDefinition.any() + .setValue(DOWN, ConnectionType.NONE) + .setValue(UP, ConnectionType.NONE) + .setValue(NORTH, ConnectionType.NONE) + .setValue(SOUTH, ConnectionType.NONE) + .setValue(WEST, ConnectionType.NONE) + .setValue(EAST, ConnectionType.NONE) + .setValue(WATERLOGGED, false) + ); + } + + public Transport getTransport() { + return transport; + } + + @Override + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + builder.add(DOWN, UP, NORTH, SOUTH, WEST, EAST, WATERLOGGED); + } + + @Nullable + @Override + public BlockState getStateForPlacement(BlockPlaceContext ctx) { + Level level = ctx.getLevel(); + BlockPos pos = ctx.getClickedPos(); + BlockState state = this.defaultBlockState() + .setValue(WATERLOGGED, level.getFluidState(pos).getType() == Fluids.WATER); + for (Direction dir : Direction.values()) { + state = setConnectionForDirection(level, pos, state, dir); + } + return state; + } + + @Override + @SuppressWarnings("deprecation") + public BlockState updateShape(BlockState state, Direction direction, BlockState neighborState, LevelAccessor level, BlockPos pos, BlockPos neighborPos) { + if (state.getValue(WATERLOGGED)) { + level.scheduleTick(pos, Fluids.WATER, Fluids.WATER.getTickDelay(level)); + } + return setConnectionForDirection(level, pos, state, direction); + } + + @Override + public FluidState getFluidState(BlockState state) { + return state.getValue(WATERLOGGED) ? Fluids.WATER.getSource(false) : super.getFluidState(state); + } + + @Override + public @Nullable BlockEntity newBlockEntity(BlockPos pos, BlockState state) { + return new PipeBlockEntity(pos, state); + } + + @Override + public @Nullable BlockEntityTicker getTicker(Level level, BlockState state, BlockEntityType type) { + if (level.isClientSide) return null; + return (lvl, pos, st, be) -> { + if (be instanceof PipeBlockEntity pbe) { + PipeBlockEntity.serverTick(lvl, pos, st, pbe); + } + }; + } + + private BlockState setConnectionForDirection(LevelAccessor level, BlockPos pos, BlockState state, Direction dir) { + EnumProperty prop = getProp(dir); + ConnectionType existing = state.getValue(prop); + ConnectionType connection = determineConnection(level, pos, dir, existing); + return state.setValue(prop, connection); + } + + private ConnectionType determineConnection(LevelAccessor level, BlockPos pos, Direction dir, ConnectionType existing) { + BlockPos neighborPos = pos.relative(dir); + BlockState neighborState = level.getBlockState(neighborPos); + Block neighbor = neighborState.getBlock(); + + if (neighbor instanceof PipeBlock otherPipe) { + if (otherPipe.transport == this.transport) { + return ConnectionType.PIPE; + } + } + + BlockEntity be = level.getBlockEntity(neighborPos); + boolean hasCommonConnection = false; + + if (be != null) { + hasCommonConnection = switch (this.transport) { + case ITEMS -> be instanceof Container; + case FLUIDS -> false; + case ENERGY -> be instanceof ModEnergyUtil.EnergyProvider; + }; + } + + if (hasCommonConnection) { + if (existing == ConnectionType.CONNECTOR_PULL) { + return ConnectionType.CONNECTOR_PULL; + } + return ConnectionType.CONNECTOR; + } + + return ConnectionType.NONE; + } + + public static EnumProperty getProp(Direction dir) { + return switch (dir) { + case DOWN -> DOWN; + case UP -> UP; + case NORTH -> NORTH; + case SOUTH -> SOUTH; + case WEST -> WEST; + case EAST -> EAST; + }; + } + + //? if >1.20.1 { + /*@Override + protected ItemInteractionResult useItemOn(ItemStack stack, BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, BlockHitResult hit) { + if (stack.is(ModItems.WRENCH.get())) { + BlockState newState = toggleConnection(state, hit, pos); + if (newState == null) return ItemInteractionResult.PASS_TO_DEFAULT_BLOCK_INTERACTION; + + if (!level.isClientSide) { + level.setBlock(pos, newState, Block.UPDATE_ALL); + level.sendBlockUpdated(pos, state, newState, Block.UPDATE_ALL); + } + return ItemInteractionResult.sidedSuccess(level.isClientSide); + } + return super.useItemOn(stack, state, level, pos, player, hand, hit); + } + *///?} else { + @Override + public InteractionResult use(BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, BlockHitResult hit) { + ItemStack stack = player.getItemInHand(hand); + if (stack.is(ModItems.WRENCH.get())) { + BlockState newState = toggleConnection(state, hit, pos); + if (newState == null) return InteractionResult.PASS; + + if (!level.isClientSide) { + level.setBlock(pos, newState, Block.UPDATE_ALL); + level.sendBlockUpdated(pos, state, newState, Block.UPDATE_ALL); + } + return InteractionResult.sidedSuccess(level.isClientSide); + } + return super.use(state, level, pos, player, hand, hit); + } + //?} + + private @Nullable BlockState toggleConnection(BlockState state, BlockHitResult hit, BlockPos pos) { + Direction target = getInteractionFace(state, hit, pos); + if (target == null) return null; + + EnumProperty prop = getProp(target); + ConnectionType val = state.getValue(prop); + + if (val == ConnectionType.CONNECTOR) { + return state.setValue(prop, ConnectionType.CONNECTOR_PULL); + } else if (val == ConnectionType.CONNECTOR_PULL) { + return state.setValue(prop, ConnectionType.CONNECTOR); + } else { + Direction nearest = nearestConnectedDirection(state, hit, pos); + if (nearest == null) return null; + prop = getProp(nearest); + val = state.getValue(prop); + if (val == ConnectionType.CONNECTOR) { + return state.setValue(prop, ConnectionType.CONNECTOR_PULL); + } else if (val == ConnectionType.CONNECTOR_PULL) { + return state.setValue(prop, ConnectionType.CONNECTOR); + } + } + return null; + } + + private Direction getInteractionFace(BlockState state, BlockHitResult hit, BlockPos pos) { + Vec3 local = hit.getLocation().subtract(pos.getX(), pos.getY(), pos.getZ()); + double x = Mth.clamp(local.x * 16.0, 0.0, 16.0); + double y = Mth.clamp(local.y * 16.0, 0.0, 16.0); + double z = Mth.clamp(local.z * 16.0, 0.0, 16.0); + + if (intersects(CAP_UP, x, y, z) || intersects(ARM_UP, x, y, z)) { if (state.getValue(UP) != ConnectionType.NONE) return Direction.UP; } + if (intersects(CAP_DOWN, x, y, z) || intersects(ARM_DOWN, x, y, z)) { if (state.getValue(DOWN) != ConnectionType.NONE) return Direction.DOWN; } + if (intersects(CAP_NORTH, x, y, z) || intersects(ARM_NORTH, x, y, z)) { if (state.getValue(NORTH) != ConnectionType.NONE) return Direction.NORTH; } + if (intersects(CAP_SOUTH, x, y, z) || intersects(ARM_SOUTH, x, y, z)) { if (state.getValue(SOUTH) != ConnectionType.NONE) return Direction.SOUTH; } + if (intersects(CAP_WEST, x, y, z) || intersects(ARM_WEST, x, y, z)) { if (state.getValue(WEST) != ConnectionType.NONE) return Direction.WEST; } + if (intersects(CAP_EAST, x, y, z) || intersects(ARM_EAST, x, y, z)) { if (state.getValue(EAST) != ConnectionType.NONE) return Direction.EAST; } + + return nearestConnectedDirection(state, x, y, z); + } + + private static Direction nearestConnectedDirection(BlockState state, BlockHitResult hit, BlockPos pos) { + Vec3 local = hit.getLocation().subtract(pos.getX(), pos.getY(), pos.getZ()); + double x = Mth.clamp(local.x * 16.0, 0.0, 16.0); + double y = Mth.clamp(local.y * 16.0, 0.0, 16.0); + double z = Mth.clamp(local.z * 16.0, 0.0, 16.0); + return nearestConnectedDirection(state, x, y, z); + } + + private static Direction nearestConnectedDirection(BlockState state, double x, double y, double z) { + Direction best = null; + double bestScore = Double.NEGATIVE_INFINITY; + + for (Direction d : Direction.values()) { + ConnectionType t = state.getValue(getProp(d)); + if (t == ConnectionType.NONE) continue; + + double score = switch (d) { + case UP -> y; + case DOWN -> 16 - y; + case NORTH -> 16 - z; + case SOUTH -> z; + case WEST -> 16 - x; + case EAST -> x; + }; + if (score > bestScore) { + bestScore = score; + best = d; + } + } + return best; + } + + private static boolean intersects(VoxelShape shape, double x, double y, double z) { + var it = shape.toAabbs().iterator(); + if (!it.hasNext()) return false; + var bb = it.next(); + return x >= bb.minX && x <= bb.maxX && + y >= bb.minY && y <= bb.maxY && + z >= bb.minZ && z <= bb.maxZ; + } + + @Override + public RenderShape getRenderShape(BlockState state) { + return RenderShape.MODEL; + } + + @Override + public VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext ctx) { + int mask = 0; + if (state.getValue(DOWN) != ConnectionType.NONE) mask |= (1 << 0); + if (state.getValue(UP) != ConnectionType.NONE) mask |= (1 << 1); + if (state.getValue(NORTH) != ConnectionType.NONE) mask |= (1 << 2); + if (state.getValue(SOUTH) != ConnectionType.NONE) mask |= (1 << 3); + if (state.getValue(WEST) != ConnectionType.NONE) mask |= (1 << 4); + if (state.getValue(EAST) != ConnectionType.NONE) mask |= (1 << 5); + return SHAPES[mask]; + } + + public int getMaxItemsPerTick() { + return Math.max(0, JRConfigManager.get().itemsPerSecond / 20); + } + + public int getMaxFluidPerTick() { + return Math.max(0, JRConfigManager.get().milliBucketsPerSecond / 20); + } + + public int getMaxEnergyPerTick() { + return Math.max(0, JRConfigManager.get().fePerSecond / 20); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/block/custom/PowerCellBlock.java b/common/src/main/java/net/cmr/jurassicrevived/block/custom/PowerCellBlock.java new file mode 100644 index 0000000..92d9a8e --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/block/custom/PowerCellBlock.java @@ -0,0 +1,147 @@ +package net.cmr.jurassicrevived.block.custom; + +//? if >1.20.1 { +/*import com.mojang.serialization.MapCodec; +import net.minecraft.core.component.DataComponents; +import net.minecraft.world.item.component.CustomData; +import net.minecraft.world.ItemInteractionResult; +*///?} + +import dev.architectury.registry.menu.MenuRegistry; +import net.cmr.jurassicrevived.block.entity.ModBlockEntities; +import net.cmr.jurassicrevived.block.entity.custom.PowerCellBlockEntity; +import net.minecraft.core.BlockPos; +import net.minecraft.core.registries.Registries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.BaseEntityBlock; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.RenderShape; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityTicker; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.shapes.VoxelShape; +import org.jetbrains.annotations.Nullable; + +public class PowerCellBlock extends BaseEntityBlock { + public PowerCellBlock(Properties properties) { + super(properties); + } + + public static final VoxelShape SHAPE = Block.box(2.0D, 0.0D, 2.0D, 14.0D, 15.0D, 14.0D); + + //? if >1.20.1 { + /*public static final MapCodec CODEC = simpleCodec(PowerCellBlock::new); + @Override protected MapCodec codec() { return CODEC; } + *///?} + + @Override + public RenderShape getRenderShape(BlockState pState) { + return RenderShape.MODEL; + } + + @Override + //? if >1.20.1 { + /*public BlockState playerWillDestroy(Level level, BlockPos pos, BlockState state, Player player) { + *///?} else { + public void playerWillDestroy(Level level, BlockPos pos, BlockState state, Player player) { + //?} + if (!level.isClientSide) { + if (player.getAbilities().instabuild) { + level.removeBlockEntity(pos); + level.setBlock(pos, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL); + //? if >1.20.1 { + /*return state; + *///?} else { + return; + //?} + } + + BlockEntity be = level.getBlockEntity(pos); + if (be instanceof PowerCellBlockEntity fbe) { + ItemStack stack = new ItemStack(this.asItem()); + + if (!fbe.isEmptyForDrop()) { + //? if >1.20.1 { + /*CompoundTag tag = fbe.saveWithoutMetadata(level.registryAccess()); + var beTypeKey = level.registryAccess().registryOrThrow(Registries.BLOCK_ENTITY_TYPE).getKey(fbe.getType()); + if (beTypeKey != null) tag.putString("id", beTypeKey.toString()); + stack.set(DataComponents.BLOCK_ENTITY_DATA, CustomData.of(tag)); + *///?} else { + CompoundTag tag = fbe.saveWithoutMetadata(); + stack.getOrCreateTagElement("BlockEntityTag").merge(tag); + //?} + } + + popResource(level, pos, stack); + level.removeBlockEntity(pos); + level.setBlock(pos, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL); + //? if >1.20.1 { + /*return state; + *///?} else { + return; + //?} + } + } + super.playerWillDestroy(level, pos, state, player); + //? if >1.20.1 { + /*return state; + *///?} else { + return; + //?} + } + + @Override + public void onRemove(BlockState pState, Level pLevel, BlockPos pPos, BlockState pNewState, boolean pIsMoving) { + super.onRemove(pState, pLevel, pPos, pNewState, pIsMoving); + } + + //? if >1.20.1 { + /*@Override + protected ItemInteractionResult useItemOn(ItemStack pStack, BlockState pState, Level pLevel, BlockPos pPos, Player pPlayer, InteractionHand pHand, BlockHitResult pHitResult) { + if (!pLevel.isClientSide()) { + BlockEntity entity = pLevel.getBlockEntity(pPos); + if (entity instanceof PowerCellBlockEntity powerCellBlockEntity) { + MenuRegistry.openExtendedMenu((ServerPlayer) pPlayer, powerCellBlockEntity); + } + } + return ItemInteractionResult.sidedSuccess(pLevel.isClientSide()); + } + *///?} else { + @Override + public InteractionResult use(BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, BlockHitResult hit) { + if (!level.isClientSide()) { + BlockEntity entity = level.getBlockEntity(pos); + if (entity instanceof PowerCellBlockEntity powerCellBlockEntity) { + MenuRegistry.openExtendedMenu((ServerPlayer) player, powerCellBlockEntity); + } + } + return InteractionResult.sidedSuccess(level.isClientSide()); + } + //?} + + @Nullable + @Override + public BlockEntity newBlockEntity(BlockPos blockPos, BlockState blockState) { + return new PowerCellBlockEntity(blockPos, blockState); + } + + @Nullable + @Override + public BlockEntityTicker getTicker(Level pLevel, BlockState pState, BlockEntityType pBlockEntityType) { + if (pLevel.isClientSide()) { + return null; + } + + return createTickerHelper(pBlockEntityType, ModBlockEntities.POWER_CELL_BE.get(), + ((level, blockPos, blockState, powerCellBlockEntity) -> powerCellBlockEntity.tick(level, blockPos, blockState))); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/block/custom/RotatableBlock.java b/common/src/main/java/net/cmr/jurassicrevived/block/custom/RotatableBlock.java new file mode 100644 index 0000000..19225b0 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/block/custom/RotatableBlock.java @@ -0,0 +1,39 @@ +package net.cmr.jurassicrevived.block.custom; + +import com.mojang.serialization.MapCodec; +import net.minecraft.core.Direction; +import net.minecraft.world.item.context.BlockPlaceContext; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.DirectionalBlock; +import net.minecraft.world.level.block.Mirror; +import net.minecraft.world.level.block.Rotation; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.StateDefinition; +import net.minecraft.world.level.block.state.properties.Property; + +public class RotatableBlock extends DirectionalBlock { + public RotatableBlock(Properties properties) { + super(properties); + this.registerDefaultState((BlockState)((BlockState)((BlockState)this.stateDefinition.any()).setValue(FACING, Direction.SOUTH))); + } + + protected MapCodec codec() { + return null; + } + + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + builder.add(new Property[]{FACING}); + } + + public BlockState rotate(BlockState state, Rotation rot) { + return (BlockState)state.setValue(FACING, rot.rotate((Direction)state.getValue(FACING))); + } + + public BlockState mirror(BlockState state, Mirror mirror) { + return state.rotate(mirror.getRotation((Direction)state.getValue(FACING))); + } + + public BlockState getStateForPlacement(BlockPlaceContext context) { + return (BlockState)this.defaultBlockState().setValue(FACING, context.getNearestLookingDirection().getOpposite().getOpposite()); + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/block/custom/TankBlock.java b/common/src/main/java/net/cmr/jurassicrevived/block/custom/TankBlock.java new file mode 100644 index 0000000..b411186 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/block/custom/TankBlock.java @@ -0,0 +1,146 @@ +package net.cmr.jurassicrevived.block.custom; + +//? if >1.20.1 { +/*import com.mojang.serialization.MapCodec; +import net.minecraft.core.component.DataComponents; +import net.minecraft.world.item.component.CustomData; +import net.minecraft.world.ItemInteractionResult; +*///?} + +import dev.architectury.registry.menu.MenuRegistry; +import net.cmr.jurassicrevived.block.entity.ModBlockEntities; +import net.cmr.jurassicrevived.block.entity.custom.TankBlockEntity; +import net.minecraft.core.BlockPos; +import net.minecraft.core.registries.Registries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.BaseEntityBlock; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.RenderShape; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityTicker; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.shapes.VoxelShape; +import org.jetbrains.annotations.Nullable; + +public class TankBlock extends BaseEntityBlock { + public TankBlock(Properties properties) { + super(properties); + } + + public static final VoxelShape SHAPE = Block.box(1.0D, 0.0D, 1.0D, 15.0D, 15.0D, 15.0D); + + //? if >1.20.1 { + /*@Override protected MapCodec codec() { return null; } + *///?} + + @Override + public RenderShape getRenderShape(BlockState pState) { + return RenderShape.MODEL; + } + + @Override + //? if >1.20.1 { + /*public BlockState playerWillDestroy(Level level, BlockPos pos, BlockState state, Player player) { + *///?} else { + public void playerWillDestroy(Level level, BlockPos pos, BlockState state, Player player) { + //?} + if (!level.isClientSide) { + if (player.getAbilities().instabuild) { + level.removeBlockEntity(pos); + level.setBlock(pos, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL); + //? if >1.20.1 { + /*return state; + *///?} else { + return; + //?} + } + + BlockEntity be = level.getBlockEntity(pos); + if (be instanceof TankBlockEntity fbe) { + ItemStack stack = new ItemStack(this.asItem()); + + if (!fbe.isEmptyForDrop()) { + //? if >1.20.1 { + /*CompoundTag tag = fbe.saveWithoutMetadata(level.registryAccess()); + var beTypeKey = level.registryAccess().registryOrThrow(Registries.BLOCK_ENTITY_TYPE).getKey(fbe.getType()); + if (beTypeKey != null) tag.putString("id", beTypeKey.toString()); + stack.set(DataComponents.BLOCK_ENTITY_DATA, CustomData.of(tag)); + *///?} else { + CompoundTag tag = fbe.saveWithoutMetadata(); + stack.getOrCreateTagElement("BlockEntityTag").merge(tag); + //?} + } + + popResource(level, pos, stack); + level.removeBlockEntity(pos); + level.setBlock(pos, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL); + //? if >1.20.1 { + /*return state; + *///?} else { + return; + //?} + } + } + super.playerWillDestroy(level, pos, state, player); + //? if >1.20.1 { + /*return state; + *///?} else { + return; + //?} + } + + @Override + public void onRemove(BlockState pState, Level pLevel, BlockPos pPos, BlockState pNewState, boolean pIsMoving) { + super.onRemove(pState, pLevel, pPos, pNewState, pIsMoving); + } + + //? if >1.20.1 { + /*@Override + protected ItemInteractionResult useItemOn(ItemStack pStack, BlockState pState, Level pLevel, BlockPos pPos, Player pPlayer, InteractionHand pHand, BlockHitResult pHitResult) { + if (!pLevel.isClientSide()) { + BlockEntity entity = pLevel.getBlockEntity(pPos); + if (entity instanceof TankBlockEntity tankBlockEntity) { + MenuRegistry.openExtendedMenu((ServerPlayer) pPlayer, tankBlockEntity); + } + } + return ItemInteractionResult.sidedSuccess(pLevel.isClientSide()); + } + *///?} else { + @Override + public InteractionResult use(BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, BlockHitResult hit) { + if (!level.isClientSide()) { + BlockEntity entity = level.getBlockEntity(pos); + if (entity instanceof TankBlockEntity tankBlockEntity) { + MenuRegistry.openExtendedMenu((ServerPlayer) player, tankBlockEntity); + } + } + return InteractionResult.sidedSuccess(level.isClientSide()); + } + //?} + + @Nullable + @Override + public BlockEntity newBlockEntity(BlockPos blockPos, BlockState blockState) { + return new TankBlockEntity(blockPos, blockState); + } + + @Nullable + @Override + public BlockEntityTicker getTicker(Level pLevel, BlockState pState, BlockEntityType pBlockEntityType) { + if(pLevel.isClientSide()) { + return null; + } + + return createTickerHelper(pBlockEntityType, ModBlockEntities.TANK_BE.get(), + ((level, blockPos, blockState, tankBlockEntity) -> tankBlockEntity.tick(level, blockPos, blockState))); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/block/custom/TrashBlock.java b/common/src/main/java/net/cmr/jurassicrevived/block/custom/TrashBlock.java new file mode 100644 index 0000000..eb225b1 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/block/custom/TrashBlock.java @@ -0,0 +1,95 @@ +package net.cmr.jurassicrevived.block.custom; + +import com.mojang.serialization.MapCodec; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.context.BlockPlaceContext; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.HorizontalDirectionalBlock; +import net.minecraft.world.level.block.Mirror; +import net.minecraft.world.level.block.Rotation; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.StateDefinition; +import net.minecraft.world.level.block.state.properties.Property; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.shapes.CollisionContext; +import net.minecraft.world.phys.shapes.VoxelShape; + +public class TrashBlock extends HorizontalDirectionalBlock { + + public static final VoxelShape SHAPE = Block.box(4.0D, 0.0D, 4.0D, 12.0D, 16.0D, 12.0D); + + public TrashBlock(Properties properties) { + super(properties); + this.registerDefaultState((BlockState)((BlockState)((BlockState)this.stateDefinition.any()).setValue(FACING, Direction.SOUTH))); + } + + protected MapCodec codec() { + return null; + } + + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + builder.add(new Property[]{FACING}); + } + + public BlockState mirror(BlockState state, Mirror mirror) { + return state.rotate(mirror.getRotation((Direction)state.getValue(FACING))); + } + + // Ensure rotation of FACING property when the block is rotated (e.g., structure placement, commands, etc.) + @Override + public BlockState rotate(BlockState state, Rotation rotation) { + return state.setValue(FACING, rotation.rotate(state.getValue(FACING))); + } + + public BlockState getStateForPlacement(BlockPlaceContext context) { + // Ensure we always use a horizontal facing (N/E/S/W), even when looking up or down. + return (BlockState)this.defaultBlockState().setValue(FACING, context.getHorizontalDirection().getOpposite()); + } + + //? if >1.20.1 { + /*@Override + protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, + Player player, BlockHitResult hit) { + // Called when player right-clicks the block without using an item on it + if (level.isClientSide) return InteractionResult.SUCCESS; + + InteractionHand hand = hit.isInside() ? InteractionHand.MAIN_HAND : player.getUsedItemHand(); // fallback + if (hand == null) hand = InteractionHand.MAIN_HAND; + + ItemStack stack = player.getItemInHand(hand); + if (!player.isCreative() && !stack.isEmpty()) { + player.setItemInHand(hand, ItemStack.EMPTY); + player.inventoryMenu.broadcastChanges(); + } + return InteractionResult.CONSUME; + } + *///?} else { + @Override + public InteractionResult use(BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, BlockHitResult hit) { + if (level.isClientSide) { + return InteractionResult.SUCCESS; + } + + ItemStack stack = player.getItemInHand(hand); + if (!player.isCreative() && !stack.isEmpty()) { + player.setItemInHand(hand, ItemStack.EMPTY); + player.inventoryMenu.broadcastChanges(); + } + + return InteractionResult.CONSUME; + } + //?} + + @Override + public VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) { + Direction facing = state.getValue(FACING); + return SHAPE; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/block/entity/ModBlockEntities.java b/common/src/main/java/net/cmr/jurassicrevived/block/entity/ModBlockEntities.java new file mode 100644 index 0000000..dc5e7e9 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/block/entity/ModBlockEntities.java @@ -0,0 +1,233 @@ +package net.cmr.jurassicrevived.block.entity; + +import dev.architectury.registry.registries.DeferredRegister; +import dev.architectury.registry.registries.RegistrySupplier; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.block.ModBlocks; +import net.cmr.jurassicrevived.block.entity.custom.*; +import net.minecraft.core.registries.Registries; +import net.minecraft.world.level.block.entity.BlockEntityType; + +public class ModBlockEntities { + public static final DeferredRegister> BLOCK_ENTITIES = + DeferredRegister.create(Constants.MOD_ID, Registries.BLOCK_ENTITY_TYPE); + + public static final RegistrySupplier> CRATE_BE = + BLOCK_ENTITIES.register("crate", () -> + BlockEntityType.Builder.of( + (pos, state) -> new CrateBlockEntity(pos, state, 9), + ModBlocks.WOOD_CRATE.get(), ModBlocks.IRON_CRATE.get() + ).build(null) + ); + + public static final RegistrySupplier> TANK_BE = + BLOCK_ENTITIES.register("tank_be", () -> BlockEntityType.Builder.of( + TankBlockEntity::new, ModBlocks.TANK.get()).build(null)); + + public static final RegistrySupplier> POWER_CELL_BE = + BLOCK_ENTITIES.register("power_cell_be", () -> BlockEntityType.Builder.of( + PowerCellBlockEntity::new, ModBlocks.POWER_CELL.get()).build(null)); + + public static final RegistrySupplier> EGG_BE = + BLOCK_ENTITIES.register("egg_be", () -> + BlockEntityType.Builder.of(EggBlockEntity::new, + ModBlocks.APATOSAURUS_EGG.get(), + ModBlocks.ALBERTOSAURUS_EGG.get(), + ModBlocks.VELOCIRAPTOR_EGG.get(), + ModBlocks.TYRANNOSAURUS_REX_EGG.get(), + ModBlocks.TRICERATOPS_EGG.get(), + ModBlocks.SPINOSAURUS_EGG.get(), + ModBlocks.PARASAUROLOPHUS_EGG.get(), + ModBlocks.INDOMINUS_REX_EGG.get(), + ModBlocks.GALLIMIMUS_EGG.get(), + ModBlocks.DIPLODOCUS_EGG.get(), + ModBlocks.OURANOSAURUS_EGG.get(), + ModBlocks.DILOPHOSAURUS_EGG.get(), + ModBlocks.COMPSOGNATHUS_EGG.get(), + ModBlocks.CERATOSAURUS_EGG.get(), + ModBlocks.BRACHIOSAURUS_EGG.get(), + ModBlocks.BARYONYX_EGG.get(), + ModBlocks.CARNOTAURUS_EGG.get(), + ModBlocks.CONCAVENATOR_EGG.get(), + ModBlocks.DEINONYCHUS_EGG.get(), + ModBlocks.EDMONTOSAURUS_EGG.get(), + ModBlocks.GIGANOTOSAURUS_EGG.get(), + ModBlocks.GUANLONG_EGG.get(), + ModBlocks.HERRERASAURUS_EGG.get(), + ModBlocks.MAJUNGASAURUS_EGG.get(), + ModBlocks.PROCOMPSOGNATHUS_EGG.get(), + ModBlocks.PROTOCERATOPS_EGG.get(), + ModBlocks.RUGOPS_EGG.get(), + ModBlocks.SHANTUNGOSAURUS_EGG.get(), + ModBlocks.STEGOSAURUS_EGG.get(), + ModBlocks.STYRACOSAURUS_EGG.get(), + ModBlocks.THERIZINOSAURUS_EGG.get(), + ModBlocks.DISTORTUS_REX_EGG.get(), + ModBlocks.ALLOSAURUS_EGG.get(), + ModBlocks.ALVAREZSAURUS_EGG.get(), + ModBlocks.ANKYLOSAURUS_EGG.get(), + ModBlocks.ARAMBOURGIANIA_EGG.get(), + ModBlocks.CARCHARODONTOSAURUS_EGG.get(), + ModBlocks.CEARADACTYLUS_EGG.get(), + ModBlocks.CHASMOSAURUS_EGG.get(), + ModBlocks.COELOPHYSIS_EGG.get(), + ModBlocks.COELURUS_EGG.get(), + ModBlocks.CORYTHOSAURUS_EGG.get(), + ModBlocks.DIMORPHODON_EGG.get(), + ModBlocks.DRYOSAURUS_EGG.get(), + ModBlocks.GEOSTERNBERGIA_EGG.get(), + ModBlocks.GUIDRACO_EGG.get(), + ModBlocks.HADROSAURUS_EGG.get(), + ModBlocks.HYPSILOPHODON_EGG.get(), + ModBlocks.INDORAPTOR_EGG.get(), + ModBlocks.INOSTRANCEVIA_EGG.get(), + ModBlocks.LAMBEOSAURUS_EGG.get(), + ModBlocks.LUDODACTYLUS_EGG.get(), + ModBlocks.MAMENCHISAURUS_EGG.get(), + ModBlocks.METRIACANTHOSAURUS_EGG.get(), + ModBlocks.MOGANOPTERUS_EGG.get(), + ModBlocks.NYCTOSAURUS_EGG.get(), + ModBlocks.ORNITHOLESTES_EGG.get(), + ModBlocks.ORNITHOMIMUS_EGG.get(), + ModBlocks.OVIRAPTOR_EGG.get(), + ModBlocks.PACHYCEPHALOSAURUS_EGG.get(), + ModBlocks.PROCERATOSAURUS_EGG.get(), + ModBlocks.PTERANODON_EGG.get(), + ModBlocks.PTERODAUSTRO_EGG.get(), + ModBlocks.QUETZALCOATLUS_EGG.get(), + ModBlocks.RAJASAURUS_EGG.get(), + ModBlocks.SEGISAURUS_EGG.get(), + ModBlocks.TAPEJARA_EGG.get(), + ModBlocks.TITANOSAURUS_EGG.get(), + ModBlocks.TROODON_EGG.get(), + ModBlocks.TROPEOGNATHUS_EGG.get(), + ModBlocks.TUPUXUARA_EGG.get(), + ModBlocks.UTAHRAPTOR_EGG.get(), + ModBlocks.ZHENYUANOPTERUS_EGG.get(), + ModBlocks.INCUBATED_APATOSAURUS_EGG.get(), + ModBlocks.INCUBATED_ALBERTOSAURUS_EGG.get(), + ModBlocks.INCUBATED_VELOCIRAPTOR_EGG.get(), + ModBlocks.INCUBATED_TYRANNOSAURUS_REX_EGG.get(), + ModBlocks.INCUBATED_TRICERATOPS_EGG.get(), + ModBlocks.INCUBATED_SPINOSAURUS_EGG.get(), + ModBlocks.INCUBATED_PARASAUROLOPHUS_EGG.get(), + ModBlocks.INCUBATED_INDOMINUS_REX_EGG.get(), + ModBlocks.INCUBATED_GALLIMIMUS_EGG.get(), + ModBlocks.INCUBATED_DIPLODOCUS_EGG.get(), + ModBlocks.INCUBATED_OURANOSAURUS_EGG.get(), + ModBlocks.INCUBATED_DILOPHOSAURUS_EGG.get(), + ModBlocks.INCUBATED_COMPSOGNATHUS_EGG.get(), + ModBlocks.INCUBATED_CERATOSAURUS_EGG.get(), + ModBlocks.INCUBATED_BRACHIOSAURUS_EGG.get(), + ModBlocks.INCUBATED_BARYONYX_EGG.get(), + ModBlocks.INCUBATED_CARNOTAURUS_EGG.get(), + ModBlocks.INCUBATED_CONCAVENATOR_EGG.get(), + ModBlocks.INCUBATED_DEINONYCHUS_EGG.get(), + ModBlocks.INCUBATED_EDMONTOSAURUS_EGG.get(), + ModBlocks.INCUBATED_GIGANOTOSAURUS_EGG.get(), + ModBlocks.INCUBATED_GUANLONG_EGG.get(), + ModBlocks.INCUBATED_HERRERASAURUS_EGG.get(), + ModBlocks.INCUBATED_MAJUNGASAURUS_EGG.get(), + ModBlocks.INCUBATED_PROCOMPSOGNATHUS_EGG.get(), + ModBlocks.INCUBATED_PROTOCERATOPS_EGG.get(), + ModBlocks.INCUBATED_RUGOPS_EGG.get(), + ModBlocks.INCUBATED_SHANTUNGOSAURUS_EGG.get(), + ModBlocks.INCUBATED_STEGOSAURUS_EGG.get(), + ModBlocks.INCUBATED_STYRACOSAURUS_EGG.get(), + ModBlocks.INCUBATED_THERIZINOSAURUS_EGG.get(), + ModBlocks.INCUBATED_DISTORTUS_REX_EGG.get(), + ModBlocks.INCUBATED_ALLOSAURUS_EGG.get(), + ModBlocks.INCUBATED_ALVAREZSAURUS_EGG.get(), + ModBlocks.INCUBATED_ANKYLOSAURUS_EGG.get(), + ModBlocks.INCUBATED_ARAMBOURGIANIA_EGG.get(), + ModBlocks.INCUBATED_CARCHARODONTOSAURUS_EGG.get(), + ModBlocks.INCUBATED_CEARADACTYLUS_EGG.get(), + ModBlocks.INCUBATED_CHASMOSAURUS_EGG.get(), + ModBlocks.INCUBATED_COELOPHYSIS_EGG.get(), + ModBlocks.INCUBATED_COELURUS_EGG.get(), + ModBlocks.INCUBATED_CORYTHOSAURUS_EGG.get(), + ModBlocks.INCUBATED_DIMORPHODON_EGG.get(), + ModBlocks.INCUBATED_DRYOSAURUS_EGG.get(), + ModBlocks.INCUBATED_GEOSTERNBERGIA_EGG.get(), + ModBlocks.INCUBATED_GUIDRACO_EGG.get(), + ModBlocks.INCUBATED_HADROSAURUS_EGG.get(), + ModBlocks.INCUBATED_HYPSILOPHODON_EGG.get(), + ModBlocks.INCUBATED_INDORAPTOR_EGG.get(), + ModBlocks.INCUBATED_INOSTRANCEVIA_EGG.get(), + ModBlocks.INCUBATED_LAMBEOSAURUS_EGG.get(), + ModBlocks.INCUBATED_LUDODACTYLUS_EGG.get(), + ModBlocks.INCUBATED_MAMENCHISAURUS_EGG.get(), + ModBlocks.INCUBATED_METRIACANTHOSAURUS_EGG.get(), + ModBlocks.INCUBATED_MOGANOPTERUS_EGG.get(), + ModBlocks.INCUBATED_NYCTOSAURUS_EGG.get(), + ModBlocks.INCUBATED_ORNITHOLESTES_EGG.get(), + ModBlocks.INCUBATED_ORNITHOMIMUS_EGG.get(), + ModBlocks.INCUBATED_OVIRAPTOR_EGG.get(), + ModBlocks.INCUBATED_PACHYCEPHALOSAURUS_EGG.get(), + ModBlocks.INCUBATED_PROCERATOSAURUS_EGG.get(), + ModBlocks.INCUBATED_PTERANODON_EGG.get(), + ModBlocks.INCUBATED_PTERODAUSTRO_EGG.get(), + ModBlocks.INCUBATED_QUETZALCOATLUS_EGG.get(), + ModBlocks.INCUBATED_RAJASAURUS_EGG.get(), + ModBlocks.INCUBATED_SEGISAURUS_EGG.get(), + ModBlocks.INCUBATED_TAPEJARA_EGG.get(), + ModBlocks.INCUBATED_TITANOSAURUS_EGG.get(), + ModBlocks.INCUBATED_TROODON_EGG.get(), + ModBlocks.INCUBATED_TROPEOGNATHUS_EGG.get(), + ModBlocks.INCUBATED_TUPUXUARA_EGG.get(), + ModBlocks.INCUBATED_UTAHRAPTOR_EGG.get(), + ModBlocks.INCUBATED_ZHENYUANOPTERUS_EGG.get() + ).build(null)); + + public static final RegistrySupplier> DNA_EXTRACTOR_BE = + BLOCK_ENTITIES.register("dna_extractor_be", () -> BlockEntityType.Builder.of( + DNAExtractorBlockEntity::new, ModBlocks.DNA_EXTRACTOR.get(), ModBlocks.WHITE_DNA_EXTRACTOR.get()).build(null)); + + public static final RegistrySupplier> DNA_ANALYZER_BE = + BLOCK_ENTITIES.register("dna_analyzer_be", () -> BlockEntityType.Builder.of( + DNAAnalyzerBlockEntity::new, ModBlocks.DNA_ANALYZER.get(), ModBlocks.WHITE_DNA_ANALYZER.get()).build(null)); + + public static final RegistrySupplier> FOSSIL_GRINDER_BE = + BLOCK_ENTITIES.register("fossil_grinder_be", () -> BlockEntityType.Builder.of( + FossilGrinderBlockEntity::new, ModBlocks.FOSSIL_GRINDER.get(), ModBlocks.WHITE_FOSSIL_GRINDER.get()).build(null)); + + public static final RegistrySupplier> FOSSIL_CLEANER_BE = + BLOCK_ENTITIES.register("fossil_cleaner_be", () -> BlockEntityType.Builder.of( + FossilCleanerBlockEntity::new, ModBlocks.FOSSIL_CLEANER.get(), ModBlocks.WHITE_FOSSIL_CLEANER.get()).build(null)); + + public static final RegistrySupplier> ITEM_PIPE_BE = + BLOCK_ENTITIES.register("item_pipe_be", () -> BlockEntityType.Builder.of( + PipeBlockEntity::new, ModBlocks.ITEM_PIPE.get()).build(null)); + + public static final RegistrySupplier> FLUID_PIPE_BE = + BLOCK_ENTITIES.register("fluid_pipe_be", () -> BlockEntityType.Builder.of( + PipeBlockEntity::new, ModBlocks.FLUID_PIPE.get()).build(null)); + + public static final RegistrySupplier> POWER_PIPE_BE = + BLOCK_ENTITIES.register("power_pipe_be", () -> BlockEntityType.Builder.of( + PipeBlockEntity::new, ModBlocks.POWER_PIPE.get()).build(null)); + + public static final RegistrySupplier> GENERATOR_BE = + BLOCK_ENTITIES.register("generator_be", () -> BlockEntityType.Builder.of( + GeneratorBlockEntity::new, ModBlocks.GENERATOR.get(), ModBlocks.WHITE_GENERATOR.get()).build(null)); + + public static final RegistrySupplier> DNA_HYBRIDIZER_BE = + BLOCK_ENTITIES.register("dna_hybridizer_be", () -> BlockEntityType.Builder.of( + DNAHybridizerBlockEntity::new, ModBlocks.DNA_HYBRIDIZER.get(), ModBlocks.WHITE_DNA_HYBRIDIZER.get()).build(null)); + + public static final RegistrySupplier> EMBRYONIC_MACHINE_BE = + BLOCK_ENTITIES.register("embryonic_machine_be", () -> BlockEntityType.Builder.of( + EmbryonicMachineBlockEntity::new, ModBlocks.EMBRYONIC_MACHINE.get(), ModBlocks.WHITE_EMBRYONIC_MACHINE.get()).build(null)); + + public static final RegistrySupplier> EMBRYO_CALCIFICATION_MACHINE_BE = + BLOCK_ENTITIES.register("embryo_calcification_machine_be", () -> BlockEntityType.Builder.of( + EmbryoCalcificationMachineBlockEntity::new, ModBlocks.EMBRYO_CALCIFICATION_MACHINE.get(), ModBlocks.WHITE_EMBRYO_CALCIFICATION_MACHINE.get()).build(null)); + + public static final RegistrySupplier> INCUBATOR_BE = + BLOCK_ENTITIES.register("incubator_be", () -> BlockEntityType.Builder.of( + IncubatorBlockEntity::new, ModBlocks.INCUBATOR.get(), ModBlocks.WHITE_INCUBATOR.get()).build(null)); + + public static void register() { + BLOCK_ENTITIES.register(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/block/entity/custom/CrateBlockEntity.java b/common/src/main/java/net/cmr/jurassicrevived/block/entity/custom/CrateBlockEntity.java new file mode 100644 index 0000000..8696014 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/block/entity/custom/CrateBlockEntity.java @@ -0,0 +1,107 @@ +package net.cmr.jurassicrevived.block.entity.custom; + +import net.cmr.jurassicrevived.block.entity.ModBlockEntities; +import net.cmr.jurassicrevived.screen.custom.CrateMenu; +import net.minecraft.core.BlockPos; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.chat.Component; +import net.minecraft.world.Containers; +import net.minecraft.world.MenuProvider; +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import org.jetbrains.annotations.Nullable; + +import dev.architectury.registry.menu.ExtendedMenuProvider; +import net.minecraft.network.FriendlyByteBuf; +//? if >1.20.1 { +/*import net.minecraft.core.HolderLookup; + *///?} + +public class CrateBlockEntity extends BlockEntity implements ExtendedMenuProvider { + private final int size; + public final SimpleContainer itemHandler; + + public CrateBlockEntity(BlockPos pos, BlockState state, int size) { + super(ModBlockEntities.CRATE_BE.get(), pos, state); + this.size = size; + this.itemHandler = new SimpleContainer(size) { + @Override + public void setChanged() { + super.setChanged(); + CrateBlockEntity.this.setChanged(); + } + }; + } + + public boolean isEmptyForDrop() { + return itemHandler.isEmpty(); + } + + public int getSize() { return size; } + + public void dropContents(Level level, BlockPos pos) { + Containers.dropContents(level, pos, itemHandler); + } + + public int redstoneSignal() { + int filled = 0; + for (int i = 0; i < itemHandler.getContainerSize(); i++) { + if (!itemHandler.getItem(i).isEmpty()) filled++; + } + if (itemHandler.getContainerSize() == 0) return 0; + return Math.round((filled / (float) itemHandler.getContainerSize()) * 15f); + } + + //? if >1.20.1 { + /*@Override + protected void saveAdditional(CompoundTag tag, HolderLookup.Provider registries) { + super.saveAdditional(tag, registries); + tag.putInt("crate.size", this.size); + tag.put("crate.inventory", itemHandler.createTag(registries)); + } + + @Override + protected void loadAdditional(CompoundTag tag, HolderLookup.Provider registries) { + super.loadAdditional(tag, registries); + if (tag.contains("crate.inventory")) { + itemHandler.fromTag(tag.getList("crate.inventory", 10), registries); + } + } + *///?} else { + @Override + protected void saveAdditional(CompoundTag tag) { + super.saveAdditional(tag); + tag.putInt("crate.size", this.size); + tag.put("crate.inventory", itemHandler.createTag()); + } + + @Override + public void load(CompoundTag tag) { + super.load(tag); + if (tag.contains("crate.inventory")) { + itemHandler.fromTag(tag.getList("crate.inventory", 10)); + } + } + //?} + + @Override + public Component getDisplayName() { + return Component.translatable(size <= 9 ? "block.jurassicrevived.wood_crate" : "block.jurassicrevived.iron_crate"); + } + + @Override + public void saveExtraData(FriendlyByteBuf buf) { + buf.writeBlockPos(getBlockPos()); + } + + @Nullable + @Override + public AbstractContainerMenu createMenu(int id, Inventory inv, Player player) { + return new CrateMenu(id, inv, this); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/block/entity/custom/DNAAnalyzerBlockEntity.java b/common/src/main/java/net/cmr/jurassicrevived/block/entity/custom/DNAAnalyzerBlockEntity.java new file mode 100644 index 0000000..787c202 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/block/entity/custom/DNAAnalyzerBlockEntity.java @@ -0,0 +1,343 @@ +package net.cmr.jurassicrevived.block.entity.custom; + +import dev.architectury.registry.menu.ExtendedMenuProvider; +import net.cmr.jurassicrevived.block.custom.DNAAnalyzerBlock; +import net.cmr.jurassicrevived.block.entity.ModBlockEntities; +import net.cmr.jurassicrevived.block.entity.energy.ModEnergyStorage; +import net.cmr.jurassicrevived.block.entity.energy.ModEnergyUtil; +import net.cmr.jurassicrevived.item.ModItems; +import net.cmr.jurassicrevived.recipe.DNAAnalyzerRecipe; +import net.cmr.jurassicrevived.recipe.DNAAnalyzerRecipeInput; +import net.cmr.jurassicrevived.recipe.ModRecipes; +import net.cmr.jurassicrevived.screen.custom.DNAAnalyzerMenu; +import net.cmr.jurassicrevived.util.ModTags; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.chat.Component; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.game.ClientGamePacketListener; +import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket; +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ContainerData; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import org.jetbrains.annotations.Nullable; + +//? if >1.20.1 { +/*import net.minecraft.core.HolderLookup; +import net.minecraft.world.item.crafting.RecipeHolder; +*///?} else { +import net.minecraft.core.RegistryAccess; +//?} + +import java.util.Optional; + +public class DNAAnalyzerBlockEntity extends BlockEntity implements ExtendedMenuProvider, ModEnergyUtil.EnergyProvider { + + public final SimpleContainer itemHandler = new SimpleContainer(5) { + @Override + public void setChanged() { + super.setChanged(); + DNAAnalyzerBlockEntity.this.setChanged(); + if (level != null && !level.isClientSide()) { + level.sendBlockUpdated(getBlockPos(), getBlockState(), getBlockState(), 3); + } + } + }; + + private static final int TEST_TUBE_SLOT = 0; + private static final int MATERIAL_SLOT = 1; + private static final int OUTPUT_SLOT_1 = 2; + + private ItemStack lockedOutput = ItemStack.EMPTY; + private String lastInputSignature = ""; + + private final ContainerData data; + private int progress = 0; + private int maxProgress = 600; + private final int DEFAULT_MAX_PROGRESS = 600; + + private static final int TRANSFER_RATE = 1000; + private final ModEnergyStorage energyStorage = createEnergyStorage(); + + public DNAAnalyzerBlockEntity(BlockPos pos, BlockState blockState) { + super(ModBlockEntities.DNA_ANALYZER_BE.get(), pos, blockState); + this.data = new ContainerData() { + @Override + public int get(int pIndex) { + return switch (pIndex) { + case 0 -> DNAAnalyzerBlockEntity.this.progress; + case 1 -> DNAAnalyzerBlockEntity.this.maxProgress; + default -> 0; + }; + } + + @Override + public void set(int pIndex, int pValue) { + switch (pIndex) { + case 0 -> DNAAnalyzerBlockEntity.this.progress = pValue; + case 1 -> DNAAnalyzerBlockEntity.this.maxProgress = pValue; + } + } + + @Override + public int getCount() { + return 2; + } + }; + } + + private ModEnergyStorage createEnergyStorage() { + return new ModEnergyStorage(64000, TRANSFER_RATE) { + @Override + public void onEnergyChanged() { + setChanged(); + if (level != null && !level.isClientSide()) { + level.sendBlockUpdated(getBlockPos(), getBlockState(), getBlockState(), 3); + } + } + }; + } + + @Override + public ModEnergyStorage getEnergyStorage(@Nullable Direction direction) { + return this.energyStorage; + } + + @Override + public Component getDisplayName() { + return Component.translatable("block.jurassicrevived.dna_analyzer"); + } + + @Override + public void saveExtraData(FriendlyByteBuf buf) { + buf.writeBlockPos(getBlockPos()); + } + + @Nullable + @Override + public AbstractContainerMenu createMenu(int i, Inventory inventory, Player player) { + return new DNAAnalyzerMenu(i, inventory, this, this.data); + } + + public boolean isEmptyForDrop() { + return itemHandler.isEmpty() && this.progress == 0; + } + + //? if >1.20.1 { + /*@Override + protected void saveAdditional(CompoundTag tag, HolderLookup.Provider registries) { + super.saveAdditional(tag, registries); + tag.put("Inventory", itemHandler.createTag(registries)); + tag.putInt("Prog", this.progress); + tag.putInt("MaxProg", this.maxProgress); + tag.put("Energy", energyStorage.saveNBT()); + } + + @Override + protected void loadAdditional(CompoundTag tag, HolderLookup.Provider registries) { + super.loadAdditional(tag, registries); + itemHandler.fromTag(tag.getList("Inventory", 10), registries); + progress = tag.getInt("Prog"); + maxProgress = tag.getInt("MaxProg"); + if (tag.contains("Energy")) { + energyStorage.loadNBT(tag.getCompound("Energy")); + } + } + *///?} else { + @Override + protected void saveAdditional(CompoundTag tag) { + super.saveAdditional(tag); + tag.put("Inventory", itemHandler.createTag()); + tag.putInt("Prog", this.progress); + tag.putInt("MaxProg", this.maxProgress); + tag.put("Energy", energyStorage.saveNBT()); + } + + @Override + public void load(CompoundTag tag) { + super.load(tag); + itemHandler.fromTag(tag.getList("Inventory", 10)); + progress = tag.getInt("Prog"); + maxProgress = tag.getInt("MaxProg"); + if (tag.contains("Energy")) { + energyStorage.loadNBT(tag.getCompound("Energy")); + } + } + //?} + + public void tick(Level level, BlockPos pos, BlockState state) { + if (level.isClientSide) return; + + pullEnergyFromNeighbors(); + + //? if >1.20.1 { + /*Optional> recipeOpt = getCurrentRecipe(); + *///?} else { + Optional recipeOpt = getCurrentRecipe(); + //?} + + if (recipeOpt.isEmpty()) { + resetProgress(); + level.setBlockAndUpdate(pos, state.setValue(DNAAnalyzerBlock.LIT, false)); + this.lockedOutput = ItemStack.EMPTY; + this.lastInputSignature = ""; + return; + } + + String currentSignature = signatureOf(itemHandler.getItem(TEST_TUBE_SLOT), itemHandler.getItem(MATERIAL_SLOT)); + + if (progress == 0 && (lockedOutput.isEmpty() || !currentSignature.equals(lastInputSignature))) { + //? if >1.20.1 { + /*lockedOutput = determineOutput(recipeOpt.get().value()).copy(); + *///?} else { + lockedOutput = determineOutput(recipeOpt.get()).copy(); + //?} + lastInputSignature = currentSignature; + } + + if (!lockedOutput.isEmpty() && canInsertOutput(lockedOutput)) { + if (energyStorage.getEnergyStored() < 10) return; + energyStorage.extractEnergy(10, false); + + progress++; + level.setBlockAndUpdate(pos, state.setValue(DNAAnalyzerBlock.LIT, true)); + + if (progress >= maxProgress) { + craftItem(lockedOutput); + resetProgress(); + level.setBlockAndUpdate(pos, state.setValue(DNAAnalyzerBlock.LIT, false)); + this.lockedOutput = ItemStack.EMPTY; + this.lastInputSignature = ""; + } + } else { + resetProgress(); + level.setBlockAndUpdate(pos, state.setValue(DNAAnalyzerBlock.LIT, false)); + } + } + + private void craftItem(ItemStack output) { + ItemStack current = itemHandler.getItem(OUTPUT_SLOT_1); + if (current.isEmpty()) { + itemHandler.setItem(OUTPUT_SLOT_1, output.copy()); + } else { + current.grow(output.getCount()); + } + itemHandler.removeItem(TEST_TUBE_SLOT, 1); + itemHandler.removeItem(MATERIAL_SLOT, 1); + } + + private boolean canInsertOutput(ItemStack output) { + ItemStack stack = itemHandler.getItem(OUTPUT_SLOT_1); + //? if >1.20.1 { + /*return stack.isEmpty() || (ItemStack.isSameItemSameComponents(stack, output) && stack.getCount() + output.getCount() <= stack.getMaxStackSize()); + *///?} else { + return stack.isEmpty() || (ItemStack.isSameItemSameTags(stack, output) && stack.getCount() + output.getCount() <= stack.getMaxStackSize()); + //?} + } + + //? if >1.20.1 { + /*private Optional> getCurrentRecipe() { + return level.getRecipeManager().getRecipeFor(ModRecipes.DNA_ANALYZER_RECIPE_TYPE.get(), + new DNAAnalyzerRecipeInput(itemHandler.getItem(TEST_TUBE_SLOT), itemHandler.getItem(MATERIAL_SLOT)), level); + } + *///?} else { + private Optional getCurrentRecipe() { + return level.getRecipeManager().getRecipeFor(ModRecipes.DNA_ANALYZER_RECIPE_TYPE.get(), + new DNAAnalyzerRecipeInput(itemHandler.getItem(TEST_TUBE_SLOT), itemHandler.getItem(MATERIAL_SLOT)), level); + } + //?} + + private ItemStack determineOutput(DNAAnalyzerRecipe recipe) { + ItemStack material = itemHandler.getItem(MATERIAL_SLOT); + if (material.is(ModItems.MOSQUITO_IN_AMBER.get())) { + return pickWeightedRandomDna(recipe); + } + //? if >1.20.1 { + /*return recipe.output().copy(); + *///?} else { + return recipe.getResultItem(level.registryAccess()).copy(); + //?} + } + + private ItemStack pickWeightedRandomDna(DNAAnalyzerRecipe recipe) { + var registry = level.registryAccess().registryOrThrow(Registries.ITEM); + var tagged = registry.getTag(ModTags.Items.DNA); + + //? if >1.20.1 { + /*ItemStack fallback = recipe.output().copy(); + *///?} else { + ItemStack fallback = recipe.getResultItem(level.registryAccess()).copy(); + //?} + + if (tagged.isEmpty()) return fallback; + + int totalWeight = 0; + java.util.List items = new java.util.ArrayList<>(); + java.util.List weights = new java.util.ArrayList<>(); + for (var h : tagged.get()) { + int w = recipe.getWeightFor(h.value()); + if (w > 0) { + items.add(h.value()); + weights.add(w); + totalWeight += w; + } + } + if (totalWeight <= 0) return fallback; + int roll = level.random.nextInt(totalWeight); + int acc = 0; + for (int i = 0; i < items.size(); i++) { + acc += weights.get(i); + if (roll < acc) return new ItemStack(items.get(i), Math.max(1, fallback.getCount())); + } + return fallback; + } + + private String signatureOf(ItemStack s1, ItemStack s2) { + return stackSig(s1) + "#" + stackSig(s2); + } + + private String stackSig(ItemStack s) { + return s.isEmpty() ? "empty" : BuiltInRegistries.ITEM.getKey(s.getItem()) + "x" + s.getCount(); + } + + private void resetProgress() { + this.progress = 0; + this.maxProgress = DEFAULT_MAX_PROGRESS; + } + + private void pullEnergyFromNeighbors() { + for (Direction dir : Direction.values()) { + BlockEntity be = level.getBlockEntity(worldPosition.relative(dir)); + if (be instanceof ModEnergyUtil.EnergyProvider provider) { + ModEnergyStorage source = provider.getEnergyStorage(dir.getOpposite()); + if (source != null && source.canExtract()) { + int accepted = energyStorage.receiveEnergy(TRANSFER_RATE, true); + if (accepted > 0) { + energyStorage.receiveEnergy(source.extractEnergy(accepted, false), false); + } + } + } + } + } + + //? if >1.20.1 { + /*@Override + public CompoundTag getUpdateTag(HolderLookup.Provider registries) { return saveWithoutMetadata(registries); } + *///?} else { + @Override + public CompoundTag getUpdateTag() { return saveWithoutMetadata(); } + //?} + + @Override + public @Nullable Packet getUpdatePacket() { return ClientboundBlockEntityDataPacket.create(this); } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/block/entity/custom/DNAExtractorBlockEntity.java b/common/src/main/java/net/cmr/jurassicrevived/block/entity/custom/DNAExtractorBlockEntity.java new file mode 100644 index 0000000..4e63d64 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/block/entity/custom/DNAExtractorBlockEntity.java @@ -0,0 +1,356 @@ +package net.cmr.jurassicrevived.block.entity.custom; + +import dev.architectury.registry.menu.ExtendedMenuProvider; +import net.cmr.jurassicrevived.block.custom.DNAExtractorBlock; +import net.cmr.jurassicrevived.block.entity.ModBlockEntities; +import net.cmr.jurassicrevived.block.entity.energy.ModEnergyStorage; +import net.cmr.jurassicrevived.block.entity.energy.ModEnergyUtil; +import net.cmr.jurassicrevived.item.ModItems; +import net.cmr.jurassicrevived.recipe.DNAExtractorRecipe; +import net.cmr.jurassicrevived.recipe.DNAExtractorRecipeInput; +import net.cmr.jurassicrevived.recipe.ModRecipes; +import net.cmr.jurassicrevived.screen.custom.DNAExtractorMenu; +import net.cmr.jurassicrevived.util.ModTags; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.chat.Component; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.game.ClientGamePacketListener; +import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket; +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ContainerData; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import org.jetbrains.annotations.Nullable; + +//? if >1.20.1 { +/*import net.minecraft.core.HolderLookup; +import net.minecraft.world.item.crafting.RecipeHolder; +*///?} else { +import net.minecraft.core.RegistryAccess; +//?} + +import java.util.Optional; + +public class DNAExtractorBlockEntity extends BlockEntity implements ExtendedMenuProvider, ModEnergyUtil.EnergyProvider { + + public final SimpleContainer itemHandler = new SimpleContainer(5) { + @Override + public void setChanged() { + super.setChanged(); + DNAExtractorBlockEntity.this.setChanged(); + if (level != null && !level.isClientSide()) { + level.sendBlockUpdated(getBlockPos(), getBlockState(), getBlockState(), 3); + } + } + }; + + private static final int TEST_TUBE_SLOT = 0; + private static final int MATERIAL_SLOT = 1; + private static final int[] OUTPUT_SLOTS = {2, 3, 4}; + + private ItemStack lockedOutput = ItemStack.EMPTY; + private String lastInputSignature = ""; + + private final ContainerData data; + private int progress = 0; + private int maxProgress = 600; + private final int DEFAULT_MAX_PROGRESS = 600; + + private static final int TRANSFER_RATE = 1000; + private final ModEnergyStorage energyStorage = createEnergyStorage(); + + public DNAExtractorBlockEntity(BlockPos pos, BlockState blockState) { + super(ModBlockEntities.DNA_EXTRACTOR_BE.get(), pos, blockState); + this.data = new ContainerData() { + @Override + public int get(int pIndex) { + return switch (pIndex) { + case 0 -> DNAExtractorBlockEntity.this.progress; + case 1 -> DNAExtractorBlockEntity.this.maxProgress; + default -> 0; + }; + } + + @Override + public void set(int pIndex, int pValue) { + switch (pIndex) { + case 0 -> DNAExtractorBlockEntity.this.progress = pValue; + case 1 -> DNAExtractorBlockEntity.this.maxProgress = pValue; + } + } + + @Override + public int getCount() { + return 2; + } + }; + } + + private ModEnergyStorage createEnergyStorage() { + return new ModEnergyStorage(64000, TRANSFER_RATE) { + @Override + public void onEnergyChanged() { + setChanged(); + if (level != null && !level.isClientSide()) { + level.sendBlockUpdated(getBlockPos(), getBlockState(), getBlockState(), 3); + } + } + }; + } + + @Override + public ModEnergyStorage getEnergyStorage(@Nullable Direction direction) { + return this.energyStorage; + } + + @Override + public Component getDisplayName() { + return Component.translatable("block.jurassicrevived.dna_extractor"); + } + + @Override + public void saveExtraData(FriendlyByteBuf buf) { + buf.writeBlockPos(getBlockPos()); + } + + @Nullable + @Override + public AbstractContainerMenu createMenu(int i, Inventory inventory, Player player) { + return new DNAExtractorMenu(i, inventory, this, this.data); + } + + public boolean isEmptyForDrop() { + return itemHandler.isEmpty() && this.progress == 0; + } + + //? if >1.20.1 { + /*@Override + protected void saveAdditional(CompoundTag tag, HolderLookup.Provider registries) { + super.saveAdditional(tag, registries); + tag.put("Inventory", itemHandler.createTag(registries)); + tag.putInt("Prog", this.progress); + tag.putInt("MaxProg", this.maxProgress); + tag.put("Energy", energyStorage.saveNBT()); + } + + @Override + protected void loadAdditional(CompoundTag tag, HolderLookup.Provider registries) { + super.loadAdditional(tag, registries); + itemHandler.fromTag(tag.getList("Inventory", 10), registries); + progress = tag.getInt("Prog"); + maxProgress = tag.getInt("MaxProg"); + if (tag.contains("Energy")) { + energyStorage.loadNBT(tag.getCompound("Energy")); + } + } + *///?} else { + @Override + protected void saveAdditional(CompoundTag tag) { + super.saveAdditional(tag); + tag.put("Inventory", itemHandler.createTag()); + tag.putInt("Prog", this.progress); + tag.putInt("MaxProg", this.maxProgress); + tag.put("Energy", energyStorage.saveNBT()); + } + + @Override + public void load(CompoundTag tag) { + super.load(tag); + itemHandler.fromTag(tag.getList("Inventory", 10)); + progress = tag.getInt("Prog"); + maxProgress = tag.getInt("MaxProg"); + if (tag.contains("Energy")) { + energyStorage.loadNBT(tag.getCompound("Energy")); + } + } + //?} + + public void tick(Level level, BlockPos pos, BlockState state) { + if (level.isClientSide) return; + + pullEnergyFromNeighbors(); + + //? if >1.20.1 { + /*Optional> recipeOpt = getCurrentRecipe(); + *///?} else { + Optional recipeOpt = getCurrentRecipe(); + //?} + + if (recipeOpt.isEmpty()) { + resetProgress(); + level.setBlockAndUpdate(pos, state.setValue(DNAExtractorBlock.LIT, false)); + this.lockedOutput = ItemStack.EMPTY; + this.lastInputSignature = ""; + return; + } + + String currentSignature = signatureOf(itemHandler.getItem(TEST_TUBE_SLOT), itemHandler.getItem(MATERIAL_SLOT)); + + if (progress == 0 && (lockedOutput.isEmpty() || !currentSignature.equals(lastInputSignature))) { + //? if >1.20.1 { + /*lockedOutput = determineOutput(recipeOpt.get().value()).copy(); + *///?} else { + lockedOutput = determineOutput(recipeOpt.get()).copy(); + //?} + lastInputSignature = currentSignature; + } + + if (!lockedOutput.isEmpty() && canInsertOutput(lockedOutput)) { + if (energyStorage.getEnergyStored() < 10) return; + energyStorage.extractEnergy(10, false); + + progress++; + level.setBlockAndUpdate(pos, state.setValue(DNAExtractorBlock.LIT, true)); + + if (progress >= maxProgress) { + craftItem(lockedOutput); + resetProgress(); + level.setBlockAndUpdate(pos, state.setValue(DNAExtractorBlock.LIT, false)); + this.lockedOutput = ItemStack.EMPTY; + this.lastInputSignature = ""; + } + } else { + resetProgress(); + level.setBlockAndUpdate(pos, state.setValue(DNAExtractorBlock.LIT, false)); + } + } + + private void craftItem(ItemStack output) { + for (int slot : OUTPUT_SLOTS) { + ItemStack stack = itemHandler.getItem(slot); + if (stack.isEmpty()) { + itemHandler.setItem(slot, output.copy()); + itemHandler.removeItem(TEST_TUBE_SLOT, 1); + itemHandler.removeItem(MATERIAL_SLOT, 1); + return; + } else if (isSameItem(stack, output) && stack.getCount() + output.getCount() <= stack.getMaxStackSize()) { + stack.grow(output.getCount()); + itemHandler.removeItem(TEST_TUBE_SLOT, 1); + itemHandler.removeItem(MATERIAL_SLOT, 1); + return; + } + } + } + + private boolean canInsertOutput(ItemStack output) { + for (int slot : OUTPUT_SLOTS) { + ItemStack stack = itemHandler.getItem(slot); + if (stack.isEmpty() || (isSameItem(stack, output) && stack.getCount() + output.getCount() <= stack.getMaxStackSize())) return true; + } + return false; + } + + private boolean isSameItem(ItemStack stack, ItemStack other) { + //? if >1.20.1 { + /*return ItemStack.isSameItemSameComponents(stack, other); + *///?} else { + return ItemStack.isSameItemSameTags(stack, other); + //?} + } + + //? if >1.20.1 { + /*private Optional> getCurrentRecipe() { + return level.getRecipeManager().getRecipeFor(ModRecipes.DNA_EXTRACTOR_RECIPE_TYPE.get(), + new DNAExtractorRecipeInput(itemHandler.getItem(TEST_TUBE_SLOT), itemHandler.getItem(MATERIAL_SLOT)), level); + } + *///?} else { + private Optional getCurrentRecipe() { + return level.getRecipeManager().getRecipeFor(ModRecipes.DNA_EXTRACTOR_RECIPE_TYPE.get(), + new DNAExtractorRecipeInput(itemHandler.getItem(TEST_TUBE_SLOT), itemHandler.getItem(MATERIAL_SLOT)), level); + } + //?} + + private ItemStack determineOutput(DNAExtractorRecipe recipe) { + ItemStack material = itemHandler.getItem(MATERIAL_SLOT); + if (material.is(ModItems.MOSQUITO_IN_AMBER.get())) { + return pickWeightedRandomDna(recipe); + } + //? if >1.20.1 { + /*return recipe.output().copy(); + *///?} else { + return recipe.getResultItem(level.registryAccess()).copy(); + //?} + } + + private ItemStack pickWeightedRandomDna(DNAExtractorRecipe recipe) { + var registry = level.registryAccess().registryOrThrow(Registries.ITEM); + var tagged = registry.getTag(ModTags.Items.DNA); + + //? if >1.20.1 { + /*ItemStack fallback = recipe.output().copy(); + *///?} else { + ItemStack fallback = recipe.getResultItem(level.registryAccess()).copy(); + //?} + + if (tagged.isEmpty()) return fallback; + + int totalWeight = 0; + java.util.List items = new java.util.ArrayList<>(); + java.util.List weights = new java.util.ArrayList<>(); + for (var h : tagged.get()) { + int w = recipe.getWeightFor(h.value()); + if (w > 0) { + items.add(h.value()); + weights.add(w); + totalWeight += w; + } + } + if (totalWeight <= 0) return fallback; + int roll = level.random.nextInt(totalWeight); + int acc = 0; + for (int i = 0; i < items.size(); i++) { + acc += weights.get(i); + if (roll < acc) return new ItemStack(items.get(i), Math.max(1, fallback.getCount())); + } + return fallback; + } + + private String signatureOf(ItemStack s1, ItemStack s2) { + return stackSig(s1) + "#" + stackSig(s2); + } + + private String stackSig(ItemStack s) { + return s.isEmpty() ? "empty" : BuiltInRegistries.ITEM.getKey(s.getItem()) + "x" + s.getCount(); + } + + private void resetProgress() { + this.progress = 0; + this.maxProgress = DEFAULT_MAX_PROGRESS; + } + + private void pullEnergyFromNeighbors() { + for (Direction dir : Direction.values()) { + BlockEntity be = level.getBlockEntity(worldPosition.relative(dir)); + if (be instanceof ModEnergyUtil.EnergyProvider provider) { + ModEnergyStorage source = provider.getEnergyStorage(dir.getOpposite()); + if (source != null && source.canExtract()) { + int accepted = energyStorage.receiveEnergy(TRANSFER_RATE, true); + if (accepted > 0) { + energyStorage.receiveEnergy(source.extractEnergy(accepted, false), false); + } + } + } + } + } + + //? if >1.20.1 { + /*@Override + public CompoundTag getUpdateTag(HolderLookup.Provider registries) { return saveWithoutMetadata(registries); } + *///?} else { + @Override + public CompoundTag getUpdateTag() { return saveWithoutMetadata(); } + //?} + + @Override + public @Nullable Packet getUpdatePacket() { return ClientboundBlockEntityDataPacket.create(this); } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/block/entity/custom/DNAHybridizerBlockEntity.java b/common/src/main/java/net/cmr/jurassicrevived/block/entity/custom/DNAHybridizerBlockEntity.java new file mode 100644 index 0000000..d33a169 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/block/entity/custom/DNAHybridizerBlockEntity.java @@ -0,0 +1,360 @@ +package net.cmr.jurassicrevived.block.entity.custom; + +import dev.architectury.registry.menu.ExtendedMenuProvider; +import net.cmr.jurassicrevived.block.custom.DNAHybridizerBlock; +import net.cmr.jurassicrevived.block.entity.ModBlockEntities; +import net.cmr.jurassicrevived.block.entity.energy.ModEnergyStorage; +import net.cmr.jurassicrevived.block.entity.energy.ModEnergyUtil; +import net.cmr.jurassicrevived.recipe.DNAHybridizerRecipe; +import net.cmr.jurassicrevived.recipe.DNAHybridizerRecipeInput; +import net.cmr.jurassicrevived.recipe.ModRecipes; +import net.cmr.jurassicrevived.screen.custom.DNAHybridizerMenu; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.chat.Component; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.game.ClientGamePacketListener; +import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket; +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ContainerData; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import org.jetbrains.annotations.Nullable; + +//? if >1.20.1 { +/*import net.minecraft.core.HolderLookup; +import net.minecraft.world.item.crafting.RecipeHolder; +*///?} else { +import net.minecraft.core.RegistryAccess; +//?} + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +public class DNAHybridizerBlockEntity extends BlockEntity implements ExtendedMenuProvider, ModEnergyUtil.EnergyProvider { + + public final SimpleContainer itemHandler = new SimpleContainer(11) { + @Override + public void setChanged() { + super.setChanged(); + DNAHybridizerBlockEntity.this.setChanged(); + if (level != null && !level.isClientSide()) { + level.sendBlockUpdated(getBlockPos(), getBlockState(), getBlockState(), 3); + } + } + }; + + private static final int[] DNA_SLOTS = {0, 1, 2, 3, 4, 5, 6, 7}; + private static final int CATALYST_SLOT = 8; + private static final int OUTPUT_SLOT = 9; + + private ItemStack lockedOutput = ItemStack.EMPTY; + private String lastInputSignature = ""; + + private final ContainerData data; + private int progress = 0; + private int maxProgress = 3000; + private final int DEFAULT_MAX_PROGRESS = 3000; + + private static final int TRANSFER_RATE = 1000; + private final ModEnergyStorage energyStorage = createEnergyStorage(); + + public DNAHybridizerBlockEntity(BlockPos pos, BlockState blockState) { + super(ModBlockEntities.DNA_HYBRIDIZER_BE.get(), pos, blockState); + this.data = new ContainerData() { + @Override + public int get(int pIndex) { + return switch (pIndex) { + case 0 -> DNAHybridizerBlockEntity.this.progress; + case 1 -> DNAHybridizerBlockEntity.this.maxProgress; + default -> 0; + }; + } + + @Override + public void set(int pIndex, int pValue) { + switch (pIndex) { + case 0 -> DNAHybridizerBlockEntity.this.progress = pValue; + case 1 -> DNAHybridizerBlockEntity.this.maxProgress = pValue; + } + } + + @Override + public int getCount() { + return 2; + } + }; + } + + private ModEnergyStorage createEnergyStorage() { + return new ModEnergyStorage(64000, TRANSFER_RATE) { + @Override + public void onEnergyChanged() { + setChanged(); + if (level != null && !level.isClientSide()) { + level.sendBlockUpdated(getBlockPos(), getBlockState(), getBlockState(), 3); + } + } + }; + } + + @Override + public ModEnergyStorage getEnergyStorage(@Nullable Direction direction) { + return this.energyStorage; + } + + @Override + public Component getDisplayName() { + return Component.translatable("block.jurassicrevived.dna_hybridizer"); + } + + @Override + public void saveExtraData(FriendlyByteBuf buf) { + buf.writeBlockPos(getBlockPos()); + } + + @Nullable + @Override + public AbstractContainerMenu createMenu(int i, Inventory inventory, Player player) { + return new DNAHybridizerMenu(i, inventory, this, this.data); + } + + public boolean isEmptyForDrop() { + return itemHandler.isEmpty() && this.progress == 0; + } + + //? if >1.20.1 { + /*@Override + protected void saveAdditional(CompoundTag tag, HolderLookup.Provider registries) { + super.saveAdditional(tag, registries); + tag.put("Inventory", itemHandler.createTag(registries)); + saveCommonData(tag); + } + + @Override + protected void loadAdditional(CompoundTag tag, HolderLookup.Provider registries) { + super.loadAdditional(tag, registries); + itemHandler.fromTag(tag.getList("Inventory", 10), registries); + loadCommonData(tag); + } + *///?} else { + @Override + protected void saveAdditional(CompoundTag tag) { + super.saveAdditional(tag); + tag.put("Inventory", itemHandler.createTag()); + saveCommonData(tag); + } + + @Override + public void load(CompoundTag tag) { + super.load(tag); + itemHandler.fromTag(tag.getList("Inventory", 10)); + loadCommonData(tag); + } + //?} + + private void saveCommonData(CompoundTag tag) { + tag.putInt("Prog", this.progress); + tag.putInt("MaxProg", this.maxProgress); + tag.put("Energy", energyStorage.saveNBT()); + } + + private void loadCommonData(CompoundTag tag) { + progress = tag.getInt("Prog"); + maxProgress = tag.getInt("MaxProg"); + if (tag.contains("Energy")) { + energyStorage.loadNBT(tag.getCompound("Energy")); + } + } + + public void tick(Level level, BlockPos pos, BlockState state) { + if (level.isClientSide) return; + + pullEnergyFromNeighbors(); + + //? if >1.20.1 { + /*Optional> recipeOpt = getCurrentRecipe(); + *///?} else { + Optional recipeOpt = getCurrentRecipe(); + //?} + + if (recipeOpt.isEmpty()) { + resetProgress(); + level.setBlockAndUpdate(pos, state.setValue(DNAHybridizerBlock.LIT, false)); + this.lockedOutput = ItemStack.EMPTY; + this.lastInputSignature = ""; + return; + } + + String currentSignature = buildSignature(); + + //? if >1.20.1 { + /*DNAHybridizerRecipe recipe = recipeOpt.get().value(); + *///?} else { + DNAHybridizerRecipe recipe = recipeOpt.get(); + //?} + + if (progress == 0 && (lockedOutput.isEmpty() || !currentSignature.equals(lastInputSignature))) { + //? if >1.20.1 { + /*lockedOutput = recipe.output().copy(); + *///?} else { + lockedOutput = recipe.getResultItem(level.registryAccess()).copy(); + //?} + lastInputSignature = currentSignature; + } + + List exactMatch = findExactUnorderedMatchIndices(recipe); + boolean canProceed = exactMatch != null && !lockedOutput.isEmpty() && canInsertOutput(lockedOutput); + + if (canProceed) { + if (energyStorage.getEnergyStored() < 10) return; + energyStorage.extractEnergy(10, false); + + progress++; + level.setBlockAndUpdate(pos, state.setValue(DNAHybridizerBlock.LIT, true)); + + if (progress >= maxProgress) { + craftItem(lockedOutput, exactMatch); + resetProgress(); + level.setBlockAndUpdate(pos, state.setValue(DNAHybridizerBlock.LIT, false)); + this.lockedOutput = ItemStack.EMPTY; + this.lastInputSignature = ""; + } + } else { + resetProgress(); + level.setBlockAndUpdate(pos, state.setValue(DNAHybridizerBlock.LIT, false)); + } + } + + private void craftItem(ItemStack output, List matchedIndices) { + ItemStack current = itemHandler.getItem(OUTPUT_SLOT); + if (current.isEmpty()) { + itemHandler.setItem(OUTPUT_SLOT, output.copy()); + } else { + current.grow(output.getCount()); + } + for (int idx : matchedIndices) { + itemHandler.removeItem(idx, 1); + } + } + + private boolean canInsertOutput(ItemStack output) { + ItemStack stack = itemHandler.getItem(OUTPUT_SLOT); + //? if >1.20.1 { + /*return stack.isEmpty() || (ItemStack.isSameItemSameComponents(stack, output) && stack.getCount() + output.getCount() <= stack.getMaxStackSize()); + *///?} else { + return stack.isEmpty() || (ItemStack.isSameItemSameTags(stack, output) && stack.getCount() + output.getCount() <= stack.getMaxStackSize()); + //?} + } + + private @Nullable List findExactUnorderedMatchIndices(DNAHybridizerRecipe recipe) { + var inputs = recipe.getIngredients(); + boolean hasCatalyst = inputs.size() >= 9 && !inputs.get(8).isEmpty(); + ItemStack catStack = itemHandler.getItem(CATALYST_SLOT); + + if (hasCatalyst) { + if (catStack.isEmpty() || !inputs.get(8).test(catStack)) return null; + } else if (!catStack.isEmpty()) return null; + + List required = new ArrayList<>(); + for (int i = 0; i < Math.min(8, inputs.size()); i++) { + if (!inputs.get(i).isEmpty()) required.add(inputs.get(i)); + } + if (required.isEmpty()) return null; + + boolean[] used = new boolean[8]; + List matched = new ArrayList<>(); + + for (var need : required) { + boolean found = false; + for (int i = 0; i < 8; i++) { + if (used[i]) continue; + ItemStack stack = itemHandler.getItem(i); + if (!stack.isEmpty() && need.test(stack)) { + used[i] = true; + matched.add(i); + found = true; + break; + } + } + if (!found) return null; + } + + for (int i = 0; i < 8; i++) { + if (!used[i] && !itemHandler.getItem(i).isEmpty()) return null; + } + + if (hasCatalyst) matched.add(CATALYST_SLOT); + return matched; + } + + //? if >1.20.1 { + /*private Optional> getCurrentRecipe() { + return level.getRecipeManager().getRecipeFor(ModRecipes.DNA_HYBRIDIZER_RECIPE_TYPE.get(), + new DNAHybridizerRecipeInput( + itemHandler.getItem(0), itemHandler.getItem(1), itemHandler.getItem(2), + itemHandler.getItem(3), itemHandler.getItem(4), itemHandler.getItem(5), + itemHandler.getItem(6), itemHandler.getItem(7), itemHandler.getItem(8) + ), level); + } + *///?} else { + private Optional getCurrentRecipe() { + return level.getRecipeManager().getRecipeFor(ModRecipes.DNA_HYBRIDIZER_RECIPE_TYPE.get(), + new DNAHybridizerRecipeInput( + itemHandler.getItem(0), itemHandler.getItem(1), itemHandler.getItem(2), + itemHandler.getItem(3), itemHandler.getItem(4), itemHandler.getItem(5), + itemHandler.getItem(6), itemHandler.getItem(7), itemHandler.getItem(8) + ), level); + } + //?} + + private String buildSignature() { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 9; i++) { + ItemStack s = itemHandler.getItem(i); + sb.append(s.isEmpty() ? "e" : BuiltInRegistries.ITEM.getKey(s.getItem()) + "x" + s.getCount()).append("#"); + } + return sb.toString(); + } + + private void resetProgress() { + this.progress = 0; + this.maxProgress = DEFAULT_MAX_PROGRESS; + } + + private void pullEnergyFromNeighbors() { + for (Direction dir : Direction.values()) { + BlockEntity be = level.getBlockEntity(worldPosition.relative(dir)); + if (be instanceof ModEnergyUtil.EnergyProvider provider) { + ModEnergyStorage source = provider.getEnergyStorage(dir.getOpposite()); + if (source != null && source.canExtract()) { + int accepted = energyStorage.receiveEnergy(TRANSFER_RATE, true); + if (accepted > 0) { + energyStorage.receiveEnergy(source.extractEnergy(accepted, false), false); + } + } + } + } + } + + //? if >1.20.1 { + /*@Override + public CompoundTag getUpdateTag(HolderLookup.Provider registries) { return saveWithoutMetadata(registries); } + *///?} else { + @Override + public CompoundTag getUpdateTag() { return saveWithoutMetadata(); } + //?} + + @Override + public @Nullable Packet getUpdatePacket() { return ClientboundBlockEntityDataPacket.create(this); } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/block/entity/custom/EggBlockEntity.java b/common/src/main/java/net/cmr/jurassicrevived/block/entity/custom/EggBlockEntity.java new file mode 100644 index 0000000..4d0a279 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/block/entity/custom/EggBlockEntity.java @@ -0,0 +1,100 @@ +package net.cmr.jurassicrevived.block.entity.custom; + +import net.cmr.jurassicrevived.block.entity.ModBlockEntities; +import net.minecraft.core.BlockPos; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.chat.Component; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; + +//? if >1.20.1 { +/*import net.minecraft.core.HolderLookup; + *///?} + +public class EggBlockEntity extends BlockEntity { + private long placedAt = -1L; + private int totalSeconds = 5; // default + + public EggBlockEntity(BlockPos pos, BlockState state) { + super(ModBlockEntities.EGG_BE.get(), pos, state); + } + + public void setPlacedAt(long gameTime) { + this.placedAt = gameTime; + setChanged(); + } + + public void resetForNewPlacement(Level level, int totalSeconds) { + this.placedAt = level.getGameTime(); + this.totalSeconds = Math.max(1, totalSeconds); + setChanged(); + } + + public void setTotalSeconds(int secs) { + this.totalSeconds = Math.max(1, secs); + setChanged(); + } + + public int getTotalSeconds() { + return totalSeconds; + } + + public int getSecondsRemaining(Level level) { + if (placedAt < 0) return totalSeconds; + long elapsed = level.getGameTime() - placedAt; + long remainingTicks = Math.max(0, (20L * totalSeconds) - elapsed); + return (int) Math.ceil(remainingTicks / 20.0); + } + + //? if >1.20.1 { + /*@Override + protected void saveAdditional(CompoundTag tag, HolderLookup.Provider registries) { + super.saveAdditional(tag, registries); + saveCommonData(tag); + } + + @Override + protected void loadAdditional(CompoundTag tag, HolderLookup.Provider registries) { + super.loadAdditional(tag, registries); + loadCommonData(tag); + } + *///?} else { + @Override + protected void saveAdditional(CompoundTag tag) { + super.saveAdditional(tag); + saveCommonData(tag); + } + + @Override + public void load(CompoundTag tag) { + super.load(tag); + loadCommonData(tag); + } + //?} + + private void saveCommonData(CompoundTag tag) { + tag.putLong("egg.placedAt", placedAt); + tag.putInt("egg.totalSeconds", totalSeconds); + } + + private void loadCommonData(CompoundTag tag) { + if (tag.contains("egg.placedAt")) { + placedAt = tag.getLong("egg.placedAt"); + } + if (tag.contains("egg.totalSeconds")) { + totalSeconds = Math.max(1, tag.getInt("egg.totalSeconds")); + } + } + + public void invalidateTimer() { + this.placedAt = -1L; + setChanged(); + } + + public Component getHatchTooltip(Level level, Player player) { + int secs = getSecondsRemaining(level); + return Component.translatable("tooltip.jurassicrevived.egg.hatches_in_seconds", secs); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/block/entity/custom/EmbryoCalcificationMachineBlockEntity.java b/common/src/main/java/net/cmr/jurassicrevived/block/entity/custom/EmbryoCalcificationMachineBlockEntity.java new file mode 100644 index 0000000..340aed5 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/block/entity/custom/EmbryoCalcificationMachineBlockEntity.java @@ -0,0 +1,297 @@ +package net.cmr.jurassicrevived.block.entity.custom; + +import dev.architectury.registry.menu.ExtendedMenuProvider; +import net.cmr.jurassicrevived.block.custom.EmbryoCalcificationMachineBlock; +import net.cmr.jurassicrevived.block.entity.ModBlockEntities; +import net.cmr.jurassicrevived.block.entity.energy.ModEnergyStorage; +import net.cmr.jurassicrevived.block.entity.energy.ModEnergyUtil; +import net.cmr.jurassicrevived.recipe.EmbryoCalcificationMachineRecipe; +import net.cmr.jurassicrevived.recipe.EmbryoCalcificationMachineRecipeInput; +import net.cmr.jurassicrevived.recipe.ModRecipes; +import net.cmr.jurassicrevived.screen.custom.EmbryoCalcificationMachineMenu; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.chat.Component; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.game.ClientGamePacketListener; +import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket; +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ContainerData; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import org.jetbrains.annotations.Nullable; + +//? if >1.20.1 { +/*import net.minecraft.core.HolderLookup; +import net.minecraft.world.item.crafting.RecipeHolder; +*///?} else { +import net.minecraft.core.RegistryAccess; +//?} + +import java.util.Optional; + +public class EmbryoCalcificationMachineBlockEntity extends BlockEntity implements ExtendedMenuProvider, ModEnergyUtil.EnergyProvider { + + public final SimpleContainer itemHandler = new SimpleContainer(5) { + @Override + public void setChanged() { + super.setChanged(); + EmbryoCalcificationMachineBlockEntity.this.setChanged(); + if (level != null && !level.isClientSide()) { + level.sendBlockUpdated(getBlockPos(), getBlockState(), getBlockState(), 3); + } + } + }; + + private static final int SYRINGE_SLOT = 0; + private static final int EGG_SLOT = 1; + private static final int OUTPUT_SLOT = 2; + + private ItemStack lockedOutput = ItemStack.EMPTY; + private String lastInputSignature = ""; + + private final ContainerData data; + private int progress = 0; + private int maxProgress = 100; + private final int DEFAULT_MAX_PROGRESS = 100; + + private static final int TRANSFER_RATE = 1000; + private final ModEnergyStorage energyStorage = createEnergyStorage(); + + public EmbryoCalcificationMachineBlockEntity(BlockPos pos, BlockState blockState) { + super(ModBlockEntities.EMBRYO_CALCIFICATION_MACHINE_BE.get(), pos, blockState); + this.data = new ContainerData() { + @Override + public int get(int pIndex) { + return switch (pIndex) { + case 0 -> EmbryoCalcificationMachineBlockEntity.this.progress; + case 1 -> EmbryoCalcificationMachineBlockEntity.this.maxProgress; + default -> 0; + }; + } + + @Override + public void set(int pIndex, int pValue) { + switch (pIndex) { + case 0 -> EmbryoCalcificationMachineBlockEntity.this.progress = pValue; + case 1 -> EmbryoCalcificationMachineBlockEntity.this.maxProgress = pValue; + } + } + + @Override + public int getCount() { + return 2; + } + }; + } + + private ModEnergyStorage createEnergyStorage() { + return new ModEnergyStorage(64000, TRANSFER_RATE) { + @Override + public void onEnergyChanged() { + setChanged(); + if (level != null && !level.isClientSide()) { + level.sendBlockUpdated(getBlockPos(), getBlockState(), getBlockState(), 3); + } + } + }; + } + + @Override + public ModEnergyStorage getEnergyStorage(@Nullable Direction direction) { + return this.energyStorage; + } + + @Override + public Component getDisplayName() { + return Component.translatable("block.jurassicrevived.embryo_calcification_machine"); + } + + @Override + public void saveExtraData(FriendlyByteBuf buf) { + buf.writeBlockPos(getBlockPos()); + } + + @Nullable + @Override + public AbstractContainerMenu createMenu(int i, Inventory inventory, Player player) { + return new EmbryoCalcificationMachineMenu(i, inventory, this, this.data); + } + + public boolean isEmptyForDrop() { + return itemHandler.isEmpty() && this.progress == 0; + } + + //? if >1.20.1 { + /*@Override + protected void saveAdditional(CompoundTag tag, HolderLookup.Provider registries) { + super.saveAdditional(tag, registries); + tag.put("Inventory", itemHandler.createTag(registries)); + saveCommonData(tag); + } + + @Override + protected void loadAdditional(CompoundTag tag, HolderLookup.Provider registries) { + super.loadAdditional(tag, registries); + itemHandler.fromTag(tag.getList("Inventory", 10), registries); + loadCommonData(tag); + } + *///?} else { + @Override + protected void saveAdditional(CompoundTag tag) { + super.saveAdditional(tag); + tag.put("Inventory", itemHandler.createTag()); + saveCommonData(tag); + } + + @Override + public void load(CompoundTag tag) { + super.load(tag); + itemHandler.fromTag(tag.getList("Inventory", 10)); + loadCommonData(tag); + } + //?} + + private void saveCommonData(CompoundTag tag) { + tag.putInt("Prog", this.progress); + tag.putInt("MaxProg", this.maxProgress); + tag.put("Energy", energyStorage.saveNBT()); + } + + private void loadCommonData(CompoundTag tag) { + progress = tag.getInt("Prog"); + maxProgress = tag.getInt("MaxProg"); + if (tag.contains("Energy")) { + energyStorage.loadNBT(tag.getCompound("Energy")); + } + } + + public void tick(Level level, BlockPos pos, BlockState state) { + if (level.isClientSide) return; + + pullEnergyFromNeighbors(); + + //? if >1.20.1 { + /*Optional> recipeOpt = getCurrentRecipe(); + *///?} else { + Optional recipeOpt = getCurrentRecipe(); + //?} + + if (recipeOpt.isEmpty()) { + resetProgress(); + level.setBlockAndUpdate(pos, state.setValue(EmbryoCalcificationMachineBlock.LIT, false)); + this.lockedOutput = ItemStack.EMPTY; + this.lastInputSignature = ""; + return; + } + + String currentSignature = signatureOf(itemHandler.getItem(SYRINGE_SLOT), itemHandler.getItem(EGG_SLOT)); + + if (progress == 0 && (lockedOutput.isEmpty() || !currentSignature.equals(lastInputSignature))) { + //? if >1.20.1 { + /*lockedOutput = recipeOpt.get().value().assemble(new EmbryoCalcificationMachineRecipeInput(itemHandler.getItem(SYRINGE_SLOT), itemHandler.getItem(EGG_SLOT)), level.registryAccess()).copy(); + *///?} else { + lockedOutput = recipeOpt.get().assemble(new EmbryoCalcificationMachineRecipeInput(itemHandler.getItem(SYRINGE_SLOT), itemHandler.getItem(EGG_SLOT)), level.registryAccess()).copy(); + //?} + lastInputSignature = currentSignature; + } + + if (!lockedOutput.isEmpty() && canInsertOutput(lockedOutput)) { + if (energyStorage.getEnergyStored() < 10) return; + energyStorage.extractEnergy(10, false); + + progress++; + level.setBlockAndUpdate(pos, state.setValue(EmbryoCalcificationMachineBlock.LIT, true)); + + if (progress >= maxProgress) { + craftItem(lockedOutput); + resetProgress(); + level.setBlockAndUpdate(pos, state.setValue(EmbryoCalcificationMachineBlock.LIT, false)); + this.lockedOutput = ItemStack.EMPTY; + this.lastInputSignature = ""; + } + } else { + resetProgress(); + level.setBlockAndUpdate(pos, state.setValue(EmbryoCalcificationMachineBlock.LIT, false)); + } + } + + private void craftItem(ItemStack output) { + ItemStack current = itemHandler.getItem(OUTPUT_SLOT); + if (current.isEmpty()) { + itemHandler.setItem(OUTPUT_SLOT, output.copy()); + } else { + current.grow(output.getCount()); + } + itemHandler.removeItem(SYRINGE_SLOT, 1); + itemHandler.removeItem(EGG_SLOT, 1); + } + + private boolean canInsertOutput(ItemStack output) { + ItemStack stack = itemHandler.getItem(OUTPUT_SLOT); + //? if >1.20.1 { + /*return stack.isEmpty() || (ItemStack.isSameItemSameComponents(stack, output) && stack.getCount() + output.getCount() <= stack.getMaxStackSize()); + *///?} else { + return stack.isEmpty() || (ItemStack.isSameItemSameTags(stack, output) && stack.getCount() + output.getCount() <= stack.getMaxStackSize()); + //?} + } + + //? if >1.20.1 { + /*private Optional> getCurrentRecipe() { + return level.getRecipeManager().getRecipeFor(ModRecipes.EMBRYO_CALCIFICATION_MACHINE_RECIPE_TYPE.get(), + new EmbryoCalcificationMachineRecipeInput(itemHandler.getItem(SYRINGE_SLOT), itemHandler.getItem(EGG_SLOT)), level); + } + *///?} else { + private Optional getCurrentRecipe() { + return level.getRecipeManager().getRecipeFor(ModRecipes.EMBRYO_CALCIFICATION_MACHINE_RECIPE_TYPE.get(), + new EmbryoCalcificationMachineRecipeInput(itemHandler.getItem(SYRINGE_SLOT), itemHandler.getItem(EGG_SLOT)), level); + } + //?} + + private String signatureOf(ItemStack s1, ItemStack s2) { + return stackSig(s1) + "#" + stackSig(s2); + } + + private String stackSig(ItemStack s) { + return s.isEmpty() ? "empty" : BuiltInRegistries.ITEM.getKey(s.getItem()) + "x" + s.getCount(); + } + + private void resetProgress() { + this.progress = 0; + this.maxProgress = DEFAULT_MAX_PROGRESS; + } + + private void pullEnergyFromNeighbors() { + for (Direction dir : Direction.values()) { + BlockEntity be = level.getBlockEntity(worldPosition.relative(dir)); + if (be instanceof ModEnergyUtil.EnergyProvider provider) { + ModEnergyStorage source = provider.getEnergyStorage(dir.getOpposite()); + if (source != null && source.canExtract()) { + int accepted = energyStorage.receiveEnergy(TRANSFER_RATE, true); + if (accepted > 0) { + energyStorage.receiveEnergy(source.extractEnergy(accepted, false), false); + } + } + } + } + } + + //? if >1.20.1 { + /*@Override + public CompoundTag getUpdateTag(HolderLookup.Provider registries) { return saveWithoutMetadata(registries); } + *///?} else { + @Override + public CompoundTag getUpdateTag() { return saveWithoutMetadata(); } + //?} + + @Override + public @Nullable Packet getUpdatePacket() { return ClientboundBlockEntityDataPacket.create(this); } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/block/entity/custom/EmbryonicMachineBlockEntity.java b/common/src/main/java/net/cmr/jurassicrevived/block/entity/custom/EmbryonicMachineBlockEntity.java new file mode 100644 index 0000000..6881328 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/block/entity/custom/EmbryonicMachineBlockEntity.java @@ -0,0 +1,349 @@ +package net.cmr.jurassicrevived.block.entity.custom; + +import dev.architectury.registry.menu.ExtendedMenuProvider; +import net.cmr.jurassicrevived.block.custom.EmbryonicMachineBlock; +import net.cmr.jurassicrevived.block.entity.ModBlockEntities; +import net.cmr.jurassicrevived.block.entity.energy.ModEnergyStorage; +import net.cmr.jurassicrevived.block.entity.energy.ModEnergyUtil; +import net.cmr.jurassicrevived.item.ModItems; +import net.cmr.jurassicrevived.recipe.EmbryonicMachineRecipe; +import net.cmr.jurassicrevived.recipe.EmbryonicMachineRecipeInput; +import net.cmr.jurassicrevived.recipe.ModRecipes; +import net.cmr.jurassicrevived.screen.custom.EmbryonicMachineMenu; +import net.cmr.jurassicrevived.util.ModTags; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.chat.Component; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.game.ClientGamePacketListener; +import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket; +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ContainerData; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import org.jetbrains.annotations.Nullable; + +//? if >1.20.1 { +/*import net.minecraft.core.HolderLookup; +import net.minecraft.world.item.crafting.RecipeHolder; +*///?} else { +import net.minecraft.core.RegistryAccess; +//?} + +import java.util.Optional; + +public class EmbryonicMachineBlockEntity extends BlockEntity implements ExtendedMenuProvider, ModEnergyUtil.EnergyProvider { + + public final SimpleContainer itemHandler = new SimpleContainer(4) { + @Override + public void setChanged() { + super.setChanged(); + EmbryonicMachineBlockEntity.this.setChanged(); + if (level != null && !level.isClientSide()) { + level.sendBlockUpdated(getBlockPos(), getBlockState(), getBlockState(), 3); + } + } + }; + + private static final int SYRINGE_SLOT = 0; + private static final int MATERIAL_SLOT = 1; + private static final int FROG_SLOT = 2; + private static final int OUTPUT_SLOT = 3; + + private ItemStack lockedOutput = ItemStack.EMPTY; + private String lastInputSignature = ""; + + private final ContainerData data; + private int progress = 0; + private int maxProgress = 200; + private final int DEFAULT_MAX_PROGRESS = 200; + + private static final int TRANSFER_RATE = 1000; + private final ModEnergyStorage energyStorage = createEnergyStorage(); + + public EmbryonicMachineBlockEntity(BlockPos pos, BlockState blockState) { + super(ModBlockEntities.EMBRYONIC_MACHINE_BE.get(), pos, blockState); + this.data = new ContainerData() { + @Override + public int get(int pIndex) { + return switch (pIndex) { + case 0 -> EmbryonicMachineBlockEntity.this.progress; + case 1 -> EmbryonicMachineBlockEntity.this.maxProgress; + default -> 0; + }; + } + + @Override + public void set(int pIndex, int pValue) { + switch (pIndex) { + case 0 -> EmbryonicMachineBlockEntity.this.progress = pValue; + case 1 -> EmbryonicMachineBlockEntity.this.maxProgress = pValue; + } + } + + @Override + public int getCount() { + return 2; + } + }; + } + + private ModEnergyStorage createEnergyStorage() { + return new ModEnergyStorage(64000, TRANSFER_RATE) { + @Override + public void onEnergyChanged() { + setChanged(); + if (level != null && !level.isClientSide()) { + level.sendBlockUpdated(getBlockPos(), getBlockState(), getBlockState(), 3); + } + } + }; + } + + @Override + public ModEnergyStorage getEnergyStorage(@Nullable Direction direction) { + return this.energyStorage; + } + + @Override + public Component getDisplayName() { + return Component.translatable("block.jurassicrevived.embryonic_machine"); + } + + @Override + public void saveExtraData(FriendlyByteBuf buf) { + buf.writeBlockPos(getBlockPos()); + } + + @Nullable + @Override + public AbstractContainerMenu createMenu(int i, Inventory inventory, Player player) { + return new EmbryonicMachineMenu(i, inventory, this, this.data); + } + + public boolean isEmptyForDrop() { + return itemHandler.isEmpty() && this.progress == 0; + } + + //? if >1.20.1 { + /*@Override + protected void saveAdditional(CompoundTag tag, HolderLookup.Provider registries) { + super.saveAdditional(tag, registries); + tag.put("Inventory", itemHandler.createTag(registries)); + saveCommonData(tag); + } + + @Override + protected void loadAdditional(CompoundTag tag, HolderLookup.Provider registries) { + super.loadAdditional(tag, registries); + itemHandler.fromTag(tag.getList("Inventory", 10), registries); + loadCommonData(tag); + } + *///?} else { + @Override + protected void saveAdditional(CompoundTag tag) { + super.saveAdditional(tag); + tag.put("Inventory", itemHandler.createTag()); + saveCommonData(tag); + } + + @Override + public void load(CompoundTag tag) { + super.load(tag); + itemHandler.fromTag(tag.getList("Inventory", 10)); + loadCommonData(tag); + } + //?} + + private void saveCommonData(CompoundTag tag) { + tag.putInt("Prog", this.progress); + tag.putInt("MaxProg", this.maxProgress); + tag.put("Energy", energyStorage.saveNBT()); + } + + private void loadCommonData(CompoundTag tag) { + progress = tag.getInt("Prog"); + maxProgress = tag.getInt("MaxProg"); + if (tag.contains("Energy")) { + energyStorage.loadNBT(tag.getCompound("Energy")); + } + } + + public void tick(Level level, BlockPos pos, BlockState state) { + if (level.isClientSide) return; + + pullEnergyFromNeighbors(); + + //? if >1.20.1 { + /*Optional> recipeOpt = getCurrentRecipe(); + *///?} else { + Optional recipeOpt = getCurrentRecipe(); + //?} + + if (recipeOpt.isEmpty()) { + resetProgress(); + level.setBlockAndUpdate(pos, state.setValue(EmbryonicMachineBlock.LIT, false)); + this.lockedOutput = ItemStack.EMPTY; + this.lastInputSignature = ""; + return; + } + + String currentSignature = signatureOf(itemHandler.getItem(SYRINGE_SLOT), itemHandler.getItem(MATERIAL_SLOT)); + + //? if >1.20.1 { + /*EmbryonicMachineRecipe recipe = recipeOpt.get().value(); + *///?} else { + EmbryonicMachineRecipe recipe = recipeOpt.get(); + //?} + + if (progress == 0 && (lockedOutput.isEmpty() || !currentSignature.equals(lastInputSignature))) { + lockedOutput = determineOutput(recipe).copy(); + lastInputSignature = currentSignature; + } + + if (!lockedOutput.isEmpty() && canInsertOutput(lockedOutput)) { + if (energyStorage.getEnergyStored() < 10) return; + energyStorage.extractEnergy(10, false); + + progress++; + level.setBlockAndUpdate(pos, state.setValue(EmbryonicMachineBlock.LIT, true)); + + if (progress >= maxProgress) { + craftItem(lockedOutput); + resetProgress(); + level.setBlockAndUpdate(pos, state.setValue(EmbryonicMachineBlock.LIT, false)); + this.lockedOutput = ItemStack.EMPTY; + this.lastInputSignature = ""; + } + } else { + resetProgress(); + level.setBlockAndUpdate(pos, state.setValue(EmbryonicMachineBlock.LIT, false)); + } + } + + private void craftItem(ItemStack output) { + ItemStack current = itemHandler.getItem(OUTPUT_SLOT); + if (current.isEmpty()) { + itemHandler.setItem(OUTPUT_SLOT, output.copy()); + } else { + current.grow(output.getCount()); + } + itemHandler.removeItem(SYRINGE_SLOT, 1); + itemHandler.removeItem(MATERIAL_SLOT, 1); + itemHandler.removeItem(FROG_SLOT, 1); + } + + private boolean canInsertOutput(ItemStack output) { + ItemStack stack = itemHandler.getItem(OUTPUT_SLOT); + //? if >1.20.1 { + /*return stack.isEmpty() || (ItemStack.isSameItemSameComponents(stack, output) && stack.getCount() + output.getCount() <= stack.getMaxStackSize()); + *///?} else { + return stack.isEmpty() || (ItemStack.isSameItemSameTags(stack, output) && stack.getCount() + output.getCount() <= stack.getMaxStackSize()); + //?} + } + + //? if >1.20.1 { + /*private Optional> getCurrentRecipe() { + return level.getRecipeManager().getRecipeFor(ModRecipes.EMBRYONIC_MACHINE_RECIPE_TYPE.get(), + new EmbryonicMachineRecipeInput(itemHandler.getItem(SYRINGE_SLOT), itemHandler.getItem(MATERIAL_SLOT), itemHandler.getItem(FROG_SLOT)), level); + } + *///?} else { + private Optional getCurrentRecipe() { + return level.getRecipeManager().getRecipeFor(ModRecipes.EMBRYONIC_MACHINE_RECIPE_TYPE.get(), + new EmbryonicMachineRecipeInput(itemHandler.getItem(SYRINGE_SLOT), itemHandler.getItem(MATERIAL_SLOT), itemHandler.getItem(FROG_SLOT)), level); + } + //?} + + private ItemStack determineOutput(EmbryonicMachineRecipe recipe) { + ItemStack material = itemHandler.getItem(MATERIAL_SLOT); + if (material.is(ModItems.MOSQUITO_IN_AMBER.get())) { + return pickWeightedRandomDna(recipe); + } + //? if >1.20.1 { + /*return recipe.output().copy(); + *///?} else { + return recipe.getResultItem(level.registryAccess()).copy(); + //?} + } + + private ItemStack pickWeightedRandomDna(EmbryonicMachineRecipe recipe) { + var registry = level.registryAccess().registryOrThrow(Registries.ITEM); + var tagged = registry.getTag(ModTags.Items.DNA); + + //? if >1.20.1 { + /*ItemStack fallback = recipe.output().copy(); + *///?} else { + ItemStack fallback = recipe.getResultItem(level.registryAccess()).copy(); + //?} + + if (tagged.isEmpty()) return fallback; + + int totalWeight = 0; + java.util.List items = new java.util.ArrayList<>(); + java.util.List weights = new java.util.ArrayList<>(); + for (var h : tagged.get()) { + int w = recipe.getWeightFor(h.value()); + if (w > 0) { + items.add(h.value()); + weights.add(w); + totalWeight += w; + } + } + if (totalWeight <= 0) return fallback; + int roll = level.random.nextInt(totalWeight); + int acc = 0; + for (int i = 0; i < items.size(); i++) { + acc += weights.get(i); + if (roll < acc) return new ItemStack(items.get(i), Math.max(1, fallback.getCount())); + } + return fallback; + } + + private String signatureOf(ItemStack s1, ItemStack s2) { + return stackSig(s1) + "#" + stackSig(s2); + } + + private String stackSig(ItemStack s) { + return s.isEmpty() ? "empty" : BuiltInRegistries.ITEM.getKey(s.getItem()) + "x" + s.getCount(); + } + + private void resetProgress() { + this.progress = 0; + this.maxProgress = DEFAULT_MAX_PROGRESS; + } + + private void pullEnergyFromNeighbors() { + for (Direction dir : Direction.values()) { + BlockEntity be = level.getBlockEntity(worldPosition.relative(dir)); + if (be instanceof ModEnergyUtil.EnergyProvider provider) { + ModEnergyStorage source = provider.getEnergyStorage(dir.getOpposite()); + if (source != null && source.canExtract()) { + int accepted = energyStorage.receiveEnergy(TRANSFER_RATE, true); + if (accepted > 0) { + energyStorage.receiveEnergy(source.extractEnergy(accepted, false), false); + } + } + } + } + } + + //? if >1.20.1 { + /*@Override + public CompoundTag getUpdateTag(HolderLookup.Provider registries) { return saveWithoutMetadata(registries); } + *///?} else { + @Override + public CompoundTag getUpdateTag() { return saveWithoutMetadata(); } + //?} + + @Override + public @Nullable Packet getUpdatePacket() { return ClientboundBlockEntityDataPacket.create(this); } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/block/entity/custom/FossilCleanerBlockEntity.java b/common/src/main/java/net/cmr/jurassicrevived/block/entity/custom/FossilCleanerBlockEntity.java new file mode 100644 index 0000000..42ad14b --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/block/entity/custom/FossilCleanerBlockEntity.java @@ -0,0 +1,357 @@ +package net.cmr.jurassicrevived.block.entity.custom; + +import dev.architectury.fluid.FluidStack; +import dev.architectury.registry.menu.ExtendedMenuProvider; +import net.cmr.jurassicrevived.block.custom.FossilCleanerBlock; +import net.cmr.jurassicrevived.block.entity.ModBlockEntities; +import net.cmr.jurassicrevived.block.entity.energy.ModEnergyStorage; +import net.cmr.jurassicrevived.block.entity.energy.ModEnergyUtil; +import net.cmr.jurassicrevived.recipe.FossilCleanerRecipe; +import net.cmr.jurassicrevived.recipe.FossilCleanerRecipeInput; +import net.cmr.jurassicrevived.recipe.ModRecipes; +import net.cmr.jurassicrevived.screen.custom.FossilCleanerMenu; +import net.cmr.jurassicrevived.util.ModTags; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.chat.Component; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.game.ClientGamePacketListener; +import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket; +import net.minecraft.tags.FluidTags; +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ContainerData; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import org.jetbrains.annotations.Nullable; + +//? if >1.20.1 { +/*import net.minecraft.core.HolderLookup; +import net.minecraft.world.item.crafting.RecipeHolder; +*///?} else { +import net.minecraft.core.RegistryAccess; +//?} + +import java.util.Optional; + +public class FossilCleanerBlockEntity extends BlockEntity implements ExtendedMenuProvider, ModEnergyUtil.EnergyProvider { + + public final SimpleContainer itemHandler = new SimpleContainer(5) { + @Override + public void setChanged() { + super.setChanged(); + FossilCleanerBlockEntity.this.setChanged(); + if (level != null && !level.isClientSide()) { + level.sendBlockUpdated(getBlockPos(), getBlockState(), getBlockState(), 3); + } + } + }; + + private static final int WATER_SLOT = 0; + private static final int FOSSILBLOCK_SLOT = 1; + private static final int[] OUTPUT_SLOTS = {2, 3, 4}; + private static final int WATER_CRAFT_AMOUNT = 250; + private static final long TANK_CAPACITY = 16000; + + private FluidStack fluidStack = FluidStack.empty(); + private ItemStack lockedOutput = ItemStack.EMPTY; + private String lastInputSignature = ""; + + private final ContainerData data; + private int progress = 0; + private int maxProgress = 200; + private final int DEFAULT_MAX_PROGRESS = 200; + + private static final int TRANSFER_RATE = 1000; + private final ModEnergyStorage energyStorage = createEnergyStorage(); + + public FossilCleanerBlockEntity(BlockPos pos, BlockState blockState) { + super(ModBlockEntities.FOSSIL_CLEANER_BE.get(), pos, blockState); + this.data = new ContainerData() { + @Override + public int get(int pIndex) { + return switch (pIndex) { + case 0 -> FossilCleanerBlockEntity.this.progress; + case 1 -> FossilCleanerBlockEntity.this.maxProgress; + default -> 0; + }; + } + + @Override + public void set(int pIndex, int pValue) { + switch (pIndex) { + case 0 -> FossilCleanerBlockEntity.this.progress = pValue; + case 1 -> FossilCleanerBlockEntity.this.maxProgress = pValue; + } + } + + @Override + public int getCount() { + return 2; + } + }; + } + + private ModEnergyStorage createEnergyStorage() { + return new ModEnergyStorage(64000, TRANSFER_RATE) { + @Override + public void onEnergyChanged() { + setChanged(); + if (level != null && !level.isClientSide()) { + level.sendBlockUpdated(getBlockPos(), getBlockState(), getBlockState(), 3); + } + } + }; + } + + @Override + public ModEnergyStorage getEnergyStorage(@Nullable Direction direction) { + return this.energyStorage; + } + + public FluidStack getFluid() { + return fluidStack; + } + + @Override + public Component getDisplayName() { + return Component.translatable("block.jurassicrevived.fossil_cleaner"); + } + + @Override + public void saveExtraData(FriendlyByteBuf buf) { + buf.writeBlockPos(getBlockPos()); + } + + @Nullable + @Override + public AbstractContainerMenu createMenu(int i, Inventory inventory, Player player) { + return new FossilCleanerMenu(i, inventory, this, this.data); + } + + public boolean isEmptyForDrop() { + return itemHandler.isEmpty() && fluidStack.isEmpty() && progress == 0; + } + + //? if >1.20.1 { + /*@Override + protected void saveAdditional(CompoundTag tag, HolderLookup.Provider registries) { + super.saveAdditional(tag, registries); + tag.put("Inventory", itemHandler.createTag(registries)); + tag.putInt("Prog", this.progress); + tag.putInt("MaxProg", this.maxProgress); + tag.put("Energy", energyStorage.saveNBT()); + CompoundTag fluidTag = new CompoundTag(); + fluidStack.write(registries, fluidTag); + tag.put("Fluid", fluidTag); + } + + @Override + protected void loadAdditional(CompoundTag tag, HolderLookup.Provider registries) { + super.loadAdditional(tag, registries); + itemHandler.fromTag(tag.getList("Inventory", 10), registries); + progress = tag.getInt("Prog"); + maxProgress = tag.getInt("MaxProg"); + if (tag.contains("Energy")) energyStorage.loadNBT(tag.getCompound("Energy")); + if (tag.contains("Fluid")) fluidStack = FluidStack.read(registries, tag.getCompound("Fluid")).orElse(FluidStack.empty()); + } + *///?} else { + @Override + protected void saveAdditional(CompoundTag tag) { + super.saveAdditional(tag); + tag.put("Inventory", itemHandler.createTag()); + tag.putInt("Prog", this.progress); + tag.putInt("MaxProg", this.maxProgress); + tag.put("Energy", energyStorage.saveNBT()); + CompoundTag fluidTag = new CompoundTag(); + fluidStack.write(fluidTag); + tag.put("Fluid", fluidTag); + } + + @Override + public void load(CompoundTag tag) { + super.load(tag); + itemHandler.fromTag(tag.getList("Inventory", 10)); + progress = tag.getInt("Prog"); + maxProgress = tag.getInt("MaxProg"); + if (tag.contains("Energy")) energyStorage.loadNBT(tag.getCompound("Energy")); + if (tag.contains("Fluid")) fluidStack = FluidStack.read(tag.getCompound("Fluid")); + } + //?} + + public void tick(Level level, BlockPos pos, BlockState state) { + if (level.isClientSide) return; + + pullEnergyFromNeighbors(); + handleBucketInput(); + + //? if >1.20.1 { + /*Optional> recipeOpt = getCurrentRecipe(); + *///?} else { + Optional recipeOpt = getCurrentRecipe(); + //?} + + if (recipeOpt.isEmpty()) { + resetProgress(); + level.setBlockAndUpdate(pos, state.setValue(FossilCleanerBlock.LIT, false)); + return; + } + + String currentSignature = stackSig(itemHandler.getItem(FOSSILBLOCK_SLOT)); + if (progress == 0 && (lockedOutput.isEmpty() || !currentSignature.equals(lastInputSignature))) { + //? if >1.20.1 { + /*lockedOutput = determineOutput(recipeOpt.get().value()).copy(); + *///?} else { + lockedOutput = determineOutput(recipeOpt.get()).copy(); + //?} + lastInputSignature = currentSignature; + } + + if (!lockedOutput.isEmpty() && canInsertOutput(lockedOutput) && fluidStack.getAmount() >= WATER_CRAFT_AMOUNT) { + if (energyStorage.getEnergyStored() < 10) return; + energyStorage.extractEnergy(10, false); + + progress++; + level.setBlockAndUpdate(pos, state.setValue(FossilCleanerBlock.LIT, true)); + + if (progress >= maxProgress) { + craftItem(lockedOutput); + fluidStack.setAmount(fluidStack.getAmount() - WATER_CRAFT_AMOUNT); + resetProgress(); + level.setBlockAndUpdate(pos, state.setValue(FossilCleanerBlock.LIT, false)); + } + } else { + resetProgress(); + level.setBlockAndUpdate(pos, state.setValue(FossilCleanerBlock.LIT, false)); + } + } + + private void handleBucketInput() { + ItemStack stack = itemHandler.getItem(WATER_SLOT); + if (stack.is(Items.WATER_BUCKET) && (TANK_CAPACITY - fluidStack.getAmount() >= 1000)) { + if (fluidStack.isEmpty() || fluidStack.getFluid().is(FluidTags.WATER)) { + fluidStack = FluidStack.create(net.minecraft.world.level.material.Fluids.WATER, fluidStack.getAmount() + 1000); + itemHandler.setItem(WATER_SLOT, new ItemStack(Items.BUCKET)); + setChanged(); + } + } + } + + private void craftItem(ItemStack output) { + for (int slot : OUTPUT_SLOTS) { + ItemStack stack = itemHandler.getItem(slot); + if (stack.isEmpty()) { + itemHandler.setItem(slot, output.copy()); + itemHandler.removeItem(FOSSILBLOCK_SLOT, 1); + return; + } else if (isSameItem(stack, output) && stack.getCount() + output.getCount() <= stack.getMaxStackSize()) { + stack.grow(output.getCount()); + itemHandler.removeItem(FOSSILBLOCK_SLOT, 1); + return; + } + } + } + + private boolean canInsertOutput(ItemStack output) { + for (int slot : OUTPUT_SLOTS) { + ItemStack stack = itemHandler.getItem(slot); + if (stack.isEmpty() || (isSameItem(stack, output) && stack.getCount() + output.getCount() <= stack.getMaxStackSize())) return true; + } + return false; + } + + private boolean isSameItem(ItemStack stack, ItemStack other) { + //? if >1.20.1 { + /*return ItemStack.isSameItemSameComponents(stack, other); + *///?} else { + return ItemStack.isSameItemSameTags(stack, other); + //?} + } + + //? if >1.20.1 { + /*private Optional> getCurrentRecipe() { + return level.getRecipeManager().getRecipeFor(ModRecipes.FOSSIL_CLEANER_RECIPE_TYPE.get(), new FossilCleanerRecipeInput(itemHandler.getItem(FOSSILBLOCK_SLOT), itemHandler.getItem(WATER_SLOT)), level); + } + *///?} else { + private Optional getCurrentRecipe() { + return level.getRecipeManager().getRecipeFor(ModRecipes.FOSSIL_CLEANER_RECIPE_TYPE.get(), new FossilCleanerRecipeInput(itemHandler.getItem(FOSSILBLOCK_SLOT), itemHandler.getItem(WATER_SLOT)), level); + } + //?} + + private ItemStack determineOutput(FossilCleanerRecipe recipe) { + var registry = level.registryAccess().registryOrThrow(Registries.ITEM); + var tagged = registry.getTag(ModTags.Items.FOSSILS); + + //? if >1.20.1 { + /*ItemStack fallback = recipe.output().copy(); + *///?} else { + ItemStack fallback = recipe.getResultItem(level.registryAccess()).copy(); + //?} + + if (tagged.isEmpty()) return fallback; + + int total = 0; + java.util.List items = new java.util.ArrayList<>(); + java.util.List weights = new java.util.ArrayList<>(); + for (var h : tagged.get()) { + int w = recipe.getWeightFor(h.value()); + if (w > 0) { + items.add(h.value()); + weights.add(w); + total += w; + } + } + if (total <= 0) return fallback; + int roll = level.random.nextInt(total); + int acc = 0; + for (int i = 0; i < items.size(); i++) { + acc += weights.get(i); + if (roll < acc) return new ItemStack(items.get(i), Math.max(1, fallback.getCount())); + } + return fallback; + } + + private String stackSig(ItemStack s) { + return s.isEmpty() ? "empty" : BuiltInRegistries.ITEM.getKey(s.getItem()).toString() + ":" + s.getCount(); + } + + private void resetProgress() { + this.progress = 0; + this.maxProgress = DEFAULT_MAX_PROGRESS; + } + + private void pullEnergyFromNeighbors() { + for (Direction dir : Direction.values()) { + BlockEntity be = level.getBlockEntity(worldPosition.relative(dir)); + if (be instanceof ModEnergyUtil.EnergyProvider provider) { + ModEnergyStorage source = provider.getEnergyStorage(dir.getOpposite()); + if (source != null && source.canExtract()) { + int accepted = energyStorage.receiveEnergy(TRANSFER_RATE, true); + if (accepted > 0) { + energyStorage.receiveEnergy(source.extractEnergy(accepted, false), false); + } + } + } + } + } + + //? if >1.20.1 { + /*@Override + public CompoundTag getUpdateTag(HolderLookup.Provider registries) { return saveWithoutMetadata(registries); } + *///?} else { + @Override + public CompoundTag getUpdateTag() { return saveWithoutMetadata(); } + //?} + + @Override + public @Nullable Packet getUpdatePacket() { return ClientboundBlockEntityDataPacket.create(this); } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/block/entity/custom/FossilGrinderBlockEntity.java b/common/src/main/java/net/cmr/jurassicrevived/block/entity/custom/FossilGrinderBlockEntity.java new file mode 100644 index 0000000..7b7c4de --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/block/entity/custom/FossilGrinderBlockEntity.java @@ -0,0 +1,353 @@ +package net.cmr.jurassicrevived.block.entity.custom; + +import dev.architectury.registry.menu.ExtendedMenuProvider; +import net.cmr.jurassicrevived.block.custom.FossilGrinderBlock; +import net.cmr.jurassicrevived.block.entity.ModBlockEntities; +import net.cmr.jurassicrevived.block.entity.energy.ModEnergyStorage; +import net.cmr.jurassicrevived.block.entity.energy.ModEnergyUtil; +import net.cmr.jurassicrevived.recipe.FossilGrinderRecipe; +import net.cmr.jurassicrevived.recipe.FossilGrinderRecipeInput; +import net.cmr.jurassicrevived.recipe.ModRecipes; +import net.cmr.jurassicrevived.screen.custom.FossilGrinderMenu; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.chat.Component; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.game.ClientGamePacketListener; +import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ContainerData; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import org.jetbrains.annotations.Nullable; + +//? if >1.20.1 { +/*import net.minecraft.core.HolderLookup; +import net.minecraft.world.item.crafting.RecipeHolder; +*///?} else { +import net.minecraft.core.RegistryAccess; +//?} + +import java.util.Optional; + +public class FossilGrinderBlockEntity extends BlockEntity implements ExtendedMenuProvider, ModEnergyUtil.EnergyProvider { + + public final SimpleContainer itemHandler = new SimpleContainer(4) { + @Override + public void setChanged() { + super.setChanged(); + FossilGrinderBlockEntity.this.setChanged(); + if (level != null && !level.isClientSide()) { + level.sendBlockUpdated(getBlockPos(), getBlockState(), getBlockState(), 3); + } + } + }; + + private static final int FOSSIL_SLOT = 0; + private static final int[] OUTPUT_SLOTS = {1, 2, 3}; + + private ItemStack lockedOutput = ItemStack.EMPTY; + private String lastInputSignature = ""; + + private final ContainerData data; + private int progress = 0; + private int maxProgress = 100; + private final int DEFAULT_MAX_PROGRESS = 100; + + private static final int TRANSFER_RATE = 1000; + private final ModEnergyStorage energyStorage = createEnergyStorage(); + + public FossilGrinderBlockEntity(BlockPos pos, BlockState blockState) { + super(ModBlockEntities.FOSSIL_GRINDER_BE.get(), pos, blockState); + this.data = new ContainerData() { + @Override + public int get(int pIndex) { + return switch (pIndex) { + case 0 -> FossilGrinderBlockEntity.this.progress; + case 1 -> FossilGrinderBlockEntity.this.maxProgress; + default -> 0; + }; + } + + @Override + public void set(int pIndex, int pValue) { + switch (pIndex) { + case 0 -> FossilGrinderBlockEntity.this.progress = pValue; + case 1 -> FossilGrinderBlockEntity.this.maxProgress = pValue; + } + } + + @Override + public int getCount() { + return 2; + } + }; + } + + private ModEnergyStorage createEnergyStorage() { + return new ModEnergyStorage(64000, TRANSFER_RATE) { + @Override + public void onEnergyChanged() { + setChanged(); + if (level != null && !level.isClientSide()) { + level.sendBlockUpdated(getBlockPos(), getBlockState(), getBlockState(), 3); + } + } + }; + } + + @Override + public ModEnergyStorage getEnergyStorage(@Nullable Direction direction) { + return this.energyStorage; + } + + @Override + public Component getDisplayName() { + return Component.translatable("block.jurassicrevived.fossil_grinder"); + } + + @Override + public void saveExtraData(FriendlyByteBuf buf) { + buf.writeBlockPos(getBlockPos()); + } + + @Nullable + @Override + public AbstractContainerMenu createMenu(int i, Inventory inventory, Player player) { + return new FossilGrinderMenu(i, inventory, this, this.data); + } + + public boolean isEmptyForDrop() { + return itemHandler.isEmpty() && this.progress == 0; + } + + //? if >1.20.1 { + /*@Override + protected void saveAdditional(CompoundTag tag, HolderLookup.Provider registries) { + super.saveAdditional(tag, registries); + tag.put("Inventory", itemHandler.createTag(registries)); + saveCommonData(tag); + } + + @Override + protected void loadAdditional(CompoundTag tag, HolderLookup.Provider registries) { + super.loadAdditional(tag, registries); + itemHandler.fromTag(tag.getList("Inventory", 10), registries); + loadCommonData(tag); + } + *///?} else { + @Override + protected void saveAdditional(CompoundTag tag) { + super.saveAdditional(tag); + tag.put("Inventory", itemHandler.createTag()); + saveCommonData(tag); + } + + @Override + public void load(CompoundTag tag) { + super.load(tag); + itemHandler.fromTag(tag.getList("Inventory", 10)); + loadCommonData(tag); + } + //?} + + private void saveCommonData(CompoundTag tag) { + tag.putInt("Prog", this.progress); + tag.putInt("MaxProg", this.maxProgress); + tag.put("Energy", energyStorage.saveNBT()); + } + + private void loadCommonData(CompoundTag tag) { + progress = tag.getInt("Prog"); + maxProgress = tag.getInt("MaxProg"); + if (tag.contains("Energy")) { + energyStorage.loadNBT(tag.getCompound("Energy")); + } + } + + public void tick(Level level, BlockPos pos, BlockState state) { + if (level.isClientSide) return; + + pullEnergyFromNeighbors(); + + //? if >1.20.1 { + /*Optional> recipeOpt = getCurrentRecipe(); + *///?} else { + Optional recipeOpt = getCurrentRecipe(); + //?} + + if (recipeOpt.isEmpty()) { + resetProgress(); + level.setBlockAndUpdate(pos, state.setValue(FossilGrinderBlock.LIT, false)); + this.lockedOutput = ItemStack.EMPTY; + this.lastInputSignature = ""; + return; + } + + String currentSignature = stackSig(itemHandler.getItem(FOSSIL_SLOT)); + + //? if >1.20.1 { + /*FossilGrinderRecipe recipe = recipeOpt.get().value(); + *///?} else { + FossilGrinderRecipe recipe = recipeOpt.get(); + //?} + + if (progress == 0) { + if (lockedOutput.isEmpty() || !currentSignature.equals(lastInputSignature)) { + lockedOutput = determineOutputForCurrentInputs(recipe).copy(); + lastInputSignature = currentSignature; + } + } + + ItemStack output = lockedOutput; + boolean canOutput = !output.isEmpty() && canInsertOutput(output); + + if (canOutput) { + if (energyStorage.getEnergyStored() < 10) return; + energyStorage.extractEnergy(10, false); + + progress++; + level.setBlockAndUpdate(pos, state.setValue(FossilGrinderBlock.LIT, true)); + + if (progress >= maxProgress) { + craftItem(output); + resetProgress(); + level.setBlockAndUpdate(pos, state.setValue(FossilGrinderBlock.LIT, false)); + this.lockedOutput = ItemStack.EMPTY; + this.lastInputSignature = ""; + } + } else { + resetProgress(); + level.setBlockAndUpdate(pos, state.setValue(FossilGrinderBlock.LIT, false)); + } + } + + private void craftItem(ItemStack output) { + for (int slot : OUTPUT_SLOTS) { + ItemStack stack = itemHandler.getItem(slot); + if (stack.isEmpty()) { + itemHandler.setItem(slot, output.copy()); + itemHandler.removeItem(FOSSIL_SLOT, 1); + return; + } else if (isSameItem(stack, output) && stack.getCount() + output.getCount() <= stack.getMaxStackSize()) { + stack.grow(output.getCount()); + itemHandler.removeItem(FOSSIL_SLOT, 1); + return; + } + } + } + + private boolean canInsertOutput(ItemStack output) { + for (int slot : OUTPUT_SLOTS) { + ItemStack stack = itemHandler.getItem(slot); + if (stack.isEmpty() || (isSameItem(stack, output) && stack.getCount() + output.getCount() <= stack.getMaxStackSize())) { + return true; + } + } + return false; + } + + private boolean isSameItem(ItemStack stack, ItemStack other) { + //? if >1.20.1 { + /*return ItemStack.isSameItemSameComponents(stack, other); + *///?} else { + return ItemStack.isSameItemSameTags(stack, other); + //?} + } + + //? if >1.20.1 { + /*private Optional> getCurrentRecipe() { + return level.getRecipeManager().getRecipeFor(ModRecipes.FOSSIL_GRINDER_RECIPE_TYPE.get(), + new FossilGrinderRecipeInput(itemHandler.getItem(FOSSIL_SLOT)), level); + } + *///?} else { + private Optional getCurrentRecipe() { + return level.getRecipeManager().getRecipeFor(ModRecipes.FOSSIL_GRINDER_RECIPE_TYPE.get(), + new FossilGrinderRecipeInput(itemHandler.getItem(FOSSIL_SLOT)), level); + } + //?} + + private ItemStack determineOutputForCurrentInputs(FossilGrinderRecipe recipe) { + if (!recipe.weights().isEmpty()) { + return pickWeighted(recipe); + } + //? if >1.20.1 { + /*return recipe.output().copy(); + *///?} else { + return recipe.getResultItem(level.registryAccess()).copy(); + //?} + } + + private ItemStack pickWeighted(FossilGrinderRecipe recipe) { + java.util.Map map = recipe.weights(); + int total = map.values().stream().mapToInt(Integer::intValue).sum(); + if (total <= 0) return ItemStack.EMPTY; + + int roll = level.random.nextInt(total); + int acc = 0; + for (var entry : map.entrySet()) { + acc += entry.getValue(); + if (roll < acc) { + var item = BuiltInRegistries.ITEM.get(entry.getKey()); + //? if >1.20.1 { + /*int count = recipe.output().getCount(); + *///?} else { + int count = recipe.getResultItem(level.registryAccess()).getCount(); + //?} + return item != null ? new ItemStack(item, Math.max(1, count)) : ItemStack.EMPTY; + } + } + return ItemStack.EMPTY; + } + + private String stackSig(ItemStack s) { + return s.isEmpty() ? "empty" : BuiltInRegistries.ITEM.getKey(s.getItem()) + "x" + s.getCount(); + } + + private void resetProgress() { + this.progress = 0; + this.maxProgress = DEFAULT_MAX_PROGRESS; + } + + private void pullEnergyFromNeighbors() { + for (Direction dir : Direction.values()) { + BlockEntity be = level.getBlockEntity(worldPosition.relative(dir)); + if (be instanceof ModEnergyUtil.EnergyProvider provider) { + ModEnergyStorage source = provider.getEnergyStorage(dir.getOpposite()); + if (source != null && source.canExtract()) { + int accepted = energyStorage.receiveEnergy(TRANSFER_RATE, true); + if (accepted > 0) { + int extracted = source.extractEnergy(accepted, false); + energyStorage.receiveEnergy(extracted, false); + } + } + } + } + } + + //? if >1.20.1 { + /*@Override + public CompoundTag getUpdateTag(HolderLookup.Provider registries) { + return saveWithoutMetadata(registries); + } + *///?} else { + @Override + public CompoundTag getUpdateTag() { + return saveWithoutMetadata(); + } + //?} + + @Override + public @Nullable Packet getUpdatePacket() { + return ClientboundBlockEntityDataPacket.create(this); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/block/entity/custom/GeneratorBlockEntity.java b/common/src/main/java/net/cmr/jurassicrevived/block/entity/custom/GeneratorBlockEntity.java new file mode 100644 index 0000000..a452d8a --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/block/entity/custom/GeneratorBlockEntity.java @@ -0,0 +1,264 @@ +package net.cmr.jurassicrevived.block.entity.custom; + +import dev.architectury.registry.menu.ExtendedMenuProvider; +import net.cmr.jurassicrevived.block.custom.GeneratorBlock; +import net.cmr.jurassicrevived.block.entity.ModBlockEntities; +import net.cmr.jurassicrevived.block.entity.energy.ModEnergyStorage; +import net.cmr.jurassicrevived.block.entity.energy.ModEnergyUtil; +import net.cmr.jurassicrevived.screen.custom.GeneratorMenu; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.chat.Component; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.game.ClientGamePacketListener; +import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket; +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ContainerData; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.AbstractFurnaceBlockEntity; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import org.jetbrains.annotations.Nullable; + +//? if >1.20.1 { +/*import net.minecraft.core.HolderLookup; + *///?} + +public class GeneratorBlockEntity extends BlockEntity implements ExtendedMenuProvider, ModEnergyUtil.EnergyProvider { + + public final SimpleContainer itemHandler = new SimpleContainer(1) { + @Override + public void setChanged() { + super.setChanged(); + GeneratorBlockEntity.this.setChanged(); + if (level != null && !level.isClientSide()) { + level.sendBlockUpdated(getBlockPos(), getBlockState(), getBlockState(), 3); + } + } + }; + + public static final int INPUT_SLOT = 0; + protected final ContainerData data; + private int burnTime = 0; + private int burnTimeTotal = 0; + private boolean isBurning = false; + + private static final int TRANSFER_RATE = 1000; + + private final ModEnergyStorage energyStorage = createEnergyStorage(); + + public GeneratorBlockEntity(BlockPos pos, BlockState blockState) { + super(ModBlockEntities.GENERATOR_BE.get(), pos, blockState); + this.data = new ContainerData() { + @Override + public int get(int i) { + return switch (i) { + case 0 -> GeneratorBlockEntity.this.burnTime; + case 1 -> GeneratorBlockEntity.this.burnTimeTotal; + default -> 0; + }; + } + + @Override + public void set(int i, int i1) { + switch (i) { + case 0 -> GeneratorBlockEntity.this.burnTime = i1; + case 1 -> GeneratorBlockEntity.this.burnTimeTotal = i1; + } + } + + @Override + public int getCount() { + return 2; + } + }; + } + + private ModEnergyStorage createEnergyStorage() { + return new ModEnergyStorage(256000, 0, TRANSFER_RATE, 0) { + @Override + public void onEnergyChanged() { + setChanged(); + if (level != null && !level.isClientSide()) { + level.sendBlockUpdated(getBlockPos(), getBlockState(), getBlockState(), 3); + } + } + }; + } + + @Override + public ModEnergyStorage getEnergyStorage(@Nullable Direction direction) { + return this.energyStorage; + } + + @Override + public Component getDisplayName() { + return Component.translatable("block.jurassicrevived.generator"); + } + + @Override + public void saveExtraData(FriendlyByteBuf buf) { + buf.writeBlockPos(getBlockPos()); + } + + @Nullable + @Override + public AbstractContainerMenu createMenu(int i, Inventory inventory, Player player) { + return new GeneratorMenu(i, inventory, this, this.data); + } + + public boolean isEmptyForDrop() { + return itemHandler.isEmpty() && !this.isBurning; + } + + public void tick(Level level1, BlockPos blockPos, BlockState blockState) { + if (level1.isClientSide) return; + + boolean storageFull = this.energyStorage.getEnergyStored() >= this.energyStorage.getMaxEnergyStored(); + + if (!storageFull && !isBurningFuel() && hasFuelItemInSlot()) { + startBurning(); + } + + if (isBurningFuel() && !storageFull) { + int space = this.energyStorage.getMaxEnergyStored() - this.energyStorage.getEnergyStored(); + int toAdd = Math.min(100, Math.max(0, space)); + if (toAdd > 0) { + this.energyStorage.setEnergy(this.energyStorage.getEnergyStored() + toAdd); + } + + this.burnTime++; + if (this.burnTime >= this.burnTimeTotal) { + this.isBurning = false; + } + } else if (!isBurningFuel() && this.burnTime >= this.burnTimeTotal && this.burnTimeTotal > 0) { + this.burnTime = 0; + this.burnTimeTotal = 0; + } + + pushEnergyToNeighbors(); + + boolean shouldBeLit = isBurningFuel() && !storageFull; + if (blockState.hasProperty(GeneratorBlock.LIT) && blockState.getValue(GeneratorBlock.LIT) != shouldBeLit) { + level1.setBlockAndUpdate(blockPos, blockState.setValue(GeneratorBlock.LIT, shouldBeLit)); + } + } + + private void pushEnergyToNeighbors() { + if (this.energyStorage.getEnergyStored() <= 0) return; + + for (Direction dir : Direction.values()) { + BlockEntity be = level.getBlockEntity(worldPosition.relative(dir)); + if (be instanceof ModEnergyUtil.EnergyProvider provider) { + ModEnergyStorage target = provider.getEnergyStorage(dir.getOpposite()); + if (target != null && target.canReceive()) { + int toSend = Math.min(TRANSFER_RATE, this.energyStorage.getEnergyStored()); + int accepted = target.receiveEnergy(toSend, true); + if (accepted > 0) { + int actuallyExtracted = this.energyStorage.extractEnergy(accepted, false); + target.receiveEnergy(actuallyExtracted, false); + } + } + } + } + } + + private boolean isBurningFuel() { + return this.isBurning && this.burnTimeTotal > 0 && this.burnTime < this.burnTimeTotal; + } + + private void startBurning() { + ItemStack stack = this.itemHandler.getItem(INPUT_SLOT); + //? if >1.20.1 { + /*int burn = AbstractFurnaceBlockEntity.getFuel().getOrDefault(stack.getItem(), 0); + *///?} else { + int burn = AbstractFurnaceBlockEntity.getFuel().getOrDefault(stack.getItem(), 0); + //?} + if (burn <= 0) return; + + stack.shrink(1); + this.burnTimeTotal = burn; + this.burnTime = 0; + this.isBurning = true; + setChanged(); + } + + private boolean hasFuelItemInSlot() { + ItemStack stack = this.itemHandler.getItem(INPUT_SLOT); + if (stack.isEmpty()) return false; + //? if >1.20.1 { + /*return AbstractFurnaceBlockEntity.getFuel().getOrDefault(stack.getItem(), 0) > 0; + *///?} else { + return AbstractFurnaceBlockEntity.getFuel().getOrDefault(stack.getItem(), 0) > 0; + //?} + } + + //? if >1.20.1 { + /*@Override + protected void saveAdditional(CompoundTag tag, HolderLookup.Provider registries) { + super.saveAdditional(tag, registries); + tag.put("Inventory", itemHandler.createTag(registries)); + saveCommonData(tag); + } + + @Override + protected void loadAdditional(CompoundTag tag, HolderLookup.Provider registries) { + super.loadAdditional(tag, registries); + itemHandler.fromTag(tag.getList("Inventory", 10), registries); + loadCommonData(tag); + } + *///?} else { + @Override + protected void saveAdditional(CompoundTag tag) { + super.saveAdditional(tag); + tag.put("Inventory", itemHandler.createTag()); + saveCommonData(tag); + } + + @Override + public void load(CompoundTag tag) { + super.load(tag); + itemHandler.fromTag(tag.getList("Inventory", 10)); + loadCommonData(tag); + } + //?} + + private void saveCommonData(CompoundTag tag) { + tag.putInt("BurnTime", this.burnTime); + tag.putInt("BurnTotal", this.burnTimeTotal); + tag.putBoolean("IsBurning", this.isBurning); + tag.put("Energy", energyStorage.saveNBT()); + } + + private void loadCommonData(CompoundTag tag) { + if (tag.contains("Energy")) { + energyStorage.loadNBT(tag.getCompound("Energy")); + } + this.burnTime = tag.getInt("BurnTime"); + this.burnTimeTotal = tag.getInt("BurnTotal"); + this.isBurning = tag.getBoolean("IsBurning"); + } + + @Override + public @Nullable Packet getUpdatePacket() { + return ClientboundBlockEntityDataPacket.create(this); + } + + //? if >1.20.1 { + /*@Override + public CompoundTag getUpdateTag(HolderLookup.Provider registries) { + return saveWithoutMetadata(registries); + } + *///?} else { + @Override + public CompoundTag getUpdateTag() { + return saveWithoutMetadata(); + } + //?} +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/block/entity/custom/IncubatorBlockEntity.java b/common/src/main/java/net/cmr/jurassicrevived/block/entity/custom/IncubatorBlockEntity.java new file mode 100644 index 0000000..216d079 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/block/entity/custom/IncubatorBlockEntity.java @@ -0,0 +1,305 @@ +package net.cmr.jurassicrevived.block.entity.custom; + +import dev.architectury.registry.menu.ExtendedMenuProvider; +import net.cmr.jurassicrevived.block.custom.IncubatorBlock; +import net.cmr.jurassicrevived.block.entity.ModBlockEntities; +import net.cmr.jurassicrevived.block.entity.energy.ModEnergyStorage; +import net.cmr.jurassicrevived.block.entity.energy.ModEnergyUtil; +import net.cmr.jurassicrevived.recipe.IncubatorRecipe; +import net.cmr.jurassicrevived.recipe.IncubatorRecipeInput; +import net.cmr.jurassicrevived.recipe.ModRecipes; +import net.cmr.jurassicrevived.screen.custom.IncubatorMenu; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.chat.Component; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.game.ClientGamePacketListener; +import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket; +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ContainerData; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import org.jetbrains.annotations.Nullable; + +//? if >1.20.1 { +/*import net.minecraft.core.HolderLookup; +import net.minecraft.world.item.crafting.RecipeHolder; +*///?} else { +import net.minecraft.core.RegistryAccess; +//?} + +import java.util.Optional; + +public class IncubatorBlockEntity extends BlockEntity implements ExtendedMenuProvider, ModEnergyUtil.EnergyProvider { + + public final SimpleContainer itemHandler = new SimpleContainer(3) { + @Override + public void setChanged() { + super.setChanged(); + IncubatorBlockEntity.this.setChanged(); + if (level != null && !level.isClientSide()) { + level.sendBlockUpdated(getBlockPos(), getBlockState(), getBlockState(), 3); + } + } + }; + + private final ContainerData data; + private final int[] progress = new int[]{0, 0, 0}; + private final int[] maxProgress = new int[]{4800, 4800, 4800}; + private final int DEFAULT_MAX_PROGRESS = 4800; + + private static final int TRANSFER_RATE = 1000; + + private final ModEnergyStorage energyStorage = createEnergyStorage(); + + public IncubatorBlockEntity(BlockPos pos, BlockState blockState) { + super(ModBlockEntities.INCUBATOR_BE.get(), pos, blockState); + this.data = new ContainerData() { + @Override + public int get(int pIndex) { + return switch (pIndex) { + case 0 -> progress[0]; + case 1 -> maxProgress[0]; + case 2 -> progress[1]; + case 3 -> maxProgress[1]; + case 4 -> progress[2]; + case 5 -> maxProgress[2]; + default -> 0; + }; + } + + @Override + public void set(int pIndex, int pValue) { + switch (pIndex) { + case 0 -> progress[0] = pValue; + case 1 -> maxProgress[0] = pValue; + case 2 -> progress[1] = pValue; + case 3 -> maxProgress[1] = pValue; + case 4 -> progress[2] = pValue; + case 5 -> maxProgress[2] = pValue; + } + } + + @Override + public int getCount() { + return 6; + } + }; + } + + private ModEnergyStorage createEnergyStorage() { + return new ModEnergyStorage(64000, TRANSFER_RATE) { + @Override + public void onEnergyChanged() { + setChanged(); + if (level != null && !level.isClientSide()) { + level.sendBlockUpdated(getBlockPos(), getBlockState(), getBlockState(), 3); + } + } + }; + } + + @Override + public ModEnergyStorage getEnergyStorage(@Nullable Direction direction) { + return this.energyStorage; + } + + @Override + public Component getDisplayName() { + return Component.translatable("block.jurassicrevived.incubator"); + } + + @Override + public void saveExtraData(FriendlyByteBuf buf) { + buf.writeBlockPos(getBlockPos()); + } + + public boolean isEmptyForDrop() { + return itemHandler.isEmpty() && progress[0] == 0 && progress[1] == 0 && progress[2] == 0; + } + + @Nullable + @Override + public AbstractContainerMenu createMenu(int i, Inventory inventory, Player player) { + return new IncubatorMenu(i, inventory, this, this.data); + } + + //? if >1.20.1 { + /*@Override + protected void saveAdditional(CompoundTag tag, HolderLookup.Provider registries) { + super.saveAdditional(tag, registries); + tag.put("Inventory", itemHandler.createTag(registries)); + tag.putInt("Prog0", this.progress[0]); + tag.putInt("Prog1", this.progress[1]); + tag.putInt("Prog2", this.progress[2]); + tag.putInt("Max0", this.maxProgress[0]); + tag.putInt("Max1", this.maxProgress[1]); + tag.putInt("Max2", this.maxProgress[2]); + tag.put("Energy", energyStorage.saveNBT()); + } + + @Override + protected void loadAdditional(CompoundTag tag, HolderLookup.Provider registries) { + super.loadAdditional(tag, registries); + itemHandler.fromTag(tag.getList("Inventory", 10), registries); + if (tag.contains("Energy")) { + energyStorage.loadNBT(tag.getCompound("Energy")); + } + progress[0] = tag.getInt("Prog0"); + progress[1] = tag.getInt("Prog1"); + progress[2] = tag.getInt("Prog2"); + maxProgress[0] = tag.getInt("Max0"); + maxProgress[1] = tag.getInt("Max1"); + maxProgress[2] = tag.getInt("Max2"); + } + *///?} else { + @Override + protected void saveAdditional(CompoundTag tag) { + super.saveAdditional(tag); + tag.put("Inventory", itemHandler.createTag()); + tag.putInt("Prog0", this.progress[0]); + tag.putInt("Prog1", this.progress[1]); + tag.putInt("Prog2", this.progress[2]); + tag.putInt("Max0", this.maxProgress[0]); + tag.putInt("Max1", this.maxProgress[1]); + tag.putInt("Max2", this.maxProgress[2]); + tag.put("Energy", energyStorage.saveNBT()); + } + + @Override + public void load(CompoundTag tag) { + super.load(tag); + itemHandler.fromTag(tag.getList("Inventory", 10)); + if (tag.contains("Energy")) { + energyStorage.loadNBT(tag.getCompound("Energy")); + } + progress[0] = tag.getInt("Prog0"); + progress[1] = tag.getInt("Prog1"); + progress[2] = tag.getInt("Prog2"); + maxProgress[0] = tag.getInt("Max0"); + maxProgress[1] = tag.getInt("Max1"); + maxProgress[2] = tag.getInt("Max2"); + } + //?} + + public void tick(Level level, BlockPos pos, BlockState state) { + if (level.isClientSide) return; + + pullEnergyFromNeighbors(); + + boolean changed = false; + boolean anyActive = false; + + for (int s = 0; s < 3; s++) { + ItemStack stack = itemHandler.getItem(s); + if (stack.isEmpty()) { + if (progress[s] != 0) { progress[s] = 0; changed = true; } + continue; + } + + //? if >1.20.1 { + /*Optional> recipeOpt = getRecipeFor(stack); + *///?} else { + Optional recipeOpt = getRecipeFor(stack); + //?} + if (recipeOpt.isEmpty()) { + if (progress[s] != 0) { progress[s] = 0; changed = true; } + continue; + } + anyActive = true; + } + + if (state.getValue(IncubatorBlock.LIT) != anyActive) { + level.setBlockAndUpdate(pos, state.setValue(IncubatorBlock.LIT, anyActive)); + } + + if (anyActive) { + if (energyStorage.getEnergyStored() < 10) return; + energyStorage.extractEnergy(10, false); + } + + for (int s = 0; s < 3; s++) { + ItemStack stack = itemHandler.getItem(s); + if (stack.isEmpty()) continue; + + //? if >1.20.1 { + /*Optional> recipeOpt = getRecipeFor(stack); + *///?} else { + Optional recipeOpt = getRecipeFor(stack); + //?} + if (recipeOpt.isEmpty()) continue; + + if (progress[s] < maxProgress[s]) { + progress[s]++; + changed = true; + } + + if (progress[s] >= maxProgress[s]) { + //? if >1.20.1 { + /*ItemStack out = recipeOpt.get().value().assemble(new IncubatorRecipeInput(stack), level.registryAccess()); + *///?} else { + ItemStack out = recipeOpt.get().assemble(new IncubatorRecipeInput(stack), level.registryAccess()); + //?} + if (!out.isEmpty()) { + itemHandler.setItem(s, out.copy()); + progress[s] = 0; + maxProgress[s] = DEFAULT_MAX_PROGRESS; + changed = true; + } + } + } + + if (changed) setChanged(); + } + + //? if >1.20.1 { + /*private Optional> getRecipeFor(ItemStack input) { + return level.getRecipeManager().getRecipeFor(ModRecipes.INCUBATOR_RECIPE_TYPE.get(), new IncubatorRecipeInput(input), level); + } + *///?} else { + private Optional getRecipeFor(ItemStack input) { + return level.getRecipeManager().getRecipeFor(ModRecipes.INCUBATOR_RECIPE_TYPE.get(), new IncubatorRecipeInput(input), level); + } + //?} + + private void pullEnergyFromNeighbors() { + for (Direction dir : Direction.values()) { + BlockEntity be = level.getBlockEntity(worldPosition.relative(dir)); + if (be instanceof ModEnergyUtil.EnergyProvider provider) { + ModEnergyStorage source = provider.getEnergyStorage(dir.getOpposite()); + if (source != null && source.canExtract()) { + int canAccept = energyStorage.receiveEnergy(TRANSFER_RATE, true); + if (canAccept > 0) { + int extracted = source.extractEnergy(canAccept, false); + energyStorage.receiveEnergy(extracted, false); + } + } + } + } + } + + @Nullable + @Override + public Packet getUpdatePacket() { + return ClientboundBlockEntityDataPacket.create(this); + } + + //? if >1.20.1 { + /*@Override + public CompoundTag getUpdateTag(HolderLookup.Provider registries) { + return saveWithoutMetadata(registries); + } + *///?} else { + @Override + public CompoundTag getUpdateTag() { + return saveWithoutMetadata(); + } + //?} +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/block/entity/custom/PipeBlockEntity.java b/common/src/main/java/net/cmr/jurassicrevived/block/entity/custom/PipeBlockEntity.java new file mode 100644 index 0000000..9937bc7 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/block/entity/custom/PipeBlockEntity.java @@ -0,0 +1,171 @@ +package net.cmr.jurassicrevived.block.entity.custom; + +import net.cmr.jurassicrevived.block.custom.PipeBlock; +import net.cmr.jurassicrevived.block.custom.PipeBlock.ConnectionType; +import net.cmr.jurassicrevived.block.custom.PipeBlock.Transport; +import net.cmr.jurassicrevived.block.entity.ModBlockEntities; +import net.cmr.jurassicrevived.block.entity.energy.ModEnergyStorage; +import net.cmr.jurassicrevived.block.entity.energy.ModEnergyUtil; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.Container; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; + +import java.util.*; + +public class PipeBlockEntity extends BlockEntity { + + private final Transport transport; + + public PipeBlockEntity(BlockPos pos, BlockState state) { + super(resolveType(state), pos, state); + this.transport = ((PipeBlock) state.getBlock()).getTransport(); + } + + private static net.minecraft.world.level.block.entity.BlockEntityType resolveType(BlockState state) { + PipeBlock block = (PipeBlock) state.getBlock(); + return switch (block.getTransport()) { + case ITEMS -> ModBlockEntities.ITEM_PIPE_BE.get(); + case FLUIDS -> ModBlockEntities.FLUID_PIPE_BE.get(); + case ENERGY -> ModBlockEntities.POWER_PIPE_BE.get(); + }; + } + + public static void serverTick(Level level, BlockPos pos, BlockState state, PipeBlockEntity be) { + if (level == null || level.isClientSide) return; + + PipeBlock block = (PipeBlock) state.getBlock(); + int itemCap = block.getMaxItemsPerTick(); + // Fallback caps if Config is not ready + int fluidCap = 1000; + int energyCap = 1000; + + switch (be.transport) { + case ITEMS -> transferItems(level, pos, state, itemCap); + case FLUIDS -> transferFluids(level, pos, state, fluidCap); + case ENERGY -> transferEnergy(level, pos, state, energyCap); + } + } + + // ===== Network discovery ===== + + private record PipeEndpoint(BlockPos pipePos, Direction side) {} + + private static class Network { + final List sources = new ArrayList<>(); + final List sinks = new ArrayList<>(); + } + + private static Network discoverNetwork(Level level, BlockPos start, Transport transport) { + Network net = new Network(); + ArrayDeque q = new ArrayDeque<>(); + HashSet seen = new HashSet<>(); + q.add(start); + seen.add(start); + + while (!q.isEmpty()) { + BlockPos p = q.removeFirst(); + BlockState st = level.getBlockState(p); + if (!(st.getBlock() instanceof PipeBlock pb) || pb.getTransport() != transport) continue; + + for (Direction d : Direction.values()) { + ConnectionType ct = st.getValue(PipeBlock.getProp(d)); + + if (ct == ConnectionType.CONNECTOR_PULL) { + net.sources.add(new PipeEndpoint(p, d)); + } else if (ct == ConnectionType.CONNECTOR) { + net.sinks.add(new PipeEndpoint(p, d)); + } + + if (ct == ConnectionType.PIPE) { + BlockPos np = p.relative(d); + if (!seen.contains(np)) { + BlockState ns = level.getBlockState(np); + if (ns.getBlock() instanceof PipeBlock op && op.getTransport() == transport) { + seen.add(np); + q.add(np); + } + } + } + } + } + return net; + } + + // ===== Item Transfer (Using Vanilla Container) ===== + + private static void transferItems(Level level, BlockPos pos, BlockState state, int perTickLimit) { + Network net = discoverNetwork(level, pos, Transport.ITEMS); + List outputs = new ArrayList<>(); + for (PipeEndpoint ep : net.sinks) { + BlockEntity be = level.getBlockEntity(ep.pipePos.relative(ep.side)); + if (be instanceof Container c) outputs.add(c); + } + if (outputs.isEmpty()) return; + + int remaining = perTickLimit; + for (PipeEndpoint ep : net.sources) { + if (remaining <= 0) break; + BlockEntity be = level.getBlockEntity(ep.pipePos.relative(ep.side)); + if (!(be instanceof Container src)) continue; + + for (int i = 0; i < src.getContainerSize() && remaining > 0; i++) { + ItemStack stack = src.getItem(i); + if (stack.isEmpty()) continue; + + ItemStack toMove = stack.copy(); + toMove.setCount(Math.min(stack.getCount(), remaining)); + + for (Container out : outputs) { + // Logic to insert into vanilla container (simplified) + // You might want a helper for this + } + } + } + } + + // ===== Energy Transfer (Using Custom Energy System) ===== + + private static void transferEnergy(Level level, BlockPos pos, BlockState state, int perTickLimit) { + Network net = discoverNetwork(level, pos, Transport.ENERGY); + List outputs = new ArrayList<>(); + for (PipeEndpoint ep : net.sinks) { + BlockEntity be = level.getBlockEntity(ep.pipePos.relative(ep.side)); + if (be instanceof ModEnergyUtil.EnergyProvider provider) { + ModEnergyStorage storage = provider.getEnergyStorage(ep.side.getOpposite()); + if (storage != null && storage.canReceive()) outputs.add(storage); + } + } + if (outputs.isEmpty()) return; + + int remaining = perTickLimit; + for (PipeEndpoint ep : net.sources) { + if (remaining <= 0) break; + BlockEntity be = level.getBlockEntity(ep.pipePos.relative(ep.side)); + if (be instanceof ModEnergyUtil.EnergyProvider provider) { + ModEnergyStorage src = provider.getEnergyStorage(ep.side.getOpposite()); + if (src == null || !src.canExtract()) continue; + + int canExtract = src.extractEnergy(remaining, true); + if (canExtract <= 0) continue; + + for (ModEnergyStorage out : outputs) { + int accepted = out.receiveEnergy(canExtract, true); + if (accepted > 0) { + int actuallyExtracted = src.extractEnergy(accepted, false); + out.receiveEnergy(actuallyExtracted, false); + remaining -= actuallyExtracted; + break; + } + } + } + } + } + + private static void transferFluids(Level level, BlockPos pos, BlockState state, int perTickLimit) { + // Implementation would use Architectury FluidStack similarly to energy + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/block/entity/custom/PowerCellBlockEntity.java b/common/src/main/java/net/cmr/jurassicrevived/block/entity/custom/PowerCellBlockEntity.java new file mode 100644 index 0000000..a4aa683 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/block/entity/custom/PowerCellBlockEntity.java @@ -0,0 +1,170 @@ +package net.cmr.jurassicrevived.block.entity.custom; + +import dev.architectury.registry.menu.ExtendedMenuProvider; +import net.cmr.jurassicrevived.block.entity.ModBlockEntities; +import net.cmr.jurassicrevived.block.entity.energy.ModEnergyStorage; +import net.cmr.jurassicrevived.block.entity.energy.ModEnergyUtil; +import net.cmr.jurassicrevived.screen.custom.PowerCellMenu; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.chat.Component; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.game.ClientGamePacketListener; +import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket; +import net.minecraft.world.Containers; +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import org.jetbrains.annotations.Nullable; + +//? if >1.20.1 { +/*import net.minecraft.core.HolderLookup; + *///?} + +public class PowerCellBlockEntity extends BlockEntity implements ExtendedMenuProvider, ModEnergyUtil.EnergyProvider { + public final SimpleContainer itemHandler = new SimpleContainer(2) { + @Override + public void setChanged() { + super.setChanged(); + PowerCellBlockEntity.this.setChanged(); + if (level != null && !level.isClientSide()) { + level.sendBlockUpdated(getBlockPos(), getBlockState(), getBlockState(), 3); + } + } + + @Override + public int getMaxStackSize() { + return 64; + } + }; + + private final ModEnergyStorage energyStorage = createEnergyStorage(); + private static final int TRANSFER_RATE = 1000; + + public PowerCellBlockEntity(BlockPos pos, BlockState blockState) { + super(ModBlockEntities.POWER_CELL_BE.get(), pos, blockState); + } + + private ModEnergyStorage createEnergyStorage() { + return new ModEnergyStorage(256000, TRANSFER_RATE) { + @Override + public void onEnergyChanged() { + setChanged(); + if (level != null && !level.isClientSide()) { + level.sendBlockUpdated(getBlockPos(), getBlockState(), getBlockState(), 3); + } + } + }; + } + + public boolean isEmptyForDrop() { + return itemHandler.isEmpty() && energyStorage.getEnergyStored() == 0; + } + + @Override + public ModEnergyStorage getEnergyStorage(@Nullable Direction direction) { + return this.energyStorage; + } + + @Override + public Component getDisplayName() { + return Component.literal("Power Cell"); + } + + @Nullable + @Override + public AbstractContainerMenu createMenu(int containerId, Inventory playerInventory, Player player) { + return new PowerCellMenu(containerId, playerInventory, this); + } + + @Override + public void saveExtraData(FriendlyByteBuf buf) { + buf.writeBlockPos(getBlockPos()); + } + + public void drops() { + Containers.dropContents(this.level, this.worldPosition, itemHandler); + } + + public void tick(Level level, BlockPos blockPos, BlockState blockState) { + if (level.isClientSide) return; + pushEnergyToAboveNeighbour(); + } + + private void pushEnergyToAboveNeighbour() { + BlockPos up = worldPosition.above(); + BlockEntity targetBE = level.getBlockEntity(up); + + if (targetBE instanceof ModEnergyUtil.EnergyProvider provider) { + ModEnergyStorage targetStorage = provider.getEnergyStorage(Direction.DOWN); + if (targetStorage != null && targetStorage.canReceive()) { + int available = energyStorage.getEnergyStored(); + int toSend = Math.min(available, TRANSFER_RATE); + + int accepted = targetStorage.receiveEnergy(toSend, true); + if (accepted > 0) { + int extracted = energyStorage.extractEnergy(accepted, false); + targetStorage.receiveEnergy(extracted, false); + } + } + } + } + + //? if >1.20.1 { + /*@Override + protected void saveAdditional(CompoundTag pTag, HolderLookup.Provider pRegistries) { + super.saveAdditional(pTag, pRegistries); + pTag.put("Inventory", itemHandler.createTag(pRegistries)); + pTag.put("Energy", energyStorage.saveNBT()); + } + + @Override + protected void loadAdditional(CompoundTag pTag, HolderLookup.Provider pRegistries) { + super.loadAdditional(pTag, pRegistries); + itemHandler.fromTag(pTag.getList("Inventory", 10), pRegistries); + if (pTag.contains("Energy")) { + energyStorage.loadNBT(pTag.getCompound("Energy")); + } + } + *///?} else { + @Override + protected void saveAdditional(CompoundTag pTag) { + super.saveAdditional(pTag); + pTag.put("Inventory", itemHandler.createTag()); + pTag.put("Energy", energyStorage.saveNBT()); + } + + @Override + public void load(CompoundTag pTag) { + super.load(pTag); + itemHandler.fromTag(pTag.getList("Inventory", 10)); + if (pTag.contains("Energy")) { + energyStorage.loadNBT(pTag.getCompound("Energy")); + } + } + //?} + + @Nullable + @Override + public Packet getUpdatePacket() { + return ClientboundBlockEntityDataPacket.create(this); + } + + //? if >1.20.1 { + /*@Override + public CompoundTag getUpdateTag(HolderLookup.Provider pRegistries) { + return saveWithoutMetadata(pRegistries); + } + *///?} else { + @Override + public CompoundTag getUpdateTag() { + return saveWithoutMetadata(); + } + //?} +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/block/entity/custom/TankBlockEntity.java b/common/src/main/java/net/cmr/jurassicrevived/block/entity/custom/TankBlockEntity.java new file mode 100644 index 0000000..4715381 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/block/entity/custom/TankBlockEntity.java @@ -0,0 +1,147 @@ +package net.cmr.jurassicrevived.block.entity.custom; + +import dev.architectury.fluid.FluidStack; +import dev.architectury.registry.menu.ExtendedMenuProvider; +import net.cmr.jurassicrevived.block.entity.ModBlockEntities; +import net.cmr.jurassicrevived.screen.custom.TankMenu; +import net.minecraft.core.BlockPos; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.chat.Component; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.game.ClientGamePacketListener; +import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket; +import net.minecraft.world.Containers; +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import org.jetbrains.annotations.Nullable; + +//? if >1.20.1 { +/*import net.minecraft.core.HolderLookup; + *///?} + +public class TankBlockEntity extends BlockEntity implements ExtendedMenuProvider { + public final SimpleContainer itemHandler = new SimpleContainer(2) { + @Override + public void setChanged() { + super.setChanged(); + TankBlockEntity.this.setChanged(); + if (level != null && !level.isClientSide()) { + level.sendBlockUpdated(getBlockPos(), getBlockState(), getBlockState(), 3); + } + } + + @Override + public int getMaxStackSize() { + return 64; + } + }; + + + private FluidStack fluidStack = FluidStack.empty(); + private static final long CAPACITY = 64000; + + public TankBlockEntity(BlockPos pos, BlockState blockState) { + super(ModBlockEntities.TANK_BE.get(), pos, blockState); + } + + public FluidStack getFluid() { + return fluidStack; + } + + public void setFluid(FluidStack stack) { + this.fluidStack = stack; + setChanged(); + if (level != null && !level.isClientSide()) { + level.sendBlockUpdated(getBlockPos(), getBlockState(), getBlockState(), 3); + } + } + + @Override + public Component getDisplayName() { + return Component.literal("Tank"); + } + + @Nullable + @Override + public AbstractContainerMenu createMenu(int containerId, Inventory playerInventory, Player player) { + return new TankMenu(containerId, playerInventory, this); + } + + @Override + public void saveExtraData(FriendlyByteBuf buf) { + buf.writeBlockPos(getBlockPos()); + } + + public void drops() { + Containers.dropContents(this.level, this.worldPosition, itemHandler); + } + + public void tick(Level level, BlockPos blockPos, BlockState blockState) { + } + + public boolean isEmptyForDrop() { + return itemHandler.isEmpty() && fluidStack.isEmpty(); + } + + //? if >1.20.1 { + /*@Override + protected void saveAdditional(CompoundTag pTag, HolderLookup.Provider pRegistries) { + super.saveAdditional(pTag, pRegistries); + pTag.put("Inventory", itemHandler.createTag(pRegistries)); + CompoundTag fluidTag = new CompoundTag(); + fluidStack.write(pRegistries, fluidTag); + pTag.put("Fluid", fluidTag); + } + + @Override + protected void loadAdditional(CompoundTag pTag, HolderLookup.Provider pRegistries) { + super.loadAdditional(pTag, pRegistries); + itemHandler.fromTag(pTag.getList("Inventory", 10), pRegistries); + if (pTag.contains("Fluid")) { + this.fluidStack = FluidStack.read(pRegistries, pTag.getCompound("Fluid")).orElse(FluidStack.empty()); + } + } + *///?} else { + @Override + protected void saveAdditional(CompoundTag pTag) { + super.saveAdditional(pTag); + pTag.put("Inventory", itemHandler.createTag()); + CompoundTag fluidTag = new CompoundTag(); + fluidStack.write(fluidTag); + pTag.put("Fluid", fluidTag); + } + + @Override + public void load(CompoundTag pTag) { + super.load(pTag); + itemHandler.fromTag(pTag.getList("Inventory", 10)); + if (pTag.contains("Fluid")) { + this.fluidStack = FluidStack.read(pTag.getCompound("Fluid")); + } + } + //?} + + @Nullable + @Override + public Packet getUpdatePacket() { + return ClientboundBlockEntityDataPacket.create(this); + } + + //? if >1.20.1 { + /*@Override + public CompoundTag getUpdateTag(HolderLookup.Provider pRegistries) { + return saveWithoutMetadata(pRegistries); + } + *///?} else { + @Override + public CompoundTag getUpdateTag() { + return saveWithoutMetadata(); + } + //?} +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/block/entity/energy/ModEnergyStorage.java b/common/src/main/java/net/cmr/jurassicrevived/block/entity/energy/ModEnergyStorage.java new file mode 100644 index 0000000..1fd247e --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/block/entity/energy/ModEnergyStorage.java @@ -0,0 +1,72 @@ +package net.cmr.jurassicrevived.block.entity.energy; + +import net.minecraft.nbt.CompoundTag; + +public abstract class ModEnergyStorage { + protected int energy; + protected int capacity; + protected int maxReceive; + protected int maxExtract; + + public ModEnergyStorage(int capacity, int maxTransfer) { + this(capacity, maxTransfer, maxTransfer, 0); + } + + public ModEnergyStorage(int capacity, int maxReceive, int maxExtract, int energy) { + this.capacity = capacity; + this.maxReceive = maxReceive; + this.maxExtract = maxExtract; + this.energy = Math.max(0, Math.min(capacity, energy)); + } + + public int receiveEnergy(int maxReceive, boolean simulate) { + int energyReceived = Math.min(capacity - energy, Math.min(this.maxReceive, maxReceive)); + if (!simulate && energyReceived != 0) { + energy += energyReceived; + onEnergyChanged(); + } + return energyReceived; + } + + public int extractEnergy(int maxExtract, boolean simulate) { + int energyExtracted = Math.min(energy, Math.min(this.maxExtract, maxExtract)); + if (!simulate && energyExtracted != 0) { + energy -= energyExtracted; + onEnergyChanged(); + } + return energyExtracted; + } + + public int getEnergyStored() { + return energy; + } + + public int getMaxEnergyStored() { + return capacity; + } + + public boolean canExtract() { + return this.maxExtract > 0; + } + + public boolean canReceive() { + return this.maxReceive > 0; + } + + public void setEnergy(int energy) { + this.energy = Math.max(0, Math.min(capacity, energy)); + onEnergyChanged(); + } + + public abstract void onEnergyChanged(); + + public CompoundTag saveNBT() { + CompoundTag tag = new CompoundTag(); + tag.putInt("Energy", energy); + return tag; + } + + public void loadNBT(CompoundTag tag) { + this.energy = tag.getInt("Energy"); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/block/entity/energy/ModEnergyUtil.java b/common/src/main/java/net/cmr/jurassicrevived/block/entity/energy/ModEnergyUtil.java new file mode 100644 index 0000000..7553031 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/block/entity/energy/ModEnergyUtil.java @@ -0,0 +1,54 @@ +package net.cmr.jurassicrevived.block.entity.energy; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; + +public class ModEnergyUtil { + public static boolean move(BlockPos from, BlockPos to, int amount, Level level) { + BlockEntity fromBE = level.getBlockEntity(from); + BlockEntity toBE = level.getBlockEntity(to); + + // In a common module, we check if our custom BEs implement a common energy provider + // or access the storage directly if they are your mod's blocks. + if (fromBE instanceof EnergyProvider fromProvider && toBE instanceof EnergyProvider toProvider) { + ModEnergyStorage fromStorage = fromProvider.getEnergyStorage(null); + ModEnergyStorage toStorage = toProvider.getEnergyStorage(null); + + if (fromStorage == null || toStorage == null) return false; + + if (canEnergyStorageExtractThisAmount(fromStorage, amount)) { + return false; + } + + if (canEnergyStorageStillReceiveEnergy(toStorage)) { + return false; + } + + int maxAmountToReceive = toStorage.receiveEnergy(amount, true); + int extractedEnergy = fromStorage.extractEnergy(maxAmountToReceive, false); + toStorage.receiveEnergy(extractedEnergy, false); + + return true; + } + return false; + } + + private static boolean canEnergyStorageStillReceiveEnergy(ModEnergyStorage toStorage) { + return toStorage.getEnergyStored() >= toStorage.getMaxEnergyStored() || !toStorage.canReceive(); + } + + private static boolean canEnergyStorageExtractThisAmount(ModEnergyStorage fromStorage, int amount) { + return fromStorage.getEnergyStored() <= 0 || fromStorage.getEnergyStored() < amount || !fromStorage.canExtract(); + } + + public static boolean doesBlockHaveEnergyStorage(BlockPos positionToCheck, Level level) { + BlockEntity be = level.getBlockEntity(positionToCheck); + return be instanceof EnergyProvider; + } + + // Common interface for your BlockEntities to implement + public interface EnergyProvider { + ModEnergyStorage getEnergyStorage(net.minecraft.core.Direction direction); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/block/renderer/TankBlockEntityRenderer.java b/common/src/main/java/net/cmr/jurassicrevived/block/renderer/TankBlockEntityRenderer.java new file mode 100644 index 0000000..a3d991d --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/block/renderer/TankBlockEntityRenderer.java @@ -0,0 +1,114 @@ +package net.cmr.jurassicrevived.block.renderer; + +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.VertexConsumer; +import com.mojang.math.Axis; +import dev.architectury.fluid.FluidStack; +import dev.architectury.hooks.fluid.FluidStackHooks; +import net.cmr.jurassicrevived.block.entity.custom.TankBlockEntity; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.ItemBlockRenderTypes; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.blockentity.BlockEntityRenderer; +import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider; +import net.minecraft.client.renderer.texture.TextureAtlasSprite; +import net.minecraft.core.BlockPos; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.inventory.InventoryMenu; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.material.FluidState; + +import java.util.Objects; + +// Credits to TurtyWurty +// Under MIT-License: https://github.com/DaRealTurtyWurty/1.20-Tutorial-Mod?tab=MIT-1-ov-file#readme +public class TankBlockEntityRenderer implements BlockEntityRenderer { + public TankBlockEntityRenderer(BlockEntityRendererProvider.Context context) { + } + + @Override + public void render(TankBlockEntity pBlockEntity, float partialTick, PoseStack pPoseStack, MultiBufferSource pBuffer, int pPackedLight, int packedOverlay) { + FluidStack fluidStack = pBlockEntity.getFluid(); + if (fluidStack.isEmpty()) + return; + + Level level = pBlockEntity.getLevel(); + if (level == null) + return; + + BlockPos pos = pBlockEntity.getBlockPos(); + + // Use Architectury Hooks instead of IClientFluidTypeExtensions + ResourceLocation stillTexture = Objects.requireNonNull(FluidStackHooks.getStillTexture(fluidStack)).atlasLocation(); + int tintColor = FluidStackHooks.getColor(fluidStack); + + FluidState state = fluidStack.getFluid().defaultFluidState(); + + TextureAtlasSprite sprite = Minecraft.getInstance().getTextureAtlas(InventoryMenu.BLOCK_ATLAS).apply(stillTexture); + + // Adjust height calculation to be platform-independent + // (Assuming you'll update TankBlockEntity to use a multi-loader tank system) + float height = (((float) fluidStack.getAmount() / 64000f) * 0.625f) + 0.25f; + + VertexConsumer builder = pBuffer.getBuffer(ItemBlockRenderTypes.getRenderLayer(state)); + + // Top Texture + drawQuad(builder, pPoseStack, 0.1f, height, 0.1f, 0.9f, height, 0.9f, sprite.getU0(), sprite.getV0(), sprite.getU1(), sprite.getV1(), pPackedLight, tintColor); + drawQuad(builder, pPoseStack, 0.1f, 0, 0.1f, 0.9f, height, 0.1f, sprite.getU0(), sprite.getV0(), sprite.getU1(), sprite.getV1(), pPackedLight, tintColor); + + pPoseStack.pushPose(); + pPoseStack.mulPose(Axis.XP.rotationDegrees(180)); + pPoseStack.translate(0, -0.9f, -1f); + drawQuad(builder, pPoseStack, 0.1f, height, 0.1f, 0.9f, height, 0.9f, sprite.getU0(), sprite.getV0(), sprite.getU1(), sprite.getV1(), pPackedLight, tintColor); + pPoseStack.popPose(); + + pPoseStack.pushPose(); + pPoseStack.mulPose(Axis.YP.rotationDegrees(180)); + pPoseStack.translate(-1f, 0, -1.8f); + drawQuad(builder, pPoseStack, 0.1f, 0, 0.9f, 0.9f, height, 0.9f, sprite.getU0(), sprite.getV0(), sprite.getU1(), sprite.getV1(), pPackedLight, tintColor); + pPoseStack.popPose(); + + pPoseStack.pushPose(); + pPoseStack.mulPose(Axis.YP.rotationDegrees(90)); + pPoseStack.translate(-1f, 0, 0); + drawQuad(builder, pPoseStack, 0.1f, 0, 0.1f, 0.9f, height, 0.1f, sprite.getU0(), sprite.getV0(), sprite.getU1(), sprite.getV1(), pPackedLight, tintColor); + pPoseStack.popPose(); + + pPoseStack.pushPose(); + pPoseStack.mulPose(Axis.YN.rotationDegrees(90)); + pPoseStack.translate(0, 0, -1f); + drawQuad(builder, pPoseStack, 0.1f, 0, 0.1f, 0.9f, height, 0.1f, sprite.getU0(), sprite.getV0(), sprite.getU1(), sprite.getV1(), pPackedLight, tintColor); + pPoseStack.popPose(); + } + + //? if >1.20.1 { + /*private static void drawVertex(VertexConsumer builder, PoseStack poseStack, float x, float y, float z, float u, float v, int packedLight, int color) { + builder.addVertex(poseStack.last().pose(), x, y, z) + .setColor(color) + .setUv(u, v) + .setLight(packedLight) + .setNormal(1, 0, 0); + } + *///?} else { + private static void drawVertex(VertexConsumer builder, PoseStack poseStack, float x, float y, float z, float u, float v, int packedLight, int color) { + int a = (color >> 24) & 0xFF; + int r = (color >> 16) & 0xFF; + int g = (color >> 8) & 0xFF; + int b = (color) & 0xFF; + + builder.vertex(poseStack.last().pose(), x, y, z) + .color(r, g, b, a) + .uv(u, v) + .uv2(packedLight) + .normal(poseStack.last().normal(), 1, 0, 0) + .endVertex(); + } + //?} + + private static void drawQuad(VertexConsumer builder, PoseStack poseStack, float x0, float y0, float z0, float x1, float y1, float z1, float u0, float v0, float u1, float v1, int packedLight, int color) { + drawVertex(builder, poseStack, x0, y0, z0, u0, v0, packedLight, color); + drawVertex(builder, poseStack, x0, y1, z1, u0, v1, packedLight, color); + drawVertex(builder, poseStack, x1, y1, z1, u1, v1, packedLight, color); + drawVertex(builder, poseStack, x1, y0, z0, u1, v0, packedLight, color); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/compat/DNAAnalyzerRecipeCategory.java b/common/src/main/java/net/cmr/jurassicrevived/compat/DNAAnalyzerRecipeCategory.java new file mode 100644 index 0000000..487a90c --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/compat/DNAAnalyzerRecipeCategory.java @@ -0,0 +1,158 @@ +package net.cmr.jurassicrevived.compat; + +import mezz.jei.api.constants.VanillaTypes; +import mezz.jei.api.gui.builder.IRecipeLayoutBuilder; +import mezz.jei.api.gui.drawable.IDrawable; +import mezz.jei.api.gui.ingredient.IRecipeSlotsView; +import mezz.jei.api.helpers.IGuiHelper; +import mezz.jei.api.recipe.IFocusGroup; +import mezz.jei.api.recipe.RecipeIngredientRole; +import mezz.jei.api.recipe.RecipeType; +import mezz.jei.api.recipe.category.IRecipeCategory; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.block.ModBlocks; +import net.cmr.jurassicrevived.config.JRConfigManager; +import net.cmr.jurassicrevived.item.ModItems; +import net.cmr.jurassicrevived.recipe.DNAAnalyzerRecipe; +import net.cmr.jurassicrevived.util.ModTags; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public class DNAAnalyzerRecipeCategory implements IRecipeCategory { + public static final ResourceLocation UID = Constants.rl("dna_analyzing"); + public static final ResourceLocation TEXTURE = Constants.rl("textures/gui/dna_analyzer/dna_analyzer_gui.png"); + private static final ResourceLocation DNA_TEXTURE = Constants.rl("textures/gui/generic/syringe_dna.png"); + private static final ResourceLocation POWER_BAR_TEXTURE = Constants.rl("textures/gui/generic/power_bar.png"); + + public static final RecipeType DNA_ANALYZER_RECIPE_RECIPE_TYPE = + new RecipeType<>(UID, DNAAnalyzerRecipe.class); + + private final IDrawable background; + private final IDrawable icon; + + public DNAAnalyzerRecipeCategory(IGuiHelper guiHelper) { + this.background = guiHelper.drawableBuilder(TEXTURE, 0, 0, 176, 80).setTextureSize(176, 166).build(); + this.icon = guiHelper.createDrawableIngredient(VanillaTypes.ITEM_STACK, new ItemStack(ModBlocks.DNA_ANALYZER.get())); + } + + @Override + public RecipeType getRecipeType() { + return DNA_ANALYZER_RECIPE_RECIPE_TYPE; + } + + @Override + public Component getTitle() { + return Component.translatable("block.jurassicrevived.dna_analyzer"); + } + + @Override + public int getWidth() { + return background.getWidth(); + } + + @Override + public int getHeight() { + return background.getHeight(); + } + + @Override + public void draw(DNAAnalyzerRecipe recipe, IRecipeSlotsView recipeSlotsView, GuiGraphics guiGraphics, double mouseX, double mouseY) { + background.draw(guiGraphics); + if (JRConfigManager.get().requirePower) { + guiGraphics.blit(POWER_BAR_TEXTURE, 159, 10, 0, 0, 10, 66, 10, 66); + } + + { + int fullW = 6, fullH = 16; + int drawBaseX = 85; + int drawBaseY = 38; + + // Loop progress at ~20 TPS over 200 ticks + int maxTicks = 200; + long now = System.currentTimeMillis(); + int progress = (int) ((now / 50L) % maxTicks); + + // Match the screen’s scale behavior: clamp to fullH (pixels) + int visible = Math.min(fullH, Math.max(0, progress * fullH / maxTicks)); + + // Top-down drain: shrink from the top toward the bottom + int remaining = fullH - visible; + int srcU = 0; + int srcV = visible; + + int drawX = drawBaseX; + int drawY = drawBaseY + visible; + + if (remaining > 0) { + guiGraphics.blit(DNA_TEXTURE, drawX, drawY, srcU, srcV, fullW, remaining, fullW, fullH); + } + } + + if (JRConfigManager.get().requirePower) { + // Simple visual fill to hint energy usage in JEI (not bound to a BE) + int barX = 160, barY = 11, barW = 8, barH = 64; + int requiredFE = 6000, capacityFE = 64000; + int filled = (int) (barH * (requiredFE / (float) capacityFE)); + guiGraphics.fillGradient(barX, barY + (barH - filled), barX + barW, barY + barH, 0xffb51500, 0xff600b00); + + // Tooltip for the energy bar + int mx = (int) mouseX, my = (int) mouseY; + if (mx >= barX && mx < barX + barW && my >= barY && my < barY + barH) { + List tips = List.of(Component.literal("6000 / 64000 FE")); + guiGraphics.renderTooltip(Minecraft.getInstance().font, tips, java.util.Optional.empty(), mx, my); + } + } + } + + @Override + public @Nullable IDrawable getIcon() { + return icon; + } + + @Override + public void setRecipe(IRecipeLayoutBuilder builder, DNAAnalyzerRecipe recipe, IFocusGroup focuses) { + + builder.addSlot(RecipeIngredientRole.INPUT, 57, 35).addIngredients(recipe.getIngredients().get(0)); + builder.addSlot(RecipeIngredientRole.INPUT, 80, 7).addIngredients(recipe.getIngredients().get(1)); + + ItemStack amber = new ItemStack(ModItems.MOSQUITO_IN_AMBER.get()); + boolean isMosquitoRecipe = recipe.getIngredients().size() > 1 && recipe.getIngredients().get(1).test(amber); + + if (isMosquitoRecipe) { + var level = Minecraft.getInstance().level; + if (level != null) { + var itemRegistry = level.registryAccess().registryOrThrow(Registries.ITEM); + var dnaTagOpt = itemRegistry.getTag(ModTags.Items.DNA); + List dnaOutputs = dnaTagOpt.map(holderSet -> + holderSet.stream() + .map(h -> new ItemStack(h.value(), Math.max(1, recipe.getResultItem(null).getCount()))) + .collect(java.util.stream.Collectors.toList()) + ).orElse(List.of()); + + // Filter out items with a weight of 0 so they don't show in JEI + dnaOutputs = dnaOutputs.stream() + .filter(stack -> recipe.getWeightFor(stack.getItem()) > 0) + .collect(java.util.stream.Collectors.toList()); + + var slot = builder.addSlot(RecipeIngredientRole.OUTPUT, 80, 63).addItemStacks(dnaOutputs); + slot.addRichTooltipCallback((view, tooltip) -> { + var opt = view.getDisplayedItemStack(); + if (opt.isPresent()) { + int weight = recipe.getWeightFor(opt.get().getItem()); + //tooltip.add(Component.literal("Weight: " + weight)); + } + }); + return; + } + } + + builder.addSlot(RecipeIngredientRole.OUTPUT, 80, 63).addItemStack(recipe.getResultItem(null)); + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/compat/DNAExtractorRecipeCategory.java b/common/src/main/java/net/cmr/jurassicrevived/compat/DNAExtractorRecipeCategory.java new file mode 100644 index 0000000..40e6249 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/compat/DNAExtractorRecipeCategory.java @@ -0,0 +1,156 @@ +package net.cmr.jurassicrevived.compat; + +import mezz.jei.api.constants.VanillaTypes; +import mezz.jei.api.gui.builder.IRecipeLayoutBuilder; +import mezz.jei.api.gui.drawable.IDrawable; +import mezz.jei.api.gui.ingredient.IRecipeSlotsView; +import mezz.jei.api.helpers.IGuiHelper; +import mezz.jei.api.recipe.IFocusGroup; +import mezz.jei.api.recipe.RecipeIngredientRole; +import mezz.jei.api.recipe.RecipeType; +import mezz.jei.api.recipe.category.IRecipeCategory; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.block.ModBlocks; +import net.cmr.jurassicrevived.config.JRConfigManager; +import net.cmr.jurassicrevived.item.ModItems; +import net.cmr.jurassicrevived.recipe.DNAExtractorRecipe; +import net.cmr.jurassicrevived.util.ModTags; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public class DNAExtractorRecipeCategory implements IRecipeCategory { + public static final ResourceLocation UID = Constants.rl("dna_extracting"); + public static final ResourceLocation TEXTURE = Constants.rl("textures/gui/dna_extractor/dna_extractor_gui.png"); + private static final ResourceLocation DNA_TEXTURE = Constants.rl("textures/gui/generic/dna.png"); + private static final ResourceLocation POWER_BAR_TEXTURE = Constants.rl("textures/gui/generic/power_bar.png"); + + public static final RecipeType DNA_EXTRACTOR_RECIPE_RECIPE_TYPE = + new RecipeType<>(UID, DNAExtractorRecipe.class); + + private final IDrawable background; + private final IDrawable icon; + + public DNAExtractorRecipeCategory(IGuiHelper guiHelper) { + this.background = guiHelper.drawableBuilder(TEXTURE, 0, 0, 176, 80).setTextureSize(176, 166).build(); + this.icon = guiHelper.createDrawableIngredient(VanillaTypes.ITEM_STACK, new ItemStack(ModBlocks.DNA_EXTRACTOR.get())); + } + + @Override + public RecipeType getRecipeType() { + return DNA_EXTRACTOR_RECIPE_RECIPE_TYPE; + } + + @Override + public Component getTitle() { + return Component.translatable("block.jurassicrevived.dna_extractor"); + } + + @Override + public int getWidth() { + return background.getWidth(); + } + + @Override + public int getHeight() { + return background.getHeight(); + } + + @Override + public void draw(DNAExtractorRecipe recipe, IRecipeSlotsView recipeSlotsView, GuiGraphics guiGraphics, double mouseX, double mouseY) { + background.draw(guiGraphics); + if (JRConfigManager.get().requirePower) { + guiGraphics.blit(POWER_BAR_TEXTURE, 159, 10, 0, 0, 10, 66, 10, 66); + } + + { + int fullW = 8, fullH = 16; + int drawBaseX = 84; + int drawBaseY = 38; + + // Loop progress at ~20 TPS over 200 ticks + int maxTicks = 200; + long now = System.currentTimeMillis(); + int progress = (int) ((now / 50L) % maxTicks); + + // Match the screen’s scale behavior: clamp to fullH (pixels) + int visible = Math.min(fullH, Math.max(0, progress * fullH / maxTicks)); + + int srcU = 0; + int srcV = fullH - visible; + + int drawX = drawBaseX; + int drawY = drawBaseY + (fullH - visible); + + if (visible > 0) { + guiGraphics.blit(DNA_TEXTURE, drawX, drawY, srcU, srcV, fullW, visible, fullW, fullH); + } + } + + if (JRConfigManager.get().requirePower) { + // Simple visual fill to hint energy usage in JEI (not bound to a BE) + int barX = 160, barY = 11, barW = 8, barH = 64; + int requiredFE = 6000, capacityFE = 64000; + int filled = (int) (barH * (requiredFE / (float) capacityFE)); + guiGraphics.fillGradient(barX, barY + (barH - filled), barX + barW, barY + barH, 0xffb51500, 0xff600b00); + + // Tooltip for the energy bar + int mx = (int) mouseX, my = (int) mouseY; + if (mx >= barX && mx < barX + barW && my >= barY && my < barY + barH) { + List tips = List.of(Component.literal("6000 / 64000 FE")); + guiGraphics.renderTooltip(Minecraft.getInstance().font, tips, java.util.Optional.empty(), mx, my); + } + } + } + + @Override + public @Nullable IDrawable getIcon() { + return icon; + } + + @Override + public void setRecipe(IRecipeLayoutBuilder builder, DNAExtractorRecipe recipe, IFocusGroup focuses) { + + builder.addSlot(RecipeIngredientRole.INPUT, 57, 35).addIngredients(recipe.getIngredients().get(0)); + builder.addSlot(RecipeIngredientRole.INPUT, 80, 7).addIngredients(recipe.getIngredients().get(1)); + + ItemStack amber = new ItemStack(ModItems.MOSQUITO_IN_AMBER.get()); + boolean isMosquitoRecipe = recipe.getIngredients().size() > 1 && recipe.getIngredients().get(1).test(amber); + + if (isMosquitoRecipe) { + var level = Minecraft.getInstance().level; + if (level != null) { + var itemRegistry = level.registryAccess().registryOrThrow(Registries.ITEM); + var dnaTagOpt = itemRegistry.getTag(ModTags.Items.DNA); + List dnaOutputs = dnaTagOpt.map(holderSet -> + holderSet.stream() + .map(h -> new ItemStack(h.value(), Math.max(1, recipe.getResultItem(null).getCount()))) + .collect(java.util.stream.Collectors.toList()) + ).orElse(List.of()); + + // Filter out items with a weight of 0 so they don't show in JEI + dnaOutputs = dnaOutputs.stream() + .filter(stack -> recipe.getWeightFor(stack.getItem()) > 0) + .collect(java.util.stream.Collectors.toList()); + + var slot = builder.addSlot(RecipeIngredientRole.OUTPUT, 62, 63).addItemStacks(dnaOutputs); + slot.addRichTooltipCallback((view, tooltip) -> { + var opt = view.getDisplayedItemStack(); + if (opt.isPresent()) { + int weight = recipe.getWeightFor(opt.get().getItem()); + //tooltip.add(Component.literal("Weight: " + weight)); + } + }); + return; + } + } + + builder.addSlot(RecipeIngredientRole.OUTPUT, 62, 63).addItemStack(recipe.getResultItem(null)); + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/compat/DNAHybridizerRecipeCategory.java b/common/src/main/java/net/cmr/jurassicrevived/compat/DNAHybridizerRecipeCategory.java new file mode 100644 index 0000000..ca8499d --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/compat/DNAHybridizerRecipeCategory.java @@ -0,0 +1,130 @@ +package net.cmr.jurassicrevived.compat; + +import mezz.jei.api.constants.VanillaTypes; +import mezz.jei.api.gui.builder.IRecipeLayoutBuilder; +import mezz.jei.api.gui.drawable.IDrawable; +import mezz.jei.api.gui.ingredient.IRecipeSlotsView; +import mezz.jei.api.helpers.IGuiHelper; +import mezz.jei.api.recipe.IFocusGroup; +import mezz.jei.api.recipe.RecipeIngredientRole; +import mezz.jei.api.recipe.RecipeType; +import mezz.jei.api.recipe.category.IRecipeCategory; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.block.ModBlocks; +import net.cmr.jurassicrevived.config.JRConfigManager; +import net.cmr.jurassicrevived.recipe.DNAHybridizerRecipe; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public class DNAHybridizerRecipeCategory implements IRecipeCategory { + public static final ResourceLocation UID = Constants.rl("dna_hybridizing"); + public static final ResourceLocation TEXTURE = Constants.rl("textures/gui/dna_hybridizer/dna_hybridizer_gui.png"); + public static final ResourceLocation ARROW_TEXTURE = Constants.rl("textures/gui/generic/arrow.png"); + public static final ResourceLocation WHITE_ARROW_TEXTURE = Constants.rl("textures/gui/generic/white_arrow.png"); + private static final ResourceLocation POWER_BAR_TEXTURE = Constants.rl("textures/gui/generic/power_bar.png"); + + public static final RecipeType DNA_HYBRIDIZER_RECIPE_RECIPE_TYPE = + new RecipeType<>(UID, DNAHybridizerRecipe.class); + + private final IDrawable background; + private final IDrawable icon; + + public DNAHybridizerRecipeCategory(IGuiHelper guiHelper) { + this.background = guiHelper.drawableBuilder(TEXTURE, 0, 0, 176, 80).setTextureSize(176, 166).build(); + this.icon = guiHelper.createDrawableIngredient(VanillaTypes.ITEM_STACK, new ItemStack(ModBlocks.DNA_HYBRIDIZER.get())); + } + + @Override + public RecipeType getRecipeType() { + return DNA_HYBRIDIZER_RECIPE_RECIPE_TYPE; + } + + @Override + public Component getTitle() { + return Component.translatable("block.jurassicrevived.dna_hybridizer"); + } + + @Override + public int getWidth() { + return background.getWidth(); + } + + @Override + public int getHeight() { + return background.getHeight(); + } + + @Override + public void draw(DNAHybridizerRecipe recipe, IRecipeSlotsView recipeSlotsView, GuiGraphics guiGraphics, double mouseX, double mouseY) { + background.draw(guiGraphics); + guiGraphics.blit(ARROW_TEXTURE, 105, 35, 0, 0, 24, 16, 24, 16); + if (JRConfigManager.get().requirePower) { + guiGraphics.blit(POWER_BAR_TEXTURE, 159, 10, 0, 0, 10, 66, 10, 66); + // Fill amount for JEI: show total required energy (2000 FE) relative to 64000 FE capacity + // Our simple fill is purely visual for JEI, not tied to any BE + int barX = 160; + int barY = 11; + int barW = 8; + int barH = 64; + + int maxTicks = 200; + long now = System.currentTimeMillis(); + int progress = (int)((now / 50L) % maxTicks); // ~20 TPS + int arrowPixels = 24; + int progFilled = progress * arrowPixels / maxTicks; + if (progFilled > 0) { + guiGraphics.blit(WHITE_ARROW_TEXTURE, 105, 35, 0, 0, progFilled, 16, 24, 16); + } + + int requiredFE = 30000; + int capacityFE = 64000; + int filled = (int)(barH * (requiredFE / (float)capacityFE)); + // Render red fill similar to EnergyDisplayTooltipArea + guiGraphics.fillGradient(barX, barY + (barH - filled), barX + barW, barY + barH, 0xffb51500, 0xff600b00); + + // Tooltip "2000 / 64000 FE" on hover over the energy area + int mx = (int) mouseX; + int my = (int) mouseY; + if (mx >= barX && mx < barX + barW && my >= barY && my < barY + barH) { + List tips = List.of(Component.literal("30000 / 64000 FE")); + guiGraphics.renderTooltip(Minecraft.getInstance().font, tips, java.util.Optional.empty(), mx, my); + } + } + } + + @Override + public @Nullable IDrawable getIcon() { + return icon; + } + + @Override + public void setRecipe(IRecipeLayoutBuilder builder, DNAHybridizerRecipe recipe, IFocusGroup focuses) { + + int[][] coords = { + {8, 25}, {26, 25}, {44, 25}, + {62, 25}, {8, 43}, {26, 43}, + {44, 43}, {62, 43}, {83, 35} + }; + + int count = Math.min(9, recipe.getIngredients().size()); + for (int i = 0; i < Math.min(8, count); i++) { + var ing = recipe.getIngredients().get(i); + if (ing.isEmpty()) continue; + builder.addSlot(RecipeIngredientRole.INPUT, coords[i][0], coords[i][1]).addIngredients(ing); + } + + // Catalyst slot: if present in recipe (index 8), show that; otherwise show nothing + if (count >= 9 && !recipe.getIngredients().get(8).isEmpty()) { + builder.addSlot(RecipeIngredientRole.INPUT, coords[8][0], coords[8][1]) + .addIngredients(recipe.getIngredients().get(8)); + } + + builder.addSlot(RecipeIngredientRole.OUTPUT, 134, 35).addItemStack(recipe.getResultItem(null)); + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/compat/EggJadePlugin.java b/common/src/main/java/net/cmr/jurassicrevived/compat/EggJadePlugin.java new file mode 100644 index 0000000..9222c89 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/compat/EggJadePlugin.java @@ -0,0 +1,159 @@ +package net.cmr.jurassicrevived.compat; + +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.block.custom.EggBlock; +import net.cmr.jurassicrevived.block.custom.IncubatedEggBlock; +import net.cmr.jurassicrevived.block.entity.custom.EggBlockEntity; +import net.minecraft.ChatFormatting; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +import net.minecraft.world.level.block.entity.BlockEntity; +import snownee.jade.api.*; +import snownee.jade.api.config.IPluginConfig; +import snownee.jade.api.ui.*; +//? if <=1.20.1 { +import net.minecraft.client.gui.navigation.ScreenDirection; +import snownee.jade.impl.ui.ProgressStyle; +//?} + +// ... existing code ... +@WailaPlugin +public class EggJadePlugin implements IWailaPlugin { + private static final ResourceLocation EGG_UID = Constants.rl("egg"); + private static final String NBT_SECS = "jr_secs"; + private static final String NBT_TOTAL = "jr_total"; + + @Override + public void registerClient(IWailaClientRegistration reg) { + reg.registerBlockComponent(new snownee.jade.api.IBlockComponentProvider() { + //? if >1.20.1 { + /*@Override + public void appendTooltip(ITooltip tooltip, BlockAccessor accessor, IPluginConfig config) { + CompoundTag data = accessor.getServerData(); + if (data == null || !data.contains(NBT_SECS)) return; + + int secs = data.getInt(NBT_SECS); + int total = data.contains(NBT_TOTAL) ? Math.max(1, data.getInt(NBT_TOTAL)) : 5; + float ratio = Mth.clamp(1.0f - (secs / (float) total), 0.0f, 1.0f); + + IElementHelper h = IElementHelper.get(); + ProgressStyle style = h.progressStyle() + .color(0xFFFFFFFF, 0xFFFFFFFF) + .direction(ScreenDirection.RIGHT) + .fitContentX(true) + .fitContentY(true); + BoxStyle box = BoxStyle.getNestedBox(); + IElement progress = h.progress(ratio, Component.empty(), style, box, true); + tooltip.add(progress); + + tooltip.add(Component.translatable("tooltip.jurassicrevived.egg.hatches_in_seconds", secs) + .withStyle(ChatFormatting.YELLOW)); + } + *///?} else { + @Override + public void appendTooltip(ITooltip tooltip, BlockAccessor accessor, IPluginConfig config) { + CompoundTag data = accessor.getServerData(); + if (!data.contains(NBT_SECS)) return; + + int secs = data.getInt(NBT_SECS); + int total = data.contains(NBT_TOTAL) ? Math.max(1, data.getInt(NBT_TOTAL)) : 5; + float ratio = Mth.clamp(1.0f - (secs / (float) total), 0.0f, 1.0f); + + IElementHelper h = tooltip.getElementHelper(); + + // Properly build each style from its own factory + IProgressStyle pStyle = h.progressStyle() + .color(0xFFFFFFFF) // ARGB filled color + .textColor(0xFFFFFFFF); // ARGB text color + + IBoxStyle box = new ThickBorderBox(1.0f); // any width you want + tooltip.add(h.progress(ratio, Component.empty(), pStyle, box, false)); + + tooltip.add(Component.translatable("tooltip.jurassicrevived.egg.hatches_in_seconds", secs) + .withStyle(ChatFormatting.YELLOW)); + } + //?} + @Override + public ResourceLocation getUid() { + return EGG_UID; + } + }, EggBlock.class); + // Register the same provider for IncubatedEggBlock separately + reg.registerBlockComponent(new snownee.jade.api.IBlockComponentProvider() { + //? if >1.20.1 { + /*@Override + public void appendTooltip(ITooltip tooltip, BlockAccessor accessor, IPluginConfig config) { + CompoundTag data = accessor.getServerData(); + if (data == null || !data.contains(NBT_SECS)) return; + + int secs = data.getInt(NBT_SECS); + int total = data.contains(NBT_TOTAL) ? Math.max(1, data.getInt(NBT_TOTAL)) : 5; + float ratio = Mth.clamp(1.0f - (secs / (float) total), 0.0f, 1.0f); + + IElementHelper h = IElementHelper.get(); + ProgressStyle style = h.progressStyle() + .color(0xFFFFFFFF, 0xFFFFFFFF) + .direction(ScreenDirection.RIGHT) + .fitContentX(true) + .fitContentY(true); + BoxStyle box = BoxStyle.getNestedBox(); + IElement progress = h.progress(ratio, Component.empty(), style, box, true); + tooltip.add(progress); + + tooltip.add(Component.translatable("tooltip.jurassicrevived.egg.hatches_in_seconds", secs) + .withStyle(ChatFormatting.YELLOW)); + } + *///?} else { + @Override + public void appendTooltip(ITooltip tooltip, BlockAccessor accessor, IPluginConfig config) { + CompoundTag data = accessor.getServerData(); + if (!data.contains(NBT_SECS)) return; + + int secs = data.getInt(NBT_SECS); + int total = data.contains(NBT_TOTAL) ? Math.max(1, data.getInt(NBT_TOTAL)) : 5; + float ratio = Mth.clamp(1.0f - (secs / (float) total), 0.0f, 1.0f); + + IElementHelper h = tooltip.getElementHelper(); + + // Properly build each style from its own factory + IProgressStyle pStyle = h.progressStyle() + .color(0xFFFFFFFF) // ARGB filled color + .textColor(0xFFFFFFFF); // ARGB text color + + IBoxStyle box = new ThickBorderBox(1.0f); // any width you want + tooltip.add(h.progress(ratio, Component.empty(), pStyle, box, false)); + + tooltip.add(Component.translatable("tooltip.jurassicrevived.egg.hatches_in_seconds", secs) + .withStyle(ChatFormatting.YELLOW)); + } + //?} + + @Override + public ResourceLocation getUid() { + return EGG_UID; + } + }, IncubatedEggBlock.class); + } + + @Override + public void register(IWailaCommonRegistration reg) { + reg.registerBlockDataProvider(new IServerDataProvider() { + @Override + public void appendServerData(CompoundTag data, BlockAccessor accessor) { + BlockEntity be = accessor.getBlockEntity(); + if (be instanceof EggBlockEntity egg) { + int secs = egg.getSecondsRemaining(accessor.getLevel()); + data.putInt(NBT_SECS, secs); + data.putInt(NBT_TOTAL, egg.getTotalSeconds()); + } + } + + @Override + public ResourceLocation getUid() { + return EGG_UID; + } + }, EggBlockEntity.class); // BE is shared by both blocks + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/compat/EmbryoCalcificationMachineRecipeCategory.java b/common/src/main/java/net/cmr/jurassicrevived/compat/EmbryoCalcificationMachineRecipeCategory.java new file mode 100644 index 0000000..ab8bced --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/compat/EmbryoCalcificationMachineRecipeCategory.java @@ -0,0 +1,114 @@ +package net.cmr.jurassicrevived.compat; + +import mezz.jei.api.constants.VanillaTypes; +import mezz.jei.api.gui.builder.IRecipeLayoutBuilder; +import mezz.jei.api.gui.drawable.IDrawable; +import mezz.jei.api.gui.ingredient.IRecipeSlotsView; +import mezz.jei.api.helpers.IGuiHelper; +import mezz.jei.api.recipe.IFocusGroup; +import mezz.jei.api.recipe.RecipeIngredientRole; +import mezz.jei.api.recipe.RecipeType; +import mezz.jei.api.recipe.category.IRecipeCategory; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.block.ModBlocks; +import net.cmr.jurassicrevived.config.JRConfigManager; +import net.cmr.jurassicrevived.recipe.EmbryoCalcificationMachineRecipe; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public class EmbryoCalcificationMachineRecipeCategory implements IRecipeCategory { + public static final ResourceLocation UID = Constants.rl("embryo_calcification_machining"); + public static final ResourceLocation TEXTURE = Constants.rl("textures/gui/embryo_calcification_machine/embryo_calcification_machine_gui.png"); + private static final ResourceLocation SYRINGE_BAR_TEXTURE = Constants.rl("textures/gui/generic/syringe_bar.png"); + private static final ResourceLocation WHITE_SYRINGE_BAR_TEXTURE = Constants.rl("textures/gui/generic/white_syringe_bar.png"); + private static final ResourceLocation POWER_BAR_TEXTURE = Constants.rl("textures/gui/generic/power_bar.png"); + + public static final RecipeType EMBRYO_CALCIFICATION_MACHINE_RECIPE_RECIPE_TYPE = + new RecipeType<>(UID, EmbryoCalcificationMachineRecipe.class); + + private final IDrawable background; + private final IDrawable icon; + + public EmbryoCalcificationMachineRecipeCategory(IGuiHelper guiHelper) { + this.background = guiHelper.drawableBuilder(TEXTURE, 0, 0, 176, 80).setTextureSize(176, 166).build(); + this.icon = guiHelper.createDrawableIngredient(VanillaTypes.ITEM_STACK, new ItemStack(ModBlocks.EMBRYO_CALCIFICATION_MACHINE.get())); + } + + @Override + public RecipeType getRecipeType() { + return EMBRYO_CALCIFICATION_MACHINE_RECIPE_RECIPE_TYPE; + } + + @Override + public Component getTitle() { + return Component.translatable("block.jurassicrevived.embryo_calcification_machine"); + } + + @Override + public int getWidth() { + return background.getWidth(); + } + + @Override + public int getHeight() { + return background.getHeight(); + } + + @Override + public void draw(EmbryoCalcificationMachineRecipe recipe, IRecipeSlotsView recipeSlotsView, GuiGraphics guiGraphics, double mouseX, double mouseY) { + background.draw(guiGraphics); + guiGraphics.blit(SYRINGE_BAR_TEXTURE, 76, 35, 0, 0, 24, 16, 24, 16); + if (JRConfigManager.get().requirePower) { + guiGraphics.blit(POWER_BAR_TEXTURE, 159, 10, 0, 0, 10, 66, 10, 66); + // Fill amount for JEI: show total required energy (2000 FE) relative to 64000 FE capacity + // Our simple fill is purely visual for JEI, not tied to any BE + int barX = 160; + int barY = 11; + int barW = 8; + int barH = 64; + + int maxTicks = 200; + long now = System.currentTimeMillis(); + int progress = (int)((now / 50L) % maxTicks); // ~20 TPS + int arrowPixels = 24; + int progFilled = progress * arrowPixels / maxTicks; + if (progFilled > 0) { + guiGraphics.blit(WHITE_SYRINGE_BAR_TEXTURE, 76, 35, 0, 0, progFilled, 16, 24, 16); + } + + int requiredFE = 1000; + int capacityFE = 64000; + int filled = (int)(barH * (requiredFE / (float)capacityFE)); + // Render red fill similar to EnergyDisplayTooltipArea + guiGraphics.fillGradient(barX, barY + (barH - filled), barX + barW, barY + barH, 0xffb51500, 0xff600b00); + + // Tooltip "2000 / 64000 FE" on hover over the energy area + int mx = (int) mouseX; + int my = (int) mouseY; + if (mx >= barX && mx < barX + barW && my >= barY && my < barY + barH) { + List tips = List.of(Component.literal("1000 / 64000 FE")); + guiGraphics.renderTooltip(Minecraft.getInstance().font, tips, java.util.Optional.empty(), mx, my); + } + } + } + + @Override + public @Nullable IDrawable getIcon() { + return icon; + } + + @Override + public void setRecipe(IRecipeLayoutBuilder builder, EmbryoCalcificationMachineRecipe recipe, IFocusGroup focuses) { + + builder.addSlot(RecipeIngredientRole.INPUT, 39, 35).addIngredients(recipe.getIngredients().get(0)); + builder.addSlot(RecipeIngredientRole.INPUT, 57, 35).addIngredients(recipe.getIngredients().get(1)); + + builder.addSlot(RecipeIngredientRole.OUTPUT, 103, 35).addItemStack(recipe.getResultItem(null)); + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/compat/EmbryonicMachineRecipeCategory.java b/common/src/main/java/net/cmr/jurassicrevived/compat/EmbryonicMachineRecipeCategory.java new file mode 100644 index 0000000..70ad878 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/compat/EmbryonicMachineRecipeCategory.java @@ -0,0 +1,147 @@ +package net.cmr.jurassicrevived.compat; + +import mezz.jei.api.constants.VanillaTypes; +import mezz.jei.api.gui.builder.IRecipeLayoutBuilder; +import mezz.jei.api.gui.drawable.IDrawable; +import mezz.jei.api.gui.ingredient.IRecipeSlotsView; +import mezz.jei.api.helpers.IGuiHelper; +import mezz.jei.api.recipe.IFocusGroup; +import mezz.jei.api.recipe.RecipeIngredientRole; +import mezz.jei.api.recipe.RecipeType; +import mezz.jei.api.recipe.category.IRecipeCategory; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.block.ModBlocks; +import net.cmr.jurassicrevived.config.JRConfigManager; +import net.cmr.jurassicrevived.item.ModItems; +import net.cmr.jurassicrevived.recipe.EmbryonicMachineRecipe; +import net.cmr.jurassicrevived.util.ModTags; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public class EmbryonicMachineRecipeCategory implements IRecipeCategory { + public static final ResourceLocation UID = Constants.rl("embryonic_machining"); + public static final ResourceLocation TEXTURE = Constants.rl("textures/gui/embryonic_machine/embryonic_machine_gui.png"); + private static final ResourceLocation SYRINGE_BAR_REVERSED_TEXTURE = Constants.rl("textures/gui/generic/syringe_bar_reversed.png"); + private static final ResourceLocation WHITE_SYRINGE_BAR_REVERSED_TEXTURE = Constants.rl("textures/gui/generic/white_syringe_bar_reversed.png"); + private static final ResourceLocation POWER_BAR_TEXTURE = Constants.rl("textures/gui/generic/power_bar.png"); + + public static final RecipeType EMBRYONIC_MACHINE_RECIPE_RECIPE_TYPE = + new RecipeType<>(UID, EmbryonicMachineRecipe.class); + + private final IDrawable background; + private final IDrawable icon; + + public EmbryonicMachineRecipeCategory(IGuiHelper guiHelper) { + this.background = guiHelper.drawableBuilder(TEXTURE, 0, 0, 176, 80).setTextureSize(176, 166).build(); + this.icon = guiHelper.createDrawableIngredient(VanillaTypes.ITEM_STACK, new ItemStack(ModBlocks.EMBRYONIC_MACHINE.get())); + } + + @Override + public RecipeType getRecipeType() { + return EMBRYONIC_MACHINE_RECIPE_RECIPE_TYPE; + } + + @Override + public Component getTitle() { + return Component.translatable("block.jurassicrevived.embryonic_machine"); + } + + @Override + public int getWidth() { + return background.getWidth(); + } + + @Override + public int getHeight() { + return background.getHeight(); + } + + @Override + public void draw(EmbryonicMachineRecipe recipe, IRecipeSlotsView recipeSlotsView, GuiGraphics guiGraphics, double mouseX, double mouseY) { + background.draw(guiGraphics); + guiGraphics.blit(SYRINGE_BAR_REVERSED_TEXTURE, 76, 35, 0, 0, 24, 16, 24, 16); + if (JRConfigManager.get().requirePower) { + guiGraphics.blit(POWER_BAR_TEXTURE, 159, 10, 0, 0, 10, 66, 10, 66); + // Fill amount for JEI: show total required energy (2000 FE) relative to 64000 FE capacity + // Our simple fill is purely visual for JEI, not tied to any BE + int barX = 160; + int barY = 11; + int barW = 8; + int barH = 64; + + int maxTicks = 200; + long now = System.currentTimeMillis(); + int progress = (int)((now / 50L) % maxTicks); // ~20 TPS + int arrowPixels = 24; + int progFilled = progress * arrowPixels / maxTicks; + if (progFilled > 0) { + guiGraphics.blit(WHITE_SYRINGE_BAR_REVERSED_TEXTURE, 76, 35, 0, 0, progFilled, 16, 24, 16); + } + + int requiredFE = 2000; + int capacityFE = 64000; + int filled = (int)(barH * (requiredFE / (float)capacityFE)); + // Render red fill similar to EnergyDisplayTooltipArea + guiGraphics.fillGradient(barX, barY + (barH - filled), barX + barW, barY + barH, 0xffb51500, 0xff600b00); + + // Tooltip "2000 / 64000 FE" on hover over the energy area + int mx = (int) mouseX; + int my = (int) mouseY; + if (mx >= barX && mx < barX + barW && my >= barY && my < barY + barH) { + List tips = List.of(Component.literal("2000 / 64000 FE")); + guiGraphics.renderTooltip(Minecraft.getInstance().font, tips, java.util.Optional.empty(), mx, my); + } + } + } + + @Override + public @Nullable IDrawable getIcon() { + return icon; + } + + @Override + public void setRecipe(IRecipeLayoutBuilder builder, EmbryonicMachineRecipe recipe, IFocusGroup focuses) { + + builder.addSlot(RecipeIngredientRole.INPUT, 39, 35).addIngredients(recipe.getIngredients().get(0)); + builder.addSlot(RecipeIngredientRole.INPUT, 57, 35).addIngredients(recipe.getIngredients().get(1)); + // Frog DNA third input at (48,53) + if (recipe.getIngredients().size() >= 3) { + builder.addSlot(RecipeIngredientRole.INPUT, 48, 53).addIngredients(recipe.getIngredients().get(2)); + } + + ItemStack amber = new ItemStack(ModItems.MOSQUITO_IN_AMBER.get()); + boolean isMosquitoRecipe = recipe.getIngredients().size() > 1 && recipe.getIngredients().get(1).test(amber); + + if (isMosquitoRecipe) { + var level = Minecraft.getInstance().level; + if (level != null) { + var itemRegistry = level.registryAccess().registryOrThrow(Registries.ITEM); + var dnaTagOpt = itemRegistry.getTag(ModTags.Items.DNA); + List dnaOutputs = dnaTagOpt.map(holderSet -> + holderSet.stream() + .map(h -> new ItemStack(h.value(), Math.max(1, recipe.getResultItem(null).getCount()))) + .collect(java.util.stream.Collectors.toList()) + ).orElse(List.of()); + + var slot = builder.addSlot(RecipeIngredientRole.OUTPUT, 103, 35).addItemStacks(dnaOutputs); + slot.addRichTooltipCallback((view, tooltip) -> { + var opt = view.getDisplayedItemStack(); + if (opt.isPresent()) { + int weight = recipe.getWeightFor(opt.get().getItem()); + tooltip.add(Component.literal("Weight: " + weight)); + } + }); + return; + } + } + + builder.addSlot(RecipeIngredientRole.OUTPUT, 103, 35).addItemStack(recipe.getResultItem(null)); + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/compat/FluidStackSlotRenderer.java b/common/src/main/java/net/cmr/jurassicrevived/compat/FluidStackSlotRenderer.java new file mode 100644 index 0000000..ddd3d21 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/compat/FluidStackSlotRenderer.java @@ -0,0 +1,39 @@ +package net.cmr.jurassicrevived.compat; + +import dev.architectury.fluid.FluidStack; +import mezz.jei.api.ingredients.IIngredientRenderer; +import net.cmr.jurassicrevived.screen.renderer.FluidTankRenderer; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.network.chat.Component; +import net.minecraft.world.item.TooltipFlag; + +import java.util.List; + +public class FluidStackSlotRenderer implements IIngredientRenderer { + private final FluidTankRenderer tankRenderer; + + public FluidStackSlotRenderer(FluidTankRenderer tankRenderer) { + this.tankRenderer = tankRenderer; + } + + @Override + public int getWidth() { + return tankRenderer.getWidth(); + } + + @Override + public int getHeight() { + return tankRenderer.getHeight(); + } + + @Override + public void render(GuiGraphics guiGraphics, FluidStack ingredient) { + // Slot renders at its own x,y; JEI calls us with the pose already translated. + tankRenderer.render(guiGraphics, 0, 0, ingredient); + } + + @Override + public List getTooltip(FluidStack ingredient, TooltipFlag tooltipFlag) { + return tankRenderer.getTooltip(ingredient, tooltipFlag); + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/compat/FossilCleanerRecipeCategory.java b/common/src/main/java/net/cmr/jurassicrevived/compat/FossilCleanerRecipeCategory.java new file mode 100644 index 0000000..319a54e --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/compat/FossilCleanerRecipeCategory.java @@ -0,0 +1,202 @@ +package net.cmr.jurassicrevived.compat; + +import dev.architectury.fluid.FluidStack; +import dev.architectury.hooks.fluid.FluidStackHooks; +import mezz.jei.api.constants.VanillaTypes; +import mezz.jei.api.gui.builder.IRecipeLayoutBuilder; +import mezz.jei.api.gui.builder.IRecipeSlotBuilder; +import mezz.jei.api.gui.drawable.IDrawable; +import mezz.jei.api.gui.ingredient.IRecipeSlotsView; +import mezz.jei.api.helpers.IGuiHelper; +import mezz.jei.api.recipe.IFocusGroup; +import mezz.jei.api.recipe.RecipeIngredientRole; +import mezz.jei.api.recipe.RecipeType; +import mezz.jei.api.recipe.category.IRecipeCategory; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.block.ModBlocks; +import net.cmr.jurassicrevived.config.JRConfigManager; +import net.cmr.jurassicrevived.recipe.FossilCleanerRecipe; +import net.cmr.jurassicrevived.screen.renderer.FluidTankRenderer; +import net.cmr.jurassicrevived.util.ModTags; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.tags.FluidTags; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.material.Fluids; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +public class FossilCleanerRecipeCategory implements IRecipeCategory { + public static final ResourceLocation UID = Constants.rl("fossil_cleaning"); + public static final ResourceLocation TEXTURE = Constants.rl("textures/gui/fossil_cleaner/fossil_cleaner_gui.png"); + private static final ResourceLocation BUBBLES_TEXTURE = Constants.rl("textures/gui/generic/bubbles.png"); + private static final ResourceLocation WHITE_BUBBLES_TEXTURE = Constants.rl("textures/gui/generic/white_bubbles.png"); + private static final ResourceLocation POWER_BAR_TEXTURE = Constants.rl("textures/gui/generic/power_bar.png"); + + public static final RecipeType FOSSIL_CLEANER_RECIPE_RECIPE_TYPE = + new RecipeType<>(UID, FossilCleanerRecipe.class); + + private final IDrawable background; + private final IDrawable icon; + private final FluidTankRenderer fluidRenderer; + private static List WATER_CONTAINERS_CACHE = null; + + public FossilCleanerRecipeCategory(IGuiHelper guiHelper) { + this.background = guiHelper.drawableBuilder(TEXTURE, 0, 0, 176, 80).setTextureSize(176, 166).build(); + this.icon = guiHelper.createDrawableIngredient(VanillaTypes.ITEM_STACK, new ItemStack(ModBlocks.FOSSIL_CLEANER.get())); + this.fluidRenderer = new FluidTankRenderer(64000, true, 16, 50); + if (WATER_CONTAINERS_CACHE == null) { + WATER_CONTAINERS_CACHE = buildWaterContainersList(); + } + } + + @Override + public RecipeType getRecipeType() { + return FOSSIL_CLEANER_RECIPE_RECIPE_TYPE; + } + + @Override + public Component getTitle() { + return Component.translatable("block.jurassicrevived.fossil_cleaner"); + } + + @Override + public int getWidth() { + return background.getWidth(); + } + + @Override + public int getHeight() { + return background.getHeight(); + } + + @Override + public void draw(FossilCleanerRecipe recipe, IRecipeSlotsView recipeSlotsView, GuiGraphics guiGraphics, double mouseX, double mouseY) { + background.draw(guiGraphics); + guiGraphics.blit(BUBBLES_TEXTURE, 73, 37, 0, 0, 24, 16, 24, 12); + if (JRConfigManager.get().requirePower) { + guiGraphics.blit(POWER_BAR_TEXTURE, 159, 10, 0, 0, 10, 66, 10, 66); + // Fill amount for JEI: show total required energy (2000 FE) relative to 64000 FE capacity + // Our simple fill is purely visual for JEI, not tied to any BE + int barX = 160; + int barY = 11; + int barW = 8; + int barH = 64; + + int maxTicks = 200; + long now = System.currentTimeMillis(); + int progress = (int)((now / 50L) % maxTicks); // ~20 TPS + int arrowPixels = 29; + int progFilled = progress * arrowPixels / maxTicks; + if (progFilled > 0) { + guiGraphics.blit(WHITE_BUBBLES_TEXTURE, 73, 37, 0, 0, progFilled, 16, 29, 12); + } + + int requiredFE = 2000; + int capacityFE = 64000; + int filled = (int)(barH * (requiredFE / (float)capacityFE)); + // Render red fill similar to EnergyDisplayTooltipArea + guiGraphics.fillGradient(barX, barY + (barH - filled), barX + barW, barY + barH, 0xffb51500, 0xff600b00); + + // Tooltip "2000 / 64000 FE" on hover over the energy area + int mx = (int) mouseX; + int my = (int) mouseY; + if (mx >= barX && mx < barX + barW && my >= barY && my < barY + barH) { + List tips = List.of(Component.literal("2000 / 64000 FE")); + guiGraphics.renderTooltip(Minecraft.getInstance().font, tips, java.util.Optional.empty(), mx, my); + } + } + } + + @Override + public @Nullable IDrawable getIcon() { + return icon; + } + + @Override + public void setRecipe(IRecipeLayoutBuilder builder, FossilCleanerRecipe recipe, IFocusGroup focuses) { + + // Single consumable input (fossil block) + builder.addSlot(RecipeIngredientRole.INPUT, 57, 35).addIngredients(recipe.getIngredients().get(0)); + + // Fluid "tank" visualization using custom renderer at (7, 8) + IRecipeSlotBuilder tankSlot = builder.addSlot(RecipeIngredientRole.RENDER_ONLY, 7, 8); + tankSlot.setCustomRenderer(JEIJRPlugin.FLUID_STACK_TYPE, new FluidStackSlotRenderer(fluidRenderer)); + tankSlot.addIngredient(JEIJRPlugin.FLUID_STACK_TYPE, FluidStack.create(Fluids.WATER, 250)); + + // Water container acceptance list at (57, 61), discovered dynamically + var waterItems = builder.addSlot(RecipeIngredientRole.INPUT, 7, 61).addItemStacks(WATER_CONTAINERS_CACHE); + waterItems.addRichTooltipCallback((view, tooltip) -> { + tooltip.add(Component.translatable("jurassicrevived.tooltip.accepts_any_water_container")); + }); + + // Output list: all fossils from the tag, tooltip shows per-item weight from the recipe + var level = Minecraft.getInstance().level; + if (level != null) { + var itemRegistry = level.registryAccess().registryOrThrow(Registries.ITEM); + var fossilsTagOpt = itemRegistry.getTag(ModTags.Items.FOSSILS); + List fossilOutputs = fossilsTagOpt.map(holderSet -> + holderSet.stream() + .map(h -> new ItemStack(h.value(), Math.max(1, recipe.output().getCount()))) + .collect(Collectors.toList()) + ).orElse(List.of()); + + // Hide zero-weight fossils + fossilOutputs = fossilOutputs.stream() + .filter(stack -> recipe.getWeightFor(stack.getItem()) > 0) + .collect(Collectors.toList()); + + var slot = builder.addSlot(RecipeIngredientRole.OUTPUT, 103, 35).addItemStacks(fossilOutputs); + slot.addRichTooltipCallback((view, tooltip) -> { + var opt = view.getDisplayedItemStack(); + if (opt.isPresent()) { + int weight = recipe.getWeightFor(opt.get().getItem()); + //tooltip.add(Component.literal("Weight: " + weight)); + } + }); + return; + } + + builder.addSlot(RecipeIngredientRole.OUTPUT, 103, 35).addItemStack(recipe.output()); + } + + private static List buildWaterContainersList() { + var list = new ArrayList(); + // Always include vanilla water bucket (already filled) + list.add(new ItemStack(Items.WATER_BUCKET)); + + var mc = Minecraft.getInstance(); + var level = mc.level; + if (level == null) { + return Collections.unmodifiableList(list); + } + + final int REQUIRED_MB = 250; + + var itemRegistry = level.registryAccess().registryOrThrow(Registries.ITEM); + for (Item item : itemRegistry) { + if (item == Items.WATER_BUCKET) continue; + + ItemStack empty = new ItemStack(item); + // Check if item is a fluid container using Architectury's hooks + // This is a simplified check; a more robust one would involve checking capabilities or similar hooks + // For now, we'll rely on known items or basic checks if available via Architectury + // Since Architectury abstracts this, we might need to rely on platform-specific implementations or common helpers if available. + // However, without a direct common "isFluidContainer" helper in the provided context, we might skip complex checks or use a simple list. + + // For the sake of this conversion, we will stick to just the water bucket as a safe default + // or implement a more complex check if Architectury provides one. + // Given the constraints, we'll simplify this to just water buckets for now to avoid platform-specific code in common. + } + return Collections.unmodifiableList(list); + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/compat/FossilGrinderRecipeCategory.java b/common/src/main/java/net/cmr/jurassicrevived/compat/FossilGrinderRecipeCategory.java new file mode 100644 index 0000000..f0234c3 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/compat/FossilGrinderRecipeCategory.java @@ -0,0 +1,150 @@ +package net.cmr.jurassicrevived.compat; + +import mezz.jei.api.constants.VanillaTypes; +import mezz.jei.api.gui.builder.IRecipeLayoutBuilder; +import mezz.jei.api.gui.drawable.IDrawable; +import mezz.jei.api.gui.ingredient.IRecipeSlotsView; +import mezz.jei.api.helpers.IGuiHelper; +import mezz.jei.api.recipe.IFocusGroup; +import mezz.jei.api.recipe.RecipeIngredientRole; +import mezz.jei.api.recipe.RecipeType; +import mezz.jei.api.recipe.category.IRecipeCategory; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.block.ModBlocks; +import net.cmr.jurassicrevived.config.JRConfigManager; +import net.cmr.jurassicrevived.recipe.FossilGrinderRecipe; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +public class FossilGrinderRecipeCategory implements IRecipeCategory { + public static final ResourceLocation UID = Constants.rl("fossil_grinding"); + public static final ResourceLocation TEXTURE = Constants.rl("textures/gui/fossil_grinder/fossil_grinder_gui.png"); + private static final ResourceLocation CUTTING_BLADES_TEXTURE = Constants.rl("textures/gui/generic/cutting_blades.png"); + private static final ResourceLocation POWER_BAR_TEXTURE = Constants.rl("textures/gui/generic/power_bar.png"); + + public static final RecipeType FOSSIL_GRINDER_RECIPE_RECIPE_TYPE = + new RecipeType<>(UID, FossilGrinderRecipe.class); + + private final IDrawable background; + private final IDrawable icon; + + public FossilGrinderRecipeCategory(IGuiHelper guiHelper) { + this.background = guiHelper.drawableBuilder(TEXTURE, 0, 0, 176, 80).setTextureSize(176, 166).build(); + this.icon = guiHelper.createDrawableIngredient(VanillaTypes.ITEM_STACK, new ItemStack(ModBlocks.FOSSIL_GRINDER.get())); + } + + @Override + public RecipeType getRecipeType() { + return FOSSIL_GRINDER_RECIPE_RECIPE_TYPE; + } + + @Override + public Component getTitle() { + return Component.translatable("block.jurassicrevived.fossil_grinder"); + } + + @Override + public int getWidth() { + return background.getWidth(); + } + + @Override + public int getHeight() { + return background.getHeight(); + } + + @Override + public void draw(FossilGrinderRecipe recipe, IRecipeSlotsView recipeSlotsView, GuiGraphics guiGraphics, double mouseX, double mouseY) { + background.draw(guiGraphics); + { + final float scale = 1.25f; + final int texSize = 16; + + float baseAngle = (System.currentTimeMillis() % 700L) / 700.0f * ((float)Math.PI * 2.0f); + + java.util.function.BiConsumer drawBlade = (center, ang) -> { + guiGraphics.pose().pushPose(); + guiGraphics.pose().translate(center[0], center[1], 0); + guiGraphics.pose().scale(scale, scale, 1.0f); + if (ang != null) { + guiGraphics.pose().mulPose(com.mojang.math.Axis.ZP.rotation(-ang)); + } + guiGraphics.pose().translate(-texSize / 2f, -texSize / 2f, 0); + guiGraphics.blit(CUTTING_BLADES_TEXTURE, 0, 0, 0, 0, texSize, texSize, texSize, texSize); + guiGraphics.pose().popPose(); + }; + + int x = 0; + int y = 0; + int cx1 = x + 89, cy1 = y + 34; + int cx2 = x + 89, cy2 = y + 52; + + drawBlade.accept(new int[]{cx1, cy1}, baseAngle); + drawBlade.accept(new int[]{cx2, cy2}, -baseAngle); + } + + if (JRConfigManager.get().requirePower) { + guiGraphics.blit(POWER_BAR_TEXTURE, 159, 10, 0, 0, 10, 66, 10, 66); + int barX = 160; + int barY = 11; + int barW = 8; + int barH = 64; + + + int requiredFE = 1000; + int capacityFE = 64000; + int filled = (int)(barH * (requiredFE / (float)capacityFE)); + guiGraphics.fillGradient(barX, barY + (barH - filled), barX + barW, barY + barH, 0xffb51500, 0xff600b00); + + int mx = (int) mouseX; + int my = (int) mouseY; + if (mx >= barX && mx < barX + barW && my >= barY && my < barY + barH) { + List tips = List.of(Component.literal("1000 / 64000 FE")); + guiGraphics.renderTooltip(Minecraft.getInstance().font, tips, java.util.Optional.empty(), mx, my); + } + } + } + + @Override + public @Nullable IDrawable getIcon() { + return icon; + } + + @Override + public void setRecipe(IRecipeLayoutBuilder builder, FossilGrinderRecipe recipe, IFocusGroup focuses) { + + // Single input + builder.addSlot(RecipeIngredientRole.INPUT, 57, 35).addIngredients(recipe.getIngredients().get(0)); + + // If weights are present, show all possible outputs with tooltips; otherwise show the fixed result + if (!recipe.weights().isEmpty()) { + List outputs = new ArrayList<>(); + for (var entry : recipe.weights().entrySet()) { + var item = BuiltInRegistries.ITEM.get(entry.getKey()); + if (item != null) { + ItemStack stack = new ItemStack(item, Math.max(1, recipe.getResultItem(null).getCount())); + outputs.add(stack); + } + } + var slot = builder.addSlot(RecipeIngredientRole.OUTPUT, 103, 17).addItemStacks(outputs); + slot.addRichTooltipCallback((view, tooltip) -> { + var opt = view.getDisplayedItemStack(); + if (opt.isPresent()) { + int weight = recipe.getWeightFor(opt.get().getItem()); + tooltip.add(Component.literal("Weight: " + weight)); + } + }); + return; + } + + builder.addSlot(RecipeIngredientRole.OUTPUT, 103, 17).addItemStack(recipe.getResultItem(null)); + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/compat/IncubatorRecipeCategory.java b/common/src/main/java/net/cmr/jurassicrevived/compat/IncubatorRecipeCategory.java new file mode 100644 index 0000000..d6966cc --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/compat/IncubatorRecipeCategory.java @@ -0,0 +1,131 @@ +package net.cmr.jurassicrevived.compat; + +import mezz.jei.api.constants.VanillaTypes; +import mezz.jei.api.gui.builder.IRecipeLayoutBuilder; +import mezz.jei.api.gui.drawable.IDrawable; +import mezz.jei.api.gui.ingredient.IRecipeSlotsView; +import mezz.jei.api.helpers.IGuiHelper; +import mezz.jei.api.recipe.IFocusGroup; +import mezz.jei.api.recipe.RecipeIngredientRole; +import mezz.jei.api.recipe.RecipeType; +import mezz.jei.api.recipe.category.IRecipeCategory; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.block.ModBlocks; +import net.cmr.jurassicrevived.config.JRConfigManager; +import net.cmr.jurassicrevived.recipe.IncubatorRecipe; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +import net.minecraft.world.item.ItemStack; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public class IncubatorRecipeCategory implements IRecipeCategory { + public static final ResourceLocation UID = Constants.rl("incubating"); + public static final ResourceLocation TEXTURE = Constants.rl("textures/gui/incubator/incubator_gui.png"); + private static final ResourceLocation LANTERN_TEXTURE = Constants.rl("textures/gui/generic/lantern.png"); + private static final ResourceLocation POWER_BAR_TEXTURE = Constants.rl("textures/gui/generic/power_bar.png"); + + public static final RecipeType INCUBATOR_RECIPE_RECIPE_TYPE = + new RecipeType<>(UID, IncubatorRecipe.class); + + + private final IDrawable background; + private final IDrawable icon; + + public IncubatorRecipeCategory(IGuiHelper guiHelper) { + this.background = guiHelper.drawableBuilder(TEXTURE, 0, 0, 176, 80).setTextureSize(176, 166).build(); + this.icon = guiHelper.createDrawableIngredient(VanillaTypes.ITEM_STACK, new ItemStack(ModBlocks.INCUBATOR.get())); + } + + @Override + public RecipeType getRecipeType() { + return INCUBATOR_RECIPE_RECIPE_TYPE; + } + + @Override + public Component getTitle() { + return Component.translatable("block.jurassicrevived.incubator"); + } + + @Override + public int getWidth() { + return background.getWidth(); + } + + @Override + public int getHeight() { + return background.getHeight(); + } + + @Override + public void draw(IncubatorRecipe recipe, IRecipeSlotsView recipeSlotsView, GuiGraphics g, double mouseX, double mouseY) { + background.draw(g); + // Rising flame indicator on all three slots + renderLanternFill(g, 50); + renderLanternFill(g, 80); + renderLanternFill(g, 110); + + // 2-second toggle for which stack to paint on top of each slot + boolean showOutput = ((System.currentTimeMillis() / 2000L) & 1L) == 1L; + ItemStack out = recipe.getResultItem(null); + ItemStack in = recipe.getIngredients().isEmpty() ? ItemStack.EMPTY + : recipe.getIngredients().get(0).getItems().length > 0 + ? recipe.getIngredients().get(0).getItems()[0] + : ItemStack.EMPTY; + + // Paint chosen stack at the three slot centers (icons are 16x16; slot centers are 50/80/110,35) + if (!in.isEmpty() || !out.isEmpty()) { + ItemStack toRender = showOutput && !out.isEmpty() ? out : in; + // Render at all three positions so it looks like each slot is toggling + drawStackIcon(g, toRender, 50, 35); + } + + if (JRConfigManager.get().requirePower) { + g.blit(POWER_BAR_TEXTURE, 159, 10, 0, 0, 10, 66, 10, 66); + int barX = 160, barY = 11, barW = 8, barH = 64; + int requiredFE = 48000, capacityFE = 64000; + int filled = (int)(barH * (requiredFE / (float)capacityFE)); + g.fillGradient(barX, barY + (barH - filled), barX + barW, barY + barH, 0xffb51500, 0xff600b00); + + int mx = (int) mouseX, my = (int) mouseY; + if (mx >= barX && mx < barX + barW && my >= barY && my < barY + barH) { + List tips = List.of(Component.literal("48000 / 64000 FE")); + g.renderTooltip(Minecraft.getInstance().font, tips, java.util.Optional.empty(), mx, my); + } + } + } + + private void drawStackIcon(GuiGraphics g, ItemStack stack, int x, int y) { + if (stack.isEmpty()) return; + g.renderItem(stack, x, y); + g.renderItemDecorations(Minecraft.getInstance().font, stack, x, y); + } + + private void renderLanternFill(GuiGraphics g, int slotPixelX) { + long now = System.currentTimeMillis(); + float t = (now % 10000L) / 10000f; + int l = Mth.ceil(t * 13.0F) + 1; + g.blit(LANTERN_TEXTURE, slotPixelX, 16, 0, 0, 16, l, 16, 16); + } + + @Override + public @Nullable IDrawable getIcon() { + return icon; + } + + @Override + public void setRecipe(IRecipeLayoutBuilder builder, IncubatorRecipe recipe, IFocusGroup focuses) { + // Build static slots once; dynamic toggle is handled in draw() + // Put the recipe’s input and output into each slot so JEI focus/tooltip works regardless of the overlay + var ing = recipe.getIngredients().isEmpty() ? null : recipe.getIngredients().get(0); + if ( ing != null ) { + builder.addSlot(RecipeIngredientRole.INPUT, 50, 35).addIngredients(ing).addItemStack(recipe.getResultItem(null)); + } else { + builder.addSlot(RecipeIngredientRole.OUTPUT, 50, 35).addItemStack(recipe.getResultItem(null)); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/compat/JEIJRPlugin.java b/common/src/main/java/net/cmr/jurassicrevived/compat/JEIJRPlugin.java new file mode 100644 index 0000000..5b30285 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/compat/JEIJRPlugin.java @@ -0,0 +1,231 @@ +package net.cmr.jurassicrevived.compat; + +import dev.architectury.fluid.FluidStack; +import mezz.jei.api.IModPlugin; +import mezz.jei.api.JeiPlugin; +import mezz.jei.api.ingredients.IIngredientType; +import mezz.jei.api.registration.*; +import mezz.jei.api.runtime.IIngredientManager; +import mezz.jei.api.runtime.IJeiRuntime; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.block.ModBlocks; +import net.cmr.jurassicrevived.recipe.*; +import net.cmr.jurassicrevived.screen.ModMenuTypes; +import net.cmr.jurassicrevived.screen.custom.*; +import net.minecraft.client.Minecraft; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.RecipeManager; + +import java.util.List; + +@JeiPlugin +public class JEIJRPlugin implements IModPlugin { + public static final IIngredientType FLUID_STACK_TYPE = () -> FluidStack.class; + + @Override + public ResourceLocation getPluginUid() { + return Constants.rl("jei_plugin"); + } + + // Expose JEI ingredient manager so categories can access all item variants (including mod-provided filled tanks) + public static @org.jetbrains.annotations.Nullable IIngredientManager INGREDIENT_MANAGER; + + @Override + public void onRuntimeAvailable(IJeiRuntime jeiRuntime) { + INGREDIENT_MANAGER = jeiRuntime.getIngredientManager(); + } + + @Override + public void registerIngredients(IModIngredientRegistration registration) { + // We need to register the FluidStack ingredient type to use it in recipes + // However, JEI usually provides its own fluid type on each platform. + // In a common environment, we might be defining a duplicate type if we aren't careful. + // But since we are using Architectury's FluidStack, we can try to register it as a custom ingredient. + // Note: This might not automatically work with JEI's built-in fluid rendering unless we provide a helper. + // For now, we register it so we can use it in our custom renderer. + + // registration.register(FLUID_STACK_TYPE, Collections.emptyList(), new FluidStackHelper(), new FluidStackRenderer()); + // Implementing the helper and renderer is non-trivial without platform specifics. + // If we just want to use it in a custom slot renderer, we might not strictly need to register the *collection* of ingredients, + // but we do need the type to be recognized if we pass it to addIngredient. + + // Actually, addIngredient(IIngredientType type, T ingredient) requires the type to be known? + // JEI documentation says: "Custom ingredients must be registered with IModIngredientRegistration" + + // Given the complexity of cross-platform fluid registration in a common module without a bridge library, + // and that we only want to render a static water stack, we might be better off faking it or waiting for a bridge. + // But let's try to define the constant at least. + } + + @Override + public void registerCategories(IRecipeCategoryRegistration registration) { + registration.addRecipeCategories(new DNAExtractorRecipeCategory(registration.getJeiHelpers().getGuiHelper())); + registration.addRecipeCategories(new DNAAnalyzerRecipeCategory(registration.getJeiHelpers().getGuiHelper())); + registration.addRecipeCategories(new FossilGrinderRecipeCategory(registration.getJeiHelpers().getGuiHelper())); + registration.addRecipeCategories(new FossilCleanerRecipeCategory(registration.getJeiHelpers().getGuiHelper())); + registration.addRecipeCategories(new DNAHybridizerRecipeCategory(registration.getJeiHelpers().getGuiHelper())); + registration.addRecipeCategories(new EmbryonicMachineRecipeCategory(registration.getJeiHelpers().getGuiHelper())); + registration.addRecipeCategories(new EmbryoCalcificationMachineRecipeCategory(registration.getJeiHelpers().getGuiHelper())); + registration.addRecipeCategories(new IncubatorRecipeCategory(registration.getJeiHelpers().getGuiHelper())); + } + + @Override + public void registerRecipes(IRecipeRegistration registration) { + RecipeManager recipeManager = Minecraft.getInstance().level.getRecipeManager(); + + /*? if >1.20.1 {*/ + + /*List dnaExtractorRecipes = recipeManager + .getAllRecipesFor(ModRecipes.DNA_EXTRACTOR_RECIPE_TYPE.get()).stream().map(net.minecraft.world.item.crafting.RecipeHolder::value).toList(); + List dnaAnalyzerRecipes = recipeManager + .getAllRecipesFor(ModRecipes.DNA_ANALYZER_RECIPE_TYPE.get()).stream().map(net.minecraft.world.item.crafting.RecipeHolder::value).toList(); + List fossilGrinderRecipes = recipeManager + .getAllRecipesFor(ModRecipes.FOSSIL_GRINDER_RECIPE_TYPE.get()).stream().map(net.minecraft.world.item.crafting.RecipeHolder::value).toList(); + List fossilCleanerRecipes = recipeManager + .getAllRecipesFor(ModRecipes.FOSSIL_CLEANER_RECIPE_TYPE.get()).stream().map(net.minecraft.world.item.crafting.RecipeHolder::value).toList(); + List dnaHybridizerRecipes = recipeManager + .getAllRecipesFor(ModRecipes.DNA_HYBRIDIZER_RECIPE_TYPE.get()).stream().map(net.minecraft.world.item.crafting.RecipeHolder::value).toList(); + List embryonicMachineRecipes = recipeManager + .getAllRecipesFor(ModRecipes.EMBRYONIC_MACHINE_RECIPE_TYPE.get()).stream().map(net.minecraft.world.item.crafting.RecipeHolder::value).toList(); + List embryoCalcificationMachineRecipes = recipeManager + .getAllRecipesFor(ModRecipes.EMBRYO_CALCIFICATION_MACHINE_RECIPE_TYPE.get()).stream().map(net.minecraft.world.item.crafting.RecipeHolder::value).toList(); + List incubatorRecipes = recipeManager + .getAllRecipesFor(ModRecipes.INCUBATOR_RECIPE_TYPE.get()).stream().map(net.minecraft.world.item.crafting.RecipeHolder::value).toList(); + *//*?} else {*/ + List dnaExtractorRecipes = recipeManager.getAllRecipesFor(ModRecipes.DNA_EXTRACTOR_RECIPE_TYPE.get()); + List dnaAnalyzerRecipes = recipeManager.getAllRecipesFor(ModRecipes.DNA_ANALYZER_RECIPE_TYPE.get()); + List fossilGrinderRecipes = recipeManager.getAllRecipesFor(ModRecipes.FOSSIL_GRINDER_RECIPE_TYPE.get()); + List fossilCleanerRecipes = recipeManager.getAllRecipesFor(ModRecipes.FOSSIL_CLEANER_RECIPE_TYPE.get()); + List dnaHybridizerRecipes = recipeManager.getAllRecipesFor(ModRecipes.DNA_HYBRIDIZER_RECIPE_TYPE.get()); + List embryonicMachineRecipes = recipeManager.getAllRecipesFor(ModRecipes.EMBRYONIC_MACHINE_RECIPE_TYPE.get()); + List embryoCalcificationMachineRecipes = recipeManager.getAllRecipesFor(ModRecipes.EMBRYO_CALCIFICATION_MACHINE_RECIPE_TYPE.get()); + List incubatorRecipes = recipeManager.getAllRecipesFor(ModRecipes.INCUBATOR_RECIPE_TYPE.get()); + /*?}*/ + + registration.addRecipes(DNAExtractorRecipeCategory.DNA_EXTRACTOR_RECIPE_RECIPE_TYPE, dnaExtractorRecipes); + registration.addRecipes(DNAAnalyzerRecipeCategory.DNA_ANALYZER_RECIPE_RECIPE_TYPE, dnaAnalyzerRecipes); + registration.addRecipes(FossilGrinderRecipeCategory.FOSSIL_GRINDER_RECIPE_RECIPE_TYPE, fossilGrinderRecipes); + registration.addRecipes(FossilCleanerRecipeCategory.FOSSIL_CLEANER_RECIPE_RECIPE_TYPE, fossilCleanerRecipes); + registration.addRecipes(DNAHybridizerRecipeCategory.DNA_HYBRIDIZER_RECIPE_RECIPE_TYPE, dnaHybridizerRecipes); + registration.addRecipes(EmbryonicMachineRecipeCategory.EMBRYONIC_MACHINE_RECIPE_RECIPE_TYPE, embryonicMachineRecipes); + registration.addRecipes(EmbryoCalcificationMachineRecipeCategory.EMBRYO_CALCIFICATION_MACHINE_RECIPE_RECIPE_TYPE, embryoCalcificationMachineRecipes); + registration.addRecipes(IncubatorRecipeCategory.INCUBATOR_RECIPE_RECIPE_TYPE, incubatorRecipes); + } + + @Override + public void registerGuiHandlers(IGuiHandlerRegistration registration) { + registration.addRecipeClickArea(DNAExtractorScreen.class, 81, 31, 14, 25, + DNAExtractorRecipeCategory.DNA_EXTRACTOR_RECIPE_RECIPE_TYPE); + registration.addRecipeClickArea(DNAAnalyzerScreen.class, 81, 31, 14, 25, + DNAAnalyzerRecipeCategory.DNA_ANALYZER_RECIPE_RECIPE_TYPE); + registration.addRecipeClickArea(FossilGrinderScreen.class, 80, 25, 18, 36, + FossilGrinderRecipeCategory.FOSSIL_GRINDER_RECIPE_RECIPE_TYPE); + registration.addRecipeClickArea(FossilCleanerScreen.class, 74, 35, 29, 16, + FossilCleanerRecipeCategory.FOSSIL_CLEANER_RECIPE_RECIPE_TYPE); + registration.addRecipeClickArea(DNAHybridizerScreen.class, 105, 35, 24, 16, + DNAHybridizerRecipeCategory.DNA_HYBRIDIZER_RECIPE_RECIPE_TYPE); + registration.addRecipeClickArea(EmbryonicMachineScreen.class, 76, 35, 24, 16, + EmbryonicMachineRecipeCategory.EMBRYONIC_MACHINE_RECIPE_RECIPE_TYPE); + registration.addRecipeClickArea(EmbryoCalcificationMachineScreen.class, 76, 35, 24, 16, + EmbryoCalcificationMachineRecipeCategory.EMBRYO_CALCIFICATION_MACHINE_RECIPE_RECIPE_TYPE); + registration.addRecipeClickArea(IncubatorScreen.class, 51, 16, 72, 16, + IncubatorRecipeCategory.INCUBATOR_RECIPE_RECIPE_TYPE); + } + + @Override + public void registerRecipeCatalysts(IRecipeCatalystRegistration registration) { + registration.addRecipeCatalyst(new ItemStack(ModBlocks.DNA_EXTRACTOR.get()), DNAExtractorRecipeCategory.DNA_EXTRACTOR_RECIPE_RECIPE_TYPE); + registration.addRecipeCatalyst(new ItemStack(ModBlocks.DNA_ANALYZER.get()), DNAAnalyzerRecipeCategory.DNA_ANALYZER_RECIPE_RECIPE_TYPE); + registration.addRecipeCatalyst(new ItemStack(ModBlocks.FOSSIL_GRINDER.get()), FossilGrinderRecipeCategory.FOSSIL_GRINDER_RECIPE_RECIPE_TYPE); + registration.addRecipeCatalyst(new ItemStack(ModBlocks.FOSSIL_CLEANER.get()), FossilCleanerRecipeCategory.FOSSIL_CLEANER_RECIPE_RECIPE_TYPE); + registration.addRecipeCatalyst(new ItemStack(ModBlocks.DNA_HYBRIDIZER.get()), DNAHybridizerRecipeCategory.DNA_HYBRIDIZER_RECIPE_RECIPE_TYPE); + registration.addRecipeCatalyst(new ItemStack(ModBlocks.EMBRYONIC_MACHINE.get()), EmbryonicMachineRecipeCategory.EMBRYONIC_MACHINE_RECIPE_RECIPE_TYPE); + registration.addRecipeCatalyst(new ItemStack(ModBlocks.EMBRYO_CALCIFICATION_MACHINE.get()), EmbryoCalcificationMachineRecipeCategory.EMBRYO_CALCIFICATION_MACHINE_RECIPE_RECIPE_TYPE); + registration.addRecipeCatalyst(new ItemStack(ModBlocks.INCUBATOR.get()), IncubatorRecipeCategory.INCUBATOR_RECIPE_RECIPE_TYPE); + registration.addRecipeCatalyst(new ItemStack(ModBlocks.WHITE_DNA_EXTRACTOR.get()), DNAExtractorRecipeCategory.DNA_EXTRACTOR_RECIPE_RECIPE_TYPE); + registration.addRecipeCatalyst(new ItemStack(ModBlocks.WHITE_DNA_ANALYZER.get()), DNAAnalyzerRecipeCategory.DNA_ANALYZER_RECIPE_RECIPE_TYPE); + registration.addRecipeCatalyst(new ItemStack(ModBlocks.WHITE_FOSSIL_GRINDER.get()), FossilGrinderRecipeCategory.FOSSIL_GRINDER_RECIPE_RECIPE_TYPE); + registration.addRecipeCatalyst(new ItemStack(ModBlocks.WHITE_FOSSIL_CLEANER.get()), FossilCleanerRecipeCategory.FOSSIL_CLEANER_RECIPE_RECIPE_TYPE); + registration.addRecipeCatalyst(new ItemStack(ModBlocks.WHITE_DNA_HYBRIDIZER.get()), DNAHybridizerRecipeCategory.DNA_HYBRIDIZER_RECIPE_RECIPE_TYPE); + registration.addRecipeCatalyst(new ItemStack(ModBlocks.WHITE_EMBRYONIC_MACHINE.get()), EmbryonicMachineRecipeCategory.EMBRYONIC_MACHINE_RECIPE_RECIPE_TYPE); + registration.addRecipeCatalyst(new ItemStack(ModBlocks.WHITE_EMBRYO_CALCIFICATION_MACHINE.get()), EmbryoCalcificationMachineRecipeCategory.EMBRYO_CALCIFICATION_MACHINE_RECIPE_RECIPE_TYPE); + registration.addRecipeCatalyst(new ItemStack(ModBlocks.WHITE_INCUBATOR.get()), IncubatorRecipeCategory.INCUBATOR_RECIPE_RECIPE_TYPE); + } + + @Override + public void registerRecipeTransferHandlers(IRecipeTransferRegistration registration) { + registration.addRecipeTransferHandler( + DNAExtractorMenu.class, + ModMenuTypes.DNA_EXTRACTOR_MENU.get(), + DNAExtractorRecipeCategory.DNA_EXTRACTOR_RECIPE_RECIPE_TYPE, + 36, // The index of the FIRST recipe input slot in your Menu (slot 36) + 2, // The NUMBER of recipe input slots (slots 36, 37) + 0, // The index where the player inventory slots START (slot 0) + 36 // The NUMBER of player inventory slots to check (slots 0-35) + ); + registration.addRecipeTransferHandler( + DNAAnalyzerMenu.class, + ModMenuTypes.DNA_ANALYZER_MENU.get(), + DNAAnalyzerRecipeCategory.DNA_ANALYZER_RECIPE_RECIPE_TYPE, + 36, // The index of the FIRST recipe input slot in your Menu (slot 36) + 2, // The NUMBER of recipe input slots (slots 36, 37) + 0, // The index where the player inventory slots START (slot 0) + 36 // The NUMBER of player inventory slots to check (slots 0-35) + ); + registration.addRecipeTransferHandler( + FossilGrinderMenu.class, + ModMenuTypes.FOSSIL_GRINDER_MENU.get(), + FossilGrinderRecipeCategory.FOSSIL_GRINDER_RECIPE_RECIPE_TYPE, + 36, // The index of the FIRST recipe input slot in your Menu (slot 36) + 1, // The NUMBER of recipe input slots (slots 36, 37) + 0, // The index where the player inventory slots START (slot 0) + 36 // The NUMBER of player inventory slots to check (slots 0-35) + ); + registration.addRecipeTransferHandler( + FossilCleanerMenu.class, + ModMenuTypes.FOSSIL_CLEANER_MENU.get(), + FossilCleanerRecipeCategory.FOSSIL_CLEANER_RECIPE_RECIPE_TYPE, + 36, // The index of the FIRST recipe input slot in your Menu (slot 36) + 2, // The NUMBER of recipe input slots (slots 36, 37) + 0, // The index where the player inventory slots START (slot 0) + 36 // The NUMBER of player inventory slots to check (slots 0-35) + ); + registration.addRecipeTransferHandler( + DNAHybridizerMenu.class, + ModMenuTypes.DNA_HYBRIDIZER_MENU.get(), + DNAHybridizerRecipeCategory.DNA_HYBRIDIZER_RECIPE_RECIPE_TYPE, + 36, // The index of the FIRST recipe input slot in your Menu (slot 36) + 9, // The NUMBER of recipe input slots (slots 36, 37) + 0, // The index where the player inventory slots START (slot 0) + 36 // The NUMBER of player inventory slots to check (slots 0-35) + ); + registration.addRecipeTransferHandler( + EmbryonicMachineMenu.class, + ModMenuTypes.EMBRYONIC_MACHINE_MENU.get(), + EmbryonicMachineRecipeCategory.EMBRYONIC_MACHINE_RECIPE_RECIPE_TYPE, + 36, // The index of the FIRST recipe input slot in your Menu (slot 36) + 2, // The NUMBER of recipe input slots (slots 36, 37) + 0, // The index where the player inventory slots START (slot 0) + 36 // The NUMBER of player inventory slots to check (slots 0-35) + ); + registration.addRecipeTransferHandler( + EmbryoCalcificationMachineMenu.class, + ModMenuTypes.EMBRYO_CALCIFICATION_MACHINE_MENU.get(), + EmbryoCalcificationMachineRecipeCategory.EMBRYO_CALCIFICATION_MACHINE_RECIPE_RECIPE_TYPE, + 36, // The index of the FIRST recipe input slot in your Menu (slot 36) + 2, // The NUMBER of recipe input slots (slots 36, 37) + 0, // The index where the player inventory slots START (slot 0) + 36 // The NUMBER of player inventory slots to check (slots 0-35) + ); + registration.addRecipeTransferHandler( + IncubatorMenu.class, + ModMenuTypes.INCUBATOR_MENU.get(), + IncubatorRecipeCategory.INCUBATOR_RECIPE_RECIPE_TYPE, + 36, // The index of the FIRST recipe input slot in your Menu (slot 36) + 3, // The NUMBER of recipe input slots (slots 36, 37) + 0, // The index where the player inventory slots START (slot 0) + 36 // The NUMBER of player inventory slots to check (slots 0-35) + ); + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/compat/MachineJadePlugin.java b/common/src/main/java/net/cmr/jurassicrevived/compat/MachineJadePlugin.java new file mode 100644 index 0000000..91c66ee --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/compat/MachineJadePlugin.java @@ -0,0 +1,426 @@ +package net.cmr.jurassicrevived.compat; + +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.block.custom.*; +import net.cmr.jurassicrevived.block.entity.custom.*; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +import net.minecraft.world.level.block.entity.BlockEntity; +import snownee.jade.api.*; +import snownee.jade.api.config.IPluginConfig; +import snownee.jade.api.ui.*; +//? if <=1.20.1 { +import net.minecraft.client.gui.navigation.ScreenDirection; +import snownee.jade.impl.ui.ProgressStyle; +//?} + +@WailaPlugin +public class MachineJadePlugin implements IWailaPlugin { + private static final ResourceLocation UID = Constants.rl("machine_progress"); + private static final String NBT_PROGRESS = "jr_progress"; + private static final String NBT_MAX = "jr_max"; + + @Override + public void registerClient(IWailaClientRegistration reg) { + reg.registerBlockComponent(new snownee.jade.api.IBlockComponentProvider() { + //? if >1.20.1 { + /*@Override + public void appendTooltip(ITooltip tooltip, BlockAccessor accessor, IPluginConfig config) { + CompoundTag data = accessor.getServerData(); + if (data == null || !data.contains(NBT_PROGRESS) || !data.contains(NBT_MAX)) return; + + int progress = Math.max(0, data.getInt(NBT_PROGRESS)); + int max = Math.max(1, data.getInt(NBT_MAX)); + float ratio = Mth.clamp(progress / (float) max, 0.0f, 1.0f); + + IElementHelper h = IElementHelper.get(); + ProgressStyle style = h.progressStyle() + .color(0xFFFFFFFF, 0xFFFFFFFF) + .direction(ScreenDirection.RIGHT) + .fitContentX(true) + .fitContentY(true); + BoxStyle box = BoxStyle.getNestedBox(); + IElement bar = h.progress(ratio, Component.empty(), style, box, true); + tooltip.add(bar); + } + *///?} else { + @Override + public void appendTooltip(ITooltip tooltip, BlockAccessor accessor, IPluginConfig config) { + CompoundTag data = accessor.getServerData(); + if (!data.contains(NBT_PROGRESS) || !data.contains(NBT_MAX)) return; + + int progress = data.getInt(NBT_PROGRESS); + int max = Math.max(1, data.getInt(NBT_MAX)); + float ratio = Mth.clamp(progress / (float) max, 0.0f, 1.0f); + + IElementHelper h = tooltip.getElementHelper(); + IProgressStyle pStyle = h.progressStyle() + .color(0xFFFFFFFF) + .textColor(0xFFFFFFFF); + IBoxStyle box = new ThickBorderBox(1.0f); + + tooltip.add(h.progress(ratio, Component.empty(), pStyle, box, false)); + } + //?} + @Override + public ResourceLocation getUid() { + return UID; + } + }, DNAExtractorBlock.class + ); + reg.registerBlockComponent(new snownee.jade.api.IBlockComponentProvider() { + //? if >1.20.1 { + /*@Override + public void appendTooltip(ITooltip tooltip, BlockAccessor accessor, IPluginConfig config) { + CompoundTag data = accessor.getServerData(); + if (data == null || !data.contains(NBT_PROGRESS) || !data.contains(NBT_MAX)) return; + + int progress = Math.max(0, data.getInt(NBT_PROGRESS)); + int max = Math.max(1, data.getInt(NBT_MAX)); + float ratio = Mth.clamp(progress / (float) max, 0.0f, 1.0f); + + IElementHelper h = IElementHelper.get(); + ProgressStyle style = h.progressStyle() + .color(0xFFFFFFFF, 0xFFFFFFFF) + .direction(ScreenDirection.RIGHT) + .fitContentX(true) + .fitContentY(true); + BoxStyle box = BoxStyle.getNestedBox(); + IElement bar = h.progress(ratio, Component.empty(), style, box, true); + tooltip.add(bar); + } + *///?} else { + @Override + public void appendTooltip(ITooltip tooltip, BlockAccessor accessor, IPluginConfig config) { + CompoundTag data = accessor.getServerData(); + if (!data.contains(NBT_PROGRESS) || !data.contains(NBT_MAX)) return; + + int progress = data.getInt(NBT_PROGRESS); + int max = Math.max(1, data.getInt(NBT_MAX)); + float ratio = Mth.clamp(progress / (float) max, 0.0f, 1.0f); + + IElementHelper h = tooltip.getElementHelper(); + IProgressStyle pStyle = h.progressStyle() + .color(0xFFFFFFFF) + .textColor(0xFFFFFFFF); + IBoxStyle box = new ThickBorderBox(1.0f); + + tooltip.add(h.progress(ratio, Component.empty(), pStyle, box, false)); + } + //?} + @Override + public ResourceLocation getUid() { + return UID; + } + }, DNAAnalyzerBlock.class + ); + reg.registerBlockComponent(new snownee.jade.api.IBlockComponentProvider() { + //? if >1.20.1 { + /*@Override + public void appendTooltip(ITooltip tooltip, BlockAccessor accessor, IPluginConfig config) { + CompoundTag data = accessor.getServerData(); + if (data == null || !data.contains(NBT_PROGRESS) || !data.contains(NBT_MAX)) return; + + int progress = Math.max(0, data.getInt(NBT_PROGRESS)); + int max = Math.max(1, data.getInt(NBT_MAX)); + float ratio = Mth.clamp(progress / (float) max, 0.0f, 1.0f); + + IElementHelper h = IElementHelper.get(); + ProgressStyle style = h.progressStyle() + .color(0xFFFFFFFF, 0xFFFFFFFF) + .direction(ScreenDirection.RIGHT) + .fitContentX(true) + .fitContentY(true); + BoxStyle box = BoxStyle.getNestedBox(); + IElement bar = h.progress(ratio, Component.empty(), style, box, true); + tooltip.add(bar); + } + *///?} else { + @Override + public void appendTooltip(ITooltip tooltip, BlockAccessor accessor, IPluginConfig config) { + CompoundTag data = accessor.getServerData(); + if (!data.contains(NBT_PROGRESS) || !data.contains(NBT_MAX)) return; + + int progress = data.getInt(NBT_PROGRESS); + int max = Math.max(1, data.getInt(NBT_MAX)); + float ratio = Mth.clamp(progress / (float) max, 0.0f, 1.0f); + + IElementHelper h = tooltip.getElementHelper(); + IProgressStyle pStyle = h.progressStyle() + .color(0xFFFFFFFF) + .textColor(0xFFFFFFFF); + IBoxStyle box = new ThickBorderBox(1.0f); + + tooltip.add(h.progress(ratio, Component.empty(), pStyle, box, false)); + } + //?} + + @Override + public ResourceLocation getUid() { + return UID; + } + }, DNAHybridizerBlock.class + ); + reg.registerBlockComponent(new snownee.jade.api.IBlockComponentProvider() { + //? if >1.20.1 { + /*@Override + public void appendTooltip(ITooltip tooltip, BlockAccessor accessor, IPluginConfig config) { + CompoundTag data = accessor.getServerData(); + if (data == null || !data.contains(NBT_PROGRESS) || !data.contains(NBT_MAX)) return; + + int progress = Math.max(0, data.getInt(NBT_PROGRESS)); + int max = Math.max(1, data.getInt(NBT_MAX)); + float ratio = Mth.clamp(progress / (float) max, 0.0f, 1.0f); + + IElementHelper h = IElementHelper.get(); + ProgressStyle style = h.progressStyle() + .color(0xFFFFFFFF, 0xFFFFFFFF) + .direction(ScreenDirection.RIGHT) + .fitContentX(true) + .fitContentY(true); + BoxStyle box = BoxStyle.getNestedBox(); + IElement bar = h.progress(ratio, Component.empty(), style, box, true); + tooltip.add(bar); + } + *///?} else { + @Override + public void appendTooltip(ITooltip tooltip, BlockAccessor accessor, IPluginConfig config) { + CompoundTag data = accessor.getServerData(); + if (!data.contains(NBT_PROGRESS) || !data.contains(NBT_MAX)) return; + + int progress = data.getInt(NBT_PROGRESS); + int max = Math.max(1, data.getInt(NBT_MAX)); + float ratio = Mth.clamp(progress / (float) max, 0.0f, 1.0f); + + IElementHelper h = tooltip.getElementHelper(); + IProgressStyle pStyle = h.progressStyle() + .color(0xFFFFFFFF) + .textColor(0xFFFFFFFF); + IBoxStyle box = new ThickBorderBox(1.0f); + + tooltip.add(h.progress(ratio, Component.empty(), pStyle, box, false)); + } + //?} + + @Override + public ResourceLocation getUid() { + return UID; + } + }, EmbryoCalcificationMachineBlock.class + ); + reg.registerBlockComponent(new snownee.jade.api.IBlockComponentProvider() { + //? if >1.20.1 { + /*@Override + public void appendTooltip(ITooltip tooltip, BlockAccessor accessor, IPluginConfig config) { + CompoundTag data = accessor.getServerData(); + if (data == null || !data.contains(NBT_PROGRESS) || !data.contains(NBT_MAX)) return; + + int progress = Math.max(0, data.getInt(NBT_PROGRESS)); + int max = Math.max(1, data.getInt(NBT_MAX)); + float ratio = Mth.clamp(progress / (float) max, 0.0f, 1.0f); + + IElementHelper h = IElementHelper.get(); + ProgressStyle style = h.progressStyle() + .color(0xFFFFFFFF, 0xFFFFFFFF) + .direction(ScreenDirection.RIGHT) + .fitContentX(true) + .fitContentY(true); + BoxStyle box = BoxStyle.getNestedBox(); + IElement bar = h.progress(ratio, Component.empty(), style, box, true); + tooltip.add(bar); + } + *///?} else { + @Override + public void appendTooltip(ITooltip tooltip, BlockAccessor accessor, IPluginConfig config) { + CompoundTag data = accessor.getServerData(); + if (!data.contains(NBT_PROGRESS) || !data.contains(NBT_MAX)) return; + + int progress = data.getInt(NBT_PROGRESS); + int max = Math.max(1, data.getInt(NBT_MAX)); + float ratio = Mth.clamp(progress / (float) max, 0.0f, 1.0f); + + IElementHelper h = tooltip.getElementHelper(); + IProgressStyle pStyle = h.progressStyle() + .color(0xFFFFFFFF) + .textColor(0xFFFFFFFF); + IBoxStyle box = new ThickBorderBox(1.0f); + + tooltip.add(h.progress(ratio, Component.empty(), pStyle, box, false)); + } + //?} + + @Override + public ResourceLocation getUid() { + return UID; + } + }, EmbryonicMachineBlock.class + ); + reg.registerBlockComponent(new snownee.jade.api.IBlockComponentProvider() { + //? if >1.20.1 { + /*@Override + public void appendTooltip(ITooltip tooltip, BlockAccessor accessor, IPluginConfig config) { + CompoundTag data = accessor.getServerData(); + if (data == null || !data.contains(NBT_PROGRESS) || !data.contains(NBT_MAX)) return; + + int progress = Math.max(0, data.getInt(NBT_PROGRESS)); + int max = Math.max(1, data.getInt(NBT_MAX)); + float ratio = Mth.clamp(progress / (float) max, 0.0f, 1.0f); + + IElementHelper h = IElementHelper.get(); + ProgressStyle style = h.progressStyle() + .color(0xFFFFFFFF, 0xFFFFFFFF) + .direction(ScreenDirection.RIGHT) + .fitContentX(true) + .fitContentY(true); + BoxStyle box = BoxStyle.getNestedBox(); + IElement bar = h.progress(ratio, Component.empty(), style, box, true); + tooltip.add(bar); + } + *///?} else { + @Override + public void appendTooltip(ITooltip tooltip, BlockAccessor accessor, IPluginConfig config) { + CompoundTag data = accessor.getServerData(); + if (!data.contains(NBT_PROGRESS) || !data.contains(NBT_MAX)) return; + + int progress = data.getInt(NBT_PROGRESS); + int max = Math.max(1, data.getInt(NBT_MAX)); + float ratio = Mth.clamp(progress / (float) max, 0.0f, 1.0f); + + IElementHelper h = tooltip.getElementHelper(); + IProgressStyle pStyle = h.progressStyle() + .color(0xFFFFFFFF) + .textColor(0xFFFFFFFF); + IBoxStyle box = new ThickBorderBox(1.0f); + + tooltip.add(h.progress(ratio, Component.empty(), pStyle, box, false)); + } + //?} + + @Override + public ResourceLocation getUid() { + return UID; + } + }, FossilGrinderBlock.class + ); + reg.registerBlockComponent(new snownee.jade.api.IBlockComponentProvider() { + //? if >1.20.1 { + /*@Override + public void appendTooltip(ITooltip tooltip, BlockAccessor accessor, IPluginConfig config) { + CompoundTag data = accessor.getServerData(); + if (data == null || !data.contains(NBT_PROGRESS) || !data.contains(NBT_MAX)) return; + + int progress = Math.max(0, data.getInt(NBT_PROGRESS)); + int max = Math.max(1, data.getInt(NBT_MAX)); + float ratio = Mth.clamp(progress / (float) max, 0.0f, 1.0f); + + IElementHelper h = IElementHelper.get(); + ProgressStyle style = h.progressStyle() + .color(0xFFFFFFFF, 0xFFFFFFFF) + .direction(ScreenDirection.RIGHT) + .fitContentX(true) + .fitContentY(true); + BoxStyle box = BoxStyle.getNestedBox(); + IElement bar = h.progress(ratio, Component.empty(), style, box, true); + tooltip.add(bar); + } + *///?} else { + @Override + public void appendTooltip(ITooltip tooltip, BlockAccessor accessor, IPluginConfig config) { + CompoundTag data = accessor.getServerData(); + if (!data.contains(NBT_PROGRESS) || !data.contains(NBT_MAX)) return; + + int progress = data.getInt(NBT_PROGRESS); + int max = Math.max(1, data.getInt(NBT_MAX)); + float ratio = Mth.clamp(progress / (float) max, 0.0f, 1.0f); + + IElementHelper h = tooltip.getElementHelper(); + IProgressStyle pStyle = h.progressStyle() + .color(0xFFFFFFFF) + .textColor(0xFFFFFFFF); + IBoxStyle box = new ThickBorderBox(1.0f); + + tooltip.add(h.progress(ratio, Component.empty(), pStyle, box, false)); + } + //?} + + @Override + public ResourceLocation getUid() { + return UID; + } + }, IncubatorBlock.class + ); + } + + @Override + public void register(IWailaCommonRegistration reg) { + IServerDataProvider provider = new IServerDataProvider<>() { + @Override + public void appendServerData(CompoundTag data, BlockAccessor accessor) { + BlockEntity be = accessor.getBlockEntity(); + if (be == null) return; + + if (be instanceof IncubatorBlockEntity inc) { + int[] p = getArray(inc, "progress"); + int[] m = getArray(inc, "maxProgress"); + if (p != null && m != null && p.length == m.length && p.length > 0) { + float best = 0f; + for (int i = 0; i < p.length; i++) { + int max = Math.max(1, m[i]); + best = Math.max(best, p[i] / (float) max); + } + int scaledMax = 1000; + int scaledProgress = Math.round(best * scaledMax); + put(data, scaledProgress, scaledMax); + } + return; + } + + int progress = getInt(be, "progress"); + int max = Math.max(1, getInt(be, "maxProgress")); + put(data, progress, max); + } + + private void put(CompoundTag data, int progress, int max) { + data.putInt(NBT_PROGRESS, progress); + data.putInt(NBT_MAX, Math.max(1, max)); + } + + private int getInt(Object be, String field) { + try { + var f = be.getClass().getDeclaredField(field); + f.setAccessible(true); + return f.getInt(be); + } catch (Throwable t) { + return 0; + } + } + + private int[] getArray(Object be, String field) { + try { + var f = be.getClass().getDeclaredField(field); + f.setAccessible(true); + return (int[]) f.get(be); + } catch (Throwable t) { + return null; + } + } + + @Override + public ResourceLocation getUid() { + return UID; + } + }; + + // Register individually per BE class + reg.registerBlockDataProvider(provider, DNAExtractorBlockEntity.class); + reg.registerBlockDataProvider(provider, DNAAnalyzerBlockEntity.class); + reg.registerBlockDataProvider(provider, DNAHybridizerBlockEntity.class); + reg.registerBlockDataProvider(provider, EmbryoCalcificationMachineBlockEntity.class); + reg.registerBlockDataProvider(provider, EmbryonicMachineBlockEntity.class); + reg.registerBlockDataProvider(provider, FossilCleanerBlockEntity.class); + reg.registerBlockDataProvider(provider, FossilGrinderBlockEntity.class); + reg.registerBlockDataProvider(provider, IncubatorBlockEntity.class); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/compat/ThickBorderBox.java b/common/src/main/java/net/cmr/jurassicrevived/compat/ThickBorderBox.java new file mode 100644 index 0000000..3cd722a --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/compat/ThickBorderBox.java @@ -0,0 +1,41 @@ +package net.cmr.jurassicrevived.compat; +//? if <=1.20.1 { +import net.minecraft.client.gui.GuiGraphics; +import snownee.jade.api.ui.IBoxStyle; + +// Simple solid border with configurable width and color (ARGB) +public final class ThickBorderBox implements IBoxStyle { + private final float width; + private final int borderColor; // ARGB, e.g. 0xFFFFFFFF for white + + public ThickBorderBox(float width) { + this(width, 0xFFFFFFFF); // default opaque white + } + + public ThickBorderBox(float width, int borderColor) { + this.width = width; + this.borderColor = borderColor; + } + + @Override + public float borderWidth() { + return width; + } + + @Override + public void render(GuiGraphics g, float x, float y, float w, float h) { + if (width <= 0) return; + float r = x + w; + float b = y + h; + + // top + g.fill((int) x, (int) y, (int) r, (int) (y + width), borderColor); + // bottom + g.fill((int) x, (int) (b - width), (int) r, (int) b, borderColor); + // left + g.fill((int) x, (int) (y + width), (int) (x + width), (int) (b - width), borderColor); + // right + g.fill((int) (r - width), (int) (y + width), (int) r, (int) (b - width), borderColor); + } +} +//?} diff --git a/common/src/main/java/net/cmr/jurassicrevived/config/JRConfig.java b/common/src/main/java/net/cmr/jurassicrevived/config/JRConfig.java index ae2156b..5699747 100644 --- a/common/src/main/java/net/cmr/jurassicrevived/config/JRConfig.java +++ b/common/src/main/java/net/cmr/jurassicrevived/config/JRConfig.java @@ -4,6 +4,10 @@ public final class JRConfig { // Example options (replace with your real ones) public boolean enableDinosaurs = true; public int spawnWeight = 10; + public boolean requirePower = true; + public int fePerSecond = 1000; + public int itemsPerSecond = 10; + public int milliBucketsPerSecond = 1000; public JRConfig() { } diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/ModEntities.java b/common/src/main/java/net/cmr/jurassicrevived/entity/ModEntities.java index a56fa14..2df9fb5 100644 --- a/common/src/main/java/net/cmr/jurassicrevived/entity/ModEntities.java +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/ModEntities.java @@ -1,22 +1,427 @@ package net.cmr.jurassicrevived.entity; +import dev.architectury.registry.level.entity.EntityAttributeRegistry; +import dev.architectury.registry.level.entity.SpawnPlacementsRegistry; import dev.architectury.registry.registries.DeferredRegister; +import dev.architectury.registry.registries.RegistrySupplier; import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.config.JRConfigManager; +import net.cmr.jurassicrevived.entity.custom.*; import net.minecraft.core.registries.Registries; import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.MobCategory; +/*? if >1.20.1 {*/ +/*import net.minecraft.world.entity.SpawnPlacementTypes; +*//*?} else {*/ +import net.minecraft.world.entity.SpawnPlacements; +/*?}*/ +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.level.levelgen.Heightmap; public class ModEntities { public static final DeferredRegister> ENTITIES = DeferredRegister.create(Constants.MOD_ID, Registries.ENTITY_TYPE); - // --- Example (Generic Entity) --- - // You will need your own Entity class (e.g., VelociraptorEntity::new) - /* - public static final RegistrySupplier> VELOCIRAPTOR = ENTITIES.register("velociraptor", - () -> EntityType.Builder.of(VelociraptorEntity::new, MobCategory.CREATURE) - .sized(1.0f, 2.0f) - .build(Constants.MOD_ID + ":velociraptor")); - */ + public static final RegistrySupplier> SEAT = + ENTITIES.register("seat", () -> + EntityType.Builder.of(SeatEntity::new, MobCategory.MISC) + .sized(0.001f, 0.001f) + .clientTrackingRange(16) + .updateInterval(1) + .build("jurassicrevived:seat") + ); + + public static final RegistrySupplier> ALBERTOSAURUS = + ENTITIES.register("albertosaurus", () -> EntityType.Builder.of(AlbertosaurusEntity::new, MobCategory.CREATURE) + .sized(1.875f, 3.0f).build("albertosaurus")); + + public static final RegistrySupplier> APATOSAURUS = + ENTITIES.register("apatosaurus", () -> EntityType.Builder.of(ApatosaurusEntity::new, MobCategory.CREATURE) + .sized(1.875f, 2.375f).build("apatosaurus")); + + public static final RegistrySupplier> BRACHIOSAURUS = + ENTITIES.register("brachiosaurus", () -> EntityType.Builder.of(BrachiosaurusEntity::new, MobCategory.CREATURE) + .sized(1.875f, 2.375f).build("brachiosaurus")); + + public static final RegistrySupplier> CERATOSAURUS = + ENTITIES.register("ceratosaurus", () -> EntityType.Builder.of(CeratosaurusEntity::new, MobCategory.CREATURE) + .sized(1.875f, 2.375f).build("ceratosaurus")); + + public static final RegistrySupplier> COMPSOGNATHUS = + ENTITIES.register("compsognathus", () -> EntityType.Builder.of(CompsognathusEntity::new, MobCategory.CREATURE) + .sized(1.875f, 2.375f).build("compsognathus")); + + public static final RegistrySupplier> DILOPHOSAURUS = + ENTITIES.register("dilophosaurus", () -> EntityType.Builder.of(DilophosaurusEntity::new, MobCategory.CREATURE) + .sized(1.875f, 2.375f).build("dilophosaurus")); + + public static final RegistrySupplier> DIPLODOCUS = + ENTITIES.register("diplodocus", () -> EntityType.Builder.of(DiplodocusEntity::new, MobCategory.CREATURE) + .sized(1.875f, 2.375f).build("diplodocus")); + + public static final RegistrySupplier> FDUCK = + ENTITIES.register("fduck", () -> EntityType.Builder.of(FDuckEntity::new, MobCategory.CREATURE) + .sized(1.875f, 2.375f).build("fduck")); + + public static final RegistrySupplier> GALLIMIMUS = + ENTITIES.register("gallimimus", () -> EntityType.Builder.of(GallimimusEntity::new, MobCategory.CREATURE) + .sized(1.875f, 2.375f).build("gallimimus")); + + public static final RegistrySupplier> INDOMINUS_REX = + ENTITIES.register("indominus_rex", () -> EntityType.Builder.of(IndominusRexEntity::new, MobCategory.CREATURE) + .sized(1.875f, 2.375f).build("indominus_rex")); + + public static final RegistrySupplier> OURANOSAURUS = + ENTITIES.register("ouranosaurus", () -> EntityType.Builder.of(OuranosaurusEntity::new, MobCategory.CREATURE) + .sized(1.875f, 2.375f).build("ouranosaurus")); + + public static final RegistrySupplier> PARASAUROLOPHUS = + ENTITIES.register("parasaurolophus", () -> EntityType.Builder.of(ParasaurolophusEntity::new, MobCategory.CREATURE) + .sized(1.875f, 2.375f).build("parasaurolophus")); + + public static final RegistrySupplier> SPINOSAURUS = + ENTITIES.register("spinosaurus", () -> EntityType.Builder.of(SpinosaurusEntity::new, MobCategory.CREATURE) + .sized(1.875f, 2.375f).build("spinosaurus")); + + public static final RegistrySupplier> TRICERATOPS = + ENTITIES.register("triceratops", () -> EntityType.Builder.of(TriceratopsEntity::new, MobCategory.CREATURE) + .sized(1.875f, 2.375f).build("triceratops")); + + public static final RegistrySupplier> TYRANNOSAURUS_REX = + ENTITIES.register("tyrannosaurus_rex", () -> EntityType.Builder.of(TyrannosaurusRexEntity::new, MobCategory.CREATURE) + .sized(1.875f, 2.375f).build("tyrannosaurus_rex")); + + public static final RegistrySupplier> VELOCIRAPTOR = + ENTITIES.register("velociraptor", () -> EntityType.Builder.of(VelociraptorEntity::new, MobCategory.CREATURE) + .sized(1.875f, 2.375f).build("velociraptor")); + + + public static final RegistrySupplier> BARYONYX = + ENTITIES.register("baryonyx", () -> EntityType.Builder.of(BaryonyxEntity::new, MobCategory.CREATURE) + .sized(1.875f, 2.375f).build("baryonyx")); + + public static final RegistrySupplier> CARNOTAURUS = + ENTITIES.register("carnotaurus", () -> EntityType.Builder.of(CarnotaurusEntity::new, MobCategory.CREATURE) + .sized(1.875f, 2.375f).build("carnotaurus")); + + public static final RegistrySupplier> CONCAVENATOR = + ENTITIES.register("concavenator", () -> EntityType.Builder.of(ConcavenatorEntity::new, MobCategory.CREATURE) + .sized(1.875f, 2.375f).build("concavenator")); + + public static final RegistrySupplier> DEINONYCHUS = + ENTITIES.register("deinonychus", () -> EntityType.Builder.of(DeinonychusEntity::new, MobCategory.CREATURE) + .sized(1.875f, 2.375f).build("deinonychus")); + + public static final RegistrySupplier> EDMONTOSAURUS = + ENTITIES.register("edmontosaurus", () -> EntityType.Builder.of(EdmontosaurusEntity::new, MobCategory.CREATURE) + .sized(1.875f, 2.375f).build("edmontosaurus")); + + public static final RegistrySupplier> GIGANOTOSAURUS = + ENTITIES.register("giganotosaurus", () -> EntityType.Builder.of(GiganotosaurusEntity::new, MobCategory.CREATURE) + .sized(1.875f, 2.375f).build("giganotosaurus")); + + public static final RegistrySupplier> GUANLONG = + ENTITIES.register("guanlong", () -> EntityType.Builder.of(GuanlongEntity::new, MobCategory.CREATURE) + .sized(1.875f, 2.375f).build("guanlong")); + + public static final RegistrySupplier> HERRERASAURUS = + ENTITIES.register("herrerasaurus", () -> EntityType.Builder.of(HerrerasaurusEntity::new, MobCategory.CREATURE) + .sized(1.875f, 2.375f).build("herrerasaurus")); + + public static final RegistrySupplier> MAJUNGASAURUS = + ENTITIES.register("majungasaurus", () -> EntityType.Builder.of(MajungasaurusEntity::new, MobCategory.CREATURE) + .sized(1.875f, 2.375f).build("majungasaurus")); + + public static final RegistrySupplier> PROCOMPSOGNATHUS = + ENTITIES.register("procompsognathus", () -> EntityType.Builder.of(ProcompsognathusEntity::new, MobCategory.CREATURE) + .sized(1.875f, 2.375f).build("procompsognathus")); + + public static final RegistrySupplier> PROTOCERATOPS = + ENTITIES.register("protoceratops", () -> EntityType.Builder.of(ProtoceratopsEntity::new, MobCategory.CREATURE) + .sized(1.875f, 2.375f).build("protoceratops")); + + public static final RegistrySupplier> ARAMBOURGIANIA = + ENTITIES.register("arambourgiania", () -> EntityType.Builder.of(ArambourgianiaEntity::new, MobCategory.CREATURE) + .sized(1.875f, 2.375f).build("arambourgiania")); + + public static final RegistrySupplier> CEARADACTYLUS = + ENTITIES.register("cearadactylus", () -> EntityType.Builder.of(CearadactylusEntity::new, MobCategory.CREATURE) + .sized(1.875f, 2.375f).build("cearadactylus")); + + public static final RegistrySupplier> DIMORPHODON = + ENTITIES.register("dimorphodon", () -> EntityType.Builder.of(DimorphodonEntity::new, MobCategory.CREATURE) + .sized(1.875f, 2.375f).build("dimorphodon")); + + public static final RegistrySupplier> GEOSTERNBERGIA = + ENTITIES.register("geosternbergia", () -> EntityType.Builder.of(GeosternbergiaEntity::new, MobCategory.CREATURE) + .sized(1.875f, 2.375f).build("geosternbergia")); + + public static final RegistrySupplier> GUIDRACO = + ENTITIES.register("guidraco", () -> EntityType.Builder.of(GuidracoEntity::new, MobCategory.CREATURE) + .sized(1.875f, 2.375f).build("guidraco")); + + public static final RegistrySupplier> LUDODACTYLUS = + ENTITIES.register("ludodactylus", () -> EntityType.Builder.of(LudodactylusEntity::new, MobCategory.CREATURE) + .sized(1.875f, 2.375f).build("ludodactylus")); + + public static final RegistrySupplier> MOGANOPTERUS = + ENTITIES.register("moganopterus", () -> EntityType.Builder.of(MoganopterusEntity::new, MobCategory.CREATURE) + .sized(1.875f, 2.375f).build("moganopterus")); + + public static final RegistrySupplier> NYCTOSAURUS = + ENTITIES.register("nyctosaurus", () -> EntityType.Builder.of(NyctosaurusEntity::new, MobCategory.CREATURE) + .sized(1.875f, 2.375f).build("nyctosaurus")); + + public static final RegistrySupplier> PTERANODON = + ENTITIES.register("pteranodon", () -> EntityType.Builder.of(PteranodonEntity::new, MobCategory.CREATURE) + .sized(1.875f, 2.375f).build("pteranodon")); + + public static final RegistrySupplier> PTERODAUSTRO = + ENTITIES.register("pterodaustro", () -> EntityType.Builder.of(PterodaustroEntity::new, MobCategory.CREATURE) + .sized(1.875f, 2.375f).build("pterodaustro")); + + public static final RegistrySupplier> QUETZALCOATLUS = + ENTITIES.register("quetzalcoatlus", () -> EntityType.Builder.of(QuetzalcoatlusEntity::new, MobCategory.CREATURE) + .sized(1.875f, 2.375f).build("quetzalcoatlus")); + + public static final RegistrySupplier> TAPEJARA = + ENTITIES.register("tapejara", () -> EntityType.Builder.of(TapejaraEntity::new, MobCategory.CREATURE) + .sized(1.875f, 2.375f).build("tapejara")); + + public static final RegistrySupplier> TROPEOGNATHUS = + ENTITIES.register("tropeognathus", () -> EntityType.Builder.of(TropeognathusEntity::new, MobCategory.CREATURE) + .sized(1.875f, 2.375f).build("tropeognathus")); + + public static final RegistrySupplier> TUPUXUARA = + ENTITIES.register("tupuxuara", () -> EntityType.Builder.of(TupuxuaraEntity::new, MobCategory.CREATURE) + .sized(1.875f, 2.375f).build("tupuxuara")); + + public static final RegistrySupplier> ZHENYUANOPTERUS = + ENTITIES.register("zhenyuanopterus", () -> EntityType.Builder.of(ZhenyuanopterusEntity::new, MobCategory.CREATURE) + .sized(1.875f, 2.375f).build("zhenyuanopterus")); + + public static final RegistrySupplier> RUGOPS = + ENTITIES.register("rugops", () -> EntityType.Builder.of(RugopsEntity::new, MobCategory.CREATURE) + .sized(1.875f, 2.375f).build("rugops")); + + public static final RegistrySupplier> SHANTUNGOSAURUS = + ENTITIES.register("shantungosaurus", () -> EntityType.Builder.of(ShantungosaurusEntity::new, MobCategory.CREATURE) + .sized(1.875f, 2.375f).build("shantungosaurus")); + + public static final RegistrySupplier> STEGOSAURUS = + ENTITIES.register("stegosaurus", () -> EntityType.Builder.of(StegosaurusEntity::new, MobCategory.CREATURE) + .sized(1.875f, 2.375f).build("stegosaurus")); + + public static final RegistrySupplier> STYRACOSAURUS = + ENTITIES.register("styracosaurus", () -> EntityType.Builder.of(StyracosaurusEntity::new, MobCategory.CREATURE) + .sized(1.875f, 2.375f).build("styracosaurus")); + + public static final RegistrySupplier> THERIZINOSAURUS = + ENTITIES.register("therizinosaurus", () -> EntityType.Builder.of(TherizinosaurusEntity::new, MobCategory.CREATURE) + .sized(1.875f, 2.375f).build("therizinosaurus")); + + public static final RegistrySupplier> DISTORTUS_REX = + ENTITIES.register("distortus_rex", () -> EntityType.Builder.of(DistortusRexEntity::new, MobCategory.CREATURE) + .sized(1.875f, 2.375f).build("distortus_rex")); + + public static final RegistrySupplier> CHICKENOSAURUS = + ENTITIES.register("chickenosaurus", () -> EntityType.Builder.of(ChickenosaurusEntity::new, MobCategory.CREATURE) + .sized(1.875f, 2.375f).build("chickenosaurus")); + + public static final RegistrySupplier> ALLOSAURUS = + ENTITIES.register("allosaurus", () -> EntityType.Builder.of(AllosaurusEntity::new, MobCategory.CREATURE) + .sized(1.875f, 2.375f).build("allosaurus")); + + public static final RegistrySupplier> ALVAREZSAURUS = + ENTITIES.register("alvarezsaurus", () -> EntityType.Builder.of(AlvarezsaurusEntity::new, MobCategory.CREATURE) + .sized(1.875f, 2.375f).build("alvarezsaurus")); + + public static final RegistrySupplier> ANKYLOSAURUS = + ENTITIES.register("ankylosaurus", () -> EntityType.Builder.of(AnkylosaurusEntity::new, MobCategory.CREATURE) + .sized(1.875f, 2.375f).build("ankylosaurus")); + + public static final RegistrySupplier> CARCHARODONTOSAURUS = + ENTITIES.register("carcharodontosaurus", () -> EntityType.Builder.of(CarcharodontosaurusEntity::new, MobCategory.CREATURE) + .sized(1.875f, 2.375f).build("carcharodontosaurus")); + + public static final RegistrySupplier> CHASMOSAURUS = + ENTITIES.register("chasmosaurus", () -> EntityType.Builder.of(ChasmosaurusEntity::new, MobCategory.CREATURE) + .sized(1.875f, 2.375f).build("chasmosaurus")); + + public static final RegistrySupplier> COELOPHYSIS = + ENTITIES.register("coelophysis", () -> EntityType.Builder.of(CoelophysisEntity::new, MobCategory.CREATURE) + .sized(1.875f, 2.375f).build("coelophysis")); + + public static final RegistrySupplier> COELURUS = + ENTITIES.register("coelurus", () -> EntityType.Builder.of(CoelurusEntity::new, MobCategory.CREATURE) + .sized(1.875f, 2.375f).build("coelurus")); + + public static final RegistrySupplier> CORYTHOSAURUS = + ENTITIES.register("corythosaurus", () -> EntityType.Builder.of(CorythosaurusEntity::new, MobCategory.CREATURE) + .sized(1.875f, 2.375f).build("corythosaurus")); + + public static final RegistrySupplier> DRYOSAURUS = + ENTITIES.register("dryosaurus", () -> EntityType.Builder.of(DryosaurusEntity::new, MobCategory.CREATURE) + .sized(1.875f, 2.375f).build("dryosaurus")); + + public static final RegistrySupplier> HADROSAURUS = + ENTITIES.register("hadrosaurus", () -> EntityType.Builder.of(HadrosaurusEntity::new, MobCategory.CREATURE) + .sized(1.875f, 2.375f).build("hadrosaurus")); + + public static final RegistrySupplier> HYPSILOPHODON = + ENTITIES.register("hypsilophodon", () -> EntityType.Builder.of(HypsilophodonEntity::new, MobCategory.CREATURE) + .sized(1.875f, 2.375f).build("hypsilophodon")); + + public static final RegistrySupplier> INDORAPTOR = + ENTITIES.register("indoraptor", () -> EntityType.Builder.of(IndoraptorEntity::new, MobCategory.CREATURE) + .sized(1.875f, 2.375f).build("indoraptor")); + + public static final RegistrySupplier> INOSTRANCEVIA = + ENTITIES.register("inostrancevia", () -> EntityType.Builder.of(InostranceviaEntity::new, MobCategory.CREATURE) + .sized(1.875f, 2.375f).build("inostrancevia")); + + public static final RegistrySupplier> LAMBEOSAURUS = + ENTITIES.register("lambeosaurus", () -> EntityType.Builder.of(LambeosaurusEntity::new, MobCategory.CREATURE) + .sized(1.875f, 2.375f).build("lambeosaurus")); + + public static final RegistrySupplier> MAMENCHISAURUS = + ENTITIES.register("mamenchisaurus", () -> EntityType.Builder.of(MamenchisaurusEntity::new, MobCategory.CREATURE) + .sized(1.875f, 2.375f).build("mamenchisaurus")); + + public static final RegistrySupplier> METRIACANTHOSAURUS = + ENTITIES.register("metriacanthosaurus", () -> EntityType.Builder.of(MetriacanthosaurusEntity::new, MobCategory.CREATURE) + .sized(1.875f, 2.375f).build("metriacanthosaurus")); + + public static final RegistrySupplier> ORNITHOLESTES = + ENTITIES.register("ornitholestes", () -> EntityType.Builder.of(OrnitholestesEntity::new, MobCategory.CREATURE) + .sized(1.875f, 2.375f).build("ornitholestes")); + + public static final RegistrySupplier> ORNITHOMIMUS = + ENTITIES.register("ornithomimus", () -> EntityType.Builder.of(OrnithomimusEntity::new, MobCategory.CREATURE) + .sized(1.875f, 2.375f).build("ornithomimus")); + + public static final RegistrySupplier> OVIRAPTOR = + ENTITIES.register("oviraptor", () -> EntityType.Builder.of(OviraptorEntity::new, MobCategory.CREATURE) + .sized(1.875f, 2.375f).build("oviraptor")); + + public static final RegistrySupplier> PACHYCEPHALOSAURUS = + ENTITIES.register("pachycephalosaurus", () -> EntityType.Builder.of(PachycephalosaurusEntity::new, MobCategory.CREATURE) + .sized(1.875f, 2.375f).build("pachycephalosaurus")); + + public static final RegistrySupplier> PROCERATOSAURUS = + ENTITIES.register("proceratosaurus", () -> EntityType.Builder.of(ProceratosaurusEntity::new, MobCategory.CREATURE) + .sized(1.875f, 2.375f).build("proceratosaurus")); + + public static final RegistrySupplier> RAJASAURUS = + ENTITIES.register("rajasaurus", () -> EntityType.Builder.of(RajasaurusEntity::new, MobCategory.CREATURE) + .sized(1.875f, 2.375f).build("rajasaurus")); + + public static final RegistrySupplier> SEGISAURUS = + ENTITIES.register("segisaurus", () -> EntityType.Builder.of(SegisaurusEntity::new, MobCategory.CREATURE) + .sized(1.875f, 2.375f).build("segisaurus")); + + public static final RegistrySupplier> TITANOSAURUS = + ENTITIES.register("titanosaurus", () -> EntityType.Builder.of(TitanosaurusEntity::new, MobCategory.CREATURE) + .sized(1.875f, 2.375f).build("titanosaurus")); + + public static final RegistrySupplier> TROODON = + ENTITIES.register("troodon", () -> EntityType.Builder.of(TroodonEntity::new, MobCategory.CREATURE) + .sized(1.875f, 2.375f).build("troodon")); + + public static final RegistrySupplier> UTAHRAPTOR = + ENTITIES.register("utahraptor", () -> EntityType.Builder.of(UtahraptorEntity::new, MobCategory.CREATURE) + .sized(1.875f, 2.375f).build("utahraptor")); + + public static void registerAttributes() { + EntityAttributeRegistry.register(APATOSAURUS, ApatosaurusEntity::createAttributes); + EntityAttributeRegistry.register(ALBERTOSAURUS, AlbertosaurusEntity::createAttributes); + EntityAttributeRegistry.register(BARYONYX, BaryonyxEntity::createAttributes); + EntityAttributeRegistry.register(BRACHIOSAURUS, BrachiosaurusEntity::createAttributes); + EntityAttributeRegistry.register(CARNOTAURUS, CarnotaurusEntity::createAttributes); + EntityAttributeRegistry.register(CERATOSAURUS, CeratosaurusEntity::createAttributes); + EntityAttributeRegistry.register(COMPSOGNATHUS, CompsognathusEntity::createAttributes); + EntityAttributeRegistry.register(CONCAVENATOR, ConcavenatorEntity::createAttributes); + EntityAttributeRegistry.register(DEINONYCHUS, DeinonychusEntity::createAttributes); + EntityAttributeRegistry.register(DILOPHOSAURUS, DilophosaurusEntity::createAttributes); + EntityAttributeRegistry.register(DIPLODOCUS, DiplodocusEntity::createAttributes); + EntityAttributeRegistry.register(DISTORTUS_REX, DistortusRexEntity::createAttributes); + EntityAttributeRegistry.register(EDMONTOSAURUS, EdmontosaurusEntity::createAttributes); + EntityAttributeRegistry.register(FDUCK, FDuckEntity::createAttributes); + EntityAttributeRegistry.register(GALLIMIMUS, GallimimusEntity::createAttributes); + EntityAttributeRegistry.register(GIGANOTOSAURUS, GiganotosaurusEntity::createAttributes); + EntityAttributeRegistry.register(GUANLONG, GuanlongEntity::createAttributes); + EntityAttributeRegistry.register(HERRERASAURUS, HerrerasaurusEntity::createAttributes); + EntityAttributeRegistry.register(INDOMINUS_REX, IndominusRexEntity::createAttributes); + EntityAttributeRegistry.register(MAJUNGASAURUS, MajungasaurusEntity::createAttributes); + EntityAttributeRegistry.register(OURANOSAURUS, OuranosaurusEntity::createAttributes); + EntityAttributeRegistry.register(PARASAUROLOPHUS, ParasaurolophusEntity::createAttributes); + EntityAttributeRegistry.register(PROCOMPSOGNATHUS, ProcompsognathusEntity::createAttributes); + EntityAttributeRegistry.register(PROTOCERATOPS, ProtoceratopsEntity::createAttributes); + EntityAttributeRegistry.register(ARAMBOURGIANIA, ArambourgianiaEntity::createAttributes); + EntityAttributeRegistry.register(CEARADACTYLUS, CearadactylusEntity::createAttributes); + EntityAttributeRegistry.register(DIMORPHODON, DimorphodonEntity::createAttributes); + EntityAttributeRegistry.register(GEOSTERNBERGIA, GeosternbergiaEntity::createAttributes); + EntityAttributeRegistry.register(GUIDRACO, GuidracoEntity::createAttributes); + EntityAttributeRegistry.register(LUDODACTYLUS, LudodactylusEntity::createAttributes); + EntityAttributeRegistry.register(MOGANOPTERUS, MoganopterusEntity::createAttributes); + EntityAttributeRegistry.register(NYCTOSAURUS, NyctosaurusEntity::createAttributes); + EntityAttributeRegistry.register(PTERANODON, PteranodonEntity::createAttributes); + EntityAttributeRegistry.register(PTERODAUSTRO, PterodaustroEntity::createAttributes); + EntityAttributeRegistry.register(QUETZALCOATLUS, QuetzalcoatlusEntity::createAttributes); + EntityAttributeRegistry.register(TAPEJARA, TapejaraEntity::createAttributes); + EntityAttributeRegistry.register(TROPEOGNATHUS, TropeognathusEntity::createAttributes); + EntityAttributeRegistry.register(TUPUXUARA, TupuxuaraEntity::createAttributes); + EntityAttributeRegistry.register(ZHENYUANOPTERUS, ZhenyuanopterusEntity::createAttributes); + EntityAttributeRegistry.register(RUGOPS, RugopsEntity::createAttributes); + EntityAttributeRegistry.register(SHANTUNGOSAURUS, ShantungosaurusEntity::createAttributes); + EntityAttributeRegistry.register(SPINOSAURUS, SpinosaurusEntity::createAttributes); + EntityAttributeRegistry.register(STEGOSAURUS, StegosaurusEntity::createAttributes); + EntityAttributeRegistry.register(STYRACOSAURUS, StyracosaurusEntity::createAttributes); + EntityAttributeRegistry.register(THERIZINOSAURUS, TherizinosaurusEntity::createAttributes); + EntityAttributeRegistry.register(TRICERATOPS, TriceratopsEntity::createAttributes); + EntityAttributeRegistry.register(TYRANNOSAURUS_REX, TyrannosaurusRexEntity::createAttributes); + EntityAttributeRegistry.register(VELOCIRAPTOR, VelociraptorEntity::createAttributes); + EntityAttributeRegistry.register(CHICKENOSAURUS, ChickenosaurusEntity::createAttributes); + EntityAttributeRegistry.register(ALLOSAURUS, AllosaurusEntity::createAttributes); + EntityAttributeRegistry.register(ALVAREZSAURUS, AlvarezsaurusEntity::createAttributes); + EntityAttributeRegistry.register(ANKYLOSAURUS, AnkylosaurusEntity::createAttributes); + EntityAttributeRegistry.register(CARCHARODONTOSAURUS, CarcharodontosaurusEntity::createAttributes); + EntityAttributeRegistry.register(CHASMOSAURUS, ChasmosaurusEntity::createAttributes); + EntityAttributeRegistry.register(COELOPHYSIS, CoelophysisEntity::createAttributes); + EntityAttributeRegistry.register(COELURUS, CoelurusEntity::createAttributes); + EntityAttributeRegistry.register(CORYTHOSAURUS, CorythosaurusEntity::createAttributes); + EntityAttributeRegistry.register(DRYOSAURUS, DryosaurusEntity::createAttributes); + EntityAttributeRegistry.register(HADROSAURUS, HadrosaurusEntity::createAttributes); + EntityAttributeRegistry.register(HYPSILOPHODON, HypsilophodonEntity::createAttributes); + EntityAttributeRegistry.register(INDORAPTOR, IndoraptorEntity::createAttributes); + EntityAttributeRegistry.register(INOSTRANCEVIA, InostranceviaEntity::createAttributes); + EntityAttributeRegistry.register(LAMBEOSAURUS, LambeosaurusEntity::createAttributes); + EntityAttributeRegistry.register(MAMENCHISAURUS, MamenchisaurusEntity::createAttributes); + EntityAttributeRegistry.register(METRIACANTHOSAURUS, MetriacanthosaurusEntity::createAttributes); + EntityAttributeRegistry.register(ORNITHOLESTES, OrnitholestesEntity::createAttributes); + EntityAttributeRegistry.register(ORNITHOMIMUS, OrnithomimusEntity::createAttributes); + EntityAttributeRegistry.register(OVIRAPTOR, OviraptorEntity::createAttributes); + EntityAttributeRegistry.register(PACHYCEPHALOSAURUS, PachycephalosaurusEntity::createAttributes); + EntityAttributeRegistry.register(PROCERATOSAURUS, ProceratosaurusEntity::createAttributes); + EntityAttributeRegistry.register(RAJASAURUS, RajasaurusEntity::createAttributes); + EntityAttributeRegistry.register(SEGISAURUS, SegisaurusEntity::createAttributes); + EntityAttributeRegistry.register(TITANOSAURUS, TitanosaurusEntity::createAttributes); + EntityAttributeRegistry.register(TROODON, TroodonEntity::createAttributes); + EntityAttributeRegistry.register(UTAHRAPTOR, UtahraptorEntity::createAttributes); + } + + public static void registerSpawnPlacements() { + if (!JRConfigManager.get().enableDinosaurs) { +/*? if >1.20.1 {*/ + /*SpawnPlacementsRegistry.register(ALBERTOSAURUS, SpawnPlacementTypes.ON_GROUND, + Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, Animal::checkAnimalSpawnRules); +*//*?} else {*/ + SpawnPlacementsRegistry.register(ALBERTOSAURUS, SpawnPlacements.Type.ON_GROUND, + Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, Animal::checkAnimalSpawnRules); +/*?}*/ + // ... repeat for all your dinos ... + } + } public static void register() { ENTITIES.register(); diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/ai/DinoAIController.java b/common/src/main/java/net/cmr/jurassicrevived/entity/ai/DinoAIController.java new file mode 100644 index 0000000..54bb18e --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/ai/DinoAIController.java @@ -0,0 +1,776 @@ +package net.cmr.jurassicrevived.entity.ai; + +import net.minecraft.core.BlockPos; +import net.minecraft.tags.FluidTags; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.effect.MobEffectInstance; +import net.minecraft.world.effect.MobEffects; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.util.DefaultRandomPos; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.animal.FlyingAnimal; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.level.pathfinder.Node; +import net.minecraft.world.level.pathfinder.Path; +import net.minecraft.world.phys.Vec3; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +public class DinoAIController { + + private final DinoEntityBase dino; + private State currentState = State.IDLE; + + private LivingEntity attackTarget; + private Animal mateTarget; // Target for mating + private BlockPos waterTarget; + private BlockPos homePos; // Center of territory + private Vec3 roamTarget; + + private int stateTimer = 0; + private int pathRecalcTimer = 0; + private int failedPathfindingAttempts = 0; + private boolean isSelfBreeding = false; // For parthenogenesis + + // Attack Cooldown Tracker + private int attackCooldown = 0; + + public State getCurrentState() { return currentState; } + public LivingEntity getAttackTarget() { return attackTarget; } + public BlockPos getWaterTarget() { return waterTarget; } + + public enum State { + IDLE, + ROAMING, + TERRITORIAL_ROAMING, + CHASING, + ATTACKING, + FLEEING, + SLEEPING, + MATING, // New state + DEFACATING, DROWNING, FLOCKING, HIBERNATING, HIDING, HUNTING, NESTING, RAMPAGING, SOCIALIZING + } + + public DinoAIController(DinoEntityBase dino) { + this.dino = dino; + } + + public void tick() { + if (homePos == null) homePos = dino.blockPosition(); + + updateSensors(); + + switch (currentState) { + case IDLE -> tickIdle(); + case ROAMING -> tickRoaming(); + case TERRITORIAL_ROAMING -> tickTerritorialRoaming(); + case CHASING -> tickChasing(); + case ATTACKING -> tickAttacking(); + case FLEEING -> tickFleeing(); + case SLEEPING -> tickSleeping(); + case MATING -> tickMating(); + } + + stateTimer++; + if (attackCooldown > 0) attackCooldown--; + } + + public void onHurtBy(LivingEntity attacker) { + // Retaliate if we are capable of attacking (have damage attribute > 0) + // Carnivores always attack back. Herbivores/others attack back if they have strength. + // We SKIP the generic canAttack check here because if something hurt us, + // we should try to fight back even if it's "too big" or technically invalid by roaming standards. + boolean canFightBack = dino.getAttributeValue(Attributes.ATTACK_DAMAGE) > 0 && !dino.isBaby(); + + if (canFightBack) { + this.attackTarget = attacker; + transitionTo(State.CHASING); + } else { + this.attackTarget = attacker; + transitionTo(State.FLEEING); + } + } + + private void transitionTo(State newState) { + // Handle Condition updates + if (dino.dinoData != null) { + if (newState == State.SLEEPING) dino.dinoData.addCondition(IDinoData.Condition.SLEEPING); + else dino.dinoData.removeCondition(IDinoData.Condition.SLEEPING); + } + + this.currentState = newState; + this.stateTimer = 0; + this.pathRecalcTimer = 0; + this.failedPathfindingAttempts = 0; + + // Reset sprinting if we aren't in a high-speed state + if (newState != State.CHASING && newState != State.ATTACKING && newState != State.FLEEING) { + dino.setSprinting(false); + } + + // Do NOT stop navigation here if switching Chasing <-> Attacking to maintain momentum + if (newState == State.IDLE || newState == State.ROAMING || newState == State.TERRITORIAL_ROAMING || newState == State.SLEEPING || newState == State.MATING) { + this.dino.getNavigation().stop(); + } + } + + // --- SENSORS --- + + private void updateSensors() { + DinoEntityBase.DinoAIConfig config = dino.getAIConfig(); + + // 1. Check for Mating (High priority) + // If we are in love but not currently fighting or already mating, switch to mating. + if (dino.isInLove() && currentState != State.CHASING && currentState != State.ATTACKING && currentState != State.FLEEING && currentState != State.MATING) { + transitionTo(State.MATING); + } + + // 2. Vitals Update + if (dino.dinoData != null) { + float hungerDecay = config.hungerDecay(); + float thirstDecay = config.thirstDecay(); + + if (currentState == State.SLEEPING) { + hungerDecay *= 0.5f; + thirstDecay *= 0.5f; + if (dino.tickCount % 40 == 0 && dino.getHealth() < dino.getMaxHealth()) { + dino.heal(1.0f); + } + } + + dino.dinoData.modifyHunger(-hungerDecay); + float currentThirst = dino.dinoData.getThirst(); + dino.dinoData.setThirst(Math.max(0, currentThirst - thirstDecay)); + + float hunger = dino.dinoData.getHunger(); + float thirst = dino.dinoData.getThirst(); + + if (hunger <= 0 || thirst <= 0) { + if (currentState == State.SLEEPING) { + transitionTo(State.IDLE); + } + + if (stateTimer % 20 == 0) { + dino.addEffect(new MobEffectInstance(MobEffects.MOVEMENT_SLOWDOWN, 40, 1)); + if (hunger <= 0) { + dino.hurt(dino.damageSources().starve(), 1.0f); + dino.dinoData.addCondition(IDinoData.Condition.STARVING); + } + if (thirst <= 0) { + dino.hurt(dino.damageSources().dryOut(), 1.0f); + dino.dinoData.addCondition(IDinoData.Condition.DEHYDRATED); + } + } + } else { + dino.dinoData.removeCondition(IDinoData.Condition.STARVING); + dino.dinoData.removeCondition(IDinoData.Condition.DEHYDRATED); + } + + if (hunger < 30) dino.dinoData.addCondition(IDinoData.Condition.HUNGRY); + else dino.dinoData.removeCondition(IDinoData.Condition.HUNGRY); + + if (thirst < 30) dino.dinoData.addCondition(IDinoData.Condition.THIRSTY); + else dino.dinoData.removeCondition(IDinoData.Condition.THIRSTY); + + if (dino.getHealth() < dino.getMaxHealth() * 0.25) dino.dinoData.addCondition(IDinoData.Condition.LOW_HEALTH); + else dino.dinoData.removeCondition(IDinoData.Condition.LOW_HEALTH); + } + + // 3. Sleep check + if (currentState == State.IDLE || currentState == State.ROAMING || currentState == State.TERRITORIAL_ROAMING || currentState == State.SLEEPING) { + if (dino.dinoData != null) { + IDinoData.ActivityPattern activity = dino.dinoData.getActivityPattern(); + boolean isDay = dino.level().isDay(); + + boolean shouldSleep = false; + if (activity == IDinoData.ActivityPattern.DIURNAL && !isDay) shouldSleep = true; + if (activity == IDinoData.ActivityPattern.NOCTURNAL && isDay) shouldSleep = true; + + if (dino.getLastHurtByMob() != null && dino.tickCount - dino.getLastHurtByMobTimestamp() < 200) { + shouldSleep = false; + } + + if (shouldSleep && currentState != State.SLEEPING) { + transitionTo(State.SLEEPING); + } else if (!shouldSleep && currentState == State.SLEEPING) { + transitionTo(State.IDLE); + } + } + } + + // 4. Target Validation (Attack) + if (attackTarget != null) { + boolean shouldStop = false; + + // Basic checks + if (!attackTarget.isAlive() || dino.distanceToSqr(attackTarget) > 64 * 64) { + shouldStop = true; + } + + // Player specific checks (Creative/Spectator/Peaceful) + if (attackTarget instanceof Player player) { + if (player.isCreative() || player.isSpectator()) shouldStop = true; + } + + // Note: We deliberately do NOT call dino.canAttack(attackTarget) here. + // canAttack() includes "preferences" (like size limits or whitelist) which should + // be ignored if we are actively retaliating or hunting a valid target we already selected. + + if (shouldStop) { + attackTarget = null; + if (currentState == State.CHASING || currentState == State.ATTACKING) { + transitionTo(State.IDLE); + } + } + } + + // 5. Hunt check + if ((currentState == State.IDLE || currentState == State.ROAMING || currentState == State.TERRITORIAL_ROAMING) && dino.isCarnivore()) { + boolean hungry = dino.dinoData != null && dino.dinoData.getHunger() < 70; + boolean territorial = dino.dinoData != null && dino.dinoData.getAggression() == IDinoData.Aggression.TERRITORIAL; + + if (hungry || (territorial && dino.dinoData.getHunger() < 90)) { + if (stateTimer % 10 == 0) { + findTarget(); + } + } + } + + // 6. Water check + if ((currentState == State.IDLE || currentState == State.ROAMING || currentState == State.TERRITORIAL_ROAMING)) { + if (dino.dinoData != null && dino.dinoData.getThirst() < 50 && waterTarget == null) { + if (stateTimer % 10 == 0) findWater(); + } + } + } + + private void findWater() { + // Increase vertical search range to find water even if we are high up + BlockPos pos = BlockPos.findClosestMatch(dino.blockPosition(), 32, 16, p -> dino.level().getFluidState(p).is(FluidTags.WATER)).orElse(null); + if (pos != null) { + this.waterTarget = pos; + dino.getNavigation().moveTo(pos.getX(), pos.getY(), pos.getZ(), dino.getAIConfig().walkSpeed()); + if (currentState == State.IDLE) { + float territoriality = dino.dinoData != null ? dino.dinoData.getTerritoriality() : 0.0f; + if (dino.getRandom().nextFloat() < territoriality) { + transitionTo(State.TERRITORIAL_ROAMING); + } else { + transitionTo(State.ROAMING); + } + } + } + } + + private void findTarget() { + double range = dino.getAttributeValue(Attributes.FOLLOW_RANGE); + if (range <= 0) range = 32.0; + + List nearby = dino.level().getEntitiesOfClass(LivingEntity.class, + dino.getBoundingBox().inflate(range)); + + List candidates = new ArrayList<>(); + + for (LivingEntity e : nearby) { + if (e == dino) continue; + if (!dino.canAttack(e)) continue; + + if (e instanceof Player player) { + // Apply motivation logic for Players + boolean isTerritorial = dino.dinoData != null && dino.dinoData.getAggression() == IDinoData.Aggression.TERRITORIAL; + boolean hungry = dino.dinoData != null && dino.dinoData.getHunger() < 70; + + boolean validPlayerTarget = false; + if (isTerritorial) { + if (hungry || dino.distanceToSqr(player) < 225) { + validPlayerTarget = true; + } + } else { + if (hungry) { + validPlayerTarget = true; + } + } + + if (validPlayerTarget) { + candidates.add(player); + } + } else { + // Animals are valid if canAttack passes (which checks size etc) + candidates.add(e); + } + } + + // Sort candidates by distance to prefer closest valid target + candidates.sort(Comparator.comparingDouble(dino::distanceToSqr)); + + // Check pathfinding for the closest targets to ensure we can reach them + int checks = 0; + for (LivingEntity candidate : candidates) { + if (checks >= 5) break; // Limit pathfinding checks to avoid lag + checks++; + + Path path = dino.getNavigation().createPath(candidate, 0); + if (path != null) { + // VERIFY PATH REACHES TARGET + // The pathfinder may return a partial path that ends at a wall. + // We check if the end point of the path is reasonably close to the target. + Node endNode = path.getEndNode(); + if (endNode != null) { + double distToTargetSqr = candidate.distanceToSqr(endNode.x + 0.5, endNode.y + 0.5, endNode.z + 0.5); + // 25.0 = 5 blocks tolerance. If the path ends > 5 blocks away from target, it's likely obstructed. + if (distToTargetSqr < 25.0) { + this.attackTarget = candidate; + transitionTo(State.CHASING); + return; + } + } + } + } + } + + // --- STATE LOGIC --- + + private void tickIdle() { + dino.getNavigation().stop(); + + // Check for Natural Breeding (approx once every 2 in-game days = 48000 ticks) + if (!dino.level().isClientSide && stateTimer % 100 == 0) { + // 1. Trigger Ready State + if (dino.getAge() == 0 && !dino.isInLove() && dino.canBreed()) { + if (dino.dinoData != null && !dino.dinoData.hasCondition(IDinoData.Condition.READY_TO_MATE)) { + // Chance: 1 in 480 checks (~ once per 48000 ticks / 2 days) + if (dino.getRandom().nextInt(480) == 0) { + // Parthenogenesis check (1% chance) + if (dino.getRandom().nextInt(100) == 0) { + dino.setInLoveTime(600); // 30 Seconds of hearts + this.isSelfBreeding = true; + } else { + // Standard: Set condition, wait for partner + dino.dinoData.addCondition(IDinoData.Condition.READY_TO_MATE); + } + } + } + } + + // 2. Scan for Partner if Ready + if (dino.dinoData != null && dino.dinoData.hasCondition(IDinoData.Condition.READY_TO_MATE)) { + // Add a chance to "lose interest" so they aren't ready forever (1 in 50 chance every 5 seconds = approx 4 minutes duration) + if (dino.getRandom().nextInt(50) == 0) { + dino.dinoData.removeCondition(IDinoData.Condition.READY_TO_MATE); + } else { + List nearby = dino.level().getEntitiesOfClass(DinoEntityBase.class, + dino.getBoundingBox().inflate(8.0), + e -> e.getType() == dino.getType() && e != dino && !e.isBaby()); + + for (DinoEntityBase potentialPartner : nearby) { + if (dino.canMate(potentialPartner)) { + // Initiate mating for both + dino.setInLoveTime(600); // 30 seconds + potentialPartner.setInLoveTime(600); // 30 seconds + + dino.dinoData.removeCondition(IDinoData.Condition.READY_TO_MATE); + if (potentialPartner.dinoData != null) { + potentialPartner.dinoData.removeCondition(IDinoData.Condition.READY_TO_MATE); + } + break; + } + } + } + } + } + + float territoriality = 0.0f; + if (dino.dinoData != null) { + territoriality = dino.dinoData.getTerritoriality(); + } + + int idleTime = 60; // Default 3 seconds + // If we are a flying animal and on the ground, stay down longer (e.g. 15-30 seconds) to walk around + if (dino instanceof FlyingAnimal && dino.onGround()) { + idleTime = 300 + dino.getRandom().nextInt(300); + } + + if (stateTimer > idleTime) { + if (dino.getRandom().nextFloat() < 0.05f) { + if (dino.getRandom().nextFloat() < territoriality) { + transitionTo(State.TERRITORIAL_ROAMING); + } else { + transitionTo(State.ROAMING); + } + } + } + } + + private void tickMating() { + // If love ran out, stop + if (!dino.isInLove()) { + this.mateTarget = null; + this.isSelfBreeding = false; + // Also ensure we don't have the condition anymore if we just failed/finished + if (dino.dinoData != null) dino.dinoData.removeCondition(IDinoData.Condition.READY_TO_MATE); + transitionTo(State.IDLE); + return; + } + + // Parthenogenesis Logic + if (this.isSelfBreeding) { + dino.spawnChildFromBreeding((net.minecraft.server.level.ServerLevel)dino.level(), dino); + dino.setInLoveTime(0); // Reset + this.isSelfBreeding = false; + transitionTo(State.IDLE); + return; + } + + // Find Partner + if (this.mateTarget == null || !this.mateTarget.isAlive() || !this.mateTarget.isInLove()) { + List nearby = dino.level().getEntitiesOfClass(Animal.class, dino.getBoundingBox().inflate(16.0), + e -> e.getType() == dino.getType() && e != dino && e.isInLove()); + + this.mateTarget = nearby.stream() + .min(Comparator.comparingDouble(dino::distanceToSqr)) + .orElse(null); + } + + if (this.mateTarget != null) { + dino.getNavigation().moveTo(this.mateTarget, dino.getAIConfig().walkSpeed()); + if (dino.distanceToSqr(this.mateTarget) < 4.0) { // < 2 blocks + dino.spawnChildFromBreeding((net.minecraft.server.level.ServerLevel)dino.level(), this.mateTarget); + // Breeding consumes love in spawnChildFromBreeding + this.mateTarget = null; + transitionTo(State.IDLE); + } + } else { + // No partner found yet, wander slowly? + if (dino.getNavigation().isDone()) { + Vec3 pos = DefaultRandomPos.getPos(dino, 10, 3); + if (pos != null) dino.getNavigation().moveTo(pos.x, pos.y, pos.z, dino.getAIConfig().walkSpeed()); + } + } + } + + private void tickSleeping() { + dino.getNavigation().stop(); + } + + private boolean handleWaterPathing() { + if (waterTarget != null) { + double dist = dino.distanceToSqr(waterTarget.getCenter()); + double reach = dino.getAIConfig().attackReach() * dino.getBbWidth(); + if (dist < (reach * reach) + 9.0) { + dino.dinoData.setThirst(dino.getAIConfig().maxThirst()); + waterTarget = null; + dino.getNavigation().stop(); + transitionTo(State.IDLE); + return true; + } + + if (dino.getNavigation().isDone()) { + if (dist < 1024) { + dino.getNavigation().moveTo(waterTarget.getX(), waterTarget.getY(), waterTarget.getZ(), dino.getAIConfig().walkSpeed()); + } else { + waterTarget = null; + } + } + return true; + } + return false; + } + + private void tickRoaming() { + if (handleWaterPathing()) return; + + if (stateTimer == 0) { + this.roamTarget = null; + findAndSetRoamTarget(); + if (this.roamTarget == null) { + transitionTo(State.IDLE); + return; + } + } + + if (stateTimer > 400) { + transitionTo(State.IDLE); + return; + } + + if (dino.getNavigation().isDone()) { + if (roamTarget != null && dino.distanceToSqr(roamTarget) < 9.0) { + // If we are a flyer and in the air, don't stop! Keep flying! + if (dino instanceof FlyingAnimal && !dino.onGround()) { + findAndSetRoamTarget(); // Immediately pick next waypoint + return; + } + + transitionTo(State.IDLE); + return; + } + + boolean resumed = false; + if (roamTarget != null) { + resumed = dino.getNavigation().moveTo(roamTarget.x, roamTarget.y, roamTarget.z, dino.getAIConfig().walkSpeed()); + } + + if (!resumed) { + findAndSetRoamTarget(); + if (this.roamTarget == null) { + transitionTo(State.IDLE); + } + } + } + } + + private void findAndSetRoamTarget() { + this.roamTarget = null; + + // Flying Logic + if (dino instanceof FlyingAnimal) { + Vec3 airPos = getAirRoamPos(); + if (airPos != null) { + // Use walkSpeed as cruising speed + if (dino.getNavigation().moveTo(airPos.x, airPos.y, airPos.z, dino.getAIConfig().walkSpeed())) { + this.roamTarget = airPos; + return; + } + } + } + + // Ground Logic + for (int i = 0; i < 3; i++) { + Vec3 pos = DefaultRandomPos.getPos(dino, 20, 7); + if (pos != null && dino.distanceToSqr(pos) > 49.0) { + if (dino.getNavigation().moveTo(pos.x, pos.y, pos.z, dino.getAIConfig().walkSpeed())) { + this.roamTarget = pos; + return; + } + } + } + } + + private Vec3 getAirRoamPos() { + net.minecraft.util.RandomSource random = dino.getRandom(); + Vec3 pos = dino.position(); + + // Increased radius for better flight + double x = pos.x + (random.nextFloat() * 2 - 1) * 64; + double z = pos.z + (random.nextFloat() * 2 - 1) * 64; + + // Height check + int groundY = dino.level().getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, (int)x, (int)z); + double y; + + if (dino.onGround()) { + // Takeoff: 10-20 blocks up + y = pos.y + 10 + random.nextInt(10); + } else { + // 20% chance to land if already flying + if (random.nextFloat() < 0.20f) { + y = groundY; + } else { + // Wander vertically + if (random.nextFloat() < 0.30f) { + y = groundY + 10 + random.nextInt(30); + } else { + y = pos.y + (random.nextFloat() * 2 - 1) * 20; + } + + if (y < groundY + 5) y = groundY + 5; + if (y > groundY + 50) y = groundY + 50; + } + } + + // --- Water Avoidance --- + // If the target is over water, try to pull it back towards us/land + BlockPos targetPos = new BlockPos((int)x, (int)groundY, (int)z); + if (dino.level().getFluidState(targetPos).is(FluidTags.WATER)) { + // Check if it's "deep" water (just checking surface isn't enough, but usually good proxy) + // Move target 50% closer to current position (which presumably was safe) + x = (x + pos.x) / 2.0; + z = (z + pos.z) / 2.0; + // Re-calculate ground Y for new X/Z + groundY = dino.level().getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, (int)x, (int)z); + if (y < groundY) y = groundY; + } + + // --- Landing Logic --- + // If we are aiming for a spot very close to the ground (<= 4 blocks), just land. + // This prevents awkward low-altitude hovering. + if (y <= groundY + 4) { + y = groundY; + } + + return new Vec3(x, y, z); + } + + private void tickTerritorialRoaming() { + if (handleWaterPathing()) return; + + if (stateTimer == 0) { + findAndSetTerritorialTarget(); + if (this.roamTarget == null) { + transitionTo(State.IDLE); + return; + } + } + + if (stateTimer > 400) { + transitionTo(State.IDLE); + return; + } + + if (dino.getNavigation().isDone()) { + if (roamTarget != null && dino.distanceToSqr(roamTarget) < 9.0) { + transitionTo(State.IDLE); + return; + } + + boolean resumed = false; + if (roamTarget != null) { + resumed = dino.getNavigation().moveTo(roamTarget.x, roamTarget.y, roamTarget.z, dino.getAIConfig().walkSpeed()); + } + + if (!resumed) { + findAndSetTerritorialTarget(); + if (this.roamTarget == null) { + transitionTo(State.IDLE); + } + } + } + } + + private void findAndSetTerritorialTarget() { + this.roamTarget = null; + Vec3 target = null; + + for (int i = 0; i < 5; i++) { + Vec3 candidate; + if (homePos != null && dino.distanceToSqr(homePos.getCenter()) > 40 * 40) { + Vec3 toHome = Vec3.atCenterOf(homePos).subtract(dino.position()).normalize().scale(10); + Vec3 biasTarget = dino.position().add(toHome); + candidate = DefaultRandomPos.getPosTowards(dino, 15, 7, biasTarget, 1.57); + } else { + candidate = DefaultRandomPos.getPos(dino, 15, 7); + } + + if (candidate != null && dino.distanceToSqr(candidate) > 25.0) { + if (dino.getNavigation().moveTo(candidate.x, candidate.y, candidate.z, dino.getAIConfig().walkSpeed())) { + this.roamTarget = candidate; + return; + } + } + } + + Vec3 fallback = DefaultRandomPos.getPos(dino, 10, 5); + if (fallback != null) { + if (dino.getNavigation().moveTo(fallback.x, fallback.y, fallback.z, dino.getAIConfig().walkSpeed())) { + this.roamTarget = fallback; + } + } + } + + private void tickChasing() { + if (attackTarget == null) { + transitionTo(State.IDLE); + return; + } + dino.setSprinting(true); + + waterTarget = null; + + dino.getLookControl().setLookAt(attackTarget, 30.0F, 30.0F); + + double distSqr = dino.distanceToSqr(attackTarget); + double reachMult = dino.getAIConfig().attackReach(); + double reach = (double)(dino.getBbWidth() * reachMult * dino.getBbWidth() * reachMult) + attackTarget.getBbWidth(); + + if (distSqr <= reach * 1.1) { + transitionTo(State.ATTACKING); + return; + } + + if (pathRecalcTimer-- <= 0 || dino.getNavigation().isDone()) { + if (!dino.getNavigation().moveTo(attackTarget, dino.getAIConfig().runSpeed())) { + pathRecalcTimer = 10; // Wait before retrying to prevent rapid failure loops + failedPathfindingAttempts++; + + // Tolerance allows for temporary pathfinding failures (e.g., target inside hitbox) + if (failedPathfindingAttempts > 5) { + attackTarget = null; + transitionTo(State.IDLE); + } + } else { + pathRecalcTimer = 10; + failedPathfindingAttempts = 0; + } + } + } + + private void tickAttacking() { + if (attackTarget == null) { + transitionTo(State.IDLE); + return; + } + dino.setSprinting(true); + + dino.getLookControl().setLookAt(attackTarget, 30.0F, 30.0F); + + double distSqr = dino.distanceToSqr(attackTarget); + double reachMult = dino.getAIConfig().attackReach(); + double reach = (double)(dino.getBbWidth() * reachMult * dino.getBbWidth() * reachMult) + attackTarget.getBbWidth(); + + if (distSqr > reach * 2.5) { + transitionTo(State.CHASING); + return; + } + + double stopDist = (dino.getBbWidth()/2.0 + attackTarget.getBbWidth()/2.0) + 0.5; + double stopDistSqr = stopDist * stopDist; + + if (distSqr > stopDistSqr) { + dino.getNavigation().moveTo(attackTarget, dino.getAIConfig().runSpeed()); + } else { + dino.getNavigation().stop(); + } + + if (attackCooldown <= 0) { + dino.swing(InteractionHand.MAIN_HAND); + + boolean success = false; + if (dino.isWithinMeleeAttackRange(attackTarget)) { + success = dino.doHurtTarget(attackTarget); + } + + if (!success && attackTarget.isAlive()) { + if (distSqr <= reach) { + success = attackTarget.hurt(dino.damageSources().mobAttack(dino), (float)dino.getAttributeValue(Attributes.ATTACK_DAMAGE)); + } + } + + if (success) { + attackCooldown = 20; + } else { + attackCooldown = 5; + } + } + } + + private void tickFleeing() { + if (attackTarget == null) { + transitionTo(State.IDLE); + return; + } + + if (dino.getNavigation().isDone() || stateTimer % 10 == 0) { + Vec3 awayDir = DefaultRandomPos.getPosAway(dino, 16, 7, attackTarget.position()); + if (awayDir != null) { + dino.getNavigation().moveTo(awayDir.x, awayDir.y, awayDir.z, dino.getAIConfig().runSpeed() * 1.2); + } + } + + if (dino.distanceToSqr(attackTarget) > 48 * 48) { + transitionTo(State.IDLE); + } + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/ai/DinoEntityBase.java b/common/src/main/java/net/cmr/jurassicrevived/entity/ai/DinoEntityBase.java new file mode 100644 index 0000000..8657eb6 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/ai/DinoEntityBase.java @@ -0,0 +1,232 @@ +package net.cmr.jurassicrevived.entity.ai; + +import net.cmr.jurassicrevived.entity.ai.navigation.CustomDinoNavigation; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.tags.ItemTags; +import net.minecraft.world.Difficulty; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.effect.MobEffects; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.ai.navigation.PathNavigation; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; + +import java.util.HashMap; +import java.util.Map; + +public abstract class DinoEntityBase extends Animal { + + protected IDinoData dinoData; + protected final DinoAIController aiController; + + public DinoEntityBase(EntityType type, Level level) { + super(type, level); + this.aiController = new DinoAIController(this); + } + + // Disable Vanilla Goals + @Override + protected void registerGoals() {} + + // Ensure our AI ticks regardless of what vanilla method is called + @Override + public void tick() { + super.tick(); // Vanilla Mob tick + + if (!this.level().isClientSide) { + // FORCE TICK AI + this.aiController.tick(); + + // Sync + if (dinoData != null) { + dinoData.tickSync(this.entityData); + } + } + } + + // --- Navigation --- + @Override + protected PathNavigation createNavigation(Level level) { + return new CustomDinoNavigation(this, level); + } + + // --- Hooks --- + public abstract boolean isCarnivore(); + public abstract boolean isMarine(); + public abstract boolean isAmphibious(); + public abstract Block getEggBlock(); // New hook for breeding + public abstract DinoAIConfig getAIConfig(); + + public record DinoAIConfig(double walkSpeed, double runSpeed, double attackReach, + float maxHunger, float maxThirst, + float hungerDecay, float thirstDecay, + float defaultHungerReplenishment) {} + + private static final Map, Float> ENTITY_HUNGER_VALUES = new HashMap<>(); + + public static void registerHungerValue(EntityType type, float value) { + ENTITY_HUNGER_VALUES.put(type, value); + } + + public float getHungerReplenishment(LivingEntity target) { + return ENTITY_HUNGER_VALUES.getOrDefault(target.getType(), getAIConfig().defaultHungerReplenishment()); + } + + public IDinoData getDinoData() { return this.dinoData; } + + @Override + public InteractionResult mobInteract(Player player, InteractionHand hand) { + if (!this.level().isClientSide && hand == InteractionHand.MAIN_HAND && player.isShiftKeyDown() && player.getMainHandItem().isEmpty()) { + if (this.dinoData != null) { + // Format details nicely + StringBuilder sb = new StringBuilder(); + sb.append("§6--- Dino Status ---§r\n"); + sb.append("§eState:§r ").append(this.aiController.getCurrentState()).append("\n"); + + // Line 1: Biological Classification + sb.append("§eType:§r ").append(dinoData.getType()) + .append(" §eGroup:§r ").append(dinoData.getGroup()) + .append(" §eDiet:§r ").append(dinoData.getDiet()).append("\n"); + + // Line 2: Behavioral/Lifecycle + sb.append("§eBehavior:§r ").append(dinoData.getAggression()) + .append(" §eBirth:§r ").append(dinoData.getBirthType()) + .append(" §eActivity:§r ").append(dinoData.getActivityPattern()).append("\n"); + + // Line 3: Status + sb.append("§eMood:§r ").append(dinoData.getMood()) + .append(" §eConditions:§r ").append(dinoData.getConditions()).append("\n"); + + // Line 4: Vitals + sb.append("§aHunger:§r ").append(String.format("%.1f", dinoData.getHunger())) + .append(" §bThirst:§r ").append(String.format("%.1f", dinoData.getThirst())).append("\n"); + + if (this.aiController.getAttackTarget() != null) { + sb.append("§cTarget:§r ").append(this.aiController.getAttackTarget().getName().getString()).append("\n"); + } + if (this.aiController.getWaterTarget() != null) { + sb.append("§9Water Target:§r ").append(this.aiController.getWaterTarget().toShortString()).append("\n"); + } + + player.sendSystemMessage(Component.literal(sb.toString())); + return InteractionResult.SUCCESS; + } + } + return super.mobInteract(player, hand); + } + + @Override + public boolean isFood(ItemStack stack) { + if (dinoData == null) return false; + IDinoData.DietaryClassification diet = dinoData.getDiet(); + + // 1. Carnivores: All meat + if (diet == IDinoData.DietaryClassification.CARNIVORE || diet == IDinoData.DietaryClassification.PISCIVORE) { + // WOLF_FOOD includes beef, pork, chicken, rabbit, mutton, rotten flesh + //if (stack.is(ItemTags.WOLF_FOOD)) return true; + // Fallback for fish items + if (stack.is(ItemTags.FISHES)) return true; + } + + // 3. Herbivores: Leaves, Fruits, Vegetables + if (diet == IDinoData.DietaryClassification.HERBIVORE) { + if (stack.is(ItemTags.LEAVES)) return true; + if (stack.is(ItemTags.FLOWERS)) return true; + if (stack.is(Items.APPLE) || stack.is(Items.MELON_SLICE) || stack.is(Items.SWEET_BERRIES) || stack.is(Items.GLOW_BERRIES)) return true; + if (stack.is(Items.SEAGRASS) || stack.is(Items.KELP)) return true; + } + + // 4. Omnivores: Both + if (diet == IDinoData.DietaryClassification.OMNIVORE) { + //if (stack.is(ItemTags.WOLF_FOOD)) return true; + if (stack.is(ItemTags.LEAVES)) return true; + } + + return false; + } + + @Override + public void spawnChildFromBreeding(ServerLevel level, Animal partner) { + // Custom breeding logic + if (this.dinoData != null && this.dinoData.getBirthType() == IDinoData.BirthType.EGG_LAYING) { + // Place Egg Block + Block eggBlock = getEggBlock(); + if (eggBlock != null) { + // Consume breeding status + this.setAge(6000); + partner.setAge(6000); + this.resetLove(); + partner.resetLove(); + + // Place egg at parent location + level.setBlock(this.blockPosition(), eggBlock.defaultBlockState(), 3); + level.levelEvent(2001, this.blockPosition(), Block.getId(eggBlock.defaultBlockState())); // Particles? + } + } else { + // Live Birth (Default Vanilla) + super.spawnChildFromBreeding(level, partner); + } + } + + // --- Attack Logic --- + public boolean canAttack(LivingEntity target) { + if (target == null || target == this) return false; + if (!target.isAlive()) return false; + if (this.getClass().equals(target.getClass())) return false; + + // Prevent targeting if the entity is invisible (unless we have special senses, but basic AI implies sight) + if (target.hasEffect(MobEffects.INVISIBILITY)) return false; + + if (target instanceof Player player) { + if (player.isCreative() || player.isSpectator()) return false; + + // Peaceful mode check: Do not target players if difficulty is Peaceful + if (this.level().getDifficulty() == Difficulty.PEACEFUL) return false; + + if (dinoData != null && dinoData.isWhitelisted(player.getUUID())) return false; + return true; // Always attack valid players + } + + double myVolume = this.getBbWidth() * this.getBbWidth() * this.getBbHeight(); + double targetVolume = target.getBbWidth() * target.getBbWidth() * target.getBbHeight(); + + // Prevent attacking entities significantly larger than self + // Check Volume (2.5x) + if (myVolume > 0 && targetVolume > myVolume * 2.5) return false; + + // Check Height (1.2x) - Prevents attacking tall things like Brachiosaurus even if volume calc is close + if (this.getBbHeight() > 0 && target.getBbHeight() > this.getBbHeight() * 1.2) return false; + + return true; + } + + @Override + public boolean hurt(DamageSource source, float amount) { + boolean result = super.hurt(source, amount); + if (result && !this.level().isClientSide && source.getEntity() instanceof LivingEntity attacker) { + this.aiController.onHurtBy(attacker); + } + return result; + } + + @Override + public void addAdditionalSaveData(CompoundTag tag) { + super.addAdditionalSaveData(tag); + if(dinoData != null) dinoData.saveNBT(tag); + } + + @Override + public void readAdditionalSaveData(CompoundTag tag) { + super.readAdditionalSaveData(tag); + if(dinoData != null) dinoData.loadNBT(tag); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/ai/IDinoData.java b/common/src/main/java/net/cmr/jurassicrevived/entity/ai/IDinoData.java new file mode 100644 index 0000000..55a61df --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/ai/IDinoData.java @@ -0,0 +1,74 @@ +package net.cmr.jurassicrevived.entity.ai; + +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.syncher.SynchedEntityData; + +import java.util.List; +import java.util.Set; +import java.util.UUID; + +public interface IDinoData { + // Core Vitals + float getHunger(); + void setHunger(float value); + void modifyHunger(float change); + + float getThirst(); + void setThirst(float value); + + // Mood & Personality + enum Mood { ANGRY, NEUTRAL, CONTENT, HAPPY, SCARED, SLEEPY } + Mood getMood(); + void setMood(Mood mood); + + // Aggression / Territoriality + // Replaces old float getTerritoriality? Or keeps it? + // "I want to add... Aggression, and a Condition." and "And this list for Aggression: Free-Roaming, Territorial, and Neutral" + // We will keep getTerritoriality() as a float for logic if needed, but add the Enum. + enum Aggression { FREE_ROAMING, TERRITORIAL, NEUTRAL, SCAVENGER } + Aggression getAggression(); + void setAggression(Aggression aggression); + + float getTerritoriality(); + void setTerritoriality(float value); + + // New Stats + enum DietaryClassification { CRUSTACIVORE, DETRITIVORE, HERBIVORE, INSECTIVORE, OMNIVORE, PISCIVORE, PLANKTIVORE, CARNIVORE } + DietaryClassification getDiet(); + void setDiet(DietaryClassification diet); + + enum Type { TERRESTRIAL, AVIAN, AMPHIBIOUS, MARINE } + Type getType(); + void setType(Type type); + + enum Group { THEROPOD, THYREOPHORAN, CERAPOD, SAUROPOD, ORNITHOPOD, AMPHIBIAN, ARCHOSAUROMORPH, PLEURODIRE, PTEROSAUR, REPTILIOMORPH, SQUAMATE } + Group getGroup(); + void setGroup(Group group); + + enum BirthType { EGG_LAYING, LIVE_BIRTH } + BirthType getBirthType(); + void setBirthType(BirthType birthType); + + enum ActivityPattern { CATHEMERAL, CREPUSCULAR, DIURNAL, NOCTURNAL } + ActivityPattern getActivityPattern(); + void setActivityPattern(ActivityPattern pattern); + + // Conditions + enum Condition { COMATOSE, INFECTED, LOW_HEALTH, HUNGRY, OVERHEATING, POISONED, SEDATED, SUFFOCATING, TAMED, WITHER, READY_TO_MATE, THIRSTY, STARVING, DEHYDRATED, FREEZING, SLEEPING } + Set getConditions(); + void addCondition(Condition condition); + void removeCondition(Condition condition); + boolean hasCondition(Condition condition); + + // Taming / Whitelist + void addWhitelistedPlayer(UUID playerUUID); + boolean isWhitelisted(UUID playerUUID); + List getWhitelistedPlayers(); + + // Serialization & Sync + void saveNBT(CompoundTag tag); + void loadNBT(CompoundTag tag); + + // Called on entity tick to sync specific data (Hunger/Mood) to client via SynchedEntityData if needed + void tickSync(SynchedEntityData entityData); +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/ai/SprintingMeleeAttackGoal.java b/common/src/main/java/net/cmr/jurassicrevived/entity/ai/SprintingMeleeAttackGoal.java new file mode 100644 index 0000000..dcac457 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/ai/SprintingMeleeAttackGoal.java @@ -0,0 +1,61 @@ +package net.cmr.jurassicrevived.entity.ai; + +import net.minecraft.world.Difficulty; +import net.minecraft.world.entity.AgeableMob; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.PathfinderMob; +import net.minecraft.world.entity.ai.goal.MeleeAttackGoal; +import net.minecraft.world.entity.player.Player; + +public class SprintingMeleeAttackGoal extends MeleeAttackGoal { + private final LivingEntity entity; + + public SprintingMeleeAttackGoal(PathfinderMob pMob, double pSpeedModifier, boolean pCanUnseenMemory) { + super(pMob, pSpeedModifier, pCanUnseenMemory); + this.entity = pMob; + } + + // Override to prevent attacking players in peaceful difficulty and prevent babies from attacking + @Override + public boolean canUse() { + if (!super.canUse()) { + return false; + } + + // Prevent baby mobs from attacking + if (this.mob instanceof AgeableMob ageableMob && ageableMob.isBaby()) { + return false; + } + + // Check if target is a player and difficulty is peaceful + LivingEntity target = this.mob.getTarget(); + if (target instanceof Player && this.mob.level().getDifficulty() == Difficulty.PEACEFUL) { + return false; + } + + return true; + } + + // This method is called to start the goal + @Override + public void start() { + super.start(); + this.entity.setSprinting(true); // Force the entity to sprint + } + + // This method is called to tick the goal + @Override + public void tick() { + super.tick(); + // The movement speed is handled by the default goal behavior + // The sprint state is maintained from the start() method + } + + // This method is called to end the goal + @Override + public void stop() { + super.stop(); + this.entity.setSprinting(false); // Stop sprinting when the goal is finished + } +} + diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/ai/SprintingPanicGoal.java b/common/src/main/java/net/cmr/jurassicrevived/entity/ai/SprintingPanicGoal.java new file mode 100644 index 0000000..52728a7 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/ai/SprintingPanicGoal.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.ai; + +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.PathfinderMob; +import net.minecraft.world.entity.ai.goal.PanicGoal; + +public class SprintingPanicGoal extends PanicGoal { + private final LivingEntity entity; + + public SprintingPanicGoal(PathfinderMob mob, double speedModifier) { + super(mob, speedModifier); + this.entity = mob; + } + + @Override + public void start() { + super.start(); + this.entity.setSprinting(true); + } + + @Override + public void stop() { + super.stop(); + this.entity.setSprinting(false); + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/ai/navigation/CustomDinoNavigation.java b/common/src/main/java/net/cmr/jurassicrevived/entity/ai/navigation/CustomDinoNavigation.java new file mode 100644 index 0000000..ef267c5 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/ai/navigation/CustomDinoNavigation.java @@ -0,0 +1,27 @@ +package net.cmr.jurassicrevived.entity.ai.navigation; + +import net.minecraft.world.entity.Mob; +import net.minecraft.world.entity.ai.navigation.GroundPathNavigation; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.pathfinder.PathFinder; +import net.minecraft.world.phys.Vec3; + +public class CustomDinoNavigation extends GroundPathNavigation { + + public CustomDinoNavigation(Mob mob, Level level) { + super(mob, level); + } + + @Override + protected PathFinder createPathFinder(int maxVisitedNodes) { + this.nodeEvaluator = new LargeEntityNodeEvaluator(); + this.nodeEvaluator.setCanPassDoors(true); + return new PathFinder(this.nodeEvaluator, maxVisitedNodes); + } + + // Fix for large entities getting stuck: Ensure we don't try to path to the exact center of a block if our hitbox is huge + @Override + protected Vec3 getTempMobPos() { + return this.mob.position(); + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/ai/navigation/LargeEntityNodeEvaluator.java b/common/src/main/java/net/cmr/jurassicrevived/entity/ai/navigation/LargeEntityNodeEvaluator.java new file mode 100644 index 0000000..ff53f27 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/ai/navigation/LargeEntityNodeEvaluator.java @@ -0,0 +1,46 @@ +package net.cmr.jurassicrevived.entity.ai.navigation; + +import net.minecraft.core.BlockPos; +import net.minecraft.tags.BlockTags; +import net.minecraft.world.entity.Mob; +import net.minecraft.world.level.block.state.BlockState; +/*? if <=1.20.1 {*/ +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.pathfinder.BlockPathTypes; +/*?} else {*/ +/*import net.minecraft.world.level.pathfinder.PathType; +import net.minecraft.world.level.pathfinder.PathfindingContext; +*//*?}*/ +import net.minecraft.world.level.pathfinder.WalkNodeEvaluator; + +public class LargeEntityNodeEvaluator extends WalkNodeEvaluator { + /*? if <=1.20.1 {*/ + public BlockPathTypes getBlockPathType(BlockGetter level, int x, int y, int z) { + Mob entity = this.mob; + BlockPos pos = new BlockPos(x, y, z); + BlockState state = level.getBlockState(pos); + + if (entity != null && entity.getBbWidth() > 1.5f && state.is(BlockTags.LEAVES)) { + return BlockPathTypes.OPEN; + } + /*?} else {*/ + /*@Override + public PathType getPathType(PathfindingContext context, int x, int y, int z) { + Mob entity = this.mob; + BlockPos pos = new BlockPos(x, y, z); + BlockState state = context.level().getBlockState(pos); + + if (entity != null && entity.getBbWidth() > 1.5f && state.is(BlockTags.LEAVES)) { + return PathType.OPEN; + } + *//*?}*/ + + if (entity != null && entity.getBbWidth() >= 2.0f) { + } + /*? if <=1.20.1 {*/ + return super.getBlockPathType(level, x, y, z); + /*?} else {*/ + /*return super.getPathType(context, x, y, z); + *//*?}*/ + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/AlbertosaurusModel.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/AlbertosaurusModel.java new file mode 100644 index 0000000..371d902 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/AlbertosaurusModel.java @@ -0,0 +1,112 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.google.common.collect.Maps; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.entity.custom.AlbertosaurusEntity; +import net.minecraft.Util; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animation.AnimationState; +/*?} else {*/ +/*import software.bernie.geckolib.animation.AnimationState; + *//*?}*/ +import software.bernie.geckolib.cache.object.GeoBone; +import software.bernie.geckolib.constant.DataTickets; +import software.bernie.geckolib.model.GeoModel; + +import java.util.Map; + +public class AlbertosaurusModel extends GeoModel { + + private static final Map LOCATION_BY_VARIANT = + Util.make(Maps.newEnumMap(AlbertosaurusVariant.class), map -> { + map.put(AlbertosaurusVariant.MALE, Constants.rl("textures/entity/albertosaurus.png")); + map.put(AlbertosaurusVariant.FEMALE, Constants.rl("textures/entity/albertosaurus_female.png")); + }); + + // Model-local "currently applied" offsets; cleared before each entity render + private float[] appliedYaw = null; + private float[] appliedRoll = null; + + @Override + public ResourceLocation getModelResource(AlbertosaurusEntity animatable) { + return Constants.rl("geo/albertosaurus.geo.json"); + } + + @Override + public ResourceLocation getTextureResource(AlbertosaurusEntity animatable) { + return LOCATION_BY_VARIANT.get(animatable.getVariant()); + } + + @Override + public ResourceLocation getAnimationResource(AlbertosaurusEntity animatable) { + return Constants.rl("animations/albertosaurus.animation.json"); + } + + @Override + public void setCustomAnimations(AlbertosaurusEntity entity, long id, AnimationState state) { + super.setCustomAnimations(entity, id, state); + + String[] tailBones = { "TailBASE", "Tail2", "Tail3", "Tail4", "Tail5", "Tail6" }; + int n = tailBones.length; + + if (appliedYaw == null || appliedYaw.length != n) { + appliedYaw = new float[n]; + appliedRoll = new float[n]; + } + + // 1) Clear previous offsets (from the last entity rendered with this model instance) + for (int i = 0; i < n; i++) { + if (appliedYaw[i] == 0.0f && appliedRoll[i] == 0.0f) continue; + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + if (appliedYaw[i] != 0.0f) bone.setRotY(bone.getRotY() - appliedYaw[i]); + if (appliedRoll[i] != 0.0f) bone.setRotZ(bone.getRotZ() - appliedRoll[i]); + appliedYaw[i] = 0.0f; + appliedRoll[i] = 0.0f; + } + + // 2) Interpolated sway for extra smoothness between ticks + float sway = entity.getTailSwayOffset(state.getPartialTick()); // [-1, 1] + + // Tuning + float maxYawDeg = 22.0f; // increased max sweep + float swayGain = 1.35f; // amplifies overall power + float rollFraction = 0.40f; // slightly stronger roll for heft + + float deg2rad = (float)Math.PI / 180f; + + // Direction: positive sway (left turn) -> tail swings right (negative yaw) + // Flip the sign here if the sway feels inverted + float baseYaw = sway * maxYawDeg * deg2rad; + float baseRoll = -baseYaw * rollFraction; + + float[] weights = { 1.00f, 0.78f, 0.58f, 0.42f, 0.30f, 0.22f }; + + for (int i = 0; i < n; i++) { + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + + float w = weights[i]; + float yaw = baseYaw * w; + float roll = baseRoll * w; + + // OVERRIDE animations on Y/Z only: keep the model's predefined X bend intact + // Do NOT reset rotX here, so the upward bend stays + bone.setRotY(yaw); + bone.setRotZ(roll); + + appliedYaw[i] = yaw; + appliedRoll[i] = roll; + } + + GeoBone head = (GeoBone) getAnimationProcessor().getBone("NeckBASE"); + + if (head != null) { + var entityData = state.getData(DataTickets.ENTITY_MODEL_DATA); + float clampedYawDeg = Mth.clamp(entityData.netHeadYaw(), -20.0f, 20.0f); + head.setRotY(clampedYawDeg * Mth.DEG_TO_RAD); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/AlbertosaurusRenderer.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/AlbertosaurusRenderer.java new file mode 100644 index 0000000..9634b31 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/AlbertosaurusRenderer.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.cmr.jurassicrevived.entity.custom.AlbertosaurusEntity; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.util.Mth; +import software.bernie.geckolib.cache.object.BakedGeoModel; +import software.bernie.geckolib.renderer.GeoEntityRenderer; + + +public class AlbertosaurusRenderer extends GeoEntityRenderer { + private final float animalScale = 1.5F; + public AlbertosaurusRenderer(EntityRendererProvider.Context renderManager) { + super(renderManager, new AlbertosaurusModel()); + } + + @Override + public void scaleModelForRender(float widthScale, float heightScale, PoseStack poseStack, AlbertosaurusEntity animatable, BakedGeoModel model, boolean isReRender, float partialTick, int packedLight, int packedOverlay) { + poseStack.scale(animalScale, animalScale, animalScale); + if(animatable.isBaby()) { + float growthProgress = Mth.clamp((24000.0F + animatable.getSyncedAge()) / 24000.0F, 0.0F, 1.0F); + float scale = Mth.lerp(growthProgress, 0.2F, 1.0F); + poseStack.scale(scale, scale, scale); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/AlbertosaurusVariant.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/AlbertosaurusVariant.java new file mode 100644 index 0000000..facfeef --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/AlbertosaurusVariant.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import java.util.Arrays; +import java.util.Comparator; + +public enum AlbertosaurusVariant { + MALE(0), + FEMALE(1); + + private static final AlbertosaurusVariant[] BY_ID = Arrays.stream(values()).sorted( + Comparator.comparingInt(AlbertosaurusVariant::getId)).toArray(AlbertosaurusVariant[]::new); + + private final int id; + + AlbertosaurusVariant(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static AlbertosaurusVariant byId(int id) { + return BY_ID[id % BY_ID.length]; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/AllosaurusModel.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/AllosaurusModel.java new file mode 100644 index 0000000..54ab2fb --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/AllosaurusModel.java @@ -0,0 +1,112 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.google.common.collect.Maps; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.entity.custom.AllosaurusEntity; +import net.minecraft.Util; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animation.AnimationState; +/*?} else {*/ +/*import software.bernie.geckolib.animation.AnimationState; + *//*?}*/ +import software.bernie.geckolib.cache.object.GeoBone; +import software.bernie.geckolib.constant.DataTickets; +import software.bernie.geckolib.model.GeoModel; + +import java.util.Map; + +public class AllosaurusModel extends GeoModel { + + private static final Map LOCATION_BY_VARIANT = + Util.make(Maps.newEnumMap(AllosaurusVariant.class), map -> { + map.put(AllosaurusVariant.MALE, Constants.rl("textures/entity/allosaurus.png")); + map.put(AllosaurusVariant.FEMALE, Constants.rl("textures/entity/allosaurus_female.png")); + }); + + // Model-local "currently applied" offsets; cleared before each entity render + private float[] appliedYaw = null; + private float[] appliedRoll = null; + + @Override + public ResourceLocation getModelResource(AllosaurusEntity animatable) { + return Constants.rl("geo/allosaurus.geo.json"); + } + + @Override + public ResourceLocation getTextureResource(AllosaurusEntity animatable) { + return LOCATION_BY_VARIANT.get(animatable.getVariant()); + } + + @Override + public ResourceLocation getAnimationResource(AllosaurusEntity animatable) { + return Constants.rl("animations/allosaurus.animation.json"); + } + + @Override + public void setCustomAnimations(AllosaurusEntity entity, long id, AnimationState state) { + super.setCustomAnimations(entity, id, state); + + String[] tailBones = { "Tail1", "Tail2", "Tail3", "Tail4", "Tail5", "Tail6" }; + int n = tailBones.length; + + if (appliedYaw == null || appliedYaw.length != n) { + appliedYaw = new float[n]; + appliedRoll = new float[n]; + } + + // 1) Clear previous offsets (from the last entity rendered with this model instance) + for (int i = 0; i < n; i++) { + if (appliedYaw[i] == 0.0f && appliedRoll[i] == 0.0f) continue; + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + if (appliedYaw[i] != 0.0f) bone.setRotY(bone.getRotY() - appliedYaw[i]); + if (appliedRoll[i] != 0.0f) bone.setRotZ(bone.getRotZ() - appliedRoll[i]); + appliedYaw[i] = 0.0f; + appliedRoll[i] = 0.0f; + } + + // 2) Interpolated sway for extra smoothness between ticks + float sway = entity.getTailSwayOffset(state.getPartialTick()); // [-1, 1] + + // Tuning + float maxYawDeg = 22.0f; // increased max sweep + float swayGain = 1.35f; // amplifies overall power + float rollFraction = 0.40f; // slightly stronger roll for heft + + float deg2rad = (float)Math.PI / 180f; + + // Direction: positive sway (left turn) -> tail swings right (negative yaw) + // Flip the sign here if the sway feels inverted + float baseYaw = sway * maxYawDeg * deg2rad; + float baseRoll = -baseYaw * rollFraction; + + float[] weights = { 1.00f, 0.78f, 0.58f, 0.42f, 0.30f, 0.22f }; + + for (int i = 0; i < n; i++) { + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + + float w = weights[i]; + float yaw = baseYaw * w; + float roll = baseRoll * w; + + // OVERRIDE animations on Y/Z only: keep the model's predefined X bend intact + // Do NOT reset rotX here, so the upward bend stays + bone.setRotY(yaw); + bone.setRotZ(roll); + + appliedYaw[i] = yaw; + appliedRoll[i] = roll; + } + + GeoBone head = (GeoBone) getAnimationProcessor().getBone("Neck1"); + + if (head != null) { + var entityData = state.getData(DataTickets.ENTITY_MODEL_DATA); + float clampedYawDeg = Mth.clamp(entityData.netHeadYaw(), -20.0f, 20.0f); + head.setRotY(clampedYawDeg * Mth.DEG_TO_RAD); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/AllosaurusRenderer.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/AllosaurusRenderer.java new file mode 100644 index 0000000..eb2106f --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/AllosaurusRenderer.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.cmr.jurassicrevived.entity.custom.AllosaurusEntity; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.util.Mth; +import software.bernie.geckolib.cache.object.BakedGeoModel; +import software.bernie.geckolib.renderer.GeoEntityRenderer; + + +public class AllosaurusRenderer extends GeoEntityRenderer { + private final float animalScale = 1.65F; + public AllosaurusRenderer(EntityRendererProvider.Context renderManager) { + super(renderManager, new AllosaurusModel()); + } + + @Override + public void scaleModelForRender(float widthScale, float heightScale, PoseStack poseStack, AllosaurusEntity animatable, BakedGeoModel model, boolean isReRender, float partialTick, int packedLight, int packedOverlay) { + poseStack.scale(animalScale, animalScale, animalScale); + if(animatable.isBaby()) { + float growthProgress = Mth.clamp((24000.0F + animatable.getSyncedAge()) / 24000.0F, 0.0F, 1.0F); + float scale = Mth.lerp(growthProgress, 0.2F, 1.0F); + poseStack.scale(scale, scale, scale); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/AllosaurusVariant.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/AllosaurusVariant.java new file mode 100644 index 0000000..5e1da03 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/AllosaurusVariant.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import java.util.Arrays; +import java.util.Comparator; + +public enum AllosaurusVariant { + MALE(0), + FEMALE(1); + + private static final AllosaurusVariant[] BY_ID = Arrays.stream(values()).sorted( + Comparator.comparingInt(AllosaurusVariant::getId)).toArray(AllosaurusVariant[]::new); + + private final int id; + + AllosaurusVariant(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static AllosaurusVariant byId(int id) { + return BY_ID[id % BY_ID.length]; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/AlvarezsaurusModel.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/AlvarezsaurusModel.java new file mode 100644 index 0000000..0050be1 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/AlvarezsaurusModel.java @@ -0,0 +1,112 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.google.common.collect.Maps; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.entity.custom.AlvarezsaurusEntity; +import net.minecraft.Util; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animation.AnimationState; +/*?} else {*/ +/*import software.bernie.geckolib.animation.AnimationState; + *//*?}*/ +import software.bernie.geckolib.cache.object.GeoBone; +import software.bernie.geckolib.constant.DataTickets; +import software.bernie.geckolib.model.GeoModel; + +import java.util.Map; + +public class AlvarezsaurusModel extends GeoModel { + + private static final Map LOCATION_BY_VARIANT = + Util.make(Maps.newEnumMap(AlvarezsaurusVariant.class), map -> { + map.put(AlvarezsaurusVariant.MALE, Constants.rl("textures/entity/alvarezsaurus.png")); + map.put(AlvarezsaurusVariant.FEMALE, Constants.rl("textures/entity/alvarezsaurus_female.png")); + }); + + // Model-local "currently applied" offsets; cleared before each entity render + private float[] appliedYaw = null; + private float[] appliedRoll = null; + + @Override + public ResourceLocation getModelResource(AlvarezsaurusEntity animatable) { + return Constants.rl("geo/alvarezsaurus.geo.json"); + } + + @Override + public ResourceLocation getTextureResource(AlvarezsaurusEntity animatable) { + return LOCATION_BY_VARIANT.get(animatable.getVariant()); + } + + @Override + public ResourceLocation getAnimationResource(AlvarezsaurusEntity animatable) { + return Constants.rl("animations/alvarezsaurus.animation.json"); + } + + @Override + public void setCustomAnimations(AlvarezsaurusEntity entity, long id, AnimationState state) { + super.setCustomAnimations(entity, id, state); + + String[] tailBones = { "TailBase", "TailMid1", "TailMid2", "TailTip"}; + int n = tailBones.length; + + if (appliedYaw == null || appliedYaw.length != n) { + appliedYaw = new float[n]; + appliedRoll = new float[n]; + } + + // 1) Clear previous offsets (from the last entity rendered with this model instance) + for (int i = 0; i < n; i++) { + if (appliedYaw[i] == 0.0f && appliedRoll[i] == 0.0f) continue; + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + if (appliedYaw[i] != 0.0f) bone.setRotY(bone.getRotY() - appliedYaw[i]); + if (appliedRoll[i] != 0.0f) bone.setRotZ(bone.getRotZ() - appliedRoll[i]); + appliedYaw[i] = 0.0f; + appliedRoll[i] = 0.0f; + } + + // 2) Interpolated sway for extra smoothness between ticks + float sway = entity.getTailSwayOffset(state.getPartialTick()); // [-1, 1] + + // Tuning + float maxYawDeg = 22.0f; // increased max sweep + float swayGain = 1.35f; // amplifies overall power + float rollFraction = 0.40f; // slightly stronger roll for heft + + float deg2rad = (float)Math.PI / 180f; + + // Direction: positive sway (left turn) -> tail swings right (negative yaw) + // Flip the sign here if the sway feels inverted + float baseYaw = sway * maxYawDeg * deg2rad; + float baseRoll = -baseYaw * rollFraction; + + float[] weights = { 1.00f, 0.78f, 0.58f, 0.42f }; + + for (int i = 0; i < n; i++) { + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + + float w = weights[i]; + float yaw = baseYaw * w; + float roll = baseRoll * w; + + // OVERRIDE animations on Y/Z only: keep the model's predefined X bend intact + // Do NOT reset rotX here, so the upward bend stays + bone.setRotY(yaw); + bone.setRotZ(roll); + + appliedYaw[i] = yaw; + appliedRoll[i] = roll; + } + + GeoBone head = (GeoBone) getAnimationProcessor().getBone("NeckBase"); + + if (head != null) { + var entityData = state.getData(DataTickets.ENTITY_MODEL_DATA); + float clampedYawDeg = Mth.clamp(entityData.netHeadYaw(), -30.0f, 30.0f); + head.setRotY(clampedYawDeg * Mth.DEG_TO_RAD); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/AlvarezsaurusRenderer.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/AlvarezsaurusRenderer.java new file mode 100644 index 0000000..ba129a8 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/AlvarezsaurusRenderer.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.cmr.jurassicrevived.entity.custom.AlvarezsaurusEntity; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.util.Mth; +import software.bernie.geckolib.cache.object.BakedGeoModel; +import software.bernie.geckolib.renderer.GeoEntityRenderer; + + +public class AlvarezsaurusRenderer extends GeoEntityRenderer { + private final float animalScale = 0.5F; + public AlvarezsaurusRenderer(EntityRendererProvider.Context renderManager) { + super(renderManager, new AlvarezsaurusModel()); + } + + @Override + public void scaleModelForRender(float widthScale, float heightScale, PoseStack poseStack, AlvarezsaurusEntity animatable, BakedGeoModel model, boolean isReRender, float partialTick, int packedLight, int packedOverlay) { + poseStack.scale(animalScale, animalScale, animalScale); + if(animatable.isBaby()) { + float growthProgress = Mth.clamp((24000.0F + animatable.getSyncedAge()) / 24000.0F, 0.0F, 1.0F); + float scale = Mth.lerp(growthProgress, 0.2F, 1.0F); + poseStack.scale(scale, scale, scale); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/AlvarezsaurusVariant.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/AlvarezsaurusVariant.java new file mode 100644 index 0000000..66c0fd9 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/AlvarezsaurusVariant.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import java.util.Arrays; +import java.util.Comparator; + +public enum AlvarezsaurusVariant { + MALE(0), + FEMALE(1); + + private static final AlvarezsaurusVariant[] BY_ID = Arrays.stream(values()).sorted( + Comparator.comparingInt(AlvarezsaurusVariant::getId)).toArray(AlvarezsaurusVariant[]::new); + + private final int id; + + AlvarezsaurusVariant(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static AlvarezsaurusVariant byId(int id) { + return BY_ID[id % BY_ID.length]; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/AnkylosaurusModel.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/AnkylosaurusModel.java new file mode 100644 index 0000000..c34302a --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/AnkylosaurusModel.java @@ -0,0 +1,112 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.google.common.collect.Maps; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.entity.custom.AnkylosaurusEntity; +import net.minecraft.Util; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animation.AnimationState; +/*?} else {*/ +/*import software.bernie.geckolib.animation.AnimationState; + *//*?}*/ +import software.bernie.geckolib.cache.object.GeoBone; +import software.bernie.geckolib.constant.DataTickets; +import software.bernie.geckolib.model.GeoModel; + +import java.util.Map; + +public class AnkylosaurusModel extends GeoModel { + + private static final Map LOCATION_BY_VARIANT = + Util.make(Maps.newEnumMap(AnkylosaurusVariant.class), map -> { + map.put(AnkylosaurusVariant.MALE, Constants.rl("textures/entity/ankylosaurus.png")); + map.put(AnkylosaurusVariant.FEMALE, Constants.rl("textures/entity/ankylosaurus_female.png")); + }); + + // Model-local "currently applied" offsets; cleared before each entity render + private float[] appliedYaw = null; + private float[] appliedRoll = null; + + @Override + public ResourceLocation getModelResource(AnkylosaurusEntity animatable) { + return Constants.rl("geo/ankylosaurus.geo.json"); + } + + @Override + public ResourceLocation getTextureResource(AnkylosaurusEntity animatable) { + return LOCATION_BY_VARIANT.get(animatable.getVariant()); + } + + @Override + public ResourceLocation getAnimationResource(AnkylosaurusEntity animatable) { + return Constants.rl("animations/ankylosaurus.animation.json"); + } + + @Override + public void setCustomAnimations(AnkylosaurusEntity entity, long id, AnimationState state) { + super.setCustomAnimations(entity, id, state); + + String[] tailBones = { "tail1", "tail2", "tail3", "tail4", "tailend" }; + int n = tailBones.length; + + if (appliedYaw == null || appliedYaw.length != n) { + appliedYaw = new float[n]; + appliedRoll = new float[n]; + } + + // 1) Clear previous offsets (from the last entity rendered with this model instance) + for (int i = 0; i < n; i++) { + if (appliedYaw[i] == 0.0f && appliedRoll[i] == 0.0f) continue; + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + if (appliedYaw[i] != 0.0f) bone.setRotY(bone.getRotY() - appliedYaw[i]); + if (appliedRoll[i] != 0.0f) bone.setRotZ(bone.getRotZ() - appliedRoll[i]); + appliedYaw[i] = 0.0f; + appliedRoll[i] = 0.0f; + } + + // 2) Interpolated sway for extra smoothness between ticks + float sway = entity.getTailSwayOffset(state.getPartialTick()); // [-1, 1] + + // Tuning + float maxYawDeg = 22.0f; // increased max sweep + float swayGain = 1.35f; // amplifies overall power + float rollFraction = 0.40f; // slightly stronger roll for heft + + float deg2rad = (float)Math.PI / 180f; + + // Direction: positive sway (left turn) -> tail swings right (negative yaw) + // Flip the sign here if the sway feels inverted + float baseYaw = sway * maxYawDeg * deg2rad; + float baseRoll = -baseYaw * rollFraction; + + float[] weights = { 1.00f, 0.78f, 0.58f, 0.42f, 0.30f }; + + for (int i = 0; i < n; i++) { + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + + float w = weights[i]; + float yaw = baseYaw * w; + float roll = baseRoll * w; + + // OVERRIDE animations on Y/Z only: keep the model's predefined X bend intact + // Do NOT reset rotX here, so the upward bend stays + bone.setRotY(yaw); + bone.setRotZ(roll); + + appliedYaw[i] = yaw; + appliedRoll[i] = roll; + } + + GeoBone head = (GeoBone) getAnimationProcessor().getBone("neck1"); + + if (head != null) { + var entityData = state.getData(DataTickets.ENTITY_MODEL_DATA); + float clampedYawDeg = Mth.clamp(entityData.netHeadYaw(), -30.0f, 30.0f); + head.setRotY(clampedYawDeg * Mth.DEG_TO_RAD); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/AnkylosaurusRenderer.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/AnkylosaurusRenderer.java new file mode 100644 index 0000000..bac1d77 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/AnkylosaurusRenderer.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.cmr.jurassicrevived.entity.custom.AnkylosaurusEntity; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.util.Mth; +import software.bernie.geckolib.cache.object.BakedGeoModel; +import software.bernie.geckolib.renderer.GeoEntityRenderer; + + +public class AnkylosaurusRenderer extends GeoEntityRenderer { + private final float animalScale = 2.4F; + public AnkylosaurusRenderer(EntityRendererProvider.Context renderManager) { + super(renderManager, new AnkylosaurusModel()); + } + + @Override + public void scaleModelForRender(float widthScale, float heightScale, PoseStack poseStack, AnkylosaurusEntity animatable, BakedGeoModel model, boolean isReRender, float partialTick, int packedLight, int packedOverlay) { + poseStack.scale(animalScale, animalScale, animalScale); + if(animatable.isBaby()) { + float growthProgress = Mth.clamp((24000.0F + animatable.getSyncedAge()) / 24000.0F, 0.0F, 1.0F); + float scale = Mth.lerp(growthProgress, 0.2F, 1.0F); + poseStack.scale(scale, scale, scale); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/AnkylosaurusVariant.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/AnkylosaurusVariant.java new file mode 100644 index 0000000..8a7d6c3 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/AnkylosaurusVariant.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import java.util.Arrays; +import java.util.Comparator; + +public enum AnkylosaurusVariant { + MALE(0), + FEMALE(1); + + private static final AnkylosaurusVariant[] BY_ID = Arrays.stream(values()).sorted( + Comparator.comparingInt(AnkylosaurusVariant::getId)).toArray(AnkylosaurusVariant[]::new); + + private final int id; + + AnkylosaurusVariant(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static AnkylosaurusVariant byId(int id) { + return BY_ID[id % BY_ID.length]; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/ApatosaurusModel.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/ApatosaurusModel.java new file mode 100644 index 0000000..7abee53 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/ApatosaurusModel.java @@ -0,0 +1,112 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.google.common.collect.Maps; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.entity.custom.ApatosaurusEntity; +import net.minecraft.Util; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animation.AnimationState; +/*?} else {*/ +/*import software.bernie.geckolib.animation.AnimationState; + *//*?}*/ +import software.bernie.geckolib.cache.object.GeoBone; +import software.bernie.geckolib.constant.DataTickets; +import software.bernie.geckolib.model.GeoModel; + +import java.util.Map; + +public class ApatosaurusModel extends GeoModel { + + private static final Map LOCATION_BY_VARIANT = + Util.make(Maps.newEnumMap(ApatosaurusVariant.class), map -> { + map.put(ApatosaurusVariant.MALE, Constants.rl("textures/entity/apatosaurus.png")); + map.put(ApatosaurusVariant.FEMALE, Constants.rl("textures/entity/apatosaurus_female.png")); + }); + + // Model-local "currently applied" offsets; cleared before each entity render + private float[] appliedYaw = null; + private float[] appliedRoll = null; + + @Override + public ResourceLocation getModelResource(ApatosaurusEntity animatable) { + return Constants.rl("geo/apatosaurus.geo.json"); + } + + @Override + public ResourceLocation getTextureResource(ApatosaurusEntity animatable) { + return LOCATION_BY_VARIANT.get(animatable.getVariant()); + } + + @Override + public ResourceLocation getAnimationResource(ApatosaurusEntity animatable) { + return Constants.rl("animations/apatosaurus.animation.json"); + } + + @Override + public void setCustomAnimations(ApatosaurusEntity entity, long id, AnimationState state) { + super.setCustomAnimations(entity, id, state); + + String[] tailBones = { "tail1", "tail2", "tail3", "tail4", "tail5", "tail6" }; + int n = tailBones.length; + + if (appliedYaw == null || appliedYaw.length != n) { + appliedYaw = new float[n]; + appliedRoll = new float[n]; + } + + // 1) Clear previous offsets (from the last entity rendered with this model instance) + for (int i = 0; i < n; i++) { + if (appliedYaw[i] == 0.0f && appliedRoll[i] == 0.0f) continue; + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + if (appliedYaw[i] != 0.0f) bone.setRotY(bone.getRotY() - appliedYaw[i]); + if (appliedRoll[i] != 0.0f) bone.setRotZ(bone.getRotZ() - appliedRoll[i]); + appliedYaw[i] = 0.0f; + appliedRoll[i] = 0.0f; + } + + // 2) Interpolated sway for extra smoothness between ticks + float sway = entity.getTailSwayOffset(state.getPartialTick()); // [-1, 1] + + // Tuning + float maxYawDeg = 22.0f; // increased max sweep + float swayGain = 1.35f; // amplifies overall power + float rollFraction = 0.40f; // slightly stronger roll for heft + + float deg2rad = (float)Math.PI / 180f; + + // Direction: positive sway (left turn) -> tail swings right (negative yaw) + // Flip the sign here if the sway feels inverted + float baseYaw = sway * maxYawDeg * deg2rad; + float baseRoll = -baseYaw * rollFraction; + + float[] weights = { 1.00f, 0.78f, 0.58f, 0.42f, 0.30f, 0.22f }; + + for (int i = 0; i < n; i++) { + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + + float w = weights[i]; + float yaw = baseYaw * w; + float roll = baseRoll * w; + + // OVERRIDE animations on Y/Z only: keep the model's predefined X bend intact + // Do NOT reset rotX here, so the upward bend stays + bone.setRotY(yaw); + bone.setRotZ(roll); + + appliedYaw[i] = yaw; + appliedRoll[i] = roll; + } + + GeoBone head = (GeoBone) getAnimationProcessor().getBone("neck1"); + + if (head != null) { + var entityData = state.getData(DataTickets.ENTITY_MODEL_DATA); + float clampedYawDeg = Mth.clamp(entityData.netHeadYaw(), -10.0f, 10.0f); + head.setRotY(clampedYawDeg * Mth.DEG_TO_RAD); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/ApatosaurusRenderer.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/ApatosaurusRenderer.java new file mode 100644 index 0000000..3ec0fb5 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/ApatosaurusRenderer.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.cmr.jurassicrevived.entity.custom.ApatosaurusEntity; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.util.Mth; +import software.bernie.geckolib.cache.object.BakedGeoModel; +import software.bernie.geckolib.renderer.GeoEntityRenderer; + + +public class ApatosaurusRenderer extends GeoEntityRenderer { + private final float animalScale = 1.75F; + public ApatosaurusRenderer(EntityRendererProvider.Context renderManager) { + super(renderManager, new ApatosaurusModel()); + } + + @Override + public void scaleModelForRender(float widthScale, float heightScale, PoseStack poseStack, ApatosaurusEntity animatable, BakedGeoModel model, boolean isReRender, float partialTick, int packedLight, int packedOverlay) { + poseStack.scale(animalScale, animalScale, animalScale); + if(animatable.isBaby()) { + float growthProgress = Mth.clamp((24000.0F + animatable.getSyncedAge()) / 24000.0F, 0.0F, 1.0F); + float scale = Mth.lerp(growthProgress, 0.2F, 1.0F); + poseStack.scale(scale, scale, scale); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/ApatosaurusVariant.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/ApatosaurusVariant.java new file mode 100644 index 0000000..04b112e --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/ApatosaurusVariant.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import java.util.Arrays; +import java.util.Comparator; + +public enum ApatosaurusVariant { + MALE(0), + FEMALE(1); + + private static final ApatosaurusVariant[] BY_ID = Arrays.stream(values()).sorted( + Comparator.comparingInt(ApatosaurusVariant::getId)).toArray(ApatosaurusVariant[]::new); + + private final int id; + + ApatosaurusVariant(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static ApatosaurusVariant byId(int id) { + return BY_ID[id % BY_ID.length]; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/ArambourgianiaModel.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/ArambourgianiaModel.java new file mode 100644 index 0000000..5b84d1e --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/ArambourgianiaModel.java @@ -0,0 +1,112 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.google.common.collect.Maps; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.entity.custom.ArambourgianiaEntity; +import net.minecraft.Util; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animation.AnimationState; +/*?} else {*/ +/*import software.bernie.geckolib.animation.AnimationState; + *//*?}*/ +import software.bernie.geckolib.cache.object.GeoBone; +import software.bernie.geckolib.constant.DataTickets; +import software.bernie.geckolib.model.GeoModel; + +import java.util.Map; + +public class ArambourgianiaModel extends GeoModel { + + private static final Map LOCATION_BY_VARIANT = + Util.make(Maps.newEnumMap(ArambourgianiaVariant.class), map -> { + map.put(ArambourgianiaVariant.MALE, Constants.rl("textures/entity/arambourgiania.png")); + map.put(ArambourgianiaVariant.FEMALE, Constants.rl("textures/entity/arambourgiania_female.png")); + }); + + // Model-local "currently applied" offsets; cleared before each entity render + private float[] appliedYaw = null; + private float[] appliedRoll = null; + + @Override + public ResourceLocation getModelResource(ArambourgianiaEntity animatable) { + return Constants.rl("geo/arambourgiania.geo.json"); + } + + @Override + public ResourceLocation getTextureResource(ArambourgianiaEntity animatable) { + return LOCATION_BY_VARIANT.get(animatable.getVariant()); + } + + @Override + public ResourceLocation getAnimationResource(ArambourgianiaEntity animatable) { + return Constants.rl("animations/arambourgiania.animation.json"); + } + + @Override + public void setCustomAnimations(ArambourgianiaEntity entity, long id, AnimationState state) { + super.setCustomAnimations(entity, id, state); + + String[] tailBones = { "Tail1", "Tail2", "Tail3" }; + int n = tailBones.length; + + if (appliedYaw == null || appliedYaw.length != n) { + appliedYaw = new float[n]; + appliedRoll = new float[n]; + } + + // 1) Clear previous offsets (from the last entity rendered with this model instance) + for (int i = 0; i < n; i++) { + if (appliedYaw[i] == 0.0f && appliedRoll[i] == 0.0f) continue; + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + if (appliedYaw[i] != 0.0f) bone.setRotY(bone.getRotY() - appliedYaw[i]); + if (appliedRoll[i] != 0.0f) bone.setRotZ(bone.getRotZ() - appliedRoll[i]); + appliedYaw[i] = 0.0f; + appliedRoll[i] = 0.0f; + } + + // 2) Interpolated sway for extra smoothness between ticks + float sway = entity.getTailSwayOffset(state.getPartialTick()); // [-1, 1] + + // Tuning + float maxYawDeg = 22.0f; // increased max sweep + float swayGain = 1.35f; // amplifies overall power + float rollFraction = 0.40f; // slightly stronger roll for heft + + float deg2rad = (float)Math.PI / 180f; + + // Direction: positive sway (left turn) -> tail swings right (negative yaw) + // Flip the sign here if the sway feels inverted + float baseYaw = sway * maxYawDeg * deg2rad; + float baseRoll = -baseYaw * rollFraction; + + float[] weights = { 1.00f, 0.78f, 0.58f }; + + for (int i = 0; i < n; i++) { + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + + float w = weights[i]; + float yaw = baseYaw * w; + float roll = baseRoll * w; + + // OVERRIDE animations on Y/Z only: keep the model's predefined X bend intact + // Do NOT reset rotX here, so the upward bend stays + bone.setRotY(yaw); + bone.setRotZ(roll); + + appliedYaw[i] = yaw; + appliedRoll[i] = roll; + } + + GeoBone head = (GeoBone) getAnimationProcessor().getBone("Head"); + + if (head != null) { + var entityData = state.getData(DataTickets.ENTITY_MODEL_DATA); + float clampedYawDeg = Mth.clamp(entityData.netHeadYaw(), -20.0f, 20.0f); + head.setRotY(clampedYawDeg * Mth.DEG_TO_RAD); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/ArambourgianiaRenderer.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/ArambourgianiaRenderer.java new file mode 100644 index 0000000..bf79792 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/ArambourgianiaRenderer.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.cmr.jurassicrevived.entity.custom.ArambourgianiaEntity; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.util.Mth; +import software.bernie.geckolib.cache.object.BakedGeoModel; +import software.bernie.geckolib.renderer.GeoEntityRenderer; + + +public class ArambourgianiaRenderer extends GeoEntityRenderer { + private final float animalScale = 1.75F; + public ArambourgianiaRenderer(EntityRendererProvider.Context renderManager) { + super(renderManager, new ArambourgianiaModel()); + } + + @Override + public void scaleModelForRender(float widthScale, float heightScale, PoseStack poseStack, ArambourgianiaEntity animatable, BakedGeoModel model, boolean isReRender, float partialTick, int packedLight, int packedOverlay) { + poseStack.scale(animalScale, animalScale, animalScale); + if(animatable.isBaby()) { + float growthProgress = Mth.clamp((24000.0F + animatable.getSyncedAge()) / 24000.0F, 0.0F, 1.0F); + float scale = Mth.lerp(growthProgress, 0.2F, 1.0F); + poseStack.scale(scale, scale, scale); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/ArambourgianiaVariant.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/ArambourgianiaVariant.java new file mode 100644 index 0000000..7ed1f08 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/ArambourgianiaVariant.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import java.util.Arrays; +import java.util.Comparator; + +public enum ArambourgianiaVariant { + MALE(0), + FEMALE(1); + + private static final ArambourgianiaVariant[] BY_ID = Arrays.stream(values()).sorted( + Comparator.comparingInt(ArambourgianiaVariant::getId)).toArray(ArambourgianiaVariant[]::new); + + private final int id; + + ArambourgianiaVariant(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static ArambourgianiaVariant byId(int id) { + return BY_ID[id % BY_ID.length]; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/BaryonyxModel.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/BaryonyxModel.java new file mode 100644 index 0000000..1ef520d --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/BaryonyxModel.java @@ -0,0 +1,112 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.google.common.collect.Maps; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.entity.custom.BaryonyxEntity; +import net.minecraft.Util; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animation.AnimationState; +/*?} else {*/ +/*import software.bernie.geckolib.animation.AnimationState; + *//*?}*/ +import software.bernie.geckolib.cache.object.GeoBone; +import software.bernie.geckolib.constant.DataTickets; +import software.bernie.geckolib.model.GeoModel; + +import java.util.Map; + +public class BaryonyxModel extends GeoModel { + + private static final Map LOCATION_BY_VARIANT = + Util.make(Maps.newEnumMap(BaryonyxVariant.class), map -> { + map.put(BaryonyxVariant.MALE, Constants.rl("textures/entity/baryonyx.png")); + map.put(BaryonyxVariant.FEMALE, Constants.rl("textures/entity/baryonyx_female.png")); + }); + + // Model-local "currently applied" offsets; cleared before each entity render + private float[] appliedYaw = null; + private float[] appliedRoll = null; + + @Override + public ResourceLocation getModelResource(BaryonyxEntity animatable) { + return Constants.rl("geo/baryonyx.geo.json"); + } + + @Override + public ResourceLocation getTextureResource(BaryonyxEntity animatable) { + return LOCATION_BY_VARIANT.get(animatable.getVariant()); + } + + @Override + public ResourceLocation getAnimationResource(BaryonyxEntity animatable) { + return Constants.rl("animations/baryonyx.animation.json"); + } + + @Override + public void setCustomAnimations(BaryonyxEntity entity, long id, AnimationState state) { + super.setCustomAnimations(entity, id, state); + + String[] tailBones = { "Tail1", "Tail2", "Tail3", "Tail4", "Tail5", "Tail6" }; + int n = tailBones.length; + + if (appliedYaw == null || appliedYaw.length != n) { + appliedYaw = new float[n]; + appliedRoll = new float[n]; + } + + // 1) Clear previous offsets (from the last entity rendered with this model instance) + for (int i = 0; i < n; i++) { + if (appliedYaw[i] == 0.0f && appliedRoll[i] == 0.0f) continue; + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + if (appliedYaw[i] != 0.0f) bone.setRotY(bone.getRotY() - appliedYaw[i]); + if (appliedRoll[i] != 0.0f) bone.setRotZ(bone.getRotZ() - appliedRoll[i]); + appliedYaw[i] = 0.0f; + appliedRoll[i] = 0.0f; + } + + // 2) Interpolated sway for extra smoothness between ticks + float sway = entity.getTailSwayOffset(state.getPartialTick()); // [-1, 1] + + // Tuning + float maxYawDeg = 22.0f; // increased max sweep + float swayGain = 1.35f; // amplifies overall power + float rollFraction = 0.40f; // slightly stronger roll for heft + + float deg2rad = (float)Math.PI / 180f; + + // Direction: positive sway (left turn) -> tail swings right (negative yaw) + // Flip the sign here if the sway feels inverted + float baseYaw = sway * maxYawDeg * deg2rad; + float baseRoll = -baseYaw * rollFraction; + + float[] weights = { 1.00f, 0.78f, 0.58f, 0.42f, 0.30f, 0.22f }; + + for (int i = 0; i < n; i++) { + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + + float w = weights[i]; + float yaw = baseYaw * w; + float roll = baseRoll * w; + + // OVERRIDE animations on Y/Z only: keep the model's predefined X bend intact + // Do NOT reset rotX here, so the upward bend stays + bone.setRotY(yaw); + bone.setRotZ(roll); + + appliedYaw[i] = yaw; + appliedRoll[i] = roll; + } + + GeoBone head = (GeoBone) getAnimationProcessor().getBone("Neck_under2"); + + if (head != null) { + var entityData = state.getData(DataTickets.ENTITY_MODEL_DATA); + float clampedYawDeg = Mth.clamp(entityData.netHeadYaw(), -20.0f, 20.0f); + head.setRotY(clampedYawDeg * Mth.DEG_TO_RAD); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/BaryonyxRenderer.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/BaryonyxRenderer.java new file mode 100644 index 0000000..9c0b6c7 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/BaryonyxRenderer.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.cmr.jurassicrevived.entity.custom.BaryonyxEntity; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.util.Mth; +import software.bernie.geckolib.cache.object.BakedGeoModel; +import software.bernie.geckolib.renderer.GeoEntityRenderer; + + +public class BaryonyxRenderer extends GeoEntityRenderer { + private final float animalScale = 1.5F; + public BaryonyxRenderer(EntityRendererProvider.Context renderManager) { + super(renderManager, new BaryonyxModel()); + } + + @Override + public void scaleModelForRender(float widthScale, float heightScale, PoseStack poseStack, BaryonyxEntity animatable, BakedGeoModel model, boolean isReRender, float partialTick, int packedLight, int packedOverlay) { + poseStack.scale(animalScale, animalScale, animalScale); + if(animatable.isBaby()) { + float growthProgress = Mth.clamp((24000.0F + animatable.getSyncedAge()) / 24000.0F, 0.0F, 1.0F); + float scale = Mth.lerp(growthProgress, 0.2F, 1.0F); + poseStack.scale(scale, scale, scale); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/BaryonyxVariant.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/BaryonyxVariant.java new file mode 100644 index 0000000..da7bd1a --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/BaryonyxVariant.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import java.util.Arrays; +import java.util.Comparator; + +public enum BaryonyxVariant { + MALE(0), + FEMALE(1); + + private static final BaryonyxVariant[] BY_ID = Arrays.stream(values()).sorted( + Comparator.comparingInt(BaryonyxVariant::getId)).toArray(BaryonyxVariant[]::new); + + private final int id; + + BaryonyxVariant(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static BaryonyxVariant byId(int id) { + return BY_ID[id % BY_ID.length]; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/BrachiosaurusModel.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/BrachiosaurusModel.java new file mode 100644 index 0000000..f46e5ad --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/BrachiosaurusModel.java @@ -0,0 +1,112 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.google.common.collect.Maps; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.entity.custom.BrachiosaurusEntity; +import net.minecraft.Util; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animation.AnimationState; +/*?} else {*/ +/*import software.bernie.geckolib.animation.AnimationState; + *//*?}*/ +import software.bernie.geckolib.cache.object.GeoBone; +import software.bernie.geckolib.constant.DataTickets; +import software.bernie.geckolib.model.GeoModel; + +import java.util.Map; + +public class BrachiosaurusModel extends GeoModel { + + private static final Map LOCATION_BY_VARIANT = + Util.make(Maps.newEnumMap(BrachiosaurusVariant.class), map -> { + map.put(BrachiosaurusVariant.MALE, Constants.rl("textures/entity/brachiosaurus.png")); + map.put(BrachiosaurusVariant.FEMALE, Constants.rl("textures/entity/brachiosaurus_female.png")); + }); + + // Model-local "currently applied" offsets; cleared before each entity render + private float[] appliedYaw = null; + private float[] appliedRoll = null; + + @Override + public ResourceLocation getModelResource(BrachiosaurusEntity animatable) { + return Constants.rl("geo/brachiosaurus.geo.json"); + } + + @Override + public ResourceLocation getTextureResource(BrachiosaurusEntity animatable) { + return LOCATION_BY_VARIANT.get(animatable.getVariant()); + } + + @Override + public ResourceLocation getAnimationResource(BrachiosaurusEntity animatable) { + return Constants.rl("animations/brachiosaurus.animation.json"); + } + + @Override + public void setCustomAnimations(BrachiosaurusEntity entity, long id, AnimationState state) { + super.setCustomAnimations(entity, id, state); + + String[] tailBones = { "tail1", "tail2", "tail3", "tail4", "tail5", "tail6" }; + int n = tailBones.length; + + if (appliedYaw == null || appliedYaw.length != n) { + appliedYaw = new float[n]; + appliedRoll = new float[n]; + } + + // 1) Clear previous offsets (from the last entity rendered with this model instance) + for (int i = 0; i < n; i++) { + if (appliedYaw[i] == 0.0f && appliedRoll[i] == 0.0f) continue; + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + if (appliedYaw[i] != 0.0f) bone.setRotY(bone.getRotY() - appliedYaw[i]); + if (appliedRoll[i] != 0.0f) bone.setRotZ(bone.getRotZ() - appliedRoll[i]); + appliedYaw[i] = 0.0f; + appliedRoll[i] = 0.0f; + } + + // 2) Interpolated sway for extra smoothness between ticks + float sway = entity.getTailSwayOffset(state.getPartialTick()); // [-1, 1] + + // Tuning + float maxYawDeg = 22.0f; // increased max sweep + float swayGain = 1.35f; // amplifies overall power + float rollFraction = 0.40f; // slightly stronger roll for heft + + float deg2rad = (float)Math.PI / 180f; + + // Direction: positive sway (left turn) -> tail swings right (negative yaw) + // Flip the sign here if the sway feels inverted + float baseYaw = sway * maxYawDeg * deg2rad; + float baseRoll = -baseYaw * rollFraction; + + float[] weights = { 1.00f, 0.78f, 0.58f, 0.42f, 0.30f, 0.22f }; + + for (int i = 0; i < n; i++) { + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + + float w = weights[i]; + float yaw = baseYaw * w; + float roll = baseRoll * w; + + // OVERRIDE animations on Y/Z only: keep the model's predefined X bend intact + // Do NOT reset rotX here, so the upward bend stays + bone.setRotY(yaw); + bone.setRotZ(roll); + + appliedYaw[i] = yaw; + appliedRoll[i] = roll; + } + + GeoBone head = (GeoBone) getAnimationProcessor().getBone("Neck1"); + + if (head != null) { + var entityData = state.getData(DataTickets.ENTITY_MODEL_DATA); + float clampedYawDeg = Mth.clamp(entityData.netHeadYaw(), -10.0f, 10.0f); + head.setRotY(clampedYawDeg * Mth.DEG_TO_RAD); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/BrachiosaurusRenderer.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/BrachiosaurusRenderer.java new file mode 100644 index 0000000..a600ed0 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/BrachiosaurusRenderer.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.cmr.jurassicrevived.entity.custom.BrachiosaurusEntity; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.util.Mth; +import software.bernie.geckolib.cache.object.BakedGeoModel; +import software.bernie.geckolib.renderer.GeoEntityRenderer; + + +public class BrachiosaurusRenderer extends GeoEntityRenderer { + private final float animalScale = 1.6F; + public BrachiosaurusRenderer(EntityRendererProvider.Context renderManager) { + super(renderManager, new BrachiosaurusModel()); + } + + @Override + public void scaleModelForRender(float widthScale, float heightScale, PoseStack poseStack, BrachiosaurusEntity animatable, BakedGeoModel model, boolean isReRender, float partialTick, int packedLight, int packedOverlay) { + poseStack.scale(animalScale, animalScale, animalScale); + if(animatable.isBaby()) { + float growthProgress = Mth.clamp((24000.0F + animatable.getSyncedAge()) / 24000.0F, 0.0F, 1.0F); + float scale = Mth.lerp(growthProgress, 0.2F, 1.0F); + poseStack.scale(scale, scale, scale); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/BrachiosaurusVariant.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/BrachiosaurusVariant.java new file mode 100644 index 0000000..3a027ed --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/BrachiosaurusVariant.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import java.util.Arrays; +import java.util.Comparator; + +public enum BrachiosaurusVariant { + MALE(0), + FEMALE(1); + + private static final BrachiosaurusVariant[] BY_ID = Arrays.stream(values()).sorted( + Comparator.comparingInt(BrachiosaurusVariant::getId)).toArray(BrachiosaurusVariant[]::new); + + private final int id; + + BrachiosaurusVariant(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static BrachiosaurusVariant byId(int id) { + return BY_ID[id % BY_ID.length]; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/CarcharodontosaurusModel.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/CarcharodontosaurusModel.java new file mode 100644 index 0000000..721e294 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/CarcharodontosaurusModel.java @@ -0,0 +1,112 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.google.common.collect.Maps; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.entity.custom.CarcharodontosaurusEntity; +import net.minecraft.Util; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animation.AnimationState; +/*?} else {*/ +/*import software.bernie.geckolib.animation.AnimationState; + *//*?}*/ +import software.bernie.geckolib.cache.object.GeoBone; +import software.bernie.geckolib.constant.DataTickets; +import software.bernie.geckolib.model.GeoModel; + +import java.util.Map; + +public class CarcharodontosaurusModel extends GeoModel { + + private static final Map LOCATION_BY_VARIANT = + Util.make(Maps.newEnumMap(CarcharodontosaurusVariant.class), map -> { + map.put(CarcharodontosaurusVariant.MALE, Constants.rl("textures/entity/carcharodontosaurus.png")); + map.put(CarcharodontosaurusVariant.FEMALE, Constants.rl("textures/entity/carcharodontosaurus_female.png")); + }); + + // Model-local "currently applied" offsets; cleared before each entity render + private float[] appliedYaw = null; + private float[] appliedRoll = null; + + @Override + public ResourceLocation getModelResource(CarcharodontosaurusEntity animatable) { + return Constants.rl("geo/carcharodontosaurus.geo.json"); + } + + @Override + public ResourceLocation getTextureResource(CarcharodontosaurusEntity animatable) { + return LOCATION_BY_VARIANT.get(animatable.getVariant()); + } + + @Override + public ResourceLocation getAnimationResource(CarcharodontosaurusEntity animatable) { + return Constants.rl("animations/carcharodontosaurus.animation.json"); + } + + @Override + public void setCustomAnimations(CarcharodontosaurusEntity entity, long id, AnimationState state) { + super.setCustomAnimations(entity, id, state); + + String[] tailBones = { "Tail1", "Tail2", "Tail3", "Tail4", "Tail5", "Tail6" }; + int n = tailBones.length; + + if (appliedYaw == null || appliedYaw.length != n) { + appliedYaw = new float[n]; + appliedRoll = new float[n]; + } + + // 1) Clear previous offsets (from the last entity rendered with this model instance) + for (int i = 0; i < n; i++) { + if (appliedYaw[i] == 0.0f && appliedRoll[i] == 0.0f) continue; + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + if (appliedYaw[i] != 0.0f) bone.setRotY(bone.getRotY() - appliedYaw[i]); + if (appliedRoll[i] != 0.0f) bone.setRotZ(bone.getRotZ() - appliedRoll[i]); + appliedYaw[i] = 0.0f; + appliedRoll[i] = 0.0f; + } + + // 2) Interpolated sway for extra smoothness between ticks + float sway = entity.getTailSwayOffset(state.getPartialTick()); // [-1, 1] + + // Tuning + float maxYawDeg = 22.0f; // increased max sweep + float swayGain = 1.35f; // amplifies overall power + float rollFraction = 0.40f; // slightly stronger roll for heft + + float deg2rad = (float)Math.PI / 180f; + + // Direction: positive sway (left turn) -> tail swings right (negative yaw) + // Flip the sign here if the sway feels inverted + float baseYaw = sway * maxYawDeg * deg2rad; + float baseRoll = -baseYaw * rollFraction; + + float[] weights = { 1.00f, 0.78f, 0.58f, 0.42f, 0.30f, 0.22f }; + + for (int i = 0; i < n; i++) { + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + + float w = weights[i]; + float yaw = baseYaw * w; + float roll = baseRoll * w; + + // OVERRIDE animations on Y/Z only: keep the model's predefined X bend intact + // Do NOT reset rotX here, so the upward bend stays + bone.setRotY(yaw); + bone.setRotZ(roll); + + appliedYaw[i] = yaw; + appliedRoll[i] = roll; + } + + GeoBone head = (GeoBone) getAnimationProcessor().getBone("Neck1"); + + if (head != null) { + var entityData = state.getData(DataTickets.ENTITY_MODEL_DATA); + float clampedYawDeg = Mth.clamp(entityData.netHeadYaw(), -20.0f, 20.0f); + head.setRotY(clampedYawDeg * Mth.DEG_TO_RAD); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/CarcharodontosaurusRenderer.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/CarcharodontosaurusRenderer.java new file mode 100644 index 0000000..e5f4db1 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/CarcharodontosaurusRenderer.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.cmr.jurassicrevived.entity.custom.CarcharodontosaurusEntity; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.util.Mth; +import software.bernie.geckolib.cache.object.BakedGeoModel; +import software.bernie.geckolib.renderer.GeoEntityRenderer; + + +public class CarcharodontosaurusRenderer extends GeoEntityRenderer { + private final float animalScale = 2.25F; + public CarcharodontosaurusRenderer(EntityRendererProvider.Context renderManager) { + super(renderManager, new CarcharodontosaurusModel()); + } + + @Override + public void scaleModelForRender(float widthScale, float heightScale, PoseStack poseStack, CarcharodontosaurusEntity animatable, BakedGeoModel model, boolean isReRender, float partialTick, int packedLight, int packedOverlay) { + poseStack.scale(animalScale, animalScale, animalScale); + if(animatable.isBaby()) { + float growthProgress = Mth.clamp((24000.0F + animatable.getSyncedAge()) / 24000.0F, 0.0F, 1.0F); + float scale = Mth.lerp(growthProgress, 0.2F, 1.0F); + poseStack.scale(scale, scale, scale); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/CarcharodontosaurusVariant.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/CarcharodontosaurusVariant.java new file mode 100644 index 0000000..8dcd4ba --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/CarcharodontosaurusVariant.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import java.util.Arrays; +import java.util.Comparator; + +public enum CarcharodontosaurusVariant { + MALE(0), + FEMALE(1); + + private static final CarcharodontosaurusVariant[] BY_ID = Arrays.stream(values()).sorted( + Comparator.comparingInt(CarcharodontosaurusVariant::getId)).toArray(CarcharodontosaurusVariant[]::new); + + private final int id; + + CarcharodontosaurusVariant(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static CarcharodontosaurusVariant byId(int id) { + return BY_ID[id % BY_ID.length]; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/CarnotaurusModel.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/CarnotaurusModel.java new file mode 100644 index 0000000..8f54a22 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/CarnotaurusModel.java @@ -0,0 +1,112 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.google.common.collect.Maps; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.entity.custom.CarnotaurusEntity; +import net.minecraft.Util; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animation.AnimationState; +/*?} else {*/ +/*import software.bernie.geckolib.animation.AnimationState; + *//*?}*/ +import software.bernie.geckolib.cache.object.GeoBone; +import software.bernie.geckolib.constant.DataTickets; +import software.bernie.geckolib.model.GeoModel; + +import java.util.Map; + +public class CarnotaurusModel extends GeoModel { + + private static final Map LOCATION_BY_VARIANT = + Util.make(Maps.newEnumMap(CarnotaurusVariant.class), map -> { + map.put(CarnotaurusVariant.MALE, Constants.rl("textures/entity/carnotaurus.png")); + map.put(CarnotaurusVariant.FEMALE, Constants.rl("textures/entity/carnotaurus_female.png")); + }); + + // Model-local "currently applied" offsets; cleared before each entity render + private float[] appliedYaw = null; + private float[] appliedRoll = null; + + @Override + public ResourceLocation getModelResource(CarnotaurusEntity animatable) { + return Constants.rl("geo/carnotaurus.geo.json"); + } + + @Override + public ResourceLocation getTextureResource(CarnotaurusEntity animatable) { + return LOCATION_BY_VARIANT.get(animatable.getVariant()); + } + + @Override + public ResourceLocation getAnimationResource(CarnotaurusEntity animatable) { + return Constants.rl("animations/carnotaurus.animation.json"); + } + + @Override + public void setCustomAnimations(CarnotaurusEntity entity, long id, AnimationState state) { + super.setCustomAnimations(entity, id, state); + + String[] tailBones = { "Tail1", "Tail2", "Tail3", "Tail4", "Tail5", "Tail6" }; + int n = tailBones.length; + + if (appliedYaw == null || appliedYaw.length != n) { + appliedYaw = new float[n]; + appliedRoll = new float[n]; + } + + // 1) Clear previous offsets (from the last entity rendered with this model instance) + for (int i = 0; i < n; i++) { + if (appliedYaw[i] == 0.0f && appliedRoll[i] == 0.0f) continue; + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + if (appliedYaw[i] != 0.0f) bone.setRotY(bone.getRotY() - appliedYaw[i]); + if (appliedRoll[i] != 0.0f) bone.setRotZ(bone.getRotZ() - appliedRoll[i]); + appliedYaw[i] = 0.0f; + appliedRoll[i] = 0.0f; + } + + // 2) Interpolated sway for extra smoothness between ticks + float sway = entity.getTailSwayOffset(state.getPartialTick()); // [-1, 1] + + // Tuning + float maxYawDeg = 22.0f; // increased max sweep + float swayGain = 1.35f; // amplifies overall power + float rollFraction = 0.40f; // slightly stronger roll for heft + + float deg2rad = (float)Math.PI / 180f; + + // Direction: positive sway (left turn) -> tail swings right (negative yaw) + // Flip the sign here if the sway feels inverted + float baseYaw = sway * maxYawDeg * deg2rad; + float baseRoll = -baseYaw * rollFraction; + + float[] weights = { 1.00f, 0.78f, 0.58f, 0.42f, 0.30f, 0.22f }; + + for (int i = 0; i < n; i++) { + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + + float w = weights[i]; + float yaw = baseYaw * w; + float roll = baseRoll * w; + + // OVERRIDE animations on Y/Z only: keep the model's predefined X bend intact + // Do NOT reset rotX here, so the upward bend stays + bone.setRotY(yaw); + bone.setRotZ(roll); + + appliedYaw[i] = yaw; + appliedRoll[i] = roll; + } + + GeoBone head = (GeoBone) getAnimationProcessor().getBone("Neck1"); + + if (head != null) { + var entityData = state.getData(DataTickets.ENTITY_MODEL_DATA); + float clampedYawDeg = Mth.clamp(entityData.netHeadYaw(), -20.0f, 20.0f); + head.setRotY(clampedYawDeg * Mth.DEG_TO_RAD); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/CarnotaurusRenderer.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/CarnotaurusRenderer.java new file mode 100644 index 0000000..83c730c --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/CarnotaurusRenderer.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.cmr.jurassicrevived.entity.custom.CarnotaurusEntity; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.util.Mth; +import software.bernie.geckolib.cache.object.BakedGeoModel; +import software.bernie.geckolib.renderer.GeoEntityRenderer; + + +public class CarnotaurusRenderer extends GeoEntityRenderer { + private final float animalScale = 1.5F; + public CarnotaurusRenderer(EntityRendererProvider.Context renderManager) { + super(renderManager, new CarnotaurusModel()); + } + + @Override + public void scaleModelForRender(float widthScale, float heightScale, PoseStack poseStack, CarnotaurusEntity animatable, BakedGeoModel model, boolean isReRender, float partialTick, int packedLight, int packedOverlay) { + poseStack.scale(animalScale, animalScale, animalScale); + if(animatable.isBaby()) { + float growthProgress = Mth.clamp((24000.0F + animatable.getSyncedAge()) / 24000.0F, 0.0F, 1.0F); + float scale = Mth.lerp(growthProgress, 0.2F, 1.0F); + poseStack.scale(scale, scale, scale); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/CarnotaurusVariant.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/CarnotaurusVariant.java new file mode 100644 index 0000000..32c187f --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/CarnotaurusVariant.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import java.util.Arrays; +import java.util.Comparator; + +public enum CarnotaurusVariant { + MALE(0), + FEMALE(1); + + private static final CarnotaurusVariant[] BY_ID = Arrays.stream(values()).sorted( + Comparator.comparingInt(CarnotaurusVariant::getId)).toArray(CarnotaurusVariant[]::new); + + private final int id; + + CarnotaurusVariant(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static CarnotaurusVariant byId(int id) { + return BY_ID[id % BY_ID.length]; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/CearadactylusModel.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/CearadactylusModel.java new file mode 100644 index 0000000..0459d5b --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/CearadactylusModel.java @@ -0,0 +1,112 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.google.common.collect.Maps; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.entity.custom.CearadactylusEntity; +import net.minecraft.Util; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animation.AnimationState; +/*?} else {*/ +/*import software.bernie.geckolib.animation.AnimationState; + *//*?}*/ +import software.bernie.geckolib.cache.object.GeoBone; +import software.bernie.geckolib.constant.DataTickets; +import software.bernie.geckolib.model.GeoModel; + +import java.util.Map; + +public class CearadactylusModel extends GeoModel { + + private static final Map LOCATION_BY_VARIANT = + Util.make(Maps.newEnumMap(CearadactylusVariant.class), map -> { + map.put(CearadactylusVariant.MALE, Constants.rl("textures/entity/cearadactylus.png")); + map.put(CearadactylusVariant.FEMALE, Constants.rl("textures/entity/cearadactylus_female.png")); + }); + + // Model-local "currently applied" offsets; cleared before each entity render + private float[] appliedYaw = null; + private float[] appliedRoll = null; + + @Override + public ResourceLocation getModelResource(CearadactylusEntity animatable) { + return Constants.rl("geo/cearadactylus.geo.json"); + } + + @Override + public ResourceLocation getTextureResource(CearadactylusEntity animatable) { + return LOCATION_BY_VARIANT.get(animatable.getVariant()); + } + + @Override + public ResourceLocation getAnimationResource(CearadactylusEntity animatable) { + return Constants.rl("animations/cearadactylus.animation.json"); + } + + @Override + public void setCustomAnimations(CearadactylusEntity entity, long id, AnimationState state) { + super.setCustomAnimations(entity, id, state); + + String[] tailBones = { "Tail1", "Tail2", "Tail3" }; + int n = tailBones.length; + + if (appliedYaw == null || appliedYaw.length != n) { + appliedYaw = new float[n]; + appliedRoll = new float[n]; + } + + // 1) Clear previous offsets (from the last entity rendered with this model instance) + for (int i = 0; i < n; i++) { + if (appliedYaw[i] == 0.0f && appliedRoll[i] == 0.0f) continue; + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + if (appliedYaw[i] != 0.0f) bone.setRotY(bone.getRotY() - appliedYaw[i]); + if (appliedRoll[i] != 0.0f) bone.setRotZ(bone.getRotZ() - appliedRoll[i]); + appliedYaw[i] = 0.0f; + appliedRoll[i] = 0.0f; + } + + // 2) Interpolated sway for extra smoothness between ticks + float sway = entity.getTailSwayOffset(state.getPartialTick()); // [-1, 1] + + // Tuning + float maxYawDeg = 22.0f; // increased max sweep + float swayGain = 1.35f; // amplifies overall power + float rollFraction = 0.40f; // slightly stronger roll for heft + + float deg2rad = (float)Math.PI / 180f; + + // Direction: positive sway (left turn) -> tail swings right (negative yaw) + // Flip the sign here if the sway feels inverted + float baseYaw = sway * maxYawDeg * deg2rad; + float baseRoll = -baseYaw * rollFraction; + + float[] weights = { 1.00f, 0.78f, 0.58f, 0.42f, 0.30f, 0.22f }; + + for (int i = 0; i < n; i++) { + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + + float w = weights[i]; + float yaw = baseYaw * w; + float roll = baseRoll * w; + + // OVERRIDE animations on Y/Z only: keep the model's predefined X bend intact + // Do NOT reset rotX here, so the upward bend stays + bone.setRotY(yaw); + bone.setRotZ(roll); + + appliedYaw[i] = yaw; + appliedRoll[i] = roll; + } + + GeoBone head = (GeoBone) getAnimationProcessor().getBone("Neck1"); + + if (head != null) { + var entityData = state.getData(DataTickets.ENTITY_MODEL_DATA); + float clampedYawDeg = Mth.clamp(entityData.netHeadYaw(), -20.0f, 20.0f); + head.setRotY(clampedYawDeg * Mth.DEG_TO_RAD); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/CearadactylusRenderer.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/CearadactylusRenderer.java new file mode 100644 index 0000000..ccd7cfd --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/CearadactylusRenderer.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.cmr.jurassicrevived.entity.custom.CearadactylusEntity; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.util.Mth; +import software.bernie.geckolib.cache.object.BakedGeoModel; +import software.bernie.geckolib.renderer.GeoEntityRenderer; + + +public class CearadactylusRenderer extends GeoEntityRenderer { + private final float animalScale = 1.0F; + public CearadactylusRenderer(EntityRendererProvider.Context renderManager) { + super(renderManager, new CearadactylusModel()); + } + + @Override + public void scaleModelForRender(float widthScale, float heightScale, PoseStack poseStack, CearadactylusEntity animatable, BakedGeoModel model, boolean isReRender, float partialTick, int packedLight, int packedOverlay) { + poseStack.scale(animalScale, animalScale, animalScale); + if(animatable.isBaby()) { + float growthProgress = Mth.clamp((24000.0F + animatable.getSyncedAge()) / 24000.0F, 0.0F, 1.0F); + float scale = Mth.lerp(growthProgress, 0.2F, 1.0F); + poseStack.scale(scale, scale, scale); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/CearadactylusVariant.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/CearadactylusVariant.java new file mode 100644 index 0000000..3d763af --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/CearadactylusVariant.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import java.util.Arrays; +import java.util.Comparator; + +public enum CearadactylusVariant { + MALE(0), + FEMALE(1); + + private static final CearadactylusVariant[] BY_ID = Arrays.stream(values()).sorted( + Comparator.comparingInt(CearadactylusVariant::getId)).toArray(CearadactylusVariant[]::new); + + private final int id; + + CearadactylusVariant(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static CearadactylusVariant byId(int id) { + return BY_ID[id % BY_ID.length]; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/CeratosaurusModel.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/CeratosaurusModel.java new file mode 100644 index 0000000..842d1c5 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/CeratosaurusModel.java @@ -0,0 +1,112 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.google.common.collect.Maps; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.entity.custom.CeratosaurusEntity; +import net.minecraft.Util; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animation.AnimationState; +/*?} else {*/ +/*import software.bernie.geckolib.animation.AnimationState; + *//*?}*/ +import software.bernie.geckolib.cache.object.GeoBone; +import software.bernie.geckolib.constant.DataTickets; +import software.bernie.geckolib.model.GeoModel; + +import java.util.Map; + +public class CeratosaurusModel extends GeoModel { + + private static final Map LOCATION_BY_VARIANT = + Util.make(Maps.newEnumMap(CeratosaurusVariant.class), map -> { + map.put(CeratosaurusVariant.MALE, Constants.rl("textures/entity/ceratosaurus.png")); + map.put(CeratosaurusVariant.FEMALE, Constants.rl("textures/entity/ceratosaurus_female.png")); + }); + + // Model-local "currently applied" offsets; cleared before each entity render + private float[] appliedYaw = null; + private float[] appliedRoll = null; + + @Override + public ResourceLocation getModelResource(CeratosaurusEntity animatable) { + return Constants.rl("geo/ceratosaurus.geo.json"); + } + + @Override + public ResourceLocation getTextureResource(CeratosaurusEntity animatable) { + return LOCATION_BY_VARIANT.get(animatable.getVariant()); + } + + @Override + public ResourceLocation getAnimationResource(CeratosaurusEntity animatable) { + return Constants.rl("animations/ceratosaurus.animation.json"); + } + + @Override + public void setCustomAnimations(CeratosaurusEntity entity, long id, AnimationState state) { + super.setCustomAnimations(entity, id, state); + + String[] tailBones = { "TailBASE", "Tail2", "Tail3", "Tail4", "Tail5", "Tail6" }; + int n = tailBones.length; + + if (appliedYaw == null || appliedYaw.length != n) { + appliedYaw = new float[n]; + appliedRoll = new float[n]; + } + + // 1) Clear previous offsets (from the last entity rendered with this model instance) + for (int i = 0; i < n; i++) { + if (appliedYaw[i] == 0.0f && appliedRoll[i] == 0.0f) continue; + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + if (appliedYaw[i] != 0.0f) bone.setRotY(bone.getRotY() - appliedYaw[i]); + if (appliedRoll[i] != 0.0f) bone.setRotZ(bone.getRotZ() - appliedRoll[i]); + appliedYaw[i] = 0.0f; + appliedRoll[i] = 0.0f; + } + + // 2) Interpolated sway for extra smoothness between ticks + float sway = entity.getTailSwayOffset(state.getPartialTick()); // [-1, 1] + + // Tuning + float maxYawDeg = 22.0f; // increased max sweep + float swayGain = 1.35f; // amplifies overall power + float rollFraction = 0.40f; // slightly stronger roll for heft + + float deg2rad = (float)Math.PI / 180f; + + // Direction: positive sway (left turn) -> tail swings right (negative yaw) + // Flip the sign here if the sway feels inverted + float baseYaw = sway * maxYawDeg * deg2rad; + float baseRoll = -baseYaw * rollFraction; + + float[] weights = { 1.00f, 0.78f, 0.58f, 0.42f, 0.30f, 0.22f }; + + for (int i = 0; i < n; i++) { + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + + float w = weights[i]; + float yaw = baseYaw * w; + float roll = baseRoll * w; + + // OVERRIDE animations on Y/Z only: keep the model's predefined X bend intact + // Do NOT reset rotX here, so the upward bend stays + bone.setRotY(yaw); + bone.setRotZ(roll); + + appliedYaw[i] = yaw; + appliedRoll[i] = roll; + } + + GeoBone head = (GeoBone) getAnimationProcessor().getBone("Neck1"); + + if (head != null) { + var entityData = state.getData(DataTickets.ENTITY_MODEL_DATA); + float clampedYawDeg = Mth.clamp(entityData.netHeadYaw(), -30.0f, 30.0f); + head.setRotY(clampedYawDeg * Mth.DEG_TO_RAD); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/CeratosaurusRenderer.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/CeratosaurusRenderer.java new file mode 100644 index 0000000..322e01e --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/CeratosaurusRenderer.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.cmr.jurassicrevived.entity.custom.CeratosaurusEntity; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.util.Mth; +import software.bernie.geckolib.cache.object.BakedGeoModel; +import software.bernie.geckolib.renderer.GeoEntityRenderer; + + +public class CeratosaurusRenderer extends GeoEntityRenderer { + private final float animalScale = 1.75F; + public CeratosaurusRenderer(EntityRendererProvider.Context renderManager) { + super(renderManager, new CeratosaurusModel()); + } + + @Override + public void scaleModelForRender(float widthScale, float heightScale, PoseStack poseStack, CeratosaurusEntity animatable, BakedGeoModel model, boolean isReRender, float partialTick, int packedLight, int packedOverlay) { + poseStack.scale(animalScale, animalScale, animalScale); + if(animatable.isBaby()) { + float growthProgress = Mth.clamp((24000.0F + animatable.getSyncedAge()) / 24000.0F, 0.0F, 1.0F); + float scale = Mth.lerp(growthProgress, 0.2F, 1.0F); + poseStack.scale(scale, scale, scale); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/CeratosaurusVariant.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/CeratosaurusVariant.java new file mode 100644 index 0000000..104127d --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/CeratosaurusVariant.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import java.util.Arrays; +import java.util.Comparator; + +public enum CeratosaurusVariant { + MALE(0), + FEMALE(1); + + private static final CeratosaurusVariant[] BY_ID = Arrays.stream(values()).sorted( + Comparator.comparingInt(CeratosaurusVariant::getId)).toArray(CeratosaurusVariant[]::new); + + private final int id; + + CeratosaurusVariant(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static CeratosaurusVariant byId(int id) { + return BY_ID[id % BY_ID.length]; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/ChasmosaurusModel.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/ChasmosaurusModel.java new file mode 100644 index 0000000..a058725 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/ChasmosaurusModel.java @@ -0,0 +1,112 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.google.common.collect.Maps; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.entity.custom.ChasmosaurusEntity; +import net.minecraft.Util; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animation.AnimationState; +/*?} else {*/ +/*import software.bernie.geckolib.animation.AnimationState; + *//*?}*/ +import software.bernie.geckolib.cache.object.GeoBone; +import software.bernie.geckolib.constant.DataTickets; +import software.bernie.geckolib.model.GeoModel; + +import java.util.Map; + +public class ChasmosaurusModel extends GeoModel { + + private static final Map LOCATION_BY_VARIANT = + Util.make(Maps.newEnumMap(ChasmosaurusVariant.class), map -> { + map.put(ChasmosaurusVariant.MALE, Constants.rl("textures/entity/chasmosaurus.png")); + map.put(ChasmosaurusVariant.FEMALE, Constants.rl("textures/entity/chasmosaurus_female.png")); + }); + + // Model-local "currently applied" offsets; cleared before each entity render + private float[] appliedYaw = null; + private float[] appliedRoll = null; + + @Override + public ResourceLocation getModelResource(ChasmosaurusEntity animatable) { + return Constants.rl("geo/chasmosaurus.geo.json"); + } + + @Override + public ResourceLocation getTextureResource(ChasmosaurusEntity animatable) { + return LOCATION_BY_VARIANT.get(animatable.getVariant()); + } + + @Override + public ResourceLocation getAnimationResource(ChasmosaurusEntity animatable) { + return Constants.rl("animations/chasmosaurus.animation.json"); + } + + @Override + public void setCustomAnimations(ChasmosaurusEntity entity, long id, AnimationState state) { + super.setCustomAnimations(entity, id, state); + + String[] tailBones = { "Tail1", "Tail2", "Tail3", "Tail4", "Tail5" }; + int n = tailBones.length; + + if (appliedYaw == null || appliedYaw.length != n) { + appliedYaw = new float[n]; + appliedRoll = new float[n]; + } + + // 1) Clear previous offsets (from the last entity rendered with this model instance) + for (int i = 0; i < n; i++) { + if (appliedYaw[i] == 0.0f && appliedRoll[i] == 0.0f) continue; + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + if (appliedYaw[i] != 0.0f) bone.setRotY(bone.getRotY() - appliedYaw[i]); + if (appliedRoll[i] != 0.0f) bone.setRotZ(bone.getRotZ() - appliedRoll[i]); + appliedYaw[i] = 0.0f; + appliedRoll[i] = 0.0f; + } + + // 2) Interpolated sway for extra smoothness between ticks + float sway = entity.getTailSwayOffset(state.getPartialTick()); // [-1, 1] + + // Tuning + float maxYawDeg = 22.0f; // increased max sweep + float swayGain = 1.35f; // amplifies overall power + float rollFraction = 0.40f; // slightly stronger roll for heft + + float deg2rad = (float)Math.PI / 180f; + + // Direction: positive sway (left turn) -> tail swings right (negative yaw) + // Flip the sign here if the sway feels inverted + float baseYaw = sway * maxYawDeg * deg2rad; + float baseRoll = -baseYaw * rollFraction; + + float[] weights = { 1.00f, 0.78f, 0.58f, 0.42f, 0.30f }; + + for (int i = 0; i < n; i++) { + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + + float w = weights[i]; + float yaw = baseYaw * w; + float roll = baseRoll * w; + + // OVERRIDE animations on Y/Z only: keep the model's predefined X bend intact + // Do NOT reset rotX here, so the upward bend stays + bone.setRotY(yaw); + bone.setRotZ(roll); + + appliedYaw[i] = yaw; + appliedRoll[i] = roll; + } + + GeoBone head = (GeoBone) getAnimationProcessor().getBone("Neck"); + + if (head != null) { + var entityData = state.getData(DataTickets.ENTITY_MODEL_DATA); + float clampedYawDeg = Mth.clamp(entityData.netHeadYaw(), -30.0f, 30.0f); + head.setRotY(clampedYawDeg * Mth.DEG_TO_RAD); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/ChasmosaurusRenderer.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/ChasmosaurusRenderer.java new file mode 100644 index 0000000..a330934 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/ChasmosaurusRenderer.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.cmr.jurassicrevived.entity.custom.ChasmosaurusEntity; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.util.Mth; +import software.bernie.geckolib.cache.object.BakedGeoModel; +import software.bernie.geckolib.renderer.GeoEntityRenderer; + + +public class ChasmosaurusRenderer extends GeoEntityRenderer { + private final float animalScale = 1.25F; + public ChasmosaurusRenderer(EntityRendererProvider.Context renderManager) { + super(renderManager, new ChasmosaurusModel()); + } + + @Override + public void scaleModelForRender(float widthScale, float heightScale, PoseStack poseStack, ChasmosaurusEntity animatable, BakedGeoModel model, boolean isReRender, float partialTick, int packedLight, int packedOverlay) { + poseStack.scale(animalScale, animalScale, animalScale); + if(animatable.isBaby()) { + float growthProgress = Mth.clamp((24000.0F + animatable.getSyncedAge()) / 24000.0F, 0.0F, 1.0F); + float scale = Mth.lerp(growthProgress, 0.2F, 1.0F); + poseStack.scale(scale, scale, scale); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/ChasmosaurusVariant.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/ChasmosaurusVariant.java new file mode 100644 index 0000000..e7e3809 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/ChasmosaurusVariant.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import java.util.Arrays; +import java.util.Comparator; + +public enum ChasmosaurusVariant { + MALE(0), + FEMALE(1); + + private static final ChasmosaurusVariant[] BY_ID = Arrays.stream(values()).sorted( + Comparator.comparingInt(ChasmosaurusVariant::getId)).toArray(ChasmosaurusVariant[]::new); + + private final int id; + + ChasmosaurusVariant(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static ChasmosaurusVariant byId(int id) { + return BY_ID[id % BY_ID.length]; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/ChickenosaurusModel.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/ChickenosaurusModel.java new file mode 100644 index 0000000..bd6dea3 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/ChickenosaurusModel.java @@ -0,0 +1,112 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.google.common.collect.Maps; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.entity.custom.ChickenosaurusEntity; +import net.minecraft.Util; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animation.AnimationState; +/*?} else {*/ +/*import software.bernie.geckolib.animation.AnimationState; + *//*?}*/ +import software.bernie.geckolib.cache.object.GeoBone; +import software.bernie.geckolib.constant.DataTickets; +import software.bernie.geckolib.model.GeoModel; + +import java.util.Map; + +public class ChickenosaurusModel extends GeoModel { + + private static final Map LOCATION_BY_VARIANT = + Util.make(Maps.newEnumMap(ChickenosaurusVariant.class), map -> { + map.put(ChickenosaurusVariant.MALE, Constants.rl("textures/entity/chickenosaurus.png")); + map.put(ChickenosaurusVariant.FEMALE, Constants.rl("textures/entity/chickenosaurus_female.png")); + }); + + // Model-local "currently applied" offsets; cleared before each entity render + private float[] appliedYaw = null; + private float[] appliedRoll = null; + + @Override + public ResourceLocation getModelResource(ChickenosaurusEntity animatable) { + return Constants.rl("geo/chickenosaurus.geo.json"); + } + + @Override + public ResourceLocation getTextureResource(ChickenosaurusEntity animatable) { + return LOCATION_BY_VARIANT.get(animatable.getVariant()); + } + + @Override + public ResourceLocation getAnimationResource(ChickenosaurusEntity animatable) { + return Constants.rl("animations/chickenosaurus.animation.json"); + } + + @Override + public void setCustomAnimations(ChickenosaurusEntity entity, long id, AnimationState state) { + super.setCustomAnimations(entity, id, state); + + String[] tailBones = { "Tail1", "Tail2", "Tail3" }; + int n = tailBones.length; + + if (appliedYaw == null || appliedYaw.length != n) { + appliedYaw = new float[n]; + appliedRoll = new float[n]; + } + + // 1) Clear previous offsets (from the last entity rendered with this model instance) + for (int i = 0; i < n; i++) { + if (appliedYaw[i] == 0.0f && appliedRoll[i] == 0.0f) continue; + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + if (appliedYaw[i] != 0.0f) bone.setRotY(bone.getRotY() - appliedYaw[i]); + if (appliedRoll[i] != 0.0f) bone.setRotZ(bone.getRotZ() - appliedRoll[i]); + appliedYaw[i] = 0.0f; + appliedRoll[i] = 0.0f; + } + + // 2) Interpolated sway for extra smoothness between ticks + float sway = entity.getTailSwayOffset(state.getPartialTick()); // [-1, 1] + + // Tuning + float maxYawDeg = 22.0f; // increased max sweep + float swayGain = 1.35f; // amplifies overall power + float rollFraction = 0.40f; // slightly stronger roll for heft + + float deg2rad = (float)Math.PI / 180f; + + // Direction: positive sway (left turn) -> tail swings right (negative yaw) + // Flip the sign here if the sway feels inverted + float baseYaw = sway * maxYawDeg * deg2rad; + float baseRoll = -baseYaw * rollFraction; + + float[] weights = { 1.00f, 0.78f, 0.58f }; + + for (int i = 0; i < n; i++) { + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + + float w = weights[i]; + float yaw = baseYaw * w; + float roll = baseRoll * w; + + // OVERRIDE animations on Y/Z only: keep the model's predefined X bend intact + // Do NOT reset rotX here, so the upward bend stays + bone.setRotY(yaw); + bone.setRotZ(roll); + + appliedYaw[i] = yaw; + appliedRoll[i] = roll; + } + + GeoBone head = (GeoBone) getAnimationProcessor().getBone("Neck1"); + + if (head != null) { + var entityData = state.getData(DataTickets.ENTITY_MODEL_DATA); + float clampedYawDeg = Mth.clamp(entityData.netHeadYaw(), -30.0f, 30.0f); + head.setRotY(clampedYawDeg * Mth.DEG_TO_RAD); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/ChickenosaurusRenderer.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/ChickenosaurusRenderer.java new file mode 100644 index 0000000..4d997da --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/ChickenosaurusRenderer.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.cmr.jurassicrevived.entity.custom.ChickenosaurusEntity; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.util.Mth; +import software.bernie.geckolib.cache.object.BakedGeoModel; +import software.bernie.geckolib.renderer.GeoEntityRenderer; + + +public class ChickenosaurusRenderer extends GeoEntityRenderer { + private final float animalScale = 1.0F; + public ChickenosaurusRenderer(EntityRendererProvider.Context renderManager) { + super(renderManager, new ChickenosaurusModel()); + } + + @Override + public void scaleModelForRender(float widthScale, float heightScale, PoseStack poseStack, ChickenosaurusEntity animatable, BakedGeoModel model, boolean isReRender, float partialTick, int packedLight, int packedOverlay) { + poseStack.scale(animalScale, animalScale, animalScale); + if(animatable.isBaby()) { + float growthProgress = Mth.clamp((24000.0F + animatable.getSyncedAge()) / 24000.0F, 0.0F, 1.0F); + float scale = Mth.lerp(growthProgress, 0.2F, 1.0F); + poseStack.scale(scale, scale, scale); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/ChickenosaurusVariant.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/ChickenosaurusVariant.java new file mode 100644 index 0000000..d08af9f --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/ChickenosaurusVariant.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import java.util.Arrays; +import java.util.Comparator; + +public enum ChickenosaurusVariant { + MALE(0), + FEMALE(1); + + private static final ChickenosaurusVariant[] BY_ID = Arrays.stream(values()).sorted( + Comparator.comparingInt(ChickenosaurusVariant::getId)).toArray(ChickenosaurusVariant[]::new); + + private final int id; + + ChickenosaurusVariant(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static ChickenosaurusVariant byId(int id) { + return BY_ID[id % BY_ID.length]; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/CoelophysisModel.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/CoelophysisModel.java new file mode 100644 index 0000000..5d7123b --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/CoelophysisModel.java @@ -0,0 +1,112 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.google.common.collect.Maps; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.entity.custom.CoelophysisEntity; +import net.minecraft.Util; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animation.AnimationState; +/*?} else {*/ +/*import software.bernie.geckolib.animation.AnimationState; + *//*?}*/ +import software.bernie.geckolib.cache.object.GeoBone; +import software.bernie.geckolib.constant.DataTickets; +import software.bernie.geckolib.model.GeoModel; + +import java.util.Map; + +public class CoelophysisModel extends GeoModel { + + private static final Map LOCATION_BY_VARIANT = + Util.make(Maps.newEnumMap(CoelophysisVariant.class), map -> { + map.put(CoelophysisVariant.MALE, Constants.rl("textures/entity/coelophysis.png")); + map.put(CoelophysisVariant.FEMALE, Constants.rl("textures/entity/coelophysis_female.png")); + }); + + // Model-local "currently applied" offsets; cleared before each entity render + private float[] appliedYaw = null; + private float[] appliedRoll = null; + + @Override + public ResourceLocation getModelResource(CoelophysisEntity animatable) { + return Constants.rl("geo/coelophysis.geo.json"); + } + + @Override + public ResourceLocation getTextureResource(CoelophysisEntity animatable) { + return LOCATION_BY_VARIANT.get(animatable.getVariant()); + } + + @Override + public ResourceLocation getAnimationResource(CoelophysisEntity animatable) { + return Constants.rl("animations/coelophysis.animation.json"); + } + + @Override + public void setCustomAnimations(CoelophysisEntity entity, long id, AnimationState state) { + super.setCustomAnimations(entity, id, state); + + String[] tailBones = { "tail1", "tail2", "tail3", "tail4", "Tail5" }; + int n = tailBones.length; + + if (appliedYaw == null || appliedYaw.length != n) { + appliedYaw = new float[n]; + appliedRoll = new float[n]; + } + + // 1) Clear previous offsets (from the last entity rendered with this model instance) + for (int i = 0; i < n; i++) { + if (appliedYaw[i] == 0.0f && appliedRoll[i] == 0.0f) continue; + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + if (appliedYaw[i] != 0.0f) bone.setRotY(bone.getRotY() - appliedYaw[i]); + if (appliedRoll[i] != 0.0f) bone.setRotZ(bone.getRotZ() - appliedRoll[i]); + appliedYaw[i] = 0.0f; + appliedRoll[i] = 0.0f; + } + + // 2) Interpolated sway for extra smoothness between ticks + float sway = entity.getTailSwayOffset(state.getPartialTick()); // [-1, 1] + + // Tuning + float maxYawDeg = 22.0f; // increased max sweep + float swayGain = 1.35f; // amplifies overall power + float rollFraction = 0.40f; // slightly stronger roll for heft + + float deg2rad = (float)Math.PI / 180f; + + // Direction: positive sway (left turn) -> tail swings right (negative yaw) + // Flip the sign here if the sway feels inverted + float baseYaw = sway * maxYawDeg * deg2rad; + float baseRoll = -baseYaw * rollFraction; + + float[] weights = { 1.00f, 0.78f, 0.58f, 0.42f, 0.30f }; + + for (int i = 0; i < n; i++) { + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + + float w = weights[i]; + float yaw = baseYaw * w; + float roll = baseRoll * w; + + // OVERRIDE animations on Y/Z only: keep the model's predefined X bend intact + // Do NOT reset rotX here, so the upward bend stays + bone.setRotY(yaw); + bone.setRotZ(roll); + + appliedYaw[i] = yaw; + appliedRoll[i] = roll; + } + + GeoBone head = (GeoBone) getAnimationProcessor().getBone("neck1"); + + if (head != null) { + var entityData = state.getData(DataTickets.ENTITY_MODEL_DATA); + float clampedYawDeg = Mth.clamp(entityData.netHeadYaw(), -30.0f, 30.0f); + head.setRotY(clampedYawDeg * Mth.DEG_TO_RAD); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/CoelophysisRenderer.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/CoelophysisRenderer.java new file mode 100644 index 0000000..6fe0dbf --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/CoelophysisRenderer.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.cmr.jurassicrevived.entity.custom.CoelophysisEntity; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.util.Mth; +import software.bernie.geckolib.cache.object.BakedGeoModel; +import software.bernie.geckolib.renderer.GeoEntityRenderer; + + +public class CoelophysisRenderer extends GeoEntityRenderer { + private final float animalScale = 0.9F; + public CoelophysisRenderer(EntityRendererProvider.Context renderManager) { + super(renderManager, new CoelophysisModel()); + } + + @Override + public void scaleModelForRender(float widthScale, float heightScale, PoseStack poseStack, CoelophysisEntity animatable, BakedGeoModel model, boolean isReRender, float partialTick, int packedLight, int packedOverlay) { + poseStack.scale(animalScale, animalScale, animalScale); + if(animatable.isBaby()) { + float growthProgress = Mth.clamp((24000.0F + animatable.getSyncedAge()) / 24000.0F, 0.0F, 1.0F); + float scale = Mth.lerp(growthProgress, 0.2F, 1.0F); + poseStack.scale(scale, scale, scale); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/CoelophysisVariant.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/CoelophysisVariant.java new file mode 100644 index 0000000..b40edf2 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/CoelophysisVariant.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import java.util.Arrays; +import java.util.Comparator; + +public enum CoelophysisVariant { + MALE(0), + FEMALE(1); + + private static final CoelophysisVariant[] BY_ID = Arrays.stream(values()).sorted( + Comparator.comparingInt(CoelophysisVariant::getId)).toArray(CoelophysisVariant[]::new); + + private final int id; + + CoelophysisVariant(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static CoelophysisVariant byId(int id) { + return BY_ID[id % BY_ID.length]; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/CoelurusModel.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/CoelurusModel.java new file mode 100644 index 0000000..fa9cafb --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/CoelurusModel.java @@ -0,0 +1,112 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.google.common.collect.Maps; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.entity.custom.CoelurusEntity; +import net.minecraft.Util; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animation.AnimationState; +/*?} else {*/ +/*import software.bernie.geckolib.animation.AnimationState; + *//*?}*/ +import software.bernie.geckolib.cache.object.GeoBone; +import software.bernie.geckolib.constant.DataTickets; +import software.bernie.geckolib.model.GeoModel; + +import java.util.Map; + +public class CoelurusModel extends GeoModel { + + private static final Map LOCATION_BY_VARIANT = + Util.make(Maps.newEnumMap(CoelurusVariant.class), map -> { + map.put(CoelurusVariant.MALE, Constants.rl("textures/entity/coelurus.png")); + map.put(CoelurusVariant.FEMALE, Constants.rl("textures/entity/coelurus_female.png")); + }); + + // Model-local "currently applied" offsets; cleared before each entity render + private float[] appliedYaw = null; + private float[] appliedRoll = null; + + @Override + public ResourceLocation getModelResource(CoelurusEntity animatable) { + return Constants.rl("geo/coelurus.geo.json"); + } + + @Override + public ResourceLocation getTextureResource(CoelurusEntity animatable) { + return LOCATION_BY_VARIANT.get(animatable.getVariant()); + } + + @Override + public ResourceLocation getAnimationResource(CoelurusEntity animatable) { + return Constants.rl("animations/coelurus.animation.json"); + } + + @Override + public void setCustomAnimations(CoelurusEntity entity, long id, AnimationState state) { + super.setCustomAnimations(entity, id, state); + + String[] tailBones = { "TailBASE", "Tail2", "Tail3", "Tail4", "Tail5", "Tail6" }; + int n = tailBones.length; + + if (appliedYaw == null || appliedYaw.length != n) { + appliedYaw = new float[n]; + appliedRoll = new float[n]; + } + + // 1) Clear previous offsets (from the last entity rendered with this model instance) + for (int i = 0; i < n; i++) { + if (appliedYaw[i] == 0.0f && appliedRoll[i] == 0.0f) continue; + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + if (appliedYaw[i] != 0.0f) bone.setRotY(bone.getRotY() - appliedYaw[i]); + if (appliedRoll[i] != 0.0f) bone.setRotZ(bone.getRotZ() - appliedRoll[i]); + appliedYaw[i] = 0.0f; + appliedRoll[i] = 0.0f; + } + + // 2) Interpolated sway for extra smoothness between ticks + float sway = entity.getTailSwayOffset(state.getPartialTick()); // [-1, 1] + + // Tuning + float maxYawDeg = 22.0f; // increased max sweep + float swayGain = 1.35f; // amplifies overall power + float rollFraction = 0.40f; // slightly stronger roll for heft + + float deg2rad = (float)Math.PI / 180f; + + // Direction: positive sway (left turn) -> tail swings right (negative yaw) + // Flip the sign here if the sway feels inverted + float baseYaw = sway * maxYawDeg * deg2rad; + float baseRoll = -baseYaw * rollFraction; + + float[] weights = { 1.00f, 0.78f, 0.58f, 0.42f, 0.30f, 0.22f }; + + for (int i = 0; i < n; i++) { + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + + float w = weights[i]; + float yaw = baseYaw * w; + float roll = baseRoll * w; + + // OVERRIDE animations on Y/Z only: keep the model's predefined X bend intact + // Do NOT reset rotX here, so the upward bend stays + bone.setRotY(yaw); + bone.setRotZ(roll); + + appliedYaw[i] = yaw; + appliedRoll[i] = roll; + } + + GeoBone head = (GeoBone) getAnimationProcessor().getBone("NeckBase"); + + if (head != null) { + var entityData = state.getData(DataTickets.ENTITY_MODEL_DATA); + float clampedYawDeg = Mth.clamp(entityData.netHeadYaw(), -30.0f, 30.0f); + head.setRotY(clampedYawDeg * Mth.DEG_TO_RAD); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/CoelurusRenderer.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/CoelurusRenderer.java new file mode 100644 index 0000000..c84fd88 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/CoelurusRenderer.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.cmr.jurassicrevived.entity.custom.CoelurusEntity; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.util.Mth; +import software.bernie.geckolib.cache.object.BakedGeoModel; +import software.bernie.geckolib.renderer.GeoEntityRenderer; + + +public class CoelurusRenderer extends GeoEntityRenderer { + private final float animalScale = 0.5F; + public CoelurusRenderer(EntityRendererProvider.Context renderManager) { + super(renderManager, new CoelurusModel()); + } + + @Override + public void scaleModelForRender(float widthScale, float heightScale, PoseStack poseStack, CoelurusEntity animatable, BakedGeoModel model, boolean isReRender, float partialTick, int packedLight, int packedOverlay) { + poseStack.scale(animalScale, animalScale, animalScale); + if(animatable.isBaby()) { + float growthProgress = Mth.clamp((24000.0F + animatable.getSyncedAge()) / 24000.0F, 0.0F, 1.0F); + float scale = Mth.lerp(growthProgress, 0.2F, 1.0F); + poseStack.scale(scale, scale, scale); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/CoelurusVariant.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/CoelurusVariant.java new file mode 100644 index 0000000..48e7983 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/CoelurusVariant.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import java.util.Arrays; +import java.util.Comparator; + +public enum CoelurusVariant { + MALE(0), + FEMALE(1); + + private static final CoelurusVariant[] BY_ID = Arrays.stream(values()).sorted( + Comparator.comparingInt(CoelurusVariant::getId)).toArray(CoelurusVariant[]::new); + + private final int id; + + CoelurusVariant(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static CoelurusVariant byId(int id) { + return BY_ID[id % BY_ID.length]; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/CompsognathusModel.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/CompsognathusModel.java new file mode 100644 index 0000000..67b7f13 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/CompsognathusModel.java @@ -0,0 +1,112 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.google.common.collect.Maps; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.entity.custom.CompsognathusEntity; +import net.minecraft.Util; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animation.AnimationState; +/*?} else {*/ +/*import software.bernie.geckolib.animation.AnimationState; + *//*?}*/ +import software.bernie.geckolib.cache.object.GeoBone; +import software.bernie.geckolib.constant.DataTickets; +import software.bernie.geckolib.model.GeoModel; + +import java.util.Map; + +public class CompsognathusModel extends GeoModel { + + private static final Map LOCATION_BY_VARIANT = + Util.make(Maps.newEnumMap(CompsognathusVariant.class), map -> { + map.put(CompsognathusVariant.MALE, Constants.rl("textures/entity/compsognathus.png")); + map.put(CompsognathusVariant.FEMALE, Constants.rl("textures/entity/compsognathus_female.png")); + }); + + // Model-local "currently applied" offsets; cleared before each entity render + private float[] appliedYaw = null; + private float[] appliedRoll = null; + + @Override + public ResourceLocation getModelResource(CompsognathusEntity animatable) { + return Constants.rl("geo/compsognathus.geo.json"); + } + + @Override + public ResourceLocation getTextureResource(CompsognathusEntity animatable) { + return LOCATION_BY_VARIANT.get(animatable.getVariant()); + } + + @Override + public ResourceLocation getAnimationResource(CompsognathusEntity animatable) { + return Constants.rl("animations/compsognathus.animation.json"); + } + + @Override + public void setCustomAnimations(CompsognathusEntity entity, long id, AnimationState state) { + super.setCustomAnimations(entity, id, state); + + String[] tailBones = { "Tail1", "Tail2", "Tail3", "Tail4" }; + int n = tailBones.length; + + if (appliedYaw == null || appliedYaw.length != n) { + appliedYaw = new float[n]; + appliedRoll = new float[n]; + } + + // 1) Clear previous offsets (from the last entity rendered with this model instance) + for (int i = 0; i < n; i++) { + if (appliedYaw[i] == 0.0f && appliedRoll[i] == 0.0f) continue; + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + if (appliedYaw[i] != 0.0f) bone.setRotY(bone.getRotY() - appliedYaw[i]); + if (appliedRoll[i] != 0.0f) bone.setRotZ(bone.getRotZ() - appliedRoll[i]); + appliedYaw[i] = 0.0f; + appliedRoll[i] = 0.0f; + } + + // 2) Interpolated sway for extra smoothness between ticks + float sway = entity.getTailSwayOffset(state.getPartialTick()); // [-1, 1] + + // Tuning + float maxYawDeg = 22.0f; // increased max sweep + float swayGain = 1.35f; // amplifies overall power + float rollFraction = 0.40f; // slightly stronger roll for heft + + float deg2rad = (float)Math.PI / 180f; + + // Direction: positive sway (left turn) -> tail swings right (negative yaw) + // Flip the sign here if the sway feels inverted + float baseYaw = sway * maxYawDeg * deg2rad; + float baseRoll = -baseYaw * rollFraction; + + float[] weights = { 1.00f, 0.78f, 0.58f, 0.42f }; + + for (int i = 0; i < n; i++) { + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + + float w = weights[i]; + float yaw = baseYaw * w; + float roll = baseRoll * w; + + // OVERRIDE animations on Y/Z only: keep the model's predefined X bend intact + // Do NOT reset rotX here, so the upward bend stays + bone.setRotY(yaw); + bone.setRotZ(roll); + + appliedYaw[i] = yaw; + appliedRoll[i] = roll; + } + + GeoBone head = (GeoBone) getAnimationProcessor().getBone("Neck1"); + + if (head != null) { + var entityData = state.getData(DataTickets.ENTITY_MODEL_DATA); + float clampedYawDeg = Mth.clamp(entityData.netHeadYaw(), -30.0f, 30.0f); + head.setRotY(clampedYawDeg * Mth.DEG_TO_RAD); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/CompsognathusRenderer.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/CompsognathusRenderer.java new file mode 100644 index 0000000..212eb28 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/CompsognathusRenderer.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.cmr.jurassicrevived.entity.custom.CompsognathusEntity; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.util.Mth; +import software.bernie.geckolib.cache.object.BakedGeoModel; +import software.bernie.geckolib.renderer.GeoEntityRenderer; + + +public class CompsognathusRenderer extends GeoEntityRenderer { + private final float animalScale = 0.35F; + public CompsognathusRenderer(EntityRendererProvider.Context renderManager) { + super(renderManager, new CompsognathusModel()); + } + + @Override + public void scaleModelForRender(float widthScale, float heightScale, PoseStack poseStack, CompsognathusEntity animatable, BakedGeoModel model, boolean isReRender, float partialTick, int packedLight, int packedOverlay) { + poseStack.scale(animalScale, animalScale, animalScale); + if(animatable.isBaby()) { + float growthProgress = Mth.clamp((24000.0F + animatable.getSyncedAge()) / 24000.0F, 0.0F, 1.0F); + float scale = Mth.lerp(growthProgress, 0.2F, 1.0F); + poseStack.scale(scale, scale, scale); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/CompsognathusVariant.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/CompsognathusVariant.java new file mode 100644 index 0000000..64c4436 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/CompsognathusVariant.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import java.util.Arrays; +import java.util.Comparator; + +public enum CompsognathusVariant { + MALE(0), + FEMALE(1); + + private static final CompsognathusVariant[] BY_ID = Arrays.stream(values()).sorted( + Comparator.comparingInt(CompsognathusVariant::getId)).toArray(CompsognathusVariant[]::new); + + private final int id; + + CompsognathusVariant(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static CompsognathusVariant byId(int id) { + return BY_ID[id % BY_ID.length]; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/ConcavenatorModel.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/ConcavenatorModel.java new file mode 100644 index 0000000..0e33eb3 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/ConcavenatorModel.java @@ -0,0 +1,112 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.google.common.collect.Maps; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.entity.custom.ConcavenatorEntity; +import net.minecraft.Util; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animation.AnimationState; +/*?} else {*/ +/*import software.bernie.geckolib.animation.AnimationState; + *//*?}*/ +import software.bernie.geckolib.cache.object.GeoBone; +import software.bernie.geckolib.constant.DataTickets; +import software.bernie.geckolib.model.GeoModel; + +import java.util.Map; + +public class ConcavenatorModel extends GeoModel { + + private static final Map LOCATION_BY_VARIANT = + Util.make(Maps.newEnumMap(ConcavenatorVariant.class), map -> { + map.put(ConcavenatorVariant.MALE, Constants.rl("textures/entity/concavenator.png")); + map.put(ConcavenatorVariant.FEMALE, Constants.rl("textures/entity/concavenator_female.png")); + }); + + // Model-local "currently applied" offsets; cleared before each entity render + private float[] appliedYaw = null; + private float[] appliedRoll = null; + + @Override + public ResourceLocation getModelResource(ConcavenatorEntity animatable) { + return Constants.rl("geo/concavenator.geo.json"); + } + + @Override + public ResourceLocation getTextureResource(ConcavenatorEntity animatable) { + return LOCATION_BY_VARIANT.get(animatable.getVariant()); + } + + @Override + public ResourceLocation getAnimationResource(ConcavenatorEntity animatable) { + return Constants.rl("animations/concavenator.animation.json"); + } + + @Override + public void setCustomAnimations(ConcavenatorEntity entity, long id, AnimationState state) { + super.setCustomAnimations(entity, id, state); + + String[] tailBones = { "Tail1", "Tail2", "Tail3", "Tail4", "Tail5", "Tail6" }; + int n = tailBones.length; + + if (appliedYaw == null || appliedYaw.length != n) { + appliedYaw = new float[n]; + appliedRoll = new float[n]; + } + + // 1) Clear previous offsets (from the last entity rendered with this model instance) + for (int i = 0; i < n; i++) { + if (appliedYaw[i] == 0.0f && appliedRoll[i] == 0.0f) continue; + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + if (appliedYaw[i] != 0.0f) bone.setRotY(bone.getRotY() - appliedYaw[i]); + if (appliedRoll[i] != 0.0f) bone.setRotZ(bone.getRotZ() - appliedRoll[i]); + appliedYaw[i] = 0.0f; + appliedRoll[i] = 0.0f; + } + + // 2) Interpolated sway for extra smoothness between ticks + float sway = entity.getTailSwayOffset(state.getPartialTick()); // [-1, 1] + + // Tuning + float maxYawDeg = 22.0f; // increased max sweep + float swayGain = 1.35f; // amplifies overall power + float rollFraction = 0.40f; // slightly stronger roll for heft + + float deg2rad = (float)Math.PI / 180f; + + // Direction: positive sway (left turn) -> tail swings right (negative yaw) + // Flip the sign here if the sway feels inverted + float baseYaw = sway * maxYawDeg * deg2rad; + float baseRoll = -baseYaw * rollFraction; + + float[] weights = { 1.00f, 0.78f, 0.58f, 0.42f, 0.30f, 0.22f }; + + for (int i = 0; i < n; i++) { + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + + float w = weights[i]; + float yaw = baseYaw * w; + float roll = baseRoll * w; + + // OVERRIDE animations on Y/Z only: keep the model's predefined X bend intact + // Do NOT reset rotX here, so the upward bend stays + bone.setRotY(yaw); + bone.setRotZ(roll); + + appliedYaw[i] = yaw; + appliedRoll[i] = roll; + } + + GeoBone head = (GeoBone) getAnimationProcessor().getBone("Neck1"); + + if (head != null) { + var entityData = state.getData(DataTickets.ENTITY_MODEL_DATA); + float clampedYawDeg = Mth.clamp(entityData.netHeadYaw(), -20.0f, 20.0f); + head.setRotY(clampedYawDeg * Mth.DEG_TO_RAD); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/ConcavenatorRenderer.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/ConcavenatorRenderer.java new file mode 100644 index 0000000..5b093af --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/ConcavenatorRenderer.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.cmr.jurassicrevived.entity.custom.ConcavenatorEntity; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.util.Mth; +import software.bernie.geckolib.cache.object.BakedGeoModel; +import software.bernie.geckolib.renderer.GeoEntityRenderer; + + +public class ConcavenatorRenderer extends GeoEntityRenderer { + private final float animalScale = 1.2F; + public ConcavenatorRenderer(EntityRendererProvider.Context renderManager) { + super(renderManager, new ConcavenatorModel()); + } + + @Override + public void scaleModelForRender(float widthScale, float heightScale, PoseStack poseStack, ConcavenatorEntity animatable, BakedGeoModel model, boolean isReRender, float partialTick, int packedLight, int packedOverlay) { + poseStack.scale(animalScale, animalScale, animalScale); + if(animatable.isBaby()) { + float growthProgress = Mth.clamp((24000.0F + animatable.getSyncedAge()) / 24000.0F, 0.0F, 1.0F); + float scale = Mth.lerp(growthProgress, 0.2F, 1.0F); + poseStack.scale(scale, scale, scale); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/ConcavenatorVariant.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/ConcavenatorVariant.java new file mode 100644 index 0000000..27c153d --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/ConcavenatorVariant.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import java.util.Arrays; +import java.util.Comparator; + +public enum ConcavenatorVariant { + MALE(0), + FEMALE(1); + + private static final ConcavenatorVariant[] BY_ID = Arrays.stream(values()).sorted( + Comparator.comparingInt(ConcavenatorVariant::getId)).toArray(ConcavenatorVariant[]::new); + + private final int id; + + ConcavenatorVariant(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static ConcavenatorVariant byId(int id) { + return BY_ID[id % BY_ID.length]; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/CorythosaurusModel.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/CorythosaurusModel.java new file mode 100644 index 0000000..525cbd3 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/CorythosaurusModel.java @@ -0,0 +1,112 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.google.common.collect.Maps; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.entity.custom.CorythosaurusEntity; +import net.minecraft.Util; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animation.AnimationState; +/*?} else {*/ +/*import software.bernie.geckolib.animation.AnimationState; + *//*?}*/ +import software.bernie.geckolib.cache.object.GeoBone; +import software.bernie.geckolib.constant.DataTickets; +import software.bernie.geckolib.model.GeoModel; + +import java.util.Map; + +public class CorythosaurusModel extends GeoModel { + + private static final Map LOCATION_BY_VARIANT = + Util.make(Maps.newEnumMap(CorythosaurusVariant.class), map -> { + map.put(CorythosaurusVariant.MALE, Constants.rl("textures/entity/corythosaurus.png")); + map.put(CorythosaurusVariant.FEMALE, Constants.rl("textures/entity/corythosaurus_female.png")); + }); + + // Model-local "currently applied" offsets; cleared before each entity render + private float[] appliedYaw = null; + private float[] appliedRoll = null; + + @Override + public ResourceLocation getModelResource(CorythosaurusEntity animatable) { + return Constants.rl("geo/corythosaurus.geo.json"); + } + + @Override + public ResourceLocation getTextureResource(CorythosaurusEntity animatable) { + return LOCATION_BY_VARIANT.get(animatable.getVariant()); + } + + @Override + public ResourceLocation getAnimationResource(CorythosaurusEntity animatable) { + return Constants.rl("animations/corythosaurus.animation.json"); + } + + @Override + public void setCustomAnimations(CorythosaurusEntity entity, long id, AnimationState state) { + super.setCustomAnimations(entity, id, state); + + String[] tailBones = { "Tail1", "Tail2", "Tail3", "Tail4", "Tail5", "Tail6" }; + int n = tailBones.length; + + if (appliedYaw == null || appliedYaw.length != n) { + appliedYaw = new float[n]; + appliedRoll = new float[n]; + } + + // 1) Clear previous offsets (from the last entity rendered with this model instance) + for (int i = 0; i < n; i++) { + if (appliedYaw[i] == 0.0f && appliedRoll[i] == 0.0f) continue; + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + if (appliedYaw[i] != 0.0f) bone.setRotY(bone.getRotY() - appliedYaw[i]); + if (appliedRoll[i] != 0.0f) bone.setRotZ(bone.getRotZ() - appliedRoll[i]); + appliedYaw[i] = 0.0f; + appliedRoll[i] = 0.0f; + } + + // 2) Interpolated sway for extra smoothness between ticks + float sway = entity.getTailSwayOffset(state.getPartialTick()); // [-1, 1] + + // Tuning + float maxYawDeg = 22.0f; // increased max sweep + float swayGain = 1.35f; // amplifies overall power + float rollFraction = 0.40f; // slightly stronger roll for heft + + float deg2rad = (float)Math.PI / 180f; + + // Direction: positive sway (left turn) -> tail swings right (negative yaw) + // Flip the sign here if the sway feels inverted + float baseYaw = sway * maxYawDeg * deg2rad; + float baseRoll = -baseYaw * rollFraction; + + float[] weights = { 1.00f, 0.78f, 0.58f, 0.42f, 0.30f, 0.22f }; + + for (int i = 0; i < n; i++) { + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + + float w = weights[i]; + float yaw = baseYaw * w; + float roll = baseRoll * w; + + // OVERRIDE animations on Y/Z only: keep the model's predefined X bend intact + // Do NOT reset rotX here, so the upward bend stays + bone.setRotY(yaw); + bone.setRotZ(roll); + + appliedYaw[i] = yaw; + appliedRoll[i] = roll; + } + + GeoBone head = (GeoBone) getAnimationProcessor().getBone("Neck"); + + if (head != null) { + var entityData = state.getData(DataTickets.ENTITY_MODEL_DATA); + float clampedYawDeg = Mth.clamp(entityData.netHeadYaw(), -30.0f, 30.0f); + head.setRotY(clampedYawDeg * Mth.DEG_TO_RAD); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/CorythosaurusRenderer.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/CorythosaurusRenderer.java new file mode 100644 index 0000000..f40427f --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/CorythosaurusRenderer.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.cmr.jurassicrevived.entity.custom.CorythosaurusEntity; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.util.Mth; +import software.bernie.geckolib.cache.object.BakedGeoModel; +import software.bernie.geckolib.renderer.GeoEntityRenderer; + + +public class CorythosaurusRenderer extends GeoEntityRenderer { + private final float animalScale = 1.25F; + public CorythosaurusRenderer(EntityRendererProvider.Context renderManager) { + super(renderManager, new CorythosaurusModel()); + } + + @Override + public void scaleModelForRender(float widthScale, float heightScale, PoseStack poseStack, CorythosaurusEntity animatable, BakedGeoModel model, boolean isReRender, float partialTick, int packedLight, int packedOverlay) { + poseStack.scale(animalScale, animalScale, animalScale); + if(animatable.isBaby()) { + float growthProgress = Mth.clamp((24000.0F + animatable.getSyncedAge()) / 24000.0F, 0.0F, 1.0F); + float scale = Mth.lerp(growthProgress, 0.2F, 1.0F); + poseStack.scale(scale, scale, scale); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/CorythosaurusVariant.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/CorythosaurusVariant.java new file mode 100644 index 0000000..ffea718 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/CorythosaurusVariant.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import java.util.Arrays; +import java.util.Comparator; + +public enum CorythosaurusVariant { + MALE(0), + FEMALE(1); + + private static final CorythosaurusVariant[] BY_ID = Arrays.stream(values()).sorted( + Comparator.comparingInt(CorythosaurusVariant::getId)).toArray(CorythosaurusVariant[]::new); + + private final int id; + + CorythosaurusVariant(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static CorythosaurusVariant byId(int id) { + return BY_ID[id % BY_ID.length]; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/DeinonychusModel.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/DeinonychusModel.java new file mode 100644 index 0000000..599d5dc --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/DeinonychusModel.java @@ -0,0 +1,112 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.google.common.collect.Maps; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.entity.custom.DeinonychusEntity; +import net.minecraft.Util; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animation.AnimationState; +/*?} else {*/ +/*import software.bernie.geckolib.animation.AnimationState; + *//*?}*/ +import software.bernie.geckolib.cache.object.GeoBone; +import software.bernie.geckolib.constant.DataTickets; +import software.bernie.geckolib.model.GeoModel; + +import java.util.Map; + +public class DeinonychusModel extends GeoModel { + + private static final Map LOCATION_BY_VARIANT = + Util.make(Maps.newEnumMap(DeinonychusVariant.class), map -> { + map.put(DeinonychusVariant.MALE, Constants.rl("textures/entity/deinonychus.png")); + map.put(DeinonychusVariant.FEMALE, Constants.rl("textures/entity/deinonychus_female.png")); + }); + + // Model-local "currently applied" offsets; cleared before each entity render + private float[] appliedYaw = null; + private float[] appliedRoll = null; + + @Override + public ResourceLocation getModelResource(DeinonychusEntity animatable) { + return Constants.rl("geo/deinonychus.geo.json"); + } + + @Override + public ResourceLocation getTextureResource(DeinonychusEntity animatable) { + return LOCATION_BY_VARIANT.get(animatable.getVariant()); + } + + @Override + public ResourceLocation getAnimationResource(DeinonychusEntity animatable) { + return Constants.rl("animations/deinonychus.animation.json"); + } + + @Override + public void setCustomAnimations(DeinonychusEntity entity, long id, AnimationState state) { + super.setCustomAnimations(entity, id, state); + + String[] tailBones = { "tail1", "tail2", "tail3", "tail4", "tail5", "tail6" }; + int n = tailBones.length; + + if (appliedYaw == null || appliedYaw.length != n) { + appliedYaw = new float[n]; + appliedRoll = new float[n]; + } + + // 1) Clear previous offsets (from the last entity rendered with this model instance) + for (int i = 0; i < n; i++) { + if (appliedYaw[i] == 0.0f && appliedRoll[i] == 0.0f) continue; + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + if (appliedYaw[i] != 0.0f) bone.setRotY(bone.getRotY() - appliedYaw[i]); + if (appliedRoll[i] != 0.0f) bone.setRotZ(bone.getRotZ() - appliedRoll[i]); + appliedYaw[i] = 0.0f; + appliedRoll[i] = 0.0f; + } + + // 2) Interpolated sway for extra smoothness between ticks + float sway = entity.getTailSwayOffset(state.getPartialTick()); // [-1, 1] + + // Tuning + float maxYawDeg = 22.0f; // increased max sweep + float swayGain = 1.35f; // amplifies overall power + float rollFraction = 0.40f; // slightly stronger roll for heft + + float deg2rad = (float)Math.PI / 180f; + + // Direction: positive sway (left turn) -> tail swings right (negative yaw) + // Flip the sign here if the sway feels inverted + float baseYaw = sway * maxYawDeg * deg2rad; + float baseRoll = -baseYaw * rollFraction; + + float[] weights = { 1.00f, 0.78f, 0.58f, 0.42f, 0.30f, 0.22f }; + + for (int i = 0; i < n; i++) { + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + + float w = weights[i]; + float yaw = baseYaw * w; + float roll = baseRoll * w; + + // OVERRIDE animations on Y/Z only: keep the model's predefined X bend intact + // Do NOT reset rotX here, so the upward bend stays + bone.setRotY(yaw); + bone.setRotZ(roll); + + appliedYaw[i] = yaw; + appliedRoll[i] = roll; + } + + GeoBone head = (GeoBone) getAnimationProcessor().getBone("body1"); + + if (head != null) { + var entityData = state.getData(DataTickets.ENTITY_MODEL_DATA); + float clampedYawDeg = Mth.clamp(entityData.netHeadYaw(), -20.0f, 20.0f); + head.setRotY(clampedYawDeg * Mth.DEG_TO_RAD); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/DeinonychusRenderer.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/DeinonychusRenderer.java new file mode 100644 index 0000000..417476b --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/DeinonychusRenderer.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.cmr.jurassicrevived.entity.custom.DeinonychusEntity; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.util.Mth; +import software.bernie.geckolib.cache.object.BakedGeoModel; +import software.bernie.geckolib.renderer.GeoEntityRenderer; + + +public class DeinonychusRenderer extends GeoEntityRenderer { + private final float animalScale = 1.0F; + public DeinonychusRenderer(EntityRendererProvider.Context renderManager) { + super(renderManager, new DeinonychusModel()); + } + + @Override + public void scaleModelForRender(float widthScale, float heightScale, PoseStack poseStack, DeinonychusEntity animatable, BakedGeoModel model, boolean isReRender, float partialTick, int packedLight, int packedOverlay) { + poseStack.scale(animalScale, animalScale, animalScale); + if(animatable.isBaby()) { + float growthProgress = Mth.clamp((24000.0F + animatable.getSyncedAge()) / 24000.0F, 0.0F, 1.0F); + float scale = Mth.lerp(growthProgress, 0.2F, 1.0F); + poseStack.scale(scale, scale, scale); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/DeinonychusVariant.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/DeinonychusVariant.java new file mode 100644 index 0000000..22d9f06 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/DeinonychusVariant.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import java.util.Arrays; +import java.util.Comparator; + +public enum DeinonychusVariant { + MALE(0), + FEMALE(1); + + private static final DeinonychusVariant[] BY_ID = Arrays.stream(values()).sorted( + Comparator.comparingInt(DeinonychusVariant::getId)).toArray(DeinonychusVariant[]::new); + + private final int id; + + DeinonychusVariant(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static DeinonychusVariant byId(int id) { + return BY_ID[id % BY_ID.length]; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/DilophosaurusModel.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/DilophosaurusModel.java new file mode 100644 index 0000000..e84a928 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/DilophosaurusModel.java @@ -0,0 +1,112 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.google.common.collect.Maps; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.entity.custom.DilophosaurusEntity; +import net.minecraft.Util; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animation.AnimationState; +/*?} else {*/ +/*import software.bernie.geckolib.animation.AnimationState; +*//*?}*/ +import software.bernie.geckolib.cache.object.GeoBone; +import software.bernie.geckolib.constant.DataTickets; +import software.bernie.geckolib.model.GeoModel; + +import java.util.Map; + +public class DilophosaurusModel extends GeoModel { + + private static final Map LOCATION_BY_VARIANT = + Util.make(Maps.newEnumMap(DilophosaurusVariant.class), map -> { + map.put(DilophosaurusVariant.MALE, Constants.rl("textures/entity/dilophosaurus.png")); + map.put(DilophosaurusVariant.FEMALE, Constants.rl("textures/entity/dilophosaurus_female.png")); + }); + + // Model-local "currently applied" offsets; cleared before each entity render + private float[] appliedYaw = null; + private float[] appliedRoll = null; + + @Override + public ResourceLocation getModelResource(DilophosaurusEntity animatable) { + return Constants.rl("geo/dilophosaurus.geo.json"); + } + + @Override + public ResourceLocation getTextureResource(DilophosaurusEntity animatable) { + return LOCATION_BY_VARIANT.get(animatable.getVariant()); + } + + @Override + public ResourceLocation getAnimationResource(DilophosaurusEntity animatable) { + return Constants.rl("animations/dilophosaurus.animation.json"); + } + + @Override + public void setCustomAnimations(DilophosaurusEntity entity, long id, AnimationState state) { + super.setCustomAnimations(entity, id, state); + + String[] tailBones = { "TailBASE", "Tail2", "Tail3", "Tail4", "Tail5", "Tail6", "Tail7" }; + int n = tailBones.length; + + if (appliedYaw == null || appliedYaw.length != n) { + appliedYaw = new float[n]; + appliedRoll = new float[n]; + } + + // 1) Clear previous offsets (from the last entity rendered with this model instance) + for (int i = 0; i < n; i++) { + if (appliedYaw[i] == 0.0f && appliedRoll[i] == 0.0f) continue; + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + if (appliedYaw[i] != 0.0f) bone.setRotY(bone.getRotY() - appliedYaw[i]); + if (appliedRoll[i] != 0.0f) bone.setRotZ(bone.getRotZ() - appliedRoll[i]); + appliedYaw[i] = 0.0f; + appliedRoll[i] = 0.0f; + } + + // 2) Interpolated sway for extra smoothness between ticks + float sway = entity.getTailSwayOffset(state.getPartialTick()); // [-1, 1] + + // Tuning + float maxYawDeg = 22.0f; // increased max sweep + float swayGain = 1.35f; // amplifies overall power + float rollFraction = 0.40f; // slightly stronger roll for heft + + float deg2rad = (float)Math.PI / 180f; + + // Direction: positive sway (left turn) -> tail swings right (negative yaw) + // Flip the sign here if the sway feels inverted + float baseYaw = sway * maxYawDeg * deg2rad; + float baseRoll = -baseYaw * rollFraction; + + float[] weights = { 1.00f, 0.78f, 0.58f, 0.42f, 0.30f, 0.22f, 0.16f }; + + for (int i = 0; i < n; i++) { + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + + float w = weights[i]; + float yaw = baseYaw * w; + float roll = baseRoll * w; + + // OVERRIDE animations on Y/Z only: keep the model's predefined X bend intact + // Do NOT reset rotX here, so the upward bend stays + bone.setRotY(yaw); + bone.setRotZ(roll); + + appliedYaw[i] = yaw; + appliedRoll[i] = roll; + } + + GeoBone head = (GeoBone) getAnimationProcessor().getBone("NeckBase"); + + if (head != null) { + var entityData = state.getData(DataTickets.ENTITY_MODEL_DATA); + float clampedYawDeg = Mth.clamp(entityData.netHeadYaw(), -30.0f, 30.0f); + head.setRotY(clampedYawDeg * Mth.DEG_TO_RAD); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/DilophosaurusRenderer.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/DilophosaurusRenderer.java new file mode 100644 index 0000000..9c8c806 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/DilophosaurusRenderer.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.cmr.jurassicrevived.entity.custom.DilophosaurusEntity; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.util.Mth; +import software.bernie.geckolib.cache.object.BakedGeoModel; +import software.bernie.geckolib.renderer.GeoEntityRenderer; + + +public class DilophosaurusRenderer extends GeoEntityRenderer { + private final float animalScale = 1.0F; + public DilophosaurusRenderer(EntityRendererProvider.Context renderManager) { + super(renderManager, new DilophosaurusModel()); + } + + @Override + public void scaleModelForRender(float widthScale, float heightScale, PoseStack poseStack, DilophosaurusEntity animatable, BakedGeoModel model, boolean isReRender, float partialTick, int packedLight, int packedOverlay) { + poseStack.scale(animalScale, animalScale, animalScale); + if(animatable.isBaby()) { + float growthProgress = Mth.clamp((24000.0F + animatable.getSyncedAge()) / 24000.0F, 0.0F, 1.0F); + float scale = Mth.lerp(growthProgress, 0.2F, 1.0F); + poseStack.scale(scale, scale, scale); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/DilophosaurusVariant.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/DilophosaurusVariant.java new file mode 100644 index 0000000..8e03b8f --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/DilophosaurusVariant.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import java.util.Arrays; +import java.util.Comparator; + +public enum DilophosaurusVariant { + MALE(0), + FEMALE(1); + + private static final DilophosaurusVariant[] BY_ID = Arrays.stream(values()).sorted( + Comparator.comparingInt(DilophosaurusVariant::getId)).toArray(DilophosaurusVariant[]::new); + + private final int id; + + DilophosaurusVariant(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static DilophosaurusVariant byId(int id) { + return BY_ID[id % BY_ID.length]; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/DimorphodonModel.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/DimorphodonModel.java new file mode 100644 index 0000000..9f85cba --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/DimorphodonModel.java @@ -0,0 +1,112 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.google.common.collect.Maps; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.entity.custom.DimorphodonEntity; +import net.minecraft.Util; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animation.AnimationState; +/*?} else {*/ +/*import software.bernie.geckolib.animation.AnimationState; + *//*?}*/ +import software.bernie.geckolib.cache.object.GeoBone; +import software.bernie.geckolib.constant.DataTickets; +import software.bernie.geckolib.model.GeoModel; + +import java.util.Map; + +public class DimorphodonModel extends GeoModel { + + private static final Map LOCATION_BY_VARIANT = + Util.make(Maps.newEnumMap(DimorphodonVariant.class), map -> { + map.put(DimorphodonVariant.MALE, Constants.rl("textures/entity/dimorphodon.png")); + map.put(DimorphodonVariant.FEMALE, Constants.rl("textures/entity/dimorphodon_female.png")); + }); + + // Model-local "currently applied" offsets; cleared before each entity render + private float[] appliedYaw = null; + private float[] appliedRoll = null; + + @Override + public ResourceLocation getModelResource(DimorphodonEntity animatable) { + return Constants.rl("geo/dimorphodon.geo.json"); + } + + @Override + public ResourceLocation getTextureResource(DimorphodonEntity animatable) { + return LOCATION_BY_VARIANT.get(animatable.getVariant()); + } + + @Override + public ResourceLocation getAnimationResource(DimorphodonEntity animatable) { + return Constants.rl("animations/dimorphodon.animation.json"); + } + + @Override + public void setCustomAnimations(DimorphodonEntity entity, long id, AnimationState state) { + super.setCustomAnimations(entity, id, state); + + String[] tailBones = { "Tail1", "Tail2", "Tail3" }; + int n = tailBones.length; + + if (appliedYaw == null || appliedYaw.length != n) { + appliedYaw = new float[n]; + appliedRoll = new float[n]; + } + + // 1) Clear previous offsets (from the last entity rendered with this model instance) + for (int i = 0; i < n; i++) { + if (appliedYaw[i] == 0.0f && appliedRoll[i] == 0.0f) continue; + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + if (appliedYaw[i] != 0.0f) bone.setRotY(bone.getRotY() - appliedYaw[i]); + if (appliedRoll[i] != 0.0f) bone.setRotZ(bone.getRotZ() - appliedRoll[i]); + appliedYaw[i] = 0.0f; + appliedRoll[i] = 0.0f; + } + + // 2) Interpolated sway for extra smoothness between ticks + float sway = entity.getTailSwayOffset(state.getPartialTick()); // [-1, 1] + + // Tuning + float maxYawDeg = 22.0f; // increased max sweep + float swayGain = 1.35f; // amplifies overall power + float rollFraction = 0.40f; // slightly stronger roll for heft + + float deg2rad = (float)Math.PI / 180f; + + // Direction: positive sway (left turn) -> tail swings right (negative yaw) + // Flip the sign here if the sway feels inverted + float baseYaw = sway * maxYawDeg * deg2rad; + float baseRoll = -baseYaw * rollFraction; + + float[] weights = { 1.00f, 0.78f, 0.58f }; + + for (int i = 0; i < n; i++) { + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + + float w = weights[i]; + float yaw = baseYaw * w; + float roll = baseRoll * w; + + // OVERRIDE animations on Y/Z only: keep the model's predefined X bend intact + // Do NOT reset rotX here, so the upward bend stays + bone.setRotY(yaw); + bone.setRotZ(roll); + + appliedYaw[i] = yaw; + appliedRoll[i] = roll; + } + + GeoBone head = (GeoBone) getAnimationProcessor().getBone("Neck"); + + if (head != null) { + var entityData = state.getData(DataTickets.ENTITY_MODEL_DATA); + float clampedYawDeg = Mth.clamp(entityData.netHeadYaw(), -20.0f, 20.0f); + head.setRotY(clampedYawDeg * Mth.DEG_TO_RAD); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/DimorphodonRenderer.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/DimorphodonRenderer.java new file mode 100644 index 0000000..c668fe9 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/DimorphodonRenderer.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.cmr.jurassicrevived.entity.custom.DimorphodonEntity; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.util.Mth; +import software.bernie.geckolib.cache.object.BakedGeoModel; +import software.bernie.geckolib.renderer.GeoEntityRenderer; + + +public class DimorphodonRenderer extends GeoEntityRenderer { + private final float animalScale = 0.4F; + public DimorphodonRenderer(EntityRendererProvider.Context renderManager) { + super(renderManager, new DimorphodonModel()); + } + + @Override + public void scaleModelForRender(float widthScale, float heightScale, PoseStack poseStack, DimorphodonEntity animatable, BakedGeoModel model, boolean isReRender, float partialTick, int packedLight, int packedOverlay) { + poseStack.scale(animalScale, animalScale, animalScale); + if(animatable.isBaby()) { + float growthProgress = Mth.clamp((24000.0F + animatable.getSyncedAge()) / 24000.0F, 0.0F, 1.0F); + float scale = Mth.lerp(growthProgress, 0.2F, 1.0F); + poseStack.scale(scale, scale, scale); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/DimorphodonVariant.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/DimorphodonVariant.java new file mode 100644 index 0000000..e66ed72 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/DimorphodonVariant.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import java.util.Arrays; +import java.util.Comparator; + +public enum DimorphodonVariant { + MALE(0), + FEMALE(1); + + private static final DimorphodonVariant[] BY_ID = Arrays.stream(values()).sorted( + Comparator.comparingInt(DimorphodonVariant::getId)).toArray(DimorphodonVariant[]::new); + + private final int id; + + DimorphodonVariant(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static DimorphodonVariant byId(int id) { + return BY_ID[id % BY_ID.length]; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/DiplodocusModel.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/DiplodocusModel.java new file mode 100644 index 0000000..41af320 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/DiplodocusModel.java @@ -0,0 +1,112 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.google.common.collect.Maps; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.entity.custom.DiplodocusEntity; +import net.minecraft.Util; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animation.AnimationState; +/*?} else {*/ +/*import software.bernie.geckolib.animation.AnimationState; + *//*?}*/ +import software.bernie.geckolib.cache.object.GeoBone; +import software.bernie.geckolib.constant.DataTickets; +import software.bernie.geckolib.model.GeoModel; + +import java.util.Map; + +public class DiplodocusModel extends GeoModel { + + private static final Map LOCATION_BY_VARIANT = + Util.make(Maps.newEnumMap(DiplodocusVariant.class), map -> { + map.put(DiplodocusVariant.MALE, Constants.rl("textures/entity/diplodocus.png")); + map.put(DiplodocusVariant.FEMALE, Constants.rl("textures/entity/diplodocus_female.png")); + }); + + // Model-local "currently applied" offsets; cleared before each entity render + private float[] appliedYaw = null; + private float[] appliedRoll = null; + + @Override + public ResourceLocation getModelResource(DiplodocusEntity animatable) { + return Constants.rl("geo/diplodocus.geo.json"); + } + + @Override + public ResourceLocation getTextureResource(DiplodocusEntity animatable) { + return LOCATION_BY_VARIANT.get(animatable.getVariant()); + } + + @Override + public ResourceLocation getAnimationResource(DiplodocusEntity animatable) { + return Constants.rl("animations/diplodocus.animation.json"); + } + + @Override + public void setCustomAnimations(DiplodocusEntity entity, long id, AnimationState state) { + super.setCustomAnimations(entity, id, state); + + String[] tailBones = { "tail1", "tail2", "tail3", "tail4", "tail5", "tail6" }; + int n = tailBones.length; + + if (appliedYaw == null || appliedYaw.length != n) { + appliedYaw = new float[n]; + appliedRoll = new float[n]; + } + + // 1) Clear previous offsets (from the last entity rendered with this model instance) + for (int i = 0; i < n; i++) { + if (appliedYaw[i] == 0.0f && appliedRoll[i] == 0.0f) continue; + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + if (appliedYaw[i] != 0.0f) bone.setRotY(bone.getRotY() - appliedYaw[i]); + if (appliedRoll[i] != 0.0f) bone.setRotZ(bone.getRotZ() - appliedRoll[i]); + appliedYaw[i] = 0.0f; + appliedRoll[i] = 0.0f; + } + + // 2) Interpolated sway for extra smoothness between ticks + float sway = entity.getTailSwayOffset(state.getPartialTick()); // [-1, 1] + + // Tuning + float maxYawDeg = 22.0f; // increased max sweep + float swayGain = 1.35f; // amplifies overall power + float rollFraction = 0.40f; // slightly stronger roll for heft + + float deg2rad = (float)Math.PI / 180f; + + // Direction: positive sway (left turn) -> tail swings right (negative yaw) + // Flip the sign here if the sway feels inverted + float baseYaw = sway * maxYawDeg * deg2rad; + float baseRoll = -baseYaw * rollFraction; + + float[] weights = { 1.00f, 0.78f, 0.58f, 0.42f, 0.30f, 0.22f }; + + for (int i = 0; i < n; i++) { + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + + float w = weights[i]; + float yaw = baseYaw * w; + float roll = baseRoll * w; + + // OVERRIDE animations on Y/Z only: keep the model's predefined X bend intact + // Do NOT reset rotX here, so the upward bend stays + bone.setRotY(yaw); + bone.setRotZ(roll); + + appliedYaw[i] = yaw; + appliedRoll[i] = roll; + } + + GeoBone head = (GeoBone) getAnimationProcessor().getBone("Neck1"); + + if (head != null) { + var entityData = state.getData(DataTickets.ENTITY_MODEL_DATA); + float clampedYawDeg = Mth.clamp(entityData.netHeadYaw(), -10.0f, 10.0f); + head.setRotY(clampedYawDeg * Mth.DEG_TO_RAD); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/DiplodocusRenderer.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/DiplodocusRenderer.java new file mode 100644 index 0000000..2ad3865 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/DiplodocusRenderer.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.cmr.jurassicrevived.entity.custom.DiplodocusEntity; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.util.Mth; +import software.bernie.geckolib.cache.object.BakedGeoModel; +import software.bernie.geckolib.renderer.GeoEntityRenderer; + + +public class DiplodocusRenderer extends GeoEntityRenderer { + private final float animalScale = 2.5F; + public DiplodocusRenderer(EntityRendererProvider.Context renderManager) { + super(renderManager, new DiplodocusModel()); + } + + @Override + public void scaleModelForRender(float widthScale, float heightScale, PoseStack poseStack, DiplodocusEntity animatable, BakedGeoModel model, boolean isReRender, float partialTick, int packedLight, int packedOverlay) { + poseStack.scale(animalScale, animalScale, animalScale); + if(animatable.isBaby()) { + float growthProgress = Mth.clamp((24000.0F + animatable.getSyncedAge()) / 24000.0F, 0.0F, 1.0F); + float scale = Mth.lerp(growthProgress, 0.2F, 1.0F); + poseStack.scale(scale, scale, scale); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/DiplodocusVariant.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/DiplodocusVariant.java new file mode 100644 index 0000000..20aa51b --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/DiplodocusVariant.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import java.util.Arrays; +import java.util.Comparator; + +public enum DiplodocusVariant { + MALE(0), + FEMALE(1); + + private static final DiplodocusVariant[] BY_ID = Arrays.stream(values()).sorted( + Comparator.comparingInt(DiplodocusVariant::getId)).toArray(DiplodocusVariant[]::new); + + private final int id; + + DiplodocusVariant(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static DiplodocusVariant byId(int id) { + return BY_ID[id % BY_ID.length]; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/DistortusRexModel.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/DistortusRexModel.java new file mode 100644 index 0000000..ba03738 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/DistortusRexModel.java @@ -0,0 +1,112 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.google.common.collect.Maps; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.entity.custom.DistortusRexEntity; +import net.minecraft.Util; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animation.AnimationState; +/*?} else {*/ +/*import software.bernie.geckolib.animation.AnimationState; + *//*?}*/ +import software.bernie.geckolib.cache.object.GeoBone; +import software.bernie.geckolib.constant.DataTickets; +import software.bernie.geckolib.model.GeoModel; + +import java.util.Map; + +public class DistortusRexModel extends GeoModel { + + private static final Map LOCATION_BY_VARIANT = + Util.make(Maps.newEnumMap(DistortusRexVariant.class), map -> { + map.put(DistortusRexVariant.MALE, Constants.rl("textures/entity/distortus_rex.png")); + map.put(DistortusRexVariant.FEMALE, Constants.rl("textures/entity/distortus_rex_female.png")); + }); + + // Model-local "currently applied" offsets; cleared before each entity render + private float[] appliedYaw = null; + private float[] appliedRoll = null; + + @Override + public ResourceLocation getModelResource(DistortusRexEntity animatable) { + return Constants.rl("geo/distortus_rex.geo.json"); + } + + @Override + public ResourceLocation getTextureResource(DistortusRexEntity animatable) { + return LOCATION_BY_VARIANT.get(animatable.getVariant()); + } + + @Override + public ResourceLocation getAnimationResource(DistortusRexEntity animatable) { + return Constants.rl("animations/distortus_rex.animation.json"); + } + + @Override + public void setCustomAnimations(DistortusRexEntity entity, long id, AnimationState state) { + super.setCustomAnimations(entity, id, state); + + String[] tailBones = { "Tail1", "Tail2", "Tail3", "Tail4", "Tail5", "Tail6", "Tail7" }; + int n = tailBones.length; + + if (appliedYaw == null || appliedYaw.length != n) { + appliedYaw = new float[n]; + appliedRoll = new float[n]; + } + + // 1) Clear previous offsets (from the last entity rendered with this model instance) + for (int i = 0; i < n; i++) { + if (appliedYaw[i] == 0.0f && appliedRoll[i] == 0.0f) continue; + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + if (appliedYaw[i] != 0.0f) bone.setRotY(bone.getRotY() - appliedYaw[i]); + if (appliedRoll[i] != 0.0f) bone.setRotZ(bone.getRotZ() - appliedRoll[i]); + appliedYaw[i] = 0.0f; + appliedRoll[i] = 0.0f; + } + + // 2) Interpolated sway for extra smoothness between ticks + float sway = entity.getTailSwayOffset(state.getPartialTick()); // [-1, 1] + + // Tuning + float maxYawDeg = 22.0f; // increased max sweep + float swayGain = 1.35f; // amplifies overall power + float rollFraction = 0.40f; // slightly stronger roll for heft + + float deg2rad = (float)Math.PI / 180f; + + // Direction: positive sway (left turn) -> tail swings right (negative yaw) + // Flip the sign here if the sway feels inverted + float baseYaw = sway * maxYawDeg * deg2rad; + float baseRoll = -baseYaw * rollFraction; + + float[] weights = { 1.00f, 0.78f, 0.58f, 0.42f, 0.30f, 0.22f, 0.16f }; + + for (int i = 0; i < n; i++) { + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + + float w = weights[i]; + float yaw = baseYaw * w; + float roll = baseRoll * w; + + // OVERRIDE animations on Y/Z only: keep the model's predefined X bend intact + // Do NOT reset rotX here, so the upward bend stays + bone.setRotY(yaw); + bone.setRotZ(roll); + + appliedYaw[i] = yaw; + appliedRoll[i] = roll; + } + + GeoBone head = (GeoBone) getAnimationProcessor().getBone("Chest"); + + if (head != null) { + var entityData = state.getData(DataTickets.ENTITY_MODEL_DATA); + float clampedYawDeg = Mth.clamp(entityData.netHeadYaw(), -20.0f, 20.0f); + head.setRotY(clampedYawDeg * Mth.DEG_TO_RAD); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/DistortusRexRenderer.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/DistortusRexRenderer.java new file mode 100644 index 0000000..468a6ed --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/DistortusRexRenderer.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.cmr.jurassicrevived.entity.custom.DistortusRexEntity; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.util.Mth; +import software.bernie.geckolib.cache.object.BakedGeoModel; +import software.bernie.geckolib.renderer.GeoEntityRenderer; + + +public class DistortusRexRenderer extends GeoEntityRenderer { + private final float animalScale = 2.75F; + public DistortusRexRenderer(EntityRendererProvider.Context renderManager) { + super(renderManager, new DistortusRexModel()); + } + + @Override + public void scaleModelForRender(float widthScale, float heightScale, PoseStack poseStack, DistortusRexEntity animatable, BakedGeoModel model, boolean isReRender, float partialTick, int packedLight, int packedOverlay) { + poseStack.scale(animalScale, animalScale, animalScale); + if(animatable.isBaby()) { + float growthProgress = Mth.clamp((24000.0F + animatable.getSyncedAge()) / 24000.0F, 0.0F, 1.0F); + float scale = Mth.lerp(growthProgress, 0.2F, 1.0F); + poseStack.scale(scale, scale, scale); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/DistortusRexVariant.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/DistortusRexVariant.java new file mode 100644 index 0000000..d359f61 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/DistortusRexVariant.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import java.util.Arrays; +import java.util.Comparator; + +public enum DistortusRexVariant { + MALE(0), + FEMALE(1); + + private static final DistortusRexVariant[] BY_ID = Arrays.stream(values()).sorted( + Comparator.comparingInt(DistortusRexVariant::getId)).toArray(DistortusRexVariant[]::new); + + private final int id; + + DistortusRexVariant(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static DistortusRexVariant byId(int id) { + return BY_ID[id % BY_ID.length]; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/DryosaurusModel.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/DryosaurusModel.java new file mode 100644 index 0000000..835f2bc --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/DryosaurusModel.java @@ -0,0 +1,112 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.google.common.collect.Maps; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.entity.custom.DryosaurusEntity; +import net.minecraft.Util; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animation.AnimationState; +/*?} else {*/ +/*import software.bernie.geckolib.animation.AnimationState; + *//*?}*/ +import software.bernie.geckolib.cache.object.GeoBone; +import software.bernie.geckolib.constant.DataTickets; +import software.bernie.geckolib.model.GeoModel; + +import java.util.Map; + +public class DryosaurusModel extends GeoModel { + + private static final Map LOCATION_BY_VARIANT = + Util.make(Maps.newEnumMap(DryosaurusVariant.class), map -> { + map.put(DryosaurusVariant.MALE, Constants.rl("textures/entity/dryosaurus.png")); + map.put(DryosaurusVariant.FEMALE, Constants.rl("textures/entity/dryosaurus_female.png")); + }); + + // Model-local "currently applied" offsets; cleared before each entity render + private float[] appliedYaw = null; + private float[] appliedRoll = null; + + @Override + public ResourceLocation getModelResource(DryosaurusEntity animatable) { + return Constants.rl("geo/dryosaurus.geo.json"); + } + + @Override + public ResourceLocation getTextureResource(DryosaurusEntity animatable) { + return LOCATION_BY_VARIANT.get(animatable.getVariant()); + } + + @Override + public ResourceLocation getAnimationResource(DryosaurusEntity animatable) { + return Constants.rl("animations/dryosaurus.animation.json"); + } + + @Override + public void setCustomAnimations(DryosaurusEntity entity, long id, AnimationState state) { + super.setCustomAnimations(entity, id, state); + + String[] tailBones = { "Tail1", "Tail2", "Tail3", "Tail4", "Tail5", "Tail6", "Tail7" }; + int n = tailBones.length; + + if (appliedYaw == null || appliedYaw.length != n) { + appliedYaw = new float[n]; + appliedRoll = new float[n]; + } + + // 1) Clear previous offsets (from the last entity rendered with this model instance) + for (int i = 0; i < n; i++) { + if (appliedYaw[i] == 0.0f && appliedRoll[i] == 0.0f) continue; + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + if (appliedYaw[i] != 0.0f) bone.setRotY(bone.getRotY() - appliedYaw[i]); + if (appliedRoll[i] != 0.0f) bone.setRotZ(bone.getRotZ() - appliedRoll[i]); + appliedYaw[i] = 0.0f; + appliedRoll[i] = 0.0f; + } + + // 2) Interpolated sway for extra smoothness between ticks + float sway = entity.getTailSwayOffset(state.getPartialTick()); // [-1, 1] + + // Tuning + float maxYawDeg = 22.0f; // increased max sweep + float swayGain = 1.35f; // amplifies overall power + float rollFraction = 0.40f; // slightly stronger roll for heft + + float deg2rad = (float)Math.PI / 180f; + + // Direction: positive sway (left turn) -> tail swings right (negative yaw) + // Flip the sign here if the sway feels inverted + float baseYaw = sway * maxYawDeg * deg2rad; + float baseRoll = -baseYaw * rollFraction; + + float[] weights = { 1.00f, 0.78f, 0.58f, 0.42f, 0.30f, 0.22f, 0.16f }; + + for (int i = 0; i < n; i++) { + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + + float w = weights[i]; + float yaw = baseYaw * w; + float roll = baseRoll * w; + + // OVERRIDE animations on Y/Z only: keep the model's predefined X bend intact + // Do NOT reset rotX here, so the upward bend stays + bone.setRotY(yaw); + bone.setRotZ(roll); + + appliedYaw[i] = yaw; + appliedRoll[i] = roll; + } + + GeoBone head = (GeoBone) getAnimationProcessor().getBone("Body4"); + + if (head != null) { + var entityData = state.getData(DataTickets.ENTITY_MODEL_DATA); + float clampedYawDeg = Mth.clamp(entityData.netHeadYaw(), -30.0f, 30.0f); + head.setRotY(clampedYawDeg * Mth.DEG_TO_RAD); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/DryosaurusRenderer.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/DryosaurusRenderer.java new file mode 100644 index 0000000..ec680f7 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/DryosaurusRenderer.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.cmr.jurassicrevived.entity.custom.DryosaurusEntity; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.util.Mth; +import software.bernie.geckolib.cache.object.BakedGeoModel; +import software.bernie.geckolib.renderer.GeoEntityRenderer; + + +public class DryosaurusRenderer extends GeoEntityRenderer { + private final float animalScale = 0.35F; + public DryosaurusRenderer(EntityRendererProvider.Context renderManager) { + super(renderManager, new DryosaurusModel()); + } + + @Override + public void scaleModelForRender(float widthScale, float heightScale, PoseStack poseStack, DryosaurusEntity animatable, BakedGeoModel model, boolean isReRender, float partialTick, int packedLight, int packedOverlay) { + poseStack.scale(animalScale, animalScale, animalScale); + if(animatable.isBaby()) { + float growthProgress = Mth.clamp((24000.0F + animatable.getSyncedAge()) / 24000.0F, 0.0F, 1.0F); + float scale = Mth.lerp(growthProgress, 0.2F, 1.0F); + poseStack.scale(scale, scale, scale); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/DryosaurusVariant.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/DryosaurusVariant.java new file mode 100644 index 0000000..ffdea91 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/DryosaurusVariant.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import java.util.Arrays; +import java.util.Comparator; + +public enum DryosaurusVariant { + MALE(0), + FEMALE(1); + + private static final DryosaurusVariant[] BY_ID = Arrays.stream(values()).sorted( + Comparator.comparingInt(DryosaurusVariant::getId)).toArray(DryosaurusVariant[]::new); + + private final int id; + + DryosaurusVariant(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static DryosaurusVariant byId(int id) { + return BY_ID[id % BY_ID.length]; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/EdmontosaurusModel.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/EdmontosaurusModel.java new file mode 100644 index 0000000..195e3fb --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/EdmontosaurusModel.java @@ -0,0 +1,112 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.google.common.collect.Maps; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.entity.custom.EdmontosaurusEntity; +import net.minecraft.Util; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animation.AnimationState; +/*?} else {*/ +/*import software.bernie.geckolib.animation.AnimationState; + *//*?}*/ +import software.bernie.geckolib.cache.object.GeoBone; +import software.bernie.geckolib.constant.DataTickets; +import software.bernie.geckolib.model.GeoModel; + +import java.util.Map; + +public class EdmontosaurusModel extends GeoModel { + + private static final Map LOCATION_BY_VARIANT = + Util.make(Maps.newEnumMap(EdmontosaurusVariant.class), map -> { + map.put(EdmontosaurusVariant.MALE, Constants.rl("textures/entity/edmontosaurus.png")); + map.put(EdmontosaurusVariant.FEMALE, Constants.rl("textures/entity/edmontosaurus_female.png")); + }); + + // Model-local "currently applied" offsets; cleared before each entity render + private float[] appliedYaw = null; + private float[] appliedRoll = null; + + @Override + public ResourceLocation getModelResource(EdmontosaurusEntity animatable) { + return Constants.rl("geo/edmontosaurus.geo.json"); + } + + @Override + public ResourceLocation getTextureResource(EdmontosaurusEntity animatable) { + return LOCATION_BY_VARIANT.get(animatable.getVariant()); + } + + @Override + public ResourceLocation getAnimationResource(EdmontosaurusEntity animatable) { + return Constants.rl("animations/edmontosaurus.animation.json"); + } + + @Override + public void setCustomAnimations(EdmontosaurusEntity entity, long id, AnimationState state) { + super.setCustomAnimations(entity, id, state); + + String[] tailBones = { "Tail1", "Tail2", "Tail3", "Tail4", "Tail5", "Tail6" }; + int n = tailBones.length; + + if (appliedYaw == null || appliedYaw.length != n) { + appliedYaw = new float[n]; + appliedRoll = new float[n]; + } + + // 1) Clear previous offsets (from the last entity rendered with this model instance) + for (int i = 0; i < n; i++) { + if (appliedYaw[i] == 0.0f && appliedRoll[i] == 0.0f) continue; + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + if (appliedYaw[i] != 0.0f) bone.setRotY(bone.getRotY() - appliedYaw[i]); + if (appliedRoll[i] != 0.0f) bone.setRotZ(bone.getRotZ() - appliedRoll[i]); + appliedYaw[i] = 0.0f; + appliedRoll[i] = 0.0f; + } + + // 2) Interpolated sway for extra smoothness between ticks + float sway = entity.getTailSwayOffset(state.getPartialTick()); // [-1, 1] + + // Tuning + float maxYawDeg = 22.0f; // increased max sweep + float swayGain = 1.35f; // amplifies overall power + float rollFraction = 0.40f; // slightly stronger roll for heft + + float deg2rad = (float)Math.PI / 180f; + + // Direction: positive sway (left turn) -> tail swings right (negative yaw) + // Flip the sign here if the sway feels inverted + float baseYaw = sway * maxYawDeg * deg2rad; + float baseRoll = -baseYaw * rollFraction; + + float[] weights = { 1.00f, 0.78f, 0.58f, 0.42f, 0.30f, 0.22f }; + + for (int i = 0; i < n; i++) { + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + + float w = weights[i]; + float yaw = baseYaw * w; + float roll = baseRoll * w; + + // OVERRIDE animations on Y/Z only: keep the model's predefined X bend intact + // Do NOT reset rotX here, so the upward bend stays + bone.setRotY(yaw); + bone.setRotZ(roll); + + appliedYaw[i] = yaw; + appliedRoll[i] = roll; + } + + GeoBone head = (GeoBone) getAnimationProcessor().getBone("Neck"); + + if (head != null) { + var entityData = state.getData(DataTickets.ENTITY_MODEL_DATA); + float clampedYawDeg = Mth.clamp(entityData.netHeadYaw(), -20.0f, 20.0f); + head.setRotY(clampedYawDeg * Mth.DEG_TO_RAD); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/EdmontosaurusRenderer.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/EdmontosaurusRenderer.java new file mode 100644 index 0000000..2328097 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/EdmontosaurusRenderer.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.cmr.jurassicrevived.entity.custom.EdmontosaurusEntity; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.util.Mth; +import software.bernie.geckolib.cache.object.BakedGeoModel; +import software.bernie.geckolib.renderer.GeoEntityRenderer; + + +public class EdmontosaurusRenderer extends GeoEntityRenderer { + private final float animalScale = 1.5F; + public EdmontosaurusRenderer(EntityRendererProvider.Context renderManager) { + super(renderManager, new EdmontosaurusModel()); + } + + @Override + public void scaleModelForRender(float widthScale, float heightScale, PoseStack poseStack, EdmontosaurusEntity animatable, BakedGeoModel model, boolean isReRender, float partialTick, int packedLight, int packedOverlay) { + poseStack.scale(animalScale, animalScale, animalScale); + if(animatable.isBaby()) { + float growthProgress = Mth.clamp((24000.0F + animatable.getSyncedAge()) / 24000.0F, 0.0F, 1.0F); + float scale = Mth.lerp(growthProgress, 0.2F, 1.0F); + poseStack.scale(scale, scale, scale); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/EdmontosaurusVariant.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/EdmontosaurusVariant.java new file mode 100644 index 0000000..e3eae18 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/EdmontosaurusVariant.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import java.util.Arrays; +import java.util.Comparator; + +public enum EdmontosaurusVariant { + MALE(0), + FEMALE(1); + + private static final EdmontosaurusVariant[] BY_ID = Arrays.stream(values()).sorted( + Comparator.comparingInt(EdmontosaurusVariant::getId)).toArray(EdmontosaurusVariant[]::new); + + private final int id; + + EdmontosaurusVariant(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static EdmontosaurusVariant byId(int id) { + return BY_ID[id % BY_ID.length]; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/FDuckModel.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/FDuckModel.java new file mode 100644 index 0000000..fcbf804 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/FDuckModel.java @@ -0,0 +1,112 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.google.common.collect.Maps; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.entity.custom.FDuckEntity; +import net.minecraft.Util; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animation.AnimationState; +/*?} else {*/ +/*import software.bernie.geckolib.animation.AnimationState; + *//*?}*/ +import software.bernie.geckolib.cache.object.GeoBone; +import software.bernie.geckolib.constant.DataTickets; +import software.bernie.geckolib.model.GeoModel; + +import java.util.Map; + +public class FDuckModel extends GeoModel { + + private static final Map LOCATION_BY_VARIANT = + Util.make(Maps.newEnumMap(FDuckVariant.class), map -> { + map.put(FDuckVariant.MALE, Constants.rl("textures/entity/fduck.png")); + map.put(FDuckVariant.FEMALE, Constants.rl("textures/entity/fduck_female.png")); + }); + + // Model-local "currently applied" offsets; cleared before each entity render + private float[] appliedYaw = null; + private float[] appliedRoll = null; + + @Override + public ResourceLocation getModelResource(FDuckEntity animatable) { + return Constants.rl("geo/fduck.geo.json"); + } + + @Override + public ResourceLocation getTextureResource(FDuckEntity animatable) { + return LOCATION_BY_VARIANT.get(animatable.getVariant()); + } + + @Override + public ResourceLocation getAnimationResource(FDuckEntity animatable) { + return Constants.rl("animations/fduck.animation.json"); + } + + @Override + public void setCustomAnimations(FDuckEntity entity, long id, AnimationState state) { + super.setCustomAnimations(entity, id, state); + + String[] tailBones = { "Tail1", "Tail2", "Tail3" }; + int n = tailBones.length; + + if (appliedYaw == null || appliedYaw.length != n) { + appliedYaw = new float[n]; + appliedRoll = new float[n]; + } + + // 1) Clear previous offsets (from the last entity rendered with this model instance) + for (int i = 0; i < n; i++) { + if (appliedYaw[i] == 0.0f && appliedRoll[i] == 0.0f) continue; + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + if (appliedYaw[i] != 0.0f) bone.setRotY(bone.getRotY() - appliedYaw[i]); + if (appliedRoll[i] != 0.0f) bone.setRotZ(bone.getRotZ() - appliedRoll[i]); + appliedYaw[i] = 0.0f; + appliedRoll[i] = 0.0f; + } + + // 2) Interpolated sway for extra smoothness between ticks + float sway = entity.getTailSwayOffset(state.getPartialTick()); // [-1, 1] + + // Tuning + float maxYawDeg = 22.0f; // increased max sweep + float swayGain = 1.35f; // amplifies overall power + float rollFraction = 0.40f; // slightly stronger roll for heft + + float deg2rad = (float)Math.PI / 180f; + + // Direction: positive sway (left turn) -> tail swings right (negative yaw) + // Flip the sign here if the sway feels inverted + float baseYaw = sway * maxYawDeg * deg2rad; + float baseRoll = -baseYaw * rollFraction; + + float[] weights = { 1.00f, 0.78f, 0.58f }; + + for (int i = 0; i < n; i++) { + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + + float w = weights[i]; + float yaw = baseYaw * w; + float roll = baseRoll * w; + + // OVERRIDE animations on Y/Z only: keep the model's predefined X bend intact + // Do NOT reset rotX here, so the upward bend stays + bone.setRotY(yaw); + bone.setRotZ(roll); + + appliedYaw[i] = yaw; + appliedRoll[i] = roll; + } + + GeoBone head = (GeoBone) getAnimationProcessor().getBone("Neck1"); + + if (head != null) { + var entityData = state.getData(DataTickets.ENTITY_MODEL_DATA); + float clampedYawDeg = Mth.clamp(entityData.netHeadYaw(), -30.0f, 30.0f); + head.setRotY(clampedYawDeg * Mth.DEG_TO_RAD); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/FDuckRenderer.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/FDuckRenderer.java new file mode 100644 index 0000000..9ce48b7 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/FDuckRenderer.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.cmr.jurassicrevived.entity.custom.FDuckEntity; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.util.Mth; +import software.bernie.geckolib.cache.object.BakedGeoModel; +import software.bernie.geckolib.renderer.GeoEntityRenderer; + + +public class FDuckRenderer extends GeoEntityRenderer { + private final float animalScale = 1.0F; + public FDuckRenderer(EntityRendererProvider.Context renderManager) { + super(renderManager, new FDuckModel()); + } + + @Override + public void scaleModelForRender(float widthScale, float heightScale, PoseStack poseStack, FDuckEntity animatable, BakedGeoModel model, boolean isReRender, float partialTick, int packedLight, int packedOverlay) { + poseStack.scale(animalScale, animalScale, animalScale); + if(animatable.isBaby()) { + float growthProgress = Mth.clamp((24000.0F + animatable.getSyncedAge()) / 24000.0F, 0.0F, 1.0F); + float scale = Mth.lerp(growthProgress, 0.2F, 1.0F); + poseStack.scale(scale, scale, scale); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/FDuckVariant.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/FDuckVariant.java new file mode 100644 index 0000000..6915740 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/FDuckVariant.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import java.util.Arrays; +import java.util.Comparator; + +public enum FDuckVariant { + MALE(0), + FEMALE(1); + + private static final FDuckVariant[] BY_ID = Arrays.stream(values()).sorted( + Comparator.comparingInt(FDuckVariant::getId)).toArray(FDuckVariant[]::new); + + private final int id; + + FDuckVariant(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static FDuckVariant byId(int id) { + return BY_ID[id % BY_ID.length]; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/GallimimusModel.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/GallimimusModel.java new file mode 100644 index 0000000..3901d41 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/GallimimusModel.java @@ -0,0 +1,112 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.google.common.collect.Maps; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.entity.custom.GallimimusEntity; +import net.minecraft.Util; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animation.AnimationState; +/*?} else {*/ +/*import software.bernie.geckolib.animation.AnimationState; + *//*?}*/ +import software.bernie.geckolib.cache.object.GeoBone; +import software.bernie.geckolib.constant.DataTickets; +import software.bernie.geckolib.model.GeoModel; + +import java.util.Map; + +public class GallimimusModel extends GeoModel { + + private static final Map LOCATION_BY_VARIANT = + Util.make(Maps.newEnumMap(GallimimusVariant.class), map -> { + map.put(GallimimusVariant.MALE, Constants.rl("textures/entity/gallimimus.png")); + map.put(GallimimusVariant.FEMALE, Constants.rl("textures/entity/gallimimus_female.png")); + }); + + // Model-local "currently applied" offsets; cleared before each entity render + private float[] appliedYaw = null; + private float[] appliedRoll = null; + + @Override + public ResourceLocation getModelResource(GallimimusEntity animatable) { + return Constants.rl("geo/gallimimus.geo.json"); + } + + @Override + public ResourceLocation getTextureResource(GallimimusEntity animatable) { + return LOCATION_BY_VARIANT.get(animatable.getVariant()); + } + + @Override + public ResourceLocation getAnimationResource(GallimimusEntity animatable) { + return Constants.rl("animations/gallimimus.animation.json"); + } + + @Override + public void setCustomAnimations(GallimimusEntity entity, long id, AnimationState state) { + super.setCustomAnimations(entity, id, state); + + String[] tailBones = { "Tailpart1", "Tailpart2", "Tailpart3", "Tailpart4", "Tailpart5", "Tailpart6" }; + int n = tailBones.length; + + if (appliedYaw == null || appliedYaw.length != n) { + appliedYaw = new float[n]; + appliedRoll = new float[n]; + } + + // 1) Clear previous offsets (from the last entity rendered with this model instance) + for (int i = 0; i < n; i++) { + if (appliedYaw[i] == 0.0f && appliedRoll[i] == 0.0f) continue; + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + if (appliedYaw[i] != 0.0f) bone.setRotY(bone.getRotY() - appliedYaw[i]); + if (appliedRoll[i] != 0.0f) bone.setRotZ(bone.getRotZ() - appliedRoll[i]); + appliedYaw[i] = 0.0f; + appliedRoll[i] = 0.0f; + } + + // 2) Interpolated sway for extra smoothness between ticks + float sway = entity.getTailSwayOffset(state.getPartialTick()); // [-1, 1] + + // Tuning + float maxYawDeg = 22.0f; // increased max sweep + float swayGain = 1.35f; // amplifies overall power + float rollFraction = 0.40f; // slightly stronger roll for heft + + float deg2rad = (float)Math.PI / 180f; + + // Direction: positive sway (left turn) -> tail swings right (negative yaw) + // Flip the sign here if the sway feels inverted + float baseYaw = sway * maxYawDeg * deg2rad; + float baseRoll = -baseYaw * rollFraction; + + float[] weights = { 1.00f, 0.78f, 0.58f, 0.42f, 0.30f, 0.22f }; + + for (int i = 0; i < n; i++) { + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + + float w = weights[i]; + float yaw = baseYaw * w; + float roll = baseRoll * w; + + // OVERRIDE animations on Y/Z only: keep the model's predefined X bend intact + // Do NOT reset rotX here, so the upward bend stays + bone.setRotY(yaw); + bone.setRotZ(roll); + + appliedYaw[i] = yaw; + appliedRoll[i] = roll; + } + + GeoBone head = (GeoBone) getAnimationProcessor().getBone("Neckpart1"); + + if (head != null) { + var entityData = state.getData(DataTickets.ENTITY_MODEL_DATA); + float clampedYawDeg = Mth.clamp(entityData.netHeadYaw(), -30.0f, 30.0f); + head.setRotY(clampedYawDeg * Mth.DEG_TO_RAD); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/GallimimusRenderer.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/GallimimusRenderer.java new file mode 100644 index 0000000..341f7e9 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/GallimimusRenderer.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.cmr.jurassicrevived.entity.custom.GallimimusEntity; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.util.Mth; +import software.bernie.geckolib.cache.object.BakedGeoModel; +import software.bernie.geckolib.renderer.GeoEntityRenderer; + + +public class GallimimusRenderer extends GeoEntityRenderer { + private final float animalScale = 1.0F; + public GallimimusRenderer(EntityRendererProvider.Context renderManager) { + super(renderManager, new GallimimusModel()); + } + + @Override + public void scaleModelForRender(float widthScale, float heightScale, PoseStack poseStack, GallimimusEntity animatable, BakedGeoModel model, boolean isReRender, float partialTick, int packedLight, int packedOverlay) { + poseStack.scale(animalScale, animalScale, animalScale); + if(animatable.isBaby()) { + float growthProgress = Mth.clamp((24000.0F + animatable.getSyncedAge()) / 24000.0F, 0.0F, 1.0F); + float scale = Mth.lerp(growthProgress, 0.2F, 1.0F); + poseStack.scale(scale, scale, scale); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/GallimimusVariant.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/GallimimusVariant.java new file mode 100644 index 0000000..4aa3a88 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/GallimimusVariant.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import java.util.Arrays; +import java.util.Comparator; + +public enum GallimimusVariant { + MALE(0), + FEMALE(1); + + private static final GallimimusVariant[] BY_ID = Arrays.stream(values()).sorted( + Comparator.comparingInt(GallimimusVariant::getId)).toArray(GallimimusVariant[]::new); + + private final int id; + + GallimimusVariant(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static GallimimusVariant byId(int id) { + return BY_ID[id % BY_ID.length]; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/GeosternbergiaModel.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/GeosternbergiaModel.java new file mode 100644 index 0000000..b033455 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/GeosternbergiaModel.java @@ -0,0 +1,114 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.google.common.collect.Maps; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.entity.custom.GeosternbergiaEntity; +import net.minecraft.Util; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animation.AnimationState; +/*?} else {*/ +/*import software.bernie.geckolib.animation.AnimationState; + *//*?}*/ +import software.bernie.geckolib.cache.object.GeoBone; +import software.bernie.geckolib.constant.DataTickets; +import software.bernie.geckolib.model.GeoModel; + +import java.util.Map; + +public class GeosternbergiaModel extends GeoModel { + + private static final Map LOCATION_BY_VARIANT = + Util.make(Maps.newEnumMap(GeosternbergiaVariant.class), map -> { + map.put(GeosternbergiaVariant.MALE, Constants.rl("textures/entity/geosternbergia.png")); + map.put(GeosternbergiaVariant.FEMALE, Constants.rl("textures/entity/geosternbergia_female.png")); + }); + + // Model-local "currently applied" offsets; cleared before each entity render + private float[] appliedYaw = null; + private float[] appliedRoll = null; + + @Override + public ResourceLocation getModelResource(GeosternbergiaEntity animatable) { + return Constants.rl("geo/geosternbergia.geo.json"); + } + + @Override + public ResourceLocation getTextureResource(GeosternbergiaEntity animatable) { + return LOCATION_BY_VARIANT.get(animatable.getVariant()); + } + + @Override + public ResourceLocation getAnimationResource(GeosternbergiaEntity animatable) { + //? if >1.20.1 { + /*return ResourceLocation.fromNamespaceAndPath(Constants.MOD_ID, "animations/geosternbergia.animation.json"); + *///?} else { + return new ResourceLocation(Constants.MOD_ID, "animations/geosternbergia.animation.json"); + //?} + } + public void setCustomAnimations(GeosternbergiaEntity entity, long id, AnimationState state) { + super.setCustomAnimations(entity, id, state); + + String[] tailBones = { "Tail1", "Tail2", "Tail3" }; + int n = tailBones.length; + + if (appliedYaw == null || appliedYaw.length != n) { + appliedYaw = new float[n]; + appliedRoll = new float[n]; + } + + // 1) Clear previous offsets (from the last entity rendered with this model instance) + for (int i = 0; i < n; i++) { + if (appliedYaw[i] == 0.0f && appliedRoll[i] == 0.0f) continue; + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + if (appliedYaw[i] != 0.0f) bone.setRotY(bone.getRotY() - appliedYaw[i]); + if (appliedRoll[i] != 0.0f) bone.setRotZ(bone.getRotZ() - appliedRoll[i]); + appliedYaw[i] = 0.0f; + appliedRoll[i] = 0.0f; + } + + // 2) Interpolated sway for extra smoothness between ticks + float sway = entity.getTailSwayOffset(state.getPartialTick()); // [-1, 1] + + // Tuning + float maxYawDeg = 22.0f; // increased max sweep + float swayGain = 1.35f; // amplifies overall power + float rollFraction = 0.40f; // slightly stronger roll for heft + + float deg2rad = (float)Math.PI / 180f; + + // Direction: positive sway (left turn) -> tail swings right (negative yaw) + // Flip the sign here if the sway feels inverted + float baseYaw = sway * maxYawDeg * deg2rad; + float baseRoll = -baseYaw * rollFraction; + + float[] weights = { 1.00f, 0.78f, 0.58f }; + + for (int i = 0; i < n; i++) { + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + + float w = weights[i]; + float yaw = baseYaw * w; + float roll = baseRoll * w; + + // OVERRIDE animations on Y/Z only: keep the model's predefined X bend intact + // Do NOT reset rotX here, so the upward bend stays + bone.setRotY(yaw); + bone.setRotZ(roll); + + appliedYaw[i] = yaw; + appliedRoll[i] = roll; + } + + GeoBone head = (GeoBone) getAnimationProcessor().getBone("Neck1"); + + if (head != null) { + var entityData = state.getData(DataTickets.ENTITY_MODEL_DATA); + float clampedYawDeg = Mth.clamp(entityData.netHeadYaw(), -20.0f, 20.0f); + head.setRotY(clampedYawDeg * Mth.DEG_TO_RAD); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/GeosternbergiaRenderer.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/GeosternbergiaRenderer.java new file mode 100644 index 0000000..384ec2a --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/GeosternbergiaRenderer.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.cmr.jurassicrevived.entity.custom.GeosternbergiaEntity; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.util.Mth; +import software.bernie.geckolib.cache.object.BakedGeoModel; +import software.bernie.geckolib.renderer.GeoEntityRenderer; + + +public class GeosternbergiaRenderer extends GeoEntityRenderer { + private final float animalScale = 1.0F; + public GeosternbergiaRenderer(EntityRendererProvider.Context renderManager) { + super(renderManager, new GeosternbergiaModel()); + } + + @Override + public void scaleModelForRender(float widthScale, float heightScale, PoseStack poseStack, GeosternbergiaEntity animatable, BakedGeoModel model, boolean isReRender, float partialTick, int packedLight, int packedOverlay) { + poseStack.scale(animalScale, animalScale, animalScale); + if(animatable.isBaby()) { + float growthProgress = Mth.clamp((24000.0F + animatable.getSyncedAge()) / 24000.0F, 0.0F, 1.0F); + float scale = Mth.lerp(growthProgress, 0.2F, 1.0F); + poseStack.scale(scale, scale, scale); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/GeosternbergiaVariant.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/GeosternbergiaVariant.java new file mode 100644 index 0000000..ece985c --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/GeosternbergiaVariant.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import java.util.Arrays; +import java.util.Comparator; + +public enum GeosternbergiaVariant { + MALE(0), + FEMALE(1); + + private static final GeosternbergiaVariant[] BY_ID = Arrays.stream(values()).sorted( + Comparator.comparingInt(GeosternbergiaVariant::getId)).toArray(GeosternbergiaVariant[]::new); + + private final int id; + + GeosternbergiaVariant(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static GeosternbergiaVariant byId(int id) { + return BY_ID[id % BY_ID.length]; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/GiganotosaurusModel.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/GiganotosaurusModel.java new file mode 100644 index 0000000..afd63ea --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/GiganotosaurusModel.java @@ -0,0 +1,112 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.google.common.collect.Maps; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.entity.custom.GiganotosaurusEntity; +import net.minecraft.Util; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animation.AnimationState; +/*?} else {*/ +/*import software.bernie.geckolib.animation.AnimationState; + *//*?}*/ +import software.bernie.geckolib.cache.object.GeoBone; +import software.bernie.geckolib.constant.DataTickets; +import software.bernie.geckolib.model.GeoModel; + +import java.util.Map; + +public class GiganotosaurusModel extends GeoModel { + + private static final Map LOCATION_BY_VARIANT = + Util.make(Maps.newEnumMap(GiganotosaurusVariant.class), map -> { + map.put(GiganotosaurusVariant.MALE, Constants.rl("textures/entity/giganotosaurus.png")); + map.put(GiganotosaurusVariant.FEMALE, Constants.rl("textures/entity/giganotosaurus_female.png")); + }); + + // Model-local "currently applied" offsets; cleared before each entity render + private float[] appliedYaw = null; + private float[] appliedRoll = null; + + @Override + public ResourceLocation getModelResource(GiganotosaurusEntity animatable) { + return Constants.rl("geo/giganotosaurus.geo.json"); + } + + @Override + public ResourceLocation getTextureResource(GiganotosaurusEntity animatable) { + return LOCATION_BY_VARIANT.get(animatable.getVariant()); + } + + @Override + public ResourceLocation getAnimationResource(GiganotosaurusEntity animatable) { + return Constants.rl("animations/giganotosaurus.animation.json"); + } + + @Override + public void setCustomAnimations(GiganotosaurusEntity entity, long id, AnimationState state) { + super.setCustomAnimations(entity, id, state); + + String[] tailBones = { "Tail1", "Tail2", "Tail3", "Tail4", "Tail5", "Tail6" }; + int n = tailBones.length; + + if (appliedYaw == null || appliedYaw.length != n) { + appliedYaw = new float[n]; + appliedRoll = new float[n]; + } + + // 1) Clear previous offsets (from the last entity rendered with this model instance) + for (int i = 0; i < n; i++) { + if (appliedYaw[i] == 0.0f && appliedRoll[i] == 0.0f) continue; + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + if (appliedYaw[i] != 0.0f) bone.setRotY(bone.getRotY() - appliedYaw[i]); + if (appliedRoll[i] != 0.0f) bone.setRotZ(bone.getRotZ() - appliedRoll[i]); + appliedYaw[i] = 0.0f; + appliedRoll[i] = 0.0f; + } + + // 2) Interpolated sway for extra smoothness between ticks + float sway = entity.getTailSwayOffset(state.getPartialTick()); // [-1, 1] + + // Tuning + float maxYawDeg = 22.0f; // increased max sweep + float swayGain = 1.35f; // amplifies overall power + float rollFraction = 0.40f; // slightly stronger roll for heft + + float deg2rad = (float)Math.PI / 180f; + + // Direction: positive sway (left turn) -> tail swings right (negative yaw) + // Flip the sign here if the sway feels inverted + float baseYaw = sway * maxYawDeg * deg2rad; + float baseRoll = -baseYaw * rollFraction; + + float[] weights = { 1.00f, 0.78f, 0.58f, 0.42f, 0.30f, 0.22f }; + + for (int i = 0; i < n; i++) { + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + + float w = weights[i]; + float yaw = baseYaw * w; + float roll = baseRoll * w; + + // OVERRIDE animations on Y/Z only: keep the model's predefined X bend intact + // Do NOT reset rotX here, so the upward bend stays + bone.setRotY(yaw); + bone.setRotZ(roll); + + appliedYaw[i] = yaw; + appliedRoll[i] = roll; + } + + GeoBone head = (GeoBone) getAnimationProcessor().getBone("Neck1"); + + if (head != null) { + var entityData = state.getData(DataTickets.ENTITY_MODEL_DATA); + float clampedYawDeg = Mth.clamp(entityData.netHeadYaw(), -20.0f, 20.0f); + head.setRotY(clampedYawDeg * Mth.DEG_TO_RAD); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/GiganotosaurusRenderer.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/GiganotosaurusRenderer.java new file mode 100644 index 0000000..5a003f2 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/GiganotosaurusRenderer.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.cmr.jurassicrevived.entity.custom.GiganotosaurusEntity; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.util.Mth; +import software.bernie.geckolib.cache.object.BakedGeoModel; +import software.bernie.geckolib.renderer.GeoEntityRenderer; + + +public class GiganotosaurusRenderer extends GeoEntityRenderer { + private final float animalScale = 2.1F; + public GiganotosaurusRenderer(EntityRendererProvider.Context renderManager) { + super(renderManager, new GiganotosaurusModel()); + } + + @Override + public void scaleModelForRender(float widthScale, float heightScale, PoseStack poseStack, GiganotosaurusEntity animatable, BakedGeoModel model, boolean isReRender, float partialTick, int packedLight, int packedOverlay) { + poseStack.scale(animalScale, animalScale, animalScale); + if(animatable.isBaby()) { + float growthProgress = Mth.clamp((24000.0F + animatable.getSyncedAge()) / 24000.0F, 0.0F, 1.0F); + float scale = Mth.lerp(growthProgress, 0.2F, 1.0F); + poseStack.scale(scale, scale, scale); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/GiganotosaurusVariant.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/GiganotosaurusVariant.java new file mode 100644 index 0000000..5b7dbfe --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/GiganotosaurusVariant.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import java.util.Arrays; +import java.util.Comparator; + +public enum GiganotosaurusVariant { + MALE(0), + FEMALE(1); + + private static final GiganotosaurusVariant[] BY_ID = Arrays.stream(values()).sorted( + Comparator.comparingInt(GiganotosaurusVariant::getId)).toArray(GiganotosaurusVariant[]::new); + + private final int id; + + GiganotosaurusVariant(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static GiganotosaurusVariant byId(int id) { + return BY_ID[id % BY_ID.length]; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/GuanlongModel.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/GuanlongModel.java new file mode 100644 index 0000000..31521d8 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/GuanlongModel.java @@ -0,0 +1,112 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.google.common.collect.Maps; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.entity.custom.GuanlongEntity; +import net.minecraft.Util; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animation.AnimationState; +/*?} else {*/ +/*import software.bernie.geckolib.animation.AnimationState; + *//*?}*/ +import software.bernie.geckolib.cache.object.GeoBone; +import software.bernie.geckolib.constant.DataTickets; +import software.bernie.geckolib.model.GeoModel; + +import java.util.Map; + +public class GuanlongModel extends GeoModel { + + private static final Map LOCATION_BY_VARIANT = + Util.make(Maps.newEnumMap(GuanlongVariant.class), map -> { + map.put(GuanlongVariant.MALE, Constants.rl("textures/entity/guanlong.png")); + map.put(GuanlongVariant.FEMALE, Constants.rl("textures/entity/guanlong_female.png")); + }); + + // Model-local "currently applied" offsets; cleared before each entity render + private float[] appliedYaw = null; + private float[] appliedRoll = null; + + @Override + public ResourceLocation getModelResource(GuanlongEntity animatable) { + return Constants.rl("geo/guanlong.geo.json"); + } + + @Override + public ResourceLocation getTextureResource(GuanlongEntity animatable) { + return LOCATION_BY_VARIANT.get(animatable.getVariant()); + } + + @Override + public ResourceLocation getAnimationResource(GuanlongEntity animatable) { + return Constants.rl("animations/guanlong.animation.json"); + } + + @Override + public void setCustomAnimations(GuanlongEntity entity, long id, AnimationState state) { + super.setCustomAnimations(entity, id, state); + + String[] tailBones = { "TailBASE", "Tail2", "Tail3", "Tail4", "Tail5", "Tail6" }; + int n = tailBones.length; + + if (appliedYaw == null || appliedYaw.length != n) { + appliedYaw = new float[n]; + appliedRoll = new float[n]; + } + + // 1) Clear previous offsets (from the last entity rendered with this model instance) + for (int i = 0; i < n; i++) { + if (appliedYaw[i] == 0.0f && appliedRoll[i] == 0.0f) continue; + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + if (appliedYaw[i] != 0.0f) bone.setRotY(bone.getRotY() - appliedYaw[i]); + if (appliedRoll[i] != 0.0f) bone.setRotZ(bone.getRotZ() - appliedRoll[i]); + appliedYaw[i] = 0.0f; + appliedRoll[i] = 0.0f; + } + + // 2) Interpolated sway for extra smoothness between ticks + float sway = entity.getTailSwayOffset(state.getPartialTick()); // [-1, 1] + + // Tuning + float maxYawDeg = 22.0f; // increased max sweep + float swayGain = 1.35f; // amplifies overall power + float rollFraction = 0.40f; // slightly stronger roll for heft + + float deg2rad = (float)Math.PI / 180f; + + // Direction: positive sway (left turn) -> tail swings right (negative yaw) + // Flip the sign here if the sway feels inverted + float baseYaw = sway * maxYawDeg * deg2rad; + float baseRoll = -baseYaw * rollFraction; + + float[] weights = { 1.00f, 0.78f, 0.58f, 0.42f, 0.30f, 0.22f }; + + for (int i = 0; i < n; i++) { + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + + float w = weights[i]; + float yaw = baseYaw * w; + float roll = baseRoll * w; + + // OVERRIDE animations on Y/Z only: keep the model's predefined X bend intact + // Do NOT reset rotX here, so the upward bend stays + bone.setRotY(yaw); + bone.setRotZ(roll); + + appliedYaw[i] = yaw; + appliedRoll[i] = roll; + } + + GeoBone head = (GeoBone) getAnimationProcessor().getBone("NeckBASE"); + + if (head != null) { + var entityData = state.getData(DataTickets.ENTITY_MODEL_DATA); + float clampedYawDeg = Mth.clamp(entityData.netHeadYaw(), -20.0f, 20.0f); + head.setRotY(clampedYawDeg * Mth.DEG_TO_RAD); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/GuanlongRenderer.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/GuanlongRenderer.java new file mode 100644 index 0000000..0eea2af --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/GuanlongRenderer.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.cmr.jurassicrevived.entity.custom.GuanlongEntity; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.util.Mth; +import software.bernie.geckolib.cache.object.BakedGeoModel; +import software.bernie.geckolib.renderer.GeoEntityRenderer; + + +public class GuanlongRenderer extends GeoEntityRenderer { + private final float animalScale = 0.75F; + public GuanlongRenderer(EntityRendererProvider.Context renderManager) { + super(renderManager, new GuanlongModel()); + } + + @Override + public void scaleModelForRender(float widthScale, float heightScale, PoseStack poseStack, GuanlongEntity animatable, BakedGeoModel model, boolean isReRender, float partialTick, int packedLight, int packedOverlay) { + poseStack.scale(animalScale, animalScale, animalScale); + if(animatable.isBaby()) { + float growthProgress = Mth.clamp((24000.0F + animatable.getSyncedAge()) / 24000.0F, 0.0F, 1.0F); + float scale = Mth.lerp(growthProgress, 0.2F, 1.0F); + poseStack.scale(scale, scale, scale); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/GuanlongVariant.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/GuanlongVariant.java new file mode 100644 index 0000000..35dfb84 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/GuanlongVariant.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import java.util.Arrays; +import java.util.Comparator; + +public enum GuanlongVariant { + MALE(0), + FEMALE(1); + + private static final GuanlongVariant[] BY_ID = Arrays.stream(values()).sorted( + Comparator.comparingInt(GuanlongVariant::getId)).toArray(GuanlongVariant[]::new); + + private final int id; + + GuanlongVariant(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static GuanlongVariant byId(int id) { + return BY_ID[id % BY_ID.length]; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/GuidracoModel.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/GuidracoModel.java new file mode 100644 index 0000000..b5a48c7 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/GuidracoModel.java @@ -0,0 +1,112 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.google.common.collect.Maps; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.entity.custom.GuidracoEntity; +import net.minecraft.Util; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animation.AnimationState; +/*?} else {*/ +/*import software.bernie.geckolib.animation.AnimationState; + *//*?}*/ +import software.bernie.geckolib.cache.object.GeoBone; +import software.bernie.geckolib.constant.DataTickets; +import software.bernie.geckolib.model.GeoModel; + +import java.util.Map; + +public class GuidracoModel extends GeoModel { + + private static final Map LOCATION_BY_VARIANT = + Util.make(Maps.newEnumMap(GuidracoVariant.class), map -> { + map.put(GuidracoVariant.MALE, Constants.rl("textures/entity/guidraco.png")); + map.put(GuidracoVariant.FEMALE, Constants.rl("textures/entity/guidraco_female.png")); + }); + + // Model-local "currently applied" offsets; cleared before each entity render + private float[] appliedYaw = null; + private float[] appliedRoll = null; + + @Override + public ResourceLocation getModelResource(GuidracoEntity animatable) { + return Constants.rl("geo/guidraco.geo.json"); + } + + @Override + public ResourceLocation getTextureResource(GuidracoEntity animatable) { + return LOCATION_BY_VARIANT.get(animatable.getVariant()); + } + + @Override + public ResourceLocation getAnimationResource(GuidracoEntity animatable) { + return Constants.rl("animations/guidraco.animation.json"); + } + + @Override + public void setCustomAnimations(GuidracoEntity entity, long id, AnimationState state) { + super.setCustomAnimations(entity, id, state); + + String[] tailBones = { "Tail1", "Tail2", "Tail3" }; + int n = tailBones.length; + + if (appliedYaw == null || appliedYaw.length != n) { + appliedYaw = new float[n]; + appliedRoll = new float[n]; + } + + // 1) Clear previous offsets (from the last entity rendered with this model instance) + for (int i = 0; i < n; i++) { + if (appliedYaw[i] == 0.0f && appliedRoll[i] == 0.0f) continue; + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + if (appliedYaw[i] != 0.0f) bone.setRotY(bone.getRotY() - appliedYaw[i]); + if (appliedRoll[i] != 0.0f) bone.setRotZ(bone.getRotZ() - appliedRoll[i]); + appliedYaw[i] = 0.0f; + appliedRoll[i] = 0.0f; + } + + // 2) Interpolated sway for extra smoothness between ticks + float sway = entity.getTailSwayOffset(state.getPartialTick()); // [-1, 1] + + // Tuning + float maxYawDeg = 22.0f; // increased max sweep + float swayGain = 1.35f; // amplifies overall power + float rollFraction = 0.40f; // slightly stronger roll for heft + + float deg2rad = (float)Math.PI / 180f; + + // Direction: positive sway (left turn) -> tail swings right (negative yaw) + // Flip the sign here if the sway feels inverted + float baseYaw = sway * maxYawDeg * deg2rad; + float baseRoll = -baseYaw * rollFraction; + + float[] weights = { 1.00f, 0.78f, 0.58f }; + + for (int i = 0; i < n; i++) { + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + + float w = weights[i]; + float yaw = baseYaw * w; + float roll = baseRoll * w; + + // OVERRIDE animations on Y/Z only: keep the model's predefined X bend intact + // Do NOT reset rotX here, so the upward bend stays + bone.setRotY(yaw); + bone.setRotZ(roll); + + appliedYaw[i] = yaw; + appliedRoll[i] = roll; + } + + GeoBone head = (GeoBone) getAnimationProcessor().getBone("Head"); + + if (head != null) { + var entityData = state.getData(DataTickets.ENTITY_MODEL_DATA); + float clampedYawDeg = Mth.clamp(entityData.netHeadYaw(), -20.0f, 20.0f); + head.setRotY(clampedYawDeg * Mth.DEG_TO_RAD); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/GuidracoRenderer.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/GuidracoRenderer.java new file mode 100644 index 0000000..a473e10 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/GuidracoRenderer.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.cmr.jurassicrevived.entity.custom.GuidracoEntity; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.util.Mth; +import software.bernie.geckolib.cache.object.BakedGeoModel; +import software.bernie.geckolib.renderer.GeoEntityRenderer; + + +public class GuidracoRenderer extends GeoEntityRenderer { + private final float animalScale = 0.55F; + public GuidracoRenderer(EntityRendererProvider.Context renderManager) { + super(renderManager, new GuidracoModel()); + } + + @Override + public void scaleModelForRender(float widthScale, float heightScale, PoseStack poseStack, GuidracoEntity animatable, BakedGeoModel model, boolean isReRender, float partialTick, int packedLight, int packedOverlay) { + poseStack.scale(animalScale, animalScale, animalScale); + if(animatable.isBaby()) { + float growthProgress = Mth.clamp((24000.0F + animatable.getSyncedAge()) / 24000.0F, 0.0F, 1.0F); + float scale = Mth.lerp(growthProgress, 0.2F, 1.0F); + poseStack.scale(scale, scale, scale); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/GuidracoVariant.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/GuidracoVariant.java new file mode 100644 index 0000000..27f17b9 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/GuidracoVariant.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import java.util.Arrays; +import java.util.Comparator; + +public enum GuidracoVariant { + MALE(0), + FEMALE(1); + + private static final GuidracoVariant[] BY_ID = Arrays.stream(values()).sorted( + Comparator.comparingInt(GuidracoVariant::getId)).toArray(GuidracoVariant[]::new); + + private final int id; + + GuidracoVariant(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static GuidracoVariant byId(int id) { + return BY_ID[id % BY_ID.length]; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/HadrosaurusModel.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/HadrosaurusModel.java new file mode 100644 index 0000000..76a4580 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/HadrosaurusModel.java @@ -0,0 +1,112 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.google.common.collect.Maps; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.entity.custom.HadrosaurusEntity; +import net.minecraft.Util; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animation.AnimationState; +/*?} else {*/ +/*import software.bernie.geckolib.animation.AnimationState; + *//*?}*/ +import software.bernie.geckolib.cache.object.GeoBone; +import software.bernie.geckolib.constant.DataTickets; +import software.bernie.geckolib.model.GeoModel; + +import java.util.Map; + +public class HadrosaurusModel extends GeoModel { + + private static final Map LOCATION_BY_VARIANT = + Util.make(Maps.newEnumMap(HadrosaurusVariant.class), map -> { + map.put(HadrosaurusVariant.MALE, Constants.rl("textures/entity/hadrosaurus.png")); + map.put(HadrosaurusVariant.FEMALE, Constants.rl("textures/entity/hadrosaurus_female.png")); + }); + + // Model-local "currently applied" offsets; cleared before each entity render + private float[] appliedYaw = null; + private float[] appliedRoll = null; + + @Override + public ResourceLocation getModelResource(HadrosaurusEntity animatable) { + return Constants.rl("geo/hadrosaurus.geo.json"); + } + + @Override + public ResourceLocation getTextureResource(HadrosaurusEntity animatable) { + return LOCATION_BY_VARIANT.get(animatable.getVariant()); + } + + @Override + public ResourceLocation getAnimationResource(HadrosaurusEntity animatable) { + return Constants.rl("animations/hadrosaurus.animation.json"); + } + + @Override + public void setCustomAnimations(HadrosaurusEntity entity, long id, AnimationState state) { + super.setCustomAnimations(entity, id, state); + + String[] tailBones = { "Tail1", "Tail2", "Tail3", "Tail4", "Tail5", "Tail6", "Tail7" }; + int n = tailBones.length; + + if (appliedYaw == null || appliedYaw.length != n) { + appliedYaw = new float[n]; + appliedRoll = new float[n]; + } + + // 1) Clear previous offsets (from the last entity rendered with this model instance) + for (int i = 0; i < n; i++) { + if (appliedYaw[i] == 0.0f && appliedRoll[i] == 0.0f) continue; + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + if (appliedYaw[i] != 0.0f) bone.setRotY(bone.getRotY() - appliedYaw[i]); + if (appliedRoll[i] != 0.0f) bone.setRotZ(bone.getRotZ() - appliedRoll[i]); + appliedYaw[i] = 0.0f; + appliedRoll[i] = 0.0f; + } + + // 2) Interpolated sway for extra smoothness between ticks + float sway = entity.getTailSwayOffset(state.getPartialTick()); // [-1, 1] + + // Tuning + float maxYawDeg = 22.0f; // increased max sweep + float swayGain = 1.35f; // amplifies overall power + float rollFraction = 0.40f; // slightly stronger roll for heft + + float deg2rad = (float)Math.PI / 180f; + + // Direction: positive sway (left turn) -> tail swings right (negative yaw) + // Flip the sign here if the sway feels inverted + float baseYaw = sway * maxYawDeg * deg2rad; + float baseRoll = -baseYaw * rollFraction; + + float[] weights = { 1.00f, 0.78f, 0.58f, 0.42f, 0.30f, 0.22f, 0.16f }; + + for (int i = 0; i < n; i++) { + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + + float w = weights[i]; + float yaw = baseYaw * w; + float roll = baseRoll * w; + + // OVERRIDE animations on Y/Z only: keep the model's predefined X bend intact + // Do NOT reset rotX here, so the upward bend stays + bone.setRotY(yaw); + bone.setRotZ(roll); + + appliedYaw[i] = yaw; + appliedRoll[i] = roll; + } + + GeoBone head = (GeoBone) getAnimationProcessor().getBone("Neck1"); + + if (head != null) { + var entityData = state.getData(DataTickets.ENTITY_MODEL_DATA); + float clampedYawDeg = Mth.clamp(entityData.netHeadYaw(), -30.0f, 30.0f); + head.setRotY(clampedYawDeg * Mth.DEG_TO_RAD); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/HadrosaurusRenderer.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/HadrosaurusRenderer.java new file mode 100644 index 0000000..e42d1ce --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/HadrosaurusRenderer.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.cmr.jurassicrevived.entity.custom.HadrosaurusEntity; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.util.Mth; +import software.bernie.geckolib.cache.object.BakedGeoModel; +import software.bernie.geckolib.renderer.GeoEntityRenderer; + + +public class HadrosaurusRenderer extends GeoEntityRenderer { + private final float animalScale = 1.4F; + public HadrosaurusRenderer(EntityRendererProvider.Context renderManager) { + super(renderManager, new HadrosaurusModel()); + } + + @Override + public void scaleModelForRender(float widthScale, float heightScale, PoseStack poseStack, HadrosaurusEntity animatable, BakedGeoModel model, boolean isReRender, float partialTick, int packedLight, int packedOverlay) { + poseStack.scale(animalScale, animalScale, animalScale); + if(animatable.isBaby()) { + float growthProgress = Mth.clamp((24000.0F + animatable.getSyncedAge()) / 24000.0F, 0.0F, 1.0F); + float scale = Mth.lerp(growthProgress, 0.2F, 1.0F); + poseStack.scale(scale, scale, scale); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/HadrosaurusVariant.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/HadrosaurusVariant.java new file mode 100644 index 0000000..d371f32 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/HadrosaurusVariant.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import java.util.Arrays; +import java.util.Comparator; + +public enum HadrosaurusVariant { + MALE(0), + FEMALE(1); + + private static final HadrosaurusVariant[] BY_ID = Arrays.stream(values()).sorted( + Comparator.comparingInt(HadrosaurusVariant::getId)).toArray(HadrosaurusVariant[]::new); + + private final int id; + + HadrosaurusVariant(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static HadrosaurusVariant byId(int id) { + return BY_ID[id % BY_ID.length]; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/HerrerasaurusModel.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/HerrerasaurusModel.java new file mode 100644 index 0000000..d7e2c32 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/HerrerasaurusModel.java @@ -0,0 +1,112 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.google.common.collect.Maps; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.entity.custom.HerrerasaurusEntity; +import net.minecraft.Util; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animation.AnimationState; +/*?} else {*/ +/*import software.bernie.geckolib.animation.AnimationState; + *//*?}*/ +import software.bernie.geckolib.cache.object.GeoBone; +import software.bernie.geckolib.constant.DataTickets; +import software.bernie.geckolib.model.GeoModel; + +import java.util.Map; + +public class HerrerasaurusModel extends GeoModel { + + private static final Map LOCATION_BY_VARIANT = + Util.make(Maps.newEnumMap(HerrerasaurusVariant.class), map -> { + map.put(HerrerasaurusVariant.MALE, Constants.rl("textures/entity/herrerasaurus.png")); + map.put(HerrerasaurusVariant.FEMALE, Constants.rl("textures/entity/herrerasaurus_female.png")); + }); + + // Model-local "currently applied" offsets; cleared before each entity render + private float[] appliedYaw = null; + private float[] appliedRoll = null; + + @Override + public ResourceLocation getModelResource(HerrerasaurusEntity animatable) { + return Constants.rl("geo/herrerasaurus.geo.json"); + } + + @Override + public ResourceLocation getTextureResource(HerrerasaurusEntity animatable) { + return LOCATION_BY_VARIANT.get(animatable.getVariant()); + } + + @Override + public ResourceLocation getAnimationResource(HerrerasaurusEntity animatable) { + return Constants.rl("animations/herrerasaurus.animation.json"); + } + + @Override + public void setCustomAnimations(HerrerasaurusEntity entity, long id, AnimationState state) { + super.setCustomAnimations(entity, id, state); + + String[] tailBones = { "Tail1", "Tail2", "Tail3", "Tail4", "Tail5", "Tail6" }; + int n = tailBones.length; + + if (appliedYaw == null || appliedYaw.length != n) { + appliedYaw = new float[n]; + appliedRoll = new float[n]; + } + + // 1) Clear previous offsets (from the last entity rendered with this model instance) + for (int i = 0; i < n; i++) { + if (appliedYaw[i] == 0.0f && appliedRoll[i] == 0.0f) continue; + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + if (appliedYaw[i] != 0.0f) bone.setRotY(bone.getRotY() - appliedYaw[i]); + if (appliedRoll[i] != 0.0f) bone.setRotZ(bone.getRotZ() - appliedRoll[i]); + appliedYaw[i] = 0.0f; + appliedRoll[i] = 0.0f; + } + + // 2) Interpolated sway for extra smoothness between ticks + float sway = entity.getTailSwayOffset(state.getPartialTick()); // [-1, 1] + + // Tuning + float maxYawDeg = 22.0f; // increased max sweep + float swayGain = 1.35f; // amplifies overall power + float rollFraction = 0.40f; // slightly stronger roll for heft + + float deg2rad = (float)Math.PI / 180f; + + // Direction: positive sway (left turn) -> tail swings right (negative yaw) + // Flip the sign here if the sway feels inverted + float baseYaw = sway * maxYawDeg * deg2rad; + float baseRoll = -baseYaw * rollFraction; + + float[] weights = { 1.00f, 0.78f, 0.58f, 0.42f, 0.30f, 0.22f }; + + for (int i = 0; i < n; i++) { + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + + float w = weights[i]; + float yaw = baseYaw * w; + float roll = baseRoll * w; + + // OVERRIDE animations on Y/Z only: keep the model's predefined X bend intact + // Do NOT reset rotX here, so the upward bend stays + bone.setRotY(yaw); + bone.setRotZ(roll); + + appliedYaw[i] = yaw; + appliedRoll[i] = roll; + } + + GeoBone head = (GeoBone) getAnimationProcessor().getBone("Neckunder1"); + + if (head != null) { + var entityData = state.getData(DataTickets.ENTITY_MODEL_DATA); + float clampedYawDeg = Mth.clamp(entityData.netHeadYaw(), -20.0f, 20.0f); + head.setRotY(clampedYawDeg * Mth.DEG_TO_RAD); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/HerrerasaurusRenderer.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/HerrerasaurusRenderer.java new file mode 100644 index 0000000..3094d53 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/HerrerasaurusRenderer.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.cmr.jurassicrevived.entity.custom.HerrerasaurusEntity; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.util.Mth; +import software.bernie.geckolib.cache.object.BakedGeoModel; +import software.bernie.geckolib.renderer.GeoEntityRenderer; + + +public class HerrerasaurusRenderer extends GeoEntityRenderer { + private final float animalScale = 1.095F; + public HerrerasaurusRenderer(EntityRendererProvider.Context renderManager) { + super(renderManager, new HerrerasaurusModel()); + } + + @Override + public void scaleModelForRender(float widthScale, float heightScale, PoseStack poseStack, HerrerasaurusEntity animatable, BakedGeoModel model, boolean isReRender, float partialTick, int packedLight, int packedOverlay) { + poseStack.scale(animalScale, animalScale, animalScale); + if(animatable.isBaby()) { + float growthProgress = Mth.clamp((24000.0F + animatable.getSyncedAge()) / 24000.0F, 0.0F, 1.0F); + float scale = Mth.lerp(growthProgress, 0.2F, 1.0F); + poseStack.scale(scale, scale, scale); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/HerrerasaurusVariant.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/HerrerasaurusVariant.java new file mode 100644 index 0000000..48103d9 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/HerrerasaurusVariant.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import java.util.Arrays; +import java.util.Comparator; + +public enum HerrerasaurusVariant { + MALE(0), + FEMALE(1); + + private static final HerrerasaurusVariant[] BY_ID = Arrays.stream(values()).sorted( + Comparator.comparingInt(HerrerasaurusVariant::getId)).toArray(HerrerasaurusVariant[]::new); + + private final int id; + + HerrerasaurusVariant(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static HerrerasaurusVariant byId(int id) { + return BY_ID[id % BY_ID.length]; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/HypsilophodonModel.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/HypsilophodonModel.java new file mode 100644 index 0000000..afa4d6e --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/HypsilophodonModel.java @@ -0,0 +1,112 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.google.common.collect.Maps; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.entity.custom.HypsilophodonEntity; +import net.minecraft.Util; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animation.AnimationState; +/*?} else {*/ +/*import software.bernie.geckolib.animation.AnimationState; + *//*?}*/ +import software.bernie.geckolib.cache.object.GeoBone; +import software.bernie.geckolib.constant.DataTickets; +import software.bernie.geckolib.model.GeoModel; + +import java.util.Map; + +public class HypsilophodonModel extends GeoModel { + + private static final Map LOCATION_BY_VARIANT = + Util.make(Maps.newEnumMap(HypsilophodonVariant.class), map -> { + map.put(HypsilophodonVariant.MALE, Constants.rl("textures/entity/hypsilophodon.png")); + map.put(HypsilophodonVariant.FEMALE, Constants.rl("textures/entity/hypsilophodon_female.png")); + }); + + // Model-local "currently applied" offsets; cleared before each entity render + private float[] appliedYaw = null; + private float[] appliedRoll = null; + + @Override + public ResourceLocation getModelResource(HypsilophodonEntity animatable) { + return Constants.rl("geo/hypsilophodon.geo.json"); + } + + @Override + public ResourceLocation getTextureResource(HypsilophodonEntity animatable) { + return LOCATION_BY_VARIANT.get(animatable.getVariant()); + } + + @Override + public ResourceLocation getAnimationResource(HypsilophodonEntity animatable) { + return Constants.rl("animations/hypsilophodon.animation.json"); + } + + @Override + public void setCustomAnimations(HypsilophodonEntity entity, long id, AnimationState state) { + super.setCustomAnimations(entity, id, state); + + String[] tailBones = { "TailBASE", "Tail2", "Tail3", "Tail4", "Tail5", "Tail6" }; + int n = tailBones.length; + + if (appliedYaw == null || appliedYaw.length != n) { + appliedYaw = new float[n]; + appliedRoll = new float[n]; + } + + // 1) Clear previous offsets (from the last entity rendered with this model instance) + for (int i = 0; i < n; i++) { + if (appliedYaw[i] == 0.0f && appliedRoll[i] == 0.0f) continue; + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + if (appliedYaw[i] != 0.0f) bone.setRotY(bone.getRotY() - appliedYaw[i]); + if (appliedRoll[i] != 0.0f) bone.setRotZ(bone.getRotZ() - appliedRoll[i]); + appliedYaw[i] = 0.0f; + appliedRoll[i] = 0.0f; + } + + // 2) Interpolated sway for extra smoothness between ticks + float sway = entity.getTailSwayOffset(state.getPartialTick()); // [-1, 1] + + // Tuning + float maxYawDeg = 22.0f; // increased max sweep + float swayGain = 1.35f; // amplifies overall power + float rollFraction = 0.40f; // slightly stronger roll for heft + + float deg2rad = (float)Math.PI / 180f; + + // Direction: positive sway (left turn) -> tail swings right (negative yaw) + // Flip the sign here if the sway feels inverted + float baseYaw = sway * maxYawDeg * deg2rad; + float baseRoll = -baseYaw * rollFraction; + + float[] weights = { 1.00f, 0.78f, 0.58f, 0.42f, 0.30f, 0.22f }; + + for (int i = 0; i < n; i++) { + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + + float w = weights[i]; + float yaw = baseYaw * w; + float roll = baseRoll * w; + + // OVERRIDE animations on Y/Z only: keep the model's predefined X bend intact + // Do NOT reset rotX here, so the upward bend stays + bone.setRotY(yaw); + bone.setRotZ(roll); + + appliedYaw[i] = yaw; + appliedRoll[i] = roll; + } + + GeoBone head = (GeoBone) getAnimationProcessor().getBone("NeckBASE"); + + if (head != null) { + var entityData = state.getData(DataTickets.ENTITY_MODEL_DATA); + float clampedYawDeg = Mth.clamp(entityData.netHeadYaw(), -30.0f, 30.0f); + head.setRotY(clampedYawDeg * Mth.DEG_TO_RAD); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/HypsilophodonRenderer.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/HypsilophodonRenderer.java new file mode 100644 index 0000000..d2d0f68 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/HypsilophodonRenderer.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.cmr.jurassicrevived.entity.custom.HypsilophodonEntity; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.util.Mth; +import software.bernie.geckolib.cache.object.BakedGeoModel; +import software.bernie.geckolib.renderer.GeoEntityRenderer; + + +public class HypsilophodonRenderer extends GeoEntityRenderer { + private final float animalScale = 0.6F; + public HypsilophodonRenderer(EntityRendererProvider.Context renderManager) { + super(renderManager, new HypsilophodonModel()); + } + + @Override + public void scaleModelForRender(float widthScale, float heightScale, PoseStack poseStack, HypsilophodonEntity animatable, BakedGeoModel model, boolean isReRender, float partialTick, int packedLight, int packedOverlay) { + poseStack.scale(animalScale, animalScale, animalScale); + if(animatable.isBaby()) { + float growthProgress = Mth.clamp((24000.0F + animatable.getSyncedAge()) / 24000.0F, 0.0F, 1.0F); + float scale = Mth.lerp(growthProgress, 0.2F, 1.0F); + poseStack.scale(scale, scale, scale); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/HypsilophodonVariant.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/HypsilophodonVariant.java new file mode 100644 index 0000000..c52f781 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/HypsilophodonVariant.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import java.util.Arrays; +import java.util.Comparator; + +public enum HypsilophodonVariant { + MALE(0), + FEMALE(1); + + private static final HypsilophodonVariant[] BY_ID = Arrays.stream(values()).sorted( + Comparator.comparingInt(HypsilophodonVariant::getId)).toArray(HypsilophodonVariant[]::new); + + private final int id; + + HypsilophodonVariant(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static HypsilophodonVariant byId(int id) { + return BY_ID[id % BY_ID.length]; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/IndominusRexModel.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/IndominusRexModel.java new file mode 100644 index 0000000..227bd4c --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/IndominusRexModel.java @@ -0,0 +1,112 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.google.common.collect.Maps; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.entity.custom.IndominusRexEntity; +import net.minecraft.Util; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animation.AnimationState; +/*?} else {*/ +/*import software.bernie.geckolib.animation.AnimationState; + *//*?}*/ +import software.bernie.geckolib.cache.object.GeoBone; +import software.bernie.geckolib.constant.DataTickets; +import software.bernie.geckolib.model.GeoModel; + +import java.util.Map; + +public class IndominusRexModel extends GeoModel { + + private static final Map LOCATION_BY_VARIANT = + Util.make(Maps.newEnumMap(IndominusRexVariant.class), map -> { + map.put(IndominusRexVariant.MALE, Constants.rl("textures/entity/indominus_rex.png")); + map.put(IndominusRexVariant.FEMALE, Constants.rl("textures/entity/indominus_rex_female.png")); + }); + + // Model-local "currently applied" offsets; cleared before each entity render + private float[] appliedYaw = null; + private float[] appliedRoll = null; + + @Override + public ResourceLocation getModelResource(IndominusRexEntity animatable) { + return Constants.rl("geo/indominus_rex.geo.json"); + } + + @Override + public ResourceLocation getTextureResource(IndominusRexEntity animatable) { + return LOCATION_BY_VARIANT.get(animatable.getVariant()); + } + + @Override + public ResourceLocation getAnimationResource(IndominusRexEntity animatable) { + return Constants.rl("animations/indominus_rex.animation.json"); + } + + @Override + public void setCustomAnimations(IndominusRexEntity entity, long id, AnimationState state) { + super.setCustomAnimations(entity, id, state); + + String[] tailBones = { "TailBASE", "Tail2", "Tail3", "Tail4", "Tail5", "Tail6", "Tail7", "Tail8" }; + int n = tailBones.length; + + if (appliedYaw == null || appliedYaw.length != n) { + appliedYaw = new float[n]; + appliedRoll = new float[n]; + } + + // 1) Clear previous offsets (from the last entity rendered with this model instance) + for (int i = 0; i < n; i++) { + if (appliedYaw[i] == 0.0f && appliedRoll[i] == 0.0f) continue; + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + if (appliedYaw[i] != 0.0f) bone.setRotY(bone.getRotY() - appliedYaw[i]); + if (appliedRoll[i] != 0.0f) bone.setRotZ(bone.getRotZ() - appliedRoll[i]); + appliedYaw[i] = 0.0f; + appliedRoll[i] = 0.0f; + } + + // 2) Interpolated sway for extra smoothness between ticks + float sway = entity.getTailSwayOffset(state.getPartialTick()); // [-1, 1] + + // Tuning + float maxYawDeg = 22.0f; // increased max sweep + float swayGain = 1.35f; // amplifies overall power + float rollFraction = 0.40f; // slightly stronger roll for heft + + float deg2rad = (float)Math.PI / 180f; + + // Direction: positive sway (left turn) -> tail swings right (negative yaw) + // Flip the sign here if the sway feels inverted + float baseYaw = sway * maxYawDeg * deg2rad; + float baseRoll = -baseYaw * rollFraction; + + float[] weights = { 1.00f, 0.78f, 0.58f, 0.42f, 0.30f, 0.22f, 0.16f, 0.12f }; + + for (int i = 0; i < n; i++) { + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + + float w = weights[i]; + float yaw = baseYaw * w; + float roll = baseRoll * w; + + // OVERRIDE animations on Y/Z only: keep the model's predefined X bend intact + // Do NOT reset rotX here, so the upward bend stays + bone.setRotY(yaw); + bone.setRotZ(roll); + + appliedYaw[i] = yaw; + appliedRoll[i] = roll; + } + + GeoBone head = (GeoBone) getAnimationProcessor().getBone("NeckBASE"); + + if (head != null) { + var entityData = state.getData(DataTickets.ENTITY_MODEL_DATA); + float clampedYawDeg = Mth.clamp(entityData.netHeadYaw(), -20.0f, 20.0f); + head.setRotY(clampedYawDeg * Mth.DEG_TO_RAD); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/IndominusRexRenderer.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/IndominusRexRenderer.java new file mode 100644 index 0000000..28e2a24 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/IndominusRexRenderer.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.cmr.jurassicrevived.entity.custom.IndominusRexEntity; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.util.Mth; +import software.bernie.geckolib.cache.object.BakedGeoModel; +import software.bernie.geckolib.renderer.GeoEntityRenderer; + + +public class IndominusRexRenderer extends GeoEntityRenderer { + private final float animalScale = 2.77F; + public IndominusRexRenderer(EntityRendererProvider.Context renderManager) { + super(renderManager, new IndominusRexModel()); + } + + @Override + public void scaleModelForRender(float widthScale, float heightScale, PoseStack poseStack, IndominusRexEntity animatable, BakedGeoModel model, boolean isReRender, float partialTick, int packedLight, int packedOverlay) { + poseStack.scale(animalScale, animalScale, animalScale); + if(animatable.isBaby()) { + float growthProgress = Mth.clamp((24000.0F + animatable.getSyncedAge()) / 24000.0F, 0.0F, 1.0F); + float scale = Mth.lerp(growthProgress, 0.2F, 1.0F); + poseStack.scale(scale, scale, scale); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/IndominusRexVariant.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/IndominusRexVariant.java new file mode 100644 index 0000000..7342f93 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/IndominusRexVariant.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import java.util.Arrays; +import java.util.Comparator; + +public enum IndominusRexVariant { + MALE(0), + FEMALE(1); + + private static final IndominusRexVariant[] BY_ID = Arrays.stream(values()).sorted( + Comparator.comparingInt(IndominusRexVariant::getId)).toArray(IndominusRexVariant[]::new); + + private final int id; + + IndominusRexVariant(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static IndominusRexVariant byId(int id) { + return BY_ID[id % BY_ID.length]; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/IndoraptorModel.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/IndoraptorModel.java new file mode 100644 index 0000000..625e462 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/IndoraptorModel.java @@ -0,0 +1,112 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.google.common.collect.Maps; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.entity.custom.IndoraptorEntity; +import net.minecraft.Util; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animation.AnimationState; +/*?} else {*/ +/*import software.bernie.geckolib.animation.AnimationState; + *//*?}*/ +import software.bernie.geckolib.cache.object.GeoBone; +import software.bernie.geckolib.constant.DataTickets; +import software.bernie.geckolib.model.GeoModel; + +import java.util.Map; + +public class IndoraptorModel extends GeoModel { + + private static final Map LOCATION_BY_VARIANT = + Util.make(Maps.newEnumMap(IndoraptorVariant.class), map -> { + map.put(IndoraptorVariant.MALE, Constants.rl("textures/entity/indoraptor.png")); + map.put(IndoraptorVariant.FEMALE, Constants.rl("textures/entity/indoraptor_female.png")); + }); + + // Model-local "currently applied" offsets; cleared before each entity render + private float[] appliedYaw = null; + private float[] appliedRoll = null; + + @Override + public ResourceLocation getModelResource(IndoraptorEntity animatable) { + return Constants.rl("geo/indoraptor.geo.json"); + } + + @Override + public ResourceLocation getTextureResource(IndoraptorEntity animatable) { + return LOCATION_BY_VARIANT.get(animatable.getVariant()); + } + + @Override + public ResourceLocation getAnimationResource(IndoraptorEntity animatable) { + return Constants.rl("animations/indoraptor.animation.json"); + } + + @Override + public void setCustomAnimations(IndoraptorEntity entity, long id, AnimationState state) { + super.setCustomAnimations(entity, id, state); + + String[] tailBones = { "TailMain", "Tail1", "Tail2", "Tail3", "Tail4", "Tail5", "Tail6", "Tail7" }; + int n = tailBones.length; + + if (appliedYaw == null || appliedYaw.length != n) { + appliedYaw = new float[n]; + appliedRoll = new float[n]; + } + + // 1) Clear previous offsets (from the last entity rendered with this model instance) + for (int i = 0; i < n; i++) { + if (appliedYaw[i] == 0.0f && appliedRoll[i] == 0.0f) continue; + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + if (appliedYaw[i] != 0.0f) bone.setRotY(bone.getRotY() - appliedYaw[i]); + if (appliedRoll[i] != 0.0f) bone.setRotZ(bone.getRotZ() - appliedRoll[i]); + appliedYaw[i] = 0.0f; + appliedRoll[i] = 0.0f; + } + + // 2) Interpolated sway for extra smoothness between ticks + float sway = entity.getTailSwayOffset(state.getPartialTick()); // [-1, 1] + + // Tuning + float maxYawDeg = 22.0f; // increased max sweep + float swayGain = 1.35f; // amplifies overall power + float rollFraction = 0.40f; // slightly stronger roll for heft + + float deg2rad = (float)Math.PI / 180f; + + // Direction: positive sway (left turn) -> tail swings right (negative yaw) + // Flip the sign here if the sway feels inverted + float baseYaw = sway * maxYawDeg * deg2rad; + float baseRoll = -baseYaw * rollFraction; + + float[] weights = { 1.00f, 0.78f, 0.58f, 0.42f, 0.30f, 0.22f, 0.16f, 0.12f }; + + for (int i = 0; i < n; i++) { + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + + float w = weights[i]; + float yaw = baseYaw * w; + float roll = baseRoll * w; + + // OVERRIDE animations on Y/Z only: keep the model's predefined X bend intact + // Do NOT reset rotX here, so the upward bend stays + bone.setRotY(yaw); + bone.setRotZ(roll); + + appliedYaw[i] = yaw; + appliedRoll[i] = roll; + } + + GeoBone head = (GeoBone) getAnimationProcessor().getBone("Neck1"); + + if (head != null) { + var entityData = state.getData(DataTickets.ENTITY_MODEL_DATA); + float clampedYawDeg = Mth.clamp(entityData.netHeadYaw(), -30.0f, 30.0f); + head.setRotY(clampedYawDeg * Mth.DEG_TO_RAD); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/IndoraptorRenderer.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/IndoraptorRenderer.java new file mode 100644 index 0000000..8663cff --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/IndoraptorRenderer.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.cmr.jurassicrevived.entity.custom.IndoraptorEntity; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.util.Mth; +import software.bernie.geckolib.cache.object.BakedGeoModel; +import software.bernie.geckolib.renderer.GeoEntityRenderer; + + +public class IndoraptorRenderer extends GeoEntityRenderer { + private final float animalScale = 1.0F; + public IndoraptorRenderer(EntityRendererProvider.Context renderManager) { + super(renderManager, new IndoraptorModel()); + } + + @Override + public void scaleModelForRender(float widthScale, float heightScale, PoseStack poseStack, IndoraptorEntity animatable, BakedGeoModel model, boolean isReRender, float partialTick, int packedLight, int packedOverlay) { + poseStack.scale(animalScale, animalScale, animalScale); + if(animatable.isBaby()) { + float growthProgress = Mth.clamp((24000.0F + animatable.getSyncedAge()) / 24000.0F, 0.0F, 1.0F); + float scale = Mth.lerp(growthProgress, 0.2F, 1.0F); + poseStack.scale(scale, scale, scale); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/IndoraptorVariant.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/IndoraptorVariant.java new file mode 100644 index 0000000..849e849 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/IndoraptorVariant.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import java.util.Arrays; +import java.util.Comparator; + +public enum IndoraptorVariant { + MALE(0), + FEMALE(1); + + private static final IndoraptorVariant[] BY_ID = Arrays.stream(values()).sorted( + Comparator.comparingInt(IndoraptorVariant::getId)).toArray(IndoraptorVariant[]::new); + + private final int id; + + IndoraptorVariant(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static IndoraptorVariant byId(int id) { + return BY_ID[id % BY_ID.length]; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/InostranceviaModel.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/InostranceviaModel.java new file mode 100644 index 0000000..7881c7a --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/InostranceviaModel.java @@ -0,0 +1,112 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.google.common.collect.Maps; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.entity.custom.InostranceviaEntity; +import net.minecraft.Util; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animation.AnimationState; +/*?} else {*/ +/*import software.bernie.geckolib.animation.AnimationState; + *//*?}*/ +import software.bernie.geckolib.cache.object.GeoBone; +import software.bernie.geckolib.constant.DataTickets; +import software.bernie.geckolib.model.GeoModel; + +import java.util.Map; + +public class InostranceviaModel extends GeoModel { + + private static final Map LOCATION_BY_VARIANT = + Util.make(Maps.newEnumMap(InostranceviaVariant.class), map -> { + map.put(InostranceviaVariant.MALE, Constants.rl("textures/entity/inostrancevia.png")); + map.put(InostranceviaVariant.FEMALE, Constants.rl("textures/entity/inostrancevia_female.png")); + }); + + // Model-local "currently applied" offsets; cleared before each entity render + private float[] appliedYaw = null; + private float[] appliedRoll = null; + + @Override + public ResourceLocation getModelResource(InostranceviaEntity animatable) { + return Constants.rl("geo/inostrancevia.geo.json"); + } + + @Override + public ResourceLocation getTextureResource(InostranceviaEntity animatable) { + return LOCATION_BY_VARIANT.get(animatable.getVariant()); + } + + @Override + public ResourceLocation getAnimationResource(InostranceviaEntity animatable) { + return Constants.rl("animations/inostrancevia.animation.json"); + } + + @Override + public void setCustomAnimations(InostranceviaEntity entity, long id, AnimationState state) { + super.setCustomAnimations(entity, id, state); + + String[] tailBones = { "tail1", "tail2", "tail3", "tail4", "tail5" }; + int n = tailBones.length; + + if (appliedYaw == null || appliedYaw.length != n) { + appliedYaw = new float[n]; + appliedRoll = new float[n]; + } + + // 1) Clear previous offsets (from the last entity rendered with this model instance) + for (int i = 0; i < n; i++) { + if (appliedYaw[i] == 0.0f && appliedRoll[i] == 0.0f) continue; + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + if (appliedYaw[i] != 0.0f) bone.setRotY(bone.getRotY() - appliedYaw[i]); + if (appliedRoll[i] != 0.0f) bone.setRotZ(bone.getRotZ() - appliedRoll[i]); + appliedYaw[i] = 0.0f; + appliedRoll[i] = 0.0f; + } + + // 2) Interpolated sway for extra smoothness between ticks + float sway = entity.getTailSwayOffset(state.getPartialTick()); // [-1, 1] + + // Tuning + float maxYawDeg = 22.0f; // increased max sweep + float swayGain = 1.35f; // amplifies overall power + float rollFraction = 0.40f; // slightly stronger roll for heft + + float deg2rad = (float)Math.PI / 180f; + + // Direction: positive sway (left turn) -> tail swings right (negative yaw) + // Flip the sign here if the sway feels inverted + float baseYaw = sway * maxYawDeg * deg2rad; + float baseRoll = -baseYaw * rollFraction; + + float[] weights = { 1.00f, 0.78f, 0.58f, 0.42f, 0.30f }; + + for (int i = 0; i < n; i++) { + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + + float w = weights[i]; + float yaw = baseYaw * w; + float roll = baseRoll * w; + + // OVERRIDE animations on Y/Z only: keep the model's predefined X bend intact + // Do NOT reset rotX here, so the upward bend stays + bone.setRotY(yaw); + bone.setRotZ(roll); + + appliedYaw[i] = yaw; + appliedRoll[i] = roll; + } + + GeoBone head = (GeoBone) getAnimationProcessor().getBone("neck1"); + + if (head != null) { + var entityData = state.getData(DataTickets.ENTITY_MODEL_DATA); + float clampedYawDeg = Mth.clamp(entityData.netHeadYaw(), -30.0f, 30.0f); + head.setRotY(clampedYawDeg * Mth.DEG_TO_RAD); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/InostranceviaRenderer.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/InostranceviaRenderer.java new file mode 100644 index 0000000..3d9837a --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/InostranceviaRenderer.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.cmr.jurassicrevived.entity.custom.InostranceviaEntity; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.util.Mth; +import software.bernie.geckolib.cache.object.BakedGeoModel; +import software.bernie.geckolib.renderer.GeoEntityRenderer; + + +public class InostranceviaRenderer extends GeoEntityRenderer { + private final float animalScale = 1.0F; + public InostranceviaRenderer(EntityRendererProvider.Context renderManager) { + super(renderManager, new InostranceviaModel()); + } + + @Override + public void scaleModelForRender(float widthScale, float heightScale, PoseStack poseStack, InostranceviaEntity animatable, BakedGeoModel model, boolean isReRender, float partialTick, int packedLight, int packedOverlay) { + poseStack.scale(animalScale, animalScale, animalScale); + if(animatable.isBaby()) { + float growthProgress = Mth.clamp((24000.0F + animatable.getSyncedAge()) / 24000.0F, 0.0F, 1.0F); + float scale = Mth.lerp(growthProgress, 0.2F, 1.0F); + poseStack.scale(scale, scale, scale); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/InostranceviaVariant.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/InostranceviaVariant.java new file mode 100644 index 0000000..625a1b8 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/InostranceviaVariant.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import java.util.Arrays; +import java.util.Comparator; + +public enum InostranceviaVariant { + MALE(0), + FEMALE(1); + + private static final InostranceviaVariant[] BY_ID = Arrays.stream(values()).sorted( + Comparator.comparingInt(InostranceviaVariant::getId)).toArray(InostranceviaVariant[]::new); + + private final int id; + + InostranceviaVariant(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static InostranceviaVariant byId(int id) { + return BY_ID[id % BY_ID.length]; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/LambeosaurusModel.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/LambeosaurusModel.java new file mode 100644 index 0000000..71ff8a9 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/LambeosaurusModel.java @@ -0,0 +1,112 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.google.common.collect.Maps; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.entity.custom.LambeosaurusEntity; +import net.minecraft.Util; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animation.AnimationState; +/*?} else {*/ +/*import software.bernie.geckolib.animation.AnimationState; + *//*?}*/ +import software.bernie.geckolib.cache.object.GeoBone; +import software.bernie.geckolib.constant.DataTickets; +import software.bernie.geckolib.model.GeoModel; + +import java.util.Map; + +public class LambeosaurusModel extends GeoModel { + + private static final Map LOCATION_BY_VARIANT = + Util.make(Maps.newEnumMap(LambeosaurusVariant.class), map -> { + map.put(LambeosaurusVariant.MALE, Constants.rl("textures/entity/lambeosaurus.png")); + map.put(LambeosaurusVariant.FEMALE, Constants.rl("textures/entity/lambeosaurus_female.png")); + }); + + // Model-local "currently applied" offsets; cleared before each entity render + private float[] appliedYaw = null; + private float[] appliedRoll = null; + + @Override + public ResourceLocation getModelResource(LambeosaurusEntity animatable) { + return Constants.rl("geo/lambeosaurus.geo.json"); + } + + @Override + public ResourceLocation getTextureResource(LambeosaurusEntity animatable) { + return LOCATION_BY_VARIANT.get(animatable.getVariant()); + } + + @Override + public ResourceLocation getAnimationResource(LambeosaurusEntity animatable) { + return Constants.rl("animations/lambeosaurus.animation.json"); + } + + @Override + public void setCustomAnimations(LambeosaurusEntity entity, long id, AnimationState state) { + super.setCustomAnimations(entity, id, state); + + String[] tailBones = { "Tail1", "Tail2", "Tail3", "Tail4", "Tail5", "Tail6" }; + int n = tailBones.length; + + if (appliedYaw == null || appliedYaw.length != n) { + appliedYaw = new float[n]; + appliedRoll = new float[n]; + } + + // 1) Clear previous offsets (from the last entity rendered with this model instance) + for (int i = 0; i < n; i++) { + if (appliedYaw[i] == 0.0f && appliedRoll[i] == 0.0f) continue; + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + if (appliedYaw[i] != 0.0f) bone.setRotY(bone.getRotY() - appliedYaw[i]); + if (appliedRoll[i] != 0.0f) bone.setRotZ(bone.getRotZ() - appliedRoll[i]); + appliedYaw[i] = 0.0f; + appliedRoll[i] = 0.0f; + } + + // 2) Interpolated sway for extra smoothness between ticks + float sway = entity.getTailSwayOffset(state.getPartialTick()); // [-1, 1] + + // Tuning + float maxYawDeg = 22.0f; // increased max sweep + float swayGain = 1.35f; // amplifies overall power + float rollFraction = 0.40f; // slightly stronger roll for heft + + float deg2rad = (float)Math.PI / 180f; + + // Direction: positive sway (left turn) -> tail swings right (negative yaw) + // Flip the sign here if the sway feels inverted + float baseYaw = sway * maxYawDeg * deg2rad; + float baseRoll = -baseYaw * rollFraction; + + float[] weights = { 1.00f, 0.78f, 0.58f, 0.42f, 0.30f, 0.22f }; + + for (int i = 0; i < n; i++) { + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + + float w = weights[i]; + float yaw = baseYaw * w; + float roll = baseRoll * w; + + // OVERRIDE animations on Y/Z only: keep the model's predefined X bend intact + // Do NOT reset rotX here, so the upward bend stays + bone.setRotY(yaw); + bone.setRotZ(roll); + + appliedYaw[i] = yaw; + appliedRoll[i] = roll; + } + + GeoBone head = (GeoBone) getAnimationProcessor().getBone("Neck"); + + if (head != null) { + var entityData = state.getData(DataTickets.ENTITY_MODEL_DATA); + float clampedYawDeg = Mth.clamp(entityData.netHeadYaw(), -30.0f, 30.0f); + head.setRotY(clampedYawDeg * Mth.DEG_TO_RAD); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/LambeosaurusRenderer.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/LambeosaurusRenderer.java new file mode 100644 index 0000000..3ef3fb9 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/LambeosaurusRenderer.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.cmr.jurassicrevived.entity.custom.LambeosaurusEntity; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.util.Mth; +import software.bernie.geckolib.cache.object.BakedGeoModel; +import software.bernie.geckolib.renderer.GeoEntityRenderer; + + +public class LambeosaurusRenderer extends GeoEntityRenderer { + private final float animalScale = 1.65F; + public LambeosaurusRenderer(EntityRendererProvider.Context renderManager) { + super(renderManager, new LambeosaurusModel()); + } + + @Override + public void scaleModelForRender(float widthScale, float heightScale, PoseStack poseStack, LambeosaurusEntity animatable, BakedGeoModel model, boolean isReRender, float partialTick, int packedLight, int packedOverlay) { + poseStack.scale(animalScale, animalScale, animalScale); + if(animatable.isBaby()) { + float growthProgress = Mth.clamp((24000.0F + animatable.getSyncedAge()) / 24000.0F, 0.0F, 1.0F); + float scale = Mth.lerp(growthProgress, 0.2F, 1.0F); + poseStack.scale(scale, scale, scale); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/LambeosaurusVariant.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/LambeosaurusVariant.java new file mode 100644 index 0000000..7993b1d --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/LambeosaurusVariant.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import java.util.Arrays; +import java.util.Comparator; + +public enum LambeosaurusVariant { + MALE(0), + FEMALE(1); + + private static final LambeosaurusVariant[] BY_ID = Arrays.stream(values()).sorted( + Comparator.comparingInt(LambeosaurusVariant::getId)).toArray(LambeosaurusVariant[]::new); + + private final int id; + + LambeosaurusVariant(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static LambeosaurusVariant byId(int id) { + return BY_ID[id % BY_ID.length]; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/LudodactylusModel.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/LudodactylusModel.java new file mode 100644 index 0000000..5a29272 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/LudodactylusModel.java @@ -0,0 +1,112 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.google.common.collect.Maps; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.entity.custom.LudodactylusEntity; +import net.minecraft.Util; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animation.AnimationState; +/*?} else {*/ +/*import software.bernie.geckolib.animation.AnimationState; + *//*?}*/ +import software.bernie.geckolib.cache.object.GeoBone; +import software.bernie.geckolib.constant.DataTickets; +import software.bernie.geckolib.model.GeoModel; + +import java.util.Map; + +public class LudodactylusModel extends GeoModel { + + private static final Map LOCATION_BY_VARIANT = + Util.make(Maps.newEnumMap(LudodactylusVariant.class), map -> { + map.put(LudodactylusVariant.MALE, Constants.rl("textures/entity/ludodactylus.png")); + map.put(LudodactylusVariant.FEMALE, Constants.rl("textures/entity/ludodactylus_female.png")); + }); + + // Model-local "currently applied" offsets; cleared before each entity render + private float[] appliedYaw = null; + private float[] appliedRoll = null; + + @Override + public ResourceLocation getModelResource(LudodactylusEntity animatable) { + return Constants.rl("geo/ludodactylus.geo.json"); + } + + @Override + public ResourceLocation getTextureResource(LudodactylusEntity animatable) { + return LOCATION_BY_VARIANT.get(animatable.getVariant()); + } + + @Override + public ResourceLocation getAnimationResource(LudodactylusEntity animatable) { + return Constants.rl("animations/ludodactylus.animation.json"); + } + + @Override + public void setCustomAnimations(LudodactylusEntity entity, long id, AnimationState state) { + super.setCustomAnimations(entity, id, state); + + String[] tailBones = { "Tail1", "Tail2", "Tail3" }; + int n = tailBones.length; + + if (appliedYaw == null || appliedYaw.length != n) { + appliedYaw = new float[n]; + appliedRoll = new float[n]; + } + + // 1) Clear previous offsets (from the last entity rendered with this model instance) + for (int i = 0; i < n; i++) { + if (appliedYaw[i] == 0.0f && appliedRoll[i] == 0.0f) continue; + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + if (appliedYaw[i] != 0.0f) bone.setRotY(bone.getRotY() - appliedYaw[i]); + if (appliedRoll[i] != 0.0f) bone.setRotZ(bone.getRotZ() - appliedRoll[i]); + appliedYaw[i] = 0.0f; + appliedRoll[i] = 0.0f; + } + + // 2) Interpolated sway for extra smoothness between ticks + float sway = entity.getTailSwayOffset(state.getPartialTick()); // [-1, 1] + + // Tuning + float maxYawDeg = 22.0f; // increased max sweep + float swayGain = 1.35f; // amplifies overall power + float rollFraction = 0.40f; // slightly stronger roll for heft + + float deg2rad = (float)Math.PI / 180f; + + // Direction: positive sway (left turn) -> tail swings right (negative yaw) + // Flip the sign here if the sway feels inverted + float baseYaw = sway * maxYawDeg * deg2rad; + float baseRoll = -baseYaw * rollFraction; + + float[] weights = { 1.00f, 0.78f, 0.58f }; + + for (int i = 0; i < n; i++) { + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + + float w = weights[i]; + float yaw = baseYaw * w; + float roll = baseRoll * w; + + // OVERRIDE animations on Y/Z only: keep the model's predefined X bend intact + // Do NOT reset rotX here, so the upward bend stays + bone.setRotY(yaw); + bone.setRotZ(roll); + + appliedYaw[i] = yaw; + appliedRoll[i] = roll; + } + + GeoBone head = (GeoBone) getAnimationProcessor().getBone("Neck1"); + + if (head != null) { + var entityData = state.getData(DataTickets.ENTITY_MODEL_DATA); + float clampedYawDeg = Mth.clamp(entityData.netHeadYaw(), -20.0f, 20.0f); + head.setRotY(clampedYawDeg * Mth.DEG_TO_RAD); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/LudodactylusRenderer.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/LudodactylusRenderer.java new file mode 100644 index 0000000..18e6c74 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/LudodactylusRenderer.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.cmr.jurassicrevived.entity.custom.LudodactylusEntity; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.util.Mth; +import software.bernie.geckolib.cache.object.BakedGeoModel; +import software.bernie.geckolib.renderer.GeoEntityRenderer; + + +public class LudodactylusRenderer extends GeoEntityRenderer { + private final float animalScale = 1.0F; + public LudodactylusRenderer(EntityRendererProvider.Context renderManager) { + super(renderManager, new LudodactylusModel()); + } + + @Override + public void scaleModelForRender(float widthScale, float heightScale, PoseStack poseStack, LudodactylusEntity animatable, BakedGeoModel model, boolean isReRender, float partialTick, int packedLight, int packedOverlay) { + poseStack.scale(animalScale, animalScale, animalScale); + if(animatable.isBaby()) { + float growthProgress = Mth.clamp((24000.0F + animatable.getSyncedAge()) / 24000.0F, 0.0F, 1.0F); + float scale = Mth.lerp(growthProgress, 0.2F, 1.0F); + poseStack.scale(scale, scale, scale); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/LudodactylusVariant.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/LudodactylusVariant.java new file mode 100644 index 0000000..7790629 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/LudodactylusVariant.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import java.util.Arrays; +import java.util.Comparator; + +public enum LudodactylusVariant { + MALE(0), + FEMALE(1); + + private static final LudodactylusVariant[] BY_ID = Arrays.stream(values()).sorted( + Comparator.comparingInt(LudodactylusVariant::getId)).toArray(LudodactylusVariant[]::new); + + private final int id; + + LudodactylusVariant(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static LudodactylusVariant byId(int id) { + return BY_ID[id % BY_ID.length]; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/MajungasaurusModel.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/MajungasaurusModel.java new file mode 100644 index 0000000..cc67fd5 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/MajungasaurusModel.java @@ -0,0 +1,112 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.google.common.collect.Maps; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.entity.custom.MajungasaurusEntity; +import net.minecraft.Util; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animation.AnimationState; +/*?} else {*/ +/*import software.bernie.geckolib.animation.AnimationState; + *//*?}*/ +import software.bernie.geckolib.cache.object.GeoBone; +import software.bernie.geckolib.constant.DataTickets; +import software.bernie.geckolib.model.GeoModel; + +import java.util.Map; + +public class MajungasaurusModel extends GeoModel { + + private static final Map LOCATION_BY_VARIANT = + Util.make(Maps.newEnumMap(MajungasaurusVariant.class), map -> { + map.put(MajungasaurusVariant.MALE, Constants.rl("textures/entity/majungasaurus.png")); + map.put(MajungasaurusVariant.FEMALE, Constants.rl("textures/entity/majungasaurus_female.png")); + }); + + // Model-local "currently applied" offsets; cleared before each entity render + private float[] appliedYaw = null; + private float[] appliedRoll = null; + + @Override + public ResourceLocation getModelResource(MajungasaurusEntity animatable) { + return Constants.rl("geo/majungasaurus.geo.json"); + } + + @Override + public ResourceLocation getTextureResource(MajungasaurusEntity animatable) { + return LOCATION_BY_VARIANT.get(animatable.getVariant()); + } + + @Override + public ResourceLocation getAnimationResource(MajungasaurusEntity animatable) { + return Constants.rl("animations/majungasaurus.animation.json"); + } + + @Override + public void setCustomAnimations(MajungasaurusEntity entity, long id, AnimationState state) { + super.setCustomAnimations(entity, id, state); + + String[] tailBones = { "TailBase", "Tail2", "Tail3", "Tail4", "Tail5", "Tail6" }; + int n = tailBones.length; + + if (appliedYaw == null || appliedYaw.length != n) { + appliedYaw = new float[n]; + appliedRoll = new float[n]; + } + + // 1) Clear previous offsets (from the last entity rendered with this model instance) + for (int i = 0; i < n; i++) { + if (appliedYaw[i] == 0.0f && appliedRoll[i] == 0.0f) continue; + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + if (appliedYaw[i] != 0.0f) bone.setRotY(bone.getRotY() - appliedYaw[i]); + if (appliedRoll[i] != 0.0f) bone.setRotZ(bone.getRotZ() - appliedRoll[i]); + appliedYaw[i] = 0.0f; + appliedRoll[i] = 0.0f; + } + + // 2) Interpolated sway for extra smoothness between ticks + float sway = entity.getTailSwayOffset(state.getPartialTick()); // [-1, 1] + + // Tuning + float maxYawDeg = 22.0f; // increased max sweep + float swayGain = 1.35f; // amplifies overall power + float rollFraction = 0.40f; // slightly stronger roll for heft + + float deg2rad = (float)Math.PI / 180f; + + // Direction: positive sway (left turn) -> tail swings right (negative yaw) + // Flip the sign here if the sway feels inverted + float baseYaw = sway * maxYawDeg * deg2rad; + float baseRoll = -baseYaw * rollFraction; + + float[] weights = { 1.00f, 0.78f, 0.58f, 0.42f, 0.30f, 0.22f }; + + for (int i = 0; i < n; i++) { + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + + float w = weights[i]; + float yaw = baseYaw * w; + float roll = baseRoll * w; + + // OVERRIDE animations on Y/Z only: keep the model's predefined X bend intact + // Do NOT reset rotX here, so the upward bend stays + bone.setRotY(yaw); + bone.setRotZ(roll); + + appliedYaw[i] = yaw; + appliedRoll[i] = roll; + } + + GeoBone head = (GeoBone) getAnimationProcessor().getBone("NeckBASE"); + + if (head != null) { + var entityData = state.getData(DataTickets.ENTITY_MODEL_DATA); + float clampedYawDeg = Mth.clamp(entityData.netHeadYaw(), -20.0f, 20.0f); + head.setRotY(clampedYawDeg * Mth.DEG_TO_RAD); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/MajungasaurusRenderer.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/MajungasaurusRenderer.java new file mode 100644 index 0000000..6c83b32 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/MajungasaurusRenderer.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.cmr.jurassicrevived.entity.custom.MajungasaurusEntity; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.util.Mth; +import software.bernie.geckolib.cache.object.BakedGeoModel; +import software.bernie.geckolib.renderer.GeoEntityRenderer; + + +public class MajungasaurusRenderer extends GeoEntityRenderer { + private final float animalScale = 1.57F; + public MajungasaurusRenderer(EntityRendererProvider.Context renderManager) { + super(renderManager, new MajungasaurusModel()); + } + + @Override + public void scaleModelForRender(float widthScale, float heightScale, PoseStack poseStack, MajungasaurusEntity animatable, BakedGeoModel model, boolean isReRender, float partialTick, int packedLight, int packedOverlay) { + poseStack.scale(animalScale, animalScale, animalScale); + if(animatable.isBaby()) { + float growthProgress = Mth.clamp((24000.0F + animatable.getSyncedAge()) / 24000.0F, 0.0F, 1.0F); + float scale = Mth.lerp(growthProgress, 0.2F, 1.0F); + poseStack.scale(scale, scale, scale); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/MajungasaurusVariant.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/MajungasaurusVariant.java new file mode 100644 index 0000000..11b5f16 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/MajungasaurusVariant.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import java.util.Arrays; +import java.util.Comparator; + +public enum MajungasaurusVariant { + MALE(0), + FEMALE(1); + + private static final MajungasaurusVariant[] BY_ID = Arrays.stream(values()).sorted( + Comparator.comparingInt(MajungasaurusVariant::getId)).toArray(MajungasaurusVariant[]::new); + + private final int id; + + MajungasaurusVariant(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static MajungasaurusVariant byId(int id) { + return BY_ID[id % BY_ID.length]; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/MamenchisaurusModel.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/MamenchisaurusModel.java new file mode 100644 index 0000000..e38481b --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/MamenchisaurusModel.java @@ -0,0 +1,112 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.google.common.collect.Maps; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.entity.custom.MamenchisaurusEntity; +import net.minecraft.Util; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animation.AnimationState; +/*?} else {*/ +/*import software.bernie.geckolib.animation.AnimationState; + *//*?}*/ +import software.bernie.geckolib.cache.object.GeoBone; +import software.bernie.geckolib.constant.DataTickets; +import software.bernie.geckolib.model.GeoModel; + +import java.util.Map; + +public class MamenchisaurusModel extends GeoModel { + + private static final Map LOCATION_BY_VARIANT = + Util.make(Maps.newEnumMap(MamenchisaurusVariant.class), map -> { + map.put(MamenchisaurusVariant.MALE, Constants.rl("textures/entity/mamenchisaurus.png")); + map.put(MamenchisaurusVariant.FEMALE, Constants.rl("textures/entity/mamenchisaurus_female.png")); + }); + + // Model-local "currently applied" offsets; cleared before each entity render + private float[] appliedYaw = null; + private float[] appliedRoll = null; + + @Override + public ResourceLocation getModelResource(MamenchisaurusEntity animatable) { + return Constants.rl("geo/mamenchisaurus.geo.json"); + } + + @Override + public ResourceLocation getTextureResource(MamenchisaurusEntity animatable) { + return LOCATION_BY_VARIANT.get(animatable.getVariant()); + } + + @Override + public ResourceLocation getAnimationResource(MamenchisaurusEntity animatable) { + return Constants.rl("animations/mamenchisaurus.animation.json"); + } + + @Override + public void setCustomAnimations(MamenchisaurusEntity entity, long id, AnimationState state) { + super.setCustomAnimations(entity, id, state); + + String[] tailBones = { "tail1", "tail2", "tail3", "tail4", "tail5", "tail6", "tail7", "tail8", "tail9", "tail10" }; + int n = tailBones.length; + + if (appliedYaw == null || appliedYaw.length != n) { + appliedYaw = new float[n]; + appliedRoll = new float[n]; + } + + // 1) Clear previous offsets (from the last entity rendered with this model instance) + for (int i = 0; i < n; i++) { + if (appliedYaw[i] == 0.0f && appliedRoll[i] == 0.0f) continue; + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + if (appliedYaw[i] != 0.0f) bone.setRotY(bone.getRotY() - appliedYaw[i]); + if (appliedRoll[i] != 0.0f) bone.setRotZ(bone.getRotZ() - appliedRoll[i]); + appliedYaw[i] = 0.0f; + appliedRoll[i] = 0.0f; + } + + // 2) Interpolated sway for extra smoothness between ticks + float sway = entity.getTailSwayOffset(state.getPartialTick()); // [-1, 1] + + // Tuning + float maxYawDeg = 22.0f; // increased max sweep + float swayGain = 1.35f; // amplifies overall power + float rollFraction = 0.40f; // slightly stronger roll for heft + + float deg2rad = (float)Math.PI / 180f; + + // Direction: positive sway (left turn) -> tail swings right (negative yaw) + // Flip the sign here if the sway feels inverted + float baseYaw = sway * maxYawDeg * deg2rad; + float baseRoll = -baseYaw * rollFraction; + + float[] weights = { 1.00f, 0.78f, 0.58f, 0.42f, 0.30f, 0.22f, 0.16f, 0.12f, 0.08f, 0.06f }; + + for (int i = 0; i < n; i++) { + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + + float w = weights[i]; + float yaw = baseYaw * w; + float roll = baseRoll * w; + + // OVERRIDE animations on Y/Z only: keep the model's predefined X bend intact + // Do NOT reset rotX here, so the upward bend stays + bone.setRotY(yaw); + bone.setRotZ(roll); + + appliedYaw[i] = yaw; + appliedRoll[i] = roll; + } + + GeoBone head = (GeoBone) getAnimationProcessor().getBone("neck1"); + + if (head != null) { + var entityData = state.getData(DataTickets.ENTITY_MODEL_DATA); + float clampedYawDeg = Mth.clamp(entityData.netHeadYaw(), -10.0f, 10.0f); + head.setRotY(clampedYawDeg * Mth.DEG_TO_RAD); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/MamenchisaurusRenderer.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/MamenchisaurusRenderer.java new file mode 100644 index 0000000..526b4a3 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/MamenchisaurusRenderer.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.cmr.jurassicrevived.entity.custom.MamenchisaurusEntity; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.util.Mth; +import software.bernie.geckolib.cache.object.BakedGeoModel; +import software.bernie.geckolib.renderer.GeoEntityRenderer; + + +public class MamenchisaurusRenderer extends GeoEntityRenderer { + private final float animalScale = 1.85F; + public MamenchisaurusRenderer(EntityRendererProvider.Context renderManager) { + super(renderManager, new MamenchisaurusModel()); + } + + @Override + public void scaleModelForRender(float widthScale, float heightScale, PoseStack poseStack, MamenchisaurusEntity animatable, BakedGeoModel model, boolean isReRender, float partialTick, int packedLight, int packedOverlay) { + poseStack.scale(animalScale, animalScale, animalScale); + if(animatable.isBaby()) { + float growthProgress = Mth.clamp((24000.0F + animatable.getSyncedAge()) / 24000.0F, 0.0F, 1.0F); + float scale = Mth.lerp(growthProgress, 0.2F, 1.0F); + poseStack.scale(scale, scale, scale); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/MamenchisaurusVariant.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/MamenchisaurusVariant.java new file mode 100644 index 0000000..03d63de --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/MamenchisaurusVariant.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import java.util.Arrays; +import java.util.Comparator; + +public enum MamenchisaurusVariant { + MALE(0), + FEMALE(1); + + private static final MamenchisaurusVariant[] BY_ID = Arrays.stream(values()).sorted( + Comparator.comparingInt(MamenchisaurusVariant::getId)).toArray(MamenchisaurusVariant[]::new); + + private final int id; + + MamenchisaurusVariant(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static MamenchisaurusVariant byId(int id) { + return BY_ID[id % BY_ID.length]; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/MetriacanthosaurusModel.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/MetriacanthosaurusModel.java new file mode 100644 index 0000000..7a89009 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/MetriacanthosaurusModel.java @@ -0,0 +1,112 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.google.common.collect.Maps; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.entity.custom.MetriacanthosaurusEntity; +import net.minecraft.Util; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animation.AnimationState; +/*?} else {*/ +/*import software.bernie.geckolib.animation.AnimationState; + *//*?}*/ +import software.bernie.geckolib.cache.object.GeoBone; +import software.bernie.geckolib.constant.DataTickets; +import software.bernie.geckolib.model.GeoModel; + +import java.util.Map; + +public class MetriacanthosaurusModel extends GeoModel { + + private static final Map LOCATION_BY_VARIANT = + Util.make(Maps.newEnumMap(MetriacanthosaurusVariant.class), map -> { + map.put(MetriacanthosaurusVariant.MALE, Constants.rl("textures/entity/metriacanthosaurus.png")); + map.put(MetriacanthosaurusVariant.FEMALE, Constants.rl("textures/entity/metriacanthosaurus_female.png")); + }); + + // Model-local "currently applied" offsets; cleared before each entity render + private float[] appliedYaw = null; + private float[] appliedRoll = null; + + @Override + public ResourceLocation getModelResource(MetriacanthosaurusEntity animatable) { + return Constants.rl("geo/metriacanthosaurus.geo.json"); + } + + @Override + public ResourceLocation getTextureResource(MetriacanthosaurusEntity animatable) { + return LOCATION_BY_VARIANT.get(animatable.getVariant()); + } + + @Override + public ResourceLocation getAnimationResource(MetriacanthosaurusEntity animatable) { + return Constants.rl("animations/metriacanthosaurus.animation.json"); + } + + @Override + public void setCustomAnimations(MetriacanthosaurusEntity entity, long id, AnimationState state) { + super.setCustomAnimations(entity, id, state); + + String[] tailBones = { "TailBase", "Tail2", "Tail3", "Tail4", "Tail5", "Tail6", "Tail7", "Tail8" }; + int n = tailBones.length; + + if (appliedYaw == null || appliedYaw.length != n) { + appliedYaw = new float[n]; + appliedRoll = new float[n]; + } + + // 1) Clear previous offsets (from the last entity rendered with this model instance) + for (int i = 0; i < n; i++) { + if (appliedYaw[i] == 0.0f && appliedRoll[i] == 0.0f) continue; + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + if (appliedYaw[i] != 0.0f) bone.setRotY(bone.getRotY() - appliedYaw[i]); + if (appliedRoll[i] != 0.0f) bone.setRotZ(bone.getRotZ() - appliedRoll[i]); + appliedYaw[i] = 0.0f; + appliedRoll[i] = 0.0f; + } + + // 2) Interpolated sway for extra smoothness between ticks + float sway = entity.getTailSwayOffset(state.getPartialTick()); // [-1, 1] + + // Tuning + float maxYawDeg = 22.0f; // increased max sweep + float swayGain = 1.35f; // amplifies overall power + float rollFraction = 0.40f; // slightly stronger roll for heft + + float deg2rad = (float)Math.PI / 180f; + + // Direction: positive sway (left turn) -> tail swings right (negative yaw) + // Flip the sign here if the sway feels inverted + float baseYaw = sway * maxYawDeg * deg2rad; + float baseRoll = -baseYaw * rollFraction; + + float[] weights = { 1.00f, 0.78f, 0.58f, 0.42f, 0.30f, 0.22f, 0.16f, 0.12f }; + + for (int i = 0; i < n; i++) { + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + + float w = weights[i]; + float yaw = baseYaw * w; + float roll = baseRoll * w; + + // OVERRIDE animations on Y/Z only: keep the model's predefined X bend intact + // Do NOT reset rotX here, so the upward bend stays + bone.setRotY(yaw); + bone.setRotZ(roll); + + appliedYaw[i] = yaw; + appliedRoll[i] = roll; + } + + GeoBone head = (GeoBone) getAnimationProcessor().getBone("NeckBASE"); + + if (head != null) { + var entityData = state.getData(DataTickets.ENTITY_MODEL_DATA); + float clampedYawDeg = Mth.clamp(entityData.netHeadYaw(), -30.0f, 30.0f); + head.setRotY(clampedYawDeg * Mth.DEG_TO_RAD); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/MetriacanthosaurusRenderer.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/MetriacanthosaurusRenderer.java new file mode 100644 index 0000000..a4bd5c0 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/MetriacanthosaurusRenderer.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.cmr.jurassicrevived.entity.custom.MetriacanthosaurusEntity; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.util.Mth; +import software.bernie.geckolib.cache.object.BakedGeoModel; +import software.bernie.geckolib.renderer.GeoEntityRenderer; + + +public class MetriacanthosaurusRenderer extends GeoEntityRenderer { + private final float animalScale = 1.75F; + public MetriacanthosaurusRenderer(EntityRendererProvider.Context renderManager) { + super(renderManager, new MetriacanthosaurusModel()); + } + + @Override + public void scaleModelForRender(float widthScale, float heightScale, PoseStack poseStack, MetriacanthosaurusEntity animatable, BakedGeoModel model, boolean isReRender, float partialTick, int packedLight, int packedOverlay) { + poseStack.scale(animalScale, animalScale, animalScale); + if(animatable.isBaby()) { + float growthProgress = Mth.clamp((24000.0F + animatable.getSyncedAge()) / 24000.0F, 0.0F, 1.0F); + float scale = Mth.lerp(growthProgress, 0.2F, 1.0F); + poseStack.scale(scale, scale, scale); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/MetriacanthosaurusVariant.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/MetriacanthosaurusVariant.java new file mode 100644 index 0000000..128d6be --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/MetriacanthosaurusVariant.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import java.util.Arrays; +import java.util.Comparator; + +public enum MetriacanthosaurusVariant { + MALE(0), + FEMALE(1); + + private static final MetriacanthosaurusVariant[] BY_ID = Arrays.stream(values()).sorted( + Comparator.comparingInt(MetriacanthosaurusVariant::getId)).toArray(MetriacanthosaurusVariant[]::new); + + private final int id; + + MetriacanthosaurusVariant(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static MetriacanthosaurusVariant byId(int id) { + return BY_ID[id % BY_ID.length]; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/MoganopterusModel.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/MoganopterusModel.java new file mode 100644 index 0000000..ae1098f --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/MoganopterusModel.java @@ -0,0 +1,112 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.google.common.collect.Maps; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.entity.custom.MoganopterusEntity; +import net.minecraft.Util; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animation.AnimationState; +/*?} else {*/ +/*import software.bernie.geckolib.animation.AnimationState; + *//*?}*/ +import software.bernie.geckolib.cache.object.GeoBone; +import software.bernie.geckolib.constant.DataTickets; +import software.bernie.geckolib.model.GeoModel; + +import java.util.Map; + +public class MoganopterusModel extends GeoModel { + + private static final Map LOCATION_BY_VARIANT = + Util.make(Maps.newEnumMap(MoganopterusVariant.class), map -> { + map.put(MoganopterusVariant.MALE, Constants.rl("textures/entity/moganopterus.png")); + map.put(MoganopterusVariant.FEMALE, Constants.rl("textures/entity/moganopterus_female.png")); + }); + + // Model-local "currently applied" offsets; cleared before each entity render + private float[] appliedYaw = null; + private float[] appliedRoll = null; + + @Override + public ResourceLocation getModelResource(MoganopterusEntity animatable) { + return Constants.rl("geo/moganopterus.geo.json"); + } + + @Override + public ResourceLocation getTextureResource(MoganopterusEntity animatable) { + return LOCATION_BY_VARIANT.get(animatable.getVariant()); + } + + @Override + public ResourceLocation getAnimationResource(MoganopterusEntity animatable) { + return Constants.rl("animations/moganopterus.animation.json"); + } + + @Override + public void setCustomAnimations(MoganopterusEntity entity, long id, AnimationState state) { + super.setCustomAnimations(entity, id, state); + + String[] tailBones = { "Tail1", "Tail2", "Tail3" }; + int n = tailBones.length; + + if (appliedYaw == null || appliedYaw.length != n) { + appliedYaw = new float[n]; + appliedRoll = new float[n]; + } + + // 1) Clear previous offsets (from the last entity rendered with this model instance) + for (int i = 0; i < n; i++) { + if (appliedYaw[i] == 0.0f && appliedRoll[i] == 0.0f) continue; + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + if (appliedYaw[i] != 0.0f) bone.setRotY(bone.getRotY() - appliedYaw[i]); + if (appliedRoll[i] != 0.0f) bone.setRotZ(bone.getRotZ() - appliedRoll[i]); + appliedYaw[i] = 0.0f; + appliedRoll[i] = 0.0f; + } + + // 2) Interpolated sway for extra smoothness between ticks + float sway = entity.getTailSwayOffset(state.getPartialTick()); // [-1, 1] + + // Tuning + float maxYawDeg = 22.0f; // increased max sweep + float swayGain = 1.35f; // amplifies overall power + float rollFraction = 0.40f; // slightly stronger roll for heft + + float deg2rad = (float)Math.PI / 180f; + + // Direction: positive sway (left turn) -> tail swings right (negative yaw) + // Flip the sign here if the sway feels inverted + float baseYaw = sway * maxYawDeg * deg2rad; + float baseRoll = -baseYaw * rollFraction; + + float[] weights = { 1.00f, 0.78f, 0.58f }; + + for (int i = 0; i < n; i++) { + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + + float w = weights[i]; + float yaw = baseYaw * w; + float roll = baseRoll * w; + + // OVERRIDE animations on Y/Z only: keep the model's predefined X bend intact + // Do NOT reset rotX here, so the upward bend stays + bone.setRotY(yaw); + bone.setRotZ(roll); + + appliedYaw[i] = yaw; + appliedRoll[i] = roll; + } + + GeoBone head = (GeoBone) getAnimationProcessor().getBone("Neck1"); + + if (head != null) { + var entityData = state.getData(DataTickets.ENTITY_MODEL_DATA); + float clampedYawDeg = Mth.clamp(entityData.netHeadYaw(), -20.0f, 20.0f); + head.setRotY(clampedYawDeg * Mth.DEG_TO_RAD); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/MoganopterusRenderer.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/MoganopterusRenderer.java new file mode 100644 index 0000000..c622d4a --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/MoganopterusRenderer.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.cmr.jurassicrevived.entity.custom.MoganopterusEntity; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.util.Mth; +import software.bernie.geckolib.cache.object.BakedGeoModel; +import software.bernie.geckolib.renderer.GeoEntityRenderer; + + +public class MoganopterusRenderer extends GeoEntityRenderer { + private final float animalScale = 0.75F; + public MoganopterusRenderer(EntityRendererProvider.Context renderManager) { + super(renderManager, new MoganopterusModel()); + } + + @Override + public void scaleModelForRender(float widthScale, float heightScale, PoseStack poseStack, MoganopterusEntity animatable, BakedGeoModel model, boolean isReRender, float partialTick, int packedLight, int packedOverlay) { + poseStack.scale(animalScale, animalScale, animalScale); + if(animatable.isBaby()) { + float growthProgress = Mth.clamp((24000.0F + animatable.getSyncedAge()) / 24000.0F, 0.0F, 1.0F); + float scale = Mth.lerp(growthProgress, 0.2F, 1.0F); + poseStack.scale(scale, scale, scale); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/MoganopterusVariant.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/MoganopterusVariant.java new file mode 100644 index 0000000..4a483c8 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/MoganopterusVariant.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import java.util.Arrays; +import java.util.Comparator; + +public enum MoganopterusVariant { + MALE(0), + FEMALE(1); + + private static final MoganopterusVariant[] BY_ID = Arrays.stream(values()).sorted( + Comparator.comparingInt(MoganopterusVariant::getId)).toArray(MoganopterusVariant[]::new); + + private final int id; + + MoganopterusVariant(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static MoganopterusVariant byId(int id) { + return BY_ID[id % BY_ID.length]; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/NyctosaurusModel.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/NyctosaurusModel.java new file mode 100644 index 0000000..d3e5e44 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/NyctosaurusModel.java @@ -0,0 +1,112 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.google.common.collect.Maps; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.entity.custom.NyctosaurusEntity; +import net.minecraft.Util; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animation.AnimationState; +/*?} else {*/ +/*import software.bernie.geckolib.animation.AnimationState; + *//*?}*/ +import software.bernie.geckolib.cache.object.GeoBone; +import software.bernie.geckolib.constant.DataTickets; +import software.bernie.geckolib.model.GeoModel; + +import java.util.Map; + +public class NyctosaurusModel extends GeoModel { + + private static final Map LOCATION_BY_VARIANT = + Util.make(Maps.newEnumMap(NyctosaurusVariant.class), map -> { + map.put(NyctosaurusVariant.MALE, Constants.rl("textures/entity/nyctosaurus.png")); + map.put(NyctosaurusVariant.FEMALE, Constants.rl("textures/entity/nyctosaurus_female.png")); + }); + + // Model-local "currently applied" offsets; cleared before each entity render + private float[] appliedYaw = null; + private float[] appliedRoll = null; + + @Override + public ResourceLocation getModelResource(NyctosaurusEntity animatable) { + return Constants.rl("geo/nyctosaurus.geo.json"); + } + + @Override + public ResourceLocation getTextureResource(NyctosaurusEntity animatable) { + return LOCATION_BY_VARIANT.get(animatable.getVariant()); + } + + @Override + public ResourceLocation getAnimationResource(NyctosaurusEntity animatable) { + return Constants.rl("animations/nyctosaurus.animation.json"); + } + + @Override + public void setCustomAnimations(NyctosaurusEntity entity, long id, AnimationState state) { + super.setCustomAnimations(entity, id, state); + + String[] tailBones = { "Tail1", "Tail2", "Tail3" }; + int n = tailBones.length; + + if (appliedYaw == null || appliedYaw.length != n) { + appliedYaw = new float[n]; + appliedRoll = new float[n]; + } + + // 1) Clear previous offsets (from the last entity rendered with this model instance) + for (int i = 0; i < n; i++) { + if (appliedYaw[i] == 0.0f && appliedRoll[i] == 0.0f) continue; + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + if (appliedYaw[i] != 0.0f) bone.setRotY(bone.getRotY() - appliedYaw[i]); + if (appliedRoll[i] != 0.0f) bone.setRotZ(bone.getRotZ() - appliedRoll[i]); + appliedYaw[i] = 0.0f; + appliedRoll[i] = 0.0f; + } + + // 2) Interpolated sway for extra smoothness between ticks + float sway = entity.getTailSwayOffset(state.getPartialTick()); // [-1, 1] + + // Tuning + float maxYawDeg = 22.0f; // increased max sweep + float swayGain = 1.35f; // amplifies overall power + float rollFraction = 0.40f; // slightly stronger roll for heft + + float deg2rad = (float)Math.PI / 180f; + + // Direction: positive sway (left turn) -> tail swings right (negative yaw) + // Flip the sign here if the sway feels inverted + float baseYaw = sway * maxYawDeg * deg2rad; + float baseRoll = -baseYaw * rollFraction; + + float[] weights = { 1.00f, 0.78f, 0.58f }; + + for (int i = 0; i < n; i++) { + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + + float w = weights[i]; + float yaw = baseYaw * w; + float roll = baseRoll * w; + + // OVERRIDE animations on Y/Z only: keep the model's predefined X bend intact + // Do NOT reset rotX here, so the upward bend stays + bone.setRotY(yaw); + bone.setRotZ(roll); + + appliedYaw[i] = yaw; + appliedRoll[i] = roll; + } + + GeoBone head = (GeoBone) getAnimationProcessor().getBone("Neck1"); + + if (head != null) { + var entityData = state.getData(DataTickets.ENTITY_MODEL_DATA); + float clampedYawDeg = Mth.clamp(entityData.netHeadYaw(), -20.0f, 20.0f); + head.setRotY(clampedYawDeg * Mth.DEG_TO_RAD); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/NyctosaurusRenderer.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/NyctosaurusRenderer.java new file mode 100644 index 0000000..f6c26cb --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/NyctosaurusRenderer.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.cmr.jurassicrevived.entity.custom.NyctosaurusEntity; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.util.Mth; +import software.bernie.geckolib.cache.object.BakedGeoModel; +import software.bernie.geckolib.renderer.GeoEntityRenderer; + + +public class NyctosaurusRenderer extends GeoEntityRenderer { + private final float animalScale = 0.45F; + public NyctosaurusRenderer(EntityRendererProvider.Context renderManager) { + super(renderManager, new NyctosaurusModel()); + } + + @Override + public void scaleModelForRender(float widthScale, float heightScale, PoseStack poseStack, NyctosaurusEntity animatable, BakedGeoModel model, boolean isReRender, float partialTick, int packedLight, int packedOverlay) { + poseStack.scale(animalScale, animalScale, animalScale); + if(animatable.isBaby()) { + float growthProgress = Mth.clamp((24000.0F + animatable.getSyncedAge()) / 24000.0F, 0.0F, 1.0F); + float scale = Mth.lerp(growthProgress, 0.2F, 1.0F); + poseStack.scale(scale, scale, scale); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/NyctosaurusVariant.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/NyctosaurusVariant.java new file mode 100644 index 0000000..bc7bb4a --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/NyctosaurusVariant.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import java.util.Arrays; +import java.util.Comparator; + +public enum NyctosaurusVariant { + MALE(0), + FEMALE(1); + + private static final NyctosaurusVariant[] BY_ID = Arrays.stream(values()).sorted( + Comparator.comparingInt(NyctosaurusVariant::getId)).toArray(NyctosaurusVariant[]::new); + + private final int id; + + NyctosaurusVariant(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static NyctosaurusVariant byId(int id) { + return BY_ID[id % BY_ID.length]; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/OrnitholestesModel.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/OrnitholestesModel.java new file mode 100644 index 0000000..c935e1f --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/OrnitholestesModel.java @@ -0,0 +1,112 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.google.common.collect.Maps; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.entity.custom.OrnitholestesEntity; +import net.minecraft.Util; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animation.AnimationState; +/*?} else {*/ +/*import software.bernie.geckolib.animation.AnimationState; + *//*?}*/ +import software.bernie.geckolib.cache.object.GeoBone; +import software.bernie.geckolib.constant.DataTickets; +import software.bernie.geckolib.model.GeoModel; + +import java.util.Map; + +public class OrnitholestesModel extends GeoModel { + + private static final Map LOCATION_BY_VARIANT = + Util.make(Maps.newEnumMap(OrnitholestesVariant.class), map -> { + map.put(OrnitholestesVariant.MALE, Constants.rl("textures/entity/ornitholestes.png")); + map.put(OrnitholestesVariant.FEMALE, Constants.rl("textures/entity/ornitholestes_female.png")); + }); + + // Model-local "currently applied" offsets; cleared before each entity render + private float[] appliedYaw = null; + private float[] appliedRoll = null; + + @Override + public ResourceLocation getModelResource(OrnitholestesEntity animatable) { + return Constants.rl("geo/ornitholestes.geo.json"); + } + + @Override + public ResourceLocation getTextureResource(OrnitholestesEntity animatable) { + return LOCATION_BY_VARIANT.get(animatable.getVariant()); + } + + @Override + public ResourceLocation getAnimationResource(OrnitholestesEntity animatable) { + return Constants.rl("animations/ornitholestes.animation.json"); + } + + @Override + public void setCustomAnimations(OrnitholestesEntity entity, long id, AnimationState state) { + super.setCustomAnimations(entity, id, state); + + String[] tailBones = { "TailBASE", "Tail2", "Tail3", "Tail4", "Tail5" }; + int n = tailBones.length; + + if (appliedYaw == null || appliedYaw.length != n) { + appliedYaw = new float[n]; + appliedRoll = new float[n]; + } + + // 1) Clear previous offsets (from the last entity rendered with this model instance) + for (int i = 0; i < n; i++) { + if (appliedYaw[i] == 0.0f && appliedRoll[i] == 0.0f) continue; + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + if (appliedYaw[i] != 0.0f) bone.setRotY(bone.getRotY() - appliedYaw[i]); + if (appliedRoll[i] != 0.0f) bone.setRotZ(bone.getRotZ() - appliedRoll[i]); + appliedYaw[i] = 0.0f; + appliedRoll[i] = 0.0f; + } + + // 2) Interpolated sway for extra smoothness between ticks + float sway = entity.getTailSwayOffset(state.getPartialTick()); // [-1, 1] + + // Tuning + float maxYawDeg = 22.0f; // increased max sweep + float swayGain = 1.35f; // amplifies overall power + float rollFraction = 0.40f; // slightly stronger roll for heft + + float deg2rad = (float)Math.PI / 180f; + + // Direction: positive sway (left turn) -> tail swings right (negative yaw) + // Flip the sign here if the sway feels inverted + float baseYaw = sway * maxYawDeg * deg2rad; + float baseRoll = -baseYaw * rollFraction; + + float[] weights = { 1.00f, 0.78f, 0.58f, 0.42f, 0.30f}; + + for (int i = 0; i < n; i++) { + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + + float w = weights[i]; + float yaw = baseYaw * w; + float roll = baseRoll * w; + + // OVERRIDE animations on Y/Z only: keep the model's predefined X bend intact + // Do NOT reset rotX here, so the upward bend stays + bone.setRotY(yaw); + bone.setRotZ(roll); + + appliedYaw[i] = yaw; + appliedRoll[i] = roll; + } + + GeoBone head = (GeoBone) getAnimationProcessor().getBone("NeckBase"); + + if (head != null) { + var entityData = state.getData(DataTickets.ENTITY_MODEL_DATA); + float clampedYawDeg = Mth.clamp(entityData.netHeadYaw(), -30.0f, 30.0f); + head.setRotY(clampedYawDeg * Mth.DEG_TO_RAD); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/OrnitholestesRenderer.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/OrnitholestesRenderer.java new file mode 100644 index 0000000..c349e66 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/OrnitholestesRenderer.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.cmr.jurassicrevived.entity.custom.OrnitholestesEntity; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.util.Mth; +import software.bernie.geckolib.cache.object.BakedGeoModel; +import software.bernie.geckolib.renderer.GeoEntityRenderer; + + +public class OrnitholestesRenderer extends GeoEntityRenderer { + private final float animalScale = 1.0F; + public OrnitholestesRenderer(EntityRendererProvider.Context renderManager) { + super(renderManager, new OrnitholestesModel()); + } + + @Override + public void scaleModelForRender(float widthScale, float heightScale, PoseStack poseStack, OrnitholestesEntity animatable, BakedGeoModel model, boolean isReRender, float partialTick, int packedLight, int packedOverlay) { + poseStack.scale(animalScale, animalScale, animalScale); + if(animatable.isBaby()) { + float growthProgress = Mth.clamp((24000.0F + animatable.getSyncedAge()) / 24000.0F, 0.0F, 1.0F); + float scale = Mth.lerp(growthProgress, 0.2F, 1.0F); + poseStack.scale(scale, scale, scale); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/OrnitholestesVariant.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/OrnitholestesVariant.java new file mode 100644 index 0000000..2891a77 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/OrnitholestesVariant.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import java.util.Arrays; +import java.util.Comparator; + +public enum OrnitholestesVariant { + MALE(0), + FEMALE(1); + + private static final OrnitholestesVariant[] BY_ID = Arrays.stream(values()).sorted( + Comparator.comparingInt(OrnitholestesVariant::getId)).toArray(OrnitholestesVariant[]::new); + + private final int id; + + OrnitholestesVariant(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static OrnitholestesVariant byId(int id) { + return BY_ID[id % BY_ID.length]; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/OrnithomimusModel.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/OrnithomimusModel.java new file mode 100644 index 0000000..27b0862 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/OrnithomimusModel.java @@ -0,0 +1,112 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.google.common.collect.Maps; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.entity.custom.OrnithomimusEntity; +import net.minecraft.Util; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animation.AnimationState; +/*?} else {*/ +/*import software.bernie.geckolib.animation.AnimationState; + *//*?}*/ +import software.bernie.geckolib.cache.object.GeoBone; +import software.bernie.geckolib.constant.DataTickets; +import software.bernie.geckolib.model.GeoModel; + +import java.util.Map; + +public class OrnithomimusModel extends GeoModel { + + private static final Map LOCATION_BY_VARIANT = + Util.make(Maps.newEnumMap(OrnithomimusVariant.class), map -> { + map.put(OrnithomimusVariant.MALE, Constants.rl("textures/entity/ornithomimus.png")); + map.put(OrnithomimusVariant.FEMALE, Constants.rl("textures/entity/ornithomimus_female.png")); + }); + + // Model-local "currently applied" offsets; cleared before each entity render + private float[] appliedYaw = null; + private float[] appliedRoll = null; + + @Override + public ResourceLocation getModelResource(OrnithomimusEntity animatable) { + return Constants.rl("geo/ornithomimus.geo.json"); + } + + @Override + public ResourceLocation getTextureResource(OrnithomimusEntity animatable) { + return LOCATION_BY_VARIANT.get(animatable.getVariant()); + } + + @Override + public ResourceLocation getAnimationResource(OrnithomimusEntity animatable) { + return Constants.rl("animations/ornithomimus.animation.json"); + } + + @Override + public void setCustomAnimations(OrnithomimusEntity entity, long id, AnimationState state) { + super.setCustomAnimations(entity, id, state); + + String[] tailBones = { "tail1", "tail2", "tail3", "tail4", "tail5", "tail6" }; + int n = tailBones.length; + + if (appliedYaw == null || appliedYaw.length != n) { + appliedYaw = new float[n]; + appliedRoll = new float[n]; + } + + // 1) Clear previous offsets (from the last entity rendered with this model instance) + for (int i = 0; i < n; i++) { + if (appliedYaw[i] == 0.0f && appliedRoll[i] == 0.0f) continue; + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + if (appliedYaw[i] != 0.0f) bone.setRotY(bone.getRotY() - appliedYaw[i]); + if (appliedRoll[i] != 0.0f) bone.setRotZ(bone.getRotZ() - appliedRoll[i]); + appliedYaw[i] = 0.0f; + appliedRoll[i] = 0.0f; + } + + // 2) Interpolated sway for extra smoothness between ticks + float sway = entity.getTailSwayOffset(state.getPartialTick()); // [-1, 1] + + // Tuning + float maxYawDeg = 22.0f; // increased max sweep + float swayGain = 1.35f; // amplifies overall power + float rollFraction = 0.40f; // slightly stronger roll for heft + + float deg2rad = (float)Math.PI / 180f; + + // Direction: positive sway (left turn) -> tail swings right (negative yaw) + // Flip the sign here if the sway feels inverted + float baseYaw = sway * maxYawDeg * deg2rad; + float baseRoll = -baseYaw * rollFraction; + + float[] weights = { 1.00f, 0.78f, 0.58f, 0.42f, 0.30f, 0.22f }; + + for (int i = 0; i < n; i++) { + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + + float w = weights[i]; + float yaw = baseYaw * w; + float roll = baseRoll * w; + + // OVERRIDE animations on Y/Z only: keep the model's predefined X bend intact + // Do NOT reset rotX here, so the upward bend stays + bone.setRotY(yaw); + bone.setRotZ(roll); + + appliedYaw[i] = yaw; + appliedRoll[i] = roll; + } + + GeoBone head = (GeoBone) getAnimationProcessor().getBone("body1"); + + if (head != null) { + var entityData = state.getData(DataTickets.ENTITY_MODEL_DATA); + float clampedYawDeg = Mth.clamp(entityData.netHeadYaw(), -30.0f, 30.0f); + head.setRotY(clampedYawDeg * Mth.DEG_TO_RAD); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/OrnithomimusRenderer.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/OrnithomimusRenderer.java new file mode 100644 index 0000000..17480eb --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/OrnithomimusRenderer.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.cmr.jurassicrevived.entity.custom.OrnithomimusEntity; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.util.Mth; +import software.bernie.geckolib.cache.object.BakedGeoModel; +import software.bernie.geckolib.renderer.GeoEntityRenderer; + + +public class OrnithomimusRenderer extends GeoEntityRenderer { + private final float animalScale = 1.0F; + public OrnithomimusRenderer(EntityRendererProvider.Context renderManager) { + super(renderManager, new OrnithomimusModel()); + } + + @Override + public void scaleModelForRender(float widthScale, float heightScale, PoseStack poseStack, OrnithomimusEntity animatable, BakedGeoModel model, boolean isReRender, float partialTick, int packedLight, int packedOverlay) { + poseStack.scale(animalScale, animalScale, animalScale); + if(animatable.isBaby()) { + float growthProgress = Mth.clamp((24000.0F + animatable.getSyncedAge()) / 24000.0F, 0.0F, 1.0F); + float scale = Mth.lerp(growthProgress, 0.2F, 1.0F); + poseStack.scale(scale, scale, scale); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/OrnithomimusVariant.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/OrnithomimusVariant.java new file mode 100644 index 0000000..b80c93c --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/OrnithomimusVariant.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import java.util.Arrays; +import java.util.Comparator; + +public enum OrnithomimusVariant { + MALE(0), + FEMALE(1); + + private static final OrnithomimusVariant[] BY_ID = Arrays.stream(values()).sorted( + Comparator.comparingInt(OrnithomimusVariant::getId)).toArray(OrnithomimusVariant[]::new); + + private final int id; + + OrnithomimusVariant(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static OrnithomimusVariant byId(int id) { + return BY_ID[id % BY_ID.length]; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/OuranosaurusModel.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/OuranosaurusModel.java new file mode 100644 index 0000000..20bba05 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/OuranosaurusModel.java @@ -0,0 +1,112 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.google.common.collect.Maps; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.entity.custom.OuranosaurusEntity; +import net.minecraft.Util; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animation.AnimationState; +/*?} else {*/ +/*import software.bernie.geckolib.animation.AnimationState; + *//*?}*/ +import software.bernie.geckolib.cache.object.GeoBone; +import software.bernie.geckolib.constant.DataTickets; +import software.bernie.geckolib.model.GeoModel; + +import java.util.Map; + +public class OuranosaurusModel extends GeoModel { + + private static final Map LOCATION_BY_VARIANT = + Util.make(Maps.newEnumMap(OuranosaurusVariant.class), map -> { + map.put(OuranosaurusVariant.MALE, Constants.rl("textures/entity/ouranosaurus.png")); + map.put(OuranosaurusVariant.FEMALE, Constants.rl("textures/entity/ouranosaurus_female.png")); + }); + + // Model-local "currently applied" offsets; cleared before each entity render + private float[] appliedYaw = null; + private float[] appliedRoll = null; + + @Override + public ResourceLocation getModelResource(OuranosaurusEntity animatable) { + return Constants.rl("geo/ouranosaurus.geo.json"); + } + + @Override + public ResourceLocation getTextureResource(OuranosaurusEntity animatable) { + return LOCATION_BY_VARIANT.get(animatable.getVariant()); + } + + @Override + public ResourceLocation getAnimationResource(OuranosaurusEntity animatable) { + return Constants.rl("animations/ouranosaurus.animation.json"); + } + + @Override + public void setCustomAnimations(OuranosaurusEntity entity, long id, AnimationState state) { + super.setCustomAnimations(entity, id, state); + + String[] tailBones = { "Tail1", "Tail2", "Tail3", "Tail4", "Tail5", "Tail6", "Tail7" }; + int n = tailBones.length; + + if (appliedYaw == null || appliedYaw.length != n) { + appliedYaw = new float[n]; + appliedRoll = new float[n]; + } + + // 1) Clear previous offsets (from the last entity rendered with this model instance) + for (int i = 0; i < n; i++) { + if (appliedYaw[i] == 0.0f && appliedRoll[i] == 0.0f) continue; + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + if (appliedYaw[i] != 0.0f) bone.setRotY(bone.getRotY() - appliedYaw[i]); + if (appliedRoll[i] != 0.0f) bone.setRotZ(bone.getRotZ() - appliedRoll[i]); + appliedYaw[i] = 0.0f; + appliedRoll[i] = 0.0f; + } + + // 2) Interpolated sway for extra smoothness between ticks + float sway = entity.getTailSwayOffset(state.getPartialTick()); // [-1, 1] + + // Tuning + float maxYawDeg = 22.0f; // increased max sweep + float swayGain = 1.35f; // amplifies overall power + float rollFraction = 0.40f; // slightly stronger roll for heft + + float deg2rad = (float)Math.PI / 180f; + + // Direction: positive sway (left turn) -> tail swings right (negative yaw) + // Flip the sign here if the sway feels inverted + float baseYaw = sway * maxYawDeg * deg2rad; + float baseRoll = -baseYaw * rollFraction; + + float[] weights = { 1.00f, 0.78f, 0.58f, 0.42f, 0.30f, 0.22f, 0.16f }; + + for (int i = 0; i < n; i++) { + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + + float w = weights[i]; + float yaw = baseYaw * w; + float roll = baseRoll * w; + + // OVERRIDE animations on Y/Z only: keep the model's predefined X bend intact + // Do NOT reset rotX here, so the upward bend stays + bone.setRotY(yaw); + bone.setRotZ(roll); + + appliedYaw[i] = yaw; + appliedRoll[i] = roll; + } + + GeoBone head = (GeoBone) getAnimationProcessor().getBone("Neck2"); + + if (head != null) { + var entityData = state.getData(DataTickets.ENTITY_MODEL_DATA); + float clampedYawDeg = Mth.clamp(entityData.netHeadYaw(), -10.0f, 10.0f); + head.setRotY(clampedYawDeg * Mth.DEG_TO_RAD); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/OuranosaurusRenderer.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/OuranosaurusRenderer.java new file mode 100644 index 0000000..2557a26 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/OuranosaurusRenderer.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.cmr.jurassicrevived.entity.custom.OuranosaurusEntity; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.util.Mth; +import software.bernie.geckolib.cache.object.BakedGeoModel; +import software.bernie.geckolib.renderer.GeoEntityRenderer; + + +public class OuranosaurusRenderer extends GeoEntityRenderer { + private final float animalScale = 1.0F; + public OuranosaurusRenderer(EntityRendererProvider.Context renderManager) { + super(renderManager, new OuranosaurusModel()); + } + + @Override + public void scaleModelForRender(float widthScale, float heightScale, PoseStack poseStack, OuranosaurusEntity animatable, BakedGeoModel model, boolean isReRender, float partialTick, int packedLight, int packedOverlay) { + poseStack.scale(animalScale, animalScale, animalScale); + if(animatable.isBaby()) { + float growthProgress = Mth.clamp((24000.0F + animatable.getSyncedAge()) / 24000.0F, 0.0F, 1.0F); + float scale = Mth.lerp(growthProgress, 0.2F, 1.0F); + poseStack.scale(scale, scale, scale); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/OuranosaurusVariant.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/OuranosaurusVariant.java new file mode 100644 index 0000000..1bbb9d3 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/OuranosaurusVariant.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import java.util.Arrays; +import java.util.Comparator; + +public enum OuranosaurusVariant { + MALE(0), + FEMALE(1); + + private static final OuranosaurusVariant[] BY_ID = Arrays.stream(values()).sorted( + Comparator.comparingInt(OuranosaurusVariant::getId)).toArray(OuranosaurusVariant[]::new); + + private final int id; + + OuranosaurusVariant(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static OuranosaurusVariant byId(int id) { + return BY_ID[id % BY_ID.length]; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/OviraptorModel.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/OviraptorModel.java new file mode 100644 index 0000000..abc2709 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/OviraptorModel.java @@ -0,0 +1,112 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.google.common.collect.Maps; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.entity.custom.OviraptorEntity; +import net.minecraft.Util; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animation.AnimationState; +/*?} else {*/ +/*import software.bernie.geckolib.animation.AnimationState; + *//*?}*/ +import software.bernie.geckolib.cache.object.GeoBone; +import software.bernie.geckolib.constant.DataTickets; +import software.bernie.geckolib.model.GeoModel; + +import java.util.Map; + +public class OviraptorModel extends GeoModel { + + private static final Map LOCATION_BY_VARIANT = + Util.make(Maps.newEnumMap(OviraptorVariant.class), map -> { + map.put(OviraptorVariant.MALE, Constants.rl("textures/entity/oviraptor.png")); + map.put(OviraptorVariant.FEMALE, Constants.rl("textures/entity/oviraptor_female.png")); + }); + + // Model-local "currently applied" offsets; cleared before each entity render + private float[] appliedYaw = null; + private float[] appliedRoll = null; + + @Override + public ResourceLocation getModelResource(OviraptorEntity animatable) { + return Constants.rl("geo/oviraptor.geo.json"); + } + + @Override + public ResourceLocation getTextureResource(OviraptorEntity animatable) { + return LOCATION_BY_VARIANT.get(animatable.getVariant()); + } + + @Override + public ResourceLocation getAnimationResource(OviraptorEntity animatable) { + return Constants.rl("animations/oviraptor.animation.json"); + } + + @Override + public void setCustomAnimations(OviraptorEntity entity, long id, AnimationState state) { + super.setCustomAnimations(entity, id, state); + + String[] tailBones = { "TailBase", "TailMid1", "TailMid2", "TailTip" }; + int n = tailBones.length; + + if (appliedYaw == null || appliedYaw.length != n) { + appliedYaw = new float[n]; + appliedRoll = new float[n]; + } + + // 1) Clear previous offsets (from the last entity rendered with this model instance) + for (int i = 0; i < n; i++) { + if (appliedYaw[i] == 0.0f && appliedRoll[i] == 0.0f) continue; + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + if (appliedYaw[i] != 0.0f) bone.setRotY(bone.getRotY() - appliedYaw[i]); + if (appliedRoll[i] != 0.0f) bone.setRotZ(bone.getRotZ() - appliedRoll[i]); + appliedYaw[i] = 0.0f; + appliedRoll[i] = 0.0f; + } + + // 2) Interpolated sway for extra smoothness between ticks + float sway = entity.getTailSwayOffset(state.getPartialTick()); // [-1, 1] + + // Tuning + float maxYawDeg = 22.0f; // increased max sweep + float swayGain = 1.35f; // amplifies overall power + float rollFraction = 0.40f; // slightly stronger roll for heft + + float deg2rad = (float)Math.PI / 180f; + + // Direction: positive sway (left turn) -> tail swings right (negative yaw) + // Flip the sign here if the sway feels inverted + float baseYaw = sway * maxYawDeg * deg2rad; + float baseRoll = -baseYaw * rollFraction; + + float[] weights = { 1.00f, 0.78f, 0.58f, 0.42f }; + + for (int i = 0; i < n; i++) { + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + + float w = weights[i]; + float yaw = baseYaw * w; + float roll = baseRoll * w; + + // OVERRIDE animations on Y/Z only: keep the model's predefined X bend intact + // Do NOT reset rotX here, so the upward bend stays + bone.setRotY(yaw); + bone.setRotZ(roll); + + appliedYaw[i] = yaw; + appliedRoll[i] = roll; + } + + GeoBone head = (GeoBone) getAnimationProcessor().getBone("NeckBase"); + + if (head != null) { + var entityData = state.getData(DataTickets.ENTITY_MODEL_DATA); + float clampedYawDeg = Mth.clamp(entityData.netHeadYaw(), -30.0f, 30.0f); + head.setRotY(clampedYawDeg * Mth.DEG_TO_RAD); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/OviraptorRenderer.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/OviraptorRenderer.java new file mode 100644 index 0000000..80f6e38 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/OviraptorRenderer.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.cmr.jurassicrevived.entity.custom.OviraptorEntity; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.util.Mth; +import software.bernie.geckolib.cache.object.BakedGeoModel; +import software.bernie.geckolib.renderer.GeoEntityRenderer; + + +public class OviraptorRenderer extends GeoEntityRenderer { + private final float animalScale = 0.55F; + public OviraptorRenderer(EntityRendererProvider.Context renderManager) { + super(renderManager, new OviraptorModel()); + } + + @Override + public void scaleModelForRender(float widthScale, float heightScale, PoseStack poseStack, OviraptorEntity animatable, BakedGeoModel model, boolean isReRender, float partialTick, int packedLight, int packedOverlay) { + poseStack.scale(animalScale, animalScale, animalScale); + if(animatable.isBaby()) { + float growthProgress = Mth.clamp((24000.0F + animatable.getSyncedAge()) / 24000.0F, 0.0F, 1.0F); + float scale = Mth.lerp(growthProgress, 0.2F, 1.0F); + poseStack.scale(scale, scale, scale); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/OviraptorVariant.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/OviraptorVariant.java new file mode 100644 index 0000000..2aa9ff6 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/OviraptorVariant.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import java.util.Arrays; +import java.util.Comparator; + +public enum OviraptorVariant { + MALE(0), + FEMALE(1); + + private static final OviraptorVariant[] BY_ID = Arrays.stream(values()).sorted( + Comparator.comparingInt(OviraptorVariant::getId)).toArray(OviraptorVariant[]::new); + + private final int id; + + OviraptorVariant(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static OviraptorVariant byId(int id) { + return BY_ID[id % BY_ID.length]; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/PachycephalosaurusModel.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/PachycephalosaurusModel.java new file mode 100644 index 0000000..1325b33 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/PachycephalosaurusModel.java @@ -0,0 +1,112 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.google.common.collect.Maps; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.entity.custom.PachycephalosaurusEntity; +import net.minecraft.Util; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animation.AnimationState; +/*?} else {*/ +/*import software.bernie.geckolib.animation.AnimationState; + *//*?}*/ +import software.bernie.geckolib.cache.object.GeoBone; +import software.bernie.geckolib.constant.DataTickets; +import software.bernie.geckolib.model.GeoModel; + +import java.util.Map; + +public class PachycephalosaurusModel extends GeoModel { + + private static final Map LOCATION_BY_VARIANT = + Util.make(Maps.newEnumMap(PachycephalosaurusVariant.class), map -> { + map.put(PachycephalosaurusVariant.MALE, Constants.rl("textures/entity/pachycephalosaurus.png")); + map.put(PachycephalosaurusVariant.FEMALE, Constants.rl("textures/entity/pachycephalosaurus_female.png")); + }); + + // Model-local "currently applied" offsets; cleared before each entity render + private float[] appliedYaw = null; + private float[] appliedRoll = null; + + @Override + public ResourceLocation getModelResource(PachycephalosaurusEntity animatable) { + return Constants.rl("geo/pachycephalosaurus.geo.json"); + } + + @Override + public ResourceLocation getTextureResource(PachycephalosaurusEntity animatable) { + return LOCATION_BY_VARIANT.get(animatable.getVariant()); + } + + @Override + public ResourceLocation getAnimationResource(PachycephalosaurusEntity animatable) { + return Constants.rl("animations/pachycephalosaurus.animation.json"); + } + + @Override + public void setCustomAnimations(PachycephalosaurusEntity entity, long id, AnimationState state) { + super.setCustomAnimations(entity, id, state); + + String[] tailBones = { "Tail1", "Tail2", "Tail3", "Tail4", "Tail5", "Tail6", "Tail7" }; + int n = tailBones.length; + + if (appliedYaw == null || appliedYaw.length != n) { + appliedYaw = new float[n]; + appliedRoll = new float[n]; + } + + // 1) Clear previous offsets (from the last entity rendered with this model instance) + for (int i = 0; i < n; i++) { + if (appliedYaw[i] == 0.0f && appliedRoll[i] == 0.0f) continue; + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + if (appliedYaw[i] != 0.0f) bone.setRotY(bone.getRotY() - appliedYaw[i]); + if (appliedRoll[i] != 0.0f) bone.setRotZ(bone.getRotZ() - appliedRoll[i]); + appliedYaw[i] = 0.0f; + appliedRoll[i] = 0.0f; + } + + // 2) Interpolated sway for extra smoothness between ticks + float sway = entity.getTailSwayOffset(state.getPartialTick()); // [-1, 1] + + // Tuning + float maxYawDeg = 22.0f; // increased max sweep + float swayGain = 1.35f; // amplifies overall power + float rollFraction = 0.40f; // slightly stronger roll for heft + + float deg2rad = (float)Math.PI / 180f; + + // Direction: positive sway (left turn) -> tail swings right (negative yaw) + // Flip the sign here if the sway feels inverted + float baseYaw = sway * maxYawDeg * deg2rad; + float baseRoll = -baseYaw * rollFraction; + + float[] weights = { 1.00f, 0.78f, 0.58f, 0.42f, 0.30f, 0.22f, 0.16f }; + + for (int i = 0; i < n; i++) { + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + + float w = weights[i]; + float yaw = baseYaw * w; + float roll = baseRoll * w; + + // OVERRIDE animations on Y/Z only: keep the model's predefined X bend intact + // Do NOT reset rotX here, so the upward bend stays + bone.setRotY(yaw); + bone.setRotZ(roll); + + appliedYaw[i] = yaw; + appliedRoll[i] = roll; + } + + GeoBone head = (GeoBone) getAnimationProcessor().getBone("Neck1"); + + if (head != null) { + var entityData = state.getData(DataTickets.ENTITY_MODEL_DATA); + float clampedYawDeg = Mth.clamp(entityData.netHeadYaw(), -30.0f, 30.0f); + head.setRotY(clampedYawDeg * Mth.DEG_TO_RAD); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/PachycephalosaurusRenderer.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/PachycephalosaurusRenderer.java new file mode 100644 index 0000000..bd9b507 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/PachycephalosaurusRenderer.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.cmr.jurassicrevived.entity.custom.PachycephalosaurusEntity; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.util.Mth; +import software.bernie.geckolib.cache.object.BakedGeoModel; +import software.bernie.geckolib.renderer.GeoEntityRenderer; + + +public class PachycephalosaurusRenderer extends GeoEntityRenderer { + private final float animalScale = 1.0F; + public PachycephalosaurusRenderer(EntityRendererProvider.Context renderManager) { + super(renderManager, new PachycephalosaurusModel()); + } + + @Override + public void scaleModelForRender(float widthScale, float heightScale, PoseStack poseStack, PachycephalosaurusEntity animatable, BakedGeoModel model, boolean isReRender, float partialTick, int packedLight, int packedOverlay) { + poseStack.scale(animalScale, animalScale, animalScale); + if(animatable.isBaby()) { + float growthProgress = Mth.clamp((24000.0F + animatable.getSyncedAge()) / 24000.0F, 0.0F, 1.0F); + float scale = Mth.lerp(growthProgress, 0.2F, 1.0F); + poseStack.scale(scale, scale, scale); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/PachycephalosaurusVariant.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/PachycephalosaurusVariant.java new file mode 100644 index 0000000..8bd1989 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/PachycephalosaurusVariant.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import java.util.Arrays; +import java.util.Comparator; + +public enum PachycephalosaurusVariant { + MALE(0), + FEMALE(1); + + private static final PachycephalosaurusVariant[] BY_ID = Arrays.stream(values()).sorted( + Comparator.comparingInt(PachycephalosaurusVariant::getId)).toArray(PachycephalosaurusVariant[]::new); + + private final int id; + + PachycephalosaurusVariant(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static PachycephalosaurusVariant byId(int id) { + return BY_ID[id % BY_ID.length]; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/ParasaurolophusModel.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/ParasaurolophusModel.java new file mode 100644 index 0000000..296ab00 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/ParasaurolophusModel.java @@ -0,0 +1,112 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.google.common.collect.Maps; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.entity.custom.ParasaurolophusEntity; +import net.minecraft.Util; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animation.AnimationState; +/*?} else {*/ +/*import software.bernie.geckolib.animation.AnimationState; + *//*?}*/ +import software.bernie.geckolib.cache.object.GeoBone; +import software.bernie.geckolib.constant.DataTickets; +import software.bernie.geckolib.model.GeoModel; + +import java.util.Map; + +public class ParasaurolophusModel extends GeoModel { + + private static final Map LOCATION_BY_VARIANT = + Util.make(Maps.newEnumMap(ParasaurolophusVariant.class), map -> { + map.put(ParasaurolophusVariant.MALE, Constants.rl("textures/entity/parasaurolophus.png")); + map.put(ParasaurolophusVariant.FEMALE, Constants.rl("textures/entity/parasaurolophus_female.png")); + }); + + // Model-local "currently applied" offsets; cleared before each entity render + private float[] appliedYaw = null; + private float[] appliedRoll = null; + + @Override + public ResourceLocation getModelResource(ParasaurolophusEntity animatable) { + return Constants.rl("geo/parasaurolophus.geo.json"); + } + + @Override + public ResourceLocation getTextureResource(ParasaurolophusEntity animatable) { + return LOCATION_BY_VARIANT.get(animatable.getVariant()); + } + + @Override + public ResourceLocation getAnimationResource(ParasaurolophusEntity animatable) { + return Constants.rl("animations/parasaurolophus.animation.json"); + } + + @Override + public void setCustomAnimations(ParasaurolophusEntity entity, long id, AnimationState state) { + super.setCustomAnimations(entity, id, state); + + String[] tailBones = { "Tail1", "Tail2", "Tail3", "Tail4", "Tail5", "Tail6" }; + int n = tailBones.length; + + if (appliedYaw == null || appliedYaw.length != n) { + appliedYaw = new float[n]; + appliedRoll = new float[n]; + } + + // 1) Clear previous offsets (from the last entity rendered with this model instance) + for (int i = 0; i < n; i++) { + if (appliedYaw[i] == 0.0f && appliedRoll[i] == 0.0f) continue; + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + if (appliedYaw[i] != 0.0f) bone.setRotY(bone.getRotY() - appliedYaw[i]); + if (appliedRoll[i] != 0.0f) bone.setRotZ(bone.getRotZ() - appliedRoll[i]); + appliedYaw[i] = 0.0f; + appliedRoll[i] = 0.0f; + } + + // 2) Interpolated sway for extra smoothness between ticks + float sway = entity.getTailSwayOffset(state.getPartialTick()); // [-1, 1] + + // Tuning + float maxYawDeg = 22.0f; // increased max sweep + float swayGain = 1.35f; // amplifies overall power + float rollFraction = 0.40f; // slightly stronger roll for heft + + float deg2rad = (float)Math.PI / 180f; + + // Direction: positive sway (left turn) -> tail swings right (negative yaw) + // Flip the sign here if the sway feels inverted + float baseYaw = sway * maxYawDeg * deg2rad; + float baseRoll = -baseYaw * rollFraction; + + float[] weights = { 1.00f, 0.78f, 0.58f, 0.42f, 0.30f, 0.22f }; + + for (int i = 0; i < n; i++) { + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + + float w = weights[i]; + float yaw = baseYaw * w; + float roll = baseRoll * w; + + // OVERRIDE animations on Y/Z only: keep the model's predefined X bend intact + // Do NOT reset rotX here, so the upward bend stays + bone.setRotY(yaw); + bone.setRotZ(roll); + + appliedYaw[i] = yaw; + appliedRoll[i] = roll; + } + + GeoBone head = (GeoBone) getAnimationProcessor().getBone("Neck"); + + if (head != null) { + var entityData = state.getData(DataTickets.ENTITY_MODEL_DATA); + float clampedYawDeg = Mth.clamp(entityData.netHeadYaw(), -30.0f, 30.0f); + head.setRotY(clampedYawDeg * Mth.DEG_TO_RAD); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/ParasaurolophusRenderer.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/ParasaurolophusRenderer.java new file mode 100644 index 0000000..b469df6 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/ParasaurolophusRenderer.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.cmr.jurassicrevived.entity.custom.ParasaurolophusEntity; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.util.Mth; +import software.bernie.geckolib.cache.object.BakedGeoModel; +import software.bernie.geckolib.renderer.GeoEntityRenderer; + + +public class ParasaurolophusRenderer extends GeoEntityRenderer { + private final float animalScale = 1.5F; + public ParasaurolophusRenderer(EntityRendererProvider.Context renderManager) { + super(renderManager, new ParasaurolophusModel()); + } + + @Override + public void scaleModelForRender(float widthScale, float heightScale, PoseStack poseStack, ParasaurolophusEntity animatable, BakedGeoModel model, boolean isReRender, float partialTick, int packedLight, int packedOverlay) { + poseStack.scale(animalScale, animalScale, animalScale); + if(animatable.isBaby()) { + float growthProgress = Mth.clamp((24000.0F + animatable.getSyncedAge()) / 24000.0F, 0.0F, 1.0F); + float scale = Mth.lerp(growthProgress, 0.2F, 1.0F); + poseStack.scale(scale, scale, scale); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/ParasaurolophusVariant.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/ParasaurolophusVariant.java new file mode 100644 index 0000000..231b2c2 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/ParasaurolophusVariant.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import java.util.Arrays; +import java.util.Comparator; + +public enum ParasaurolophusVariant { + MALE(0), + FEMALE(1); + + private static final ParasaurolophusVariant[] BY_ID = Arrays.stream(values()).sorted( + Comparator.comparingInt(ParasaurolophusVariant::getId)).toArray(ParasaurolophusVariant[]::new); + + private final int id; + + ParasaurolophusVariant(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static ParasaurolophusVariant byId(int id) { + return BY_ID[id % BY_ID.length]; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/ProceratosaurusModel.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/ProceratosaurusModel.java new file mode 100644 index 0000000..e928de1 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/ProceratosaurusModel.java @@ -0,0 +1,112 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.google.common.collect.Maps; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.entity.custom.ProceratosaurusEntity; +import net.minecraft.Util; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animation.AnimationState; +/*?} else {*/ +/*import software.bernie.geckolib.animation.AnimationState; + *//*?}*/ +import software.bernie.geckolib.cache.object.GeoBone; +import software.bernie.geckolib.constant.DataTickets; +import software.bernie.geckolib.model.GeoModel; + +import java.util.Map; + +public class ProceratosaurusModel extends GeoModel { + + private static final Map LOCATION_BY_VARIANT = + Util.make(Maps.newEnumMap(ProceratosaurusVariant.class), map -> { + map.put(ProceratosaurusVariant.MALE, Constants.rl("textures/entity/proceratosaurus.png")); + map.put(ProceratosaurusVariant.FEMALE, Constants.rl("textures/entity/proceratosaurus_female.png")); + }); + + // Model-local "currently applied" offsets; cleared before each entity render + private float[] appliedYaw = null; + private float[] appliedRoll = null; + + @Override + public ResourceLocation getModelResource(ProceratosaurusEntity animatable) { + return Constants.rl("geo/proceratosaurus.geo.json"); + } + + @Override + public ResourceLocation getTextureResource(ProceratosaurusEntity animatable) { + return LOCATION_BY_VARIANT.get(animatable.getVariant()); + } + + @Override + public ResourceLocation getAnimationResource(ProceratosaurusEntity animatable) { + return Constants.rl("animations/proceratosaurus.animation.json"); + } + + @Override + public void setCustomAnimations(ProceratosaurusEntity entity, long id, AnimationState state) { + super.setCustomAnimations(entity, id, state); + + String[] tailBones = { "tail1", "tail2", "tail3", "tail4", "tail5" }; + int n = tailBones.length; + + if (appliedYaw == null || appliedYaw.length != n) { + appliedYaw = new float[n]; + appliedRoll = new float[n]; + } + + // 1) Clear previous offsets (from the last entity rendered with this model instance) + for (int i = 0; i < n; i++) { + if (appliedYaw[i] == 0.0f && appliedRoll[i] == 0.0f) continue; + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + if (appliedYaw[i] != 0.0f) bone.setRotY(bone.getRotY() - appliedYaw[i]); + if (appliedRoll[i] != 0.0f) bone.setRotZ(bone.getRotZ() - appliedRoll[i]); + appliedYaw[i] = 0.0f; + appliedRoll[i] = 0.0f; + } + + // 2) Interpolated sway for extra smoothness between ticks + float sway = entity.getTailSwayOffset(state.getPartialTick()); // [-1, 1] + + // Tuning + float maxYawDeg = 22.0f; // increased max sweep + float swayGain = 1.35f; // amplifies overall power + float rollFraction = 0.40f; // slightly stronger roll for heft + + float deg2rad = (float)Math.PI / 180f; + + // Direction: positive sway (left turn) -> tail swings right (negative yaw) + // Flip the sign here if the sway feels inverted + float baseYaw = sway * maxYawDeg * deg2rad; + float baseRoll = -baseYaw * rollFraction; + + float[] weights = { 1.00f, 0.78f, 0.58f, 0.42f, 0.30f }; + + for (int i = 0; i < n; i++) { + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + + float w = weights[i]; + float yaw = baseYaw * w; + float roll = baseRoll * w; + + // OVERRIDE animations on Y/Z only: keep the model's predefined X bend intact + // Do NOT reset rotX here, so the upward bend stays + bone.setRotY(yaw); + bone.setRotZ(roll); + + appliedYaw[i] = yaw; + appliedRoll[i] = roll; + } + + GeoBone head = (GeoBone) getAnimationProcessor().getBone("neck1"); + + if (head != null) { + var entityData = state.getData(DataTickets.ENTITY_MODEL_DATA); + float clampedYawDeg = Mth.clamp(entityData.netHeadYaw(), -30.0f, 30.0f); + head.setRotY(clampedYawDeg * Mth.DEG_TO_RAD); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/ProceratosaurusRenderer.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/ProceratosaurusRenderer.java new file mode 100644 index 0000000..467c252 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/ProceratosaurusRenderer.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.cmr.jurassicrevived.entity.custom.ProceratosaurusEntity; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.util.Mth; +import software.bernie.geckolib.cache.object.BakedGeoModel; +import software.bernie.geckolib.renderer.GeoEntityRenderer; + + +public class ProceratosaurusRenderer extends GeoEntityRenderer { + private final float animalScale = 0.65F; + public ProceratosaurusRenderer(EntityRendererProvider.Context renderManager) { + super(renderManager, new ProceratosaurusModel()); + } + + @Override + public void scaleModelForRender(float widthScale, float heightScale, PoseStack poseStack, ProceratosaurusEntity animatable, BakedGeoModel model, boolean isReRender, float partialTick, int packedLight, int packedOverlay) { + poseStack.scale(animalScale, animalScale, animalScale); + if(animatable.isBaby()) { + float growthProgress = Mth.clamp((24000.0F + animatable.getSyncedAge()) / 24000.0F, 0.0F, 1.0F); + float scale = Mth.lerp(growthProgress, 0.2F, 1.0F); + poseStack.scale(scale, scale, scale); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/ProceratosaurusVariant.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/ProceratosaurusVariant.java new file mode 100644 index 0000000..dda4be4 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/ProceratosaurusVariant.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import java.util.Arrays; +import java.util.Comparator; + +public enum ProceratosaurusVariant { + MALE(0), + FEMALE(1); + + private static final ProceratosaurusVariant[] BY_ID = Arrays.stream(values()).sorted( + Comparator.comparingInt(ProceratosaurusVariant::getId)).toArray(ProceratosaurusVariant[]::new); + + private final int id; + + ProceratosaurusVariant(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static ProceratosaurusVariant byId(int id) { + return BY_ID[id % BY_ID.length]; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/ProcompsognathusModel.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/ProcompsognathusModel.java new file mode 100644 index 0000000..2ed3877 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/ProcompsognathusModel.java @@ -0,0 +1,112 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.google.common.collect.Maps; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.entity.custom.ProcompsognathusEntity; +import net.minecraft.Util; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animation.AnimationState; +/*?} else {*/ +/*import software.bernie.geckolib.animation.AnimationState; + *//*?}*/ +import software.bernie.geckolib.cache.object.GeoBone; +import software.bernie.geckolib.constant.DataTickets; +import software.bernie.geckolib.model.GeoModel; + +import java.util.Map; + +public class ProcompsognathusModel extends GeoModel { + + private static final Map LOCATION_BY_VARIANT = + Util.make(Maps.newEnumMap(ProcompsognathusVariant.class), map -> { + map.put(ProcompsognathusVariant.MALE, Constants.rl("textures/entity/procompsognathus.png")); + map.put(ProcompsognathusVariant.FEMALE, Constants.rl("textures/entity/procompsognathus_female.png")); + }); + + // Model-local "currently applied" offsets; cleared before each entity render + private float[] appliedYaw = null; + private float[] appliedRoll = null; + + @Override + public ResourceLocation getModelResource(ProcompsognathusEntity animatable) { + return Constants.rl("geo/procompsognathus.geo.json"); + } + + @Override + public ResourceLocation getTextureResource(ProcompsognathusEntity animatable) { + return LOCATION_BY_VARIANT.get(animatable.getVariant()); + } + + @Override + public ResourceLocation getAnimationResource(ProcompsognathusEntity animatable) { + return Constants.rl("animations/procompsognathus.animation.json"); + } + + @Override + public void setCustomAnimations(ProcompsognathusEntity entity, long id, AnimationState state) { + super.setCustomAnimations(entity, id, state); + + String[] tailBones = { "Tailpart1", "Tailpart2", "Tailpart3", "Tailpart4", "Tailpart5", "Tailend" }; + int n = tailBones.length; + + if (appliedYaw == null || appliedYaw.length != n) { + appliedYaw = new float[n]; + appliedRoll = new float[n]; + } + + // 1) Clear previous offsets (from the last entity rendered with this model instance) + for (int i = 0; i < n; i++) { + if (appliedYaw[i] == 0.0f && appliedRoll[i] == 0.0f) continue; + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + if (appliedYaw[i] != 0.0f) bone.setRotY(bone.getRotY() - appliedYaw[i]); + if (appliedRoll[i] != 0.0f) bone.setRotZ(bone.getRotZ() - appliedRoll[i]); + appliedYaw[i] = 0.0f; + appliedRoll[i] = 0.0f; + } + + // 2) Interpolated sway for extra smoothness between ticks + float sway = entity.getTailSwayOffset(state.getPartialTick()); // [-1, 1] + + // Tuning + float maxYawDeg = 22.0f; // increased max sweep + float swayGain = 1.35f; // amplifies overall power + float rollFraction = 0.40f; // slightly stronger roll for heft + + float deg2rad = (float)Math.PI / 180f; + + // Direction: positive sway (left turn) -> tail swings right (negative yaw) + // Flip the sign here if the sway feels inverted + float baseYaw = sway * maxYawDeg * deg2rad; + float baseRoll = -baseYaw * rollFraction; + + float[] weights = { 1.00f, 0.78f, 0.58f, 0.42f, 0.30f, 0.22f }; + + for (int i = 0; i < n; i++) { + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + + float w = weights[i]; + float yaw = baseYaw * w; + float roll = baseRoll * w; + + // OVERRIDE animations on Y/Z only: keep the model's predefined X bend intact + // Do NOT reset rotX here, so the upward bend stays + bone.setRotY(yaw); + bone.setRotZ(roll); + + appliedYaw[i] = yaw; + appliedRoll[i] = roll; + } + + GeoBone head = (GeoBone) getAnimationProcessor().getBone("Chest2"); + + if (head != null) { + var entityData = state.getData(DataTickets.ENTITY_MODEL_DATA); + float clampedYawDeg = Mth.clamp(entityData.netHeadYaw(), -20.0f, 20.0f); + head.setRotY(clampedYawDeg * Mth.DEG_TO_RAD); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/ProcompsognathusRenderer.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/ProcompsognathusRenderer.java new file mode 100644 index 0000000..7ae3b0e --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/ProcompsognathusRenderer.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.cmr.jurassicrevived.entity.custom.ProcompsognathusEntity; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.util.Mth; +import software.bernie.geckolib.cache.object.BakedGeoModel; +import software.bernie.geckolib.renderer.GeoEntityRenderer; + + +public class ProcompsognathusRenderer extends GeoEntityRenderer { + private final float animalScale = 0.44F; + public ProcompsognathusRenderer(EntityRendererProvider.Context renderManager) { + super(renderManager, new ProcompsognathusModel()); + } + + @Override + public void scaleModelForRender(float widthScale, float heightScale, PoseStack poseStack, ProcompsognathusEntity animatable, BakedGeoModel model, boolean isReRender, float partialTick, int packedLight, int packedOverlay) { + poseStack.scale(animalScale, animalScale, animalScale); + if(animatable.isBaby()) { + float growthProgress = Mth.clamp((24000.0F + animatable.getSyncedAge()) / 24000.0F, 0.0F, 1.0F); + float scale = Mth.lerp(growthProgress, 0.2F, 1.0F); + poseStack.scale(scale, scale, scale); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/ProcompsognathusVariant.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/ProcompsognathusVariant.java new file mode 100644 index 0000000..7ccfe57 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/ProcompsognathusVariant.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import java.util.Arrays; +import java.util.Comparator; + +public enum ProcompsognathusVariant { + MALE(0), + FEMALE(1); + + private static final ProcompsognathusVariant[] BY_ID = Arrays.stream(values()).sorted( + Comparator.comparingInt(ProcompsognathusVariant::getId)).toArray(ProcompsognathusVariant[]::new); + + private final int id; + + ProcompsognathusVariant(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static ProcompsognathusVariant byId(int id) { + return BY_ID[id % BY_ID.length]; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/ProtoceratopsModel.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/ProtoceratopsModel.java new file mode 100644 index 0000000..c6b1feb --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/ProtoceratopsModel.java @@ -0,0 +1,112 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.google.common.collect.Maps; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.entity.custom.ProtoceratopsEntity; +import net.minecraft.Util; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animation.AnimationState; +/*?} else {*/ +/*import software.bernie.geckolib.animation.AnimationState; + *//*?}*/ +import software.bernie.geckolib.cache.object.GeoBone; +import software.bernie.geckolib.constant.DataTickets; +import software.bernie.geckolib.model.GeoModel; + +import java.util.Map; + +public class ProtoceratopsModel extends GeoModel { + + private static final Map LOCATION_BY_VARIANT = + Util.make(Maps.newEnumMap(ProtoceratopsVariant.class), map -> { + map.put(ProtoceratopsVariant.MALE, Constants.rl("textures/entity/protoceratops.png")); + map.put(ProtoceratopsVariant.FEMALE, Constants.rl("textures/entity/protoceratops_female.png")); + }); + + // Model-local "currently applied" offsets; cleared before each entity render + private float[] appliedYaw = null; + private float[] appliedRoll = null; + + @Override + public ResourceLocation getModelResource(ProtoceratopsEntity animatable) { + return Constants.rl("geo/protoceratops.geo.json"); + } + + @Override + public ResourceLocation getTextureResource(ProtoceratopsEntity animatable) { + return LOCATION_BY_VARIANT.get(animatable.getVariant()); + } + + @Override + public ResourceLocation getAnimationResource(ProtoceratopsEntity animatable) { + return Constants.rl("animations/protoceratops.animation.json"); + } + + @Override + public void setCustomAnimations(ProtoceratopsEntity entity, long id, AnimationState state) { + super.setCustomAnimations(entity, id, state); + + String[] tailBones = { "Tail1", "Tail2", "Tail3", "Tail4" }; + int n = tailBones.length; + + if (appliedYaw == null || appliedYaw.length != n) { + appliedYaw = new float[n]; + appliedRoll = new float[n]; + } + + // 1) Clear previous offsets (from the last entity rendered with this model instance) + for (int i = 0; i < n; i++) { + if (appliedYaw[i] == 0.0f && appliedRoll[i] == 0.0f) continue; + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + if (appliedYaw[i] != 0.0f) bone.setRotY(bone.getRotY() - appliedYaw[i]); + if (appliedRoll[i] != 0.0f) bone.setRotZ(bone.getRotZ() - appliedRoll[i]); + appliedYaw[i] = 0.0f; + appliedRoll[i] = 0.0f; + } + + // 2) Interpolated sway for extra smoothness between ticks + float sway = entity.getTailSwayOffset(state.getPartialTick()); // [-1, 1] + + // Tuning + float maxYawDeg = 22.0f; // increased max sweep + float swayGain = 1.35f; // amplifies overall power + float rollFraction = 0.40f; // slightly stronger roll for heft + + float deg2rad = (float)Math.PI / 180f; + + // Direction: positive sway (left turn) -> tail swings right (negative yaw) + // Flip the sign here if the sway feels inverted + float baseYaw = sway * maxYawDeg * deg2rad; + float baseRoll = -baseYaw * rollFraction; + + float[] weights = { 1.00f, 0.78f, 0.58f, 0.42f }; + + for (int i = 0; i < n; i++) { + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + + float w = weights[i]; + float yaw = baseYaw * w; + float roll = baseRoll * w; + + // OVERRIDE animations on Y/Z only: keep the model's predefined X bend intact + // Do NOT reset rotX here, so the upward bend stays + bone.setRotY(yaw); + bone.setRotZ(roll); + + appliedYaw[i] = yaw; + appliedRoll[i] = roll; + } + + GeoBone head = (GeoBone) getAnimationProcessor().getBone("Neck1"); + + if (head != null) { + var entityData = state.getData(DataTickets.ENTITY_MODEL_DATA); + float clampedYawDeg = Mth.clamp(entityData.netHeadYaw(), -20.0f, 20.0f); + head.setRotY(clampedYawDeg * Mth.DEG_TO_RAD); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/ProtoceratopsRenderer.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/ProtoceratopsRenderer.java new file mode 100644 index 0000000..e1eeef3 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/ProtoceratopsRenderer.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.cmr.jurassicrevived.entity.custom.ProtoceratopsEntity; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.util.Mth; +import software.bernie.geckolib.cache.object.BakedGeoModel; +import software.bernie.geckolib.renderer.GeoEntityRenderer; + + +public class ProtoceratopsRenderer extends GeoEntityRenderer { + private final float animalScale = 1.0F; + public ProtoceratopsRenderer(EntityRendererProvider.Context renderManager) { + super(renderManager, new ProtoceratopsModel()); + } + + @Override + public void scaleModelForRender(float widthScale, float heightScale, PoseStack poseStack, ProtoceratopsEntity animatable, BakedGeoModel model, boolean isReRender, float partialTick, int packedLight, int packedOverlay) { + poseStack.scale(animalScale, animalScale, animalScale); + if(animatable.isBaby()) { + float growthProgress = Mth.clamp((24000.0F + animatable.getSyncedAge()) / 24000.0F, 0.0F, 1.0F); + float scale = Mth.lerp(growthProgress, 0.2F, 1.0F); + poseStack.scale(scale, scale, scale); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/ProtoceratopsVariant.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/ProtoceratopsVariant.java new file mode 100644 index 0000000..dc09db7 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/ProtoceratopsVariant.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import java.util.Arrays; +import java.util.Comparator; + +public enum ProtoceratopsVariant { + MALE(0), + FEMALE(1); + + private static final ProtoceratopsVariant[] BY_ID = Arrays.stream(values()).sorted( + Comparator.comparingInt(ProtoceratopsVariant::getId)).toArray(ProtoceratopsVariant[]::new); + + private final int id; + + ProtoceratopsVariant(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static ProtoceratopsVariant byId(int id) { + return BY_ID[id % BY_ID.length]; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/PteranodonModel.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/PteranodonModel.java new file mode 100644 index 0000000..78551a4 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/PteranodonModel.java @@ -0,0 +1,112 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.google.common.collect.Maps; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.entity.custom.PteranodonEntity; +import net.minecraft.Util; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animation.AnimationState; +/*?} else {*/ +/*import software.bernie.geckolib.animation.AnimationState; + *//*?}*/ +import software.bernie.geckolib.cache.object.GeoBone; +import software.bernie.geckolib.constant.DataTickets; +import software.bernie.geckolib.model.GeoModel; + +import java.util.Map; + +public class PteranodonModel extends GeoModel { + + private static final Map LOCATION_BY_VARIANT = + Util.make(Maps.newEnumMap(PteranodonVariant.class), map -> { + map.put(PteranodonVariant.MALE, Constants.rl("textures/entity/pteranodon.png")); + map.put(PteranodonVariant.FEMALE, Constants.rl("textures/entity/pteranodon_female.png")); + }); + + // Model-local "currently applied" offsets; cleared before each entity render + private float[] appliedYaw = null; + private float[] appliedRoll = null; + + @Override + public ResourceLocation getModelResource(PteranodonEntity animatable) { + return Constants.rl("geo/pteranodon.geo.json"); + } + + @Override + public ResourceLocation getTextureResource(PteranodonEntity animatable) { + return LOCATION_BY_VARIANT.get(animatable.getVariant()); + } + + @Override + public ResourceLocation getAnimationResource(PteranodonEntity animatable) { + return Constants.rl("animations/pteranodon.animation.json"); + } + + @Override + public void setCustomAnimations(PteranodonEntity entity, long id, AnimationState state) { + super.setCustomAnimations(entity, id, state); + + String[] tailBones = { "Tail1", "Tail2", "Tail3" }; + int n = tailBones.length; + + if (appliedYaw == null || appliedYaw.length != n) { + appliedYaw = new float[n]; + appliedRoll = new float[n]; + } + + // 1) Clear previous offsets (from the last entity rendered with this model instance) + for (int i = 0; i < n; i++) { + if (appliedYaw[i] == 0.0f && appliedRoll[i] == 0.0f) continue; + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + if (appliedYaw[i] != 0.0f) bone.setRotY(bone.getRotY() - appliedYaw[i]); + if (appliedRoll[i] != 0.0f) bone.setRotZ(bone.getRotZ() - appliedRoll[i]); + appliedYaw[i] = 0.0f; + appliedRoll[i] = 0.0f; + } + + // 2) Interpolated sway for extra smoothness between ticks + float sway = entity.getTailSwayOffset(state.getPartialTick()); // [-1, 1] + + // Tuning + float maxYawDeg = 22.0f; // increased max sweep + float swayGain = 1.35f; // amplifies overall power + float rollFraction = 0.40f; // slightly stronger roll for heft + + float deg2rad = (float)Math.PI / 180f; + + // Direction: positive sway (left turn) -> tail swings right (negative yaw) + // Flip the sign here if the sway feels inverted + float baseYaw = sway * maxYawDeg * deg2rad; + float baseRoll = -baseYaw * rollFraction; + + float[] weights = { 1.00f, 0.78f, 0.58f }; + + for (int i = 0; i < n; i++) { + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + + float w = weights[i]; + float yaw = baseYaw * w; + float roll = baseRoll * w; + + // OVERRIDE animations on Y/Z only: keep the model's predefined X bend intact + // Do NOT reset rotX here, so the upward bend stays + bone.setRotY(yaw); + bone.setRotZ(roll); + + appliedYaw[i] = yaw; + appliedRoll[i] = roll; + } + + GeoBone head = (GeoBone) getAnimationProcessor().getBone("Neck1"); + + if (head != null) { + var entityData = state.getData(DataTickets.ENTITY_MODEL_DATA); + float clampedYawDeg = Mth.clamp(entityData.netHeadYaw(), -20.0f, 20.0f); + head.setRotY(clampedYawDeg * Mth.DEG_TO_RAD); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/PteranodonRenderer.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/PteranodonRenderer.java new file mode 100644 index 0000000..14eefcb --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/PteranodonRenderer.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.cmr.jurassicrevived.entity.custom.PteranodonEntity; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.util.Mth; +import software.bernie.geckolib.cache.object.BakedGeoModel; +import software.bernie.geckolib.renderer.GeoEntityRenderer; + + +public class PteranodonRenderer extends GeoEntityRenderer { + private final float animalScale = 1.0F; + public PteranodonRenderer(EntityRendererProvider.Context renderManager) { + super(renderManager, new PteranodonModel()); + } + + @Override + public void scaleModelForRender(float widthScale, float heightScale, PoseStack poseStack, PteranodonEntity animatable, BakedGeoModel model, boolean isReRender, float partialTick, int packedLight, int packedOverlay) { + poseStack.scale(animalScale, animalScale, animalScale); + if(animatable.isBaby()) { + float growthProgress = Mth.clamp((24000.0F + animatable.getSyncedAge()) / 24000.0F, 0.0F, 1.0F); + float scale = Mth.lerp(growthProgress, 0.2F, 1.0F); + poseStack.scale(scale, scale, scale); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/PteranodonVariant.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/PteranodonVariant.java new file mode 100644 index 0000000..7772797 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/PteranodonVariant.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import java.util.Arrays; +import java.util.Comparator; + +public enum PteranodonVariant { + MALE(0), + FEMALE(1); + + private static final PteranodonVariant[] BY_ID = Arrays.stream(values()).sorted( + Comparator.comparingInt(PteranodonVariant::getId)).toArray(PteranodonVariant[]::new); + + private final int id; + + PteranodonVariant(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static PteranodonVariant byId(int id) { + return BY_ID[id % BY_ID.length]; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/PterodaustroModel.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/PterodaustroModel.java new file mode 100644 index 0000000..83d2651 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/PterodaustroModel.java @@ -0,0 +1,112 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.google.common.collect.Maps; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.entity.custom.PterodaustroEntity; +import net.minecraft.Util; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animation.AnimationState; +/*?} else {*/ +/*import software.bernie.geckolib.animation.AnimationState; + *//*?}*/ +import software.bernie.geckolib.cache.object.GeoBone; +import software.bernie.geckolib.constant.DataTickets; +import software.bernie.geckolib.model.GeoModel; + +import java.util.Map; + +public class PterodaustroModel extends GeoModel { + + private static final Map LOCATION_BY_VARIANT = + Util.make(Maps.newEnumMap(PterodaustroVariant.class), map -> { + map.put(PterodaustroVariant.MALE, Constants.rl("textures/entity/pterodaustro.png")); + map.put(PterodaustroVariant.FEMALE, Constants.rl("textures/entity/pterodaustro_female.png")); + }); + + // Model-local "currently applied" offsets; cleared before each entity render + private float[] appliedYaw = null; + private float[] appliedRoll = null; + + @Override + public ResourceLocation getModelResource(PterodaustroEntity animatable) { + return Constants.rl("geo/pterodaustro.geo.json"); + } + + @Override + public ResourceLocation getTextureResource(PterodaustroEntity animatable) { + return LOCATION_BY_VARIANT.get(animatable.getVariant()); + } + + @Override + public ResourceLocation getAnimationResource(PterodaustroEntity animatable) { + return Constants.rl("animations/pterodaustro.animation.json"); + } + + @Override + public void setCustomAnimations(PterodaustroEntity entity, long id, AnimationState state) { + super.setCustomAnimations(entity, id, state); + + String[] tailBones = { "Tail1", "Tail2", "Tail3" }; + int n = tailBones.length; + + if (appliedYaw == null || appliedYaw.length != n) { + appliedYaw = new float[n]; + appliedRoll = new float[n]; + } + + // 1) Clear previous offsets (from the last entity rendered with this model instance) + for (int i = 0; i < n; i++) { + if (appliedYaw[i] == 0.0f && appliedRoll[i] == 0.0f) continue; + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + if (appliedYaw[i] != 0.0f) bone.setRotY(bone.getRotY() - appliedYaw[i]); + if (appliedRoll[i] != 0.0f) bone.setRotZ(bone.getRotZ() - appliedRoll[i]); + appliedYaw[i] = 0.0f; + appliedRoll[i] = 0.0f; + } + + // 2) Interpolated sway for extra smoothness between ticks + float sway = entity.getTailSwayOffset(state.getPartialTick()); // [-1, 1] + + // Tuning + float maxYawDeg = 22.0f; // increased max sweep + float swayGain = 1.35f; // amplifies overall power + float rollFraction = 0.40f; // slightly stronger roll for heft + + float deg2rad = (float)Math.PI / 180f; + + // Direction: positive sway (left turn) -> tail swings right (negative yaw) + // Flip the sign here if the sway feels inverted + float baseYaw = sway * maxYawDeg * deg2rad; + float baseRoll = -baseYaw * rollFraction; + + float[] weights = { 1.00f, 0.78f, 0.58f }; + + for (int i = 0; i < n; i++) { + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + + float w = weights[i]; + float yaw = baseYaw * w; + float roll = baseRoll * w; + + // OVERRIDE animations on Y/Z only: keep the model's predefined X bend intact + // Do NOT reset rotX here, so the upward bend stays + bone.setRotY(yaw); + bone.setRotZ(roll); + + appliedYaw[i] = yaw; + appliedRoll[i] = roll; + } + + GeoBone head = (GeoBone) getAnimationProcessor().getBone("Neck1"); + + if (head != null) { + var entityData = state.getData(DataTickets.ENTITY_MODEL_DATA); + float clampedYawDeg = Mth.clamp(entityData.netHeadYaw(), -20.0f, 20.0f); + head.setRotY(clampedYawDeg * Mth.DEG_TO_RAD); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/PterodaustroRenderer.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/PterodaustroRenderer.java new file mode 100644 index 0000000..83875f3 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/PterodaustroRenderer.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.cmr.jurassicrevived.entity.custom.PterodaustroEntity; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.util.Mth; +import software.bernie.geckolib.cache.object.BakedGeoModel; +import software.bernie.geckolib.renderer.GeoEntityRenderer; + + +public class PterodaustroRenderer extends GeoEntityRenderer { + private final float animalScale = 0.65F; + public PterodaustroRenderer(EntityRendererProvider.Context renderManager) { + super(renderManager, new PterodaustroModel()); + } + + @Override + public void scaleModelForRender(float widthScale, float heightScale, PoseStack poseStack, PterodaustroEntity animatable, BakedGeoModel model, boolean isReRender, float partialTick, int packedLight, int packedOverlay) { + poseStack.scale(animalScale, animalScale, animalScale); + if(animatable.isBaby()) { + float growthProgress = Mth.clamp((24000.0F + animatable.getSyncedAge()) / 24000.0F, 0.0F, 1.0F); + float scale = Mth.lerp(growthProgress, 0.2F, 1.0F); + poseStack.scale(scale, scale, scale); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/PterodaustroVariant.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/PterodaustroVariant.java new file mode 100644 index 0000000..167afec --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/PterodaustroVariant.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import java.util.Arrays; +import java.util.Comparator; + +public enum PterodaustroVariant { + MALE(0), + FEMALE(1); + + private static final PterodaustroVariant[] BY_ID = Arrays.stream(values()).sorted( + Comparator.comparingInt(PterodaustroVariant::getId)).toArray(PterodaustroVariant[]::new); + + private final int id; + + PterodaustroVariant(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static PterodaustroVariant byId(int id) { + return BY_ID[id % BY_ID.length]; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/QuetzalcoatlusModel.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/QuetzalcoatlusModel.java new file mode 100644 index 0000000..99fb8e5 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/QuetzalcoatlusModel.java @@ -0,0 +1,112 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.google.common.collect.Maps; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.entity.custom.QuetzalcoatlusEntity; +import net.minecraft.Util; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animation.AnimationState; +/*?} else {*/ +/*import software.bernie.geckolib.animation.AnimationState; + *//*?}*/ +import software.bernie.geckolib.cache.object.GeoBone; +import software.bernie.geckolib.constant.DataTickets; +import software.bernie.geckolib.model.GeoModel; + +import java.util.Map; + +public class QuetzalcoatlusModel extends GeoModel { + + private static final Map LOCATION_BY_VARIANT = + Util.make(Maps.newEnumMap(QuetzalcoatlusVariant.class), map -> { + map.put(QuetzalcoatlusVariant.MALE, Constants.rl("textures/entity/quetzalcoatlus.png")); + map.put(QuetzalcoatlusVariant.FEMALE, Constants.rl("textures/entity/quetzalcoatlus_female.png")); + }); + + // Model-local "currently applied" offsets; cleared before each entity render + private float[] appliedYaw = null; + private float[] appliedRoll = null; + + @Override + public ResourceLocation getModelResource(QuetzalcoatlusEntity animatable) { + return Constants.rl("geo/quetzalcoatlus.geo.json"); + } + + @Override + public ResourceLocation getTextureResource(QuetzalcoatlusEntity animatable) { + return LOCATION_BY_VARIANT.get(animatable.getVariant()); + } + + @Override + public ResourceLocation getAnimationResource(QuetzalcoatlusEntity animatable) { + return Constants.rl("animations/quetzalcoatlus.animation.json"); + } + + @Override + public void setCustomAnimations(QuetzalcoatlusEntity entity, long id, AnimationState state) { + super.setCustomAnimations(entity, id, state); + + String[] tailBones = { "Tail1", "Tail2", "Tail3" }; + int n = tailBones.length; + + if (appliedYaw == null || appliedYaw.length != n) { + appliedYaw = new float[n]; + appliedRoll = new float[n]; + } + + // 1) Clear previous offsets (from the last entity rendered with this model instance) + for (int i = 0; i < n; i++) { + if (appliedYaw[i] == 0.0f && appliedRoll[i] == 0.0f) continue; + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + if (appliedYaw[i] != 0.0f) bone.setRotY(bone.getRotY() - appliedYaw[i]); + if (appliedRoll[i] != 0.0f) bone.setRotZ(bone.getRotZ() - appliedRoll[i]); + appliedYaw[i] = 0.0f; + appliedRoll[i] = 0.0f; + } + + // 2) Interpolated sway for extra smoothness between ticks + float sway = entity.getTailSwayOffset(state.getPartialTick()); // [-1, 1] + + // Tuning + float maxYawDeg = 22.0f; // increased max sweep + float swayGain = 1.35f; // amplifies overall power + float rollFraction = 0.40f; // slightly stronger roll for heft + + float deg2rad = (float)Math.PI / 180f; + + // Direction: positive sway (left turn) -> tail swings right (negative yaw) + // Flip the sign here if the sway feels inverted + float baseYaw = sway * maxYawDeg * deg2rad; + float baseRoll = -baseYaw * rollFraction; + + float[] weights = { 1.00f, 0.78f, 0.58f }; + + for (int i = 0; i < n; i++) { + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + + float w = weights[i]; + float yaw = baseYaw * w; + float roll = baseRoll * w; + + // OVERRIDE animations on Y/Z only: keep the model's predefined X bend intact + // Do NOT reset rotX here, so the upward bend stays + bone.setRotY(yaw); + bone.setRotZ(roll); + + appliedYaw[i] = yaw; + appliedRoll[i] = roll; + } + + GeoBone head = (GeoBone) getAnimationProcessor().getBone("Neck1"); + + if (head != null) { + var entityData = state.getData(DataTickets.ENTITY_MODEL_DATA); + float clampedYawDeg = Mth.clamp(entityData.netHeadYaw(), -20.0f, 20.0f); + head.setRotY(clampedYawDeg * Mth.DEG_TO_RAD); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/QuetzalcoatlusRenderer.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/QuetzalcoatlusRenderer.java new file mode 100644 index 0000000..d4f3b37 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/QuetzalcoatlusRenderer.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.cmr.jurassicrevived.entity.custom.QuetzalcoatlusEntity; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.util.Mth; +import software.bernie.geckolib.cache.object.BakedGeoModel; +import software.bernie.geckolib.renderer.GeoEntityRenderer; + + +public class QuetzalcoatlusRenderer extends GeoEntityRenderer { + private final float animalScale = 2.0F; + public QuetzalcoatlusRenderer(EntityRendererProvider.Context renderManager) { + super(renderManager, new QuetzalcoatlusModel()); + } + + @Override + public void scaleModelForRender(float widthScale, float heightScale, PoseStack poseStack, QuetzalcoatlusEntity animatable, BakedGeoModel model, boolean isReRender, float partialTick, int packedLight, int packedOverlay) { + poseStack.scale(animalScale, animalScale, animalScale); + if(animatable.isBaby()) { + float growthProgress = Mth.clamp((24000.0F + animatable.getSyncedAge()) / 24000.0F, 0.0F, 1.0F); + float scale = Mth.lerp(growthProgress, 0.2F, 1.0F); + poseStack.scale(scale, scale, scale); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/QuetzalcoatlusVariant.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/QuetzalcoatlusVariant.java new file mode 100644 index 0000000..8408fbb --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/QuetzalcoatlusVariant.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import java.util.Arrays; +import java.util.Comparator; + +public enum QuetzalcoatlusVariant { + MALE(0), + FEMALE(1); + + private static final QuetzalcoatlusVariant[] BY_ID = Arrays.stream(values()).sorted( + Comparator.comparingInt(QuetzalcoatlusVariant::getId)).toArray(QuetzalcoatlusVariant[]::new); + + private final int id; + + QuetzalcoatlusVariant(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static QuetzalcoatlusVariant byId(int id) { + return BY_ID[id % BY_ID.length]; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/RajasaurusModel.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/RajasaurusModel.java new file mode 100644 index 0000000..270be31 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/RajasaurusModel.java @@ -0,0 +1,112 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.google.common.collect.Maps; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.entity.custom.RajasaurusEntity; +import net.minecraft.Util; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animation.AnimationState; +/*?} else {*/ +/*import software.bernie.geckolib.animation.AnimationState; + *//*?}*/ +import software.bernie.geckolib.cache.object.GeoBone; +import software.bernie.geckolib.constant.DataTickets; +import software.bernie.geckolib.model.GeoModel; + +import java.util.Map; + +public class RajasaurusModel extends GeoModel { + + private static final Map LOCATION_BY_VARIANT = + Util.make(Maps.newEnumMap(RajasaurusVariant.class), map -> { + map.put(RajasaurusVariant.MALE, Constants.rl("textures/entity/rajasaurus.png")); + map.put(RajasaurusVariant.FEMALE, Constants.rl("textures/entity/rajasaurus_female.png")); + }); + + // Model-local "currently applied" offsets; cleared before each entity render + private float[] appliedYaw = null; + private float[] appliedRoll = null; + + @Override + public ResourceLocation getModelResource(RajasaurusEntity animatable) { + return Constants.rl("geo/rajasaurus.geo.json"); + } + + @Override + public ResourceLocation getTextureResource(RajasaurusEntity animatable) { + return LOCATION_BY_VARIANT.get(animatable.getVariant()); + } + + @Override + public ResourceLocation getAnimationResource(RajasaurusEntity animatable) { + return Constants.rl("animations/rajasaurus.animation.json"); + } + + @Override + public void setCustomAnimations(RajasaurusEntity entity, long id, AnimationState state) { + super.setCustomAnimations(entity, id, state); + + String[] tailBones = { "TailBase", "Tail2", "Tail3", "Tail4", "Tail5", "Tail6" }; + int n = tailBones.length; + + if (appliedYaw == null || appliedYaw.length != n) { + appliedYaw = new float[n]; + appliedRoll = new float[n]; + } + + // 1) Clear previous offsets (from the last entity rendered with this model instance) + for (int i = 0; i < n; i++) { + if (appliedYaw[i] == 0.0f && appliedRoll[i] == 0.0f) continue; + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + if (appliedYaw[i] != 0.0f) bone.setRotY(bone.getRotY() - appliedYaw[i]); + if (appliedRoll[i] != 0.0f) bone.setRotZ(bone.getRotZ() - appliedRoll[i]); + appliedYaw[i] = 0.0f; + appliedRoll[i] = 0.0f; + } + + // 2) Interpolated sway for extra smoothness between ticks + float sway = entity.getTailSwayOffset(state.getPartialTick()); // [-1, 1] + + // Tuning + float maxYawDeg = 22.0f; // increased max sweep + float swayGain = 1.35f; // amplifies overall power + float rollFraction = 0.40f; // slightly stronger roll for heft + + float deg2rad = (float)Math.PI / 180f; + + // Direction: positive sway (left turn) -> tail swings right (negative yaw) + // Flip the sign here if the sway feels inverted + float baseYaw = sway * maxYawDeg * deg2rad; + float baseRoll = -baseYaw * rollFraction; + + float[] weights = { 1.00f, 0.78f, 0.58f, 0.42f, 0.30f, 0.22f }; + + for (int i = 0; i < n; i++) { + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + + float w = weights[i]; + float yaw = baseYaw * w; + float roll = baseRoll * w; + + // OVERRIDE animations on Y/Z only: keep the model's predefined X bend intact + // Do NOT reset rotX here, so the upward bend stays + bone.setRotY(yaw); + bone.setRotZ(roll); + + appliedYaw[i] = yaw; + appliedRoll[i] = roll; + } + + GeoBone head = (GeoBone) getAnimationProcessor().getBone("NeckBASE"); + + if (head != null) { + var entityData = state.getData(DataTickets.ENTITY_MODEL_DATA); + float clampedYawDeg = Mth.clamp(entityData.netHeadYaw(), -20.0f, 20.0f); + head.setRotY(clampedYawDeg * Mth.DEG_TO_RAD); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/RajasaurusRenderer.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/RajasaurusRenderer.java new file mode 100644 index 0000000..1452784 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/RajasaurusRenderer.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.cmr.jurassicrevived.entity.custom.RajasaurusEntity; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.util.Mth; +import software.bernie.geckolib.cache.object.BakedGeoModel; +import software.bernie.geckolib.renderer.GeoEntityRenderer; + + +public class RajasaurusRenderer extends GeoEntityRenderer { + private final float animalScale = 1.45F; + public RajasaurusRenderer(EntityRendererProvider.Context renderManager) { + super(renderManager, new RajasaurusModel()); + } + + @Override + public void scaleModelForRender(float widthScale, float heightScale, PoseStack poseStack, RajasaurusEntity animatable, BakedGeoModel model, boolean isReRender, float partialTick, int packedLight, int packedOverlay) { + poseStack.scale(animalScale, animalScale, animalScale); + if(animatable.isBaby()) { + float growthProgress = Mth.clamp((24000.0F + animatable.getSyncedAge()) / 24000.0F, 0.0F, 1.0F); + float scale = Mth.lerp(growthProgress, 0.2F, 1.0F); + poseStack.scale(scale, scale, scale); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/RajasaurusVariant.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/RajasaurusVariant.java new file mode 100644 index 0000000..4b3b663 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/RajasaurusVariant.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import java.util.Arrays; +import java.util.Comparator; + +public enum RajasaurusVariant { + MALE(0), + FEMALE(1); + + private static final RajasaurusVariant[] BY_ID = Arrays.stream(values()).sorted( + Comparator.comparingInt(RajasaurusVariant::getId)).toArray(RajasaurusVariant[]::new); + + private final int id; + + RajasaurusVariant(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static RajasaurusVariant byId(int id) { + return BY_ID[id % BY_ID.length]; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/RugopsModel.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/RugopsModel.java new file mode 100644 index 0000000..5833be8 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/RugopsModel.java @@ -0,0 +1,112 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.google.common.collect.Maps; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.entity.custom.RugopsEntity; +import net.minecraft.Util; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animation.AnimationState; +/*?} else {*/ +/*import software.bernie.geckolib.animation.AnimationState; + *//*?}*/ +import software.bernie.geckolib.cache.object.GeoBone; +import software.bernie.geckolib.constant.DataTickets; +import software.bernie.geckolib.model.GeoModel; + +import java.util.Map; + +public class RugopsModel extends GeoModel { + + private static final Map LOCATION_BY_VARIANT = + Util.make(Maps.newEnumMap(RugopsVariant.class), map -> { + map.put(RugopsVariant.MALE, Constants.rl("textures/entity/rugops.png")); + map.put(RugopsVariant.FEMALE, Constants.rl("textures/entity/rugops_female.png")); + }); + + // Model-local "currently applied" offsets; cleared before each entity render + private float[] appliedYaw = null; + private float[] appliedRoll = null; + + @Override + public ResourceLocation getModelResource(RugopsEntity animatable) { + return Constants.rl("geo/rugops.geo.json"); + } + + @Override + public ResourceLocation getTextureResource(RugopsEntity animatable) { + return LOCATION_BY_VARIANT.get(animatable.getVariant()); + } + + @Override + public ResourceLocation getAnimationResource(RugopsEntity animatable) { + return Constants.rl("animations/rugops.animation.json"); + } + + @Override + public void setCustomAnimations(RugopsEntity entity, long id, AnimationState state) { + super.setCustomAnimations(entity, id, state); + + String[] tailBones = { "Tail1", "Tail2", "Tail3", "Tail4", "Tail5", "Tail6" }; + int n = tailBones.length; + + if (appliedYaw == null || appliedYaw.length != n) { + appliedYaw = new float[n]; + appliedRoll = new float[n]; + } + + // 1) Clear previous offsets (from the last entity rendered with this model instance) + for (int i = 0; i < n; i++) { + if (appliedYaw[i] == 0.0f && appliedRoll[i] == 0.0f) continue; + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + if (appliedYaw[i] != 0.0f) bone.setRotY(bone.getRotY() - appliedYaw[i]); + if (appliedRoll[i] != 0.0f) bone.setRotZ(bone.getRotZ() - appliedRoll[i]); + appliedYaw[i] = 0.0f; + appliedRoll[i] = 0.0f; + } + + // 2) Interpolated sway for extra smoothness between ticks + float sway = entity.getTailSwayOffset(state.getPartialTick()); // [-1, 1] + + // Tuning + float maxYawDeg = 22.0f; // increased max sweep + float swayGain = 1.35f; // amplifies overall power + float rollFraction = 0.40f; // slightly stronger roll for heft + + float deg2rad = (float)Math.PI / 180f; + + // Direction: positive sway (left turn) -> tail swings right (negative yaw) + // Flip the sign here if the sway feels inverted + float baseYaw = sway * maxYawDeg * deg2rad; + float baseRoll = -baseYaw * rollFraction; + + float[] weights = { 1.00f, 0.78f, 0.58f, 0.42f, 0.30f, 0.22f }; + + for (int i = 0; i < n; i++) { + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + + float w = weights[i]; + float yaw = baseYaw * w; + float roll = baseRoll * w; + + // OVERRIDE animations on Y/Z only: keep the model's predefined X bend intact + // Do NOT reset rotX here, so the upward bend stays + bone.setRotY(yaw); + bone.setRotZ(roll); + + appliedYaw[i] = yaw; + appliedRoll[i] = roll; + } + + GeoBone head = (GeoBone) getAnimationProcessor().getBone("Neck1"); + + if (head != null) { + var entityData = state.getData(DataTickets.ENTITY_MODEL_DATA); + float clampedYawDeg = Mth.clamp(entityData.netHeadYaw(), -20.0f, 20.0f); + head.setRotY(clampedYawDeg * Mth.DEG_TO_RAD); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/RugopsRenderer.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/RugopsRenderer.java new file mode 100644 index 0000000..7441d56 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/RugopsRenderer.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.cmr.jurassicrevived.entity.custom.RugopsEntity; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.util.Mth; +import software.bernie.geckolib.cache.object.BakedGeoModel; +import software.bernie.geckolib.renderer.GeoEntityRenderer; + + +public class RugopsRenderer extends GeoEntityRenderer { + private final float animalScale = 1.2F; + public RugopsRenderer(EntityRendererProvider.Context renderManager) { + super(renderManager, new RugopsModel()); + } + + @Override + public void scaleModelForRender(float widthScale, float heightScale, PoseStack poseStack, RugopsEntity animatable, BakedGeoModel model, boolean isReRender, float partialTick, int packedLight, int packedOverlay) { + poseStack.scale(animalScale, animalScale, animalScale); + if(animatable.isBaby()) { + float growthProgress = Mth.clamp((24000.0F + animatable.getSyncedAge()) / 24000.0F, 0.0F, 1.0F); + float scale = Mth.lerp(growthProgress, 0.2F, 1.0F); + poseStack.scale(scale, scale, scale); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/RugopsVariant.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/RugopsVariant.java new file mode 100644 index 0000000..4d5b8d9 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/RugopsVariant.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import java.util.Arrays; +import java.util.Comparator; + +public enum RugopsVariant { + MALE(0), + FEMALE(1); + + private static final RugopsVariant[] BY_ID = Arrays.stream(values()).sorted( + Comparator.comparingInt(RugopsVariant::getId)).toArray(RugopsVariant[]::new); + + private final int id; + + RugopsVariant(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static RugopsVariant byId(int id) { + return BY_ID[id % BY_ID.length]; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/SegisaurusModel.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/SegisaurusModel.java new file mode 100644 index 0000000..4de2de3 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/SegisaurusModel.java @@ -0,0 +1,112 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.google.common.collect.Maps; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.entity.custom.SegisaurusEntity; +import net.minecraft.Util; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animation.AnimationState; +/*?} else {*/ +/*import software.bernie.geckolib.animation.AnimationState; + *//*?}*/ +import software.bernie.geckolib.cache.object.GeoBone; +import software.bernie.geckolib.constant.DataTickets; +import software.bernie.geckolib.model.GeoModel; + +import java.util.Map; + +public class SegisaurusModel extends GeoModel { + + private static final Map LOCATION_BY_VARIANT = + Util.make(Maps.newEnumMap(SegisaurusVariant.class), map -> { + map.put(SegisaurusVariant.MALE, Constants.rl("textures/entity/segisaurus.png")); + map.put(SegisaurusVariant.FEMALE, Constants.rl("textures/entity/segisaurus_female.png")); + }); + + // Model-local "currently applied" offsets; cleared before each entity render + private float[] appliedYaw = null; + private float[] appliedRoll = null; + + @Override + public ResourceLocation getModelResource(SegisaurusEntity animatable) { + return Constants.rl("geo/segisaurus.geo.json"); + } + + @Override + public ResourceLocation getTextureResource(SegisaurusEntity animatable) { + return LOCATION_BY_VARIANT.get(animatable.getVariant()); + } + + @Override + public ResourceLocation getAnimationResource(SegisaurusEntity animatable) { + return Constants.rl("animations/segisaurus.animation.json"); + } + + @Override + public void setCustomAnimations(SegisaurusEntity entity, long id, AnimationState state) { + super.setCustomAnimations(entity, id, state); + + String[] tailBones = { "tail1", "tail2", "tail3", "tail4", "tail5" }; + int n = tailBones.length; + + if (appliedYaw == null || appliedYaw.length != n) { + appliedYaw = new float[n]; + appliedRoll = new float[n]; + } + + // 1) Clear previous offsets (from the last entity rendered with this model instance) + for (int i = 0; i < n; i++) { + if (appliedYaw[i] == 0.0f && appliedRoll[i] == 0.0f) continue; + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + if (appliedYaw[i] != 0.0f) bone.setRotY(bone.getRotY() - appliedYaw[i]); + if (appliedRoll[i] != 0.0f) bone.setRotZ(bone.getRotZ() - appliedRoll[i]); + appliedYaw[i] = 0.0f; + appliedRoll[i] = 0.0f; + } + + // 2) Interpolated sway for extra smoothness between ticks + float sway = entity.getTailSwayOffset(state.getPartialTick()); // [-1, 1] + + // Tuning + float maxYawDeg = 22.0f; // increased max sweep + float swayGain = 1.35f; // amplifies overall power + float rollFraction = 0.40f; // slightly stronger roll for heft + + float deg2rad = (float)Math.PI / 180f; + + // Direction: positive sway (left turn) -> tail swings right (negative yaw) + // Flip the sign here if the sway feels inverted + float baseYaw = sway * maxYawDeg * deg2rad; + float baseRoll = -baseYaw * rollFraction; + + float[] weights = { 1.00f, 0.78f, 0.58f, 0.42f, 0.30f }; + + for (int i = 0; i < n; i++) { + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + + float w = weights[i]; + float yaw = baseYaw * w; + float roll = baseRoll * w; + + // OVERRIDE animations on Y/Z only: keep the model's predefined X bend intact + // Do NOT reset rotX here, so the upward bend stays + bone.setRotY(yaw); + bone.setRotZ(roll); + + appliedYaw[i] = yaw; + appliedRoll[i] = roll; + } + + GeoBone head = (GeoBone) getAnimationProcessor().getBone("body1"); + + if (head != null) { + var entityData = state.getData(DataTickets.ENTITY_MODEL_DATA); + float clampedYawDeg = Mth.clamp(entityData.netHeadYaw(), -30.0f, 30.0f); + head.setRotY(clampedYawDeg * Mth.DEG_TO_RAD); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/SegisaurusRenderer.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/SegisaurusRenderer.java new file mode 100644 index 0000000..be71eb4 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/SegisaurusRenderer.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.cmr.jurassicrevived.entity.custom.SegisaurusEntity; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.util.Mth; +import software.bernie.geckolib.cache.object.BakedGeoModel; +import software.bernie.geckolib.renderer.GeoEntityRenderer; + + +public class SegisaurusRenderer extends GeoEntityRenderer { + private final float animalScale = 0.55F; + public SegisaurusRenderer(EntityRendererProvider.Context renderManager) { + super(renderManager, new SegisaurusModel()); + } + + @Override + public void scaleModelForRender(float widthScale, float heightScale, PoseStack poseStack, SegisaurusEntity animatable, BakedGeoModel model, boolean isReRender, float partialTick, int packedLight, int packedOverlay) { + poseStack.scale(animalScale, animalScale, animalScale); + if(animatable.isBaby()) { + float growthProgress = Mth.clamp((24000.0F + animatable.getSyncedAge()) / 24000.0F, 0.0F, 1.0F); + float scale = Mth.lerp(growthProgress, 0.2F, 1.0F); + poseStack.scale(scale, scale, scale); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/SegisaurusVariant.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/SegisaurusVariant.java new file mode 100644 index 0000000..aad5e0c --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/SegisaurusVariant.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import java.util.Arrays; +import java.util.Comparator; + +public enum SegisaurusVariant { + MALE(0), + FEMALE(1); + + private static final SegisaurusVariant[] BY_ID = Arrays.stream(values()).sorted( + Comparator.comparingInt(SegisaurusVariant::getId)).toArray(SegisaurusVariant[]::new); + + private final int id; + + SegisaurusVariant(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static SegisaurusVariant byId(int id) { + return BY_ID[id % BY_ID.length]; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/ShantungosaurusModel.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/ShantungosaurusModel.java new file mode 100644 index 0000000..32036aa --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/ShantungosaurusModel.java @@ -0,0 +1,112 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.google.common.collect.Maps; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.entity.custom.ShantungosaurusEntity; +import net.minecraft.Util; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animation.AnimationState; +/*?} else {*/ +/*import software.bernie.geckolib.animation.AnimationState; + *//*?}*/ +import software.bernie.geckolib.cache.object.GeoBone; +import software.bernie.geckolib.constant.DataTickets; +import software.bernie.geckolib.model.GeoModel; + +import java.util.Map; + +public class ShantungosaurusModel extends GeoModel { + + private static final Map LOCATION_BY_VARIANT = + Util.make(Maps.newEnumMap(ShantungosaurusVariant.class), map -> { + map.put(ShantungosaurusVariant.MALE, Constants.rl("textures/entity/shantungosaurus.png")); + map.put(ShantungosaurusVariant.FEMALE, Constants.rl("textures/entity/shantungosaurus_female.png")); + }); + + // Model-local "currently applied" offsets; cleared before each entity render + private float[] appliedYaw = null; + private float[] appliedRoll = null; + + @Override + public ResourceLocation getModelResource(ShantungosaurusEntity animatable) { + return Constants.rl("geo/shantungosaurus.geo.json"); + } + + @Override + public ResourceLocation getTextureResource(ShantungosaurusEntity animatable) { + return LOCATION_BY_VARIANT.get(animatable.getVariant()); + } + + @Override + public ResourceLocation getAnimationResource(ShantungosaurusEntity animatable) { + return Constants.rl("animations/shantungosaurus.animation.json"); + } + + @Override + public void setCustomAnimations(ShantungosaurusEntity entity, long id, AnimationState state) { + super.setCustomAnimations(entity, id, state); + + String[] tailBones = { "Tail1", "Tail2", "Tail3", "Tail4", "Tail5", "Tail6" }; + int n = tailBones.length; + + if (appliedYaw == null || appliedYaw.length != n) { + appliedYaw = new float[n]; + appliedRoll = new float[n]; + } + + // 1) Clear previous offsets (from the last entity rendered with this model instance) + for (int i = 0; i < n; i++) { + if (appliedYaw[i] == 0.0f && appliedRoll[i] == 0.0f) continue; + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + if (appliedYaw[i] != 0.0f) bone.setRotY(bone.getRotY() - appliedYaw[i]); + if (appliedRoll[i] != 0.0f) bone.setRotZ(bone.getRotZ() - appliedRoll[i]); + appliedYaw[i] = 0.0f; + appliedRoll[i] = 0.0f; + } + + // 2) Interpolated sway for extra smoothness between ticks + float sway = entity.getTailSwayOffset(state.getPartialTick()); // [-1, 1] + + // Tuning + float maxYawDeg = 22.0f; // increased max sweep + float swayGain = 1.35f; // amplifies overall power + float rollFraction = 0.40f; // slightly stronger roll for heft + + float deg2rad = (float)Math.PI / 180f; + + // Direction: positive sway (left turn) -> tail swings right (negative yaw) + // Flip the sign here if the sway feels inverted + float baseYaw = sway * maxYawDeg * deg2rad; + float baseRoll = -baseYaw * rollFraction; + + float[] weights = { 1.00f, 0.78f, 0.58f, 0.42f, 0.30f, 0.22f }; + + for (int i = 0; i < n; i++) { + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + + float w = weights[i]; + float yaw = baseYaw * w; + float roll = baseRoll * w; + + // OVERRIDE animations on Y/Z only: keep the model's predefined X bend intact + // Do NOT reset rotX here, so the upward bend stays + bone.setRotY(yaw); + bone.setRotZ(roll); + + appliedYaw[i] = yaw; + appliedRoll[i] = roll; + } + + GeoBone head = (GeoBone) getAnimationProcessor().getBone("Neck"); + + if (head != null) { + var entityData = state.getData(DataTickets.ENTITY_MODEL_DATA); + float clampedYawDeg = Mth.clamp(entityData.netHeadYaw(), -20.0f, 20.0f); + head.setRotY(clampedYawDeg * Mth.DEG_TO_RAD); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/ShantungosaurusRenderer.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/ShantungosaurusRenderer.java new file mode 100644 index 0000000..b7a1440 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/ShantungosaurusRenderer.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.cmr.jurassicrevived.entity.custom.ShantungosaurusEntity; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.util.Mth; +import software.bernie.geckolib.cache.object.BakedGeoModel; +import software.bernie.geckolib.renderer.GeoEntityRenderer; + + +public class ShantungosaurusRenderer extends GeoEntityRenderer { + private final float animalScale = 1.67F; + public ShantungosaurusRenderer(EntityRendererProvider.Context renderManager) { + super(renderManager, new ShantungosaurusModel()); + } + + @Override + public void scaleModelForRender(float widthScale, float heightScale, PoseStack poseStack, ShantungosaurusEntity animatable, BakedGeoModel model, boolean isReRender, float partialTick, int packedLight, int packedOverlay) { + poseStack.scale(animalScale, animalScale, animalScale); + if(animatable.isBaby()) { + float growthProgress = Mth.clamp((24000.0F + animatable.getSyncedAge()) / 24000.0F, 0.0F, 1.0F); + float scale = Mth.lerp(growthProgress, 0.2F, 1.0F); + poseStack.scale(scale, scale, scale); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/ShantungosaurusVariant.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/ShantungosaurusVariant.java new file mode 100644 index 0000000..a85d2e6 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/ShantungosaurusVariant.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import java.util.Arrays; +import java.util.Comparator; + +public enum ShantungosaurusVariant { + MALE(0), + FEMALE(1); + + private static final ShantungosaurusVariant[] BY_ID = Arrays.stream(values()).sorted( + Comparator.comparingInt(ShantungosaurusVariant::getId)).toArray(ShantungosaurusVariant[]::new); + + private final int id; + + ShantungosaurusVariant(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static ShantungosaurusVariant byId(int id) { + return BY_ID[id % BY_ID.length]; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/SpinosaurusModel.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/SpinosaurusModel.java new file mode 100644 index 0000000..841c03c --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/SpinosaurusModel.java @@ -0,0 +1,112 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.google.common.collect.Maps; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.entity.custom.SpinosaurusEntity; +import net.minecraft.Util; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animation.AnimationState; +/*?} else {*/ +/*import software.bernie.geckolib.animation.AnimationState; + *//*?}*/ +import software.bernie.geckolib.cache.object.GeoBone; +import software.bernie.geckolib.constant.DataTickets; +import software.bernie.geckolib.model.GeoModel; + +import java.util.Map; + +public class SpinosaurusModel extends GeoModel { + + private static final Map LOCATION_BY_VARIANT = + Util.make(Maps.newEnumMap(SpinosaurusVariant.class), map -> { + map.put(SpinosaurusVariant.MALE, Constants.rl("textures/entity/spinosaurus.png")); + map.put(SpinosaurusVariant.FEMALE, Constants.rl("textures/entity/spinosaurus_female.png")); + }); + + // Model-local "currently applied" offsets; cleared before each entity render + private float[] appliedYaw = null; + private float[] appliedRoll = null; + + @Override + public ResourceLocation getModelResource(SpinosaurusEntity animatable) { + return Constants.rl("geo/spinosaurus.geo.json"); + } + + @Override + public ResourceLocation getTextureResource(SpinosaurusEntity animatable) { + return LOCATION_BY_VARIANT.get(animatable.getVariant()); + } + + @Override + public ResourceLocation getAnimationResource(SpinosaurusEntity animatable) { + return Constants.rl("animations/spinosaurus.animation.json"); + } + + @Override + public void setCustomAnimations(SpinosaurusEntity entity, long id, AnimationState state) { + super.setCustomAnimations(entity, id, state); + + String[] tailBones = { "Tail1", "Tail2", "Tail3", "Tail4", "Tail5", "Tail6" }; + int n = tailBones.length; + + if (appliedYaw == null || appliedYaw.length != n) { + appliedYaw = new float[n]; + appliedRoll = new float[n]; + } + + // 1) Clear previous offsets (from the last entity rendered with this model instance) + for (int i = 0; i < n; i++) { + if (appliedYaw[i] == 0.0f && appliedRoll[i] == 0.0f) continue; + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + if (appliedYaw[i] != 0.0f) bone.setRotY(bone.getRotY() - appliedYaw[i]); + if (appliedRoll[i] != 0.0f) bone.setRotZ(bone.getRotZ() - appliedRoll[i]); + appliedYaw[i] = 0.0f; + appliedRoll[i] = 0.0f; + } + + // 2) Interpolated sway for extra smoothness between ticks + float sway = entity.getTailSwayOffset(state.getPartialTick()); // [-1, 1] + + // Tuning + float maxYawDeg = 22.0f; // increased max sweep + float swayGain = 1.35f; // amplifies overall power + float rollFraction = 0.40f; // slightly stronger roll for heft + + float deg2rad = (float)Math.PI / 180f; + + // Direction: positive sway (left turn) -> tail swings right (negative yaw) + // Flip the sign here if the sway feels inverted + float baseYaw = sway * maxYawDeg * deg2rad; + float baseRoll = -baseYaw * rollFraction; + + float[] weights = { 1.00f, 0.78f, 0.58f, 0.42f, 0.30f, 0.22f }; + + for (int i = 0; i < n; i++) { + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + + float w = weights[i]; + float yaw = baseYaw * w; + float roll = baseRoll * w; + + // OVERRIDE animations on Y/Z only: keep the model's predefined X bend intact + // Do NOT reset rotX here, so the upward bend stays + bone.setRotY(yaw); + bone.setRotZ(roll); + + appliedYaw[i] = yaw; + appliedRoll[i] = roll; + } + + GeoBone head = (GeoBone) getAnimationProcessor().getBone("Neck1"); + + if (head != null) { + var entityData = state.getData(DataTickets.ENTITY_MODEL_DATA); + float clampedYawDeg = Mth.clamp(entityData.netHeadYaw(), -20.0f, 20.0f); + head.setRotY(clampedYawDeg * Mth.DEG_TO_RAD); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/SpinosaurusRenderer.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/SpinosaurusRenderer.java new file mode 100644 index 0000000..a5542df --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/SpinosaurusRenderer.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.cmr.jurassicrevived.entity.custom.SpinosaurusEntity; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.util.Mth; +import software.bernie.geckolib.cache.object.BakedGeoModel; +import software.bernie.geckolib.renderer.GeoEntityRenderer; + + +public class SpinosaurusRenderer extends GeoEntityRenderer { + private final float animalScale = 2.77F; + public SpinosaurusRenderer(EntityRendererProvider.Context renderManager) { + super(renderManager, new SpinosaurusModel()); + } + + @Override + public void scaleModelForRender(float widthScale, float heightScale, PoseStack poseStack, SpinosaurusEntity animatable, BakedGeoModel model, boolean isReRender, float partialTick, int packedLight, int packedOverlay) { + poseStack.scale(animalScale, animalScale, animalScale); + if(animatable.isBaby()) { + float growthProgress = Mth.clamp((24000.0F + animatable.getSyncedAge()) / 24000.0F, 0.0F, 1.0F); + float scale = Mth.lerp(growthProgress, 0.2F, 1.0F); + poseStack.scale(scale, scale, scale); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/SpinosaurusVariant.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/SpinosaurusVariant.java new file mode 100644 index 0000000..d53a0d8 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/SpinosaurusVariant.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import java.util.Arrays; +import java.util.Comparator; + +public enum SpinosaurusVariant { + MALE(0), + FEMALE(1); + + private static final SpinosaurusVariant[] BY_ID = Arrays.stream(values()).sorted( + Comparator.comparingInt(SpinosaurusVariant::getId)).toArray(SpinosaurusVariant[]::new); + + private final int id; + + SpinosaurusVariant(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static SpinosaurusVariant byId(int id) { + return BY_ID[id % BY_ID.length]; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/StegosaurusModel.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/StegosaurusModel.java new file mode 100644 index 0000000..902930d --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/StegosaurusModel.java @@ -0,0 +1,112 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.google.common.collect.Maps; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.entity.custom.StegosaurusEntity; +import net.minecraft.Util; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animation.AnimationState; +/*?} else {*/ +/*import software.bernie.geckolib.animation.AnimationState; + *//*?}*/ +import software.bernie.geckolib.cache.object.GeoBone; +import software.bernie.geckolib.constant.DataTickets; +import software.bernie.geckolib.model.GeoModel; + +import java.util.Map; + +public class StegosaurusModel extends GeoModel { + + private static final Map LOCATION_BY_VARIANT = + Util.make(Maps.newEnumMap(StegosaurusVariant.class), map -> { + map.put(StegosaurusVariant.MALE, Constants.rl("textures/entity/stegosaurus.png")); + map.put(StegosaurusVariant.FEMALE, Constants.rl("textures/entity/stegosaurus_female.png")); + }); + + // Model-local "currently applied" offsets; cleared before each entity render + private float[] appliedYaw = null; + private float[] appliedRoll = null; + + @Override + public ResourceLocation getModelResource(StegosaurusEntity animatable) { + return Constants.rl("geo/stegosaurus.geo.json"); + } + + @Override + public ResourceLocation getTextureResource(StegosaurusEntity animatable) { + return LOCATION_BY_VARIANT.get(animatable.getVariant()); + } + + @Override + public ResourceLocation getAnimationResource(StegosaurusEntity animatable) { + return Constants.rl("animations/stegosaurus.animation.json"); + } + + @Override + public void setCustomAnimations(StegosaurusEntity entity, long id, AnimationState state) { + super.setCustomAnimations(entity, id, state); + + String[] tailBones = { "Tail1", "Tail2", "Tail3", "Tail4", "Tail5", "Tail6" }; + int n = tailBones.length; + + if (appliedYaw == null || appliedYaw.length != n) { + appliedYaw = new float[n]; + appliedRoll = new float[n]; + } + + // 1) Clear previous offsets (from the last entity rendered with this model instance) + for (int i = 0; i < n; i++) { + if (appliedYaw[i] == 0.0f && appliedRoll[i] == 0.0f) continue; + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + if (appliedYaw[i] != 0.0f) bone.setRotY(bone.getRotY() - appliedYaw[i]); + if (appliedRoll[i] != 0.0f) bone.setRotZ(bone.getRotZ() - appliedRoll[i]); + appliedYaw[i] = 0.0f; + appliedRoll[i] = 0.0f; + } + + // 2) Interpolated sway for extra smoothness between ticks + float sway = entity.getTailSwayOffset(state.getPartialTick()); // [-1, 1] + + // Tuning + float maxYawDeg = 22.0f; // increased max sweep + float swayGain = 1.35f; // amplifies overall power + float rollFraction = 0.40f; // slightly stronger roll for heft + + float deg2rad = (float)Math.PI / 180f; + + // Direction: positive sway (left turn) -> tail swings right (negative yaw) + // Flip the sign here if the sway feels inverted + float baseYaw = sway * maxYawDeg * deg2rad; + float baseRoll = -baseYaw * rollFraction; + + float[] weights = { 1.00f, 0.78f, 0.58f, 0.42f, 0.30f, 0.22f }; + + for (int i = 0; i < n; i++) { + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + + float w = weights[i]; + float yaw = baseYaw * w; + float roll = baseRoll * w; + + // OVERRIDE animations on Y/Z only: keep the model's predefined X bend intact + // Do NOT reset rotX here, so the upward bend stays + bone.setRotY(yaw); + bone.setRotZ(roll); + + appliedYaw[i] = yaw; + appliedRoll[i] = roll; + } + + GeoBone head = (GeoBone) getAnimationProcessor().getBone("Neck3"); + + if (head != null) { + var entityData = state.getData(DataTickets.ENTITY_MODEL_DATA); + float clampedYawDeg = Mth.clamp(entityData.netHeadYaw(), -20.0f, 20.0f); + head.setRotY(clampedYawDeg * Mth.DEG_TO_RAD); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/StegosaurusRenderer.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/StegosaurusRenderer.java new file mode 100644 index 0000000..daa5077 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/StegosaurusRenderer.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.cmr.jurassicrevived.entity.custom.StegosaurusEntity; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.util.Mth; +import software.bernie.geckolib.cache.object.BakedGeoModel; +import software.bernie.geckolib.renderer.GeoEntityRenderer; + + +public class StegosaurusRenderer extends GeoEntityRenderer { + private final float animalScale = 1.62F; + public StegosaurusRenderer(EntityRendererProvider.Context renderManager) { + super(renderManager, new StegosaurusModel()); + } + + @Override + public void scaleModelForRender(float widthScale, float heightScale, PoseStack poseStack, StegosaurusEntity animatable, BakedGeoModel model, boolean isReRender, float partialTick, int packedLight, int packedOverlay) { + poseStack.scale(animalScale, animalScale, animalScale); + if(animatable.isBaby()) { + float growthProgress = Mth.clamp((24000.0F + animatable.getSyncedAge()) / 24000.0F, 0.0F, 1.0F); + float scale = Mth.lerp(growthProgress, 0.2F, 1.0F); + poseStack.scale(scale, scale, scale); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/StegosaurusVariant.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/StegosaurusVariant.java new file mode 100644 index 0000000..fe83ba1 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/StegosaurusVariant.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import java.util.Arrays; +import java.util.Comparator; + +public enum StegosaurusVariant { + MALE(0), + FEMALE(1); + + private static final StegosaurusVariant[] BY_ID = Arrays.stream(values()).sorted( + Comparator.comparingInt(StegosaurusVariant::getId)).toArray(StegosaurusVariant[]::new); + + private final int id; + + StegosaurusVariant(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static StegosaurusVariant byId(int id) { + return BY_ID[id % BY_ID.length]; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/StyracosaurusModel.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/StyracosaurusModel.java new file mode 100644 index 0000000..45a0a27 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/StyracosaurusModel.java @@ -0,0 +1,112 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.google.common.collect.Maps; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.entity.custom.StyracosaurusEntity; +import net.minecraft.Util; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animation.AnimationState; +/*?} else {*/ +/*import software.bernie.geckolib.animation.AnimationState; + *//*?}*/ +import software.bernie.geckolib.cache.object.GeoBone; +import software.bernie.geckolib.constant.DataTickets; +import software.bernie.geckolib.model.GeoModel; + +import java.util.Map; + +public class StyracosaurusModel extends GeoModel { + + private static final Map LOCATION_BY_VARIANT = + Util.make(Maps.newEnumMap(StyracosaurusVariant.class), map -> { + map.put(StyracosaurusVariant.MALE, Constants.rl("textures/entity/styracosaurus.png")); + map.put(StyracosaurusVariant.FEMALE, Constants.rl("textures/entity/styracosaurus_female.png")); + }); + + // Model-local "currently applied" offsets; cleared before each entity render + private float[] appliedYaw = null; + private float[] appliedRoll = null; + + @Override + public ResourceLocation getModelResource(StyracosaurusEntity animatable) { + return Constants.rl("geo/styracosaurus.geo.json"); + } + + @Override + public ResourceLocation getTextureResource(StyracosaurusEntity animatable) { + return LOCATION_BY_VARIANT.get(animatable.getVariant()); + } + + @Override + public ResourceLocation getAnimationResource(StyracosaurusEntity animatable) { + return Constants.rl("animations/styracosaurus.animation.json"); + } + + @Override + public void setCustomAnimations(StyracosaurusEntity entity, long id, AnimationState state) { + super.setCustomAnimations(entity, id, state); + + String[] tailBones = { "Tail1", "Tail2", "Tail3", "Tail4", "Tail5", "Tail6", "Tail7" }; + int n = tailBones.length; + + if (appliedYaw == null || appliedYaw.length != n) { + appliedYaw = new float[n]; + appliedRoll = new float[n]; + } + + // 1) Clear previous offsets (from the last entity rendered with this model instance) + for (int i = 0; i < n; i++) { + if (appliedYaw[i] == 0.0f && appliedRoll[i] == 0.0f) continue; + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + if (appliedYaw[i] != 0.0f) bone.setRotY(bone.getRotY() - appliedYaw[i]); + if (appliedRoll[i] != 0.0f) bone.setRotZ(bone.getRotZ() - appliedRoll[i]); + appliedYaw[i] = 0.0f; + appliedRoll[i] = 0.0f; + } + + // 2) Interpolated sway for extra smoothness between ticks + float sway = entity.getTailSwayOffset(state.getPartialTick()); // [-1, 1] + + // Tuning + float maxYawDeg = 22.0f; // increased max sweep + float swayGain = 1.35f; // amplifies overall power + float rollFraction = 0.40f; // slightly stronger roll for heft + + float deg2rad = (float)Math.PI / 180f; + + // Direction: positive sway (left turn) -> tail swings right (negative yaw) + // Flip the sign here if the sway feels inverted + float baseYaw = sway * maxYawDeg * deg2rad; + float baseRoll = -baseYaw * rollFraction; + + float[] weights = { 1.00f, 0.78f, 0.58f, 0.42f, 0.30f, 0.22f, 0.16f }; + + for (int i = 0; i < n; i++) { + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + + float w = weights[i]; + float yaw = baseYaw * w; + float roll = baseRoll * w; + + // OVERRIDE animations on Y/Z only: keep the model's predefined X bend intact + // Do NOT reset rotX here, so the upward bend stays + bone.setRotY(yaw); + bone.setRotZ(roll); + + appliedYaw[i] = yaw; + appliedRoll[i] = roll; + } + + GeoBone head = (GeoBone) getAnimationProcessor().getBone("Neck1"); + + if (head != null) { + var entityData = state.getData(DataTickets.ENTITY_MODEL_DATA); + float clampedYawDeg = Mth.clamp(entityData.netHeadYaw(), -20.0f, 20.0f); + head.setRotY(clampedYawDeg * Mth.DEG_TO_RAD); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/StyracosaurusRenderer.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/StyracosaurusRenderer.java new file mode 100644 index 0000000..0ba9d66 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/StyracosaurusRenderer.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.cmr.jurassicrevived.entity.custom.StyracosaurusEntity; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.util.Mth; +import software.bernie.geckolib.cache.object.BakedGeoModel; +import software.bernie.geckolib.renderer.GeoEntityRenderer; + + +public class StyracosaurusRenderer extends GeoEntityRenderer { + private final float animalScale = 1.28F; + public StyracosaurusRenderer(EntityRendererProvider.Context renderManager) { + super(renderManager, new StyracosaurusModel()); + } + + @Override + public void scaleModelForRender(float widthScale, float heightScale, PoseStack poseStack, StyracosaurusEntity animatable, BakedGeoModel model, boolean isReRender, float partialTick, int packedLight, int packedOverlay) { + poseStack.scale(animalScale, animalScale, animalScale); + if(animatable.isBaby()) { + float growthProgress = Mth.clamp((24000.0F + animatable.getSyncedAge()) / 24000.0F, 0.0F, 1.0F); + float scale = Mth.lerp(growthProgress, 0.2F, 1.0F); + poseStack.scale(scale, scale, scale); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/StyracosaurusVariant.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/StyracosaurusVariant.java new file mode 100644 index 0000000..8ed25b6 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/StyracosaurusVariant.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import java.util.Arrays; +import java.util.Comparator; + +public enum StyracosaurusVariant { + MALE(0), + FEMALE(1); + + private static final StyracosaurusVariant[] BY_ID = Arrays.stream(values()).sorted( + Comparator.comparingInt(StyracosaurusVariant::getId)).toArray(StyracosaurusVariant[]::new); + + private final int id; + + StyracosaurusVariant(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static StyracosaurusVariant byId(int id) { + return BY_ID[id % BY_ID.length]; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/TapejaraModel.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/TapejaraModel.java new file mode 100644 index 0000000..864ef84 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/TapejaraModel.java @@ -0,0 +1,112 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.google.common.collect.Maps; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.entity.custom.TapejaraEntity; +import net.minecraft.Util; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animation.AnimationState; +/*?} else {*/ +/*import software.bernie.geckolib.animation.AnimationState; + *//*?}*/ +import software.bernie.geckolib.cache.object.GeoBone; +import software.bernie.geckolib.constant.DataTickets; +import software.bernie.geckolib.model.GeoModel; + +import java.util.Map; + +public class TapejaraModel extends GeoModel { + + private static final Map LOCATION_BY_VARIANT = + Util.make(Maps.newEnumMap(TapejaraVariant.class), map -> { + map.put(TapejaraVariant.MALE, Constants.rl("textures/entity/tapejara.png")); + map.put(TapejaraVariant.FEMALE, Constants.rl("textures/entity/tapejara_female.png")); + }); + + // Model-local "currently applied" offsets; cleared before each entity render + private float[] appliedYaw = null; + private float[] appliedRoll = null; + + @Override + public ResourceLocation getModelResource(TapejaraEntity animatable) { + return Constants.rl("geo/tapejara.geo.json"); + } + + @Override + public ResourceLocation getTextureResource(TapejaraEntity animatable) { + return LOCATION_BY_VARIANT.get(animatable.getVariant()); + } + + @Override + public ResourceLocation getAnimationResource(TapejaraEntity animatable) { + return Constants.rl("animations/tapejara.animation.json"); + } + + @Override + public void setCustomAnimations(TapejaraEntity entity, long id, AnimationState state) { + super.setCustomAnimations(entity, id, state); + + String[] tailBones = { "Tail1", "Tail2", "Tail3" }; + int n = tailBones.length; + + if (appliedYaw == null || appliedYaw.length != n) { + appliedYaw = new float[n]; + appliedRoll = new float[n]; + } + + // 1) Clear previous offsets (from the last entity rendered with this model instance) + for (int i = 0; i < n; i++) { + if (appliedYaw[i] == 0.0f && appliedRoll[i] == 0.0f) continue; + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + if (appliedYaw[i] != 0.0f) bone.setRotY(bone.getRotY() - appliedYaw[i]); + if (appliedRoll[i] != 0.0f) bone.setRotZ(bone.getRotZ() - appliedRoll[i]); + appliedYaw[i] = 0.0f; + appliedRoll[i] = 0.0f; + } + + // 2) Interpolated sway for extra smoothness between ticks + float sway = entity.getTailSwayOffset(state.getPartialTick()); // [-1, 1] + + // Tuning + float maxYawDeg = 22.0f; // increased max sweep + float swayGain = 1.35f; // amplifies overall power + float rollFraction = 0.40f; // slightly stronger roll for heft + + float deg2rad = (float)Math.PI / 180f; + + // Direction: positive sway (left turn) -> tail swings right (negative yaw) + // Flip the sign here if the sway feels inverted + float baseYaw = sway * maxYawDeg * deg2rad; + float baseRoll = -baseYaw * rollFraction; + + float[] weights = { 1.00f, 0.78f, 0.58f }; + + for (int i = 0; i < n; i++) { + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + + float w = weights[i]; + float yaw = baseYaw * w; + float roll = baseRoll * w; + + // OVERRIDE animations on Y/Z only: keep the model's predefined X bend intact + // Do NOT reset rotX here, so the upward bend stays + bone.setRotY(yaw); + bone.setRotZ(roll); + + appliedYaw[i] = yaw; + appliedRoll[i] = roll; + } + + GeoBone head = (GeoBone) getAnimationProcessor().getBone("Neck1"); + + if (head != null) { + var entityData = state.getData(DataTickets.ENTITY_MODEL_DATA); + float clampedYawDeg = Mth.clamp(entityData.netHeadYaw(), -20.0f, 20.0f); + head.setRotY(clampedYawDeg * Mth.DEG_TO_RAD); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/TapejaraRenderer.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/TapejaraRenderer.java new file mode 100644 index 0000000..878fd15 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/TapejaraRenderer.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.cmr.jurassicrevived.entity.custom.TapejaraEntity; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.util.Mth; +import software.bernie.geckolib.cache.object.BakedGeoModel; +import software.bernie.geckolib.renderer.GeoEntityRenderer; + + +public class TapejaraRenderer extends GeoEntityRenderer { + private final float animalScale = 0.45F; + public TapejaraRenderer(EntityRendererProvider.Context renderManager) { + super(renderManager, new TapejaraModel()); + } + + @Override + public void scaleModelForRender(float widthScale, float heightScale, PoseStack poseStack, TapejaraEntity animatable, BakedGeoModel model, boolean isReRender, float partialTick, int packedLight, int packedOverlay) { + poseStack.scale(animalScale, animalScale, animalScale); + if(animatable.isBaby()) { + float growthProgress = Mth.clamp((24000.0F + animatable.getSyncedAge()) / 24000.0F, 0.0F, 1.0F); + float scale = Mth.lerp(growthProgress, 0.2F, 1.0F); + poseStack.scale(scale, scale, scale); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/TapejaraVariant.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/TapejaraVariant.java new file mode 100644 index 0000000..d1206e1 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/TapejaraVariant.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import java.util.Arrays; +import java.util.Comparator; + +public enum TapejaraVariant { + MALE(0), + FEMALE(1); + + private static final TapejaraVariant[] BY_ID = Arrays.stream(values()).sorted( + Comparator.comparingInt(TapejaraVariant::getId)).toArray(TapejaraVariant[]::new); + + private final int id; + + TapejaraVariant(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static TapejaraVariant byId(int id) { + return BY_ID[id % BY_ID.length]; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/TherizinosaurusModel.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/TherizinosaurusModel.java new file mode 100644 index 0000000..f376b31 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/TherizinosaurusModel.java @@ -0,0 +1,112 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.google.common.collect.Maps; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.entity.custom.TherizinosaurusEntity; +import net.minecraft.Util; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animation.AnimationState; +/*?} else {*/ +/*import software.bernie.geckolib.animation.AnimationState; + *//*?}*/ +import software.bernie.geckolib.cache.object.GeoBone; +import software.bernie.geckolib.constant.DataTickets; +import software.bernie.geckolib.model.GeoModel; + +import java.util.Map; + +public class TherizinosaurusModel extends GeoModel { + + private static final Map LOCATION_BY_VARIANT = + Util.make(Maps.newEnumMap(TherizinosaurusVariant.class), map -> { + map.put(TherizinosaurusVariant.MALE, Constants.rl("textures/entity/therizinosaurus.png")); + map.put(TherizinosaurusVariant.FEMALE, Constants.rl("textures/entity/therizinosaurus_female.png")); + }); + + // Model-local "currently applied" offsets; cleared before each entity render + private float[] appliedYaw = null; + private float[] appliedRoll = null; + + @Override + public ResourceLocation getModelResource(TherizinosaurusEntity animatable) { + return Constants.rl("geo/therizinosaurus.geo.json"); + } + + @Override + public ResourceLocation getTextureResource(TherizinosaurusEntity animatable) { + return LOCATION_BY_VARIANT.get(animatable.getVariant()); + } + + @Override + public ResourceLocation getAnimationResource(TherizinosaurusEntity animatable) { + return Constants.rl("animations/therizinosaurus.animation.json"); + } + + @Override + public void setCustomAnimations(TherizinosaurusEntity entity, long id, AnimationState state) { + super.setCustomAnimations(entity, id, state); + + String[] tailBones = { "Tail1", "Tail2", "Tail3", "Tail4", "Tail5", "Tail6" }; + int n = tailBones.length; + + if (appliedYaw == null || appliedYaw.length != n) { + appliedYaw = new float[n]; + appliedRoll = new float[n]; + } + + // 1) Clear previous offsets (from the last entity rendered with this model instance) + for (int i = 0; i < n; i++) { + if (appliedYaw[i] == 0.0f && appliedRoll[i] == 0.0f) continue; + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + if (appliedYaw[i] != 0.0f) bone.setRotY(bone.getRotY() - appliedYaw[i]); + if (appliedRoll[i] != 0.0f) bone.setRotZ(bone.getRotZ() - appliedRoll[i]); + appliedYaw[i] = 0.0f; + appliedRoll[i] = 0.0f; + } + + // 2) Interpolated sway for extra smoothness between ticks + float sway = entity.getTailSwayOffset(state.getPartialTick()); // [-1, 1] + + // Tuning + float maxYawDeg = 22.0f; // increased max sweep + float swayGain = 1.35f; // amplifies overall power + float rollFraction = 0.40f; // slightly stronger roll for heft + + float deg2rad = (float)Math.PI / 180f; + + // Direction: positive sway (left turn) -> tail swings right (negative yaw) + // Flip the sign here if the sway feels inverted + float baseYaw = sway * maxYawDeg * deg2rad; + float baseRoll = -baseYaw * rollFraction; + + float[] weights = { 1.00f, 0.78f, 0.58f, 0.42f, 0.30f, 0.22f }; + + for (int i = 0; i < n; i++) { + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + + float w = weights[i]; + float yaw = baseYaw * w; + float roll = baseRoll * w; + + // OVERRIDE animations on Y/Z only: keep the model's predefined X bend intact + // Do NOT reset rotX here, so the upward bend stays + bone.setRotY(yaw); + bone.setRotZ(roll); + + appliedYaw[i] = yaw; + appliedRoll[i] = roll; + } + + GeoBone head = (GeoBone) getAnimationProcessor().getBone("NeckBase"); + + if (head != null) { + var entityData = state.getData(DataTickets.ENTITY_MODEL_DATA); + float clampedYawDeg = Mth.clamp(entityData.netHeadYaw(), -20.0f, 20.0f); + head.setRotY(clampedYawDeg * Mth.DEG_TO_RAD); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/TherizinosaurusRenderer.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/TherizinosaurusRenderer.java new file mode 100644 index 0000000..88ec90b --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/TherizinosaurusRenderer.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.cmr.jurassicrevived.entity.custom.TherizinosaurusEntity; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.util.Mth; +import software.bernie.geckolib.cache.object.BakedGeoModel; +import software.bernie.geckolib.renderer.GeoEntityRenderer; + + +public class TherizinosaurusRenderer extends GeoEntityRenderer { + private final float animalScale = 2.5F; + public TherizinosaurusRenderer(EntityRendererProvider.Context renderManager) { + super(renderManager, new TherizinosaurusModel()); + } + + @Override + public void scaleModelForRender(float widthScale, float heightScale, PoseStack poseStack, TherizinosaurusEntity animatable, BakedGeoModel model, boolean isReRender, float partialTick, int packedLight, int packedOverlay) { + poseStack.scale(animalScale, animalScale, animalScale); + if(animatable.isBaby()) { + float growthProgress = Mth.clamp((24000.0F + animatable.getSyncedAge()) / 24000.0F, 0.0F, 1.0F); + float scale = Mth.lerp(growthProgress, 0.2F, 1.0F); + poseStack.scale(scale, scale, scale); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/TherizinosaurusVariant.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/TherizinosaurusVariant.java new file mode 100644 index 0000000..be93dd6 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/TherizinosaurusVariant.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import java.util.Arrays; +import java.util.Comparator; + +public enum TherizinosaurusVariant { + MALE(0), + FEMALE(1); + + private static final TherizinosaurusVariant[] BY_ID = Arrays.stream(values()).sorted( + Comparator.comparingInt(TherizinosaurusVariant::getId)).toArray(TherizinosaurusVariant[]::new); + + private final int id; + + TherizinosaurusVariant(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static TherizinosaurusVariant byId(int id) { + return BY_ID[id % BY_ID.length]; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/TitanosaurusModel.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/TitanosaurusModel.java new file mode 100644 index 0000000..f2e09dd --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/TitanosaurusModel.java @@ -0,0 +1,112 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.google.common.collect.Maps; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.entity.custom.TitanosaurusEntity; +import net.minecraft.Util; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animation.AnimationState; +/*?} else {*/ +/*import software.bernie.geckolib.animation.AnimationState; + *//*?}*/ +import software.bernie.geckolib.cache.object.GeoBone; +import software.bernie.geckolib.constant.DataTickets; +import software.bernie.geckolib.model.GeoModel; + +import java.util.Map; + +public class TitanosaurusModel extends GeoModel { + + private static final Map LOCATION_BY_VARIANT = + Util.make(Maps.newEnumMap(TitanosaurusVariant.class), map -> { + map.put(TitanosaurusVariant.MALE, Constants.rl("textures/entity/titanosaurus.png")); + map.put(TitanosaurusVariant.FEMALE, Constants.rl("textures/entity/titanosaurus_female.png")); + }); + + // Model-local "currently applied" offsets; cleared before each entity render + private float[] appliedYaw = null; + private float[] appliedRoll = null; + + @Override + public ResourceLocation getModelResource(TitanosaurusEntity animatable) { + return Constants.rl("geo/titanosaurus.geo.json"); + } + + @Override + public ResourceLocation getTextureResource(TitanosaurusEntity animatable) { + return LOCATION_BY_VARIANT.get(animatable.getVariant()); + } + + @Override + public ResourceLocation getAnimationResource(TitanosaurusEntity animatable) { + return Constants.rl("animations/titanosaurus.animation.json"); + } + + @Override + public void setCustomAnimations(TitanosaurusEntity entity, long id, AnimationState state) { + super.setCustomAnimations(entity, id, state); + + String[] tailBones = { "tail", "tail1", "tail2", "tail3", "tail4", "tail5", "tail6", "tail7" }; + int n = tailBones.length; + + if (appliedYaw == null || appliedYaw.length != n) { + appliedYaw = new float[n]; + appliedRoll = new float[n]; + } + + // 1) Clear previous offsets (from the last entity rendered with this model instance) + for (int i = 0; i < n; i++) { + if (appliedYaw[i] == 0.0f && appliedRoll[i] == 0.0f) continue; + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + if (appliedYaw[i] != 0.0f) bone.setRotY(bone.getRotY() - appliedYaw[i]); + if (appliedRoll[i] != 0.0f) bone.setRotZ(bone.getRotZ() - appliedRoll[i]); + appliedYaw[i] = 0.0f; + appliedRoll[i] = 0.0f; + } + + // 2) Interpolated sway for extra smoothness between ticks + float sway = entity.getTailSwayOffset(state.getPartialTick()); // [-1, 1] + + // Tuning + float maxYawDeg = 22.0f; // increased max sweep + float swayGain = 1.35f; // amplifies overall power + float rollFraction = 0.40f; // slightly stronger roll for heft + + float deg2rad = (float)Math.PI / 180f; + + // Direction: positive sway (left turn) -> tail swings right (negative yaw) + // Flip the sign here if the sway feels inverted + float baseYaw = sway * maxYawDeg * deg2rad; + float baseRoll = -baseYaw * rollFraction; + + float[] weights = { 1.00f, 0.78f, 0.58f, 0.42f, 0.30f, 0.22f, 0.16f, 0.12f }; + + for (int i = 0; i < n; i++) { + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + + float w = weights[i]; + float yaw = baseYaw * w; + float roll = baseRoll * w; + + // OVERRIDE animations on Y/Z only: keep the model's predefined X bend intact + // Do NOT reset rotX here, so the upward bend stays + bone.setRotY(yaw); + bone.setRotZ(roll); + + appliedYaw[i] = yaw; + appliedRoll[i] = roll; + } + + GeoBone head = (GeoBone) getAnimationProcessor().getBone("neck"); + + if (head != null) { + var entityData = state.getData(DataTickets.ENTITY_MODEL_DATA); + float clampedYawDeg = Mth.clamp(entityData.netHeadYaw(), -10.0f, 10.0f); + head.setRotY(clampedYawDeg * Mth.DEG_TO_RAD); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/TitanosaurusRenderer.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/TitanosaurusRenderer.java new file mode 100644 index 0000000..d68aad0 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/TitanosaurusRenderer.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.cmr.jurassicrevived.entity.custom.TitanosaurusEntity; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.util.Mth; +import software.bernie.geckolib.cache.object.BakedGeoModel; +import software.bernie.geckolib.renderer.GeoEntityRenderer; + + +public class TitanosaurusRenderer extends GeoEntityRenderer { + private final float animalScale = 2.1F; + public TitanosaurusRenderer(EntityRendererProvider.Context renderManager) { + super(renderManager, new TitanosaurusModel()); + } + + @Override + public void scaleModelForRender(float widthScale, float heightScale, PoseStack poseStack, TitanosaurusEntity animatable, BakedGeoModel model, boolean isReRender, float partialTick, int packedLight, int packedOverlay) { + poseStack.scale(animalScale, animalScale, animalScale); + if(animatable.isBaby()) { + float growthProgress = Mth.clamp((24000.0F + animatable.getSyncedAge()) / 24000.0F, 0.0F, 1.0F); + float scale = Mth.lerp(growthProgress, 0.2F, 1.0F); + poseStack.scale(scale, scale, scale); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/TitanosaurusVariant.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/TitanosaurusVariant.java new file mode 100644 index 0000000..c805d5b --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/TitanosaurusVariant.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import java.util.Arrays; +import java.util.Comparator; + +public enum TitanosaurusVariant { + MALE(0), + FEMALE(1); + + private static final TitanosaurusVariant[] BY_ID = Arrays.stream(values()).sorted( + Comparator.comparingInt(TitanosaurusVariant::getId)).toArray(TitanosaurusVariant[]::new); + + private final int id; + + TitanosaurusVariant(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static TitanosaurusVariant byId(int id) { + return BY_ID[id % BY_ID.length]; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/TriceratopsModel.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/TriceratopsModel.java new file mode 100644 index 0000000..b6e99e7 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/TriceratopsModel.java @@ -0,0 +1,112 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.google.common.collect.Maps; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.entity.custom.TriceratopsEntity; +import net.minecraft.Util; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animation.AnimationState; +/*?} else {*/ +/*import software.bernie.geckolib.animation.AnimationState; + *//*?}*/ +import software.bernie.geckolib.cache.object.GeoBone; +import software.bernie.geckolib.constant.DataTickets; +import software.bernie.geckolib.model.GeoModel; + +import java.util.Map; + +public class TriceratopsModel extends GeoModel { + + private static final Map LOCATION_BY_VARIANT = + Util.make(Maps.newEnumMap(TriceratopsVariant.class), map -> { + map.put(TriceratopsVariant.MALE, Constants.rl("textures/entity/triceratops.png")); + map.put(TriceratopsVariant.FEMALE, Constants.rl("textures/entity/triceratops_female.png")); + }); + + // Model-local "currently applied" offsets; cleared before each entity render + private float[] appliedYaw = null; + private float[] appliedRoll = null; + + @Override + public ResourceLocation getModelResource(TriceratopsEntity animatable) { + return Constants.rl("geo/triceratops.geo.json"); + } + + @Override + public ResourceLocation getTextureResource(TriceratopsEntity animatable) { + return LOCATION_BY_VARIANT.get(animatable.getVariant()); + } + + @Override + public ResourceLocation getAnimationResource(TriceratopsEntity animatable) { + return Constants.rl("animations/triceratops.animation.json"); + } + + @Override + public void setCustomAnimations(TriceratopsEntity entity, long id, AnimationState state) { + super.setCustomAnimations(entity, id, state); + + String[] tailBones = { "Tail1", "Tail2", "Tail3", "Tail4", "Tail5", "Tail6" }; + int n = tailBones.length; + + if (appliedYaw == null || appliedYaw.length != n) { + appliedYaw = new float[n]; + appliedRoll = new float[n]; + } + + // 1) Clear previous offsets (from the last entity rendered with this model instance) + for (int i = 0; i < n; i++) { + if (appliedYaw[i] == 0.0f && appliedRoll[i] == 0.0f) continue; + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + if (appliedYaw[i] != 0.0f) bone.setRotY(bone.getRotY() - appliedYaw[i]); + if (appliedRoll[i] != 0.0f) bone.setRotZ(bone.getRotZ() - appliedRoll[i]); + appliedYaw[i] = 0.0f; + appliedRoll[i] = 0.0f; + } + + // 2) Interpolated sway for extra smoothness between ticks + float sway = entity.getTailSwayOffset(state.getPartialTick()); // [-1, 1] + + // Tuning + float maxYawDeg = 22.0f; // increased max sweep + float swayGain = 1.35f; // amplifies overall power + float rollFraction = 0.40f; // slightly stronger roll for heft + + float deg2rad = (float)Math.PI / 180f; + + // Direction: positive sway (left turn) -> tail swings right (negative yaw) + // Flip the sign here if the sway feels inverted + float baseYaw = sway * maxYawDeg * deg2rad; + float baseRoll = -baseYaw * rollFraction; + + float[] weights = { 1.00f, 0.78f, 0.58f, 0.42f, 0.30f, 0.22f }; + + for (int i = 0; i < n; i++) { + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + + float w = weights[i]; + float yaw = baseYaw * w; + float roll = baseRoll * w; + + // OVERRIDE animations on Y/Z only: keep the model's predefined X bend intact + // Do NOT reset rotX here, so the upward bend stays + bone.setRotY(yaw); + bone.setRotZ(roll); + + appliedYaw[i] = yaw; + appliedRoll[i] = roll; + } + + GeoBone head = (GeoBone) getAnimationProcessor().getBone("Neck1"); + + if (head != null) { + var entityData = state.getData(DataTickets.ENTITY_MODEL_DATA); + float clampedYawDeg = Mth.clamp(entityData.netHeadYaw(), -30.0f, 30.0f); + head.setRotY(clampedYawDeg * Mth.DEG_TO_RAD); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/TriceratopsRenderer.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/TriceratopsRenderer.java new file mode 100644 index 0000000..f589f07 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/TriceratopsRenderer.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.cmr.jurassicrevived.entity.custom.TriceratopsEntity; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.util.Mth; +import software.bernie.geckolib.cache.object.BakedGeoModel; +import software.bernie.geckolib.renderer.GeoEntityRenderer; + + +public class TriceratopsRenderer extends GeoEntityRenderer { + private final float animalScale = 1.5F; + public TriceratopsRenderer(EntityRendererProvider.Context renderManager) { + super(renderManager, new TriceratopsModel()); + } + + @Override + public void scaleModelForRender(float widthScale, float heightScale, PoseStack poseStack, TriceratopsEntity animatable, BakedGeoModel model, boolean isReRender, float partialTick, int packedLight, int packedOverlay) { + poseStack.scale(animalScale, animalScale, animalScale); + if(animatable.isBaby()) { + float growthProgress = Mth.clamp((24000.0F + animatable.getSyncedAge()) / 24000.0F, 0.0F, 1.0F); + float scale = Mth.lerp(growthProgress, 0.2F, 1.0F); + poseStack.scale(scale, scale, scale); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/TriceratopsVariant.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/TriceratopsVariant.java new file mode 100644 index 0000000..f1e534f --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/TriceratopsVariant.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import java.util.Arrays; +import java.util.Comparator; + +public enum TriceratopsVariant { + MALE(0), + FEMALE(1); + + private static final TriceratopsVariant[] BY_ID = Arrays.stream(values()).sorted( + Comparator.comparingInt(TriceratopsVariant::getId)).toArray(TriceratopsVariant[]::new); + + private final int id; + + TriceratopsVariant(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static TriceratopsVariant byId(int id) { + return BY_ID[id % BY_ID.length]; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/TroodonModel.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/TroodonModel.java new file mode 100644 index 0000000..93aa642 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/TroodonModel.java @@ -0,0 +1,112 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.google.common.collect.Maps; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.entity.custom.TroodonEntity; +import net.minecraft.Util; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animation.AnimationState; +/*?} else {*/ +/*import software.bernie.geckolib.animation.AnimationState; + *//*?}*/ +import software.bernie.geckolib.cache.object.GeoBone; +import software.bernie.geckolib.constant.DataTickets; +import software.bernie.geckolib.model.GeoModel; + +import java.util.Map; + +public class TroodonModel extends GeoModel { + + private static final Map LOCATION_BY_VARIANT = + Util.make(Maps.newEnumMap(TroodonVariant.class), map -> { + map.put(TroodonVariant.MALE, Constants.rl("textures/entity/troodon.png")); + map.put(TroodonVariant.FEMALE, Constants.rl("textures/entity/troodon_female.png")); + }); + + // Model-local "currently applied" offsets; cleared before each entity render + private float[] appliedYaw = null; + private float[] appliedRoll = null; + + @Override + public ResourceLocation getModelResource(TroodonEntity animatable) { + return Constants.rl("geo/troodon.geo.json"); + } + + @Override + public ResourceLocation getTextureResource(TroodonEntity animatable) { + return LOCATION_BY_VARIANT.get(animatable.getVariant()); + } + + @Override + public ResourceLocation getAnimationResource(TroodonEntity animatable) { + return Constants.rl("animations/troodon.animation.json"); + } + + @Override + public void setCustomAnimations(TroodonEntity entity, long id, AnimationState state) { + super.setCustomAnimations(entity, id, state); + + String[] tailBones = { "Tailstart", "Tail1", "Tail2", "Tail3", "Tail4", "Tail5" }; + int n = tailBones.length; + + if (appliedYaw == null || appliedYaw.length != n) { + appliedYaw = new float[n]; + appliedRoll = new float[n]; + } + + // 1) Clear previous offsets (from the last entity rendered with this model instance) + for (int i = 0; i < n; i++) { + if (appliedYaw[i] == 0.0f && appliedRoll[i] == 0.0f) continue; + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + if (appliedYaw[i] != 0.0f) bone.setRotY(bone.getRotY() - appliedYaw[i]); + if (appliedRoll[i] != 0.0f) bone.setRotZ(bone.getRotZ() - appliedRoll[i]); + appliedYaw[i] = 0.0f; + appliedRoll[i] = 0.0f; + } + + // 2) Interpolated sway for extra smoothness between ticks + float sway = entity.getTailSwayOffset(state.getPartialTick()); // [-1, 1] + + // Tuning + float maxYawDeg = 22.0f; // increased max sweep + float swayGain = 1.35f; // amplifies overall power + float rollFraction = 0.40f; // slightly stronger roll for heft + + float deg2rad = (float)Math.PI / 180f; + + // Direction: positive sway (left turn) -> tail swings right (negative yaw) + // Flip the sign here if the sway feels inverted + float baseYaw = sway * maxYawDeg * deg2rad; + float baseRoll = -baseYaw * rollFraction; + + float[] weights = { 1.00f, 0.78f, 0.58f, 0.42f, 0.30f, 0.22f }; + + for (int i = 0; i < n; i++) { + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + + float w = weights[i]; + float yaw = baseYaw * w; + float roll = baseRoll * w; + + // OVERRIDE animations on Y/Z only: keep the model's predefined X bend intact + // Do NOT reset rotX here, so the upward bend stays + bone.setRotY(yaw); + bone.setRotZ(roll); + + appliedYaw[i] = yaw; + appliedRoll[i] = roll; + } + + GeoBone head = (GeoBone) getAnimationProcessor().getBone("Neck1"); + + if (head != null) { + var entityData = state.getData(DataTickets.ENTITY_MODEL_DATA); + float clampedYawDeg = Mth.clamp(entityData.netHeadYaw(), -30.0f, 30.0f); + head.setRotY(clampedYawDeg * Mth.DEG_TO_RAD); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/TroodonRenderer.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/TroodonRenderer.java new file mode 100644 index 0000000..b2fca3d --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/TroodonRenderer.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.cmr.jurassicrevived.entity.custom.TroodonEntity; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.util.Mth; +import software.bernie.geckolib.cache.object.BakedGeoModel; +import software.bernie.geckolib.renderer.GeoEntityRenderer; + + +public class TroodonRenderer extends GeoEntityRenderer { + private final float animalScale = 0.5F; + public TroodonRenderer(EntityRendererProvider.Context renderManager) { + super(renderManager, new TroodonModel()); + } + + @Override + public void scaleModelForRender(float widthScale, float heightScale, PoseStack poseStack, TroodonEntity animatable, BakedGeoModel model, boolean isReRender, float partialTick, int packedLight, int packedOverlay) { + poseStack.scale(animalScale, animalScale, animalScale); + if(animatable.isBaby()) { + float growthProgress = Mth.clamp((24000.0F + animatable.getSyncedAge()) / 24000.0F, 0.0F, 1.0F); + float scale = Mth.lerp(growthProgress, 0.2F, 1.0F); + poseStack.scale(scale, scale, scale); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/TroodonVariant.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/TroodonVariant.java new file mode 100644 index 0000000..6818f99 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/TroodonVariant.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import java.util.Arrays; +import java.util.Comparator; + +public enum TroodonVariant { + MALE(0), + FEMALE(1); + + private static final TroodonVariant[] BY_ID = Arrays.stream(values()).sorted( + Comparator.comparingInt(TroodonVariant::getId)).toArray(TroodonVariant[]::new); + + private final int id; + + TroodonVariant(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static TroodonVariant byId(int id) { + return BY_ID[id % BY_ID.length]; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/TropeognathusModel.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/TropeognathusModel.java new file mode 100644 index 0000000..db7cfdf --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/TropeognathusModel.java @@ -0,0 +1,112 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.google.common.collect.Maps; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.entity.custom.TropeognathusEntity; +import net.minecraft.Util; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animation.AnimationState; +/*?} else {*/ +/*import software.bernie.geckolib.animation.AnimationState; + *//*?}*/ +import software.bernie.geckolib.cache.object.GeoBone; +import software.bernie.geckolib.constant.DataTickets; +import software.bernie.geckolib.model.GeoModel; + +import java.util.Map; + +public class TropeognathusModel extends GeoModel { + + private static final Map LOCATION_BY_VARIANT = + Util.make(Maps.newEnumMap(TropeognathusVariant.class), map -> { + map.put(TropeognathusVariant.MALE, Constants.rl("textures/entity/tropeognathus.png")); + map.put(TropeognathusVariant.FEMALE, Constants.rl("textures/entity/tropeognathus_female.png")); + }); + + // Model-local "currently applied" offsets; cleared before each entity render + private float[] appliedYaw = null; + private float[] appliedRoll = null; + + @Override + public ResourceLocation getModelResource(TropeognathusEntity animatable) { + return Constants.rl("geo/tropeognathus.geo.json"); + } + + @Override + public ResourceLocation getTextureResource(TropeognathusEntity animatable) { + return LOCATION_BY_VARIANT.get(animatable.getVariant()); + } + + @Override + public ResourceLocation getAnimationResource(TropeognathusEntity animatable) { + return Constants.rl("animations/tropeognathus.animation.json"); + } + + @Override + public void setCustomAnimations(TropeognathusEntity entity, long id, AnimationState state) { + super.setCustomAnimations(entity, id, state); + + String[] tailBones = { "Tail1", "Tail2", "Tail3" }; + int n = tailBones.length; + + if (appliedYaw == null || appliedYaw.length != n) { + appliedYaw = new float[n]; + appliedRoll = new float[n]; + } + + // 1) Clear previous offsets (from the last entity rendered with this model instance) + for (int i = 0; i < n; i++) { + if (appliedYaw[i] == 0.0f && appliedRoll[i] == 0.0f) continue; + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + if (appliedYaw[i] != 0.0f) bone.setRotY(bone.getRotY() - appliedYaw[i]); + if (appliedRoll[i] != 0.0f) bone.setRotZ(bone.getRotZ() - appliedRoll[i]); + appliedYaw[i] = 0.0f; + appliedRoll[i] = 0.0f; + } + + // 2) Interpolated sway for extra smoothness between ticks + float sway = entity.getTailSwayOffset(state.getPartialTick()); // [-1, 1] + + // Tuning + float maxYawDeg = 22.0f; // increased max sweep + float swayGain = 1.35f; // amplifies overall power + float rollFraction = 0.40f; // slightly stronger roll for heft + + float deg2rad = (float)Math.PI / 180f; + + // Direction: positive sway (left turn) -> tail swings right (negative yaw) + // Flip the sign here if the sway feels inverted + float baseYaw = sway * maxYawDeg * deg2rad; + float baseRoll = -baseYaw * rollFraction; + + float[] weights = { 1.00f, 0.78f, 0.58f }; + + for (int i = 0; i < n; i++) { + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + + float w = weights[i]; + float yaw = baseYaw * w; + float roll = baseRoll * w; + + // OVERRIDE animations on Y/Z only: keep the model's predefined X bend intact + // Do NOT reset rotX here, so the upward bend stays + bone.setRotY(yaw); + bone.setRotZ(roll); + + appliedYaw[i] = yaw; + appliedRoll[i] = roll; + } + + GeoBone head = (GeoBone) getAnimationProcessor().getBone("Neck1"); + + if (head != null) { + var entityData = state.getData(DataTickets.ENTITY_MODEL_DATA); + float clampedYawDeg = Mth.clamp(entityData.netHeadYaw(), -20.0f, 20.0f); + head.setRotY(clampedYawDeg * Mth.DEG_TO_RAD); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/TropeognathusRenderer.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/TropeognathusRenderer.java new file mode 100644 index 0000000..198c196 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/TropeognathusRenderer.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.cmr.jurassicrevived.entity.custom.TropeognathusEntity; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.util.Mth; +import software.bernie.geckolib.cache.object.BakedGeoModel; +import software.bernie.geckolib.renderer.GeoEntityRenderer; + + +public class TropeognathusRenderer extends GeoEntityRenderer { + private final float animalScale = 1.0F; + public TropeognathusRenderer(EntityRendererProvider.Context renderManager) { + super(renderManager, new TropeognathusModel()); + } + + @Override + public void scaleModelForRender(float widthScale, float heightScale, PoseStack poseStack, TropeognathusEntity animatable, BakedGeoModel model, boolean isReRender, float partialTick, int packedLight, int packedOverlay) { + poseStack.scale(animalScale, animalScale, animalScale); + if(animatable.isBaby()) { + float growthProgress = Mth.clamp((24000.0F + animatable.getSyncedAge()) / 24000.0F, 0.0F, 1.0F); + float scale = Mth.lerp(growthProgress, 0.2F, 1.0F); + poseStack.scale(scale, scale, scale); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/TropeognathusVariant.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/TropeognathusVariant.java new file mode 100644 index 0000000..8793ab8 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/TropeognathusVariant.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import java.util.Arrays; +import java.util.Comparator; + +public enum TropeognathusVariant { + MALE(0), + FEMALE(1); + + private static final TropeognathusVariant[] BY_ID = Arrays.stream(values()).sorted( + Comparator.comparingInt(TropeognathusVariant::getId)).toArray(TropeognathusVariant[]::new); + + private final int id; + + TropeognathusVariant(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static TropeognathusVariant byId(int id) { + return BY_ID[id % BY_ID.length]; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/TupuxuaraModel.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/TupuxuaraModel.java new file mode 100644 index 0000000..4b1051b --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/TupuxuaraModel.java @@ -0,0 +1,112 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.google.common.collect.Maps; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.entity.custom.TupuxuaraEntity; +import net.minecraft.Util; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animation.AnimationState; +/*?} else {*/ +/*import software.bernie.geckolib.animation.AnimationState; + *//*?}*/ +import software.bernie.geckolib.cache.object.GeoBone; +import software.bernie.geckolib.constant.DataTickets; +import software.bernie.geckolib.model.GeoModel; + +import java.util.Map; + +public class TupuxuaraModel extends GeoModel { + + private static final Map LOCATION_BY_VARIANT = + Util.make(Maps.newEnumMap(TupuxuaraVariant.class), map -> { + map.put(TupuxuaraVariant.MALE, Constants.rl("textures/entity/tupuxuara.png")); + map.put(TupuxuaraVariant.FEMALE, Constants.rl("textures/entity/tupuxuara_female.png")); + }); + + // Model-local "currently applied" offsets; cleared before each entity render + private float[] appliedYaw = null; + private float[] appliedRoll = null; + + @Override + public ResourceLocation getModelResource(TupuxuaraEntity animatable) { + return Constants.rl("geo/tupuxuara.geo.json"); + } + + @Override + public ResourceLocation getTextureResource(TupuxuaraEntity animatable) { + return LOCATION_BY_VARIANT.get(animatable.getVariant()); + } + + @Override + public ResourceLocation getAnimationResource(TupuxuaraEntity animatable) { + return Constants.rl("animations/tupuxuara.animation.json"); + } + + @Override + public void setCustomAnimations(TupuxuaraEntity entity, long id, AnimationState state) { + super.setCustomAnimations(entity, id, state); + + String[] tailBones = { "Tail1", "Tail2", "Tail3" }; + int n = tailBones.length; + + if (appliedYaw == null || appliedYaw.length != n) { + appliedYaw = new float[n]; + appliedRoll = new float[n]; + } + + // 1) Clear previous offsets (from the last entity rendered with this model instance) + for (int i = 0; i < n; i++) { + if (appliedYaw[i] == 0.0f && appliedRoll[i] == 0.0f) continue; + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + if (appliedYaw[i] != 0.0f) bone.setRotY(bone.getRotY() - appliedYaw[i]); + if (appliedRoll[i] != 0.0f) bone.setRotZ(bone.getRotZ() - appliedRoll[i]); + appliedYaw[i] = 0.0f; + appliedRoll[i] = 0.0f; + } + + // 2) Interpolated sway for extra smoothness between ticks + float sway = entity.getTailSwayOffset(state.getPartialTick()); // [-1, 1] + + // Tuning + float maxYawDeg = 22.0f; // increased max sweep + float swayGain = 1.35f; // amplifies overall power + float rollFraction = 0.40f; // slightly stronger roll for heft + + float deg2rad = (float)Math.PI / 180f; + + // Direction: positive sway (left turn) -> tail swings right (negative yaw) + // Flip the sign here if the sway feels inverted + float baseYaw = sway * maxYawDeg * deg2rad; + float baseRoll = -baseYaw * rollFraction; + + float[] weights = { 1.00f, 0.78f, 0.58f }; + + for (int i = 0; i < n; i++) { + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + + float w = weights[i]; + float yaw = baseYaw * w; + float roll = baseRoll * w; + + // OVERRIDE animations on Y/Z only: keep the model's predefined X bend intact + // Do NOT reset rotX here, so the upward bend stays + bone.setRotY(yaw); + bone.setRotZ(roll); + + appliedYaw[i] = yaw; + appliedRoll[i] = roll; + } + + GeoBone head = (GeoBone) getAnimationProcessor().getBone("Neck1"); + + if (head != null) { + var entityData = state.getData(DataTickets.ENTITY_MODEL_DATA); + float clampedYawDeg = Mth.clamp(entityData.netHeadYaw(), -20.0f, 20.0f); + head.setRotY(clampedYawDeg * Mth.DEG_TO_RAD); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/TupuxuaraRenderer.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/TupuxuaraRenderer.java new file mode 100644 index 0000000..4cbd84c --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/TupuxuaraRenderer.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.cmr.jurassicrevived.entity.custom.TupuxuaraEntity; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.util.Mth; +import software.bernie.geckolib.cache.object.BakedGeoModel; +import software.bernie.geckolib.renderer.GeoEntityRenderer; + + +public class TupuxuaraRenderer extends GeoEntityRenderer { + private final float animalScale = 0.8F; + public TupuxuaraRenderer(EntityRendererProvider.Context renderManager) { + super(renderManager, new TupuxuaraModel()); + } + + @Override + public void scaleModelForRender(float widthScale, float heightScale, PoseStack poseStack, TupuxuaraEntity animatable, BakedGeoModel model, boolean isReRender, float partialTick, int packedLight, int packedOverlay) { + poseStack.scale(animalScale, animalScale, animalScale); + if(animatable.isBaby()) { + float growthProgress = Mth.clamp((24000.0F + animatable.getSyncedAge()) / 24000.0F, 0.0F, 1.0F); + float scale = Mth.lerp(growthProgress, 0.2F, 1.0F); + poseStack.scale(scale, scale, scale); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/TupuxuaraVariant.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/TupuxuaraVariant.java new file mode 100644 index 0000000..131b3de --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/TupuxuaraVariant.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import java.util.Arrays; +import java.util.Comparator; + +public enum TupuxuaraVariant { + MALE(0), + FEMALE(1); + + private static final TupuxuaraVariant[] BY_ID = Arrays.stream(values()).sorted( + Comparator.comparingInt(TupuxuaraVariant::getId)).toArray(TupuxuaraVariant[]::new); + + private final int id; + + TupuxuaraVariant(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static TupuxuaraVariant byId(int id) { + return BY_ID[id % BY_ID.length]; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/TyrannosaurusRexModel.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/TyrannosaurusRexModel.java new file mode 100644 index 0000000..6fdca42 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/TyrannosaurusRexModel.java @@ -0,0 +1,112 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.google.common.collect.Maps; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.entity.custom.TyrannosaurusRexEntity; +import net.minecraft.Util; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animation.AnimationState; +/*?} else {*/ +/*import software.bernie.geckolib.animation.AnimationState; + *//*?}*/ +import software.bernie.geckolib.cache.object.GeoBone; +import software.bernie.geckolib.constant.DataTickets; +import software.bernie.geckolib.model.GeoModel; + +import java.util.Map; + +public class TyrannosaurusRexModel extends GeoModel { + + private static final Map LOCATION_BY_VARIANT = + Util.make(Maps.newEnumMap(TyrannosaurusRexVariant.class), map -> { + map.put(TyrannosaurusRexVariant.MALE, Constants.rl("textures/entity/tyrannosaurus_rex.png")); + map.put(TyrannosaurusRexVariant.FEMALE, Constants.rl("textures/entity/tyrannosaurus_rex_female.png")); + }); + + // Model-local "currently applied" offsets; cleared before each entity render + private float[] appliedYaw = null; + private float[] appliedRoll = null; + + @Override + public ResourceLocation getModelResource(TyrannosaurusRexEntity animatable) { + return Constants.rl("geo/tyrannosaurus_rex.geo.json"); + } + + @Override + public ResourceLocation getTextureResource(TyrannosaurusRexEntity animatable) { + return LOCATION_BY_VARIANT.get(animatable.getVariant()); + } + + @Override + public ResourceLocation getAnimationResource(TyrannosaurusRexEntity animatable) { + return Constants.rl("animations/tyrannosaurus_rex.animation.json"); + } + + @Override + public void setCustomAnimations(TyrannosaurusRexEntity entity, long id, AnimationState state) { + super.setCustomAnimations(entity, id, state); + + String[] tailBones = { "Tail1", "Tail2", "Tail3", "Tail4", "Tail5", "Tail6" }; + int n = tailBones.length; + + if (appliedYaw == null || appliedYaw.length != n) { + appliedYaw = new float[n]; + appliedRoll = new float[n]; + } + + // 1) Clear previous offsets (from the last entity rendered with this model instance) + for (int i = 0; i < n; i++) { + if (appliedYaw[i] == 0.0f && appliedRoll[i] == 0.0f) continue; + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + if (appliedYaw[i] != 0.0f) bone.setRotY(bone.getRotY() - appliedYaw[i]); + if (appliedRoll[i] != 0.0f) bone.setRotZ(bone.getRotZ() - appliedRoll[i]); + appliedYaw[i] = 0.0f; + appliedRoll[i] = 0.0f; + } + + // 2) Interpolated sway for extra smoothness between ticks + float sway = entity.getTailSwayOffset(state.getPartialTick()); // [-1, 1] + + // Tuning + float maxYawDeg = 22.0f; // increased max sweep + float swayGain = 1.35f; // amplifies overall power + float rollFraction = 0.40f; // slightly stronger roll for heft + + float deg2rad = (float)Math.PI / 180f; + + // Direction: positive sway (left turn) -> tail swings right (negative yaw) + // Flip the sign here if the sway feels inverted + float baseYaw = sway * maxYawDeg * deg2rad; + float baseRoll = -baseYaw * rollFraction; + + float[] weights = { 1.00f, 0.78f, 0.58f, 0.42f, 0.30f, 0.22f }; + + for (int i = 0; i < n; i++) { + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + + float w = weights[i]; + float yaw = baseYaw * w; + float roll = baseRoll * w; + + // OVERRIDE animations on Y/Z only: keep the model's predefined X bend intact + // Do NOT reset rotX here, so the upward bend stays + bone.setRotY(yaw); + bone.setRotZ(roll); + + appliedYaw[i] = yaw; + appliedRoll[i] = roll; + } + + GeoBone head = (GeoBone) getAnimationProcessor().getBone("Neck1"); + + if (head != null) { + var entityData = state.getData(DataTickets.ENTITY_MODEL_DATA); + float clampedYawDeg = Mth.clamp(entityData.netHeadYaw(), -20.0f, 20.0f); + head.setRotY(clampedYawDeg * Mth.DEG_TO_RAD); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/TyrannosaurusRexRenderer.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/TyrannosaurusRexRenderer.java new file mode 100644 index 0000000..57041aa --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/TyrannosaurusRexRenderer.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.cmr.jurassicrevived.entity.custom.TyrannosaurusRexEntity; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.util.Mth; +import software.bernie.geckolib.cache.object.BakedGeoModel; +import software.bernie.geckolib.renderer.GeoEntityRenderer; + + +public class TyrannosaurusRexRenderer extends GeoEntityRenderer { + private final float animalScale = 2.5F; + public TyrannosaurusRexRenderer(EntityRendererProvider.Context renderManager) { + super(renderManager, new TyrannosaurusRexModel()); + } + + @Override + public void scaleModelForRender(float widthScale, float heightScale, PoseStack poseStack, TyrannosaurusRexEntity animatable, BakedGeoModel model, boolean isReRender, float partialTick, int packedLight, int packedOverlay) { + poseStack.scale(animalScale, animalScale, animalScale); + if(animatable.isBaby()) { + float growthProgress = Mth.clamp((24000.0F + animatable.getSyncedAge()) / 24000.0F, 0.0F, 1.0F); + float scale = Mth.lerp(growthProgress, 0.2F, 1.0F); + poseStack.scale(scale, scale, scale); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/TyrannosaurusRexVariant.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/TyrannosaurusRexVariant.java new file mode 100644 index 0000000..9bf67d6 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/TyrannosaurusRexVariant.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import java.util.Arrays; +import java.util.Comparator; + +public enum TyrannosaurusRexVariant { + MALE(0), + FEMALE(1); + + private static final TyrannosaurusRexVariant[] BY_ID = Arrays.stream(values()).sorted( + Comparator.comparingInt(TyrannosaurusRexVariant::getId)).toArray(TyrannosaurusRexVariant[]::new); + + private final int id; + + TyrannosaurusRexVariant(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static TyrannosaurusRexVariant byId(int id) { + return BY_ID[id % BY_ID.length]; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/UtahraptorModel.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/UtahraptorModel.java new file mode 100644 index 0000000..3bf79de --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/UtahraptorModel.java @@ -0,0 +1,112 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.google.common.collect.Maps; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.entity.custom.UtahraptorEntity; +import net.minecraft.Util; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animation.AnimationState; +/*?} else {*/ +/*import software.bernie.geckolib.animation.AnimationState; + *//*?}*/ +import software.bernie.geckolib.cache.object.GeoBone; +import software.bernie.geckolib.constant.DataTickets; +import software.bernie.geckolib.model.GeoModel; + +import java.util.Map; + +public class UtahraptorModel extends GeoModel { + + private static final Map LOCATION_BY_VARIANT = + Util.make(Maps.newEnumMap(UtahraptorVariant.class), map -> { + map.put(UtahraptorVariant.MALE, Constants.rl("textures/entity/utahraptor.png")); + map.put(UtahraptorVariant.FEMALE, Constants.rl("textures/entity/utahraptor_female.png")); + }); + + // Model-local "currently applied" offsets; cleared before each entity render + private float[] appliedYaw = null; + private float[] appliedRoll = null; + + @Override + public ResourceLocation getModelResource(UtahraptorEntity animatable) { + return Constants.rl("geo/utahraptor.geo.json"); + } + + @Override + public ResourceLocation getTextureResource(UtahraptorEntity animatable) { + return LOCATION_BY_VARIANT.get(animatable.getVariant()); + } + + @Override + public ResourceLocation getAnimationResource(UtahraptorEntity animatable) { + return Constants.rl("animations/utahraptor.animation.json"); + } + + @Override + public void setCustomAnimations(UtahraptorEntity entity, long id, AnimationState state) { + super.setCustomAnimations(entity, id, state); + + String[] tailBones = { "TailBASE", "Tail2", "Tail3", "Tail4", "Tail5", "Tail6" }; + int n = tailBones.length; + + if (appliedYaw == null || appliedYaw.length != n) { + appliedYaw = new float[n]; + appliedRoll = new float[n]; + } + + // 1) Clear previous offsets (from the last entity rendered with this model instance) + for (int i = 0; i < n; i++) { + if (appliedYaw[i] == 0.0f && appliedRoll[i] == 0.0f) continue; + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + if (appliedYaw[i] != 0.0f) bone.setRotY(bone.getRotY() - appliedYaw[i]); + if (appliedRoll[i] != 0.0f) bone.setRotZ(bone.getRotZ() - appliedRoll[i]); + appliedYaw[i] = 0.0f; + appliedRoll[i] = 0.0f; + } + + // 2) Interpolated sway for extra smoothness between ticks + float sway = entity.getTailSwayOffset(state.getPartialTick()); // [-1, 1] + + // Tuning + float maxYawDeg = 22.0f; // increased max sweep + float swayGain = 1.35f; // amplifies overall power + float rollFraction = 0.40f; // slightly stronger roll for heft + + float deg2rad = (float)Math.PI / 180f; + + // Direction: positive sway (left turn) -> tail swings right (negative yaw) + // Flip the sign here if the sway feels inverted + float baseYaw = sway * maxYawDeg * deg2rad; + float baseRoll = -baseYaw * rollFraction; + + float[] weights = { 1.00f, 0.78f, 0.58f, 0.42f, 0.30f, 0.22f }; + + for (int i = 0; i < n; i++) { + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + + float w = weights[i]; + float yaw = baseYaw * w; + float roll = baseRoll * w; + + // OVERRIDE animations on Y/Z only: keep the model's predefined X bend intact + // Do NOT reset rotX here, so the upward bend stays + bone.setRotY(yaw); + bone.setRotZ(roll); + + appliedYaw[i] = yaw; + appliedRoll[i] = roll; + } + + GeoBone head = (GeoBone) getAnimationProcessor().getBone("Neck1"); + + if (head != null) { + var entityData = state.getData(DataTickets.ENTITY_MODEL_DATA); + float clampedYawDeg = Mth.clamp(entityData.netHeadYaw(), -30.0f, 30.0f); + head.setRotY(clampedYawDeg * Mth.DEG_TO_RAD); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/UtahraptorRenderer.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/UtahraptorRenderer.java new file mode 100644 index 0000000..530242d --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/UtahraptorRenderer.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.cmr.jurassicrevived.entity.custom.UtahraptorEntity; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.util.Mth; +import software.bernie.geckolib.cache.object.BakedGeoModel; +import software.bernie.geckolib.renderer.GeoEntityRenderer; + + +public class UtahraptorRenderer extends GeoEntityRenderer { + private final float animalScale = 1.0F; + public UtahraptorRenderer(EntityRendererProvider.Context renderManager) { + super(renderManager, new UtahraptorModel()); + } + + @Override + public void scaleModelForRender(float widthScale, float heightScale, PoseStack poseStack, UtahraptorEntity animatable, BakedGeoModel model, boolean isReRender, float partialTick, int packedLight, int packedOverlay) { + poseStack.scale(animalScale, animalScale, animalScale); + if(animatable.isBaby()) { + float growthProgress = Mth.clamp((24000.0F + animatable.getSyncedAge()) / 24000.0F, 0.0F, 1.0F); + float scale = Mth.lerp(growthProgress, 0.2F, 1.0F); + poseStack.scale(scale, scale, scale); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/UtahraptorVariant.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/UtahraptorVariant.java new file mode 100644 index 0000000..d7146dd --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/UtahraptorVariant.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import java.util.Arrays; +import java.util.Comparator; + +public enum UtahraptorVariant { + MALE(0), + FEMALE(1); + + private static final UtahraptorVariant[] BY_ID = Arrays.stream(values()).sorted( + Comparator.comparingInt(UtahraptorVariant::getId)).toArray(UtahraptorVariant[]::new); + + private final int id; + + UtahraptorVariant(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static UtahraptorVariant byId(int id) { + return BY_ID[id % BY_ID.length]; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/VelociraptorModel.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/VelociraptorModel.java new file mode 100644 index 0000000..16d8559 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/VelociraptorModel.java @@ -0,0 +1,112 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.google.common.collect.Maps; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.entity.custom.VelociraptorEntity; +import net.minecraft.Util; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animation.AnimationState; +/*?} else {*/ +/*import software.bernie.geckolib.animation.AnimationState; + *//*?}*/ +import software.bernie.geckolib.cache.object.GeoBone; +import software.bernie.geckolib.constant.DataTickets; +import software.bernie.geckolib.model.GeoModel; + +import java.util.Map; + +public class VelociraptorModel extends GeoModel { + + private static final Map LOCATION_BY_VARIANT = + Util.make(Maps.newEnumMap(VelociraptorVariant.class), map -> { + map.put(VelociraptorVariant.MALE, Constants.rl("textures/entity/velociraptor.png")); + map.put(VelociraptorVariant.FEMALE, Constants.rl("textures/entity/velociraptor_female.png")); + }); + + // Model-local "currently applied" offsets; cleared before each entity render + private float[] appliedYaw = null; + private float[] appliedRoll = null; + + @Override + public ResourceLocation getModelResource(VelociraptorEntity animatable) { + return Constants.rl("geo/velociraptor.geo.json"); + } + + @Override + public ResourceLocation getTextureResource(VelociraptorEntity animatable) { + return LOCATION_BY_VARIANT.get(animatable.getVariant()); + } + + @Override + public ResourceLocation getAnimationResource(VelociraptorEntity animatable) { + return Constants.rl("animations/velociraptor.animation.json"); + } + + @Override + public void setCustomAnimations(VelociraptorEntity entity, long id, AnimationState state) { + super.setCustomAnimations(entity, id, state); + + String[] tailBones = { "tail1", "tail2", "tail3", "tail4", "tail5", "tail6" }; + int n = tailBones.length; + + if (appliedYaw == null || appliedYaw.length != n) { + appliedYaw = new float[n]; + appliedRoll = new float[n]; + } + + // 1) Clear previous offsets (from the last entity rendered with this model instance) + for (int i = 0; i < n; i++) { + if (appliedYaw[i] == 0.0f && appliedRoll[i] == 0.0f) continue; + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + if (appliedYaw[i] != 0.0f) bone.setRotY(bone.getRotY() - appliedYaw[i]); + if (appliedRoll[i] != 0.0f) bone.setRotZ(bone.getRotZ() - appliedRoll[i]); + appliedYaw[i] = 0.0f; + appliedRoll[i] = 0.0f; + } + + // 2) Interpolated sway for extra smoothness between ticks + float sway = entity.getTailSwayOffset(state.getPartialTick()); // [-1, 1] + + // Tuning + float maxYawDeg = 22.0f; // increased max sweep + float swayGain = 1.35f; // amplifies overall power + float rollFraction = 0.40f; // slightly stronger roll for heft + + float deg2rad = (float)Math.PI / 180f; + + // Direction: positive sway (left turn) -> tail swings right (negative yaw) + // Flip the sign here if the sway feels inverted + float baseYaw = sway * maxYawDeg * deg2rad; + float baseRoll = -baseYaw * rollFraction; + + float[] weights = { 1.00f, 0.78f, 0.58f, 0.42f, 0.30f, 0.22f }; + + for (int i = 0; i < n; i++) { + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + + float w = weights[i]; + float yaw = baseYaw * w; + float roll = baseRoll * w; + + // OVERRIDE animations on Y/Z only: keep the model's predefined X bend intact + // Do NOT reset rotX here, so the upward bend stays + bone.setRotY(yaw); + bone.setRotZ(roll); + + appliedYaw[i] = yaw; + appliedRoll[i] = roll; + } + + GeoBone head = (GeoBone) getAnimationProcessor().getBone("body1"); + + if (head != null) { + var entityData = state.getData(DataTickets.ENTITY_MODEL_DATA); + float clampedYawDeg = Mth.clamp(entityData.netHeadYaw(), -30.0f, 30.0f); + head.setRotY(clampedYawDeg * Mth.DEG_TO_RAD); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/VelociraptorRenderer.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/VelociraptorRenderer.java new file mode 100644 index 0000000..06216da --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/VelociraptorRenderer.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.cmr.jurassicrevived.entity.custom.VelociraptorEntity; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.util.Mth; +import software.bernie.geckolib.cache.object.BakedGeoModel; +import software.bernie.geckolib.renderer.GeoEntityRenderer; + + +public class VelociraptorRenderer extends GeoEntityRenderer { + private final float animalScale = 1.0F; + public VelociraptorRenderer(EntityRendererProvider.Context renderManager) { + super(renderManager, new VelociraptorModel()); + } + + @Override + public void scaleModelForRender(float widthScale, float heightScale, PoseStack poseStack, VelociraptorEntity animatable, BakedGeoModel model, boolean isReRender, float partialTick, int packedLight, int packedOverlay) { + poseStack.scale(animalScale, animalScale, animalScale); + if(animatable.isBaby()) { + float growthProgress = Mth.clamp((24000.0F + animatable.getSyncedAge()) / 24000.0F, 0.0F, 1.0F); + float scale = Mth.lerp(growthProgress, 0.2F, 1.0F); + poseStack.scale(scale, scale, scale); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/VelociraptorVariant.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/VelociraptorVariant.java new file mode 100644 index 0000000..1bf7874 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/VelociraptorVariant.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import java.util.Arrays; +import java.util.Comparator; + +public enum VelociraptorVariant { + MALE(0), + FEMALE(1); + + private static final VelociraptorVariant[] BY_ID = Arrays.stream(values()).sorted( + Comparator.comparingInt(VelociraptorVariant::getId)).toArray(VelociraptorVariant[]::new); + + private final int id; + + VelociraptorVariant(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static VelociraptorVariant byId(int id) { + return BY_ID[id % BY_ID.length]; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/ZhenyuanopterusModel.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/ZhenyuanopterusModel.java new file mode 100644 index 0000000..e0a4d67 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/ZhenyuanopterusModel.java @@ -0,0 +1,112 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.google.common.collect.Maps; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.entity.custom.ZhenyuanopterusEntity; +import net.minecraft.Util; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animation.AnimationState; +/*?} else {*/ +/*import software.bernie.geckolib.animation.AnimationState; + *//*?}*/ +import software.bernie.geckolib.cache.object.GeoBone; +import software.bernie.geckolib.constant.DataTickets; +import software.bernie.geckolib.model.GeoModel; + +import java.util.Map; + +public class ZhenyuanopterusModel extends GeoModel { + + private static final Map LOCATION_BY_VARIANT = + Util.make(Maps.newEnumMap(ZhenyuanopterusVariant.class), map -> { + map.put(ZhenyuanopterusVariant.MALE, Constants.rl("textures/entity/zhenyuanopterus.png")); + map.put(ZhenyuanopterusVariant.FEMALE, Constants.rl("textures/entity/zhenyuanopterus_female.png")); + }); + + // Model-local "currently applied" offsets; cleared before each entity render + private float[] appliedYaw = null; + private float[] appliedRoll = null; + + @Override + public ResourceLocation getModelResource(ZhenyuanopterusEntity animatable) { + return Constants.rl("geo/zhenyuanopterus.geo.json"); + } + + @Override + public ResourceLocation getTextureResource(ZhenyuanopterusEntity animatable) { + return LOCATION_BY_VARIANT.get(animatable.getVariant()); + } + + @Override + public ResourceLocation getAnimationResource(ZhenyuanopterusEntity animatable) { + return Constants.rl("animations/zhenyuanopterus.animation.json"); + } + + @Override + public void setCustomAnimations(ZhenyuanopterusEntity entity, long id, AnimationState state) { + super.setCustomAnimations(entity, id, state); + + String[] tailBones = { "Tail1", "Tail2", "Tail3" }; + int n = tailBones.length; + + if (appliedYaw == null || appliedYaw.length != n) { + appliedYaw = new float[n]; + appliedRoll = new float[n]; + } + + // 1) Clear previous offsets (from the last entity rendered with this model instance) + for (int i = 0; i < n; i++) { + if (appliedYaw[i] == 0.0f && appliedRoll[i] == 0.0f) continue; + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + if (appliedYaw[i] != 0.0f) bone.setRotY(bone.getRotY() - appliedYaw[i]); + if (appliedRoll[i] != 0.0f) bone.setRotZ(bone.getRotZ() - appliedRoll[i]); + appliedYaw[i] = 0.0f; + appliedRoll[i] = 0.0f; + } + + // 2) Interpolated sway for extra smoothness between ticks + float sway = entity.getTailSwayOffset(state.getPartialTick()); // [-1, 1] + + // Tuning + float maxYawDeg = 22.0f; // increased max sweep + float swayGain = 1.35f; // amplifies overall power + float rollFraction = 0.40f; // slightly stronger roll for heft + + float deg2rad = (float)Math.PI / 180f; + + // Direction: positive sway (left turn) -> tail swings right (negative yaw) + // Flip the sign here if the sway feels inverted + float baseYaw = sway * maxYawDeg * deg2rad; + float baseRoll = -baseYaw * rollFraction; + + float[] weights = { 1.00f, 0.78f, 0.58f }; + + for (int i = 0; i < n; i++) { + GeoBone bone = (GeoBone) getAnimationProcessor().getBone(tailBones[i]); + if (bone == null) continue; + + float w = weights[i]; + float yaw = baseYaw * w; + float roll = baseRoll * w; + + // OVERRIDE animations on Y/Z only: keep the model's predefined X bend intact + // Do NOT reset rotX here, so the upward bend stays + bone.setRotY(yaw); + bone.setRotZ(roll); + + appliedYaw[i] = yaw; + appliedRoll[i] = roll; + } + + GeoBone head = (GeoBone) getAnimationProcessor().getBone("Neck1"); + + if (head != null) { + var entityData = state.getData(DataTickets.ENTITY_MODEL_DATA); + float clampedYawDeg = Mth.clamp(entityData.netHeadYaw(), -20.0f, 20.0f); + head.setRotY(clampedYawDeg * Mth.DEG_TO_RAD); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/ZhenyuanopterusRenderer.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/ZhenyuanopterusRenderer.java new file mode 100644 index 0000000..cd48d7b --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/ZhenyuanopterusRenderer.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.cmr.jurassicrevived.entity.custom.ZhenyuanopterusEntity; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.util.Mth; +import software.bernie.geckolib.cache.object.BakedGeoModel; +import software.bernie.geckolib.renderer.GeoEntityRenderer; + + +public class ZhenyuanopterusRenderer extends GeoEntityRenderer { + private final float animalScale = 0.7F; + public ZhenyuanopterusRenderer(EntityRendererProvider.Context renderManager) { + super(renderManager, new ZhenyuanopterusModel()); + } + + @Override + public void scaleModelForRender(float widthScale, float heightScale, PoseStack poseStack, ZhenyuanopterusEntity animatable, BakedGeoModel model, boolean isReRender, float partialTick, int packedLight, int packedOverlay) { + poseStack.scale(animalScale, animalScale, animalScale); + if(animatable.isBaby()) { + float growthProgress = Mth.clamp((24000.0F + animatable.getSyncedAge()) / 24000.0F, 0.0F, 1.0F); + float scale = Mth.lerp(growthProgress, 0.2F, 1.0F); + poseStack.scale(scale, scale, scale); + } + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/ZhenyuanopterusVariant.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/ZhenyuanopterusVariant.java new file mode 100644 index 0000000..e905749 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/ZhenyuanopterusVariant.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import java.util.Arrays; +import java.util.Comparator; + +public enum ZhenyuanopterusVariant { + MALE(0), + FEMALE(1); + + private static final ZhenyuanopterusVariant[] BY_ID = Arrays.stream(values()).sorted( + Comparator.comparingInt(ZhenyuanopterusVariant::getId)).toArray(ZhenyuanopterusVariant[]::new); + + private final int id; + + ZhenyuanopterusVariant(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static ZhenyuanopterusVariant byId(int id) { + return BY_ID[id % BY_ID.length]; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/custom/AlbertosaurusEntity.java b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/AlbertosaurusEntity.java new file mode 100644 index 0000000..8068634 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/AlbertosaurusEntity.java @@ -0,0 +1,319 @@ +package net.cmr.jurassicrevived.entity.custom; + +import net.cmr.jurassicrevived.entity.ai.SprintingMeleeAttackGoal; +import net.cmr.jurassicrevived.entity.ModEntities; +import net.cmr.jurassicrevived.entity.ai.SprintingPanicGoal; +import net.cmr.jurassicrevived.entity.client.AlbertosaurusVariant; +import net.cmr.jurassicrevived.sound.ModSounds; +import net.minecraft.Util; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.util.Mth; +import net.minecraft.world.Difficulty; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.*; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.attributes.DefaultAttributes; +import net.minecraft.world.entity.ai.goal.*; +import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal; +import net.minecraft.world.entity.ai.goal.target.NearestAttackableTargetGoal; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.monster.Monster; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerLevelAccessor; +import org.jetbrains.annotations.Nullable; +import software.bernie.geckolib.animatable.GeoEntity; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.core.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.core.animation.AnimatableManager; +import software.bernie.geckolib.core.animation.Animation; +import software.bernie.geckolib.core.animation.AnimationController; +import software.bernie.geckolib.core.animation.RawAnimation; +import software.bernie.geckolib.core.object.PlayState; +/*?} else {*/ +/*import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.animation.*; +*//*?}*/ + +public class AlbertosaurusEntity extends Animal implements GeoEntity { + private AnimatableInstanceCache cache = new SingletonAnimatableInstanceCache(this); + + private static final EntityDataAccessor VARIANT = + SynchedEntityData.defineId(AlbertosaurusEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SYNCED_AGE = + SynchedEntityData.defineId(AlbertosaurusEntity.class, EntityDataSerializers.INT); + + // Procedural tail sway state (client-side use for rendering) + private float tailSwayOffset; // Smoothed offset in range roughly [-1, 1] + private float tailSwayVelocity; // Internal velocity for spring-damper + private float tailSwayPrev; // Previous frame value for interpolation + + public AlbertosaurusEntity(EntityType pEntityType, Level pLevel) { + super(pEntityType, pLevel); + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new SprintingPanicGoal(this, 1.15) { + @Override + public boolean canUse() { + return AlbertosaurusEntity.this.isBaby() && super.canUse(); + } + }); + this.targetSelector.addGoal(1, new HurtByTargetGoal(this).setAlertOthers()); + this.goalSelector.addGoal(2, new FloatGoal(this)); + this.goalSelector.addGoal(3, new AvoidEntityGoal<>(this, BrachiosaurusEntity.class, (float) 20, 1.2, 1)); + this.goalSelector.addGoal(4, new SprintingMeleeAttackGoal(this, 1.1, false)); + this.goalSelector.addGoal(5, new BreedGoal(this, 1.0)); + this.goalSelector.addGoal(6, new FollowParentGoal(this, 1.25)); + this.goalSelector.addGoal(7, new WaterAvoidingRandomStrollGoal(this, 1.0)); + this.goalSelector.addGoal(8, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(9, new FollowMobGoal(this, 1.2, (float) 20, (float) 10)); + this.targetSelector.addGoal(10, new NearestAttackableTargetGoal<>(this, Monster.class, true)); + this.targetSelector.addGoal(11, new NearestAttackableTargetGoal<>(this, Animal.class, 10, false, false, + target -> target.getType() != this.getType())); + this.targetSelector.addGoal(12, new NearestAttackableTargetGoal(this, TriceratopsEntity.class, false, false)); + this.targetSelector.addGoal(13, new NearestAttackableTargetGoal(this, GallimimusEntity.class, false, false)); + this.targetSelector.addGoal(14, new NearestAttackableTargetGoal(this, DilophosaurusEntity.class, false, false)); + this.targetSelector.addGoal(15, new NearestAttackableTargetGoal(this, CeratosaurusEntity.class, false, false)); + this.targetSelector.addGoal(16, new NearestAttackableTargetGoal(this, ParasaurolophusEntity.class, false, false)); + this.targetSelector.addGoal(17, new NearestAttackableTargetGoal(this, VelociraptorEntity.class, false, false)); + this.targetSelector.addGoal(18, new NearestAttackableTargetGoal(this, SpinosaurusEntity.class, false, false)); + this.targetSelector.addGoal(19, new NearestAttackableTargetGoal(this, IndominusRexEntity.class, false, false)); + this.targetSelector.addGoal(20, new NearestAttackableTargetGoal(this, Player.class, false, false)); + this.goalSelector.addGoal(21, new RandomLookAroundGoal(this)); + } + + public static AttributeSupplier.Builder createAttributes() { + return Animal.createLivingAttributes() + .add(Attributes.MAX_HEALTH, 55D) + .add(Attributes.MOVEMENT_SPEED, 0.3D) + .add(Attributes.ARMOR, 0D) + .add(Attributes.FOLLOW_RANGE, 32D) + .add(Attributes.KNOCKBACK_RESISTANCE, 0D) + .add(Attributes.ATTACK_KNOCKBACK, 0D) + .add(Attributes.ATTACK_DAMAGE, 16D); + } + + @Override + public boolean isFood(ItemStack pStack) { + return pStack.is(Items.BEEF); + } + + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel pLevel, AgeableMob pOtherParent) { + AgeableMob child = ModEntities.ALBERTOSAURUS.get().create(pLevel); + if (child instanceof AlbertosaurusEntity baby) { + AlbertosaurusVariant randomVariant = Util.getRandom(AlbertosaurusVariant.values(), this.random); + baby.setVariant(randomVariant); + } + return child; + } + + @Override + public boolean doHurtTarget(Entity target) { + boolean hit = super.doHurtTarget(target); + if (!level().isClientSide && hit && target instanceof LivingEntity) { + if (this.level() instanceof ServerLevel serverLevel) { + this.triggerAnim("attackController", "attack"); + this.playSound(ModSounds.MEDIUM_THEROPOD_ATTACK.get(), 1.0F, 1.0F); + } + } + return hit; + } + + @Override + public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { + controllers.add(new AnimationController<>(this, "Walk/Run/Idle", state -> { + if (state.isMoving()) + return state.setAndContinue(AlbertosaurusEntity.this.isSprinting() ? RawAnimation.begin().then("anim.albertosaurus.run", Animation.LoopType.LOOP) : RawAnimation.begin().then("anim.albertosaurus.walk", Animation.LoopType.LOOP)); + + return state.setAndContinue(RawAnimation.begin().then("anim.albertosaurus.idle", Animation.LoopType.LOOP)); + })); + + controllers.add(new AnimationController<>(this, "attackController", state -> PlayState.STOP) + .triggerableAnim("attack", RawAnimation.begin().then("anim.albertosaurus.attack", Animation.LoopType.PLAY_ONCE))); + + controllers.add(new AnimationController<>(this, "mouthController", state -> PlayState.STOP) + .triggerableAnim("mouth", RawAnimation.begin().then("anim.albertosaurus.mouth", Animation.LoopType.PLAY_ONCE))); + } + + private float getSignedTurnDelta() { + // Only consider the body (torso) rotation so head look does not affect tail sway + return Mth.wrapDegrees(this.yBodyRot - this.yBodyRotO); + } + + private int mouthAnimCooldown = 0; + + @Override + public void tick() { + super.tick(); + + if (!level().isClientSide) { + this.entityData.set(DATA_SYNCED_AGE, this.getAge()); + var maxHealthAttr = getAttribute(Attributes.MAX_HEALTH); + if (maxHealthAttr != null) { + double baseAdult = DefaultAttributes.getSupplier((EntityType) this.getType()).getValue(Attributes.MAX_HEALTH); + double desired = this.isBaby() ? baseAdult * 0.10D : baseAdult; + if (maxHealthAttr.getBaseValue() != desired) { + double oldMax = maxHealthAttr.getBaseValue(); + double healthRatio = this.getHealth() / (float) oldMax; + maxHealthAttr.setBaseValue(desired); + this.setHealth((float) (desired * Mth.clamp(healthRatio, 0.0F, 1.0F))); + } + } + } + + if (!level().isClientSide) { + if (mouthAnimCooldown > 0) { + mouthAnimCooldown--; + } else { + this.triggerAnim("mouthController", "mouth"); + this.playSound(ModSounds.ALBERTOSAURUS_CALL.get(), 1.0F, 1.0F); + // 30s–60s in ticks + mouthAnimCooldown = this.random.nextInt(1200 - 600 + 1) + 600; + } + } + + if (level().isClientSide) { + // Capture previous for smooth interpolation between ticks + this.tailSwayPrev = this.tailSwayOffset; + updateProceduralTailSway(); + } + } + + private void updateProceduralTailSway() { + // Turn input derived from rotation deltas; works even when standing still and turning + float turnDegrees = getSignedTurnDelta(); + + // Deadzone to ignore tiny jitter so the tail can return to center cleanly + float deadzoneDeg = 0.6f; // smaller deadzone for more responsiveness + float turnInput = 0.0f; + if (Math.abs(turnDegrees) >= deadzoneDeg) { + // Higher sensitivity so small in-place turns still affect the model + turnInput = Mth.clamp(turnDegrees / 15.0f, -1.0f, 1.0f); + } + + // Target offset: keep intuitive sign (positive input -> positive sway) + float target = turnInput; + + // One-pole low-pass (no bounce). Larger alpha => snappier and less "stiff". + float alpha = 0.24f; // try 0.20–0.30 to taste + + tailSwayOffset += (target - tailSwayOffset) * alpha; + + // Snap tiny residuals to zero so it visibly settles + if (Math.abs(tailSwayOffset) < 0.003f) { + tailSwayOffset = 0.0f; + } + + // No oscillation velocity retained + tailSwayVelocity = 0.0f; + + tailSwayOffset = Mth.clamp(tailSwayOffset, -1.5f, 1.5f); + } + + // Expose to the model for bone rotation + public float getTailSwayOffset() { + return tailSwayOffset; + } + + // Interpolated sway for smooth rendering between ticks + public float getTailSwayOffset(float partialTick) { + return Mth.lerp(Mth.clamp(partialTick, 0.0f, 1.0f), tailSwayPrev, tailSwayOffset); + } + + /*? if <=1.20.1 {*/ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); + this.entityData.define(VARIANT, 0); + this.entityData.define(DATA_SYNCED_AGE, 0); + } + /*?} else {*/ + /*@Override + protected void defineSynchedData(SynchedEntityData.Builder pBuilder) { + super.defineSynchedData(pBuilder); + pBuilder.define(VARIANT, 0); + pBuilder.define(DATA_SYNCED_AGE, 0); + } + *//*?}*/ + + public int getSyncedAge() { + return this.entityData.get(DATA_SYNCED_AGE); + } + + public int getTypeVariant() { + return this.entityData.get(VARIANT); + } + + public AlbertosaurusVariant getVariant() { + return AlbertosaurusVariant.byId(this.getTypeVariant() & 255); + } + + private void setVariant(AlbertosaurusVariant variant) { + this.entityData.set(VARIANT, variant.getId() & 255); + } + + @Override + public boolean canMate(Animal other) { + if (!super.canMate(other)) return false; + if (!(other instanceof AlbertosaurusEntity that)) return false; + return this.getVariant() != that.getVariant(); + } + @Override + public void addAdditionalSaveData(CompoundTag pCompound) { + super.addAdditionalSaveData(pCompound); + pCompound.putInt("Variant", this.getTypeVariant()); + } + + @Override + public void readAdditionalSaveData(CompoundTag pCompound) { + super.readAdditionalSaveData(pCompound); + this.entityData.set(VARIANT, pCompound.getInt("Variant")); + } + + /*? if <=1.20.1 {*/ + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor pLevel, DifficultyInstance pDifficulty, MobSpawnType pReason, @Nullable SpawnGroupData pSpawnData, @Nullable CompoundTag pDataTag) { + AlbertosaurusVariant variant = Util.getRandom(AlbertosaurusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(pLevel, pDifficulty, pReason, pSpawnData, pDataTag); + } + /*?} else {*/ + /*@Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, MobSpawnType spawnType, @Nullable SpawnGroupData spawnGroupData) { + AlbertosaurusVariant variant = Util.getRandom(AlbertosaurusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(level, difficulty, spawnType, spawnGroupData); + } + *//*?}*/ + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } + + @Override + protected @Nullable SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.ALBERTOSAURUS_HURT.get(); + } + + @Override + protected @Nullable SoundEvent getDeathSound() { + return ModSounds.ALBERTOSAURUS_DEATH.get(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/custom/AllosaurusEntity.java b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/AllosaurusEntity.java new file mode 100644 index 0000000..e624e7b --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/AllosaurusEntity.java @@ -0,0 +1,316 @@ +package net.cmr.jurassicrevived.entity.custom; + +import net.cmr.jurassicrevived.entity.ModEntities; +import net.cmr.jurassicrevived.entity.ai.SprintingMeleeAttackGoal; +import net.cmr.jurassicrevived.entity.ai.SprintingPanicGoal; +import net.cmr.jurassicrevived.entity.client.AllosaurusVariant; +import net.cmr.jurassicrevived.entity.client.AllosaurusVariant; +import net.cmr.jurassicrevived.sound.ModSounds; +import net.minecraft.Util; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.util.Mth; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.*; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.attributes.DefaultAttributes; +import net.minecraft.world.entity.ai.goal.*; +import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal; +import net.minecraft.world.entity.ai.goal.target.NearestAttackableTargetGoal; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerLevelAccessor; +import org.jetbrains.annotations.Nullable; +import software.bernie.geckolib.animatable.GeoEntity; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.core.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.core.animation.AnimatableManager; +import software.bernie.geckolib.core.animation.Animation; +import software.bernie.geckolib.core.animation.AnimationController; +import software.bernie.geckolib.core.animation.RawAnimation; +import software.bernie.geckolib.core.object.PlayState; +/*?} else {*/ +/*import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.animation.*; +*//*?}*/ + +public class AllosaurusEntity extends Animal implements GeoEntity { + private AnimatableInstanceCache cache = new SingletonAnimatableInstanceCache(this); + + private static final EntityDataAccessor VARIANT = + SynchedEntityData.defineId(AllosaurusEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SYNCED_AGE = + SynchedEntityData.defineId(AllosaurusEntity.class, EntityDataSerializers.INT); + + // Procedural tail sway state (client-side use for rendering) + private float tailSwayOffset; // Smoothed offset in range roughly [-1, 1] + private float tailSwayVelocity; // Internal velocity for spring-damper + private float tailSwayPrev; // Previous frame value for interpolation + + public AllosaurusEntity(EntityType pEntityType, Level pLevel) { + super(pEntityType, pLevel); + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new SprintingPanicGoal(this, 1.15) { + @Override + public boolean canUse() { + return AllosaurusEntity.this.isBaby() && super.canUse(); + } + }); + this.targetSelector.addGoal(1, new HurtByTargetGoal(this).setAlertOthers()); + this.goalSelector.addGoal(2, new FloatGoal(this)); + this.goalSelector.addGoal(3, new AvoidEntityGoal<>(this, BrachiosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(4, new SprintingMeleeAttackGoal(this, 1.1, false)); + this.goalSelector.addGoal(5, new BreedGoal(this, 1.0)); + this.goalSelector.addGoal(6, new FollowParentGoal(this, 1.25)); + this.goalSelector.addGoal(7, new WaterAvoidingRandomStrollGoal(this, 1.0)); + this.goalSelector.addGoal(8, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.targetSelector.addGoal(9, new NearestAttackableTargetGoal<>(this, Animal.class, 10, false, false, + target -> target.getType() != this.getType())); + this.targetSelector.addGoal(10, new NearestAttackableTargetGoal(this, TriceratopsEntity.class, false, false)); + this.targetSelector.addGoal(11, new NearestAttackableTargetGoal(this, GallimimusEntity.class, false, false)); + this.targetSelector.addGoal(12, new NearestAttackableTargetGoal(this, DilophosaurusEntity.class, false, false)); + this.targetSelector.addGoal(13, new NearestAttackableTargetGoal(this, CeratosaurusEntity.class, false, false)); + this.targetSelector.addGoal(14, new NearestAttackableTargetGoal(this, ParasaurolophusEntity.class, false, false)); + this.targetSelector.addGoal(15, new NearestAttackableTargetGoal(this, VelociraptorEntity.class, false, false)); + this.targetSelector.addGoal(16, new NearestAttackableTargetGoal(this, TyrannosaurusRexEntity.class, false, false)); + this.targetSelector.addGoal(17, new NearestAttackableTargetGoal(this, IndominusRexEntity.class, false, false)); + this.targetSelector.addGoal(18, new NearestAttackableTargetGoal(this, Player.class, false, false)); + this.goalSelector.addGoal(19, new RandomLookAroundGoal(this)); + + + } + + public static AttributeSupplier.Builder createAttributes() { + return Animal.createLivingAttributes() + .add(Attributes.MAX_HEALTH, 65D) + .add(Attributes.MOVEMENT_SPEED, 0.3D) + .add(Attributes.ARMOR, 0D) + .add(Attributes.FOLLOW_RANGE, 32D) + .add(Attributes.KNOCKBACK_RESISTANCE, 0D) + .add(Attributes.ATTACK_KNOCKBACK, 0D) + .add(Attributes.ATTACK_DAMAGE, 18D); + } + + @Override + public boolean isFood(ItemStack pStack) { + return pStack.is(Items.BEEF); + } + + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel pLevel, AgeableMob pOtherParent) { + AgeableMob child = ModEntities.ALLOSAURUS.get().create(pLevel); + if (child instanceof AllosaurusEntity baby) { + AllosaurusVariant randomVariant = Util.getRandom(AllosaurusVariant.values(), this.random); + baby.setVariant(randomVariant); + } + return child; + } + + @Override + public boolean doHurtTarget(Entity target) { + boolean hit = super.doHurtTarget(target); + if (!level().isClientSide && hit && target instanceof LivingEntity) { + if (this.level() instanceof ServerLevel serverLevel) { + this.triggerAnim("attackController", "attack"); + this.playSound(ModSounds.MEDIUM_THEROPOD_ATTACK.get(), 1.0F, 1.0F); + } + } + return hit; + } + + @Override + public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { + controllers.add(new AnimationController<>(this, "Walk/Run/Idle", state -> { + if (state.isMoving()) + return state.setAndContinue(AllosaurusEntity.this.isSprinting() ? RawAnimation.begin().then("anim.allosaurus.run", Animation.LoopType.LOOP) : RawAnimation.begin().then("anim.allosaurus.walk", Animation.LoopType.LOOP)); + + return state.setAndContinue(RawAnimation.begin().then("anim.allosaurus.idle", Animation.LoopType.LOOP)); + })); + + controllers.add(new AnimationController<>(this, "attackController", state -> PlayState.STOP) + .triggerableAnim("attack", RawAnimation.begin().then("anim.allosaurus.attack", Animation.LoopType.PLAY_ONCE))); + + controllers.add(new AnimationController<>(this, "mouthController", state -> PlayState.STOP) + .triggerableAnim("mouth", RawAnimation.begin().then("anim.allosaurus.mouth", Animation.LoopType.PLAY_ONCE))); + } + + private float getSignedTurnDelta() { + // Only consider the body (torso) rotation so head look does not affect tail sway + return Mth.wrapDegrees(this.yBodyRot - this.yBodyRotO); + } + + private int mouthAnimCooldown = 0; + + @Override + public void tick() { + super.tick(); + + if (!level().isClientSide) { + this.entityData.set(DATA_SYNCED_AGE, this.getAge()); + var maxHealthAttr = getAttribute(Attributes.MAX_HEALTH); + if (maxHealthAttr != null) { + double baseAdult = DefaultAttributes.getSupplier((EntityType) this.getType()).getValue(Attributes.MAX_HEALTH); + double desired = this.isBaby() ? baseAdult * 0.10D : baseAdult; + if (maxHealthAttr.getBaseValue() != desired) { + double oldMax = maxHealthAttr.getBaseValue(); + double healthRatio = this.getHealth() / (float) oldMax; + maxHealthAttr.setBaseValue(desired); + this.setHealth((float) (desired * Mth.clamp(healthRatio, 0.0F, 1.0F))); + } + } + } + + if (!level().isClientSide) { + if (mouthAnimCooldown > 0) { + mouthAnimCooldown--; + } else { + this.triggerAnim("mouthController", "mouth"); + this.playSound(ModSounds.ALLOSAURUS_CALL.get(), 1.0F, 1.0F); + // 30s–60s in ticks + mouthAnimCooldown = this.random.nextInt(1200 - 600 + 1) + 600; + } + } + + if (level().isClientSide) { + // Capture previous for smooth interpolation between ticks + this.tailSwayPrev = this.tailSwayOffset; + updateProceduralTailSway(); + } + } + + private void updateProceduralTailSway() { + // Turn input derived from rotation deltas; works even when standing still and turning + float turnDegrees = getSignedTurnDelta(); + + // Deadzone to ignore tiny jitter so the tail can return to center cleanly + float deadzoneDeg = 0.6f; // smaller deadzone for more responsiveness + float turnInput = 0.0f; + if (Math.abs(turnDegrees) >= deadzoneDeg) { + // Higher sensitivity so small in-place turns still affect the model + turnInput = Mth.clamp(turnDegrees / 15.0f, -1.0f, 1.0f); + } + + // Target offset: keep intuitive sign (positive input -> positive sway) + float target = turnInput; + + // One-pole low-pass (no bounce). Larger alpha => snappier and less "stiff". + float alpha = 0.24f; // try 0.20–0.30 to taste + + tailSwayOffset += (target - tailSwayOffset) * alpha; + + // Snap tiny residuals to zero so it visibly settles + if (Math.abs(tailSwayOffset) < 0.003f) { + tailSwayOffset = 0.0f; + } + + // No oscillation velocity retained + tailSwayVelocity = 0.0f; + + tailSwayOffset = Mth.clamp(tailSwayOffset, -1.5f, 1.5f); + } + + // Expose to the model for bone rotation + public float getTailSwayOffset() { + return tailSwayOffset; + } + + // Interpolated sway for smooth rendering between ticks + public float getTailSwayOffset(float partialTick) { + return Mth.lerp(Mth.clamp(partialTick, 0.0f, 1.0f), tailSwayPrev, tailSwayOffset); + } + + /*? if <=1.20.1 {*/ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); + this.entityData.define(VARIANT, 0); + this.entityData.define(DATA_SYNCED_AGE, 0); + } + /*?} else {*/ + /*@Override + protected void defineSynchedData(SynchedEntityData.Builder pBuilder) { + super.defineSynchedData(pBuilder); + pBuilder.define(VARIANT, 0); + pBuilder.define(DATA_SYNCED_AGE, 0); + } + *//*?}*/ + + public int getSyncedAge() { + return this.entityData.get(DATA_SYNCED_AGE); + } + public int getTypeVariant() { + return this.entityData.get(VARIANT); + } + + public AllosaurusVariant getVariant() { + return AllosaurusVariant.byId(this.getTypeVariant() & 255); + } + + private void setVariant(AllosaurusVariant variant) { + this.entityData.set(VARIANT, variant.getId() & 255); + } + + @Override + public boolean canMate(Animal other) { + if (!super.canMate(other)) return false; + if (!(other instanceof AllosaurusEntity that)) return false; + return this.getVariant() != that.getVariant(); + } + @Override + public void addAdditionalSaveData(CompoundTag pCompound) { + super.addAdditionalSaveData(pCompound); + pCompound.putInt("Variant", this.getTypeVariant()); + } + + @Override + public void readAdditionalSaveData(CompoundTag pCompound) { + super.readAdditionalSaveData(pCompound); + this.entityData.set(VARIANT, pCompound.getInt("Variant")); + } + + /*? if <=1.20.1 {*/ + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor pLevel, DifficultyInstance pDifficulty, MobSpawnType pReason, @Nullable SpawnGroupData pSpawnData, @Nullable CompoundTag pDataTag) { + AllosaurusVariant variant = Util.getRandom(AllosaurusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(pLevel, pDifficulty, pReason, pSpawnData, pDataTag); + } + /*?} else {*/ + /*@Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, MobSpawnType spawnType, @Nullable SpawnGroupData spawnGroupData) { + AllosaurusVariant variant = Util.getRandom(AllosaurusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(level, difficulty, spawnType, spawnGroupData); + } + *//*?}*/ + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } + + @Override + protected @Nullable SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.ALLOSAURUS_HURT.get(); + } + + @Override + protected @Nullable SoundEvent getDeathSound() { + return ModSounds.ALLOSAURUS_DEATH.get(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/custom/AlvarezsaurusEntity.java b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/AlvarezsaurusEntity.java new file mode 100644 index 0000000..35d8fe8 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/AlvarezsaurusEntity.java @@ -0,0 +1,313 @@ +package net.cmr.jurassicrevived.entity.custom; + +import net.cmr.jurassicrevived.entity.ModEntities; +import net.cmr.jurassicrevived.entity.ai.SprintingMeleeAttackGoal; +import net.cmr.jurassicrevived.entity.ai.SprintingPanicGoal; +import net.cmr.jurassicrevived.entity.client.AlvarezsaurusVariant; +import net.cmr.jurassicrevived.entity.client.AlvarezsaurusVariant; +import net.cmr.jurassicrevived.sound.ModSounds; +import net.minecraft.Util; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.util.Mth; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.*; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.attributes.DefaultAttributes; +import net.minecraft.world.entity.ai.goal.*; +import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal; +import net.minecraft.world.entity.ai.goal.target.NearestAttackableTargetGoal; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerLevelAccessor; +import org.jetbrains.annotations.Nullable; +import software.bernie.geckolib.animatable.GeoEntity; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.core.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.core.animation.AnimatableManager; +import software.bernie.geckolib.core.animation.Animation; +import software.bernie.geckolib.core.animation.AnimationController; +import software.bernie.geckolib.core.animation.RawAnimation; +import software.bernie.geckolib.core.object.PlayState; +/*?} else {*/ +/*import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.animation.*; +*//*?}*/ + +public class AlvarezsaurusEntity extends Animal implements GeoEntity { + private AnimatableInstanceCache cache = new SingletonAnimatableInstanceCache(this); + + private static final EntityDataAccessor VARIANT = + SynchedEntityData.defineId(AlvarezsaurusEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SYNCED_AGE = + SynchedEntityData.defineId(AlvarezsaurusEntity.class, EntityDataSerializers.INT); + + // Procedural tail sway state (client-side use for rendering) + private float tailSwayOffset; // Smoothed offset in range roughly [-1, 1] + private float tailSwayVelocity; // Internal velocity for spring-damper + private float tailSwayPrev; // Previous frame value for interpolation + + public AlvarezsaurusEntity(EntityType pEntityType, Level pLevel) { + super(pEntityType, pLevel); + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new SprintingPanicGoal(this, 1.15) { + @Override + public boolean canUse() { + return AlvarezsaurusEntity.this.isBaby() && super.canUse(); + } + }); + this.targetSelector.addGoal(1, new HurtByTargetGoal(this).setAlertOthers()); + this.goalSelector.addGoal(2, new FloatGoal(this)); + this.goalSelector.addGoal(3, new AvoidEntityGoal<>(this, SpinosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(4, new AvoidEntityGoal<>(this, TyrannosaurusRexEntity.class, (float) 20, 1.2, 1.2)); + this.goalSelector.addGoal(5, new AvoidEntityGoal<>(this, VelociraptorEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(6, new AvoidEntityGoal<>(this, ParasaurolophusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(7, new AvoidEntityGoal<>(this, CeratosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(8, new AvoidEntityGoal<>(this, TriceratopsEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(9, new AvoidEntityGoal<>(this, BrachiosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(10, new SprintingMeleeAttackGoal(this, 1.1, false)); + this.goalSelector.addGoal(11, new BreedGoal(this, 1.0)); + this.goalSelector.addGoal(12, new FollowParentGoal(this, 1.25)); + this.goalSelector.addGoal(13, new WaterAvoidingRandomStrollGoal(this, 1.0)); + this.goalSelector.addGoal(14, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(15, new FollowMobGoal(this, 1, (float) 20, (float) 10)); + this.targetSelector.addGoal(16, new NearestAttackableTargetGoal<>(this, Animal.class, 10, false, false, + target -> target.getType() != this.getType())); + this.targetSelector.addGoal(17, new NearestAttackableTargetGoal(this, GallimimusEntity.class, false, false)); + this.targetSelector.addGoal(18, new NearestAttackableTargetGoal(this, Player.class, false, false)); + this.targetSelector.addGoal(19, new NearestAttackableTargetGoal(this, CompsognathusEntity.class, false, false)); + this.goalSelector.addGoal(20, new RandomLookAroundGoal(this)); + } + + public static AttributeSupplier.Builder createAttributes() { + return Animal.createLivingAttributes() + .add(Attributes.MAX_HEALTH, 5D) + .add(Attributes.MOVEMENT_SPEED, 0.3D) + .add(Attributes.ARMOR, 0D) + .add(Attributes.FOLLOW_RANGE, 32D) + .add(Attributes.KNOCKBACK_RESISTANCE, 0.3D) + .add(Attributes.ATTACK_KNOCKBACK, 0D) + .add(Attributes.ATTACK_DAMAGE, 2D); + } + + @Override + public boolean isFood(ItemStack pStack) { + return pStack.is(Items.BEEF); + } + + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel pLevel, AgeableMob pOtherParent) { + AgeableMob child = ModEntities.ALVAREZSAURUS.get().create(pLevel); + if (child instanceof AlvarezsaurusEntity baby) { + AlvarezsaurusVariant randomVariant = Util.getRandom(AlvarezsaurusVariant.values(), this.random); + baby.setVariant(randomVariant); + } + return child; + } + + @Override + public boolean doHurtTarget(Entity target) { + boolean hit = super.doHurtTarget(target); + if (!level().isClientSide && hit && target instanceof LivingEntity) { + if (this.level() instanceof ServerLevel serverLevel) { + this.triggerAnim("attackController", "attack"); + } + } + return hit; + } + + @Override + public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { + controllers.add(new AnimationController<>(this, "Walk/Run/Idle", state -> { + if (state.isMoving()) + return state.setAndContinue(AlvarezsaurusEntity.this.isSprinting() ? RawAnimation.begin().then("anim.alvarezsaurus.run", Animation.LoopType.LOOP) : RawAnimation.begin().then("anim.alvarezsaurus.walk", Animation.LoopType.LOOP)); + + return state.setAndContinue(RawAnimation.begin().then("anim.alvarezsaurus.idle", Animation.LoopType.LOOP)); + })); + + controllers.add(new AnimationController<>(this, "attackController", state -> PlayState.STOP) + .triggerableAnim("attack", RawAnimation.begin().then("anim.alvarezsaurus.attack", Animation.LoopType.PLAY_ONCE))); + + controllers.add(new AnimationController<>(this, "mouthController", state -> PlayState.STOP) + .triggerableAnim("mouth", RawAnimation.begin().then("anim.alvarezsaurus.mouth", Animation.LoopType.PLAY_ONCE))); + } + + private float getSignedTurnDelta() { + // Only consider the body (torso) rotation so head look does not affect tail sway + return Mth.wrapDegrees(this.yBodyRot - this.yBodyRotO); + } + + private int mouthAnimCooldown = 0; + + @Override + public void tick() { + super.tick(); + if (!level().isClientSide) { + this.entityData.set(DATA_SYNCED_AGE, this.getAge()); + var maxHealthAttr = getAttribute(Attributes.MAX_HEALTH); + if (maxHealthAttr != null) { + double baseAdult = DefaultAttributes.getSupplier((EntityType) this.getType()).getValue(Attributes.MAX_HEALTH); + double desired = this.isBaby() ? baseAdult * 0.10D : baseAdult; + if (maxHealthAttr.getBaseValue() != desired) { + double oldMax = maxHealthAttr.getBaseValue(); + double healthRatio = this.getHealth() / (float) oldMax; + maxHealthAttr.setBaseValue(desired); + this.setHealth((float) (desired * Mth.clamp(healthRatio, 0.0F, 1.0F))); + } + } + } + + if (!level().isClientSide) { + if (mouthAnimCooldown > 0) { + mouthAnimCooldown--; + } else { + this.triggerAnim("mouthController", "mouth"); + this.playSound(ModSounds.ALVAREZSAURUS_CALL.get(), 1.0F, 1.0F); + // 30s–60s in ticks + mouthAnimCooldown = this.random.nextInt(1200 - 600 + 1) + 600; + } + } + + if (level().isClientSide) { + // Capture previous for smooth interpolation between ticks + this.tailSwayPrev = this.tailSwayOffset; + updateProceduralTailSway(); + } + } + + private void updateProceduralTailSway() { + // Turn input derived from rotation deltas; works even when standing still and turning + float turnDegrees = getSignedTurnDelta(); + + // Deadzone to ignore tiny jitter so the tail can return to center cleanly + float deadzoneDeg = 0.6f; // smaller deadzone for more responsiveness + float turnInput = 0.0f; + if (Math.abs(turnDegrees) >= deadzoneDeg) { + // Higher sensitivity so small in-place turns still affect the model + turnInput = Mth.clamp(turnDegrees / 15.0f, -1.0f, 1.0f); + } + + // Target offset: keep intuitive sign (positive input -> positive sway) + float target = turnInput; + + // One-pole low-pass (no bounce). Larger alpha => snappier and less "stiff". + float alpha = 0.24f; // try 0.20–0.30 to taste + + tailSwayOffset += (target - tailSwayOffset) * alpha; + + // Snap tiny residuals to zero so it visibly settles + if (Math.abs(tailSwayOffset) < 0.003f) { + tailSwayOffset = 0.0f; + } + + // No oscillation velocity retained + tailSwayVelocity = 0.0f; + + tailSwayOffset = Mth.clamp(tailSwayOffset, -1.5f, 1.5f); + } + + // Expose to the model for bone rotation + public float getTailSwayOffset() { + return tailSwayOffset; + } + + // Interpolated sway for smooth rendering between ticks + public float getTailSwayOffset(float partialTick) { + return Mth.lerp(Mth.clamp(partialTick, 0.0f, 1.0f), tailSwayPrev, tailSwayOffset); + } + + /*? if <=1.20.1 {*/ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); + this.entityData.define(VARIANT, 0); + this.entityData.define(DATA_SYNCED_AGE, 0); + } + /*?} else {*/ + /*@Override + protected void defineSynchedData(SynchedEntityData.Builder pBuilder) { + super.defineSynchedData(pBuilder); + pBuilder.define(VARIANT, 0); + pBuilder.define(DATA_SYNCED_AGE, 0); + } + *//*?}*/ + + public int getSyncedAge() { + return this.entityData.get(DATA_SYNCED_AGE); + } + public int getTypeVariant() { + return this.entityData.get(VARIANT); + } + + public AlvarezsaurusVariant getVariant() { + return AlvarezsaurusVariant.byId(this.getTypeVariant() & 255); + } + + private void setVariant(AlvarezsaurusVariant variant) { + this.entityData.set(VARIANT, variant.getId() & 255); + } + + @Override + public boolean canMate(Animal other) { + if (!super.canMate(other)) return false; + if (!(other instanceof AlvarezsaurusEntity that)) return false; + return this.getVariant() != that.getVariant(); + } + @Override + public void addAdditionalSaveData(CompoundTag pCompound) { + super.addAdditionalSaveData(pCompound); + pCompound.putInt("Variant", this.getTypeVariant()); + } + + @Override + public void readAdditionalSaveData(CompoundTag pCompound) { + super.readAdditionalSaveData(pCompound); + this.entityData.set(VARIANT, pCompound.getInt("Variant")); + } + + /*? if <=1.20.1 {*/ + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor pLevel, DifficultyInstance pDifficulty, MobSpawnType pReason, @Nullable SpawnGroupData pSpawnData, @Nullable CompoundTag pDataTag) { + AlvarezsaurusVariant variant = Util.getRandom(AlvarezsaurusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(pLevel, pDifficulty, pReason, pSpawnData, pDataTag); + } + /*?} else {*/ + /*@Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, MobSpawnType spawnType, @Nullable SpawnGroupData spawnGroupData) { + AlvarezsaurusVariant variant = Util.getRandom(AlvarezsaurusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(level, difficulty, spawnType, spawnGroupData); + } + *//*?}*/ + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } + + @Override + protected @Nullable SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.ALVAREZSAURUS_HURT.get(); + } + + @Override + protected @Nullable SoundEvent getDeathSound() { + return ModSounds.ALVAREZSAURUS_DEATH.get(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/custom/AnkylosaurusEntity.java b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/AnkylosaurusEntity.java new file mode 100644 index 0000000..6fb8c5b --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/AnkylosaurusEntity.java @@ -0,0 +1,307 @@ +package net.cmr.jurassicrevived.entity.custom; + +import net.cmr.jurassicrevived.entity.ModEntities; +import net.cmr.jurassicrevived.entity.ai.SprintingMeleeAttackGoal; +import net.cmr.jurassicrevived.entity.ai.SprintingPanicGoal; +import net.cmr.jurassicrevived.entity.client.AnkylosaurusVariant; +import net.cmr.jurassicrevived.entity.client.AnkylosaurusVariant; +import net.cmr.jurassicrevived.sound.ModSounds; +import net.minecraft.Util; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.util.Mth; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.*; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.attributes.DefaultAttributes; +import net.minecraft.world.entity.ai.goal.*; +import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerLevelAccessor; +import org.jetbrains.annotations.Nullable; +import software.bernie.geckolib.animatable.GeoEntity; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.core.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.core.animation.AnimatableManager; +import software.bernie.geckolib.core.animation.Animation; +import software.bernie.geckolib.core.animation.AnimationController; +import software.bernie.geckolib.core.animation.RawAnimation; +import software.bernie.geckolib.core.object.PlayState; +/*?} else {*/ +/*import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.animation.*; +*//*?}*/ + +public class AnkylosaurusEntity extends Animal implements GeoEntity { + private AnimatableInstanceCache cache = new SingletonAnimatableInstanceCache(this); + + private static final EntityDataAccessor VARIANT = + SynchedEntityData.defineId(AnkylosaurusEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SYNCED_AGE = + SynchedEntityData.defineId(AnkylosaurusEntity.class, EntityDataSerializers.INT); + + // Procedural tail sway state (client-side use for rendering) + private float tailSwayOffset; // Smoothed offset in range roughly [-1, 1] + private float tailSwayVelocity; // Internal velocity for spring-damper + private float tailSwayPrev; // Previous frame value for interpolation + + public AnkylosaurusEntity(EntityType pEntityType, Level pLevel) { + super(pEntityType, pLevel); + } + + @Override + protected void registerGoals() {this.goalSelector.addGoal(1, new SprintingPanicGoal(this, 1.15) { + @Override + public boolean canUse() { + return AnkylosaurusEntity.this.isBaby() && super.canUse(); + } + }); + this.targetSelector.addGoal(1, new HurtByTargetGoal(this).setAlertOthers()); + this.goalSelector.addGoal(2, new FloatGoal(this)); + this.goalSelector.addGoal(3, new AvoidEntityGoal<>(this, VelociraptorEntity.class, (float) 20, 0.8, 0.8)); + this.goalSelector.addGoal(4, new AvoidEntityGoal<>(this, CeratosaurusEntity.class, (float) 20, 0.8, 0.8)); + this.goalSelector.addGoal(5, new AvoidEntityGoal<>(this, DilophosaurusEntity.class, (float) 20, 0.8, 0.8)); + this.goalSelector.addGoal(6, new SprintingMeleeAttackGoal(this, 1.1, false)); + this.goalSelector.addGoal(7, new BreedGoal(this, 1.0)); + this.goalSelector.addGoal(8, new FollowParentGoal(this, 1.25)); + this.goalSelector.addGoal(8, new WaterAvoidingRandomStrollGoal(this, 1.0)); + this.goalSelector.addGoal(10, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(11, new FollowMobGoal(this, 0.8, (float) 20, (float) 10)); + this.goalSelector.addGoal(12, new AvoidEntityGoal<>(this, SpinosaurusEntity.class, (float) 20, 0.8, 0.8)); + this.goalSelector.addGoal(13, new AvoidEntityGoal<>(this, TyrannosaurusRexEntity.class, (float) 20, 0.8, 0.8)); + this.goalSelector.addGoal(14, new AvoidEntityGoal<>(this, IndominusRexEntity.class, (float) 20, 0.8, 0.8)); + this.goalSelector.addGoal(15, new EatBlockGoal(this)); + this.goalSelector.addGoal(16, new RandomLookAroundGoal(this)); + + } + + public static AttributeSupplier.Builder createAttributes() { + return Animal.createLivingAttributes() + .add(Attributes.MAX_HEALTH, 75D) + .add(Attributes.MOVEMENT_SPEED, 0.3D) + .add(Attributes.ARMOR, 0D) + .add(Attributes.FOLLOW_RANGE, 32D) + .add(Attributes.KNOCKBACK_RESISTANCE, 0D) + .add(Attributes.ATTACK_KNOCKBACK, 0D) + .add(Attributes.ATTACK_DAMAGE, 18D); + } + + @Override + public boolean isFood(ItemStack pStack) { + return pStack.is(Items.KELP); + } + + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel pLevel, AgeableMob pOtherParent) { + AgeableMob child = ModEntities.ANKYLOSAURUS.get().create(pLevel); + if (child instanceof AnkylosaurusEntity baby) { + AnkylosaurusVariant randomVariant = Util.getRandom(AnkylosaurusVariant.values(), this.random); + baby.setVariant(randomVariant); + } + return child; + } + + @Override + public boolean doHurtTarget(Entity target) { + boolean hit = super.doHurtTarget(target); + if (!level().isClientSide && hit && target instanceof LivingEntity) { + if (this.level() instanceof ServerLevel serverLevel) { + this.triggerAnim("attackController", "attack"); + } + } + return hit; + } + + @Override + public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { + controllers.add(new AnimationController<>(this, "Walk/Run/Idle", state -> { + if (state.isMoving()) + return state.setAndContinue(AnkylosaurusEntity.this.isSprinting() ? RawAnimation.begin().then("anim.ankylosaurus.run", Animation.LoopType.LOOP) : RawAnimation.begin().then("anim.ankylosaurus.walk", Animation.LoopType.LOOP)); + + return state.setAndContinue(RawAnimation.begin().then("anim.ankylosaurus.idle", Animation.LoopType.LOOP)); + })); + + controllers.add(new AnimationController<>(this, "attackController", state -> PlayState.STOP) + .triggerableAnim("attack", RawAnimation.begin().then("anim.ankylosaurus.attack", Animation.LoopType.PLAY_ONCE))); + + controllers.add(new AnimationController<>(this, "mouthController", state -> PlayState.STOP) + .triggerableAnim("mouth", RawAnimation.begin().then("anim.ankylosaurus.mouth", Animation.LoopType.PLAY_ONCE))); + } + + private float getSignedTurnDelta() { + // Only consider the body (torso) rotation so head look does not affect tail sway + return Mth.wrapDegrees(this.yBodyRot - this.yBodyRotO); + } + + private int mouthAnimCooldown = 0; + + @Override + public void tick() { + super.tick(); + if (!level().isClientSide) { + this.entityData.set(DATA_SYNCED_AGE, this.getAge()); + var maxHealthAttr = getAttribute(Attributes.MAX_HEALTH); + if (maxHealthAttr != null) { + double baseAdult = DefaultAttributes.getSupplier((EntityType) this.getType()).getValue(Attributes.MAX_HEALTH); + double desired = this.isBaby() ? baseAdult * 0.10D : baseAdult; + if (maxHealthAttr.getBaseValue() != desired) { + double oldMax = maxHealthAttr.getBaseValue(); + double healthRatio = this.getHealth() / (float) oldMax; + maxHealthAttr.setBaseValue(desired); + this.setHealth((float) (desired * Mth.clamp(healthRatio, 0.0F, 1.0F))); + } + } + } + + if (!level().isClientSide) { + if (mouthAnimCooldown > 0) { + mouthAnimCooldown--; + } else { + this.triggerAnim("mouthController", "mouth"); + this.playSound(ModSounds.ANKYLOSAURUS_CALL.get(), 1.0F, 1.0F); + // 30s–60s in ticks + mouthAnimCooldown = this.random.nextInt(1200 - 600 + 1) + 600; + } + } + + if (level().isClientSide) { + // Capture previous for smooth interpolation between ticks + this.tailSwayPrev = this.tailSwayOffset; + updateProceduralTailSway(); + } + } + + private void updateProceduralTailSway() { + // Turn input derived from rotation deltas; works even when standing still and turning + float turnDegrees = getSignedTurnDelta(); + + // Deadzone to ignore tiny jitter so the tail can return to center cleanly + float deadzoneDeg = 0.6f; // smaller deadzone for more responsiveness + float turnInput = 0.0f; + if (Math.abs(turnDegrees) >= deadzoneDeg) { + // Higher sensitivity so small in-place turns still affect the model + turnInput = Mth.clamp(turnDegrees / 15.0f, -1.0f, 1.0f); + } + + // Target offset: keep intuitive sign (positive input -> positive sway) + float target = turnInput; + + // One-pole low-pass (no bounce). Larger alpha => snappier and less "stiff". + float alpha = 0.24f; // try 0.20–0.30 to taste + + tailSwayOffset += (target - tailSwayOffset) * alpha; + + // Snap tiny residuals to zero so it visibly settles + if (Math.abs(tailSwayOffset) < 0.003f) { + tailSwayOffset = 0.0f; + } + + // No oscillation velocity retained + tailSwayVelocity = 0.0f; + + tailSwayOffset = Mth.clamp(tailSwayOffset, -1.5f, 1.5f); + } + + // Expose to the model for bone rotation + public float getTailSwayOffset() { + return tailSwayOffset; + } + + // Interpolated sway for smooth rendering between ticks + public float getTailSwayOffset(float partialTick) { + return Mth.lerp(Mth.clamp(partialTick, 0.0f, 1.0f), tailSwayPrev, tailSwayOffset); + } + + /*? if <=1.20.1 {*/ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); + this.entityData.define(VARIANT, 0); + this.entityData.define(DATA_SYNCED_AGE, 0); + } + /*?} else {*/ + /*@Override + protected void defineSynchedData(SynchedEntityData.Builder pBuilder) { + super.defineSynchedData(pBuilder); + pBuilder.define(VARIANT, 0); + pBuilder.define(DATA_SYNCED_AGE, 0); + } + *//*?}*/ + + public int getSyncedAge() { + return this.entityData.get(DATA_SYNCED_AGE); + } + public int getTypeVariant() { + return this.entityData.get(VARIANT); + } + + public AnkylosaurusVariant getVariant() { + return AnkylosaurusVariant.byId(this.getTypeVariant() & 255); + } + + private void setVariant(AnkylosaurusVariant variant) { + this.entityData.set(VARIANT, variant.getId() & 255); + } + + @Override + public boolean canMate(Animal other) { + if (!super.canMate(other)) return false; + if (!(other instanceof AnkylosaurusEntity that)) return false; + return this.getVariant() != that.getVariant(); + } + @Override + public void addAdditionalSaveData(CompoundTag pCompound) { + super.addAdditionalSaveData(pCompound); + pCompound.putInt("Variant", this.getTypeVariant()); + } + + @Override + public void readAdditionalSaveData(CompoundTag pCompound) { + super.readAdditionalSaveData(pCompound); + this.entityData.set(VARIANT, pCompound.getInt("Variant")); + } + + /*? if <=1.20.1 {*/ + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor pLevel, DifficultyInstance pDifficulty, MobSpawnType pReason, @Nullable SpawnGroupData pSpawnData, @Nullable CompoundTag pDataTag) { + AnkylosaurusVariant variant = Util.getRandom(AnkylosaurusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(pLevel, pDifficulty, pReason, pSpawnData, pDataTag); + } + /*?} else {*/ + /*@Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, MobSpawnType spawnType, @Nullable SpawnGroupData spawnGroupData) { + AnkylosaurusVariant variant = Util.getRandom(AnkylosaurusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(level, difficulty, spawnType, spawnGroupData); + } + *//*?}*/ + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } + + @Override + protected @Nullable SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.ANKYLOSAURUS_HURT.get(); + } + + @Override + protected @Nullable SoundEvent getDeathSound() { + return ModSounds.ANKYLOSAURUS_DEATH.get(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/custom/ApatosaurusEntity.java b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/ApatosaurusEntity.java new file mode 100644 index 0000000..bc4b6da --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/ApatosaurusEntity.java @@ -0,0 +1,315 @@ +package net.cmr.jurassicrevived.entity.custom; + +import net.cmr.jurassicrevived.entity.ModEntities; +import net.cmr.jurassicrevived.entity.ai.SprintingMeleeAttackGoal; +import net.cmr.jurassicrevived.entity.ai.SprintingPanicGoal; +import net.cmr.jurassicrevived.entity.client.ApatosaurusVariant; +import net.cmr.jurassicrevived.entity.client.ApatosaurusVariant; +import net.cmr.jurassicrevived.sound.ModSounds; +import net.minecraft.Util; +import net.minecraft.core.BlockPos; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.util.Mth; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.*; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.attributes.DefaultAttributes; +import net.minecraft.world.entity.ai.goal.*; +import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerLevelAccessor; +import net.minecraft.world.level.block.state.BlockState; +import org.jetbrains.annotations.Nullable; +import software.bernie.geckolib.animatable.GeoEntity; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.core.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.core.animation.AnimatableManager; +import software.bernie.geckolib.core.animation.Animation; +import software.bernie.geckolib.core.animation.AnimationController; +import software.bernie.geckolib.core.animation.RawAnimation; +import software.bernie.geckolib.core.object.PlayState; +/*?} else {*/ +/*import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.animation.*; +*//*?}*/ + +public class ApatosaurusEntity extends Animal implements GeoEntity { + private AnimatableInstanceCache cache = new SingletonAnimatableInstanceCache(this); + + private static final EntityDataAccessor VARIANT = + SynchedEntityData.defineId(ApatosaurusEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SYNCED_AGE = + SynchedEntityData.defineId(ApatosaurusEntity.class, EntityDataSerializers.INT); + + // Procedural tail sway state (client-side use for rendering) + private float tailSwayOffset; // Smoothed offset in range roughly [-1, 1] + private float tailSwayVelocity; // Internal velocity for spring-damper + private float tailSwayPrev; // Previous frame value for interpolation + + public ApatosaurusEntity(EntityType pEntityType, Level pLevel) { + super(pEntityType, pLevel); + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new SprintingPanicGoal(this, 1.15) { + @Override + public boolean canUse() { + return ApatosaurusEntity.this.isBaby() && super.canUse(); + } + }); + this.targetSelector.addGoal(1, new HurtByTargetGoal(this).setAlertOthers()); + this.goalSelector.addGoal(2, new FloatGoal(this)); + this.goalSelector.addGoal(3, new AvoidEntityGoal<>(this, DilophosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(4, new AvoidEntityGoal<>(this, VelociraptorEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(5, new AvoidEntityGoal<>(this, CeratosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(6, new AvoidEntityGoal<>(this, SpinosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(7, new AvoidEntityGoal<>(this, TyrannosaurusRexEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(8, new SprintingMeleeAttackGoal(this, 1.1, false)); + this.goalSelector.addGoal(9, new BreedGoal(this, 1.0)); + this.goalSelector.addGoal(9, new FollowParentGoal(this, 1.25)); + this.goalSelector.addGoal(10, new WaterAvoidingRandomStrollGoal(this, 1.0)); + this.goalSelector.addGoal(11, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(12, new FollowMobGoal(this, 1, (float) 20, (float) 10)); + this.goalSelector.addGoal(13, new RandomLookAroundGoal(this)); + + } + + public static AttributeSupplier.Builder createAttributes() { + return Animal.createLivingAttributes() + .add(Attributes.MAX_HEALTH, 150D) + .add(Attributes.MOVEMENT_SPEED, 0.3D) + .add(Attributes.ARMOR, 0D) + .add(Attributes.FOLLOW_RANGE, 32D) + .add(Attributes.KNOCKBACK_RESISTANCE, 1D) + .add(Attributes.ATTACK_KNOCKBACK, 0D) + .add(Attributes.ATTACK_DAMAGE, 15D); + } + + @Override + public boolean isFood(ItemStack pStack) { + return pStack.is(Items.KELP); + } + + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel pLevel, AgeableMob pOtherParent) { + AgeableMob child = ModEntities.APATOSAURUS.get().create(pLevel); + if (child instanceof ApatosaurusEntity baby) { + ApatosaurusVariant randomVariant = Util.getRandom(ApatosaurusVariant.values(), this.random); + baby.setVariant(randomVariant); + } + return child; + } + + @Override + public boolean doHurtTarget(Entity target) { + boolean hit = super.doHurtTarget(target); + if (!level().isClientSide && hit && target instanceof LivingEntity) { + if (this.level() instanceof ServerLevel serverLevel) { + this.triggerAnim("attackController", "attack"); + this.playSound(ModSounds.TAIL_WHIP.get(), 1.0F, 1.0F); + } + } + return hit; + } + + @Override + public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { + controllers.add(new AnimationController<>(this, "Walk/Run/Idle", state -> { + if (state.isMoving()) + return state.setAndContinue(ApatosaurusEntity.this.isSprinting() ? RawAnimation.begin().then("anim.apatosaurus.run", Animation.LoopType.LOOP) : RawAnimation.begin().then("anim.apatosaurus.walk", Animation.LoopType.LOOP)); + + return state.setAndContinue(RawAnimation.begin().then("anim.apatosaurus.idle", Animation.LoopType.LOOP)); + })); + + controllers.add(new AnimationController<>(this, "attackController", state -> PlayState.STOP) + .triggerableAnim("attack", RawAnimation.begin().then("anim.apatosaurus.attack", Animation.LoopType.PLAY_ONCE))); + + controllers.add(new AnimationController<>(this, "mouthController", state -> PlayState.STOP) + .triggerableAnim("mouth", RawAnimation.begin().then("anim.apatosaurus.mouth", Animation.LoopType.PLAY_ONCE))); + } + + private float getSignedTurnDelta() { + // Only consider the body (torso) rotation so head look does not affect tail sway + return Mth.wrapDegrees(this.yBodyRot - this.yBodyRotO); + } + + private int mouthAnimCooldown = 0; + + @Override + public void tick() { + super.tick(); + + if (!level().isClientSide) { + this.entityData.set(DATA_SYNCED_AGE, this.getAge()); + var maxHealthAttr = getAttribute(Attributes.MAX_HEALTH); + if (maxHealthAttr != null) { + double baseAdult = DefaultAttributes.getSupplier((EntityType) this.getType()).getValue(Attributes.MAX_HEALTH); + double desired = this.isBaby() ? baseAdult * 0.10D : baseAdult; + if (maxHealthAttr.getBaseValue() != desired) { + double oldMax = maxHealthAttr.getBaseValue(); + double healthRatio = this.getHealth() / (float) oldMax; + maxHealthAttr.setBaseValue(desired); + this.setHealth((float) (desired * Mth.clamp(healthRatio, 0.0F, 1.0F))); + } + } + } + + if (!level().isClientSide) { + if (mouthAnimCooldown > 0) { + mouthAnimCooldown--; + } else { + this.triggerAnim("mouthController", "mouth"); + this.playSound(ModSounds.APATOSAURUS_CALL.get(), 1.0F, 1.0F); + // 30s–60s in ticks + mouthAnimCooldown = this.random.nextInt(1200 - 600 + 1) + 600; + } + } + + if (level().isClientSide) { + // Capture previous for smooth interpolation between ticks + this.tailSwayPrev = this.tailSwayOffset; + updateProceduralTailSway(); + } + } + + private void updateProceduralTailSway() { + // Turn input derived from rotation deltas; works even when standing still and turning + float turnDegrees = getSignedTurnDelta(); + + // Deadzone to ignore tiny jitter so the tail can return to center cleanly + float deadzoneDeg = 0.6f; // smaller deadzone for more responsiveness + float turnInput = 0.0f; + if (Math.abs(turnDegrees) >= deadzoneDeg) { + // Higher sensitivity so small in-place turns still affect the model + turnInput = Mth.clamp(turnDegrees / 15.0f, -1.0f, 1.0f); + } + + // Target offset: keep intuitive sign (positive input -> positive sway) + float target = turnInput; + + // One-pole low-pass (no bounce). Larger alpha => snappier and less "stiff". + float alpha = 0.24f; // try 0.20–0.30 to taste + + tailSwayOffset += (target - tailSwayOffset) * alpha; + + // Snap tiny residuals to zero so it visibly settles + if (Math.abs(tailSwayOffset) < 0.003f) { + tailSwayOffset = 0.0f; + } + + // No oscillation velocity retained + tailSwayVelocity = 0.0f; + + tailSwayOffset = Mth.clamp(tailSwayOffset, -1.5f, 1.5f); + } + + // Expose to the model for bone rotation + public float getTailSwayOffset() { + return tailSwayOffset; + } + + // Interpolated sway for smooth rendering between ticks + public float getTailSwayOffset(float partialTick) { + return Mth.lerp(Mth.clamp(partialTick, 0.0f, 1.0f), tailSwayPrev, tailSwayOffset); + } + + /*? if <=1.20.1 {*/ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); + this.entityData.define(VARIANT, 0); + this.entityData.define(DATA_SYNCED_AGE, 0); + } + /*?} else {*/ + /*@Override + protected void defineSynchedData(SynchedEntityData.Builder pBuilder) { + super.defineSynchedData(pBuilder); + pBuilder.define(VARIANT, 0); + pBuilder.define(DATA_SYNCED_AGE, 0); + } + *//*?}*/ + + public int getSyncedAge() { + return this.entityData.get(DATA_SYNCED_AGE); + } + public int getTypeVariant() { + return this.entityData.get(VARIANT); + } + + public ApatosaurusVariant getVariant() { + return ApatosaurusVariant.byId(this.getTypeVariant() & 255); + } + + private void setVariant(ApatosaurusVariant variant) { + this.entityData.set(VARIANT, variant.getId() & 255); + } + + @Override + public boolean canMate(Animal other) { + if (!super.canMate(other)) return false; + if (!(other instanceof ApatosaurusEntity that)) return false; + return this.getVariant() != that.getVariant(); + } + @Override + public void addAdditionalSaveData(CompoundTag pCompound) { + super.addAdditionalSaveData(pCompound); + pCompound.putInt("Variant", this.getTypeVariant()); + } + + @Override + public void readAdditionalSaveData(CompoundTag pCompound) { + super.readAdditionalSaveData(pCompound); + this.entityData.set(VARIANT, pCompound.getInt("Variant")); + } + + /*? if <=1.20.1 {*/ + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor pLevel, DifficultyInstance pDifficulty, MobSpawnType pReason, @Nullable SpawnGroupData pSpawnData, @Nullable CompoundTag pDataTag) { + ApatosaurusVariant variant = Util.getRandom(ApatosaurusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(pLevel, pDifficulty, pReason, pSpawnData, pDataTag); + } + /*?} else {*/ + /*@Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, MobSpawnType spawnType, @Nullable SpawnGroupData spawnGroupData) { + ApatosaurusVariant variant = Util.getRandom(ApatosaurusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(level, difficulty, spawnType, spawnGroupData); + } + *//*?}*/ + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } + + @Override + protected @Nullable SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.APATOSAURUS_HURT.get(); + } + + @Override + protected void playStepSound(BlockPos pos, BlockState blockIn) { + this.playSound(ModSounds.STOMP.get(), 0.5F, 1.0F); + } + + @Override + protected @Nullable SoundEvent getDeathSound() { + return ModSounds.APATOSAURUS_DEATH.get(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/custom/ArambourgianiaEntity.java b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/ArambourgianiaEntity.java new file mode 100644 index 0000000..99fee44 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/ArambourgianiaEntity.java @@ -0,0 +1,410 @@ +package net.cmr.jurassicrevived.entity.custom; + +import net.cmr.jurassicrevived.entity.ModEntities; +import net.cmr.jurassicrevived.entity.ai.SprintingMeleeAttackGoal; +import net.cmr.jurassicrevived.entity.ai.SprintingPanicGoal; +import net.cmr.jurassicrevived.entity.client.ArambourgianiaVariant; +import net.cmr.jurassicrevived.entity.client.ArambourgianiaVariant; +import net.cmr.jurassicrevived.sound.ModSounds; +import net.minecraft.Util; +import net.minecraft.core.BlockPos; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.*; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.attributes.DefaultAttributes; +import net.minecraft.world.entity.ai.control.FlyingMoveControl; +import net.minecraft.world.entity.ai.goal.*; +import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal; +import net.minecraft.world.entity.ai.goal.target.NearestAttackableTargetGoal; +import net.minecraft.world.entity.ai.navigation.FlyingPathNavigation; +import net.minecraft.world.entity.ai.navigation.PathNavigation; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.animal.FlyingAnimal; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerLevelAccessor; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.phys.Vec3; +import org.jetbrains.annotations.Nullable; +import software.bernie.geckolib.animatable.GeoEntity; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.core.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.core.animation.AnimatableManager; +import software.bernie.geckolib.core.animation.Animation; +import software.bernie.geckolib.core.animation.AnimationController; +import software.bernie.geckolib.core.animation.RawAnimation; +import software.bernie.geckolib.core.object.PlayState; +/*?} else {*/ +/*import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.animation.*; +*//*?}*/ + +public class ArambourgianiaEntity extends Animal implements GeoEntity, FlyingAnimal { + private final AnimatableInstanceCache cache = new SingletonAnimatableInstanceCache(this); + + private static final EntityDataAccessor VARIANT = + SynchedEntityData.defineId(ArambourgianiaEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SYNCED_AGE = + SynchedEntityData.defineId(ArambourgianiaEntity.class, EntityDataSerializers.INT); + + // Procedural tail sway state (client-side use for rendering) + private float tailSwayOffset; // Smoothed offset in range roughly [-1, 1] + private float tailSwayVelocity; // Internal velocity for spring-damper + private float tailSwayPrev; // Previous frame value for interpolation + + public ArambourgianiaEntity(EntityType pEntityType, Level pLevel) { + super(pEntityType, pLevel); + this.moveControl = new FlyingMoveControl(this, 20, true); + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new SprintingPanicGoal(this, 1.15) { + @Override + public boolean canUse() { + return ArambourgianiaEntity.this.isBaby() && super.canUse(); + } + }); + this.goalSelector.addGoal(2, new FloatGoal(this)); + this.goalSelector.addGoal(3, new AvoidEntityGoal<>(this, BrachiosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(4, new SprintingMeleeAttackGoal(this, 1.1, false)); + this.goalSelector.addGoal(5, new BreedGoal(this, 1.0)); + this.goalSelector.addGoal(6, new FollowParentGoal(this, 1.25)); + + // Goal 7: Wander on ground (Walk) - Only when on ground + this.goalSelector.addGoal(7, new WaterAvoidingRandomStrollGoal(this, 1.0) { + @Override + public boolean canUse() { + return ArambourgianiaEntity.this.onGround() && super.canUse(); + } + }); + + // Goal 8: Wander in air (Fly) - Handles takeoff, flying, and landing + this.goalSelector.addGoal(8, new WaterAvoidingRandomStrollGoal(this, 1.0, 20) { + @Override + public boolean canUse() { + boolean isFlying = !ArambourgianiaEntity.this.onGround(); + // If flying, keep flying. If on ground, small chance (1/400 ticks) to take off. + return (isFlying || ArambourgianiaEntity.this.getRandom().nextInt(400) == 0) && super.canUse(); + } + + @Override + protected Vec3 getPosition() { + Vec3 pos = ArambourgianiaEntity.this.position(); + RandomSource random = ArambourgianiaEntity.this.getRandom(); + + double x = pos.x + (random.nextFloat() * 2 - 1) * 32; + double z = pos.z + (random.nextFloat() * 2 - 1) * 32; + + // Get ground height at the random destination (returns Y of first air block) + int groundY = ArambourgianiaEntity.this.level().getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, (int)x, (int)z); + double y; + + if (ArambourgianiaEntity.this.onGround()) { + // Takeoff: Target well above ground to ensure liftoff + y = pos.y + 15 + random.nextInt(10); + } else { + // Flying: 5% chance to land, otherwise stay airborne but capped height + if (random.nextFloat() < 0.05f) { + y = groundY; // Land + } else if (pos.y > groundY + 20) { + // Too high: Force descent + y = pos.y - 5 - random.nextInt(10); + } else { + // Just wander up or down a bit + y = pos.y + (random.nextFloat() * 2 - 1) * 10; + } + } + + // Don't target below the ground (blocks) + if (y < groundY) y = groundY; + + return new Vec3(x, y, z); + } + }); + + this.goalSelector.addGoal(9, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(10, new FollowMobGoal(this, 0.8, (float) 20, (float) 10)); + this.targetSelector.addGoal(11, new NearestAttackableTargetGoal<>(this, Animal.class, 10, false, false, + target -> { + // 1. Don't eat your own species + if (target.getType() == this.getType()) return false; + + // 2. Don't eat Flying Animals + if (target instanceof FlyingAnimal) return false; + + // 3. SIZE CHECK: specific height and width limits + // Example: Height < 1.0 blocks AND Width < 1.0 blocks + boolean isSmallEnough = target.getBbHeight() <= 1.0F && target.getBbWidth() <= 1.0F; + + return isSmallEnough; + } + )); this.targetSelector.addGoal(12, new NearestAttackableTargetGoal<>(this, Player.class, false, false)); + this.targetSelector.addGoal(13, new HurtByTargetGoal(this)); + this.goalSelector.addGoal(14, new RandomLookAroundGoal(this)); + this.goalSelector.addGoal(15, new FloatGoal(this)); + } + + public static AttributeSupplier.Builder createAttributes() { + return Animal.createLivingAttributes() + .add(Attributes.MAX_HEALTH, 100D) + .add(Attributes.MOVEMENT_SPEED, 0.3D) + .add(Attributes.FLYING_SPEED, 0.6D) + .add(Attributes.ARMOR, 0D) + .add(Attributes.FOLLOW_RANGE, 32D) + .add(Attributes.KNOCKBACK_RESISTANCE, 0D) + .add(Attributes.ATTACK_KNOCKBACK, 0D) + .add(Attributes.ATTACK_DAMAGE, 20D); + } + + @Override + protected PathNavigation createNavigation(Level pLevel) { + FlyingPathNavigation navigation = new FlyingPathNavigation(this, pLevel); + navigation.setCanOpenDoors(false); + navigation.setCanFloat(true); + navigation.setCanPassDoors(true); + return navigation; + } + + @Override + public boolean isFlying() { + return !this.onGround(); + } + + @Override + public boolean causeFallDamage(float pFallDistance, float pMultiplier, DamageSource pSource) { + return false; + } + + @Override + protected void checkFallDamage(double pY, boolean pOnGround, BlockState pState, BlockPos pPos) { + } + + @Override + public boolean isFood(ItemStack pStack) { + return pStack.is(Items.BEEF); + } + + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel pLevel, AgeableMob pOtherParent) { + AgeableMob child = ModEntities.ARAMBOURGIANIA.get().create(pLevel); + if (child instanceof ArambourgianiaEntity baby) { + ArambourgianiaVariant randomVariant = Util.getRandom(ArambourgianiaVariant.values(), this.random); + baby.setVariant(randomVariant); + } + return child; + } + + @Override + public boolean doHurtTarget(Entity target) { + boolean hit = super.doHurtTarget(target); + if (!level().isClientSide && hit && target instanceof LivingEntity) { + if (this.level() instanceof ServerLevel serverLevel) { + this.triggerAnim("attackController", "attack"); + this.playSound(ModSounds.BEAK_ATTACK.get(), 1.0F, 1.0F); + } + } + return hit; + } + + @Override + public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { + controllers.add(new AnimationController<>(this, "Walk/Run/Idle", state -> { + if (!ArambourgianiaEntity.this.onGround()) { + return state.setAndContinue(RawAnimation.begin().then("anim.arambourgiania.fly", Animation.LoopType.LOOP)); + } + + if (state.isMoving()) + return state.setAndContinue(ArambourgianiaEntity.this.isSprinting() ? RawAnimation.begin().then("anim.arambourgiania.walk", Animation.LoopType.LOOP) : RawAnimation.begin().then("anim.arambourgiania.walk", Animation.LoopType.LOOP)); + + return state.setAndContinue(RawAnimation.begin().then("anim.arambourgiania.idle", Animation.LoopType.LOOP)); + })); + + controllers.add(new AnimationController<>(this, "attackController", state -> PlayState.STOP) + .triggerableAnim("attack", RawAnimation.begin().then("anim.arambourgiania.attack", Animation.LoopType.PLAY_ONCE))); + + controllers.add(new AnimationController<>(this, "mouthController", state -> PlayState.STOP) + .triggerableAnim("mouth", RawAnimation.begin().then("anim.arambourgiania.mouth", Animation.LoopType.PLAY_ONCE))); + } + + private float getSignedTurnDelta() { + // Only consider the body (torso) rotation so head look does not affect tail sway + return Mth.wrapDegrees(this.yBodyRot - this.yBodyRotO); + } + + private int mouthAnimCooldown = 0; + + @Override + public void tick() { + super.tick(); + if (!level().isClientSide) { + this.entityData.set(DATA_SYNCED_AGE, this.getAge()); + var maxHealthAttr = getAttribute(Attributes.MAX_HEALTH); + if (maxHealthAttr != null) { + double baseAdult = DefaultAttributes.getSupplier((EntityType) this.getType()).getValue(Attributes.MAX_HEALTH); + double desired = this.isBaby() ? baseAdult * 0.10D : baseAdult; + if (maxHealthAttr.getBaseValue() != desired) { + double oldMax = maxHealthAttr.getBaseValue(); + double healthRatio = this.getHealth() / (float) oldMax; + maxHealthAttr.setBaseValue(desired); + this.setHealth((float) (desired * Mth.clamp(healthRatio, 0.0F, 1.0F))); + } + } + } + + if (!level().isClientSide) { + if (mouthAnimCooldown > 0) { + mouthAnimCooldown--; + } else { + this.triggerAnim("mouthController", "mouth"); + this.playSound(ModSounds.ARAMBOURGIANIA_CALL.get(), 1.0F, 1.0F); + // 30s–60s in ticks + mouthAnimCooldown = this.random.nextInt(1200 - 600 + 1) + 600; + } + } + + if (level().isClientSide) { + // Capture previous for smooth interpolation between ticks + this.tailSwayPrev = this.tailSwayOffset; + updateProceduralTailSway(); + } + } + + private void updateProceduralTailSway() { + // Turn input derived from rotation deltas; works even when standing still and turning + float turnDegrees = getSignedTurnDelta(); + + // Deadzone to ignore tiny jitter so the tail can return to center cleanly + float deadzoneDeg = 0.6f; // smaller deadzone for more responsiveness + float turnInput = 0.0f; + if (Math.abs(turnDegrees) >= deadzoneDeg) { + // Higher sensitivity so small in-place turns still affect the model + turnInput = Mth.clamp(turnDegrees / 15.0f, -1.0f, 1.0f); + } + + // Target offset: keep intuitive sign (positive input -> positive sway) + float target = turnInput; + + // One-pole low-pass (no bounce). Larger alpha => snappier and less "stiff". + float alpha = 0.24f; // try 0.20–0.30 to taste + + tailSwayOffset += (target - tailSwayOffset) * alpha; + + // Snap tiny residuals to zero so it visibly settles + if (Math.abs(tailSwayOffset) < 0.003f) { + tailSwayOffset = 0.0f; + } + + // No oscillation velocity retained + tailSwayVelocity = 0.0f; + + tailSwayOffset = Mth.clamp(tailSwayOffset, -1.5f, 1.5f); + } + + // Expose to the model for bone rotation + public float getTailSwayOffset() { + return tailSwayOffset; + } + + // Interpolated sway for smooth rendering between ticks + public float getTailSwayOffset(float partialTick) { + return Mth.lerp(Mth.clamp(partialTick, 0.0f, 1.0f), tailSwayPrev, tailSwayOffset); + } + + /*? if <=1.20.1 {*/ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); + this.entityData.define(VARIANT, 0); + this.entityData.define(DATA_SYNCED_AGE, 0); + } + /*?} else {*/ + /*@Override + protected void defineSynchedData(SynchedEntityData.Builder pBuilder) { + super.defineSynchedData(pBuilder); + pBuilder.define(VARIANT, 0); + pBuilder.define(DATA_SYNCED_AGE, 0); + } + *//*?}*/ + + public int getSyncedAge() { + return this.entityData.get(DATA_SYNCED_AGE); + } + + public ArambourgianiaVariant getVariant() { + return ArambourgianiaVariant.byId(this.getTypeVariant() & 255); + } + public int getTypeVariant() { + return this.entityData.get(VARIANT); + } + + private void setVariant(ArambourgianiaVariant variant) { + this.entityData.set(VARIANT, variant.getId() & 255); + } + + @Override + public boolean canMate(Animal other) { + if (!super.canMate(other)) return false; + if (!(other instanceof ArambourgianiaEntity that)) return false; + return this.getVariant() != that.getVariant(); + } + + /*? if <=1.20.1 {*/ + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor pLevel, DifficultyInstance pDifficulty, MobSpawnType pReason, @Nullable SpawnGroupData pSpawnData, @Nullable CompoundTag pDataTag) { + ArambourgianiaVariant variant = Util.getRandom(ArambourgianiaVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(pLevel, pDifficulty, pReason, pSpawnData, pDataTag); + } + /*?} else {*/ + /*@Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, MobSpawnType spawnType, @Nullable SpawnGroupData spawnGroupData) { + ArambourgianiaVariant variant = Util.getRandom(ArambourgianiaVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(level, difficulty, spawnType, spawnGroupData); + } + *//*?}*/ + + @Override + public void readAdditionalSaveData(CompoundTag pCompound) { + super.readAdditionalSaveData(pCompound); + this.entityData.set(VARIANT, pCompound.getInt("Variant")); + } + + @Override + public void addAdditionalSaveData(CompoundTag pCompound) { + super.addAdditionalSaveData(pCompound); + pCompound.putInt("Variant", this.getTypeVariant()); + } + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } + + @Override + protected @Nullable SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.ARAMBOURGIANIA_HURT.get(); + } + + @Override + protected @Nullable SoundEvent getDeathSound() { + return ModSounds.ARAMBOURGIANIA_DEATH.get(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/custom/BaryonyxEntity.java b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/BaryonyxEntity.java new file mode 100644 index 0000000..c214191 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/BaryonyxEntity.java @@ -0,0 +1,316 @@ +package net.cmr.jurassicrevived.entity.custom; + +import net.cmr.jurassicrevived.entity.ModEntities; +import net.cmr.jurassicrevived.entity.ai.SprintingMeleeAttackGoal; +import net.cmr.jurassicrevived.entity.ai.SprintingPanicGoal; +import net.cmr.jurassicrevived.entity.client.BaryonyxVariant; +import net.cmr.jurassicrevived.entity.client.BaryonyxVariant; +import net.cmr.jurassicrevived.sound.ModSounds; +import net.minecraft.Util; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.util.Mth; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.*; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.attributes.DefaultAttributes; +import net.minecraft.world.entity.ai.goal.*; +import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal; +import net.minecraft.world.entity.ai.goal.target.NearestAttackableTargetGoal; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerLevelAccessor; +import org.jetbrains.annotations.Nullable; +import software.bernie.geckolib.animatable.GeoEntity; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.core.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.core.animation.AnimatableManager; +import software.bernie.geckolib.core.animation.Animation; +import software.bernie.geckolib.core.animation.AnimationController; +import software.bernie.geckolib.core.animation.RawAnimation; +import software.bernie.geckolib.core.object.PlayState; +/*?} else {*/ +/*import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.animation.*; +*//*?}*/ + +public class BaryonyxEntity extends Animal implements GeoEntity { + private AnimatableInstanceCache cache = new SingletonAnimatableInstanceCache(this); + + private static final EntityDataAccessor VARIANT = + SynchedEntityData.defineId(BaryonyxEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SYNCED_AGE = + SynchedEntityData.defineId(BaryonyxEntity.class, EntityDataSerializers.INT); + + // Procedural tail sway state (client-side use for rendering) + private float tailSwayOffset; // Smoothed offset in range roughly [-1, 1] + private float tailSwayVelocity; // Internal velocity for spring-damper + private float tailSwayPrev; // Previous frame value for interpolation + + public BaryonyxEntity(EntityType pEntityType, Level pLevel) { + super(pEntityType, pLevel); + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new SprintingPanicGoal(this, 1.15) { + @Override + public boolean canUse() { + return BaryonyxEntity.this.isBaby() && super.canUse(); + } + }); + this.targetSelector.addGoal(1, new HurtByTargetGoal(this).setAlertOthers()); + this.goalSelector.addGoal(2, new FloatGoal(this)); + this.goalSelector.addGoal(3, new AvoidEntityGoal<>(this, BrachiosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(4, new SprintingMeleeAttackGoal(this, 1.1, false)); + this.goalSelector.addGoal(5, new BreedGoal(this, 1.0)); + this.goalSelector.addGoal(6, new FollowParentGoal(this, 1.25)); + this.goalSelector.addGoal(7, new WaterAvoidingRandomStrollGoal(this, 1.0)); + this.goalSelector.addGoal(8, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.targetSelector.addGoal(9, new NearestAttackableTargetGoal<>(this, Animal.class, 10, false, false, + target -> target.getType() != this.getType())); + this.targetSelector.addGoal(10, new NearestAttackableTargetGoal(this, TriceratopsEntity.class, false, false)); + this.targetSelector.addGoal(11, new NearestAttackableTargetGoal(this, GallimimusEntity.class, false, false)); + this.targetSelector.addGoal(12, new NearestAttackableTargetGoal(this, DilophosaurusEntity.class, false, false)); + this.targetSelector.addGoal(13, new NearestAttackableTargetGoal(this, CeratosaurusEntity.class, false, false)); + this.targetSelector.addGoal(14, new NearestAttackableTargetGoal(this, ParasaurolophusEntity.class, false, false)); + this.targetSelector.addGoal(15, new NearestAttackableTargetGoal(this, VelociraptorEntity.class, false, false)); + this.targetSelector.addGoal(16, new NearestAttackableTargetGoal(this, TyrannosaurusRexEntity.class, false, false)); + this.targetSelector.addGoal(17, new NearestAttackableTargetGoal(this, IndominusRexEntity.class, false, false)); + this.targetSelector.addGoal(18, new NearestAttackableTargetGoal(this, Player.class, false, false)); + this.goalSelector.addGoal(19, new RandomLookAroundGoal(this)); + + + } + + public static AttributeSupplier.Builder createAttributes() { + return Animal.createLivingAttributes() + .add(Attributes.MAX_HEALTH, 55D) + .add(Attributes.MOVEMENT_SPEED, 0.3D) + .add(Attributes.ARMOR, 0D) + .add(Attributes.FOLLOW_RANGE, 32D) + .add(Attributes.KNOCKBACK_RESISTANCE, 0D) + .add(Attributes.ATTACK_KNOCKBACK, 0D) + .add(Attributes.ATTACK_DAMAGE, 15D); + } + + @Override + public boolean isFood(ItemStack pStack) { + return pStack.is(Items.BEEF); + } + + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel pLevel, AgeableMob pOtherParent) { + AgeableMob child = ModEntities.BARYONYX.get().create(pLevel); + if (child instanceof BaryonyxEntity baby) { + BaryonyxVariant randomVariant = Util.getRandom(BaryonyxVariant.values(), this.random); + baby.setVariant(randomVariant); + } + return child; + } + + @Override + public boolean doHurtTarget(Entity target) { + boolean hit = super.doHurtTarget(target); + if (!level().isClientSide && hit && target instanceof LivingEntity) { + if (this.level() instanceof ServerLevel serverLevel) { + this.triggerAnim("attackController", "attack"); + this.playSound(ModSounds.BARYONYX_ATTACK.get(), 1.0F, 1.0F); + } + } + return hit; + } + + @Override + public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { + controllers.add(new AnimationController<>(this, "Walk/Run/Idle", state -> { + if (state.isMoving()) + return state.setAndContinue(BaryonyxEntity.this.isSprinting() ? RawAnimation.begin().then("anim.baryonyx.run", Animation.LoopType.LOOP) : RawAnimation.begin().then("anim.baryonyx.walk", Animation.LoopType.LOOP)); + + return state.setAndContinue(RawAnimation.begin().then("anim.baryonyx.idle", Animation.LoopType.LOOP)); + })); + + controllers.add(new AnimationController<>(this, "attackController", state -> PlayState.STOP) + .triggerableAnim("attack", RawAnimation.begin().then("anim.baryonyx.attack", Animation.LoopType.PLAY_ONCE))); + + controllers.add(new AnimationController<>(this, "mouthController", state -> PlayState.STOP) + .triggerableAnim("mouth", RawAnimation.begin().then("anim.baryonyx.mouth", Animation.LoopType.PLAY_ONCE))); + } + + private float getSignedTurnDelta() { + // Only consider the body (torso) rotation so head look does not affect tail sway + return Mth.wrapDegrees(this.yBodyRot - this.yBodyRotO); + } + + private int mouthAnimCooldown = 0; + + @Override + public void tick() { + super.tick(); + + if (!level().isClientSide) { + this.entityData.set(DATA_SYNCED_AGE, this.getAge()); + var maxHealthAttr = getAttribute(Attributes.MAX_HEALTH); + if (maxHealthAttr != null) { + double baseAdult = DefaultAttributes.getSupplier((EntityType) this.getType()).getValue(Attributes.MAX_HEALTH); + double desired = this.isBaby() ? baseAdult * 0.10D : baseAdult; + if (maxHealthAttr.getBaseValue() != desired) { + double oldMax = maxHealthAttr.getBaseValue(); + double healthRatio = this.getHealth() / (float) oldMax; + maxHealthAttr.setBaseValue(desired); + this.setHealth((float) (desired * Mth.clamp(healthRatio, 0.0F, 1.0F))); + } + } + } + + if (!level().isClientSide) { + if (mouthAnimCooldown > 0) { + mouthAnimCooldown--; + } else { + this.triggerAnim("mouthController", "mouth"); + this.playSound(ModSounds.BARYONYX_CALL.get(), 1.0F, 1.0F); + // 30s–60s in ticks + mouthAnimCooldown = this.random.nextInt(1200 - 600 + 1) + 600; + } + } + + if (level().isClientSide) { + // Capture previous for smooth interpolation between ticks + this.tailSwayPrev = this.tailSwayOffset; + updateProceduralTailSway(); + } + } + + private void updateProceduralTailSway() { + // Turn input derived from rotation deltas; works even when standing still and turning + float turnDegrees = getSignedTurnDelta(); + + // Deadzone to ignore tiny jitter so the tail can return to center cleanly + float deadzoneDeg = 0.6f; // smaller deadzone for more responsiveness + float turnInput = 0.0f; + if (Math.abs(turnDegrees) >= deadzoneDeg) { + // Higher sensitivity so small in-place turns still affect the model + turnInput = Mth.clamp(turnDegrees / 15.0f, -1.0f, 1.0f); + } + + // Target offset: keep intuitive sign (positive input -> positive sway) + float target = turnInput; + + // One-pole low-pass (no bounce). Larger alpha => snappier and less "stiff". + float alpha = 0.24f; // try 0.20–0.30 to taste + + tailSwayOffset += (target - tailSwayOffset) * alpha; + + // Snap tiny residuals to zero so it visibly settles + if (Math.abs(tailSwayOffset) < 0.003f) { + tailSwayOffset = 0.0f; + } + + // No oscillation velocity retained + tailSwayVelocity = 0.0f; + + tailSwayOffset = Mth.clamp(tailSwayOffset, -1.5f, 1.5f); + } + + // Expose to the model for bone rotation + public float getTailSwayOffset() { + return tailSwayOffset; + } + + // Interpolated sway for smooth rendering between ticks + public float getTailSwayOffset(float partialTick) { + return Mth.lerp(Mth.clamp(partialTick, 0.0f, 1.0f), tailSwayPrev, tailSwayOffset); + } + + /*? if <=1.20.1 {*/ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); + this.entityData.define(VARIANT, 0); + this.entityData.define(DATA_SYNCED_AGE, 0); + } + /*?} else {*/ + /*@Override + protected void defineSynchedData(SynchedEntityData.Builder pBuilder) { + super.defineSynchedData(pBuilder); + pBuilder.define(VARIANT, 0); + pBuilder.define(DATA_SYNCED_AGE, 0); + } + *//*?}*/ + + public int getSyncedAge() { + return this.entityData.get(DATA_SYNCED_AGE); + } + public int getTypeVariant() { + return this.entityData.get(VARIANT); + } + + public BaryonyxVariant getVariant() { + return BaryonyxVariant.byId(this.getTypeVariant() & 255); + } + + private void setVariant(BaryonyxVariant variant) { + this.entityData.set(VARIANT, variant.getId() & 255); + } + + @Override + public boolean canMate(Animal other) { + if (!super.canMate(other)) return false; + if (!(other instanceof BaryonyxEntity that)) return false; + return this.getVariant() != that.getVariant(); + } + @Override + public void addAdditionalSaveData(CompoundTag pCompound) { + super.addAdditionalSaveData(pCompound); + pCompound.putInt("Variant", this.getTypeVariant()); + } + + @Override + public void readAdditionalSaveData(CompoundTag pCompound) { + super.readAdditionalSaveData(pCompound); + this.entityData.set(VARIANT, pCompound.getInt("Variant")); + } + + /*? if <=1.20.1 {*/ + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor pLevel, DifficultyInstance pDifficulty, MobSpawnType pReason, @Nullable SpawnGroupData pSpawnData, @Nullable CompoundTag pDataTag) { + BaryonyxVariant variant = Util.getRandom(BaryonyxVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(pLevel, pDifficulty, pReason, pSpawnData, pDataTag); + } + /*?} else {*/ + /*@Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, MobSpawnType spawnType, @Nullable SpawnGroupData spawnGroupData) { + BaryonyxVariant variant = Util.getRandom(BaryonyxVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(level, difficulty, spawnType, spawnGroupData); + } + *//*?}*/ + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } + + @Override + protected @Nullable SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.BARYONYX_HURT.get(); + } + + @Override + protected @Nullable SoundEvent getDeathSound() { + return ModSounds.BARYONYX_DEATH.get(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/custom/BrachiosaurusEntity.java b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/BrachiosaurusEntity.java new file mode 100644 index 0000000..1439e5a --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/BrachiosaurusEntity.java @@ -0,0 +1,315 @@ +package net.cmr.jurassicrevived.entity.custom; + +import net.cmr.jurassicrevived.entity.ModEntities; +import net.cmr.jurassicrevived.entity.ai.SprintingMeleeAttackGoal; +import net.cmr.jurassicrevived.entity.ai.SprintingPanicGoal; +import net.cmr.jurassicrevived.entity.client.BrachiosaurusVariant; +import net.cmr.jurassicrevived.entity.client.BrachiosaurusVariant; +import net.cmr.jurassicrevived.sound.ModSounds; +import net.minecraft.Util; +import net.minecraft.core.BlockPos; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.util.Mth; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.*; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.attributes.DefaultAttributes; +import net.minecraft.world.entity.ai.goal.*; +import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerLevelAccessor; +import net.minecraft.world.level.block.state.BlockState; +import org.jetbrains.annotations.Nullable; +import software.bernie.geckolib.animatable.GeoEntity; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.core.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.core.animation.AnimatableManager; +import software.bernie.geckolib.core.animation.Animation; +import software.bernie.geckolib.core.animation.AnimationController; +import software.bernie.geckolib.core.animation.RawAnimation; +import software.bernie.geckolib.core.object.PlayState; +/*?} else {*/ +/*import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.animation.*; +*//*?}*/ + +public class BrachiosaurusEntity extends Animal implements GeoEntity { + private AnimatableInstanceCache cache = new SingletonAnimatableInstanceCache(this); + + private static final EntityDataAccessor VARIANT = + SynchedEntityData.defineId(BrachiosaurusEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SYNCED_AGE = + SynchedEntityData.defineId(BrachiosaurusEntity.class, EntityDataSerializers.INT); + + // Procedural tail sway state (client-side use for rendering) + private float tailSwayOffset; // Smoothed offset in range roughly [-1, 1] + private float tailSwayVelocity; // Internal velocity for spring-damper + private float tailSwayPrev; // Previous frame value for interpolation + + public BrachiosaurusEntity(EntityType pEntityType, Level pLevel) { + super(pEntityType, pLevel); + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new SprintingPanicGoal(this, 1.15) { + @Override + public boolean canUse() { + return BrachiosaurusEntity.this.isBaby() && super.canUse(); + } + }); + this.targetSelector.addGoal(1, new HurtByTargetGoal(this).setAlertOthers()); + this.goalSelector.addGoal(2, new FloatGoal(this)); + this.goalSelector.addGoal(3, new AvoidEntityGoal<>(this, DilophosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(4, new AvoidEntityGoal<>(this, VelociraptorEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(5, new AvoidEntityGoal<>(this, CeratosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(6, new AvoidEntityGoal<>(this, SpinosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(7, new AvoidEntityGoal<>(this, TyrannosaurusRexEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(8, new SprintingMeleeAttackGoal(this, 1.1, false)); + this.goalSelector.addGoal(9, new BreedGoal(this, 1.0)); + this.goalSelector.addGoal(9, new FollowParentGoal(this, 1.25)); + this.goalSelector.addGoal(10, new WaterAvoidingRandomStrollGoal(this, 1.0)); + this.goalSelector.addGoal(11, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(12, new FollowMobGoal(this, 1, (float) 20, (float) 10)); + this.goalSelector.addGoal(13, new RandomLookAroundGoal(this)); + + } + + public static AttributeSupplier.Builder createAttributes() { + return Animal.createLivingAttributes() + .add(Attributes.MAX_HEALTH, 200D) + .add(Attributes.MOVEMENT_SPEED, 0.3D) + .add(Attributes.ARMOR, 0D) + .add(Attributes.FOLLOW_RANGE, 32D) + .add(Attributes.KNOCKBACK_RESISTANCE, 1D) + .add(Attributes.ATTACK_KNOCKBACK, 0D) + .add(Attributes.ATTACK_DAMAGE, 20D); + } + + @Override + public boolean isFood(ItemStack pStack) { + return pStack.is(Items.KELP); + } + + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel pLevel, AgeableMob pOtherParent) { + AgeableMob child = ModEntities.BRACHIOSAURUS.get().create(pLevel); + if (child instanceof BrachiosaurusEntity baby) { + BrachiosaurusVariant randomVariant = Util.getRandom(BrachiosaurusVariant.values(), this.random); + baby.setVariant(randomVariant); + } + return child; + } + + @Override + public boolean doHurtTarget(Entity target) { + boolean hit = super.doHurtTarget(target); + if (!level().isClientSide && hit && target instanceof LivingEntity) { + if (this.level() instanceof ServerLevel serverLevel) { + this.triggerAnim("attackController", "attack"); + this.playSound(ModSounds.STOMP_ATTACK.get(), 1.0F, 1.0F); + } + } + return hit; + } + + @Override + public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { + controllers.add(new AnimationController<>(this, "Walk/Run/Idle", state -> { + if (state.isMoving()) + return state.setAndContinue(BrachiosaurusEntity.this.isSprinting() ? RawAnimation.begin().then("anim.brachiosaurus.run", Animation.LoopType.LOOP) : RawAnimation.begin().then("anim.brachiosaurus.walk", Animation.LoopType.LOOP)); + + return state.setAndContinue(RawAnimation.begin().then("anim.brachiosaurus.idle", Animation.LoopType.LOOP)); + })); + + controllers.add(new AnimationController<>(this, "attackController", state -> PlayState.STOP) + .triggerableAnim("attack", RawAnimation.begin().then("anim.brachiosaurus.attack", Animation.LoopType.PLAY_ONCE))); + + controllers.add(new AnimationController<>(this, "mouthController", state -> PlayState.STOP) + .triggerableAnim("mouth", RawAnimation.begin().then("anim.brachiosaurus.mouth", Animation.LoopType.PLAY_ONCE))); + } + + private float getSignedTurnDelta() { + // Only consider the body (torso) rotation so head look does not affect tail sway + return Mth.wrapDegrees(this.yBodyRot - this.yBodyRotO); + } + + private int mouthAnimCooldown = 0; + + @Override + public void tick() { + super.tick(); + + if (!level().isClientSide) { + this.entityData.set(DATA_SYNCED_AGE, this.getAge()); + var maxHealthAttr = getAttribute(Attributes.MAX_HEALTH); + if (maxHealthAttr != null) { + double baseAdult = DefaultAttributes.getSupplier((EntityType) this.getType()).getValue(Attributes.MAX_HEALTH); + double desired = this.isBaby() ? baseAdult * 0.10D : baseAdult; + if (maxHealthAttr.getBaseValue() != desired) { + double oldMax = maxHealthAttr.getBaseValue(); + double healthRatio = this.getHealth() / (float) oldMax; + maxHealthAttr.setBaseValue(desired); + this.setHealth((float) (desired * Mth.clamp(healthRatio, 0.0F, 1.0F))); + } + } + } + + if (!level().isClientSide) { + if (mouthAnimCooldown > 0) { + mouthAnimCooldown--; + } else { + this.triggerAnim("mouthController", "mouth"); + this.playSound(ModSounds.BRACHIOSAURUS_CALL.get(), 1.0F, 1.0F); + // 30s–60s in ticks + mouthAnimCooldown = this.random.nextInt(1200 - 600 + 1) + 600; + } + } + + if (level().isClientSide) { + // Capture previous for smooth interpolation between ticks + this.tailSwayPrev = this.tailSwayOffset; + updateProceduralTailSway(); + } + } + + private void updateProceduralTailSway() { + // Turn input derived from rotation deltas; works even when standing still and turning + float turnDegrees = getSignedTurnDelta(); + + // Deadzone to ignore tiny jitter so the tail can return to center cleanly + float deadzoneDeg = 0.6f; // smaller deadzone for more responsiveness + float turnInput = 0.0f; + if (Math.abs(turnDegrees) >= deadzoneDeg) { + // Higher sensitivity so small in-place turns still affect the model + turnInput = Mth.clamp(turnDegrees / 15.0f, -1.0f, 1.0f); + } + + // Target offset: keep intuitive sign (positive input -> positive sway) + float target = turnInput; + + // One-pole low-pass (no bounce). Larger alpha => snappier and less "stiff". + float alpha = 0.24f; // try 0.20–0.30 to taste + + tailSwayOffset += (target - tailSwayOffset) * alpha; + + // Snap tiny residuals to zero so it visibly settles + if (Math.abs(tailSwayOffset) < 0.003f) { + tailSwayOffset = 0.0f; + } + + // No oscillation velocity retained + tailSwayVelocity = 0.0f; + + tailSwayOffset = Mth.clamp(tailSwayOffset, -1.5f, 1.5f); + } + + // Expose to the model for bone rotation + public float getTailSwayOffset() { + return tailSwayOffset; + } + + // Interpolated sway for smooth rendering between ticks + public float getTailSwayOffset(float partialTick) { + return Mth.lerp(Mth.clamp(partialTick, 0.0f, 1.0f), tailSwayPrev, tailSwayOffset); + } + + /*? if <=1.20.1 {*/ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); + this.entityData.define(VARIANT, 0); + this.entityData.define(DATA_SYNCED_AGE, 0); + } + /*?} else {*/ + /*@Override + protected void defineSynchedData(SynchedEntityData.Builder pBuilder) { + super.defineSynchedData(pBuilder); + pBuilder.define(VARIANT, 0); + pBuilder.define(DATA_SYNCED_AGE, 0); + } + *//*?}*/ + + public int getSyncedAge() { + return this.entityData.get(DATA_SYNCED_AGE); + } + public int getTypeVariant() { + return this.entityData.get(VARIANT); + } + + public BrachiosaurusVariant getVariant() { + return BrachiosaurusVariant.byId(this.getTypeVariant() & 255); + } + + private void setVariant(BrachiosaurusVariant variant) { + this.entityData.set(VARIANT, variant.getId() & 255); + } + + @Override + public boolean canMate(Animal other) { + if (!super.canMate(other)) return false; + if (!(other instanceof BrachiosaurusEntity that)) return false; + return this.getVariant() != that.getVariant(); + } + @Override + public void addAdditionalSaveData(CompoundTag pCompound) { + super.addAdditionalSaveData(pCompound); + pCompound.putInt("Variant", this.getTypeVariant()); + } + + @Override + public void readAdditionalSaveData(CompoundTag pCompound) { + super.readAdditionalSaveData(pCompound); + this.entityData.set(VARIANT, pCompound.getInt("Variant")); + } + + /*? if <=1.20.1 {*/ + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor pLevel, DifficultyInstance pDifficulty, MobSpawnType pReason, @Nullable SpawnGroupData pSpawnData, @Nullable CompoundTag pDataTag) { + BrachiosaurusVariant variant = Util.getRandom(BrachiosaurusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(pLevel, pDifficulty, pReason, pSpawnData, pDataTag); + } + /*?} else {*/ + /*@Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, MobSpawnType spawnType, @Nullable SpawnGroupData spawnGroupData) { + BrachiosaurusVariant variant = Util.getRandom(BrachiosaurusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(level, difficulty, spawnType, spawnGroupData); + } + *//*?}*/ + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } + + @Override + protected @Nullable SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.BRACHIOSAURUS_HURT.get(); + } + + @Override + protected void playStepSound(BlockPos pos, BlockState blockIn) { + this.playSound(ModSounds.STOMP.get(), 0.5F, 1.0F); + } + + @Override + protected @Nullable SoundEvent getDeathSound() { + return ModSounds.BRACHIOSAURUS_DEATH.get(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/custom/CarcharodontosaurusEntity.java b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/CarcharodontosaurusEntity.java new file mode 100644 index 0000000..612a1de --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/CarcharodontosaurusEntity.java @@ -0,0 +1,325 @@ +package net.cmr.jurassicrevived.entity.custom; + +import net.cmr.jurassicrevived.entity.ModEntities; +import net.cmr.jurassicrevived.entity.ai.SprintingMeleeAttackGoal; +import net.cmr.jurassicrevived.entity.ai.SprintingPanicGoal; +import net.cmr.jurassicrevived.entity.client.CarcharodontosaurusVariant; +import net.cmr.jurassicrevived.entity.client.CarcharodontosaurusVariant; +import net.cmr.jurassicrevived.sound.ModSounds; +import net.minecraft.Util; +import net.minecraft.core.BlockPos; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.util.Mth; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.*; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.attributes.DefaultAttributes; +import net.minecraft.world.entity.ai.goal.*; +import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal; +import net.minecraft.world.entity.ai.goal.target.NearestAttackableTargetGoal; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.monster.Monster; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerLevelAccessor; +import net.minecraft.world.level.block.state.BlockState; +import org.jetbrains.annotations.Nullable; +import software.bernie.geckolib.animatable.GeoEntity; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.core.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.core.animation.AnimatableManager; +import software.bernie.geckolib.core.animation.Animation; +import software.bernie.geckolib.core.animation.AnimationController; +import software.bernie.geckolib.core.animation.RawAnimation; +import software.bernie.geckolib.core.object.PlayState; +/*?} else {*/ +/*import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.animation.*; +*//*?}*/ + +public class CarcharodontosaurusEntity extends Animal implements GeoEntity { + private AnimatableInstanceCache cache = new SingletonAnimatableInstanceCache(this); + + private static final EntityDataAccessor VARIANT = + SynchedEntityData.defineId(CarcharodontosaurusEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SYNCED_AGE = + SynchedEntityData.defineId(CarcharodontosaurusEntity.class, EntityDataSerializers.INT); + + // Procedural tail sway state (client-side use for rendering) + private float tailSwayOffset; // Smoothed offset in range roughly [-1, 1] + private float tailSwayVelocity; // Internal velocity for spring-damper + private float tailSwayPrev; // Previous frame value for interpolation + + public CarcharodontosaurusEntity(EntityType pEntityType, Level pLevel) { + super(pEntityType, pLevel); + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new SprintingPanicGoal(this, 1.15) { + @Override + public boolean canUse() { + return CarcharodontosaurusEntity.this.isBaby() && super.canUse(); + } + }); + this.targetSelector.addGoal(1, new HurtByTargetGoal(this).setAlertOthers()); + this.goalSelector.addGoal(2, new FloatGoal(this)); + this.goalSelector.addGoal(3, new AvoidEntityGoal<>(this, BrachiosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(4, new SprintingMeleeAttackGoal(this, 1.1, false)); + this.goalSelector.addGoal(5, new BreedGoal(this, 1.0)); + this.goalSelector.addGoal(6, new FollowParentGoal(this, 1.25)); + this.goalSelector.addGoal(7, new WaterAvoidingRandomStrollGoal(this, 1.0)); + this.goalSelector.addGoal(8, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(9, new FollowMobGoal(this, 1.2, (float) 20, (float) 10)); + this.targetSelector.addGoal(10, new NearestAttackableTargetGoal<>(this, Monster.class, true)); + this.targetSelector.addGoal(11, new NearestAttackableTargetGoal<>(this, Animal.class, 10, false, false, + target -> target.getType() != this.getType())); + this.targetSelector.addGoal(12, new NearestAttackableTargetGoal(this, TriceratopsEntity.class, false, false)); + this.targetSelector.addGoal(13, new NearestAttackableTargetGoal(this, GallimimusEntity.class, false, false)); + this.targetSelector.addGoal(14, new NearestAttackableTargetGoal(this, DilophosaurusEntity.class, false, false)); + this.targetSelector.addGoal(15, new NearestAttackableTargetGoal(this, CeratosaurusEntity.class, false, false)); + this.targetSelector.addGoal(16, new NearestAttackableTargetGoal(this, ParasaurolophusEntity.class, false, false)); + this.targetSelector.addGoal(17, new NearestAttackableTargetGoal(this, VelociraptorEntity.class, false, false)); + this.targetSelector.addGoal(18, new NearestAttackableTargetGoal(this, SpinosaurusEntity.class, false, false)); + this.targetSelector.addGoal(19, new NearestAttackableTargetGoal(this, IndominusRexEntity.class, false, false)); + this.targetSelector.addGoal(20, new NearestAttackableTargetGoal(this, Player.class, false, false)); + this.goalSelector.addGoal(21, new RandomLookAroundGoal(this)); + + + } + + public static AttributeSupplier.Builder createAttributes() { + return Animal.createLivingAttributes() + .add(Attributes.MAX_HEALTH, 80D) + .add(Attributes.MOVEMENT_SPEED, 0.3D) + .add(Attributes.ARMOR, 0D) + .add(Attributes.FOLLOW_RANGE, 32D) + .add(Attributes.KNOCKBACK_RESISTANCE, 0D) + .add(Attributes.ATTACK_KNOCKBACK, 0D) + .add(Attributes.ATTACK_DAMAGE, 20D); + } + + @Override + public boolean isFood(ItemStack pStack) { + return pStack.is(Items.BEEF); + } + + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel pLevel, AgeableMob pOtherParent) { + AgeableMob child = ModEntities.CARCHARODONTOSAURUS.get().create(pLevel); + if (child instanceof CarcharodontosaurusEntity baby) { + CarcharodontosaurusVariant randomVariant = Util.getRandom(CarcharodontosaurusVariant.values(), this.random); + baby.setVariant(randomVariant); + } + return child; + } + + @Override + public boolean doHurtTarget(Entity target) { + boolean hit = super.doHurtTarget(target); + if (!level().isClientSide && hit && target instanceof LivingEntity) { + if (this.level() instanceof ServerLevel serverLevel) { + this.triggerAnim("attackController", "attack"); + this.playSound(ModSounds.LARGE_THEROPOD_ATTACK.get(), 1.0F, 1.0F); + } + } + return hit; + } + + @Override + public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { + controllers.add(new AnimationController<>(this, "Walk/Run/Idle", state -> { + if (state.isMoving()) + return state.setAndContinue(CarcharodontosaurusEntity.this.isSprinting() ? RawAnimation.begin().then("anim.carcharodontosaurus.run", Animation.LoopType.LOOP) : RawAnimation.begin().then("anim.carcharodontosaurus.walk", Animation.LoopType.LOOP)); + + return state.setAndContinue(RawAnimation.begin().then("anim.carcharodontosaurus.idle", Animation.LoopType.LOOP)); + })); + + controllers.add(new AnimationController<>(this, "attackController", state -> PlayState.STOP) + .triggerableAnim("attack", RawAnimation.begin().then("anim.carcharodontosaurus.attack", Animation.LoopType.PLAY_ONCE))); + + controllers.add(new AnimationController<>(this, "mouthController", state -> PlayState.STOP) + .triggerableAnim("mouth", RawAnimation.begin().then("anim.carcharodontosaurus.mouth", Animation.LoopType.PLAY_ONCE))); + } + + private float getSignedTurnDelta() { + // Only consider the body (torso) rotation so head look does not affect tail sway + return Mth.wrapDegrees(this.yBodyRot - this.yBodyRotO); + } + + private int mouthAnimCooldown = 0; + + @Override + public void tick() { + super.tick(); + if (!level().isClientSide) { + this.entityData.set(DATA_SYNCED_AGE, this.getAge()); + var maxHealthAttr = getAttribute(Attributes.MAX_HEALTH); + if (maxHealthAttr != null) { + double baseAdult = DefaultAttributes.getSupplier((EntityType) this.getType()).getValue(Attributes.MAX_HEALTH); + double desired = this.isBaby() ? baseAdult * 0.10D : baseAdult; + if (maxHealthAttr.getBaseValue() != desired) { + double oldMax = maxHealthAttr.getBaseValue(); + double healthRatio = this.getHealth() / (float) oldMax; + maxHealthAttr.setBaseValue(desired); + this.setHealth((float) (desired * Mth.clamp(healthRatio, 0.0F, 1.0F))); + } + } + } + + if (!level().isClientSide) { + if (mouthAnimCooldown > 0) { + mouthAnimCooldown--; + } else { + this.triggerAnim("mouthController", "mouth"); + this.playSound(ModSounds.CARCHARODONTOSAURUS_CALL.get(), 1.0F, 1.0F); + // 30s–60s in ticks + mouthAnimCooldown = this.random.nextInt(1200 - 600 + 1) + 600; + } + } + + if (level().isClientSide) { + // Capture previous for smooth interpolation between ticks + this.tailSwayPrev = this.tailSwayOffset; + updateProceduralTailSway(); + } + } + + private void updateProceduralTailSway() { + // Turn input derived from rotation deltas; works even when standing still and turning + float turnDegrees = getSignedTurnDelta(); + + // Deadzone to ignore tiny jitter so the tail can return to center cleanly + float deadzoneDeg = 0.6f; // smaller deadzone for more responsiveness + float turnInput = 0.0f; + if (Math.abs(turnDegrees) >= deadzoneDeg) { + // Higher sensitivity so small in-place turns still affect the model + turnInput = Mth.clamp(turnDegrees / 15.0f, -1.0f, 1.0f); + } + + // Target offset: keep intuitive sign (positive input -> positive sway) + float target = turnInput; + + // One-pole low-pass (no bounce). Larger alpha => snappier and less "stiff". + float alpha = 0.24f; // try 0.20–0.30 to taste + + tailSwayOffset += (target - tailSwayOffset) * alpha; + + // Snap tiny residuals to zero so it visibly settles + if (Math.abs(tailSwayOffset) < 0.003f) { + tailSwayOffset = 0.0f; + } + + // No oscillation velocity retained + tailSwayVelocity = 0.0f; + + tailSwayOffset = Mth.clamp(tailSwayOffset, -1.5f, 1.5f); + } + + // Expose to the model for bone rotation + public float getTailSwayOffset() { + return tailSwayOffset; + } + + // Interpolated sway for smooth rendering between ticks + public float getTailSwayOffset(float partialTick) { + return Mth.lerp(Mth.clamp(partialTick, 0.0f, 1.0f), tailSwayPrev, tailSwayOffset); + } + + /*? if <=1.20.1 {*/ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); + this.entityData.define(VARIANT, 0); + this.entityData.define(DATA_SYNCED_AGE, 0); + } + /*?} else {*/ + /*@Override + protected void defineSynchedData(SynchedEntityData.Builder pBuilder) { + super.defineSynchedData(pBuilder); + pBuilder.define(VARIANT, 0); + pBuilder.define(DATA_SYNCED_AGE, 0); + } + *//*?}*/ + + public int getSyncedAge() { + return this.entityData.get(DATA_SYNCED_AGE); + } + public int getTypeVariant() { + return this.entityData.get(VARIANT); + } + + public CarcharodontosaurusVariant getVariant() { + return CarcharodontosaurusVariant.byId(this.getTypeVariant() & 255); + } + + private void setVariant(CarcharodontosaurusVariant variant) { + this.entityData.set(VARIANT, variant.getId() & 255); + } + + @Override + public boolean canMate(Animal other) { + if (!super.canMate(other)) return false; + if (!(other instanceof CarcharodontosaurusEntity that)) return false; + return this.getVariant() != that.getVariant(); + } + @Override + public void addAdditionalSaveData(CompoundTag pCompound) { + super.addAdditionalSaveData(pCompound); + pCompound.putInt("Variant", this.getTypeVariant()); + } + + @Override + public void readAdditionalSaveData(CompoundTag pCompound) { + super.readAdditionalSaveData(pCompound); + this.entityData.set(VARIANT, pCompound.getInt("Variant")); + } + + /*? if <=1.20.1 {*/ + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor pLevel, DifficultyInstance pDifficulty, MobSpawnType pReason, @Nullable SpawnGroupData pSpawnData, @Nullable CompoundTag pDataTag) { + CarcharodontosaurusVariant variant = Util.getRandom(CarcharodontosaurusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(pLevel, pDifficulty, pReason, pSpawnData, pDataTag); + } + /*?} else {*/ + /*@Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, MobSpawnType spawnType, @Nullable SpawnGroupData spawnGroupData) { + CarcharodontosaurusVariant variant = Util.getRandom(CarcharodontosaurusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(level, difficulty, spawnType, spawnGroupData); + } + *//*?}*/ + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } + + @Override + protected @Nullable SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.CARCHARODONTOSAURUS_HURT.get(); + } + + @Override + protected void playStepSound(BlockPos pos, BlockState blockIn) { + this.playSound(ModSounds.STOMP.get(), 0.5F, 1.0F); + } + + @Override + protected @Nullable SoundEvent getDeathSound() { + return ModSounds.CARCHARODONTOSAURUS_DEATH.get(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/custom/CarnotaurusEntity.java b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/CarnotaurusEntity.java new file mode 100644 index 0000000..7514fde --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/CarnotaurusEntity.java @@ -0,0 +1,318 @@ +package net.cmr.jurassicrevived.entity.custom; + +import net.cmr.jurassicrevived.entity.ModEntities; +import net.cmr.jurassicrevived.entity.ai.SprintingMeleeAttackGoal; +import net.cmr.jurassicrevived.entity.ai.SprintingPanicGoal; +import net.cmr.jurassicrevived.entity.client.CarnotaurusVariant; +import net.cmr.jurassicrevived.entity.client.CarnotaurusVariant; +import net.cmr.jurassicrevived.sound.ModSounds; +import net.minecraft.Util; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.util.Mth; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.*; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.attributes.DefaultAttributes; +import net.minecraft.world.entity.ai.goal.*; +import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal; +import net.minecraft.world.entity.ai.goal.target.NearestAttackableTargetGoal; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.monster.Monster; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerLevelAccessor; +import org.jetbrains.annotations.Nullable; +import software.bernie.geckolib.animatable.GeoEntity; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.core.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.core.animation.AnimatableManager; +import software.bernie.geckolib.core.animation.Animation; +import software.bernie.geckolib.core.animation.AnimationController; +import software.bernie.geckolib.core.animation.RawAnimation; +import software.bernie.geckolib.core.object.PlayState; +/*?} else {*/ +/*import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.animation.*; +*//*?}*/ + +public class CarnotaurusEntity extends Animal implements GeoEntity { + private AnimatableInstanceCache cache = new SingletonAnimatableInstanceCache(this); + + private static final EntityDataAccessor VARIANT = + SynchedEntityData.defineId(CarnotaurusEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SYNCED_AGE = + SynchedEntityData.defineId(CarnotaurusEntity.class, EntityDataSerializers.INT); + + // Procedural tail sway state (client-side use for rendering) + private float tailSwayOffset; // Smoothed offset in range roughly [-1, 1] + private float tailSwayVelocity; // Internal velocity for spring-damper + private float tailSwayPrev; // Previous frame value for interpolation + + public CarnotaurusEntity(EntityType pEntityType, Level pLevel) { + super(pEntityType, pLevel); + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new SprintingPanicGoal(this, 1.15) { + @Override + public boolean canUse() { + return CarnotaurusEntity.this.isBaby() && super.canUse(); + } + }); + this.targetSelector.addGoal(1, new HurtByTargetGoal(this).setAlertOthers()); + this.goalSelector.addGoal(2, new FloatGoal(this)); + this.goalSelector.addGoal(3, new AvoidEntityGoal<>(this, BrachiosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(4, new SprintingMeleeAttackGoal(this, 1.1, false)); + this.goalSelector.addGoal(5, new BreedGoal(this, 1.0)); + this.goalSelector.addGoal(6, new FollowParentGoal(this, 1.25)); + this.goalSelector.addGoal(7, new WaterAvoidingRandomStrollGoal(this, 1.0)); + this.goalSelector.addGoal(8, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(9, new FollowMobGoal(this, 1.2, (float) 20, (float) 10)); + this.targetSelector.addGoal(10, new NearestAttackableTargetGoal<>(this, Monster.class, true)); + this.targetSelector.addGoal(11, new NearestAttackableTargetGoal<>(this, Animal.class, 10, false, false, + target -> target.getType() != this.getType())); + this.targetSelector.addGoal(12, new NearestAttackableTargetGoal(this, TriceratopsEntity.class, false, false)); + this.targetSelector.addGoal(13, new NearestAttackableTargetGoal(this, GallimimusEntity.class, false, false)); + this.targetSelector.addGoal(14, new NearestAttackableTargetGoal(this, DilophosaurusEntity.class, false, false)); + this.targetSelector.addGoal(15, new NearestAttackableTargetGoal(this, CeratosaurusEntity.class, false, false)); + this.targetSelector.addGoal(16, new NearestAttackableTargetGoal(this, ParasaurolophusEntity.class, false, false)); + this.targetSelector.addGoal(17, new NearestAttackableTargetGoal(this, VelociraptorEntity.class, false, false)); + this.targetSelector.addGoal(18, new NearestAttackableTargetGoal(this, SpinosaurusEntity.class, false, false)); + this.targetSelector.addGoal(19, new NearestAttackableTargetGoal(this, IndominusRexEntity.class, false, false)); + this.targetSelector.addGoal(20, new NearestAttackableTargetGoal(this, Player.class, false, false)); + this.goalSelector.addGoal(21, new RandomLookAroundGoal(this)); + + + } + + public static AttributeSupplier.Builder createAttributes() { + return Animal.createLivingAttributes() + .add(Attributes.MAX_HEALTH, 55D) + .add(Attributes.MOVEMENT_SPEED, 0.3D) + .add(Attributes.ARMOR, 0D) + .add(Attributes.FOLLOW_RANGE, 32D) + .add(Attributes.KNOCKBACK_RESISTANCE, 0D) + .add(Attributes.ATTACK_KNOCKBACK, 0D) + .add(Attributes.ATTACK_DAMAGE, 16D); + } + + @Override + public boolean isFood(ItemStack pStack) { + return pStack.is(Items.BEEF); + } + + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel pLevel, AgeableMob pOtherParent) { + AgeableMob child = ModEntities.CARNOTAURUS.get().create(pLevel); + if (child instanceof CarnotaurusEntity baby) { + CarnotaurusVariant randomVariant = Util.getRandom(CarnotaurusVariant.values(), this.random); + baby.setVariant(randomVariant); + } + return child; + } + + @Override + public boolean doHurtTarget(Entity target) { + boolean hit = super.doHurtTarget(target); + if (!level().isClientSide && hit && target instanceof LivingEntity) { + if (this.level() instanceof ServerLevel serverLevel) { + this.triggerAnim("attackController", "attack"); + this.playSound(ModSounds.CARNOTAURUS_ATTACK.get(), 1.0F, 1.0F); + } + } + return hit; + } + + @Override + public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { + controllers.add(new AnimationController<>(this, "Walk/Run/Idle", state -> { + if (state.isMoving()) + return state.setAndContinue(CarnotaurusEntity.this.isSprinting() ? RawAnimation.begin().then("anim.carnotaurus.run", Animation.LoopType.LOOP) : RawAnimation.begin().then("anim.carnotaurus.walk", Animation.LoopType.LOOP)); + + return state.setAndContinue(RawAnimation.begin().then("anim.carnotaurus.idle", Animation.LoopType.LOOP)); + })); + + controllers.add(new AnimationController<>(this, "attackController", state -> PlayState.STOP) + .triggerableAnim("attack", RawAnimation.begin().then("anim.carnotaurus.attack", Animation.LoopType.PLAY_ONCE))); + + controllers.add(new AnimationController<>(this, "mouthController", state -> PlayState.STOP) + .triggerableAnim("mouth", RawAnimation.begin().then("anim.carnotaurus.mouth", Animation.LoopType.PLAY_ONCE))); + } + + private float getSignedTurnDelta() { + // Only consider the body (torso) rotation so head look does not affect tail sway + return Mth.wrapDegrees(this.yBodyRot - this.yBodyRotO); + } + + private int mouthAnimCooldown = 0; + + @Override + public void tick() { + super.tick(); + if (!level().isClientSide) { + this.entityData.set(DATA_SYNCED_AGE, this.getAge()); + var maxHealthAttr = getAttribute(Attributes.MAX_HEALTH); + if (maxHealthAttr != null) { + double baseAdult = DefaultAttributes.getSupplier((EntityType) this.getType()).getValue(Attributes.MAX_HEALTH); + double desired = this.isBaby() ? baseAdult * 0.10D : baseAdult; + if (maxHealthAttr.getBaseValue() != desired) { + double oldMax = maxHealthAttr.getBaseValue(); + double healthRatio = this.getHealth() / (float) oldMax; + maxHealthAttr.setBaseValue(desired); + this.setHealth((float) (desired * Mth.clamp(healthRatio, 0.0F, 1.0F))); + } + } + } + + if (!level().isClientSide) { + if (mouthAnimCooldown > 0) { + mouthAnimCooldown--; + } else { + this.triggerAnim("mouthController", "mouth"); + this.playSound(ModSounds.CARNOTAURUS_CALL.get(), 1.0F, 1.0F); + // 30s–60s in ticks + mouthAnimCooldown = this.random.nextInt(1200 - 600 + 1) + 600; + } + } + + if (level().isClientSide) { + // Capture previous for smooth interpolation between ticks + this.tailSwayPrev = this.tailSwayOffset; + updateProceduralTailSway(); + } + } + + private void updateProceduralTailSway() { + // Turn input derived from rotation deltas; works even when standing still and turning + float turnDegrees = getSignedTurnDelta(); + + // Deadzone to ignore tiny jitter so the tail can return to center cleanly + float deadzoneDeg = 0.6f; // smaller deadzone for more responsiveness + float turnInput = 0.0f; + if (Math.abs(turnDegrees) >= deadzoneDeg) { + // Higher sensitivity so small in-place turns still affect the model + turnInput = Mth.clamp(turnDegrees / 15.0f, -1.0f, 1.0f); + } + + // Target offset: keep intuitive sign (positive input -> positive sway) + float target = turnInput; + + // One-pole low-pass (no bounce). Larger alpha => snappier and less "stiff". + float alpha = 0.24f; // try 0.20–0.30 to taste + + tailSwayOffset += (target - tailSwayOffset) * alpha; + + // Snap tiny residuals to zero so it visibly settles + if (Math.abs(tailSwayOffset) < 0.003f) { + tailSwayOffset = 0.0f; + } + + // No oscillation velocity retained + tailSwayVelocity = 0.0f; + + tailSwayOffset = Mth.clamp(tailSwayOffset, -1.5f, 1.5f); + } + + // Expose to the model for bone rotation + public float getTailSwayOffset() { + return tailSwayOffset; + } + + // Interpolated sway for smooth rendering between ticks + public float getTailSwayOffset(float partialTick) { + return Mth.lerp(Mth.clamp(partialTick, 0.0f, 1.0f), tailSwayPrev, tailSwayOffset); + } + + /*? if <=1.20.1 {*/ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); + this.entityData.define(VARIANT, 0); + this.entityData.define(DATA_SYNCED_AGE, 0); + } + /*?} else {*/ + /*@Override + protected void defineSynchedData(SynchedEntityData.Builder pBuilder) { + super.defineSynchedData(pBuilder); + pBuilder.define(VARIANT, 0); + pBuilder.define(DATA_SYNCED_AGE, 0); + } + *//*?}*/ + + public int getSyncedAge() { + return this.entityData.get(DATA_SYNCED_AGE); + } + public int getTypeVariant() { + return this.entityData.get(VARIANT); + } + + public CarnotaurusVariant getVariant() { + return CarnotaurusVariant.byId(this.getTypeVariant() & 255); + } + + private void setVariant(CarnotaurusVariant variant) { + this.entityData.set(VARIANT, variant.getId() & 255); + } + + @Override + public boolean canMate(Animal other) { + if (!super.canMate(other)) return false; + if (!(other instanceof CarnotaurusEntity that)) return false; + return this.getVariant() != that.getVariant(); + } + @Override + public void addAdditionalSaveData(CompoundTag pCompound) { + super.addAdditionalSaveData(pCompound); + pCompound.putInt("Variant", this.getTypeVariant()); + } + + @Override + public void readAdditionalSaveData(CompoundTag pCompound) { + super.readAdditionalSaveData(pCompound); + this.entityData.set(VARIANT, pCompound.getInt("Variant")); + } + + /*? if <=1.20.1 {*/ + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor pLevel, DifficultyInstance pDifficulty, MobSpawnType pReason, @Nullable SpawnGroupData pSpawnData, @Nullable CompoundTag pDataTag) { + CarnotaurusVariant variant = Util.getRandom(CarnotaurusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(pLevel, pDifficulty, pReason, pSpawnData, pDataTag); + } + /*?} else {*/ + /*@Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, MobSpawnType spawnType, @Nullable SpawnGroupData spawnGroupData) { + CarnotaurusVariant variant = Util.getRandom(CarnotaurusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(level, difficulty, spawnType, spawnGroupData); + } + *//*?}*/ + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } + + @Override + protected @Nullable SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.CARNOTAURUS_HURT.get(); + } + + @Override + protected @Nullable SoundEvent getDeathSound() { + return ModSounds.CARNOTAURUS_DEATH.get(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/custom/CearadactylusEntity.java b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/CearadactylusEntity.java new file mode 100644 index 0000000..08b34e2 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/CearadactylusEntity.java @@ -0,0 +1,410 @@ +package net.cmr.jurassicrevived.entity.custom; + +import net.cmr.jurassicrevived.entity.ModEntities; +import net.cmr.jurassicrevived.entity.ai.SprintingMeleeAttackGoal; +import net.cmr.jurassicrevived.entity.ai.SprintingPanicGoal; +import net.cmr.jurassicrevived.entity.client.CearadactylusVariant; +import net.cmr.jurassicrevived.entity.client.CearadactylusVariant; +import net.cmr.jurassicrevived.sound.ModSounds; +import net.minecraft.Util; +import net.minecraft.core.BlockPos; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.*; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.attributes.DefaultAttributes; +import net.minecraft.world.entity.ai.control.FlyingMoveControl; +import net.minecraft.world.entity.ai.goal.*; +import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal; +import net.minecraft.world.entity.ai.goal.target.NearestAttackableTargetGoal; +import net.minecraft.world.entity.ai.navigation.FlyingPathNavigation; +import net.minecraft.world.entity.ai.navigation.PathNavigation; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.animal.FlyingAnimal; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerLevelAccessor; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.phys.Vec3; +import org.jetbrains.annotations.Nullable; +import software.bernie.geckolib.animatable.GeoEntity; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.core.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.core.animation.AnimatableManager; +import software.bernie.geckolib.core.animation.Animation; +import software.bernie.geckolib.core.animation.AnimationController; +import software.bernie.geckolib.core.animation.RawAnimation; +import software.bernie.geckolib.core.object.PlayState; +/*?} else {*/ +/*import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.animation.*; +*//*?}*/ + +public class CearadactylusEntity extends Animal implements GeoEntity, FlyingAnimal { + private final AnimatableInstanceCache cache = new SingletonAnimatableInstanceCache(this); + + private static final EntityDataAccessor VARIANT = + SynchedEntityData.defineId(CearadactylusEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SYNCED_AGE = + SynchedEntityData.defineId(CearadactylusEntity.class, EntityDataSerializers.INT); + + // Procedural tail sway state (client-side use for rendering) + private float tailSwayOffset; // Smoothed offset in range roughly [-1, 1] + private float tailSwayVelocity; // Internal velocity for spring-damper + private float tailSwayPrev; // Previous frame value for interpolation + + public CearadactylusEntity(EntityType pEntityType, Level pLevel) { + super(pEntityType, pLevel); + this.moveControl = new FlyingMoveControl(this, 20, true); + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new SprintingPanicGoal(this, 1.15) { + @Override + public boolean canUse() { + return CearadactylusEntity.this.isBaby() && super.canUse(); + } + }); + this.goalSelector.addGoal(2, new FloatGoal(this)); + this.goalSelector.addGoal(3, new AvoidEntityGoal<>(this, BrachiosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(4, new SprintingMeleeAttackGoal(this, 1.1, false)); + this.goalSelector.addGoal(5, new BreedGoal(this, 1.0)); + this.goalSelector.addGoal(6, new FollowParentGoal(this, 1.25)); + + // Goal 7: Wander on ground (Walk) - Only when on ground + this.goalSelector.addGoal(7, new WaterAvoidingRandomStrollGoal(this, 1.0) { + @Override + public boolean canUse() { + return CearadactylusEntity.this.onGround() && super.canUse(); + } + }); + + // Goal 8: Wander in air (Fly) - Handles takeoff, flying, and landing + this.goalSelector.addGoal(8, new WaterAvoidingRandomStrollGoal(this, 1.0, 20) { + @Override + public boolean canUse() { + boolean isFlying = !CearadactylusEntity.this.onGround(); + // If flying, keep flying. If on ground, small chance (1/400 ticks) to take off. + return (isFlying || CearadactylusEntity.this.getRandom().nextInt(400) == 0) && super.canUse(); + } + + @Override + protected Vec3 getPosition() { + Vec3 pos = CearadactylusEntity.this.position(); + RandomSource random = CearadactylusEntity.this.getRandom(); + + double x = pos.x + (random.nextFloat() * 2 - 1) * 32; + double z = pos.z + (random.nextFloat() * 2 - 1) * 32; + + // Get ground height at the random destination (returns Y of first air block) + int groundY = CearadactylusEntity.this.level().getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, (int)x, (int)z); + double y; + + if (CearadactylusEntity.this.onGround()) { + // Takeoff: Target well above ground to ensure liftoff + y = pos.y + 15 + random.nextInt(10); + } else { + // Flying: 5% chance to land, otherwise stay airborne but capped height + if (random.nextFloat() < 0.05f) { + y = groundY; // Land + } else if (pos.y > groundY + 20) { + // Too high: Force descent + y = pos.y - 5 - random.nextInt(10); + } else { + // Just wander up or down a bit + y = pos.y + (random.nextFloat() * 2 - 1) * 10; + } + } + + // Don't target below the ground (blocks) + if (y < groundY) y = groundY; + + return new Vec3(x, y, z); + } + }); + + this.goalSelector.addGoal(9, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(10, new FollowMobGoal(this, 0.8, (float) 20, (float) 10)); + this.targetSelector.addGoal(11, new NearestAttackableTargetGoal<>(this, Animal.class, 10, false, false, + target -> { + // 1. Don't eat your own species + if (target.getType() == this.getType()) return false; + + // 2. Don't eat Flying Animals + if (target instanceof FlyingAnimal) return false; + + // 3. SIZE CHECK: specific height and width limits + // Example: Height < 1.0 blocks AND Width < 1.0 blocks + boolean isSmallEnough = target.getBbHeight() <= 1.0F && target.getBbWidth() <= 1.0F; + + return isSmallEnough; + } + )); this.targetSelector.addGoal(12, new NearestAttackableTargetGoal<>(this, Player.class, false, false)); + this.targetSelector.addGoal(13, new HurtByTargetGoal(this)); + this.goalSelector.addGoal(14, new RandomLookAroundGoal(this)); + this.goalSelector.addGoal(15, new FloatGoal(this)); + } + + public static AttributeSupplier.Builder createAttributes() { + return Animal.createLivingAttributes() + .add(Attributes.MAX_HEALTH, 50D) + .add(Attributes.MOVEMENT_SPEED, 0.3D) + .add(Attributes.FLYING_SPEED, 0.6D) + .add(Attributes.ARMOR, 0D) + .add(Attributes.FOLLOW_RANGE, 32D) + .add(Attributes.KNOCKBACK_RESISTANCE, 0D) + .add(Attributes.ATTACK_KNOCKBACK, 0D) + .add(Attributes.ATTACK_DAMAGE, 8D); + } + + @Override + protected PathNavigation createNavigation(Level pLevel) { + FlyingPathNavigation navigation = new FlyingPathNavigation(this, pLevel); + navigation.setCanOpenDoors(false); + navigation.setCanFloat(true); + navigation.setCanPassDoors(true); + return navigation; + } + + @Override + public boolean isFlying() { + return !this.onGround(); + } + + @Override + public boolean causeFallDamage(float pFallDistance, float pMultiplier, DamageSource pSource) { + return false; + } + + @Override + protected void checkFallDamage(double pY, boolean pOnGround, BlockState pState, BlockPos pPos) { + } + + @Override + public boolean isFood(ItemStack pStack) { + return pStack.is(Items.BEEF); + } + + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel pLevel, AgeableMob pOtherParent) { + AgeableMob child = ModEntities.CEARADACTYLUS.get().create(pLevel); + if (child instanceof CearadactylusEntity baby) { + CearadactylusVariant randomVariant = Util.getRandom(CearadactylusVariant.values(), this.random); + baby.setVariant(randomVariant); + } + return child; + } + + @Override + public boolean doHurtTarget(Entity target) { + boolean hit = super.doHurtTarget(target); + if (!level().isClientSide && hit && target instanceof LivingEntity) { + if (this.level() instanceof ServerLevel serverLevel) { + this.triggerAnim("attackController", "attack"); + this.playSound(ModSounds.BEAK_ATTACK.get(), 1.0F, 1.0F); + } + } + return hit; + } + + @Override + public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { + controllers.add(new AnimationController<>(this, "Walk/Run/Idle", state -> { + if (!CearadactylusEntity.this.onGround()) { + return state.setAndContinue(RawAnimation.begin().then("anim.cearadactylus.fly", Animation.LoopType.LOOP)); + } + + if (state.isMoving()) + return state.setAndContinue(CearadactylusEntity.this.isSprinting() ? RawAnimation.begin().then("anim.cearadactylus.walk", Animation.LoopType.LOOP) : RawAnimation.begin().then("anim.cearadactylus.walk", Animation.LoopType.LOOP)); + + return state.setAndContinue(RawAnimation.begin().then("anim.cearadactylus.idle", Animation.LoopType.LOOP)); + })); + + controllers.add(new AnimationController<>(this, "attackController", state -> PlayState.STOP) + .triggerableAnim("attack", RawAnimation.begin().then("anim.cearadactylus.attack", Animation.LoopType.PLAY_ONCE))); + + controllers.add(new AnimationController<>(this, "mouthController", state -> PlayState.STOP) + .triggerableAnim("mouth", RawAnimation.begin().then("anim.cearadactylus.mouth", Animation.LoopType.PLAY_ONCE))); + } + + private float getSignedTurnDelta() { + // Only consider the body (torso) rotation so head look does not affect tail sway + return Mth.wrapDegrees(this.yBodyRot - this.yBodyRotO); + } + + private int mouthAnimCooldown = 0; + + @Override + public void tick() { + super.tick(); + if (!level().isClientSide) { + this.entityData.set(DATA_SYNCED_AGE, this.getAge()); + var maxHealthAttr = getAttribute(Attributes.MAX_HEALTH); + if (maxHealthAttr != null) { + double baseAdult = DefaultAttributes.getSupplier((EntityType) this.getType()).getValue(Attributes.MAX_HEALTH); + double desired = this.isBaby() ? baseAdult * 0.10D : baseAdult; + if (maxHealthAttr.getBaseValue() != desired) { + double oldMax = maxHealthAttr.getBaseValue(); + double healthRatio = this.getHealth() / (float) oldMax; + maxHealthAttr.setBaseValue(desired); + this.setHealth((float) (desired * Mth.clamp(healthRatio, 0.0F, 1.0F))); + } + } + } + + if (!level().isClientSide) { + if (mouthAnimCooldown > 0) { + mouthAnimCooldown--; + } else { + this.triggerAnim("mouthController", "mouth"); + this.playSound(ModSounds.CEARADACTYLUS_CALL.get(), 1.0F, 1.0F); + // 30s–60s in ticks + mouthAnimCooldown = this.random.nextInt(1200 - 600 + 1) + 600; + } + } + + if (level().isClientSide) { + // Capture previous for smooth interpolation between ticks + this.tailSwayPrev = this.tailSwayOffset; + updateProceduralTailSway(); + } + } + + private void updateProceduralTailSway() { + // Turn input derived from rotation deltas; works even when standing still and turning + float turnDegrees = getSignedTurnDelta(); + + // Deadzone to ignore tiny jitter so the tail can return to center cleanly + float deadzoneDeg = 0.6f; // smaller deadzone for more responsiveness + float turnInput = 0.0f; + if (Math.abs(turnDegrees) >= deadzoneDeg) { + // Higher sensitivity so small in-place turns still affect the model + turnInput = Mth.clamp(turnDegrees / 15.0f, -1.0f, 1.0f); + } + + // Target offset: keep intuitive sign (positive input -> positive sway) + float target = turnInput; + + // One-pole low-pass (no bounce). Larger alpha => snappier and less "stiff". + float alpha = 0.24f; // try 0.20–0.30 to taste + + tailSwayOffset += (target - tailSwayOffset) * alpha; + + // Snap tiny residuals to zero so it visibly settles + if (Math.abs(tailSwayOffset) < 0.003f) { + tailSwayOffset = 0.0f; + } + + // No oscillation velocity retained + tailSwayVelocity = 0.0f; + + tailSwayOffset = Mth.clamp(tailSwayOffset, -1.5f, 1.5f); + } + + // Expose to the model for bone rotation + public float getTailSwayOffset() { + return tailSwayOffset; + } + + // Interpolated sway for smooth rendering between ticks + public float getTailSwayOffset(float partialTick) { + return Mth.lerp(Mth.clamp(partialTick, 0.0f, 1.0f), tailSwayPrev, tailSwayOffset); + } + + /*? if <=1.20.1 {*/ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); + this.entityData.define(VARIANT, 0); + this.entityData.define(DATA_SYNCED_AGE, 0); + } + /*?} else {*/ + /*@Override + protected void defineSynchedData(SynchedEntityData.Builder pBuilder) { + super.defineSynchedData(pBuilder); + pBuilder.define(VARIANT, 0); + pBuilder.define(DATA_SYNCED_AGE, 0); + } + *//*?}*/ + + public int getSyncedAge() { + return this.entityData.get(DATA_SYNCED_AGE); + } + + public CearadactylusVariant getVariant() { + return CearadactylusVariant.byId(this.getTypeVariant() & 255); + } + public int getTypeVariant() { + return this.entityData.get(VARIANT); + } + + private void setVariant(CearadactylusVariant variant) { + this.entityData.set(VARIANT, variant.getId() & 255); + } + + @Override + public boolean canMate(Animal other) { + if (!super.canMate(other)) return false; + if (!(other instanceof CearadactylusEntity that)) return false; + return this.getVariant() != that.getVariant(); + } + + /*? if <=1.20.1 {*/ + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor pLevel, DifficultyInstance pDifficulty, MobSpawnType pReason, @Nullable SpawnGroupData pSpawnData, @Nullable CompoundTag pDataTag) { + CearadactylusVariant variant = Util.getRandom(CearadactylusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(pLevel, pDifficulty, pReason, pSpawnData, pDataTag); + } + /*?} else {*/ + /*@Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, MobSpawnType spawnType, @Nullable SpawnGroupData spawnGroupData) { + CearadactylusVariant variant = Util.getRandom(CearadactylusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(level, difficulty, spawnType, spawnGroupData); + } + *//*?}*/ + + @Override + public void readAdditionalSaveData(CompoundTag pCompound) { + super.readAdditionalSaveData(pCompound); + this.entityData.set(VARIANT, pCompound.getInt("Variant")); + } + + @Override + public void addAdditionalSaveData(CompoundTag pCompound) { + super.addAdditionalSaveData(pCompound); + pCompound.putInt("Variant", this.getTypeVariant()); + } + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } + + @Override + protected @Nullable SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.CEARADACTYLUS_HURT.get(); + } + + @Override + protected @Nullable SoundEvent getDeathSound() { + return ModSounds.CEARADACTYLUS_DEATH.get(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/custom/CeratosaurusEntity.java b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/CeratosaurusEntity.java new file mode 100644 index 0000000..22fbae2 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/CeratosaurusEntity.java @@ -0,0 +1,315 @@ +package net.cmr.jurassicrevived.entity.custom; + +import net.cmr.jurassicrevived.entity.ModEntities; +import net.cmr.jurassicrevived.entity.ai.SprintingMeleeAttackGoal; +import net.cmr.jurassicrevived.entity.ai.SprintingPanicGoal; +import net.cmr.jurassicrevived.entity.client.CeratosaurusVariant; +import net.cmr.jurassicrevived.entity.client.CeratosaurusVariant; +import net.cmr.jurassicrevived.sound.ModSounds; +import net.minecraft.Util; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.util.Mth; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.*; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.attributes.DefaultAttributes; +import net.minecraft.world.entity.ai.goal.*; +import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal; +import net.minecraft.world.entity.ai.goal.target.NearestAttackableTargetGoal; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerLevelAccessor; +import org.jetbrains.annotations.Nullable; +import software.bernie.geckolib.animatable.GeoEntity; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.core.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.core.animation.AnimatableManager; +import software.bernie.geckolib.core.animation.Animation; +import software.bernie.geckolib.core.animation.AnimationController; +import software.bernie.geckolib.core.animation.RawAnimation; +import software.bernie.geckolib.core.object.PlayState; +/*?} else {*/ +/*import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.animation.*; +*//*?}*/ + +public class CeratosaurusEntity extends Animal implements GeoEntity { + private AnimatableInstanceCache cache = new SingletonAnimatableInstanceCache(this); + + private static final EntityDataAccessor VARIANT = + SynchedEntityData.defineId(CeratosaurusEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SYNCED_AGE = + SynchedEntityData.defineId(CeratosaurusEntity.class, EntityDataSerializers.INT); + + // Procedural tail sway state (client-side use for rendering) + private float tailSwayOffset; // Smoothed offset in range roughly [-1, 1] + private float tailSwayVelocity; // Internal velocity for spring-damper + private float tailSwayPrev; // Previous frame value for interpolation + + public CeratosaurusEntity(EntityType pEntityType, Level pLevel) { + super(pEntityType, pLevel); + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new SprintingPanicGoal(this, 1.15) { + @Override + public boolean canUse() { + return CeratosaurusEntity.this.isBaby() && super.canUse(); + } + }); + this.targetSelector.addGoal(1, new HurtByTargetGoal(this).setAlertOthers()); + this.goalSelector.addGoal(2, new FloatGoal(this)); + this.goalSelector.addGoal(3, new AvoidEntityGoal<>(this, TriceratopsEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(4, new AvoidEntityGoal<>(this, SpinosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(5, new AvoidEntityGoal<>(this, TyrannosaurusRexEntity.class, (float) 20, 1.2, 1.2)); + this.goalSelector.addGoal(6, new AvoidEntityGoal<>(this, BrachiosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(7, new SprintingMeleeAttackGoal(this, 1.1, false)); + this.goalSelector.addGoal(8, new BreedGoal(this, 1.0)); + this.goalSelector.addGoal(9, new FollowParentGoal(this, 1.25)); + this.goalSelector.addGoal(10, new WaterAvoidingRandomStrollGoal(this, 1.0)); + this.goalSelector.addGoal(11, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(12, new FollowMobGoal(this, 1, (float) 20, (float) 10)); + this.targetSelector.addGoal(13, new NearestAttackableTargetGoal<>(this, Animal.class, 10, false, false, + target -> target.getType() != this.getType())); + this.targetSelector.addGoal(14, new NearestAttackableTargetGoal(this, GallimimusEntity.class, false, false)); + this.targetSelector.addGoal(15, new NearestAttackableTargetGoal(this, DilophosaurusEntity.class, false, false)); + this.targetSelector.addGoal(16, new NearestAttackableTargetGoal(this, VelociraptorEntity.class, false, false)); + this.targetSelector.addGoal(17, new NearestAttackableTargetGoal(this, ParasaurolophusEntity.class, false, false)); + this.targetSelector.addGoal(18, new NearestAttackableTargetGoal(this, Player.class, false, false)); + this.targetSelector.addGoal(19, new NearestAttackableTargetGoal(this, CompsognathusEntity.class, false, false)); + this.goalSelector.addGoal(20, new RandomLookAroundGoal(this)); + } + + public static AttributeSupplier.Builder createAttributes() { + return Animal.createLivingAttributes() + .add(Attributes.MAX_HEALTH, 55D) + .add(Attributes.MOVEMENT_SPEED, 0.3D) + .add(Attributes.ARMOR, 0D) + .add(Attributes.FOLLOW_RANGE, 32D) + .add(Attributes.KNOCKBACK_RESISTANCE, 0.3D) + .add(Attributes.ATTACK_KNOCKBACK, 0D) + .add(Attributes.ATTACK_DAMAGE, 16D ); + } + + @Override + public boolean isFood(ItemStack pStack) { + return pStack.is(Items.BEEF); + } + + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel pLevel, AgeableMob pOtherParent) { + AgeableMob child = ModEntities.CERATOSAURUS.get().create(pLevel); + if (child instanceof CeratosaurusEntity baby) { + CeratosaurusVariant randomVariant = Util.getRandom(CeratosaurusVariant.values(), this.random); + baby.setVariant(randomVariant); + } + return child; + } + + @Override + public boolean doHurtTarget(Entity target) { + boolean hit = super.doHurtTarget(target); + if (!level().isClientSide && hit && target instanceof LivingEntity) { + if (this.level() instanceof ServerLevel serverLevel) { + this.triggerAnim("attackController", "attack"); + this.playSound(ModSounds.MEDIUM_THEROPOD_ATTACK.get(), 1.0F, 1.0F); + } + } + return hit; + } + + @Override + public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { + controllers.add(new AnimationController<>(this, "Walk/Run/Idle", state -> { + if (state.isMoving()) + return state.setAndContinue(CeratosaurusEntity.this.isSprinting() ? RawAnimation.begin().then("anim.ceratosaurus.run", Animation.LoopType.LOOP) : RawAnimation.begin().then("anim.ceratosaurus.walk", Animation.LoopType.LOOP)); + + return state.setAndContinue(RawAnimation.begin().then("anim.ceratosaurus.idle", Animation.LoopType.LOOP)); + })); + + controllers.add(new AnimationController<>(this, "attackController", state -> PlayState.STOP) + .triggerableAnim("attack", RawAnimation.begin().then("anim.ceratosaurus.attack", Animation.LoopType.PLAY_ONCE))); + + controllers.add(new AnimationController<>(this, "mouthController", state -> PlayState.STOP) + .triggerableAnim("mouth", RawAnimation.begin().then("anim.ceratosaurus.mouth", Animation.LoopType.PLAY_ONCE))); + } + + private float getSignedTurnDelta() { + // Only consider the body (torso) rotation so head look does not affect tail sway + return Mth.wrapDegrees(this.yBodyRot - this.yBodyRotO); + } + + private int mouthAnimCooldown = 0; + + @Override + public void tick() { + super.tick(); + if (!level().isClientSide) { + this.entityData.set(DATA_SYNCED_AGE, this.getAge()); + var maxHealthAttr = getAttribute(Attributes.MAX_HEALTH); + if (maxHealthAttr != null) { + double baseAdult = DefaultAttributes.getSupplier((EntityType) this.getType()).getValue(Attributes.MAX_HEALTH); + double desired = this.isBaby() ? baseAdult * 0.10D : baseAdult; + if (maxHealthAttr.getBaseValue() != desired) { + double oldMax = maxHealthAttr.getBaseValue(); + double healthRatio = this.getHealth() / (float) oldMax; + maxHealthAttr.setBaseValue(desired); + this.setHealth((float) (desired * Mth.clamp(healthRatio, 0.0F, 1.0F))); + } + } + } + + if (!level().isClientSide) { + if (mouthAnimCooldown > 0) { + mouthAnimCooldown--; + } else { + this.triggerAnim("mouthController", "mouth"); + this.playSound(ModSounds.CERATOSAURUS_CALL.get(), 1.0F, 1.0F); + // 30s–60s in ticks + mouthAnimCooldown = this.random.nextInt(1200 - 600 + 1) + 600; + } + } + + if (level().isClientSide) { + // Capture previous for smooth interpolation between ticks + this.tailSwayPrev = this.tailSwayOffset; + updateProceduralTailSway(); + } + } + + private void updateProceduralTailSway() { + // Turn input derived from rotation deltas; works even when standing still and turning + float turnDegrees = getSignedTurnDelta(); + + // Deadzone to ignore tiny jitter so the tail can return to center cleanly + float deadzoneDeg = 0.6f; // smaller deadzone for more responsiveness + float turnInput = 0.0f; + if (Math.abs(turnDegrees) >= deadzoneDeg) { + // Higher sensitivity so small in-place turns still affect the model + turnInput = Mth.clamp(turnDegrees / 15.0f, -1.0f, 1.0f); + } + + // Target offset: keep intuitive sign (positive input -> positive sway) + float target = turnInput; + + // One-pole low-pass (no bounce). Larger alpha => snappier and less "stiff". + float alpha = 0.24f; // try 0.20–0.30 to taste + + tailSwayOffset += (target - tailSwayOffset) * alpha; + + // Snap tiny residuals to zero so it visibly settles + if (Math.abs(tailSwayOffset) < 0.003f) { + tailSwayOffset = 0.0f; + } + + // No oscillation velocity retained + tailSwayVelocity = 0.0f; + + tailSwayOffset = Mth.clamp(tailSwayOffset, -1.5f, 1.5f); + } + + // Expose to the model for bone rotation + public float getTailSwayOffset() { + return tailSwayOffset; + } + + // Interpolated sway for smooth rendering between ticks + public float getTailSwayOffset(float partialTick) { + return Mth.lerp(Mth.clamp(partialTick, 0.0f, 1.0f), tailSwayPrev, tailSwayOffset); + } + + /*? if <=1.20.1 {*/ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); + this.entityData.define(VARIANT, 0); + this.entityData.define(DATA_SYNCED_AGE, 0); + } + /*?} else {*/ + /*@Override + protected void defineSynchedData(SynchedEntityData.Builder pBuilder) { + super.defineSynchedData(pBuilder); + pBuilder.define(VARIANT, 0); + pBuilder.define(DATA_SYNCED_AGE, 0); + } + *//*?}*/ + + public int getSyncedAge() { + return this.entityData.get(DATA_SYNCED_AGE); + } + + public int getTypeVariant() { + return this.entityData.get(VARIANT); + } + + public CeratosaurusVariant getVariant() { + return CeratosaurusVariant.byId(this.getTypeVariant() & 255); + } + + private void setVariant(CeratosaurusVariant variant) { + this.entityData.set(VARIANT, variant.getId() & 255); + } + + @Override + public boolean canMate(Animal other) { + if (!super.canMate(other)) return false; + if (!(other instanceof CeratosaurusEntity that)) return false; + return this.getVariant() != that.getVariant(); + } + @Override + public void addAdditionalSaveData(CompoundTag pCompound) { + super.addAdditionalSaveData(pCompound); + pCompound.putInt("Variant", this.getTypeVariant()); + } + + @Override + public void readAdditionalSaveData(CompoundTag pCompound) { + super.readAdditionalSaveData(pCompound); + this.entityData.set(VARIANT, pCompound.getInt("Variant")); + } + + /*? if <=1.20.1 {*/ + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor pLevel, DifficultyInstance pDifficulty, MobSpawnType pReason, @Nullable SpawnGroupData pSpawnData, @Nullable CompoundTag pDataTag) { + CeratosaurusVariant variant = Util.getRandom(CeratosaurusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(pLevel, pDifficulty, pReason, pSpawnData, pDataTag); + } + /*?} else {*/ + /*@Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, MobSpawnType spawnType, @Nullable SpawnGroupData spawnGroupData) { + CeratosaurusVariant variant = Util.getRandom(CeratosaurusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(level, difficulty, spawnType, spawnGroupData); + } + *//*?}*/ + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } + + @Override + protected @Nullable SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.CERATOSAURUS_HURT.get(); + } + + @Override + protected @Nullable SoundEvent getDeathSound() { + return ModSounds.CERATOSAURUS_DEATH.get(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/custom/ChasmosaurusEntity.java b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/ChasmosaurusEntity.java new file mode 100644 index 0000000..61172fb --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/ChasmosaurusEntity.java @@ -0,0 +1,307 @@ +package net.cmr.jurassicrevived.entity.custom; + +import net.cmr.jurassicrevived.entity.ModEntities; +import net.cmr.jurassicrevived.entity.ai.SprintingMeleeAttackGoal; +import net.cmr.jurassicrevived.entity.ai.SprintingPanicGoal; +import net.cmr.jurassicrevived.entity.client.ChasmosaurusVariant; +import net.cmr.jurassicrevived.entity.client.ChasmosaurusVariant; +import net.cmr.jurassicrevived.sound.ModSounds; +import net.minecraft.Util; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.util.Mth; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.*; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.attributes.DefaultAttributes; +import net.minecraft.world.entity.ai.goal.*; +import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerLevelAccessor; +import org.jetbrains.annotations.Nullable; +import software.bernie.geckolib.animatable.GeoEntity; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.core.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.core.animation.AnimatableManager; +import software.bernie.geckolib.core.animation.Animation; +import software.bernie.geckolib.core.animation.AnimationController; +import software.bernie.geckolib.core.animation.RawAnimation; +import software.bernie.geckolib.core.object.PlayState; +/*?} else {*/ +/*import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.animation.*; +*//*?}*/ + +public class ChasmosaurusEntity extends Animal implements GeoEntity { + private AnimatableInstanceCache cache = new SingletonAnimatableInstanceCache(this); + + private static final EntityDataAccessor VARIANT = + SynchedEntityData.defineId(ChasmosaurusEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SYNCED_AGE = + SynchedEntityData.defineId(ChasmosaurusEntity.class, EntityDataSerializers.INT); + + // Procedural tail sway state (client-side use for rendering) + private float tailSwayOffset; // Smoothed offset in range roughly [-1, 1] + private float tailSwayVelocity; // Internal velocity for spring-damper + private float tailSwayPrev; // Previous frame value for interpolation + + public ChasmosaurusEntity(EntityType pEntityType, Level pLevel) { + super(pEntityType, pLevel); + } + + @Override + protected void registerGoals() {this.goalSelector.addGoal(1, new SprintingPanicGoal(this, 1.15) { + @Override + public boolean canUse() { + return ChasmosaurusEntity.this.isBaby() && super.canUse(); + } + }); + this.targetSelector.addGoal(1, new HurtByTargetGoal(this).setAlertOthers()); + this.goalSelector.addGoal(2, new FloatGoal(this)); + this.goalSelector.addGoal(3, new AvoidEntityGoal<>(this, VelociraptorEntity.class, (float) 20, 0.8, 0.8)); + this.goalSelector.addGoal(4, new AvoidEntityGoal<>(this, CeratosaurusEntity.class, (float) 20, 0.8, 0.8)); + this.goalSelector.addGoal(5, new AvoidEntityGoal<>(this, DilophosaurusEntity.class, (float) 20, 0.8, 0.8)); + this.goalSelector.addGoal(6, new SprintingMeleeAttackGoal(this, 1.1, false)); + this.goalSelector.addGoal(7, new BreedGoal(this, 1.0)); + this.goalSelector.addGoal(8, new FollowParentGoal(this, 1.25)); + this.goalSelector.addGoal(8, new WaterAvoidingRandomStrollGoal(this, 1.0)); + this.goalSelector.addGoal(10, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(11, new FollowMobGoal(this, 0.8, (float) 20, (float) 10)); + this.goalSelector.addGoal(12, new AvoidEntityGoal<>(this, SpinosaurusEntity.class, (float) 20, 0.8, 0.8)); + this.goalSelector.addGoal(13, new AvoidEntityGoal<>(this, TyrannosaurusRexEntity.class, (float) 20, 0.8, 0.8)); + this.goalSelector.addGoal(14, new AvoidEntityGoal<>(this, IndominusRexEntity.class, (float) 20, 0.8, 0.8)); + this.goalSelector.addGoal(15, new EatBlockGoal(this)); + this.goalSelector.addGoal(16, new RandomLookAroundGoal(this)); + + } + + public static AttributeSupplier.Builder createAttributes() { + return Animal.createLivingAttributes() + .add(Attributes.MAX_HEALTH, 50D) + .add(Attributes.MOVEMENT_SPEED, 0.3D) + .add(Attributes.ARMOR, 0D) + .add(Attributes.FOLLOW_RANGE, 32D) + .add(Attributes.KNOCKBACK_RESISTANCE, 0D) + .add(Attributes.ATTACK_KNOCKBACK, 0D) + .add(Attributes.ATTACK_DAMAGE, 14D); + } + + @Override + public boolean isFood(ItemStack pStack) { + return pStack.is(Items.KELP); + } + + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel pLevel, AgeableMob pOtherParent) { + AgeableMob child = ModEntities.CHASMOSAURUS.get().create(pLevel); + if (child instanceof ChasmosaurusEntity baby) { + ChasmosaurusVariant randomVariant = Util.getRandom(ChasmosaurusVariant.values(), this.random); + baby.setVariant(randomVariant); + } + return child; + } + + @Override + public boolean doHurtTarget(Entity target) { + boolean hit = super.doHurtTarget(target); + if (!level().isClientSide && hit && target instanceof LivingEntity) { + if (this.level() instanceof ServerLevel serverLevel) { + this.triggerAnim("attackController", "attack"); + } + } + return hit; + } + + @Override + public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { + controllers.add(new AnimationController<>(this, "Walk/Run/Idle", state -> { + if (state.isMoving()) + return state.setAndContinue(ChasmosaurusEntity.this.isSprinting() ? RawAnimation.begin().then("anim.chasmosaurus.run", Animation.LoopType.LOOP) : RawAnimation.begin().then("anim.chasmosaurus.walk", Animation.LoopType.LOOP)); + + return state.setAndContinue(RawAnimation.begin().then("anim.chasmosaurus.idle", Animation.LoopType.LOOP)); + })); + + controllers.add(new AnimationController<>(this, "attackController", state -> PlayState.STOP) + .triggerableAnim("attack", RawAnimation.begin().then("anim.chasmosaurus.attack", Animation.LoopType.PLAY_ONCE))); + + controllers.add(new AnimationController<>(this, "mouthController", state -> PlayState.STOP) + .triggerableAnim("mouth", RawAnimation.begin().then("anim.chasmosaurus.mouth", Animation.LoopType.PLAY_ONCE))); + } + + private float getSignedTurnDelta() { + // Only consider the body (torso) rotation so head look does not affect tail sway + return Mth.wrapDegrees(this.yBodyRot - this.yBodyRotO); + } + + private int mouthAnimCooldown = 0; + + @Override + public void tick() { + super.tick(); + if (!level().isClientSide) { + this.entityData.set(DATA_SYNCED_AGE, this.getAge()); + var maxHealthAttr = getAttribute(Attributes.MAX_HEALTH); + if (maxHealthAttr != null) { + double baseAdult = DefaultAttributes.getSupplier((EntityType) this.getType()).getValue(Attributes.MAX_HEALTH); + double desired = this.isBaby() ? baseAdult * 0.10D : baseAdult; + if (maxHealthAttr.getBaseValue() != desired) { + double oldMax = maxHealthAttr.getBaseValue(); + double healthRatio = this.getHealth() / (float) oldMax; + maxHealthAttr.setBaseValue(desired); + this.setHealth((float) (desired * Mth.clamp(healthRatio, 0.0F, 1.0F))); + } + } + } + + if (!level().isClientSide) { + if (mouthAnimCooldown > 0) { + mouthAnimCooldown--; + } else { + this.triggerAnim("mouthController", "mouth"); + this.playSound(ModSounds.CHASMOSAURUS_CALL.get(), 1.0F, 1.0F); + // 30s–60s in ticks + mouthAnimCooldown = this.random.nextInt(1200 - 600 + 1) + 600; + } + } + + if (level().isClientSide) { + // Capture previous for smooth interpolation between ticks + this.tailSwayPrev = this.tailSwayOffset; + updateProceduralTailSway(); + } + } + + private void updateProceduralTailSway() { + // Turn input derived from rotation deltas; works even when standing still and turning + float turnDegrees = getSignedTurnDelta(); + + // Deadzone to ignore tiny jitter so the tail can return to center cleanly + float deadzoneDeg = 0.6f; // smaller deadzone for more responsiveness + float turnInput = 0.0f; + if (Math.abs(turnDegrees) >= deadzoneDeg) { + // Higher sensitivity so small in-place turns still affect the model + turnInput = Mth.clamp(turnDegrees / 15.0f, -1.0f, 1.0f); + } + + // Target offset: keep intuitive sign (positive input -> positive sway) + float target = turnInput; + + // One-pole low-pass (no bounce). Larger alpha => snappier and less "stiff". + float alpha = 0.24f; // try 0.20–0.30 to taste + + tailSwayOffset += (target - tailSwayOffset) * alpha; + + // Snap tiny residuals to zero so it visibly settles + if (Math.abs(tailSwayOffset) < 0.003f) { + tailSwayOffset = 0.0f; + } + + // No oscillation velocity retained + tailSwayVelocity = 0.0f; + + tailSwayOffset = Mth.clamp(tailSwayOffset, -1.5f, 1.5f); + } + + // Expose to the model for bone rotation + public float getTailSwayOffset() { + return tailSwayOffset; + } + + // Interpolated sway for smooth rendering between ticks + public float getTailSwayOffset(float partialTick) { + return Mth.lerp(Mth.clamp(partialTick, 0.0f, 1.0f), tailSwayPrev, tailSwayOffset); + } + + /*? if <=1.20.1 {*/ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); + this.entityData.define(VARIANT, 0); + this.entityData.define(DATA_SYNCED_AGE, 0); + } + /*?} else {*/ + /*@Override + protected void defineSynchedData(SynchedEntityData.Builder pBuilder) { + super.defineSynchedData(pBuilder); + pBuilder.define(VARIANT, 0); + pBuilder.define(DATA_SYNCED_AGE, 0); + } + *//*?}*/ + + public int getSyncedAge() { + return this.entityData.get(DATA_SYNCED_AGE); + } + public int getTypeVariant() { + return this.entityData.get(VARIANT); + } + + public ChasmosaurusVariant getVariant() { + return ChasmosaurusVariant.byId(this.getTypeVariant() & 255); + } + + private void setVariant(ChasmosaurusVariant variant) { + this.entityData.set(VARIANT, variant.getId() & 255); + } + + @Override + public boolean canMate(Animal other) { + if (!super.canMate(other)) return false; + if (!(other instanceof ChasmosaurusEntity that)) return false; + return this.getVariant() != that.getVariant(); + } + @Override + public void addAdditionalSaveData(CompoundTag pCompound) { + super.addAdditionalSaveData(pCompound); + pCompound.putInt("Variant", this.getTypeVariant()); + } + + @Override + public void readAdditionalSaveData(CompoundTag pCompound) { + super.readAdditionalSaveData(pCompound); + this.entityData.set(VARIANT, pCompound.getInt("Variant")); + } + + /*? if <=1.20.1 {*/ + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor pLevel, DifficultyInstance pDifficulty, MobSpawnType pReason, @Nullable SpawnGroupData pSpawnData, @Nullable CompoundTag pDataTag) { + ChasmosaurusVariant variant = Util.getRandom(ChasmosaurusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(pLevel, pDifficulty, pReason, pSpawnData, pDataTag); + } + /*?} else {*/ + /*@Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, MobSpawnType spawnType, @Nullable SpawnGroupData spawnGroupData) { + ChasmosaurusVariant variant = Util.getRandom(ChasmosaurusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(level, difficulty, spawnType, spawnGroupData); + } + *//*?}*/ + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } + + @Override + protected @Nullable SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.CHASMOSAURUS_HURT.get(); + } + + @Override + protected @Nullable SoundEvent getDeathSound() { + return ModSounds.CHASMOSAURUS_DEATH.get(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/custom/ChickenosaurusEntity.java b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/ChickenosaurusEntity.java new file mode 100644 index 0000000..04dee91 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/ChickenosaurusEntity.java @@ -0,0 +1,314 @@ +package net.cmr.jurassicrevived.entity.custom; + +import net.cmr.jurassicrevived.entity.ModEntities; +import net.cmr.jurassicrevived.entity.ai.SprintingMeleeAttackGoal; +import net.cmr.jurassicrevived.entity.ai.SprintingPanicGoal; +import net.cmr.jurassicrevived.entity.client.ChickenosaurusVariant; +import net.cmr.jurassicrevived.entity.client.ChickenosaurusVariant; +import net.cmr.jurassicrevived.item.ModItems; +import net.cmr.jurassicrevived.sound.ModSounds; +import net.minecraft.Util; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.util.Mth; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.*; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.attributes.DefaultAttributes; +import net.minecraft.world.entity.ai.goal.*; +import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal; +import net.minecraft.world.entity.ai.goal.target.NearestAttackableTargetGoal; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerLevelAccessor; +import org.jetbrains.annotations.Nullable; +import software.bernie.geckolib.animatable.GeoEntity; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.core.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.core.animation.AnimatableManager; +import software.bernie.geckolib.core.animation.Animation; +import software.bernie.geckolib.core.animation.AnimationController; +import software.bernie.geckolib.core.animation.RawAnimation; +import software.bernie.geckolib.core.object.PlayState; +/*?} else {*/ +/*import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.animation.*; +*//*?}*/ + +public class ChickenosaurusEntity extends Animal implements GeoEntity { + private AnimatableInstanceCache cache = new SingletonAnimatableInstanceCache(this); + + private static final EntityDataAccessor VARIANT = + SynchedEntityData.defineId(ChickenosaurusEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SYNCED_AGE = + SynchedEntityData.defineId(ChickenosaurusEntity.class, EntityDataSerializers.INT); + + // Procedural tail sway state (client-side use for rendering) + private float tailSwayOffset; // Smoothed offset in range roughly [-1, 1] + private float tailSwayVelocity; // Internal velocity for spring-damper + private float tailSwayPrev; // Previous frame value for interpolation + + public ChickenosaurusEntity(EntityType pEntityType, Level pLevel) { + super(pEntityType, pLevel); + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new SprintingPanicGoal(this, 1.15) { + @Override + public boolean canUse() { + return ChickenosaurusEntity.this.isBaby() && super.canUse(); + } + }); + this.targetSelector.addGoal(1, new HurtByTargetGoal(this).setAlertOthers()); + this.goalSelector.addGoal(2, new FloatGoal(this)); + this.goalSelector.addGoal(3, new AvoidEntityGoal<>(this, SpinosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(4, new AvoidEntityGoal<>(this, TyrannosaurusRexEntity.class, (float) 20, 1.2, 1.2)); + this.goalSelector.addGoal(5, new AvoidEntityGoal<>(this, VelociraptorEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(6, new AvoidEntityGoal<>(this, ParasaurolophusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(7, new AvoidEntityGoal<>(this, CeratosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(8, new AvoidEntityGoal<>(this, TriceratopsEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(9, new AvoidEntityGoal<>(this, BrachiosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(10, new SprintingMeleeAttackGoal(this, 1.1, false)); + this.goalSelector.addGoal(11, new BreedGoal(this, 1.0)); + this.goalSelector.addGoal(12, new FollowParentGoal(this, 1.25)); + this.goalSelector.addGoal(13, new WaterAvoidingRandomStrollGoal(this, 1.0)); + this.goalSelector.addGoal(14, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(15, new FollowMobGoal(this, 1, (float) 20, (float) 10)); + this.targetSelector.addGoal(16, new NearestAttackableTargetGoal<>(this, Animal.class, 10, false, false, + target -> target.getType() != this.getType())); + this.targetSelector.addGoal(17, new NearestAttackableTargetGoal(this, GallimimusEntity.class, false, false)); + this.targetSelector.addGoal(18, new NearestAttackableTargetGoal(this, Player.class, false, false)); + this.targetSelector.addGoal(19, new NearestAttackableTargetGoal(this, CompsognathusEntity.class, false, false)); + this.goalSelector.addGoal(20, new RandomLookAroundGoal(this)); + } + + public static AttributeSupplier.Builder createAttributes() { + return Animal.createLivingAttributes() + .add(Attributes.MAX_HEALTH, 30D) + .add(Attributes.MOVEMENT_SPEED, 0.3D) + .add(Attributes.ARMOR, 0D) + .add(Attributes.FOLLOW_RANGE, 32D) + .add(Attributes.KNOCKBACK_RESISTANCE, 0.3D) + .add(Attributes.ATTACK_KNOCKBACK, 0D) + .add(Attributes.ATTACK_DAMAGE, 8D); + } + + @Override + public boolean isFood(ItemStack pStack) { + return pStack.is((Item) ModItems.MAC_N_CHEESE); + } + + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel pLevel, AgeableMob pOtherParent) { + AgeableMob child = ModEntities.CHICKENOSAURUS.get().create(pLevel); + if (child instanceof ChickenosaurusEntity baby) { + ChickenosaurusVariant randomVariant = Util.getRandom(ChickenosaurusVariant.values(), this.random); + baby.setVariant(randomVariant); + } + return child; + } + + @Override + public boolean doHurtTarget(Entity target) { + boolean hit = super.doHurtTarget(target); + if (!level().isClientSide && hit && target instanceof LivingEntity) { + if (this.level() instanceof ServerLevel serverLevel) { + this.triggerAnim("attackController", "attack"); + } + } + return hit; + } + + @Override + public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { + controllers.add(new AnimationController<>(this, "Walk/Run/Idle", state -> { + if (state.isMoving()) + return state.setAndContinue(ChickenosaurusEntity.this.isSprinting() ? RawAnimation.begin().then("anim.chickenosaurus.run", Animation.LoopType.LOOP) : RawAnimation.begin().then("anim.chickenosaurus.walk", Animation.LoopType.LOOP)); + + return state.setAndContinue(RawAnimation.begin().then("anim.chickenosaurus.idle", Animation.LoopType.LOOP)); + })); + + controllers.add(new AnimationController<>(this, "attackController", state -> PlayState.STOP) + .triggerableAnim("attack", RawAnimation.begin().then("anim.chickenosaurus.attack", Animation.LoopType.PLAY_ONCE))); + + controllers.add(new AnimationController<>(this, "mouthController", state -> PlayState.STOP) + .triggerableAnim("mouth", RawAnimation.begin().then("anim.chickenosaurus.mouth", Animation.LoopType.PLAY_ONCE))); + } + + private float getSignedTurnDelta() { + // Only consider the body (torso) rotation so head look does not affect tail sway + return Mth.wrapDegrees(this.yBodyRot - this.yBodyRotO); + } + + private int mouthAnimCooldown = 0; + + @Override + public void tick() { + super.tick(); + if (!level().isClientSide) { + this.entityData.set(DATA_SYNCED_AGE, this.getAge()); + var maxHealthAttr = getAttribute(Attributes.MAX_HEALTH); + if (maxHealthAttr != null) { + double baseAdult = DefaultAttributes.getSupplier((EntityType) this.getType()).getValue(Attributes.MAX_HEALTH); + double desired = this.isBaby() ? baseAdult * 0.10D : baseAdult; + if (maxHealthAttr.getBaseValue() != desired) { + double oldMax = maxHealthAttr.getBaseValue(); + double healthRatio = this.getHealth() / (float) oldMax; + maxHealthAttr.setBaseValue(desired); + this.setHealth((float) (desired * Mth.clamp(healthRatio, 0.0F, 1.0F))); + } + } + } + + if (!level().isClientSide) { + if (mouthAnimCooldown > 0) { + mouthAnimCooldown--; + } else { + this.triggerAnim("mouthController", "mouth"); + this.playSound(ModSounds.CHICKENOSAURUS_CALL.get(), 1.0F, 1.0F); + // 30s–60s in ticks + mouthAnimCooldown = this.random.nextInt(1200 - 600 + 1) + 600; + } + } + + if (level().isClientSide) { + // Capture previous for smooth interpolation between ticks + this.tailSwayPrev = this.tailSwayOffset; + updateProceduralTailSway(); + } + } + + private void updateProceduralTailSway() { + // Turn input derived from rotation deltas; works even when standing still and turning + float turnDegrees = getSignedTurnDelta(); + + // Deadzone to ignore tiny jitter so the tail can return to center cleanly + float deadzoneDeg = 0.6f; // smaller deadzone for more responsiveness + float turnInput = 0.0f; + if (Math.abs(turnDegrees) >= deadzoneDeg) { + // Higher sensitivity so small in-place turns still affect the model + turnInput = Mth.clamp(turnDegrees / 15.0f, -1.0f, 1.0f); + } + + // Target offset: keep intuitive sign (positive input -> positive sway) + float target = turnInput; + + // One-pole low-pass (no bounce). Larger alpha => snappier and less "stiff". + float alpha = 0.24f; // try 0.20–0.30 to taste + + tailSwayOffset += (target - tailSwayOffset) * alpha; + + // Snap tiny residuals to zero so it visibly settles + if (Math.abs(tailSwayOffset) < 0.003f) { + tailSwayOffset = 0.0f; + } + + // No oscillation velocity retained + tailSwayVelocity = 0.0f; + + tailSwayOffset = Mth.clamp(tailSwayOffset, -1.5f, 1.5f); + } + + // Expose to the model for bone rotation + public float getTailSwayOffset() { + return tailSwayOffset; + } + + // Interpolated sway for smooth rendering between ticks + public float getTailSwayOffset(float partialTick) { + return Mth.lerp(Mth.clamp(partialTick, 0.0f, 1.0f), tailSwayPrev, tailSwayOffset); + } + + /*? if <=1.20.1 {*/ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); + this.entityData.define(VARIANT, 0); + this.entityData.define(DATA_SYNCED_AGE, 0); + } + /*?} else {*/ + /*@Override + protected void defineSynchedData(SynchedEntityData.Builder pBuilder) { + super.defineSynchedData(pBuilder); + pBuilder.define(VARIANT, 0); + pBuilder.define(DATA_SYNCED_AGE, 0); + } + *//*?}*/ + + public int getSyncedAge() { + return this.entityData.get(DATA_SYNCED_AGE); + } + public int getTypeVariant() { + return this.entityData.get(VARIANT); + } + + public ChickenosaurusVariant getVariant() { + return ChickenosaurusVariant.byId(this.getTypeVariant() & 255); + } + + private void setVariant(ChickenosaurusVariant variant) { + this.entityData.set(VARIANT, variant.getId() & 255); + } + + @Override + public boolean canMate(Animal other) { + if (!super.canMate(other)) return false; + if (!(other instanceof ChickenosaurusEntity that)) return false; + return this.getVariant() != that.getVariant(); + } + @Override + public void addAdditionalSaveData(CompoundTag pCompound) { + super.addAdditionalSaveData(pCompound); + pCompound.putInt("Variant", this.getTypeVariant()); + } + + @Override + public void readAdditionalSaveData(CompoundTag pCompound) { + super.readAdditionalSaveData(pCompound); + this.entityData.set(VARIANT, pCompound.getInt("Variant")); + } + + /*? if <=1.20.1 {*/ + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor pLevel, DifficultyInstance pDifficulty, MobSpawnType pReason, @Nullable SpawnGroupData pSpawnData, @Nullable CompoundTag pDataTag) { + ChickenosaurusVariant variant = Util.getRandom(ChickenosaurusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(pLevel, pDifficulty, pReason, pSpawnData, pDataTag); + } + /*?} else {*/ + /*@Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, MobSpawnType spawnType, @Nullable SpawnGroupData spawnGroupData) { + ChickenosaurusVariant variant = Util.getRandom(ChickenosaurusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(level, difficulty, spawnType, spawnGroupData); + } + *//*?}*/ + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } + + @Override + protected @Nullable SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.CHICKENOSAURUS_HURT.get(); + } + + @Override + protected @Nullable SoundEvent getDeathSound() { + return ModSounds.CHICKENOSAURUS_DEATH.get(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/custom/CoelophysisEntity.java b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/CoelophysisEntity.java new file mode 100644 index 0000000..27d9241 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/CoelophysisEntity.java @@ -0,0 +1,313 @@ +package net.cmr.jurassicrevived.entity.custom; + +import net.cmr.jurassicrevived.entity.ModEntities; +import net.cmr.jurassicrevived.entity.ai.SprintingMeleeAttackGoal; +import net.cmr.jurassicrevived.entity.ai.SprintingPanicGoal; +import net.cmr.jurassicrevived.entity.client.CoelophysisVariant; +import net.cmr.jurassicrevived.entity.client.CoelophysisVariant; +import net.cmr.jurassicrevived.sound.ModSounds; +import net.minecraft.Util; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.util.Mth; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.*; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.attributes.DefaultAttributes; +import net.minecraft.world.entity.ai.goal.*; +import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal; +import net.minecraft.world.entity.ai.goal.target.NearestAttackableTargetGoal; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerLevelAccessor; +import org.jetbrains.annotations.Nullable; +import software.bernie.geckolib.animatable.GeoEntity; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.core.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.core.animation.AnimatableManager; +import software.bernie.geckolib.core.animation.Animation; +import software.bernie.geckolib.core.animation.AnimationController; +import software.bernie.geckolib.core.animation.RawAnimation; +import software.bernie.geckolib.core.object.PlayState; +/*?} else {*/ +/*import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.animation.*; +*//*?}*/ + +public class CoelophysisEntity extends Animal implements GeoEntity { + private AnimatableInstanceCache cache = new SingletonAnimatableInstanceCache(this); + + private static final EntityDataAccessor VARIANT = + SynchedEntityData.defineId(CoelophysisEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SYNCED_AGE = + SynchedEntityData.defineId(CoelophysisEntity.class, EntityDataSerializers.INT); + + // Procedural tail sway state (client-side use for rendering) + private float tailSwayOffset; // Smoothed offset in range roughly [-1, 1] + private float tailSwayVelocity; // Internal velocity for spring-damper + private float tailSwayPrev; // Previous frame value for interpolation + + public CoelophysisEntity(EntityType pEntityType, Level pLevel) { + super(pEntityType, pLevel); + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new SprintingPanicGoal(this, 1.15) { + @Override + public boolean canUse() { + return CoelophysisEntity.this.isBaby() && super.canUse(); + } + }); + this.targetSelector.addGoal(1, new HurtByTargetGoal(this).setAlertOthers()); + this.goalSelector.addGoal(2, new FloatGoal(this)); + this.goalSelector.addGoal(3, new AvoidEntityGoal<>(this, SpinosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(4, new AvoidEntityGoal<>(this, TyrannosaurusRexEntity.class, (float) 20, 1.2, 1.2)); + this.goalSelector.addGoal(5, new AvoidEntityGoal<>(this, VelociraptorEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(6, new AvoidEntityGoal<>(this, ParasaurolophusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(7, new AvoidEntityGoal<>(this, CeratosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(8, new AvoidEntityGoal<>(this, TriceratopsEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(9, new AvoidEntityGoal<>(this, BrachiosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(10, new SprintingMeleeAttackGoal(this, 1.1, false)); + this.goalSelector.addGoal(11, new BreedGoal(this, 1.0)); + this.goalSelector.addGoal(12, new FollowParentGoal(this, 1.25)); + this.goalSelector.addGoal(13, new WaterAvoidingRandomStrollGoal(this, 1.0)); + this.goalSelector.addGoal(14, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(15, new FollowMobGoal(this, 1, (float) 20, (float) 10)); + this.targetSelector.addGoal(16, new NearestAttackableTargetGoal<>(this, Animal.class, 10, false, false, + target -> target.getType() != this.getType())); + this.targetSelector.addGoal(17, new NearestAttackableTargetGoal(this, GallimimusEntity.class, false, false)); + this.targetSelector.addGoal(18, new NearestAttackableTargetGoal(this, Player.class, false, false)); + this.targetSelector.addGoal(19, new NearestAttackableTargetGoal(this, CompsognathusEntity.class, false, false)); + this.goalSelector.addGoal(20, new RandomLookAroundGoal(this)); + } + + public static AttributeSupplier.Builder createAttributes() { + return Animal.createLivingAttributes() + .add(Attributes.MAX_HEALTH, 14D) + .add(Attributes.MOVEMENT_SPEED, 0.3D) + .add(Attributes.ARMOR, 0D) + .add(Attributes.FOLLOW_RANGE, 32D) + .add(Attributes.KNOCKBACK_RESISTANCE, 0.3D) + .add(Attributes.ATTACK_KNOCKBACK, 0D) + .add(Attributes.ATTACK_DAMAGE, 4D); + } + + @Override + public boolean isFood(ItemStack pStack) { + return pStack.is(Items.BEEF); + } + + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel pLevel, AgeableMob pOtherParent) { + AgeableMob child = ModEntities.COELOPHYSIS.get().create(pLevel); + if (child instanceof CoelophysisEntity baby) { + CoelophysisVariant randomVariant = Util.getRandom(CoelophysisVariant.values(), this.random); + baby.setVariant(randomVariant); + } + return child; + } + + @Override + public boolean doHurtTarget(Entity target) { + boolean hit = super.doHurtTarget(target); + if (!level().isClientSide && hit && target instanceof LivingEntity) { + if (this.level() instanceof ServerLevel serverLevel) { + this.triggerAnim("attackController", "attack"); + } + } + return hit; + } + + @Override + public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { + controllers.add(new AnimationController<>(this, "Walk/Run/Idle", state -> { + if (state.isMoving()) + return state.setAndContinue(CoelophysisEntity.this.isSprinting() ? RawAnimation.begin().then("anim.coelophysis.run", Animation.LoopType.LOOP) : RawAnimation.begin().then("anim.coelophysis.walk", Animation.LoopType.LOOP)); + + return state.setAndContinue(RawAnimation.begin().then("anim.coelophysis.idle", Animation.LoopType.LOOP)); + })); + + controllers.add(new AnimationController<>(this, "attackController", state -> PlayState.STOP) + .triggerableAnim("attack", RawAnimation.begin().then("anim.coelophysis.attack", Animation.LoopType.PLAY_ONCE))); + + controllers.add(new AnimationController<>(this, "mouthController", state -> PlayState.STOP) + .triggerableAnim("mouth", RawAnimation.begin().then("anim.coelophysis.mouth", Animation.LoopType.PLAY_ONCE))); + } + + private float getSignedTurnDelta() { + // Only consider the body (torso) rotation so head look does not affect tail sway + return Mth.wrapDegrees(this.yBodyRot - this.yBodyRotO); + } + + private int mouthAnimCooldown = 0; + + @Override + public void tick() { + super.tick(); + if (!level().isClientSide) { + this.entityData.set(DATA_SYNCED_AGE, this.getAge()); + var maxHealthAttr = getAttribute(Attributes.MAX_HEALTH); + if (maxHealthAttr != null) { + double baseAdult = DefaultAttributes.getSupplier((EntityType) this.getType()).getValue(Attributes.MAX_HEALTH); + double desired = this.isBaby() ? baseAdult * 0.10D : baseAdult; + if (maxHealthAttr.getBaseValue() != desired) { + double oldMax = maxHealthAttr.getBaseValue(); + double healthRatio = this.getHealth() / (float) oldMax; + maxHealthAttr.setBaseValue(desired); + this.setHealth((float) (desired * Mth.clamp(healthRatio, 0.0F, 1.0F))); + } + } + } + + if (!level().isClientSide) { + if (mouthAnimCooldown > 0) { + mouthAnimCooldown--; + } else { + this.triggerAnim("mouthController", "mouth"); + this.playSound(ModSounds.COELOPHYSIS_CALL.get(), 1.0F, 1.0F); + // 30s–60s in ticks + mouthAnimCooldown = this.random.nextInt(1200 - 600 + 1) + 600; + } + } + + if (level().isClientSide) { + // Capture previous for smooth interpolation between ticks + this.tailSwayPrev = this.tailSwayOffset; + updateProceduralTailSway(); + } + } + + private void updateProceduralTailSway() { + // Turn input derived from rotation deltas; works even when standing still and turning + float turnDegrees = getSignedTurnDelta(); + + // Deadzone to ignore tiny jitter so the tail can return to center cleanly + float deadzoneDeg = 0.6f; // smaller deadzone for more responsiveness + float turnInput = 0.0f; + if (Math.abs(turnDegrees) >= deadzoneDeg) { + // Higher sensitivity so small in-place turns still affect the model + turnInput = Mth.clamp(turnDegrees / 15.0f, -1.0f, 1.0f); + } + + // Target offset: keep intuitive sign (positive input -> positive sway) + float target = turnInput; + + // One-pole low-pass (no bounce). Larger alpha => snappier and less "stiff". + float alpha = 0.24f; // try 0.20–0.30 to taste + + tailSwayOffset += (target - tailSwayOffset) * alpha; + + // Snap tiny residuals to zero so it visibly settles + if (Math.abs(tailSwayOffset) < 0.003f) { + tailSwayOffset = 0.0f; + } + + // No oscillation velocity retained + tailSwayVelocity = 0.0f; + + tailSwayOffset = Mth.clamp(tailSwayOffset, -1.5f, 1.5f); + } + + // Expose to the model for bone rotation + public float getTailSwayOffset() { + return tailSwayOffset; + } + + // Interpolated sway for smooth rendering between ticks + public float getTailSwayOffset(float partialTick) { + return Mth.lerp(Mth.clamp(partialTick, 0.0f, 1.0f), tailSwayPrev, tailSwayOffset); + } + + /*? if <=1.20.1 {*/ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); + this.entityData.define(VARIANT, 0); + this.entityData.define(DATA_SYNCED_AGE, 0); + } + /*?} else {*/ + /*@Override + protected void defineSynchedData(SynchedEntityData.Builder pBuilder) { + super.defineSynchedData(pBuilder); + pBuilder.define(VARIANT, 0); + pBuilder.define(DATA_SYNCED_AGE, 0); + } + *//*?}*/ + + public int getSyncedAge() { + return this.entityData.get(DATA_SYNCED_AGE); + } + public int getTypeVariant() { + return this.entityData.get(VARIANT); + } + + public CoelophysisVariant getVariant() { + return CoelophysisVariant.byId(this.getTypeVariant() & 255); + } + + private void setVariant(CoelophysisVariant variant) { + this.entityData.set(VARIANT, variant.getId() & 255); + } + + @Override + public boolean canMate(Animal other) { + if (!super.canMate(other)) return false; + if (!(other instanceof CoelophysisEntity that)) return false; + return this.getVariant() != that.getVariant(); + } + @Override + public void addAdditionalSaveData(CompoundTag pCompound) { + super.addAdditionalSaveData(pCompound); + pCompound.putInt("Variant", this.getTypeVariant()); + } + + @Override + public void readAdditionalSaveData(CompoundTag pCompound) { + super.readAdditionalSaveData(pCompound); + this.entityData.set(VARIANT, pCompound.getInt("Variant")); + } + + /*? if <=1.20.1 {*/ + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor pLevel, DifficultyInstance pDifficulty, MobSpawnType pReason, @Nullable SpawnGroupData pSpawnData, @Nullable CompoundTag pDataTag) { + CoelophysisVariant variant = Util.getRandom(CoelophysisVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(pLevel, pDifficulty, pReason, pSpawnData, pDataTag); + } + /*?} else {*/ + /*@Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, MobSpawnType spawnType, @Nullable SpawnGroupData spawnGroupData) { + CoelophysisVariant variant = Util.getRandom(CoelophysisVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(level, difficulty, spawnType, spawnGroupData); + } + *//*?}*/ + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } + + @Override + protected @Nullable SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.COELOPHYSIS_HURT.get(); + } + + @Override + protected @Nullable SoundEvent getDeathSound() { + return ModSounds.COELOPHYSIS_DEATH.get(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/custom/CoelurusEntity.java b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/CoelurusEntity.java new file mode 100644 index 0000000..f10b65e --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/CoelurusEntity.java @@ -0,0 +1,313 @@ +package net.cmr.jurassicrevived.entity.custom; + +import net.cmr.jurassicrevived.entity.ModEntities; +import net.cmr.jurassicrevived.entity.ai.SprintingMeleeAttackGoal; +import net.cmr.jurassicrevived.entity.ai.SprintingPanicGoal; +import net.cmr.jurassicrevived.entity.client.CoelurusVariant; +import net.cmr.jurassicrevived.entity.client.CoelurusVariant; +import net.cmr.jurassicrevived.sound.ModSounds; +import net.minecraft.Util; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.util.Mth; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.*; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.attributes.DefaultAttributes; +import net.minecraft.world.entity.ai.goal.*; +import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal; +import net.minecraft.world.entity.ai.goal.target.NearestAttackableTargetGoal; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerLevelAccessor; +import org.jetbrains.annotations.Nullable; +import software.bernie.geckolib.animatable.GeoEntity; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.core.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.core.animation.AnimatableManager; +import software.bernie.geckolib.core.animation.Animation; +import software.bernie.geckolib.core.animation.AnimationController; +import software.bernie.geckolib.core.animation.RawAnimation; +import software.bernie.geckolib.core.object.PlayState; +/*?} else {*/ +/*import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.animation.*; +*//*?}*/ + +public class CoelurusEntity extends Animal implements GeoEntity { + private AnimatableInstanceCache cache = new SingletonAnimatableInstanceCache(this); + + private static final EntityDataAccessor VARIANT = + SynchedEntityData.defineId(CoelurusEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SYNCED_AGE = + SynchedEntityData.defineId(CoelurusEntity.class, EntityDataSerializers.INT); + + // Procedural tail sway state (client-side use for rendering) + private float tailSwayOffset; // Smoothed offset in range roughly [-1, 1] + private float tailSwayVelocity; // Internal velocity for spring-damper + private float tailSwayPrev; // Previous frame value for interpolation + + public CoelurusEntity(EntityType pEntityType, Level pLevel) { + super(pEntityType, pLevel); + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new SprintingPanicGoal(this, 1.15) { + @Override + public boolean canUse() { + return CoelurusEntity.this.isBaby() && super.canUse(); + } + }); + this.targetSelector.addGoal(1, new HurtByTargetGoal(this).setAlertOthers()); + this.goalSelector.addGoal(2, new FloatGoal(this)); + this.goalSelector.addGoal(3, new AvoidEntityGoal<>(this, SpinosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(4, new AvoidEntityGoal<>(this, TyrannosaurusRexEntity.class, (float) 20, 1.2, 1.2)); + this.goalSelector.addGoal(5, new AvoidEntityGoal<>(this, VelociraptorEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(6, new AvoidEntityGoal<>(this, ParasaurolophusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(7, new AvoidEntityGoal<>(this, CeratosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(8, new AvoidEntityGoal<>(this, TriceratopsEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(9, new AvoidEntityGoal<>(this, BrachiosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(10, new SprintingMeleeAttackGoal(this, 1.1, false)); + this.goalSelector.addGoal(11, new BreedGoal(this, 1.0)); + this.goalSelector.addGoal(12, new FollowParentGoal(this, 1.25)); + this.goalSelector.addGoal(13, new WaterAvoidingRandomStrollGoal(this, 1.0)); + this.goalSelector.addGoal(14, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(15, new FollowMobGoal(this, 1, (float) 20, (float) 10)); + this.targetSelector.addGoal(16, new NearestAttackableTargetGoal<>(this, Animal.class, 10, false, false, + target -> target.getType() != this.getType())); + this.targetSelector.addGoal(17, new NearestAttackableTargetGoal(this, GallimimusEntity.class, false, false)); + this.targetSelector.addGoal(18, new NearestAttackableTargetGoal(this, Player.class, false, false)); + this.targetSelector.addGoal(19, new NearestAttackableTargetGoal(this, CompsognathusEntity.class, false, false)); + this.goalSelector.addGoal(20, new RandomLookAroundGoal(this)); + } + + public static AttributeSupplier.Builder createAttributes() { + return Animal.createLivingAttributes() + .add(Attributes.MAX_HEALTH, 14D) + .add(Attributes.MOVEMENT_SPEED, 0.3D) + .add(Attributes.ARMOR, 0D) + .add(Attributes.FOLLOW_RANGE, 32D) + .add(Attributes.KNOCKBACK_RESISTANCE, 0.3D) + .add(Attributes.ATTACK_KNOCKBACK, 0D) + .add(Attributes.ATTACK_DAMAGE, 4D); + } + + @Override + public boolean isFood(ItemStack pStack) { + return pStack.is(Items.BEEF); + } + + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel pLevel, AgeableMob pOtherParent) { + AgeableMob child = ModEntities.COELURUS.get().create(pLevel); + if (child instanceof CoelurusEntity baby) { + CoelurusVariant randomVariant = Util.getRandom(CoelurusVariant.values(), this.random); + baby.setVariant(randomVariant); + } + return child; + } + + @Override + public boolean doHurtTarget(Entity target) { + boolean hit = super.doHurtTarget(target); + if (!level().isClientSide && hit && target instanceof LivingEntity) { + if (this.level() instanceof ServerLevel serverLevel) { + this.triggerAnim("attackController", "attack"); + } + } + return hit; + } + + @Override + public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { + controllers.add(new AnimationController<>(this, "Walk/Run/Idle", state -> { + if (state.isMoving()) + return state.setAndContinue(CoelurusEntity.this.isSprinting() ? RawAnimation.begin().then("anim.coelurus.run", Animation.LoopType.LOOP) : RawAnimation.begin().then("anim.coelurus.walk", Animation.LoopType.LOOP)); + + return state.setAndContinue(RawAnimation.begin().then("anim.coelurus.idle", Animation.LoopType.LOOP)); + })); + + controllers.add(new AnimationController<>(this, "attackController", state -> PlayState.STOP) + .triggerableAnim("attack", RawAnimation.begin().then("anim.coelurus.attack", Animation.LoopType.PLAY_ONCE))); + + controllers.add(new AnimationController<>(this, "mouthController", state -> PlayState.STOP) + .triggerableAnim("mouth", RawAnimation.begin().then("anim.coelurus.mouth", Animation.LoopType.PLAY_ONCE))); + } + + private float getSignedTurnDelta() { + // Only consider the body (torso) rotation so head look does not affect tail sway + return Mth.wrapDegrees(this.yBodyRot - this.yBodyRotO); + } + + private int mouthAnimCooldown = 0; + + @Override + public void tick() { + super.tick(); + if (!level().isClientSide) { + this.entityData.set(DATA_SYNCED_AGE, this.getAge()); + var maxHealthAttr = getAttribute(Attributes.MAX_HEALTH); + if (maxHealthAttr != null) { + double baseAdult = DefaultAttributes.getSupplier((EntityType) this.getType()).getValue(Attributes.MAX_HEALTH); + double desired = this.isBaby() ? baseAdult * 0.10D : baseAdult; + if (maxHealthAttr.getBaseValue() != desired) { + double oldMax = maxHealthAttr.getBaseValue(); + double healthRatio = this.getHealth() / (float) oldMax; + maxHealthAttr.setBaseValue(desired); + this.setHealth((float) (desired * Mth.clamp(healthRatio, 0.0F, 1.0F))); + } + } + } + + if (!level().isClientSide) { + if (mouthAnimCooldown > 0) { + mouthAnimCooldown--; + } else { + this.triggerAnim("mouthController", "mouth"); + this.playSound(ModSounds.COELURUS_CALL.get(), 1.0F, 1.0F); + // 30s–60s in ticks + mouthAnimCooldown = this.random.nextInt(1200 - 600 + 1) + 600; + } + } + + if (level().isClientSide) { + // Capture previous for smooth interpolation between ticks + this.tailSwayPrev = this.tailSwayOffset; + updateProceduralTailSway(); + } + } + + private void updateProceduralTailSway() { + // Turn input derived from rotation deltas; works even when standing still and turning + float turnDegrees = getSignedTurnDelta(); + + // Deadzone to ignore tiny jitter so the tail can return to center cleanly + float deadzoneDeg = 0.6f; // smaller deadzone for more responsiveness + float turnInput = 0.0f; + if (Math.abs(turnDegrees) >= deadzoneDeg) { + // Higher sensitivity so small in-place turns still affect the model + turnInput = Mth.clamp(turnDegrees / 15.0f, -1.0f, 1.0f); + } + + // Target offset: keep intuitive sign (positive input -> positive sway) + float target = turnInput; + + // One-pole low-pass (no bounce). Larger alpha => snappier and less "stiff". + float alpha = 0.24f; // try 0.20–0.30 to taste + + tailSwayOffset += (target - tailSwayOffset) * alpha; + + // Snap tiny residuals to zero so it visibly settles + if (Math.abs(tailSwayOffset) < 0.003f) { + tailSwayOffset = 0.0f; + } + + // No oscillation velocity retained + tailSwayVelocity = 0.0f; + + tailSwayOffset = Mth.clamp(tailSwayOffset, -1.5f, 1.5f); + } + + // Expose to the model for bone rotation + public float getTailSwayOffset() { + return tailSwayOffset; + } + + // Interpolated sway for smooth rendering between ticks + public float getTailSwayOffset(float partialTick) { + return Mth.lerp(Mth.clamp(partialTick, 0.0f, 1.0f), tailSwayPrev, tailSwayOffset); + } + + /*? if <=1.20.1 {*/ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); + this.entityData.define(VARIANT, 0); + this.entityData.define(DATA_SYNCED_AGE, 0); + } + /*?} else {*/ + /*@Override + protected void defineSynchedData(SynchedEntityData.Builder pBuilder) { + super.defineSynchedData(pBuilder); + pBuilder.define(VARIANT, 0); + pBuilder.define(DATA_SYNCED_AGE, 0); + } + *//*?}*/ + + public int getSyncedAge() { + return this.entityData.get(DATA_SYNCED_AGE); + } + public int getTypeVariant() { + return this.entityData.get(VARIANT); + } + + public CoelurusVariant getVariant() { + return CoelurusVariant.byId(this.getTypeVariant() & 255); + } + + private void setVariant(CoelurusVariant variant) { + this.entityData.set(VARIANT, variant.getId() & 255); + } + + @Override + public boolean canMate(Animal other) { + if (!super.canMate(other)) return false; + if (!(other instanceof CoelurusEntity that)) return false; + return this.getVariant() != that.getVariant(); + } + @Override + public void addAdditionalSaveData(CompoundTag pCompound) { + super.addAdditionalSaveData(pCompound); + pCompound.putInt("Variant", this.getTypeVariant()); + } + + @Override + public void readAdditionalSaveData(CompoundTag pCompound) { + super.readAdditionalSaveData(pCompound); + this.entityData.set(VARIANT, pCompound.getInt("Variant")); + } + + /*? if <=1.20.1 {*/ + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor pLevel, DifficultyInstance pDifficulty, MobSpawnType pReason, @Nullable SpawnGroupData pSpawnData, @Nullable CompoundTag pDataTag) { + CoelurusVariant variant = Util.getRandom(CoelurusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(pLevel, pDifficulty, pReason, pSpawnData, pDataTag); + } + /*?} else {*/ + /*@Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, MobSpawnType spawnType, @Nullable SpawnGroupData spawnGroupData) { + CoelurusVariant variant = Util.getRandom(CoelurusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(level, difficulty, spawnType, spawnGroupData); + } + *//*?}*/ + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } + + @Override + protected @Nullable SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.COELURUS_HURT.get(); + } + + @Override + protected @Nullable SoundEvent getDeathSound() { + return ModSounds.COELURUS_DEATH.get(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/custom/CompsognathusEntity.java b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/CompsognathusEntity.java new file mode 100644 index 0000000..735d4cf --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/CompsognathusEntity.java @@ -0,0 +1,300 @@ +package net.cmr.jurassicrevived.entity.custom; + +import net.cmr.jurassicrevived.entity.ModEntities; +import net.cmr.jurassicrevived.entity.ai.SprintingMeleeAttackGoal; +import net.cmr.jurassicrevived.entity.ai.SprintingPanicGoal; +import net.cmr.jurassicrevived.entity.client.CompsognathusVariant; +import net.cmr.jurassicrevived.entity.client.CompsognathusVariant; +import net.cmr.jurassicrevived.sound.ModSounds; +import net.minecraft.Util; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.util.Mth; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.*; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.attributes.DefaultAttributes; +import net.minecraft.world.entity.ai.goal.*; +import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerLevelAccessor; +import org.jetbrains.annotations.Nullable; +import software.bernie.geckolib.animatable.GeoEntity; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.core.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.core.animation.AnimatableManager; +import software.bernie.geckolib.core.animation.Animation; +import software.bernie.geckolib.core.animation.AnimationController; +import software.bernie.geckolib.core.animation.RawAnimation; +import software.bernie.geckolib.core.object.PlayState; +/*?} else {*/ +/*import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.animation.*; +*//*?}*/ + +public class CompsognathusEntity extends Animal implements GeoEntity { + private AnimatableInstanceCache cache = new SingletonAnimatableInstanceCache(this); + + private static final EntityDataAccessor VARIANT = + SynchedEntityData.defineId(CompsognathusEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SYNCED_AGE = + SynchedEntityData.defineId(CompsognathusEntity.class, EntityDataSerializers.INT); + + // Procedural tail sway state (client-side use for rendering) + private float tailSwayOffset; // Smoothed offset in range roughly [-1, 1] + private float tailSwayVelocity; // Internal velocity for spring-damper + private float tailSwayPrev; // Previous frame value for interpolation + + public CompsognathusEntity(EntityType pEntityType, Level pLevel) { + super(pEntityType, pLevel); + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new SprintingPanicGoal(this, 1.15) { + @Override + public boolean canUse() { + return CompsognathusEntity.this.isBaby() && super.canUse(); + } + }); + this.targetSelector.addGoal(1, new HurtByTargetGoal(this).setAlertOthers()); + this.goalSelector.addGoal(2, new FloatGoal(this)); + this.goalSelector.addGoal(3, new SprintingMeleeAttackGoal(this, 1.1, false)); + this.goalSelector.addGoal(4, new BreedGoal(this, 1.0)); + this.goalSelector.addGoal(5, new FollowParentGoal(this, 1.25)); + this.goalSelector.addGoal(6, new WaterAvoidingRandomStrollGoal(this, 1.0)); + this.goalSelector.addGoal(7, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(8, new FollowMobGoal(this, 1, (float) 20, (float) 10)); + this.goalSelector.addGoal(9, new RandomLookAroundGoal(this)); + } + + public static AttributeSupplier.Builder createAttributes() { + return Animal.createLivingAttributes() + .add(Attributes.MAX_HEALTH, 5D) + .add(Attributes.MOVEMENT_SPEED, 0.3D) + .add(Attributes.ARMOR, 0D) + .add(Attributes.FOLLOW_RANGE, 32D) + .add(Attributes.ATTACK_DAMAGE, 2D) + .add(Attributes.KNOCKBACK_RESISTANCE, 0.3D) + .add(Attributes.ATTACK_KNOCKBACK, 0D); + } + + @Override + public boolean isFood(ItemStack pStack) { + return pStack.is(Items.BEEF); + } + + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel pLevel, AgeableMob pOtherParent) { + AgeableMob child = ModEntities.COMPSOGNATHUS.get().create(pLevel); + if (child instanceof CompsognathusEntity baby) { + CompsognathusVariant randomVariant = Util.getRandom(CompsognathusVariant.values(), this.random); + baby.setVariant(randomVariant); + } + return child; + } + + @Override + public boolean doHurtTarget(Entity target) { + boolean hit = super.doHurtTarget(target); + if (!level().isClientSide && hit && target instanceof LivingEntity) { + if (this.level() instanceof ServerLevel serverLevel) { + this.triggerAnim("attackController", "attack"); + } + } + return hit; + } + + @Override + public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { + controllers.add(new AnimationController<>(this, "Walk/Run/Idle", state -> { + if (state.isMoving()) + return state.setAndContinue(CompsognathusEntity.this.isSprinting() ? RawAnimation.begin().then("anim.compsognathus.run", Animation.LoopType.LOOP) : RawAnimation.begin().then("anim.compsognathus.walk", Animation.LoopType.LOOP)); + + return state.setAndContinue(RawAnimation.begin().then("anim.compsognathus.idle", Animation.LoopType.LOOP)); + })); + + controllers.add(new AnimationController<>(this, "attackController", state -> PlayState.STOP) + .triggerableAnim("attack", RawAnimation.begin().then("anim.compsognathus.attack", Animation.LoopType.PLAY_ONCE))); + + controllers.add(new AnimationController<>(this, "mouthController", state -> PlayState.STOP) + .triggerableAnim("mouth", RawAnimation.begin().then("anim.compsognathus.mouth", Animation.LoopType.PLAY_ONCE))); + } + + private float getSignedTurnDelta() { + // Only consider the body (torso) rotation so head look does not affect tail sway + return Mth.wrapDegrees(this.yBodyRot - this.yBodyRotO); + } + + private int mouthAnimCooldown = 0; + + @Override + public void tick() { + super.tick(); + if (!level().isClientSide) { + this.entityData.set(DATA_SYNCED_AGE, this.getAge()); + var maxHealthAttr = getAttribute(Attributes.MAX_HEALTH); + if (maxHealthAttr != null) { + double baseAdult = DefaultAttributes.getSupplier((EntityType) this.getType()).getValue(Attributes.MAX_HEALTH); + double desired = this.isBaby() ? baseAdult * 0.10D : baseAdult; + if (maxHealthAttr.getBaseValue() != desired) { + double oldMax = maxHealthAttr.getBaseValue(); + double healthRatio = this.getHealth() / (float) oldMax; + maxHealthAttr.setBaseValue(desired); + this.setHealth((float) (desired * Mth.clamp(healthRatio, 0.0F, 1.0F))); + } + } + } + + if (!level().isClientSide) { + if (mouthAnimCooldown > 0) { + mouthAnimCooldown--; + } else { + this.triggerAnim("mouthController", "mouth"); + this.playSound(ModSounds.COMPSOGNATHUS_CALL.get(), 1.0F, 1.0F); + // 30s–60s in ticks + mouthAnimCooldown = this.random.nextInt(1200 - 600 + 1) + 600; + } + } + + if (level().isClientSide) { + // Capture previous for smooth interpolation between ticks + this.tailSwayPrev = this.tailSwayOffset; + updateProceduralTailSway(); + } + } + + private void updateProceduralTailSway() { + // Turn input derived from rotation deltas; works even when standing still and turning + float turnDegrees = getSignedTurnDelta(); + + // Deadzone to ignore tiny jitter so the tail can return to center cleanly + float deadzoneDeg = 0.6f; // smaller deadzone for more responsiveness + float turnInput = 0.0f; + if (Math.abs(turnDegrees) >= deadzoneDeg) { + // Higher sensitivity so small in-place turns still affect the model + turnInput = Mth.clamp(turnDegrees / 15.0f, -1.0f, 1.0f); + } + + // Target offset: keep intuitive sign (positive input -> positive sway) + float target = turnInput; + + // One-pole low-pass (no bounce). Larger alpha => snappier and less "stiff". + float alpha = 0.24f; // try 0.20–0.30 to taste + + tailSwayOffset += (target - tailSwayOffset) * alpha; + + // Snap tiny residuals to zero so it visibly settles + if (Math.abs(tailSwayOffset) < 0.003f) { + tailSwayOffset = 0.0f; + } + + // No oscillation velocity retained + tailSwayVelocity = 0.0f; + + tailSwayOffset = Mth.clamp(tailSwayOffset, -1.5f, 1.5f); + } + + // Expose to the model for bone rotation + public float getTailSwayOffset() { + return tailSwayOffset; + } + + // Interpolated sway for smooth rendering between ticks + public float getTailSwayOffset(float partialTick) { + return Mth.lerp(Mth.clamp(partialTick, 0.0f, 1.0f), tailSwayPrev, tailSwayOffset); + } + + /*? if <=1.20.1 {*/ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); + this.entityData.define(VARIANT, 0); + this.entityData.define(DATA_SYNCED_AGE, 0); + } + /*?} else {*/ + /*@Override + protected void defineSynchedData(SynchedEntityData.Builder pBuilder) { + super.defineSynchedData(pBuilder); + pBuilder.define(VARIANT, 0); + pBuilder.define(DATA_SYNCED_AGE, 0); + } + *//*?}*/ + + public int getSyncedAge() { + return this.entityData.get(DATA_SYNCED_AGE); + } + public int getTypeVariant() { + return this.entityData.get(VARIANT); + } + + public CompsognathusVariant getVariant() { + return CompsognathusVariant.byId(this.getTypeVariant() & 255); + } + + private void setVariant(CompsognathusVariant variant) { + this.entityData.set(VARIANT, variant.getId() & 255); + } + + @Override + public boolean canMate(Animal other) { + if (!super.canMate(other)) return false; + if (!(other instanceof CompsognathusEntity that)) return false; + return this.getVariant() != that.getVariant(); + } + @Override + public void addAdditionalSaveData(CompoundTag pCompound) { + super.addAdditionalSaveData(pCompound); + pCompound.putInt("Variant", this.getTypeVariant()); + } + + @Override + public void readAdditionalSaveData(CompoundTag pCompound) { + super.readAdditionalSaveData(pCompound); + this.entityData.set(VARIANT, pCompound.getInt("Variant")); + } + + /*? if <=1.20.1 {*/ + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor pLevel, DifficultyInstance pDifficulty, MobSpawnType pReason, @Nullable SpawnGroupData pSpawnData, @Nullable CompoundTag pDataTag) { + CompsognathusVariant variant = Util.getRandom(CompsognathusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(pLevel, pDifficulty, pReason, pSpawnData, pDataTag); + } + /*?} else {*/ + /*@Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, MobSpawnType spawnType, @Nullable SpawnGroupData spawnGroupData) { + CompsognathusVariant variant = Util.getRandom(CompsognathusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(level, difficulty, spawnType, spawnGroupData); + } + *//*?}*/ + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } + + @Override + protected @Nullable SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.COMPSOGNATHUS_HURT.get(); + } + + @Override + protected @Nullable SoundEvent getDeathSound() { + return ModSounds.COMPSOGNATHUS_DEATH.get(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/custom/ConcavenatorEntity.java b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/ConcavenatorEntity.java new file mode 100644 index 0000000..e95c295 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/ConcavenatorEntity.java @@ -0,0 +1,315 @@ +package net.cmr.jurassicrevived.entity.custom; + +import net.cmr.jurassicrevived.entity.ModEntities; +import net.cmr.jurassicrevived.entity.ai.SprintingMeleeAttackGoal; +import net.cmr.jurassicrevived.entity.ai.SprintingPanicGoal; +import net.cmr.jurassicrevived.entity.client.ConcavenatorVariant; +import net.cmr.jurassicrevived.entity.client.ConcavenatorVariant; +import net.cmr.jurassicrevived.sound.ModSounds; +import net.minecraft.Util; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.util.Mth; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.*; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.attributes.DefaultAttributes; +import net.minecraft.world.entity.ai.goal.*; +import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal; +import net.minecraft.world.entity.ai.goal.target.NearestAttackableTargetGoal; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerLevelAccessor; +import org.jetbrains.annotations.Nullable; +import software.bernie.geckolib.animatable.GeoEntity; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.core.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.core.animation.AnimatableManager; +import software.bernie.geckolib.core.animation.Animation; +import software.bernie.geckolib.core.animation.AnimationController; +import software.bernie.geckolib.core.animation.RawAnimation; +import software.bernie.geckolib.core.object.PlayState; +/*?} else {*/ +/*import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.animation.*; +*//*?}*/ + +public class ConcavenatorEntity extends Animal implements GeoEntity { + private AnimatableInstanceCache cache = new SingletonAnimatableInstanceCache(this); + + private static final EntityDataAccessor VARIANT = + SynchedEntityData.defineId(ConcavenatorEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SYNCED_AGE = + SynchedEntityData.defineId(ConcavenatorEntity.class, EntityDataSerializers.INT); + + // Procedural tail sway state (client-side use for rendering) + private float tailSwayOffset; // Smoothed offset in range roughly [-1, 1] + private float tailSwayVelocity; // Internal velocity for spring-damper + private float tailSwayPrev; // Previous frame value for interpolation + + public ConcavenatorEntity(EntityType pEntityType, Level pLevel) { + super(pEntityType, pLevel); + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new SprintingPanicGoal(this, 1.15) { + @Override + public boolean canUse() { + return ConcavenatorEntity.this.isBaby() && super.canUse(); + } + }); + this.targetSelector.addGoal(1, new HurtByTargetGoal(this).setAlertOthers()); + this.goalSelector.addGoal(2, new FloatGoal(this)); + this.goalSelector.addGoal(3, new AvoidEntityGoal<>(this, TriceratopsEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(4, new AvoidEntityGoal<>(this, SpinosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(5, new AvoidEntityGoal<>(this, TyrannosaurusRexEntity.class, (float) 20, 1.2, 1.2)); + this.goalSelector.addGoal(6, new AvoidEntityGoal<>(this, IndominusRexEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(7, new AvoidEntityGoal<>(this, BrachiosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(8, new SprintingMeleeAttackGoal(this, 1.1, false)); + this.goalSelector.addGoal(9, new BreedGoal(this, 1.0)); + this.goalSelector.addGoal(10, new FollowParentGoal(this, 1.25)); + this.goalSelector.addGoal(11, new WaterAvoidingRandomStrollGoal(this, 1.0)); + this.goalSelector.addGoal(12, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(13, new FollowMobGoal(this, 1, (float) 20, (float) 10)); + this.targetSelector.addGoal(14, new NearestAttackableTargetGoal<>(this, Animal.class, 10, false, false, + target -> target.getType() != this.getType())); + this.targetSelector.addGoal(15, new NearestAttackableTargetGoal(this, GallimimusEntity.class, false, false)); + this.targetSelector.addGoal(16, new NearestAttackableTargetGoal(this, DilophosaurusEntity.class, false, false)); + this.targetSelector.addGoal(17, new NearestAttackableTargetGoal(this, CeratosaurusEntity.class, false, false)); + this.targetSelector.addGoal(18, new NearestAttackableTargetGoal(this, ParasaurolophusEntity.class, false, false)); + this.targetSelector.addGoal(19, new NearestAttackableTargetGoal(this, Player.class, false, false)); + this.targetSelector.addGoal(20, new NearestAttackableTargetGoal(this, CompsognathusEntity.class, false, false)); + this.goalSelector.addGoal(21, new RandomLookAroundGoal(this)); + } + + public static AttributeSupplier.Builder createAttributes() { + return Animal.createLivingAttributes() + .add(Attributes.MAX_HEALTH, 45D) + .add(Attributes.MOVEMENT_SPEED, 0.3D) + .add(Attributes.ARMOR, 0D) + .add(Attributes.FOLLOW_RANGE, 32D) + .add(Attributes.ATTACK_DAMAGE, 14D) + .add(Attributes.KNOCKBACK_RESISTANCE, 0.3D) + .add(Attributes.ATTACK_KNOCKBACK, 0D); + } + + @Override + public boolean isFood(ItemStack pStack) { + return pStack.is(Items.BEEF); + } + + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel pLevel, AgeableMob pOtherParent) { + AgeableMob child = ModEntities.CONCAVENATOR.get().create(pLevel); + if (child instanceof ConcavenatorEntity baby) { + ConcavenatorVariant randomVariant = Util.getRandom(ConcavenatorVariant.values(), this.random); + baby.setVariant(randomVariant); + } + return child; + } + + @Override + public boolean doHurtTarget(Entity target) { + boolean hit = super.doHurtTarget(target); + if (!level().isClientSide && hit && target instanceof LivingEntity) { + if (this.level() instanceof ServerLevel serverLevel) { + this.triggerAnim("attackController", "attack"); + this.playSound(ModSounds.MEDIUM_THEROPOD_ATTACK.get(), 1.0F, 1.0F); + } + } + return hit; + } + + @Override + public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { + controllers.add(new AnimationController<>(this, "Walk/Run/Idle", state -> { + if (state.isMoving()) + return state.setAndContinue(ConcavenatorEntity.this.isSprinting() ? RawAnimation.begin().then("anim.concavenator.run", Animation.LoopType.LOOP) : RawAnimation.begin().then("anim.concavenator.walk", Animation.LoopType.LOOP)); + + return state.setAndContinue(RawAnimation.begin().then("anim.concavenator.idle", Animation.LoopType.LOOP)); + })); + + controllers.add(new AnimationController<>(this, "attackController", state -> PlayState.STOP) + .triggerableAnim("attack", RawAnimation.begin().then("anim.concavenator.attack", Animation.LoopType.PLAY_ONCE))); + + controllers.add(new AnimationController<>(this, "mouthController", state -> PlayState.STOP) + .triggerableAnim("mouth", RawAnimation.begin().then("anim.concavenator.mouth", Animation.LoopType.PLAY_ONCE))); + } + + private float getSignedTurnDelta() { + // Only consider the body (torso) rotation so head look does not affect tail sway + return Mth.wrapDegrees(this.yBodyRot - this.yBodyRotO); + } + + private int mouthAnimCooldown = 0; + + @Override + public void tick() { + super.tick(); + if (!level().isClientSide) { + this.entityData.set(DATA_SYNCED_AGE, this.getAge()); + var maxHealthAttr = getAttribute(Attributes.MAX_HEALTH); + if (maxHealthAttr != null) { + double baseAdult = DefaultAttributes.getSupplier((EntityType) this.getType()).getValue(Attributes.MAX_HEALTH); + double desired = this.isBaby() ? baseAdult * 0.10D : baseAdult; + if (maxHealthAttr.getBaseValue() != desired) { + double oldMax = maxHealthAttr.getBaseValue(); + double healthRatio = this.getHealth() / (float) oldMax; + maxHealthAttr.setBaseValue(desired); + this.setHealth((float) (desired * Mth.clamp(healthRatio, 0.0F, 1.0F))); + } + } + } + + if (!level().isClientSide) { + if (mouthAnimCooldown > 0) { + mouthAnimCooldown--; + } else { + this.triggerAnim("mouthController", "mouth"); + this.playSound(ModSounds.CONCAVENATOR_CALL.get(), 1.0F, 1.0F); + // 30s–60s in ticks + mouthAnimCooldown = this.random.nextInt(1200 - 600 + 1) + 600; + } + } + + if (level().isClientSide) { + // Capture previous for smooth interpolation between ticks + this.tailSwayPrev = this.tailSwayOffset; + updateProceduralTailSway(); + } + } + + private void updateProceduralTailSway() { + // Turn input derived from rotation deltas; works even when standing still and turning + float turnDegrees = getSignedTurnDelta(); + + // Deadzone to ignore tiny jitter so the tail can return to center cleanly + float deadzoneDeg = 0.6f; // smaller deadzone for more responsiveness + float turnInput = 0.0f; + if (Math.abs(turnDegrees) >= deadzoneDeg) { + // Higher sensitivity so small in-place turns still affect the model + turnInput = Mth.clamp(turnDegrees / 15.0f, -1.0f, 1.0f); + } + + // Target offset: keep intuitive sign (positive input -> positive sway) + float target = turnInput; + + // One-pole low-pass (no bounce). Larger alpha => snappier and less "stiff". + float alpha = 0.24f; // try 0.20–0.30 to taste + + tailSwayOffset += (target - tailSwayOffset) * alpha; + + // Snap tiny residuals to zero so it visibly settles + if (Math.abs(tailSwayOffset) < 0.003f) { + tailSwayOffset = 0.0f; + } + + // No oscillation velocity retained + tailSwayVelocity = 0.0f; + + tailSwayOffset = Mth.clamp(tailSwayOffset, -1.5f, 1.5f); + } + + // Expose to the model for bone rotation + public float getTailSwayOffset() { + return tailSwayOffset; + } + + // Interpolated sway for smooth rendering between ticks + public float getTailSwayOffset(float partialTick) { + return Mth.lerp(Mth.clamp(partialTick, 0.0f, 1.0f), tailSwayPrev, tailSwayOffset); + } + + /*? if <=1.20.1 {*/ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); + this.entityData.define(VARIANT, 0); + this.entityData.define(DATA_SYNCED_AGE, 0); + } + /*?} else {*/ + /*@Override + protected void defineSynchedData(SynchedEntityData.Builder pBuilder) { + super.defineSynchedData(pBuilder); + pBuilder.define(VARIANT, 0); + pBuilder.define(DATA_SYNCED_AGE, 0); + } + *//*?}*/ + + public int getSyncedAge() { + return this.entityData.get(DATA_SYNCED_AGE); + } + public int getTypeVariant() { + return this.entityData.get(VARIANT); + } + + public ConcavenatorVariant getVariant() { + return ConcavenatorVariant.byId(this.getTypeVariant() & 255); + } + + private void setVariant(ConcavenatorVariant variant) { + this.entityData.set(VARIANT, variant.getId() & 255); + } + + @Override + public boolean canMate(Animal other) { + if (!super.canMate(other)) return false; + if (!(other instanceof ConcavenatorEntity that)) return false; + return this.getVariant() != that.getVariant(); + } + @Override + public void addAdditionalSaveData(CompoundTag pCompound) { + super.addAdditionalSaveData(pCompound); + pCompound.putInt("Variant", this.getTypeVariant()); + } + + @Override + public void readAdditionalSaveData(CompoundTag pCompound) { + super.readAdditionalSaveData(pCompound); + this.entityData.set(VARIANT, pCompound.getInt("Variant")); + } + + /*? if <=1.20.1 {*/ + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor pLevel, DifficultyInstance pDifficulty, MobSpawnType pReason, @Nullable SpawnGroupData pSpawnData, @Nullable CompoundTag pDataTag) { + ConcavenatorVariant variant = Util.getRandom(ConcavenatorVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(pLevel, pDifficulty, pReason, pSpawnData, pDataTag); + } + /*?} else {*/ + /*@Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, MobSpawnType spawnType, @Nullable SpawnGroupData spawnGroupData) { + ConcavenatorVariant variant = Util.getRandom(ConcavenatorVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(level, difficulty, spawnType, spawnGroupData); + } + *//*?}*/ + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } + + @Override + protected @Nullable SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.CONCAVENATOR_HURT.get(); + } + + @Override + protected @Nullable SoundEvent getDeathSound() { + return ModSounds.CONCAVENATOR_DEATH.get(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/custom/CorythosaurusEntity.java b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/CorythosaurusEntity.java new file mode 100644 index 0000000..a2774fc --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/CorythosaurusEntity.java @@ -0,0 +1,297 @@ +package net.cmr.jurassicrevived.entity.custom; + +import net.cmr.jurassicrevived.entity.ModEntities; +import net.cmr.jurassicrevived.entity.ai.SprintingPanicGoal; +import net.cmr.jurassicrevived.entity.client.CorythosaurusVariant; +import net.cmr.jurassicrevived.entity.client.CorythosaurusVariant; +import net.cmr.jurassicrevived.sound.ModSounds; +import net.minecraft.Util; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.util.Mth; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.*; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.attributes.DefaultAttributes; +import net.minecraft.world.entity.ai.goal.*; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerLevelAccessor; +import org.jetbrains.annotations.Nullable; +import software.bernie.geckolib.animatable.GeoEntity; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.core.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.core.animation.AnimatableManager; +import software.bernie.geckolib.core.animation.Animation; +import software.bernie.geckolib.core.animation.AnimationController; +import software.bernie.geckolib.core.animation.RawAnimation; +import software.bernie.geckolib.core.object.PlayState; +/*?} else {*/ +/*import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.animation.*; +*//*?}*/ + +public class CorythosaurusEntity extends Animal implements GeoEntity { + private AnimatableInstanceCache cache = new SingletonAnimatableInstanceCache(this); + + private static final EntityDataAccessor VARIANT = + SynchedEntityData.defineId(CorythosaurusEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SYNCED_AGE = + SynchedEntityData.defineId(CorythosaurusEntity.class, EntityDataSerializers.INT); + + // Procedural tail sway state (client-side use for rendering) + private float tailSwayOffset; // Smoothed offset in range roughly [-1, 1] + private float tailSwayVelocity; // Internal velocity for spring-damper + private float tailSwayPrev; // Previous frame value for interpolation + + public CorythosaurusEntity(EntityType pEntityType, Level pLevel) { + super(pEntityType, pLevel); + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new SprintingPanicGoal(this, 1.15)); + this.goalSelector.addGoal(2, new FloatGoal(this)); + this.goalSelector.addGoal(3, new BreedGoal(this, 1.0)); + this.goalSelector.addGoal(4, new FollowParentGoal(this, 1.25)); + this.goalSelector.addGoal(5, new WaterAvoidingRandomStrollGoal(this, 1.0)); + this.goalSelector.addGoal(6, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(7, new FollowMobGoal(this, 1, (float) 20, (float) 10)); + this.goalSelector.addGoal(8, new AvoidEntityGoal<>(this, DilophosaurusEntity.class, (float) 20, 1.2, 1.2)); + this.goalSelector.addGoal(9, new AvoidEntityGoal<>(this, VelociraptorEntity.class, (float) 20, 1.2, 1.2)); + this.goalSelector.addGoal(10, new AvoidEntityGoal<>(this, CeratosaurusEntity.class, (float) 20, 1.2, 1.2)); + this.goalSelector.addGoal(11, new AvoidEntityGoal<>(this, SpinosaurusEntity.class, (float) 20, 1.2, 1.2)); + this.goalSelector.addGoal(12, new AvoidEntityGoal<>(this, TyrannosaurusRexEntity.class, (float) 20, 1.2, 1.2)); + this.goalSelector.addGoal(13, new RandomLookAroundGoal(this)); + + } + + public static AttributeSupplier.Builder createAttributes() { + return Animal.createLivingAttributes() + .add(Attributes.MAX_HEALTH, 55D) + .add(Attributes.MOVEMENT_SPEED, 0.3D) + .add(Attributes.ARMOR, 0D) + .add(Attributes.FOLLOW_RANGE, 32D) + .add(Attributes.KNOCKBACK_RESISTANCE, 0D) + .add(Attributes.ATTACK_KNOCKBACK, 0D) + .add(Attributes.ATTACK_DAMAGE, 12D); + } + + @Override + public boolean isFood(ItemStack pStack) { + return pStack.is(Items.KELP); + } + + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel pLevel, AgeableMob pOtherParent) { + AgeableMob child = ModEntities.CORYTHOSAURUS.get().create(pLevel); + if (child instanceof CorythosaurusEntity baby) { + CorythosaurusVariant randomVariant = Util.getRandom(CorythosaurusVariant.values(), this.random); + baby.setVariant(randomVariant); + } + return child; + } + + @Override + public boolean doHurtTarget(Entity target) { + boolean hit = super.doHurtTarget(target); + if (!level().isClientSide && hit && target instanceof LivingEntity) { + if (this.level() instanceof ServerLevel serverLevel) { + this.triggerAnim("attackController", "attack"); + } + } + return hit; + } + + @Override + public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { + controllers.add(new AnimationController<>(this, "Walk/Run/Idle", state -> { + if (state.isMoving()) + return state.setAndContinue(CorythosaurusEntity.this.isSprinting() ? RawAnimation.begin().then("anim.corythosaurus.run", Animation.LoopType.LOOP) : RawAnimation.begin().then("anim.corythosaurus.walk", Animation.LoopType.LOOP)); + + return state.setAndContinue(RawAnimation.begin().then("anim.corythosaurus.idle", Animation.LoopType.LOOP)); + })); + + controllers.add(new AnimationController<>(this, "attackController", state -> PlayState.STOP) + .triggerableAnim("attack", RawAnimation.begin().then("anim.corythosaurus.attack", Animation.LoopType.PLAY_ONCE))); + + controllers.add(new AnimationController<>(this, "mouthController", state -> PlayState.STOP) + .triggerableAnim("mouth", RawAnimation.begin().then("anim.corythosaurus.mouth", Animation.LoopType.PLAY_ONCE))); + } + + private float getSignedTurnDelta() { + // Only consider the body (torso) rotation so head look does not affect tail sway + return Mth.wrapDegrees(this.yBodyRot - this.yBodyRotO); + } + + private int mouthAnimCooldown = 0; + + @Override + public void tick() { + super.tick(); + if (!level().isClientSide) { + this.entityData.set(DATA_SYNCED_AGE, this.getAge()); + var maxHealthAttr = getAttribute(Attributes.MAX_HEALTH); + if (maxHealthAttr != null) { + double baseAdult = DefaultAttributes.getSupplier((EntityType) this.getType()).getValue(Attributes.MAX_HEALTH); + double desired = this.isBaby() ? baseAdult * 0.10D : baseAdult; + if (maxHealthAttr.getBaseValue() != desired) { + double oldMax = maxHealthAttr.getBaseValue(); + double healthRatio = this.getHealth() / (float) oldMax; + maxHealthAttr.setBaseValue(desired); + this.setHealth((float) (desired * Mth.clamp(healthRatio, 0.0F, 1.0F))); + } + } + } + + if (!level().isClientSide) { + if (mouthAnimCooldown > 0) { + mouthAnimCooldown--; + } else { + this.triggerAnim("mouthController", "mouth"); + this.playSound(ModSounds.CORYTHOSAURUS_CALL.get(), 1.0F, 1.0F); + // 30s–60s in ticks + mouthAnimCooldown = this.random.nextInt(1200 - 600 + 1) + 600; + } + } + + if (level().isClientSide) { + // Capture previous for smooth interpolation between ticks + this.tailSwayPrev = this.tailSwayOffset; + updateProceduralTailSway(); + } + } + + private void updateProceduralTailSway() { + // Turn input derived from rotation deltas; works even when standing still and turning + float turnDegrees = getSignedTurnDelta(); + + // Deadzone to ignore tiny jitter so the tail can return to center cleanly + float deadzoneDeg = 0.6f; // smaller deadzone for more responsiveness + float turnInput = 0.0f; + if (Math.abs(turnDegrees) >= deadzoneDeg) { + // Higher sensitivity so small in-place turns still affect the model + turnInput = Mth.clamp(turnDegrees / 15.0f, -1.0f, 1.0f); + } + + // Target offset: keep intuitive sign (positive input -> positive sway) + float target = turnInput; + + // One-pole low-pass (no bounce). Larger alpha => snappier and less "stiff". + float alpha = 0.24f; // try 0.20–0.30 to taste + + tailSwayOffset += (target - tailSwayOffset) * alpha; + + // Snap tiny residuals to zero so it visibly settles + if (Math.abs(tailSwayOffset) < 0.003f) { + tailSwayOffset = 0.0f; + } + + // No oscillation velocity retained + tailSwayVelocity = 0.0f; + + tailSwayOffset = Mth.clamp(tailSwayOffset, -1.5f, 1.5f); + } + + // Expose to the model for bone rotation + public float getTailSwayOffset() { + return tailSwayOffset; + } + + // Interpolated sway for smooth rendering between ticks + public float getTailSwayOffset(float partialTick) { + return Mth.lerp(Mth.clamp(partialTick, 0.0f, 1.0f), tailSwayPrev, tailSwayOffset); + } + + /*? if <=1.20.1 {*/ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); + this.entityData.define(VARIANT, 0); + this.entityData.define(DATA_SYNCED_AGE, 0); + } + /*?} else {*/ + /*@Override + protected void defineSynchedData(SynchedEntityData.Builder pBuilder) { + super.defineSynchedData(pBuilder); + pBuilder.define(VARIANT, 0); + pBuilder.define(DATA_SYNCED_AGE, 0); + } + *//*?}*/ + + public int getSyncedAge() { + return this.entityData.get(DATA_SYNCED_AGE); + } + public int getTypeVariant() { + return this.entityData.get(VARIANT); + } + + public CorythosaurusVariant getVariant() { + return CorythosaurusVariant.byId(this.getTypeVariant() & 255); + } + + private void setVariant(CorythosaurusVariant variant) { + this.entityData.set(VARIANT, variant.getId() & 255); + } + + @Override + public boolean canMate(Animal other) { + if (!super.canMate(other)) return false; + if (!(other instanceof CorythosaurusEntity that)) return false; + return this.getVariant() != that.getVariant(); + } + @Override + public void addAdditionalSaveData(CompoundTag pCompound) { + super.addAdditionalSaveData(pCompound); + pCompound.putInt("Variant", this.getTypeVariant()); + } + + @Override + public void readAdditionalSaveData(CompoundTag pCompound) { + super.readAdditionalSaveData(pCompound); + this.entityData.set(VARIANT, pCompound.getInt("Variant")); + } + + /*? if <=1.20.1 {*/ + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor pLevel, DifficultyInstance pDifficulty, MobSpawnType pReason, @Nullable SpawnGroupData pSpawnData, @Nullable CompoundTag pDataTag) { + CorythosaurusVariant variant = Util.getRandom(CorythosaurusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(pLevel, pDifficulty, pReason, pSpawnData, pDataTag); + } + /*?} else {*/ + /*@Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, MobSpawnType spawnType, @Nullable SpawnGroupData spawnGroupData) { + CorythosaurusVariant variant = Util.getRandom(CorythosaurusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(level, difficulty, spawnType, spawnGroupData); + } + *//*?}*/ + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } + + @Override + protected @Nullable SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.CORYTHOSAURUS_HURT.get(); + } + + @Override + protected @Nullable SoundEvent getDeathSound() { + return ModSounds.CORYTHOSAURUS_DEATH.get(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/custom/DeinonychusEntity.java b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/DeinonychusEntity.java new file mode 100644 index 0000000..67d1c93 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/DeinonychusEntity.java @@ -0,0 +1,314 @@ +package net.cmr.jurassicrevived.entity.custom; + +import net.cmr.jurassicrevived.entity.ModEntities; +import net.cmr.jurassicrevived.entity.ai.SprintingMeleeAttackGoal; +import net.cmr.jurassicrevived.entity.ai.SprintingPanicGoal; +import net.cmr.jurassicrevived.entity.client.DeinonychusVariant; +import net.cmr.jurassicrevived.entity.client.DeinonychusVariant; +import net.cmr.jurassicrevived.sound.ModSounds; +import net.minecraft.Util; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.util.Mth; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.*; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.attributes.DefaultAttributes; +import net.minecraft.world.entity.ai.goal.*; +import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal; +import net.minecraft.world.entity.ai.goal.target.NearestAttackableTargetGoal; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerLevelAccessor; +import org.jetbrains.annotations.Nullable; +import software.bernie.geckolib.animatable.GeoEntity; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.core.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.core.animation.AnimatableManager; +import software.bernie.geckolib.core.animation.Animation; +import software.bernie.geckolib.core.animation.AnimationController; +import software.bernie.geckolib.core.animation.RawAnimation; +import software.bernie.geckolib.core.object.PlayState; +/*?} else {*/ +/*import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.animation.*; +*//*?}*/ + +public class DeinonychusEntity extends Animal implements GeoEntity { + private AnimatableInstanceCache cache = new SingletonAnimatableInstanceCache(this); + + private static final EntityDataAccessor VARIANT = + SynchedEntityData.defineId(DeinonychusEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SYNCED_AGE = + SynchedEntityData.defineId(DeinonychusEntity.class, EntityDataSerializers.INT); + + // Procedural tail sway state (client-side use for rendering) + private float tailSwayOffset; // Smoothed offset in range roughly [-1, 1] + private float tailSwayVelocity; // Internal velocity for spring-damper + private float tailSwayPrev; // Previous frame value for interpolation + + public DeinonychusEntity(EntityType pEntityType, Level pLevel) { + super(pEntityType, pLevel); + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new SprintingPanicGoal(this, 1.15) { + @Override + public boolean canUse() { + return DeinonychusEntity.this.isBaby() && super.canUse(); + } + }); + this.targetSelector.addGoal(1, new HurtByTargetGoal(this).setAlertOthers()); + this.goalSelector.addGoal(2, new FloatGoal(this)); + this.goalSelector.addGoal(3, new AvoidEntityGoal<>(this, TriceratopsEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(4, new AvoidEntityGoal<>(this, SpinosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(5, new AvoidEntityGoal<>(this, TyrannosaurusRexEntity.class, (float) 20, 1.2, 1.2)); + this.goalSelector.addGoal(6, new AvoidEntityGoal<>(this, IndominusRexEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(7, new AvoidEntityGoal<>(this, BrachiosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(8, new SprintingMeleeAttackGoal(this, 1.1, false)); + this.goalSelector.addGoal(9, new BreedGoal(this, 1.0)); + this.goalSelector.addGoal(10, new FollowParentGoal(this, 1.25)); + this.goalSelector.addGoal(11, new WaterAvoidingRandomStrollGoal(this, 1.0)); + this.goalSelector.addGoal(12, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(13, new FollowMobGoal(this, 1, (float) 20, (float) 10)); + this.targetSelector.addGoal(14, new NearestAttackableTargetGoal<>(this, Animal.class, 10, false, false, + target -> target.getType() != this.getType())); + this.targetSelector.addGoal(15, new NearestAttackableTargetGoal(this, GallimimusEntity.class, false, false)); + this.targetSelector.addGoal(16, new NearestAttackableTargetGoal(this, DilophosaurusEntity.class, false, false)); + this.targetSelector.addGoal(17, new NearestAttackableTargetGoal(this, CeratosaurusEntity.class, false, false)); + this.targetSelector.addGoal(18, new NearestAttackableTargetGoal(this, ParasaurolophusEntity.class, false, false)); + this.targetSelector.addGoal(19, new NearestAttackableTargetGoal(this, Player.class, false, false)); + this.targetSelector.addGoal(20, new NearestAttackableTargetGoal(this, CompsognathusEntity.class, false, false)); + this.goalSelector.addGoal(21, new RandomLookAroundGoal(this)); + } + + public static AttributeSupplier.Builder createAttributes() { + return Animal.createLivingAttributes() + .add(Attributes.MAX_HEALTH, 30D) + .add(Attributes.MOVEMENT_SPEED, 0.3D) + .add(Attributes.ARMOR, 0D) + .add(Attributes.FOLLOW_RANGE, 32D) + .add(Attributes.ATTACK_DAMAGE, 8D) + .add(Attributes.KNOCKBACK_RESISTANCE, 0.3D) + .add(Attributes.ATTACK_KNOCKBACK, 0D); + } + + @Override + public boolean isFood(ItemStack pStack) { + return pStack.is(Items.BEEF); + } + + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel pLevel, AgeableMob pOtherParent) { + AgeableMob child = ModEntities.DEINONYCHUS.get().create(pLevel); + if (child instanceof DeinonychusEntity baby) { + DeinonychusVariant randomVariant = Util.getRandom(DeinonychusVariant.values(), this.random); + baby.setVariant(randomVariant); + } + return child; + } + + @Override + public boolean doHurtTarget(Entity target) { + boolean hit = super.doHurtTarget(target); + if (!level().isClientSide && hit && target instanceof LivingEntity) { + if (this.level() instanceof ServerLevel serverLevel) { + this.triggerAnim("attackController", "attack"); + } + } + return hit; + } + + @Override + public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { + controllers.add(new AnimationController<>(this, "Walk/Run/Idle", state -> { + if (state.isMoving()) + return state.setAndContinue(DeinonychusEntity.this.isSprinting() ? RawAnimation.begin().then("anim.deinonychus.run", Animation.LoopType.LOOP) : RawAnimation.begin().then("anim.deinonychus.walk", Animation.LoopType.LOOP)); + + return state.setAndContinue(RawAnimation.begin().then("anim.deinonychus.idle", Animation.LoopType.LOOP)); + })); + + controllers.add(new AnimationController<>(this, "attackController", state -> PlayState.STOP) + .triggerableAnim("attack", RawAnimation.begin().then("anim.deinonychus.attack", Animation.LoopType.PLAY_ONCE))); + + controllers.add(new AnimationController<>(this, "mouthController", state -> PlayState.STOP) + .triggerableAnim("mouth", RawAnimation.begin().then("anim.deinonychus.mouth", Animation.LoopType.PLAY_ONCE))); + } + + private float getSignedTurnDelta() { + // Only consider the body (torso) rotation so head look does not affect tail sway + return Mth.wrapDegrees(this.yBodyRot - this.yBodyRotO); + } + + private int mouthAnimCooldown = 0; + + @Override + public void tick() { + super.tick(); + if (!level().isClientSide) { + this.entityData.set(DATA_SYNCED_AGE, this.getAge()); + var maxHealthAttr = getAttribute(Attributes.MAX_HEALTH); + if (maxHealthAttr != null) { + double baseAdult = DefaultAttributes.getSupplier((EntityType) this.getType()).getValue(Attributes.MAX_HEALTH); + double desired = this.isBaby() ? baseAdult * 0.10D : baseAdult; + if (maxHealthAttr.getBaseValue() != desired) { + double oldMax = maxHealthAttr.getBaseValue(); + double healthRatio = this.getHealth() / (float) oldMax; + maxHealthAttr.setBaseValue(desired); + this.setHealth((float) (desired * Mth.clamp(healthRatio, 0.0F, 1.0F))); + } + } + } + + if (!level().isClientSide) { + if (mouthAnimCooldown > 0) { + mouthAnimCooldown--; + } else { + this.triggerAnim("mouthController", "mouth"); + this.playSound(ModSounds.DEINONYCHUS_CALL.get(), 1.0F, 1.0F); + // 30s–60s in ticks + mouthAnimCooldown = this.random.nextInt(1200 - 600 + 1) + 600; + } + } + + if (level().isClientSide) { + // Capture previous for smooth interpolation between ticks + this.tailSwayPrev = this.tailSwayOffset; + updateProceduralTailSway(); + } + } + + private void updateProceduralTailSway() { + // Turn input derived from rotation deltas; works even when standing still and turning + float turnDegrees = getSignedTurnDelta(); + + // Deadzone to ignore tiny jitter so the tail can return to center cleanly + float deadzoneDeg = 0.6f; // smaller deadzone for more responsiveness + float turnInput = 0.0f; + if (Math.abs(turnDegrees) >= deadzoneDeg) { + // Higher sensitivity so small in-place turns still affect the model + turnInput = Mth.clamp(turnDegrees / 15.0f, -1.0f, 1.0f); + } + + // Target offset: keep intuitive sign (positive input -> positive sway) + float target = turnInput; + + // One-pole low-pass (no bounce). Larger alpha => snappier and less "stiff". + float alpha = 0.24f; // try 0.20–0.30 to taste + + tailSwayOffset += (target - tailSwayOffset) * alpha; + + // Snap tiny residuals to zero so it visibly settles + if (Math.abs(tailSwayOffset) < 0.003f) { + tailSwayOffset = 0.0f; + } + + // No oscillation velocity retained + tailSwayVelocity = 0.0f; + + tailSwayOffset = Mth.clamp(tailSwayOffset, -1.5f, 1.5f); + } + + // Expose to the model for bone rotation + public float getTailSwayOffset() { + return tailSwayOffset; + } + + // Interpolated sway for smooth rendering between ticks + public float getTailSwayOffset(float partialTick) { + return Mth.lerp(Mth.clamp(partialTick, 0.0f, 1.0f), tailSwayPrev, tailSwayOffset); + } + + /*? if <=1.20.1 {*/ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); + this.entityData.define(VARIANT, 0); + this.entityData.define(DATA_SYNCED_AGE, 0); + } + /*?} else {*/ + /*@Override + protected void defineSynchedData(SynchedEntityData.Builder pBuilder) { + super.defineSynchedData(pBuilder); + pBuilder.define(VARIANT, 0); + pBuilder.define(DATA_SYNCED_AGE, 0); + } + *//*?}*/ + + public int getSyncedAge() { + return this.entityData.get(DATA_SYNCED_AGE); + } + public int getTypeVariant() { + return this.entityData.get(VARIANT); + } + + public DeinonychusVariant getVariant() { + return DeinonychusVariant.byId(this.getTypeVariant() & 255); + } + + private void setVariant(DeinonychusVariant variant) { + this.entityData.set(VARIANT, variant.getId() & 255); + } + + @Override + public boolean canMate(Animal other) { + if (!super.canMate(other)) return false; + if (!(other instanceof DeinonychusEntity that)) return false; + return this.getVariant() != that.getVariant(); + } + @Override + public void addAdditionalSaveData(CompoundTag pCompound) { + super.addAdditionalSaveData(pCompound); + pCompound.putInt("Variant", this.getTypeVariant()); + } + + @Override + public void readAdditionalSaveData(CompoundTag pCompound) { + super.readAdditionalSaveData(pCompound); + this.entityData.set(VARIANT, pCompound.getInt("Variant")); + } + + /*? if <=1.20.1 {*/ + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor pLevel, DifficultyInstance pDifficulty, MobSpawnType pReason, @Nullable SpawnGroupData pSpawnData, @Nullable CompoundTag pDataTag) { + DeinonychusVariant variant = Util.getRandom(DeinonychusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(pLevel, pDifficulty, pReason, pSpawnData, pDataTag); + } + /*?} else {*/ + /*@Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, MobSpawnType spawnType, @Nullable SpawnGroupData spawnGroupData) { + DeinonychusVariant variant = Util.getRandom(DeinonychusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(level, difficulty, spawnType, spawnGroupData); + } + *//*?}*/ + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } + + @Override + protected @Nullable SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.DEINONYCHUS_HURT.get(); + } + + @Override + protected @Nullable SoundEvent getDeathSound() { + return ModSounds.DEINONYCHUS_DEATH.get(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/custom/DilophosaurusEntity.java b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/DilophosaurusEntity.java new file mode 100644 index 0000000..fa14f26 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/DilophosaurusEntity.java @@ -0,0 +1,314 @@ +package net.cmr.jurassicrevived.entity.custom; + +import net.cmr.jurassicrevived.entity.ModEntities; +import net.cmr.jurassicrevived.entity.ai.SprintingMeleeAttackGoal; +import net.cmr.jurassicrevived.entity.ai.SprintingPanicGoal; +import net.cmr.jurassicrevived.entity.client.DilophosaurusVariant; +import net.cmr.jurassicrevived.entity.client.DilophosaurusVariant; +import net.cmr.jurassicrevived.sound.ModSounds; +import net.minecraft.Util; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.util.Mth; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.*; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.attributes.DefaultAttributes; +import net.minecraft.world.entity.ai.goal.*; +import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal; +import net.minecraft.world.entity.ai.goal.target.NearestAttackableTargetGoal; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerLevelAccessor; +import org.jetbrains.annotations.Nullable; +import software.bernie.geckolib.animatable.GeoEntity; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.core.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.core.animation.AnimatableManager; +import software.bernie.geckolib.core.animation.Animation; +import software.bernie.geckolib.core.animation.AnimationController; +import software.bernie.geckolib.core.animation.RawAnimation; +import software.bernie.geckolib.core.object.PlayState; +/*?} else {*/ +/*import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.animation.*; +*//*?}*/ + +public class DilophosaurusEntity extends Animal implements GeoEntity { + private AnimatableInstanceCache cache = new SingletonAnimatableInstanceCache(this); + + private static final EntityDataAccessor VARIANT = + SynchedEntityData.defineId(DilophosaurusEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SYNCED_AGE = + SynchedEntityData.defineId(DilophosaurusEntity.class, EntityDataSerializers.INT); + + // Procedural tail sway state (client-side use for rendering) + private float tailSwayOffset; // Smoothed offset in range roughly [-1, 1] + private float tailSwayVelocity; // Internal velocity for spring-damper + private float tailSwayPrev; // Previous frame value for interpolation + + public DilophosaurusEntity(EntityType pEntityType, Level pLevel) { + super(pEntityType, pLevel); + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new SprintingPanicGoal(this, 1.15) { + @Override + public boolean canUse() { + return DilophosaurusEntity.this.isBaby() && super.canUse(); + } + }); + this.targetSelector.addGoal(1, new HurtByTargetGoal(this).setAlertOthers()); + this.goalSelector.addGoal(2, new FloatGoal(this)); + this.goalSelector.addGoal(3, new AvoidEntityGoal<>(this, SpinosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(4, new AvoidEntityGoal<>(this, TyrannosaurusRexEntity.class, (float) 20, 1.2, 1.2)); + this.goalSelector.addGoal(5, new AvoidEntityGoal<>(this, VelociraptorEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(6, new AvoidEntityGoal<>(this, ParasaurolophusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(7, new AvoidEntityGoal<>(this, CeratosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(8, new AvoidEntityGoal<>(this, TriceratopsEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(9, new AvoidEntityGoal<>(this, BrachiosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(10, new SprintingMeleeAttackGoal(this, 1.1, false)); + this.goalSelector.addGoal(11, new BreedGoal(this, 1.0)); + this.goalSelector.addGoal(12, new FollowParentGoal(this, 1.25)); + this.goalSelector.addGoal(13, new WaterAvoidingRandomStrollGoal(this, 1.0)); + this.goalSelector.addGoal(14, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(15, new FollowMobGoal(this, 1, (float) 20, (float) 10)); + this.targetSelector.addGoal(16, new NearestAttackableTargetGoal<>(this, Animal.class, 10, false, false, + target -> target.getType() != this.getType())); + this.targetSelector.addGoal(17, new NearestAttackableTargetGoal(this, GallimimusEntity.class, false, false)); + this.targetSelector.addGoal(18, new NearestAttackableTargetGoal(this, Player.class, false, false)); + this.targetSelector.addGoal(19, new NearestAttackableTargetGoal(this, CompsognathusEntity.class, false, false)); + this.goalSelector.addGoal(20, new RandomLookAroundGoal(this)); + } + + public static AttributeSupplier.Builder createAttributes() { + return Animal.createLivingAttributes() + .add(Attributes.MAX_HEALTH, 30D) + .add(Attributes.MOVEMENT_SPEED, 0.3D) + .add(Attributes.ARMOR, 0D) + .add(Attributes.FOLLOW_RANGE, 32D) + .add(Attributes.KNOCKBACK_RESISTANCE, 0.3D) + .add(Attributes.ATTACK_KNOCKBACK, 0D) + .add(Attributes.ATTACK_DAMAGE, 5D); + } + + @Override + public boolean isFood(ItemStack pStack) { + return pStack.is(Items.BEEF); + } + + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel pLevel, AgeableMob pOtherParent) { + AgeableMob child = ModEntities.DILOPHOSAURUS.get().create(pLevel); + if (child instanceof DilophosaurusEntity baby) { + DilophosaurusVariant randomVariant = Util.getRandom(DilophosaurusVariant.values(), this.random); + baby.setVariant(randomVariant); + } + return child; + } + + @Override + public boolean doHurtTarget(Entity target) { + boolean hit = super.doHurtTarget(target); + if (!level().isClientSide && hit && target instanceof LivingEntity) { + if (this.level() instanceof ServerLevel serverLevel) { + this.triggerAnim("attackController", "attack"); + this.playSound(ModSounds.DILOPHOSAURUS_ATTACK.get(), 1.0F, 1.0F); + } + } + return hit; + } + + @Override + public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { + controllers.add(new AnimationController<>(this, "Walk/Run/Idle", state -> { + if (state.isMoving()) + return state.setAndContinue(DilophosaurusEntity.this.isSprinting() ? RawAnimation.begin().then("anim.dilophosaurus.run", Animation.LoopType.LOOP) : RawAnimation.begin().then("anim.dilophosaurus.walk", Animation.LoopType.LOOP)); + + return state.setAndContinue(RawAnimation.begin().then("anim.dilophosaurus.idle", Animation.LoopType.LOOP)); + })); + + controllers.add(new AnimationController<>(this, "attackController", state -> PlayState.STOP) + .triggerableAnim("attack", RawAnimation.begin().then("anim.dilophosaurus.attack", Animation.LoopType.PLAY_ONCE))); + + controllers.add(new AnimationController<>(this, "mouthController", state -> PlayState.STOP) + .triggerableAnim("mouth", RawAnimation.begin().then("anim.dilophosaurus.mouth", Animation.LoopType.PLAY_ONCE))); + } + + private float getSignedTurnDelta() { + // Only consider the body (torso) rotation so head look does not affect tail sway + return Mth.wrapDegrees(this.yBodyRot - this.yBodyRotO); + } + + private int mouthAnimCooldown = 0; + + @Override + public void tick() { + super.tick(); + if (!level().isClientSide) { + this.entityData.set(DATA_SYNCED_AGE, this.getAge()); + var maxHealthAttr = getAttribute(Attributes.MAX_HEALTH); + if (maxHealthAttr != null) { + double baseAdult = DefaultAttributes.getSupplier((EntityType) this.getType()).getValue(Attributes.MAX_HEALTH); + double desired = this.isBaby() ? baseAdult * 0.10D : baseAdult; + if (maxHealthAttr.getBaseValue() != desired) { + double oldMax = maxHealthAttr.getBaseValue(); + double healthRatio = this.getHealth() / (float) oldMax; + maxHealthAttr.setBaseValue(desired); + this.setHealth((float) (desired * Mth.clamp(healthRatio, 0.0F, 1.0F))); + } + } + } + + if (!level().isClientSide) { + if (mouthAnimCooldown > 0) { + mouthAnimCooldown--; + } else { + this.triggerAnim("mouthController", "mouth"); + this.playSound(ModSounds.DILOPHOSAURUS_CALL.get(), 1.0F, 1.0F); + // 30s–60s in ticks + mouthAnimCooldown = this.random.nextInt(1200 - 600 + 1) + 600; + } + } + + if (level().isClientSide) { + // Capture previous for smooth interpolation between ticks + this.tailSwayPrev = this.tailSwayOffset; + updateProceduralTailSway(); + } + } + + private void updateProceduralTailSway() { + // Turn input derived from rotation deltas; works even when standing still and turning + float turnDegrees = getSignedTurnDelta(); + + // Deadzone to ignore tiny jitter so the tail can return to center cleanly + float deadzoneDeg = 0.6f; // smaller deadzone for more responsiveness + float turnInput = 0.0f; + if (Math.abs(turnDegrees) >= deadzoneDeg) { + // Higher sensitivity so small in-place turns still affect the model + turnInput = Mth.clamp(turnDegrees / 15.0f, -1.0f, 1.0f); + } + + // Target offset: keep intuitive sign (positive input -> positive sway) + float target = turnInput; + + // One-pole low-pass (no bounce). Larger alpha => snappier and less "stiff". + float alpha = 0.24f; // try 0.20–0.30 to taste + + tailSwayOffset += (target - tailSwayOffset) * alpha; + + // Snap tiny residuals to zero so it visibly settles + if (Math.abs(tailSwayOffset) < 0.003f) { + tailSwayOffset = 0.0f; + } + + // No oscillation velocity retained + tailSwayVelocity = 0.0f; + + tailSwayOffset = Mth.clamp(tailSwayOffset, -1.5f, 1.5f); + } + + // Expose to the model for bone rotation + public float getTailSwayOffset() { + return tailSwayOffset; + } + + // Interpolated sway for smooth rendering between ticks + public float getTailSwayOffset(float partialTick) { + return Mth.lerp(Mth.clamp(partialTick, 0.0f, 1.0f), tailSwayPrev, tailSwayOffset); + } + + /*? if <=1.20.1 {*/ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); + this.entityData.define(VARIANT, 0); + this.entityData.define(DATA_SYNCED_AGE, 0); + } + /*?} else {*/ + /*@Override + protected void defineSynchedData(SynchedEntityData.Builder pBuilder) { + super.defineSynchedData(pBuilder); + pBuilder.define(VARIANT, 0); + pBuilder.define(DATA_SYNCED_AGE, 0); + } + *//*?}*/ + + public int getSyncedAge() { + return this.entityData.get(DATA_SYNCED_AGE); + } + public int getTypeVariant() { + return this.entityData.get(VARIANT); + } + + public DilophosaurusVariant getVariant() { + return DilophosaurusVariant.byId(this.getTypeVariant() & 255); + } + + private void setVariant(DilophosaurusVariant variant) { + this.entityData.set(VARIANT, variant.getId() & 255); + } + + @Override + public boolean canMate(Animal other) { + if (!super.canMate(other)) return false; + if (!(other instanceof DilophosaurusEntity that)) return false; + return this.getVariant() != that.getVariant(); + } + @Override + public void addAdditionalSaveData(CompoundTag pCompound) { + super.addAdditionalSaveData(pCompound); + pCompound.putInt("Variant", this.getTypeVariant()); + } + + @Override + public void readAdditionalSaveData(CompoundTag pCompound) { + super.readAdditionalSaveData(pCompound); + this.entityData.set(VARIANT, pCompound.getInt("Variant")); + } + + /*? if <=1.20.1 {*/ + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor pLevel, DifficultyInstance pDifficulty, MobSpawnType pReason, @Nullable SpawnGroupData pSpawnData, @Nullable CompoundTag pDataTag) { + DilophosaurusVariant variant = Util.getRandom(DilophosaurusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(pLevel, pDifficulty, pReason, pSpawnData, pDataTag); + } + /*?} else {*/ + /*@Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, MobSpawnType spawnType, @Nullable SpawnGroupData spawnGroupData) { + DilophosaurusVariant variant = Util.getRandom(DilophosaurusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(level, difficulty, spawnType, spawnGroupData); + } + *//*?}*/ + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } + + @Override + protected @Nullable SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.DILOPHOSAURUS_HURT.get(); + } + + @Override + protected @Nullable SoundEvent getDeathSound() { + return ModSounds.DILOPHOSAURUS_DEATH.get(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/custom/DimorphodonEntity.java b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/DimorphodonEntity.java new file mode 100644 index 0000000..44e32f0 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/DimorphodonEntity.java @@ -0,0 +1,410 @@ +package net.cmr.jurassicrevived.entity.custom; + +import net.cmr.jurassicrevived.entity.ModEntities; +import net.cmr.jurassicrevived.entity.ai.SprintingMeleeAttackGoal; +import net.cmr.jurassicrevived.entity.ai.SprintingPanicGoal; +import net.cmr.jurassicrevived.entity.client.DimorphodonVariant; +import net.cmr.jurassicrevived.entity.client.DimorphodonVariant; +import net.cmr.jurassicrevived.sound.ModSounds; +import net.minecraft.Util; +import net.minecraft.core.BlockPos; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.*; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.attributes.DefaultAttributes; +import net.minecraft.world.entity.ai.control.FlyingMoveControl; +import net.minecraft.world.entity.ai.goal.*; +import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal; +import net.minecraft.world.entity.ai.goal.target.NearestAttackableTargetGoal; +import net.minecraft.world.entity.ai.navigation.FlyingPathNavigation; +import net.minecraft.world.entity.ai.navigation.PathNavigation; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.animal.FlyingAnimal; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerLevelAccessor; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.phys.Vec3; +import org.jetbrains.annotations.Nullable; +import software.bernie.geckolib.animatable.GeoEntity; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.core.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.core.animation.AnimatableManager; +import software.bernie.geckolib.core.animation.Animation; +import software.bernie.geckolib.core.animation.AnimationController; +import software.bernie.geckolib.core.animation.RawAnimation; +import software.bernie.geckolib.core.object.PlayState; +/*?} else {*/ +/*import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.animation.*; +*//*?}*/ + +public class DimorphodonEntity extends Animal implements GeoEntity, FlyingAnimal { + private final AnimatableInstanceCache cache = new SingletonAnimatableInstanceCache(this); + + private static final EntityDataAccessor VARIANT = + SynchedEntityData.defineId(DimorphodonEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SYNCED_AGE = + SynchedEntityData.defineId(DimorphodonEntity.class, EntityDataSerializers.INT); + + // Procedural tail sway state (client-side use for rendering) + private float tailSwayOffset; // Smoothed offset in range roughly [-1, 1] + private float tailSwayVelocity; // Internal velocity for spring-damper + private float tailSwayPrev; // Previous frame value for interpolation + + public DimorphodonEntity(EntityType pEntityType, Level pLevel) { + super(pEntityType, pLevel); + this.moveControl = new FlyingMoveControl(this, 20, true); + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new SprintingPanicGoal(this, 1.15) { + @Override + public boolean canUse() { + return DimorphodonEntity.this.isBaby() && super.canUse(); + } + }); + this.goalSelector.addGoal(2, new FloatGoal(this)); + this.goalSelector.addGoal(3, new AvoidEntityGoal<>(this, BrachiosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(4, new SprintingMeleeAttackGoal(this, 1.1, false)); + this.goalSelector.addGoal(5, new BreedGoal(this, 1.0)); + this.goalSelector.addGoal(6, new FollowParentGoal(this, 1.25)); + + // Goal 7: Wander on ground (Walk) - Only when on ground + this.goalSelector.addGoal(7, new WaterAvoidingRandomStrollGoal(this, 1.0) { + @Override + public boolean canUse() { + return DimorphodonEntity.this.onGround() && super.canUse(); + } + }); + + // Goal 8: Wander in air (Fly) - Handles takeoff, flying, and landing + this.goalSelector.addGoal(8, new WaterAvoidingRandomStrollGoal(this, 1.0, 20) { + @Override + public boolean canUse() { + boolean isFlying = !DimorphodonEntity.this.onGround(); + // If flying, keep flying. If on ground, small chance (1/400 ticks) to take off. + return (isFlying || DimorphodonEntity.this.getRandom().nextInt(400) == 0) && super.canUse(); + } + + @Override + protected Vec3 getPosition() { + Vec3 pos = DimorphodonEntity.this.position(); + RandomSource random = DimorphodonEntity.this.getRandom(); + + double x = pos.x + (random.nextFloat() * 2 - 1) * 32; + double z = pos.z + (random.nextFloat() * 2 - 1) * 32; + + // Get ground height at the random destination (returns Y of first air block) + int groundY = DimorphodonEntity.this.level().getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, (int)x, (int)z); + double y; + + if (DimorphodonEntity.this.onGround()) { + // Takeoff: Target well above ground to ensure liftoff + y = pos.y + 15 + random.nextInt(10); + } else { + // Flying: 5% chance to land, otherwise stay airborne but capped height + if (random.nextFloat() < 0.05f) { + y = groundY; // Land + } else if (pos.y > groundY + 20) { + // Too high: Force descent + y = pos.y - 5 - random.nextInt(10); + } else { + // Just wander up or down a bit + y = pos.y + (random.nextFloat() * 2 - 1) * 10; + } + } + + // Don't target below the ground (blocks) + if (y < groundY) y = groundY; + + return new Vec3(x, y, z); + } + }); + + this.goalSelector.addGoal(9, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(10, new FollowMobGoal(this, 0.8, (float) 20, (float) 10)); + this.targetSelector.addGoal(11, new NearestAttackableTargetGoal<>(this, Animal.class, 10, false, false, + target -> { + // 1. Don't eat your own species + if (target.getType() == this.getType()) return false; + + // 2. Don't eat Flying Animals + if (target instanceof FlyingAnimal) return false; + + // 3. SIZE CHECK: specific height and width limits + // Example: Height < 1.0 blocks AND Width < 1.0 blocks + boolean isSmallEnough = target.getBbHeight() <= 1.0F && target.getBbWidth() <= 1.0F; + + return isSmallEnough; + } + )); this.targetSelector.addGoal(12, new NearestAttackableTargetGoal<>(this, Player.class, false, false)); + this.targetSelector.addGoal(13, new HurtByTargetGoal(this)); + this.goalSelector.addGoal(14, new RandomLookAroundGoal(this)); + this.goalSelector.addGoal(15, new FloatGoal(this)); + } + + public static AttributeSupplier.Builder createAttributes() { + return Animal.createLivingAttributes() + .add(Attributes.MAX_HEALTH, 20D) + .add(Attributes.MOVEMENT_SPEED, 0.3D) + .add(Attributes.FLYING_SPEED, 0.6D) + .add(Attributes.ARMOR, 0D) + .add(Attributes.FOLLOW_RANGE, 32D) + .add(Attributes.KNOCKBACK_RESISTANCE, 0D) + .add(Attributes.ATTACK_KNOCKBACK, 0D) + .add(Attributes.ATTACK_DAMAGE, 5D); + } + + @Override + protected PathNavigation createNavigation(Level pLevel) { + FlyingPathNavigation navigation = new FlyingPathNavigation(this, pLevel); + navigation.setCanOpenDoors(false); + navigation.setCanFloat(true); + navigation.setCanPassDoors(true); + return navigation; + } + + @Override + public boolean isFlying() { + return !this.onGround(); + } + + @Override + public boolean causeFallDamage(float pFallDistance, float pMultiplier, DamageSource pSource) { + return false; + } + + @Override + protected void checkFallDamage(double pY, boolean pOnGround, BlockState pState, BlockPos pPos) { + } + + @Override + public boolean isFood(ItemStack pStack) { + return pStack.is(Items.BEEF); + } + + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel pLevel, AgeableMob pOtherParent) { + AgeableMob child = ModEntities.DIMORPHODON.get().create(pLevel); + if (child instanceof DimorphodonEntity baby) { + DimorphodonVariant randomVariant = Util.getRandom(DimorphodonVariant.values(), this.random); + baby.setVariant(randomVariant); + } + return child; + } + + @Override + public boolean doHurtTarget(Entity target) { + boolean hit = super.doHurtTarget(target); + if (!level().isClientSide && hit && target instanceof LivingEntity) { + if (this.level() instanceof ServerLevel serverLevel) { + this.triggerAnim("attackController", "attack"); + this.playSound(ModSounds.BEAK_ATTACK.get(), 1.0F, 1.0F); + } + } + return hit; + } + + @Override + public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { + controllers.add(new AnimationController<>(this, "Walk/Run/Idle", state -> { + if (!DimorphodonEntity.this.onGround()) { + return state.setAndContinue(RawAnimation.begin().then("anim.dimorphodon.fly", Animation.LoopType.LOOP)); + } + + if (state.isMoving()) + return state.setAndContinue(DimorphodonEntity.this.isSprinting() ? RawAnimation.begin().then("anim.dimorphodon.walk", Animation.LoopType.LOOP) : RawAnimation.begin().then("anim.dimorphodon.walk", Animation.LoopType.LOOP)); + + return state.setAndContinue(RawAnimation.begin().then("anim.dimorphodon.idle", Animation.LoopType.LOOP)); + })); + + controllers.add(new AnimationController<>(this, "attackController", state -> PlayState.STOP) + .triggerableAnim("attack", RawAnimation.begin().then("anim.dimorphodon.attack", Animation.LoopType.PLAY_ONCE))); + + controllers.add(new AnimationController<>(this, "mouthController", state -> PlayState.STOP) + .triggerableAnim("mouth", RawAnimation.begin().then("anim.dimorphodon.mouth", Animation.LoopType.PLAY_ONCE))); + } + + private float getSignedTurnDelta() { + // Only consider the body (torso) rotation so head look does not affect tail sway + return Mth.wrapDegrees(this.yBodyRot - this.yBodyRotO); + } + + private int mouthAnimCooldown = 0; + + @Override + public void tick() { + super.tick(); + if (!level().isClientSide) { + this.entityData.set(DATA_SYNCED_AGE, this.getAge()); + var maxHealthAttr = getAttribute(Attributes.MAX_HEALTH); + if (maxHealthAttr != null) { + double baseAdult = DefaultAttributes.getSupplier((EntityType) this.getType()).getValue(Attributes.MAX_HEALTH); + double desired = this.isBaby() ? baseAdult * 0.10D : baseAdult; + if (maxHealthAttr.getBaseValue() != desired) { + double oldMax = maxHealthAttr.getBaseValue(); + double healthRatio = this.getHealth() / (float) oldMax; + maxHealthAttr.setBaseValue(desired); + this.setHealth((float) (desired * Mth.clamp(healthRatio, 0.0F, 1.0F))); + } + } + } + + if (!level().isClientSide) { + if (mouthAnimCooldown > 0) { + mouthAnimCooldown--; + } else { + this.triggerAnim("mouthController", "mouth"); + this.playSound(ModSounds.DIMORPHODON_CALL.get(), 1.0F, 1.0F); + // 30s–60s in ticks + mouthAnimCooldown = this.random.nextInt(1200 - 600 + 1) + 600; + } + } + + if (level().isClientSide) { + // Capture previous for smooth interpolation between ticks + this.tailSwayPrev = this.tailSwayOffset; + updateProceduralTailSway(); + } + } + + private void updateProceduralTailSway() { + // Turn input derived from rotation deltas; works even when standing still and turning + float turnDegrees = getSignedTurnDelta(); + + // Deadzone to ignore tiny jitter so the tail can return to center cleanly + float deadzoneDeg = 0.6f; // smaller deadzone for more responsiveness + float turnInput = 0.0f; + if (Math.abs(turnDegrees) >= deadzoneDeg) { + // Higher sensitivity so small in-place turns still affect the model + turnInput = Mth.clamp(turnDegrees / 15.0f, -1.0f, 1.0f); + } + + // Target offset: keep intuitive sign (positive input -> positive sway) + float target = turnInput; + + // One-pole low-pass (no bounce). Larger alpha => snappier and less "stiff". + float alpha = 0.24f; // try 0.20–0.30 to taste + + tailSwayOffset += (target - tailSwayOffset) * alpha; + + // Snap tiny residuals to zero so it visibly settles + if (Math.abs(tailSwayOffset) < 0.003f) { + tailSwayOffset = 0.0f; + } + + // No oscillation velocity retained + tailSwayVelocity = 0.0f; + + tailSwayOffset = Mth.clamp(tailSwayOffset, -1.5f, 1.5f); + } + + // Expose to the model for bone rotation + public float getTailSwayOffset() { + return tailSwayOffset; + } + + // Interpolated sway for smooth rendering between ticks + public float getTailSwayOffset(float partialTick) { + return Mth.lerp(Mth.clamp(partialTick, 0.0f, 1.0f), tailSwayPrev, tailSwayOffset); + } + + /*? if <=1.20.1 {*/ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); + this.entityData.define(VARIANT, 0); + this.entityData.define(DATA_SYNCED_AGE, 0); + } + /*?} else {*/ + /*@Override + protected void defineSynchedData(SynchedEntityData.Builder pBuilder) { + super.defineSynchedData(pBuilder); + pBuilder.define(VARIANT, 0); + pBuilder.define(DATA_SYNCED_AGE, 0); + } + *//*?}*/ + + public int getSyncedAge() { + return this.entityData.get(DATA_SYNCED_AGE); + } + + public DimorphodonVariant getVariant() { + return DimorphodonVariant.byId(this.getTypeVariant() & 255); + } + public int getTypeVariant() { + return this.entityData.get(VARIANT); + } + + private void setVariant(DimorphodonVariant variant) { + this.entityData.set(VARIANT, variant.getId() & 255); + } + + @Override + public boolean canMate(Animal other) { + if (!super.canMate(other)) return false; + if (!(other instanceof DimorphodonEntity that)) return false; + return this.getVariant() != that.getVariant(); + } + + /*? if <=1.20.1 {*/ + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor pLevel, DifficultyInstance pDifficulty, MobSpawnType pReason, @Nullable SpawnGroupData pSpawnData, @Nullable CompoundTag pDataTag) { + DimorphodonVariant variant = Util.getRandom(DimorphodonVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(pLevel, pDifficulty, pReason, pSpawnData, pDataTag); + } + /*?} else {*/ + /*@Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, MobSpawnType spawnType, @Nullable SpawnGroupData spawnGroupData) { + DimorphodonVariant variant = Util.getRandom(DimorphodonVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(level, difficulty, spawnType, spawnGroupData); + } + *//*?}*/ + + @Override + public void readAdditionalSaveData(CompoundTag pCompound) { + super.readAdditionalSaveData(pCompound); + this.entityData.set(VARIANT, pCompound.getInt("Variant")); + } + + @Override + public void addAdditionalSaveData(CompoundTag pCompound) { + super.addAdditionalSaveData(pCompound); + pCompound.putInt("Variant", this.getTypeVariant()); + } + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } + + @Override + protected @Nullable SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.DIMORPHODON_HURT.get(); + } + + @Override + protected @Nullable SoundEvent getDeathSound() { + return ModSounds.DIMORPHODON_DEATH.get(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/custom/DiplodocusEntity.java b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/DiplodocusEntity.java new file mode 100644 index 0000000..25ffc57 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/DiplodocusEntity.java @@ -0,0 +1,314 @@ +package net.cmr.jurassicrevived.entity.custom; + +import net.cmr.jurassicrevived.entity.ModEntities; +import net.cmr.jurassicrevived.entity.ai.SprintingMeleeAttackGoal; +import net.cmr.jurassicrevived.entity.ai.SprintingPanicGoal; +import net.cmr.jurassicrevived.entity.client.DiplodocusVariant; +import net.cmr.jurassicrevived.entity.client.DiplodocusVariant; +import net.cmr.jurassicrevived.sound.ModSounds; +import net.minecraft.Util; +import net.minecraft.core.BlockPos; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.util.Mth; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.*; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.attributes.DefaultAttributes; +import net.minecraft.world.entity.ai.goal.*; +import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerLevelAccessor; +import net.minecraft.world.level.block.state.BlockState; +import org.jetbrains.annotations.Nullable; +import software.bernie.geckolib.animatable.GeoEntity; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.core.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.core.animation.AnimatableManager; +import software.bernie.geckolib.core.animation.Animation; +import software.bernie.geckolib.core.animation.AnimationController; +import software.bernie.geckolib.core.animation.RawAnimation; +import software.bernie.geckolib.core.object.PlayState; +/*?} else {*/ +/*import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.animation.*; +*//*?}*/ + +public class DiplodocusEntity extends Animal implements GeoEntity { + private AnimatableInstanceCache cache = new SingletonAnimatableInstanceCache(this); + + private static final EntityDataAccessor VARIANT = + SynchedEntityData.defineId(DiplodocusEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SYNCED_AGE = + SynchedEntityData.defineId(DiplodocusEntity.class, EntityDataSerializers.INT); + + // Procedural tail sway state (client-side use for rendering) + private float tailSwayOffset; // Smoothed offset in range roughly [-1, 1] + private float tailSwayVelocity; // Internal velocity for spring-damper + private float tailSwayPrev; // Previous frame value for interpolation + + public DiplodocusEntity(EntityType pEntityType, Level pLevel) { + super(pEntityType, pLevel); + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new SprintingPanicGoal(this, 1.15) { + @Override + public boolean canUse() { + return DiplodocusEntity.this.isBaby() && super.canUse(); + } + }); + this.targetSelector.addGoal(1, new HurtByTargetGoal(this).setAlertOthers()); + this.goalSelector.addGoal(2, new FloatGoal(this)); + this.goalSelector.addGoal(3, new AvoidEntityGoal<>(this, DilophosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(4, new AvoidEntityGoal<>(this, VelociraptorEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(5, new AvoidEntityGoal<>(this, CeratosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(6, new AvoidEntityGoal<>(this, SpinosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(7, new AvoidEntityGoal<>(this, TyrannosaurusRexEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(8, new SprintingMeleeAttackGoal(this, 1.1, false)); + this.goalSelector.addGoal(9, new BreedGoal(this, 1.0)); + this.goalSelector.addGoal(9, new FollowParentGoal(this, 1.25)); + this.goalSelector.addGoal(10, new WaterAvoidingRandomStrollGoal(this, 1.0)); + this.goalSelector.addGoal(11, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(12, new FollowMobGoal(this, 1, (float) 20, (float) 10)); + this.goalSelector.addGoal(13, new RandomLookAroundGoal(this)); + + } + + public static AttributeSupplier.Builder createAttributes() { + return Animal.createLivingAttributes() + .add(Attributes.MAX_HEALTH, 150D) + .add(Attributes.MOVEMENT_SPEED, 0.3D) + .add(Attributes.ARMOR, 0D) + .add(Attributes.FOLLOW_RANGE, 32D) + .add(Attributes.KNOCKBACK_RESISTANCE, 1D) + .add(Attributes.ATTACK_KNOCKBACK, 0D) + .add(Attributes.ATTACK_DAMAGE, 15D); + } + + @Override + public boolean isFood(ItemStack pStack) { + return pStack.is(Items.KELP); + } + + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel pLevel, AgeableMob pOtherParent) { + AgeableMob child = ModEntities.DIPLODOCUS.get().create(pLevel); + if (child instanceof DiplodocusEntity baby) { + DiplodocusVariant randomVariant = Util.getRandom(DiplodocusVariant.values(), this.random); + baby.setVariant(randomVariant); + } + return child; + } + + @Override + public boolean doHurtTarget(Entity target) { + boolean hit = super.doHurtTarget(target); + if (!level().isClientSide && hit && target instanceof LivingEntity) { + if (this.level() instanceof ServerLevel serverLevel) { + this.triggerAnim("attackController", "attack"); + this.playSound(ModSounds.TAIL_WHIP.get(), 1.0F, 1.0F); + } + } + return hit; + } + + @Override + public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { + controllers.add(new AnimationController<>(this, "Walk/Run/Idle", state -> { + if (state.isMoving()) + return state.setAndContinue(DiplodocusEntity.this.isSprinting() ? RawAnimation.begin().then("anim.diplodocus.run", Animation.LoopType.LOOP) : RawAnimation.begin().then("anim.diplodocus.walk", Animation.LoopType.LOOP)); + + return state.setAndContinue(RawAnimation.begin().then("anim.diplodocus.idle", Animation.LoopType.LOOP)); + })); + + controllers.add(new AnimationController<>(this, "attackController", state -> PlayState.STOP) + .triggerableAnim("attack", RawAnimation.begin().then("anim.diplodocus.attack", Animation.LoopType.PLAY_ONCE))); + + controllers.add(new AnimationController<>(this, "mouthController", state -> PlayState.STOP) + .triggerableAnim("mouth", RawAnimation.begin().then("anim.diplodocus.call", Animation.LoopType.PLAY_ONCE))); + } + + private float getSignedTurnDelta() { + // Only consider the body (torso) rotation so head look does not affect tail sway + return Mth.wrapDegrees(this.yBodyRot - this.yBodyRotO); + } + + private int mouthAnimCooldown = 0; + + @Override + public void tick() { + super.tick(); + if (!level().isClientSide) { + this.entityData.set(DATA_SYNCED_AGE, this.getAge()); + var maxHealthAttr = getAttribute(Attributes.MAX_HEALTH); + if (maxHealthAttr != null) { + double baseAdult = DefaultAttributes.getSupplier((EntityType) this.getType()).getValue(Attributes.MAX_HEALTH); + double desired = this.isBaby() ? baseAdult * 0.10D : baseAdult; + if (maxHealthAttr.getBaseValue() != desired) { + double oldMax = maxHealthAttr.getBaseValue(); + double healthRatio = this.getHealth() / (float) oldMax; + maxHealthAttr.setBaseValue(desired); + this.setHealth((float) (desired * Mth.clamp(healthRatio, 0.0F, 1.0F))); + } + } + } + + if (!level().isClientSide) { + if (mouthAnimCooldown > 0) { + mouthAnimCooldown--; + } else { + this.triggerAnim("mouthController", "mouth"); + this.playSound(ModSounds.DIPLODOCUS_CALL.get(), 1.0F, 1.0F); + // 30s–60s in ticks + mouthAnimCooldown = this.random.nextInt(1200 - 600 + 1) + 600; + } + } + + if (level().isClientSide) { + // Capture previous for smooth interpolation between ticks + this.tailSwayPrev = this.tailSwayOffset; + updateProceduralTailSway(); + } + } + + private void updateProceduralTailSway() { + // Turn input derived from rotation deltas; works even when standing still and turning + float turnDegrees = getSignedTurnDelta(); + + // Deadzone to ignore tiny jitter so the tail can return to center cleanly + float deadzoneDeg = 0.6f; // smaller deadzone for more responsiveness + float turnInput = 0.0f; + if (Math.abs(turnDegrees) >= deadzoneDeg) { + // Higher sensitivity so small in-place turns still affect the model + turnInput = Mth.clamp(turnDegrees / 15.0f, -1.0f, 1.0f); + } + + // Target offset: keep intuitive sign (positive input -> positive sway) + float target = turnInput; + + // One-pole low-pass (no bounce). Larger alpha => snappier and less "stiff". + float alpha = 0.24f; // try 0.20–0.30 to taste + + tailSwayOffset += (target - tailSwayOffset) * alpha; + + // Snap tiny residuals to zero so it visibly settles + if (Math.abs(tailSwayOffset) < 0.003f) { + tailSwayOffset = 0.0f; + } + + // No oscillation velocity retained + tailSwayVelocity = 0.0f; + + tailSwayOffset = Mth.clamp(tailSwayOffset, -1.5f, 1.5f); + } + + // Expose to the model for bone rotation + public float getTailSwayOffset() { + return tailSwayOffset; + } + + // Interpolated sway for smooth rendering between ticks + public float getTailSwayOffset(float partialTick) { + return Mth.lerp(Mth.clamp(partialTick, 0.0f, 1.0f), tailSwayPrev, tailSwayOffset); + } + + /*? if <=1.20.1 {*/ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); + this.entityData.define(VARIANT, 0); + this.entityData.define(DATA_SYNCED_AGE, 0); + } + /*?} else {*/ + /*@Override + protected void defineSynchedData(SynchedEntityData.Builder pBuilder) { + super.defineSynchedData(pBuilder); + pBuilder.define(VARIANT, 0); + pBuilder.define(DATA_SYNCED_AGE, 0); + } + *//*?}*/ + + public int getSyncedAge() { + return this.entityData.get(DATA_SYNCED_AGE); + } + public int getTypeVariant() { + return this.entityData.get(VARIANT); + } + + public DiplodocusVariant getVariant() { + return DiplodocusVariant.byId(this.getTypeVariant() & 255); + } + + private void setVariant(DiplodocusVariant variant) { + this.entityData.set(VARIANT, variant.getId() & 255); + } + + @Override + public boolean canMate(Animal other) { + if (!super.canMate(other)) return false; + if (!(other instanceof DiplodocusEntity that)) return false; + return this.getVariant() != that.getVariant(); + } + @Override + public void addAdditionalSaveData(CompoundTag pCompound) { + super.addAdditionalSaveData(pCompound); + pCompound.putInt("Variant", this.getTypeVariant()); + } + + @Override + public void readAdditionalSaveData(CompoundTag pCompound) { + super.readAdditionalSaveData(pCompound); + this.entityData.set(VARIANT, pCompound.getInt("Variant")); + } + + /*? if <=1.20.1 {*/ + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor pLevel, DifficultyInstance pDifficulty, MobSpawnType pReason, @Nullable SpawnGroupData pSpawnData, @Nullable CompoundTag pDataTag) { + DiplodocusVariant variant = Util.getRandom(DiplodocusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(pLevel, pDifficulty, pReason, pSpawnData, pDataTag); + } + /*?} else {*/ + /*@Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, MobSpawnType spawnType, @Nullable SpawnGroupData spawnGroupData) { + DiplodocusVariant variant = Util.getRandom(DiplodocusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(level, difficulty, spawnType, spawnGroupData); + } + *//*?}*/ + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } + + @Override + protected @Nullable SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.DIPLODOCUS_HURT.get(); + } + + @Override + protected void playStepSound(BlockPos pos, BlockState blockIn) { + this.playSound(ModSounds.STOMP.get(), 0.5F, 1.0F); + } + + @Override + protected @Nullable SoundEvent getDeathSound() { + return ModSounds.DIPLODOCUS_DEATH.get(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/custom/DistortusRexEntity.java b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/DistortusRexEntity.java new file mode 100644 index 0000000..7d551d8 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/DistortusRexEntity.java @@ -0,0 +1,325 @@ +package net.cmr.jurassicrevived.entity.custom; + +import net.cmr.jurassicrevived.entity.ModEntities; +import net.cmr.jurassicrevived.entity.ai.SprintingMeleeAttackGoal; +import net.cmr.jurassicrevived.entity.ai.SprintingPanicGoal; +import net.cmr.jurassicrevived.entity.client.DistortusRexVariant; +import net.cmr.jurassicrevived.entity.client.DistortusRexVariant; +import net.cmr.jurassicrevived.sound.ModSounds; +import net.minecraft.Util; +import net.minecraft.core.BlockPos; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.util.Mth; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.*; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.attributes.DefaultAttributes; +import net.minecraft.world.entity.ai.goal.*; +import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal; +import net.minecraft.world.entity.ai.goal.target.NearestAttackableTargetGoal; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.monster.Monster; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerLevelAccessor; +import net.minecraft.world.level.block.state.BlockState; +import org.jetbrains.annotations.Nullable; +import software.bernie.geckolib.animatable.GeoEntity; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.core.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.core.animation.AnimatableManager; +import software.bernie.geckolib.core.animation.Animation; +import software.bernie.geckolib.core.animation.AnimationController; +import software.bernie.geckolib.core.animation.RawAnimation; +import software.bernie.geckolib.core.object.PlayState; +/*?} else {*/ +/*import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.animation.*; +*//*?}*/ + +public class DistortusRexEntity extends Animal implements GeoEntity { + private AnimatableInstanceCache cache = new SingletonAnimatableInstanceCache(this); + + private static final EntityDataAccessor VARIANT = + SynchedEntityData.defineId(DistortusRexEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SYNCED_AGE = + SynchedEntityData.defineId(DistortusRexEntity.class, EntityDataSerializers.INT); + + // Procedural tail sway state (client-side use for rendering) + private float tailSwayOffset; // Smoothed offset in range roughly [-1, 1] + private float tailSwayVelocity; // Internal velocity for spring-damper + private float tailSwayPrev; // Previous frame value for interpolation + + public DistortusRexEntity(EntityType pEntityType, Level pLevel) { + super(pEntityType, pLevel); + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new SprintingPanicGoal(this, 1.15) { + @Override + public boolean canUse() { + return DistortusRexEntity.this.isBaby() && super.canUse(); + } + }); + this.targetSelector.addGoal(1, new HurtByTargetGoal(this).setAlertOthers()); + this.goalSelector.addGoal(2, new FloatGoal(this)); + this.goalSelector.addGoal(3, new AvoidEntityGoal<>(this, BrachiosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(4, new SprintingMeleeAttackGoal(this, 1.1, false)); + this.goalSelector.addGoal(5, new BreedGoal(this, 1.0)); + this.goalSelector.addGoal(6, new FollowParentGoal(this, 1.25)); + this.goalSelector.addGoal(7, new WaterAvoidingRandomStrollGoal(this, 1.0)); + this.goalSelector.addGoal(8, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(9, new FollowMobGoal(this, 1.2, (float) 20, (float) 10)); + this.targetSelector.addGoal(10, new NearestAttackableTargetGoal<>(this, Monster.class, true)); + this.targetSelector.addGoal(11, new NearestAttackableTargetGoal<>(this, Animal.class, 10, false, false, + target -> target.getType() != this.getType())); + this.targetSelector.addGoal(12, new NearestAttackableTargetGoal(this, TriceratopsEntity.class, false, false)); + this.targetSelector.addGoal(13, new NearestAttackableTargetGoal(this, GallimimusEntity.class, false, false)); + this.targetSelector.addGoal(14, new NearestAttackableTargetGoal(this, DilophosaurusEntity.class, false, false)); + this.targetSelector.addGoal(15, new NearestAttackableTargetGoal(this, CeratosaurusEntity.class, false, false)); + this.targetSelector.addGoal(16, new NearestAttackableTargetGoal(this, ParasaurolophusEntity.class, false, false)); + this.targetSelector.addGoal(17, new NearestAttackableTargetGoal(this, VelociraptorEntity.class, false, false)); + this.targetSelector.addGoal(18, new NearestAttackableTargetGoal(this, SpinosaurusEntity.class, false, false)); + this.targetSelector.addGoal(19, new NearestAttackableTargetGoal(this, IndominusRexEntity.class, false, false)); + this.targetSelector.addGoal(20, new NearestAttackableTargetGoal(this, Player.class, false, false)); + this.goalSelector.addGoal(21, new RandomLookAroundGoal(this)); + + + } + + public static AttributeSupplier.Builder createAttributes() { + return Animal.createLivingAttributes() + .add(Attributes.MAX_HEALTH, 120D) + .add(Attributes.MOVEMENT_SPEED, 0.25D) + .add(Attributes.ARMOR, 0D) + .add(Attributes.FOLLOW_RANGE, 32D) + .add(Attributes.KNOCKBACK_RESISTANCE, 0D) + .add(Attributes.ATTACK_KNOCKBACK, 0D) + .add(Attributes.ATTACK_DAMAGE, 35D); + } + + @Override + public boolean isFood(ItemStack pStack) { + return pStack.is(Items.BEEF); + } + + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel pLevel, AgeableMob pOtherParent) { + AgeableMob child = ModEntities.DISTORTUS_REX.get().create(pLevel); + if (child instanceof DistortusRexEntity baby) { + DistortusRexVariant randomVariant = Util.getRandom(DistortusRexVariant.values(), this.random); + baby.setVariant(randomVariant); + } + return child; + } + + @Override + public boolean doHurtTarget(Entity target) { + boolean hit = super.doHurtTarget(target); + if (!level().isClientSide && hit && target instanceof LivingEntity) { + if (this.level() instanceof ServerLevel serverLevel) { + this.triggerAnim("attackController", "attack"); + this.playSound(ModSounds.DISTORTUS_REX_ATTACK.get(), 1.0F, 1.0F); + } + } + return hit; + } + + @Override + public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { + controllers.add(new AnimationController<>(this, "Walk/Run/Idle", state -> { + if (state.isMoving()) + return state.setAndContinue(DistortusRexEntity.this.isSprinting() ? RawAnimation.begin().then("anim.distortus_rex.walk", Animation.LoopType.LOOP) : RawAnimation.begin().then("anim.distortus_rex.walk", Animation.LoopType.LOOP)); + + return state.setAndContinue(RawAnimation.begin().then("anim.distortus_rex.idle", Animation.LoopType.LOOP)); + })); + + controllers.add(new AnimationController<>(this, "attackController", state -> PlayState.STOP) + .triggerableAnim("attack", RawAnimation.begin().then("anim.distortus_rex.attack", Animation.LoopType.PLAY_ONCE))); + + controllers.add(new AnimationController<>(this, "mouthController", state -> PlayState.STOP) + .triggerableAnim("mouth", RawAnimation.begin().then("anim.distortus_rex.mouth", Animation.LoopType.PLAY_ONCE))); + } + + private float getSignedTurnDelta() { + // Only consider the body (torso) rotation so head look does not affect tail sway + return Mth.wrapDegrees(this.yBodyRot - this.yBodyRotO); + } + + private int mouthAnimCooldown = 0; + + @Override + public void tick() { + super.tick(); + if (!level().isClientSide) { + this.entityData.set(DATA_SYNCED_AGE, this.getAge()); + var maxHealthAttr = getAttribute(Attributes.MAX_HEALTH); + if (maxHealthAttr != null) { + double baseAdult = DefaultAttributes.getSupplier((EntityType) this.getType()).getValue(Attributes.MAX_HEALTH); + double desired = this.isBaby() ? baseAdult * 0.10D : baseAdult; + if (maxHealthAttr.getBaseValue() != desired) { + double oldMax = maxHealthAttr.getBaseValue(); + double healthRatio = this.getHealth() / (float) oldMax; + maxHealthAttr.setBaseValue(desired); + this.setHealth((float) (desired * Mth.clamp(healthRatio, 0.0F, 1.0F))); + } + } + } + + if (!level().isClientSide) { + if (mouthAnimCooldown > 0) { + mouthAnimCooldown--; + } else { + this.triggerAnim("mouthController", "mouth"); + this.playSound(ModSounds.DISTORTUS_REX_CALL.get(), 1.0F, 1.0F); + // 30s–60s in ticks + mouthAnimCooldown = this.random.nextInt(1200 - 600 + 1) + 600; + } + } + + if (level().isClientSide) { + // Capture previous for smooth interpolation between ticks + this.tailSwayPrev = this.tailSwayOffset; + updateProceduralTailSway(); + } + } + + private void updateProceduralTailSway() { + // Turn input derived from rotation deltas; works even when standing still and turning + float turnDegrees = getSignedTurnDelta(); + + // Deadzone to ignore tiny jitter so the tail can return to center cleanly + float deadzoneDeg = 0.6f; // smaller deadzone for more responsiveness + float turnInput = 0.0f; + if (Math.abs(turnDegrees) >= deadzoneDeg) { + // Higher sensitivity so small in-place turns still affect the model + turnInput = Mth.clamp(turnDegrees / 15.0f, -1.0f, 1.0f); + } + + // Target offset: keep intuitive sign (positive input -> positive sway) + float target = turnInput; + + // One-pole low-pass (no bounce). Larger alpha => snappier and less "stiff". + float alpha = 0.24f; // try 0.20–0.30 to taste + + tailSwayOffset += (target - tailSwayOffset) * alpha; + + // Snap tiny residuals to zero so it visibly settles + if (Math.abs(tailSwayOffset) < 0.003f) { + tailSwayOffset = 0.0f; + } + + // No oscillation velocity retained + tailSwayVelocity = 0.0f; + + tailSwayOffset = Mth.clamp(tailSwayOffset, -1.5f, 1.5f); + } + + // Expose to the model for bone rotation + public float getTailSwayOffset() { + return tailSwayOffset; + } + + // Interpolated sway for smooth rendering between ticks + public float getTailSwayOffset(float partialTick) { + return Mth.lerp(Mth.clamp(partialTick, 0.0f, 1.0f), tailSwayPrev, tailSwayOffset); + } + + /*? if <=1.20.1 {*/ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); + this.entityData.define(VARIANT, 0); + this.entityData.define(DATA_SYNCED_AGE, 0); + } + /*?} else {*/ + /*@Override + protected void defineSynchedData(SynchedEntityData.Builder pBuilder) { + super.defineSynchedData(pBuilder); + pBuilder.define(VARIANT, 0); + pBuilder.define(DATA_SYNCED_AGE, 0); + } + *//*?}*/ + + public int getSyncedAge() { + return this.entityData.get(DATA_SYNCED_AGE); + } + public int getTypeVariant() { + return this.entityData.get(VARIANT); + } + + public DistortusRexVariant getVariant() { + return DistortusRexVariant.byId(this.getTypeVariant() & 255); + } + + private void setVariant(DistortusRexVariant variant) { + this.entityData.set(VARIANT, variant.getId() & 255); + } + + @Override + public boolean canMate(Animal other) { + if (!super.canMate(other)) return false; + if (!(other instanceof DistortusRexEntity that)) return false; + return this.getVariant() != that.getVariant(); + } + @Override + public void addAdditionalSaveData(CompoundTag pCompound) { + super.addAdditionalSaveData(pCompound); + pCompound.putInt("Variant", this.getTypeVariant()); + } + + @Override + public void readAdditionalSaveData(CompoundTag pCompound) { + super.readAdditionalSaveData(pCompound); + this.entityData.set(VARIANT, pCompound.getInt("Variant")); + } + + /*? if <=1.20.1 {*/ + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor pLevel, DifficultyInstance pDifficulty, MobSpawnType pReason, @Nullable SpawnGroupData pSpawnData, @Nullable CompoundTag pDataTag) { + DistortusRexVariant variant = Util.getRandom(DistortusRexVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(pLevel, pDifficulty, pReason, pSpawnData, pDataTag); + } + /*?} else {*/ + /*@Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, MobSpawnType spawnType, @Nullable SpawnGroupData spawnGroupData) { + DistortusRexVariant variant = Util.getRandom(DistortusRexVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(level, difficulty, spawnType, spawnGroupData); + } + *//*?}*/ + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } + + @Override + protected @Nullable SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.DISTORTUS_REX_HURT.get(); + } + + @Override + protected void playStepSound(BlockPos pos, BlockState blockIn) { + this.playSound(ModSounds.STOMP.get(), 0.5F, 1.0F); + } + + @Override + protected @Nullable SoundEvent getDeathSound() { + return ModSounds.DISTORTUS_REX_DEATH.get(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/custom/DryosaurusEntity.java b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/DryosaurusEntity.java new file mode 100644 index 0000000..d400d6b --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/DryosaurusEntity.java @@ -0,0 +1,299 @@ +package net.cmr.jurassicrevived.entity.custom; + +import net.cmr.jurassicrevived.entity.ModEntities; +import net.cmr.jurassicrevived.entity.ai.SprintingPanicGoal; +import net.cmr.jurassicrevived.entity.client.DryosaurusVariant; +import net.cmr.jurassicrevived.entity.client.DryosaurusVariant; +import net.cmr.jurassicrevived.sound.ModSounds; +import net.minecraft.Util; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.util.Mth; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.*; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.attributes.DefaultAttributes; +import net.minecraft.world.entity.ai.goal.*; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerLevelAccessor; +import org.jetbrains.annotations.Nullable; +import software.bernie.geckolib.animatable.GeoEntity; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.core.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.core.animation.AnimatableManager; +import software.bernie.geckolib.core.animation.Animation; +import software.bernie.geckolib.core.animation.AnimationController; +import software.bernie.geckolib.core.animation.RawAnimation; +import software.bernie.geckolib.core.object.PlayState; +/*?} else {*/ +/*import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.animation.*; +*//*?}*/ + +public class DryosaurusEntity extends Animal implements GeoEntity { + private AnimatableInstanceCache cache = new SingletonAnimatableInstanceCache(this); + + private static final EntityDataAccessor VARIANT = + SynchedEntityData.defineId(DryosaurusEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SYNCED_AGE = + SynchedEntityData.defineId(DryosaurusEntity.class, EntityDataSerializers.INT); + + // Procedural tail sway state (client-side use for rendering) + private float tailSwayOffset; // Smoothed offset in range roughly [-1, 1] + private float tailSwayVelocity; // Internal velocity for spring-damper + private float tailSwayPrev; // Previous frame value for interpolation + + public DryosaurusEntity(EntityType pEntityType, Level pLevel) { + super(pEntityType, pLevel); + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new SprintingPanicGoal(this, 1.15)); + this.goalSelector.addGoal(2, new FloatGoal(this)); + this.goalSelector.addGoal(3, new BreedGoal(this, 1.0)); + this.goalSelector.addGoal(4, new FollowParentGoal(this, 1.25)); + this.goalSelector.addGoal(5, new WaterAvoidingRandomStrollGoal(this, 1.0)); + this.goalSelector.addGoal(6, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(7, new FollowMobGoal(this, 1, (float) 20, (float) 10)); + this.goalSelector.addGoal(8, new AvoidEntityGoal<>(this, TriceratopsEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(9, new AvoidEntityGoal<>(this, DilophosaurusEntity.class, (float) 20, 1.2, 1.2)); + this.goalSelector.addGoal(10, new AvoidEntityGoal<>(this, VelociraptorEntity.class, (float) 20, 1.2, 1.2)); + this.goalSelector.addGoal(11, new AvoidEntityGoal<>(this, CeratosaurusEntity.class, (float) 20, 1.2, 1.2)); + this.goalSelector.addGoal(12, new AvoidEntityGoal<>(this, SpinosaurusEntity.class, (float) 20, 1.2, 1.2)); + this.goalSelector.addGoal(13, new AvoidEntityGoal<>(this, TyrannosaurusRexEntity.class, (float) 20, 1.2, 1.2)); + this.goalSelector.addGoal(14, new AvoidEntityGoal<>(this, IndominusRexEntity.class, (float) 20, 1.2, 1.2)); + this.goalSelector.addGoal(15, new RandomLookAroundGoal(this)); + + } + + public static AttributeSupplier.Builder createAttributes() { + return Animal.createLivingAttributes() + .add(Attributes.MAX_HEALTH, 20D) + .add(Attributes.MOVEMENT_SPEED, 0.3D) + .add(Attributes.ARMOR, 0D) + .add(Attributes.FOLLOW_RANGE, 32D) + .add(Attributes.KNOCKBACK_RESISTANCE, 0D) + .add(Attributes.ATTACK_KNOCKBACK, 0D) + .add(Attributes.ATTACK_DAMAGE, 0D); + } + + @Override + public boolean isFood(ItemStack pStack) { + return pStack.is(Items.KELP); + } + + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel pLevel, AgeableMob pOtherParent) { + AgeableMob child = ModEntities.DRYOSAURUS.get().create(pLevel); + if (child instanceof DryosaurusEntity baby) { + DryosaurusVariant randomVariant = Util.getRandom(DryosaurusVariant.values(), this.random); + baby.setVariant(randomVariant); + } + return child; + } + + @Override + public boolean doHurtTarget(Entity target) { + boolean hit = super.doHurtTarget(target); + if (!level().isClientSide && hit && target instanceof LivingEntity) { + if (this.level() instanceof ServerLevel serverLevel) { + this.triggerAnim("attackController", "attack"); + } + } + return hit; + } + + @Override + public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { + controllers.add(new AnimationController<>(this, "Walk/Run/Idle", state -> { + if (state.isMoving()) + return state.setAndContinue(DryosaurusEntity.this.isSprinting() ? RawAnimation.begin().then("anim.dryosaurus.run", Animation.LoopType.LOOP) : RawAnimation.begin().then("anim.dryosaurus.walk", Animation.LoopType.LOOP)); + + return state.setAndContinue(RawAnimation.begin().then("anim.dryosaurus.idle", Animation.LoopType.LOOP)); + })); + + controllers.add(new AnimationController<>(this, "attackController", state -> PlayState.STOP) + .triggerableAnim("attack", RawAnimation.begin().then("anim.dryosaurus.attack", Animation.LoopType.PLAY_ONCE))); + + controllers.add(new AnimationController<>(this, "mouthController", state -> PlayState.STOP) + .triggerableAnim("mouth", RawAnimation.begin().then("anim.dryosaurus.mouth", Animation.LoopType.PLAY_ONCE))); + } + + private float getSignedTurnDelta() { + // Only consider the body (torso) rotation so head look does not affect tail sway + return Mth.wrapDegrees(this.yBodyRot - this.yBodyRotO); + } + + private int mouthAnimCooldown = 0; + + @Override + public void tick() { + super.tick(); + if (!level().isClientSide) { + this.entityData.set(DATA_SYNCED_AGE, this.getAge()); + var maxHealthAttr = getAttribute(Attributes.MAX_HEALTH); + if (maxHealthAttr != null) { + double baseAdult = DefaultAttributes.getSupplier((EntityType) this.getType()).getValue(Attributes.MAX_HEALTH); + double desired = this.isBaby() ? baseAdult * 0.10D : baseAdult; + if (maxHealthAttr.getBaseValue() != desired) { + double oldMax = maxHealthAttr.getBaseValue(); + double healthRatio = this.getHealth() / (float) oldMax; + maxHealthAttr.setBaseValue(desired); + this.setHealth((float) (desired * Mth.clamp(healthRatio, 0.0F, 1.0F))); + } + } + } + + if (!level().isClientSide) { + if (mouthAnimCooldown > 0) { + mouthAnimCooldown--; + } else { + this.triggerAnim("mouthController", "mouth"); + this.playSound(ModSounds.DRYOSAURUS_CALL.get(), 1.0F, 1.0F); + // 30s–60s in ticks + mouthAnimCooldown = this.random.nextInt(1200 - 600 + 1) + 600; + } + } + + if (level().isClientSide) { + // Capture previous for smooth interpolation between ticks + this.tailSwayPrev = this.tailSwayOffset; + updateProceduralTailSway(); + } + } + + private void updateProceduralTailSway() { + // Turn input derived from rotation deltas; works even when standing still and turning + float turnDegrees = getSignedTurnDelta(); + + // Deadzone to ignore tiny jitter so the tail can return to center cleanly + float deadzoneDeg = 0.6f; // smaller deadzone for more responsiveness + float turnInput = 0.0f; + if (Math.abs(turnDegrees) >= deadzoneDeg) { + // Higher sensitivity so small in-place turns still affect the model + turnInput = Mth.clamp(turnDegrees / 15.0f, -1.0f, 1.0f); + } + + // Target offset: keep intuitive sign (positive input -> positive sway) + float target = turnInput; + + // One-pole low-pass (no bounce). Larger alpha => snappier and less "stiff". + float alpha = 0.24f; // try 0.20–0.30 to taste + + tailSwayOffset += (target - tailSwayOffset) * alpha; + + // Snap tiny residuals to zero so it visibly settles + if (Math.abs(tailSwayOffset) < 0.003f) { + tailSwayOffset = 0.0f; + } + + // No oscillation velocity retained + tailSwayVelocity = 0.0f; + + tailSwayOffset = Mth.clamp(tailSwayOffset, -1.5f, 1.5f); + } + + // Expose to the model for bone rotation + public float getTailSwayOffset() { + return tailSwayOffset; + } + + // Interpolated sway for smooth rendering between ticks + public float getTailSwayOffset(float partialTick) { + return Mth.lerp(Mth.clamp(partialTick, 0.0f, 1.0f), tailSwayPrev, tailSwayOffset); + } + + /*? if <=1.20.1 {*/ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); + this.entityData.define(VARIANT, 0); + this.entityData.define(DATA_SYNCED_AGE, 0); + } + /*?} else {*/ + /*@Override + protected void defineSynchedData(SynchedEntityData.Builder pBuilder) { + super.defineSynchedData(pBuilder); + pBuilder.define(VARIANT, 0); + pBuilder.define(DATA_SYNCED_AGE, 0); + } + *//*?}*/ + + public int getSyncedAge() { + return this.entityData.get(DATA_SYNCED_AGE); + } + public int getTypeVariant() { + return this.entityData.get(VARIANT); + } + + public DryosaurusVariant getVariant() { + return DryosaurusVariant.byId(this.getTypeVariant() & 255); + } + + private void setVariant(DryosaurusVariant variant) { + this.entityData.set(VARIANT, variant.getId() & 255); + } + + @Override + public boolean canMate(Animal other) { + if (!super.canMate(other)) return false; + if (!(other instanceof DryosaurusEntity that)) return false; + return this.getVariant() != that.getVariant(); + } + @Override + public void addAdditionalSaveData(CompoundTag pCompound) { + super.addAdditionalSaveData(pCompound); + pCompound.putInt("Variant", this.getTypeVariant()); + } + + @Override + public void readAdditionalSaveData(CompoundTag pCompound) { + super.readAdditionalSaveData(pCompound); + this.entityData.set(VARIANT, pCompound.getInt("Variant")); + } + + /*? if <=1.20.1 {*/ + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor pLevel, DifficultyInstance pDifficulty, MobSpawnType pReason, @Nullable SpawnGroupData pSpawnData, @Nullable CompoundTag pDataTag) { + DryosaurusVariant variant = Util.getRandom(DryosaurusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(pLevel, pDifficulty, pReason, pSpawnData, pDataTag); + } + /*?} else {*/ + /*@Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, MobSpawnType spawnType, @Nullable SpawnGroupData spawnGroupData) { + DryosaurusVariant variant = Util.getRandom(DryosaurusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(level, difficulty, spawnType, spawnGroupData); + } + *//*?}*/ + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } + + @Override + protected @Nullable SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.DRYOSAURUS_HURT.get(); + } + + @Override + protected @Nullable SoundEvent getDeathSound() { + return ModSounds.DRYOSAURUS_DEATH.get(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/custom/EdmontosaurusEntity.java b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/EdmontosaurusEntity.java new file mode 100644 index 0000000..899e97d --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/EdmontosaurusEntity.java @@ -0,0 +1,297 @@ +package net.cmr.jurassicrevived.entity.custom; + +import net.cmr.jurassicrevived.entity.ModEntities; +import net.cmr.jurassicrevived.entity.ai.SprintingPanicGoal; +import net.cmr.jurassicrevived.entity.client.EdmontosaurusVariant; +import net.cmr.jurassicrevived.entity.client.EdmontosaurusVariant; +import net.cmr.jurassicrevived.sound.ModSounds; +import net.minecraft.Util; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.util.Mth; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.*; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.attributes.DefaultAttributes; +import net.minecraft.world.entity.ai.goal.*; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerLevelAccessor; +import org.jetbrains.annotations.Nullable; +import software.bernie.geckolib.animatable.GeoEntity; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.core.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.core.animation.AnimatableManager; +import software.bernie.geckolib.core.animation.Animation; +import software.bernie.geckolib.core.animation.AnimationController; +import software.bernie.geckolib.core.animation.RawAnimation; +import software.bernie.geckolib.core.object.PlayState; +/*?} else {*/ +/*import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.animation.*; +*//*?}*/ + +public class EdmontosaurusEntity extends Animal implements GeoEntity { + private AnimatableInstanceCache cache = new SingletonAnimatableInstanceCache(this); + + private static final EntityDataAccessor VARIANT = + SynchedEntityData.defineId(EdmontosaurusEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SYNCED_AGE = + SynchedEntityData.defineId(EdmontosaurusEntity.class, EntityDataSerializers.INT); + + // Procedural tail sway state (client-side use for rendering) + private float tailSwayOffset; // Smoothed offset in range roughly [-1, 1] + private float tailSwayVelocity; // Internal velocity for spring-damper + private float tailSwayPrev; // Previous frame value for interpolation + + public EdmontosaurusEntity(EntityType pEntityType, Level pLevel) { + super(pEntityType, pLevel); + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new SprintingPanicGoal(this, 1.15)); + this.goalSelector.addGoal(2, new FloatGoal(this)); + this.goalSelector.addGoal(3, new BreedGoal(this, 1.0)); + this.goalSelector.addGoal(4, new FollowParentGoal(this, 1.25)); + this.goalSelector.addGoal(5, new WaterAvoidingRandomStrollGoal(this, 1.0)); + this.goalSelector.addGoal(6, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(7, new FollowMobGoal(this, 1, (float) 20, (float) 10)); + this.goalSelector.addGoal(8, new AvoidEntityGoal<>(this, DilophosaurusEntity.class, (float) 20, 1.2, 1.2)); + this.goalSelector.addGoal(9, new AvoidEntityGoal<>(this, VelociraptorEntity.class, (float) 20, 1.2, 1.2)); + this.goalSelector.addGoal(10, new AvoidEntityGoal<>(this, CeratosaurusEntity.class, (float) 20, 1.2, 1.2)); + this.goalSelector.addGoal(11, new AvoidEntityGoal<>(this, SpinosaurusEntity.class, (float) 20, 1.2, 1.2)); + this.goalSelector.addGoal(12, new AvoidEntityGoal<>(this, TyrannosaurusRexEntity.class, (float) 20, 1.2, 1.2)); + this.goalSelector.addGoal(13, new RandomLookAroundGoal(this)); + + } + + public static AttributeSupplier.Builder createAttributes() { + return Animal.createLivingAttributes() + .add(Attributes.MAX_HEALTH, 60D) + .add(Attributes.MOVEMENT_SPEED, 0.3D) + .add(Attributes.ARMOR, 0D) + .add(Attributes.FOLLOW_RANGE, 32D) + .add(Attributes.KNOCKBACK_RESISTANCE, 0D) + .add(Attributes.ATTACK_KNOCKBACK, 0D) + .add(Attributes.ATTACK_DAMAGE, 12D); + } + + @Override + public boolean isFood(ItemStack pStack) { + return pStack.is(Items.KELP); + } + + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel pLevel, AgeableMob pOtherParent) { + AgeableMob child = ModEntities.EDMONTOSAURUS.get().create(pLevel); + if (child instanceof EdmontosaurusEntity baby) { + EdmontosaurusVariant randomVariant = Util.getRandom(EdmontosaurusVariant.values(), this.random); + baby.setVariant(randomVariant); + } + return child; + } + + @Override + public boolean doHurtTarget(Entity target) { + boolean hit = super.doHurtTarget(target); + if (!level().isClientSide && hit && target instanceof LivingEntity) { + if (this.level() instanceof ServerLevel serverLevel) { + this.triggerAnim("attackController", "attack"); + } + } + return hit; + } + + @Override + public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { + controllers.add(new AnimationController<>(this, "Walk/Run/Idle", state -> { + if (state.isMoving()) + return state.setAndContinue(EdmontosaurusEntity.this.isSprinting() ? RawAnimation.begin().then("anim.edmontosaurus.run", Animation.LoopType.LOOP) : RawAnimation.begin().then("anim.edmontosaurus.walk", Animation.LoopType.LOOP)); + + return state.setAndContinue(RawAnimation.begin().then("anim.edmontosaurus.idle", Animation.LoopType.LOOP)); + })); + + controllers.add(new AnimationController<>(this, "attackController", state -> PlayState.STOP) + .triggerableAnim("attack", RawAnimation.begin().then("anim.edmontosaurus.attack", Animation.LoopType.PLAY_ONCE))); + + controllers.add(new AnimationController<>(this, "mouthController", state -> PlayState.STOP) + .triggerableAnim("mouth", RawAnimation.begin().then("anim.edmontosaurus.mouth", Animation.LoopType.PLAY_ONCE))); + } + + private float getSignedTurnDelta() { + // Only consider the body (torso) rotation so head look does not affect tail sway + return Mth.wrapDegrees(this.yBodyRot - this.yBodyRotO); + } + + private int mouthAnimCooldown = 0; + + @Override + public void tick() { + super.tick(); + if (!level().isClientSide) { + this.entityData.set(DATA_SYNCED_AGE, this.getAge()); + var maxHealthAttr = getAttribute(Attributes.MAX_HEALTH); + if (maxHealthAttr != null) { + double baseAdult = DefaultAttributes.getSupplier((EntityType) this.getType()).getValue(Attributes.MAX_HEALTH); + double desired = this.isBaby() ? baseAdult * 0.10D : baseAdult; + if (maxHealthAttr.getBaseValue() != desired) { + double oldMax = maxHealthAttr.getBaseValue(); + double healthRatio = this.getHealth() / (float) oldMax; + maxHealthAttr.setBaseValue(desired); + this.setHealth((float) (desired * Mth.clamp(healthRatio, 0.0F, 1.0F))); + } + } + } + + if (!level().isClientSide) { + if (mouthAnimCooldown > 0) { + mouthAnimCooldown--; + } else { + this.triggerAnim("mouthController", "mouth"); + this.playSound(ModSounds.EDMONTOSAURUS_CALL.get(), 1.0F, 1.0F); + // 30s–60s in ticks + mouthAnimCooldown = this.random.nextInt(1200 - 600 + 1) + 600; + } + } + + if (level().isClientSide) { + // Capture previous for smooth interpolation between ticks + this.tailSwayPrev = this.tailSwayOffset; + updateProceduralTailSway(); + } + } + + private void updateProceduralTailSway() { + // Turn input derived from rotation deltas; works even when standing still and turning + float turnDegrees = getSignedTurnDelta(); + + // Deadzone to ignore tiny jitter so the tail can return to center cleanly + float deadzoneDeg = 0.6f; // smaller deadzone for more responsiveness + float turnInput = 0.0f; + if (Math.abs(turnDegrees) >= deadzoneDeg) { + // Higher sensitivity so small in-place turns still affect the model + turnInput = Mth.clamp(turnDegrees / 15.0f, -1.0f, 1.0f); + } + + // Target offset: keep intuitive sign (positive input -> positive sway) + float target = turnInput; + + // One-pole low-pass (no bounce). Larger alpha => snappier and less "stiff". + float alpha = 0.24f; // try 0.20–0.30 to taste + + tailSwayOffset += (target - tailSwayOffset) * alpha; + + // Snap tiny residuals to zero so it visibly settles + if (Math.abs(tailSwayOffset) < 0.003f) { + tailSwayOffset = 0.0f; + } + + // No oscillation velocity retained + tailSwayVelocity = 0.0f; + + tailSwayOffset = Mth.clamp(tailSwayOffset, -1.5f, 1.5f); + } + + // Expose to the model for bone rotation + public float getTailSwayOffset() { + return tailSwayOffset; + } + + // Interpolated sway for smooth rendering between ticks + public float getTailSwayOffset(float partialTick) { + return Mth.lerp(Mth.clamp(partialTick, 0.0f, 1.0f), tailSwayPrev, tailSwayOffset); + } + + /*? if <=1.20.1 {*/ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); + this.entityData.define(VARIANT, 0); + this.entityData.define(DATA_SYNCED_AGE, 0); + } + /*?} else {*/ + /*@Override + protected void defineSynchedData(SynchedEntityData.Builder pBuilder) { + super.defineSynchedData(pBuilder); + pBuilder.define(VARIANT, 0); + pBuilder.define(DATA_SYNCED_AGE, 0); + } + *//*?}*/ + + public int getSyncedAge() { + return this.entityData.get(DATA_SYNCED_AGE); + } + public int getTypeVariant() { + return this.entityData.get(VARIANT); + } + + public EdmontosaurusVariant getVariant() { + return EdmontosaurusVariant.byId(this.getTypeVariant() & 255); + } + + private void setVariant(EdmontosaurusVariant variant) { + this.entityData.set(VARIANT, variant.getId() & 255); + } + + @Override + public boolean canMate(Animal other) { + if (!super.canMate(other)) return false; + if (!(other instanceof EdmontosaurusEntity that)) return false; + return this.getVariant() != that.getVariant(); + } + @Override + public void addAdditionalSaveData(CompoundTag pCompound) { + super.addAdditionalSaveData(pCompound); + pCompound.putInt("Variant", this.getTypeVariant()); + } + + @Override + public void readAdditionalSaveData(CompoundTag pCompound) { + super.readAdditionalSaveData(pCompound); + this.entityData.set(VARIANT, pCompound.getInt("Variant")); + } + + /*? if <=1.20.1 {*/ + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor pLevel, DifficultyInstance pDifficulty, MobSpawnType pReason, @Nullable SpawnGroupData pSpawnData, @Nullable CompoundTag pDataTag) { + EdmontosaurusVariant variant = Util.getRandom(EdmontosaurusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(pLevel, pDifficulty, pReason, pSpawnData, pDataTag); + } + /*?} else {*/ + /*@Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, MobSpawnType spawnType, @Nullable SpawnGroupData spawnGroupData) { + EdmontosaurusVariant variant = Util.getRandom(EdmontosaurusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(level, difficulty, spawnType, spawnGroupData); + } + *//*?}*/ + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } + + @Override + protected @Nullable SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.EDMONTOSAURUS_HURT.get(); + } + + @Override + protected @Nullable SoundEvent getDeathSound() { + return ModSounds.EDMONTOSAURUS_DEATH.get(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/custom/FDuckEntity.java b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/FDuckEntity.java new file mode 100644 index 0000000..c92066b --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/FDuckEntity.java @@ -0,0 +1,314 @@ +package net.cmr.jurassicrevived.entity.custom; + +import net.cmr.jurassicrevived.entity.ModEntities; +import net.cmr.jurassicrevived.entity.ai.SprintingMeleeAttackGoal; +import net.cmr.jurassicrevived.entity.ai.SprintingPanicGoal; +import net.cmr.jurassicrevived.entity.client.FDuckVariant; +import net.cmr.jurassicrevived.entity.client.FDuckVariant; +import net.cmr.jurassicrevived.item.ModItems; +import net.cmr.jurassicrevived.sound.ModSounds; +import net.minecraft.Util; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.util.Mth; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.*; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.attributes.DefaultAttributes; +import net.minecraft.world.entity.ai.goal.*; +import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal; +import net.minecraft.world.entity.ai.goal.target.NearestAttackableTargetGoal; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerLevelAccessor; +import org.jetbrains.annotations.Nullable; +import software.bernie.geckolib.animatable.GeoEntity; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.core.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.core.animation.AnimatableManager; +import software.bernie.geckolib.core.animation.Animation; +import software.bernie.geckolib.core.animation.AnimationController; +import software.bernie.geckolib.core.animation.RawAnimation; +import software.bernie.geckolib.core.object.PlayState; +/*?} else {*/ +/*import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.animation.*; +*//*?}*/ + +public class FDuckEntity extends Animal implements GeoEntity { + private AnimatableInstanceCache cache = new SingletonAnimatableInstanceCache(this); + + private static final EntityDataAccessor VARIANT = + SynchedEntityData.defineId(FDuckEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SYNCED_AGE = + SynchedEntityData.defineId(FDuckEntity.class, EntityDataSerializers.INT); + + // Procedural tail sway state (client-side use for rendering) + private float tailSwayOffset; // Smoothed offset in range roughly [-1, 1] + private float tailSwayVelocity; // Internal velocity for spring-damper + private float tailSwayPrev; // Previous frame value for interpolation + + public FDuckEntity(EntityType pEntityType, Level pLevel) { + super(pEntityType, pLevel); + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new SprintingPanicGoal(this, 1.15) { + @Override + public boolean canUse() { + return FDuckEntity.this.isBaby() && super.canUse(); + } + }); + this.targetSelector.addGoal(1, new HurtByTargetGoal(this).setAlertOthers()); + this.goalSelector.addGoal(2, new FloatGoal(this)); + this.goalSelector.addGoal(3, new AvoidEntityGoal<>(this, SpinosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(4, new AvoidEntityGoal<>(this, TyrannosaurusRexEntity.class, (float) 20, 1.2, 1.2)); + this.goalSelector.addGoal(5, new AvoidEntityGoal<>(this, VelociraptorEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(6, new AvoidEntityGoal<>(this, ParasaurolophusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(7, new AvoidEntityGoal<>(this, CeratosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(8, new AvoidEntityGoal<>(this, TriceratopsEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(9, new AvoidEntityGoal<>(this, BrachiosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(10, new SprintingMeleeAttackGoal(this, 1.1, false)); + this.goalSelector.addGoal(11, new BreedGoal(this, 1.0)); + this.goalSelector.addGoal(12, new FollowParentGoal(this, 1.25)); + this.goalSelector.addGoal(13, new WaterAvoidingRandomStrollGoal(this, 1.0)); + this.goalSelector.addGoal(14, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(15, new FollowMobGoal(this, 1, (float) 20, (float) 10)); + this.targetSelector.addGoal(16, new NearestAttackableTargetGoal<>(this, Animal.class, 10, false, false, + target -> target.getType() != this.getType())); + this.targetSelector.addGoal(17, new NearestAttackableTargetGoal(this, GallimimusEntity.class, false, false)); + this.targetSelector.addGoal(18, new NearestAttackableTargetGoal(this, Player.class, false, false)); + this.targetSelector.addGoal(19, new NearestAttackableTargetGoal(this, CompsognathusEntity.class, false, false)); + this.goalSelector.addGoal(20, new RandomLookAroundGoal(this)); + } + + public static AttributeSupplier.Builder createAttributes() { + return Animal.createLivingAttributes() + .add(Attributes.MAX_HEALTH, 30D) + .add(Attributes.MOVEMENT_SPEED, 0.3D) + .add(Attributes.ARMOR, 0D) + .add(Attributes.FOLLOW_RANGE, 32D) + .add(Attributes.KNOCKBACK_RESISTANCE, 0.3D) + .add(Attributes.ATTACK_KNOCKBACK, 0D) + .add(Attributes.ATTACK_DAMAGE, 8D); + } + + @Override + public boolean isFood(ItemStack pStack) { + return pStack.is((Item) ModItems.MAC_N_CHEESE); + } + + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel pLevel, AgeableMob pOtherParent) { + AgeableMob child = ModEntities.FDUCK.get().create(pLevel); + if (child instanceof FDuckEntity baby) { + FDuckVariant randomVariant = Util.getRandom(FDuckVariant.values(), this.random); + baby.setVariant(randomVariant); + } + return child; + } + + @Override + public boolean doHurtTarget(Entity target) { + boolean hit = super.doHurtTarget(target); + if (!level().isClientSide && hit && target instanceof LivingEntity) { + if (this.level() instanceof ServerLevel serverLevel) { + this.triggerAnim("attackController", "attack"); + } + } + return hit; + } + + @Override + public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { + controllers.add(new AnimationController<>(this, "Walk/Run/Idle", state -> { + if (state.isMoving()) + return state.setAndContinue(FDuckEntity.this.isSprinting() ? RawAnimation.begin().then("anim.fduck.walk", Animation.LoopType.LOOP) : RawAnimation.begin().then("anim.fduck.walk", Animation.LoopType.LOOP)); + + return state.setAndContinue(RawAnimation.begin().then("anim.fduck.idle", Animation.LoopType.LOOP)); + })); + + controllers.add(new AnimationController<>(this, "attackController", state -> PlayState.STOP) + .triggerableAnim("attack", RawAnimation.begin().then("anim.fduck.attack", Animation.LoopType.PLAY_ONCE))); + + controllers.add(new AnimationController<>(this, "mouthController", state -> PlayState.STOP) + .triggerableAnim("mouth", RawAnimation.begin().then("anim.fduck.mouth", Animation.LoopType.PLAY_ONCE))); + } + + private float getSignedTurnDelta() { + // Only consider the body (torso) rotation so head look does not affect tail sway + return Mth.wrapDegrees(this.yBodyRot - this.yBodyRotO); + } + + private int mouthAnimCooldown = 0; + + @Override + public void tick() { + super.tick(); + if (!level().isClientSide) { + this.entityData.set(DATA_SYNCED_AGE, this.getAge()); + var maxHealthAttr = getAttribute(Attributes.MAX_HEALTH); + if (maxHealthAttr != null) { + double baseAdult = DefaultAttributes.getSupplier((EntityType) this.getType()).getValue(Attributes.MAX_HEALTH); + double desired = this.isBaby() ? baseAdult * 0.10D : baseAdult; + if (maxHealthAttr.getBaseValue() != desired) { + double oldMax = maxHealthAttr.getBaseValue(); + double healthRatio = this.getHealth() / (float) oldMax; + maxHealthAttr.setBaseValue(desired); + this.setHealth((float) (desired * Mth.clamp(healthRatio, 0.0F, 1.0F))); + } + } + } + + if (!level().isClientSide) { + if (mouthAnimCooldown > 0) { + mouthAnimCooldown--; + } else { + this.triggerAnim("mouthController", "mouth"); + this.playSound(ModSounds.FDUCK_CALL.get(), 1.0F, 1.0F); + // 30s–60s in ticks + mouthAnimCooldown = this.random.nextInt(1200 - 600 + 1) + 600; + } + } + + if (level().isClientSide) { + // Capture previous for smooth interpolation between ticks + this.tailSwayPrev = this.tailSwayOffset; + updateProceduralTailSway(); + } + } + + private void updateProceduralTailSway() { + // Turn input derived from rotation deltas; works even when standing still and turning + float turnDegrees = getSignedTurnDelta(); + + // Deadzone to ignore tiny jitter so the tail can return to center cleanly + float deadzoneDeg = 0.6f; // smaller deadzone for more responsiveness + float turnInput = 0.0f; + if (Math.abs(turnDegrees) >= deadzoneDeg) { + // Higher sensitivity so small in-place turns still affect the model + turnInput = Mth.clamp(turnDegrees / 15.0f, -1.0f, 1.0f); + } + + // Target offset: keep intuitive sign (positive input -> positive sway) + float target = turnInput; + + // One-pole low-pass (no bounce). Larger alpha => snappier and less "stiff". + float alpha = 0.24f; // try 0.20–0.30 to taste + + tailSwayOffset += (target - tailSwayOffset) * alpha; + + // Snap tiny residuals to zero so it visibly settles + if (Math.abs(tailSwayOffset) < 0.003f) { + tailSwayOffset = 0.0f; + } + + // No oscillation velocity retained + tailSwayVelocity = 0.0f; + + tailSwayOffset = Mth.clamp(tailSwayOffset, -1.5f, 1.5f); + } + + // Expose to the model for bone rotation + public float getTailSwayOffset() { + return tailSwayOffset; + } + + // Interpolated sway for smooth rendering between ticks + public float getTailSwayOffset(float partialTick) { + return Mth.lerp(Mth.clamp(partialTick, 0.0f, 1.0f), tailSwayPrev, tailSwayOffset); + } + + /*? if <=1.20.1 {*/ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); + this.entityData.define(VARIANT, 0); + this.entityData.define(DATA_SYNCED_AGE, 0); + } + /*?} else {*/ + /*@Override + protected void defineSynchedData(SynchedEntityData.Builder pBuilder) { + super.defineSynchedData(pBuilder); + pBuilder.define(VARIANT, 0); + pBuilder.define(DATA_SYNCED_AGE, 0); + } + *//*?}*/ + + public int getSyncedAge() { + return this.entityData.get(DATA_SYNCED_AGE); + } + public int getTypeVariant() { + return this.entityData.get(VARIANT); + } + + public FDuckVariant getVariant() { + return FDuckVariant.byId(this.getTypeVariant() & 255); + } + + private void setVariant(FDuckVariant variant) { + this.entityData.set(VARIANT, variant.getId() & 255); + } + + @Override + public boolean canMate(Animal other) { + if (!super.canMate(other)) return false; + if (!(other instanceof FDuckEntity that)) return false; + return this.getVariant() != that.getVariant(); + } + @Override + public void addAdditionalSaveData(CompoundTag pCompound) { + super.addAdditionalSaveData(pCompound); + pCompound.putInt("Variant", this.getTypeVariant()); + } + + @Override + public void readAdditionalSaveData(CompoundTag pCompound) { + super.readAdditionalSaveData(pCompound); + this.entityData.set(VARIANT, pCompound.getInt("Variant")); + } + + /*? if <=1.20.1 {*/ + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor pLevel, DifficultyInstance pDifficulty, MobSpawnType pReason, @Nullable SpawnGroupData pSpawnData, @Nullable CompoundTag pDataTag) { + FDuckVariant variant = Util.getRandom(FDuckVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(pLevel, pDifficulty, pReason, pSpawnData, pDataTag); + } + /*?} else {*/ + /*@Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, MobSpawnType spawnType, @Nullable SpawnGroupData spawnGroupData) { + FDuckVariant variant = Util.getRandom(FDuckVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(level, difficulty, spawnType, spawnGroupData); + } + *//*?}*/ + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } + + @Override + protected @Nullable SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.FDUCK_HURT.get(); + } + + @Override + protected @Nullable SoundEvent getDeathSound() { + return ModSounds.FDUCK_DEATH.get(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/custom/GallimimusEntity.java b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/GallimimusEntity.java new file mode 100644 index 0000000..5511571 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/GallimimusEntity.java @@ -0,0 +1,299 @@ +package net.cmr.jurassicrevived.entity.custom; + +import net.cmr.jurassicrevived.entity.ModEntities; +import net.cmr.jurassicrevived.entity.ai.SprintingPanicGoal; +import net.cmr.jurassicrevived.entity.client.GallimimusVariant; +import net.cmr.jurassicrevived.entity.client.GallimimusVariant; +import net.cmr.jurassicrevived.sound.ModSounds; +import net.minecraft.Util; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.util.Mth; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.*; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.attributes.DefaultAttributes; +import net.minecraft.world.entity.ai.goal.*; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerLevelAccessor; +import org.jetbrains.annotations.Nullable; +import software.bernie.geckolib.animatable.GeoEntity; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.core.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.core.animation.AnimatableManager; +import software.bernie.geckolib.core.animation.Animation; +import software.bernie.geckolib.core.animation.AnimationController; +import software.bernie.geckolib.core.animation.RawAnimation; +import software.bernie.geckolib.core.object.PlayState; +/*?} else {*/ +/*import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.animation.*; +*//*?}*/ + +public class GallimimusEntity extends Animal implements GeoEntity { + private AnimatableInstanceCache cache = new SingletonAnimatableInstanceCache(this); + + private static final EntityDataAccessor VARIANT = + SynchedEntityData.defineId(GallimimusEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SYNCED_AGE = + SynchedEntityData.defineId(GallimimusEntity.class, EntityDataSerializers.INT); + + // Procedural tail sway state (client-side use for rendering) + private float tailSwayOffset; // Smoothed offset in range roughly [-1, 1] + private float tailSwayVelocity; // Internal velocity for spring-damper + private float tailSwayPrev; // Previous frame value for interpolation + + public GallimimusEntity(EntityType pEntityType, Level pLevel) { + super(pEntityType, pLevel); + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new SprintingPanicGoal(this, 1.15)); + this.goalSelector.addGoal(2, new FloatGoal(this)); + this.goalSelector.addGoal(3, new BreedGoal(this, 1.0)); + this.goalSelector.addGoal(4, new FollowParentGoal(this, 1.25)); + this.goalSelector.addGoal(5, new WaterAvoidingRandomStrollGoal(this, 1.0)); + this.goalSelector.addGoal(6, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(7, new FollowMobGoal(this, 1, (float) 20, (float) 10)); + this.goalSelector.addGoal(8, new AvoidEntityGoal<>(this, TriceratopsEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(9, new AvoidEntityGoal<>(this, DilophosaurusEntity.class, (float) 20, 1.2, 1.2)); + this.goalSelector.addGoal(10, new AvoidEntityGoal<>(this, VelociraptorEntity.class, (float) 20, 1.2, 1.2)); + this.goalSelector.addGoal(11, new AvoidEntityGoal<>(this, CeratosaurusEntity.class, (float) 20, 1.2, 1.2)); + this.goalSelector.addGoal(12, new AvoidEntityGoal<>(this, SpinosaurusEntity.class, (float) 20, 1.2, 1.2)); + this.goalSelector.addGoal(13, new AvoidEntityGoal<>(this, TyrannosaurusRexEntity.class, (float) 20, 1.2, 1.2)); + this.goalSelector.addGoal(14, new AvoidEntityGoal<>(this, IndominusRexEntity.class, (float) 20, 1.2, 1.2)); + this.goalSelector.addGoal(15, new RandomLookAroundGoal(this)); + + } + + public static AttributeSupplier.Builder createAttributes() { + return Animal.createLivingAttributes() + .add(Attributes.MAX_HEALTH, 30D) + .add(Attributes.MOVEMENT_SPEED, 0.3D) + .add(Attributes.ARMOR, 0D) + .add(Attributes.FOLLOW_RANGE, 32D) + .add(Attributes.KNOCKBACK_RESISTANCE, 0D) + .add(Attributes.ATTACK_KNOCKBACK, 0D) + .add(Attributes.ATTACK_DAMAGE, 0D); + } + + @Override + public boolean isFood(ItemStack pStack) { + return pStack.is(Items.KELP); + } + + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel pLevel, AgeableMob pOtherParent) { + AgeableMob child = ModEntities.GALLIMIMUS.get().create(pLevel); + if (child instanceof GallimimusEntity baby) { + GallimimusVariant randomVariant = Util.getRandom(GallimimusVariant.values(), this.random); + baby.setVariant(randomVariant); + } + return child; + } + + @Override + public boolean doHurtTarget(Entity target) { + boolean hit = super.doHurtTarget(target); + if (!level().isClientSide && hit && target instanceof LivingEntity) { + if (this.level() instanceof ServerLevel serverLevel) { + this.triggerAnim("attackController", "attack"); + } + } + return hit; + } + + @Override + public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { + controllers.add(new AnimationController<>(this, "Walk/Run/Idle", state -> { + if (state.isMoving()) + return state.setAndContinue(GallimimusEntity.this.isSprinting() ? RawAnimation.begin().then("anim.gallimimus.run", Animation.LoopType.LOOP) : RawAnimation.begin().then("anim.gallimimus.walk", Animation.LoopType.LOOP)); + + return state.setAndContinue(RawAnimation.begin().then("anim.gallimimus.idle", Animation.LoopType.LOOP)); + })); + + controllers.add(new AnimationController<>(this, "attackController", state -> PlayState.STOP) + .triggerableAnim("attack", RawAnimation.begin().then("anim.gallimimus.attack", Animation.LoopType.PLAY_ONCE))); + + controllers.add(new AnimationController<>(this, "mouthController", state -> PlayState.STOP) + .triggerableAnim("mouth", RawAnimation.begin().then("anim.gallimimus.mouth", Animation.LoopType.PLAY_ONCE))); + } + + private float getSignedTurnDelta() { + // Only consider the body (torso) rotation so head look does not affect tail sway + return Mth.wrapDegrees(this.yBodyRot - this.yBodyRotO); + } + + private int mouthAnimCooldown = 0; + + @Override + public void tick() { + super.tick(); + if (!level().isClientSide) { + this.entityData.set(DATA_SYNCED_AGE, this.getAge()); + var maxHealthAttr = getAttribute(Attributes.MAX_HEALTH); + if (maxHealthAttr != null) { + double baseAdult = DefaultAttributes.getSupplier((EntityType) this.getType()).getValue(Attributes.MAX_HEALTH); + double desired = this.isBaby() ? baseAdult * 0.10D : baseAdult; + if (maxHealthAttr.getBaseValue() != desired) { + double oldMax = maxHealthAttr.getBaseValue(); + double healthRatio = this.getHealth() / (float) oldMax; + maxHealthAttr.setBaseValue(desired); + this.setHealth((float) (desired * Mth.clamp(healthRatio, 0.0F, 1.0F))); + } + } + } + + if (!level().isClientSide) { + if (mouthAnimCooldown > 0) { + mouthAnimCooldown--; + } else { + this.triggerAnim("mouthController", "mouth"); + this.playSound(ModSounds.GALLIMIMUS_CALL.get(), 1.0F, 1.0F); + // 30s–60s in ticks + mouthAnimCooldown = this.random.nextInt(1200 - 600 + 1) + 600; + } + } + + if (level().isClientSide) { + // Capture previous for smooth interpolation between ticks + this.tailSwayPrev = this.tailSwayOffset; + updateProceduralTailSway(); + } + } + + private void updateProceduralTailSway() { + // Turn input derived from rotation deltas; works even when standing still and turning + float turnDegrees = getSignedTurnDelta(); + + // Deadzone to ignore tiny jitter so the tail can return to center cleanly + float deadzoneDeg = 0.6f; // smaller deadzone for more responsiveness + float turnInput = 0.0f; + if (Math.abs(turnDegrees) >= deadzoneDeg) { + // Higher sensitivity so small in-place turns still affect the model + turnInput = Mth.clamp(turnDegrees / 15.0f, -1.0f, 1.0f); + } + + // Target offset: keep intuitive sign (positive input -> positive sway) + float target = turnInput; + + // One-pole low-pass (no bounce). Larger alpha => snappier and less "stiff". + float alpha = 0.24f; // try 0.20–0.30 to taste + + tailSwayOffset += (target - tailSwayOffset) * alpha; + + // Snap tiny residuals to zero so it visibly settles + if (Math.abs(tailSwayOffset) < 0.003f) { + tailSwayOffset = 0.0f; + } + + // No oscillation velocity retained + tailSwayVelocity = 0.0f; + + tailSwayOffset = Mth.clamp(tailSwayOffset, -1.5f, 1.5f); + } + + // Expose to the model for bone rotation + public float getTailSwayOffset() { + return tailSwayOffset; + } + + // Interpolated sway for smooth rendering between ticks + public float getTailSwayOffset(float partialTick) { + return Mth.lerp(Mth.clamp(partialTick, 0.0f, 1.0f), tailSwayPrev, tailSwayOffset); + } + + /*? if <=1.20.1 {*/ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); + this.entityData.define(VARIANT, 0); + this.entityData.define(DATA_SYNCED_AGE, 0); + } + /*?} else {*/ + /*@Override + protected void defineSynchedData(SynchedEntityData.Builder pBuilder) { + super.defineSynchedData(pBuilder); + pBuilder.define(VARIANT, 0); + pBuilder.define(DATA_SYNCED_AGE, 0); + } + *//*?}*/ + + public int getSyncedAge() { + return this.entityData.get(DATA_SYNCED_AGE); + } + public int getTypeVariant() { + return this.entityData.get(VARIANT); + } + + public GallimimusVariant getVariant() { + return GallimimusVariant.byId(this.getTypeVariant() & 255); + } + + private void setVariant(GallimimusVariant variant) { + this.entityData.set(VARIANT, variant.getId() & 255); + } + + @Override + public boolean canMate(Animal other) { + if (!super.canMate(other)) return false; + if (!(other instanceof GallimimusEntity that)) return false; + return this.getVariant() != that.getVariant(); + } + @Override + public void addAdditionalSaveData(CompoundTag pCompound) { + super.addAdditionalSaveData(pCompound); + pCompound.putInt("Variant", this.getTypeVariant()); + } + + @Override + public void readAdditionalSaveData(CompoundTag pCompound) { + super.readAdditionalSaveData(pCompound); + this.entityData.set(VARIANT, pCompound.getInt("Variant")); + } + + /*? if <=1.20.1 {*/ + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor pLevel, DifficultyInstance pDifficulty, MobSpawnType pReason, @Nullable SpawnGroupData pSpawnData, @Nullable CompoundTag pDataTag) { + GallimimusVariant variant = Util.getRandom(GallimimusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(pLevel, pDifficulty, pReason, pSpawnData, pDataTag); + } + /*?} else {*/ + /*@Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, MobSpawnType spawnType, @Nullable SpawnGroupData spawnGroupData) { + GallimimusVariant variant = Util.getRandom(GallimimusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(level, difficulty, spawnType, spawnGroupData); + } + *//*?}*/ + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } + + @Override + protected @Nullable SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.GALLIMIMUS_HURT.get(); + } + + @Override + protected @Nullable SoundEvent getDeathSound() { + return ModSounds.GALLIMIMUS_DEATH.get(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/custom/GeosternbergiaEntity.java b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/GeosternbergiaEntity.java new file mode 100644 index 0000000..15535aa --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/GeosternbergiaEntity.java @@ -0,0 +1,410 @@ +package net.cmr.jurassicrevived.entity.custom; + +import net.cmr.jurassicrevived.entity.ModEntities; +import net.cmr.jurassicrevived.entity.ai.SprintingMeleeAttackGoal; +import net.cmr.jurassicrevived.entity.ai.SprintingPanicGoal; +import net.cmr.jurassicrevived.entity.client.GeosternbergiaVariant; +import net.cmr.jurassicrevived.entity.client.GeosternbergiaVariant; +import net.cmr.jurassicrevived.sound.ModSounds; +import net.minecraft.Util; +import net.minecraft.core.BlockPos; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.*; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.attributes.DefaultAttributes; +import net.minecraft.world.entity.ai.control.FlyingMoveControl; +import net.minecraft.world.entity.ai.goal.*; +import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal; +import net.minecraft.world.entity.ai.goal.target.NearestAttackableTargetGoal; +import net.minecraft.world.entity.ai.navigation.FlyingPathNavigation; +import net.minecraft.world.entity.ai.navigation.PathNavigation; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.animal.FlyingAnimal; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerLevelAccessor; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.phys.Vec3; +import org.jetbrains.annotations.Nullable; +import software.bernie.geckolib.animatable.GeoEntity; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.core.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.core.animation.AnimatableManager; +import software.bernie.geckolib.core.animation.Animation; +import software.bernie.geckolib.core.animation.AnimationController; +import software.bernie.geckolib.core.animation.RawAnimation; +import software.bernie.geckolib.core.object.PlayState; +/*?} else {*/ +/*import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.animation.*; +*//*?}*/ + +public class GeosternbergiaEntity extends Animal implements GeoEntity, FlyingAnimal { + private final AnimatableInstanceCache cache = new SingletonAnimatableInstanceCache(this); + + private static final EntityDataAccessor VARIANT = + SynchedEntityData.defineId(GeosternbergiaEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SYNCED_AGE = + SynchedEntityData.defineId(GeosternbergiaEntity.class, EntityDataSerializers.INT); + + // Procedural tail sway state (client-side use for rendering) + private float tailSwayOffset; // Smoothed offset in range roughly [-1, 1] + private float tailSwayVelocity; // Internal velocity for spring-damper + private float tailSwayPrev; // Previous frame value for interpolation + + public GeosternbergiaEntity(EntityType pEntityType, Level pLevel) { + super(pEntityType, pLevel); + this.moveControl = new FlyingMoveControl(this, 20, true); + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new SprintingPanicGoal(this, 1.15) { + @Override + public boolean canUse() { + return GeosternbergiaEntity.this.isBaby() && super.canUse(); + } + }); + this.goalSelector.addGoal(2, new FloatGoal(this)); + this.goalSelector.addGoal(3, new AvoidEntityGoal<>(this, BrachiosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(4, new SprintingMeleeAttackGoal(this, 1.1, false)); + this.goalSelector.addGoal(5, new BreedGoal(this, 1.0)); + this.goalSelector.addGoal(6, new FollowParentGoal(this, 1.25)); + + // Goal 7: Wander on ground (Walk) - Only when on ground + this.goalSelector.addGoal(7, new WaterAvoidingRandomStrollGoal(this, 1.0) { + @Override + public boolean canUse() { + return GeosternbergiaEntity.this.onGround() && super.canUse(); + } + }); + + // Goal 8: Wander in air (Fly) - Handles takeoff, flying, and landing + this.goalSelector.addGoal(8, new WaterAvoidingRandomStrollGoal(this, 1.0, 20) { + @Override + public boolean canUse() { + boolean isFlying = !GeosternbergiaEntity.this.onGround(); + // If flying, keep flying. If on ground, small chance (1/400 ticks) to take off. + return (isFlying || GeosternbergiaEntity.this.getRandom().nextInt(400) == 0) && super.canUse(); + } + + @Override + protected Vec3 getPosition() { + Vec3 pos = GeosternbergiaEntity.this.position(); + RandomSource random = GeosternbergiaEntity.this.getRandom(); + + double x = pos.x + (random.nextFloat() * 2 - 1) * 32; + double z = pos.z + (random.nextFloat() * 2 - 1) * 32; + + // Get ground height at the random destination (returns Y of first air block) + int groundY = GeosternbergiaEntity.this.level().getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, (int)x, (int)z); + double y; + + if (GeosternbergiaEntity.this.onGround()) { + // Takeoff: Target well above ground to ensure liftoff + y = pos.y + 15 + random.nextInt(10); + } else { + // Flying: 5% chance to land, otherwise stay airborne but capped height + if (random.nextFloat() < 0.05f) { + y = groundY; // Land + } else if (pos.y > groundY + 20) { + // Too high: Force descent + y = pos.y - 5 - random.nextInt(10); + } else { + // Just wander up or down a bit + y = pos.y + (random.nextFloat() * 2 - 1) * 10; + } + } + + // Don't target below the ground (blocks) + if (y < groundY) y = groundY; + + return new Vec3(x, y, z); + } + }); + + this.goalSelector.addGoal(9, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(10, new FollowMobGoal(this, 0.8, (float) 20, (float) 10)); + this.targetSelector.addGoal(11, new NearestAttackableTargetGoal<>(this, Animal.class, 10, false, false, + target -> { + // 1. Don't eat your own species + if (target.getType() == this.getType()) return false; + + // 2. Don't eat Flying Animals + if (target instanceof FlyingAnimal) return false; + + // 3. SIZE CHECK: specific height and width limits + // Example: Height < 1.0 blocks AND Width < 1.0 blocks + boolean isSmallEnough = target.getBbHeight() <= 1.0F && target.getBbWidth() <= 1.0F; + + return isSmallEnough; + } + )); this.targetSelector.addGoal(12, new NearestAttackableTargetGoal<>(this, Player.class, false, false)); + this.targetSelector.addGoal(13, new HurtByTargetGoal(this)); + this.goalSelector.addGoal(14, new RandomLookAroundGoal(this)); + this.goalSelector.addGoal(15, new FloatGoal(this)); + } + + public static AttributeSupplier.Builder createAttributes() { + return Animal.createLivingAttributes() + .add(Attributes.MAX_HEALTH, 50D) + .add(Attributes.MOVEMENT_SPEED, 0.3D) + .add(Attributes.FLYING_SPEED, 0.6D) + .add(Attributes.ARMOR, 0D) + .add(Attributes.FOLLOW_RANGE, 32D) + .add(Attributes.KNOCKBACK_RESISTANCE, 0D) + .add(Attributes.ATTACK_KNOCKBACK, 0D) + .add(Attributes.ATTACK_DAMAGE, 8D); + } + + @Override + protected PathNavigation createNavigation(Level pLevel) { + FlyingPathNavigation navigation = new FlyingPathNavigation(this, pLevel); + navigation.setCanOpenDoors(false); + navigation.setCanFloat(true); + navigation.setCanPassDoors(true); + return navigation; + } + + @Override + public boolean isFlying() { + return !this.onGround(); + } + + @Override + public boolean causeFallDamage(float pFallDistance, float pMultiplier, DamageSource pSource) { + return false; + } + + @Override + protected void checkFallDamage(double pY, boolean pOnGround, BlockState pState, BlockPos pPos) { + } + + @Override + public boolean isFood(ItemStack pStack) { + return pStack.is(Items.BEEF); + } + + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel pLevel, AgeableMob pOtherParent) { + AgeableMob child = ModEntities.GEOSTERNBERGIA.get().create(pLevel); + if (child instanceof GeosternbergiaEntity baby) { + GeosternbergiaVariant randomVariant = Util.getRandom(GeosternbergiaVariant.values(), this.random); + baby.setVariant(randomVariant); + } + return child; + } + + @Override + public boolean doHurtTarget(Entity target) { + boolean hit = super.doHurtTarget(target); + if (!level().isClientSide && hit && target instanceof LivingEntity) { + if (this.level() instanceof ServerLevel serverLevel) { + this.triggerAnim("attackController", "attack"); + this.playSound(ModSounds.BEAK_ATTACK.get(), 1.0F, 1.0F); + } + } + return hit; + } + + @Override + public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { + controllers.add(new AnimationController<>(this, "Walk/Run/Idle", state -> { + if (!GeosternbergiaEntity.this.onGround()) { + return state.setAndContinue(RawAnimation.begin().then("anim.geosternbergia.fly", Animation.LoopType.LOOP)); + } + + if (state.isMoving()) + return state.setAndContinue(GeosternbergiaEntity.this.isSprinting() ? RawAnimation.begin().then("anim.geosternbergia.walk", Animation.LoopType.LOOP) : RawAnimation.begin().then("anim.geosternbergia.walk", Animation.LoopType.LOOP)); + + return state.setAndContinue(RawAnimation.begin().then("anim.geosternbergia.idle", Animation.LoopType.LOOP)); + })); + + controllers.add(new AnimationController<>(this, "attackController", state -> PlayState.STOP) + .triggerableAnim("attack", RawAnimation.begin().then("anim.geosternbergia.attack", Animation.LoopType.PLAY_ONCE))); + + controllers.add(new AnimationController<>(this, "mouthController", state -> PlayState.STOP) + .triggerableAnim("mouth", RawAnimation.begin().then("anim.geosternbergia.mouth", Animation.LoopType.PLAY_ONCE))); + } + + private float getSignedTurnDelta() { + // Only consider the body (torso) rotation so head look does not affect tail sway + return Mth.wrapDegrees(this.yBodyRot - this.yBodyRotO); + } + + private int mouthAnimCooldown = 0; + + @Override + public void tick() { + super.tick(); + if (!level().isClientSide) { + this.entityData.set(DATA_SYNCED_AGE, this.getAge()); + var maxHealthAttr = getAttribute(Attributes.MAX_HEALTH); + if (maxHealthAttr != null) { + double baseAdult = DefaultAttributes.getSupplier((EntityType) this.getType()).getValue(Attributes.MAX_HEALTH); + double desired = this.isBaby() ? baseAdult * 0.10D : baseAdult; + if (maxHealthAttr.getBaseValue() != desired) { + double oldMax = maxHealthAttr.getBaseValue(); + double healthRatio = this.getHealth() / (float) oldMax; + maxHealthAttr.setBaseValue(desired); + this.setHealth((float) (desired * Mth.clamp(healthRatio, 0.0F, 1.0F))); + } + } + } + + if (!level().isClientSide) { + if (mouthAnimCooldown > 0) { + mouthAnimCooldown--; + } else { + this.triggerAnim("mouthController", "mouth"); + this.playSound(ModSounds.GEOSTERNBERGIA_CALL.get(), 1.0F, 1.0F); + // 30s–60s in ticks + mouthAnimCooldown = this.random.nextInt(1200 - 600 + 1) + 600; + } + } + + if (level().isClientSide) { + // Capture previous for smooth interpolation between ticks + this.tailSwayPrev = this.tailSwayOffset; + updateProceduralTailSway(); + } + } + + private void updateProceduralTailSway() { + // Turn input derived from rotation deltas; works even when standing still and turning + float turnDegrees = getSignedTurnDelta(); + + // Deadzone to ignore tiny jitter so the tail can return to center cleanly + float deadzoneDeg = 0.6f; // smaller deadzone for more responsiveness + float turnInput = 0.0f; + if (Math.abs(turnDegrees) >= deadzoneDeg) { + // Higher sensitivity so small in-place turns still affect the model + turnInput = Mth.clamp(turnDegrees / 15.0f, -1.0f, 1.0f); + } + + // Target offset: keep intuitive sign (positive input -> positive sway) + float target = turnInput; + + // One-pole low-pass (no bounce). Larger alpha => snappier and less "stiff". + float alpha = 0.24f; // try 0.20–0.30 to taste + + tailSwayOffset += (target - tailSwayOffset) * alpha; + + // Snap tiny residuals to zero so it visibly settles + if (Math.abs(tailSwayOffset) < 0.003f) { + tailSwayOffset = 0.0f; + } + + // No oscillation velocity retained + tailSwayVelocity = 0.0f; + + tailSwayOffset = Mth.clamp(tailSwayOffset, -1.5f, 1.5f); + } + + // Expose to the model for bone rotation + public float getTailSwayOffset() { + return tailSwayOffset; + } + + // Interpolated sway for smooth rendering between ticks + public float getTailSwayOffset(float partialTick) { + return Mth.lerp(Mth.clamp(partialTick, 0.0f, 1.0f), tailSwayPrev, tailSwayOffset); + } + + /*? if <=1.20.1 {*/ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); + this.entityData.define(VARIANT, 0); + this.entityData.define(DATA_SYNCED_AGE, 0); + } + /*?} else {*/ + /*@Override + protected void defineSynchedData(SynchedEntityData.Builder pBuilder) { + super.defineSynchedData(pBuilder); + pBuilder.define(VARIANT, 0); + pBuilder.define(DATA_SYNCED_AGE, 0); + } + *//*?}*/ + + public int getSyncedAge() { + return this.entityData.get(DATA_SYNCED_AGE); + } + + public GeosternbergiaVariant getVariant() { + return GeosternbergiaVariant.byId(this.getTypeVariant() & 255); + } + public int getTypeVariant() { + return this.entityData.get(VARIANT); + } + + private void setVariant(GeosternbergiaVariant variant) { + this.entityData.set(VARIANT, variant.getId() & 255); + } + + @Override + public boolean canMate(Animal other) { + if (!super.canMate(other)) return false; + if (!(other instanceof GeosternbergiaEntity that)) return false; + return this.getVariant() != that.getVariant(); + } + + /*? if <=1.20.1 {*/ + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor pLevel, DifficultyInstance pDifficulty, MobSpawnType pReason, @Nullable SpawnGroupData pSpawnData, @Nullable CompoundTag pDataTag) { + GeosternbergiaVariant variant = Util.getRandom(GeosternbergiaVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(pLevel, pDifficulty, pReason, pSpawnData, pDataTag); + } + /*?} else {*/ + /*@Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, MobSpawnType spawnType, @Nullable SpawnGroupData spawnGroupData) { + GeosternbergiaVariant variant = Util.getRandom(GeosternbergiaVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(level, difficulty, spawnType, spawnGroupData); + } + *//*?}*/ + + @Override + public void readAdditionalSaveData(CompoundTag pCompound) { + super.readAdditionalSaveData(pCompound); + this.entityData.set(VARIANT, pCompound.getInt("Variant")); + } + + @Override + public void addAdditionalSaveData(CompoundTag pCompound) { + super.addAdditionalSaveData(pCompound); + pCompound.putInt("Variant", this.getTypeVariant()); + } + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } + + @Override + protected @Nullable SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.GEOSTERNBERGIA_HURT.get(); + } + + @Override + protected @Nullable SoundEvent getDeathSound() { + return ModSounds.GEOSTERNBERGIA_DEATH.get(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/custom/GiganotosaurusEntity.java b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/GiganotosaurusEntity.java new file mode 100644 index 0000000..572cc0f --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/GiganotosaurusEntity.java @@ -0,0 +1,318 @@ +package net.cmr.jurassicrevived.entity.custom; + +import net.cmr.jurassicrevived.entity.ModEntities; +import net.cmr.jurassicrevived.entity.ai.SprintingMeleeAttackGoal; +import net.cmr.jurassicrevived.entity.ai.SprintingPanicGoal; +import net.cmr.jurassicrevived.entity.client.GiganotosaurusVariant; +import net.cmr.jurassicrevived.entity.client.GiganotosaurusVariant; +import net.cmr.jurassicrevived.sound.ModSounds; +import net.minecraft.Util; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.util.Mth; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.*; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.attributes.DefaultAttributes; +import net.minecraft.world.entity.ai.goal.*; +import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal; +import net.minecraft.world.entity.ai.goal.target.NearestAttackableTargetGoal; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.monster.Monster; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerLevelAccessor; +import org.jetbrains.annotations.Nullable; +import software.bernie.geckolib.animatable.GeoEntity; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.core.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.core.animation.AnimatableManager; +import software.bernie.geckolib.core.animation.Animation; +import software.bernie.geckolib.core.animation.AnimationController; +import software.bernie.geckolib.core.animation.RawAnimation; +import software.bernie.geckolib.core.object.PlayState; +/*?} else {*/ +/*import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.animation.*; +*//*?}*/ + +public class GiganotosaurusEntity extends Animal implements GeoEntity { + private AnimatableInstanceCache cache = new SingletonAnimatableInstanceCache(this); + + private static final EntityDataAccessor VARIANT = + SynchedEntityData.defineId(GiganotosaurusEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SYNCED_AGE = + SynchedEntityData.defineId(GiganotosaurusEntity.class, EntityDataSerializers.INT); + + // Procedural tail sway state (client-side use for rendering) + private float tailSwayOffset; // Smoothed offset in range roughly [-1, 1] + private float tailSwayVelocity; // Internal velocity for spring-damper + private float tailSwayPrev; // Previous frame value for interpolation + + public GiganotosaurusEntity(EntityType pEntityType, Level pLevel) { + super(pEntityType, pLevel); + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new SprintingPanicGoal(this, 1.15) { + @Override + public boolean canUse() { + return GiganotosaurusEntity.this.isBaby() && super.canUse(); + } + }); + this.targetSelector.addGoal(1, new HurtByTargetGoal(this).setAlertOthers()); + this.goalSelector.addGoal(2, new FloatGoal(this)); + this.goalSelector.addGoal(3, new AvoidEntityGoal<>(this, BrachiosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(4, new SprintingMeleeAttackGoal(this, 1.1, false)); + this.goalSelector.addGoal(5, new BreedGoal(this, 1.0)); + this.goalSelector.addGoal(6, new FollowParentGoal(this, 1.25)); + this.goalSelector.addGoal(7, new WaterAvoidingRandomStrollGoal(this, 1.0)); + this.goalSelector.addGoal(8, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(9, new FollowMobGoal(this, 1.2, (float) 20, (float) 10)); + this.targetSelector.addGoal(10, new NearestAttackableTargetGoal<>(this, Monster.class, true)); + this.targetSelector.addGoal(11, new NearestAttackableTargetGoal<>(this, Animal.class, 10, false, false, + target -> target.getType() != this.getType())); + this.targetSelector.addGoal(12, new NearestAttackableTargetGoal(this, TriceratopsEntity.class, false, false)); + this.targetSelector.addGoal(13, new NearestAttackableTargetGoal(this, GallimimusEntity.class, false, false)); + this.targetSelector.addGoal(14, new NearestAttackableTargetGoal(this, DilophosaurusEntity.class, false, false)); + this.targetSelector.addGoal(15, new NearestAttackableTargetGoal(this, CeratosaurusEntity.class, false, false)); + this.targetSelector.addGoal(16, new NearestAttackableTargetGoal(this, ParasaurolophusEntity.class, false, false)); + this.targetSelector.addGoal(17, new NearestAttackableTargetGoal(this, VelociraptorEntity.class, false, false)); + this.targetSelector.addGoal(18, new NearestAttackableTargetGoal(this, SpinosaurusEntity.class, false, false)); + this.targetSelector.addGoal(19, new NearestAttackableTargetGoal(this, IndominusRexEntity.class, false, false)); + this.targetSelector.addGoal(20, new NearestAttackableTargetGoal(this, Player.class, false, false)); + this.goalSelector.addGoal(21, new RandomLookAroundGoal(this)); + + + } + + public static AttributeSupplier.Builder createAttributes() { + return Animal.createLivingAttributes() + .add(Attributes.MAX_HEALTH, 80D) + .add(Attributes.MOVEMENT_SPEED, 0.3D) + .add(Attributes.ARMOR, 0D) + .add(Attributes.FOLLOW_RANGE, 32D) + .add(Attributes.KNOCKBACK_RESISTANCE, 0D) + .add(Attributes.ATTACK_KNOCKBACK, 0D) + .add(Attributes.ATTACK_DAMAGE, 20D); + } + + @Override + public boolean isFood(ItemStack pStack) { + return pStack.is(Items.BEEF); + } + + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel pLevel, AgeableMob pOtherParent) { + AgeableMob child = ModEntities.GIGANOTOSAURUS.get().create(pLevel); + if (child instanceof GiganotosaurusEntity baby) { + GiganotosaurusVariant randomVariant = Util.getRandom(GiganotosaurusVariant.values(), this.random); + baby.setVariant(randomVariant); + } + return child; + } + + @Override + public boolean doHurtTarget(Entity target) { + boolean hit = super.doHurtTarget(target); + if (!level().isClientSide && hit && target instanceof LivingEntity) { + if (this.level() instanceof ServerLevel serverLevel) { + this.triggerAnim("attackController", "attack"); + this.playSound(ModSounds.GIGANOTOSAURUS_ATTACK.get(), 1.0F, 1.0F); + } + } + return hit; + } + + @Override + public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { + controllers.add(new AnimationController<>(this, "Walk/Run/Idle", state -> { + if (state.isMoving()) + return state.setAndContinue(GiganotosaurusEntity.this.isSprinting() ? RawAnimation.begin().then("anim.giganotosaurus.run", Animation.LoopType.LOOP) : RawAnimation.begin().then("anim.giganotosaurus.walk", Animation.LoopType.LOOP)); + + return state.setAndContinue(RawAnimation.begin().then("anim.giganotosaurus.idle", Animation.LoopType.LOOP)); + })); + + controllers.add(new AnimationController<>(this, "attackController", state -> PlayState.STOP) + .triggerableAnim("attack", RawAnimation.begin().then("anim.giganotosaurus.attack", Animation.LoopType.PLAY_ONCE))); + + controllers.add(new AnimationController<>(this, "mouthController", state -> PlayState.STOP) + .triggerableAnim("mouth", RawAnimation.begin().then("anim.giganotosaurus.mouth", Animation.LoopType.PLAY_ONCE))); + } + + private float getSignedTurnDelta() { + // Only consider the body (torso) rotation so head look does not affect tail sway + return Mth.wrapDegrees(this.yBodyRot - this.yBodyRotO); + } + + private int mouthAnimCooldown = 0; + + @Override + public void tick() { + super.tick(); + if (!level().isClientSide) { + this.entityData.set(DATA_SYNCED_AGE, this.getAge()); + var maxHealthAttr = getAttribute(Attributes.MAX_HEALTH); + if (maxHealthAttr != null) { + double baseAdult = DefaultAttributes.getSupplier((EntityType) this.getType()).getValue(Attributes.MAX_HEALTH); + double desired = this.isBaby() ? baseAdult * 0.10D : baseAdult; + if (maxHealthAttr.getBaseValue() != desired) { + double oldMax = maxHealthAttr.getBaseValue(); + double healthRatio = this.getHealth() / (float) oldMax; + maxHealthAttr.setBaseValue(desired); + this.setHealth((float) (desired * Mth.clamp(healthRatio, 0.0F, 1.0F))); + } + } + } + + if (!level().isClientSide) { + if (mouthAnimCooldown > 0) { + mouthAnimCooldown--; + } else { + this.triggerAnim("mouthController", "mouth"); + this.playSound(ModSounds.GIGANOTOSAURUS_CALL.get(), 1.0F, 1.0F); + // 30s–60s in ticks + mouthAnimCooldown = this.random.nextInt(1200 - 600 + 1) + 600; + } + } + + if (level().isClientSide) { + // Capture previous for smooth interpolation between ticks + this.tailSwayPrev = this.tailSwayOffset; + updateProceduralTailSway(); + } + } + + private void updateProceduralTailSway() { + // Turn input derived from rotation deltas; works even when standing still and turning + float turnDegrees = getSignedTurnDelta(); + + // Deadzone to ignore tiny jitter so the tail can return to center cleanly + float deadzoneDeg = 0.6f; // smaller deadzone for more responsiveness + float turnInput = 0.0f; + if (Math.abs(turnDegrees) >= deadzoneDeg) { + // Higher sensitivity so small in-place turns still affect the model + turnInput = Mth.clamp(turnDegrees / 15.0f, -1.0f, 1.0f); + } + + // Target offset: keep intuitive sign (positive input -> positive sway) + float target = turnInput; + + // One-pole low-pass (no bounce). Larger alpha => snappier and less "stiff". + float alpha = 0.24f; // try 0.20–0.30 to taste + + tailSwayOffset += (target - tailSwayOffset) * alpha; + + // Snap tiny residuals to zero so it visibly settles + if (Math.abs(tailSwayOffset) < 0.003f) { + tailSwayOffset = 0.0f; + } + + // No oscillation velocity retained + tailSwayVelocity = 0.0f; + + tailSwayOffset = Mth.clamp(tailSwayOffset, -1.5f, 1.5f); + } + + // Expose to the model for bone rotation + public float getTailSwayOffset() { + return tailSwayOffset; + } + + // Interpolated sway for smooth rendering between ticks + public float getTailSwayOffset(float partialTick) { + return Mth.lerp(Mth.clamp(partialTick, 0.0f, 1.0f), tailSwayPrev, tailSwayOffset); + } + + /*? if <=1.20.1 {*/ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); + this.entityData.define(VARIANT, 0); + this.entityData.define(DATA_SYNCED_AGE, 0); + } + /*?} else {*/ + /*@Override + protected void defineSynchedData(SynchedEntityData.Builder pBuilder) { + super.defineSynchedData(pBuilder); + pBuilder.define(VARIANT, 0); + pBuilder.define(DATA_SYNCED_AGE, 0); + } + *//*?}*/ + + public int getSyncedAge() { + return this.entityData.get(DATA_SYNCED_AGE); + } + public int getTypeVariant() { + return this.entityData.get(VARIANT); + } + + public GiganotosaurusVariant getVariant() { + return GiganotosaurusVariant.byId(this.getTypeVariant() & 255); + } + + private void setVariant(GiganotosaurusVariant variant) { + this.entityData.set(VARIANT, variant.getId() & 255); + } + + @Override + public boolean canMate(Animal other) { + if (!super.canMate(other)) return false; + if (!(other instanceof GiganotosaurusEntity that)) return false; + return this.getVariant() != that.getVariant(); + } + @Override + public void addAdditionalSaveData(CompoundTag pCompound) { + super.addAdditionalSaveData(pCompound); + pCompound.putInt("Variant", this.getTypeVariant()); + } + + @Override + public void readAdditionalSaveData(CompoundTag pCompound) { + super.readAdditionalSaveData(pCompound); + this.entityData.set(VARIANT, pCompound.getInt("Variant")); + } + + /*? if <=1.20.1 {*/ + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor pLevel, DifficultyInstance pDifficulty, MobSpawnType pReason, @Nullable SpawnGroupData pSpawnData, @Nullable CompoundTag pDataTag) { + GiganotosaurusVariant variant = Util.getRandom(GiganotosaurusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(pLevel, pDifficulty, pReason, pSpawnData, pDataTag); + } + /*?} else {*/ + /*@Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, MobSpawnType spawnType, @Nullable SpawnGroupData spawnGroupData) { + GiganotosaurusVariant variant = Util.getRandom(GiganotosaurusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(level, difficulty, spawnType, spawnGroupData); + } + *//*?}*/ + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } + + @Override + protected @Nullable SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.GIGANOTOSAURUS_HURT.get(); + } + + @Override + protected @Nullable SoundEvent getDeathSound() { + return ModSounds.GIGANOTOSAURUS_DEATH.get(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/custom/GuanlongEntity.java b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/GuanlongEntity.java new file mode 100644 index 0000000..7a59bbd --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/GuanlongEntity.java @@ -0,0 +1,317 @@ +package net.cmr.jurassicrevived.entity.custom; + +import net.cmr.jurassicrevived.entity.ModEntities; +import net.cmr.jurassicrevived.entity.ai.SprintingMeleeAttackGoal; +import net.cmr.jurassicrevived.entity.ai.SprintingPanicGoal; +import net.cmr.jurassicrevived.entity.client.GuanlongVariant; +import net.cmr.jurassicrevived.entity.client.GuanlongVariant; +import net.cmr.jurassicrevived.sound.ModSounds; +import net.minecraft.Util; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.util.Mth; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.*; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.attributes.DefaultAttributes; +import net.minecraft.world.entity.ai.goal.*; +import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal; +import net.minecraft.world.entity.ai.goal.target.NearestAttackableTargetGoal; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.monster.Monster; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerLevelAccessor; +import org.jetbrains.annotations.Nullable; +import software.bernie.geckolib.animatable.GeoEntity; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.core.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.core.animation.AnimatableManager; +import software.bernie.geckolib.core.animation.Animation; +import software.bernie.geckolib.core.animation.AnimationController; +import software.bernie.geckolib.core.animation.RawAnimation; +import software.bernie.geckolib.core.object.PlayState; +/*?} else {*/ +/*import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.animation.*; +*//*?}*/ + +public class GuanlongEntity extends Animal implements GeoEntity { + private AnimatableInstanceCache cache = new SingletonAnimatableInstanceCache(this); + + private static final EntityDataAccessor VARIANT = + SynchedEntityData.defineId(GuanlongEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SYNCED_AGE = + SynchedEntityData.defineId(GuanlongEntity.class, EntityDataSerializers.INT); + + // Procedural tail sway state (client-side use for rendering) + private float tailSwayOffset; // Smoothed offset in range roughly [-1, 1] + private float tailSwayVelocity; // Internal velocity for spring-damper + private float tailSwayPrev; // Previous frame value for interpolation + + public GuanlongEntity(EntityType pEntityType, Level pLevel) { + super(pEntityType, pLevel); + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new SprintingPanicGoal(this, 1.15) { + @Override + public boolean canUse() { + return GuanlongEntity.this.isBaby() && super.canUse(); + } + }); + this.targetSelector.addGoal(1, new HurtByTargetGoal(this).setAlertOthers()); + this.goalSelector.addGoal(2, new FloatGoal(this)); + this.goalSelector.addGoal(3, new AvoidEntityGoal<>(this, BrachiosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(4, new SprintingMeleeAttackGoal(this, 1.1, false)); + this.goalSelector.addGoal(5, new BreedGoal(this, 1.0)); + this.goalSelector.addGoal(6, new FollowParentGoal(this, 1.25)); + this.goalSelector.addGoal(7, new WaterAvoidingRandomStrollGoal(this, 1.0)); + this.goalSelector.addGoal(8, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(9, new FollowMobGoal(this, 1.2, (float) 20, (float) 10)); + this.targetSelector.addGoal(10, new NearestAttackableTargetGoal<>(this, Monster.class, true)); + this.targetSelector.addGoal(11, new NearestAttackableTargetGoal<>(this, Animal.class, 10, false, false, + target -> target.getType() != this.getType())); + this.targetSelector.addGoal(12, new NearestAttackableTargetGoal(this, TriceratopsEntity.class, false, false)); + this.targetSelector.addGoal(13, new NearestAttackableTargetGoal(this, GallimimusEntity.class, false, false)); + this.targetSelector.addGoal(14, new NearestAttackableTargetGoal(this, DilophosaurusEntity.class, false, false)); + this.targetSelector.addGoal(15, new NearestAttackableTargetGoal(this, CeratosaurusEntity.class, false, false)); + this.targetSelector.addGoal(16, new NearestAttackableTargetGoal(this, ParasaurolophusEntity.class, false, false)); + this.targetSelector.addGoal(17, new NearestAttackableTargetGoal(this, VelociraptorEntity.class, false, false)); + this.targetSelector.addGoal(18, new NearestAttackableTargetGoal(this, SpinosaurusEntity.class, false, false)); + this.targetSelector.addGoal(19, new NearestAttackableTargetGoal(this, IndominusRexEntity.class, false, false)); + this.targetSelector.addGoal(20, new NearestAttackableTargetGoal(this, Player.class, false, false)); + this.goalSelector.addGoal(21, new RandomLookAroundGoal(this)); + + + } + + public static AttributeSupplier.Builder createAttributes() { + return Animal.createLivingAttributes() + .add(Attributes.MAX_HEALTH, 20D) + .add(Attributes.MOVEMENT_SPEED, 0.3D) + .add(Attributes.ARMOR, 0D) + .add(Attributes.FOLLOW_RANGE, 32D) + .add(Attributes.KNOCKBACK_RESISTANCE, 0D) + .add(Attributes.ATTACK_KNOCKBACK, 0D) + .add(Attributes.ATTACK_DAMAGE, 5D); + } + + @Override + public boolean isFood(ItemStack pStack) { + return pStack.is(Items.BEEF); + } + + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel pLevel, AgeableMob pOtherParent) { + AgeableMob child = ModEntities.GUANLONG.get().create(pLevel); + if (child instanceof GuanlongEntity baby) { + GuanlongVariant randomVariant = Util.getRandom(GuanlongVariant.values(), this.random); + baby.setVariant(randomVariant); + } + return child; + } + + @Override + public boolean doHurtTarget(Entity target) { + boolean hit = super.doHurtTarget(target); + if (!level().isClientSide && hit && target instanceof LivingEntity) { + if (this.level() instanceof ServerLevel serverLevel) { + this.triggerAnim("attackController", "attack"); + } + } + return hit; + } + + @Override + public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { + controllers.add(new AnimationController<>(this, "Walk/Run/Idle", state -> { + if (state.isMoving()) + return state.setAndContinue(GuanlongEntity.this.isSprinting() ? RawAnimation.begin().then("anim.guanlong.run", Animation.LoopType.LOOP) : RawAnimation.begin().then("anim.guanlong.walk", Animation.LoopType.LOOP)); + + return state.setAndContinue(RawAnimation.begin().then("anim.guanlong.idle", Animation.LoopType.LOOP)); + })); + + controllers.add(new AnimationController<>(this, "attackController", state -> PlayState.STOP) + .triggerableAnim("attack", RawAnimation.begin().then("anim.guanlong.attack", Animation.LoopType.PLAY_ONCE))); + + controllers.add(new AnimationController<>(this, "mouthController", state -> PlayState.STOP) + .triggerableAnim("mouth", RawAnimation.begin().then("anim.guanlong.mouth", Animation.LoopType.PLAY_ONCE))); + } + + private float getSignedTurnDelta() { + // Only consider the body (torso) rotation so head look does not affect tail sway + return Mth.wrapDegrees(this.yBodyRot - this.yBodyRotO); + } + + private int mouthAnimCooldown = 0; + + @Override + public void tick() { + super.tick(); + if (!level().isClientSide) { + this.entityData.set(DATA_SYNCED_AGE, this.getAge()); + var maxHealthAttr = getAttribute(Attributes.MAX_HEALTH); + if (maxHealthAttr != null) { + double baseAdult = DefaultAttributes.getSupplier((EntityType) this.getType()).getValue(Attributes.MAX_HEALTH); + double desired = this.isBaby() ? baseAdult * 0.10D : baseAdult; + if (maxHealthAttr.getBaseValue() != desired) { + double oldMax = maxHealthAttr.getBaseValue(); + double healthRatio = this.getHealth() / (float) oldMax; + maxHealthAttr.setBaseValue(desired); + this.setHealth((float) (desired * Mth.clamp(healthRatio, 0.0F, 1.0F))); + } + } + } + + if (!level().isClientSide) { + if (mouthAnimCooldown > 0) { + mouthAnimCooldown--; + } else { + this.triggerAnim("mouthController", "mouth"); + this.playSound(ModSounds.GUANLONG_CALL.get(), 1.0F, 1.0F); + // 30s–60s in ticks + mouthAnimCooldown = this.random.nextInt(1200 - 600 + 1) + 600; + } + } + + if (level().isClientSide) { + // Capture previous for smooth interpolation between ticks + this.tailSwayPrev = this.tailSwayOffset; + updateProceduralTailSway(); + } + } + + private void updateProceduralTailSway() { + // Turn input derived from rotation deltas; works even when standing still and turning + float turnDegrees = getSignedTurnDelta(); + + // Deadzone to ignore tiny jitter so the tail can return to center cleanly + float deadzoneDeg = 0.6f; // smaller deadzone for more responsiveness + float turnInput = 0.0f; + if (Math.abs(turnDegrees) >= deadzoneDeg) { + // Higher sensitivity so small in-place turns still affect the model + turnInput = Mth.clamp(turnDegrees / 15.0f, -1.0f, 1.0f); + } + + // Target offset: keep intuitive sign (positive input -> positive sway) + float target = turnInput; + + // One-pole low-pass (no bounce). Larger alpha => snappier and less "stiff". + float alpha = 0.24f; // try 0.20–0.30 to taste + + tailSwayOffset += (target - tailSwayOffset) * alpha; + + // Snap tiny residuals to zero so it visibly settles + if (Math.abs(tailSwayOffset) < 0.003f) { + tailSwayOffset = 0.0f; + } + + // No oscillation velocity retained + tailSwayVelocity = 0.0f; + + tailSwayOffset = Mth.clamp(tailSwayOffset, -1.5f, 1.5f); + } + + // Expose to the model for bone rotation + public float getTailSwayOffset() { + return tailSwayOffset; + } + + // Interpolated sway for smooth rendering between ticks + public float getTailSwayOffset(float partialTick) { + return Mth.lerp(Mth.clamp(partialTick, 0.0f, 1.0f), tailSwayPrev, tailSwayOffset); + } + + /*? if <=1.20.1 {*/ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); + this.entityData.define(VARIANT, 0); + this.entityData.define(DATA_SYNCED_AGE, 0); + } + /*?} else {*/ + /*@Override + protected void defineSynchedData(SynchedEntityData.Builder pBuilder) { + super.defineSynchedData(pBuilder); + pBuilder.define(VARIANT, 0); + pBuilder.define(DATA_SYNCED_AGE, 0); + } + *//*?}*/ + + public int getSyncedAge() { + return this.entityData.get(DATA_SYNCED_AGE); + } + public int getTypeVariant() { + return this.entityData.get(VARIANT); + } + + public GuanlongVariant getVariant() { + return GuanlongVariant.byId(this.getTypeVariant() & 255); + } + + private void setVariant(GuanlongVariant variant) { + this.entityData.set(VARIANT, variant.getId() & 255); + } + + @Override + public boolean canMate(Animal other) { + if (!super.canMate(other)) return false; + if (!(other instanceof GuanlongEntity that)) return false; + return this.getVariant() != that.getVariant(); + } + @Override + public void addAdditionalSaveData(CompoundTag pCompound) { + super.addAdditionalSaveData(pCompound); + pCompound.putInt("Variant", this.getTypeVariant()); + } + + @Override + public void readAdditionalSaveData(CompoundTag pCompound) { + super.readAdditionalSaveData(pCompound); + this.entityData.set(VARIANT, pCompound.getInt("Variant")); + } + + /*? if <=1.20.1 {*/ + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor pLevel, DifficultyInstance pDifficulty, MobSpawnType pReason, @Nullable SpawnGroupData pSpawnData, @Nullable CompoundTag pDataTag) { + GuanlongVariant variant = Util.getRandom(GuanlongVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(pLevel, pDifficulty, pReason, pSpawnData, pDataTag); + } + /*?} else {*/ + /*@Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, MobSpawnType spawnType, @Nullable SpawnGroupData spawnGroupData) { + GuanlongVariant variant = Util.getRandom(GuanlongVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(level, difficulty, spawnType, spawnGroupData); + } + *//*?}*/ + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } + + @Override + protected @Nullable SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.GUANLONG_HURT.get(); + } + + @Override + protected @Nullable SoundEvent getDeathSound() { + return ModSounds.GUANLONG_DEATH.get(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/custom/GuidracoEntity.java b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/GuidracoEntity.java new file mode 100644 index 0000000..1135bac --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/GuidracoEntity.java @@ -0,0 +1,410 @@ +package net.cmr.jurassicrevived.entity.custom; + +import net.cmr.jurassicrevived.entity.ModEntities; +import net.cmr.jurassicrevived.entity.ai.SprintingMeleeAttackGoal; +import net.cmr.jurassicrevived.entity.ai.SprintingPanicGoal; +import net.cmr.jurassicrevived.entity.client.GuidracoVariant; +import net.cmr.jurassicrevived.entity.client.GuidracoVariant; +import net.cmr.jurassicrevived.sound.ModSounds; +import net.minecraft.Util; +import net.minecraft.core.BlockPos; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.*; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.attributes.DefaultAttributes; +import net.minecraft.world.entity.ai.control.FlyingMoveControl; +import net.minecraft.world.entity.ai.goal.*; +import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal; +import net.minecraft.world.entity.ai.goal.target.NearestAttackableTargetGoal; +import net.minecraft.world.entity.ai.navigation.FlyingPathNavigation; +import net.minecraft.world.entity.ai.navigation.PathNavigation; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.animal.FlyingAnimal; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerLevelAccessor; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.phys.Vec3; +import org.jetbrains.annotations.Nullable; +import software.bernie.geckolib.animatable.GeoEntity; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.core.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.core.animation.AnimatableManager; +import software.bernie.geckolib.core.animation.Animation; +import software.bernie.geckolib.core.animation.AnimationController; +import software.bernie.geckolib.core.animation.RawAnimation; +import software.bernie.geckolib.core.object.PlayState; +/*?} else {*/ +/*import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.animation.*; +*//*?}*/ + +public class GuidracoEntity extends Animal implements GeoEntity, FlyingAnimal { + private final AnimatableInstanceCache cache = new SingletonAnimatableInstanceCache(this); + + private static final EntityDataAccessor VARIANT = + SynchedEntityData.defineId(GuidracoEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SYNCED_AGE = + SynchedEntityData.defineId(GuidracoEntity.class, EntityDataSerializers.INT); + + // Procedural tail sway state (client-side use for rendering) + private float tailSwayOffset; // Smoothed offset in range roughly [-1, 1] + private float tailSwayVelocity; // Internal velocity for spring-damper + private float tailSwayPrev; // Previous frame value for interpolation + + public GuidracoEntity(EntityType pEntityType, Level pLevel) { + super(pEntityType, pLevel); + this.moveControl = new FlyingMoveControl(this, 20, true); + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new SprintingPanicGoal(this, 1.15) { + @Override + public boolean canUse() { + return GuidracoEntity.this.isBaby() && super.canUse(); + } + }); + this.goalSelector.addGoal(2, new FloatGoal(this)); + this.goalSelector.addGoal(3, new AvoidEntityGoal<>(this, BrachiosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(4, new SprintingMeleeAttackGoal(this, 1.1, false)); + this.goalSelector.addGoal(5, new BreedGoal(this, 1.0)); + this.goalSelector.addGoal(6, new FollowParentGoal(this, 1.25)); + + // Goal 7: Wander on ground (Walk) - Only when on ground + this.goalSelector.addGoal(7, new WaterAvoidingRandomStrollGoal(this, 1.0) { + @Override + public boolean canUse() { + return GuidracoEntity.this.onGround() && super.canUse(); + } + }); + + // Goal 8: Wander in air (Fly) - Handles takeoff, flying, and landing + this.goalSelector.addGoal(8, new WaterAvoidingRandomStrollGoal(this, 1.0, 20) { + @Override + public boolean canUse() { + boolean isFlying = !GuidracoEntity.this.onGround(); + // If flying, keep flying. If on ground, small chance (1/400 ticks) to take off. + return (isFlying || GuidracoEntity.this.getRandom().nextInt(400) == 0) && super.canUse(); + } + + @Override + protected Vec3 getPosition() { + Vec3 pos = GuidracoEntity.this.position(); + RandomSource random = GuidracoEntity.this.getRandom(); + + double x = pos.x + (random.nextFloat() * 2 - 1) * 32; + double z = pos.z + (random.nextFloat() * 2 - 1) * 32; + + // Get ground height at the random destination (returns Y of first air block) + int groundY = GuidracoEntity.this.level().getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, (int)x, (int)z); + double y; + + if (GuidracoEntity.this.onGround()) { + // Takeoff: Target well above ground to ensure liftoff + y = pos.y + 15 + random.nextInt(10); + } else { + // Flying: 5% chance to land, otherwise stay airborne but capped height + if (random.nextFloat() < 0.05f) { + y = groundY; // Land + } else if (pos.y > groundY + 20) { + // Too high: Force descent + y = pos.y - 5 - random.nextInt(10); + } else { + // Just wander up or down a bit + y = pos.y + (random.nextFloat() * 2 - 1) * 10; + } + } + + // Don't target below the ground (blocks) + if (y < groundY) y = groundY; + + return new Vec3(x, y, z); + } + }); + + this.goalSelector.addGoal(9, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(10, new FollowMobGoal(this, 0.8, (float) 20, (float) 10)); + this.targetSelector.addGoal(11, new NearestAttackableTargetGoal<>(this, Animal.class, 10, false, false, + target -> { + // 1. Don't eat your own species + if (target.getType() == this.getType()) return false; + + // 2. Don't eat Flying Animals + if (target instanceof FlyingAnimal) return false; + + // 3. SIZE CHECK: specific height and width limits + // Example: Height < 1.0 blocks AND Width < 1.0 blocks + boolean isSmallEnough = target.getBbHeight() <= 1.0F && target.getBbWidth() <= 1.0F; + + return isSmallEnough; + } + )); this.targetSelector.addGoal(12, new NearestAttackableTargetGoal<>(this, Player.class, false, false)); + this.targetSelector.addGoal(13, new HurtByTargetGoal(this)); + this.goalSelector.addGoal(14, new RandomLookAroundGoal(this)); + this.goalSelector.addGoal(15, new FloatGoal(this)); + } + + public static AttributeSupplier.Builder createAttributes() { + return Animal.createLivingAttributes() + .add(Attributes.MAX_HEALTH, 20D) + .add(Attributes.MOVEMENT_SPEED, 0.3D) + .add(Attributes.FLYING_SPEED, 0.6D) + .add(Attributes.ARMOR, 0D) + .add(Attributes.FOLLOW_RANGE, 32D) + .add(Attributes.KNOCKBACK_RESISTANCE, 0D) + .add(Attributes.ATTACK_KNOCKBACK, 0D) + .add(Attributes.ATTACK_DAMAGE, 5D); + } + + @Override + protected PathNavigation createNavigation(Level pLevel) { + FlyingPathNavigation navigation = new FlyingPathNavigation(this, pLevel); + navigation.setCanOpenDoors(false); + navigation.setCanFloat(true); + navigation.setCanPassDoors(true); + return navigation; + } + + @Override + public boolean isFlying() { + return !this.onGround(); + } + + @Override + public boolean causeFallDamage(float pFallDistance, float pMultiplier, DamageSource pSource) { + return false; + } + + @Override + protected void checkFallDamage(double pY, boolean pOnGround, BlockState pState, BlockPos pPos) { + } + + @Override + public boolean isFood(ItemStack pStack) { + return pStack.is(Items.BEEF); + } + + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel pLevel, AgeableMob pOtherParent) { + AgeableMob child = ModEntities.GUIDRACO.get().create(pLevel); + if (child instanceof GuidracoEntity baby) { + GuidracoVariant randomVariant = Util.getRandom(GuidracoVariant.values(), this.random); + baby.setVariant(randomVariant); + } + return child; + } + + @Override + public boolean doHurtTarget(Entity target) { + boolean hit = super.doHurtTarget(target); + if (!level().isClientSide && hit && target instanceof LivingEntity) { + if (this.level() instanceof ServerLevel serverLevel) { + this.triggerAnim("attackController", "attack"); + this.playSound(ModSounds.BEAK_ATTACK.get(), 1.0F, 1.0F); + } + } + return hit; + } + + @Override + public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { + controllers.add(new AnimationController<>(this, "Walk/Run/Idle", state -> { + if (!GuidracoEntity.this.onGround()) { + return state.setAndContinue(RawAnimation.begin().then("anim.guidraco.fly", Animation.LoopType.LOOP)); + } + + if (state.isMoving()) + return state.setAndContinue(GuidracoEntity.this.isSprinting() ? RawAnimation.begin().then("anim.guidraco.walk", Animation.LoopType.LOOP) : RawAnimation.begin().then("anim.guidraco.walk", Animation.LoopType.LOOP)); + + return state.setAndContinue(RawAnimation.begin().then("anim.guidraco.idle", Animation.LoopType.LOOP)); + })); + + controllers.add(new AnimationController<>(this, "attackController", state -> PlayState.STOP) + .triggerableAnim("attack", RawAnimation.begin().then("anim.guidraco.attack", Animation.LoopType.PLAY_ONCE))); + + controllers.add(new AnimationController<>(this, "mouthController", state -> PlayState.STOP) + .triggerableAnim("mouth", RawAnimation.begin().then("anim.guidraco.mouth", Animation.LoopType.PLAY_ONCE))); + } + + private float getSignedTurnDelta() { + // Only consider the body (torso) rotation so head look does not affect tail sway + return Mth.wrapDegrees(this.yBodyRot - this.yBodyRotO); + } + + private int mouthAnimCooldown = 0; + + @Override + public void tick() { + super.tick(); + if (!level().isClientSide) { + this.entityData.set(DATA_SYNCED_AGE, this.getAge()); + var maxHealthAttr = getAttribute(Attributes.MAX_HEALTH); + if (maxHealthAttr != null) { + double baseAdult = DefaultAttributes.getSupplier((EntityType) this.getType()).getValue(Attributes.MAX_HEALTH); + double desired = this.isBaby() ? baseAdult * 0.10D : baseAdult; + if (maxHealthAttr.getBaseValue() != desired) { + double oldMax = maxHealthAttr.getBaseValue(); + double healthRatio = this.getHealth() / (float) oldMax; + maxHealthAttr.setBaseValue(desired); + this.setHealth((float) (desired * Mth.clamp(healthRatio, 0.0F, 1.0F))); + } + } + } + + if (!level().isClientSide) { + if (mouthAnimCooldown > 0) { + mouthAnimCooldown--; + } else { + this.triggerAnim("mouthController", "mouth"); + this.playSound(ModSounds.GUIDRACO_CALL.get(), 1.0F, 1.0F); + // 30s–60s in ticks + mouthAnimCooldown = this.random.nextInt(1200 - 600 + 1) + 600; + } + } + + if (level().isClientSide) { + // Capture previous for smooth interpolation between ticks + this.tailSwayPrev = this.tailSwayOffset; + updateProceduralTailSway(); + } + } + + private void updateProceduralTailSway() { + // Turn input derived from rotation deltas; works even when standing still and turning + float turnDegrees = getSignedTurnDelta(); + + // Deadzone to ignore tiny jitter so the tail can return to center cleanly + float deadzoneDeg = 0.6f; // smaller deadzone for more responsiveness + float turnInput = 0.0f; + if (Math.abs(turnDegrees) >= deadzoneDeg) { + // Higher sensitivity so small in-place turns still affect the model + turnInput = Mth.clamp(turnDegrees / 15.0f, -1.0f, 1.0f); + } + + // Target offset: keep intuitive sign (positive input -> positive sway) + float target = turnInput; + + // One-pole low-pass (no bounce). Larger alpha => snappier and less "stiff". + float alpha = 0.24f; // try 0.20–0.30 to taste + + tailSwayOffset += (target - tailSwayOffset) * alpha; + + // Snap tiny residuals to zero so it visibly settles + if (Math.abs(tailSwayOffset) < 0.003f) { + tailSwayOffset = 0.0f; + } + + // No oscillation velocity retained + tailSwayVelocity = 0.0f; + + tailSwayOffset = Mth.clamp(tailSwayOffset, -1.5f, 1.5f); + } + + // Expose to the model for bone rotation + public float getTailSwayOffset() { + return tailSwayOffset; + } + + // Interpolated sway for smooth rendering between ticks + public float getTailSwayOffset(float partialTick) { + return Mth.lerp(Mth.clamp(partialTick, 0.0f, 1.0f), tailSwayPrev, tailSwayOffset); + } + + /*? if <=1.20.1 {*/ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); + this.entityData.define(VARIANT, 0); + this.entityData.define(DATA_SYNCED_AGE, 0); + } + /*?} else {*/ + /*@Override + protected void defineSynchedData(SynchedEntityData.Builder pBuilder) { + super.defineSynchedData(pBuilder); + pBuilder.define(VARIANT, 0); + pBuilder.define(DATA_SYNCED_AGE, 0); + } + *//*?}*/ + + public int getSyncedAge() { + return this.entityData.get(DATA_SYNCED_AGE); + } + + public GuidracoVariant getVariant() { + return GuidracoVariant.byId(this.getTypeVariant() & 255); + } + public int getTypeVariant() { + return this.entityData.get(VARIANT); + } + + private void setVariant(GuidracoVariant variant) { + this.entityData.set(VARIANT, variant.getId() & 255); + } + + @Override + public boolean canMate(Animal other) { + if (!super.canMate(other)) return false; + if (!(other instanceof GuidracoEntity that)) return false; + return this.getVariant() != that.getVariant(); + } + + /*? if <=1.20.1 {*/ + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor pLevel, DifficultyInstance pDifficulty, MobSpawnType pReason, @Nullable SpawnGroupData pSpawnData, @Nullable CompoundTag pDataTag) { + GuidracoVariant variant = Util.getRandom(GuidracoVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(pLevel, pDifficulty, pReason, pSpawnData, pDataTag); + } + /*?} else {*/ + /*@Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, MobSpawnType spawnType, @Nullable SpawnGroupData spawnGroupData) { + GuidracoVariant variant = Util.getRandom(GuidracoVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(level, difficulty, spawnType, spawnGroupData); + } + *//*?}*/ + + @Override + public void readAdditionalSaveData(CompoundTag pCompound) { + super.readAdditionalSaveData(pCompound); + this.entityData.set(VARIANT, pCompound.getInt("Variant")); + } + + @Override + public void addAdditionalSaveData(CompoundTag pCompound) { + super.addAdditionalSaveData(pCompound); + pCompound.putInt("Variant", this.getTypeVariant()); + } + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } + + @Override + protected @Nullable SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.GUIDRACO_HURT.get(); + } + + @Override + protected @Nullable SoundEvent getDeathSound() { + return ModSounds.GUIDRACO_DEATH.get(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/custom/HadrosaurusEntity.java b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/HadrosaurusEntity.java new file mode 100644 index 0000000..c3802da --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/HadrosaurusEntity.java @@ -0,0 +1,297 @@ +package net.cmr.jurassicrevived.entity.custom; + +import net.cmr.jurassicrevived.entity.ModEntities; +import net.cmr.jurassicrevived.entity.ai.SprintingPanicGoal; +import net.cmr.jurassicrevived.entity.client.HadrosaurusVariant; +import net.cmr.jurassicrevived.entity.client.HadrosaurusVariant; +import net.cmr.jurassicrevived.sound.ModSounds; +import net.minecraft.Util; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.util.Mth; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.*; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.attributes.DefaultAttributes; +import net.minecraft.world.entity.ai.goal.*; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerLevelAccessor; +import org.jetbrains.annotations.Nullable; +import software.bernie.geckolib.animatable.GeoEntity; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.core.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.core.animation.AnimatableManager; +import software.bernie.geckolib.core.animation.Animation; +import software.bernie.geckolib.core.animation.AnimationController; +import software.bernie.geckolib.core.animation.RawAnimation; +import software.bernie.geckolib.core.object.PlayState; +/*?} else {*/ +/*import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.animation.*; +*//*?}*/ + +public class HadrosaurusEntity extends Animal implements GeoEntity { + private AnimatableInstanceCache cache = new SingletonAnimatableInstanceCache(this); + + private static final EntityDataAccessor VARIANT = + SynchedEntityData.defineId(HadrosaurusEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SYNCED_AGE = + SynchedEntityData.defineId(HadrosaurusEntity.class, EntityDataSerializers.INT); + + // Procedural tail sway state (client-side use for rendering) + private float tailSwayOffset; // Smoothed offset in range roughly [-1, 1] + private float tailSwayVelocity; // Internal velocity for spring-damper + private float tailSwayPrev; // Previous frame value for interpolation + + public HadrosaurusEntity(EntityType pEntityType, Level pLevel) { + super(pEntityType, pLevel); + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new SprintingPanicGoal(this, 1.15)); + this.goalSelector.addGoal(2, new FloatGoal(this)); + this.goalSelector.addGoal(3, new BreedGoal(this, 1.0)); + this.goalSelector.addGoal(4, new FollowParentGoal(this, 1.25)); + this.goalSelector.addGoal(5, new WaterAvoidingRandomStrollGoal(this, 1.0)); + this.goalSelector.addGoal(6, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(7, new FollowMobGoal(this, 1, (float) 20, (float) 10)); + this.goalSelector.addGoal(8, new AvoidEntityGoal<>(this, DilophosaurusEntity.class, (float) 20, 1.2, 1.2)); + this.goalSelector.addGoal(9, new AvoidEntityGoal<>(this, VelociraptorEntity.class, (float) 20, 1.2, 1.2)); + this.goalSelector.addGoal(10, new AvoidEntityGoal<>(this, CeratosaurusEntity.class, (float) 20, 1.2, 1.2)); + this.goalSelector.addGoal(11, new AvoidEntityGoal<>(this, SpinosaurusEntity.class, (float) 20, 1.2, 1.2)); + this.goalSelector.addGoal(12, new AvoidEntityGoal<>(this, TyrannosaurusRexEntity.class, (float) 20, 1.2, 1.2)); + this.goalSelector.addGoal(13, new RandomLookAroundGoal(this)); + + } + + public static AttributeSupplier.Builder createAttributes() { + return Animal.createLivingAttributes() + .add(Attributes.MAX_HEALTH, 55D) + .add(Attributes.MOVEMENT_SPEED, 0.3D) + .add(Attributes.ARMOR, 0D) + .add(Attributes.FOLLOW_RANGE, 32D) + .add(Attributes.KNOCKBACK_RESISTANCE, 0D) + .add(Attributes.ATTACK_KNOCKBACK, 0D) + .add(Attributes.ATTACK_DAMAGE, 12D); + } + + @Override + public boolean isFood(ItemStack pStack) { + return pStack.is(Items.KELP); + } + + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel pLevel, AgeableMob pOtherParent) { + AgeableMob child = ModEntities.HADROSAURUS.get().create(pLevel); + if (child instanceof HadrosaurusEntity baby) { + HadrosaurusVariant randomVariant = Util.getRandom(HadrosaurusVariant.values(), this.random); + baby.setVariant(randomVariant); + } + return child; + } + + @Override + public boolean doHurtTarget(Entity target) { + boolean hit = super.doHurtTarget(target); + if (!level().isClientSide && hit && target instanceof LivingEntity) { + if (this.level() instanceof ServerLevel serverLevel) { + this.triggerAnim("attackController", "attack"); + } + } + return hit; + } + + @Override + public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { + controllers.add(new AnimationController<>(this, "Walk/Run/Idle", state -> { + if (state.isMoving()) + return state.setAndContinue(HadrosaurusEntity.this.isSprinting() ? RawAnimation.begin().then("anim.hadrosaurus.run", Animation.LoopType.LOOP) : RawAnimation.begin().then("anim.hadrosaurus.walk", Animation.LoopType.LOOP)); + + return state.setAndContinue(RawAnimation.begin().then("anim.hadrosaurus.idle", Animation.LoopType.LOOP)); + })); + + controllers.add(new AnimationController<>(this, "attackController", state -> PlayState.STOP) + .triggerableAnim("attack", RawAnimation.begin().then("anim.hadrosaurus.attack", Animation.LoopType.PLAY_ONCE))); + + controllers.add(new AnimationController<>(this, "mouthController", state -> PlayState.STOP) + .triggerableAnim("mouth", RawAnimation.begin().then("anim.hadrosaurus.mouth", Animation.LoopType.PLAY_ONCE))); + } + + private float getSignedTurnDelta() { + // Only consider the body (torso) rotation so head look does not affect tail sway + return Mth.wrapDegrees(this.yBodyRot - this.yBodyRotO); + } + + private int mouthAnimCooldown = 0; + + @Override + public void tick() { + super.tick(); + if (!level().isClientSide) { + this.entityData.set(DATA_SYNCED_AGE, this.getAge()); + var maxHealthAttr = getAttribute(Attributes.MAX_HEALTH); + if (maxHealthAttr != null) { + double baseAdult = DefaultAttributes.getSupplier((EntityType) this.getType()).getValue(Attributes.MAX_HEALTH); + double desired = this.isBaby() ? baseAdult * 0.10D : baseAdult; + if (maxHealthAttr.getBaseValue() != desired) { + double oldMax = maxHealthAttr.getBaseValue(); + double healthRatio = this.getHealth() / (float) oldMax; + maxHealthAttr.setBaseValue(desired); + this.setHealth((float) (desired * Mth.clamp(healthRatio, 0.0F, 1.0F))); + } + } + } + + if (!level().isClientSide) { + if (mouthAnimCooldown > 0) { + mouthAnimCooldown--; + } else { + this.triggerAnim("mouthController", "mouth"); + this.playSound(ModSounds.HADROSAURUS_CALL.get(), 1.0F, 1.0F); + // 30s–60s in ticks + mouthAnimCooldown = this.random.nextInt(1200 - 600 + 1) + 600; + } + } + + if (level().isClientSide) { + // Capture previous for smooth interpolation between ticks + this.tailSwayPrev = this.tailSwayOffset; + updateProceduralTailSway(); + } + } + + private void updateProceduralTailSway() { + // Turn input derived from rotation deltas; works even when standing still and turning + float turnDegrees = getSignedTurnDelta(); + + // Deadzone to ignore tiny jitter so the tail can return to center cleanly + float deadzoneDeg = 0.6f; // smaller deadzone for more responsiveness + float turnInput = 0.0f; + if (Math.abs(turnDegrees) >= deadzoneDeg) { + // Higher sensitivity so small in-place turns still affect the model + turnInput = Mth.clamp(turnDegrees / 15.0f, -1.0f, 1.0f); + } + + // Target offset: keep intuitive sign (positive input -> positive sway) + float target = turnInput; + + // One-pole low-pass (no bounce). Larger alpha => snappier and less "stiff". + float alpha = 0.24f; // try 0.20–0.30 to taste + + tailSwayOffset += (target - tailSwayOffset) * alpha; + + // Snap tiny residuals to zero so it visibly settles + if (Math.abs(tailSwayOffset) < 0.003f) { + tailSwayOffset = 0.0f; + } + + // No oscillation velocity retained + tailSwayVelocity = 0.0f; + + tailSwayOffset = Mth.clamp(tailSwayOffset, -1.5f, 1.5f); + } + + // Expose to the model for bone rotation + public float getTailSwayOffset() { + return tailSwayOffset; + } + + // Interpolated sway for smooth rendering between ticks + public float getTailSwayOffset(float partialTick) { + return Mth.lerp(Mth.clamp(partialTick, 0.0f, 1.0f), tailSwayPrev, tailSwayOffset); + } + + /*? if <=1.20.1 {*/ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); + this.entityData.define(VARIANT, 0); + this.entityData.define(DATA_SYNCED_AGE, 0); + } + /*?} else {*/ + /*@Override + protected void defineSynchedData(SynchedEntityData.Builder pBuilder) { + super.defineSynchedData(pBuilder); + pBuilder.define(VARIANT, 0); + pBuilder.define(DATA_SYNCED_AGE, 0); + } + *//*?}*/ + + public int getSyncedAge() { + return this.entityData.get(DATA_SYNCED_AGE); + } + public int getTypeVariant() { + return this.entityData.get(VARIANT); + } + + public HadrosaurusVariant getVariant() { + return HadrosaurusVariant.byId(this.getTypeVariant() & 255); + } + + private void setVariant(HadrosaurusVariant variant) { + this.entityData.set(VARIANT, variant.getId() & 255); + } + + @Override + public boolean canMate(Animal other) { + if (!super.canMate(other)) return false; + if (!(other instanceof HadrosaurusEntity that)) return false; + return this.getVariant() != that.getVariant(); + } + @Override + public void addAdditionalSaveData(CompoundTag pCompound) { + super.addAdditionalSaveData(pCompound); + pCompound.putInt("Variant", this.getTypeVariant()); + } + + @Override + public void readAdditionalSaveData(CompoundTag pCompound) { + super.readAdditionalSaveData(pCompound); + this.entityData.set(VARIANT, pCompound.getInt("Variant")); + } + + /*? if <=1.20.1 {*/ + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor pLevel, DifficultyInstance pDifficulty, MobSpawnType pReason, @Nullable SpawnGroupData pSpawnData, @Nullable CompoundTag pDataTag) { + HadrosaurusVariant variant = Util.getRandom(HadrosaurusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(pLevel, pDifficulty, pReason, pSpawnData, pDataTag); + } + /*?} else {*/ + /*@Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, MobSpawnType spawnType, @Nullable SpawnGroupData spawnGroupData) { + HadrosaurusVariant variant = Util.getRandom(HadrosaurusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(level, difficulty, spawnType, spawnGroupData); + } + *//*?}*/ + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } + + @Override + protected @Nullable SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.HADROSAURUS_HURT.get(); + } + + @Override + protected @Nullable SoundEvent getDeathSound() { + return ModSounds.HADROSAURUS_DEATH.get(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/custom/HerrerasaurusEntity.java b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/HerrerasaurusEntity.java new file mode 100644 index 0000000..882ef67 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/HerrerasaurusEntity.java @@ -0,0 +1,318 @@ +package net.cmr.jurassicrevived.entity.custom; + +import net.cmr.jurassicrevived.entity.ModEntities; +import net.cmr.jurassicrevived.entity.ai.SprintingMeleeAttackGoal; +import net.cmr.jurassicrevived.entity.ai.SprintingPanicGoal; +import net.cmr.jurassicrevived.entity.client.HerrerasaurusVariant; +import net.cmr.jurassicrevived.entity.client.HerrerasaurusVariant; +import net.cmr.jurassicrevived.sound.ModSounds; +import net.minecraft.Util; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.util.Mth; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.*; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.attributes.DefaultAttributes; +import net.minecraft.world.entity.ai.goal.*; +import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal; +import net.minecraft.world.entity.ai.goal.target.NearestAttackableTargetGoal; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.monster.Monster; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerLevelAccessor; +import org.jetbrains.annotations.Nullable; +import software.bernie.geckolib.animatable.GeoEntity; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.core.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.core.animation.AnimatableManager; +import software.bernie.geckolib.core.animation.Animation; +import software.bernie.geckolib.core.animation.AnimationController; +import software.bernie.geckolib.core.animation.RawAnimation; +import software.bernie.geckolib.core.object.PlayState; +/*?} else {*/ +/*import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.animation.*; +*//*?}*/ + +public class HerrerasaurusEntity extends Animal implements GeoEntity { + private AnimatableInstanceCache cache = new SingletonAnimatableInstanceCache(this); + + private static final EntityDataAccessor VARIANT = + SynchedEntityData.defineId(HerrerasaurusEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SYNCED_AGE = + SynchedEntityData.defineId(HerrerasaurusEntity.class, EntityDataSerializers.INT); + + // Procedural tail sway state (client-side use for rendering) + private float tailSwayOffset; // Smoothed offset in range roughly [-1, 1] + private float tailSwayVelocity; // Internal velocity for spring-damper + private float tailSwayPrev; // Previous frame value for interpolation + + public HerrerasaurusEntity(EntityType pEntityType, Level pLevel) { + super(pEntityType, pLevel); + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new SprintingPanicGoal(this, 1.15) { + @Override + public boolean canUse() { + return HerrerasaurusEntity.this.isBaby() && super.canUse(); + } + }); + this.targetSelector.addGoal(1, new HurtByTargetGoal(this).setAlertOthers()); + this.goalSelector.addGoal(2, new FloatGoal(this)); + this.goalSelector.addGoal(3, new AvoidEntityGoal<>(this, BrachiosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(4, new SprintingMeleeAttackGoal(this, 1.1, false)); + this.goalSelector.addGoal(5, new BreedGoal(this, 1.0)); + this.goalSelector.addGoal(6, new FollowParentGoal(this, 1.25)); + this.goalSelector.addGoal(7, new WaterAvoidingRandomStrollGoal(this, 1.0)); + this.goalSelector.addGoal(8, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(9, new FollowMobGoal(this, 1.2, (float) 20, (float) 10)); + this.targetSelector.addGoal(10, new NearestAttackableTargetGoal<>(this, Monster.class, true)); + this.targetSelector.addGoal(11, new NearestAttackableTargetGoal<>(this, Animal.class, 10, false, false, + target -> target.getType() != this.getType())); + this.targetSelector.addGoal(12, new NearestAttackableTargetGoal(this, TriceratopsEntity.class, false, false)); + this.targetSelector.addGoal(13, new NearestAttackableTargetGoal(this, GallimimusEntity.class, false, false)); + this.targetSelector.addGoal(14, new NearestAttackableTargetGoal(this, DilophosaurusEntity.class, false, false)); + this.targetSelector.addGoal(15, new NearestAttackableTargetGoal(this, CeratosaurusEntity.class, false, false)); + this.targetSelector.addGoal(16, new NearestAttackableTargetGoal(this, ParasaurolophusEntity.class, false, false)); + this.targetSelector.addGoal(17, new NearestAttackableTargetGoal(this, VelociraptorEntity.class, false, false)); + this.targetSelector.addGoal(18, new NearestAttackableTargetGoal(this, SpinosaurusEntity.class, false, false)); + this.targetSelector.addGoal(19, new NearestAttackableTargetGoal(this, IndominusRexEntity.class, false, false)); + this.targetSelector.addGoal(20, new NearestAttackableTargetGoal(this, Player.class, false, false)); + this.goalSelector.addGoal(21, new RandomLookAroundGoal(this)); + + + } + + public static AttributeSupplier.Builder createAttributes() { + return Animal.createLivingAttributes() + .add(Attributes.MAX_HEALTH, 35D) + .add(Attributes.MOVEMENT_SPEED, 0.3D) + .add(Attributes.ARMOR, 0D) + .add(Attributes.FOLLOW_RANGE, 32D) + .add(Attributes.KNOCKBACK_RESISTANCE, 0D) + .add(Attributes.ATTACK_KNOCKBACK, 0D) + .add(Attributes.ATTACK_DAMAGE, 8D); + } + + @Override + public boolean isFood(ItemStack pStack) { + return pStack.is(Items.BEEF); + } + + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel pLevel, AgeableMob pOtherParent) { + AgeableMob child = ModEntities.HERRERASAURUS.get().create(pLevel); + if (child instanceof HerrerasaurusEntity baby) { + HerrerasaurusVariant randomVariant = Util.getRandom(HerrerasaurusVariant.values(), this.random); + baby.setVariant(randomVariant); + } + return child; + } + + @Override + public boolean doHurtTarget(Entity target) { + boolean hit = super.doHurtTarget(target); + if (!level().isClientSide && hit && target instanceof LivingEntity) { + if (this.level() instanceof ServerLevel serverLevel) { + this.triggerAnim("attackController", "attack"); + this.playSound(ModSounds.HERRERASAURUS_ATTACK.get(), 1.0F, 1.0F); + } + } + return hit; + } + + @Override + public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { + controllers.add(new AnimationController<>(this, "Walk/Run/Idle", state -> { + if (state.isMoving()) + return state.setAndContinue(HerrerasaurusEntity.this.isSprinting() ? RawAnimation.begin().then("anim.herrerasaurus.run", Animation.LoopType.LOOP) : RawAnimation.begin().then("anim.herrerasaurus.walk", Animation.LoopType.LOOP)); + + return state.setAndContinue(RawAnimation.begin().then("anim.herrerasaurus.idle", Animation.LoopType.LOOP)); + })); + + controllers.add(new AnimationController<>(this, "attackController", state -> PlayState.STOP) + .triggerableAnim("attack", RawAnimation.begin().then("anim.herrerasaurus.attack", Animation.LoopType.PLAY_ONCE))); + + controllers.add(new AnimationController<>(this, "mouthController", state -> PlayState.STOP) + .triggerableAnim("mouth", RawAnimation.begin().then("anim.herrerasaurus.mouth", Animation.LoopType.PLAY_ONCE))); + } + + private float getSignedTurnDelta() { + // Only consider the body (torso) rotation so head look does not affect tail sway + return Mth.wrapDegrees(this.yBodyRot - this.yBodyRotO); + } + + private int mouthAnimCooldown = 0; + + @Override + public void tick() { + super.tick(); + if (!level().isClientSide) { + this.entityData.set(DATA_SYNCED_AGE, this.getAge()); + var maxHealthAttr = getAttribute(Attributes.MAX_HEALTH); + if (maxHealthAttr != null) { + double baseAdult = DefaultAttributes.getSupplier((EntityType) this.getType()).getValue(Attributes.MAX_HEALTH); + double desired = this.isBaby() ? baseAdult * 0.10D : baseAdult; + if (maxHealthAttr.getBaseValue() != desired) { + double oldMax = maxHealthAttr.getBaseValue(); + double healthRatio = this.getHealth() / (float) oldMax; + maxHealthAttr.setBaseValue(desired); + this.setHealth((float) (desired * Mth.clamp(healthRatio, 0.0F, 1.0F))); + } + } + } + + if (!level().isClientSide) { + if (mouthAnimCooldown > 0) { + mouthAnimCooldown--; + } else { + this.triggerAnim("mouthController", "mouth"); + this.playSound(ModSounds.HERRERASAURUS_CALL.get(), 1.0F, 1.0F); + // 30s–60s in ticks + mouthAnimCooldown = this.random.nextInt(1200 - 600 + 1) + 600; + } + } + + if (level().isClientSide) { + // Capture previous for smooth interpolation between ticks + this.tailSwayPrev = this.tailSwayOffset; + updateProceduralTailSway(); + } + } + + private void updateProceduralTailSway() { + // Turn input derived from rotation deltas; works even when standing still and turning + float turnDegrees = getSignedTurnDelta(); + + // Deadzone to ignore tiny jitter so the tail can return to center cleanly + float deadzoneDeg = 0.6f; // smaller deadzone for more responsiveness + float turnInput = 0.0f; + if (Math.abs(turnDegrees) >= deadzoneDeg) { + // Higher sensitivity so small in-place turns still affect the model + turnInput = Mth.clamp(turnDegrees / 15.0f, -1.0f, 1.0f); + } + + // Target offset: keep intuitive sign (positive input -> positive sway) + float target = turnInput; + + // One-pole low-pass (no bounce). Larger alpha => snappier and less "stiff". + float alpha = 0.24f; // try 0.20–0.30 to taste + + tailSwayOffset += (target - tailSwayOffset) * alpha; + + // Snap tiny residuals to zero so it visibly settles + if (Math.abs(tailSwayOffset) < 0.003f) { + tailSwayOffset = 0.0f; + } + + // No oscillation velocity retained + tailSwayVelocity = 0.0f; + + tailSwayOffset = Mth.clamp(tailSwayOffset, -1.5f, 1.5f); + } + + // Expose to the model for bone rotation + public float getTailSwayOffset() { + return tailSwayOffset; + } + + // Interpolated sway for smooth rendering between ticks + public float getTailSwayOffset(float partialTick) { + return Mth.lerp(Mth.clamp(partialTick, 0.0f, 1.0f), tailSwayPrev, tailSwayOffset); + } + + /*? if <=1.20.1 {*/ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); + this.entityData.define(VARIANT, 0); + this.entityData.define(DATA_SYNCED_AGE, 0); + } + /*?} else {*/ + /*@Override + protected void defineSynchedData(SynchedEntityData.Builder pBuilder) { + super.defineSynchedData(pBuilder); + pBuilder.define(VARIANT, 0); + pBuilder.define(DATA_SYNCED_AGE, 0); + } + *//*?}*/ + + public int getSyncedAge() { + return this.entityData.get(DATA_SYNCED_AGE); + } + public int getTypeVariant() { + return this.entityData.get(VARIANT); + } + + public HerrerasaurusVariant getVariant() { + return HerrerasaurusVariant.byId(this.getTypeVariant() & 255); + } + + private void setVariant(HerrerasaurusVariant variant) { + this.entityData.set(VARIANT, variant.getId() & 255); + } + + @Override + public boolean canMate(Animal other) { + if (!super.canMate(other)) return false; + if (!(other instanceof HerrerasaurusEntity that)) return false; + return this.getVariant() != that.getVariant(); + } + @Override + public void addAdditionalSaveData(CompoundTag pCompound) { + super.addAdditionalSaveData(pCompound); + pCompound.putInt("Variant", this.getTypeVariant()); + } + + @Override + public void readAdditionalSaveData(CompoundTag pCompound) { + super.readAdditionalSaveData(pCompound); + this.entityData.set(VARIANT, pCompound.getInt("Variant")); + } + + /*? if <=1.20.1 {*/ + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor pLevel, DifficultyInstance pDifficulty, MobSpawnType pReason, @Nullable SpawnGroupData pSpawnData, @Nullable CompoundTag pDataTag) { + HerrerasaurusVariant variant = Util.getRandom(HerrerasaurusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(pLevel, pDifficulty, pReason, pSpawnData, pDataTag); + } + /*?} else {*/ + /*@Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, MobSpawnType spawnType, @Nullable SpawnGroupData spawnGroupData) { + HerrerasaurusVariant variant = Util.getRandom(HerrerasaurusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(level, difficulty, spawnType, spawnGroupData); + } + *//*?}*/ + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } + + @Override + protected @Nullable SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.HERRERASAURUS_HURT.get(); + } + + @Override + protected @Nullable SoundEvent getDeathSound() { + return ModSounds.HERRERASAURUS_DEATH.get(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/custom/HypsilophodonEntity.java b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/HypsilophodonEntity.java new file mode 100644 index 0000000..9d53381 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/HypsilophodonEntity.java @@ -0,0 +1,299 @@ +package net.cmr.jurassicrevived.entity.custom; + +import net.cmr.jurassicrevived.entity.ModEntities; +import net.cmr.jurassicrevived.entity.ai.SprintingPanicGoal; +import net.cmr.jurassicrevived.entity.client.HypsilophodonVariant; +import net.cmr.jurassicrevived.entity.client.HypsilophodonVariant; +import net.cmr.jurassicrevived.sound.ModSounds; +import net.minecraft.Util; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.util.Mth; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.*; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.attributes.DefaultAttributes; +import net.minecraft.world.entity.ai.goal.*; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerLevelAccessor; +import org.jetbrains.annotations.Nullable; +import software.bernie.geckolib.animatable.GeoEntity; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.core.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.core.animation.AnimatableManager; +import software.bernie.geckolib.core.animation.Animation; +import software.bernie.geckolib.core.animation.AnimationController; +import software.bernie.geckolib.core.animation.RawAnimation; +import software.bernie.geckolib.core.object.PlayState; +/*?} else {*/ +/*import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.animation.*; +*//*?}*/ + +public class HypsilophodonEntity extends Animal implements GeoEntity { + private AnimatableInstanceCache cache = new SingletonAnimatableInstanceCache(this); + + private static final EntityDataAccessor VARIANT = + SynchedEntityData.defineId(HypsilophodonEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SYNCED_AGE = + SynchedEntityData.defineId(HypsilophodonEntity.class, EntityDataSerializers.INT); + + // Procedural tail sway state (client-side use for rendering) + private float tailSwayOffset; // Smoothed offset in range roughly [-1, 1] + private float tailSwayVelocity; // Internal velocity for spring-damper + private float tailSwayPrev; // Previous frame value for interpolation + + public HypsilophodonEntity(EntityType pEntityType, Level pLevel) { + super(pEntityType, pLevel); + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new SprintingPanicGoal(this, 1.15)); + this.goalSelector.addGoal(2, new FloatGoal(this)); + this.goalSelector.addGoal(3, new BreedGoal(this, 1.0)); + this.goalSelector.addGoal(4, new FollowParentGoal(this, 1.25)); + this.goalSelector.addGoal(5, new WaterAvoidingRandomStrollGoal(this, 1.0)); + this.goalSelector.addGoal(6, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(7, new FollowMobGoal(this, 1, (float) 20, (float) 10)); + this.goalSelector.addGoal(8, new AvoidEntityGoal<>(this, TriceratopsEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(9, new AvoidEntityGoal<>(this, DilophosaurusEntity.class, (float) 20, 1.2, 1.2)); + this.goalSelector.addGoal(10, new AvoidEntityGoal<>(this, VelociraptorEntity.class, (float) 20, 1.2, 1.2)); + this.goalSelector.addGoal(11, new AvoidEntityGoal<>(this, CeratosaurusEntity.class, (float) 20, 1.2, 1.2)); + this.goalSelector.addGoal(12, new AvoidEntityGoal<>(this, SpinosaurusEntity.class, (float) 20, 1.2, 1.2)); + this.goalSelector.addGoal(13, new AvoidEntityGoal<>(this, TyrannosaurusRexEntity.class, (float) 20, 1.2, 1.2)); + this.goalSelector.addGoal(14, new AvoidEntityGoal<>(this, IndominusRexEntity.class, (float) 20, 1.2, 1.2)); + this.goalSelector.addGoal(15, new RandomLookAroundGoal(this)); + + } + + public static AttributeSupplier.Builder createAttributes() { + return Animal.createLivingAttributes() + .add(Attributes.MAX_HEALTH, 20D) + .add(Attributes.MOVEMENT_SPEED, 0.3D) + .add(Attributes.ARMOR, 0D) + .add(Attributes.FOLLOW_RANGE, 32D) + .add(Attributes.KNOCKBACK_RESISTANCE, 0D) + .add(Attributes.ATTACK_KNOCKBACK, 0D) + .add(Attributes.ATTACK_DAMAGE, 0D); + } + + @Override + public boolean isFood(ItemStack pStack) { + return pStack.is(Items.KELP); + } + + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel pLevel, AgeableMob pOtherParent) { + AgeableMob child = ModEntities.HYPSILOPHODON.get().create(pLevel); + if (child instanceof HypsilophodonEntity baby) { + HypsilophodonVariant randomVariant = Util.getRandom(HypsilophodonVariant.values(), this.random); + baby.setVariant(randomVariant); + } + return child; + } + + @Override + public boolean doHurtTarget(Entity target) { + boolean hit = super.doHurtTarget(target); + if (!level().isClientSide && hit && target instanceof LivingEntity) { + if (this.level() instanceof ServerLevel serverLevel) { + this.triggerAnim("attackController", "attack"); + } + } + return hit; + } + + @Override + public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { + controllers.add(new AnimationController<>(this, "Walk/Run/Idle", state -> { + if (state.isMoving()) + return state.setAndContinue(HypsilophodonEntity.this.isSprinting() ? RawAnimation.begin().then("anim.hypsilophodon.run", Animation.LoopType.LOOP) : RawAnimation.begin().then("anim.hypsilophodon.walk", Animation.LoopType.LOOP)); + + return state.setAndContinue(RawAnimation.begin().then("anim.hypsilophodon.idle", Animation.LoopType.LOOP)); + })); + + controllers.add(new AnimationController<>(this, "attackController", state -> PlayState.STOP) + .triggerableAnim("attack", RawAnimation.begin().then("anim.hypsilophodon.attack", Animation.LoopType.PLAY_ONCE))); + + controllers.add(new AnimationController<>(this, "mouthController", state -> PlayState.STOP) + .triggerableAnim("mouth", RawAnimation.begin().then("anim.hypsilophodon.mouth", Animation.LoopType.PLAY_ONCE))); + } + + private float getSignedTurnDelta() { + // Only consider the body (torso) rotation so head look does not affect tail sway + return Mth.wrapDegrees(this.yBodyRot - this.yBodyRotO); + } + + private int mouthAnimCooldown = 0; + + @Override + public void tick() { + super.tick(); + if (!level().isClientSide) { + this.entityData.set(DATA_SYNCED_AGE, this.getAge()); + var maxHealthAttr = getAttribute(Attributes.MAX_HEALTH); + if (maxHealthAttr != null) { + double baseAdult = DefaultAttributes.getSupplier((EntityType) this.getType()).getValue(Attributes.MAX_HEALTH); + double desired = this.isBaby() ? baseAdult * 0.10D : baseAdult; + if (maxHealthAttr.getBaseValue() != desired) { + double oldMax = maxHealthAttr.getBaseValue(); + double healthRatio = this.getHealth() / (float) oldMax; + maxHealthAttr.setBaseValue(desired); + this.setHealth((float) (desired * Mth.clamp(healthRatio, 0.0F, 1.0F))); + } + } + } + + if (!level().isClientSide) { + if (mouthAnimCooldown > 0) { + mouthAnimCooldown--; + } else { + this.triggerAnim("mouthController", "mouth"); + this.playSound(ModSounds.HYPSILOPHODON_CALL.get(), 1.0F, 1.0F); + // 30s–60s in ticks + mouthAnimCooldown = this.random.nextInt(1200 - 600 + 1) + 600; + } + } + + if (level().isClientSide) { + // Capture previous for smooth interpolation between ticks + this.tailSwayPrev = this.tailSwayOffset; + updateProceduralTailSway(); + } + } + + private void updateProceduralTailSway() { + // Turn input derived from rotation deltas; works even when standing still and turning + float turnDegrees = getSignedTurnDelta(); + + // Deadzone to ignore tiny jitter so the tail can return to center cleanly + float deadzoneDeg = 0.6f; // smaller deadzone for more responsiveness + float turnInput = 0.0f; + if (Math.abs(turnDegrees) >= deadzoneDeg) { + // Higher sensitivity so small in-place turns still affect the model + turnInput = Mth.clamp(turnDegrees / 15.0f, -1.0f, 1.0f); + } + + // Target offset: keep intuitive sign (positive input -> positive sway) + float target = turnInput; + + // One-pole low-pass (no bounce). Larger alpha => snappier and less "stiff". + float alpha = 0.24f; // try 0.20–0.30 to taste + + tailSwayOffset += (target - tailSwayOffset) * alpha; + + // Snap tiny residuals to zero so it visibly settles + if (Math.abs(tailSwayOffset) < 0.003f) { + tailSwayOffset = 0.0f; + } + + // No oscillation velocity retained + tailSwayVelocity = 0.0f; + + tailSwayOffset = Mth.clamp(tailSwayOffset, -1.5f, 1.5f); + } + + // Expose to the model for bone rotation + public float getTailSwayOffset() { + return tailSwayOffset; + } + + // Interpolated sway for smooth rendering between ticks + public float getTailSwayOffset(float partialTick) { + return Mth.lerp(Mth.clamp(partialTick, 0.0f, 1.0f), tailSwayPrev, tailSwayOffset); + } + + /*? if <=1.20.1 {*/ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); + this.entityData.define(VARIANT, 0); + this.entityData.define(DATA_SYNCED_AGE, 0); + } + /*?} else {*/ + /*@Override + protected void defineSynchedData(SynchedEntityData.Builder pBuilder) { + super.defineSynchedData(pBuilder); + pBuilder.define(VARIANT, 0); + pBuilder.define(DATA_SYNCED_AGE, 0); + } + *//*?}*/ + + public int getSyncedAge() { + return this.entityData.get(DATA_SYNCED_AGE); + } + public int getTypeVariant() { + return this.entityData.get(VARIANT); + } + + public HypsilophodonVariant getVariant() { + return HypsilophodonVariant.byId(this.getTypeVariant() & 255); + } + + private void setVariant(HypsilophodonVariant variant) { + this.entityData.set(VARIANT, variant.getId() & 255); + } + + @Override + public boolean canMate(Animal other) { + if (!super.canMate(other)) return false; + if (!(other instanceof HypsilophodonEntity that)) return false; + return this.getVariant() != that.getVariant(); + } + @Override + public void addAdditionalSaveData(CompoundTag pCompound) { + super.addAdditionalSaveData(pCompound); + pCompound.putInt("Variant", this.getTypeVariant()); + } + + @Override + public void readAdditionalSaveData(CompoundTag pCompound) { + super.readAdditionalSaveData(pCompound); + this.entityData.set(VARIANT, pCompound.getInt("Variant")); + } + + /*? if <=1.20.1 {*/ + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor pLevel, DifficultyInstance pDifficulty, MobSpawnType pReason, @Nullable SpawnGroupData pSpawnData, @Nullable CompoundTag pDataTag) { + HypsilophodonVariant variant = Util.getRandom(HypsilophodonVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(pLevel, pDifficulty, pReason, pSpawnData, pDataTag); + } + /*?} else {*/ + /*@Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, MobSpawnType spawnType, @Nullable SpawnGroupData spawnGroupData) { + HypsilophodonVariant variant = Util.getRandom(HypsilophodonVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(level, difficulty, spawnType, spawnGroupData); + } + *//*?}*/ + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } + + @Override + protected @Nullable SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.HYPSILOPHODON_HURT.get(); + } + + @Override + protected @Nullable SoundEvent getDeathSound() { + return ModSounds.HYPSILOPHODON_DEATH.get(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/custom/IndominusRexEntity.java b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/IndominusRexEntity.java new file mode 100644 index 0000000..c15083c --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/IndominusRexEntity.java @@ -0,0 +1,323 @@ +package net.cmr.jurassicrevived.entity.custom; + +import net.cmr.jurassicrevived.entity.ModEntities; +import net.cmr.jurassicrevived.entity.ai.SprintingMeleeAttackGoal; +import net.cmr.jurassicrevived.entity.ai.SprintingPanicGoal; +import net.cmr.jurassicrevived.entity.client.IndominusRexVariant; +import net.cmr.jurassicrevived.entity.client.IndominusRexVariant; +import net.cmr.jurassicrevived.sound.ModSounds; +import net.minecraft.Util; +import net.minecraft.core.BlockPos; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.util.Mth; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.*; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.attributes.DefaultAttributes; +import net.minecraft.world.entity.ai.goal.*; +import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal; +import net.minecraft.world.entity.ai.goal.target.NearestAttackableTargetGoal; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerLevelAccessor; +import net.minecraft.world.level.block.state.BlockState; +import org.jetbrains.annotations.Nullable; +import software.bernie.geckolib.animatable.GeoEntity; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.core.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.core.animation.AnimatableManager; +import software.bernie.geckolib.core.animation.Animation; +import software.bernie.geckolib.core.animation.AnimationController; +import software.bernie.geckolib.core.animation.RawAnimation; +import software.bernie.geckolib.core.object.PlayState; +/*?} else {*/ +/*import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.animation.*; +*//*?}*/ + +public class IndominusRexEntity extends Animal implements GeoEntity { + private AnimatableInstanceCache cache = new SingletonAnimatableInstanceCache(this); + + private static final EntityDataAccessor VARIANT = + SynchedEntityData.defineId(IndominusRexEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SYNCED_AGE = + SynchedEntityData.defineId(IndominusRexEntity.class, EntityDataSerializers.INT); + + // Procedural tail sway state (client-side use for rendering) + private float tailSwayOffset; // Smoothed offset in range roughly [-1, 1] + private float tailSwayVelocity; // Internal velocity for spring-damper + private float tailSwayPrev; // Previous frame value for interpolation + + public IndominusRexEntity(EntityType pEntityType, Level pLevel) { + super(pEntityType, pLevel); + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new SprintingPanicGoal(this, 1.15) { + @Override + public boolean canUse() { + return IndominusRexEntity.this.isBaby() && super.canUse(); + } + }); + this.targetSelector.addGoal(1, new HurtByTargetGoal(this).setAlertOthers()); + this.goalSelector.addGoal(2, new FloatGoal(this)); + this.goalSelector.addGoal(3, new AvoidEntityGoal<>(this, BrachiosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(4, new SprintingMeleeAttackGoal(this, 1.1, false)); + this.goalSelector.addGoal(5, new BreedGoal(this, 1.0)); + this.goalSelector.addGoal(6, new FollowParentGoal(this, 1.25)); + this.goalSelector.addGoal(7, new WaterAvoidingRandomStrollGoal(this, 1.0)); + this.targetSelector.addGoal(8, new NearestAttackableTargetGoal<>(this, Animal.class, 10, false, false, + target -> target.getType() != this.getType())); + this.goalSelector.addGoal(9, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(10, new FollowMobGoal(this, 1.2, (float) 20, (float) 10)); + this.targetSelector.addGoal(11, new NearestAttackableTargetGoal(this, TriceratopsEntity.class, false, false)); + this.targetSelector.addGoal(12, new NearestAttackableTargetGoal(this, GallimimusEntity.class, false, false)); + this.targetSelector.addGoal(13, new NearestAttackableTargetGoal(this, DilophosaurusEntity.class, false, false)); + this.targetSelector.addGoal(14, new NearestAttackableTargetGoal(this, CeratosaurusEntity.class, false, false)); + this.targetSelector.addGoal(15, new NearestAttackableTargetGoal(this, ParasaurolophusEntity.class, false, false)); + this.targetSelector.addGoal(16, new NearestAttackableTargetGoal(this, VelociraptorEntity.class, false, false)); + this.targetSelector.addGoal(17, new NearestAttackableTargetGoal(this, TyrannosaurusRexEntity.class, false, false)); + this.targetSelector.addGoal(18, new NearestAttackableTargetGoal(this, SpinosaurusEntity.class, false, false)); + this.targetSelector.addGoal(19, new NearestAttackableTargetGoal(this, Player.class, false, false)); + this.goalSelector.addGoal(20, new RandomLookAroundGoal(this)); + + + } + + public static AttributeSupplier.Builder createAttributes() { + return Animal.createLivingAttributes() + .add(Attributes.MAX_HEALTH, 90D) + .add(Attributes.MOVEMENT_SPEED, 0.3D) + .add(Attributes.ARMOR, 0D) + .add(Attributes.FOLLOW_RANGE, 32D) + .add(Attributes.KNOCKBACK_RESISTANCE, 0D) + .add(Attributes.ATTACK_KNOCKBACK, 0D) + .add(Attributes.ATTACK_DAMAGE, 25D); + } + + @Override + public boolean isFood(ItemStack pStack) { + return pStack.is(Items.BEEF); + } + + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel pLevel, AgeableMob pOtherParent) { + AgeableMob child = ModEntities.INDOMINUS_REX.get().create(pLevel); + if (child instanceof IndominusRexEntity baby) { + IndominusRexVariant randomVariant = Util.getRandom(IndominusRexVariant.values(), this.random); + baby.setVariant(randomVariant); + } + return child; + } + + @Override + public boolean doHurtTarget(Entity target) { + boolean hit = super.doHurtTarget(target); + if (!level().isClientSide && hit && target instanceof LivingEntity) { + if (this.level() instanceof ServerLevel serverLevel) { + this.triggerAnim("attackController", "attack"); + this.playSound(ModSounds.INDOMINUS_REX_ATTACK.get(), 1.0F, 1.0F); + } + } + return hit; + } + + @Override + public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { + controllers.add(new AnimationController<>(this, "Walk/Run/Idle", state -> { + if (state.isMoving()) + return state.setAndContinue(IndominusRexEntity.this.isSprinting() ? RawAnimation.begin().then("anim.indominus_rex.run", Animation.LoopType.LOOP) : RawAnimation.begin().then("anim.indominus_rex.walk", Animation.LoopType.LOOP)); + + return state.setAndContinue(RawAnimation.begin().then("anim.indominus_rex.idle", Animation.LoopType.LOOP)); + })); + + controllers.add(new AnimationController<>(this, "attackController", state -> PlayState.STOP) + .triggerableAnim("attack", RawAnimation.begin().then("anim.indominus_rex.attack", Animation.LoopType.PLAY_ONCE))); + + controllers.add(new AnimationController<>(this, "mouthController", state -> PlayState.STOP) + .triggerableAnim("mouth", RawAnimation.begin().then("anim.indominus_rex.mouth", Animation.LoopType.PLAY_ONCE))); + } + + private float getSignedTurnDelta() { + // Only consider the body (torso) rotation so head look does not affect tail sway + return Mth.wrapDegrees(this.yBodyRot - this.yBodyRotO); + } + + private int mouthAnimCooldown = 0; + + @Override + public void tick() { + super.tick(); + if (!level().isClientSide) { + this.entityData.set(DATA_SYNCED_AGE, this.getAge()); + var maxHealthAttr = getAttribute(Attributes.MAX_HEALTH); + if (maxHealthAttr != null) { + double baseAdult = DefaultAttributes.getSupplier((EntityType) this.getType()).getValue(Attributes.MAX_HEALTH); + double desired = this.isBaby() ? baseAdult * 0.10D : baseAdult; + if (maxHealthAttr.getBaseValue() != desired) { + double oldMax = maxHealthAttr.getBaseValue(); + double healthRatio = this.getHealth() / (float) oldMax; + maxHealthAttr.setBaseValue(desired); + this.setHealth((float) (desired * Mth.clamp(healthRatio, 0.0F, 1.0F))); + } + } + } + + if (!level().isClientSide) { + if (mouthAnimCooldown > 0) { + mouthAnimCooldown--; + } else { + this.triggerAnim("mouthController", "mouth"); + this.playSound(ModSounds.INDOMINUS_REX_CALL.get(), 1.0F, 1.0F); + // 30s–60s in ticks + mouthAnimCooldown = this.random.nextInt(1200 - 600 + 1) + 600; + } + } + + if (level().isClientSide) { + // Capture previous for smooth interpolation between ticks + this.tailSwayPrev = this.tailSwayOffset; + updateProceduralTailSway(); + } + } + + private void updateProceduralTailSway() { + // Turn input derived from rotation deltas; works even when standing still and turning + float turnDegrees = getSignedTurnDelta(); + + // Deadzone to ignore tiny jitter so the tail can return to center cleanly + float deadzoneDeg = 0.6f; // smaller deadzone for more responsiveness + float turnInput = 0.0f; + if (Math.abs(turnDegrees) >= deadzoneDeg) { + // Higher sensitivity so small in-place turns still affect the model + turnInput = Mth.clamp(turnDegrees / 15.0f, -1.0f, 1.0f); + } + + // Target offset: keep intuitive sign (positive input -> positive sway) + float target = turnInput; + + // One-pole low-pass (no bounce). Larger alpha => snappier and less "stiff". + float alpha = 0.24f; // try 0.20–0.30 to taste + + tailSwayOffset += (target - tailSwayOffset) * alpha; + + // Snap tiny residuals to zero so it visibly settles + if (Math.abs(tailSwayOffset) < 0.003f) { + tailSwayOffset = 0.0f; + } + + // No oscillation velocity retained + tailSwayVelocity = 0.0f; + + tailSwayOffset = Mth.clamp(tailSwayOffset, -1.5f, 1.5f); + } + + // Expose to the model for bone rotation + public float getTailSwayOffset() { + return tailSwayOffset; + } + + // Interpolated sway for smooth rendering between ticks + public float getTailSwayOffset(float partialTick) { + return Mth.lerp(Mth.clamp(partialTick, 0.0f, 1.0f), tailSwayPrev, tailSwayOffset); + } + + /*? if <=1.20.1 {*/ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); + this.entityData.define(VARIANT, 0); + this.entityData.define(DATA_SYNCED_AGE, 0); + } + /*?} else {*/ + /*@Override + protected void defineSynchedData(SynchedEntityData.Builder pBuilder) { + super.defineSynchedData(pBuilder); + pBuilder.define(VARIANT, 0); + pBuilder.define(DATA_SYNCED_AGE, 0); + } + *//*?}*/ + + public int getSyncedAge() { + return this.entityData.get(DATA_SYNCED_AGE); + } + public int getTypeVariant() { + return this.entityData.get(VARIANT); + } + + public IndominusRexVariant getVariant() { + return IndominusRexVariant.byId(this.getTypeVariant() & 255); + } + + private void setVariant(IndominusRexVariant variant) { + this.entityData.set(VARIANT, variant.getId() & 255); + } + + @Override + public boolean canMate(Animal other) { + if (!super.canMate(other)) return false; + if (!(other instanceof IndominusRexEntity that)) return false; + return this.getVariant() != that.getVariant(); + } + @Override + public void addAdditionalSaveData(CompoundTag pCompound) { + super.addAdditionalSaveData(pCompound); + pCompound.putInt("Variant", this.getTypeVariant()); + } + + @Override + public void readAdditionalSaveData(CompoundTag pCompound) { + super.readAdditionalSaveData(pCompound); + this.entityData.set(VARIANT, pCompound.getInt("Variant")); + } + + /*? if <=1.20.1 {*/ + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor pLevel, DifficultyInstance pDifficulty, MobSpawnType pReason, @Nullable SpawnGroupData pSpawnData, @Nullable CompoundTag pDataTag) { + IndominusRexVariant variant = Util.getRandom(IndominusRexVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(pLevel, pDifficulty, pReason, pSpawnData, pDataTag); + } + /*?} else {*/ + /*@Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, MobSpawnType spawnType, @Nullable SpawnGroupData spawnGroupData) { + IndominusRexVariant variant = Util.getRandom(IndominusRexVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(level, difficulty, spawnType, spawnGroupData); + } + *//*?}*/ + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } + + @Override + protected @Nullable SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.INDOMINUS_REX_HURT.get(); + } + + @Override + protected void playStepSound(BlockPos pos, BlockState blockIn) { + this.playSound(ModSounds.STOMP.get(), 0.5F, 1.0F); + } + + @Override + protected @Nullable SoundEvent getDeathSound() { + return ModSounds.INDOMINUS_REX_DEATH.get(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/custom/IndoraptorEntity.java b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/IndoraptorEntity.java new file mode 100644 index 0000000..561294c --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/IndoraptorEntity.java @@ -0,0 +1,315 @@ +package net.cmr.jurassicrevived.entity.custom; + +import net.cmr.jurassicrevived.entity.ModEntities; +import net.cmr.jurassicrevived.entity.ai.SprintingMeleeAttackGoal; +import net.cmr.jurassicrevived.entity.ai.SprintingPanicGoal; +import net.cmr.jurassicrevived.entity.client.IndoraptorVariant; +import net.cmr.jurassicrevived.entity.client.IndoraptorVariant; +import net.cmr.jurassicrevived.sound.ModSounds; +import net.minecraft.Util; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.util.Mth; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.*; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.attributes.DefaultAttributes; +import net.minecraft.world.entity.ai.goal.*; +import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal; +import net.minecraft.world.entity.ai.goal.target.NearestAttackableTargetGoal; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerLevelAccessor; +import org.jetbrains.annotations.Nullable; +import software.bernie.geckolib.animatable.GeoEntity; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.core.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.core.animation.AnimatableManager; +import software.bernie.geckolib.core.animation.Animation; +import software.bernie.geckolib.core.animation.AnimationController; +import software.bernie.geckolib.core.animation.RawAnimation; +import software.bernie.geckolib.core.object.PlayState; +/*?} else {*/ +/*import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.animation.*; +*//*?}*/ + +public class IndoraptorEntity extends Animal implements GeoEntity { + private AnimatableInstanceCache cache = new SingletonAnimatableInstanceCache(this); + + private static final EntityDataAccessor VARIANT = + SynchedEntityData.defineId(IndoraptorEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SYNCED_AGE = + SynchedEntityData.defineId(IndoraptorEntity.class, EntityDataSerializers.INT); + + // Procedural tail sway state (client-side use for rendering) + private float tailSwayOffset; // Smoothed offset in range roughly [-1, 1] + private float tailSwayVelocity; // Internal velocity for spring-damper + private float tailSwayPrev; // Previous frame value for interpolation + + public IndoraptorEntity(EntityType pEntityType, Level pLevel) { + super(pEntityType, pLevel); + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new SprintingPanicGoal(this, 1.15) { + @Override + public boolean canUse() { + return IndoraptorEntity.this.isBaby() && super.canUse(); + } + }); + this.targetSelector.addGoal(1, new HurtByTargetGoal(this).setAlertOthers()); + this.goalSelector.addGoal(2, new FloatGoal(this)); + this.goalSelector.addGoal(3, new AvoidEntityGoal<>(this, TriceratopsEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(4, new AvoidEntityGoal<>(this, SpinosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(5, new AvoidEntityGoal<>(this, TyrannosaurusRexEntity.class, (float) 20, 1.2, 1.2)); + this.goalSelector.addGoal(6, new AvoidEntityGoal<>(this, IndominusRexEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(7, new AvoidEntityGoal<>(this, BrachiosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(8, new SprintingMeleeAttackGoal(this, 1.1, false)); + this.goalSelector.addGoal(9, new BreedGoal(this, 1.0)); + this.goalSelector.addGoal(10, new FollowParentGoal(this, 1.25)); + this.goalSelector.addGoal(11, new WaterAvoidingRandomStrollGoal(this, 1.0)); + this.goalSelector.addGoal(12, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(13, new FollowMobGoal(this, 1, (float) 20, (float) 10)); + this.targetSelector.addGoal(14, new NearestAttackableTargetGoal<>(this, Animal.class, 10, false, false, + target -> target.getType() != this.getType())); + this.targetSelector.addGoal(15, new NearestAttackableTargetGoal(this, GallimimusEntity.class, false, false)); + this.targetSelector.addGoal(16, new NearestAttackableTargetGoal(this, DilophosaurusEntity.class, false, false)); + this.targetSelector.addGoal(17, new NearestAttackableTargetGoal(this, CeratosaurusEntity.class, false, false)); + this.targetSelector.addGoal(18, new NearestAttackableTargetGoal(this, ParasaurolophusEntity.class, false, false)); + this.targetSelector.addGoal(19, new NearestAttackableTargetGoal(this, Player.class, false, false)); + this.targetSelector.addGoal(20, new NearestAttackableTargetGoal(this, CompsognathusEntity.class, false, false)); + this.goalSelector.addGoal(21, new RandomLookAroundGoal(this)); + } + + public static AttributeSupplier.Builder createAttributes() { + return Animal.createLivingAttributes() + .add(Attributes.MAX_HEALTH, 55D) + .add(Attributes.MOVEMENT_SPEED, 0.3D) + .add(Attributes.ARMOR, 0D) + .add(Attributes.FOLLOW_RANGE, 32D) + .add(Attributes.ATTACK_DAMAGE, 16D) + .add(Attributes.KNOCKBACK_RESISTANCE, 0.3D) + .add(Attributes.ATTACK_KNOCKBACK, 0D); + } + + @Override + public boolean isFood(ItemStack pStack) { + return pStack.is(Items.BEEF); + } + + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel pLevel, AgeableMob pOtherParent) { + AgeableMob child = ModEntities.INDORAPTOR.get().create(pLevel); + if (child instanceof IndoraptorEntity baby) { + IndoraptorVariant randomVariant = Util.getRandom(IndoraptorVariant.values(), this.random); + baby.setVariant(randomVariant); + } + return child; + } + + @Override + public boolean doHurtTarget(Entity target) { + boolean hit = super.doHurtTarget(target); + if (!level().isClientSide && hit && target instanceof LivingEntity) { + if (this.level() instanceof ServerLevel serverLevel) { + this.triggerAnim("attackController", "attack"); + this.playSound(ModSounds.INDORAPTOR_ATTACK.get(), 1.0F, 1.0F); + } + } + return hit; + } + + @Override + public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { + controllers.add(new AnimationController<>(this, "Walk/Run/Idle", state -> { + if (state.isMoving()) + return state.setAndContinue(IndoraptorEntity.this.isSprinting() ? RawAnimation.begin().then("anim.indoraptor.run", Animation.LoopType.LOOP) : RawAnimation.begin().then("anim.indoraptor.walk", Animation.LoopType.LOOP)); + + return state.setAndContinue(RawAnimation.begin().then("anim.indoraptor.idle", Animation.LoopType.LOOP)); + })); + + controllers.add(new AnimationController<>(this, "attackController", state -> PlayState.STOP) + .triggerableAnim("attack", RawAnimation.begin().then("anim.indoraptor.attack", Animation.LoopType.PLAY_ONCE))); + + controllers.add(new AnimationController<>(this, "mouthController", state -> PlayState.STOP) + .triggerableAnim("mouth", RawAnimation.begin().then("anim.indoraptor.mouth", Animation.LoopType.PLAY_ONCE))); + } + + private float getSignedTurnDelta() { + // Only consider the body (torso) rotation so head look does not affect tail sway + return Mth.wrapDegrees(this.yBodyRot - this.yBodyRotO); + } + + private int mouthAnimCooldown = 0; + + @Override + public void tick() { + super.tick(); + if (!level().isClientSide) { + this.entityData.set(DATA_SYNCED_AGE, this.getAge()); + var maxHealthAttr = getAttribute(Attributes.MAX_HEALTH); + if (maxHealthAttr != null) { + double baseAdult = DefaultAttributes.getSupplier((EntityType) this.getType()).getValue(Attributes.MAX_HEALTH); + double desired = this.isBaby() ? baseAdult * 0.10D : baseAdult; + if (maxHealthAttr.getBaseValue() != desired) { + double oldMax = maxHealthAttr.getBaseValue(); + double healthRatio = this.getHealth() / (float) oldMax; + maxHealthAttr.setBaseValue(desired); + this.setHealth((float) (desired * Mth.clamp(healthRatio, 0.0F, 1.0F))); + } + } + } + + if (!level().isClientSide) { + if (mouthAnimCooldown > 0) { + mouthAnimCooldown--; + } else { + this.triggerAnim("mouthController", "mouth"); + this.playSound(ModSounds.INDORAPTOR_CALL.get(), 1.0F, 1.0F); + // 30s–60s in ticks + mouthAnimCooldown = this.random.nextInt(1200 - 600 + 1) + 600; + } + } + + if (level().isClientSide) { + // Capture previous for smooth interpolation between ticks + this.tailSwayPrev = this.tailSwayOffset; + updateProceduralTailSway(); + } + } + + private void updateProceduralTailSway() { + // Turn input derived from rotation deltas; works even when standing still and turning + float turnDegrees = getSignedTurnDelta(); + + // Deadzone to ignore tiny jitter so the tail can return to center cleanly + float deadzoneDeg = 0.6f; // smaller deadzone for more responsiveness + float turnInput = 0.0f; + if (Math.abs(turnDegrees) >= deadzoneDeg) { + // Higher sensitivity so small in-place turns still affect the model + turnInput = Mth.clamp(turnDegrees / 15.0f, -1.0f, 1.0f); + } + + // Target offset: keep intuitive sign (positive input -> positive sway) + float target = turnInput; + + // One-pole low-pass (no bounce). Larger alpha => snappier and less "stiff". + float alpha = 0.24f; // try 0.20–0.30 to taste + + tailSwayOffset += (target - tailSwayOffset) * alpha; + + // Snap tiny residuals to zero so it visibly settles + if (Math.abs(tailSwayOffset) < 0.003f) { + tailSwayOffset = 0.0f; + } + + // No oscillation velocity retained + tailSwayVelocity = 0.0f; + + tailSwayOffset = Mth.clamp(tailSwayOffset, -1.5f, 1.5f); + } + + // Expose to the model for bone rotation + public float getTailSwayOffset() { + return tailSwayOffset; + } + + // Interpolated sway for smooth rendering between ticks + public float getTailSwayOffset(float partialTick) { + return Mth.lerp(Mth.clamp(partialTick, 0.0f, 1.0f), tailSwayPrev, tailSwayOffset); + } + + /*? if <=1.20.1 {*/ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); + this.entityData.define(VARIANT, 0); + this.entityData.define(DATA_SYNCED_AGE, 0); + } + /*?} else {*/ + /*@Override + protected void defineSynchedData(SynchedEntityData.Builder pBuilder) { + super.defineSynchedData(pBuilder); + pBuilder.define(VARIANT, 0); + pBuilder.define(DATA_SYNCED_AGE, 0); + } + *//*?}*/ + + public int getSyncedAge() { + return this.entityData.get(DATA_SYNCED_AGE); + } + public int getTypeVariant() { + return this.entityData.get(VARIANT); + } + + public IndoraptorVariant getVariant() { + return IndoraptorVariant.byId(this.getTypeVariant() & 255); + } + + private void setVariant(IndoraptorVariant variant) { + this.entityData.set(VARIANT, variant.getId() & 255); + } + + @Override + public boolean canMate(Animal other) { + if (!super.canMate(other)) return false; + if (!(other instanceof IndoraptorEntity that)) return false; + return this.getVariant() != that.getVariant(); + } + @Override + public void addAdditionalSaveData(CompoundTag pCompound) { + super.addAdditionalSaveData(pCompound); + pCompound.putInt("Variant", this.getTypeVariant()); + } + + @Override + public void readAdditionalSaveData(CompoundTag pCompound) { + super.readAdditionalSaveData(pCompound); + this.entityData.set(VARIANT, pCompound.getInt("Variant")); + } + + /*? if <=1.20.1 {*/ + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor pLevel, DifficultyInstance pDifficulty, MobSpawnType pReason, @Nullable SpawnGroupData pSpawnData, @Nullable CompoundTag pDataTag) { + IndoraptorVariant variant = Util.getRandom(IndoraptorVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(pLevel, pDifficulty, pReason, pSpawnData, pDataTag); + } + /*?} else {*/ + /*@Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, MobSpawnType spawnType, @Nullable SpawnGroupData spawnGroupData) { + IndoraptorVariant variant = Util.getRandom(IndoraptorVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(level, difficulty, spawnType, spawnGroupData); + } + *//*?}*/ + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } + + @Override + protected @Nullable SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.INDORAPTOR_HURT.get(); + } + + @Override + protected @Nullable SoundEvent getDeathSound() { + return ModSounds.INDORAPTOR_DEATH.get(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/custom/InostranceviaEntity.java b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/InostranceviaEntity.java new file mode 100644 index 0000000..8f8143d --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/InostranceviaEntity.java @@ -0,0 +1,314 @@ +package net.cmr.jurassicrevived.entity.custom; + +import net.cmr.jurassicrevived.entity.ModEntities; +import net.cmr.jurassicrevived.entity.ai.SprintingMeleeAttackGoal; +import net.cmr.jurassicrevived.entity.ai.SprintingPanicGoal; +import net.cmr.jurassicrevived.entity.client.InostranceviaVariant; +import net.cmr.jurassicrevived.entity.client.InostranceviaVariant; +import net.cmr.jurassicrevived.sound.ModSounds; +import net.minecraft.Util; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.util.Mth; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.*; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.attributes.DefaultAttributes; +import net.minecraft.world.entity.ai.goal.*; +import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal; +import net.minecraft.world.entity.ai.goal.target.NearestAttackableTargetGoal; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerLevelAccessor; +import org.jetbrains.annotations.Nullable; +import software.bernie.geckolib.animatable.GeoEntity; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.core.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.core.animation.AnimatableManager; +import software.bernie.geckolib.core.animation.Animation; +import software.bernie.geckolib.core.animation.AnimationController; +import software.bernie.geckolib.core.animation.RawAnimation; +import software.bernie.geckolib.core.object.PlayState; +/*?} else {*/ +/*import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.animation.*; +*//*?}*/ + +public class InostranceviaEntity extends Animal implements GeoEntity { + private AnimatableInstanceCache cache = new SingletonAnimatableInstanceCache(this); + + private static final EntityDataAccessor VARIANT = + SynchedEntityData.defineId(InostranceviaEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SYNCED_AGE = + SynchedEntityData.defineId(InostranceviaEntity.class, EntityDataSerializers.INT); + + // Procedural tail sway state (client-side use for rendering) + private float tailSwayOffset; // Smoothed offset in range roughly [-1, 1] + private float tailSwayVelocity; // Internal velocity for spring-damper + private float tailSwayPrev; // Previous frame value for interpolation + + public InostranceviaEntity(EntityType pEntityType, Level pLevel) { + super(pEntityType, pLevel); + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new SprintingPanicGoal(this, 1.15) { + @Override + public boolean canUse() { + return InostranceviaEntity.this.isBaby() && super.canUse(); + } + }); + this.targetSelector.addGoal(1, new HurtByTargetGoal(this).setAlertOthers()); + this.goalSelector.addGoal(2, new FloatGoal(this)); + this.goalSelector.addGoal(3, new AvoidEntityGoal<>(this, SpinosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(4, new AvoidEntityGoal<>(this, TyrannosaurusRexEntity.class, (float) 20, 1.2, 1.2)); + this.goalSelector.addGoal(5, new AvoidEntityGoal<>(this, VelociraptorEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(6, new AvoidEntityGoal<>(this, ParasaurolophusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(7, new AvoidEntityGoal<>(this, CeratosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(8, new AvoidEntityGoal<>(this, TriceratopsEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(9, new AvoidEntityGoal<>(this, BrachiosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(10, new SprintingMeleeAttackGoal(this, 1.1, false)); + this.goalSelector.addGoal(11, new BreedGoal(this, 1.0)); + this.goalSelector.addGoal(12, new FollowParentGoal(this, 1.25)); + this.goalSelector.addGoal(13, new WaterAvoidingRandomStrollGoal(this, 1.0)); + this.goalSelector.addGoal(14, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(15, new FollowMobGoal(this, 1, (float) 20, (float) 10)); + this.targetSelector.addGoal(16, new NearestAttackableTargetGoal<>(this, Animal.class, 10, false, false, + target -> target.getType() != this.getType())); + this.targetSelector.addGoal(17, new NearestAttackableTargetGoal(this, GallimimusEntity.class, false, false)); + this.targetSelector.addGoal(18, new NearestAttackableTargetGoal(this, Player.class, false, false)); + this.targetSelector.addGoal(19, new NearestAttackableTargetGoal(this, CompsognathusEntity.class, false, false)); + this.goalSelector.addGoal(20, new RandomLookAroundGoal(this)); + } + + public static AttributeSupplier.Builder createAttributes() { + return Animal.createLivingAttributes() + .add(Attributes.MAX_HEALTH, 35D) + .add(Attributes.MOVEMENT_SPEED, 0.3D) + .add(Attributes.ARMOR, 0D) + .add(Attributes.FOLLOW_RANGE, 32D) + .add(Attributes.KNOCKBACK_RESISTANCE, 0.3D) + .add(Attributes.ATTACK_KNOCKBACK, 0D) + .add(Attributes.ATTACK_DAMAGE, 10D); + } + + @Override + public boolean isFood(ItemStack pStack) { + return pStack.is(Items.BEEF); + } + + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel pLevel, AgeableMob pOtherParent) { + AgeableMob child = ModEntities.INOSTRANCEVIA.get().create(pLevel); + if (child instanceof InostranceviaEntity baby) { + InostranceviaVariant randomVariant = Util.getRandom(InostranceviaVariant.values(), this.random); + baby.setVariant(randomVariant); + } + return child; + } + + @Override + public boolean doHurtTarget(Entity target) { + boolean hit = super.doHurtTarget(target); + if (!level().isClientSide && hit && target instanceof LivingEntity) { + if (this.level() instanceof ServerLevel serverLevel) { + this.triggerAnim("attackController", "attack"); + this.playSound(ModSounds.INOSTRANCEVIA_ATTACK.get(), 1.0F, 1.0F); + } + } + return hit; + } + + @Override + public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { + controllers.add(new AnimationController<>(this, "Walk/Run/Idle", state -> { + if (state.isMoving()) + return state.setAndContinue(InostranceviaEntity.this.isSprinting() ? RawAnimation.begin().then("anim.inostrancevia.run", Animation.LoopType.LOOP) : RawAnimation.begin().then("anim.inostrancevia.walk", Animation.LoopType.LOOP)); + + return state.setAndContinue(RawAnimation.begin().then("anim.inostrancevia.idle", Animation.LoopType.LOOP)); + })); + + controllers.add(new AnimationController<>(this, "attackController", state -> PlayState.STOP) + .triggerableAnim("attack", RawAnimation.begin().then("anim.inostrancevia.attack", Animation.LoopType.PLAY_ONCE))); + + controllers.add(new AnimationController<>(this, "mouthController", state -> PlayState.STOP) + .triggerableAnim("mouth", RawAnimation.begin().then("anim.inostrancevia.mouth", Animation.LoopType.PLAY_ONCE))); + } + + private float getSignedTurnDelta() { + // Only consider the body (torso) rotation so head look does not affect tail sway + return Mth.wrapDegrees(this.yBodyRot - this.yBodyRotO); + } + + private int mouthAnimCooldown = 0; + + @Override + public void tick() { + super.tick(); + if (!level().isClientSide) { + this.entityData.set(DATA_SYNCED_AGE, this.getAge()); + var maxHealthAttr = getAttribute(Attributes.MAX_HEALTH); + if (maxHealthAttr != null) { + double baseAdult = DefaultAttributes.getSupplier((EntityType) this.getType()).getValue(Attributes.MAX_HEALTH); + double desired = this.isBaby() ? baseAdult * 0.10D : baseAdult; + if (maxHealthAttr.getBaseValue() != desired) { + double oldMax = maxHealthAttr.getBaseValue(); + double healthRatio = this.getHealth() / (float) oldMax; + maxHealthAttr.setBaseValue(desired); + this.setHealth((float) (desired * Mth.clamp(healthRatio, 0.0F, 1.0F))); + } + } + } + + if (!level().isClientSide) { + if (mouthAnimCooldown > 0) { + mouthAnimCooldown--; + } else { + this.triggerAnim("mouthController", "mouth"); + this.playSound(ModSounds.INOSTRANCEVIA_CALL.get(), 1.0F, 1.0F); + // 30s–60s in ticks + mouthAnimCooldown = this.random.nextInt(1200 - 600 + 1) + 600; + } + } + + if (level().isClientSide) { + // Capture previous for smooth interpolation between ticks + this.tailSwayPrev = this.tailSwayOffset; + updateProceduralTailSway(); + } + } + + private void updateProceduralTailSway() { + // Turn input derived from rotation deltas; works even when standing still and turning + float turnDegrees = getSignedTurnDelta(); + + // Deadzone to ignore tiny jitter so the tail can return to center cleanly + float deadzoneDeg = 0.6f; // smaller deadzone for more responsiveness + float turnInput = 0.0f; + if (Math.abs(turnDegrees) >= deadzoneDeg) { + // Higher sensitivity so small in-place turns still affect the model + turnInput = Mth.clamp(turnDegrees / 15.0f, -1.0f, 1.0f); + } + + // Target offset: keep intuitive sign (positive input -> positive sway) + float target = turnInput; + + // One-pole low-pass (no bounce). Larger alpha => snappier and less "stiff". + float alpha = 0.24f; // try 0.20–0.30 to taste + + tailSwayOffset += (target - tailSwayOffset) * alpha; + + // Snap tiny residuals to zero so it visibly settles + if (Math.abs(tailSwayOffset) < 0.003f) { + tailSwayOffset = 0.0f; + } + + // No oscillation velocity retained + tailSwayVelocity = 0.0f; + + tailSwayOffset = Mth.clamp(tailSwayOffset, -1.5f, 1.5f); + } + + // Expose to the model for bone rotation + public float getTailSwayOffset() { + return tailSwayOffset; + } + + // Interpolated sway for smooth rendering between ticks + public float getTailSwayOffset(float partialTick) { + return Mth.lerp(Mth.clamp(partialTick, 0.0f, 1.0f), tailSwayPrev, tailSwayOffset); + } + + /*? if <=1.20.1 {*/ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); + this.entityData.define(VARIANT, 0); + this.entityData.define(DATA_SYNCED_AGE, 0); + } + /*?} else {*/ + /*@Override + protected void defineSynchedData(SynchedEntityData.Builder pBuilder) { + super.defineSynchedData(pBuilder); + pBuilder.define(VARIANT, 0); + pBuilder.define(DATA_SYNCED_AGE, 0); + } + *//*?}*/ + + public int getSyncedAge() { + return this.entityData.get(DATA_SYNCED_AGE); + } + public int getTypeVariant() { + return this.entityData.get(VARIANT); + } + + public InostranceviaVariant getVariant() { + return InostranceviaVariant.byId(this.getTypeVariant() & 255); + } + + private void setVariant(InostranceviaVariant variant) { + this.entityData.set(VARIANT, variant.getId() & 255); + } + + @Override + public boolean canMate(Animal other) { + if (!super.canMate(other)) return false; + if (!(other instanceof InostranceviaEntity that)) return false; + return this.getVariant() != that.getVariant(); + } + @Override + public void addAdditionalSaveData(CompoundTag pCompound) { + super.addAdditionalSaveData(pCompound); + pCompound.putInt("Variant", this.getTypeVariant()); + } + + @Override + public void readAdditionalSaveData(CompoundTag pCompound) { + super.readAdditionalSaveData(pCompound); + this.entityData.set(VARIANT, pCompound.getInt("Variant")); + } + + /*? if <=1.20.1 {*/ + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor pLevel, DifficultyInstance pDifficulty, MobSpawnType pReason, @Nullable SpawnGroupData pSpawnData, @Nullable CompoundTag pDataTag) { + InostranceviaVariant variant = Util.getRandom(InostranceviaVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(pLevel, pDifficulty, pReason, pSpawnData, pDataTag); + } + /*?} else {*/ + /*@Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, MobSpawnType spawnType, @Nullable SpawnGroupData spawnGroupData) { + InostranceviaVariant variant = Util.getRandom(InostranceviaVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(level, difficulty, spawnType, spawnGroupData); + } + *//*?}*/ + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } + + @Override + protected @Nullable SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.INOSTRANCEVIA_HURT.get(); + } + + @Override + protected @Nullable SoundEvent getDeathSound() { + return ModSounds.INOSTRANCEVIA_DEATH.get(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/custom/LambeosaurusEntity.java b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/LambeosaurusEntity.java new file mode 100644 index 0000000..a7729d7 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/LambeosaurusEntity.java @@ -0,0 +1,297 @@ +package net.cmr.jurassicrevived.entity.custom; + +import net.cmr.jurassicrevived.entity.ModEntities; +import net.cmr.jurassicrevived.entity.ai.SprintingPanicGoal; +import net.cmr.jurassicrevived.entity.client.LambeosaurusVariant; +import net.cmr.jurassicrevived.entity.client.LambeosaurusVariant; +import net.cmr.jurassicrevived.sound.ModSounds; +import net.minecraft.Util; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.util.Mth; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.*; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.attributes.DefaultAttributes; +import net.minecraft.world.entity.ai.goal.*; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerLevelAccessor; +import org.jetbrains.annotations.Nullable; +import software.bernie.geckolib.animatable.GeoEntity; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.core.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.core.animation.AnimatableManager; +import software.bernie.geckolib.core.animation.Animation; +import software.bernie.geckolib.core.animation.AnimationController; +import software.bernie.geckolib.core.animation.RawAnimation; +import software.bernie.geckolib.core.object.PlayState; +/*?} else {*/ +/*import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.animation.*; +*//*?}*/ + +public class LambeosaurusEntity extends Animal implements GeoEntity { + private AnimatableInstanceCache cache = new SingletonAnimatableInstanceCache(this); + + private static final EntityDataAccessor VARIANT = + SynchedEntityData.defineId(LambeosaurusEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SYNCED_AGE = + SynchedEntityData.defineId(LambeosaurusEntity.class, EntityDataSerializers.INT); + + // Procedural tail sway state (client-side use for rendering) + private float tailSwayOffset; // Smoothed offset in range roughly [-1, 1] + private float tailSwayVelocity; // Internal velocity for spring-damper + private float tailSwayPrev; // Previous frame value for interpolation + + public LambeosaurusEntity(EntityType pEntityType, Level pLevel) { + super(pEntityType, pLevel); + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new SprintingPanicGoal(this, 1.15)); + this.goalSelector.addGoal(2, new FloatGoal(this)); + this.goalSelector.addGoal(3, new BreedGoal(this, 1.0)); + this.goalSelector.addGoal(4, new FollowParentGoal(this, 1.25)); + this.goalSelector.addGoal(5, new WaterAvoidingRandomStrollGoal(this, 1.0)); + this.goalSelector.addGoal(6, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(7, new FollowMobGoal(this, 1, (float) 20, (float) 10)); + this.goalSelector.addGoal(8, new AvoidEntityGoal<>(this, DilophosaurusEntity.class, (float) 20, 1.2, 1.2)); + this.goalSelector.addGoal(9, new AvoidEntityGoal<>(this, VelociraptorEntity.class, (float) 20, 1.2, 1.2)); + this.goalSelector.addGoal(10, new AvoidEntityGoal<>(this, CeratosaurusEntity.class, (float) 20, 1.2, 1.2)); + this.goalSelector.addGoal(11, new AvoidEntityGoal<>(this, SpinosaurusEntity.class, (float) 20, 1.2, 1.2)); + this.goalSelector.addGoal(12, new AvoidEntityGoal<>(this, TyrannosaurusRexEntity.class, (float) 20, 1.2, 1.2)); + this.goalSelector.addGoal(13, new RandomLookAroundGoal(this)); + + } + + public static AttributeSupplier.Builder createAttributes() { + return Animal.createLivingAttributes() + .add(Attributes.MAX_HEALTH, 55D) + .add(Attributes.MOVEMENT_SPEED, 0.3D) + .add(Attributes.ARMOR, 0D) + .add(Attributes.FOLLOW_RANGE, 32D) + .add(Attributes.KNOCKBACK_RESISTANCE, 0D) + .add(Attributes.ATTACK_KNOCKBACK, 0D) + .add(Attributes.ATTACK_DAMAGE, 12D); + } + + @Override + public boolean isFood(ItemStack pStack) { + return pStack.is(Items.KELP); + } + + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel pLevel, AgeableMob pOtherParent) { + AgeableMob child = ModEntities.LAMBEOSAURUS.get().create(pLevel); + if (child instanceof LambeosaurusEntity baby) { + LambeosaurusVariant randomVariant = Util.getRandom(LambeosaurusVariant.values(), this.random); + baby.setVariant(randomVariant); + } + return child; + } + + @Override + public boolean doHurtTarget(Entity target) { + boolean hit = super.doHurtTarget(target); + if (!level().isClientSide && hit && target instanceof LivingEntity) { + if (this.level() instanceof ServerLevel serverLevel) { + this.triggerAnim("attackController", "attack"); + } + } + return hit; + } + + @Override + public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { + controllers.add(new AnimationController<>(this, "Walk/Run/Idle", state -> { + if (state.isMoving()) + return state.setAndContinue(LambeosaurusEntity.this.isSprinting() ? RawAnimation.begin().then("anim.lambeosaurus.run", Animation.LoopType.LOOP) : RawAnimation.begin().then("anim.lambeosaurus.walk", Animation.LoopType.LOOP)); + + return state.setAndContinue(RawAnimation.begin().then("anim.lambeosaurus.idle", Animation.LoopType.LOOP)); + })); + + controllers.add(new AnimationController<>(this, "attackController", state -> PlayState.STOP) + .triggerableAnim("attack", RawAnimation.begin().then("anim.lambeosaurus.attack", Animation.LoopType.PLAY_ONCE))); + + controllers.add(new AnimationController<>(this, "mouthController", state -> PlayState.STOP) + .triggerableAnim("mouth", RawAnimation.begin().then("anim.lambeosaurus.mouth", Animation.LoopType.PLAY_ONCE))); + } + + private float getSignedTurnDelta() { + // Only consider the body (torso) rotation so head look does not affect tail sway + return Mth.wrapDegrees(this.yBodyRot - this.yBodyRotO); + } + + private int mouthAnimCooldown = 0; + + @Override + public void tick() { + super.tick(); + if (!level().isClientSide) { + this.entityData.set(DATA_SYNCED_AGE, this.getAge()); + var maxHealthAttr = getAttribute(Attributes.MAX_HEALTH); + if (maxHealthAttr != null) { + double baseAdult = DefaultAttributes.getSupplier((EntityType) this.getType()).getValue(Attributes.MAX_HEALTH); + double desired = this.isBaby() ? baseAdult * 0.10D : baseAdult; + if (maxHealthAttr.getBaseValue() != desired) { + double oldMax = maxHealthAttr.getBaseValue(); + double healthRatio = this.getHealth() / (float) oldMax; + maxHealthAttr.setBaseValue(desired); + this.setHealth((float) (desired * Mth.clamp(healthRatio, 0.0F, 1.0F))); + } + } + } + + if (!level().isClientSide) { + if (mouthAnimCooldown > 0) { + mouthAnimCooldown--; + } else { + this.triggerAnim("mouthController", "mouth"); + this.playSound(ModSounds.LAMBEOSAURUS_CALL.get(), 1.0F, 1.0F); + // 30s–60s in ticks + mouthAnimCooldown = this.random.nextInt(1200 - 600 + 1) + 600; + } + } + + if (level().isClientSide) { + // Capture previous for smooth interpolation between ticks + this.tailSwayPrev = this.tailSwayOffset; + updateProceduralTailSway(); + } + } + + private void updateProceduralTailSway() { + // Turn input derived from rotation deltas; works even when standing still and turning + float turnDegrees = getSignedTurnDelta(); + + // Deadzone to ignore tiny jitter so the tail can return to center cleanly + float deadzoneDeg = 0.6f; // smaller deadzone for more responsiveness + float turnInput = 0.0f; + if (Math.abs(turnDegrees) >= deadzoneDeg) { + // Higher sensitivity so small in-place turns still affect the model + turnInput = Mth.clamp(turnDegrees / 15.0f, -1.0f, 1.0f); + } + + // Target offset: keep intuitive sign (positive input -> positive sway) + float target = turnInput; + + // One-pole low-pass (no bounce). Larger alpha => snappier and less "stiff". + float alpha = 0.24f; // try 0.20–0.30 to taste + + tailSwayOffset += (target - tailSwayOffset) * alpha; + + // Snap tiny residuals to zero so it visibly settles + if (Math.abs(tailSwayOffset) < 0.003f) { + tailSwayOffset = 0.0f; + } + + // No oscillation velocity retained + tailSwayVelocity = 0.0f; + + tailSwayOffset = Mth.clamp(tailSwayOffset, -1.5f, 1.5f); + } + + // Expose to the model for bone rotation + public float getTailSwayOffset() { + return tailSwayOffset; + } + + // Interpolated sway for smooth rendering between ticks + public float getTailSwayOffset(float partialTick) { + return Mth.lerp(Mth.clamp(partialTick, 0.0f, 1.0f), tailSwayPrev, tailSwayOffset); + } + + /*? if <=1.20.1 {*/ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); + this.entityData.define(VARIANT, 0); + this.entityData.define(DATA_SYNCED_AGE, 0); + } + /*?} else {*/ + /*@Override + protected void defineSynchedData(SynchedEntityData.Builder pBuilder) { + super.defineSynchedData(pBuilder); + pBuilder.define(VARIANT, 0); + pBuilder.define(DATA_SYNCED_AGE, 0); + } + *//*?}*/ + + public int getSyncedAge() { + return this.entityData.get(DATA_SYNCED_AGE); + } + public int getTypeVariant() { + return this.entityData.get(VARIANT); + } + + public LambeosaurusVariant getVariant() { + return LambeosaurusVariant.byId(this.getTypeVariant() & 255); + } + + private void setVariant(LambeosaurusVariant variant) { + this.entityData.set(VARIANT, variant.getId() & 255); + } + + @Override + public boolean canMate(Animal other) { + if (!super.canMate(other)) return false; + if (!(other instanceof LambeosaurusEntity that)) return false; + return this.getVariant() != that.getVariant(); + } + @Override + public void addAdditionalSaveData(CompoundTag pCompound) { + super.addAdditionalSaveData(pCompound); + pCompound.putInt("Variant", this.getTypeVariant()); + } + + @Override + public void readAdditionalSaveData(CompoundTag pCompound) { + super.readAdditionalSaveData(pCompound); + this.entityData.set(VARIANT, pCompound.getInt("Variant")); + } + + /*? if <=1.20.1 {*/ + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor pLevel, DifficultyInstance pDifficulty, MobSpawnType pReason, @Nullable SpawnGroupData pSpawnData, @Nullable CompoundTag pDataTag) { + LambeosaurusVariant variant = Util.getRandom(LambeosaurusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(pLevel, pDifficulty, pReason, pSpawnData, pDataTag); + } + /*?} else {*/ + /*@Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, MobSpawnType spawnType, @Nullable SpawnGroupData spawnGroupData) { + LambeosaurusVariant variant = Util.getRandom(LambeosaurusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(level, difficulty, spawnType, spawnGroupData); + } + *//*?}*/ + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } + + @Override + protected @Nullable SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.LAMBEOSAURUS_HURT.get(); + } + + @Override + protected @Nullable SoundEvent getDeathSound() { + return ModSounds.LAMBEOSAURUS_DEATH.get(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/custom/LudodactylusEntity.java b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/LudodactylusEntity.java new file mode 100644 index 0000000..9793f3b --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/LudodactylusEntity.java @@ -0,0 +1,411 @@ +package net.cmr.jurassicrevived.entity.custom; + +import net.cmr.jurassicrevived.entity.ModEntities; +import net.cmr.jurassicrevived.entity.ai.SprintingMeleeAttackGoal; +import net.cmr.jurassicrevived.entity.ai.SprintingPanicGoal; +import net.cmr.jurassicrevived.entity.client.LudodactylusVariant; +import net.cmr.jurassicrevived.entity.client.LudodactylusVariant; +import net.cmr.jurassicrevived.sound.ModSounds; +import net.minecraft.Util; +import net.minecraft.core.BlockPos; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.*; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.attributes.DefaultAttributes; +import net.minecraft.world.entity.ai.control.FlyingMoveControl; +import net.minecraft.world.entity.ai.goal.*; +import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal; +import net.minecraft.world.entity.ai.goal.target.NearestAttackableTargetGoal; +import net.minecraft.world.entity.ai.navigation.FlyingPathNavigation; +import net.minecraft.world.entity.ai.navigation.PathNavigation; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.animal.FlyingAnimal; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerLevelAccessor; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.phys.Vec3; +import org.jetbrains.annotations.Nullable; +import software.bernie.geckolib.animatable.GeoEntity; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.core.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.core.animation.AnimatableManager; +import software.bernie.geckolib.core.animation.Animation; +import software.bernie.geckolib.core.animation.AnimationController; +import software.bernie.geckolib.core.animation.RawAnimation; +import software.bernie.geckolib.core.object.PlayState; +/*?} else {*/ +/*import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.animation.*; +*//*?}*/ + +public class LudodactylusEntity extends Animal implements GeoEntity, FlyingAnimal { + private final AnimatableInstanceCache cache = new SingletonAnimatableInstanceCache(this); + + private static final EntityDataAccessor VARIANT = + SynchedEntityData.defineId(LudodactylusEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SYNCED_AGE = + SynchedEntityData.defineId(LudodactylusEntity.class, EntityDataSerializers.INT); + + // Procedural tail sway state (client-side use for rendering) + private float tailSwayOffset; // Smoothed offset in range roughly [-1, 1] + private float tailSwayVelocity; // Internal velocity for spring-damper + private float tailSwayPrev; // Previous frame value for interpolation + + public LudodactylusEntity(EntityType pEntityType, Level pLevel) { + super(pEntityType, pLevel); + this.moveControl = new FlyingMoveControl(this, 20, true); + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new SprintingPanicGoal(this, 1.15) { + @Override + public boolean canUse() { + return LudodactylusEntity.this.isBaby() && super.canUse(); + } + }); + this.goalSelector.addGoal(2, new FloatGoal(this)); + this.goalSelector.addGoal(3, new AvoidEntityGoal<>(this, BrachiosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(4, new SprintingMeleeAttackGoal(this, 1.1, false)); + this.goalSelector.addGoal(5, new BreedGoal(this, 1.0)); + this.goalSelector.addGoal(6, new FollowParentGoal(this, 1.25)); + + // Goal 7: Wander on ground (Walk) - Only when on ground + this.goalSelector.addGoal(7, new WaterAvoidingRandomStrollGoal(this, 1.0) { + @Override + public boolean canUse() { + return LudodactylusEntity.this.onGround() && super.canUse(); + } + }); + + // Goal 8: Wander in air (Fly) - Handles takeoff, flying, and landing + this.goalSelector.addGoal(8, new WaterAvoidingRandomStrollGoal(this, 1.0, 20) { + @Override + public boolean canUse() { + boolean isFlying = !LudodactylusEntity.this.onGround(); + // If flying, keep flying. If on ground, small chance (1/400 ticks) to take off. + return (isFlying || LudodactylusEntity.this.getRandom().nextInt(400) == 0) && super.canUse(); + } + + @Override + protected Vec3 getPosition() { + Vec3 pos = LudodactylusEntity.this.position(); + RandomSource random = LudodactylusEntity.this.getRandom(); + + double x = pos.x + (random.nextFloat() * 2 - 1) * 32; + double z = pos.z + (random.nextFloat() * 2 - 1) * 32; + + // Get ground height at the random destination (returns Y of first air block) + int groundY = LudodactylusEntity.this.level().getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, (int)x, (int)z); + double y; + + if (LudodactylusEntity.this.onGround()) { + // Takeoff: Target well above ground to ensure liftoff + y = pos.y + 15 + random.nextInt(10); + } else { + // Flying: 5% chance to land, otherwise stay airborne but capped height + if (random.nextFloat() < 0.05f) { + y = groundY; // Land + } else if (pos.y > groundY + 20) { + // Too high: Force descent + y = pos.y - 5 - random.nextInt(10); + } else { + // Just wander up or down a bit + y = pos.y + (random.nextFloat() * 2 - 1) * 10; + } + } + + // Don't target below the ground (blocks) + if (y < groundY) y = groundY; + + return new Vec3(x, y, z); + } + }); + + this.goalSelector.addGoal(9, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(10, new FollowMobGoal(this, 0.8, (float) 20, (float) 10)); + this.targetSelector.addGoal(11, new NearestAttackableTargetGoal<>(this, Animal.class, 10, false, false, + target -> { + // 1. Don't eat your own species + if (target.getType() == this.getType()) return false; + + // 2. Don't eat Flying Animals + if (target instanceof FlyingAnimal) return false; + + // 3. SIZE CHECK: specific height and width limits + // Example: Height < 1.0 blocks AND Width < 1.0 blocks + boolean isSmallEnough = target.getBbHeight() <= 1.0F && target.getBbWidth() <= 1.0F; + + return isSmallEnough; + } + )); + this.targetSelector.addGoal(12, new NearestAttackableTargetGoal<>(this, Player.class, false, false)); + this.targetSelector.addGoal(13, new HurtByTargetGoal(this)); + this.goalSelector.addGoal(14, new RandomLookAroundGoal(this)); + this.goalSelector.addGoal(15, new FloatGoal(this)); + } + + public static AttributeSupplier.Builder createAttributes() { + return Animal.createLivingAttributes() + .add(Attributes.MAX_HEALTH, 50D) + .add(Attributes.MOVEMENT_SPEED, 0.3D) + .add(Attributes.FLYING_SPEED, 0.6D) + .add(Attributes.ARMOR, 0D) + .add(Attributes.FOLLOW_RANGE, 32D) + .add(Attributes.KNOCKBACK_RESISTANCE, 0D) + .add(Attributes.ATTACK_KNOCKBACK, 0D) + .add(Attributes.ATTACK_DAMAGE, 8D); + } + + @Override + protected PathNavigation createNavigation(Level pLevel) { + FlyingPathNavigation navigation = new FlyingPathNavigation(this, pLevel); + navigation.setCanOpenDoors(false); + navigation.setCanFloat(true); + navigation.setCanPassDoors(true); + return navigation; + } + + @Override + public boolean isFlying() { + return !this.onGround(); + } + + @Override + public boolean causeFallDamage(float pFallDistance, float pMultiplier, DamageSource pSource) { + return false; + } + + @Override + protected void checkFallDamage(double pY, boolean pOnGround, BlockState pState, BlockPos pPos) { + } + + @Override + public boolean isFood(ItemStack pStack) { + return pStack.is(Items.BEEF); + } + + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel pLevel, AgeableMob pOtherParent) { + AgeableMob child = ModEntities.LUDODACTYLUS.get().create(pLevel); + if (child instanceof LudodactylusEntity baby) { + LudodactylusVariant randomVariant = Util.getRandom(LudodactylusVariant.values(), this.random); + baby.setVariant(randomVariant); + } + return child; + } + + @Override + public boolean doHurtTarget(Entity target) { + boolean hit = super.doHurtTarget(target); + if (!level().isClientSide && hit && target instanceof LivingEntity) { + if (this.level() instanceof ServerLevel serverLevel) { + this.triggerAnim("attackController", "attack"); + this.playSound(ModSounds.BEAK_ATTACK.get(), 1.0F, 1.0F); + } + } + return hit; + } + + @Override + public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { + controllers.add(new AnimationController<>(this, "Walk/Run/Idle", state -> { + if (!LudodactylusEntity.this.onGround()) { + return state.setAndContinue(RawAnimation.begin().then("anim.ludodactylus.fly", Animation.LoopType.LOOP)); + } + + if (state.isMoving()) + return state.setAndContinue(LudodactylusEntity.this.isSprinting() ? RawAnimation.begin().then("anim.ludodactylus.walk", Animation.LoopType.LOOP) : RawAnimation.begin().then("anim.ludodactylus.walk", Animation.LoopType.LOOP)); + + return state.setAndContinue(RawAnimation.begin().then("anim.ludodactylus.idle", Animation.LoopType.LOOP)); + })); + + controllers.add(new AnimationController<>(this, "attackController", state -> PlayState.STOP) + .triggerableAnim("attack", RawAnimation.begin().then("anim.ludodactylus.attack", Animation.LoopType.PLAY_ONCE))); + + controllers.add(new AnimationController<>(this, "mouthController", state -> PlayState.STOP) + .triggerableAnim("mouth", RawAnimation.begin().then("anim.ludodactylus.mouth", Animation.LoopType.PLAY_ONCE))); + } + + private float getSignedTurnDelta() { + // Only consider the body (torso) rotation so head look does not affect tail sway + return Mth.wrapDegrees(this.yBodyRot - this.yBodyRotO); + } + + private int mouthAnimCooldown = 0; + + @Override + public void tick() { + super.tick(); + if (!level().isClientSide) { + this.entityData.set(DATA_SYNCED_AGE, this.getAge()); + var maxHealthAttr = getAttribute(Attributes.MAX_HEALTH); + if (maxHealthAttr != null) { + double baseAdult = DefaultAttributes.getSupplier((EntityType) this.getType()).getValue(Attributes.MAX_HEALTH); + double desired = this.isBaby() ? baseAdult * 0.10D : baseAdult; + if (maxHealthAttr.getBaseValue() != desired) { + double oldMax = maxHealthAttr.getBaseValue(); + double healthRatio = this.getHealth() / (float) oldMax; + maxHealthAttr.setBaseValue(desired); + this.setHealth((float) (desired * Mth.clamp(healthRatio, 0.0F, 1.0F))); + } + } + } + + if (!level().isClientSide) { + if (mouthAnimCooldown > 0) { + mouthAnimCooldown--; + } else { + this.triggerAnim("mouthController", "mouth"); + this.playSound(ModSounds.LUDODACTYLUS_CALL.get(), 1.0F, 1.0F); + // 30s–60s in ticks + mouthAnimCooldown = this.random.nextInt(1200 - 600 + 1) + 600; + } + } + + if (level().isClientSide) { + // Capture previous for smooth interpolation between ticks + this.tailSwayPrev = this.tailSwayOffset; + updateProceduralTailSway(); + } + } + + private void updateProceduralTailSway() { + // Turn input derived from rotation deltas; works even when standing still and turning + float turnDegrees = getSignedTurnDelta(); + + // Deadzone to ignore tiny jitter so the tail can return to center cleanly + float deadzoneDeg = 0.6f; // smaller deadzone for more responsiveness + float turnInput = 0.0f; + if (Math.abs(turnDegrees) >= deadzoneDeg) { + // Higher sensitivity so small in-place turns still affect the model + turnInput = Mth.clamp(turnDegrees / 15.0f, -1.0f, 1.0f); + } + + // Target offset: keep intuitive sign (positive input -> positive sway) + float target = turnInput; + + // One-pole low-pass (no bounce). Larger alpha => snappier and less "stiff". + float alpha = 0.24f; // try 0.20–0.30 to taste + + tailSwayOffset += (target - tailSwayOffset) * alpha; + + // Snap tiny residuals to zero so it visibly settles + if (Math.abs(tailSwayOffset) < 0.003f) { + tailSwayOffset = 0.0f; + } + + // No oscillation velocity retained + tailSwayVelocity = 0.0f; + + tailSwayOffset = Mth.clamp(tailSwayOffset, -1.5f, 1.5f); + } + + // Expose to the model for bone rotation + public float getTailSwayOffset() { + return tailSwayOffset; + } + + // Interpolated sway for smooth rendering between ticks + public float getTailSwayOffset(float partialTick) { + return Mth.lerp(Mth.clamp(partialTick, 0.0f, 1.0f), tailSwayPrev, tailSwayOffset); + } + + /*? if <=1.20.1 {*/ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); + this.entityData.define(VARIANT, 0); + this.entityData.define(DATA_SYNCED_AGE, 0); + } + /*?} else {*/ + /*@Override + protected void defineSynchedData(SynchedEntityData.Builder pBuilder) { + super.defineSynchedData(pBuilder); + pBuilder.define(VARIANT, 0); + pBuilder.define(DATA_SYNCED_AGE, 0); + } + *//*?}*/ + + public int getSyncedAge() { + return this.entityData.get(DATA_SYNCED_AGE); + } + + public LudodactylusVariant getVariant() { + return LudodactylusVariant.byId(this.getTypeVariant() & 255); + } + public int getTypeVariant() { + return this.entityData.get(VARIANT); + } + + private void setVariant(LudodactylusVariant variant) { + this.entityData.set(VARIANT, variant.getId() & 255); + } + + @Override + public boolean canMate(Animal other) { + if (!super.canMate(other)) return false; + if (!(other instanceof LudodactylusEntity that)) return false; + return this.getVariant() != that.getVariant(); + } + + /*? if <=1.20.1 {*/ + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor pLevel, DifficultyInstance pDifficulty, MobSpawnType pReason, @Nullable SpawnGroupData pSpawnData, @Nullable CompoundTag pDataTag) { + LudodactylusVariant variant = Util.getRandom(LudodactylusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(pLevel, pDifficulty, pReason, pSpawnData, pDataTag); + } + /*?} else {*/ + /*@Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, MobSpawnType spawnType, @Nullable SpawnGroupData spawnGroupData) { + LudodactylusVariant variant = Util.getRandom(LudodactylusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(level, difficulty, spawnType, spawnGroupData); + } + *//*?}*/ + + @Override + public void readAdditionalSaveData(CompoundTag pCompound) { + super.readAdditionalSaveData(pCompound); + this.entityData.set(VARIANT, pCompound.getInt("Variant")); + } + + @Override + public void addAdditionalSaveData(CompoundTag pCompound) { + super.addAdditionalSaveData(pCompound); + pCompound.putInt("Variant", this.getTypeVariant()); + } + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } + + @Override + protected @Nullable SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.LUDODACTYLUS_HURT.get(); + } + + @Override + protected @Nullable SoundEvent getDeathSound() { + return ModSounds.LUDODACTYLUS_DEATH.get(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/custom/MajungasaurusEntity.java b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/MajungasaurusEntity.java new file mode 100644 index 0000000..3796183 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/MajungasaurusEntity.java @@ -0,0 +1,318 @@ +package net.cmr.jurassicrevived.entity.custom; + +import net.cmr.jurassicrevived.entity.ModEntities; +import net.cmr.jurassicrevived.entity.ai.SprintingMeleeAttackGoal; +import net.cmr.jurassicrevived.entity.ai.SprintingPanicGoal; +import net.cmr.jurassicrevived.entity.client.MajungasaurusVariant; +import net.cmr.jurassicrevived.entity.client.MajungasaurusVariant; +import net.cmr.jurassicrevived.sound.ModSounds; +import net.minecraft.Util; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.util.Mth; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.*; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.attributes.DefaultAttributes; +import net.minecraft.world.entity.ai.goal.*; +import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal; +import net.minecraft.world.entity.ai.goal.target.NearestAttackableTargetGoal; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.monster.Monster; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerLevelAccessor; +import org.jetbrains.annotations.Nullable; +import software.bernie.geckolib.animatable.GeoEntity; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.core.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.core.animation.AnimatableManager; +import software.bernie.geckolib.core.animation.Animation; +import software.bernie.geckolib.core.animation.AnimationController; +import software.bernie.geckolib.core.animation.RawAnimation; +import software.bernie.geckolib.core.object.PlayState; +/*?} else {*/ +/*import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.animation.*; +*//*?}*/ + +public class MajungasaurusEntity extends Animal implements GeoEntity { + private AnimatableInstanceCache cache = new SingletonAnimatableInstanceCache(this); + + private static final EntityDataAccessor VARIANT = + SynchedEntityData.defineId(MajungasaurusEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SYNCED_AGE = + SynchedEntityData.defineId(MajungasaurusEntity.class, EntityDataSerializers.INT); + + // Procedural tail sway state (client-side use for rendering) + private float tailSwayOffset; // Smoothed offset in range roughly [-1, 1] + private float tailSwayVelocity; // Internal velocity for spring-damper + private float tailSwayPrev; // Previous frame value for interpolation + + public MajungasaurusEntity(EntityType pEntityType, Level pLevel) { + super(pEntityType, pLevel); + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new SprintingPanicGoal(this, 1.15) { + @Override + public boolean canUse() { + return MajungasaurusEntity.this.isBaby() && super.canUse(); + } + }); + this.targetSelector.addGoal(1, new HurtByTargetGoal(this).setAlertOthers()); + this.goalSelector.addGoal(2, new FloatGoal(this)); + this.goalSelector.addGoal(3, new AvoidEntityGoal<>(this, BrachiosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(4, new SprintingMeleeAttackGoal(this, 1.1, false)); + this.goalSelector.addGoal(5, new BreedGoal(this, 1.0)); + this.goalSelector.addGoal(6, new FollowParentGoal(this, 1.25)); + this.goalSelector.addGoal(7, new WaterAvoidingRandomStrollGoal(this, 1.0)); + this.goalSelector.addGoal(8, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(9, new FollowMobGoal(this, 1.2, (float) 20, (float) 10)); + this.targetSelector.addGoal(10, new NearestAttackableTargetGoal<>(this, Monster.class, true)); + this.targetSelector.addGoal(11, new NearestAttackableTargetGoal<>(this, Animal.class, 10, false, false, + target -> target.getType() != this.getType())); + this.targetSelector.addGoal(12, new NearestAttackableTargetGoal(this, TriceratopsEntity.class, false, false)); + this.targetSelector.addGoal(13, new NearestAttackableTargetGoal(this, GallimimusEntity.class, false, false)); + this.targetSelector.addGoal(14, new NearestAttackableTargetGoal(this, DilophosaurusEntity.class, false, false)); + this.targetSelector.addGoal(15, new NearestAttackableTargetGoal(this, CeratosaurusEntity.class, false, false)); + this.targetSelector.addGoal(16, new NearestAttackableTargetGoal(this, ParasaurolophusEntity.class, false, false)); + this.targetSelector.addGoal(17, new NearestAttackableTargetGoal(this, VelociraptorEntity.class, false, false)); + this.targetSelector.addGoal(18, new NearestAttackableTargetGoal(this, SpinosaurusEntity.class, false, false)); + this.targetSelector.addGoal(19, new NearestAttackableTargetGoal(this, IndominusRexEntity.class, false, false)); + this.targetSelector.addGoal(20, new NearestAttackableTargetGoal(this, Player.class, false, false)); + this.goalSelector.addGoal(21, new RandomLookAroundGoal(this)); + + + } + + public static AttributeSupplier.Builder createAttributes() { + return Animal.createLivingAttributes() + .add(Attributes.MAX_HEALTH, 55D) + .add(Attributes.MOVEMENT_SPEED, 0.3D) + .add(Attributes.ARMOR, 0D) + .add(Attributes.FOLLOW_RANGE, 32D) + .add(Attributes.KNOCKBACK_RESISTANCE, 0D) + .add(Attributes.ATTACK_KNOCKBACK, 0D) + .add(Attributes.ATTACK_DAMAGE, 16D); + } + + @Override + public boolean isFood(ItemStack pStack) { + return pStack.is(Items.BEEF); + } + + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel pLevel, AgeableMob pOtherParent) { + AgeableMob child = ModEntities.MAJUNGASAURUS.get().create(pLevel); + if (child instanceof MajungasaurusEntity baby) { + MajungasaurusVariant randomVariant = Util.getRandom(MajungasaurusVariant.values(), this.random); + baby.setVariant(randomVariant); + } + return child; + } + + @Override + public boolean doHurtTarget(Entity target) { + boolean hit = super.doHurtTarget(target); + if (!level().isClientSide && hit && target instanceof LivingEntity) { + if (this.level() instanceof ServerLevel serverLevel) { + this.triggerAnim("attackController", "attack"); + this.playSound(ModSounds.MEDIUM_THEROPOD_ATTACK.get(), 1.0F, 1.0F); + } + } + return hit; + } + + @Override + public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { + controllers.add(new AnimationController<>(this, "Walk/Run/Idle", state -> { + if (state.isMoving()) + return state.setAndContinue(MajungasaurusEntity.this.isSprinting() ? RawAnimation.begin().then("anim.majungasaurus.run", Animation.LoopType.LOOP) : RawAnimation.begin().then("anim.majungasaurus.walk", Animation.LoopType.LOOP)); + + return state.setAndContinue(RawAnimation.begin().then("anim.majungasaurus.idle", Animation.LoopType.LOOP)); + })); + + controllers.add(new AnimationController<>(this, "attackController", state -> PlayState.STOP) + .triggerableAnim("attack", RawAnimation.begin().then("anim.majungasaurus.attack", Animation.LoopType.PLAY_ONCE))); + + controllers.add(new AnimationController<>(this, "mouthController", state -> PlayState.STOP) + .triggerableAnim("mouth", RawAnimation.begin().then("anim.majungasaurus.mouth", Animation.LoopType.PLAY_ONCE))); + } + + private float getSignedTurnDelta() { + // Only consider the body (torso) rotation so head look does not affect tail sway + return Mth.wrapDegrees(this.yBodyRot - this.yBodyRotO); + } + + private int mouthAnimCooldown = 0; + + @Override + public void tick() { + super.tick(); + if (!level().isClientSide) { + this.entityData.set(DATA_SYNCED_AGE, this.getAge()); + var maxHealthAttr = getAttribute(Attributes.MAX_HEALTH); + if (maxHealthAttr != null) { + double baseAdult = DefaultAttributes.getSupplier((EntityType) this.getType()).getValue(Attributes.MAX_HEALTH); + double desired = this.isBaby() ? baseAdult * 0.10D : baseAdult; + if (maxHealthAttr.getBaseValue() != desired) { + double oldMax = maxHealthAttr.getBaseValue(); + double healthRatio = this.getHealth() / (float) oldMax; + maxHealthAttr.setBaseValue(desired); + this.setHealth((float) (desired * Mth.clamp(healthRatio, 0.0F, 1.0F))); + } + } + } + + if (!level().isClientSide) { + if (mouthAnimCooldown > 0) { + mouthAnimCooldown--; + } else { + this.triggerAnim("mouthController", "mouth"); + this.playSound(ModSounds.MAJUNGASAURUS_CALL.get(), 1.0F, 1.0F); + // 30s–60s in ticks + mouthAnimCooldown = this.random.nextInt(1200 - 600 + 1) + 600; + } + } + + if (level().isClientSide) { + // Capture previous for smooth interpolation between ticks + this.tailSwayPrev = this.tailSwayOffset; + updateProceduralTailSway(); + } + } + + private void updateProceduralTailSway() { + // Turn input derived from rotation deltas; works even when standing still and turning + float turnDegrees = getSignedTurnDelta(); + + // Deadzone to ignore tiny jitter so the tail can return to center cleanly + float deadzoneDeg = 0.6f; // smaller deadzone for more responsiveness + float turnInput = 0.0f; + if (Math.abs(turnDegrees) >= deadzoneDeg) { + // Higher sensitivity so small in-place turns still affect the model + turnInput = Mth.clamp(turnDegrees / 15.0f, -1.0f, 1.0f); + } + + // Target offset: keep intuitive sign (positive input -> positive sway) + float target = turnInput; + + // One-pole low-pass (no bounce). Larger alpha => snappier and less "stiff". + float alpha = 0.24f; // try 0.20–0.30 to taste + + tailSwayOffset += (target - tailSwayOffset) * alpha; + + // Snap tiny residuals to zero so it visibly settles + if (Math.abs(tailSwayOffset) < 0.003f) { + tailSwayOffset = 0.0f; + } + + // No oscillation velocity retained + tailSwayVelocity = 0.0f; + + tailSwayOffset = Mth.clamp(tailSwayOffset, -1.5f, 1.5f); + } + + // Expose to the model for bone rotation + public float getTailSwayOffset() { + return tailSwayOffset; + } + + // Interpolated sway for smooth rendering between ticks + public float getTailSwayOffset(float partialTick) { + return Mth.lerp(Mth.clamp(partialTick, 0.0f, 1.0f), tailSwayPrev, tailSwayOffset); + } + + /*? if <=1.20.1 {*/ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); + this.entityData.define(VARIANT, 0); + this.entityData.define(DATA_SYNCED_AGE, 0); + } + /*?} else {*/ + /*@Override + protected void defineSynchedData(SynchedEntityData.Builder pBuilder) { + super.defineSynchedData(pBuilder); + pBuilder.define(VARIANT, 0); + pBuilder.define(DATA_SYNCED_AGE, 0); + } + *//*?}*/ + + public int getSyncedAge() { + return this.entityData.get(DATA_SYNCED_AGE); + } + public int getTypeVariant() { + return this.entityData.get(VARIANT); + } + + public MajungasaurusVariant getVariant() { + return MajungasaurusVariant.byId(this.getTypeVariant() & 255); + } + + private void setVariant(MajungasaurusVariant variant) { + this.entityData.set(VARIANT, variant.getId() & 255); + } + + @Override + public boolean canMate(Animal other) { + if (!super.canMate(other)) return false; + if (!(other instanceof MajungasaurusEntity that)) return false; + return this.getVariant() != that.getVariant(); + } + @Override + public void addAdditionalSaveData(CompoundTag pCompound) { + super.addAdditionalSaveData(pCompound); + pCompound.putInt("Variant", this.getTypeVariant()); + } + + @Override + public void readAdditionalSaveData(CompoundTag pCompound) { + super.readAdditionalSaveData(pCompound); + this.entityData.set(VARIANT, pCompound.getInt("Variant")); + } + + /*? if <=1.20.1 {*/ + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor pLevel, DifficultyInstance pDifficulty, MobSpawnType pReason, @Nullable SpawnGroupData pSpawnData, @Nullable CompoundTag pDataTag) { + MajungasaurusVariant variant = Util.getRandom(MajungasaurusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(pLevel, pDifficulty, pReason, pSpawnData, pDataTag); + } + /*?} else {*/ + /*@Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, MobSpawnType spawnType, @Nullable SpawnGroupData spawnGroupData) { + MajungasaurusVariant variant = Util.getRandom(MajungasaurusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(level, difficulty, spawnType, spawnGroupData); + } + *//*?}*/ + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } + + @Override + protected @Nullable SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.MAJUNGASAURUS_HURT.get(); + } + + @Override + protected @Nullable SoundEvent getDeathSound() { + return ModSounds.MAJUNGASAURUS_DEATH.get(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/custom/MamenchisaurusEntity.java b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/MamenchisaurusEntity.java new file mode 100644 index 0000000..b96d85c --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/MamenchisaurusEntity.java @@ -0,0 +1,315 @@ +package net.cmr.jurassicrevived.entity.custom; + +import net.cmr.jurassicrevived.entity.ModEntities; +import net.cmr.jurassicrevived.entity.ai.SprintingMeleeAttackGoal; +import net.cmr.jurassicrevived.entity.ai.SprintingPanicGoal; +import net.cmr.jurassicrevived.entity.client.MamenchisaurusVariant; +import net.cmr.jurassicrevived.entity.client.MamenchisaurusVariant; +import net.cmr.jurassicrevived.sound.ModSounds; +import net.minecraft.Util; +import net.minecraft.core.BlockPos; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.util.Mth; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.*; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.attributes.DefaultAttributes; +import net.minecraft.world.entity.ai.goal.*; +import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerLevelAccessor; +import net.minecraft.world.level.block.state.BlockState; +import org.jetbrains.annotations.Nullable; +import software.bernie.geckolib.animatable.GeoEntity; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.core.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.core.animation.AnimatableManager; +import software.bernie.geckolib.core.animation.Animation; +import software.bernie.geckolib.core.animation.AnimationController; +import software.bernie.geckolib.core.animation.RawAnimation; +import software.bernie.geckolib.core.object.PlayState; +/*?} else {*/ +/*import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.animation.*; +*//*?}*/ + +public class MamenchisaurusEntity extends Animal implements GeoEntity { + private AnimatableInstanceCache cache = new SingletonAnimatableInstanceCache(this); + + private static final EntityDataAccessor VARIANT = + SynchedEntityData.defineId(MamenchisaurusEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SYNCED_AGE = + SynchedEntityData.defineId(MamenchisaurusEntity.class, EntityDataSerializers.INT); + + // Procedural tail sway state (client-side use for rendering) + private float tailSwayOffset; // Smoothed offset in range roughly [-1, 1] + private float tailSwayVelocity; // Internal velocity for spring-damper + private float tailSwayPrev; // Previous frame value for interpolation + + public MamenchisaurusEntity(EntityType pEntityType, Level pLevel) { + super(pEntityType, pLevel); + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new SprintingPanicGoal(this, 1.15) { + @Override + public boolean canUse() { + return MamenchisaurusEntity.this.isBaby() && super.canUse(); + } + }); + this.targetSelector.addGoal(1, new HurtByTargetGoal(this).setAlertOthers()); + this.goalSelector.addGoal(2, new FloatGoal(this)); + this.goalSelector.addGoal(3, new AvoidEntityGoal<>(this, DilophosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(4, new AvoidEntityGoal<>(this, VelociraptorEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(5, new AvoidEntityGoal<>(this, CeratosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(6, new AvoidEntityGoal<>(this, SpinosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(7, new AvoidEntityGoal<>(this, TyrannosaurusRexEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(8, new SprintingMeleeAttackGoal(this, 1.1, false)); + this.goalSelector.addGoal(9, new BreedGoal(this, 1.0)); + this.goalSelector.addGoal(9, new FollowParentGoal(this, 1.25)); + this.goalSelector.addGoal(10, new WaterAvoidingRandomStrollGoal(this, 1.0)); + this.goalSelector.addGoal(11, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(12, new FollowMobGoal(this, 1, (float) 20, (float) 10)); + this.goalSelector.addGoal(13, new RandomLookAroundGoal(this)); + + } + + public static AttributeSupplier.Builder createAttributes() { + return Animal.createLivingAttributes() + .add(Attributes.MAX_HEALTH, 200D) + .add(Attributes.MOVEMENT_SPEED, 0.3D) + .add(Attributes.ARMOR, 0D) + .add(Attributes.FOLLOW_RANGE, 32D) + .add(Attributes.KNOCKBACK_RESISTANCE, 1D) + .add(Attributes.ATTACK_KNOCKBACK, 0D) + .add(Attributes.ATTACK_DAMAGE, 20D); + } + + @Override + public boolean isFood(ItemStack pStack) { + return pStack.is(Items.KELP); + } + + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel pLevel, AgeableMob pOtherParent) { + AgeableMob child = ModEntities.MAMENCHISAURUS.get().create(pLevel); + if (child instanceof MamenchisaurusEntity baby) { + MamenchisaurusVariant randomVariant = Util.getRandom(MamenchisaurusVariant.values(), this.random); + baby.setVariant(randomVariant); + } + return child; + } + + @Override + public boolean doHurtTarget(Entity target) { + boolean hit = super.doHurtTarget(target); + if (!level().isClientSide && hit && target instanceof LivingEntity) { + if (this.level() instanceof ServerLevel serverLevel) { + this.triggerAnim("attackController", "attack"); + this.playSound(ModSounds.STOMP_ATTACK.get(), 1.0F, 1.0F); + } + } + return hit; + } + + @Override + public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { + controllers.add(new AnimationController<>(this, "Walk/Run/Idle", state -> { + if (state.isMoving()) + return state.setAndContinue(MamenchisaurusEntity.this.isSprinting() ? RawAnimation.begin().then("anim.mamenchisaurus.run", Animation.LoopType.LOOP) : RawAnimation.begin().then("anim.mamenchisaurus.walk", Animation.LoopType.LOOP)); + + return state.setAndContinue(RawAnimation.begin().then("anim.mamenchisaurus.idle", Animation.LoopType.LOOP)); + })); + + controllers.add(new AnimationController<>(this, "attackController", state -> PlayState.STOP) + .triggerableAnim("attack", RawAnimation.begin().then("anim.mamenchisaurus.attack", Animation.LoopType.PLAY_ONCE))); + + controllers.add(new AnimationController<>(this, "mouthController", state -> PlayState.STOP) + .triggerableAnim("mouth", RawAnimation.begin().then("anim.mamenchisaurus.mouth", Animation.LoopType.PLAY_ONCE))); + } + + private float getSignedTurnDelta() { + // Only consider the body (torso) rotation so head look does not affect tail sway + return Mth.wrapDegrees(this.yBodyRot - this.yBodyRotO); + } + + private int mouthAnimCooldown = 0; + + @Override + public void tick() { + super.tick(); + + if (!level().isClientSide) { + this.entityData.set(DATA_SYNCED_AGE, this.getAge()); + var maxHealthAttr = getAttribute(Attributes.MAX_HEALTH); + if (maxHealthAttr != null) { + double baseAdult = DefaultAttributes.getSupplier((EntityType) this.getType()).getValue(Attributes.MAX_HEALTH); + double desired = this.isBaby() ? baseAdult * 0.10D : baseAdult; + if (maxHealthAttr.getBaseValue() != desired) { + double oldMax = maxHealthAttr.getBaseValue(); + double healthRatio = this.getHealth() / (float) oldMax; + maxHealthAttr.setBaseValue(desired); + this.setHealth((float) (desired * Mth.clamp(healthRatio, 0.0F, 1.0F))); + } + } + } + + if (!level().isClientSide) { + if (mouthAnimCooldown > 0) { + mouthAnimCooldown--; + } else { + this.triggerAnim("mouthController", "mouth"); + this.playSound(ModSounds.MAMENCHISAURUS_CALL.get(), 1.0F, 1.0F); + // 30s–60s in ticks + mouthAnimCooldown = this.random.nextInt(1200 - 600 + 1) + 600; + } + } + + if (level().isClientSide) { + // Capture previous for smooth interpolation between ticks + this.tailSwayPrev = this.tailSwayOffset; + updateProceduralTailSway(); + } + } + + private void updateProceduralTailSway() { + // Turn input derived from rotation deltas; works even when standing still and turning + float turnDegrees = getSignedTurnDelta(); + + // Deadzone to ignore tiny jitter so the tail can return to center cleanly + float deadzoneDeg = 0.6f; // smaller deadzone for more responsiveness + float turnInput = 0.0f; + if (Math.abs(turnDegrees) >= deadzoneDeg) { + // Higher sensitivity so small in-place turns still affect the model + turnInput = Mth.clamp(turnDegrees / 15.0f, -1.0f, 1.0f); + } + + // Target offset: keep intuitive sign (positive input -> positive sway) + float target = turnInput; + + // One-pole low-pass (no bounce). Larger alpha => snappier and less "stiff". + float alpha = 0.24f; // try 0.20–0.30 to taste + + tailSwayOffset += (target - tailSwayOffset) * alpha; + + // Snap tiny residuals to zero so it visibly settles + if (Math.abs(tailSwayOffset) < 0.003f) { + tailSwayOffset = 0.0f; + } + + // No oscillation velocity retained + tailSwayVelocity = 0.0f; + + tailSwayOffset = Mth.clamp(tailSwayOffset, -1.5f, 1.5f); + } + + // Expose to the model for bone rotation + public float getTailSwayOffset() { + return tailSwayOffset; + } + + // Interpolated sway for smooth rendering between ticks + public float getTailSwayOffset(float partialTick) { + return Mth.lerp(Mth.clamp(partialTick, 0.0f, 1.0f), tailSwayPrev, tailSwayOffset); + } + + /*? if <=1.20.1 {*/ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); + this.entityData.define(VARIANT, 0); + this.entityData.define(DATA_SYNCED_AGE, 0); + } + /*?} else {*/ + /*@Override + protected void defineSynchedData(SynchedEntityData.Builder pBuilder) { + super.defineSynchedData(pBuilder); + pBuilder.define(VARIANT, 0); + pBuilder.define(DATA_SYNCED_AGE, 0); + } + *//*?}*/ + + public int getSyncedAge() { + return this.entityData.get(DATA_SYNCED_AGE); + } + public int getTypeVariant() { + return this.entityData.get(VARIANT); + } + + public MamenchisaurusVariant getVariant() { + return MamenchisaurusVariant.byId(this.getTypeVariant() & 255); + } + + private void setVariant(MamenchisaurusVariant variant) { + this.entityData.set(VARIANT, variant.getId() & 255); + } + + @Override + public boolean canMate(Animal other) { + if (!super.canMate(other)) return false; + if (!(other instanceof MamenchisaurusEntity that)) return false; + return this.getVariant() != that.getVariant(); + } + @Override + public void addAdditionalSaveData(CompoundTag pCompound) { + super.addAdditionalSaveData(pCompound); + pCompound.putInt("Variant", this.getTypeVariant()); + } + + @Override + public void readAdditionalSaveData(CompoundTag pCompound) { + super.readAdditionalSaveData(pCompound); + this.entityData.set(VARIANT, pCompound.getInt("Variant")); + } + + /*? if <=1.20.1 {*/ + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor pLevel, DifficultyInstance pDifficulty, MobSpawnType pReason, @Nullable SpawnGroupData pSpawnData, @Nullable CompoundTag pDataTag) { + MamenchisaurusVariant variant = Util.getRandom(MamenchisaurusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(pLevel, pDifficulty, pReason, pSpawnData, pDataTag); + } + /*?} else {*/ + /*@Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, MobSpawnType spawnType, @Nullable SpawnGroupData spawnGroupData) { + MamenchisaurusVariant variant = Util.getRandom(MamenchisaurusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(level, difficulty, spawnType, spawnGroupData); + } + *//*?}*/ + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } + + @Override + protected @Nullable SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.MAMENCHISAURUS_HURT.get(); + } + + @Override + protected void playStepSound(BlockPos pos, BlockState blockIn) { + this.playSound(ModSounds.STOMP.get(), 0.5F, 1.0F); + } + + @Override + protected @Nullable SoundEvent getDeathSound() { + return ModSounds.MAMENCHISAURUS_DEATH.get(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/custom/MetriacanthosaurusEntity.java b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/MetriacanthosaurusEntity.java new file mode 100644 index 0000000..2e3c572 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/MetriacanthosaurusEntity.java @@ -0,0 +1,315 @@ +package net.cmr.jurassicrevived.entity.custom; + +import net.cmr.jurassicrevived.entity.ModEntities; +import net.cmr.jurassicrevived.entity.ai.SprintingMeleeAttackGoal; +import net.cmr.jurassicrevived.entity.ai.SprintingPanicGoal; +import net.cmr.jurassicrevived.entity.client.MetriacanthosaurusVariant; +import net.cmr.jurassicrevived.entity.client.MetriacanthosaurusVariant; +import net.cmr.jurassicrevived.sound.ModSounds; +import net.minecraft.Util; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.util.Mth; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.*; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.attributes.DefaultAttributes; +import net.minecraft.world.entity.ai.goal.*; +import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal; +import net.minecraft.world.entity.ai.goal.target.NearestAttackableTargetGoal; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerLevelAccessor; +import org.jetbrains.annotations.Nullable; +import software.bernie.geckolib.animatable.GeoEntity; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.core.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.core.animation.AnimatableManager; +import software.bernie.geckolib.core.animation.Animation; +import software.bernie.geckolib.core.animation.AnimationController; +import software.bernie.geckolib.core.animation.RawAnimation; +import software.bernie.geckolib.core.object.PlayState; +/*?} else {*/ +/*import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.animation.*; +*//*?}*/ + +public class MetriacanthosaurusEntity extends Animal implements GeoEntity { + private AnimatableInstanceCache cache = new SingletonAnimatableInstanceCache(this); + + private static final EntityDataAccessor VARIANT = + SynchedEntityData.defineId(MetriacanthosaurusEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SYNCED_AGE = + SynchedEntityData.defineId(MetriacanthosaurusEntity.class, EntityDataSerializers.INT); + + // Procedural tail sway state (client-side use for rendering) + private float tailSwayOffset; // Smoothed offset in range roughly [-1, 1] + private float tailSwayVelocity; // Internal velocity for spring-damper + private float tailSwayPrev; // Previous frame value for interpolation + + public MetriacanthosaurusEntity(EntityType pEntityType, Level pLevel) { + super(pEntityType, pLevel); + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new SprintingPanicGoal(this, 1.15) { + @Override + public boolean canUse() { + return MetriacanthosaurusEntity.this.isBaby() && super.canUse(); + } + }); + this.targetSelector.addGoal(1, new HurtByTargetGoal(this).setAlertOthers()); + this.goalSelector.addGoal(2, new FloatGoal(this)); + this.goalSelector.addGoal(3, new AvoidEntityGoal<>(this, TriceratopsEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(4, new AvoidEntityGoal<>(this, SpinosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(5, new AvoidEntityGoal<>(this, TyrannosaurusRexEntity.class, (float) 20, 1.2, 1.2)); + this.goalSelector.addGoal(6, new AvoidEntityGoal<>(this, BrachiosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(7, new SprintingMeleeAttackGoal(this, 1.1, false)); + this.goalSelector.addGoal(8, new BreedGoal(this, 1.0)); + this.goalSelector.addGoal(9, new FollowParentGoal(this, 1.25)); + this.goalSelector.addGoal(10, new WaterAvoidingRandomStrollGoal(this, 1.0)); + this.goalSelector.addGoal(11, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(12, new FollowMobGoal(this, 1, (float) 20, (float) 10)); + this.targetSelector.addGoal(13, new NearestAttackableTargetGoal<>(this, Animal.class, 10, false, false, + target -> target.getType() != this.getType())); + this.targetSelector.addGoal(14, new NearestAttackableTargetGoal(this, GallimimusEntity.class, false, false)); + this.targetSelector.addGoal(15, new NearestAttackableTargetGoal(this, DilophosaurusEntity.class, false, false)); + this.targetSelector.addGoal(16, new NearestAttackableTargetGoal(this, VelociraptorEntity.class, false, false)); + this.targetSelector.addGoal(17, new NearestAttackableTargetGoal(this, ParasaurolophusEntity.class, false, false)); + this.targetSelector.addGoal(18, new NearestAttackableTargetGoal(this, Player.class, false, false)); + this.targetSelector.addGoal(19, new NearestAttackableTargetGoal(this, CompsognathusEntity.class, false, false)); + this.goalSelector.addGoal(20, new RandomLookAroundGoal(this)); + } + + public static AttributeSupplier.Builder createAttributes() { + return Animal.createLivingAttributes() + .add(Attributes.MAX_HEALTH, 45D) + .add(Attributes.MOVEMENT_SPEED, 0.3D) + .add(Attributes.ARMOR, 0D) + .add(Attributes.FOLLOW_RANGE, 32D) + .add(Attributes.KNOCKBACK_RESISTANCE, 0.3D) + .add(Attributes.ATTACK_KNOCKBACK, 0D) + .add(Attributes.ATTACK_DAMAGE, 14D ); + } + + @Override + public boolean isFood(ItemStack pStack) { + return pStack.is(Items.BEEF); + } + + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel pLevel, AgeableMob pOtherParent) { + AgeableMob child = ModEntities.METRIACANTHOSAURUS.get().create(pLevel); + if (child instanceof MetriacanthosaurusEntity baby) { + MetriacanthosaurusVariant randomVariant = Util.getRandom(MetriacanthosaurusVariant.values(), this.random); + baby.setVariant(randomVariant); + } + return child; + } + + @Override + public boolean doHurtTarget(Entity target) { + boolean hit = super.doHurtTarget(target); + if (!level().isClientSide && hit && target instanceof LivingEntity) { + if (this.level() instanceof ServerLevel serverLevel) { + this.triggerAnim("attackController", "attack"); + this.playSound(ModSounds.MEDIUM_THEROPOD_ATTACK.get(), 1.0F, 1.0F); + } + } + return hit; + } + + @Override + public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { + controllers.add(new AnimationController<>(this, "Walk/Run/Idle", state -> { + if (state.isMoving()) + return state.setAndContinue(MetriacanthosaurusEntity.this.isSprinting() ? RawAnimation.begin().then("anim.metriacanthosaurus.run", Animation.LoopType.LOOP) : RawAnimation.begin().then("anim.metriacanthosaurus.walk", Animation.LoopType.LOOP)); + + return state.setAndContinue(RawAnimation.begin().then("anim.metriacanthosaurus.idle", Animation.LoopType.LOOP)); + })); + + controllers.add(new AnimationController<>(this, "attackController", state -> PlayState.STOP) + .triggerableAnim("attack", RawAnimation.begin().then("anim.metriacanthosaurus.attack", Animation.LoopType.PLAY_ONCE))); + + controllers.add(new AnimationController<>(this, "mouthController", state -> PlayState.STOP) + .triggerableAnim("mouth", RawAnimation.begin().then("anim.metriacanthosaurus.mouth", Animation.LoopType.PLAY_ONCE))); + } + + private float getSignedTurnDelta() { + // Only consider the body (torso) rotation so head look does not affect tail sway + return Mth.wrapDegrees(this.yBodyRot - this.yBodyRotO); + } + + private int mouthAnimCooldown = 0; + + @Override + public void tick() { + super.tick(); + if (!level().isClientSide) { + this.entityData.set(DATA_SYNCED_AGE, this.getAge()); + var maxHealthAttr = getAttribute(Attributes.MAX_HEALTH); + if (maxHealthAttr != null) { + double baseAdult = DefaultAttributes.getSupplier((EntityType) this.getType()).getValue(Attributes.MAX_HEALTH); + double desired = this.isBaby() ? baseAdult * 0.10D : baseAdult; + if (maxHealthAttr.getBaseValue() != desired) { + double oldMax = maxHealthAttr.getBaseValue(); + double healthRatio = this.getHealth() / (float) oldMax; + maxHealthAttr.setBaseValue(desired); + this.setHealth((float) (desired * Mth.clamp(healthRatio, 0.0F, 1.0F))); + } + } + } + + if (!level().isClientSide) { + if (mouthAnimCooldown > 0) { + mouthAnimCooldown--; + } else { + this.triggerAnim("mouthController", "mouth"); + this.playSound(ModSounds.METRIACANTHOSAURUS_CALL.get(), 1.0F, 1.0F); + // 30s–60s in ticks + mouthAnimCooldown = this.random.nextInt(1200 - 600 + 1) + 600; + } + } + + if (level().isClientSide) { + // Capture previous for smooth interpolation between ticks + this.tailSwayPrev = this.tailSwayOffset; + updateProceduralTailSway(); + } + } + + private void updateProceduralTailSway() { + // Turn input derived from rotation deltas; works even when standing still and turning + float turnDegrees = getSignedTurnDelta(); + + // Deadzone to ignore tiny jitter so the tail can return to center cleanly + float deadzoneDeg = 0.6f; // smaller deadzone for more responsiveness + float turnInput = 0.0f; + if (Math.abs(turnDegrees) >= deadzoneDeg) { + // Higher sensitivity so small in-place turns still affect the model + turnInput = Mth.clamp(turnDegrees / 15.0f, -1.0f, 1.0f); + } + + // Target offset: keep intuitive sign (positive input -> positive sway) + float target = turnInput; + + // One-pole low-pass (no bounce). Larger alpha => snappier and less "stiff". + float alpha = 0.24f; // try 0.20–0.30 to taste + + tailSwayOffset += (target - tailSwayOffset) * alpha; + + // Snap tiny residuals to zero so it visibly settles + if (Math.abs(tailSwayOffset) < 0.003f) { + tailSwayOffset = 0.0f; + } + + // No oscillation velocity retained + tailSwayVelocity = 0.0f; + + tailSwayOffset = Mth.clamp(tailSwayOffset, -1.5f, 1.5f); + } + + // Expose to the model for bone rotation + public float getTailSwayOffset() { + return tailSwayOffset; + } + + // Interpolated sway for smooth rendering between ticks + public float getTailSwayOffset(float partialTick) { + return Mth.lerp(Mth.clamp(partialTick, 0.0f, 1.0f), tailSwayPrev, tailSwayOffset); + } + + /*? if <=1.20.1 {*/ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); + this.entityData.define(VARIANT, 0); + this.entityData.define(DATA_SYNCED_AGE, 0); + } + /*?} else {*/ + /*@Override + protected void defineSynchedData(SynchedEntityData.Builder pBuilder) { + super.defineSynchedData(pBuilder); + pBuilder.define(VARIANT, 0); + pBuilder.define(DATA_SYNCED_AGE, 0); + } + *//*?}*/ + + public int getSyncedAge() { + return this.entityData.get(DATA_SYNCED_AGE); + } + + public int getTypeVariant() { + return this.entityData.get(VARIANT); + } + + public MetriacanthosaurusVariant getVariant() { + return MetriacanthosaurusVariant.byId(this.getTypeVariant() & 255); + } + + private void setVariant(MetriacanthosaurusVariant variant) { + this.entityData.set(VARIANT, variant.getId() & 255); + } + + @Override + public boolean canMate(Animal other) { + if (!super.canMate(other)) return false; + if (!(other instanceof MetriacanthosaurusEntity that)) return false; + return this.getVariant() != that.getVariant(); + } + @Override + public void addAdditionalSaveData(CompoundTag pCompound) { + super.addAdditionalSaveData(pCompound); + pCompound.putInt("Variant", this.getTypeVariant()); + } + + @Override + public void readAdditionalSaveData(CompoundTag pCompound) { + super.readAdditionalSaveData(pCompound); + this.entityData.set(VARIANT, pCompound.getInt("Variant")); + } + + /*? if <=1.20.1 {*/ + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor pLevel, DifficultyInstance pDifficulty, MobSpawnType pReason, @Nullable SpawnGroupData pSpawnData, @Nullable CompoundTag pDataTag) { + MetriacanthosaurusVariant variant = Util.getRandom(MetriacanthosaurusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(pLevel, pDifficulty, pReason, pSpawnData, pDataTag); + } + /*?} else {*/ + /*@Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, MobSpawnType spawnType, @Nullable SpawnGroupData spawnGroupData) { + MetriacanthosaurusVariant variant = Util.getRandom(MetriacanthosaurusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(level, difficulty, spawnType, spawnGroupData); + } + *//*?}*/ + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } + + @Override + protected @Nullable SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.METRIACANTHOSAURUS_HURT.get(); + } + + @Override + protected @Nullable SoundEvent getDeathSound() { + return ModSounds.METRIACANTHOSAURUS_DEATH.get(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/custom/MoganopterusEntity.java b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/MoganopterusEntity.java new file mode 100644 index 0000000..f5c4c09 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/MoganopterusEntity.java @@ -0,0 +1,411 @@ +package net.cmr.jurassicrevived.entity.custom; + +import net.cmr.jurassicrevived.entity.ModEntities; +import net.cmr.jurassicrevived.entity.ai.SprintingMeleeAttackGoal; +import net.cmr.jurassicrevived.entity.ai.SprintingPanicGoal; +import net.cmr.jurassicrevived.entity.client.MoganopterusVariant; +import net.cmr.jurassicrevived.entity.client.MoganopterusVariant; +import net.cmr.jurassicrevived.sound.ModSounds; +import net.minecraft.Util; +import net.minecraft.core.BlockPos; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.*; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.attributes.DefaultAttributes; +import net.minecraft.world.entity.ai.control.FlyingMoveControl; +import net.minecraft.world.entity.ai.goal.*; +import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal; +import net.minecraft.world.entity.ai.goal.target.NearestAttackableTargetGoal; +import net.minecraft.world.entity.ai.navigation.FlyingPathNavigation; +import net.minecraft.world.entity.ai.navigation.PathNavigation; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.animal.FlyingAnimal; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerLevelAccessor; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.phys.Vec3; +import org.jetbrains.annotations.Nullable; +import software.bernie.geckolib.animatable.GeoEntity; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.core.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.core.animation.AnimatableManager; +import software.bernie.geckolib.core.animation.Animation; +import software.bernie.geckolib.core.animation.AnimationController; +import software.bernie.geckolib.core.animation.RawAnimation; +import software.bernie.geckolib.core.object.PlayState; +/*?} else {*/ +/*import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.animation.*; +*//*?}*/ + +public class MoganopterusEntity extends Animal implements GeoEntity, FlyingAnimal { + private final AnimatableInstanceCache cache = new SingletonAnimatableInstanceCache(this); + + private static final EntityDataAccessor VARIANT = + SynchedEntityData.defineId(MoganopterusEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SYNCED_AGE = + SynchedEntityData.defineId(MoganopterusEntity.class, EntityDataSerializers.INT); + + // Procedural tail sway state (client-side use for rendering) + private float tailSwayOffset; // Smoothed offset in range roughly [-1, 1] + private float tailSwayVelocity; // Internal velocity for spring-damper + private float tailSwayPrev; // Previous frame value for interpolation + + public MoganopterusEntity(EntityType pEntityType, Level pLevel) { + super(pEntityType, pLevel); + this.moveControl = new FlyingMoveControl(this, 20, true); + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new SprintingPanicGoal(this, 1.15) { + @Override + public boolean canUse() { + return MoganopterusEntity.this.isBaby() && super.canUse(); + } + }); + this.goalSelector.addGoal(2, new FloatGoal(this)); + this.goalSelector.addGoal(3, new AvoidEntityGoal<>(this, BrachiosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(4, new SprintingMeleeAttackGoal(this, 1.1, false)); + this.goalSelector.addGoal(5, new BreedGoal(this, 1.0)); + this.goalSelector.addGoal(6, new FollowParentGoal(this, 1.25)); + + // Goal 7: Wander on ground (Walk) - Only when on ground + this.goalSelector.addGoal(7, new WaterAvoidingRandomStrollGoal(this, 1.0) { + @Override + public boolean canUse() { + return MoganopterusEntity.this.onGround() && super.canUse(); + } + }); + + // Goal 8: Wander in air (Fly) - Handles takeoff, flying, and landing + this.goalSelector.addGoal(8, new WaterAvoidingRandomStrollGoal(this, 1.0, 20) { + @Override + public boolean canUse() { + boolean isFlying = !MoganopterusEntity.this.onGround(); + // If flying, keep flying. If on ground, small chance (1/400 ticks) to take off. + return (isFlying || MoganopterusEntity.this.getRandom().nextInt(400) == 0) && super.canUse(); + } + + @Override + protected Vec3 getPosition() { + Vec3 pos = MoganopterusEntity.this.position(); + RandomSource random = MoganopterusEntity.this.getRandom(); + + double x = pos.x + (random.nextFloat() * 2 - 1) * 32; + double z = pos.z + (random.nextFloat() * 2 - 1) * 32; + + // Get ground height at the random destination (returns Y of first air block) + int groundY = MoganopterusEntity.this.level().getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, (int)x, (int)z); + double y; + + if (MoganopterusEntity.this.onGround()) { + // Takeoff: Target well above ground to ensure liftoff + y = pos.y + 15 + random.nextInt(10); + } else { + // Flying: 5% chance to land, otherwise stay airborne but capped height + if (random.nextFloat() < 0.05f) { + y = groundY; // Land + } else if (pos.y > groundY + 20) { + // Too high: Force descent + y = pos.y - 5 - random.nextInt(10); + } else { + // Just wander up or down a bit + y = pos.y + (random.nextFloat() * 2 - 1) * 10; + } + } + + // Don't target below the ground (blocks) + if (y < groundY) y = groundY; + + return new Vec3(x, y, z); + } + }); + + this.goalSelector.addGoal(9, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(10, new FollowMobGoal(this, 0.8, (float) 20, (float) 10)); + this.targetSelector.addGoal(11, new NearestAttackableTargetGoal<>(this, Animal.class, 10, false, false, + target -> { + // 1. Don't eat your own species + if (target.getType() == this.getType()) return false; + + // 2. Don't eat Flying Animals + if (target instanceof FlyingAnimal) return false; + + // 3. SIZE CHECK: specific height and width limits + // Example: Height < 1.0 blocks AND Width < 1.0 blocks + boolean isSmallEnough = target.getBbHeight() <= 1.0F && target.getBbWidth() <= 1.0F; + + return isSmallEnough; + } + )); + this.targetSelector.addGoal(12, new NearestAttackableTargetGoal<>(this, Player.class, false, false)); + this.targetSelector.addGoal(13, new HurtByTargetGoal(this)); + this.goalSelector.addGoal(14, new RandomLookAroundGoal(this)); + this.goalSelector.addGoal(15, new FloatGoal(this)); + } + + public static AttributeSupplier.Builder createAttributes() { + return Animal.createLivingAttributes() + .add(Attributes.MAX_HEALTH, 50D) + .add(Attributes.MOVEMENT_SPEED, 0.3D) + .add(Attributes.FLYING_SPEED, 0.6D) + .add(Attributes.ARMOR, 0D) + .add(Attributes.FOLLOW_RANGE, 32D) + .add(Attributes.KNOCKBACK_RESISTANCE, 0D) + .add(Attributes.ATTACK_KNOCKBACK, 0D) + .add(Attributes.ATTACK_DAMAGE, 8D); + } + + @Override + protected PathNavigation createNavigation(Level pLevel) { + FlyingPathNavigation navigation = new FlyingPathNavigation(this, pLevel); + navigation.setCanOpenDoors(false); + navigation.setCanFloat(true); + navigation.setCanPassDoors(true); + return navigation; + } + + @Override + public boolean isFlying() { + return !this.onGround(); + } + + @Override + public boolean causeFallDamage(float pFallDistance, float pMultiplier, DamageSource pSource) { + return false; + } + + @Override + protected void checkFallDamage(double pY, boolean pOnGround, BlockState pState, BlockPos pPos) { + } + + @Override + public boolean isFood(ItemStack pStack) { + return pStack.is(Items.BEEF); + } + + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel pLevel, AgeableMob pOtherParent) { + AgeableMob child = ModEntities.MOGANOPTERUS.get().create(pLevel); + if (child instanceof MoganopterusEntity baby) { + MoganopterusVariant randomVariant = Util.getRandom(MoganopterusVariant.values(), this.random); + baby.setVariant(randomVariant); + } + return child; + } + + @Override + public boolean doHurtTarget(Entity target) { + boolean hit = super.doHurtTarget(target); + if (!level().isClientSide && hit && target instanceof LivingEntity) { + if (this.level() instanceof ServerLevel serverLevel) { + this.triggerAnim("attackController", "attack"); + this.playSound(ModSounds.BEAK_ATTACK.get(), 1.0F, 1.0F); + } + } + return hit; + } + + @Override + public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { + controllers.add(new AnimationController<>(this, "Walk/Run/Idle", state -> { + if (!MoganopterusEntity.this.onGround()) { + return state.setAndContinue(RawAnimation.begin().then("anim.moganopterus.fly", Animation.LoopType.LOOP)); + } + + if (state.isMoving()) + return state.setAndContinue(MoganopterusEntity.this.isSprinting() ? RawAnimation.begin().then("anim.moganopterus.walk", Animation.LoopType.LOOP) : RawAnimation.begin().then("anim.moganopterus.walk", Animation.LoopType.LOOP)); + + return state.setAndContinue(RawAnimation.begin().then("anim.moganopterus.idle", Animation.LoopType.LOOP)); + })); + + controllers.add(new AnimationController<>(this, "attackController", state -> PlayState.STOP) + .triggerableAnim("attack", RawAnimation.begin().then("anim.moganopterus.attack", Animation.LoopType.PLAY_ONCE))); + + controllers.add(new AnimationController<>(this, "mouthController", state -> PlayState.STOP) + .triggerableAnim("mouth", RawAnimation.begin().then("anim.moganopterus.mouth", Animation.LoopType.PLAY_ONCE))); + } + + private float getSignedTurnDelta() { + // Only consider the body (torso) rotation so head look does not affect tail sway + return Mth.wrapDegrees(this.yBodyRot - this.yBodyRotO); + } + + private int mouthAnimCooldown = 0; + + @Override + public void tick() { + super.tick(); + if (!level().isClientSide) { + this.entityData.set(DATA_SYNCED_AGE, this.getAge()); + var maxHealthAttr = getAttribute(Attributes.MAX_HEALTH); + if (maxHealthAttr != null) { + double baseAdult = DefaultAttributes.getSupplier((EntityType) this.getType()).getValue(Attributes.MAX_HEALTH); + double desired = this.isBaby() ? baseAdult * 0.10D : baseAdult; + if (maxHealthAttr.getBaseValue() != desired) { + double oldMax = maxHealthAttr.getBaseValue(); + double healthRatio = this.getHealth() / (float) oldMax; + maxHealthAttr.setBaseValue(desired); + this.setHealth((float) (desired * Mth.clamp(healthRatio, 0.0F, 1.0F))); + } + } + } + + if (!level().isClientSide) { + if (mouthAnimCooldown > 0) { + mouthAnimCooldown--; + } else { + this.triggerAnim("mouthController", "mouth"); + this.playSound(ModSounds.MOGANOPTERUS_CALL.get(), 1.0F, 1.0F); + // 30s–60s in ticks + mouthAnimCooldown = this.random.nextInt(1200 - 600 + 1) + 600; + } + } + + if (level().isClientSide) { + // Capture previous for smooth interpolation between ticks + this.tailSwayPrev = this.tailSwayOffset; + updateProceduralTailSway(); + } + } + + private void updateProceduralTailSway() { + // Turn input derived from rotation deltas; works even when standing still and turning + float turnDegrees = getSignedTurnDelta(); + + // Deadzone to ignore tiny jitter so the tail can return to center cleanly + float deadzoneDeg = 0.6f; // smaller deadzone for more responsiveness + float turnInput = 0.0f; + if (Math.abs(turnDegrees) >= deadzoneDeg) { + // Higher sensitivity so small in-place turns still affect the model + turnInput = Mth.clamp(turnDegrees / 15.0f, -1.0f, 1.0f); + } + + // Target offset: keep intuitive sign (positive input -> positive sway) + float target = turnInput; + + // One-pole low-pass (no bounce). Larger alpha => snappier and less "stiff". + float alpha = 0.24f; // try 0.20–0.30 to taste + + tailSwayOffset += (target - tailSwayOffset) * alpha; + + // Snap tiny residuals to zero so it visibly settles + if (Math.abs(tailSwayOffset) < 0.003f) { + tailSwayOffset = 0.0f; + } + + // No oscillation velocity retained + tailSwayVelocity = 0.0f; + + tailSwayOffset = Mth.clamp(tailSwayOffset, -1.5f, 1.5f); + } + + // Expose to the model for bone rotation + public float getTailSwayOffset() { + return tailSwayOffset; + } + + // Interpolated sway for smooth rendering between ticks + public float getTailSwayOffset(float partialTick) { + return Mth.lerp(Mth.clamp(partialTick, 0.0f, 1.0f), tailSwayPrev, tailSwayOffset); + } + + /*? if <=1.20.1 {*/ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); + this.entityData.define(VARIANT, 0); + this.entityData.define(DATA_SYNCED_AGE, 0); + } + /*?} else {*/ + /*@Override + protected void defineSynchedData(SynchedEntityData.Builder pBuilder) { + super.defineSynchedData(pBuilder); + pBuilder.define(VARIANT, 0); + pBuilder.define(DATA_SYNCED_AGE, 0); + } + *//*?}*/ + + public int getSyncedAge() { + return this.entityData.get(DATA_SYNCED_AGE); + } + + public MoganopterusVariant getVariant() { + return MoganopterusVariant.byId(this.getTypeVariant() & 255); + } + public int getTypeVariant() { + return this.entityData.get(VARIANT); + } + + private void setVariant(MoganopterusVariant variant) { + this.entityData.set(VARIANT, variant.getId() & 255); + } + + @Override + public boolean canMate(Animal other) { + if (!super.canMate(other)) return false; + if (!(other instanceof MoganopterusEntity that)) return false; + return this.getVariant() != that.getVariant(); + } + + /*? if <=1.20.1 {*/ + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor pLevel, DifficultyInstance pDifficulty, MobSpawnType pReason, @Nullable SpawnGroupData pSpawnData, @Nullable CompoundTag pDataTag) { + MoganopterusVariant variant = Util.getRandom(MoganopterusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(pLevel, pDifficulty, pReason, pSpawnData, pDataTag); + } + /*?} else {*/ + /*@Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, MobSpawnType spawnType, @Nullable SpawnGroupData spawnGroupData) { + MoganopterusVariant variant = Util.getRandom(MoganopterusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(level, difficulty, spawnType, spawnGroupData); + } + *//*?}*/ + + @Override + public void readAdditionalSaveData(CompoundTag pCompound) { + super.readAdditionalSaveData(pCompound); + this.entityData.set(VARIANT, pCompound.getInt("Variant")); + } + + @Override + public void addAdditionalSaveData(CompoundTag pCompound) { + super.addAdditionalSaveData(pCompound); + pCompound.putInt("Variant", this.getTypeVariant()); + } + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } + + @Override + protected @Nullable SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.MOGANOPTERUS_HURT.get(); + } + + @Override + protected @Nullable SoundEvent getDeathSound() { + return ModSounds.MOGANOPTERUS_DEATH.get(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/custom/NyctosaurusEntity.java b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/NyctosaurusEntity.java new file mode 100644 index 0000000..f1bccf1 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/NyctosaurusEntity.java @@ -0,0 +1,411 @@ +package net.cmr.jurassicrevived.entity.custom; + +import net.cmr.jurassicrevived.entity.ModEntities; +import net.cmr.jurassicrevived.entity.ai.SprintingMeleeAttackGoal; +import net.cmr.jurassicrevived.entity.ai.SprintingPanicGoal; +import net.cmr.jurassicrevived.entity.client.NyctosaurusVariant; +import net.cmr.jurassicrevived.entity.client.NyctosaurusVariant; +import net.cmr.jurassicrevived.sound.ModSounds; +import net.minecraft.Util; +import net.minecraft.core.BlockPos; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.*; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.attributes.DefaultAttributes; +import net.minecraft.world.entity.ai.control.FlyingMoveControl; +import net.minecraft.world.entity.ai.goal.*; +import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal; +import net.minecraft.world.entity.ai.goal.target.NearestAttackableTargetGoal; +import net.minecraft.world.entity.ai.navigation.FlyingPathNavigation; +import net.minecraft.world.entity.ai.navigation.PathNavigation; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.animal.FlyingAnimal; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerLevelAccessor; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.phys.Vec3; +import org.jetbrains.annotations.Nullable; +import software.bernie.geckolib.animatable.GeoEntity; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.core.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.core.animation.AnimatableManager; +import software.bernie.geckolib.core.animation.Animation; +import software.bernie.geckolib.core.animation.AnimationController; +import software.bernie.geckolib.core.animation.RawAnimation; +import software.bernie.geckolib.core.object.PlayState; +/*?} else {*/ +/*import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.animation.*; +*//*?}*/ + +public class NyctosaurusEntity extends Animal implements GeoEntity, FlyingAnimal { + private final AnimatableInstanceCache cache = new SingletonAnimatableInstanceCache(this); + + private static final EntityDataAccessor VARIANT = + SynchedEntityData.defineId(NyctosaurusEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SYNCED_AGE = + SynchedEntityData.defineId(NyctosaurusEntity.class, EntityDataSerializers.INT); + + // Procedural tail sway state (client-side use for rendering) + private float tailSwayOffset; // Smoothed offset in range roughly [-1, 1] + private float tailSwayVelocity; // Internal velocity for spring-damper + private float tailSwayPrev; // Previous frame value for interpolation + + public NyctosaurusEntity(EntityType pEntityType, Level pLevel) { + super(pEntityType, pLevel); + this.moveControl = new FlyingMoveControl(this, 20, true); + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new SprintingPanicGoal(this, 1.15) { + @Override + public boolean canUse() { + return NyctosaurusEntity.this.isBaby() && super.canUse(); + } + }); + this.goalSelector.addGoal(2, new FloatGoal(this)); + this.goalSelector.addGoal(3, new AvoidEntityGoal<>(this, BrachiosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(4, new SprintingMeleeAttackGoal(this, 1.1, false)); + this.goalSelector.addGoal(5, new BreedGoal(this, 1.0)); + this.goalSelector.addGoal(6, new FollowParentGoal(this, 1.25)); + + // Goal 7: Wander on ground (Walk) - Only when on ground + this.goalSelector.addGoal(7, new WaterAvoidingRandomStrollGoal(this, 1.0) { + @Override + public boolean canUse() { + return NyctosaurusEntity.this.onGround() && super.canUse(); + } + }); + + // Goal 8: Wander in air (Fly) - Handles takeoff, flying, and landing + this.goalSelector.addGoal(8, new WaterAvoidingRandomStrollGoal(this, 1.0, 20) { + @Override + public boolean canUse() { + boolean isFlying = !NyctosaurusEntity.this.onGround(); + // If flying, keep flying. If on ground, small chance (1/400 ticks) to take off. + return (isFlying || NyctosaurusEntity.this.getRandom().nextInt(400) == 0) && super.canUse(); + } + + @Override + protected Vec3 getPosition() { + Vec3 pos = NyctosaurusEntity.this.position(); + RandomSource random = NyctosaurusEntity.this.getRandom(); + + double x = pos.x + (random.nextFloat() * 2 - 1) * 32; + double z = pos.z + (random.nextFloat() * 2 - 1) * 32; + + // Get ground height at the random destination (returns Y of first air block) + int groundY = NyctosaurusEntity.this.level().getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, (int)x, (int)z); + double y; + + if (NyctosaurusEntity.this.onGround()) { + // Takeoff: Target well above ground to ensure liftoff + y = pos.y + 15 + random.nextInt(10); + } else { + // Flying: 5% chance to land, otherwise stay airborne but capped height + if (random.nextFloat() < 0.05f) { + y = groundY; // Land + } else if (pos.y > groundY + 20) { + // Too high: Force descent + y = pos.y - 5 - random.nextInt(10); + } else { + // Just wander up or down a bit + y = pos.y + (random.nextFloat() * 2 - 1) * 10; + } + } + + // Don't target below the ground (blocks) + if (y < groundY) y = groundY; + + return new Vec3(x, y, z); + } + }); + + this.goalSelector.addGoal(9, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(10, new FollowMobGoal(this, 0.8, (float) 20, (float) 10)); + this.targetSelector.addGoal(11, new NearestAttackableTargetGoal<>(this, Animal.class, 10, false, false, + target -> { + // 1. Don't eat your own species + if (target.getType() == this.getType()) return false; + + // 2. Don't eat Flying Animals + if (target instanceof FlyingAnimal) return false; + + // 3. SIZE CHECK: specific height and width limits + // Example: Height < 1.0 blocks AND Width < 1.0 blocks + boolean isSmallEnough = target.getBbHeight() <= 1.0F && target.getBbWidth() <= 1.0F; + + return isSmallEnough; + } + )); + this.targetSelector.addGoal(12, new NearestAttackableTargetGoal<>(this, Player.class, false, false)); + this.targetSelector.addGoal(13, new HurtByTargetGoal(this)); + this.goalSelector.addGoal(14, new RandomLookAroundGoal(this)); + this.goalSelector.addGoal(15, new FloatGoal(this)); + } + + public static AttributeSupplier.Builder createAttributes() { + return Animal.createLivingAttributes() + .add(Attributes.MAX_HEALTH, 20D) + .add(Attributes.MOVEMENT_SPEED, 0.3D) + .add(Attributes.FLYING_SPEED, 0.6D) + .add(Attributes.ARMOR, 0D) + .add(Attributes.FOLLOW_RANGE, 32D) + .add(Attributes.KNOCKBACK_RESISTANCE, 0D) + .add(Attributes.ATTACK_KNOCKBACK, 0D) + .add(Attributes.ATTACK_DAMAGE, 5D); + } + + @Override + protected PathNavigation createNavigation(Level pLevel) { + FlyingPathNavigation navigation = new FlyingPathNavigation(this, pLevel); + navigation.setCanOpenDoors(false); + navigation.setCanFloat(true); + navigation.setCanPassDoors(true); + return navigation; + } + + @Override + public boolean isFlying() { + return !this.onGround(); + } + + @Override + public boolean causeFallDamage(float pFallDistance, float pMultiplier, DamageSource pSource) { + return false; + } + + @Override + protected void checkFallDamage(double pY, boolean pOnGround, BlockState pState, BlockPos pPos) { + } + + @Override + public boolean isFood(ItemStack pStack) { + return pStack.is(Items.BEEF); + } + + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel pLevel, AgeableMob pOtherParent) { + AgeableMob child = ModEntities.NYCTOSAURUS.get().create(pLevel); + if (child instanceof NyctosaurusEntity baby) { + NyctosaurusVariant randomVariant = Util.getRandom(NyctosaurusVariant.values(), this.random); + baby.setVariant(randomVariant); + } + return child; + } + + @Override + public boolean doHurtTarget(Entity target) { + boolean hit = super.doHurtTarget(target); + if (!level().isClientSide && hit && target instanceof LivingEntity) { + if (this.level() instanceof ServerLevel serverLevel) { + this.triggerAnim("attackController", "attack"); + this.playSound(ModSounds.BEAK_ATTACK.get(), 1.0F, 1.0F); + } + } + return hit; + } + + @Override + public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { + controllers.add(new AnimationController<>(this, "Walk/Run/Idle", state -> { + if (!NyctosaurusEntity.this.onGround()) { + return state.setAndContinue(RawAnimation.begin().then("anim.nyctosaurus.fly", Animation.LoopType.LOOP)); + } + + if (state.isMoving()) + return state.setAndContinue(NyctosaurusEntity.this.isSprinting() ? RawAnimation.begin().then("anim.nyctosaurus.walk", Animation.LoopType.LOOP) : RawAnimation.begin().then("anim.nyctosaurus.walk", Animation.LoopType.LOOP)); + + return state.setAndContinue(RawAnimation.begin().then("anim.nyctosaurus.idle", Animation.LoopType.LOOP)); + })); + + controllers.add(new AnimationController<>(this, "attackController", state -> PlayState.STOP) + .triggerableAnim("attack", RawAnimation.begin().then("anim.nyctosaurus.attack", Animation.LoopType.PLAY_ONCE))); + + controllers.add(new AnimationController<>(this, "mouthController", state -> PlayState.STOP) + .triggerableAnim("mouth", RawAnimation.begin().then("anim.nyctosaurus.mouth", Animation.LoopType.PLAY_ONCE))); + } + + private float getSignedTurnDelta() { + // Only consider the body (torso) rotation so head look does not affect tail sway + return Mth.wrapDegrees(this.yBodyRot - this.yBodyRotO); + } + + private int mouthAnimCooldown = 0; + + @Override + public void tick() { + super.tick(); + if (!level().isClientSide) { + this.entityData.set(DATA_SYNCED_AGE, this.getAge()); + var maxHealthAttr = getAttribute(Attributes.MAX_HEALTH); + if (maxHealthAttr != null) { + double baseAdult = DefaultAttributes.getSupplier((EntityType) this.getType()).getValue(Attributes.MAX_HEALTH); + double desired = this.isBaby() ? baseAdult * 0.10D : baseAdult; + if (maxHealthAttr.getBaseValue() != desired) { + double oldMax = maxHealthAttr.getBaseValue(); + double healthRatio = this.getHealth() / (float) oldMax; + maxHealthAttr.setBaseValue(desired); + this.setHealth((float) (desired * Mth.clamp(healthRatio, 0.0F, 1.0F))); + } + } + } + + if (!level().isClientSide) { + if (mouthAnimCooldown > 0) { + mouthAnimCooldown--; + } else { + this.triggerAnim("mouthController", "mouth"); + this.playSound(ModSounds.NYCTOSAURUS_CALL.get(), 1.0F, 1.0F); + // 30s–60s in ticks + mouthAnimCooldown = this.random.nextInt(1200 - 600 + 1) + 600; + } + } + + if (level().isClientSide) { + // Capture previous for smooth interpolation between ticks + this.tailSwayPrev = this.tailSwayOffset; + updateProceduralTailSway(); + } + } + + private void updateProceduralTailSway() { + // Turn input derived from rotation deltas; works even when standing still and turning + float turnDegrees = getSignedTurnDelta(); + + // Deadzone to ignore tiny jitter so the tail can return to center cleanly + float deadzoneDeg = 0.6f; // smaller deadzone for more responsiveness + float turnInput = 0.0f; + if (Math.abs(turnDegrees) >= deadzoneDeg) { + // Higher sensitivity so small in-place turns still affect the model + turnInput = Mth.clamp(turnDegrees / 15.0f, -1.0f, 1.0f); + } + + // Target offset: keep intuitive sign (positive input -> positive sway) + float target = turnInput; + + // One-pole low-pass (no bounce). Larger alpha => snappier and less "stiff". + float alpha = 0.24f; // try 0.20–0.30 to taste + + tailSwayOffset += (target - tailSwayOffset) * alpha; + + // Snap tiny residuals to zero so it visibly settles + if (Math.abs(tailSwayOffset) < 0.003f) { + tailSwayOffset = 0.0f; + } + + // No oscillation velocity retained + tailSwayVelocity = 0.0f; + + tailSwayOffset = Mth.clamp(tailSwayOffset, -1.5f, 1.5f); + } + + // Expose to the model for bone rotation + public float getTailSwayOffset() { + return tailSwayOffset; + } + + // Interpolated sway for smooth rendering between ticks + public float getTailSwayOffset(float partialTick) { + return Mth.lerp(Mth.clamp(partialTick, 0.0f, 1.0f), tailSwayPrev, tailSwayOffset); + } + + /*? if <=1.20.1 {*/ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); + this.entityData.define(VARIANT, 0); + this.entityData.define(DATA_SYNCED_AGE, 0); + } + /*?} else {*/ + /*@Override + protected void defineSynchedData(SynchedEntityData.Builder pBuilder) { + super.defineSynchedData(pBuilder); + pBuilder.define(VARIANT, 0); + pBuilder.define(DATA_SYNCED_AGE, 0); + } + *//*?}*/ + + public int getSyncedAge() { + return this.entityData.get(DATA_SYNCED_AGE); + } + + public NyctosaurusVariant getVariant() { + return NyctosaurusVariant.byId(this.getTypeVariant() & 255); + } + public int getTypeVariant() { + return this.entityData.get(VARIANT); + } + + private void setVariant(NyctosaurusVariant variant) { + this.entityData.set(VARIANT, variant.getId() & 255); + } + + @Override + public boolean canMate(Animal other) { + if (!super.canMate(other)) return false; + if (!(other instanceof NyctosaurusEntity that)) return false; + return this.getVariant() != that.getVariant(); + } + + /*? if <=1.20.1 {*/ + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor pLevel, DifficultyInstance pDifficulty, MobSpawnType pReason, @Nullable SpawnGroupData pSpawnData, @Nullable CompoundTag pDataTag) { + NyctosaurusVariant variant = Util.getRandom(NyctosaurusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(pLevel, pDifficulty, pReason, pSpawnData, pDataTag); + } + /*?} else {*/ + /*@Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, MobSpawnType spawnType, @Nullable SpawnGroupData spawnGroupData) { + NyctosaurusVariant variant = Util.getRandom(NyctosaurusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(level, difficulty, spawnType, spawnGroupData); + } + *//*?}*/ + + @Override + public void readAdditionalSaveData(CompoundTag pCompound) { + super.readAdditionalSaveData(pCompound); + this.entityData.set(VARIANT, pCompound.getInt("Variant")); + } + + @Override + public void addAdditionalSaveData(CompoundTag pCompound) { + super.addAdditionalSaveData(pCompound); + pCompound.putInt("Variant", this.getTypeVariant()); + } + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } + + @Override + protected @Nullable SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.NYCTOSAURUS_HURT.get(); + } + + @Override + protected @Nullable SoundEvent getDeathSound() { + return ModSounds.NYCTOSAURUS_DEATH.get(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/custom/OrnitholestesEntity.java b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/OrnitholestesEntity.java new file mode 100644 index 0000000..ca12b44 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/OrnitholestesEntity.java @@ -0,0 +1,313 @@ +package net.cmr.jurassicrevived.entity.custom; + +import net.cmr.jurassicrevived.entity.ModEntities; +import net.cmr.jurassicrevived.entity.ai.SprintingMeleeAttackGoal; +import net.cmr.jurassicrevived.entity.ai.SprintingPanicGoal; +import net.cmr.jurassicrevived.entity.client.OrnitholestesVariant; +import net.cmr.jurassicrevived.entity.client.OrnitholestesVariant; +import net.cmr.jurassicrevived.sound.ModSounds; +import net.minecraft.Util; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.util.Mth; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.*; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.attributes.DefaultAttributes; +import net.minecraft.world.entity.ai.goal.*; +import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal; +import net.minecraft.world.entity.ai.goal.target.NearestAttackableTargetGoal; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerLevelAccessor; +import org.jetbrains.annotations.Nullable; +import software.bernie.geckolib.animatable.GeoEntity; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.core.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.core.animation.AnimatableManager; +import software.bernie.geckolib.core.animation.Animation; +import software.bernie.geckolib.core.animation.AnimationController; +import software.bernie.geckolib.core.animation.RawAnimation; +import software.bernie.geckolib.core.object.PlayState; +/*?} else {*/ +/*import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.animation.*; +*//*?}*/ + +public class OrnitholestesEntity extends Animal implements GeoEntity { + private AnimatableInstanceCache cache = new SingletonAnimatableInstanceCache(this); + + private static final EntityDataAccessor VARIANT = + SynchedEntityData.defineId(OrnitholestesEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SYNCED_AGE = + SynchedEntityData.defineId(OrnitholestesEntity.class, EntityDataSerializers.INT); + + // Procedural tail sway state (client-side use for rendering) + private float tailSwayOffset; // Smoothed offset in range roughly [-1, 1] + private float tailSwayVelocity; // Internal velocity for spring-damper + private float tailSwayPrev; // Previous frame value for interpolation + + public OrnitholestesEntity(EntityType pEntityType, Level pLevel) { + super(pEntityType, pLevel); + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new SprintingPanicGoal(this, 1.15) { + @Override + public boolean canUse() { + return OrnitholestesEntity.this.isBaby() && super.canUse(); + } + }); + this.targetSelector.addGoal(1, new HurtByTargetGoal(this).setAlertOthers()); + this.goalSelector.addGoal(2, new FloatGoal(this)); + this.goalSelector.addGoal(3, new AvoidEntityGoal<>(this, SpinosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(4, new AvoidEntityGoal<>(this, TyrannosaurusRexEntity.class, (float) 20, 1.2, 1.2)); + this.goalSelector.addGoal(5, new AvoidEntityGoal<>(this, VelociraptorEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(6, new AvoidEntityGoal<>(this, ParasaurolophusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(7, new AvoidEntityGoal<>(this, CeratosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(8, new AvoidEntityGoal<>(this, TriceratopsEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(9, new AvoidEntityGoal<>(this, BrachiosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(10, new SprintingMeleeAttackGoal(this, 1.1, false)); + this.goalSelector.addGoal(11, new BreedGoal(this, 1.0)); + this.goalSelector.addGoal(12, new FollowParentGoal(this, 1.25)); + this.goalSelector.addGoal(13, new WaterAvoidingRandomStrollGoal(this, 1.0)); + this.goalSelector.addGoal(14, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(15, new FollowMobGoal(this, 1, (float) 20, (float) 10)); + this.targetSelector.addGoal(16, new NearestAttackableTargetGoal<>(this, Animal.class, 10, false, false, + target -> target.getType() != this.getType())); + this.targetSelector.addGoal(17, new NearestAttackableTargetGoal(this, GallimimusEntity.class, false, false)); + this.targetSelector.addGoal(18, new NearestAttackableTargetGoal(this, Player.class, false, false)); + this.targetSelector.addGoal(19, new NearestAttackableTargetGoal(this, CompsognathusEntity.class, false, false)); + this.goalSelector.addGoal(20, new RandomLookAroundGoal(this)); + } + + public static AttributeSupplier.Builder createAttributes() { + return Animal.createLivingAttributes() + .add(Attributes.MAX_HEALTH, 14D) + .add(Attributes.MOVEMENT_SPEED, 0.3D) + .add(Attributes.ARMOR, 0D) + .add(Attributes.FOLLOW_RANGE, 32D) + .add(Attributes.KNOCKBACK_RESISTANCE, 0.3D) + .add(Attributes.ATTACK_KNOCKBACK, 0D) + .add(Attributes.ATTACK_DAMAGE, 4D); + } + + @Override + public boolean isFood(ItemStack pStack) { + return pStack.is(Items.BEEF); + } + + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel pLevel, AgeableMob pOtherParent) { + AgeableMob child = ModEntities.ORNITHOLESTES.get().create(pLevel); + if (child instanceof OrnitholestesEntity baby) { + OrnitholestesVariant randomVariant = Util.getRandom(OrnitholestesVariant.values(), this.random); + baby.setVariant(randomVariant); + } + return child; + } + + @Override + public boolean doHurtTarget(Entity target) { + boolean hit = super.doHurtTarget(target); + if (!level().isClientSide && hit && target instanceof LivingEntity) { + if (this.level() instanceof ServerLevel serverLevel) { + this.triggerAnim("attackController", "attack"); + } + } + return hit; + } + + @Override + public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { + controllers.add(new AnimationController<>(this, "Walk/Run/Idle", state -> { + if (state.isMoving()) + return state.setAndContinue(OrnitholestesEntity.this.isSprinting() ? RawAnimation.begin().then("anim.ornitholestes.run", Animation.LoopType.LOOP) : RawAnimation.begin().then("anim.ornitholestes.walk", Animation.LoopType.LOOP)); + + return state.setAndContinue(RawAnimation.begin().then("anim.ornitholestes.idle", Animation.LoopType.LOOP)); + })); + + controllers.add(new AnimationController<>(this, "attackController", state -> PlayState.STOP) + .triggerableAnim("attack", RawAnimation.begin().then("anim.ornitholestes.attack", Animation.LoopType.PLAY_ONCE))); + + controllers.add(new AnimationController<>(this, "mouthController", state -> PlayState.STOP) + .triggerableAnim("mouth", RawAnimation.begin().then("anim.ornitholestes.mouth", Animation.LoopType.PLAY_ONCE))); + } + + private float getSignedTurnDelta() { + // Only consider the body (torso) rotation so head look does not affect tail sway + return Mth.wrapDegrees(this.yBodyRot - this.yBodyRotO); + } + + private int mouthAnimCooldown = 0; + + @Override + public void tick() { + super.tick(); + if (!level().isClientSide) { + this.entityData.set(DATA_SYNCED_AGE, this.getAge()); + var maxHealthAttr = getAttribute(Attributes.MAX_HEALTH); + if (maxHealthAttr != null) { + double baseAdult = DefaultAttributes.getSupplier((EntityType) this.getType()).getValue(Attributes.MAX_HEALTH); + double desired = this.isBaby() ? baseAdult * 0.10D : baseAdult; + if (maxHealthAttr.getBaseValue() != desired) { + double oldMax = maxHealthAttr.getBaseValue(); + double healthRatio = this.getHealth() / (float) oldMax; + maxHealthAttr.setBaseValue(desired); + this.setHealth((float) (desired * Mth.clamp(healthRatio, 0.0F, 1.0F))); + } + } + } + + if (!level().isClientSide) { + if (mouthAnimCooldown > 0) { + mouthAnimCooldown--; + } else { + this.triggerAnim("mouthController", "mouth"); + this.playSound(ModSounds.ORNITHOLESTES_CALL.get(), 1.0F, 1.0F); + // 30s–60s in ticks + mouthAnimCooldown = this.random.nextInt(1200 - 600 + 1) + 600; + } + } + + if (level().isClientSide) { + // Capture previous for smooth interpolation between ticks + this.tailSwayPrev = this.tailSwayOffset; + updateProceduralTailSway(); + } + } + + private void updateProceduralTailSway() { + // Turn input derived from rotation deltas; works even when standing still and turning + float turnDegrees = getSignedTurnDelta(); + + // Deadzone to ignore tiny jitter so the tail can return to center cleanly + float deadzoneDeg = 0.6f; // smaller deadzone for more responsiveness + float turnInput = 0.0f; + if (Math.abs(turnDegrees) >= deadzoneDeg) { + // Higher sensitivity so small in-place turns still affect the model + turnInput = Mth.clamp(turnDegrees / 15.0f, -1.0f, 1.0f); + } + + // Target offset: keep intuitive sign (positive input -> positive sway) + float target = turnInput; + + // One-pole low-pass (no bounce). Larger alpha => snappier and less "stiff". + float alpha = 0.24f; // try 0.20–0.30 to taste + + tailSwayOffset += (target - tailSwayOffset) * alpha; + + // Snap tiny residuals to zero so it visibly settles + if (Math.abs(tailSwayOffset) < 0.003f) { + tailSwayOffset = 0.0f; + } + + // No oscillation velocity retained + tailSwayVelocity = 0.0f; + + tailSwayOffset = Mth.clamp(tailSwayOffset, -1.5f, 1.5f); + } + + // Expose to the model for bone rotation + public float getTailSwayOffset() { + return tailSwayOffset; + } + + // Interpolated sway for smooth rendering between ticks + public float getTailSwayOffset(float partialTick) { + return Mth.lerp(Mth.clamp(partialTick, 0.0f, 1.0f), tailSwayPrev, tailSwayOffset); + } + + /*? if <=1.20.1 {*/ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); + this.entityData.define(VARIANT, 0); + this.entityData.define(DATA_SYNCED_AGE, 0); + } + /*?} else {*/ + /*@Override + protected void defineSynchedData(SynchedEntityData.Builder pBuilder) { + super.defineSynchedData(pBuilder); + pBuilder.define(VARIANT, 0); + pBuilder.define(DATA_SYNCED_AGE, 0); + } + *//*?}*/ + + public int getSyncedAge() { + return this.entityData.get(DATA_SYNCED_AGE); + } + public int getTypeVariant() { + return this.entityData.get(VARIANT); + } + + public OrnitholestesVariant getVariant() { + return OrnitholestesVariant.byId(this.getTypeVariant() & 255); + } + + private void setVariant(OrnitholestesVariant variant) { + this.entityData.set(VARIANT, variant.getId() & 255); + } + + @Override + public boolean canMate(Animal other) { + if (!super.canMate(other)) return false; + if (!(other instanceof OrnitholestesEntity that)) return false; + return this.getVariant() != that.getVariant(); + } + @Override + public void addAdditionalSaveData(CompoundTag pCompound) { + super.addAdditionalSaveData(pCompound); + pCompound.putInt("Variant", this.getTypeVariant()); + } + + @Override + public void readAdditionalSaveData(CompoundTag pCompound) { + super.readAdditionalSaveData(pCompound); + this.entityData.set(VARIANT, pCompound.getInt("Variant")); + } + + /*? if <=1.20.1 {*/ + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor pLevel, DifficultyInstance pDifficulty, MobSpawnType pReason, @Nullable SpawnGroupData pSpawnData, @Nullable CompoundTag pDataTag) { + OrnitholestesVariant variant = Util.getRandom(OrnitholestesVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(pLevel, pDifficulty, pReason, pSpawnData, pDataTag); + } + /*?} else {*/ + /*@Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, MobSpawnType spawnType, @Nullable SpawnGroupData spawnGroupData) { + OrnitholestesVariant variant = Util.getRandom(OrnitholestesVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(level, difficulty, spawnType, spawnGroupData); + } + *//*?}*/ + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } + + @Override + protected @Nullable SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.ORNITHOLESTES_HURT.get(); + } + + @Override + protected @Nullable SoundEvent getDeathSound() { + return ModSounds.ORNITHOLESTES_DEATH.get(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/custom/OrnithomimusEntity.java b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/OrnithomimusEntity.java new file mode 100644 index 0000000..324b43d --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/OrnithomimusEntity.java @@ -0,0 +1,299 @@ +package net.cmr.jurassicrevived.entity.custom; + +import net.cmr.jurassicrevived.entity.ModEntities; +import net.cmr.jurassicrevived.entity.ai.SprintingPanicGoal; +import net.cmr.jurassicrevived.entity.client.AlbertosaurusVariant; +import net.cmr.jurassicrevived.entity.client.OrnithomimusVariant; +import net.cmr.jurassicrevived.sound.ModSounds; +import net.minecraft.Util; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.util.Mth; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.*; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.attributes.DefaultAttributes; +import net.minecraft.world.entity.ai.goal.*; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerLevelAccessor; +import org.jetbrains.annotations.Nullable; +import software.bernie.geckolib.animatable.GeoEntity; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.core.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.core.animation.AnimatableManager; +import software.bernie.geckolib.core.animation.Animation; +import software.bernie.geckolib.core.animation.AnimationController; +import software.bernie.geckolib.core.animation.RawAnimation; +import software.bernie.geckolib.core.object.PlayState; +/*?} else {*/ +/*import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.animation.*; +*//*?}*/ + +public class OrnithomimusEntity extends Animal implements GeoEntity { + private AnimatableInstanceCache cache = new SingletonAnimatableInstanceCache(this); + + private static final EntityDataAccessor VARIANT = + SynchedEntityData.defineId(OrnithomimusEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SYNCED_AGE = + SynchedEntityData.defineId(OrnithomimusEntity.class, EntityDataSerializers.INT); + + // Procedural tail sway state (client-side use for rendering) + private float tailSwayOffset; // Smoothed offset in range roughly [-1, 1] + private float tailSwayVelocity; // Internal velocity for spring-damper + private float tailSwayPrev; // Previous frame value for interpolation + + public OrnithomimusEntity(EntityType pEntityType, Level pLevel) { + super(pEntityType, pLevel); + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new SprintingPanicGoal(this, 1.15)); + this.goalSelector.addGoal(2, new FloatGoal(this)); + this.goalSelector.addGoal(3, new BreedGoal(this, 1.0)); + this.goalSelector.addGoal(4, new FollowParentGoal(this, 1.25)); + this.goalSelector.addGoal(5, new WaterAvoidingRandomStrollGoal(this, 1.0)); + this.goalSelector.addGoal(6, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(7, new FollowMobGoal(this, 1, (float) 20, (float) 10)); + this.goalSelector.addGoal(8, new AvoidEntityGoal<>(this, TriceratopsEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(9, new AvoidEntityGoal<>(this, DilophosaurusEntity.class, (float) 20, 1.2, 1.2)); + this.goalSelector.addGoal(10, new AvoidEntityGoal<>(this, VelociraptorEntity.class, (float) 20, 1.2, 1.2)); + this.goalSelector.addGoal(11, new AvoidEntityGoal<>(this, CeratosaurusEntity.class, (float) 20, 1.2, 1.2)); + this.goalSelector.addGoal(12, new AvoidEntityGoal<>(this, SpinosaurusEntity.class, (float) 20, 1.2, 1.2)); + this.goalSelector.addGoal(13, new AvoidEntityGoal<>(this, TyrannosaurusRexEntity.class, (float) 20, 1.2, 1.2)); + this.goalSelector.addGoal(14, new AvoidEntityGoal<>(this, IndominusRexEntity.class, (float) 20, 1.2, 1.2)); + this.goalSelector.addGoal(15, new RandomLookAroundGoal(this)); + + } + + public static AttributeSupplier.Builder createAttributes() { + return Animal.createLivingAttributes() + .add(Attributes.MAX_HEALTH, 25D) + .add(Attributes.MOVEMENT_SPEED, 0.3D) + .add(Attributes.ARMOR, 0D) + .add(Attributes.FOLLOW_RANGE, 32D) + .add(Attributes.KNOCKBACK_RESISTANCE, 0D) + .add(Attributes.ATTACK_KNOCKBACK, 0D) + .add(Attributes.ATTACK_DAMAGE, 0D); + } + + @Override + public boolean isFood(ItemStack pStack) { + return pStack.is(Items.KELP); + } + + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel pLevel, AgeableMob pOtherParent) { + AgeableMob child = ModEntities.ORNITHOMIMUS.get().create(pLevel); + if (child instanceof OrnithomimusEntity baby) { + OrnithomimusVariant randomVariant = Util.getRandom(OrnithomimusVariant.values(), this.random); + baby.setVariant(randomVariant); + } + return child; + } + + @Override + public boolean doHurtTarget(Entity target) { + boolean hit = super.doHurtTarget(target); + if (!level().isClientSide && hit && target instanceof LivingEntity) { + if (this.level() instanceof ServerLevel serverLevel) { + this.triggerAnim("attackController", "attack"); + } + } + return hit; + } + + @Override + public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { + controllers.add(new AnimationController<>(this, "Walk/Run/Idle", state -> { + if (state.isMoving()) + return state.setAndContinue(OrnithomimusEntity.this.isSprinting() ? RawAnimation.begin().then("anim.ornithomimus.run", Animation.LoopType.LOOP) : RawAnimation.begin().then("anim.ornithomimus.walk", Animation.LoopType.LOOP)); + + return state.setAndContinue(RawAnimation.begin().then("anim.ornithomimus.idle", Animation.LoopType.LOOP)); + })); + + controllers.add(new AnimationController<>(this, "attackController", state -> PlayState.STOP) + .triggerableAnim("attack", RawAnimation.begin().then("anim.ornithomimus.attack", Animation.LoopType.PLAY_ONCE))); + + controllers.add(new AnimationController<>(this, "mouthController", state -> PlayState.STOP) + .triggerableAnim("mouth", RawAnimation.begin().then("anim.ornithomimus.mouth", Animation.LoopType.PLAY_ONCE))); + } + + private float getSignedTurnDelta() { + // Only consider the body (torso) rotation so head look does not affect tail sway + return Mth.wrapDegrees(this.yBodyRot - this.yBodyRotO); + } + + private int mouthAnimCooldown = 0; + + @Override + public void tick() { + super.tick(); + if (!level().isClientSide) { + this.entityData.set(DATA_SYNCED_AGE, this.getAge()); + var maxHealthAttr = getAttribute(Attributes.MAX_HEALTH); + if (maxHealthAttr != null) { + double baseAdult = DefaultAttributes.getSupplier((EntityType) this.getType()).getValue(Attributes.MAX_HEALTH); + double desired = this.isBaby() ? baseAdult * 0.10D : baseAdult; + if (maxHealthAttr.getBaseValue() != desired) { + double oldMax = maxHealthAttr.getBaseValue(); + double healthRatio = this.getHealth() / (float) oldMax; + maxHealthAttr.setBaseValue(desired); + this.setHealth((float) (desired * Mth.clamp(healthRatio, 0.0F, 1.0F))); + } + } + } + + if (!level().isClientSide) { + if (mouthAnimCooldown > 0) { + mouthAnimCooldown--; + } else { + this.triggerAnim("mouthController", "mouth"); + this.playSound(ModSounds.ORNITHOMIMUS_CALL.get(), 1.0F, 1.0F); + // 30s–60s in ticks + mouthAnimCooldown = this.random.nextInt(1200 - 600 + 1) + 600; + } + } + + if (level().isClientSide) { + // Capture previous for smooth interpolation between ticks + this.tailSwayPrev = this.tailSwayOffset; + updateProceduralTailSway(); + } + } + + private void updateProceduralTailSway() { + // Turn input derived from rotation deltas; works even when standing still and turning + float turnDegrees = getSignedTurnDelta(); + + // Deadzone to ignore tiny jitter so the tail can return to center cleanly + float deadzoneDeg = 0.6f; // smaller deadzone for more responsiveness + float turnInput = 0.0f; + if (Math.abs(turnDegrees) >= deadzoneDeg) { + // Higher sensitivity so small in-place turns still affect the model + turnInput = Mth.clamp(turnDegrees / 15.0f, -1.0f, 1.0f); + } + + // Target offset: keep intuitive sign (positive input -> positive sway) + float target = turnInput; + + // One-pole low-pass (no bounce). Larger alpha => snappier and less "stiff". + float alpha = 0.24f; // try 0.20–0.30 to taste + + tailSwayOffset += (target - tailSwayOffset) * alpha; + + // Snap tiny residuals to zero so it visibly settles + if (Math.abs(tailSwayOffset) < 0.003f) { + tailSwayOffset = 0.0f; + } + + // No oscillation velocity retained + tailSwayVelocity = 0.0f; + + tailSwayOffset = Mth.clamp(tailSwayOffset, -1.5f, 1.5f); + } + + // Expose to the model for bone rotation + public float getTailSwayOffset() { + return tailSwayOffset; + } + + // Interpolated sway for smooth rendering between ticks + public float getTailSwayOffset(float partialTick) { + return Mth.lerp(Mth.clamp(partialTick, 0.0f, 1.0f), tailSwayPrev, tailSwayOffset); + } + + /*? if <=1.20.1 {*/ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); + this.entityData.define(VARIANT, 0); + this.entityData.define(DATA_SYNCED_AGE, 0); + } + /*?} else {*/ + /*@Override + protected void defineSynchedData(SynchedEntityData.Builder pBuilder) { + super.defineSynchedData(pBuilder); + pBuilder.define(VARIANT, 0); + pBuilder.define(DATA_SYNCED_AGE, 0); + } + *//*?}*/ + + public int getSyncedAge() { + return this.entityData.get(DATA_SYNCED_AGE); + } + public int getTypeVariant() { + return this.entityData.get(VARIANT); + } + + public OrnithomimusVariant getVariant() { + return OrnithomimusVariant.byId(this.getTypeVariant() & 255); + } + + private void setVariant(OrnithomimusVariant variant) { + this.entityData.set(VARIANT, variant.getId() & 255); + } + + @Override + public boolean canMate(Animal other) { + if (!super.canMate(other)) return false; + if (!(other instanceof OrnithomimusEntity that)) return false; + return this.getVariant() != that.getVariant(); + } + @Override + public void addAdditionalSaveData(CompoundTag pCompound) { + super.addAdditionalSaveData(pCompound); + pCompound.putInt("Variant", this.getTypeVariant()); + } + + @Override + public void readAdditionalSaveData(CompoundTag pCompound) { + super.readAdditionalSaveData(pCompound); + this.entityData.set(VARIANT, pCompound.getInt("Variant")); + } + + /*? if <=1.20.1 {*/ + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor pLevel, DifficultyInstance pDifficulty, MobSpawnType pReason, @Nullable SpawnGroupData pSpawnData, @Nullable CompoundTag pDataTag) { + OrnithomimusVariant variant = Util.getRandom(OrnithomimusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(pLevel, pDifficulty, pReason, pSpawnData, pDataTag); + } + /*?} else {*/ + /*@Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, MobSpawnType spawnType, @Nullable SpawnGroupData spawnGroupData) { + OrnithomimusVariant variant = Util.getRandom(OrnithomimusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(level, difficulty, spawnType, spawnGroupData); + } + *//*?}*/ + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } + + @Override + protected @Nullable SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.ORNITHOMIMUS_HURT.get(); + } + + @Override + protected @Nullable SoundEvent getDeathSound() { + return ModSounds.ORNITHOMIMUS_DEATH.get(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/custom/OuranosaurusEntity.java b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/OuranosaurusEntity.java new file mode 100644 index 0000000..7ec28f2 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/OuranosaurusEntity.java @@ -0,0 +1,297 @@ +package net.cmr.jurassicrevived.entity.custom; + +import net.cmr.jurassicrevived.entity.ModEntities; +import net.cmr.jurassicrevived.entity.ai.SprintingPanicGoal; +import net.cmr.jurassicrevived.entity.client.AlbertosaurusVariant; +import net.cmr.jurassicrevived.entity.client.OuranosaurusVariant; +import net.cmr.jurassicrevived.sound.ModSounds; +import net.minecraft.Util; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.util.Mth; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.*; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.attributes.DefaultAttributes; +import net.minecraft.world.entity.ai.goal.*; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerLevelAccessor; +import org.jetbrains.annotations.Nullable; +import software.bernie.geckolib.animatable.GeoEntity; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.core.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.core.animation.AnimatableManager; +import software.bernie.geckolib.core.animation.Animation; +import software.bernie.geckolib.core.animation.AnimationController; +import software.bernie.geckolib.core.animation.RawAnimation; +import software.bernie.geckolib.core.object.PlayState; +/*?} else {*/ +/*import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.animation.*; +*//*?}*/ + +public class OuranosaurusEntity extends Animal implements GeoEntity { + private AnimatableInstanceCache cache = new SingletonAnimatableInstanceCache(this); + + private static final EntityDataAccessor VARIANT = + SynchedEntityData.defineId(OuranosaurusEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SYNCED_AGE = + SynchedEntityData.defineId(OuranosaurusEntity.class, EntityDataSerializers.INT); + + // Procedural tail sway state (client-side use for rendering) + private float tailSwayOffset; // Smoothed offset in range roughly [-1, 1] + private float tailSwayVelocity; // Internal velocity for spring-damper + private float tailSwayPrev; // Previous frame value for interpolation + + public OuranosaurusEntity(EntityType pEntityType, Level pLevel) { + super(pEntityType, pLevel); + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new SprintingPanicGoal(this, 1.15)); + this.goalSelector.addGoal(2, new FloatGoal(this)); + this.goalSelector.addGoal(3, new BreedGoal(this, 1.0)); + this.goalSelector.addGoal(4, new FollowParentGoal(this, 1.25)); + this.goalSelector.addGoal(5, new WaterAvoidingRandomStrollGoal(this, 1.0)); + this.goalSelector.addGoal(6, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(7, new FollowMobGoal(this, 1, (float) 20, (float) 10)); + this.goalSelector.addGoal(8, new AvoidEntityGoal<>(this, DilophosaurusEntity.class, (float) 20, 1.2, 1.2)); + this.goalSelector.addGoal(9, new AvoidEntityGoal<>(this, VelociraptorEntity.class, (float) 20, 1.2, 1.2)); + this.goalSelector.addGoal(10, new AvoidEntityGoal<>(this, CeratosaurusEntity.class, (float) 20, 1.2, 1.2)); + this.goalSelector.addGoal(11, new AvoidEntityGoal<>(this, SpinosaurusEntity.class, (float) 20, 1.2, 1.2)); + this.goalSelector.addGoal(12, new AvoidEntityGoal<>(this, TyrannosaurusRexEntity.class, (float) 20, 1.2, 1.2)); + this.goalSelector.addGoal(13, new RandomLookAroundGoal(this)); + + } + + public static AttributeSupplier.Builder createAttributes() { + return Animal.createLivingAttributes() + .add(Attributes.MAX_HEALTH, 55D) + .add(Attributes.MOVEMENT_SPEED, 0.3D) + .add(Attributes.ARMOR, 0D) + .add(Attributes.FOLLOW_RANGE, 32D) + .add(Attributes.KNOCKBACK_RESISTANCE, 0D) + .add(Attributes.ATTACK_KNOCKBACK, 0D) + .add(Attributes.ATTACK_DAMAGE, 12D); + } + + @Override + public boolean isFood(ItemStack pStack) { + return pStack.is(Items.KELP); + } + + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel pLevel, AgeableMob pOtherParent) { + AgeableMob child = ModEntities.OURANOSAURUS.get().create(pLevel); + if (child instanceof OuranosaurusEntity baby) { + OuranosaurusVariant randomVariant = Util.getRandom(OuranosaurusVariant.values(), this.random); + baby.setVariant(randomVariant); + } + return child; + } + + @Override + public boolean doHurtTarget(Entity target) { + boolean hit = super.doHurtTarget(target); + if (!level().isClientSide && hit && target instanceof LivingEntity) { + if (this.level() instanceof ServerLevel serverLevel) { + this.triggerAnim("attackController", "attack"); + } + } + return hit; + } + + @Override + public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { + controllers.add(new AnimationController<>(this, "Walk/Run/Idle", state -> { + if (state.isMoving()) + return state.setAndContinue(OuranosaurusEntity.this.isSprinting() ? RawAnimation.begin().then("anim.ouranosaurus.run", Animation.LoopType.LOOP) : RawAnimation.begin().then("anim.ouranosaurus.walk", Animation.LoopType.LOOP)); + + return state.setAndContinue(RawAnimation.begin().then("anim.ouranosaurus.idle", Animation.LoopType.LOOP)); + })); + + controllers.add(new AnimationController<>(this, "attackController", state -> PlayState.STOP) + .triggerableAnim("attack", RawAnimation.begin().then("anim.ouranosaurus.attack", Animation.LoopType.PLAY_ONCE))); + + controllers.add(new AnimationController<>(this, "mouthController", state -> PlayState.STOP) + .triggerableAnim("mouth", RawAnimation.begin().then("anim.ouranosaurus.mouth", Animation.LoopType.PLAY_ONCE))); + } + + private float getSignedTurnDelta() { + // Only consider the body (torso) rotation so head look does not affect tail sway + return Mth.wrapDegrees(this.yBodyRot - this.yBodyRotO); + } + + private int mouthAnimCooldown = 0; + + @Override + public void tick() { + super.tick(); + if (!level().isClientSide) { + this.entityData.set(DATA_SYNCED_AGE, this.getAge()); + var maxHealthAttr = getAttribute(Attributes.MAX_HEALTH); + if (maxHealthAttr != null) { + double baseAdult = DefaultAttributes.getSupplier((EntityType) this.getType()).getValue(Attributes.MAX_HEALTH); + double desired = this.isBaby() ? baseAdult * 0.10D : baseAdult; + if (maxHealthAttr.getBaseValue() != desired) { + double oldMax = maxHealthAttr.getBaseValue(); + double healthRatio = this.getHealth() / (float) oldMax; + maxHealthAttr.setBaseValue(desired); + this.setHealth((float) (desired * Mth.clamp(healthRatio, 0.0F, 1.0F))); + } + } + } + + if (!level().isClientSide) { + if (mouthAnimCooldown > 0) { + mouthAnimCooldown--; + } else { + this.triggerAnim("mouthController", "mouth"); + this.playSound(ModSounds.OURANOSAURUS_CALL.get(), 1.0F, 1.0F); + // 30s–60s in ticks + mouthAnimCooldown = this.random.nextInt(1200 - 600 + 1) + 600; + } + } + + if (level().isClientSide) { + // Capture previous for smooth interpolation between ticks + this.tailSwayPrev = this.tailSwayOffset; + updateProceduralTailSway(); + } + } + + private void updateProceduralTailSway() { + // Turn input derived from rotation deltas; works even when standing still and turning + float turnDegrees = getSignedTurnDelta(); + + // Deadzone to ignore tiny jitter so the tail can return to center cleanly + float deadzoneDeg = 0.6f; // smaller deadzone for more responsiveness + float turnInput = 0.0f; + if (Math.abs(turnDegrees) >= deadzoneDeg) { + // Higher sensitivity so small in-place turns still affect the model + turnInput = Mth.clamp(turnDegrees / 15.0f, -1.0f, 1.0f); + } + + // Target offset: keep intuitive sign (positive input -> positive sway) + float target = turnInput; + + // One-pole low-pass (no bounce). Larger alpha => snappier and less "stiff". + float alpha = 0.24f; // try 0.20–0.30 to taste + + tailSwayOffset += (target - tailSwayOffset) * alpha; + + // Snap tiny residuals to zero so it visibly settles + if (Math.abs(tailSwayOffset) < 0.003f) { + tailSwayOffset = 0.0f; + } + + // No oscillation velocity retained + tailSwayVelocity = 0.0f; + + tailSwayOffset = Mth.clamp(tailSwayOffset, -1.5f, 1.5f); + } + + // Expose to the model for bone rotation + public float getTailSwayOffset() { + return tailSwayOffset; + } + + // Interpolated sway for smooth rendering between ticks + public float getTailSwayOffset(float partialTick) { + return Mth.lerp(Mth.clamp(partialTick, 0.0f, 1.0f), tailSwayPrev, tailSwayOffset); + } + + /*? if <=1.20.1 {*/ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); + this.entityData.define(VARIANT, 0); + this.entityData.define(DATA_SYNCED_AGE, 0); + } + /*?} else {*/ + /*@Override + protected void defineSynchedData(SynchedEntityData.Builder pBuilder) { + super.defineSynchedData(pBuilder); + pBuilder.define(VARIANT, 0); + pBuilder.define(DATA_SYNCED_AGE, 0); + } + *//*?}*/ + + public int getSyncedAge() { + return this.entityData.get(DATA_SYNCED_AGE); + } + public int getTypeVariant() { + return this.entityData.get(VARIANT); + } + + public OuranosaurusVariant getVariant() { + return OuranosaurusVariant.byId(this.getTypeVariant() & 255); + } + + private void setVariant(OuranosaurusVariant variant) { + this.entityData.set(VARIANT, variant.getId() & 255); + } + + @Override + public boolean canMate(Animal other) { + if (!super.canMate(other)) return false; + if (!(other instanceof OuranosaurusEntity that)) return false; + return this.getVariant() != that.getVariant(); + } + @Override + public void addAdditionalSaveData(CompoundTag pCompound) { + super.addAdditionalSaveData(pCompound); + pCompound.putInt("Variant", this.getTypeVariant()); + } + + @Override + public void readAdditionalSaveData(CompoundTag pCompound) { + super.readAdditionalSaveData(pCompound); + this.entityData.set(VARIANT, pCompound.getInt("Variant")); + } + + /*? if <=1.20.1 {*/ + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor pLevel, DifficultyInstance pDifficulty, MobSpawnType pReason, @Nullable SpawnGroupData pSpawnData, @Nullable CompoundTag pDataTag) { + OuranosaurusVariant variant = Util.getRandom(OuranosaurusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(pLevel, pDifficulty, pReason, pSpawnData, pDataTag); + } + /*?} else {*/ + /*@Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, MobSpawnType spawnType, @Nullable SpawnGroupData spawnGroupData) { + OuranosaurusVariant variant = Util.getRandom(OuranosaurusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(level, difficulty, spawnType, spawnGroupData); + } + *//*?}*/ + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } + + @Override + protected @Nullable SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.OURANOSAURUS_HURT.get(); + } + + @Override + protected @Nullable SoundEvent getDeathSound() { + return ModSounds.OURANOSAURUS_DEATH.get(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/custom/OviraptorEntity.java b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/OviraptorEntity.java new file mode 100644 index 0000000..53de8d9 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/OviraptorEntity.java @@ -0,0 +1,300 @@ +package net.cmr.jurassicrevived.entity.custom; + +import net.cmr.jurassicrevived.entity.ModEntities; +import net.cmr.jurassicrevived.entity.ai.SprintingMeleeAttackGoal; +import net.cmr.jurassicrevived.entity.ai.SprintingPanicGoal; +import net.cmr.jurassicrevived.entity.client.AlbertosaurusVariant; +import net.cmr.jurassicrevived.entity.client.OviraptorVariant; +import net.cmr.jurassicrevived.sound.ModSounds; +import net.minecraft.Util; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.util.Mth; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.*; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.attributes.DefaultAttributes; +import net.minecraft.world.entity.ai.goal.*; +import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerLevelAccessor; +import org.jetbrains.annotations.Nullable; +import software.bernie.geckolib.animatable.GeoEntity; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.core.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.core.animation.AnimatableManager; +import software.bernie.geckolib.core.animation.Animation; +import software.bernie.geckolib.core.animation.AnimationController; +import software.bernie.geckolib.core.animation.RawAnimation; +import software.bernie.geckolib.core.object.PlayState; +/*?} else {*/ +/*import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.animation.*; +*//*?}*/ + +public class OviraptorEntity extends Animal implements GeoEntity { + private AnimatableInstanceCache cache = new SingletonAnimatableInstanceCache(this); + + private static final EntityDataAccessor VARIANT = + SynchedEntityData.defineId(OviraptorEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SYNCED_AGE = + SynchedEntityData.defineId(OviraptorEntity.class, EntityDataSerializers.INT); + + // Procedural tail sway state (client-side use for rendering) + private float tailSwayOffset; // Smoothed offset in range roughly [-1, 1] + private float tailSwayVelocity; // Internal velocity for spring-damper + private float tailSwayPrev; // Previous frame value for interpolation + + public OviraptorEntity(EntityType pEntityType, Level pLevel) { + super(pEntityType, pLevel); + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new SprintingPanicGoal(this, 1.15) { + @Override + public boolean canUse() { + return OviraptorEntity.this.isBaby() && super.canUse(); + } + }); + this.targetSelector.addGoal(1, new HurtByTargetGoal(this).setAlertOthers()); + this.goalSelector.addGoal(2, new FloatGoal(this)); + this.goalSelector.addGoal(3, new SprintingMeleeAttackGoal(this, 1.1, false)); + this.goalSelector.addGoal(4, new BreedGoal(this, 1.0)); + this.goalSelector.addGoal(5, new FollowParentGoal(this, 1.25)); + this.goalSelector.addGoal(6, new WaterAvoidingRandomStrollGoal(this, 1.0)); + this.goalSelector.addGoal(7, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(8, new FollowMobGoal(this, 1, (float) 20, (float) 10)); + this.goalSelector.addGoal(9, new RandomLookAroundGoal(this)); + } + + public static AttributeSupplier.Builder createAttributes() { + return Animal.createLivingAttributes() + .add(Attributes.MAX_HEALTH, 10D) + .add(Attributes.MOVEMENT_SPEED, 0.3D) + .add(Attributes.ARMOR, 0D) + .add(Attributes.FOLLOW_RANGE, 32D) + .add(Attributes.ATTACK_DAMAGE, 2D) + .add(Attributes.KNOCKBACK_RESISTANCE, 0.3D) + .add(Attributes.ATTACK_KNOCKBACK, 0D); + } + + @Override + public boolean isFood(ItemStack pStack) { + return pStack.is(Items.BEEF); + } + + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel pLevel, AgeableMob pOtherParent) { + AgeableMob child = ModEntities.OVIRAPTOR.get().create(pLevel); + if (child instanceof OviraptorEntity baby) { + OviraptorVariant randomVariant = Util.getRandom(OviraptorVariant.values(), this.random); + baby.setVariant(randomVariant); + } + return child; + } + + @Override + public boolean doHurtTarget(Entity target) { + boolean hit = super.doHurtTarget(target); + if (!level().isClientSide && hit && target instanceof LivingEntity) { + if (this.level() instanceof ServerLevel serverLevel) { + this.triggerAnim("attackController", "attack"); + } + } + return hit; + } + + @Override + public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { + controllers.add(new AnimationController<>(this, "Walk/Run/Idle", state -> { + if (state.isMoving()) + return state.setAndContinue(OviraptorEntity.this.isSprinting() ? RawAnimation.begin().then("anim.oviraptor.run", Animation.LoopType.LOOP) : RawAnimation.begin().then("anim.oviraptor.walk", Animation.LoopType.LOOP)); + + return state.setAndContinue(RawAnimation.begin().then("anim.oviraptor.idle", Animation.LoopType.LOOP)); + })); + + controllers.add(new AnimationController<>(this, "attackController", state -> PlayState.STOP) + .triggerableAnim("attack", RawAnimation.begin().then("anim.oviraptor.attack", Animation.LoopType.PLAY_ONCE))); + + controllers.add(new AnimationController<>(this, "mouthController", state -> PlayState.STOP) + .triggerableAnim("mouth", RawAnimation.begin().then("anim.oviraptor.mouth", Animation.LoopType.PLAY_ONCE))); + } + + private float getSignedTurnDelta() { + // Only consider the body (torso) rotation so head look does not affect tail sway + return Mth.wrapDegrees(this.yBodyRot - this.yBodyRotO); + } + + private int mouthAnimCooldown = 0; + + @Override + public void tick() { + super.tick(); + if (!level().isClientSide) { + this.entityData.set(DATA_SYNCED_AGE, this.getAge()); + var maxHealthAttr = getAttribute(Attributes.MAX_HEALTH); + if (maxHealthAttr != null) { + double baseAdult = DefaultAttributes.getSupplier((EntityType) this.getType()).getValue(Attributes.MAX_HEALTH); + double desired = this.isBaby() ? baseAdult * 0.10D : baseAdult; + if (maxHealthAttr.getBaseValue() != desired) { + double oldMax = maxHealthAttr.getBaseValue(); + double healthRatio = this.getHealth() / (float) oldMax; + maxHealthAttr.setBaseValue(desired); + this.setHealth((float) (desired * Mth.clamp(healthRatio, 0.0F, 1.0F))); + } + } + } + + if (!level().isClientSide) { + if (mouthAnimCooldown > 0) { + mouthAnimCooldown--; + } else { + this.triggerAnim("mouthController", "mouth"); + this.playSound(ModSounds.OVIRAPTOR_CALL.get(), 1.0F, 1.0F); + // 30s–60s in ticks + mouthAnimCooldown = this.random.nextInt(1200 - 600 + 1) + 600; + } + } + + if (level().isClientSide) { + // Capture previous for smooth interpolation between ticks + this.tailSwayPrev = this.tailSwayOffset; + updateProceduralTailSway(); + } + } + + private void updateProceduralTailSway() { + // Turn input derived from rotation deltas; works even when standing still and turning + float turnDegrees = getSignedTurnDelta(); + + // Deadzone to ignore tiny jitter so the tail can return to center cleanly + float deadzoneDeg = 0.6f; // smaller deadzone for more responsiveness + float turnInput = 0.0f; + if (Math.abs(turnDegrees) >= deadzoneDeg) { + // Higher sensitivity so small in-place turns still affect the model + turnInput = Mth.clamp(turnDegrees / 15.0f, -1.0f, 1.0f); + } + + // Target offset: keep intuitive sign (positive input -> positive sway) + float target = turnInput; + + // One-pole low-pass (no bounce). Larger alpha => snappier and less "stiff". + float alpha = 0.24f; // try 0.20–0.30 to taste + + tailSwayOffset += (target - tailSwayOffset) * alpha; + + // Snap tiny residuals to zero so it visibly settles + if (Math.abs(tailSwayOffset) < 0.003f) { + tailSwayOffset = 0.0f; + } + + // No oscillation velocity retained + tailSwayVelocity = 0.0f; + + tailSwayOffset = Mth.clamp(tailSwayOffset, -1.5f, 1.5f); + } + + // Expose to the model for bone rotation + public float getTailSwayOffset() { + return tailSwayOffset; + } + + // Interpolated sway for smooth rendering between ticks + public float getTailSwayOffset(float partialTick) { + return Mth.lerp(Mth.clamp(partialTick, 0.0f, 1.0f), tailSwayPrev, tailSwayOffset); + } + + /*? if <=1.20.1 {*/ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); + this.entityData.define(VARIANT, 0); + this.entityData.define(DATA_SYNCED_AGE, 0); + } + /*?} else {*/ + /*@Override + protected void defineSynchedData(SynchedEntityData.Builder pBuilder) { + super.defineSynchedData(pBuilder); + pBuilder.define(VARIANT, 0); + pBuilder.define(DATA_SYNCED_AGE, 0); + } + *//*?}*/ + + public int getSyncedAge() { + return this.entityData.get(DATA_SYNCED_AGE); + } + public int getTypeVariant() { + return this.entityData.get(VARIANT); + } + + public OviraptorVariant getVariant() { + return OviraptorVariant.byId(this.getTypeVariant() & 255); + } + + private void setVariant(OviraptorVariant variant) { + this.entityData.set(VARIANT, variant.getId() & 255); + } + + @Override + public boolean canMate(Animal other) { + if (!super.canMate(other)) return false; + if (!(other instanceof OviraptorEntity that)) return false; + return this.getVariant() != that.getVariant(); + } + @Override + public void addAdditionalSaveData(CompoundTag pCompound) { + super.addAdditionalSaveData(pCompound); + pCompound.putInt("Variant", this.getTypeVariant()); + } + + @Override + public void readAdditionalSaveData(CompoundTag pCompound) { + super.readAdditionalSaveData(pCompound); + this.entityData.set(VARIANT, pCompound.getInt("Variant")); + } + + /*? if <=1.20.1 {*/ + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor pLevel, DifficultyInstance pDifficulty, MobSpawnType pReason, @Nullable SpawnGroupData pSpawnData, @Nullable CompoundTag pDataTag) { + OviraptorVariant variant = Util.getRandom(OviraptorVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(pLevel, pDifficulty, pReason, pSpawnData, pDataTag); + } + /*?} else {*/ + /*@Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, MobSpawnType spawnType, @Nullable SpawnGroupData spawnGroupData) { + OviraptorVariant variant = Util.getRandom(OviraptorVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(level, difficulty, spawnType, spawnGroupData); + } + *//*?}*/ + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } + + @Override + protected @Nullable SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.OVIRAPTOR_HURT.get(); + } + + @Override + protected @Nullable SoundEvent getDeathSound() { + return ModSounds.OVIRAPTOR_DEATH.get(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/custom/PachycephalosaurusEntity.java b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/PachycephalosaurusEntity.java new file mode 100644 index 0000000..225f945 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/PachycephalosaurusEntity.java @@ -0,0 +1,307 @@ +package net.cmr.jurassicrevived.entity.custom; + +import net.cmr.jurassicrevived.entity.ModEntities; +import net.cmr.jurassicrevived.entity.ai.SprintingMeleeAttackGoal; +import net.cmr.jurassicrevived.entity.ai.SprintingPanicGoal; +import net.cmr.jurassicrevived.entity.client.AlbertosaurusVariant; +import net.cmr.jurassicrevived.entity.client.PachycephalosaurusVariant; +import net.cmr.jurassicrevived.sound.ModSounds; +import net.minecraft.Util; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.util.Mth; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.*; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.attributes.DefaultAttributes; +import net.minecraft.world.entity.ai.goal.*; +import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerLevelAccessor; +import org.jetbrains.annotations.Nullable; +import software.bernie.geckolib.animatable.GeoEntity; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.core.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.core.animation.AnimatableManager; +import software.bernie.geckolib.core.animation.Animation; +import software.bernie.geckolib.core.animation.AnimationController; +import software.bernie.geckolib.core.animation.RawAnimation; +import software.bernie.geckolib.core.object.PlayState; +/*?} else {*/ +/*import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.animation.*; +*//*?}*/ + +public class PachycephalosaurusEntity extends Animal implements GeoEntity { + private AnimatableInstanceCache cache = new SingletonAnimatableInstanceCache(this); + + private static final EntityDataAccessor VARIANT = + SynchedEntityData.defineId(PachycephalosaurusEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SYNCED_AGE = + SynchedEntityData.defineId(PachycephalosaurusEntity.class, EntityDataSerializers.INT); + + // Procedural tail sway state (client-side use for rendering) + private float tailSwayOffset; // Smoothed offset in range roughly [-1, 1] + private float tailSwayVelocity; // Internal velocity for spring-damper + private float tailSwayPrev; // Previous frame value for interpolation + + public PachycephalosaurusEntity(EntityType pEntityType, Level pLevel) { + super(pEntityType, pLevel); + } + + @Override + protected void registerGoals() {this.goalSelector.addGoal(1, new SprintingPanicGoal(this, 1.15) { + @Override + public boolean canUse() { + return PachycephalosaurusEntity.this.isBaby() && super.canUse(); + } + }); + this.targetSelector.addGoal(1, new HurtByTargetGoal(this).setAlertOthers()); + this.goalSelector.addGoal(2, new FloatGoal(this)); + this.goalSelector.addGoal(3, new AvoidEntityGoal<>(this, VelociraptorEntity.class, (float) 20, 0.8, 0.8)); + this.goalSelector.addGoal(4, new AvoidEntityGoal<>(this, CeratosaurusEntity.class, (float) 20, 0.8, 0.8)); + this.goalSelector.addGoal(5, new AvoidEntityGoal<>(this, DilophosaurusEntity.class, (float) 20, 0.8, 0.8)); + this.goalSelector.addGoal(6, new SprintingMeleeAttackGoal(this, 1.1, false)); + this.goalSelector.addGoal(7, new BreedGoal(this, 1.0)); + this.goalSelector.addGoal(8, new FollowParentGoal(this, 1.25)); + this.goalSelector.addGoal(8, new WaterAvoidingRandomStrollGoal(this, 1.0)); + this.goalSelector.addGoal(10, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(11, new FollowMobGoal(this, 0.8, (float) 20, (float) 10)); + this.goalSelector.addGoal(12, new AvoidEntityGoal<>(this, SpinosaurusEntity.class, (float) 20, 0.8, 0.8)); + this.goalSelector.addGoal(13, new AvoidEntityGoal<>(this, TyrannosaurusRexEntity.class, (float) 20, 0.8, 0.8)); + this.goalSelector.addGoal(14, new AvoidEntityGoal<>(this, IndominusRexEntity.class, (float) 20, 0.8, 0.8)); + this.goalSelector.addGoal(15, new EatBlockGoal(this)); + this.goalSelector.addGoal(16, new RandomLookAroundGoal(this)); + + } + + public static AttributeSupplier.Builder createAttributes() { + return Animal.createLivingAttributes() + .add(Attributes.MAX_HEALTH, 30D) + .add(Attributes.MOVEMENT_SPEED, 0.3D) + .add(Attributes.ARMOR, 0D) + .add(Attributes.FOLLOW_RANGE, 32D) + .add(Attributes.KNOCKBACK_RESISTANCE, 0D) + .add(Attributes.ATTACK_KNOCKBACK, 0D) + .add(Attributes.ATTACK_DAMAGE, 10D); + } + + @Override + public boolean isFood(ItemStack pStack) { + return pStack.is(Items.KELP); + } + + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel pLevel, AgeableMob pOtherParent) { + AgeableMob child = ModEntities.PACHYCEPHALOSAURUS.get().create(pLevel); + if (child instanceof PachycephalosaurusEntity baby) { + PachycephalosaurusVariant randomVariant = Util.getRandom(PachycephalosaurusVariant.values(), this.random); + baby.setVariant(randomVariant); + } + return child; + } + + @Override + public boolean doHurtTarget(Entity target) { + boolean hit = super.doHurtTarget(target); + if (!level().isClientSide && hit && target instanceof LivingEntity) { + if (this.level() instanceof ServerLevel serverLevel) { + this.triggerAnim("attackController", "attack"); + } + } + return hit; + } + + @Override + public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { + controllers.add(new AnimationController<>(this, "Walk/Run/Idle", state -> { + if (state.isMoving()) + return state.setAndContinue(PachycephalosaurusEntity.this.isSprinting() ? RawAnimation.begin().then("anim.pachycephalosaurus.run", Animation.LoopType.LOOP) : RawAnimation.begin().then("anim.pachycephalosaurus.walk", Animation.LoopType.LOOP)); + + return state.setAndContinue(RawAnimation.begin().then("anim.pachycephalosaurus.idle", Animation.LoopType.LOOP)); + })); + + controllers.add(new AnimationController<>(this, "attackController", state -> PlayState.STOP) + .triggerableAnim("attack", RawAnimation.begin().then("anim.pachycephalosaurus.attack", Animation.LoopType.PLAY_ONCE))); + + controllers.add(new AnimationController<>(this, "mouthController", state -> PlayState.STOP) + .triggerableAnim("mouth", RawAnimation.begin().then("anim.pachycephalosaurus.mouth", Animation.LoopType.PLAY_ONCE))); + } + + private float getSignedTurnDelta() { + // Only consider the body (torso) rotation so head look does not affect tail sway + return Mth.wrapDegrees(this.yBodyRot - this.yBodyRotO); + } + + private int mouthAnimCooldown = 0; + + @Override + public void tick() { + super.tick(); + if (!level().isClientSide) { + this.entityData.set(DATA_SYNCED_AGE, this.getAge()); + var maxHealthAttr = getAttribute(Attributes.MAX_HEALTH); + if (maxHealthAttr != null) { + double baseAdult = DefaultAttributes.getSupplier((EntityType) this.getType()).getValue(Attributes.MAX_HEALTH); + double desired = this.isBaby() ? baseAdult * 0.10D : baseAdult; + if (maxHealthAttr.getBaseValue() != desired) { + double oldMax = maxHealthAttr.getBaseValue(); + double healthRatio = this.getHealth() / (float) oldMax; + maxHealthAttr.setBaseValue(desired); + this.setHealth((float) (desired * Mth.clamp(healthRatio, 0.0F, 1.0F))); + } + } + } + + if (!level().isClientSide) { + if (mouthAnimCooldown > 0) { + mouthAnimCooldown--; + } else { + this.triggerAnim("mouthController", "mouth"); + this.playSound(ModSounds.PACHYCEPHALOSAURUS_CALL.get(), 1.0F, 1.0F); + // 30s–60s in ticks + mouthAnimCooldown = this.random.nextInt(1200 - 600 + 1) + 600; + } + } + + if (level().isClientSide) { + // Capture previous for smooth interpolation between ticks + this.tailSwayPrev = this.tailSwayOffset; + updateProceduralTailSway(); + } + } + + private void updateProceduralTailSway() { + // Turn input derived from rotation deltas; works even when standing still and turning + float turnDegrees = getSignedTurnDelta(); + + // Deadzone to ignore tiny jitter so the tail can return to center cleanly + float deadzoneDeg = 0.6f; // smaller deadzone for more responsiveness + float turnInput = 0.0f; + if (Math.abs(turnDegrees) >= deadzoneDeg) { + // Higher sensitivity so small in-place turns still affect the model + turnInput = Mth.clamp(turnDegrees / 15.0f, -1.0f, 1.0f); + } + + // Target offset: keep intuitive sign (positive input -> positive sway) + float target = turnInput; + + // One-pole low-pass (no bounce). Larger alpha => snappier and less "stiff". + float alpha = 0.24f; // try 0.20–0.30 to taste + + tailSwayOffset += (target - tailSwayOffset) * alpha; + + // Snap tiny residuals to zero so it visibly settles + if (Math.abs(tailSwayOffset) < 0.003f) { + tailSwayOffset = 0.0f; + } + + // No oscillation velocity retained + tailSwayVelocity = 0.0f; + + tailSwayOffset = Mth.clamp(tailSwayOffset, -1.5f, 1.5f); + } + + // Expose to the model for bone rotation + public float getTailSwayOffset() { + return tailSwayOffset; + } + + // Interpolated sway for smooth rendering between ticks + public float getTailSwayOffset(float partialTick) { + return Mth.lerp(Mth.clamp(partialTick, 0.0f, 1.0f), tailSwayPrev, tailSwayOffset); + } + + /*? if <=1.20.1 {*/ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); + this.entityData.define(VARIANT, 0); + this.entityData.define(DATA_SYNCED_AGE, 0); + } + /*?} else {*/ + /*@Override + protected void defineSynchedData(SynchedEntityData.Builder pBuilder) { + super.defineSynchedData(pBuilder); + pBuilder.define(VARIANT, 0); + pBuilder.define(DATA_SYNCED_AGE, 0); + } + *//*?}*/ + + public int getSyncedAge() { + return this.entityData.get(DATA_SYNCED_AGE); + } + public int getTypeVariant() { + return this.entityData.get(VARIANT); + } + + public PachycephalosaurusVariant getVariant() { + return PachycephalosaurusVariant.byId(this.getTypeVariant() & 255); + } + + private void setVariant(PachycephalosaurusVariant variant) { + this.entityData.set(VARIANT, variant.getId() & 255); + } + + @Override + public boolean canMate(Animal other) { + if (!super.canMate(other)) return false; + if (!(other instanceof PachycephalosaurusEntity that)) return false; + return this.getVariant() != that.getVariant(); + } + @Override + public void addAdditionalSaveData(CompoundTag pCompound) { + super.addAdditionalSaveData(pCompound); + pCompound.putInt("Variant", this.getTypeVariant()); + } + + @Override + public void readAdditionalSaveData(CompoundTag pCompound) { + super.readAdditionalSaveData(pCompound); + this.entityData.set(VARIANT, pCompound.getInt("Variant")); + } + + /*? if <=1.20.1 {*/ + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor pLevel, DifficultyInstance pDifficulty, MobSpawnType pReason, @Nullable SpawnGroupData pSpawnData, @Nullable CompoundTag pDataTag) { + PachycephalosaurusVariant variant = Util.getRandom(PachycephalosaurusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(pLevel, pDifficulty, pReason, pSpawnData, pDataTag); + } + /*?} else {*/ + /*@Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, MobSpawnType spawnType, @Nullable SpawnGroupData spawnGroupData) { + PachycephalosaurusVariant variant = Util.getRandom(PachycephalosaurusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(level, difficulty, spawnType, spawnGroupData); + } + *//*?}*/ + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } + + @Override + protected @Nullable SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.PACHYCEPHALOSAURUS_HURT.get(); + } + + @Override + protected @Nullable SoundEvent getDeathSound() { + return ModSounds.PACHYCEPHALOSAURUS_DEATH.get(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/custom/ParasaurolophusEntity.java b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/ParasaurolophusEntity.java new file mode 100644 index 0000000..5666fc8 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/ParasaurolophusEntity.java @@ -0,0 +1,297 @@ +package net.cmr.jurassicrevived.entity.custom; + +import net.cmr.jurassicrevived.entity.ModEntities; +import net.cmr.jurassicrevived.entity.ai.SprintingPanicGoal; +import net.cmr.jurassicrevived.entity.client.AlbertosaurusVariant; +import net.cmr.jurassicrevived.entity.client.ParasaurolophusVariant; +import net.cmr.jurassicrevived.sound.ModSounds; +import net.minecraft.Util; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.util.Mth; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.*; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.attributes.DefaultAttributes; +import net.minecraft.world.entity.ai.goal.*; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerLevelAccessor; +import org.jetbrains.annotations.Nullable; +import software.bernie.geckolib.animatable.GeoEntity; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.core.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.core.animation.AnimatableManager; +import software.bernie.geckolib.core.animation.Animation; +import software.bernie.geckolib.core.animation.AnimationController; +import software.bernie.geckolib.core.animation.RawAnimation; +import software.bernie.geckolib.core.object.PlayState; +/*?} else {*/ +/*import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.animation.*; +*//*?}*/ + +public class ParasaurolophusEntity extends Animal implements GeoEntity { + private AnimatableInstanceCache cache = new SingletonAnimatableInstanceCache(this); + + private static final EntityDataAccessor VARIANT = + SynchedEntityData.defineId(ParasaurolophusEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SYNCED_AGE = + SynchedEntityData.defineId(ParasaurolophusEntity.class, EntityDataSerializers.INT); + + // Procedural tail sway state (client-side use for rendering) + private float tailSwayOffset; // Smoothed offset in range roughly [-1, 1] + private float tailSwayVelocity; // Internal velocity for spring-damper + private float tailSwayPrev; // Previous frame value for interpolation + + public ParasaurolophusEntity(EntityType pEntityType, Level pLevel) { + super(pEntityType, pLevel); + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new SprintingPanicGoal(this, 1.15)); + this.goalSelector.addGoal(2, new FloatGoal(this)); + this.goalSelector.addGoal(3, new BreedGoal(this, 1.0)); + this.goalSelector.addGoal(4, new FollowParentGoal(this, 1.25)); + this.goalSelector.addGoal(5, new WaterAvoidingRandomStrollGoal(this, 1.0)); + this.goalSelector.addGoal(6, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(7, new FollowMobGoal(this, 1, (float) 20, (float) 10)); + this.goalSelector.addGoal(8, new AvoidEntityGoal<>(this, DilophosaurusEntity.class, (float) 20, 1.2, 1.2)); + this.goalSelector.addGoal(9, new AvoidEntityGoal<>(this, VelociraptorEntity.class, (float) 20, 1.2, 1.2)); + this.goalSelector.addGoal(10, new AvoidEntityGoal<>(this, CeratosaurusEntity.class, (float) 20, 1.2, 1.2)); + this.goalSelector.addGoal(11, new AvoidEntityGoal<>(this, SpinosaurusEntity.class, (float) 20, 1.2, 1.2)); + this.goalSelector.addGoal(12, new AvoidEntityGoal<>(this, TyrannosaurusRexEntity.class, (float) 20, 1.2, 1.2)); + this.goalSelector.addGoal(13, new RandomLookAroundGoal(this)); + + } + + public static AttributeSupplier.Builder createAttributes() { + return Animal.createLivingAttributes() + .add(Attributes.MAX_HEALTH, 55D) + .add(Attributes.MOVEMENT_SPEED, 0.3D) + .add(Attributes.ARMOR, 0D) + .add(Attributes.FOLLOW_RANGE, 32D) + .add(Attributes.KNOCKBACK_RESISTANCE, 0D) + .add(Attributes.ATTACK_KNOCKBACK, 0D) + .add(Attributes.ATTACK_DAMAGE, 12D); + } + + @Override + public boolean isFood(ItemStack pStack) { + return pStack.is(Items.KELP); + } + + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel pLevel, AgeableMob pOtherParent) { + AgeableMob child = ModEntities.PARASAUROLOPHUS.get().create(pLevel); + if (child instanceof ParasaurolophusEntity baby) { + ParasaurolophusVariant randomVariant = Util.getRandom(ParasaurolophusVariant.values(), this.random); + baby.setVariant(randomVariant); + } + return child; + } + + @Override + public boolean doHurtTarget(Entity target) { + boolean hit = super.doHurtTarget(target); + if (!level().isClientSide && hit && target instanceof LivingEntity) { + if (this.level() instanceof ServerLevel serverLevel) { + this.triggerAnim("attackController", "attack"); + } + } + return hit; + } + + @Override + public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { + controllers.add(new AnimationController<>(this, "Walk/Run/Idle", state -> { + if (state.isMoving()) + return state.setAndContinue(ParasaurolophusEntity.this.isSprinting() ? RawAnimation.begin().then("anim.parasaurolophus.run", Animation.LoopType.LOOP) : RawAnimation.begin().then("anim.parasaurolophus.walk", Animation.LoopType.LOOP)); + + return state.setAndContinue(RawAnimation.begin().then("anim.parasaurolophus.idle", Animation.LoopType.LOOP)); + })); + + controllers.add(new AnimationController<>(this, "attackController", state -> PlayState.STOP) + .triggerableAnim("attack", RawAnimation.begin().then("anim.parasaurolophus.attack", Animation.LoopType.PLAY_ONCE))); + + controllers.add(new AnimationController<>(this, "mouthController", state -> PlayState.STOP) + .triggerableAnim("mouth", RawAnimation.begin().then("anim.parasaurolophus.mouth", Animation.LoopType.PLAY_ONCE))); + } + + private float getSignedTurnDelta() { + // Only consider the body (torso) rotation so head look does not affect tail sway + return Mth.wrapDegrees(this.yBodyRot - this.yBodyRotO); + } + + private int mouthAnimCooldown = 0; + + @Override + public void tick() { + super.tick(); + if (!level().isClientSide) { + this.entityData.set(DATA_SYNCED_AGE, this.getAge()); + var maxHealthAttr = getAttribute(Attributes.MAX_HEALTH); + if (maxHealthAttr != null) { + double baseAdult = DefaultAttributes.getSupplier((EntityType) this.getType()).getValue(Attributes.MAX_HEALTH); + double desired = this.isBaby() ? baseAdult * 0.10D : baseAdult; + if (maxHealthAttr.getBaseValue() != desired) { + double oldMax = maxHealthAttr.getBaseValue(); + double healthRatio = this.getHealth() / (float) oldMax; + maxHealthAttr.setBaseValue(desired); + this.setHealth((float) (desired * Mth.clamp(healthRatio, 0.0F, 1.0F))); + } + } + } + + if (!level().isClientSide) { + if (mouthAnimCooldown > 0) { + mouthAnimCooldown--; + } else { + this.triggerAnim("mouthController", "mouth"); + this.playSound(ModSounds.PARASAUROLOPHUS_CALL.get(), 1.0F, 1.0F); + // 30s–60s in ticks + mouthAnimCooldown = this.random.nextInt(1200 - 600 + 1) + 600; + } + } + + if (level().isClientSide) { + // Capture previous for smooth interpolation between ticks + this.tailSwayPrev = this.tailSwayOffset; + updateProceduralTailSway(); + } + } + + private void updateProceduralTailSway() { + // Turn input derived from rotation deltas; works even when standing still and turning + float turnDegrees = getSignedTurnDelta(); + + // Deadzone to ignore tiny jitter so the tail can return to center cleanly + float deadzoneDeg = 0.6f; // smaller deadzone for more responsiveness + float turnInput = 0.0f; + if (Math.abs(turnDegrees) >= deadzoneDeg) { + // Higher sensitivity so small in-place turns still affect the model + turnInput = Mth.clamp(turnDegrees / 15.0f, -1.0f, 1.0f); + } + + // Target offset: keep intuitive sign (positive input -> positive sway) + float target = turnInput; + + // One-pole low-pass (no bounce). Larger alpha => snappier and less "stiff". + float alpha = 0.24f; // try 0.20–0.30 to taste + + tailSwayOffset += (target - tailSwayOffset) * alpha; + + // Snap tiny residuals to zero so it visibly settles + if (Math.abs(tailSwayOffset) < 0.003f) { + tailSwayOffset = 0.0f; + } + + // No oscillation velocity retained + tailSwayVelocity = 0.0f; + + tailSwayOffset = Mth.clamp(tailSwayOffset, -1.5f, 1.5f); + } + + // Expose to the model for bone rotation + public float getTailSwayOffset() { + return tailSwayOffset; + } + + // Interpolated sway for smooth rendering between ticks + public float getTailSwayOffset(float partialTick) { + return Mth.lerp(Mth.clamp(partialTick, 0.0f, 1.0f), tailSwayPrev, tailSwayOffset); + } + + /*? if <=1.20.1 {*/ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); + this.entityData.define(VARIANT, 0); + this.entityData.define(DATA_SYNCED_AGE, 0); + } + /*?} else {*/ + /*@Override + protected void defineSynchedData(SynchedEntityData.Builder pBuilder) { + super.defineSynchedData(pBuilder); + pBuilder.define(VARIANT, 0); + pBuilder.define(DATA_SYNCED_AGE, 0); + } + *//*?}*/ + + public int getSyncedAge() { + return this.entityData.get(DATA_SYNCED_AGE); + } + public int getTypeVariant() { + return this.entityData.get(VARIANT); + } + + public ParasaurolophusVariant getVariant() { + return ParasaurolophusVariant.byId(this.getTypeVariant() & 255); + } + + private void setVariant(ParasaurolophusVariant variant) { + this.entityData.set(VARIANT, variant.getId() & 255); + } + + @Override + public boolean canMate(Animal other) { + if (!super.canMate(other)) return false; + if (!(other instanceof ParasaurolophusEntity that)) return false; + return this.getVariant() != that.getVariant(); + } + @Override + public void addAdditionalSaveData(CompoundTag pCompound) { + super.addAdditionalSaveData(pCompound); + pCompound.putInt("Variant", this.getTypeVariant()); + } + + @Override + public void readAdditionalSaveData(CompoundTag pCompound) { + super.readAdditionalSaveData(pCompound); + this.entityData.set(VARIANT, pCompound.getInt("Variant")); + } + + /*? if <=1.20.1 {*/ + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor pLevel, DifficultyInstance pDifficulty, MobSpawnType pReason, @Nullable SpawnGroupData pSpawnData, @Nullable CompoundTag pDataTag) { + ParasaurolophusVariant variant = Util.getRandom(ParasaurolophusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(pLevel, pDifficulty, pReason, pSpawnData, pDataTag); + } + /*?} else {*/ + /*@Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, MobSpawnType spawnType, @Nullable SpawnGroupData spawnGroupData) { + ParasaurolophusVariant variant = Util.getRandom(ParasaurolophusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(level, difficulty, spawnType, spawnGroupData); + } + *//*?}*/ + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } + + @Override + protected @Nullable SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.PARASAUROLOPHUS_HURT.get(); + } + + @Override + protected @Nullable SoundEvent getDeathSound() { + return ModSounds.PARASAUROLOPHUS_DEATH.get(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/custom/ProceratosaurusEntity.java b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/ProceratosaurusEntity.java new file mode 100644 index 0000000..09d0325 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/ProceratosaurusEntity.java @@ -0,0 +1,313 @@ +package net.cmr.jurassicrevived.entity.custom; + +import net.cmr.jurassicrevived.entity.ModEntities; +import net.cmr.jurassicrevived.entity.ai.SprintingMeleeAttackGoal; +import net.cmr.jurassicrevived.entity.ai.SprintingPanicGoal; +import net.cmr.jurassicrevived.entity.client.AlbertosaurusVariant; +import net.cmr.jurassicrevived.entity.client.ProceratosaurusVariant; +import net.cmr.jurassicrevived.sound.ModSounds; +import net.minecraft.Util; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.util.Mth; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.*; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.attributes.DefaultAttributes; +import net.minecraft.world.entity.ai.goal.*; +import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal; +import net.minecraft.world.entity.ai.goal.target.NearestAttackableTargetGoal; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerLevelAccessor; +import org.jetbrains.annotations.Nullable; +import software.bernie.geckolib.animatable.GeoEntity; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.core.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.core.animation.AnimatableManager; +import software.bernie.geckolib.core.animation.Animation; +import software.bernie.geckolib.core.animation.AnimationController; +import software.bernie.geckolib.core.animation.RawAnimation; +import software.bernie.geckolib.core.object.PlayState; +/*?} else {*/ +/*import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.animation.*; +*//*?}*/ + +public class ProceratosaurusEntity extends Animal implements GeoEntity { + private AnimatableInstanceCache cache = new SingletonAnimatableInstanceCache(this); + + private static final EntityDataAccessor VARIANT = + SynchedEntityData.defineId(ProceratosaurusEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SYNCED_AGE = + SynchedEntityData.defineId(ProceratosaurusEntity.class, EntityDataSerializers.INT); + + // Procedural tail sway state (client-side use for rendering) + private float tailSwayOffset; // Smoothed offset in range roughly [-1, 1] + private float tailSwayVelocity; // Internal velocity for spring-damper + private float tailSwayPrev; // Previous frame value for interpolation + + public ProceratosaurusEntity(EntityType pEntityType, Level pLevel) { + super(pEntityType, pLevel); + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new SprintingPanicGoal(this, 1.15) { + @Override + public boolean canUse() { + return ProceratosaurusEntity.this.isBaby() && super.canUse(); + } + }); + this.targetSelector.addGoal(1, new HurtByTargetGoal(this).setAlertOthers()); + this.goalSelector.addGoal(2, new FloatGoal(this)); + this.goalSelector.addGoal(3, new AvoidEntityGoal<>(this, SpinosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(4, new AvoidEntityGoal<>(this, TyrannosaurusRexEntity.class, (float) 20, 1.2, 1.2)); + this.goalSelector.addGoal(5, new AvoidEntityGoal<>(this, VelociraptorEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(6, new AvoidEntityGoal<>(this, ParasaurolophusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(7, new AvoidEntityGoal<>(this, CeratosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(8, new AvoidEntityGoal<>(this, TriceratopsEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(9, new AvoidEntityGoal<>(this, BrachiosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(10, new SprintingMeleeAttackGoal(this, 1.1, false)); + this.goalSelector.addGoal(11, new BreedGoal(this, 1.0)); + this.goalSelector.addGoal(12, new FollowParentGoal(this, 1.25)); + this.goalSelector.addGoal(13, new WaterAvoidingRandomStrollGoal(this, 1.0)); + this.goalSelector.addGoal(14, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(15, new FollowMobGoal(this, 1, (float) 20, (float) 10)); + this.targetSelector.addGoal(16, new NearestAttackableTargetGoal<>(this, Animal.class, 10, false, false, + target -> target.getType() != this.getType())); + this.targetSelector.addGoal(17, new NearestAttackableTargetGoal(this, GallimimusEntity.class, false, false)); + this.targetSelector.addGoal(18, new NearestAttackableTargetGoal(this, Player.class, false, false)); + this.targetSelector.addGoal(19, new NearestAttackableTargetGoal(this, CompsognathusEntity.class, false, false)); + this.goalSelector.addGoal(20, new RandomLookAroundGoal(this)); + } + + public static AttributeSupplier.Builder createAttributes() { + return Animal.createLivingAttributes() + .add(Attributes.MAX_HEALTH, 14D) + .add(Attributes.MOVEMENT_SPEED, 0.3D) + .add(Attributes.ARMOR, 0D) + .add(Attributes.FOLLOW_RANGE, 32D) + .add(Attributes.KNOCKBACK_RESISTANCE, 0.3D) + .add(Attributes.ATTACK_KNOCKBACK, 0D) + .add(Attributes.ATTACK_DAMAGE, 4D); + } + + @Override + public boolean isFood(ItemStack pStack) { + return pStack.is(Items.BEEF); + } + + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel pLevel, AgeableMob pOtherParent) { + AgeableMob child = ModEntities.PROCERATOSAURUS.get().create(pLevel); + if (child instanceof ProceratosaurusEntity baby) { + ProceratosaurusVariant randomVariant = Util.getRandom(ProceratosaurusVariant.values(), this.random); + baby.setVariant(randomVariant); + } + return child; + } + + @Override + public boolean doHurtTarget(Entity target) { + boolean hit = super.doHurtTarget(target); + if (!level().isClientSide && hit && target instanceof LivingEntity) { + if (this.level() instanceof ServerLevel serverLevel) { + this.triggerAnim("attackController", "attack"); + } + } + return hit; + } + + @Override + public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { + controllers.add(new AnimationController<>(this, "Walk/Run/Idle", state -> { + if (state.isMoving()) + return state.setAndContinue(ProceratosaurusEntity.this.isSprinting() ? RawAnimation.begin().then("anim.proceratosaurus.run", Animation.LoopType.LOOP) : RawAnimation.begin().then("anim.proceratosaurus.walk", Animation.LoopType.LOOP)); + + return state.setAndContinue(RawAnimation.begin().then("anim.proceratosaurus.idle", Animation.LoopType.LOOP)); + })); + + controllers.add(new AnimationController<>(this, "attackController", state -> PlayState.STOP) + .triggerableAnim("attack", RawAnimation.begin().then("anim.proceratosaurus.attack", Animation.LoopType.PLAY_ONCE))); + + controllers.add(new AnimationController<>(this, "mouthController", state -> PlayState.STOP) + .triggerableAnim("mouth", RawAnimation.begin().then("anim.proceratosaurus.mouth", Animation.LoopType.PLAY_ONCE))); + } + + private float getSignedTurnDelta() { + // Only consider the body (torso) rotation so head look does not affect tail sway + return Mth.wrapDegrees(this.yBodyRot - this.yBodyRotO); + } + + private int mouthAnimCooldown = 0; + + @Override + public void tick() { + super.tick(); + if (!level().isClientSide) { + this.entityData.set(DATA_SYNCED_AGE, this.getAge()); + var maxHealthAttr = getAttribute(Attributes.MAX_HEALTH); + if (maxHealthAttr != null) { + double baseAdult = DefaultAttributes.getSupplier((EntityType) this.getType()).getValue(Attributes.MAX_HEALTH); + double desired = this.isBaby() ? baseAdult * 0.10D : baseAdult; + if (maxHealthAttr.getBaseValue() != desired) { + double oldMax = maxHealthAttr.getBaseValue(); + double healthRatio = this.getHealth() / (float) oldMax; + maxHealthAttr.setBaseValue(desired); + this.setHealth((float) (desired * Mth.clamp(healthRatio, 0.0F, 1.0F))); + } + } + } + + if (!level().isClientSide) { + if (mouthAnimCooldown > 0) { + mouthAnimCooldown--; + } else { + this.triggerAnim("mouthController", "mouth"); + this.playSound(ModSounds.PROCERATOSAURUS_CALL.get(), 1.0F, 1.0F); + // 30s–60s in ticks + mouthAnimCooldown = this.random.nextInt(1200 - 600 + 1) + 600; + } + } + + if (level().isClientSide) { + // Capture previous for smooth interpolation between ticks + this.tailSwayPrev = this.tailSwayOffset; + updateProceduralTailSway(); + } + } + + private void updateProceduralTailSway() { + // Turn input derived from rotation deltas; works even when standing still and turning + float turnDegrees = getSignedTurnDelta(); + + // Deadzone to ignore tiny jitter so the tail can return to center cleanly + float deadzoneDeg = 0.6f; // smaller deadzone for more responsiveness + float turnInput = 0.0f; + if (Math.abs(turnDegrees) >= deadzoneDeg) { + // Higher sensitivity so small in-place turns still affect the model + turnInput = Mth.clamp(turnDegrees / 15.0f, -1.0f, 1.0f); + } + + // Target offset: keep intuitive sign (positive input -> positive sway) + float target = turnInput; + + // One-pole low-pass (no bounce). Larger alpha => snappier and less "stiff". + float alpha = 0.24f; // try 0.20–0.30 to taste + + tailSwayOffset += (target - tailSwayOffset) * alpha; + + // Snap tiny residuals to zero so it visibly settles + if (Math.abs(tailSwayOffset) < 0.003f) { + tailSwayOffset = 0.0f; + } + + // No oscillation velocity retained + tailSwayVelocity = 0.0f; + + tailSwayOffset = Mth.clamp(tailSwayOffset, -1.5f, 1.5f); + } + + // Expose to the model for bone rotation + public float getTailSwayOffset() { + return tailSwayOffset; + } + + // Interpolated sway for smooth rendering between ticks + public float getTailSwayOffset(float partialTick) { + return Mth.lerp(Mth.clamp(partialTick, 0.0f, 1.0f), tailSwayPrev, tailSwayOffset); + } + + /*? if <=1.20.1 {*/ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); + this.entityData.define(VARIANT, 0); + this.entityData.define(DATA_SYNCED_AGE, 0); + } + /*?} else {*/ + /*@Override + protected void defineSynchedData(SynchedEntityData.Builder pBuilder) { + super.defineSynchedData(pBuilder); + pBuilder.define(VARIANT, 0); + pBuilder.define(DATA_SYNCED_AGE, 0); + } + *//*?}*/ + + public int getSyncedAge() { + return this.entityData.get(DATA_SYNCED_AGE); + } + public int getTypeVariant() { + return this.entityData.get(VARIANT); + } + + public ProceratosaurusVariant getVariant() { + return ProceratosaurusVariant.byId(this.getTypeVariant() & 255); + } + + private void setVariant(ProceratosaurusVariant variant) { + this.entityData.set(VARIANT, variant.getId() & 255); + } + + @Override + public boolean canMate(Animal other) { + if (!super.canMate(other)) return false; + if (!(other instanceof ProceratosaurusEntity that)) return false; + return this.getVariant() != that.getVariant(); + } + @Override + public void addAdditionalSaveData(CompoundTag pCompound) { + super.addAdditionalSaveData(pCompound); + pCompound.putInt("Variant", this.getTypeVariant()); + } + + @Override + public void readAdditionalSaveData(CompoundTag pCompound) { + super.readAdditionalSaveData(pCompound); + this.entityData.set(VARIANT, pCompound.getInt("Variant")); + } + + /*? if <=1.20.1 {*/ + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor pLevel, DifficultyInstance pDifficulty, MobSpawnType pReason, @Nullable SpawnGroupData pSpawnData, @Nullable CompoundTag pDataTag) { + ProceratosaurusVariant variant = Util.getRandom(ProceratosaurusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(pLevel, pDifficulty, pReason, pSpawnData, pDataTag); + } + /*?} else {*/ + /*@Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, MobSpawnType spawnType, @Nullable SpawnGroupData spawnGroupData) { + ProceratosaurusVariant variant = Util.getRandom(ProceratosaurusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(level, difficulty, spawnType, spawnGroupData); + } + *//*?}*/ + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } + + @Override + protected @Nullable SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.PROCERATOSAURUS_HURT.get(); + } + + @Override + protected @Nullable SoundEvent getDeathSound() { + return ModSounds.PROCERATOSAURUS_DEATH.get(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/custom/ProcompsognathusEntity.java b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/ProcompsognathusEntity.java new file mode 100644 index 0000000..6040af5 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/ProcompsognathusEntity.java @@ -0,0 +1,304 @@ +package net.cmr.jurassicrevived.entity.custom; + +import net.cmr.jurassicrevived.entity.ModEntities; +import net.cmr.jurassicrevived.entity.ai.SprintingMeleeAttackGoal; +import net.cmr.jurassicrevived.entity.ai.SprintingPanicGoal; +import net.cmr.jurassicrevived.entity.client.AlbertosaurusVariant; +import net.cmr.jurassicrevived.entity.client.ProcompsognathusVariant; +import net.cmr.jurassicrevived.sound.ModSounds; +import net.minecraft.Util; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.util.Mth; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.*; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.attributes.DefaultAttributes; +import net.minecraft.world.entity.ai.goal.*; +import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal; +import net.minecraft.world.entity.ai.goal.target.NearestAttackableTargetGoal; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerLevelAccessor; +import org.jetbrains.annotations.Nullable; +import software.bernie.geckolib.animatable.GeoEntity; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.core.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.core.animation.AnimatableManager; +import software.bernie.geckolib.core.animation.Animation; +import software.bernie.geckolib.core.animation.AnimationController; +import software.bernie.geckolib.core.animation.RawAnimation; +import software.bernie.geckolib.core.object.PlayState; +/*?} else {*/ +/*import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.animation.*; +*//*?}*/ + +public class ProcompsognathusEntity extends Animal implements GeoEntity { + private AnimatableInstanceCache cache = new SingletonAnimatableInstanceCache(this); + + private static final EntityDataAccessor VARIANT = + SynchedEntityData.defineId(ProcompsognathusEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SYNCED_AGE = + SynchedEntityData.defineId(ProcompsognathusEntity.class, EntityDataSerializers.INT); + + // Procedural tail sway state (client-side use for rendering) + private float tailSwayOffset; // Smoothed offset in range roughly [-1, 1] + private float tailSwayVelocity; // Internal velocity for spring-damper + private float tailSwayPrev; // Previous frame value for interpolation + + public ProcompsognathusEntity(EntityType pEntityType, Level pLevel) { + super(pEntityType, pLevel); + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new SprintingPanicGoal(this, 1.15) { + @Override + public boolean canUse() { + return ProcompsognathusEntity.this.isBaby() && super.canUse(); + } + }); + this.targetSelector.addGoal(1, new HurtByTargetGoal(this).setAlertOthers()); + this.goalSelector.addGoal(2, new FloatGoal(this)); + this.goalSelector.addGoal(3, new SprintingMeleeAttackGoal(this, 1.1, false)); + this.targetSelector.addGoal(11, new NearestAttackableTargetGoal<>(this, Animal.class, 10, false, false, + target -> target.getType() != this.getType())); + this.goalSelector.addGoal(5, new BreedGoal(this, 1.0)); + this.goalSelector.addGoal(6, new FollowParentGoal(this, 1.25)); + this.goalSelector.addGoal(7, new WaterAvoidingRandomStrollGoal(this, 1.0)); + this.goalSelector.addGoal(8, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(9, new FollowMobGoal(this, 1, (float) 20, (float) 10)); + this.goalSelector.addGoal(10, new AvoidEntityGoal<>(this, LivingEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(11, new RandomLookAroundGoal(this)); + } + + public static AttributeSupplier.Builder createAttributes() { + return Animal.createLivingAttributes() + .add(Attributes.MAX_HEALTH, 8D) + .add(Attributes.MOVEMENT_SPEED, 0.3D) + .add(Attributes.ARMOR, 0D) + .add(Attributes.FOLLOW_RANGE, 32D) + .add(Attributes.ATTACK_DAMAGE, 2D) + .add(Attributes.KNOCKBACK_RESISTANCE, 0.3D) + .add(Attributes.ATTACK_KNOCKBACK, 0D); + } + + @Override + public boolean isFood(ItemStack pStack) { + return pStack.is(Items.BEEF); + } + + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel pLevel, AgeableMob pOtherParent) { + AgeableMob child = ModEntities.PROCOMPSOGNATHUS.get().create(pLevel); + if (child instanceof ProcompsognathusEntity baby) { + ProcompsognathusVariant randomVariant = Util.getRandom(ProcompsognathusVariant.values(), this.random); + baby.setVariant(randomVariant); + } + return child; + } + + @Override + public boolean doHurtTarget(Entity target) { + boolean hit = super.doHurtTarget(target); + if (!level().isClientSide && hit && target instanceof LivingEntity) { + if (this.level() instanceof ServerLevel serverLevel) { + this.triggerAnim("attackController", "attack"); + } + } + return hit; + } + + @Override + public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { + controllers.add(new AnimationController<>(this, "Walk/Run/Idle", state -> { + if (state.isMoving()) + return state.setAndContinue(ProcompsognathusEntity.this.isSprinting() ? RawAnimation.begin().then("anim.procompsognathus.run", Animation.LoopType.LOOP) : RawAnimation.begin().then("anim.procompsognathus.walk", Animation.LoopType.LOOP)); + + return state.setAndContinue(RawAnimation.begin().then("anim.procompsognathus.idle", Animation.LoopType.LOOP)); + })); + + controllers.add(new AnimationController<>(this, "attackController", state -> PlayState.STOP) + .triggerableAnim("attack", RawAnimation.begin().then("anim.procompsognathus.attack", Animation.LoopType.PLAY_ONCE))); + + controllers.add(new AnimationController<>(this, "mouthController", state -> PlayState.STOP) + .triggerableAnim("mouth", RawAnimation.begin().then("anim.procompsognathus.mouth", Animation.LoopType.PLAY_ONCE))); + } + + private float getSignedTurnDelta() { + // Only consider the body (torso) rotation so head look does not affect tail sway + return Mth.wrapDegrees(this.yBodyRot - this.yBodyRotO); + } + + private int mouthAnimCooldown = 0; + + @Override + public void tick() { + super.tick(); + if (!level().isClientSide) { + this.entityData.set(DATA_SYNCED_AGE, this.getAge()); + var maxHealthAttr = getAttribute(Attributes.MAX_HEALTH); + if (maxHealthAttr != null) { + double baseAdult = DefaultAttributes.getSupplier((EntityType) this.getType()).getValue(Attributes.MAX_HEALTH); + double desired = this.isBaby() ? baseAdult * 0.10D : baseAdult; + if (maxHealthAttr.getBaseValue() != desired) { + double oldMax = maxHealthAttr.getBaseValue(); + double healthRatio = this.getHealth() / (float) oldMax; + maxHealthAttr.setBaseValue(desired); + this.setHealth((float) (desired * Mth.clamp(healthRatio, 0.0F, 1.0F))); + } + } + } + + if (!level().isClientSide) { + if (mouthAnimCooldown > 0) { + mouthAnimCooldown--; + } else { + this.triggerAnim("mouthController", "mouth"); + this.playSound(ModSounds.PROCOMPSOGNATHUS_CALL.get(), 1.0F, 1.0F); + // 30s–60s in ticks + mouthAnimCooldown = this.random.nextInt(1200 - 600 + 1) + 600; + } + } + + if (level().isClientSide) { + // Capture previous for smooth interpolation between ticks + this.tailSwayPrev = this.tailSwayOffset; + updateProceduralTailSway(); + } + } + + private void updateProceduralTailSway() { + // Turn input derived from rotation deltas; works even when standing still and turning + float turnDegrees = getSignedTurnDelta(); + + // Deadzone to ignore tiny jitter so the tail can return to center cleanly + float deadzoneDeg = 0.6f; // smaller deadzone for more responsiveness + float turnInput = 0.0f; + if (Math.abs(turnDegrees) >= deadzoneDeg) { + // Higher sensitivity so small in-place turns still affect the model + turnInput = Mth.clamp(turnDegrees / 15.0f, -1.0f, 1.0f); + } + + // Target offset: keep intuitive sign (positive input -> positive sway) + float target = turnInput; + + // One-pole low-pass (no bounce). Larger alpha => snappier and less "stiff". + float alpha = 0.24f; // try 0.20–0.30 to taste + + tailSwayOffset += (target - tailSwayOffset) * alpha; + + // Snap tiny residuals to zero so it visibly settles + if (Math.abs(tailSwayOffset) < 0.003f) { + tailSwayOffset = 0.0f; + } + + // No oscillation velocity retained + tailSwayVelocity = 0.0f; + + tailSwayOffset = Mth.clamp(tailSwayOffset, -1.5f, 1.5f); + } + + // Expose to the model for bone rotation + public float getTailSwayOffset() { + return tailSwayOffset; + } + + // Interpolated sway for smooth rendering between ticks + public float getTailSwayOffset(float partialTick) { + return Mth.lerp(Mth.clamp(partialTick, 0.0f, 1.0f), tailSwayPrev, tailSwayOffset); + } + + /*? if <=1.20.1 {*/ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); + this.entityData.define(VARIANT, 0); + this.entityData.define(DATA_SYNCED_AGE, 0); + } + /*?} else {*/ + /*@Override + protected void defineSynchedData(SynchedEntityData.Builder pBuilder) { + super.defineSynchedData(pBuilder); + pBuilder.define(VARIANT, 0); + pBuilder.define(DATA_SYNCED_AGE, 0); + } + *//*?}*/ + + public int getSyncedAge() { + return this.entityData.get(DATA_SYNCED_AGE); + } + public int getTypeVariant() { + return this.entityData.get(VARIANT); + } + + public ProcompsognathusVariant getVariant() { + return ProcompsognathusVariant.byId(this.getTypeVariant() & 255); + } + + private void setVariant(ProcompsognathusVariant variant) { + this.entityData.set(VARIANT, variant.getId() & 255); + } + + @Override + public boolean canMate(Animal other) { + if (!super.canMate(other)) return false; + if (!(other instanceof ProcompsognathusEntity that)) return false; + return this.getVariant() != that.getVariant(); + } + @Override + public void addAdditionalSaveData(CompoundTag pCompound) { + super.addAdditionalSaveData(pCompound); + pCompound.putInt("Variant", this.getTypeVariant()); + } + + @Override + public void readAdditionalSaveData(CompoundTag pCompound) { + super.readAdditionalSaveData(pCompound); + this.entityData.set(VARIANT, pCompound.getInt("Variant")); + } + + /*? if <=1.20.1 {*/ + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor pLevel, DifficultyInstance pDifficulty, MobSpawnType pReason, @Nullable SpawnGroupData pSpawnData, @Nullable CompoundTag pDataTag) { + ProcompsognathusVariant variant = Util.getRandom(ProcompsognathusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(pLevel, pDifficulty, pReason, pSpawnData, pDataTag); + } + /*?} else {*/ + /*@Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, MobSpawnType spawnType, @Nullable SpawnGroupData spawnGroupData) { + ProcompsognathusVariant variant = Util.getRandom(ProcompsognathusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(level, difficulty, spawnType, spawnGroupData); + } + *//*?}*/ + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } + + @Override + protected @Nullable SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.PROCOMPSOGNATHUS_HURT.get(); + } + + @Override + protected @Nullable SoundEvent getDeathSound() { + return ModSounds.PROCOMPSOGNATHUS_DEATH.get(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/custom/ProtoceratopsEntity.java b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/ProtoceratopsEntity.java new file mode 100644 index 0000000..7f4f555 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/ProtoceratopsEntity.java @@ -0,0 +1,308 @@ +package net.cmr.jurassicrevived.entity.custom; + +import net.cmr.jurassicrevived.entity.ModEntities; +import net.cmr.jurassicrevived.entity.ai.SprintingMeleeAttackGoal; +import net.cmr.jurassicrevived.entity.ai.SprintingPanicGoal; +import net.cmr.jurassicrevived.entity.client.AlbertosaurusVariant; +import net.cmr.jurassicrevived.entity.client.ProtoceratopsVariant; +import net.cmr.jurassicrevived.sound.ModSounds; +import net.minecraft.Util; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.util.Mth; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.*; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.attributes.DefaultAttributes; +import net.minecraft.world.entity.ai.goal.*; +import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerLevelAccessor; +import org.jetbrains.annotations.Nullable; +import software.bernie.geckolib.animatable.GeoEntity; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.core.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.core.animation.AnimatableManager; +import software.bernie.geckolib.core.animation.Animation; +import software.bernie.geckolib.core.animation.AnimationController; +import software.bernie.geckolib.core.animation.RawAnimation; +import software.bernie.geckolib.core.object.PlayState; +/*?} else {*/ +/*import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.animation.*; +*//*?}*/ + +public class ProtoceratopsEntity extends Animal implements GeoEntity { + private AnimatableInstanceCache cache = new SingletonAnimatableInstanceCache(this); + + private static final EntityDataAccessor VARIANT = + SynchedEntityData.defineId(ProtoceratopsEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SYNCED_AGE = + SynchedEntityData.defineId(ProtoceratopsEntity.class, EntityDataSerializers.INT); + + // Procedural tail sway state (client-side use for rendering) + private float tailSwayOffset; // Smoothed offset in range roughly [-1, 1] + private float tailSwayVelocity; // Internal velocity for spring-damper + private float tailSwayPrev; // Previous frame value for interpolation + + public ProtoceratopsEntity(EntityType pEntityType, Level pLevel) { + super(pEntityType, pLevel); + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new SprintingPanicGoal(this, 1.15) { + @Override + public boolean canUse() { + return ProtoceratopsEntity.this.isBaby() && super.canUse(); + } + }); + this.targetSelector.addGoal(1, new HurtByTargetGoal(this).setAlertOthers()); + this.goalSelector.addGoal(2, new FloatGoal(this)); + this.goalSelector.addGoal(3, new AvoidEntityGoal<>(this, VelociraptorEntity.class, (float) 20, 0.8, 0.8)); + this.goalSelector.addGoal(4, new AvoidEntityGoal<>(this, CeratosaurusEntity.class, (float) 20, 0.8, 0.8)); + this.goalSelector.addGoal(5, new AvoidEntityGoal<>(this, DilophosaurusEntity.class, (float) 20, 0.8, 0.8)); + this.goalSelector.addGoal(6, new SprintingMeleeAttackGoal(this, 1.1, false)); + this.goalSelector.addGoal(7, new BreedGoal(this, 1.0)); + this.goalSelector.addGoal(8, new FollowParentGoal(this, 1.25)); + this.goalSelector.addGoal(8, new WaterAvoidingRandomStrollGoal(this, 1.0)); + this.goalSelector.addGoal(10, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(11, new FollowMobGoal(this, 0.8, (float) 20, (float) 10)); + this.goalSelector.addGoal(12, new AvoidEntityGoal<>(this, SpinosaurusEntity.class, (float) 20, 0.8, 0.8)); + this.goalSelector.addGoal(13, new AvoidEntityGoal<>(this, TyrannosaurusRexEntity.class, (float) 20, 0.8, 0.8)); + this.goalSelector.addGoal(14, new AvoidEntityGoal<>(this, IndominusRexEntity.class, (float) 20, 0.8, 0.8)); + this.goalSelector.addGoal(15, new EatBlockGoal(this)); + this.goalSelector.addGoal(16, new RandomLookAroundGoal(this)); + + } + + public static AttributeSupplier.Builder createAttributes() { + return Animal.createLivingAttributes() + .add(Attributes.MAX_HEALTH, 20D) + .add(Attributes.MOVEMENT_SPEED, 0.3D) + .add(Attributes.ARMOR, 0D) + .add(Attributes.FOLLOW_RANGE, 32D) + .add(Attributes.KNOCKBACK_RESISTANCE, 0D) + .add(Attributes.ATTACK_KNOCKBACK, 0D) + .add(Attributes.ATTACK_DAMAGE, 6D); + } + + @Override + public boolean isFood(ItemStack pStack) { + return pStack.is(Items.KELP); + } + + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel pLevel, AgeableMob pOtherParent) { + AgeableMob child = ModEntities.PROTOCERATOPS.get().create(pLevel); + if (child instanceof ProtoceratopsEntity baby) { + ProtoceratopsVariant randomVariant = Util.getRandom(ProtoceratopsVariant.values(), this.random); + baby.setVariant(randomVariant); + } + return child; + } + + @Override + public boolean doHurtTarget(Entity target) { + boolean hit = super.doHurtTarget(target); + if (!level().isClientSide && hit && target instanceof LivingEntity) { + if (this.level() instanceof ServerLevel serverLevel) { + this.triggerAnim("attackController", "attack"); + } + } + return hit; + } + + @Override + public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { + controllers.add(new AnimationController<>(this, "Walk/Run/Idle", state -> { + if (state.isMoving()) + return state.setAndContinue(ProtoceratopsEntity.this.isSprinting() ? RawAnimation.begin().then("anim.protoceratops.run", Animation.LoopType.LOOP) : RawAnimation.begin().then("anim.protoceratops.walk", Animation.LoopType.LOOP)); + + return state.setAndContinue(RawAnimation.begin().then("anim.protoceratops.idle", Animation.LoopType.LOOP)); + })); + + controllers.add(new AnimationController<>(this, "attackController", state -> PlayState.STOP) + .triggerableAnim("attack", RawAnimation.begin().then("anim.protoceratops.attack", Animation.LoopType.PLAY_ONCE))); + + controllers.add(new AnimationController<>(this, "mouthController", state -> PlayState.STOP) + .triggerableAnim("mouth", RawAnimation.begin().then("anim.protoceratops.mouth", Animation.LoopType.PLAY_ONCE))); + } + + private float getSignedTurnDelta() { + // Only consider the body (torso) rotation so head look does not affect tail sway + return Mth.wrapDegrees(this.yBodyRot - this.yBodyRotO); + } + + private int mouthAnimCooldown = 0; + + @Override + public void tick() { + super.tick(); + if (!level().isClientSide) { + this.entityData.set(DATA_SYNCED_AGE, this.getAge()); + var maxHealthAttr = getAttribute(Attributes.MAX_HEALTH); + if (maxHealthAttr != null) { + double baseAdult = DefaultAttributes.getSupplier((EntityType) this.getType()).getValue(Attributes.MAX_HEALTH); + double desired = this.isBaby() ? baseAdult * 0.10D : baseAdult; + if (maxHealthAttr.getBaseValue() != desired) { + double oldMax = maxHealthAttr.getBaseValue(); + double healthRatio = this.getHealth() / (float) oldMax; + maxHealthAttr.setBaseValue(desired); + this.setHealth((float) (desired * Mth.clamp(healthRatio, 0.0F, 1.0F))); + } + } + } + + if (!level().isClientSide) { + if (mouthAnimCooldown > 0) { + mouthAnimCooldown--; + } else { + this.triggerAnim("mouthController", "mouth"); + this.playSound(ModSounds.PROTOCERATOPS_CALL.get(), 1.0F, 1.0F); + // 30s–60s in ticks + mouthAnimCooldown = this.random.nextInt(1200 - 600 + 1) + 600; + } + } + + if (level().isClientSide) { + // Capture previous for smooth interpolation between ticks + this.tailSwayPrev = this.tailSwayOffset; + updateProceduralTailSway(); + } + } + + private void updateProceduralTailSway() { + // Turn input derived from rotation deltas; works even when standing still and turning + float turnDegrees = getSignedTurnDelta(); + + // Deadzone to ignore tiny jitter so the tail can return to center cleanly + float deadzoneDeg = 0.6f; // smaller deadzone for more responsiveness + float turnInput = 0.0f; + if (Math.abs(turnDegrees) >= deadzoneDeg) { + // Higher sensitivity so small in-place turns still affect the model + turnInput = Mth.clamp(turnDegrees / 15.0f, -1.0f, 1.0f); + } + + // Target offset: keep intuitive sign (positive input -> positive sway) + float target = turnInput; + + // One-pole low-pass (no bounce). Larger alpha => snappier and less "stiff". + float alpha = 0.24f; // try 0.20–0.30 to taste + + tailSwayOffset += (target - tailSwayOffset) * alpha; + + // Snap tiny residuals to zero so it visibly settles + if (Math.abs(tailSwayOffset) < 0.003f) { + tailSwayOffset = 0.0f; + } + + // No oscillation velocity retained + tailSwayVelocity = 0.0f; + + tailSwayOffset = Mth.clamp(tailSwayOffset, -1.5f, 1.5f); + } + + // Expose to the model for bone rotation + public float getTailSwayOffset() { + return tailSwayOffset; + } + + // Interpolated sway for smooth rendering between ticks + public float getTailSwayOffset(float partialTick) { + return Mth.lerp(Mth.clamp(partialTick, 0.0f, 1.0f), tailSwayPrev, tailSwayOffset); + } + + /*? if <=1.20.1 {*/ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); + this.entityData.define(VARIANT, 0); + this.entityData.define(DATA_SYNCED_AGE, 0); + } + /*?} else {*/ + /*@Override + protected void defineSynchedData(SynchedEntityData.Builder pBuilder) { + super.defineSynchedData(pBuilder); + pBuilder.define(VARIANT, 0); + pBuilder.define(DATA_SYNCED_AGE, 0); + } + *//*?}*/ + + public int getSyncedAge() { + return this.entityData.get(DATA_SYNCED_AGE); + } + public int getTypeVariant() { + return this.entityData.get(VARIANT); + } + + public ProtoceratopsVariant getVariant() { + return ProtoceratopsVariant.byId(this.getTypeVariant() & 255); + } + + private void setVariant(ProtoceratopsVariant variant) { + this.entityData.set(VARIANT, variant.getId() & 255); + } + + @Override + public boolean canMate(Animal other) { + if (!super.canMate(other)) return false; + if (!(other instanceof ProtoceratopsEntity that)) return false; + return this.getVariant() != that.getVariant(); + } + @Override + public void addAdditionalSaveData(CompoundTag pCompound) { + super.addAdditionalSaveData(pCompound); + pCompound.putInt("Variant", this.getTypeVariant()); + } + + @Override + public void readAdditionalSaveData(CompoundTag pCompound) { + super.readAdditionalSaveData(pCompound); + this.entityData.set(VARIANT, pCompound.getInt("Variant")); + } + + /*? if <=1.20.1 {*/ + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor pLevel, DifficultyInstance pDifficulty, MobSpawnType pReason, @Nullable SpawnGroupData pSpawnData, @Nullable CompoundTag pDataTag) { + ProtoceratopsVariant variant = Util.getRandom(ProtoceratopsVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(pLevel, pDifficulty, pReason, pSpawnData, pDataTag); + } + /*?} else {*/ + /*@Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, MobSpawnType spawnType, @Nullable SpawnGroupData spawnGroupData) { + ProtoceratopsVariant variant = Util.getRandom(ProtoceratopsVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(level, difficulty, spawnType, spawnGroupData); + } + *//*?}*/ + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } + + @Override + protected @Nullable SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.PROTOCERATOPS_HURT.get(); + } + + @Override + protected @Nullable SoundEvent getDeathSound() { + return ModSounds.PROTOCERATOPS_DEATH.get(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/custom/PteranodonEntity.java b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/PteranodonEntity.java new file mode 100644 index 0000000..1700a7d --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/PteranodonEntity.java @@ -0,0 +1,411 @@ +package net.cmr.jurassicrevived.entity.custom; + +import net.cmr.jurassicrevived.entity.ModEntities; +import net.cmr.jurassicrevived.entity.ai.SprintingMeleeAttackGoal; +import net.cmr.jurassicrevived.entity.ai.SprintingPanicGoal; +import net.cmr.jurassicrevived.entity.client.AlbertosaurusVariant; +import net.cmr.jurassicrevived.entity.client.PteranodonVariant; +import net.cmr.jurassicrevived.sound.ModSounds; +import net.minecraft.Util; +import net.minecraft.core.BlockPos; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.*; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.attributes.DefaultAttributes; +import net.minecraft.world.entity.ai.control.FlyingMoveControl; +import net.minecraft.world.entity.ai.goal.*; +import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal; +import net.minecraft.world.entity.ai.goal.target.NearestAttackableTargetGoal; +import net.minecraft.world.entity.ai.navigation.FlyingPathNavigation; +import net.minecraft.world.entity.ai.navigation.PathNavigation; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.animal.FlyingAnimal; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerLevelAccessor; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.phys.Vec3; +import org.jetbrains.annotations.Nullable; +import software.bernie.geckolib.animatable.GeoEntity; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.core.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.core.animation.AnimatableManager; +import software.bernie.geckolib.core.animation.Animation; +import software.bernie.geckolib.core.animation.AnimationController; +import software.bernie.geckolib.core.animation.RawAnimation; +import software.bernie.geckolib.core.object.PlayState; +/*?} else {*/ +/*import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.animation.*; +*//*?}*/ + +public class PteranodonEntity extends Animal implements GeoEntity, FlyingAnimal { + private final AnimatableInstanceCache cache = new SingletonAnimatableInstanceCache(this); + + private static final EntityDataAccessor VARIANT = + SynchedEntityData.defineId(PteranodonEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SYNCED_AGE = + SynchedEntityData.defineId(PteranodonEntity.class, EntityDataSerializers.INT); + + // Procedural tail sway state (client-side use for rendering) + private float tailSwayOffset; // Smoothed offset in range roughly [-1, 1] + private float tailSwayVelocity; // Internal velocity for spring-damper + private float tailSwayPrev; // Previous frame value for interpolation + + public PteranodonEntity(EntityType pEntityType, Level pLevel) { + super(pEntityType, pLevel); + this.moveControl = new FlyingMoveControl(this, 20, true); + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new SprintingPanicGoal(this, 1.15) { + @Override + public boolean canUse() { + return PteranodonEntity.this.isBaby() && super.canUse(); + } + }); + this.goalSelector.addGoal(2, new FloatGoal(this)); + this.goalSelector.addGoal(3, new AvoidEntityGoal<>(this, BrachiosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(4, new SprintingMeleeAttackGoal(this, 1.1, false)); + this.goalSelector.addGoal(5, new BreedGoal(this, 1.0)); + this.goalSelector.addGoal(6, new FollowParentGoal(this, 1.25)); + + // Goal 7: Wander on ground (Walk) - Only when on ground + this.goalSelector.addGoal(7, new WaterAvoidingRandomStrollGoal(this, 1.0) { + @Override + public boolean canUse() { + return PteranodonEntity.this.onGround() && super.canUse(); + } + }); + + // Goal 8: Wander in air (Fly) - Handles takeoff, flying, and landing + this.goalSelector.addGoal(8, new WaterAvoidingRandomStrollGoal(this, 1.0, 20) { + @Override + public boolean canUse() { + boolean isFlying = !PteranodonEntity.this.onGround(); + // If flying, keep flying. If on ground, small chance (1/400 ticks) to take off. + return (isFlying || PteranodonEntity.this.getRandom().nextInt(400) == 0) && super.canUse(); + } + + @Override + protected Vec3 getPosition() { + Vec3 pos = PteranodonEntity.this.position(); + RandomSource random = PteranodonEntity.this.getRandom(); + + double x = pos.x + (random.nextFloat() * 2 - 1) * 32; + double z = pos.z + (random.nextFloat() * 2 - 1) * 32; + + // Get ground height at the random destination (returns Y of first air block) + int groundY = PteranodonEntity.this.level().getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, (int)x, (int)z); + double y; + + if (PteranodonEntity.this.onGround()) { + // Takeoff: Target well above ground to ensure liftoff + y = pos.y + 15 + random.nextInt(10); + } else { + // Flying: 5% chance to land, otherwise stay airborne but capped height + if (random.nextFloat() < 0.05f) { + y = groundY; // Land + } else if (pos.y > groundY + 20) { + // Too high: Force descent + y = pos.y - 5 - random.nextInt(10); + } else { + // Just wander up or down a bit + y = pos.y + (random.nextFloat() * 2 - 1) * 10; + } + } + + // Don't target below the ground (blocks) + if (y < groundY) y = groundY; + + return new Vec3(x, y, z); + } + }); + + this.goalSelector.addGoal(9, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(10, new FollowMobGoal(this, 0.8, (float) 20, (float) 10)); + this.targetSelector.addGoal(11, new NearestAttackableTargetGoal<>(this, Animal.class, 10, false, false, + target -> { + // 1. Don't eat your own species + if (target.getType() == this.getType()) return false; + + // 2. Don't eat Flying Animals + if (target instanceof FlyingAnimal) return false; + + // 3. SIZE CHECK: specific height and width limits + // Example: Height < 1.0 blocks AND Width < 1.0 blocks + boolean isSmallEnough = target.getBbHeight() <= 1.0F && target.getBbWidth() <= 1.0F; + + return isSmallEnough; + } + )); + this.targetSelector.addGoal(12, new NearestAttackableTargetGoal<>(this, Player.class, false, false)); + this.targetSelector.addGoal(13, new HurtByTargetGoal(this)); + this.goalSelector.addGoal(14, new RandomLookAroundGoal(this)); + this.goalSelector.addGoal(15, new FloatGoal(this)); + } + + public static AttributeSupplier.Builder createAttributes() { + return Animal.createLivingAttributes() + .add(Attributes.MAX_HEALTH, 50D) + .add(Attributes.MOVEMENT_SPEED, 0.3D) + .add(Attributes.FLYING_SPEED, 0.6D) + .add(Attributes.ARMOR, 0D) + .add(Attributes.FOLLOW_RANGE, 32D) + .add(Attributes.KNOCKBACK_RESISTANCE, 0D) + .add(Attributes.ATTACK_KNOCKBACK, 0D) + .add(Attributes.ATTACK_DAMAGE, 8D); + } + + @Override + protected PathNavigation createNavigation(Level pLevel) { + FlyingPathNavigation navigation = new FlyingPathNavigation(this, pLevel); + navigation.setCanOpenDoors(false); + navigation.setCanFloat(true); + navigation.setCanPassDoors(true); + return navigation; + } + + @Override + public boolean isFlying() { + return !this.onGround(); + } + + @Override + public boolean causeFallDamage(float pFallDistance, float pMultiplier, DamageSource pSource) { + return false; + } + + @Override + protected void checkFallDamage(double pY, boolean pOnGround, BlockState pState, BlockPos pPos) { + } + + @Override + public boolean isFood(ItemStack pStack) { + return pStack.is(Items.BEEF); + } + + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel pLevel, AgeableMob pOtherParent) { + AgeableMob child = ModEntities.PTERANODON.get().create(pLevel); + if (child instanceof PteranodonEntity baby) { + PteranodonVariant randomVariant = Util.getRandom(PteranodonVariant.values(), this.random); + baby.setVariant(randomVariant); + } + return child; + } + + @Override + public boolean doHurtTarget(Entity target) { + boolean hit = super.doHurtTarget(target); + if (!level().isClientSide && hit && target instanceof LivingEntity) { + if (this.level() instanceof ServerLevel serverLevel) { + this.triggerAnim("attackController", "attack"); + this.playSound(ModSounds.BEAK_ATTACK.get(), 1.0F, 1.0F); + } + } + return hit; + } + + @Override + public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { + controllers.add(new AnimationController<>(this, "Walk/Run/Idle", state -> { + if (!PteranodonEntity.this.onGround()) { + return state.setAndContinue(RawAnimation.begin().then("anim.pteranodon.fly", Animation.LoopType.LOOP)); + } + + if (state.isMoving()) + return state.setAndContinue(PteranodonEntity.this.isSprinting() ? RawAnimation.begin().then("anim.pteranodon.walk", Animation.LoopType.LOOP) : RawAnimation.begin().then("anim.pteranodon.walk", Animation.LoopType.LOOP)); + + return state.setAndContinue(RawAnimation.begin().then("anim.pteranodon.idle", Animation.LoopType.LOOP)); + })); + + controllers.add(new AnimationController<>(this, "attackController", state -> PlayState.STOP) + .triggerableAnim("attack", RawAnimation.begin().then("anim.pteranodon.attack", Animation.LoopType.PLAY_ONCE))); + + controllers.add(new AnimationController<>(this, "mouthController", state -> PlayState.STOP) + .triggerableAnim("mouth", RawAnimation.begin().then("anim.pteranodon.mouth", Animation.LoopType.PLAY_ONCE))); + } + + private float getSignedTurnDelta() { + // Only consider the body (torso) rotation so head look does not affect tail sway + return Mth.wrapDegrees(this.yBodyRot - this.yBodyRotO); + } + + private int mouthAnimCooldown = 0; + + @Override + public void tick() { + super.tick(); + if (!level().isClientSide) { + this.entityData.set(DATA_SYNCED_AGE, this.getAge()); + var maxHealthAttr = getAttribute(Attributes.MAX_HEALTH); + if (maxHealthAttr != null) { + double baseAdult = DefaultAttributes.getSupplier((EntityType) this.getType()).getValue(Attributes.MAX_HEALTH); + double desired = this.isBaby() ? baseAdult * 0.10D : baseAdult; + if (maxHealthAttr.getBaseValue() != desired) { + double oldMax = maxHealthAttr.getBaseValue(); + double healthRatio = this.getHealth() / (float) oldMax; + maxHealthAttr.setBaseValue(desired); + this.setHealth((float) (desired * Mth.clamp(healthRatio, 0.0F, 1.0F))); + } + } + } + + if (!level().isClientSide) { + if (mouthAnimCooldown > 0) { + mouthAnimCooldown--; + } else { + this.triggerAnim("mouthController", "mouth"); + this.playSound(ModSounds.PTERANODON_CALL.get(), 1.0F, 1.0F); + // 30s–60s in ticks + mouthAnimCooldown = this.random.nextInt(1200 - 600 + 1) + 600; + } + } + + if (level().isClientSide) { + // Capture previous for smooth interpolation between ticks + this.tailSwayPrev = this.tailSwayOffset; + updateProceduralTailSway(); + } + } + + private void updateProceduralTailSway() { + // Turn input derived from rotation deltas; works even when standing still and turning + float turnDegrees = getSignedTurnDelta(); + + // Deadzone to ignore tiny jitter so the tail can return to center cleanly + float deadzoneDeg = 0.6f; // smaller deadzone for more responsiveness + float turnInput = 0.0f; + if (Math.abs(turnDegrees) >= deadzoneDeg) { + // Higher sensitivity so small in-place turns still affect the model + turnInput = Mth.clamp(turnDegrees / 15.0f, -1.0f, 1.0f); + } + + // Target offset: keep intuitive sign (positive input -> positive sway) + float target = turnInput; + + // One-pole low-pass (no bounce). Larger alpha => snappier and less "stiff". + float alpha = 0.24f; // try 0.20–0.30 to taste + + tailSwayOffset += (target - tailSwayOffset) * alpha; + + // Snap tiny residuals to zero so it visibly settles + if (Math.abs(tailSwayOffset) < 0.003f) { + tailSwayOffset = 0.0f; + } + + // No oscillation velocity retained + tailSwayVelocity = 0.0f; + + tailSwayOffset = Mth.clamp(tailSwayOffset, -1.5f, 1.5f); + } + + // Expose to the model for bone rotation + public float getTailSwayOffset() { + return tailSwayOffset; + } + + // Interpolated sway for smooth rendering between ticks + public float getTailSwayOffset(float partialTick) { + return Mth.lerp(Mth.clamp(partialTick, 0.0f, 1.0f), tailSwayPrev, tailSwayOffset); + } + + /*? if <=1.20.1 {*/ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); + this.entityData.define(VARIANT, 0); + this.entityData.define(DATA_SYNCED_AGE, 0); + } + /*?} else {*/ + /*@Override + protected void defineSynchedData(SynchedEntityData.Builder pBuilder) { + super.defineSynchedData(pBuilder); + pBuilder.define(VARIANT, 0); + pBuilder.define(DATA_SYNCED_AGE, 0); + } + *//*?}*/ + + public int getSyncedAge() { + return this.entityData.get(DATA_SYNCED_AGE); + } + + public PteranodonVariant getVariant() { + return PteranodonVariant.byId(this.getTypeVariant() & 255); + } + public int getTypeVariant() { + return this.entityData.get(VARIANT); + } + + private void setVariant(PteranodonVariant variant) { + this.entityData.set(VARIANT, variant.getId() & 255); + } + + @Override + public boolean canMate(Animal other) { + if (!super.canMate(other)) return false; + if (!(other instanceof PteranodonEntity that)) return false; + return this.getVariant() != that.getVariant(); + } + + /*? if <=1.20.1 {*/ + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor pLevel, DifficultyInstance pDifficulty, MobSpawnType pReason, @Nullable SpawnGroupData pSpawnData, @Nullable CompoundTag pDataTag) { + PteranodonVariant variant = Util.getRandom(PteranodonVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(pLevel, pDifficulty, pReason, pSpawnData, pDataTag); + } + /*?} else {*/ + /*@Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, MobSpawnType spawnType, @Nullable SpawnGroupData spawnGroupData) { + PteranodonVariant variant = Util.getRandom(PteranodonVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(level, difficulty, spawnType, spawnGroupData); + } + *//*?}*/ + + @Override + public void readAdditionalSaveData(CompoundTag pCompound) { + super.readAdditionalSaveData(pCompound); + this.entityData.set(VARIANT, pCompound.getInt("Variant")); + } + + @Override + public void addAdditionalSaveData(CompoundTag pCompound) { + super.addAdditionalSaveData(pCompound); + pCompound.putInt("Variant", this.getTypeVariant()); + } + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } + + @Override + protected @Nullable SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.PTERANODON_HURT.get(); + } + + @Override + protected @Nullable SoundEvent getDeathSound() { + return ModSounds.PTERANODON_DEATH.get(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/custom/PterodaustroEntity.java b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/PterodaustroEntity.java new file mode 100644 index 0000000..636065d --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/PterodaustroEntity.java @@ -0,0 +1,411 @@ +package net.cmr.jurassicrevived.entity.custom; + +import net.cmr.jurassicrevived.entity.ModEntities; +import net.cmr.jurassicrevived.entity.ai.SprintingMeleeAttackGoal; +import net.cmr.jurassicrevived.entity.ai.SprintingPanicGoal; +import net.cmr.jurassicrevived.entity.client.AlbertosaurusVariant; +import net.cmr.jurassicrevived.entity.client.PterodaustroVariant; +import net.cmr.jurassicrevived.sound.ModSounds; +import net.minecraft.Util; +import net.minecraft.core.BlockPos; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.*; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.attributes.DefaultAttributes; +import net.minecraft.world.entity.ai.control.FlyingMoveControl; +import net.minecraft.world.entity.ai.goal.*; +import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal; +import net.minecraft.world.entity.ai.goal.target.NearestAttackableTargetGoal; +import net.minecraft.world.entity.ai.navigation.FlyingPathNavigation; +import net.minecraft.world.entity.ai.navigation.PathNavigation; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.animal.FlyingAnimal; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerLevelAccessor; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.phys.Vec3; +import org.jetbrains.annotations.Nullable; +import software.bernie.geckolib.animatable.GeoEntity; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.core.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.core.animation.AnimatableManager; +import software.bernie.geckolib.core.animation.Animation; +import software.bernie.geckolib.core.animation.AnimationController; +import software.bernie.geckolib.core.animation.RawAnimation; +import software.bernie.geckolib.core.object.PlayState; +/*?} else {*/ +/*import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.animation.*; +*//*?}*/ + +public class PterodaustroEntity extends Animal implements GeoEntity, FlyingAnimal { + private final AnimatableInstanceCache cache = new SingletonAnimatableInstanceCache(this); + + private static final EntityDataAccessor VARIANT = + SynchedEntityData.defineId(PterodaustroEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SYNCED_AGE = + SynchedEntityData.defineId(PterodaustroEntity.class, EntityDataSerializers.INT); + + // Procedural tail sway state (client-side use for rendering) + private float tailSwayOffset; // Smoothed offset in range roughly [-1, 1] + private float tailSwayVelocity; // Internal velocity for spring-damper + private float tailSwayPrev; // Previous frame value for interpolation + + public PterodaustroEntity(EntityType pEntityType, Level pLevel) { + super(pEntityType, pLevel); + this.moveControl = new FlyingMoveControl(this, 20, true); + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new SprintingPanicGoal(this, 1.15) { + @Override + public boolean canUse() { + return PterodaustroEntity.this.isBaby() && super.canUse(); + } + }); + this.goalSelector.addGoal(2, new FloatGoal(this)); + this.goalSelector.addGoal(3, new AvoidEntityGoal<>(this, BrachiosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(4, new SprintingMeleeAttackGoal(this, 1.1, false)); + this.goalSelector.addGoal(5, new BreedGoal(this, 1.0)); + this.goalSelector.addGoal(6, new FollowParentGoal(this, 1.25)); + + // Goal 7: Wander on ground (Walk) - Only when on ground + this.goalSelector.addGoal(7, new WaterAvoidingRandomStrollGoal(this, 1.0) { + @Override + public boolean canUse() { + return PterodaustroEntity.this.onGround() && super.canUse(); + } + }); + + // Goal 8: Wander in air (Fly) - Handles takeoff, flying, and landing + this.goalSelector.addGoal(8, new WaterAvoidingRandomStrollGoal(this, 1.0, 20) { + @Override + public boolean canUse() { + boolean isFlying = !PterodaustroEntity.this.onGround(); + // If flying, keep flying. If on ground, small chance (1/400 ticks) to take off. + return (isFlying || PterodaustroEntity.this.getRandom().nextInt(400) == 0) && super.canUse(); + } + + @Override + protected Vec3 getPosition() { + Vec3 pos = PterodaustroEntity.this.position(); + RandomSource random = PterodaustroEntity.this.getRandom(); + + double x = pos.x + (random.nextFloat() * 2 - 1) * 32; + double z = pos.z + (random.nextFloat() * 2 - 1) * 32; + + // Get ground height at the random destination (returns Y of first air block) + int groundY = PterodaustroEntity.this.level().getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, (int)x, (int)z); + double y; + + if (PterodaustroEntity.this.onGround()) { + // Takeoff: Target well above ground to ensure liftoff + y = pos.y + 15 + random.nextInt(10); + } else { + // Flying: 5% chance to land, otherwise stay airborne but capped height + if (random.nextFloat() < 0.05f) { + y = groundY; // Land + } else if (pos.y > groundY + 20) { + // Too high: Force descent + y = pos.y - 5 - random.nextInt(10); + } else { + // Just wander up or down a bit + y = pos.y + (random.nextFloat() * 2 - 1) * 10; + } + } + + // Don't target below the ground (blocks) + if (y < groundY) y = groundY; + + return new Vec3(x, y, z); + } + }); + + this.goalSelector.addGoal(9, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(10, new FollowMobGoal(this, 0.8, (float) 20, (float) 10)); + this.targetSelector.addGoal(11, new NearestAttackableTargetGoal<>(this, Animal.class, 10, false, false, + target -> { + // 1. Don't eat your own species + if (target.getType() == this.getType()) return false; + + // 2. Don't eat Flying Animals + if (target instanceof FlyingAnimal) return false; + + // 3. SIZE CHECK: specific height and width limits + // Example: Height < 1.0 blocks AND Width < 1.0 blocks + boolean isSmallEnough = target.getBbHeight() <= 1.0F && target.getBbWidth() <= 1.0F; + + return isSmallEnough; + } + )); + this.targetSelector.addGoal(12, new NearestAttackableTargetGoal<>(this, Player.class, false, false)); + this.targetSelector.addGoal(13, new HurtByTargetGoal(this)); + this.goalSelector.addGoal(14, new RandomLookAroundGoal(this)); + this.goalSelector.addGoal(15, new FloatGoal(this)); + } + + public static AttributeSupplier.Builder createAttributes() { + return Animal.createLivingAttributes() + .add(Attributes.MAX_HEALTH, 50D) + .add(Attributes.MOVEMENT_SPEED, 0.3D) + .add(Attributes.FLYING_SPEED, 0.6D) + .add(Attributes.ARMOR, 0D) + .add(Attributes.FOLLOW_RANGE, 32D) + .add(Attributes.KNOCKBACK_RESISTANCE, 0D) + .add(Attributes.ATTACK_KNOCKBACK, 0D) + .add(Attributes.ATTACK_DAMAGE, 8D); + } + + @Override + protected PathNavigation createNavigation(Level pLevel) { + FlyingPathNavigation navigation = new FlyingPathNavigation(this, pLevel); + navigation.setCanOpenDoors(false); + navigation.setCanFloat(true); + navigation.setCanPassDoors(true); + return navigation; + } + + @Override + public boolean isFlying() { + return !this.onGround(); + } + + @Override + public boolean causeFallDamage(float pFallDistance, float pMultiplier, DamageSource pSource) { + return false; + } + + @Override + protected void checkFallDamage(double pY, boolean pOnGround, BlockState pState, BlockPos pPos) { + } + + @Override + public boolean isFood(ItemStack pStack) { + return pStack.is(Items.BEEF); + } + + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel pLevel, AgeableMob pOtherParent) { + AgeableMob child = ModEntities.PTERODAUSTRO.get().create(pLevel); + if (child instanceof PterodaustroEntity baby) { + PterodaustroVariant randomVariant = Util.getRandom(PterodaustroVariant.values(), this.random); + baby.setVariant(randomVariant); + } + return child; + } + + @Override + public boolean doHurtTarget(Entity target) { + boolean hit = super.doHurtTarget(target); + if (!level().isClientSide && hit && target instanceof LivingEntity) { + if (this.level() instanceof ServerLevel serverLevel) { + this.triggerAnim("attackController", "attack"); + this.playSound(ModSounds.BEAK_ATTACK.get(), 1.0F, 1.0F); + } + } + return hit; + } + + @Override + public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { + controllers.add(new AnimationController<>(this, "Walk/Run/Idle", state -> { + if (!PterodaustroEntity.this.onGround()) { + return state.setAndContinue(RawAnimation.begin().then("anim.pterodaustro.fly", Animation.LoopType.LOOP)); + } + + if (state.isMoving()) + return state.setAndContinue(PterodaustroEntity.this.isSprinting() ? RawAnimation.begin().then("anim.pterodaustro.walk", Animation.LoopType.LOOP) : RawAnimation.begin().then("anim.pterodaustro.walk", Animation.LoopType.LOOP)); + + return state.setAndContinue(RawAnimation.begin().then("anim.pterodaustro.idle", Animation.LoopType.LOOP)); + })); + + controllers.add(new AnimationController<>(this, "attackController", state -> PlayState.STOP) + .triggerableAnim("attack", RawAnimation.begin().then("anim.pterodaustro.attack", Animation.LoopType.PLAY_ONCE))); + + controllers.add(new AnimationController<>(this, "mouthController", state -> PlayState.STOP) + .triggerableAnim("mouth", RawAnimation.begin().then("anim.pterodaustro.mouth", Animation.LoopType.PLAY_ONCE))); + } + + private float getSignedTurnDelta() { + // Only consider the body (torso) rotation so head look does not affect tail sway + return Mth.wrapDegrees(this.yBodyRot - this.yBodyRotO); + } + + private int mouthAnimCooldown = 0; + + @Override + public void tick() { + super.tick(); + if (!level().isClientSide) { + this.entityData.set(DATA_SYNCED_AGE, this.getAge()); + var maxHealthAttr = getAttribute(Attributes.MAX_HEALTH); + if (maxHealthAttr != null) { + double baseAdult = DefaultAttributes.getSupplier((EntityType) this.getType()).getValue(Attributes.MAX_HEALTH); + double desired = this.isBaby() ? baseAdult * 0.10D : baseAdult; + if (maxHealthAttr.getBaseValue() != desired) { + double oldMax = maxHealthAttr.getBaseValue(); + double healthRatio = this.getHealth() / (float) oldMax; + maxHealthAttr.setBaseValue(desired); + this.setHealth((float) (desired * Mth.clamp(healthRatio, 0.0F, 1.0F))); + } + } + } + + if (!level().isClientSide) { + if (mouthAnimCooldown > 0) { + mouthAnimCooldown--; + } else { + this.triggerAnim("mouthController", "mouth"); + this.playSound(ModSounds.PTERODAUSTRO_CALL.get(), 1.0F, 1.0F); + // 30s–60s in ticks + mouthAnimCooldown = this.random.nextInt(1200 - 600 + 1) + 600; + } + } + + if (level().isClientSide) { + // Capture previous for smooth interpolation between ticks + this.tailSwayPrev = this.tailSwayOffset; + updateProceduralTailSway(); + } + } + + private void updateProceduralTailSway() { + // Turn input derived from rotation deltas; works even when standing still and turning + float turnDegrees = getSignedTurnDelta(); + + // Deadzone to ignore tiny jitter so the tail can return to center cleanly + float deadzoneDeg = 0.6f; // smaller deadzone for more responsiveness + float turnInput = 0.0f; + if (Math.abs(turnDegrees) >= deadzoneDeg) { + // Higher sensitivity so small in-place turns still affect the model + turnInput = Mth.clamp(turnDegrees / 15.0f, -1.0f, 1.0f); + } + + // Target offset: keep intuitive sign (positive input -> positive sway) + float target = turnInput; + + // One-pole low-pass (no bounce). Larger alpha => snappier and less "stiff". + float alpha = 0.24f; // try 0.20–0.30 to taste + + tailSwayOffset += (target - tailSwayOffset) * alpha; + + // Snap tiny residuals to zero so it visibly settles + if (Math.abs(tailSwayOffset) < 0.003f) { + tailSwayOffset = 0.0f; + } + + // No oscillation velocity retained + tailSwayVelocity = 0.0f; + + tailSwayOffset = Mth.clamp(tailSwayOffset, -1.5f, 1.5f); + } + + // Expose to the model for bone rotation + public float getTailSwayOffset() { + return tailSwayOffset; + } + + // Interpolated sway for smooth rendering between ticks + public float getTailSwayOffset(float partialTick) { + return Mth.lerp(Mth.clamp(partialTick, 0.0f, 1.0f), tailSwayPrev, tailSwayOffset); + } + + /*? if <=1.20.1 {*/ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); + this.entityData.define(VARIANT, 0); + this.entityData.define(DATA_SYNCED_AGE, 0); + } + /*?} else {*/ + /*@Override + protected void defineSynchedData(SynchedEntityData.Builder pBuilder) { + super.defineSynchedData(pBuilder); + pBuilder.define(VARIANT, 0); + pBuilder.define(DATA_SYNCED_AGE, 0); + } + *//*?}*/ + + public int getSyncedAge() { + return this.entityData.get(DATA_SYNCED_AGE); + } + + public PterodaustroVariant getVariant() { + return PterodaustroVariant.byId(this.getTypeVariant() & 255); + } + public int getTypeVariant() { + return this.entityData.get(VARIANT); + } + + private void setVariant(PterodaustroVariant variant) { + this.entityData.set(VARIANT, variant.getId() & 255); + } + + @Override + public boolean canMate(Animal other) { + if (!super.canMate(other)) return false; + if (!(other instanceof PterodaustroEntity that)) return false; + return this.getVariant() != that.getVariant(); + } + + /*? if <=1.20.1 {*/ + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor pLevel, DifficultyInstance pDifficulty, MobSpawnType pReason, @Nullable SpawnGroupData pSpawnData, @Nullable CompoundTag pDataTag) { + PterodaustroVariant variant = Util.getRandom(PterodaustroVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(pLevel, pDifficulty, pReason, pSpawnData, pDataTag); + } + /*?} else {*/ + /*@Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, MobSpawnType spawnType, @Nullable SpawnGroupData spawnGroupData) { + PterodaustroVariant variant = Util.getRandom(PterodaustroVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(level, difficulty, spawnType, spawnGroupData); + } + *//*?}*/ + + @Override + public void readAdditionalSaveData(CompoundTag pCompound) { + super.readAdditionalSaveData(pCompound); + this.entityData.set(VARIANT, pCompound.getInt("Variant")); + } + + @Override + public void addAdditionalSaveData(CompoundTag pCompound) { + super.addAdditionalSaveData(pCompound); + pCompound.putInt("Variant", this.getTypeVariant()); + } + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } + + @Override + protected @Nullable SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.PTERODAUSTRO_HURT.get(); + } + + @Override + protected @Nullable SoundEvent getDeathSound() { + return ModSounds.PTERODAUSTRO_DEATH.get(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/custom/QuetzalcoatlusEntity.java b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/QuetzalcoatlusEntity.java new file mode 100644 index 0000000..ee895ad --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/QuetzalcoatlusEntity.java @@ -0,0 +1,411 @@ +package net.cmr.jurassicrevived.entity.custom; + +import net.cmr.jurassicrevived.entity.ModEntities; +import net.cmr.jurassicrevived.entity.ai.SprintingMeleeAttackGoal; +import net.cmr.jurassicrevived.entity.ai.SprintingPanicGoal; +import net.cmr.jurassicrevived.entity.client.AlbertosaurusVariant; +import net.cmr.jurassicrevived.entity.client.QuetzalcoatlusVariant; +import net.cmr.jurassicrevived.sound.ModSounds; +import net.minecraft.Util; +import net.minecraft.core.BlockPos; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.*; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.attributes.DefaultAttributes; +import net.minecraft.world.entity.ai.control.FlyingMoveControl; +import net.minecraft.world.entity.ai.goal.*; +import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal; +import net.minecraft.world.entity.ai.goal.target.NearestAttackableTargetGoal; +import net.minecraft.world.entity.ai.navigation.FlyingPathNavigation; +import net.minecraft.world.entity.ai.navigation.PathNavigation; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.animal.FlyingAnimal; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerLevelAccessor; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.phys.Vec3; +import org.jetbrains.annotations.Nullable; +import software.bernie.geckolib.animatable.GeoEntity; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.core.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.core.animation.AnimatableManager; +import software.bernie.geckolib.core.animation.Animation; +import software.bernie.geckolib.core.animation.AnimationController; +import software.bernie.geckolib.core.animation.RawAnimation; +import software.bernie.geckolib.core.object.PlayState; +/*?} else {*/ +/*import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.animation.*; +*//*?}*/ + +public class QuetzalcoatlusEntity extends Animal implements GeoEntity, FlyingAnimal { + private final AnimatableInstanceCache cache = new SingletonAnimatableInstanceCache(this); + + private static final EntityDataAccessor VARIANT = + SynchedEntityData.defineId(QuetzalcoatlusEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SYNCED_AGE = + SynchedEntityData.defineId(QuetzalcoatlusEntity.class, EntityDataSerializers.INT); + + // Procedural tail sway state (client-side use for rendering) + private float tailSwayOffset; // Smoothed offset in range roughly [-1, 1] + private float tailSwayVelocity; // Internal velocity for spring-damper + private float tailSwayPrev; // Previous frame value for interpolation + + public QuetzalcoatlusEntity(EntityType pEntityType, Level pLevel) { + super(pEntityType, pLevel); + this.moveControl = new FlyingMoveControl(this, 20, true); + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new SprintingPanicGoal(this, 1.15) { + @Override + public boolean canUse() { + return QuetzalcoatlusEntity.this.isBaby() && super.canUse(); + } + }); + this.goalSelector.addGoal(2, new FloatGoal(this)); + this.goalSelector.addGoal(3, new AvoidEntityGoal<>(this, BrachiosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(4, new SprintingMeleeAttackGoal(this, 1.1, false)); + this.goalSelector.addGoal(5, new BreedGoal(this, 1.0)); + this.goalSelector.addGoal(6, new FollowParentGoal(this, 1.25)); + + // Goal 7: Wander on ground (Walk) - Only when on ground + this.goalSelector.addGoal(7, new WaterAvoidingRandomStrollGoal(this, 1.0) { + @Override + public boolean canUse() { + return QuetzalcoatlusEntity.this.onGround() && super.canUse(); + } + }); + + // Goal 8: Wander in air (Fly) - Handles takeoff, flying, and landing + this.goalSelector.addGoal(8, new WaterAvoidingRandomStrollGoal(this, 1.0, 20) { + @Override + public boolean canUse() { + boolean isFlying = !QuetzalcoatlusEntity.this.onGround(); + // If flying, keep flying. If on ground, small chance (1/400 ticks) to take off. + return (isFlying || QuetzalcoatlusEntity.this.getRandom().nextInt(400) == 0) && super.canUse(); + } + + @Override + protected Vec3 getPosition() { + Vec3 pos = QuetzalcoatlusEntity.this.position(); + RandomSource random = QuetzalcoatlusEntity.this.getRandom(); + + double x = pos.x + (random.nextFloat() * 2 - 1) * 32; + double z = pos.z + (random.nextFloat() * 2 - 1) * 32; + + // Get ground height at the random destination (returns Y of first air block) + int groundY = QuetzalcoatlusEntity.this.level().getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, (int)x, (int)z); + double y; + + if (QuetzalcoatlusEntity.this.onGround()) { + // Takeoff: Target well above ground to ensure liftoff + y = pos.y + 15 + random.nextInt(10); + } else { + // Flying: 5% chance to land, otherwise stay airborne but capped height + if (random.nextFloat() < 0.05f) { + y = groundY; // Land + } else if (pos.y > groundY + 20) { + // Too high: Force descent + y = pos.y - 5 - random.nextInt(10); + } else { + // Just wander up or down a bit + y = pos.y + (random.nextFloat() * 2 - 1) * 10; + } + } + + // Don't target below the ground (blocks) + if (y < groundY) y = groundY; + + return new Vec3(x, y, z); + } + }); + + this.goalSelector.addGoal(9, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(10, new FollowMobGoal(this, 0.8, (float) 20, (float) 10)); + this.targetSelector.addGoal(11, new NearestAttackableTargetGoal<>(this, Animal.class, 10, false, false, + target -> { + // 1. Don't eat your own species + if (target.getType() == this.getType()) return false; + + // 2. Don't eat Flying Animals + if (target instanceof FlyingAnimal) return false; + + // 3. SIZE CHECK: specific height and width limits + // Example: Height < 1.0 blocks AND Width < 1.0 blocks + boolean isSmallEnough = target.getBbHeight() <= 1.0F && target.getBbWidth() <= 1.0F; + + return isSmallEnough; + } + )); + this.targetSelector.addGoal(12, new NearestAttackableTargetGoal<>(this, Player.class, false, false)); + this.targetSelector.addGoal(13, new HurtByTargetGoal(this)); + this.goalSelector.addGoal(14, new RandomLookAroundGoal(this)); + this.goalSelector.addGoal(15, new FloatGoal(this)); + } + + public static AttributeSupplier.Builder createAttributes() { + return Animal.createLivingAttributes() + .add(Attributes.MAX_HEALTH, 100D) + .add(Attributes.MOVEMENT_SPEED, 0.3D) + .add(Attributes.FLYING_SPEED, 0.6D) + .add(Attributes.ARMOR, 0D) + .add(Attributes.FOLLOW_RANGE, 32D) + .add(Attributes.KNOCKBACK_RESISTANCE, 0D) + .add(Attributes.ATTACK_KNOCKBACK, 0D) + .add(Attributes.ATTACK_DAMAGE, 20D); + } + + @Override + protected PathNavigation createNavigation(Level pLevel) { + FlyingPathNavigation navigation = new FlyingPathNavigation(this, pLevel); + navigation.setCanOpenDoors(false); + navigation.setCanFloat(true); + navigation.setCanPassDoors(true); + return navigation; + } + + @Override + public boolean isFlying() { + return !this.onGround(); + } + + @Override + public boolean causeFallDamage(float pFallDistance, float pMultiplier, DamageSource pSource) { + return false; + } + + @Override + protected void checkFallDamage(double pY, boolean pOnGround, BlockState pState, BlockPos pPos) { + } + + @Override + public boolean isFood(ItemStack pStack) { + return pStack.is(Items.BEEF); + } + + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel pLevel, AgeableMob pOtherParent) { + AgeableMob child = ModEntities.QUETZALCOATLUS.get().create(pLevel); + if (child instanceof QuetzalcoatlusEntity baby) { + QuetzalcoatlusVariant randomVariant = Util.getRandom(QuetzalcoatlusVariant.values(), this.random); + baby.setVariant(randomVariant); + } + return child; + } + + @Override + public boolean doHurtTarget(Entity target) { + boolean hit = super.doHurtTarget(target); + if (!level().isClientSide && hit && target instanceof LivingEntity) { + if (this.level() instanceof ServerLevel serverLevel) { + this.triggerAnim("attackController", "attack"); + this.playSound(ModSounds.BEAK_ATTACK.get(), 1.0F, 1.0F); + } + } + return hit; + } + + @Override + public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { + controllers.add(new AnimationController<>(this, "Walk/Run/Idle", state -> { + if (!QuetzalcoatlusEntity.this.onGround()) { + return state.setAndContinue(RawAnimation.begin().then("anim.quetzalcoatlus.fly", Animation.LoopType.LOOP)); + } + + if (state.isMoving()) + return state.setAndContinue(QuetzalcoatlusEntity.this.isSprinting() ? RawAnimation.begin().then("anim.quetzalcoatlus.walk", Animation.LoopType.LOOP) : RawAnimation.begin().then("anim.quetzalcoatlus.walk", Animation.LoopType.LOOP)); + + return state.setAndContinue(RawAnimation.begin().then("anim.quetzalcoatlus.idle", Animation.LoopType.LOOP)); + })); + + controllers.add(new AnimationController<>(this, "attackController", state -> PlayState.STOP) + .triggerableAnim("attack", RawAnimation.begin().then("anim.quetzalcoatlus.attack", Animation.LoopType.PLAY_ONCE))); + + controllers.add(new AnimationController<>(this, "mouthController", state -> PlayState.STOP) + .triggerableAnim("mouth", RawAnimation.begin().then("anim.quetzalcoatlus.mouth", Animation.LoopType.PLAY_ONCE))); + } + + private float getSignedTurnDelta() { + // Only consider the body (torso) rotation so head look does not affect tail sway + return Mth.wrapDegrees(this.yBodyRot - this.yBodyRotO); + } + + private int mouthAnimCooldown = 0; + + @Override + public void tick() { + super.tick(); + if (!level().isClientSide) { + this.entityData.set(DATA_SYNCED_AGE, this.getAge()); + var maxHealthAttr = getAttribute(Attributes.MAX_HEALTH); + if (maxHealthAttr != null) { + double baseAdult = DefaultAttributes.getSupplier((EntityType) this.getType()).getValue(Attributes.MAX_HEALTH); + double desired = this.isBaby() ? baseAdult * 0.10D : baseAdult; + if (maxHealthAttr.getBaseValue() != desired) { + double oldMax = maxHealthAttr.getBaseValue(); + double healthRatio = this.getHealth() / (float) oldMax; + maxHealthAttr.setBaseValue(desired); + this.setHealth((float) (desired * Mth.clamp(healthRatio, 0.0F, 1.0F))); + } + } + } + + if (!level().isClientSide) { + if (mouthAnimCooldown > 0) { + mouthAnimCooldown--; + } else { + this.triggerAnim("mouthController", "mouth"); + this.playSound(ModSounds.QUETZALCOATLUS_CALL.get(), 1.0F, 1.0F); + // 30s–60s in ticks + mouthAnimCooldown = this.random.nextInt(1200 - 600 + 1) + 600; + } + } + + if (level().isClientSide) { + // Capture previous for smooth interpolation between ticks + this.tailSwayPrev = this.tailSwayOffset; + updateProceduralTailSway(); + } + } + + private void updateProceduralTailSway() { + // Turn input derived from rotation deltas; works even when standing still and turning + float turnDegrees = getSignedTurnDelta(); + + // Deadzone to ignore tiny jitter so the tail can return to center cleanly + float deadzoneDeg = 0.6f; // smaller deadzone for more responsiveness + float turnInput = 0.0f; + if (Math.abs(turnDegrees) >= deadzoneDeg) { + // Higher sensitivity so small in-place turns still affect the model + turnInput = Mth.clamp(turnDegrees / 15.0f, -1.0f, 1.0f); + } + + // Target offset: keep intuitive sign (positive input -> positive sway) + float target = turnInput; + + // One-pole low-pass (no bounce). Larger alpha => snappier and less "stiff". + float alpha = 0.24f; // try 0.20–0.30 to taste + + tailSwayOffset += (target - tailSwayOffset) * alpha; + + // Snap tiny residuals to zero so it visibly settles + if (Math.abs(tailSwayOffset) < 0.003f) { + tailSwayOffset = 0.0f; + } + + // No oscillation velocity retained + tailSwayVelocity = 0.0f; + + tailSwayOffset = Mth.clamp(tailSwayOffset, -1.5f, 1.5f); + } + + // Expose to the model for bone rotation + public float getTailSwayOffset() { + return tailSwayOffset; + } + + // Interpolated sway for smooth rendering between ticks + public float getTailSwayOffset(float partialTick) { + return Mth.lerp(Mth.clamp(partialTick, 0.0f, 1.0f), tailSwayPrev, tailSwayOffset); + } + + /*? if <=1.20.1 {*/ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); + this.entityData.define(VARIANT, 0); + this.entityData.define(DATA_SYNCED_AGE, 0); + } + /*?} else {*/ + /*@Override + protected void defineSynchedData(SynchedEntityData.Builder pBuilder) { + super.defineSynchedData(pBuilder); + pBuilder.define(VARIANT, 0); + pBuilder.define(DATA_SYNCED_AGE, 0); + } + *//*?}*/ + + public int getSyncedAge() { + return this.entityData.get(DATA_SYNCED_AGE); + } + + public QuetzalcoatlusVariant getVariant() { + return QuetzalcoatlusVariant.byId(this.getTypeVariant() & 255); + } + public int getTypeVariant() { + return this.entityData.get(VARIANT); + } + + private void setVariant(QuetzalcoatlusVariant variant) { + this.entityData.set(VARIANT, variant.getId() & 255); + } + + @Override + public boolean canMate(Animal other) { + if (!super.canMate(other)) return false; + if (!(other instanceof QuetzalcoatlusEntity that)) return false; + return this.getVariant() != that.getVariant(); + } + + /*? if <=1.20.1 {*/ + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor pLevel, DifficultyInstance pDifficulty, MobSpawnType pReason, @Nullable SpawnGroupData pSpawnData, @Nullable CompoundTag pDataTag) { + QuetzalcoatlusVariant variant = Util.getRandom(QuetzalcoatlusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(pLevel, pDifficulty, pReason, pSpawnData, pDataTag); + } + /*?} else {*/ + /*@Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, MobSpawnType spawnType, @Nullable SpawnGroupData spawnGroupData) { + QuetzalcoatlusVariant variant = Util.getRandom(QuetzalcoatlusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(level, difficulty, spawnType, spawnGroupData); + } + *//*?}*/ + + @Override + public void readAdditionalSaveData(CompoundTag pCompound) { + super.readAdditionalSaveData(pCompound); + this.entityData.set(VARIANT, pCompound.getInt("Variant")); + } + + @Override + public void addAdditionalSaveData(CompoundTag pCompound) { + super.addAdditionalSaveData(pCompound); + pCompound.putInt("Variant", this.getTypeVariant()); + } + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } + + @Override + protected @Nullable SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.QUETZALCOATLUS_HURT.get(); + } + + @Override + protected @Nullable SoundEvent getDeathSound() { + return ModSounds.QUETZALCOATLUS_DEATH.get(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/custom/RajasaurusEntity.java b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/RajasaurusEntity.java new file mode 100644 index 0000000..62bdcdc --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/RajasaurusEntity.java @@ -0,0 +1,318 @@ +package net.cmr.jurassicrevived.entity.custom; + +import net.cmr.jurassicrevived.entity.ModEntities; +import net.cmr.jurassicrevived.entity.ai.SprintingMeleeAttackGoal; +import net.cmr.jurassicrevived.entity.ai.SprintingPanicGoal; +import net.cmr.jurassicrevived.entity.client.AlbertosaurusVariant; +import net.cmr.jurassicrevived.entity.client.RajasaurusVariant; +import net.cmr.jurassicrevived.sound.ModSounds; +import net.minecraft.Util; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.util.Mth; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.*; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.attributes.DefaultAttributes; +import net.minecraft.world.entity.ai.goal.*; +import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal; +import net.minecraft.world.entity.ai.goal.target.NearestAttackableTargetGoal; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.monster.Monster; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerLevelAccessor; +import org.jetbrains.annotations.Nullable; +import software.bernie.geckolib.animatable.GeoEntity; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.core.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.core.animation.AnimatableManager; +import software.bernie.geckolib.core.animation.Animation; +import software.bernie.geckolib.core.animation.AnimationController; +import software.bernie.geckolib.core.animation.RawAnimation; +import software.bernie.geckolib.core.object.PlayState; +/*?} else {*/ +/*import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.animation.*; +*//*?}*/ + +public class RajasaurusEntity extends Animal implements GeoEntity { + private AnimatableInstanceCache cache = new SingletonAnimatableInstanceCache(this); + + private static final EntityDataAccessor VARIANT = + SynchedEntityData.defineId(RajasaurusEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SYNCED_AGE = + SynchedEntityData.defineId(RajasaurusEntity.class, EntityDataSerializers.INT); + + // Procedural tail sway state (client-side use for rendering) + private float tailSwayOffset; // Smoothed offset in range roughly [-1, 1] + private float tailSwayVelocity; // Internal velocity for spring-damper + private float tailSwayPrev; // Previous frame value for interpolation + + public RajasaurusEntity(EntityType pEntityType, Level pLevel) { + super(pEntityType, pLevel); + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new SprintingPanicGoal(this, 1.15) { + @Override + public boolean canUse() { + return RajasaurusEntity.this.isBaby() && super.canUse(); + } + }); + this.targetSelector.addGoal(1, new HurtByTargetGoal(this).setAlertOthers()); + this.goalSelector.addGoal(2, new FloatGoal(this)); + this.goalSelector.addGoal(3, new AvoidEntityGoal<>(this, BrachiosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(4, new SprintingMeleeAttackGoal(this, 1.1, false)); + this.goalSelector.addGoal(5, new BreedGoal(this, 1.0)); + this.goalSelector.addGoal(6, new FollowParentGoal(this, 1.25)); + this.goalSelector.addGoal(7, new WaterAvoidingRandomStrollGoal(this, 1.0)); + this.goalSelector.addGoal(8, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(9, new FollowMobGoal(this, 1.2, (float) 20, (float) 10)); + this.targetSelector.addGoal(10, new NearestAttackableTargetGoal<>(this, Monster.class, true)); + this.targetSelector.addGoal(11, new NearestAttackableTargetGoal<>(this, Animal.class, 10, false, false, + target -> target.getType() != this.getType())); + this.targetSelector.addGoal(12, new NearestAttackableTargetGoal(this, TriceratopsEntity.class, false, false)); + this.targetSelector.addGoal(13, new NearestAttackableTargetGoal(this, GallimimusEntity.class, false, false)); + this.targetSelector.addGoal(14, new NearestAttackableTargetGoal(this, DilophosaurusEntity.class, false, false)); + this.targetSelector.addGoal(15, new NearestAttackableTargetGoal(this, CeratosaurusEntity.class, false, false)); + this.targetSelector.addGoal(16, new NearestAttackableTargetGoal(this, ParasaurolophusEntity.class, false, false)); + this.targetSelector.addGoal(17, new NearestAttackableTargetGoal(this, VelociraptorEntity.class, false, false)); + this.targetSelector.addGoal(18, new NearestAttackableTargetGoal(this, SpinosaurusEntity.class, false, false)); + this.targetSelector.addGoal(19, new NearestAttackableTargetGoal(this, IndominusRexEntity.class, false, false)); + this.targetSelector.addGoal(20, new NearestAttackableTargetGoal(this, Player.class, false, false)); + this.goalSelector.addGoal(21, new RandomLookAroundGoal(this)); + + + } + + public static AttributeSupplier.Builder createAttributes() { + return Animal.createLivingAttributes() + .add(Attributes.MAX_HEALTH, 45D) + .add(Attributes.MOVEMENT_SPEED, 0.3D) + .add(Attributes.ARMOR, 0D) + .add(Attributes.FOLLOW_RANGE, 32D) + .add(Attributes.KNOCKBACK_RESISTANCE, 0D) + .add(Attributes.ATTACK_KNOCKBACK, 0D) + .add(Attributes.ATTACK_DAMAGE, 14D); + } + + @Override + public boolean isFood(ItemStack pStack) { + return pStack.is(Items.BEEF); + } + + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel pLevel, AgeableMob pOtherParent) { + AgeableMob child = ModEntities.RAJASAURUS.get().create(pLevel); + if (child instanceof RajasaurusEntity baby) { + RajasaurusVariant randomVariant = Util.getRandom(RajasaurusVariant.values(), this.random); + baby.setVariant(randomVariant); + } + return child; + } + + @Override + public boolean doHurtTarget(Entity target) { + boolean hit = super.doHurtTarget(target); + if (!level().isClientSide && hit && target instanceof LivingEntity) { + if (this.level() instanceof ServerLevel serverLevel) { + this.triggerAnim("attackController", "attack"); + this.playSound(ModSounds.MEDIUM_THEROPOD_ATTACK.get(), 1.0F, 1.0F); + } + } + return hit; + } + + @Override + public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { + controllers.add(new AnimationController<>(this, "Walk/Run/Idle", state -> { + if (state.isMoving()) + return state.setAndContinue(RajasaurusEntity.this.isSprinting() ? RawAnimation.begin().then("anim.rajasaurus.run", Animation.LoopType.LOOP) : RawAnimation.begin().then("anim.rajasaurus.walk", Animation.LoopType.LOOP)); + + return state.setAndContinue(RawAnimation.begin().then("anim.rajasaurus.idle", Animation.LoopType.LOOP)); + })); + + controllers.add(new AnimationController<>(this, "attackController", state -> PlayState.STOP) + .triggerableAnim("attack", RawAnimation.begin().then("anim.rajasaurus.attack", Animation.LoopType.PLAY_ONCE))); + + controllers.add(new AnimationController<>(this, "mouthController", state -> PlayState.STOP) + .triggerableAnim("mouth", RawAnimation.begin().then("anim.rajasaurus.mouth", Animation.LoopType.PLAY_ONCE))); + } + + private float getSignedTurnDelta() { + // Only consider the body (torso) rotation so head look does not affect tail sway + return Mth.wrapDegrees(this.yBodyRot - this.yBodyRotO); + } + + private int mouthAnimCooldown = 0; + + @Override + public void tick() { + super.tick(); + if (!level().isClientSide) { + this.entityData.set(DATA_SYNCED_AGE, this.getAge()); + var maxHealthAttr = getAttribute(Attributes.MAX_HEALTH); + if (maxHealthAttr != null) { + double baseAdult = DefaultAttributes.getSupplier((EntityType) this.getType()).getValue(Attributes.MAX_HEALTH); + double desired = this.isBaby() ? baseAdult * 0.10D : baseAdult; + if (maxHealthAttr.getBaseValue() != desired) { + double oldMax = maxHealthAttr.getBaseValue(); + double healthRatio = this.getHealth() / (float) oldMax; + maxHealthAttr.setBaseValue(desired); + this.setHealth((float) (desired * Mth.clamp(healthRatio, 0.0F, 1.0F))); + } + } + } + + if (!level().isClientSide) { + if (mouthAnimCooldown > 0) { + mouthAnimCooldown--; + } else { + this.triggerAnim("mouthController", "mouth"); + this.playSound(ModSounds.RAJASAURUS_CALL.get(), 1.0F, 1.0F); + // 30s–60s in ticks + mouthAnimCooldown = this.random.nextInt(1200 - 600 + 1) + 600; + } + } + + if (level().isClientSide) { + // Capture previous for smooth interpolation between ticks + this.tailSwayPrev = this.tailSwayOffset; + updateProceduralTailSway(); + } + } + + private void updateProceduralTailSway() { + // Turn input derived from rotation deltas; works even when standing still and turning + float turnDegrees = getSignedTurnDelta(); + + // Deadzone to ignore tiny jitter so the tail can return to center cleanly + float deadzoneDeg = 0.6f; // smaller deadzone for more responsiveness + float turnInput = 0.0f; + if (Math.abs(turnDegrees) >= deadzoneDeg) { + // Higher sensitivity so small in-place turns still affect the model + turnInput = Mth.clamp(turnDegrees / 15.0f, -1.0f, 1.0f); + } + + // Target offset: keep intuitive sign (positive input -> positive sway) + float target = turnInput; + + // One-pole low-pass (no bounce). Larger alpha => snappier and less "stiff". + float alpha = 0.24f; // try 0.20–0.30 to taste + + tailSwayOffset += (target - tailSwayOffset) * alpha; + + // Snap tiny residuals to zero so it visibly settles + if (Math.abs(tailSwayOffset) < 0.003f) { + tailSwayOffset = 0.0f; + } + + // No oscillation velocity retained + tailSwayVelocity = 0.0f; + + tailSwayOffset = Mth.clamp(tailSwayOffset, -1.5f, 1.5f); + } + + // Expose to the model for bone rotation + public float getTailSwayOffset() { + return tailSwayOffset; + } + + // Interpolated sway for smooth rendering between ticks + public float getTailSwayOffset(float partialTick) { + return Mth.lerp(Mth.clamp(partialTick, 0.0f, 1.0f), tailSwayPrev, tailSwayOffset); + } + + /*? if <=1.20.1 {*/ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); + this.entityData.define(VARIANT, 0); + this.entityData.define(DATA_SYNCED_AGE, 0); + } + /*?} else {*/ + /*@Override + protected void defineSynchedData(SynchedEntityData.Builder pBuilder) { + super.defineSynchedData(pBuilder); + pBuilder.define(VARIANT, 0); + pBuilder.define(DATA_SYNCED_AGE, 0); + } + *//*?}*/ + + public int getSyncedAge() { + return this.entityData.get(DATA_SYNCED_AGE); + } + public int getTypeVariant() { + return this.entityData.get(VARIANT); + } + + public RajasaurusVariant getVariant() { + return RajasaurusVariant.byId(this.getTypeVariant() & 255); + } + + private void setVariant(RajasaurusVariant variant) { + this.entityData.set(VARIANT, variant.getId() & 255); + } + + @Override + public boolean canMate(Animal other) { + if (!super.canMate(other)) return false; + if (!(other instanceof RajasaurusEntity that)) return false; + return this.getVariant() != that.getVariant(); + } + @Override + public void addAdditionalSaveData(CompoundTag pCompound) { + super.addAdditionalSaveData(pCompound); + pCompound.putInt("Variant", this.getTypeVariant()); + } + + @Override + public void readAdditionalSaveData(CompoundTag pCompound) { + super.readAdditionalSaveData(pCompound); + this.entityData.set(VARIANT, pCompound.getInt("Variant")); + } + + /*? if <=1.20.1 {*/ + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor pLevel, DifficultyInstance pDifficulty, MobSpawnType pReason, @Nullable SpawnGroupData pSpawnData, @Nullable CompoundTag pDataTag) { + RajasaurusVariant variant = Util.getRandom(RajasaurusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(pLevel, pDifficulty, pReason, pSpawnData, pDataTag); + } + /*?} else {*/ + /*@Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, MobSpawnType spawnType, @Nullable SpawnGroupData spawnGroupData) { + RajasaurusVariant variant = Util.getRandom(RajasaurusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(level, difficulty, spawnType, spawnGroupData); + } + *//*?}*/ + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } + + @Override + protected @Nullable SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.RAJASAURUS_HURT.get(); + } + + @Override + protected @Nullable SoundEvent getDeathSound() { + return ModSounds.RAJASAURUS_DEATH.get(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/custom/RugopsEntity.java b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/RugopsEntity.java new file mode 100644 index 0000000..7512670 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/RugopsEntity.java @@ -0,0 +1,318 @@ +package net.cmr.jurassicrevived.entity.custom; + +import net.cmr.jurassicrevived.entity.ModEntities; +import net.cmr.jurassicrevived.entity.ai.SprintingMeleeAttackGoal; +import net.cmr.jurassicrevived.entity.ai.SprintingPanicGoal; +import net.cmr.jurassicrevived.entity.client.AlbertosaurusVariant; +import net.cmr.jurassicrevived.entity.client.RugopsVariant; +import net.cmr.jurassicrevived.sound.ModSounds; +import net.minecraft.Util; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.util.Mth; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.*; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.attributes.DefaultAttributes; +import net.minecraft.world.entity.ai.goal.*; +import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal; +import net.minecraft.world.entity.ai.goal.target.NearestAttackableTargetGoal; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.monster.Monster; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerLevelAccessor; +import org.jetbrains.annotations.Nullable; +import software.bernie.geckolib.animatable.GeoEntity; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.core.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.core.animation.AnimatableManager; +import software.bernie.geckolib.core.animation.Animation; +import software.bernie.geckolib.core.animation.AnimationController; +import software.bernie.geckolib.core.animation.RawAnimation; +import software.bernie.geckolib.core.object.PlayState; +/*?} else {*/ +/*import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.animation.*; +*//*?}*/ + +public class RugopsEntity extends Animal implements GeoEntity { + private AnimatableInstanceCache cache = new SingletonAnimatableInstanceCache(this); + + private static final EntityDataAccessor VARIANT = + SynchedEntityData.defineId(RugopsEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SYNCED_AGE = + SynchedEntityData.defineId(RugopsEntity.class, EntityDataSerializers.INT); + + // Procedural tail sway state (client-side use for rendering) + private float tailSwayOffset; // Smoothed offset in range roughly [-1, 1] + private float tailSwayVelocity; // Internal velocity for spring-damper + private float tailSwayPrev; // Previous frame value for interpolation + + public RugopsEntity(EntityType pEntityType, Level pLevel) { + super(pEntityType, pLevel); + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new SprintingPanicGoal(this, 1.15) { + @Override + public boolean canUse() { + return RugopsEntity.this.isBaby() && super.canUse(); + } + }); + this.targetSelector.addGoal(1, new HurtByTargetGoal(this).setAlertOthers()); + this.goalSelector.addGoal(2, new FloatGoal(this)); + this.goalSelector.addGoal(3, new AvoidEntityGoal<>(this, BrachiosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(4, new SprintingMeleeAttackGoal(this, 1.1, false)); + this.goalSelector.addGoal(5, new BreedGoal(this, 1.0)); + this.goalSelector.addGoal(6, new FollowParentGoal(this, 1.25)); + this.goalSelector.addGoal(7, new WaterAvoidingRandomStrollGoal(this, 1.0)); + this.goalSelector.addGoal(8, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(9, new FollowMobGoal(this, 1.2, (float) 20, (float) 10)); + this.targetSelector.addGoal(10, new NearestAttackableTargetGoal<>(this, Monster.class, true)); + this.targetSelector.addGoal(11, new NearestAttackableTargetGoal<>(this, Animal.class, 10, false, false, + target -> target.getType() != this.getType())); + this.targetSelector.addGoal(12, new NearestAttackableTargetGoal(this, TriceratopsEntity.class, false, false)); + this.targetSelector.addGoal(13, new NearestAttackableTargetGoal(this, GallimimusEntity.class, false, false)); + this.targetSelector.addGoal(14, new NearestAttackableTargetGoal(this, DilophosaurusEntity.class, false, false)); + this.targetSelector.addGoal(15, new NearestAttackableTargetGoal(this, CeratosaurusEntity.class, false, false)); + this.targetSelector.addGoal(16, new NearestAttackableTargetGoal(this, ParasaurolophusEntity.class, false, false)); + this.targetSelector.addGoal(17, new NearestAttackableTargetGoal(this, VelociraptorEntity.class, false, false)); + this.targetSelector.addGoal(18, new NearestAttackableTargetGoal(this, SpinosaurusEntity.class, false, false)); + this.targetSelector.addGoal(19, new NearestAttackableTargetGoal(this, IndominusRexEntity.class, false, false)); + this.targetSelector.addGoal(20, new NearestAttackableTargetGoal(this, Player.class, false, false)); + this.goalSelector.addGoal(21, new RandomLookAroundGoal(this)); + + + } + + public static AttributeSupplier.Builder createAttributes() { + return Animal.createLivingAttributes() + .add(Attributes.MAX_HEALTH, 45D) + .add(Attributes.MOVEMENT_SPEED, 0.3D) + .add(Attributes.ARMOR, 0D) + .add(Attributes.FOLLOW_RANGE, 32D) + .add(Attributes.KNOCKBACK_RESISTANCE, 0D) + .add(Attributes.ATTACK_KNOCKBACK, 0D) + .add(Attributes.ATTACK_DAMAGE, 14D); + } + + @Override + public boolean isFood(ItemStack pStack) { + return pStack.is(Items.BEEF); + } + + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel pLevel, AgeableMob pOtherParent) { + AgeableMob child = ModEntities.RUGOPS.get().create(pLevel); + if (child instanceof RugopsEntity baby) { + RugopsVariant randomVariant = Util.getRandom(RugopsVariant.values(), this.random); + baby.setVariant(randomVariant); + } + return child; + } + + @Override + public boolean doHurtTarget(Entity target) { + boolean hit = super.doHurtTarget(target); + if (!level().isClientSide && hit && target instanceof LivingEntity) { + if (this.level() instanceof ServerLevel serverLevel) { + this.triggerAnim("attackController", "attack"); + this.playSound(ModSounds.MEDIUM_THEROPOD_ATTACK.get(), 1.0F, 1.0F); + } + } + return hit; + } + + @Override + public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { + controllers.add(new AnimationController<>(this, "Walk/Run/Idle", state -> { + if (state.isMoving()) + return state.setAndContinue(RugopsEntity.this.isSprinting() ? RawAnimation.begin().then("anim.rugops.run", Animation.LoopType.LOOP) : RawAnimation.begin().then("anim.rugops.walk", Animation.LoopType.LOOP)); + + return state.setAndContinue(RawAnimation.begin().then("anim.rugops.idle", Animation.LoopType.LOOP)); + })); + + controllers.add(new AnimationController<>(this, "attackController", state -> PlayState.STOP) + .triggerableAnim("attack", RawAnimation.begin().then("anim.rugops.attack", Animation.LoopType.PLAY_ONCE))); + + controllers.add(new AnimationController<>(this, "mouthController", state -> PlayState.STOP) + .triggerableAnim("mouth", RawAnimation.begin().then("anim.rugops.mouth", Animation.LoopType.PLAY_ONCE))); + } + + private float getSignedTurnDelta() { + // Only consider the body (torso) rotation so head look does not affect tail sway + return Mth.wrapDegrees(this.yBodyRot - this.yBodyRotO); + } + + private int mouthAnimCooldown = 0; + + @Override + public void tick() { + super.tick(); + if (!level().isClientSide) { + this.entityData.set(DATA_SYNCED_AGE, this.getAge()); + var maxHealthAttr = getAttribute(Attributes.MAX_HEALTH); + if (maxHealthAttr != null) { + double baseAdult = DefaultAttributes.getSupplier((EntityType) this.getType()).getValue(Attributes.MAX_HEALTH); + double desired = this.isBaby() ? baseAdult * 0.10D : baseAdult; + if (maxHealthAttr.getBaseValue() != desired) { + double oldMax = maxHealthAttr.getBaseValue(); + double healthRatio = this.getHealth() / (float) oldMax; + maxHealthAttr.setBaseValue(desired); + this.setHealth((float) (desired * Mth.clamp(healthRatio, 0.0F, 1.0F))); + } + } + } + + if (!level().isClientSide) { + if (mouthAnimCooldown > 0) { + mouthAnimCooldown--; + } else { + this.triggerAnim("mouthController", "mouth"); + this.playSound(ModSounds.RUGOPS_CALL.get(), 1.0F, 1.0F); + // 30s–60s in ticks + mouthAnimCooldown = this.random.nextInt(1200 - 600 + 1) + 600; + } + } + + if (level().isClientSide) { + // Capture previous for smooth interpolation between ticks + this.tailSwayPrev = this.tailSwayOffset; + updateProceduralTailSway(); + } + } + + private void updateProceduralTailSway() { + // Turn input derived from rotation deltas; works even when standing still and turning + float turnDegrees = getSignedTurnDelta(); + + // Deadzone to ignore tiny jitter so the tail can return to center cleanly + float deadzoneDeg = 0.6f; // smaller deadzone for more responsiveness + float turnInput = 0.0f; + if (Math.abs(turnDegrees) >= deadzoneDeg) { + // Higher sensitivity so small in-place turns still affect the model + turnInput = Mth.clamp(turnDegrees / 15.0f, -1.0f, 1.0f); + } + + // Target offset: keep intuitive sign (positive input -> positive sway) + float target = turnInput; + + // One-pole low-pass (no bounce). Larger alpha => snappier and less "stiff". + float alpha = 0.24f; // try 0.20–0.30 to taste + + tailSwayOffset += (target - tailSwayOffset) * alpha; + + // Snap tiny residuals to zero so it visibly settles + if (Math.abs(tailSwayOffset) < 0.003f) { + tailSwayOffset = 0.0f; + } + + // No oscillation velocity retained + tailSwayVelocity = 0.0f; + + tailSwayOffset = Mth.clamp(tailSwayOffset, -1.5f, 1.5f); + } + + // Expose to the model for bone rotation + public float getTailSwayOffset() { + return tailSwayOffset; + } + + // Interpolated sway for smooth rendering between ticks + public float getTailSwayOffset(float partialTick) { + return Mth.lerp(Mth.clamp(partialTick, 0.0f, 1.0f), tailSwayPrev, tailSwayOffset); + } + + /*? if <=1.20.1 {*/ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); + this.entityData.define(VARIANT, 0); + this.entityData.define(DATA_SYNCED_AGE, 0); + } + /*?} else {*/ + /*@Override + protected void defineSynchedData(SynchedEntityData.Builder pBuilder) { + super.defineSynchedData(pBuilder); + pBuilder.define(VARIANT, 0); + pBuilder.define(DATA_SYNCED_AGE, 0); + } + *//*?}*/ + + public int getSyncedAge() { + return this.entityData.get(DATA_SYNCED_AGE); + } + public int getTypeVariant() { + return this.entityData.get(VARIANT); + } + + public RugopsVariant getVariant() { + return RugopsVariant.byId(this.getTypeVariant() & 255); + } + + private void setVariant(RugopsVariant variant) { + this.entityData.set(VARIANT, variant.getId() & 255); + } + + @Override + public boolean canMate(Animal other) { + if (!super.canMate(other)) return false; + if (!(other instanceof RugopsEntity that)) return false; + return this.getVariant() != that.getVariant(); + } + @Override + public void addAdditionalSaveData(CompoundTag pCompound) { + super.addAdditionalSaveData(pCompound); + pCompound.putInt("Variant", this.getTypeVariant()); + } + + @Override + public void readAdditionalSaveData(CompoundTag pCompound) { + super.readAdditionalSaveData(pCompound); + this.entityData.set(VARIANT, pCompound.getInt("Variant")); + } + + /*? if <=1.20.1 {*/ + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor pLevel, DifficultyInstance pDifficulty, MobSpawnType pReason, @Nullable SpawnGroupData pSpawnData, @Nullable CompoundTag pDataTag) { + RugopsVariant variant = Util.getRandom(RugopsVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(pLevel, pDifficulty, pReason, pSpawnData, pDataTag); + } + /*?} else {*/ + /*@Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, MobSpawnType spawnType, @Nullable SpawnGroupData spawnGroupData) { + RugopsVariant variant = Util.getRandom(RugopsVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(level, difficulty, spawnType, spawnGroupData); + } + *//*?}*/ + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } + + @Override + protected @Nullable SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.RUGOPS_HURT.get(); + } + + @Override + protected @Nullable SoundEvent getDeathSound() { + return ModSounds.RUGOPS_DEATH.get(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/custom/SeatEntity.java b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/SeatEntity.java new file mode 100644 index 0000000..91f2372 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/SeatEntity.java @@ -0,0 +1,134 @@ +package net.cmr.jurassicrevived.entity.custom; + +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; + +public class SeatEntity extends Entity { + + public SeatEntity(EntityType type, Level level) { + super(type, level); + this.noPhysics = true; + this.noCulling = true; + this.setInvulnerable(true); + } + + /*? if <=1.20.1 {*/ + @Override + protected void defineSynchedData() { + // no data + } + /*?} else {*/ + /*@Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + // no data + } + *//*?}*/ + + @Override + protected void readAdditionalSaveData(CompoundTag tag) {} + + @Override + protected void addAdditionalSaveData(CompoundTag tag) {} + +private java.util.UUID mountNextTick; +private int aliveTicks = 0; + +public void scheduleMount(java.util.UUID rider) { + this.mountNextTick = rider; + this.aliveTicks = 0; // reset so delay counts from spawn/schedule +} + + @Override + public void tick() { + setDeltaMovement(Vec3.ZERO); + aliveTicks++; + + if (!level().isClientSide) { + // Delay mounting by ~2 ticks so clients have seat pose + if (mountNextTick != null && aliveTicks >= 2) { + Entity e = ((net.minecraft.server.level.ServerLevel) level()).getEntity(mountNextTick); + if (e instanceof net.minecraft.world.entity.player.Player p && !p.isPassenger()) { + p.startRiding(this, true); + p.setYRot(this.getYRot()); + p.setYHeadRot(this.getYRot()); + p.setXRot(0.0f); + } + mountNextTick = null; + } + + if (!this.getPassengers().isEmpty()) { + Entity rider = this.getPassengers().get(0); + rider.setDeltaMovement(Vec3.ZERO); + rider.setYRot(this.getYRot()); + rider.setYHeadRot(this.getYRot()); + if (rider instanceof net.minecraft.world.entity.player.Player p) p.setSprinting(false); + } else if (aliveTicks > 10) { // keep a bit longer due to delayed mount + discard(); + return; + } + } + + // Reassert pose both sides + this.setPos(this.getX(), this.getY(), this.getZ()); + this.setYRot(this.getYRot()); + this.setYHeadRot(this.getYRot()); + + super.tick(); +} + +/*? if <=1.20.1 {*/ + @Override + protected boolean canAddPassenger(Entity passenger) { + // Single rider only + return this.getPassengers().isEmpty(); + } + + @Override + protected void positionRider(Entity passenger, Entity.MoveFunction moveFunc) { + // Base attach offset relative to seat origin + Vec3 attach = new Vec3(0.0, -0.6, 0.0); + + // Compute absolute target position + double x = this.getX() + attach.x; + double y = this.getY() + attach.y; + double z = this.getZ() + attach.z; + + // Apply position via provided move function (server and client) + moveFunc.accept(passenger, x, y, z); + + // Orientation and motion constraints + passenger.setDeltaMovement(Vec3.ZERO); + + if (passenger instanceof Player p) { + p.setSprinting(false); + } + } + /*?} else {*/ +/*@Override +public Vec3 getPassengerRidingPosition(Entity passenger) { + return new Vec3(0.0, -0.6, 0.0); +} + +@Override +public void onPassengerTurned(Entity passenger) { + Vec3 attach = getPassengerRidingPosition(passenger); + double x = this.getX() + attach.x; + double y = this.getY() + attach.y; + double z = this.getZ() + attach.z; + if (!level().isClientSide) { + passenger.setPos(x, y, z); + passenger.setDeltaMovement(Vec3.ZERO); + passenger.setYRot(this.getYRot()); + passenger.setYHeadRot(this.getYRot()); + passenger.setXRot(0.0f); + } else { + passenger.setPos(x, y, z); + } +} +*//*?}*/ +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/custom/SegisaurusEntity.java b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/SegisaurusEntity.java new file mode 100644 index 0000000..26cbb0f --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/SegisaurusEntity.java @@ -0,0 +1,313 @@ +package net.cmr.jurassicrevived.entity.custom; + +import net.cmr.jurassicrevived.entity.ModEntities; +import net.cmr.jurassicrevived.entity.ai.SprintingMeleeAttackGoal; +import net.cmr.jurassicrevived.entity.ai.SprintingPanicGoal; +import net.cmr.jurassicrevived.entity.client.AlbertosaurusVariant; +import net.cmr.jurassicrevived.entity.client.SegisaurusVariant; +import net.cmr.jurassicrevived.sound.ModSounds; +import net.minecraft.Util; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.util.Mth; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.*; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.attributes.DefaultAttributes; +import net.minecraft.world.entity.ai.goal.*; +import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal; +import net.minecraft.world.entity.ai.goal.target.NearestAttackableTargetGoal; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerLevelAccessor; +import org.jetbrains.annotations.Nullable; +import software.bernie.geckolib.animatable.GeoEntity; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.core.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.core.animation.AnimatableManager; +import software.bernie.geckolib.core.animation.Animation; +import software.bernie.geckolib.core.animation.AnimationController; +import software.bernie.geckolib.core.animation.RawAnimation; +import software.bernie.geckolib.core.object.PlayState; +/*?} else {*/ +/*import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.animation.*; +*//*?}*/ + +public class SegisaurusEntity extends Animal implements GeoEntity { + private AnimatableInstanceCache cache = new SingletonAnimatableInstanceCache(this); + + private static final EntityDataAccessor VARIANT = + SynchedEntityData.defineId(SegisaurusEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SYNCED_AGE = + SynchedEntityData.defineId(SegisaurusEntity.class, EntityDataSerializers.INT); + + // Procedural tail sway state (client-side use for rendering) + private float tailSwayOffset; // Smoothed offset in range roughly [-1, 1] + private float tailSwayVelocity; // Internal velocity for spring-damper + private float tailSwayPrev; // Previous frame value for interpolation + + public SegisaurusEntity(EntityType pEntityType, Level pLevel) { + super(pEntityType, pLevel); + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new SprintingPanicGoal(this, 1.15) { + @Override + public boolean canUse() { + return SegisaurusEntity.this.isBaby() && super.canUse(); + } + }); + this.targetSelector.addGoal(1, new HurtByTargetGoal(this).setAlertOthers()); + this.goalSelector.addGoal(2, new FloatGoal(this)); + this.goalSelector.addGoal(3, new AvoidEntityGoal<>(this, SpinosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(4, new AvoidEntityGoal<>(this, TyrannosaurusRexEntity.class, (float) 20, 1.2, 1.2)); + this.goalSelector.addGoal(5, new AvoidEntityGoal<>(this, VelociraptorEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(6, new AvoidEntityGoal<>(this, ParasaurolophusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(7, new AvoidEntityGoal<>(this, CeratosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(8, new AvoidEntityGoal<>(this, TriceratopsEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(9, new AvoidEntityGoal<>(this, BrachiosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(10, new SprintingMeleeAttackGoal(this, 1.1, false)); + this.goalSelector.addGoal(11, new BreedGoal(this, 1.0)); + this.goalSelector.addGoal(12, new FollowParentGoal(this, 1.25)); + this.goalSelector.addGoal(13, new WaterAvoidingRandomStrollGoal(this, 1.0)); + this.goalSelector.addGoal(14, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(15, new FollowMobGoal(this, 1, (float) 20, (float) 10)); + this.targetSelector.addGoal(16, new NearestAttackableTargetGoal<>(this, Animal.class, 10, false, false, + target -> target.getType() != this.getType())); + this.targetSelector.addGoal(17, new NearestAttackableTargetGoal(this, GallimimusEntity.class, false, false)); + this.targetSelector.addGoal(18, new NearestAttackableTargetGoal(this, Player.class, false, false)); + this.targetSelector.addGoal(19, new NearestAttackableTargetGoal(this, CompsognathusEntity.class, false, false)); + this.goalSelector.addGoal(20, new RandomLookAroundGoal(this)); + } + + public static AttributeSupplier.Builder createAttributes() { + return Animal.createLivingAttributes() + .add(Attributes.MAX_HEALTH, 10D) + .add(Attributes.MOVEMENT_SPEED, 0.3D) + .add(Attributes.ARMOR, 0D) + .add(Attributes.FOLLOW_RANGE, 32D) + .add(Attributes.KNOCKBACK_RESISTANCE, 0.3D) + .add(Attributes.ATTACK_KNOCKBACK, 0D) + .add(Attributes.ATTACK_DAMAGE, 2D); + } + + @Override + public boolean isFood(ItemStack pStack) { + return pStack.is(Items.BEEF); + } + + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel pLevel, AgeableMob pOtherParent) { + AgeableMob child = ModEntities.SEGISAURUS.get().create(pLevel); + if (child instanceof SegisaurusEntity baby) { + SegisaurusVariant randomVariant = Util.getRandom(SegisaurusVariant.values(), this.random); + baby.setVariant(randomVariant); + } + return child; + } + + @Override + public boolean doHurtTarget(Entity target) { + boolean hit = super.doHurtTarget(target); + if (!level().isClientSide && hit && target instanceof LivingEntity) { + if (this.level() instanceof ServerLevel serverLevel) { + this.triggerAnim("attackController", "attack"); + } + } + return hit; + } + + @Override + public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { + controllers.add(new AnimationController<>(this, "Walk/Run/Idle", state -> { + if (state.isMoving()) + return state.setAndContinue(SegisaurusEntity.this.isSprinting() ? RawAnimation.begin().then("anim.segisaurus.run", Animation.LoopType.LOOP) : RawAnimation.begin().then("anim.segisaurus.walk", Animation.LoopType.LOOP)); + + return state.setAndContinue(RawAnimation.begin().then("anim.segisaurus.idle", Animation.LoopType.LOOP)); + })); + + controllers.add(new AnimationController<>(this, "attackController", state -> PlayState.STOP) + .triggerableAnim("attack", RawAnimation.begin().then("anim.segisaurus.attack", Animation.LoopType.PLAY_ONCE))); + + controllers.add(new AnimationController<>(this, "mouthController", state -> PlayState.STOP) + .triggerableAnim("mouth", RawAnimation.begin().then("anim.segisaurus.mouth", Animation.LoopType.PLAY_ONCE))); + } + + private float getSignedTurnDelta() { + // Only consider the body (torso) rotation so head look does not affect tail sway + return Mth.wrapDegrees(this.yBodyRot - this.yBodyRotO); + } + + private int mouthAnimCooldown = 0; + + @Override + public void tick() { + super.tick(); + if (!level().isClientSide) { + this.entityData.set(DATA_SYNCED_AGE, this.getAge()); + var maxHealthAttr = getAttribute(Attributes.MAX_HEALTH); + if (maxHealthAttr != null) { + double baseAdult = DefaultAttributes.getSupplier((EntityType) this.getType()).getValue(Attributes.MAX_HEALTH); + double desired = this.isBaby() ? baseAdult * 0.10D : baseAdult; + if (maxHealthAttr.getBaseValue() != desired) { + double oldMax = maxHealthAttr.getBaseValue(); + double healthRatio = this.getHealth() / (float) oldMax; + maxHealthAttr.setBaseValue(desired); + this.setHealth((float) (desired * Mth.clamp(healthRatio, 0.0F, 1.0F))); + } + } + } + + if (!level().isClientSide) { + if (mouthAnimCooldown > 0) { + mouthAnimCooldown--; + } else { + this.triggerAnim("mouthController", "mouth"); + this.playSound(ModSounds.SEGISAURUS_CALL.get(), 1.0F, 1.0F); + // 30s–60s in ticks + mouthAnimCooldown = this.random.nextInt(1200 - 600 + 1) + 600; + } + } + + if (level().isClientSide) { + // Capture previous for smooth interpolation between ticks + this.tailSwayPrev = this.tailSwayOffset; + updateProceduralTailSway(); + } + } + + private void updateProceduralTailSway() { + // Turn input derived from rotation deltas; works even when standing still and turning + float turnDegrees = getSignedTurnDelta(); + + // Deadzone to ignore tiny jitter so the tail can return to center cleanly + float deadzoneDeg = 0.6f; // smaller deadzone for more responsiveness + float turnInput = 0.0f; + if (Math.abs(turnDegrees) >= deadzoneDeg) { + // Higher sensitivity so small in-place turns still affect the model + turnInput = Mth.clamp(turnDegrees / 15.0f, -1.0f, 1.0f); + } + + // Target offset: keep intuitive sign (positive input -> positive sway) + float target = turnInput; + + // One-pole low-pass (no bounce). Larger alpha => snappier and less "stiff". + float alpha = 0.24f; // try 0.20–0.30 to taste + + tailSwayOffset += (target - tailSwayOffset) * alpha; + + // Snap tiny residuals to zero so it visibly settles + if (Math.abs(tailSwayOffset) < 0.003f) { + tailSwayOffset = 0.0f; + } + + // No oscillation velocity retained + tailSwayVelocity = 0.0f; + + tailSwayOffset = Mth.clamp(tailSwayOffset, -1.5f, 1.5f); + } + + // Expose to the model for bone rotation + public float getTailSwayOffset() { + return tailSwayOffset; + } + + // Interpolated sway for smooth rendering between ticks + public float getTailSwayOffset(float partialTick) { + return Mth.lerp(Mth.clamp(partialTick, 0.0f, 1.0f), tailSwayPrev, tailSwayOffset); + } + + /*? if <=1.20.1 {*/ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); + this.entityData.define(VARIANT, 0); + this.entityData.define(DATA_SYNCED_AGE, 0); + } + /*?} else {*/ + /*@Override + protected void defineSynchedData(SynchedEntityData.Builder pBuilder) { + super.defineSynchedData(pBuilder); + pBuilder.define(VARIANT, 0); + pBuilder.define(DATA_SYNCED_AGE, 0); + } + *//*?}*/ + + public int getSyncedAge() { + return this.entityData.get(DATA_SYNCED_AGE); + } + public int getTypeVariant() { + return this.entityData.get(VARIANT); + } + + public SegisaurusVariant getVariant() { + return SegisaurusVariant.byId(this.getTypeVariant() & 255); + } + + private void setVariant(SegisaurusVariant variant) { + this.entityData.set(VARIANT, variant.getId() & 255); + } + + @Override + public boolean canMate(Animal other) { + if (!super.canMate(other)) return false; + if (!(other instanceof SegisaurusEntity that)) return false; + return this.getVariant() != that.getVariant(); + } + @Override + public void addAdditionalSaveData(CompoundTag pCompound) { + super.addAdditionalSaveData(pCompound); + pCompound.putInt("Variant", this.getTypeVariant()); + } + + @Override + public void readAdditionalSaveData(CompoundTag pCompound) { + super.readAdditionalSaveData(pCompound); + this.entityData.set(VARIANT, pCompound.getInt("Variant")); + } + + /*? if <=1.20.1 {*/ + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor pLevel, DifficultyInstance pDifficulty, MobSpawnType pReason, @Nullable SpawnGroupData pSpawnData, @Nullable CompoundTag pDataTag) { + SegisaurusVariant variant = Util.getRandom(SegisaurusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(pLevel, pDifficulty, pReason, pSpawnData, pDataTag); + } + /*?} else {*/ + /*@Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, MobSpawnType spawnType, @Nullable SpawnGroupData spawnGroupData) { + SegisaurusVariant variant = Util.getRandom(SegisaurusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(level, difficulty, spawnType, spawnGroupData); + } + *//*?}*/ + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } + + @Override + protected @Nullable SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.SEGISAURUS_HURT.get(); + } + + @Override + protected @Nullable SoundEvent getDeathSound() { + return ModSounds.SEGISAURUS_DEATH.get(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/custom/ShantungosaurusEntity.java b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/ShantungosaurusEntity.java new file mode 100644 index 0000000..1f6afb8 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/ShantungosaurusEntity.java @@ -0,0 +1,297 @@ +package net.cmr.jurassicrevived.entity.custom; + +import net.cmr.jurassicrevived.entity.ModEntities; +import net.cmr.jurassicrevived.entity.ai.SprintingPanicGoal; +import net.cmr.jurassicrevived.entity.client.AlbertosaurusVariant; +import net.cmr.jurassicrevived.entity.client.ShantungosaurusVariant; +import net.cmr.jurassicrevived.sound.ModSounds; +import net.minecraft.Util; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.util.Mth; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.*; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.attributes.DefaultAttributes; +import net.minecraft.world.entity.ai.goal.*; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerLevelAccessor; +import org.jetbrains.annotations.Nullable; +import software.bernie.geckolib.animatable.GeoEntity; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.core.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.core.animation.AnimatableManager; +import software.bernie.geckolib.core.animation.Animation; +import software.bernie.geckolib.core.animation.AnimationController; +import software.bernie.geckolib.core.animation.RawAnimation; +import software.bernie.geckolib.core.object.PlayState; +/*?} else {*/ +/*import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.animation.*; +*//*?}*/ + +public class ShantungosaurusEntity extends Animal implements GeoEntity { + private AnimatableInstanceCache cache = new SingletonAnimatableInstanceCache(this); + + private static final EntityDataAccessor VARIANT = + SynchedEntityData.defineId(ShantungosaurusEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SYNCED_AGE = + SynchedEntityData.defineId(ShantungosaurusEntity.class, EntityDataSerializers.INT); + + // Procedural tail sway state (client-side use for rendering) + private float tailSwayOffset; // Smoothed offset in range roughly [-1, 1] + private float tailSwayVelocity; // Internal velocity for spring-damper + private float tailSwayPrev; // Previous frame value for interpolation + + public ShantungosaurusEntity(EntityType pEntityType, Level pLevel) { + super(pEntityType, pLevel); + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new SprintingPanicGoal(this, 1.15)); + this.goalSelector.addGoal(2, new FloatGoal(this)); + this.goalSelector.addGoal(3, new BreedGoal(this, 1.0)); + this.goalSelector.addGoal(4, new FollowParentGoal(this, 1.25)); + this.goalSelector.addGoal(5, new WaterAvoidingRandomStrollGoal(this, 1.0)); + this.goalSelector.addGoal(6, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(7, new FollowMobGoal(this, 1, (float) 20, (float) 10)); + this.goalSelector.addGoal(8, new AvoidEntityGoal<>(this, DilophosaurusEntity.class, (float) 20, 1.2, 1.2)); + this.goalSelector.addGoal(9, new AvoidEntityGoal<>(this, VelociraptorEntity.class, (float) 20, 1.2, 1.2)); + this.goalSelector.addGoal(10, new AvoidEntityGoal<>(this, CeratosaurusEntity.class, (float) 20, 1.2, 1.2)); + this.goalSelector.addGoal(11, new AvoidEntityGoal<>(this, SpinosaurusEntity.class, (float) 20, 1.2, 1.2)); + this.goalSelector.addGoal(12, new AvoidEntityGoal<>(this, TyrannosaurusRexEntity.class, (float) 20, 1.2, 1.2)); + this.goalSelector.addGoal(13, new RandomLookAroundGoal(this)); + + } + + public static AttributeSupplier.Builder createAttributes() { + return Animal.createLivingAttributes() + .add(Attributes.MAX_HEALTH, 60D) + .add(Attributes.MOVEMENT_SPEED, 0.3D) + .add(Attributes.ARMOR, 0D) + .add(Attributes.FOLLOW_RANGE, 32D) + .add(Attributes.KNOCKBACK_RESISTANCE, 0D) + .add(Attributes.ATTACK_KNOCKBACK, 0D) + .add(Attributes.ATTACK_DAMAGE, 12D); + } + + @Override + public boolean isFood(ItemStack pStack) { + return pStack.is(Items.KELP); + } + + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel pLevel, AgeableMob pOtherParent) { + AgeableMob child = ModEntities.SHANTUNGOSAURUS.get().create(pLevel); + if (child instanceof ShantungosaurusEntity baby) { + ShantungosaurusVariant randomVariant = Util.getRandom(ShantungosaurusVariant.values(), this.random); + baby.setVariant(randomVariant); + } + return child; + } + + @Override + public boolean doHurtTarget(Entity target) { + boolean hit = super.doHurtTarget(target); + if (!level().isClientSide && hit && target instanceof LivingEntity) { + if (this.level() instanceof ServerLevel serverLevel) { + this.triggerAnim("attackController", "attack"); + } + } + return hit; + } + + @Override + public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { + controllers.add(new AnimationController<>(this, "Walk/Run/Idle", state -> { + if (state.isMoving()) + return state.setAndContinue(ShantungosaurusEntity.this.isSprinting() ? RawAnimation.begin().then("anim.shantungosaurus.run", Animation.LoopType.LOOP) : RawAnimation.begin().then("anim.shantungosaurus.walk", Animation.LoopType.LOOP)); + + return state.setAndContinue(RawAnimation.begin().then("anim.shantungosaurus.idle", Animation.LoopType.LOOP)); + })); + + controllers.add(new AnimationController<>(this, "attackController", state -> PlayState.STOP) + .triggerableAnim("attack", RawAnimation.begin().then("anim.shantungosaurus.attack", Animation.LoopType.PLAY_ONCE))); + + controllers.add(new AnimationController<>(this, "mouthController", state -> PlayState.STOP) + .triggerableAnim("mouth", RawAnimation.begin().then("anim.shantungosaurus.mouth", Animation.LoopType.PLAY_ONCE))); + } + + private float getSignedTurnDelta() { + // Only consider the body (torso) rotation so head look does not affect tail sway + return Mth.wrapDegrees(this.yBodyRot - this.yBodyRotO); + } + + private int mouthAnimCooldown = 0; + + @Override + public void tick() { + super.tick(); + if (!level().isClientSide) { + this.entityData.set(DATA_SYNCED_AGE, this.getAge()); + var maxHealthAttr = getAttribute(Attributes.MAX_HEALTH); + if (maxHealthAttr != null) { + double baseAdult = DefaultAttributes.getSupplier((EntityType) this.getType()).getValue(Attributes.MAX_HEALTH); + double desired = this.isBaby() ? baseAdult * 0.10D : baseAdult; + if (maxHealthAttr.getBaseValue() != desired) { + double oldMax = maxHealthAttr.getBaseValue(); + double healthRatio = this.getHealth() / (float) oldMax; + maxHealthAttr.setBaseValue(desired); + this.setHealth((float) (desired * Mth.clamp(healthRatio, 0.0F, 1.0F))); + } + } + } + + if (!level().isClientSide) { + if (mouthAnimCooldown > 0) { + mouthAnimCooldown--; + } else { + this.triggerAnim("mouthController", "mouth"); + this.playSound(ModSounds.SHANTUNGOSAURUS_CALL.get(), 1.0F, 1.0F); + // 30s–60s in ticks + mouthAnimCooldown = this.random.nextInt(1200 - 600 + 1) + 600; + } + } + + if (level().isClientSide) { + // Capture previous for smooth interpolation between ticks + this.tailSwayPrev = this.tailSwayOffset; + updateProceduralTailSway(); + } + } + + private void updateProceduralTailSway() { + // Turn input derived from rotation deltas; works even when standing still and turning + float turnDegrees = getSignedTurnDelta(); + + // Deadzone to ignore tiny jitter so the tail can return to center cleanly + float deadzoneDeg = 0.6f; // smaller deadzone for more responsiveness + float turnInput = 0.0f; + if (Math.abs(turnDegrees) >= deadzoneDeg) { + // Higher sensitivity so small in-place turns still affect the model + turnInput = Mth.clamp(turnDegrees / 15.0f, -1.0f, 1.0f); + } + + // Target offset: keep intuitive sign (positive input -> positive sway) + float target = turnInput; + + // One-pole low-pass (no bounce). Larger alpha => snappier and less "stiff". + float alpha = 0.24f; // try 0.20–0.30 to taste + + tailSwayOffset += (target - tailSwayOffset) * alpha; + + // Snap tiny residuals to zero so it visibly settles + if (Math.abs(tailSwayOffset) < 0.003f) { + tailSwayOffset = 0.0f; + } + + // No oscillation velocity retained + tailSwayVelocity = 0.0f; + + tailSwayOffset = Mth.clamp(tailSwayOffset, -1.5f, 1.5f); + } + + // Expose to the model for bone rotation + public float getTailSwayOffset() { + return tailSwayOffset; + } + + // Interpolated sway for smooth rendering between ticks + public float getTailSwayOffset(float partialTick) { + return Mth.lerp(Mth.clamp(partialTick, 0.0f, 1.0f), tailSwayPrev, tailSwayOffset); + } + + /*? if <=1.20.1 {*/ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); + this.entityData.define(VARIANT, 0); + this.entityData.define(DATA_SYNCED_AGE, 0); + } + /*?} else {*/ + /*@Override + protected void defineSynchedData(SynchedEntityData.Builder pBuilder) { + super.defineSynchedData(pBuilder); + pBuilder.define(VARIANT, 0); + pBuilder.define(DATA_SYNCED_AGE, 0); + } + *//*?}*/ + + public int getSyncedAge() { + return this.entityData.get(DATA_SYNCED_AGE); + } + public int getTypeVariant() { + return this.entityData.get(VARIANT); + } + + public ShantungosaurusVariant getVariant() { + return ShantungosaurusVariant.byId(this.getTypeVariant() & 255); + } + + private void setVariant(ShantungosaurusVariant variant) { + this.entityData.set(VARIANT, variant.getId() & 255); + } + + @Override + public boolean canMate(Animal other) { + if (!super.canMate(other)) return false; + if (!(other instanceof ShantungosaurusEntity that)) return false; + return this.getVariant() != that.getVariant(); + } + @Override + public void addAdditionalSaveData(CompoundTag pCompound) { + super.addAdditionalSaveData(pCompound); + pCompound.putInt("Variant", this.getTypeVariant()); + } + + @Override + public void readAdditionalSaveData(CompoundTag pCompound) { + super.readAdditionalSaveData(pCompound); + this.entityData.set(VARIANT, pCompound.getInt("Variant")); + } + + /*? if <=1.20.1 {*/ + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor pLevel, DifficultyInstance pDifficulty, MobSpawnType pReason, @Nullable SpawnGroupData pSpawnData, @Nullable CompoundTag pDataTag) { + ShantungosaurusVariant variant = Util.getRandom(ShantungosaurusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(pLevel, pDifficulty, pReason, pSpawnData, pDataTag); + } + /*?} else {*/ + /*@Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, MobSpawnType spawnType, @Nullable SpawnGroupData spawnGroupData) { + ShantungosaurusVariant variant = Util.getRandom(ShantungosaurusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(level, difficulty, spawnType, spawnGroupData); + } + *//*?}*/ + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } + + @Override + protected @Nullable SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.SHANTUNGOSAURUS_HURT.get(); + } + + @Override + protected @Nullable SoundEvent getDeathSound() { + return ModSounds.SHANTUNGOSAURUS_DEATH.get(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/custom/SpinosaurusEntity.java b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/SpinosaurusEntity.java new file mode 100644 index 0000000..c8f4361 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/SpinosaurusEntity.java @@ -0,0 +1,315 @@ +package net.cmr.jurassicrevived.entity.custom; + +import net.cmr.jurassicrevived.entity.ModEntities; +import net.cmr.jurassicrevived.entity.ai.SprintingMeleeAttackGoal; +import net.cmr.jurassicrevived.entity.ai.SprintingPanicGoal; +import net.cmr.jurassicrevived.entity.client.AlbertosaurusVariant; +import net.cmr.jurassicrevived.entity.client.SpinosaurusVariant; +import net.cmr.jurassicrevived.sound.ModSounds; +import net.minecraft.Util; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.util.Mth; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.*; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.attributes.DefaultAttributes; +import net.minecraft.world.entity.ai.goal.*; +import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal; +import net.minecraft.world.entity.ai.goal.target.NearestAttackableTargetGoal; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerLevelAccessor; +import org.jetbrains.annotations.Nullable; +import software.bernie.geckolib.animatable.GeoEntity; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.core.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.core.animation.AnimatableManager; +import software.bernie.geckolib.core.animation.Animation; +import software.bernie.geckolib.core.animation.AnimationController; +import software.bernie.geckolib.core.animation.RawAnimation; +import software.bernie.geckolib.core.object.PlayState; +/*?} else {*/ +/*import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.animation.*; +*//*?}*/ + +public class SpinosaurusEntity extends Animal implements GeoEntity { + private AnimatableInstanceCache cache = new SingletonAnimatableInstanceCache(this); + + private static final EntityDataAccessor VARIANT = + SynchedEntityData.defineId(SpinosaurusEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SYNCED_AGE = + SynchedEntityData.defineId(SpinosaurusEntity.class, EntityDataSerializers.INT); + + // Procedural tail sway state (client-side use for rendering) + private float tailSwayOffset; // Smoothed offset in range roughly [-1, 1] + private float tailSwayVelocity; // Internal velocity for spring-damper + private float tailSwayPrev; // Previous frame value for interpolation + + public SpinosaurusEntity(EntityType pEntityType, Level pLevel) { + super(pEntityType, pLevel); + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new SprintingPanicGoal(this, 1.15) { + @Override + public boolean canUse() { + return SpinosaurusEntity.this.isBaby() && super.canUse(); + } + }); + this.targetSelector.addGoal(1, new HurtByTargetGoal(this).setAlertOthers()); + this.goalSelector.addGoal(2, new FloatGoal(this)); + this.goalSelector.addGoal(3, new AvoidEntityGoal<>(this, BrachiosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(4, new SprintingMeleeAttackGoal(this, 1.1, false)); + this.goalSelector.addGoal(5, new BreedGoal(this, 1.0)); + this.goalSelector.addGoal(6, new FollowParentGoal(this, 1.25)); + this.goalSelector.addGoal(7, new WaterAvoidingRandomStrollGoal(this, 1.0)); + this.goalSelector.addGoal(8, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.targetSelector.addGoal(9, new NearestAttackableTargetGoal<>(this, Animal.class, 10, false, false, + target -> target.getType() != this.getType())); + this.targetSelector.addGoal(10, new NearestAttackableTargetGoal(this, TriceratopsEntity.class, false, false)); + this.targetSelector.addGoal(11, new NearestAttackableTargetGoal(this, GallimimusEntity.class, false, false)); + this.targetSelector.addGoal(12, new NearestAttackableTargetGoal(this, DilophosaurusEntity.class, false, false)); + this.targetSelector.addGoal(13, new NearestAttackableTargetGoal(this, CeratosaurusEntity.class, false, false)); + this.targetSelector.addGoal(14, new NearestAttackableTargetGoal(this, ParasaurolophusEntity.class, false, false)); + this.targetSelector.addGoal(15, new NearestAttackableTargetGoal(this, VelociraptorEntity.class, false, false)); + this.targetSelector.addGoal(16, new NearestAttackableTargetGoal(this, TyrannosaurusRexEntity.class, false, false)); + this.targetSelector.addGoal(17, new NearestAttackableTargetGoal(this, IndominusRexEntity.class, false, false)); + this.targetSelector.addGoal(18, new NearestAttackableTargetGoal(this, Player.class, false, false)); + this.goalSelector.addGoal(19, new RandomLookAroundGoal(this)); + + + } + + public static AttributeSupplier.Builder createAttributes() { + return Animal.createLivingAttributes() + .add(Attributes.MAX_HEALTH, 80D) + .add(Attributes.MOVEMENT_SPEED, 0.3D) + .add(Attributes.ARMOR, 0D) + .add(Attributes.FOLLOW_RANGE, 32D) + .add(Attributes.KNOCKBACK_RESISTANCE, 0D) + .add(Attributes.ATTACK_KNOCKBACK, 0D) + .add(Attributes.ATTACK_DAMAGE, 20D); + } + + @Override + public boolean isFood(ItemStack pStack) { + return pStack.is(Items.BEEF); + } + + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel pLevel, AgeableMob pOtherParent) { + AgeableMob child = ModEntities.SPINOSAURUS.get().create(pLevel); + if (child instanceof SpinosaurusEntity baby) { + SpinosaurusVariant randomVariant = Util.getRandom(SpinosaurusVariant.values(), this.random); + baby.setVariant(randomVariant); + } + return child; + } + + @Override + public boolean doHurtTarget(Entity target) { + boolean hit = super.doHurtTarget(target); + if (!level().isClientSide && hit && target instanceof LivingEntity) { + if (this.level() instanceof ServerLevel serverLevel) { + this.triggerAnim("attackController", "attack"); + this.playSound(ModSounds.SPINOSAURUS_ATTACK.get(), 1.0F, 1.0F); + } + } + return hit; + } + + @Override + public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { + controllers.add(new AnimationController<>(this, "Walk/Run/Idle", state -> { + if (state.isMoving()) + return state.setAndContinue(SpinosaurusEntity.this.isSprinting() ? RawAnimation.begin().then("anim.spinosaurus.run", Animation.LoopType.LOOP) : RawAnimation.begin().then("anim.spinosaurus.walk", Animation.LoopType.LOOP)); + + return state.setAndContinue(RawAnimation.begin().then("anim.spinosaurus.idle", Animation.LoopType.LOOP)); + })); + + controllers.add(new AnimationController<>(this, "attackController", state -> PlayState.STOP) + .triggerableAnim("attack", RawAnimation.begin().then("anim.spinosaurus.attack", Animation.LoopType.PLAY_ONCE))); + + controllers.add(new AnimationController<>(this, "mouthController", state -> PlayState.STOP) + .triggerableAnim("mouth", RawAnimation.begin().then("anim.spinosaurus.mouth", Animation.LoopType.PLAY_ONCE))); + } + + private float getSignedTurnDelta() { + // Only consider the body (torso) rotation so head look does not affect tail sway + return Mth.wrapDegrees(this.yBodyRot - this.yBodyRotO); + } + + private int mouthAnimCooldown = 0; + + @Override + public void tick() { + super.tick(); + if (!level().isClientSide) { + this.entityData.set(DATA_SYNCED_AGE, this.getAge()); + var maxHealthAttr = getAttribute(Attributes.MAX_HEALTH); + if (maxHealthAttr != null) { + double baseAdult = DefaultAttributes.getSupplier((EntityType) this.getType()).getValue(Attributes.MAX_HEALTH); + double desired = this.isBaby() ? baseAdult * 0.10D : baseAdult; + if (maxHealthAttr.getBaseValue() != desired) { + double oldMax = maxHealthAttr.getBaseValue(); + double healthRatio = this.getHealth() / (float) oldMax; + maxHealthAttr.setBaseValue(desired); + this.setHealth((float) (desired * Mth.clamp(healthRatio, 0.0F, 1.0F))); + } + } + } + + if (!level().isClientSide) { + if (mouthAnimCooldown > 0) { + mouthAnimCooldown--; + } else { + this.triggerAnim("mouthController", "mouth"); + this.playSound(ModSounds.SPINOSAURUS_CALL.get(), 1.0F, 1.0F); + // 30s–60s in ticks + mouthAnimCooldown = this.random.nextInt(1200 - 600 + 1) + 600; + } + } + + if (level().isClientSide) { + // Capture previous for smooth interpolation between ticks + this.tailSwayPrev = this.tailSwayOffset; + updateProceduralTailSway(); + } + } + + private void updateProceduralTailSway() { + // Turn input derived from rotation deltas; works even when standing still and turning + float turnDegrees = getSignedTurnDelta(); + + // Deadzone to ignore tiny jitter so the tail can return to center cleanly + float deadzoneDeg = 0.6f; // smaller deadzone for more responsiveness + float turnInput = 0.0f; + if (Math.abs(turnDegrees) >= deadzoneDeg) { + // Higher sensitivity so small in-place turns still affect the model + turnInput = Mth.clamp(turnDegrees / 15.0f, -1.0f, 1.0f); + } + + // Target offset: keep intuitive sign (positive input -> positive sway) + float target = turnInput; + + // One-pole low-pass (no bounce). Larger alpha => snappier and less "stiff". + float alpha = 0.24f; // try 0.20–0.30 to taste + + tailSwayOffset += (target - tailSwayOffset) * alpha; + + // Snap tiny residuals to zero so it visibly settles + if (Math.abs(tailSwayOffset) < 0.003f) { + tailSwayOffset = 0.0f; + } + + // No oscillation velocity retained + tailSwayVelocity = 0.0f; + + tailSwayOffset = Mth.clamp(tailSwayOffset, -1.5f, 1.5f); + } + + // Expose to the model for bone rotation + public float getTailSwayOffset() { + return tailSwayOffset; + } + + // Interpolated sway for smooth rendering between ticks + public float getTailSwayOffset(float partialTick) { + return Mth.lerp(Mth.clamp(partialTick, 0.0f, 1.0f), tailSwayPrev, tailSwayOffset); + } + + /*? if <=1.20.1 {*/ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); + this.entityData.define(VARIANT, 0); + this.entityData.define(DATA_SYNCED_AGE, 0); + } + /*?} else {*/ + /*@Override + protected void defineSynchedData(SynchedEntityData.Builder pBuilder) { + super.defineSynchedData(pBuilder); + pBuilder.define(VARIANT, 0); + pBuilder.define(DATA_SYNCED_AGE, 0); + } + *//*?}*/ + + public int getSyncedAge() { + return this.entityData.get(DATA_SYNCED_AGE); + } + public int getTypeVariant() { + return this.entityData.get(VARIANT); + } + + public SpinosaurusVariant getVariant() { + return SpinosaurusVariant.byId(this.getTypeVariant() & 255); + } + + private void setVariant(SpinosaurusVariant variant) { + this.entityData.set(VARIANT, variant.getId() & 255); + } + + @Override + public boolean canMate(Animal other) { + if (!super.canMate(other)) return false; + if (!(other instanceof SpinosaurusEntity that)) return false; + return this.getVariant() != that.getVariant(); + } + @Override + public void addAdditionalSaveData(CompoundTag pCompound) { + super.addAdditionalSaveData(pCompound); + pCompound.putInt("Variant", this.getTypeVariant()); + } + + @Override + public void readAdditionalSaveData(CompoundTag pCompound) { + super.readAdditionalSaveData(pCompound); + this.entityData.set(VARIANT, pCompound.getInt("Variant")); + } + + /*? if <=1.20.1 {*/ + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor pLevel, DifficultyInstance pDifficulty, MobSpawnType pReason, @Nullable SpawnGroupData pSpawnData, @Nullable CompoundTag pDataTag) { + SpinosaurusVariant variant = Util.getRandom(SpinosaurusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(pLevel, pDifficulty, pReason, pSpawnData, pDataTag); + } + /*?} else {*/ + /*@Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, MobSpawnType spawnType, @Nullable SpawnGroupData spawnGroupData) { + SpinosaurusVariant variant = Util.getRandom(SpinosaurusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(level, difficulty, spawnType, spawnGroupData); + } + *//*?}*/ + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } + + @Override + protected @Nullable SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.SPINOSAURUS_HURT.get(); + } + + @Override + protected @Nullable SoundEvent getDeathSound() { + return ModSounds.SPINOSAURUS_DEATH.get(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/custom/StegosaurusEntity.java b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/StegosaurusEntity.java new file mode 100644 index 0000000..2a3ab6f --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/StegosaurusEntity.java @@ -0,0 +1,309 @@ +package net.cmr.jurassicrevived.entity.custom; + +import net.cmr.jurassicrevived.entity.ModEntities; +import net.cmr.jurassicrevived.entity.ai.SprintingMeleeAttackGoal; +import net.cmr.jurassicrevived.entity.ai.SprintingPanicGoal; +import net.cmr.jurassicrevived.entity.client.AlbertosaurusVariant; +import net.cmr.jurassicrevived.entity.client.StegosaurusVariant; +import net.cmr.jurassicrevived.sound.ModSounds; +import net.minecraft.Util; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.util.Mth; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.*; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.attributes.DefaultAttributes; +import net.minecraft.world.entity.ai.goal.*; +import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerLevelAccessor; +import org.jetbrains.annotations.Nullable; +import software.bernie.geckolib.animatable.GeoEntity; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.core.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.core.animation.AnimatableManager; +import software.bernie.geckolib.core.animation.Animation; +import software.bernie.geckolib.core.animation.AnimationController; +import software.bernie.geckolib.core.animation.RawAnimation; +import software.bernie.geckolib.core.object.PlayState; +/*?} else {*/ +/*import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.animation.*; +*//*?}*/ + +public class StegosaurusEntity extends Animal implements GeoEntity { + private AnimatableInstanceCache cache = new SingletonAnimatableInstanceCache(this); + + private static final EntityDataAccessor VARIANT = + SynchedEntityData.defineId(StegosaurusEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SYNCED_AGE = + SynchedEntityData.defineId(StegosaurusEntity.class, EntityDataSerializers.INT); + + // Procedural tail sway state (client-side use for rendering) + private float tailSwayOffset; // Smoothed offset in range roughly [-1, 1] + private float tailSwayVelocity; // Internal velocity for spring-damper + private float tailSwayPrev; // Previous frame value for interpolation + + public StegosaurusEntity(EntityType pEntityType, Level pLevel) { + super(pEntityType, pLevel); + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new SprintingPanicGoal(this, 1.15) { + @Override + public boolean canUse() { + return StegosaurusEntity.this.isBaby() && super.canUse(); + } + }); + this.targetSelector.addGoal(1, new HurtByTargetGoal(this).setAlertOthers()); + this.goalSelector.addGoal(2, new FloatGoal(this)); + this.goalSelector.addGoal(3, new AvoidEntityGoal<>(this, VelociraptorEntity.class, (float) 20, 0.8, 0.8)); + this.goalSelector.addGoal(4, new AvoidEntityGoal<>(this, CeratosaurusEntity.class, (float) 20, 0.8, 0.8)); + this.goalSelector.addGoal(5, new AvoidEntityGoal<>(this, DilophosaurusEntity.class, (float) 20, 0.8, 0.8)); + this.goalSelector.addGoal(6, new SprintingMeleeAttackGoal(this, 1.1, false)); + this.goalSelector.addGoal(7, new BreedGoal(this, 1.0)); + this.goalSelector.addGoal(8, new FollowParentGoal(this, 1.25)); + this.goalSelector.addGoal(8, new WaterAvoidingRandomStrollGoal(this, 1.0)); + this.goalSelector.addGoal(10, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(11, new FollowMobGoal(this, 0.8, (float) 20, (float) 10)); + this.goalSelector.addGoal(12, new AvoidEntityGoal<>(this, SpinosaurusEntity.class, (float) 20, 0.8, 0.8)); + this.goalSelector.addGoal(13, new AvoidEntityGoal<>(this, TyrannosaurusRexEntity.class, (float) 20, 0.8, 0.8)); + this.goalSelector.addGoal(14, new AvoidEntityGoal<>(this, IndominusRexEntity.class, (float) 20, 0.8, 0.8)); + this.goalSelector.addGoal(15, new EatBlockGoal(this)); + this.goalSelector.addGoal(16, new RandomLookAroundGoal(this)); + + } + + public static AttributeSupplier.Builder createAttributes() { + return Animal.createLivingAttributes() + .add(Attributes.MAX_HEALTH, 65D) + .add(Attributes.MOVEMENT_SPEED, 0.3D) + .add(Attributes.ARMOR, 0D) + .add(Attributes.FOLLOW_RANGE, 32D) + .add(Attributes.KNOCKBACK_RESISTANCE, 0D) + .add(Attributes.ATTACK_KNOCKBACK, 0D) + .add(Attributes.ATTACK_DAMAGE, 16D); + } + + @Override + public boolean isFood(ItemStack pStack) { + return pStack.is(Items.KELP); + } + + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel pLevel, AgeableMob pOtherParent) { + AgeableMob child = ModEntities.STEGOSAURUS.get().create(pLevel); + if (child instanceof StegosaurusEntity baby) { + StegosaurusVariant randomVariant = Util.getRandom(StegosaurusVariant.values(), this.random); + baby.setVariant(randomVariant); + } + return child; + } + + @Override + public boolean doHurtTarget(Entity target) { + boolean hit = super.doHurtTarget(target); + if (!level().isClientSide && hit && target instanceof LivingEntity) { + if (this.level() instanceof ServerLevel serverLevel) { + this.triggerAnim("attackController", "attack"); + this.playSound(ModSounds.TAIL_STRIKE.get(), 1.0F, 1.0F); + } + } + return hit; + } + + @Override + public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { + controllers.add(new AnimationController<>(this, "Walk/Run/Idle", state -> { + if (state.isMoving()) + return state.setAndContinue(StegosaurusEntity.this.isSprinting() ? RawAnimation.begin().then("anim.stegosaurus.run", Animation.LoopType.LOOP) : RawAnimation.begin().then("anim.stegosaurus.walk", Animation.LoopType.LOOP)); + + return state.setAndContinue(RawAnimation.begin().then("anim.stegosaurus.idle", Animation.LoopType.LOOP)); + })); + + controllers.add(new AnimationController<>(this, "attackController", state -> PlayState.STOP) + .triggerableAnim("attack", RawAnimation.begin().then("anim.stegosaurus.attack", Animation.LoopType.PLAY_ONCE))); + + controllers.add(new AnimationController<>(this, "mouthController", state -> PlayState.STOP) + .triggerableAnim("mouth", RawAnimation.begin().then("anim.stegosaurus.mouth", Animation.LoopType.PLAY_ONCE))); + } + + private float getSignedTurnDelta() { + // Only consider the body (torso) rotation so head look does not affect tail sway + return Mth.wrapDegrees(this.yBodyRot - this.yBodyRotO); + } + + private int mouthAnimCooldown = 0; + + @Override + public void tick() { + super.tick(); + if (!level().isClientSide) { + this.entityData.set(DATA_SYNCED_AGE, this.getAge()); + var maxHealthAttr = getAttribute(Attributes.MAX_HEALTH); + if (maxHealthAttr != null) { + double baseAdult = DefaultAttributes.getSupplier((EntityType) this.getType()).getValue(Attributes.MAX_HEALTH); + double desired = this.isBaby() ? baseAdult * 0.10D : baseAdult; + if (maxHealthAttr.getBaseValue() != desired) { + double oldMax = maxHealthAttr.getBaseValue(); + double healthRatio = this.getHealth() / (float) oldMax; + maxHealthAttr.setBaseValue(desired); + this.setHealth((float) (desired * Mth.clamp(healthRatio, 0.0F, 1.0F))); + } + } + } + + if (!level().isClientSide) { + if (mouthAnimCooldown > 0) { + mouthAnimCooldown--; + } else { + this.triggerAnim("mouthController", "mouth"); + this.playSound(ModSounds.STEGOSAURUS_CALL.get(), 1.0F, 1.0F); + // 30s–60s in ticks + mouthAnimCooldown = this.random.nextInt(1200 - 600 + 1) + 600; + } + } + + if (level().isClientSide) { + // Capture previous for smooth interpolation between ticks + this.tailSwayPrev = this.tailSwayOffset; + updateProceduralTailSway(); + } + } + + private void updateProceduralTailSway() { + // Turn input derived from rotation deltas; works even when standing still and turning + float turnDegrees = getSignedTurnDelta(); + + // Deadzone to ignore tiny jitter so the tail can return to center cleanly + float deadzoneDeg = 0.6f; // smaller deadzone for more responsiveness + float turnInput = 0.0f; + if (Math.abs(turnDegrees) >= deadzoneDeg) { + // Higher sensitivity so small in-place turns still affect the model + turnInput = Mth.clamp(turnDegrees / 15.0f, -1.0f, 1.0f); + } + + // Target offset: keep intuitive sign (positive input -> positive sway) + float target = turnInput; + + // One-pole low-pass (no bounce). Larger alpha => snappier and less "stiff". + float alpha = 0.24f; // try 0.20–0.30 to taste + + tailSwayOffset += (target - tailSwayOffset) * alpha; + + // Snap tiny residuals to zero so it visibly settles + if (Math.abs(tailSwayOffset) < 0.003f) { + tailSwayOffset = 0.0f; + } + + // No oscillation velocity retained + tailSwayVelocity = 0.0f; + + tailSwayOffset = Mth.clamp(tailSwayOffset, -1.5f, 1.5f); + } + + // Expose to the model for bone rotation + public float getTailSwayOffset() { + return tailSwayOffset; + } + + // Interpolated sway for smooth rendering between ticks + public float getTailSwayOffset(float partialTick) { + return Mth.lerp(Mth.clamp(partialTick, 0.0f, 1.0f), tailSwayPrev, tailSwayOffset); + } + + /*? if <=1.20.1 {*/ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); + this.entityData.define(VARIANT, 0); + this.entityData.define(DATA_SYNCED_AGE, 0); + } + /*?} else {*/ + /*@Override + protected void defineSynchedData(SynchedEntityData.Builder pBuilder) { + super.defineSynchedData(pBuilder); + pBuilder.define(VARIANT, 0); + pBuilder.define(DATA_SYNCED_AGE, 0); + } + *//*?}*/ + + public int getSyncedAge() { + return this.entityData.get(DATA_SYNCED_AGE); + } + public int getTypeVariant() { + return this.entityData.get(VARIANT); + } + + public StegosaurusVariant getVariant() { + return StegosaurusVariant.byId(this.getTypeVariant() & 255); + } + + private void setVariant(StegosaurusVariant variant) { + this.entityData.set(VARIANT, variant.getId() & 255); + } + + @Override + public boolean canMate(Animal other) { + if (!super.canMate(other)) return false; + if (!(other instanceof StegosaurusEntity that)) return false; + return this.getVariant() != that.getVariant(); + } + @Override + public void addAdditionalSaveData(CompoundTag pCompound) { + super.addAdditionalSaveData(pCompound); + pCompound.putInt("Variant", this.getTypeVariant()); + } + + @Override + public void readAdditionalSaveData(CompoundTag pCompound) { + super.readAdditionalSaveData(pCompound); + this.entityData.set(VARIANT, pCompound.getInt("Variant")); + } + + /*? if <=1.20.1 {*/ + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor pLevel, DifficultyInstance pDifficulty, MobSpawnType pReason, @Nullable SpawnGroupData pSpawnData, @Nullable CompoundTag pDataTag) { + StegosaurusVariant variant = Util.getRandom(StegosaurusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(pLevel, pDifficulty, pReason, pSpawnData, pDataTag); + } + /*?} else {*/ + /*@Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, MobSpawnType spawnType, @Nullable SpawnGroupData spawnGroupData) { + StegosaurusVariant variant = Util.getRandom(StegosaurusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(level, difficulty, spawnType, spawnGroupData); + } + *//*?}*/ + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } + + @Override + protected @Nullable SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.STEGOSAURUS_HURT.get(); + } + + @Override + protected @Nullable SoundEvent getDeathSound() { + return ModSounds.STEGOSAURUS_DEATH.get(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/custom/StyracosaurusEntity.java b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/StyracosaurusEntity.java new file mode 100644 index 0000000..06186ef --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/StyracosaurusEntity.java @@ -0,0 +1,308 @@ +package net.cmr.jurassicrevived.entity.custom; + +import net.cmr.jurassicrevived.entity.ModEntities; +import net.cmr.jurassicrevived.entity.ai.SprintingMeleeAttackGoal; +import net.cmr.jurassicrevived.entity.ai.SprintingPanicGoal; +import net.cmr.jurassicrevived.entity.client.AlbertosaurusVariant; +import net.cmr.jurassicrevived.entity.client.StyracosaurusVariant; +import net.cmr.jurassicrevived.sound.ModSounds; +import net.minecraft.Util; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.util.Mth; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.*; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.attributes.DefaultAttributes; +import net.minecraft.world.entity.ai.goal.*; +import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerLevelAccessor; +import org.jetbrains.annotations.Nullable; +import software.bernie.geckolib.animatable.GeoEntity; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.core.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.core.animation.AnimatableManager; +import software.bernie.geckolib.core.animation.Animation; +import software.bernie.geckolib.core.animation.AnimationController; +import software.bernie.geckolib.core.animation.RawAnimation; +import software.bernie.geckolib.core.object.PlayState; +/*?} else {*/ +/*import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.animation.*; +*//*?}*/ + +public class StyracosaurusEntity extends Animal implements GeoEntity { + private AnimatableInstanceCache cache = new SingletonAnimatableInstanceCache(this); + + private static final EntityDataAccessor VARIANT = + SynchedEntityData.defineId(StyracosaurusEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SYNCED_AGE = + SynchedEntityData.defineId(StyracosaurusEntity.class, EntityDataSerializers.INT); + + // Procedural tail sway state (client-side use for rendering) + private float tailSwayOffset; // Smoothed offset in range roughly [-1, 1] + private float tailSwayVelocity; // Internal velocity for spring-damper + private float tailSwayPrev; // Previous frame value for interpolation + + public StyracosaurusEntity(EntityType pEntityType, Level pLevel) { + super(pEntityType, pLevel); + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new SprintingPanicGoal(this, 1.15) { + @Override + public boolean canUse() { + return StyracosaurusEntity.this.isBaby() && super.canUse(); + } + }); + this.targetSelector.addGoal(1, new HurtByTargetGoal(this).setAlertOthers()); + this.goalSelector.addGoal(2, new FloatGoal(this)); + this.goalSelector.addGoal(3, new AvoidEntityGoal<>(this, VelociraptorEntity.class, (float) 20, 0.8, 0.8)); + this.goalSelector.addGoal(4, new AvoidEntityGoal<>(this, CeratosaurusEntity.class, (float) 20, 0.8, 0.8)); + this.goalSelector.addGoal(5, new AvoidEntityGoal<>(this, DilophosaurusEntity.class, (float) 20, 0.8, 0.8)); + this.goalSelector.addGoal(6, new SprintingMeleeAttackGoal(this, 1.1, false)); + this.goalSelector.addGoal(7, new BreedGoal(this, 1.0)); + this.goalSelector.addGoal(8, new FollowParentGoal(this, 1.25)); + this.goalSelector.addGoal(8, new WaterAvoidingRandomStrollGoal(this, 1.0)); + this.goalSelector.addGoal(10, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(11, new FollowMobGoal(this, 0.8, (float) 20, (float) 10)); + this.goalSelector.addGoal(12, new AvoidEntityGoal<>(this, SpinosaurusEntity.class, (float) 20, 0.8, 0.8)); + this.goalSelector.addGoal(13, new AvoidEntityGoal<>(this, TyrannosaurusRexEntity.class, (float) 20, 0.8, 0.8)); + this.goalSelector.addGoal(14, new AvoidEntityGoal<>(this, IndominusRexEntity.class, (float) 20, 0.8, 0.8)); + this.goalSelector.addGoal(15, new EatBlockGoal(this)); + this.goalSelector.addGoal(16, new RandomLookAroundGoal(this)); + + } + + public static AttributeSupplier.Builder createAttributes() { + return Animal.createLivingAttributes() + .add(Attributes.MAX_HEALTH, 50D) + .add(Attributes.MOVEMENT_SPEED, 0.3D) + .add(Attributes.ARMOR, 0D) + .add(Attributes.FOLLOW_RANGE, 32D) + .add(Attributes.KNOCKBACK_RESISTANCE, 0D) + .add(Attributes.ATTACK_KNOCKBACK, 0D) + .add(Attributes.ATTACK_DAMAGE, 14D); + } + + @Override + public boolean isFood(ItemStack pStack) { + return pStack.is(Items.KELP); + } + + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel pLevel, AgeableMob pOtherParent) { + AgeableMob child = ModEntities.STYRACOSAURUS.get().create(pLevel); + if (child instanceof StyracosaurusEntity baby) { + StyracosaurusVariant randomVariant = Util.getRandom(StyracosaurusVariant.values(), this.random); + baby.setVariant(randomVariant); + } + return child; + } + + @Override + public boolean doHurtTarget(Entity target) { + boolean hit = super.doHurtTarget(target); + if (!level().isClientSide && hit && target instanceof LivingEntity) { + if (this.level() instanceof ServerLevel serverLevel) { + this.triggerAnim("attackController", "attack"); + } + } + return hit; + } + + @Override + public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { + controllers.add(new AnimationController<>(this, "Walk/Run/Idle", state -> { + if (state.isMoving()) + return state.setAndContinue(StyracosaurusEntity.this.isSprinting() ? RawAnimation.begin().then("anim.styracosaurus.run", Animation.LoopType.LOOP) : RawAnimation.begin().then("anim.styracosaurus.walk", Animation.LoopType.LOOP)); + + return state.setAndContinue(RawAnimation.begin().then("anim.styracosaurus.idle", Animation.LoopType.LOOP)); + })); + + controllers.add(new AnimationController<>(this, "attackController", state -> PlayState.STOP) + .triggerableAnim("attack", RawAnimation.begin().then("anim.styracosaurus.attack", Animation.LoopType.PLAY_ONCE))); + + controllers.add(new AnimationController<>(this, "mouthController", state -> PlayState.STOP) + .triggerableAnim("mouth", RawAnimation.begin().then("anim.styracosaurus.mouth", Animation.LoopType.PLAY_ONCE))); + } + + private float getSignedTurnDelta() { + // Only consider the body (torso) rotation so head look does not affect tail sway + return Mth.wrapDegrees(this.yBodyRot - this.yBodyRotO); + } + + private int mouthAnimCooldown = 0; + + @Override + public void tick() { + super.tick(); + if (!level().isClientSide) { + this.entityData.set(DATA_SYNCED_AGE, this.getAge()); + var maxHealthAttr = getAttribute(Attributes.MAX_HEALTH); + if (maxHealthAttr != null) { + double baseAdult = DefaultAttributes.getSupplier((EntityType) this.getType()).getValue(Attributes.MAX_HEALTH); + double desired = this.isBaby() ? baseAdult * 0.10D : baseAdult; + if (maxHealthAttr.getBaseValue() != desired) { + double oldMax = maxHealthAttr.getBaseValue(); + double healthRatio = this.getHealth() / (float) oldMax; + maxHealthAttr.setBaseValue(desired); + this.setHealth((float) (desired * Mth.clamp(healthRatio, 0.0F, 1.0F))); + } + } + } + + if (!level().isClientSide) { + if (mouthAnimCooldown > 0) { + mouthAnimCooldown--; + } else { + this.triggerAnim("mouthController", "mouth"); + this.playSound(ModSounds.STYRACOSAURUS_CALL.get(), 1.0F, 1.0F); + // 30s–60s in ticks + mouthAnimCooldown = this.random.nextInt(1200 - 600 + 1) + 600; + } + } + + if (level().isClientSide) { + // Capture previous for smooth interpolation between ticks + this.tailSwayPrev = this.tailSwayOffset; + updateProceduralTailSway(); + } + } + + private void updateProceduralTailSway() { + // Turn input derived from rotation deltas; works even when standing still and turning + float turnDegrees = getSignedTurnDelta(); + + // Deadzone to ignore tiny jitter so the tail can return to center cleanly + float deadzoneDeg = 0.6f; // smaller deadzone for more responsiveness + float turnInput = 0.0f; + if (Math.abs(turnDegrees) >= deadzoneDeg) { + // Higher sensitivity so small in-place turns still affect the model + turnInput = Mth.clamp(turnDegrees / 15.0f, -1.0f, 1.0f); + } + + // Target offset: keep intuitive sign (positive input -> positive sway) + float target = turnInput; + + // One-pole low-pass (no bounce). Larger alpha => snappier and less "stiff". + float alpha = 0.24f; // try 0.20–0.30 to taste + + tailSwayOffset += (target - tailSwayOffset) * alpha; + + // Snap tiny residuals to zero so it visibly settles + if (Math.abs(tailSwayOffset) < 0.003f) { + tailSwayOffset = 0.0f; + } + + // No oscillation velocity retained + tailSwayVelocity = 0.0f; + + tailSwayOffset = Mth.clamp(tailSwayOffset, -1.5f, 1.5f); + } + + // Expose to the model for bone rotation + public float getTailSwayOffset() { + return tailSwayOffset; + } + + // Interpolated sway for smooth rendering between ticks + public float getTailSwayOffset(float partialTick) { + return Mth.lerp(Mth.clamp(partialTick, 0.0f, 1.0f), tailSwayPrev, tailSwayOffset); + } + + /*? if <=1.20.1 {*/ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); + this.entityData.define(VARIANT, 0); + this.entityData.define(DATA_SYNCED_AGE, 0); + } + /*?} else {*/ + /*@Override + protected void defineSynchedData(SynchedEntityData.Builder pBuilder) { + super.defineSynchedData(pBuilder); + pBuilder.define(VARIANT, 0); + pBuilder.define(DATA_SYNCED_AGE, 0); + } + *//*?}*/ + + public int getSyncedAge() { + return this.entityData.get(DATA_SYNCED_AGE); + } + public int getTypeVariant() { + return this.entityData.get(VARIANT); + } + + public StyracosaurusVariant getVariant() { + return StyracosaurusVariant.byId(this.getTypeVariant() & 255); + } + + private void setVariant(StyracosaurusVariant variant) { + this.entityData.set(VARIANT, variant.getId() & 255); + } + + @Override + public boolean canMate(Animal other) { + if (!super.canMate(other)) return false; + if (!(other instanceof StyracosaurusEntity that)) return false; + return this.getVariant() != that.getVariant(); + } + @Override + public void addAdditionalSaveData(CompoundTag pCompound) { + super.addAdditionalSaveData(pCompound); + pCompound.putInt("Variant", this.getTypeVariant()); + } + + @Override + public void readAdditionalSaveData(CompoundTag pCompound) { + super.readAdditionalSaveData(pCompound); + this.entityData.set(VARIANT, pCompound.getInt("Variant")); + } + + /*? if <=1.20.1 {*/ + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor pLevel, DifficultyInstance pDifficulty, MobSpawnType pReason, @Nullable SpawnGroupData pSpawnData, @Nullable CompoundTag pDataTag) { + StyracosaurusVariant variant = Util.getRandom(StyracosaurusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(pLevel, pDifficulty, pReason, pSpawnData, pDataTag); + } + /*?} else {*/ + /*@Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, MobSpawnType spawnType, @Nullable SpawnGroupData spawnGroupData) { + StyracosaurusVariant variant = Util.getRandom(StyracosaurusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(level, difficulty, spawnType, spawnGroupData); + } + *//*?}*/ + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } + + @Override + protected @Nullable SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.STYRACOSAURUS_HURT.get(); + } + + @Override + protected @Nullable SoundEvent getDeathSound() { + return ModSounds.STYRACOSAURUS_DEATH.get(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/custom/TapejaraEntity.java b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/TapejaraEntity.java new file mode 100644 index 0000000..079d2de --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/TapejaraEntity.java @@ -0,0 +1,411 @@ +package net.cmr.jurassicrevived.entity.custom; + +import net.cmr.jurassicrevived.entity.ModEntities; +import net.cmr.jurassicrevived.entity.ai.SprintingMeleeAttackGoal; +import net.cmr.jurassicrevived.entity.ai.SprintingPanicGoal; +import net.cmr.jurassicrevived.entity.client.AlbertosaurusVariant; +import net.cmr.jurassicrevived.entity.client.TapejaraVariant; +import net.cmr.jurassicrevived.sound.ModSounds; +import net.minecraft.Util; +import net.minecraft.core.BlockPos; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.*; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.attributes.DefaultAttributes; +import net.minecraft.world.entity.ai.control.FlyingMoveControl; +import net.minecraft.world.entity.ai.goal.*; +import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal; +import net.minecraft.world.entity.ai.goal.target.NearestAttackableTargetGoal; +import net.minecraft.world.entity.ai.navigation.FlyingPathNavigation; +import net.minecraft.world.entity.ai.navigation.PathNavigation; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.animal.FlyingAnimal; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerLevelAccessor; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.phys.Vec3; +import org.jetbrains.annotations.Nullable; +import software.bernie.geckolib.animatable.GeoEntity; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.core.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.core.animation.AnimatableManager; +import software.bernie.geckolib.core.animation.Animation; +import software.bernie.geckolib.core.animation.AnimationController; +import software.bernie.geckolib.core.animation.RawAnimation; +import software.bernie.geckolib.core.object.PlayState; +/*?} else {*/ +/*import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.animation.*; +*//*?}*/ + +public class TapejaraEntity extends Animal implements GeoEntity, FlyingAnimal { + private final AnimatableInstanceCache cache = new SingletonAnimatableInstanceCache(this); + + private static final EntityDataAccessor VARIANT = + SynchedEntityData.defineId(TapejaraEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SYNCED_AGE = + SynchedEntityData.defineId(TapejaraEntity.class, EntityDataSerializers.INT); + + // Procedural tail sway state (client-side use for rendering) + private float tailSwayOffset; // Smoothed offset in range roughly [-1, 1] + private float tailSwayVelocity; // Internal velocity for spring-damper + private float tailSwayPrev; // Previous frame value for interpolation + + public TapejaraEntity(EntityType pEntityType, Level pLevel) { + super(pEntityType, pLevel); + this.moveControl = new FlyingMoveControl(this, 20, true); + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new SprintingPanicGoal(this, 1.15) { + @Override + public boolean canUse() { + return TapejaraEntity.this.isBaby() && super.canUse(); + } + }); + this.goalSelector.addGoal(2, new FloatGoal(this)); + this.goalSelector.addGoal(3, new AvoidEntityGoal<>(this, BrachiosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(4, new SprintingMeleeAttackGoal(this, 1.1, false)); + this.goalSelector.addGoal(5, new BreedGoal(this, 1.0)); + this.goalSelector.addGoal(6, new FollowParentGoal(this, 1.25)); + + // Goal 7: Wander on ground (Walk) - Only when on ground + this.goalSelector.addGoal(7, new WaterAvoidingRandomStrollGoal(this, 1.0) { + @Override + public boolean canUse() { + return TapejaraEntity.this.onGround() && super.canUse(); + } + }); + + // Goal 8: Wander in air (Fly) - Handles takeoff, flying, and landing + this.goalSelector.addGoal(8, new WaterAvoidingRandomStrollGoal(this, 1.0, 20) { + @Override + public boolean canUse() { + boolean isFlying = !TapejaraEntity.this.onGround(); + // If flying, keep flying. If on ground, small chance (1/400 ticks) to take off. + return (isFlying || TapejaraEntity.this.getRandom().nextInt(400) == 0) && super.canUse(); + } + + @Override + protected Vec3 getPosition() { + Vec3 pos = TapejaraEntity.this.position(); + RandomSource random = TapejaraEntity.this.getRandom(); + + double x = pos.x + (random.nextFloat() * 2 - 1) * 32; + double z = pos.z + (random.nextFloat() * 2 - 1) * 32; + + // Get ground height at the random destination (returns Y of first air block) + int groundY = TapejaraEntity.this.level().getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, (int)x, (int)z); + double y; + + if (TapejaraEntity.this.onGround()) { + // Takeoff: Target well above ground to ensure liftoff + y = pos.y + 15 + random.nextInt(10); + } else { + // Flying: 5% chance to land, otherwise stay airborne but capped height + if (random.nextFloat() < 0.05f) { + y = groundY; // Land + } else if (pos.y > groundY + 20) { + // Too high: Force descent + y = pos.y - 5 - random.nextInt(10); + } else { + // Just wander up or down a bit + y = pos.y + (random.nextFloat() * 2 - 1) * 10; + } + } + + // Don't target below the ground (blocks) + if (y < groundY) y = groundY; + + return new Vec3(x, y, z); + } + }); + + this.goalSelector.addGoal(9, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(10, new FollowMobGoal(this, 0.8, (float) 20, (float) 10)); + this.targetSelector.addGoal(11, new NearestAttackableTargetGoal<>(this, Animal.class, 10, false, false, + target -> { + // 1. Don't eat your own species + if (target.getType() == this.getType()) return false; + + // 2. Don't eat Flying Animals + if (target instanceof FlyingAnimal) return false; + + // 3. SIZE CHECK: specific height and width limits + // Example: Height < 1.0 blocks AND Width < 1.0 blocks + boolean isSmallEnough = target.getBbHeight() <= 1.0F && target.getBbWidth() <= 1.0F; + + return isSmallEnough; + } + )); + this.targetSelector.addGoal(12, new NearestAttackableTargetGoal<>(this, Player.class, false, false)); + this.targetSelector.addGoal(13, new HurtByTargetGoal(this)); + this.goalSelector.addGoal(14, new RandomLookAroundGoal(this)); + this.goalSelector.addGoal(15, new FloatGoal(this)); + } + + public static AttributeSupplier.Builder createAttributes() { + return Animal.createLivingAttributes() + .add(Attributes.MAX_HEALTH, 20D) + .add(Attributes.MOVEMENT_SPEED, 0.3D) + .add(Attributes.FLYING_SPEED, 0.6D) + .add(Attributes.ARMOR, 0D) + .add(Attributes.FOLLOW_RANGE, 32D) + .add(Attributes.KNOCKBACK_RESISTANCE, 0D) + .add(Attributes.ATTACK_KNOCKBACK, 0D) + .add(Attributes.ATTACK_DAMAGE, 5D); + } + + @Override + protected PathNavigation createNavigation(Level pLevel) { + FlyingPathNavigation navigation = new FlyingPathNavigation(this, pLevel); + navigation.setCanOpenDoors(false); + navigation.setCanFloat(true); + navigation.setCanPassDoors(true); + return navigation; + } + + @Override + public boolean isFlying() { + return !this.onGround(); + } + + @Override + public boolean causeFallDamage(float pFallDistance, float pMultiplier, DamageSource pSource) { + return false; + } + + @Override + protected void checkFallDamage(double pY, boolean pOnGround, BlockState pState, BlockPos pPos) { + } + + @Override + public boolean isFood(ItemStack pStack) { + return pStack.is(Items.BEEF); + } + + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel pLevel, AgeableMob pOtherParent) { + AgeableMob child = ModEntities.TAPEJARA.get().create(pLevel); + if (child instanceof TapejaraEntity baby) { + TapejaraVariant randomVariant = Util.getRandom(TapejaraVariant.values(), this.random); + baby.setVariant(randomVariant); + } + return child; + } + + @Override + public boolean doHurtTarget(Entity target) { + boolean hit = super.doHurtTarget(target); + if (!level().isClientSide && hit && target instanceof LivingEntity) { + if (this.level() instanceof ServerLevel serverLevel) { + this.triggerAnim("attackController", "attack"); + this.playSound(ModSounds.BEAK_ATTACK.get(), 1.0F, 1.0F); + } + } + return hit; + } + + @Override + public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { + controllers.add(new AnimationController<>(this, "Walk/Run/Idle", state -> { + if (!TapejaraEntity.this.onGround()) { + return state.setAndContinue(RawAnimation.begin().then("anim.tapejara.fly", Animation.LoopType.LOOP)); + } + + if (state.isMoving()) + return state.setAndContinue(TapejaraEntity.this.isSprinting() ? RawAnimation.begin().then("anim.tapejara.walk", Animation.LoopType.LOOP) : RawAnimation.begin().then("anim.tapejara.walk", Animation.LoopType.LOOP)); + + return state.setAndContinue(RawAnimation.begin().then("anim.tapejara.idle", Animation.LoopType.LOOP)); + })); + + controllers.add(new AnimationController<>(this, "attackController", state -> PlayState.STOP) + .triggerableAnim("attack", RawAnimation.begin().then("anim.tapejara.attack", Animation.LoopType.PLAY_ONCE))); + + controllers.add(new AnimationController<>(this, "mouthController", state -> PlayState.STOP) + .triggerableAnim("mouth", RawAnimation.begin().then("anim.tapejara.mouth", Animation.LoopType.PLAY_ONCE))); + } + + private float getSignedTurnDelta() { + // Only consider the body (torso) rotation so head look does not affect tail sway + return Mth.wrapDegrees(this.yBodyRot - this.yBodyRotO); + } + + private int mouthAnimCooldown = 0; + + @Override + public void tick() { + super.tick(); + if (!level().isClientSide) { + this.entityData.set(DATA_SYNCED_AGE, this.getAge()); + var maxHealthAttr = getAttribute(Attributes.MAX_HEALTH); + if (maxHealthAttr != null) { + double baseAdult = DefaultAttributes.getSupplier((EntityType) this.getType()).getValue(Attributes.MAX_HEALTH); + double desired = this.isBaby() ? baseAdult * 0.10D : baseAdult; + if (maxHealthAttr.getBaseValue() != desired) { + double oldMax = maxHealthAttr.getBaseValue(); + double healthRatio = this.getHealth() / (float) oldMax; + maxHealthAttr.setBaseValue(desired); + this.setHealth((float) (desired * Mth.clamp(healthRatio, 0.0F, 1.0F))); + } + } + } + + if (!level().isClientSide) { + if (mouthAnimCooldown > 0) { + mouthAnimCooldown--; + } else { + this.triggerAnim("mouthController", "mouth"); + this.playSound(ModSounds.TAPEJARA_CALL.get(), 1.0F, 1.0F); + // 30s–60s in ticks + mouthAnimCooldown = this.random.nextInt(1200 - 600 + 1) + 600; + } + } + + if (level().isClientSide) { + // Capture previous for smooth interpolation between ticks + this.tailSwayPrev = this.tailSwayOffset; + updateProceduralTailSway(); + } + } + + private void updateProceduralTailSway() { + // Turn input derived from rotation deltas; works even when standing still and turning + float turnDegrees = getSignedTurnDelta(); + + // Deadzone to ignore tiny jitter so the tail can return to center cleanly + float deadzoneDeg = 0.6f; // smaller deadzone for more responsiveness + float turnInput = 0.0f; + if (Math.abs(turnDegrees) >= deadzoneDeg) { + // Higher sensitivity so small in-place turns still affect the model + turnInput = Mth.clamp(turnDegrees / 15.0f, -1.0f, 1.0f); + } + + // Target offset: keep intuitive sign (positive input -> positive sway) + float target = turnInput; + + // One-pole low-pass (no bounce). Larger alpha => snappier and less "stiff". + float alpha = 0.24f; // try 0.20–0.30 to taste + + tailSwayOffset += (target - tailSwayOffset) * alpha; + + // Snap tiny residuals to zero so it visibly settles + if (Math.abs(tailSwayOffset) < 0.003f) { + tailSwayOffset = 0.0f; + } + + // No oscillation velocity retained + tailSwayVelocity = 0.0f; + + tailSwayOffset = Mth.clamp(tailSwayOffset, -1.5f, 1.5f); + } + + // Expose to the model for bone rotation + public float getTailSwayOffset() { + return tailSwayOffset; + } + + // Interpolated sway for smooth rendering between ticks + public float getTailSwayOffset(float partialTick) { + return Mth.lerp(Mth.clamp(partialTick, 0.0f, 1.0f), tailSwayPrev, tailSwayOffset); + } + + /*? if <=1.20.1 {*/ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); + this.entityData.define(VARIANT, 0); + this.entityData.define(DATA_SYNCED_AGE, 0); + } + /*?} else {*/ + /*@Override + protected void defineSynchedData(SynchedEntityData.Builder pBuilder) { + super.defineSynchedData(pBuilder); + pBuilder.define(VARIANT, 0); + pBuilder.define(DATA_SYNCED_AGE, 0); + } + *//*?}*/ + + public int getSyncedAge() { + return this.entityData.get(DATA_SYNCED_AGE); + } + + public TapejaraVariant getVariant() { + return TapejaraVariant.byId(this.getTypeVariant() & 255); + } + public int getTypeVariant() { + return this.entityData.get(VARIANT); + } + + private void setVariant(TapejaraVariant variant) { + this.entityData.set(VARIANT, variant.getId() & 255); + } + + @Override + public boolean canMate(Animal other) { + if (!super.canMate(other)) return false; + if (!(other instanceof TapejaraEntity that)) return false; + return this.getVariant() != that.getVariant(); + } + + /*? if <=1.20.1 {*/ + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor pLevel, DifficultyInstance pDifficulty, MobSpawnType pReason, @Nullable SpawnGroupData pSpawnData, @Nullable CompoundTag pDataTag) { + TapejaraVariant variant = Util.getRandom(TapejaraVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(pLevel, pDifficulty, pReason, pSpawnData, pDataTag); + } + /*?} else {*/ + /*@Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, MobSpawnType spawnType, @Nullable SpawnGroupData spawnGroupData) { + TapejaraVariant variant = Util.getRandom(TapejaraVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(level, difficulty, spawnType, spawnGroupData); + } + *//*?}*/ + + @Override + public void readAdditionalSaveData(CompoundTag pCompound) { + super.readAdditionalSaveData(pCompound); + this.entityData.set(VARIANT, pCompound.getInt("Variant")); + } + + @Override + public void addAdditionalSaveData(CompoundTag pCompound) { + super.addAdditionalSaveData(pCompound); + pCompound.putInt("Variant", this.getTypeVariant()); + } + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } + + @Override + protected @Nullable SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.TAPEJARA_HURT.get(); + } + + @Override + protected @Nullable SoundEvent getDeathSound() { + return ModSounds.TAPEJARA_DEATH.get(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/custom/TherizinosaurusEntity.java b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/TherizinosaurusEntity.java new file mode 100644 index 0000000..2de6aa5 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/TherizinosaurusEntity.java @@ -0,0 +1,309 @@ +package net.cmr.jurassicrevived.entity.custom; + +import net.cmr.jurassicrevived.entity.ModEntities; +import net.cmr.jurassicrevived.entity.ai.SprintingMeleeAttackGoal; +import net.cmr.jurassicrevived.entity.ai.SprintingPanicGoal; +import net.cmr.jurassicrevived.entity.client.AlbertosaurusVariant; +import net.cmr.jurassicrevived.entity.client.TherizinosaurusVariant; +import net.cmr.jurassicrevived.sound.ModSounds; +import net.minecraft.Util; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.util.Mth; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.*; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.attributes.DefaultAttributes; +import net.minecraft.world.entity.ai.goal.*; +import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerLevelAccessor; +import org.jetbrains.annotations.Nullable; +import software.bernie.geckolib.animatable.GeoEntity; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.core.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.core.animation.AnimatableManager; +import software.bernie.geckolib.core.animation.Animation; +import software.bernie.geckolib.core.animation.AnimationController; +import software.bernie.geckolib.core.animation.RawAnimation; +import software.bernie.geckolib.core.object.PlayState; +/*?} else {*/ +/*import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.animation.*; +*//*?}*/ + +public class TherizinosaurusEntity extends Animal implements GeoEntity { + private AnimatableInstanceCache cache = new SingletonAnimatableInstanceCache(this); + + private static final EntityDataAccessor VARIANT = + SynchedEntityData.defineId(TherizinosaurusEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SYNCED_AGE = + SynchedEntityData.defineId(TherizinosaurusEntity.class, EntityDataSerializers.INT); + + // Procedural tail sway state (client-side use for rendering) + private float tailSwayOffset; // Smoothed offset in range roughly [-1, 1] + private float tailSwayVelocity; // Internal velocity for spring-damper + private float tailSwayPrev; // Previous frame value for interpolation + + public TherizinosaurusEntity(EntityType pEntityType, Level pLevel) { + super(pEntityType, pLevel); + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new SprintingPanicGoal(this, 1.15) { + @Override + public boolean canUse() { + return TherizinosaurusEntity.this.isBaby() && super.canUse(); + } + }); + this.targetSelector.addGoal(1, new HurtByTargetGoal(this).setAlertOthers()); + this.goalSelector.addGoal(2, new FloatGoal(this)); + this.goalSelector.addGoal(3, new AvoidEntityGoal<>(this, VelociraptorEntity.class, (float) 20, 0.8, 0.8)); + this.goalSelector.addGoal(4, new AvoidEntityGoal<>(this, CeratosaurusEntity.class, (float) 20, 0.8, 0.8)); + this.goalSelector.addGoal(5, new AvoidEntityGoal<>(this, DilophosaurusEntity.class, (float) 20, 0.8, 0.8)); + this.goalSelector.addGoal(6, new SprintingMeleeAttackGoal(this, 1.1, false)); + this.goalSelector.addGoal(7, new BreedGoal(this, 1.0)); + this.goalSelector.addGoal(8, new FollowParentGoal(this, 1.25)); + this.goalSelector.addGoal(8, new WaterAvoidingRandomStrollGoal(this, 1.0)); + this.goalSelector.addGoal(10, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(11, new FollowMobGoal(this, 0.8, (float) 20, (float) 10)); + this.goalSelector.addGoal(12, new AvoidEntityGoal<>(this, SpinosaurusEntity.class, (float) 20, 0.8, 0.8)); + this.goalSelector.addGoal(13, new AvoidEntityGoal<>(this, TyrannosaurusRexEntity.class, (float) 20, 0.8, 0.8)); + this.goalSelector.addGoal(14, new AvoidEntityGoal<>(this, IndominusRexEntity.class, (float) 20, 0.8, 0.8)); + this.goalSelector.addGoal(15, new EatBlockGoal(this)); + this.goalSelector.addGoal(16, new RandomLookAroundGoal(this)); + + } + + public static AttributeSupplier.Builder createAttributes() { + return Animal.createLivingAttributes() + .add(Attributes.MAX_HEALTH, 70D) + .add(Attributes.MOVEMENT_SPEED, 0.3D) + .add(Attributes.ARMOR, 0D) + .add(Attributes.FOLLOW_RANGE, 32D) + .add(Attributes.KNOCKBACK_RESISTANCE, 0D) + .add(Attributes.ATTACK_KNOCKBACK, 0D) + .add(Attributes.ATTACK_DAMAGE, 18D); + } + + @Override + public boolean isFood(ItemStack pStack) { + return pStack.is(Items.KELP); + } + + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel pLevel, AgeableMob pOtherParent) { + AgeableMob child = ModEntities.THERIZINOSAURUS.get().create(pLevel); + if (child instanceof TherizinosaurusEntity baby) { + TherizinosaurusVariant randomVariant = Util.getRandom(TherizinosaurusVariant.values(), this.random); + baby.setVariant(randomVariant); + } + return child; + } + + @Override + public boolean doHurtTarget(Entity target) { + boolean hit = super.doHurtTarget(target); + if (!level().isClientSide && hit && target instanceof LivingEntity) { + if (this.level() instanceof ServerLevel serverLevel) { + this.triggerAnim("attackController", "attack"); + this.playSound(ModSounds.TAIL_STRIKE.get(), 1.0F, 1.0F); + } + } + return hit; + } + + @Override + public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { + controllers.add(new AnimationController<>(this, "Walk/Run/Idle", state -> { + if (state.isMoving()) + return state.setAndContinue(TherizinosaurusEntity.this.isSprinting() ? RawAnimation.begin().then("anim.therizinosaurus.run", Animation.LoopType.LOOP) : RawAnimation.begin().then("anim.therizinosaurus.walk", Animation.LoopType.LOOP)); + + return state.setAndContinue(RawAnimation.begin().then("anim.therizinosaurus.idle", Animation.LoopType.LOOP)); + })); + + controllers.add(new AnimationController<>(this, "attackController", state -> PlayState.STOP) + .triggerableAnim("attack", RawAnimation.begin().then("anim.therizinosaurus.attack", Animation.LoopType.PLAY_ONCE))); + + controllers.add(new AnimationController<>(this, "mouthController", state -> PlayState.STOP) + .triggerableAnim("mouth", RawAnimation.begin().then("anim.therizinosaurus.mouth", Animation.LoopType.PLAY_ONCE))); + } + + private float getSignedTurnDelta() { + // Only consider the body (torso) rotation so head look does not affect tail sway + return Mth.wrapDegrees(this.yBodyRot - this.yBodyRotO); + } + + private int mouthAnimCooldown = 0; + + @Override + public void tick() { + super.tick(); + if (!level().isClientSide) { + this.entityData.set(DATA_SYNCED_AGE, this.getAge()); + var maxHealthAttr = getAttribute(Attributes.MAX_HEALTH); + if (maxHealthAttr != null) { + double baseAdult = DefaultAttributes.getSupplier((EntityType) this.getType()).getValue(Attributes.MAX_HEALTH); + double desired = this.isBaby() ? baseAdult * 0.10D : baseAdult; + if (maxHealthAttr.getBaseValue() != desired) { + double oldMax = maxHealthAttr.getBaseValue(); + double healthRatio = this.getHealth() / (float) oldMax; + maxHealthAttr.setBaseValue(desired); + this.setHealth((float) (desired * Mth.clamp(healthRatio, 0.0F, 1.0F))); + } + } + } + + if (!level().isClientSide) { + if (mouthAnimCooldown > 0) { + mouthAnimCooldown--; + } else { + this.triggerAnim("mouthController", "mouth"); + this.playSound(ModSounds.THERIZINOSAURUS_CALL.get(), 1.0F, 1.0F); + // 30s–60s in ticks + mouthAnimCooldown = this.random.nextInt(1200 - 600 + 1) + 600; + } + } + + if (level().isClientSide) { + // Capture previous for smooth interpolation between ticks + this.tailSwayPrev = this.tailSwayOffset; + updateProceduralTailSway(); + } + } + + private void updateProceduralTailSway() { + // Turn input derived from rotation deltas; works even when standing still and turning + float turnDegrees = getSignedTurnDelta(); + + // Deadzone to ignore tiny jitter so the tail can return to center cleanly + float deadzoneDeg = 0.6f; // smaller deadzone for more responsiveness + float turnInput = 0.0f; + if (Math.abs(turnDegrees) >= deadzoneDeg) { + // Higher sensitivity so small in-place turns still affect the model + turnInput = Mth.clamp(turnDegrees / 15.0f, -1.0f, 1.0f); + } + + // Target offset: keep intuitive sign (positive input -> positive sway) + float target = turnInput; + + // One-pole low-pass (no bounce). Larger alpha => snappier and less "stiff". + float alpha = 0.24f; // try 0.20–0.30 to taste + + tailSwayOffset += (target - tailSwayOffset) * alpha; + + // Snap tiny residuals to zero so it visibly settles + if (Math.abs(tailSwayOffset) < 0.003f) { + tailSwayOffset = 0.0f; + } + + // No oscillation velocity retained + tailSwayVelocity = 0.0f; + + tailSwayOffset = Mth.clamp(tailSwayOffset, -1.5f, 1.5f); + } + + // Expose to the model for bone rotation + public float getTailSwayOffset() { + return tailSwayOffset; + } + + // Interpolated sway for smooth rendering between ticks + public float getTailSwayOffset(float partialTick) { + return Mth.lerp(Mth.clamp(partialTick, 0.0f, 1.0f), tailSwayPrev, tailSwayOffset); + } + + /*? if <=1.20.1 {*/ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); + this.entityData.define(VARIANT, 0); + this.entityData.define(DATA_SYNCED_AGE, 0); + } + /*?} else {*/ + /*@Override + protected void defineSynchedData(SynchedEntityData.Builder pBuilder) { + super.defineSynchedData(pBuilder); + pBuilder.define(VARIANT, 0); + pBuilder.define(DATA_SYNCED_AGE, 0); + } + *//*?}*/ + + public int getSyncedAge() { + return this.entityData.get(DATA_SYNCED_AGE); + } + public int getTypeVariant() { + return this.entityData.get(VARIANT); + } + + public TherizinosaurusVariant getVariant() { + return TherizinosaurusVariant.byId(this.getTypeVariant() & 255); + } + + private void setVariant(TherizinosaurusVariant variant) { + this.entityData.set(VARIANT, variant.getId() & 255); + } + + @Override + public boolean canMate(Animal other) { + if (!super.canMate(other)) return false; + if (!(other instanceof TherizinosaurusEntity that)) return false; + return this.getVariant() != that.getVariant(); + } + @Override + public void addAdditionalSaveData(CompoundTag pCompound) { + super.addAdditionalSaveData(pCompound); + pCompound.putInt("Variant", this.getTypeVariant()); + } + + @Override + public void readAdditionalSaveData(CompoundTag pCompound) { + super.readAdditionalSaveData(pCompound); + this.entityData.set(VARIANT, pCompound.getInt("Variant")); + } + + /*? if <=1.20.1 {*/ + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor pLevel, DifficultyInstance pDifficulty, MobSpawnType pReason, @Nullable SpawnGroupData pSpawnData, @Nullable CompoundTag pDataTag) { + TherizinosaurusVariant variant = Util.getRandom(TherizinosaurusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(pLevel, pDifficulty, pReason, pSpawnData, pDataTag); + } + /*?} else {*/ + /*@Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, MobSpawnType spawnType, @Nullable SpawnGroupData spawnGroupData) { + TherizinosaurusVariant variant = Util.getRandom(TherizinosaurusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(level, difficulty, spawnType, spawnGroupData); + } + *//*?}*/ + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } + + @Override + protected @Nullable SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.THERIZINOSAURUS_HURT.get(); + } + + @Override + protected @Nullable SoundEvent getDeathSound() { + return ModSounds.THERIZINOSAURUS_DEATH.get(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/custom/TitanosaurusEntity.java b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/TitanosaurusEntity.java new file mode 100644 index 0000000..b763e81 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/TitanosaurusEntity.java @@ -0,0 +1,315 @@ +package net.cmr.jurassicrevived.entity.custom; + +import net.cmr.jurassicrevived.entity.ModEntities; +import net.cmr.jurassicrevived.entity.ai.SprintingMeleeAttackGoal; +import net.cmr.jurassicrevived.entity.ai.SprintingPanicGoal; +import net.cmr.jurassicrevived.entity.client.AlbertosaurusVariant; +import net.cmr.jurassicrevived.entity.client.TitanosaurusVariant; +import net.cmr.jurassicrevived.sound.ModSounds; +import net.minecraft.Util; +import net.minecraft.core.BlockPos; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.util.Mth; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.*; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.attributes.DefaultAttributes; +import net.minecraft.world.entity.ai.goal.*; +import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerLevelAccessor; +import net.minecraft.world.level.block.state.BlockState; +import org.jetbrains.annotations.Nullable; +import software.bernie.geckolib.animatable.GeoEntity; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.core.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.core.animation.AnimatableManager; +import software.bernie.geckolib.core.animation.Animation; +import software.bernie.geckolib.core.animation.AnimationController; +import software.bernie.geckolib.core.animation.RawAnimation; +import software.bernie.geckolib.core.object.PlayState; +/*?} else {*/ +/*import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.animation.*; +*//*?}*/ + +public class TitanosaurusEntity extends Animal implements GeoEntity { + private AnimatableInstanceCache cache = new SingletonAnimatableInstanceCache(this); + + private static final EntityDataAccessor VARIANT = + SynchedEntityData.defineId(TitanosaurusEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SYNCED_AGE = + SynchedEntityData.defineId(TitanosaurusEntity.class, EntityDataSerializers.INT); + + // Procedural tail sway state (client-side use for rendering) + private float tailSwayOffset; // Smoothed offset in range roughly [-1, 1] + private float tailSwayVelocity; // Internal velocity for spring-damper + private float tailSwayPrev; // Previous frame value for interpolation + + public TitanosaurusEntity(EntityType pEntityType, Level pLevel) { + super(pEntityType, pLevel); + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new SprintingPanicGoal(this, 1.15) { + @Override + public boolean canUse() { + return TitanosaurusEntity.this.isBaby() && super.canUse(); + } + }); + this.targetSelector.addGoal(1, new HurtByTargetGoal(this).setAlertOthers()); + this.goalSelector.addGoal(2, new FloatGoal(this)); + this.goalSelector.addGoal(3, new AvoidEntityGoal<>(this, DilophosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(4, new AvoidEntityGoal<>(this, VelociraptorEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(5, new AvoidEntityGoal<>(this, CeratosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(6, new AvoidEntityGoal<>(this, SpinosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(7, new AvoidEntityGoal<>(this, TyrannosaurusRexEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(8, new SprintingMeleeAttackGoal(this, 1.1, false)); + this.goalSelector.addGoal(9, new BreedGoal(this, 1.0)); + this.goalSelector.addGoal(9, new FollowParentGoal(this, 1.25)); + this.goalSelector.addGoal(10, new WaterAvoidingRandomStrollGoal(this, 1.0)); + this.goalSelector.addGoal(11, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(12, new FollowMobGoal(this, 1, (float) 20, (float) 10)); + this.goalSelector.addGoal(13, new RandomLookAroundGoal(this)); + + } + + public static AttributeSupplier.Builder createAttributes() { + return Animal.createLivingAttributes() + .add(Attributes.MAX_HEALTH, 250D) + .add(Attributes.MOVEMENT_SPEED, 0.3D) + .add(Attributes.ARMOR, 0D) + .add(Attributes.FOLLOW_RANGE, 32D) + .add(Attributes.KNOCKBACK_RESISTANCE, 1D) + .add(Attributes.ATTACK_KNOCKBACK, 0D) + .add(Attributes.ATTACK_DAMAGE, 30D); + } + + @Override + public boolean isFood(ItemStack pStack) { + return pStack.is(Items.KELP); + } + + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel pLevel, AgeableMob pOtherParent) { + AgeableMob child = ModEntities.TITANOSAURUS.get().create(pLevel); + if (child instanceof TitanosaurusEntity baby) { + TitanosaurusVariant randomVariant = Util.getRandom(TitanosaurusVariant.values(), this.random); + baby.setVariant(randomVariant); + } + return child; + } + + @Override + public boolean doHurtTarget(Entity target) { + boolean hit = super.doHurtTarget(target); + if (!level().isClientSide && hit && target instanceof LivingEntity) { + if (this.level() instanceof ServerLevel serverLevel) { + this.triggerAnim("attackController", "attack"); + this.playSound(ModSounds.STOMP_ATTACK.get(), 1.0F, 1.0F); + } + } + return hit; + } + + @Override + public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { + controllers.add(new AnimationController<>(this, "Walk/Run/Idle", state -> { + if (state.isMoving()) + return state.setAndContinue(TitanosaurusEntity.this.isSprinting() ? RawAnimation.begin().then("anim.titanosaurus.run", Animation.LoopType.LOOP) : RawAnimation.begin().then("anim.titanosaurus.walk", Animation.LoopType.LOOP)); + + return state.setAndContinue(RawAnimation.begin().then("anim.titanosaurus.idle", Animation.LoopType.LOOP)); + })); + + controllers.add(new AnimationController<>(this, "attackController", state -> PlayState.STOP) + .triggerableAnim("attack", RawAnimation.begin().then("anim.titanosaurus.attack", Animation.LoopType.PLAY_ONCE))); + + controllers.add(new AnimationController<>(this, "mouthController", state -> PlayState.STOP) + .triggerableAnim("mouth", RawAnimation.begin().then("anim.titanosaurus.mouth", Animation.LoopType.PLAY_ONCE))); + } + + private float getSignedTurnDelta() { + // Only consider the body (torso) rotation so head look does not affect tail sway + return Mth.wrapDegrees(this.yBodyRot - this.yBodyRotO); + } + + private int mouthAnimCooldown = 0; + + @Override + public void tick() { + super.tick(); + + if (!level().isClientSide) { + this.entityData.set(DATA_SYNCED_AGE, this.getAge()); + var maxHealthAttr = getAttribute(Attributes.MAX_HEALTH); + if (maxHealthAttr != null) { + double baseAdult = DefaultAttributes.getSupplier((EntityType) this.getType()).getValue(Attributes.MAX_HEALTH); + double desired = this.isBaby() ? baseAdult * 0.10D : baseAdult; + if (maxHealthAttr.getBaseValue() != desired) { + double oldMax = maxHealthAttr.getBaseValue(); + double healthRatio = this.getHealth() / (float) oldMax; + maxHealthAttr.setBaseValue(desired); + this.setHealth((float) (desired * Mth.clamp(healthRatio, 0.0F, 1.0F))); + } + } + } + + if (!level().isClientSide) { + if (mouthAnimCooldown > 0) { + mouthAnimCooldown--; + } else { + this.triggerAnim("mouthController", "mouth"); + this.playSound(ModSounds.TITANOSAURUS_CALL.get(), 1.0F, 1.0F); + // 30s–60s in ticks + mouthAnimCooldown = this.random.nextInt(1200 - 600 + 1) + 600; + } + } + + if (level().isClientSide) { + // Capture previous for smooth interpolation between ticks + this.tailSwayPrev = this.tailSwayOffset; + updateProceduralTailSway(); + } + } + + private void updateProceduralTailSway() { + // Turn input derived from rotation deltas; works even when standing still and turning + float turnDegrees = getSignedTurnDelta(); + + // Deadzone to ignore tiny jitter so the tail can return to center cleanly + float deadzoneDeg = 0.6f; // smaller deadzone for more responsiveness + float turnInput = 0.0f; + if (Math.abs(turnDegrees) >= deadzoneDeg) { + // Higher sensitivity so small in-place turns still affect the model + turnInput = Mth.clamp(turnDegrees / 15.0f, -1.0f, 1.0f); + } + + // Target offset: keep intuitive sign (positive input -> positive sway) + float target = turnInput; + + // One-pole low-pass (no bounce). Larger alpha => snappier and less "stiff". + float alpha = 0.24f; // try 0.20–0.30 to taste + + tailSwayOffset += (target - tailSwayOffset) * alpha; + + // Snap tiny residuals to zero so it visibly settles + if (Math.abs(tailSwayOffset) < 0.003f) { + tailSwayOffset = 0.0f; + } + + // No oscillation velocity retained + tailSwayVelocity = 0.0f; + + tailSwayOffset = Mth.clamp(tailSwayOffset, -1.5f, 1.5f); + } + + // Expose to the model for bone rotation + public float getTailSwayOffset() { + return tailSwayOffset; + } + + // Interpolated sway for smooth rendering between ticks + public float getTailSwayOffset(float partialTick) { + return Mth.lerp(Mth.clamp(partialTick, 0.0f, 1.0f), tailSwayPrev, tailSwayOffset); + } + + /*? if <=1.20.1 {*/ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); + this.entityData.define(VARIANT, 0); + this.entityData.define(DATA_SYNCED_AGE, 0); + } + /*?} else {*/ + /*@Override + protected void defineSynchedData(SynchedEntityData.Builder pBuilder) { + super.defineSynchedData(pBuilder); + pBuilder.define(VARIANT, 0); + pBuilder.define(DATA_SYNCED_AGE, 0); + } + *//*?}*/ + + public int getSyncedAge() { + return this.entityData.get(DATA_SYNCED_AGE); + } + public int getTypeVariant() { + return this.entityData.get(VARIANT); + } + + public TitanosaurusVariant getVariant() { + return TitanosaurusVariant.byId(this.getTypeVariant() & 255); + } + + private void setVariant(TitanosaurusVariant variant) { + this.entityData.set(VARIANT, variant.getId() & 255); + } + + @Override + public boolean canMate(Animal other) { + if (!super.canMate(other)) return false; + if (!(other instanceof TitanosaurusEntity that)) return false; + return this.getVariant() != that.getVariant(); + } + @Override + public void addAdditionalSaveData(CompoundTag pCompound) { + super.addAdditionalSaveData(pCompound); + pCompound.putInt("Variant", this.getTypeVariant()); + } + + @Override + public void readAdditionalSaveData(CompoundTag pCompound) { + super.readAdditionalSaveData(pCompound); + this.entityData.set(VARIANT, pCompound.getInt("Variant")); + } + + /*? if <=1.20.1 {*/ + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor pLevel, DifficultyInstance pDifficulty, MobSpawnType pReason, @Nullable SpawnGroupData pSpawnData, @Nullable CompoundTag pDataTag) { + TitanosaurusVariant variant = Util.getRandom(TitanosaurusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(pLevel, pDifficulty, pReason, pSpawnData, pDataTag); + } + /*?} else {*/ + /*@Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, MobSpawnType spawnType, @Nullable SpawnGroupData spawnGroupData) { + TitanosaurusVariant variant = Util.getRandom(TitanosaurusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(level, difficulty, spawnType, spawnGroupData); + } + *//*?}*/ + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } + + @Override + protected @Nullable SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.TITANOSAURUS_HURT.get(); + } + + @Override + protected void playStepSound(BlockPos pos, BlockState blockIn) { + this.playSound(ModSounds.STOMP.get(), 0.5F, 1.0F); + } + + @Override + protected @Nullable SoundEvent getDeathSound() { + return ModSounds.TITANOSAURUS_DEATH.get(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/custom/TriceratopsEntity.java b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/TriceratopsEntity.java new file mode 100644 index 0000000..d418f90 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/TriceratopsEntity.java @@ -0,0 +1,307 @@ +package net.cmr.jurassicrevived.entity.custom; + +import net.cmr.jurassicrevived.entity.ModEntities; +import net.cmr.jurassicrevived.entity.ai.SprintingMeleeAttackGoal; +import net.cmr.jurassicrevived.entity.ai.SprintingPanicGoal; +import net.cmr.jurassicrevived.entity.client.AlbertosaurusVariant; +import net.cmr.jurassicrevived.entity.client.TriceratopsVariant; +import net.cmr.jurassicrevived.sound.ModSounds; +import net.minecraft.Util; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.util.Mth; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.*; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.attributes.DefaultAttributes; +import net.minecraft.world.entity.ai.goal.*; +import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerLevelAccessor; +import org.jetbrains.annotations.Nullable; +import software.bernie.geckolib.animatable.GeoEntity; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.core.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.core.animation.AnimatableManager; +import software.bernie.geckolib.core.animation.Animation; +import software.bernie.geckolib.core.animation.AnimationController; +import software.bernie.geckolib.core.animation.RawAnimation; +import software.bernie.geckolib.core.object.PlayState; +/*?} else {*/ +/*import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.animation.*; +*//*?}*/ + +public class TriceratopsEntity extends Animal implements GeoEntity { + private AnimatableInstanceCache cache = new SingletonAnimatableInstanceCache(this); + + private static final EntityDataAccessor VARIANT = + SynchedEntityData.defineId(TriceratopsEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SYNCED_AGE = + SynchedEntityData.defineId(TriceratopsEntity.class, EntityDataSerializers.INT); + + // Procedural tail sway state (client-side use for rendering) + private float tailSwayOffset; // Smoothed offset in range roughly [-1, 1] + private float tailSwayVelocity; // Internal velocity for spring-damper + private float tailSwayPrev; // Previous frame value for interpolation + + public TriceratopsEntity(EntityType pEntityType, Level pLevel) { + super(pEntityType, pLevel); + } + + @Override + protected void registerGoals() {this.goalSelector.addGoal(1, new SprintingPanicGoal(this, 1.15) { + @Override + public boolean canUse() { + return TriceratopsEntity.this.isBaby() && super.canUse(); + } + }); + this.targetSelector.addGoal(1, new HurtByTargetGoal(this).setAlertOthers()); + this.goalSelector.addGoal(2, new FloatGoal(this)); + this.goalSelector.addGoal(3, new AvoidEntityGoal<>(this, VelociraptorEntity.class, (float) 20, 0.8, 0.8)); + this.goalSelector.addGoal(4, new AvoidEntityGoal<>(this, CeratosaurusEntity.class, (float) 20, 0.8, 0.8)); + this.goalSelector.addGoal(5, new AvoidEntityGoal<>(this, DilophosaurusEntity.class, (float) 20, 0.8, 0.8)); + this.goalSelector.addGoal(6, new SprintingMeleeAttackGoal(this, 1.1, false)); + this.goalSelector.addGoal(7, new BreedGoal(this, 1.0)); + this.goalSelector.addGoal(8, new FollowParentGoal(this, 1.25)); + this.goalSelector.addGoal(8, new WaterAvoidingRandomStrollGoal(this, 1.0)); + this.goalSelector.addGoal(10, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(11, new FollowMobGoal(this, 0.8, (float) 20, (float) 10)); + this.goalSelector.addGoal(12, new AvoidEntityGoal<>(this, SpinosaurusEntity.class, (float) 20, 0.8, 0.8)); + this.goalSelector.addGoal(13, new AvoidEntityGoal<>(this, TyrannosaurusRexEntity.class, (float) 20, 0.8, 0.8)); + this.goalSelector.addGoal(14, new AvoidEntityGoal<>(this, IndominusRexEntity.class, (float) 20, 0.8, 0.8)); + this.goalSelector.addGoal(15, new EatBlockGoal(this)); + this.goalSelector.addGoal(16, new RandomLookAroundGoal(this)); + + } + + public static AttributeSupplier.Builder createAttributes() { + return Animal.createLivingAttributes() + .add(Attributes.MAX_HEALTH, 60D) + .add(Attributes.MOVEMENT_SPEED, 0.3D) + .add(Attributes.ARMOR, 0D) + .add(Attributes.FOLLOW_RANGE, 32D) + .add(Attributes.KNOCKBACK_RESISTANCE, 0D) + .add(Attributes.ATTACK_KNOCKBACK, 0D) + .add(Attributes.ATTACK_DAMAGE, 16D); + } + + @Override + public boolean isFood(ItemStack pStack) { + return pStack.is(Items.KELP); + } + + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel pLevel, AgeableMob pOtherParent) { + AgeableMob child = ModEntities.TRICERATOPS.get().create(pLevel); + if (child instanceof TriceratopsEntity baby) { + TriceratopsVariant randomVariant = Util.getRandom(TriceratopsVariant.values(), this.random); + baby.setVariant(randomVariant); + } + return child; + } + + @Override + public boolean doHurtTarget(Entity target) { + boolean hit = super.doHurtTarget(target); + if (!level().isClientSide && hit && target instanceof LivingEntity) { + if (this.level() instanceof ServerLevel serverLevel) { + this.triggerAnim("attackController", "attack"); + } + } + return hit; + } + + @Override + public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { + controllers.add(new AnimationController<>(this, "Walk/Run/Idle", state -> { + if (state.isMoving()) + return state.setAndContinue(TriceratopsEntity.this.isSprinting() ? RawAnimation.begin().then("anim.triceratops.run", Animation.LoopType.LOOP) : RawAnimation.begin().then("anim.triceratops.walk", Animation.LoopType.LOOP)); + + return state.setAndContinue(RawAnimation.begin().then("anim.triceratops.idle", Animation.LoopType.LOOP)); + })); + + controllers.add(new AnimationController<>(this, "attackController", state -> PlayState.STOP) + .triggerableAnim("attack", RawAnimation.begin().then("anim.triceratops.attack", Animation.LoopType.PLAY_ONCE))); + + controllers.add(new AnimationController<>(this, "mouthController", state -> PlayState.STOP) + .triggerableAnim("mouth", RawAnimation.begin().then("anim.triceratops.mouth", Animation.LoopType.PLAY_ONCE))); + } + + private float getSignedTurnDelta() { + // Only consider the body (torso) rotation so head look does not affect tail sway + return Mth.wrapDegrees(this.yBodyRot - this.yBodyRotO); + } + + private int mouthAnimCooldown = 0; + + @Override + public void tick() { + super.tick(); + if (!level().isClientSide) { + this.entityData.set(DATA_SYNCED_AGE, this.getAge()); + var maxHealthAttr = getAttribute(Attributes.MAX_HEALTH); + if (maxHealthAttr != null) { + double baseAdult = DefaultAttributes.getSupplier((EntityType) this.getType()).getValue(Attributes.MAX_HEALTH); + double desired = this.isBaby() ? baseAdult * 0.10D : baseAdult; + if (maxHealthAttr.getBaseValue() != desired) { + double oldMax = maxHealthAttr.getBaseValue(); + double healthRatio = this.getHealth() / (float) oldMax; + maxHealthAttr.setBaseValue(desired); + this.setHealth((float) (desired * Mth.clamp(healthRatio, 0.0F, 1.0F))); + } + } + } + + if (!level().isClientSide) { + if (mouthAnimCooldown > 0) { + mouthAnimCooldown--; + } else { + this.triggerAnim("mouthController", "mouth"); + this.playSound(ModSounds.TRICERATOPS_CALL.get(), 1.0F, 1.0F); + // 30s–60s in ticks + mouthAnimCooldown = this.random.nextInt(1200 - 600 + 1) + 600; + } + } + + if (level().isClientSide) { + // Capture previous for smooth interpolation between ticks + this.tailSwayPrev = this.tailSwayOffset; + updateProceduralTailSway(); + } + } + + private void updateProceduralTailSway() { + // Turn input derived from rotation deltas; works even when standing still and turning + float turnDegrees = getSignedTurnDelta(); + + // Deadzone to ignore tiny jitter so the tail can return to center cleanly + float deadzoneDeg = 0.6f; // smaller deadzone for more responsiveness + float turnInput = 0.0f; + if (Math.abs(turnDegrees) >= deadzoneDeg) { + // Higher sensitivity so small in-place turns still affect the model + turnInput = Mth.clamp(turnDegrees / 15.0f, -1.0f, 1.0f); + } + + // Target offset: keep intuitive sign (positive input -> positive sway) + float target = turnInput; + + // One-pole low-pass (no bounce). Larger alpha => snappier and less "stiff". + float alpha = 0.24f; // try 0.20–0.30 to taste + + tailSwayOffset += (target - tailSwayOffset) * alpha; + + // Snap tiny residuals to zero so it visibly settles + if (Math.abs(tailSwayOffset) < 0.003f) { + tailSwayOffset = 0.0f; + } + + // No oscillation velocity retained + tailSwayVelocity = 0.0f; + + tailSwayOffset = Mth.clamp(tailSwayOffset, -1.5f, 1.5f); + } + + // Expose to the model for bone rotation + public float getTailSwayOffset() { + return tailSwayOffset; + } + + // Interpolated sway for smooth rendering between ticks + public float getTailSwayOffset(float partialTick) { + return Mth.lerp(Mth.clamp(partialTick, 0.0f, 1.0f), tailSwayPrev, tailSwayOffset); + } + + /*? if <=1.20.1 {*/ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); + this.entityData.define(VARIANT, 0); + this.entityData.define(DATA_SYNCED_AGE, 0); + } + /*?} else {*/ + /*@Override + protected void defineSynchedData(SynchedEntityData.Builder pBuilder) { + super.defineSynchedData(pBuilder); + pBuilder.define(VARIANT, 0); + pBuilder.define(DATA_SYNCED_AGE, 0); + } + *//*?}*/ + + public int getSyncedAge() { + return this.entityData.get(DATA_SYNCED_AGE); + } + public int getTypeVariant() { + return this.entityData.get(VARIANT); + } + + public TriceratopsVariant getVariant() { + return TriceratopsVariant.byId(this.getTypeVariant() & 255); + } + + private void setVariant(TriceratopsVariant variant) { + this.entityData.set(VARIANT, variant.getId() & 255); + } + + @Override + public boolean canMate(Animal other) { + if (!super.canMate(other)) return false; + if (!(other instanceof TriceratopsEntity that)) return false; + return this.getVariant() != that.getVariant(); + } + @Override + public void addAdditionalSaveData(CompoundTag pCompound) { + super.addAdditionalSaveData(pCompound); + pCompound.putInt("Variant", this.getTypeVariant()); + } + + @Override + public void readAdditionalSaveData(CompoundTag pCompound) { + super.readAdditionalSaveData(pCompound); + this.entityData.set(VARIANT, pCompound.getInt("Variant")); + } + + /*? if <=1.20.1 {*/ + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor pLevel, DifficultyInstance pDifficulty, MobSpawnType pReason, @Nullable SpawnGroupData pSpawnData, @Nullable CompoundTag pDataTag) { + TriceratopsVariant variant = Util.getRandom(TriceratopsVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(pLevel, pDifficulty, pReason, pSpawnData, pDataTag); + } + /*?} else {*/ + /*@Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, MobSpawnType spawnType, @Nullable SpawnGroupData spawnGroupData) { + TriceratopsVariant variant = Util.getRandom(TriceratopsVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(level, difficulty, spawnType, spawnGroupData); + } + *//*?}*/ + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } + + @Override + protected @Nullable SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.TRICERATOPS_HURT.get(); + } + + @Override + protected @Nullable SoundEvent getDeathSound() { + return ModSounds.TRICERATOPS_DEATH.get(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/custom/TroodonEntity.java b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/TroodonEntity.java new file mode 100644 index 0000000..55c8269 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/TroodonEntity.java @@ -0,0 +1,314 @@ +package net.cmr.jurassicrevived.entity.custom; + +import net.cmr.jurassicrevived.entity.ModEntities; +import net.cmr.jurassicrevived.entity.ai.SprintingMeleeAttackGoal; +import net.cmr.jurassicrevived.entity.ai.SprintingPanicGoal; +import net.cmr.jurassicrevived.entity.client.AlbertosaurusVariant; +import net.cmr.jurassicrevived.entity.client.TroodonVariant; +import net.cmr.jurassicrevived.sound.ModSounds; +import net.minecraft.Util; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.util.Mth; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.*; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.attributes.DefaultAttributes; +import net.minecraft.world.entity.ai.goal.*; +import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal; +import net.minecraft.world.entity.ai.goal.target.NearestAttackableTargetGoal; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerLevelAccessor; +import org.jetbrains.annotations.Nullable; +import software.bernie.geckolib.animatable.GeoEntity; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.core.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.core.animation.AnimatableManager; +import software.bernie.geckolib.core.animation.Animation; +import software.bernie.geckolib.core.animation.AnimationController; +import software.bernie.geckolib.core.animation.RawAnimation; +import software.bernie.geckolib.core.object.PlayState; +/*?} else {*/ +/*import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.animation.*; +*//*?}*/ + +public class TroodonEntity extends Animal implements GeoEntity { + private AnimatableInstanceCache cache = new SingletonAnimatableInstanceCache(this); + + private static final EntityDataAccessor VARIANT = + SynchedEntityData.defineId(TroodonEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SYNCED_AGE = + SynchedEntityData.defineId(TroodonEntity.class, EntityDataSerializers.INT); + + // Procedural tail sway state (client-side use for rendering) + private float tailSwayOffset; // Smoothed offset in range roughly [-1, 1] + private float tailSwayVelocity; // Internal velocity for spring-damper + private float tailSwayPrev; // Previous frame value for interpolation + + public TroodonEntity(EntityType pEntityType, Level pLevel) { + super(pEntityType, pLevel); + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new SprintingPanicGoal(this, 1.15) { + @Override + public boolean canUse() { + return TroodonEntity.this.isBaby() && super.canUse(); + } + }); + this.targetSelector.addGoal(1, new HurtByTargetGoal(this).setAlertOthers()); + this.goalSelector.addGoal(2, new FloatGoal(this)); + this.goalSelector.addGoal(3, new AvoidEntityGoal<>(this, TriceratopsEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(4, new AvoidEntityGoal<>(this, SpinosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(5, new AvoidEntityGoal<>(this, TyrannosaurusRexEntity.class, (float) 20, 1.2, 1.2)); + this.goalSelector.addGoal(6, new AvoidEntityGoal<>(this, IndominusRexEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(7, new AvoidEntityGoal<>(this, BrachiosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(8, new SprintingMeleeAttackGoal(this, 1.1, false)); + this.goalSelector.addGoal(9, new BreedGoal(this, 1.0)); + this.goalSelector.addGoal(10, new FollowParentGoal(this, 1.25)); + this.goalSelector.addGoal(11, new WaterAvoidingRandomStrollGoal(this, 1.0)); + this.goalSelector.addGoal(12, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(13, new FollowMobGoal(this, 1, (float) 20, (float) 10)); + this.targetSelector.addGoal(14, new NearestAttackableTargetGoal<>(this, Animal.class, 10, false, false, + target -> target.getType() != this.getType())); + this.targetSelector.addGoal(15, new NearestAttackableTargetGoal(this, GallimimusEntity.class, false, false)); + this.targetSelector.addGoal(16, new NearestAttackableTargetGoal(this, DilophosaurusEntity.class, false, false)); + this.targetSelector.addGoal(17, new NearestAttackableTargetGoal(this, CeratosaurusEntity.class, false, false)); + this.targetSelector.addGoal(18, new NearestAttackableTargetGoal(this, ParasaurolophusEntity.class, false, false)); + this.targetSelector.addGoal(19, new NearestAttackableTargetGoal(this, Player.class, false, false)); + this.targetSelector.addGoal(20, new NearestAttackableTargetGoal(this, CompsognathusEntity.class, false, false)); + this.goalSelector.addGoal(21, new RandomLookAroundGoal(this)); + } + + public static AttributeSupplier.Builder createAttributes() { + return Animal.createLivingAttributes() + .add(Attributes.MAX_HEALTH, 20D) + .add(Attributes.MOVEMENT_SPEED, 0.3D) + .add(Attributes.ARMOR, 0D) + .add(Attributes.FOLLOW_RANGE, 32D) + .add(Attributes.ATTACK_DAMAGE, 5D) + .add(Attributes.KNOCKBACK_RESISTANCE, 0.3D) + .add(Attributes.ATTACK_KNOCKBACK, 0D); + } + + @Override + public boolean isFood(ItemStack pStack) { + return pStack.is(Items.BEEF); + } + + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel pLevel, AgeableMob pOtherParent) { + AgeableMob child = ModEntities.TROODON.get().create(pLevel); + if (child instanceof TroodonEntity baby) { + TroodonVariant randomVariant = Util.getRandom(TroodonVariant.values(), this.random); + baby.setVariant(randomVariant); + } + return child; + } + + @Override + public boolean doHurtTarget(Entity target) { + boolean hit = super.doHurtTarget(target); + if (!level().isClientSide && hit && target instanceof LivingEntity) { + if (this.level() instanceof ServerLevel serverLevel) { + this.triggerAnim("attackController", "attack"); + } + } + return hit; + } + + @Override + public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { + controllers.add(new AnimationController<>(this, "Walk/Run/Idle", state -> { + if (state.isMoving()) + return state.setAndContinue(TroodonEntity.this.isSprinting() ? RawAnimation.begin().then("anim.troodon.run", Animation.LoopType.LOOP) : RawAnimation.begin().then("anim.troodon.walk", Animation.LoopType.LOOP)); + + return state.setAndContinue(RawAnimation.begin().then("anim.troodon.idle", Animation.LoopType.LOOP)); + })); + + controllers.add(new AnimationController<>(this, "attackController", state -> PlayState.STOP) + .triggerableAnim("attack", RawAnimation.begin().then("anim.troodon.attack", Animation.LoopType.PLAY_ONCE))); + + controllers.add(new AnimationController<>(this, "mouthController", state -> PlayState.STOP) + .triggerableAnim("mouth", RawAnimation.begin().then("anim.troodon.mouth", Animation.LoopType.PLAY_ONCE))); + } + + private float getSignedTurnDelta() { + // Only consider the body (torso) rotation so head look does not affect tail sway + return Mth.wrapDegrees(this.yBodyRot - this.yBodyRotO); + } + + private int mouthAnimCooldown = 0; + + @Override + public void tick() { + super.tick(); + if (!level().isClientSide) { + this.entityData.set(DATA_SYNCED_AGE, this.getAge()); + var maxHealthAttr = getAttribute(Attributes.MAX_HEALTH); + if (maxHealthAttr != null) { + double baseAdult = DefaultAttributes.getSupplier((EntityType) this.getType()).getValue(Attributes.MAX_HEALTH); + double desired = this.isBaby() ? baseAdult * 0.10D : baseAdult; + if (maxHealthAttr.getBaseValue() != desired) { + double oldMax = maxHealthAttr.getBaseValue(); + double healthRatio = this.getHealth() / (float) oldMax; + maxHealthAttr.setBaseValue(desired); + this.setHealth((float) (desired * Mth.clamp(healthRatio, 0.0F, 1.0F))); + } + } + } + + if (!level().isClientSide) { + if (mouthAnimCooldown > 0) { + mouthAnimCooldown--; + } else { + this.triggerAnim("mouthController", "mouth"); + this.playSound(ModSounds.TROODON_CALL.get(), 1.0F, 1.0F); + // 30s–60s in ticks + mouthAnimCooldown = this.random.nextInt(1200 - 600 + 1) + 600; + } + } + + if (level().isClientSide) { + // Capture previous for smooth interpolation between ticks + this.tailSwayPrev = this.tailSwayOffset; + updateProceduralTailSway(); + } + } + + private void updateProceduralTailSway() { + // Turn input derived from rotation deltas; works even when standing still and turning + float turnDegrees = getSignedTurnDelta(); + + // Deadzone to ignore tiny jitter so the tail can return to center cleanly + float deadzoneDeg = 0.6f; // smaller deadzone for more responsiveness + float turnInput = 0.0f; + if (Math.abs(turnDegrees) >= deadzoneDeg) { + // Higher sensitivity so small in-place turns still affect the model + turnInput = Mth.clamp(turnDegrees / 15.0f, -1.0f, 1.0f); + } + + // Target offset: keep intuitive sign (positive input -> positive sway) + float target = turnInput; + + // One-pole low-pass (no bounce). Larger alpha => snappier and less "stiff". + float alpha = 0.24f; // try 0.20–0.30 to taste + + tailSwayOffset += (target - tailSwayOffset) * alpha; + + // Snap tiny residuals to zero so it visibly settles + if (Math.abs(tailSwayOffset) < 0.003f) { + tailSwayOffset = 0.0f; + } + + // No oscillation velocity retained + tailSwayVelocity = 0.0f; + + tailSwayOffset = Mth.clamp(tailSwayOffset, -1.5f, 1.5f); + } + + // Expose to the model for bone rotation + public float getTailSwayOffset() { + return tailSwayOffset; + } + + // Interpolated sway for smooth rendering between ticks + public float getTailSwayOffset(float partialTick) { + return Mth.lerp(Mth.clamp(partialTick, 0.0f, 1.0f), tailSwayPrev, tailSwayOffset); + } + + /*? if <=1.20.1 {*/ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); + this.entityData.define(VARIANT, 0); + this.entityData.define(DATA_SYNCED_AGE, 0); + } + /*?} else {*/ + /*@Override + protected void defineSynchedData(SynchedEntityData.Builder pBuilder) { + super.defineSynchedData(pBuilder); + pBuilder.define(VARIANT, 0); + pBuilder.define(DATA_SYNCED_AGE, 0); + } + *//*?}*/ + + public int getSyncedAge() { + return this.entityData.get(DATA_SYNCED_AGE); + } + public int getTypeVariant() { + return this.entityData.get(VARIANT); + } + + public TroodonVariant getVariant() { + return TroodonVariant.byId(this.getTypeVariant() & 255); + } + + private void setVariant(TroodonVariant variant) { + this.entityData.set(VARIANT, variant.getId() & 255); + } + + @Override + public boolean canMate(Animal other) { + if (!super.canMate(other)) return false; + if (!(other instanceof TroodonEntity that)) return false; + return this.getVariant() != that.getVariant(); + } + @Override + public void addAdditionalSaveData(CompoundTag pCompound) { + super.addAdditionalSaveData(pCompound); + pCompound.putInt("Variant", this.getTypeVariant()); + } + + @Override + public void readAdditionalSaveData(CompoundTag pCompound) { + super.readAdditionalSaveData(pCompound); + this.entityData.set(VARIANT, pCompound.getInt("Variant")); + } + + /*? if <=1.20.1 {*/ + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor pLevel, DifficultyInstance pDifficulty, MobSpawnType pReason, @Nullable SpawnGroupData pSpawnData, @Nullable CompoundTag pDataTag) { + TroodonVariant variant = Util.getRandom(TroodonVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(pLevel, pDifficulty, pReason, pSpawnData, pDataTag); + } + /*?} else {*/ + /*@Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, MobSpawnType spawnType, @Nullable SpawnGroupData spawnGroupData) { + TroodonVariant variant = Util.getRandom(TroodonVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(level, difficulty, spawnType, spawnGroupData); + } + *//*?}*/ + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } + + @Override + protected @Nullable SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.TROODON_HURT.get(); + } + + @Override + protected @Nullable SoundEvent getDeathSound() { + return ModSounds.TROODON_DEATH.get(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/custom/TropeognathusEntity.java b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/TropeognathusEntity.java new file mode 100644 index 0000000..1a07680 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/TropeognathusEntity.java @@ -0,0 +1,411 @@ +package net.cmr.jurassicrevived.entity.custom; + +import net.cmr.jurassicrevived.entity.ModEntities; +import net.cmr.jurassicrevived.entity.ai.SprintingMeleeAttackGoal; +import net.cmr.jurassicrevived.entity.ai.SprintingPanicGoal; +import net.cmr.jurassicrevived.entity.client.AlbertosaurusVariant; +import net.cmr.jurassicrevived.entity.client.TropeognathusVariant; +import net.cmr.jurassicrevived.sound.ModSounds; +import net.minecraft.Util; +import net.minecraft.core.BlockPos; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.*; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.attributes.DefaultAttributes; +import net.minecraft.world.entity.ai.control.FlyingMoveControl; +import net.minecraft.world.entity.ai.goal.*; +import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal; +import net.minecraft.world.entity.ai.goal.target.NearestAttackableTargetGoal; +import net.minecraft.world.entity.ai.navigation.FlyingPathNavigation; +import net.minecraft.world.entity.ai.navigation.PathNavigation; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.animal.FlyingAnimal; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerLevelAccessor; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.phys.Vec3; +import org.jetbrains.annotations.Nullable; +import software.bernie.geckolib.animatable.GeoEntity; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.core.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.core.animation.AnimatableManager; +import software.bernie.geckolib.core.animation.Animation; +import software.bernie.geckolib.core.animation.AnimationController; +import software.bernie.geckolib.core.animation.RawAnimation; +import software.bernie.geckolib.core.object.PlayState; +/*?} else {*/ +/*import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.animation.*; +*//*?}*/ + +public class TropeognathusEntity extends Animal implements GeoEntity, FlyingAnimal { + private final AnimatableInstanceCache cache = new SingletonAnimatableInstanceCache(this); + + private static final EntityDataAccessor VARIANT = + SynchedEntityData.defineId(TropeognathusEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SYNCED_AGE = + SynchedEntityData.defineId(TropeognathusEntity.class, EntityDataSerializers.INT); + + // Procedural tail sway state (client-side use for rendering) + private float tailSwayOffset; // Smoothed offset in range roughly [-1, 1] + private float tailSwayVelocity; // Internal velocity for spring-damper + private float tailSwayPrev; // Previous frame value for interpolation + + public TropeognathusEntity(EntityType pEntityType, Level pLevel) { + super(pEntityType, pLevel); + this.moveControl = new FlyingMoveControl(this, 20, true); + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new SprintingPanicGoal(this, 1.15) { + @Override + public boolean canUse() { + return TropeognathusEntity.this.isBaby() && super.canUse(); + } + }); + this.goalSelector.addGoal(2, new FloatGoal(this)); + this.goalSelector.addGoal(3, new AvoidEntityGoal<>(this, BrachiosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(4, new SprintingMeleeAttackGoal(this, 1.1, false)); + this.goalSelector.addGoal(5, new BreedGoal(this, 1.0)); + this.goalSelector.addGoal(6, new FollowParentGoal(this, 1.25)); + + // Goal 7: Wander on ground (Walk) - Only when on ground + this.goalSelector.addGoal(7, new WaterAvoidingRandomStrollGoal(this, 1.0) { + @Override + public boolean canUse() { + return TropeognathusEntity.this.onGround() && super.canUse(); + } + }); + + // Goal 8: Wander in air (Fly) - Handles takeoff, flying, and landing + this.goalSelector.addGoal(8, new WaterAvoidingRandomStrollGoal(this, 1.0, 20) { + @Override + public boolean canUse() { + boolean isFlying = !TropeognathusEntity.this.onGround(); + // If flying, keep flying. If on ground, small chance (1/400 ticks) to take off. + return (isFlying || TropeognathusEntity.this.getRandom().nextInt(400) == 0) && super.canUse(); + } + + @Override + protected Vec3 getPosition() { + Vec3 pos = TropeognathusEntity.this.position(); + RandomSource random = TropeognathusEntity.this.getRandom(); + + double x = pos.x + (random.nextFloat() * 2 - 1) * 32; + double z = pos.z + (random.nextFloat() * 2 - 1) * 32; + + // Get ground height at the random destination (returns Y of first air block) + int groundY = TropeognathusEntity.this.level().getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, (int)x, (int)z); + double y; + + if (TropeognathusEntity.this.onGround()) { + // Takeoff: Target well above ground to ensure liftoff + y = pos.y + 15 + random.nextInt(10); + } else { + // Flying: 5% chance to land, otherwise stay airborne but capped height + if (random.nextFloat() < 0.05f) { + y = groundY; // Land + } else if (pos.y > groundY + 20) { + // Too high: Force descent + y = pos.y - 5 - random.nextInt(10); + } else { + // Just wander up or down a bit + y = pos.y + (random.nextFloat() * 2 - 1) * 10; + } + } + + // Don't target below the ground (blocks) + if (y < groundY) y = groundY; + + return new Vec3(x, y, z); + } + }); + + this.goalSelector.addGoal(9, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(10, new FollowMobGoal(this, 0.8, (float) 20, (float) 10)); + this.targetSelector.addGoal(11, new NearestAttackableTargetGoal<>(this, Animal.class, 10, false, false, + target -> { + // 1. Don't eat your own species + if (target.getType() == this.getType()) return false; + + // 2. Don't eat Flying Animals + if (target instanceof FlyingAnimal) return false; + + // 3. SIZE CHECK: specific height and width limits + // Example: Height < 1.0 blocks AND Width < 1.0 blocks + boolean isSmallEnough = target.getBbHeight() <= 1.0F && target.getBbWidth() <= 1.0F; + + return isSmallEnough; + } + )); + this.targetSelector.addGoal(12, new NearestAttackableTargetGoal<>(this, Player.class, false, false)); + this.targetSelector.addGoal(13, new HurtByTargetGoal(this)); + this.goalSelector.addGoal(14, new RandomLookAroundGoal(this)); + this.goalSelector.addGoal(15, new FloatGoal(this)); + } + + public static AttributeSupplier.Builder createAttributes() { + return Animal.createLivingAttributes() + .add(Attributes.MAX_HEALTH, 50D) + .add(Attributes.MOVEMENT_SPEED, 0.3D) + .add(Attributes.FLYING_SPEED, 0.6D) + .add(Attributes.ARMOR, 0D) + .add(Attributes.FOLLOW_RANGE, 32D) + .add(Attributes.KNOCKBACK_RESISTANCE, 0D) + .add(Attributes.ATTACK_KNOCKBACK, 0D) + .add(Attributes.ATTACK_DAMAGE, 8D); + } + + @Override + protected PathNavigation createNavigation(Level pLevel) { + FlyingPathNavigation navigation = new FlyingPathNavigation(this, pLevel); + navigation.setCanOpenDoors(false); + navigation.setCanFloat(true); + navigation.setCanPassDoors(true); + return navigation; + } + + @Override + public boolean isFlying() { + return !this.onGround(); + } + + @Override + public boolean causeFallDamage(float pFallDistance, float pMultiplier, DamageSource pSource) { + return false; + } + + @Override + protected void checkFallDamage(double pY, boolean pOnGround, BlockState pState, BlockPos pPos) { + } + + @Override + public boolean isFood(ItemStack pStack) { + return pStack.is(Items.BEEF); + } + + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel pLevel, AgeableMob pOtherParent) { + AgeableMob child = ModEntities.TROPEOGNATHUS.get().create(pLevel); + if (child instanceof TropeognathusEntity baby) { + TropeognathusVariant randomVariant = Util.getRandom(TropeognathusVariant.values(), this.random); + baby.setVariant(randomVariant); + } + return child; + } + + @Override + public boolean doHurtTarget(Entity target) { + boolean hit = super.doHurtTarget(target); + if (!level().isClientSide && hit && target instanceof LivingEntity) { + if (this.level() instanceof ServerLevel serverLevel) { + this.triggerAnim("attackController", "attack"); + this.playSound(ModSounds.BEAK_ATTACK.get(), 1.0F, 1.0F); + } + } + return hit; + } + + @Override + public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { + controllers.add(new AnimationController<>(this, "Walk/Run/Idle", state -> { + if (!TropeognathusEntity.this.onGround()) { + return state.setAndContinue(RawAnimation.begin().then("anim.tropeognathus.fly", Animation.LoopType.LOOP)); + } + + if (state.isMoving()) + return state.setAndContinue(TropeognathusEntity.this.isSprinting() ? RawAnimation.begin().then("anim.tropeognathus.walk", Animation.LoopType.LOOP) : RawAnimation.begin().then("anim.tropeognathus.walk", Animation.LoopType.LOOP)); + + return state.setAndContinue(RawAnimation.begin().then("anim.tropeognathus.idle", Animation.LoopType.LOOP)); + })); + + controllers.add(new AnimationController<>(this, "attackController", state -> PlayState.STOP) + .triggerableAnim("attack", RawAnimation.begin().then("anim.tropeognathus.attack", Animation.LoopType.PLAY_ONCE))); + + controllers.add(new AnimationController<>(this, "mouthController", state -> PlayState.STOP) + .triggerableAnim("mouth", RawAnimation.begin().then("anim.tropeognathus.mouth", Animation.LoopType.PLAY_ONCE))); + } + + private float getSignedTurnDelta() { + // Only consider the body (torso) rotation so head look does not affect tail sway + return Mth.wrapDegrees(this.yBodyRot - this.yBodyRotO); + } + + private int mouthAnimCooldown = 0; + + @Override + public void tick() { + super.tick(); + if (!level().isClientSide) { + this.entityData.set(DATA_SYNCED_AGE, this.getAge()); + var maxHealthAttr = getAttribute(Attributes.MAX_HEALTH); + if (maxHealthAttr != null) { + double baseAdult = DefaultAttributes.getSupplier((EntityType) this.getType()).getValue(Attributes.MAX_HEALTH); + double desired = this.isBaby() ? baseAdult * 0.10D : baseAdult; + if (maxHealthAttr.getBaseValue() != desired) { + double oldMax = maxHealthAttr.getBaseValue(); + double healthRatio = this.getHealth() / (float) oldMax; + maxHealthAttr.setBaseValue(desired); + this.setHealth((float) (desired * Mth.clamp(healthRatio, 0.0F, 1.0F))); + } + } + } + + if (!level().isClientSide) { + if (mouthAnimCooldown > 0) { + mouthAnimCooldown--; + } else { + this.triggerAnim("mouthController", "mouth"); + this.playSound(ModSounds.TROPEOGNATHUS_CALL.get(), 1.0F, 1.0F); + // 30s–60s in ticks + mouthAnimCooldown = this.random.nextInt(1200 - 600 + 1) + 600; + } + } + + if (level().isClientSide) { + // Capture previous for smooth interpolation between ticks + this.tailSwayPrev = this.tailSwayOffset; + updateProceduralTailSway(); + } + } + + private void updateProceduralTailSway() { + // Turn input derived from rotation deltas; works even when standing still and turning + float turnDegrees = getSignedTurnDelta(); + + // Deadzone to ignore tiny jitter so the tail can return to center cleanly + float deadzoneDeg = 0.6f; // smaller deadzone for more responsiveness + float turnInput = 0.0f; + if (Math.abs(turnDegrees) >= deadzoneDeg) { + // Higher sensitivity so small in-place turns still affect the model + turnInput = Mth.clamp(turnDegrees / 15.0f, -1.0f, 1.0f); + } + + // Target offset: keep intuitive sign (positive input -> positive sway) + float target = turnInput; + + // One-pole low-pass (no bounce). Larger alpha => snappier and less "stiff". + float alpha = 0.24f; // try 0.20–0.30 to taste + + tailSwayOffset += (target - tailSwayOffset) * alpha; + + // Snap tiny residuals to zero so it visibly settles + if (Math.abs(tailSwayOffset) < 0.003f) { + tailSwayOffset = 0.0f; + } + + // No oscillation velocity retained + tailSwayVelocity = 0.0f; + + tailSwayOffset = Mth.clamp(tailSwayOffset, -1.5f, 1.5f); + } + + // Expose to the model for bone rotation + public float getTailSwayOffset() { + return tailSwayOffset; + } + + // Interpolated sway for smooth rendering between ticks + public float getTailSwayOffset(float partialTick) { + return Mth.lerp(Mth.clamp(partialTick, 0.0f, 1.0f), tailSwayPrev, tailSwayOffset); + } + + /*? if <=1.20.1 {*/ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); + this.entityData.define(VARIANT, 0); + this.entityData.define(DATA_SYNCED_AGE, 0); + } + /*?} else {*/ + /*@Override + protected void defineSynchedData(SynchedEntityData.Builder pBuilder) { + super.defineSynchedData(pBuilder); + pBuilder.define(VARIANT, 0); + pBuilder.define(DATA_SYNCED_AGE, 0); + } + *//*?}*/ + + public int getSyncedAge() { + return this.entityData.get(DATA_SYNCED_AGE); + } + + public TropeognathusVariant getVariant() { + return TropeognathusVariant.byId(this.getTypeVariant() & 255); + } + public int getTypeVariant() { + return this.entityData.get(VARIANT); + } + + private void setVariant(TropeognathusVariant variant) { + this.entityData.set(VARIANT, variant.getId() & 255); + } + + @Override + public boolean canMate(Animal other) { + if (!super.canMate(other)) return false; + if (!(other instanceof TropeognathusEntity that)) return false; + return this.getVariant() != that.getVariant(); + } + + /*? if <=1.20.1 {*/ + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor pLevel, DifficultyInstance pDifficulty, MobSpawnType pReason, @Nullable SpawnGroupData pSpawnData, @Nullable CompoundTag pDataTag) { + TropeognathusVariant variant = Util.getRandom(TropeognathusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(pLevel, pDifficulty, pReason, pSpawnData, pDataTag); + } + /*?} else {*/ + /*@Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, MobSpawnType spawnType, @Nullable SpawnGroupData spawnGroupData) { + TropeognathusVariant variant = Util.getRandom(TropeognathusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(level, difficulty, spawnType, spawnGroupData); + } + *//*?}*/ + + @Override + public void readAdditionalSaveData(CompoundTag pCompound) { + super.readAdditionalSaveData(pCompound); + this.entityData.set(VARIANT, pCompound.getInt("Variant")); + } + + @Override + public void addAdditionalSaveData(CompoundTag pCompound) { + super.addAdditionalSaveData(pCompound); + pCompound.putInt("Variant", this.getTypeVariant()); + } + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } + + @Override + protected @Nullable SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.TROPEOGNATHUS_HURT.get(); + } + + @Override + protected @Nullable SoundEvent getDeathSound() { + return ModSounds.TROPEOGNATHUS_DEATH.get(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/custom/TupuxuaraEntity.java b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/TupuxuaraEntity.java new file mode 100644 index 0000000..75a8e32 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/TupuxuaraEntity.java @@ -0,0 +1,411 @@ +package net.cmr.jurassicrevived.entity.custom; + +import net.cmr.jurassicrevived.entity.ModEntities; +import net.cmr.jurassicrevived.entity.ai.SprintingMeleeAttackGoal; +import net.cmr.jurassicrevived.entity.ai.SprintingPanicGoal; +import net.cmr.jurassicrevived.entity.client.AlbertosaurusVariant; +import net.cmr.jurassicrevived.entity.client.TupuxuaraVariant; +import net.cmr.jurassicrevived.sound.ModSounds; +import net.minecraft.Util; +import net.minecraft.core.BlockPos; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.*; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.attributes.DefaultAttributes; +import net.minecraft.world.entity.ai.control.FlyingMoveControl; +import net.minecraft.world.entity.ai.goal.*; +import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal; +import net.minecraft.world.entity.ai.goal.target.NearestAttackableTargetGoal; +import net.minecraft.world.entity.ai.navigation.FlyingPathNavigation; +import net.minecraft.world.entity.ai.navigation.PathNavigation; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.animal.FlyingAnimal; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerLevelAccessor; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.phys.Vec3; +import org.jetbrains.annotations.Nullable; +import software.bernie.geckolib.animatable.GeoEntity; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.core.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.core.animation.AnimatableManager; +import software.bernie.geckolib.core.animation.Animation; +import software.bernie.geckolib.core.animation.AnimationController; +import software.bernie.geckolib.core.animation.RawAnimation; +import software.bernie.geckolib.core.object.PlayState; +/*?} else {*/ +/*import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.animation.*; +*//*?}*/ + +public class TupuxuaraEntity extends Animal implements GeoEntity, FlyingAnimal { + private final AnimatableInstanceCache cache = new SingletonAnimatableInstanceCache(this); + + private static final EntityDataAccessor VARIANT = + SynchedEntityData.defineId(TupuxuaraEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SYNCED_AGE = + SynchedEntityData.defineId(TupuxuaraEntity.class, EntityDataSerializers.INT); + + // Procedural tail sway state (client-side use for rendering) + private float tailSwayOffset; // Smoothed offset in range roughly [-1, 1] + private float tailSwayVelocity; // Internal velocity for spring-damper + private float tailSwayPrev; // Previous frame value for interpolation + + public TupuxuaraEntity(EntityType pEntityType, Level pLevel) { + super(pEntityType, pLevel); + this.moveControl = new FlyingMoveControl(this, 20, true); + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new SprintingPanicGoal(this, 1.15) { + @Override + public boolean canUse() { + return TupuxuaraEntity.this.isBaby() && super.canUse(); + } + }); + this.goalSelector.addGoal(2, new FloatGoal(this)); + this.goalSelector.addGoal(3, new AvoidEntityGoal<>(this, BrachiosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(4, new SprintingMeleeAttackGoal(this, 1.1, false)); + this.goalSelector.addGoal(5, new BreedGoal(this, 1.0)); + this.goalSelector.addGoal(6, new FollowParentGoal(this, 1.25)); + + // Goal 7: Wander on ground (Walk) - Only when on ground + this.goalSelector.addGoal(7, new WaterAvoidingRandomStrollGoal(this, 1.0) { + @Override + public boolean canUse() { + return TupuxuaraEntity.this.onGround() && super.canUse(); + } + }); + + // Goal 8: Wander in air (Fly) - Handles takeoff, flying, and landing + this.goalSelector.addGoal(8, new WaterAvoidingRandomStrollGoal(this, 1.0, 20) { + @Override + public boolean canUse() { + boolean isFlying = !TupuxuaraEntity.this.onGround(); + // If flying, keep flying. If on ground, small chance (1/400 ticks) to take off. + return (isFlying || TupuxuaraEntity.this.getRandom().nextInt(400) == 0) && super.canUse(); + } + + @Override + protected Vec3 getPosition() { + Vec3 pos = TupuxuaraEntity.this.position(); + RandomSource random = TupuxuaraEntity.this.getRandom(); + + double x = pos.x + (random.nextFloat() * 2 - 1) * 32; + double z = pos.z + (random.nextFloat() * 2 - 1) * 32; + + // Get ground height at the random destination (returns Y of first air block) + int groundY = TupuxuaraEntity.this.level().getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, (int)x, (int)z); + double y; + + if (TupuxuaraEntity.this.onGround()) { + // Takeoff: Target well above ground to ensure liftoff + y = pos.y + 15 + random.nextInt(10); + } else { + // Flying: 5% chance to land, otherwise stay airborne but capped height + if (random.nextFloat() < 0.05f) { + y = groundY; // Land + } else if (pos.y > groundY + 20) { + // Too high: Force descent + y = pos.y - 5 - random.nextInt(10); + } else { + // Just wander up or down a bit + y = pos.y + (random.nextFloat() * 2 - 1) * 10; + } + } + + // Don't target below the ground (blocks) + if (y < groundY) y = groundY; + + return new Vec3(x, y, z); + } + }); + + this.goalSelector.addGoal(9, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(10, new FollowMobGoal(this, 0.8, (float) 20, (float) 10)); + this.targetSelector.addGoal(11, new NearestAttackableTargetGoal<>(this, Animal.class, 10, false, false, + target -> { + // 1. Don't eat your own species + if (target.getType() == this.getType()) return false; + + // 2. Don't eat Flying Animals + if (target instanceof FlyingAnimal) return false; + + // 3. SIZE CHECK: specific height and width limits + // Example: Height < 1.0 blocks AND Width < 1.0 blocks + boolean isSmallEnough = target.getBbHeight() <= 1.0F && target.getBbWidth() <= 1.0F; + + return isSmallEnough; + } + )); + this.targetSelector.addGoal(12, new NearestAttackableTargetGoal<>(this, Player.class, false, false)); + this.targetSelector.addGoal(13, new HurtByTargetGoal(this)); + this.goalSelector.addGoal(14, new RandomLookAroundGoal(this)); + this.goalSelector.addGoal(15, new FloatGoal(this)); + } + + public static AttributeSupplier.Builder createAttributes() { + return Animal.createLivingAttributes() + .add(Attributes.MAX_HEALTH, 50D) + .add(Attributes.MOVEMENT_SPEED, 0.3D) + .add(Attributes.FLYING_SPEED, 0.6D) + .add(Attributes.ARMOR, 0D) + .add(Attributes.FOLLOW_RANGE, 32D) + .add(Attributes.KNOCKBACK_RESISTANCE, 0D) + .add(Attributes.ATTACK_KNOCKBACK, 0D) + .add(Attributes.ATTACK_DAMAGE, 8D); + } + + @Override + protected PathNavigation createNavigation(Level pLevel) { + FlyingPathNavigation navigation = new FlyingPathNavigation(this, pLevel); + navigation.setCanOpenDoors(false); + navigation.setCanFloat(true); + navigation.setCanPassDoors(true); + return navigation; + } + + @Override + public boolean isFlying() { + return !this.onGround(); + } + + @Override + public boolean causeFallDamage(float pFallDistance, float pMultiplier, DamageSource pSource) { + return false; + } + + @Override + protected void checkFallDamage(double pY, boolean pOnGround, BlockState pState, BlockPos pPos) { + } + + @Override + public boolean isFood(ItemStack pStack) { + return pStack.is(Items.BEEF); + } + + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel pLevel, AgeableMob pOtherParent) { + AgeableMob child = ModEntities.TUPUXUARA.get().create(pLevel); + if (child instanceof TupuxuaraEntity baby) { + TupuxuaraVariant randomVariant = Util.getRandom(TupuxuaraVariant.values(), this.random); + baby.setVariant(randomVariant); + } + return child; + } + + @Override + public boolean doHurtTarget(Entity target) { + boolean hit = super.doHurtTarget(target); + if (!level().isClientSide && hit && target instanceof LivingEntity) { + if (this.level() instanceof ServerLevel serverLevel) { + this.triggerAnim("attackController", "attack"); + this.playSound(ModSounds.BEAK_ATTACK.get(), 1.0F, 1.0F); + } + } + return hit; + } + + @Override + public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { + controllers.add(new AnimationController<>(this, "Walk/Run/Idle", state -> { + if (!TupuxuaraEntity.this.onGround()) { + return state.setAndContinue(RawAnimation.begin().then("anim.tupuxuara.fly", Animation.LoopType.LOOP)); + } + + if (state.isMoving()) + return state.setAndContinue(TupuxuaraEntity.this.isSprinting() ? RawAnimation.begin().then("anim.tupuxuara.walk", Animation.LoopType.LOOP) : RawAnimation.begin().then("anim.tupuxuara.walk", Animation.LoopType.LOOP)); + + return state.setAndContinue(RawAnimation.begin().then("anim.tupuxuara.idle", Animation.LoopType.LOOP)); + })); + + controllers.add(new AnimationController<>(this, "attackController", state -> PlayState.STOP) + .triggerableAnim("attack", RawAnimation.begin().then("anim.tupuxuara.attack", Animation.LoopType.PLAY_ONCE))); + + controllers.add(new AnimationController<>(this, "mouthController", state -> PlayState.STOP) + .triggerableAnim("mouth", RawAnimation.begin().then("anim.tupuxuara.mouth", Animation.LoopType.PLAY_ONCE))); + } + + private float getSignedTurnDelta() { + // Only consider the body (torso) rotation so head look does not affect tail sway + return Mth.wrapDegrees(this.yBodyRot - this.yBodyRotO); + } + + private int mouthAnimCooldown = 0; + + @Override + public void tick() { + super.tick(); + if (!level().isClientSide) { + this.entityData.set(DATA_SYNCED_AGE, this.getAge()); + var maxHealthAttr = getAttribute(Attributes.MAX_HEALTH); + if (maxHealthAttr != null) { + double baseAdult = DefaultAttributes.getSupplier((EntityType) this.getType()).getValue(Attributes.MAX_HEALTH); + double desired = this.isBaby() ? baseAdult * 0.10D : baseAdult; + if (maxHealthAttr.getBaseValue() != desired) { + double oldMax = maxHealthAttr.getBaseValue(); + double healthRatio = this.getHealth() / (float) oldMax; + maxHealthAttr.setBaseValue(desired); + this.setHealth((float) (desired * Mth.clamp(healthRatio, 0.0F, 1.0F))); + } + } + } + + if (!level().isClientSide) { + if (mouthAnimCooldown > 0) { + mouthAnimCooldown--; + } else { + this.triggerAnim("mouthController", "mouth"); + this.playSound(ModSounds.TUPUXUARA_CALL.get(), 1.0F, 1.0F); + // 30s–60s in ticks + mouthAnimCooldown = this.random.nextInt(1200 - 600 + 1) + 600; + } + } + + if (level().isClientSide) { + // Capture previous for smooth interpolation between ticks + this.tailSwayPrev = this.tailSwayOffset; + updateProceduralTailSway(); + } + } + + private void updateProceduralTailSway() { + // Turn input derived from rotation deltas; works even when standing still and turning + float turnDegrees = getSignedTurnDelta(); + + // Deadzone to ignore tiny jitter so the tail can return to center cleanly + float deadzoneDeg = 0.6f; // smaller deadzone for more responsiveness + float turnInput = 0.0f; + if (Math.abs(turnDegrees) >= deadzoneDeg) { + // Higher sensitivity so small in-place turns still affect the model + turnInput = Mth.clamp(turnDegrees / 15.0f, -1.0f, 1.0f); + } + + // Target offset: keep intuitive sign (positive input -> positive sway) + float target = turnInput; + + // One-pole low-pass (no bounce). Larger alpha => snappier and less "stiff". + float alpha = 0.24f; // try 0.20–0.30 to taste + + tailSwayOffset += (target - tailSwayOffset) * alpha; + + // Snap tiny residuals to zero so it visibly settles + if (Math.abs(tailSwayOffset) < 0.003f) { + tailSwayOffset = 0.0f; + } + + // No oscillation velocity retained + tailSwayVelocity = 0.0f; + + tailSwayOffset = Mth.clamp(tailSwayOffset, -1.5f, 1.5f); + } + + // Expose to the model for bone rotation + public float getTailSwayOffset() { + return tailSwayOffset; + } + + // Interpolated sway for smooth rendering between ticks + public float getTailSwayOffset(float partialTick) { + return Mth.lerp(Mth.clamp(partialTick, 0.0f, 1.0f), tailSwayPrev, tailSwayOffset); + } + + /*? if <=1.20.1 {*/ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); + this.entityData.define(VARIANT, 0); + this.entityData.define(DATA_SYNCED_AGE, 0); + } + /*?} else {*/ + /*@Override + protected void defineSynchedData(SynchedEntityData.Builder pBuilder) { + super.defineSynchedData(pBuilder); + pBuilder.define(VARIANT, 0); + pBuilder.define(DATA_SYNCED_AGE, 0); + } + *//*?}*/ + + public int getSyncedAge() { + return this.entityData.get(DATA_SYNCED_AGE); + } + + public TupuxuaraVariant getVariant() { + return TupuxuaraVariant.byId(this.getTypeVariant() & 255); + } + public int getTypeVariant() { + return this.entityData.get(VARIANT); + } + + private void setVariant(TupuxuaraVariant variant) { + this.entityData.set(VARIANT, variant.getId() & 255); + } + + @Override + public boolean canMate(Animal other) { + if (!super.canMate(other)) return false; + if (!(other instanceof TupuxuaraEntity that)) return false; + return this.getVariant() != that.getVariant(); + } + + /*? if <=1.20.1 {*/ + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor pLevel, DifficultyInstance pDifficulty, MobSpawnType pReason, @Nullable SpawnGroupData pSpawnData, @Nullable CompoundTag pDataTag) { + TupuxuaraVariant variant = Util.getRandom(TupuxuaraVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(pLevel, pDifficulty, pReason, pSpawnData, pDataTag); + } + /*?} else {*/ + /*@Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, MobSpawnType spawnType, @Nullable SpawnGroupData spawnGroupData) { + TupuxuaraVariant variant = Util.getRandom(TupuxuaraVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(level, difficulty, spawnType, spawnGroupData); + } + *//*?}*/ + + @Override + public void readAdditionalSaveData(CompoundTag pCompound) { + super.readAdditionalSaveData(pCompound); + this.entityData.set(VARIANT, pCompound.getInt("Variant")); + } + + @Override + public void addAdditionalSaveData(CompoundTag pCompound) { + super.addAdditionalSaveData(pCompound); + pCompound.putInt("Variant", this.getTypeVariant()); + } + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } + + @Override + protected @Nullable SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.TUPUXUARA_HURT.get(); + } + + @Override + protected @Nullable SoundEvent getDeathSound() { + return ModSounds.TUPUXUARA_DEATH.get(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/custom/TyrannosaurusRexEntity.java b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/TyrannosaurusRexEntity.java new file mode 100644 index 0000000..516808b --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/TyrannosaurusRexEntity.java @@ -0,0 +1,325 @@ +package net.cmr.jurassicrevived.entity.custom; + +import net.cmr.jurassicrevived.entity.ModEntities; +import net.cmr.jurassicrevived.entity.ai.SprintingMeleeAttackGoal; +import net.cmr.jurassicrevived.entity.ai.SprintingPanicGoal; +import net.cmr.jurassicrevived.entity.client.AlbertosaurusVariant; +import net.cmr.jurassicrevived.entity.client.TyrannosaurusRexVariant; +import net.cmr.jurassicrevived.sound.ModSounds; +import net.minecraft.Util; +import net.minecraft.core.BlockPos; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.util.Mth; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.*; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.attributes.DefaultAttributes; +import net.minecraft.world.entity.ai.goal.*; +import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal; +import net.minecraft.world.entity.ai.goal.target.NearestAttackableTargetGoal; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.monster.Monster; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerLevelAccessor; +import net.minecraft.world.level.block.state.BlockState; +import org.jetbrains.annotations.Nullable; +import software.bernie.geckolib.animatable.GeoEntity; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.core.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.core.animation.AnimatableManager; +import software.bernie.geckolib.core.animation.Animation; +import software.bernie.geckolib.core.animation.AnimationController; +import software.bernie.geckolib.core.animation.RawAnimation; +import software.bernie.geckolib.core.object.PlayState; +/*?} else {*/ +/*import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.animation.*; +*//*?}*/ + +public class TyrannosaurusRexEntity extends Animal implements GeoEntity { + private AnimatableInstanceCache cache = new SingletonAnimatableInstanceCache(this); + + private static final EntityDataAccessor VARIANT = + SynchedEntityData.defineId(TyrannosaurusRexEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SYNCED_AGE = + SynchedEntityData.defineId(TyrannosaurusRexEntity.class, EntityDataSerializers.INT); + + // Procedural tail sway state (client-side use for rendering) + private float tailSwayOffset; // Smoothed offset in range roughly [-1, 1] + private float tailSwayVelocity; // Internal velocity for spring-damper + private float tailSwayPrev; // Previous frame value for interpolation + + public TyrannosaurusRexEntity(EntityType pEntityType, Level pLevel) { + super(pEntityType, pLevel); + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new SprintingPanicGoal(this, 1.15) { + @Override + public boolean canUse() { + return TyrannosaurusRexEntity.this.isBaby() && super.canUse(); + } + }); + this.targetSelector.addGoal(1, new HurtByTargetGoal(this).setAlertOthers()); + this.goalSelector.addGoal(2, new FloatGoal(this)); + this.goalSelector.addGoal(3, new AvoidEntityGoal<>(this, BrachiosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(4, new SprintingMeleeAttackGoal(this, 1.1, false)); + this.goalSelector.addGoal(5, new BreedGoal(this, 1.0)); + this.goalSelector.addGoal(6, new FollowParentGoal(this, 1.25)); + this.goalSelector.addGoal(7, new WaterAvoidingRandomStrollGoal(this, 1.0)); + this.goalSelector.addGoal(8, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(9, new FollowMobGoal(this, 1.2, (float) 20, (float) 10)); + this.targetSelector.addGoal(10, new NearestAttackableTargetGoal<>(this, Monster.class, true)); + this.targetSelector.addGoal(11, new NearestAttackableTargetGoal<>(this, Animal.class, 10, false, false, + target -> target.getType() != this.getType())); + this.targetSelector.addGoal(12, new NearestAttackableTargetGoal(this, TriceratopsEntity.class, false, false)); + this.targetSelector.addGoal(13, new NearestAttackableTargetGoal(this, GallimimusEntity.class, false, false)); + this.targetSelector.addGoal(14, new NearestAttackableTargetGoal(this, DilophosaurusEntity.class, false, false)); + this.targetSelector.addGoal(15, new NearestAttackableTargetGoal(this, CeratosaurusEntity.class, false, false)); + this.targetSelector.addGoal(16, new NearestAttackableTargetGoal(this, ParasaurolophusEntity.class, false, false)); + this.targetSelector.addGoal(17, new NearestAttackableTargetGoal(this, VelociraptorEntity.class, false, false)); + this.targetSelector.addGoal(18, new NearestAttackableTargetGoal(this, SpinosaurusEntity.class, false, false)); + this.targetSelector.addGoal(19, new NearestAttackableTargetGoal(this, IndominusRexEntity.class, false, false)); + this.targetSelector.addGoal(20, new NearestAttackableTargetGoal(this, Player.class, false, false)); + this.goalSelector.addGoal(21, new RandomLookAroundGoal(this)); + + + } + + public static AttributeSupplier.Builder createAttributes() { + return Animal.createLivingAttributes() + .add(Attributes.MAX_HEALTH, 80D) + .add(Attributes.MOVEMENT_SPEED, 0.3D) + .add(Attributes.ARMOR, 0D) + .add(Attributes.FOLLOW_RANGE, 32D) + .add(Attributes.KNOCKBACK_RESISTANCE, 0D) + .add(Attributes.ATTACK_KNOCKBACK, 0D) + .add(Attributes.ATTACK_DAMAGE, 20D); + } + + @Override + public boolean isFood(ItemStack pStack) { + return pStack.is(Items.BEEF); + } + + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel pLevel, AgeableMob pOtherParent) { + AgeableMob child = ModEntities.TYRANNOSAURUS_REX.get().create(pLevel); + if (child instanceof TyrannosaurusRexEntity baby) { + TyrannosaurusRexVariant randomVariant = Util.getRandom(TyrannosaurusRexVariant.values(), this.random); + baby.setVariant(randomVariant); + } + return child; + } + + @Override + public boolean doHurtTarget(Entity target) { + boolean hit = super.doHurtTarget(target); + if (!level().isClientSide && hit && target instanceof LivingEntity) { + if (this.level() instanceof ServerLevel serverLevel) { + this.triggerAnim("attackController", "attack"); + this.playSound(ModSounds.TYRANNOSAURUS_REX_ATTACK.get(), 1.0F, 1.0F); + } + } + return hit; + } + + @Override + public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { + controllers.add(new AnimationController<>(this, "Walk/Run/Idle", state -> { + if (state.isMoving()) + return state.setAndContinue(TyrannosaurusRexEntity.this.isSprinting() ? RawAnimation.begin().then("anim.tyrannosaurus_rex.run", Animation.LoopType.LOOP) : RawAnimation.begin().then("anim.tyrannosaurus_rex.walk", Animation.LoopType.LOOP)); + + return state.setAndContinue(RawAnimation.begin().then("anim.tyrannosaurus_rex.idle", Animation.LoopType.LOOP)); + })); + + controllers.add(new AnimationController<>(this, "attackController", state -> PlayState.STOP) + .triggerableAnim("attack", RawAnimation.begin().then("anim.tyrannosaurus_rex.attack", Animation.LoopType.PLAY_ONCE))); + + controllers.add(new AnimationController<>(this, "mouthController", state -> PlayState.STOP) + .triggerableAnim("mouth", RawAnimation.begin().then("anim.tyrannosaurus_rex.mouth", Animation.LoopType.PLAY_ONCE))); + } + + private float getSignedTurnDelta() { + // Only consider the body (torso) rotation so head look does not affect tail sway + return Mth.wrapDegrees(this.yBodyRot - this.yBodyRotO); + } + + private int mouthAnimCooldown = 0; + + @Override + public void tick() { + super.tick(); + if (!level().isClientSide) { + this.entityData.set(DATA_SYNCED_AGE, this.getAge()); + var maxHealthAttr = getAttribute(Attributes.MAX_HEALTH); + if (maxHealthAttr != null) { + double baseAdult = DefaultAttributes.getSupplier((EntityType) this.getType()).getValue(Attributes.MAX_HEALTH); + double desired = this.isBaby() ? baseAdult * 0.10D : baseAdult; + if (maxHealthAttr.getBaseValue() != desired) { + double oldMax = maxHealthAttr.getBaseValue(); + double healthRatio = this.getHealth() / (float) oldMax; + maxHealthAttr.setBaseValue(desired); + this.setHealth((float) (desired * Mth.clamp(healthRatio, 0.0F, 1.0F))); + } + } + } + + if (!level().isClientSide) { + if (mouthAnimCooldown > 0) { + mouthAnimCooldown--; + } else { + this.triggerAnim("mouthController", "mouth"); + this.playSound(ModSounds.TYRANNOSAURUS_REX_CALL.get(), 1.0F, 1.0F); + // 30s–60s in ticks + mouthAnimCooldown = this.random.nextInt(1200 - 600 + 1) + 600; + } + } + + if (level().isClientSide) { + // Capture previous for smooth interpolation between ticks + this.tailSwayPrev = this.tailSwayOffset; + updateProceduralTailSway(); + } + } + + private void updateProceduralTailSway() { + // Turn input derived from rotation deltas; works even when standing still and turning + float turnDegrees = getSignedTurnDelta(); + + // Deadzone to ignore tiny jitter so the tail can return to center cleanly + float deadzoneDeg = 0.6f; // smaller deadzone for more responsiveness + float turnInput = 0.0f; + if (Math.abs(turnDegrees) >= deadzoneDeg) { + // Higher sensitivity so small in-place turns still affect the model + turnInput = Mth.clamp(turnDegrees / 15.0f, -1.0f, 1.0f); + } + + // Target offset: keep intuitive sign (positive input -> positive sway) + float target = turnInput; + + // One-pole low-pass (no bounce). Larger alpha => snappier and less "stiff". + float alpha = 0.24f; // try 0.20–0.30 to taste + + tailSwayOffset += (target - tailSwayOffset) * alpha; + + // Snap tiny residuals to zero so it visibly settles + if (Math.abs(tailSwayOffset) < 0.003f) { + tailSwayOffset = 0.0f; + } + + // No oscillation velocity retained + tailSwayVelocity = 0.0f; + + tailSwayOffset = Mth.clamp(tailSwayOffset, -1.5f, 1.5f); + } + + // Expose to the model for bone rotation + public float getTailSwayOffset() { + return tailSwayOffset; + } + + // Interpolated sway for smooth rendering between ticks + public float getTailSwayOffset(float partialTick) { + return Mth.lerp(Mth.clamp(partialTick, 0.0f, 1.0f), tailSwayPrev, tailSwayOffset); + } + + /*? if <=1.20.1 {*/ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); + this.entityData.define(VARIANT, 0); + this.entityData.define(DATA_SYNCED_AGE, 0); + } + /*?} else {*/ + /*@Override + protected void defineSynchedData(SynchedEntityData.Builder pBuilder) { + super.defineSynchedData(pBuilder); + pBuilder.define(VARIANT, 0); + pBuilder.define(DATA_SYNCED_AGE, 0); + } + *//*?}*/ + + public int getSyncedAge() { + return this.entityData.get(DATA_SYNCED_AGE); + } + public int getTypeVariant() { + return this.entityData.get(VARIANT); + } + + public TyrannosaurusRexVariant getVariant() { + return TyrannosaurusRexVariant.byId(this.getTypeVariant() & 255); + } + + private void setVariant(TyrannosaurusRexVariant variant) { + this.entityData.set(VARIANT, variant.getId() & 255); + } + + @Override + public boolean canMate(Animal other) { + if (!super.canMate(other)) return false; + if (!(other instanceof TyrannosaurusRexEntity that)) return false; + return this.getVariant() != that.getVariant(); + } + @Override + public void addAdditionalSaveData(CompoundTag pCompound) { + super.addAdditionalSaveData(pCompound); + pCompound.putInt("Variant", this.getTypeVariant()); + } + + @Override + public void readAdditionalSaveData(CompoundTag pCompound) { + super.readAdditionalSaveData(pCompound); + this.entityData.set(VARIANT, pCompound.getInt("Variant")); + } + + /*? if <=1.20.1 {*/ + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor pLevel, DifficultyInstance pDifficulty, MobSpawnType pReason, @Nullable SpawnGroupData pSpawnData, @Nullable CompoundTag pDataTag) { + TyrannosaurusRexVariant variant = Util.getRandom(TyrannosaurusRexVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(pLevel, pDifficulty, pReason, pSpawnData, pDataTag); + } + /*?} else {*/ + /*@Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, MobSpawnType spawnType, @Nullable SpawnGroupData spawnGroupData) { + TyrannosaurusRexVariant variant = Util.getRandom(TyrannosaurusRexVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(level, difficulty, spawnType, spawnGroupData); + } + *//*?}*/ + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } + + @Override + protected @Nullable SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.TYRANNOSAURUS_REX_HURT.get(); + } + + @Override + protected void playStepSound(BlockPos pos, BlockState blockIn) { + this.playSound(ModSounds.STOMP.get(), 0.5F, 1.0F); + } + + @Override + protected @Nullable SoundEvent getDeathSound() { + return ModSounds.TYRANNOSAURUS_REX_DEATH.get(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/custom/UtahraptorEntity.java b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/UtahraptorEntity.java new file mode 100644 index 0000000..0d8b84b --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/UtahraptorEntity.java @@ -0,0 +1,313 @@ +package net.cmr.jurassicrevived.entity.custom; + +import net.cmr.jurassicrevived.entity.ModEntities; +import net.cmr.jurassicrevived.entity.ai.SprintingMeleeAttackGoal; +import net.cmr.jurassicrevived.entity.ai.SprintingPanicGoal; +import net.cmr.jurassicrevived.entity.client.AlbertosaurusVariant; +import net.cmr.jurassicrevived.entity.client.UtahraptorVariant; +import net.cmr.jurassicrevived.sound.ModSounds; +import net.minecraft.Util; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.util.Mth; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.*; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.attributes.DefaultAttributes; +import net.minecraft.world.entity.ai.goal.*; +import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal; +import net.minecraft.world.entity.ai.goal.target.NearestAttackableTargetGoal; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerLevelAccessor; +import org.jetbrains.annotations.Nullable; +import software.bernie.geckolib.animatable.GeoEntity; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.core.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.core.animation.AnimatableManager; +import software.bernie.geckolib.core.animation.Animation; +import software.bernie.geckolib.core.animation.AnimationController; +import software.bernie.geckolib.core.animation.RawAnimation; +import software.bernie.geckolib.core.object.PlayState; +/*?} else {*/ +/*import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.animation.*; +*//*?}*/ +public class UtahraptorEntity extends Animal implements GeoEntity { + private AnimatableInstanceCache cache = new SingletonAnimatableInstanceCache(this); + + private static final EntityDataAccessor VARIANT = + SynchedEntityData.defineId(UtahraptorEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SYNCED_AGE = + SynchedEntityData.defineId(UtahraptorEntity.class, EntityDataSerializers.INT); + + // Procedural tail sway state (client-side use for rendering) + private float tailSwayOffset; // Smoothed offset in range roughly [-1, 1] + private float tailSwayVelocity; // Internal velocity for spring-damper + private float tailSwayPrev; // Previous frame value for interpolation + + public UtahraptorEntity(EntityType pEntityType, Level pLevel) { + super(pEntityType, pLevel); + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new SprintingPanicGoal(this, 1.15) { + @Override + public boolean canUse() { + return UtahraptorEntity.this.isBaby() && super.canUse(); + } + }); + this.targetSelector.addGoal(1, new HurtByTargetGoal(this).setAlertOthers()); + this.goalSelector.addGoal(2, new FloatGoal(this)); + this.goalSelector.addGoal(3, new AvoidEntityGoal<>(this, TriceratopsEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(4, new AvoidEntityGoal<>(this, SpinosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(5, new AvoidEntityGoal<>(this, TyrannosaurusRexEntity.class, (float) 20, 1.2, 1.2)); + this.goalSelector.addGoal(6, new AvoidEntityGoal<>(this, IndominusRexEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(7, new AvoidEntityGoal<>(this, BrachiosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(8, new SprintingMeleeAttackGoal(this, 1.1, false)); + this.goalSelector.addGoal(9, new BreedGoal(this, 1.0)); + this.goalSelector.addGoal(10, new FollowParentGoal(this, 1.25)); + this.goalSelector.addGoal(11, new WaterAvoidingRandomStrollGoal(this, 1.0)); + this.goalSelector.addGoal(12, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(13, new FollowMobGoal(this, 1, (float) 20, (float) 10)); + this.targetSelector.addGoal(14, new NearestAttackableTargetGoal<>(this, Animal.class, 10, false, false, + target -> target.getType() != this.getType())); + this.targetSelector.addGoal(15, new NearestAttackableTargetGoal(this, GallimimusEntity.class, false, false)); + this.targetSelector.addGoal(16, new NearestAttackableTargetGoal(this, DilophosaurusEntity.class, false, false)); + this.targetSelector.addGoal(17, new NearestAttackableTargetGoal(this, CeratosaurusEntity.class, false, false)); + this.targetSelector.addGoal(18, new NearestAttackableTargetGoal(this, ParasaurolophusEntity.class, false, false)); + this.targetSelector.addGoal(19, new NearestAttackableTargetGoal(this, Player.class, false, false)); + this.targetSelector.addGoal(20, new NearestAttackableTargetGoal(this, CompsognathusEntity.class, false, false)); + this.goalSelector.addGoal(21, new RandomLookAroundGoal(this)); + } + + public static AttributeSupplier.Builder createAttributes() { + return Animal.createLivingAttributes() + .add(Attributes.MAX_HEALTH, 35D) + .add(Attributes.MOVEMENT_SPEED, 0.3D) + .add(Attributes.ARMOR, 0D) + .add(Attributes.FOLLOW_RANGE, 32D) + .add(Attributes.ATTACK_DAMAGE, 10D) + .add(Attributes.KNOCKBACK_RESISTANCE, 0.3D) + .add(Attributes.ATTACK_KNOCKBACK, 0D); + } + + @Override + public boolean isFood(ItemStack pStack) { + return pStack.is(Items.BEEF); + } + + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel pLevel, AgeableMob pOtherParent) { + AgeableMob child = ModEntities.UTAHRAPTOR.get().create(pLevel); + if (child instanceof UtahraptorEntity baby) { + UtahraptorVariant randomVariant = Util.getRandom(UtahraptorVariant.values(), this.random); + baby.setVariant(randomVariant); + } + return child; + } + + @Override + public boolean doHurtTarget(Entity target) { + boolean hit = super.doHurtTarget(target); + if (!level().isClientSide && hit && target instanceof LivingEntity) { + if (this.level() instanceof ServerLevel serverLevel) { + this.triggerAnim("attackController", "attack"); + } + } + return hit; + } + + @Override + public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { + controllers.add(new AnimationController<>(this, "Walk/Run/Idle", state -> { + if (state.isMoving()) + return state.setAndContinue(UtahraptorEntity.this.isSprinting() ? RawAnimation.begin().then("anim.utahraptor.run", Animation.LoopType.LOOP) : RawAnimation.begin().then("anim.utahraptor.walk", Animation.LoopType.LOOP)); + + return state.setAndContinue(RawAnimation.begin().then("anim.utahraptor.idle", Animation.LoopType.LOOP)); + })); + + controllers.add(new AnimationController<>(this, "attackController", state -> PlayState.STOP) + .triggerableAnim("attack", RawAnimation.begin().then("anim.utahraptor.attack", Animation.LoopType.PLAY_ONCE))); + + controllers.add(new AnimationController<>(this, "mouthController", state -> PlayState.STOP) + .triggerableAnim("mouth", RawAnimation.begin().then("anim.utahraptor.mouth", Animation.LoopType.PLAY_ONCE))); + } + + private float getSignedTurnDelta() { + // Only consider the body (torso) rotation so head look does not affect tail sway + return Mth.wrapDegrees(this.yBodyRot - this.yBodyRotO); + } + + private int mouthAnimCooldown = 0; + + @Override + public void tick() { + super.tick(); + if (!level().isClientSide) { + this.entityData.set(DATA_SYNCED_AGE, this.getAge()); + var maxHealthAttr = getAttribute(Attributes.MAX_HEALTH); + if (maxHealthAttr != null) { + double baseAdult = DefaultAttributes.getSupplier((EntityType) this.getType()).getValue(Attributes.MAX_HEALTH); + double desired = this.isBaby() ? baseAdult * 0.10D : baseAdult; + if (maxHealthAttr.getBaseValue() != desired) { + double oldMax = maxHealthAttr.getBaseValue(); + double healthRatio = this.getHealth() / (float) oldMax; + maxHealthAttr.setBaseValue(desired); + this.setHealth((float) (desired * Mth.clamp(healthRatio, 0.0F, 1.0F))); + } + } + } + + if (!level().isClientSide) { + if (mouthAnimCooldown > 0) { + mouthAnimCooldown--; + } else { + this.triggerAnim("mouthController", "mouth"); + this.playSound(ModSounds.UTAHRAPTOR_CALL.get(), 1.0F, 1.0F); + // 30s–60s in ticks + mouthAnimCooldown = this.random.nextInt(1200 - 600 + 1) + 600; + } + } + + if (level().isClientSide) { + // Capture previous for smooth interpolation between ticks + this.tailSwayPrev = this.tailSwayOffset; + updateProceduralTailSway(); + } + } + + private void updateProceduralTailSway() { + // Turn input derived from rotation deltas; works even when standing still and turning + float turnDegrees = getSignedTurnDelta(); + + // Deadzone to ignore tiny jitter so the tail can return to center cleanly + float deadzoneDeg = 0.6f; // smaller deadzone for more responsiveness + float turnInput = 0.0f; + if (Math.abs(turnDegrees) >= deadzoneDeg) { + // Higher sensitivity so small in-place turns still affect the model + turnInput = Mth.clamp(turnDegrees / 15.0f, -1.0f, 1.0f); + } + + // Target offset: keep intuitive sign (positive input -> positive sway) + float target = turnInput; + + // One-pole low-pass (no bounce). Larger alpha => snappier and less "stiff". + float alpha = 0.24f; // try 0.20–0.30 to taste + + tailSwayOffset += (target - tailSwayOffset) * alpha; + + // Snap tiny residuals to zero so it visibly settles + if (Math.abs(tailSwayOffset) < 0.003f) { + tailSwayOffset = 0.0f; + } + + // No oscillation velocity retained + tailSwayVelocity = 0.0f; + + tailSwayOffset = Mth.clamp(tailSwayOffset, -1.5f, 1.5f); + } + + // Expose to the model for bone rotation + public float getTailSwayOffset() { + return tailSwayOffset; + } + + // Interpolated sway for smooth rendering between ticks + public float getTailSwayOffset(float partialTick) { + return Mth.lerp(Mth.clamp(partialTick, 0.0f, 1.0f), tailSwayPrev, tailSwayOffset); + } + + /*? if <=1.20.1 {*/ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); + this.entityData.define(VARIANT, 0); + this.entityData.define(DATA_SYNCED_AGE, 0); + } + /*?} else {*/ + /*@Override + protected void defineSynchedData(SynchedEntityData.Builder pBuilder) { + super.defineSynchedData(pBuilder); + pBuilder.define(VARIANT, 0); + pBuilder.define(DATA_SYNCED_AGE, 0); + } + *//*?}*/ + + public int getSyncedAge() { + return this.entityData.get(DATA_SYNCED_AGE); + } + public int getTypeVariant() { + return this.entityData.get(VARIANT); + } + + public UtahraptorVariant getVariant() { + return UtahraptorVariant.byId(this.getTypeVariant() & 255); + } + + private void setVariant(UtahraptorVariant variant) { + this.entityData.set(VARIANT, variant.getId() & 255); + } + + @Override + public boolean canMate(Animal other) { + if (!super.canMate(other)) return false; + if (!(other instanceof UtahraptorEntity that)) return false; + return this.getVariant() != that.getVariant(); + } + @Override + public void addAdditionalSaveData(CompoundTag pCompound) { + super.addAdditionalSaveData(pCompound); + pCompound.putInt("Variant", this.getTypeVariant()); + } + + @Override + public void readAdditionalSaveData(CompoundTag pCompound) { + super.readAdditionalSaveData(pCompound); + this.entityData.set(VARIANT, pCompound.getInt("Variant")); + } + + /*? if <=1.20.1 {*/ + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor pLevel, DifficultyInstance pDifficulty, MobSpawnType pReason, @Nullable SpawnGroupData pSpawnData, @Nullable CompoundTag pDataTag) { + UtahraptorVariant variant = Util.getRandom(UtahraptorVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(pLevel, pDifficulty, pReason, pSpawnData, pDataTag); + } + /*?} else {*/ + /*@Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, MobSpawnType spawnType, @Nullable SpawnGroupData spawnGroupData) { + UtahraptorVariant variant = Util.getRandom(UtahraptorVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(level, difficulty, spawnType, spawnGroupData); + } + *//*?}*/ + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } + + @Override + protected @Nullable SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.UTAHRAPTOR_HURT.get(); + } + + @Override + protected @Nullable SoundEvent getDeathSound() { + return ModSounds.UTAHRAPTOR_DEATH.get(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/custom/VelociraptorEntity.java b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/VelociraptorEntity.java new file mode 100644 index 0000000..afec321 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/VelociraptorEntity.java @@ -0,0 +1,315 @@ +package net.cmr.jurassicrevived.entity.custom; + +import net.cmr.jurassicrevived.entity.ModEntities; +import net.cmr.jurassicrevived.entity.ai.SprintingMeleeAttackGoal; +import net.cmr.jurassicrevived.entity.ai.SprintingPanicGoal; +import net.cmr.jurassicrevived.entity.client.VelociraptorVariant; +import net.cmr.jurassicrevived.entity.client.VelociraptorVariant; +import net.cmr.jurassicrevived.sound.ModSounds; +import net.minecraft.Util; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.util.Mth; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.*; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.attributes.DefaultAttributes; +import net.minecraft.world.entity.ai.goal.*; +import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal; +import net.minecraft.world.entity.ai.goal.target.NearestAttackableTargetGoal; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerLevelAccessor; +import org.jetbrains.annotations.Nullable; +import software.bernie.geckolib.animatable.GeoEntity; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.core.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.core.animation.AnimatableManager; +import software.bernie.geckolib.core.animation.Animation; +import software.bernie.geckolib.core.animation.AnimationController; +import software.bernie.geckolib.core.animation.RawAnimation; +import software.bernie.geckolib.core.object.PlayState; +/*?} else {*/ +/*import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.animation.*; +*//*?}*/ + +public class VelociraptorEntity extends Animal implements GeoEntity { + private AnimatableInstanceCache cache = new SingletonAnimatableInstanceCache(this); + + private static final EntityDataAccessor VARIANT = + SynchedEntityData.defineId(VelociraptorEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SYNCED_AGE = + SynchedEntityData.defineId(VelociraptorEntity.class, EntityDataSerializers.INT); + + // Procedural tail sway state (client-side use for rendering) + private float tailSwayOffset; // Smoothed offset in range roughly [-1, 1] + private float tailSwayVelocity; // Internal velocity for spring-damper + private float tailSwayPrev; // Previous frame value for interpolation + + public VelociraptorEntity(EntityType pEntityType, Level pLevel) { + super(pEntityType, pLevel); + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new SprintingPanicGoal(this, 1.15) { + @Override + public boolean canUse() { + return VelociraptorEntity.this.isBaby() && super.canUse(); + } + }); + this.targetSelector.addGoal(1, new HurtByTargetGoal(this).setAlertOthers()); + this.goalSelector.addGoal(2, new FloatGoal(this)); + this.goalSelector.addGoal(3, new AvoidEntityGoal<>(this, TriceratopsEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(4, new AvoidEntityGoal<>(this, SpinosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(5, new AvoidEntityGoal<>(this, TyrannosaurusRexEntity.class, (float) 20, 1.2, 1.2)); + this.goalSelector.addGoal(6, new AvoidEntityGoal<>(this, IndominusRexEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(7, new AvoidEntityGoal<>(this, BrachiosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(8, new SprintingMeleeAttackGoal(this, 1.1, false)); + this.goalSelector.addGoal(9, new BreedGoal(this, 1.0)); + this.goalSelector.addGoal(10, new FollowParentGoal(this, 1.25)); + this.goalSelector.addGoal(11, new WaterAvoidingRandomStrollGoal(this, 1.0)); + this.goalSelector.addGoal(12, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(13, new FollowMobGoal(this, 1, (float) 20, (float) 10)); + this.targetSelector.addGoal(14, new NearestAttackableTargetGoal<>(this, Animal.class, 10, false, false, + target -> target.getType() != this.getType())); + this.targetSelector.addGoal(15, new NearestAttackableTargetGoal(this, GallimimusEntity.class, false, false)); + this.targetSelector.addGoal(16, new NearestAttackableTargetGoal(this, DilophosaurusEntity.class, false, false)); + this.targetSelector.addGoal(17, new NearestAttackableTargetGoal(this, CeratosaurusEntity.class, false, false)); + this.targetSelector.addGoal(18, new NearestAttackableTargetGoal(this, ParasaurolophusEntity.class, false, false)); + this.targetSelector.addGoal(19, new NearestAttackableTargetGoal(this, Player.class, false, false)); + this.targetSelector.addGoal(20, new NearestAttackableTargetGoal(this, CompsognathusEntity.class, false, false)); + this.goalSelector.addGoal(21, new RandomLookAroundGoal(this)); + } + + public static AttributeSupplier.Builder createAttributes() { + return Animal.createLivingAttributes() + .add(Attributes.MAX_HEALTH, 30D) + .add(Attributes.MOVEMENT_SPEED, 0.3D) + .add(Attributes.ARMOR, 0D) + .add(Attributes.FOLLOW_RANGE, 32D) + .add(Attributes.ATTACK_DAMAGE, 8D) + .add(Attributes.KNOCKBACK_RESISTANCE, 0.3D) + .add(Attributes.ATTACK_KNOCKBACK, 0D); + } + + @Override + public boolean isFood(ItemStack pStack) { + return pStack.is(Items.BEEF); + } + + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel pLevel, AgeableMob pOtherParent) { + AgeableMob child = ModEntities.VELOCIRAPTOR.get().create(pLevel); + if (child instanceof VelociraptorEntity baby) { + VelociraptorVariant randomVariant = Util.getRandom(VelociraptorVariant.values(), this.random); + baby.setVariant(randomVariant); + } + return child; + } + + @Override + public boolean doHurtTarget(Entity target) { + boolean hit = super.doHurtTarget(target); + if (!level().isClientSide && hit && target instanceof LivingEntity) { + if (this.level() instanceof ServerLevel serverLevel) { + this.triggerAnim("attackController", "attack"); + this.playSound(ModSounds.VELOCIRAPTOR_ATTACK.get(), 1.0F, 1.0F); + } + } + return hit; + } + + @Override + public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { + controllers.add(new AnimationController<>(this, "Walk/Run/Idle", state -> { + if (state.isMoving()) + return state.setAndContinue(VelociraptorEntity.this.isSprinting() ? RawAnimation.begin().then("anim.velociraptor.run", Animation.LoopType.LOOP) : RawAnimation.begin().then("anim.velociraptor.walk", Animation.LoopType.LOOP)); + + return state.setAndContinue(RawAnimation.begin().then("anim.velociraptor.idle", Animation.LoopType.LOOP)); + })); + + controllers.add(new AnimationController<>(this, "attackController", state -> PlayState.STOP) + .triggerableAnim("attack", RawAnimation.begin().then("anim.velociraptor.attack", Animation.LoopType.PLAY_ONCE))); + + controllers.add(new AnimationController<>(this, "mouthController", state -> PlayState.STOP) + .triggerableAnim("mouth", RawAnimation.begin().then("anim.velociraptor.mouth", Animation.LoopType.PLAY_ONCE))); + } + + private float getSignedTurnDelta() { + // Only consider the body (torso) rotation so head look does not affect tail sway + return Mth.wrapDegrees(this.yBodyRot - this.yBodyRotO); + } + + private int mouthAnimCooldown = 0; + + @Override + public void tick() { + super.tick(); + if (!level().isClientSide) { + this.entityData.set(DATA_SYNCED_AGE, this.getAge()); + var maxHealthAttr = getAttribute(Attributes.MAX_HEALTH); + if (maxHealthAttr != null) { + double baseAdult = DefaultAttributes.getSupplier((EntityType) this.getType()).getValue(Attributes.MAX_HEALTH); + double desired = this.isBaby() ? baseAdult * 0.10D : baseAdult; + if (maxHealthAttr.getBaseValue() != desired) { + double oldMax = maxHealthAttr.getBaseValue(); + double healthRatio = this.getHealth() / (float) oldMax; + maxHealthAttr.setBaseValue(desired); + this.setHealth((float) (desired * Mth.clamp(healthRatio, 0.0F, 1.0F))); + } + } + } + + if (!level().isClientSide) { + if (mouthAnimCooldown > 0) { + mouthAnimCooldown--; + } else { + this.triggerAnim("mouthController", "mouth"); + this.playSound(ModSounds.VELOCIRAPTOR_CALL.get(), 1.0F, 1.0F); + // 30s–60s in ticks + mouthAnimCooldown = this.random.nextInt(1200 - 600 + 1) + 600; + } + } + + if (level().isClientSide) { + // Capture previous for smooth interpolation between ticks + this.tailSwayPrev = this.tailSwayOffset; + updateProceduralTailSway(); + } + } + + private void updateProceduralTailSway() { + // Turn input derived from rotation deltas; works even when standing still and turning + float turnDegrees = getSignedTurnDelta(); + + // Deadzone to ignore tiny jitter so the tail can return to center cleanly + float deadzoneDeg = 0.6f; // smaller deadzone for more responsiveness + float turnInput = 0.0f; + if (Math.abs(turnDegrees) >= deadzoneDeg) { + // Higher sensitivity so small in-place turns still affect the model + turnInput = Mth.clamp(turnDegrees / 15.0f, -1.0f, 1.0f); + } + + // Target offset: keep intuitive sign (positive input -> positive sway) + float target = turnInput; + + // One-pole low-pass (no bounce). Larger alpha => snappier and less "stiff". + float alpha = 0.24f; // try 0.20–0.30 to taste + + tailSwayOffset += (target - tailSwayOffset) * alpha; + + // Snap tiny residuals to zero so it visibly settles + if (Math.abs(tailSwayOffset) < 0.003f) { + tailSwayOffset = 0.0f; + } + + // No oscillation velocity retained + tailSwayVelocity = 0.0f; + + tailSwayOffset = Mth.clamp(tailSwayOffset, -1.5f, 1.5f); + } + + // Expose to the model for bone rotation + public float getTailSwayOffset() { + return tailSwayOffset; + } + + // Interpolated sway for smooth rendering between ticks + public float getTailSwayOffset(float partialTick) { + return Mth.lerp(Mth.clamp(partialTick, 0.0f, 1.0f), tailSwayPrev, tailSwayOffset); + } + + /*? if <=1.20.1 {*/ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); + this.entityData.define(VARIANT, 0); + this.entityData.define(DATA_SYNCED_AGE, 0); + } + /*?} else {*/ + /*@Override + protected void defineSynchedData(SynchedEntityData.Builder pBuilder) { + super.defineSynchedData(pBuilder); + pBuilder.define(VARIANT, 0); + pBuilder.define(DATA_SYNCED_AGE, 0); + } + *//*?}*/ + + public int getSyncedAge() { + return this.entityData.get(DATA_SYNCED_AGE); + } + public int getTypeVariant() { + return this.entityData.get(VARIANT); + } + + public VelociraptorVariant getVariant() { + return VelociraptorVariant.byId(this.getTypeVariant() & 255); + } + + private void setVariant(VelociraptorVariant variant) { + this.entityData.set(VARIANT, variant.getId() & 255); + } + + @Override + public boolean canMate(Animal other) { + if (!super.canMate(other)) return false; + if (!(other instanceof VelociraptorEntity that)) return false; + return this.getVariant() != that.getVariant(); + } + @Override + public void addAdditionalSaveData(CompoundTag pCompound) { + super.addAdditionalSaveData(pCompound); + pCompound.putInt("Variant", this.getTypeVariant()); + } + + @Override + public void readAdditionalSaveData(CompoundTag pCompound) { + super.readAdditionalSaveData(pCompound); + this.entityData.set(VARIANT, pCompound.getInt("Variant")); + } + + /*? if <=1.20.1 {*/ + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor pLevel, DifficultyInstance pDifficulty, MobSpawnType pReason, @Nullable SpawnGroupData pSpawnData, @Nullable CompoundTag pDataTag) { + VelociraptorVariant variant = Util.getRandom(VelociraptorVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(pLevel, pDifficulty, pReason, pSpawnData, pDataTag); + } + /*?} else {*/ + /*@Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, MobSpawnType spawnType, @Nullable SpawnGroupData spawnGroupData) { + VelociraptorVariant variant = Util.getRandom(VelociraptorVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(level, difficulty, spawnType, spawnGroupData); + } + *//*?}*/ + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } + + @Override + protected @Nullable SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.VELOCIRAPTOR_HURT.get(); + } + + @Override + protected @Nullable SoundEvent getDeathSound() { + return ModSounds.VELOCIRAPTOR_DEATH.get(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/custom/ZhenyuanopterusEntity.java b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/ZhenyuanopterusEntity.java new file mode 100644 index 0000000..7ef2ac6 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/ZhenyuanopterusEntity.java @@ -0,0 +1,411 @@ +package net.cmr.jurassicrevived.entity.custom; + +import net.cmr.jurassicrevived.entity.ModEntities; +import net.cmr.jurassicrevived.entity.ai.SprintingMeleeAttackGoal; +import net.cmr.jurassicrevived.entity.ai.SprintingPanicGoal; +import net.cmr.jurassicrevived.entity.client.ZhenyuanopterusVariant; +import net.cmr.jurassicrevived.entity.client.ZhenyuanopterusVariant; +import net.cmr.jurassicrevived.sound.ModSounds; +import net.minecraft.Util; +import net.minecraft.core.BlockPos; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.*; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.attributes.DefaultAttributes; +import net.minecraft.world.entity.ai.control.FlyingMoveControl; +import net.minecraft.world.entity.ai.goal.*; +import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal; +import net.minecraft.world.entity.ai.goal.target.NearestAttackableTargetGoal; +import net.minecraft.world.entity.ai.navigation.FlyingPathNavigation; +import net.minecraft.world.entity.ai.navigation.PathNavigation; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.animal.FlyingAnimal; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerLevelAccessor; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.phys.Vec3; +import org.jetbrains.annotations.Nullable; +import software.bernie.geckolib.animatable.GeoEntity; +/*? if <=1.20.1 {*/ +import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.core.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.core.animation.AnimatableManager; +import software.bernie.geckolib.core.animation.Animation; +import software.bernie.geckolib.core.animation.AnimationController; +import software.bernie.geckolib.core.animation.RawAnimation; +import software.bernie.geckolib.core.object.PlayState; +/*?} else {*/ +/*import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.animatable.instance.SingletonAnimatableInstanceCache; +import software.bernie.geckolib.animation.*; +*//*?}*/ + +public class ZhenyuanopterusEntity extends Animal implements GeoEntity, FlyingAnimal { + private final AnimatableInstanceCache cache = new SingletonAnimatableInstanceCache(this); + + private static final EntityDataAccessor VARIANT = + SynchedEntityData.defineId(ZhenyuanopterusEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SYNCED_AGE = + SynchedEntityData.defineId(ZhenyuanopterusEntity.class, EntityDataSerializers.INT); + + // Procedural tail sway state (client-side use for rendering) + private float tailSwayOffset; // Smoothed offset in range roughly [-1, 1] + private float tailSwayVelocity; // Internal velocity for spring-damper + private float tailSwayPrev; // Previous frame value for interpolation + + public ZhenyuanopterusEntity(EntityType pEntityType, Level pLevel) { + super(pEntityType, pLevel); + this.moveControl = new FlyingMoveControl(this, 20, true); + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(1, new SprintingPanicGoal(this, 1.15) { + @Override + public boolean canUse() { + return ZhenyuanopterusEntity.this.isBaby() && super.canUse(); + } + }); + this.goalSelector.addGoal(2, new FloatGoal(this)); + this.goalSelector.addGoal(3, new AvoidEntityGoal<>(this, BrachiosaurusEntity.class, (float) 20, 1, 1)); + this.goalSelector.addGoal(4, new SprintingMeleeAttackGoal(this, 1.1, false)); + this.goalSelector.addGoal(5, new BreedGoal(this, 1.0)); + this.goalSelector.addGoal(6, new FollowParentGoal(this, 1.25)); + + // Goal 7: Wander on ground (Walk) - Only when on ground + this.goalSelector.addGoal(7, new WaterAvoidingRandomStrollGoal(this, 1.0) { + @Override + public boolean canUse() { + return ZhenyuanopterusEntity.this.onGround() && super.canUse(); + } + }); + + // Goal 8: Wander in air (Fly) - Handles takeoff, flying, and landing + this.goalSelector.addGoal(8, new WaterAvoidingRandomStrollGoal(this, 1.0, 20) { + @Override + public boolean canUse() { + boolean isFlying = !ZhenyuanopterusEntity.this.onGround(); + // If flying, keep flying. If on ground, small chance (1/400 ticks) to take off. + return (isFlying || ZhenyuanopterusEntity.this.getRandom().nextInt(400) == 0) && super.canUse(); + } + + @Override + protected Vec3 getPosition() { + Vec3 pos = ZhenyuanopterusEntity.this.position(); + RandomSource random = ZhenyuanopterusEntity.this.getRandom(); + + double x = pos.x + (random.nextFloat() * 2 - 1) * 32; + double z = pos.z + (random.nextFloat() * 2 - 1) * 32; + + // Get ground height at the random destination (returns Y of first air block) + int groundY = ZhenyuanopterusEntity.this.level().getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, (int)x, (int)z); + double y; + + if (ZhenyuanopterusEntity.this.onGround()) { + // Takeoff: Target well above ground to ensure liftoff + y = pos.y + 15 + random.nextInt(10); + } else { + // Flying: 5% chance to land, otherwise stay airborne but capped height + if (random.nextFloat() < 0.05f) { + y = groundY; // Land + } else if (pos.y > groundY + 20) { + // Too high: Force descent + y = pos.y - 5 - random.nextInt(10); + } else { + // Just wander up or down a bit + y = pos.y + (random.nextFloat() * 2 - 1) * 10; + } + } + + // Don't target below the ground (blocks) + if (y < groundY) y = groundY; + + return new Vec3(x, y, z); + } + }); + + this.goalSelector.addGoal(9, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(10, new FollowMobGoal(this, 0.8, (float) 20, (float) 10)); + this.targetSelector.addGoal(11, new NearestAttackableTargetGoal<>(this, Animal.class, 10, false, false, + target -> { + // 1. Don't eat your own species + if (target.getType() == this.getType()) return false; + + // 2. Don't eat Flying Animals + if (target instanceof FlyingAnimal) return false; + + // 3. SIZE CHECK: specific height and width limits + // Example: Height < 1.0 blocks AND Width < 1.0 blocks + boolean isSmallEnough = target.getBbHeight() <= 1.0F && target.getBbWidth() <= 1.0F; + + return isSmallEnough; + } + )); + this.targetSelector.addGoal(12, new NearestAttackableTargetGoal<>(this, Player.class, false, false)); + this.targetSelector.addGoal(13, new HurtByTargetGoal(this)); + this.goalSelector.addGoal(14, new RandomLookAroundGoal(this)); + this.goalSelector.addGoal(15, new FloatGoal(this)); + } + + public static AttributeSupplier.Builder createAttributes() { + return Animal.createLivingAttributes() + .add(Attributes.MAX_HEALTH, 50D) + .add(Attributes.MOVEMENT_SPEED, 0.3D) + .add(Attributes.FLYING_SPEED, 0.6D) + .add(Attributes.ARMOR, 0D) + .add(Attributes.FOLLOW_RANGE, 32D) + .add(Attributes.KNOCKBACK_RESISTANCE, 0D) + .add(Attributes.ATTACK_KNOCKBACK, 0D) + .add(Attributes.ATTACK_DAMAGE, 8D); + } + + @Override + protected PathNavigation createNavigation(Level pLevel) { + FlyingPathNavigation navigation = new FlyingPathNavigation(this, pLevel); + navigation.setCanOpenDoors(false); + navigation.setCanFloat(true); + navigation.setCanPassDoors(true); + return navigation; + } + + @Override + public boolean isFlying() { + return !this.onGround(); + } + + @Override + public boolean causeFallDamage(float pFallDistance, float pMultiplier, DamageSource pSource) { + return false; + } + + @Override + protected void checkFallDamage(double pY, boolean pOnGround, BlockState pState, BlockPos pPos) { + } + + @Override + public boolean isFood(ItemStack pStack) { + return pStack.is(Items.BEEF); + } + + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel pLevel, AgeableMob pOtherParent) { + AgeableMob child = ModEntities.ZHENYUANOPTERUS.get().create(pLevel); + if (child instanceof ZhenyuanopterusEntity baby) { + ZhenyuanopterusVariant randomVariant = Util.getRandom(ZhenyuanopterusVariant.values(), this.random); + baby.setVariant(randomVariant); + } + return child; + } + + @Override + public boolean doHurtTarget(Entity target) { + boolean hit = super.doHurtTarget(target); + if (!level().isClientSide && hit && target instanceof LivingEntity) { + if (this.level() instanceof ServerLevel serverLevel) { + this.triggerAnim("attackController", "attack"); + this.playSound(ModSounds.BEAK_ATTACK.get(), 1.0F, 1.0F); + } + } + return hit; + } + + @Override + public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { + controllers.add(new AnimationController<>(this, "Walk/Run/Idle", state -> { + if (!ZhenyuanopterusEntity.this.onGround()) { + return state.setAndContinue(RawAnimation.begin().then("anim.zhenyuanopterus.fly", Animation.LoopType.LOOP)); + } + + if (state.isMoving()) + return state.setAndContinue(ZhenyuanopterusEntity.this.isSprinting() ? RawAnimation.begin().then("anim.zhenyuanopterus.walk", Animation.LoopType.LOOP) : RawAnimation.begin().then("anim.zhenyuanopterus.walk", Animation.LoopType.LOOP)); + + return state.setAndContinue(RawAnimation.begin().then("anim.zhenyuanopterus.idle", Animation.LoopType.LOOP)); + })); + + controllers.add(new AnimationController<>(this, "attackController", state -> PlayState.STOP) + .triggerableAnim("attack", RawAnimation.begin().then("anim.zhenyuanopterus.attack", Animation.LoopType.PLAY_ONCE))); + + controllers.add(new AnimationController<>(this, "mouthController", state -> PlayState.STOP) + .triggerableAnim("mouth", RawAnimation.begin().then("anim.zhenyuanopterus.mouth", Animation.LoopType.PLAY_ONCE))); + } + + private float getSignedTurnDelta() { + // Only consider the body (torso) rotation so head look does not affect tail sway + return Mth.wrapDegrees(this.yBodyRot - this.yBodyRotO); + } + + private int mouthAnimCooldown = 0; + + @Override + public void tick() { + super.tick(); + if (!level().isClientSide) { + this.entityData.set(DATA_SYNCED_AGE, this.getAge()); + var maxHealthAttr = getAttribute(Attributes.MAX_HEALTH); + if (maxHealthAttr != null) { + double baseAdult = DefaultAttributes.getSupplier((EntityType) this.getType()).getValue(Attributes.MAX_HEALTH); + double desired = this.isBaby() ? baseAdult * 0.10D : baseAdult; + if (maxHealthAttr.getBaseValue() != desired) { + double oldMax = maxHealthAttr.getBaseValue(); + double healthRatio = this.getHealth() / (float) oldMax; + maxHealthAttr.setBaseValue(desired); + this.setHealth((float) (desired * Mth.clamp(healthRatio, 0.0F, 1.0F))); + } + } + } + + if (!level().isClientSide) { + if (mouthAnimCooldown > 0) { + mouthAnimCooldown--; + } else { + this.triggerAnim("mouthController", "mouth"); + this.playSound(ModSounds.ZHENYUANOPTERUS_CALL.get(), 1.0F, 1.0F); + // 30s–60s in ticks + mouthAnimCooldown = this.random.nextInt(1200 - 600 + 1) + 600; + } + } + + if (level().isClientSide) { + // Capture previous for smooth interpolation between ticks + this.tailSwayPrev = this.tailSwayOffset; + updateProceduralTailSway(); + } + } + + private void updateProceduralTailSway() { + // Turn input derived from rotation deltas; works even when standing still and turning + float turnDegrees = getSignedTurnDelta(); + + // Deadzone to ignore tiny jitter so the tail can return to center cleanly + float deadzoneDeg = 0.6f; // smaller deadzone for more responsiveness + float turnInput = 0.0f; + if (Math.abs(turnDegrees) >= deadzoneDeg) { + // Higher sensitivity so small in-place turns still affect the model + turnInput = Mth.clamp(turnDegrees / 15.0f, -1.0f, 1.0f); + } + + // Target offset: keep intuitive sign (positive input -> positive sway) + float target = turnInput; + + // One-pole low-pass (no bounce). Larger alpha => snappier and less "stiff". + float alpha = 0.24f; // try 0.20–0.30 to taste + + tailSwayOffset += (target - tailSwayOffset) * alpha; + + // Snap tiny residuals to zero so it visibly settles + if (Math.abs(tailSwayOffset) < 0.003f) { + tailSwayOffset = 0.0f; + } + + // No oscillation velocity retained + tailSwayVelocity = 0.0f; + + tailSwayOffset = Mth.clamp(tailSwayOffset, -1.5f, 1.5f); + } + + // Expose to the model for bone rotation + public float getTailSwayOffset() { + return tailSwayOffset; + } + + // Interpolated sway for smooth rendering between ticks + public float getTailSwayOffset(float partialTick) { + return Mth.lerp(Mth.clamp(partialTick, 0.0f, 1.0f), tailSwayPrev, tailSwayOffset); + } + + /*? if <=1.20.1 {*/ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); + this.entityData.define(VARIANT, 0); + this.entityData.define(DATA_SYNCED_AGE, 0); + } + /*?} else {*/ + /*@Override + protected void defineSynchedData(SynchedEntityData.Builder pBuilder) { + super.defineSynchedData(pBuilder); + pBuilder.define(VARIANT, 0); + pBuilder.define(DATA_SYNCED_AGE, 0); + } + *//*?}*/ + + public int getSyncedAge() { + return this.entityData.get(DATA_SYNCED_AGE); + } + + public ZhenyuanopterusVariant getVariant() { + return ZhenyuanopterusVariant.byId(this.getTypeVariant() & 255); + } + public int getTypeVariant() { + return this.entityData.get(VARIANT); + } + + private void setVariant(ZhenyuanopterusVariant variant) { + this.entityData.set(VARIANT, variant.getId() & 255); + } + + @Override + public boolean canMate(Animal other) { + if (!super.canMate(other)) return false; + if (!(other instanceof ZhenyuanopterusEntity that)) return false; + return this.getVariant() != that.getVariant(); + } + + /*? if <=1.20.1 {*/ + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor pLevel, DifficultyInstance pDifficulty, MobSpawnType pReason, @Nullable SpawnGroupData pSpawnData, @Nullable CompoundTag pDataTag) { + ZhenyuanopterusVariant variant = Util.getRandom(ZhenyuanopterusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(pLevel, pDifficulty, pReason, pSpawnData, pDataTag); + } + /*?} else {*/ + /*@Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, MobSpawnType spawnType, @Nullable SpawnGroupData spawnGroupData) { + ZhenyuanopterusVariant variant = Util.getRandom(ZhenyuanopterusVariant.values(), this.random); + this.setVariant(variant); + return super.finalizeSpawn(level, difficulty, spawnType, spawnGroupData); + } + *//*?}*/ + + @Override + public void readAdditionalSaveData(CompoundTag pCompound) { + super.readAdditionalSaveData(pCompound); + this.entityData.set(VARIANT, pCompound.getInt("Variant")); + } + + @Override + public void addAdditionalSaveData(CompoundTag pCompound) { + super.addAdditionalSaveData(pCompound); + pCompound.putInt("Variant", this.getTypeVariant()); + } + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } + + @Override + protected @Nullable SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.ZHENYUANOPTERUS_HURT.get(); + } + + @Override + protected @Nullable SoundEvent getDeathSound() { + return ModSounds.ZHENYUANOPTERUS_DEATH.get(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/item/ModCreativeTabs.java b/common/src/main/java/net/cmr/jurassicrevived/item/ModCreativeTabs.java index 99196e3..126c20b 100644 --- a/common/src/main/java/net/cmr/jurassicrevived/item/ModCreativeTabs.java +++ b/common/src/main/java/net/cmr/jurassicrevived/item/ModCreativeTabs.java @@ -4,6 +4,7 @@ import dev.architectury.registry.CreativeTabRegistry; import dev.architectury.registry.registries.DeferredRegister; import dev.architectury.registry.registries.RegistrySupplier; import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.block.ModBlocks; import net.minecraft.core.registries.Registries; import net.minecraft.network.chat.Component; import net.minecraft.world.item.CreativeModeTab; @@ -13,17 +14,644 @@ public class ModCreativeTabs { public static final DeferredRegister TABS = DeferredRegister.create(Constants.MOD_ID, Registries.CREATIVE_MODE_TAB); - public static final RegistrySupplier JURASSIC_TAB = TABS.register("jurassic_tab", - () -> CreativeTabRegistry.create( - Component.translatable("itemGroup." + Constants.MOD_ID + ".jurassic_tab"), - () -> new ItemStack(ModItems.AMBER_SHARD.get()) // Tab Icon - )); + public static final RegistrySupplier ITEM_TAB = TABS.register("item_tab", + () -> CreativeTabRegistry.create(builder -> { + builder.title(Component.translatable("itemGroup." + Constants.MOD_ID + ".item_tab")); + builder.icon(() -> new ItemStack(ModItems.MOSQUITO_IN_AMBER.get())); + builder.displayItems((params, output) -> { + output.accept(ModItems.FROG_MATERIAL.get()); + output.accept(ModItems.FROG_DNA.get()); + output.accept(ModItems.TEST_TUBE.get()); + output.accept(ModItems.SYRINGE.get()); + output.accept(ModItems.CRUSHED_FOSSIL.get()); + output.accept(ModItems.MOSQUITO_IN_AMBER.get()); + output.accept(ModItems.FROZEN_LEECH.get()); + output.accept(ModItems.CABLE.get()); + output.accept(ModItems.SCREEN.get()); + output.accept(ModItems.PROCESSOR.get()); + output.accept(ModItems.TIRE.get()); + output.accept(ModItems.CUTTING_BLADES.get()); + output.accept(ModItems.WRENCH.get()); + output.accept(ModItems.MAC_N_CHEESE.get()); + output.accept(ModItems.WALNUT_PUMPKIN_PIE.get()); + output.accept(ModItems.BANANA_NUT_COOKIE.get()); + }); + })); + + public static final RegistrySupplier BLOCK_TAB = TABS.register("block_tab", + () -> CreativeTabRegistry.create(builder -> { + builder.title(Component.translatable("itemGroup." + Constants.MOD_ID + ".block_tab")); + builder.icon(() -> new ItemStack(ModBlocks.GYPSUM_STONE_BRICKS.get())); + builder.displayItems((params, output) -> { + output.accept(ModBlocks.CAT_PLUSHIE.get()); + output.accept(ModBlocks.TRASH_CAN.get()); + output.accept(ModBlocks.BENCH.get()); + output.accept(ModBlocks.CHARRED_TERRACOTTA.get()); + output.accept(ModBlocks.FENCE_LIGHT.get()); + output.accept(ModBlocks.LIGHT_POST.get()); + output.accept(ModBlocks.GYPSUM_STONE.get()); + output.accept(ModBlocks.GYPSUM_COBBLESTONE.get()); + output.accept(ModBlocks.GYPSUM_STONE_BRICKS.get()); + output.accept(ModBlocks.SMOOTH_GYPSUM_STONE.get()); + output.accept(ModBlocks.CHISELED_GYPSUM_STONE.get()); + output.accept(ModBlocks.GYPSUM_BRICK_STAIRS.get()); + output.accept(ModBlocks.GYPSUM_BRICK_SLAB.get()); + output.accept(ModBlocks.GYPSUM_BRICK_WALL.get()); + + output.accept(ModBlocks.REINFORCED_STONE.get()); + output.accept(ModBlocks.REINFORCED_STONE_BRICKS.get()); + output.accept(ModBlocks.CHISELED_REINFORCED_STONE.get()); + output.accept(ModBlocks.REINFORCED_BRICK_STAIRS.get()); + output.accept(ModBlocks.REINFORCED_BRICK_SLAB.get()); + output.accept(ModBlocks.REINFORCED_BRICK_WALL.get()); + + output.accept(ModBlocks.LOW_SECURITY_FENCE_POLE.get()); + output.accept(ModBlocks.LOW_SECURITY_FENCE_WIRE.get()); + output.accept(ModBlocks.MEDIUM_SECURITY_FENCE_POLE.get()); + output.accept(ModBlocks.MEDIUM_SECURITY_FENCE_WIRE.get()); + + output.accept(ModBlocks.ITEM_PIPE.get()); + output.accept(ModBlocks.FLUID_PIPE.get()); + output.accept(ModBlocks.POWER_PIPE.get()); + + output.accept(ModBlocks.TANK.get()); + output.accept(ModBlocks.POWER_CELL.get()); + output.accept(ModBlocks.WOOD_CRATE.get()); + output.accept(ModBlocks.IRON_CRATE.get()); + + output.accept(ModBlocks.GENERATOR.get()); + output.accept(ModBlocks.DNA_EXTRACTOR.get()); + output.accept(ModBlocks.DNA_ANALYZER.get()); + output.accept(ModBlocks.FOSSIL_GRINDER.get()); + output.accept(ModBlocks.FOSSIL_CLEANER.get()); + output.accept(ModBlocks.DNA_HYBRIDIZER.get()); + output.accept(ModBlocks.EMBRYONIC_MACHINE.get()); + output.accept(ModBlocks.EMBRYO_CALCIFICATION_MACHINE.get()); + output.accept(ModBlocks.INCUBATOR.get()); + + output.accept(ModBlocks.WHITE_GENERATOR.get()); + output.accept(ModBlocks.WHITE_DNA_EXTRACTOR.get()); + output.accept(ModBlocks.WHITE_DNA_ANALYZER.get()); + output.accept(ModBlocks.WHITE_FOSSIL_GRINDER.get()); + output.accept(ModBlocks.WHITE_FOSSIL_CLEANER.get()); + output.accept(ModBlocks.WHITE_DNA_HYBRIDIZER.get()); + output.accept(ModBlocks.WHITE_EMBRYONIC_MACHINE.get()); + output.accept(ModBlocks.WHITE_EMBRYO_CALCIFICATION_MACHINE.get()); + output.accept(ModBlocks.WHITE_INCUBATOR.get()); + + output.accept(ModBlocks.STONE_FOSSIL.get()); + output.accept(ModBlocks.DEEPSLATE_FOSSIL.get()); + output.accept(ModBlocks.AMBER_ORE.get()); + output.accept(ModBlocks.DEEPSLATE_ICE_SHARD_ORE.get()); + }); + })); + + public static final RegistrySupplier PLANT_TAB = TABS.register("plant_tab", + () -> CreativeTabRegistry.create(builder -> { + builder.title(Component.translatable("itemGroup." + Constants.MOD_ID + ".plant_tab")); + builder.icon(() -> new ItemStack(ModBlocks.ROYAL_FERN.get().asItem())); + builder.displayItems((params, output) -> { + output.accept(ModBlocks.ROYAL_FERN.get().asItem()); + output.accept(ModBlocks.HORSETAIL_FERN.get().asItem()); + output.accept(ModBlocks.WESTERN_SWORD_FERN.get().asItem()); + output.accept(ModBlocks.ONYCHIOPSIS.get().asItem()); + }); + })); + + public static final RegistrySupplier DNA_TAB = TABS.register("dna_tab", + () -> CreativeTabRegistry.create(builder -> { + builder.title(Component.translatable("itemGroup." + Constants.MOD_ID + ".dna_tab")); + builder.icon(() -> new ItemStack(ModItems.TYRANNOSAURUS_REX_DNA.get().asItem())); + builder.displayItems((params, output) -> { + // Skull fossils (alphabetical) + output.accept(ModItems.ALBERTOSAURUS_SKULL_FOSSIL.get()); + output.accept(ModItems.ALLOSAURUS_SKULL_FOSSIL.get()); + output.accept(ModItems.ALVAREZSAURUS_SKULL_FOSSIL.get()); + output.accept(ModItems.ANKYLOSAURUS_SKULL_FOSSIL.get()); + output.accept(ModItems.APATOSAURUS_SKULL_FOSSIL.get()); + output.accept(ModItems.ARAMBOURGIANIA_SKULL_FOSSIL.get()); + output.accept(ModItems.BARYONYX_SKULL_FOSSIL.get()); + output.accept(ModItems.BRACHIOSAURUS_SKULL_FOSSIL.get()); + output.accept(ModItems.CARCHARODONTOSAURUS_SKULL_FOSSIL.get()); + output.accept(ModItems.CARNOTAURUS_SKULL_FOSSIL.get()); + output.accept(ModItems.CEARADACTYLUS_SKULL_FOSSIL.get()); + output.accept(ModItems.CERATOSAURUS_SKULL_FOSSIL.get()); + output.accept(ModItems.CHASMOSAURUS_SKULL_FOSSIL.get()); + output.accept(ModItems.COELOPHYSIS_SKULL_FOSSIL.get()); + output.accept(ModItems.COELURUS_SKULL_FOSSIL.get()); + output.accept(ModItems.COMPSOGNATHUS_SKULL_FOSSIL.get()); + output.accept(ModItems.CONCAVENATOR_SKULL_FOSSIL.get()); + output.accept(ModItems.CORYTHOSAURUS_SKULL_FOSSIL.get()); + output.accept(ModItems.DEINONYCHUS_SKULL_FOSSIL.get()); + output.accept(ModItems.DILOPHOSAURUS_SKULL_FOSSIL.get()); + output.accept(ModItems.DIMORPHODON_SKULL_FOSSIL.get()); + output.accept(ModItems.DIPLODOCUS_SKULL_FOSSIL.get()); + output.accept(ModItems.DRYOSAURUS_SKULL_FOSSIL.get()); + output.accept(ModItems.EDMONTOSAURUS_SKULL_FOSSIL.get()); + output.accept(ModItems.GALLIMIMUS_SKULL_FOSSIL.get()); + output.accept(ModItems.GEOSTERNBERGIA_SKULL_FOSSIL.get()); + output.accept(ModItems.GIGANOTOSAURUS_SKULL_FOSSIL.get()); + output.accept(ModItems.GUANLONG_SKULL_FOSSIL.get()); + output.accept(ModItems.GUIDRACO_SKULL_FOSSIL.get()); + output.accept(ModItems.HADROSAURUS_SKULL_FOSSIL.get()); + output.accept(ModItems.HERRERASAURUS_SKULL_FOSSIL.get()); + output.accept(ModItems.HYPSILOPHODON_SKULL_FOSSIL.get()); + output.accept(ModItems.INOSTRANCEVIA_SKULL_FOSSIL.get()); + output.accept(ModItems.LAMBEOSAURUS_SKULL_FOSSIL.get()); + output.accept(ModItems.LUDODACTYLUS_SKULL_FOSSIL.get()); + output.accept(ModItems.MAJUNGASAURUS_SKULL_FOSSIL.get()); + output.accept(ModItems.MAMENCHISAURUS_SKULL_FOSSIL.get()); + output.accept(ModItems.METRIACANTHOSAURUS_SKULL_FOSSIL.get()); + output.accept(ModItems.MOGANOPTERUS_SKULL_FOSSIL.get()); + output.accept(ModItems.NYCTOSAURUS_SKULL_FOSSIL.get()); + output.accept(ModItems.ORNITHOLESTES_SKULL_FOSSIL.get()); + output.accept(ModItems.ORNITHOMIMUS_SKULL_FOSSIL.get()); + output.accept(ModItems.OURANOSAURUS_SKULL_FOSSIL.get()); + output.accept(ModItems.OVIRAPTOR_SKULL_FOSSIL.get()); + output.accept(ModItems.PACHYCEPHALOSAURUS_SKULL_FOSSIL.get()); + output.accept(ModItems.PARASAUROLOPHUS_SKULL_FOSSIL.get()); + output.accept(ModItems.PROCERATOSAURUS_SKULL_FOSSIL.get()); + output.accept(ModItems.PROCOMPSOGNATHUS_SKULL_FOSSIL.get()); + output.accept(ModItems.PROTOCERATOPS_SKULL_FOSSIL.get()); + output.accept(ModItems.PTERANODON_SKULL_FOSSIL.get()); + output.accept(ModItems.PTERODAUSTRO_SKULL_FOSSIL.get()); + output.accept(ModItems.QUETZALCOATLUS_SKULL_FOSSIL.get()); + output.accept(ModItems.RAJASAURUS_SKULL_FOSSIL.get()); + output.accept(ModItems.RUGOPS_SKULL_FOSSIL.get()); + output.accept(ModItems.SEGISAURUS_SKULL_FOSSIL.get()); + output.accept(ModItems.SHANTUNGOSAURUS_SKULL_FOSSIL.get()); + output.accept(ModItems.SPINOSAURUS_SKULL_FOSSIL.get()); + output.accept(ModItems.STEGOSAURUS_SKULL_FOSSIL.get()); + output.accept(ModItems.STYRACOSAURUS_SKULL_FOSSIL.get()); + output.accept(ModItems.TAPEJARA_SKULL_FOSSIL.get()); + output.accept(ModItems.THERIZINOSAURUS_SKULL_FOSSIL.get()); + output.accept(ModItems.TITANOSAURUS_SKULL_FOSSIL.get()); + output.accept(ModItems.TRICERATOPS_SKULL_FOSSIL.get()); + output.accept(ModItems.TROODON_SKULL_FOSSIL.get()); + output.accept(ModItems.TROPEOGNATHUS_SKULL_FOSSIL.get()); + output.accept(ModItems.TUPUXUARA_SKULL_FOSSIL.get()); + output.accept(ModItems.TYRANNOSAURUS_REX_SKULL_FOSSIL.get()); + output.accept(ModItems.UTAHRAPTOR_SKULL_FOSSIL.get()); + output.accept(ModItems.VELOCIRAPTOR_SKULL_FOSSIL.get()); + output.accept(ModItems.ZHENYUANOPTERUS_SKULL_FOSSIL.get()); + +// Fresh skulls (alphabetical) + output.accept(ModItems.FRESH_ALBERTOSAURUS_SKULL.get()); + output.accept(ModItems.FRESH_ALLOSAURUS_SKULL.get()); + output.accept(ModItems.FRESH_ALVAREZSAURUS_SKULL.get()); + output.accept(ModItems.FRESH_ANKYLOSAURUS_SKULL.get()); + output.accept(ModItems.FRESH_APATOSAURUS_SKULL.get()); + output.accept(ModItems.FRESH_ARAMBOURGIANIA_SKULL.get()); + output.accept(ModItems.FRESH_BARYONYX_SKULL.get()); + output.accept(ModItems.FRESH_BRACHIOSAURUS_SKULL.get()); + output.accept(ModItems.FRESH_CARCHARODONTOSAURUS_SKULL.get()); + output.accept(ModItems.FRESH_CARNOTAURUS_SKULL.get()); + output.accept(ModItems.FRESH_CEARADACTYLUS_SKULL.get()); + output.accept(ModItems.FRESH_CERATOSAURUS_SKULL.get()); + output.accept(ModItems.FRESH_CHASMOSAURUS_SKULL.get()); + output.accept(ModItems.FRESH_COELOPHYSIS_SKULL.get()); + output.accept(ModItems.FRESH_COELURUS_SKULL.get()); + output.accept(ModItems.FRESH_COMPSOGNATHUS_SKULL.get()); + output.accept(ModItems.FRESH_CONCAVENATOR_SKULL.get()); + output.accept(ModItems.FRESH_CORYTHOSAURUS_SKULL.get()); + output.accept(ModItems.FRESH_DEINONYCHUS_SKULL.get()); + output.accept(ModItems.FRESH_DILOPHOSAURUS_SKULL.get()); + output.accept(ModItems.FRESH_DIMORPHODON_SKULL.get()); + output.accept(ModItems.FRESH_DIPLODOCUS_SKULL.get()); + output.accept(ModItems.FRESH_DISTORTUS_REX_SKULL.get()); + output.accept(ModItems.FRESH_DRYOSAURUS_SKULL.get()); + output.accept(ModItems.FRESH_EDMONTOSAURUS_SKULL.get()); + output.accept(ModItems.FRESH_GALLIMIMUS_SKULL.get()); + output.accept(ModItems.FRESH_GEOSTERNBERGIA_SKULL.get()); + output.accept(ModItems.FRESH_GIGANOTOSAURUS_SKULL.get()); + output.accept(ModItems.FRESH_GUANLONG_SKULL.get()); + output.accept(ModItems.FRESH_GUIDRACO_SKULL.get()); + output.accept(ModItems.FRESH_HADROSAURUS_SKULL.get()); + output.accept(ModItems.FRESH_HERRERASAURUS_SKULL.get()); + output.accept(ModItems.FRESH_HYPSILOPHODON_SKULL.get()); + output.accept(ModItems.FRESH_INDOMINUS_REX_SKULL.get()); + output.accept(ModItems.FRESH_INDORAPTOR_SKULL.get()); + output.accept(ModItems.FRESH_INOSTRANCEVIA_SKULL.get()); + output.accept(ModItems.FRESH_LAMBEOSAURUS_SKULL.get()); + output.accept(ModItems.FRESH_LUDODACTYLUS_SKULL.get()); + output.accept(ModItems.FRESH_MAJUNGASAURUS_SKULL.get()); + output.accept(ModItems.FRESH_MAMENCHISAURUS_SKULL.get()); + output.accept(ModItems.FRESH_METRIACANTHOSAURUS_SKULL.get()); + output.accept(ModItems.FRESH_MOGANOPTERUS_SKULL.get()); + output.accept(ModItems.FRESH_NYCTOSAURUS_SKULL.get()); + output.accept(ModItems.FRESH_ORNITHOLESTES_SKULL.get()); + output.accept(ModItems.FRESH_ORNITHOMIMUS_SKULL.get()); + output.accept(ModItems.FRESH_OURANOSAURUS_SKULL.get()); + output.accept(ModItems.FRESH_OVIRAPTOR_SKULL.get()); + output.accept(ModItems.FRESH_PACHYCEPHALOSAURUS_SKULL.get()); + output.accept(ModItems.FRESH_PARASAUROLOPHUS_SKULL.get()); + output.accept(ModItems.FRESH_PROCERATOSAURUS_SKULL.get()); + output.accept(ModItems.FRESH_PROCOMPSOGNATHUS_SKULL.get()); + output.accept(ModItems.FRESH_PROTOCERATOPS_SKULL.get()); + output.accept(ModItems.FRESH_PTERANODON_SKULL.get()); + output.accept(ModItems.FRESH_PTERODAUSTRO_SKULL.get()); + output.accept(ModItems.FRESH_QUETZALCOATLUS_SKULL.get()); + output.accept(ModItems.FRESH_RAJASAURUS_SKULL.get()); + output.accept(ModItems.FRESH_RUGOPS_SKULL.get()); + output.accept(ModItems.FRESH_SEGISAURUS_SKULL.get()); + output.accept(ModItems.FRESH_SHANTUNGOSAURUS_SKULL.get()); + output.accept(ModItems.FRESH_STEGOSAURUS_SKULL.get()); + output.accept(ModItems.FRESH_STYRACOSAURUS_SKULL.get()); + output.accept(ModItems.FRESH_TAPEJARA_SKULL.get()); + output.accept(ModItems.FRESH_THERIZINOSAURUS_SKULL.get()); + output.accept(ModItems.FRESH_TITANOSAURUS_SKULL.get()); + output.accept(ModItems.FRESH_TRICERATOPS_SKULL.get()); + output.accept(ModItems.FRESH_TROODON_SKULL.get()); + output.accept(ModItems.FRESH_TROPEOGNATHUS_SKULL.get()); + output.accept(ModItems.FRESH_TUPUXUARA_SKULL.get()); + output.accept(ModItems.FRESH_TYRANNOSAURUS_REX_SKULL.get()); + output.accept(ModItems.FRESH_UTAHRAPTOR_SKULL.get()); + output.accept(ModItems.FRESH_VELOCIRAPTOR_SKULL.get()); + output.accept(ModItems.FRESH_ZHENYUANOPTERUS_SKULL.get()); + +// Tissue (alphabetical) + output.accept(ModItems.ALBERTOSAURUS_TISSUE.get()); + output.accept(ModItems.ALLOSAURUS_TISSUE.get()); + output.accept(ModItems.ALVAREZSAURUS_TISSUE.get()); + output.accept(ModItems.ANKYLOSAURUS_TISSUE.get()); + output.accept(ModItems.APATOSAURUS_TISSUE.get()); + output.accept(ModItems.ARAMBOURGIANIA_TISSUE.get()); + output.accept(ModItems.BARYONYX_TISSUE.get()); + output.accept(ModItems.BRACHIOSAURUS_TISSUE.get()); + output.accept(ModItems.CARCHARODONTOSAURUS_TISSUE.get()); + output.accept(ModItems.CARNOTAURUS_TISSUE.get()); + output.accept(ModItems.CEARADACTYLUS_TISSUE.get()); + output.accept(ModItems.CERATOSAURUS_TISSUE.get()); + output.accept(ModItems.CHASMOSAURUS_TISSUE.get()); + output.accept(ModItems.COELOPHYSIS_TISSUE.get()); + output.accept(ModItems.COELURUS_TISSUE.get()); + output.accept(ModItems.COMPSOGNATHUS_TISSUE.get()); + output.accept(ModItems.CONCAVENATOR_TISSUE.get()); + output.accept(ModItems.CORYTHOSAURUS_TISSUE.get()); + output.accept(ModItems.DEINONYCHUS_TISSUE.get()); + output.accept(ModItems.DILOPHOSAURUS_TISSUE.get()); + output.accept(ModItems.DIMORPHODON_TISSUE.get()); + output.accept(ModItems.DIPLODOCUS_TISSUE.get()); + output.accept(ModItems.DISTORTUS_REX_TISSUE.get()); + output.accept(ModItems.DRYOSAURUS_TISSUE.get()); + output.accept(ModItems.EDMONTOSAURUS_TISSUE.get()); + output.accept(ModItems.GALLIMIMUS_TISSUE.get()); + output.accept(ModItems.GEOSTERNBERGIA_TISSUE.get()); + output.accept(ModItems.GIGANOTOSAURUS_TISSUE.get()); + output.accept(ModItems.GUANLONG_TISSUE.get()); + output.accept(ModItems.GUIDRACO_TISSUE.get()); + output.accept(ModItems.HADROSAURUS_TISSUE.get()); + output.accept(ModItems.HERRERASAURUS_TISSUE.get()); + output.accept(ModItems.HYPSILOPHODON_TISSUE.get()); + output.accept(ModItems.INDOMINUS_REX_TISSUE.get()); + output.accept(ModItems.INDORAPTOR_TISSUE.get()); + output.accept(ModItems.INOSTRANCEVIA_TISSUE.get()); + output.accept(ModItems.LAMBEOSAURUS_TISSUE.get()); + output.accept(ModItems.LUDODACTYLUS_TISSUE.get()); + output.accept(ModItems.MAJUNGASAURUS_TISSUE.get()); + output.accept(ModItems.MAMENCHISAURUS_TISSUE.get()); + output.accept(ModItems.METRIACANTHOSAURUS_TISSUE.get()); + output.accept(ModItems.MOGANOPTERUS_TISSUE.get()); + output.accept(ModItems.NYCTOSAURUS_TISSUE.get()); + output.accept(ModItems.ORNITHOLESTES_TISSUE.get()); + output.accept(ModItems.ORNITHOMIMUS_TISSUE.get()); + output.accept(ModItems.OURANOSAURUS_TISSUE.get()); + output.accept(ModItems.OVIRAPTOR_TISSUE.get()); + output.accept(ModItems.PACHYCEPHALOSAURUS_TISSUE.get()); + output.accept(ModItems.PARASAUROLOPHUS_TISSUE.get()); + output.accept(ModItems.PROCERATOSAURUS_TISSUE.get()); + output.accept(ModItems.PROCOMPSOGNATHUS_TISSUE.get()); + output.accept(ModItems.PROTOCERATOPS_TISSUE.get()); + output.accept(ModItems.PTERANODON_TISSUE.get()); + output.accept(ModItems.PTERODAUSTRO_TISSUE.get()); + output.accept(ModItems.QUETZALCOATLUS_TISSUE.get()); + output.accept(ModItems.RAJASAURUS_TISSUE.get()); + output.accept(ModItems.RUGOPS_TISSUE.get()); + output.accept(ModItems.SEGISAURUS_TISSUE.get()); + output.accept(ModItems.SHANTUNGOSAURUS_TISSUE.get()); + output.accept(ModItems.SPINOSAURUS_TISSUE.get()); + output.accept(ModItems.STEGOSAURUS_TISSUE.get()); + output.accept(ModItems.STYRACOSAURUS_TISSUE.get()); + output.accept(ModItems.TAPEJARA_TISSUE.get()); + output.accept(ModItems.THERIZINOSAURUS_TISSUE.get()); + output.accept(ModItems.TITANOSAURUS_TISSUE.get()); + output.accept(ModItems.TRICERATOPS_TISSUE.get()); + output.accept(ModItems.TROODON_TISSUE.get()); + output.accept(ModItems.TROPEOGNATHUS_TISSUE.get()); + output.accept(ModItems.TUPUXUARA_TISSUE.get()); + output.accept(ModItems.TYRANNOSAURUS_REX_TISSUE.get()); + output.accept(ModItems.UTAHRAPTOR_TISSUE.get()); + output.accept(ModItems.VELOCIRAPTOR_TISSUE.get()); + output.accept(ModItems.ZHENYUANOPTERUS_TISSUE.get()); + +// DNA (alphabetical) + output.accept(ModItems.ALBERTOSAURUS_DNA.get()); + output.accept(ModItems.ALLOSAURUS_DNA.get()); + output.accept(ModItems.ALVAREZSAURUS_DNA.get()); + output.accept(ModItems.ANKYLOSAURUS_DNA.get()); + output.accept(ModItems.APATOSAURUS_DNA.get()); + output.accept(ModItems.ARAMBOURGIANIA_DNA.get()); + output.accept(ModItems.BARYONYX_DNA.get()); + output.accept(ModItems.BRACHIOSAURUS_DNA.get()); + output.accept(ModItems.CARCHARODONTOSAURUS_DNA.get()); + output.accept(ModItems.CARNOTAURUS_DNA.get()); + output.accept(ModItems.CEARADACTYLUS_DNA.get()); + output.accept(ModItems.CERATOSAURUS_DNA.get()); + output.accept(ModItems.CHASMOSAURUS_DNA.get()); + output.accept(ModItems.COELOPHYSIS_DNA.get()); + output.accept(ModItems.COELURUS_DNA.get()); + output.accept(ModItems.COMPSOGNATHUS_DNA.get()); + output.accept(ModItems.CONCAVENATOR_DNA.get()); + output.accept(ModItems.CORYTHOSAURUS_DNA.get()); + output.accept(ModItems.DEINONYCHUS_DNA.get()); + output.accept(ModItems.DILOPHOSAURUS_DNA.get()); + output.accept(ModItems.DIMORPHODON_DNA.get()); + output.accept(ModItems.DIPLODOCUS_DNA.get()); + output.accept(ModItems.DISTORTUS_REX_DNA.get()); + output.accept(ModItems.DRYOSAURUS_DNA.get()); + output.accept(ModItems.EDMONTOSAURUS_DNA.get()); + output.accept(ModItems.GALLIMIMUS_DNA.get()); + output.accept(ModItems.GEOSTERNBERGIA_DNA.get()); + output.accept(ModItems.GIGANOTOSAURUS_DNA.get()); + output.accept(ModItems.GUANLONG_DNA.get()); + output.accept(ModItems.GUIDRACO_DNA.get()); + output.accept(ModItems.HADROSAURUS_DNA.get()); + output.accept(ModItems.HERRERASAURUS_DNA.get()); + output.accept(ModItems.HYPSILOPHODON_DNA.get()); + output.accept(ModItems.INDOMINUS_REX_DNA.get()); + output.accept(ModItems.INDORAPTOR_DNA.get()); + output.accept(ModItems.INOSTRANCEVIA_DNA.get()); + output.accept(ModItems.LAMBEOSAURUS_DNA.get()); + output.accept(ModItems.LUDODACTYLUS_DNA.get()); + output.accept(ModItems.MAJUNGASAURUS_DNA.get()); + output.accept(ModItems.MAMENCHISAURUS_DNA.get()); + output.accept(ModItems.METRIACANTHOSAURUS_DNA.get()); + output.accept(ModItems.MOGANOPTERUS_DNA.get()); + output.accept(ModItems.NYCTOSAURUS_DNA.get()); + output.accept(ModItems.ORNITHOLESTES_DNA.get()); + output.accept(ModItems.ORNITHOMIMUS_DNA.get()); + output.accept(ModItems.OURANOSAURUS_DNA.get()); + output.accept(ModItems.OVIRAPTOR_DNA.get()); + output.accept(ModItems.PACHYCEPHALOSAURUS_DNA.get()); + output.accept(ModItems.PARASAUROLOPHUS_DNA.get()); + output.accept(ModItems.PROCERATOSAURUS_DNA.get()); + output.accept(ModItems.PROCOMPSOGNATHUS_DNA.get()); + output.accept(ModItems.PROTOCERATOPS_DNA.get()); + output.accept(ModItems.PTERANODON_DNA.get()); + output.accept(ModItems.PTERODAUSTRO_DNA.get()); + output.accept(ModItems.QUETZALCOATLUS_DNA.get()); + output.accept(ModItems.RAJASAURUS_DNA.get()); + output.accept(ModItems.RUGOPS_DNA.get()); + output.accept(ModItems.SEGISAURUS_DNA.get()); + output.accept(ModItems.SHANTUNGOSAURUS_DNA.get()); + output.accept(ModItems.SPINOSAURUS_DNA.get()); + output.accept(ModItems.STEGOSAURUS_DNA.get()); + output.accept(ModItems.STYRACOSAURUS_DNA.get()); + output.accept(ModItems.TAPEJARA_DNA.get()); + output.accept(ModItems.THERIZINOSAURUS_DNA.get()); + output.accept(ModItems.TITANOSAURUS_DNA.get()); + output.accept(ModItems.TRICERATOPS_DNA.get()); + output.accept(ModItems.TROODON_DNA.get()); + output.accept(ModItems.TROPEOGNATHUS_DNA.get()); + output.accept(ModItems.TUPUXUARA_DNA.get()); + output.accept(ModItems.TYRANNOSAURUS_REX_DNA.get()); + output.accept(ModItems.UTAHRAPTOR_DNA.get()); + output.accept(ModItems.VELOCIRAPTOR_DNA.get()); + output.accept(ModItems.ZHENYUANOPTERUS_DNA.get()); + +// Syringes (alphabetical) + output.accept(ModItems.ALBERTOSAURUS_SYRINGE.get()); + output.accept(ModItems.ALLOSAURUS_SYRINGE.get()); + output.accept(ModItems.ALVAREZSAURUS_SYRINGE.get()); + output.accept(ModItems.ANKYLOSAURUS_SYRINGE.get()); + output.accept(ModItems.APATOSAURUS_SYRINGE.get()); + output.accept(ModItems.ARAMBOURGIANIA_SYRINGE.get()); + output.accept(ModItems.BARYONYX_SYRINGE.get()); + output.accept(ModItems.BRACHIOSAURUS_SYRINGE.get()); + output.accept(ModItems.CARCHARODONTOSAURUS_SYRINGE.get()); + output.accept(ModItems.CARNOTAURUS_SYRINGE.get()); + output.accept(ModItems.CEARADACTYLUS_SYRINGE.get()); + output.accept(ModItems.CERATOSAURUS_SYRINGE.get()); + output.accept(ModItems.CHASMOSAURUS_SYRINGE.get()); + output.accept(ModItems.COELOPHYSIS_SYRINGE.get()); + output.accept(ModItems.COELURUS_SYRINGE.get()); + output.accept(ModItems.COMPSOGNATHUS_SYRINGE.get()); + output.accept(ModItems.CONCAVENATOR_SYRINGE.get()); + output.accept(ModItems.CORYTHOSAURUS_SYRINGE.get()); + output.accept(ModItems.DEINONYCHUS_SYRINGE.get()); + output.accept(ModItems.DILOPHOSAURUS_SYRINGE.get()); + output.accept(ModItems.DIMORPHODON_SYRINGE.get()); + output.accept(ModItems.DIPLODOCUS_SYRINGE.get()); + output.accept(ModItems.DISTORTUS_REX_SYRINGE.get()); + output.accept(ModItems.DRYOSAURUS_SYRINGE.get()); + output.accept(ModItems.EDMONTOSAURUS_SYRINGE.get()); + output.accept(ModItems.GALLIMIMUS_SYRINGE.get()); + output.accept(ModItems.GEOSTERNBERGIA_SYRINGE.get()); + output.accept(ModItems.GIGANOTOSAURUS_SYRINGE.get()); + output.accept(ModItems.GUANLONG_SYRINGE.get()); + output.accept(ModItems.GUIDRACO_SYRINGE.get()); + output.accept(ModItems.HADROSAURUS_SYRINGE.get()); + output.accept(ModItems.HERRERASAURUS_SYRINGE.get()); + output.accept(ModItems.HYPSILOPHODON_SYRINGE.get()); + output.accept(ModItems.INDOMINUS_REX_SYRINGE.get()); + output.accept(ModItems.INDORAPTOR_SYRINGE.get()); + output.accept(ModItems.INOSTRANCEVIA_SYRINGE.get()); + output.accept(ModItems.LAMBEOSAURUS_SYRINGE.get()); + output.accept(ModItems.LUDODACTYLUS_SYRINGE.get()); + output.accept(ModItems.MAJUNGASAURUS_SYRINGE.get()); + output.accept(ModItems.MAMENCHISAURUS_SYRINGE.get()); + output.accept(ModItems.METRIACANTHOSAURUS_SYRINGE.get()); + output.accept(ModItems.MOGANOPTERUS_SYRINGE.get()); + output.accept(ModItems.NYCTOSAURUS_SYRINGE.get()); + output.accept(ModItems.ORNITHOLESTES_SYRINGE.get()); + output.accept(ModItems.ORNITHOMIMUS_SYRINGE.get()); + output.accept(ModItems.OURANOSAURUS_SYRINGE.get()); + output.accept(ModItems.OVIRAPTOR_SYRINGE.get()); + output.accept(ModItems.PACHYCEPHALOSAURUS_SYRINGE.get()); + output.accept(ModItems.PARASAUROLOPHUS_SYRINGE.get()); + output.accept(ModItems.PROCERATOSAURUS_SYRINGE.get()); + output.accept(ModItems.PROCOMPSOGNATHUS_SYRINGE.get()); + output.accept(ModItems.PROTOCERATOPS_SYRINGE.get()); + output.accept(ModItems.PTERANODON_SYRINGE.get()); + output.accept(ModItems.PTERODAUSTRO_SYRINGE.get()); + output.accept(ModItems.QUETZALCOATLUS_SYRINGE.get()); + output.accept(ModItems.RAJASAURUS_SYRINGE.get()); + output.accept(ModItems.RUGOPS_SYRINGE.get()); + output.accept(ModItems.SEGISAURUS_SYRINGE.get()); + output.accept(ModItems.SHANTUNGOSAURUS_SYRINGE.get()); + output.accept(ModItems.SPINOSAURUS_SYRINGE.get()); + output.accept(ModItems.STEGOSAURUS_SYRINGE.get()); + output.accept(ModItems.STYRACOSAURUS_SYRINGE.get()); + output.accept(ModItems.TAPEJARA_SYRINGE.get()); + output.accept(ModItems.THERIZINOSAURUS_SYRINGE.get()); + output.accept(ModItems.TITANOSAURUS_SYRINGE.get()); + output.accept(ModItems.TRICERATOPS_SYRINGE.get()); + output.accept(ModItems.TROODON_SYRINGE.get()); + output.accept(ModItems.TROPEOGNATHUS_SYRINGE.get()); + output.accept(ModItems.TUPUXUARA_SYRINGE.get()); + output.accept(ModItems.TYRANNOSAURUS_REX_SYRINGE.get()); + output.accept(ModItems.UTAHRAPTOR_SYRINGE.get()); + output.accept(ModItems.VELOCIRAPTOR_SYRINGE.get()); + output.accept(ModItems.ZHENYUANOPTERUS_SYRINGE.get()); + +// Eggs (alphabetical) + output.accept(ModBlocks.ALBERTOSAURUS_EGG.get()); + output.accept(ModBlocks.ALLOSAURUS_EGG.get()); + output.accept(ModBlocks.ALVAREZSAURUS_EGG.get()); + output.accept(ModBlocks.ANKYLOSAURUS_EGG.get()); + output.accept(ModBlocks.APATOSAURUS_EGG.get()); + output.accept(ModBlocks.ARAMBOURGIANIA_EGG.get()); + output.accept(ModBlocks.BARYONYX_EGG.get()); + output.accept(ModBlocks.BRACHIOSAURUS_EGG.get()); + output.accept(ModBlocks.CARCHARODONTOSAURUS_EGG.get()); + output.accept(ModBlocks.CARNOTAURUS_EGG.get()); + output.accept(ModBlocks.CEARADACTYLUS_EGG.get()); + output.accept(ModBlocks.CERATOSAURUS_EGG.get()); + output.accept(ModBlocks.CHASMOSAURUS_EGG.get()); + output.accept(ModBlocks.COELOPHYSIS_EGG.get()); + output.accept(ModBlocks.COELURUS_EGG.get()); + output.accept(ModBlocks.COMPSOGNATHUS_EGG.get()); + output.accept(ModBlocks.CONCAVENATOR_EGG.get()); + output.accept(ModBlocks.CORYTHOSAURUS_EGG.get()); + output.accept(ModBlocks.DEINONYCHUS_EGG.get()); + output.accept(ModBlocks.DILOPHOSAURUS_EGG.get()); + output.accept(ModBlocks.DIMORPHODON_EGG.get()); + output.accept(ModBlocks.DIPLODOCUS_EGG.get()); + output.accept(ModBlocks.DISTORTUS_REX_EGG.get()); + output.accept(ModBlocks.DRYOSAURUS_EGG.get()); + output.accept(ModBlocks.EDMONTOSAURUS_EGG.get()); + output.accept(ModBlocks.GALLIMIMUS_EGG.get()); + output.accept(ModBlocks.GEOSTERNBERGIA_EGG.get()); + output.accept(ModBlocks.GIGANOTOSAURUS_EGG.get()); + output.accept(ModBlocks.GUANLONG_EGG.get()); + output.accept(ModBlocks.GUIDRACO_EGG.get()); + output.accept(ModBlocks.HADROSAURUS_EGG.get()); + output.accept(ModBlocks.HERRERASAURUS_EGG.get()); + output.accept(ModBlocks.HYPSILOPHODON_EGG.get()); + output.accept(ModBlocks.INDOMINUS_REX_EGG.get()); + output.accept(ModBlocks.INDORAPTOR_EGG.get()); + output.accept(ModBlocks.INOSTRANCEVIA_EGG.get()); + output.accept(ModBlocks.LAMBEOSAURUS_EGG.get()); + output.accept(ModBlocks.LUDODACTYLUS_EGG.get()); + output.accept(ModBlocks.MAJUNGASAURUS_EGG.get()); + output.accept(ModBlocks.MAMENCHISAURUS_EGG.get()); + output.accept(ModBlocks.METRIACANTHOSAURUS_EGG.get()); + output.accept(ModBlocks.MOGANOPTERUS_EGG.get()); + output.accept(ModBlocks.NYCTOSAURUS_EGG.get()); + output.accept(ModBlocks.ORNITHOLESTES_EGG.get()); + output.accept(ModBlocks.ORNITHOMIMUS_EGG.get()); + output.accept(ModBlocks.OURANOSAURUS_EGG.get()); + output.accept(ModBlocks.OVIRAPTOR_EGG.get()); + output.accept(ModBlocks.PACHYCEPHALOSAURUS_EGG.get()); + output.accept(ModBlocks.PROCERATOSAURUS_EGG.get()); + output.accept(ModBlocks.PROCOMPSOGNATHUS_EGG.get()); + output.accept(ModBlocks.PROTOCERATOPS_EGG.get()); + output.accept(ModBlocks.PTERANODON_EGG.get()); + output.accept(ModBlocks.PTERODAUSTRO_EGG.get()); + output.accept(ModBlocks.QUETZALCOATLUS_EGG.get()); + output.accept(ModBlocks.RAJASAURUS_EGG.get()); + output.accept(ModBlocks.RUGOPS_EGG.get()); + output.accept(ModBlocks.SEGISAURUS_EGG.get()); + output.accept(ModBlocks.SHANTUNGOSAURUS_EGG.get()); + output.accept(ModBlocks.SPINOSAURUS_EGG.get()); + output.accept(ModBlocks.STEGOSAURUS_EGG.get()); + output.accept(ModBlocks.STYRACOSAURUS_EGG.get()); + output.accept(ModBlocks.TAPEJARA_EGG.get()); + output.accept(ModBlocks.THERIZINOSAURUS_EGG.get()); + output.accept(ModBlocks.TITANOSAURUS_EGG.get()); + output.accept(ModBlocks.TRICERATOPS_EGG.get()); + output.accept(ModBlocks.TROODON_EGG.get()); + output.accept(ModBlocks.TROPEOGNATHUS_EGG.get()); + output.accept(ModBlocks.TUPUXUARA_EGG.get()); + output.accept(ModBlocks.TYRANNOSAURUS_REX_EGG.get()); + output.accept(ModBlocks.UTAHRAPTOR_EGG.get()); + output.accept(ModBlocks.VELOCIRAPTOR_EGG.get()); + output.accept(ModBlocks.ZHENYUANOPTERUS_EGG.get()); + }); + })); + + public static final RegistrySupplier DINO_TAB = TABS.register("dino_tab", + () -> CreativeTabRegistry.create(builder -> { + builder.title(Component.translatable("itemGroup." + Constants.MOD_ID + ".dino_tab")); + builder.icon(() -> new ItemStack(ModItems.TYRANNOSAURUS_REX_SPAWN_EGG.get().asItem())); + builder.displayItems((params, output) -> { + output.accept(ModItems.ALBERTOSAURUS_SPAWN_EGG.get()); + output.accept(ModItems.ALLOSAURUS_SPAWN_EGG.get()); + output.accept(ModItems.ALVAREZSAURUS_SPAWN_EGG.get()); + output.accept(ModItems.ANKYLOSAURUS_SPAWN_EGG.get()); + output.accept(ModItems.APATOSAURUS_SPAWN_EGG.get()); + output.accept(ModItems.ARAMBOURGIANIA_SPAWN_EGG.get()); + output.accept(ModItems.BARYONYX_SPAWN_EGG.get()); + output.accept(ModItems.BRACHIOSAURUS_SPAWN_EGG.get()); + output.accept(ModItems.CARCHARODONTOSAURUS_SPAWN_EGG.get()); + output.accept(ModItems.CARNOTAURUS_SPAWN_EGG.get()); + output.accept(ModItems.CEARADACTYLUS_SPAWN_EGG.get()); + output.accept(ModItems.CERATOSAURUS_SPAWN_EGG.get()); + output.accept(ModItems.CHASMOSAURUS_SPAWN_EGG.get()); + output.accept(ModItems.COELOPHYSIS_SPAWN_EGG.get()); + output.accept(ModItems.COELURUS_SPAWN_EGG.get()); + output.accept(ModItems.COMPSOGNATHUS_SPAWN_EGG.get()); + output.accept(ModItems.CONCAVENATOR_SPAWN_EGG.get()); + output.accept(ModItems.CORYTHOSAURUS_SPAWN_EGG.get()); + output.accept(ModItems.DEINONYCHUS_SPAWN_EGG.get()); + output.accept(ModItems.DILOPHOSAURUS_SPAWN_EGG.get()); + output.accept(ModItems.DIMORPHODON_SPAWN_EGG.get()); + output.accept(ModItems.DIPLODOCUS_SPAWN_EGG.get()); + output.accept(ModItems.DISTORTUS_REX_SPAWN_EGG.get()); + output.accept(ModItems.DRYOSAURUS_SPAWN_EGG.get()); + output.accept(ModItems.EDMONTOSAURUS_SPAWN_EGG.get()); + output.accept(ModItems.GALLIMIMUS_SPAWN_EGG.get()); + output.accept(ModItems.GEOSTERNBERGIA_SPAWN_EGG.get()); + output.accept(ModItems.GIGANOTOSAURUS_SPAWN_EGG.get()); + output.accept(ModItems.GUANLONG_SPAWN_EGG.get()); + output.accept(ModItems.GUIDRACO_SPAWN_EGG.get()); + output.accept(ModItems.HADROSAURUS_SPAWN_EGG.get()); + output.accept(ModItems.HERRERASAURUS_SPAWN_EGG.get()); + output.accept(ModItems.HYPSILOPHODON_SPAWN_EGG.get()); + output.accept(ModItems.INDOMINUS_REX_SPAWN_EGG.get()); + output.accept(ModItems.INDORAPTOR_SPAWN_EGG.get()); + output.accept(ModItems.INOSTRANCEVIA_SPAWN_EGG.get()); + output.accept(ModItems.LAMBEOSAURUS_SPAWN_EGG.get()); + output.accept(ModItems.LUDODACTYLUS_SPAWN_EGG.get()); + output.accept(ModItems.MAJUNGASAURUS_SPAWN_EGG.get()); + output.accept(ModItems.MAMENCHISAURUS_SPAWN_EGG.get()); + output.accept(ModItems.METRIACANTHOSAURUS_SPAWN_EGG.get()); + output.accept(ModItems.MOGANOPTERUS_SPAWN_EGG.get()); + output.accept(ModItems.NYCTOSAURUS_SPAWN_EGG.get()); + output.accept(ModItems.ORNITHOLESTES_SPAWN_EGG.get()); + output.accept(ModItems.ORNITHOMIMUS_SPAWN_EGG.get()); + output.accept(ModItems.OURANOSAURUS_SPAWN_EGG.get()); + output.accept(ModItems.OVIRAPTOR_SPAWN_EGG.get()); + output.accept(ModItems.PACHYCEPHALOSAURUS_SPAWN_EGG.get()); + output.accept(ModItems.PARASAUROLOPHUS_SPAWN_EGG.get()); + output.accept(ModItems.PROCERATOSAURUS_SPAWN_EGG.get()); + output.accept(ModItems.PROCOMPSOGNATHUS_SPAWN_EGG.get()); + output.accept(ModItems.PROTOCERATOPS_SPAWN_EGG.get()); + output.accept(ModItems.PTERANODON_SPAWN_EGG.get()); + output.accept(ModItems.PTERODAUSTRO_SPAWN_EGG.get()); + output.accept(ModItems.QUETZALCOATLUS_SPAWN_EGG.get()); + output.accept(ModItems.RAJASAURUS_SPAWN_EGG.get()); + output.accept(ModItems.RUGOPS_SPAWN_EGG.get()); + output.accept(ModItems.SEGISAURUS_SPAWN_EGG.get()); + output.accept(ModItems.SHANTUNGOSAURUS_SPAWN_EGG.get()); + output.accept(ModItems.SPINOSAURUS_SPAWN_EGG.get()); + output.accept(ModItems.STEGOSAURUS_SPAWN_EGG.get()); + output.accept(ModItems.STYRACOSAURUS_SPAWN_EGG.get()); + output.accept(ModItems.TAPEJARA_SPAWN_EGG.get()); + output.accept(ModItems.THERIZINOSAURUS_SPAWN_EGG.get()); + output.accept(ModItems.TITANOSAURUS_SPAWN_EGG.get()); + output.accept(ModItems.TRICERATOPS_SPAWN_EGG.get()); + output.accept(ModItems.TROODON_SPAWN_EGG.get()); + output.accept(ModItems.TROPEOGNATHUS_SPAWN_EGG.get()); + output.accept(ModItems.TUPUXUARA_SPAWN_EGG.get()); + output.accept(ModItems.TYRANNOSAURUS_REX_SPAWN_EGG.get()); + output.accept(ModItems.UTAHRAPTOR_SPAWN_EGG.get()); + output.accept(ModItems.VELOCIRAPTOR_SPAWN_EGG.get()); + output.accept(ModItems.ZHENYUANOPTERUS_SPAWN_EGG.get()); + }); + })); public static void register() { - // Items must be explicitly added to tabs in 1.20+ - // This usually goes in your Common setup, but you can trigger it here if your - // loader-specific entry points call this method. - TABS.register(); } } \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/item/ModItems.java b/common/src/main/java/net/cmr/jurassicrevived/item/ModItems.java index 77933d6..b9efdf9 100644 --- a/common/src/main/java/net/cmr/jurassicrevived/item/ModItems.java +++ b/common/src/main/java/net/cmr/jurassicrevived/item/ModItems.java @@ -3,19 +3,569 @@ package net.cmr.jurassicrevived.item; import dev.architectury.registry.registries.DeferredRegister; import dev.architectury.registry.registries.RegistrySupplier; import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.entity.ModEntities; +import net.cmr.jurassicrevived.item.custom.CustomGenderedSpawnEggItem; +import net.cmr.jurassicrevived.item.custom.FrogSyringeItem; import net.minecraft.core.registries.Registries; +import net.minecraft.world.food.FoodProperties; import net.minecraft.world.item.Item; +import net.minecraft.world.item.Rarity; public class ModItems { public static final DeferredRegister ITEMS = DeferredRegister.create(Constants.MOD_ID, Registries.ITEM); - // --- Examples --- - public static final RegistrySupplier AMBER_SHARD = ITEMS.register("amber_shard", - () -> new Item(new Item.Properties())); + public static final RegistrySupplier FROG_MATERIAL = ITEMS.register("frog_material", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier FROG_DNA = ITEMS.register("frog_dna", () -> new Item(new Item.Properties().stacksTo(16))); + + public static final RegistrySupplier WRENCH = ITEMS.register("wrench", () -> new Item(new Item.Properties().stacksTo(1))); + //? if >1.20.1 { + /*public static final RegistrySupplier MAC_N_CHEESE = ITEMS.register("mac_n_cheese", () -> new Item(new Item.Properties().food(new FoodProperties.Builder().nutrition(12).saturationModifier(0.6f).build()))); + public static final RegistrySupplier WALNUT_PUMPKIN_PIE = ITEMS.register("walnut_pumpkin_pie", () -> new Item(new Item.Properties().food(new FoodProperties.Builder().nutrition(12).saturationModifier(0.6f).build()))); + public static final RegistrySupplier BANANA_NUT_COOKIE = ITEMS.register("banana_nut_cookie", () -> new Item(new Item.Properties().food(new FoodProperties.Builder().nutrition(12).saturationModifier(0.6f).build()))); + *///?} else { + public static final RegistrySupplier MAC_N_CHEESE = ITEMS.register("mac_n_cheese", () -> new Item(new Item.Properties().food(new FoodProperties.Builder().nutrition(12).saturationMod(0.6f).build()))); + public static final RegistrySupplier WALNUT_PUMPKIN_PIE = ITEMS.register("walnut_pumpkin_pie", () -> new Item(new Item.Properties().food(new FoodProperties.Builder().nutrition(12).saturationMod(0.6f).build()))); + public static final RegistrySupplier BANANA_NUT_COOKIE = ITEMS.register("banana_nut_cookie", () -> new Item(new Item.Properties().food(new FoodProperties.Builder().nutrition(12).saturationMod(0.6f).build()))); + //?} + + public static final RegistrySupplier APATOSAURUS_SPAWN_EGG = ITEMS.register("apatosaurus_spawn_egg", + () -> new CustomGenderedSpawnEggItem(ModEntities.APATOSAURUS, 0x7f7d6f, 0x36373b, new Item.Properties())); + public static final RegistrySupplier ALBERTOSAURUS_SPAWN_EGG = ITEMS.register("albertosaurus_spawn_egg", + () -> new CustomGenderedSpawnEggItem(ModEntities.ALBERTOSAURUS, 0x2b2315, 0x7a442d, new Item.Properties())); + public static final RegistrySupplier BRACHIOSAURUS_SPAWN_EGG = ITEMS.register("brachiosaurus_spawn_egg", + () -> new CustomGenderedSpawnEggItem(ModEntities.BRACHIOSAURUS, 0x95846D, 0x4B4236, new Item.Properties())); + public static final RegistrySupplier CERATOSAURUS_SPAWN_EGG = ITEMS.register("ceratosaurus_spawn_egg", + () -> new CustomGenderedSpawnEggItem(ModEntities.CERATOSAURUS, 0x954846, 0x221F1D, new Item.Properties())); + public static final RegistrySupplier COMPSOGNATHUS_SPAWN_EGG = ITEMS.register("compsognathus_spawn_egg", + () -> new CustomGenderedSpawnEggItem(ModEntities.COMPSOGNATHUS, 0x676D24, 0x373E16, new Item.Properties())); + public static final RegistrySupplier DIPLODOCUS_SPAWN_EGG = ITEMS.register("diplodocus_spawn_egg", + () -> new CustomGenderedSpawnEggItem(ModEntities.DIPLODOCUS, 0xbf9a5e, 0x624d2c, new Item.Properties())); + public static final RegistrySupplier DILOPHOSAURUS_SPAWN_EGG = ITEMS.register("dilophosaurus_spawn_egg", + () -> new CustomGenderedSpawnEggItem(ModEntities.DILOPHOSAURUS, 0x575D32, 0x16191C, new Item.Properties())); + public static final RegistrySupplier FDUCK_SPAWN_EGG = ITEMS.register("fduck_spawn_egg", + () -> new CustomGenderedSpawnEggItem(ModEntities.FDUCK, 0xff8800, 0x421111, new Item.Properties())); + public static final RegistrySupplier GALLIMIMUS_SPAWN_EGG = ITEMS.register("gallimimus_spawn_egg", + () -> new CustomGenderedSpawnEggItem(ModEntities.GALLIMIMUS, 0xAD7341, 0x5C3925, new Item.Properties())); + public static final RegistrySupplier INDOMINUS_REX_SPAWN_EGG = ITEMS.register("indominus_rex_spawn_egg", + () -> new CustomGenderedSpawnEggItem(ModEntities.INDOMINUS_REX, 0x9C9B99, 0x60605F, new Item.Properties())); + public static final RegistrySupplier OURANOSAURUS_SPAWN_EGG = ITEMS.register("ouranosaurus_spawn_egg", + () -> new CustomGenderedSpawnEggItem(ModEntities.OURANOSAURUS, 0x5e6e49, 0x6c511c, new Item.Properties())); + public static final RegistrySupplier PARASAUROLOPHUS_SPAWN_EGG = ITEMS.register("parasaurolophus_spawn_egg", + () -> new CustomGenderedSpawnEggItem(ModEntities.PARASAUROLOPHUS, 0x856836, 0x442911, new Item.Properties())); + public static final RegistrySupplier SPINOSAURUS_SPAWN_EGG = ITEMS.register("spinosaurus_spawn_egg", + () -> new CustomGenderedSpawnEggItem(ModEntities.SPINOSAURUS, 0x685E5A, 0x5D3831, new Item.Properties())); + public static final RegistrySupplier TRICERATOPS_SPAWN_EGG = ITEMS.register("triceratops_spawn_egg", + () -> new CustomGenderedSpawnEggItem(ModEntities.TRICERATOPS, 0x353A30, 0x121212, new Item.Properties())); + public static final RegistrySupplier TYRANNOSAURUS_REX_SPAWN_EGG = ITEMS.register("tyrannosaurus_rex_spawn_egg", + () -> new CustomGenderedSpawnEggItem(ModEntities.TYRANNOSAURUS_REX, 0x4C3C2D, 0x241F1E, new Item.Properties())); + public static final RegistrySupplier VELOCIRAPTOR_SPAWN_EGG = ITEMS.register("velociraptor_spawn_egg", + () -> new CustomGenderedSpawnEggItem(ModEntities.VELOCIRAPTOR, 0x8A5837, 0x45220D, new Item.Properties())); + public static final RegistrySupplier BARYONYX_SPAWN_EGG = ITEMS.register("baryonyx_spawn_egg", + () -> new CustomGenderedSpawnEggItem(ModEntities.BARYONYX, 0x2e5325, 0x7dcf35, new Item.Properties())); + public static final RegistrySupplier CARNOTAURUS_SPAWN_EGG = ITEMS.register("carnotaurus_spawn_egg", + () -> new CustomGenderedSpawnEggItem(ModEntities.CARNOTAURUS, 0xa6996e, 0xc36e60, new Item.Properties())); + public static final RegistrySupplier CONCAVENATOR_SPAWN_EGG = ITEMS.register("concavenator_spawn_egg", + () -> new CustomGenderedSpawnEggItem(ModEntities.CONCAVENATOR, 0xa6a49b, 0x964b22, new Item.Properties())); + public static final RegistrySupplier DEINONYCHUS_SPAWN_EGG = ITEMS.register("deinonychus_spawn_egg", + () -> new CustomGenderedSpawnEggItem(ModEntities.DEINONYCHUS, 0x8d7d51, 0x95c9a2, new Item.Properties())); + public static final RegistrySupplier DISTORTUS_REX_SPAWN_EGG = ITEMS.register("distortus_rex_spawn_egg", + () -> new CustomGenderedSpawnEggItem(ModEntities.DISTORTUS_REX, 0x59422b, 0x33f2e1a, new Item.Properties())); + public static final RegistrySupplier EDMONTOSAURUS_SPAWN_EGG = ITEMS.register("edmontosaurus_spawn_egg", + () -> new CustomGenderedSpawnEggItem(ModEntities.EDMONTOSAURUS, 0xeaa569, 0xbe783e, new Item.Properties())); + public static final RegistrySupplier GIGANOTOSAURUS_SPAWN_EGG = ITEMS.register("giganotosaurus_spawn_egg", + () -> new CustomGenderedSpawnEggItem(ModEntities.GIGANOTOSAURUS, 0x5c483b, 0x2d2b30, new Item.Properties())); + public static final RegistrySupplier GUANLONG_SPAWN_EGG = ITEMS.register("guanlong_spawn_egg", + () -> new CustomGenderedSpawnEggItem(ModEntities.GUANLONG, 0xefe7cb, 0x624e18, new Item.Properties())); + public static final RegistrySupplier HERRERASAURUS_SPAWN_EGG = ITEMS.register("herrerasaurus_spawn_egg", + () -> new CustomGenderedSpawnEggItem(ModEntities.HERRERASAURUS, 0x93211e, 0x987839, new Item.Properties())); + public static final RegistrySupplier MAJUNGASAURUS_SPAWN_EGG = ITEMS.register("majungasaurus_spawn_egg", + () -> new CustomGenderedSpawnEggItem(ModEntities.MAJUNGASAURUS, 0x657774, 0xce8039, new Item.Properties())); + public static final RegistrySupplier PROCOMPSOGNATHUS_SPAWN_EGG = ITEMS.register("procompsognathus_spawn_egg", + () -> new CustomGenderedSpawnEggItem(ModEntities.PROCOMPSOGNATHUS, 0xe3c775, 0x362c18, new Item.Properties())); + public static final RegistrySupplier PROTOCERATOPS_SPAWN_EGG = ITEMS.register("protoceratops_spawn_egg", + () -> new CustomGenderedSpawnEggItem(ModEntities.PROTOCERATOPS, 0xfccdb4, 0xfdc079, new Item.Properties())); + public static final RegistrySupplier ARAMBOURGIANIA_SPAWN_EGG = ITEMS.register("arambourgiania_spawn_egg", + () -> new CustomGenderedSpawnEggItem(ModEntities.ARAMBOURGIANIA, 0xd2c294, 0x95b2c2, new Item.Properties())); + public static final RegistrySupplier CEARADACTYLUS_SPAWN_EGG = ITEMS.register("cearadactylus_spawn_egg", + () -> new CustomGenderedSpawnEggItem(ModEntities.CEARADACTYLUS, 0x68594e, 0x64a0b3, new Item.Properties())); + public static final RegistrySupplier DIMORPHODON_SPAWN_EGG = ITEMS.register("dimorphodon_spawn_egg", + () -> new CustomGenderedSpawnEggItem(ModEntities.DIMORPHODON, 0xb4aba0, 0x674a43, new Item.Properties())); + public static final RegistrySupplier GEOSTERNBERGIA_SPAWN_EGG = ITEMS.register("geosternbergia_spawn_egg", + () -> new CustomGenderedSpawnEggItem(ModEntities.GEOSTERNBERGIA, 0xd8cb8b, 0x3e677f, new Item.Properties())); + public static final RegistrySupplier GUIDRACO_SPAWN_EGG = ITEMS.register("guidraco_spawn_egg", + () -> new CustomGenderedSpawnEggItem(ModEntities.GUIDRACO, 0x19132e, 0x572749, new Item.Properties())); + public static final RegistrySupplier LUDODACTYLUS_SPAWN_EGG = ITEMS.register("ludodactylus_spawn_egg", + () -> new CustomGenderedSpawnEggItem(ModEntities.LUDODACTYLUS, 0x303133, 0x72502b, new Item.Properties())); + public static final RegistrySupplier MOGANOPTERUS_SPAWN_EGG = ITEMS.register("moganopterus_spawn_egg", + () -> new CustomGenderedSpawnEggItem(ModEntities.MOGANOPTERUS, 0xdeb7ab, 0x8d5a47, new Item.Properties())); + public static final RegistrySupplier NYCTOSAURUS_SPAWN_EGG = ITEMS.register("nyctosaurus_spawn_egg", + () -> new CustomGenderedSpawnEggItem(ModEntities.NYCTOSAURUS, 0xf3f1e9, 0x2087b3, new Item.Properties())); + public static final RegistrySupplier PTERANODON_SPAWN_EGG = ITEMS.register("pteranodon_spawn_egg", + () -> new CustomGenderedSpawnEggItem(ModEntities.PTERANODON, 0x4b4541, 0x173d4a, new Item.Properties())); + public static final RegistrySupplier PTERODAUSTRO_SPAWN_EGG = ITEMS.register("pterodaustro_spawn_egg", + () -> new CustomGenderedSpawnEggItem(ModEntities.PTERODAUSTRO, 0x2f2f36, 0xf5d33c, new Item.Properties())); + public static final RegistrySupplier QUETZALCOATLUS_SPAWN_EGG = ITEMS.register("quetzalcoatlus_spawn_egg", + () -> new CustomGenderedSpawnEggItem(ModEntities.QUETZALCOATLUS, 0x1c1b1a, 0xa6a69f, new Item.Properties())); + public static final RegistrySupplier TAPEJARA_SPAWN_EGG = ITEMS.register("tapejara_spawn_egg", + () -> new CustomGenderedSpawnEggItem(ModEntities.TAPEJARA, 0xd8dbe4, 0x710b0c, new Item.Properties())); + public static final RegistrySupplier TROPEOGNATHUS_SPAWN_EGG = ITEMS.register("tropeognathus_spawn_egg", + () -> new CustomGenderedSpawnEggItem(ModEntities.TROPEOGNATHUS, 0x4e6067, 0x483141, new Item.Properties())); + public static final RegistrySupplier TUPUXUARA_SPAWN_EGG = ITEMS.register("tupuxuara_spawn_egg", + () -> new CustomGenderedSpawnEggItem(ModEntities.TUPUXUARA, 0x6b4e40, 0x3a647e, new Item.Properties())); + public static final RegistrySupplier ZHENYUANOPTERUS_SPAWN_EGG = ITEMS.register("zhenyuanopterus_spawn_egg", + () -> new CustomGenderedSpawnEggItem(ModEntities.ZHENYUANOPTERUS, 0x485654, 0xd1cdb6, new Item.Properties())); + public static final RegistrySupplier RUGOPS_SPAWN_EGG = ITEMS.register("rugops_spawn_egg", + () -> new CustomGenderedSpawnEggItem(ModEntities.RUGOPS, 0xd3ecf0, 0xafef5a, new Item.Properties())); + public static final RegistrySupplier SHANTUNGOSAURUS_SPAWN_EGG = ITEMS.register("shantungosaurus_spawn_egg", + () -> new CustomGenderedSpawnEggItem(ModEntities.SHANTUNGOSAURUS, 0x272727, 0xb57942, new Item.Properties())); + public static final RegistrySupplier STEGOSAURUS_SPAWN_EGG = ITEMS.register("stegosaurus_spawn_egg", + () -> new CustomGenderedSpawnEggItem(ModEntities.STEGOSAURUS, 0x6b6e29, 0x441500, new Item.Properties())); + public static final RegistrySupplier STYRACOSAURUS_SPAWN_EGG = ITEMS.register("styracosaurus_spawn_egg", + () -> new CustomGenderedSpawnEggItem(ModEntities.STYRACOSAURUS, 0x813b2b, 0x6a342c, new Item.Properties())); + public static final RegistrySupplier THERIZINOSAURUS_SPAWN_EGG = ITEMS.register("therizinosaurus_spawn_egg", + () -> new CustomGenderedSpawnEggItem(ModEntities.THERIZINOSAURUS, 0x787878, 0x454545, new Item.Properties())); + public static final RegistrySupplier CHICKENOSAURUS_SPAWN_EGG = ITEMS.register("chickenosaurus_spawn_egg", + () -> new CustomGenderedSpawnEggItem(ModEntities.CHICKENOSAURUS, 0x5d3c11, 0x3a2934, new Item.Properties())); + public static final RegistrySupplier ALLOSAURUS_SPAWN_EGG = ITEMS.register("allosaurus_spawn_egg", + () -> new CustomGenderedSpawnEggItem(ModEntities.ALLOSAURUS, 0xc0a086, 0x653333, new Item.Properties())); + public static final RegistrySupplier ALVAREZSAURUS_SPAWN_EGG = ITEMS.register("alvarezsaurus_spawn_egg", + () -> new CustomGenderedSpawnEggItem(ModEntities.ALVAREZSAURUS, 0xa8a8a8, 0xf3962a, new Item.Properties())); + public static final RegistrySupplier ANKYLOSAURUS_SPAWN_EGG = ITEMS.register("ankylosaurus_spawn_egg", + () -> new CustomGenderedSpawnEggItem(ModEntities.ANKYLOSAURUS, 0xaf9f86, 0xa55d52, new Item.Properties())); + public static final RegistrySupplier CARCHARODONTOSAURUS_SPAWN_EGG = ITEMS.register("carcharodontosaurus_spawn_egg", + () -> new CustomGenderedSpawnEggItem(ModEntities.CARCHARODONTOSAURUS, 0x1b1613, 0x9d321d, new Item.Properties())); + public static final RegistrySupplier CHASMOSAURUS_SPAWN_EGG = ITEMS.register("chasmosaurus_spawn_egg", + () -> new CustomGenderedSpawnEggItem(ModEntities.CHASMOSAURUS, 0xbab697, 0x825038, new Item.Properties())); + public static final RegistrySupplier COELOPHYSIS_SPAWN_EGG = ITEMS.register("coelophysis_spawn_egg", + () -> new CustomGenderedSpawnEggItem(ModEntities.COELOPHYSIS, 0x95a248, 0xa55031, new Item.Properties())); + public static final RegistrySupplier COELURUS_SPAWN_EGG = ITEMS.register("coelurus_spawn_egg", + () -> new CustomGenderedSpawnEggItem(ModEntities.COELURUS, 0x9c7219, 0x2d1b06, new Item.Properties())); + public static final RegistrySupplier CORYTHOSAURUS_SPAWN_EGG = ITEMS.register("corythosaurus_spawn_egg", + () -> new CustomGenderedSpawnEggItem(ModEntities.CORYTHOSAURUS, 0xa2926b, 0xe9c451, new Item.Properties())); + public static final RegistrySupplier DRYOSAURUS_SPAWN_EGG = ITEMS.register("dryosaurus_spawn_egg", + () -> new CustomGenderedSpawnEggItem(ModEntities.DRYOSAURUS, 0xb8992b, 0x271c03, new Item.Properties())); + public static final RegistrySupplier HADROSAURUS_SPAWN_EGG = ITEMS.register("hadrosaurus_spawn_egg", + () -> new CustomGenderedSpawnEggItem(ModEntities.HADROSAURUS, 0x95b18f, 0xd2ce55, new Item.Properties())); + public static final RegistrySupplier HYPSILOPHODON_SPAWN_EGG = ITEMS.register("hypsilophodon_spawn_egg", + () -> new CustomGenderedSpawnEggItem(ModEntities.HYPSILOPHODON, 0x1d1e1f, 0x9277f0, new Item.Properties())); + public static final RegistrySupplier INDORAPTOR_SPAWN_EGG = ITEMS.register("indoraptor_spawn_egg", + () -> new CustomGenderedSpawnEggItem(ModEntities.INDORAPTOR, 0x070707, 0xcfac1c, new Item.Properties())); + public static final RegistrySupplier INOSTRANCEVIA_SPAWN_EGG = ITEMS.register("inostrancevia_spawn_egg", + () -> new CustomGenderedSpawnEggItem(ModEntities.INOSTRANCEVIA, 0x6a6b57, 0x484330, new Item.Properties())); + public static final RegistrySupplier LAMBEOSAURUS_SPAWN_EGG = ITEMS.register("lambeosaurus_spawn_egg", + () -> new CustomGenderedSpawnEggItem(ModEntities.LAMBEOSAURUS, 0x6f8765, 0x5d2855, new Item.Properties())); + public static final RegistrySupplier MAMENCHISAURUS_SPAWN_EGG = ITEMS.register("mamenchisaurus_spawn_egg", + () -> new CustomGenderedSpawnEggItem(ModEntities.MAMENCHISAURUS, 0xe1c77a, 0x979d16, new Item.Properties())); + public static final RegistrySupplier METRIACANTHOSAURUS_SPAWN_EGG = ITEMS.register("metriacanthosaurus_spawn_egg", + () -> new CustomGenderedSpawnEggItem(ModEntities.METRIACANTHOSAURUS, 0xb15e1b, 0xe7e92f, new Item.Properties())); + public static final RegistrySupplier ORNITHOLESTES_SPAWN_EGG = ITEMS.register("ornitholestes_spawn_egg", + () -> new CustomGenderedSpawnEggItem(ModEntities.ORNITHOLESTES, 0x7ac7e6, 0x091d07, new Item.Properties())); + public static final RegistrySupplier ORNITHOMIMUS_SPAWN_EGG = ITEMS.register("ornithomimus_spawn_egg", + () -> new CustomGenderedSpawnEggItem(ModEntities.ORNITHOMIMUS, 0x8ea4d3, 0x7ac7e6, new Item.Properties())); + public static final RegistrySupplier OVIRAPTOR_SPAWN_EGG = ITEMS.register("oviraptor_spawn_egg", + () -> new CustomGenderedSpawnEggItem(ModEntities.OVIRAPTOR, 0xddd9c3, 0x6c3545, new Item.Properties())); + public static final RegistrySupplier PACHYCEPHALOSAURUS_SPAWN_EGG = ITEMS.register("pachycephalosaurus_spawn_egg", + () -> new CustomGenderedSpawnEggItem(ModEntities.PACHYCEPHALOSAURUS, 0x8a7e61, 0x495156, new Item.Properties())); + public static final RegistrySupplier PROCERATOSAURUS_SPAWN_EGG = ITEMS.register("proceratosaurus_spawn_egg", + () -> new CustomGenderedSpawnEggItem(ModEntities.PROCERATOSAURUS, 0x8f8e8a, 0x040203, new Item.Properties())); + public static final RegistrySupplier RAJASAURUS_SPAWN_EGG = ITEMS.register("rajasaurus_spawn_egg", + () -> new CustomGenderedSpawnEggItem(ModEntities.RAJASAURUS, 0x3f3a32, 0x62c6ce, new Item.Properties())); + public static final RegistrySupplier SEGISAURUS_SPAWN_EGG = ITEMS.register("segisaurus_spawn_egg", + () -> new CustomGenderedSpawnEggItem(ModEntities.SEGISAURUS, 0x72383a, 0x69abcc, new Item.Properties())); + public static final RegistrySupplier TITANOSAURUS_SPAWN_EGG = ITEMS.register("titanosaurus_spawn_egg", + () -> new CustomGenderedSpawnEggItem(ModEntities.TITANOSAURUS, 0x6f6960, 0xd43d13, new Item.Properties())); + public static final RegistrySupplier TROODON_SPAWN_EGG = ITEMS.register("troodon_spawn_egg", + () -> new CustomGenderedSpawnEggItem(ModEntities.TROODON, 0x414632, 0x640600, new Item.Properties())); + public static final RegistrySupplier UTAHRAPTOR_SPAWN_EGG = ITEMS.register("utahraptor_spawn_egg", + () -> new CustomGenderedSpawnEggItem(ModEntities.UTAHRAPTOR, 0x474131, 0xdad8db, new Item.Properties())); + + + public static final RegistrySupplier TEST_TUBE = ITEMS.register("test_tube", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier SYRINGE = ITEMS.register("syringe", () -> new FrogSyringeItem(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier MOSQUITO_IN_AMBER = ITEMS.register("mosquito_in_amber", () -> new Item(new Item.Properties().stacksTo(16).rarity(Rarity.UNCOMMON))); + public static final RegistrySupplier CRUSHED_FOSSIL = ITEMS.register("crushed_fossil", () -> new Item(new Item.Properties())); + public static final RegistrySupplier FROZEN_LEECH = ITEMS.register("frozen_leech", () -> new Item(new Item.Properties().stacksTo(16).rarity(Rarity.RARE))); + public static final RegistrySupplier CABLE = ITEMS.register("cable", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier SCREEN = ITEMS.register("screen", () -> new Item(new Item.Properties().stacksTo(8))); + public static final RegistrySupplier PROCESSOR = ITEMS.register("processor", () -> new Item(new Item.Properties().stacksTo(8))); + public static final RegistrySupplier TIRE = ITEMS.register("tire", () -> new Item(new Item.Properties().stacksTo(4))); + public static final RegistrySupplier CUTTING_BLADES = ITEMS.register("cutting_blades", () -> new Item(new Item.Properties().stacksTo(16))); + + public static final RegistrySupplier VELOCIRAPTOR_SKULL_FOSSIL = ITEMS.register("velociraptor_skull_fossil", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier TYRANNOSAURUS_REX_SKULL_FOSSIL = ITEMS.register("tyrannosaurus_rex_skull_fossil", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier TRICERATOPS_SKULL_FOSSIL = ITEMS.register("triceratops_skull_fossil", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier SPINOSAURUS_SKULL_FOSSIL = ITEMS.register("spinosaurus_skull_fossil", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier PARASAUROLOPHUS_SKULL_FOSSIL = ITEMS.register("parasaurolophus_skull_fossil", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier OURANOSAURUS_SKULL_FOSSIL = ITEMS.register("ouranosaurus_skull_fossil", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier GALLIMIMUS_SKULL_FOSSIL = ITEMS.register("gallimimus_skull_fossil", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier DIPLODOCUS_SKULL_FOSSIL = ITEMS.register("diplodocus_skull_fossil", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier DILOPHOSAURUS_SKULL_FOSSIL = ITEMS.register("dilophosaurus_skull_fossil", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier COMPSOGNATHUS_SKULL_FOSSIL = ITEMS.register("compsognathus_skull_fossil", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier CERATOSAURUS_SKULL_FOSSIL = ITEMS.register("ceratosaurus_skull_fossil", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier BRACHIOSAURUS_SKULL_FOSSIL = ITEMS.register("brachiosaurus_skull_fossil", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier ALBERTOSAURUS_SKULL_FOSSIL = ITEMS.register("albertosaurus_skull_fossil", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier APATOSAURUS_SKULL_FOSSIL = ITEMS.register("apatosaurus_skull_fossil", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier BARYONYX_SKULL_FOSSIL = ITEMS.register("baryonyx_skull_fossil", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier CARNOTAURUS_SKULL_FOSSIL = ITEMS.register("carnotaurus_skull_fossil", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier CONCAVENATOR_SKULL_FOSSIL = ITEMS.register("concavenator_skull_fossil", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier DEINONYCHUS_SKULL_FOSSIL = ITEMS.register("deinonychus_skull_fossil", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier EDMONTOSAURUS_SKULL_FOSSIL = ITEMS.register("edmontosaurus_skull_fossil", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier GIGANOTOSAURUS_SKULL_FOSSIL = ITEMS.register("giganotosaurus_skull_fossil", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier GUANLONG_SKULL_FOSSIL = ITEMS.register("guanlong_skull_fossil", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier HERRERASAURUS_SKULL_FOSSIL = ITEMS.register("herrerasaurus_skull_fossil", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier MAJUNGASAURUS_SKULL_FOSSIL = ITEMS.register("majungasaurus_skull_fossil", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier PROCOMPSOGNATHUS_SKULL_FOSSIL = ITEMS.register("procompsognathus_skull_fossil", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier PROTOCERATOPS_SKULL_FOSSIL = ITEMS.register("protoceratops_skull_fossil", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier RUGOPS_SKULL_FOSSIL = ITEMS.register("rugops_skull_fossil", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier SHANTUNGOSAURUS_SKULL_FOSSIL = ITEMS.register("shantungosaurus_skull_fossil", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier STEGOSAURUS_SKULL_FOSSIL = ITEMS.register("stegosaurus_skull_fossil", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier STYRACOSAURUS_SKULL_FOSSIL = ITEMS.register("styracosaurus_skull_fossil", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier THERIZINOSAURUS_SKULL_FOSSIL = ITEMS.register("therizinosaurus_skull_fossil", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier ALLOSAURUS_SKULL_FOSSIL = ITEMS.register("allosaurus_skull_fossil", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier ALVAREZSAURUS_SKULL_FOSSIL = ITEMS.register("alvarezsaurus_skull_fossil", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier ANKYLOSAURUS_SKULL_FOSSIL = ITEMS.register("ankylosaurus_skull_fossil", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier ARAMBOURGIANIA_SKULL_FOSSIL = ITEMS.register("arambourgiania_skull_fossil", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier CARCHARODONTOSAURUS_SKULL_FOSSIL = ITEMS.register("carcharodontosaurus_skull_fossil", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier CEARADACTYLUS_SKULL_FOSSIL = ITEMS.register("cearadactylus_skull_fossil", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier CHASMOSAURUS_SKULL_FOSSIL = ITEMS.register("chasmosaurus_skull_fossil", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier COELOPHYSIS_SKULL_FOSSIL = ITEMS.register("coelophysis_skull_fossil", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier COELURUS_SKULL_FOSSIL = ITEMS.register("coelurus_skull_fossil", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier CORYTHOSAURUS_SKULL_FOSSIL = ITEMS.register("corythosaurus_skull_fossil", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier DIMORPHODON_SKULL_FOSSIL = ITEMS.register("dimorphodon_skull_fossil", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier DRYOSAURUS_SKULL_FOSSIL = ITEMS.register("dryosaurus_skull_fossil", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier GEOSTERNBERGIA_SKULL_FOSSIL = ITEMS.register("geosternbergia_skull_fossil", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier GUIDRACO_SKULL_FOSSIL = ITEMS.register("guidraco_skull_fossil", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier HADROSAURUS_SKULL_FOSSIL = ITEMS.register("hadrosaurus_skull_fossil", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier HYPSILOPHODON_SKULL_FOSSIL = ITEMS.register("hypsilophodon_skull_fossil", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier INOSTRANCEVIA_SKULL_FOSSIL = ITEMS.register("inostrancevia_skull_fossil", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier LAMBEOSAURUS_SKULL_FOSSIL = ITEMS.register("lambeosaurus_skull_fossil", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier LUDODACTYLUS_SKULL_FOSSIL = ITEMS.register("ludodactylus_skull_fossil", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier MAMENCHISAURUS_SKULL_FOSSIL = ITEMS.register("mamenchisaurus_skull_fossil", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier METRIACANTHOSAURUS_SKULL_FOSSIL = ITEMS.register("metriacanthosaurus_skull_fossil", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier MOGANOPTERUS_SKULL_FOSSIL = ITEMS.register("moganopterus_skull_fossil", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier NYCTOSAURUS_SKULL_FOSSIL = ITEMS.register("nyctosaurus_skull_fossil", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier ORNITHOLESTES_SKULL_FOSSIL = ITEMS.register("ornitholestes_skull_fossil", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier ORNITHOMIMUS_SKULL_FOSSIL = ITEMS.register("ornithomimus_skull_fossil", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier OVIRAPTOR_SKULL_FOSSIL = ITEMS.register("oviraptor_skull_fossil", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier PACHYCEPHALOSAURUS_SKULL_FOSSIL = ITEMS.register("pachycephalosaurus_skull_fossil", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier PROCERATOSAURUS_SKULL_FOSSIL = ITEMS.register("proceratosaurus_skull_fossil", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier PTERANODON_SKULL_FOSSIL = ITEMS.register("pteranodon_skull_fossil", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier PTERODAUSTRO_SKULL_FOSSIL = ITEMS.register("pterodaustro_skull_fossil", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier QUETZALCOATLUS_SKULL_FOSSIL = ITEMS.register("quetzalcoatlus_skull_fossil", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier RAJASAURUS_SKULL_FOSSIL = ITEMS.register("rajasaurus_skull_fossil", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier SEGISAURUS_SKULL_FOSSIL = ITEMS.register("segisaurus_skull_fossil", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier TAPEJARA_SKULL_FOSSIL = ITEMS.register("tapejara_skull_fossil", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier TITANOSAURUS_SKULL_FOSSIL = ITEMS.register("titanosaurus_skull_fossil", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier TROODON_SKULL_FOSSIL = ITEMS.register("troodon_skull_fossil", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier TROPEOGNATHUS_SKULL_FOSSIL = ITEMS.register("tropeognathus_skull_fossil", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier TUPUXUARA_SKULL_FOSSIL = ITEMS.register("tupuxuara_skull_fossil", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier UTAHRAPTOR_SKULL_FOSSIL = ITEMS.register("utahraptor_skull_fossil", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier ZHENYUANOPTERUS_SKULL_FOSSIL = ITEMS.register("zhenyuanopterus_skull_fossil", () -> new Item(new Item.Properties().stacksTo(16))); + + + public static final RegistrySupplier FRESH_VELOCIRAPTOR_SKULL = ITEMS.register("fresh_velociraptor_skull", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier FRESH_TYRANNOSAURUS_REX_SKULL = ITEMS.register("fresh_tyrannosaurus_rex_skull", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier FRESH_TRICERATOPS_SKULL = ITEMS.register("fresh_triceratops_skull", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier FRESH_SPINOSAURUS_SKULL = ITEMS.register("fresh_spinosaurus_skull", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier FRESH_PARASAUROLOPHUS_SKULL = ITEMS.register("fresh_parasaurolophus_skull", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier FRESH_OURANOSAURUS_SKULL = ITEMS.register("fresh_ouranosaurus_skull", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier FRESH_INDOMINUS_REX_SKULL = ITEMS.register("fresh_indominus_rex_skull", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier FRESH_GALLIMIMUS_SKULL = ITEMS.register("fresh_gallimimus_skull", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier FRESH_DIPLODOCUS_SKULL = ITEMS.register("fresh_diplodocus_skull", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier FRESH_DILOPHOSAURUS_SKULL = ITEMS.register("fresh_dilophosaurus_skull", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier FRESH_COMPSOGNATHUS_SKULL = ITEMS.register("fresh_compsognathus_skull", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier FRESH_CERATOSAURUS_SKULL = ITEMS.register("fresh_ceratosaurus_skull", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier FRESH_BRACHIOSAURUS_SKULL = ITEMS.register("fresh_brachiosaurus_skull", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier FRESH_ALBERTOSAURUS_SKULL = ITEMS.register("fresh_albertosaurus_skull", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier FRESH_APATOSAURUS_SKULL = ITEMS.register("fresh_apatosaurus_skull", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier FRESH_BARYONYX_SKULL = ITEMS.register("fresh_baryonyx_skull", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier FRESH_CARNOTAURUS_SKULL = ITEMS.register("fresh_carnotaurus_skull", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier FRESH_CONCAVENATOR_SKULL = ITEMS.register("fresh_concavenator_skull", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier FRESH_DEINONYCHUS_SKULL = ITEMS.register("fresh_deinonychus_skull", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier FRESH_EDMONTOSAURUS_SKULL = ITEMS.register("fresh_edmontosaurus_skull", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier FRESH_GIGANOTOSAURUS_SKULL = ITEMS.register("fresh_giganotosaurus_skull", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier FRESH_GUANLONG_SKULL = ITEMS.register("fresh_guanlong_skull", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier FRESH_HERRERASAURUS_SKULL = ITEMS.register("fresh_herrerasaurus_skull", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier FRESH_MAJUNGASAURUS_SKULL = ITEMS.register("fresh_majungasaurus_skull", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier FRESH_PROTOCERATOPS_SKULL = ITEMS.register("fresh_protoceratops_skull", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier FRESH_PROCOMPSOGNATHUS_SKULL = ITEMS.register("fresh_procompsognathus_skull", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier FRESH_RUGOPS_SKULL = ITEMS.register("fresh_rugops_skull", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier FRESH_SHANTUNGOSAURUS_SKULL = ITEMS.register("fresh_shantungosaurus_skull", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier FRESH_STEGOSAURUS_SKULL = ITEMS.register("fresh_stegosaurus_skull", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier FRESH_STYRACOSAURUS_SKULL = ITEMS.register("fresh_styracosaurus_skull", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier FRESH_THERIZINOSAURUS_SKULL = ITEMS.register("fresh_therizinosaurus_skull", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier FRESH_DISTORTUS_REX_SKULL = ITEMS.register("fresh_distortus_rex_skull", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier FRESH_ALLOSAURUS_SKULL = ITEMS.register("fresh_allosaurus_skull", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier FRESH_ALVAREZSAURUS_SKULL = ITEMS.register("fresh_alvarezsaurus_skull", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier FRESH_ANKYLOSAURUS_SKULL = ITEMS.register("fresh_ankylosaurus_skull", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier FRESH_ARAMBOURGIANIA_SKULL = ITEMS.register("fresh_arambourgiania_skull", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier FRESH_CARCHARODONTOSAURUS_SKULL = ITEMS.register("fresh_carcharodontosaurus_skull", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier FRESH_CEARADACTYLUS_SKULL = ITEMS.register("fresh_cearadactylus_skull", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier FRESH_CHASMOSAURUS_SKULL = ITEMS.register("fresh_chasmosaurus_skull", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier FRESH_COELOPHYSIS_SKULL = ITEMS.register("fresh_coelophysis_skull", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier FRESH_COELURUS_SKULL = ITEMS.register("fresh_coelurus_skull", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier FRESH_CORYTHOSAURUS_SKULL = ITEMS.register("fresh_corythosaurus_skull", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier FRESH_DIMORPHODON_SKULL = ITEMS.register("fresh_dimorphodon_skull", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier FRESH_DRYOSAURUS_SKULL = ITEMS.register("fresh_dryosaurus_skull", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier FRESH_GEOSTERNBERGIA_SKULL = ITEMS.register("fresh_geosternbergia_skull", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier FRESH_GUIDRACO_SKULL = ITEMS.register("fresh_guidraco_skull", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier FRESH_HADROSAURUS_SKULL = ITEMS.register("fresh_hadrosaurus_skull", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier FRESH_HYPSILOPHODON_SKULL = ITEMS.register("fresh_hypsilophodon_skull", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier FRESH_INDORAPTOR_SKULL = ITEMS.register("fresh_indoraptor_skull", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier FRESH_INOSTRANCEVIA_SKULL = ITEMS.register("fresh_inostrancevia_skull", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier FRESH_LAMBEOSAURUS_SKULL = ITEMS.register("fresh_lambeosaurus_skull", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier FRESH_LUDODACTYLUS_SKULL = ITEMS.register("fresh_ludodactylus_skull", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier FRESH_MAMENCHISAURUS_SKULL = ITEMS.register("fresh_mamenchisaurus_skull", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier FRESH_METRIACANTHOSAURUS_SKULL = ITEMS.register("fresh_metriacanthosaurus_skull", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier FRESH_MOGANOPTERUS_SKULL = ITEMS.register("fresh_moganopterus_skull", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier FRESH_NYCTOSAURUS_SKULL = ITEMS.register("fresh_nyctosaurus_skull", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier FRESH_ORNITHOLESTES_SKULL = ITEMS.register("fresh_ornitholestes_skull", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier FRESH_ORNITHOMIMUS_SKULL = ITEMS.register("fresh_ornithomimus_skull", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier FRESH_OVIRAPTOR_SKULL = ITEMS.register("fresh_oviraptor_skull", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier FRESH_PACHYCEPHALOSAURUS_SKULL = ITEMS.register("fresh_pachycephalosaurus_skull", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier FRESH_PROCERATOSAURUS_SKULL = ITEMS.register("fresh_proceratosaurus_skull", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier FRESH_PTERANODON_SKULL = ITEMS.register("fresh_pteranodon_skull", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier FRESH_PTERODAUSTRO_SKULL = ITEMS.register("fresh_pterodaustro_skull", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier FRESH_QUETZALCOATLUS_SKULL = ITEMS.register("fresh_quetzalcoatlus_skull", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier FRESH_RAJASAURUS_SKULL = ITEMS.register("fresh_rajasaurus_skull", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier FRESH_SEGISAURUS_SKULL = ITEMS.register("fresh_segisaurus_skull", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier FRESH_TAPEJARA_SKULL = ITEMS.register("fresh_tapejara_skull", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier FRESH_TITANOSAURUS_SKULL = ITEMS.register("fresh_titanosaurus_skull", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier FRESH_TROODON_SKULL = ITEMS.register("fresh_troodon_skull", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier FRESH_TROPEOGNATHUS_SKULL = ITEMS.register("fresh_tropeognathus_skull", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier FRESH_TUPUXUARA_SKULL = ITEMS.register("fresh_tupuxuara_skull", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier FRESH_UTAHRAPTOR_SKULL = ITEMS.register("fresh_utahraptor_skull", () -> new Item(new Item.Properties().stacksTo(16))); + public static final RegistrySupplier FRESH_ZHENYUANOPTERUS_SKULL = ITEMS.register("fresh_zhenyuanopterus_skull", () -> new Item(new Item.Properties().stacksTo(16))); + + + // Tissue group + public static final RegistrySupplier VELOCIRAPTOR_TISSUE = ITEMS.register("velociraptor_tissue", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.EPIC))); + public static final RegistrySupplier TYRANNOSAURUS_REX_TISSUE = ITEMS.register("tyrannosaurus_rex_tissue", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.EPIC))); + public static final RegistrySupplier TRICERATOPS_TISSUE = ITEMS.register("triceratops_tissue", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.EPIC))); + public static final RegistrySupplier SPINOSAURUS_TISSUE = ITEMS.register("spinosaurus_tissue", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.EPIC))); + public static final RegistrySupplier PARASAUROLOPHUS_TISSUE = ITEMS.register("parasaurolophus_tissue", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.EPIC))); + public static final RegistrySupplier OURANOSAURUS_TISSUE = ITEMS.register("ouranosaurus_tissue", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.EPIC))); + public static final RegistrySupplier INDOMINUS_REX_TISSUE = ITEMS.register("indominus_rex_tissue", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.EPIC))); + public static final RegistrySupplier GALLIMIMUS_TISSUE = ITEMS.register("gallimimus_tissue", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.EPIC))); + public static final RegistrySupplier DIPLODOCUS_TISSUE = ITEMS.register("diplodocus_tissue", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.EPIC))); + public static final RegistrySupplier DILOPHOSAURUS_TISSUE = ITEMS.register("dilophosaurus_tissue", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.EPIC))); + public static final RegistrySupplier COMPSOGNATHUS_TISSUE = ITEMS.register("compsognathus_tissue", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.EPIC))); + public static final RegistrySupplier CERATOSAURUS_TISSUE = ITEMS.register("ceratosaurus_tissue", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.EPIC))); + public static final RegistrySupplier BRACHIOSAURUS_TISSUE = ITEMS.register("brachiosaurus_tissue", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.EPIC))); + public static final RegistrySupplier ALBERTOSAURUS_TISSUE = ITEMS.register("albertosaurus_tissue", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.EPIC))); + public static final RegistrySupplier APATOSAURUS_TISSUE = ITEMS.register("apatosaurus_tissue", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.EPIC))); + public static final RegistrySupplier BARYONYX_TISSUE = ITEMS.register("baryonyx_tissue", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.EPIC))); + public static final RegistrySupplier CARNOTAURUS_TISSUE = ITEMS.register("carnotaurus_tissue", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.EPIC))); + public static final RegistrySupplier CONCAVENATOR_TISSUE = ITEMS.register("concavenator_tissue", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.EPIC))); + public static final RegistrySupplier DEINONYCHUS_TISSUE = ITEMS.register("deinonychus_tissue", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.EPIC))); + public static final RegistrySupplier EDMONTOSAURUS_TISSUE = ITEMS.register("edmontosaurus_tissue", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.EPIC))); + public static final RegistrySupplier GIGANOTOSAURUS_TISSUE = ITEMS.register("giganotosaurus_tissue", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.EPIC))); + public static final RegistrySupplier GUANLONG_TISSUE = ITEMS.register("guanlong_tissue", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.EPIC))); + public static final RegistrySupplier HERRERASAURUS_TISSUE = ITEMS.register("herrerasaurus_tissue", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.EPIC))); + public static final RegistrySupplier MAJUNGASAURUS_TISSUE = ITEMS.register("majungasaurus_tissue", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.EPIC))); + public static final RegistrySupplier PROCOMPSOGNATHUS_TISSUE = ITEMS.register("procompsognathus_tissue", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.EPIC))); + public static final RegistrySupplier PROTOCERATOPS_TISSUE = ITEMS.register("protoceratops_tissue", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.EPIC))); + public static final RegistrySupplier RUGOPS_TISSUE = ITEMS.register("rugops_tissue", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.EPIC))); + public static final RegistrySupplier SHANTUNGOSAURUS_TISSUE = ITEMS.register("shantungosaurus_tissue", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.EPIC))); + public static final RegistrySupplier STEGOSAURUS_TISSUE = ITEMS.register("stegosaurus_tissue", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.EPIC))); + public static final RegistrySupplier STYRACOSAURUS_TISSUE = ITEMS.register("styracosaurus_tissue", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.EPIC))); + public static final RegistrySupplier THERIZINOSAURUS_TISSUE = ITEMS.register("therizinosaurus_tissue", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.EPIC))); + public static final RegistrySupplier DISTORTUS_REX_TISSUE = ITEMS.register("distortus_rex_tissue", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.EPIC))); + public static final RegistrySupplier ALLOSAURUS_TISSUE = ITEMS.register("allosaurus_tissue", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.EPIC))); + public static final RegistrySupplier ALVAREZSAURUS_TISSUE = ITEMS.register("alvarezsaurus_tissue", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.EPIC))); + public static final RegistrySupplier ANKYLOSAURUS_TISSUE = ITEMS.register("ankylosaurus_tissue", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.EPIC))); + public static final RegistrySupplier ARAMBOURGIANIA_TISSUE = ITEMS.register("arambourgiania_tissue", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.EPIC))); + public static final RegistrySupplier CARCHARODONTOSAURUS_TISSUE = ITEMS.register("carcharodontosaurus_tissue", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.EPIC))); + public static final RegistrySupplier CEARADACTYLUS_TISSUE = ITEMS.register("cearadactylus_tissue", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.EPIC))); + public static final RegistrySupplier CHASMOSAURUS_TISSUE = ITEMS.register("chasmosaurus_tissue", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.EPIC))); + public static final RegistrySupplier COELOPHYSIS_TISSUE = ITEMS.register("coelophysis_tissue", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.EPIC))); + public static final RegistrySupplier COELURUS_TISSUE = ITEMS.register("coelurus_tissue", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.EPIC))); + public static final RegistrySupplier CORYTHOSAURUS_TISSUE = ITEMS.register("corythosaurus_tissue", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.EPIC))); + public static final RegistrySupplier DIMORPHODON_TISSUE = ITEMS.register("dimorphodon_tissue", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.EPIC))); + public static final RegistrySupplier DRYOSAURUS_TISSUE = ITEMS.register("dryosaurus_tissue", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.EPIC))); + public static final RegistrySupplier GEOSTERNBERGIA_TISSUE = ITEMS.register("geosternbergia_tissue", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.EPIC))); + public static final RegistrySupplier GUIDRACO_TISSUE = ITEMS.register("guidraco_tissue", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.EPIC))); + public static final RegistrySupplier HADROSAURUS_TISSUE = ITEMS.register("hadrosaurus_tissue", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.EPIC))); + public static final RegistrySupplier HYPSILOPHODON_TISSUE = ITEMS.register("hypsilophodon_tissue", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.EPIC))); + public static final RegistrySupplier INDORAPTOR_TISSUE = ITEMS.register("indoraptor_tissue", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.EPIC))); + public static final RegistrySupplier INOSTRANCEVIA_TISSUE = ITEMS.register("inostrancevia_tissue", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.EPIC))); + public static final RegistrySupplier LAMBEOSAURUS_TISSUE = ITEMS.register("lambeosaurus_tissue", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.EPIC))); + public static final RegistrySupplier LUDODACTYLUS_TISSUE = ITEMS.register("ludodactylus_tissue", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.EPIC))); + public static final RegistrySupplier MAMENCHISAURUS_TISSUE = ITEMS.register("mamenchisaurus_tissue", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.EPIC))); + public static final RegistrySupplier METRIACANTHOSAURUS_TISSUE = ITEMS.register("metriacanthosaurus_tissue", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.EPIC))); + public static final RegistrySupplier MOGANOPTERUS_TISSUE = ITEMS.register("moganopterus_tissue", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.EPIC))); + public static final RegistrySupplier NYCTOSAURUS_TISSUE = ITEMS.register("nyctosaurus_tissue", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.EPIC))); + public static final RegistrySupplier ORNITHOLESTES_TISSUE = ITEMS.register("ornitholestes_tissue", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.EPIC))); + public static final RegistrySupplier ORNITHOMIMUS_TISSUE = ITEMS.register("ornithomimus_tissue", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.EPIC))); + public static final RegistrySupplier OVIRAPTOR_TISSUE = ITEMS.register("oviraptor_tissue", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.EPIC))); + public static final RegistrySupplier PACHYCEPHALOSAURUS_TISSUE = ITEMS.register("pachycephalosaurus_tissue", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.EPIC))); + public static final RegistrySupplier PROCERATOSAURUS_TISSUE = ITEMS.register("proceratosaurus_tissue", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.EPIC))); + public static final RegistrySupplier PTERANODON_TISSUE = ITEMS.register("pteranodon_tissue", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.EPIC))); + public static final RegistrySupplier PTERODAUSTRO_TISSUE = ITEMS.register("pterodaustro_tissue", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.EPIC))); + public static final RegistrySupplier QUETZALCOATLUS_TISSUE = ITEMS.register("quetzalcoatlus_tissue", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.EPIC))); + public static final RegistrySupplier RAJASAURUS_TISSUE = ITEMS.register("rajasaurus_tissue", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.EPIC))); + public static final RegistrySupplier SEGISAURUS_TISSUE = ITEMS.register("segisaurus_tissue", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.EPIC))); + public static final RegistrySupplier TAPEJARA_TISSUE = ITEMS.register("tapejara_tissue", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.EPIC))); + public static final RegistrySupplier TITANOSAURUS_TISSUE = ITEMS.register("titanosaurus_tissue", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.EPIC))); + public static final RegistrySupplier TROODON_TISSUE = ITEMS.register("troodon_tissue", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.EPIC))); + public static final RegistrySupplier TROPEOGNATHUS_TISSUE = ITEMS.register("tropeognathus_tissue", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.EPIC))); + public static final RegistrySupplier TUPUXUARA_TISSUE = ITEMS.register("tupuxuara_tissue", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.EPIC))); + public static final RegistrySupplier UTAHRAPTOR_TISSUE = ITEMS.register("utahraptor_tissue", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.EPIC))); + public static final RegistrySupplier ZHENYUANOPTERUS_TISSUE = ITEMS.register("zhenyuanopterus_tissue", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.EPIC))); + + + // DNA group + public static final RegistrySupplier VELOCIRAPTOR_DNA = ITEMS.register("velociraptor_dna", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.RARE))); + public static final RegistrySupplier TYRANNOSAURUS_REX_DNA = ITEMS.register("tyrannosaurus_rex_dna", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.RARE))); + public static final RegistrySupplier TRICERATOPS_DNA = ITEMS.register("triceratops_dna", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.RARE))); + public static final RegistrySupplier SPINOSAURUS_DNA = ITEMS.register("spinosaurus_dna", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.RARE))); + public static final RegistrySupplier PARASAUROLOPHUS_DNA = ITEMS.register("parasaurolophus_dna", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.RARE))); + public static final RegistrySupplier OURANOSAURUS_DNA = ITEMS.register("ouranosaurus_dna", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.RARE))); + public static final RegistrySupplier INDOMINUS_REX_DNA = ITEMS.register("indominus_rex_dna", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.RARE))); + public static final RegistrySupplier GALLIMIMUS_DNA = ITEMS.register("gallimimus_dna", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.RARE))); + public static final RegistrySupplier DIPLODOCUS_DNA = ITEMS.register("diplodocus_dna", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.RARE))); + public static final RegistrySupplier DILOPHOSAURUS_DNA = ITEMS.register("dilophosaurus_dna", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.RARE))); + public static final RegistrySupplier COMPSOGNATHUS_DNA = ITEMS.register("compsognathus_dna", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.RARE))); + public static final RegistrySupplier CERATOSAURUS_DNA = ITEMS.register("ceratosaurus_dna", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.RARE))); + public static final RegistrySupplier BRACHIOSAURUS_DNA = ITEMS.register("brachiosaurus_dna", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.RARE))); + public static final RegistrySupplier ALBERTOSAURUS_DNA = ITEMS.register("albertosaurus_dna", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.RARE))); + public static final RegistrySupplier APATOSAURUS_DNA = ITEMS.register("apatosaurus_dna", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.RARE))); + public static final RegistrySupplier BARYONYX_DNA = ITEMS.register("baryonyx_dna", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.RARE))); + public static final RegistrySupplier CARNOTAURUS_DNA = ITEMS.register("carnotaurus_dna", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.RARE))); + public static final RegistrySupplier CONCAVENATOR_DNA = ITEMS.register("concavenator_dna", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.RARE))); + public static final RegistrySupplier DEINONYCHUS_DNA = ITEMS.register("deinonychus_dna", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.RARE))); + public static final RegistrySupplier EDMONTOSAURUS_DNA = ITEMS.register("edmontosaurus_dna", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.RARE))); + public static final RegistrySupplier GIGANOTOSAURUS_DNA = ITEMS.register("giganotosaurus_dna", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.RARE))); + public static final RegistrySupplier GUANLONG_DNA = ITEMS.register("guanlong_dna", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.RARE))); + public static final RegistrySupplier HERRERASAURUS_DNA = ITEMS.register("herrerasaurus_dna", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.RARE))); + public static final RegistrySupplier MAJUNGASAURUS_DNA = ITEMS.register("majungasaurus_dna", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.RARE))); + public static final RegistrySupplier PROCOMPSOGNATHUS_DNA = ITEMS.register("procompsognathus_dna", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.RARE))); + public static final RegistrySupplier PROTOCERATOPS_DNA = ITEMS.register("protoceratops_dna", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.RARE))); + public static final RegistrySupplier RUGOPS_DNA = ITEMS.register("rugops_dna", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.RARE))); + public static final RegistrySupplier SHANTUNGOSAURUS_DNA = ITEMS.register("shantungosaurus_dna", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.RARE))); + public static final RegistrySupplier STEGOSAURUS_DNA = ITEMS.register("stegosaurus_dna", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.RARE))); + public static final RegistrySupplier STYRACOSAURUS_DNA = ITEMS.register("styracosaurus_dna", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.RARE))); + public static final RegistrySupplier THERIZINOSAURUS_DNA = ITEMS.register("therizinosaurus_dna", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.RARE))); + public static final RegistrySupplier DISTORTUS_REX_DNA = ITEMS.register("distortus_rex_dna", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.RARE))); + public static final RegistrySupplier ALLOSAURUS_DNA = ITEMS.register("allosaurus_dna", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.RARE))); + public static final RegistrySupplier ALVAREZSAURUS_DNA = ITEMS.register("alvarezsaurus_dna", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.RARE))); + public static final RegistrySupplier ANKYLOSAURUS_DNA = ITEMS.register("ankylosaurus_dna", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.RARE))); + public static final RegistrySupplier ARAMBOURGIANIA_DNA = ITEMS.register("arambourgiania_dna", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.RARE))); + public static final RegistrySupplier CARCHARODONTOSAURUS_DNA = ITEMS.register("carcharodontosaurus_dna", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.RARE))); + public static final RegistrySupplier CEARADACTYLUS_DNA = ITEMS.register("cearadactylus_dna", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.RARE))); + public static final RegistrySupplier CHASMOSAURUS_DNA = ITEMS.register("chasmosaurus_dna", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.RARE))); + public static final RegistrySupplier COELOPHYSIS_DNA = ITEMS.register("coelophysis_dna", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.RARE))); + public static final RegistrySupplier COELURUS_DNA = ITEMS.register("coelurus_dna", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.RARE))); + public static final RegistrySupplier CORYTHOSAURUS_DNA = ITEMS.register("corythosaurus_dna", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.RARE))); + public static final RegistrySupplier DIMORPHODON_DNA = ITEMS.register("dimorphodon_dna", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.RARE))); + public static final RegistrySupplier DRYOSAURUS_DNA = ITEMS.register("dryosaurus_dna", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.RARE))); + public static final RegistrySupplier GEOSTERNBERGIA_DNA = ITEMS.register("geosternbergia_dna", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.RARE))); + public static final RegistrySupplier GUIDRACO_DNA = ITEMS.register("guidraco_dna", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.RARE))); + public static final RegistrySupplier HADROSAURUS_DNA = ITEMS.register("hadrosaurus_dna", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.RARE))); + public static final RegistrySupplier HYPSILOPHODON_DNA = ITEMS.register("hypsilophodon_dna", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.RARE))); + public static final RegistrySupplier INDORAPTOR_DNA = ITEMS.register("indoraptor_dna", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.RARE))); + public static final RegistrySupplier INOSTRANCEVIA_DNA = ITEMS.register("inostrancevia_dna", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.RARE))); + public static final RegistrySupplier LAMBEOSAURUS_DNA = ITEMS.register("lambeosaurus_dna", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.RARE))); + public static final RegistrySupplier LUDODACTYLUS_DNA = ITEMS.register("ludodactylus_dna", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.RARE))); + public static final RegistrySupplier MAMENCHISAURUS_DNA = ITEMS.register("mamenchisaurus_dna", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.RARE))); + public static final RegistrySupplier METRIACANTHOSAURUS_DNA = ITEMS.register("metriacanthosaurus_dna", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.RARE))); + public static final RegistrySupplier MOGANOPTERUS_DNA = ITEMS.register("moganopterus_dna", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.RARE))); + public static final RegistrySupplier NYCTOSAURUS_DNA = ITEMS.register("nyctosaurus_dna", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.RARE))); + public static final RegistrySupplier ORNITHOLESTES_DNA = ITEMS.register("ornitholestes_dna", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.RARE))); + public static final RegistrySupplier ORNITHOMIMUS_DNA = ITEMS.register("ornithomimus_dna", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.RARE))); + public static final RegistrySupplier OVIRAPTOR_DNA = ITEMS.register("oviraptor_dna", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.RARE))); + public static final RegistrySupplier PACHYCEPHALOSAURUS_DNA = ITEMS.register("pachycephalosaurus_dna", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.RARE))); + public static final RegistrySupplier PROCERATOSAURUS_DNA = ITEMS.register("proceratosaurus_dna", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.RARE))); + public static final RegistrySupplier PTERANODON_DNA = ITEMS.register("pteranodon_dna", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.RARE))); + public static final RegistrySupplier PTERODAUSTRO_DNA = ITEMS.register("pterodaustro_dna", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.RARE))); + public static final RegistrySupplier QUETZALCOATLUS_DNA = ITEMS.register("quetzalcoatlus_dna", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.RARE))); + public static final RegistrySupplier RAJASAURUS_DNA = ITEMS.register("rajasaurus_dna", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.RARE))); + public static final RegistrySupplier SEGISAURUS_DNA = ITEMS.register("segisaurus_dna", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.RARE))); + public static final RegistrySupplier TAPEJARA_DNA = ITEMS.register("tapejara_dna", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.RARE))); + public static final RegistrySupplier TITANOSAURUS_DNA = ITEMS.register("titanosaurus_dna", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.RARE))); + public static final RegistrySupplier TROODON_DNA = ITEMS.register("troodon_dna", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.RARE))); + public static final RegistrySupplier TROPEOGNATHUS_DNA = ITEMS.register("tropeognathus_dna", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.RARE))); + public static final RegistrySupplier TUPUXUARA_DNA = ITEMS.register("tupuxuara_dna", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.RARE))); + public static final RegistrySupplier UTAHRAPTOR_DNA = ITEMS.register("utahraptor_dna", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.RARE))); + public static final RegistrySupplier ZHENYUANOPTERUS_DNA = ITEMS.register("zhenyuanopterus_dna", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.RARE))); + + + // Syringe group + public static final RegistrySupplier VELOCIRAPTOR_SYRINGE = ITEMS.register("velociraptor_syringe", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.UNCOMMON))); + public static final RegistrySupplier TYRANNOSAURUS_REX_SYRINGE = ITEMS.register("tyrannosaurus_rex_syringe", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.UNCOMMON))); + public static final RegistrySupplier TRICERATOPS_SYRINGE = ITEMS.register("triceratops_syringe", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.UNCOMMON))); + public static final RegistrySupplier SPINOSAURUS_SYRINGE = ITEMS.register("spinosaurus_syringe", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.UNCOMMON))); + public static final RegistrySupplier PARASAUROLOPHUS_SYRINGE = ITEMS.register("parasaurolophus_syringe", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.UNCOMMON))); + public static final RegistrySupplier OURANOSAURUS_SYRINGE = ITEMS.register("ouranosaurus_syringe", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.UNCOMMON))); + public static final RegistrySupplier INDOMINUS_REX_SYRINGE = ITEMS.register("indominus_rex_syringe", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.UNCOMMON))); + public static final RegistrySupplier GALLIMIMUS_SYRINGE = ITEMS.register("gallimimus_syringe", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.UNCOMMON))); + public static final RegistrySupplier DIPLODOCUS_SYRINGE = ITEMS.register("diplodocus_syringe", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.UNCOMMON))); + public static final RegistrySupplier DILOPHOSAURUS_SYRINGE = ITEMS.register("dilophosaurus_syringe", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.UNCOMMON))); + public static final RegistrySupplier COMPSOGNATHUS_SYRINGE = ITEMS.register("compsognathus_syringe", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.UNCOMMON))); + public static final RegistrySupplier CERATOSAURUS_SYRINGE = ITEMS.register("ceratosaurus_syringe", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.UNCOMMON))); + public static final RegistrySupplier BRACHIOSAURUS_SYRINGE = ITEMS.register("brachiosaurus_syringe", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.UNCOMMON))); + public static final RegistrySupplier ALBERTOSAURUS_SYRINGE = ITEMS.register("albertosaurus_syringe", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.UNCOMMON))); + public static final RegistrySupplier APATOSAURUS_SYRINGE = ITEMS.register("apatosaurus_syringe", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.UNCOMMON))); + public static final RegistrySupplier BARYONYX_SYRINGE = ITEMS.register("baryonyx_syringe", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.UNCOMMON))); + public static final RegistrySupplier CARNOTAURUS_SYRINGE = ITEMS.register("carnotaurus_syringe", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.UNCOMMON))); + public static final RegistrySupplier CONCAVENATOR_SYRINGE = ITEMS.register("concavenator_syringe", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.UNCOMMON))); + public static final RegistrySupplier DEINONYCHUS_SYRINGE = ITEMS.register("deinonychus_syringe", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.UNCOMMON))); + public static final RegistrySupplier EDMONTOSAURUS_SYRINGE = ITEMS.register("edmontosaurus_syringe", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.UNCOMMON))); + public static final RegistrySupplier GIGANOTOSAURUS_SYRINGE = ITEMS.register("giganotosaurus_syringe", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.UNCOMMON))); + public static final RegistrySupplier GUANLONG_SYRINGE = ITEMS.register("guanlong_syringe", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.UNCOMMON))); + public static final RegistrySupplier HERRERASAURUS_SYRINGE = ITEMS.register("herrerasaurus_syringe", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.UNCOMMON))); + public static final RegistrySupplier MAJUNGASAURUS_SYRINGE = ITEMS.register("majungasaurus_syringe", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.UNCOMMON))); + public static final RegistrySupplier PROCOMPSOGNATHUS_SYRINGE = ITEMS.register("procompsognathus_syringe", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.UNCOMMON))); + public static final RegistrySupplier PROTOCERATOPS_SYRINGE = ITEMS.register("protoceratops_syringe", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.UNCOMMON))); + public static final RegistrySupplier RUGOPS_SYRINGE = ITEMS.register("rugops_syringe", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.UNCOMMON))); + public static final RegistrySupplier SHANTUNGOSAURUS_SYRINGE = ITEMS.register("shantungosaurus_syringe", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.UNCOMMON))); + public static final RegistrySupplier STEGOSAURUS_SYRINGE = ITEMS.register("stegosaurus_syringe", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.UNCOMMON))); + public static final RegistrySupplier STYRACOSAURUS_SYRINGE = ITEMS.register("styracosaurus_syringe", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.UNCOMMON))); + public static final RegistrySupplier THERIZINOSAURUS_SYRINGE = ITEMS.register("therizinosaurus_syringe", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.UNCOMMON))); + public static final RegistrySupplier DISTORTUS_REX_SYRINGE = ITEMS.register("distortus_rex_syringe", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.UNCOMMON))); + public static final RegistrySupplier ALLOSAURUS_SYRINGE = ITEMS.register("allosaurus_syringe", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.UNCOMMON))); + public static final RegistrySupplier ALVAREZSAURUS_SYRINGE = ITEMS.register("alvarezsaurus_syringe", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.UNCOMMON))); + public static final RegistrySupplier ANKYLOSAURUS_SYRINGE = ITEMS.register("ankylosaurus_syringe", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.UNCOMMON))); + public static final RegistrySupplier ARAMBOURGIANIA_SYRINGE = ITEMS.register("arambourgiania_syringe", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.UNCOMMON))); + public static final RegistrySupplier CARCHARODONTOSAURUS_SYRINGE = ITEMS.register("carcharodontosaurus_syringe", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.UNCOMMON))); + public static final RegistrySupplier CEARADACTYLUS_SYRINGE = ITEMS.register("cearadactylus_syringe", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.UNCOMMON))); + public static final RegistrySupplier CHASMOSAURUS_SYRINGE = ITEMS.register("chasmosaurus_syringe", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.UNCOMMON))); + public static final RegistrySupplier COELOPHYSIS_SYRINGE = ITEMS.register("coelophysis_syringe", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.UNCOMMON))); + public static final RegistrySupplier COELURUS_SYRINGE = ITEMS.register("coelurus_syringe", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.UNCOMMON))); + public static final RegistrySupplier CORYTHOSAURUS_SYRINGE = ITEMS.register("corythosaurus_syringe", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.UNCOMMON))); + public static final RegistrySupplier DIMORPHODON_SYRINGE = ITEMS.register("dimorphodon_syringe", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.UNCOMMON))); + public static final RegistrySupplier DRYOSAURUS_SYRINGE = ITEMS.register("dryosaurus_syringe", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.UNCOMMON))); + public static final RegistrySupplier GEOSTERNBERGIA_SYRINGE = ITEMS.register("geosternbergia_syringe", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.UNCOMMON))); + public static final RegistrySupplier GUIDRACO_SYRINGE = ITEMS.register("guidraco_syringe", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.UNCOMMON))); + public static final RegistrySupplier HADROSAURUS_SYRINGE = ITEMS.register("hadrosaurus_syringe", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.UNCOMMON))); + public static final RegistrySupplier HYPSILOPHODON_SYRINGE = ITEMS.register("hypsilophodon_syringe", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.UNCOMMON))); + public static final RegistrySupplier INDORAPTOR_SYRINGE = ITEMS.register("indoraptor_syringe", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.UNCOMMON))); + public static final RegistrySupplier INOSTRANCEVIA_SYRINGE = ITEMS.register("inostrancevia_syringe", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.UNCOMMON))); + public static final RegistrySupplier LAMBEOSAURUS_SYRINGE = ITEMS.register("lambeosaurus_syringe", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.UNCOMMON))); + public static final RegistrySupplier LUDODACTYLUS_SYRINGE = ITEMS.register("ludodactylus_syringe", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.UNCOMMON))); + public static final RegistrySupplier MAMENCHISAURUS_SYRINGE = ITEMS.register("mamenchisaurus_syringe", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.UNCOMMON))); + public static final RegistrySupplier METRIACANTHOSAURUS_SYRINGE = ITEMS.register("metriacanthosaurus_syringe", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.UNCOMMON))); + public static final RegistrySupplier MOGANOPTERUS_SYRINGE = ITEMS.register("moganopterus_syringe", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.UNCOMMON))); + public static final RegistrySupplier NYCTOSAURUS_SYRINGE = ITEMS.register("nyctosaurus_syringe", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.UNCOMMON))); + public static final RegistrySupplier ORNITHOLESTES_SYRINGE = ITEMS.register("ornitholestes_syringe", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.UNCOMMON))); + public static final RegistrySupplier ORNITHOMIMUS_SYRINGE = ITEMS.register("ornithomimus_syringe", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.UNCOMMON))); + public static final RegistrySupplier OVIRAPTOR_SYRINGE = ITEMS.register("oviraptor_syringe", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.UNCOMMON))); + public static final RegistrySupplier PACHYCEPHALOSAURUS_SYRINGE = ITEMS.register("pachycephalosaurus_syringe", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.UNCOMMON))); + public static final RegistrySupplier PROCERATOSAURUS_SYRINGE = ITEMS.register("proceratosaurus_syringe", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.UNCOMMON))); + public static final RegistrySupplier PTERANODON_SYRINGE = ITEMS.register("pteranodon_syringe", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.UNCOMMON))); + public static final RegistrySupplier PTERODAUSTRO_SYRINGE = ITEMS.register("pterodaustro_syringe", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.UNCOMMON))); + public static final RegistrySupplier QUETZALCOATLUS_SYRINGE = ITEMS.register("quetzalcoatlus_syringe", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.UNCOMMON))); + public static final RegistrySupplier RAJASAURUS_SYRINGE = ITEMS.register("rajasaurus_syringe", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.UNCOMMON))); + public static final RegistrySupplier SEGISAURUS_SYRINGE = ITEMS.register("segisaurus_syringe", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.UNCOMMON))); + public static final RegistrySupplier TAPEJARA_SYRINGE = ITEMS.register("tapejara_syringe", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.UNCOMMON))); + public static final RegistrySupplier TITANOSAURUS_SYRINGE = ITEMS.register("titanosaurus_syringe", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.UNCOMMON))); + public static final RegistrySupplier TROODON_SYRINGE = ITEMS.register("troodon_syringe", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.UNCOMMON))); + public static final RegistrySupplier TROPEOGNATHUS_SYRINGE = ITEMS.register("tropeognathus_syringe", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.UNCOMMON))); + public static final RegistrySupplier TUPUXUARA_SYRINGE = ITEMS.register("tupuxuara_syringe", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.UNCOMMON))); + public static final RegistrySupplier UTAHRAPTOR_SYRINGE = ITEMS.register("utahraptor_syringe", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.UNCOMMON))); + public static final RegistrySupplier ZHENYUANOPTERUS_SYRINGE = ITEMS.register("zhenyuanopterus_syringe", () -> new Item(new Item.Properties().stacksTo(8).rarity(Rarity.UNCOMMON))); - public static final RegistrySupplier DNA_SYRINGE = ITEMS.register("dna_syringe", - () -> new Item(new Item.Properties().stacksTo(1))); public static void register() { ITEMS.register(); diff --git a/common/src/main/java/net/cmr/jurassicrevived/item/custom/CustomGenderedSpawnEggItem.java b/common/src/main/java/net/cmr/jurassicrevived/item/custom/CustomGenderedSpawnEggItem.java new file mode 100644 index 0000000..8ddecbc --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/item/custom/CustomGenderedSpawnEggItem.java @@ -0,0 +1,171 @@ +package net.cmr.jurassicrevived.item.custom; + +import net.minecraft.ChatFormatting; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.chat.Component; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.InteractionResultHolder; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.Mob; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.SpawnEggItem; +import net.minecraft.world.item.TooltipFlag; +import net.minecraft.world.item.context.UseOnContext; +import net.minecraft.world.level.Level; + +//? if >1.20.1 { +/*import net.minecraft.core.component.DataComponents; +import net.minecraft.world.item.component.CustomData; +import net.minecraft.world.item.Item; +import org.jetbrains.annotations.Nullable; +*///?} else { +import net.minecraft.world.item.Item; +import org.jetbrains.annotations.Nullable; +//?} + +import java.util.List; +import java.util.function.Supplier; + +public class CustomGenderedSpawnEggItem extends SpawnEggItem { + private static final String KEY_SELECTED_VARIANT = "SelectedVariant"; + private static final String KEY_VARIANT = "Variant"; + private static final String KEY_ENTITY_TAG = "EntityTag"; + private static final int VARIANT_COUNT = 2; // 0=Male, 1=Female + + private final Supplier> typeSupplier; + + public CustomGenderedSpawnEggItem(Supplier> type, + int backgroundColor, + int highlightColor, + Item.Properties properties) { + //? if >1.20.1 { + /*super(EntityType.PIG, backgroundColor, highlightColor, properties); + *///?} else { + super(EntityType.PIG, backgroundColor, highlightColor, properties); + //?} + this.typeSupplier = type; + } + + public EntityType getType(@Nullable CompoundTag nbt) { + return typeSupplier.get(); + } + + private static int getSelectedVariant(ItemStack stack) { + //? if >1.20.1 { + /*CustomData data = stack.get(DataComponents.CUSTOM_DATA); + if (data == null) return 0; + CompoundTag tag = data.copyTag(); + if (!tag.contains(KEY_SELECTED_VARIANT)) return 0; + return Math.floorMod(tag.getInt(KEY_SELECTED_VARIANT), VARIANT_COUNT); + *///?} else { + CompoundTag tag = stack.getTag(); + if (tag == null || !tag.contains(KEY_SELECTED_VARIANT)) return 0; + return Math.floorMod(tag.getInt(KEY_SELECTED_VARIANT), VARIANT_COUNT); + //?} + } + + private static void setSelectedVariant(ItemStack stack, int variant) { + int v = Math.floorMod(variant, VARIANT_COUNT); + //? if >1.20.1 { + /*stack.update(DataComponents.CUSTOM_DATA, CustomData.EMPTY, existing -> { + CompoundTag tag = existing.copyTag(); + tag.putInt(KEY_SELECTED_VARIANT, v); + return CustomData.of(tag); + }); + *///?} else { + stack.getOrCreateTag().putInt(KEY_SELECTED_VARIANT, v); + //?} + } + + private static void cycleVariant(ItemStack stack) { + setSelectedVariant(stack, (getSelectedVariant(stack) + 1) % VARIANT_COUNT); + } + + private static void ensureEntityDataHasVariant(ItemStack stack) { + final int variant = getSelectedVariant(stack); + //? if >1.20.1 { + /*stack.update(DataComponents.ENTITY_DATA, CustomData.EMPTY, existing -> { + CompoundTag tag = existing.copyTag(); + tag.putInt(KEY_VARIANT, variant); + return CustomData.of(tag); + }); + *///?} else { + CompoundTag root = stack.getOrCreateTag(); + CompoundTag entityTag = root.getCompound(KEY_ENTITY_TAG); + entityTag.putInt(KEY_VARIANT, variant); + root.put(KEY_ENTITY_TAG, entityTag); + //?} + } + + @Override + public InteractionResultHolder use(Level level, Player player, InteractionHand hand) { + ItemStack stack = player.getItemInHand(hand); + + if (player.isSecondaryUseActive()) { + if (!level.isClientSide) { + cycleVariant(stack); + level.playSound(null, player.getX(), player.getY(), player.getZ(), + SoundEvents.EXPERIENCE_ORB_PICKUP, SoundSource.PLAYERS, 0.5f, 1.1f); + } + return InteractionResultHolder.sidedSuccess(stack, level.isClientSide); + } + + ensureEntityDataHasVariant(stack); + return super.use(level, player, hand); + } + + @Override + public InteractionResult useOn(UseOnContext context) { + Player player = context.getPlayer(); + Level level = context.getLevel(); + + if (player != null && player.isSecondaryUseActive()) { + if (!level.isClientSide) { + cycleVariant(context.getItemInHand()); + level.playSound(null, context.getClickedPos(), + SoundEvents.EXPERIENCE_ORB_PICKUP, SoundSource.PLAYERS, 0.5f, 1.1f); + } + return InteractionResult.sidedSuccess(level.isClientSide()); + } + + if (player == null || !player.isSecondaryUseActive()) { + ensureEntityDataHasVariant(context.getItemInHand()); + } + return super.useOn(context); + } + + //? if >1.20.1 { + /*@Override + public void appendHoverText(ItemStack stack, Item.TooltipContext context, List tooltip, TooltipFlag flag) { + super.appendHoverText(stack, context, tooltip, flag); + addGenderTooltip(stack, tooltip); + } + *///?} else { + @Override + public void appendHoverText(ItemStack stack, Level level, List tooltip, TooltipFlag flag) { + super.appendHoverText(stack, level, tooltip, flag); + addGenderTooltip(stack, tooltip); + } + //?} + + private void addGenderTooltip(ItemStack stack, List tooltip) { + int v = getSelectedVariant(stack); + String genderText = (v == 0) ? "Male" : "Female"; + tooltip.add(Component.translatable("tooltip.jurassicrevived.gender", genderText)); + tooltip.add(Component.translatable("tooltip.jurassicrevived.gender.hint", "Shift-Right-Click")); + } + + @Override + public Component getName(ItemStack stack) { + Component base = super.getName(stack); + boolean male = getSelectedVariant(stack) == 0; + Component gender = Component.literal(male ? "Male" : "Female") + .withStyle(male ? ChatFormatting.AQUA : ChatFormatting.LIGHT_PURPLE); + + return base.copy().append(Component.literal(" (")).append(gender).append(Component.literal(")")); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/item/custom/FrogSyringeItem.java b/common/src/main/java/net/cmr/jurassicrevived/item/custom/FrogSyringeItem.java new file mode 100644 index 0000000..49b69f2 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/item/custom/FrogSyringeItem.java @@ -0,0 +1,41 @@ +package net.cmr.jurassicrevived.item.custom; + +import net.cmr.jurassicrevived.item.ModItems; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.animal.frog.Frog; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; + +public class FrogSyringeItem extends Item { + public FrogSyringeItem(Properties properties) { + super(properties); + } + + @Override + public InteractionResult interactLivingEntity(ItemStack stack, Player player, LivingEntity target, InteractionHand hand) { + if (!(target instanceof Frog)) { + return InteractionResult.PASS; + } + + if (!player.level().isClientSide) { + // consume one syringe + if (!player.getAbilities().instabuild) { + stack.shrink(1); + } + // give frog material + ItemStack reward = new ItemStack(ModItems.FROG_MATERIAL.get()); + if (!player.addItem(reward)) { + player.drop(reward, false); + } + // feedback + player.level().playSound(null, target.blockPosition(), SoundEvents.HONEY_DRINK, SoundSource.PLAYERS, 0.7f, 1.2f); + } + + return InteractionResult.sidedSuccess(player.level().isClientSide); + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/mixin/FlowerPotBlockAccessor.java b/common/src/main/java/net/cmr/jurassicrevived/mixin/FlowerPotBlockAccessor.java new file mode 100644 index 0000000..26d6bee --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/mixin/FlowerPotBlockAccessor.java @@ -0,0 +1,14 @@ +package net.cmr.jurassicrevived.mixin; + +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.FlowerPotBlock; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +import java.util.Map; + +@Mixin(FlowerPotBlock.class) +public interface FlowerPotBlockAccessor { + @Accessor("POTTED_BY_CONTENT") + Map getPottedByContent(); +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/mixin/LivingEntityMixin.java b/common/src/main/java/net/cmr/jurassicrevived/mixin/LivingEntityMixin.java new file mode 100644 index 0000000..24a2c75 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/mixin/LivingEntityMixin.java @@ -0,0 +1,36 @@ +package net.cmr.jurassicrevived.mixin; + +import net.cmr.jurassicrevived.entity.ai.DinoEntityBase; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.player.Player; +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(LivingEntity.class) +public abstract class LivingEntityMixin { + /*? if >1.20.1 {*/ + /*@Inject(method = "dropAllDeathLoot", at = @At("HEAD"), cancellable = true) + private void jurassicrevived$preventDropsFromDinoKill(ServerLevel level, DamageSource damageSource, CallbackInfo ci) { + jurassicrevived$handleDinoKill(damageSource, ci); + } + *//*?} else {*/ + @Inject(method = "dropAllDeathLoot", at = @At("HEAD"), cancellable = true) + private void jurassicrevived$preventDropsFromDinoKill(DamageSource damageSource, CallbackInfo ci) { + jurassicrevived$handleDinoKill(damageSource, ci); + } + /*?}*/ + + private void jurassicrevived$handleDinoKill(DamageSource damageSource, CallbackInfo ci) { + LivingEntity victim = (LivingEntity) (Object) this; + if (damageSource.getEntity() instanceof DinoEntityBase attacker && attacker.isCarnivore()) { + // If it's not a player, cancel the loot drop (Dino ate the body) + if (!(victim instanceof Player)) { + ci.cancel(); + } + } + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/mixin/MenuScreensMixin.java b/common/src/main/java/net/cmr/jurassicrevived/mixin/MenuScreensMixin.java new file mode 100644 index 0000000..733d30f --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/mixin/MenuScreensMixin.java @@ -0,0 +1,34 @@ +package net.cmr.jurassicrevived.mixin; + +import dev.architectury.registry.menu.MenuRegistry; +import net.cmr.jurassicrevived.screen.ModMenuTypes; +import net.cmr.jurassicrevived.screen.custom.*; +import net.minecraft.client.gui.screens.MenuScreens; +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(MenuScreens.class) +public class MenuScreensMixin { + @Inject(method = "", at = @At("RETURN")) + private static void onClinit(CallbackInfo ci) { + //? if >1.20.1 { + /*// This runs when MenuScreens class is loaded by the JVM + // Registering here ensures the MenuTypes are already constructed and the registries are ready + MenuRegistry.registerScreenFactory(ModMenuTypes.GENERATOR_MENU.get(), GeneratorScreen::new); + MenuRegistry.registerScreenFactory(ModMenuTypes.DNA_EXTRACTOR_MENU.get(), DNAExtractorScreen::new); + MenuRegistry.registerScreenFactory(ModMenuTypes.DNA_ANALYZER_MENU.get(), DNAAnalyzerScreen::new); + MenuRegistry.registerScreenFactory(ModMenuTypes.FOSSIL_GRINDER_MENU.get(), FossilGrinderScreen::new); + MenuRegistry.registerScreenFactory(ModMenuTypes.FOSSIL_CLEANER_MENU.get(), FossilCleanerScreen::new); + MenuRegistry.registerScreenFactory(ModMenuTypes.DNA_HYBRIDIZER_MENU.get(), DNAHybridizerScreen::new); + MenuRegistry.registerScreenFactory(ModMenuTypes.EMBRYONIC_MACHINE_MENU.get(), EmbryonicMachineScreen::new); + MenuRegistry.registerScreenFactory(ModMenuTypes.EMBRYO_CALCIFICATION_MACHINE_MENU.get(), EmbryoCalcificationMachineScreen::new); + MenuRegistry.registerScreenFactory(ModMenuTypes.INCUBATOR_MENU.get(), IncubatorScreen::new); + MenuRegistry.registerScreenFactory(ModMenuTypes.TANK_MENU.get(), TankScreen::new); + MenuRegistry.registerScreenFactory(ModMenuTypes.POWER_CELL_MENU.get(), PowerCellScreen::new); + MenuRegistry.registerScreenFactory(ModMenuTypes.WOOD_CRATE_MENU.get(), CrateScreen::new); + MenuRegistry.registerScreenFactory(ModMenuTypes.IRON_CRATE_MENU.get(), CrateScreen::new); + *///?} + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/recipe/DNAAnalyzerRecipe.java b/common/src/main/java/net/cmr/jurassicrevived/recipe/DNAAnalyzerRecipe.java new file mode 100644 index 0000000..f8b5dc7 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/recipe/DNAAnalyzerRecipe.java @@ -0,0 +1,142 @@ +package net.cmr.jurassicrevived.recipe; + +import net.cmr.jurassicrevived.Constants; +import net.minecraft.core.NonNullList; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.*; +import net.minecraft.world.level.Level; +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.NotNull; + +/*? if >1.20.1 {*/ +/*import com.mojang.serialization.Codec; +import com.mojang.serialization.MapCodec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import com.mojang.serialization.DataResult; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.core.HolderLookup; +import java.util.List; +*//*?} else {*/ +import com.google.gson.JsonObject; +import com.google.gson.JsonArray; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.util.GsonHelper; +import net.minecraft.core.RegistryAccess; +/*?}*/ + +public record DNAAnalyzerRecipe( + NonNullList inputs, + ItemStack output, + java.util.Map weights +) implements Recipe { + + @Override + public boolean matches(DNAAnalyzerRecipeInput input, Level level) { + if (level.isClientSide) return false; + ItemStack in0 = input.getItem(0); + ItemStack in1 = input.getItem(1); + return (inputs.get(0).test(in0) && inputs.get(1).test(in1)) || + (inputs.get(0).test(in1) && inputs.get(1).test(in0)); + } + + /*? if >1.20.1 {*/ + /*@Override + public ItemStack assemble(DNAAnalyzerRecipeInput input, HolderLookup.Provider provider) { + return output.copy(); + } + + @Override + public ItemStack getResultItem(HolderLookup.Provider provider) { + return output.copy(); + } + *//*?} else {*/ + @Override + public ItemStack assemble(DNAAnalyzerRecipeInput input, RegistryAccess access) { + return output.copy(); + } + + @Override + public ItemStack getResultItem(RegistryAccess access) { + return output.copy(); + } + + @Override + public ResourceLocation getId() { + // In 1.20.1, the ID was stored. In 1.21.1, it's handled by the registry. + // If you need the ID specifically in 1.20.1, you'd need to add it to the record. + return Constants.rl("dna_analyzing"); + } + /*?}*/ + + @Override + public boolean canCraftInDimensions(int width, int height) { return true; } + + @Override + public RecipeSerializer getSerializer() { return ModRecipes.DNA_ANALYZER_SERIALIZER.get(); } + + @Override + public RecipeType getType() { return ModRecipes.DNA_ANALYZER_RECIPE_TYPE.get(); } + + public int getWeightFor(Item item) { + ResourceLocation key = BuiltInRegistries.ITEM.getKey(item); + return weights.getOrDefault(key, 1); + } + + public static class Serializer implements RecipeSerializer { + /*? if >1.20.1 {*/ + /*public static final MapCodec CODEC = RecordCodecBuilder.mapCodec(inst -> inst.group( + Ingredient.CODEC_NONEMPTY.listOf().fieldOf("ingredients").flatXmap( + l -> l.size() == 2 ? DataResult.success(NonNullList.of(Ingredient.EMPTY, l.get(0), l.get(1))) : DataResult.error(() -> "Needs 2 ingredients"), + l -> DataResult.success(List.copyOf(l)) + ).forGetter(DNAAnalyzerRecipe::inputs), + ItemStack.CODEC.fieldOf("result").forGetter(DNAAnalyzerRecipe::output), + Codec.unboundedMap(ResourceLocation.CODEC, Codec.INT).optionalFieldOf("weights", java.util.Map.of()).forGetter(DNAAnalyzerRecipe::weights) + ).apply(inst, DNAAnalyzerRecipe::new)); + + public static final StreamCodec STREAM_CODEC = StreamCodec.of( + (buf, r) -> { + buf.writeVarInt(r.inputs().size()); + for(Ingredient i : r.inputs()) Ingredient.CONTENTS_STREAM_CODEC.encode(buf, i); + ItemStack.STREAM_CODEC.encode(buf, r.output()); + buf.writeMap(r.weights(), ResourceLocation.STREAM_CODEC, ByteBufCodecs.VAR_INT); + }, + buf -> { + int size = buf.readVarInt(); + NonNullList ins = NonNullList.create(); + for(int i=0; i new java.util.HashMap<>(), ResourceLocation.STREAM_CODEC, ByteBufCodecs.VAR_INT)); + } + ); + + @Override public MapCodec codec() { return CODEC; } + @Override public StreamCodec streamCodec() { return STREAM_CODEC; } + *//*?} else {*/ + @Override + public DNAAnalyzerRecipe fromJson(ResourceLocation id, JsonObject json) { + JsonArray ingredients = GsonHelper.getAsJsonArray(json, "ingredients"); + NonNullList inputs = NonNullList.withSize(2, Ingredient.EMPTY); + for (int i = 0; i < 2; i++) inputs.set(i, Ingredient.fromJson(ingredients.get(i))); + ItemStack output = ShapedRecipe.itemStackFromJson(GsonHelper.getAsJsonObject(json, "result")); + return new DNAAnalyzerRecipe(inputs, output, java.util.Map.of()); // Weights logic can be added here + } + + @Override + public DNAAnalyzerRecipe fromNetwork(ResourceLocation id, FriendlyByteBuf buf) { + NonNullList inputs = NonNullList.withSize(buf.readInt(), Ingredient.EMPTY); + for (int i = 0; i < inputs.size(); i++) inputs.set(i, Ingredient.fromNetwork(buf)); + return new DNAAnalyzerRecipe(inputs, buf.readItem(), java.util.Map.of()); + } + + @Override + public void toNetwork(FriendlyByteBuf buf, DNAAnalyzerRecipe recipe) { + buf.writeInt(recipe.inputs().size()); + for (Ingredient ing : recipe.inputs()) ing.toNetwork(buf); + buf.writeItem(recipe.output()); + } + /*?}*/ + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/recipe/DNAAnalyzerRecipeInput.java b/common/src/main/java/net/cmr/jurassicrevived/recipe/DNAAnalyzerRecipeInput.java new file mode 100644 index 0000000..a63522d --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/recipe/DNAAnalyzerRecipeInput.java @@ -0,0 +1,47 @@ +package net.cmr.jurassicrevived.recipe; + +import net.minecraft.world.item.ItemStack; +import java.util.List; +import java.util.Objects; + +/*? if >1.20.1 {*/ +/*import net.minecraft.world.item.crafting.RecipeInput; +*//*?} else {*/ +import net.minecraft.world.Container; +import net.minecraft.world.entity.player.Player; +/*?}*/ + +public record DNAAnalyzerRecipeInput(List inputs) + /*? if >1.20.1 {*/ /*implements RecipeInput *//*?} else {*/ implements Container /*?}*/ { + + public DNAAnalyzerRecipeInput { + Objects.requireNonNull(inputs, "inputs"); + if (inputs.size() != 2) { + throw new IllegalArgumentException("DNAAnalyzerRecipeInput requires exactly 2 input stacks"); + } + } + + public DNAAnalyzerRecipeInput(ItemStack first, ItemStack second) { + this(List.of(first, second)); + } + + @Override + public ItemStack getItem(int i) { + return inputs.get(i); + } + + public int size() { + return inputs.size(); + } + + /*? if <=1.20.1 {*/ + @Override public int getContainerSize() { return size(); } + @Override public boolean isEmpty() { return inputs.stream().allMatch(ItemStack::isEmpty); } + @Override public ItemStack removeItem(int i, int j) { return ItemStack.EMPTY; } + @Override public ItemStack removeItemNoUpdate(int i) { return ItemStack.EMPTY; } + @Override public void setItem(int i, ItemStack itemStack) {} + @Override public void setChanged() {} + @Override public boolean stillValid(Player player) { return true; } + @Override public void clearContent() {} + /*?}*/ +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/recipe/DNAExtractorRecipe.java b/common/src/main/java/net/cmr/jurassicrevived/recipe/DNAExtractorRecipe.java new file mode 100644 index 0000000..619cb5d --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/recipe/DNAExtractorRecipe.java @@ -0,0 +1,142 @@ +package net.cmr.jurassicrevived.recipe; + +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.item.ModItems; +import net.cmr.jurassicrevived.util.ModTags; +import net.minecraft.core.NonNullList; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.*; +import net.minecraft.world.level.Level; +import net.minecraft.util.RandomSource; +import org.jetbrains.annotations.NotNull; + +/*? if >1.20.1 {*/ +/*import com.mojang.serialization.Codec; +import com.mojang.serialization.MapCodec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import com.mojang.serialization.DataResult; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.core.HolderLookup; +import java.util.List; +*//*?} else {*/ +import com.google.gson.JsonObject; +import com.google.gson.JsonArray; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.util.GsonHelper; +import net.minecraft.core.RegistryAccess; +/*?}*/ + +public record DNAExtractorRecipe( + NonNullList inputs, + ItemStack output, + java.util.Map weights +) implements Recipe { + + @Override + public boolean matches(DNAExtractorRecipeInput input, Level level) { + if (level.isClientSide) return false; + ItemStack in0 = input.getItem(0); + ItemStack in1 = input.getItem(1); + return (inputs.get(0).test(in0) && inputs.get(1).test(in1)) || + (inputs.get(0).test(in1) && inputs.get(1).test(in0)); + } + + private ItemStack handleAmberExtraction(DNAExtractorRecipeInput input) { + if (!input.getItem(1).isEmpty() && input.getItem(1).is(ModItems.MOSQUITO_IN_AMBER.get())) { + ItemStack[] candidates = Ingredient.of(ModTags.Items.DNA).getItems(); + if (candidates.length > 0) { + return new ItemStack(pickWeightedRandomDNA(candidates, RandomSource.create())); + } + } + return output.copy(); + } + + private net.minecraft.world.item.Item pickWeightedRandomDNA(ItemStack[] candidates, RandomSource random) { + long total = 0; + int[] itemWeights = new int[candidates.length]; + for (int i = 0; i < candidates.length; i++) { + ResourceLocation key = net.minecraft.core.registries.BuiltInRegistries.ITEM.getKey(candidates[i].getItem()); + int w = Math.max(0, weights.getOrDefault(key, 1)); + itemWeights[i] = w; + total += w; + } + if (total <= 0) return candidates[random.nextInt(candidates.length)].getItem(); + long roll = 1L + (total <= Integer.MAX_VALUE ? random.nextInt((int) total) : Math.abs(random.nextLong()) % total); + long cumulative = 0; + for (int i = 0; i < candidates.length; i++) { + cumulative += itemWeights[i]; + if (roll <= cumulative) return candidates[i].getItem(); + } + return candidates[candidates.length - 1].getItem(); + } + + /*? if >1.20.1 {*/ + /*@Override public ItemStack assemble(DNAExtractorRecipeInput input, HolderLookup.Provider p) { return handleAmberExtraction(input); } + @Override public ItemStack getResultItem(HolderLookup.Provider p) { return output.copy(); } + *//*?} else {*/ + @Override public ItemStack assemble(DNAExtractorRecipeInput input, RegistryAccess a) { return handleAmberExtraction(input); } + @Override public ItemStack getResultItem(RegistryAccess a) { return output.copy(); } + @Override public ResourceLocation getId() { return Constants.rl("dna_extracting"); } + /*?}*/ + + @Override public boolean canCraftInDimensions(int w, int h) { return true; } + @Override public RecipeSerializer getSerializer() { return ModRecipes.DNA_EXTRACTOR_SERIALIZER.get(); } + @Override public RecipeType getType() { return ModRecipes.DNA_EXTRACTOR_RECIPE_TYPE.get(); } + + public int getWeightFor(Item item) { + ResourceLocation key = BuiltInRegistries.ITEM.getKey(item); + return weights.getOrDefault(key, 1); + } + + public static class Serializer implements RecipeSerializer { + /*? if >1.20.1 {*/ + /*public static final MapCodec CODEC = RecordCodecBuilder.mapCodec(inst -> inst.group( + Ingredient.CODEC_NONEMPTY.listOf().fieldOf("ingredients").flatXmap( + l -> l.size() == 2 ? DataResult.success(NonNullList.of(Ingredient.EMPTY, l.get(0), l.get(1))) : DataResult.error(() -> "Needs 2 ingredients"), + l -> DataResult.success(List.copyOf(l)) + ).forGetter(DNAExtractorRecipe::inputs), + ItemStack.CODEC.fieldOf("result").forGetter(DNAExtractorRecipe::output), + Codec.unboundedMap(ResourceLocation.CODEC, Codec.INT).optionalFieldOf("weights", java.util.Map.of()).forGetter(DNAExtractorRecipe::weights) + ).apply(inst, DNAExtractorRecipe::new)); + + public static final StreamCodec STREAM_CODEC = StreamCodec.of( + (buf, r) -> { + buf.writeVarInt(r.inputs().size()); + for(Ingredient i : r.inputs()) Ingredient.CONTENTS_STREAM_CODEC.encode(buf, i); + ItemStack.STREAM_CODEC.encode(buf, r.output()); + buf.writeMap(r.weights(), ResourceLocation.STREAM_CODEC, ByteBufCodecs.VAR_INT); + }, + buf -> { + int size = buf.readVarInt(); + NonNullList ins = NonNullList.create(); + for(int i=0; i new java.util.HashMap<>(), ResourceLocation.STREAM_CODEC, ByteBufCodecs.VAR_INT)); + } + ); + @Override public MapCodec codec() { return CODEC; } + @Override public StreamCodec streamCodec() { return STREAM_CODEC; } + *//*?} else {*/ + @Override public DNAExtractorRecipe fromJson(ResourceLocation id, JsonObject json) { + JsonArray ingredients = GsonHelper.getAsJsonArray(json, "ingredients"); + NonNullList inputs = NonNullList.withSize(2, Ingredient.EMPTY); + for (int i = 0; i < 2; i++) inputs.set(i, Ingredient.fromJson(ingredients.get(i))); + return new DNAExtractorRecipe(inputs, ShapedRecipe.itemStackFromJson(GsonHelper.getAsJsonObject(json, "result")), java.util.Map.of()); + } + @Override public DNAExtractorRecipe fromNetwork(ResourceLocation id, FriendlyByteBuf buf) { + NonNullList inputs = NonNullList.withSize(buf.readInt(), Ingredient.EMPTY); + for (int i = 0; i < inputs.size(); i++) inputs.set(i, Ingredient.fromNetwork(buf)); + return new DNAExtractorRecipe(inputs, buf.readItem(), java.util.Map.of()); + } + @Override public void toNetwork(FriendlyByteBuf buf, DNAExtractorRecipe recipe) { + buf.writeInt(recipe.inputs().size()); + for (Ingredient ing : recipe.inputs()) ing.toNetwork(buf); + buf.writeItem(recipe.output()); + } + /*?}*/ + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/recipe/DNAExtractorRecipeInput.java b/common/src/main/java/net/cmr/jurassicrevived/recipe/DNAExtractorRecipeInput.java new file mode 100644 index 0000000..e4e3c49 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/recipe/DNAExtractorRecipeInput.java @@ -0,0 +1,47 @@ +package net.cmr.jurassicrevived.recipe; + +import net.minecraft.world.item.ItemStack; +import java.util.List; +import java.util.Objects; + +/*? if >1.20.1 {*/ +/*import net.minecraft.world.item.crafting.RecipeInput; +*//*?} else {*/ +import net.minecraft.world.Container; +import net.minecraft.world.entity.player.Player; +/*?}*/ + +public record DNAExtractorRecipeInput(List inputs) + /*? if >1.20.1 {*/ /*implements RecipeInput *//*?} else {*/ implements Container /*?}*/ { + + public DNAExtractorRecipeInput { + Objects.requireNonNull(inputs, "inputs"); + if (inputs.size() != 2) { + throw new IllegalArgumentException("DNAExtractorRecipeInput requires exactly 2 input stacks"); + } + } + + public DNAExtractorRecipeInput(ItemStack first, ItemStack second) { + this(List.of(first, second)); + } + + @Override + public ItemStack getItem(int i) { + return inputs.get(i); + } + + public int size() { + return inputs.size(); + } + + /*? if <=1.20.1 {*/ + @Override public int getContainerSize() { return size(); } + @Override public boolean isEmpty() { return inputs.stream().allMatch(ItemStack::isEmpty); } + @Override public ItemStack removeItem(int i, int j) { return ItemStack.EMPTY; } + @Override public ItemStack removeItemNoUpdate(int i) { return ItemStack.EMPTY; } + @Override public void setItem(int i, ItemStack itemStack) {} + @Override public void setChanged() {} + @Override public boolean stillValid(Player player) { return true; } + @Override public void clearContent() {} + /*?}*/ +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/recipe/DNAHybridizerRecipe.java b/common/src/main/java/net/cmr/jurassicrevived/recipe/DNAHybridizerRecipe.java new file mode 100644 index 0000000..dca2223 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/recipe/DNAHybridizerRecipe.java @@ -0,0 +1,128 @@ +package net.cmr.jurassicrevived.recipe; + +import net.cmr.jurassicrevived.Constants; +import net.minecraft.core.NonNullList; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.*; +import net.minecraft.world.level.Level; +import org.jetbrains.annotations.NotNull; +import java.util.ArrayList; +import java.util.List; + +/*? if >1.20.1 {*/ +/*import com.mojang.serialization.MapCodec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import com.mojang.serialization.DataResult; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.core.HolderLookup; +*//*?} else {*/ +import com.google.gson.JsonObject; +import com.google.gson.JsonArray; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.util.GsonHelper; +import net.minecraft.core.RegistryAccess; +/*?}*/ + +public record DNAHybridizerRecipe(NonNullList inputs, ItemStack output) implements Recipe { + + @Override + public boolean matches(DNAHybridizerRecipeInput recipeInput, Level level) { + if (level.isClientSide) return false; + + // Catalyst matching (Slot 8) + Ingredient catalystNeed = inputs.size() > 8 ? inputs.get(8) : Ingredient.EMPTY; + ItemStack catalystIn = recipeInput.getItem(8); + if (!catalystNeed.test(catalystIn)) return false; + + // Unordered matching for DNA slots (0-7) + List dnaNeeds = new ArrayList<>(); + for (int i = 0; i < 8 && i < inputs.size(); i++) { + if (!inputs.get(i).isEmpty()) dnaNeeds.add(inputs.get(i)); + } + + List dnaInputs = new ArrayList<>(); + for (int i = 0; i < 8; i++) { + if (!recipeInput.getItem(i).isEmpty()) dnaInputs.add(recipeInput.getItem(i)); + } + + if (dnaNeeds.size() != dnaInputs.size()) return false; + + boolean[] used = new boolean[dnaNeeds.size()]; + for (ItemStack stack : dnaInputs) { + boolean matched = false; + for (int i = 0; i < dnaNeeds.size(); i++) { + if (!used[i] && dnaNeeds.get(i).test(stack)) { + used[i] = true; + matched = true; + break; + } + } + if (!matched) return false; + } + return true; + } + + /*? if >1.20.1 {*/ + /*@Override public ItemStack assemble(DNAHybridizerRecipeInput input, HolderLookup.Provider p) { return output.copy(); } + @Override public ItemStack getResultItem(HolderLookup.Provider p) { return output.copy(); } + *//*?} else {*/ + @Override public ItemStack assemble(DNAHybridizerRecipeInput input, RegistryAccess a) { return output.copy(); } + @Override public ItemStack getResultItem(RegistryAccess a) { return output.copy(); } + @Override public ResourceLocation getId() { return Constants.rl("dna_hybridizing"); } + /*?}*/ + + @Override public boolean canCraftInDimensions(int w, int h) { return true; } + @Override public RecipeSerializer getSerializer() { return ModRecipes.DNA_HYBRIDIZER_SERIALIZER.get(); } + @Override public RecipeType getType() { return ModRecipes.DNA_HYBRIDIZER_RECIPE_TYPE.get(); } + + + public static class Serializer implements RecipeSerializer { + /*? if >1.20.1 {*/ + /*public static final MapCodec CODEC = RecordCodecBuilder.mapCodec(inst -> inst.group( + Ingredient.CODEC.listOf().fieldOf("ingredients").flatXmap( + l -> l.isEmpty() || l.size() > 9 ? DataResult.error(() -> "Needs 1-9 ingredients") : DataResult.success(NonNullList.of(Ingredient.EMPTY, l.toArray(new Ingredient[0]))), + l -> DataResult.success(List.copyOf(l)) + ).forGetter(DNAHybridizerRecipe::inputs), + ItemStack.CODEC.fieldOf("result").forGetter(DNAHybridizerRecipe::output) + ).apply(inst, DNAHybridizerRecipe::new)); + + public static final StreamCodec STREAM_CODEC = StreamCodec.of( + (buf, r) -> { + buf.writeVarInt(r.inputs().size()); + for(Ingredient i : r.inputs()) Ingredient.CONTENTS_STREAM_CODEC.encode(buf, i); + ItemStack.STREAM_CODEC.encode(buf, r.output()); + }, + buf -> { + int size = buf.readVarInt(); + NonNullList ins = NonNullList.create(); + for(int i=0; i codec() { return CODEC; } + @Override public StreamCodec streamCodec() { return STREAM_CODEC; } + *//*?} else {*/ + @Override public DNAHybridizerRecipe fromJson(ResourceLocation id, JsonObject json) { + JsonArray ings = GsonHelper.getAsJsonArray(json, "ingredients"); + NonNullList inputs = NonNullList.withSize(9, Ingredient.EMPTY); + for (int i = 0; i < Math.min(ings.size(), 8); i++) inputs.set(i, Ingredient.fromJson(ings.get(i))); + if (json.has("catalyst")) inputs.set(8, Ingredient.fromJson(json.get("catalyst"))); + return new DNAHybridizerRecipe(inputs, ShapedRecipe.itemStackFromJson(GsonHelper.getAsJsonObject(json, "result"))); + } + @Override public DNAHybridizerRecipe fromNetwork(ResourceLocation id, FriendlyByteBuf buf) { + NonNullList inputs = NonNullList.withSize(9, Ingredient.EMPTY); + for (int i = 0; i < 9; i++) inputs.set(i, Ingredient.fromNetwork(buf)); + return new DNAHybridizerRecipe(inputs, buf.readItem()); + } + @Override public void toNetwork(FriendlyByteBuf buf, DNAHybridizerRecipe r) { + for (int i = 0; i < 9; i++) (i < r.inputs().size() ? r.inputs().get(i) : Ingredient.EMPTY).toNetwork(buf); + buf.writeItem(r.output()); + } + /*?}*/ + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/recipe/DNAHybridizerRecipeInput.java b/common/src/main/java/net/cmr/jurassicrevived/recipe/DNAHybridizerRecipeInput.java new file mode 100644 index 0000000..fbcd38f --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/recipe/DNAHybridizerRecipeInput.java @@ -0,0 +1,33 @@ +package net.cmr.jurassicrevived.recipe; + +import net.minecraft.world.item.ItemStack; +//? if >1.20.1 { +/*import net.minecraft.world.item.crafting.RecipeInput; +import java.util.List; +*///?} else { +import net.minecraft.world.SimpleContainer; +//?} + +//? if >1.20.1 { +/*public record DNAHybridizerRecipeInput(List inputs) implements RecipeInput { + public DNAHybridizerRecipeInput(ItemStack i0, ItemStack i1, ItemStack i2, ItemStack i3, ItemStack i4, ItemStack i5, ItemStack i6, ItemStack i7, ItemStack i8) { + this(List.of(i0, i1, i2, i3, i4, i5, i6, i7, i8)); + } + + @Override + public ItemStack getItem(int index) { + return inputs.get(index); + } + + @Override + public int size() { + return inputs.size(); + } +} +*///?} else { +public class DNAHybridizerRecipeInput extends SimpleContainer { + public DNAHybridizerRecipeInput(ItemStack i0, ItemStack i1, ItemStack i2, ItemStack i3, ItemStack i4, ItemStack i5, ItemStack i6, ItemStack i7, ItemStack i8) { + super(i0, i1, i2, i3, i4, i5, i6, i7, i8); + } +} +//?} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/recipe/EmbryoCalcificationMachineRecipe.java b/common/src/main/java/net/cmr/jurassicrevived/recipe/EmbryoCalcificationMachineRecipe.java new file mode 100644 index 0000000..177fae5 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/recipe/EmbryoCalcificationMachineRecipe.java @@ -0,0 +1,103 @@ +package net.cmr.jurassicrevived.recipe; + +import net.cmr.jurassicrevived.Constants; +import net.minecraft.core.NonNullList; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.*; +import net.minecraft.world.level.Level; +import org.jetbrains.annotations.NotNull; + +/*? if >1.20.1 {*/ +/*import com.mojang.serialization.MapCodec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import com.mojang.serialization.DataResult; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.core.HolderLookup; +import java.util.List; +*//*?} else {*/ +import com.google.gson.JsonObject; +import com.google.gson.JsonArray; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.util.GsonHelper; +import net.minecraft.core.RegistryAccess; +/*?}*/ + +public record EmbryoCalcificationMachineRecipe(NonNullList inputs, ItemStack output) + implements Recipe { + + @Override + public boolean matches(EmbryoCalcificationMachineRecipeInput recipeInput, Level level) { + if (level.isClientSide) return false; + if (recipeInput.size() < 2 || inputs.size() < 2) return false; + + ItemStack in0 = recipeInput.getItem(0); + ItemStack in1 = recipeInput.getItem(1); + Ingredient a = inputs.get(0); + Ingredient b = inputs.get(1); + + // Symmetric match: (A+B) or (B+A) + return (a.test(in0) && b.test(in1)) || (a.test(in1) && b.test(in0)); + } + + /*? if >1.20.1 {*/ + /*@Override public ItemStack assemble(EmbryoCalcificationMachineRecipeInput input, HolderLookup.Provider p) { return output.copy(); } + @Override public ItemStack getResultItem(HolderLookup.Provider p) { return output.copy(); } + *//*?} else {*/ + @Override public ItemStack assemble(EmbryoCalcificationMachineRecipeInput input, RegistryAccess a) { return output.copy(); } + @Override public ItemStack getResultItem(RegistryAccess a) { return output.copy(); } + @Override public ResourceLocation getId() { return Constants.rl("embryonic_machining"); } + /*?}*/ + + @Override public boolean canCraftInDimensions(int width, int height) { return true; } + @Override public RecipeSerializer getSerializer() { return ModRecipes.EMBRYO_CALCIFICATION_MACHINE_SERIALIZER.get(); } + @Override public RecipeType getType() { return ModRecipes.EMBRYO_CALCIFICATION_MACHINE_RECIPE_TYPE.get(); } + + public static class Serializer implements RecipeSerializer { + /*? if >1.20.1 {*/ + /*public static final MapCodec CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group( + Ingredient.CODEC_NONEMPTY.listOf().fieldOf("ingredients").flatXmap( + list -> list.size() != 2 ? DataResult.error(() -> "Requires exactly 2 ingredients") : DataResult.success(NonNullList.of(Ingredient.EMPTY, list.get(0), list.get(1))), + list -> DataResult.success(List.copyOf(list)) + ).forGetter(EmbryoCalcificationMachineRecipe::inputs), + ItemStack.CODEC.fieldOf("result").forGetter(EmbryoCalcificationMachineRecipe::output) + ).apply(instance, EmbryoCalcificationMachineRecipe::new)); + + public static final StreamCodec STREAM_CODEC = StreamCodec.of( + (buf, recipe) -> { + buf.writeVarInt(recipe.inputs().size()); + for(Ingredient i : recipe.inputs()) Ingredient.CONTENTS_STREAM_CODEC.encode(buf, i); + ItemStack.STREAM_CODEC.encode(buf, recipe.output()); + }, + buf -> { + int size = buf.readVarInt(); + NonNullList ins = NonNullList.create(); + for(int i=0; i codec() { return CODEC; } + @Override public StreamCodec streamCodec() { return STREAM_CODEC; } + *//*?} else {*/ + @Override public EmbryoCalcificationMachineRecipe fromJson(ResourceLocation id, JsonObject json) { + JsonArray ingredients = GsonHelper.getAsJsonArray(json, "ingredients"); + NonNullList inputs = NonNullList.withSize(2, Ingredient.EMPTY); + for (int i = 0; i < 2; i++) inputs.set(i, Ingredient.fromJson(ingredients.get(i))); + ItemStack output = ShapedRecipe.itemStackFromJson(GsonHelper.getAsJsonObject(json, "result")); + return new EmbryoCalcificationMachineRecipe(inputs, output); + } + @Override public EmbryoCalcificationMachineRecipe fromNetwork(ResourceLocation id, FriendlyByteBuf buf) { + NonNullList inputs = NonNullList.withSize(buf.readInt(), Ingredient.EMPTY); + for (int i = 0; i < inputs.size(); i++) inputs.set(i, Ingredient.fromNetwork(buf)); + return new EmbryoCalcificationMachineRecipe(inputs, buf.readItem()); + } + @Override public void toNetwork(FriendlyByteBuf buf, EmbryoCalcificationMachineRecipe recipe) { + buf.writeInt(recipe.inputs().size()); + for (Ingredient ing : recipe.inputs()) ing.toNetwork(buf); + buf.writeItem(recipe.output()); + } + /*?}*/ + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/recipe/EmbryoCalcificationMachineRecipeInput.java b/common/src/main/java/net/cmr/jurassicrevived/recipe/EmbryoCalcificationMachineRecipeInput.java new file mode 100644 index 0000000..338155b --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/recipe/EmbryoCalcificationMachineRecipeInput.java @@ -0,0 +1,47 @@ +package net.cmr.jurassicrevived.recipe; + +import net.minecraft.world.item.ItemStack; +import java.util.List; +import java.util.Objects; + +/*? if >1.20.1 {*/ +/*import net.minecraft.world.item.crafting.RecipeInput; +*//*?} else {*/ +import net.minecraft.world.Container; +import net.minecraft.world.entity.player.Player; +/*?}*/ + +public record EmbryoCalcificationMachineRecipeInput(List inputs) + /*? if >1.20.1 {*/ /*implements RecipeInput *//*?} else {*/ implements Container /*?}*/ { + + public EmbryoCalcificationMachineRecipeInput { + Objects.requireNonNull(inputs, "inputs"); + if (inputs.size() != 2) { + throw new IllegalArgumentException("EmbryoCalcificationMachineRecipeInput requires exactly 2 input stacks"); + } + } + + public EmbryoCalcificationMachineRecipeInput(ItemStack first, ItemStack second) { + this(List.of(first, second)); + } + + @Override + public ItemStack getItem(int i) { + return inputs.get(i); + } + + public int size() { + return inputs.size(); + } + + /*? if <=1.20.1 {*/ + @Override public int getContainerSize() { return size(); } + @Override public boolean isEmpty() { return inputs.stream().allMatch(ItemStack::isEmpty); } + @Override public ItemStack removeItem(int i, int j) { return ItemStack.EMPTY; } + @Override public ItemStack removeItemNoUpdate(int i) { return ItemStack.EMPTY; } + @Override public void setItem(int i, ItemStack itemStack) {} + @Override public void setChanged() {} + @Override public boolean stillValid(Player player) { return true; } + @Override public void clearContent() {} + /*?}*/ +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/recipe/EmbryonicMachineRecipe.java b/common/src/main/java/net/cmr/jurassicrevived/recipe/EmbryonicMachineRecipe.java new file mode 100644 index 0000000..c5b1e33 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/recipe/EmbryonicMachineRecipe.java @@ -0,0 +1,202 @@ +package net.cmr.jurassicrevived.recipe; + +//? if >1.20.1 { +/*import com.mojang.serialization.Codec; +import com.mojang.serialization.DataResult; +import com.mojang.serialization.MapCodec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.minecraft.core.HolderLookup; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +*///?} else { +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import net.minecraft.core.RegistryAccess; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.util.GsonHelper; +//?} + +import net.minecraft.core.NonNullList; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.*; +import net.minecraft.world.level.Level; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.Map; +import java.util.HashMap; + +//? if >1.20.1 { +/*public record EmbryonicMachineRecipe(NonNullList inputs, ItemStack output, Map weights) implements Recipe { + *///?} else { +public class EmbryonicMachineRecipe implements Recipe { + private final NonNullList inputs; + private final ItemStack output; + private final ResourceLocation id; + private final Map weights; +//?} + + //? if <=1.20.1 { + public EmbryonicMachineRecipe(ResourceLocation id, NonNullList inputs, ItemStack output, Map weights) { + this.id = id; + this.inputs = inputs; + this.output = output; + this.weights = Map.copyOf(weights); + } + //?} + + @Override + public boolean matches(@NotNull EmbryonicMachineRecipeInput input, Level level) { + if (level.isClientSide()) return false; + + //? if >1.20.1 { + /*ItemStack in0 = input.getItem(0); + ItemStack in1 = input.getItem(1); + ItemStack in2 = input.getItem(2); + *///?} else { + ItemStack in0 = input.getItem(0); + ItemStack in1 = input.getItem(1); + ItemStack in2 = input.getItem(2); + //?} + + boolean testTubeOk = inputs.get(0).test(in0); + boolean materialOk = inputs.get(1).test(in1); + boolean frogOk = inputs.size() > 2 && inputs.get(2).test(in2); + + return testTubeOk && materialOk && frogOk; + } + + //? if >1.20.1 { + /*@Override + public @NotNull ItemStack assemble(EmbryonicMachineRecipeInput input, HolderLookup.Provider registries) { + return output.copy(); + } + + @Override + public @NotNull ItemStack getResultItem(HolderLookup.Provider registries) { + return output.copy(); + } + *///?} else { + @Override + public ItemStack assemble(EmbryonicMachineRecipeInput input, RegistryAccess registries) { + return output.copy(); + } + + @Override + public ItemStack getResultItem(RegistryAccess registries) { + return output.copy(); + } + + @Override + public ResourceLocation getId() { + return id; + } + //?} + + @Override + public boolean canCraftInDimensions(int width, int height) { + return true; + } + + @Override + public @NotNull NonNullList getIngredients() { + return inputs; + } + + @Override + public @NotNull RecipeSerializer getSerializer() { + return ModRecipes.EMBRYONIC_MACHINE_SERIALIZER.get(); + } + + @Override + public @NotNull RecipeType getType() { + return ModRecipes.EMBRYONIC_MACHINE_RECIPE_TYPE.get(); + } + + public int getWeightFor(Item item) { + ResourceLocation key = BuiltInRegistries.ITEM.getKey(item); + return weights.getOrDefault(key, 1); + } + + //? if >1.20.1 { + /*public Map weights() { return weights; } + public ItemStack output() { return output; } + *///?} + + public static class Serializer implements RecipeSerializer { + //? if >1.20.1 { + /*private static final Codec> WEIGHTS_CODEC = + Codec.unboundedMap(ResourceLocation.CODEC, Codec.INT); + + public static final MapCodec CODEC = RecordCodecBuilder.mapCodec(inst -> inst.group( + Ingredient.CODEC_NONEMPTY.listOf().fieldOf("ingredients").flatXmap(list -> { + if (list.size() != 3) return DataResult.error(() -> "Must have 3 ingredients"); + return DataResult.success(NonNullList.of(Ingredient.EMPTY, list.toArray(Ingredient[]::new))); + }, DataResult::success).forGetter(EmbryonicMachineRecipe::inputs), + ItemStack.CODEC.fieldOf("result").forGetter(EmbryonicMachineRecipe::output), + WEIGHTS_CODEC.optionalFieldOf("weights", Map.of()).forGetter(EmbryonicMachineRecipe::weights) + ).apply(inst, EmbryonicMachineRecipe::new)); + + public static final StreamCodec STREAM_CODEC = StreamCodec.of( + (buf, recipe) -> { + ByteBufCodecs.collection(NonNullList::createWithCapacity, Ingredient.CONTENTS_STREAM_CODEC).encode(buf, recipe.inputs()); + ItemStack.STREAM_CODEC.encode(buf, recipe.output()); + buf.writeMap(recipe.weights(), ResourceLocation.STREAM_CODEC, ByteBufCodecs.VAR_INT); + }, + buf -> { + NonNullList inputs = ByteBufCodecs.collection(NonNullList::createWithCapacity, Ingredient.CONTENTS_STREAM_CODEC).decode(buf); + ItemStack output = ItemStack.STREAM_CODEC.decode(buf); + Map weights = buf.readMap(ResourceLocation.STREAM_CODEC, ByteBufCodecs.VAR_INT); + return new EmbryonicMachineRecipe(inputs, output, weights); + } + ); + + @Override public MapCodec codec() { return CODEC; } + @Override public StreamCodec streamCodec() { return STREAM_CODEC; } + *///?} else { + @Override + public EmbryonicMachineRecipe fromJson(ResourceLocation id, JsonObject json) { + ItemStack output = ShapedRecipe.itemStackFromJson(GsonHelper.getAsJsonObject(json, "result")); + JsonArray ingredients = GsonHelper.getAsJsonArray(json, "ingredients"); + NonNullList inputs = NonNullList.withSize(3, Ingredient.EMPTY); + for (int i = 0; i < inputs.size(); i++) { + inputs.set(i, Ingredient.fromJson(ingredients.get(i))); + } + Map weights = new HashMap<>(); + if (json.has("weights")) { + JsonObject weightsObj = json.getAsJsonObject("weights"); + for (String key : weightsObj.keySet()) { + weights.put(ResourceLocation.tryParse(key), weightsObj.get(key).getAsInt()); + } + } + return new EmbryonicMachineRecipe(id, inputs, output, weights); + } + + @Override + public EmbryonicMachineRecipe fromNetwork(ResourceLocation id, FriendlyByteBuf buf) { + int size = buf.readVarInt(); + NonNullList inputs = NonNullList.withSize(size, Ingredient.EMPTY); + for (int i = 0; i < size; i++) { + inputs.set(i, Ingredient.fromNetwork(buf)); + } + ItemStack output = buf.readItem(); + Map weights = buf.readMap(FriendlyByteBuf::readResourceLocation, FriendlyByteBuf::readVarInt); + return new EmbryonicMachineRecipe(id, inputs, output, weights); + } + + @Override + public void toNetwork(FriendlyByteBuf buf, EmbryonicMachineRecipe recipe) { + buf.writeVarInt(recipe.getIngredients().size()); + for (Ingredient ing : recipe.getIngredients()) { + ing.toNetwork(buf); + } + buf.writeItem(recipe.output); + buf.writeMap(recipe.weights, FriendlyByteBuf::writeResourceLocation, FriendlyByteBuf::writeVarInt); + } + //?} + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/recipe/EmbryonicMachineRecipeInput.java b/common/src/main/java/net/cmr/jurassicrevived/recipe/EmbryonicMachineRecipeInput.java new file mode 100644 index 0000000..ecddfc6 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/recipe/EmbryonicMachineRecipeInput.java @@ -0,0 +1,36 @@ +package net.cmr.jurassicrevived.recipe; + +import net.minecraft.world.item.ItemStack; +//? if >1.20.1 { +/*import net.minecraft.world.item.crafting.RecipeInput; + *///?} else { +import net.minecraft.world.Container; +import net.minecraft.world.SimpleContainer; +//?} + +import java.util.List; + +//? if >1.20.1 { +/*public record EmbryonicMachineRecipeInput(ItemStack testTube, ItemStack material, ItemStack frog) implements RecipeInput { + @Override + public ItemStack getItem(int index) { + return switch (index) { + case 0 -> testTube; + case 1 -> material; + case 2 -> frog; + default -> throw new IllegalArgumentException("Unexpected slot index: " + index); + }; + } + + @Override + public int size() { + return 3; + } +} +*///?} else { +public class EmbryonicMachineRecipeInput extends SimpleContainer { + public EmbryonicMachineRecipeInput(ItemStack testTube, ItemStack material, ItemStack frog) { + super(testTube, material, frog); + } +} +//?} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/recipe/FossilCleanerRecipe.java b/common/src/main/java/net/cmr/jurassicrevived/recipe/FossilCleanerRecipe.java new file mode 100644 index 0000000..6bea462 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/recipe/FossilCleanerRecipe.java @@ -0,0 +1,228 @@ +package net.cmr.jurassicrevived.recipe; + +//? if >1.20.1 { +/*import com.mojang.serialization.Codec; +import com.mojang.serialization.DataResult; +import com.mojang.serialization.MapCodec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.minecraft.core.HolderLookup; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +*///?} else { +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import net.minecraft.core.RegistryAccess; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.util.GsonHelper; +//?} + +import net.cmr.jurassicrevived.util.ModTags; +import net.minecraft.core.NonNullList; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.RandomSource; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.*; +import net.minecraft.world.level.Level; +import org.jetbrains.annotations.NotNull; + +import java.util.Map; +import java.util.HashMap; +import java.util.List; + +//? if >1.20.1 { +/*public record FossilCleanerRecipe(NonNullList inputs, ItemStack output, Map weights) implements Recipe { + *///?} else { +public class FossilCleanerRecipe implements Recipe { + private final NonNullList inputs; + private final ItemStack output; + private final ResourceLocation id; + private final Map weights; +//?} + + //? if <=1.20.1 { + public FossilCleanerRecipe(ResourceLocation id, NonNullList inputs, ItemStack output, Map weights) { + this.id = id; + this.inputs = inputs; + this.output = output; + this.weights = Map.copyOf(weights); + } + //?} + + @Override + public boolean matches(@NotNull FossilCleanerRecipeInput input, Level level) { + if (level.isClientSide()) return false; + if (inputs.isEmpty()) return false; + + // Single fossil block ingredient lives in machine slot 1 (input.getItem(1)) + return inputs.get(0).test(input.getItem(1)); + } + + //? if >1.20.1 { + /*@Override + public @NotNull ItemStack assemble(FossilCleanerRecipeInput input, HolderLookup.Provider registries) { + return getWeightedResult(registries); + } + + @Override + public @NotNull ItemStack getResultItem(HolderLookup.Provider registries) { + return output.copy(); + } + *///?} else { + @Override + public ItemStack assemble(FossilCleanerRecipeInput input, RegistryAccess registries) { + return getWeightedResult(registries); + } + + @Override + public ItemStack getResultItem(RegistryAccess registries) { + return output.copy(); + } + + @Override + public ResourceLocation getId() { + return id; + } + //?} + + private ItemStack getWeightedResult(Object registries) { + //? if >1.20.1 { + /*var fossilTag = BuiltInRegistries.ITEM.getOrCreateTag(ModTags.Items.FOSSILS); + List candidates = fossilTag.stream().map(ItemStack::new).toList(); + *///?} else { + ItemStack[] candidatesArr = Ingredient.of(ModTags.Items.FOSSILS).getItems(); + List candidates = List.of(candidatesArr); + //?} + + if (candidates.isEmpty()) return output.copy(); + + RandomSource random = RandomSource.create(); + long total = 0; + int[] weightsArray = new int[candidates.size()]; + for (int i = 0; i < candidates.size(); i++) { + Item item = candidates.get(i).getItem(); + ResourceLocation key = BuiltInRegistries.ITEM.getKey(item); + int w = Math.max(0, weights.getOrDefault(key, 1)); + weightsArray[i] = w; + total += w; + } + + if (total <= 0) return candidates.get(random.nextInt(candidates.size())).copy(); + + long roll = 1L + (total <= Integer.MAX_VALUE ? random.nextInt((int) total) : Math.abs(random.nextLong()) % total); + long cumulative = 0; + for (int i = 0; i < candidates.size(); i++) { + cumulative += weightsArray[i]; + if (roll <= cumulative) return candidates.get(i).copy(); + } + return candidates.get(candidates.size() - 1).copy(); + } + + @Override + public boolean canCraftInDimensions(int width, int height) { + return true; + } + + @Override + public @NotNull NonNullList getIngredients() { + return inputs; + } + + @Override + public @NotNull RecipeSerializer getSerializer() { + return ModRecipes.FOSSIL_CLEANER_SERIALIZER.get(); + } + + @Override + public @NotNull RecipeType getType() { + return ModRecipes.FOSSIL_CLEANER_RECIPE_TYPE.get(); + } + + public int getWeightFor(Item item) { + ResourceLocation key = BuiltInRegistries.ITEM.getKey(item); + return weights.getOrDefault(key, 1); + } + + //? if <=1.20.1 { + public ItemStack output() { return output; } + //?} + + //? if >1.20.1 { + /*public Map weights() { return weights; } + public ItemStack output() { return output; } + *///?} + + public static class Serializer implements RecipeSerializer { + //? if >1.20.1 { + /*private static final Codec> WEIGHTS_CODEC = + Codec.unboundedMap(ResourceLocation.CODEC, Codec.INT); + + public static final MapCodec CODEC = RecordCodecBuilder.mapCodec(inst -> inst.group( + Ingredient.CODEC_NONEMPTY.listOf().fieldOf("ingredients").flatXmap(list -> { + if (list.size() != 1) return DataResult.error(() -> "Must have 1 ingredient"); + return DataResult.success(NonNullList.of(Ingredient.EMPTY, list.toArray(Ingredient[]::new))); + }, DataResult::success).forGetter(FossilCleanerRecipe::inputs), + ItemStack.CODEC.fieldOf("result").forGetter(FossilCleanerRecipe::output), + WEIGHTS_CODEC.optionalFieldOf("fossil_weights", Map.of()).forGetter(FossilCleanerRecipe::weights) + ).apply(inst, FossilCleanerRecipe::new)); + + public static final StreamCodec STREAM_CODEC = StreamCodec.of( + (buf, recipe) -> { + ByteBufCodecs.collection(NonNullList::createWithCapacity, Ingredient.CONTENTS_STREAM_CODEC).encode(buf, recipe.inputs()); + ItemStack.STREAM_CODEC.encode(buf, recipe.output()); + buf.writeMap(recipe.weights(), ResourceLocation.STREAM_CODEC, ByteBufCodecs.VAR_INT); + }, + buf -> { + NonNullList inputs = ByteBufCodecs.collection(NonNullList::createWithCapacity, Ingredient.CONTENTS_STREAM_CODEC).decode(buf); + ItemStack output = ItemStack.STREAM_CODEC.decode(buf); + Map weights = buf.readMap(ResourceLocation.STREAM_CODEC, ByteBufCodecs.VAR_INT); + return new FossilCleanerRecipe(inputs, output, weights); + } + ); + + @Override public MapCodec codec() { return CODEC; } + @Override public StreamCodec streamCodec() { return STREAM_CODEC; } + *///?} else { + @Override + public FossilCleanerRecipe fromJson(ResourceLocation id, JsonObject json) { + ItemStack output = ShapedRecipe.itemStackFromJson(GsonHelper.getAsJsonObject(json, "result")); + JsonArray ingredients = GsonHelper.getAsJsonArray(json, "ingredients"); + NonNullList inputs = NonNullList.withSize(1, Ingredient.EMPTY); + inputs.set(0, Ingredient.fromJson(ingredients.get(0))); + Map weights = new HashMap<>(); + if (json.has("fossil_weights")) { + JsonObject weightsObj = json.getAsJsonObject("fossil_weights"); + for (String key : weightsObj.keySet()) { + weights.put(ResourceLocation.tryParse(key), weightsObj.get(key).getAsInt()); + } + } + return new FossilCleanerRecipe(id, inputs, output, weights); + } + + @Override + public FossilCleanerRecipe fromNetwork(ResourceLocation id, FriendlyByteBuf buf) { + int size = buf.readVarInt(); + NonNullList inputs = NonNullList.withSize(size, Ingredient.EMPTY); + for (int i = 0; i < size; i++) { + inputs.set(i, Ingredient.fromNetwork(buf)); + } + ItemStack output = buf.readItem(); + Map weights = buf.readMap(FriendlyByteBuf::readResourceLocation, FriendlyByteBuf::readVarInt); + return new FossilCleanerRecipe(id, inputs, output, weights); + } + + @Override + public void toNetwork(FriendlyByteBuf buf, FossilCleanerRecipe recipe) { + buf.writeVarInt(recipe.inputs.size()); + for (Ingredient ing : recipe.inputs) { + ing.toNetwork(buf); + } + buf.writeItem(recipe.output); + buf.writeMap(recipe.weights, FriendlyByteBuf::writeResourceLocation, FriendlyByteBuf::writeVarInt); + } + //?} + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/recipe/FossilCleanerRecipeInput.java b/common/src/main/java/net/cmr/jurassicrevived/recipe/FossilCleanerRecipeInput.java new file mode 100644 index 0000000..9b1dc26 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/recipe/FossilCleanerRecipeInput.java @@ -0,0 +1,34 @@ +package net.cmr.jurassicrevived.recipe; + +import net.minecraft.world.item.ItemStack; +//? if >1.20.1 { +/*import net.minecraft.world.item.crafting.RecipeInput; + *///?} else { +import net.minecraft.world.SimpleContainer; +//?} + +import java.util.List; + +//? if >1.20.1 { +/*public record FossilCleanerRecipeInput(ItemStack material, ItemStack fuel) implements RecipeInput { + @Override + public ItemStack getItem(int index) { + return switch (index) { + case 0 -> material; + case 1 -> fuel; + default -> throw new IllegalArgumentException("Unexpected slot index: " + index); + }; + } + + @Override + public int size() { + return 2; + } +} +*///?} else { +public class FossilCleanerRecipeInput extends SimpleContainer { + public FossilCleanerRecipeInput(ItemStack material, ItemStack fuel) { + super(material, fuel); + } +} +//?} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/recipe/FossilGrinderRecipe.java b/common/src/main/java/net/cmr/jurassicrevived/recipe/FossilGrinderRecipe.java new file mode 100644 index 0000000..38750e2 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/recipe/FossilGrinderRecipe.java @@ -0,0 +1,224 @@ +package net.cmr.jurassicrevived.recipe; + +//? if >1.20.1 { +/*import com.mojang.serialization.Codec; +import com.mojang.serialization.DataResult; +import com.mojang.serialization.MapCodec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.minecraft.core.HolderLookup; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +*///?} else { +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import net.minecraft.core.RegistryAccess; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.util.GsonHelper; +//?} + +import net.minecraft.core.NonNullList; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.RandomSource; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.*; +import net.minecraft.world.level.Level; +import org.jetbrains.annotations.NotNull; + +import java.util.Map; +import java.util.HashMap; +import java.util.List; +import java.util.ArrayList; + +//? if >1.20.1 { +/*public record FossilGrinderRecipe(NonNullList inputs, ItemStack output, Map weights) implements Recipe { + *///?} else { +public class FossilGrinderRecipe implements Recipe { + private final NonNullList inputs; + private final ItemStack output; + private final ResourceLocation id; + private final Map weights; +//?} + + //? if <=1.20.1 { + public FossilGrinderRecipe(ResourceLocation id, NonNullList inputs, ItemStack output, Map weights) { + this.id = id; + this.inputs = inputs; + this.output = output; + this.weights = Map.copyOf(weights); + } + //?} + + @Override + public boolean matches(@NotNull FossilGrinderRecipeInput input, Level level) { + if (level.isClientSide()) return false; + if (inputs.isEmpty()) return false; + return inputs.get(0).test(input.getItem(0)); + } + + //? if >1.20.1 { + /*@Override + public @NotNull ItemStack assemble(FossilGrinderRecipeInput input, HolderLookup.Provider registries) { + return getWeightedResult(); + } + + @Override + public @NotNull ItemStack getResultItem(HolderLookup.Provider registries) { + return output.copy(); + } + *///?} else { + @Override + public ItemStack assemble(FossilGrinderRecipeInput input, RegistryAccess registries) { + return getWeightedResult(); + } + + @Override + public ItemStack getResultItem(RegistryAccess registries) { + return output.copy(); + } + + @Override + public ResourceLocation getId() { + return id; + } + //?} + + private ItemStack getWeightedResult() { + if (weights.isEmpty()) return output.copy(); + + RandomSource random = RandomSource.create(); + long total = 0; + List keys = new ArrayList<>(weights.keySet()); + int[] weightsArray = new int[keys.size()]; + + for (int i = 0; i < keys.size(); i++) { + int w = Math.max(0, weights.getOrDefault(keys.get(i), 0)); + weightsArray[i] = w; + total += w; + } + + if (total <= 0) { + Item fallback = BuiltInRegistries.ITEM.get(keys.get(random.nextInt(keys.size()))); + return new ItemStack(fallback); + } + + long roll = 1L + (total <= Integer.MAX_VALUE ? random.nextInt((int) total) : Math.abs(random.nextLong()) % total); + long cumulative = 0; + for (int i = 0; i < keys.size(); i++) { + cumulative += weightsArray[i]; + if (roll <= cumulative) { + Item chosen = BuiltInRegistries.ITEM.get(keys.get(i)); + return new ItemStack(chosen); + } + } + return new ItemStack(BuiltInRegistries.ITEM.get(keys.get(keys.size() - 1))); + } + + @Override + public boolean canCraftInDimensions(int width, int height) { + return true; + } + + @Override + public @NotNull NonNullList getIngredients() { + return inputs; + } + + @Override + public @NotNull RecipeSerializer getSerializer() { + return ModRecipes.FOSSIL_GRINDER_SERIALIZER.get(); + } + + @Override + public @NotNull RecipeType getType() { + return ModRecipes.FOSSIL_GRINDER_RECIPE_TYPE.get(); + } + + //? if <=1.20.1 { + public Map weights() { return weights; } + //?} + + //? if >1.20.1 { + /*public Map weights() { return weights; } + public ItemStack output() { return output; } + *///?} + + public int getWeightFor(Item item) { + ResourceLocation key = BuiltInRegistries.ITEM.getKey(item); + return weights.getOrDefault(key, 0); + } + + public static class Serializer implements RecipeSerializer { + //? if >1.20.1 { + /*private static final Codec> WEIGHTS_CODEC = + Codec.unboundedMap(ResourceLocation.CODEC, Codec.INT); + + public static final MapCodec CODEC = RecordCodecBuilder.mapCodec(inst -> inst.group( + Ingredient.CODEC_NONEMPTY.listOf().fieldOf("ingredients").flatXmap(list -> { + if (list.size() != 1) return DataResult.error(() -> "Must have 1 ingredient"); + return DataResult.success(NonNullList.of(Ingredient.EMPTY, list.toArray(Ingredient[]::new))); + }, DataResult::success).forGetter(FossilGrinderRecipe::inputs), + ItemStack.CODEC.fieldOf("result").forGetter(FossilGrinderRecipe::output), + WEIGHTS_CODEC.optionalFieldOf("weighted_outputs", Map.of()).forGetter(FossilGrinderRecipe::weights) + ).apply(inst, FossilGrinderRecipe::new)); + + public static final StreamCodec STREAM_CODEC = StreamCodec.of( + (buf, recipe) -> { + ByteBufCodecs.collection(NonNullList::createWithCapacity, Ingredient.CONTENTS_STREAM_CODEC).encode(buf, recipe.inputs()); + ItemStack.STREAM_CODEC.encode(buf, recipe.output()); + buf.writeMap(recipe.weights(), ResourceLocation.STREAM_CODEC, ByteBufCodecs.VAR_INT); + }, + buf -> { + NonNullList inputs = ByteBufCodecs.collection(NonNullList::createWithCapacity, Ingredient.CONTENTS_STREAM_CODEC).decode(buf); + ItemStack output = ItemStack.STREAM_CODEC.decode(buf); + Map weights = buf.readMap(ResourceLocation.STREAM_CODEC, ByteBufCodecs.VAR_INT); + return new FossilGrinderRecipe(inputs, output, weights); + } + ); + + @Override public MapCodec codec() { return CODEC; } + @Override public StreamCodec streamCodec() { return STREAM_CODEC; } + *///?} else { + @Override + public FossilGrinderRecipe fromJson(ResourceLocation id, JsonObject json) { + ItemStack output = ShapedRecipe.itemStackFromJson(GsonHelper.getAsJsonObject(json, "result")); + JsonArray ingredients = GsonHelper.getAsJsonArray(json, "ingredients"); + NonNullList inputs = NonNullList.withSize(1, Ingredient.EMPTY); + inputs.set(0, Ingredient.fromJson(ingredients.get(0))); + Map weights = new HashMap<>(); + if (json.has("weighted_outputs")) { + JsonObject weightsObj = json.getAsJsonObject("weighted_outputs"); + for (String key : weightsObj.keySet()) { + weights.put(ResourceLocation.tryParse(key), weightsObj.get(key).getAsInt()); + } + } + return new FossilGrinderRecipe(id, inputs, output, weights); + } + + @Override + public FossilGrinderRecipe fromNetwork(ResourceLocation id, FriendlyByteBuf buf) { + int size = buf.readVarInt(); + NonNullList inputs = NonNullList.withSize(size, Ingredient.EMPTY); + for (int i = 0; i < size; i++) { + inputs.set(i, Ingredient.fromNetwork(buf)); + } + ItemStack output = buf.readItem(); + Map weights = buf.readMap(FriendlyByteBuf::readResourceLocation, FriendlyByteBuf::readVarInt); + return new FossilGrinderRecipe(id, inputs, output, weights); + } + + @Override + public void toNetwork(FriendlyByteBuf buf, FossilGrinderRecipe recipe) { + buf.writeVarInt(recipe.inputs.size()); + for (Ingredient ing : recipe.inputs) { + ing.toNetwork(buf); + } + buf.writeItem(recipe.output); + buf.writeMap(recipe.weights, FriendlyByteBuf::writeResourceLocation, FriendlyByteBuf::writeVarInt); + } + //?} + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/recipe/FossilGrinderRecipeInput.java b/common/src/main/java/net/cmr/jurassicrevived/recipe/FossilGrinderRecipeInput.java new file mode 100644 index 0000000..c3f36b4 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/recipe/FossilGrinderRecipeInput.java @@ -0,0 +1,29 @@ +package net.cmr.jurassicrevived.recipe; + +import net.minecraft.world.item.ItemStack; +//? if >1.20.1 { +/*import net.minecraft.world.item.crafting.RecipeInput; + *///?} else { +import net.minecraft.world.SimpleContainer; +//?} + +//? if >1.20.1 { +/*public record FossilGrinderRecipeInput(ItemStack input) implements RecipeInput { + @Override + public ItemStack getItem(int index) { + if (index == 0) return input; + throw new IllegalArgumentException("Unexpected slot index: " + index); + } + + @Override + public int size() { + return 1; + } +} +*///?} else { +public class FossilGrinderRecipeInput extends SimpleContainer { + public FossilGrinderRecipeInput(ItemStack input) { + super(input); + } +} +//?} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/recipe/IncubatorRecipe.java b/common/src/main/java/net/cmr/jurassicrevived/recipe/IncubatorRecipe.java new file mode 100644 index 0000000..4770bc3 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/recipe/IncubatorRecipe.java @@ -0,0 +1,158 @@ +package net.cmr.jurassicrevived.recipe; + +//? if >1.20.1 { +/*import com.mojang.serialization.DataResult; +import com.mojang.serialization.MapCodec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.minecraft.core.HolderLookup; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +*///?} else { +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import net.minecraft.core.RegistryAccess; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.util.GsonHelper; +//?} + +import net.minecraft.core.NonNullList; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.*; +import net.minecraft.world.level.Level; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +//? if >1.20.1 { +/*public record IncubatorRecipe(NonNullList inputs, ItemStack output) implements Recipe { + *///?} else { +public class IncubatorRecipe implements Recipe { + private final NonNullList inputs; + private final ItemStack output; + private final ResourceLocation id; +//?} + + //? if <=1.20.1 { + public IncubatorRecipe(ResourceLocation id, NonNullList inputs, ItemStack output) { + this.id = id; + this.inputs = inputs; + this.output = output; + } + //?} + + @Override + public boolean matches(@NotNull IncubatorRecipeInput input, Level level) { + if (level.isClientSide()) return false; + if (inputs.isEmpty()) return false; + return inputs.get(0).test(input.getItem(0)); + } + + //? if >1.20.1 { + /*@Override + public @NotNull ItemStack assemble(IncubatorRecipeInput input, HolderLookup.Provider registries) { + return output.copy(); + } + + @Override + public @NotNull ItemStack getResultItem(HolderLookup.Provider registries) { + return output.copy(); + } + *///?} else { + @Override + public ItemStack assemble(IncubatorRecipeInput input, RegistryAccess registries) { + return output.copy(); + } + + @Override + public ItemStack getResultItem(RegistryAccess registries) { + return output.copy(); + } + + @Override + public ResourceLocation getId() { + return id; + } + //?} + + @Override + public boolean canCraftInDimensions(int width, int height) { + return true; + } + + @Override + public @NotNull NonNullList getIngredients() { + return inputs; + } + + @Override + public @NotNull RecipeSerializer getSerializer() { + return ModRecipes.INCUBATOR_SERIALIZER.get(); + } + + @Override + public @NotNull RecipeType getType() { + return ModRecipes.INCUBATOR_RECIPE_TYPE.get(); + } + + //? if >1.20.1 { + /*public ItemStack output() { return output; } + *///?} + + public static class Serializer implements RecipeSerializer { + //? if >1.20.1 { + /*public static final MapCodec CODEC = RecordCodecBuilder.mapCodec(inst -> inst.group( + Ingredient.CODEC_NONEMPTY.listOf().fieldOf("ingredients").flatXmap(list -> { + if (list.size() != 1) return DataResult.error(() -> "Must have 1 ingredient"); + return DataResult.success(NonNullList.of(Ingredient.EMPTY, list.toArray(Ingredient[]::new))); + }, DataResult::success).forGetter(IncubatorRecipe::inputs), + ItemStack.CODEC.fieldOf("result").forGetter(IncubatorRecipe::output) + ).apply(inst, IncubatorRecipe::new)); + + public static final StreamCodec STREAM_CODEC = StreamCodec.of( + (buf, recipe) -> { + ByteBufCodecs.collection(NonNullList::createWithCapacity, Ingredient.CONTENTS_STREAM_CODEC).encode(buf, recipe.inputs()); + ItemStack.STREAM_CODEC.encode(buf, recipe.output()); + }, + buf -> { + NonNullList inputs = ByteBufCodecs.collection(NonNullList::createWithCapacity, Ingredient.CONTENTS_STREAM_CODEC).decode(buf); + ItemStack output = ItemStack.STREAM_CODEC.decode(buf); + return new IncubatorRecipe(inputs, output); + } + ); + + @Override public MapCodec codec() { return CODEC; } + @Override public StreamCodec streamCodec() { return STREAM_CODEC; } + *///?} else { + @Override + public IncubatorRecipe fromJson(ResourceLocation id, JsonObject json) { + JsonArray arr = GsonHelper.getAsJsonArray(json, "ingredients"); + NonNullList inputs = NonNullList.withSize(1, Ingredient.EMPTY); + inputs.set(0, Ingredient.fromJson(arr.get(0))); + ItemStack output = ShapedRecipe.itemStackFromJson(GsonHelper.getAsJsonObject(json, "result")); + return new IncubatorRecipe(id, inputs, output); + } + + @Override + public IncubatorRecipe fromNetwork(ResourceLocation id, FriendlyByteBuf buf) { + int size = buf.readVarInt(); + NonNullList inputs = NonNullList.withSize(size, Ingredient.EMPTY); + for (int i = 0; i < size; i++) { + inputs.set(i, Ingredient.fromNetwork(buf)); + } + ItemStack output = buf.readItem(); + return new IncubatorRecipe(id, inputs, output); + } + + @Override + public void toNetwork(FriendlyByteBuf buf, IncubatorRecipe recipe) { + buf.writeVarInt(recipe.inputs.size()); + for (Ingredient ing : recipe.inputs) { + ing.toNetwork(buf); + } + buf.writeItem(recipe.output); + } + //?} + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/recipe/IncubatorRecipeInput.java b/common/src/main/java/net/cmr/jurassicrevived/recipe/IncubatorRecipeInput.java new file mode 100644 index 0000000..742ac70 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/recipe/IncubatorRecipeInput.java @@ -0,0 +1,29 @@ +package net.cmr.jurassicrevived.recipe; + +import net.minecraft.world.item.ItemStack; +//? if >1.20.1 { +/*import net.minecraft.world.item.crafting.RecipeInput; + *///?} else { +import net.minecraft.world.SimpleContainer; +//?} + +//? if >1.20.1 { +/*public record IncubatorRecipeInput(ItemStack input) implements RecipeInput { + @Override + public ItemStack getItem(int index) { + if (index == 0) return input; + throw new IllegalArgumentException("Unexpected slot index: " + index); + } + + @Override + public int size() { + return 1; + } +} +*///?} else { +public class IncubatorRecipeInput extends SimpleContainer { + public IncubatorRecipeInput(ItemStack input) { + super(input); + } +} +//?} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/recipe/ModRecipes.java b/common/src/main/java/net/cmr/jurassicrevived/recipe/ModRecipes.java new file mode 100644 index 0000000..9009d23 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/recipe/ModRecipes.java @@ -0,0 +1,100 @@ +package net.cmr.jurassicrevived.recipe; + +import dev.architectury.registry.registries.DeferredRegister; +import dev.architectury.registry.registries.RegistrySupplier; +import net.cmr.jurassicrevived.Constants; +import net.minecraft.core.registries.Registries; +import net.minecraft.world.item.crafting.RecipeSerializer; +import net.minecraft.world.item.crafting.RecipeType; + +public class ModRecipes { + public static final DeferredRegister> SERIALIZERS = + DeferredRegister.create(Constants.MOD_ID, Registries.RECIPE_SERIALIZER); + public static final DeferredRegister> TYPES = + DeferredRegister.create(Constants.MOD_ID, Registries.RECIPE_TYPE); + + public static final RegistrySupplier> DNA_EXTRACTOR_SERIALIZER = + SERIALIZERS.register("dna_extracting", DNAExtractorRecipe.Serializer::new); + public static final RegistrySupplier> DNA_EXTRACTOR_RECIPE_TYPE = + TYPES.register("dna_extracting", () -> new RecipeType() { + @Override + public String toString() { + return "dna_extracting"; + } + }); + + public static final RegistrySupplier> DNA_ANALYZER_SERIALIZER = + SERIALIZERS.register("dna_analyzing", DNAAnalyzerRecipe.Serializer::new); + public static final RegistrySupplier> DNA_ANALYZER_RECIPE_TYPE = + TYPES.register("dna_analyzing", () -> new RecipeType() { + @Override + public String toString() { + return "dna_analyzing"; + } + }); + + public static final RegistrySupplier> FOSSIL_GRINDER_SERIALIZER = + SERIALIZERS.register("fossil_grinding", FossilGrinderRecipe.Serializer::new); + public static final RegistrySupplier> FOSSIL_GRINDER_RECIPE_TYPE = + TYPES.register("fossil_grinding", () -> new RecipeType() { + @Override + public String toString() { + return "fossil_grinding"; + } + }); + + public static final RegistrySupplier> FOSSIL_CLEANER_SERIALIZER = + SERIALIZERS.register("fossil_cleaning", FossilCleanerRecipe.Serializer::new); + public static final RegistrySupplier> FOSSIL_CLEANER_RECIPE_TYPE = + TYPES.register("fossil_cleaning", () -> new RecipeType() { + @Override + public String toString() { + return "fossil_cleaning"; + } + }); + + public static final RegistrySupplier> DNA_HYBRIDIZER_SERIALIZER = + SERIALIZERS.register("dna_hybridizing", DNAHybridizerRecipe.Serializer::new); + public static final RegistrySupplier> DNA_HYBRIDIZER_RECIPE_TYPE = + TYPES.register("dna_hybridizing", () -> new RecipeType() { + @Override + public String toString() { + return "dna_hybridizing"; + } + }); + + public static final RegistrySupplier> EMBRYONIC_MACHINE_SERIALIZER = + SERIALIZERS.register("embryonic_machining", EmbryonicMachineRecipe.Serializer::new); + public static final RegistrySupplier> EMBRYONIC_MACHINE_RECIPE_TYPE = + TYPES.register("embryonic_machining", () -> new RecipeType() { + @Override + public String toString() { + return "embryonic_machining"; + } + }); + + public static final RegistrySupplier> EMBRYO_CALCIFICATION_MACHINE_SERIALIZER = + SERIALIZERS.register("embryo_calcification_machining", EmbryoCalcificationMachineRecipe.Serializer::new); + public static final RegistrySupplier> EMBRYO_CALCIFICATION_MACHINE_RECIPE_TYPE = + TYPES.register("embryo_calcification_machining", () -> new RecipeType() { + @Override + public String toString() { + return "embryo_calcification_machining"; + } + }); + + public static final RegistrySupplier> INCUBATOR_SERIALIZER = + SERIALIZERS.register("incubating", IncubatorRecipe.Serializer::new); + public static final RegistrySupplier> INCUBATOR_RECIPE_TYPE = + TYPES.register("incubating", () -> new RecipeType() { + @Override + public String toString() { + return "incubating"; + } + }); + + public static void register() { + SERIALIZERS.register(); + TYPES.register(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/screen/ModMenuTypes.java b/common/src/main/java/net/cmr/jurassicrevived/screen/ModMenuTypes.java new file mode 100644 index 0000000..5dcdbf3 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/screen/ModMenuTypes.java @@ -0,0 +1,45 @@ +package net.cmr.jurassicrevived.screen; + +import dev.architectury.registry.menu.MenuRegistry; +import dev.architectury.registry.registries.DeferredRegister; +import dev.architectury.registry.registries.RegistrySupplier; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.screen.custom.*; +import net.minecraft.core.registries.Registries; +import net.minecraft.world.inventory.MenuType; + +public class ModMenuTypes { + public static final DeferredRegister> MENUS = + DeferredRegister.create(Constants.MOD_ID, Registries.MENU); + + public static final RegistrySupplier> GENERATOR_MENU = + MENUS.register("generator_menu", () -> MenuRegistry.ofExtended(GeneratorMenu::new)); + public static final RegistrySupplier> DNA_EXTRACTOR_MENU = + MENUS.register("dna_extractor_menu", () -> MenuRegistry.ofExtended(DNAExtractorMenu::new)); + public static final RegistrySupplier> DNA_ANALYZER_MENU = + MENUS.register("dna_analyzer_menu", () -> MenuRegistry.ofExtended(DNAAnalyzerMenu::new)); + public static final RegistrySupplier> FOSSIL_GRINDER_MENU = + MENUS.register("fossil_grinder_menu", () -> MenuRegistry.ofExtended(FossilGrinderMenu::new)); + public static final RegistrySupplier> FOSSIL_CLEANER_MENU = + MENUS.register("fossil_cleaner_menu", () -> MenuRegistry.ofExtended(FossilCleanerMenu::new)); + public static final RegistrySupplier> DNA_HYBRIDIZER_MENU = + MENUS.register("dna_hybridizer_menu", () -> MenuRegistry.ofExtended(DNAHybridizerMenu::new)); + public static final RegistrySupplier> EMBRYONIC_MACHINE_MENU = + MENUS.register("embryonic_machine_menu", () -> MenuRegistry.ofExtended(EmbryonicMachineMenu::new)); + public static final RegistrySupplier> EMBRYO_CALCIFICATION_MACHINE_MENU = + MENUS.register("embryo_calcification_machine_menu", () -> MenuRegistry.ofExtended(EmbryoCalcificationMachineMenu::new)); + public static final RegistrySupplier> INCUBATOR_MENU = + MENUS.register("incubator_menu", () -> MenuRegistry.ofExtended(IncubatorMenu::new)); + public static final RegistrySupplier> TANK_MENU = + MENUS.register("tank_menu", () -> MenuRegistry.ofExtended(TankMenu::new)); + public static final RegistrySupplier> POWER_CELL_MENU = + MENUS.register("power_cell_menu", () -> MenuRegistry.ofExtended(PowerCellMenu::new)); + public static final RegistrySupplier> WOOD_CRATE_MENU = + MENUS.register("wood_crate_menu", () -> MenuRegistry.ofExtended(CrateMenu::new)); + public static final RegistrySupplier> IRON_CRATE_MENU = + MENUS.register("iron_crate_menu", () -> MenuRegistry.ofExtended(CrateMenu::new)); + + public static void register() { + MENUS.register(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/screen/custom/CrateMenu.java b/common/src/main/java/net/cmr/jurassicrevived/screen/custom/CrateMenu.java new file mode 100644 index 0000000..68a6d9b --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/screen/custom/CrateMenu.java @@ -0,0 +1,112 @@ +package net.cmr.jurassicrevived.screen.custom; + +import net.cmr.jurassicrevived.block.entity.custom.CrateBlockEntity; +import net.cmr.jurassicrevived.screen.ModMenuTypes; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ContainerLevelAccess; +import net.minecraft.world.inventory.MenuType; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; + +public class CrateMenu extends AbstractContainerMenu { + public final CrateBlockEntity blockEntity; + private final Level level; + + public CrateMenu(int containerId, Inventory inventory, FriendlyByteBuf data) { + this(containerId, inventory, (CrateBlockEntity) inventory.player.level().getBlockEntity(data.readBlockPos())); + } + + public CrateMenu(int containerId, Inventory inventory, CrateBlockEntity entity) { + super(resolveType(entity), containerId); + this.blockEntity = entity; + this.level = inventory.player.level(); + + addPlayerInventory(inventory); + addPlayerHotbar(inventory); + + int size = entity.getSize(); + if (size == 9) { + // One row of 9 at (8,62) + int startX = 8; + int startY = 62; + for (int c = 0; c < 9; c++) { + this.addSlot(new Slot(entity.itemHandler, c, startX + c * 18, startY)); + } + } else { + // 18 slots: two rows of 9, y=44 and y=62 + int startX = 8; + int row1Y = 44; + int row2Y = 62; + for (int c = 0; c < 9; c++) { + this.addSlot(new Slot(entity.itemHandler, c, startX + c * 18, row1Y)); + } + for (int c = 0; c < 9; c++) { + this.addSlot(new Slot(entity.itemHandler, 9 + c, startX + c * 18, row2Y)); + } + } + } + + private static MenuType resolveType(CrateBlockEntity be) { + return be.getSize() == 9 ? ModMenuTypes.WOOD_CRATE_MENU.get() : ModMenuTypes.IRON_CRATE_MENU.get(); + } + + // Inventory move logic + private static final int HOTBAR_SLOT_COUNT = 9; + private static final int PLAYER_INVENTORY_ROW_COUNT = 3; + private static final int PLAYER_INVENTORY_COLUMN_COUNT = 9; + private static final int PLAYER_INVENTORY_SLOT_COUNT = PLAYER_INVENTORY_COLUMN_COUNT * PLAYER_INVENTORY_ROW_COUNT; + private static final int VANILLA_SLOT_COUNT = HOTBAR_SLOT_COUNT + PLAYER_INVENTORY_SLOT_COUNT; + private static final int VANILLA_FIRST_SLOT_INDEX = 0; + + @Override + public ItemStack quickMoveStack(Player playerIn, int index) { + Slot source = slots.get(index); + if (source == null || !source.hasItem()) return ItemStack.EMPTY; + ItemStack sourceStack = source.getItem(); + ItemStack copy = sourceStack.copy(); + + int teSlots = blockEntity.itemHandler.getContainerSize(); + int teFirst = VANILLA_FIRST_SLOT_INDEX + VANILLA_SLOT_COUNT; + int teEnd = teFirst + teSlots; + + if (index < VANILLA_FIRST_SLOT_INDEX + VANILLA_SLOT_COUNT) { + if (!moveItemStackTo(sourceStack, teFirst, teEnd, false)) { + return ItemStack.EMPTY; + } + } else if (index >= teFirst && index < teEnd) { + if (!moveItemStackTo(sourceStack, VANILLA_FIRST_SLOT_INDEX, VANILLA_FIRST_SLOT_INDEX + VANILLA_SLOT_COUNT, false)) { + return ItemStack.EMPTY; + } + } else { + return ItemStack.EMPTY; + } + + if (sourceStack.isEmpty()) source.set(ItemStack.EMPTY); + else source.setChanged(); + source.onTake(playerIn, sourceStack); + return copy; + } + + @Override + public boolean stillValid(Player player) { + return stillValid(ContainerLevelAccess.create(level, blockEntity.getBlockPos()), player, blockEntity.getBlockState().getBlock()); + } + + private void addPlayerInventory(Inventory inv) { + for (int r = 0; r < 3; ++r) { + for (int c = 0; c < 9; ++c) { + this.addSlot(new Slot(inv, c + r * 9 + 9, 8 + c * 18, 84 + r * 18)); + } + } + } + + private void addPlayerHotbar(Inventory inv) { + for (int c = 0; c < 9; ++c) { + this.addSlot(new Slot(inv, c, 8 + c * 18, 142)); + } + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/screen/custom/CrateScreen.java b/common/src/main/java/net/cmr/jurassicrevived/screen/custom/CrateScreen.java new file mode 100644 index 0000000..102c707 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/screen/custom/CrateScreen.java @@ -0,0 +1,45 @@ +package net.cmr.jurassicrevived.screen.custom; + +import net.cmr.jurassicrevived.Constants; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.player.Inventory; + +public class CrateScreen extends AbstractContainerScreen { + private static final ResourceLocation WOOD_CRATE = + Constants.rl("textures/gui/crate/wood_crate_gui.png"); + private static final ResourceLocation IRON_CRATE = + Constants.rl("textures/gui/crate/iron_crate_gui.png"); + + public CrateScreen(CrateMenu menu, Inventory inv, Component title) { + super(menu, inv, title); + } + + @Override + protected void init() { + super.init(); + this.inventoryLabelY = 10000; + this.titleLabelY = 10000; + } + + @Override + protected void renderBg(GuiGraphics g, float partial, int mouseX, int mouseY) { + var tex = menu.blockEntity.getSize() == 9 ? WOOD_CRATE : IRON_CRATE; + int x = (width - imageWidth) / 2; + int y = (height - imageHeight) / 2; + g.blit(tex, x, y, 0, 0, imageWidth, imageHeight, 176, 166); + } + + @Override + public void render(GuiGraphics g, int mouseX, int mouseY, float delta) { + //? if >1.20.1 { + /*this.renderBackground(g, mouseX, mouseY, delta); + *///?} else { + renderBackground(g); + //?} + super.render(g, mouseX, mouseY, delta); + renderTooltip(g, mouseX, mouseY); + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/screen/custom/DNAAnalyzerMenu.java b/common/src/main/java/net/cmr/jurassicrevived/screen/custom/DNAAnalyzerMenu.java new file mode 100644 index 0000000..bb2eeb7 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/screen/custom/DNAAnalyzerMenu.java @@ -0,0 +1,130 @@ +package net.cmr.jurassicrevived.screen.custom; + +import net.cmr.jurassicrevived.block.entity.custom.DNAAnalyzerBlockEntity; +import net.cmr.jurassicrevived.item.ModItems; +import net.cmr.jurassicrevived.screen.ModMenuTypes; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.*; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; + +public class DNAAnalyzerMenu extends AbstractContainerMenu { + public final DNAAnalyzerBlockEntity blockEntity; + public final Level level; + public final ContainerData data; + + public DNAAnalyzerMenu(int containerId, Inventory inventory, FriendlyByteBuf data) { + this(containerId, inventory, inventory.player.level().getBlockEntity(data.readBlockPos()), new SimpleContainerData(2)); + } + + public DNAAnalyzerMenu(int containerId, Inventory inventory, BlockEntity entity, ContainerData data) { + super(ModMenuTypes.DNA_ANALYZER_MENU.get(), containerId); + blockEntity = ((DNAAnalyzerBlockEntity) entity); + this.level = inventory.player.level(); + this.data = data; + + addPlayerInventory(inventory); + addPlayerHotbar(inventory); + + // Test Tube Slot (0) + this.addSlot(new Slot(blockEntity.itemHandler, 0, 57, 35) { + @Override + public boolean mayPlace(ItemStack stack) { + return stack.getItem() == ModItems.TEST_TUBE.get(); + } + }); + + // Material Slot (1) + this.addSlot(new Slot(blockEntity.itemHandler, 1, 80, 7) { + @Override + public boolean mayPlace(ItemStack stack) { + return stack.getItem() == ModItems.FROG_MATERIAL.get(); + } + }); + + // Output Slot (2) + this.addSlot(new Slot(blockEntity.itemHandler, 2, 80, 63) { + @Override + public boolean mayPlace(ItemStack stack) { + return false; + } + }); + addDataSlots(data); + } + + public boolean isCrafting() { + return data.get(0) > 0; + } + + public int getScaledArrowProgress() { + int progress = this.data.get(0); + int maxProgress = this.data.get(1); + int arrowPixelSize = 16; + + return maxProgress != 0 && progress != 0 ? progress * arrowPixelSize / maxProgress : 0; + } + + private static final int HOTBAR_SLOT_COUNT = 9; + private static final int PLAYER_INVENTORY_ROW_COUNT = 3; + private static final int PLAYER_INVENTORY_COLUMN_COUNT = 9; + private static final int PLAYER_INVENTORY_SLOT_COUNT = PLAYER_INVENTORY_COLUMN_COUNT * PLAYER_INVENTORY_ROW_COUNT; + private static final int VANILLA_SLOT_COUNT = HOTBAR_SLOT_COUNT + PLAYER_INVENTORY_SLOT_COUNT; + private static final int VANILLA_FIRST_SLOT_INDEX = 0; + private static final int TE_INVENTORY_FIRST_SLOT_INDEX = VANILLA_SLOT_COUNT; + private static final int TE_INVENTORY_SLOT_COUNT = 3; + + @Override + public ItemStack quickMoveStack(Player playerIn, int pIndex) { + Slot sourceSlot = slots.get(pIndex); + if (sourceSlot == null || !sourceSlot.hasItem()) return ItemStack.EMPTY; + ItemStack sourceStack = sourceSlot.getItem(); + ItemStack copyOfSourceStack = sourceStack.copy(); + + if (pIndex < VANILLA_SLOT_COUNT) { + // Player inventory -> machine input (indices 0 and 1) + int teInputStart = TE_INVENTORY_FIRST_SLOT_INDEX; + int teInputEndExclusive = TE_INVENTORY_FIRST_SLOT_INDEX + 2; + if (!moveItemStackTo(sourceStack, teInputStart, teInputEndExclusive, false)) { + return ItemStack.EMPTY; + } + } else if (pIndex < TE_INVENTORY_FIRST_SLOT_INDEX + TE_INVENTORY_SLOT_COUNT) { + // Machine -> player inventory + if (!moveItemStackTo(sourceStack, VANILLA_FIRST_SLOT_INDEX, VANILLA_SLOT_COUNT, false)) { + return ItemStack.EMPTY; + } + } else { + return ItemStack.EMPTY; + } + + if (sourceStack.isEmpty()) { + sourceSlot.set(ItemStack.EMPTY); + } else { + sourceSlot.setChanged(); + } + sourceSlot.onTake(playerIn, sourceStack); + return copyOfSourceStack; + } + + @Override + public boolean stillValid(Player player) { + return stillValid(ContainerLevelAccess.create(this.level, this.blockEntity.getBlockPos()), + player, this.blockEntity.getBlockState().getBlock()); + } + + public void addPlayerInventory(Inventory playerInventory) { + for (int i = 0; i < 3; i++) { + for (int l = 0; l < 9; l++) { + this.addSlot(new Slot(playerInventory, l + i * 9 + 9, 8 + l * 18, 84 + i * 18)); + } + } + } + + public void addPlayerHotbar(Inventory playerInventory) { + for (int i = 0; i < 9; i++) { + this.addSlot(new Slot(playerInventory, i, 8 + i * 18, 142)); + } + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/screen/custom/DNAAnalyzerScreen.java b/common/src/main/java/net/cmr/jurassicrevived/screen/custom/DNAAnalyzerScreen.java new file mode 100644 index 0000000..fdb0276 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/screen/custom/DNAAnalyzerScreen.java @@ -0,0 +1,128 @@ +package net.cmr.jurassicrevived.screen.custom; + +import com.mojang.blaze3d.systems.RenderSystem; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.config.JRConfigManager; +import net.cmr.jurassicrevived.screen.renderer.EnergyDisplayTooltipArea; +import net.cmr.jurassicrevived.util.MouseUtil; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.player.Inventory; + +import java.util.Optional; + +public class DNAAnalyzerScreen extends AbstractContainerScreen { + private static final ResourceLocation GUI_TEXTURE = + Constants.rl("textures/gui/dna_analyzer/dna_analyzer_gui.png"); + private static final ResourceLocation DNA_TEXTURE = + Constants.rl("textures/gui/generic/syringe_dna.png"); + private static final ResourceLocation POWER_BAR_TEXTURE = + Constants.rl("textures/gui/generic/power_bar.png"); + private static final ResourceLocation TEST_TUBE_TEXTURE = + Constants.rl("textures/gui/generic/test_tube.png"); + private static final ResourceLocation AMBER_TEXTURE = + Constants.rl("textures/gui/generic/syringe.png"); + private EnergyDisplayTooltipArea energyInfoArea; + + public DNAAnalyzerScreen(DNAAnalyzerMenu menu, Inventory playerInventory, Component title) { + super(menu, playerInventory, title); + } + + @Override + protected void init() { + super.init(); + + this.inventoryLabelY = 10000; + this.titleLabelY = 10000; + + assignEnergyInfoArea(); + } + + private void renderEnergyAreaTooltip(GuiGraphics guiGraphics, int pMouseX, int pMouseY, int x, int y) { + if(isMouseAboveArea(pMouseX, pMouseY, x, y, 160, 11, 8, 64) && JRConfigManager.get().requirePower) { + guiGraphics.renderTooltip(this.font, energyInfoArea.getTooltips(), + Optional.empty(), pMouseX - x, pMouseY - y); + } + } + + private void assignEnergyInfoArea() { + energyInfoArea = new EnergyDisplayTooltipArea(((width - imageWidth) / 2) + 160, + ((height - imageHeight) / 2) + 11, menu.blockEntity.getEnergyStorage(null)); + } + + @Override + protected void renderLabels(GuiGraphics guiGraphics, int pMouseX, int pMouseY) { + int x = (width - imageWidth) / 2; + int y = (height - imageHeight) / 2; + + if (JRConfigManager.get().requirePower) { + renderEnergyAreaTooltip(guiGraphics, pMouseX, pMouseY, x, y); + } + } + + @Override + protected void renderBg(GuiGraphics guiGraphics, float v, int i, int i1) { + RenderSystem.setShader(GameRenderer::getPositionTexShader); + RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F); + RenderSystem.setShaderTexture(0, GUI_TEXTURE); + int x = (this.width - this.imageWidth) / 2; + int y = (this.height - this.imageHeight) /2; + + guiGraphics.blit(GUI_TEXTURE, x, y, 0, 0, this.imageWidth, this.imageHeight, 176, 166); + + guiGraphics.blit(TEST_TUBE_TEXTURE, x + 57, y + 35, 0, 0, 16, 16, 16, 16); + guiGraphics.blit(AMBER_TEXTURE, x + 80, y + 7, 0, 0, 16, 16, 16, 16); + + + if (JRConfigManager.get().requirePower) { + guiGraphics.blit(POWER_BAR_TEXTURE, x+159, y+10, 0, 0, 10, 66, 10, 66); + + } + } + + private void RenderProgressArrow(GuiGraphics guiGraphics, int x, int y) { + if (menu.isCrafting()) { + int fullW = 6, fullH = 16; + int visible = Mth.clamp(menu.getScaledArrowProgress(), 0, fullH); + + // Top-down drain: draw the remaining part starting below the drained portion + int remaining = fullH - visible; + int srcU = 0; + int srcV = visible; + + int drawX = x + 85; + int drawY = y + 38 + visible; + + if (remaining > 0) { + guiGraphics.blit(DNA_TEXTURE, drawX, drawY, srcU, srcV, fullW, remaining, fullW, fullH); + } + } + } + + @Override + public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { + //? if >1.20.1 { + /*this.renderBackground(guiGraphics, mouseX, mouseY, partialTick); + *///?} else { + renderBackground(guiGraphics); + //?} + super.render(guiGraphics, mouseX, mouseY, partialTick); + renderTooltip(guiGraphics, mouseX, mouseY); + int x = (this.width - this.imageWidth) / 2; + int y = (this.height - this.imageHeight) /2; + + RenderProgressArrow(guiGraphics, x, y); + + if (JRConfigManager.get().requirePower) { + energyInfoArea.render(guiGraphics); + } + } + + public static boolean isMouseAboveArea(int pMouseX, int pMouseY, int x, int y, int offsetX, int offsetY, int width, int height) { + return MouseUtil.isMouseOver(pMouseX, pMouseY, x + offsetX, y + offsetY, width, height); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/screen/custom/DNAExtractorMenu.java b/common/src/main/java/net/cmr/jurassicrevived/screen/custom/DNAExtractorMenu.java new file mode 100644 index 0000000..af3cb90 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/screen/custom/DNAExtractorMenu.java @@ -0,0 +1,142 @@ +package net.cmr.jurassicrevived.screen.custom; + +import net.cmr.jurassicrevived.block.entity.custom.DNAExtractorBlockEntity; +import net.cmr.jurassicrevived.item.ModItems; +import net.cmr.jurassicrevived.screen.ModMenuTypes; +import net.cmr.jurassicrevived.util.ModTags; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.*; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; + +public class DNAExtractorMenu extends AbstractContainerMenu { + public final DNAExtractorBlockEntity blockEntity; + public final Level level; + public final ContainerData data; + + public DNAExtractorMenu(int containerId, Inventory inventory, FriendlyByteBuf data) { + this(containerId, inventory, inventory.player.level().getBlockEntity(data.readBlockPos()), new SimpleContainerData(2)); + } + + public DNAExtractorMenu(int containerId, Inventory inventory, BlockEntity entity, ContainerData data) { + super(ModMenuTypes.DNA_EXTRACTOR_MENU.get(), containerId); + blockEntity = ((DNAExtractorBlockEntity) entity); + this.level = inventory.player.level(); + this.data = data; + + addPlayerInventory(inventory); + addPlayerHotbar(inventory); + + this.addSlot(new Slot(blockEntity.itemHandler, 0, 57, 35) { + @Override + public boolean mayPlace(ItemStack stack) { + return stack.getItem() == ModItems.TEST_TUBE.get(); + } + }); + + this.addSlot(new Slot(blockEntity.itemHandler, 1, 80, 7) { + @Override + public boolean mayPlace(ItemStack stack) { + return stack.is(ModTags.Items.TISSUES) || stack.getItem() == ModItems.MOSQUITO_IN_AMBER.get(); + } + }); + + this.addSlot(new Slot(blockEntity.itemHandler, 2, 62, 63) { + @Override + public boolean mayPlace(ItemStack stack) { + return false; + } + }); + this.addSlot(new Slot(blockEntity.itemHandler, 3, 80, 63) { + @Override + public boolean mayPlace(ItemStack stack) { + return false; + } + }); + this.addSlot(new Slot(blockEntity.itemHandler, 4, 98, 63) { + @Override + public boolean mayPlace(ItemStack stack) { + return false; + } + }); + + addDataSlots(data); + } + + public boolean isCrafting() { + return data.get(0) > 0; + } + + public int getScaledArrowProgress() { + int progress = this.data.get(0); + int maxProgress = this.data.get(1); + int arrowPixelSize = 16; + + return maxProgress != 0 && progress != 0 ? progress * arrowPixelSize / maxProgress : 0; + } + + // Inventory move logic constants + private static final int HOTBAR_SLOT_COUNT = 9; + private static final int PLAYER_INVENTORY_ROW_COUNT = 3; + private static final int PLAYER_INVENTORY_COLUMN_COUNT = 9; + private static final int PLAYER_INVENTORY_SLOT_COUNT = PLAYER_INVENTORY_COLUMN_COUNT * PLAYER_INVENTORY_ROW_COUNT; + private static final int VANILLA_SLOT_COUNT = HOTBAR_SLOT_COUNT + PLAYER_INVENTORY_SLOT_COUNT; + private static final int VANILLA_FIRST_SLOT_INDEX = 0; + private static final int TE_INVENTORY_FIRST_SLOT_INDEX = VANILLA_SLOT_COUNT; + private static final int TE_INVENTORY_SLOT_COUNT = 5; + + @Override + public ItemStack quickMoveStack(Player playerIn, int pIndex) { + Slot sourceSlot = slots.get(pIndex); + if (sourceSlot == null || !sourceSlot.hasItem()) return ItemStack.EMPTY; + ItemStack sourceStack = sourceSlot.getItem(); + ItemStack copyOfSourceStack = sourceStack.copy(); + + if (pIndex < VANILLA_SLOT_COUNT) { + // Player inventory -> machine inputs + int teInputStart = TE_INVENTORY_FIRST_SLOT_INDEX; + int teInputEndExclusive = TE_INVENTORY_FIRST_SLOT_INDEX + 2; + if (!moveItemStackTo(sourceStack, teInputStart, teInputEndExclusive, false)) { + return ItemStack.EMPTY; + } + } else if (pIndex < TE_INVENTORY_FIRST_SLOT_INDEX + TE_INVENTORY_SLOT_COUNT) { + // Machine -> player inventory + if (!moveItemStackTo(sourceStack, VANILLA_FIRST_SLOT_INDEX, VANILLA_SLOT_COUNT, false)) { + return ItemStack.EMPTY; + } + } else { + return ItemStack.EMPTY; + } + + if (sourceStack.isEmpty()) { + sourceSlot.set(ItemStack.EMPTY); + } else { + sourceSlot.setChanged(); + } + sourceSlot.onTake(playerIn, sourceStack); + return copyOfSourceStack; + } + + @Override + public boolean stillValid(Player player) { + return stillValid(ContainerLevelAccess.create(this.level, this.blockEntity.getBlockPos()), + player, this.blockEntity.getBlockState().getBlock()); + } + + public void addPlayerInventory(Inventory playerInventory) { + for (int i = 0; i < 3; i++) { + for (int l = 0; l < 9; l++) { + this.addSlot(new Slot(playerInventory, l + i * 9 + 9, 8 + l * 18, 84 + i * 18)); + } + } + } + + public void addPlayerHotbar(Inventory playerInventory) { + for (int i = 0; i < 9; i++) { + this.addSlot(new Slot(playerInventory, i, 8 + i * 18, 142)); + } + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/screen/custom/DNAExtractorScreen.java b/common/src/main/java/net/cmr/jurassicrevived/screen/custom/DNAExtractorScreen.java new file mode 100644 index 0000000..b21493b --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/screen/custom/DNAExtractorScreen.java @@ -0,0 +1,124 @@ +package net.cmr.jurassicrevived.screen.custom; + +import com.mojang.blaze3d.systems.RenderSystem; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.config.JRConfigManager; +import net.cmr.jurassicrevived.screen.renderer.EnergyDisplayTooltipArea; +import net.cmr.jurassicrevived.util.MouseUtil; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.player.Inventory; + +import java.util.Optional; + +public class DNAExtractorScreen extends AbstractContainerScreen { + private static final ResourceLocation GUI_TEXTURE = + Constants.rl("textures/gui/dna_extractor/dna_extractor_gui.png"); + private static final ResourceLocation DNA_TEXTURE = + Constants.rl("textures/gui/generic/dna.png"); + private static final ResourceLocation POWER_BAR_TEXTURE = + Constants.rl("textures/gui/generic/power_bar.png"); + private static final ResourceLocation TEST_TUBE_TEXTURE = + Constants.rl("textures/gui/generic/test_tube.png"); + private static final ResourceLocation AMBER_TEXTURE = + Constants.rl("textures/gui/dna_extractor/amber.png"); + private EnergyDisplayTooltipArea energyInfoArea; + + public DNAExtractorScreen(DNAExtractorMenu menu, Inventory playerInventory, Component title) { + super(menu, playerInventory, title); + } + + @Override + protected void init() { + super.init(); + + this.inventoryLabelY = 10000; + this.titleLabelY = 10000; + + assignEnergyInfoArea(); + } + + private void renderEnergyAreaTooltip(GuiGraphics guiGraphics, int pMouseX, int pMouseY, int x, int y) { + if(isMouseAboveArea(pMouseX, pMouseY, x, y, 160, 11, 8, 64) && JRConfigManager.get().requirePower) { + guiGraphics.renderTooltip(this.font, energyInfoArea.getTooltips(), + Optional.empty(), pMouseX - x, pMouseY - y); + } + } + + private void assignEnergyInfoArea() { + energyInfoArea = new EnergyDisplayTooltipArea(((width - imageWidth) / 2) + 160, + ((height - imageHeight) / 2) + 11, menu.blockEntity.getEnergyStorage(null)); + } + + @Override + protected void renderLabels(GuiGraphics guiGraphics, int pMouseX, int pMouseY) { + int x = (width - imageWidth) / 2; + int y = (height - imageHeight) / 2; + + if (JRConfigManager.get().requirePower) { + renderEnergyAreaTooltip(guiGraphics, pMouseX, pMouseY, x, y); + } + } + + @Override + protected void renderBg(GuiGraphics guiGraphics, float v, int i, int i1) { + RenderSystem.setShader(GameRenderer::getPositionTexShader); + RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F); + RenderSystem.setShaderTexture(0, GUI_TEXTURE); + int x = (this.width - this.imageWidth) / 2; + int y = (this.height - this.imageHeight) /2; + + guiGraphics.blit(GUI_TEXTURE, x, y, 0, 0, this.imageWidth, this.imageHeight, 176, 166); + + guiGraphics.blit(TEST_TUBE_TEXTURE, x + 57, y + 35, 0, 0, 16, 16, 16, 16); + guiGraphics.blit(AMBER_TEXTURE, x + 80, y + 7, 0, 0, 16, 16, 16, 16); + + + if (JRConfigManager.get().requirePower) { + guiGraphics.blit(POWER_BAR_TEXTURE, x+159, y+10, 0, 0, 10, 66, 10, 66); + + } + } + + private void RenderProgressArrow(GuiGraphics guiGraphics, int x, int y) { + if (menu.isCrafting()) { + int fullW = 8, fullH = 16; + int visible = Mth.clamp(menu.getScaledArrowProgress(), 0, fullH); + + int srcU = 0; + int srcV = fullH - visible; + + int drawX = x + 84; + int drawY = y + 38 + (fullH - visible); + + guiGraphics.blit(DNA_TEXTURE, drawX, drawY, srcU, srcV, fullW, visible, fullW, fullH); + } + } + + @Override + public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { + //? if >1.20.1 { + /*this.renderBackground(guiGraphics, mouseX, mouseY, partialTick); + *///?} else { + renderBackground(guiGraphics); + //?} + super.render(guiGraphics, mouseX, mouseY, partialTick); + renderTooltip(guiGraphics, mouseX, mouseY); + int x = (this.width - this.imageWidth) / 2; + int y = (this.height - this.imageHeight) /2; + + RenderProgressArrow(guiGraphics, x, y); + + if (JRConfigManager.get().requirePower) { + energyInfoArea.render(guiGraphics); + } + } + + public static boolean isMouseAboveArea(int pMouseX, int pMouseY, int x, int y, int offsetX, int offsetY, int width, int height) { + return MouseUtil.isMouseOver(pMouseX, pMouseY, x + offsetX, y + offsetY, width, height); + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/screen/custom/DNAHybridizerMenu.java b/common/src/main/java/net/cmr/jurassicrevived/screen/custom/DNAHybridizerMenu.java new file mode 100644 index 0000000..930ef25 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/screen/custom/DNAHybridizerMenu.java @@ -0,0 +1,137 @@ +package net.cmr.jurassicrevived.screen.custom; + +import net.cmr.jurassicrevived.block.entity.custom.DNAHybridizerBlockEntity; +import net.cmr.jurassicrevived.item.ModItems; +import net.cmr.jurassicrevived.screen.ModMenuTypes; +import net.cmr.jurassicrevived.util.ModTags; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.*; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; + +public class DNAHybridizerMenu extends AbstractContainerMenu { + public final DNAHybridizerBlockEntity blockEntity; + public final Level level; + public final ContainerData data; + + public DNAHybridizerMenu(int containerId, Inventory inventory, FriendlyByteBuf data) { + this(containerId, inventory, inventory.player.level().getBlockEntity(data.readBlockPos()), new SimpleContainerData(2)); + } + + public DNAHybridizerMenu(int containerId, Inventory inventory, BlockEntity entity, ContainerData data) { + super(ModMenuTypes.DNA_HYBRIDIZER_MENU.get(), containerId); + blockEntity = ((DNAHybridizerBlockEntity) entity); + this.level = inventory.player.level(); + this.data = data; + + addPlayerInventory(inventory); + addPlayerHotbar(inventory); + + // DNA Slots 0-7 + for (int i = 0; i < 8; i++) { + int x = 8 + (i % 4) * 18; + int y = 25 + (i / 4) * 18; + this.addSlot(new Slot(blockEntity.itemHandler, i, x, y) { + @Override + public boolean mayPlace(ItemStack stack) { + return stack.is(ModTags.Items.DNA); + } + }); + } + + // Catalyst Slot (8) + this.addSlot(new Slot(blockEntity.itemHandler, 8, 83, 35) { + @Override + public boolean mayPlace(ItemStack stack) { + return stack.getItem() == ModItems.FROG_DNA.get(); + } + }); + + // Output Slot (9) + this.addSlot(new Slot(blockEntity.itemHandler, 9, 134, 35) { + @Override + public boolean mayPlace(ItemStack stack) { + return false; + } + }); + + addDataSlots(data); + } + + public boolean isCrafting() { + return data.get(0) > 0; + } + + public int getScaledArrowProgress() { + int progress = this.data.get(0); + int maxProgress = this.data.get(1); + int arrowPixelSize = 24; + + return maxProgress != 0 && progress != 0 ? progress * arrowPixelSize / maxProgress : 0; + } + + // Inventory move logic constants + private static final int HOTBAR_SLOT_COUNT = 9; + private static final int PLAYER_INVENTORY_ROW_COUNT = 3; + private static final int PLAYER_INVENTORY_COLUMN_COUNT = 9; + private static final int PLAYER_INVENTORY_SLOT_COUNT = PLAYER_INVENTORY_COLUMN_COUNT * PLAYER_INVENTORY_ROW_COUNT; + private static final int VANILLA_SLOT_COUNT = HOTBAR_SLOT_COUNT + PLAYER_INVENTORY_SLOT_COUNT; + private static final int VANILLA_FIRST_SLOT_INDEX = 0; + private static final int TE_INVENTORY_FIRST_SLOT_INDEX = VANILLA_SLOT_COUNT; + private static final int TE_INVENTORY_SLOT_COUNT = 10; + + @Override + public ItemStack quickMoveStack(Player playerIn, int pIndex) { + Slot sourceSlot = slots.get(pIndex); + if (sourceSlot == null || !sourceSlot.hasItem()) return ItemStack.EMPTY; + ItemStack sourceStack = sourceSlot.getItem(); + ItemStack copyOfSourceStack = sourceStack.copy(); + + if (pIndex < VANILLA_SLOT_COUNT) { + // Player inventory -> machine inputs (DNA slots + Catalyst) + int teInputStart = TE_INVENTORY_FIRST_SLOT_INDEX; + int teInputEndExclusive = TE_INVENTORY_FIRST_SLOT_INDEX + 9; + if (!moveItemStackTo(sourceStack, teInputStart, teInputEndExclusive, false)) { + return ItemStack.EMPTY; + } + } else if (pIndex < TE_INVENTORY_FIRST_SLOT_INDEX + TE_INVENTORY_SLOT_COUNT) { + // Machine -> player inventory + if (!moveItemStackTo(sourceStack, VANILLA_FIRST_SLOT_INDEX, VANILLA_SLOT_COUNT, false)) { + return ItemStack.EMPTY; + } + } else { + return ItemStack.EMPTY; + } + + if (sourceStack.isEmpty()) { + sourceSlot.set(ItemStack.EMPTY); + } else { + sourceSlot.setChanged(); + } + sourceSlot.onTake(playerIn, sourceStack); + return copyOfSourceStack; + } + + @Override + public boolean stillValid(Player player) { + return stillValid(ContainerLevelAccess.create(this.level, this.blockEntity.getBlockPos()), + player, this.blockEntity.getBlockState().getBlock()); + } + + public void addPlayerInventory(Inventory playerInventory) { + for (int i = 0; i < 3; i++) { + for (int l = 0; l < 9; l++) { + this.addSlot(new Slot(playerInventory, l + i * 9 + 9, 8 + l * 18, 84 + i * 18)); + } + } + } + + public void addPlayerHotbar(Inventory playerInventory) { + for (int i = 0; i < 9; i++) { + this.addSlot(new Slot(playerInventory, i, 8 + i * 18, 142)); + } + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/screen/custom/DNAHybridizerScreen.java b/common/src/main/java/net/cmr/jurassicrevived/screen/custom/DNAHybridizerScreen.java new file mode 100644 index 0000000..3b79333 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/screen/custom/DNAHybridizerScreen.java @@ -0,0 +1,109 @@ +package net.cmr.jurassicrevived.screen.custom; + +import com.mojang.blaze3d.systems.RenderSystem; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.config.JRConfigManager; +import net.cmr.jurassicrevived.screen.renderer.EnergyDisplayTooltipArea; +import net.cmr.jurassicrevived.util.MouseUtil; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.player.Inventory; + +import java.util.Optional; + +public class DNAHybridizerScreen extends AbstractContainerScreen { + private static final ResourceLocation GUI_TEXTURE = + Constants.rl("textures/gui/dna_hybridizer/dna_hybridizer_gui.png"); + private static final ResourceLocation ARROW_TEXTURE = + Constants.rl("textures/gui/generic/arrow.png"); + private static final ResourceLocation WHITE_ARROW_TEXTURE = + Constants.rl("textures/gui/generic/white_arrow.png"); + private static final ResourceLocation POWER_BAR_TEXTURE = + Constants.rl("textures/gui/generic/power_bar.png"); + private EnergyDisplayTooltipArea energyInfoArea; + + public DNAHybridizerScreen(DNAHybridizerMenu menu, Inventory playerInventory, Component title) { + super(menu, playerInventory, title); + } + + @Override + protected void init() { + super.init(); + + this.inventoryLabelY = 10000; + this.titleLabelY = 10000; + + assignEnergyInfoArea(); + } + + private void renderEnergyAreaTooltip(GuiGraphics guiGraphics, int pMouseX, int pMouseY, int x, int y) { + if(isMouseAboveArea(pMouseX, pMouseY, x, y, 160, 11, 8, 64) && JRConfigManager.get().requirePower) { + guiGraphics.renderTooltip(this.font, energyInfoArea.getTooltips(), + Optional.empty(), pMouseX - x, pMouseY - y); + } + } + + private void assignEnergyInfoArea() { + energyInfoArea = new EnergyDisplayTooltipArea(((width - imageWidth) / 2) + 160, + ((height - imageHeight) / 2) + 11, menu.blockEntity.getEnergyStorage(null)); + } + + @Override + protected void renderLabels(GuiGraphics guiGraphics, int pMouseX, int pMouseY) { + int x = (width - imageWidth) / 2; + int y = (height - imageHeight) / 2; + + if (JRConfigManager.get().requirePower) { + renderEnergyAreaTooltip(guiGraphics, pMouseX, pMouseY, x, y); + } + } + + @Override + protected void renderBg(GuiGraphics guiGraphics, float v, int i, int i1) { + RenderSystem.setShader(GameRenderer::getPositionTexShader); + RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F); + RenderSystem.setShaderTexture(0, GUI_TEXTURE); + int x = (this.width - this.imageWidth) / 2; + int y = (this.height - this.imageHeight) /2; + + guiGraphics.blit(GUI_TEXTURE, x, y, 0, 0, this.imageWidth, this.imageHeight, 176, 166); + guiGraphics.blit(ARROW_TEXTURE, x + 104, y + 35, 0, 0, 24, 16, 24, 16); + + if (JRConfigManager.get().requirePower) { + guiGraphics.blit(POWER_BAR_TEXTURE, x+159, y+10, 0, 0, 10, 66, 10, 66); + + } + } + + private void RenderProgressArrow(GuiGraphics guiGraphics, int x, int y) { + if(menu.isCrafting()) { + guiGraphics.blit(WHITE_ARROW_TEXTURE, x+104, y + 35, 0, 0, menu.getScaledArrowProgress(), 16, 24, 16); + } + } + + @Override + public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { + //? if >1.20.1 { + /*this.renderBackground(guiGraphics, mouseX, mouseY, partialTick); + *///?} else { + renderBackground(guiGraphics); + //?} + super.render(guiGraphics, mouseX, mouseY, partialTick); + renderTooltip(guiGraphics, mouseX, mouseY); + int x = (this.width - this.imageWidth) / 2; + int y = (this.height - this.imageHeight) /2; + + RenderProgressArrow(guiGraphics, x, y); + + if (JRConfigManager.get().requirePower) { + energyInfoArea.render(guiGraphics); + } + } + + public static boolean isMouseAboveArea(int pMouseX, int pMouseY, int x, int y, int offsetX, int offsetY, int width, int height) { + return MouseUtil.isMouseOver(pMouseX, pMouseY, x + offsetX, y + offsetY, width, height); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/screen/custom/EmbryoCalcificationMachineMenu.java b/common/src/main/java/net/cmr/jurassicrevived/screen/custom/EmbryoCalcificationMachineMenu.java new file mode 100644 index 0000000..74f8545 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/screen/custom/EmbryoCalcificationMachineMenu.java @@ -0,0 +1,130 @@ +package net.cmr.jurassicrevived.screen.custom; + +import net.cmr.jurassicrevived.block.entity.custom.EmbryoCalcificationMachineBlockEntity; +import net.cmr.jurassicrevived.screen.ModMenuTypes; +import net.cmr.jurassicrevived.util.ModTags; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.*; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; + +public class EmbryoCalcificationMachineMenu extends AbstractContainerMenu { + public final EmbryoCalcificationMachineBlockEntity blockEntity; + public final Level level; + public final ContainerData data; + + public EmbryoCalcificationMachineMenu(int containerId, Inventory inventory, FriendlyByteBuf data) { + this(containerId, inventory, inventory.player.level().getBlockEntity(data.readBlockPos()), new SimpleContainerData(2)); + } + + public EmbryoCalcificationMachineMenu(int containerId, Inventory inventory, BlockEntity entity, ContainerData data) { + super(ModMenuTypes.EMBRYO_CALCIFICATION_MACHINE_MENU.get(), containerId); + blockEntity = ((EmbryoCalcificationMachineBlockEntity) entity); + this.level = inventory.player.level(); + this.data = data; + + addPlayerInventory(inventory); + addPlayerHotbar(inventory); + + this.addSlot(new Slot(blockEntity.itemHandler, 0, 39, 35) { + @Override + public boolean mayPlace(ItemStack stack) { + return stack.is(ModTags.Items.SYRINGES); + } + }); + + this.addSlot(new Slot(blockEntity.itemHandler, 1, 57, 35) { + @Override + public boolean mayPlace(ItemStack stack) { + return stack.is(Items.EGG); + } + }); + + this.addSlot(new Slot(blockEntity.itemHandler, 2, 103, 35) { + @Override + public boolean mayPlace(ItemStack stack) { + return false; + } + }); + + addDataSlots(data); + } + + public boolean isCrafting() { + return data.get(0) > 0; + } + + public int getScaledArrowProgress() { + int progress = this.data.get(0); + int maxProgress = this.data.get(1); + int arrowPixelSize = 24; + + return maxProgress != 0 && progress != 0 ? progress * arrowPixelSize / maxProgress : 0; + } + + // Inventory move logic constants + private static final int HOTBAR_SLOT_COUNT = 9; + private static final int PLAYER_INVENTORY_ROW_COUNT = 3; + private static final int PLAYER_INVENTORY_COLUMN_COUNT = 9; + private static final int PLAYER_INVENTORY_SLOT_COUNT = PLAYER_INVENTORY_COLUMN_COUNT * PLAYER_INVENTORY_ROW_COUNT; + private static final int VANILLA_SLOT_COUNT = HOTBAR_SLOT_COUNT + PLAYER_INVENTORY_SLOT_COUNT; + private static final int VANILLA_FIRST_SLOT_INDEX = 0; + private static final int TE_INVENTORY_FIRST_SLOT_INDEX = VANILLA_SLOT_COUNT; + private static final int TE_INVENTORY_SLOT_COUNT = 3; + + @Override + public ItemStack quickMoveStack(Player playerIn, int pIndex) { + Slot sourceSlot = slots.get(pIndex); + if (sourceSlot == null || !sourceSlot.hasItem()) return ItemStack.EMPTY; + ItemStack sourceStack = sourceSlot.getItem(); + ItemStack copyOfSourceStack = sourceStack.copy(); + + if (pIndex < VANILLA_SLOT_COUNT) { + // Player inventory -> machine inputs + int teInputStart = TE_INVENTORY_FIRST_SLOT_INDEX; + int teInputEndExclusive = TE_INVENTORY_FIRST_SLOT_INDEX + 2; + if (!moveItemStackTo(sourceStack, teInputStart, teInputEndExclusive, false)) { + return ItemStack.EMPTY; + } + } else if (pIndex < TE_INVENTORY_FIRST_SLOT_INDEX + TE_INVENTORY_SLOT_COUNT) { + // Machine -> player inventory + if (!moveItemStackTo(sourceStack, VANILLA_FIRST_SLOT_INDEX, VANILLA_SLOT_COUNT, false)) { + return ItemStack.EMPTY; + } + } else { + return ItemStack.EMPTY; + } + + if (sourceStack.isEmpty()) { + sourceSlot.set(ItemStack.EMPTY); + } else { + sourceSlot.setChanged(); + } + sourceSlot.onTake(playerIn, sourceStack); + return copyOfSourceStack; + } + + @Override + public boolean stillValid(Player player) { + return stillValid(ContainerLevelAccess.create(this.level, this.blockEntity.getBlockPos()), + player, this.blockEntity.getBlockState().getBlock()); + } + + public void addPlayerInventory(Inventory playerInventory) { + for (int i = 0; i < 3; i++) { + for (int l = 0; l < 9; l++) { + this.addSlot(new Slot(playerInventory, l + i * 9 + 9, 8 + l * 18, 84 + i * 18)); + } + } + } + + public void addPlayerHotbar(Inventory playerInventory) { + for (int i = 0; i < 9; i++) { + this.addSlot(new Slot(playerInventory, i, 8 + i * 18, 142)); + } + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/screen/custom/EmbryoCalcificationMachineScreen.java b/common/src/main/java/net/cmr/jurassicrevived/screen/custom/EmbryoCalcificationMachineScreen.java new file mode 100644 index 0000000..77882b7 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/screen/custom/EmbryoCalcificationMachineScreen.java @@ -0,0 +1,116 @@ +package net.cmr.jurassicrevived.screen.custom; + +import com.mojang.blaze3d.systems.RenderSystem; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.config.JRConfigManager; +import net.cmr.jurassicrevived.screen.renderer.EnergyDisplayTooltipArea; +import net.cmr.jurassicrevived.util.MouseUtil; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.player.Inventory; + +import java.util.Optional; + +public class EmbryoCalcificationMachineScreen extends AbstractContainerScreen { + private static final ResourceLocation GUI_TEXTURE = + Constants.rl("textures/gui/embryo_calcification_machine/embryo_calcification_machine_gui.png"); + private static final ResourceLocation SYRINGE_BAR_TEXTURE = + Constants.rl("textures/gui/generic/syringe_bar.png"); + private static final ResourceLocation WHITE_SYRINGE_BAR_TEXTURE = + Constants.rl("textures/gui/generic/white_syringe_bar.png"); + private static final ResourceLocation POWER_BAR_TEXTURE = + Constants.rl("textures/gui/generic/power_bar.png"); + private static final ResourceLocation SYRINGE_TEXTURE = + Constants.rl("textures/gui/generic/syringe.png"); + private static final ResourceLocation EGG_TEXTURE = + Constants.rl("textures/gui/generic/egg.png"); + private EnergyDisplayTooltipArea energyInfoArea; + + public EmbryoCalcificationMachineScreen(EmbryoCalcificationMachineMenu menu, Inventory playerInventory, Component title) { + super(menu, playerInventory, title); + } + + @Override + protected void init() { + super.init(); + + this.inventoryLabelY = 10000; + this.titleLabelY = 10000; + + assignEnergyInfoArea(); + } + + private void renderEnergyAreaTooltip(GuiGraphics guiGraphics, int pMouseX, int pMouseY, int x, int y) { + if(isMouseAboveArea(pMouseX, pMouseY, x, y, 160, 11, 8, 64) && JRConfigManager.get().requirePower) { + guiGraphics.renderTooltip(this.font, energyInfoArea.getTooltips(), + Optional.empty(), pMouseX - x, pMouseY - y); + } + } + + private void assignEnergyInfoArea() { + energyInfoArea = new EnergyDisplayTooltipArea(((width - imageWidth) / 2) + 160, + ((height - imageHeight) / 2) + 11, menu.blockEntity.getEnergyStorage(null)); + } + + @Override + protected void renderLabels(GuiGraphics guiGraphics, int pMouseX, int pMouseY) { + int x = (width - imageWidth) / 2; + int y = (height - imageHeight) / 2; + + if (JRConfigManager.get().requirePower) { + renderEnergyAreaTooltip(guiGraphics, pMouseX, pMouseY, x, y); + } + } + + @Override + protected void renderBg(GuiGraphics guiGraphics, float v, int i, int i1) { + RenderSystem.setShader(GameRenderer::getPositionTexShader); + RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F); + RenderSystem.setShaderTexture(0, GUI_TEXTURE); + int x = (this.width - this.imageWidth) / 2; + int y = (this.height - this.imageHeight) /2; + + guiGraphics.blit(GUI_TEXTURE, x, y, 0, 0, this.imageWidth, this.imageHeight, 176, 166); + guiGraphics.blit(WHITE_SYRINGE_BAR_TEXTURE, x + 76, y + 35, 0, 0, 24, 16, 24, 16); + guiGraphics.blit(SYRINGE_TEXTURE, x + 39, y + 35, 0, 0, 16, 16, 16, 16); + guiGraphics.blit(EGG_TEXTURE, x + 57, y + 35, 0, 0, 16, 16, 16, 16); + + + if (JRConfigManager.get().requirePower) { + guiGraphics.blit(POWER_BAR_TEXTURE, x+159, y+10, 0, 0, 10, 66, 10, 66); + + } + } + + private void RenderProgressArrow(GuiGraphics guiGraphics, int x, int y) { + if(menu.isCrafting()) { + guiGraphics.blit(SYRINGE_BAR_TEXTURE, x+76, y + 35, 0, 0, menu.getScaledArrowProgress(), 16, 24, 16); + } + } + + @Override + public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { + //? if >1.20.1 { + /*this.renderBackground(guiGraphics, mouseX, mouseY, partialTick); + *///?} else { + renderBackground(guiGraphics); + //?} + super.render(guiGraphics, mouseX, mouseY, partialTick); + renderTooltip(guiGraphics, mouseX, mouseY); + int x = (this.width - this.imageWidth) / 2; + int y = (this.height - this.imageHeight) /2; + + RenderProgressArrow(guiGraphics, x, y); + + if (JRConfigManager.get().requirePower) { + energyInfoArea.render(guiGraphics); + } + } + + public static boolean isMouseAboveArea(int pMouseX, int pMouseY, int x, int y, int offsetX, int offsetY, int width, int height) { + return MouseUtil.isMouseOver(pMouseX, pMouseY, x + offsetX, y + offsetY, width, height); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/screen/custom/EmbryonicMachineMenu.java b/common/src/main/java/net/cmr/jurassicrevived/screen/custom/EmbryonicMachineMenu.java new file mode 100644 index 0000000..b2fbdf2 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/screen/custom/EmbryonicMachineMenu.java @@ -0,0 +1,141 @@ +package net.cmr.jurassicrevived.screen.custom; + +import net.cmr.jurassicrevived.block.entity.custom.EmbryonicMachineBlockEntity; +import net.cmr.jurassicrevived.item.ModItems; +import net.cmr.jurassicrevived.screen.ModMenuTypes; +import net.cmr.jurassicrevived.util.ModTags; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.*; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; + +public class EmbryonicMachineMenu extends AbstractContainerMenu { + public final EmbryonicMachineBlockEntity blockEntity; + public final Level level; + public final ContainerData data; + + public EmbryonicMachineMenu(int containerId, Inventory inventory, FriendlyByteBuf data) { + this(containerId, inventory, inventory.player.level().getBlockEntity(data.readBlockPos()), new SimpleContainerData(2)); + } + + public EmbryonicMachineMenu(int containerId, Inventory inventory, BlockEntity entity, ContainerData data) { + super(ModMenuTypes.EMBRYONIC_MACHINE_MENU.get(), containerId); + blockEntity = ((EmbryonicMachineBlockEntity) entity); + this.level = inventory.player.level(); + this.data = data; + + addPlayerInventory(inventory); + addPlayerHotbar(inventory); + + // Syringe Slot (0) + this.addSlot(new Slot(blockEntity.itemHandler, 0, 39, 35) { + @Override + public boolean mayPlace(ItemStack stack) { + return stack.getItem() == ModItems.SYRINGE.get(); + } + }); + + // DNA Slot (1) + this.addSlot(new Slot(blockEntity.itemHandler, 1, 57, 35) { + @Override + public boolean mayPlace(ItemStack stack) { + return stack.is(ModTags.Items.DNA); + } + }); + + // Frog DNA Slot (2) + this.addSlot(new Slot(blockEntity.itemHandler, 2, 48, 53) { + @Override + public boolean mayPlace(ItemStack stack) { + return stack.is(ModItems.FROG_DNA.get()); + } + }); + + // Output Slot (3) + this.addSlot(new Slot(blockEntity.itemHandler, 3, 103, 35) { + @Override + public boolean mayPlace(ItemStack stack) { + return false; + } + }); + + addDataSlots(data); + } + + public boolean isCrafting() { + return data.get(0) > 0; + } + + public int getScaledArrowProgress() { + int progress = this.data.get(0); + int maxProgress = this.data.get(1); + int arrowPixelSize = 24; + + return maxProgress != 0 && progress != 0 ? progress * arrowPixelSize / maxProgress : 0; + } + + // Inventory move logic constants + private static final int HOTBAR_SLOT_COUNT = 9; + private static final int PLAYER_INVENTORY_ROW_COUNT = 3; + private static final int PLAYER_INVENTORY_COLUMN_COUNT = 9; + private static final int PLAYER_INVENTORY_SLOT_COUNT = PLAYER_INVENTORY_COLUMN_COUNT * PLAYER_INVENTORY_ROW_COUNT; + private static final int VANILLA_SLOT_COUNT = HOTBAR_SLOT_COUNT + PLAYER_INVENTORY_SLOT_COUNT; + private static final int VANILLA_FIRST_SLOT_INDEX = 0; + private static final int TE_INVENTORY_FIRST_SLOT_INDEX = VANILLA_SLOT_COUNT; + private static final int TE_INVENTORY_SLOT_COUNT = 4; + + @Override + public ItemStack quickMoveStack(Player playerIn, int pIndex) { + Slot sourceSlot = slots.get(pIndex); + if (sourceSlot == null || !sourceSlot.hasItem()) return ItemStack.EMPTY; + ItemStack sourceStack = sourceSlot.getItem(); + ItemStack copyOfSourceStack = sourceStack.copy(); + + if (pIndex < VANILLA_SLOT_COUNT) { + // Player inventory -> machine inputs + int teInputStart = TE_INVENTORY_FIRST_SLOT_INDEX; + int teInputEndExclusive = TE_INVENTORY_FIRST_SLOT_INDEX + 3; + if (!moveItemStackTo(sourceStack, teInputStart, teInputEndExclusive, false)) { + return ItemStack.EMPTY; + } + } else if (pIndex < TE_INVENTORY_FIRST_SLOT_INDEX + TE_INVENTORY_SLOT_COUNT) { + // Machine -> player inventory + if (!moveItemStackTo(sourceStack, VANILLA_FIRST_SLOT_INDEX, VANILLA_SLOT_COUNT, false)) { + return ItemStack.EMPTY; + } + } else { + return ItemStack.EMPTY; + } + + if (sourceStack.isEmpty()) { + sourceSlot.set(ItemStack.EMPTY); + } else { + sourceSlot.setChanged(); + } + sourceSlot.onTake(playerIn, sourceStack); + return copyOfSourceStack; + } + + @Override + public boolean stillValid(Player player) { + return stillValid(ContainerLevelAccess.create(this.level, this.blockEntity.getBlockPos()), + player, this.blockEntity.getBlockState().getBlock()); + } + + public void addPlayerInventory(Inventory playerInventory) { + for (int i = 0; i < 3; i++) { + for (int l = 0; l < 9; l++) { + this.addSlot(new Slot(playerInventory, l + i * 9 + 9, 8 + l * 18, 84 + i * 18)); + } + } + } + + public void addPlayerHotbar(Inventory playerInventory) { + for (int i = 0; i < 9; i++) { + this.addSlot(new Slot(playerInventory, i, 8 + i * 18, 142)); + } + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/screen/custom/EmbryonicMachineScreen.java b/common/src/main/java/net/cmr/jurassicrevived/screen/custom/EmbryonicMachineScreen.java new file mode 100644 index 0000000..c8e6924 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/screen/custom/EmbryonicMachineScreen.java @@ -0,0 +1,116 @@ +package net.cmr.jurassicrevived.screen.custom; + +import com.mojang.blaze3d.systems.RenderSystem; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.config.JRConfigManager; +import net.cmr.jurassicrevived.screen.renderer.EnergyDisplayTooltipArea; +import net.cmr.jurassicrevived.util.MouseUtil; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.player.Inventory; + +import java.util.Optional; + +public class EmbryonicMachineScreen extends AbstractContainerScreen { + private static final ResourceLocation GUI_TEXTURE = + Constants.rl("textures/gui/embryonic_machine/embryonic_machine_gui.png"); + private static final ResourceLocation SYRINGE_BAR_REVERSED_TEXTURE = + Constants.rl("textures/gui/generic/syringe_bar_reversed.png"); + private static final ResourceLocation WHITE_SYRINGE_BAR_REVERSED_TEXTURE = + Constants.rl("textures/gui/generic/white_syringe_bar_reversed.png"); + private static final ResourceLocation POWER_BAR_TEXTURE = + Constants.rl("textures/gui/generic/power_bar.png"); + private static final ResourceLocation TEST_TUBE_TEXTURE = + Constants.rl("textures/gui/generic/test_tube.png"); + private static final ResourceLocation SYRINGE_TEXTURE = + Constants.rl("textures/gui/generic/syringe.png"); + private EnergyDisplayTooltipArea energyInfoArea; + + public EmbryonicMachineScreen(EmbryonicMachineMenu menu, Inventory playerInventory, Component title) { + super(menu, playerInventory, title); + } + + @Override + protected void init() { + super.init(); + + this.inventoryLabelY = 10000; + this.titleLabelY = 10000; + + assignEnergyInfoArea(); + } + + private void renderEnergyAreaTooltip(GuiGraphics guiGraphics, int pMouseX, int pMouseY, int x, int y) { + if(isMouseAboveArea(pMouseX, pMouseY, x, y, 160, 11, 8, 64) && JRConfigManager.get().requirePower) { + guiGraphics.renderTooltip(this.font, energyInfoArea.getTooltips(), + Optional.empty(), pMouseX - x, pMouseY - y); + } + } + + private void assignEnergyInfoArea() { + energyInfoArea = new EnergyDisplayTooltipArea(((width - imageWidth) / 2) + 160, + ((height - imageHeight) / 2) + 11, menu.blockEntity.getEnergyStorage(null)); + } + + @Override + protected void renderLabels(GuiGraphics guiGraphics, int pMouseX, int pMouseY) { + int x = (width - imageWidth) / 2; + int y = (height - imageHeight) / 2; + + if (JRConfigManager.get().requirePower) { + renderEnergyAreaTooltip(guiGraphics, pMouseX, pMouseY, x, y); + } + } + + @Override + protected void renderBg(GuiGraphics guiGraphics, float v, int i, int i1) { + RenderSystem.setShader(GameRenderer::getPositionTexShader); + RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F); + RenderSystem.setShaderTexture(0, GUI_TEXTURE); + int x = (this.width - this.imageWidth) / 2; + int y = (this.height - this.imageHeight) /2; + + guiGraphics.blit(GUI_TEXTURE, x, y, 0, 0, this.imageWidth, this.imageHeight, 176, 166); + guiGraphics.blit(SYRINGE_BAR_REVERSED_TEXTURE, x + 76, y + 35, 0, 0, 24, 16, 24, 16); + guiGraphics.blit(SYRINGE_TEXTURE, x + 39, y + 35, 0, 0, 16, 16, 16, 16); + guiGraphics.blit(TEST_TUBE_TEXTURE, x + 57, y + 35, 0, 0, 16, 16, 16, 16); + + + if (JRConfigManager.get().requirePower) { + guiGraphics.blit(POWER_BAR_TEXTURE, x+159, y+10, 0, 0, 10, 66, 10, 66); + + } + } + + private void RenderProgressArrow(GuiGraphics guiGraphics, int x, int y) { + if(menu.isCrafting()) { + guiGraphics.blit(WHITE_SYRINGE_BAR_REVERSED_TEXTURE, x+76, y + 35, 0, 0, menu.getScaledArrowProgress(), 16, 24, 16); + } + } + + @Override + public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { + //? if >1.20.1 { + /*this.renderBackground(guiGraphics, mouseX, mouseY, partialTick); + *///?} else { + renderBackground(guiGraphics); + //?} + super.render(guiGraphics, mouseX, mouseY, partialTick); + renderTooltip(guiGraphics, mouseX, mouseY); + int x = (this.width - this.imageWidth) / 2; + int y = (this.height - this.imageHeight) /2; + + RenderProgressArrow(guiGraphics, x, y); + + if (JRConfigManager.get().requirePower) { + energyInfoArea.render(guiGraphics); + } + } + + public static boolean isMouseAboveArea(int pMouseX, int pMouseY, int x, int y, int offsetX, int offsetY, int width, int height) { + return MouseUtil.isMouseOver(pMouseX, pMouseY, x + offsetX, y + offsetY, width, height); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/screen/custom/FossilCleanerMenu.java b/common/src/main/java/net/cmr/jurassicrevived/screen/custom/FossilCleanerMenu.java new file mode 100644 index 0000000..7811023 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/screen/custom/FossilCleanerMenu.java @@ -0,0 +1,146 @@ +package net.cmr.jurassicrevived.screen.custom; + +import net.cmr.jurassicrevived.block.ModBlocks; +import net.cmr.jurassicrevived.block.entity.custom.FossilCleanerBlockEntity; +import net.cmr.jurassicrevived.screen.ModMenuTypes; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.tags.FluidTags; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.*; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; + +public class FossilCleanerMenu extends AbstractContainerMenu { + public final FossilCleanerBlockEntity blockEntity; + public final Level level; + public final ContainerData data; + + public FossilCleanerMenu(int containerId, Inventory inventory, FriendlyByteBuf data) { + this(containerId, inventory, inventory.player.level().getBlockEntity(data.readBlockPos()), new SimpleContainerData(2)); + } + + public FossilCleanerMenu(int containerId, Inventory inventory, BlockEntity entity, ContainerData data) { + super(ModMenuTypes.FOSSIL_CLEANER_MENU.get(), containerId); + blockEntity = ((FossilCleanerBlockEntity) entity); + this.level = inventory.player.level(); + this.data = data; + + addPlayerInventory(inventory); + addPlayerHotbar(inventory); + + // Water Input Slot (0) + this.addSlot(new Slot(blockEntity.itemHandler, 0, 7, 61) { + @Override + public boolean mayPlace(ItemStack stack) { + // Simplified validation for common: accepts bucket or water bucket + return stack.is(Items.WATER_BUCKET); + } + }); + + // Fossil Block Slot (1) + this.addSlot(new Slot(blockEntity.itemHandler, 1, 56, 35) { + @Override + public boolean mayPlace(ItemStack stack) { + return stack.getItem() == ModBlocks.STONE_FOSSIL.get().asItem() || stack.getItem() == ModBlocks.DEEPSLATE_FOSSIL.get().asItem(); + } + }); + + // Output Slots (2, 3, 4) + this.addSlot(new Slot(blockEntity.itemHandler, 2, 103, 35) { + @Override + public boolean mayPlace(ItemStack stack) { + return false; + } + }); + this.addSlot(new Slot(blockEntity.itemHandler, 3, 121, 35) { + @Override + public boolean mayPlace(ItemStack stack) { + return false; + } + }); + this.addSlot(new Slot(blockEntity.itemHandler, 4, 139, 35) { + @Override + public boolean mayPlace(ItemStack stack) { + return false; + } + }); + + addDataSlots(data); + } + + public boolean isCrafting() { + return data.get(0) > 0; + } + + public int getScaledArrowProgress() { + int progress = this.data.get(0); + int maxProgress = this.data.get(1); + int arrowPixelSize = 29; + + return maxProgress != 0 && progress != 0 ? progress * arrowPixelSize / maxProgress : 0; + } + + private static final int HOTBAR_SLOT_COUNT = 9; + private static final int PLAYER_INVENTORY_ROW_COUNT = 3; + private static final int PLAYER_INVENTORY_COLUMN_COUNT = 9; + private static final int PLAYER_INVENTORY_SLOT_COUNT = PLAYER_INVENTORY_COLUMN_COUNT * PLAYER_INVENTORY_ROW_COUNT; + private static final int VANILLA_SLOT_COUNT = HOTBAR_SLOT_COUNT + PLAYER_INVENTORY_SLOT_COUNT; + private static final int VANILLA_FIRST_SLOT_INDEX = 0; + private static final int TE_INVENTORY_FIRST_SLOT_INDEX = VANILLA_SLOT_COUNT; + private static final int TE_INVENTORY_SLOT_COUNT = 5; + + @Override + public ItemStack quickMoveStack(Player playerIn, int pIndex) { + Slot sourceSlot = slots.get(pIndex); + if (sourceSlot == null || !sourceSlot.hasItem()) return ItemStack.EMPTY; + ItemStack sourceStack = sourceSlot.getItem(); + ItemStack copyOfSourceStack = sourceStack.copy(); + + if (pIndex < VANILLA_SLOT_COUNT) { + // Player inventory -> machine inputs (slots 0 and 1) + int teInputStart = TE_INVENTORY_FIRST_SLOT_INDEX; + int teInputEndExclusive = TE_INVENTORY_FIRST_SLOT_INDEX + 2; + if (!moveItemStackTo(sourceStack, teInputStart, teInputEndExclusive, false)) { + return ItemStack.EMPTY; + } + } else if (pIndex < TE_INVENTORY_FIRST_SLOT_INDEX + TE_INVENTORY_SLOT_COUNT) { + // Machine -> player inventory + if (!moveItemStackTo(sourceStack, VANILLA_FIRST_SLOT_INDEX, VANILLA_SLOT_COUNT, false)) { + return ItemStack.EMPTY; + } + } else { + return ItemStack.EMPTY; + } + + if (sourceStack.isEmpty()) { + sourceSlot.set(ItemStack.EMPTY); + } else { + sourceSlot.setChanged(); + } + sourceSlot.onTake(playerIn, sourceStack); + return copyOfSourceStack; + } + + @Override + public boolean stillValid(Player player) { + return stillValid(ContainerLevelAccess.create(this.level, this.blockEntity.getBlockPos()), + player, this.blockEntity.getBlockState().getBlock()); + } + + public void addPlayerInventory(Inventory playerInventory) { + for (int i = 0; i < 3; i++) { + for (int l = 0; l < 9; l++) { + this.addSlot(new Slot(playerInventory, l + i * 9 + 9, 8 + l * 18, 84 + i * 18)); + } + } + } + + public void addPlayerHotbar(Inventory playerInventory) { + for (int i = 0; i < 9; i++) { + this.addSlot(new Slot(playerInventory, i, 8 + i * 18, 142)); + } + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/screen/custom/FossilCleanerScreen.java b/common/src/main/java/net/cmr/jurassicrevived/screen/custom/FossilCleanerScreen.java new file mode 100644 index 0000000..f244e88 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/screen/custom/FossilCleanerScreen.java @@ -0,0 +1,142 @@ +package net.cmr.jurassicrevived.screen.custom; + +import com.mojang.blaze3d.systems.RenderSystem; +import dev.architectury.fluid.FluidStack; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.config.JRConfigManager; +import net.cmr.jurassicrevived.screen.renderer.EnergyDisplayTooltipArea; +import net.cmr.jurassicrevived.screen.renderer.FluidTankRenderer; +import net.cmr.jurassicrevived.util.MouseUtil; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.item.TooltipFlag; + +import java.util.Optional; + +public class FossilCleanerScreen extends AbstractContainerScreen { + private static final ResourceLocation GUI_TEXTURE = + Constants.rl("textures/gui/fossil_cleaner/fossil_cleaner_gui.png"); + private static final ResourceLocation BUBBLES_TEXTURE = + Constants.rl("textures/gui/generic/bubbles.png"); + private static final ResourceLocation WHITE_BUBBLES_TEXTURE = + Constants.rl("textures/gui/generic/white_bubbles.png"); + private static final ResourceLocation POWER_BAR_TEXTURE = + Constants.rl("textures/gui/generic/power_bar.png"); + private static final ResourceLocation SKULL_TEXTURE = + Constants.rl("textures/gui/generic/skull.png"); + private FluidTankRenderer fluidRenderer; + private EnergyDisplayTooltipArea energyInfoArea; + + public FossilCleanerScreen(FossilCleanerMenu menu, Inventory playerInventory, Component title) { + super(menu, playerInventory, title); + } + + @Override + protected void init() { + super.init(); + + this.inventoryLabelY = 10000; + this.titleLabelY = 10000; + + assignFluidRenderer(); + assignEnergyInfoArea(); + } + + private void renderEnergyAreaTooltip(GuiGraphics guiGraphics, int pMouseX, int pMouseY, int x, int y) { + if(isMouseAboveArea(pMouseX, pMouseY, x, y, 160, 11, 8, 64) && JRConfigManager.get().requirePower) { + guiGraphics.renderTooltip(this.font, energyInfoArea.getTooltips(), + Optional.empty(), pMouseX - x, pMouseY - y); + } + } + + private void assignEnergyInfoArea() { + energyInfoArea = new EnergyDisplayTooltipArea(((width - imageWidth) / 2) + 160, + ((height - imageHeight) / 2) + 11, menu.blockEntity.getEnergyStorage(null)); + } + + private void assignFluidRenderer() { + fluidRenderer = new FluidTankRenderer(16000, true, 16, 50); + } + + private void renderFluidTooltipArea(GuiGraphics guiGraphics, int MouseX, int MouseY, int x, int y, + FluidStack stack, int offsetX, int offsetY, FluidTankRenderer renderer) { + if(isMouseAboveArea(MouseX, MouseY, x, y, offsetX, offsetY, renderer)) { + guiGraphics.renderTooltip(this.font, renderer.getTooltip(stack, TooltipFlag.Default.NORMAL), + Optional.empty(), MouseX - x, MouseY - y); + } + } + + @Override + protected void renderLabels(GuiGraphics guiGraphics, int pMouseX, int pMouseY) { + int x = (width - imageWidth) / 2; + int y = (height - imageHeight) / 2; + + renderFluidTooltipArea(guiGraphics, pMouseX, pMouseY, x, y, menu.blockEntity.getFluid(), 7, 8, fluidRenderer); + if (JRConfigManager.get().requirePower) { + renderEnergyAreaTooltip(guiGraphics, pMouseX, pMouseY, x, y); + } + } + + @Override + protected void renderBg(GuiGraphics guiGraphics, float v, int i, int i1) { + RenderSystem.setShader(GameRenderer::getPositionTexShader); + RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F); + RenderSystem.setShaderTexture(0, GUI_TEXTURE); + int x = (this.width - this.imageWidth) / 2; + int y = (this.height - this.imageHeight) /2; + + guiGraphics.blit(GUI_TEXTURE, x, y, 0, 0, this.imageWidth, this.imageHeight, 176, 166); + guiGraphics.blit(BUBBLES_TEXTURE, x + 73, y + 37, 0, 0, 29, 12, 29, 12); + guiGraphics.blit(SKULL_TEXTURE, x + 57, y + 35, 0, 0, 16, 16, 16, 16); + RenderProgressArrow(guiGraphics, x, y); + + if (JRConfigManager.get().requirePower) { + guiGraphics.blit(POWER_BAR_TEXTURE, x+159, y+10, 0, 0, 10, 66, 10, 66); + } + } + + private void RenderProgressArrow(GuiGraphics guiGraphics, int x, int y) { + if(menu.isCrafting()) { + guiGraphics.blit(WHITE_BUBBLES_TEXTURE, x+73, y + 37, 0, 0, menu.getScaledArrowProgress(), 12, 29, 12); + } + } + + @Override + public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { + //? if >1.20.1 { + /*this.renderBackground(guiGraphics, mouseX, mouseY, partialTick); + *///?} else { + renderBackground(guiGraphics); + //?} + super.render(guiGraphics, mouseX, mouseY, partialTick); + int x = (this.width - this.imageWidth) / 2; + int y = (this.height - this.imageHeight) /2; + if (MouseUtil.isMouseOver(mouseX, mouseY, x + 7, y + 8, 16, 50)) { + renderHoverHighlight(guiGraphics, x + 7, y + 8, 16, 50); + } + renderTooltip(guiGraphics, mouseX, mouseY); + fluidRenderer.render(guiGraphics, x + 7, y + 8, menu.blockEntity.getFluid()); + if (JRConfigManager.get().requirePower) { + energyInfoArea.render(guiGraphics); + } + } + + private static void renderHoverHighlight(GuiGraphics g, int x, int y, int w, int h) { + g.pose().pushPose(); + g.pose().translate(0, 0, 200); + g.fillGradient(x, y, x + w, y + h, 0x80FFFFFF, 0x80FFFFFF); + g.pose().popPose(); + } + + public static boolean isMouseAboveArea(int pMouseX, int pMouseY, int x, int y, int offsetX, int offsetY, FluidTankRenderer renderer) { + return MouseUtil.isMouseOver(pMouseX, pMouseY, x + offsetX, y + offsetY, renderer.getWidth(), renderer.getHeight()); + } + + public static boolean isMouseAboveArea(int pMouseX, int pMouseY, int x, int y, int offsetX, int offsetY, int width, int height) { + return MouseUtil.isMouseOver(pMouseX, pMouseY, x + offsetX, y + offsetY, width, height); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/screen/custom/FossilGrinderMenu.java b/common/src/main/java/net/cmr/jurassicrevived/screen/custom/FossilGrinderMenu.java new file mode 100644 index 0000000..69bfb81 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/screen/custom/FossilGrinderMenu.java @@ -0,0 +1,136 @@ +package net.cmr.jurassicrevived.screen.custom; + +import net.cmr.jurassicrevived.block.entity.custom.FossilGrinderBlockEntity; +import net.cmr.jurassicrevived.screen.ModMenuTypes; +import net.cmr.jurassicrevived.util.ModTags; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.*; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; + +public class FossilGrinderMenu extends AbstractContainerMenu { + public final FossilGrinderBlockEntity blockEntity; + public final Level level; + public final ContainerData data; + + public FossilGrinderMenu(int containerId, Inventory inventory, FriendlyByteBuf data) { + this(containerId, inventory, inventory.player.level().getBlockEntity(data.readBlockPos()), new SimpleContainerData(2)); + } + + public FossilGrinderMenu(int containerId, Inventory inventory, BlockEntity entity, ContainerData data) { + super(ModMenuTypes.FOSSIL_GRINDER_MENU.get(), containerId); + blockEntity = ((FossilGrinderBlockEntity) entity); + this.level = inventory.player.level(); + this.data = data; + + addPlayerInventory(inventory); + addPlayerHotbar(inventory); + + // Fossil Input Slot (0) + this.addSlot(new Slot(blockEntity.itemHandler, 0, 58, 35) { + @Override + public boolean mayPlace(ItemStack stack) { + return stack.is(ModTags.Items.FOSSILS) || stack.is(ModTags.Items.SKULLS); + } + }); + + // Output Slots (1, 2, 3) + this.addSlot(new Slot(blockEntity.itemHandler, 1, 103, 17) { + @Override + public boolean mayPlace(ItemStack stack) { + return false; + } + }); + this.addSlot(new Slot(blockEntity.itemHandler, 2, 103, 35) { + @Override + public boolean mayPlace(ItemStack stack) { + return false; + } + }); + this.addSlot(new Slot(blockEntity.itemHandler, 3, 103, 53) { + @Override + public boolean mayPlace(ItemStack stack) { + return false; + } + }); + + addDataSlots(data); + } + + public boolean isCrafting() { + return data.get(0) > 0; + } + + public int getScaledArrowProgress() { + int progress = this.data.get(0); + int maxProgress = this.data.get(1); + int arrowPixelSize = 19; + + return maxProgress != 0 && progress != 0 ? progress * arrowPixelSize / maxProgress : 0; + } + + // Inventory move logic constants + private static final int HOTBAR_SLOT_COUNT = 9; + private static final int PLAYER_INVENTORY_ROW_COUNT = 3; + private static final int PLAYER_INVENTORY_COLUMN_COUNT = 9; + private static final int PLAYER_INVENTORY_SLOT_COUNT = PLAYER_INVENTORY_COLUMN_COUNT * PLAYER_INVENTORY_ROW_COUNT; + private static final int VANILLA_SLOT_COUNT = HOTBAR_SLOT_COUNT + PLAYER_INVENTORY_SLOT_COUNT; + private static final int VANILLA_FIRST_SLOT_INDEX = 0; + private static final int TE_INVENTORY_FIRST_SLOT_INDEX = VANILLA_SLOT_COUNT; + private static final int TE_INVENTORY_SLOT_COUNT = 4; + + @Override + public ItemStack quickMoveStack(Player playerIn, int pIndex) { + Slot sourceSlot = slots.get(pIndex); + if (sourceSlot == null || !sourceSlot.hasItem()) return ItemStack.EMPTY; + ItemStack sourceStack = sourceSlot.getItem(); + ItemStack copyOfSourceStack = sourceStack.copy(); + + if (pIndex < VANILLA_SLOT_COUNT) { + // Player inventory -> machine input (slot 0 only) + int teInputStart = TE_INVENTORY_FIRST_SLOT_INDEX; + int teInputEndExclusive = TE_INVENTORY_FIRST_SLOT_INDEX + 1; + if (!moveItemStackTo(sourceStack, teInputStart, teInputEndExclusive, false)) { + return ItemStack.EMPTY; + } + } else if (pIndex < TE_INVENTORY_FIRST_SLOT_INDEX + TE_INVENTORY_SLOT_COUNT) { + // Machine -> player inventory + if (!moveItemStackTo(sourceStack, VANILLA_FIRST_SLOT_INDEX, VANILLA_SLOT_COUNT, false)) { + return ItemStack.EMPTY; + } + } else { + return ItemStack.EMPTY; + } + + if (sourceStack.isEmpty()) { + sourceSlot.set(ItemStack.EMPTY); + } else { + sourceSlot.setChanged(); + } + sourceSlot.onTake(playerIn, sourceStack); + return copyOfSourceStack; + } + + @Override + public boolean stillValid(Player player) { + return stillValid(ContainerLevelAccess.create(this.level, this.blockEntity.getBlockPos()), + player, this.blockEntity.getBlockState().getBlock()); + } + + public void addPlayerInventory(Inventory playerInventory) { + for (int i = 0; i < 3; i++) { + for (int l = 0; l < 9; l++) { + this.addSlot(new Slot(playerInventory, l + i * 9 + 9, 8 + l * 18, 84 + i * 18)); + } + } + } + + public void addPlayerHotbar(Inventory playerInventory) { + for (int i = 0; i < 9; i++) { + this.addSlot(new Slot(playerInventory, i, 8 + i * 18, 142)); + } + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/screen/custom/FossilGrinderScreen.java b/common/src/main/java/net/cmr/jurassicrevived/screen/custom/FossilGrinderScreen.java new file mode 100644 index 0000000..30c59b6 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/screen/custom/FossilGrinderScreen.java @@ -0,0 +1,131 @@ +package net.cmr.jurassicrevived.screen.custom; + +import com.mojang.blaze3d.systems.RenderSystem; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.config.JRConfigManager; +import net.cmr.jurassicrevived.screen.renderer.EnergyDisplayTooltipArea; +import net.cmr.jurassicrevived.util.MouseUtil; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.player.Inventory; + +import java.util.Optional; + +public class FossilGrinderScreen extends AbstractContainerScreen { + private static final ResourceLocation GUI_TEXTURE = + Constants.rl("textures/gui/fossil_grinder/fossil_grinder_gui.png"); + private static final ResourceLocation CUTTING_BLADES_TEXTURE = + Constants.rl("textures/gui/generic/cutting_blades.png"); + private static final ResourceLocation POWER_BAR_TEXTURE = + Constants.rl("textures/gui/generic/power_bar.png"); + private static final ResourceLocation SKULL_TEXTURE = + Constants.rl("textures/gui/generic/skull.png"); + private EnergyDisplayTooltipArea energyInfoArea; + + public FossilGrinderScreen(FossilGrinderMenu menu, Inventory playerInventory, Component title) { + super(menu, playerInventory, title); + } + + @Override + protected void init() { + super.init(); + + this.inventoryLabelY = 10000; + this.titleLabelY = 10000; + + assignEnergyInfoArea(); + } + + private void renderEnergyAreaTooltip(GuiGraphics guiGraphics, int pMouseX, int pMouseY, int x, int y) { + if(isMouseAboveArea(pMouseX, pMouseY, x, y, 160, 11, 8, 64) && JRConfigManager.get().requirePower) { + guiGraphics.renderTooltip(this.font, energyInfoArea.getTooltips(), + Optional.empty(), pMouseX - x, pMouseY - y); + } + } + + private void assignEnergyInfoArea() { + energyInfoArea = new EnergyDisplayTooltipArea(((width - imageWidth) / 2) + 160, + ((height - imageHeight) / 2) + 11, menu.blockEntity.getEnergyStorage(null)); + } + + @Override + protected void renderLabels(GuiGraphics guiGraphics, int pMouseX, int pMouseY) { + int x = (width - imageWidth) / 2; + int y = (height - imageHeight) / 2; + + if (JRConfigManager.get().requirePower) { + renderEnergyAreaTooltip(guiGraphics, pMouseX, pMouseY, x, y); + } + } + + @Override + protected void renderBg(GuiGraphics guiGraphics, float v, int i, int i1) { + RenderSystem.setShader(GameRenderer::getPositionTexShader); + RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F); + RenderSystem.setShaderTexture(0, GUI_TEXTURE); + int x = (this.width - this.imageWidth) / 2; + int y = (this.height - this.imageHeight) /2; + + guiGraphics.blit(GUI_TEXTURE, x, y, 0, 0, this.imageWidth, this.imageHeight, 176, 166); + guiGraphics.blit(SKULL_TEXTURE, x + 57, y + 35, 0, 0, 16, 16, 16, 16); + + { + final float scale = 1.25f; + final int texSize = 16; + + float baseAngle = (System.currentTimeMillis() % 700L) / 700.0f * ((float)Math.PI * 2.0f); + + java.util.function.BiConsumer drawBlade = (center, ang) -> { + guiGraphics.pose().pushPose(); + guiGraphics.pose().translate(center[0], center[1], 0); + guiGraphics.pose().scale(scale, scale, 1.0f); + if (ang != null) { + guiGraphics.pose().mulPose(com.mojang.math.Axis.ZP.rotation(-ang)); + } + guiGraphics.pose().translate(-texSize / 2f, -texSize / 2f, 0); + guiGraphics.blit(CUTTING_BLADES_TEXTURE, 0, 0, 0, 0, texSize, texSize, texSize, texSize); + guiGraphics.pose().popPose(); + }; + + int cx1 = x + 89, cy1 = y + 34; + int cx2 = x + 89, cy2 = y + 52; + + if (menu.isCrafting()) { + drawBlade.accept(new int[]{cx1, cy1}, baseAngle); + drawBlade.accept(new int[]{cx2, cy2}, -baseAngle); + } else { + drawBlade.accept(new int[]{cx1, cy1}, null); + drawBlade.accept(new int[]{cx2, cy2}, null); + } + } + + if (JRConfigManager.get().requirePower) { + guiGraphics.blit(POWER_BAR_TEXTURE, x+159, y+10, 0, 0, 10, 66, 10, 66); + + } + } + + @Override + public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { + //? if >1.20.1 { + /*this.renderBackground(guiGraphics, mouseX, mouseY, partialTick); + *///?} else { + renderBackground(guiGraphics); + //?} + super.render(guiGraphics, mouseX, mouseY, partialTick); + renderTooltip(guiGraphics, mouseX, mouseY); + int x = (this.width - this.imageWidth) / 2; + int y = (this.height - this.imageHeight) /2; + + if (JRConfigManager.get().requirePower) { + energyInfoArea.render(guiGraphics); + } + } + + public static boolean isMouseAboveArea(int pMouseX, int pMouseY, int x, int y, int offsetX, int offsetY, int width, int height) { + return MouseUtil.isMouseOver(pMouseX, pMouseY, x + offsetX, y + offsetY, width, height); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/screen/custom/GeneratorMenu.java b/common/src/main/java/net/cmr/jurassicrevived/screen/custom/GeneratorMenu.java new file mode 100644 index 0000000..ae7a1f4 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/screen/custom/GeneratorMenu.java @@ -0,0 +1,108 @@ +package net.cmr.jurassicrevived.screen.custom; + +import net.cmr.jurassicrevived.block.entity.custom.GeneratorBlockEntity; +import net.cmr.jurassicrevived.screen.ModMenuTypes; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.*; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; + +public class GeneratorMenu extends AbstractContainerMenu { + public final GeneratorBlockEntity blockEntity; + private final Level level; + private final ContainerData data; + + public GeneratorMenu(int pContainerId, Inventory inv, FriendlyByteBuf extraData) { + this(pContainerId, inv, inv.player.level().getBlockEntity(extraData.readBlockPos()), new SimpleContainerData(2)); + } + + public GeneratorMenu(int pContainerId, Inventory inv, BlockEntity blockEntity, ContainerData data) { + super(ModMenuTypes.GENERATOR_MENU.get(), pContainerId); + this.blockEntity = ((GeneratorBlockEntity) blockEntity); + this.level = inv.player.level(); + this.data = data; + + addPlayerInventory(inv); + addPlayerHotbar(inv); + + this.addSlot(new Slot(this.blockEntity.itemHandler, 0, 80, 35)); + + addDataSlots(data); + } + + public boolean isBurning() { + int elapsed = data.get(0); + int total = data.get(1); + return total > 0 && elapsed < total; + } + + public float getFuelProgress() { + int elapsed = this.data.get(0); + int total = this.data.get(1); + if (total <= 0) return 0.0f; + float remaining = (float) (total - Math.min(elapsed, total)) / (float) total; + return Mth.clamp(remaining, 0.0f, 1.0f); + } + + private static final int HOTBAR_SLOT_COUNT = 9; + private static final int PLAYER_INVENTORY_ROW_COUNT = 3; + private static final int PLAYER_INVENTORY_COLUMN_COUNT = 9; + private static final int PLAYER_INVENTORY_SLOT_COUNT = PLAYER_INVENTORY_COLUMN_COUNT * PLAYER_INVENTORY_ROW_COUNT; + private static final int VANILLA_SLOT_COUNT = HOTBAR_SLOT_COUNT + PLAYER_INVENTORY_SLOT_COUNT; + private static final int VANILLA_FIRST_SLOT_INDEX = 0; + private static final int TE_INVENTORY_FIRST_SLOT_INDEX = VANILLA_SLOT_COUNT; + private static final int TE_INVENTORY_SLOT_COUNT = 1; + + @Override + public ItemStack quickMoveStack(Player playerIn, int pIndex) { + Slot sourceSlot = slots.get(pIndex); + if (sourceSlot == null || !sourceSlot.hasItem()) return ItemStack.EMPTY; + ItemStack sourceStack = sourceSlot.getItem(); + ItemStack copyOfSourceStack = sourceStack.copy(); + + if (pIndex < VANILLA_SLOT_COUNT) { + if (!moveItemStackTo(sourceStack, TE_INVENTORY_FIRST_SLOT_INDEX, TE_INVENTORY_FIRST_SLOT_INDEX + + TE_INVENTORY_SLOT_COUNT, false)) { + return ItemStack.EMPTY; + } + } else if (pIndex < TE_INVENTORY_FIRST_SLOT_INDEX + TE_INVENTORY_SLOT_COUNT) { + if (!moveItemStackTo(sourceStack, VANILLA_FIRST_SLOT_INDEX, VANILLA_SLOT_COUNT, false)) { + return ItemStack.EMPTY; + } + } else { + return ItemStack.EMPTY; + } + + if (sourceStack.isEmpty()) { + sourceSlot.set(ItemStack.EMPTY); + } else { + sourceSlot.setChanged(); + } + sourceSlot.onTake(playerIn, sourceStack); + return copyOfSourceStack; + } + + @Override + public boolean stillValid(Player player) { + return stillValid(ContainerLevelAccess.create(this.level, this.blockEntity.getBlockPos()), + player, this.blockEntity.getBlockState().getBlock()); + } + + private void addPlayerInventory(Inventory playerInventory) { + for (int i = 0; i < 3; ++i) { + for (int l = 0; l < 9; ++l) { + this.addSlot(new Slot(playerInventory, l + i * 9 + 9, 8 + l * 18, 84 + i * 18)); + } + } + } + + private void addPlayerHotbar(Inventory playerInventory) { + for (int i = 0; i < 9; ++i) { + this.addSlot(new Slot(playerInventory, i, 8 + i * 18, 142)); + } + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/screen/custom/GeneratorScreen.java b/common/src/main/java/net/cmr/jurassicrevived/screen/custom/GeneratorScreen.java new file mode 100644 index 0000000..b4cbc22 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/screen/custom/GeneratorScreen.java @@ -0,0 +1,118 @@ +package net.cmr.jurassicrevived.screen.custom; + +import com.mojang.blaze3d.systems.RenderSystem; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.screen.renderer.EnergyDisplayTooltipArea; +import net.cmr.jurassicrevived.util.MouseUtil; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.player.Inventory; + +import java.util.Optional; + +public class GeneratorScreen extends AbstractContainerScreen { + private static final ResourceLocation GUI_TEXTURE = + Constants.rl("textures/gui/generator/generator_gui.png"); + private static final ResourceLocation LIT_PROGRESS_TEXTURE = + Constants.rl("container/furnace/lit_progress"); + private EnergyDisplayTooltipArea energyInfoArea; + + public GeneratorScreen(GeneratorMenu pMenu, Inventory pPlayerInventory, Component pTitle) { + super(pMenu, pPlayerInventory, pTitle); + } + + @Override + protected void init() { + super.init(); + // Gets rid of title and inventory title + this.inventoryLabelY = 10000; + this.titleLabelY = 10000; + + assignEnergyInfoArea(); + } + + private void renderEnergyAreaTooltip(GuiGraphics guiGraphics, int pMouseX, int pMouseY, int x, int y) { + if(isMouseAboveArea(pMouseX, pMouseY, x, y, 156, 11, 8, 64)) { + guiGraphics.renderTooltip(this.font, energyInfoArea.getTooltips(), + Optional.empty(), pMouseX - x, pMouseY - y); + } + } + + private void assignEnergyInfoArea() { + energyInfoArea = new EnergyDisplayTooltipArea(((width - imageWidth) / 2) + 156, + ((height - imageHeight) / 2) + 11, menu.blockEntity.getEnergyStorage(null)); + } + + @Override + protected void renderLabels(GuiGraphics guiGraphics, int pMouseX, int pMouseY) { + int x = (width - imageWidth) / 2; + int y = (height - imageHeight) / 2; + + renderEnergyAreaTooltip(guiGraphics, pMouseX, pMouseY, x, y); + } + + @Override + protected void renderBg(GuiGraphics guiGraphics, float pPartialTick, int pMouseX, int pMouseY) { + RenderSystem.setShader(GameRenderer::getPositionTexShader); + RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F); + RenderSystem.setShaderTexture(0, GUI_TEXTURE); + int x = (width - imageWidth) / 2; + int y = (height - imageHeight) / 2; + + guiGraphics.blit(GUI_TEXTURE, x, y, 0, 0, imageWidth, imageHeight); + + renderFuelBurning(guiGraphics, x, y); + } + //? if >1.20.1 { + /*private void renderFuelBurning(GuiGraphics guiGraphics, int x, int y) { + if(this.menu.isBurning()) { + int l = Mth.ceil(this.menu.getFuelProgress() * 13.0F) + 1; + guiGraphics.blitSprite(LIT_PROGRESS_TEXTURE, 14, 14, 0, 14 - l, + x + 80, y + 18 + 14 - l, 14, l); + } + } + *///?} else { + private void renderFuelBurning(GuiGraphics guiGraphics, int x, int y) { + if (this.menu.isBurning()) { + float progress = Mth.clamp(this.menu.getFuelProgress(), 0.0F, 1.0F); + int minPixels = 2; + int visible = Mth.clamp(Mth.ceil(progress * 14.0F), minPixels, 14); + + int texW = 14, texH = 14; + int width = 14, height = visible; + + int srcU = 0; + int srcV = texH - visible; + + int destX = x + 80; + int bottomY = y + 18 + 14; + int destY = bottomY - visible; + + guiGraphics.blit(LIT_PROGRESS_TEXTURE, destX, destY, srcU, srcV, width, height, texW, texH); + } + } + //?} + + @Override + public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float delta) { + //? if >1.20.1 { + /*this.renderBackground(guiGraphics, mouseX, mouseY, delta); + *///?} else { + renderBackground(guiGraphics); + //?} + super.render(guiGraphics, mouseX, mouseY, delta); + renderTooltip(guiGraphics, mouseX, mouseY); + + if (energyInfoArea != null) { + energyInfoArea.render(guiGraphics); + } + } + + public static boolean isMouseAboveArea(int pMouseX, int pMouseY, int x, int y, int offsetX, int offsetY, int width, int height) { + return MouseUtil.isMouseOver(pMouseX, pMouseY, x + offsetX, y + offsetY, width, height); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/screen/custom/IncubatorMenu.java b/common/src/main/java/net/cmr/jurassicrevived/screen/custom/IncubatorMenu.java new file mode 100644 index 0000000..ed96b1e --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/screen/custom/IncubatorMenu.java @@ -0,0 +1,130 @@ +package net.cmr.jurassicrevived.screen.custom; + +import net.cmr.jurassicrevived.block.entity.custom.IncubatorBlockEntity; +import net.cmr.jurassicrevived.screen.ModMenuTypes; +import net.cmr.jurassicrevived.util.ModTags; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.*; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; + +public class IncubatorMenu extends AbstractContainerMenu { + public final IncubatorBlockEntity blockEntity; + public final Level level; + public final ContainerData data; + + public IncubatorMenu(int containerId, Inventory inventory, FriendlyByteBuf data) { + this(containerId, inventory, inventory.player.level().getBlockEntity(data.readBlockPos()), new SimpleContainerData(6)); + } + + public IncubatorMenu(int containerId, Inventory inventory, BlockEntity entity, ContainerData data) { + super(ModMenuTypes.INCUBATOR_MENU.get(), containerId); + blockEntity = ((IncubatorBlockEntity) entity); + this.level = inventory.player.level(); + this.data = data; + + addPlayerInventory(inventory); + addPlayerHotbar(inventory); + + // Incubator Slots 0, 1, 2 + this.addSlot(new Slot(blockEntity.itemHandler, 0, 50, 35) { + @Override + public boolean mayPlace(ItemStack stack) { + return stack.is(ModTags.Items.EGGS); + } + }); + + this.addSlot(new Slot(blockEntity.itemHandler, 1, 80, 35) { + @Override + public boolean mayPlace(ItemStack stack) { + return stack.is(ModTags.Items.EGGS); + } + }); + + this.addSlot(new Slot(blockEntity.itemHandler, 2, 110, 35) { + @Override + public boolean mayPlace(ItemStack stack) { + return stack.is(ModTags.Items.EGGS); + } + }); + + addDataSlots(data); + } + + public boolean isCrafting(int slotIndex) { + if (slotIndex < 0 || slotIndex > 2) return false; + return this.data.get(slotIndex * 2) > 0; + } + + public float getScaledArrowProgress(int slotIndex) { + if (slotIndex < 0 || slotIndex > 2) return 0f; + int p = this.data.get(slotIndex * 2); + int m = this.data.get(slotIndex * 2 + 1); + if (m == 0) return 0f; + return Math.max(0f, Math.min(1f, (float) p / (float) m)); + } + + private static final int HOTBAR_SLOT_COUNT = 9; + private static final int PLAYER_INVENTORY_ROW_COUNT = 3; + private static final int PLAYER_INVENTORY_COLUMN_COUNT = 9; + private static final int PLAYER_INVENTORY_SLOT_COUNT = PLAYER_INVENTORY_COLUMN_COUNT * PLAYER_INVENTORY_ROW_COUNT; + private static final int VANILLA_SLOT_COUNT = HOTBAR_SLOT_COUNT + PLAYER_INVENTORY_SLOT_COUNT; + private static final int VANILLA_FIRST_SLOT_INDEX = 0; + private static final int TE_INVENTORY_FIRST_SLOT_INDEX = VANILLA_SLOT_COUNT; + private static final int TE_INVENTORY_SLOT_COUNT = 3; + + @Override + public ItemStack quickMoveStack(Player playerIn, int pIndex) { + Slot sourceSlot = slots.get(pIndex); + if (sourceSlot == null || !sourceSlot.hasItem()) return ItemStack.EMPTY; + ItemStack sourceStack = sourceSlot.getItem(); + ItemStack copyOfSourceStack = sourceStack.copy(); + + if (pIndex < VANILLA_SLOT_COUNT) { + // Player inventory -> incubator inputs + int teInputStart = TE_INVENTORY_FIRST_SLOT_INDEX; + int teInputEndExclusive = TE_INVENTORY_FIRST_SLOT_INDEX + 3; + if (!moveItemStackTo(sourceStack, teInputStart, teInputEndExclusive, false)) { + return ItemStack.EMPTY; + } + } else if (pIndex < TE_INVENTORY_FIRST_SLOT_INDEX + TE_INVENTORY_SLOT_COUNT) { + // Incubator -> player inventory + if (!moveItemStackTo(sourceStack, VANILLA_FIRST_SLOT_INDEX, VANILLA_SLOT_COUNT, false)) { + return ItemStack.EMPTY; + } + } else { + return ItemStack.EMPTY; + } + + if (sourceStack.isEmpty()) { + sourceSlot.set(ItemStack.EMPTY); + } else { + sourceSlot.setChanged(); + } + sourceSlot.onTake(playerIn, sourceStack); + return copyOfSourceStack; + } + + @Override + public boolean stillValid(Player player) { + return stillValid(ContainerLevelAccess.create(this.level, this.blockEntity.getBlockPos()), + player, this.blockEntity.getBlockState().getBlock()); + } + + public void addPlayerInventory(Inventory playerInventory) { + for (int i = 0; i < 3; i++) { + for (int l = 0; l < 9; l++) { + this.addSlot(new Slot(playerInventory, l + i * 9 + 9, 8 + l * 18, 84 + i * 18)); + } + } + } + + public void addPlayerHotbar(Inventory playerInventory) { + for (int i = 0; i < 9; i++) { + this.addSlot(new Slot(playerInventory, i, 8 + i * 18, 142)); + } + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/screen/custom/IncubatorScreen.java b/common/src/main/java/net/cmr/jurassicrevived/screen/custom/IncubatorScreen.java new file mode 100644 index 0000000..178349b --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/screen/custom/IncubatorScreen.java @@ -0,0 +1,117 @@ +package net.cmr.jurassicrevived.screen.custom; + +import com.mojang.blaze3d.systems.RenderSystem; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.config.JRConfigManager; +import net.cmr.jurassicrevived.screen.renderer.EnergyDisplayTooltipArea; +import net.cmr.jurassicrevived.util.MouseUtil; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.player.Inventory; + +import java.util.Optional; + +public class IncubatorScreen extends AbstractContainerScreen { + private static final ResourceLocation GUI_TEXTURE = + Constants.rl("textures/gui/incubator/incubator_gui.png"); + private static final ResourceLocation LANTERN_TEXTURE = + Constants.rl("textures/gui/generic/lantern.png"); + private static final ResourceLocation POWER_BAR_TEXTURE = + Constants.rl("textures/gui/generic/power_bar.png"); + private static final ResourceLocation EGG_TEXTURE = + Constants.rl("textures/gui/generic/egg.png"); + private EnergyDisplayTooltipArea energyInfoArea; + + public IncubatorScreen(IncubatorMenu menu, Inventory playerInventory, Component title) { + super(menu, playerInventory, title); + } + + @Override + protected void init() { + super.init(); + + this.inventoryLabelY = 10000; + this.titleLabelY = 10000; + + assignEnergyInfoArea(); + } + + private void renderEnergyAreaTooltip(GuiGraphics guiGraphics, int pMouseX, int pMouseY, int x, int y) { + if(isMouseAboveArea(pMouseX, pMouseY, x, y, 160, 11, 8, 64) && JRConfigManager.get().requirePower) { + guiGraphics.renderTooltip(this.font, energyInfoArea.getTooltips(), + Optional.empty(), pMouseX - x, pMouseY - y); + } + } + + private void assignEnergyInfoArea() { + energyInfoArea = new EnergyDisplayTooltipArea(((width - imageWidth) / 2) + 160, + ((height - imageHeight) / 2) + 11, menu.blockEntity.getEnergyStorage(null)); + } + + @Override + protected void renderLabels(GuiGraphics guiGraphics, int pMouseX, int pMouseY) { + int x = (width - imageWidth) / 2; + int y = (height - imageHeight) / 2; + + if (JRConfigManager.get().requirePower) { + renderEnergyAreaTooltip(guiGraphics, pMouseX, pMouseY, x, y); + } + } + + @Override + protected void renderBg(GuiGraphics guiGraphics, float v, int i, int i1) { + RenderSystem.setShader(GameRenderer::getPositionTexShader); + RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F); + RenderSystem.setShaderTexture(0, GUI_TEXTURE); + int x = (this.width - this.imageWidth) / 2; + int y = (this.height - this.imageHeight) /2; + + guiGraphics.blit(GUI_TEXTURE, x, y, 0, 0, this.imageWidth, this.imageHeight, 176, 166); + + guiGraphics.blit(EGG_TEXTURE, x + 50, y + 35, 0, 0, 16, 16, 16, 16); + guiGraphics.blit(EGG_TEXTURE, x + 80, y + 35, 0, 0, 16, 16, 16, 16); + guiGraphics.blit(EGG_TEXTURE, x + 110, y + 35, 0, 0, 16, 16, 16, 16); + + + if (JRConfigManager.get().requirePower) { + guiGraphics.blit(POWER_BAR_TEXTURE, x+159, y+10, 0, 0, 10, 66, 10, 66); + + } + } + + private void renderProgressArrowForSlot(GuiGraphics g, int x, int y, int slotIdx, int slotPixelX) { + if (this.menu.isCrafting(slotIdx)) { + int l = Mth.ceil(this.menu.getScaledArrowProgress(slotIdx) * 13.0F) + 1; + g.blit(LANTERN_TEXTURE, x + slotPixelX, y +16 , 0, 0, 16, l, 16, 16); + } + } + + @Override + public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { + //? if >1.20.1 { + /*this.renderBackground(guiGraphics, mouseX, mouseY, partialTick); + *///?} else { + renderBackground(guiGraphics); + //?} + super.render(guiGraphics, mouseX, mouseY, partialTick); + renderTooltip(guiGraphics, mouseX, mouseY); + int x = (this.width - this.imageWidth) / 2; + int y = (this.height - this.imageHeight) /2; + + renderProgressArrowForSlot(guiGraphics, x, y, 0, 50); + renderProgressArrowForSlot(guiGraphics, x, y, 1, 80); + renderProgressArrowForSlot(guiGraphics, x, y, 2, 110); + + if (JRConfigManager.get().requirePower) { + energyInfoArea.render(guiGraphics); + } + } + + public static boolean isMouseAboveArea(int pMouseX, int pMouseY, int x, int y, int offsetX, int offsetY, int width, int height) { + return MouseUtil.isMouseOver(pMouseX, pMouseY, x + offsetX, y + offsetY, width, height); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/screen/custom/PowerCellMenu.java b/common/src/main/java/net/cmr/jurassicrevived/screen/custom/PowerCellMenu.java new file mode 100644 index 0000000..8c410e1 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/screen/custom/PowerCellMenu.java @@ -0,0 +1,101 @@ +package net.cmr.jurassicrevived.screen.custom; + +import net.cmr.jurassicrevived.block.ModBlocks; +import net.cmr.jurassicrevived.block.entity.custom.PowerCellBlockEntity; +import net.cmr.jurassicrevived.screen.ModMenuTypes; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ContainerLevelAccess; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; + +public class PowerCellMenu extends AbstractContainerMenu { + public final PowerCellBlockEntity blockEntity; + private final Level level; + + public PowerCellMenu(int pContainerId, Inventory inv, FriendlyByteBuf extraData) { + this(pContainerId, inv, inv.player.level().getBlockEntity(extraData.readBlockPos())); + } + + public PowerCellMenu(int pContainerId, Inventory inv, BlockEntity blockEntity) { + super(ModMenuTypes.POWER_CELL_MENU.get(), pContainerId); + this.blockEntity = ((PowerCellBlockEntity) blockEntity); + this.level = inv.player.level(); + + addPlayerInventory(inv); + addPlayerHotbar(inv); + + // Charging Slot (0) + this.addSlot(new Slot(this.blockEntity.itemHandler, 0, 44, 34)); + + // Discharging Slot (1) + this.addSlot(new Slot(this.blockEntity.itemHandler, 1, 116, 34) { + @Override + public int getMaxStackSize() { + return 1; + } + }); + } + + private static final int HOTBAR_SLOT_COUNT = 9; + private static final int PLAYER_INVENTORY_ROW_COUNT = 3; + private static final int PLAYER_INVENTORY_COLUMN_COUNT = 9; + private static final int PLAYER_INVENTORY_SLOT_COUNT = PLAYER_INVENTORY_COLUMN_COUNT * PLAYER_INVENTORY_ROW_COUNT; + private static final int VANILLA_SLOT_COUNT = HOTBAR_SLOT_COUNT + PLAYER_INVENTORY_SLOT_COUNT; + private static final int VANILLA_FIRST_SLOT_INDEX = 0; + private static final int TE_INVENTORY_FIRST_SLOT_INDEX = VANILLA_SLOT_COUNT; + private static final int TE_INVENTORY_SLOT_COUNT = 2; + + @Override + public ItemStack quickMoveStack(Player playerIn, int pIndex) { + Slot sourceSlot = slots.get(pIndex); + if (sourceSlot == null || !sourceSlot.hasItem()) return ItemStack.EMPTY; + ItemStack sourceStack = sourceSlot.getItem(); + ItemStack copyOfSourceStack = sourceStack.copy(); + + if (pIndex < VANILLA_SLOT_COUNT) { + if (!moveItemStackTo(sourceStack, TE_INVENTORY_FIRST_SLOT_INDEX, TE_INVENTORY_FIRST_SLOT_INDEX + + TE_INVENTORY_SLOT_COUNT, false)) { + return ItemStack.EMPTY; + } + } else if (pIndex < TE_INVENTORY_FIRST_SLOT_INDEX + TE_INVENTORY_SLOT_COUNT) { + if (!moveItemStackTo(sourceStack, VANILLA_FIRST_SLOT_INDEX, VANILLA_SLOT_COUNT, false)) { + return ItemStack.EMPTY; + } + } else { + return ItemStack.EMPTY; + } + + if (sourceStack.isEmpty()) { + sourceSlot.set(ItemStack.EMPTY); + } else { + sourceSlot.setChanged(); + } + sourceSlot.onTake(playerIn, sourceStack); + return copyOfSourceStack; + } + + @Override + public boolean stillValid(Player pPlayer) { + return stillValid(ContainerLevelAccess.create(level, blockEntity.getBlockPos()), + pPlayer, ModBlocks.POWER_CELL.get()); + } + + private void addPlayerInventory(Inventory playerInventory) { + for (int i = 0; i < 3; ++i) { + for (int l = 0; l < 9; ++l) { + this.addSlot(new Slot(playerInventory, l + i * 9 + 9, 8 + l * 18, 84 + i * 18)); + } + } + } + + private void addPlayerHotbar(Inventory playerInventory) { + for (int i = 0; i < 9; ++i) { + this.addSlot(new Slot(playerInventory, i, 8 + i * 18, 142)); + } + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/screen/custom/PowerCellScreen.java b/common/src/main/java/net/cmr/jurassicrevived/screen/custom/PowerCellScreen.java new file mode 100644 index 0000000..7e95ed8 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/screen/custom/PowerCellScreen.java @@ -0,0 +1,99 @@ +package net.cmr.jurassicrevived.screen.custom; + +import com.mojang.blaze3d.systems.RenderSystem; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.screen.renderer.EnergyDisplayTooltipAreaDouble; +import net.cmr.jurassicrevived.util.MouseUtil; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.player.Inventory; + +import java.util.Optional; + +public class PowerCellScreen extends AbstractContainerScreen { + private static final ResourceLocation GUI_TEXTURE = + Constants.rl("textures/gui/tank/tank_gui.png"); + private EnergyDisplayTooltipAreaDouble energyInfoArea; + + public PowerCellScreen(PowerCellMenu pMenu, Inventory pPlayerInventory, Component pTitle) { + super(pMenu, pPlayerInventory, pTitle); + } + + @Override + protected void init() { + super.init(); + // Gets rid of title and inventory title + this.inventoryLabelY = 10000; + this.titleLabelY = 10000; + + assignEnergyInfoArea(); + } + + private void renderEnergyAreaTooltip(GuiGraphics guiGraphics, int pMouseX, int pMouseY, int x, int y) { + if(isMouseAboveArea(pMouseX, pMouseY, x, y, 80, 8, 16, 64)) { + guiGraphics.renderTooltip(this.font, energyInfoArea.getTooltips(), + Optional.empty(), pMouseX - x, pMouseY - y); + } + } + + private void assignEnergyInfoArea() { + energyInfoArea = new EnergyDisplayTooltipAreaDouble(((width - imageWidth) / 2) + 80, + ((height - imageHeight) / 2) + 8, menu.blockEntity.getEnergyStorage(null)); + } + + @Override + protected void renderLabels(GuiGraphics guiGraphics, int pMouseX, int pMouseY) { + int x = (width - imageWidth) / 2; + int y = (height - imageHeight) / 2; + + renderEnergyAreaTooltip(guiGraphics, pMouseX, pMouseY, x, y); + } + + @Override + protected void renderBg(GuiGraphics guiGraphics, float pPartialTick, int pMouseX, int pMouseY) { + RenderSystem.setShader(GameRenderer::getPositionTexShader); + RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F); + RenderSystem.setShaderTexture(0, GUI_TEXTURE); + int x = (width - imageWidth) / 2; + int y = (height - imageHeight) / 2; + + guiGraphics.blit(GUI_TEXTURE, x, y, 0, 0, imageWidth, imageHeight); + + energyInfoArea.render(guiGraphics); + } + + @Override + public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float delta) { + //? if >1.20.1 { + /*this.renderBackground(guiGraphics, mouseX, mouseY, delta); + *///?} else { + renderBackground(guiGraphics); + //?} + super.render(guiGraphics, mouseX, mouseY, delta); + int x = (this.width - this.imageWidth) / 2; + int y = (this.height - this.imageHeight) /2; + renderTooltip(guiGraphics, mouseX, mouseY); + + // Draw hover highlight ABOVE background + fluid, but BELOW tooltips + if (MouseUtil.isMouseOver(mouseX, mouseY, x + 80, y + 8, 16, 64)) { + renderHoverHighlight(guiGraphics, x + 80, y + 8, 16, 64); + } + } + + private static void renderHoverHighlight(GuiGraphics g, int x, int y, int w, int h) { + // Push to a higher Z so it renders above the tank contents + var pose = g.pose(); + pose.pushPose(); + pose.translate(0.0F, 0.0F, 200.0F); // large enough to be on top of background/fluid + // Same color at top and bottom to avoid gradient banding; alpha ~0.5 + g.fillGradient(x, y, x + w, y + h, 0x80FFFFFF, 0x80FFFFFF); + pose.popPose(); + } + + private boolean isMouseAboveArea(int pMouseX, int pMouseY, int x, int y, int offsetX, int offsetY, int width, int height) { + return MouseUtil.isMouseOver(pMouseX, pMouseY, x + offsetX, y + offsetY, width, height); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/screen/custom/TankMenu.java b/common/src/main/java/net/cmr/jurassicrevived/screen/custom/TankMenu.java new file mode 100644 index 0000000..02d83aa --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/screen/custom/TankMenu.java @@ -0,0 +1,103 @@ +package net.cmr.jurassicrevived.screen.custom; + +import net.cmr.jurassicrevived.block.ModBlocks; +import net.cmr.jurassicrevived.block.entity.custom.TankBlockEntity; +import net.cmr.jurassicrevived.screen.ModMenuTypes; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ContainerLevelAccess; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; + +public class TankMenu extends AbstractContainerMenu { + public final TankBlockEntity blockEntity; + private final Level level; + + public TankMenu(int pContainerId, Inventory inv, FriendlyByteBuf extraData) { + this(pContainerId, inv, inv.player.level().getBlockEntity(extraData.readBlockPos())); + } + + public TankMenu(int pContainerId, Inventory inv, BlockEntity blockEntity) { + super(ModMenuTypes.TANK_MENU.get(), pContainerId); + this.blockEntity = ((TankBlockEntity) blockEntity); + this.level = inv.player.level(); + + addPlayerInventory(inv); + addPlayerHotbar(inv); + + // Input Slot (0) + this.addSlot(new Slot(this.blockEntity.itemHandler, 0, 44, 34)); + + // Output Slot (1) + this.addSlot(new Slot(this.blockEntity.itemHandler, 1, 116, 34) { + @Override + public int getMaxStackSize() { + return 1; + } + }); + } + + private static final int HOTBAR_SLOT_COUNT = 9; + private static final int PLAYER_INVENTORY_ROW_COUNT = 3; + private static final int PLAYER_INVENTORY_COLUMN_COUNT = 9; + private static final int PLAYER_INVENTORY_SLOT_COUNT = PLAYER_INVENTORY_COLUMN_COUNT * PLAYER_INVENTORY_ROW_COUNT; + private static final int VANILLA_SLOT_COUNT = HOTBAR_SLOT_COUNT + PLAYER_INVENTORY_SLOT_COUNT; + private static final int VANILLA_FIRST_SLOT_INDEX = 0; + private static final int TE_INVENTORY_FIRST_SLOT_INDEX = VANILLA_SLOT_COUNT; + private static final int TE_INVENTORY_SLOT_COUNT = 2; + + @Override + public ItemStack quickMoveStack(Player playerIn, int pIndex) { + Slot sourceSlot = slots.get(pIndex); + if (sourceSlot == null || !sourceSlot.hasItem()) return ItemStack.EMPTY; + ItemStack sourceStack = sourceSlot.getItem(); + ItemStack copyOfSourceStack = sourceStack.copy(); + + if (pIndex < VANILLA_SLOT_COUNT) { + // Player inventory -> tank slots + if (!moveItemStackTo(sourceStack, TE_INVENTORY_FIRST_SLOT_INDEX, TE_INVENTORY_FIRST_SLOT_INDEX + + TE_INVENTORY_SLOT_COUNT, false)) { + return ItemStack.EMPTY; + } + } else if (pIndex < TE_INVENTORY_FIRST_SLOT_INDEX + TE_INVENTORY_SLOT_COUNT) { + // Tank slots -> player inventory + if (!moveItemStackTo(sourceStack, VANILLA_FIRST_SLOT_INDEX, VANILLA_SLOT_COUNT, false)) { + return ItemStack.EMPTY; + } + } else { + return ItemStack.EMPTY; + } + + if (sourceStack.isEmpty()) { + sourceSlot.set(ItemStack.EMPTY); + } else { + sourceSlot.setChanged(); + } + sourceSlot.onTake(playerIn, sourceStack); + return copyOfSourceStack; + } + + @Override + public boolean stillValid(Player pPlayer) { + return stillValid(ContainerLevelAccess.create(level, blockEntity.getBlockPos()), + pPlayer, ModBlocks.TANK.get()); + } + + private void addPlayerInventory(Inventory playerInventory) { + for (int i = 0; i < 3; ++i) { + for (int l = 0; l < 9; ++l) { + this.addSlot(new Slot(playerInventory, l + i * 9 + 9, 8 + l * 18, 84 + i * 18)); + } + } + } + + private void addPlayerHotbar(Inventory playerInventory) { + for (int i = 0; i < 9; ++i) { + this.addSlot(new Slot(playerInventory, i, 8 + i * 18, 142)); + } + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/screen/custom/TankScreen.java b/common/src/main/java/net/cmr/jurassicrevived/screen/custom/TankScreen.java new file mode 100644 index 0000000..0e73cb7 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/screen/custom/TankScreen.java @@ -0,0 +1,84 @@ +package net.cmr.jurassicrevived.screen.custom; + +import com.mojang.blaze3d.systems.RenderSystem; +import dev.architectury.fluid.FluidStack; +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.screen.renderer.FluidTankRenderer; +import net.cmr.jurassicrevived.util.MouseUtil; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.item.TooltipFlag; + +import java.util.Optional; + +public class TankScreen extends AbstractContainerScreen { + private static final ResourceLocation GUI_TEXTURE = + Constants.rl("textures/gui/tank/tank_gui.png"); + private FluidTankRenderer fluidRenderer; + + public TankScreen(TankMenu pMenu, Inventory pPlayerInventory, Component pTitle) { + super(pMenu, pPlayerInventory, pTitle); + } + + @Override + protected void init() { + super.init(); + // Gets rid of title and inventory title + this.inventoryLabelY = 10000; + this.titleLabelY = 10000; + + assignFluidRenderer(); + } + + private void assignFluidRenderer() { + fluidRenderer = new FluidTankRenderer(64000, true, 16, 64); + } + + private void renderFluidTooltipArea(GuiGraphics guiGraphics, int pMouseX, int pMouseY, int x, int y, + FluidStack stack, int offsetX, int offsetY, FluidTankRenderer renderer) { + if(isMouseAboveArea(pMouseX, pMouseY, x, y, offsetX, offsetY, renderer)) { + guiGraphics.renderTooltip(this.font, renderer.getTooltip(stack, TooltipFlag.Default.NORMAL), + Optional.empty(), pMouseX - x, pMouseY - y); + } + } + + @Override + protected void renderLabels(GuiGraphics guiGraphics, int pMouseX, int pMouseY) { + int x = (width - imageWidth) / 2; + int y = (height - imageHeight) / 2; + + renderFluidTooltipArea(guiGraphics, pMouseX, pMouseY, x, y, menu.blockEntity.getFluid(), 80, 8, fluidRenderer); + } + + @Override + protected void renderBg(GuiGraphics guiGraphics, float pPartialTick, int pMouseX, int pMouseY) { + RenderSystem.setShader(GameRenderer::getPositionTexShader); + RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F); + RenderSystem.setShaderTexture(0, GUI_TEXTURE); + int x = (width - imageWidth) / 2; + int y = (height - imageHeight) / 2; + + guiGraphics.blit(GUI_TEXTURE, x, y, 0, 0, imageWidth, imageHeight); + + fluidRenderer.render(guiGraphics, x + 80, y + 8, menu.blockEntity.getFluid()); + } + + @Override + public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float delta) { + //? if >1.20.1 { + /*this.renderBackground(guiGraphics, mouseX, mouseY, delta); + *///?} else { + renderBackground(guiGraphics); + //?} + super.render(guiGraphics, mouseX, mouseY, delta); + renderTooltip(guiGraphics, mouseX, mouseY); + } + + public static boolean isMouseAboveArea(int pMouseX, int pMouseY, int x, int y, int offsetX, int offsetY, FluidTankRenderer renderer) { + return MouseUtil.isMouseOver(pMouseX, pMouseY, x + offsetX, y + offsetY, renderer.getWidth(), renderer.getHeight()); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/screen/renderer/EnergyDisplayTooltipArea.java b/common/src/main/java/net/cmr/jurassicrevived/screen/renderer/EnergyDisplayTooltipArea.java new file mode 100644 index 0000000..28f98fa --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/screen/renderer/EnergyDisplayTooltipArea.java @@ -0,0 +1,46 @@ +package net.cmr.jurassicrevived.screen.renderer; + +import net.cmr.jurassicrevived.block.entity.energy.ModEnergyStorage; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.network.chat.Component; + +import java.util.List; + +/* + * BluSunrize + * Copyright (c) 2021 + * + * This code is licensed under "Blu's License of Common Sense" + * https://github.com/BluSunrize/ImmersiveEngineering/blob/1.19.2/LICENSE + * + * Slightly Modified Version by: Kaupenjoe + */ +public class EnergyDisplayTooltipArea { + private final int xPos; + private final int yPos; + private final int width; + private final int height; + private final ModEnergyStorage energy; + + public EnergyDisplayTooltipArea(int xMin, int yMin, ModEnergyStorage energy) { + this(xMin, yMin, energy, 8, 64); + } + + public EnergyDisplayTooltipArea(int xMin, int yMin, ModEnergyStorage energy, int width, int height) { + xPos = xMin; + yPos = yMin; + this.width = width; + this.height = height; + this.energy = energy; + } + + public List getTooltips() { + return List.of(Component.literal(energy.getEnergyStored() + " / " + energy.getMaxEnergyStored() + " FE")); + } + + public void render(GuiGraphics guiGraphics) { + int stored = (int)(height * (energy.getEnergyStored() / (float)energy.getMaxEnergyStored())); + guiGraphics.fillGradient(xPos, yPos + (height - stored), xPos + width, + yPos + height, 0xffb51500, 0xff600b00); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/screen/renderer/EnergyDisplayTooltipAreaDouble.java b/common/src/main/java/net/cmr/jurassicrevived/screen/renderer/EnergyDisplayTooltipAreaDouble.java new file mode 100644 index 0000000..61f56bb --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/screen/renderer/EnergyDisplayTooltipAreaDouble.java @@ -0,0 +1,46 @@ +package net.cmr.jurassicrevived.screen.renderer; + +import net.cmr.jurassicrevived.block.entity.energy.ModEnergyStorage; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.network.chat.Component; + +import java.util.List; + +/* + * BluSunrize + * Copyright (c) 2021 + * + * This code is licensed under "Blu's License of Common Sense" + * https://github.com/BluSunrize/ImmersiveEngineering/blob/1.19.2/LICENSE + * + * Slightly Modified Version by: Kaupenjoe + */ +public class EnergyDisplayTooltipAreaDouble { + private final int xPos; + private final int yPos; + private final int width; + private final int height; + private final ModEnergyStorage energy; + + public EnergyDisplayTooltipAreaDouble(int xMin, int yMin, ModEnergyStorage energy) { + this(xMin, yMin, energy, 16, 64); + } + + public EnergyDisplayTooltipAreaDouble(int xMin, int yMin, ModEnergyStorage energy, int width, int height) { + xPos = xMin; + yPos = yMin; + this.width = width; + this.height = height; + this.energy = energy; + } + + public List getTooltips() { + return List.of(Component.literal(energy.getEnergyStored() + " / " + energy.getMaxEnergyStored() + " FE")); + } + + public void render(GuiGraphics guiGraphics) { + int stored = (int)(height * (energy.getEnergyStored() / (float)energy.getMaxEnergyStored())); + guiGraphics.fillGradient(xPos, yPos + (height - stored), xPos + width, + yPos + height, 0xffb51500, 0xff600b00); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/screen/renderer/FluidTankRenderer.java b/common/src/main/java/net/cmr/jurassicrevived/screen/renderer/FluidTankRenderer.java new file mode 100644 index 0000000..d5251e6 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/screen/renderer/FluidTankRenderer.java @@ -0,0 +1,200 @@ +package net.cmr.jurassicrevived.screen.renderer; + +import com.google.common.base.Preconditions; +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.vertex.*; +import dev.architectury.fluid.FluidStack; +import dev.architectury.hooks.fluid.FluidStackHooks; +import net.cmr.jurassicrevived.Constants; +import net.minecraft.ChatFormatting; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.client.renderer.texture.TextureAtlasSprite; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.inventory.InventoryMenu; +import net.minecraft.world.item.TooltipFlag; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.level.material.Fluids; +import org.joml.Matrix4f; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.text.NumberFormat; +import java.util.ArrayList; +import java.util.List; + +// CREDIT: https://github.com/mezz/JustEnoughItems by mezz +// Under MIT-License: https://github.com/mezz/JustEnoughItems/blob/1.19/LICENSE.txt +public class FluidTankRenderer { + private static final Logger LOGGER = LoggerFactory.getLogger(FluidTankRenderer.class); + private static final NumberFormat nf = NumberFormat.getIntegerInstance(); + private static final int TEXTURE_SIZE = 16; + private static final int MIN_FLUID_HEIGHT = 1; + + private final long capacity; + private final TooltipMode tooltipMode; + private final int width; + private final int height; + + enum TooltipMode { + SHOW_AMOUNT, + SHOW_AMOUNT_AND_CAPACITY, + ITEM_LIST + } + + public FluidTankRenderer(long capacity, boolean showCapacity, int width, int height) { + this(capacity, showCapacity ? TooltipMode.SHOW_AMOUNT_AND_CAPACITY : TooltipMode.SHOW_AMOUNT, width, height); + } + + private FluidTankRenderer(long capacity, TooltipMode tooltipMode, int width, int height) { + Preconditions.checkArgument(capacity > 0, "capacity must be > 0"); + Preconditions.checkArgument(width > 0, "width must be > 0"); + Preconditions.checkArgument(height > 0, "height must be > 0"); + + this.capacity = capacity; + this.tooltipMode = tooltipMode; + this.width = width; + this.height = height; + } + + public void render(GuiGraphics guiGraphics, int x, int y, FluidStack fluidStack) { + RenderSystem.enableBlend(); + guiGraphics.pose().pushPose(); + { + guiGraphics.pose().translate(x, y, 0); + drawFluid(guiGraphics, width, height, fluidStack); + } + guiGraphics.pose().popPose(); + RenderSystem.setShaderColor(1, 1, 1, 1); + RenderSystem.disableBlend(); + } + + private void drawFluid(GuiGraphics guiGraphics, final int width, final int height, FluidStack fluidStack) { + Fluid fluid = fluidStack.getFluid(); + if (fluid.isSame(Fluids.EMPTY)) { + return; + } + + TextureAtlasSprite fluidStillSprite = getStillFluidSprite(fluidStack); + int fluidColor = (int) FluidStackHooks.getColor(fluidStack); + + long amount = fluidStack.getAmount(); + long scaledAmount = (amount * height) / capacity; + + if (amount > 0 && scaledAmount < MIN_FLUID_HEIGHT) { + scaledAmount = MIN_FLUID_HEIGHT; + } + if (scaledAmount > height) { + scaledAmount = height; + } + + drawTiledSprite(guiGraphics, width, height, fluidColor, scaledAmount, fluidStillSprite); + } + + private TextureAtlasSprite getStillFluidSprite(FluidStack fluidStack) { + ResourceLocation fluidStill = FluidStackHooks.getStillTexture(fluidStack).atlasLocation(); + return Minecraft.getInstance().getTextureAtlas(InventoryMenu.BLOCK_ATLAS).apply(fluidStill); + } + + private static void drawTiledSprite(GuiGraphics guiGraphics, final int tiledWidth, final int tiledHeight, int color, long scaledAmount, TextureAtlasSprite sprite) { + RenderSystem.setShaderTexture(0, InventoryMenu.BLOCK_ATLAS); + Matrix4f matrix = guiGraphics.pose().last().pose(); + setGLColorFromInt(color); + + final int xTileCount = tiledWidth / TEXTURE_SIZE; + final int xRemainder = tiledWidth - (xTileCount * TEXTURE_SIZE); + final long yTileCount = scaledAmount / TEXTURE_SIZE; + final long yRemainder = scaledAmount - (yTileCount * TEXTURE_SIZE); + + final int yStart = tiledHeight; + + for (int xTile = 0; xTile <= xTileCount; xTile++) { + for (int yTile = 0; yTile <= yTileCount; yTile++) { + int width = (xTile == xTileCount) ? xRemainder : TEXTURE_SIZE; + long height = (yTile == yTileCount) ? yRemainder : TEXTURE_SIZE; + int x = (xTile * TEXTURE_SIZE); + int y = yStart - ((yTile + 1) * TEXTURE_SIZE); + if (width > 0 && height > 0) { + long maskTop = TEXTURE_SIZE - height; + int maskRight = TEXTURE_SIZE - width; + + drawTextureWithMasking(matrix, x, y, sprite, maskTop, maskRight, 100); + } + } + } + } + + private static void setGLColorFromInt(int color) { + float red = (color >> 16 & 0xFF) / 255.0F; + float green = (color >> 8 & 0xFF) / 255.0F; + float blue = (color & 0xFF) / 255.0F; + float alpha = ((color >> 24) & 0xFF) / 255F; + + RenderSystem.setShaderColor(red, green, blue, alpha); + } + + private static void drawTextureWithMasking(Matrix4f matrix, float xCoord, float yCoord, TextureAtlasSprite textureSprite, long maskTop, long maskRight, float zLevel) { + float uMin = textureSprite.getU0(); + float uMax = textureSprite.getU1(); + float vMin = textureSprite.getV0(); + float vMax = textureSprite.getV1(); + uMax = uMax - (maskRight / 16F * (uMax - uMin)); + vMax = vMax - (maskTop / 16F * (vMax - vMin)); + + RenderSystem.setShader(GameRenderer::getPositionTexShader); + + Tesselator tessellator = Tesselator.getInstance(); + //? if >1.20.1 { + /*BufferBuilder bufferBuilder = tessellator.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_TEX); + bufferBuilder.addVertex(matrix, xCoord, yCoord + 16, zLevel).setUv(uMin, vMax); + bufferBuilder.addVertex(matrix, xCoord + 16 - maskRight, yCoord + 16, zLevel).setUv(uMax, vMax); + bufferBuilder.addVertex(matrix, xCoord + 16 - maskRight, yCoord + maskTop, zLevel).setUv(uMax, vMin); + bufferBuilder.addVertex(matrix, xCoord, yCoord + maskTop, zLevel).setUv(uMin, vMin); + BufferUploader.drawWithShader(bufferBuilder.build()); + *///?} else { + BufferBuilder bufferBuilder = tessellator.getBuilder(); + bufferBuilder.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_TEX); + bufferBuilder.vertex(matrix, xCoord, yCoord + 16, zLevel).uv(uMin, vMax).endVertex(); + bufferBuilder.vertex(matrix, xCoord + 16 - maskRight, yCoord + 16, zLevel).uv(uMax, vMax).endVertex(); + bufferBuilder.vertex(matrix, xCoord + 16 - maskRight, yCoord + maskTop, zLevel).uv(uMax, vMin).endVertex(); + bufferBuilder.vertex(matrix, xCoord, yCoord + maskTop, zLevel).uv(uMin, vMin).endVertex(); + tessellator.end(); + //?} + } + + public List getTooltip(FluidStack fluidStack, TooltipFlag tooltipFlag) { + List tooltip = new ArrayList<>(); + + Fluid fluidType = fluidStack.getFluid(); + try { + if (fluidType.isSame(Fluids.EMPTY)) { + tooltip.add(Component.literal("Empty")); + tooltip.add(Component.translatable("jurassicrevived.tooltip.liquid.amount.with.capacity", 0, nf.format(capacity)).withStyle(ChatFormatting.GRAY)); + return tooltip; + } + + Component displayName = fluidStack.getName(); + tooltip.add(displayName); + + long amount = fluidStack.getAmount(); + + if (tooltipMode == TooltipMode.SHOW_AMOUNT_AND_CAPACITY) { + MutableComponent amountString = Component.translatable("jurassicrevived.tooltip.liquid.amount.with.capacity", nf.format(amount), nf.format(capacity)); + tooltip.add(amountString.withStyle(ChatFormatting.GRAY)); + } else if (tooltipMode == TooltipMode.SHOW_AMOUNT) { + MutableComponent amountString = Component.translatable("jurassicrevived.tooltip.liquid.amount", nf.format(amount)); + tooltip.add(amountString.withStyle(ChatFormatting.GRAY)); + } + } catch (RuntimeException e) { + LOGGER.error("Failed to get tooltip for fluid: " + e); + } + + return tooltip; + } + + public int getWidth() { return width; } + public int getHeight() { return height; } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/sound/MachineHumLoopSound.java b/common/src/main/java/net/cmr/jurassicrevived/sound/MachineHumLoopSound.java new file mode 100644 index 0000000..16b709e --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/sound/MachineHumLoopSound.java @@ -0,0 +1,43 @@ +package net.cmr.jurassicrevived.sound; + +import net.minecraft.client.resources.sounds.AbstractTickableSoundInstance; +import net.minecraft.client.resources.sounds.SoundInstance; +import net.minecraft.core.BlockPos; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.properties.BlockStateProperties; + +// Java +public class MachineHumLoopSound extends AbstractTickableSoundInstance { + private final Level level; + private final BlockPos pos; + + public MachineHumLoopSound(Level level, BlockPos pos) { + super(ModSounds.MACHINE_HUM_SOUND.get(), SoundSource.BLOCKS, SoundInstance.createUnseededRandom()); + this.level = level; + this.pos = pos.immutable(); + this.x = pos.getX() + 0.5; + this.y = pos.getY() + 0.5; + this.z = pos.getZ() + 0.5; + this.volume = 1.0f; + this.pitch = 1.0f; + this.looping = true; // loop + this.delay = 0; // no gap + this.relative = false; // world-positioned + } + + @Override + public void tick() { + if (this.isStopped()) return; + var state = level.getBlockState(pos); + // Stop if the block is gone or not lit + if (state.isAir() || !(state.hasProperty(BlockStateProperties.LIT) && state.getValue(BlockStateProperties.LIT))) { + this.stop(); + } + } + + // Public wrapper so callers can stop the sound + public void stopPlaying() { + this.stop(); + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/sound/MachineHumSoundHandler.java b/common/src/main/java/net/cmr/jurassicrevived/sound/MachineHumSoundHandler.java new file mode 100644 index 0000000..0c28996 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/sound/MachineHumSoundHandler.java @@ -0,0 +1,95 @@ +package net.cmr.jurassicrevived.sound; + +import dev.architectury.event.events.client.ClientTickEvent; +import net.cmr.jurassicrevived.block.custom.*; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.properties.BlockStateProperties; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +public final class MachineHumSoundHandler { + private static final Map ACTIVE_SOUNDS = new HashMap<>(); + + public static void init() { + ClientTickEvent.CLIENT_POST.register(MachineHumSoundHandler::onClientTick); + } + + private static void onClientTick(Minecraft mc) { + if (mc.isPaused()) return; + + ClientLevel level = mc.level; + if (level == null) { + stopAllSounds(); + return; + } + + // Clean up sounds + Iterator> it = ACTIVE_SOUNDS.entrySet().iterator(); + while (it.hasNext()) { + var entry = it.next(); + BlockPos pos = entry.getKey(); + MachineHumLoopSound sound = entry.getValue(); + + BlockState state = level.getBlockState(pos); + if (shouldStopHum(state) || sound.isStopped()) { + sound.stopPlaying(); + it.remove(); + } + } + + // Start sounds near player + if (mc.player != null) { + int radius = 16; + BlockPos center = mc.player.blockPosition(); + + for (BlockPos pos : BlockPos.betweenClosed( + center.offset(-radius, -radius, -radius), + center.offset(radius, radius, radius))) { + + BlockPos immutablePos = pos.immutable(); + if (ACTIVE_SOUNDS.containsKey(immutablePos)) continue; + + BlockState state = level.getBlockState(immutablePos); + if (shouldStartHum(state)) { + MachineHumLoopSound sound = new MachineHumLoopSound(level, immutablePos); + ACTIVE_SOUNDS.put(immutablePos, sound); + mc.getSoundManager().play(sound); + } + } + } + } + + private static boolean shouldStartHum(BlockState state) { + Block block = state.getBlock(); + // Check if the block is one of our machines + boolean isMachine = block instanceof DNAAnalyzerBlock + || block instanceof DNAExtractorBlock + || block instanceof DNAHybridizerBlock + || block instanceof EmbryoCalcificationMachineBlock + || block instanceof EmbryonicMachineBlock + || block instanceof FossilCleanerBlock + || block instanceof FossilGrinderBlock + || block instanceof GeneratorBlock + || block instanceof IncubatorBlock; + + if (!isMachine) return false; + + return state.hasProperty(BlockStateProperties.LIT) && state.getValue(BlockStateProperties.LIT); + } + + private static boolean shouldStopHum(BlockState state) { + if (state.isAir()) return true; + return !state.hasProperty(BlockStateProperties.LIT) || !state.getValue(BlockStateProperties.LIT); + } + + private static void stopAllSounds() { + ACTIVE_SOUNDS.values().forEach(MachineHumLoopSound::stopPlaying); + ACTIVE_SOUNDS.clear(); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/sound/ModSounds.java b/common/src/main/java/net/cmr/jurassicrevived/sound/ModSounds.java new file mode 100644 index 0000000..a4b6c96 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/sound/ModSounds.java @@ -0,0 +1,363 @@ +package net.cmr.jurassicrevived.sound; + +import dev.architectury.registry.registries.DeferredRegister; +import net.cmr.jurassicrevived.Constants; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.sounds.SoundEvent; + +import java.util.function.Supplier; + +public class ModSounds { + public static final DeferredRegister SOUND_EVENTS = + DeferredRegister.create(Constants.MOD_ID, Registries.SOUND_EVENT); + + public static final Supplier MACHINE_HUM_SOUND = registerSoundEvent("machine_hum"); + + public static final Supplier MEDIUM_THEROPOD_ATTACK = registerSoundEvent("medium_theropod_attack"); + public static final Supplier LARGE_THEROPOD_ATTACK = registerSoundEvent("large_theropod_attack"); + public static final Supplier TAIL_WHIP = registerSoundEvent("tail_whip"); + public static final Supplier TAIL_STRIKE = registerSoundEvent("tail_strike"); + public static final Supplier STOMP_ATTACK = registerSoundEvent("stomp_attack"); + public static final Supplier BEAK_ATTACK = registerSoundEvent("beak_attack"); + + public static final Supplier CROCODILE = registerSoundEvent("crocodile"); + public static final Supplier CROW = registerSoundEvent("crow"); + + public static final Supplier GOAT_CALL = registerSoundEvent("goat_call"); + public static final Supplier GOAT_HURT = registerSoundEvent("goat_hurt"); + public static final Supplier GOAT_DEATH = registerSoundEvent("goat_death"); + + public static final Supplier TREE_FROG_CALL = registerSoundEvent("tree_frog_call"); + public static final Supplier TREE_FROG_HURT = registerSoundEvent("tree_frog_hurt"); + public static final Supplier TREE_FROG_DEATH = registerSoundEvent("tree_frog_death"); + + public static final Supplier OSTRICH_CALL = registerSoundEvent("ostrich_call"); + public static final Supplier OSTRICH_HURT = registerSoundEvent("ostrich_hurt"); + public static final Supplier OSTRICH_DEATH = registerSoundEvent("ostrich_death"); + + public static final Supplier STOMP = registerSoundEvent("stomp"); + + public static final Supplier ALBERTOSAURUS_CALL = registerSoundEvent("albertosaurus_call"); + public static final Supplier ALBERTOSAURUS_HURT = registerSoundEvent("albertosaurus_hurt"); + public static final Supplier ALBERTOSAURUS_DEATH = registerSoundEvent("albertosaurus_death"); + + public static final Supplier ALLOSAURUS_CALL = registerSoundEvent("allosaurus_call"); + public static final Supplier ALLOSAURUS_HURT = registerSoundEvent("allosaurus_hurt"); + public static final Supplier ALLOSAURUS_DEATH = registerSoundEvent("allosaurus_death"); + + public static final Supplier ALVAREZSAURUS_CALL = registerSoundEvent("alvarezsaurus_call"); + public static final Supplier ALVAREZSAURUS_HURT = registerSoundEvent("alvarezsaurus_hurt"); + public static final Supplier ALVAREZSAURUS_DEATH = registerSoundEvent("alvarezsaurus_death"); + + public static final Supplier ANKYLOSAURUS_CALL = registerSoundEvent("ankylosaurus_call"); + public static final Supplier ANKYLOSAURUS_HURT = registerSoundEvent("ankylosaurus_hurt"); + public static final Supplier ANKYLOSAURUS_DEATH = registerSoundEvent("ankylosaurus_death"); + + public static final Supplier APATOSAURUS_CALL = registerSoundEvent("apatosaurus_call"); + public static final Supplier APATOSAURUS_HURT = registerSoundEvent("apatosaurus_hurt"); + public static final Supplier APATOSAURUS_DEATH = registerSoundEvent("apatosaurus_death"); + + public static final Supplier ARAMBOURGIANIA_CALL = registerSoundEvent("arambourgiania_call"); + public static final Supplier ARAMBOURGIANIA_HURT = registerSoundEvent("arambourgiania_hurt"); + public static final Supplier ARAMBOURGIANIA_DEATH = registerSoundEvent("arambourgiania_death"); + + public static final Supplier BARYONYX_CALL = registerSoundEvent("baryonyx_call"); + public static final Supplier BARYONYX_ATTACK = registerSoundEvent("baryonyx_attack"); + public static final Supplier BARYONYX_HURT = registerSoundEvent("baryonyx_hurt"); + public static final Supplier BARYONYX_DEATH = registerSoundEvent("baryonyx_death"); + + public static final Supplier BRACHIOSAURUS_CALL = registerSoundEvent("brachiosaurus_call"); + public static final Supplier BRACHIOSAURUS_HURT = registerSoundEvent("brachiosaurus_hurt"); + public static final Supplier BRACHIOSAURUS_DEATH = registerSoundEvent("brachiosaurus_death"); + + public static final Supplier CARCHARODONTOSAURUS_CALL = registerSoundEvent("carcharodontosaurus_call"); + public static final Supplier CARCHARODONTOSAURUS_HURT = registerSoundEvent("carcharodontosaurus_hurt"); + public static final Supplier CARCHARODONTOSAURUS_DEATH = registerSoundEvent("carcharodontosaurus_death"); + + public static final Supplier CARNOTAURUS_CALL = registerSoundEvent("carnotaurus_call"); + public static final Supplier CARNOTAURUS_ATTACK = registerSoundEvent("carnotaurus_attack"); + public static final Supplier CARNOTAURUS_HURT = registerSoundEvent("carnotaurus_hurt"); + public static final Supplier CARNOTAURUS_DEATH = registerSoundEvent("carnotaurus_death"); + + public static final Supplier CEARADACTYLUS_CALL = registerSoundEvent("cearadactylus_call"); + public static final Supplier CEARADACTYLUS_HURT = registerSoundEvent("cearadactylus_hurt"); + public static final Supplier CEARADACTYLUS_DEATH = registerSoundEvent("cearadactylus_death"); + + public static final Supplier CERATOSAURUS_CALL = registerSoundEvent("ceratosaurus_call"); + public static final Supplier CERATOSAURUS_HURT = registerSoundEvent("ceratosaurus_hurt"); + public static final Supplier CERATOSAURUS_DEATH = registerSoundEvent("ceratosaurus_death"); + + public static final Supplier CHASMOSAURUS_CALL = registerSoundEvent("chasmosaurus_call"); + public static final Supplier CHASMOSAURUS_ATTACK = registerSoundEvent("chasmosaurus_attack");//////////// + public static final Supplier CHASMOSAURUS_HURT = registerSoundEvent("chasmosaurus_hurt"); + public static final Supplier CHASMOSAURUS_DEATH = registerSoundEvent("chasmosaurus_death"); + + public static final Supplier CHICKENOSAURUS_CALL = registerSoundEvent("chickenosaurus_call"); + public static final Supplier CHICKENOSAURUS_HURT = registerSoundEvent("chickenosaurus_hurt"); + public static final Supplier CHICKENOSAURUS_DEATH = registerSoundEvent("chickenosaurus_death"); + + public static final Supplier COELOPHYSIS_CALL = registerSoundEvent("coelophysis_call"); + public static final Supplier COELOPHYSIS_HURT = registerSoundEvent("coelophysis_hurt"); + public static final Supplier COELOPHYSIS_DEATH = registerSoundEvent("coelophysis_death"); + + public static final Supplier COELURUS_CALL = registerSoundEvent("coelurus_call"); + public static final Supplier COELURUS_HURT = registerSoundEvent("coelurus_hurt"); + public static final Supplier COELURUS_DEATH = registerSoundEvent("coelurus_death"); + + public static final Supplier COMPSOGNATHUS_CALL = registerSoundEvent("compsognathus_call"); + public static final Supplier COMPSOGNATHUS_HURT = registerSoundEvent("compsognathus_hurt"); + public static final Supplier COMPSOGNATHUS_DEATH = registerSoundEvent("compsognathus_death"); + + public static final Supplier CONCAVENATOR_CALL = registerSoundEvent("concavenator_call"); + public static final Supplier CONCAVENATOR_HURT = registerSoundEvent("concavenator_hurt"); + public static final Supplier CONCAVENATOR_DEATH = registerSoundEvent("concavenator_death"); + + public static final Supplier CORYTHOSAURUS_CALL = registerSoundEvent("corythosaurus_call"); + public static final Supplier CORYTHOSAURUS_HURT = registerSoundEvent("corythosaurus_hurt"); + public static final Supplier CORYTHOSAURUS_DEATH = registerSoundEvent("corythosaurus_death"); + + public static final Supplier DEINONYCHUS_CALL = registerSoundEvent("deinonychus_call"); + public static final Supplier DEINONYCHUS_HURT = registerSoundEvent("deinonychus_hurt"); + public static final Supplier DEINONYCHUS_DEATH = registerSoundEvent("deinonychus_death"); + + public static final Supplier DILOPHOSAURUS_CALL = registerSoundEvent("dilophosaurus_call"); + public static final Supplier DILOPHOSAURUS_ATTACK = registerSoundEvent("dilophosaurus_attack"); + public static final Supplier DILOPHOSAURUS_HURT = registerSoundEvent("dilophosaurus_hurt"); + public static final Supplier DILOPHOSAURUS_DEATH = registerSoundEvent("dilophosaurus_death"); + + public static final Supplier DIMORPHODON_CALL = registerSoundEvent("dimorphodon_call"); + public static final Supplier DIMORPHODON_HURT = registerSoundEvent("dimorphodon_hurt"); + public static final Supplier DIMORPHODON_DEATH = registerSoundEvent("dimorphodon_death"); + + public static final Supplier DIPLODOCUS_CALL = registerSoundEvent("diplodocus_call"); + public static final Supplier DIPLODOCUS_HURT = registerSoundEvent("diplodocus_hurt"); + public static final Supplier DIPLODOCUS_DEATH = registerSoundEvent("diplodocus_death"); + + public static final Supplier DISTORTUS_REX_CALL = registerSoundEvent("distortus_rex_call"); + public static final Supplier DISTORTUS_REX_ATTACK = registerSoundEvent("distortus_rex_attack"); + public static final Supplier DISTORTUS_REX_HURT = registerSoundEvent("distortus_rex_hurt"); + public static final Supplier DISTORTUS_REX_DEATH = registerSoundEvent("distortus_rex_death"); + + public static final Supplier DRYOSAURUS_CALL = registerSoundEvent("dryosaurus_call"); + public static final Supplier DRYOSAURUS_HURT = registerSoundEvent("dryosaurus_hurt"); + public static final Supplier DRYOSAURUS_DEATH = registerSoundEvent("dryosaurus_death"); + + public static final Supplier EDMONTOSAURUS_CALL = registerSoundEvent("edmontosaurus_call"); + public static final Supplier EDMONTOSAURUS_HURT = registerSoundEvent("edmontosaurus_hurt"); + public static final Supplier EDMONTOSAURUS_DEATH = registerSoundEvent("edmontosaurus_death"); + + public static final Supplier FDUCK_CALL = registerSoundEvent("fduck_call"); + public static final Supplier FDUCK_HURT = registerSoundEvent("fduck_hurt"); + public static final Supplier FDUCK_DEATH = registerSoundEvent("fduck_death"); + + public static final Supplier GALLIMIMUS_CALL = registerSoundEvent("gallimimus_call"); + public static final Supplier GALLIMIMUS_HURT = registerSoundEvent("gallimimus_hurt"); + public static final Supplier GALLIMIMUS_DEATH = registerSoundEvent("gallimimus_death"); + + public static final Supplier GEOSTERNBERGIA_CALL = registerSoundEvent("geosternbergia_call"); + public static final Supplier GEOSTERNBERGIA_HURT = registerSoundEvent("geosternbergia_hurt"); + public static final Supplier GEOSTERNBERGIA_DEATH = registerSoundEvent("geosternbergia_death"); + + public static final Supplier GIGANOTOSAURUS_CALL = registerSoundEvent("giganotosaurus_call"); + public static final Supplier GIGANOTOSAURUS_ATTACK = registerSoundEvent("giganotosaurus_attack"); + public static final Supplier GIGANOTOSAURUS_HURT = registerSoundEvent("giganotosaurus_hurt"); + public static final Supplier GIGANOTOSAURUS_DEATH = registerSoundEvent("giganotosaurus_death"); + + public static final Supplier GUANLONG_CALL = registerSoundEvent("guanlong_call"); + public static final Supplier GUANLONG_HURT = registerSoundEvent("guanlong_hurt"); + public static final Supplier GUANLONG_DEATH = registerSoundEvent("guanlong_death"); + + public static final Supplier GUIDRACO_CALL = registerSoundEvent("guidraco_call"); + public static final Supplier GUIDRACO_HURT = registerSoundEvent("guidraco_hurt"); + public static final Supplier GUIDRACO_DEATH = registerSoundEvent("guidraco_death"); + + public static final Supplier HADROSAURUS_CALL = registerSoundEvent("hadrosaurus_call"); + public static final Supplier HADROSAURUS_HURT = registerSoundEvent("hadrosaurus_hurt"); + public static final Supplier HADROSAURUS_DEATH = registerSoundEvent("hadrosaurus_death"); + + public static final Supplier HERRERASAURUS_CALL = registerSoundEvent("herrerasaurus_call"); + public static final Supplier HERRERASAURUS_ATTACK = registerSoundEvent("herrerasaurus_attack"); + public static final Supplier HERRERASAURUS_HURT = registerSoundEvent("herrerasaurus_hurt"); + public static final Supplier HERRERASAURUS_DEATH = registerSoundEvent("herrerasaurus_death"); + + public static final Supplier HYPSILOPHODON_CALL = registerSoundEvent("hypsilophodon_call"); + public static final Supplier HYPSILOPHODON_HURT = registerSoundEvent("hypsilophodon_hurt"); + public static final Supplier HYPSILOPHODON_DEATH = registerSoundEvent("hypsilophodon_death"); + + public static final Supplier INDOMINUS_REX_CALL = registerSoundEvent("indominus_rex_call"); + public static final Supplier INDOMINUS_REX_ATTACK = registerSoundEvent("indominus_rex_attack"); + public static final Supplier INDOMINUS_REX_HURT = registerSoundEvent("indominus_rex_hurt"); + public static final Supplier INDOMINUS_REX_DEATH = registerSoundEvent("indominus_rex_death"); + + public static final Supplier INDORAPTOR_CALL = registerSoundEvent("indoraptor_call"); + public static final Supplier INDORAPTOR_ATTACK = registerSoundEvent("indoraptor_attack"); + public static final Supplier INDORAPTOR_HURT = registerSoundEvent("indoraptor_hurt"); + public static final Supplier INDORAPTOR_DEATH = registerSoundEvent("indoraptor_death"); + + public static final Supplier INOSTRANCEVIA_CALL = registerSoundEvent("inostrancevia_call"); + public static final Supplier INOSTRANCEVIA_ATTACK = registerSoundEvent("inostrancevia_attack"); + public static final Supplier INOSTRANCEVIA_HURT = registerSoundEvent("inostrancevia_hurt"); + public static final Supplier INOSTRANCEVIA_DEATH = registerSoundEvent("inostrancevia_death"); + + public static final Supplier LAMBEOSAURUS_CALL = registerSoundEvent("lambeosaurus_call"); + public static final Supplier LAMBEOSAURUS_HURT = registerSoundEvent("lambeosaurus_hurt"); + public static final Supplier LAMBEOSAURUS_DEATH = registerSoundEvent("lambeosaurus_death"); + + public static final Supplier LUDODACTYLUS_CALL = registerSoundEvent("ludodactylus_call"); + public static final Supplier LUDODACTYLUS_HURT = registerSoundEvent("ludodactylus_hurt"); + public static final Supplier LUDODACTYLUS_DEATH = registerSoundEvent("ludodactylus_death"); + + public static final Supplier MAJUNGASAURUS_CALL = registerSoundEvent("majungasaurus_call"); + public static final Supplier MAJUNGASAURUS_HURT = registerSoundEvent("majungasaurus_hurt"); + public static final Supplier MAJUNGASAURUS_DEATH = registerSoundEvent("majungasaurus_death"); + + public static final Supplier MAMENCHISAURUS_CALL = registerSoundEvent("mamenchisaurus_call"); + public static final Supplier MAMENCHISAURUS_HURT = registerSoundEvent("mamenchisaurus_hurt"); + public static final Supplier MAMENCHISAURUS_DEATH = registerSoundEvent("mamenchisaurus_death"); + + public static final Supplier METRIACANTHOSAURUS_CALL = registerSoundEvent("metriacanthosaurus_call"); + public static final Supplier METRIACANTHOSAURUS_HURT = registerSoundEvent("metriacanthosaurus_hurt"); + public static final Supplier METRIACANTHOSAURUS_DEATH = registerSoundEvent("metriacanthosaurus_death"); + + public static final Supplier MOGANOPTERUS_CALL = registerSoundEvent("moganopterus_call"); + public static final Supplier MOGANOPTERUS_HURT = registerSoundEvent("moganopterus_hurt"); + public static final Supplier MOGANOPTERUS_DEATH = registerSoundEvent("moganopterus_death"); + + public static final Supplier NYCTOSAURUS_CALL = registerSoundEvent("nyctosaurus_call"); + public static final Supplier NYCTOSAURUS_HURT = registerSoundEvent("nyctosaurus_hurt"); + public static final Supplier NYCTOSAURUS_DEATH = registerSoundEvent("nyctosaurus_death"); + + public static final Supplier ORNITHOLESTES_CALL = registerSoundEvent("ornitholestes_call"); + public static final Supplier ORNITHOLESTES_HURT = registerSoundEvent("ornitholestes_hurt"); + public static final Supplier ORNITHOLESTES_DEATH = registerSoundEvent("ornitholestes_death"); + + public static final Supplier ORNITHOMIMUS_CALL = registerSoundEvent("ornithomimus_call"); + public static final Supplier ORNITHOMIMUS_HURT = registerSoundEvent("ornithomimus_hurt"); + public static final Supplier ORNITHOMIMUS_DEATH = registerSoundEvent("ornithomimus_death"); + + public static final Supplier OURANOSAURUS_CALL = registerSoundEvent("ouranosaurus_call"); + public static final Supplier OURANOSAURUS_HURT = registerSoundEvent("ouranosaurus_hurt"); + public static final Supplier OURANOSAURUS_DEATH = registerSoundEvent("ouranosaurus_death"); + + public static final Supplier OVIRAPTOR_CALL = registerSoundEvent("oviraptor_call"); + public static final Supplier OVIRAPTOR_HURT = registerSoundEvent("oviraptor_hurt"); + public static final Supplier OVIRAPTOR_DEATH = registerSoundEvent("oviraptor_death"); + + public static final Supplier PACHYCEPHALOSAURUS_CALL = registerSoundEvent("pachycephalosaurus_call"); + public static final Supplier PACHYCEPHALOSAURUS_HURT = registerSoundEvent("pachycephalosaurus_hurt"); + public static final Supplier PACHYCEPHALOSAURUS_DEATH = registerSoundEvent("pachycephalosaurus_death"); + + public static final Supplier PARASAUROLOPHUS_CALL = registerSoundEvent("parasaurolophus_call"); + public static final Supplier PARASAUROLOPHUS_HURT = registerSoundEvent("parasaurolophus_hurt"); + public static final Supplier PARASAUROLOPHUS_DEATH = registerSoundEvent("parasaurolophus_death"); + + public static final Supplier PROCERATOSAURUS_CALL = registerSoundEvent("proceratosaurus_call"); + public static final Supplier PROCERATOSAURUS_HURT = registerSoundEvent("proceratosaurus_hurt"); + public static final Supplier PROCERATOSAURUS_DEATH = registerSoundEvent("proceratosaurus_death"); + + public static final Supplier PROCOMPSOGNATHUS_CALL = registerSoundEvent("procompsognathus_call"); + public static final Supplier PROCOMPSOGNATHUS_HURT = registerSoundEvent("procompsognathus_hurt"); + public static final Supplier PROCOMPSOGNATHUS_DEATH = registerSoundEvent("procompsognathus_death"); + + public static final Supplier PROTOCERATOPS_CALL = registerSoundEvent("protoceratops_call"); + public static final Supplier PROTOCERATOPS_HURT = registerSoundEvent("protoceratops_hurt"); + public static final Supplier PROTOCERATOPS_DEATH = registerSoundEvent("protoceratops_death"); + + public static final Supplier PTERANODON_CALL = registerSoundEvent("pteranodon_call"); + public static final Supplier PTERANODON_HURT = registerSoundEvent("pteranodon_hurt"); + public static final Supplier PTERANODON_DEATH = registerSoundEvent("pteranodon_death"); + + public static final Supplier PTERODAUSTRO_CALL = registerSoundEvent("pterodaustro_call"); + public static final Supplier PTERODAUSTRO_HURT = registerSoundEvent("pterodaustro_hurt"); + public static final Supplier PTERODAUSTRO_DEATH = registerSoundEvent("pterodaustro_death"); + + public static final Supplier QUETZALCOATLUS_CALL = registerSoundEvent("quetzalcoatlus_call"); + public static final Supplier QUETZALCOATLUS_HURT = registerSoundEvent("quetzalcoatlus_hurt"); + public static final Supplier QUETZALCOATLUS_DEATH = registerSoundEvent("quetzalcoatlus_death"); + + public static final Supplier RAJASAURUS_CALL = registerSoundEvent("rajasaurus_call"); + public static final Supplier RAJASAURUS_HURT = registerSoundEvent("rajasaurus_hurt"); + public static final Supplier RAJASAURUS_DEATH = registerSoundEvent("rajasaurus_death"); + + public static final Supplier RUGOPS_CALL = registerSoundEvent("rugops_call"); + public static final Supplier RUGOPS_HURT = registerSoundEvent("rugops_hurt"); + public static final Supplier RUGOPS_DEATH = registerSoundEvent("rugops_death"); + + public static final Supplier SEGISAURUS_CALL = registerSoundEvent("segisaurus_call"); + public static final Supplier SEGISAURUS_HURT = registerSoundEvent("segisaurus_hurt"); + public static final Supplier SEGISAURUS_DEATH = registerSoundEvent("segisaurus_death"); + + public static final Supplier SHANTUNGOSAURUS_CALL = registerSoundEvent("shantungosaurus_call"); + public static final Supplier SHANTUNGOSAURUS_HURT = registerSoundEvent("shantungosaurus_hurt"); + public static final Supplier SHANTUNGOSAURUS_DEATH = registerSoundEvent("shantungosaurus_death"); + + public static final Supplier SPINOSAURUS_CALL = registerSoundEvent("spinosaurus_call"); + public static final Supplier SPINOSAURUS_ATTACK = registerSoundEvent("spinosaurus_attack"); + public static final Supplier SPINOSAURUS_HURT = registerSoundEvent("spinosaurus_hurt"); + public static final Supplier SPINOSAURUS_DEATH = registerSoundEvent("spinosaurus_death"); + + public static final Supplier STEGOSAURUS_CALL = registerSoundEvent("stegosaurus_call"); + public static final Supplier STEGOSAURUS_HURT = registerSoundEvent("stegosaurus_hurt"); + public static final Supplier STEGOSAURUS_DEATH = registerSoundEvent("stegosaurus_death"); + + public static final Supplier STYRACOSAURUS_CALL = registerSoundEvent("styracosaurus_call"); + public static final Supplier STYRACOSAURUS_HURT = registerSoundEvent("styracosaurus_hurt"); + public static final Supplier STYRACOSAURUS_DEATH = registerSoundEvent("styracosaurus_death"); + + public static final Supplier TAPEJARA_CALL = registerSoundEvent("tapejara_call"); + public static final Supplier TAPEJARA_HURT = registerSoundEvent("tapejara_hurt"); + public static final Supplier TAPEJARA_DEATH = registerSoundEvent("tapejara_death"); + + public static final Supplier THERIZINOSAURUS_CALL = registerSoundEvent("therizinosaurus_call"); + public static final Supplier THERIZINOSAURUS_HURT = registerSoundEvent("therizinosaurus_hurt"); + public static final Supplier THERIZINOSAURUS_DEATH = registerSoundEvent("therizinosaurus_death"); + + public static final Supplier TITANOSAURUS_CALL = registerSoundEvent("titanosaurus_call"); + public static final Supplier TITANOSAURUS_HURT = registerSoundEvent("titanosaurus_hurt"); + public static final Supplier TITANOSAURUS_DEATH = registerSoundEvent("titanosaurus_death"); + + public static final Supplier TRICERATOPS_CALL = registerSoundEvent("triceratops_call"); + public static final Supplier TRICERATOPS_HURT = registerSoundEvent("triceratops_hurt"); + public static final Supplier TRICERATOPS_DEATH = registerSoundEvent("triceratops_death"); + + public static final Supplier TROODON_CALL = registerSoundEvent("troodon_call"); + public static final Supplier TROODON_HURT = registerSoundEvent("troodon_hurt"); + public static final Supplier TROODON_DEATH = registerSoundEvent("troodon_death"); + + public static final Supplier TROPEOGNATHUS_CALL = registerSoundEvent("tropeognathus_call"); + public static final Supplier TROPEOGNATHUS_HURT = registerSoundEvent("tropeognathus_hurt"); + public static final Supplier TROPEOGNATHUS_DEATH = registerSoundEvent("tropeognathus_death"); + + public static final Supplier TUPUXUARA_CALL = registerSoundEvent("tupuxuara_call"); + public static final Supplier TUPUXUARA_HURT = registerSoundEvent("tupuxuara_hurt"); + public static final Supplier TUPUXUARA_DEATH = registerSoundEvent("tupuxuara_death"); + + public static final Supplier TYRANNOSAURUS_REX_CALL = registerSoundEvent("tyrannosaurus_rex_call"); + public static final Supplier TYRANNOSAURUS_REX_ATTACK = registerSoundEvent("tyrannosaurus_rex_attack"); + public static final Supplier TYRANNOSAURUS_REX_HURT = registerSoundEvent("tyrannosaurus_rex_hurt"); + public static final Supplier TYRANNOSAURUS_REX_DEATH = registerSoundEvent("tyrannosaurus_rex_death"); + + public static final Supplier UTAHRAPTOR_CALL = registerSoundEvent("utahraptor_call"); + public static final Supplier UTAHRAPTOR_HURT = registerSoundEvent("utahraptor_hurt"); + public static final Supplier UTAHRAPTOR_DEATH = registerSoundEvent("utahraptor_death"); + + public static final Supplier VELOCIRAPTOR_CALL = registerSoundEvent("velociraptor_call"); + public static final Supplier VELOCIRAPTOR_ATTACK = registerSoundEvent("velociraptor_attack"); + public static final Supplier VELOCIRAPTOR_HURT = registerSoundEvent("velociraptor_hurt"); + public static final Supplier VELOCIRAPTOR_DEATH = registerSoundEvent("velociraptor_death"); + + public static final Supplier ZHENYUANOPTERUS_CALL = registerSoundEvent("zhenyuanopterus_call"); + public static final Supplier ZHENYUANOPTERUS_HURT = registerSoundEvent("zhenyuanopterus_hurt"); + public static final Supplier ZHENYUANOPTERUS_DEATH = registerSoundEvent("zhenyuanopterus_death"); + + + private static Supplier registerSoundEvent(String name) { + ResourceLocation id = Constants.rl(name); + return SOUND_EVENTS.register(name, () -> SoundEvent.createVariableRangeEvent(id)); + } + + public static void register() { + SOUND_EVENTS.register(); + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/util/FenceClimbClientHandler.java b/common/src/main/java/net/cmr/jurassicrevived/util/FenceClimbClientHandler.java new file mode 100644 index 0000000..33e108f --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/util/FenceClimbClientHandler.java @@ -0,0 +1,110 @@ +package net.cmr.jurassicrevived.util; + +import dev.architectury.event.events.client.ClientTickEvent; +import net.minecraft.client.Minecraft; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.core.BlockPos; +import net.minecraft.util.Mth; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; +import net.minecraft.world.phys.shapes.CollisionContext; +import net.minecraft.world.phys.shapes.VoxelShape; + +public final class FenceClimbClientHandler { + public static void register() { + // Architectury client tick event + ClientTickEvent.CLIENT_POST.register(minecraft -> { + LocalPlayer player = minecraft.player; + if (player == null) return; + + Level level = player.level(); + if (!player.isAlive()) return; + + AABB bb = player.getBoundingBox().inflate(0.05, 0.05, 0.05); + int minX = Mth.floor(bb.minX), maxX = Mth.floor(bb.maxX); + int minY = Mth.floor(bb.minY), maxY = Mth.floor(Math.min(bb.minY + 1.0, bb.maxY)); + int minZ = Mth.floor(bb.minZ), maxZ = Mth.floor(bb.maxZ); + + boolean touching = false; + outer: + for (int x = minX; x <= maxX; x++) { + for (int y = minY; y <= maxY; y++) { + for (int z = minZ; z <= maxZ; z++) { + BlockPos pos = new BlockPos(x, y, z); + BlockState state = level.getBlockState(pos); + + boolean isWire = state.getBlock() instanceof net.cmr.jurassicrevived.block.custom.FenceWireBlock; + boolean isPole = state.getBlock() instanceof net.cmr.jurassicrevived.block.custom.FencePoleBlock; + if (!isWire && !isPole) continue; + + VoxelShape shape = state.getCollisionShape(level, pos, CollisionContext.of(player)); + if (shape.isEmpty()) continue; + + if (isPole) { + AABB postAabbWorld = new AABB( + pos.getX() + 6 / 16.0, pos.getY() + 0.0, pos.getZ() + 6 / 16.0, + pos.getX() + 10 / 16.0, pos.getY() + 16 / 16.0, pos.getZ() + 10 / 16.0 + ); + for (AABB aabb : shape.toAabbs()) { + AABB moved = aabb.move(pos.getX(), pos.getY(), pos.getZ()); + if (approximatelySame(moved, postAabbWorld)) continue; + if (bb.intersects(moved) && notStandingOnTop(bb, moved)) { + touching = true; + break outer; + } + } + } else { + for (AABB aabb : shape.toAabbs()) { + AABB moved = aabb.move(pos.getX(), pos.getY(), pos.getZ()); + if (bb.intersects(moved) && notStandingOnTop(bb, moved)) { + touching = true; + break outer; + } + } + } + } + } + } + if (!touching) return; + + Vec3 v = player.getDeltaMovement(); + boolean sneaking = player.isShiftKeyDown(); + + double maxHoriz = 0.15; + double vx = Mth.clamp(v.x, -maxHoriz, maxHoriz); + double vz = Mth.clamp(v.z, -maxHoriz, maxHoriz); + + boolean forward = minecraft.options.keyUp.isDown(); + + double vy = v.y; + if (sneaking) { + vy = 0.0; + } else if (forward) { + vy = 0.2; + } else { + vy = Math.max(vy, -0.15); + } + + player.fallDistance = 0.0F; + player.setDeltaMovement(vx, vy, vz); + player.hasImpulse = true; + }); + } + + private static boolean approximatelySame(AABB a, AABB b) { + double eps = 1e-6; + return Math.abs(a.minX - b.minX) < eps && + Math.abs(a.minY - b.minY) < eps && + Math.abs(a.minZ - b.minZ) < eps && + Math.abs(a.maxX - b.maxX) < eps && + Math.abs(a.maxY - b.maxY) < eps && + Math.abs(a.maxZ - b.maxZ) < eps; + } + + private static boolean notStandingOnTop(AABB player, AABB block) { + double eps = 0.05; + return !(player.minY >= block.maxY - eps && player.minY <= block.maxY + eps); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/util/FenceClimbHandler.java b/common/src/main/java/net/cmr/jurassicrevived/util/FenceClimbHandler.java new file mode 100644 index 0000000..7a1c26d --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/util/FenceClimbHandler.java @@ -0,0 +1,106 @@ +package net.cmr.jurassicrevived.util; + +import dev.architectury.event.events.common.TickEvent; +import net.cmr.jurassicrevived.block.custom.FencePoleBlock; +import net.cmr.jurassicrevived.block.custom.FenceWireBlock; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.util.Mth; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; +import net.minecraft.world.phys.shapes.CollisionContext; + +public final class FenceClimbHandler { + + public static void register() { + // Architectury replacement for ServerTickEvent.Post + TickEvent.SERVER_POST.register(server -> { + for (ServerLevel level : server.getAllLevels()) { + for (ServerPlayer player : level.players()) { + if (!player.isAlive()) continue; + + boolean touching = false; + AABB bb = player.getBoundingBox().inflate(0.05, 0.05, 0.05); + int minX = Mth.floor(bb.minX), maxX = Mth.floor(bb.maxX); + int minY = Mth.floor(bb.minY), maxY = Mth.floor(Math.min(bb.minY + 1.0, bb.maxY)); + int minZ = Mth.floor(bb.minZ), maxZ = Mth.floor(bb.maxZ); + + outer: + for (int x = minX; x <= maxX; x++) { + for (int y = minY; y <= maxY; y++) { + for (int z = minZ; z <= maxZ; z++) { + BlockPos pos = new BlockPos(x, y, z); + var state = level.getBlockState(pos); + + boolean isWire = state.getBlock() instanceof FenceWireBlock; + boolean isPole = state.getBlock() instanceof FencePoleBlock; + if (!isWire && !isPole) continue; + + var shape = state.getCollisionShape(level, pos, CollisionContext.of(player)); + if (shape.isEmpty()) continue; + + if (isPole) { + AABB postAabb = new AABB(6 / 16.0, 0.0, 6 / 16.0, 10 / 16.0, 1.0, 10 / 16.0).move(pos.getX(), pos.getY(), pos.getZ()); + for (AABB aabb : shape.toAabbs()) { + AABB moved = aabb.move(pos.getX(), pos.getY(), pos.getZ()); + if (approximatelySame(moved, postAabb)) continue; + if (bb.intersects(moved) && notStandingOnTop(bb, moved)) { + touching = true; + break outer; + } + } + } else { + for (AABB aabb : shape.toAabbs()) { + AABB moved = aabb.move(pos.getX(), pos.getY(), pos.getZ()); + if (bb.intersects(moved) && notStandingOnTop(bb, moved)) { + touching = true; + break outer; + } + } + } + } + } + } + + if (!touching) continue; + + Vec3 v = player.getDeltaMovement(); + boolean sneaking = player.isShiftKeyDown(); + + double maxHoriz = 0.15; + double vx = Mth.clamp(v.x, -maxHoriz, maxHoriz); + double vz = Mth.clamp(v.z, -maxHoriz, maxHoriz); + + double vy = v.y; + if (sneaking) { + vy = Math.max(vy, 0.0); + } else { + vy = Math.max(vy, -0.15); + vy = Math.max(vy, 0.01); + } + + player.setOnGround(false); + player.fallDistance = 0.0F; + player.setDeltaMovement(vx, vy, vz); + player.hasImpulse = true; + } + } + }); + } + + private static boolean approximatelySame(AABB a, AABB b) { + double eps = 1e-6; + return Math.abs(a.minX - b.minX) < eps && + Math.abs(a.minY - b.minY) < eps && + Math.abs(a.minZ - b.minZ) < eps && + Math.abs(a.maxX - b.maxX) < eps && + Math.abs(a.maxY - b.maxY) < eps && + Math.abs(a.maxZ - b.maxZ) < eps; + } + + private static boolean notStandingOnTop(AABB player, AABB block) { + double eps = 0.05; + return !(player.minY >= block.maxY - eps && player.minY <= block.maxY + eps); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/util/FenceDiagonalHandler.java b/common/src/main/java/net/cmr/jurassicrevived/util/FenceDiagonalHandler.java new file mode 100644 index 0000000..fcf94a4 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/util/FenceDiagonalHandler.java @@ -0,0 +1,64 @@ +package net.cmr.jurassicrevived.util; + +import dev.architectury.event.events.common.BlockEvent; +import net.cmr.jurassicrevived.block.custom.FencePoleBlock; +import net.cmr.jurassicrevived.block.custom.FenceWireBlock; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockState; + +public final class FenceDiagonalHandler { + + public static void init() { + // Register for block placement + BlockEvent.PLACE.register((level, pos, state, entity) -> { + if (level instanceof Level) { + notifyDiagonalFences((Level) level, pos); + } + return dev.architectury.event.EventResult.pass(); + }); + + // Register for block breaking + BlockEvent.BREAK.register((level, pos, state, player, xp) -> { + if (level instanceof Level) { + notifyDiagonalFences((Level) level, pos); + } + return dev.architectury.event.EventResult.pass(); + }); + } + + private static void notifyDiagonalFences(Level level, BlockPos changedPos) { + BlockPos[] diagonals = new BlockPos[] { + changedPos.north().east(), + changedPos.south().east(), + changedPos.south().west(), + changedPos.north().west() + }; + + for (BlockPos p : diagonals) { + BlockState bs = level.getBlockState(p); + + if (bs.getBlock() instanceof FenceWireBlock) { + BlockState updated = bs + .setValue(FenceWireBlock.NE, FenceWireBlock.canConnectDiagonally(level, p, Direction.NORTH, Direction.EAST)) + .setValue(FenceWireBlock.SE, FenceWireBlock.canConnectDiagonally(level, p, Direction.SOUTH, Direction.EAST)) + .setValue(FenceWireBlock.SW, FenceWireBlock.canConnectDiagonally(level, p, Direction.SOUTH, Direction.WEST)) + .setValue(FenceWireBlock.NW, FenceWireBlock.canConnectDiagonally(level, p, Direction.NORTH, Direction.WEST)); + if (updated != bs) { + level.setBlock(p, updated, Block.UPDATE_CLIENTS); + } + } else if (bs.getBlock() instanceof FencePoleBlock) { + BlockState updated = bs + .setValue(FencePoleBlock.NE, FenceWireBlock.canConnectDiagonally(level, p, Direction.NORTH, Direction.EAST)) + .setValue(FencePoleBlock.SE, FenceWireBlock.canConnectDiagonally(level, p, Direction.SOUTH, Direction.EAST)) + .setValue(FencePoleBlock.SW, FenceWireBlock.canConnectDiagonally(level, p, Direction.SOUTH, Direction.WEST)) + .setValue(FencePoleBlock.NW, FenceWireBlock.canConnectDiagonally(level, p, Direction.NORTH, Direction.WEST)); + if (updated != bs) { + level.setBlock(p, updated, Block.UPDATE_CLIENTS); + } + } + } + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/util/FenceUpdateGuard.java b/common/src/main/java/net/cmr/jurassicrevived/util/FenceUpdateGuard.java new file mode 100644 index 0000000..1dfc5ba --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/util/FenceUpdateGuard.java @@ -0,0 +1,17 @@ +package net.cmr.jurassicrevived.util; + +public final class FenceUpdateGuard { + private static final ThreadLocal GUARD = ThreadLocal.withInitial(() -> false); + + public static boolean begin() { + if (Boolean.TRUE.equals(GUARD.get())) return false; + GUARD.set(true); + return true; + } + + public static void end() { + GUARD.set(false); + } + + private FenceUpdateGuard() {} +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/util/ModEvents.java b/common/src/main/java/net/cmr/jurassicrevived/util/ModEvents.java new file mode 100644 index 0000000..11452bf --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/util/ModEvents.java @@ -0,0 +1,29 @@ +package net.cmr.jurassicrevived.util; + +import dev.architectury.event.events.common.EntityEvent; +import net.cmr.jurassicrevived.entity.ai.DinoEntityBase; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.player.Player; + +public class ModEvents { + public static void init() { + EntityEvent.LIVING_DEATH.register((entity, source) -> { + // Check if the killer is our Dino + if (source.getEntity() instanceof DinoEntityBase attacker && attacker.isCarnivore()) { + + // 1. Feed the Dino + if (attacker.getDinoData() != null) { + float amount = attacker.getHungerReplenishment(entity); + attacker.getDinoData().modifyHunger(amount); + } + + // 2. Clear Drops Logic + // Note: Clearing drops directly in LIVING_DEATH is tricky cross-platform. + // It is better to use a Mixin to 'dropCustomDeathLoot' or + // handle it via a common LootCondition. + } + return dev.architectury.event.EventResult.pass(); + }); + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/util/ModTags.java b/common/src/main/java/net/cmr/jurassicrevived/util/ModTags.java new file mode 100644 index 0000000..a2bc9f4 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/util/ModTags.java @@ -0,0 +1,40 @@ +package net.cmr.jurassicrevived.util; + +import net.cmr.jurassicrevived.Constants; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.tags.TagKey; +import net.minecraft.world.item.Item; +import net.minecraft.world.level.block.Block; + +public class ModTags { + public static class Blocks { + public static final TagKey AQUATIC_PLACEMENT_REPLACEABLES = createTag("aquatic_placement_replaceables"); + public static final TagKey INCUBATED_EGGS = createTag("incubated_eggs"); + + private static TagKey createTag(String name) { + //? if >1.20.1 { + /*return TagKey.create(Registries.BLOCK, ResourceLocation.fromNamespaceAndPath(Constants.MOD_ID, name)); + *///?} else { + return TagKey.create(Registries.BLOCK, new ResourceLocation(Constants.MOD_ID, name)); + //?} + } + } + + public static class Items { + public static final TagKey TISSUES = createTag("tissues"); + public static final TagKey DNA = createTag("dna"); + public static final TagKey SYRINGES = createTag("syringes"); + public static final TagKey EGGS = createTag("eggs"); + public static final TagKey FOSSILS = createTag("fossils"); + public static final TagKey SKULLS = createTag("skulls"); + + private static TagKey createTag(String name) { + //? if >1.20.1 { + /*return TagKey.create(Registries.ITEM, ResourceLocation.fromNamespaceAndPath(Constants.MOD_ID, name)); + *///?} else { + return TagKey.create(Registries.ITEM, new ResourceLocation(Constants.MOD_ID, name)); + //?} + } + } +} \ No newline at end of file diff --git a/common/src/main/java/net/cmr/jurassicrevived/util/MouseUtil.java b/common/src/main/java/net/cmr/jurassicrevived/util/MouseUtil.java new file mode 100644 index 0000000..c35e7f9 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/util/MouseUtil.java @@ -0,0 +1,15 @@ +package net.cmr.jurassicrevived.util; + +public class MouseUtil { + public static boolean isMouseOver(double mouseX, double mouseY, int x, int y) { + return isMouseOver(mouseX, mouseY, x, y, 16); + } + + public static boolean isMouseOver(double mouseX, double mouseY, int x, int y, int size) { + return isMouseOver(mouseX, mouseY, x, y, size, size); + } + + public static boolean isMouseOver(double mouseX, double mouseY, int x, int y, int sizeX, int sizeY) { + return (mouseX >= x && mouseX <= x + sizeX) && (mouseY >= y && mouseY <= y + sizeY); + } +} diff --git a/common/src/main/resources/assets/jurassicrevived/lang/en_us.json b/common/src/main/resources/assets/jurassicrevived/lang/en_us.json index 1ea3907..379b2e0 100644 --- a/common/src/main/resources/assets/jurassicrevived/lang/en_us.json +++ b/common/src/main/resources/assets/jurassicrevived/lang/en_us.json @@ -1,9 +1,9 @@ { - "itemGroup.jurassicrevived.jurassicrevived_item_tab": "Items and Tools", - "itemGroup.jurassicrevived.jurassicrevived_block_tab": "Blocks and Machines", - "itemGroup.jurassicrevived.jurassicrevived_plant_tab": "Prehistoric Plants", - "itemGroup.jurassicrevived.jurassicrevived_dna_tab": "Genetic Material", - "itemGroup.jurassicrevived.jurassicrevived_dino_tab": "Dinosaur Spawn Eggs", + "itemGroup.jurassicrevived.item_tab": "Items and Tools", + "itemGroup.jurassicrevived.block_tab": "Blocks and Machines", + "itemGroup.jurassicrevived.plant_tab": "Prehistoric Plants", + "itemGroup.jurassicrevived.dna_tab": "Genetic Material", + "itemGroup.jurassicrevived.dino_tab": "Dinosaur Spawn Eggs", "item.jurassicrevived.apatosaurus_spawn_egg": "Apatosaurus Spawn Egg", "item.jurassicrevived.albertosaurus_spawn_egg": "Albertosaurus Spawn Egg", diff --git a/common/src/main/resources/jurassicrevived.mixins.json b/common/src/main/resources/jurassicrevived.mixins.json index 614973d..cdf36ff 100644 --- a/common/src/main/resources/jurassicrevived.mixins.json +++ b/common/src/main/resources/jurassicrevived.mixins.json @@ -1,16 +1,20 @@ { - "required": true, - "minVersion": "0.8", - "package": "net.cmr.jurassicrevived.mixin", - "refmap": "${mod_id}.refmap.json", - "compatibilityLevel": "JAVA_17", - "mixins": [], - "client": [ - "MixinMinecraft" - ], - "server": [], - "injectors": { - "defaultRequire": 1 + "required": true, + "minVersion": "0.8", + "package": "net.cmr.jurassicrevived.mixin", + "refmap": "${mod_id}.refmap.json", + "compatibilityLevel": "JAVA_17", + "mixins": [ + "FlowerPotBlockAccessor", + "LivingEntityMixin" + ], + "client": [ + "MenuScreensMixin", + "MixinMinecraft" + ], + "server": [], + "injectors": { + "defaultRequire": 1 } } diff --git a/fabricmc/build.gradle.kts b/fabricmc/build.gradle.kts index 42a6abc..d4e2c3b 100644 --- a/fabricmc/build.gradle.kts +++ b/fabricmc/build.gradle.kts @@ -49,6 +49,7 @@ repositories { maven("https://dl.cloudsmith.io/public/geckolib3/geckolib/maven/") maven("https://maven.blamejared.com/") maven("https://maven.shedaniel.me/") + maven("https://api.modrinth.com/maven") { name = "Modrinth" } } dependencies { @@ -75,6 +76,10 @@ dependencies { modCompileOnly("mezz.jei:jei-${commonMod.minecraft_version}-common-api:${commonMod.prop("jei_version")}") modCompileOnly("mezz.jei:jei-${commonMod.minecraft_version}-fabric-api:${commonMod.prop("jei_version")}") modLocalRuntime("mezz.jei:jei-${commonMod.minecraft_version}-fabric:${commonMod.prop("jei_version")}") + + // Jade + val jadeVersion = if (is120) commonMod.prop("jade_version_1_20_1") else commonMod.prop("jade_version_1_21_1") + modImplementation("maven.modrinth:jade:$jadeVersion+fabric") } loom { diff --git a/fabricmc/src/main/java/net/cmr/jurassicrevived/JRMod.java b/fabricmc/src/main/java/net/cmr/jurassicrevived/JRMod.java index cc80c0e..a7e2173 100644 --- a/fabricmc/src/main/java/net/cmr/jurassicrevived/JRMod.java +++ b/fabricmc/src/main/java/net/cmr/jurassicrevived/JRMod.java @@ -1,6 +1,12 @@ package net.cmr.jurassicrevived; +import net.cmr.jurassicrevived.block.entity.ModBlockEntities; +import net.cmr.jurassicrevived.block.entity.custom.*; import net.fabricmc.api.ModInitializer; +import net.fabricmc.fabric.api.transfer.v1.fluid.FluidStorage; +import net.fabricmc.fabric.api.transfer.v1.item.ItemStorage; +import net.fabricmc.fabric.api.transfer.v1.item.ItemVariant; +import net.fabricmc.fabric.api.transfer.v1.storage.Storage; public class JRMod implements ModInitializer { @@ -15,5 +21,16 @@ public class JRMod implements ModInitializer // Use Fabric to bootstrap the Common mod. Constants.LOG.info("Hello Fabric world!"); CommonClass.init(); + + /* + Items (Fabric Transfer API) + ItemStorage.SIDED.registerForBlockEntities((be, side) -> (Storage) ((CrateBlockEntity)be).itemHandler, ModBlockEntities.CRATE_BE.get()); + + Energy (TeamReborn Energy API is standard for Fabric) + EnergyStorage.SIDED.registerForBlockEntities((be, side) -> ((PowerCellBlockEntity)be).getEnergyStorage(side), ModBlockEntities.POWER_CELL_BE.get()); + + Fluids (Fabric Transfer API) + FluidStorage.SIDED.registerForBlockEntities((be, side) -> ((TankBlockEntity)be).getTank(side), ModBlockEntities.TANK_BE.get()); + */ } } diff --git a/fabricmc/src/main/java/net/cmr/jurassicrevived/JRModClient.java b/fabricmc/src/main/java/net/cmr/jurassicrevived/JRModClient.java new file mode 100644 index 0000000..fde5737 --- /dev/null +++ b/fabricmc/src/main/java/net/cmr/jurassicrevived/JRModClient.java @@ -0,0 +1,10 @@ +package net.cmr.jurassicrevived; + +import net.fabricmc.api.ClientModInitializer; + +public class JRModClient implements ClientModInitializer { + @Override + public void onInitializeClient() { + CommonClientClass.init(); + } +} diff --git a/fabricmc/src/main/resources/fabric.mod.json b/fabricmc/src/main/resources/fabric.mod.json index 272bcb7..0950819 100644 --- a/fabricmc/src/main/resources/fabric.mod.json +++ b/fabricmc/src/main/resources/fabric.mod.json @@ -18,6 +18,9 @@ "main": [ "net.cmr.jurassicrevived.JRMod" ], + "client": [ + "net.cmr.jurassicrevived.JRModClient" + ], "modmenu": [ "net.cmr.jurassicrevived.client.config.JRModMenuIntegration" ] @@ -41,4 +44,3 @@ "another-mod": "*" } } - \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 90c5aea..1a1d8c6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -36,4 +36,9 @@ java.version=21 architectury_version=9.2.14 cloth_config_version_1_20_1=11.1.136 -cloth_config_version_1_21_1=15.0.140 \ No newline at end of file +cloth_config_version_1_21_1=15.0.140 + +# Jade +jade_version=14.3.4 +jade_version_1_20_1=11.13.1 +jade_version_1_21_1=15.10.4 \ No newline at end of file diff --git a/minecraftforge/build.gradle.kts b/minecraftforge/build.gradle.kts index e3ed47a..83ed321 100644 --- a/minecraftforge/build.gradle.kts +++ b/minecraftforge/build.gradle.kts @@ -5,6 +5,24 @@ plugins { // id("net.minecraftforge.accesstransformers") } +val is120 = commonMod.minecraft_version.startsWith("1.20") + +// Define repositories first so they are available for dependencies and mavenizer +repositories { + maven("https://api.modrinth.com/maven") { name = "Modrinth" } + maven { + name = "CurseMaven" + url = uri("https://www.cursemaven.com") + content { + includeGroup("curse.maven") + } + } + maven("https://maven.architectury.dev/") + maven("https://maven.terraformersmc.com/releases/") + maven("https://dl.cloudsmith.io/public/geckolib3/geckolib/maven/") + maven("https://maven.blamejared.com/") +} + minecraft.mavenizer(repositories) println( @@ -16,12 +34,12 @@ println( ) minecraft { - /*mappings( + mappings( if (commonMod.propOrNull("parchment_mappings") != null) "parchment" else "official", if (commonMod.propOrNull("parchment_mappings") != null) "${commonMod.minecraft_version}-${commonMod.prop("parchment_mappings")}" else commonMod.minecraft_version - )*/ - mappings("official", commonMod.minecraft_version) + ) + // REMOVED: mappings("official", commonMod.minecraft_version) -- this was overriding the block above //setAccessTransformers(true) @@ -38,7 +56,12 @@ minecraft { //args ("-mixin.config=${commonMod.id}.mixins.json") //classpath(sourceSets.main.get()) - } + // This ensures the implementation dependencies are on the runtime classpath + lazy { + val jadeVersion = if (is120) commonMod.prop("jade_version_1_20_1") else commonMod.prop("jade_version_1_21_1") + property("forge.enabledGameTestNamespaces", commonMod.id) + } + } register("client") { systemProperty("forge.enabledGameTestNamespaces", commonMod.id) @@ -88,30 +111,32 @@ dependencies { // then special handling is done to allow a setup of a vanilla dependency without the use of an external repository. implementation(minecraft.dependency("net.minecraftforge:forge:${commonMod.minecraft_version}-${commonMod.prop("minecraftforge_version")}")) - // Forge 1.21.6+ uses EventBus 7, which shifts most of its runtime validation to compile-time via an annotation processor - // to improve performance in production environments. This line is required to enable said compile-time validation - // in your development environment, helping you catch issues early. - if (stonecutter.eval(stonecutter.current.version, ">=1.21.6")) - annotationProcessor("net.minecraftforge:eventbus-validator:${commonMod.prop("minecraftforge_eventbus_validator_version")}") + // Jade - Use fg.deobf to ensure the obfuscated mod jar is remapped for the dev environment + if (is120) { + val jadeDep = fg.deobf("curse.maven:jade-324717:6855440") + implementation(jadeDep) + compileOnly(jadeDep) + } else { + val jadeVersion = commonMod.prop("jade_version_1_21_1") + val jadeDep = fg.deobf("maven.modrinth:jade:$jadeVersion+forge") + implementation(jadeDep) + compileOnly(jadeDep) + } - // Example mod dependency with JEI - // The JEI API is declared for compile time use, while the full JEI artifact is used at runtime - //compileOnly "mezz.jei:jei-${mc_version}-common-api:${jei_version}" - //compileOnly "mezz.jei:jei-${mc_version}-forge-api:${jei_version}" - //runtimeOnly "mezz.jei:jei-${mc_version}-forge:${jei_version}" + // Forge 1.21.6+ uses EventBus 7, which shifts most of its runtime validation to compile-time via an annotation processor + // to improve performance in production environments. This line is required to enable said compile-time validation + // in your development environment, helping you catch issues early. + if (stonecutter.eval(stonecutter.current.version, ">=1.21.6")) + annotationProcessor("net.minecraftforge:eventbus-validator:${commonMod.prop("minecraftforge_eventbus_validator_version")}") - // Example mod dependency using a mod jar from ./libs with a flat dir repository - // This maps to ./libs/coolmod-${mc_version}-${coolmod_version}.jar - // The group id is ignored when searching -- in this case, it is "blank" - // NOTE: Support for deobfuscated dependencies has not yet been added in ForgeGradle 7. - //implementation "blank:coolmod-${mc_version}:${coolmod_version}" - - // For more info: - // http://www.gradle.org/docs/current/userguide/artifact_dependencies_tutorial.html - // http://www.gradle.org/docs/current/userguide/dependency_management.html + // Example mod dependency with JEI + // The JEI API is declared for compile time use, while the full JEI artifact is used at runtime + //compileOnly "mezz.jei:jei-${mc_version}-common-api:${jei_version}" + //compileOnly "mezz.jei:jei-${mc_version}-forge-api:${jei_version}" + //runtimeOnly "mezz.jei:jei-${mc_version}-forge:${jei_version}" } -tasks.withType(JavaCompile::class).configureEach { + tasks.withType(JavaCompile::class).configureEach { options.encoding = "UTF-8" // Use the UTF-8 charset for Java compilation } diff --git a/minecraftforge/legacy.gradle.kts b/minecraftforge/legacy.gradle.kts index dc6f01c..c739077 100644 --- a/minecraftforge/legacy.gradle.kts +++ b/minecraftforge/legacy.gradle.kts @@ -28,6 +28,7 @@ legacyForge { repositories { mavenCentral() + maven("https://api.modrinth.com/maven") { name = "Modrinth" } maven("https://maven.architectury.dev/") maven("https://maven.terraformersmc.com/releases/") maven("https://dl.cloudsmith.io/public/geckolib3/geckolib/maven/") @@ -49,6 +50,12 @@ dependencies { modCompileOnly("mezz.jei:jei-${commonMod.minecraft_version}-common-api:${commonMod.prop("jei_version")}") modCompileOnly("mezz.jei:jei-${commonMod.minecraft_version}-forge-api:${commonMod.prop("jei_version")}") modRuntimeOnly("mezz.jei:jei-${commonMod.minecraft_version}-forge:${commonMod.prop("jei_version")}") + + // Jade + val jadeVersion = if (isForge1201) commonMod.prop("jade_version_1_20_1") else commonMod.prop("jade_version_1_21_1") + val jadeDep = "maven.modrinth:jade:$jadeVersion+forge" + modImplementation(jadeDep) + modCompileOnly(jadeDep) } diff --git a/minecraftforge/src/main/java/net/cmr/jurassicrevived/JRMod.java b/minecraftforge/src/main/java/net/cmr/jurassicrevived/JRMod.java index c0465e1..1014063 100644 --- a/minecraftforge/src/main/java/net/cmr/jurassicrevived/JRMod.java +++ b/minecraftforge/src/main/java/net/cmr/jurassicrevived/JRMod.java @@ -2,7 +2,9 @@ package net.cmr.jurassicrevived; import dev.architectury.platform.forge.EventBuses; import net.cmr.jurassicrevived.client.config.JRClothConfigScreens; +import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.client.ConfigScreenHandler; +import net.minecraftforge.fml.DistExecutor; import net.minecraftforge.fml.ModLoadingContext; import net.minecraftforge.fml.common.Mod; import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; @@ -23,6 +25,7 @@ public class JRMod { Constants.LOG.info("Hello Forge world!"); CommonClass.init(); + DistExecutor.safeRunWhenOn(Dist.CLIENT, () -> CommonClientClass::init); ModLoadingContext.get().registerExtensionPoint( ConfigScreenHandler.ConfigScreenFactory.class, diff --git a/neoforge/build.gradle.kts b/neoforge/build.gradle.kts index af169dd..3dab8d8 100644 --- a/neoforge/build.gradle.kts +++ b/neoforge/build.gradle.kts @@ -13,6 +13,7 @@ repositories { maven("https://dl.cloudsmith.io/public/geckolib3/geckolib/maven/") maven("https://maven.blamejared.com/") maven("https://maven.shedaniel.me/") + maven("https://api.modrinth.com/maven") { name = "Modrinth" } } java { @@ -53,6 +54,10 @@ dependencies { compileOnly("mezz.jei:jei-${commonMod.minecraft_version}-common-api:${commonMod.prop("jei_version")}") compileOnly("mezz.jei:jei-${commonMod.minecraft_version}-neoforge-api:${commonMod.prop("jei_version")}") runtimeOnly("mezz.jei:jei-${commonMod.minecraft_version}-neoforge:${commonMod.prop("jei_version")}") + + // Jade + val jadeVersion = if (is120) commonMod.prop("jade_version_1_20_1") else commonMod.prop("jade_version_1_21_1") + implementation("maven.modrinth:jade:$jadeVersion+neoforge") } neoForge { diff --git a/neoforge/src/main/java/net/cmr/jurassicrevived/JRMod.java b/neoforge/src/main/java/net/cmr/jurassicrevived/JRMod.java index ff06244..b588116 100644 --- a/neoforge/src/main/java/net/cmr/jurassicrevived/JRMod.java +++ b/neoforge/src/main/java/net/cmr/jurassicrevived/JRMod.java @@ -4,6 +4,7 @@ import net.cmr.jurassicrevived.client.config.JRClothConfigScreens; import net.neoforged.bus.api.IEventBus; import net.neoforged.fml.ModLoadingContext; import net.neoforged.fml.common.Mod; +import net.neoforged.fml.loading.FMLEnvironment; import net.neoforged.neoforge.client.gui.IConfigScreenFactory; import java.util.function.Supplier; @@ -19,5 +20,10 @@ public class JRMod { IConfigScreenFactory.class, (Supplier) () -> (client, parent) -> JRClothConfigScreens.create(parent) ); + + // Add this to initialize client-side logic on NeoForge + if (FMLEnvironment.dist.isClient()) { + CommonClientClass.init(); + } } } \ No newline at end of file diff --git a/neoforge/src/main/java/net/cmr/jurassicrevived/event/NeoForgeEvents.java b/neoforge/src/main/java/net/cmr/jurassicrevived/event/NeoForgeEvents.java new file mode 100644 index 0000000..20a7dc6 --- /dev/null +++ b/neoforge/src/main/java/net/cmr/jurassicrevived/event/NeoForgeEvents.java @@ -0,0 +1,50 @@ +package net.cmr.jurassicrevived.event; + +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.block.entity.ModBlockEntities; +import net.cmr.jurassicrevived.config.JRConfigManager; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.common.EventBusSubscriber; +import net.neoforged.neoforge.capabilities.Capabilities; +import net.neoforged.neoforge.capabilities.RegisterCapabilitiesEvent; + +@EventBusSubscriber(modid = Constants.MOD_ID) +public class NeoForgeEvents +{ + @SubscribeEvent + public static void registerCapabilities(RegisterCapabilitiesEvent event) { + /* Items + event.registerBlockEntity(Capabilities.ItemHandler.BLOCK, ModBlockEntities.GENERATOR_BE.get(), (be, side) -> be.getItemHandler(side)); + event.registerBlockEntity(Capabilities.ItemHandler.BLOCK, ModBlockEntities.DNA_EXTRACTOR_BE.get(), (be, side) -> be.getItemHandler(side)); + event.registerBlockEntity(Capabilities.ItemHandler.BLOCK, ModBlockEntities.DNA_ANALYZER_BE.get(), (be, side) -> be.getItemHandler(side)); + event.registerBlockEntity(Capabilities.ItemHandler.BLOCK, ModBlockEntities.DNA_HYBRIDIZER_BE.get(), (be, side) -> be.getItemHandler(side)); + event.registerBlockEntity(Capabilities.ItemHandler.BLOCK, ModBlockEntities.FOSSIL_CLEANER_BE.get(), (be, side) -> be.getItemHandler(side)); + event.registerBlockEntity(Capabilities.ItemHandler.BLOCK, ModBlockEntities.FOSSIL_GRINDER_BE.get(), (be, side) -> be.getItemHandler(side)); + event.registerBlockEntity(Capabilities.ItemHandler.BLOCK, ModBlockEntities.EMBRYONIC_MACHINE_BE.get(), (be, side) -> be.getItemHandler(side)); + event.registerBlockEntity(Capabilities.ItemHandler.BLOCK, ModBlockEntities.EMBRYO_CALCIFICATION_MACHINE_BE.get(), (be, side) -> be.getItemHandler(side)); + event.registerBlockEntity(Capabilities.ItemHandler.BLOCK, ModBlockEntities.CRATE_BE.get(), (be, side) -> be.getItemHandler(side)); + event.registerBlockEntity(Capabilities.ItemHandler.BLOCK, ModBlockEntities.INCUBATOR_BE.get(), (be, side) -> be.getItemHandler(side)); + + Energy + event.registerBlockEntity(Capabilities.EnergyStorage.BLOCK, ModBlockEntities.POWER_CELL_BE.get(), (be, side) -> be.getEnergyStorage(side)); + event.registerBlockEntity(Capabilities.EnergyStorage.BLOCK, ModBlockEntities.GENERATOR_BE.get(), (be, side) -> be.getEnergyStorage(side)); + + Fluids + event.registerBlockEntity(Capabilities.FluidHandler.BLOCK, ModBlockEntities.TANK_BE.get(), (be, side) -> be.getTank(side)); + event.registerBlockEntity(Capabilities.FluidHandler.BLOCK, ModBlockEntities.FOSSIL_CLEANER_BE.get(), (be, side) -> be.getTank(side)); + + Machine Logic (if config allows) + if (JRConfigManager.get().requirePower) { + event.registerBlockEntity(Capabilities.EnergyStorage.BLOCK, ModBlockEntities.DNA_EXTRACTOR_BE.get(), (be, side) -> be.getEnergyStorage(side)); + event.registerBlockEntity(Capabilities.EnergyStorage.BLOCK, ModBlockEntities.DNA_ANALYZER_BE.get(), (be, side) -> be.getEnergyStorage(side)); + event.registerBlockEntity(Capabilities.EnergyStorage.BLOCK, ModBlockEntities.FOSSIL_CLEANER_BE.get(), (be, side) -> be.getEnergyStorage(side)); + event.registerBlockEntity(Capabilities.EnergyStorage.BLOCK, ModBlockEntities.FOSSIL_GRINDER_BE.get(), (be, side) -> be.getEnergyStorage(side)); + event.registerBlockEntity(Capabilities.EnergyStorage.BLOCK, ModBlockEntities.DNA_HYBRIDIZER_BE.get(), (be, side) -> be.getEnergyStorage(side)); + event.registerBlockEntity(Capabilities.EnergyStorage.BLOCK, ModBlockEntities.EMBRYONIC_MACHINE_BE.get(), (be, side) -> be.getEnergyStorage(side)); + event.registerBlockEntity(Capabilities.EnergyStorage.BLOCK, ModBlockEntities.EMBRYO_CALCIFICATION_MACHINE_BE.get(), (be, side) -> be.getEnergyStorage(side)); + event.registerBlockEntity(Capabilities.EnergyStorage.BLOCK, ModBlockEntities.INCUBATOR_BE.get(), (be, side) -> be.getEnergyStorage(side)); + } + + */ + } +} diff --git a/stonecutter.gradle.kts b/stonecutter.gradle.kts index 4f52a35..874e85f 100644 --- a/stonecutter.gradle.kts +++ b/stonecutter.gradle.kts @@ -11,4 +11,4 @@ plugins { } if (IS_CI) stonecutter active null -else stonecutter active "1.21.1" /* [SC] DO NOT EDIT */ \ No newline at end of file +else stonecutter active "1.20.1" /* [SC] DO NOT EDIT */ \ No newline at end of file