Crystal Check

Combine dropped crystals after hunting

// ==UserScript==
// @name            Crystal Check
// @match           http://*.legacy-game.net/hunting3.php
// @match           http://*.legacy-game.net/inventory.php*
// @version         1.11
// @namespace       https://greasyfork.org/users/4585
// @description     Combine dropped crystals after hunting
// @grant           none
/*global $ */
// ==/UserScript==


/*****************************************************
                    General Methods                    
/****************************************************/
//Method to create elements on the fly without injecting html
function create(type, attr, text, parent) {
    var ele = document.createElement(type);
    for (var x in attr) {
        if (x.hasOwnProperty) {
            ele.setAttribute(x, attr[x]);
        }
    }
    if (text) {
        ele.innerHTML = text;
    }
    if (parent) {
        parent.appendChild(ele);
    }
    return ele;
}
//Return user cookies as object
function getCookies() {
    return document.cookie
        .split(/[;\s]+/g)
        .reduce(function(a, b) {
            a[b.split('=')[0]] = b.split('=')[1];
            return a;
        }, {});
}
/*****************************************************
                    Inventory                    
/****************************************************/

//Inventory API to manipulate inventory items
function Inventory(html) {
    this.items = getItems();
    this.key = $(html).find('form').attr('action').match(/[a-zA-Z]+$/g).pop();
    var key = this.key;
    //Inventory items encapsulated to organise data
    function Item(name, slot, trades, id) {
        this.name = name;
        this.slot = slot;
        this.trades = trades;
        this.id = id;

        //Crystal merging method
        this.merge = function(that, callback) {
            var slot1 = this.slot;
            var slot2 = that.slot;
            $.ajax({
                type: "POST",
                url: "inventory10.php?i=" + slot1 + "&key=" + key,
                async: false,
                data: {
                    item: slot2
                },
                success: function(data) {
                    //If method available, use on response data to check for further crystal merging
                    //Response from merging should be the redirected inventory page, which can be used to
                    //refresh inventory data
                    if (callback)
                        callback.call(this, data);
                }
            });
        };
    }
    //captures all item data for the inventory & encapsulates as an object
    function getItems() {
        //Match javascript lines on inventory.php for item id & trades
        //Each resulting index corresponds to the slot in inventory
        var itemPreviews = html.match(/(itemPreviews\[)((.|\n)*?)(Default|table>');/g);
        var tempItems = [];
        $(html).find('img.item').each(function() {
            var slot = parseInt($(this).attr('name'));
            var dat = itemPreviews[slot].replace("Un-tradable", "0 Trade").match(/(\d+)(?=(&c=|\sTrade))/g);
            tempItems.push(new Item($(this).attr('title'), slot, dat[0], dat[1]));
        });
        return tempItems;
    }

    //Returns all crystals in the inventory, sorted by item id (newest first)
    this.getCrystals = function() {
        return this.items
            //check if item is a crystal (must be have some word before crystal Crystal to avoid Crstal Rings)
            .filter(function(a) {
                return a.name.match(/.+Crystal/) !== null;
            })
            //order from newest to oldest (item id descending)
            .sort(function(a, b) {
                return b.id - a.id;
            });
    };
    //Returns an array of matching size/colour crystals
    //Ordered by trades on given crystal ascending, then everything else descending
    this.matchCrystal = function(crystal) {
        return this.getCrystals().filter(function(a) {
            return a.id !== crystal.id && a.name == crystal.name && crystal.name.search('Perfect')==-1;
        }).sort(function(a, b) {
            return (a.trades >= crystal.trades && b.trades >= crystal.trades) ? a.trades - b.trades : b.trades - a.trades;
        });
    };
}

/*****************************************************
                    Hunting Page Methods                    
/****************************************************/

function huntMerge() {
    //Once a crystal drop has been detected
    function checkDrop() {
        //Display 'found match' message on single line row
        function singleRow(crystal) {
            var r = create('tr', {}, false, $('tbody:contains("Item Found")')[0]);
            create('td', {
                    width: "100%",
                    class: "standardrow",
                    align: "center",
                    colspan: "2"
                },
                'You have a matching ' + crystal.name + ' (' + crystal.trades + 't) in your inventory. <span class="merge" style="cursor:pointer;">[Merge]</span>',
                r);
        }

        //create 'item drop' row displaying merged crystal image + result message
        function doubleRow(crystal) {
            var r = create('tr', {}, false, $('tbody:contains("Item Found")')[0]);
            create('td', {
                width: "10%",
                class: "standardrow",
                align: "center"
            }, "<img src='img-bin/items/" + crystal.name.toLowerCase() + ".png'/>", r);
            create('td', {
                width: "90%",
                class: "standardrow",
                align: "center"
            }, crystal.name + " added to inventory.", r);
        }
        $.ajax({
            url: 'inventory.php',
            async: true,
            success: function(data) {
                var inv = new Inventory(data);
                var drop = inv.getCrystals()[0];
                option(drop, inv.matchCrystal(drop));
                //recursive method - if crystal match, display option to merge - if merged, display result
                //if crystal match with previous result, offer to match again, etc
                function option(crystal, matches) {
                    if (matches.length > 0) {
                        singleRow(matches[0]);
                        $('span.merge').click(function() {
                            $(this).remove();
                            //merge newest crystal [0] with best matching crystal [1]
                            crystal.merge(matches[0],
                                //anonymous function as input for merge callback to display merge option on previously merged crystal
                                function(data) {
                                    var nextInv = new Inventory(data);
                                    var newCrystal = nextInv.getCrystals()[0];
                                    doubleRow(newCrystal);
                                    option(newCrystal, nextInv.matchCrystal(newCrystal));
                                }
                            );
                        });
                    }
                }
            }
        });
    }

    //Build a clickable button which allows the user to attack 
    //the same hunt group again without visiting the hunt page again
    //If there are buttons present, the fight is ongoing
    //If there is a page title, user is on error or multiattack screen
    if (!($('.button').length || $('.pagetitle').length)) {
        //Create Start Another Hunt button & position next to Back to Hunting page url, change row to fit
        //Button defaulted to disabled while waiting for attack string
        var td = $('td:has("#back-to-hunting")');
        td.prop("width", "50%");
        var newCell = create("td", {
                class: "standardrow",
                width: "50%",
                align: "center"
            }, false, td.parent()[0]),
            newButt = create("input", {
                type: "button",
                class: "button",
                value: "Start Another Hunt",
                disabled: "disabled"
            }, false, newCell);
        td.before(newCell);

        //Gather attack string from hunting.php && cookie info for next hunt form
        var form = create("form", {
                action: "hunting3.php",
                method: 'post',
                style: "display:none;"
            }, false, document.body),
            attackString,
            cookies = getCookies();
        create("input", {
            type: "hidden",
            name: "group",
            value: cookies.hunting_group
        }, false, form);
        create("input", {
            type: "hidden",
            name: "level",
            value: cookies.hunting_level
        }, false, form);
        create("input", {
            type: "submit",
            value: "Attack Target",
            class: "button",
            id: "hunt-" + cookies.hunting_group + "-" + cookies.hunting_level
        }, false, form);

        $.ajax({
            url: 'hunting.php',
            async: true,
            success: function(data) {
                attackString = data.match(/([a-zA-Z]{20})(?=">')/g).pop();
                create("input", {
                    type: "hidden",
                    name: "attackstring",
                    value: attackString
                }, false, form);
                newButt.disabled = false;
            }
        });
        //Button method - disable to prevent multi-attack & submit to proceed
        $(newButt).click(function() {
            $(this).prop("disabled", true);
            form.submit();
        });
        //Hunt Group 18 - Crystal Entities and crystal drop
        if (cookies.hunting_group == "18" && $('tbody:contains("Item Found")').length) {
            checkDrop();
        }
    }
}
/*****************************************************
                    Inventory Page Methods                    
/****************************************************/

function invMerge() {
    //Call current inventory as instance of inventory object defined above for managing crystals
    var inv = new Inventory(document.body.innerHTML);

    $('img[title*=" Crystal"]').each(function() {
        //Build selectbox containing matching crystal options & merge button
        var slot = $(this).attr('name');
        var row = create('tr', {}, false, false);
        var col1 = create('td', {
            colspan: '2'
        }, false, row);
        var select = create('select', {
            name: 'item',
            id: 'crystalSelect',
            class: 'selectbox',
            style: 'width:100%;'
        }, false, col1);
        var col2 = create('td', {}, false, row);
        var merge = create('input', {
            id: 'merge',
            type: 'button',
            class: 'button',
            style: 'width:100%',
            value: 'Merge'
        }, false, col2);
        var crystal = inv.getCrystals().reduce(function(a, b) {
            return b.slot == slot ? b : a;
        }, null);
        var matches = inv.matchCrystal(crystal);
        if (matches.length > 0) {
            matches.forEach(function(a) {
                create('option', {
                    value: a.slot
                }, a.trades + ' Trades | ID:' + a.id, select);
            });
        } else {
            create('option', {}, 'No Matching Crystals', select);
            select.disabled = true;
            merge.disabled = true;
        }
        select.selectedIndex = 0;
        //Add selectbox & button after crystal is selected in inventory
        $(this).click(function() {
            $('#ItemPreview tbody tbody:contains("Use")').append(row);
        });
        //Merging!
        $(merge).click(function() {
            //Re-create 'crystal to be merged' input as child of form
            create("input", {
                type: "hidden",
                name: "item",
                value: select.selectedOptions[0].value
            }, false, document.getElementsByTagName('form')[0]);
            //remove unneeded Save Changes input
            $('[name=itemarray]').remove();
            //Override Save Changes form for with merging url
            $('form').attr('action', 'inventory10.php?i=' + crystal.slot + '&key=' + inv.key).submit();
        });
    });
}
/*****************************************************
                    Misc                    
/****************************************************/
//call methods where relevant
switch (location.pathname) {
    case '/inventory.php':
        invMerge();
        break;
    case '/hunting3.php':
        huntMerge();
        break;
}