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 0000000..8a20532 Binary files /dev/null and b/common/src/main/resources/assets/jurassicrevived/textures/entity/coelacanth.png differ 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 0000000..b558ab6 Binary files /dev/null and b/common/src/main/resources/assets/jurassicrevived/textures/entity/coelacanth_female.png differ