Miscellaneous setup

This commit is contained in:
Electroblob
2018-01-30 16:02:13 +00:00
parent da6b007a3a
commit 2af7329478
59 changed files with 1833 additions and 13509 deletions
@@ -3,16 +3,18 @@ package electroblob.wizardry.entity.living;
import java.util.ArrayList;
import java.util.List;
import electroblob.wizardry.event.SpellCastEvent;
import electroblob.wizardry.event.SpellCastEvent.Source;
import electroblob.wizardry.packet.PacketNPCCastSpell;
import electroblob.wizardry.packet.WizardryPacketHandler;
import electroblob.wizardry.registry.Spells;
import electroblob.wizardry.registry.WizardryPotions;
import electroblob.wizardry.spell.Spell;
import electroblob.wizardry.util.SpellModifiers;
import net.minecraft.entity.EntityLiving;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.entity.ai.EntityAIBase;
import net.minecraft.util.EnumHand;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.fml.common.network.NetworkRegistry.TargetPoint;
import net.minecraftforge.fml.common.network.simpleimpl.IMessage;
@@ -20,192 +22,217 @@ import net.minecraftforge.fml.common.network.simpleimpl.IMessage;
* and the attack cooldown. Also provides an automatic implementation of continuous spell casting using the methods
* specified in {@code ISpellCaster}; all the entity class needs to do is implement those methods. */
public class EntityAIAttackSpell extends EntityAIBase {
/** The entity the AI instance has been applied to. */
private final EntityLiving attacker;
/** The entity the AI instance has been applied to, but as an ISpellCaster. */
private final ISpellCaster caster;
/** The tagret to be attacked. */
private EntityLivingBase target;
/** Decremented each tick while greater than 0. When a spell is cast, this is set to that spell's cooldown plus
* the base cooldown. */
private int cooldown;
/** The number of ticks between the entity finding a new target and when it first starts attacking, and also the
* amount that is added to the spell's cooldown between casting spells. */
private final int baseCooldown;
/** Decremented each tick while greater than 0. When a continuous spell is first cast, this is set to
* the value of {@link EntityAIAttackSpell#continuousSpellDuration}. */
// I think that in this case this is only necessary on the server side. If any inconsistent behaviour
// occurs, look into syncing this as well.
private int continuousSpellTimer;
/** The number of ticks that continuous spells will be cast for before cooling down. */
private final int continuousSpellDuration;
/** The speed that the entity should move when attacking. Only used when passed into the navigator. */
private final double speed;
private int seeTime;
private final float maxAttackDistance;
/**
* Creates a new spell attack AI with the given parameters.
* @param attacker The entity that that uses this AI.
* @param speed The speed that the entity should move when attacking. Only used when passed into the navigator.
* @param maxDistance The maximum distance the entity should be from its target.
* @param baseCooldown The number of ticks between the entity finding a new target and when it first starts attacking,
* and also the amount that is added to the cooldown of the spell that has just been cast.
* @param continuousSpellDuration The number of ticks that continuous spells will be cast for before cooling down.
*/
public EntityAIAttackSpell(ISpellCaster attacker, double speed, float maxDistance, int baseCooldown, int continuousSpellDuration){
this.cooldown = -1;
if(!(attacker instanceof EntityLiving)){
throw new IllegalArgumentException("Tried to create an EntityAICastSpell for an entity that isn't an EntityLiving");
}else{
this.caster = attacker;
this.attacker = (EntityLiving)attacker;
this.baseCooldown = baseCooldown;
this.continuousSpellDuration = continuousSpellDuration;
this.speed = speed;
this.maxAttackDistance = maxDistance * maxDistance;
this.setMutexBits(3);
}
}
/** The entity the AI instance has been applied to. */
private final EntityLiving attacker;
/** The entity the AI instance has been applied to, but as an ISpellCaster. */
private final ISpellCaster caster;
/** The tagret to be attacked. */
private EntityLivingBase target;
/** Decremented each tick while greater than 0. When a spell is cast, this is set to that spell's cooldown plus
* the base cooldown. */
private int cooldown;
/** The number of ticks between the entity finding a new target and when it first starts attacking, and also the
* amount that is added to the spell's cooldown between casting spells. */
private final int baseCooldown;
/** Decremented each tick while greater than 0. When a continuous spell is first cast, this is set to
* the value of {@link EntityAIAttackSpell#continuousSpellDuration}. */
// I think that in this case this is only necessary on the server side. If any inconsistent behaviour
// occurs, look into syncing this as well.
private int continuousSpellTimer;
/** The number of ticks that continuous spells will be cast for before cooling down. */
private final int continuousSpellDuration;
/** The speed that the entity should move when attacking. Only used when passed into the navigator. */
private final double speed;
private int seeTime;
private final float maxAttackDistance;
@Override
public boolean shouldExecute(){
EntityLivingBase entitylivingbase = this.attacker.getAttackTarget();
/**
* Creates a new spell attack AI with the given parameters.
* @param attacker The entity that that uses this AI.
* @param speed The speed that the entity should move when attacking. Only used when passed into the navigator.
* @param maxDistance The maximum distance the entity should be from its target.
* @param baseCooldown The number of ticks between the entity finding a new target and when it first starts attacking,
* and also the amount that is added to the cooldown of the spell that has just been cast.
* @param continuousSpellDuration The number of ticks that continuous spells will be cast for before cooling down.
*/
public EntityAIAttackSpell(ISpellCaster attacker, double speed, float maxDistance, int baseCooldown, int continuousSpellDuration){
if(entitylivingbase == null){
return false;
}else{
this.target = entitylivingbase;
return true;
}
}
this.cooldown = -1;
if(!(attacker instanceof EntityLiving)){
throw new IllegalArgumentException("Tried to create an EntityAICastSpell for an entity that isn't an EntityLiving");
}else{
this.caster = attacker;
this.attacker = (EntityLiving)attacker;
this.baseCooldown = baseCooldown;
this.continuousSpellDuration = continuousSpellDuration;
this.speed = speed;
this.maxAttackDistance = maxDistance * maxDistance;
this.setMutexBits(3);
}
}
@Override
public boolean continueExecuting(){
return this.shouldExecute() || !this.attacker.getNavigator().noPath();
}
public boolean shouldExecute(){
@Override
public void resetTask(){
this.target = null;
this.seeTime = 0;
this.cooldown = -1;
this.setContinuousSpellAndNotify(Spells.none, new SpellModifiers());
this.continuousSpellTimer = 0;
}
private void setContinuousSpellAndNotify(Spell spell, SpellModifiers modifiers){
caster.setContinuousSpell(spell);
WizardryPacketHandler.net.sendToAllAround(new PacketNPCCastSpell.Message(attacker.getEntityId(),
target == null ? -1 : target.getEntityId(), EnumHand.MAIN_HAND, spell.id(), modifiers),
// Particles are usually only visible from 16 blocks away, so 128 is more than far enough.
new TargetPoint(attacker.dimension, attacker.posX, attacker.posY, attacker.posZ, 128));
}
EntityLivingBase entitylivingbase = this.attacker.getAttackTarget();
@Override
public void updateTask(){
// Only executed server side.
double distanceSq = this.attacker.getDistanceSq(this.target.posX, this.target.getEntityBoundingBox().minY, this.target.posZ);
boolean targetIsVisible = this.attacker.getEntitySenses().canSee(this.target);
if(entitylivingbase == null){
return false;
}else{
this.target = entitylivingbase;
return true;
}
}
if(targetIsVisible){
++this.seeTime;
}else{
this.seeTime = 0;
}
@Override
public boolean continueExecuting(){
return this.shouldExecute() || !this.attacker.getNavigator().noPath();
}
if(distanceSq <= (double)this.maxAttackDistance && this.seeTime >= 20){
this.attacker.getNavigator().clearPathEntity();
}else{
this.attacker.getNavigator().tryMoveToEntityLiving(this.target, this.speed);
}
@Override
public void resetTask(){
this.target = null;
this.seeTime = 0;
this.cooldown = -1;
this.setContinuousSpellAndNotify(Spells.none, new SpellModifiers());
this.continuousSpellTimer = 0;
}
this.attacker.getLookHelper().setLookPositionWithEntity(this.target, 30.0F, 30.0F);
private void setContinuousSpellAndNotify(Spell spell, SpellModifiers modifiers){
caster.setContinuousSpell(spell);
WizardryPacketHandler.net.sendToAllAround(new PacketNPCCastSpell.Message(attacker.getEntityId(),
target == null ? -1 : target.getEntityId(), EnumHand.MAIN_HAND, spell.id(), modifiers),
// Particles are usually only visible from 16 blocks away, so 128 is more than far enough.
new TargetPoint(attacker.dimension, attacker.posX, attacker.posY, attacker.posZ, 128));
}
if(this.continuousSpellTimer > 0){
this.continuousSpellTimer--;
if(distanceSq > (double)this.maxAttackDistance || !targetIsVisible
|| !caster.getContinuousSpell().cast(attacker.worldObj, attacker, EnumHand.MAIN_HAND,
this.continuousSpellDuration - this.continuousSpellTimer, target, caster.getModifiers())
|| this.continuousSpellTimer == 0){
// If the spell no longer succeeds, the target goes out of range or sight, or the time has elapsed,
// reset the continuous spell timer and start the cooldown.
this.continuousSpellTimer = 0;
setContinuousSpellAndNotify(Spells.none, new SpellModifiers());
this.cooldown = this.baseCooldown;
return;
}
}else if(--this.cooldown == 0){
if(distanceSq > (double)this.maxAttackDistance || !targetIsVisible){
return;
}
if(!attacker.isPotionActive(WizardryPotions.arcane_jammer)){
double dx = target.posX - attacker.posX;
double dz = target.posZ - attacker.posZ;
@Override
public void updateTask(){
List<Spell> spells = new ArrayList<Spell>(caster.getSpells());
if(spells.size() > 0){
// Only executed server side.
if(!attacker.worldObj.isRemote){
double distanceSq = this.attacker.getDistanceSq(this.target.posX, this.target.getEntityBoundingBox().minY, this.target.posZ);
boolean targetIsVisible = this.attacker.getEntitySenses().canSee(this.target);
// New way of choosing a spell; keeps trying until one works or all have been tried
if(targetIsVisible){
++this.seeTime;
}else{
this.seeTime = 0;
}
Spell spell;
if(distanceSq <= (double)this.maxAttackDistance && this.seeTime >= 20){
this.attacker.getNavigator().clearPathEntity();
}else{
this.attacker.getNavigator().tryMoveToEntityLiving(this.target, this.speed);
}
while(!spells.isEmpty()){
this.attacker.getLookHelper().setLookPositionWithEntity(this.target, 30.0F, 30.0F);
spell = spells.get(attacker.worldObj.rand.nextInt(spells.size()));
SpellModifiers modifiers = caster.getModifiers();
if(this.continuousSpellTimer > 0){
if(spell != null && spell.cast(attacker.worldObj, attacker, EnumHand.MAIN_HAND, 0, target, modifiers)){
this.continuousSpellTimer--;
// If the target goes out of range or out of sight...
if(distanceSq > (double)this.maxAttackDistance || !targetIsVisible
// ...or the spell is cancelled via events...
|| MinecraftForge.EVENT_BUS.post(new SpellCastEvent.Tick(attacker, caster.getContinuousSpell(),
caster.getModifiers(), Source.NPC, this.continuousSpellDuration - this.continuousSpellTimer))
// ...or the spell no longer succeeds...
|| !caster.getContinuousSpell().cast(attacker.worldObj, attacker, EnumHand.MAIN_HAND,
this.continuousSpellDuration - this.continuousSpellTimer, target, caster.getModifiers())
// ...or the time has elapsed...
|| this.continuousSpellTimer == 0){
// ...reset the continuous spell timer and start the cooldown.
this.continuousSpellTimer = 0;
setContinuousSpellAndNotify(Spells.none, new SpellModifiers());
this.cooldown = this.baseCooldown;
return;
}else if(this.continuousSpellDuration - this.continuousSpellTimer == 1){
// On the first tick, if the spell did succeed, fire SpellCastEvent.Post.
MinecraftForge.EVENT_BUS.post(new SpellCastEvent.Post(attacker, caster.getContinuousSpell(),
caster.getModifiers(), Source.NPC));
}
if(spell.isContinuous){
// -1 because the spell has been cast once already!
this.continuousSpellTimer = this.continuousSpellDuration - 1;
setContinuousSpellAndNotify(spell, modifiers);
}else{
// For now, the cooldown is just added to the constant base cooldown. I think this
// is a reasonable way of doing things; it's certainly better than before.
this.cooldown = this.baseCooldown + spell.cooldown;
if(spell.doesSpellRequirePacket()){
// Sends a packet to all players in dimension to tell them to spawn particles.
IMessage msg = new PacketNPCCastSpell.Message(attacker.getEntityId(),
target.getEntityId(), EnumHand.MAIN_HAND, spell.id(), modifiers);
WizardryPacketHandler.net.sendToDimension(msg, attacker.worldObj.provider.getDimension());
}
}
}else if(--this.cooldown == 0){
attacker.rotationYaw = (float)(Math.atan2(dz, dx) * 180.0D / Math.PI) - 90.0F;
if(distanceSq > (double)this.maxAttackDistance || !targetIsVisible){
return;
}
return;
double dx = target.posX - attacker.posX;
double dz = target.posZ - attacker.posZ;
}else{
spells.remove(spell);
}
}
}
}
}
}else if(this.cooldown < 0){
// This should only be reached when the entity first starts attacking. Stops it attacking instantly.
this.cooldown = this.baseCooldown;
}
}
List<Spell> spells = new ArrayList<Spell>(caster.getSpells());
if(spells.size() > 0){
if(!attacker.worldObj.isRemote){
// New way of choosing a spell; keeps trying until one works or all have been tried
Spell spell;
while(!spells.isEmpty()){
spell = spells.get(attacker.worldObj.rand.nextInt(spells.size()));
SpellModifiers modifiers = caster.getModifiers();
if(spell != null && attemptCastSpell(spell, modifiers)){
// The spell worked, so we're done!
attacker.rotationYaw = (float)(Math.atan2(dz, dx) * 180.0D / Math.PI) - 90.0F;
return;
}else{
spells.remove(spell);
}
}
}
}
}else if(this.cooldown < 0){
// This should only be reached when the entity first starts attacking. Stops it attacking instantly.
this.cooldown = this.baseCooldown;
}
}
/** Attempts to cast the given spell (including event firing) and returns true if it succeeded. */
private boolean attemptCastSpell(Spell spell, SpellModifiers modifiers){
// If anything stops the spell working at this point, nothing else happens.
if(MinecraftForge.EVENT_BUS.post(new SpellCastEvent.Pre(attacker, spell, modifiers, Source.NPC))){
return false;
}
if(spell.cast(attacker.worldObj, attacker, EnumHand.MAIN_HAND, 0, target, modifiers)){
if(spell.isContinuous){
// -1 because the spell has been cast once already!
this.continuousSpellTimer = this.continuousSpellDuration - 1;
setContinuousSpellAndNotify(spell, modifiers);
}else{
MinecraftForge.EVENT_BUS.post(new SpellCastEvent.Post(attacker, spell, modifiers, Source.NPC));
// For now, the cooldown is just added to the constant base cooldown. I think this
// is a reasonable way of doing things; it's certainly better than before.
this.cooldown = this.baseCooldown + spell.cooldown;
if(spell.doesSpellRequirePacket()){
// Sends a packet to all players in dimension to tell them to spawn particles.
IMessage msg = new PacketNPCCastSpell.Message(attacker.getEntityId(),
target.getEntityId(), EnumHand.MAIN_HAND, spell.id(), modifiers);
WizardryPacketHandler.net.sendToDimension(msg, attacker.worldObj.provider.getDimension());
}
}
return true;
}
return false;
}
}
@@ -66,7 +66,7 @@ public class EntityEvilWizard extends EntityMob implements ISpellCaster, IEntity
protected Predicate<Entity> targetSelector;
/** Data parameter for the cooldown time for wizards healing themselves. */
private static final DataParameter<Integer> HEAL_COOLDOWN = EntityDataManager.createKey(EntityWizard.class, DataSerializers.VARINT);
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. */
@@ -278,6 +278,7 @@ public class EntityIceWraith extends EntityBlazeMinion {
if(this.attackStep > 1){
// Proof, if it were at all needed, of the elegance and versatility of the spell system.
Spells.ice_shard.cast(this.blaze.worldObj, this.blaze, EnumHand.MAIN_HAND, 0, entitylivingbase, new SpellModifiers());
// TODO: Decide if an event should be fired here. I'm guessing no.
}
}
@@ -162,6 +162,7 @@ public class EntityLightningWraith extends EntityBlazeMinion {
if(this.attackStep > 1){
// Proof, if it were at all needed, of the elegance and versatility of the spell system.
Spells.arc.cast(this.blaze.worldObj, this.blaze, EnumHand.MAIN_HAND, 0, entitylivingbase, new SpellModifiers());
// TODO: Decide if an event should be fired here. I'm guessing no.
}
}
@@ -69,13 +69,18 @@ import net.minecraft.village.MerchantRecipe;
import net.minecraft.village.MerchantRecipeList;
import net.minecraft.world.DifficultyInstance;
import net.minecraft.world.World;
import net.minecraftforge.common.util.FakePlayer;
import net.minecraftforge.common.util.Constants.NBT;
import net.minecraftforge.event.world.BlockEvent;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
import net.minecraftforge.fml.common.registry.IEntityAdditionalSpawnData;
import net.minecraftforge.fml.common.registry.VillagerRegistry.VillagerProfession;
import net.minecraftforge.fml.relauncher.Side;
import net.minecraftforge.fml.relauncher.SideOnly;
import net.minecraftforge.oredict.OreDictionary;
@Mod.EventBusSubscriber
public class EntityWizard extends EntityVillager implements ISpellCaster, IEntityAdditionalSpawnData {
/*
@@ -779,6 +784,25 @@ public class EntityWizard extends EntityVillager implements ISpellCaster, IEntit
// Uses .equals() rather than == so this will work fine.
return this.towerBlocks.contains(pos);
}
@SubscribeEvent
public static void onBlockBreakEvent(BlockEvent.BreakEvent event){
// Makes wizards angry if a player breaks a block in their tower
if(!(event.getPlayer() instanceof FakePlayer)){
List<EntityWizard> wizards = WizardryUtilities.getEntitiesWithinRadius(64, event.getPos().getX(),
event.getPos().getY(), event.getPos().getZ(), event.getWorld(), EntityWizard.class);
if(!wizards.isEmpty()){
for(EntityWizard wizard : wizards){
if(wizard.isBlockPartOfTower(event.getPos())){
wizard.setRevengeTarget(event.getPlayer());
event.getPlayer().addStat(WizardryAchievements.anger_wizard);
}
}
}
}
}
// EntityVillager overrides (that don't add features)
@@ -13,6 +13,10 @@ import electroblob.wizardry.WizardData;
import electroblob.wizardry.Wizardry;
import electroblob.wizardry.WizardryEventHandler;
import electroblob.wizardry.item.ItemWand;
import electroblob.wizardry.util.IElementalDamage;
import electroblob.wizardry.util.IndirectMinionDamage;
import electroblob.wizardry.util.MagicDamage.DamageType;
import electroblob.wizardry.util.MinionDamage;
import electroblob.wizardry.util.WizardryParticleType;
import electroblob.wizardry.util.WizardryUtilities;
import io.netty.buffer.ByteBuf;
@@ -23,10 +27,16 @@ import net.minecraft.entity.monster.IMob;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.item.ItemStack;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.DamageSource;
import net.minecraft.util.EntityDamageSource;
import net.minecraft.util.EntityDamageSourceIndirect;
import net.minecraft.util.EnumHand;
import net.minecraftforge.event.entity.living.LivingAttackEvent;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
import net.minecraftforge.fml.common.registry.IEntityAdditionalSpawnData;
/** Interface for all summoned creatures. The code for summoned creatures has been overhauled in Wizardry 1.2, and this
/** Interface for all summoned creatures. The code for summoned creatures has been overhauled in Wizardry 2.1, and this
* interface allows summoned creatures to extend vanilla (or indeed modded) entity classes, so <code>EntitySummonedZombie</code>
* now extends <code>EntityZombie</code>, for example. This change has two major benefits:
* <p>
@@ -59,13 +69,14 @@ import net.minecraftforge.fml.common.registry.IEntityAdditionalSpawnData;
* <p>
* Due to the limitations of interfaces, some methods that really ought to be protected are public. These are clearly
* marked as 'Internal, DO NOT CALL'. <b>Don't call them, only implement them.</b>
* @since Wizardry 1.2
* @since Wizardry 2.1
* @author Electroblob */
/*
* Quite honestly, this is not what default methods are really for. However, this is modding, and in modding some
* sacrifices have to be made when it comes to Java style - because adding on to a pre-existing program is not a
* good way of doing this sort of thing anyway, but we have no choice about that!
*/
@Mod.EventBusSubscriber
public interface ISummonedCreature extends IEntityAdditionalSpawnData {
// Remember that ALL fields are static and final in interfaces, even if they don't explicitly state that.
@@ -78,7 +89,7 @@ public interface ISummonedCreature extends IEntityAdditionalSpawnData {
/** Returns the lifetime of the summoned creature in ticks. Allows primarily for duration multiplier support, but
* also for example the skeleton legion spell which lasts for 60 seconds instead of the usual 30. Syncing and saving
* is done automatically. As of Wizardry 1.2, despawning is handled in ISummonedCreature; see
* is done automatically. As of Wizardry 2.1, despawning is handled in ISummonedCreature; see
* {@link ISummonedCreature#onDespawn()} for details. */
int getLifetime();
@@ -274,5 +285,49 @@ public interface ISummonedCreature extends IEntityAdditionalSpawnData {
return false;
}
// Damage system
@SubscribeEvent
public static void onLivingAttackEvent(LivingAttackEvent event){
// Rather than bother overriding entire attack methods in ISummonedCreature implementations, it's easier (and
// more robust) to use LivingAttackEvent to modify the damage source.
if(event.getSource().getEntity() instanceof ISummonedCreature){
EntityLivingBase summoner = ((ISummonedCreature)event.getSource().getEntity()).getCaster();
if(summoner != null){
event.setCanceled(true);
DamageSource newSource = event.getSource();
// Copies over the original DamageType if appropriate.
DamageType type = event.getSource() instanceof IElementalDamage ? ((IElementalDamage)event.getSource()).getType() : DamageType.MAGIC;
// Copies over the original isRetaliatory flag if appropriate.
boolean isRetaliatory = event.getSource() instanceof IElementalDamage && ((IElementalDamage)event.getSource()).isRetaliatory();
// All summoned creatures are classified as magic, so it makes sense to do it this way.
if(event.getSource() instanceof EntityDamageSourceIndirect){
newSource = new IndirectMinionDamage(event.getSource().damageType, event.getSource().getSourceOfDamage(), event.getSource().getEntity(), summoner, type, isRetaliatory);
}else if(event.getSource() instanceof EntityDamageSource){
// Name is copied over so it uses the appropriate vanilla death message
newSource = new MinionDamage(event.getSource().damageType, event.getSource().getEntity(), summoner, type, isRetaliatory);
}
// Copy over any relevant 'attributes' the original DamageSource might have had.
if(event.getSource().isExplosion()) newSource.setExplosion();
if(event.getSource().isFireDamage()) newSource.setFireDamage();
if(event.getSource().isProjectile()) newSource.setProjectile();
// For some reason Minecraft calculates knockback relative to DamageSource#getEntity. In vanilla this
// is unnoticeable, but it looks a bit weird with summoned creatures involved - so this fixes that.
if(WizardryUtilities.attackEntityWithoutKnockback(event.getEntity(), newSource, event.getAmount())){
WizardryUtilities.applyStandardKnockback(event.getSource().getEntity(), event.getEntityLiving());
((ISummonedCreature)event.getSource().getEntity()).onSuccessfulAttack(event.getEntityLiving());
}
}
}
}
}