Widget:Sandbox: Difference between revisions

From Path of Exile Wiki
Jump to navigation Jump to search
>OmegaK2
(Created page with "<table class="monster_container" style="border: white solid 1px; width: 100%;"> <thead> <tr> <th class="info_header">Fetching data...</th> </tr> </thead> <tbod...")
 
>OmegaK2
No edit summary
Line 7: Line 7:
<tbody>
<tbody>
</tbody>
</tbody>
</table>
</table><script type="text/javascript">{
const i18n = {
    missing_monster: 'No monster found with <!--{$metadata_id}-->',
    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 = `<tr class="monster"><td>
<table class="monster_info_container">
<tr class="monster_name"><th><em class="monster_name tc">?</em></th></tr>
<tr class="controls"><td><table class="wikitable controls">
    <thead>
        <tr>
            <th colspan="2">Controls</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <th>Level</th>
            <td><input type="number" name="level" min="1" max="100" value="1"/></td>
        </tr>
        <tr>
            <th>Rarity</th>
            <td><select name="rarity">
                  <option value="Normal" selected>Normal</option>
                  <option value="Magic">Magic</option>
                  <option value="Rare">Rare</option>
                  <option value="Unique">Unique</option>
            </select></td>
        </tr>
        <tr>
            <th>Is map monster?</th>
            <td><input type="checkbox" name="is_map_monster" value="1" /></td>
        </tr>
        <tr>
            <th>Is map boss?</th>
            <td><input type="checkbox" name="is_map_boss" value="1" /></td>
        </tr>
        <tr>
            <th>Is minion</th>
            <td><input type="checkbox" name="is_minion" value="1" /></td>
        </tr>
        <tr>
            <th>Difficuty</th>
            <td><select name="difficulty">
                  <option value="part1" selected>Part 1</option>
                  <option value="part2">Part 2</option>
                  <option value="endgame">Endgame</option>
            </select></td>
        </tr>
    </tbody>
</table></td></tr>
 
<tr class="monster_table"><td><table class="wikitable monster_table">
    <!-- static attributes -->
    <tr>
        <th colspan="2"><em class="monster_name tc">?</em></th>
    </tr>
    <tr>
        <th>Tags</th>
        <td class="monster_tags">?</td>
    </tr>
    <tr>
        <th>Metadata ID</th>
        <td class="monster_metadata_id">?</td>
    </tr>
    <tr>
        <th>Skills</th>
        <td class="monster_skill_ids">?</td>
    </tr>
    <tr>
        <th>Size</th>
        <td class="monster_size">?</td>
    </tr>
    <tr>
        <th>Minimum attack distance</th>
        <td class="monster_minimum_attack_distance">?</td>
    </tr>
    <tr>
        <th>Maximum attack distance</th>
        <td class="monster_maximum_attack_distance">?</td>
    </tr>
   
    <!-- Variable attributes -->
    <tr>
        <th>Experience</th>
        <td class="monster_experience">?</td>
    </tr>
    <tr>
        <th>Life</th>
        <td class="monster_life">?</td>
    </tr>
    <!-- Wiki colours every other line differently, prevent that from happening -->
    <tr style="display:none"></tr>
    <tr>
        <th>Life</th>
        <td class="monster_summon_life">?</td>
    </tr>
    <tr>
        <th>Accuracy</th>
        <td class="monster_accuracy">?</td>
    </tr>
    <tr>
        <th>Armour</th>
        <td class="monster_armour">?</td>
    </tr>
    <tr>
        <th>Evasion</th>
        <td class="monster_evasion">?</td>
    </tr>
    <tr>
        <th>Base Physical Damage</th>
        <td class="monster_damage">?</td>
    </tr>
    <tr>
        <th>Base Attack Speed</th>
        <td class="monster_attack_speed">?</td>
    </tr>
    <tr>
        <th>Critical Strike Chance</th>
        <td class="monster_critical_strike_chance">?</td>
    </tr>
    <tr>
        <th>Critical Strike Multiplier</th>
        <td class="monster_critical_strike_multiplier">?</td>
    </tr>
    <tr>
        <th>Increased Rarity</th>
        <td class="monster_rarity">?</td>
    </tr>
    <tr>
        <th>Increased Quantity</th>
        <td class="monster_quantity">?</td>
    </tr>
    <tr>
        <th>Resistances</th>
        <td><table class="monster_resistance"><tr>
            <td class="tc -fire">?</td>
            <td class="tc -cold">?</td>
            <td class="tc -lightning">?</td>
            <td class="tc -chaos">?</td>
        </tr></table></td>
    </tr>
</table></td></tr>
 
<tr class="monster_stats"><td><table class="wikitable monster_stats">
<thead>
    <tr>
        <th colspan="3">Stats</th>
    </tr>
    <tr>
        <th>Stat</td>
        <th>Min</td>
        <th>Max</td>
    </tr>
</thead>
<tbody>
</tbody>
</table></td></tr>
</table></td></tr>`
 
 
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': '<!--{$level}-->',
                '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': '<!--{$rarity}-->',
                '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': '<!--{$is_map_monster}-->',
                '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': '<!--{$is_map_boss}-->',
                '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': '<!--{$is_minion}-->',
                'default': false,
                'type': 'input',
                'check': 'checked',
                'func_update': this.update_is_minion.bind(this),
                'func_validate': function (value) {
                    return Util.bool(value);
                },
            },
            difficulty: {
                'var': '<!--{$difficulty}-->',
                '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.stat_text += '<br>' + 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');
       
        // 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 == '<!--' + '{$' + name + '}-->' || 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;
                }
            }
           
            // 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;
                        }
                    }
                }
            }
           
            // Only copy the value to monster instance, since the other data won't be needed anymore
            this[name] = cdata.var;
           
            // 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();
            });
        }
       
        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(controls.difficulty.var);
        this.update_is_minion(controls.is_minion.var);
       
        // 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([
            'metadata_id',
            '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(', '));
       
        // 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_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, true, 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 += '<br>' + 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<stats.length;i++) {
            var variable = stats[i];
            var v = Monster.level_data[level][variable];
            if (typeof v === 'undefined') {
                if (this.level < 66) {
                    console.log('Map monster is set but undefined damage/hp multiplier value at level ' + this.level);
                    return;
                } else {
                    console.log('Map monster is set but undefined damage/hp multiplier value at level ' + this.level + '. The highest available value will be used');
                    for (var l=Monster.level_data.length-1; l>=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;
        }
   
        this._update_map_monster_stats(['map_life_multiplier', 'map_damage_multiplier'], true, level);
        this.is_map_monster = is_map_monster;
        this._update_map_monster_stats(['map_life_multiplier', 'map_damage_multiplier'], !(this.is_map_monster), new_level);
       
        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);
            }
        }
    }
   
    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;
        }
       
        // Map bosses must be both a map monster and am map boss
        this._update_map_monster_stats(['boss_life', 'boss_damage', 'boss_item_quantity', 'boss_item_rarity'], true, level);
        this.is_map_boss = is_map_boss;
        this._update_map_monster_stats(['boss_life', 'boss_damage', 'boss_item_quantity', 'boss_item_rarity'], !(this.is_map_monster && this.is_map_boss), 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.stat_bdy.find('tr').remove();
        for (const [stat_id, stat] of Object.entries(this.stats)) {
            this.stat_bdy.append(`<tr><td>${stat.id}</td><td>${stat.min}</td><td>${stat.max}</td></tr>`);
        }
       
        // 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.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/AnimatedItem/AnimatedArmourBossSideAreaInvasion"',
        where: '<!--{$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',
                    '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);
});
}</script>

Revision as of 21:03, 22 March 2020

<thead> </thead> <tbody> </tbody>
Fetching data...

<script type="text/javascript">{

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 = `

?
   <thead>
</thead> <tbody> </tbody>
Controls
Level <input type="number" name="level" min="1" max="100" value="1"/>
Rarity <select name="rarity">
                 <option value="Normal" selected>Normal</option>
                 <option value="Magic">Magic</option>
                 <option value="Rare">Rare</option>
                 <option value="Unique">Unique</option>
</select>
Is map monster? <input type="checkbox" name="is_map_monster" value="1" />
Is map boss? <input type="checkbox" name="is_map_boss" value="1" />
Is minion <input type="checkbox" name="is_minion" value="1" />
Difficuty <select name="difficulty">
                 <option value="part1" selected>Part 1</option>
                 <option value="part2">Part 2</option>
                 <option value="endgame">Endgame</option>
</select>
?
Tags ?
Metadata ID
Skills ?
Size ?
Minimum attack distance ?
Maximum attack distance ?
Experience ?
Life ?
Life ?
Accuracy ?
Armour ?
Evasion ?
Base Physical Damage ?
Base Attack Speed ?
Critical Strike Chance ?
Critical Strike Multiplier ?
Increased Rarity ?
Increased Quantity ?
Resistances
? ? ? ?

<thead>

</thead> <tbody> </tbody>
Stats
Stat Min Max

`


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.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'); // 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; } } // 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; } } } } // Only copy the value to monster instance, since the other data won't be needed anymore this[name] = cdata.var; // 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(); }); } 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(controls.difficulty.var); this.update_is_minion(controls.is_minion.var); // 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([ 'metadata_id', '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(', ')); // 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_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, true, 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<stats.length;i++) { var variable = stats[i]; var v = Monster.level_data[level][variable]; if (typeof v === 'undefined') { if (this.level < 66) { console.log('Map monster is set but undefined damage/hp multiplier value at level ' + this.level); return; } else { console.log('Map monster is set but undefined damage/hp multiplier value at level ' + this.level + '. The highest available value will be used'); for (var l=Monster.level_data.length-1; l>=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; } this._update_map_monster_stats(['map_life_multiplier', 'map_damage_multiplier'], true, level); this.is_map_monster = is_map_monster; this._update_map_monster_stats(['map_life_multiplier', 'map_damage_multiplier'], !(this.is_map_monster), new_level); 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); } } } 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; } // Map bosses must be both a map monster and am map boss this._update_map_monster_stats(['boss_life', 'boss_damage', 'boss_item_quantity', 'boss_item_rarity'], true, level); this.is_map_boss = is_map_boss; this._update_map_monster_stats(['boss_life', 'boss_damage', 'boss_item_quantity', 'boss_item_rarity'], !(this.is_map_monster && this.is_map_boss), 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.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.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/AnimatedItem/AnimatedArmourBossSideAreaInvasion"',
       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',
                   '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);

}); }</script>