mirror of
https://github.com/Sollace/Unicopia.git
synced 2024-11-28 15:38:00 +01:00
542 lines
21 KiB
Java
542 lines
21 KiB
Java
package com.minelittlepony.unicopia.entity;
|
|
|
|
import java.util.*;
|
|
import java.util.stream.Stream;
|
|
import java.util.stream.StreamSupport;
|
|
|
|
import org.jetbrains.annotations.Nullable;
|
|
|
|
import com.minelittlepony.unicopia.USounds;
|
|
import com.minelittlepony.unicopia.UTags;
|
|
import com.minelittlepony.unicopia.Unicopia;
|
|
import com.minelittlepony.unicopia.ability.Abilities;
|
|
import com.minelittlepony.unicopia.ability.Ability;
|
|
import com.minelittlepony.unicopia.ability.magic.Caster;
|
|
import com.minelittlepony.unicopia.ability.magic.SpellContainer;
|
|
import com.minelittlepony.unicopia.ability.magic.SpellPredicate;
|
|
import com.minelittlepony.unicopia.ability.magic.spell.AbstractDisguiseSpell;
|
|
import com.minelittlepony.unicopia.ability.magic.spell.Situation;
|
|
import com.minelittlepony.unicopia.advancement.UCriteria;
|
|
import com.minelittlepony.unicopia.compat.trinkets.TrinketsDelegate;
|
|
import com.minelittlepony.unicopia.entity.behaviour.EntityAppearance;
|
|
import com.minelittlepony.unicopia.entity.behaviour.Guest;
|
|
import com.minelittlepony.unicopia.entity.damage.MagicalDamageSource;
|
|
import com.minelittlepony.unicopia.entity.duck.LivingEntityDuck;
|
|
import com.minelittlepony.unicopia.entity.effect.CorruptInfluenceStatusEffect;
|
|
import com.minelittlepony.unicopia.entity.effect.EffectUtils;
|
|
import com.minelittlepony.unicopia.entity.effect.UEffects;
|
|
import com.minelittlepony.unicopia.entity.player.Pony;
|
|
import com.minelittlepony.unicopia.input.Heuristic;
|
|
import com.minelittlepony.unicopia.input.Interactable;
|
|
import com.minelittlepony.unicopia.item.GlassesItem;
|
|
import com.minelittlepony.unicopia.item.UItems;
|
|
import com.minelittlepony.unicopia.item.enchantment.UEnchantments;
|
|
import com.minelittlepony.unicopia.network.datasync.EffectSync;
|
|
import com.minelittlepony.unicopia.network.datasync.Transmittable;
|
|
import com.minelittlepony.unicopia.particle.ParticleUtils;
|
|
import com.minelittlepony.unicopia.projectile.ProjectileImpactListener;
|
|
import com.minelittlepony.unicopia.server.world.DragonBreathStore;
|
|
import com.minelittlepony.unicopia.util.*;
|
|
|
|
import it.unimi.dsi.fastutil.floats.Float2ObjectFunction;
|
|
import net.fabricmc.fabric.api.util.TriState;
|
|
import net.minecraft.block.BlockState;
|
|
import net.minecraft.enchantment.Enchantment;
|
|
import net.minecraft.enchantment.EnchantmentHelper;
|
|
import net.minecraft.entity.*;
|
|
import net.minecraft.entity.attribute.EntityAttribute;
|
|
import net.minecraft.entity.attribute.EntityAttributeInstance;
|
|
import net.minecraft.entity.attribute.EntityAttributeModifier;
|
|
import net.minecraft.entity.damage.DamageSource;
|
|
import net.minecraft.entity.damage.DamageTypes;
|
|
import net.minecraft.entity.data.*;
|
|
import net.minecraft.entity.mob.HostileEntity;
|
|
import net.minecraft.entity.player.PlayerEntity;
|
|
import net.minecraft.entity.projectile.ProjectileEntity;
|
|
import net.minecraft.item.BlockItem;
|
|
import net.minecraft.item.Item;
|
|
import net.minecraft.item.ItemPlacementContext;
|
|
import net.minecraft.item.ItemStack;
|
|
import net.minecraft.nbt.NbtCompound;
|
|
import net.minecraft.network.packet.s2c.play.EntityPassengersSetS2CPacket;
|
|
import net.minecraft.particle.ParticleTypes;
|
|
import net.minecraft.registry.tag.DamageTypeTags;
|
|
import net.minecraft.server.world.ServerWorld;
|
|
import net.minecraft.sound.BlockSoundGroup;
|
|
import net.minecraft.sound.SoundCategory;
|
|
import net.minecraft.util.Hand;
|
|
import net.minecraft.util.hit.BlockHitResult;
|
|
import net.minecraft.util.math.BlockPos;
|
|
import net.minecraft.util.math.Direction;
|
|
import net.minecraft.util.math.MathHelper;
|
|
import net.minecraft.util.math.Vec3d;
|
|
|
|
public abstract class Living<T extends LivingEntity> implements Equine<T>, Caster<T>, Transmittable {
|
|
private static final TrackedData<Optional<UUID>> CARRIER_ID = DataTracker.registerData(LivingEntity.class, TrackedDataHandlerRegistry.OPTIONAL_UUID);
|
|
|
|
protected final T entity;
|
|
|
|
private final EffectSync effectDelegate;
|
|
|
|
private final Interactable sneakingHeuristic;
|
|
private final Interactable landedHeuristic;
|
|
private final Interactable jumpingHeuristic;
|
|
|
|
private boolean invisible = false;
|
|
|
|
@Nullable
|
|
private Caster<?> attacker;
|
|
@Nullable
|
|
private transient Caster<?> host;
|
|
|
|
private Optional<Living<?>> target = Optional.empty();
|
|
|
|
private int invinsibilityTicks;
|
|
|
|
private final List<Tickable> tickers = new ArrayList<>();
|
|
|
|
private final LandingEventHandler landEvent = addTicker(new LandingEventHandler(this));
|
|
private final Enchantments enchants = addTicker(new Enchantments(this));
|
|
private final ItemTracker armour = addTicker(new ItemTracker(this));
|
|
private final Transportation<T> transportation = new Transportation<>(this);
|
|
|
|
protected Living(T entity, TrackedData<NbtCompound> effect) {
|
|
this.entity = entity;
|
|
this.effectDelegate = new EffectSync(this, effect);
|
|
this.sneakingHeuristic = addTicker(new Interactable(entity::isSneaking));
|
|
this.landedHeuristic = addTicker(new Interactable(entity::isOnGround));
|
|
this.jumpingHeuristic = addTicker(new Interactable(((LivingEntityDuck)entity)::isJumping));
|
|
}
|
|
|
|
@Override
|
|
public void initDataTracker() {
|
|
effectDelegate.initDataTracker();
|
|
entity.getDataTracker().startTracking(Creature.GRAVITY, 1F);
|
|
entity.getDataTracker().startTracking(CARRIER_ID, Optional.empty());
|
|
}
|
|
|
|
public <Q extends Tickable> Q addTicker(Q tickable) {
|
|
tickers.add(Objects.requireNonNull(tickable, "tickable cannot be null"));
|
|
return tickable;
|
|
}
|
|
|
|
public boolean isInvisible() {
|
|
return invisible && SpellPredicate.IS_DISGUISE.isOn(this);
|
|
}
|
|
|
|
public void setInvisible(boolean invisible) {
|
|
this.invisible = invisible;
|
|
}
|
|
|
|
public void waitForFall(LandingEventHandler.Callback callback) {
|
|
landEvent.setCallback(callback);
|
|
}
|
|
|
|
public boolean sneakingChanged() {
|
|
return sneakingHeuristic.hasChanged(Heuristic.ONCE);
|
|
}
|
|
|
|
public boolean landedChanged() {
|
|
return landedHeuristic.hasChanged(Heuristic.ONCE);
|
|
}
|
|
|
|
public Interactable getJumpingHeuristic() {
|
|
return jumpingHeuristic;
|
|
}
|
|
|
|
@Override
|
|
public SpellContainer getSpellSlot() {
|
|
return effectDelegate;
|
|
}
|
|
|
|
public Enchantments getEnchants() {
|
|
return enchants;
|
|
}
|
|
|
|
public ItemTracker getArmour() {
|
|
return armour;
|
|
}
|
|
|
|
public Transportation<T> getTransportation() {
|
|
return transportation;
|
|
}
|
|
|
|
@Override
|
|
public final T asEntity() {
|
|
return entity;
|
|
}
|
|
|
|
public Optional<UUID> getCarrierId() {
|
|
return entity.getDataTracker().get(CARRIER_ID);
|
|
}
|
|
|
|
public void setCarrier(UUID carrier) {
|
|
entity.getDataTracker().set(CARRIER_ID, Optional.ofNullable(carrier));
|
|
}
|
|
|
|
public void setCarrier(Entity carrier) {
|
|
entity.getDataTracker().set(CARRIER_ID, Optional.ofNullable(carrier).map(Entity::getUuid));
|
|
}
|
|
|
|
@Nullable
|
|
public Optional<Living<?>> getTarget() {
|
|
return target;
|
|
}
|
|
|
|
public void setTarget(Living<?> target) {
|
|
this.target = Optional.ofNullable(target);
|
|
}
|
|
|
|
public boolean isBeingCarried() {
|
|
Entity vehicle = entity.getVehicle();
|
|
return vehicle != null && getCarrierId().filter(vehicle.getUuid()::equals).isPresent();
|
|
}
|
|
|
|
@Override
|
|
public boolean hasFeatherTouch() {
|
|
return EnchantmentHelper.getEquipmentLevel(UEnchantments.FEATHER_TOUCH, entity) > 0;
|
|
}
|
|
|
|
@Override
|
|
public boolean beforeUpdate() {
|
|
landEvent.beforeTick();
|
|
if (EffectUtils.getAmplifier(entity, UEffects.PARALYSIS) > 1 && entity.getVelocity().horizontalLengthSquared() > 0) {
|
|
entity.setVelocity(entity.getVelocity().multiply(0, 1, 0));
|
|
updateVelocity();
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
@Override
|
|
public void tick() {
|
|
tickers.forEach(Tickable::tick);
|
|
effectDelegate.tick(Situation.BODY);
|
|
|
|
if (!(entity instanceof PlayerEntity)) {
|
|
if (!entity.hasVehicle() && getCarrierId().isPresent() && !asWorld().isClient && entity.age % 10 == 0) {
|
|
UUID carrierId = getCarrierId().get();
|
|
Entity carrier = ((ServerWorld)asWorld()).getEntity(carrierId);
|
|
if (carrier != null) {
|
|
asEntity().startRiding(carrier, true);
|
|
Living.transmitPassengers(carrier);
|
|
} else {
|
|
Unicopia.LOGGER.warn("No passenger with id {]", carrierId);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (invinsibilityTicks > 0) {
|
|
invinsibilityTicks--;
|
|
}
|
|
|
|
if (entity.hasStatusEffect(UEffects.PARALYSIS) && entity.getVelocity().horizontalLengthSquared() > 0) {
|
|
entity.setVelocity(entity.getVelocity().multiply(0, 1, 0));
|
|
updateVelocity();
|
|
}
|
|
|
|
if (isBeingCarried()) {
|
|
Pony carrier = Pony.of(entity.getVehicle()).orElse(null);
|
|
if (!Abilities.CARRY.canUse(carrier.getCompositeRace()) && !Abilities.HUG.canUse(carrier.getCompositeRace())) {
|
|
entity.stopRiding();
|
|
entity.refreshPositionAfterTeleport(carrier.getOriginVector());
|
|
Living.transmitPassengers(carrier.asEntity());
|
|
}
|
|
entity.setYaw(carrier.asEntity().getYaw());
|
|
}
|
|
|
|
updateDragonBreath();
|
|
|
|
transportation.tick();
|
|
}
|
|
|
|
public void updateAttributeModifier(UUID id, EntityAttribute attribute, float desiredValue, Float2ObjectFunction<EntityAttributeModifier> modifierSupplier, boolean permanent) {
|
|
@Nullable
|
|
EntityAttributeInstance instance = asEntity().getAttributeInstance(attribute);
|
|
if (instance == null) {
|
|
return;
|
|
}
|
|
|
|
@Nullable
|
|
EntityAttributeModifier modifier = instance.getModifier(id);
|
|
|
|
if (!MathHelper.approximatelyEquals(desiredValue, modifier == null ? 0 : modifier.getValue())) {
|
|
if (modifier != null) {
|
|
instance.removeModifier(modifier);
|
|
}
|
|
|
|
if (desiredValue != 0) {
|
|
if (permanent) {
|
|
instance.addPersistentModifier(modifierSupplier.get(desiredValue));
|
|
} else {
|
|
instance.addTemporaryModifier(modifierSupplier.get(desiredValue));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public boolean canBeSeenBy(Entity entity) {
|
|
return !isInvisible()
|
|
&& getSpellSlot()
|
|
.get(SpellPredicate.IS_DISGUISE, true)
|
|
.filter(spell -> spell.getDisguise().getAppearance() == entity)
|
|
.isEmpty();
|
|
}
|
|
|
|
public Optional<Vec3d> adjustMovementSpeedInWater(Vec3d speed) {
|
|
return Optional.empty();
|
|
}
|
|
|
|
private void updateDragonBreath() {
|
|
if (!entity.getWorld().isClient && (entity instanceof PlayerEntity || entity.hasCustomName())) {
|
|
|
|
Vec3d targetPos = entity.getRotationVector().multiply(2).add(entity.getEyePos());
|
|
|
|
if (entity.getWorld().isAir(BlockPos.ofFloored(targetPos))) {
|
|
DragonBreathStore store = DragonBreathStore.get(entity.getWorld());
|
|
String name = entity.getDisplayName().getString();
|
|
store.popEntries(name).forEach(stack -> {
|
|
ItemStack payload = stack.payload();
|
|
Item item = payload.getItem();
|
|
|
|
boolean deliverAggressively = payload.isIn(UTags.Items.IS_DELIVERED_AGGRESSIVELY);
|
|
|
|
Vec3d randomPos = deliverAggressively ? targetPos.add(0, 2, 0) : targetPos.add(VecHelper.supply(() -> entity.getRandom().nextTriangular(0.1, 0.5)));
|
|
|
|
if (deliverAggressively && item instanceof BlockItem blockItem) {
|
|
do {
|
|
ItemStack instance = payload.split(1);
|
|
BlockPos pos = BlockPos.ofFloored(randomPos);
|
|
if (!entity.getWorld().isAir(pos)) {
|
|
store.put(name, instance);
|
|
} else {
|
|
|
|
for (int i = 0; i < 10; i++) {
|
|
ParticleUtils.spawnParticle(entity.getWorld(), ParticleTypes.FLAME, randomPos.add(
|
|
VecHelper.supply(() -> entity.getRandom().nextTriangular(0.1, 0.5))
|
|
), Vec3d.ZERO);
|
|
}
|
|
|
|
ItemPlacementContext context = new ItemPlacementContext(entity.getWorld(), (PlayerEntity)null, Hand.MAIN_HAND, instance,
|
|
BlockHitResult.createMissed(Vec3d.ZERO, Direction.UP, pos)
|
|
);
|
|
|
|
BlockState state = blockItem.getBlock().getPlacementState(context);
|
|
if (state == null) {
|
|
state = blockItem.getBlock().getDefaultState();
|
|
}
|
|
|
|
entity.getWorld().setBlockState(pos, state);
|
|
BlockSoundGroup sound = state.getSoundGroup();
|
|
entity.getWorld().playSound(null, pos, sound.getPlaceSound(), SoundCategory.BLOCKS, (sound.getVolume() + 1) * 0.5F, sound.getPitch() * 0.8F);
|
|
}
|
|
randomPos = targetPos.add(VecHelper.supply(() -> entity.getRandom().nextTriangular(0.1, 0.5)));
|
|
} while (!payload.isEmpty());
|
|
} else {
|
|
if (!entity.getWorld().isAir(BlockPos.ofFloored(randomPos))) {
|
|
store.put(name, stack.payload());
|
|
} else {
|
|
for (int i = 0; i < 10; i++) {
|
|
ParticleUtils.spawnParticle(entity.getWorld(), ParticleTypes.FLAME, randomPos.add(
|
|
VecHelper.supply(() -> entity.getRandom().nextTriangular(0.1, 0.5))
|
|
), Vec3d.ZERO);
|
|
}
|
|
|
|
ItemEntity itemEntity = EntityType.ITEM.create(entity.getWorld());
|
|
itemEntity.setStack(payload);
|
|
itemEntity.setPosition(randomPos);
|
|
itemEntity.getWorld().spawnEntity(itemEntity);
|
|
entity.getWorld().playSoundFromEntity(null, entity, USounds.ITEM_DRAGON_BREATH_ARRIVE, entity.getSoundCategory(), 1, 1);
|
|
UCriteria.SEND_DRAGON_BREATH.triggerReceived(entity, payload.copy());
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
public boolean onUpdatePassengerPosition(Entity passender, Entity.PositionUpdater positionUpdater) {
|
|
return false;
|
|
}
|
|
|
|
@Nullable
|
|
public final Caster<?> getAttacker() {
|
|
return attacker;
|
|
}
|
|
|
|
public Optional<Boolean> onDamage(DamageSource source, float amount) {
|
|
|
|
if (Guest.of(source.getAttacker()).hostIs(this)
|
|
|| Guest.of(source.getSource()).hostIs(this)) {
|
|
var type = source.getTypeRegistryEntry();
|
|
return Optional.of(entity.damage(
|
|
type.matchesKey(DamageTypes.FIREBALL) ? entity.getDamageSources().create(DamageTypes.UNATTRIBUTED_FIREBALL) :
|
|
type.matchesKey(DamageTypes.PLAYER_EXPLOSION) ? entity.getDamageSources().create(DamageTypes.EXPLOSION) :
|
|
new DamageSource(type, entity, entity), amount));
|
|
}
|
|
|
|
if (Guest.of(entity).getHost() instanceof Living l) {
|
|
l.asEntity().damage(source, amount);
|
|
}
|
|
|
|
if (source.isIn(DamageTypeTags.IS_LIGHTNING) && (invinsibilityTicks > 0 || tryCaptureLightning())) {
|
|
return Optional.of(false);
|
|
}
|
|
|
|
if (source instanceof MagicalDamageSource magical) {
|
|
Caster<?> attacker = ((MagicalDamageSource)source).getSpell();
|
|
if (attacker != null) {
|
|
this.attacker = attacker;
|
|
}
|
|
|
|
if (magical.isIn(UTags.DamageTypes.BREAKS_SUNGLASSES)) {
|
|
ItemStack glasses = GlassesItem.getForEntity(entity);
|
|
if (glasses.getItem() == UItems.SUNGLASSES) {
|
|
ItemStack broken = UItems.BROKEN_SUNGLASSES.getDefaultStack();
|
|
broken.setNbt(glasses.getNbt());
|
|
TrinketsDelegate.getInstance(entity).setEquippedStack(entity, TrinketsDelegate.FACE, broken);
|
|
playSound(USounds.ITEM_SUNGLASSES_SHATTER, 1, 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (entity instanceof HostileEntity mob && mob.hasStatusEffect(UEffects.CORRUPT_INFLUENCE) && mob.getRandom().nextInt(4) == 0) {
|
|
CorruptInfluenceStatusEffect.reproduce(mob);
|
|
}
|
|
|
|
return Optional.empty();
|
|
}
|
|
|
|
public TriState canBeHurtByWater() {
|
|
return TriState.DEFAULT;
|
|
}
|
|
|
|
public Optional<BlockPos> chooseClimbingPos() {
|
|
return getSpellSlot().get(SpellPredicate.IS_DISGUISE, false)
|
|
.map(AbstractDisguiseSpell::getDisguise)
|
|
.filter(EntityAppearance::canClimbWalls)
|
|
.map(v -> entity.getBlockPos());
|
|
}
|
|
|
|
private boolean tryCaptureLightning() {
|
|
return getInventoryStacks().filter(stack -> !stack.isEmpty() && stack.getItem() == UItems.EMPTY_JAR).findFirst().map(stack -> {
|
|
invinsibilityTicks = 20;
|
|
stack.split(1);
|
|
giveBackItem(UItems.LIGHTNING_JAR.getDefaultStack());
|
|
return stack;
|
|
}).isPresent();
|
|
}
|
|
|
|
protected Stream<ItemStack> getInventoryStacks() {
|
|
return Stream.of(entity.getStackInHand(Hand.MAIN_HAND), entity.getStackInHand(Hand.OFF_HAND));
|
|
}
|
|
|
|
protected Stream<ItemStack> getArmourStacks() {
|
|
if (!TrinketsDelegate.hasTrinkets()) {
|
|
return StreamSupport.stream(entity.getArmorItems().spliterator(), false);
|
|
}
|
|
return Stream.concat(
|
|
TrinketsDelegate.getInstance(entity).getEquipped(entity, TrinketsDelegate.NECKLACE),
|
|
StreamSupport.stream(entity.getArmorItems().spliterator(), false)
|
|
);
|
|
}
|
|
|
|
protected void giveBackItem(ItemStack stack) {
|
|
entity.dropStack(stack);
|
|
}
|
|
|
|
@Override
|
|
public boolean onProjectileImpact(ProjectileEntity projectile) {
|
|
return getSpellSlot().get(true)
|
|
.filter(effect -> !effect.isDead()
|
|
&& effect instanceof ProjectileImpactListener
|
|
&& ((ProjectileImpactListener)effect).onProjectileImpact(projectile))
|
|
.isPresent();
|
|
}
|
|
|
|
public float onImpact(float distance, float damageMultiplier, DamageSource cause) {
|
|
float fallDistance = landEvent.fire(getEffectiveFallDistance(distance));
|
|
|
|
getSpellSlot().get(SpellPredicate.IS_DISGUISE, false).ifPresent(spell -> {
|
|
spell.getDisguise().onImpact(this, fallDistance, damageMultiplier, cause);
|
|
});
|
|
return fallDistance;
|
|
}
|
|
|
|
protected float getEffectiveFallDistance(float distance) {
|
|
return distance;
|
|
}
|
|
|
|
@Override
|
|
public float getCloudWalkingStrength() {
|
|
Enchantment featherFalling = net.minecraft.enchantment.Enchantments.FEATHER_FALLING;
|
|
int maxLevel = featherFalling.getMaxLevel();
|
|
int level = EnchantmentHelper.getEquipmentLevel(featherFalling, entity);
|
|
return MathHelper.clamp(level / (float)maxLevel, 0, 1);
|
|
}
|
|
|
|
@Override
|
|
public void setDirty() {}
|
|
|
|
@Override
|
|
public void toNBT(NbtCompound compound) {
|
|
enchants.toNBT(compound);
|
|
effectDelegate.toNBT(compound);
|
|
toSyncronisedNbt(compound);
|
|
}
|
|
|
|
@Override
|
|
public void fromNBT(NbtCompound compound) {
|
|
enchants.fromNBT(compound);
|
|
effectDelegate.fromNBT(compound);
|
|
fromSynchronizedNbt(compound);
|
|
}
|
|
|
|
@Override
|
|
public void toSyncronisedNbt(NbtCompound compound) {
|
|
compound.put("armour", armour.toNBT());
|
|
getCarrierId().ifPresent(id -> compound.putUuid("carrier", id));
|
|
}
|
|
|
|
@Override
|
|
public void fromSynchronizedNbt(NbtCompound compound) {
|
|
armour.fromNBT(compound.getCompound("armour"));
|
|
setCarrier(compound.containsUuid("carrier") ? compound.getUuid("carrier") : null);
|
|
}
|
|
|
|
public void updateVelocity() {
|
|
updateVelocity(entity);
|
|
}
|
|
|
|
@Override
|
|
public boolean canUse(Ability<?> ability) {
|
|
return ability.canUse(getCompositeRace());
|
|
}
|
|
|
|
public static Optional<Living<?>> getOrEmpty(Entity entity) {
|
|
return Equine.of(entity, a -> a instanceof Living);
|
|
}
|
|
|
|
public static Living<?> living(Entity entity) {
|
|
return getOrEmpty(entity).orElse(null);
|
|
}
|
|
|
|
public static <E extends LivingEntity> Living<E> living(E entity) {
|
|
return Equine.<E, Living<E>>of(entity, e -> e instanceof Living<?>).orElse(null);
|
|
}
|
|
|
|
public static void updateVelocity(@Nullable Entity entity) {
|
|
if (entity != null) {
|
|
entity.velocityModified = true;
|
|
//if (entity instanceof ServerPlayerEntity ply) {
|
|
// ply.networkHandler.sendPacket(new EntityVelocityUpdateS2CPacket(ply));
|
|
//}
|
|
}
|
|
}
|
|
|
|
public static void transmitPassengers(@Nullable Entity entity) {
|
|
if (entity != null && entity.getWorld() instanceof ServerWorld sw) {
|
|
sw.getChunkManager().sendToNearbyPlayers(entity, new EntityPassengersSetS2CPacket(entity));
|
|
}
|
|
}
|
|
}
|