<commit_before>'use strict';

//  ---------------------------------------------------------------------------

const Exchange = require ('./base/Exchange');
const { ExchangeError } = require ('./base/errors');

//  ---------------------------------------------------------------------------

module.exports = class coinmarketcap extends Exchange {
    describe () {
        return this.deepExtend (super.describe (), {
            'id': 'coinmarketcap',
            'name': 'CoinMarketCap',
            'rateLimit': 10000,
            'version': 'v1',
            'countries': 'US',
            'has': {
                'CORS': true,
                'privateAPI': false,
                'createOrder': false,
                'createMarketOrder': false,
                'createLimitOrder': false,
                'cancelOrder': false,
                'editOrder': false,
                'fetchBalance': false,
                'fetchOrderBook': false,
                'fetchOHLCV': false,
                'fetchTrades': false,
                'fetchTickers': true,
                'fetchCurrencies': true,
            },
            'urls': {
                'logo': 'https://user-images.githubusercontent.com/1294454/28244244-9be6312a-69ed-11e7-99c1-7c1797275265.jpg',
                'api': {
                    'public': 'https://api.coinmarketcap.com',
                    'files': 'https://files.coinmarketcap.com',
                    'charts': 'https://graph.coinmarketcap.com',
                },
                'www': 'https://coinmarketcap.com',
                'doc': 'https://coinmarketcap.com/api',
            },
            'requiredCredentials': {
                'apiKey': false,
                'secret': false,
            },
            'api': {
                'files': {
                    'get': [
                        'generated/stats/global.json',
                    ],
                },
                'graphs': {
                    'get': [
                        'currencies/{name}/',
                    ],
                },
                'public': {
                    'get': [
                        'ticker/',
                        'ticker/{id}/',
                        'global/',
                    ],
                },
            },
            'currencyCodes': [
                'AUD',
                'BRL',
                'CAD',
                'CHF',
                'CNY',
                'EUR',
                'GBP',
                'HKD',
                'IDR',
                'INR',
                'JPY',
                'KRW',
                'MXN',
                'RUB',
                'USD',
                'BTC',
                'ETH',
                'LTC',
            ],
        });
    }

    async fetchOrderBook (symbol, limit = undefined, params = {}) {
        throw new ExchangeError ('Fetching order books is not supported by the API of ' + this.id);
    }

    currencyCode (base, name) {
        const currencies = {
            'ACChain': 'ACChain',
            'AdCoin': 'AdCoin',
            'BatCoin': 'BatCoin',
            'Bitgem': 'Bitgem',
            'BlazeCoin': 'BlazeCoin',
            'BlockCAT': 'BlockCAT',
            'Catcoin': 'Catcoin',
            'CanYaCoin': 'CanYaCoin',
            'Comet': 'Comet',
            'CPChain': 'CPChain',
            'DAO.Casino': 'DAO.Casino',
            'ENTCash': 'ENTCash',
            'FairGame': 'FairGame',
            'GET Protocol': 'GET Protocol',
            'Global Tour Coin': 'Global Tour Coin',
            'GuccioneCoin': 'GuccioneCoin',
            'Hi Mutual Society': 'Hi Mutual Society',
            'Huncoin': 'Huncoin',
            'iCoin': 'iCoin',
            'KingN Coin': 'KingN Coin',
            'LiteBitcoin': 'LiteBitcoin',
            'Maggie': 'Maggie',
            'MIOTA': 'IOTA', // a special case, most exchanges list it as IOTA, therefore we change just the Coinmarketcap instead of changing them all
            'NetCoin': 'NetCoin',
            'Polcoin': 'Polcoin',
            'PutinCoin': 'PutinCoin',
        };
        if (name in currencies)
            return currencies[name];
        return base;
    }

    async fetchMarkets () {
        let markets = await this.publicGetTicker ({
            'limit': 0,
        });
        let result = [];
        for (let p = 0; p < markets.length; p++) {
            let market = markets[p];
            let currencies = this.currencyCodes;
            for (let i = 0; i < currencies.length; i++) {
                let quote = currencies[i];
                let quoteId = quote.toLowerCase ();
                let baseId = market['id'];
                let base = this.currencyCode (market['symbol'], market['name']);
                let symbol = base + '/' + quote;
                let id = baseId + '/' + quoteId;
                if (market['symbol'].indexOf ('dao') >= 0) {
                    console.log (market);
                }
                result.push ({
                    'id': id,
                    'symbol': symbol,
                    'base': base,
                    'quote': quote,
                    'baseId': baseId,
                    'quoteId': quoteId,
                    'info': market,
                });
            }
        }
        return result;
    }

    async fetchGlobal (currency = 'USD') {
        await this.loadMarkets ();
        let request = {};
        if (currency)
            request['convert'] = currency;
        return await this.publicGetGlobal (request);
    }

    parseTicker (ticker, market = undefined) {
        let timestamp = this.milliseconds ();
        if ('last_updated' in ticker)
            if (ticker['last_updated'])
                timestamp = parseInt (ticker['last_updated']) * 1000;
        let change = undefined;
        if ('percent_change_24h' in ticker)
            if (ticker['percent_change_24h'])
                change = this.safeFloat (ticker, 'percent_change_24h');
        let last = undefined;
        let symbol = undefined;
        let volume = undefined;
        if (market) {
            let priceKey = 'price_' + market['quoteId'];
            if (priceKey in ticker)
                if (ticker[priceKey])
                    last = this.safeFloat (ticker, priceKey);
            symbol = market['symbol'];
            let volumeKey = '24h_volume_' + market['quoteId'];
            if (volumeKey in ticker)
                if (ticker[volumeKey])
                    volume = this.safeFloat (ticker, volumeKey);
        }
        return {
            'symbol': symbol,
            'timestamp': timestamp,
            'datetime': this.iso8601 (timestamp),
            'high': undefined,
            'low': undefined,
            'bid': undefined,
            'bidVolume': undefined,
            'ask': undefined,
            'askVolume': undefined,
            'vwap': undefined,
            'open': undefined,
            'close': last,
            'last': last,
            'previousClose': undefined,
            'change': change,
            'percentage': undefined,
            'average': undefined,
            'baseVolume': undefined,
            'quoteVolume': volume,
            'info': ticker,
        };
    }

    async fetchTickers (currency = 'USD', params = {}) {
        await this.loadMarkets ();
        let request = {
            'limit': 10000,
        };
        if (currency)
            request['convert'] = currency;
        let response = await this.publicGetTicker (this.extend (request, params));
        let tickers = {};
        for (let t = 0; t < response.length; t++) {
            let ticker = response[t];
            let currencyId = (currency in this.currencies) ? this.currencies[currency]['id'] : currency.toLowerCase ();
            let id = ticker['id'] + '/' + currencyId;
            let symbol = id;
            let market = undefined;
            if (id in this.markets_by_id) {
                market = this.markets_by_id[id];
                symbol = market['symbol'];
            }
            tickers[symbol] = this.parseTicker (ticker, market);
        }
        return tickers;
    }

    async fetchTicker (symbol, params = {}) {
        await this.loadMarkets ();
        let market = this.market (symbol);
        let request = this.extend ({
            'convert': market['quote'],
            'id': market['baseId'],
        }, params);
        let response = await this.publicGetTickerId (request);
        let ticker = response[0];
        return this.parseTicker (ticker, market);
    }

    async fetchCurrencies (params = {}) {
        let currencies = await this.publicGetTicker (this.extend ({
            'limit': 0,
        }, params));
        let result = {};
        for (let i = 0; i < currencies.length; i++) {
            let currency = currencies[i];
            let id = currency['symbol'];
            let name = currency['name'];
            // todo: will need to rethink the fees
            // to add support for multiple withdrawal/deposit methods and
            // differentiated fees for each particular method
            let precision = 8; // default precision, todo: fix "magic constants"
            let code = this.currencyCode (id, name);
            result[code] = {
                'id': id,
                'code': code,
                'info': currency,
                'name': name,
                'active': true,
                'status': 'ok',
                'fee': undefined, // todo: redesign
                'precision': precision,
                'limits': {
                    'amount': {
                        'min': Math.pow (10, -precision),
                        'max': Math.pow (10, precision),
                    },
                    'price': {
                        'min': Math.pow (10, -precision),
                        'max': Math.pow (10, precision),
                    },
                    'cost': {
                        'min': undefined,
                        'max': undefined,
                    },
                    'withdraw': {
                        'min': undefined,
                        'max': undefined,
                    },
                },
            };
        }
        return result;
    }

    sign (path, api = 'public', method = 'GET', params = {}, headers = undefined, body = undefined) {
        let url = this.urls['api'][api] + '/' + this.version + '/' + this.implodeParams (path, params);
        let query = this.omit (params, this.extractParams (path));
        if (Object.keys (query).length)
            url += '?' + this.urlencode (query);
        return { 'url': url, 'method': method, 'body': body, 'headers': headers };
    }

    async request (path, api = 'public', method = 'GET', params = {}, headers = undefined, body = undefined) {
        let response = await this.fetch2 (path, api, method, params, headers, body);
        if ('error' in response) {
            if (response['error']) {
                throw new ExchangeError (this.id + ' ' + this.json (response));
            }
        }
        return response;
    }
};
<commit_msg>coinmarketcap QBT → Cubits fix #2604<commit_after>'use strict';

//  ---------------------------------------------------------------------------

const Exchange = require ('./base/Exchange');
const { ExchangeError } = require ('./base/errors');

//  ---------------------------------------------------------------------------

module.exports = class coinmarketcap extends Exchange {
    describe () {
        return this.deepExtend (super.describe (), {
            'id': 'coinmarketcap',
            'name': 'CoinMarketCap',
            'rateLimit': 10000,
            'version': 'v1',
            'countries': 'US',
            'has': {
                'CORS': true,
                'privateAPI': false,
                'createOrder': false,
                'createMarketOrder': false,
                'createLimitOrder': false,
                'cancelOrder': false,
                'editOrder': false,
                'fetchBalance': false,
                'fetchOrderBook': false,
                'fetchOHLCV': false,
                'fetchTrades': false,
                'fetchTickers': true,
                'fetchCurrencies': true,
            },
            'urls': {
                'logo': 'https://user-images.githubusercontent.com/1294454/28244244-9be6312a-69ed-11e7-99c1-7c1797275265.jpg',
                'api': {
                    'public': 'https://api.coinmarketcap.com',
                    'files': 'https://files.coinmarketcap.com',
                    'charts': 'https://graph.coinmarketcap.com',
                },
                'www': 'https://coinmarketcap.com',
                'doc': 'https://coinmarketcap.com/api',
            },
            'requiredCredentials': {
                'apiKey': false,
                'secret': false,
            },
            'api': {
                'files': {
                    'get': [
                        'generated/stats/global.json',
                    ],
                },
                'graphs': {
                    'get': [
                        'currencies/{name}/',
                    ],
                },
                'public': {
                    'get': [
                        'ticker/',
                        'ticker/{id}/',
                        'global/',
                    ],
                },
            },
            'currencyCodes': [
                'AUD',
                'BRL',
                'CAD',
                'CHF',
                'CNY',
                'EUR',
                'GBP',
                'HKD',
                'IDR',
                'INR',
                'JPY',
                'KRW',
                'MXN',
                'RUB',
                'USD',
                'BTC',
                'ETH',
                'LTC',
            ],
        });
    }

    async fetchOrderBook (symbol, limit = undefined, params = {}) {
        throw new ExchangeError ('Fetching order books is not supported by the API of ' + this.id);
    }

    currencyCode (base, name) {
        const currencies = {
            'ACChain': 'ACChain',
            'AdCoin': 'AdCoin',
            'BatCoin': 'BatCoin',
            'Bitgem': 'Bitgem',
            'BlazeCoin': 'BlazeCoin',
            'BlockCAT': 'BlockCAT',
            'Catcoin': 'Catcoin',
            'CanYaCoin': 'CanYaCoin',
            'Comet': 'Comet',
            'CPChain': 'CPChain',
            'Cubits': 'Cubits',
            'DAO.Casino': 'DAO.Casino',
            'ENTCash': 'ENTCash',
            'FairGame': 'FairGame',
            'GET Protocol': 'GET Protocol',
            'Global Tour Coin': 'Global Tour Coin',
            'GuccioneCoin': 'GuccioneCoin',
            'Hi Mutual Society': 'Hi Mutual Society',
            'Huncoin': 'Huncoin',
            'iCoin': 'iCoin',
            'KingN Coin': 'KingN Coin',
            'LiteBitcoin': 'LiteBitcoin',
            'Maggie': 'Maggie',
            'MIOTA': 'IOTA', // a special case, most exchanges list it as IOTA, therefore we change just the Coinmarketcap instead of changing them all
            'NetCoin': 'NetCoin',
            'Polcoin': 'Polcoin',
            'PutinCoin': 'PutinCoin',
        };
        if (name in currencies)
            return currencies[name];
        return base;
    }

    async fetchMarkets () {
        let markets = await this.publicGetTicker ({
            'limit': 0,
        });
        let result = [];
        for (let p = 0; p < markets.length; p++) {
            let market = markets[p];
            let currencies = this.currencyCodes;
            for (let i = 0; i < currencies.length; i++) {
                let quote = currencies[i];
                let quoteId = quote.toLowerCase ();
                let baseId = market['id'];
                let base = this.currencyCode (market['symbol'], market['name']);
                let symbol = base + '/' + quote;
                let id = baseId + '/' + quoteId;
                if (market['symbol'].indexOf ('dao') >= 0) {
                    console.log (market);
                }
                result.push ({
                    'id': id,
                    'symbol': symbol,
                    'base': base,
                    'quote': quote,
                    'baseId': baseId,
                    'quoteId': quoteId,
                    'info': market,
                });
            }
        }
        return result;
    }

    async fetchGlobal (currency = 'USD') {
        await this.loadMarkets ();
        let request = {};
        if (currency)
            request['convert'] = currency;
        return await this.publicGetGlobal (request);
    }

    parseTicker (ticker, market = undefined) {
        let timestamp = this.milliseconds ();
        if ('last_updated' in ticker)
            if (ticker['last_updated'])
                timestamp = parseInt (ticker['last_updated']) * 1000;
        let change = undefined;
        if ('percent_change_24h' in ticker)
            if (ticker['percent_change_24h'])
                change = this.safeFloat (ticker, 'percent_change_24h');
        let last = undefined;
        let symbol = undefined;
        let volume = undefined;
        if (market) {
            let priceKey = 'price_' + market['quoteId'];
            if (priceKey in ticker)
                if (ticker[priceKey])
                    last = this.safeFloat (ticker, priceKey);
            symbol = market['symbol'];
            let volumeKey = '24h_volume_' + market['quoteId'];
            if (volumeKey in ticker)
                if (ticker[volumeKey])
                    volume = this.safeFloat (ticker, volumeKey);
        }
        return {
            'symbol': symbol,
            'timestamp': timestamp,
            'datetime': this.iso8601 (timestamp),
            'high': undefined,
            'low': undefined,
            'bid': undefined,
            'bidVolume': undefined,
            'ask': undefined,
            'askVolume': undefined,
            'vwap': undefined,
            'open': undefined,
            'close': last,
            'last': last,
            'previousClose': undefined,
            'change': change,
            'percentage': undefined,
            'average': undefined,
            'baseVolume': undefined,
            'quoteVolume': volume,
            'info': ticker,
        };
    }

    async fetchTickers (currency = 'USD', params = {}) {
        await this.loadMarkets ();
        let request = {
            'limit': 10000,
        };
        if (currency)
            request['convert'] = currency;
        let response = await this.publicGetTicker (this.extend (request, params));
        let tickers = {};
        for (let t = 0; t < response.length; t++) {
            let ticker = response[t];
            let currencyId = (currency in this.currencies) ? this.currencies[currency]['id'] : currency.toLowerCase ();
            let id = ticker['id'] + '/' + currencyId;
            let symbol = id;
            let market = undefined;
            if (id in this.markets_by_id) {
                market = this.markets_by_id[id];
                symbol = market['symbol'];
            }
            tickers[symbol] = this.parseTicker (ticker, market);
        }
        return tickers;
    }

    async fetchTicker (symbol, params = {}) {
        await this.loadMarkets ();
        let market = this.market (symbol);
        let request = this.extend ({
            'convert': market['quote'],
            'id': market['baseId'],
        }, params);
        let response = await this.publicGetTickerId (request);
        let ticker = response[0];
        return this.parseTicker (ticker, market);
    }

    async fetchCurrencies (params = {}) {
        let currencies = await this.publicGetTicker (this.extend ({
            'limit': 0,
        }, params));
        let result = {};
        for (let i = 0; i < currencies.length; i++) {
            let currency = currencies[i];
            let id = currency['symbol'];
            let name = currency['name'];
            // todo: will need to rethink the fees
            // to add support for multiple withdrawal/deposit methods and
            // differentiated fees for each particular method
            let precision = 8; // default precision, todo: fix "magic constants"
            let code = this.currencyCode (id, name);
            result[code] = {
                'id': id,
                'code': code,
                'info': currency,
                'name': name,
                'active': true,
                'status': 'ok',
                'fee': undefined, // todo: redesign
                'precision': precision,
                'limits': {
                    'amount': {
                        'min': Math.pow (10, -precision),
                        'max': Math.pow (10, precision),
                    },
                    'price': {
                        'min': Math.pow (10, -precision),
                        'max': Math.pow (10, precision),
                    },
                    'cost': {
                        'min': undefined,
                        'max': undefined,
                    },
                    'withdraw': {
                        'min': undefined,
                        'max': undefined,
                    },
                },
            };
        }
        return result;
    }

    sign (path, api = 'public', method = 'GET', params = {}, headers = undefined, body = undefined) {
        let url = this.urls['api'][api] + '/' + this.version + '/' + this.implodeParams (path, params);
        let query = this.omit (params, this.extractParams (path));
        if (Object.keys (query).length)
            url += '?' + this.urlencode (query);
        return { 'url': url, 'method': method, 'body': body, 'headers': headers };
    }

    async request (path, api = 'public', method = 'GET', params = {}, headers = undefined, body = undefined) {
        let response = await this.fetch2 (path, api, method, params, headers, body);
        if ('error' in response) {
            if (response['error']) {
                throw new ExchangeError (this.id + ' ' + this.json (response));
            }
        }
        return response;
    }
};
<|endoftext|>


<commit_before>var combat = {
    enemies: [], state: 0, season: 0, numPlantTurns: 0,
    lastTarget: 0, lastTargetCrop: false,
    expEarned: 0, moniesEarned: 0, itemsEarned: [], happyCows: [], usedShooters: [],
    grid: [], effectGrid: [], enemyGrid: [], enemywidth: 0, enemyheight: 0, enemyTile: "tech",
    isBossBattle: false, dx: 0, dy: 0, enemydx: 0, enemydy: 0,
    animHelper: null,
    startBattle: function(enemies) {
        worldmap.clean();
        gfx.clearAll();
        player.initGridDimensions();
        this.grid = this.getGrid(player.gridWidth, player.gridHeight);
        this.effectGrid = this.getGrid(player.gridWidth, player.gridHeight);
        game.currentInputHandler = this.menu;
        this.lastTargetCrop = false;
        this.lastTarget = 0;
        this.setSeason(enemies);
        this.expEarned = 0;
        this.moniesEarned = 0;
        this.itemsEarned = [];
        this.happyCows = [];
        this.usedShooters = [];
        if(player.equipment.weapon !== null && GetEquipment(player.equipment.weapon).tech) {
            var hasCharger = false;
            for(var x = 0; x < player.gridWidth; x++) {
                if(hasCharger) { break; }
                for(var y = 0; y < player.gridHeight; y++) {
                    var item = combat.grid[x][y]
                    if(item !== null && item.type === "sickle2") {
                        hasCharger = true;
                        break;
                    }
                }
            }
            if(!hasCharger) { player.equipment.weapon = "!sickle2_weak"; }
        }
        switch(player.gridWidth) {  // 3, 4, 6, 8, 10
            case 4: this.dx = 2; break;
            case 6: this.dx = 1; break;
            case 8: this.dx = 0.5; break;
            case 10: this.dx = 0; break;
            default: this.dx = 2; break;
        }
        switch(player.gridHeight) {  // 3, 4, 5, 6
            case 4: this.dy = 0.5; break;
            case 5: this.dy = 0.5; break;
            case 6: this.dy = 0.25; break;
            default: this.dy = 1.5; break;
        }
        this.enemywidth = 0;
        this.enemyheight = 0;
        this.isBossBattle = false;
        this.enemies = [];
        for(var i = 0; i < enemies.length; i++) {
            var enemy = GetEnemy(enemies[i]);
            this.isBossBattle = this.isBossBattle || enemy.boss;
            this.enemywidth += enemy.fieldwidth;
            this.enemyheight = Math.max(this.enemyheight, enemy.fieldheight);
            if(enemy.tile) { this.enemyTile = enemy.tile; }
            this.enemies.push(enemy);
        }
        this.animHelper = new CombatAnimHelper(this.enemies);
        this.enemyGrid = this.getGrid(this.enemywidth, this.enemyheight);
        this.enemydx = 10 + Math.floor((5 - this.enemywidth) / 2);
        this.enemydy = this.dy + Math.floor((player.gridHeight - this.enemyheight) / 2);
        combat.animHelper.DrawBackground();
        combat.animHelper.DrawCrops();
        combat.charAnimIdx = setInterval(function() { combat.animHelper.Animate() }, timers.CHARANIM);
        this.startRound();
        this.menu.setup();
    },
    getGrid: function(w, h) {
        var g = [];
        for(var x = 0; x < w; x++) {
            var row = [];
            for(var y = 0; y < h; y++) { row.push(null); }
            g.push(row);
        }
        return g;
    },
    setSeason: function(enemies) {
        var dist = [0, 0, 0, 0];
        for(var i = 0; i < enemies.length; i++) {
            var sd = GetEnemy(enemies[i]).seasonDistribution;
            for(var j = 0; j < 4; j++) { dist[j] += sd[j]; }
        }
        var season = Math.random();
        for(var j = 0; j < 4; j++) {
            dist[j] /= enemies.length;
            if(season <= dist[j]) {
                this.season = j;
                break;
            }
            season -= dist[j];
        }
    },
    startRound: function() {
        this.cleanUpEffects();
        this.numPlantTurns = player.getPlantingTurns();
        for(var i = 0; i < combat.enemies.length; i++) {
            combat.enemies[i].stickTurns = Math.max(0, combat.enemies[i].stickTurns - 1);
        }
        if(this.usedShooters.length > 0) {
            this.usedShooters = [];
            combat.animHelper.DrawBackground();
        }
        this.ageCrops();
        this.state = 0;
    },
    damagePlayer: function(damage) {
        if(player.equipment.gloves !== null) {
            var g = GetEquipment(player.equipment.gloves);
            var mult = (g.def === undefined) ? 1 : (1 - g.def);
            damage = Math.max(1, Math.floor(damage * mult));
        }
        player.health = Math.max(0, player.health - damage);
        return damage;
    },
    damageEnemy: function(enemyidx, damage) {
        this.enemies[enemyidx].health -= damage;
        var isFinalKill = false;
        if(this.enemies[enemyidx].health <= 0) {
            var e = this.enemies[enemyidx];
            this.expEarned += e.exp;
            if(e.soleKill) { isFinalKill = true; }
            for(var i = 0; i < e.drops.length; i++) {
                var dropInfo = e.drops[i];
                if(dropInfo.money) {
                    this.moniesEarned += Range(dropInfo.min, dropInfo.max);
                } else {
                    this.addDroppedSeedToItemsEarned(dropInfo.seed, Math.max(0, Range(dropInfo.min, dropInfo.max)));
                }
            }
        }
        if(isFinalKill) {
            for(var i = 0; i < this.enemies.length; i++) {
                if(i === enemyidx) { continue; }
                this.damageEnemy(i, this.enemies[i].health + 1);
            }
        }
    },
    stickEnemy: function(enemyidx, turns) {
        if(this.enemies[enemyidx].stickTurns === 0) { this.enemies[enemyidx].justStuck = true; }
        this.enemies[enemyidx].stickTurns = Math.min(this.enemies[enemyidx].stickTurns + turns, 8);
    },
    addDroppedSeedToItemsEarned: function(seed, amount) {
        if(amount === 0) { return; }
        for(var i = 0; i < this.itemsEarned; i++) {
            if(this.itemsEarned[i][0] == seed) {
                this.itemsEarned[i][1] += amount;
                return;
            }
        }
        this.itemsEarned.push([seed, amount]);
    },
    endTurn: function(caller) {
        this.clearAnimsAndRemoveCorpses();
        combat.animHelper.DrawBackground();
        combat.animHelper.DrawCrops();
        if(player.health <= 0 && !game.currentInputHandler.isTutorial) {
            combat.animHelper.SetPlayerAnimInfo([[3, 1, false, true]]);
            game.innerTransition(game.currentInputHandler, combat.inbetween, {
                next: combat.fuckingDead,
                text: "i can't believe the protagonist is fucking dead."
            });
            return;
        } else if(this.enemies.length == 0) {
            player.addExp(this.expEarned);
            combat.animHelper.SetPlayerAnimInfo([[1, 1]]);
            var text = "You did a the win.! You's's " + this.expEarned + "EXP";
            player.monies += this.moniesEarned;
            for(var i = 0; i < this.itemsEarned.length; i++) {
                player.increaseItem(this.itemsEarned[i][0], this.itemsEarned[i][1]);
            }
            if(this.moniesEarned > 0 && this.itemsEarned.length > 0) {
                text += ", " + this.moniesEarned + " coins";
                if(this.itemsEarned.length > 2) {
                    var count = 0;
                    for(var i = 0; i < this.itemsEarned.length; i++) { count += this.itemsEarned[i][1]; }
                    text += ", and " + count + " seeds!";
                } else {
                    for(var i = 0; i < this.itemsEarned.length; i++) {
                        var t = (i == (this.itemsEarned.length - 1) ? " and " : ", ");
                        t += this.itemsEarned[i][1] + " " + this.itemsEarned[i][0] + " seed" + (this.itemsEarned[i][1] > 1 ? "s" : "");
                        text += t;
                    }
                    text += "!";
                }
            } else if(this.moniesEarned > 0) {
                text += " and " + this.moniesEarned + " coins!";
            } else if(this.itemsEarned.length > 0) {
                if(this.itemsEarned.length > 2) {
                    var count = 0;
                    for(var i = 0; i < this.itemsEarned.length; i++) { count += this.itemsEarned[i][1]; }
                    text += " and " + count + " seeds!";
                } else {
                    for(var i = 0; i < this.itemsEarned.length; i++) {
                        var t = (i == (this.itemsEarned.length - 1) ? " and " : ", ");
                        t += this.itemsEarned[i][1] + " " + this.itemsEarned[i][0] + " seed" + (this.itemsEarned[i][1] > 1 ? "s" : "");
                        text += t;
                    }
                    text += "!";
                }
            } else {
                text += "!";
            }
            game.innerTransition(game.currentInputHandler, combat.inbetween, {
                next: combat.checkForLevelUp,
                text: text
            });
            return;
        }
        this.state++;
        if(this.state > combat.enemies.length) {
            this.startRound();
            game.innerTransition(caller, combat.menu);
        } else {
            var idx = this.state - 1;
            game.innerTransition(caller, combat.enemyTurn, { enemy: this.enemies[idx], idx: idx });
        }
    },
    cleanUpEffects: function() {
        var redraw = false;
        for(var x = 0; x < player.gridWidth; x++) {
            for(var y = 0; y < player.gridHeight; y++) {
                var obj = combat.effectGrid[x][y];
                if(obj === null) { continue; }
                if(--obj.duration <= 0) { redraw = true; combat.effectGrid[x][y] = null; }
            }
        }
        if(redraw) { combat.animHelper.DrawBackground(); }
    },
    clearAnimsAndRemoveCorpses: function() {
        combat.animHelper.CleanAnims();
        combat.animHelper.CleanEntities();
        combat.cleanFlaggedCrops();
        for(var i = combat.enemies.length - 1; i >= 0; i--) {
            combat.enemies[i].justStuck = false;
            if(combat.enemies[i].health <= 0) {
                combat.animHelper.RemoveEnemy(i);
                combat.enemies.splice(i, 1);
            }
        }
        for(var i = 0; i < combat.enemies.length; i++) {
            combat.animHelper.SetEnemyAnimInfo(i, [[combat.enemies[i].spriteidx, 0]]);
        }
    },
    wrapUpCombat: function() {
        if(player.equipment.weapon === "!sickle2_weak") { player.equipment.weapon = "!sickle2"; }
    },
    fuckingDead: function() {
        var inn = inns[player.lastInn];
        if(game.target !== null) {
            if(game.target.nonStandardGameOver !== undefined) {
                combat.wrapUpCombat();
                var postCombat = game.target.nonStandardGameOver;
                worldmap.clearTarget();
                clearInterval(combat.charAnimIdx);
                game.transition(combat.inbetween, worldmap, {
                    init: { x: 8.5, y: 9 },
                    map: "gameover",
                    noEntityUpdate: true,
                    postCombat: postCombat
                });
                return;
            }
            game.target.failed = true;
            game.target = null;
        }
        player.health = player.maxhealth;
        clearInterval(combat.charAnimIdx);
        combat.wrapUpCombat();
        game.transition(game.currentInputHandler, worldmap, {  init: { x: inn.x,  y: inn.y }, map: inn.map, isInn: true });
    },
    checkForLevelUp: function() {
        if(player.exp >= player.nextExp) {
            player.levelUp();
            combat.animHelper.SetPlayerAnimInfo([[2, 2], [2, 3]]);
            game.innerTransition(game.currentInputHandler, combat.inbetween, {
                next: combat.checkForLevelUp,
                text: "Whoah [gamer voice] nice! You hit level " + player.level + "!"
            });
        } else {
            combat.wrapUpCombat();
            var postCombat = (game.target !== null ? game.target.postBattle : undefined);
            if(game.target !== null && !game.target.dontClearTarget) { worldmap.clearTarget(); }
            clearInterval(combat.charAnimIdx);
            game.transition(combat.inbetween, worldmap, {
                init: worldmap.pos,
                map: worldmap.mapName,
                noEntityUpdate: true,
                postCombat: postCombat
            });
        }
    }
};<commit_msg>fixed issue with special enemy tiles persisting<commit_after>var combat = {
    enemies: [], state: 0, season: 0, numPlantTurns: 0,
    lastTarget: 0, lastTargetCrop: false,
    expEarned: 0, moniesEarned: 0, itemsEarned: [], happyCows: [], usedShooters: [],
    grid: [], effectGrid: [], enemyGrid: [], enemywidth: 0, enemyheight: 0, enemyTile: "tech",
    isBossBattle: false, dx: 0, dy: 0, enemydx: 0, enemydy: 0,
    animHelper: null,
    startBattle: function(enemies) {
        worldmap.clean();
        gfx.clearAll();
        player.initGridDimensions();
        this.grid = this.getGrid(player.gridWidth, player.gridHeight);
        this.effectGrid = this.getGrid(player.gridWidth, player.gridHeight);
        game.currentInputHandler = this.menu;
        this.lastTargetCrop = false;
        this.lastTarget = 0;
        this.setSeason(enemies);
        this.expEarned = 0;
        this.moniesEarned = 0;
        this.itemsEarned = [];
        this.happyCows = [];
        this.usedShooters = [];
        if(player.equipment.weapon !== null && GetEquipment(player.equipment.weapon).tech) {
            var hasCharger = false;
            for(var x = 0; x < player.gridWidth; x++) {
                if(hasCharger) { break; }
                for(var y = 0; y < player.gridHeight; y++) {
                    var item = combat.grid[x][y]
                    if(item !== null && item.type === "sickle2") {
                        hasCharger = true;
                        break;
                    }
                }
            }
            if(!hasCharger) { player.equipment.weapon = "!sickle2_weak"; }
        }
        switch(player.gridWidth) {  // 3, 4, 6, 8, 10
            case 4: this.dx = 2; break;
            case 6: this.dx = 1; break;
            case 8: this.dx = 0.5; break;
            case 10: this.dx = 0; break;
            default: this.dx = 2; break;
        }
        switch(player.gridHeight) {  // 3, 4, 5, 6
            case 4: this.dy = 0.5; break;
            case 5: this.dy = 0.5; break;
            case 6: this.dy = 0.25; break;
            default: this.dy = 1.5; break;
        }
        this.enemywidth = 0;
        this.enemyheight = 0;
        this.isBossBattle = false;
        this.enemies = [];
        for(var i = 0; i < enemies.length; i++) {
            var enemy = GetEnemy(enemies[i]);
            this.isBossBattle = this.isBossBattle || enemy.boss;
            this.enemywidth += enemy.fieldwidth;
            this.enemyheight = Math.max(this.enemyheight, enemy.fieldheight);
            if(enemy.tile) { this.enemyTile = enemy.tile; } else { this.enemyTile = "tech"; }
            this.enemies.push(enemy);
        }
        this.animHelper = new CombatAnimHelper(this.enemies);
        this.enemyGrid = this.getGrid(this.enemywidth, this.enemyheight);
        this.enemydx = 10 + Math.floor((5 - this.enemywidth) / 2);
        this.enemydy = this.dy + Math.floor((player.gridHeight - this.enemyheight) / 2);
        combat.animHelper.DrawBackground();
        combat.animHelper.DrawCrops();
        combat.charAnimIdx = setInterval(function() { combat.animHelper.Animate() }, timers.CHARANIM);
        this.startRound();
        this.menu.setup();
    },
    getGrid: function(w, h) {
        var g = [];
        for(var x = 0; x < w; x++) {
            var row = [];
            for(var y = 0; y < h; y++) { row.push(null); }
            g.push(row);
        }
        return g;
    },
    setSeason: function(enemies) {
        var dist = [0, 0, 0, 0];
        for(var i = 0; i < enemies.length; i++) {
            var sd = GetEnemy(enemies[i]).seasonDistribution;
            for(var j = 0; j < 4; j++) { dist[j] += sd[j]; }
        }
        var season = Math.random();
        for(var j = 0; j < 4; j++) {
            dist[j] /= enemies.length;
            if(season <= dist[j]) {
                this.season = j;
                break;
            }
            season -= dist[j];
        }
    },
    startRound: function() {
        this.cleanUpEffects();
        this.numPlantTurns = player.getPlantingTurns();
        for(var i = 0; i < combat.enemies.length; i++) {
            combat.enemies[i].stickTurns = Math.max(0, combat.enemies[i].stickTurns - 1);
        }
        if(this.usedShooters.length > 0) {
            this.usedShooters = [];
            combat.animHelper.DrawBackground();
        }
        this.ageCrops();
        this.state = 0;
    },
    damagePlayer: function(damage) {
        if(player.equipment.gloves !== null) {
            var g = GetEquipment(player.equipment.gloves);
            var mult = (g.def === undefined) ? 1 : (1 - g.def);
            damage = Math.max(1, Math.floor(damage * mult));
        }
        player.health = Math.max(0, player.health - damage);
        return damage;
    },
    damageEnemy: function(enemyidx, damage) {
        this.enemies[enemyidx].health -= damage;
        var isFinalKill = false;
        if(this.enemies[enemyidx].health <= 0) {
            var e = this.enemies[enemyidx];
            this.expEarned += e.exp;
            if(e.soleKill) { isFinalKill = true; }
            for(var i = 0; i < e.drops.length; i++) {
                var dropInfo = e.drops[i];
                if(dropInfo.money) {
                    this.moniesEarned += Range(dropInfo.min, dropInfo.max);
                } else {
                    this.addDroppedSeedToItemsEarned(dropInfo.seed, Math.max(0, Range(dropInfo.min, dropInfo.max)));
                }
            }
        }
        if(isFinalKill) {
            for(var i = 0; i < this.enemies.length; i++) {
                if(i === enemyidx) { continue; }
                this.damageEnemy(i, this.enemies[i].health + 1);
            }
        }
    },
    stickEnemy: function(enemyidx, turns) {
        if(this.enemies[enemyidx].stickTurns === 0) { this.enemies[enemyidx].justStuck = true; }
        this.enemies[enemyidx].stickTurns = Math.min(this.enemies[enemyidx].stickTurns + turns, 8);
    },
    addDroppedSeedToItemsEarned: function(seed, amount) {
        if(amount === 0) { return; }
        for(var i = 0; i < this.itemsEarned; i++) {
            if(this.itemsEarned[i][0] == seed) {
                this.itemsEarned[i][1] += amount;
                return;
            }
        }
        this.itemsEarned.push([seed, amount]);
    },
    endTurn: function(caller) {
        this.clearAnimsAndRemoveCorpses();
        combat.animHelper.DrawBackground();
        combat.animHelper.DrawCrops();
        if(player.health <= 0 && !game.currentInputHandler.isTutorial) {
            combat.animHelper.SetPlayerAnimInfo([[3, 1, false, true]]);
            game.innerTransition(game.currentInputHandler, combat.inbetween, {
                next: combat.fuckingDead,
                text: "i can't believe the protagonist is fucking dead."
            });
            return;
        } else if(this.enemies.length == 0) {
            player.addExp(this.expEarned);
            combat.animHelper.SetPlayerAnimInfo([[1, 1]]);
            var text = "You did a the win.! You's's " + this.expEarned + "EXP";
            player.monies += this.moniesEarned;
            for(var i = 0; i < this.itemsEarned.length; i++) {
                player.increaseItem(this.itemsEarned[i][0], this.itemsEarned[i][1]);
            }
            if(this.moniesEarned > 0 && this.itemsEarned.length > 0) {
                text += ", " + this.moniesEarned + " coins";
                if(this.itemsEarned.length > 2) {
                    var count = 0;
                    for(var i = 0; i < this.itemsEarned.length; i++) { count += this.itemsEarned[i][1]; }
                    text += ", and " + count + " seeds!";
                } else {
                    for(var i = 0; i < this.itemsEarned.length; i++) {
                        var t = (i == (this.itemsEarned.length - 1) ? " and " : ", ");
                        t += this.itemsEarned[i][1] + " " + this.itemsEarned[i][0] + " seed" + (this.itemsEarned[i][1] > 1 ? "s" : "");
                        text += t;
                    }
                    text += "!";
                }
            } else if(this.moniesEarned > 0) {
                text += " and " + this.moniesEarned + " coins!";
            } else if(this.itemsEarned.length > 0) {
                if(this.itemsEarned.length > 2) {
                    var count = 0;
                    for(var i = 0; i < this.itemsEarned.length; i++) { count += this.itemsEarned[i][1]; }
                    text += " and " + count + " seeds!";
                } else {
                    for(var i = 0; i < this.itemsEarned.length; i++) {
                        var t = (i == (this.itemsEarned.length - 1) ? " and " : ", ");
                        t += this.itemsEarned[i][1] + " " + this.itemsEarned[i][0] + " seed" + (this.itemsEarned[i][1] > 1 ? "s" : "");
                        text += t;
                    }
                    text += "!";
                }
            } else {
                text += "!";
            }
            game.innerTransition(game.currentInputHandler, combat.inbetween, {
                next: combat.checkForLevelUp,
                text: text
            });
            return;
        }
        this.state++;
        if(this.state > combat.enemies.length) {
            this.startRound();
            game.innerTransition(caller, combat.menu);
        } else {
            var idx = this.state - 1;
            game.innerTransition(caller, combat.enemyTurn, { enemy: this.enemies[idx], idx: idx });
        }
    },
    cleanUpEffects: function() {
        var redraw = false;
        for(var x = 0; x < player.gridWidth; x++) {
            for(var y = 0; y < player.gridHeight; y++) {
                var obj = combat.effectGrid[x][y];
                if(obj === null) { continue; }
                if(--obj.duration <= 0) { redraw = true; combat.effectGrid[x][y] = null; }
            }
        }
        if(redraw) { combat.animHelper.DrawBackground(); }
    },
    clearAnimsAndRemoveCorpses: function() {
        combat.animHelper.CleanAnims();
        combat.animHelper.CleanEntities();
        combat.cleanFlaggedCrops();
        for(var i = combat.enemies.length - 1; i >= 0; i--) {
            combat.enemies[i].justStuck = false;
            if(combat.enemies[i].health <= 0) {
                combat.animHelper.RemoveEnemy(i);
                combat.enemies.splice(i, 1);
            }
        }
        for(var i = 0; i < combat.enemies.length; i++) {
            combat.animHelper.SetEnemyAnimInfo(i, [[combat.enemies[i].spriteidx, 0]]);
        }
    },
    wrapUpCombat: function() {
        if(player.equipment.weapon === "!sickle2_weak") { player.equipment.weapon = "!sickle2"; }
    },
    fuckingDead: function() {
        var inn = inns[player.lastInn];
        if(game.target !== null) {
            if(game.target.nonStandardGameOver !== undefined) {
                combat.wrapUpCombat();
                var postCombat = game.target.nonStandardGameOver;
                worldmap.clearTarget();
                clearInterval(combat.charAnimIdx);
                game.transition(combat.inbetween, worldmap, {
                    init: { x: 8.5, y: 9 },
                    map: "gameover",
                    noEntityUpdate: true,
                    postCombat: postCombat
                });
                return;
            }
            game.target.failed = true;
            game.target = null;
        }
        player.health = player.maxhealth;
        clearInterval(combat.charAnimIdx);
        combat.wrapUpCombat();
        game.transition(game.currentInputHandler, worldmap, {  init: { x: inn.x,  y: inn.y }, map: inn.map, isInn: true });
    },
    checkForLevelUp: function() {
        if(player.exp >= player.nextExp) {
            player.levelUp();
            combat.animHelper.SetPlayerAnimInfo([[2, 2], [2, 3]]);
            game.innerTransition(game.currentInputHandler, combat.inbetween, {
                next: combat.checkForLevelUp,
                text: "Whoah [gamer voice] nice! You hit level " + player.level + "!"
            });
        } else {
            combat.wrapUpCombat();
            var postCombat = (game.target !== null ? game.target.postBattle : undefined);
            if(game.target !== null && !game.target.dontClearTarget) { worldmap.clearTarget(); }
            clearInterval(combat.charAnimIdx);
            game.transition(combat.inbetween, worldmap, {
                init: worldmap.pos,
                map: worldmap.mapName,
                noEntityUpdate: true,
                postCombat: postCombat
            });
        }
    }
};<|endoftext|>


<commit_before>define(["js/core/EventDispatcher", "js/lib/parser", "js/core/Binding","underscore"],

    function (EventDispatcher, Parser, Binding, _) {
        // global invalidation timer
        var globalInvalidationQueue = (function() {

            var id,
                callbacks = [],
                work = function() {
                    for (var i = 0; i < callbacks.length; i++) {
                        try {
                            callbacks[i]();
                        } catch (e) {
                            (console.warn || console.log).call(console, e);
                        }
                    }

                    callbacks = [];
                    id = null;
                };

            return {
                addCallback: function(callback) {
                    callbacks.push(callback);

                    if (!id) {
                        id = setTimeout(work, 1000 / 60);
                    }
                }
            }
        })();

        var isEqual = function(a,b){
            if(a instanceof EventDispatcher || b instanceof EventDispatcher){
                return a === b;
            }
            return _.isEqual(a,b);
        };

        var Bindable = EventDispatcher.inherit("js.core.Bindable",
            {
                /***
                 * creates a new instance of Bindable, which can be bound to components
                 *
                 * @param {Object} [attributes] the default attributes which will be set during instantiation
                 */
                ctor: function (attributes) {
                    this.$eventBindables = [];

                    // call the base class constructor
                    this.callBase(null);

                    this.$ = {};
                    this.$invalidatedProperties = {};
                    this.$invalidated = false;

                    _.extend(this._eventAttributes, this.base._eventAttributes || {});

                    attributes = attributes || {};

                    var defaultAttributes = this._defaultAttributes(), defaultAttribute;

                    for (var key in defaultAttributes) {
                        if (defaultAttributes.hasOwnProperty(key)) {
                            defaultAttribute = defaultAttributes[key];
                            if (!attributes.hasOwnProperty(key)) {
                                if (_.isFunction(defaultAttribute)) {
                                    // Function as default -> construct new Object
                                    attributes[key] = new (defaultAttribute)();
                                } else {
                                    // RegExp should not be cloned!
                                    if(defaultAttributes[key] instanceof RegExp){
                                        attributes[key] = defaultAttribute;
                                    }else{
                                        attributes[key] = _.clone(defaultAttribute);
                                    }
                                }
                            }
                        }
                    }

                    this.$ = attributes;

                    this.$previousAttributes = {}

                },
                /**
                 * Here you can define the default attributes of the instance.
                 *
                 * @static
                 */
                defaults: {
                },
                /**
                 * Writes attributes back to the source
                 */
                sync: function(){
                    if(this._$source) {
                        var val, attributes = {}, unsetAttributes = {};
                        for (var key in this.$) {
                            if (this.$.hasOwnProperty(key)) {
                                val = this.$[key];
                                if(val instanceof Bindable && val.sync()){
                                    attributes[key] = val._$source;
                                }else{
                                    attributes[key] = val;
                                }
                            }
                        }
                        // remove all attributes, which are not in clone
                        for (var sourceKey in this._$source.$){
                            if(this._$source.$.hasOwnProperty(sourceKey)){
                                if(!attributes.hasOwnProperty(sourceKey)){
                                    unsetAttributes[sourceKey] = "";
                                }
                            }
                        }
                        this._$source.set(unsetAttributes,{unset:true});
                        this._$source.set(attributes);
                        return true;
                    }else{
                        return false;
                    }

                },
                /***
                 * This method returns a copy of the Object
                 * @return {js.core.Bindable} a fresh copy of the Bindable
                 */
                clone: function (options) {
                    var ret = {};
                    options = options || {};
                    for (var key in this.$) {
                        if (this.$.hasOwnProperty(key)) {
                            if(options.exclude){
                                if(options.exclude instanceof Array){
                                    if(_.include(options.exclude,key)){
                                        ret[key] = this.$[key];
                                        continue;
                                    }
                                }
                            }
                            ret[key] = this._cloneAttribute(this.$[key], key);
                        }
                    }
                    var b = new this.factory(ret);
                    b._$source = this;
                    return b;
                },
                /**
                 * Returns a copy of the attribute. This method is a hook for further cloning options
                 * @param attribute
                 * @param key
                 * @private
                 */
                _cloneAttribute: function(attribute, key){
                    if(attribute instanceof Bindable){
                        return attribute.clone();
                    }else if(_.isArray(attribute)){
                        var retArray = [];
                        for(var i = 0; i < attribute.length; i++){
                            retArray.push(this._cloneAttribute(attribute[i]));
                        }
                        return retArray;
                    } else if(attribute instanceof Date){
                        return new Date(attribute.getTime());
                    } else if(_.isObject(attribute)){
                        var retObject = {};
                        for (var attrKey in attribute){
                            if(attribute.hasOwnProperty(attrKey)){
                                retObject[attrKey] = this._cloneAttribute(attribute[attrKey], attrKey);
                            }
                        }
                        return retObject;
                    } else{
                        return attribute;
                    }
                },
                /***
                 *
                 * @return {Object} returns the default attributes and includes the defaults from base classes
                 * @private
                 */
                _defaultAttributes: function () {
                    return this._generateDefaultsChain("defaults");
                },

                /***
                 * generates a default chain containing the values from this instance and base classes
                 *
                 * @param {String} property - the name of the static property used to find defaults
                 * @return {*}
                 * @private
                 */
                _generateDefaultsChain: function (property) {

                    this.factory.chainCache = this.factory.chainCache || {};

                    if (this.factory.chainCache[property]) {
                        return this.factory.chainCache[property];
                    }

                    var ret = this[property],
                        base = this.base;

                    while (base) {
                        var baseValue = base[property];
                        for (var key in baseValue) {
                            if (baseValue.hasOwnProperty(key)) {
                                if (_.isUndefined(ret[key])) {
                                    ret[key] = baseValue[key];
                                }
                            }
                        }

                        base = base.base;
                    }

                    // cache the chain
                    this.factory.chainCache[property] = ret;

                    return ret;
                },

                /**
                 * Sets new values for attributes and notify about changes
                 *
                 * @param {String} key The attribute key
                 * @param {String} value The attribute value
                 * @param {Object} options A hash of options
                 * @param {Boolean} [options.silent=false] if true no event is triggered on change
                 * @param {Boolean} [options.unset=false] if true the attribute gets deleted
                 * @return {js.core.Bindable} this
                 */
                set: function (key, value, options) {
                    var attributes = {};

                    if(_.isNumber(key)){
                        key = String(key);
                    }

                    if (_.isString(key)) {
                        attributes[key] = value;
                    } else {
                        options = value;
                        attributes = key;
                    }

                    options = options || {silent: false, unset: false, force: false};

                    // for un-setting attributes
                    if (options.unset) {
                        for (key in attributes) {
                            if (attributes.hasOwnProperty(key)) {
                                attributes[key] = void 0;
                            }
                        }
                    }

                    var changedAttributes = {},
                        changedAttributesCount = 0,
                        now = this.$,
                        val, prev;

                    for (key in attributes) {
                        if (attributes.hasOwnProperty(key)) {
                            // get the value
                            val = attributes[key];
                            // unset attribute or change it ...
                            if (options.unset === true) {
                                prev = now[key];
                                delete now[key];
                                changedAttributes[key] = undefined;
                                changedAttributesCount++;
                                this.$previousAttributes[key] = prev;
                            } else {
                                if (options.force || !isEqual(now[key],attributes[key])) {
                                    prev = now[key];
                                    this.$previousAttributes[key] = prev;
                                    now[key] = attributes[key];
                                    changedAttributes[key] = now[key];

                                    changedAttributesCount++;
                                }
                            }
                        }
                    }

                    if (changedAttributesCount) {
                        for (key in changedAttributes) {
                            if (changedAttributes.hasOwnProperty(key)) {
                                var commitMethodName = '_commit' + key.charAt(0).toUpperCase() + key.substr(1);

                                if (this[commitMethodName] instanceof Function) {
                                    // call method

                                    if (this[commitMethodName](now[key], this.$previousAttributes[key], options) === false) {
                                        // false returned rollback
                                        changedAttributesCount--;
                                        now[key] = this.$previousAttributes[key];
                                    }
                                }
                            }
                        }

                        if (changedAttributesCount) {
                            this._commitChangedAttributes(changedAttributes,options);
                            if (!options.silent) {
                                for (key in changedAttributes) {
                                    if (changedAttributes.hasOwnProperty(key)) {
                                        this.trigger('change:' + key, changedAttributes[key], this);
                                    }
                                }
                                this.trigger('change', changedAttributes, this);
                            }
                        }

                    }

                    return this;
                },

                setLater: function(key, value) {

                    if (_.isString(key)) {
                        key = {};
                        key[key] = value;
                    }

                    _.extend(this.$invalidatedProperties, key);

                    if (!this.$invalidated) {
                        this.$invalidated = true;
                        globalInvalidationQueue.addCallback(this._commitInvalidatedAttributes);
                    }

                },

                _commitInvalidatedAttributes: function() {
                    this.set(this.$invalidatedProperties);
                    this.$invalidatedProperties = {};
                },

                /***
                 * evaluates a path to retrieve a value
                 *
                 * @param {Object} [scope=this] the scope where the path is evaluated
                 * @param {String} key or path
                 * @returns the value for the path or undefined
                 */
                get: function(scope, key) {
                    if(!key){
                        key = scope;
                        scope = this;
                    }

                    if(!key) {
                        return null;
                    }

                    var path;

                    // if we have a path object
                    if(_.isArray(key)){
                        path = key;
                    // path element
                    }else if(_.isObject(key)){
                        path = [key];
                    }else{
                        path = Parser.parse(key, "path");
                    }

                    var pathElement, val;
                    for (var j = 0; scope && j < path.length; j++) {
                        pathElement = path[j];
                        if (pathElement.type == "fnc") {
                            var fnc = scope[pathElement.name];
                            var parameters = pathElement.parameter;
                            for (var i = 0; i < parameters.length; i++) {
                                var param = parameters[i];
                                if (_.isObject(param)) {
                                    param.type = "static";
                                    parameters[i] = this.get(param.path);
                                }
                            }
                            scope = fnc.apply(scope, parameters);
                        } else if (pathElement.type == "var") {
                            if (scope instanceof Bindable) {
                                if (path.length - 1 === j ) {
                                    val = scope.$[pathElement.name];
                                    if(_.isUndefined(val)){
                                        val = scope[pathElement.name];
                                    }
                                    scope = val;
                                } else {
                                    scope = scope.get(pathElement.name);
                                }

                            } else {
                                scope = scope[pathElement.name];
                            }
                        }

                        if (scope && pathElement.index !== '') {
                            // if it's an array
                            if (_.isArray(scope)) {
                                scope = scope[pathElement.index];
                                // if it's a list
                            } else if (scope.at) {
                                scope = scope.at(pathElement.index);
                            }
                        }
                    }
                    return scope;
                },

                /***
                 * determinate if a attribute is available
                 *
                 * @param {String} path - to get the value
                 * @returns {Boolean} true if attribute is not undefined
                 */
                has: function (path) {
                    return !_.isUndefined(this.get(path));
                },

                /***
                 * called after attributes has set and some of the has been changed
                 *
                 * @param {Object} attributes - contains the changed attributes
                 * @param {Object} options - the options passed in the set method
                 * @abstract
                 * @private
                 */
                _commitChangedAttributes: function (attributes, options) {
                    // override
                },

                /***
                 * Unset attribute on $
                 * @param {String|Object} key - the attribute or attributes to unset
                 * @param {Object} [options]
                 * @return {this}
                 */
                unset: function (key, options) {
                    (options || (options = {})).unset = true;
                    return this.set(key, null, options);
                },

                /***
                 * Clears all attributes
                 * @return {this}
                 */
                clear: function () {
                    return this.set(this.$, {unset: true});
                },

                /***
                 * Binds an event handler function to an EventDispatcher
                 *
                 * @param {String} path - a binding path e.g. a.b.c
                 * @param {String} event - the type of the event which should be bound
                 * @param {Function} callback - the event handler function
                 * @param {Object} [thisArg] - the thisArg used for calling the event handler
                 */
                bind: function (path, event, callback, thisArg) {
                    if (event instanceof Function && _.isString(path)) {
                        this.callBase(path, event, callback);
                    } else {
                        if(_.isArray(path) && path.length > 0){
                            thisArg = callback;
                            callback = event;
                            event = path[1];
                            path = path[0];
                        }
                        var eb = new EventBindable({
                            path: path,
                            event: event,
                            scope: thisArg,
                            callback: callback,
                            value: null
                        });
                        eb.set('binding', new Binding({path: path, scope: this, target: eb, targetKey: 'value'}));
                        this.$eventBindables.push(eb);
                    }
                },

                /***
                 * Unbinds an bound event handler from an EventDispatcher.
                 *
                 * @param {String} [path] - the path from which the event should be unbound
                 * @param {String} event - the type of the event
                 * @param {Function} callback - the event handler which is currently bound
                 * TODO: why a scope is passed here?
                 * @param {Object} [scope]
                 */
                unbind: function (path, event, callback, scope) {
                    if (event instanceof Function) {
                        this.callBase(path, event, callback);
                    } else {
                        var eb;
                        for (var i = this.$eventBindables.length - 1; i >= 0; i--) {
                            eb = this.$eventBindables[i];
                            if (eb.$.scope === scope && eb.$.path === path && eb.$.event === event && eb.$.callback === callback) {
                                // unbind
                                eb.destroy();
                                this.$eventBindables.slice(i, 1);
                            }
                        }
                    }
                },

                /***
                 * Destroys all event bindings and triggers a destroy event
                 * @return {this}
                 */
                destroy: function() {
                    this.callBase();
                    for(var i = 0; i < this.$eventBindables.length; i++){
                        this.$eventBindables[i].destroy();
                    }

                    this.trigger('destroy',this);
                    return this;
                },
                isEqual: function (b) {
                    return isEqual(this,b);
                }
            });

        var EventBindable = Bindable.inherit({
            _commitChangedAttributes: function(attributes){
                this.callBase();
                if(attributes.binding){
                    this.set('value', attributes.binding.getValue());
                }else if(attributes.hasOwnProperty('value')){
                    var value = attributes.value;
                    this._unbindEvent(this.$previousAttributes['value']);
                    this._bindEvent(value);

                    if(this.$.binding){
                        this.$.binding.trigger();
                    }
                }

            },
            _unbindEvent: function (value) {
                if (value && value instanceof EventDispatcher) {
                    value.unbind(this.$.event, this.$.callback, this.$.scope);
                }
            },
            _bindEvent: function (value) {
                if (value && value instanceof EventDispatcher) {
                    value.bind(this.$.event, this.$.callback, this.$.scope);
                }
            },
            destroy: function () {
                this._unbindEvent(this.$.value);
                this.$.binding.destroy();

                this.callBase();
            }
        });

        return Bindable;

    });<commit_msg>fixed unbind of binding paths<commit_after>define(["js/core/EventDispatcher", "js/lib/parser", "js/core/Binding","underscore"],

    function (EventDispatcher, Parser, Binding, _) {
        // global invalidation timer
        var globalInvalidationQueue = (function() {

            var id,
                callbacks = [],
                work = function() {
                    for (var i = 0; i < callbacks.length; i++) {
                        try {
                            callbacks[i]();
                        } catch (e) {
                            (console.warn || console.log).call(console, e);
                        }
                    }

                    callbacks = [];
                    id = null;
                };

            return {
                addCallback: function(callback) {
                    callbacks.push(callback);

                    if (!id) {
                        id = setTimeout(work, 1000 / 60);
                    }
                }
            }
        })();

        var isEqual = function(a,b){
            if(a instanceof EventDispatcher || b instanceof EventDispatcher){
                return a === b;
            }
            return _.isEqual(a,b);
        };

        var Bindable = EventDispatcher.inherit("js.core.Bindable",
            {
                /***
                 * creates a new instance of Bindable, which can be bound to components
                 *
                 * @param {Object} [attributes] the default attributes which will be set during instantiation
                 */
                ctor: function (attributes) {
                    this.$eventBindables = [];

                    // call the base class constructor
                    this.callBase(null);

                    this.$ = {};
                    this.$invalidatedProperties = {};
                    this.$invalidated = false;

                    _.extend(this._eventAttributes, this.base._eventAttributes || {});

                    attributes = attributes || {};

                    var defaultAttributes = this._defaultAttributes(), defaultAttribute;

                    for (var key in defaultAttributes) {
                        if (defaultAttributes.hasOwnProperty(key)) {
                            defaultAttribute = defaultAttributes[key];
                            if (!attributes.hasOwnProperty(key)) {
                                if (_.isFunction(defaultAttribute)) {
                                    // Function as default -> construct new Object
                                    attributes[key] = new (defaultAttribute)();
                                } else {
                                    // RegExp should not be cloned!
                                    if(defaultAttributes[key] instanceof RegExp){
                                        attributes[key] = defaultAttribute;
                                    }else{
                                        attributes[key] = _.clone(defaultAttribute);
                                    }
                                }
                            }
                        }
                    }

                    this.$ = attributes;

                    this.$previousAttributes = {}

                },
                /**
                 * Here you can define the default attributes of the instance.
                 *
                 * @static
                 */
                defaults: {
                },
                /**
                 * Writes attributes back to the source
                 */
                sync: function(){
                    if(this._$source) {
                        var val, attributes = {}, unsetAttributes = {};
                        for (var key in this.$) {
                            if (this.$.hasOwnProperty(key)) {
                                val = this.$[key];
                                if(val instanceof Bindable && val.sync()){
                                    attributes[key] = val._$source;
                                }else{
                                    attributes[key] = val;
                                }
                            }
                        }
                        // remove all attributes, which are not in clone
                        for (var sourceKey in this._$source.$){
                            if(this._$source.$.hasOwnProperty(sourceKey)){
                                if(!attributes.hasOwnProperty(sourceKey)){
                                    unsetAttributes[sourceKey] = "";
                                }
                            }
                        }
                        this._$source.set(unsetAttributes,{unset:true});
                        this._$source.set(attributes);
                        return true;
                    }else{
                        return false;
                    }

                },
                /***
                 * This method returns a copy of the Object
                 * @return {js.core.Bindable} a fresh copy of the Bindable
                 */
                clone: function (options) {
                    var ret = {};
                    options = options || {};
                    for (var key in this.$) {
                        if (this.$.hasOwnProperty(key)) {
                            if(options.exclude){
                                if(options.exclude instanceof Array){
                                    if(_.include(options.exclude,key)){
                                        ret[key] = this.$[key];
                                        continue;
                                    }
                                }
                            }
                            ret[key] = this._cloneAttribute(this.$[key], key);
                        }
                    }
                    var b = new this.factory(ret);
                    b._$source = this;
                    return b;
                },
                /**
                 * Returns a copy of the attribute. This method is a hook for further cloning options
                 * @param attribute
                 * @param key
                 * @private
                 */
                _cloneAttribute: function(attribute, key){
                    if(attribute instanceof Bindable){
                        return attribute.clone();
                    }else if(_.isArray(attribute)){
                        var retArray = [];
                        for(var i = 0; i < attribute.length; i++){
                            retArray.push(this._cloneAttribute(attribute[i]));
                        }
                        return retArray;
                    } else if(attribute instanceof Date){
                        return new Date(attribute.getTime());
                    } else if(_.isObject(attribute)){
                        var retObject = {};
                        for (var attrKey in attribute){
                            if(attribute.hasOwnProperty(attrKey)){
                                retObject[attrKey] = this._cloneAttribute(attribute[attrKey], attrKey);
                            }
                        }
                        return retObject;
                    } else{
                        return attribute;
                    }
                },
                /***
                 *
                 * @return {Object} returns the default attributes and includes the defaults from base classes
                 * @private
                 */
                _defaultAttributes: function () {
                    return this._generateDefaultsChain("defaults");
                },

                /***
                 * generates a default chain containing the values from this instance and base classes
                 *
                 * @param {String} property - the name of the static property used to find defaults
                 * @return {*}
                 * @private
                 */
                _generateDefaultsChain: function (property) {

                    this.factory.chainCache = this.factory.chainCache || {};

                    if (this.factory.chainCache[property]) {
                        return this.factory.chainCache[property];
                    }

                    var ret = this[property],
                        base = this.base;

                    while (base) {
                        var baseValue = base[property];
                        for (var key in baseValue) {
                            if (baseValue.hasOwnProperty(key)) {
                                if (_.isUndefined(ret[key])) {
                                    ret[key] = baseValue[key];
                                }
                            }
                        }

                        base = base.base;
                    }

                    // cache the chain
                    this.factory.chainCache[property] = ret;

                    return ret;
                },

                /**
                 * Sets new values for attributes and notify about changes
                 *
                 * @param {String} key The attribute key
                 * @param {String} value The attribute value
                 * @param {Object} options A hash of options
                 * @param {Boolean} [options.silent=false] if true no event is triggered on change
                 * @param {Boolean} [options.unset=false] if true the attribute gets deleted
                 * @return {js.core.Bindable} this
                 */
                set: function (key, value, options) {
                    var attributes = {};

                    if(_.isNumber(key)){
                        key = String(key);
                    }

                    if (_.isString(key)) {
                        attributes[key] = value;
                    } else {
                        options = value;
                        attributes = key;
                    }

                    options = options || {silent: false, unset: false, force: false};

                    // for un-setting attributes
                    if (options.unset) {
                        for (key in attributes) {
                            if (attributes.hasOwnProperty(key)) {
                                attributes[key] = void 0;
                            }
                        }
                    }

                    var changedAttributes = {},
                        changedAttributesCount = 0,
                        now = this.$,
                        val, prev;

                    for (key in attributes) {
                        if (attributes.hasOwnProperty(key)) {
                            // get the value
                            val = attributes[key];
                            // unset attribute or change it ...
                            if (options.unset === true) {
                                prev = now[key];
                                delete now[key];
                                changedAttributes[key] = undefined;
                                changedAttributesCount++;
                                this.$previousAttributes[key] = prev;
                            } else {
                                if (options.force || !isEqual(now[key],attributes[key])) {
                                    prev = now[key];
                                    this.$previousAttributes[key] = prev;
                                    now[key] = attributes[key];
                                    changedAttributes[key] = now[key];

                                    changedAttributesCount++;
                                }
                            }
                        }
                    }

                    if (changedAttributesCount) {
                        for (key in changedAttributes) {
                            if (changedAttributes.hasOwnProperty(key)) {
                                var commitMethodName = '_commit' + key.charAt(0).toUpperCase() + key.substr(1);

                                if (this[commitMethodName] instanceof Function) {
                                    // call method

                                    if (this[commitMethodName](now[key], this.$previousAttributes[key], options) === false) {
                                        // false returned rollback
                                        changedAttributesCount--;
                                        now[key] = this.$previousAttributes[key];
                                    }
                                }
                            }
                        }

                        if (changedAttributesCount) {
                            this._commitChangedAttributes(changedAttributes,options);
                            if (!options.silent) {
                                for (key in changedAttributes) {
                                    if (changedAttributes.hasOwnProperty(key)) {
                                        this.trigger('change:' + key, changedAttributes[key], this);
                                    }
                                }
                                this.trigger('change', changedAttributes, this);
                            }
                        }

                    }

                    return this;
                },

                setLater: function(key, value) {

                    if (_.isString(key)) {
                        key = {};
                        key[key] = value;
                    }

                    _.extend(this.$invalidatedProperties, key);

                    if (!this.$invalidated) {
                        this.$invalidated = true;
                        globalInvalidationQueue.addCallback(this._commitInvalidatedAttributes);
                    }

                },

                _commitInvalidatedAttributes: function() {
                    this.set(this.$invalidatedProperties);
                    this.$invalidatedProperties = {};
                },

                /***
                 * evaluates a path to retrieve a value
                 *
                 * @param {Object} [scope=this] the scope where the path is evaluated
                 * @param {String} key or path
                 * @returns the value for the path or undefined
                 */
                get: function(scope, key) {
                    if(!key){
                        key = scope;
                        scope = this;
                    }

                    if(!key) {
                        return null;
                    }

                    var path;

                    // if we have a path object
                    if(_.isArray(key)){
                        path = key;
                    // path element
                    }else if(_.isObject(key)){
                        path = [key];
                    }else{
                        path = Parser.parse(key, "path");
                    }

                    var pathElement, val;
                    for (var j = 0; scope && j < path.length; j++) {
                        pathElement = path[j];
                        if (pathElement.type == "fnc") {
                            var fnc = scope[pathElement.name];
                            var parameters = pathElement.parameter;
                            for (var i = 0; i < parameters.length; i++) {
                                var param = parameters[i];
                                if (_.isObject(param)) {
                                    param.type = "static";
                                    parameters[i] = this.get(param.path);
                                }
                            }
                            scope = fnc.apply(scope, parameters);
                        } else if (pathElement.type == "var") {
                            if (scope instanceof Bindable) {
                                if (path.length - 1 === j ) {
                                    val = scope.$[pathElement.name];
                                    if(_.isUndefined(val)){
                                        val = scope[pathElement.name];
                                    }
                                    scope = val;
                                } else {
                                    scope = scope.get(pathElement.name);
                                }

                            } else {
                                scope = scope[pathElement.name];
                            }
                        }

                        if (scope && pathElement.index !== '') {
                            // if it's an array
                            if (_.isArray(scope)) {
                                scope = scope[pathElement.index];
                                // if it's a list
                            } else if (scope.at) {
                                scope = scope.at(pathElement.index);
                            }
                        }
                    }
                    return scope;
                },

                /***
                 * determinate if a attribute is available
                 *
                 * @param {String} path - to get the value
                 * @returns {Boolean} true if attribute is not undefined
                 */
                has: function (path) {
                    return !_.isUndefined(this.get(path));
                },

                /***
                 * called after attributes has set and some of the has been changed
                 *
                 * @param {Object} attributes - contains the changed attributes
                 * @param {Object} options - the options passed in the set method
                 * @abstract
                 * @private
                 */
                _commitChangedAttributes: function (attributes, options) {
                    // override
                },

                /***
                 * Unset attribute on $
                 * @param {String|Object} key - the attribute or attributes to unset
                 * @param {Object} [options]
                 * @return {this}
                 */
                unset: function (key, options) {
                    (options || (options = {})).unset = true;
                    return this.set(key, null, options);
                },

                /***
                 * Clears all attributes
                 * @return {this}
                 */
                clear: function () {
                    return this.set(this.$, {unset: true});
                },

                /***
                 * Binds an event handler function to an EventDispatcher
                 *
                 * @param {String} path - a binding path e.g. a.b.c
                 * @param {String} event - the type of the event which should be bound
                 * @param {Function} callback - the event handler function
                 * @param {Object} [thisArg] - the thisArg used for calling the event handler
                 */
                bind: function (path, event, callback, thisArg) {
                    if (event instanceof Function && _.isString(path)) {
                        this.callBase(path, event, callback);
                    } else {
                        if(_.isArray(path) && path.length > 0){
                            thisArg = callback;
                            callback = event;
                            event = path[1];
                            path = path[0];
                        }
                        var eb = new EventBindable({
                            path: path,
                            event: event,
                            scope: thisArg,
                            callback: callback,
                            value: null
                        });
                        eb.set('binding', new Binding({path: path, scope: this, target: eb, targetKey: 'value'}));
                        this.$eventBindables.push(eb);
                    }
                },

                /***
                 * Unbinds an bound event handler from an EventDispatcher.
                 *
                 * @param {String} [path] - the path from which the event should be unbound
                 * @param {String} event - the type of the event
                 * @param {Function} callback - the event handler which is currently bound
                 * TODO: why a scope is passed here?
                 * @param {Object} [scope]
                 */
                unbind: function (path, event, callback, scope) {
                    if (event instanceof Function && _.isString(path)) {
                        this.callBase(path, event, callback);
                    } else {
                        var eb;
                        if (_.isArray(path) && path.length > 0) {
                            scope = callback;
                            callback = event;
                            event = path[1];
                            path = path[0];
                        }
                        for (var i = this.$eventBindables.length - 1; i >= 0; i--) {
                            eb = this.$eventBindables[i];
                            if (eb.$.scope === scope && eb.$.path === path && eb.$.event === event && eb.$.callback === callback) {
                                // unbind
                                eb.destroy();
                                this.$eventBindables.slice(i, 1);
                            }
                        }
                    }
                },

                /***
                 * Destroys all event bindings and triggers a destroy event
                 * @return {this}
                 */
                destroy: function() {
                    this.callBase();
                    for(var i = 0; i < this.$eventBindables.length; i++){
                        this.$eventBindables[i].destroy();
                    }

                    this.trigger('destroy',this);
                    return this;
                },
                isEqual: function (b) {
                    return isEqual(this,b);
                }
            });

        var EventBindable = Bindable.inherit({
            _commitChangedAttributes: function(attributes){
                this.callBase();
                if(attributes.binding){
                    this.set('value', attributes.binding.getValue());
                }else if(attributes.hasOwnProperty('value')){
                    var value = attributes.value;
                    this._unbindEvent(this.$previousAttributes['value']);
                    this._bindEvent(value);

                    if(this.$.binding){
                        this.$.binding.trigger();
                    }
                }

            },
            _unbindEvent: function (value) {
                if (value && value instanceof EventDispatcher) {
                    value.unbind(this.$.event, this.$.callback, this.$.scope);
                }
            },
            _bindEvent: function (value) {
                if (value && value instanceof EventDispatcher) {
                    value.bind(this.$.event, this.$.callback, this.$.scope);
                }
            },
            destroy: function () {
                this._unbindEvent(this.$.value);
                this.$.binding.destroy();

                this.callBase();
            }
        });

        return Bindable;

    });<|endoftext|>