diff --git a/src/main/java/com/minelittlepony/unicopia/ability/Abilities.java b/src/main/java/com/minelittlepony/unicopia/ability/Abilities.java index 30091d60..9dd0d724 100644 --- a/src/main/java/com/minelittlepony/unicopia/ability/Abilities.java +++ b/src/main/java/com/minelittlepony/unicopia/ability/Abilities.java @@ -36,6 +36,7 @@ public interface Abilities { Ability KICK = register(new EarthPonyKickAbility(), "kick", AbilitySlot.PRIMARY); Ability GROW = register(new EarthPonyGrowAbility(), "grow", AbilitySlot.SECONDARY); Ability STOMP = register(new EarthPonyStompAbility(), "stomp", AbilitySlot.TERTIARY); + Ability HUG = register(new HugAbility(), "hug", AbilitySlot.TERTIARY); // pegasus Ability RAINBOOM = register(new PegasusRainboomAbility(), "rainboom", AbilitySlot.PRIMARY); diff --git a/src/main/java/com/minelittlepony/unicopia/ability/CarryAbility.java b/src/main/java/com/minelittlepony/unicopia/ability/CarryAbility.java index 7daab3d2..e93d1c2a 100644 --- a/src/main/java/com/minelittlepony/unicopia/ability/CarryAbility.java +++ b/src/main/java/com/minelittlepony/unicopia/ability/CarryAbility.java @@ -74,6 +74,18 @@ public class CarryAbility implements Ability { PlayerEntity player = iplayer.asEntity(); LivingEntity rider = findRider(player, iplayer.asWorld()); + dropAllPassengers(player); + + if (rider != null) { + rider.startRiding(player, true); + Living.getOrEmpty(rider).ifPresent(living -> living.setCarrier(player)); + } + + Living.transmitPassengers(player); + return true; + } + + protected void dropAllPassengers(PlayerEntity player) { if (player.hasPassengers()) { List passengers = StreamSupport.stream(player.getPassengersDeep().spliterator(), false).toList(); player.removeAllPassengers(); @@ -85,14 +97,6 @@ public class CarryAbility implements Ability { } } } - - if (rider != null) { - rider.startRiding(player, true); - Living.getOrEmpty(rider).ifPresent(living -> living.setCarrier(player)); - } - - Living.transmitPassengers(player); - return true; } @Override diff --git a/src/main/java/com/minelittlepony/unicopia/ability/HugAbility.java b/src/main/java/com/minelittlepony/unicopia/ability/HugAbility.java new file mode 100644 index 00000000..6457ccb9 --- /dev/null +++ b/src/main/java/com/minelittlepony/unicopia/ability/HugAbility.java @@ -0,0 +1,55 @@ +package com.minelittlepony.unicopia.ability; + +import com.minelittlepony.unicopia.Race; +import com.minelittlepony.unicopia.ability.data.Hit; +import com.minelittlepony.unicopia.client.render.PlayerPoser.Animation; +import com.minelittlepony.unicopia.entity.Living; +import com.minelittlepony.unicopia.entity.mob.FriendlyCreeperEntity; +import com.minelittlepony.unicopia.entity.mob.UEntities; +import com.minelittlepony.unicopia.entity.player.Pony; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.mob.CreeperEntity; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.particle.ParticleTypes; + +/** + * Ability to hug mobs. Not all of them are receptive to your advances though, so be careful! + */ +public class HugAbility extends CarryAbility { + + @Override + public boolean canUse(Race race) { + return race.canUseEarth(); + } + + @Override + public boolean apply(Pony pony, Hit data) { + PlayerEntity player = pony.asEntity(); + LivingEntity rider = findRider(player, pony.asWorld()); + + dropAllPassengers(player); + + pony.setAnimation(Animation.ARMS_FORWARD, Animation.Recipient.ANYONE); + + if (rider instanceof CreeperEntity creeper) { + FriendlyCreeperEntity friendlyCreeper = creeper.convertTo(UEntities.FRIENDLY_CREEPER, true); + player.getWorld().spawnEntity(friendlyCreeper); + + friendlyCreeper.startRiding(player, true); + Living.getOrEmpty(friendlyCreeper).ifPresent(living -> living.setCarrier(player)); + } else if (rider instanceof FriendlyCreeperEntity creeper) { + creeper.startRiding(player, true); + Living.getOrEmpty(creeper).ifPresent(living -> living.setCarrier(player)); + } else if (rider != null) { + rider.teleport(player.getX(), player.getY() + 0.5, player.getZ()); + rider.setYaw(player.getYaw() + 180); + + if (rider instanceof FriendlyCreeperEntity) { + pony.spawnParticles(ParticleTypes.HEART, 10); + } + } + + Living.transmitPassengers(player); + return true; + } +} diff --git a/src/main/java/com/minelittlepony/unicopia/ability/NirikBlastAbility.java b/src/main/java/com/minelittlepony/unicopia/ability/NirikBlastAbility.java index 05fd3d74..3c516f32 100644 --- a/src/main/java/com/minelittlepony/unicopia/ability/NirikBlastAbility.java +++ b/src/main/java/com/minelittlepony/unicopia/ability/NirikBlastAbility.java @@ -10,20 +10,17 @@ import com.minelittlepony.unicopia.ability.data.Hit; import com.minelittlepony.unicopia.client.render.PlayerPoser.Animation; import com.minelittlepony.unicopia.client.render.PlayerPoser.Animation.Recipient; import com.minelittlepony.unicopia.entity.player.Pony; -import net.minecraft.block.BlockState; +import com.minelittlepony.unicopia.util.ExplosionUtil; + import net.minecraft.entity.Entity; import net.minecraft.entity.LivingEntity; import net.minecraft.entity.damage.DamageTypes; import net.minecraft.particle.ParticleTypes; import net.minecraft.predicate.entity.EntityPredicates; import net.minecraft.sound.SoundEvents; -import net.minecraft.util.math.BlockPos; import net.minecraft.util.math.Vec3d; import net.minecraft.util.math.random.Random; -import net.minecraft.world.BlockView; import net.minecraft.world.World.ExplosionSourceType; -import net.minecraft.world.explosion.Explosion; -import net.minecraft.world.explosion.ExplosionBehavior; /** * Kirin ability to transform into a nirik @@ -63,12 +60,7 @@ public class NirikBlastAbility implements Ability { @Override public boolean apply(Pony player, Hit data) { - player.asWorld().createExplosion(player.asEntity(), player.damageOf(DamageTypes.FIREBALL), new ExplosionBehavior(){ - @Override - public boolean canDestroyBlock(Explosion explosion, BlockView world, BlockPos pos, BlockState state, float power) { - return false; - } - }, player.getOriginVector(), 5, true, ExplosionSourceType.MOB); + player.asWorld().createExplosion(player.asEntity(), player.damageOf(DamageTypes.FIREBALL), ExplosionUtil.NON_DESTRUCTIVE, player.getOriginVector(), 5, true, ExplosionSourceType.MOB); player.setInvulnerabilityTicks(5); player.setAnimation(Animation.ARMS_UP, Recipient.ANYONE, 12); diff --git a/src/main/java/com/minelittlepony/unicopia/client/URenderers.java b/src/main/java/com/minelittlepony/unicopia/client/URenderers.java index 51bc0442..cb118590 100644 --- a/src/main/java/com/minelittlepony/unicopia/client/URenderers.java +++ b/src/main/java/com/minelittlepony/unicopia/client/URenderers.java @@ -84,6 +84,7 @@ public interface URenderers { EntityRendererRegistry.register(UEntities.CRYSTAL_SHARDS, CrystalShardsEntityRenderer::new); EntityRendererRegistry.register(UEntities.STORM_CLOUD, StormCloudEntityRenderer::new); EntityRendererRegistry.register(UEntities.AIR_BALLOON, AirBalloonEntityRenderer::new); + EntityRendererRegistry.register(UEntities.FRIENDLY_CREEPER, FriendlyCreeperEntityRenderer::new); BlockEntityRendererFactories.register(UBlockEntities.WEATHER_VANE, WeatherVaneBlockEntityRenderer::new); diff --git a/src/main/java/com/minelittlepony/unicopia/client/render/entity/FriendlyCreeperEntityRenderer.java b/src/main/java/com/minelittlepony/unicopia/client/render/entity/FriendlyCreeperEntityRenderer.java new file mode 100644 index 00000000..bce17a39 --- /dev/null +++ b/src/main/java/com/minelittlepony/unicopia/client/render/entity/FriendlyCreeperEntityRenderer.java @@ -0,0 +1,131 @@ +package com.minelittlepony.unicopia.client.render.entity; + +import net.minecraft.client.render.entity.feature.EnergySwirlOverlayFeatureRenderer; +import net.minecraft.client.render.entity.feature.FeatureRendererContext; + +import com.minelittlepony.unicopia.Unicopia; +import com.minelittlepony.unicopia.entity.mob.FriendlyCreeperEntity; + +import net.minecraft.client.model.ModelPart; +import net.minecraft.client.render.entity.EntityRendererFactory; +import net.minecraft.client.render.entity.MobEntityRenderer; +import net.minecraft.client.render.entity.model.CreeperEntityModel; +import net.minecraft.client.render.entity.model.EntityModel; +import net.minecraft.client.render.entity.model.EntityModelLayers; +import net.minecraft.client.render.entity.model.EntityModelLoader; +import net.minecraft.client.render.entity.model.EntityModelPartNames; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.MathHelper; + +public class FriendlyCreeperEntityRenderer extends MobEntityRenderer { + private static final Identifier FRIENDLY_TEXTURE = Unicopia.id("textures/entity/creeper/friendly.png"); + private static final Identifier UNFIRENDLY_TEXTURE = new Identifier("textures/entity/creeper/creeper.png"); + + public FriendlyCreeperEntityRenderer(EntityRendererFactory.Context context) { + super(context, new Model(context.getPart(EntityModelLayers.CREEPER)), 0.5f); + addFeature(new ChargeFeature(this, context.getModelLoader())); + } + + @Override + protected void scale(FriendlyCreeperEntity creeperEntity, MatrixStack matrixStack, float f) { + float g = creeperEntity.getClientFuseTime(f); + float h = 1.0f + MathHelper.sin(g * 100.0f) * g * 0.01f; + g = MathHelper.clamp(g, 0.0f, 1.0f); + g *= g; + g *= g; + float i = (1.0f + g * 0.4f) * h; + float j = (1.0f + g * 0.1f) / h; + matrixStack.scale(i, j, i); + } + + @Override + protected void setupTransforms(FriendlyCreeperEntity entity, MatrixStack matrices, float animationProgress, float bodyYaw, float tickDelta) { + super.setupTransforms(entity, matrices, animationProgress, bodyYaw, tickDelta); + if (entity.isSitting()) { + matrices.translate(0, -0.25, 0); + } + } + + @Override + protected boolean isShaking(FriendlyCreeperEntity entity) { + return super.isShaking(entity) || entity.isConverting(); + } + + @Override + protected float getAnimationCounter(FriendlyCreeperEntity entity, float f) { + float fuseTime = entity.getClientFuseTime(f); + if ((int)(fuseTime * 10) % 2 == 0) { + return 0; + } + return MathHelper.clamp(fuseTime, 0.5f, 1.0f); + } + + @Override + public Identifier getTexture(FriendlyCreeperEntity entity) { + return entity.isConverting() ? UNFIRENDLY_TEXTURE : FRIENDLY_TEXTURE; + } + + public static class Model extends CreeperEntityModel { + private final ModelPart leftHindLeg; + private final ModelPart rightHindLeg; + private final ModelPart leftFrontLeg; + private final ModelPart rightFrontLeg; + public Model(ModelPart root) { + super(root); + this.rightHindLeg = root.getChild(EntityModelPartNames.RIGHT_HIND_LEG); + this.leftHindLeg = root.getChild(EntityModelPartNames.LEFT_HIND_LEG); + this.rightFrontLeg = root.getChild(EntityModelPartNames.RIGHT_FRONT_LEG); + this.leftFrontLeg = root.getChild(EntityModelPartNames.LEFT_FRONT_LEG); + } + + @Override + public void setAngles(FriendlyCreeperEntity entity, float limbAngle, float limbDistance, float animationProgress, float headYaw, float headPitch) { + leftHindLeg.resetTransform(); + rightHindLeg.resetTransform(); + leftFrontLeg.resetTransform(); + rightFrontLeg.resetTransform(); + super.setAngles(entity, limbAngle, limbDistance, animationProgress, headYaw, headPitch); + if (entity.isSitting()) { + float legSpread = 0.001F; + leftHindLeg.pivotZ -= 3; + leftHindLeg.pitch = MathHelper.HALF_PI; + leftHindLeg.yaw = legSpread; + rightHindLeg.pivotZ -= 3; + rightHindLeg.pitch = MathHelper.HALF_PI; + rightHindLeg.yaw = -legSpread; + leftFrontLeg.pivotZ += 3; + leftFrontLeg.pitch = -MathHelper.HALF_PI; + leftFrontLeg.yaw = -legSpread; + rightFrontLeg.pivotZ += 3; + rightFrontLeg.pitch = -MathHelper.HALF_PI; + rightFrontLeg.yaw = legSpread; + } + } + } + + public static class ChargeFeature extends EnergySwirlOverlayFeatureRenderer { + private static final Identifier SKIN = new Identifier("textures/entity/creeper/creeper_armor.png"); + private final CreeperEntityModel model; + + public ChargeFeature(FeatureRendererContext context, EntityModelLoader loader) { + super(context); + model = new Model(loader.getModelPart(EntityModelLayers.CREEPER_ARMOR)); + } + + @Override + protected float getEnergySwirlX(float partialAge) { + return partialAge * 0.01f; + } + + @Override + protected Identifier getEnergySwirlTexture() { + return SKIN; + } + + @Override + protected EntityModel getEnergySwirlModel() { + return this.model; + } + } +} diff --git a/src/main/java/com/minelittlepony/unicopia/entity/Creature.java b/src/main/java/com/minelittlepony/unicopia/entity/Creature.java index f82e1552..eec0a798 100644 --- a/src/main/java/com/minelittlepony/unicopia/entity/Creature.java +++ b/src/main/java/com/minelittlepony/unicopia/entity/Creature.java @@ -16,6 +16,7 @@ import com.minelittlepony.unicopia.ability.magic.spell.effect.TargetSelecter; import com.minelittlepony.unicopia.entity.ai.BreakHeartGoal; import com.minelittlepony.unicopia.entity.ai.DynamicTargetGoal; import com.minelittlepony.unicopia.entity.ai.EatMuffinGoal; +import com.minelittlepony.unicopia.entity.ai.FleeExplosionGoal; import com.minelittlepony.unicopia.entity.ai.WantItTakeItGoal; import com.minelittlepony.unicopia.entity.mob.UEntityAttributes; @@ -152,6 +153,9 @@ public class Creature extends Living implements WeaklyOwned.Mutabl eatMuffinGoal = new EatMuffinGoal(pig, targetter); goals.add(3, eatMuffinGoal); } + if (entity instanceof TameableEntity tameable) { + goals.add(3, new FleeExplosionGoal(tameable, 6, 1, 1.2)); + } initMinionAi(targets); initDiscordedAi(); diff --git a/src/main/java/com/minelittlepony/unicopia/entity/Living.java b/src/main/java/com/minelittlepony/unicopia/entity/Living.java index ed657348..17d53f73 100644 --- a/src/main/java/com/minelittlepony/unicopia/entity/Living.java +++ b/src/main/java/com/minelittlepony/unicopia/entity/Living.java @@ -334,7 +334,7 @@ public abstract class Living implements Equine, Caste if (isBeingCarried()) { Pony carrier = Pony.of(entity.getVehicle()).orElse(null); - if (!Abilities.CARRY.canUse(carrier.getCompositeRace())) { + if (!Abilities.CARRY.canUse(carrier.getCompositeRace()) && !Abilities.HUG.canUse(carrier.getCompositeRace())) { entity.stopRiding(); entity.refreshPositionAfterTeleport(carrier.getOriginVector()); Living.transmitPassengers(carrier.asEntity()); diff --git a/src/main/java/com/minelittlepony/unicopia/entity/ai/FleeExplosionGoal.java b/src/main/java/com/minelittlepony/unicopia/entity/ai/FleeExplosionGoal.java new file mode 100644 index 00000000..1074ce90 --- /dev/null +++ b/src/main/java/com/minelittlepony/unicopia/entity/ai/FleeExplosionGoal.java @@ -0,0 +1,106 @@ +package com.minelittlepony.unicopia.entity.ai; + +import java.util.Comparator; +import java.util.EnumSet; +import java.util.function.Predicate; +import java.util.stream.Stream; + +import org.jetbrains.annotations.Nullable; + +import com.minelittlepony.unicopia.entity.Creature; +import com.minelittlepony.unicopia.entity.Living; + +import net.minecraft.entity.Entity; +import net.minecraft.entity.TntEntity; +import net.minecraft.entity.ai.NoPenaltyTargeting; +import net.minecraft.entity.ai.goal.Goal; +import net.minecraft.entity.ai.goal.PrioritizedGoal; +import net.minecraft.entity.ai.pathing.Path; +import net.minecraft.entity.mob.PathAwareEntity; +import net.minecraft.entity.vehicle.TntMinecartEntity; +import net.minecraft.util.math.Vec3d; + +public class FleeExplosionGoal extends Goal { + private static final Predicate SOURCE_PREDICATE = e -> e instanceof TntMinecartEntity || e instanceof TntEntity; + + private final PathAwareEntity mob; + private final double slowSpeed; + private final double fastSpeed; + private final Comparator sorting; + + @Nullable + private Entity targetEntity; + @Nullable + private Path fleePath; + + public FleeExplosionGoal(PathAwareEntity mob, float distance, double slowSpeed, double fastSpeed) { + this.setControls(EnumSet.of(Goal.Control.MOVE)); + this.mob = mob; + this.slowSpeed = slowSpeed; + this.fastSpeed = fastSpeed; + this.sorting = Comparator.comparingDouble(e -> e.squaredDistanceTo(mob)); + } + + public void setFleeTarget(@Nullable Entity target) { + this.targetEntity = target; + } + + @Override + public boolean canStart() { + if (targetEntity == null || targetEntity.isRemoved()) { + targetEntity = mob.getWorld().getOtherEntities(mob, mob.getBoundingBox().expand(5, 3, 5), SOURCE_PREDICATE).stream().sorted(sorting).findFirst().orElse(null); + } + + if (targetEntity == null) { + return false; + } + + Vec3d targetPosition = NoPenaltyTargeting.findFrom(mob, 16, 7, targetEntity.getPos()); + if (targetPosition == null + || targetEntity.squaredDistanceTo(targetPosition.x, targetPosition.y, targetPosition.z) < targetEntity.squaredDistanceTo(mob)) { + return false; + } + fleePath = mob.getNavigation().findPathTo(targetPosition.x, targetPosition.y, targetPosition.z, 0); + return fleePath != null; + } + + @Override + public boolean shouldContinue() { + return !mob.getNavigation().isIdle(); + } + + @Override + public void start() { + mob.getNavigation().startMovingAlong(fleePath, slowSpeed); + } + + @Override + public void stop() { + targetEntity = null; + } + + @Override + public void tick() { + if (mob.squaredDistanceTo(targetEntity) < 49.0) { + mob.getNavigation().setSpeed(fastSpeed); + } else { + mob.getNavigation().setSpeed(slowSpeed); + } + } + + public static void notifySurroundings(Entity explosionSource, float radius) { + explosionSource.getWorld().getOtherEntities(explosionSource, explosionSource.getBoundingBox().expand(radius), e -> { + return Living.getOrEmpty(e).filter(l -> l instanceof Creature c).isPresent(); + }).forEach(e -> { + getGoals((Creature)Living.living(e)).forEach(goal -> goal.setFleeTarget(explosionSource)); + }); + } + + private static Stream getGoals(Creature creature) { + return creature.getGoals().stream() + .flatMap(goals -> goals.getGoals().stream()) + .map(PrioritizedGoal::getGoal) + .filter(g -> g instanceof FleeExplosionGoal) + .map(FleeExplosionGoal.class::cast); + } +} \ No newline at end of file diff --git a/src/main/java/com/minelittlepony/unicopia/entity/mob/FriendlyCreeperEntity.java b/src/main/java/com/minelittlepony/unicopia/entity/mob/FriendlyCreeperEntity.java new file mode 100644 index 00000000..01e128d5 --- /dev/null +++ b/src/main/java/com/minelittlepony/unicopia/entity/mob/FriendlyCreeperEntity.java @@ -0,0 +1,511 @@ +package com.minelittlepony.unicopia.entity.mob; + +import java.util.Collection; +import java.util.EnumSet; +import java.util.UUID; +import java.util.function.Consumer; + +import org.jetbrains.annotations.Nullable; + +import com.minelittlepony.unicopia.entity.Creature; +import com.minelittlepony.unicopia.entity.Equine; +import com.minelittlepony.unicopia.entity.ai.FleeExplosionGoal; + +import net.minecraft.block.BlockState; +import net.minecraft.client.render.entity.feature.SkinOverlayOwner; +import net.minecraft.entity.AreaEffectCloudEntity; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityStatuses; +import net.minecraft.entity.EntityType; +import net.minecraft.entity.LightningEntity; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.ai.goal.ActiveTargetGoal; +import net.minecraft.entity.ai.goal.AttackWithOwnerGoal; +import net.minecraft.entity.ai.goal.EscapeDangerGoal; +import net.minecraft.entity.ai.goal.FleeEntityGoal; +import net.minecraft.entity.ai.goal.Goal; +import net.minecraft.entity.ai.goal.LookAroundGoal; +import net.minecraft.entity.ai.goal.LookAtEntityGoal; +import net.minecraft.entity.ai.goal.MeleeAttackGoal; +import net.minecraft.entity.ai.goal.RevengeGoal; +import net.minecraft.entity.ai.goal.SitGoal; +import net.minecraft.entity.ai.goal.SwimGoal; +import net.minecraft.entity.ai.goal.TrackOwnerAttackerGoal; +import net.minecraft.entity.ai.goal.UniversalAngerGoal; +import net.minecraft.entity.ai.goal.WanderAroundFarGoal; +import net.minecraft.entity.ai.pathing.PathNodeType; +import net.minecraft.entity.attribute.DefaultAttributeContainer; +import net.minecraft.entity.attribute.EntityAttributes; +import net.minecraft.entity.damage.DamageSource; +import net.minecraft.entity.data.DataTracker; +import net.minecraft.entity.data.TrackedData; +import net.minecraft.entity.data.TrackedDataHandlerRegistry; +import net.minecraft.entity.effect.StatusEffectInstance; +import net.minecraft.entity.mob.AbstractSkeletonEntity; +import net.minecraft.entity.mob.Angerable; +import net.minecraft.entity.mob.CreeperEntity; +import net.minecraft.entity.mob.HostileEntity; +import net.minecraft.entity.passive.CatEntity; +import net.minecraft.entity.passive.GoatEntity; +import net.minecraft.entity.passive.OcelotEntity; +import net.minecraft.entity.passive.PassiveEntity; +import net.minecraft.entity.passive.TameableEntity; +import net.minecraft.entity.passive.WolfEntity; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.nbt.NbtElement; +import net.minecraft.particle.ParticleTypes; +import net.minecraft.registry.tag.ItemTags; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.sound.SoundEvent; +import net.minecraft.sound.SoundEvents; +import net.minecraft.text.Text; +import net.minecraft.util.ActionResult; +import net.minecraft.util.Hand; +import net.minecraft.util.TimeHelper; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.MathHelper; +import net.minecraft.util.math.intprovider.UniformIntProvider; +import net.minecraft.world.BlockView; +import net.minecraft.world.EntityView; +import net.minecraft.world.World; +import net.minecraft.world.event.GameEvent; +import net.minecraft.world.explosion.Explosion; + +public class FriendlyCreeperEntity extends TameableEntity implements SkinOverlayOwner, Angerable { + private static final TrackedData FUSE_SPEED = DataTracker.registerData(FriendlyCreeperEntity.class, TrackedDataHandlerRegistry.INTEGER); + private static final TrackedData CHARGED = DataTracker.registerData(FriendlyCreeperEntity.class, TrackedDataHandlerRegistry.BOOLEAN); + private static final TrackedData IGNITED = DataTracker.registerData(FriendlyCreeperEntity.class, TrackedDataHandlerRegistry.BOOLEAN); + private static final TrackedData ANGER_TIME = DataTracker.registerData(FriendlyCreeperEntity.class, TrackedDataHandlerRegistry.INTEGER); + private static final UniformIntProvider ANGER_TIME_RANGE = TimeHelper.betweenSeconds(20, 39); + + private int lastFuseTime; + private int currentFuseTime; + private short fuseTime = 30; + private byte explosionRadius = 3; + private int headsDropped; + private short hugTime; + private short lastHugTime; + + protected FriendlyCreeperEntity(EntityType type, World world) { + super(type, world); + setTamed(false); + setPathfindingPenalty(PathNodeType.POWDER_SNOW, -1); + setPathfindingPenalty(PathNodeType.DANGER_POWDER_SNOW, -1); + } + + @Override + protected void initDataTracker() { + super.initDataTracker(); + dataTracker.startTracking(FUSE_SPEED, -1); + dataTracker.startTracking(CHARGED, false); + dataTracker.startTracking(IGNITED, false); + dataTracker.startTracking(ANGER_TIME, 0); + } + + @Override + protected void initGoals() { + goalSelector.add(1, new SwimGoal(this)); + goalSelector.add(1, new EscapeGoal(1.5)); + goalSelector.add(2, new SitGoal(this)); + goalSelector.add(3, new IgniteGoal()); + goalSelector.add(4, new FleeEntityGoal<>(this, OcelotEntity.class, 6, 1, 1.2)); + goalSelector.add(4, new FleeEntityGoal<>(this, CatEntity.class, 6, 1, 1.2)); + goalSelector.add(5, new MeleeAttackGoal(this, 1, false)); + goalSelector.add(6, new WanderAroundFarGoal(this, 0.8)); + goalSelector.add(7, new LookAtEntityGoal(this, PlayerEntity.class, 8)); + goalSelector.add(7, new LookAroundGoal(this)); + targetSelector.add(1, new TrackOwnerAttackerGoal(this)); + targetSelector.add(2, new AttackWithOwnerGoal(this)); + targetSelector.add(3, new RevengeGoal(this).setGroupRevenge(WolfEntity.class)); + targetSelector.add(7, new ActiveTargetGoal<>(this, AbstractSkeletonEntity.class, false)); + targetSelector.add(8, new UniversalAngerGoal<>(this, true)); + } + + public static DefaultAttributeContainer.Builder createCreeperAttributes() { + return HostileEntity.createHostileAttributes().add(EntityAttributes.GENERIC_MOVEMENT_SPEED, 0.25); + } + + @Override + protected Text getDefaultName() { + return EntityType.CREEPER.getName(); + } + + @Override + public int getSafeFallDistance() { + return 3 + (getTarget() == null ? 0 : ((int)(getHealth() - 1))); + } + + @Override + public boolean handleFallDamage(float fallDistance, float damageMultiplier, DamageSource damageSource) { + boolean bl = super.handleFallDamage(fallDistance, damageMultiplier, damageSource); + currentFuseTime += (int)(fallDistance * 1.5f); + if (currentFuseTime > fuseTime - 5) { + currentFuseTime = fuseTime - 5; + } + return bl; + } + + @Override + public void writeCustomDataToNbt(NbtCompound nbt) { + super.writeCustomDataToNbt(nbt); + if (dataTracker.get(CHARGED).booleanValue()) { + nbt.putBoolean("powered", true); + } + nbt.putShort("Fuse", fuseTime); + nbt.putShort("Hugged", hugTime); + nbt.putByte("ExplosionRadius", explosionRadius); + nbt.putBoolean("ignited", isIgnited()); + } + + @Override + public void readCustomDataFromNbt(NbtCompound nbt) { + super.readCustomDataFromNbt(nbt); + dataTracker.set(CHARGED, nbt.getBoolean("powered")); + if (nbt.contains("Fuse", NbtElement.NUMBER_TYPE)) { + fuseTime = nbt.getShort("Fuse"); + } + if (nbt.contains("Hugged", NbtElement.NUMBER_TYPE)) { + hugTime = nbt.getShort("Hugged"); + } + if (nbt.contains("ExplosionRadius", NbtElement.NUMBER_TYPE)) { + explosionRadius = nbt.getByte("ExplosionRadius"); + } + if (nbt.getBoolean("ignited")) { + ignite(); + } + } + + @Override + public void tick() { + setSitting(isInSittingPose()); + + if (isAlive()) { + lastFuseTime = currentFuseTime; + if (isIgnited()) { + setFuseSpeed(1); + } + int fuseSpeed = getFuseSpeed(); + if (fuseSpeed > 0 && currentFuseTime == 0) { + playSound(SoundEvents.ENTITY_CREEPER_PRIMED, 1.0f, 0.5f); + emitGameEvent(GameEvent.PRIME_FUSE); + } + currentFuseTime = Math.max(0, currentFuseTime + fuseSpeed); + if (currentFuseTime >= fuseTime) { + currentFuseTime = fuseTime; + explode(); + } + + lastHugTime = hugTime; + + if (!isTamed()) { + if (isConverting()) { + if (++hugTime >= 100) { + if (!getWorld().isClient) { + setOwner(getCreature().getCarrierId().map(getWorld()::getPlayerByUuid).orElse(null)); + getWorld().sendEntityStatus(this, EntityStatuses.ADD_POSITIVE_PLAYER_REACTION_PARTICLES); + } + } + + if (hugTime % 5 == 0) { + playHurtSound(getDamageSources().generic()); + } + } else { + hugTime = 0; + if (!getWorld().isClient) { + getWorld().spawnEntity(convertTo(EntityType.CREEPER, true)); + discard(); + } + } + } else { + if (random.nextInt(30) == 0) { + spawnHeart(); + } + hugTime = 0; + } + } + + super.tick(); + } + + @Override + public void tickMovement() { + super.tickMovement(); + + if (!getWorld().isClient) { + tickAngerLogic((ServerWorld)getWorld(), true); + } + } + + private void spawnHeart() { + getWorld().addParticle(ParticleTypes.HEART, random.nextTriangular(getX(), 0.5), getY() + getHeight(), random.nextTriangular(getZ(), 0.5), 0, 0, 0); + } + + private Creature getCreature() { + return Equine.of(this, c -> c instanceof Creature).get(); + } + + public boolean isConverting() { + return !isTamed() && getCreature().isBeingCarried(); + } + + @Override + public void setTarget(@Nullable LivingEntity target) { + if (target instanceof GoatEntity) { + return; + } + super.setTarget(target); + } + + @Override + protected SoundEvent getHurtSound(DamageSource source) { + return SoundEvents.ENTITY_CREEPER_HURT; + } + + @Override + protected SoundEvent getDeathSound() { + return SoundEvents.ENTITY_CREEPER_DEATH; + } + + @Override + protected void dropEquipment(DamageSource source, int lootingMultiplier, boolean allowDrops) { + super.dropEquipment(source, lootingMultiplier, allowDrops); + if (source.getAttacker() instanceof CreeperEntity c && c.shouldDropHead()) { + c.onHeadDropped(); + dropItem(Items.CREEPER_HEAD); + } + } + + @Override + public boolean tryAttack(Entity target) { + return true; + } + + @Override + public boolean shouldRenderOverlay() { + return dataTracker.get(CHARGED); + } + + public float getClientFuseTime(float timeDelta) { + return MathHelper.lerp(timeDelta, (float)lastFuseTime, (float)currentFuseTime) / (fuseTime - 2) + + MathHelper.lerp(timeDelta, (float)lastHugTime, (float)hugTime) / 98F; + } + + public int getFuseSpeed() { + return dataTracker.get(FUSE_SPEED); + } + + public void setFuseSpeed(int fuseSpeed) { + dataTracker.set(FUSE_SPEED, fuseSpeed); + } + + @Override + public void onStruckByLightning(ServerWorld world, LightningEntity lightning) { + super.onStruckByLightning(world, lightning); + dataTracker.set(CHARGED, true); + } + + @Override + public ActionResult interactMob(PlayerEntity player, Hand hand) { + ItemStack stack = player.getStackInHand(hand); + + Consumer statusCallback = p -> p.sendToolBreakStatus(hand); + + if (stack.isEmpty() && isOwner(player)) { + setSitting(!isSitting()); + setInSittingPose(isSitting()); + return ActionResult.success(getWorld().isClient); + } + + if (stack.isIn(ItemTags.CREEPER_IGNITERS)) { + SoundEvent soundEvent = stack.isOf(Items.FIRE_CHARGE) ? SoundEvents.ITEM_FIRECHARGE_USE : SoundEvents.ITEM_FLINTANDSTEEL_USE; + getWorld().playSound(player, getX(), getY(), getZ(), soundEvent, getSoundCategory(), 1, random.nextFloat() * 0.4f + 0.8f); + + if (!getWorld().isClient) { + ignite(); + if (!stack.isDamageable()) { + stack.decrement(1); + } else { + stack.damage(1, player, statusCallback); + } + } + + return ActionResult.success(getWorld().isClient); + } + + if (stack.isOf(Items.GUNPOWDER) && getHealth() < getMaxHealth()) { + getWorld().playSound(player, getX(), getY(), getZ(), SoundEvents.ENTITY_CAT_EAT, getSoundCategory(), 1, random.nextFloat() * 0.4f + 0.8f); + + currentFuseTime = fuseTime - 1; + if (!getWorld().isClient) { + heal(3); + getWorld().sendEntityStatus(this, EntityStatuses.ADD_POSITIVE_PLAYER_REACTION_PARTICLES); + if (!stack.isDamageable()) { + stack.decrement(1); + } else { + stack.damage(1, player, statusCallback); + } + } + + return ActionResult.success(getWorld().isClient); + } + + return super.interactMob(player, hand); + } + + private void explode() { + if (!getWorld().isClient) { + dead = true; + getWorld().createExplosion(this, getX(), getY(), getZ(), getExplosionRadius(), World.ExplosionSourceType.MOB); + discard(); + spawnEffectsCloud(); + } + } + + @Override + public boolean canExplosionDestroyBlock(Explosion explosion, BlockView world, BlockPos pos, BlockState state, float explosionPower) { + return false; + } + + private float getExplosionRadius() { + return explosionRadius * (shouldRenderOverlay() ? 2 : 1); + } + + private void spawnEffectsCloud() { + Collection effects = getStatusEffects(); + if (!effects.isEmpty()) { + AreaEffectCloudEntity cloud = new AreaEffectCloudEntity(getWorld(), getX(), getY(), getZ()); + cloud.setRadius(2.5f); + cloud.setRadiusOnUse(-0.5f); + cloud.setWaitTime(10); + cloud.setDuration(cloud.getDuration() / 2); + cloud.setRadiusGrowth(-cloud.getRadius() / cloud.getDuration()); + effects.forEach(effect -> cloud.addEffect(new StatusEffectInstance(effect))); + getWorld().spawnEntity(cloud); + } + } + + public boolean isIgnited() { + return dataTracker.get(IGNITED); + } + + public void ignite() { + dataTracker.set(IGNITED, true); + FleeExplosionGoal.notifySurroundings(this, getExplosionRadius()); + } + + public boolean shouldDropHead() { + return shouldRenderOverlay() && headsDropped < 1; + } + + public void onHeadDropped() { + headsDropped++; + } + + @Override + public EntityView method_48926() { + return getWorld(); + } + + @Override + public PassiveEntity createChild(ServerWorld world, PassiveEntity partner) { + FriendlyCreeperEntity child = (FriendlyCreeperEntity)getType().create(world); + UUID uUID = getOwnerUuid(); + if (uUID != null) { + child.setOwnerUuid(uUID); + child.setTamed(true); + } + return child; + } + + @Nullable + private UUID angerTarget; + + @Override + public int getAngerTime() { + return dataTracker.get(ANGER_TIME); + } + + @Override + public void setAngerTime(int time) { + dataTracker.set(ANGER_TIME, time); + } + + @Nullable + @Override + public UUID getAngryAt() { + return angerTarget; + } + + @Override + public void setAngryAt(UUID target) { + angerTarget = target; + } + + @Override + public void chooseRandomAngerTime() { + setAngerTime(ANGER_TIME_RANGE.get(random)); + } + + class IgniteGoal extends Goal { + @Nullable + private LivingEntity target; + + public IgniteGoal() { + this.setControls(EnumSet.of(Goal.Control.MOVE)); + } + + @Override + public boolean canStart() { + LivingEntity livingEntity = getTarget(); + return getFuseSpeed() > 0 || livingEntity != null && squaredDistanceTo(livingEntity) < 9.0; + } + + @Override + public void start() { + getNavigation().stop(); + target = getTarget(); + } + + @Override + public void stop() { + target = null; + } + + @Override + public boolean shouldRunEveryTick() { + return true; + } + + @Override + public void tick() { + if (target == null) { + setFuseSpeed(-1); + return; + } + if (squaredDistanceTo(target) > 49.0) { + setFuseSpeed(-1); + return; + } + if (!getVisibilityCache().canSee(target)) { + setFuseSpeed(-1); + return; + } + setFuseSpeed(1); + } + } + + class EscapeGoal extends EscapeDangerGoal { + + public EscapeGoal(double speed) { + super(FriendlyCreeperEntity.this, speed); + } + + @Override + protected boolean isInDanger() { + return mob.shouldEscapePowderSnow() || mob.isOnFire(); + } + } +} diff --git a/src/main/java/com/minelittlepony/unicopia/entity/mob/UEntities.java b/src/main/java/com/minelittlepony/unicopia/entity/mob/UEntities.java index 59a58ea3..d369e99c 100644 --- a/src/main/java/com/minelittlepony/unicopia/entity/mob/UEntities.java +++ b/src/main/java/com/minelittlepony/unicopia/entity/mob/UEntities.java @@ -45,6 +45,10 @@ public interface UEntities { EntityType TWITTERMITE = register("twittermite", FabricEntityTypeBuilder.create(SpawnGroup.MISC, FairyEntity::new) .trackRangeBlocks(200) .dimensions(EntityDimensions.fixed(0.1F, 0.1F))); + EntityType FRIENDLY_CREEPER = register("friendly_creeper", FabricEntityTypeBuilder.create(SpawnGroup.MISC, FriendlyCreeperEntity::new) + .trackRangeBlocks(8) + .dimensions(EntityDimensions.fixed(0.6f, 1.7f)) + ); EntityType SPELLBOOK = register("spellbook", FabricEntityTypeBuilder.create(SpawnGroup.MISC, SpellbookEntity::new) .trackRangeBlocks(200) .dimensions(EntityDimensions.fixed(0.9F, 0.5F))); @@ -72,6 +76,7 @@ public interface UEntities { FabricDefaultAttributeRegistry.register(TWITTERMITE, FairyEntity.createMobAttributes()); FabricDefaultAttributeRegistry.register(AIR_BALLOON, FlyingEntity.createMobAttributes()); FabricDefaultAttributeRegistry.register(SOMBRA, SombraEntity.createMobAttributes()); + FabricDefaultAttributeRegistry.register(FRIENDLY_CREEPER, FriendlyCreeperEntity.createCreeperAttributes()); if (!Unicopia.getConfig().disableButterflySpawning.get()) { final Predicate butterflySpawnable = BiomeSelectors.foundInOverworld() diff --git a/src/main/java/com/minelittlepony/unicopia/util/ExplosionUtil.java b/src/main/java/com/minelittlepony/unicopia/util/ExplosionUtil.java new file mode 100644 index 00000000..3cd37b84 --- /dev/null +++ b/src/main/java/com/minelittlepony/unicopia/util/ExplosionUtil.java @@ -0,0 +1,16 @@ +package com.minelittlepony.unicopia.util; + +import net.minecraft.block.BlockState; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.BlockView; +import net.minecraft.world.explosion.Explosion; +import net.minecraft.world.explosion.ExplosionBehavior; + +public interface ExplosionUtil { + ExplosionBehavior NON_DESTRUCTIVE = new ExplosionBehavior(){ + @Override + public boolean canDestroyBlock(Explosion explosion, BlockView world, BlockPos pos, BlockState state, float power) { + return false; + } + }; +} diff --git a/src/main/resources/assets/unicopia/lang/en_us.json b/src/main/resources/assets/unicopia/lang/en_us.json index 65883e5c..88bf1146 100644 --- a/src/main/resources/assets/unicopia/lang/en_us.json +++ b/src/main/resources/assets/unicopia/lang/en_us.json @@ -406,6 +406,7 @@ "ability.unicopia.grow": "Nourish Earth", "ability.unicopia.stomp": "Ground Pound", "ability.unicopia.kick": "Crushing Blow", + "ability.unicopia.hug": "Hug", "ability.unicopia.pummel": "Devestating Smash", "ability.unicopia.carry": "Pickup/Drop Passenger", "ability.unicopia.toggle_flight": "Take-off/Land", diff --git a/src/main/resources/assets/unicopia/textures/entity/creeper/friendly.png b/src/main/resources/assets/unicopia/textures/entity/creeper/friendly.png new file mode 100644 index 00000000..8763f99b Binary files /dev/null and b/src/main/resources/assets/unicopia/textures/entity/creeper/friendly.png differ diff --git a/src/main/resources/assets/unicopia/textures/gui/ability/hug.png b/src/main/resources/assets/unicopia/textures/gui/ability/hug.png new file mode 100644 index 00000000..75041d45 Binary files /dev/null and b/src/main/resources/assets/unicopia/textures/gui/ability/hug.png differ