Widget:Sandbox: Difference between revisions

From Path of Exile Wiki
Jump to navigation Jump to search
>OmegaK2
No edit summary
m (Module:Documentation now expects documentation for widgets to be in the talkspace)
 
(6 intermediate revisions by 2 users not shown)
Line 1: Line 1:
<table class="monster_container" style="border: white solid 1px; width: 100%;">
<!--
<thead>
    <tr>
        <th class="info_header">Fetching data...</th>
    </tr>
</thead>
<tbody>
</tbody>
</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;
This is not an actual widget. Do not add code.
const init_max = 3;
const difficulties = {
    part1: 45,
    part2: 67,
    endgame: 100,
};


const difficulty_order = ['part1', 'part2', 'endgame'];
--><noinclude>
 
{{Documentation}}
const rarities = {
</noinclude>
    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>

Latest revision as of 20:51, 16 January 2021

Widget documentation[view] [edit] [history] [purge]


This page is not an actual widget. It exists to provide editors a place to create experimental widgets. Please note that widgets can only be edited by users who have special permissions, namely administrators.

Naming your widgets

To keep things tidy, please use the following format to name your experimental widgets:

Widget:Sandbox/Your username/Widget name

Cleaning up unused widgets

Experimental widgets may be deleted by the author once they are no longer in use, or by others after a long period of inactivity.

List of widgets in this area

For a list of the experimental widgets under Widget:Sandbox, see Special:PrefixIndex/Widget:Sandbox/.