public class AberrationBoss extends Zombie {
    // Final Variables
    private static final int EXP_VALUE = 5000;
    private static final int COOL_TIME = 3000 / (int)Globals.SLEEP_TIME;
    private static final double ATTACK_DISTANCE = 314.0;
    private static final int PARTICLE_LIFE = 2000;
    private static final double PARTICLE_SPREAD = 10.0;
    private static final int PARTICLE_COUNT = 5;
    private static final int PARTICLE_DAMAGE = 80;
    
    // Member Variables
    private List<Particle> particles;
    @Override
    public List<Particle> getParticles() { return this.particles; }
    @Override
    public int getParticleDamage() { return AberrationBoss.PARTICLE_DAMAGE; }
    private int cooldown;
    public boolean canFire() { return this.cooldown == 0; }
    
    public AberrationBoss(Point2D.Double p_, int health_, int damage_, double speed_, int score_, Animation animation_) {
        super(p_, Globals.ZOMBIE_BOSS_ABERRATION_TYPE, health_, damage_, speed_, score_, AberrationBoss.EXP_VALUE, animation_);
        this.particles = new ArrayList<Particle>();
        this.cooldown = AberrationBoss.COOL_TIME;
    }
    
    public void drawParticles(Graphics2D g2d) {
        synchronized(this.particles) {
            Iterator<Particle> it = this.particles.iterator();
            while(it.hasNext()) {
                Particle p = it.next();
                if(p.isAlive()) p.draw(g2d);
            }
        }
    }
    
    @Override
    public void update(Player player, List<Zombie> zombies) {
        // Update the zombie's acid particles.
        if(!this.particles.isEmpty()) {
            synchronized(this.particles) {
                Iterator<Particle> it = this.particles.iterator();
                while(it.hasNext()) {
                    Particle p = it.next();
                    p.update();
                    if(!p.isAlive() || p.outOfBounds()) {
                        it.remove();
                        continue;
                    }
                }
            }
        }
        // If the zombie's attack has cooled down, fire a particle at the player.
        if(canFire()) {
            Point2D.Double playerPos = new Point2D.Double(player.getCenterX(), player.getCenterY());
            Point2D.Double myPos = new Point2D.Double(this.x, this.y);
            if(inRange(playerPos, myPos)) {
                // Fire a new particle at the player.
                this.fire(playerPos);
                this.cooldown = AberrationBoss.COOL_TIME;
            }
        } else this.cooldown--;
    }
    
    @Override
    public void draw(Graphics2D g2d) {
        super.draw(g2d);
        if(!this.particles.isEmpty()) drawParticles(g2d);
    }
    
    @Override
    public void moan(Player player) {
        if(!this.moaned) {
            if(Globals.gameTime.getElapsedMillis() >= this.nextMoan) {
                double xD = player.getCenterX() - this.x;
                double yD = player.getCenterY() - this.y;
                double dist = Math.sqrt((xD * xD) + (yD * yD));
                double gain = 1.0 - (dist / Player.AUDIO_RANGE);
                Sounds.MOAN7.play(gain);
                this.moaned = true;
            }
        }
    }
    
    private void fire(Point2D.Double playerPos) {
        // Create group of particles.
        for(int i = 0; i < AberrationBoss.PARTICLE_COUNT; i++) {
            double theta = Math.atan2((playerPos.y - this.y), (playerPos.x - this.x)) - Math.PI;
            { // Deviate particle from intended target.
                boolean deviate = Globals.r.nextBoolean();
                if(deviate) {
                    double spread = Math.toRadians(Globals.r.nextDouble() * AberrationBoss.PARTICLE_SPREAD);
                    if(spread > 0) {
                        boolean clockwise = Globals.r.nextBoolean();
                        if(clockwise) theta += spread;
                        else theta -= spread;
                    }
                }
            } // End particle deviation.
            
            Particle p = new Particle(theta, 0.0, 8.0, (AberrationBoss.PARTICLE_LIFE / (int)Globals.SLEEP_TIME),
                new Point2D.Double(this.x, this.y), new Dimension(20, 20), Images.ACID_PARTICLE) {
                  @Override
                  public void update() {
                      if(this.isAlive()) {
                          // Age the particle.
                          this.life--;
                          // Update the position.
                          this.pos.x += -(this.speed * Math.cos(this.theta));
                          this.pos.y += -(this.speed * Math.sin(this.theta));
                      }
                  }
                };
            this.particles.add(p);
        }
    }
    
    private boolean inRange(Point2D.Double playerPos, Point2D.Double myPos) {
        double xD = playerPos.x - myPos.x;
        double yD = playerPos.y - myPos.y;
        return Math.sqrt((xD * xD) + (yD * yD)) <= AberrationBoss.ATTACK_DISTANCE;
    }
}
 public class AcidZombie extends Zombie {
    // Final Variables
    private static final int EXP_VALUE = 50;
    private static final int COOL_TIME = 3000 / (int)Globals.SLEEP_TIME;
    private static final double ATTACK_DISTANCE = 316.0;
    private static final int PARTICLE_LIFE = 2000;
    private static final int ACID_DAMAGE = 40;
    
    // Member Variables
    private List<Particle> particles;
    @Override
    public List<Particle> getParticles() { return this.particles; }
    @Override
    public int getParticleDamage() { return AcidZombie.ACID_DAMAGE; }
    private int cooldown;
    public boolean canFire() { return this.cooldown == 0; }
    
    public AcidZombie(Point2D.Double p_, int health_, int damage_, double speed_, int score_, Animation animation_) {
        super(p_, Globals.ZOMBIE_ACID_TYPE, health_, damage_, speed_, score_, AcidZombie.EXP_VALUE, animation_);
        this.particles = new ArrayList<Particle>();
        this.cooldown = AcidZombie.COOL_TIME;
    }
    
    public void drawParticles(Graphics2D g2d) {
        synchronized(this.particles) {
            Iterator<Particle> it = this.particles.iterator();
            while(it.hasNext()) {
                Particle p = it.next();
                if(p.isAlive()) p.draw(g2d);
            }
        }
    }
    
    @Override
    public void update(Player player, List<Zombie> zombies) {
        // Update the zombie's acid particles.
        if(!this.particles.isEmpty()) {
            synchronized(this.particles) {
                Iterator<Particle> it = this.particles.iterator();
                while(it.hasNext()) {
                    Particle p = it.next();
                    p.update();
                    if(!p.isAlive() || p.outOfBounds()) {
                        it.remove();
                        continue;
                    }
                }
            }
        }
        // If the zombie's attack has cooled down, fire a particle at the player.
        if(canFire()) {
            Point2D.Double playerPos = new Point2D.Double(player.getCenterX(), player.getCenterY());
            Point2D.Double myPos = new Point2D.Double(this.x, this.y);
            if(inRange(playerPos, myPos)) {
                // Fire a new particle at the player.
                this.fire(playerPos);
                this.cooldown = AcidZombie.COOL_TIME;
            }
        } else this.cooldown--;
    }
    
    @Override
    public void draw(Graphics2D g2d) {
        super.draw(g2d);
        if(!this.particles.isEmpty()) drawParticles(g2d);
    }
    
    @Override
    public void moan(Player player) {
        if(!this.moaned) {
            if(Globals.gameTime.getElapsedMillis() >= this.nextMoan) {
                double xD = player.getCenterX() - this.x;
                double yD = player.getCenterY() - this.y;
                double dist = Math.sqrt((xD * xD) + (yD * yD));
                double gain = 1.0 - (dist / Player.AUDIO_RANGE);
                Sounds.MOAN3.play(gain);
                this.moaned = true;
            }
        }
    }
    
    private void fire(Point2D.Double playerPos) {
        double theta = Math.atan2((playerPos.y - this.y), (playerPos.x - this.x)) - Math.PI;
        Particle p = new Particle(theta, 0.0, 8.0, (AcidZombie.PARTICLE_LIFE / (int)Globals.SLEEP_TIME),
            new Point2D.Double(this.x, this.y), new Dimension(20, 20), Images.ACID_PARTICLE) {
              @Override
              public void update() {
                  if(this.isAlive()) {
                      // Age the particle.
                      this.life--;
                      // Update the position.
                      this.pos.x += -(this.speed * Math.cos(this.theta));
                      this.pos.y += -(this.speed * Math.sin(this.theta));
                  }
              }
            };
        this.particles.add(p);
    }
    
    private boolean inRange(Point2D.Double playerPos, Point2D.Double myPos) {
        double xD = playerPos.x - myPos.x;
        double yD = playerPos.y - myPos.y;
        return Math.sqrt((xD * xD) + (yD * yD)) <= AcidZombie.ATTACK_DISTANCE;
    }
}
 public class Player extends Rectangle2D.Double {
    // Final variables.
    public static final int DEFAULT_HEALTH = 150;
    public static final long INVINCIBILITY_LENGTH = 3000;
    private static final double MOVE_SPEED = 2; // How many pixels per tick the player moves.
    public static final double AUDIO_RANGE = 500;
    
    public static final int MAX_HEALTH_ID = 1;
    private static final int MAX_HEALTH_INC = 50;
    public static final int DAMAGE_ID = 2;
    private static final double DAMAGE_INC = 0.2;
    public static final int SPEED_ID = 3;
    private static final double SPEED_INC = 0.2;
    
    // Member variables.
    private AffineTransform af;
    private double theta;
    private BufferedImage img;
    private LightSource light;
    
    private int health;
    private int maxHealth;
    private double damageBonus;
    private double speed;
    private double speedBonus;
    private int cash;
    
    private int experience;
    private int experienceMultiplier;
    private int level;
    private int skillPoints;
    
    private int lives;
    
    private boolean blink;
    private long nextBlinkChange;
    
    private HashMap<String, StatusEffect> statusEffects;
    private long lastPoisoned;
    
    private String currentWeaponName;
    private HashMap<String, Weapon> weaponsMap;
    
    // Statistic Variables
    private long deathTime;
    public int killCount;
    public int medkitsUsed;
    public int ammoCratesUsed;

    public Player(double x_, double y_, double w_, double h_) {
        super(x_, y_, w_, h_);
        this.af = new AffineTransform();
        this.theta = 0;
        this.img = Images.PLAYER;
        
        {
            int xLoc = (int)this.getCenterX();
            int yLoc = (int)this.getCenterY();
            float intensity = 64.0f;
            float [] distance = {0.0f, 0.6f, 0.8f, 1.0f};
            Color [] colors = {new Color(0.0f, 0.0f, 0.0f, 0.0f),
                                       new Color(0.0f, 0.0f, 0.0f, 0.75f),
                                       new Color(0.0f, 0.0f, 0.0f, 0.9f),
                                       Color.BLACK};
            this.light = new LightSource(new Point2D.Double(xLoc, yLoc), 0, intensity, distance, colors);
        }

        this.health = Player.DEFAULT_HEALTH;
        this.maxHealth = Player.DEFAULT_HEALTH;
        this.damageBonus = 0;
        this.speed = Player.MOVE_SPEED;
        this.speedBonus = 0;
        this.cash = 0;
        
        this.experience = 0;
        this.experienceMultiplier = 1;
        this.level = 1;
        this.skillPoints = 0;
        
        this.lives = 3;
        
        this.blink = false;
        this.nextBlinkChange = Globals.gameTime.getElapsedMillis();
        
        this.statusEffects = new HashMap<String, StatusEffect>();
        
        this.lastPoisoned = Globals.gameTime.getElapsedMillis();
        
        this.deathTime = Globals.gameTime.getElapsedMillis();
        this.killCount = 0;
        this.medkitsUsed = 0;
        this.ammoCratesUsed = 0;
        
        this.weaponsMap = new HashMap<String, Weapon>();
        this.addWeapon(Globals.HANDGUN);
        this.currentWeaponName = Globals.HANDGUN.getName();
    }

    // Getter/Setter methods.
    public AffineTransform getTransform() { return this.af; }
    public double getTheta() { return this.theta; }
    public BufferedImage getImage() { return this.img; }
    public LightSource getLightSource() { return this.light; }

    public int getHealth() { return this.health; }
    public int getMaxHealth() { return this.maxHealth; }
    public double getDamageBonus() { return this.damageBonus; }
    public double getSpeed() { return (this.speed + this.speedBonus); }
    public int getCash() { return this.cash; }
    public void addCash(int amount) { this.cash += amount; }
    public void removeCash(int amount) { this.cash -= amount; }
    public int getExp() { return this.experience; }
    public void addExp(int amount) { this.experience += (amount * this.experienceMultiplier); }
    public int getNextLevelExp() {
        return ((this.level * 1000) + (((int)(this.level / 4)) * 2000));
    }
    public int getLevel() { return this.level; }
    public int getSkillPoints() { return this.skillPoints; }
    public void addKill() { this.killCount++; }
    public void takeDamage(int damage_) { this.health -= damage_; }
    public void addHealth(int amount) { 
        if((this.health + amount) > this.maxHealth) this.health = this.maxHealth;
        else this.health += amount;
    }
    public boolean isAlive() { return this.health > 0; }
    public long getDeathTime() { return this.deathTime; }
    public int getLives() { return this.lives; }
    public void addLife() { this.lives++; }
    public void die() { 
        this.lives--;
        if(this.lives > 0) reset();
    }
    public int spendPoint(int id, int currentLevel) {
        if(id == Player.MAX_HEALTH_ID) {
            // Determine if player has enough points to buy the next upgrade.;
            if(this.skillPoints >= (currentLevel + 1)) {
                // Determine if the player has already maxed out this skill.
                if(currentLevel < 5) {
                    // Deduct the appropriate number of skill points and raise the skill to the next level.
                    this.skillPoints -= (currentLevel + 1);
                    this.maxHealth += Player.MAX_HEALTH_INC;
                    this.addHealth(Player.MAX_HEALTH_INC);
                    synchronized(Globals.GAME_MESSAGES) { Globals.GAME_MESSAGES.add(new Message("Max Health increased!", 5000)); }
                    return 1;
                }
            }
        } else if(id == Player.DAMAGE_ID) {
            // Determine if player has enough points to buy the next upgrade.
            if(this.skillPoints >= (currentLevel + 1)) {
                // Determine if the player has already maxed out this skill.
                if(currentLevel < 5) {
                    // Deduct the appropriate number of skill points and raise the skill to the next level.
                    this.skillPoints -= (currentLevel + 1);
                    this.damageBonus += Player.DAMAGE_INC;
                    synchronized(Globals.GAME_MESSAGES) { Globals.GAME_MESSAGES.add(new Message("Damage increased!", 5000)); }
                    return 1;
                }
            }
        } else if(id == Player.SPEED_ID) {
            // Determine if player has enough points to buy the next upgrade.
            if(this.skillPoints >= (currentLevel + 1)) {
                // Determine if the player has already maxed out this skill.
                if(currentLevel < 5) {
                    // Deduct the appropriate number of skill points and raise the skill to the next level.
                    this.skillPoints -= (currentLevel + 1);
                    this.speedBonus += Player.SPEED_INC;
                    synchronized(Globals.GAME_MESSAGES) { Globals.GAME_MESSAGES.add(new Message("Speed increased!", 5000)); }
                    return 1;
                }
            }
        }
        return 0;
    }
    public void reset() {
        this.statusEffects.clear();
        
        if(this.lives == 0) {
            this.maxHealth = Player.DEFAULT_HEALTH;
            this.damageBonus = 0;
            this.speedBonus = 0;
            this.cash = 0;
            
            this.experience = 0;
            this.level = 1;
            this.skillPoints = 0;
            
            this.lives = 3;
            
            this.deathTime = Globals.gameTime.getElapsedMillis();
            this.weaponsMap = new HashMap<String, Weapon>();
            this.weaponsMap.put(Globals.HANDGUN.getName(), Globals.HANDGUN);
        }
        else {
            this.addStatusEffect(Invulnerability.ID, Invulnerability.EFFECT_NAME, Images.INVULNERABILITY, 3000, 0);
            this.blink = true;
            this.nextBlinkChange = Globals.gameTime.getElapsedMillis() + 300;
        }
        if(this.hasEffect("Poison")) this.removeEffect("Poison");
        this.lastPoisoned = Globals.gameTime.getElapsedMillis();
        
        this.health = this.maxHealth;
        this.speed = Player.MOVE_SPEED;
        this.experienceMultiplier = 1;
        this.currentWeaponName = Globals.HANDGUN.getName();
        this.x = (Globals.W_WIDTH / 2) - (this.width / 2);
        this.y = (Globals.W_HEIGHT / 2) - (this.height / 2);
        this.light.move(new Point2D.Double((int)this.getCenterX(), (int)this.getCenterY()));
    }
    public void resetStatistics() {
        this.killCount = 0;
        this.medkitsUsed = 0;
        this.ammoCratesUsed = 0;
    }
    
    public boolean hasWeapon(String name) { return this.weaponsMap.containsKey(name); }
    public Weapon getWeapon() { return this.weaponsMap.get(this.currentWeaponName); }
    public Weapon getWeapon(String name) { return this.weaponsMap.get(name); }
    public String getCurrentWeaponName() { return this.currentWeaponName; }
    public HashMap<String, Weapon> getWeaponsMap() { return this.weaponsMap; }
    public int setWeapon(String name) {
        if(this.weaponsMap.containsKey(name)) {
            this.currentWeaponName = name;
            Sounds.FLAMETHROWER.getAudio().stop();
            return 1;
        } else return 0;
    }
    public void addWeapon(Weapon w) {
        this.weaponsMap.put(w.getName(), w);
    }
    
    public void update() {
        // Calculate the player's angle based on the mouse position.
        double cX = this.getCenterX();
        double cY = this.getCenterY();
        this.theta = Math.atan2((cY - Globals.mousePos.y), (cX - Globals.mousePos.x)) - Math.PI / 2;
        this.rotate(this.theta);
        
        // Move the player according to which keys are being held down.
        for(int i = 0; i < Globals.keys.length; i++)  {
            if(Globals.keys[i]) this.move(i);
        }
        
        // If the left mouse button is held down, create a new projectile.
        if(Globals.buttons[0]) {
            Point target = new Point(Globals.mousePos);
            Point2D.Double pos = new Point2D.Double((this.x + 28), (this.y + 2));
            AffineTransform.getRotateInstance(this.theta, this.getCenterX(), this.getCenterY()).transform(pos, pos);
            double theta = Math.atan2((target.x - pos.x), (target.y - pos.y));
            this.getWeapon().fire(theta, pos, this);
        }
        
        { // Resolve status effects.
            // Power-Up Effects
            if(this.hasEffect(SpeedUp.EFFECT_NAME)) {
                StatusEffect status = this.statusEffects.get(SpeedUp.EFFECT_NAME);
                if(status.isActive()) {
                    // Change the player's move speed.
                    this.speed = Player.MOVE_SPEED * status.getValue();
                } else {
                    // Reset the player's movement speed and remove the status.
                    this.speed = Player.MOVE_SPEED;
                    this.removeEffect(SpeedUp.EFFECT_NAME);
                }
            }
            if(this.hasEffect(UnlimitedAmmo.EFFECT_NAME)) {
                // If the player has the unlimited ammo effect, but it is no longer active, remove it.
                StatusEffect status = this.statusEffects.get(UnlimitedAmmo.EFFECT_NAME);
                if(!status.isActive()) this.removeEffect(UnlimitedAmmo.EFFECT_NAME);
            }
            if(this.hasEffect(ExpMultiplier.EFFECT_NAME)) {
                // If the player no longer has the experience multiplier effect, remove it and reset the multiplier.
                StatusEffect status = this.statusEffects.get(ExpMultiplier.EFFECT_NAME);
                if(status.isActive()) {
                    // Change the player's experience multiplier.
                    this.experienceMultiplier = 2;
                } else {
                    this.experienceMultiplier = 1;
                    this.removeEffect(ExpMultiplier.EFFECT_NAME);
                }
            }
            if(this.hasEffect(NightVision.EFFECT_NAME)) {
                // If the player no longer has the effect, remove it.
                StatusEffect status = this.statusEffects.get(NightVision.EFFECT_NAME);
                if(!status.isActive()) this.removeEffect(NightVision.EFFECT_NAME);
            }
            
            // Negative Effects
            if(this.hasEffect("Poison")) {
                StatusEffect status = this.statusEffects.get("Poison");
                if(status.isActive()) {
                    if(!this.hasEffect(Invulnerability.EFFECT_NAME)) {
                        // If the player is poisoned, take damage.
                        if(Globals.gameTime.getElapsedMillis() >= (this.lastPoisoned + 1000)) {
                            this.lastPoisoned = Globals.gameTime.getElapsedMillis();
                            this.takeDamage((int)status.getValue());
                        }
                    }
                } else this.removeEffect("Poison");
            }
            
            // Other Effects
            if(this.hasEffect(Invulnerability.EFFECT_NAME)) {
                StatusEffect status = this.statusEffects.get(Invulnerability.EFFECT_NAME);
                if(status.isActive()) {
                    if(Globals.gameTime.getElapsedMillis() >= this.nextBlinkChange) {
                        this.blink = ((this.blink)?false:true);
                        this.nextBlinkChange = Globals.gameTime.getElapsedMillis() + 300;
                    }
                } else {
                    this.removeEffect(Invulnerability.EFFECT_NAME);
                    this.blink = false;
                }
            }
            
            // Check player to see if he has leveled up.
            this.checkLevel();
        } // End resolving status effects.
    }
    
    public void draw(Graphics2D g2d) {
        if(!this.blink) {
            g2d.setTransform(this.af);
            { // Draw player shadow.
                int w = this.img.getWidth();
                int h = this.img.getHeight();
                int xPos = (int)(this.getCenterX() - (w / 2));
                int yPos = (int)(this.getCenterY() - (h / 2) + 5);
                g2d.setColor(new Color(0, 0, 0, 100));
                g2d.fillOval((xPos + 5), (yPos + 6), (w - 10), (h - 12));
            } // End drawing player shadow.
            g2d.drawImage(this.img, (int) this.x, (int) this.y, null);
        }
    }

    // Shape manipulation.
    public void rotate(double theta_) { this.af.setToRotation(theta_, getCenterX(), getCenterY()); }

    // Player manipulation.
    public void checkLevel() {
        while(this.experience >= this.getNextLevelExp()) {
            this.experience -= this.getNextLevelExp();
            this.level++;
            this.skillPoints++;
            synchronized(Globals.GAME_MESSAGES) { 
                Globals.GAME_MESSAGES.add(new Message(("Played reached level " + this.level + "!"), 5000)); 
            }
        }
    }
    
    public void move(int direction) {
        if(direction == 0) y -= (this.speed + this.speedBonus);
        else if(direction == 1) x -= (this.speed + this.speedBonus);
        else if(direction == 2) y += (this.speed + this.speedBonus);
        else if(direction == 3) x += (this.speed + this.speedBonus);
        this.light.move(new Point2D.Double((int)this.getCenterX(), (int)this.getCenterY()));
    }
    
    public void move(Point2D.Double p) {
        this.x = p.x;
        this.y = p.y;
        this.light.move(p);
    }
    
    public HashMap<String, StatusEffect> getStatusEffects() { return this.statusEffects; }
    
    public void addStatusEffect(int id, String name, BufferedImage img, long duration, int value) {
        if(!this.statusEffects.containsKey(name)) this.statusEffects.put(name, new StatusEffect(img, duration, value));
        else this.statusEffects.get(name).refresh(duration);
    }
    
    public void removeEffect(String name) {
        if(this.hasEffect(name)) this.statusEffects.remove(name);
    }
    
    public boolean hasEffect(String name) {
        return this.statusEffects.containsKey(name);
    }
}
 public class PoisonFogZombie extends Zombie {
    // Final Variables
    private static final int EXP_VALUE = 100;
    private static final int POISON_DAMAGE = 5;
    // Member Variables
    private Explosion explosion;
    private boolean exploded;
    private boolean damageDone;
    
    public PoisonFogZombie(Point2D.Double p_, int health_, int damage_, double speed_, int score_, Animation animation_) {
        super(p_, Globals.ZOMBIE_POISONFOG_TYPE, health_, damage_, speed_, score_, PoisonFogZombie.EXP_VALUE, animation_);
        this.explosion = new Explosion(Images.POISON_GAS_SHEET, new Point2D.Double(this.x, this.y));
        this.exploded = false;
        this.damageDone = false;
    }
    
    @Override
    public boolean isDead() {
        // If the zombie has exploded and the explosion animation is no longer active, return true.
        return (this.exploded && !this.explosion.getImage().isActive());
    }
    
    @Override
    public int getDamage() {
        if(!isDead()) return super.getDamage();
        else return 0;
    }
    
    private boolean collidesWithExplosion(Rectangle2D.Double rect) {
        int width = this.explosion.getSize().width;
        int height = this.explosion.getSize().height;
        Rectangle2D.Double expRect = new Rectangle2D.Double((this.explosion.x - (width / 2)), 
                                                            (this.explosion.y - (height / 2)), 
                                                            width, height);
        return rect.intersects(expRect);
    }
    
    @Override
    public void update(Player player, List<Zombie> zombies) {
        // If the zombie has been shot and killed, set exploded to true.
        if(this.getHealth() <= 0 && !this.exploded) {
            this.exploded = true;
            this.explosion = new Explosion(Images.POISON_GAS_SHEET, new Point2D.Double(this.x, this.y));
            Sounds.POISONCLOUD.play();
        }
        if(this.exploded) { // Otherwise...
            // Update the explosion animation and damage the player if he is touching it.
            if(collidesWithExplosion(player) && !this.damageDone) {
                if(!player.hasEffect(Invulnerability.EFFECT_NAME)) {
                    player.addStatusEffect(6, "Poison", Images.POISON_STATUS_ICON, 
                            (15 * 1000), PoisonFogZombie.POISON_DAMAGE);
                    this.damageDone = true;
                }
            }
            if(this.explosion.getImage().isActive()) this.explosion.getImage().update();
        }
    }
    
    @Override
    public void draw(Graphics2D g2d) {
        if(!this.exploded) {
            // If the zombie has not been shot yet, continue drawing the zombie.
            super.draw(g2d);
        } else {
            // Otherwise, draw the explosion.
            if(this.explosion.getImage().isActive()) this.explosion.draw(g2d);
        }
    }
    
    @Override
    public void moan(Player player) {
        if(!this.moaned) {
            if(Globals.gameTime.getElapsedMillis() >= this.nextMoan) {
                double xD = player.getCenterX() - this.x;
                double yD = player.getCenterY() - this.y;
                double dist = Math.sqrt((xD * xD) + (yD * yD));
                double gain = 1.0 - (dist / Player.AUDIO_RANGE);
                Sounds.MOAN4.play(gain);
                this.moaned = true;
            }
        }
    }
}
 public class StitchesBoss extends Zombie {
    // Final Variables
    private static final int EXP_VALUE = 10000;
    private static final int COOL_TIME = 5000 / (int)Globals.SLEEP_TIME;
    private static final double ATTACK_DISTANCE = 364.0;
    private static final double POISON_RANGE = 114.0;
    private static final int POISON_DAMAGE = 10;
    private static final long HOOK_DURATION = 10000;
    
    // Member Variables
    private List<Particle> particles;
    @Override
    public List<Particle> getParticles() { return this.particles; }
    private int cooldown;
    public boolean canFire() { return (this.cooldown == 0 && this.particles.isEmpty()); }
    private boolean hooked;
    private long hookEndTime;
    
    public StitchesBoss(Point2D.Double p_, int health_, int damage_, double speed_, int score_, Animation animation_) {
        super(p_, Globals.ZOMBIE_BOSS_STITCHES_TYPE, health_, damage_, speed_, score_, StitchesBoss.EXP_VALUE, animation_);
        this.particles = new ArrayList<Particle>();
        this.cooldown = StitchesBoss.COOL_TIME;
        this.hooked = false;
        this.hookEndTime = 0;
    }
    
    public void drawParticles(Graphics2D g2d) {
        synchronized(this.particles) {
            Iterator<Particle> it = this.particles.iterator();
            while(it.hasNext()) {
                Particle p = it.next();
                if(p.isAlive() || this.hooked) {
                    Stroke s = g2d.getStroke();
                    g2d.setColor(Color.BLACK);
                    g2d.setStroke(new BasicStroke(2.0f));
                    g2d.drawLine((int)(this.x), (int)(this.y), (int)p.getPos().x, (int)p.getPos().y);
                    g2d.setStroke(s);
                    p.draw(g2d);
                }
            }
        }
    }
    
    @Override
    public void update(Player player, List<Zombie> zombies) {
        Point2D.Double playerPos = new Point2D.Double(player.getCenterX(), player.getCenterY());
        Point2D.Double myPos = new Point2D.Double(this.x, this.y);
        
        // Update the hook.
        if(!this.particles.isEmpty()) {
            synchronized(this.particles) {
                Iterator<Particle> it = this.particles.iterator();
                while(it.hasNext()) {
                    Particle p = it.next();
                    // If the hook hasn't attached to something yet...
                    if(!this.hooked) p.update();
                    else p.setPos(new Point2D.Double(player.getCenterX(), player.getCenterY())); // Otherwise, move it to the player...
                    
                    if(this.hooked) { // Update the hook's theta.
                        double theta = Math.atan2((player.y - p.getPos().y), (player.x - p.getPos().x)) - Math.PI;
                        p.setTheta(theta);
                    } // End hook's theta update.
                    
                    // If not hooked and the hook is not alive or is out of bounds... remove it.
                    if(!this.hooked && (!p.isAlive() || p.outOfBounds())) {
                        it.remove();
                        continue;
                    }
                    
                    // If not hooked and there's a collision between a hook and a player...
                    if(!this.hooked && p.checkCollision(player)) {
                        // If the hook hits the player...
                        this.hooked = true;
                        this.hookEndTime = Globals.gameTime.getElapsedMillis() + StitchesBoss.HOOK_DURATION;
                        p.setLife((int)(StitchesBoss.HOOK_DURATION / Globals.SLEEP_TIME));
                    }
                    
                    // If there IS a hook attached to the player...
                    if(this.hooked) {
                        // Will detach the hook if the player is invulnerable or has recently respawned.
                        if(!player.hasEffect(Invulnerability.EFFECT_NAME)) {
                            if(Globals.gameTime.getElapsedMillis() >= this.hookEndTime) {
                                // Detach the hook.
                                this.hooked = false;
                                this.hookEndTime = 0;
                                it.remove();
                                continue;
                            } else {
                                // Reel in the player.
                                double theta = Math.atan2((this.y - player.y), (this.x - player.x));
                                Point2D.Double newPos = new Point2D.Double(player.x, player.y);
                                newPos.x += (player.getSpeed() + 0.2) * Math.cos(theta);
                                newPos.y += (player.getSpeed() + 0.2) * Math.sin(theta);
                                player.move(newPos);
                            }
                        } else {
                            // Detach the hook.
                            this.hooked = false;
                            this.hookEndTime = 0;
                            it.remove();
                            continue;
                        }
                    }
                }
            }
        } else { // If there are no active hooks...
            // If Stitches can throw a hook, throw it.
            if(this.canFire()) {
                if(inRange(playerPos, myPos, StitchesBoss.ATTACK_DISTANCE)) {
                    this.throwHook(playerPos);
                    this.cooldown = StitchesBoss.COOL_TIME;
                }
            } else this.cooldown--;
        }
        // If the player is within range of Stitches, poison him...
        if(inRange(playerPos, myPos, StitchesBoss.POISON_RANGE)) {
            player.addStatusEffect(6, "Poison", Images.POISON_STATUS_ICON, 
                                   1000, StitchesBoss.POISON_DAMAGE);
        }
    }
    
    @Override
    public void draw(Graphics2D g2d) {
        AffineTransform saved = g2d.getTransform();
        if(!this.particles.isEmpty()) drawParticles(g2d);
        super.draw(g2d);
        g2d.setTransform(saved);
    }
    
    @Override
    public void moan(Player player) {
        if(!this.moaned) {
            if(Globals.gameTime.getElapsedMillis() >= this.nextMoan) {
                double xD = player.getCenterX() - this.x;
                double yD = player.getCenterY() - this.y;
                double dist = Math.sqrt((xD * xD) + (yD * yD));
                double gain = 1.0 - (dist / Player.AUDIO_RANGE);
                Sounds.MOAN6.play(gain);
                this.moaned = true;
            }
        }
    }
    
    private void throwHook(Point2D.Double playerPos) {
        double theta = Math.atan2((playerPos.y - this.y), (playerPos.x - this.x)) - Math.PI;
        
        Particle p = new Particle(theta, 0.0, 6.0, 1, new Point2D.Double(this.x, this.y), 
                                  new Dimension(16, 16), Images.STITCHES_HOOK) {
            @Override
            public void update() {
              if(this.isAlive()) {
                  // Update the position.
                  this.pos.x += -(this.speed * Math.cos(this.theta));
                  this.pos.y += -(this.speed * Math.sin(this.theta));
              }
            }
            @Override
            public boolean checkCollision(Rectangle2D.Double rect) {
                if(rect.contains(this.pos)) {
                    return true;
                } else return false;
            }
        };
        this.particles.add(p);
    }
    
    private boolean inRange(Point2D.Double playerPos, Point2D.Double myPos, double distance) {
        double xD = playerPos.x - myPos.x;
        double yD = playerPos.y - myPos.y;
        return Math.sqrt((xD * xD) + (yD * yD)) <= distance;
    }
}
 public class ZombatBoss extends Zombie {
    // Final Variables
    private static final int EXP_VALUE = 2500;
    private static final double ATTACK_DISTANCE = 166.0;
    private static final int DRAIN_COOLDOWN = 2000 / (int)Globals.SLEEP_TIME;
    private static final int DRAIN_AMOUNT = 5;
    
    // Member Variables
    private double maxHealth;
    private int cooldown;
    public boolean canFire() { return this.cooldown == 0; }
    
    public ZombatBoss(Point2D.Double p_, int health_, int damage_, double speed_, int score_, Animation animation_) {
        super(p_, Globals.ZOMBIE_BOSS_ZOMBAT_TYPE, health_, damage_, speed_, score_, ZombatBoss.EXP_VALUE, animation_);
        this.maxHealth = health_;
        this.cooldown = ZombatBoss.DRAIN_COOLDOWN;
    }
    
    @Override
    public void update(Player player, List<Zombie> zombies) {
        // If the Zombat's attack is cooled down and he is within range, drain the player.
        if(canFire()) {
            Point2D.Double playerPos = new Point2D.Double(player.getCenterX(), player.getCenterY());
            Point2D.Double myPos = new Point2D.Double(this.x, this.y);
            if(inRange(playerPos, myPos)) {
                // Drain the player's health, healing the Zombat.
                this.drain(player);
                this.cooldown = ZombatBoss.DRAIN_COOLDOWN;
            }
        } else this.cooldown--;
    }
    
    private void drain(Player player) {
        if(!player.hasEffect(Invulnerability.EFFECT_NAME)) {
            player.takeDamage(ZombatBoss.DRAIN_AMOUNT);
            this.health += ZombatBoss.DRAIN_AMOUNT;
            if(this.health > this.maxHealth) this.health = this.maxHealth;
        }
    }
    
    @Override
    public void moan(Player player) {
        if(!this.moaned) {
            if(Globals.gameTime.getElapsedMillis() >= this.nextMoan) {
                double xD = player.getCenterX() - this.x;
                double yD = player.getCenterY() - this.y;
                double dist = Math.sqrt((xD * xD) + (yD * yD));
                double gain = 1.0 - (dist / Player.AUDIO_RANGE);
                Sounds.MOAN8.play(gain);
                this.moaned = true;
            }
        }
    }
    
    private boolean inRange(Point2D.Double playerPos, Point2D.Double myPos) {
        double xD = playerPos.x - myPos.x;
        double yD = playerPos.y - myPos.y;
        return Math.sqrt((xD * xD) + (yD * yD)) <= ZombatBoss.ATTACK_DISTANCE;
    }
}
 public class Zombie extends Point2D.Double {
    // Member variables.
    private AffineTransform af;
    private Animation img;
    
    private int type;
    protected double health; // How much health the zombie has.
    private int damage; // How much damage the zombie does per tick to the player.
    private double speed; // How fast the zombie moves.
    private int cashValue; // How many points the zombie is worth.
    private int experience; // How many experience points the zombie is worth.
    
    protected long nextMoan;
    protected boolean moaned;
    
    public Zombie(Point2D.Double p_, int type_,  int health_, int damage_, double speed_, int cash_, int exp_, Animation animation_) {
        super(p_.x, p_.y);
        this.af = new AffineTransform();
        this.img = animation_;
        
        this.type = type_;
        this.health = health_;
        this.damage = damage_;
        this.speed = speed_;
        this.cashValue = cash_;
        this.experience = exp_;
        
        this.nextMoan = Globals.gameTime.getElapsedMillis() + ((Globals.r.nextInt(7) + 6) * 1000);
        this.moaned = false;
    }
    
    // Getter/Setter methods.
    public void set(int id, long value) {
        // To be overridden.
    }
    public int getType() { return this.type; }
    public double getHealth() { return this.health; }
    public void takeDamage(int damage_) { this.health -= damage_; }
    public int getDamage() { return this.damage; }
    public int getCashValue() { return this.cashValue; }
    public int getExpValue() { return this.experience; }
    public AffineTransform getTransform() { return this.af; }
    public Animation getImage() { return this.img; }
    public Rectangle2D.Double getRect() {
        double width = this.getImage().getWidth();
        double height = this.getImage().getHeight();
        return new Rectangle2D.Double((this.x - (width / 2)), (this.y - (height / 2)), width, height); 
    }
    
    public boolean isDead() { return this.health <= 0; }
    
    // Shape manipulation.
    public void rotate(double theta_) { this.af.setToRotation(theta_, this.x, this.y); }
    
    public void move(double theta_) {
        this.x += this.speed * Math.cos(theta_ - (Math.PI / 2));
        this.y += this.speed * Math.sin(theta_ - (Math.PI / 2));
        this.img.move((int)this.x, (int)this.y);
    }
    
    public void update(Player player, List<Zombie> zombies) {
        // To be overridden.
    }
    
    public void draw(Graphics2D g2d) {
        // Can be overridden.
        g2d.setTransform(this.af);
        { // Draw zombie's shadow.
            int width = this.img.getWidth();
            int height = this.img.getHeight();
            if(this.type == Globals.ZOMBIE_REGULAR_TYPE) height /= 2;
            else if(this.type == Globals.ZOMBIE_DOG_TYPE) width /= 2;
            else {
                width /= 2;
                height /= 2;
            }
            int xPos = (int)(this.x - (width / 2));
            int yPos = (int)(this.y - (height / 2));
            g2d.setColor(new Color(0, 0, 0, 100));
            g2d.fillOval(xPos, yPos, width, height);
        } // End drawing zombie's shadow.
        this.img.draw((Graphics2D)g2d);
    }
    
    public void moan(Player player) {
        // To be overridden.
        if(!this.moaned) {
            boolean regular = this.type == Globals.ZOMBIE_REGULAR_TYPE;
            boolean dog = this.type == Globals.ZOMBIE_DOG_TYPE;
            boolean tiny = this.type == Globals.ZOMBIE_TINY_TYPE;
            if(regular || dog || tiny) {
                if(Globals.gameTime.getElapsedMillis() >= this.nextMoan) {
                    double xD = player.getCenterX() - this.x;
                    double yD = player.getCenterY() - this.y;
                    double dist = Math.sqrt((xD * xD) + (yD * yD));
                    double gain = 1.0 - (dist / Player.AUDIO_RANGE);
                    if(regular || tiny) Sounds.MOAN1.play(gain);
                    else if(dog) Sounds.MOAN2.play(gain);
                    this.moaned = true;
                }
            }
        }
    }
    
    public List<Particle> getParticles() {
        // To be overridden.
        return null;
    }
    
    public int getParticleDamage() { 
        // To be overridden.
        return 0;
    }
}
 public class ZombieMatron extends Zombie {
    // Final Variables
    public static final int EXP_VALUE = 200;
    public static final long TIME_TO_BURST = 8000;
    private static final int MIN_ZOMBIES = 5;
    private static final int MAX_ZOMBIES = 10;
    private static final int MIN_RADIUS = 50;
    private static final int MAX_RADIUS = 100;
    private static final int EXPLOSION_DAMAGE = 50;
    // Member Variables
    private boolean burst;
    private boolean damageDone;
    private Explosion explosion;
    private long spawnTime;
    private long burstTime;
    
    public ZombieMatron(Point2D.Double p_, int health_, int damage_, double speed_, int score_, Animation animation_) {
        super(p_, Globals.ZOMBIE_MATRON_TYPE, health_, damage_, speed_, score_, ZombieMatron.EXP_VALUE, animation_);
        this.burst = false;
        this.damageDone = false;
        this.explosion = new Explosion(Images.BLOOD_SHEET, new Point2D.Double(this.x, this.y));
        this.spawnTime = Globals.gameTime.getElapsedMillis();
        this.burstTime = spawnTime + ZombieMatron.TIME_TO_BURST;
    }
    
    @Override
    public void set(int id, long value) {
        if(id == 0) this.burstTime = value;
    }
    
    @Override
    public boolean isDead() { return (this.getHealth() <= 0 || (this.burst && !this.explosion.getImage().isActive())); }
    
    private boolean collidesWithExplosion(Rectangle2D.Double rect) {
        // Checks to see if the player is intersecting the explosion.
        int width = this.explosion.getSize().width;
        int height = this.explosion.getSize().height;
        Rectangle2D.Double expRect = new Rectangle2D.Double((this.explosion.x - (width / 2)), 
                                                            (this.explosion.y - (height / 2)), 
                                                            width, height);
        return rect.intersects(expRect);
    }
    
    @Override
    public void update(Player player, List<Zombie> toAdd) {
        // If the Zombie Matron's timer has run out, the Matron bursts...
        if((Globals.gameTime.getElapsedMillis() >= burstTime) && !this.burst) {
            this.burst = true;
            this.explosion = new Explosion(Images.BLOOD_SHEET, new Point2D.Double(this.x, this.y));
            Sounds.EXPLOSION.play();
            // A certain number of mini zombies are added to a list, which are later added to the zombies list.
            int numOfZombies = Globals.r.nextInt((ZombieMatron.MAX_ZOMBIES - ZombieMatron.MIN_ZOMBIES) + 1) + ZombieMatron.MIN_ZOMBIES;
            for(int i = 0; i < numOfZombies; i++) {
                Point2D.Double p_ = new Point2D.Double(this.x, this.y);
                double theta_ = Globals.r.nextDouble() * (Math.PI * 2);
                int radius_ = Globals.r.nextInt((ZombieMatron.MAX_RADIUS - ZombieMatron.MIN_RADIUS) + 1) + ZombieMatron.MIN_RADIUS;
                p_.x += radius_ * Math.cos(theta_);
                p_.y += radius_ * Math.sin(theta_);
                Animation a_ = new Animation(Images.ZOMBIE_TINY, 20, 20, 2, (int)p_.x, (int)p_.y, 100, 0, true);
                Zombie z_ = new Zombie(p_, Globals.ZOMBIE_TINY_TYPE, 100, 1, 2, 50, (ZombieMatron.EXP_VALUE / numOfZombies), a_);
                toAdd.add(z_);
            }
        }
        // If the Matron has already burst...
        if(this.burst) {
            // And if the explosion has collided with the player, but no damage has yet been done to the player...
            if(collidesWithExplosion(player) && (!this.damageDone)) {
                // Deal damage to the player and set the damageDone flag to true so it can't damage again.
                player.takeDamage(ZombieMatron.EXPLOSION_DAMAGE);
                this.damageDone = true;
            }
            // If the blood burst animation is not yet finished, update it.
            if(this.explosion.getImage().isActive()) this.explosion.getImage().update();
        }
    }
    
    @Override
    public void draw(Graphics2D g2d) {
        // If the Matron has not yet burst, draw the Matron herself.
        if(!this.burst) super.draw(g2d);
        else {
            // Otherwise, if the explosion animation is still active, draw it.
            if(this.explosion.getImage().isActive()) this.explosion.draw(g2d);
        }
    }
    
    @Override
    public void moan(Player player) {
        if(!this.moaned) {
            if(Globals.gameTime.getElapsedMillis() >= this.nextMoan) {
                double xD = player.getCenterX() - this.x;
                double yD = player.getCenterY() - this.y;
                double dist = Math.sqrt((xD * xD) + (yD * yD));
                double gain = 1.0 - (dist / Player.AUDIO_RANGE);
                Sounds.MOAN5.play(gain);
                this.moaned = true;
            }
        }
    }
}
 public class Globals {
    // Game Information
    public static final String VERSION = "1.0";
    public static final int W_WIDTH = 800; // The width of the game window.
    public static final int W_HEIGHT = 640; // The height of the game window.
    public static final long SLEEP_TIME = 20; // The sleep time of the animation thread.
    public static final long WAVE_BREAK_TIME = 30 * 1000;
    public static final Random r = new Random();
    public static List<Message> GAME_MESSAGES = new ArrayList<Message>();
    
    // Zombie Information
    public static final int ZOMBIE_REGULAR_TYPE = 1;
    public static final long ZOMBIE_REGULAR_SPAWN = 1000;
    public static final int ZOMBIE_DOG_TYPE = 2;
    public static final long ZOMBIE_DOG_SPAWN = 10000;
    public static final int ZOMBIE_ACID_TYPE = 3;
    public static final long ZOMBIE_ACID_SPAWN = 30000;
    public static final int ZOMBIE_POISONFOG_TYPE = 4;
    public static final long ZOMBIE_POISONFOG_SPAWN = 20000;
    public static final int ZOMBIE_MATRON_TYPE = 5;
    public static final long ZOMBIE_MATRON_SPAWN = 50000;
    public static final int ZOMBIE_TINY_TYPE = 6;
    
    // Boss Information
    public static final int ZOMBIE_BOSS_ABERRATION_TYPE = 7;
    public static final int ZOMBIE_BOSS_ZOMBAT_TYPE = 8;
    public static final int ZOMBIE_BOSS_STITCHES_TYPE = 9;
    
    // Game-State Related
    public static Runnable animation; // The primary animation thread.
    public static GameTime gameTime; // Used to keep track of the time.
    
    public static boolean running; // Whether or not the game is currently running.
    public static boolean started;
    public static boolean crashed; // Tells the game whether or not there was a crash.
    public static boolean paused;
    public static boolean storeOpen;
    public static boolean levelScreenOpen;
    public static boolean deathScreen;
    public static boolean waveInProgress; // Whether the player is fighting or waiting for another wave.
    public static long nextWave;
    
    // Input Related
    public static boolean [] keys; // The state of the game key controls.
    public static boolean [] buttons; // The state of the game mouse button controls.
    public static Point mousePos; // The current position of the mouse on the screen.
    
    // Static Weapons
    public static Handgun HANDGUN = new Handgun();
    public static AssaultRifle ASSAULT_RIFLE = new AssaultRifle();
    public static Shotgun SHOTGUN = new Shotgun();
    public static Flamethrower FLAMETHROWER = new Flamethrower();
    public static Grenade GRENADE = new Grenade();
    public static Landmine LANDMINE = new Landmine();
    public static Flare FLARE = new Flare();
    public static LaserWire LASERWIRE = new LaserWire();
    public static TurretWeapon TURRETWEAPON = new TurretWeapon();
    public static Teleporter TELEPORTER = new Teleporter();
    
    public static Weapon getWeaponByName(String name) {
        if(name.equals(Globals.HANDGUN.getName())) return Globals.HANDGUN;
        else if(name.equals(Globals.ASSAULT_RIFLE.getName())) return Globals.ASSAULT_RIFLE;
        else if(name.equals(Globals.SHOTGUN.getName())) return Globals.SHOTGUN;
        else if(name.equals(Globals.FLAMETHROWER.getName())) return Globals.FLAMETHROWER;
        else if(name.equals(Globals.GRENADE.getName())) return Globals.GRENADE;
        else if(name.equals(Globals.LANDMINE.getName())) return Globals.LANDMINE;
        else if(name.equals(Globals.FLARE.getName())) return Globals.FLARE;
        else if(name.equals(Globals.LASERWIRE.getName())) return Globals.LASERWIRE;
        else if(name.equals(Globals.TURRETWEAPON.getName())) return Globals.TURRETWEAPON;
        else if(name.equals(Globals.TELEPORTER.getName())) return Globals.TELEPORTER;
        else return null;
    }
    
    public static void resetWeapons() {
        Globals.HANDGUN.resetAmmo();
        Globals.ASSAULT_RIFLE.resetAmmo();
        Globals.SHOTGUN.resetAmmo();
        Globals.FLAMETHROWER.resetAmmo();
        Globals.GRENADE.resetAmmo();
        Globals.LANDMINE.resetAmmo();
        Globals.FLARE.resetAmmo();
        Globals.LASERWIRE.resetAmmo();
        Globals.TURRETWEAPON.resetAmmo();
        Globals.TELEPORTER.resetAmmo();
    }
} public class Images {
    // Background Images
    public static final BufferedImage START_SCREEN = GZSFramework.loadImage("/resources/images/GZS_Splash.png");
    public static final BufferedImage BACKGROUND = GZSFramework.loadImage("/resources/images/GZS_Background6.png");
    public static final BufferedImage DEATH_SCREEN = GZSFramework.loadImage("/resources/images/GZS_DeathScreen.png");
    
    // Player-Related
    public static final BufferedImage PLAYER = GZSFramework.loadImage("/resources/images/GZS_Player.png");
    public static final BufferedImage CROSSHAIR = GZSFramework.loadImage("/resources/images/GZS_Crosshair.png");
    
    // Zombie-Related
    public static final BufferedImage ZOMBIE_REGULAR = GZSFramework.loadImage("/resources/images/GZS_Zumby2.png");
    public static final BufferedImage ZOMBIE_DOG = GZSFramework.loadImage("/resources/images/GZS_Rotdog2.png");
    public static final BufferedImage ZOMBIE_ACID = GZSFramework.loadImage("/resources/images/GZS_Upchuck2.png");
    public static final BufferedImage ZOMBIE_POISONFOG = GZSFramework.loadImage("/resources/images/GZS_Gasbag2.png");
    public static final BufferedImage ZOMBIE_MATRON = GZSFramework.loadImage("/resources/images/GZS_BigMama2.png");
    public static final BufferedImage ZOMBIE_TINY = GZSFramework.loadImage("/resources/images/GZS_TinyZumby.png");
    public static final BufferedImage BOSS_ABERRATION = GZSFramework.loadImage("/resources/images/GZS_Aberration2.png");
    public static final BufferedImage BOSS_ZOMBAT = GZSFramework.loadImage("/resources/images/GZS_Zombat.png");
    public static final BufferedImage BOSS_STITCHES = GZSFramework.loadImage("/resources/images/GZS_Stitches.png");
    
    public static final BufferedImage ACID_PARTICLE = GZSFramework.loadImage("/resources/images/GZS_AcidParticle.png");
    public static final BufferedImage STITCHES_HOOK = GZSFramework.loadImage("/resources/images/GZS_Hook.png");
    public static final BufferedImage POISON_GAS_SHEET = GZSFramework.loadImage("/resources/images/GZS_PoisonExplosion.png");
    public static final BufferedImage POISON_STATUS_ICON = GZSFramework.loadImage("/resources/images/GZS_PoisonIcon.png");
    public static final BufferedImage BLOOD_SHEET = GZSFramework.loadImage("/resources/images/GZS_BloodExplosion.png");
    
    // Power-Up Related
    public static final BufferedImage HEALTH_PACK = GZSFramework.loadImage("/resources/images/GZS_Health.png");
    public static final BufferedImage AMMO_PACK = GZSFramework.loadImage("/resources/images/GZS_Ammo.png");
    public static final BufferedImage SPEED_UP = GZSFramework.loadImage("/resources/images/GZS_SpeedUp.png");
    public static final BufferedImage UNLIMITED_AMMO = GZSFramework.loadImage("/resources/images/GZS_UnlimitedAmmo.png");
    public static final BufferedImage EXTRA_LIFE = GZSFramework.loadImage("/resources/images/GZS_ExtraLife.png");
    public static final BufferedImage EXP_MULTIPLIER = GZSFramework.loadImage("/resources/images/GZS_ExpMultiplier.png");
    public static final BufferedImage INVULNERABILITY = GZSFramework.loadImage("/resources/images/GZS_Invulnerability.png");
    public static final BufferedImage NIGHT_VISION = GZSFramework.loadImage("/resources/images/GZS_NightVision.png");
    
    // Ammo-Related
    public static final BufferedImage POPGUN_BULLET = GZSFramework.loadImage("/resources/images/GZS_Bullet.png");
    public static final BufferedImage RTPS_BULLET = GZSFramework.loadImage("/resources/images/GZS_Bullet2.png");
    public static final BufferedImage FIRE_PARTICLE = GZSFramework.loadImage("/resources/images/GZS_FireParticle2.png");
    public static final BufferedImage GRENADE_PARTICLE = GZSFramework.loadImage("/resources/images/GZS_HandEggParticle.png");
    public static final BufferedImage LANDMINE_PARTICLE = GZSFramework.loadImage("/resources/images/GZS_FlipFlopParticle.png");
    public static final BufferedImage FLARE_PARTICLE = GZSFramework.loadImage("/resources/images/GZS_FlareParticle.png");
    public static final BufferedImage LASER_TERMINAL = GZSFramework.loadImage("/resources/images/GZS_LaserTerminal.png");
    public static final BufferedImage EXPLOSION_SHEET = GZSFramework.loadImage("/resources/images/GZS_Explosion.png");
}
 public class ExpMultiplier extends Item {
    // Final Variables
    public static final int ID = 6;
    public static final String EFFECT_NAME = "Exp Multiplier";
    public static final long ITEM_DURATION = 15 * 1000;
    public static final long DURATION = 20 * 1000;
    
    public ExpMultiplier(Point2D.Double p) {
        super(ExpMultiplier.ID, ExpMultiplier.EFFECT_NAME, Images.EXP_MULTIPLIER, ExpMultiplier.DURATION);
        this.x = p.x;
        this.y = p.y;
    }
    
    @Override
    public void applyEffect(Player p) {
        p.addStatusEffect(ExpMultiplier.ID, ExpMultiplier.EFFECT_NAME, Images.EXP_MULTIPLIER, ExpMultiplier.DURATION, 0);
        synchronized(Globals.GAME_MESSAGES) { Globals.GAME_MESSAGES.add(new Message("Player experience doubled for " + (ExpMultiplier.DURATION / 1000) +" seconds!", 5000)); }
        Sounds.POWERUP.play();
    }
}
 public class ExtraLife extends Item {
    // Final Variables
    public static final int ID = 5;
    public static final String EFFECT_NAME = "Extra Life";
    public static final long DURATION = 15 * 1000;
    
    public ExtraLife(Point2D.Double p) {
        super(ExtraLife.ID, ExtraLife.EFFECT_NAME, Images.EXTRA_LIFE, ExtraLife.DURATION);
        this.x = p.x;
        this.y = p.y;
    }
    
    @Override
    public void applyEffect(Player p) {
        p.addLife();
        synchronized(Globals.GAME_MESSAGES) {
            Globals.GAME_MESSAGES.add(new Message("Played collected an extra life!", 5000));
        }
        Sounds.POWERUP.play();
    }
}
 public class Invulnerability extends Item {
    // Final Variables
    public static final int ID = 7;
    public static final String EFFECT_NAME = "Invulnerability";
    public static final long ITEM_DURATION = 15 * 1000;
    public static final long DURATION = 10 * 1000;
    
    public Invulnerability(Point2D.Double p) {
        super(Invulnerability.ID, Invulnerability.EFFECT_NAME, Images.INVULNERABILITY, Invulnerability.DURATION);
        this.x = p.x;
        this.y = p.y;
    }
    
    @Override
    public void applyEffect(Player p) {
        p.addStatusEffect(Invulnerability.ID, this.getName(), Images.INVULNERABILITY, Invulnerability.DURATION, 0);
        synchronized(Globals.GAME_MESSAGES) {
            Globals.GAME_MESSAGES.add(new Message(("Player has invulnerability for " + (Invulnerability.DURATION / 1000) + " seconds!"), 5000));
        }
        Sounds.POWERUP.play();
    }
}
 public class NightVision extends Item {
    // Final Variables
    public static final int ID = 6;
    public static final String EFFECT_NAME = "Night Vision";
    public static final long ITEM_DURATION = 15 * 1000;
    public static final long DURATION = 30 * 1000;
    
    public NightVision(Point2D.Double p) {
        super(NightVision.ID, NightVision.EFFECT_NAME, Images.NIGHT_VISION, NightVision.DURATION);
        this.x = p.x;
        this.y = p.y;
    }
    
    @Override
    public void applyEffect(Player p) {
        p.addStatusEffect(NightVision.ID, NightVision.EFFECT_NAME, Images.NIGHT_VISION, NightVision.DURATION, 0);
        synchronized(Globals.GAME_MESSAGES) { Globals.GAME_MESSAGES.add(new Message("Player has night vision for " + (NightVision.DURATION / 1000) +" seconds!", 5000)); }
        Sounds.POWERUP.play();
    }
}
 public class SpeedUp extends Item {
    // Final Variables
    public static final int ID = 3;
    public static final String EFFECT_NAME = "SpeedUp";
    public static final long ITEM_DURATION = 15 * 1000;
    public static final long DURATION = 10 * 1000;
    public static final int SPEED_MULTIPLIER = 2;
    
    public SpeedUp(Point2D.Double p) {
        super(SpeedUp.ID, SpeedUp.EFFECT_NAME, Images.SPEED_UP, SpeedUp.DURATION);
        this.x = p.x;
        this.y = p.y;
    }
    
    @Override
    public void applyEffect(Player p) {
        p.addStatusEffect(SpeedUp.ID, this.getName(), Images.SPEED_UP, SpeedUp.DURATION, SpeedUp.SPEED_MULTIPLIER);
        synchronized(Globals.GAME_MESSAGES) {
            Globals.GAME_MESSAGES.add(new Message("Player speed doubled for " + (SpeedUp.DURATION / 1000) + " seconds!", 5000));
        }
        Sounds.POWERUP.play();
    }
}
 public class UnlimitedAmmo extends Item {
    // Final Variables
    public static final int ID = 4;
    public static final String EFFECT_NAME = "Unlimited Ammo";
    public static final long ITEM_DURATION = 15 * 1000;
    public static final long DURATION = 10 * 1000;
    
    public UnlimitedAmmo(Point2D.Double p) {
        super(UnlimitedAmmo.ID, UnlimitedAmmo.EFFECT_NAME, Images.UNLIMITED_AMMO, UnlimitedAmmo.DURATION);
        this.x = p.x;
        this.y = p.y;
    }
    
    @Override
    public void applyEffect(Player p) {
        p.addStatusEffect(UnlimitedAmmo.ID, this.getName(), Images.UNLIMITED_AMMO, UnlimitedAmmo.DURATION, 0);
        synchronized(Globals.GAME_MESSAGES) {
            Globals.GAME_MESSAGES.add(new Message(("Player has unlimited ammo for " + (UnlimitedAmmo.DURATION / 1000) + " seconds!"), 5000));
        }
        Sounds.POWERUP.play();
    }
}
 public class Weapon {
    private String name;
    public String getName() { return this.name; }
    private int key;
    public int getKey() { return this.key; }
    private BufferedImage image;
    public BufferedImage getImage() { return this.image; }
    
    protected int ammoLeft;
    public int getAmmoLeft() { return this.ammoLeft; }
    private int maxAmmo;
    public int getMaxAmmo() { return this.maxAmmo; }
    private int ammoPerUse;
    
    public int getWeaponPrice() { return 0; }
    public int getAmmoPrice() { return 0; }
    
    private boolean automatic; // Indicates if the weapon can be fired continuously.
    public boolean isAutomatic() { return this.automatic; }
    protected boolean fired; // Used with automatic to determine if the weapon needs to be fired again.
    public boolean hasFired() { return this.fired; }
    public void resetFire() { this.fired = false; }
    private int cooldown;
    private int coolPeriod;
    public double getCooldownPercentage() { return ((double)cooldown / (double)coolPeriod); }
    public void resetCooldown() { this.cooldown = this.coolPeriod; }
    public void cool() { if(this.cooldown > 0) this.cooldown--; }
    
    public boolean canFire() {
        boolean a = this.ammoLeft >= this.ammoPerUse;
        boolean b = this.cooldown == 0;
        boolean c = this.automatic || (!this.automatic && !this.fired);
        //System.out.println("Enough Ammo? " + a + ", Cooled? " + b + ", Automatic or Manual and Cool? " + c);
        return (a) && (b) && (c); 
    }
    public boolean ammoFull() { return this.ammoLeft == this.maxAmmo; }
    public void addAmmo(int amount) { 
        if((this.ammoLeft + amount) > this.maxAmmo) this.ammoLeft = this.maxAmmo;
        else this.ammoLeft += amount;
    }
    public int getAmmoPackAmount() {
        // To be overridden...
        return 0;
    }
    public void resetAmmo() {
        synchronized(this.particles) { this.particles.clear(); }
    }
    
    protected List<Particle> particles;
    public List<Particle> getParticles() { return this.particles; }
    
    public List<LightSource> getLights() { return null; }
    
    public Weapon(String name, int key, String filename, int ammoLeft, int maxAmmo, int ammoPerUse, int cooldown, boolean automatic) {
        this.name = name;
        this.key = key;
        
        this.image = GZSFramework.loadImage(filename);
        
        this.ammoLeft = ammoLeft;
        this.maxAmmo = maxAmmo;
        this.ammoPerUse = ammoPerUse;
        
        this.automatic = automatic;
        this.fired = false;
        this.cooldown = cooldown;
        this.coolPeriod = cooldown;
        
        this.particles = Collections.synchronizedList(new ArrayList<Particle>());
    }
    
    public void updateWeapon(List<Zombie> zombies) {
        // To be overridden.
    }
    
    public void drawAmmo(Graphics2D g2d) {
        // To be overridden.
    }
    
    public void fire(double theta, Point2D.Double pos, Player player) {
        // To be overridden.
    }
    
    public int checkForDamage(Rectangle2D.Double rect) {
        // To be overridden.
        return 0;
    }
    
    public void consumeAmmo() {
        this.ammoLeft -= this.ammoPerUse;
    }
}

import genericzombieshooter.actors.AberrationBoss;
import genericzombieshooter.actors.AcidZombie;
import genericzombieshooter.actors.Player;
import genericzombieshooter.actors.PoisonFogZombie;
import genericzombieshooter.actors.StitchesBoss;
import genericzombieshooter.actors.ZombatBoss;
import genericzombieshooter.actors.Zombie;
import genericzombieshooter.actors.ZombieMatron;
import genericzombieshooter.misc.Globals;
import genericzombieshooter.misc.Images;
import genericzombieshooter.structures.items.ExpMultiplier;
import genericzombieshooter.structures.items.ExtraLife;
import genericzombieshooter.structures.items.Invulnerability;
import genericzombieshooter.structures.items.NightVision;
import genericzombieshooter.structures.items.SpeedUp;
import genericzombieshooter.structures.items.UnlimitedAmmo;
import genericzombieshooter.structures.weapons.Weapon;
import java.awt.Graphics2D;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

package genericzombieshooter.structures;


/**
 * Represents all the zombies in a wave of combat. Used to handle all zombies on screen.
 * @author Darin Beaudreau
 */
public class ZombieWave {
    // Final Variables
    private static final long ZOMBIE_SPAWN_TIME = 1000;
    private static final int ZOMBIES_PER_WAVE = 3;
    
    // Member Variables
    private int waveNumber;
    private boolean bossWave;
    private List<Zombie> zombiesUnborn;
    private List<Zombie> zombiesAlive;
    private List<Zombie> zombiesToDie;
    private long nextZombieSpawn;
    public int getWaveNumber() { return this.waveNumber; }
    public List<Zombie> getUnbornZombies() { return this.zombiesUnborn; }
    public List<Zombie> getZombies() { return this.zombiesAlive; }
    public boolean waveFinished() { return (this.zombiesAlive.isEmpty() && this.zombiesUnborn.isEmpty()); }
    
    public ZombieWave(int wave) {
        this.waveNumber = wave;
        this.bossWave = ((wave % 15) == 0);
        this.zombiesUnborn = constructWave(wave);
        this.zombiesAlive = new ArrayList<Zombie>();
        this.zombiesToDie = new ArrayList<Zombie>();
        this.nextZombieSpawn = Globals.gameTime.getElapsedMillis() + ZombieWave.ZOMBIE_SPAWN_TIME;
    }
    
    private List<Zombie> constructWave(int currentWave) {
        List<Zombie> wave = new ArrayList<Zombie>();
        
        if(!this.bossWave) {
            int difficulty = 2;
            if(currentWave >= 5) difficulty++;
            if(currentWave >= 10) difficulty++;
            if(currentWave >= 20) difficulty++;

            int enemyCount = currentWave * ZombieWave.ZOMBIES_PER_WAVE;
            int specialsThisWave = (int)(enemyCount / 4);
            int specialsSpawned = 0;
            for(int i = 0; i < enemyCount; i++) {
                // Decide which side of the screen to spawn the zombie on.
                double x = 0;
                double y = 0;

                int spawnSide = Globals.r.nextInt(4) + 1;
                if(spawnSide == 1) x = Globals.r.nextInt((Globals.W_WIDTH - 40) + 1);
                else if(spawnSide == 2) {
                    x = Globals.W_WIDTH - 40;
                    y = Globals.r.nextInt((Globals.W_HEIGHT - 40) + 1);
                } else if(spawnSide == 3) {
                    x = Globals.r.nextInt((Globals.W_WIDTH - 40) + 1);
                    y = Globals.W_HEIGHT - 40;
                } else if(spawnSide == 4) y = Globals.r.nextInt((Globals.W_HEIGHT - 40) + 1);

                Point2D.Double p_ = new Point2D.Double(x, y);

                if(specialsSpawned >= specialsThisWave) difficulty = 2;
                int zombieType = Globals.r.nextInt(difficulty) + 1;
                if(zombieType == Globals.ZOMBIE_REGULAR_TYPE) {
                    // Zumby
                    Animation a_ = new Animation(Images.ZOMBIE_REGULAR, 48, 48, 4, (int)p_.x, (int)p_.y, 200, 0, true);
                    Zombie z_ = new Zombie(p_, Globals.ZOMBIE_REGULAR_TYPE, 250, 1, 1, 25, 20, a_);
                    wave.add(z_);
                } else if(zombieType == Globals.ZOMBIE_DOG_TYPE) {
                    // Rotdog
                    Animation a_ = new Animation(Images.ZOMBIE_DOG, 48, 48, 4, (int)p_.x, (int)p_.y, 80, 0, true);
                    Zombie z_ = new Zombie(p_, Globals.ZOMBIE_DOG_TYPE, 100, 1, 2, 50, 30, a_);
                    wave.add(z_);
                } else if(zombieType == Globals.ZOMBIE_ACID_TYPE) {
                    // Upchuck
                    Animation a_ = new Animation(Images.ZOMBIE_ACID, 64, 64, 4, (int)p_.x, (int)p_.y, 200, 0, true);

                    AcidZombie z_ = new AcidZombie(p_, 300, 1, 1, 100, a_);
