starts to add coelacanth and marine AI

This commit is contained in:
2026-06-11 17:14:10 -04:00
parent e5892ea61d
commit 7640d77b1e
13 changed files with 2019 additions and 23 deletions
@@ -115,6 +115,7 @@ public class CommonClientClass {
EntityRendererRegistry.register(ModEntities.CHILESAURUS, ChilesaurusRenderer::new); EntityRendererRegistry.register(ModEntities.CHILESAURUS, ChilesaurusRenderer::new);
EntityRendererRegistry.register(ModEntities.MUSSASAURUS, MussasaurusRenderer::new); EntityRendererRegistry.register(ModEntities.MUSSASAURUS, MussasaurusRenderer::new);
EntityRendererRegistry.register(ModEntities.THESCELOSAURUS, ThescelosaurusRenderer::new); EntityRendererRegistry.register(ModEntities.THESCELOSAURUS, ThescelosaurusRenderer::new);
EntityRendererRegistry.register(ModEntities.COELACANTH, CoelacanthRenderer::new);
if (Platform.isFabric()) { if (Platform.isFabric()) {
registerSpawnEggColors(); registerSpawnEggColors();
@@ -351,6 +351,10 @@ public class ModEntities {
ENTITIES.register("suchomimus", () -> EntityType.Builder.of(SuchomimusEntity::new, MobCategory.CREATURE) ENTITIES.register("suchomimus", () -> EntityType.Builder.of(SuchomimusEntity::new, MobCategory.CREATURE)
.sized(1.3f, 1.8f).build("suchomimus")); .sized(1.3f, 1.8f).build("suchomimus"));
public static final RegistrySupplier<EntityType<CoelacanthEntity>> COELACANTH =
ENTITIES.register("coelacanth", () -> EntityType.Builder.of(CoelacanthEntity::new, MobCategory.CREATURE)
.sized(1.3f, 1.8f).build("coelacanth"));
public static void registerAttributes() { public static void registerAttributes() {
EntityAttributeRegistry.register(APATOSAURUS, ApatosaurusEntity::createAttributes); EntityAttributeRegistry.register(APATOSAURUS, ApatosaurusEntity::createAttributes);
EntityAttributeRegistry.register(ALBERTOSAURUS, AlbertosaurusEntity::createAttributes); EntityAttributeRegistry.register(ALBERTOSAURUS, AlbertosaurusEntity::createAttributes);
@@ -432,6 +436,7 @@ public class ModEntities {
EntityAttributeRegistry.register(CHILESAURUS, ChilesaurusEntity::createAttributes); EntityAttributeRegistry.register(CHILESAURUS, ChilesaurusEntity::createAttributes);
EntityAttributeRegistry.register(MUSSASAURUS, MussasaurusEntity::createAttributes); EntityAttributeRegistry.register(MUSSASAURUS, MussasaurusEntity::createAttributes);
EntityAttributeRegistry.register(THESCELOSAURUS, ThescelosaurusEntity::createAttributes); EntityAttributeRegistry.register(THESCELOSAURUS, ThescelosaurusEntity::createAttributes);
EntityAttributeRegistry.register(COELACANTH, CoelacanthEntity::createAttributes);
} }
public static void registerSpawnPlacements() { public static void registerSpawnPlacements() {
@@ -70,6 +70,10 @@ public class DinoAIController {
private static final int HERBIVORE_BROWSE_VERTICAL_RANGE = 2; 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_HUNGER_THRESHOLD = 0.75f;
private static final float HERBIVORE_SELF_FEED_REPLENISHMENT_MULTIPLIER = 0.50f; 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; private final DinoEntityBase dino;
@@ -175,6 +179,10 @@ public class DinoAIController {
return; return;
} }
if (dino.isMarine()) {
return;
}
if (handleWaterMovementHelper(null)) { if (handleWaterMovementHelper(null)) {
return; return;
} }
@@ -266,6 +274,20 @@ public class DinoAIController {
float hungerDecay = jrConfig.hungerConsumption ? config.hungerDecay() * VITAL_DECAY_MULTIPLIER : 0.0f; float hungerDecay = jrConfig.hungerConsumption ? config.hungerDecay() * VITAL_DECAY_MULTIPLIER : 0.0f;
float thirstDecay = jrConfig.waterConsumption ? config.thirstDecay() * 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) { if (currentState == State.SLEEPING) {
hungerDecay *= 0.5f; hungerDecay *= 0.5f;
thirstDecay *= 0.5f; thirstDecay *= 0.5f;
@@ -381,7 +403,7 @@ public class DinoAIController {
} }
// 5. Hunt check // 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(); JRConfig jrConfig = JRConfigManager.get();
boolean hungerConsumptionEnabled = jrConfig.hungerConsumption; boolean hungerConsumptionEnabled = jrConfig.hungerConsumption;
boolean waterConsumptionEnabled = jrConfig.waterConsumption; boolean waterConsumptionEnabled = jrConfig.waterConsumption;
@@ -407,11 +429,30 @@ public class DinoAIController {
} }
// 6. Water check // 6. Water check
if ((currentState == State.IDLE || currentState == State.ROAMING || currentState == State.TERRITORIAL_ROAMING)) { if (!dino.isMarine() && (currentState == State.IDLE || currentState == State.ROAMING || currentState == State.TERRITORIAL_ROAMING)) {
if (dino.dinoData != null && dino.dinoData.getThirst() < 50 && waterTarget == null) { if (dino.dinoData != null && dino.dinoData.getThirst() < 50 && waterTarget == null) {
if (stateTimer % 10 == 0) findWater(); 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() { private void findWater() {
@@ -783,6 +824,11 @@ public class DinoAIController {
return; return;
} }
if (dino.isMarine()) {
transitionTo(State.ROAMING);
return;
}
float territoriality = 0.0f; float territoriality = 0.0f;
if (dino.dinoData != null) { if (dino.dinoData != null) {
territoriality = dino.dinoData.getTerritoriality(); territoriality = dino.dinoData.getTerritoriality();
@@ -1055,6 +1101,16 @@ public class DinoAIController {
private void findAndSetRoamTarget() { private void findAndSetRoamTarget() {
this.roamTarget = null; 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. // Grounded flyers should walk to nearby grounded targets instead of taking off immediately.
if (dino instanceof FlyingAnimal && dino.onGround()) { if (dino instanceof FlyingAnimal && dino.onGround()) {
Vec3 groundPos = getNearbyGroundRoamPosForFlyer(); Vec3 groundPos = getNearbyGroundRoamPosForFlyer();
@@ -1262,6 +1318,10 @@ public class DinoAIController {
return false; return false;
} }
if (dino.isSimpleFish()) {
return false;
}
if (dino.tickCount % HERBIVORE_SELF_FEED_INTERVAL != 0 || dino.getRandom().nextFloat() > HERBIVORE_SELF_FEED_CHANCE) { if (dino.tickCount % HERBIVORE_SELF_FEED_INTERVAL != 0 || dino.getRandom().nextFloat() > HERBIVORE_SELF_FEED_CHANCE) {
return false; return false;
} }
@@ -1593,9 +1653,17 @@ public class DinoAIController {
} }
if (dino.getNavigation().isDone() || stateTimer % 10 == 0) { if (dino.getNavigation().isDone() || stateTimer % 10 == 0) {
Vec3 awayDir = DefaultRandomPos.getPosAway(dino, 16, 7, attackTarget.position()); Vec3 awayDir;
if (dino.isMarine()) {
awayDir = getMarineFleePos(attackTarget.position());
} else {
awayDir = DefaultRandomPos.getPosAway(dino, 16, 7, attackTarget.position());
}
if (awayDir != null) { if (awayDir != null) {
dino.getNavigation().moveTo(awayDir.x, awayDir.y, awayDir.z, dino.getAIConfig().runSpeed() * 1.2); 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);
} }
} }
@@ -1603,4 +1671,41 @@ public class DinoAIController {
transitionTo(State.IDLE); 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();
}
} }
@@ -107,7 +107,10 @@ public abstract class DinoEntityBase extends Animal {
public abstract boolean isCarnivore(); public abstract boolean isCarnivore();
public abstract boolean isMarine(); public abstract boolean isMarine();
public abstract boolean isAmphibious(); 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 abstract DinoAIConfig getAIConfig();
public record DinoAIConfig(double walkSpeed, double runSpeed, double attackReach, public record DinoAIConfig(double walkSpeed, double runSpeed, double attackReach,
@@ -41,7 +41,7 @@ public interface IDinoData {
Type getType(); Type getType();
void setType(Type type); 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(); Group getGroup();
void setGroup(Group group); void setGroup(Group group);
@@ -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<CoelacanthEntity> {
private static final Map<CoelacanthVariant, ResourceLocation> 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<CoelacanthEntity> 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);
}
}
}
@@ -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<CoelacanthEntity> {
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);
}
}
@@ -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];
}
}
@@ -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<Integer> VARIANT =
SynchedEntityData.defineId(CoelacanthEntity.class, EntityDataSerializers.INT);
private static final EntityDataAccessor<Integer> DATA_SYNCED_AGE =
SynchedEntityData.defineId(CoelacanthEntity.class, EntityDataSerializers.INT);
private static final EntityDataAccessor<Float> 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<? extends Animal> 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<? extends LivingEntity>) 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");
// 30s60s 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.200.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();
}
}
@@ -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}
]
}
]
}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB