<commit_before>// ==UserScript==
// @author   Robert Slifka (github @rslifka)
// @connect  docs.google.com
// @connect  rslifka.github.io
// @description Customizations on top of the Destiny Item Manager
// @grant    GM_addStyle
// @grant    GM_log
// @grant    GM_getResourceText
// @grant    GM_xmlhttpRequest
// @homepage https://github.com/rslifka/fate_of_all_fools
// @license   MIT; https://raw.githubusercontent.com/rslifka/fate_of_all_fools/master/LICENSE.txt
// @copyright 2017, Robert Slifka (https://github.com/rslifka/fate_of_all_fools)
// @match    https://*.destinyitemmanager.com/*
// @name     FateOfAllFools - DIM Customization
// @require  http://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js
// @require  https://unpkg.com/tippy.js@1.4.0/dist/tippy.js
// @run-at   document-start
// @supportURL https://github.com/rslifka/fate_of_all_fools/issues
// ==/UserScript==

(function() {
    'use strict';

    /*
        **************************************************************
        C H A N G E   O N L Y   T H E S E   U R L S
        **************************************************************
    */
    const ITEM_DATA_TSVS = [
        'https://docs.google.com/spreadsheets/d/e/2PACX-1vQ06pCDSdvu2nQzgHMXl22ci-6pO9rTTmvZmlKXaiBrIHVhl1X1awIaHEOagZcs4ME4X9ZMEghBP9NE/pub?gid=0&single=true&output=tsv',
        'https://docs.google.com/spreadsheets/d/e/2PACX-1vQ06pCDSdvu2nQzgHMXl22ci-6pO9rTTmvZmlKXaiBrIHVhl1X1awIaHEOagZcs4ME4X9ZMEghBP9NE/pub?gid=945724952&single=true&output=tsv',
        'https://docs.google.com/spreadsheets/d/e/2PACX-1vQ06pCDSdvu2nQzgHMXl22ci-6pO9rTTmvZmlKXaiBrIHVhl1X1awIaHEOagZcs4ME4X9ZMEghBP9NE/pub?gid=1848980798&single=true&output=tsv'
    ];

    const Suitability = {
        YES: 'y',
        NO: 'n',
        UNKNOWN: '?'
    };

    const STATUS_CLASSES = new Map();
    STATUS_CLASSES.set(Suitability.YES, 'foaf-yes');
    STATUS_CLASSES.set(Suitability.NO, 'foaf-no');
    STATUS_CLASSES.set(Suitability.UNKNOWN, 'foaf-unknown');

    const WEAPONS = new Map();

    class Weapon {
        constructor(name, type, subtype, favourite, pveUseful, pvpUseful, comments) {
            this.name = name;
            this.type = type;
            this.subtype = subtype;
            this.favourite = favourite.toLowerCase() === 'y';
            this.comments = (comments === '') ? '(no comments entered)' : comments;
            switch(pveUseful.toLowerCase()) {
                case 'y':
                    this.pveUseful = Suitability.YES;
                    break;
                case 'n':
                    this.pveUseful = Suitability.NO;
                    break;
                default:
                    this.pveUseful = Suitability.UNKNOWN;
            }
            switch(pvpUseful.toLowerCase()) {
                case 'y':
                    this.pvpUseful = Suitability.YES;
                    break;
                case 'n':
                    this.pvpUseful = Suitability.NO;
                    break;
                default:
                    this.pvpUseful = Suitability.UNKNOWN;
            }
        }

        isJunk() {
            return this.pveUseful === Suitability.NO && this.pvpUseful === Suitability.NO;
        }

        isUnknown() {
            return this.pveUseful === Suitability.UNKNOWN && this.pvpUseful == Suitability.UNKNOWN;
        }
    }

    /*
        When there's a full data refresh from the sheet, we do a more aggressive
        reset of the interface elements.
    */
    var dataRefreshed;

    function log(message) {
      GM_log('[FOAF] ' + message);
    }

    /*
        [title] = name of the weapon, which we need to find the weapons, but it
        also does double-duty as a tooltip, which we'd like to customize. Save
        off the name of the weapon so we can always refer to it once we modify
        the [title].
    */
    function saveWeaponNames() {
        ["Kinetic","Energy","Power"].forEach(function(dimWeaponType) {
            $('div[title][drag-channel="'+dimWeaponType+'"]').not('[data-foaf-weapon-name]').each(function(index,element) {
                $(this).attr('data-foaf-weapon-name', $(this).attr('title'));
            });
        });
    }

    /*
        Save off the light level of the weapon up at the top of the tree where
        the weapon is defined, so we can use it for later calculations (e.g. dupe,
        infusion, etc.).
    */
    function saveLightLevel() {
        ["Kinetic","Energy","Power"].forEach(function(dimWeaponType) {
            $('div[data-foaf-weapon-name][drag-channel="'+dimWeaponType+'"]').not('[data-foaf-base-light-level]').each(function(index,element) {
                const modifiedLightLevel = $(this).children('.item-stat').text();
                const isModded = $(this).children('.item-img').hasClass('complete');
                const baseLightLevel = isModded ? modifiedLightLevel-5 : modifiedLightLevel;
                $(this).attr('data-foaf-base-light-level', baseLightLevel);
            });
        });
    }

    // We're replacing DIM's tags with our own
    function clearDIMTags() {
        ["Kinetic","Energy","Power"].forEach(function(dimWeaponType) {
            $("div[drag-channel='"+dimWeaponType+"'] .fa").each(function(index,element) {
                $(this).remove();
            });
            $("div[drag-channel='"+dimWeaponType+"'] .no-tag").each(function(index,element) {
                $(this).remove();
            });
        });
    }

    /*
        Instead of a yellow border to indicate a mod, we're going to add "+M" to the end of
        the item's Power Level. In D1, yellow borders used to indicate that an item was fully
        leveled up, and considering how eventually all items will have legendary mods in them,
        it ends up being visual noise.

        We hang on to the original light as we want to use it later for other calculations.
    */
    function addLegendaryModInfo() {
        $('.item-img.complete').siblings('.item-stat').not('[data-original-light]').each(function(index, element) {
            $(this).attr('data-original-light', $(this).text());
            $(this).text($(this).text()+'+M');
        });
    }

    /*
        Pull more information from our sheet in to the tooltip.
    */
    function tooltipComments() {
        ["Kinetic","Energy","Power"].forEach(function(dimWeaponType) {
            $('div[data-foaf-weapon-name][drag-channel="'+dimWeaponType+'"]').each(function(index,element) {
                const weaponName = $(this).attr('data-foaf-weapon-name');
                if (!WEAPONS.has(weaponName)) {
                    return;
                }

                var weapon = WEAPONS.get(weaponName);
                let tooltipText = weaponName + ' // ' + weapon.type + ' - ' + weapon.subtype + '\n';
                tooltipText += '\n';
                tooltipText += 'PvE: GOOD' + '\n';
                tooltipText += 'PvP: BAD' + '\n';
                tooltipText += '\n';
                tooltipText += 'Comments: \n' + weapon.comments;
                $(this).attr('title', tooltipText);
            });
        });
    }

    /*
        Create a visual representation of the rankings from our Google Sheet.
    */
    function iconifyWeapons() {
        const exclusion = dataRefreshed ? '' : '[data-foaf-tagged]';
        ["Kinetic","Energy","Power"].forEach(function(dimWeaponType) {
            $('div[data-foaf-weapon-name][drag-channel="'+dimWeaponType+'"]').not(exclusion).each(function(index,element) {
                if (dataRefreshed) {
                    $(this).children('.item-tag').remove();
                }

                const weaponName = $(this).attr('data-foaf-weapon-name');
                $(this).attr('data-foaf-tagged', true);

                if (!WEAPONS.has(weaponName)) {
                    $(this).append($("<div>", {"class": "item-tag foaf-question-mark"}));
                    return;
                }

                var weapon = WEAPONS.get(weaponName);
                if (weapon.isJunk()) {
                    $(this).append($("<div>", {"class": "item-tag foaf-thumbs-down"}));
                } else if (weapon.isUnknown()) {
                    $(this).append($("<div>", {"class": "item-tag foaf-question-mark"}));
                } else {
                    if (weapon.pveUseful !== Suitability.UNKNOWN) {
                        $(this).append($("<div>", {"class": "item-tag foaf-pve " + STATUS_CLASSES.get(weapon.pveUseful)}));
                    }
                    if (weapon.pvpUseful !== Suitability.UNKNOWN) {
                        let leftPadding = '';
                        if (weapon.pveUseful === Suitability.UNKNOWN) {
                            leftPadding = 'left:0;';
                        }
                        $(this).append($("<div>", {"class": "item-tag foaf-pvp " + STATUS_CLASSES.get(weapon.pvpUseful), "style": leftPadding}));
                    }
                }
            });
        });
    }

    function indicateDupes() {
        var weapons = new Map();
        ["Kinetic","Energy","Power"].forEach(function(dimWeaponType) {
            $('div[data-foaf-weapon-name][drag-channel="'+dimWeaponType+'"]').each(function(index,element) {
                let weaponName = $(this).attr('data-foaf-weapon-name');
                let weaponData = {
                    name: weaponName,
                    domElement: this,
                    light: $(this).attr('data-foaf-base-light-level')
                };
                if (weapons.has(weaponName)) {
                    weapons.set(weaponName, weapons.get(weaponName).concat(weaponData));
                } else {
                    weapons.set(weaponName, [weaponData]);
                }
            });
        });

        weapons.forEach(function(weaponInstances, key, map) {
            if (weaponInstances.length == 1) {
                return;
            }
            const maxLight = Math.max(...weaponInstances.map(function(w) {return w.light;}));
            weaponInstances.forEach(function(weapon) {
                let dupeDesc = (weapon.light < maxLight) ? ('dupe-lower') : ('dupe');
                let dupeClass = (weapon.light < maxLight) ? ('dupe-lower') : ('dupe-higher');

                // Does this exact element exist already?
                let existingDuperino = $(weapon.domElement).children(".dupe-stat."+dupeClass+":contains('"+dupeDesc+"')");
                if (existingDuperino.length > 0) {
                    return;
                }

                $(weapon.domElement).remove('.dupe-stat');
                $(weapon.domElement).append($("<div>", {"class": "dupe-stat " + dupeClass}).text(dupeDesc));

                $(weapon.domElement).hover(function() {
                    ["Kinetic","Energy","Power"].forEach(function(dimWeaponType) {
                        $('div[data-foaf-weapon-name][drag-channel="'+dimWeaponType+'"]').not('[data-foaf-weapon-name="'+weapon.name+'"]').addClass('search-hidden');
                    });
                },function() {
                    $('.search-hidden').removeClass('search-hidden');
                });
            });
        });
    }

    function refresh() {
        if (dataRefreshed) {
            log('Refreshing (full)...');
        } else {
            log('Refreshing (partial)...');
        }

        log('  Saving weapon names...');
        saveWeaponNames();
        log('  Saving light levels...');
        saveLightLevel();
        log('  Clearing DIM weapon item tags...');
        clearDIMTags();
        log('  Enhancing tooltips...');
        tooltipComments();
        log('  Adding mod indicator...');
        addLegendaryModInfo();
        log('  Adding icons to weapons...');
        iconifyWeapons();
        log('  Dupify!...');
        indicateDupes();

        dataRefreshed = false;
        log('Done!');
    }

    function itemsAreLoaded() {
        var deferredReady = $.Deferred();
        setInterval(function() {
            if ($('.item-img').length > 0) {
                deferredReady.resolve();
            }
        }, 1000);
        return deferredReady.promise();
    }

    function rankingsDownloaded(tsvUrl) {
        var deferredReady = $.Deferred();
        GM_xmlhttpRequest({
            method: 'GET',
            url: tsvUrl,
            context: deferredReady,
            onload: function(response) {
                log('Processing collection: "'+tsvUrl+'"');

                var dataLines = response.responseText.split(/[\r\n]+/);
                log('Found ('+(dataLines.length-1)+') weapons');

                for (var i = 1; i < dataLines.length; i++) {
                    // We've split by TAB because no weapon names have tabs in them
                    var data = dataLines[i].split('\t');
                    // log('Examining ' + data);

                    // Name=0,Type,Subtype,Personal Fave?,PvE, PvP, Comments
                    WEAPONS.set(data[0], new Weapon(data[0], data[1], data[2], data[3], data[4], data[5], data[6]));
                }

                deferredReady.resolve();
            }
        });
        return deferredReady.promise();
    }

    log('Applying CSS...');
    [
        'https://rslifka.github.io/fate_of_all_fools/css/fateofallfools.css',
        'https://rslifka.github.io/fate_of_all_fools/css/overrides.css'
    ].forEach(function(cssPath) {
        log('Downloading style: '+cssPath);
        GM_xmlhttpRequest({
            method: 'GET',
            url: cssPath,
            onload: function(response) {
                log('Done! Installing...');
                GM_addStyle(response.responseText);
            }
        });
    });

    /*
        Pull down new weapon data at a separate interval than the refresh timer.
        The idea is that you're updating this sheet on a much longer interval
        than you're dragging items around (which requires a periodic refresh to
        re-apply our styles).

        That being said, chances are when you do make an update, you'd like to
        see the change sooner than later, so we'll use a time conducive to
        interactivity.
    */
    log('Initializing data refresh timer...');
    setInterval(function() {
        $.when(
            rankingsDownloaded(ITEM_DATA_TSVS[0]),
            rankingsDownloaded(ITEM_DATA_TSVS[1]),
            rankingsDownloaded(ITEM_DATA_TSVS[2])
        ).then(function() {
            log('Data refresh complete!');
            dataRefreshed = true;
        });
    }, 30000);

    log('Initialized, waiting for items to appear...');
    $.when(
        itemsAreLoaded(),
        rankingsDownloaded(ITEM_DATA_TSVS[0]),
        rankingsDownloaded(ITEM_DATA_TSVS[1]),
        rankingsDownloaded(ITEM_DATA_TSVS[2])
    ).then(function() {
        dataRefreshed = true;
        refresh();
        setInterval(function() {
            refresh();
        }, 5000);
    });
})();
<commit_msg>Save off weapon type for infusion calculation<commit_after>// ==UserScript==
// @author   Robert Slifka (github @rslifka)
// @connect  docs.google.com
// @connect  rslifka.github.io
// @description Customizations on top of the Destiny Item Manager
// @grant    GM_addStyle
// @grant    GM_log
// @grant    GM_getResourceText
// @grant    GM_xmlhttpRequest
// @homepage https://github.com/rslifka/fate_of_all_fools
// @license   MIT; https://raw.githubusercontent.com/rslifka/fate_of_all_fools/master/LICENSE.txt
// @copyright 2017, Robert Slifka (https://github.com/rslifka/fate_of_all_fools)
// @match    https://*.destinyitemmanager.com/*
// @name     FateOfAllFools - DIM Customization
// @require  http://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js
// @require  https://unpkg.com/tippy.js@1.4.0/dist/tippy.js
// @run-at   document-start
// @supportURL https://github.com/rslifka/fate_of_all_fools/issues
// ==/UserScript==

(function() {
    'use strict';

    /*
        **************************************************************
        C H A N G E   O N L Y   T H E S E   U R L S
        **************************************************************
    */
    const ITEM_DATA_TSVS = [
        'https://docs.google.com/spreadsheets/d/e/2PACX-1vQ06pCDSdvu2nQzgHMXl22ci-6pO9rTTmvZmlKXaiBrIHVhl1X1awIaHEOagZcs4ME4X9ZMEghBP9NE/pub?gid=0&single=true&output=tsv',
        'https://docs.google.com/spreadsheets/d/e/2PACX-1vQ06pCDSdvu2nQzgHMXl22ci-6pO9rTTmvZmlKXaiBrIHVhl1X1awIaHEOagZcs4ME4X9ZMEghBP9NE/pub?gid=945724952&single=true&output=tsv',
        'https://docs.google.com/spreadsheets/d/e/2PACX-1vQ06pCDSdvu2nQzgHMXl22ci-6pO9rTTmvZmlKXaiBrIHVhl1X1awIaHEOagZcs4ME4X9ZMEghBP9NE/pub?gid=1848980798&single=true&output=tsv'
    ];

    const Suitability = {
        YES: 'y',
        NO: 'n',
        UNKNOWN: '?'
    };

    const STATUS_CLASSES = new Map();
    STATUS_CLASSES.set(Suitability.YES, 'foaf-yes');
    STATUS_CLASSES.set(Suitability.NO, 'foaf-no');
    STATUS_CLASSES.set(Suitability.UNKNOWN, 'foaf-unknown');

    const WEAPONS = new Map();

    class Weapon {
        constructor(name, type, subtype, favourite, pveUseful, pvpUseful, comments) {
            this.name = name;
            this.type = type;
            this.subtype = subtype;
            this.favourite = favourite.toLowerCase() === 'y';
            this.comments = (comments === '') ? '(no comments entered)' : comments;
            switch(pveUseful.toLowerCase()) {
                case 'y':
                    this.pveUseful = Suitability.YES;
                    break;
                case 'n':
                    this.pveUseful = Suitability.NO;
                    break;
                default:
                    this.pveUseful = Suitability.UNKNOWN;
            }
            switch(pvpUseful.toLowerCase()) {
                case 'y':
                    this.pvpUseful = Suitability.YES;
                    break;
                case 'n':
                    this.pvpUseful = Suitability.NO;
                    break;
                default:
                    this.pvpUseful = Suitability.UNKNOWN;
            }
        }

        isJunk() {
            return this.pveUseful === Suitability.NO && this.pvpUseful === Suitability.NO;
        }

        isUnknown() {
            return this.pveUseful === Suitability.UNKNOWN && this.pvpUseful == Suitability.UNKNOWN;
        }
    }

    /*
        When there's a full data refresh from the sheet, we do a more aggressive
        reset of the interface elements.
    */
    var dataRefreshed;

    function log(message) {
      GM_log('[FOAF] ' + message);
    }

    /*
        [title] = name of the weapon, which we need to find the weapons, but it
        also does double-duty as a tooltip, which we'd like to customize. Save
        off the name of the weapon so we can always refer to it once we modify
        the [title].
    */
    function saveWeaponNames() {
        ["Kinetic","Energy","Power"].forEach(function(dimWeaponType) {
            $('div[title][drag-channel="'+dimWeaponType+'"]').not('[data-foaf-weapon-name]').each(function(index,element) {
                $(this).attr('data-foaf-weapon-name', $(this).attr('title'));
            });
        });
    }

    /*
        The weapon type never changes, and we need it for infusion calculation,
        so let's drop it at the weapon level.
    */
    function saveWeaponTypes() {
        ["Kinetic","Energy","Power"].forEach(function(dimWeaponType) {
            $('div[title][drag-channel="'+dimWeaponType+'"]').not('[data-foaf-weapon-type]').each(function(index,element) {
                const weaponName = $(this).attr('data-foaf-weapon-name');
                if (!WEAPONS.has(weaponName)) {
                    return;
                }
                $(this).attr('data-foaf-weapon-type', WEAPONS.get(weaponName).type);
            });
        });
    }

    /*
        Save off the light level of the weapon up at the top of the tree where
        the weapon is defined, so we can use it for later calculations (e.g. dupe,
        infusion, etc.).
    */
    function saveLightLevel() {
        ["Kinetic","Energy","Power"].forEach(function(dimWeaponType) {
            $('div[data-foaf-weapon-name][drag-channel="'+dimWeaponType+'"]').not('[data-foaf-base-light-level]').each(function(index,element) {
                const modifiedLightLevel = $(this).children('.item-stat').text();
                const isModded = $(this).children('.item-img').hasClass('complete');
                const baseLightLevel = isModded ? modifiedLightLevel-5 : modifiedLightLevel;
                $(this).attr('data-foaf-base-light-level', baseLightLevel);
            });
        });
    }

    // We're replacing DIM's tags with our own
    function clearDIMTags() {
        ["Kinetic","Energy","Power"].forEach(function(dimWeaponType) {
            $("div[drag-channel='"+dimWeaponType+"'] .fa").each(function(index,element) {
                $(this).remove();
            });
            $("div[drag-channel='"+dimWeaponType+"'] .no-tag").each(function(index,element) {
                $(this).remove();
            });
        });
    }

    /*
        Instead of a yellow border to indicate a mod, we're going to add "+M" to the end of
        the item's Power Level. In D1, yellow borders used to indicate that an item was fully
        leveled up, and considering how eventually all items will have legendary mods in them,
        it ends up being visual noise.

        We hang on to the original light as we want to use it later for other calculations.
    */
    function addLegendaryModInfo() {
        $('.item-img.complete').siblings('.item-stat').not('[data-original-light]').each(function(index, element) {
            $(this).attr('data-original-light', $(this).text());
            $(this).text($(this).text()+'+M');
        });
    }

    /*
        Pull more information from our sheet in to the tooltip.
    */
    function tooltipComments() {
        ["Kinetic","Energy","Power"].forEach(function(dimWeaponType) {
            $('div[data-foaf-weapon-name][drag-channel="'+dimWeaponType+'"]').each(function(index,element) {
                const weaponName = $(this).attr('data-foaf-weapon-name');
                if (!WEAPONS.has(weaponName)) {
                    return;
                }

                var weapon = WEAPONS.get(weaponName);
                let tooltipText = weaponName + ' // ' + weapon.type + ' - ' + weapon.subtype + '\n';
                tooltipText += '\n';
                tooltipText += 'PvE: GOOD' + '\n';
                tooltipText += 'PvP: BAD' + '\n';
                tooltipText += '\n';
                tooltipText += 'Comments: \n' + weapon.comments;
                $(this).attr('title', tooltipText);
            });
        });
    }

    /*
        Create a visual representation of the rankings from our Google Sheet.
    */
    function iconifyWeapons() {
        const exclusion = dataRefreshed ? '' : '[data-foaf-tagged]';
        ["Kinetic","Energy","Power"].forEach(function(dimWeaponType) {
            $('div[data-foaf-weapon-name][drag-channel="'+dimWeaponType+'"]').not(exclusion).each(function(index,element) {
                if (dataRefreshed) {
                    $(this).children('.item-tag').remove();
                }

                const weaponName = $(this).attr('data-foaf-weapon-name');
                $(this).attr('data-foaf-tagged', true);

                if (!WEAPONS.has(weaponName)) {
                    $(this).append($("<div>", {"class": "item-tag foaf-question-mark"}));
                    return;
                }

                var weapon = WEAPONS.get(weaponName);
                if (weapon.isJunk()) {
                    $(this).append($("<div>", {"class": "item-tag foaf-thumbs-down"}));
                } else if (weapon.isUnknown()) {
                    $(this).append($("<div>", {"class": "item-tag foaf-question-mark"}));
                } else {
                    if (weapon.pveUseful !== Suitability.UNKNOWN) {
                        $(this).append($("<div>", {"class": "item-tag foaf-pve " + STATUS_CLASSES.get(weapon.pveUseful)}));
                    }
                    if (weapon.pvpUseful !== Suitability.UNKNOWN) {
                        let leftPadding = '';
                        if (weapon.pveUseful === Suitability.UNKNOWN) {
                            leftPadding = 'left:0;';
                        }
                        $(this).append($("<div>", {"class": "item-tag foaf-pvp " + STATUS_CLASSES.get(weapon.pvpUseful), "style": leftPadding}));
                    }
                }
            });
        });
    }

    function indicateDupes() {
        var weapons = new Map();
        ["Kinetic","Energy","Power"].forEach(function(dimWeaponType) {
            $('div[data-foaf-weapon-name][drag-channel="'+dimWeaponType+'"]').each(function(index,element) {
                let weaponName = $(this).attr('data-foaf-weapon-name');
                let weaponData = {
                    name: weaponName,
                    domElement: this,
                    light: $(this).attr('data-foaf-base-light-level')
                };
                if (weapons.has(weaponName)) {
                    weapons.set(weaponName, weapons.get(weaponName).concat(weaponData));
                } else {
                    weapons.set(weaponName, [weaponData]);
                }
            });
        });

        weapons.forEach(function(weaponInstances, key, map) {
            if (weaponInstances.length == 1) {
                return;
            }
            const maxLight = Math.max(...weaponInstances.map(function(w) {return w.light;}));
            weaponInstances.forEach(function(weapon) {
                let dupeDesc = (weapon.light < maxLight) ? ('dupe-lower') : ('dupe');
                let dupeClass = (weapon.light < maxLight) ? ('dupe-lower') : ('dupe-higher');

                // Does this exact element exist already?
                let existingDuperino = $(weapon.domElement).children(".dupe-stat."+dupeClass+":contains('"+dupeDesc+"')");
                if (existingDuperino.length > 0) {
                    return;
                }

                $(weapon.domElement).remove('.dupe-stat');
                $(weapon.domElement).append($("<div>", {"class": "dupe-stat " + dupeClass}).text(dupeDesc));

                $(weapon.domElement).hover(function() {
                    ["Kinetic","Energy","Power"].forEach(function(dimWeaponType) {
                        $('div[data-foaf-weapon-name][drag-channel="'+dimWeaponType+'"]').not('[data-foaf-weapon-name="'+weapon.name+'"]').addClass('search-hidden');
                    });
                },function() {
                    $('.search-hidden').removeClass('search-hidden');
                });
            });
        });
    }

    function refresh() {
        if (dataRefreshed) {
            log('Refreshing (full)...');
        } else {
            log('Refreshing (partial)...');
        }

        log('  Saving weapon names...');
        saveWeaponNames();
        log('  Saving weapon types...');
        saveWeaponTypes();
        log('  Saving light levels...');
        saveLightLevel();
        log('  Clearing DIM weapon item tags...');
        clearDIMTags();
        log('  Enhancing tooltips...');
        tooltipComments();
        log('  Adding mod indicator...');
        addLegendaryModInfo();
        log('  Adding icons to weapons...');
        iconifyWeapons();
        log('  Dupify!...');
        indicateDupes();

        dataRefreshed = false;
        log('Done!');
    }

    function itemsAreLoaded() {
        var deferredReady = $.Deferred();
        setInterval(function() {
            if ($('.item-img').length > 0) {
                deferredReady.resolve();
            }
        }, 1000);
        return deferredReady.promise();
    }

    function rankingsDownloaded(tsvUrl) {
        var deferredReady = $.Deferred();
        GM_xmlhttpRequest({
            method: 'GET',
            url: tsvUrl,
            context: deferredReady,
            onload: function(response) {
                log('Processing collection: "'+tsvUrl+'"');

                var dataLines = response.responseText.split(/[\r\n]+/);
                log('Found ('+(dataLines.length-1)+') weapons');

                for (var i = 1; i < dataLines.length; i++) {
                    // We've split by TAB because no weapon names have tabs in them
                    var data = dataLines[i].split('\t');
                    // log('Examining ' + data);

                    // Name=0,Type,Subtype,Personal Fave?,PvE, PvP, Comments
                    WEAPONS.set(data[0], new Weapon(data[0], data[1], data[2], data[3], data[4], data[5], data[6]));
                }

                deferredReady.resolve();
            }
        });
        return deferredReady.promise();
    }

    log('Applying CSS...');
    [
        'https://rslifka.github.io/fate_of_all_fools/css/fateofallfools.css',
        'https://rslifka.github.io/fate_of_all_fools/css/overrides.css'
    ].forEach(function(cssPath) {
        log('Downloading style: '+cssPath);
        GM_xmlhttpRequest({
            method: 'GET',
            url: cssPath,
            onload: function(response) {
                log('Done! Installing...');
                GM_addStyle(response.responseText);
            }
        });
    });

    /*
        Pull down new weapon data at a separate interval than the refresh timer.
        The idea is that you're updating this sheet on a much longer interval
        than you're dragging items around (which requires a periodic refresh to
        re-apply our styles).

        That being said, chances are when you do make an update, you'd like to
        see the change sooner than later, so we'll use a time conducive to
        interactivity.
    */
    log('Initializing data refresh timer...');
    setInterval(function() {
        $.when(
            rankingsDownloaded(ITEM_DATA_TSVS[0]),
            rankingsDownloaded(ITEM_DATA_TSVS[1]),
            rankingsDownloaded(ITEM_DATA_TSVS[2])
        ).then(function() {
            log('Data refresh complete!');
            dataRefreshed = true;
        });
    }, 30000);

    log('Initialized, waiting for items to appear...');
    $.when(
        itemsAreLoaded(),
        rankingsDownloaded(ITEM_DATA_TSVS[0]),
        rankingsDownloaded(ITEM_DATA_TSVS[1]),
        rankingsDownloaded(ITEM_DATA_TSVS[2])
    ).then(function() {
        dataRefreshed = true;
        refresh();
        setInterval(function() {
            refresh();
        }, 5000);
    });
})();
<|endoftext|>


<commit_before>var scrollView = new tabris.ScrollView({left: 0, top: 0, right: 0, bottom: 0}).appendTo(tabris.ui.contentView);

const COUNTRIES = ['Germany', 'Canada', 'USA', 'Bulgaria'];
const CLASSES = ['Business', 'Economy', 'Economy Plus'];

new tabris.TextView({
  id: 'nameLabel',
  alignment: 'left',
  text: 'Name:'
}).appendTo(scrollView);

new tabris.TextInput({
  id: 'nameInput',
  message: 'Full Name'
}).appendTo(scrollView);

new tabris.TextView({
  id: 'flyerNumberLabel',
  text: 'Flyer Number:'
}).appendTo(scrollView);

new tabris.TextInput({
  id: 'flyerNumberInput',
  keyboard: 'number',
  message: 'Flyer Number'
}).appendTo(scrollView);

new tabris.TextView({
  id: 'passphraseLabel',
  text: 'Passphrase:'
}).appendTo(scrollView);

new tabris.TextInput({
  id: 'passphraseInput',
  type: 'password',
  message: 'Passphrase'
}).appendTo(scrollView);

new tabris.TextView({
  id: 'countryLabel',
  text: 'Country:'
}).appendTo(scrollView);

new tabris.Picker({
  id: 'countryPicker',
  itemCount: COUNTRIES.length,
  itemText: index => COUNTRIES[index]
}).appendTo(scrollView);

new tabris.TextView({
  id: 'classLabel',
  text: 'Class:'
}).appendTo(scrollView);

new tabris.Picker({
  id: 'classPicker',
  itemCount: CLASSES.length,
  itemText: index => CLASSES[index]
}).appendTo(scrollView);

new tabris.TextView({
  id: 'seatLabel',
  text: 'Seat:'
}).appendTo(scrollView);

new tabris.RadioButton({
  id: 'windowSeat',
  text: 'Window'
}).appendTo(scrollView);

new tabris.RadioButton({
  id: 'aisleSeat',
  text: 'Aisle'
}).appendTo(scrollView);

new tabris.RadioButton({
  id: 'anySeat',
  text: "Don't care",
  checked: true
}).appendTo(scrollView);

new tabris.Composite({
  id: 'luggagePanel'
}).append(
  new tabris.TextView({
    id: 'luggageLabel',
    text: 'Luggage:'
  })
).append(
  new tabris.TextView({
    id: 'luggageWeight',
    text: '0 Kg'
  })
).append(
  new tabris.Slider({
    id: 'luggageSlider'
  }).on('selectionChanged', ({value}) => {
    scrollView.find('#luggageWeight').set('text', value + ' Kg');
  })
).appendTo(scrollView);

new tabris.CheckBox({
  id: 'veggieChoice',
  text: 'Vegetarian'
}).appendTo(scrollView);

new tabris.Composite({
  id: 'milesPanel'
}).append(
  new tabris.TextView({
    id: 'milesLabel',
    text: 'Redeem miles:'
  })
).append(
  new tabris.Switch({
    id: 'milesSwitch'
  })
).appendTo(scrollView);

new tabris.Button({
  id: 'reservationButton',
  text: 'Place Reservation',
  background: '#8b0000',
  textColor: 'white'
}).on('select', () => {
  updateMessage();
}).appendTo(scrollView);

let message = new tabris.TextView({
  left: 10, right: 10, top: '#reservationButton 10'
}).appendTo(scrollView);

scrollView.apply({
  '#nameLabel': {left: 10, top: 18, width: 120},
  '#nameInput': {left: '#nameLabel 10', right: 10, baseline: '#nameLabel'},
  '#flyerNumberLabel': {left: 10, top: '#nameLabel 18', width: 120},
  '#flyerNumberInput': {left: '#flyerNumberLabel 10', right: 10, baseline: '#flyerNumberLabel'},
  '#passphraseLabel': {left: 10, top: '#flyerNumberLabel 18', width: 120},
  '#passphraseInput': {left: '#passphraseLabel 10', right: 10, baseline: '#passphraseLabel'},
  '#countryLabel': {left: 10, top: '#passphraseLabel 18', width: 120},
  '#countryPicker': {left: '#countryLabel 10', right: 10, baseline: '#countryLabel'},
  '#seatLabel': {left: 10, top: '#classLabel 18', width: 120},
  '#windowSeat': {left: '#seatLabel 10', right: 10, baseline: '#seatLabel'},
  '#aisleSeat': {left: '#seatLabel 10', right: 10, top: '#seatLabel 10'},
  '#classLabel': {left: 10, top: '#countryLabel 18', width: 120},
  '#classPicker': {left: '#classLabel 10', right: 10, baseline: '#classLabel'},
  '#anySeat': {left: '#seatLabel 10', right: 10, top: '#aisleSeat 10'},
  '#luggagePanel': {left: 10, top: '#anySeat 18', right: 10},
  '#luggageLabel': {left: 0, centerY: 0, width: 120},
  '#luggageWeight': {right: 10, centerY: 0, width: 50},
  '#luggageSlider': {left: '#luggageLabel 10', right: '#luggageWeight 10', centerY: 0},
  '#veggieChoice': {left: '#seatLabel 10', right: 10, top: '#luggagePanel 10'},
  '#milesPanel': {left: 10, top: '#veggieChoice 10', right: 10},
  '#milesLabel': {left: 0, centerY: 0, width: 120},
  '#milesSwitch': {left: '#milesLabel 10', centerY: 0},
  '#reservationButton': {left: 10, right: 10, top: '#milesPanel 18'}
});

function updateMessage() {
  message.text = [
    'Flight booked for: ' + scrollView.children('#nameInput').first().text,
    'Destination: ' + COUNTRIES[scrollView.children('#countryPicker').first().selectionIndex],
    'Seating: ' + createSeating(),
    'Luggage: ' + createWeight(),
    'Meal: ' + createMeal(),
    'Redeem miles: ' + createFrequentFlyerInfo()
  ].join('\n') + '\n';
}

function createSeating() {
  var seating = 'Anywhere';
  scrollView.children('RadioButton').forEach((button) => {
    if (button.checked) {
      seating = button.text;
    }
  });
  seating += ', ' + CLASSES[scrollView.children('#classPicker').first().selectionIndex];
  return seating;
}

function createWeight() {
  var panel = scrollView.children('#luggagePanel');
  return panel.children('#luggageSlider').first().selection + ' Kg';
}

function createMeal() {
  return scrollView.children('#veggieChoice').checked ? 'Vegetarian' : 'Standard';
}

function createFrequentFlyerInfo() {
  var panel = scrollView.children('#milesPanel');
  var info = panel.children('#milesSwitch').first().checked ? 'Yes' : 'No';
  info += ', acct: ' + scrollView.children('#flyerNumberInput').first().text;
  return info;
}
<commit_msg>Modernize input example<commit_after>const {
  Button, CheckBox, Composite, TextView, TextInput, Picker, RadioButton, ScrollView, Slider, Switch, ui
} = require('tabris');

let scrollView = new ScrollView({left: 0, top: 0, right: 0, bottom: 0}).appendTo(ui.contentView);

const COUNTRIES = ['Germany', 'Canada', 'USA', 'Bulgaria'];
const CLASSES = ['Business', 'Economy', 'Economy Plus'];

new TextView({
  id: 'nameLabel',
  alignment: 'left',
  text: 'Name:'
}).appendTo(scrollView);

new TextInput({
  id: 'nameInput',
  message: 'Full Name'
}).appendTo(scrollView);

new TextView({
  id: 'flyerNumberLabel',
  text: 'Flyer Number:'
}).appendTo(scrollView);

new TextInput({
  id: 'flyerNumberInput',
  keyboard: 'number',
  message: 'Flyer Number'
}).appendTo(scrollView);

new TextView({
  id: 'passphraseLabel',
  text: 'Passphrase:'
}).appendTo(scrollView);

new TextInput({
  id: 'passphraseInput',
  type: 'password',
  message: 'Passphrase'
}).appendTo(scrollView);

new TextView({
  id: 'countryLabel',
  text: 'Country:'
}).appendTo(scrollView);

new Picker({
  id: 'countryPicker',
  itemCount: COUNTRIES.length,
  itemText: index => COUNTRIES[index]
}).appendTo(scrollView);

new TextView({
  id: 'classLabel',
  text: 'Class:'
}).appendTo(scrollView);

new Picker({
  id: 'classPicker',
  itemCount: CLASSES.length,
  itemText: index => CLASSES[index]
}).appendTo(scrollView);

new TextView({
  id: 'seatLabel',
  text: 'Seat:'
}).appendTo(scrollView);

new RadioButton({
  id: 'windowSeat',
  text: 'Window'
}).appendTo(scrollView);

new RadioButton({
  id: 'aisleSeat',
  text: 'Aisle'
}).appendTo(scrollView);

new RadioButton({
  id: 'anySeat',
  text: "Don't care",
  checked: true
}).appendTo(scrollView);

new Composite({
  id: 'luggagePanel'
}).append(
  new TextView({
    id: 'luggageLabel',
    text: 'Luggage:'
  })
).append(
  new TextView({
    id: 'luggageWeight',
    text: '0 Kg'
  })
).append(
  new Slider({
    id: 'luggageSlider'
  }).on('selectionChanged', ({value}) => {
    scrollView.find('#luggageWeight').set('text', value + ' Kg');
  })
).appendTo(scrollView);

new CheckBox({
  id: 'veggieChoice',
  text: 'Vegetarian'
}).appendTo(scrollView);

new Composite({
  id: 'milesPanel'
}).append(
  new TextView({
    id: 'milesLabel',
    text: 'Redeem miles:'
  })
).append(
  new Switch({
    id: 'milesSwitch'
  })
).appendTo(scrollView);

new Button({
  id: 'reservationButton',
  text: 'Place Reservation',
  background: '#8b0000',
  textColor: 'white'
}).on('select', () => {
  updateMessage();
}).appendTo(scrollView);

let message = new TextView({
  left: 10, right: 10, top: '#reservationButton 10'
}).appendTo(scrollView);

scrollView.apply({
  '#nameLabel': {left: 10, top: 18, width: 120},
  '#nameInput': {left: '#nameLabel 10', right: 10, baseline: '#nameLabel'},
  '#flyerNumberLabel': {left: 10, top: '#nameLabel 18', width: 120},
  '#flyerNumberInput': {left: '#flyerNumberLabel 10', right: 10, baseline: '#flyerNumberLabel'},
  '#passphraseLabel': {left: 10, top: '#flyerNumberLabel 18', width: 120},
  '#passphraseInput': {left: '#passphraseLabel 10', right: 10, baseline: '#passphraseLabel'},
  '#countryLabel': {left: 10, top: '#passphraseLabel 18', width: 120},
  '#countryPicker': {left: '#countryLabel 10', right: 10, baseline: '#countryLabel'},
  '#seatLabel': {left: 10, top: '#classLabel 18', width: 120},
  '#windowSeat': {left: '#seatLabel 10', right: 10, baseline: '#seatLabel'},
  '#aisleSeat': {left: '#seatLabel 10', right: 10, top: '#seatLabel 10'},
  '#classLabel': {left: 10, top: '#countryLabel 18', width: 120},
  '#classPicker': {left: '#classLabel 10', right: 10, baseline: '#classLabel'},
  '#anySeat': {left: '#seatLabel 10', right: 10, top: '#aisleSeat 10'},
  '#luggagePanel': {left: 10, top: '#anySeat 18', right: 10},
  '#luggageLabel': {left: 0, centerY: 0, width: 120},
  '#luggageWeight': {right: 10, centerY: 0, width: 50},
  '#luggageSlider': {left: '#luggageLabel 10', right: '#luggageWeight 10', centerY: 0},
  '#veggieChoice': {left: '#seatLabel 10', right: 10, top: '#luggagePanel 10'},
  '#milesPanel': {left: 10, top: '#veggieChoice 10', right: 10},
  '#milesLabel': {left: 0, centerY: 0, width: 120},
  '#milesSwitch': {left: '#milesLabel 10', centerY: 0},
  '#reservationButton': {left: 10, right: 10, top: '#milesPanel 18'}
});

function updateMessage() {
  message.text = [
    'Flight booked for: ' + scrollView.children('#nameInput').first().text,
    'Destination: ' + COUNTRIES[scrollView.children('#countryPicker').first().selectionIndex],
    'Seating: ' + createSeating(),
    'Luggage: ' + createWeight(),
    'Meal: ' + createMeal(),
    'Redeem miles: ' + createFrequentFlyerInfo()
  ].join('\n') + '\n';
}

function createSeating() {
  let seating = 'Anywhere';
  scrollView.children('RadioButton').forEach((button) => {
    if (button.checked) {
      seating = button.text;
    }
  });
  seating += ', ' + CLASSES[scrollView.children('#classPicker').first().selectionIndex];
  return seating;
}

function createWeight() {
  let panel = scrollView.children('#luggagePanel');
  return panel.children('#luggageSlider').first().selection + ' Kg';
}

function createMeal() {
  return scrollView.children('#veggieChoice').checked ? 'Vegetarian' : 'Standard';
}

function createFrequentFlyerInfo() {
  let panel = scrollView.children('#milesPanel');
  let info = panel.children('#milesSwitch').first().checked ? 'Yes' : 'No';
  info += ', acct: ' + scrollView.children('#flyerNumberInput').first().text;
  return info;
}
<|endoftext|>


<commit_before>var classes = [
  'foam.core.Serializable',
  'foam.mlang.predicate.Predicate',
  'foam.mlang.predicate.True',
  'foam.mlang.predicate.False',
  'foam.mlang.predicate.And',
  'foam.mlang.predicate.Gt',
  'foam.mlang.predicate.Or',
  'foam.mlang.predicate.AbstractPredicate',
  'foam.mlang.predicate.Nary',
  'foam.mlang.predicate.Unary',
  'foam.mlang.predicate.Binary',
  'foam.mlang.predicate.Contains',
  'foam.mlang.predicate.StartsWithIC',
  'foam.mlang.predicate.Gt',
  'foam.mlang.predicate.Gte',
  'foam.mlang.predicate.Lt',
  'foam.mlang.predicate.Lte',
  'foam.mlang.Expr',
  'foam.mlang.AbstractExpr',
  'foam.mlang.predicate.Eq',
  'foam.mlang.Constant',
  'foam.box.Box',
  'foam.box.ProxyBox',
  'foam.box.SubBox',
  'foam.box.Message',
  'foam.box.SubBoxMessage',
  'foam.box.SubscribeMessage',
  'com.google.foam.demos.appengine.TestModel',
  'foam.box.HTTPReplyBox',
  'com.google.foam.demos.appengine.TestService',
  'com.google.foam.demos.heroes.Hero',
  'com.google.auth.TokenVerifier',
  'foam.box.RPCMessage',
  'foam.box.RPCReturnMessage',
  'foam.box.BoxRegistry',
  'foam.box.BoxRegistryBox',
  'foam.box.CheckAuthenticationBox',
  'foam.dao.DAO',
  'foam.dao.Sink',
  'foam.dao.AbstractSink',
  'foam.dao.PredicatedSink',
  'foam.dao.OrderedSink',
  'foam.dao.LimitedSink',
  'foam.dao.SkipSink',
  'foam.dao.RelationshipPropertyValue',
  'foam.mlang.order.Comparator',
  'foam.mlang.sink.Count',
  'foam.nanos.boot.NSpec',
  'foam.nanos.auth.EnabledAware',
  'foam.nanos.auth.Group',
  'foam.nanos.auth.LastModifiedAware',
  'foam.nanos.auth.LastModifiedByAware',
  'foam.nanos.auth.Permission',
  'foam.nanos.auth.User',
	'foam.nanos.auth.AuthService'
  'foam.nanos.pm.PMInfo'
];

var abstractClasses = [
//  'foam.json.Outputer'
];

var skeletons = [
  'com.google.foam.demos.appengine.TestService',
  'foam.dao.DAO'
];

var proxies = [
  'foam.dao.DAO',
  'foam.dao.Sink',
  'com.google.foam.demos.appengine.TestService'
];

module.exports = {
    classes: classes,
    abstractClasses: abstractClasses,
    skeletons: skeletons,
    proxies: proxies
}
<commit_msg>Added Auth Service model<commit_after>var classes = [
  'foam.core.Serializable',
  'foam.mlang.predicate.Predicate',
  'foam.mlang.predicate.True',
  'foam.mlang.predicate.False',
  'foam.mlang.predicate.And',
  'foam.mlang.predicate.Gt',
  'foam.mlang.predicate.Or',
  'foam.mlang.predicate.AbstractPredicate',
  'foam.mlang.predicate.Nary',
  'foam.mlang.predicate.Unary',
  'foam.mlang.predicate.Binary',
  'foam.mlang.predicate.Contains',
  'foam.mlang.predicate.StartsWithIC',
  'foam.mlang.predicate.Gt',
  'foam.mlang.predicate.Gte',
  'foam.mlang.predicate.Lt',
  'foam.mlang.predicate.Lte',
  'foam.mlang.Expr',
  'foam.mlang.AbstractExpr',
  'foam.mlang.predicate.Eq',
  'foam.mlang.Constant',
  'foam.box.Box',
  'foam.box.ProxyBox',
  'foam.box.SubBox',
  'foam.box.Message',
  'foam.box.SubBoxMessage',
  'foam.box.SubscribeMessage',
  'com.google.foam.demos.appengine.TestModel',
  'foam.box.HTTPReplyBox',
  'com.google.foam.demos.appengine.TestService',
  'com.google.foam.demos.heroes.Hero',
  'com.google.auth.TokenVerifier',
  'foam.box.RPCMessage',
  'foam.box.RPCReturnMessage',
  'foam.box.BoxRegistry',
  'foam.box.BoxRegistryBox',
  'foam.box.CheckAuthenticationBox',
  'foam.dao.DAO',
  'foam.dao.Sink',
  'foam.dao.AbstractSink',
  'foam.dao.PredicatedSink',
  'foam.dao.OrderedSink',
  'foam.dao.LimitedSink',
  'foam.dao.SkipSink',
  'foam.dao.RelationshipPropertyValue',
  'foam.mlang.order.Comparator',
  'foam.mlang.sink.Count',
  'foam.nanos.boot.NSpec',
  'foam.nanos.auth.EnabledAware',
  'foam.nanos.auth.Group',
  'foam.nanos.auth.LastModifiedAware',
  'foam.nanos.auth.LastModifiedByAware',
  'foam.nanos.auth.Permission',
  'foam.nanos.auth.User',
	'foam.nanos.auth.AuthService',
  'foam.nanos.pm.PMInfo'
];

var abstractClasses = [
//  'foam.json.Outputer'
];

var skeletons = [
  'com.google.foam.demos.appengine.TestService',
  'foam.dao.DAO'
];

var proxies = [
  'foam.dao.DAO',
  'foam.dao.Sink',
  'com.google.foam.demos.appengine.TestService'
];

module.exports = {
    classes: classes,
    abstractClasses: abstractClasses,
    skeletons: skeletons,
    proxies: proxies
}
<|endoftext|>


<commit_before>var fs = require('fs'),
    pathlib = require('path'),
    uri = require('url'),
    events = require('events'),
    mime = require('./content-type'),
    log = require('./log'),
    VERSION = "2.0";

var settings = exports.default_settings = {
    "max_bytes_per_read": 1024 * 1024 * 5, // 5MB
    "timeout_milliseconds": 1000 * 30, //30 sec
    "hosts" : {},
    "port" : 8080,
    "default_host" : {
        "root" : "./"
    },
    "log_level" : log.levels.DEBUG
};

exports.start = function(custom_settings) {
    settings = custom_settings || exports.default_settings;
    log.level = settings.log_level;
    log.info( "Starting server on port", settings.port);
    require("http").createServer(function(req,resp) {
        log.info("Request from ",req.connection.remoteAddress,"for",req.url);
        log.info(JSON.stringify(req.headers));
        resp.die = setTimeout(function() {
            finish(resp);
        }, settings.timeout_milliseconds);

        find_local_file_path(req).addListener('success', function(path) {
            try_stream(path, req, resp);
        });
    }).listen(settings.port);
}

function find_local_file_path(req) {
    var url = uri.parse(req.url);
    //if the parsed url doesn't have a pathname, default to '/'
    var pathname = (url.pathname || '/');
    //disallow parent directory access, convert spaces
    var clean_pathname = pathname.replace(/\.\.\//g,'').replace(/\%20/g,' ');

    var vhost = select_vhost(req.headers.host);

    log.debug("requested host:", req.headers.host, "selected vhost:", vhost.root);
    var path = pathlib.join(vhost.root, clean_pathname);
    log.debug("Path = ", path);

    var emitter = new events.EventEmitter();
    fs.stat(path, function (err, stat) {
        if (err) {
            emitter.emit('success', path); //continue with program, will 404 later
            return;
        }
        if (stat.isDirectory()) {
            path = pathlib.join(path, "index.html");
        }
        emitter.emit('success', path);
    });
    return emitter;
}

function select_vhost(host_header) {
    if (host_header) {
        host_header = host_header.split(':')[0]; //remove the port
        return settings.hosts[host_header] || settings.default_host;
    } else {
        return settings.default_host;
    }
}

function try_stream(path, req, resp) {
    function send_headers(httpstatus, length, content_type, modified_time) {
        var headers = {
            "Server" : "Antinode/" + VERSION + " Node.js/" + process.version,
            "Date" : (new Date()).toUTCString()
        };
        if (length) {
            headers["Content-Length"] = length;
        }
        if (content_type) {
            headers["Content-Type"] = content_type || "application/octet-stream";
        }
        if (modified_time) {
            headers["Last-Modified"] = modified_time.toUTCString();
        }
        resp.sendHeader(httpstatus, headers);
    }

    fs.stat(path, function (err, stats) {
        if (err) {
            log.error("fs.stat returned ", err, "for file: ", path);
            return file_not_found();
        }
        if (!stats.isFile()) {
            file_not_found();
        } else {
            //
            // RFC 2616, section 14.25
            //
            // Note: When handling an If-Modified-Since header field, some
            // servers will use an exact date comparison function, rather than a
            // less-than function, for deciding whether to send a 304 (Not
            // Modified) response. To get best results when sending an If-
            // Modified-Since header field for cache validation, clients are
            // advised to use the exact date string received in a previous Last-
            // Modified header field whenever possible.
            //
            var if_modified_since = req.headers['if-modified-since'];
            if (if_modified_since && if_modified_since == stats.mtime.toUTCString()) {
                not_modified();
            } else {
                stream_file(path, stats);
            }
        }});

    function stream_file(file, stats) {
        fs.open(file,'r', 0660, function(err, fd) {
            if (err) {
                file_not_found();
                return;
            }
            log.debug("opened", path, "on fd", fd);

            send_headers(200, stats.size, mime.mime_type(file), stats.mtime);
            resp.flush();
            try {
                fs.sendfileSync(req.connection.fd, fd, 0, stats.size);
            } catch (ex) {
                log.error("sendfile(", file, ") threw exception ", ex.stack);
            }
            finish(resp);
            close(fd);
        });
    }

    function not_modified() {
        // no need to send content length or type
        log.debug("304 for resource ", path);
        send_headers(304);
        finish(resp);
    }

    function file_not_found() {
        log.debug("404 opening",path,">",arguments);
        var body = "404: " + path + " not found.";
        send_headers(404,body.length,"text/plain");
        resp.write(body);
        finish(resp);
    }

    function server_error(message) {
        log.error("error opening ",path,":",message);
        send_headers(500, message.length, "text/plain");
        resp.write(message);
        finish(resp);
    }
}

function finish(resp) {
    clearTimeout(resp.die);
    resp.end();
    log.debug("finished response");
}
function close(fd) {
    fs.close(fd);
    log.debug("closed fd",fd);
}
<commit_msg>Don't expose the file path in 404 response bodies, it gives away too much information about the server configuration.<commit_after>var fs = require('fs'),
    pathlib = require('path'),
    uri = require('url'),
    events = require('events'),
    mime = require('./content-type'),
    log = require('./log'),
    VERSION = "2.0";

var settings = exports.default_settings = {
    "max_bytes_per_read": 1024 * 1024 * 5, // 5MB
    "timeout_milliseconds": 1000 * 30, //30 sec
    "hosts" : {},
    "port" : 8080,
    "default_host" : {
        "root" : "./"
    },
    "log_level" : log.levels.DEBUG
};

exports.start = function(custom_settings) {
    settings = custom_settings || exports.default_settings;
    log.level = settings.log_level;
    log.info( "Starting server on port", settings.port);
    require("http").createServer(function(req,resp) {
        log.info("Request from ",req.connection.remoteAddress,"for",req.url);
        log.info(JSON.stringify(req.headers));
        resp.die = setTimeout(function() {
            finish(resp);
        }, settings.timeout_milliseconds);

        find_local_file_path(req).addListener('success', function(path) {
            try_stream(path, req, resp);
        });
    }).listen(settings.port);
}

function find_local_file_path(req) {
    var url = uri.parse(req.url);
    //if the parsed url doesn't have a pathname, default to '/'
    var pathname = (url.pathname || '/');
    //disallow parent directory access, convert spaces
    var clean_pathname = pathname.replace(/\.\.\//g,'').replace(/\%20/g,' ');

    var vhost = select_vhost(req.headers.host);

    log.debug("requested host:", req.headers.host, "selected vhost:", vhost.root);
    var path = pathlib.join(vhost.root, clean_pathname);
    log.debug("Path = ", path);

    var emitter = new events.EventEmitter();
    fs.stat(path, function (err, stat) {
        if (err) {
            emitter.emit('success', path); //continue with program, will 404 later
            return;
        }
        if (stat.isDirectory()) {
            path = pathlib.join(path, "index.html");
        }
        emitter.emit('success', path);
    });
    return emitter;
}

function select_vhost(host_header) {
    if (host_header) {
        host_header = host_header.split(':')[0]; //remove the port
        return settings.hosts[host_header] || settings.default_host;
    } else {
        return settings.default_host;
    }
}

function try_stream(path, req, resp) {
    function send_headers(httpstatus, length, content_type, modified_time) {
        var headers = {
            "Server" : "Antinode/" + VERSION + " Node.js/" + process.version,
            "Date" : (new Date()).toUTCString()
        };
        if (length) {
            headers["Content-Length"] = length;
        }
        if (content_type) {
            headers["Content-Type"] = content_type || "application/octet-stream";
        }
        if (modified_time) {
            headers["Last-Modified"] = modified_time.toUTCString();
        }
        resp.sendHeader(httpstatus, headers);
    }

    fs.stat(path, function (err, stats) {
        if (err) {
            log.error("fs.stat returned ", err, "for file: ", path);
            return file_not_found();
        }
        if (!stats.isFile()) {
            file_not_found();
        } else {
            //
            // RFC 2616, section 14.25
            //
            // Note: When handling an If-Modified-Since header field, some
            // servers will use an exact date comparison function, rather than a
            // less-than function, for deciding whether to send a 304 (Not
            // Modified) response. To get best results when sending an If-
            // Modified-Since header field for cache validation, clients are
            // advised to use the exact date string received in a previous Last-
            // Modified header field whenever possible.
            //
            var if_modified_since = req.headers['if-modified-since'];
            if (if_modified_since && if_modified_since == stats.mtime.toUTCString()) {
                not_modified();
            } else {
                stream_file(path, stats);
            }
        }});

    function stream_file(file, stats) {
        fs.open(file,'r', 0660, function(err, fd) {
            if (err) {
                file_not_found();
                return;
            }
            log.debug("opened", path, "on fd", fd);

            send_headers(200, stats.size, mime.mime_type(file), stats.mtime);
            resp.flush();
            try {
                fs.sendfileSync(req.connection.fd, fd, 0, stats.size);
            } catch (ex) {
                log.error("sendfile(", file, ") threw exception ", ex.stack);
            }
            finish(resp);
            close(fd);
        });
    }

    function not_modified() {
        // no need to send content length or type
        log.debug("304 for resource ", path);
        send_headers(304);
        finish(resp);
    }

    function file_not_found() {
        log.debug("404 opening",path,">",arguments);
        var body = "404: " + req.url + " not found.\n";
        send_headers(404,body.length,"text/plain");
        resp.write(body);
        finish(resp);
    }

    function server_error(message) {
        log.error("error opening ",path,":",message);
        send_headers(500, message.length, "text/plain");
        resp.write(message);
        finish(resp);
    }
}

function finish(resp) {
    clearTimeout(resp.die);
    resp.end();
    log.debug("finished response");
}
function close(fd) {
    fs.close(fd);
    log.debug("closed fd",fd);
}
<|endoftext|>


<commit_before>const _ = require('underscore');

const BaseCard = require('./basecard.js');
const SetupCardAction = require('./setupcardaction.js');
const DynastyCardAction = require('./dynastycardaction.js');
const PlayCardAction = require('./playcardaction.js');
const PlayAttachmentAction = require('./playattachmentaction.js');
const PlayCharacterAction = require('./playcharacteraction.js');
const DuplicateUniqueAction = require('./duplicateuniqueaction.js');

const StandardPlayActions = [
    new SetupCardAction(),
    new DynastyCardAction(),
    new PlayAttachmentAction(),
    new PlayCharacterAction(),
    new DuplicateUniqueAction(),
    new PlayCardAction()
];

class DrawCard extends BaseCard {
    constructor(owner, cardData) {
        super(owner, cardData);

        this.attachments = _([]);
        this.parent = null;

        this.militarySkillModifier = 0;
        this.politicalSkillModifier = 0;
        this.fate = 0;
        this.glory = cardData.glory;
        this.contributesToFavor = true;
        this.bowed = false;
        this.saved = false;
        this.inConflict = false;
        this.isConflict = false;
        this.isDynasty = false;
        this.isHonored = false;
        this.isDishonored = false;
        this.readysDuringReadying = true;
        this.conflictOptions = {
            doesNotBowAs: {
                attacker: false,
                defender: false
            },
            cannotParticipateIn: {
                military: false,
                political: false
            }
        };
        this.stealthLimit = 1;

        if(cardData.side === 'conflict') {
            this.isConflict = true;
        } else if(cardData.side === 'dynasty') {
            this.isDynasty = true;
        }
    }

    isLimited() {
        return this.hasKeyword('Limited') || this.hasPrintedKeyword('Limited');
    }

    isRestricted() {
        return this.hasKeyword('restricted');
    }

    isAncestral() {
        return this.hasKeyword('ancestral');
    }

    isCovert() {
        return this.hasKeyword('covert');
    }

    hasSincerity() {
        return this.hasKeyword('sincerity');
    }

    hasPride() {
        return this.hasKeyword('pride');
    }

    hasCourtesy() {
        return this.hasKeyword('courtesy');
    }

    getCost() {
        return this.cardData.cost;
    }

    getFate() {
        return this.fate;
    }

    modifySkill(amount, type, applying = true) {
        /**
         * Direct the skill modification to the correct sub function.
         * @param  {integer} amount - The amount to modify the skill by.
         * @param  {string}   type - The type of the skill; military or political
         * @param  {boolean}  applying -  [description]
         */
        if(type === 'military') {
            this.modifyMilitarySkill(amount, applying);
        } else if(type === 'political') {
            this.modifyPoliticalSkill(amount, applying);
        }
    }

    getSkill(type, printed = false) {
        /**
         * Direct the skill query to the correct sub function.
         * @param  {string} type - The type of the skill; military or political
         * @param  {boolean} printed - Use the printed value of the skill; default false
         * @return {integer} The chosen skill value
         */
        if(type === 'military') {
            return this.getMilitarySkill(printed);
        } else if(type === 'political') {
            return this.getPoliticalSkill(printed);
        }
    }

    modifyMilitarySkill(amount, applying = true) {
        /**
         * Modify the military skill.
         * @param  {integer} amount - The amount to modify the skill by.
         * @param  {boolean}  applying -  [description]
         */
        this.militarySkillModifier += amount;
        this.game.raiseEvent('onCardMilitarySkillChanged', {
            card: this,
            amount: amount,
            applying: applying
        });
    }

    modifyPoliticalSkill(amount, applying = true) {
        /**
         * Modify the political skill.
         * @param  {integer} amount - The amount to modify the skill by.
         * @param  {boolean}  applying -  [description]
         */
        this.politicalSkillModifier += amount;
        this.game.raiseEvent('onCardPoliticalSkillChanged', {
            card: this,
            amount: amount,
            applying: applying
        });
    }

    getMilitarySkill(printed = false) {
        /**
         * Get the military skill.
         * @param  {boolean} printed - Use the printed value of the skill; default false
         * @return {integer} The military skill value
         */
        if(this.cardData.military !== null && this.cardData.military !== undefined) {
            let skillFromAttachments = _.reduce(this.attachments._wrapped, (skill, card) => skill + parseInt(card.cardData.military_bonus), 0);
            let skillFromGlory = (this.isHonored ? this.glory : 0) - (this.isDishonored ? this.glory : 0);
            return Math.max(0, this.cardData.military + this.militarySkillModifier + skillFromAttachments + skillFromGlory);
        }

        return null;
    }

    getPoliticalSkill(printed = false) {
        /**
         * Get the political skill.
         * @param  {boolean} printed - Use the printed value of the skill; default false
         * @return {integer} The political skill value
         */
        if(this.cardData.political !== null && this.cardData.political !== undefined) {
            let skillFromAttachments = _.reduce(this.attachments._wrapped, (skill, card) => skill + parseInt(card.cardData.political_bonus), 0);
            let skillFromGlory = (this.isHonored ? this.glory : 0) - (this.isDishonored ? this.glory : 0);
            return Math.max(0, this.cardData.political + this.politicalSkillModifier + skillFromAttachments + skillFromGlory);
        }

        return null;
    }

    modifyFate(fate) {
        /**
         * @param  {integer} fate - the amount of fate to modify this card's fate total by
         */
        var oldFate = this.fate;

        this.fate += fate;

        if(this.fate < 0) {
            this.fate = 0;
        }

        this.game.raiseEvent('onCardFateChanged', { card: this, fate: this.fate - oldFate });

    }

    honor() {
        if(this.isDishonored) {
            this.isDishonored = false;
        } else if(!this.isHonored) {
            this.isHonored = true;
        }
    }

    dishonor() {
        if(this.isHonored) {
            this.isHonored = false;
        } else if(!this.isDishonored) {
            this.isDishonored = true;
        }
    }

    needsCovertTarget() {
        return this.isCovert() && !this.covertTarget;
    }

    canUseCovertToBypass(targetCard) {
        return this.isCovert() && targetCard.canBeBypassedByCovert();
    }

    canBeBypassedByCovert() {
        return !this.isCovert();
    }

    useCovertToBypass(targetCard) {
        if(!this.canUseCovertToBypass(targetCard)) {
            return false;
        }

        targetCard.covert = true;
        this.covertTarget = targetCard;

        return true;
    }

    clearBlank() {
        super.clearBlank();
        this.attachments.each(attachment => {
            if(!this.allowAttachment(attachment)) {
                this.controller.discardCard(attachment, false);
            }
        });
    }

    /**
     * Checks 'no attachment' restrictions for this card when attempting to
     * attach the passed attachment card.
     */
    allowAttachment(attachment) {
        if(_.any(this.allowedAttachmentTraits, trait => attachment.hasTrait(trait))) {
            return true;
        }

        return (
            this.isBlank() ||
            this.allowedAttachmentTraits.length === 0
        );
    }

    /**
     * Applies an effect with the specified properties while the current card is
     * attached to another card. By default the effect will target the parent
     * card, but you can provide a match function to narrow down whether the
     * effect is applied (for cases where the effect only applies to specific
     * characters).
     */
    whileAttached(properties) {
        this.persistentEffect({
            condition: properties.condition,
            match: (card, context) => card === this.parent && (!properties.match || properties.match(card, context)),
            targetController: 'any',
            effect: properties.effect,
            recalculateWhen: properties.recalculateWhen
        });
    }

    /**
     * Checks whether the passed card meets the attachment restrictions (e.g.
     * Opponent cards only, specific factions, etc) for this card.
     */
    canAttach(player, card) {
        return card && this.getType() === 'attachment';
    }

    getPlayActions() {
        return StandardPlayActions
            .concat(this.abilities.playActions)
            .concat(_.filter(this.abilities.actions, action => !action.allowMenu()));
    }

    leavesPlay() {
        this.bowed = false;
        this.inConflict = false;
        this.new = false;
        this.fate = 0;
        if(this.isHonored) {
            this.game.addHonor(this.controller, 1);
            this.game.addMessage('{0} gains 1 honor due to {1}\'s personal honor', this.controller, this);
            this.isHonored = false;
        } else if(this.isDishonored) {
            this.game.addHonor(this.controller, -1);
            this.game.addMessage('{0} loses 1 honor due to {1}\'s personal honor', this.controller, this);
            this.isDishonored = false;
        }
        if(this.hasSincerity()) {
            this.controller.drawCardsToHand(1);
            this.game.addMessage('{0} draws a card due to {1}\'s Sincerity', this.controller, this);
        }
        if(this.hasCourtesy()) {
            this.game.addFate(this.controller, 1);
            this.game.addMessage('{0} gains a fate due to {1}\'s Courtesy', this.controller, this);
        }
        this.resetForConflict();
        super.leavesPlay();
    }

    resetForConflict() {
        this.stealth = false;
        //this.stealthTarget = undefined;
        this.inConflict = false;
    }

    canDeclareAsAttacker(conflictType) {
        return this.allowGameAction('declareAsAttacker') && this.canDeclareAsParticipant(conflictType);
    }

    canDeclareAsDefender(conflictType) {
        return this.allowGameAction('declareAsDefender') && this.canDeclareAsParticipant(conflictType);
    }

    canDeclareAsParticipant(conflictType) {
        return (
            this.canParticipateInConflict() &&
            this.location === 'play area' &&
            !this.stealth &&
            (!this.bowed || this.conflictOptions.canBeDeclaredWhileBowing) &&
            !this.conflictOptions.cannotParticipateIn[conflictType]
        );
    }

    canParticipateInConflict() {
        return this.allowGameAction('participateInConflict');
    }

    canBeKilled() {
        return this.allowGameAction('kill');
    }

    canBePlayed() {
        return this.allowGameAction('play');
    }

    returnHomeFromConflict(side) {
        if(!this.conflictOptions.doesNotBowAs[side] && !this.bowed) {
            this.controller.bowCard(this);
        }
        this.inConflict = false;
    }

    play() {
    //empty function so playcardaction doesn't crash the game
    }

    getSummary(activePlayer, hideWhenFaceup) {
        let baseSummary = super.getSummary(activePlayer, hideWhenFaceup);

        return _.extend(baseSummary, {
            attached: !!this.parent,
            attachments: this.attachments.map(attachment => {
                return attachment.getSummary(activePlayer, hideWhenFaceup);
            }),
            isConflict: this.isConflict,
            isDynasty: this.isDynasty,
            isDishonored: this.isDishonored,
            isHonored: this.isHonored,
            bowed: this.bowed,
            saved: this.saved,
            fate: this.fate,
            new: this.new,
            stealth: this.stealth,
            militaryskill: this.getMilitarySkill(),
            politicalskill: this.getPoliticalSkill()
        });
    }
}

module.exports = DrawCard;
<commit_msg>lint fix<commit_after>const _ = require('underscore');

const BaseCard = require('./basecard.js');
const SetupCardAction = require('./setupcardaction.js');
const DynastyCardAction = require('./dynastycardaction.js');
const PlayCardAction = require('./playcardaction.js');
const PlayAttachmentAction = require('./playattachmentaction.js');
const PlayCharacterAction = require('./playcharacteraction.js');
const DuplicateUniqueAction = require('./duplicateuniqueaction.js');

const StandardPlayActions = [
    new SetupCardAction(),
    new DynastyCardAction(),
    new PlayAttachmentAction(),
    new PlayCharacterAction(),
    new DuplicateUniqueAction(),
    new PlayCardAction()
];

class DrawCard extends BaseCard {
    constructor(owner, cardData) {
        super(owner, cardData);

        this.attachments = _([]);
        this.parent = null;

        this.militarySkillModifier = 0;
        this.politicalSkillModifier = 0;
        this.fate = 0;
        this.glory = cardData.glory;
        this.contributesToFavor = true;
        this.bowed = false;
        this.saved = false;
        this.inConflict = false;
        this.isConflict = false;
        this.isDynasty = false;
        this.isHonored = false;
        this.isDishonored = false;
        this.readysDuringReadying = true;
        this.conflictOptions = {
            doesNotBowAs: {
                attacker: false,
                defender: false
            },
            cannotParticipateIn: {
                military: false,
                political: false
            }
        };
        this.stealthLimit = 1;

        if(cardData.side === 'conflict') {
            this.isConflict = true;
        } else if(cardData.side === 'dynasty') {
            this.isDynasty = true;
        }
    }

    isLimited() {
        return this.hasKeyword('Limited') || this.hasPrintedKeyword('Limited');
    }

    isRestricted() {
        return this.hasKeyword('restricted');
    }

    isAncestral() {
        return this.hasKeyword('ancestral');
    }

    isCovert() {
        return this.hasKeyword('covert');
    }

    hasSincerity() {
        return this.hasKeyword('sincerity');
    }

    hasPride() {
        return this.hasKeyword('pride');
    }

    hasCourtesy() {
        return this.hasKeyword('courtesy');
    }

    getCost() {
        return this.cardData.cost;
    }

    getFate() {
        return this.fate;
    }

    modifySkill(amount, type, applying = true) {
        /**
         * Direct the skill modification to the correct sub function.
         * @param  {integer} amount - The amount to modify the skill by.
         * @param  {string}   type - The type of the skill; military or political
         * @param  {boolean}  applying -  [description]
         */
        if(type === 'military') {
            this.modifyMilitarySkill(amount, applying);
        } else if(type === 'political') {
            this.modifyPoliticalSkill(amount, applying);
        }
    }

    getSkill(type, printed = false) {
        /**
         * Direct the skill query to the correct sub function.
         * @param  {string} type - The type of the skill; military or political
         * @param  {boolean} printed - Use the printed value of the skill; default false
         * @return {integer} The chosen skill value
         */
        if(type === 'military') {
            return this.getMilitarySkill(printed);
        } else if(type === 'political') {
            return this.getPoliticalSkill(printed);
        }
    }

    modifyMilitarySkill(amount, applying = true) {
        /**
         * Modify the military skill.
         * @param  {integer} amount - The amount to modify the skill by.
         * @param  {boolean}  applying -  [description]
         */
        this.militarySkillModifier += amount;
        this.game.raiseEvent('onCardMilitarySkillChanged', {
            card: this,
            amount: amount,
            applying: applying
        });
    }

    modifyPoliticalSkill(amount, applying = true) {
        /**
         * Modify the political skill.
         * @param  {integer} amount - The amount to modify the skill by.
         * @param  {boolean}  applying -  [description]
         */
        this.politicalSkillModifier += amount;
        this.game.raiseEvent('onCardPoliticalSkillChanged', {
            card: this,
            amount: amount,
            applying: applying
        });
    }

    getMilitarySkill(printed = false) {
        /**
         * Get the military skill.
         * @param  {boolean} printed - Use the printed value of the skill; default false
         * @return {integer} The military skill value
         */
        if(printed) {
            return this.cardData.military;
        }

        if(this.cardData.military !== null && this.cardData.military !== undefined) {
            let skillFromAttachments = _.reduce(this.attachments._wrapped, (skill, card) => skill + parseInt(card.cardData.military_bonus), 0);
            let skillFromGlory = (this.isHonored ? this.glory : 0) - (this.isDishonored ? this.glory : 0);
            return Math.max(0, this.cardData.military + this.militarySkillModifier + skillFromAttachments + skillFromGlory);
        }

        return null;
    }

    getPoliticalSkill(printed = false) {
        /**
         * Get the political skill.
         * @param  {boolean} printed - Use the printed value of the skill; default false
         * @return {integer} The political skill value
         */
        if(printed) {
            return this.cardData.military;
        }

        if(this.cardData.political !== null && this.cardData.political !== undefined) {
            let skillFromAttachments = _.reduce(this.attachments._wrapped, (skill, card) => skill + parseInt(card.cardData.political_bonus), 0);
            let skillFromGlory = (this.isHonored ? this.glory : 0) - (this.isDishonored ? this.glory : 0);
            return Math.max(0, this.cardData.political + this.politicalSkillModifier + skillFromAttachments + skillFromGlory);
        }

        return null;
    }

    modifyFate(fate) {
        /**
         * @param  {integer} fate - the amount of fate to modify this card's fate total by
         */
        var oldFate = this.fate;

        this.fate += fate;

        if(this.fate < 0) {
            this.fate = 0;
        }

        this.game.raiseEvent('onCardFateChanged', { card: this, fate: this.fate - oldFate });

    }

    honor() {
        if(this.isDishonored) {
            this.isDishonored = false;
        } else if(!this.isHonored) {
            this.isHonored = true;
        }
    }

    dishonor() {
        if(this.isHonored) {
            this.isHonored = false;
        } else if(!this.isDishonored) {
            this.isDishonored = true;
        }
    }

    needsCovertTarget() {
        return this.isCovert() && !this.covertTarget;
    }

    canUseCovertToBypass(targetCard) {
        return this.isCovert() && targetCard.canBeBypassedByCovert();
    }

    canBeBypassedByCovert() {
        return !this.isCovert();
    }

    useCovertToBypass(targetCard) {
        if(!this.canUseCovertToBypass(targetCard)) {
            return false;
        }

        targetCard.covert = true;
        this.covertTarget = targetCard;

        return true;
    }

    clearBlank() {
        super.clearBlank();
        this.attachments.each(attachment => {
            if(!this.allowAttachment(attachment)) {
                this.controller.discardCard(attachment, false);
            }
        });
    }

    /**
     * Checks 'no attachment' restrictions for this card when attempting to
     * attach the passed attachment card.
     */
    allowAttachment(attachment) {
        if(_.any(this.allowedAttachmentTraits, trait => attachment.hasTrait(trait))) {
            return true;
        }

        return (
            this.isBlank() ||
            this.allowedAttachmentTraits.length === 0
        );
    }

    /**
     * Applies an effect with the specified properties while the current card is
     * attached to another card. By default the effect will target the parent
     * card, but you can provide a match function to narrow down whether the
     * effect is applied (for cases where the effect only applies to specific
     * characters).
     */
    whileAttached(properties) {
        this.persistentEffect({
            condition: properties.condition,
            match: (card, context) => card === this.parent && (!properties.match || properties.match(card, context)),
            targetController: 'any',
            effect: properties.effect,
            recalculateWhen: properties.recalculateWhen
        });
    }

    /**
     * Checks whether the passed card meets the attachment restrictions (e.g.
     * Opponent cards only, specific factions, etc) for this card.
     */
    canAttach(player, card) {
        return card && this.getType() === 'attachment';
    }

    getPlayActions() {
        return StandardPlayActions
            .concat(this.abilities.playActions)
            .concat(_.filter(this.abilities.actions, action => !action.allowMenu()));
    }

    leavesPlay() {
        this.bowed = false;
        this.inConflict = false;
        this.new = false;
        this.fate = 0;
        if(this.isHonored) {
            this.game.addHonor(this.controller, 1);
            this.game.addMessage('{0} gains 1 honor due to {1}\'s personal honor', this.controller, this);
            this.isHonored = false;
        } else if(this.isDishonored) {
            this.game.addHonor(this.controller, -1);
            this.game.addMessage('{0} loses 1 honor due to {1}\'s personal honor', this.controller, this);
            this.isDishonored = false;
        }
        if(this.hasSincerity()) {
            this.controller.drawCardsToHand(1);
            this.game.addMessage('{0} draws a card due to {1}\'s Sincerity', this.controller, this);
        }
        if(this.hasCourtesy()) {
            this.game.addFate(this.controller, 1);
            this.game.addMessage('{0} gains a fate due to {1}\'s Courtesy', this.controller, this);
        }
        this.resetForConflict();
        super.leavesPlay();
    }

    resetForConflict() {
        this.stealth = false;
        //this.stealthTarget = undefined;
        this.inConflict = false;
    }

    canDeclareAsAttacker(conflictType) {
        return this.allowGameAction('declareAsAttacker') && this.canDeclareAsParticipant(conflictType);
    }

    canDeclareAsDefender(conflictType) {
        return this.allowGameAction('declareAsDefender') && this.canDeclareAsParticipant(conflictType);
    }

    canDeclareAsParticipant(conflictType) {
        return (
            this.canParticipateInConflict() &&
            this.location === 'play area' &&
            !this.stealth &&
            (!this.bowed || this.conflictOptions.canBeDeclaredWhileBowing) &&
            !this.conflictOptions.cannotParticipateIn[conflictType]
        );
    }

    canParticipateInConflict() {
        return this.allowGameAction('participateInConflict');
    }

    canBeKilled() {
        return this.allowGameAction('kill');
    }

    canBePlayed() {
        return this.allowGameAction('play');
    }

    returnHomeFromConflict(side) {
        if(!this.conflictOptions.doesNotBowAs[side] && !this.bowed) {
            this.controller.bowCard(this);
        }
        this.inConflict = false;
    }

    play() {
    //empty function so playcardaction doesn't crash the game
    }

    getSummary(activePlayer, hideWhenFaceup) {
        let baseSummary = super.getSummary(activePlayer, hideWhenFaceup);

        return _.extend(baseSummary, {
            attached: !!this.parent,
            attachments: this.attachments.map(attachment => {
                return attachment.getSummary(activePlayer, hideWhenFaceup);
            }),
            isConflict: this.isConflict,
            isDynasty: this.isDynasty,
            isDishonored: this.isDishonored,
            isHonored: this.isHonored,
            bowed: this.bowed,
            saved: this.saved,
            fate: this.fate,
            new: this.new,
            stealth: this.stealth,
            militaryskill: this.getMilitarySkill(),
            politicalskill: this.getPoliticalSkill()
        });
    }
}

module.exports = DrawCard;
<|endoftext|>


<commit_before>(function (window, io, $, _, Backbone, Lawnchair, location, app) {
    "use strict";

    var ID = "id",
        SECURE = "{{secure}}" == "true",
        PORT = "{{port}}";

    app.middle = app.middle || {};

    _.extend(app.middle, Backbone.Events);

    // *******************************************************************************
    // using socket.io for Backbone.sync
    // *******************************************************************************

    function open(name, cb) {
        new Lawnchair({ name: name }, cb);
    }

    function changedAttributes(model) {
        var changed = model.changedAttributes()

        return changed && _.keys(changed);
    }

    function readAsync(collection, id, options) {
        return open(collection, function(store) {
            if (id) {
                return store.get(id, function (result) {
                    return options && options.success && options.success(result && result.val);
                });
            }

            return store.all(function (result) {
                if (!options || !options.success) {
                    return;
                }

                var found = [];

                _.each(result || [], function(result) {
                    found.push(result.val);
                });

                return options.success(found);
            });
        });
    }

    function saveLocal(model, collection, id, options, changed, data) {
        changed = changed || changedAttributes(model);

        if (!changed) {
            return;
        }

        return open(collection, function(store) {
            data = data || model.toJSON();

            return store.save({
                key:id,
                val:data
            }, function (result) {
                app.log("saving locally", collection, result, changed);
                return options && options.success && options.success(result.val);
            });
        });
    }

    function saveRemote(method, model, collection, id, options, changed, data) {
        changed = changed || changedAttributes(model);

        if (!changed) {
            return;
        }

        data = data || model.toJSON();

        return app.middle.emit("middle-store", method, collection, id, data, changed, function (err, result) {
            if (err) {
                if (options && options.errror) {
                    return options.errror(err);
                }

                return app.error("sync failed", collection, id, data, changed, err);
            }

            app.log("remote update", result);

            if (options && options.success) {
                return options.success(result);
            }
        });
    }

    function saveAsync(method, model, collection, id, options, changed, data) {
        changed = changed || changedAttributes(model);

        if (!changed) {
            return;
        }

        data = data || model.toJSON();

        saveRemote(method, model, collection, id, undefined, changed, data);
        saveLocal(model, collection, id, options, changed, data);
    }

    function createAsync(method, model, collection, id, options) {
        var data = model.toJSON() || {};

        data.id = app.utils.uuid();

        saveAsync(method, model, collection, data.id, options, _.keys(data), data);
    }

    function asyncSync(method, model, collection, id, options) {
        if ('read' === method) {
            return readAsync(collection, id, options);
        }

        if ("update" === method) {
            return saveAsync(method, model, collection, id, options);
        }

        if ("create" === method) {
            return createAsync(method, model, collection, id, options);
        }
        throw new Error("not yet supported");
    }

    function getURL(model) {
        return _.isFunction(model.url) ? model.url.call(model) : model.url;
    }

    app.middle.sync = function (method, model, options) {
        var url = getURL(model),
            segments = url.split('/'),
            type = segments.shift(),
            collection = segments.shift(),
            id = segments.join('/');

        if (type === 'async') {
            return asyncSync(method, model, collection, id, options);
        }

        throw new Error("unsupported url " + url);
    };

    // *******************************************************************************
    // Starting connections. loading device id, etc.
    // *******************************************************************************

    function getServerURL() {
        var protocol = SECURE ? "https://" : "http://",
            port = PORT || location.port;

        if (port) {
            port = ":" + port;
        }

        return protocol + location.hostname + port;
    }

    function convert(arg) {
        try {
            return JSON.stringify(arg);
        }
        catch (e) {
            var simpified = {};

            _.each(arg, function (entry, key) {
                if (entry !== window) {
                    simpified[key] = entry;
                }
            });

            return convert(simpified);
        }
    }

    function log(socket, level, _args) {
        var args = Array.prototype.slice.call(_args),
            params = [
                "log",
                level,
                new Date()
            ];

        _.each(args, function (arg) {
            var param = convert(arg);

            params.push(param);
        });

        return socket.emit.apply(socket, params);
    }

    function emitter(socket) {
        return function () {
            return socket.emit.apply(socket, arguments);
        };
    }

    function decorate(socket) {
        return {
            emit:emitter(socket)
        };
    }

    app.starter.$(function (next) {
        new Lawnchair({ name: "middle" }, function (store) {
            store.get(ID, function (device) {
                app.middle.id = (device && device.id) || app.utils.uuid();

                if (!device) {
                    store.save({
                        key:ID,
                        id:app.middle.id
                    });
                }

                var url = getServerURL(),
                    socket = io.connect(url),
                    app_log = app.log,
                    app_debug = app.debug,
                    app_error = app.error;

                app.middle.emit = emitter(socket);

                var decorated = decorate(socket, store);

                socket.on('error', function (err) {
                    app.error("socket error", err);
                });
                socket.on('connecting', function () {
                    app.log("connecting");
                });
                socket.on('connect_failed', function (err) {
                    app.error("connect_failed", err);
                });
                socket.on('reconnect', function () {
                    app.log("reconnect");
                });
                socket.on('reconnecting', function (err) {
                    app.log("reconnecting");
                });
                socket.on('connect', function (props) {
                    app.middle.connected = true;

                    socket.emit("middle-initialize", app.middle.id);

                    app.log = function () {
                        return log(socket, "info", arguments);
                    };
                    app.debug = function () {
                        return log(socket, "debug", arguments);
                    };
                    app.error = function () {
                        return log(socket, "error", arguments);
                    };
                    app.middle.trigger("connect", app.middle.id)
                    app.log("connected");
                });
                socket.on('disconnect', function (props) {
                    app.middle.connected = false;

                    app.log = app_log;
                    app.debug = app_debug;
                    app.error = app_error;
                    app.middle.trigger("disconnect", app.middle.id)
                    app.log("disconnected", app.middle.id);
                });

                return next();
            });
        });
    });
})(window, window.io, $, _, Backbone, Lawnchair, window.location, window["jolira-app"]);
<commit_msg>now continuing to try to connect every four seconds indefinitely.<commit_after>(function (window, io, $, _, Backbone, Lawnchair, location, app) {
    "use strict";

    var ID = "id",
        SECURE = "{{secure}}" == "true",
        PORT = "{{port}}";

    app.middle = app.middle || {};

    _.extend(app.middle, Backbone.Events);

    // *******************************************************************************
    // using socket.io for Backbone.sync
    // *******************************************************************************

    function open(name, cb) {
        new Lawnchair({ name: name }, cb);
    }

    function changedAttributes(model) {
        var changed = model.changedAttributes()

        return changed && _.keys(changed);
    }

    function readAsync(collection, id, options) {
        return open(collection, function(store) {
            if (id) {
                return store.get(id, function (result) {
                    return options && options.success && options.success(result && result.val);
                });
            }

            return store.all(function (result) {
                if (!options || !options.success) {
                    return;
                }

                var found = [];

                _.each(result || [], function(result) {
                    found.push(result.val);
                });

                return options.success(found);
            });
        });
    }

    function saveLocal(model, collection, id, options, changed, data) {
        changed = changed || changedAttributes(model);

        if (!changed) {
            return;
        }

        return open(collection, function(store) {
            data = data || model.toJSON();

            return store.save({
                key:id,
                val:data
            }, function (result) {
                app.log("saving locally", collection, result, changed);
                return options && options.success && options.success(result.val);
            });
        });
    }

    function saveRemote(method, model, collection, id, options, changed, data) {
        changed = changed || changedAttributes(model);

        if (!changed) {
            return;
        }

        data = data || model.toJSON();

        return app.middle.emit("middle-store", method, collection, id, data, changed, function (err, result) {
            if (err) {
                if (options && options.errror) {
                    return options.errror(err);
                }

                return app.error("sync failed", collection, id, data, changed, err);
            }

            app.log("remote update", result);

            if (options && options.success) {
                return options.success(result);
            }
        });
    }

    function saveAsync(method, model, collection, id, options, changed, data) {
        changed = changed || changedAttributes(model);

        if (!changed) {
            return;
        }

        data = data || model.toJSON();

        saveRemote(method, model, collection, id, undefined, changed, data);
        saveLocal(model, collection, id, options, changed, data);
    }

    function createAsync(method, model, collection, id, options) {
        var data = model.toJSON() || {};

        data.id = app.utils.uuid();

        saveAsync(method, model, collection, data.id, options, _.keys(data), data);
    }

    function asyncSync(method, model, collection, id, options) {
        if ('read' === method) {
            return readAsync(collection, id, options);
        }

        if ("update" === method) {
            return saveAsync(method, model, collection, id, options);
        }

        if ("create" === method) {
            return createAsync(method, model, collection, id, options);
        }
        throw new Error("not yet supported");
    }

    function getURL(model) {
        return _.isFunction(model.url) ? model.url.call(model) : model.url;
    }

    app.middle.sync = function (method, model, options) {
        var url = getURL(model),
            segments = url.split('/'),
            type = segments.shift(),
            collection = segments.shift(),
            id = segments.join('/');

        if (type === 'async') {
            return asyncSync(method, model, collection, id, options);
        }

        throw new Error("unsupported url " + url);
    };

    // *******************************************************************************
    // Starting connections. loading device id, etc.
    // *******************************************************************************

    function getServerURL() {
        var protocol = SECURE ? "https://" : "http://",
            port = PORT || location.port;

        if (port) {
            port = ":" + port;
        }

        return protocol + location.hostname + port;
    }

    function convert(arg) {
        try {
            return JSON.stringify(arg);
        }
        catch (e) {
            var simpified = {};

            _.each(arg, function (entry, key) {
                if (entry !== window) {
                    simpified[key] = entry;
                }
            });

            return convert(simpified);
        }
    }

    function log(socket, level, _args) {
        var args = Array.prototype.slice.call(_args),
            params = [
                "log",
                level,
                new Date()
            ];

        _.each(args, function (arg) {
            var param = convert(arg);

            params.push(param);
        });

        return socket.emit.apply(socket, params);
    }

    function emitter(socket) {
        return function () {
            return socket.emit.apply(socket, arguments);
        };
    }

    function decorate(socket) {
        return {
            emit:emitter(socket)
        };
    }

    app.starter.$(function (next) {
        new Lawnchair({ name: "middle" }, function (store) {
            store.get(ID, function (device) {
                app.middle.id = (device && device.id) || app.utils.uuid();

                if (!device) {
                    store.save({
                        key:ID,
                        id:app.middle.id
                    });
                }

                var url = getServerURL(),
                    socket = io.connect(url, {
                        "reconnection limit": 4001, // four second max delay
                        "max reconnection attempts": Infinity
                    }),
                    app_log = app.log,
                    app_debug = app.debug,
                    app_error = app.error;

                app.middle.emit = emitter(socket);

                var decorated = decorate(socket, store);

                socket.on('error', function (err) {
                    app.error("socket error", err);
                });
                socket.on('connecting', function () {
                    app.log("connecting");
                });
                socket.on('connect_failed', function (err) {
                    app.error("connect_failed", err);
                });
                socket.on('reconnect', function () {
                    app.log("reconnect");
                });
                socket.on('reconnecting', function (err) {
                    app.log("reconnecting");
                });
                socket.on('connect', function (props) {
                    app.middle.connected = true;

                    socket.emit("middle-initialize", app.middle.id);

                    app.log = function () {
                        return log(socket, "info", arguments);
                    };
                    app.debug = function () {
                        return log(socket, "debug", arguments);
                    };
                    app.error = function () {
                        return log(socket, "error", arguments);
                    };
                    app.middle.trigger("connect", app.middle.id)
                    app.log("connected");
                });
                socket.on('disconnect', function (props) {
                    app.middle.connected = false;

                    app.log = app_log;
                    app.debug = app_debug;
                    app.error = app_error;
                    app.middle.trigger("disconnect", app.middle.id)
                    app.log("disconnected", app.middle.id);
                });

                return next();
            });
        });
    });
})(window, window.io, $, _, Backbone, Lawnchair, window.location, window["jolira-app"]);
<|endoftext|>