<commit_before>/*jslint onevar: false, regexp: false */

(function (_) {
    _.mixin({
        // andand from ruby: returns an empty object if the input is falsey,
        // otherwise returns the input (or calls the function with the input).
        andand: function (obj, func) {
            if (obj) {
                return func ? func(obj) : obj;
            }
            return {};
        },

        // chomp() similar to ruby: Returns a new String with the given record
        // separator removed from the end of input (if present). Defaults to
        // removing spaces, newlines and carriage returns.
        chomp: function (input, separator) {
            if (!separator) {
                separator = "[\r\n ]";
            }
            return input.replace(new RegExp(separator + '+$'), '');
        },

        // Returns a new string where runs of the same character that occur in
        // this set are replaced by a single character.
        squeeze: function (input, other_string) {
            if (!other_string) {
                throw "squeeze() without a specific string to squeeze, is not supported.";
            }
            return input.replace(new RegExp("(" + other_string + ")+", 'g'), other_string);
        },

        // Find the (asymetric) difference between the two arrays.
        // _.difference([1,2,3], [2,3]) === _.without([1,2,3], 2, 3)
        difference: function (ary1, ary2) {
            return _.without.apply(_, [ary1].concat(ary2));
        },

        // Turns the first letter of a string into uppercase.
        capitalize: function (str) {
            return str.charAt(0).toUpperCase() + str.slice(1);
        },

        // Turns the first letter of a string into lowercase.
        uncapitalize: function (str) {
            return str.charAt(0).toLowerCase() + str.slice(1);
        },

        // Turns an expression_with_underscores into an ExpressionInCamelCase.
        // If the argument is truthy, makes theFirstLetterLowercase.
        camelize: function (str, first_letter_lowercase) {
            var camelized = _(str.split(/_/)).map(function (word) {
                return _.capitalize(word);
            }).join('');

            return (first_letter_lowercase ? _.uncapitalize(camelized) : _.capitalize(camelized));
        },

        // Turns a CamelCaseExpression into an underscore_expression.
        underscore: function (str) {
            return str.
                replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2').
                replace(/([a-z0-9])([A-Z])/g, '$1_$2').
                replace(/\-/g, '_').
                toLowerCase();
        },

        // Escapes a string for inclusion into a regular expression.
        // cf. http://simonwillison.net/2006/Jan/20/escape/
        regexpEscape: function (str) {
            return str.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&");
        },

        // Returns a list of items such that the given function
        // returns a unique value for each of them.
        //
        // If you know in advance that all duplicate values are
        // adjacent to each other, you can pass isSorted to use
        // a much faster algorithm.
        //
        // The fourth parameter is used as the  as with _.map
        // when the provided function is called, while the
        uniqBy: function (ary, func, isSorted, context) {
            var seen = [],
                uniq = [];
            _.each(ary, function (el, i) {
                var key = func.call(context, el, i, ary);
                if (0 === i || (isSorted === true ? _.last(seen) !== key : !_.include(seen, key))) {
                    seen.push(key);
                    uniq.push(el);
                }
            });
            return uniq;
        },

        // A function that, given an element, returns a property.
        //   _([{1:2}, {1:3}]).map(_.plucker(1)) === [2, 3]
        // (Though you could use _.pluck in that case).
        //
        plucker: function (key) {
            return function (el) {
                return el[key];
            };
        },

        // Return the first truthy output of the iterator when
        // passed over the array.
        // (equivalent to .map().compact().first(), though more efficient).
        firstMap: function (obj, iterator, context) {
            var result;
            _.any(obj, function (value, index, list) {
                result = iterator.call(context, value, index, list);
                return !!result;
            });
            return result;
        },

        // Works out the first name (and returns it) if possible. If unsure, returns
        // a trimmed version of the full input.
        informalize: function (input, fallback) {
            function potentialFirst(words) {
                var blacklist = ['mr', 'mrs', 'miss', 'ms', 'lord', 'lady', 'dame', 'dr', 'doctor', 'sir', 'master'];
                words = _(words).clone();
                var first_word = words.shift();

                if (_(blacklist).indexOf(first_word.toLowerCase()) > -1) {
                    // Disregard leading titles
                    return potentialFirst(words);

                } else if ((first_word.toUpperCase() === first_word && first_word.length < 3) ||
                           first_word.charAt(first_word.length - 1) === '.') {
                    // Leading initials suggest there's no first name present
                    return null;
                }
                return first_word;
            }

            // Non-destructively capitalize a name unless it's tiny.
            function clean(name) {
                if (name.length < 3) {
                    return name;
                }
                if (name.toUpperCase() === name) {
                    name = name.toLowerCase();
                }
                // Note: this is fine for the current capitalize() implementation. Often such implementations
                // will lowercase the entire string after the first character, which wouldn't be ok in this
                // context due to names like McFoo.
                return _(name).capitalize();
            }

            if (!input || !_(input).isString()) {
                return '';
            }

            var name = _(input).squeeze(' ').trim();
            fallback = fallback || name;

            // Bail if the input doesn't look like a name
            if (!name || name.match(/[0-9"\|\*\?\,\(\)\[\]<>_\-@~&]/) || name.length > 30) {
                return fallback;
            }

            var first_name = potentialFirst(name.split(/[\s]+/));
            if (!first_name) {
                return fallback;
            }
            return clean(first_name);
        },

        // Return a clean email address or null.
        cleanEmail: function (input) {
            return _.extractEmails(input)[0] || null;
        },

        // Return a list of all (cleaned) email addresses in a string.
        extractEmails: function (input) {
            return _((input || "").split(_.RE_EMAIL))
                .chain()
                .select(function (str, index) {
                    // string.split returns an array of alternating non-email and email strings.
                    // Selecting just the odd-numbered indexes reduces the list to only contain email addresses.
                    return index % 2 === 1;
                })
                .invoke('toLowerCase')
                .value();
        },

        inspect: function (obj) {
            return JSON.stringify(obj);
        }
    });

    // Taken from the HTML5 spec.
    _.RE_EMAIL = /([a-z0-9!#$%&'\*+\-\/=?\^_`\{\|\}~\.]+@(?:[a-z0-9\-]+)(?:\.[a-z0-9\-]+)+)/i;
}(_));
<commit_msg>More detail in comment<commit_after>/*jslint onevar: false, regexp: false */

(function (_) {
    _.mixin({
        // andand from ruby: returns an empty object if the input is falsey,
        // otherwise returns the input (or calls the function with the input).
        andand: function (obj, func) {
            if (obj) {
                return func ? func(obj) : obj;
            }
            return {};
        },

        // chomp() similar to ruby: Returns a new String with the given record
        // separator removed from the end of input (if present). Defaults to
        // removing spaces, newlines and carriage returns.
        chomp: function (input, separator) {
            if (!separator) {
                separator = "[\r\n ]";
            }
            return input.replace(new RegExp(separator + '+$'), '');
        },

        // Returns a new string where runs of the same character that occur in
        // this set are replaced by a single character.
        squeeze: function (input, other_string) {
            if (!other_string) {
                throw "squeeze() without a specific string to squeeze, is not supported.";
            }
            return input.replace(new RegExp("(" + other_string + ")+", 'g'), other_string);
        },

        // Find the (asymetric) difference between the two arrays.
        // _.difference([1,2,3], [2,3]) === _.without([1,2,3], 2, 3)
        difference: function (ary1, ary2) {
            return _.without.apply(_, [ary1].concat(ary2));
        },

        // Turns the first letter of a string into uppercase.
        capitalize: function (str) {
            return str.charAt(0).toUpperCase() + str.slice(1);
        },

        // Turns the first letter of a string into lowercase.
        uncapitalize: function (str) {
            return str.charAt(0).toLowerCase() + str.slice(1);
        },

        // Turns an expression_with_underscores into an ExpressionInCamelCase.
        // If the argument is truthy, makes theFirstLetterLowercase.
        camelize: function (str, first_letter_lowercase) {
            var camelized = _(str.split(/_/)).map(function (word) {
                return _.capitalize(word);
            }).join('');

            return (first_letter_lowercase ? _.uncapitalize(camelized) : _.capitalize(camelized));
        },

        // Turns a CamelCaseExpression into an underscore_expression.
        underscore: function (str) {
            return str.
                replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2').
                replace(/([a-z0-9])([A-Z])/g, '$1_$2').
                replace(/\-/g, '_').
                toLowerCase();
        },

        // Escapes a string for inclusion into a regular expression.
        // cf. http://simonwillison.net/2006/Jan/20/escape/
        regexpEscape: function (str) {
            return str.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&");
        },

        // Returns a list of items such that the given function
        // returns a unique value for each of them.
        //
        // If you know in advance that all duplicate values are
        // adjacent to each other, you can pass isSorted to use
        // a much faster algorithm.
        //
        // The fourth parameter is used as the  as with _.map
        // when the provided function is called, while the
        uniqBy: function (ary, func, isSorted, context) {
            var seen = [],
                uniq = [];
            _.each(ary, function (el, i) {
                var key = func.call(context, el, i, ary);
                if (0 === i || (isSorted === true ? _.last(seen) !== key : !_.include(seen, key))) {
                    seen.push(key);
                    uniq.push(el);
                }
            });
            return uniq;
        },

        // A function that, given an element, returns a property.
        //   _([{1:2}, {1:3}]).map(_.plucker(1)) === [2, 3]
        // (Though you could use _.pluck in that case).
        //
        plucker: function (key) {
            return function (el) {
                return el[key];
            };
        },

        // Return the first truthy output of the iterator when
        // passed over the array.
        // (equivalent to .map().compact().first(), though more efficient).
        firstMap: function (obj, iterator, context) {
            var result;
            _.any(obj, function (value, index, list) {
                result = iterator.call(context, value, index, list);
                return !!result;
            });
            return result;
        },

        // Works out the first name (and returns it) if possible. If unsure, returns
        // a trimmed version of the full input.
        informalize: function (input, fallback) {
            function potentialFirst(words) {
                var blacklist = ['mr', 'mrs', 'miss', 'ms', 'lord', 'lady', 'dame', 'dr', 'doctor', 'sir', 'master'];
                words = _(words).clone();
                var first_word = words.shift();

                if (_(blacklist).indexOf(first_word.toLowerCase()) > -1) {
                    // Disregard leading titles
                    return potentialFirst(words);

                } else if ((first_word.toUpperCase() === first_word && first_word.length < 3) ||
                           first_word.charAt(first_word.length - 1) === '.') {
                    // Leading initials suggest there's no first name present
                    return null;
                }
                return first_word;
            }

            // Non-destructively capitalize a name unless it's tiny.
            function clean(name) {
                if (name.length < 3) {
                    return name;
                }
                if (name.toUpperCase() === name) {
                    name = name.toLowerCase();
                }
                // Note: this is fine for the current capitalize() implementation. Often such implementations
                // will lowercase the entire string after the first character, which wouldn't be ok in this
                // context due to names like McFoo.
                return _(name).capitalize();
            }

            if (!input || !_(input).isString()) {
                return '';
            }

            var name = _(input).squeeze(' ').trim();
            fallback = fallback || name;

            // Bail if the input doesn't look like a name
            if (!name || name.match(/[0-9"\|\*\?\,\(\)\[\]<>_\-@~&]/) || name.length > 30) {
                return fallback;
            }

            var first_name = potentialFirst(name.split(/[\s]+/));
            if (!first_name) {
                return fallback;
            }
            return clean(first_name);
        },

        // Return a clean email address or null.
        cleanEmail: function (input) {
            return _.extractEmails(input)[0] || null;
        },

        // Return a list of all (cleaned) email addresses in a string.
        extractEmails: function (input) {
            return _((input || "").split(_.RE_EMAIL))
                .chain()
                .select(function (str, index) {
                    // string.split returns an array of alternating non-email and email strings.
                    // Selecting just the odd-numbered indexes reduces the list to only contain email addresses.
                    return index % 2 === 1;
                })
                .invoke('toLowerCase')
                .value();
        },

        inspect: function (obj) {
            return JSON.stringify(obj);
        }
    });

    // Based on the HTML5 spec: http://www.w3.org/TR/html5/states-of-the-type-attribute.html#valid-e-mail-address
    //
    // Differs from the HTML5 spec rule in that we require at least one dot in the domain name,
    // excluding things like root@localhost -- although technically that's a valid email address,
    // there's very little we can do with a domain name that's not rooted in the public DNS.
    //
    // The regex also excludes colons from the user part (filtering out mailto:foo@example.com) and
    // question marks from the domain part (filtering out foo@example.com?subject=bar).
    _.RE_EMAIL = /([a-z0-9!#$%&'\*+\-\/=?\^_`\{\|\}~\.]+@(?:[a-z0-9\-]+)(?:\.[a-z0-9\-]+)+)/i;
}(_));
<|endoftext|>


<commit_before>/* global __dirname, Buffer, describe, it*/
var gulp = require('gulp');
var gutil = require('gulp-util');
var assert = require('stream-assert');
var path = require('path');
var should = require('should');
var syncJSON = require('../');
var test = require('./test-stream');
require('mocha');

var chalk = gutil.colors;

function contentsAre(obj) {
	return function(file) {
		should.deepEqual(JSON.parse(file.contents.toString()), obj);
	};
}

describe('gulp-sync-json', function () {
	describe('api', function () {
		it('should throw when primary file isn\'t provided', function () {
			(function () {
				syncJSON();
			}).should.throw('Primary file is required');
		});

		it('should emit error on streamed file', function (done) {
			gulp.src(path.join(__dirname, 'test-stream.js'), { buffer: false })
				.pipe(syncJSON('fake.json'))
				.on('error', function (err) {
					err.message.should.eql('Streams not supported');
					done();
				});
		});
	});

	describe('ignored files', function() {
		it('should ignore single files', function (done) {
			var a = { a: 1 };
			test(a)
				.pipe(syncJSON('file0.json'))
				.pipe(assert.first(contentsAre(a)))
				.pipe(assert.end(done));
		});

		it('should ignore files if no primary is matched', function(done) {
			var a = { a: 1 };
			var b = { b: 2 };
			var c = { c: 3 };
			test(a, b, c)
				.pipe(syncJSON('notfound.json'))
				.pipe(assert.first(contentsAre(a)))
				.pipe(assert.second(contentsAre(b)))
				.pipe(assert.nth(2, contentsAre(c)))
				.pipe(assert.end(done));
		});

		it('should not try to parse ignored files', function (done) {
			var primary = "not json";
			test(primary)
				.pipe(syncJSON('file0.json'))
				.pipe(assert.length(1))
				.pipe(assert.end(done));
		});
	});

	describe('sync behavior', function() {
		it('should add missing keys', function (done) {
			var primary = {
				one: 1,
				two: 2
			};
			var target = {
				one: 1
			};
			test(primary, target)
				.pipe(syncJSON('file0.json'))
				.pipe(assert.second(contentsAre({
					one: 1,
					two: 2
				})))
				.pipe(assert.end(done));
		});

		it('should remove non-primary keys', function (done) {
			var primary = {
				one: 1
			};
			var target = {
				should: 'delete me'
			};
			test(primary, target)
				.pipe(syncJSON('file0.json'))
				.pipe(assert.second(contentsAre({ one: 1 })))
				.pipe(assert.end(done));
		});

		it('should not change existing values', function (done) {
			var primary = {
				one: 1,
				two: 2
			};
			var target = {
				one: 3
			};
			test(primary, target)
				.pipe(syncJSON('file0.json'))
				.pipe(assert.second(contentsAre({
					one: 3,
					two: 2
				})))
				.pipe(assert.end(done));
		});

		it('should sync nested keys', function (done) {
			var primary = {
				one: 1,
				deep: {
					two: 2,
					nested: {
						value: 3
					}
				}
			};
			var target = {
				gone: 0,
				deep: {
					gone: 0,
					nested: {
						gone: 0
					}
				}
			};
			test(primary, target)
				.pipe(syncJSON('file0.json'))
				.pipe(assert.second(contentsAre(primary)))
				.pipe(assert.end(done));
		});

		it('should scaffold new structures from primary', function (done) {
			var primary = {
				deep: {
					nested: {
						value: 3
					}
				}
			};
			var target = {};
			test(primary, target)
				.pipe(syncJSON('file0.json'))
				.pipe(assert.second(contentsAre(primary)))
				.pipe(assert.end(done));
		});

		it('should clear out structures from target', function (done) {
			var primary = {};
			var target = {
				deep: {
					nested: {
						value: 3
					}
				}
			};
			test(primary, target)
				.pipe(syncJSON('file0.json'))
				.pipe(assert.second(contentsAre(primary)))
				.pipe(assert.end(done));
		});

		it('should not inspect or sync array contents', function (done) {
			var primary = {
				arr: [{
					one: 1
				}, {
					two: 2
				}]
			};
			var target = {
				arr: [{
					three: 3
				}, {
					four: 4
				}]
			};
			test(primary, target)
				.pipe(syncJSON('file0.json'))
				.pipe(assert.second(contentsAre(target)))
				.pipe(assert.end(done));
		});

		it('should sync multiple targets', function (done) {
			var primary = {
				one: 1
			};
			var targetOne = {
				gone: 0
			};
			var targetTwo = {
				one: 2
			};
			var targetThree = {
				one: 3,
				gone: 0
			};
			test(primary, targetOne, targetTwo, targetThree)
				.pipe(syncJSON('file0.json'))
				.pipe(assert.second(contentsAre(primary)))
				.pipe(assert.nth(2, contentsAre({ one: 2 })))
				.pipe(assert.nth(3, contentsAre({ one: 3 })))
				.pipe(assert.end(done));
		});
	});

	describe('empty targets', function() {
		it('should push to empty objects', function (done) {
			var primary = { one: 1 };
			test(primary, {})
				.pipe(syncJSON('file0.json'))
				.pipe(assert.second(contentsAre(primary)))
				.pipe(assert.end(done));
		});

		it('should push to empty files', function (done) {
			var primary = { one: 1 };
			test(primary, "")
				.pipe(syncJSON('file0.json'))
				.pipe(assert.second(contentsAre(primary)))
				.pipe(assert.end(done));
		});

		it('should push to whitespace files', function (done) {
			var primary = { one: 1 };
			test(primary, " \r\n \t ")
				.pipe(syncJSON('file0.json'))
				.pipe(assert.second(contentsAre(primary)))
				.pipe(assert.end(done));
		});
	});

	describe('bad targets', function() {
		it('should emit error when trying to sync against an array', function(done) {
			var primary = { one: 1 };
			test(primary, [0, 1])
				.pipe(syncJSON('file0.json'))
				.on('error', function (err) {
					chalk.stripColor(err.message).should.endWith('is a JSON type that cannot be synced: Array. Only Objects are supported');
					done();
		        });
		});

		it('should emit error when trying to sync against a number', function(done) {
			var primary = { one: 1 };
			test(primary, 42)
				.pipe(syncJSON('file0.json'))
				.on('error', function (err) {
					chalk.stripColor(err.message).should.endWith('is a JSON type that cannot be synced: Number. Only Objects are supported');
					done();
		        });
		});

		it('should emit error when trying to sync against a string', function(done) {
			var primary = { one: 1 };
			test(primary, "\"hello world\"")
				.pipe(syncJSON('file0.json'))
				.on('error', function (err) {
					chalk.stripColor(err.message).should.endWith('is a JSON type that cannot be synced: String. Only Objects are supported');
					done();
		        });
		});

		it('should emit error when trying to sync against a boolean', function(done) {
			var primary = { one: 1 };
			test(primary, true)
				.pipe(syncJSON('file0.json'))
				.on('error', function (err) {
					chalk.stripColor(err.message).should.endWith('is a JSON type that cannot be synced: Boolean. Only Objects are supported');
					done();
		        });
		});

		it('should emit error when trying to sync against null', function(done) {
			var primary = { one: 1 };
			test(primary, null)
				.pipe(syncJSON('file0.json'))
				.on('error', function (err) {
					chalk.stripColor(err.message).should.endWith('is a JSON type that cannot be synced: Null. Only Objects are supported');
					done();
		        });
		});
	});

	describe('error handling', function() {
		it('should emit error for invalid JSON in primary', function(done) {
			var primary = "not json";
			var target = { one: 1 };
			test(primary, target)
				.pipe(syncJSON('file0.json'))
				.on('error', function (err) {
					chalk.stripColor(err.message).should.endWith('contains invalid JSON');
					done();
		        });
		});

		it('should emit error for invalid JSON in target', function(done) {
			var primary = { one: 1 };
			var target = "not json";
			test(primary, target)
				.pipe(syncJSON('file0.json'))
				.on('error', function (err) {
					chalk.stripColor(err.message).should.endWith('contains invalid JSON');
					done();
		        });
		});

		it('should emit error when types are mismatched on matching keys', function(done) {
			var primary = { badKey: 1 };
			var target = { badKey: "two" };
			test(primary, target)
				.pipe(syncJSON('file0.json'))
				.on('error', function (err) {
					chalk.stripColor(err.message).should.endWith('contains type mismatch on key badKey. Source type number, target type string');
					done();
		        });
		});
	});

	describe('report mode', function () {
		it('should do nothing if targets are synced', function(done) {
			var primary = { one: 1 };
			var target = { one: 2 };
			test(primary, target)
				.pipe(syncJSON('file0.json', { report: true }))
				.pipe(assert.end(done));
		});

		it('should supress multiple errors and emit once', function(done) {
			var primary = { one: 1 };
			var targetOne = "not json";
			var targetTwo = { bad: "key" };
			var targetThree = [];

			var originalLog = gutil.log;
			gutil.log = function() { };

			test(primary, targetOne, targetTwo, targetThree)
				.pipe(syncJSON('file0.json', { report: true }))
				.on('error', function (err) {
					chalk.stripColor(err.message).should.eql('Report failed with 3 items');
					gutil.log = originalLog;
					done();
				});
		});

		it('should capture multiple errors and log them before emitting the report error', function(done) {
			var primary = { one: 1 };
			var targetOne = "not json";
			var targetTwo = { bad: "key" };
			var targetThree = [];

			var originalLog = gutil.log;
			var capturedOutput = [];
			gutil.log = function() {
				capturedOutput = [].slice.call(arguments);
			};

			test(primary, targetOne, targetTwo, targetThree)
				.pipe(syncJSON('file0.json', { report: true }))
				.on('error', function (err) {
					var errorMessages = capturedOutput[1].split(gutil.linefeed).slice(1);
					errorMessages.length.should.eql(3);
					chalk.stripColor(errorMessages[0]).should.endWith('contains invalid JSON');
					chalk.stripColor(errorMessages[1]).should.endWith('contains unaligned key structure');
					chalk.stripColor(errorMessages[2]).should.endWith('is a JSON type that cannot be synced: Array. Only Objects are supported');
					gutil.log = originalLog;
					done();
				});
		});
	});
});<commit_msg>test fix on windows, trim line endings for string assert<commit_after>/* global __dirname, Buffer, describe, it*/
var gulp = require('gulp');
var gutil = require('gulp-util');
var assert = require('stream-assert');
var path = require('path');
var should = require('should');
var syncJSON = require('../');
var test = require('./test-stream');
require('mocha');

var chalk = gutil.colors;

function contentsAre(obj) {
	return function(file) {
		should.deepEqual(JSON.parse(file.contents.toString()), obj);
	};
}

describe('gulp-sync-json', function () {
	describe('api', function () {
		it('should throw when primary file isn\'t provided', function () {
			(function () {
				syncJSON();
			}).should.throw('Primary file is required');
		});

		it('should emit error on streamed file', function (done) {
			gulp.src(path.join(__dirname, 'test-stream.js'), { buffer: false })
				.pipe(syncJSON('fake.json'))
				.on('error', function (err) {
					err.message.should.eql('Streams not supported');
					done();
				});
		});
	});

	describe('ignored files', function() {
		it('should ignore single files', function (done) {
			var a = { a: 1 };
			test(a)
				.pipe(syncJSON('file0.json'))
				.pipe(assert.first(contentsAre(a)))
				.pipe(assert.end(done));
		});

		it('should ignore files if no primary is matched', function(done) {
			var a = { a: 1 };
			var b = { b: 2 };
			var c = { c: 3 };
			test(a, b, c)
				.pipe(syncJSON('notfound.json'))
				.pipe(assert.first(contentsAre(a)))
				.pipe(assert.second(contentsAre(b)))
				.pipe(assert.nth(2, contentsAre(c)))
				.pipe(assert.end(done));
		});

		it('should not try to parse ignored files', function (done) {
			var primary = "not json";
			test(primary)
				.pipe(syncJSON('file0.json'))
				.pipe(assert.length(1))
				.pipe(assert.end(done));
		});
	});

	describe('sync behavior', function() {
		it('should add missing keys', function (done) {
			var primary = {
				one: 1,
				two: 2
			};
			var target = {
				one: 1
			};
			test(primary, target)
				.pipe(syncJSON('file0.json'))
				.pipe(assert.second(contentsAre({
					one: 1,
					two: 2
				})))
				.pipe(assert.end(done));
		});

		it('should remove non-primary keys', function (done) {
			var primary = {
				one: 1
			};
			var target = {
				should: 'delete me'
			};
			test(primary, target)
				.pipe(syncJSON('file0.json'))
				.pipe(assert.second(contentsAre({ one: 1 })))
				.pipe(assert.end(done));
		});

		it('should not change existing values', function (done) {
			var primary = {
				one: 1,
				two: 2
			};
			var target = {
				one: 3
			};
			test(primary, target)
				.pipe(syncJSON('file0.json'))
				.pipe(assert.second(contentsAre({
					one: 3,
					two: 2
				})))
				.pipe(assert.end(done));
		});

		it('should sync nested keys', function (done) {
			var primary = {
				one: 1,
				deep: {
					two: 2,
					nested: {
						value: 3
					}
				}
			};
			var target = {
				gone: 0,
				deep: {
					gone: 0,
					nested: {
						gone: 0
					}
				}
			};
			test(primary, target)
				.pipe(syncJSON('file0.json'))
				.pipe(assert.second(contentsAre(primary)))
				.pipe(assert.end(done));
		});

		it('should scaffold new structures from primary', function (done) {
			var primary = {
				deep: {
					nested: {
						value: 3
					}
				}
			};
			var target = {};
			test(primary, target)
				.pipe(syncJSON('file0.json'))
				.pipe(assert.second(contentsAre(primary)))
				.pipe(assert.end(done));
		});

		it('should clear out structures from target', function (done) {
			var primary = {};
			var target = {
				deep: {
					nested: {
						value: 3
					}
				}
			};
			test(primary, target)
				.pipe(syncJSON('file0.json'))
				.pipe(assert.second(contentsAre(primary)))
				.pipe(assert.end(done));
		});

		it('should not inspect or sync array contents', function (done) {
			var primary = {
				arr: [{
					one: 1
				}, {
					two: 2
				}]
			};
			var target = {
				arr: [{
					three: 3
				}, {
					four: 4
				}]
			};
			test(primary, target)
				.pipe(syncJSON('file0.json'))
				.pipe(assert.second(contentsAre(target)))
				.pipe(assert.end(done));
		});

		it('should sync multiple targets', function (done) {
			var primary = {
				one: 1
			};
			var targetOne = {
				gone: 0
			};
			var targetTwo = {
				one: 2
			};
			var targetThree = {
				one: 3,
				gone: 0
			};
			test(primary, targetOne, targetTwo, targetThree)
				.pipe(syncJSON('file0.json'))
				.pipe(assert.second(contentsAre(primary)))
				.pipe(assert.nth(2, contentsAre({ one: 2 })))
				.pipe(assert.nth(3, contentsAre({ one: 3 })))
				.pipe(assert.end(done));
		});
	});

	describe('empty targets', function() {
		it('should push to empty objects', function (done) {
			var primary = { one: 1 };
			test(primary, {})
				.pipe(syncJSON('file0.json'))
				.pipe(assert.second(contentsAre(primary)))
				.pipe(assert.end(done));
		});

		it('should push to empty files', function (done) {
			var primary = { one: 1 };
			test(primary, "")
				.pipe(syncJSON('file0.json'))
				.pipe(assert.second(contentsAre(primary)))
				.pipe(assert.end(done));
		});

		it('should push to whitespace files', function (done) {
			var primary = { one: 1 };
			test(primary, " \r\n \t ")
				.pipe(syncJSON('file0.json'))
				.pipe(assert.second(contentsAre(primary)))
				.pipe(assert.end(done));
		});
	});

	describe('bad targets', function() {
		it('should emit error when trying to sync against an array', function(done) {
			var primary = { one: 1 };
			test(primary, [0, 1])
				.pipe(syncJSON('file0.json'))
				.on('error', function (err) {
					chalk.stripColor(err.message).should.endWith('is a JSON type that cannot be synced: Array. Only Objects are supported');
					done();
		        });
		});

		it('should emit error when trying to sync against a number', function(done) {
			var primary = { one: 1 };
			test(primary, 42)
				.pipe(syncJSON('file0.json'))
				.on('error', function (err) {
					chalk.stripColor(err.message).should.endWith('is a JSON type that cannot be synced: Number. Only Objects are supported');
					done();
		        });
		});

		it('should emit error when trying to sync against a string', function(done) {
			var primary = { one: 1 };
			test(primary, "\"hello world\"")
				.pipe(syncJSON('file0.json'))
				.on('error', function (err) {
					chalk.stripColor(err.message).should.endWith('is a JSON type that cannot be synced: String. Only Objects are supported');
					done();
		        });
		});

		it('should emit error when trying to sync against a boolean', function(done) {
			var primary = { one: 1 };
			test(primary, true)
				.pipe(syncJSON('file0.json'))
				.on('error', function (err) {
					chalk.stripColor(err.message).should.endWith('is a JSON type that cannot be synced: Boolean. Only Objects are supported');
					done();
		        });
		});

		it('should emit error when trying to sync against null', function(done) {
			var primary = { one: 1 };
			test(primary, null)
				.pipe(syncJSON('file0.json'))
				.on('error', function (err) {
					chalk.stripColor(err.message).should.endWith('is a JSON type that cannot be synced: Null. Only Objects are supported');
					done();
		        });
		});
	});

	describe('error handling', function() {
		it('should emit error for invalid JSON in primary', function(done) {
			var primary = "not json";
			var target = { one: 1 };
			test(primary, target)
				.pipe(syncJSON('file0.json'))
				.on('error', function (err) {
					chalk.stripColor(err.message).should.endWith('contains invalid JSON');
					done();
		        });
		});

		it('should emit error for invalid JSON in target', function(done) {
			var primary = { one: 1 };
			var target = "not json";
			test(primary, target)
				.pipe(syncJSON('file0.json'))
				.on('error', function (err) {
					chalk.stripColor(err.message).should.endWith('contains invalid JSON');
					done();
		        });
		});

		it('should emit error when types are mismatched on matching keys', function(done) {
			var primary = { badKey: 1 };
			var target = { badKey: "two" };
			test(primary, target)
				.pipe(syncJSON('file0.json'))
				.on('error', function (err) {
					chalk.stripColor(err.message).should.endWith('contains type mismatch on key badKey. Source type number, target type string');
					done();
		        });
		});
	});

	describe('report mode', function () {
		it('should do nothing if targets are synced', function(done) {
			var primary = { one: 1 };
			var target = { one: 2 };
			test(primary, target)
				.pipe(syncJSON('file0.json', { report: true }))
				.pipe(assert.end(done));
		});

		it('should supress multiple errors and emit once', function(done) {
			var primary = { one: 1 };
			var targetOne = "not json";
			var targetTwo = { bad: "key" };
			var targetThree = [];

			var originalLog = gutil.log;
			gutil.log = function() { };

			test(primary, targetOne, targetTwo, targetThree)
				.pipe(syncJSON('file0.json', { report: true }))
				.on('error', function (err) {
					chalk.stripColor(err.message).should.eql('Report failed with 3 items');
					gutil.log = originalLog;
					done();
				});
		});

		it('should capture multiple errors and log them before emitting the report error', function(done) {
			var primary = { one: 1 };
			var targetOne = "not json";
			var targetTwo = { bad: "key" };
			var targetThree = [];

			var originalLog = gutil.log;
			var capturedOutput = [];
			gutil.log = function() {
				capturedOutput = [].slice.call(arguments);
			};

			test(primary, targetOne, targetTwo, targetThree)
				.pipe(syncJSON('file0.json', { report: true }))
				.on('error', function (err) {
					var errorMessages = capturedOutput[1]
						.split(gutil.linefeed)
						.slice(1)
						.map(function(m) {
							return m.trim();
						});
					errorMessages.length.should.eql(3);
					chalk.stripColor(errorMessages[0]).should.endWith('contains invalid JSON');
					chalk.stripColor(errorMessages[1]).should.endWith('contains unaligned key structure');
					chalk.stripColor(errorMessages[2]).should.endWith('is a JSON type that cannot be synced: Array. Only Objects are supported');
					gutil.log = originalLog;
					done();
				});
		});
	});
});<|endoftext|>


<commit_before>/*
 * L.Map.Tap is used to enable mobile hacks like quick taps and long hold.
 */

L.Map.mergeOptions({
	tap: true,
	tapTolerance: 15
});

L.Map.Tap = L.Handler.extend({
	addHooks: function () {
		L.DomEvent.on(this._map._container, 'touchstart', this._onDown, this);
	},

	removeHooks: function () {
		L.DomEvent.off(this._map._container, 'touchstart', this._onDown, this);
	},

	_onDown: function (e) {
		if (!e.touches) { return; }

		L.DomEvent.preventDefault(e);

		this._fireClick = true;

		// don't simulate click or track longpress if more than 1 touch
		if (e.touches.length > 1) {
			this._fireClick = false;
			clearTimeout(this._holdTimeout);
			return;
		}

		var first = e.touches[0],
		    el = first.target;

		this._startPos = this._newPos = new L.Point(first.clientX, first.clientY);

		// if touching a link, highlight it
		if (el.tagName && el.tagName.toLowerCase() === 'a') {
			L.DomUtil.addClass(el, 'leaflet-active');
		}

		// simulate long hold but setting a timeout
		this._holdTimeout = setTimeout(L.bind(function () {
			if (this._isTapValid()) {
				this._fireClick = false;
				this._onUp();
				this._simulateEvent('contextmenu', first);
			}
		}, this), 1000);

		this._simulateEvent('mousedown', first);

		L.DomEvent.on(document, {
			touchmove: this._onMove,
			touchend: this._onUp
		}, this);
	},

	_onUp: function (e) {
		clearTimeout(this._holdTimeout);

		L.DomEvent.off(document, {
			touchmove: this._onMove,
			touchend: this._onUp
		}, this);

		if (this._fireClick && e && e.changedTouches) {

			var first = e.changedTouches[0],
			    el = first.target;

			if (el && el.tagName && el.tagName.toLowerCase() === 'a') {
				L.DomUtil.removeClass(el, 'leaflet-active');
			}

			this._simulateEvent('mouseup', first);

			// simulate click if the touch didn't move too much
			if (this._isTapValid()) {
				this._simulateEvent('click', first);
			}
		}
	},

	_isTapValid: function () {
		return this._newPos.distanceTo(this._startPos) <= this._map.options.tapTolerance;
	},

	_onMove: function (e) {
		var first = e.touches[0];
		this._newPos = new L.Point(first.clientX, first.clientY);
	},

	_simulateEvent: function (type, e) {
		var simulatedEvent = document.createEvent('MouseEvents');

		simulatedEvent._simulated = true;
		e.target._simulatedClick = true;

		simulatedEvent.initMouseEvent(
		        type, true, true, window, 1,
		        e.screenX, e.screenY,
		        e.clientX, e.clientY,
		        false, false, false, false, 0, null);

		e.target.dispatchEvent(simulatedEvent);
	}
});

if (L.Browser.touch && !L.Browser.pointer) {
	L.Map.addInitHook('addHandler', 'tap', L.Map.Tap);
}
<commit_msg>Simulate mousemove on touchmove (fix #3825)<commit_after>/*
 * L.Map.Tap is used to enable mobile hacks like quick taps and long hold.
 */

L.Map.mergeOptions({
	tap: true,
	tapTolerance: 15
});

L.Map.Tap = L.Handler.extend({
	addHooks: function () {
		L.DomEvent.on(this._map._container, 'touchstart', this._onDown, this);
	},

	removeHooks: function () {
		L.DomEvent.off(this._map._container, 'touchstart', this._onDown, this);
	},

	_onDown: function (e) {
		if (!e.touches) { return; }

		L.DomEvent.preventDefault(e);

		this._fireClick = true;

		// don't simulate click or track longpress if more than 1 touch
		if (e.touches.length > 1) {
			this._fireClick = false;
			clearTimeout(this._holdTimeout);
			return;
		}

		var first = e.touches[0],
		    el = first.target;

		this._startPos = this._newPos = new L.Point(first.clientX, first.clientY);

		// if touching a link, highlight it
		if (el.tagName && el.tagName.toLowerCase() === 'a') {
			L.DomUtil.addClass(el, 'leaflet-active');
		}

		// simulate long hold but setting a timeout
		this._holdTimeout = setTimeout(L.bind(function () {
			if (this._isTapValid()) {
				this._fireClick = false;
				this._onUp();
				this._simulateEvent('contextmenu', first);
			}
		}, this), 1000);

		this._simulateEvent('mousedown', first);

		L.DomEvent.on(document, {
			touchmove: this._onMove,
			touchend: this._onUp
		}, this);
	},

	_onUp: function (e) {
		clearTimeout(this._holdTimeout);

		L.DomEvent.off(document, {
			touchmove: this._onMove,
			touchend: this._onUp
		}, this);

		if (this._fireClick && e && e.changedTouches) {

			var first = e.changedTouches[0],
			    el = first.target;

			if (el && el.tagName && el.tagName.toLowerCase() === 'a') {
				L.DomUtil.removeClass(el, 'leaflet-active');
			}

			this._simulateEvent('mouseup', first);

			// simulate click if the touch didn't move too much
			if (this._isTapValid()) {
				this._simulateEvent('click', first);
			}
		}
	},

	_isTapValid: function () {
		return this._newPos.distanceTo(this._startPos) <= this._map.options.tapTolerance;
	},

	_onMove: function (e) {
		var first = e.touches[0];
		this._newPos = new L.Point(first.clientX, first.clientY);
		this._simulateEvent('mousemove', first);
	},

	_simulateEvent: function (type, e) {
		var simulatedEvent = document.createEvent('MouseEvents');

		simulatedEvent._simulated = true;
		e.target._simulatedClick = true;

		simulatedEvent.initMouseEvent(
		        type, true, true, window, 1,
		        e.screenX, e.screenY,
		        e.clientX, e.clientY,
		        false, false, false, false, 0, null);

		e.target.dispatchEvent(simulatedEvent);
	}
});

if (L.Browser.touch && !L.Browser.pointer) {
	L.Map.addInitHook('addHandler', 'tap', L.Map.Tap);
}
<|endoftext|>


<commit_before>/*global before,describe,it */
var fs = require('fs');

var assert = require('chai').assert;
var mkdir = require('mkdirp');

var helpers = require('./helpers');
var binaryBuffer = helpers.binaryBuffer;
var fileBuffer = helpers.fileBuffer;
var WriteHashStream = helpers.WriteHashStream;

var Packer = require('../index.js');

var testBuffer = binaryBuffer(1024 * 16);

var testDate = new Date('Jan 03 2013 14:26:38 GMT');
var testDate2 = new Date('Feb 10 2013 10:24:42 GMT');

var testDateOverflow = new Date('Jan 1 2044 00:00:00 GMT');
var testDateUnderflow = new Date('Dec 30 1979 23:59:58 GMT');

describe('pack', function() {
  before(function() {
    mkdir.sync('tmp');
  });

  describe('#entry', function() {

    it('should append Buffer sources', function(done) {
      var archive = new Packer();

      var testStream = new WriteHashStream('tmp/buffer.zip');

      testStream.on('close', function() {
        done();
      });

      archive.pipe(testStream);

      archive.entry(testBuffer, { name: 'buffer.txt', date: testDate });
      archive.finalize();
    });

    it('should append Stream sources', function(done) {
      var archive = new Packer();

      var testStream = new WriteHashStream('tmp/stream.zip');

      testStream.on('close', function() {
        done();
      });

      archive.pipe(testStream);

      archive.entry(fs.createReadStream('test/fixtures/test.txt'), { name: 'stream.txt', date: testDate });
      archive.finalize();
    });

    it('should append multiple sources', function(done) {
      var archive = new Packer();

      var testStream = new WriteHashStream('tmp/multiple.zip');

      testStream.on('close', function() {
        done();
      });

      archive.pipe(testStream);

      archive.entry('string', { name: 'string.txt', date: testDate }, function(err) {
        if (err) throw err;
        archive.entry(testBuffer, { name: 'buffer.txt', date: testDate2 }, function(err) {
          if (err) throw err;
          archive.entry(fs.createReadStream('test/fixtures/test.txt'), { name: 'stream.txt', date: testDate2 }, function(err) {
            if (err) throw err;
            archive.entry(fs.createReadStream('test/fixtures/test.txt'), { name: 'stream-store.txt', date: testDate, store: true }, function(err) {
              if (err) throw err;
              archive.finalize();
            });
          });
        });
      });
    });

    it('should support STORE for Buffer sources', function(done) {
      var archive = new Packer();

      var testStream = new WriteHashStream('tmp/buffer-store.zip');

      testStream.on('close', function() {
        done();
      });

      archive.pipe(testStream);

      archive.entry(testBuffer, { name: 'buffer.txt', date: testDate, store: true });
      archive.finalize();
    });

    it('should support STORE for Stream sources', function(done) {
      var archive = new Packer();

      var testStream = new WriteHashStream('tmp/stream-store.zip');

      testStream.on('close', function() {
        done();
      });

      archive.pipe(testStream);

      archive.entry(fs.createReadStream('test/fixtures/test.txt'), { name: 'stream.txt', date: testDate, store: true });
      archive.finalize();
    });

    it('should support archive and file comments', function(done) {
      var archive = new Packer({
        comment: 'this is a zip comment',
        forceUTC: true
      });

      var testStream = new WriteHashStream('tmp/comments.zip');

      testStream.on('close', function() {
        done();
      });

      archive.pipe(testStream);

      archive.entry(testBuffer, { name: 'buffer.txt', date: testDate, comment: 'this is a file comment' });
      archive.finalize();
    });

    it('should STORE files when compression level is zero', function(done) {
      var archive = new Packer({
        forceUTC: true,
        level: 0
      });

      var testStream = new WriteHashStream('tmp/store-level0.zip');

      testStream.on('close', function() {
        assert.equal(testStream.digest, '70b50994c971dbb0e457781cf6d23ca82e5ccbc0');
        done();
      });

      archive.pipe(testStream);

      archive.entry(testBuffer, { name: 'buffer.txt', date: testDate });
      archive.finalize();
    });

    it('should properly handle utf8 encoded characters in file names and comments', function(done) {
      var archive = new Packer();

      var testStream = new WriteHashStream('tmp/accentedchars-filenames.zip');

      testStream.on('close', function() {
        done();
      });

      archive.pipe(testStream);

      archive.entry(testBuffer, { name: 'àáâãäçèéêëìíîïñòóôõöùúûüýÿ.txt', date: testDate, comment: 'àáâãäçèéêëìíîïñòóôõöùúûüýÿ' }, function(err) {
        if (err) throw err;
        archive.entry(testBuffer, { name: 'ÀÁÂÃÄÇÈÉÊËÌÍÎÏÑÒÓÔÕÖÙÚÛÜÝ.txt', date: testDate2, comment: 'ÀÁÂÃÄÇÈÉÊËÌÍÎÏÑÒÓÔÕÖÙÚÛÜÝ' }, function(err) {
          if (err) throw err;
          archive.finalize();
        });
      });
    });

    it('should append zero length sources', function(done) {
      var archive = new Packer();

      var testStream = new WriteHashStream('tmp/zerolength.zip');

      testStream.on('close', function() {
        done();
      });

      archive.pipe(testStream);

      archive.entry('', { name: 'string.txt', date: testDate }, function(err) {
        if (err) throw err;
        archive.entry(new Buffer(0), { name: 'buffer.txt', date: testDate }, function(err) {
          if (err) throw err;
          archive.entry(fs.createReadStream('test/fixtures/empty.txt'), { name: 'stream.txt', date: testDate }, function(err) {
            if (err) throw err;
            archive.finalize();
          });
        });
      });
    });

    it('should support setting file mode (permissions)', function(done) {
      var archive = new Packer();

      var testStream = new WriteHashStream('tmp/filemode.zip');

      testStream.on('close', function() {
        done();
      });

      archive.pipe(testStream);

      archive.entry(testBuffer, { name: 'buffer.txt', date: testDate, mode: 0644 });
      archive.finalize();
    });

    it('should support creating an empty zip', function(done) {
      var archive = new Packer();

      var testStream = new WriteHashStream('tmp/empty.zip');

      testStream.on('close', function() {
        done();
      });

      archive.pipe(testStream);

      archive.finalize();
    });

    it('should support compressing images for Buffer sources', function(done) {
      var archive = new Packer();

      var testStream = new WriteHashStream('tmp/buffer-image.zip');

      testStream.on('close', function() {
        done();
      });

      archive.pipe(testStream);

      archive.entry(fileBuffer('test/fixtures/image.png'), { name: 'image.png', date: testDate });
      archive.finalize();
    });

    it('should support compressing images for Stream sources', function(done) {
      var archive = new Packer();

      var testStream = new WriteHashStream('tmp/stream-image.zip');

      testStream.on('close', function() {
        done();
      });

      archive.pipe(testStream);

      archive.entry(fs.createReadStream('test/fixtures/image.png'), { name: 'image.png', date: testDate });
      archive.finalize();
    });

    it('should prevent UInt32 under/overflow of dates', function(done) {
      var archive = new Packer();

      var testStream = new WriteHashStream('tmp/date-boundaries.zip');

      testStream.on('close', function() {
        done();
      });

      archive.pipe(testStream);

      archive.entry(testBuffer, { name: 'date-underflow.txt', date: testDateUnderflow }, function(err) {
        if (err) throw err;
        archive.entry(testBuffer, { name: 'date-overflow.txt', date: testDateOverflow }, function(err) {
          if (err) throw err;
          archive.finalize();
        });
      });
    });

    it('should handle data that exceeds its internal buffer size', function(done) {
      var archive = new Packer({
        highWaterMark: 1024 * 4,
        forceUTC: true
      });

      var testStream = new WriteHashStream('tmp/buffer-overflow.zip');

      testStream.on('close', function() {
        done();
      });

      archive.pipe(testStream);

      archive.entry(binaryBuffer(1024 * 512), { name: 'buffer-overflow.txt', date: testDate }, function(err) {
        if (err) throw err;
        archive.entry(binaryBuffer(1024 * 1024), { name: 'buffer-overflow-store.txt', date: testDate, store: true }, function(err) {
          if (err) throw err;
          archive.finalize();
        });
      });
    });

    it('should support directory entries', function(done) {
      var archive = new Packer();

      var testStream = new WriteHashStream('tmp/type-directory.zip');

      testStream.on('close', function() {
        done();
      });

      archive.pipe(testStream);

      archive.entry(null, { name: 'directory/', date: testDate });

      archive.finalize();
    });

    it('should support symlink entries', function(done) {
        var archive = new Packer();
        var testStream = new WriteHashStream('tmp/type-symlink.zip');

        testStream.on('close', function() {
            done();
        });

        archive.pipe(testStream);

        archive.entry(null, { name: 'file', date: testDate });
        archive.entry(null, { type: 'symlink', name: 'file-link', linkname: 'file', date: testDate });
        archive.entry(null, { type: 'symlink', name: 'file-link-2', linkname: 'file', date: testDate, mode: 0644 });
        archive.finalize();
    });

  });

});<commit_msg>fix tests.<commit_after>/*global before,describe,it */
var fs = require('fs');

var assert = require('chai').assert;
var mkdir = require('mkdirp');

var helpers = require('./helpers');
var binaryBuffer = helpers.binaryBuffer;
var fileBuffer = helpers.fileBuffer;
var WriteHashStream = helpers.WriteHashStream;

var Packer = require('../index.js');

var testBuffer = binaryBuffer(1024 * 16);

var testDate = new Date('Jan 03 2013 14:26:38 GMT');
var testDate2 = new Date('Feb 10 2013 10:24:42 GMT');

var testDateOverflow = new Date('Jan 1 2044 00:00:00 GMT');
var testDateUnderflow = new Date('Dec 30 1979 23:59:58 GMT');

describe('pack', function() {
  before(function() {
    mkdir.sync('tmp');
  });

  describe('#entry', function() {

    it('should append Buffer sources', function(done) {
      var archive = new Packer();

      var testStream = new WriteHashStream('tmp/buffer.zip');

      testStream.on('close', function() {
        done();
      });

      archive.pipe(testStream);

      archive.entry(testBuffer, { name: 'buffer.txt', date: testDate });
      archive.finalize();
    });

    it('should append Stream sources', function(done) {
      var archive = new Packer();

      var testStream = new WriteHashStream('tmp/stream.zip');

      testStream.on('close', function() {
        done();
      });

      archive.pipe(testStream);

      archive.entry(fs.createReadStream('test/fixtures/test.txt'), { name: 'stream.txt', date: testDate });
      archive.finalize();
    });

    it('should append multiple sources', function(done) {
      var archive = new Packer();

      var testStream = new WriteHashStream('tmp/multiple.zip');

      testStream.on('close', function() {
        done();
      });

      archive.pipe(testStream);

      archive.entry('string', { name: 'string.txt', date: testDate }, function(err) {
        if (err) throw err;
        archive.entry(testBuffer, { name: 'buffer.txt', date: testDate2 }, function(err) {
          if (err) throw err;
          archive.entry(fs.createReadStream('test/fixtures/test.txt'), { name: 'stream.txt', date: testDate2 }, function(err) {
            if (err) throw err;
            archive.entry(fs.createReadStream('test/fixtures/test.txt'), { name: 'stream-store.txt', date: testDate, store: true }, function(err) {
              if (err) throw err;
              archive.finalize();
            });
          });
        });
      });
    });

    it('should support STORE for Buffer sources', function(done) {
      var archive = new Packer();

      var testStream = new WriteHashStream('tmp/buffer-store.zip');

      testStream.on('close', function() {
        done();
      });

      archive.pipe(testStream);

      archive.entry(testBuffer, { name: 'buffer.txt', date: testDate, store: true });
      archive.finalize();
    });

    it('should support STORE for Stream sources', function(done) {
      var archive = new Packer();

      var testStream = new WriteHashStream('tmp/stream-store.zip');

      testStream.on('close', function() {
        done();
      });

      archive.pipe(testStream);

      archive.entry(fs.createReadStream('test/fixtures/test.txt'), { name: 'stream.txt', date: testDate, store: true });
      archive.finalize();
    });

    it('should support archive and file comments', function(done) {
      var archive = new Packer({
        comment: 'this is a zip comment',
        forceUTC: true
      });

      var testStream = new WriteHashStream('tmp/comments.zip');

      testStream.on('close', function() {
        done();
      });

      archive.pipe(testStream);

      archive.entry(testBuffer, { name: 'buffer.txt', date: testDate, comment: 'this is a file comment' });
      archive.finalize();
    });

    it('should STORE files when compression level is zero', function(done) {
      var archive = new Packer({
        forceUTC: true,
        level: 0
      });

      var testStream = new WriteHashStream('tmp/store-level0.zip');

      testStream.on('close', function() {
        assert.equal(testStream.digest, '70b50994c971dbb0e457781cf6d23ca82e5ccbc0');
        done();
      });

      archive.pipe(testStream);

      archive.entry(testBuffer, { name: 'buffer.txt', date: testDate });
      archive.finalize();
    });

    it('should properly handle utf8 encoded characters in file names and comments', function(done) {
      var archive = new Packer();

      var testStream = new WriteHashStream('tmp/accentedchars-filenames.zip');

      testStream.on('close', function() {
        done();
      });

      archive.pipe(testStream);

      archive.entry(testBuffer, { name: 'àáâãäçèéêëìíîïñòóôõöùúûüýÿ.txt', date: testDate, comment: 'àáâãäçèéêëìíîïñòóôõöùúûüýÿ' }, function(err) {
        if (err) throw err;
        archive.entry(testBuffer, { name: 'ÀÁÂÃÄÇÈÉÊËÌÍÎÏÑÒÓÔÕÖÙÚÛÜÝ.txt', date: testDate2, comment: 'ÀÁÂÃÄÇÈÉÊËÌÍÎÏÑÒÓÔÕÖÙÚÛÜÝ' }, function(err) {
          if (err) throw err;
          archive.finalize();
        });
      });
    });

    it('should append zero length sources', function(done) {
      var archive = new Packer();

      var testStream = new WriteHashStream('tmp/zerolength.zip');

      testStream.on('close', function() {
        done();
      });

      archive.pipe(testStream);

      archive.entry('', { name: 'string.txt', date: testDate }, function(err) {
        if (err) throw err;
        archive.entry(new Buffer(0), { name: 'buffer.txt', date: testDate }, function(err) {
          if (err) throw err;
          archive.entry(fs.createReadStream('test/fixtures/empty.txt'), { name: 'stream.txt', date: testDate }, function(err) {
            if (err) throw err;
            archive.finalize();
          });
        });
      });
    });

    it('should support setting file mode (permissions)', function(done) {
      var archive = new Packer();

      var testStream = new WriteHashStream('tmp/filemode.zip');

      testStream.on('close', function() {
        done();
      });

      archive.pipe(testStream);

      archive.entry(testBuffer, { name: 'buffer.txt', date: testDate, mode: 0644 });
      archive.finalize();
    });

    it('should support creating an empty zip', function(done) {
      var archive = new Packer();

      var testStream = new WriteHashStream('tmp/empty.zip');

      testStream.on('close', function() {
        done();
      });

      archive.pipe(testStream);

      archive.finalize();
    });

    it('should support compressing images for Buffer sources', function(done) {
      var archive = new Packer();

      var testStream = new WriteHashStream('tmp/buffer-image.zip');

      testStream.on('close', function() {
        done();
      });

      archive.pipe(testStream);

      archive.entry(fileBuffer('test/fixtures/image.png'), { name: 'image.png', date: testDate });
      archive.finalize();
    });

    it('should support compressing images for Stream sources', function(done) {
      var archive = new Packer();

      var testStream = new WriteHashStream('tmp/stream-image.zip');

      testStream.on('close', function() {
        done();
      });

      archive.pipe(testStream);

      archive.entry(fs.createReadStream('test/fixtures/image.png'), { name: 'image.png', date: testDate });
      archive.finalize();
    });

    it('should prevent UInt32 under/overflow of dates', function(done) {
      var archive = new Packer();

      var testStream = new WriteHashStream('tmp/date-boundaries.zip');

      testStream.on('close', function() {
        done();
      });

      archive.pipe(testStream);

      archive.entry(testBuffer, { name: 'date-underflow.txt', date: testDateUnderflow }, function(err) {
        if (err) throw err;
        archive.entry(testBuffer, { name: 'date-overflow.txt', date: testDateOverflow }, function(err) {
          if (err) throw err;
          archive.finalize();
        });
      });
    });

    it('should handle data that exceeds its internal buffer size', function(done) {
      var archive = new Packer({
        highWaterMark: 1024 * 4,
        forceUTC: true
      });

      var testStream = new WriteHashStream('tmp/buffer-overflow.zip');

      testStream.on('close', function() {
        done();
      });

      archive.pipe(testStream);

      archive.entry(binaryBuffer(1024 * 512), { name: 'buffer-overflow.txt', date: testDate }, function(err) {
        if (err) throw err;
        archive.entry(binaryBuffer(1024 * 1024), { name: 'buffer-overflow-store.txt', date: testDate, store: true }, function(err) {
          if (err) throw err;
          archive.finalize();
        });
      });
    });

    it('should support directory entries', function(done) {
      var archive = new Packer();

      var testStream = new WriteHashStream('tmp/type-directory.zip');

      testStream.on('close', function() {
        done();
      });

      archive.pipe(testStream);

      archive.entry(null, { name: 'directory/', date: testDate });

      archive.finalize();
    });

    it('should support symlink entries', function(done) {
        var archive = new Packer();
        var testStream = new WriteHashStream('tmp/type-symlink.zip');

        testStream.on('close', function() {
            done();
        });

        archive.pipe(testStream);

        archive.entry('some text', { name: 'file', date: testDate }, function(err) {
            if (err) throw err;
            archive.entry(null, { type: 'symlink', name: 'file-link', linkname: 'file', date: testDate }, function(err) {
                if (err) throw err;
                archive.entry(null, { type: 'symlink', name: 'file-link-2', linkname: 'file', date: testDate, mode: 0644 }, function(err) {
                    if (err) throw err;
                    archive.finalize();
                });
            });
        });
    });

  });

});<|endoftext|>


<commit_before>/* global AUDIO_BASE */
/* global SAMPLE_RATE */

import fetch_jsonp from 'fetch-jsonp';
import { format } from './audio';

const AHEAD_SIZE = 60;

let last_floor_index = 0;

export default class AudioChunks {
    constructor(stem) {
        this.stem = stem;
        this.ensured_for_min = -1;
        this.ensured_for_max = -2;
        this.load();
    }

    load() {
        const me = this;
        me.chunks_promise = new Promise((resolve, reject) => {
            const url = AUDIO_BASE + 'split-meta/' + me.stem + '.jsonp';
            fetch_jsonp(url, {
                timeout:               300000,
                jsonpCallback:         'jsonp_splits',
                jsonpCallbackFunction: 'jsonp_splits',
            })
                .catch(reject)
                .then(res => res.json())
                .then(splits => {
                    if (splits &&
                        splits[me.stem] &&
                        splits[me.stem].formats[format.suffix]
                    ) {
                        me.chunks = splits[me.stem].formats[format.suffix];
                        resolve(me.chunks);
                    }
                });
        });
    }

    get_floor_chunk(pos) {
        const me = this;
        let floor_chunk;
        let floor_index;
        const start = me.chunks[last_floor_index].from < pos ? last_floor_index : 0;
        for (let i = start; i < me.chunks.length; i++) {
            const chunk = me.chunks[i];
            if (chunk.from <= pos) {
                floor_chunk = chunk;
                floor_index = i;
            }
            else {
                break;
            }
        }
        if (!floor_chunk) {
            throw new Error('could not find floor chunk for stem ' + me.stem + ', position ' + pos);
        }
        last_floor_index = floor_index;
        return { floor_index, floor_chunk };
    }

    get_chunk_promise(chunk) {
        const me = this;
        if (chunk.promise) {
            return chunk.promise;
        }
        else {
            return me.load_chunk(chunk);
        }
    }

    load_chunk(chunk) {
        const me = this;
        chunk.promise = new Promise((resolve, reject) => {
            me.get_chunk_ea_promise(chunk).then(encoded_audio => {
                new AudioContext({ sampleRate: SAMPLE_RATE }).decodeAudioData(encoded_audio, decoded_buffer => {
                    chunk.buffer = decoded_buffer;
                    resolve(decoded_buffer);
                });
            });
        });
        return chunk.promise;
    }

    get_chunk_ea_promise(chunk) { // ea = encoded audio
        const me = this;
        if (chunk.ea_promise) {
            return chunk.ea_promise;
        }
        else {
            return me.load_chunk_ea(chunk);
        }
    }

    load_chunk_ea(chunk) {
        const me = this;
        chunk.url = AUDIO_BASE + ['splits', me.stem, format.suffix, chunk.basename].join('/');
        chunk.ea_promise = new Promise((resolve, reject) => {
            fetch(chunk.url).then(res => {
                res.arrayBuffer().then(encoded_audio => {
                    // chunk.encoded_audio = encoded_audio;
                    resolve(encoded_audio);
                }).catch(reject);
            }).catch(reject);
        });
        return chunk.ea_promise;
    }

    get_ahead_window(pos, requested) {
        const me = this;
        if (!(({ promise: true, buffer: true, audio_source: true })[requested])) {
            throw new Error('unexpected ahead window attribute ' + requested);
        }
        const { floor_chunk, floor_index } = me.get_floor_chunk(pos);
        if (!floor_chunk[requested]) {
            return [];
        }
        let ceil_index;
        for (let i = floor_index; me.chunks[i][requested]; i++) {
            ceil_index = i;
        }
        return me.chunks.slice(floor_index, ceil_index + 1);
    }

    ensure_ahead_window(pos, len = AHEAD_SIZE) {
        const me = this;
        if (pos >= me.ensured_for_min && pos <= me.ensured_for_max) {
            return [];
        }
        /*
        const ahead_window = me.get_ahead_window(pos, 'promise');
        if (ahead_window.len >= len) {
            return;
        }
        */
        const { floor_index, floor_chunk } = me.get_floor_chunk(pos);

        let i = floor_index;
        let c = floor_chunk;
        const target_pos = pos + len;
        let reached_pos = -2;
        const newly_loaded_chunks = [];
        while (reached_pos < target_pos && i < me.chunks.length) {
            if (!c.promise) {
                me.get_chunk_promise(c);
                newly_loaded_chunks.push(c);
            }
            reached_pos = c.to;
            i++;
            c = me.chunks[i];
        }

        me.ensured_for_min = floor_chunk.from;
        me.ensured_for_max = reached_pos - len;
        return newly_loaded_chunks;
    }

    get_window_chunks(from, to) {
        const me = this;
        const { floor_index, floor_chunk } = me.get_floor_chunk(from);

        let i = floor_index;
        let c = floor_chunk;
        let reached_pos = -2;
        const rv = [];
        while (reached_pos < to && i < me.chunks.length) {
            me.get_chunk_promise(c);
            rv.push(c);
            reached_pos = c.to;
            i++;
            c = me.chunks[i];
        }

        return rv;
    }
}
<commit_msg>fix bug with playback of last chunk<commit_after>/* global AUDIO_BASE */
/* global SAMPLE_RATE */

import fetch_jsonp from 'fetch-jsonp';
import { format } from './audio';

const AHEAD_SIZE = 60;

let last_floor_index = 0;

export default class AudioChunks {
    constructor(stem) {
        this.stem = stem;
        this.ensured_for_min = -1;
        this.ensured_for_max = -2;
        this.load();
    }

    load() {
        const me = this;
        me.chunks_promise = new Promise((resolve, reject) => {
            const url = AUDIO_BASE + 'split-meta/' + me.stem + '.jsonp';
            fetch_jsonp(url, {
                timeout:               300000,
                jsonpCallback:         'jsonp_splits',
                jsonpCallbackFunction: 'jsonp_splits',
            })
                .catch(reject)
                .then(res => res.json())
                .then(splits => {
                    if (splits &&
                        splits[me.stem] &&
                        splits[me.stem].formats[format.suffix]
                    ) {
                        me.chunks = splits[me.stem].formats[format.suffix];
                        resolve(me.chunks);
                    }
                });
        });
    }

    get_floor_chunk(pos) {
        const me = this;
        let floor_chunk;
        let floor_index;
        const start = me.chunks[last_floor_index].from < pos ? last_floor_index : 0;
        for (let i = start; i < me.chunks.length; i++) {
            const chunk = me.chunks[i];
            if (chunk.from <= pos) {
                floor_chunk = chunk;
                floor_index = i;
            }
            else {
                break;
            }
        }
        if (!floor_chunk) {
            throw new Error('could not find floor chunk for stem ' + me.stem + ', position ' + pos);
        }
        last_floor_index = floor_index;
        return { floor_index, floor_chunk };
    }

    get_chunk_promise(chunk) {
        const me = this;
        if (chunk.promise) {
            return chunk.promise;
        }
        else {
            return me.load_chunk(chunk);
        }
    }

    load_chunk(chunk) {
        const me = this;
        chunk.promise = new Promise((resolve, reject) => {
            me.get_chunk_ea_promise(chunk).then(encoded_audio => {
                new AudioContext({ sampleRate: SAMPLE_RATE }).decodeAudioData(encoded_audio, decoded_buffer => {
                    chunk.buffer = decoded_buffer;
                    resolve(decoded_buffer);
                });
            });
        });
        return chunk.promise;
    }

    get_chunk_ea_promise(chunk) { // ea = encoded audio
        const me = this;
        if (chunk.ea_promise) {
            return chunk.ea_promise;
        }
        else {
            return me.load_chunk_ea(chunk);
        }
    }

    load_chunk_ea(chunk) {
        const me = this;
        chunk.url = AUDIO_BASE + ['splits', me.stem, format.suffix, chunk.basename].join('/');
        chunk.ea_promise = new Promise((resolve, reject) => {
            fetch(chunk.url).then(res => {
                res.arrayBuffer().then(encoded_audio => {
                    // chunk.encoded_audio = encoded_audio;
                    resolve(encoded_audio);
                }).catch(reject);
            }).catch(reject);
        });
        return chunk.ea_promise;
    }

    get_ahead_window(pos, requested) {
        const me = this;
        if (!(({ promise: true, buffer: true, audio_source: true })[requested])) {
            throw new Error('unexpected ahead window attribute ' + requested);
        }
        const { floor_chunk, floor_index } = me.get_floor_chunk(pos);
        if (!floor_chunk[requested]) {
            return [];
        }
        let ceil_index;
        for (let i = floor_index; me.chunks[i] && me.chunks[i][requested]; i++) {
            ceil_index = i;
        }
        return me.chunks.slice(floor_index, ceil_index + 1);
    }

    ensure_ahead_window(pos, len = AHEAD_SIZE) {
        const me = this;
        if (pos >= me.ensured_for_min && pos <= me.ensured_for_max) {
            return [];
        }
        /*
        const ahead_window = me.get_ahead_window(pos, 'promise');
        if (ahead_window.len >= len) {
            return;
        }
        */
        const { floor_index, floor_chunk } = me.get_floor_chunk(pos);

        let i = floor_index;
        let c = floor_chunk;
        const target_pos = pos + len;
        let reached_pos = -2;
        const newly_loaded_chunks = [];
        while (reached_pos < target_pos && i < me.chunks.length) {
            if (!c.promise) {
                me.get_chunk_promise(c);
                newly_loaded_chunks.push(c);
            }
            reached_pos = c.to;
            i++;
            c = me.chunks[i];
        }

        me.ensured_for_min = floor_chunk.from;
        me.ensured_for_max = reached_pos - len;
        return newly_loaded_chunks;
    }

    get_window_chunks(from, to) {
        const me = this;
        const { floor_index, floor_chunk } = me.get_floor_chunk(from);

        let i = floor_index;
        let c = floor_chunk;
        let reached_pos = -2;
        const rv = [];
        while (reached_pos < to && i < me.chunks.length) {
            me.get_chunk_promise(c);
            rv.push(c);
            reached_pos = c.to;
            i++;
            c = me.chunks[i];
        }

        return rv;
    }
}
<|endoftext|>


<commit_before>// Shim Ember module
export default Ember;
<commit_msg>remove 'packages' directory<commit_after><|endoftext|>


<commit_before>

actions.fight = function (params, successCallback, errorCallback) {
    checkParams(1, params);
    var monsterKey = params[0];
    var monster = monsters[monsterKey];
    var fightAction = getAreaAction('fight', monster);
    if (!monster || !fightAction) {
        throw new ProgrammingError("There is no '" + monsterKey + "' to fight here.");
    }
    fightAction.perform();
    winBattleCallback = successCallback;
    loseBattleCallback = errorCallback;
}

var fighting = null;
var winBattleCallback = null;
var loseBattleCallback = null;
var winInstantly = false;

function BattleAction(sourceMonster, slot, victoryFunction) {
    var monster = copy(sourceMonster);
    if (!monsters[monster.key]) {
        throw new Exception("Battle Action expects a monster: " + sourceMonster);
    }
    monster.maxHealth = monster.health;
    monster.damaged = 0;
    monster.battleStatus = freshBattleStatus();
    //used to trigger special results when bosses are defeated
    monster.victoryFunction = victoryFunction;
    this.actionName = "fight";
    this.actionTarget = sourceMonster;
    this.monster = monster;
    this.getDiv = function () {
        monster.$element = $('.js-monster').clone().removeClass('js-monster').show();
        monster.$element.find('.js-name').text("Lvl " + monster.level + " " + monster.name);
        monster.$element.find('.js-graphic').html(monster.$graphic);
        updateMonster(monster);
        return $div('action slot' + slot, monster.$element).attr('helpText', monster.helpText);
    };
    this.perform = function () {
        if (fighting === monster) {
            closeAll();
        } else {
            closeAll();
            fighting = monster;
            monster.accruedDamageForDisplay = 0;
            monster.hasHitsToDisplay = false;
            player.nextAttack = 1000 / player.getAttackSpeed();
            monster.nextAttack = 1000 / monster.attackSpeed;
        }
    }
}
function applyCripple(attackSpeed, cripple) {
    return attackSpeed / (1 + Math.log(1 + cripple / 6));
}
function fightLoop(currentTime, deltaTime) {
    var monster = fighting;
    processStatusEffects(player, deltaTime);
    processStatusEffects(monster, deltaTime);
    player.nextAttack -= deltaTime;
    if (player.nextAttack <= 0) {
        var armorPierce = player.getArmorPierce();
        var damage = player.getDamage();
        if (!player.specialSkills.scan) {
            var factor = 1 / player.getTenacity();
            var parry = monster.parry ? monster.parry : 0;
            damage = applyArmorToDamage(damage, Math.max(0, ((parry * factor + Math.max(0, monster.armor - monster.battleStatus.armorReduction))) * (1 - armorPierce)));
            if (monster.reflect) {
                var mitigatedDamage = player.getDamage() - damage;
                var reflectedDamage = Math.floor(mitigatedDamage * monster.reflect);
                if (reflectedDamage) {
                    player.health = player.health - reflectedDamage;
                }
            }
        }
        monster.hasHitsToDisplay = true;
        if (damage > 0) {
            monster.health = Math.max(0, monster.health - damage);
            monster.accruedDamageForDisplay += damage;
            if (!player.specialSkills.poach) {
                monster.damaged++;
            }
        }
        var armorBreak = player.getArmorBreak();
        if (armorBreak) {
            monster.battleStatus.armorReduction += armorBreak;
        }
        var cripple = player.getCripple();
        if (cripple) {
            monster.battleStatus.crippled += cripple;
        }
        var poison = player.getPoison();
        if (poison) {
            monster.battleStatus.poisonDamage += poison;
            monster.battleStatus.poisonRate++;
        }
        var lifeSteal = player.getLifeSteal();
        if (lifeSteal) {
            player.health = Math.min(player.getMaxHealth(), player.health + Math.floor(damage * lifeSteal));
        }
        player.health = Math.max(0, player.health);
        player.nextAttack += 1000 / player.getAttackSpeed();
    }
    monster.nextAttack -= deltaTime;
    if (monster.nextAttack <= 0) {
        var factor = 1 / player.getTenacity();
        var armorPierce = monster.armorPierce ? (factor * monster.armorPierce) : 0;
        var damage = applyArmorToDamage(monster.damage, Math.max(0, player.getArmor() * (1 - armorPierce)));
        var mitigatedDamage = monster.damage - damage;
        var reflectedDamage = Math.floor(mitigatedDamage * getTotalEnchantment('reflect'));
        if (reflectedDamage) {
            monster.health = Math.max(0, monster.health - reflectedDamage);
        }
        player.health = Math.max(0, player.health - damage);
        if (monster.armorBreak) {
            player.battleStatus.armorReduction += Math.floor(factor * monster.armorBreak);
        }
        if (monster.cripple) {
            player.battleStatus.crippled += Math.floor(factor * monster.cripple);
        }
        if (monster.poison) {
            player.battleStatus.poisonDamage += Math.floor(factor * monster.poison);
            player.battleStatus.poisonRate += Math.floor(factor);
        }
        if (monster.lifeSteal) {
            monster.health = Math.min(monster.maxHealth, monster.health + Math.floor(damage * factor * monster.lifeSteal));
        }
        monster.nextAttack += 1000 / monsterAttackSpeed(monster);
    }
    if (winInstantly || monster.health <= 0) {
        gainExperience(Math.floor(monster.experience * (1 + getTotalEnchantment('experience'))), monster.level);
        player.defeatedMonsters[monster.key]++;

        var dropIndex = getDropIndex(monster);
        for (var i = dropIndex; i < monster.spoils.length; i++) {
            var dropValue = monster.spoils[i];
            if (typeof(dropValue) === 'string' && allItems[dropValue]) {
                var item = allItems[dropValue];
                player.inventory[item.slot][dropValue]++;
                if (player.specialSkills.lucky) {
                    player.inventory[item.slot][dropValue]++;
                }
                //reset coolingMagma timer each time you gain one from battle
                if (item.key == 'coolingMagma') {
                    items.coolingMagma.timer = 30000;
                }
                uiNeedsUpdate[item.slot] = true;
            } else if (typeof(dropValue) === 'number') {
                player.gold += dropValue;
                if (player.specialSkills.lucky) {
                    player.gold += dropValue;
                }
            }
        }
        stopFighting(true);
        if (monster.victoryFunction) {
            monster.victoryFunction();
        }
        recordAction('fight', monster.key);
        return;
    }
    if (player.health <= 0) {
        stopFighting(false);
    }
}
/**
 * Shows a number on the enemy graphic indicating how much damage has happened
 * since the last time this method has been called (or the start of battle),
 * if the player has hit the monster since then. Damage will show even if 0,
 * although 0 damage may not be possible in the game any longer
 */
function showAccruedDamageOnMonster() {
    var monster = fighting;
    if (!monster || !monster.hasHitsToDisplay || damageCounterRefresh > 0) {
        return;
    }
    damageCounterRefresh = 3;
    //show damage animation only when the game speed is below 30
    var $damage = $('' + monster.accruedDamageForDisplay + '');
    monster.$element.find('.js-graphic').append($damage);
    $damage.animate({top: "-=50"}, 500,
        function () {
            $damage.remove();
        }
    );
    monster.hasHitsToDisplay = false;
    monster.accruedDamageForDisplay = 0;
}
function applyArmorToDamage(damage, armor) {
    //This equation looks a bit funny but is designed to have the following properties:
    //100% damage at 0 armor
    //50% damage when armor is 1/3 of base damage
    //25% damage when armor is 2/3 of base damage
    //1/(2^N) damage when armor is N/3 of base damage
    return Math.max(1, Math.round(damage / Math.pow(2, 3 * armor / damage)));
}
function processStatusEffects(target, deltaTime) {
    //always process this for player in case the player has a special DOT on them
    if (target.battleStatus.poisonDamage || target.isPlayer) {
        var damage = Math.min(target.battleStatus.poisonDamage, target.battleStatus.poisonRate * deltaTime / 100);
        //console.log(target.battleStatus.dealtPoisonDamage + ' + ' + damage);
        //dealtPoisonDamage accrues the floating point damage from poison over time
        target.battleStatus.dealtPoisonDamage += damage;
        //use the dealt poison damage to accrue extra DOT from cooling magma and other sources during battles
        if (target.isPlayer) {
            target.battleStatus.dealtPoisonDamage += player.getDamageOverTime() * deltaTime / 1000;
        }
        //when it is greater than 1 damage is dealt to the targets health and remove from dealtPoisonDamage
        if (target.battleStatus.dealtPoisonDamage > 1) {
            target.health = Math.max(0, target.health - Math.floor(target.battleStatus.dealtPoisonDamage));
            target.battleStatus.dealtPoisonDamage = target.battleStatus.dealtPoisonDamage % 1;
        }
        target.battleStatus.poisonDamage -= damage;
    } else {
        //poison rate is reduced back to 0 if poison ever clears the target's system
        target.battleStatus.poisonRate = 0;
    }
    if (target.battleStatus.crippled) {
        target.battleStatus.crippled = Math.max(0, target.battleStatus.crippled - deltaTime / 1000);
    }
}

function stopFighting(victory) {
    player.battleStatus = freshBattleStatus();
    if (fighting) {
        showAccruedDamageOnMonster();
        var oldMonster = fighting;
        fighting = null;
        if (!oldMonster.doNotRegenerate) {
            oldMonster.health = oldMonster.maxHealth;
        }
        oldMonster.damaged = 0;
        oldMonster.battleStatus = freshBattleStatus();
        updateMonster(oldMonster);
        if (victory && winBattleCallback) {
            winBattleCallback();
            winBattleCallback = null;
        }
        if (!victory && loseBattleCallback) {
            loseBattleCallback('You did not defeat the ' + oldMonster.name);
            loseBattleCallback = null;
        }
        removeToolTip();
    }
    uiNeedsUpdate.playerStats = true;
}

function updateMonster(monster) {
    var $monster = monster.$element;
    if (!$monster) {
        return;
    }
    $monster.find('.js-experience').text(monster.experience);
    $monster.find('.js-currentHealth').text(Math.ceil(monster.health));
    $monster.find('.js-maxHealth').text(Math.ceil(monster.maxHealth));
    $monster.find('.js-damage').text(monster.damage);
    $monster.find('.js-attackSpeed').text(monsterAttackSpeed(monster).toFixed(2));
    $monster.find('.js-armor').text(Math.max(0, monster.armor - monster.battleStatus.armorReduction));
    var healthPercent = monster.health / monster.maxHealth;
    $monster.find('.js-healthFill').css('width', (100 * healthPercent) + '%');
    var dropIndex = getDropIndex(monster)
    var $itemRow = $monster.find('.js-spoils').remove().first();
    for (var i = dropIndex; i < monster.spoils.length; i++) {
        item = monster.spoils[i];
        if (typeof(item) === 'string' && allItems[item]) {
            $itemRow.find('.js-item').text(allItems[item].name);
        } else if (typeof(item) === 'number') {
            $itemRow.find('.js-icon').removeClass('item').addClass('gold');
            $itemRow.find('.js-item').text(item);
        } else {
            $itemRow.find('.js-item').text(typeof(item));
        }
        $monster.find('.js-spoilsContainer').append($itemRow.clone());
    }
}
function monsterAttackSpeed(monster) {
    var attackSpeed = applyCripple(monster.attackSpeed, monster.battleStatus.crippled);
    if (player.helmet == helmets.proudHat) {
        attackSpeed *= .9;
    }
    return attackSpeed;
}
function scheduleMonsterForUpdate(monster) {
    uiNeedsUpdate.monsters[monster.key] = monster;
}

function getDropIndex(monster) {
    return Math.min(Math.max(0, monster.spoils.length - 1), Math.floor(Math.log(1 + (monster.damaged ? monster.damaged : 0) / (1 + player.poachingSkill)) / Math.log(2)));
}<commit_msg>Only apply proud hat effect when you are actually fighting the monster.<commit_after>

actions.fight = function (params, successCallback, errorCallback) {
    checkParams(1, params);
    var monsterKey = params[0];
    var monster = monsters[monsterKey];
    var fightAction = getAreaAction('fight', monster);
    if (!monster || !fightAction) {
        throw new ProgrammingError("There is no '" + monsterKey + "' to fight here.");
    }
    fightAction.perform();
    winBattleCallback = successCallback;
    loseBattleCallback = errorCallback;
}

var fighting = null;
var winBattleCallback = null;
var loseBattleCallback = null;
var winInstantly = false;

function BattleAction(sourceMonster, slot, victoryFunction) {
    var monster = copy(sourceMonster);
    if (!monsters[monster.key]) {
        throw new Exception("Battle Action expects a monster: " + sourceMonster);
    }
    monster.maxHealth = monster.health;
    monster.damaged = 0;
    monster.battleStatus = freshBattleStatus();
    //used to trigger special results when bosses are defeated
    monster.victoryFunction = victoryFunction;
    this.actionName = "fight";
    this.actionTarget = sourceMonster;
    this.monster = monster;
    this.getDiv = function () {
        monster.$element = $('.js-monster').clone().removeClass('js-monster').show();
        monster.$element.find('.js-name').text("Lvl " + monster.level + " " + monster.name);
        monster.$element.find('.js-graphic').html(monster.$graphic);
        updateMonster(monster);
        return $div('action slot' + slot, monster.$element).attr('helpText', monster.helpText);
    };
    this.perform = function () {
        if (fighting === monster) {
            closeAll();
        } else {
            closeAll();
            fighting = monster;
            monster.accruedDamageForDisplay = 0;
            monster.hasHitsToDisplay = false;
            player.nextAttack = 1000 / player.getAttackSpeed();
            monster.nextAttack = 1000 / monster.attackSpeed;
        }
    }
}
function applyCripple(attackSpeed, cripple) {
    return attackSpeed / (1 + Math.log(1 + cripple / 6));
}
function fightLoop(currentTime, deltaTime) {
    var monster = fighting;
    processStatusEffects(player, deltaTime);
    processStatusEffects(monster, deltaTime);
    player.nextAttack -= deltaTime;
    if (player.nextAttack <= 0) {
        var armorPierce = player.getArmorPierce();
        var damage = player.getDamage();
        if (!player.specialSkills.scan) {
            var factor = 1 / player.getTenacity();
            var parry = monster.parry ? monster.parry : 0;
            damage = applyArmorToDamage(damage, Math.max(0, ((parry * factor + Math.max(0, monster.armor - monster.battleStatus.armorReduction))) * (1 - armorPierce)));
            if (monster.reflect) {
                var mitigatedDamage = player.getDamage() - damage;
                var reflectedDamage = Math.floor(mitigatedDamage * monster.reflect);
                if (reflectedDamage) {
                    player.health = player.health - reflectedDamage;
                }
            }
        }
        monster.hasHitsToDisplay = true;
        if (damage > 0) {
            monster.health = Math.max(0, monster.health - damage);
            monster.accruedDamageForDisplay += damage;
            if (!player.specialSkills.poach) {
                monster.damaged++;
            }
        }
        var armorBreak = player.getArmorBreak();
        if (armorBreak) {
            monster.battleStatus.armorReduction += armorBreak;
        }
        var cripple = player.getCripple();
        if (cripple) {
            monster.battleStatus.crippled += cripple;
        }
        var poison = player.getPoison();
        if (poison) {
            monster.battleStatus.poisonDamage += poison;
            monster.battleStatus.poisonRate++;
        }
        var lifeSteal = player.getLifeSteal();
        if (lifeSteal) {
            player.health = Math.min(player.getMaxHealth(), player.health + Math.floor(damage * lifeSteal));
        }
        player.health = Math.max(0, player.health);
        player.nextAttack += 1000 / player.getAttackSpeed();
    }
    monster.nextAttack -= deltaTime;
    if (monster.nextAttack <= 0) {
        var factor = 1 / player.getTenacity();
        var armorPierce = monster.armorPierce ? (factor * monster.armorPierce) : 0;
        var damage = applyArmorToDamage(monster.damage, Math.max(0, player.getArmor() * (1 - armorPierce)));
        var mitigatedDamage = monster.damage - damage;
        var reflectedDamage = Math.floor(mitigatedDamage * getTotalEnchantment('reflect'));
        if (reflectedDamage) {
            monster.health = Math.max(0, monster.health - reflectedDamage);
        }
        player.health = Math.max(0, player.health - damage);
        if (monster.armorBreak) {
            player.battleStatus.armorReduction += Math.floor(factor * monster.armorBreak);
        }
        if (monster.cripple) {
            player.battleStatus.crippled += Math.floor(factor * monster.cripple);
        }
        if (monster.poison) {
            player.battleStatus.poisonDamage += Math.floor(factor * monster.poison);
            player.battleStatus.poisonRate += Math.floor(factor);
        }
        if (monster.lifeSteal) {
            monster.health = Math.min(monster.maxHealth, monster.health + Math.floor(damage * factor * monster.lifeSteal));
        }
        monster.nextAttack += 1000 / monsterAttackSpeed(monster);
    }
    if (winInstantly || monster.health <= 0) {
        gainExperience(Math.floor(monster.experience * (1 + getTotalEnchantment('experience'))), monster.level);
        player.defeatedMonsters[monster.key]++;

        var dropIndex = getDropIndex(monster);
        for (var i = dropIndex; i < monster.spoils.length; i++) {
            var dropValue = monster.spoils[i];
            if (typeof(dropValue) === 'string' && allItems[dropValue]) {
                var item = allItems[dropValue];
                player.inventory[item.slot][dropValue]++;
                if (player.specialSkills.lucky) {
                    player.inventory[item.slot][dropValue]++;
                }
                //reset coolingMagma timer each time you gain one from battle
                if (item.key == 'coolingMagma') {
                    items.coolingMagma.timer = 30000;
                }
                uiNeedsUpdate[item.slot] = true;
            } else if (typeof(dropValue) === 'number') {
                player.gold += dropValue;
                if (player.specialSkills.lucky) {
                    player.gold += dropValue;
                }
            }
        }
        stopFighting(true);
        if (monster.victoryFunction) {
            monster.victoryFunction();
        }
        recordAction('fight', monster.key);
        return;
    }
    if (player.health <= 0) {
        stopFighting(false);
    }
}
/**
 * Shows a number on the enemy graphic indicating how much damage has happened
 * since the last time this method has been called (or the start of battle),
 * if the player has hit the monster since then. Damage will show even if 0,
 * although 0 damage may not be possible in the game any longer
 */
function showAccruedDamageOnMonster() {
    var monster = fighting;
    if (!monster || !monster.hasHitsToDisplay || damageCounterRefresh > 0) {
        return;
    }
    damageCounterRefresh = 3;
    //show damage animation only when the game speed is below 30
    var $damage = $('' + monster.accruedDamageForDisplay + '');
    monster.$element.find('.js-graphic').append($damage);
    $damage.animate({top: "-=50"}, 500,
        function () {
            $damage.remove();
        }
    );
    monster.hasHitsToDisplay = false;
    monster.accruedDamageForDisplay = 0;
}
function applyArmorToDamage(damage, armor) {
    //This equation looks a bit funny but is designed to have the following properties:
    //100% damage at 0 armor
    //50% damage when armor is 1/3 of base damage
    //25% damage when armor is 2/3 of base damage
    //1/(2^N) damage when armor is N/3 of base damage
    return Math.max(1, Math.round(damage / Math.pow(2, 3 * armor / damage)));
}
function processStatusEffects(target, deltaTime) {
    //always process this for player in case the player has a special DOT on them
    if (target.battleStatus.poisonDamage || target.isPlayer) {
        var damage = Math.min(target.battleStatus.poisonDamage, target.battleStatus.poisonRate * deltaTime / 100);
        //console.log(target.battleStatus.dealtPoisonDamage + ' + ' + damage);
        //dealtPoisonDamage accrues the floating point damage from poison over time
        target.battleStatus.dealtPoisonDamage += damage;
        //use the dealt poison damage to accrue extra DOT from cooling magma and other sources during battles
        if (target.isPlayer) {
            target.battleStatus.dealtPoisonDamage += player.getDamageOverTime() * deltaTime / 1000;
        }
        //when it is greater than 1 damage is dealt to the targets health and remove from dealtPoisonDamage
        if (target.battleStatus.dealtPoisonDamage > 1) {
            target.health = Math.max(0, target.health - Math.floor(target.battleStatus.dealtPoisonDamage));
            target.battleStatus.dealtPoisonDamage = target.battleStatus.dealtPoisonDamage % 1;
        }
        target.battleStatus.poisonDamage -= damage;
    } else {
        //poison rate is reduced back to 0 if poison ever clears the target's system
        target.battleStatus.poisonRate = 0;
    }
    if (target.battleStatus.crippled) {
        target.battleStatus.crippled = Math.max(0, target.battleStatus.crippled - deltaTime / 1000);
    }
}

function stopFighting(victory) {
    player.battleStatus = freshBattleStatus();
    if (fighting) {
        showAccruedDamageOnMonster();
        var oldMonster = fighting;
        fighting = null;
        if (!oldMonster.doNotRegenerate) {
            oldMonster.health = oldMonster.maxHealth;
        }
        oldMonster.damaged = 0;
        oldMonster.battleStatus = freshBattleStatus();
        updateMonster(oldMonster);
        if (victory && winBattleCallback) {
            winBattleCallback();
            winBattleCallback = null;
        }
        if (!victory && loseBattleCallback) {
            loseBattleCallback('You did not defeat the ' + oldMonster.name);
            loseBattleCallback = null;
        }
        removeToolTip();
    }
    uiNeedsUpdate.playerStats = true;
}

function updateMonster(monster) {
    var $monster = monster.$element;
    if (!$monster) {
        return;
    }
    $monster.find('.js-experience').text(monster.experience);
    $monster.find('.js-currentHealth').text(Math.ceil(monster.health));
    $monster.find('.js-maxHealth').text(Math.ceil(monster.maxHealth));
    $monster.find('.js-damage').text(monster.damage);
    $monster.find('.js-attackSpeed').text(monsterAttackSpeed(monster).toFixed(2));
    $monster.find('.js-armor').text(Math.max(0, monster.armor - monster.battleStatus.armorReduction));
    var healthPercent = monster.health / monster.maxHealth;
    $monster.find('.js-healthFill').css('width', (100 * healthPercent) + '%');
    var dropIndex = getDropIndex(monster)
    var $itemRow = $monster.find('.js-spoils').remove().first();
    for (var i = dropIndex; i < monster.spoils.length; i++) {
        item = monster.spoils[i];
        if (typeof(item) === 'string' && allItems[item]) {
            $itemRow.find('.js-item').text(allItems[item].name);
        } else if (typeof(item) === 'number') {
            $itemRow.find('.js-icon').removeClass('item').addClass('gold');
            $itemRow.find('.js-item').text(item);
        } else {
            $itemRow.find('.js-item').text(typeof(item));
        }
        $monster.find('.js-spoilsContainer').append($itemRow.clone());
    }
}
function monsterAttackSpeed(monster) {
    var attackSpeed = applyCripple(monster.attackSpeed, monster.battleStatus.crippled);
    if (fighting == monster && player.helmet == helmets.proudHat) {
        attackSpeed *= .9;
    }
    return attackSpeed;
}
function scheduleMonsterForUpdate(monster) {
    uiNeedsUpdate.monsters[monster.key] = monster;
}

function getDropIndex(monster) {
    return Math.min(Math.max(0, monster.spoils.length - 1), Math.floor(Math.log(1 + (monster.damaged ? monster.damaged : 0) / (1 + player.poachingSkill)) / Math.log(2)));
}<|endoftext|>