Widget:Monster

{ const i18n = { missing_monster: 'No monster found with ', calculating: 'Calculating properties', query_error: 'A database query error has occured.', invalid_rarity: 'Rarity given is invalid. Only Normal, Magic, Rare and Unique are acceptable values.', invalid_difficulty: 'Difficulty given is invalid. Only part1, part2 and endgame are acceptable values.', invalid_level: 'Level given is not a number or outside of the valid range (1-100)', // (x to y)   range: 'to', };

let init_level = 0; const init_max = 3; const difficulties = { part1: 45, part2: 67, endgame: 100, };

const difficulty_order = ['part1', 'part2', 'endgame'];

const rarities = { Normal: 0, Magic: 1, Rare: 2, Unique: 3, };

const html_infobox = `

 Stats ?

 `

class Cargo { static _fail(jqXHR, textStatus, errorThrown) { console.log(textStatus); }   static _wrap_success(func) { return function(data) { data = data.cargoquery; func(data); }   }    static query (query, success, fail) { if (typeof fail === 'undefined') { fail = Cargo._fail; }       query.action = 'cargoquery'; // Query size for normal users is limited to 500 if (typeof query.limit === 'undefined' || (typeof query.limit === 'number' && query.limit > 500)) { query.limit = 500; }       if (Array.isArray(query.tables)) { query.tables = query.tables.join(', '); }       if (Array.isArray(query.fields)) { query.fields.forEach(function (value, index) {               if (!value.includes('=')) {                    query.fields[index] = `${value}=${value}`;                }            }); query.fields = query.fields.join(', '); }       var mwa = new mw.Api; console.log(query.where); mwa.get(query).done(Cargo._wrap_success(success)).fail(fail); } }

class Util { static bool(value) { switch(value) { case false: case 0: case 'disabled': case 'off': case 'no': case '': case 'deactivated': return false; default: return true; }   } }

class StatCalculation { static added(values, value) { values.forEach(function (other) {           value.add(other);        }); }   static increased(values, value) { var multi = new Stat('', 0); values.forEach(function (inc) {           multi.add(inc);        }); multi.div(100); multi.add(1); value.mult(multi); }   static more(values, value) { values.forEach(function (multi) {           var temp = multi.copy;            temp.div(100);            temp.add(1);            value.mult(temp);        }); }   static less(values, value) { values.forEach(function (multi) {           var temp = multi.copy;            temp.div(100);            // since it's less it's negative and subtracted from 1            temp.mult(-1);            temp.add(1);            value.mult(temp);        }); } }

class Stat { constructor(id, min, max) { this.id = id; this.min = min; if (typeof max === 'undefined') { max = min; }       this.max = max; }   copy { return new Stat(this.id, this.min, this.max); }   str(func) { if (this.min == this.max) { if (typeof func === 'undefined') { return this.min; } else { return func(this.min); }       } else { var min; var max; if (typeof func === 'undefined') { min = this.min; max = this.max; } else { min = func(this.min); max = func(this.max); }           return `(${min} ${i18n.range} ${max})`; }   }    is_zero { return (this.min == 0 && this.max == 0); }   _type_check(other, func_number, func_stat) { var t = typeof other; if (t === 'number') { func_number(this); } else if (t === 'object' && other.constructor.name == 'Stat') { func_stat(this); } else { throw 'Stat arithmetric requires a stat object or number, got type "' + t + '"'; }   }    add(other) { this._type_check(other,            function(stat) {                stat.min += other;                stat.max += other;            },            function(stat) {                stat.min += other.min;                stat.max += other.max;            },        ) }   sub(other) { this._type_check(other,            function(stat) {                stat.min -= other;                stat.max -= other;            },            function(stat) {                stat.min -= other.min;                stat.max -= other.max;            },        ) }   mult(other) { this._type_check(other,            function(stat) {                stat.min *= other;                stat.max *= other;            },            function(stat) {                stat.min *= other.min;                stat.max *= other.max;            },        ) }   div(other) { this._type_check(other,            function(stat) {                stat.min /= other;                stat.max /= other;            },            function(stat) {                stat.min /= other.min;                stat.max /= other.max;            },        ) } }

class Monster { constructor(metadata_id) { var controls = { level: { 'var': ``, 'default': 1, 'type': 'input', 'check': 'number', 'func_update': this.update_level.bind(this), 'func_validate': function (value) { var level = Number(value); if (level < 1 || level > 100) { alert(i18n.invalid_level); return -1; }                   return level; },           },            rarity: { 'var': ``, 'default': 'Normal', 'type': 'select', 'check': 'value', 'func_update': this.update_rarity.bind(this), 'func_validate': function (value) { if (typeof rarities[value] === 'undefined') { alert(i18n.invalid_rarity); return -1; }                   return value; },           },            is_map_monster: { 'var': ``, 'default': false, 'type': 'input', 'check': 'checked', 'func_update': this.update_is_map_monster.bind(this), 'func_validate': function (value) { return Util.bool(value); },           },            is_map_boss: { 'var': ``, 'default': false, 'type': 'input', 'check': 'checked', 'func_update': this.update_is_map_boss.bind(this), 'func_validate': function (value) { return Util.bool(value); },           },            is_minion: { 'var': ``, 'default': false, 'type': 'input', 'check': 'checked', 'func_update': this.update_is_minion.bind(this), 'func_validate': function (value) { return Util.bool(value); },           },            difficulty: { 'var': ``, 'default': 'part1', 'type': 'select', 'check': 'value', 'func_update': this.update_difficulty.bind(this), 'func_validate': function (value) { if (typeof difficulties[value] === 'undefined') { alert(i18n.invalid_difficulty); return -1; }                   return value; },           },        };        this.metadata_id = metadata_id; this.data = Monster.base_data_by_id[this.metadata_id]; this.stats = {}; this.stat_text = ''; // TODO: Maybe set a parameter on the widget to determine default here? this.show = true; for (const [_, mod_id] of Object.entries(this.data['monsters.mod_ids'])) { var mod = Monster.mods[mod_id]; this._update_stat_text(mod.stat_text); for (const [_, stat] of Object.entries(mod.stats)) { this.update_stat(stat.id, stat.copy); }       }        var html = $(html_infobox); html.attr('id', this.metadata_id); $('.monster_container > tbody').append(html); this.html = $(`tr[id="${this.metadata_id}"`); this.tbl = this.html.find('table.monster_table'); this.stat_tbl = this.html.find('table.monster_stats'); this.stat_bdy = this.stat_tbl.find('tbody'); this.html.find('tr.monster_name th').click(this.toggle_show.bind(this)); this.ctl = this.html.find('table.controls'); //       // Set and init defaults //       for (const [name, cdata] of Object.entries(controls)) { this[name] = cdata.default; }       this.update_rarity(this.rarity); // Also updates map boss and map monster this.update_level(this.level); // Will also set current difficulty this.update_difficulty(this.difficulty); this.update_is_minion(this.is_minion); // Widget parameters or set defaults for (const [name, cdata] of Object.entries(controls)) { var ele = this.ctl.find(`${cdata.type}[name="${name}"]`); // Still not entirely sure how the widget works if (cdata.var ==  || cdata.var === ) { cdata.var = cdata.default; } else { var rtr = cdata.func_validate(cdata.var); if (rtr == -1) { cdata.var = cdata.default; } else { cdata.var = rtr; this[name] = cdata.default; cdata.func_update(rtr); }           }             // Update the UI element accordingly regardless of the original HTML value if (cdata.type == 'input') { switch (cdata.check) { case 'checked': ele.prop('checked', cdata.var); break; case 'number': ele.prop('value', cdata.var); break; }           } else if (cdata.type == 'select') { if (name == 'rarity') { ele.prop('selectedIndex', rarities[cdata.var]); } else if (name == 'difficulty') { for (const [index, value] of Object.entries(difficulty_order)) { if (value == cdata.var) { ele.prop('selectedIndex', index); break; }                   }                }            }            // Add listener function var m = this; ele.change(function {                var value;                // In this context, "this" is the element that called this function                switch (cdata.check) {                    case 'checked':                        value = this.checked;                        break;                    case 'number':                        value = Number(this.value);                        break;                    case 'value':                    default:                        value = this.value;                }                cdata.func_update(value);                m.update_infobox;            }); }       // Static infobox properties this.html.find('em.monster_name').html(this.data['monsters.name']); // Can be directly inserted into the releveant fields for (const [i, key] of Object.entries([ 'size', 'minimum_attack_distance', 'maximum_attack_distance', ])) {           this.tbl.find(`.monster_${key}`).html(this.data[`monsters.${key}`]); }       this.tbl.find('.monster_skill_ids').html(this.data['monsters.skill_ids'].join(', ')); var tags = this.data['monsters.tags'].concat(this.data['monster_types.tags']); this.tbl.find('.monster_tags').html(tags.join(', ')); this.tbl.find('.monster_metadata_id').html(`${this.data['monsters.metadata_id']}`); // Update for the defaults this.update_infobox; }   toggle_show { if (this.show) { this.ctl.css('display', 'none'); this.tbl.css('display', 'none'); this.stat_tbl.css('display', 'none'); this.show = false; } else { this.ctl.css('display', 'table'); this.tbl.css('display', 'table'); this.stat_tbl.css('display', 'table'); this.show = true; }   }    _update_stat_text(stat_text) { if (stat_text == '') { return; }       stat_text = new DOMParser.parseFromString(stat_text, "text/html").documentElement.textContent; // a|b style wiki links stat_text = stat_text.replace(/\[\[([^\|]+)\|([^\]]+)\]\]/g, '$2'); // a style wiki links stat_text = stat_text.replace(/\[\[([^\]]+)\]\]/g, '$1'); if (this.stat_text == '') { this.stat_text = stat_text; } else { this.stat_text += ' ' + stat_text; }   }    update_level(level) { // Deletes old and adds new in one go       this.update_is_map_boss(this.is_map_boss, this.level, level); this.update_is_map_monster(this.is_map_monster, this.level, level); // Only deletes the old level this._update_rarity(this.rarity, true, this.level); this.level = level; // Rarity data for new level this._update_rarity(this.rarity, false, this.level); }   _update_difficulty_mods(difficulty, del=false) { for (const [_, mod_id] of Object.entries(this.data[`monsters.${difficulty}_mod_ids`])) { var mod = Monster.mods[mod_id]; //TODO //this.stat_text += ' ' + mod.stat_text; for (const [_, stat] of Object.entries(mod.stats)) { this.update_stat(stat.id, stat.copy, del); }       }    }    update_difficulty(difficulty) { var old = this.difficulty; if (old == difficulty) { return; }       if (typeof difficulty !== 'undefined') { if (typeof difficulties[difficulty] === 'undefined') { alert('Undefined difficulty. You probably messed with JS.'); } else { this.difficulty = difficulty; }       } else { for (const [_, diff] of Object.entries(difficulty_order)) { if (this.level <= difficulties[diff]) { this.difficulty = diff; break; }           }        }        this._update_difficulty_mods(old, true); this._update_difficulty_mods(difficulty, false); }   _update_map_monster_stats (stats, del=false, level) { for (var i=0;i=0; l--) { v = Monster.level_data[l][variable]; if (typeof v !== 'undefined') { break; }                   }                }            } else { this.update_stat(Monster.stat_map[variable], Monster.level_data[level][variable], del); }       }    }    update_is_map_monster(is_map_monster, level, new_level) { if (typeof level === 'undefined') { level = this.level; }       if (typeof new_level === 'undefined') { new_level = level; }       // Must be toggled off before changing the value to make sure the stats are removed correclty if (is_map_monster) { this.ctl.find('input[name="is_map_boss"]').prop('disabled', false); } else { this.ctl.find('input[name="is_map_boss"]').prop('disabled', true); // Can no longer be a map boss if it's not a map monster if (this.is_map_boss) { this.update_is_map_boss(false); this.ctl.find('input[name="is_map_boss"]').prop('checked', false); }       }        if (this.is_map_monster) { this._update_map_monster_stats(['map_life_multiplier', 'map_damage_multiplier'], true, level); }       this.is_map_monster = is_map_monster; if (is_map_monster) { this._update_map_monster_stats(['map_life_multiplier', 'map_damage_multiplier'], false, new_level); }   }    update_is_map_boss(is_map_boss, level, new_level) { if (typeof level === 'undefined') { level = this.level; }       if (typeof new_level === 'undefined') { new_level = level; }       // Only delete if it has been a map boss and map monster before to avoid putting stats into the negative if (this.is_map_boss && this.is_map_monster) { this._update_map_monster_stats(['boss_life', 'boss_damage', 'boss_item_quantity', 'boss_item_rarity'], true, level); }       this.is_map_boss = is_map_boss; // Only update if it is a map boss and map monster to avoid stacking stats incorrectly if (is_map_boss && this.is_map_monster) { this._update_map_monster_stats(['boss_life', 'boss_damage', 'boss_item_quantity', 'boss_item_rarity'], false, new_level); }   }    update_is_minion(is_minion) { this.is_minion = is_minion; // Either show regular life value or minion life value if (is_minion) { this.tbl.find('.monster_life').parent('tr').css('display', 'none'); this.tbl.find('.monster_summon_life').parent('tr').css('display', 'table-row'); this.ctl.find('select[name="rarity"]').prop('disabled', true); this.ctl.find('input[name="is_map_monster"]').prop('disabled', true); this.ctl.find('input[name="is_map_boss"]').prop('disabled', true); if (this.rarity != 'Normal') { this.ctl.find('select[name="rarity"]').prop('selectedIndex', 0); this.update_rarity('Normal'); }           if (this.is_map_boss) { this.ctl.find('input[name="is_map_boss"]').prop('checked', 0); this.update_is_map_boss(false); }           if (this.is_map_monster) { this.ctl.find('input[name="is_map_monster"]').prop('checked', 0); this.update_is_map_monster(false); }       } else { this.tbl.find('.monster_life').parent('tr').css('display', 'table-row'); this.tbl.find('.monster_summon_life').parent('tr').css('display', 'none'); this.ctl.find('select[name="rarity"]').prop('disabled', false); this.ctl.find('input[name="is_map_monster"]').prop('disabled', false); this.ctl.find('input[name="is_map_boss"]').prop('disabled', false); }   }    // this can also be called via level change since monster rarity hp multiplier depend on the level _update_rarity(rarity, del=false, level) { for (const [stat_id, value] of Object.entries(Monster.rarity_data[rarity])) { this.update_stat(stat_id, value, del); }       if (rarity == 'Magic' || rarity == 'Rare') { if (typeof level === 'undefined') { level = this.level; }           var v = Monster.level_data[level][rarity + '_life_multiplier']; // In this case (i.e. level > 84) the highest available will be used // Since this might change in the future a loop is used to determine the max level if (typeof v === 'undefined') { for (var i=Monster.level_data.length; i>=0; i--) { v = Monster.level_data[i][variable]; if (typeof v !== 'undefined') { break; }               }            }            this.update_stat(Monster.stat_map[rarity + '_life_multiplier'], v, del); }   }    update_rarity(rarity, level) { // Delete stats from previous rarity level this._update_rarity(this.rarity, true, level); this.rarity = rarity; this._update_rarity(this.rarity, false, level); }

update_stat(stat_id, value, del=false) { var v = this.stats[stat_id]; if (typeof v === 'undefined') { if (!del) { if (typeof value === 'number') { value = new Stat(stat_id, value); } else { value = value.copy; }               this.stats[stat_id] = value; }           // If the stat doesn't exist, we don't need to delete it        } else { if (del) { v.sub(value); if (v.is_zero) { delete this.stats[stat_id]; }           } else { v.add(value); }       }    }    calculate_property(type) { var calc = Monster.calculation_params[type]; var value; switch (calc.type) { case 'level': value = new Stat(type, Monster.level_data[this.level][type]); if (typeof calc.base !== 'undefined') { value.mult(this.data[calc.base]); }               break; case 'base': value = new Stat(type, this.data[calc.base]); break; case 'resist': var diff = this.difficulty; var t = type.match(/(cold|chaos|fire|lightning)/)[1]; value = new Stat(type, this.data[`monster_resistances.${diff}_${t}`]); break; case 'none': default: var v;               if (typeof calc.value === 'undefined') { v = 0; } else { v = calc.value; }               value = new Stat(type, v); }       // Overriding is a special case that ingores all further calcuations var stats = this.stats; if (typeof calc.override !== 'undefined') { for (const [stat_id, override_value] of Object.entries(calc.override)) { var v = stats[stat_id]; if (typeof v !== 'undefined') { // TODO: Support for overriding to specific values if needed if (v != 0) { return override_value; }               }            }        }        for (const [_, stat_calc_type] of Object.entries(['added', 'increased', 'more', 'less'])) { var stat_ids = calc[stat_calc_type]; if (typeof stat_ids !== 'undefined') { var values = []; stat_ids.forEach(function (stat_id) {                   var v = stats[stat_id];                    if (typeof v !== 'undefined') {                        values.push(v);                    }                }); StatCalculation[stat_calc_type](values, value); }       }        return value; }   infobox_level_changed { this.update_level(Number(this.ctl.find('input[name="level"]')[0].value)); update_infobox; }   infobox_rarity_changed { this.update_rarity(this.ctl.find('select[name="rarity"]')[0].value); update_infobox; }   infobox_is_map_monster_changed { this.update_is_map_monster(this.ctl.find('input[name="is_map_monster"]')[0].checked); update_infobox; }   infobox_is_map_boss_changed { this.update_is_map_boss(this.ctl.find('input[name="is_map_boss"]')[0].checked); update_infobox; }   infobox_diffculty_changed { this.update_difficulty(this.ctl.find('select[name="difficulty"]')[0].value); update_infobox; }   update_infobox { $('.monster_container').find('.info_header').css('display', 'table-cell').html(i18n.calculating); // Correct rarity of the header this.html.find('em.monster_name').removeClass('-normal -magic -rare -unique').addClass('-' + this.rarity.toLowerCase); for (const [key, data] of Object.entries(Monster.calculation_params)) { if (key.endsWith('resistance')) { continue; }           this.tbl.find(`.monster_${key}`).html(this.calculate_property(key).str(data.fmt)); }       for (const [i, resist_type] of Object.entries(['lightning', 'cold', 'fire', 'chaos'])) { var key = resist_type + '_resistance'; this.tbl.find('.monster_resistance').find(`.-${resist_type}`).html(this.calculate_property(key).str(Monster.calculation_params[key].fmt)); }       this.tbl.find('.monster_stat_text').html(this.stat_text); this.stat_bdy.find('tr').remove; for (const [stat_id, stat] of Object.entries(this.stats)) { this.stat_bdy.append(` ${stat.id}  ${stat.min}  ${stat.max}  `); }       // Done, remove the calculating notice $('.monster_container').find('.info_header').css('display', 'none'); } }

function fmt_number(digits, format) { if (typeof digits === 'undefined') { digits = 0; }   if (typeof format === 'undefined') { format = '{1}'; }   return function(value) { value = value.toFixed(digits); return format.replace(/\{1\}/, value); } }

Monster.level_data = []; Monster.rarity_data = { // Normal shouldn't have any stats associated with it. Normal: {}, Magic: {}, Rare: {}, Unique: {}, }; Monster.mods = {}; Monster.base_data = {}; Monster.base_data_by_id = {};

// Map these stats back for calcuation purposes Monster.stat_map = { ['map_life_multiplier']: 'map_hidden_monster_life_+%_final', ['map_damage_multiplier']: 'map_hidden_monster_damage_+%_final', ['boss_life']: 'map_hidden_monster_life_+%_final', ['boss_damage']: 'map_hidden_monster_damage_+%_final', ['boss_item_quantity']: 'monster_dropped_item_quantity_+%', ['boss_item_rarity']: 'monster_dropped_item_rarity_+%', // I believe these are the ones used though not at the same time ['Magic_life_multiplier']: 'monster_life_+%_final_from_rarity_table', ['Rare_life_multiplier']: 'monster_life_+%_final_from_rarity_table', };

// List of things affected by each stat to calculate a final value Monster.calculation_params = { damage: { type: 'level', base: 'monsters.damage_multiplier', increased: [ 'damage_+%', 'map_monsters_damage_+%', 'map_boss_damage_+%', ],       more: [ 'damage_+%_final', 'monster_rarity_damage_+%_final', 'map_hidden_monster_damage_+%_final', //'map_hidden_monster_damage_+%_squared_final', 'monster_damage_+%_final_from_watchstone', ],       less: [ 'monster_rarity_attack_cast_speed_+%_and_damage_-%_final', 'monster_base_type_attack_cast_speed_+%_and_damage_-%_final', ],       fmt: fmt_number(0), },   // map_boss_life_+permyriad_final_from_awakening_level life: { type: 'level', base: 'monsters.health_multiplier', added: [ 'base_maximum_life', ],       increased: [ 'maximum_life_+%', 'map_monsters_life_+%', 'map_boss_maximum_life_+%', ],       more: [ 'maximum_life_+%_final', 'map_hidden_monster_life_+%_final', //'map_hidden_monster_life_+%_times_6_final', 'monster_life_+%_final_from_map', 'monster_life_+%_final_from_rarity', 'monster_life_+%_final_from_rarity_table', 'monster_life_+%_final_from_watchstone', ],       fmt: fmt_number(0), },   summon_life: { type: 'level', base: 'monsters.health_multiplier', added: [ 'base_maximum_life', ],       increased: [ 'maximum_life_+%', ],       more: [ 'maximum_life_+%_final', ],       fmt: fmt_number(0), },   evasion: { type: 'level', fmt: fmt_number(0), },   accuracy: { type: 'level', increased: [ 'map_monsters_accuracy_rating_+%', ],       fmt: fmt_number(0), },   armour: { type: 'level', fmt: fmt_number(0), },   experience: { type: 'level', base: 'monsters.experience_multiplier', increased: [ 'map_hidden_experience_gain_+%', ],       override: { ['map_monsters_no_drops_or_experience']: 0, },       fmt: fmt_number(0), },   attack_speed: { type: 'base', base: 'monsters.attack_speed', increased: [ 'map_monsters_attack_speed_+%', 'map_boss_attack_and_cast_speed_+%', ],       more: [ 'monster_rarity_attack_cast_speed_+%_and_damage_-%_final', 'monster_base_type_attack_cast_speed_+%_and_damage_-%_final', ],       fmt: fmt_number(2), },   rarity: { type: 'none', added : [ 'monster_dropped_item_rarity_+%', ],       //monster_dropped_item_quantity_+%_from_player_support more: [ // These two should be multiplicative at least with each other as per GGG 'map_item_drop_rarity_+%', ],       fmt: fmt_number(0, '{1} %'), },   quantity: { type: 'none', added: [ 'monster_dropped_item_quantity_+%', ],       // monster_dropped_item_quantity_+%_from_player_support more: [ // These two should be multiplicative at least with each other as per GGG 'map_item_drop_quantity_+%', 'monster_dropped_item_quantity_from_numplayers_+%', ],       override: { 'map_monsters_no_drops_or_experience': 0, },       fmt: fmt_number(0, '{1} %'), },   lightning_resistance: { type: 'resist', added: [ 'map_monsters_additional_lightning_resistance', ],       fmt: fmt_number(0, '{1} %'), },   cold_resistance: { type: 'resist', added: [ 'map_monsters_additional_cold_resistance', ],       fmt: fmt_number(0, '{1} %'), },   fire_resistance: { type: 'resist', added: [ 'map_monsters_additional_fire_resistance', ],       fmt: fmt_number(0, '{1} %'), },   chaos_resistance: { type: 'resist', added: [ 'map_monsters_additional_chaos_resistance', ],       fmt: fmt_number(0, '{1} %'), },   critical_strike_chance: { type: 'base', base: 'monsters.critical_strike_chance', increased: [ 'map_monsters_critical_strike_chance_+%', ],       fmt: fmt_number(2, '{1} %'), },   critical_strike_multiplier: { type: 'none', value: 130, base: 'monsters.critical_strike_multiplier', added: [ 'map_monsters_critical_strike_multiplier_+', ],       fmt: fmt_number(2, '{1} %'), }, }

function _run_final_init { init_level = init_level + 1; // Prevents from being run until all the cargo data is asyncronously loaded if (init_level >= init_max) { monster_finalize_init; } }

function monster_init { Cargo.query({       tables: ['monster_base_stats', 'monster_life_scaling', 'monster_map_multipliers'],        fields: [            'accuracy',             'armour',             'monster_base_stats.damage=damage',             'evasion',             'experience',             'monster_base_stats.level=level',             'monster_base_stats.life=life',             'summon_life',             'magic=Magic_life_multiplier',             'rare=Rare_life_multiplier',             'monster_map_multipliers.life=map_life_multiplier',            'monster_map_multipliers.damage=map_damage_multiplier',            'boss_damage',            'boss_item_quantity',            'boss_item_rarity',            'boss_life',        ],        join_on: 'monster_base_stats.level = monster_life_scaling.level, monster_base_stats.level=monster_map_multipliers.level',    }, function (data) {        data.forEach(function (value) { var v = {}; for (const [field, field_value] of Object.entries(value.title)) { v[field] = Number(field_value); }           Monster.level_data[v.level] = v;        });        _run_final_init;    }); Cargo.query({       tables: ['monsters', 'monster_types', 'monster_resistances'],        fields: [            //            // Monster data            //            'monsters._pageName',            'monsters.attack_speed',            'monsters.critical_strike_chance',            'monsters.damage_multiplier',            'monsters.experience_multiplier',            'monsters.health_multiplier',            //'monsters.minimum_attack_distance',            //'monsters.maximum_attack_distance',            //'monsters.skill_ids',            //'monsters.size',            'monsters.part1_mod_ids',            'monsters.part2_mod_ids',            'monsters.mod_ids',            'monsters.endgame_mod_ids',            'monsters.metadata_id',            'monsters.name',            // Apprently strips the table if I don't do this            'monsters.tags__full=monsters.tags',            'monsters.skill_ids', 'monsters.minimum_attack_distance', 'monsters.maximum_attack_distance', 'monsters.size', //           // Monster type data //           // Apprently strips the table if I don't do this 'monster_types.tags__full=monster_types.tags', // This is kinda unconfirmed, want to leave it out for the moment // 'monster_types.armour_multiplier', // 'monster_types.damage_spread', // 'monster_types.energy_shield_multiplier', // 'monster_types.evasion_multiplier', //           // Monster resistance data //           'monster_resistances.part1_fire', 'monster_resistances.part1_cold', 'monster_resistances.part1_lightning', 'monster_resistances.part1_chaos', 'monster_resistances.part2_fire', 'monster_resistances.part2_cold', 'monster_resistances.part2_lightning', 'monster_resistances.part2_chaos', // Should rename these fields for consistency -.- 'monster_resistances.maps_fire=monster_resistances.endgame_fire', 'monster_resistances.maps_cold=monster_resistances.endgame_cold', 'monster_resistances.maps_lightning=monster_resistances.endgame_lightning', 'monster_resistances.maps_chaos=monster_resistances.endgame_chaos', ],       join_on: 'monsters.monster_type_id=monster_types.id, monster_types.monster_resistance_id=monster_resistances.id', //where: 'monsters.name LIKE "%Izaro%"', //limit: 2, //where: 'monsters.metadata_id="Metadata/Monsters/Atziri/Atziri"', where: ``, }, function (data) { if (data.length == 0) { $('.monster_container').find('.info_header').text(i18n.missing_monster); return; }       var query_mods = {}; for (const [index, entry] of Object.entries(data)) { var curdata = entry.title; // Need these as numbers to calcuate values later on           [ 'monsters.attack_speed', 'monsters.critical_strike_chance', 'monsters.damage_multiplier', 'monsters.experience_multiplier', 'monsters.health_multiplier', 'monster_resistances.part1_fire', 'monster_resistances.part1_cold', 'monster_resistances.part1_lightning', 'monster_resistances.part1_chaos', 'monster_resistances.part2_fire', 'monster_resistances.part2_cold', 'monster_resistances.part2_lightning', 'monster_resistances.part2_chaos', // Should rename these fields for consistency -.- 'monster_resistances.endgame_fire', 'monster_resistances.endgame_cold', 'monster_resistances.endgame_lightning', 'monster_resistances.endgame_chaos', ].forEach(function (value) {               curdata[value] = Number(curdata[value]);             }); // Needed as lists [               'monsters.part1_mod_ids', 'monsters.part2_mod_ids', 'monsters.mod_ids', 'monsters.endgame_mod_ids', 'monsters.tags', 'monsters.skill_ids', 'monster_types.tags', ].forEach(function (key) {               var value = curdata[key];                if (value == "") {                    value = [];                } else {                    value = value.split(',');                }                curdata[key] = value;            }); Monster.base_data[index] = entry.title; Monster.base_data_by_id[entry.title['monsters.metadata_id']] = entry.title; //           // Schedule mods for querying //           [                'monsters.part1_mod_ids', 'monsters.part2_mod_ids', 'monsters.mod_ids', 'monsters.endgame_mod_ids', ].forEach(function (field) {               curdata[field].forEach(function (mod_id) { query_mods[mod_id] = true; });           });        }        var query_mods_array = []; for (const [mod_id, a] of Object.entries(query_mods)) { query_mods_array.push(mod_id); }       // avoids query errors due to empty IN clause if (query_mods_array.length > 500) { //TODO alert('FIXME: Over 500 mods'); } else if (query_mods_array.length > 0) { query_mods = query_mods_array.join('", "'); Cargo.query({               tables: ['mods', 'mod_stats'],                fields: [                    'mods.id=mod_id',                    'mods.stat_text=mods.stat_text',                    'mod_stats.id=stat_id',                    'mod_stats.min',                    'mod_stats.max',                ],                join_on: 'mods._pageID=mod_stats._pageID',                where: `                    mods.id IN ("${query_mods}") OR ( mods.generation_type = 3 AND mods.domain = 3 AND mods.id REGEXP "Monster(Magic|Rare|Unique)[0-9]*$" )`           }, function (data) {                data.forEach(function (value) { var v = value.title; var stat = new Stat(v.stat_id, Number(v['mod_stats.min']), Number(v['mod_stats.max'])); var rarity = v.mod_id.match(/Monster(Magic|Rare|Unique)[0-9]*$/); if (rarity == null) { if (typeof Monster.mods[v.mod_id] === 'undefined') { Monster.mods[v.mod_id] = { 'stat_text': v['mods.stat_text'], 'stats': [], };                       }                        Monster.mods[v.mod_id].stats.push(stat); } else { Monster.rarity_data[rarity[1]][v.stat_id] = stat; }               });                _run_final_init;            }); } else { // need to increment in any case _run_final_init; }       _run_final_init; }, function(jqXHR, textStatus, errorThrown) { console.log(textStatus); // since JS doesn't actually show the DB error the query is duplicated in the template. The error will be shown there. $('#monster_query_error').css('display', 'initial'); $('.monster_container').find('.info_header').html(i18n.query_error); }); }

var m = {};

function monster_finalize_init { for (const [metadata_id, data] of Object.entries(Monster.base_data_by_id)) { m[metadata_id] = new Monster(metadata_id); }   $('.monster_container').find('.info_header').css('display', 'none'); }

// // Test functions //

function test_stat { a = new Stat('id', 1, 1); b = new Stat('id', 5, 5); a.add(b); console.log('+ 6?', a.min, a.max); a.sub(b); console.log('- 1?', a.min, a.max); a.mult(b); console.log('* 5?', a.min, a.max); a.div(b); console.log('/ 1?', a.min, a.max); a.add(10); console.log('+ 11?', a.min, a.max); a.sub(10); console.log('- 1?', a.min, a.max); a.mult(10); console.log('* 10?', a.min, a.max); a.div(10); console.log('/ 1?', a.min, a.max); } //test_stat;

//monster_init; window.addEventListener('load', function {   setTimeout(monster_init, 1000); }); }