Skip to content

Commit

Permalink
Attribute-Dependent Underling Animations (#635)
Browse files Browse the repository at this point in the history
- Walking animation speed for `AnimatedPathfinderMob`s is now adjusted
by their Movement Speed attribute
- Gave Underlings and Pawns the Attack Speed attribute: it influences
how fast their attack animations should play

---------

Co-authored-by: kirderf1 <[email protected]>
  • Loading branch information
Cibernet83 and kirderf1 authored Sep 28, 2024
1 parent 35b3456 commit ad6b75c
Show file tree
Hide file tree
Showing 15 changed files with 161 additions and 81 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- Built in compatability for Better Combat mod

### Changed
- Gave Underlings and Carapacian Pawns the Attack Speed attribute. This influences how fast their attack animations play
- Underlings, Carapacian Pawns, and Consorts now adjust their walk animation speed according to their Movement Speed attribute
- Structure Block Registry Processor can now be used for data generated structures through a processor_list
- Adjusted the hitbox size for Lotus Flowers
- The Lotus Time Capsule now spawns a Lotus Flower when placed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@
import net.minecraft.network.syncher.SynchedEntityData;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.PathfinderMob;
import net.minecraft.world.entity.ai.attributes.Attribute;
import net.minecraft.world.entity.ai.attributes.AttributeInstance;
import net.minecraft.world.entity.ai.attributes.AttributeModifier;
import net.minecraft.world.entity.ai.attributes.Attributes;
import net.minecraft.world.level.Level;
import software.bernie.geckolib.core.animatable.GeoAnimatable;
import software.bernie.geckolib.core.animation.AnimationController;

import java.util.UUID;

Expand Down Expand Up @@ -87,19 +90,26 @@ public void unfreezeMob()
instance.removeModifier(STATIONARY_MOB_MODIFIER.getId());
}

public void setCurrentAnimation(MobAnimation animation)
{
setCurrentAnimation(animation, 1);
}


/**
* Used to set the entity's animation and action
*
* @param animation The animation to set, also contains the action to set entityData from
* @param animationSpeed Used to adjust the length of the animation. The higher the number, the shorter the animation plays for
*/
public void setCurrentAnimation(MobAnimation animation)
public void setCurrentAnimation(MobAnimation animation, double animationSpeed)
{
if(animation.freezeMovement())
freezeMob();
else
unfreezeMob();

this.entityData.set(CURRENT_ACTION, animation.action().ordinal());
this.remainingAnimationTicks = animation.animationLength();
this.remainingAnimationTicks = (int) Math.round(animation.animationLength() / animationSpeed);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public abstract class MobAnimationPhaseGoal<T extends PathfinderMob & PhasedMobA
protected int time = 0;

protected Vector3d lookTarget;
protected double speed;

public MobAnimationPhaseGoal(T entity, PhasedMobAnimation phasedAnimation)
{
Expand All @@ -51,7 +52,7 @@ public boolean canUse()
@Override
public boolean canContinueToUse()
{
return phasedAnimation.getCurrentPhase(time) != PhasedMobAnimation.Phases.NEUTRAL;
return phasedAnimation.getCurrentPhase(entity, time, speed) != PhasedMobAnimation.Phases.NEUTRAL;
}

@Override
Expand All @@ -64,9 +65,10 @@ public boolean isInterruptable()
public void start()
{
MobAnimation animation = phasedAnimation.getAnimation();
this.speed = MobAnimation.getAttributeAffectedSpeed(entity, phasedAnimation.getSpeedModifyingAttribute());

if(entity instanceof AnimatedPathfinderMob animatedMob)
animatedMob.setCurrentAnimation(animation);
animatedMob.setCurrentAnimation(animation, speed);

LivingEntity target = this.entity.getTarget();

Expand Down Expand Up @@ -95,6 +97,6 @@ public void tick()
this.entity.getLookControl().setLookAt(lookTarget.x, lookTarget.y, lookTarget.z);

this.time++;
phasedAnimation.attemptPhaseChange(time, entity);
phasedAnimation.attemptPhaseChange(time, entity, speed);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
import net.minecraft.util.Mth;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.PathfinderMob;
import net.minecraft.world.entity.ai.attributes.Attribute;
import net.minecraft.world.entity.ai.attributes.AttributeInstance;
import net.minecraft.world.entity.ai.attributes.Attributes;
import net.minecraft.world.phys.Vec3;

import javax.annotation.Nonnull;
Expand Down Expand Up @@ -75,7 +78,7 @@ public void tick()
{
super.tick();

if(time == phasedAnimation.getContactStartTime())
if(time == phasedAnimation.getContactStartTime(speed))
{
LivingEntity target = this.entity.getTarget();
attemptToLandAttack(this.entity, target);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ private void applyGroupCooldown()
{
if(iteratedEntity != this.entity && iteratedEntity.getType() == this.entity.getType() && !iteratedEntity.existingCooldownIsLonger(GROUP_SHOOT_COOLDOWN))
{
iteratedEntity.setCooldown(GROUP_SHOOT_COOLDOWN + phasedAnimation.getTotalAnimationLength()); //plays at beginning to prevent overlapping
iteratedEntity.setCooldown(GROUP_SHOOT_COOLDOWN + phasedAnimation.getTotalAnimationLength(speed)); //plays at beginning to prevent overlapping
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ private void applyGroupCooldown()
{
if(iteratedEntity != this.entity && iteratedEntity.getType() == this.entity.getType() && !iteratedEntity.existingCooldownIsLonger(GROUP_SLAM_COOLDOWN))
{
iteratedEntity.setCooldown(GROUP_SLAM_COOLDOWN + phasedAnimation.getTotalAnimationLength()); //plays at beginning to prevent simultaneous goal use by other mobs of the same type
iteratedEntity.setCooldown(GROUP_SLAM_COOLDOWN + phasedAnimation.getTotalAnimationLength(speed)); //plays at beginning to prevent simultaneous goal use by other mobs of the same type
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
package com.mraof.minestuck.entity.animation;

import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.ai.attributes.Attribute;
import net.minecraft.world.entity.ai.attributes.AttributeInstance;

import javax.annotation.Nullable;

/**
* Keeps track of a given animation. Can store stages of the animation in which to perform additional action/events
*
Expand All @@ -14,6 +21,30 @@ public record MobAnimation(Action action, int animationLength, boolean freezeMov
public static final Action IDLE_ACTION = Action.IDLE;
public static final MobAnimation DEFAULT_IDLE_ANIMATION = new MobAnimation(IDLE_ACTION, LOOPING_ANIMATION, false, false);


/**
* Util function to determine how fast an animation should go
* @param entity The entity playing the animation
* @param speedModifyingAttribute The attribute used to determine the animation's speed
* @return The speed at which the animation should play
*/
public static double getAttributeAffectedSpeed(LivingEntity entity, @Nullable Attribute speedModifyingAttribute)
{
if(speedModifyingAttribute == null)
return 1;
AttributeInstance attributeInstance = entity.getAttribute(speedModifyingAttribute);
return attributeInstance == null ? 1 : attributeInstance.getValue();
}

/**
* @param entity the entity to check
* @return whether the entity is moving horizontally
*/
public static boolean isEntityMovingHorizontally(Entity entity)
{
return entity.getDeltaMovement().horizontalDistanceSqr() > 0;
}

/**
* Animations that at least one mob uses. The only kind of animation that is likely to occur at the same time as one of these is walking
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package com.mraof.minestuck.entity.animation;

import net.minecraft.world.entity.PathfinderMob;
import net.minecraft.world.entity.ai.attributes.Attribute;
import net.minecraft.world.entity.ai.attributes.Attributes;

import javax.annotation.Nullable;

/**
* A supplement to MobAnimation that allows for certain code to be performed at the point where one phase of an animation ends and the other begins.
Expand All @@ -13,49 +17,66 @@ public class PhasedMobAnimation
private final int contactStart;
private final int recoveryStart;
private final int recoveryEnd;
@Nullable
private final Attribute speedModifyingAttribute;

public PhasedMobAnimation(MobAnimation animation, int initiationStart, int contactStart, int recoveryStart, int recoveryEnd)
{
this(animation, initiationStart, contactStart, recoveryStart, recoveryEnd, Attributes.ATTACK_SPEED);
}


/**
* @param initiationStart not the first frame of animation
* @param contactStart the apex of animations, when attacks connect
* @param speedModifyingAttribute the entity attribute responsible for affecting the animation's speed
*/
public PhasedMobAnimation(MobAnimation animation, int initiationStart, int contactStart, int recoveryStart)
public PhasedMobAnimation(MobAnimation animation, int initiationStart, int contactStart, int recoveryStart, int recoveryEnd, @Nullable Attribute speedModifyingAttribute)
{
this.animation = animation;
this.initiationStart = initiationStart;
this.contactStart = contactStart;
this.recoveryStart = recoveryStart;
this.recoveryEnd = animation.animationLength(); //recoveryEnd is identical to animationLength in MobAnimation
this.contactStart = contactStart;
this.recoveryStart = recoveryStart;
this.recoveryEnd = recoveryEnd;
this.speedModifyingAttribute = speedModifyingAttribute;
}

public Phases getCurrentPhase(int time)
@Nullable
public Attribute getSpeedModifyingAttribute()
{
if(time < initiationStart)
return speedModifyingAttribute;
}

public Phases getCurrentPhase(PathfinderMob entity, int time, double speed)
{
if(time < getInitiationStartTime(speed))
return Phases.ANTICIPATION;
else if(time < contactStart)
else if(time < getContactStartTime(speed))
return Phases.INITIATION;
else if(time < recoveryStart)
else if(time < getRecoveryStartTime(speed))
return Phases.CONTACT;
else if(time < recoveryEnd)
else if(time < getTotalAnimationLength(speed))
return Phases.RECOVERY;
else
return Phases.NEUTRAL;
}

public int getInitiationStartTime()
public int getInitiationStartTime(double speed)
{
return initiationStart;
return (int) Math.round(initiationStart / speed);
}

public int getContactStartTime()
public int getContactStartTime(double speed)
{
return contactStart;
return (int) Math.round(contactStart / speed);
}

public int getRecoveryStartTime()
public int getRecoveryStartTime(double speed)
{
return recoveryStart;
return (int) Math.round(recoveryStart / speed);
}


public MobAnimation getAnimation()
{
return animation;
Expand All @@ -64,23 +85,23 @@ public MobAnimation getAnimation()
/**
* Equivalent to getting recoveryEnd value
*/
public int getTotalAnimationLength()
public int getTotalAnimationLength(double speed)
{
return recoveryEnd;
return (int) Math.round(recoveryEnd / speed);
}

/**
* Is called every tick to check whether its time to transition to a new phase
*/
public <T extends PathfinderMob & PhasedMobAnimation.Phases.Holder> void attemptPhaseChange(int time, T entity)
public <T extends PathfinderMob & PhasedMobAnimation.Phases.Holder> void attemptPhaseChange(int time, T entity, double speed)
{
if(time == getInitiationStartTime())
if(time == getInitiationStartTime(speed))
entity.setAnimationPhase(Phases.INITIATION, animation.action());
else if(time == getContactStartTime())
else if(time == getContactStartTime(speed))
entity.setAnimationPhase(Phases.CONTACT, animation.action());
else if(time == getRecoveryStartTime())
else if(time == getRecoveryStartTime(speed))
entity.setAnimationPhase(Phases.RECOVERY, animation.action());
else if(time >= getTotalAnimationLength())
else if(time >= getTotalAnimationLength(speed))
entity.setAnimationPhase(Phases.NEUTRAL, animation.action());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,9 @@ public class PawnEntity extends CarapacianEntity implements RangedAttackMob, Ene

private static final MobAnimation TALK_PROPERTIES = new MobAnimation(MobAnimation.Action.TALK, 80, true, false);

public static final PhasedMobAnimation MELEE_ANIMATION = new PhasedMobAnimation(new MobAnimation(MobAnimation.Action.MELEE, 18, true, false), 3, 6, 7);
private static final double ATTACK_ANIMATION_SPEED = 2;

public static final PhasedMobAnimation MELEE_ANIMATION = new PhasedMobAnimation(new MobAnimation(MobAnimation.Action.MELEE, 18, true, false), 3, 5, 7, 13);
private static final RawAnimation WALK_ANIMATION = RawAnimation.begin().thenLoop("walk");
private static final RawAnimation ARMS_WALKING_ANIMATION = RawAnimation.begin().thenLoop("walkarms");
private static final RawAnimation PUNCH_ANIMATION_1 = RawAnimation.begin().then("punch1", Animation.LoopType.PLAY_ONCE);
Expand Down Expand Up @@ -87,7 +89,7 @@ public static PawnEntity createDersite(EntityType<? extends PawnEntity> type, Le

public static AttributeSupplier.Builder pawnAttributes()
{
return CarapacianEntity.carapacianAttributes().add(Attributes.ATTACK_DAMAGE)
return CarapacianEntity.carapacianAttributes().add(Attributes.ATTACK_DAMAGE).add(Attributes.ATTACK_SPEED, 4)
.add(Attributes.MOVEMENT_SPEED, 0.2);
}

Expand Down Expand Up @@ -341,15 +343,16 @@ public String getSpriteType()
public void registerControllers(AnimatableManager.ControllerRegistrar controllers)
{
controllers.add(new AnimationController<>(this, "walkArmsAnimation", PawnEntity::walkArmsAnimation));
controllers.add(new AnimationController<>(this, "walkAnimation", PawnEntity::walkAnimation));
controllers.add(new AnimationController<>(this, "walkAnimation", PawnEntity::walkAnimation)
.setAnimationSpeedHandler(entity -> MobAnimation.getAttributeAffectedSpeed(entity, Attributes.MOVEMENT_SPEED) * 5));
controllers.add(new AnimationController<>(this, "deathAnimation", PawnEntity::deathAnimation));
controllers.add(new AnimationController<>(this, "swingAnimation", PawnEntity::swingAnimation).setAnimationSpeed(2));
controllers.add(new AnimationController<>(this, "swingAnimation", PawnEntity::swingAnimation));
controllers.add(new AnimationController<>(this, "talkAnimation", PawnEntity::talkAnimation));
}

private static PlayState walkAnimation(AnimationState<PawnEntity> state)
{
if(state.isMoving())
if(MobAnimation.isEntityMovingHorizontally(state.getAnimatable()))
{
state.getController().setAnimation(WALK_ANIMATION);
return PlayState.CONTINUE;
Expand All @@ -359,19 +362,19 @@ private static PlayState walkAnimation(AnimationState<PawnEntity> state)

private static PlayState walkArmsAnimation(AnimationState<PawnEntity> state)
{
if(state.isMoving() && !state.getAnimatable().isActive())
if(MobAnimation.isEntityMovingHorizontally(state.getAnimatable()) && !state.getAnimatable().isActive())
{
state.getController().setAnimation(ARMS_WALKING_ANIMATION);
return PlayState.CONTINUE;
}
return PlayState.STOP;
}

private static PlayState deathAnimation(AnimationState<PawnEntity> event)
private static PlayState deathAnimation(AnimationState<PawnEntity> state)
{
if(event.getAnimatable().dead)
if(state.getAnimatable().dead)
{
event.getController().setAnimation(DIE_ANIMATION);
state.getController().setAnimation(DIE_ANIMATION);
return PlayState.CONTINUE;
}
return PlayState.STOP;
Expand All @@ -385,6 +388,7 @@ private static PlayState swingAnimation(AnimationState<PawnEntity> event)
return PlayState.CONTINUE;
}
event.getController().forceAnimationReset();
event.getController().setAnimationSpeed(MobAnimation.getAttributeAffectedSpeed(event.getAnimatable(), Attributes.ATTACK_SPEED)); //Setting animation speed on stop so it doesn't jump around when attack speed changes mid-attack
return PlayState.STOP;
}

Expand Down
Loading

0 comments on commit ad6b75c

Please sign in to comment.