mirror of
https://github.com/Sollace/Unicopia.git
synced 2025-04-01 08:45:28 +02:00
516 lines
18 KiB
Java
516 lines
18 KiB
Java
package com.minelittlepony.unicopia.entity;
|
|
|
|
import net.minecraft.block.ShapeContext;
|
|
import net.minecraft.entity.*;
|
|
import net.minecraft.entity.data.*;
|
|
import net.minecraft.entity.mob.FlyingEntity;
|
|
import net.minecraft.entity.player.PlayerEntity;
|
|
import net.minecraft.entity.vehicle.BoatEntity;
|
|
import net.minecraft.item.Item;
|
|
import net.minecraft.item.ItemStack;
|
|
import net.minecraft.item.Items;
|
|
import net.minecraft.nbt.NbtCompound;
|
|
import net.minecraft.particle.ParticleTypes;
|
|
import net.minecraft.sound.SoundEvents;
|
|
import net.minecraft.util.ActionResult;
|
|
import net.minecraft.util.Hand;
|
|
import net.minecraft.util.StringIdentifiable;
|
|
import net.minecraft.util.function.ValueLists;
|
|
import net.minecraft.util.math.*;
|
|
import net.minecraft.util.math.random.Random;
|
|
import net.minecraft.util.shape.VoxelShape;
|
|
import net.minecraft.util.shape.VoxelShapes;
|
|
import net.minecraft.world.World;
|
|
import net.minecraft.world.event.GameEvent;
|
|
|
|
import java.util.List;
|
|
import java.util.Locale;
|
|
import java.util.function.Consumer;
|
|
import java.util.function.IntFunction;
|
|
|
|
import com.minelittlepony.unicopia.entity.collision.EntityCollisions;
|
|
import com.minelittlepony.unicopia.entity.collision.MultiBox;
|
|
import com.minelittlepony.unicopia.entity.duck.EntityDuck;
|
|
import com.minelittlepony.unicopia.item.HotAirBalloonItem;
|
|
import com.minelittlepony.unicopia.item.UItems;
|
|
import com.minelittlepony.unicopia.server.world.WeatherConditions;
|
|
|
|
public class AirBalloonEntity extends FlyingEntity implements EntityCollisions.ComplexCollidable, MultiBoundingBoxEntity {
|
|
private static final byte HAS_BURNER = 2;
|
|
private static final byte BURNER_ACTIVE = 4;
|
|
private static final TrackedData<Integer> BURNER_FLAGS = DataTracker.registerData(AirBalloonEntity.class, TrackedDataHandlerRegistry.INTEGER);
|
|
private static final TrackedData<Integer> BOOSTING = DataTracker.registerData(AirBalloonEntity.class, TrackedDataHandlerRegistry.INTEGER);
|
|
private static final TrackedData<Integer> INFLATION = DataTracker.registerData(AirBalloonEntity.class, TrackedDataHandlerRegistry.INTEGER);
|
|
private static final TrackedData<Integer> BASKET_TYPE = DataTracker.registerData(AirBalloonEntity.class, TrackedDataHandlerRegistry.INTEGER);
|
|
private static final TrackedData<Integer> BALLOON_DESIGN = DataTracker.registerData(AirBalloonEntity.class, TrackedDataHandlerRegistry.INTEGER);
|
|
|
|
private boolean prevBoosting;
|
|
private int prevInflation;
|
|
private Vec3d oldPosition = Vec3d.ZERO;
|
|
private Vec3d manualVelocity = Vec3d.ZERO;
|
|
|
|
public AirBalloonEntity(EntityType<? extends AirBalloonEntity> type, World world) {
|
|
super(type, world);
|
|
intersectionChecked = true;
|
|
setPersistent();
|
|
}
|
|
|
|
@Override
|
|
protected void initDataTracker() {
|
|
super.initDataTracker();
|
|
dataTracker.startTracking(BURNER_FLAGS, 0);
|
|
dataTracker.startTracking(BOOSTING, 0);
|
|
dataTracker.startTracking(INFLATION, 0);
|
|
dataTracker.startTracking(BASKET_TYPE, 0);
|
|
dataTracker.startTracking(BALLOON_DESIGN, 0);
|
|
}
|
|
|
|
public BoatEntity.Type getBasketType() {
|
|
return BoatEntity.Type.getType(dataTracker.get(BASKET_TYPE));
|
|
}
|
|
|
|
public void setBasketType(BoatEntity.Type type) {
|
|
dataTracker.set(BASKET_TYPE, type.ordinal());
|
|
}
|
|
|
|
public BalloonDesign getDesign() {
|
|
return BalloonDesign.getType(dataTracker.get(BALLOON_DESIGN));
|
|
}
|
|
|
|
public void setDesign(BalloonDesign design) {
|
|
dataTracker.set(BALLOON_DESIGN, design.ordinal());
|
|
}
|
|
|
|
public boolean hasBalloon() {
|
|
return getDesign() != BalloonDesign.NONE;
|
|
}
|
|
|
|
public boolean hasBurner() {
|
|
return getFlag(HAS_BURNER);
|
|
}
|
|
|
|
public void setHasBurner(boolean hasBurner) {
|
|
setFlag(HAS_BURNER, hasBurner);
|
|
}
|
|
|
|
public float getInflation(float tickDelta) {
|
|
return MathHelper.lerp(tickDelta, prevInflation, getInflation()) / (float)getMaxInflation();
|
|
}
|
|
|
|
private void setInflation(int inflation) {
|
|
dataTracker.set(INFLATION, MathHelper.clamp(inflation, 0, getMaxInflation()));
|
|
}
|
|
|
|
private int getInflation() {
|
|
return dataTracker.get(INFLATION);
|
|
}
|
|
|
|
protected int getMaxInflation() {
|
|
return 100;
|
|
}
|
|
|
|
public boolean isBurnerActive() {
|
|
return hasBalloon() && getFlag((byte)(HAS_BURNER | BURNER_ACTIVE));
|
|
}
|
|
|
|
public void setBurnerActive(boolean burnerActive) {
|
|
setFlag(BURNER_ACTIVE, burnerActive);
|
|
}
|
|
|
|
public int getBoostTicks() {
|
|
return dataTracker.get(BOOSTING);
|
|
}
|
|
|
|
protected void setBoostTicks(int ticks) {
|
|
dataTracker.set(BOOSTING, ticks);
|
|
}
|
|
|
|
private boolean getFlag(byte flag) {
|
|
return (dataTracker.get(BURNER_FLAGS).intValue() & flag) == flag;
|
|
}
|
|
|
|
private void setFlag(byte flag, boolean val) {
|
|
int v = dataTracker.get(BURNER_FLAGS);
|
|
dataTracker.set(BURNER_FLAGS, val ? (v | flag) : (v & ~flag));
|
|
}
|
|
|
|
private boolean isAirworthy() {
|
|
return hasBalloon() && isBurnerActive();
|
|
}
|
|
|
|
@Override
|
|
public List<Box> getBoundingBoxes() {
|
|
if (hasBalloon() && getInflation(1) > 0.999F) {
|
|
return List.of(getInteriorBoundingBox(), getBalloonBoundingBox());
|
|
}
|
|
return List.of(getInteriorBoundingBox());
|
|
}
|
|
|
|
@Override
|
|
public void tick() {
|
|
setAir(getMaxAir());
|
|
int boostTicks = getBoostTicks();
|
|
|
|
int inflation = getInflation();
|
|
prevInflation = inflation;
|
|
|
|
if (boostTicks > 0) {
|
|
boostTicks--;
|
|
if (inflation < getMaxInflation()) {
|
|
boostTicks--;
|
|
}
|
|
setBoostTicks(boostTicks);
|
|
}
|
|
|
|
boolean boosting = boostTicks > 0;
|
|
|
|
if (hasBurner() && isBurnerActive()) {
|
|
if (inflation < getMaxInflation()) {
|
|
inflation++;
|
|
if (boosting) {
|
|
inflation++;
|
|
}
|
|
setInflation(inflation);
|
|
}
|
|
} else {
|
|
if (inflation < getMaxInflation() && inflation > 0) {
|
|
setInflation(--inflation);
|
|
}
|
|
}
|
|
|
|
addVelocity(0, isBurnerActive() && inflation >= getMaxInflation() ? 0.005 : -0.013, 0);
|
|
addVelocity(manualVelocity.multiply(0.1));
|
|
manualVelocity = manualVelocity.multiply(0.9);
|
|
|
|
if (!isAirworthy() && isSubmergedInWater()) {
|
|
setVelocity(getVelocity().multiply(0.9, 0.4, 0.9).add(0, 0.02, 0));
|
|
}
|
|
|
|
Random rng = getWorld().random;
|
|
|
|
if (getWorld().isClient()) {
|
|
if (hasBurner() && isBurnerActive()) {
|
|
Vec3d burnerPos = getPos().add(0, 3, 0);
|
|
for (int i = 0; i < (boosting ? 6 : 1); i++) {
|
|
getWorld().addParticle(ParticleTypes.FLAME,
|
|
rng.nextTriangular(burnerPos.x, 0.25),
|
|
rng.nextTriangular(burnerPos.y, 1),
|
|
rng.nextTriangular(burnerPos.z, 0.25),
|
|
0,
|
|
Math.max(0, getVelocity().y + (boosting ? 0.1 : 0)),
|
|
0
|
|
);
|
|
}
|
|
}
|
|
} else if (inflation >= getMaxInflation()) {
|
|
if (hasBurner() && isBurnerActive()) {
|
|
addVelocity(WeatherConditions.getAirflow(getBlockPos(), getWorld()).multiply(0.2));
|
|
setVelocity(getVelocity().multiply(0.3, 1, 0.3));
|
|
}
|
|
|
|
if (boosting) {
|
|
addVelocity(0, 0.02, 0);
|
|
}
|
|
}
|
|
|
|
if (boosting && !prevBoosting) {
|
|
playSound(SoundEvents.ENTITY_GHAST_SHOOT, 1, 1);
|
|
}
|
|
|
|
if (isBurnerActive() && age % 15 + rng.nextInt(5) == 0) {
|
|
playSound(SoundEvents.ENTITY_GHAST_SHOOT, 0.2F, 1);
|
|
}
|
|
|
|
if (isLeashed()) {
|
|
Vec3d leashPost = getHoldingEntity().getPos();
|
|
Vec3d pos = getPos();
|
|
|
|
if (leashPost.distanceTo(pos) >= 5) {
|
|
Vec3d newVel = leashPost.subtract(pos).multiply(0.01);
|
|
setVelocity(newVel.lengthSquared() < 0.03 ? Vec3d.ZERO : newVel);
|
|
}
|
|
}
|
|
|
|
prevBoosting = boosting;
|
|
oldPosition = getPos();
|
|
|
|
if (getFireTicks() > 0) {
|
|
setFireTicks(1);
|
|
}
|
|
|
|
updatePassengers();
|
|
super.tick();
|
|
setBoundingBox(MultiBox.of(getBoundingBox(), getBoundingBoxes()));
|
|
}
|
|
|
|
private void updatePassengers() {
|
|
for (Box box : getBoundingBoxes()) {
|
|
for (Entity e : getWorld().getOtherEntities(this, box.expand(getVelocity().length()).expand(0, 0.5, 0))) {
|
|
updatePassenger(e, box, e.getY() > getY() + 3);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void updatePassenger(Entity e, Box box, boolean inBalloon) {
|
|
|
|
double height = box.getYLength();
|
|
|
|
if (height < 3 || e.getBoundingBox().minY > box.minY + height / 2D) {
|
|
if (getVelocity().y > 0 && e.getBoundingBox().minY < box.maxY + 0.02) {
|
|
e.setPos(e.getX(), box.maxY, e.getZ());
|
|
}
|
|
if (getVelocity().y < 0 && e.getBoundingBox().minY > box.maxY) {
|
|
e.setPos(e.getX(), box.maxY, e.getZ());
|
|
}
|
|
}
|
|
|
|
if (manualVelocity.length() > 0.01 || getVelocity().length() > 0.3) {
|
|
e.setVelocity(e.getVelocity().multiply(0.1, 0.5, 0.1));
|
|
}
|
|
|
|
if (getVelocity().y < 0) {
|
|
e.addVelocity(0, getVelocity().y, 0);
|
|
Living.updateVelocity(e);
|
|
}
|
|
|
|
if (inBalloon && !e.isSneaky() && Math.abs(e.getVelocity().y) > 0.079) {
|
|
e.setVelocity(e.getVelocity().multiply(1, e.getVelocity().y < 0 ? -0.9 : 1.2, 1).add(0, 0.8, 0));
|
|
if (Math.abs(e.getVelocity().y) > 2) {
|
|
e.setVelocity(e.getVelocity().x, MathHelper.clamp(e.getVelocity().y, -2, 2), e.getVelocity().z);
|
|
}
|
|
}
|
|
|
|
Living.getOrEmpty(e).ifPresent(living -> {
|
|
living.setSupportingEntity(this);
|
|
living.setPositionOffset(e.getPos().subtract(oldPosition));
|
|
living.updateRelativePosition(box);
|
|
});
|
|
|
|
if (getWorld().isClient) {
|
|
if (e.distanceTraveled > ((EntityDuck)e).getNextStepSoundDistance()) {
|
|
e.distanceTraveled--;
|
|
e.playSound(inBalloon ? SoundEvents.BLOCK_WOOL_STEP : SoundEvents.BLOCK_BAMBOO_STEP, 0.5F, 1);
|
|
if (!e.isSneaky()) {
|
|
getWorld().emitGameEvent(e, GameEvent.STEP, getBlockPos());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public ActionResult interactAt(PlayerEntity player, Vec3d hitPos, Hand hand) {
|
|
ItemStack stack = player.getStackInHand(hand);
|
|
|
|
if (hitPos.y > (3 * getInflation(1))) {
|
|
if (hasBalloon() && hasBurner()) {
|
|
if (stack.isOf(Items.FLINT_AND_STEEL)) {
|
|
setBurnerActive(!isBurnerActive());
|
|
if (isBurnerActive()) {
|
|
playSound(SoundEvents.ENTITY_GHAST_SHOOT, 1, 1);
|
|
}
|
|
stack.damage(1, player, p -> p.sendEquipmentBreakStatus(hand == Hand.MAIN_HAND ? EquipmentSlot.MAINHAND : EquipmentSlot.OFFHAND));
|
|
playSound(SoundEvents.ITEM_FLINTANDSTEEL_USE, 1, 1);
|
|
getWorld().emitGameEvent(this, GameEvent.ENTITY_INTERACT, getBlockPos());
|
|
return ActionResult.SUCCESS;
|
|
}
|
|
|
|
if (stack.isEmpty() && Math.abs(hitPos.x) > 1 && Math.abs(hitPos.z) > 1) {
|
|
double xPush = Math.signum(hitPos.x);
|
|
double zPush = Math.signum(hitPos.z);
|
|
if (!getWorld().isClient) {
|
|
manualVelocity = manualVelocity.add(0.3 * xPush, 0, 0.3 * zPush);
|
|
}
|
|
} else if (stack.isEmpty() && isBurnerActive()) {
|
|
setBoostTicks(50);
|
|
getWorld().emitGameEvent(this, GameEvent.ENTITY_INTERACT, getBlockPos());
|
|
}
|
|
}
|
|
}
|
|
|
|
return ActionResult.PASS;
|
|
}
|
|
|
|
@Override
|
|
protected ActionResult interactMob(PlayerEntity player, Hand hand) {
|
|
ItemStack stack = player.getStackInHand(hand);
|
|
|
|
if (stack.getItem() instanceof HotAirBalloonItem balloon && !hasBalloon()) {
|
|
if (!player.getAbilities().creativeMode) {
|
|
stack.decrement(1);
|
|
}
|
|
playSound(SoundEvents.ITEM_ARMOR_EQUIP_LEATHER, 1, 1);
|
|
getWorld().emitGameEvent(this, GameEvent.ENTITY_INTERACT, getBlockPos());
|
|
setDesign(HotAirBalloonItem.getDesign(getWorld(), stack));
|
|
return ActionResult.SUCCESS;
|
|
}
|
|
|
|
if ((stack.isOf(Items.LANTERN) || stack.isOf(Items.SOUL_LANTERN)) && !hasBurner()) {
|
|
setStackInHand(Hand.MAIN_HAND, stack.copyWithCount(1));
|
|
if (!player.getAbilities().creativeMode) {
|
|
stack.decrement(1);
|
|
}
|
|
playSound(SoundEvents.ENTITY_IRON_GOLEM_DAMAGE, 0.2F, 1);
|
|
getWorld().emitGameEvent(this, GameEvent.ENTITY_INTERACT, getBlockPos());
|
|
setHasBurner(true);
|
|
return ActionResult.SUCCESS;
|
|
}
|
|
|
|
return ActionResult.PASS;
|
|
}
|
|
|
|
@Override
|
|
protected void dropInventory() {
|
|
ItemStack lantern = getStackInHand(Hand.MAIN_HAND);
|
|
setStackInHand(Hand.MAIN_HAND, ItemStack.EMPTY);
|
|
dropStack(lantern);
|
|
dropStack(getPickBlockStack());
|
|
}
|
|
|
|
@Override
|
|
public ItemStack getPickBlockStack() {
|
|
return asItem().getDefaultStack();
|
|
}
|
|
|
|
public Item asItem() {
|
|
return switch (getBasketType()) {
|
|
case SPRUCE -> UItems.SPRUCE_BASKET;
|
|
case BIRCH -> UItems.BIRCH_BASKET;
|
|
case JUNGLE -> UItems.JUNGLE_BASKET;
|
|
case ACACIA -> UItems.ACACIA_BASKET;
|
|
case CHERRY -> UItems.CHERRY_BASKET;
|
|
case DARK_OAK -> UItems.DARK_OAK_BASKET;
|
|
case MANGROVE -> UItems.MANGROVE_BASKET;
|
|
case BAMBOO -> UItems.BAMBOO_BASKET;
|
|
default -> UItems.OAK_BASKET;
|
|
};
|
|
}
|
|
|
|
@Override
|
|
public boolean isCollidable() {
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public boolean isPushable() {
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public void pushAwayFrom(Entity entity) {
|
|
|
|
}
|
|
|
|
@Override
|
|
public void pushAway(Entity entity) {
|
|
|
|
}
|
|
|
|
@Override
|
|
protected Entity.MoveEffect getMoveEffect() {
|
|
return Entity.MoveEffect.EVENTS;
|
|
}
|
|
|
|
|
|
@Override
|
|
public Box getVisibilityBoundingBox() {
|
|
if (hasBalloon()) {
|
|
return MultiBox.unbox(getBoundingBox()).union(getBalloonBoundingBox());
|
|
}
|
|
return MultiBox.unbox(getBoundingBox());
|
|
}
|
|
|
|
protected Box getInteriorBoundingBox() {
|
|
Box box = MultiBox.unbox(getBoundingBox());
|
|
return box.withMinY(box.minY - 0.2).contract(0.2, 0, 0.2);
|
|
}
|
|
|
|
protected Box getBalloonBoundingBox() {
|
|
float inflation = getInflation(1);
|
|
return MultiBox.unbox(getBoundingBox())
|
|
.offset(0.125, 7.3 * inflation, 0.125)
|
|
.expand(2.25, 3.7 * inflation, 2.25);
|
|
}
|
|
|
|
@Override
|
|
public void getCollissionShapes(ShapeContext context, Consumer<VoxelShape> output) {
|
|
|
|
Box box = MultiBox.unbox(getBoundingBox()).expand(0.3, 0, 0.3);
|
|
|
|
double wallheight = box.maxY + 0.7;
|
|
double wallThickness = 0.7;
|
|
|
|
// front left (next to door)
|
|
output.accept(VoxelShapes.cuboid(new Box(box.minX, box.minY, box.minZ, box.minX + wallThickness + 0.2, wallheight, box.minZ + wallThickness)));
|
|
// front right (next to door)
|
|
output.accept(VoxelShapes.cuboid(new Box(box.maxX - wallThickness - 0.2, box.minY, box.minZ, box.maxX, wallheight, box.minZ + wallThickness)));
|
|
|
|
// back
|
|
output.accept(VoxelShapes.cuboid(new Box(box.minX, box.minY, box.maxZ - wallThickness, box.maxX, wallheight, box.maxZ)));
|
|
|
|
// left
|
|
output.accept(VoxelShapes.cuboid(new Box(box.maxX - wallThickness, box.minY, box.minZ, box.maxX, wallheight, box.maxZ)));
|
|
// right
|
|
output.accept(VoxelShapes.cuboid(new Box(box.minX, box.minY, box.minZ, box.minX + wallThickness, wallheight, box.maxZ)));
|
|
|
|
// top of balloon
|
|
if (hasBalloon() && getInflation() > 0) {
|
|
output.accept(VoxelShapes.cuboid(getBalloonBoundingBox()));
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void readCustomDataFromNbt(NbtCompound compound) {
|
|
super.readCustomDataFromNbt(compound);
|
|
setBasketType(BoatEntity.Type.getType(compound.getString("basketType")));
|
|
setDesign(BalloonDesign.getType(compound.getString("design")));
|
|
setHasBurner(compound.getBoolean("hasBurner"));
|
|
setBurnerActive(compound.getBoolean("burnerActive"));
|
|
setBoostTicks(compound.getInt("boostTicks"));
|
|
prevInflation = compound.getInt("inflationAmount");
|
|
setInflation(prevInflation);
|
|
}
|
|
|
|
@Override
|
|
public void writeCustomDataToNbt(NbtCompound compound) {
|
|
super.writeCustomDataToNbt(compound);
|
|
compound.putString("design", getDesign().asString());
|
|
compound.putString("basket", getBasketType().asString());
|
|
compound.putBoolean("hasBurner", hasBurner());
|
|
compound.putBoolean("burnerActive", isBurnerActive());
|
|
compound.putInt("boostTicks", getBoostTicks());
|
|
compound.putInt("inflationAmount", getInflation());
|
|
}
|
|
|
|
@SuppressWarnings("deprecation")
|
|
public enum BalloonDesign implements StringIdentifiable {
|
|
NONE,
|
|
LUNA;
|
|
|
|
public static final StringIdentifiable.Codec<BalloonDesign> CODEC = StringIdentifiable.createCodec(BalloonDesign::values);
|
|
private static final IntFunction<BalloonDesign> BY_ID = ValueLists.<BalloonDesign>createIdToValueFunction(Enum::ordinal, values(), ValueLists.OutOfBoundsHandling.ZERO);
|
|
|
|
private final String name = name().toLowerCase(Locale.ROOT);
|
|
|
|
@Override
|
|
public String asString() {
|
|
return name;
|
|
}
|
|
|
|
public static BalloonDesign getType(int type) {
|
|
return BY_ID.apply(type);
|
|
}
|
|
|
|
public static BalloonDesign getType(String name) {
|
|
return CODEC.byId(name, LUNA);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|