From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: William Blake Galbreath <blake.galbreath@gmail.com>
Date: Thu, 9 May 2019 18:26:06 -0500
Subject: [PATCH] Phantoms attracted to crystals and crystals shoot phantoms


diff --git a/src/main/java/net/minecraft/world/entity/boss/enderdragon/EndCrystal.java b/src/main/java/net/minecraft/world/entity/boss/enderdragon/EndCrystal.java
index b643a2449e329560c936c0a06fb4cc494d0737a7..92e65f3fbc8f5d77bb8cc31e7a7780c2589f4227 100644
--- a/src/main/java/net/minecraft/world/entity/boss/enderdragon/EndCrystal.java
+++ b/src/main/java/net/minecraft/world/entity/boss/enderdragon/EndCrystal.java
@@ -32,6 +32,12 @@ public class EndCrystal extends Entity {
     private static final EntityDataAccessor<Boolean> DATA_SHOW_BOTTOM = SynchedEntityData.defineId(EndCrystal.class, EntityDataSerializers.BOOLEAN);
     public int time;
     public boolean generatedByDragonFight = false; // Paper - Fix invulnerable end crystals
+    // Purpur start
+    private net.minecraft.world.entity.monster.Phantom targetPhantom;
+    private int phantomBeamTicks = 0;
+    private int phantomDamageCooldown = 0;
+    private int idleCooldown = 0;
+    // Purpur end
 
     public EndCrystal(EntityType<? extends EndCrystal> type, Level world) {
         super(type, world);
@@ -81,7 +87,50 @@ public class EndCrystal extends Entity {
             // Paper end
         }
 
+        // Purpur start
+        if (level.purpurConfig.phantomAttackedByCrystalRadius <= 0 || --idleCooldown > 0) {
+            return; // on cooldown
+        }
+
+        if (targetPhantom == null) {
+            for (net.minecraft.world.entity.monster.Phantom phantom : level.getEntitiesOfClass(net.minecraft.world.entity.monster.Phantom.class, getBoundingBox().inflate(level.purpurConfig.phantomAttackedByCrystalRadius))) {
+                if (phantom.hasLineOfSight(this)) {
+                    attackPhantom(phantom);
+                    break;
+                }
+            }
+        } else {
+            setBeamTarget(new BlockPos(targetPhantom).offset(0, -2, 0));
+            if (--phantomBeamTicks > 0 && targetPhantom.isAlive()) {
+                phantomDamageCooldown--;
+                if (targetPhantom.hasLineOfSight(this)) {
+                    if (phantomDamageCooldown <= 0) {
+                        phantomDamageCooldown = 20;
+                        targetPhantom.hurt(DamageSource.indirectMagic(this, this), level.purpurConfig.phantomAttackedByCrystalDamage);
+                    }
+                } else {
+                    forgetPhantom(); // no longer in sight
+                }
+            } else {
+                forgetPhantom(); // attacked long enough
+            }
+        }
+    }
+
+    private void attackPhantom(net.minecraft.world.entity.monster.Phantom phantom) {
+        phantomDamageCooldown = 0;
+        phantomBeamTicks = 60;
+        targetPhantom = phantom;
+    }
+
+    private void forgetPhantom() {
+        targetPhantom = null;
+        setBeamTarget(null);
+        phantomBeamTicks = 0;
+        phantomDamageCooldown = 0;
+        idleCooldown = 60;
     }
+    // Purpur end
 
     @Override
     protected void addAdditionalSaveData(CompoundTag nbt) {
diff --git a/src/main/java/net/minecraft/world/entity/monster/Phantom.java b/src/main/java/net/minecraft/world/entity/monster/Phantom.java
index 5e069ac0c57d2a3a23f6e4483d12ce298d172691..916c29d08fbcf245ad6f50f8e8cc173677b01081 100644
--- a/src/main/java/net/minecraft/world/entity/monster/Phantom.java
+++ b/src/main/java/net/minecraft/world/entity/monster/Phantom.java
@@ -50,6 +50,7 @@ public class Phantom extends FlyingMob implements Enemy {
     Vec3 moveTargetPoint;
     BlockPos anchorPoint;
     Phantom.AttackPhase attackPhase;
+    Vec3 crystalPosition; // Purpur
 
     public Phantom(EntityType<? extends Phantom> type, Level world) {
         super(type, world);
@@ -117,6 +118,24 @@ public class Phantom extends FlyingMob implements Enemy {
     public void initAttributes() {
         this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(this.level.purpurConfig.phantomMaxHealth);
     }
+
+    @Override
+    protected net.minecraft.world.level.storage.loot.LootContext.Builder createLootContext(boolean causedByPlayer, DamageSource source) {
+        boolean dropped = false;
+        if (lastHurtByPlayer == null && source.getEntity() instanceof net.minecraft.world.entity.boss.enderdragon.EndCrystal) {
+            if (random.nextInt(5) < 1) {
+                dropped = spawnAtLocation(new net.minecraft.world.item.ItemStack(net.minecraft.world.item.Items.PHANTOM_MEMBRANE)) != null;
+            }
+        }
+        if (!dropped) {
+            return super.createLootContext(causedByPlayer, source);
+        }
+        return new net.minecraft.world.level.storage.loot.LootContext.Builder((net.minecraft.server.level.ServerLevel) level);
+    }
+
+    public boolean isCirclingCrystal() {
+        return crystalPosition != null;
+    }
     // Purpur end
 
     @Override
@@ -131,11 +150,17 @@ public class Phantom extends FlyingMob implements Enemy {
 
     @Override
     protected void registerGoals() {
-        this.goalSelector.addGoal(0, new net.pl3x.purpur.entity.ai.HasRider(this)); // Purpur
-        this.goalSelector.addGoal(1, new Phantom.PhantomAttackStrategyGoal());
-        this.goalSelector.addGoal(2, new Phantom.PhantomSweepAttackGoal());
-        this.goalSelector.addGoal(3, new Phantom.PhantomCircleAroundAnchorGoal());
-        this.targetSelector.addGoal(0, new net.pl3x.purpur.entity.ai.HasRider(this)); // Purpur
+        // Purpur start
+        this.goalSelector.addGoal(0, new net.pl3x.purpur.entity.ai.HasRider(this));
+        if (level.purpurConfig.phantomOrbitCrystalRadius > 0) {
+            this.goalSelector.addGoal(1, new FindCrystalGoal(this));
+            this.goalSelector.addGoal(2, new OrbitCrystalGoal(this));
+        }
+        this.goalSelector.addGoal(3, new Phantom.PhantomAttackStrategyGoal());
+        this.goalSelector.addGoal(4, new Phantom.PhantomSweepAttackGoal());
+        this.goalSelector.addGoal(5, new Phantom.PhantomCircleAroundAnchorGoal());
+        this.targetSelector.addGoal(0, new net.pl3x.purpur.entity.ai.HasRider(this));
+        // Purpur end
         this.targetSelector.addGoal(1, new Phantom.PhantomAttackPlayerTargetGoal());
     }
 
@@ -325,6 +350,124 @@ public class Phantom extends FlyingMob implements Enemy {
         private AttackPhase() {}
     }
 
+    // Purpur start
+    class FindCrystalGoal extends Goal {
+        private final Phantom phantom;
+        private net.minecraft.world.entity.boss.enderdragon.EndCrystal crystal;
+        private Comparator<net.minecraft.world.entity.boss.enderdragon.EndCrystal> comparator;
+
+        FindCrystalGoal(Phantom phantom) {
+            this.phantom = phantom;
+            this.comparator = Comparator.comparingDouble(phantom::distanceToSqr);
+            this.setFlags(EnumSet.of(Flag.LOOK));
+        }
+
+        @Override
+        public boolean canUse() {
+            double range = maxTargetRange();
+            List<net.minecraft.world.entity.boss.enderdragon.EndCrystal> crystals = level.getEntitiesOfClass(net.minecraft.world.entity.boss.enderdragon.EndCrystal.class, phantom.getBoundingBox().inflate(range));
+            if (crystals.isEmpty()) {
+                return false;
+            }
+            crystals.sort(comparator);
+            crystal = crystals.get(0);
+            if (phantom.distanceToSqr(crystal) > range * range) {
+                crystal = null;
+                return false;
+            }
+            return true;
+        }
+
+        @Override
+        public boolean canContinueToUse() {
+            if (crystal == null || !crystal.isAlive()) {
+                return false;
+            }
+            double range = maxTargetRange();
+            return phantom.distanceToSqr(crystal) <= (range * range) * 2;
+        }
+
+        @Override
+        public void start() {
+            phantom.crystalPosition = new Vec3(crystal.getX(), crystal.getY() + (phantom.random.nextInt(10) + 10), crystal.getZ());
+        }
+
+        @Override
+        public void stop() {
+            crystal = null;
+            phantom.crystalPosition = null;
+            super.stop();
+        }
+
+        private double maxTargetRange() {
+            return phantom.level.purpurConfig.phantomOrbitCrystalRadius;
+        }
+    }
+
+    class OrbitCrystalGoal extends Goal {
+        private final Phantom phantom;
+        private float offset;
+        private float radius;
+        private float verticalChange;
+        private float direction;
+
+        OrbitCrystalGoal(Phantom phantom) {
+            this.phantom = phantom;
+            this.setFlags(EnumSet.of(Flag.MOVE));
+        }
+
+        @Override
+        public boolean canUse() {
+            return phantom.isCirclingCrystal();
+        }
+
+        @Override
+        public void start() {
+            this.radius = 5.0F + phantom.random.nextFloat() * 10.0F;
+            this.verticalChange = -4.0F + phantom.random.nextFloat() * 9.0F;
+            this.direction = phantom.random.nextBoolean() ? 1.0F : -1.0F;
+            updateOffset();
+        }
+
+        @Override
+        public void tick() {
+            if (phantom.random.nextInt(350) == 0) {
+                this.verticalChange = -4.0F + phantom.random.nextFloat() * 9.0F;
+            }
+            if (phantom.random.nextInt(250) == 0) {
+                ++this.radius;
+                if (this.radius > 15.0F) {
+                    this.radius = 5.0F;
+                    this.direction = -this.direction;
+                }
+            }
+            if (phantom.random.nextInt(450) == 0) {
+                this.offset = phantom.random.nextFloat() * 2.0F * 3.1415927F;
+                updateOffset();
+            }
+            if (phantom.moveTargetPoint.distanceToSqr(phantom.getX(), phantom.getY(), phantom.getZ()) < 4.0D) {
+                updateOffset();
+            }
+            if (phantom.moveTargetPoint.y < phantom.getY() && !phantom.level.isEmptyBlock(new BlockPos(phantom).below(1))) {
+                this.verticalChange = Math.max(1.0F, this.verticalChange);
+                updateOffset();
+            }
+            if (phantom.moveTargetPoint.y > phantom.getY() && !phantom.level.isEmptyBlock(new BlockPos(phantom).above(1))) {
+                this.verticalChange = Math.min(-1.0F, this.verticalChange);
+                updateOffset();
+            }
+        }
+
+        private void updateOffset() {
+            this.offset += this.direction * 15.0F * 0.017453292F;
+            phantom.moveTargetPoint = phantom.crystalPosition.add(
+                    this.radius * Mth.cos(this.offset),
+                    -4.0F + this.verticalChange,
+                    this.radius * Mth.sin(this.offset));
+        }
+    }
+    // Purpur end
+
     private class PhantomMoveControl extends net.pl3x.purpur.controller.FlyingMoveControllerWASD { // Purpur
 
         private float speed = 0.1F;
diff --git a/src/main/java/net/pl3x/purpur/PurpurWorldConfig.java b/src/main/java/net/pl3x/purpur/PurpurWorldConfig.java
index d2754ceb0d87dd844a5e325ee5a89b4e586f2592..62c257b28d921d36198f925170a9ea01ec2c63be 100644
--- a/src/main/java/net/pl3x/purpur/PurpurWorldConfig.java
+++ b/src/main/java/net/pl3x/purpur/PurpurWorldConfig.java
@@ -938,6 +938,9 @@ public class PurpurWorldConfig {
     public int phantomFlameFireTime = 8;
     public boolean phantomAllowGriefing = false;
     public double phantomMaxHealth = 20.0D;
+    public double phantomAttackedByCrystalRadius = 0.0D;
+    public float phantomAttackedByCrystalDamage = 1.0F;
+    public double phantomOrbitCrystalRadius = 0.0D;
     private void phantomSettings() {
         phantomRidable = getBoolean("mobs.phantom.ridable", phantomRidable);
         phantomRidableInWater = getBoolean("mobs.phantom.ridable-in-water", phantomRidableInWater);
@@ -951,6 +954,9 @@ public class PurpurWorldConfig {
             set("mobs.phantom.attributes.max_health", oldValue);
         }
         phantomMaxHealth = getDouble("mobs.phantom.attributes.max_health", phantomMaxHealth);
+        phantomAttackedByCrystalRadius = getDouble("mobs.phantom.attacked-by-crystal-range", phantomAttackedByCrystalRadius);
+        phantomAttackedByCrystalDamage = (float) getDouble("mobs.phantom.attacked-by-crystal-damage", phantomAttackedByCrystalDamage);
+        phantomOrbitCrystalRadius = getDouble("mobs.phantom.orbit-crystal-radius", phantomOrbitCrystalRadius);
     }
 
     public boolean pigRidable = false;
