CCCPaste

Sea Otter 7

package com.mysticsbiomes.common.entity;

import com.mysticsbiomes.init.MysticEntities;
import io.netty.buffer.ByteBuf;
import net.minecraft.client.model.geom.ModelPart;
import net.minecraft.client.model.geom.PartPose;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.NbtUtils;
import net.minecraft.network.codec.ByteBufCodecs;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.network.syncher.EntityDataAccessor;
import net.minecraft.network.syncher.EntityDataSerializer;
import net.minecraft.network.syncher.EntityDataSerializers;
import net.minecraft.network.syncher.SynchedEntityData;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.tags.FluidTags;
import net.minecraft.tags.ItemTags;
import net.minecraft.util.ByIdMap;
import net.minecraft.util.Mth;
import net.minecraft.util.StringRepresentable;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
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.control.BodyRotationControl;
import net.minecraft.world.entity.ai.control.LookControl;
import net.minecraft.world.entity.ai.control.MoveControl;
import net.minecraft.world.entity.ai.goal.*;
import net.minecraft.world.entity.ai.navigation.AmphibiousPathNavigation;
import net.minecraft.world.entity.ai.navigation.PathNavigation;
import net.minecraft.world.entity.animal.Animal;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.LevelReader;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.pathfinder.*;
import net.minecraft.world.phys.Vec3;

import javax.annotation.Nullable;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.function.IntFunction;
import java.util.function.Predicate;

/**
 * can be tamed as pets to defend you in the water.
 * they can help you find treasures, or collect fish.
 * they can help you swim faster like dolphins.
 * they mostly float around, unless they are feeling playful or want to get food.
 * they will fall asleep at night, holding another sea otters hand nearby.
 * their babies will rest on their belly until they age up.
 *
 * todo:
 * swim around
 * drift around
 * hold hands
 * breed
 * search for food
 *
 * sleeping
 * hold hands while sleeping
 * babies sleeping on parent
 * babies behavior
 * search for fish
 * search for seashells
 * break open shells
 * bring items to players
 * eat
 */
public class SeaOtter extends Animal {
    public static final EntityDataSerializer SEA_OTTER_STATE = EntityDataSerializer.forValueType(State.STREAM_CODEC);
    private static final EntityDataAccessor DATA_STATE_ID = SynchedEntityData.defineId(SeaOtter.class, SEA_OTTER_STATE);
    private static final EntityDataAccessor DATA_FLAGS_ID = SynchedEntityData.defineId(SeaOtter.class, EntityDataSerializers.BYTE);
    private int ticksSinceLastSwam;
    private int ticksSpentSwimming;
    @Nullable
    private BlockPos surfacePos;
    public final Map previousPose = new HashMap<>();
    public final Map targetPose = new HashMap<>();
    public final AnimationState swimmingIdleAnimationState = new AnimationState();
    public final AnimationState swimmingAnimationState = new AnimationState();
    public final AnimationState twirlWhileSwimmingAnimationState = new AnimationState();
    public final AnimationState floatingAnimationState = new AnimationState();
    public final AnimationState startFloatingAnimationState = new AnimationState();
    public final AnimationState stopFloatingAnimationState = new AnimationState();
    public final AnimationState sleepingWhileFloatingAnimationState = new AnimationState();
    public final AnimationState eatingWhileFloatingAnimationState = new AnimationState();
    public final AnimationState sitDownAnimationState = new AnimationState();
    public final AnimationState sittingIdleAnimationState = new AnimationState();
    public final AnimationState standUpAnimationState = new AnimationState();
    public final AnimationState standingIdleAnimationState = new AnimationState();
    public final AnimationState walkingAnimationState = new AnimationState();
    private long inStateTicks = 0L;

    public SeaOtter(EntityType type, Level level) {
        super(type, level);
        this.setPathfindingMalus(PathType.WATER, 0.0F);
        this.moveControl = new SeaOtter.SeaOtterMoveControl(this);
        this.lookControl = new SeaOtter.SeaOtterLookControl(this);
    }

    public static AttributeSupplier.Builder createAttributes() {
        return Mob.createMobAttributes().add(Attributes.MAX_HEALTH, 16.0F).add(Attributes.MOVEMENT_SPEED, 0.5D).add(Attributes.STEP_HEIGHT, 1.0);
    }

    @Override
    protected void registerGoals() {
        this.goalSelector.addGoal(0, new SeaOtter.SwimToSurfaceGoal(this));
        this.goalSelector.addGoal(1, new SeaOtter.SeaOtterLookAtPlayerGoal(this, 6.0F));
        this.goalSelector.addGoal(2, new SeaOtter.FloatingGoal(this));
        this.goalSelector.addGoal(3, new SeaOtter.SeaOtterTemptGoal(this, 0.2D, (stack) -> stack.is(ItemTags.FISHES)));
        this.goalSelector.addGoal(4, new SeaOtter.SwimAroundGoal(this, 1.0D, 3));
        this.goalSelector.addGoal(5, new SeaOtter.SeaOtterRandomLookAroundGoal(this));
    }

    @Override
    protected void defineSynchedData(SynchedEntityData.Builder builder) {
        super.defineSynchedData(builder);
        builder.define(DATA_STATE_ID, State.NOTHING);
        builder.define(DATA_FLAGS_ID, (byte)0);
    }

    @Override
    public void addAdditionalSaveData(CompoundTag tag) {
        super.addAdditionalSaveData(tag);
        tag.putString("State", this.getState().getSerializedName());
        tag.putBoolean("Floating", this.isFloating());
        tag.putBoolean("Swimming", this.isSwimming());
        tag.putBoolean("Sitting", this.isSitting());
        tag.putInt("TicksSinceLastSwam", this.ticksSinceLastSwam);
        tag.putInt("TicksSpentSwimming", this.ticksSpentSwimming);

        if (this.surfacePos != null) {
            tag.put("SurfacePos", NbtUtils.writeBlockPos(this.surfacePos));
        }
    }

    @Override
    public void readAdditionalSaveData(CompoundTag tag) {
        super.readAdditionalSaveData(tag);
        this.switchToState(State.fromName(tag.getString("State")));
        this.setFloating(tag.getBoolean("Floating"));
        this.setSwimming(tag.getBoolean("Swimming"));
        this.setSitting(tag.getBoolean("Sitting"));
        this.ticksSinceLastSwam = tag.getInt("TicksSinceLastSwam");
        this.ticksSpentSwimming = tag.getInt("TicksSpentSwimming");

        this.surfacePos = null;
        if (tag.contains("SurfacePos") && NbtUtils.readBlockPos(tag, "SurfacePos").isPresent()) {
            this.surfacePos = NbtUtils.readBlockPos(tag, "SurfacePos").get();
        }
    }

    @Override
    public AgeableMob getBreedOffspring(ServerLevel level, AgeableMob mob) {
        return MysticEntities.SEA_OTTER.get().create(level);
    }

    @Override
    public int getMaxAirSupply() {
        return 6000; /// 5 minutes
    }

    @Override
    public void tick() {
        super.tick();
        if (!this.level().isClientSide()) {
            if (this.isFloating()) {
                this.ticksSinceLastSwam++;
                this.wasTouchingWater = true;
                this.setDeltaMovement(this.getDeltaMovement().multiply(1.0D, 0.0D, 1.0D));
                this.setSpeed(0.0F);
                this.setYya(0.0F);

                if (this.getAirSupply() < this.getMaxAirSupply()) {
                    this.setAirSupply(this.increaseAirSupply(this.getAirSupply()));
                }
            }

            if (this.isUnderWater()) {
                if (!this.needsToSurface() && (this.getAirSupply() < 600 || !this.wantsToSwimAround() || this.hasBeenSwimmingLongEnough())) {
                    this.setNeedsToSurface(true);
                }
            }
        }

        if (this.level().isClientSide()) {
            this.setupSwimmingAnimationStates();
            this.setupFloatingAnimationStates();
            this.setupSittingAnimationStates();
        }
        this.inStateTicks++;
    }

    private void setupSwimmingAnimationStates() {
        switch (this.getState()) {
            case SWIMMING_IDLE:
                this.swimmingIdleAnimationState.startIfStopped(this.tickCount);
                this.swimmingAnimationState.stop();
                this.twirlWhileSwimmingAnimationState.stop();
                this.floatingAnimationState.stop();
                this.startFloatingAnimationState.stop();
                this.stopFloatingAnimationState.stop();
                this.sleepingWhileFloatingAnimationState.stop();
                this.eatingWhileFloatingAnimationState.stop();
                this.sitDownAnimationState.stop();
                this.sittingIdleAnimationState.stop();
                this.standUpAnimationState.stop();
                this.standingIdleAnimationState.stop();
                this.walkingAnimationState.stop();
                break;
            case SWIMMING:
                this.swimmingAnimationState.startIfStopped(this.tickCount);
                this.swimmingIdleAnimationState.stop();
                this.twirlWhileSwimmingAnimationState.stop();
                this.floatingAnimationState.stop();
                this.startFloatingAnimationState.stop();
                this.stopFloatingAnimationState.stop();
                this.sleepingWhileFloatingAnimationState.stop();
                this.eatingWhileFloatingAnimationState.stop();
                this.sitDownAnimationState.stop();
                this.sittingIdleAnimationState.stop();
                this.standUpAnimationState.stop();
                this.standingIdleAnimationState.stop();
                this.walkingAnimationState.stop();
                break;
            case TWIRL_WHILE_SWIMMING:
                this.twirlWhileSwimmingAnimationState.startIfStopped(this.tickCount);
                break;
        }
    }

    private void setupFloatingAnimationStates() {
        switch (this.getState()) {
            case START_FLOATING:
                this.startFloatingAnimationState.startIfStopped(this.tickCount);
                this.swimmingIdleAnimationState.stop();
                this.swimmingAnimationState.stop();
                this.twirlWhileSwimmingAnimationState.stop();
                this.floatingAnimationState.stop();
                this.stopFloatingAnimationState.stop();
                this.sleepingWhileFloatingAnimationState.stop();
                this.eatingWhileFloatingAnimationState.stop();
                this.sitDownAnimationState.stop();
                this.sittingIdleAnimationState.stop();
                this.standUpAnimationState.stop();
                this.standingIdleAnimationState.stop();
                this.walkingAnimationState.stop();
                break;
            case FLOATING:
                this.floatingAnimationState.startIfStopped(this.tickCount);
                this.swimmingIdleAnimationState.stop();
                this.swimmingAnimationState.stop();
                this.twirlWhileSwimmingAnimationState.stop();
                this.startFloatingAnimationState.stop();
                this.stopFloatingAnimationState.stop();
                this.sleepingWhileFloatingAnimationState.stop();
                this.eatingWhileFloatingAnimationState.stop();
                this.sitDownAnimationState.stop();
                this.sittingIdleAnimationState.stop();
                this.standUpAnimationState.stop();
                this.standingIdleAnimationState.stop();
                this.walkingAnimationState.stop();
                break;
            case STOP_FLOATING:
                this.stopFloatingAnimationState.startIfStopped(this.tickCount);
                this.swimmingIdleAnimationState.stop();
                this.swimmingAnimationState.stop();
                this.twirlWhileSwimmingAnimationState.stop();
                this.floatingAnimationState.stop();
                this.startFloatingAnimationState.stop();
                this.sleepingWhileFloatingAnimationState.stop();
                this.eatingWhileFloatingAnimationState.stop();
                this.sitDownAnimationState.stop();
                this.sittingIdleAnimationState.stop();
                this.standUpAnimationState.stop();
                this.standingIdleAnimationState.stop();
                this.walkingAnimationState.stop();
                break;
            case SLEEPING_WHILE_FLOATING:
                this.sleepingWhileFloatingAnimationState.startIfStopped(this.tickCount);
                break;
            case EATING_WHILE_FLOATING:
                this.eatingWhileFloatingAnimationState.startIfStopped(this.tickCount);
                break;
        }
    }

    private void setupSittingAnimationStates() {
        switch (this.getState()) {
            case SIT_DOWN:
                this.sitDownAnimationState.startIfStopped(this.tickCount);
                this.swimmingIdleAnimationState.stop();
                this.swimmingAnimationState.stop();
                this.twirlWhileSwimmingAnimationState.stop();
                this.floatingAnimationState.stop();
                this.startFloatingAnimationState.stop();
                this.stopFloatingAnimationState.stop();
                this.sleepingWhileFloatingAnimationState.stop();
                this.eatingWhileFloatingAnimationState.stop();
                this.sittingIdleAnimationState.stop();
                this.standUpAnimationState.stop();
                this.standingIdleAnimationState.stop();
                this.walkingAnimationState.stop();
                break;
            case SITTING_IDLE:
                this.sittingIdleAnimationState.startIfStopped(this.tickCount);
                this.swimmingIdleAnimationState.stop();
                this.swimmingAnimationState.stop();
                this.twirlWhileSwimmingAnimationState.stop();
                this.floatingAnimationState.stop();
                this.startFloatingAnimationState.stop();
                this.stopFloatingAnimationState.stop();
                this.sleepingWhileFloatingAnimationState.stop();
                this.eatingWhileFloatingAnimationState.stop();
                this.sitDownAnimationState.stop();
                this.standUpAnimationState.stop();
                this.standingIdleAnimationState.stop();
                this.walkingAnimationState.stop();
                break;
            case STAND_UP:
                this.standUpAnimationState.startIfStopped(this.tickCount);
                this.swimmingIdleAnimationState.stop();
                this.swimmingAnimationState.stop();
                this.twirlWhileSwimmingAnimationState.stop();
                this.floatingAnimationState.stop();
                this.startFloatingAnimationState.stop();
                this.stopFloatingAnimationState.stop();
                this.sleepingWhileFloatingAnimationState.stop();
                this.eatingWhileFloatingAnimationState.stop();
                this.sitDownAnimationState.stop();
                this.sittingIdleAnimationState.stop();
                this.standingIdleAnimationState.stop();
                this.walkingAnimationState.stop();
                break;
            case WALKING:
                this.walkingAnimationState.startIfStopped(this.tickCount);
                this.swimmingIdleAnimationState.stop();
                this.swimmingAnimationState.stop();
                this.twirlWhileSwimmingAnimationState.stop();
                this.floatingAnimationState.stop();
                this.startFloatingAnimationState.stop();
                this.stopFloatingAnimationState.stop();
                this.sleepingWhileFloatingAnimationState.stop();
                this.eatingWhileFloatingAnimationState.stop();
                this.sitDownAnimationState.stop();
                this.sittingIdleAnimationState.stop();
                this.standUpAnimationState.stop();
                this.standingIdleAnimationState.stop();
                break;
        }
    }

    @Override
    public void aiStep() {
        super.aiStep();
        if (!this.level().isClientSide()) {
            if (this.onGround() && !this.isInWater() && !this.isFloating()) {
                if (this.wantsToMove()) {
                    if (this.isSitting() || this.getState() == State.SIT_DOWN) {
                        if (this.getState() != State.STAND_UP) {
                            this.switchToState(State.STAND_UP);
                            this.setSitting(false);
                        }
                    }

                    if (this.getState() == State.STAND_UP && this.inStateTicks >= State.STAND_UP.animationDuration()) {
                        this.switchToState(State.WALKING);
                    }

                    if (!this.isSitting() && this.getState() != State.STAND_UP && this.getState() != State.WALKING) {
                        this.switchToState(State.WALKING);
                    }
                } else {
                    if (!this.isSitting() && this.getState() != State.SIT_DOWN) {
                        this.switchToState(State.SIT_DOWN);
                        this.setSitting(true);
                    } else if (this.getState() == State.SIT_DOWN && this.inStateTicks >= State.SIT_DOWN.animationDuration()) {
                        this.switchToState(State.SITTING_IDLE);
                        this.setSitting(true);
                    }

                    if (this.getState() == State.STAND_UP && this.inStateTicks >= State.STAND_UP.animationDuration()) {
                        this.switchToState(State.SIT_DOWN);
                        this.setSitting(true);
                    }
                }
            }
        }
    }

    @Override
    public void onSyncedDataUpdated(EntityDataAccessor accessor) {
        if (DATA_STATE_ID.equals(accessor)) {
            this.inStateTicks = 0L;
        }

        super.onSyncedDataUpdated(accessor);
    }

    public State getState() {
        return this.entityData.get(DATA_STATE_ID);
    }

    public void switchToState(State state) {
        if (this.getState() != state) {
            this.entityData.set(DATA_STATE_ID, state);
            this.inStateTicks = 0L;
        }
    }

    public boolean isActivelyInFloatTransition() {
        return switch (this.getState()) {
            case START_FLOATING, STOP_FLOATING, FLOATING -> this.inStateTicks < this.getState().animationDuration();
            default -> false;
        };
    }

    public boolean isActivelySittingTransition() {
        return switch (this.getState()) {
            case SIT_DOWN, STAND_UP -> this.inStateTicks < this.getState().animationDuration();
            default -> false;
        };
    }

    private boolean getFlag(int id) {
        return (this.entityData.get(DATA_FLAGS_ID) & id) != 0;
    }

    private void setFlag(int id, boolean value) {
        byte flag = this.entityData.get(DATA_FLAGS_ID);
        this.entityData.set(DATA_FLAGS_ID, value ? (byte)(flag | id) : (byte)(flag & ~id));
    }

    ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

    public boolean isFloating() {
        return this.getFlag(1 << 1);
    }

    public void setFloating(boolean value) {
        this.setFlag(1 << 1, value);
    }

    ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

    public boolean isSwimming() {
        return this.getFlag(1 << 2);
    }

    public void setSwimming(boolean value) {
        this.setFlag(1 << 2, value);
    }

    /// once it's been long enough since it last swam around the sea otter will stop floating and start swimming.
    public boolean wantsToSwimAround() {
        return this.hasBeenLongEnoughSinceLastSwam();
    }

    public boolean hasBeenLongEnoughSinceLastSwam() {
        return this.ticksSinceLastSwam >= 400;
    }

    public void resetTicksSinceLastSwam() {
        this.ticksSinceLastSwam = 0;
    }

    public boolean hasBeenSwimmingLongEnough() {
        return this.ticksSpentSwimming >= 200;
    }

    public void resetTicksSpentSwimming() {
        this.ticksSpentSwimming = 0;
    }

    ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

    public boolean isSitting() {
        return this.getFlag(1 << 3);
    }

    public void setSitting(boolean value) {
        this.setFlag(1 << 3, value);
    }

    public void sitDown() {
        if (!this.isSitting() && this.onGround() && !this.isInWater()) {
            this.setSitting(true);
            this.switchToState(State.SIT_DOWN);
        }
    }

    public void standUp() {
        if (this.isSitting()) {
            this.setSitting(false);
            this.switchToState(State.STAND_UP);
        }
    }

    ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

    @Nullable
    public BlockPos getSurfacePos() {
        return this.surfacePos;
    }

    public void setSurfacePos(BlockPos pos) {
        this.surfacePos = pos;
    }

    public boolean needsToSurface() {
        return this.getFlag(1 << 4);
    }

    public void setNeedsToSurface(boolean value) {
        this.setFlag(1 << 4, value);
    }

    public boolean hasReachedSurface() {
        return this.getFlag(1 << 5);
    }

    public void setReachedSurface(boolean value) {
        this.setFlag(1 << 5, value);
    }

    ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

    @Override
    protected PathNavigation createNavigation(Level level) {
        return new SeaOtter.SeaOtterPathNavigation(this, level);
    }

    @Override
    protected BodyRotationControl createBodyControl() {
        return new SeaOtter.SeaOtterBodyRotationControl(this);
    }

    @Override
    public void travel(Vec3 pos) {
        if (this.refuseToMove()) {
            this.setDeltaMovement(this.getDeltaMovement().multiply(0.0, 1.0, 0.0));
            pos = pos.multiply(0.0, 1.0, 0.0);
        }

        super.travel(pos);
    }

    public void pathfindDirectlyTowards(BlockPos pos, double speed) {
        this.navigation.setMaxVisitedNodesMultiplier(10.0F);
        this.navigation.moveTo(pos.getX() + 0.5D, pos.getY(), pos.getZ() + 0.5D, speed);
    }

    public boolean refuseToMove() {
        return (this.isSitting() || this.isActivelySittingTransition()) || (this.getState() == State.FLOATING);
    }

    public boolean wantsToMove() {
        return !this.getNavigation().isDone() || this.getTarget() != null || this.moveControl.hasWanted();
    }

    ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

    @Override
    public Vec3 getViewVector(float ticks) {
        Vec3 view = super.getViewVector(ticks);
        if (this.isFloating()) {
            return view.reverse();
        }
        return view;
    }

    @Override
    public int getMaxHeadYRot() {
        if (this.isFloating()) {
            return 2;
        } else if (this.isSitting()) {
            return 20;
        } else {
            return super.getMaxHeadYRot();
        }
    }

    @Override
    public int getMaxHeadXRot() {
        if (this.isFloating()) {
            return 20;
        } else if (this.isSitting()) {
            return 20;
        } else {
            return super.getMaxHeadXRot();
        }
    }

    @Override
    public boolean isFood(ItemStack stack) {
        return stack.is(ItemTags.FISHES);
    }

    @Override
    public InteractionResult mobInteract(Player player, InteractionHand hand) {
        if (!this.level().isClientSide()) {
            if (player.getItemInHand(hand).isEmpty()) {
                this.setSitting(false);
                this.setFloating(true);
                this.switchToState(State.START_FLOATING);
                System.out.println("ticksSinceLastSwam: " + this.ticksSinceLastSwam);
                System.out.println("ticksSpentSwimming: " + this.ticksSpentSwimming);
            }
        }
        return super.mobInteract(player, hand);
    }

    ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

    public static class SwimToSurfaceGoal extends Goal {
        private final SeaOtter seaOtter;
        @Nullable
        private BlockPos blockPos;

        public SwimToSurfaceGoal(SeaOtter mob) {
            this.seaOtter = mob;
            this.setFlags(EnumSet.of(Flag.MOVE, Flag.LOOK));
        }

        @Override
        public boolean canUse() {
            if (this.seaOtter.isFloating()) {
                return false;
            } else if (this.seaOtter.hasBeenLongEnoughSinceLastSwam()) { /// meaning the sea otter should be swimming.
                return false;
            }
            return this.seaOtter.needsToSurface();
        }

        @Override
        public boolean canContinueToUse() {
            return !this.seaOtter.hasReachedSurface();
        }

        @Override
        public void start() {
            if (this.findNearestPos().isPresent()) {
                this.blockPos = this.findNearestPos().get();
                this.seaOtter.setSurfacePos(this.blockPos);

                System.out.println("swimming to surface at : " + this.seaOtter.getSurfacePos());
            }
        }

        @Override
        public void stop() {
            this.seaOtter.setNeedsToSurface(false);
            this.seaOtter.getNavigation().stop();

            System.out.println("reached surface");
        }

        @Override
        public void tick() {
            this.seaOtter.setReachedSurface(false);

            if (this.blockPos == null) {
                if (this.findNearestPos().isPresent()) {
                    this.blockPos = this.findNearestPos().get();
                }
            } else {
                this.seaOtter.pathfindDirectlyTowards(this.blockPos, 2.5F);
                this.seaOtter.getLookControl().setLookAt(Vec3.atLowerCornerOf(this.blockPos));

                double distanceToSurface = this.blockPos.getY() - this.seaOtter.getEyePosition().y();
                if (this.seaOtter.getEyeY() < this.blockPos.getY() && this.blockPos.distToCenterSqr(this.seaOtter.position()) <= 5.0D) {
                    this.seaOtter.push(0.0D, Math.min(0.03D, distanceToSurface * 0.05D), 0.0D);
                    this.seaOtter.setSpeed(2.5F * (float) Math.max(0.3D, Math.min(1.0D, distanceToSurface)));
                }

                if (Math.abs(distanceToSurface) <= 0.2D) {
                    this.seaOtter.setDeltaMovement(Vec3.ZERO);
                    this.seaOtter.setSpeed(0.0F);
                    this.seaOtter.setYya(0.0F);

                    /// set that the sea otter has reached the surface, ending the goal and starting the float goal.
                    this.seaOtter.setReachedSurface(true);
                }
            }
        }

        protected Optional findNearestPos() {
            BlockPos pos = this.seaOtter.blockPosition();
            BlockPos.MutableBlockPos mutablePos = new BlockPos.MutableBlockPos();

            for (int i = 0; (double)i <= 24; i = i > 0 ? -i : 1 - i) {
                for (int j = 0; (double)j < 24; ++j) {
                    for (int k = 0; k <= j; k = k > 0 ? -k : 1 - k) {
                        for (int l = k < j && k > -j ? j : 0; l <= j; l = l > 0 ? -l : 1 - l) {
                            mutablePos.setWithOffset(pos, k, i - 1, l);

                            if (this.seaOtter.isWithinRestriction(mutablePos) && this.isValidTarget(this.seaOtter.level(), mutablePos)) {
                                return Optional.of(mutablePos);
                            }
                        }
                    }
                }
            }
            return Optional.empty();
        }

        protected boolean isValidTarget(LevelReader level, BlockPos pos) {
            return level.getBlockState(pos.below()).is(Blocks.WATER) && level.getBlockState(pos).isAir();
        }
    }

    ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

    public static class FloatingGoal extends Goal {
        private final SeaOtter seaOtter;
        private Phase phase;

        public FloatingGoal(SeaOtter mob) {
            this.seaOtter = mob;
            this.setFlags(EnumSet.of(Flag.MOVE));
        }

        @Override
        public boolean canUse() {
            return this.seaOtter.hasReachedSurface() && !this.seaOtter.wantsToSwimAround();
        }

        @Override
        public boolean canContinueToUse() {
            return this.phase != Phase.SWIMMING;
        }

        @Override
        public void start() {
            if (!this.seaOtter.isFloating()) {
                this.seaOtter.setReachedSurface(false);
                this.seaOtter.resetTicksSinceLastSwam();

                this.seaOtter.setFloating(true);
                this.seaOtter.switchToState(State.START_FLOATING);
                this.phase = Phase.START_FLOATING;

                System.out.println("started floating");
            }
        }

        @Override
        public void stop() {
            this.seaOtter.setReachedSurface(false);
            this.seaOtter.setNeedsToSurface(false);
            this.seaOtter.setFloating(false);
            this.seaOtter.switchToState(State.SWIMMING);

            System.out.println("finished floating");
        }

        @Override
        public void tick() {
            switch (this.phase) {
                case START_FLOATING -> {
                    if (this.seaOtter.inStateTicks >= State.START_FLOATING.animationDuration()) {
                        this.seaOtter.switchToState(State.FLOATING);
                        this.phase = Phase.FLOATING;
                    }
                }
                case FLOATING -> {
                    if (this.seaOtter.inStateTicks >= State.FLOATING.animationDuration() && this.wantsToBeDone()) {
                        System.out.println("stopping floating");
                        this.seaOtter.switchToState(State.STOP_FLOATING);
                        this.phase = Phase.STOP_FLOATING;
                    }
                }
                case STOP_FLOATING -> {
                    if (this.seaOtter.inStateTicks >= State.STOP_FLOATING.animationDuration()) {
                        this.seaOtter.switchToState(State.SWIMMING);
                        this.phase = Phase.SWIMMING;
                    }
                }
            }
        }

        private boolean wantsToBeDone() {
            return this.seaOtter.wantsToSwimAround();
        }

        enum Phase {
            SWIMMING,
            START_FLOATING,
            FLOATING,
            STOP_FLOATING
        }
    }

    ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

    public static class SwimAroundGoal extends RandomSwimmingGoal {
        private final SeaOtter seaOtter;

        public SwimAroundGoal(SeaOtter mob, double speed, int interval) {
            super(mob, speed, interval);
            this.seaOtter = mob;
            this.setFlags(EnumSet.of(Flag.MOVE, Flag.LOOK));
        }

        @Override
        public boolean canUse() {
            if (this.seaOtter.needsToSurface()) {
                return false;
            } else if (this.seaOtter.isFloating()) {
                return false;
            }

            return super.canUse();
        }

        @Override
        public boolean canContinueToUse() {
            if (this.seaOtter.needsToSurface()) {
                return false;
            } else if (this.seaOtter.hasBeenSwimmingLongEnough()) {
                return false;
            }

            return super.canContinueToUse();
        }

        @Override
        public void start() {
            super.start();
            this.seaOtter.setSwimming(true);

            System.out.println("started swimming");
        }

        @Override
        public void stop() {
            super.stop();
            if (this.seaOtter.hasBeenSwimmingLongEnough()) {
                this.seaOtter.setNeedsToSurface(true);
                this.seaOtter.resetTicksSinceLastSwam();
                this.seaOtter.resetTicksSpentSwimming();

                System.out.println("stopped swimming");
            }
        }

        @Override
        public void tick() {
            this.seaOtter.ticksSpentSwimming++;
        }
    }

    ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

    public static class SeaOtterLookAtPlayerGoal extends LookAtPlayerGoal {
        private final SeaOtter seaOtter;

        public SeaOtterLookAtPlayerGoal(SeaOtter mob, float distance) {
            super(mob, Player.class, distance);
            this.seaOtter = mob;
        }

        @Override
        public boolean canUse() {
            return super.canUse() && !this.seaOtter.isSwimming() && !this.seaOtter.isActivelyInFloatTransition();
        }
    }

    public static class SeaOtterTemptGoal extends TemptGoal {
        private final SeaOtter seaOtter;

        public SeaOtterTemptGoal(SeaOtter mob, double speed, Predicate items) {
            super(mob, speed, items, false);
            this.seaOtter = mob;
        }
    }

    public static class SeaOtterRandomLookAroundGoal extends RandomLookAroundGoal {
        private final SeaOtter seaOtter;

        public SeaOtterRandomLookAroundGoal(SeaOtter mob) {
            super(mob);
            this.seaOtter = mob;
        }

        @Override
        public boolean canUse() {
            return super.canUse() && !this.seaOtter.isSwimming();
        }
    }

    ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

    public static class SeaOtterMoveControl extends MoveControl {
        private final SeaOtter seaOtter;

        public SeaOtterMoveControl(SeaOtter mob) {
            super(mob);
            this.seaOtter = mob;
        }

        @Override
        public void tick() {
            if (this.seaOtter.isSleeping()) return;

            if (this.seaOtter.isInWater() && !this.seaOtter.isFloating()) {
                this.seaOtter.setDeltaMovement(this.seaOtter.getDeltaMovement().add(this.seaOtter.getLookAngle().scale(this.seaOtter.isFloating() ? 0.002F : 0.005F)));

                if (this.operation == MoveControl.Operation.MOVE_TO && !this.seaOtter.getNavigation().isDone()) {
                    double d0 = this.wantedX - this.seaOtter.getX();
                    double d1 = this.wantedY - this.seaOtter.getY();
                    double d2 = this.wantedZ - this.seaOtter.getZ();
                    double d3 = d0 * d0 + d1 * d1 + d2 * d2;
                    if (d3 < (double) 2.5000003E-7F) {
                        this.seaOtter.setZza(0.0F);
                    } else {
                        float f = (float) (Mth.atan2(d2, d0) * (double) (180F / (float) Math.PI)) - 90.0F;
                        this.seaOtter.setYRot(this.rotlerp(this.seaOtter.getYRot(), f, (float) 10));
                        this.seaOtter.yBodyRot = this.seaOtter.getYRot();
                        this.seaOtter.yHeadRot = this.seaOtter.getYRot();
                        float speed = (float) (this.speedModifier * this.seaOtter.getAttributeValue(Attributes.MOVEMENT_SPEED));
                        this.seaOtter.setSpeed(speed * 0.02F);

                        double d4 = Math.sqrt(d0 * d0 + d2 * d2);
                        if (Math.abs(d1) > (double) 1.0E-5F || Math.abs(d4) > (double) 1.0E-5F) {
                            float f3 = -((float) (Mth.atan2(d1, d4) * (double) (180F / (float) Math.PI)));
                            f3 = Mth.clamp(Mth.wrapDegrees(f3), (float) (-85), (float) 85);
                            this.seaOtter.setXRot(this.rotlerp(this.seaOtter.getXRot(), f3, 5.0F));
                        }

                        if (d1 > 0.0D && d0 * d0 + d2 * d2 < 4.0F && d1 <= 1.0D && this.seaOtter.level().getBlockState(BlockPos.containing(this.wantedX, this.wantedY, this.wantedZ)).getFluidState().isEmpty()) {
                            this.seaOtter.getJumpControl().jump();
                            this.seaOtter.setSpeed(speed);
                        }

                        float f6 = Mth.cos(this.seaOtter.getXRot() * ((float) Math.PI / 180F));
                        float f4 = Mth.sin(this.seaOtter.getXRot() * ((float) Math.PI / 180F));
                        this.seaOtter.zza = f6 * speed;
                        this.seaOtter.yya = -f4 * speed;
                    }
                } else {
                    this.seaOtter.setSpeed(0.0F);
                    this.seaOtter.setXxa(0.0F);
                    this.seaOtter.setYya(0.0F);
                    this.seaOtter.setZza(0.0F);
                }
            } else {
                if (this.hasWanted() && this.seaOtter.isSitting()) {
                    this.seaOtter.standUp();
                }

                if (!this.seaOtter.isFloating()) {
                    super.tick();
                }
            }
        }
    }

    public static class SeaOtterLookControl extends LookControl {
        private final SeaOtter seaOtter;

        public SeaOtterLookControl(SeaOtter mob) {
            super(mob);
            this.seaOtter = mob;
        }

        @Override
        protected Optional getXRotD() {
            if (this.seaOtter.isFloating() && super.getXRotD().isPresent()) {
                return Optional.of(-super.getXRotD().get());
            }
            return super.getXRotD();
        }

        @Override
        protected Optional getYRotD() {
            if (this.seaOtter.isFloating() && super.getYRotD().isPresent()) {
                return Optional.of(Mth.wrapDegrees(super.getYRotD().get() + 160.0F));
            }
            return super.getYRotD();
        }

        @Override
        protected boolean resetXRotOnTick() {
            return this.seaOtter.isActivelyInFloatTransition();
        }

        @Override
        public void tick() {
            if (this.resetXRotOnTick()) {
                this.seaOtter.setXRot(0.0F);
                return;
            }

            if (this.seaOtter.isInWater() && this.seaOtter.isUnderWater()) {
                if (this.lookAtCooldown > 0) {
                    this.lookAtCooldown--;
                    this.getYRotD().ifPresent(i -> this.seaOtter.yHeadRot = this.rotateTowards(this.seaOtter.yHeadRot, i + 20.0F, this.yMaxRotSpeed));
                    this.getXRotD().ifPresent(i -> this.seaOtter.setXRot(this.rotateTowards(this.seaOtter.getXRot(), i + 10.0F, this.xMaxRotAngle)));
                } else {
                    if (this.seaOtter.getNavigation().isDone()) {
                        this.seaOtter.setXRot(this.rotateTowards(this.seaOtter.getXRot(), 0.0F, 5.0F));
                    }
                    this.seaOtter.yHeadRot = this.rotateTowards(this.seaOtter.yHeadRot, this.seaOtter.yBodyRot, this.yMaxRotSpeed);
                }

                float f = Mth.wrapDegrees(this.seaOtter.yHeadRot - this.seaOtter.yBodyRot);
                if (f < (float)(-10)) {
                    this.seaOtter.yBodyRot -= 4.0F;
                } else if (f > (float)10) {
                    this.seaOtter.yBodyRot += 4.0F;
                }
            } else {
                super.tick();
            }
        }
    }

    public static class SeaOtterBodyRotationControl extends BodyRotationControl {
        private final SeaOtter seaOtter;

        public SeaOtterBodyRotationControl(SeaOtter mob) {
            super(mob);
            this.seaOtter = mob;
        }

        @Override
        public void clientTick() {
            if (!this.seaOtter.refuseToMove() && !this.seaOtter.isFloating()) {
                super.clientTick();
            }
        }
    }

    public static class SeaOtterPathNavigation extends AmphibiousPathNavigation {
        private final SeaOtter seaOtter;

        public SeaOtterPathNavigation(SeaOtter mob, Level level) {
            super(mob, level);
            this.seaOtter = mob;
            this.setCanFloat(true);
        }

        @Override
        protected PathFinder createPathFinder(int nodes) {
            this.nodeEvaluator = new AmphibiousNodeEvaluator(true);
            return new PathFinder(this.nodeEvaluator, nodes);
        }

        @Override
        public boolean isStableDestination(BlockPos pos) {
            BlockState belowState = this.level.getBlockState(pos.below());
            if (this.seaOtter.isInWater()) {
                return !(belowState.isAir() || belowState.getFluidState().is(FluidTags.WATER));
            } else {
                return !belowState.isAir();
            }
        }
    }

    public enum State implements StringRepresentable {
        NOTHING("nothing", 0, 0),
        SWIMMING_IDLE("swimming_idle", 1, 50),
        SWIMMING("swimming", 2, 40),
        TWIRL_WHILE_SWIMMING("twirl_while_swimming", 3, 30),
        FLOATING("floating", 4, 50),
        START_FLOATING("start_floating", 5, 30),
        STOP_FLOATING("stop_floating", 6, 25),
        SLEEPING_WHILE_FLOATING("sleeping_while_floating", 7, 0),
        EATING_WHILE_FLOATING("eating_while_floating", 8, 50),
        SIT_DOWN("sit_down", 9, 30),
        SITTING_IDLE("sitting_idle", 10, 60),
        STAND_UP("stand_up", 11, 30),
        STANDING_IDLE("standing_idle", 12, 60),
        WALKING("walking", 13, 45);

        private static final IntFunction BY_ID = ByIdMap.continuous(SeaOtter.State::id, values(), ByIdMap.OutOfBoundsStrategy.ZERO);
        public static final StreamCodec STREAM_CODEC = ByteBufCodecs.idMapper(BY_ID, SeaOtter.State::id);
        private final String name;
        private final int id;
        private final int animationDuration;

        State(final String name, final int id, final int animationDuration) {
            this.name = name;
            this.id = id;
            this.animationDuration = animationDuration;
        }

        public static SeaOtter.State fromName(String name) {
            return StringRepresentable.fromEnum(SeaOtter.State::values).byName(name, NOTHING);
        }

        @Override
        public String getSerializedName() {
            return this.name;
        }

        private int id() {
            return this.id;
        }

        public int animationDuration() {
            return this.animationDuration;
        }
    }

    /**
     * todo: Current Actions
     *
     * actions decide what the entity wants to do.
     *      - i.e. a sea otter who wants to swim, if they're currently floating, once that is finished they will start swimming.
     *
     * que actions so when the current one is finished they will start the next one.
     *
     * actions will be paired with a reasoning.
     *      - i.e. the action "wants to swim" can have the reasoning for why it's doing that, being able to control what "wants to swim" will do exactly.
     *
     * each action has a base length, so like they can only start the next one if they're done with their current one. this is overridden in the case of
     * an emergency, like if they need to run away, or are gonna drown and need to stop looking for fish.
     */
    private enum CoreStates {
        ON_LAND,
        FLOATING_ON_SURFACE,
        SURFACE_SWIMMING,
        DEEP_SEA_SWIMMING
    }

    private enum ActionReasons {
        NEEDS_AIR,
        WANTS_TO_FLOAT,
        IS_BEDTIME
    }

}