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
}