text: 
  import _ from 'lodash';
  
  import { Spell, SpellType } from '../spell';
  
  import { ATTACK_STATS } from '../../../shared/stat-calculator';
  
  export class Attack extends Spell {
    static description = 'A simple attack that uses STR to deal damage.';
    static element = SpellType.PHYSICAL;
    static tiers = [
      { name: 'attack',  spellPower: 1.0, weight: 50, cost: 0, level: 1,   profession: 'Archer' },
      { name: 'attack',  spellPower: 1.2, weight: 30, cost: 0, level: 1,   profession: 'Barbarian' },
      { name: 'strike',  spellPower: 2.3, weight: 30, cost: 0, level: 50,  profession: 'Barbarian' },
      { name: 'attack',  spellPower: 1.0, weight: 40, cost: 0, level: 1,   profession: 'Bard' },
      { name: 'attack',  spellPower: 0.7, weight: 20, cost: 0, level: 1,   profession: 'Bitomancer' },
      { name: 'attack',  spellPower: 0.8, weight: 20, cost: 0, level: 1,   profession: 'Cleric' },
      { name: 'attack',  spellPower: 1.1, weight: 65, cost: 0, level: 1,   profession: 'Fighter' },
      { name: 'strike',  spellPower: 1.3, weight: 65, cost: 0, level: 50,  profession: 'Fighter' },
      { name: 'assault', spellPower: 1.7, weight: 65, cost: 0, level: 100, profession: 'Fighter',
        collectibles: ['Broken Katana'] },
      { name: 'attack',  spellPower: 1.0, weight: 30, cost: 0, level: 1,   profession: 'Generalist' },
      { name: 'attack',  spellPower: 1.0, weight: 50, cost: 0, level: 1,   profession: 'Jester' },
      { name: 'strike',  spellPower: 1.5, weight: 50, cost: 0, level: 90,  profession: 'Jester' },
      { name: 'attack',  spellPower: 0.5, weight: 50, cost: 0, level: 1,   profession: 'Lich' },
      { name: 'attack',  spellPower: 0.6, weight: 15, cost: 0, level: 1,   profession: 'Mage' },
      { name: 'attack',  spellPower: 0.9, weight: 20, cost: 0, level: 1,   profession: 'MagicalMonster' },
      { name: 'attack',  spellPower: 1.0, weight: 50, cost: 0, level: 1,   profession: 'Monster' },
      { name: 'attack',  spellPower: 0.5, weight: 20, cost: 0, level: 1,   profession: 'Necromancer' },
      { name: 'attack',  spellPower: 1.1, weight: 50, cost: 0, level: 1,   profession: 'Pirate' },
      { name: 'strike',  spellPower: 1.9, weight: 50, cost: 0, level: 75,  profession: 'Pirate' },
      { name: 'attack',  spellPower: 1.0, weight: 20, cost: 0, level: 1,   profession: 'Rogue' },
      { name: 'strike',  spellPower: 1.4, weight: 20, cost: 0, level: 25,  profession: 'Rogue' },
      { name: 'attack',  spellPower: 1.0, weight: 50, cost: 0, level: 1,   profession: 'SandwichArtist' }
    ];
  
    static shouldCast() {
      return this.$canTarget.yes();
    }
  
    calcDamage() {
      const min = (this.caster.liveStats.str / 3) * (this.caster.liveStats.silver > 0 ? 1.1 : 1);
      const max = (this.caster.liveStats.str * (this.caster.liveStats.power > 0 ? 1.1 : 1));
      return this.minMax(min, max) * this.spellPower;
    }
  
    determineTargets() {
      return this.$targetting.randomEnemy;
    }
  
    preCast() {
      let message = '%player attacked %targetName with %hisher %weaponName and dealt %damage damage!';
      const weaponName = _.get(this.caster.equipment, 'mainhand.fullname', 'claw');
      const targets = this.determineTargets();
  
      if(_.compact(targets).length === 0) {
        return;
      }
  
      _.each(targets, target => {
        let done = false;
        let connected = true;
        let damage = this.calcDamage();
        const messageData = {
          weaponName,
          damage
        };
  
        const critChance = Math.min(100, Math.max(0, 1 + this.caster.liveStats.crit + (this.caster.liveStats.vorpal ? 10 : 0)));
  
        if(Spell.chance.bool({ likelihood: critChance }) && target.liveStats.aegis <= 0) {
          this.caster.$battle.tryIncrement(this.caster, 'Combat.Give.CriticalHit');
          this.caster.$battle.tryIncrement(target, 'Combat.Receive.CriticalHit');
          damage = this.caster.liveStats.str * this.spellPower * (this.caster.liveStats.lethal > 0 ? 1.5 : 1);
          message = '%player CRITICALLY attacked %targetName with %hisher %weaponName and dealt %damage damage!';
          done = true;
        }
  
        const canDodge = Spell.chance.bool({ likelihood: 80 });
        const [dodge, overcomeDodge] = [-target.liveStats.dodge, this.caster.liveStats.overcomeDodge];
        const dodgeRoll = Spell.chance.integer({ min: dodge, max: Math.max(dodge+1, overcomeDodge) });
  
        if(!done && canDodge && dodgeRoll <= 0) {
          done = true;
          connected = false;
          this.caster.$battle.tryIncrement(this.caster, 'Combat.Give.Dodge');
          this.caster.$battle.tryIncrement(target, 'Combat.Receive.Dodge');
          message = '%player attacked %targetName with %hisher %weaponName, but %targetName dodged!';
          damage = 0;
        }
  
        const canAvoidHit = Spell.chance.bool({ likelihood: 80 });
        const [avoidHit, hit, deflect] = [-target.liveStats.avoidHit, this.caster.liveStats.hit, -target.liveStats.deflect];
        const hitRoll = Spell.chance.integer({ min: avoidHit, max: Math.max(avoidHit+1, hit) });
  
        if(!done && canAvoidHit && deflect <= hitRoll && hitRoll <= 0) {
          message = '%player attacked %targetName with %hisher %weaponName, but %player missed!';
          damage = 0;
          connected = false;
  
          if(hitRoll < deflect) {
            this.caster.$battle.tryIncrement(this.caster, 'Combat.Give.Deflect');
            this.caster.$battle.tryIncrement(target, 'Combat.Receive.Deflect');
            const deflectItem = _.get(_.sample(_.values(target.equipment)), 'fullname', 'claw');
            messageData.deflectItem = deflectItem;
            message = '%player attacked %targetName with %hisher %weaponName, but %targetName deflected it with %deflectItem!';
          } else {
            this.caster.$battle.tryIncrement(this.caster, 'Combat.Give.Miss');
            this.caster.$battle.tryIncrement(target, 'Combat.Receive.Miss');
          }
        }
  
        super.cast({
          damage,
          message,
          messageData,
          targets: [target]
        });
  
        if(!connected) return;
  
        _.each(ATTACK_STATS, stat => {
          const canUse = this.caster.liveStats[stat];
          if(canUse <= 0) return;
  
          const chance = Spell.chance.bool({ likelihood: Math.max(0, Math.min(100, canUse * 5)) });
          if(!chance) return;
  
          const properEffect = _.capitalize(stat);
  
          if(target.$effects.hasEffect(properEffect)) return;
  
          const effectProto = require(`../effects/${properEffect}`)[properEffect];
  
          const effect = new effectProto({ target, potency: canUse, duration: 0 });
          effect.origin = { name: this.caster.fullname, ref: this.caster, spell: stat };
          target.$effects.add(effect);
          effect.affect(target);
  
          this.caster.$battle.tryIncrement(this.caster, `Combat.Give.CombatEffect.${properEffect}`);
          this.caster.$battle.tryIncrement(target, `Combat.Receive.CombatEffect.${properEffect}`);
        });
      });
    }
  }
  