Dungeon monsters - I need data
Speeds of monsters in dungeons, if you know them or know how to get them, let me know on discord.
I found a bug/want a new feature
Join my discord server and tell me about it. If its a bug with the simulator please give me the exact scenario that got miscalculated. If you want a new feature, I am open to ideas but I dont promise anything.
What needs to be tested
Speed buff stacking with custom speed buffs (chilling S2)
Is speed rounded up during the battle or only before? (does 280.33 get rounded up to 281?)
Turn orders with multiple different custom buffs on different monsters (miles/misty/chilling have the same speed after buff)
Join my discord
If you need help or just want to talk, you can join the SWCALC Discord server.
Join Discord
Find logic errors in the code.
This is the code the app uses for the calculator, Can you spot any obvious mistakes in it? I could refactor is such that its not spaghetti code, but then again, don't fix what ain't broke

  simulate () {
    this.ticks = []
    // this will hold the monster values for the current tick
    let monsters = []
    for (let i = 0; i < this.allies.length; i++) {
      monsters.push(this.transformMonster(this.allies[i]))
    }
    for (let i = 0; i < this.enemies.length; i++) {
      monsters.push(this.transformMonster(this.enemies[i]))
    }
    // this determines the order of monsters when they have the same speed, passives don't come into play here
    // tested on miles
    monsters = monsters.sort((a, b) => {
      // leo goes before non leo.
      if (a.isLeo && !b.isLeo) return -1
      if (b.isLeo && !a.isLeo) return 1
      // if both or neither are leo, use normal order.
      return b.combat_speed - a.combat_speed
    })

    // this will just save the values for each tick
    this.ticks.push({
      tick: 0,
      monsters: cloneDeep(monsters)
    })
    // run the ticks
    for (let i = 1; i <= this.tickCount; i++) {
      const tickMonsters = this.runTick(this.ticks[i - 1].monsters)

      // run the tick and save the results
      this.ticks.push({
        tick: i,
        monsters: cloneDeep(tickMonsters)
      })
    }
    // The last tick does not have everything set, so we get rid of it
    this.ticks.pop()
  },
  calculateCombatSpeed (monster) {
    let speed = monster.base_speed * (100 + monster.tower_buff + monster.lead)/100 + monster.rune_speed
    // Chilling/Miles/Dongbaek
    speed = monster.flatSpeedBuffs.reduce((resultSpeed, flatSpeedBuff) => {
      switch (flatSpeedBuff.type) {
        case 'add':
          return resultSpeed + Number(flatSpeedBuff.flatSpeedBuffAmount)
        case 'add_percent':
          return resultSpeed + Number(flatSpeedBuff.flatSpeedBuffAmount)/100 * monster.base_speed
        case 'subtract':
          return resultSpeed -  Number(flatSpeedBuff.flatSpeedBuffAmount)
        case 'subtract_percent':
          return resultSpeed * ( 1 - Number(flatSpeedBuff.flatSpeedBuffAmount ) / 100)
      }
    }, speed)
    // correction for error introduced by SW always displaying speed whole number rounded up
    // for example chilling - base speed = 101 - so swift will show he gets 26 speed while its actually 25.25
    if (monster.isSwift && (monster.base_speed * 0.25) % 1 > 0) {
      speed = speed - (1 - ((monster.base_speed * 0.25) % 1))
    }
    if (monster.has_slow) {
      speed = speed * 0.7
    }
    if (monster.has_speed_buff) {
      speed = speed * (1 + 0.3 * (100 + monster.speedIncreasingEffect) / 100)
    }
    // As far as I know, speed is always a whole number rounded up.
    return Math.ceil(speed)
  },
  transformMonster (baseMonster) {
    // take the monster from the monster setup part and prepare it for battle
    let lead = 0
    if(baseMonster.isAlly) {
      lead = this.allyEffects.lead
      if (this.allyEffects.element && this.allyEffects.element !== baseMonster.element) {
        lead = 0
      }
    } else {
      lead = this.enemyEffects.lead
      if (this.enemyEffects.element && this.enemyEffects.element !== baseMonster.element) {
        lead = 0
      }
    }

    if((baseMonster.monster_family === 'Dragon Knight' && baseMonster.element === 'Wind') || baseMonster.name === 'Leo'){
      this.leoExists = true
    }
    const monster = {
      key: baseMonster.key,
      isLeo: (baseMonster.monster_family === 'Dragon Knight' && baseMonster.element === 'Wind') || baseMonster.name === 'Leo',
      isAlly: baseMonster.isAlly,
      combat_speed: 0,
      tower_buff: baseMonster.isAlly ? this.allyEffects.tower : this.enemyEffects.tower,
      lead,
      base_speed: baseMonster.base_speed,
      rune_speed: baseMonster.rune_speed,
      has_speed_buff: false,
      speedIncreasingEffect: baseMonster.speedIncreasingEffect || 0,
      speedBuffDuration: 0,
      has_slow: false,
      isSwift: baseMonster.isSwift,
      slowDuration: 0,
      flatSpeedBuffs: [],
      skills: baseMonster.skills,
      attack_bar: 0,
      turn: 0,
      element: baseMonster.element,
      tookTurn: false,
      image: baseMonster.image,
    }
    monster.combat_speed = this.calculateCombatSpeed(monster)
    return monster
  },
  runTick(monsters) {
    // if start of battle
    if (this.ticks.length === 1) {
      // take all skills that take effect at the start of battle and apply the effects
      for (let i = 0; i < monsters.length; i++) {
        const actualMonster = monsters[i].isAlly
          ? this.allies.find(item => item.key === monsters[i].key)
          : this.enemies.find(item => item.key === monsters[i].key)
        const skills = actualMonster.skills.filter(i => i.applyOnTurn === 0)
        const skillTargets = this.getSkillTargets(skills, monsters, i)
        this.applySkillEffects(monsters, skills, skillTargets)
      }
    }
    // this happens every turn

    // find who could take a turn
    let moveCandidate = this.getMonsterThatMoves(monsters)
    // let them take a turn
    if (moveCandidate) {
      const idx = monsters.findIndex(i => i.key === moveCandidate.key)
      monsters[idx].turn += 1
      const actualMonster = monsters[idx].isAlly
        ? this.allies.find(i => i.key === moveCandidate.key)
        : this.enemies.find(i => i.key === moveCandidate.key)

      // get skill targets before anything else happens
      const skills = actualMonster.skills.filter(i => i.applyOnTurn === monsters[idx].turn || i.applyOnTurn === -1)
      const skillTargets = this.getSkillTargets(skills, monsters, idx)

      // only after the targets are we set the attack bar to 0
      monsters[idx].attack_bar = 0
      // decrease buff durations - HELP HERE - do buffs end before or after the speeds of the next tick are calculated?
      if(monsters[idx].has_speed_buff) {
        monsters[idx].speedBuffDuration -= 1
        monsters[idx].has_speed_buff = monsters[idx].speedBuffDuration > 0
      }
      if(monsters[idx].has_slow) {
        monsters[idx].slowDuration -= 1
        monsters[idx].has_slow = monsters[idx].slowDuration > 0
      }
      for (let j = 0; j < monsters[idx].flatSpeedBuffs.length; j++) {
        monsters[idx].flatSpeedBuffs[j].flatSpeedBuffDuration -= 1
      }
      monsters[idx].flatSpeedBuffs = monsters[idx].flatSpeedBuffs.filter(buff => buff.flatSpeedBuffDuration > 0)

      this.applySkillEffects(monsters, skills, skillTargets)
    }
    // apply speed tick
    for (let i = 0; i < monsters.length; i++) {
      monsters[i].combat_speed = this.calculateCombatSpeed(monsters[i])
      if (this.leoExists) {
        const lowestLeoSpeed = Math.min(...monsters.map(monster => (monster.isLeo ? monster.combat_speed : 9999)))
        monsters[i].combat_speed = monsters[i].isLeo ? monsters[i].combat_speed: Math.min(monsters[i].combat_speed, lowestLeoSpeed)
      }
      monsters[i].attack_bar += monsters[i].combat_speed * (this.tickSize && Number(this.tickSize) ? Number(this.tickSize) / 100 : 0.07)
    }
    // check who is going to take a turn next (this is purely for highlighting in the ticks view)
    moveCandidate = this.getMonsterThatMoves (monsters)
    if (moveCandidate) {
      const idx = monsters.findIndex(i => i.key === moveCandidate.key)
      monsters[idx].tookTurn = true
    }
    return monsters
  },
  getMonsterThatMoves (monsters) {
    let candidate = null
    for (let i = 0; i < monsters.length; i++) {
      monsters[i].tookTurn = false
      const monster = monsters[i]
      if (monster.attack_bar >= 100) {
        if (candidate === null || monster.attack_bar > candidate.attack_bar) {
          candidate = monster
        }
      }
    }
    return candidate
  },
  applySkillEffects (monsters, skills, targets) {
    for (let j = 0; j < skills.length; j++) {
      for (let t = 0; t < targets[j].length; t++) {
        switch (skills[j].atbManipulationType){
          case 'add':
            monsters[targets[j][t]].attack_bar += skills[j].atbManipulationAmount
            break
          case 'subtract':
            monsters[targets[j][t]].attack_bar -= skills[j].atbManipulationAmount
            break
          case 'set':
            monsters[targets[j][t]].attack_bar = skills[j].atbManipulationAmount
            break
          default:
            console.warn('unknown atb manipulation type')
        }
        if(skills[j].buffSpeed){
          monsters[targets[j][t]].has_speed_buff = true
          monsters[targets[j][t]].speedBuffDuration = skills[j].speedBuffDuration
        }
        if(skills[j].stripSpeed){
          monsters[targets[j][t]].has_speed_buff = false
          monsters[targets[j][t]].speedBuffDuration = 0
        }
        if(skills[j].flatSpeedBuff){
          monsters[targets[j][t]].flatSpeedBuffs.push({
            type: skills[j].flatSpeedBuffType,
            flatSpeedBuffAmount: skills[j].flatSpeedBuffAmount,
            flatSpeedBuffDuration: skills[j].flatSpeedBuffDuration,
          })
        }
        if(skills[j].slow){
          monsters[targets[j][t]].has_slow = true
          monsters[targets[j][t]].slowDuration = skills[j].slowDuration
        }
      }
    }
  },
  getSkillTargets(skills, monsters, selfIdx){
    // don't ask, here be dragons
    // now only god now knows how this abomination works - but it seems to work correctly
    const skillTargets = []
    for (let j = 0; j < skills.length; j++) {
      let targets = []
      switch (skills[j].target) {
        case 'allies':
          targets = monsters
            .map((m, index) => ({ m, index }))
            .filter(item => item.m.isAlly)
            .map(item => item.index)
          break
        case 'enemies':
          targets = monsters
            .map((m, index) => ({ m, index }))
            .filter(item => !item.m.isAlly)
            .map(item => item.index)
          break
        case 'self':
          targets = [selfIdx]
          break
        case 'ally_atb_high': {
          const allies = monsters
            .map((m, index) => ({ m, index }))
            .filter(item => item.m.isAlly)
          if (allies.length) {
            const best = allies.reduce((prev, current) => current.m.attack_bar > prev.m.attack_bar ? current : prev)
            targets = [best.index]
          }
          break
        }
        case 'ally_atb_low': {
          const allies = monsters
            .map((m, index) => ({ m, index }))
            .filter(item => item.m.isAlly)
          if (allies.length) {
            const best = allies.reduce((prev, current) => current.m.attack_bar < prev.m.attack_bar ? current : prev)
            targets = [best.index]
          }
          break
        }
        case 'enemy_atb_high': {
          const enemies = monsters
            .map((m, index) => ({ m, index }))
            .filter(item => !item.m.isAlly)
          if (enemies.length) {
            const best = enemies.reduce((prev, current) => current.m.attack_bar > prev.m.attack_bar ? current : prev)
            targets = [best.index]
          }
          break
        }
        case 'enemy_atb_low': {
          const enemies = monsters
            .map((m, index) => ({ m, index }))
            .filter(item => !item.m.isAlly)
          if (enemies.length) {
            const best = enemies.reduce((prev, current) => current.m.attack_bar < prev.m.attack_bar ? current : prev)
            targets = [best.index]
          }
          break
        }
        default:
          targets = [monsters.findIndex(i => i.key === skills[j].target)]
      }
      skillTargets.push(targets)
    }
    return skillTargets
  }