525 lines
20 KiB
Java
525 lines
20 KiB
Java
/*
|
|
* Decompiled with CFR 0.152.
|
|
*
|
|
* Could not load the following classes:
|
|
* org.jspecify.annotations.Nullable
|
|
*/
|
|
package net.minecraft.world.entity.monster;
|
|
|
|
import java.util.Comparator;
|
|
import java.util.EnumSet;
|
|
import java.util.List;
|
|
import net.minecraft.core.BlockPos;
|
|
import net.minecraft.core.particles.ParticleTypes;
|
|
import net.minecraft.network.syncher.EntityDataAccessor;
|
|
import net.minecraft.network.syncher.EntityDataSerializers;
|
|
import net.minecraft.network.syncher.SynchedEntityData;
|
|
import net.minecraft.server.level.ServerLevel;
|
|
import net.minecraft.sounds.SoundEvent;
|
|
import net.minecraft.sounds.SoundEvents;
|
|
import net.minecraft.sounds.SoundSource;
|
|
import net.minecraft.util.Mth;
|
|
import net.minecraft.world.DifficultyInstance;
|
|
import net.minecraft.world.damagesource.DamageSource;
|
|
import net.minecraft.world.entity.Entity;
|
|
import net.minecraft.world.entity.EntityDimensions;
|
|
import net.minecraft.world.entity.EntitySelector;
|
|
import net.minecraft.world.entity.EntitySpawnReason;
|
|
import net.minecraft.world.entity.EntityType;
|
|
import net.minecraft.world.entity.LivingEntity;
|
|
import net.minecraft.world.entity.Mob;
|
|
import net.minecraft.world.entity.Pose;
|
|
import net.minecraft.world.entity.SpawnGroupData;
|
|
import net.minecraft.world.entity.ai.attributes.Attributes;
|
|
import net.minecraft.world.entity.ai.control.BodyRotationControl;
|
|
import net.minecraft.world.entity.ai.control.LookControl;
|
|
import net.minecraft.world.entity.ai.control.MoveControl;
|
|
import net.minecraft.world.entity.ai.goal.Goal;
|
|
import net.minecraft.world.entity.ai.targeting.TargetingConditions;
|
|
import net.minecraft.world.entity.animal.Cat;
|
|
import net.minecraft.world.entity.monster.Enemy;
|
|
import net.minecraft.world.entity.player.Player;
|
|
import net.minecraft.world.level.Level;
|
|
import net.minecraft.world.level.ServerLevelAccessor;
|
|
import net.minecraft.world.level.block.state.BlockState;
|
|
import net.minecraft.world.level.levelgen.Heightmap;
|
|
import net.minecraft.world.level.storage.ValueInput;
|
|
import net.minecraft.world.level.storage.ValueOutput;
|
|
import net.minecraft.world.phys.Vec3;
|
|
import org.jspecify.annotations.Nullable;
|
|
|
|
public class Phantom
|
|
extends Mob
|
|
implements Enemy {
|
|
public static final float FLAP_DEGREES_PER_TICK = 7.448451f;
|
|
public static final int TICKS_PER_FLAP = Mth.ceil(24.166098f);
|
|
private static final EntityDataAccessor<Integer> ID_SIZE = SynchedEntityData.defineId(Phantom.class, EntityDataSerializers.INT);
|
|
private Vec3 moveTargetPoint = Vec3.ZERO;
|
|
private @Nullable BlockPos anchorPoint;
|
|
private AttackPhase attackPhase = AttackPhase.CIRCLE;
|
|
|
|
public Phantom(EntityType<? extends Phantom> type, Level level) {
|
|
super((EntityType<? extends Mob>)type, level);
|
|
this.xpReward = 5;
|
|
this.moveControl = new PhantomMoveControl(this);
|
|
this.lookControl = new PhantomLookControl(this);
|
|
}
|
|
|
|
@Override
|
|
public boolean isFlapping() {
|
|
return (this.getUniqueFlapTickOffset() + this.tickCount) % TICKS_PER_FLAP == 0;
|
|
}
|
|
|
|
@Override
|
|
protected BodyRotationControl createBodyControl() {
|
|
return new PhantomBodyRotationControl(this);
|
|
}
|
|
|
|
@Override
|
|
protected void registerGoals() {
|
|
this.goalSelector.addGoal(1, new PhantomAttackStrategyGoal());
|
|
this.goalSelector.addGoal(2, new PhantomSweepAttackGoal());
|
|
this.goalSelector.addGoal(3, new PhantomCircleAroundAnchorGoal());
|
|
this.targetSelector.addGoal(1, new PhantomAttackPlayerTargetGoal());
|
|
}
|
|
|
|
@Override
|
|
protected void defineSynchedData(SynchedEntityData.Builder entityData) {
|
|
super.defineSynchedData(entityData);
|
|
entityData.define(ID_SIZE, 0);
|
|
}
|
|
|
|
public void setPhantomSize(int size) {
|
|
this.entityData.set(ID_SIZE, Mth.clamp(size, 0, 64));
|
|
}
|
|
|
|
private void updatePhantomSizeInfo() {
|
|
this.refreshDimensions();
|
|
this.getAttribute(Attributes.ATTACK_DAMAGE).setBaseValue(6 + this.getPhantomSize());
|
|
}
|
|
|
|
public int getPhantomSize() {
|
|
return this.entityData.get(ID_SIZE);
|
|
}
|
|
|
|
@Override
|
|
public void onSyncedDataUpdated(EntityDataAccessor<?> accessor) {
|
|
if (ID_SIZE.equals(accessor)) {
|
|
this.updatePhantomSizeInfo();
|
|
}
|
|
super.onSyncedDataUpdated(accessor);
|
|
}
|
|
|
|
public int getUniqueFlapTickOffset() {
|
|
return this.getId() * 3;
|
|
}
|
|
|
|
@Override
|
|
public void tick() {
|
|
super.tick();
|
|
if (this.level().isClientSide()) {
|
|
float anim = Mth.cos((float)(this.getUniqueFlapTickOffset() + this.tickCount) * 7.448451f * ((float)Math.PI / 180) + (float)Math.PI);
|
|
float nextAnim = Mth.cos((float)(this.getUniqueFlapTickOffset() + this.tickCount + 1) * 7.448451f * ((float)Math.PI / 180) + (float)Math.PI);
|
|
if (anim > 0.0f && nextAnim <= 0.0f) {
|
|
this.level().playLocalSound(this.getX(), this.getY(), this.getZ(), SoundEvents.PHANTOM_FLAP, this.getSoundSource(), 0.95f + this.random.nextFloat() * 0.05f, 0.95f + this.random.nextFloat() * 0.05f, false);
|
|
}
|
|
float width = this.getBbWidth() * 1.48f;
|
|
float c = Mth.cos(this.getYRot() * ((float)Math.PI / 180)) * width;
|
|
float s = Mth.sin(this.getYRot() * ((float)Math.PI / 180)) * width;
|
|
float h = (0.3f + anim * 0.45f) * this.getBbHeight() * 2.5f;
|
|
this.level().addParticle(ParticleTypes.MYCELIUM, this.getX() + (double)c, this.getY() + (double)h, this.getZ() + (double)s, 0.0, 0.0, 0.0);
|
|
this.level().addParticle(ParticleTypes.MYCELIUM, this.getX() - (double)c, this.getY() + (double)h, this.getZ() - (double)s, 0.0, 0.0, 0.0);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void checkFallDamage(double ya, boolean onGround, BlockState onState, BlockPos pos) {
|
|
}
|
|
|
|
@Override
|
|
public boolean onClimbable() {
|
|
return false;
|
|
}
|
|
|
|
@Override
|
|
public void travel(Vec3 input) {
|
|
this.travelFlying(input, 0.2f);
|
|
}
|
|
|
|
@Override
|
|
public SpawnGroupData finalizeSpawn(ServerLevelAccessor level, DifficultyInstance difficulty, EntitySpawnReason spawnReason, @Nullable SpawnGroupData groupData) {
|
|
this.anchorPoint = this.blockPosition().above(5);
|
|
this.setPhantomSize(0);
|
|
return super.finalizeSpawn(level, difficulty, spawnReason, groupData);
|
|
}
|
|
|
|
@Override
|
|
protected void readAdditionalSaveData(ValueInput input) {
|
|
super.readAdditionalSaveData(input);
|
|
this.anchorPoint = input.read("anchor_pos", BlockPos.CODEC).orElse(null);
|
|
this.setPhantomSize(input.getIntOr("size", 0));
|
|
}
|
|
|
|
@Override
|
|
protected void addAdditionalSaveData(ValueOutput output) {
|
|
super.addAdditionalSaveData(output);
|
|
output.storeNullable("anchor_pos", BlockPos.CODEC, this.anchorPoint);
|
|
output.putInt("size", this.getPhantomSize());
|
|
}
|
|
|
|
@Override
|
|
public boolean shouldRenderAtSqrDistance(double distance) {
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public SoundSource getSoundSource() {
|
|
return SoundSource.HOSTILE;
|
|
}
|
|
|
|
@Override
|
|
protected SoundEvent getAmbientSound() {
|
|
return SoundEvents.PHANTOM_AMBIENT;
|
|
}
|
|
|
|
@Override
|
|
protected SoundEvent getHurtSound(DamageSource source) {
|
|
return SoundEvents.PHANTOM_HURT;
|
|
}
|
|
|
|
@Override
|
|
protected SoundEvent getDeathSound() {
|
|
return SoundEvents.PHANTOM_DEATH;
|
|
}
|
|
|
|
@Override
|
|
protected float getSoundVolume() {
|
|
return 1.0f;
|
|
}
|
|
|
|
@Override
|
|
public boolean canAttackType(EntityType<?> targetType) {
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public EntityDimensions getDefaultDimensions(Pose pose) {
|
|
int size = this.getPhantomSize();
|
|
EntityDimensions originalDimensions = super.getDefaultDimensions(pose);
|
|
return originalDimensions.scale(1.0f + 0.15f * (float)size);
|
|
}
|
|
|
|
private boolean canAttack(ServerLevel level, LivingEntity target, TargetingConditions targetConditions) {
|
|
return targetConditions.test(level, this, target);
|
|
}
|
|
|
|
private static enum AttackPhase {
|
|
CIRCLE,
|
|
SWOOP;
|
|
|
|
}
|
|
|
|
private class PhantomMoveControl
|
|
extends MoveControl {
|
|
private float speed;
|
|
|
|
public PhantomMoveControl(Mob mob) {
|
|
super(mob);
|
|
this.speed = 0.1f;
|
|
}
|
|
|
|
@Override
|
|
public void tick() {
|
|
if (Phantom.this.horizontalCollision) {
|
|
Phantom.this.setYRot(Phantom.this.getYRot() + 180.0f);
|
|
this.speed = 0.1f;
|
|
}
|
|
double tdx = Phantom.this.moveTargetPoint.x - Phantom.this.getX();
|
|
double tdy = Phantom.this.moveTargetPoint.y - Phantom.this.getY();
|
|
double tdz = Phantom.this.moveTargetPoint.z - Phantom.this.getZ();
|
|
double sd = Math.sqrt(tdx * tdx + tdz * tdz);
|
|
if (Math.abs(sd) > (double)1.0E-5f) {
|
|
double yRelativeScale = 1.0 - Math.abs(tdy * (double)0.7f) / sd;
|
|
sd = Math.sqrt((tdx *= yRelativeScale) * tdx + (tdz *= yRelativeScale) * tdz);
|
|
double sd2 = Math.sqrt(tdx * tdx + tdz * tdz + tdy * tdy);
|
|
float prev = Phantom.this.getYRot();
|
|
float angle = (float)Mth.atan2(tdz, tdx);
|
|
float a = Mth.wrapDegrees(Phantom.this.getYRot() + 90.0f);
|
|
float b = Mth.wrapDegrees(angle * 57.295776f);
|
|
Phantom.this.setYRot(Mth.approachDegrees(a, b, 4.0f) - 90.0f);
|
|
Phantom.this.yBodyRot = Phantom.this.getYRot();
|
|
this.speed = Mth.degreesDifferenceAbs(prev, Phantom.this.getYRot()) < 3.0f ? Mth.approach(this.speed, 1.8f, 0.005f * (1.8f / this.speed)) : Mth.approach(this.speed, 0.2f, 0.025f);
|
|
float xRotD = (float)(-(Mth.atan2(-tdy, sd) * 57.2957763671875));
|
|
Phantom.this.setXRot(xRotD);
|
|
float moveAngle = Phantom.this.getYRot() + 90.0f;
|
|
double txd = (double)(this.speed * Mth.cos(moveAngle * ((float)Math.PI / 180))) * Math.abs(tdx / sd2);
|
|
double tzd = (double)(this.speed * Mth.sin(moveAngle * ((float)Math.PI / 180))) * Math.abs(tdz / sd2);
|
|
double tyd = (double)(this.speed * Mth.sin(xRotD * ((float)Math.PI / 180))) * Math.abs(tdy / sd2);
|
|
Vec3 movement = Phantom.this.getDeltaMovement();
|
|
Phantom.this.setDeltaMovement(movement.add(new Vec3(txd, tyd, tzd).subtract(movement).scale(0.2)));
|
|
}
|
|
}
|
|
}
|
|
|
|
private static class PhantomLookControl
|
|
extends LookControl {
|
|
public PhantomLookControl(Mob mob) {
|
|
super(mob);
|
|
}
|
|
|
|
@Override
|
|
public void tick() {
|
|
}
|
|
}
|
|
|
|
private class PhantomBodyRotationControl
|
|
extends BodyRotationControl {
|
|
public PhantomBodyRotationControl(Mob mob) {
|
|
super(mob);
|
|
}
|
|
|
|
@Override
|
|
public void clientTick() {
|
|
Phantom.this.yHeadRot = Phantom.this.yBodyRot;
|
|
Phantom.this.yBodyRot = Phantom.this.getYRot();
|
|
}
|
|
}
|
|
|
|
private class PhantomAttackStrategyGoal
|
|
extends Goal {
|
|
private int nextSweepTick;
|
|
|
|
private PhantomAttackStrategyGoal() {
|
|
}
|
|
|
|
@Override
|
|
public boolean canUse() {
|
|
LivingEntity target = Phantom.this.getTarget();
|
|
if (target != null) {
|
|
return Phantom.this.canAttack(PhantomAttackStrategyGoal.getServerLevel(Phantom.this.level()), target, TargetingConditions.DEFAULT);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
@Override
|
|
public void start() {
|
|
this.nextSweepTick = this.adjustedTickDelay(10);
|
|
Phantom.this.attackPhase = AttackPhase.CIRCLE;
|
|
this.setAnchorAboveTarget();
|
|
}
|
|
|
|
@Override
|
|
public void stop() {
|
|
if (Phantom.this.anchorPoint != null) {
|
|
Phantom.this.anchorPoint = Phantom.this.level().getHeightmapPos(Heightmap.Types.MOTION_BLOCKING, Phantom.this.anchorPoint).above(10 + Phantom.this.random.nextInt(20));
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void tick() {
|
|
if (Phantom.this.attackPhase == AttackPhase.CIRCLE) {
|
|
--this.nextSweepTick;
|
|
if (this.nextSweepTick <= 0) {
|
|
Phantom.this.attackPhase = AttackPhase.SWOOP;
|
|
this.setAnchorAboveTarget();
|
|
this.nextSweepTick = this.adjustedTickDelay((8 + Phantom.this.random.nextInt(4)) * 20);
|
|
Phantom.this.playSound(SoundEvents.PHANTOM_SWOOP, 10.0f, 0.95f + Phantom.this.random.nextFloat() * 0.1f);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void setAnchorAboveTarget() {
|
|
if (Phantom.this.anchorPoint == null) {
|
|
return;
|
|
}
|
|
Phantom.this.anchorPoint = Phantom.this.getTarget().blockPosition().above(20 + Phantom.this.random.nextInt(20));
|
|
if (Phantom.this.anchorPoint.getY() < Phantom.this.level().getSeaLevel()) {
|
|
Phantom.this.anchorPoint = new BlockPos(Phantom.this.anchorPoint.getX(), Phantom.this.level().getSeaLevel() + 1, Phantom.this.anchorPoint.getZ());
|
|
}
|
|
}
|
|
}
|
|
|
|
private class PhantomSweepAttackGoal
|
|
extends PhantomMoveTargetGoal {
|
|
private static final int CAT_SEARCH_TICK_DELAY = 20;
|
|
private boolean isScaredOfCat;
|
|
private int catSearchTick;
|
|
|
|
private PhantomSweepAttackGoal() {
|
|
}
|
|
|
|
@Override
|
|
public boolean canUse() {
|
|
return Phantom.this.getTarget() != null && Phantom.this.attackPhase == AttackPhase.SWOOP;
|
|
}
|
|
|
|
@Override
|
|
public boolean canContinueToUse() {
|
|
LivingEntity target = Phantom.this.getTarget();
|
|
if (target == null) {
|
|
return false;
|
|
}
|
|
if (!target.isAlive()) {
|
|
return false;
|
|
}
|
|
if (target instanceof Player) {
|
|
Player player = (Player)target;
|
|
if (target.isSpectator() || player.isCreative()) {
|
|
return false;
|
|
}
|
|
}
|
|
if (!this.canUse()) {
|
|
return false;
|
|
}
|
|
if (Phantom.this.tickCount > this.catSearchTick) {
|
|
this.catSearchTick = Phantom.this.tickCount + 20;
|
|
List<Entity> cats = Phantom.this.level().getEntitiesOfClass(Cat.class, Phantom.this.getBoundingBox().inflate(16.0), EntitySelector.ENTITY_STILL_ALIVE);
|
|
for (Cat cat : cats) {
|
|
cat.hiss();
|
|
}
|
|
this.isScaredOfCat = !cats.isEmpty();
|
|
}
|
|
return !this.isScaredOfCat;
|
|
}
|
|
|
|
@Override
|
|
public void start() {
|
|
}
|
|
|
|
@Override
|
|
public void stop() {
|
|
Phantom.this.setTarget(null);
|
|
Phantom.this.attackPhase = AttackPhase.CIRCLE;
|
|
}
|
|
|
|
@Override
|
|
public void tick() {
|
|
LivingEntity target = Phantom.this.getTarget();
|
|
if (target == null) {
|
|
return;
|
|
}
|
|
Phantom.this.moveTargetPoint = new Vec3(target.getX(), target.getY(0.5), target.getZ());
|
|
if (Phantom.this.getBoundingBox().inflate(0.2f).intersects(target.getBoundingBox())) {
|
|
Phantom.this.doHurtTarget(PhantomSweepAttackGoal.getServerLevel(Phantom.this.level()), target);
|
|
Phantom.this.attackPhase = AttackPhase.CIRCLE;
|
|
if (!Phantom.this.isSilent()) {
|
|
Phantom.this.level().levelEvent(1039, Phantom.this.blockPosition(), 0);
|
|
}
|
|
} else if (Phantom.this.horizontalCollision || Phantom.this.hurtTime > 0) {
|
|
Phantom.this.attackPhase = AttackPhase.CIRCLE;
|
|
}
|
|
}
|
|
}
|
|
|
|
private class PhantomCircleAroundAnchorGoal
|
|
extends PhantomMoveTargetGoal {
|
|
private float angle;
|
|
private float distance;
|
|
private float height;
|
|
private float clockwise;
|
|
|
|
private PhantomCircleAroundAnchorGoal() {
|
|
}
|
|
|
|
@Override
|
|
public boolean canUse() {
|
|
return Phantom.this.getTarget() == null || Phantom.this.attackPhase == AttackPhase.CIRCLE;
|
|
}
|
|
|
|
@Override
|
|
public void start() {
|
|
this.distance = 5.0f + Phantom.this.random.nextFloat() * 10.0f;
|
|
this.height = -4.0f + Phantom.this.random.nextFloat() * 9.0f;
|
|
this.clockwise = Phantom.this.random.nextBoolean() ? 1.0f : -1.0f;
|
|
this.selectNext();
|
|
}
|
|
|
|
@Override
|
|
public void tick() {
|
|
if (Phantom.this.random.nextInt(this.adjustedTickDelay(350)) == 0) {
|
|
this.height = -4.0f + Phantom.this.random.nextFloat() * 9.0f;
|
|
}
|
|
if (Phantom.this.random.nextInt(this.adjustedTickDelay(250)) == 0) {
|
|
this.distance += 1.0f;
|
|
if (this.distance > 15.0f) {
|
|
this.distance = 5.0f;
|
|
this.clockwise = -this.clockwise;
|
|
}
|
|
}
|
|
if (Phantom.this.random.nextInt(this.adjustedTickDelay(450)) == 0) {
|
|
this.angle = Phantom.this.random.nextFloat() * 2.0f * (float)Math.PI;
|
|
this.selectNext();
|
|
}
|
|
if (this.touchingTarget()) {
|
|
this.selectNext();
|
|
}
|
|
if (Phantom.this.moveTargetPoint.y < Phantom.this.getY() && !Phantom.this.level().isEmptyBlock(Phantom.this.blockPosition().below(1))) {
|
|
this.height = Math.max(1.0f, this.height);
|
|
this.selectNext();
|
|
}
|
|
if (Phantom.this.moveTargetPoint.y > Phantom.this.getY() && !Phantom.this.level().isEmptyBlock(Phantom.this.blockPosition().above(1))) {
|
|
this.height = Math.min(-1.0f, this.height);
|
|
this.selectNext();
|
|
}
|
|
}
|
|
|
|
private void selectNext() {
|
|
if (Phantom.this.anchorPoint == null) {
|
|
Phantom.this.anchorPoint = Phantom.this.blockPosition();
|
|
}
|
|
this.angle += this.clockwise * 15.0f * ((float)Math.PI / 180);
|
|
Phantom.this.moveTargetPoint = Vec3.atLowerCornerOf(Phantom.this.anchorPoint).add(this.distance * Mth.cos(this.angle), -4.0f + this.height, this.distance * Mth.sin(this.angle));
|
|
}
|
|
}
|
|
|
|
private class PhantomAttackPlayerTargetGoal
|
|
extends Goal {
|
|
private final TargetingConditions attackTargeting = TargetingConditions.forCombat().range(64.0);
|
|
private int nextScanTick = PhantomAttackPlayerTargetGoal.reducedTickDelay(20);
|
|
|
|
private PhantomAttackPlayerTargetGoal() {
|
|
}
|
|
|
|
@Override
|
|
public boolean canUse() {
|
|
if (this.nextScanTick > 0) {
|
|
--this.nextScanTick;
|
|
return false;
|
|
}
|
|
this.nextScanTick = PhantomAttackPlayerTargetGoal.reducedTickDelay(60);
|
|
ServerLevel level = PhantomAttackPlayerTargetGoal.getServerLevel(Phantom.this.level());
|
|
List<Player> players = level.getNearbyPlayers(this.attackTargeting, Phantom.this, Phantom.this.getBoundingBox().inflate(16.0, 64.0, 16.0));
|
|
if (!players.isEmpty()) {
|
|
players.sort(Comparator.comparing(Entity::getY).reversed());
|
|
for (Player player : players) {
|
|
if (!Phantom.this.canAttack(level, player, TargetingConditions.DEFAULT)) continue;
|
|
Phantom.this.setTarget(player);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
@Override
|
|
public boolean canContinueToUse() {
|
|
LivingEntity target = Phantom.this.getTarget();
|
|
if (target != null) {
|
|
return Phantom.this.canAttack(PhantomAttackPlayerTargetGoal.getServerLevel(Phantom.this.level()), target, TargetingConditions.DEFAULT);
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private abstract class PhantomMoveTargetGoal
|
|
extends Goal {
|
|
public PhantomMoveTargetGoal() {
|
|
this.setFlags(EnumSet.of(Goal.Flag.MOVE));
|
|
}
|
|
|
|
protected boolean touchingTarget() {
|
|
return Phantom.this.moveTargetPoint.distanceToSqr(Phantom.this.getX(), Phantom.this.getY(), Phantom.this.getZ()) < 4.0;
|
|
}
|
|
}
|
|
}
|
|
|