diff --git a/src/main/java/fathertoast/specialmobs/client/ClientRegister.java b/src/main/java/fathertoast/specialmobs/client/ClientRegister.java new file mode 100644 index 0000000..bce8a68 --- /dev/null +++ b/src/main/java/fathertoast/specialmobs/client/ClientRegister.java @@ -0,0 +1,37 @@ +package fathertoast.specialmobs.client; + +import fathertoast.specialmobs.client.renderer.entity.SpecialCreeperRenderer; +import fathertoast.specialmobs.common.bestiary.MobFamily; +import fathertoast.specialmobs.common.core.SpecialMobs; +import mcp.MethodsReturnNonnullByDefault; +import net.minecraft.entity.LivingEntity; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.client.registry.IRenderFactory; +import net.minecraftforge.fml.client.registry.RenderingRegistry; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent; + +import javax.annotation.ParametersAreNonnullByDefault; + +@ParametersAreNonnullByDefault +@MethodsReturnNonnullByDefault +@Mod.EventBusSubscriber( value = Dist.CLIENT, modid = SpecialMobs.MOD_ID, bus = Mod.EventBusSubscriber.Bus.MOD ) +public class ClientRegister { + + @SubscribeEvent + public static void onClientSetup( FMLClientSetupEvent event ) { + registerEntityRenderers(); + } + + private static void registerEntityRenderers() { + // Family-based renderers + registerFamilyRenderers( MobFamily.CREEPER, SpecialCreeperRenderer::new ); + } + + private static void registerFamilyRenderers( MobFamily family, IRenderFactory renderFactory ) { + RenderingRegistry.registerEntityRenderingHandler( family.vanillaReplacement.entityType.get(), renderFactory ); + for( MobFamily.Species species : family.variants ) + RenderingRegistry.registerEntityRenderingHandler( species.entityType.get(), renderFactory ); + } +} \ No newline at end of file diff --git a/src/main/java/fathertoast/specialmobs/client/renderer/entity/SpecialCreeperRenderer.java b/src/main/java/fathertoast/specialmobs/client/renderer/entity/SpecialCreeperRenderer.java new file mode 100644 index 0000000..5551e80 --- /dev/null +++ b/src/main/java/fathertoast/specialmobs/client/renderer/entity/SpecialCreeperRenderer.java @@ -0,0 +1,44 @@ +package fathertoast.specialmobs.client.renderer.entity; + +import com.mojang.blaze3d.matrix.MatrixStack; +import fathertoast.specialmobs.common.entity.ISpecialMob; +import mcp.MethodsReturnNonnullByDefault; +import net.minecraft.client.renderer.entity.CreeperRenderer; +import net.minecraft.client.renderer.entity.EntityRendererManager; +import net.minecraft.client.renderer.entity.model.CreeperModel; +import net.minecraft.entity.monster.CreeperEntity; +import net.minecraft.util.ResourceLocation; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import javax.annotation.ParametersAreNonnullByDefault; + +@ParametersAreNonnullByDefault +@MethodsReturnNonnullByDefault +@OnlyIn( Dist.CLIENT ) +public class SpecialCreeperRenderer extends CreeperRenderer { + + private final float baseShadowRadius; + + public SpecialCreeperRenderer( EntityRendererManager rendererManager ) { + super( rendererManager ); + baseShadowRadius = shadowRadius; + // addLayer( new LayerSpecialMobEyes<>( this ) ); TODO render layer impl + // addLayer( new LayerSpecialMobOverlay<>( this, new CreeperModel<>( 0.25F ) ) ); + } + + @Override + public ResourceLocation getTextureLocation( CreeperEntity entity ) { + return super.getTextureLocation( entity ); + //return ((ISpecialMob) entity).getSpecialData().getTexture();TODO textures + } + + @Override + protected void scale( CreeperEntity entity, MatrixStack matrixStack, float partialTick ) { + super.scale( entity, matrixStack, partialTick ); + + final float scale = ((ISpecialMob) entity).getSpecialData().getRenderScale(); + shadowRadius = baseShadowRadius * scale; + matrixStack.scale( scale, scale, scale ); + } +} \ No newline at end of file diff --git a/src/main/java/fathertoast/specialmobs/common/bestiary/BestiaryInfo.java b/src/main/java/fathertoast/specialmobs/common/bestiary/BestiaryInfo.java new file mode 100644 index 0000000..3982da4 --- /dev/null +++ b/src/main/java/fathertoast/specialmobs/common/bestiary/BestiaryInfo.java @@ -0,0 +1,121 @@ +package fathertoast.specialmobs.common.bestiary; + +//import net.minecraft.init.Biomes; + +import net.minecraft.world.DimensionType; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +public class BestiaryInfo { + + public enum BaseWeight { + DEFAULT( 600 ), + HIGHEST( DEFAULT.value * 8 ), + HIGH( DEFAULT.value * 4 ), + LOW( DEFAULT.value / 4 ), + LOWEST( DEFAULT.value / 8 ); + + public final int value; + + BaseWeight( int v ) { value = v; } + } + + //TODO default themes + // public static final EnvironmentListConfig DEFAULT_THEME_FIRE = new EnvironmentListConfig( + // new TargetEnvironment.TargetBiomeGroup( "desert", BestiaryInfo.BASE_WEIGHT_COMMON ), + // new TargetEnvironment.TargetBiomeGroup( "savanna", BestiaryInfo.BASE_WEIGHT_COMMON ), + // new TargetEnvironment.TargetBiomeGroup( "mesa", BestiaryInfo.BASE_WEIGHT_COMMON ), + // new TargetEnvironment.TargetDimension( DimensionType.NETHER, BestiaryInfo.BASE_WEIGHT_COMMON ), + // + // new TargetEnvironment.TargetBiomeGroup( "ice", BestiaryInfo.BASE_WEIGHT_RARE ), + // new TargetEnvironment.TargetBiomeGroup( "frozen", BestiaryInfo.BASE_WEIGHT_RARE ), + // new TargetEnvironment.TargetBiomeGroup( "cold", BestiaryInfo.BASE_WEIGHT_RARE ), + // new TargetEnvironment.TargetBiomeGroup( "taiga_cold", BestiaryInfo.BASE_WEIGHT_RARE ) + // ); + // + // public static final EnvironmentListConfig DEFAULT_THEME_ICE = new EnvironmentListConfig( + // new TargetEnvironment.TargetBiomeGroup( "ice", BestiaryInfo.BASE_WEIGHT_COMMON ), + // new TargetEnvironment.TargetBiomeGroup( "frozen", BestiaryInfo.BASE_WEIGHT_COMMON ), + // new TargetEnvironment.TargetBiomeGroup( "cold", BestiaryInfo.BASE_WEIGHT_COMMON ), + // new TargetEnvironment.TargetBiomeGroup( "taiga_cold", BestiaryInfo.BASE_WEIGHT_COMMON ), + // + // new TargetEnvironment.TargetBiomeGroup( "desert", BestiaryInfo.BASE_WEIGHT_RARE ), + // new TargetEnvironment.TargetBiomeGroup( "savanna", BestiaryInfo.BASE_WEIGHT_RARE ), + // new TargetEnvironment.TargetBiomeGroup( "mesa", BestiaryInfo.BASE_WEIGHT_RARE ), + // new TargetEnvironment.TargetDimension( DimensionType.NETHER, BestiaryInfo.BASE_WEIGHT_RARE ) + // ); + // + // public static final EnvironmentListConfig DEFAULT_THEME_FOREST = new EnvironmentListConfig( + // new TargetEnvironment.TargetBiomeGroup( "swamp", BestiaryInfo.BASE_WEIGHT_COMMON ), + // new TargetEnvironment.TargetBiomeGroup( "forest", BestiaryInfo.BASE_WEIGHT_COMMON ), + // new TargetEnvironment.TargetBiomeGroup( "birch_forest", BestiaryInfo.BASE_WEIGHT_COMMON ), + // new TargetEnvironment.TargetBiomeGroup( "roofed_forest", BestiaryInfo.BASE_WEIGHT_COMMON ), + // new TargetEnvironment.TargetBiomeGroup( "jungle", BestiaryInfo.BASE_WEIGHT_COMMON ), + // new TargetEnvironment.TargetBiomeGroup( "taiga", BestiaryInfo.BASE_WEIGHT_COMMON ), + // new TargetEnvironment.TargetBiomeGroup( "redwood_taiga", BestiaryInfo.BASE_WEIGHT_COMMON ), + // + // new TargetEnvironment.TargetBiomeGroup( "desert", BestiaryInfo.BASE_WEIGHT_RARE ), + // new TargetEnvironment.TargetBiomeGroup( "ice", BestiaryInfo.BASE_WEIGHT_RARE ), + // new TargetEnvironment.TargetBiomeGroup( "stone", BestiaryInfo.BASE_WEIGHT_RARE ) + // ); + // + // public static final EnvironmentListConfig DEFAULT_THEME_MOUNTAIN = new EnvironmentListConfig( + // new TargetEnvironment.TargetBiomeGroup( "extreme", BestiaryInfo.BASE_WEIGHT_COMMON ), + // new TargetEnvironment.TargetBiomeGroup( "smaller_extreme", BestiaryInfo.BASE_WEIGHT_COMMON ), + // new TargetEnvironment.TargetBiomeGroup( "mesa", BestiaryInfo.BASE_WEIGHT_COMMON ), + // new TargetEnvironment.TargetBiomeGroup( "ice_mountains", BestiaryInfo.BASE_WEIGHT_COMMON ), + // new TargetEnvironment.TargetBiomeGroup( "stone", BestiaryInfo.BASE_WEIGHT_COMMON ), + // + // new TargetEnvironment.TargetBiomeGroup( "plains", BestiaryInfo.BASE_WEIGHT_RARE ), + // new TargetEnvironment.TargetBiomeGroup( "desert", BestiaryInfo.BASE_WEIGHT_RARE ), + // new TargetEnvironment.TargetBiomeGroup( "swamp", BestiaryInfo.BASE_WEIGHT_RARE ), + // new TargetEnvironment.TargetBiomeGroup( "beach", BestiaryInfo.BASE_WEIGHT_RARE ), + // new TargetEnvironment.TargetBiomeGroup( "cold_beach", BestiaryInfo.BASE_WEIGHT_RARE ), + // new TargetEnvironment.TargetBiomeGroup( "savanna", BestiaryInfo.BASE_WEIGHT_RARE ), + // new TargetEnvironment.TargetBiomeGroup( "ice_flats", BestiaryInfo.BASE_WEIGHT_RARE ) + // ); + // + // public static final EnvironmentListConfig DEFAULT_THEME_WATER = new EnvironmentListConfig( + // new TargetEnvironment.TargetBiomeGroup( "swamp", BestiaryInfo.BASE_WEIGHT_COMMON ), + // new TargetEnvironment.TargetBiome( Biomes.BEACH, BestiaryInfo.BASE_WEIGHT_COMMON ), + // new TargetEnvironment.TargetBiome( Biomes.OCEAN, BestiaryInfo.BASE_WEIGHT_COMMON ), + // new TargetEnvironment.TargetBiome( Biomes.DEEP_OCEAN, BestiaryInfo.BASE_WEIGHT_COMMON ), + // new TargetEnvironment.TargetBiome( Biomes.FROZEN_OCEAN, BestiaryInfo.BASE_WEIGHT_COMMON ), + // + // new TargetEnvironment.TargetBiomeGroup( "desert", BestiaryInfo.BASE_WEIGHT_RARE ), + // new TargetEnvironment.TargetBiomeGroup( "savanna", BestiaryInfo.BASE_WEIGHT_RARE ), + // new TargetEnvironment.TargetDimension( DimensionType.NETHER, 0 ) + // ); + // + // public static final EnvironmentListConfig DEFAULT_THEME_FISHING = new EnvironmentListConfig( + // new TargetEnvironment.TargetBiomeGroup( "swamp", BestiaryInfo.BASE_WEIGHT_COMMON ), + // new TargetEnvironment.TargetBiome( Biomes.BEACH, BestiaryInfo.BASE_WEIGHT_COMMON ), + // new TargetEnvironment.TargetBiome( Biomes.OCEAN, BestiaryInfo.BASE_WEIGHT_COMMON ), + // new TargetEnvironment.TargetBiome( Biomes.DEEP_OCEAN, BestiaryInfo.BASE_WEIGHT_COMMON ), + // new TargetEnvironment.TargetBiome( Biomes.FROZEN_OCEAN, BestiaryInfo.BASE_WEIGHT_COMMON ), + // + // new TargetEnvironment.TargetBiomeGroup( "desert", BestiaryInfo.BASE_WEIGHT_UNCOMMON ), + // new TargetEnvironment.TargetBiomeGroup( "savanna", BestiaryInfo.BASE_WEIGHT_UNCOMMON ), + // new TargetEnvironment.TargetBiomeGroup( "mesa", BestiaryInfo.BASE_WEIGHT_UNCOMMON ) + // ); + + // These can be set in the entity class to change these default values. + //public EnvironmentListConfig weightExceptions = new EnvironmentListConfig( ); + + public final int eggSpotsColor; + public final BaseWeight weight; + + public BestiaryInfo( int eggColor ) { this( eggColor, BaseWeight.DEFAULT ); } + + public BestiaryInfo( int eggColor, BaseWeight baseWeight ) { + eggSpotsColor = eggColor; + weight = baseWeight; + } + + public BestiaryInfo setTheme() { + return this;//TODO theme builder + } +} \ No newline at end of file diff --git a/src/main/java/fathertoast/specialmobs/common/bestiary/MobFamily.java b/src/main/java/fathertoast/specialmobs/common/bestiary/MobFamily.java new file mode 100644 index 0000000..ff0889a --- /dev/null +++ b/src/main/java/fathertoast/specialmobs/common/bestiary/MobFamily.java @@ -0,0 +1,308 @@ +package fathertoast.specialmobs.common.bestiary; + +import fathertoast.specialmobs.common.core.register.SMEntities; +import fathertoast.specialmobs.common.core.register.SMItems; +import fathertoast.specialmobs.common.util.AnnotationHelper; +import fathertoast.specialmobs.common.util.References; +import mcp.MethodsReturnNonnullByDefault; +import net.minecraft.block.Block; +import net.minecraft.entity.*; +import net.minecraft.entity.monster.CreeperEntity; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; +import net.minecraftforge.common.ForgeSpawnEggItem; +import net.minecraftforge.fml.RegistryObject; + +import javax.annotation.Nullable; +import javax.annotation.ParametersAreNonnullByDefault; +import java.lang.reflect.InvocationTargetException; +import java.util.*; + +/** + * Special mobs are broken up into distinct 'families', each of which correspond to a type of vanilla mob that can be + * replaced. During mob replacement, any member of the family (species) may be chosen, depending on location and config. + * + * @see MobFamily.Species + */ +@ParametersAreNonnullByDefault +@MethodsReturnNonnullByDefault +public class MobFamily { + /** List of all families, generated to make iteration possible. */ + private static final List> FAMILY_LIST = new ArrayList<>(); + /** List of all species, generated to make iteration more convenient. */ + private static final List> SPECIES_LIST; + + /** Maps each replaceable entity type to the family that replaces it. */ + private static final Map, MobFamily> TYPE_TO_FAMILY_MAP; + + // NOTE: When adding a new mob family, do not forget to also register its renderer in the client register! + + public static final MobFamily CREEPER = new MobFamily<>( + "Creeper", "creepers", 0x0da70b, new EntityType[] { EntityType.CREEPER }, + "Dark"//, "Death", "Dirt", "Doom", "Drowning", "Ender", "Fire", "Gravel", "Jumping", "Lightning", "Mini", "Splitting" + ); + + // public static final MobFamily ZOMBIE = new MobFamily<>( + // "Zombie", "zombies", 0x00afaf, new EntityType[] { EntityType.ZOMBIE, EntityType.HUSK }, + // "Brute", "Fire", "Fishing", "Giant", "Hungry", "Husk", "Plague" + // ); + // public static final MobFamily ZOMBIFIED_PIGLIN = new MobFamily<>( + // "ZombifiedPiglin", "zombie pigmen", 0xea9393, new EntityType[] { EntityType.ZOMBIFIED_PIGLIN }, + // "Brute", "Fishing", "Giant", "Hungry", "Knight", "Plague", "Vampire" + // ); + + // public static final MobFamily SKELETON = new MobFamily<>( + // "Skeleton", "skeletons", 0xc1c1c1, new EntityType[] { EntityType.SKELETON, EntityType.STRAY }, + // "Brute", "Fire", "Gatling", "Giant", "Knight", "Ninja", "Poison", "Sniper", "Spitfire", "Stray" + // ); + // public static final MobFamily WITHER_SKELETON = new MobFamily<>( + // "WitherSkeleton", "wither skeletons", 0x141414, new EntityType[] { EntityType.WITHER_SKELETON }, + // "Brute", "Gatling", "Giant", "Knight", "Ninja", "Sniper", "Spitfire" + // ); + + // public static final MobFamily SLIME = new MobFamily<>( + // "Slime", "slimes", 0x51a03e, new EntityType[] { EntityType.SLIME }, + // "Blackberry", "Blueberry", "Caramel", "Grape", "Lemon", "Strawberry", "Watermelon" + // ); + // public static final MobFamily MAGMA_CUBE = new MobFamily<>( + // "MagmaCube", "magma cubes", 0x340000, new EntityType[] { EntityType.MAGMA_CUBE }, + // "Flying", "Hardened", "Sticky", "Volatile" + // ); + + // public static final MobFamily SPIDER = new MobFamily<>( + // "Spider", "spiders", 0x342d27, new EntityType[] { EntityType.SPIDER }, + // "Baby", "Desert", "Flying", "Giant", "Hungry", "Mother", "Pale", "Poison", "Web", "Witch" + // ); + // public static final MobFamily CAVE_SPIDER = new MobFamily<>( + // "CaveSpider", "cave spiders", 0x0c424e, new EntityType[] { EntityType.CAVE_SPIDER }, + // "Baby", "Flying", "Mother", "Web", "Witch" + // ); + + // public static final MobFamily SILVERFISH = new MobFamily<>( + // "Silverfish", "silverfish", 0x6e6e6e, new EntityType[] { EntityType.SILVERFISH }, + // "Blinding", "Fishing", "Flying", "Poison", "Tough" + // ); + + // public static final MobFamily ENDERMAN = new MobFamily<>( + // "Enderman", "endermen", 0x161616, new EntityType[] { EntityType.ENDERMAN }, + // "Blinding", "Icy", "Lightning", "Mini", "Mirage", "Thief" + // ); + + // public static final MobFamily WITCH = new MobFamily<>( + // "Witch", "witches", 0x340000, new EntityType[] { EntityType.WITCH }, + // "Domination", "Shadows", "Undead", "Wilds", "Wind" + // ); + + // public static final MobFamily GHAST = new MobFamily<>( + // "Ghast", "ghasts", 0xf9f9f9, new EntityType[] { EntityType.GHAST }, + // "Baby", "Fighter", "King", "Queen", "Unholy" + // ); + + // public static final MobFamily BLAZE = new MobFamily<>( + // "Blaze", "blazes", 0xf6b201, new EntityType[] { EntityType.BLAZE }, + // "Cinder", "Conflagration", "Ember", "Hellfire", "Inferno", "Jolt", "Wildfire" + // ); + + static { + final HashMap, MobFamily> classToFamilyMap = new HashMap<>(); + final ArrayList> allSpecies = new ArrayList<>(); + + for( MobFamily family : FAMILY_LIST ) { + for( EntityType replaceable : family.replaceableTypes ) + classToFamilyMap.put( replaceable, family ); + + allSpecies.add( family.vanillaReplacement ); + allSpecies.addAll( Arrays.asList( family.variants ) ); + } + allSpecies.trimToSize(); + + TYPE_TO_FAMILY_MAP = Collections.unmodifiableMap( classToFamilyMap ); + SPECIES_LIST = Collections.unmodifiableList( allSpecies ); + } + + /** Called during mod construction to initialize the bestiary. */ + public static void initBestiary() { } + + /** @return A list of all families. */ + public static List> getAll() { return Collections.unmodifiableList( FAMILY_LIST ); } + + /** @return A list of all species. */ + public static List> getAllSpecies() { return SPECIES_LIST; } + + /** @return The family of mobs that can replace the passed entity; returns null if the entity is not replaceable. */ + @Nullable + public static MobFamily getReplacementFamily( LivingEntity entity ) { + return TYPE_TO_FAMILY_MAP.get( entity.getType() ); + } + + /** The technical name that refers to this family. Note that this is UpperCamelCase, but is often used in lowercase. */ + public final String name; + /** The name used to refer to this family in unlocalized situations; e.g. config comments. */ + public final String configName; + + /** The base egg color for species in this family. Species' eggs differ visually only by spot color. */ + public final int eggBaseColor; + + /** List of replaceable entity types. The vanilla replacement's entity type is based on the first entry in this array. */ + public final EntityType[] replaceableTypes; + + /** + * The vanilla replacement species for this family. These will subtly replace the vanilla mob and provide extra + * capabilities (unless disabled by config) - both for the mob and for map/pack creators. + */ + public final Species vanillaReplacement; + /** Array of all special variant species in this family. In practice, these are the true "special mobs". */ + public final Species[] variants; + + //public Config.FamilyConfig config; + + private MobFamily( String familyName, String familyKey, int eggColor, EntityType[] replaceable, + String... variantNames ) { + name = familyName; + configName = familyKey; + eggBaseColor = eggColor; + replaceableTypes = replaceable; + if( replaceable.length < 1 ) + throw new IllegalArgumentException( familyName + " family must have at least one replaceable type!" ); + + final String packageRoot = References.ENTITY_PACKAGE + name.toLowerCase() + "."; + vanillaReplacement = new Species<>( this, packageRoot, null ); + //noinspection unchecked + variants = new Species[variantNames.length]; + for( int i = 0; i < variants.length; i++ ) { + variants[i] = new Species<>( this, packageRoot, variantNames[i] ); + } + + // We register here because otherwise there's no way to find all families + FAMILY_LIST.add( this ); + } + + /** Pick a new species from this family, based on the location. */ + public Species nextVariant( World world, BlockPos pos ) { // TODO mob replacer + // Build weights for the current location + int totalWeight = 0; + int[] variantWeights = new int[variants.length]; + for( int i = 0; i < variants.length; i++ ) { + int weight = 0;//config.getVariantWeight( world, pos, variants[i] ); TODO configs + if( weight > 0 ) { + totalWeight += weight; + variantWeights[i] = weight; + } + } + + // Pick one item at random + if( totalWeight > 0 ) { + int weight = world.random.nextInt( totalWeight ); + for( int i = 0; i < variants.length; i++ ) { + if( variantWeights[i] > 0 ) { + weight -= variantWeights[i]; + if( weight < 0 ) { + return variants[i]; + } + } + } + } + return vanillaReplacement; + } + + /** + * Each special mob family is effectively a collection of special mob species, and each species corresponds to one + * entity type. + *

+ * There are two types of species; vanilla replacements that closely resemble their vanilla counterparts, and + * special variants that differ both visually and mechanically. Each family has exactly one vanilla replacement + * species and may have any number of special variants. + *

+ * Though typically special variant entity classes will extend the vanilla replacement, this cannot always be assumed. + * + * @see MobFamily + */ + @ParametersAreNonnullByDefault + @MethodsReturnNonnullByDefault + public static class Species { + /** The special mob family this species belongs to. */ + public final MobFamily family; + /** The name of this special variant; or null for vanilla replacement species. */ + public final String specialVariantName; + /** The technical name that refers to this species. Note that this is UpperCamelCase, but is often used in lowercase. */ + public final String name; + + /** The entity class for this species. */ + public final Class entityClass; + /** The bestiary info describing this species */ + public final BestiaryInfo bestiaryInfo; + + /** This species's entity type, wrapped in its registry object. */ + public final RegistryObject> entityType; + /** This species's spawn egg item, wrapped in its registry object. */ + public final RegistryObject spawnEgg; + + /** Constructs a new mob species. For vanilla replacements, the variant name is null. */ + private Species( MobFamily parentFamily, String packageRoot, @Nullable String variantName ) { + final boolean vanillaReplacement = variantName == null; + + family = parentFamily; + specialVariantName = variantName; + name = vanillaReplacement ? parentFamily.name : variantName + parentFamily.name; + + // Below require unlocalized name to be defined + entityClass = findClass( vanillaReplacement ? + References.VANILLA_REPLACEMENT_FORMAT : References.SPECIAL_VARIANT_FORMAT, packageRoot ); + + // Below require variant class to be defined + final EntityType.Builder entityTypeBuilder = makeEntityTypeBuilder( parentFamily.replaceableTypes[0] ); + bestiaryInfo = makeBestiaryInfo( entityTypeBuilder ); + + // Initialize deferred registry objects + entityType = SMEntities.register( name.toLowerCase( Locale.ROOT ), entityTypeBuilder ); + spawnEgg = SMItems.registerSpawnEgg( entityType, parentFamily.eggBaseColor, bestiaryInfo.eggSpotsColor ); + } + + /** Finds the entity class based on a standard format. */ + private Class findClass( String format, String packageRoot ) { + try { + //noinspection unchecked + return (Class) Class.forName( String.format( format, packageRoot, name ) ); + } + catch( ClassNotFoundException ex ) { + throw new RuntimeException( "Failed to find entity class for mob species " + name, ex ); + } + } + + /** Calls on this species' entity class to generate its bestiary info. */ + private BestiaryInfo makeBestiaryInfo( EntityType.Builder entityTypeBuilder ) { + try { + return AnnotationHelper.getBestiaryInfo( this, entityTypeBuilder ); + } + catch( IllegalAccessException | NoSuchMethodException | InvocationTargetException ex ) { + throw new RuntimeException( "Entity class for " + name + " has invalid bestiary info method", ex ); + } + } + + /** + * Builds a deep copy of an entity type with a different entity constructor. + * Leaves the new entity type "un-built" so it can be further modified, if needed. + */ + private EntityType.Builder makeEntityTypeBuilder( EntityType original ) { + final EntityType.IFactory factory; + try { + factory = AnnotationHelper.getEntityFactory( this ); + } + catch( NoSuchMethodException ex ) { + throw new RuntimeException( "Entity class for " + name + " has no valid constructors", ex ); + } + final EntityType.Builder clone = EntityType.Builder.of( factory, original.getCategory() ); + + if( !original.canSummon() ) clone.noSummon(); + if( !original.canSerialize() ) clone.noSave(); + if( original.fireImmune() ) clone.fireImmune(); + if( original.canSpawnFarFromPlayer() ) clone.canSpawnFarFromPlayer(); + + return clone.sized( original.getWidth(), original.getHeight() ).immuneTo( original.immuneTo.toArray( new Block[0] ) ) + // Note: the below are (or also have) suppliers and they cannot be built with the vanilla builder + // - this is okay for us because we only replace vanilla mobs + .clientTrackingRange( original.clientTrackingRange() ).updateInterval( original.updateInterval() ) + .setShouldReceiveVelocityUpdates( original.trackDeltas() ); + } + } +} \ No newline at end of file diff --git a/src/main/java/fathertoast/specialmobs/common/bestiary/SpecialMob.java b/src/main/java/fathertoast/specialmobs/common/bestiary/SpecialMob.java new file mode 100644 index 0000000..73acdde --- /dev/null +++ b/src/main/java/fathertoast/specialmobs/common/bestiary/SpecialMob.java @@ -0,0 +1,73 @@ +package fathertoast.specialmobs.common.bestiary; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Used to note classes that define special mobs entities. Think of this as a kind of static interface that defines + * a number of static methods that must be implemented in each special mob class. + *

+ * Currently no runtime processing is done on special mob classes, however this could be modified in the future + * to fully automate variant registration, if desired. + */ +@Target( ElementType.TYPE ) +public @interface SpecialMob { + + /** + * REQUIRED. This is grabbed during registration to be used as a mob 'factory'; the needed constructor will probably + * already be needed by the entity's superclass, so it's just a matter of applying the annotation in that case. + *

+ * The annotated constructor must have a signature that follows the pattern: + *

+ * public CLASS_NAME( EntityType entityType, World world ) + */ + @Retention( RetentionPolicy.RUNTIME ) + @Target( ElementType.CONSTRUCTOR ) + @interface Constructor { } + + /** + * REQUIRED. This is called during registration to collect static properties of the mob needed for the bestiary + * and for building the species's entity type. + *

+ * The annotated method must have a signature that follows the pattern: + *

+ * public static BestiaryInfo METHOD_NAME( EntityType.Builder entityType ) + *

+ * The returned bestiary info will be used to describe the mob species. + * The builder passed in is a copy of the vanilla 'base' entity type. Common uses of the entity type builder are + * modifying entity size and fire/hazard immunities. + */ + @Retention( RetentionPolicy.RUNTIME ) + @Target( ElementType.METHOD ) + @interface BestiaryInfoSupplier { } + + /** + * REQUIRED. This is called during registration to build the base attributes for the species. + *

+ * The annotated method must have a signature that follows the pattern: + *

+ * public static AttributeModifierMap.MutableAttribute METHOD_NAME() + *

+ * The returned attribute modifier map builder will be 'built' immediately after the call and registered to be + * applied to all entity class instances of the mob species. + */ + @Retention( RetentionPolicy.RUNTIME ) + @Target( ElementType.METHOD ) + @interface AttributeCreator { } + + /** + * REQUIRED. This is called during data generation to build the mob's default loot table. Special variants will + * typically start this method by calling their vanilla replacement's implementation of this method. + *

+ * The annotated method must have a signature that follows the pattern: + *

+ * public static void METHOD_NAME( LootTableBuilder loot ) + *

+ * The builder passed in is a new empty instance and will be 'built' immediately after the call. + */ + @Retention( RetentionPolicy.RUNTIME ) + @Target( ElementType.METHOD ) + @interface LootTableProvider { } +} \ No newline at end of file diff --git a/src/main/java/fathertoast/specialmobs/common/core/SpecialMobs.java b/src/main/java/fathertoast/specialmobs/common/core/SpecialMobs.java index 24d0b58..cbfaf46 100644 --- a/src/main/java/fathertoast/specialmobs/common/core/SpecialMobs.java +++ b/src/main/java/fathertoast/specialmobs/common/core/SpecialMobs.java @@ -1,12 +1,12 @@ package fathertoast.specialmobs.common.core; +import fathertoast.specialmobs.common.bestiary.MobFamily; import fathertoast.specialmobs.common.config.Config; +import fathertoast.specialmobs.common.core.register.SMEntities; +import fathertoast.specialmobs.common.core.register.SMItems; import net.minecraft.util.ResourceLocation; -import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.eventbus.api.IEventBus; -import net.minecraftforge.fml.ModList; import net.minecraftforge.fml.common.Mod; -import net.minecraftforge.fml.event.lifecycle.InterModProcessEvent; import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; import net.minecraftforge.registries.ForgeRegistryEntry; import org.apache.logging.log4j.LogManager; @@ -17,14 +17,77 @@ import javax.annotation.Nullable; @Mod( SpecialMobs.MOD_ID ) public class SpecialMobs { - /* Feature List: - * TODO + /* TODO List: + * Reimplement all old features (see list below) + * Utility features: + * - Bestiary + */ + + /* Feature List: //TODO; list may not be complete + * (KEY: - = complete in current version, o = incomplete feature from previous version, + * + = incomplete new feature, ? = feature to consider adding) + * o general + * o entity replacer + * o dimension-sensitive configs + * o environment-sensitive configs + * o entities + * o nbt-driven capabilities (special mob data) + * + bestiary + * ? configurable stats + * o monster families (see doc for specifics) + * o creepers + * o chance to spawn charged + * + scope + * o zombies + * o villager infection + * o ranged attack AI (using bow) + * o use shields + * + drowned + * o zombified piglins + * o ranged attack AI (using bow) + * o use shields + * o skeletons + * o use shields + * o babies + * o wither skeletons + * o use shields + * o babies + * o slimes + * o use attack damage attribute + * o magma cubes + * o use attack damage attribute + * o spiders + * o ranged attack AI + * o cave spiders + * o ranged attack AI + * o silverfish + * ? ranged attack AI + * o endermen + * o witches + * o ability to equip held items + * o ghasts + * o melee attack AI + * o blazes + * o melee attack AI + * ? piglins + * ? hoglins + * ? zoglins + * ? endermites + * ? guardians + * ? shulkers + * ? phantoms + * + the goat */ /** Our mod ID. */ @SuppressWarnings( "SpellCheckingInspection" ) public static final String MOD_ID = "specialmobs"; + /** The path to the textures folder. */ + public static final String TEXTURE_PATH = MOD_ID + ":textures/entity/"; + /** The path to the loot tables folder. */ + public static final String LOOT_TABLE_PATH = MOD_ID + ":entities/"; + /** Logger instance for the mod. */ public static final Logger LOG = LogManager.getLogger( MOD_ID ); @@ -32,46 +95,23 @@ public class SpecialMobs { //@SuppressWarnings( "FieldCanBeLocal" ) //private final PacketHandler packetHandler = new PacketHandler(); - //** Mod API instance **/ - //private final INaturalAbsorption modApi = new NaturalAbsorptionAPI(); - - public SpecialMobs() { Config.initialize(); //packetHandler.registerMessages(); - //CraftingUtil.registerConditions(); //MinecraftForge.EVENT_BUS.register( new NAEventListener() ); - //MinecraftForge.EVENT_BUS.register( new HeartManager() ); - IEventBus modBus = FMLJavaModLoadingContext.get().getModEventBus(); + IEventBus eventBus = FMLJavaModLoadingContext.get().getModEventBus(); - //modBus.addListener( this::onInterModProcess ); - //modBus.addListener( HeartManager::onEntityAttributeCreation ); + eventBus.addListener( SMEntities::createAttributes ); - //NAItems.ITEMS.register( modBus ); - //NAAttributes.ATTRIBUTES.register( modBus ); - //NAEnchantments.ENCHANTMENTS.register( modBus ); - //NALootModifiers.LOOT_MODIFIER_SERIALIZERS.register( modBus ); + SMEntities.REGISTRY.register( eventBus ); + SMItems.REGISTRY.register( eventBus ); - //if( ModList.get().isLoaded( "tconstruct" ) ) { - // NaturalAbsorptionTC.init( modBus ); - //} + MobFamily.initBestiary(); } -// /** -// * Hands the mod API to mods that ask for it. -// */ -// private void onInterModProcess( InterModProcessEvent event ) { -// event.getIMCStream().forEach( ( message ) -> { -// if( message.getMethod().equals( "getNaturalAbsorptionAPI" ) ) { -// Supplier> supplier = message.getMessageSupplier(); -// supplier.get().apply( modApi ); -// } -// } ); -// } - /** @return A ResourceLocation with the mod's namespace. */ public static ResourceLocation resourceLoc( String path ) { return new ResourceLocation( MOD_ID, path ); } diff --git a/src/main/java/fathertoast/specialmobs/common/core/register/SMEntities.java b/src/main/java/fathertoast/specialmobs/common/core/register/SMEntities.java new file mode 100644 index 0000000..6d9402f --- /dev/null +++ b/src/main/java/fathertoast/specialmobs/common/core/register/SMEntities.java @@ -0,0 +1,45 @@ +package fathertoast.specialmobs.common.core.register; + +import fathertoast.specialmobs.common.bestiary.MobFamily; +import fathertoast.specialmobs.common.core.SpecialMobs; +import fathertoast.specialmobs.common.util.AnnotationHelper; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityType; +import net.minecraftforge.event.entity.EntityAttributeCreationEvent; +import net.minecraftforge.fml.RegistryObject; +import net.minecraftforge.registries.DeferredRegister; +import net.minecraftforge.registries.ForgeRegistries; + +import java.lang.reflect.InvocationTargetException; + +public class SMEntities { + + public static final DeferredRegister> REGISTRY = DeferredRegister.create( ForgeRegistries.ENTITIES, SpecialMobs.MOD_ID ); + + /** Registers an entity type to the deferred register. */ + public static RegistryObject> register( String name, EntityType.Builder builder ) { + return REGISTRY.register( name, () -> builder.build( name ) ); + } + + /** Sets the default attributes for entity types, such as max health, attack damage etc. */ + public static void createAttributes( EntityAttributeCreationEvent event ) { + // Bestiary-generated entities + for( MobFamily.Species variant : MobFamily.getAllSpecies() ) + createSpeciesAttributes( event, variant ); + } + + /** Builds the attributes for a specific entity species. */ + private static void createSpeciesAttributes( EntityAttributeCreationEvent event, MobFamily.Species species ) { + try { + event.put( species.entityType.get(), AnnotationHelper.createAttributes( species ) ); + } + catch( NoSuchMethodException | InvocationTargetException | IllegalAccessException ex ) { + throw new RuntimeException( "Entity class for " + species.name + " has invalid attribute creation method", ex ); + } + } + + /** Sets the natural spawn placement rules for entity types. */ + public static void registerSpawnPlacements() { + //TODO + } +} \ No newline at end of file diff --git a/src/main/java/fathertoast/specialmobs/common/core/register/SMItems.java b/src/main/java/fathertoast/specialmobs/common/core/register/SMItems.java new file mode 100644 index 0000000..16e7e0f --- /dev/null +++ b/src/main/java/fathertoast/specialmobs/common/core/register/SMItems.java @@ -0,0 +1,25 @@ +package fathertoast.specialmobs.common.core.register; + +import fathertoast.specialmobs.common.core.SpecialMobs; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityType; +import net.minecraft.item.Item; +import net.minecraft.item.ItemGroup; +import net.minecraftforge.common.ForgeSpawnEggItem; +import net.minecraftforge.fml.RegistryObject; +import net.minecraftforge.registries.DeferredRegister; +import net.minecraftforge.registries.ForgeRegistries; + +public class SMItems { + + public static final DeferredRegister REGISTRY = DeferredRegister.create( ForgeRegistries.ITEMS, SpecialMobs.MOD_ID ); + + /** Registers an entity type's spawn egg item to the deferred register. */ + public static RegistryObject registerSpawnEgg( + RegistryObject> entityType, int eggBaseColor, int eggSpotsColor ) { + final String name = entityType.getId().getPath() + "_spawn_egg"; + return REGISTRY.register( name, () -> + new ForgeSpawnEggItem( entityType, eggBaseColor, eggSpotsColor, new Item.Properties().tab( ItemGroup.TAB_MISC ) ) + ); + } +} \ No newline at end of file diff --git a/src/main/java/fathertoast/specialmobs/common/entity/ISpecialMob.java b/src/main/java/fathertoast/specialmobs/common/entity/ISpecialMob.java new file mode 100644 index 0000000..5fd5abd --- /dev/null +++ b/src/main/java/fathertoast/specialmobs/common/entity/ISpecialMob.java @@ -0,0 +1,18 @@ +package fathertoast.specialmobs.common.entity; + +import net.minecraft.entity.LivingEntity; +import net.minecraft.pathfinding.PathNodeType; + +public interface ISpecialMob> { + /** @return This mob's special data. */ + SpecialMobData getSpecialData(); + + /** @return The experience that should be dropped by the entity. */ + int getExperience(); + + /** Sets the experience that should be dropped by the entity. */ + void setExperience( int xp ); + + /** Sets the entity's pathfinding malus for a particular node type; negative value is un-walkable. */ + void setPathfindingMalus( PathNodeType nodeType, float malus ); +} \ No newline at end of file diff --git a/src/main/java/fathertoast/specialmobs/common/entity/SpecialMobData.java b/src/main/java/fathertoast/specialmobs/common/entity/SpecialMobData.java new file mode 100644 index 0000000..1b42d5f --- /dev/null +++ b/src/main/java/fathertoast/specialmobs/common/entity/SpecialMobData.java @@ -0,0 +1,591 @@ +package fathertoast.specialmobs.common.entity; + +import fathertoast.specialmobs.common.core.SpecialMobs; +import net.minecraft.block.Block; +import net.minecraft.block.BlockState; +import net.minecraft.block.Blocks; +import net.minecraft.enchantment.EnchantmentHelper; +import net.minecraft.enchantment.Enchantments; +import net.minecraft.entity.CreatureAttribute; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.ai.attributes.Attribute; +import net.minecraft.entity.ai.attributes.ModifiableAttributeInstance; +import net.minecraft.entity.monster.SpiderEntity; +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.potion.Effects; +import net.minecraft.util.ResourceLocation; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.event.entity.living.PotionEvent; + +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 collision box scale of this variant's family. */ + private final float familyScale; + /** The base collision box scale of this variant. */ + private float baseScale; + + /** 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 damage the entity uses for its ranged attacks, when applicable. */ + public float rangedAttackDamage; + /** The spread (inaccuracy) of the entity's ranged attacks. */ + public float rangedAttackSpread; + /** The movement speed multiplier the entity uses during its ranged attack ai. Requires an ai reload to take effect. */ + public float rangedWalkSpeed = 1.0F; + /** The delay (in ticks) before a new ranged attack can begin after firing. Requires an ai reload to take effect. */ + public 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 the cooldown + charge time. + * For all other attacks, this is the cooldown at maximum range (scaled down to the minimum cooldown at point-blank). + */ + public 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. + */ + public float rangedAttackMaxRange; + + /** 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 = 1.0F; + + /** 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 be leashed. */ + private boolean allowLeashing; + /** Whether the entity does not trigger pressure plates. */ + private boolean ignorePressurePlates; + + /** 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; + + /** List of blocks that the entity cannot be stuck in. */ + private final HashSet immuneToStickyBlocks = new HashSet<>(); + /** List of potions that cannot be applied to the entity. */ + private final HashSet immuneToPotions = new HashSet<>(); + + /** + * Constructs a SpecialMobData to store generic data about a mob. + * + * @param entity The entity to store data for. + * @param scale Data parameter for storing the render scale. + */ + public SpecialMobData( T entity, DataParameter scale ) { + this( entity, scale, 1.0F ); + } + + /** + * 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 is the only thing actually done while constructing. + *

+ * The #initialize() method must be called later on to complete initialization (e.g. in the entity constructor). + * + * @param entity The entity to store data for. + * @param scale Data parameter for storing the render scale. + * @param familyBaseScale Base render scale. Typically 1.0F. + */ + public SpecialMobData( T entity, DataParameter scale, float familyBaseScale ) { + theEntity = entity; + renderScale = scale; + + familyScale = baseScale = familyBaseScale; + + //setTextures( entity.getDefaultTextures() ); TODO + + entity.getEntityData().define( renderScale, nextScale() ); + } + + /** Called to finish initialization, since we can only define data watcher params in the constructor. */ + public void initialize() { + setImmuneToFire( theEntity.getType().fireImmune() ); + if( theEntity.getMobType() == CreatureAttribute.UNDEAD ) { + addPotionImmunity( Effects.REGENERATION, Effects.POISON ); + } + if( theEntity instanceof SpiderEntity ) { + addStickyBlockImmunity( Blocks.COBWEB ); + addPotionImmunity( Effects.POISON ); + } + } + + /** 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 ); + } + } + } + + /** + * Alters the entity's base attribute by adding an amount to it. + * Do NOT use this for move speed, instead use {@link SpecialMobData#multAttribute(Attribute, double)} + * + * @param attribute the attribute to modify + * @param amount the amount to add to the attribute + */ + public void addAttribute( Attribute attribute, double amount ) { + final ModifiableAttributeInstance attributeInstance = theEntity.getAttribute( attribute ); + if( attributeInstance == null ) + throw new IllegalStateException( "Special mob '" + theEntity + "' does not have registered attribute " + + attribute.getDescriptionId() ); + attributeInstance.setBaseValue( attributeInstance.getBaseValue() + amount ); + } + + /** + * Alters the entity's base attribute by multiplying it by an amount. + * Only use this for move speed, for other attributes use {@link SpecialMobData#addAttribute(Attribute, double)} + * + * @param attribute the attribute to modify + * @param amount the amount to multiply the attribute by + */ + public void multAttribute( Attribute attribute, double amount ) { + final ModifiableAttributeInstance attributeInstance = theEntity.getAttribute( attribute ); + if( attributeInstance == null ) + throw new IllegalStateException( "Special mob '" + theEntity + "' does not have registered attribute " + + attribute.getDescriptionId() ); + attributeInstance.setBaseValue( attributeInstance.getBaseValue() * amount ); + } + + /** + * @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]; + if( textures.length > 1 ) { + textureEyes = textures[1]; + } + if( textures.length > 2 ) { + textureOverlay = textures[2]; + } + } + + /** + * @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. */ + public float getRenderScale() { return theEntity.getEntityData().get( renderScale ); } + + public void setRenderScale( float scale ) { + if( !theEntity.level.isClientSide ) { + theEntity.getEntityData().set( renderScale, scale ); + } + } + + public float getFamilyBaseScale() { return familyScale; } + + public float getBaseScaleForPreScaledValues() { return getBaseScale() / getFamilyBaseScale(); } + + public float getBaseScale() { return baseScale; } + + public void setBaseScale( float newBaseScale ) { + baseScale = newBaseScale; + setRenderScale( nextScale() ); + } + + private float nextScale() { + // if( Config.get().GENERAL.RANDOM_SCALING > 0.0F ) { TODO configs + // return baseScale * (1.0F + (theEntity.getRNG().nextFloat() - 0.5F) * Config.get().GENERAL.RANDOM_SCALING); + // } + return baseScale; + } + + public double getRangedDamage( float distanceFactor ) { + final int powerEnchant = EnchantmentHelper.getEnchantmentLevel( Enchantments.POWER_ARROWS, theEntity ); + return rangedAttackDamage * (distanceFactor + + theEntity.getRandom().nextGaussian() * 0.125 + + theEntity.level.getDifficulty().getId() * 0.055F) + + (powerEnchant > 0 ? powerEnchant * 0.5 + 0.5 : 0.0); + } + + public void setRegenerationTime( int ticks ) { healTimeMax = ticks; } + + public float getFallDamageMultiplier() { return fallDamageMultiplier; } + + public void setFallDamageMultiplier( float value ) { fallDamageMultiplier = value; } + + public boolean isImmuneToFire() { return isImmuneToFire; } + + public 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; } + + public 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; } + + public void setAllowLeashing( boolean value ) { allowLeashing = value; } + + public boolean ignorePressurePlates() { return ignorePressurePlates; } + + public void setIgnorePressurePlates( boolean value ) { ignorePressurePlates = value; } + + public boolean canBreatheInWater() { return canBreatheInWater; } + + public void setCanBreatheInWater( boolean value ) { canBreatheInWater = value; } + + public boolean ignoreWaterPush() { return ignoreWaterPush; } + + public void setIgnoreWaterPush( boolean value ) { ignoreWaterPush = value; } + + public boolean isDamagedByWater() { return isDamagedByWater; } + + public 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().getDescriptionId() ); } + + /** @param blocks The sticky block(s) to grant immunity from. */ + public void addStickyBlockImmunity( Block... blocks ) { + for( Block block : blocks ) immuneToStickyBlocks.add( block.getDescriptionId() ); + } + + /** + * 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.getDescriptionId() ); + } + } + + /** @param effects The effect(s) to grant immunity from. */ + public void addPotionImmunity( Effect... effects ) { + for( Effect effect : effects ) immuneToPotions.add( effect.getDescriptionId() ); + } + + /** + * Saves this data to NBT. + * + * @param tag The tag to save to. + */ + public void writeToNBT( CompoundNBT tag ) { + tag.putFloat( TAG_RENDER_SCALE, getRenderScale() ); + tag.putInt( TAG_EXPERIENCE, theEntity.getExperience() ); + tag.putByte( TAG_REGENERATION, (byte) healTimeMax ); + + //tag.putString( TAG_TEXTURE, texture.toString() );TODO textures + tag.putString( TAG_TEXTURE_EYES, textureEyes == null ? "" : textureEyes.toString() ); + tag.putString( TAG_TEXTURE_OVER, textureOverlay == null ? "" : textureOverlay.toString() ); + + // Arrow AI + tag.putFloat( TAG_ARROW_DAMAGE, rangedAttackDamage ); + tag.putFloat( TAG_ARROW_SPREAD, rangedAttackSpread ); + tag.putFloat( TAG_ARROW_WALK_SPEED, rangedWalkSpeed ); + tag.putShort( TAG_ARROW_REFIRE_MIN, (short) rangedAttackCooldown ); + tag.putShort( TAG_ARROW_REFIRE_MAX, (short) rangedAttackMaxCooldown ); + tag.putFloat( TAG_ARROW_RANGE, rangedAttackMaxRange ); + + // Abilities + 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( String blockName : immuneToStickyBlocks ) { + stickyBlocksTag.add( StringNBT.valueOf( blockName ) ); + } + tag.put( TAG_STICKY_IMMUNE, stickyBlocksTag ); + + final ListNBT potionsTag = new ListNBT(); + for( String potionName : immuneToPotions ) { + potionsTag.add( StringNBT.valueOf( potionName ) ); + } + tag.put( TAG_POTION_IMMUNE, potionsTag ); + } + + /** + * 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 ) ); + } + 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 ); + } + + 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() ); + } + + // Arrow AI + if( tag.contains( TAG_ARROW_DAMAGE, NBT_TYPE_NUMERICAL ) ) { + rangedAttackDamage = tag.getFloat( TAG_ARROW_DAMAGE ); + } + if( tag.contains( TAG_ARROW_SPREAD, NBT_TYPE_NUMERICAL ) ) { + rangedAttackSpread = tag.getFloat( TAG_ARROW_SPREAD ); + } + if( tag.contains( TAG_ARROW_WALK_SPEED, NBT_TYPE_NUMERICAL ) ) { + rangedWalkSpeed = tag.getFloat( TAG_ARROW_WALK_SPEED ); + } + if( tag.contains( TAG_ARROW_REFIRE_MIN, NBT_TYPE_NUMERICAL ) ) { + rangedAttackCooldown = tag.getShort( TAG_ARROW_REFIRE_MIN ); + } + if( tag.contains( TAG_ARROW_REFIRE_MAX, NBT_TYPE_NUMERICAL ) ) { + rangedAttackMaxCooldown = tag.getShort( TAG_ARROW_REFIRE_MAX ); + } + if( tag.contains( TAG_ARROW_RANGE, NBT_TYPE_NUMERICAL ) ) { + rangedAttackMaxRange = tag.getFloat( TAG_ARROW_RANGE ); + } + + // Abilities + 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_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_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_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++ ) { + immuneToStickyBlocks.add( stickyBlocksTag.getString( i ) ); + } + } + 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++ ) { + immuneToPotions.add( potionsTag.getString( i ) ); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/fathertoast/specialmobs/common/entity/creeper/DarkCreeperEntity.java b/src/main/java/fathertoast/specialmobs/common/entity/creeper/DarkCreeperEntity.java new file mode 100644 index 0000000..49d9be0 --- /dev/null +++ b/src/main/java/fathertoast/specialmobs/common/entity/creeper/DarkCreeperEntity.java @@ -0,0 +1,130 @@ +package fathertoast.specialmobs.common.entity.creeper; + +import fathertoast.specialmobs.common.bestiary.BestiaryInfo; +import fathertoast.specialmobs.common.bestiary.SpecialMob; +import fathertoast.specialmobs.datagen.loot.LootTableBuilder; +import mcp.MethodsReturnNonnullByDefault; +import net.minecraft.block.BlockState; +import net.minecraft.block.Blocks; +import net.minecraft.entity.EntityType; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.ai.attributes.AttributeModifierMap; +import net.minecraft.entity.monster.CreeperEntity; +import net.minecraft.fluid.FluidState; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import net.minecraft.potion.EffectInstance; +import net.minecraft.potion.Effects; +import net.minecraft.potion.PotionUtils; +import net.minecraft.potion.Potions; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.EntityExplosionContext; +import net.minecraft.world.Explosion; +import net.minecraft.world.World; +import net.minecraft.world.server.ServerWorld; + +import javax.annotation.ParametersAreNonnullByDefault; +import java.util.List; +import java.util.Optional; + +@ParametersAreNonnullByDefault +@MethodsReturnNonnullByDefault +@SpecialMob +public class DarkCreeperEntity extends _SpecialCreeperEntity { + + //--------------- Static Special Mob Hooks ---------------- + + @SpecialMob.BestiaryInfoSupplier + public static BestiaryInfo bestiaryInfo( EntityType.Builder entityType ) { + return new BestiaryInfo( 0xf9ff3a ); + } + + @SpecialMob.AttributeCreator + public static AttributeModifierMap.MutableAttribute createAttributes() { + return _SpecialCreeperEntity.createAttributes(); + } + + @SpecialMob.LootTableProvider + public static void buildLootTable( LootTableBuilder loot ) { + addBaseLoot( loot ); + loot.addClusterDrop( "common", Blocks.TORCH ); + loot.addRareDrop( "rare", PotionUtils.setPotion( new ItemStack( Items.POTION ), Potions.NIGHT_VISION ) ); + } + + @SpecialMob.Constructor + public DarkCreeperEntity( EntityType entityType, World world ) { + super( entityType, world ); + } + + + //--------------- Variant-Specific Implementations ---------------- + + /** Override to change this creeper's explosion power multiplier. */ + protected float getVariantExplosionPower() { return super.getVariantExplosionPower() / 2.0F; } + + /** Override to change this creeper's explosion. */ + @Override + protected void makeVariantExplosion( float explosionPower, Explosion.Mode explosionMode ) { + final EntityExplosionContext damageCalculator = new EntityExplosionContext( this ); + final Explosion explosion = new Explosion( level, this, null, + damageCalculator, getX(), getY(), getZ(), explosionPower, false, explosionMode ); + + if( net.minecraftforge.event.ForgeEventFactory.onExplosionStart( level, explosion ) ) return; + explosion.explode(); + + // Add unaffected light sources to the explosion's affected area + // Note that this does NOT simulate another explosion, instead just directly searches for and targets lights + final BlockPos center = new BlockPos( getX(), getY(), getZ() ); + final int radius = explosionRadius * 4 * (isPowered() ? 2 : 1); + final BlockPos.Mutable pos = new BlockPos.Mutable(); + for( int y = -radius; y <= radius; y++ ) { + for( int x = -radius; x <= radius; x++ ) { + for( int z = -radius; z <= radius; z++ ) { + if( x * x + y * y + z * z <= radius * radius ) { + pos.set( center.getX() + x, center.getY() + y, center.getZ() + z ); + final BlockState block = level.getBlockState( pos ); + + // Ignore the block if it is not a light or is already exploded + if( block.getLightValue( level, pos ) > 1 && !explosion.getToBlow().contains( pos ) ) { + float blockDamage = (float) radius * (0.7F + random.nextFloat() * 0.6F); + final FluidState fluid = level.getFluidState( pos ); + final Optional optional = damageCalculator.getBlockExplosionResistance( + explosion, level, pos, block, fluid ); + if( optional.isPresent() ) { + blockDamage -= (optional.get() + 0.3F) * 0.3F; + } + if( blockDamage > 0.0F && damageCalculator.shouldBlockExplode( explosion, level, pos, block, blockDamage ) ) { + explosion.getToBlow().add( pos ); + } + } + } + } + } + } + + explosion.finalizeExplosion( true ); + + // Move the time forward to next night if powered + if( isPowered() && level instanceof ServerWorld ) { + // Days are 24k ticks long; find how far along we are in the current day (0-23,999) + long time = level.getDayTime(); + final int dayTime = (int) (time % 24_000L); + + // We decide that night starts at 13k ticks + time += 13_000L - dayTime; + // If we were already past the 13k point today, advance to the next night entirely + if( dayTime > 13_000 ) time += 24_000L; + + ((ServerWorld) level).setDayTime( time ); + } + } + + /** + * Override to change effects applied by the lingering cloud left by this creeper's explosion. + * If this list is empty, the lingering cloud is not created. + */ + @Override + protected void modifyVariantLingeringCloudEffects( List potions ) { + potions.add( new EffectInstance( Effects.BLINDNESS, 100 ) ); + } +} \ No newline at end of file diff --git a/src/main/java/fathertoast/specialmobs/common/entity/creeper/_SpecialCreeperEntity.java b/src/main/java/fathertoast/specialmobs/common/entity/creeper/_SpecialCreeperEntity.java new file mode 100644 index 0000000..2afeaba --- /dev/null +++ b/src/main/java/fathertoast/specialmobs/common/entity/creeper/_SpecialCreeperEntity.java @@ -0,0 +1,380 @@ +package fathertoast.specialmobs.common.entity.creeper; + +import fathertoast.specialmobs.common.bestiary.BestiaryInfo; +import fathertoast.specialmobs.common.bestiary.SpecialMob; +import fathertoast.specialmobs.common.core.SpecialMobs; +import fathertoast.specialmobs.common.entity.ISpecialMob; +import fathertoast.specialmobs.common.entity.SpecialMobData; +import fathertoast.specialmobs.common.util.References; +import fathertoast.specialmobs.datagen.loot.LootTableBuilder; +import mcp.MethodsReturnNonnullByDefault; +import net.minecraft.block.BlockState; +import net.minecraft.entity.AreaEffectCloudEntity; +import net.minecraft.entity.EntityType; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.ai.attributes.AttributeModifierMap; +import net.minecraft.entity.effect.LightningBoltEntity; +import net.minecraft.entity.monster.CreeperEntity; +import net.minecraft.nbt.CompoundNBT; +import net.minecraft.network.datasync.DataParameter; +import net.minecraft.network.datasync.DataSerializers; +import net.minecraft.network.datasync.EntityDataManager; +import net.minecraft.pathfinding.PathNodeType; +import net.minecraft.potion.EffectInstance; +import net.minecraft.util.DamageSource; +import net.minecraft.util.ResourceLocation; +import net.minecraft.util.math.vector.Vector3d; +import net.minecraft.world.Explosion; +import net.minecraft.world.World; +import net.minecraft.world.server.ServerWorld; + +import javax.annotation.ParametersAreNonnullByDefault; +import java.util.ArrayList; +import java.util.List; + +@ParametersAreNonnullByDefault +@MethodsReturnNonnullByDefault +@SpecialMob +public class _SpecialCreeperEntity extends CreeperEntity implements ISpecialMob<_SpecialCreeperEntity> { + + //--------------- Static Special Mob Hooks ---------------- + + @SpecialMob.BestiaryInfoSupplier + public static BestiaryInfo bestiaryInfo( EntityType.Builder entityType ) { + return new BestiaryInfo( 0x000000 ); + } + + @SpecialMob.AttributeCreator + public static AttributeModifierMap.MutableAttribute createAttributes() { + return CreeperEntity.createAttributes(); + } + + @SpecialMob.LootTableProvider + public static void addBaseLoot( LootTableBuilder loot ) { + loot.addLootTable( "main", EntityType.CREEPER.getDefaultLootTable() ); + } + + @SpecialMob.Constructor + public _SpecialCreeperEntity( EntityType entityType, World world ) { + super( entityType, world ); + specialData.initialize(); + } + + + //--------------- Variant-Specific Breakouts ---------------- + + /** Called in the MobEntity.class constructor to initialize AI goals. */ + @Override + protected void registerGoals() { + super.registerGoals(); + registerVariantGoals(); + } + + /** Override to change this entity's AI goals. */ + protected void registerVariantGoals() { } + + /** Called to perform this creeper's explosion 'attack'. */ + @Override + protected void explodeCreeper() { + if( !level.isClientSide ) { + final Explosion.Mode explosionMode = net.minecraftforge.event.ForgeEventFactory.getMobGriefingEvent( level, this ) ? + Explosion.Mode.DESTROY : Explosion.Mode.NONE; + final float explosionPower = getVariantExplosionPower(); + dead = true; + makeVariantExplosion( explosionPower, explosionMode ); + remove(); + spawnLingeringCloud(); + } + } + + /** Override to change this creeper's explosion power multiplier. */ + protected float getVariantExplosionPower() { return isPowered() ? 2.0F : 1.0F; } + + /** Override to change this creeper's explosion. */ + protected void makeVariantExplosion( float explosionPower, Explosion.Mode explosionMode ) { + level.explode( this, getX(), getY(), getZ(), (float) explosionRadius * explosionPower, explosionMode ); + } + + /** Called to create a lingering effect cloud as part of this creeper's explosion 'attack'. */ + @Override + protected void spawnLingeringCloud() { + final List effects = new ArrayList<>( getActiveEffects() ); + modifyVariantLingeringCloudEffects( effects ); + + if( !effects.isEmpty() ) { + final AreaEffectCloudEntity potionCloud = new AreaEffectCloudEntity( level, getX(), getY(), getZ() ); + potionCloud.setRadius( (explosionRadius - 0.5F) * getVariantExplosionPower() ); + potionCloud.setRadiusOnUse( -0.5F ); + potionCloud.setWaitTime( 10 ); + potionCloud.setDuration( potionCloud.getDuration() / 2 ); + potionCloud.setRadiusPerTick( -potionCloud.getRadius() / (float) potionCloud.getDuration() ); + for( EffectInstance effect : effects ) { + potionCloud.addEffect( new EffectInstance( effect ) ); + } + modifyVariantLingeringCloud( potionCloud ); + level.addFreshEntity( potionCloud ); + } + } + + /** + * Override to change effects applied by the lingering cloud left by this creeper's explosion. + * If this list is empty, the lingering cloud is not created. + */ + protected void modifyVariantLingeringCloudEffects( List potions ) { } + + /** Override to change stats of the lingering cloud left by this creeper's explosion. */ + protected void modifyVariantLingeringCloud( AreaEffectCloudEntity potionCloud ) { } + + + //--------------- Family-Specific Implementations ---------------- + + /** The parameter for special mob render scale. */ + private static final DataParameter SCALE = EntityDataManager.defineId( _SpecialCreeperEntity.class, DataSerializers.FLOAT ); + + /** Called from the Entity.class constructor to define data watcher variables. */ + @Override + protected void defineSynchedData() { + super.defineSynchedData(); + specialData = new SpecialMobData<>( this, SCALE, 1.0F ); + entityData.define( EXPLODE_FLAGS, (byte) 0 ); + } + + /** Called each tick to update this entity. */ + @Override + public void tick() { + if( isAlive() ) { + // Implement "cannot explode while wet" property + if( isInWaterRainOrBubble() && cannotExplodeWhileWet() ) { + if( isIgnited() ) entityData.set( DATA_IS_IGNITED, false ); + setSwellDir( -1 ); + } + // Implement "explodes while burning" property + else if( isOnFire() && explodesWhileBurning() ) { + setSwellDir( 1 ); + } + } + super.tick(); + } + + /** @return Attempts to damage this entity; returns true if the hit was successful. */ + @Override + public boolean hurt( DamageSource damageSource, float damage ) { + if( super.hurt( damageSource, damage ) ) { + // Implement "explodes when shot" property + if( damageSource.getDirectEntity() != damageSource.getEntity() && explodesWhenShot() ) ignite(); + return true; + } + return false; + } + + /** Called when this entity is struck by lightning. */ + @Override + public void thunderHit( ServerWorld world, LightningBoltEntity lightningBolt ) { + super.thunderHit( world, lightningBolt ); + + // Make it less likely for charged "explode while burning" creepers to immediately explode + if( explodesWhileBurning() ) clearFire(); + } + + // Called when this entity is first spawned to initialize it. TODO chance to be charged on spawn + // @Override + // @Nullable + // public IEntityLivingData onInitialSpawn( DifficultyInstance difficulty, @Nullable IEntityLivingData data ) { + // data = super.onInitialSpawn( difficulty, data ); + // + // if( Entity_SpecialCreeper.POWERED != null && world.isThundering() && rand.nextDouble() < Config.get().CREEPERS.CHARGED_CHANCE ) { + // dataManager.set( Entity_SpecialCreeper.POWERED, true ); + // } + // + // return data; + // } + + + //--------------- Creeper Explosion Property Setters/Getters ---------------- + + /** The parameter for creeper explosion properties. This is a combination of boolean flags. */ + private static final DataParameter EXPLODE_FLAGS = EntityDataManager.defineId( _SpecialCreeperEntity.class, DataSerializers.BYTE ); + + /** The bit for "cannot explode while wet". */ + private static final byte EXPLODE_FLAG_DEFUSE_IN_WATER = 0b0001; + /** The bit for "explodes while burning". */ + private static final byte EXPLODE_FLAG_ON_FIRE = 0b0010; + /** The bit for "explodes when shot". */ + private static final byte EXPLODE_FLAG_WHEN_SHOT = 0b0100; + + /** @return True if this creeper is unable to explode while wet. */ + public boolean cannotExplodeWhileWet() { return getExplodeFlag( EXPLODE_FLAG_DEFUSE_IN_WATER ); } + + /** Sets this creeper's capability to explode while wet. */ + public void setCannotExplodeWhileWet( boolean value ) { + setExplodeFlag( EXPLODE_FLAG_DEFUSE_IN_WATER, value ); + setPathfindingMalus( PathNodeType.WATER, value ? PathNodeType.LAVA.getMalus() : PathNodeType.WATER.getMalus() ); + } + + /** @return True if this creeper explodes while burning. */ + public boolean explodesWhileBurning() { return getExplodeFlag( EXPLODE_FLAG_ON_FIRE ); } + + /** Sets this creeper's property to explode while burning. */ + public void setExplodesWhileBurning( boolean value ) { + setExplodeFlag( EXPLODE_FLAG_ON_FIRE, value ); + if( value ) { + setPathfindingMalus( PathNodeType.DANGER_FIRE, PathNodeType.DAMAGE_FIRE.getMalus() ); + setPathfindingMalus( PathNodeType.DAMAGE_FIRE, PathNodeType.BLOCKED.getMalus() ); + } + else { + setPathfindingMalus( PathNodeType.DANGER_FIRE, PathNodeType.DANGER_FIRE.getMalus() ); + setPathfindingMalus( PathNodeType.DAMAGE_FIRE, PathNodeType.DAMAGE_FIRE.getMalus() ); + } + } + + /** @return True if this creeper explodes when shot. */ + public boolean explodesWhenShot() { return getExplodeFlag( EXPLODE_FLAG_WHEN_SHOT ); } + + /** Sets this creeper's property to explode when shot. */ + public void setExplodesWhenShot( boolean value ) { setExplodeFlag( EXPLODE_FLAG_WHEN_SHOT, value ); } + + /** @return The value for a specific explode flag. */ + private boolean getExplodeFlag( byte flag ) { return (entityData.get( EXPLODE_FLAGS ) & flag) != 0; } + + /** Sets the value for a specific explode flag. */ + private void setExplodeFlag( byte flag, boolean value ) { + final byte allFlags = entityData.get( EXPLODE_FLAGS ); + if( value == ((allFlags & flag) == 0) ) { + entityData.set( EXPLODE_FLAGS, (byte) (allFlags ^ flag) ); + } + } + + + //TODO--------------- ISpecialMob Implementation ---------------- + + private SpecialMobData<_SpecialCreeperEntity> specialData; + + /** @return This mob's special data. */ + @Override + public SpecialMobData<_SpecialCreeperEntity> getSpecialData() { return specialData; } + + // /** + // * Applies changes to this entity's attributes, applied on creation and after copying replacement data. + // */ + // @Override + // public final void applyAttributeAdjustments() { + // float prevMax = getMaxHealth(); + // adjustTypeAttributes(); + // setHealth( getMaxHealth() + getHealth() - prevMax ); + // } + + /** @return The experience that should be dropped by this entity. */ + @Override + public final int getExperience() { return xpReward; } + + /** Sets the experience that should be dropped by this entity. */ + @Override + public final void setExperience( int xp ) { xpReward = xp; } + + private static final ResourceLocation[] TEXTURES = { new ResourceLocation( "textures/entity/creeper/creeper.png" ) }; + + static String GET_TEXTURE_PATH( String type ) { + return SpecialMobs.TEXTURE_PATH + "creeper/" + type + ".png"; + } + + // /** @return This entity's default textures. */ + // @Override + // public ResourceLocation[] getDefaultTextures() { return TEXTURES; } + + + //TODO--------------- SpecialMobData Hooks ---------------- + + /** Called each tick to update this entity's movement. */ + @Override + public void aiStep() { + super.aiStep(); + getSpecialData().tick(); + } + + // // Called to attack the target. + // @Override + // public boolean attackEntityAsMob( Entity target ) { + // if( super.attackEntityAsMob( target ) ) { + // onTypeAttack( target ); + // return true; + // } + // return false; + // } + + // @Override + // public float getEyeHeight() { return super.getEyeHeight(); } // Uses boundingbox-scaled eye height + + /** @return Whether this entity is immune to fire damage. */ + @Override + public boolean fireImmune() { return specialData.isImmuneToFire(); } + + /** Sets this entity on fire for a specific duration. */ + @Override + public void setRemainingFireTicks( int ticks ) { + if( !getSpecialData().isImmuneToBurning() ) super.setRemainingFireTicks( ticks ); + } + + // @Override + // public boolean canBeLeashedTo( EntityPlayer player ) { return !getLeashed() && getSpecialData().allowLeashing(); } + + /** Sets this entity 'stuck' inside a block, such as a cobweb or sweet berry bush. Mod blocks could use this as a speed boost. */ + @Override + public void makeStuckInBlock( BlockState block, Vector3d speedMulti ) { + if( specialData.canBeStuckIn( block ) ) super.makeStuckInBlock( block, speedMulti ); + } + + // // Called when this mob falls. Calculates and applies fall damage. + // @Override + // public void fall( float distance, float damageMultiplier ) { super.fall( distance, damageMultiplier * getSpecialData().getFallDamageMultiplier() ); } + + // // Return whether this entity should NOT trigger a pressure plate or a tripwire. + // @Override + // public boolean doesEntityNotTriggerPressurePlate() { return getSpecialData().ignorePressurePlates(); } + + /** @return True if this entity can breathe underwater. */ + @Override + public boolean canBreatheUnderwater() { return getSpecialData().canBreatheInWater(); } + + /** @return True if this entity can be pushed by (flowing) fluids. */ + @Override + public boolean isPushedByFluid() { return !getSpecialData().ignoreWaterPush(); } + + /** @return True if this entity takes damage while wet. */ + @Override + public boolean isSensitiveToWater() { return getSpecialData().isDamagedByWater(); } + + /** @return True if the effect can be applied to this entity. */ + @Override + public boolean canBeAffected( EffectInstance effect ) { return getSpecialData().isPotionApplicable( effect ); } + + /** Saves data to this entity's base NBT compound that is specific to its subclass. */ + @Override + public void addAdditionalSaveData( CompoundNBT tag ) { + super.addAdditionalSaveData( tag ); + + final CompoundNBT saveTag = SpecialMobData.getSaveLocation( tag ); + + saveTag.putBoolean( References.TAG_DRY_EXPLODE, cannotExplodeWhileWet() ); + saveTag.putBoolean( References.TAG_WHEN_BURNING_EXPLODE, explodesWhileBurning() ); + saveTag.putBoolean( References.TAG_WHEN_SHOT_EXPLODE, explodesWhenShot() ); + + getSpecialData().writeToNBT( saveTag ); + } + + /** Loads data from this entity's base NBT compound that is specific to its subclass. */ + @Override + public void readAdditionalSaveData( CompoundNBT tag ) { + super.readAdditionalSaveData( tag ); + + final CompoundNBT saveTag = SpecialMobData.getSaveLocation( tag ); + + if( saveTag.contains( References.TAG_DRY_EXPLODE, References.NBT_TYPE_NUMERICAL ) ) + setCannotExplodeWhileWet( saveTag.getBoolean( References.TAG_DRY_EXPLODE ) ); + if( saveTag.contains( References.TAG_WHEN_BURNING_EXPLODE, References.NBT_TYPE_NUMERICAL ) ) + setExplodesWhileBurning( saveTag.getBoolean( References.TAG_WHEN_BURNING_EXPLODE ) ); + if( saveTag.contains( References.TAG_WHEN_SHOT_EXPLODE, References.NBT_TYPE_NUMERICAL ) ) + setExplodesWhenShot( saveTag.getBoolean( References.TAG_WHEN_SHOT_EXPLODE ) ); + + getSpecialData().readFromNBT( saveTag ); + } +} \ No newline at end of file diff --git a/src/main/java/fathertoast/specialmobs/common/util/AnnotationHelper.java b/src/main/java/fathertoast/specialmobs/common/util/AnnotationHelper.java new file mode 100644 index 0000000..48b5086 --- /dev/null +++ b/src/main/java/fathertoast/specialmobs/common/util/AnnotationHelper.java @@ -0,0 +1,97 @@ +package fathertoast.specialmobs.common.util; + +import fathertoast.specialmobs.common.bestiary.BestiaryInfo; +import fathertoast.specialmobs.common.bestiary.MobFamily; +import fathertoast.specialmobs.common.bestiary.SpecialMob; +import fathertoast.specialmobs.datagen.loot.LootTableBuilder; +import net.minecraft.entity.EntityType; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.ai.attributes.AttributeModifierMap; + +import javax.annotation.ParametersAreNonnullByDefault; +import java.lang.annotation.Annotation; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; + +@ParametersAreNonnullByDefault +public final class AnnotationHelper { + + //--------------- PRETTY HELPER METHODS ---------------- + + /** Creates an entity factory from a special mob species. Throws an exception if anything goes wrong. */ + public static EntityType.IFactory getEntityFactory( MobFamily.Species species ) + throws NoSuchMethodException { + final Constructor constructor = getConstructor( species.entityClass, SpecialMob.Constructor.class ); + return ( entityType, world ) -> { + try { + return constructor.newInstance( entityType, world ); + } + catch( InstantiationException | IllegalAccessException | InvocationTargetException ex ) { + throw new RuntimeException( "Class for " + constructor.getDeclaringClass().getName() + " has invalid constructor", ex ); + } + }; + } + + /** Gets bestiary info from a special mob species. Throws an exception if anything goes wrong. */ + public static BestiaryInfo getBestiaryInfo( MobFamily.Species species, EntityType.Builder entityType ) + throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + return (BestiaryInfo) getMethod( species.entityClass, SpecialMob.BestiaryInfoSupplier.class ).invoke( null, entityType ); + } + + /** Creates an attribute modifier map from a special mob species. Throws an exception if anything goes wrong. */ + public static AttributeModifierMap createAttributes( MobFamily.Species species ) + throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + return ((AttributeModifierMap.MutableAttribute) getMethod( species.entityClass, SpecialMob.AttributeCreator.class ).invoke( null )).build(); + } + + /** Builds a loot table from a special mob species. Throws an exception if anything goes wrong. */ + public static LootTableBuilder buildLootTable( MobFamily.Species species ) + throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + final LootTableBuilder builder = new LootTableBuilder(); + getMethod( species.entityClass, SpecialMob.LootTableProvider.class ).invoke( null, builder ); + return builder; + } + + + //--------------- RAW ANNOTATION METHODS ---------------- + + /** + * @return Pulls a static method with a specific annotation from a class. + * @throws NoSuchMethodException if the method does not exist. + */ + private static Method getMethod( Class type, Class annotation ) throws NoSuchMethodException { + final Method method = getMethodOptional( type, annotation ); + if( method == null ) { + throw new NoSuchMethodException( String.format( "Could not find static @%s annotated method in %s", + annotation.getSimpleName(), type.getName() ) ); + } + return method; + } + + /** + * @return Pulls a static method with a specific annotation from a class, or null if the method does not exist. + */ + private static Method getMethodOptional( Class type, Class annotation ) { + for( Method method : type.getMethods() ) { + if( Modifier.isStatic( method.getModifiers() ) && method.isAnnotationPresent( annotation ) ) + return method; + } + return null; + } + + /** + * @return Pulls a constructor with a specific annotation from a class. + * @throws NoSuchMethodException if the constructor does not exist. + */ + private static Constructor getConstructor( Class type, Class annotation ) throws NoSuchMethodException { + for( Constructor constructor : type.getConstructors() ) { + if( constructor.isAnnotationPresent( annotation ) ) + //noinspection unchecked + return (Constructor) constructor; + } + throw new NoSuchMethodException( String.format( "Could not find @%s annotated constructor in %s", + annotation.getSimpleName(), type.getName() ) ); + } +} \ No newline at end of file diff --git a/src/main/java/fathertoast/specialmobs/common/util/References.java b/src/main/java/fathertoast/specialmobs/common/util/References.java new file mode 100644 index 0000000..0ec824e --- /dev/null +++ b/src/main/java/fathertoast/specialmobs/common/util/References.java @@ -0,0 +1,63 @@ +package fathertoast.specialmobs.common.util; + +import fathertoast.specialmobs.common.core.SpecialMobs; +import net.minecraft.nbt.CompoundNBT; +import net.minecraft.nbt.ListNBT; +import net.minecraft.nbt.StringNBT; + +public final class References { + + //--------------- BESTIARY REFLECTION ---------------- + + public static final String ENTITY_PACKAGE = "fathertoast." + SpecialMobs.MOD_ID + ".common.entity."; + public static final String VANILLA_REPLACEMENT_FORMAT = "%s_Special%sEntity"; + public static final String SPECIAL_VARIANT_FORMAT = "%s%sEntity"; + + + //--------------- AI STUFF ---------------- + + public static final int AI_BIT_NONE = 0b00000000; + public static final int AI_BIT_MOVE = 0b00000001; + public static final int AI_BIT_FACE = 0b00000010; + public static final int AI_BIT_SWIM = 0b00000100; + + + //--------------- NBT STUFF ---------------- + + public static final int NBT_TYPE_NUMERICAL = 99; + public static final int NBT_TYPE_STRING = StringNBT.valueOf( "" ).getId(); // 8 + public static final int NBT_TYPE_LIST = new ListNBT().getId(); // 9 + public static final int NBT_TYPE_COMPOUND = new CompoundNBT().getId(); // 10 + + public static final String TAG_FORGE_DATA = "ForgeData"; + + // Special mob data + public static final String TAG_SPECIAL_MOB_DATA = "SpecialMobsData"; + public static final String TAG_RENDER_SCALE = "RenderScale"; + public static final String TAG_EXPERIENCE = "Experience"; + public static final String TAG_REGENERATION = "Regeneration"; + public static final String TAG_TEXTURE = "Texture"; + public static final String TAG_TEXTURE_EYES = "TextureEyes"; + public static final String TAG_TEXTURE_OVER = "TextureOverlay"; + public static final String TAG_ARROW_DAMAGE = "ArrowDamage"; + public static final String TAG_ARROW_SPREAD = "ArrowSpread"; + public static final String TAG_ARROW_WALK_SPEED = "ArrowWalkSpeed"; + public static final String TAG_ARROW_REFIRE_MIN = "ArrowRefireMin"; + public static final String TAG_ARROW_REFIRE_MAX = "ArrowRefireMax"; + public static final String TAG_ARROW_RANGE = "ArrowRange"; + public static final String TAG_FALL_MULTI = "FallMulti"; + public static final String TAG_FIRE_IMMUNE = "FireImmune"; + public static final String TAG_BURN_IMMUNE = "BurningImmune"; + public static final String TAG_LEASHABLE = "Leashable"; + public static final String TAG_TRAP_IMMUNE = "UnderPressure"; + public static final String TAG_DROWN_IMMUNE = "DrownImmune"; + public static final String TAG_WATER_PUSH_IMMUNE = "WaterPushImmune"; + public static final String TAG_WATER_DAMAGE = "WaterDamage"; + public static final String TAG_STICKY_IMMUNE = "StickyImmune"; + public static final String TAG_POTION_IMMUNE = "PotionImmune"; + + // Creepers + public static final String TAG_DRY_EXPLODE = "CannotExplodeWhileWet"; + public static final String TAG_WHEN_BURNING_EXPLODE = "ExplodesWhileBurning"; + public static final String TAG_WHEN_SHOT_EXPLODE = "ExplodesWhenShot"; +} \ No newline at end of file diff --git a/src/main/java/fathertoast/specialmobs/datagen/DataGatherListener.java b/src/main/java/fathertoast/specialmobs/datagen/DataGatherListener.java new file mode 100644 index 0000000..d1dec7b --- /dev/null +++ b/src/main/java/fathertoast/specialmobs/datagen/DataGatherListener.java @@ -0,0 +1,27 @@ +package fathertoast.specialmobs.datagen; + +import fathertoast.specialmobs.common.core.SpecialMobs; +import net.minecraft.data.DataGenerator; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.event.lifecycle.GatherDataEvent; + +import java.util.Map; + +@Mod.EventBusSubscriber( modid = SpecialMobs.MOD_ID, bus = Mod.EventBusSubscriber.Bus.MOD ) +public class DataGatherListener { + + @SubscribeEvent + public static void onGatherData( GatherDataEvent event ) { + DataGenerator generator = event.getGenerator(); + + if( event.includeClient() ) { + for( Map.Entry entry : SMLanguageProvider.LANG_CODE_MAP.entrySet() ) { + generator.addProvider( new SMLanguageProvider( generator, entry.getKey(), entry.getValue() ) ); + } + } + if( event.includeServer() ) { + generator.addProvider( new SMLootTableProvider( generator ) ); + } + } +} \ No newline at end of file diff --git a/src/main/java/fathertoast/specialmobs/datagen/SMLanguageProvider.java b/src/main/java/fathertoast/specialmobs/datagen/SMLanguageProvider.java new file mode 100644 index 0000000..9c55536 --- /dev/null +++ b/src/main/java/fathertoast/specialmobs/datagen/SMLanguageProvider.java @@ -0,0 +1,116 @@ +package fathertoast.specialmobs.datagen; + +import fathertoast.specialmobs.common.core.SpecialMobs; +import net.minecraft.data.DataGenerator; +import net.minecraftforge.common.data.LanguageProvider; + +import java.util.HashMap; + +public class SMLanguageProvider extends LanguageProvider { + + /** All supported translations. */ + public enum TranslationKey { + ENGLISH( "en" ), SPANISH( "es" ), PORTUGUESE( "pt" ), FRENCH( "fr" ), ITALIAN( "it" ), + GERMAN( "de" ), PIRATE( "en" ); + + public final String code; + + TranslationKey( String id ) { code = id; } + } + + /** This method provides helper tags to make linking translations up easier, and also enforces the correct array length. */ + private static String[] translations( String key, String en, String es, String pt, String fr, String it, String de, String pir ) { + // Note that this must match up EXACTLY to the TranslationKey enum above + String[] translation = { key, en, es, pt, fr, it, de, pir }; + + // Fix the encoding to allow us to use accented characters in the translation string literals + // Note: If a translation uses any non-ASCII characters, make sure they are all in this matrix! (case-sensitive) + final String[][] utf8ToUnicode = { + { "à", "\u00E0" }, { "á", "\u00E1" }, { "ã", "\u00E3" }, { "ä", "\u00E4" }, + { "ç", "\u00E7" }, + { "è", "\u00E8" }, { "é", "\u00E9" }, { "ê", "\u00EA" }, + { "í", "\u00ED" }, + { "ó", "\u00F3" }, { "õ", "\u00F5" }, { "ö", "\u00F6" }, + { "ù", "\u00F9" }, { "û", "\u00FB" }, { "ü", "\u00FC" }, + { "œ", "\u0153" } + }; + for( int i = 1; i < translation.length; i++ ) { + for( String[] fix : utf8ToUnicode ) + translation[i] = translation[i].replace( fix[0], fix[1] ); // Note: This is kinda dumb, but it works so idc + } + return translation; + } + + /** + * Matrix linking the actual translations to their lang key. + *

+ * Each row of the matrix is one translation array. + * In each translation array, the lang key is at index 0 and the translation for a particular + * translation key is at index (translationKey.ordinal() + 1). + * + * @see #addTranslations() + */ + @SuppressWarnings( "SpellCheckingInspection" ) + private static final String[][] TRANSLATIONS = { + // NYI + }; + + /** Maps which translation key each lang code uses, allowing multiple lang codes to use the same translations. */ + public static final HashMap LANG_CODE_MAP = new HashMap<>(); + + static { + // Assign all specific locales to the translation we want to use + mapAll( TranslationKey.ENGLISH, "us" ); // We can ignore other English locales, en_us is the fallback for all languages + mapAll( TranslationKey.SPANISH, "es", "ar", "cl", "ec", "mx", "uy", "ve" ); + mapAll( TranslationKey.PORTUGUESE, "pt", "br" ); + mapAll( TranslationKey.FRENCH, "fr", "ca" ); + mapAll( TranslationKey.ITALIAN, "it" ); + mapAll( TranslationKey.GERMAN, "de", "at", "ch" ); + mapAll( TranslationKey.PIRATE, "pt" ); + + // Make sure all supported languages are completely implemented + SpecialMobs.LOG.info( "Starting translation key verification..." ); + for( TranslationKey key : TranslationKey.values() ) { + if( !LANG_CODE_MAP.containsValue( key ) ) { + SpecialMobs.LOG.error( "Translation key {} has no lang codes assigned!", key.name() ); + } + final int k = key.ordinal() + 1; + for( String[] translationArray : TRANSLATIONS ) { + if( translationArray[k] == null || translationArray[k].equals( "" ) ) { + SpecialMobs.LOG.error( "Translation key {} is missing a translation for lang key \"{}\"!", + key.name(), translationArray[0] ); + } + } + } + SpecialMobs.LOG.info( "Translation key verification complete!" ); + } + + /** Maps any number of locale codes to a single translation. */ + private static void mapAll( TranslationKey translation, String... locales ) { + for( String locale : locales ) { + LANG_CODE_MAP.put( translation.code + "_" + locale, translation ); + } + } + + /** The translation key to use for this locale. */ + private final TranslationKey translationKey; + + /** Creates a language provider for a specific locale. This correlates to exactly one .json file. */ + public SMLanguageProvider( DataGenerator gen, String locale, TranslationKey translateKey ) { + super( gen, SpecialMobs.MOD_ID, locale ); + translationKey = translateKey; + } + + /** + * Build the .json file for this locale (based solely on its translation key). + * + * @see SMLanguageProvider#TRANSLATIONS + */ + @Override + protected void addTranslations() { + final int k = translationKey.ordinal() + 1; + for( String[] translationArray : TRANSLATIONS ) { + add( translationArray[0], translationArray[k] ); + } + } +} \ No newline at end of file diff --git a/src/main/java/fathertoast/specialmobs/datagen/SMLootTableProvider.java b/src/main/java/fathertoast/specialmobs/datagen/SMLootTableProvider.java new file mode 100644 index 0000000..6be5df7 --- /dev/null +++ b/src/main/java/fathertoast/specialmobs/datagen/SMLootTableProvider.java @@ -0,0 +1,75 @@ +package fathertoast.specialmobs.datagen; + +import com.google.common.collect.ImmutableList; +import com.mojang.datafixers.util.Pair; +import fathertoast.specialmobs.common.bestiary.MobFamily; +import fathertoast.specialmobs.common.core.register.SMEntities; +import fathertoast.specialmobs.common.util.AnnotationHelper; +import mcp.MethodsReturnNonnullByDefault; +import net.minecraft.advancements.criterion.EntityPredicate; +import net.minecraft.data.DataGenerator; +import net.minecraft.data.LootTableProvider; +import net.minecraft.data.loot.EntityLootTables; +import net.minecraft.entity.EntityType; +import net.minecraft.loot.*; +import net.minecraft.util.ResourceLocation; +import net.minecraftforge.fml.RegistryObject; + +import javax.annotation.ParametersAreNonnullByDefault; +import java.lang.reflect.InvocationTargetException; +import java.util.List; +import java.util.Map; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Supplier; + +@ParametersAreNonnullByDefault +@MethodsReturnNonnullByDefault +public class SMLootTableProvider extends LootTableProvider { + + public SMLootTableProvider( DataGenerator gen ) { super( gen ); } + + /** Provides all loot table sub-providers for this mod, paired with their parameter sets (context for use). */ + @Override + protected List>>, LootParameterSet>> getTables() { + return ImmutableList.of( + Pair.of( EntitySubProvider::new, LootParameterSets.ENTITY ) + ); + } + + /** Validates this mod's loot tables. */ + @Override + protected void validate( Map tables, ValidationTracker ctx ) { + tables.forEach( ( name, table ) -> LootTableManager.validate( ctx, name, table ) ); + } + + /** Provides all entity loot tables for this mod. */ + public static class EntitySubProvider extends EntityLootTables { + // Pull this protected field out into the Court of Public Opinion. + public static final EntityPredicate.Builder ENTITY_ON_FIRE = EntityLootTables.ENTITY_ON_FIRE; + + /** Builds all loot tables for this provider. */ + @Override + protected void addTables() { + // Bestiary-generated tables + for( MobFamily.Species variant : MobFamily.getAllSpecies() ) addTable( variant ); + } + + /** Builds the loot table for a specific entity species. */ + private void addTable( MobFamily.Species species ) { + try { + add( species.entityType.get(), AnnotationHelper.buildLootTable( species ).toLootTable() ); + } + catch( NoSuchMethodException | InvocationTargetException | IllegalAccessException ex ) { + throw new RuntimeException( "Entity class for " + species.name + " has invalid loot table builder method", ex ); + } + } + + /** Supplies the entity types this loot table provider will be used for. */ + @Override + protected Iterable> getKnownEntities() { + // This is basically pulled straight from the forge docs on data gen for block/entity loot tables + return SMEntities.REGISTRY.getEntries().stream().flatMap( RegistryObject::stream )::iterator; + } + } +} \ No newline at end of file diff --git a/src/main/java/fathertoast/specialmobs/datagen/loot/LootEntryItemBuilder.java b/src/main/java/fathertoast/specialmobs/datagen/loot/LootEntryItemBuilder.java new file mode 100644 index 0000000..7109978 --- /dev/null +++ b/src/main/java/fathertoast/specialmobs/datagen/loot/LootEntryItemBuilder.java @@ -0,0 +1,154 @@ +package fathertoast.specialmobs.datagen.loot; + +import fathertoast.specialmobs.datagen.SMLootTableProvider; +import net.minecraft.enchantment.Enchantment; +import net.minecraft.item.ItemStack; +import net.minecraft.loot.ItemLootEntry; +import net.minecraft.loot.LootContext; +import net.minecraft.loot.LootEntry; +import net.minecraft.loot.RandomValueRange; +import net.minecraft.loot.conditions.EntityHasProperty; +import net.minecraft.loot.conditions.ILootCondition; +import net.minecraft.loot.functions.*; +import net.minecraft.nbt.CompoundNBT; +import net.minecraft.util.IItemProvider; + +import java.util.ArrayList; +import java.util.List; + +@SuppressWarnings( { "unused", "WeakerAccess", "UnusedReturnValue" } ) +public +class LootEntryItemBuilder { + + private final IItemProvider item; + + private int weight = 1; + private int quality = 0; + + private final List itemFunctions = new ArrayList<>(); + private final List entryConditions = new ArrayList<>(); + + public LootEntryItemBuilder( IItemProvider baseItem ) { + item = baseItem; + } + + public LootEntryItemBuilder( ItemStack itemStack ) { + this( itemStack.getItem() ); + if( itemStack.getTag() != null ) { + setNBTTag( itemStack.getTag().copy() ); + } + // if( !itemStack.isDamageableItem() && itemStack.getMetadata() != 0 ) { + // setMetadata( itemStack.getMetadata() ); + // } + } + + /** @return A new loot entry object reflecting the current state of this builder. */ + public LootEntry.Builder toLootEntry() { + return LootHelper.build( ItemLootEntry.lootTableItem( item ), entryConditions, itemFunctions ) + .setWeight( weight ).setQuality( quality ); + } + + /** @param value A new weight for the loot entry. */ + public LootEntryItemBuilder setWeight( int value ) { + weight = value; + return this; + } + + /** @param value A new quality for the loot entry. Quality alters the weight of this entry based on luck level. */ + public LootEntryItemBuilder setQuality( int value ) { + quality = value; + return this; + } + + /** @param condition A condition to add to this builder. */ + public LootEntryItemBuilder addCondition( ILootCondition.IBuilder condition ) { + entryConditions.add( condition ); + return this; + } + + /** Adds a stack size function. */ + public LootEntryItemBuilder setCount( int value ) { + return addFunction( SetCount.setCount( new RandomValueRange( value ) ) ); + } + + /** Adds a stack size function. */ + public LootEntryItemBuilder setCount( int min, int max ) { + return addFunction( SetCount.setCount( new RandomValueRange( min, max ) ) ); + } + + /** Adds a looting enchant (luck) bonus function. Gross. */ + public LootEntryItemBuilder addLootingBonus( float value ) { + return addFunction( LootingEnchantBonus.lootingMultiplier( new RandomValueRange( value ) ) ); + } + + /** Adds a looting enchant (luck) bonus function. Gross. */ + public LootEntryItemBuilder addLootingBonus( float min, float max ) { + return addFunction( LootingEnchantBonus.lootingMultiplier( new RandomValueRange( min, max ) ) ); + } + + /** Adds a looting enchant (luck) bonus function. Gross. */ + public LootEntryItemBuilder addLootingBonus( float min, float max, int limit ) { + return addFunction( LootingEnchantBonus.lootingMultiplier( new RandomValueRange( min, max ) ).setLimit( limit ) ); + } + + /** Adds a set damage function. */ + public LootEntryItemBuilder setDamage( int value ) { + return addFunction( SetDamage.setDamage( new RandomValueRange( value ) ) ); + } + + /** Adds a set damage function. */ + public LootEntryItemBuilder setDamage( int min, int max ) { + return addFunction( SetDamage.setDamage( new RandomValueRange( min, max ) ) ); + } + + // /** Adds a set metadata function. */ + // public LootEntryItemBuilder setMetadata( int min, int max ) { + // return addFunction( new SetMetadata( NO_CONDITIONS, new RandomValueRange( min, max ) ) ); + // } + // + // /** Adds a set metadata function. */ + // public LootEntryItemBuilder setMetadata( int value ) { + // return addFunction( new SetMetadata( NO_CONDITIONS, new RandomValueRange( value ) ) ); + // } + + /** Adds an nbt tag compound function. */ + public LootEntryItemBuilder setNBTTag( CompoundNBT tag ) { return addFunction( SetNBT.setTag( tag ) ); } + + /** Adds a smelt function with the EntityOnFire condition. */ + public LootEntryItemBuilder smeltIfBurning() { + return addFunction( Smelt.smelted().when( EntityHasProperty.hasProperties( LootContext.EntityTarget.THIS, + SMLootTableProvider.EntitySubProvider.ENTITY_ON_FIRE ) ) ); + } + + /** Adds a random enchantment function. */ + public LootEntryItemBuilder applyOneRandomApplicableEnchant() { + return addFunction( EnchantRandomly.randomApplicableEnchantment() ); + } + + /** Adds a random enchantment function. */ + public LootEntryItemBuilder applyOneRandomEnchant( Enchantment... enchantments ) { + final EnchantRandomly.Builder builder = new EnchantRandomly.Builder(); + for( Enchantment enchant : enchantments ) builder.withEnchantment( enchant ); + return addFunction( builder ); + } + + /** Adds an enchanting function. */ + public LootEntryItemBuilder enchant( int level, boolean treasure ) { + final EnchantWithLevels.Builder builder = EnchantWithLevels.enchantWithLevels( new RandomValueRange( level ) ); + if( treasure ) builder.allowTreasure(); + return addFunction( builder ); + } + + /** Adds an enchanting function. */ + public LootEntryItemBuilder enchant( int levelMin, int levelMax, boolean treasure ) { + final EnchantWithLevels.Builder builder = EnchantWithLevels.enchantWithLevels( new RandomValueRange( levelMin, levelMax ) ); + if( treasure ) builder.allowTreasure(); + return addFunction( builder ); + } + + /** Adds an item function to this builder. */ + public LootEntryItemBuilder addFunction( ILootFunction.IBuilder function ) { + itemFunctions.add( function ); + return this; + } +} \ No newline at end of file diff --git a/src/main/java/fathertoast/specialmobs/datagen/loot/LootHelper.java b/src/main/java/fathertoast/specialmobs/datagen/loot/LootHelper.java new file mode 100644 index 0000000..d8ed2ea --- /dev/null +++ b/src/main/java/fathertoast/specialmobs/datagen/loot/LootHelper.java @@ -0,0 +1,65 @@ +package fathertoast.specialmobs.datagen.loot; + +import net.minecraft.loot.*; +import net.minecraft.loot.conditions.ILootCondition; +import net.minecraft.loot.conditions.KilledByPlayer; +import net.minecraft.loot.conditions.RandomChanceWithLooting; +import net.minecraft.loot.functions.ILootFunction; + +import javax.annotation.ParametersAreNonnullByDefault; +import java.util.List; + +@ParametersAreNonnullByDefault +public class LootHelper { + + public static final ILootCondition.IBuilder KILLED_BY_PLAYER_CONDITION = KilledByPlayer.killedByPlayer(); + + public static final ILootCondition.IBuilder[] UNCOMMON_CONDITIONS = { + RandomChanceWithLooting.randomChanceAndLootingBoost( 0.25F, 0.05F ), + KILLED_BY_PLAYER_CONDITION + }; + public static final ILootCondition.IBuilder[] RARE_CONDITIONS = { + RandomChanceWithLooting.randomChanceAndLootingBoost( 0.025F, 0.0F ), + KILLED_BY_PLAYER_CONDITION + }; + + public static final RandomValueRange ONE_ROLL = new RandomValueRange( 1 ); + public static final RandomValueRange NO_ROLL = new RandomValueRange( 0 ); + + /** Convenience method to put all loot entries, conditions, and functions into a loot pool. Returns the loot pool. */ + public static LootPool.Builder build( LootPool.Builder builder, List> entries, + List conditions, List functions ) { + return build( build( builder, entries ), conditions, functions ); + } + + /** Convenience method to put all loot conditions and functions into a loot builder. Returns the loot builder. */ + public static > // Can't figure out how to require both, but function consumer is more stringent + T build( T builder, List conditions, List functions ) { + build( (ILootConditionConsumer) builder, conditions ); + return build( builder, functions ); + } + + /** Convenience method to put all loot pools into a loot table. Returns the loot table. */ + public static LootTable.Builder build( LootTable.Builder builder, List pools ) { + for( LootPool.Builder pool : pools ) builder.withPool( pool ); + return builder; + } + + /** Convenience method to put all loot entries into a loot pool. Returns the loot pool. */ + public static LootPool.Builder build( LootPool.Builder builder, List> entries ) { + for( LootEntry.Builder entry : entries ) builder.add( entry ); + return builder; + } + + /** Convenience method to put all loot conditions into a loot builder. Returns the loot builder. */ + public static > T build( T builder, List conditions ) { + for( ILootCondition.IBuilder condition : conditions ) builder.when( condition ); + return builder; + } + + /** Convenience method to put all loot functions into a loot builder. Returns the loot builder. */ + public static > T build( T builder, List functions ) { + for( ILootFunction.IBuilder function : functions ) builder.apply( function ); + return builder; + } +} \ No newline at end of file diff --git a/src/main/java/fathertoast/specialmobs/datagen/loot/LootPoolBuilder.java b/src/main/java/fathertoast/specialmobs/datagen/loot/LootPoolBuilder.java new file mode 100644 index 0000000..0b30609 --- /dev/null +++ b/src/main/java/fathertoast/specialmobs/datagen/loot/LootPoolBuilder.java @@ -0,0 +1,100 @@ +package fathertoast.specialmobs.datagen.loot; + +import net.minecraft.loot.*; +import net.minecraft.loot.conditions.ILootCondition; +import net.minecraft.loot.functions.ILootFunction; +import net.minecraft.util.ResourceLocation; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +@SuppressWarnings( { "unused", "WeakerAccess", "UnusedReturnValue" } ) +public +class LootPoolBuilder { + + private final String name; + + private RandomValueRange rolls = LootHelper.ONE_ROLL; + private RandomValueRange bonusRolls = LootHelper.NO_ROLL; + + private final List> lootEntries = new ArrayList<>(); + private final List poolConditions = new ArrayList<>(); + private final List poolFunctions = new ArrayList<>(); + + public LootPoolBuilder( String id ) { name = id; } + + /** @return A new loot pool object reflecting the current state of this builder. */ + public LootPool.Builder toLootPool() { + return LootHelper.build( LootPool.lootPool(), lootEntries, poolConditions, poolFunctions ) + .setRolls( rolls ).bonusRolls( bonusRolls.getMin(), bonusRolls.getMax() ).name( name ); + } + + /** @param value The number of rolls for this pool. */ + public LootPoolBuilder setRolls( float value ) { + rolls = new RandomValueRange( value ); + return this; + } + + /** + * @param min Minimum rolls for this pool (inclusive). + * @param max Maximum rolls for this pool (inclusive). + */ + public LootPoolBuilder setRolls( float min, float max ) { + rolls = new RandomValueRange( min, max ); + return this; + } + + /** @param value The additional rolls for each level of looting. */ + public LootPoolBuilder setBonusRolls( float value ) { + bonusRolls = new RandomValueRange( value ); + return this; + } + + /** + * @param min Minimum additional rolls for this pool for each level of looting (inclusive). + * @param max Maximum additional rolls for this pool for each level of looting (inclusive). + */ + public LootPoolBuilder setBonusRolls( float min, float max ) { + bonusRolls = new RandomValueRange( min, max ); + return this; + } + + /** @param entry A loot entry to add to this builder. */ + public LootPoolBuilder addEntry( LootEntry.Builder entry ) { + lootEntries.add( entry ); + return this; + } + + /** @param conditions Any number of conditions to add to this builder. */ + public LootPoolBuilder addConditions( ILootCondition.IBuilder... conditions ) { + poolConditions.addAll( Arrays.asList( conditions ) ); + return this; + } + + /** @param functions Any number of functions to add to this builder. */ + public LootPoolBuilder addFunctions( ILootFunction.IBuilder... functions ) { + poolFunctions.addAll( Arrays.asList( functions ) ); + return this; + } + + /** Adds a loot table as a drop. */ + public LootPoolBuilder addEntryTable( ResourceLocation lootTable ) { + return addEntryTable( lootTable, 1, 0 ); + } + + /** Adds a loot table as a drop. */ + public LootPoolBuilder addEntryTable( ResourceLocation lootTable, int weight, int quality, ILootCondition.IBuilder... conditions ) { + final StandaloneLootEntry.Builder builder = TableLootEntry.lootTableReference( lootTable ); + // Note: we can also extend this to allow functions, if needed + for( ILootCondition.IBuilder condition : conditions ) builder.when( condition ); + return addEntry( builder.setWeight( weight ).setQuality( quality ) ); + } + + /** Adds an empty entry. */ + public LootPoolBuilder addEntryEmpty( int weight, int quality, ILootCondition.IBuilder... conditions ) { + StandaloneLootEntry.Builder builder = EmptyLootEntry.emptyItem(); + for( ILootCondition.IBuilder condition : conditions ) builder.when( condition ); + return addEntry( builder.setWeight( weight ).setQuality( quality ) ); + } +} \ No newline at end of file diff --git a/src/main/java/fathertoast/specialmobs/datagen/loot/LootTableBuilder.java b/src/main/java/fathertoast/specialmobs/datagen/loot/LootTableBuilder.java new file mode 100644 index 0000000..701e425 --- /dev/null +++ b/src/main/java/fathertoast/specialmobs/datagen/loot/LootTableBuilder.java @@ -0,0 +1,142 @@ +package fathertoast.specialmobs.datagen.loot; + +import net.minecraft.item.ItemStack; +import net.minecraft.loot.LootPool; +import net.minecraft.loot.LootTable; +import net.minecraft.util.IItemProvider; +import net.minecraft.util.ResourceLocation; + +import java.util.ArrayList; +import java.util.List; + +/** + * Wrapper for the vanilla loot table builder, largely a holdover from before the vanilla loot table builders existed, + * but still offers many convenience methods for simple and standardized loot table construction. + */ +@SuppressWarnings( { "unused", "WeakerAccess", "UnusedReturnValue" } ) +public class LootTableBuilder { + + private final List pools = new ArrayList<>(); + //private final List tableFunctions = new ArrayList<>(); - Can be implemented if needed + + /** @return A new loot table object reflecting the current state of this builder. */ + public LootTable.Builder toLootTable() { return LootHelper.build( LootTable.lootTable(), pools ); } + + /** @param pool A loot pool to add to this builder. */ + public LootTableBuilder addPool( LootPool.Builder pool ) { + pools.add( pool ); + return this; + } + + /** Adds a pool referencing a loot table. */ + public LootTableBuilder addLootTable( String id, ResourceLocation lootTable ) { + return addPool( new LootPoolBuilder( id ).addEntryTable( lootTable ).toLootPool() ); + } + + /** Adds a pool with a single item drop. */ + public LootTableBuilder addGuaranteedDrop( String id, IItemProvider item, int count ) { + return addPool( new LootPoolBuilder( id ) + .addEntry( new LootEntryItemBuilder( item ).setCount( count ).toLootEntry() ) + .toLootPool() ); + } + + /** Adds a pool with an item drop of 0-2 + (0-1 * luck). */ + public LootTableBuilder addCommonDrop( String id, IItemProvider item ) { return addCommonDrop( id, item, 2 ); } + + /** Adds a pool with an item drop of 0-2 + (0-1 * luck). */ + public LootTableBuilder addCommonDrop( String id, ItemStack item ) { return addCommonDrop( id, item, 2 ); } + + /** Adds a pool with an item drop of 0-max + (0-1 * luck). */ + public LootTableBuilder addCommonDrop( String id, IItemProvider item, int max ) { + return addPool( new LootPoolBuilder( id ) + .addEntry( new LootEntryItemBuilder( item ).setCount( 0, max ).addLootingBonus( 0, 1 ).toLootEntry() ) + .toLootPool() ); + } + + /** Adds a pool with an item drop of 0-max + (0-1 * luck). */ + public LootTableBuilder addCommonDrop( String id, ItemStack item, int max ) { + return addPool( new LootPoolBuilder( id ) + .addEntry( new LootEntryItemBuilder( item ).setCount( 0, max ).addLootingBonus( 0, 1 ).toLootEntry() ) + .toLootPool() ); + } + + /** Adds a pool with an item drop of (-1)-1 + (0-1 * luck). */ + public LootTableBuilder addSemicommonDrop( String id, IItemProvider item ) { + return addPool( new LootPoolBuilder( id ) + .addConditions( LootHelper.KILLED_BY_PLAYER_CONDITION ) + .addEntry( new LootEntryItemBuilder( item ).setCount( -1, 1 ).addLootingBonus( 0, 1 ).toLootEntry() ) + .toLootPool() ); + } + + /** Adds a pool with an item drop of (-1)-1 + (0-1 * luck). */ + public LootTableBuilder addSemicommonDrop( String id, ItemStack item ) { + return addPool( new LootPoolBuilder( id ) + .addConditions( LootHelper.KILLED_BY_PLAYER_CONDITION ) + .addEntry( new LootEntryItemBuilder( item ).setCount( -1, 1 ).addLootingBonus( 0, 1 ).toLootEntry() ) + .toLootPool() ); + } + + /** Adds a pool with an item drop of 1-8 and chance of 25% + (5% * luck). */ + public LootTableBuilder addClusterDrop( String id, IItemProvider item ) { return addClusterDrop( id, item, 8 ); } + + /** Adds a pool with an item drop of 1-8 and chance of 25% + (5% * luck). */ + public LootTableBuilder addClusterDrop( String id, ItemStack item ) { return addClusterDrop( id, item, 8 ); } + + /** Adds a pool with an item drop of 1-max and chance of 25% + (5% * luck). */ + public LootTableBuilder addClusterDrop( String id, IItemProvider item, int max ) { + return addPool( new LootPoolBuilder( id ) + .addConditions( LootHelper.UNCOMMON_CONDITIONS ) + .addEntry( new LootEntryItemBuilder( item ).setCount( 1, max ).toLootEntry() ) + .toLootPool() ); + } + + /** Adds a pool with an item drop of 1-max and chance of 25% + (5% * luck). */ + public LootTableBuilder addClusterDrop( String id, ItemStack item, int max ) { + return addPool( new LootPoolBuilder( id ) + .addConditions( LootHelper.UNCOMMON_CONDITIONS ) + .addEntry( new LootEntryItemBuilder( item ).setCount( 1, max ).toLootEntry() ) + .toLootPool() ); + } + + /** Adds a pool with a single item drop (from a list) and chance of 25% + (5% * luck). */ + public LootTableBuilder addUncommonDrop( String id, IItemProvider... items ) { + LootPoolBuilder pool = new LootPoolBuilder( id ).addConditions( LootHelper.UNCOMMON_CONDITIONS ); + for( IItemProvider item : items ) { + pool.addEntry( new LootEntryItemBuilder( item ).toLootEntry() ); + } + return addPool( pool.toLootPool() ); + } + + /** + * Adds a pool with a single item drop (from a list) and chance of 25% + (5% * luck). + * Item stack size is used as the weight of each item. + */ + public LootTableBuilder addUncommonDrop( String id, ItemStack... items ) { + LootPoolBuilder pool = new LootPoolBuilder( id ).addConditions( LootHelper.UNCOMMON_CONDITIONS ); + for( ItemStack item : items ) { + pool.addEntry( new LootEntryItemBuilder( item ).setWeight( item.getCount() ).toLootEntry() ); + } + return addPool( pool.toLootPool() ); + } + + /** Adds a pool with a single item drop (from a list) and chance of 2.5% + (0% * luck). */ + public LootTableBuilder addRareDrop( String id, IItemProvider... items ) { + LootPoolBuilder pool = new LootPoolBuilder( id ).addConditions( LootHelper.RARE_CONDITIONS ); + for( IItemProvider item : items ) { + pool.addEntry( new LootEntryItemBuilder( item ).toLootEntry() ); + } + return addPool( pool.toLootPool() ); + } + + /** + * Adds a pool with a single item drop (from a list) and chance of 2.5% + (0% * luck). + * Item stack size is used as the weight of each item. + */ + public LootTableBuilder addRareDrop( String id, ItemStack... items ) { + LootPoolBuilder pool = new LootPoolBuilder( id ).addConditions( LootHelper.RARE_CONDITIONS ); + for( ItemStack item : items ) { + pool.addEntry( new LootEntryItemBuilder( item ).setWeight( item.getCount() ).toLootEntry() ); + } + return addPool( pool.toLootPool() ); + } +} \ No newline at end of file diff --git a/src/main/resources/META-INF/accesstransformer.cfg b/src/main/resources/META-INF/accesstransformer.cfg index 71ebfae..250a107 100644 --- a/src/main/resources/META-INF/accesstransformer.cfg +++ b/src/main/resources/META-INF/accesstransformer.cfg @@ -1,6 +1,12 @@ # Note: Things that can be safely transformed are things that cannot be overridden - final methods & classes, # anything inside a final class, and private and/or static fields & methods. +# Entity registration +public net.minecraft.entity.EntityType field_233593_bg_ # immuneTo + +# Creepers +protected net.minecraft.entity.monster.CreeperEntity field_184714_b # DATA_IS_POWERED +protected net.minecraft.entity.monster.CreeperEntity field_184715_c # DATA_IS_IGNITED protected net.minecraft.entity.monster.CreeperEntity field_82226_g # explosionRadius -protected net.minecraft.entity.monster.CreeperEntity func_146077_cc()V # explodeCreeper -protected net.minecraft.entity.monster.CreeperEntity func_190741_do()V # spawnLingeringCloud \ No newline at end of file +protected net.minecraft.entity.monster.CreeperEntity func_146077_cc()V # explodeCreeper() +protected net.minecraft.entity.monster.CreeperEntity func_190741_do()V # spawnLingeringCloud() \ No newline at end of file