From 7640d77b1ef0ba015c031becb1b6b68c10d31d1b Mon Sep 17 00:00:00 2001 From: Eli Gibbs Date: Thu, 11 Jun 2026 17:14:10 -0400 Subject: [PATCH] starts to add coelacanth and marine AI --- .../jurassicrevived/CommonClientClass.java | 1 + .../jurassicrevived/entity/ModEntities.java | 5 + .../entity/ai/DinoAIController.java | 147 ++- .../entity/ai/DinoEntityBase.java | 5 +- .../jurassicrevived/entity/ai/IDinoData.java | 2 +- .../entity/client/CoelacanthModel.java | 112 ++ .../entity/client/CoelacanthRenderer.java | 21 + .../entity/client/CoelacanthVariant.java | 26 + .../entity/custom/CoelacanthEntity.java | 401 ++++++ .../animations/coelacanth.animation.json | 1120 +++++++++++++++++ .../jurassicrevived/geo/coelacanth.geo.json | 202 +++ .../textures/entity/coelacanth.png | Bin 0 -> 10676 bytes .../textures/entity/coelacanth_female.png | Bin 0 -> 10180 bytes 13 files changed, 2019 insertions(+), 23 deletions(-) create mode 100644 common/src/main/java/net/cmr/jurassicrevived/entity/client/CoelacanthModel.java create mode 100644 common/src/main/java/net/cmr/jurassicrevived/entity/client/CoelacanthRenderer.java create mode 100644 common/src/main/java/net/cmr/jurassicrevived/entity/client/CoelacanthVariant.java create mode 100644 common/src/main/java/net/cmr/jurassicrevived/entity/custom/CoelacanthEntity.java create mode 100644 common/src/main/resources/assets/jurassicrevived/animations/coelacanth.animation.json create mode 100644 common/src/main/resources/assets/jurassicrevived/geo/coelacanth.geo.json create mode 100644 common/src/main/resources/assets/jurassicrevived/textures/entity/coelacanth.png create mode 100644 common/src/main/resources/assets/jurassicrevived/textures/entity/coelacanth_female.png diff --git a/common/src/main/java/net/cmr/jurassicrevived/CommonClientClass.java b/common/src/main/java/net/cmr/jurassicrevived/CommonClientClass.java index 474bc6b..f646fcf 100644 --- a/common/src/main/java/net/cmr/jurassicrevived/CommonClientClass.java +++ b/common/src/main/java/net/cmr/jurassicrevived/CommonClientClass.java @@ -115,6 +115,7 @@ public class CommonClientClass { EntityRendererRegistry.register(ModEntities.CHILESAURUS, ChilesaurusRenderer::new); EntityRendererRegistry.register(ModEntities.MUSSASAURUS, MussasaurusRenderer::new); EntityRendererRegistry.register(ModEntities.THESCELOSAURUS, ThescelosaurusRenderer::new); + EntityRendererRegistry.register(ModEntities.COELACANTH, CoelacanthRenderer::new); if (Platform.isFabric()) { registerSpawnEggColors(); 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 4b49479..6855201 100644 --- a/common/src/main/java/net/cmr/jurassicrevived/entity/ModEntities.java +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/ModEntities.java @@ -351,6 +351,10 @@ public class ModEntities { ENTITIES.register("suchomimus", () -> EntityType.Builder.of(SuchomimusEntity::new, MobCategory.CREATURE) .sized(1.3f, 1.8f).build("suchomimus")); + public static final RegistrySupplier> COELACANTH = + ENTITIES.register("coelacanth", () -> EntityType.Builder.of(CoelacanthEntity::new, MobCategory.CREATURE) + .sized(1.3f, 1.8f).build("coelacanth")); + public static void registerAttributes() { EntityAttributeRegistry.register(APATOSAURUS, ApatosaurusEntity::createAttributes); EntityAttributeRegistry.register(ALBERTOSAURUS, AlbertosaurusEntity::createAttributes); @@ -432,6 +436,7 @@ public class ModEntities { EntityAttributeRegistry.register(CHILESAURUS, ChilesaurusEntity::createAttributes); EntityAttributeRegistry.register(MUSSASAURUS, MussasaurusEntity::createAttributes); EntityAttributeRegistry.register(THESCELOSAURUS, ThescelosaurusEntity::createAttributes); + EntityAttributeRegistry.register(COELACANTH, CoelacanthEntity::createAttributes); } public static void registerSpawnPlacements() { 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 index 1f701f7..4bd4514 100755 --- a/common/src/main/java/net/cmr/jurassicrevived/entity/ai/DinoAIController.java +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/ai/DinoAIController.java @@ -70,6 +70,10 @@ public class DinoAIController { private static final int HERBIVORE_BROWSE_VERTICAL_RANGE = 2; private static final float HERBIVORE_SELF_FEED_HUNGER_THRESHOLD = 0.75f; private static final float HERBIVORE_SELF_FEED_REPLENISHMENT_MULTIPLIER = 0.50f; + private static final double MARINE_ROAM_SPEED_MULTIPLIER = 1.0D; + private static final double MARINE_FLEE_SPEED_MULTIPLIER = 1.6D; + private static final int MARINE_ROAM_RANGE = 12; + private static final int MARINE_ROAM_VERTICAL_RANGE = 6; private final DinoEntityBase dino; @@ -175,6 +179,10 @@ public class DinoAIController { return; } + if (dino.isMarine()) { + return; + } + if (handleWaterMovementHelper(null)) { return; } @@ -266,6 +274,20 @@ public class DinoAIController { float hungerDecay = jrConfig.hungerConsumption ? config.hungerDecay() * VITAL_DECAY_MULTIPLIER : 0.0f; float thirstDecay = jrConfig.waterConsumption ? config.thirstDecay() * VITAL_DECAY_MULTIPLIER : 0.0f; + if (dino.isMarine()) { + thirstDecay = 0.0f; + if (dino.dinoData != null) { + dino.dinoData.setThirst(config.maxThirst()); + } + } + + if (dino.isSimpleFish()) { + hungerDecay = 0.0f; + if (dino.dinoData != null) { + dino.dinoData.setHunger(config.maxHunger()); + } + } + if (currentState == State.SLEEPING) { hungerDecay *= 0.5f; thirstDecay *= 0.5f; @@ -381,7 +403,7 @@ public class DinoAIController { } // 5. Hunt check - if ((currentState == State.IDLE || currentState == State.ROAMING || currentState == State.TERRITORIAL_ROAMING) && dino.isCarnivore()) { + if ((currentState == State.IDLE || currentState == State.ROAMING || currentState == State.TERRITORIAL_ROAMING) && dino.isCarnivore() && !dino.isSimpleFish()) { JRConfig jrConfig = JRConfigManager.get(); boolean hungerConsumptionEnabled = jrConfig.hungerConsumption; boolean waterConsumptionEnabled = jrConfig.waterConsumption; @@ -407,11 +429,30 @@ public class DinoAIController { } // 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(); - } - } + if (!dino.isMarine() && (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(); + } + } + + if (dino.isMarine()) { + if (dino.isInWater()) { + // Reset air supply when submerged + dino.setAirSupply(dino.getMaxAirSupply()); + } else { + // Vanilla automatically adds +4 air every tick on land. + // Subtracting 5 counters it for a perfect net loss of -1 per tick. + dino.setAirSupply(dino.getAirSupply() - 5); + + // When air runs out (-20 is the vanilla grace period before taking damage) + if (dino.getAirSupply() <= -20) { + // Setting to 0 means it will take another 20 ticks to hit -20 again, + // resulting in exactly 1 tick of damage per second, just like vanilla. + dino.setAirSupply(0); + dino.hurt(dino.damageSources().dryOut(), 2.0F); + } + } + } } private void findWater() { @@ -783,6 +824,11 @@ public class DinoAIController { return; } + if (dino.isMarine()) { + transitionTo(State.ROAMING); + return; + } + float territoriality = 0.0f; if (dino.dinoData != null) { territoriality = dino.dinoData.getTerritoriality(); @@ -1055,6 +1101,16 @@ public class DinoAIController { private void findAndSetRoamTarget() { this.roamTarget = null; + if (dino.isMarine()) { + Vec3 marinePos = getMarineRoamPos(); + if (marinePos != null) { + if (dino.getNavigation().moveTo(marinePos.x, marinePos.y, marinePos.z, getRoamSpeed() * MARINE_ROAM_SPEED_MULTIPLIER)) { + this.roamTarget = marinePos; + } + } + return; // Prevent falling through to ground/flying logic + } + // Grounded flyers should walk to nearby grounded targets instead of taking off immediately. if (dino instanceof FlyingAnimal && dino.onGround()) { Vec3 groundPos = getNearbyGroundRoamPosForFlyer(); @@ -1262,6 +1318,10 @@ public class DinoAIController { return false; } + if (dino.isSimpleFish()) { + return false; + } + if (dino.tickCount % HERBIVORE_SELF_FEED_INTERVAL != 0 || dino.getRandom().nextFloat() > HERBIVORE_SELF_FEED_CHANCE) { return false; } @@ -1586,21 +1646,66 @@ public class DinoAIController { } } - private void tickFleeing() { - if (attackTarget == null) { - transitionTo(State.IDLE); - return; - } + 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.getNavigation().isDone() || stateTimer % 10 == 0) { + Vec3 awayDir; - if (dino.distanceToSqr(attackTarget) > 48 * 48) { - transitionTo(State.IDLE); - } - } + if (dino.isMarine()) { + awayDir = getMarineFleePos(attackTarget.position()); + } else { + awayDir = DefaultRandomPos.getPosAway(dino, 16, 7, attackTarget.position()); + } + + if (awayDir != null) { + double speed = dino.isMarine() ? dino.getAIConfig().runSpeed() * MARINE_FLEE_SPEED_MULTIPLIER : dino.getAIConfig().runSpeed() * 1.2; + dino.getNavigation().moveTo(awayDir.x, awayDir.y, awayDir.z, speed); + } + } + + if (dino.distanceToSqr(attackTarget) > 48 * 48) { + transitionTo(State.IDLE); + } + } + + private Vec3 getMarineRoamPos() { + for (int i = 0; i < 10; i++) { + BlockPos randomPos = dino.blockPosition().offset( + dino.getRandom().nextInt(MARINE_ROAM_RANGE * 2) - MARINE_ROAM_RANGE, + dino.getRandom().nextInt(MARINE_ROAM_VERTICAL_RANGE * 2) - MARINE_ROAM_VERTICAL_RANGE, + dino.getRandom().nextInt(MARINE_ROAM_RANGE * 2) - MARINE_ROAM_RANGE + ); + + if (dino.level().getFluidState(randomPos).is(FluidTags.WATER)) { + return Vec3.atCenterOf(randomPos); + } + } + return null; + } + + private Vec3 getMarineFleePos(Vec3 threatPos) { + // Calculate a vector exactly opposite to the threat + Vec3 awayVector = dino.position().subtract(threatPos).normalize().scale(12); + BlockPos targetPos = BlockPos.containing(dino.position().add(awayVector)); + + // Try to find a valid water block in that general panic direction + for (int i = 0; i < 5; i++) { + BlockPos offsetPos = targetPos.offset( + dino.getRandom().nextInt(4) - 2, + dino.getRandom().nextInt(4) - 2, + dino.getRandom().nextInt(4) - 2 + ); + + if (dino.level().getFluidState(offsetPos).is(FluidTags.WATER)) { + return Vec3.atCenterOf(offsetPos); + } + } + + // Fallback: Just swim erratically if trapped + return getMarineRoamPos(); + } } \ 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 index 84e9bb8..b16d20f 100755 --- a/common/src/main/java/net/cmr/jurassicrevived/entity/ai/DinoEntityBase.java +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/ai/DinoEntityBase.java @@ -107,7 +107,10 @@ public abstract class DinoEntityBase extends Animal { public abstract boolean isCarnivore(); public abstract boolean isMarine(); public abstract boolean isAmphibious(); - public abstract Block getEggBlock(); // New hook for breeding + public boolean isSimpleFish() { + return false; + } + public abstract Block getEggBlock(); public abstract DinoAIConfig getAIConfig(); public record DinoAIConfig(double walkSpeed, double runSpeed, double attackReach, 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 index 55a61df..c0e695f 100755 --- a/common/src/main/java/net/cmr/jurassicrevived/entity/ai/IDinoData.java +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/ai/IDinoData.java @@ -41,7 +41,7 @@ public interface IDinoData { Type getType(); void setType(Type type); - enum Group { THEROPOD, THYREOPHORAN, CERAPOD, SAUROPOD, ORNITHOPOD, AMPHIBIAN, ARCHOSAUROMORPH, PLEURODIRE, PTEROSAUR, REPTILIOMORPH, SQUAMATE } + enum Group { THEROPOD, THYREOPHORAN, CERAPOD, SAUROPOD, ORNITHOPOD, AMPHIBIAN, ARCHOSAUROMORPH, PLEURODIRE, PTEROSAUR, REPTILIOMORPH, SQUAMATE, PISCINE } Group getGroup(); void setGroup(Group group); diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/CoelacanthModel.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/CoelacanthModel.java new file mode 100644 index 0000000..228e21d --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/CoelacanthModel.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.CoelacanthEntity; +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 CoelacanthModel extends GeoModel { + + private static final Map LOCATION_BY_VARIANT = + Util.make(Maps.newEnumMap(CoelacanthVariant.class), map -> { + map.put(CoelacanthVariant.MALE, Constants.rl("textures/entity/coelacanth.png")); + map.put(CoelacanthVariant.FEMALE, Constants.rl("textures/entity/coelacanth_female.png")); + }); + + // Model-local "currently applied" offsets; cleared before each entity render + private float[] appliedYaw = null; + private float[] appliedRoll = null; + + @Override + public ResourceLocation getModelResource(CoelacanthEntity animatable) { + return Constants.rl("geo/coelacanth.geo.json"); + } + + @Override + public ResourceLocation getTextureResource(CoelacanthEntity animatable) { + return LOCATION_BY_VARIANT.get(animatable.getVariant()); + } + + @Override + public ResourceLocation getAnimationResource(CoelacanthEntity animatable) { + return Constants.rl("animations/coelacanth.animation.json"); + } + + @Override + public void setCustomAnimations(CoelacanthEntity 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/CoelacanthRenderer.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/CoelacanthRenderer.java new file mode 100644 index 0000000..1c97921 --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/CoelacanthRenderer.java @@ -0,0 +1,21 @@ +package net.cmr.jurassicrevived.entity.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.cmr.jurassicrevived.entity.custom.CoelacanthEntity; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import software.bernie.geckolib.cache.object.BakedGeoModel; +import software.bernie.geckolib.renderer.GeoEntityRenderer; + + +public class CoelacanthRenderer extends GeoEntityRenderer { + public CoelacanthRenderer(EntityRendererProvider.Context renderManager) { + super(renderManager, new CoelacanthModel()); + } + + @Override + public void scaleModelForRender(float widthScale, float heightScale, PoseStack poseStack, CoelacanthEntity animatable, BakedGeoModel model, boolean isReRender, float partialTick, int packedLight, int packedOverlay) { + super.scaleModelForRender(widthScale, heightScale, poseStack, animatable, model, isReRender, partialTick, packedLight, packedOverlay); + float scale = animatable.getTotalModelScale(); + poseStack.scale(scale, scale, scale); + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/client/CoelacanthVariant.java b/common/src/main/java/net/cmr/jurassicrevived/entity/client/CoelacanthVariant.java new file mode 100644 index 0000000..485b2bb --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/client/CoelacanthVariant.java @@ -0,0 +1,26 @@ +package net.cmr.jurassicrevived.entity.client; + +import java.util.Arrays; +import java.util.Comparator; + +public enum CoelacanthVariant { + MALE(0), + FEMALE(1); + + private static final CoelacanthVariant[] BY_ID = Arrays.stream(values()).sorted( + Comparator.comparingInt(CoelacanthVariant::getId)).toArray(CoelacanthVariant[]::new); + + private final int id; + + CoelacanthVariant(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static CoelacanthVariant byId(int id) { + return BY_ID[id % BY_ID.length]; + } +} diff --git a/common/src/main/java/net/cmr/jurassicrevived/entity/custom/CoelacanthEntity.java b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/CoelacanthEntity.java new file mode 100644 index 0000000..1efe8eb --- /dev/null +++ b/common/src/main/java/net/cmr/jurassicrevived/entity/custom/CoelacanthEntity.java @@ -0,0 +1,401 @@ +package net.cmr.jurassicrevived.entity.custom; + +import net.cmr.jurassicrevived.Constants; +import net.cmr.jurassicrevived.block.ModBlocks; +import net.cmr.jurassicrevived.entity.ModEntities; +import net.cmr.jurassicrevived.entity.ai.DinoData; +import net.cmr.jurassicrevived.entity.ai.DinoEntityBase; +import net.cmr.jurassicrevived.entity.ai.IDinoData; +import net.cmr.jurassicrevived.entity.client.CoelacanthVariant; +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.control.SmoothSwimmingMoveControl; +import net.minecraft.world.entity.ai.navigation.PathNavigation; +import net.minecraft.world.entity.ai.navigation.WaterBoundPathNavigation; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.ServerLevelAccessor; +import net.minecraft.world.level.block.Block; +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 CoelacanthEntity extends DinoEntityBase implements GeoEntity { + private AnimatableInstanceCache cache = new SingletonAnimatableInstanceCache(this); + + public static final int BABY_TO_ADULT_AGE_TICKS = 28800; + private static final float ANIMAL_SCALE = 1.0F; + private static final float MIN_ANIMAL_SCALE = !Constants.DEBUG_SIZES ? (ANIMAL_SCALE - 0.2F) : ANIMAL_SCALE; + private static final float MAX_ANIMAL_SCALE = !Constants.DEBUG_SIZES ? (ANIMAL_SCALE + 0.2F) : ANIMAL_SCALE; + + private float lastDimensionsScale = 1.0F; + + private static final EntityDataAccessor VARIANT = + SynchedEntityData.defineId(CoelacanthEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_SYNCED_AGE = + SynchedEntityData.defineId(CoelacanthEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_ANIMAL_SCALE = + SynchedEntityData.defineId(CoelacanthEntity.class, EntityDataSerializers.FLOAT); + + // 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 CoelacanthEntity(EntityType pEntityType, Level pLevel) { + super(pEntityType, pLevel); + + this.dinoData = new DinoData( + getAIConfig().maxHunger(), + getAIConfig().maxThirst(), + IDinoData.Mood.NEUTRAL, + IDinoData.Aggression.TERRITORIAL, + 0.75f, + IDinoData.DietaryClassification.CARNIVORE, + IDinoData.Type.MARINE, + IDinoData.Group.PISCINE, + IDinoData.BirthType.EGG_LAYING, + IDinoData.ActivityPattern.CATHEMERAL + ); + + // Replaces the default move control with 3D swimming mechanics + this.moveControl = new SmoothSwimmingMoveControl(this, 85, 10, 0.02F, 0.1F, true); + + // Tells the pathfinder that water is perfectly safe to navigate through + /*? if <=1.20.1 {*/ + this.setPathfindingMalus(net.minecraft.world.level.pathfinder.BlockPathTypes.WATER, 0.0F); + /*?} else {*/ + /*this.setPathfindingMalus(net.minecraft.world.level.pathfinder.PathType.WATER, 0.0F); + *//*?}*/ + } + + @Override + protected PathNavigation createNavigation(Level pLevel) { + return new WaterBoundPathNavigation(this, pLevel); + } + + //@Override + //public ItemStack getPickResult() { + // return new ItemStack(ModItems.COELACANTH_SPAWN_EGG.get()); + //} + + @Override + public boolean isCarnivore() { + return true; + } + + @Override + public boolean isMarine() { + return true; + } + + @Override + public boolean isAmphibious() { + return false; + } + + @Override + public boolean isSimpleFish() { + return true; + } + + @Override + public Block getEggBlock() { + return null; + } + + @Override + public DinoAIConfig getAIConfig() { + return new DinoAIConfig(0.3D, 1.1D, 1.5D, 100, 100, 0.05f, 0.1f, 20); + } + + @Override + public void setBaby(boolean baby) { + this.setAge(baby ? -BABY_TO_ADULT_AGE_TICKS : 0); + } + + 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); + } + + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel pLevel, AgeableMob pOtherParent) { + AgeableMob child = ModEntities.COELACANTH.get().create(pLevel); + if (child instanceof CoelacanthEntity baby) { + CoelacanthVariant randomVariant = Util.getRandom(CoelacanthVariant.values(), this.random); + baby.setVariant(randomVariant); + baby.setBaby(true); + baby.setAnimalScale(Mth.nextFloat(this.random, MIN_ANIMAL_SCALE, MAX_ANIMAL_SCALE)); + } + 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, "SwimController", 5, state -> { + // 1. Highest Priority: Out of water flapping + if (!this.isInWater()) { + return state.setAndContinue(RawAnimation.begin().then("anim.coelacanth.flop", Animation.LoopType.LOOP)); + } + + // 2. Aquatic movement: Just swim + return state.setAndContinue(RawAnimation.begin().then("anim.coelacanth.swim", Animation.LoopType.LOOP)); + })); + + controllers.add(new AnimationController<>(this, "attackController", 5, state -> PlayState.STOP) + .triggerableAnim("attack", RawAnimation.begin().then("anim.coelacanth.attack", Animation.LoopType.PLAY_ONCE))); + + controllers.add(new AnimationController<>(this, "mouthController", 5, state -> PlayState.STOP) + .triggerableAnim("mouth", RawAnimation.begin().then("anim.coelacanth.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))); + } + } + } + + updateDynamicDimensions(); + + if (!level().isClientSide) { + if (mouthAnimCooldown > 0) { + mouthAnimCooldown--; + } else { + this.triggerAnim("mouthController", "mouth"); + // 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); + this.entityData.define(DATA_ANIMAL_SCALE, 1.0F); + } + /*?} else {*/ + /*@Override + protected void defineSynchedData(SynchedEntityData.Builder pBuilder) { + super.defineSynchedData(pBuilder); + pBuilder.define(VARIANT, 0); + pBuilder.define(DATA_SYNCED_AGE, 0); + pBuilder.define(DATA_ANIMAL_SCALE, 1.0F); + } + *//*?}*/ + + public int getSyncedAge() { + return this.entityData.get(DATA_SYNCED_AGE); + } + + public float getAnimalScale() { + return this.entityData.get(DATA_ANIMAL_SCALE); + } + + private void setAnimalScale(float animalScale) { + this.entityData.set(DATA_ANIMAL_SCALE, animalScale); + } + + public float getGrowthScale() { + if (!this.isBaby()) { + return 1.0F; + } + + int age = this.level().isClientSide ? this.getSyncedAge() : this.getAge(); + float growthProgress = Mth.clamp((BABY_TO_ADULT_AGE_TICKS + age) / (float) BABY_TO_ADULT_AGE_TICKS, 0.0F, 1.0F); + return Mth.lerp(growthProgress, 0.2F, 1.0F); + } + public float getTotalModelScale() { + return this.getAnimalScale() * this.getGrowthScale(); + } + + private void updateDynamicDimensions() { + float dimensionsScale = this.getTotalModelScale(); + if (Math.abs(dimensionsScale - this.lastDimensionsScale) > 0.01F) { + this.lastDimensionsScale = dimensionsScale; + this.refreshDimensions(); + } + } + + @Override + public float getDinoScale() { + return this.getTotalModelScale(); + } + + public int getTypeVariant() { + return this.entityData.get(VARIANT); + } + + public CoelacanthVariant getVariant() { + return CoelacanthVariant.byId(this.getTypeVariant() & 255); + } + + private void setVariant(CoelacanthVariant variant) { + this.entityData.set(VARIANT, variant.getId() & 255); + } + + @Override + public boolean canMate(Animal other) { + if (!super.canMate(other)) return false; + if (!(other instanceof CoelacanthEntity that)) return false; + return this.getVariant() != that.getVariant(); + } + @Override + public void addAdditionalSaveData(CompoundTag pCompound) { + super.addAdditionalSaveData(pCompound); + pCompound.putInt("Variant", this.getTypeVariant()); + pCompound.putFloat("AnimalScale", this.getAnimalScale()); + } + + @Override + public void readAdditionalSaveData(CompoundTag pCompound) { + super.readAdditionalSaveData(pCompound); + this.entityData.set(VARIANT, pCompound.getInt("Variant")); + if (pCompound.contains("AnimalScale")) { + this.setAnimalScale(pCompound.getFloat("AnimalScale")); + } + } + + /*? if <=1.20.1 {*/ + @Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor pLevel, DifficultyInstance pDifficulty, MobSpawnType pReason, @Nullable SpawnGroupData pSpawnData, @Nullable CompoundTag pDataTag) { + CoelacanthVariant variant = Util.getRandom(CoelacanthVariant.values(), this.random); + this.setVariant(variant); + this.setAnimalScale(Mth.nextFloat(this.random, MIN_ANIMAL_SCALE, MAX_ANIMAL_SCALE)); + return super.finalizeSpawn(pLevel, pDifficulty, pReason, pSpawnData, pDataTag); + } + /*?} else {*/ + /*@Override + public SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, MobSpawnType spawnType, @Nullable SpawnGroupData spawnGroupData) { + CoelacanthVariant variant = Util.getRandom(CoelacanthVariant.values(), this.random); + this.setVariant(variant); + this.setAnimalScale(Mth.nextFloat(this.random, MIN_ANIMAL_SCALE, MAX_ANIMAL_SCALE)); + return super.finalizeSpawn(level, difficulty, spawnType, spawnGroupData); + } + *//*?}*/ + + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return cache; + } + + @Override + public double getFluidJumpThreshold() { + return this.getEyeHeight(); + } +} \ No newline at end of file diff --git a/common/src/main/resources/assets/jurassicrevived/animations/coelacanth.animation.json b/common/src/main/resources/assets/jurassicrevived/animations/coelacanth.animation.json new file mode 100644 index 0000000..d81f20c --- /dev/null +++ b/common/src/main/resources/assets/jurassicrevived/animations/coelacanth.animation.json @@ -0,0 +1,1120 @@ +{ + "format_version": "1.8.0", + "animations": { + "anim.coelacanth.swim": { + "loop": true, + "animation_length": 1, + "bones": { + "Body": { + "rotation": { + "0.0": { + "post": { + "vector": [0, 10, 0] + }, + "lerp_mode": "catmullrom" + }, + "0.5": { + "post": { + "vector": [0, -10, 0] + }, + "lerp_mode": "catmullrom" + }, + "1.0": { + "post": { + "vector": [0, 10, 0] + }, + "lerp_mode": "catmullrom" + } + }, + "position": { + "0.0": { + "vector": [0, 0, 0] + }, + "0.25": { + "pre": { + "vector": [-0.5, 0, 0] + }, + "post": { + "vector": [-0.5, 0, 0] + }, + "lerp_mode": "catmullrom" + }, + "0.75": { + "post": { + "vector": [0.5, 0, 0] + }, + "lerp_mode": "catmullrom" + }, + "1.0": { + "vector": [0, 0, 0] + } + } + }, + "Head": { + "rotation": { + "0.0": { + "post": { + "vector": [0, -10, 0] + }, + "lerp_mode": "catmullrom" + }, + "0.5": { + "post": { + "vector": [0, 10, 0] + }, + "lerp_mode": "catmullrom" + }, + "1.0": { + "post": { + "vector": [0, -10, 0] + }, + "lerp_mode": "catmullrom" + } + } + }, + "Tail1": { + "rotation": { + "0.0": { + "vector": [0, 0, 0] + }, + "0.25": { + "pre": { + "vector": [0, 10, 0] + }, + "post": { + "vector": [0, 10, 0] + }, + "lerp_mode": "catmullrom" + }, + "0.75": { + "post": { + "vector": [0, -10, 0] + }, + "lerp_mode": "catmullrom" + }, + "1.0": { + "vector": [0, 0, 0] + } + } + }, + "Tail2": { + "rotation": { + "0.0": { + "vector": [0, 0, 0] + }, + "0.25": { + "pre": { + "vector": [0, 10, 0] + }, + "post": { + "vector": [0, 10, 0] + }, + "lerp_mode": "catmullrom" + }, + "0.75": { + "post": { + "vector": [0, -10, 0] + }, + "lerp_mode": "catmullrom" + }, + "1.0": { + "vector": [0, 0, 0] + } + } + }, + "Neck": { + "rotation": { + "0.0": { + "vector": [0, 0, 0] + }, + "0.25": { + "pre": { + "vector": [0, -10, 0] + }, + "post": { + "vector": [0, -10, 0] + }, + "lerp_mode": "catmullrom" + }, + "0.75": { + "post": { + "vector": [0, 10, 0] + }, + "lerp_mode": "catmullrom" + }, + "1.0": { + "vector": [0, 0, 0] + } + }, + "position": { + "0.0": { + "vector": [0, 0, 0] + }, + "0.2917": { + "vector": [0.5, 0, 0] + }, + "0.75": { + "vector": [-0.5, 0, 0] + }, + "1.0": { + "vector": [0, 0, 0] + } + } + }, + "Mainhead": { + "rotation": { + "0.0": { + "post": { + "vector": [0, -10, 0] + }, + "lerp_mode": "catmullrom" + }, + "0.25": { + "post": { + "vector": [0, -5, 0] + }, + "lerp_mode": "catmullrom" + }, + "0.5": { + "post": { + "vector": [0, 10, 0] + }, + "lerp_mode": "catmullrom" + }, + "0.7917": { + "post": { + "vector": [0, 3.33, 0] + }, + "lerp_mode": "catmullrom" + }, + "1.0": { + "post": { + "vector": [0, -10, 0] + }, + "lerp_mode": "catmullrom" + } + }, + "position": { + "0.0": { + "vector": [0, 0.2, 0] + }, + "1.0": { + "vector": [0, 0.2, 0] + } + } + }, + "BodySection1": { + "rotation": { + "0.0": { + "post": { + "vector": [0, 10, 0] + }, + "lerp_mode": "catmullrom" + }, + "0.25": { + "post": { + "vector": [0.21986, 0.01682, -4.39424] + }, + "lerp_mode": "catmullrom" + }, + "0.5": { + "post": { + "vector": [0.44067, -9.99038, -2.53852] + }, + "lerp_mode": "catmullrom" + }, + "0.75": { + "post": { + "vector": [0.21992, -0.01441, 3.7307] + }, + "lerp_mode": "catmullrom" + }, + "1.0": { + "post": { + "vector": [0, 10, 0] + }, + "lerp_mode": "catmullrom" + } + }, + "position": { + "0.0": { + "vector": [0, 0, 0] + }, + "1.0": { + "vector": [0, 0, 0] + } + } + }, + "BodySection2": { + "rotation": { + "0.0": { + "vector": [0, 0, 0] + }, + "0.25": { + "pre": { + "vector": [0, 10, 0] + }, + "post": { + "vector": [0, 10, 0] + }, + "lerp_mode": "catmullrom" + }, + "0.75": { + "post": { + "vector": [0, -10, 0] + }, + "lerp_mode": "catmullrom" + }, + "1.0": { + "vector": [0, 0, 0] + } + } + }, + "TailSection1": { + "rotation": { + "0.0": { + "vector": [0, 0, 0] + }, + "0.2083": { + "vector": [0, 5, 0] + }, + "0.625": { + "vector": [0, -7.63, 0] + }, + "1.0": { + "vector": [0, 0, 0] + } + } + }, + "TailSection2": { + "rotation": { + "0.0": { + "vector": [0, 0, 0] + }, + "0.25": { + "pre": { + "vector": [0, 10, 0] + }, + "post": { + "vector": [0, 10, 0] + }, + "lerp_mode": "catmullrom" + }, + "0.75": { + "post": { + "vector": [0, -10, 0] + }, + "lerp_mode": "catmullrom" + }, + "1.0": { + "vector": [0, 0, 0] + } + } + }, + "TailSection3": { + "rotation": { + "0.0": { + "vector": [0, 0, 0] + }, + "0.25": { + "vector": [0, 5, 0] + }, + "0.625": { + "vector": [0, -7.5, 0] + }, + "1.0": { + "vector": [0, 0, 0] + } + } + }, + "Dorsalfin2": { + "rotation": { + "0.0": { + "vector": [-2.5, 0, 0] + }, + "0.5417": { + "vector": [-4.85, 0, 0] + }, + "1.0": { + "vector": [-2.5, 0, 0] + } + } + }, + "Leftbottomfin": { + "rotation": { + "0.0": { + "vector": [0, 17.5, 0] + }, + "0.5": { + "vector": [0, 22.5, 0] + }, + "1.0": { + "vector": [0, 17.5, 0] + } + } + }, + "Dorsalfin": { + "rotation": { + "0.0": { + "vector": [0, 0, 0] + }, + "0.5": { + "vector": [-4, 0, 0] + }, + "1.0": { + "vector": [0, 0, 0] + } + } + } + } + }, + "anim.coelacanth.fast_swim": { + "loop": true, + "animation_length": 0.6711, + "bones": { + "Body": { + "rotation": { + "0.0": { + "post": { + "vector": [0, 10, 0] + }, + "lerp_mode": "catmullrom" + }, + "0.3356": { + "post": { + "vector": [0, -10, 0] + }, + "lerp_mode": "catmullrom" + }, + "0.6711": { + "post": { + "vector": [0, 10, 0] + }, + "lerp_mode": "catmullrom" + } + }, + "position": { + "0.0": { + "vector": [0, 0, 0] + }, + "0.1678": { + "pre": { + "vector": [-0.5, 0, 0] + }, + "post": { + "vector": [-0.5, 0, 0] + }, + "lerp_mode": "catmullrom" + }, + "0.5034": { + "post": { + "vector": [0.5, 0, 0] + }, + "lerp_mode": "catmullrom" + }, + "0.6711": { + "vector": [0, 0, 0] + } + } + }, + "Head": { + "rotation": { + "0.0": { + "post": { + "vector": [0, -10, 0] + }, + "lerp_mode": "catmullrom" + }, + "0.3356": { + "post": { + "vector": [0, 10, 0] + }, + "lerp_mode": "catmullrom" + }, + "0.6711": { + "post": { + "vector": [0, -10, 0] + }, + "lerp_mode": "catmullrom" + } + } + }, + "Tail1": { + "rotation": { + "0.0": { + "vector": [0, 0, 0] + }, + "0.1678": { + "pre": { + "vector": [0, 10, 0] + }, + "post": { + "vector": [0, 10, 0] + }, + "lerp_mode": "catmullrom" + }, + "0.5034": { + "post": { + "vector": [0, -10, 0] + }, + "lerp_mode": "catmullrom" + }, + "0.6711": { + "vector": [0, 0, 0] + } + } + }, + "Tail2": { + "rotation": { + "0.0": { + "vector": [0, 0, 0] + }, + "0.1678": { + "pre": { + "vector": [0, 10, 0] + }, + "post": { + "vector": [0, 10, 0] + }, + "lerp_mode": "catmullrom" + }, + "0.5034": { + "post": { + "vector": [0, -10, 0] + }, + "lerp_mode": "catmullrom" + }, + "0.6711": { + "vector": [0, 0, 0] + } + } + }, + "Neck": { + "rotation": { + "0.0": { + "vector": [0, 0, 0] + }, + "0.1678": { + "pre": { + "vector": [0, -10, 0] + }, + "post": { + "vector": [0, -10, 0] + }, + "lerp_mode": "catmullrom" + }, + "0.5034": { + "post": { + "vector": [0, 10, 0] + }, + "lerp_mode": "catmullrom" + }, + "0.6711": { + "vector": [0, 0, 0] + } + }, + "position": { + "0.0": { + "vector": [0, 0, 0] + }, + "0.1957": { + "vector": [0.5, 0, 0] + }, + "0.5034": { + "vector": [-0.5, 0, 0] + }, + "0.6711": { + "vector": [0, 0, 0] + } + } + }, + "Mainhead": { + "rotation": { + "0.0": { + "post": { + "vector": [0, -10, 0] + }, + "lerp_mode": "catmullrom" + }, + "0.1678": { + "post": { + "vector": [0, -5, 0] + }, + "lerp_mode": "catmullrom" + }, + "0.3356": { + "post": { + "vector": [0, 10, 0] + }, + "lerp_mode": "catmullrom" + }, + "0.5313": { + "post": { + "vector": [0, 3.33, 0] + }, + "lerp_mode": "catmullrom" + }, + "0.6711": { + "post": { + "vector": [0, -10, 0] + }, + "lerp_mode": "catmullrom" + } + }, + "position": { + "0.0": { + "vector": [0, 0.2, 0] + }, + "0.6711": { + "vector": [0, 0.2, 0] + } + } + }, + "BodySection1": { + "rotation": { + "0.0": { + "post": { + "vector": [0, 10, 0] + }, + "lerp_mode": "catmullrom" + }, + "0.1678": { + "post": { + "vector": [0.21986, 0.01682, -4.39424] + }, + "lerp_mode": "catmullrom" + }, + "0.3356": { + "post": { + "vector": [0.44067, -9.99038, -2.53852] + }, + "lerp_mode": "catmullrom" + }, + "0.5034": { + "post": { + "vector": [0.21992, -0.01441, 3.7307] + }, + "lerp_mode": "catmullrom" + }, + "0.6711": { + "post": { + "vector": [0, 10, 0] + }, + "lerp_mode": "catmullrom" + } + }, + "position": { + "0.0": { + "vector": [0, 0, 0] + }, + "0.6711": { + "vector": [0, 0, 0] + } + } + }, + "BodySection2": { + "rotation": { + "0.0": { + "vector": [0, 0, 0] + }, + "0.1678": { + "pre": { + "vector": [0, 10, 0] + }, + "post": { + "vector": [0, 10, 0] + }, + "lerp_mode": "catmullrom" + }, + "0.5034": { + "post": { + "vector": [0, -10, 0] + }, + "lerp_mode": "catmullrom" + }, + "0.6711": { + "vector": [0, 0, 0] + } + } + }, + "TailSection1": { + "rotation": { + "0.0": { + "vector": [0, 0, 0] + }, + "0.1398": { + "vector": [0, 5, 0] + }, + "0.4195": { + "vector": [0, -7.63, 0] + }, + "0.6711": { + "vector": [0, 0, 0] + } + } + }, + "TailSection2": { + "rotation": { + "0.0": { + "vector": [0, 0, 0] + }, + "0.1678": { + "pre": { + "vector": [0, 10, 0] + }, + "post": { + "vector": [0, 10, 0] + }, + "lerp_mode": "catmullrom" + }, + "0.5034": { + "post": { + "vector": [0, -10, 0] + }, + "lerp_mode": "catmullrom" + }, + "0.6711": { + "vector": [0, 0, 0] + } + } + }, + "TailSection3": { + "rotation": { + "0.0": { + "vector": [0, 0, 0] + }, + "0.1678": { + "vector": [0, 5, 0] + }, + "0.4195": { + "vector": [0, -7.5, 0] + }, + "0.6711": { + "vector": [0, 0, 0] + } + } + }, + "Dorsalfin2": { + "rotation": { + "0.0": { + "vector": [-2.5, 0, 0] + }, + "0.3635": { + "vector": [-4.85, 0, 0] + }, + "0.6711": { + "vector": [-2.5, 0, 0] + } + } + }, + "Leftbottomfin": { + "rotation": { + "0.0": { + "vector": [0, 17.5, 0] + }, + "0.3356": { + "vector": [0, 22.5, 0] + }, + "0.6711": { + "vector": [0, 17.5, 0] + } + } + }, + "Dorsalfin": { + "rotation": { + "0.0": { + "vector": [0, 0, 0] + }, + "0.3356": { + "vector": [-4, 0, 0] + }, + "0.6711": { + "vector": [0, 0, 0] + } + } + } + } + }, + "anim.coelacanth.mouth": { + "loop": "hold_on_last_frame", + "animation_length": 1.25, + "bones": { + "Jaw": { + "rotation": { + "0.0": { + "post": { + "vector": [0, 0, 0] + }, + "lerp_mode": "catmullrom" + }, + "0.25": { + "post": { + "vector": [22.5, 0, 0] + }, + "lerp_mode": "catmullrom" + }, + "1.0": { + "post": { + "vector": [22.5, 0, 0] + }, + "lerp_mode": "catmullrom" + }, + "1.25": { + "post": { + "vector": [0, 0, 0] + }, + "lerp_mode": "catmullrom" + } + } + }, + "Lowerjawfront": { + "rotation": { + "0.0": { + "post": { + "vector": [0, 0, 0] + }, + "lerp_mode": "catmullrom" + }, + "0.5": { + "post": { + "vector": [40, 0, 0] + }, + "lerp_mode": "catmullrom" + }, + "1.25": { + "post": { + "vector": [0, 0, 0] + }, + "lerp_mode": "catmullrom" + } + } + } + } + }, + "anim.coelacanth.flop": { + "loop": true, + "animation_length": 0.5, + "bones": { + "Body": { + "rotation": { + "0.0": { + "post": { + "vector": [0, 0, 112.5] + }, + "lerp_mode": "catmullrom" + }, + "0.25": { + "post": { + "vector": [0, 0, 67.5] + }, + "lerp_mode": "catmullrom" + }, + "0.5": { + "post": { + "vector": [0, 0, 112.5] + }, + "lerp_mode": "catmullrom" + } + }, + "position": { + "0.0": { + "vector": [0, -2, 0] + }, + "0.125": { + "pre": { + "vector": [0, 0, 0] + }, + "post": { + "vector": [0, 0, 0] + }, + "lerp_mode": "catmullrom" + }, + "0.375": { + "post": { + "vector": [0, -4, 0] + }, + "lerp_mode": "catmullrom" + }, + "0.5": { + "vector": [0, -2, 0] + } + } + }, + "Head": { + "rotation": { + "0.0": { + "post": { + "vector": [0, 0, 0] + }, + "lerp_mode": "catmullrom" + }, + "0.25": { + "post": { + "vector": [0, 22.5, 0] + }, + "lerp_mode": "catmullrom" + }, + "0.5": { + "post": { + "vector": [0, 0, 0] + }, + "lerp_mode": "catmullrom" + } + } + }, + "Jaw": { + "rotation": { + "0.0": { + "post": { + "vector": [0, 0, 0] + }, + "lerp_mode": "catmullrom" + }, + "0.25": { + "post": { + "vector": [22.5, 0, 0] + }, + "lerp_mode": "catmullrom" + }, + "0.5": { + "post": { + "vector": [0, 0, 0] + }, + "lerp_mode": "catmullrom" + } + } + }, + "Tail1": { + "rotation": { + "0.0": { + "post": { + "vector": [0, 0, 0] + }, + "lerp_mode": "catmullrom" + }, + "0.25": { + "post": { + "vector": [0, -22.5, 0] + }, + "lerp_mode": "catmullrom" + }, + "0.5": { + "post": { + "vector": [0, 0, 0] + }, + "lerp_mode": "catmullrom" + } + } + }, + "Tail2": { + "rotation": { + "0.0": { + "post": { + "vector": [0, 0, 0] + }, + "lerp_mode": "catmullrom" + }, + "0.25": { + "post": { + "vector": [0, -22.5, 0] + }, + "lerp_mode": "catmullrom" + }, + "0.5": { + "post": { + "vector": [0, 0, 0] + }, + "lerp_mode": "catmullrom" + } + } + }, + "root": { + "position": { + "0.0": { + "vector": [4, -17, -4] + }, + "0.1667": { + "vector": [4, -17, -3] + }, + "0.5": { + "vector": [4, -17, -4] + } + } + }, + "Neck": { + "rotation": { + "0.0": { + "post": { + "vector": [0, 0, 0] + }, + "lerp_mode": "catmullrom" + }, + "0.25": { + "post": { + "vector": [0, 22.5, 0] + }, + "lerp_mode": "catmullrom" + }, + "0.5": { + "post": { + "vector": [0, 0, 0] + }, + "lerp_mode": "catmullrom" + } + }, + "position": { + "0.0": { + "vector": [0, 0, 0] + }, + "0.25": { + "vector": [-0.9, 0, 0] + }, + "0.5": { + "vector": [-0.4, 0, 0] + } + } + }, + "Mainhead": { + "rotation": { + "0.0": { + "post": { + "vector": [0, 0, 0] + }, + "lerp_mode": "catmullrom" + }, + "0.25": { + "post": { + "vector": [0, 22.5, 0] + }, + "lerp_mode": "catmullrom" + }, + "0.5": { + "post": { + "vector": [0, 0, 0] + }, + "lerp_mode": "catmullrom" + } + } + }, + "BodySection1": { + "rotation": { + "0.0": { + "post": { + "vector": [0, 0, 107.5] + }, + "lerp_mode": "catmullrom" + }, + "0.25": { + "post": { + "vector": [0, 0, 67.5] + }, + "lerp_mode": "catmullrom" + }, + "0.5": { + "post": { + "vector": [0, 0, 107.5] + }, + "lerp_mode": "catmullrom" + } + }, + "position": { + "0.0": { + "vector": [0, -2, 0] + }, + "0.125": { + "pre": { + "vector": [0, 0, 0] + }, + "post": { + "vector": [0, 0, 0] + }, + "lerp_mode": "catmullrom" + }, + "0.375": { + "post": { + "vector": [0, -4, 0] + }, + "lerp_mode": "catmullrom" + }, + "0.5": { + "vector": [0, -2, 0] + } + } + }, + "BodySection2": { + "rotation": { + "0.0": { + "post": { + "vector": [0, 0, 0] + }, + "lerp_mode": "catmullrom" + }, + "0.25": { + "post": { + "vector": [0, -22.5, 0] + }, + "lerp_mode": "catmullrom" + }, + "0.3333": { + "post": { + "vector": [0, -18.67, 0] + }, + "lerp_mode": "catmullrom" + }, + "0.5": { + "post": { + "vector": [0, 0, 0] + }, + "lerp_mode": "catmullrom" + } + } + }, + "BodySection3": { + "rotation": { + "0.0": { + "post": { + "vector": [0, 0, 0] + }, + "lerp_mode": "catmullrom" + }, + "0.25": { + "post": { + "vector": [0, -22.5, 0] + }, + "lerp_mode": "catmullrom" + }, + "0.5": { + "post": { + "vector": [0, 0, 0] + }, + "lerp_mode": "catmullrom" + } + } + }, + "TailSection1": { + "rotation": { + "0.0": { + "vector": [0, 0, 0] + }, + "0.25": { + "vector": [0, -10, 0] + } + } + }, + "TailSection3": { + "rotation": { + "0.0": { + "vector": [0, 0, 0] + }, + "0.25": { + "vector": [0, -7.5, 0] + }, + "0.4583": { + "vector": [0, -2.5, 0] + } + } + } + } + } + } +} \ No newline at end of file diff --git a/common/src/main/resources/assets/jurassicrevived/geo/coelacanth.geo.json b/common/src/main/resources/assets/jurassicrevived/geo/coelacanth.geo.json new file mode 100644 index 0000000..c0e061f --- /dev/null +++ b/common/src/main/resources/assets/jurassicrevived/geo/coelacanth.geo.json @@ -0,0 +1,202 @@ +{ + "format_version": "1.12.0", + "minecraft:geometry": [ + { + "description": { + "identifier": "geometry.coelacanth", + "texture_width": 64, + "texture_height": 64, + "visible_bounds_width": 4, + "visible_bounds_height": 3.5, + "visible_bounds_offset": [0, 1.25, 0] + }, + "bones": [ + { + "name": "root", + "pivot": [0, 24, 0] + }, + { + "name": "BodySection1", + "parent": "root", + "pivot": [0, 23, 0.3], + "cubes": [ + {"origin": [-2.5, 16, 0.3], "size": [5, 7, 5], "uv": [5, 29]} + ] + }, + { + "name": "BodySection2", + "parent": "BodySection1", + "pivot": [0, 23, 4.5], + "cubes": [ + {"origin": [-2, 16, 4.5], "size": [4, 7, 4], "uv": [45, 18]} + ] + }, + { + "name": "BodySection3", + "parent": "BodySection2", + "pivot": [0, 23, 7.7], + "cubes": [ + {"origin": [-1.5, 16, 7.7], "size": [3, 7, 3], "uv": [30, 17]} + ] + }, + { + "name": "TailSection1", + "parent": "BodySection3", + "pivot": [0, 22.6, 9.9], + "cubes": [ + {"origin": [-1, 16.6, 9.9], "size": [2, 6, 3], "uv": [17, 16]} + ] + }, + { + "name": "TailSection2", + "parent": "TailSection1", + "pivot": [0, 22.1, 11.9], + "rotation": [0.09, 0, 0], + "cubes": [ + {"origin": [-1, 17.1, 11.9], "size": [2, 5, 3], "uv": [6, 19]} + ] + }, + { + "name": "TailSection3", + "parent": "TailSection2", + "pivot": [0, 19.6, 12.1], + "rotation": [45, 0, 0], + "cubes": [ + {"origin": [-0.5, 15.6, 12.1], "size": [1, 4, 4], "uv": [53, 9]} + ] + }, + { + "name": "Tailfin", + "parent": "TailSection3", + "pivot": [0, 19.6, 12.1], + "cubes": [ + {"origin": [0, 11.6, 12.1], "size": [0, 8, 8], "uv": [21, -7]} + ] + }, + { + "name": "Dorsalfin2", + "parent": "TailSection2", + "pivot": [0, 22.1, 10.4], + "rotation": [52.08903, 0, 0], + "cubes": [ + {"origin": [0, 19.1, 10.4], "size": [0, 3, 6], "uv": [1, 0]} + ] + }, + { + "name": "Assfin", + "parent": "TailSection1", + "pivot": [0, 17.4, 10.6], + "rotation": [-58.87, 0, 0], + "cubes": [ + {"origin": [0, 17.4, 10.6], "size": [0, 3, 6], "uv": [1, 4]} + ] + }, + { + "name": "Leftbottomfin", + "parent": "BodySection2", + "pivot": [0, 16, 4.1], + "rotation": [0, -63.67, -90], + "cubes": [ + {"origin": [0, 15, 4.1], "size": [4, 2, 5], "uv": [45, 1]} + ] + }, + { + "name": "Dorsalfin", + "parent": "BodySection1", + "pivot": [0, 22.8, 0.1], + "rotation": [51.73, 0, 0], + "cubes": [ + {"origin": [0, 18.8, 0.1], "size": [0, 4, 6], "uv": [1, -5]} + ] + }, + { + "name": "Neck", + "parent": "BodySection1", + "pivot": [0, 19.8, -2.4], + "cubes": [ + {"origin": [-2, 16.1, -3.4], "size": [4, 3, 4], "uv": [47, 34]} + ] + }, + { + "name": "Mainhead", + "parent": "Neck", + "pivot": [0, 18.8, -3.52], + "rotation": [42.06, 0, 0], + "cubes": [ + {"origin": [-2.5, 18.8, -6.52], "size": [5, 3, 3], "uv": [22, 48]} + ] + }, + { + "name": "Lowerjawrear", + "parent": "Mainhead", + "pivot": [0, 18.5, -2.52], + "rotation": [-44.87, 0, 0], + "cubes": [ + {"origin": [-2, 15.5, -5.52], "size": [4, 3, 3], "uv": [45, 55]} + ] + }, + { + "name": "Lowerjawfront", + "parent": "Lowerjawrear", + "pivot": [0, 15.9, -5.22], + "rotation": [-37.79953, 0, 0], + "cubes": [ + {"origin": [-2, 15.4, -8.22], "size": [4, 1, 3], "uv": [49, 50]} + ] + }, + { + "name": "Rightgill2", + "parent": "Mainhead", + "pivot": [1.7, 17.8, -4.62], + "cubes": [ + {"origin": [1.7, 17.8, -4.62], "size": [1, 3, 3], "uv": [22, 55]} + ] + }, + { + "name": "Rightgill", + "parent": "Mainhead", + "pivot": [-1.7, 17.8, -4.62], + "cubes": [ + {"origin": [-2.7, 17.8, -4.62], "size": [1, 3, 3], "uv": [22, 55], "mirror": true} + ] + }, + { + "name": "Neckback1", + "parent": "Neck", + "pivot": [0, 19.8, -5.4], + "rotation": [34.77266, 0, 0], + "cubes": [ + {"origin": [-2, 17.8, -5.4], "size": [4, 3, 3], "uv": [7, 48]} + ] + }, + { + "name": "Neckback2", + "parent": "Neckback1", + "pivot": [0, 20.8, -2.4], + "rotation": [-24.91, 0, 0], + "cubes": [ + {"origin": [-2.5, 16.8, -2.4], "size": [5, 4, 4], "uv": [28, 32]} + ] + }, + { + "name": "RightFrontFlipper", + "parent": "Neck", + "pivot": [1.5, 20, -2.8], + "rotation": [21.22, -2.61, 90], + "cubes": [ + {"origin": [2, 20, -2.8], "size": [3, 0, 7], "uv": [7, 1]} + ] + }, + { + "name": "LeftFrontFlipper", + "parent": "Neck", + "pivot": [-1.5, 20, -2.8], + "rotation": [21.22, 2.61, -90], + "cubes": [ + {"origin": [-4.5, 20, -2.8], "size": [3, 0, 7], "uv": [7, 1], "mirror": true} + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/common/src/main/resources/assets/jurassicrevived/textures/entity/coelacanth.png b/common/src/main/resources/assets/jurassicrevived/textures/entity/coelacanth.png new file mode 100644 index 0000000000000000000000000000000000000000..8a2053205d494073c14350eae1c5e98e0ab2c780 GIT binary patch literal 10676 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 F`9GIWaAp7i literal 0 HcmV?d00001 diff --git a/common/src/main/resources/assets/jurassicrevived/textures/entity/coelacanth_female.png b/common/src/main/resources/assets/jurassicrevived/textures/entity/coelacanth_female.png new file mode 100644 index 0000000000000000000000000000000000000000..b558ab68d6d458a7f1f8ef2e91ef4c3d1f477477 GIT binary patch literal 10180 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 rZ7>@GvAG$@SMCeG#p8s