diff --git a/src/main/java/com/minelittlepony/unicopia/ability/ChangelingFeedAbility.java b/src/main/java/com/minelittlepony/unicopia/ability/ChangelingFeedAbility.java index b491bed3..7a4e1f76 100644 --- a/src/main/java/com/minelittlepony/unicopia/ability/ChangelingFeedAbility.java +++ b/src/main/java/com/minelittlepony/unicopia/ability/ChangelingFeedAbility.java @@ -2,26 +2,22 @@ package com.minelittlepony.unicopia.ability; import java.util.List; import java.util.Optional; -import java.util.stream.Collectors; +import java.util.function.Predicate; +import java.util.stream.Stream; import org.jetbrains.annotations.Nullable; import com.minelittlepony.unicopia.Race; import com.minelittlepony.unicopia.USounds; import com.minelittlepony.unicopia.ability.data.Hit; -import com.minelittlepony.unicopia.entity.damage.UDamageTypes; +import com.minelittlepony.unicopia.ability.magic.spell.ChangelingFeedingSpell; +import com.minelittlepony.unicopia.ability.magic.spell.effect.SpellType; import com.minelittlepony.unicopia.entity.player.Pony; -import com.minelittlepony.unicopia.particle.FollowingParticleEffect; -import com.minelittlepony.unicopia.particle.ParticleUtils; -import com.minelittlepony.unicopia.particle.UParticles; import com.minelittlepony.unicopia.util.TraceHelper; import com.minelittlepony.unicopia.util.VecHelper; import net.minecraft.entity.Entity; import net.minecraft.entity.LivingEntity; -import net.minecraft.entity.damage.DamageSource; -import net.minecraft.entity.effect.StatusEffectInstance; -import net.minecraft.entity.effect.StatusEffects; import net.minecraft.entity.mob.HostileEntity; import net.minecraft.entity.passive.CowEntity; import net.minecraft.entity.passive.MerchantEntity; @@ -34,6 +30,13 @@ import net.minecraft.particle.ParticleTypes; * Changeling ability to restore health from mobs */ public class ChangelingFeedAbility implements Ability { + private static final Predicate TARGET_PREDICATE = e -> (e instanceof LivingEntity) + && (e instanceof CowEntity + || e instanceof MerchantEntity + || e instanceof PlayerEntity + || e instanceof SheepEntity + || e instanceof PigEntity + || e instanceof HostileEntity); @Override public int getWarmupTime(Pony player) { @@ -42,7 +45,7 @@ public class ChangelingFeedAbility implements Ability { @Override public int getCooldownTime(Pony player) { - return canFeed(player) ? 15 : 80; + return !SpellType.FEED.isOn(player) && ChangelingFeedingSpell.canFeed(player) ? 15 : 80; } @Override @@ -53,22 +56,7 @@ public class ChangelingFeedAbility implements Ability { @Nullable @Override public Optional prepare(Pony player) { - return Hit.of(canFeed(player) && !getTargets(player).isEmpty()); - } - - private boolean canFeed(Pony player) { - return player.asEntity().getHealth() < player.asEntity().getMaxHealth() - || player.asEntity().canConsume(false); - } - - private boolean canDrain(Entity e) { - return (e instanceof LivingEntity) - && (e instanceof CowEntity - || e instanceof MerchantEntity - || e instanceof PlayerEntity - || e instanceof SheepEntity - || e instanceof PigEntity - || e instanceof HostileEntity); + return Hit.of(ChangelingFeedingSpell.canFeed(player) && !getTargets(player).findAny().isEmpty()); } @Override @@ -76,16 +64,6 @@ public class ChangelingFeedAbility implements Ability { return Hit.SERIALIZER; } - protected List getTargets(Pony player) { - List list = VecHelper.findInRange(player.asEntity(), player.asWorld(), player.getOriginVector(), 3, this::canDrain); - - TraceHelper.findEntity(player.asEntity(), 17, 1, - looked -> looked instanceof LivingEntity && !list.contains(looked) && canDrain(looked)) - .ifPresent(list::add); - - return list.stream().map(i -> (LivingEntity)i).collect(Collectors.toList()); - } - @Override public double getCostEstimate(Pony player) { return 0; @@ -93,7 +71,7 @@ public class ChangelingFeedAbility implements Ability { @Override public boolean apply(Pony iplayer, Hit data) { - if (!canFeed(iplayer)) { + if (!ChangelingFeedingSpell.canFeed(iplayer)) { return false; } @@ -103,64 +81,25 @@ public class ChangelingFeedAbility implements Ability { int maximumFoodGain = player.canConsume(false) ? (20 - player.getHungerManager().getFoodLevel()) : 0; if (maximumHealthGain > 0 || maximumFoodGain > 0) { + List targets = getTargets(iplayer).map(LivingEntity.class::cast).toList(); - float healAmount = 0; + if (targets.size() > 0) { + new ChangelingFeedingSpell(targets, maximumHealthGain, maximumFoodGain).apply(iplayer); - for (LivingEntity i : getTargets(iplayer)) { - healAmount += drainFrom(iplayer, i); + iplayer.playSound(USounds.ENTITY_PLAYER_CHANGELING_FEED, 0.1F, iplayer.getRandomPitch()); + return true; } - - int foodAmount = (int)Math.floor(Math.min(healAmount / 3, maximumFoodGain)); - - if (foodAmount > 0) { - healAmount -= foodAmount; - } - player.getHungerManager().add(Math.max(1, foodAmount), 0.125f); - - player.heal(Math.max(1, Math.min(healAmount, maximumHealthGain))); - } - - - if (!canFeed(iplayer)) { - iplayer.playSound(USounds.Vanilla.ENTITY_PLAYER_BURP, 1, (float)player.getWorld().random.nextTriangular(1F, 0.2F)); - } else { - iplayer.playSound(USounds.ENTITY_PLAYER_CHANGELING_FEED, 0.1F, iplayer.getRandomPitch()); } + iplayer.playSound(USounds.Vanilla.ENTITY_PLAYER_BURP, 1, (float)player.getWorld().random.nextTriangular(1F, 0.2F)); return true; } - public float drainFrom(Pony changeling, LivingEntity living) { - - DamageSource d = changeling.damageOf(UDamageTypes.LOVE_DRAINING, changeling); - - float damage = living.getHealth()/2; - - if (damage > 0) { - living.damage(d, damage); - } - - ParticleUtils.spawnParticles(UParticles.CHANGELING_MAGIC, living, 7); - ParticleUtils.spawnParticles(new FollowingParticleEffect(UParticles.HEALTH_DRAIN, changeling.asEntity(), 0.2F), living, 1); - - if (changeling.asEntity().hasStatusEffect(StatusEffects.NAUSEA)) { - StatusEffectInstance effect = changeling.asEntity().getStatusEffect(StatusEffects.NAUSEA); - changeling.asEntity().removeStatusEffect(StatusEffects.NAUSEA); - living.addStatusEffect(effect); - } else if (changeling.asWorld().random.nextInt(2300) == 0) { - living.addStatusEffect(new StatusEffectInstance(StatusEffects.WITHER, 20, 1)); - } - - if (living instanceof PlayerEntity) { - damage ++; - damage *= 1.6F; - - if (!changeling.asEntity().hasStatusEffect(StatusEffects.HEALTH_BOOST)) { - changeling.asEntity().addStatusEffect(new StatusEffectInstance(StatusEffects.HEALTH_BOOST, 13000, 1)); - } - } - - return damage; + protected Stream getTargets(Pony player) { + return Stream.concat( + VecHelper.findInRange(player.asEntity(), player.asWorld(), player.getOriginVector(), 3, TARGET_PREDICATE).stream(), + TraceHelper.findEntity(player.asEntity(), 17, 1, TARGET_PREDICATE).stream() + ).distinct(); } @Override diff --git a/src/main/java/com/minelittlepony/unicopia/ability/magic/spell/ChangelingFeedingSpell.java b/src/main/java/com/minelittlepony/unicopia/ability/magic/spell/ChangelingFeedingSpell.java new file mode 100644 index 00000000..1bae80a1 --- /dev/null +++ b/src/main/java/com/minelittlepony/unicopia/ability/magic/spell/ChangelingFeedingSpell.java @@ -0,0 +1,162 @@ +package com.minelittlepony.unicopia.ability.magic.spell; + +import java.util.List; +import java.util.stream.Collectors; + +import com.minelittlepony.unicopia.USounds; +import com.minelittlepony.unicopia.ability.Abilities; +import com.minelittlepony.unicopia.ability.magic.Caster; +import com.minelittlepony.unicopia.ability.magic.spell.effect.AbstractSpell; +import com.minelittlepony.unicopia.ability.magic.spell.effect.CustomisedSpellType; +import com.minelittlepony.unicopia.ability.magic.spell.effect.SpellType; +import com.minelittlepony.unicopia.entity.EntityReference; +import com.minelittlepony.unicopia.entity.damage.UDamageTypes; +import com.minelittlepony.unicopia.entity.player.Pony; +import com.minelittlepony.unicopia.particle.FollowingParticleEffect; +import com.minelittlepony.unicopia.particle.ParticleUtils; +import com.minelittlepony.unicopia.particle.UParticles; + +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.damage.DamageSource; +import net.minecraft.entity.effect.StatusEffectInstance; +import net.minecraft.entity.effect.StatusEffects; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.nbt.NbtElement; +import net.minecraft.util.math.MathHelper; + +public class ChangelingFeedingSpell extends AbstractSpell { + private List> targets = List.of(); + private int nextTargetIndex; + + private float healthToDrain; + private int foodToDrain; + + private float damageThisTick; + + public ChangelingFeedingSpell(CustomisedSpellType type) { + super(type); + setHidden(true); + } + + public ChangelingFeedingSpell(List feedTarget, float healthToDrain, int foodToDrain) { + this(SpellType.FEED.withTraits()); + this.targets = feedTarget.stream().map(EntityReference::new).collect(Collectors.toList() /* make mutable */); + this.healthToDrain = healthToDrain; + this.foodToDrain = foodToDrain; + } + + @Override + public boolean tick(Caster source, Situation situation) { + if (!(source instanceof Pony changeling) || situation != Situation.BODY || !source.canUse(Abilities.FEED)) { + return false; + } + + PlayerEntity player = changeling.asEntity(); + if (!canFeed(changeling)) { + changeling.playSound(USounds.Vanilla.ENTITY_PLAYER_BURP, 1, (float)player.getWorld().random.nextTriangular(1F, 0.2F)); + return false; + } + + float tickDrain = Math.min(0.05F, healthToDrain); + damageThisTick += tickDrain; + + if (damageThisTick > 1) { + damageThisTick--; + + float healAmount = drain(changeling, 1); + float foodAmount = Math.min(healAmount / 3F, foodToDrain); + if (foodAmount > 0) { + healAmount -= foodAmount; + } + + foodAmount = MathHelper.clamp(foodAmount, 0, foodToDrain); + healAmount = MathHelper.clamp(healAmount, 0, healthToDrain); + + int shanks = MathHelper.floor(foodAmount); + player.getHungerManager().add(shanks, foodAmount - shanks); + player.heal(healAmount); + + if (!canFeed(changeling)) { + changeling.playSound(USounds.Vanilla.ENTITY_PLAYER_BURP, 1, (float)player.getWorld().random.nextTriangular(1F, 0.2F)); + } else { + changeling.playSound(USounds.ENTITY_PLAYER_CHANGELING_FEED, 0.1F, changeling.getRandomPitch()); + } + + foodToDrain -= foodAmount; + healthToDrain -= healAmount; + } + + return !targets.isEmpty() && (healthToDrain > 0 || foodToDrain > 0); + } + + private float drain(Pony changeling, float max) { + List> targets = this.targets; + while (!targets.isEmpty()) { + int index = MathHelper.clamp(nextTargetIndex, 0, targets.size()); + LivingEntity l = targets.get(index).getOrEmpty(changeling.asWorld()).orElse(null); + if (l != null && !l.isRemoved() && l.distanceTo(changeling.asEntity()) < 4) { + nextTargetIndex = (nextTargetIndex + 1) % targets.size(); + return drainFrom(changeling, l, max); + } else { + targets.remove(index); + } + } + return 0; + } + + public float drainFrom(Pony changeling, LivingEntity living, float damage) { + DamageSource d = changeling.damageOf(UDamageTypes.LOVE_DRAINING, changeling); + + if (damage > 0) { + living.damage(d, damage); + } + + ParticleUtils.spawnParticles(UParticles.CHANGELING_MAGIC, living, 7); + ParticleUtils.spawnParticles(new FollowingParticleEffect(UParticles.HEALTH_DRAIN, changeling.asEntity(), 0.2F), living, 1); + + if (changeling.asEntity().hasStatusEffect(StatusEffects.NAUSEA)) { + StatusEffectInstance effect = changeling.asEntity().getStatusEffect(StatusEffects.NAUSEA); + changeling.asEntity().removeStatusEffect(StatusEffects.NAUSEA); + living.addStatusEffect(effect); + } else if (changeling.asWorld().random.nextInt(2300) == 0) { + living.addStatusEffect(new StatusEffectInstance(StatusEffects.WITHER, 20, 1)); + } + + if (living instanceof PlayerEntity) { + damage ++; + damage *= 1.6F; + + if (!changeling.asEntity().hasStatusEffect(StatusEffects.HEALTH_BOOST)) { + changeling.asEntity().addStatusEffect(new StatusEffectInstance(StatusEffects.HEALTH_BOOST, 13000, 1)); + } + } + + return damage; + } + + public static boolean canFeed(Pony player) { + return player.asEntity().getHealth() < player.asEntity().getMaxHealth() + || player.asEntity().canConsume(false); + } + + @Override + public void toNBT(NbtCompound compound) { + super.toNBT(compound); + compound.putFloat("healthToDrain", healthToDrain); + compound.putInt("foodToDrain", foodToDrain); + compound.putFloat("damageThisTick", damageThisTick); + compound.put("targets", EntityReference.getSerializer().writeAll(targets)); + } + + @Override + public void fromNBT(NbtCompound compound) { + super.fromNBT(compound); + healthToDrain = compound.getFloat("healthToDrain"); + foodToDrain = compound.getInt("foodToDrain"); + damageThisTick = compound.getFloat("damageThisTick"); + targets = compound.contains("targets", NbtElement.LIST_TYPE) + ? EntityReference.getSerializer().readAll(compound.getList("targets", NbtElement.COMPOUND_TYPE)).toList() + : List.of(); + } +} diff --git a/src/main/java/com/minelittlepony/unicopia/ability/magic/spell/effect/SpellType.java b/src/main/java/com/minelittlepony/unicopia/ability/magic/spell/effect/SpellType.java index 63c5eddc..90bbed43 100644 --- a/src/main/java/com/minelittlepony/unicopia/ability/magic/spell/effect/SpellType.java +++ b/src/main/java/com/minelittlepony/unicopia/ability/magic/spell/effect/SpellType.java @@ -11,6 +11,7 @@ import com.minelittlepony.unicopia.Affinity; import com.minelittlepony.unicopia.Unicopia; import com.minelittlepony.unicopia.ability.magic.Affine; import com.minelittlepony.unicopia.ability.magic.SpellPredicate; +import com.minelittlepony.unicopia.ability.magic.spell.ChangelingFeedingSpell; import com.minelittlepony.unicopia.ability.magic.spell.DispersableDisguiseSpell; import com.minelittlepony.unicopia.ability.magic.spell.RainboomAbilitySpell; import com.minelittlepony.unicopia.ability.magic.spell.PlaceableSpell; @@ -49,6 +50,7 @@ public final class SpellType implements Affine, SpellPredicate< public static final SpellType THROWN_SPELL = register("thrown", Affinity.NEUTRAL, 0, false, SpellTraits.EMPTY, ThrowableSpell::new); public static final SpellType CHANGELING_DISGUISE = register("disguise", Affinity.BAD, 0x19E48E, false, SpellTraits.EMPTY, DispersableDisguiseSpell::new); + public static final SpellType FEED = register("feed", Affinity.BAD, 0xBDBDF9, false, SpellTraits.EMPTY, ChangelingFeedingSpell::new); public static final SpellType RAINBOOM = register("rainboom", Affinity.GOOD, 0xBDBDF9, false, SpellTraits.EMPTY, RainboomAbilitySpell::new); public static final SpellType RAGE = register("rage", Affinity.GOOD, 0xBDBDF9, false, SpellTraits.EMPTY, RageAbilitySpell::new); public static final SpellType TIME_CONTROL = register("time_control", Affinity.GOOD, 0xBDBDF9, false, SpellTraits.EMPTY, TimeControlAbilitySpell::new); diff --git a/src/main/java/com/minelittlepony/unicopia/entity/EntityReference.java b/src/main/java/com/minelittlepony/unicopia/entity/EntityReference.java index b738bc85..ad6a4d4c 100644 --- a/src/main/java/com/minelittlepony/unicopia/entity/EntityReference.java +++ b/src/main/java/com/minelittlepony/unicopia/entity/EntityReference.java @@ -11,7 +11,6 @@ import org.jetbrains.annotations.Nullable; import com.minelittlepony.unicopia.ability.magic.Caster; import com.minelittlepony.unicopia.ability.magic.Levelled; import com.minelittlepony.unicopia.util.NbtSerialisable; - import net.minecraft.entity.Entity; import net.minecraft.entity.player.PlayerEntity; import net.minecraft.nbt.NbtCompound; @@ -31,6 +30,13 @@ import net.minecraft.world.World; * @param The type of the entity this reference points to. */ public class EntityReference implements NbtSerialisable { + private static final Serializer SERIALIZER = Serializer.of(EntityReference::new); + + @SuppressWarnings("unchecked") + public static Serializer> getSerializer() { + return (Serializer>)SERIALIZER; + } + @Nullable private EntityValues reference; diff --git a/src/main/java/com/minelittlepony/unicopia/item/CrystalHeartItem.java b/src/main/java/com/minelittlepony/unicopia/item/CrystalHeartItem.java index 17a2ebf2..d4f9607c 100644 --- a/src/main/java/com/minelittlepony/unicopia/item/CrystalHeartItem.java +++ b/src/main/java/com/minelittlepony/unicopia/item/CrystalHeartItem.java @@ -128,9 +128,9 @@ public class CrystalHeartItem extends Item implements FloatingArtefactEntity.Art LivingEntity living = (LivingEntity)e; if (e instanceof PlayerEntity - || (living instanceof TameableEntity && ((TameableEntity)living).isTamed()) - || (living instanceof Saddleable && ((Saddleable)living).isSaddled()) - || (living instanceof MerchantEntity)) { + || (e instanceof TameableEntity t && t.isTamed()) + || (e instanceof Saddleable s && s.isSaddled()) + || (e instanceof MerchantEntity)) { if (living.getHealth() < living.getMaxHealth()) { outputs.add(living); } @@ -149,19 +149,8 @@ public class CrystalHeartItem extends Item implements FloatingArtefactEntity.Art return; } - float gives; - float takes; - - if (supply > demand) { - gives = supply / demand; - takes = 1; - } else if (demand > supply) { - takes = demand / supply; - gives = 1; - } else { - gives = 1; - takes = 1; - } + float gives = supply > demand ? supply / demand : 1; + float takes = demand > supply ? demand / supply : 1; inputs.forEach(input -> { input.damage(entity.damageOf(UDamageTypes.LIFE_DRAINING), takes); diff --git a/src/main/java/com/minelittlepony/unicopia/util/NbtSerialisable.java b/src/main/java/com/minelittlepony/unicopia/util/NbtSerialisable.java index a346c460..819153e5 100644 --- a/src/main/java/com/minelittlepony/unicopia/util/NbtSerialisable.java +++ b/src/main/java/com/minelittlepony/unicopia/util/NbtSerialisable.java @@ -97,7 +97,7 @@ public interface NbtSerialisable { return read((NbtCompound)element); } - default NbtList writeAll(Collection ts) { + default NbtList writeAll(Collection ts) { NbtList list = new NbtList(); ts.stream().map(this::write).forEach(list::add); return list;