package fathertoast.specialmobs.common.entity; import fathertoast.specialmobs.common.bestiary.BestiaryInfo; import fathertoast.specialmobs.common.bestiary.MobFamily; import fathertoast.specialmobs.common.config.Config; import fathertoast.specialmobs.common.config.species.SpeciesConfig; import fathertoast.specialmobs.common.core.SpecialMobs; import net.minecraft.block.Block; import net.minecraft.block.BlockState; import net.minecraft.block.Blocks; import net.minecraft.entity.LivingEntity; import net.minecraft.nbt.CompoundNBT; import net.minecraft.nbt.ListNBT; import net.minecraft.nbt.StringNBT; import net.minecraft.network.datasync.DataParameter; import net.minecraft.pathfinding.PathNodeType; import net.minecraft.potion.Effect; import net.minecraft.potion.EffectInstance; import net.minecraft.util.ResourceLocation; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.event.entity.living.PotionEvent; import net.minecraftforge.registries.ForgeRegistries; import java.util.Collection; import java.util.HashSet; import static fathertoast.specialmobs.common.util.References.*; public class SpecialMobData> { /** * @param tag The mob's base nbt tag. * @return The nbt tag to save special mob data to. */ public static CompoundNBT getSaveLocation( CompoundNBT tag ) { if( !tag.contains( TAG_FORGE_DATA, NBT_TYPE_COMPOUND ) ) { tag.put( TAG_FORGE_DATA, new CompoundNBT() ); } final CompoundNBT forgeTag = tag.getCompound( TAG_FORGE_DATA ); if( !forgeTag.contains( TAG_SPECIAL_MOB_DATA, NBT_TYPE_COMPOUND ) ) { forgeTag.put( TAG_SPECIAL_MOB_DATA, new CompoundNBT() ); } return forgeTag.getCompound( TAG_SPECIAL_MOB_DATA ); } /** The entity this data is for. */ private final T theEntity; /** Data manager parameter for render scale. */ private final DataParameter renderScale; /** The base texture of the entity. */ private ResourceLocation texture; /** The glowing eyes texture of the entity. */ private ResourceLocation textureEyes; /** The overlay texture of the entity. */ private ResourceLocation textureOverlay; /** True if the textures need to be sent to the client. */ private boolean updateTextures; /** The rate this mob regenerates health (ticks per 1 health). Off if 0 or less. */ private int healTimeMax; /** Counter to the next heal, if healTimeMax is greater than 0. */ private int healTime; /** Proportion of fall damage taken. */ private float fallDamageMultiplier; /** Whether the entity is immune to fire damage. */ private boolean isImmuneToFire; /** Whether the entity is immune to being set on fire. */ private boolean isImmuneToBurning; /** Whether the entity can breathe under water. */ private boolean canBreatheInWater; /** Whether the entity can ignore pushing from flowing water. */ private boolean ignoreWaterPush; /** Whether the entity is damaged when wet. */ private boolean isDamagedByWater; /** Whether the entity can be leashed. */ private boolean allowLeashing; /** Whether the entity does not trigger pressure plates. */ private boolean ignorePressurePlates; /** Set of blocks that the entity cannot be stuck in. */ private final HashSet immuneToStickyBlocks = new HashSet<>(); /** Set of potions that cannot be applied to the entity. */ private final HashSet immuneToPotions = new HashSet<>(); /** The damage the entity uses for its ranged attacks, when applicable. */ private float rangedAttackDamage; /** The spread (inaccuracy) of the entity's ranged attacks. */ private float rangedAttackSpread; /** The movement speed multiplier the entity uses during its ranged attack ai. Requires an AI reload to take effect. */ private float rangedWalkSpeed; /** The delay (in ticks) before a ranged attack can be used. Requires an AI reload to take effect. */ private int rangedAttackCooldown; /** * The delay (in ticks) between each ranged attack at maximum delay. Requires an AI reload to take effect. * Unused for bow attacks. For fireball attacks, this is "refire" time. * For spit attacks, this is the cooldown at maximum range (scaled down to the minimum cooldown at point-blank). */ private int rangedAttackMaxCooldown; /** * The maximum distance (in blocks) the entity can fire ranged attacks from. Requires an ai reload to take effect. * Ranged ai can only be used if this stat is greater than 0. Does not change aggro range. */ private float rangedAttackMaxRange; /** * Constructs a SpecialMobData to store generic data about a mob. *

* This constructor should be called during data watcher definitions, and defining the 'render scale' data watcher * parameter and setting up AI stats are the only things actually done while constructing. *

* The #initialize() method must be called later on to complete initialization (in the entity constructor). * * @param entity The entity to store data for. * @param scale Data parameter for storing the render scale. */ public SpecialMobData( T entity, DataParameter scale ) { theEntity = entity; renderScale = scale; entity.getEntityData().define( renderScale, nextScale() ); final SpeciesConfig.General config = theEntity.getSpecies().config.GENERAL; setRangedAttackDamage( config.rangedAttackDamage == null ? -1.0F : (float) config.rangedAttackDamage.get() ); setRangedAttackSpread( config.rangedAttackSpread == null ? -1.0F : (float) config.rangedAttackSpread.get() ); setRangedWalkSpeed( config.rangedWalkSpeed == null ? -1.0F : (float) config.rangedWalkSpeed.get() ); setRangedAttackCooldown( config.rangedAttackCooldown == null ? -1 : config.rangedAttackCooldown.get() ); setRangedAttackMaxCooldown( config.rangedAttackMaxCooldown == null ? -1 : config.rangedAttackMaxCooldown.get() ); setRangedAttackMaxRange( config.rangedAttackMaxRange == null ? -1.0F : (float) config.rangedAttackMaxRange.get() ); } public void initialize() { final BestiaryInfo info = theEntity.getSpecies().bestiaryInfo; texture = info.texture; textureEyes = info.eyesTexture; textureOverlay = info.overlayTexture; final SpeciesConfig.General config = theEntity.getSpecies().config.GENERAL; theEntity.setExperience( config.experience.get() ); setRegenerationTime( config.healTime.get() ); setFallDamageMultiplier( (float) config.fallDamageMultiplier.get() ); setImmuneToFire( config.isImmuneToFire.get() ); setImmuneToBurning( config.isImmuneToBurning.get() ); setCanBreatheInWater( config.canBreatheInWater.get() ); setIgnoreWaterPush( config.ignoreWaterPush.get() ); setDamagedByWater( config.isDamagedByWater.get() ); setAllowLeashing( config.allowLeashing.get() ); setIgnorePressurePlates( config.ignorePressurePlates.get() ); addStickyBlockImmunity( config.immuneToStickyBlocks.get().getEntries() ); addPotionImmunity( config.immuneToPotions.get().getEntries() ); } /** Copies all of the data from another mob, optionally copying texture(s). */ public void copyDataFrom( LivingEntity entity, boolean copyTextures ) { if( entity instanceof ISpecialMob ) { CompoundNBT tag = new CompoundNBT(); ((ISpecialMob) entity).getSpecialData().writeToNBT( tag ); if( !copyTextures ) { tag.remove( TAG_TEXTURE ); tag.remove( TAG_TEXTURE_EYES ); tag.remove( TAG_TEXTURE_OVER ); } readFromNBT( tag ); } } /** Called each tick for every living special mob. */ public void tick() { if( !theEntity.level.isClientSide && theEntity.isAlive() ) { // Send texture to client if( updateTextures && theEntity.tickCount > 1 ) { updateTextures = false; //SpecialMobs.network().sendToDimension( new MessageTexture( theEntity ), theEntity.dimension ); TODO } // Update natural regen if( healTimeMax > 0 && ++healTime >= healTimeMax ) { healTime = 0; theEntity.heal( 1.0F ); } } } /** @return Whether this entity has a glowing eyes texture. */ public boolean hasEyesTexture() { return textureEyes != null; } /** @return Whether this entity has an overlay texture. */ public boolean hasOverlayTexture() { return textureOverlay != null; } /** @return The base texture for the entity. */ public ResourceLocation getTexture() { return texture; } /** @return The glowing eyes texture for the entity. */ public ResourceLocation getTextureEyes() { return textureEyes; } /** @return The overlay texture for the entity. */ public ResourceLocation getTextureOverlay() { return textureOverlay; } /** @param textures The new texture(s) to set for the entity. */ private void setTextures( ResourceLocation[] textures ) { texture = textures[0]; textureEyes = textures.length > 1 ? textures[1] : null; textureOverlay = textures.length > 2 ? textures[2] : null; } /** @param textures The new texture(s) to load for the entity. Called when loaded from a packet. */ public void loadTextures( String[] textures ) { try { loadTexture( textures[0] ); loadTextureEyes( textures.length > 1 ? textures[1] : "" ); loadTextureOverlay( textures.length > 2 ? textures[2] : "" ); } catch( Exception ex ) { SpecialMobs.LOG.warn( "Failed to load textures for {}! ({})", theEntity, textures ); ex.printStackTrace(); } } private void loadTexture( String tex ) { if( tex.isEmpty() ) throw new IllegalArgumentException( "Entity must have a base texture" ); final ResourceLocation newTexture = new ResourceLocation( tex ); if( !newTexture.toString().equals( texture.toString() ) ) { texture = newTexture; updateTextures = true; } } private void loadTextureEyes( String tex ) { if( tex.isEmpty() ) { if( textureEyes != null ) { textureEyes = null; updateTextures = true; } } else if( textureEyes == null ) { textureEyes = new ResourceLocation( tex ); updateTextures = true; } else { final ResourceLocation newTexture = new ResourceLocation( tex ); if( !newTexture.toString().equals( textureEyes.toString() ) ) { texture = newTexture; updateTextures = true; } } } private void loadTextureOverlay( String tex ) { if( tex.isEmpty() ) { if( textureOverlay != null ) { textureOverlay = null; updateTextures = true; } } else if( textureOverlay == null ) { textureOverlay = new ResourceLocation( tex ); updateTextures = true; } else { final ResourceLocation newTexture = new ResourceLocation( tex ); if( !newTexture.toString().equals( textureOverlay.toString() ) ) { texture = newTexture; updateTextures = true; } } } /** @return The render scale for the entity, including any applied random scaling. */ public float getRenderScale() { return theEntity.getEntityData().get( renderScale ); } /** Sets the overall render scale for the entity. */ public void setRenderScale( float scale ) { if( !theEntity.level.isClientSide ) theEntity.getEntityData().set( renderScale, scale ); } //** @return The base render scale for the entity's mob family. */ //public float getFamilyBaseScale() { return familyScale; } //** @return The render scale for the entity without its family scale factored in; used to correct scaling for pre-scaled vanilla values. */ //public float getBaseScaleForPreScaledValues() { return getBaseScale() / getFamilyBaseScale(); } /** @return The base render scale for the entity, which is a property of the mob species. */ public float getBaseScale() { return theEntity.getSpecies().bestiaryInfo.baseScale; } /** @return A random render scale based on config settings. */ private float nextScale() { // Don't do random on client side stuff if( theEntity.level == null || theEntity.level.isClientSide() ) return getBaseScale(); // Prioritize most specific value available final MobFamily.Species species = theEntity.getSpecies(); final double randomScaling; if( species.config.GENERAL.randomScaling.get() >= 0.0 ) randomScaling = species.config.GENERAL.randomScaling.get(); else if( species.family.config.GENERAL.familyRandomScaling.get() >= 0.0 ) randomScaling = species.family.config.GENERAL.familyRandomScaling.get(); else randomScaling = Config.MAIN.GENERAL.masterRandomScaling.get(); return randomScaling <= 0.0 ? getBaseScale() : getBaseScale() * (1.0F + (theEntity.getRandom().nextFloat() - 0.5F) * 2.0F * (float) randomScaling); } private void setRegenerationTime( int ticks ) { healTimeMax = ticks; } public float getFallDamageMultiplier() { return fallDamageMultiplier; } private void setFallDamageMultiplier( float value ) { fallDamageMultiplier = value; } public boolean isImmuneToFire() { return isImmuneToFire; } private void setImmuneToFire( boolean value ) { isImmuneToFire = value; if( value ) { theEntity.setPathfindingMalus( PathNodeType.LAVA, PathNodeType.WATER.getMalus() ); theEntity.setPathfindingMalus( PathNodeType.DANGER_FIRE, PathNodeType.OPEN.getMalus() ); theEntity.setPathfindingMalus( PathNodeType.DAMAGE_FIRE, PathNodeType.OPEN.getMalus() ); } else { theEntity.setPathfindingMalus( PathNodeType.LAVA, PathNodeType.LAVA.getMalus() ); theEntity.setPathfindingMalus( PathNodeType.DANGER_FIRE, PathNodeType.DANGER_FIRE.getMalus() ); theEntity.setPathfindingMalus( PathNodeType.DAMAGE_FIRE, PathNodeType.DAMAGE_FIRE.getMalus() ); } } public boolean isImmuneToBurning() { return isImmuneToBurning; } private void setImmuneToBurning( boolean value ) { theEntity.clearFire(); isImmuneToBurning = value; if( value ) { theEntity.setPathfindingMalus( PathNodeType.DANGER_FIRE, PathNodeType.OPEN.getMalus() ); theEntity.setPathfindingMalus( PathNodeType.DAMAGE_FIRE, PathNodeType.DANGER_FIRE.getMalus() ); } else { theEntity.setPathfindingMalus( PathNodeType.DANGER_FIRE, PathNodeType.DANGER_FIRE.getMalus() ); theEntity.setPathfindingMalus( PathNodeType.DAMAGE_FIRE, PathNodeType.DAMAGE_FIRE.getMalus() ); } } public boolean allowLeashing() { return allowLeashing; } private void setAllowLeashing( boolean value ) { allowLeashing = value; } public boolean ignorePressurePlates() { return ignorePressurePlates; } private void setIgnorePressurePlates( boolean value ) { ignorePressurePlates = value; } public boolean canBreatheInWater() { return canBreatheInWater; } private void setCanBreatheInWater( boolean value ) { canBreatheInWater = value; } public boolean ignoreWaterPush() { return ignoreWaterPush; } private void setIgnoreWaterPush( boolean value ) { ignoreWaterPush = value; } public boolean isDamagedByWater() { return isDamagedByWater; } private void setDamagedByWater( boolean value ) { isDamagedByWater = value; theEntity.setPathfindingMalus( PathNodeType.WATER, value ? PathNodeType.LAVA.getMalus() : PathNodeType.WATER.getMalus() ); } /** * Tests a block state to see if the entity can be 'stuck' inside. * * @param block The block state to test. * @return True if the block is allowed to apply its stuck speed multiplier. */ public boolean canBeStuckIn( BlockState block ) { return !immuneToStickyBlocks.contains( block.getBlock() ); } /** @param blocks The sticky block(s) to grant immunity from. */ private void addStickyBlockImmunity( Collection blocks ) { immuneToStickyBlocks.addAll( blocks ); } /** * Tests a potion effect to see if it is applicable to the entity. * * @param effect The potion effect to test. * @return True if the potion is allowed to be applied. */ public boolean isPotionApplicable( EffectInstance effect ) { final PotionEvent.PotionApplicableEvent event = new PotionEvent.PotionApplicableEvent( theEntity, effect ); MinecraftForge.EVENT_BUS.post( event ); switch( event.getResult() ) { case DENY: return false; case ALLOW: return true; default: return !immuneToPotions.contains( effect.getEffect() ); } } /** @param effects The effect(s) to grant immunity from. */ private void addPotionImmunity( Collection effects ) { immuneToPotions.addAll( effects ); } public float getRangedAttackDamage() { return rangedAttackDamage; } public void setRangedAttackDamage( float value ) { rangedAttackDamage = value; } public float getRangedAttackSpread() { return rangedAttackSpread; } public void setRangedAttackSpread( float value ) { rangedAttackSpread = value; } public float getRangedWalkSpeed() { return rangedWalkSpeed; } public void setRangedWalkSpeed( float value ) { rangedWalkSpeed = value; } public int getRangedAttackCooldown() { return rangedAttackCooldown; } public void setRangedAttackCooldown( int value ) { rangedAttackCooldown = value; } public int getRangedAttackMaxCooldown() { return rangedAttackMaxCooldown; } public void setRangedAttackMaxCooldown( int value ) { rangedAttackMaxCooldown = value; } public float getRangedAttackMaxRange() { return rangedAttackMaxRange; } public void setRangedAttackMaxRange( float value ) { rangedAttackMaxRange = value; } public void disableRangedAttack() { setRangedAttackMaxRange( 0.0F ); } /** * Saves this data to NBT. * * @param tag The tag to save to. */ public void writeToNBT( CompoundNBT tag ) { tag.putFloat( TAG_RENDER_SCALE, getRenderScale() ); tag.putString( TAG_TEXTURE, texture.toString() ); tag.putString( TAG_TEXTURE_EYES, textureEyes == null ? "" : textureEyes.toString() ); tag.putString( TAG_TEXTURE_OVER, textureOverlay == null ? "" : textureOverlay.toString() ); // Capabilities tag.putInt( TAG_EXPERIENCE, theEntity.getExperience() ); tag.putByte( TAG_REGENERATION, (byte) healTimeMax ); tag.putFloat( TAG_FALL_MULTI, getFallDamageMultiplier() ); tag.putBoolean( TAG_FIRE_IMMUNE, isImmuneToFire() ); tag.putBoolean( TAG_BURN_IMMUNE, isImmuneToBurning() ); tag.putBoolean( TAG_LEASHABLE, allowLeashing() ); tag.putBoolean( TAG_TRAP_IMMUNE, ignorePressurePlates() ); tag.putBoolean( TAG_DROWN_IMMUNE, canBreatheInWater() ); tag.putBoolean( TAG_WATER_PUSH_IMMUNE, ignoreWaterPush() ); tag.putBoolean( TAG_WATER_DAMAGE, isDamagedByWater() ); final ListNBT stickyBlocksTag = new ListNBT(); for( Block block : immuneToStickyBlocks ) { final ResourceLocation regKey = ForgeRegistries.BLOCKS.getKey( block ); if( regKey != null ) stickyBlocksTag.add( StringNBT.valueOf( SpecialMobs.toString( regKey ) ) ); } tag.put( TAG_STICKY_IMMUNE, stickyBlocksTag ); final ListNBT potionsTag = new ListNBT(); for( Effect effect : immuneToPotions ) { final ResourceLocation regKey = ForgeRegistries.POTIONS.getKey( effect ); if( regKey != null ) potionsTag.add( StringNBT.valueOf( SpecialMobs.toString( regKey ) ) ); } tag.put( TAG_POTION_IMMUNE, potionsTag ); // Ranged attack stats (optional) if( getRangedAttackDamage() >= 0.0F ) tag.putFloat( TAG_ARROW_DAMAGE, getRangedAttackDamage() ); if( getRangedAttackSpread() >= 0.0F ) tag.putFloat( TAG_ARROW_SPREAD, getRangedAttackSpread() ); if( getRangedWalkSpeed() >= 0.0F ) tag.putFloat( TAG_ARROW_WALK_SPEED, getRangedWalkSpeed() ); if( getRangedAttackCooldown() >= 0 ) tag.putShort( TAG_ARROW_REFIRE_MIN, (short) getRangedAttackCooldown() ); if( getRangedAttackMaxCooldown() >= 0 ) tag.putShort( TAG_ARROW_REFIRE_MAX, (short) getRangedAttackMaxCooldown() ); if( getRangedAttackMaxRange() >= 0.0F ) tag.putFloat( TAG_ARROW_RANGE, getRangedAttackMaxRange() ); } /** * Loads this data from NBT. * * @param tag The tag to load from. */ public void readFromNBT( CompoundNBT tag ) { if( tag.contains( TAG_RENDER_SCALE, NBT_TYPE_NUMERICAL ) ) { setRenderScale( tag.getFloat( TAG_RENDER_SCALE ) ); } try { if( tag.contains( TAG_TEXTURE, NBT_TYPE_STRING ) ) { loadTexture( tag.getString( TAG_TEXTURE ) ); } if( tag.contains( TAG_TEXTURE_EYES, NBT_TYPE_STRING ) ) { loadTextureEyes( tag.getString( TAG_TEXTURE_EYES ) ); } if( tag.contains( TAG_TEXTURE_OVER, NBT_TYPE_STRING ) ) { loadTextureOverlay( tag.getString( TAG_TEXTURE_OVER ) ); } } catch( Exception ex ) { SpecialMobs.LOG.warn( "Failed to load textures from NBT! " + theEntity.toString() ); } // Capabilities if( tag.contains( TAG_EXPERIENCE, NBT_TYPE_NUMERICAL ) ) { theEntity.setExperience( tag.getInt( TAG_EXPERIENCE ) ); } if( tag.contains( TAG_REGENERATION, NBT_TYPE_NUMERICAL ) ) { healTimeMax = tag.getByte( TAG_REGENERATION ); } if( tag.contains( TAG_FALL_MULTI, NBT_TYPE_NUMERICAL ) ) { setFallDamageMultiplier( tag.getFloat( TAG_FALL_MULTI ) ); } if( tag.contains( TAG_FIRE_IMMUNE, NBT_TYPE_NUMERICAL ) ) { setImmuneToFire( tag.getBoolean( TAG_FIRE_IMMUNE ) ); } if( tag.contains( TAG_BURN_IMMUNE, NBT_TYPE_NUMERICAL ) ) { setImmuneToBurning( tag.getBoolean( TAG_BURN_IMMUNE ) ); } if( tag.contains( TAG_DROWN_IMMUNE, NBT_TYPE_NUMERICAL ) ) { setCanBreatheInWater( tag.getBoolean( TAG_DROWN_IMMUNE ) ); } if( tag.contains( TAG_WATER_PUSH_IMMUNE, NBT_TYPE_NUMERICAL ) ) { setIgnoreWaterPush( tag.getBoolean( TAG_WATER_PUSH_IMMUNE ) ); } if( tag.contains( TAG_WATER_DAMAGE, NBT_TYPE_NUMERICAL ) ) { setDamagedByWater( tag.getBoolean( TAG_WATER_DAMAGE ) ); } if( tag.contains( TAG_LEASHABLE, NBT_TYPE_NUMERICAL ) ) { setAllowLeashing( tag.getBoolean( TAG_LEASHABLE ) ); } if( tag.contains( TAG_TRAP_IMMUNE, NBT_TYPE_NUMERICAL ) ) { setIgnorePressurePlates( tag.getBoolean( TAG_TRAP_IMMUNE ) ); } if( tag.contains( TAG_STICKY_IMMUNE, NBT_TYPE_LIST ) ) { final ListNBT stickyBlocksTag = tag.getList( TAG_STICKY_IMMUNE, NBT_TYPE_STRING ); immuneToStickyBlocks.clear(); for( int i = 0; i < stickyBlocksTag.size(); i++ ) { final Block block = ForgeRegistries.BLOCKS.getValue( new ResourceLocation( stickyBlocksTag.getString( i ) ) ); if( block != null && !block.is( Blocks.AIR ) ) immuneToStickyBlocks.add( block ); } } if( tag.contains( TAG_POTION_IMMUNE, NBT_TYPE_LIST ) ) { final ListNBT potionsTag = tag.getList( TAG_POTION_IMMUNE, NBT_TYPE_STRING ); immuneToPotions.clear(); for( int i = 0; i < potionsTag.size(); i++ ) { final Effect effect = ForgeRegistries.POTIONS.getValue( new ResourceLocation( potionsTag.getString( i ) ) ); if( effect != null ) immuneToPotions.add( effect ); } } // Ranged attack stats if( tag.contains( TAG_ARROW_DAMAGE, NBT_TYPE_NUMERICAL ) ) { setRangedAttackDamage( tag.getFloat( TAG_ARROW_DAMAGE ) ); } if( tag.contains( TAG_ARROW_SPREAD, NBT_TYPE_NUMERICAL ) ) { setRangedAttackSpread( tag.getFloat( TAG_ARROW_SPREAD ) ); } if( tag.contains( TAG_ARROW_WALK_SPEED, NBT_TYPE_NUMERICAL ) ) { setRangedWalkSpeed( tag.getFloat( TAG_ARROW_WALK_SPEED ) ); } if( tag.contains( TAG_ARROW_REFIRE_MIN, NBT_TYPE_NUMERICAL ) ) { setRangedAttackCooldown( tag.getShort( TAG_ARROW_REFIRE_MIN ) ); } if( tag.contains( TAG_ARROW_REFIRE_MAX, NBT_TYPE_NUMERICAL ) ) { setRangedAttackMaxCooldown( tag.getShort( TAG_ARROW_REFIRE_MAX ) ); } if( tag.contains( TAG_ARROW_RANGE, NBT_TYPE_NUMERICAL ) ) { setRangedAttackMaxRange( tag.getFloat( TAG_ARROW_RANGE ) ); } } }