b374f71533
- Splits all particle textures into individual files intead of sprite sheets - Replaces arc and lightning pulse entities with particles - Pulls particle code up to the general ParticleWizardry so behaviours such as spin can be applied to all particle types - Renames a few ParticleBuilder methods for conciseness. - Adds a few new particle effects to various spells and entities, with a slight tweak to EntityMagicArrow#onBlockHit to facilitate impact effects
399 lines
14 KiB
Java
399 lines
14 KiB
Java
package electroblob.wizardry.entity.living;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.Arrays;
|
|
import java.util.List;
|
|
|
|
import com.google.common.base.Predicate;
|
|
|
|
import electroblob.wizardry.Wizardry;
|
|
import electroblob.wizardry.constants.Element;
|
|
import electroblob.wizardry.constants.Tier;
|
|
import electroblob.wizardry.item.ItemSpellBook;
|
|
import electroblob.wizardry.registry.Spells;
|
|
import electroblob.wizardry.registry.WizardryItems;
|
|
import electroblob.wizardry.registry.WizardryPotions;
|
|
import electroblob.wizardry.registry.WizardrySounds;
|
|
import electroblob.wizardry.spell.Spell;
|
|
import electroblob.wizardry.util.ParticleBuilder;
|
|
import electroblob.wizardry.util.SpellModifiers;
|
|
import electroblob.wizardry.util.ParticleBuilder.Type;
|
|
import electroblob.wizardry.util.WizardryUtilities;
|
|
import io.netty.buffer.ByteBuf;
|
|
import net.minecraft.entity.Entity;
|
|
import net.minecraft.entity.EntityList;
|
|
import net.minecraft.entity.EntityLivingBase;
|
|
import net.minecraft.entity.IEntityLivingData;
|
|
import net.minecraft.entity.SharedMonsterAttributes;
|
|
import net.minecraft.entity.ai.EntityAIHurtByTarget;
|
|
import net.minecraft.entity.ai.EntityAIMoveTowardsRestriction;
|
|
import net.minecraft.entity.ai.EntityAINearestAttackableTarget;
|
|
import net.minecraft.entity.ai.EntityAIOpenDoor;
|
|
import net.minecraft.entity.ai.EntityAIRestrictOpenDoor;
|
|
import net.minecraft.entity.ai.EntityAISwimming;
|
|
import net.minecraft.entity.ai.EntityAIWander;
|
|
import net.minecraft.entity.ai.EntityAIWatchClosest2;
|
|
import net.minecraft.entity.monster.EntityMob;
|
|
import net.minecraft.entity.player.EntityPlayer;
|
|
import net.minecraft.init.SoundEvents;
|
|
import net.minecraft.inventory.EntityEquipmentSlot;
|
|
import net.minecraft.item.ItemStack;
|
|
import net.minecraft.nbt.NBTTagCompound;
|
|
import net.minecraft.nbt.NBTTagInt;
|
|
import net.minecraft.network.datasync.DataParameter;
|
|
import net.minecraft.network.datasync.DataSerializers;
|
|
import net.minecraft.network.datasync.EntityDataManager;
|
|
import net.minecraft.pathfinding.PathNavigateGround;
|
|
import net.minecraft.util.DamageSource;
|
|
import net.minecraft.util.EnumHand;
|
|
import net.minecraft.util.ResourceLocation;
|
|
import net.minecraft.util.SoundEvent;
|
|
import net.minecraft.util.text.TextComponentTranslation;
|
|
import net.minecraft.world.DifficultyInstance;
|
|
import net.minecraft.world.EnumDifficulty;
|
|
import net.minecraft.world.World;
|
|
import net.minecraftforge.common.util.Constants.NBT;
|
|
import net.minecraftforge.fml.common.registry.IEntityAdditionalSpawnData;
|
|
|
|
public class EntityEvilWizard extends EntityMob implements ISpellCaster, IEntityAdditionalSpawnData {
|
|
|
|
private EntityAIAttackSpell<EntityEvilWizard> spellCastingAI = new EntityAIAttackSpell<EntityEvilWizard>(this, 0.5D, 14.0F, 30, 50);
|
|
|
|
public int textureIndex = 0;
|
|
|
|
public boolean hasTower = false;
|
|
|
|
/** The entity selector passed into the new AI methods. */
|
|
protected Predicate<Entity> targetSelector;
|
|
|
|
/** Data parameter for the cooldown time for wizards healing themselves. */
|
|
private static final DataParameter<Integer> HEAL_COOLDOWN = EntityDataManager.createKey(EntityEvilWizard.class,
|
|
DataSerializers.VARINT);
|
|
/** Data parameter for the wizard's element. */
|
|
private static final DataParameter<Integer> ELEMENT = EntityDataManager.createKey(EntityEvilWizard.class,
|
|
DataSerializers.VARINT);
|
|
/** The resource location for the evil wizard's loot table. */
|
|
private static final ResourceLocation LOOT_TABLE = new ResourceLocation(Wizardry.MODID, "entities/evil_wizard");
|
|
|
|
// Field implementations
|
|
private List<Spell> spells = new ArrayList<Spell>(4);
|
|
private Spell continuousSpell;
|
|
|
|
public EntityEvilWizard(World world){
|
|
|
|
super(world);
|
|
this.setSize(0.6F, 1.8F);
|
|
((PathNavigateGround)this.getNavigator()).setBreakDoors(true);
|
|
|
|
// For some reason this can't be done in initEntityAI
|
|
this.tasks.addTask(3, this.spellCastingAI);
|
|
|
|
this.detachHome();
|
|
}
|
|
|
|
@Override
|
|
protected void entityInit(){
|
|
super.entityInit();
|
|
this.dataManager.register(HEAL_COOLDOWN, -1);
|
|
this.dataManager.register(ELEMENT, 0);
|
|
}
|
|
|
|
@Override
|
|
protected void initEntityAI(){
|
|
this.tasks.addTask(0, new EntityAISwimming(this));
|
|
this.tasks.addTask(4, new EntityAIRestrictOpenDoor(this));
|
|
this.tasks.addTask(5, new EntityAIOpenDoor(this, true));
|
|
this.tasks.addTask(6, new EntityAIMoveTowardsRestriction(this, 0.6D));
|
|
this.tasks.addTask(7, new EntityAIWatchClosest2(this, EntityPlayer.class, 3.0F, 1.0F));
|
|
this.tasks.addTask(7, new EntityAIWander(this, 0.6D));
|
|
|
|
this.targetSelector = new Predicate<Entity>(){
|
|
|
|
public boolean apply(Entity entity){
|
|
|
|
// If the target is valid and not invisible...
|
|
if(entity != null && !entity.isInvisible()
|
|
&& WizardryUtilities.isValidTarget(EntityEvilWizard.this, entity)){
|
|
|
|
// ... and is a player, a summoned creature, another (non-evil) wizard ...
|
|
if(entity instanceof EntityPlayer
|
|
|| (entity instanceof ISummonedCreature || entity instanceof EntityWizard
|
|
// ... or in the whitelist ...
|
|
|| Arrays.asList(Wizardry.settings.summonedCreatureTargetsWhitelist)
|
|
.contains(EntityList.getKey(entity.getClass())))
|
|
// ... and isn't in the blacklist ...
|
|
&& !Arrays.asList(Wizardry.settings.summonedCreatureTargetsBlacklist)
|
|
.contains(EntityList.getKey(entity.getClass()))){
|
|
// ... it can be attacked.
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
};
|
|
|
|
this.targetTasks.addTask(1, new EntityAIHurtByTarget(this, true));
|
|
this.targetTasks.addTask(0, new EntityAINearestAttackableTarget<EntityLivingBase>(this, EntityLivingBase.class,
|
|
0, false, true, this.targetSelector));
|
|
}
|
|
|
|
@Override
|
|
protected void applyEntityAttributes(){
|
|
super.applyEntityAttributes();
|
|
this.getEntityAttribute(SharedMonsterAttributes.MOVEMENT_SPEED).setBaseValue(0.5D);
|
|
this.getEntityAttribute(SharedMonsterAttributes.MAX_HEALTH).setBaseValue(30);
|
|
}
|
|
|
|
private int getHealCooldown(){
|
|
return this.dataManager.get(HEAL_COOLDOWN);
|
|
}
|
|
|
|
private void setHealCooldown(int cooldown){
|
|
this.dataManager.set(HEAL_COOLDOWN, cooldown);
|
|
}
|
|
|
|
public Element getElement(){
|
|
return Element.values()[this.dataManager.get(ELEMENT)];
|
|
}
|
|
|
|
public void setElement(Element element){
|
|
this.dataManager.set(ELEMENT, element.ordinal());
|
|
}
|
|
|
|
@Override
|
|
public List<Spell> getSpells(){
|
|
return this.spells;
|
|
}
|
|
|
|
@Override
|
|
public SpellModifiers getModifiers(){
|
|
return new SpellModifiers();
|
|
}
|
|
|
|
@Override
|
|
public void setContinuousSpell(Spell spell){
|
|
this.continuousSpell = spell;
|
|
}
|
|
|
|
@Override
|
|
public Spell getContinuousSpell(){
|
|
return this.continuousSpell;
|
|
}
|
|
|
|
@Override
|
|
public int getAimingError(EnumDifficulty difficulty){
|
|
// Being more intelligent than skeletons, wizards are a little more accurate.
|
|
switch(difficulty){
|
|
case EASY: return 7;
|
|
case NORMAL: return 4;
|
|
case HARD: return 1;
|
|
default: return 7; // Peaceful counts as easy
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onLivingUpdate(){
|
|
|
|
super.onLivingUpdate();
|
|
|
|
int healCooldown = this.getHealCooldown();
|
|
|
|
// This is now done slightly differently because isPotionActive doesn't work on client here, meaning that when
|
|
// affected with arcane jammer and healCooldown == 0, whilst the wizard didn't actually heal or play the sound,
|
|
// the particles still spawned, and since healCooldown wasn't reset they spawned every tick until the arcane
|
|
// jammer wore off.
|
|
if(healCooldown == 0 && this.getHealth() < this.getMaxHealth() && this.getHealth() > 0
|
|
&& !this.isPotionActive(WizardryPotions.arcane_jammer)){
|
|
|
|
// Healer wizards use greater heal.
|
|
this.heal(this.getElement() == Element.HEALING ? 8 : 4);
|
|
this.setHealCooldown(-1);
|
|
|
|
// deathTime == 0 checks the wizard isn't currently dying
|
|
}else if(healCooldown == -1 && this.deathTime == 0){
|
|
|
|
// Heal particles TODO: Change this so it uses the heal spell directly
|
|
if(world.isRemote){
|
|
for(int i=0; i<10; i++){
|
|
double x = (double)((float)this.posX + rand.nextFloat() * 2 - 1.0F);
|
|
// Apparently the client side spawns the particles 1 block higher than it should... hence the -
|
|
// 0.5F.
|
|
double y = (double)((float)this.posY - 0.5F + rand.nextFloat());
|
|
double z = (double)((float)this.posZ + rand.nextFloat() * 2 - 1.0F);
|
|
ParticleBuilder.create(Type.SPARKLE).pos(x, y, z).vel(0, 0.1F, 0).clr(1, 1, 0.3f).spawn(world);
|
|
}
|
|
}else{
|
|
if(this.getHealth() < 10){
|
|
// Wizards heal themseselves more often if they have low health
|
|
this.setHealCooldown(150);
|
|
}else{
|
|
this.setHealCooldown(400);
|
|
}
|
|
|
|
this.playSound(WizardrySounds.SPELL_HEAL, 0.7F, rand.nextFloat() * 0.4F + 1.0F);
|
|
}
|
|
}
|
|
if(healCooldown > 0){
|
|
this.setHealCooldown(healCooldown - 1);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected boolean processInteract(EntityPlayer player, EnumHand hand){
|
|
|
|
ItemStack stack = player.getHeldItem(hand);
|
|
|
|
// Debugging
|
|
// player.addChatComponentMessage(new TextComponentTranslation("wizard.debug",
|
|
// Spell.get(spells[1]).getDisplayName(), Spell.get(spells[2]).getDisplayName(),
|
|
// Spell.get(spells[3]).getDisplayName()));
|
|
|
|
// When right-clicked with a spell book in creative, sets one of the spells to that spell
|
|
if(player.capabilities.isCreativeMode && stack.getItem() instanceof ItemSpellBook){
|
|
Spell spell = Spell.get(stack.getItemDamage());
|
|
if(this.spells.size() >= 4 && spell.canBeCastByNPCs()){
|
|
// The set(...) method returns the element that was replaced - neat!
|
|
player.sendMessage(new TextComponentTranslation("item." + Wizardry.MODID + ":spell_book.apply_to_wizard",
|
|
this.getDisplayName(), this.spells.set(rand.nextInt(3) + 1, spell).getNameForTranslationFormatted(),
|
|
spell.getNameForTranslationFormatted()));
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
@Override
|
|
public void writeEntityToNBT(NBTTagCompound nbt){
|
|
super.writeEntityToNBT(nbt);
|
|
nbt.setInteger("element", this.getElement().ordinal());
|
|
nbt.setInteger("skin", this.textureIndex);
|
|
nbt.setTag("spells", WizardryUtilities.listToNBT(spells, spell -> new NBTTagInt(spell.id())));
|
|
nbt.setBoolean("hasTower", this.hasTower);
|
|
}
|
|
|
|
@Override
|
|
public void readEntityFromNBT(NBTTagCompound nbt){
|
|
super.readEntityFromNBT(nbt);
|
|
this.setElement(Element.values()[nbt.getInteger("element")]);
|
|
this.textureIndex = nbt.getInteger("skin");
|
|
this.spells = (List<Spell>)WizardryUtilities.NBTToList(nbt.getTagList("spells", NBT.TAG_INT),
|
|
(NBTTagInt tag) -> Spell.get(tag.getInt()));
|
|
this.hasTower = nbt.getBoolean("hasTower");
|
|
}
|
|
|
|
@Override
|
|
public int getMaxSpawnedInChunk(){
|
|
return 1;
|
|
}
|
|
|
|
@Override
|
|
public boolean getCanSpawnHere(){
|
|
// Evil wizards can only spawn in the specified dimensions
|
|
for(int id : Wizardry.settings.evilWizardDimensions){
|
|
if(this.dimension == id) return super.getCanSpawnHere();
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
@Override
|
|
protected boolean canDespawn(){
|
|
// Evil wizards can only despawn if they don't have a tower (i.e. if they spawned naturally at night)
|
|
return !this.hasTower;
|
|
}
|
|
|
|
@Override
|
|
protected float getSoundPitch(){
|
|
return (this.rand.nextFloat() - this.rand.nextFloat()) * 0.2F + 0.6F;
|
|
}
|
|
|
|
@Override
|
|
protected SoundEvent getAmbientSound(){
|
|
return SoundEvents.ENTITY_WITCH_AMBIENT;
|
|
}
|
|
|
|
@Override
|
|
protected SoundEvent getHurtSound(DamageSource source){
|
|
return SoundEvents.ENTITY_WITCH_HURT;
|
|
}
|
|
|
|
@Override
|
|
protected SoundEvent getDeathSound(){
|
|
return SoundEvents.ENTITY_WITCH_DEATH;
|
|
}
|
|
|
|
// Although it *looks* like this is still called, in actual fact the only method that calls it is overridden in
|
|
// EntityLiving to use the loot table system instead. This has been kept as a fallback in case the loot table is
|
|
// not found.
|
|
@Override
|
|
protected void dropFewItems(boolean hitByPlayer, int lootingLevel){
|
|
// Drops 3-5 crystals without looting bonuses
|
|
int j = 3 + this.rand.nextInt(3) + this.rand.nextInt(1 + lootingLevel);
|
|
|
|
for(int k = 0; k < j; k++){
|
|
this.dropItem(WizardryItems.magic_crystal, 1);
|
|
}
|
|
|
|
// Evil wizards occasionally drop one of their spells as a spell book, but not magic missile. This isn't in
|
|
// the dropRareDrop method because that would be just as rare as normal mobs; instead this is half as rare.
|
|
if(this.spells.size() > 0 && rand.nextInt(100) - lootingLevel < 5)
|
|
this.entityDropItem(new ItemStack(WizardryItems.spell_book, 1,
|
|
this.spells.get(1 + rand.nextInt(this.spells.size() - 1)).id()), 0);
|
|
}
|
|
|
|
@Override
|
|
protected ResourceLocation getLootTable(){
|
|
return LOOT_TABLE;
|
|
}
|
|
|
|
@Override
|
|
public IEntityLivingData onInitialSpawn(DifficultyInstance difficulty, IEntityLivingData data){
|
|
|
|
data = super.onInitialSpawn(difficulty, data);
|
|
|
|
textureIndex = this.rand.nextInt(6);
|
|
|
|
if(rand.nextBoolean()){
|
|
this.setElement(Element.values()[rand.nextInt(Element.values().length - 1) + 1]);
|
|
}else{
|
|
this.setElement(Element.MAGIC);
|
|
}
|
|
|
|
Element element = this.getElement();
|
|
|
|
// Adds armour.
|
|
for(EntityEquipmentSlot slot : WizardryUtilities.ARMOUR_SLOTS){
|
|
this.setItemStackToSlot(slot, new ItemStack(WizardryUtilities.getArmour(element, slot)));
|
|
}
|
|
|
|
// Default chance is 0.085f, for reference.
|
|
for(EntityEquipmentSlot slot : EntityEquipmentSlot.values())
|
|
this.setDropChance(slot, 0.0f);
|
|
|
|
// All wizards know magic missile, even if it is disabled.
|
|
spells.add(Spells.magic_missile);
|
|
|
|
Tier maxTier = EntityWizard.populateSpells(spells, element, 3, rand);
|
|
|
|
// Now done after the spells so it can take the tier into account. For evil wizards this is slightly different;
|
|
// it picks a random wand which is at least a high enough tier for the spells the wizard has.
|
|
Tier tier = Tier.values()[maxTier.ordinal() + rand.nextInt(Tier.values().length - maxTier.ordinal())];
|
|
this.setItemStackToSlot(EntityEquipmentSlot.MAINHAND, new ItemStack(WizardryUtilities.getWand(tier, element)));
|
|
|
|
return data;
|
|
}
|
|
|
|
@Override
|
|
public void writeSpawnData(ByteBuf data){
|
|
data.writeInt(textureIndex);
|
|
}
|
|
|
|
@Override
|
|
public void readSpawnData(ByteBuf data){
|
|
textureIndex = data.readInt();
|
|
}
|
|
|
|
}
|