From 4c269d96022f05e353fc3253e38a4485d5675ac3 Mon Sep 17 00:00:00 2001 From: Virus5600 Date: Mon, 13 Jan 2025 02:47:24 +0800 Subject: [PATCH] Refactor 20250113 - Class Refactorization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Another refactorization commit to properly structure the classes of projectiles. Also, parabolic trajectory is currently being experimented on for potential future implementation of such turrets like mortars. [CHANGELOG] 🟢 Added the Explosive Projectile interface. This interface will handle all explosive projectiles similar to how Explosive Projectile Entity class is. This interface will soon replace the said superclass in Cannonball entity class. 🟢 Added Kinetic Projectile base class for all kinetic-base projectiles such as the Ballista Arrow and even (WIP) Cannonball. 🟢 Added several new properties for Turret Projectile, allowing its subclasses to let their damage be affected by their speed. 🟢 Added new damage and pierce level static constants to the Ballista Turret entity class. 🟢 Added new damage static constants to the Cannon Turret entity class. 🟢 Implemented the two new additional abstract methods from Turret Entity base class to all its subclasses. 🟢 Added a default (base) level to Turret Entity base class's level property. 🟢 Added a new method that returns the current barrel the turret is using. 🟢 Added safety clamps to some data to prevent any error from happening. 🟢 Added an overridable method "getMaxLevel" which dictates the maximum level a turret can be. 🟢 Refactored the Turret Projectile Velocity class to allow recalculation of velocity when some attributes are changed. 🟢 Began experimentation for the implementation of parabolic projectiles. If successful, implementations of turrets like mortars may proceed. 🟡 Updated Ballista Arrow and Cannonball entity's superclass to Kinetic Projectile, allowing it to remove some redundant codes. 🟡 Moved some repeating logics and codes from the projectile entities towards the Turret Projectile entity base class. 🟡 Updated Cannon Turret's max attack and follow range from 16 to 24. 🟡 Updated the public method "getMaxAttackRange" which previously limits the range of the turrets to 16.625. It now uses the subclass's "FOLLOW_RANGE" attribute as its maximum attack range + its eye height. 🟡 Updated the Turret Projectile Velocity class to normalize its values and allow developers to easily comprehend some values such as power and upward velocity multiplier. --- .../projectiles/BallistaArrowEntity.java | 72 +---- .../entity/projectiles/BulletEntity.java | 8 +- .../projectiles/KineticProjectileEntity.java | 145 ++++++++++ .../projectiles/TurretProjectileEntity.java | 74 +++-- .../interfaces/ExplosiveProjectile.java | 4 + .../entity/turrets/BallistaTurretEntity.java | 36 ++- .../entity/turrets/CannonTurretEntity.java | 19 +- .../entity/turrets/TurretEntity.java | 252 +++++++++++++++--- .../turrets/ballista/BallistaTurretItem.java | 6 +- .../item/turrets/cannon/CannonTurretItem.java | 5 +- src/main/resources/assets/dm/lang/en_us.json | 3 +- 11 files changed, 489 insertions(+), 135 deletions(-) create mode 100644 src/main/java/com/virus5600/defensive_measures/entity/projectiles/KineticProjectileEntity.java create mode 100644 src/main/java/com/virus5600/defensive_measures/entity/projectiles/interfaces/ExplosiveProjectile.java diff --git a/src/main/java/com/virus5600/defensive_measures/entity/projectiles/BallistaArrowEntity.java b/src/main/java/com/virus5600/defensive_measures/entity/projectiles/BallistaArrowEntity.java index 4bd9571..ed06d70 100644 --- a/src/main/java/com/virus5600/defensive_measures/entity/projectiles/BallistaArrowEntity.java +++ b/src/main/java/com/virus5600/defensive_measures/entity/projectiles/BallistaArrowEntity.java @@ -1,33 +1,26 @@ package com.virus5600.defensive_measures.entity.projectiles; -import net.minecraft.entity.Entity; import net.minecraft.entity.EntityType; import net.minecraft.entity.LivingEntity; import net.minecraft.entity.data.DataTracker.Builder; -import net.minecraft.util.hit.EntityHitResult; import net.minecraft.world.World; -import software.bernie.geckolib.animatable.GeoEntity; -import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; import software.bernie.geckolib.animation.AnimatableManager.ControllerRegistrar; import software.bernie.geckolib.animation.AnimationController; import software.bernie.geckolib.animation.AnimationState; import software.bernie.geckolib.animation.PlayState; import software.bernie.geckolib.animation.RawAnimation; -import software.bernie.geckolib.util.GeckoLibUtil; import com.virus5600.defensive_measures.entity.ModEntities; import java.util.Map; -public class BallistaArrowEntity extends TurretProjectileEntity implements GeoEntity { +public class BallistaArrowEntity extends KineticProjectileEntity { private static final Map ANIMATIONS; - private final AnimatableInstanceCache geoCache = GeckoLibUtil.createInstanceCache(this); - - //////////////////// - /// CONSTRUCTORS /// - //////////////////// + // ////////////// // + // CONSTRUCTORS // + // ////////////// // public BallistaArrowEntity( EntityType entityType, World world @@ -35,11 +28,12 @@ public BallistaArrowEntity( super(entityType, world); this.setDamage(4); - this.setPierceLevel(this.getMaxPierceLevel()); } public BallistaArrowEntity(World world, LivingEntity owner) { this(ModEntities.BALLISTA_ARROW, world); + + this.setPierceLevel(this.getMaxPierceLevel()); this.setOwner(owner); } @@ -64,55 +58,6 @@ protected void initDataTracker(Builder builder) { } @Override - protected void onEntityHit(EntityHitResult entityHitResult) { - super.onEntityHit(entityHitResult); - - // Reduces the speed of the arrow when it hits an entity - Entity entity = entityHitResult.getEntity(); - // For reference, max armor points is 30 - int armor = entity instanceof LivingEntity livingEntity ? livingEntity.getArmor() : 0; - boolean hasHeavyArmor = armor > 15, - hasLightArmor = armor <= 15 && armor > 0; - - if (entity instanceof LivingEntity livingEntity) { - double targetH = livingEntity.getHeight(), - arrowH = this.getHeight(), - variance = arrowH * 0.125; // 12.5% of the arrow's height - - double arrowMin = arrowH - variance, - arrowMax = arrowH + variance, - reducedVelocity = 0.125; - - // Reduce velocity and pierce level based on the side of the entity hit - if (this.getPierceLevel() > 0) { - // If the arrow is smaller than the target or has heavy armor, reduce the pierce level by 2 and velocity by 50% - if (targetH > arrowMax || hasHeavyArmor) { - this.setPierceLevel((byte) (this.getPierceLevel() - 2)); - reducedVelocity = 0.5; - } - // If the arrow is almost the same size as the target or has a light armor, reduce the pierce level by 1 and velocity by 25% - else if ((targetH < arrowMax && arrowMin < targetH) || hasLightArmor) { - this.setPierceLevel((byte) (this.getPierceLevel() - 1)); - reducedVelocity = 0.25; - } - // Otherwise, just reduce the velocity by 12.5% without reducing the pierce level - } - - // Apply reduced velocity - this.addVelocity( - this.getVelocity() - .multiply(reducedVelocity) - .negate() - ); - } - - this.addVelocity( - this.getVelocity() - .negate() - .multiply(0.1) - ); - } - public byte getMaxPierceLevel() { return 5; } @@ -146,11 +91,6 @@ public void registerControllers(final ControllerRegistrar controllers) { ); } - @Override - public AnimatableInstanceCache getAnimatableInstanceCache() { - return this.geoCache; - } - /////////////////////// // STATIC INITIALIZE // /////////////////////// diff --git a/src/main/java/com/virus5600/defensive_measures/entity/projectiles/BulletEntity.java b/src/main/java/com/virus5600/defensive_measures/entity/projectiles/BulletEntity.java index 5fb4860..9fc3f10 100644 --- a/src/main/java/com/virus5600/defensive_measures/entity/projectiles/BulletEntity.java +++ b/src/main/java/com/virus5600/defensive_measures/entity/projectiles/BulletEntity.java @@ -4,22 +4,16 @@ import net.minecraft.entity.LivingEntity; import net.minecraft.entity.data.DataTracker; import net.minecraft.entity.data.DataTracker.Builder; -import net.minecraft.entity.data.TrackedData; -import net.minecraft.entity.data.TrackedDataHandlerRegistry; import net.minecraft.item.ItemStack; import net.minecraft.world.World; -import software.bernie.geckolib.animatable.GeoEntity; import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; import software.bernie.geckolib.animation.AnimatableManager; -import software.bernie.geckolib.animation.AnimationController; import software.bernie.geckolib.util.GeckoLibUtil; import com.virus5600.defensive_measures.entity.ModEntities; -public class BulletEntity extends TurretProjectileEntity implements GeoEntity { - protected static final TrackedData PIERCE_LEVEL = DataTracker.registerData(BulletEntity.class, TrackedDataHandlerRegistry.BYTE); - +public class BulletEntity extends TurretProjectileEntity { private final AnimatableInstanceCache geoCache = GeckoLibUtil.createInstanceCache(this); //////////////////// diff --git a/src/main/java/com/virus5600/defensive_measures/entity/projectiles/KineticProjectileEntity.java b/src/main/java/com/virus5600/defensive_measures/entity/projectiles/KineticProjectileEntity.java new file mode 100644 index 0000000..dd0c393 --- /dev/null +++ b/src/main/java/com/virus5600/defensive_measures/entity/projectiles/KineticProjectileEntity.java @@ -0,0 +1,145 @@ +package com.virus5600.defensive_measures.entity.projectiles; + +import net.minecraft.entity.Entity; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.data.DataTracker.Builder; +import net.minecraft.entity.EntityType; +import net.minecraft.util.hit.EntityHitResult; +import net.minecraft.world.World; + +public abstract class KineticProjectileEntity extends TurretProjectileEntity { + + // ///////////// // + // CONSTRUCTORS // + // ///////////// // + public KineticProjectileEntity(EntityType entityType, World world) { + super(entityType, world); + this.setDamage(4); + this.setPierceLevel(this.getMaxPierceLevel()); + this.setSpeedAffectsDamage(false); + } + + // /////////////// // + // PROCESS METHODS // + // /////////////// // + // PROTECTED + @Override + protected void initDataTracker(Builder builder) { + super.initDataTracker(builder); + } + + /** + * {@inheritDoc} + *
+ *

{@link KineticProjectileEntity}

+ *

+ * The method in {@code KineticProjectileEntity} reduces the speed of the arrow + * along with the pierce level based on the amount of armor points the entity has. + *

+ * In this implementation, the projectile will have the following behavior + * (assuming the projectile is affected by armor points): + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Armor PointsPierce Level ReductionVelocity ReductionBase Damage Reduction
Heavy Armor250%50%
Light Armor125%20%
No ArmorN/A12.5%5%
+ * + * @param entityHitResult {@inheritDoc EntityHitResult} + */ + @Override + protected void onEntityHit(EntityHitResult entityHitResult) { + super.onEntityHit(entityHitResult); + + if (this.armorAffectsPiercing()) { + // Reduces the speed of the arrow when it hits an entity + Entity entity = entityHitResult.getEntity(); + // For reference, max armor points is 30 + int armor = entity instanceof LivingEntity livingEntity ? livingEntity.getArmor() : 0; + double dmgReduction = 0.05; + boolean hasHeavyArmor = armor > 15, + hasLightArmor = armor <= 15 && armor > 0; + + if (entity instanceof LivingEntity livingEntity) { + double targetH = livingEntity.getHeight(), + arrowH = this.getHeight(), + variance = arrowH * 0.125; // 12.5% of the arrow's height + + double arrowMin = arrowH - variance, + arrowMax = arrowH + variance, + reducedVelocity = 0.125; + + // Reduce velocity and pierce level based on the side of the entity hit + if (this.getPierceLevel() > 0) { + // If the arrow is smaller than the target or has heavy armor, reduce the pierce level by 2, velocity by 50%, and base damage by 50% + if (targetH > arrowMax || hasHeavyArmor) { + this.setPierceLevel((byte) (this.getPierceLevel() - 2)); + reducedVelocity = 0.5; + dmgReduction = 0.5; + } + // If the arrow is almost the same size as the target or has a light armor, reduce the pierce level by 1, velocity by 25%, and base damage by 20% + else if ((targetH < arrowMax && arrowMin < targetH) || hasLightArmor) { + this.setPierceLevel((byte) (this.getPierceLevel() - 1)); + reducedVelocity = 0.25; + dmgReduction = 0.2; + } + // Otherwise, just reduce the velocity by 12.5% and base damage by 5% without reducing the pierce level + } + + // Apply reduced velocity + this.addVelocity( + this.getVelocity() + .multiply(reducedVelocity) + .negate() + ); + + // Apply reduced damage + this.setDamage(this.getDamage() * (1 - dmgReduction)); + } + + this.addVelocity( + this.getVelocity() + .negate() + .multiply(0.1) + ); + } + } + + // /////////////////////////////// // + // GETTERS, SETTERS, AND QUESTIONS // + // /////////////////////////////// // + + // PUBLIC + /** + * An overridable method that determines whether an armor point affects the + * piercing of the projectile. + *

+ * By default, this method returns {@code true}. + * + * @return {@code true} if the armor point affects the piercing of the projectile, + * {@code false} otherwise. + */ + public boolean armorAffectsPiercing() { + return true; + } +} diff --git a/src/main/java/com/virus5600/defensive_measures/entity/projectiles/TurretProjectileEntity.java b/src/main/java/com/virus5600/defensive_measures/entity/projectiles/TurretProjectileEntity.java index 06193dd..a4e8581 100644 --- a/src/main/java/com/virus5600/defensive_measures/entity/projectiles/TurretProjectileEntity.java +++ b/src/main/java/com/virus5600/defensive_measures/entity/projectiles/TurretProjectileEntity.java @@ -1,5 +1,6 @@ package com.virus5600.defensive_measures.entity.projectiles; +import com.virus5600.defensive_measures.entity.turrets.TurretEntity; import net.minecraft.block.BlockState; import net.minecraft.block.Blocks; import net.minecraft.component.DataComponentTypes; @@ -46,11 +47,11 @@ import org.jetbrains.annotations.Nullable; -import java.util.List; import java.util.Objects; -import com.google.common.collect.Lists; import it.unimi.dsi.fastutil.ints.IntOpenHashSet; +import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache; +import software.bernie.geckolib.util.GeckoLibUtil; /** * {@code TurretProjectile} is an abstract class that acts nearly akin to {@link PersistentProjectileEntity} @@ -67,15 +68,13 @@ public abstract class TurretProjectileEntity extends ProjectileEntity implements protected static final int CRITICAL_FLAG = 1; protected static final int NO_CLIP_FLAG = 2; + private final AnimatableInstanceCache geoCache = GeckoLibUtil.createInstanceCache(this); /** Holds the blockstate information of the block in this projectile's position. */ @Nullable private BlockState inBlockState; /** Holds the information about the list of entities this projectile have pierced through. */ @Nullable private IntOpenHashSet piercedEntities; - /** Holds the information about the list of entities this projectile have pierced through and died. */ - @Nullable - private List piercingKilledEntities; /** Defines the current sound this projectile will play when it hit something */ private SoundEvent sound = this.getHitSound(); /** Defines the item stack this projectile is. Used when picking up the projectile to turn into items. */ @@ -147,6 +146,22 @@ protected void initDataTracker(Builder builder) { // /////////////// // // PROCESS METHODS // // /////////////// // + + /** + *

{@link ProjectileEntity}

+ * {@inheritDoc} + *
+ *

{@link TurretProjectileEntity}

+ *

+ * This method is called when the projectile hits an entity, handling the logic + * for hitting, damaging, and if applicable, piercing the entity. + *

+ * In this implementation, the method will handle the damage calculation, the + * critical damage multiplier, the pierce behavior, and the fire behavior similar + * to how the {@link PersistentProjectileEntity} handles it. + * + * @param entityHitResult {@inheritDoc EntityHitResult} + */ @Override protected void onEntityHit(EntityHitResult entityHitResult) { super.onEntityHit(entityHitResult); @@ -159,7 +174,7 @@ protected void onEntityHit(EntityHitResult entityHitResult) { float velocityMagnitude = (float) this.getVelocity().length(); double damage = this.getDamage(); int damageToDeal = MathHelper.ceil( - this.speedAffectDamage ? + this.speedAffectsDamage() ? MathHelper.clamp( (double) velocityMagnitude * damage, 0.0, @@ -173,10 +188,6 @@ protected void onEntityHit(EntityHitResult entityHitResult) { this.piercedEntities = new IntOpenHashSet(this.getMaxPierceLevel()); } - if (this.piercingKilledEntities == null) { - this.piercingKilledEntities = Lists.newArrayListWithCapacity(this.getMaxPierceLevel()); - } - if (this.piercedEntities.size() >= this.getPierceLevel() + 1) { this.discard(); return; @@ -213,9 +224,6 @@ protected void onEntityHit(EntityHitResult entityHitResult) { } this.onHit(livingEntity); - if (!hitEntity.isAlive() && this.piercingKilledEntities != null) { - this.piercingKilledEntities.add(livingEntity); - } } this.playSound(this.sound , 1f, 1.2f / (this.random.nextFloat() * 0.2f + 0.9f)); @@ -359,10 +367,6 @@ protected void age() { } protected void clearPiercingStatus() { - if (this.piercingKilledEntities != null) { - this.piercingKilledEntities.clear(); - } - if (this.piercedEntities != null) { this.piercedEntities.clear(); } @@ -584,6 +588,22 @@ public void setSound(SoundEvent sound) { this.sound = sound; } + /** + * Determines whether the speed of the projectile affects the damage it deals, + * similar to how {@link PersistentProjectileEntity} handles it. + * + * @return {@code true} if the speed affects the damage, {@code false} otherwise + * + * @see #speedAffectDamage + */ + public boolean speedAffectsDamage() { + return this.speedAffectDamage; + } + + protected void setSpeedAffectsDamage(boolean speedAffectDamage) { + this.speedAffectDamage = speedAffectDamage; + } + @Override protected double getGravity() { return 0.05; @@ -757,6 +777,26 @@ protected boolean deflectsAgainstWorldBorder() { return true; } + @Override + public void setOwner(Entity owner) { + super.setOwner(owner); + + if (owner instanceof TurretEntity turret) { + this.setDamage(turret.getProjectileDamage()); + this.setPierceLevel(turret.getProjectilePierceLevel()); + } + } + + // ///////////////////////// // + // INTERFACE IMPLEMENTATIONS // + // ///////////////////////// // + + // GeoEntity // + @Override + public AnimatableInstanceCache getAnimatableInstanceCache() { + return this.geoCache; + } + // ///////////////////// // // LOCAL CLASS AND ENUMS // // ///////////////////// // diff --git a/src/main/java/com/virus5600/defensive_measures/entity/projectiles/interfaces/ExplosiveProjectile.java b/src/main/java/com/virus5600/defensive_measures/entity/projectiles/interfaces/ExplosiveProjectile.java new file mode 100644 index 0000000..1c1bb28 --- /dev/null +++ b/src/main/java/com/virus5600/defensive_measures/entity/projectiles/interfaces/ExplosiveProjectile.java @@ -0,0 +1,4 @@ +package com.virus5600.defensive_measures.entity.projectiles.interfaces; + +public interface ExplosiveProjectile { +} diff --git a/src/main/java/com/virus5600/defensive_measures/entity/turrets/BallistaTurretEntity.java b/src/main/java/com/virus5600/defensive_measures/entity/turrets/BallistaTurretEntity.java index ba427c5..76fd299 100644 --- a/src/main/java/com/virus5600/defensive_measures/entity/turrets/BallistaTurretEntity.java +++ b/src/main/java/com/virus5600/defensive_measures/entity/turrets/BallistaTurretEntity.java @@ -49,6 +49,9 @@ public class BallistaTurretEntity extends TurretEntity implements GeoEntity { private static final Map ANIMATIONS; private static final Map> OFFSETS; private static final Map HEAL_SOUNDS; + private static final double[] DAMAGE; + private static final byte[] PIERCE_LEVELS; + /** * Contains all the items that can heal this entity. @@ -98,9 +101,9 @@ public static DefaultAttributeContainer.Builder setAttributes() { return TurretEntity.setAttributes(); } - ///////////////////// + // /////////////// // // PROCESS METHODS // - ///////////////////// + // /////////////// // @Override public void shootAt(LivingEntity target, float pullProgress) { @@ -168,9 +171,9 @@ public SoundEvent getEntityRemoveSound() { return ModSoundEvents.TURRET_REMOVED_WOOD; } - /////////////////////////// + // ///////////////////// // // ANIMATION CONTROLLERS // - /////////////////////////// + // ///////////////////// // private PlayState deathController(final AnimationState event) { if (!this.isAlive() && !this.animPlayed) { @@ -211,13 +214,22 @@ public AnimatableInstanceCache getAnimatableInstanceCache() { return this.geoCache; } - ////////////////////////////// + // //////////////////////// // // ABSTRACT IMPLEMENTATIONS // - ////////////////////////////// + // //////////////////////// // + // TurretEntity // protected List getTurretProjectileSpawn() { return OFFSETS.get(Offsets.BOLT_HOLDER); } + public double getProjectileDamage() { + return BallistaTurretEntity.DAMAGE[this.getLevel() - 1]; + } + + public byte getProjectilePierceLevel() { + return BallistaTurretEntity.PIERCE_LEVELS[this.getLevel() - 1]; + } + ///////////////////////// // LOCAL CLASSES/ENUMS // ///////////////////////// @@ -230,6 +242,18 @@ public enum Offsets { /////////////////////// static { + DAMAGE = new double[] { + 3.5, + 7.0, + 12.0 + }; + + PIERCE_LEVELS = new byte[] { + 5, + 7, + 10 + }; + OFFSETS = Map.of( Offsets.BOLT_HOLDER, List.of( new Vec3d(0, 0, 0.875) diff --git a/src/main/java/com/virus5600/defensive_measures/entity/turrets/CannonTurretEntity.java b/src/main/java/com/virus5600/defensive_measures/entity/turrets/CannonTurretEntity.java index c03e432..0b1e708 100644 --- a/src/main/java/com/virus5600/defensive_measures/entity/turrets/CannonTurretEntity.java +++ b/src/main/java/com/virus5600/defensive_measures/entity/turrets/CannonTurretEntity.java @@ -56,6 +56,7 @@ public class CannonTurretEntity extends TurretEntity implements GeoEntity { private final AnimatableInstanceCache geoCache = GeckoLibUtil.createInstanceCache(this); private boolean animPlayed = false; + private static final double[] DAMAGE; ////////////////// // CONSTRUCTORS // @@ -91,6 +92,7 @@ public static DefaultAttributeContainer.Builder setAttributes() { TurretEntity.setTurretMaxHealth(50); return TurretEntity.setAttributes() + .add(EntityAttributes.FOLLOW_RANGE, 24) .add(EntityAttributes.ARMOR, 3) .add(EntityAttributes.ARMOR_TOUGHNESS, 2); } @@ -231,13 +233,22 @@ public AnimatableInstanceCache getAnimatableInstanceCache() { return this.geoCache; } - ////////////////////////////// + // //////////////////////// // // ABSTRACT IMPLEMENTATIONS // - ////////////////////////////// + // //////////////////////// // + // TurretEntity // protected List getTurretProjectileSpawn() { return OFFSETS.get(Offsets.BARREL); } + public double getProjectileDamage() { + return CannonTurretEntity.DAMAGE[this.getLevel() - 1]; + } + + public byte getProjectilePierceLevel() { + return 0; + } + ///////////////////////// // LOCAL CLASSES/ENUMS // ///////////////////////// @@ -250,6 +261,10 @@ public enum Offsets { /////////////////////// static { + DAMAGE = new double[] { + 10.0 + }; + OFFSETS = Map.of( Offsets.BARREL, List.of( new Vec3d(0, 0, 0.875) diff --git a/src/main/java/com/virus5600/defensive_measures/entity/turrets/TurretEntity.java b/src/main/java/com/virus5600/defensive_measures/entity/turrets/TurretEntity.java index b765925..0147920 100644 --- a/src/main/java/com/virus5600/defensive_measures/entity/turrets/TurretEntity.java +++ b/src/main/java/com/virus5600/defensive_measures/entity/turrets/TurretEntity.java @@ -182,7 +182,7 @@ public abstract class TurretEntity extends MobEntity implements Itemable, Ranged * Defines the level of this turret. The higher the level, the stronger * the turret. */ - protected int level; + protected int level = 1; /** * The material of this turret. */ @@ -261,7 +261,9 @@ public abstract class TurretEntity extends MobEntity implements Itemable, Ranged * @see #itemable * @see EntityType */ - public TurretEntity(EntityType entityType, World world, TurretMaterial material, EntityType projectile, Item itemable) { + public TurretEntity(EntityType entityType, World world, + TurretMaterial material, EntityType projectile, Item itemable + ) { this(entityType, world, material, itemable); this.projectile = projectile; } @@ -278,7 +280,9 @@ public TurretEntity(EntityType entityType, World world, Tur * * @see #itemable */ - public TurretEntity(EntityType entityType, World world, TurretMaterial material, Item itemable) { + public TurretEntity(EntityType entityType, World world, + TurretMaterial material, Item itemable + ) { super(entityType, world); this.material = material; @@ -450,14 +454,12 @@ protected void initDataTracker(DataTracker.Builder builder) { * @see DefaultAttributeContainer.Builder */ public static DefaultAttributeContainer.Builder setAttributes() { - DefaultAttributeContainer.Builder builder = TurretEntity.createMobAttributes() + return TurretEntity.createMobAttributes() .add(EntityAttributes.MAX_HEALTH, TurretEntity.getTurretMaxHealth()) .add(EntityAttributes.FOLLOW_RANGE, 16) .add(EntityAttributes.MOVEMENT_SPEED, 0) .add(EntityAttributes.KNOCKBACK_RESISTANCE, 1.0) .add(EntityAttributes.EXPLOSION_KNOCKBACK_RESISTANCE, 1.0); - - return builder; } @Override @@ -1207,23 +1209,77 @@ public List getMobEffect(Item item) { return this.isEffectSource(item) ? this.effectSource.get(item) : List.of(); } + /** + * Retrieves the maximum attack range of this turret. The max range is + * dependent on the {@code FOLLOW_RANGE} attribute value, allowing the + * turret to have a range that is within the bounds of its sight. + *

+ * The maximum attack range is calculated with the formula: + *

+	 * 	Math.floor(FOLLOW_RANGE) + eyeHeight
+	 * 
+ * The additional {@code eyeHeight} is added to the {@code FOLLOW_RANGE} value + * to account for the eye displacement used by the attack goal when targeting + * entities. + * + * @return {@code float} The maximum attack range of this turret. + * + * @see EntityAttributes#FOLLOW_RANGE + */ public float getMaxAttackRange() { - return 16.625f; + return (float) Math.floor( + this.getAttributes() + .getBaseValue(EntityAttributes.FOLLOW_RANGE) + ) + this.getStandingEyeHeight(); } public float getMinAttackRange() { return 0.1f; } + /** + * Retrieves the current barrel this turret will use to shoot. + * + * @param increment Whether to increment the barrel or not. + * + * @return {@code Vec3d} The current barrel to use for shooting. + */ + public Vec3d getCurrentBarrel(boolean increment) { + int currentBarrel = this.currentBarrel; + + if (increment) { + this.currentBarrel = this.currentBarrel >= this.getTurretProjectileSpawn().size() - 1 ? + 0 : this.currentBarrel + 1; + } + + return this.getTurretProjectileSpawn() + .get(currentBarrel); + } + ////////////////// // TRACKED DATA // ////////////////// + /** + * Retrieves the maximum level of this turret. + *

+ * The default maximum level is 3. This can value can be changed + * by overriding this method and returning a different value. + *

+ * This method is also used by {@link #getLevel()} and {@link #setLevel(int)} + * to clamp the level between 1 and the maximum level, ensuring that the + * level is within the bounds of the maximum level. + * + * @return {@code int} The maximum level of this turret. + */ + public int getMaxLevel() { + return 3; + } public int getLevel() { - return this.dataTracker.get(LEVEL); + return Math.clamp(this.dataTracker.get(LEVEL), 1, this.getMaxLevel()); } public void setLevel(int level) { - this.dataTracker.set(LEVEL, level); + this.dataTracker.set(LEVEL, Math.clamp(level, 1, this.getMaxLevel())); } /** @@ -1289,6 +1345,24 @@ public void shootAt(LivingEntity target, float pullProgress) { */ abstract List getTurretProjectileSpawn(); + /** + * Determines the damage this turret's projectile will deal. This only + * works for instances of {@link com.virus5600.defensive_measures.entity.projectiles.TurretProjectileEntity TurretProjectileEntity} + * or its subclasses due to the overridden methods and tailored behavior. + * + * @return {@code double} The damage this turret's projectile will deal. + */ + public abstract double getProjectileDamage(); + + /** + * Determines the knockback this turret's projectile will deal. This only + * works for instances of {@link com.virus5600.defensive_measures.entity.projectiles.TurretProjectileEntity TurretProjectileEntity} + * or its subclasses due to the overridden methods and tailored behavior. + * + * @return {@code byte} The projectile's pierce level. + */ + public abstract byte getProjectilePierceLevel(); + // OVERRIDABLES // /** * Sets the velocity of a projectile, along with the power and uncertainty of the projectile. @@ -1334,8 +1408,8 @@ protected void setProjectileVelocity(ProjectileEntity projectile, TurretProjecti projectile.setVelocity( vx, vy, vz, - velocityData.power, - velocityData.uncertainty + velocityData.getPower(), + velocityData.getUncertainty() ); } @@ -1363,20 +1437,13 @@ protected void shoot(TurretProjectileVelocity velocityData) { return; } - Vec3d pos = this.getRelativePos( - this.getTurretProjectileSpawn() - .get(this.currentBarrel++ % this.barrels) - ); + Vec3d pos = this.getRelativePos(this.getCurrentBarrel(true)); projectile.setPosition(pos); projectile.setOwner(this); this.setProjectileVelocity(projectile, velocityData); this.playSound(this.getShootSound(), 1.0f, 1.0f / (this.getRandom().nextFloat() * 0.4f + 0.8f)); this.getWorld().spawnEntity(projectile); - - // Resets the current barrel to 0 if it exceeds the number of barrels to prevent overflow - if (this.currentBarrel % this.barrels == 0) - this.currentBarrel = 0; } catch (IllegalArgumentException | SecurityException e) { DefensiveMeasures.printErr(e); } @@ -1494,6 +1561,16 @@ protected void clampHeadYaw() { * respective getters. */ public static class TurretProjectileVelocity { + /** + * Holds the data of the last target position of the projectile. This is used to hold + * the last position of the turret's target so when some information is updated, the + * projectile velocity can be recalculated. + *

+ * When the velocity is updated, this value is also updated to the target's position. If + * the velocity update does not include a target, then this will be set to {@code null}. + */ + @Nullable + private Vec3d lastTargetPos; /** The turret entity that will be shooting the projectile */ private final TurretEntity turret; /** The power of the projectile's velocity */ @@ -1504,6 +1581,8 @@ public static class TurretProjectileVelocity { private float upwardVelocityMultiplier; /** The velocity of the projectile */ private Vec3d velocity; + /** A flag that determines whether the projectile's trajectory is parabolic */ + private boolean isParabolic; /** * Initializes the velocity data of the turret's projectile. This can only be initialized @@ -1513,9 +1592,9 @@ public static class TurretProjectileVelocity { */ private TurretProjectileVelocity(TurretEntity turret) { this.turret = turret; - this.power = 1.5f; + this.power = 1f; this.uncertainty = turret.getWorld().getDifficulty().getId() * 2; - this.upwardVelocityMultiplier = 0.1f; + this.upwardVelocityMultiplier = 1f; this.velocity = Vec3d.ZERO; } @@ -1549,13 +1628,15 @@ public TurretProjectileVelocity setPower(float power) { /** * Retrieves the power of the projectile's velocity. + * The power scales by 1.5 times the value set as this + * was the most optimal value for the projectile's velocity. * * @return float The power of the projectile's velocity * * @see #power */ public float getPower() { - return this.power; + return this.power * 1.5f; } /** @@ -1599,17 +1680,25 @@ public TurretProjectileVelocity setUpwardVelocityMultiplier(float upwardVelocity /** * Retrieves the multiplier for the upward velocity of the projectile. + * The upward velocity multiplier scales by 0.1 times the value set as this + * was the most optimal value for the projectile's velocity. * * @return float The multiplier for the upward velocity of the projectile * * @see #upwardVelocityMultiplier */ public float getUpwardVelocityMultiplier() { - return this.upwardVelocityMultiplier; + return this.upwardVelocityMultiplier * 0.1f; } /** - * Sets the velocity of the projectile. + * Sets the velocity of the projectile. When this method is used to set the velocity, the + * projectile's velocity won't respect the following properties: + *
    + *
  • {@link #power Power}
  • + *
  • {@link #upwardVelocityMultiplier Upward Velocity Multiplier}
  • + *
  • {@link #isParabolic Parabolic Arc}
  • + *
* * @param velocity The velocity of the projectile * @@ -1618,12 +1707,18 @@ public float getUpwardVelocityMultiplier() { * @see #velocity */ public TurretProjectileVelocity setVelocity(Vec3d velocity) { - this.velocity = velocity; + this.calculateVelocity(velocity, false); return this; } /** - * Sets the velocity of the projectile. + * Sets the velocity of the projectile. When this method is used to set the velocity, the + * projectile's velocity won't respect the following properties: + *
    + *
  • {@link #power Power}
  • + *
  • {@link #upwardVelocityMultiplier Upward Velocity Multiplier}
  • + *
  • {@link #isParabolic Parabolic Arc}
  • + *
* * @param x The X-axis velocity of the projectile * @param y The Y-axis velocity of the projectile @@ -1645,14 +1740,29 @@ public TurretProjectileVelocity setVelocity(double x, double y, double z) { * @return {@code TurretProjectileVelocity} The instance of the {@link TurretProjectileVelocity} class */ public TurretProjectileVelocity setVelocity(LivingEntity target) { - double vx = (target.getX() - this.turret.getX()) * 1.0625; - double vy = target.getBodyY((double) 1 / 3) - this.turret.getY(); - double vz = (target.getZ() - this.turret.getZ()) * 1.0625; - double variance = Math.sqrt(vx * vx + vz * vz); + return this.setVelocityFromPos( + target.getX(), + target.getBodyY((double) 1 / 3), + target.getZ() + ); + } - vy += variance * this.upwardVelocityMultiplier; + /** + * Sets the velocity of the projectile based from the target position given. + * + * @param pos The target position of the projectile + * + * @return {@code TurretProjectileVelocity} The instance of the {@link TurretProjectileVelocity} class + * + * @see #velocity + */ + public TurretProjectileVelocity setVelocityFromPos(Vec3d pos) { + this.calculateVelocity(pos, true); + return this; + } - return this.setVelocity(vx, vy, vz); + public TurretProjectileVelocity setVelocityFromPos(double x, double y, double z) { + return this.setVelocityFromPos(new Vec3d(x, y, z)); } /** @@ -1684,6 +1794,84 @@ public TurretProjectileVelocity setDirectionalVelocity(float range) { return this.setVelocity(targetRange); } + + /** + * Sets the projectile's trajectory to be parabolic. This will make the projectile shoot in + * an arc-like trajectory, allowing for the creation of mortar-like projectiles. + * + * @param isParabolic {@code true} if the projectile's trajectory is parabolic, otherwise {@code false} + * + * @return {@code TurretProjectileVelocity} The instance of the {@link TurretProjectileVelocity} class + */ + public TurretProjectileVelocity setParabolic(boolean isParabolic) { + this.isParabolic = isParabolic; + return this; + } + + /** + * Retrieves whether the projectile's trajectory is parabolic. + * + * @return {@code boolean} {@code true} if the projectile's trajectory is parabolic, otherwise {@code false} + */ + public boolean isParabolic() { + return this.isParabolic; + } + + private void calculateVelocity(Vec3d v3d, boolean isPos) { + if (isPos) { + double vx, vy, vz; + + this.lastTargetPos = v3d; + // TODO: Properly calculate the parabolic trajectory (low priority) + // Parabolic Trajectory + if (this.isParabolic) { + double barrelY = this.turret + .getRelativePos(this.turret.getCurrentBarrel(false)) + .getY(); + + vx = (v3d.getX() - this.turret.getX()) * 1.0625; + vy = (v3d.getY() - barrelY); + vz = (v3d.getZ() - this.turret.getZ()) * 1.0625; + + double d3D = Math.sqrt(vx * vx + vy * vy + vz * vz); + + double minAngle = 45 * Math.PI / 180; + double maxAngle = 80 * Math.PI / 180; + double angleFactor = Math.clamp(d3D / (d3D + vy), 0.0, 1.0); + float theta = (float) (minAngle + angleFactor * (maxAngle - minAngle)); + double scalingFactor = Math.tan(theta); + double scalingFactorSqrd = scalingFactor * scalingFactor; + + vx = (vx / scalingFactorSqrd); + vy += (Math.abs(vy) * scalingFactor) - vy; + vz = (vz / scalingFactorSqrd); + } + // Straight Trajectory + else { + double barrelY = this.turret + .getRelativePos(this.turret.getCurrentBarrel(false)) + .getY(); + + vx = (v3d.getX() - this.turret.getX()) * 1.0625; + vy = (v3d.getY() - barrelY); + vz = (v3d.getZ() - this.turret.getZ()) * 1.0625; + + double variance = Math.sqrt(vx * vx + vz * vz); + vy += variance * this.getUpwardVelocityMultiplier(); + } + + // Applies a scale factor to make sure that when the power + // is applied, it will still be around its target's position. + Vec3d rawV = new Vec3d(vx, vy, vz); + double magnitude = rawV.length(); + double scaleFactor = this.getPower() / magnitude; + this.velocity = rawV.multiply(scaleFactor); + } + else { + this.lastTargetPos = null; + this.velocity = v3d; + } + } } /////////////////////// diff --git a/src/main/java/com/virus5600/defensive_measures/item/turrets/ballista/BallistaTurretItem.java b/src/main/java/com/virus5600/defensive_measures/item/turrets/ballista/BallistaTurretItem.java index 1e2e6d2..c409a76 100644 --- a/src/main/java/com/virus5600/defensive_measures/item/turrets/ballista/BallistaTurretItem.java +++ b/src/main/java/com/virus5600/defensive_measures/item/turrets/ballista/BallistaTurretItem.java @@ -1,6 +1,5 @@ package com.virus5600.defensive_measures.item.turrets.ballista; -import com.virus5600.defensive_measures.entity.turrets.TurretEntity; import net.minecraft.entity.EntityType; import net.minecraft.entity.mob.MobEntity; import net.minecraft.item.Item; @@ -10,6 +9,8 @@ import net.minecraft.util.Formatting; import net.minecraft.util.Rarity; +import com.virus5600.defensive_measures.entity.turrets.BallistaTurretEntity; +import com.virus5600.defensive_measures.entity.turrets.TurretEntity; import com.virus5600.defensive_measures.item.turrets.TurretItem; import java.lang.reflect.Type; @@ -39,9 +40,10 @@ public void appendTooltip(ItemStack stack, Item.TooltipContext context, List