<commit_before>var crypto = require('crypto'),
  util = require('util'),
  ansi = require('colorize')
  .ansify,

  Commands = require('./commands')
  .Commands,
  Channels = require('./channels')
  .Channels,
  Data = require('./data')
  .Data,
  Item = require('./items')
  .Item,
  Player = require('./player')
  .Player,
  Skills = require('./skills')
  .Skills,
  l10nHelper = require('./l10n');

/**
 * Localization
 */
var l10n = null;
var l10nFile = __dirname + '/../l10n/events.yml';
// shortcut for l10n.translate
var L = null;

var players = null;
var npcs = null;
var rooms = null;
var items = null;

// Keep track of password attempts
var password_attempts = {};

/**
 * Helper for advancing staged events
 * @param string stage
 * @param object firstarg Override for the default arg
 */
var gen_next = function (event) {
  /**
   * Move to the next stage of a staged event
   * @param Socket|Player socket       Either a Socket or Player on which emit() will be called
   * @param string        nextstage
   * @param ...
   */
  return function (socket, nextstage) {
    var func = (socket instanceof Player ? socket.getSocket() : socket);
    func.emit.apply(func, [event].concat([].slice.call(arguments)));
  }
};

/**
 * Helper for repeating staged events
 * @param Array repeat_args
 * @return function
 */
var gen_repeat = function (repeat_args, next) {
  return function () {
    next.apply(null, [].slice.call(repeat_args))
  };
};

/**
 * Events object is a container for any "context switches" for the player.
 * Essentially anything that requires player input will have its own listener
 * if it isn't just a basic command. Complex listeners are staged.
 * See login or createPlayer for examples
 */
var Events = {
  /**
   * Container for events
   * @var object
   */
  events: {
    /**
     * Point of entry for the player. They aren't actually a player yet
     * @param Socket socket
     */
    login: function login(socket, stage, dontwelcome, name) {

      util.log("Login event detected... ", stage);

      // dontwelcome is used to swallow telnet bullshit
      dontwelcome = typeof dontwelcome == -'undefined' ? false :
        dontwelcome;
      stage = stage || 'intro';

      if (socket instanceof Player) {
        l10n.setLocale('en');
      }

      var next = gen_next('login');
      var repeat = gen_repeat(arguments, next);

      switch (stage) {

        case 'intro':
          var motd = Data.loadMotd();
          if (motd) socket.write(motd);
          next(socket, 'login');
          break;

        case 'login':
          if (!dontwelcome) {
            socket.write("Welcome, what is your name? ");
          }

          socket.once('data', function (name) {

            // swallow any data that's not from player input i.e., doesn't end with a newline
            // Windows can s@#* a d@#$
            var negot = false;
            if (name[name.length - 1] === 0x0a) {
              negot = true;
            } else if (name[name.length - 1] === 0x0d) {
              negot = true;
            }

            if (!negot) {
              next(socket, 'login', true);
              return;
            }

            var name = name.toString()
              .trim();
            if (/[^a-z]/i.test(name) || !name) {
              socket.write("That's not really your name, now is it?\r\n");
              return repeat();
            }

            name = name[0].toUpperCase() + name.toLowerCase()
              .substr(1);

            var data = Data.loadPlayer(name);

            // That player doesn't exit so ask if them to create it
            if (!data) {
              socket.emit('createPlayer', socket);
              return;
            }

            next(socket, 'password', false, name);
            return;
          });
          break;

        case 'password':
          if (typeof password_attempts[name] === 'undefined') {
            password_attempts[name] = 0;
          }

          // Boot and log any failed password attempts
          if (password_attempts[name] > 2) {
            socket.write(L('PASSWORD_EXCEEDED') + "\r\n");
            password_attempts[name] = 0;
            util.log('Failed login - exceeded password attempts - ' + name);
            socket.end();
            return false;
          }

          if (!dontwelcome) {
            socket.write(L('PASSWORD'));
          }

          socket.once('data', pass => {
            // Skip garbage
            if (pass[0] === 0xFA) {
              return next(socket, 'password', true, name);
            }

            pass = crypto
              .createHash('md5')
              .update(pass.toString('').trim())
              .digest('hex');

            if (pass !== Data.loadPlayer(name).password) {
              util.log("Failed password attempt by ", socket)
              socket.write(L('PASSWORD_FAIL') + "\r\n");
              password_attempts[name] += 1;
              return repeat();
            }
            next(socket, 'done', name);
          });
          break;

        case 'done':
          var name = dontwelcome;
          var haveSameName = p => p.getName().toLowerCase() === name.toLowerCase();
          var boot = players.some(haveSameName);

          if (boot) {
            players.eachIf(haveSameName,
              p => {
                p.say("Replaced.");
                p.emit('quit');
                util.log("Replaced: ", p.getName());
                players.removePlayer(p, true);
              });
          }

          player = new Player(socket);
          player.load(Data.loadPlayer(name));
          players.addPlayer(player);

          player.getSocket()
            .on('close', () => {
              player.setTraining('beginTraining', Date.now());

              if (!player.isInCombat()) {
                util.log(player.getName() + ' has gone linkdead.');
                player.save(() => players.removePlayer(player, true));
              } else {
                util.log(player.getName() + ' socket closed during combat!!!');
              }

              //TODO: Consider saving player here as well, and stuff.
              players.removePlayer(player);
            });

          players.broadcastL10n(l10n, 'WELCOME_BACK', player.getName());

          //TODO: Have load in player file?
          // Load the player's inventory (There's probably a better place to do this)
          var inv = [];
          player.getInventory()
            .forEach(item => {
              item = new Item(item);
              items.addItem(item);
              inv.push(item);
            });
          player.setInventory(inv);

          Commands.player_commands.look(null, player);
          player.checkTraining();
          player.prompt();

          // All that shit done, let them play!
          player.getSocket()
            .emit("commands", player);
          break;
      }
    },

    /**
     * Command loop
     * @param Player player
     */
    commands: function (player) {
      // Parse order is common direction shortcuts -> commands -> exits -> skills -> channels
      player.getSocket()
        .once('data', function (data) {
          data = data.toString().trim();

          var result;
          if (data) result = parseCommands(data);
          if (result !== false) { commandPrompt(); }

          // Methods

          function parseCommands(data) {
            var command = data
              .split(' ')
              .shift();
            var args = data
              .split(' ')
              .slice(1)
              .join(' ');

            var found = false;

            if (!(command in Commands.player_commands)) {

              found = checkForDirectionAlias(command);
              if (!found) found = checkForCmd(command);
              else if (found === true) return;

              if (found) return getCmd(found, args);
              else return checkForSpecializedCommand(command, args);

            } else return getCmd(command, args);
          }

          function getCmd(cmd, args) {
            try {
              return Commands.player_commands[cmd](args, player);
            } catch (e) { util.log(e) }
          }

          function checkForDirectionAlias(command) {
            var directions = {
              'n': 'north',
              'e': 'east',
              's': 'south',
              'w': 'west',
              'u': 'up',
              'd': 'down'
            };

            if (command.toLowerCase() in directions) {
              const exit = directions[command.toLowerCase()];
              Commands.room_exits(exit, player);
              return true;
            }
          }

          function checkForCmd(command) {
            for (var cmd in Commands.player_commands) {
              try {
                var regex = new RegExp("^" + command);
              } catch (err) {
                continue;
              }
              if (cmd.match(regex)) {
                return cmd;
              }
            }
          }

          // Handles skills, feats, exits
          function checkForSpecializedCommand(command, args) {
            var exit = Commands.room_exits(command, player);

            //TODO: Refactor as to not rely on negative conditionals as much?
            if (exit === false) {
              var isSkill = command in player.getSkills();
              var isFeat = command in player.getFeats();

              if (!isSkill && !isFeat) {
                if (!(command in Channels)) {
                  player.say(command + " is not a valid command.");
                  return true;
                } else {
                  Channels[command].use(args, player, players, rooms);
                  return true
                }
              } else {
                var use = isSkill ? player.useSkill : player.useFeat;
                use(command, player, args, rooms, npcs, players);
                return true;
              }
            }
          }

          function commandPrompt() {
            player.prompt();
            player.getSocket()
              .emit("commands", player);
          }

        });
    },

    /**
     * Create a player
     * Stages:
     *   check:  See if they actually want to create a player or not
     *   locale: Get the language they want to play in so we can give them
     *           the rest of the creation process in their language
     *   name:   ... get there name
     *   done:   This is always the end step, here we register them in with
     *           the rest of the logged in players and where they log in
     *
     * @param object arg This is either a Socket or a Player depending on
     *                  the stage.
     * @param string stage See above
     */
    createPlayer: function (socket, stage) {
      stage = stage || 'check';

      if (socket instanceof Player) {
        l10n.setLocale("en");
      }

      var next = gen_next('createPlayer');
      var repeat = gen_repeat(arguments, next);

      /* Multi-stage character creation i.e., races, classes, etc.
       * Always emit 'done' in your last stage to keep it clean
       * Also try to put the cases in order that they happen during creation
       */
      switch (stage) {
        case 'check':
          socket.write(
            "That player doesn't exist, would you like to create it? [y/n] "
          );
          socket.once('data', function (check) {
            check = check.toString()
              .trim()
              .toLowerCase();
            if (!/[yn]/.test(check)) {
              return repeat();
            }

            if (check === 'n') {
              socket.write("Goodbye!\r\n");
              socket.end();
              return false;
            }

            next(socket, 'name');
          });
          break;
        case 'name':
          socket = new Player(socket);
          socket.write(L('NAME_PROMPT'));
          socket.getSocket()
            .once('data', function (name) {
              name = name.toString()
                .trim();
              if (/\W/.test(name)) {
                socket.say(L('INVALID_NAME'));
                return repeat();
              }

              var player = false;
              players.every(function (p) {
                if (p.getName()
                  .toLowerCase() === name.toLowerCase()) {
                  player = true;
                  return false;
                }
                return true;
              });

              player = player || Data.loadPlayer(name);

              if (player) {
                socket.say(L('NAME_TAKEN'));
                return repeat();
              }

              // Always give them a name like Shawn instead of sHaWn
              socket.setName(name[0].toUpperCase() + name.toLowerCase()
                .substr(
                  1));
              next(socket, 'password');
            });
          break;
        case 'password':
          socket.write(L('PASSWORD'));
          socket.getSocket()
            .once('data', function (pass) {
              pass = pass.toString()
                .trim();
              if (!pass) {
                socket.sayL10n(l10n, 'EMPTY_PASS');
                return repeat();
              }

              // setPassword handles hashing
              socket.setPassword(pass);
              next(socket, 'gender');
            });
          break;
        case 'gender':
          socket.write(
            'What is your character\'s gender?\n[F]emale\n[M]ale\n[A]ndrogynous\n'
          );
          socket.getSocket()
            .once('data', function (gender) {
              gender = gender.toString()
                .toLowerCase();
              if (!gender) {
                socket.say(
                  'Please specify a gender, or [A]ndrogynous if you\'d prefer not to.'
                );
                return repeat();
              }
              socket.setGender(gender);
              next(socket, 'done');
            });
          break;
          // 'done' assumes the argument passed to the event is a player, ...so always do that.
        case 'done':
          socket.setLocale("en");
          socket.setLocation(players.getDefaultLocation());

          // create the pfile then send them on their way
          socket.save(() => {
            players.addPlayer(socket);
            socket.prompt();
            socket.getSocket()
              .emit('commands', socket);
          });

          util.log("A NEW CHALLENGER APPROACHES: ", socket);
          players.broadcastL10n(l10n, 'WELCOME', socket.getName());
          break;

      }
    }
  },

  configure: function (config) {
    players = players || config.players;
    items = items || config.items;
    rooms = rooms || config.rooms;
    npcs = npcs || config.npcs;
    if (!l10n) {
      util.log("Loading event l10n... ");
      l10n = l10nHelper(l10nFile);
      util.log("Done");
    }
    l10n.setLocale(config.locale);

    /**
     * Hijack translate to also do coloring
     * @param string text
     * @param ...
     * @return string
     */
    L = function (text) {
      return ansi(l10n.translate.apply(null, [].slice.call(arguments)));
    };
  }
};
exports.Events = Events;
<commit_msg>Think I found it...<commit_after>var crypto = require('crypto'),
  util = require('util'),
  ansi = require('colorize')
  .ansify,

  Commands = require('./commands')
  .Commands,
  Channels = require('./channels')
  .Channels,
  Data = require('./data')
  .Data,
  Item = require('./items')
  .Item,
  Player = require('./player')
  .Player,
  Skills = require('./skills')
  .Skills,
  l10nHelper = require('./l10n');

/**
 * Localization
 */
var l10n = null;
var l10nFile = __dirname + '/../l10n/events.yml';
// shortcut for l10n.translate
var L = null;

var players = null;
var npcs = null;
var rooms = null;
var items = null;

// Keep track of password attempts
var password_attempts = {};

/**
 * Helper for advancing staged events
 * @param string stage
 * @param object firstarg Override for the default arg
 */
var gen_next = function (event) {
  /**
   * Move to the next stage of a staged event
   * @param Socket|Player socket       Either a Socket or Player on which emit() will be called
   * @param string        nextstage
   * @param ...
   */
  return function (socket, nextstage) {
    var func = (socket instanceof Player ? socket.getSocket() : socket);
    func.emit.apply(func, [event].concat([].slice.call(arguments)));
  }
};

/**
 * Helper for repeating staged events
 * @param Array repeat_args
 * @return function
 */
var gen_repeat = function (repeat_args, next) {
  return function () {
    next.apply(null, [].slice.call(repeat_args))
  };
};

/**
 * Events object is a container for any "context switches" for the player.
 * Essentially anything that requires player input will have its own listener
 * if it isn't just a basic command. Complex listeners are staged.
 * See login or createPlayer for examples
 */
var Events = {
  /**
   * Container for events
   * @var object
   */
  events: {
    /**
     * Point of entry for the player. They aren't actually a player yet
     * @param Socket socket
     */
    login: function login(socket, stage, dontwelcome, name) {

      util.log("Login event detected... ", stage);

      // dontwelcome is used to swallow telnet bullshit
      dontwelcome = typeof dontwelcome == -'undefined' ? false :
        dontwelcome;
      stage = stage || 'intro';

      if (socket instanceof Player) {
        l10n.setLocale('en');
      }

      var next = gen_next('login');
      var repeat = gen_repeat(arguments, next);

      switch (stage) {

        case 'intro':
          var motd = Data.loadMotd();
          if (motd) socket.write(motd);
          next(socket, 'login');
          break;

        case 'login':
          if (!dontwelcome) {
            socket.write("Welcome, what is your name? ");
          }

          socket.once('data', function (name) {

            // swallow any data that's not from player input i.e., doesn't end with a newline
            // Windows can s@#* a d@#$
            var negot = false;
            if (name[name.length - 1] === 0x0a) {
              negot = true;
            } else if (name[name.length - 1] === 0x0d) {
              negot = true;
            }

            if (!negot) {
              next(socket, 'login', true);
              return;
            }

            var name = name.toString()
              .trim();
            if (/[^a-z]/i.test(name) || !name) {
              socket.write("That's not really your name, now is it?\r\n");
              return repeat();
            }

            name = name[0].toUpperCase() + name.toLowerCase()
              .substr(1);

            var data = Data.loadPlayer(name);

            // That player doesn't exit so ask if them to create it
            if (!data) {
              socket.emit('createPlayer', socket);
              return;
            }

            next(socket, 'password', false, name);
            return;
          });
          break;

        case 'password':
          if (typeof password_attempts[name] === 'undefined') {
            password_attempts[name] = 0;
          }

          // Boot and log any failed password attempts
          if (password_attempts[name] > 2) {
            socket.write(L('PASSWORD_EXCEEDED') + "\r\n");
            password_attempts[name] = 0;
            util.log('Failed login - exceeded password attempts - ' + name);
            socket.end();
            return false;
          }

          if (!dontwelcome) {
            socket.write(L('PASSWORD'));
          }

          socket.once('data', pass => {
            // Skip garbage
            if (pass[0] === 0xFA) {
              return next(socket, 'password', true, name);
            }

            pass = crypto
              .createHash('md5')
              .update(pass.toString('').trim())
              .digest('hex');

            if (pass !== Data.loadPlayer(name).password) {
              util.log("Failed password attempt by ", socket)
              socket.write(L('PASSWORD_FAIL') + "\r\n");
              password_attempts[name] += 1;
              return repeat();
            }
            next(socket, 'done', name);
          });
          break;

        case 'done':
          var name = dontwelcome;
          var haveSameName = p => p.getName().toLowerCase() === name.toLowerCase();
          var boot = players.some(haveSameName);

          if (boot) {
            players.eachIf(haveSameName,
              p => {
                p.say("Replaced.");
                p.emit('quit');
                util.log("Replaced: ", p.getName());
                players.removePlayer(p, true);
              });
          }

          player = new Player(socket);
          player.load(Data.loadPlayer(name));
          players.addPlayer(player);

          player.getSocket()
            .on('close', () => {
              player.setTraining('beginTraining', Date.now());

              if (!player.isInCombat()) {
                util.log(player.getName() + ' has gone linkdead.');
                player.save(() => players.removePlayer(player, true));
              } else {
                util.log(player.getName() + ' socket closed during combat!!!');
              }

              //TODO: Consider saving player here as well, and stuff.
              players.removePlayer(player);
            });

          players.broadcastL10n(l10n, 'WELCOME_BACK', player.getName());

          //TODO: Have load in player file?
          // Load the player's inventory (There's probably a better place to do this)
          var inv = [];
          player.getInventory()
            .forEach(item => {
              item = new Item(item);
              items.addItem(item);
              inv.push(item);
            });
          player.setInventory(inv);

          Commands.player_commands.look(null, player);
          player.checkTraining();

          // All that shit done, let them play!
          player.getSocket()
            .emit("commands", player);
          break;
      }
    },

    /**
     * Command loop
     * @param Player player
     */
    commands: function (player) {
      // Parse order is common direction shortcuts -> commands -> exits -> skills -> channels
      player.getSocket()
        .once('data', function (data) {
          data = data.toString().trim();

          var result;
          if (data) result = parseCommands(data);
          if (result !== false) { commandPrompt(); }

          // Methods

          function parseCommands(data) {
            var command = data
              .split(' ')
              .shift();
            var args = data
              .split(' ')
              .slice(1)
              .join(' ');

            var found = false;

            if (!(command in Commands.player_commands)) {

              found = checkForDirectionAlias(command);
              if (!found) found = checkForCmd(command);
              else if (found === true) return;

              if (found) return getCmd(found, args);
              else return checkForSpecializedCommand(command, args);

            } else return getCmd(command, args);
          }

          function getCmd(cmd, args) {
            try {
              return Commands.player_commands[cmd](args, player);
            } catch (e) { util.log(e) }
          }

          function checkForDirectionAlias(command) {
            var directions = {
              'n': 'north',
              'e': 'east',
              's': 'south',
              'w': 'west',
              'u': 'up',
              'd': 'down'
            };

            if (command.toLowerCase() in directions) {
              const exit = directions[command.toLowerCase()];
              Commands.room_exits(exit, player);
              return true;
            }
          }

          function checkForCmd(command) {
            for (var cmd in Commands.player_commands) {
              try {
                var regex = new RegExp("^" + command);
              } catch (err) {
                continue;
              }
              if (cmd.match(regex)) {
                return cmd;
              }
            }
          }

          // Handles skills, feats, exits
          function checkForSpecializedCommand(command, args) {
            var exit = Commands.room_exits(command, player);

            //TODO: Refactor as to not rely on negative conditionals as much?
            if (exit === false) {
              var isSkill = command in player.getSkills();
              var isFeat = command in player.getFeats();

              if (!isSkill && !isFeat) {
                if (!(command in Channels)) {
                  player.say(command + " is not a valid command.");
                  return true;
                } else {
                  Channels[command].use(args, player, players, rooms);
                  return true
                }
              } else {
                var use = isSkill ? player.useSkill : player.useFeat;
                use(command, player, args, rooms, npcs, players);
                return true;
              }
            }
          }

          function commandPrompt() {
            player.prompt();
            player.getSocket()
              .emit("commands", player);
          }

        });
    },

    /**
     * Create a player
     * Stages:
     *   check:  See if they actually want to create a player or not
     *   locale: Get the language they want to play in so we can give them
     *           the rest of the creation process in their language
     *   name:   ... get there name
     *   done:   This is always the end step, here we register them in with
     *           the rest of the logged in players and where they log in
     *
     * @param object arg This is either a Socket or a Player depending on
     *                  the stage.
     * @param string stage See above
     */
    createPlayer: function (socket, stage) {
      stage = stage || 'check';

      if (socket instanceof Player) {
        l10n.setLocale("en");
      }

      var next = gen_next('createPlayer');
      var repeat = gen_repeat(arguments, next);

      /* Multi-stage character creation i.e., races, classes, etc.
       * Always emit 'done' in your last stage to keep it clean
       * Also try to put the cases in order that they happen during creation
       */
      switch (stage) {
        case 'check':
          socket.write(
            "That player doesn't exist, would you like to create it? [y/n] "
          );
          socket.once('data', function (check) {
            check = check.toString()
              .trim()
              .toLowerCase();
            if (!/[yn]/.test(check)) {
              return repeat();
            }

            if (check === 'n') {
              socket.write("Goodbye!\r\n");
              socket.end();
              return false;
            }

            next(socket, 'name');
          });
          break;
        case 'name':
          socket = new Player(socket);
          socket.write(L('NAME_PROMPT'));
          socket.getSocket()
            .once('data', function (name) {
              name = name.toString()
                .trim();
              if (/\W/.test(name)) {
                socket.say(L('INVALID_NAME'));
                return repeat();
              }

              var player = false;
              players.every(function (p) {
                if (p.getName()
                  .toLowerCase() === name.toLowerCase()) {
                  player = true;
                  return false;
                }
                return true;
              });

              player = player || Data.loadPlayer(name);

              if (player) {
                socket.say(L('NAME_TAKEN'));
                return repeat();
              }

              // Always give them a name like Shawn instead of sHaWn
              socket.setName(name[0].toUpperCase() + name.toLowerCase()
                .substr(
                  1));
              next(socket, 'password');
            });
          break;
        case 'password':
          socket.write(L('PASSWORD'));
          socket.getSocket()
            .once('data', function (pass) {
              pass = pass.toString()
                .trim();
              if (!pass) {
                socket.sayL10n(l10n, 'EMPTY_PASS');
                return repeat();
              }

              // setPassword handles hashing
              socket.setPassword(pass);
              next(socket, 'gender');
            });
          break;
        case 'gender':
          socket.write(
            'What is your character\'s gender?\n[F]emale\n[M]ale\n[A]ndrogynous\n'
          );
          socket.getSocket()
            .once('data', function (gender) {
              gender = gender.toString()
                .toLowerCase();
              if (!gender) {
                socket.say(
                  'Please specify a gender, or [A]ndrogynous if you\'d prefer not to.'
                );
                return repeat();
              }
              socket.setGender(gender);
              next(socket, 'done');
            });
          break;
          // 'done' assumes the argument passed to the event is a player, ...so always do that.
        case 'done':
          socket.setLocale("en");
          socket.setLocation(players.getDefaultLocation());

          // create the pfile then send them on their way
          socket.save(() => {
            players.addPlayer(socket);
            socket.prompt();
            socket.getSocket()
              .emit('commands', socket);
          });

          util.log("A NEW CHALLENGER APPROACHES: ", socket);
          players.broadcastL10n(l10n, 'WELCOME', socket.getName());
          break;

      }
    }
  },

  configure: function (config) {
    players = players || config.players;
    items = items || config.items;
    rooms = rooms || config.rooms;
    npcs = npcs || config.npcs;
    if (!l10n) {
      util.log("Loading event l10n... ");
      l10n = l10nHelper(l10nFile);
      util.log("Done");
    }
    l10n.setLocale(config.locale);

    /**
     * Hijack translate to also do coloring
     * @param string text
     * @param ...
     * @return string
     */
    L = function (text) {
      return ansi(l10n.translate.apply(null, [].slice.call(arguments)));
    };
  }
};
exports.Events = Events;
<|endoftext|>


<commit_before>var ModeHandler = require('./lib/mode_handler');
var getFeatureAtAndSetCursors = require('./lib/get_features_and_set_cursor');
var isClick = require('./lib/is_click');

var modes = {
  'simple_select': require('./modes/simple_select'),
  'direct_select': require('./modes/direct_select'),
  'draw_point': require('./modes/draw_point'),
  'draw_line_string': require('./modes/draw_line_string'),
  'draw_polygon': require('./modes/draw_polygon')
};

module.exports = function(ctx) {

  var mouseDownInfo = {
    isDown: false
  };

  var events = {};
  var currentModeName = 'simple_select';
  var currentMode = ModeHandler(modes.simple_select(ctx), ctx);
  var featuresChanged = [];

  events.drag = function(event) {
    if (isClick(mouseDownInfo, {
      point: event.point,
      time: new Date().getTime()
    })) {
      event.originalEvent.stopPropagation();
    }
    else {
      ctx.ui.setClass({mouse: 'drag'});
      currentMode.drag(event);
    }
  };

  events.mousemove = function(event) {
    if (mouseDownInfo.isDown) {
      events.drag(event);
    }
    else {
      var target = getFeatureAtAndSetCursors(event, ctx);
      event.featureTarget = target;
      currentMode.mousemove(event);
    }
  };

  events.mousedown = function(event) {
    mouseDownInfo = {
      isDown: true,
      time: new Date().getTime(),
      point: event.point
    };
    var target = getFeatureAtAndSetCursors(event, ctx);
    event.featureTarget = target;
    currentMode.mousedown(event);
  };

  events.drawChanged = function(event) {
    featuresChanged = event.features;
  }

  events.mouseup = function(event) {
    mouseDownInfo.isDown = false;
    var target = getFeatureAtAndSetCursors(event, ctx);
    event.featureTarget = target;

    if (featuresChanged.length > 0) ctx.map.fire('draw.modified', {features: featuresChanged});
    featuresChanged = [];

    if (isClick(mouseDownInfo, {
      point: event.point,
      time: new Date().getTime()
    })) {
      currentMode.click(event);
    }
    else {
      currentMode.mouseup(event);
    }
  };

  events.trash = function() {
    currentMode.trash();
  };

  var isKeyModeValid = (code) => !(code === 8 || (code >= 48 && code <= 57));

  events.keydown = function(event) {
    if (event.keyCode === 8 && ctx.options.controls.trash) {
      event.preventDefault();
      api.fire('trash');
    }
    else if (isKeyModeValid(event.keyCode)) {
      currentMode.keydown(event);
    }
    else if (event.keyCode === 49 && ctx.options.controls.point) {
      ctx.api.changeMode('draw_point');
    }
    else if (event.keyCode === 50 && ctx.options.controls.line_string) {
      ctx.api.changeMode('draw_line_string');
    }
    else if (event.keyCode === 51 && ctx.options.controls.polygon) {
      ctx.api.changeMode('draw_polygon');
    }
  };

  events.keyup = function(event) {
    if (isKeyModeValid(event.keyCode)) {
      currentMode.keyup(event);
    }
  };

  events.zoomend = function() {
    ctx.store.changeZoom();
  };

  var api = {
    currentModeName: function() {
      return currentModeName;
    },
    currentModeRender: function(geojson, push) {
      return currentMode.render(geojson, push);
    },
    changeMode: function(modename, opts) {
      currentMode.stop();
      var modebuilder = modes[modename];
      if (modebuilder === undefined) {
        throw new Error(`${modename} is not valid`);
      }
      currentModeName = modename;
      var mode = modebuilder(ctx, opts);
      currentMode = ModeHandler(mode, ctx);

      ctx.map.fire('draw.modechange', {
        mode: modename,
        opts: opts
      });

      ctx.store.setDirty();
      ctx.store.render();
    },
    fire: function(name, event) {
      if (events[name]) {
        events[name](event);
      }
    },
    addEventListeners: function() {
      ctx.map.on('mousemove', events.mousemove);

      ctx.map.on('mousedown', events.mousedown);
      ctx.map.on('mouseup', events.mouseup);

      ctx.map.on('draw.changed', events.drawChanged);

      if (ctx.options.keybindings) {
        ctx.container.addEventListener('keydown', events.keydown);
        ctx.container.addEventListener('keyup', events.keyup);
      }
    },
    removeEventListeners: function() {
      ctx.map.off('mousemove', events.mousemove);

      ctx.map.off('mousedown', events.mousedown);
      ctx.map.off('mouseup', events.mouseup);

      ctx.map.off('draw.changed', events.drawChanged);

      if (ctx.options.keybindings) {
        ctx.container.removeEventListener('keydown', events.keydown);
        ctx.container.removeEventListener('keyup', events.keyup);
      }
    }
  };

  return api;
};
<commit_msg>fixed lint error<commit_after>var ModeHandler = require('./lib/mode_handler');
var getFeatureAtAndSetCursors = require('./lib/get_features_and_set_cursor');
var isClick = require('./lib/is_click');

var modes = {
  'simple_select': require('./modes/simple_select'),
  'direct_select': require('./modes/direct_select'),
  'draw_point': require('./modes/draw_point'),
  'draw_line_string': require('./modes/draw_line_string'),
  'draw_polygon': require('./modes/draw_polygon')
};

module.exports = function(ctx) {

  var mouseDownInfo = {
    isDown: false
  };

  var events = {};
  var currentModeName = 'simple_select';
  var currentMode = ModeHandler(modes.simple_select(ctx), ctx);
  var featuresChanged = [];

  events.drag = function(event) {
    if (isClick(mouseDownInfo, {
      point: event.point,
      time: new Date().getTime()
    })) {
      event.originalEvent.stopPropagation();
    }
    else {
      ctx.ui.setClass({mouse: 'drag'});
      currentMode.drag(event);
    }
  };

  events.mousemove = function(event) {
    if (mouseDownInfo.isDown) {
      events.drag(event);
    }
    else {
      var target = getFeatureAtAndSetCursors(event, ctx);
      event.featureTarget = target;
      currentMode.mousemove(event);
    }
  };

  events.drawChanged = function(event) {
    featuresChanged = event.features;
  };

  events.mousedown = function(event) {
    mouseDownInfo = {
      isDown: true,
      time: new Date().getTime(),
      point: event.point
    };
    var target = getFeatureAtAndSetCursors(event, ctx);
    event.featureTarget = target;
    currentMode.mousedown(event);
  };

  events.mouseup = function(event) {
    mouseDownInfo.isDown = false;
    var target = getFeatureAtAndSetCursors(event, ctx);
    event.featureTarget = target;

    if (featuresChanged.length > 0) ctx.map.fire('draw.modified', {features: featuresChanged});
    featuresChanged = [];

    if (isClick(mouseDownInfo, {
      point: event.point,
      time: new Date().getTime()
    })) {
      currentMode.click(event);
    }
    else {
      currentMode.mouseup(event);
    }
  };

  events.trash = function() {
    currentMode.trash();
  };

  var isKeyModeValid = (code) => !(code === 8 || (code >= 48 && code <= 57));

  events.keydown = function(event) {
    if (event.keyCode === 8 && ctx.options.controls.trash) {
      event.preventDefault();
      api.fire('trash');
    }
    else if (isKeyModeValid(event.keyCode)) {
      currentMode.keydown(event);
    }
    else if (event.keyCode === 49 && ctx.options.controls.point) {
      ctx.api.changeMode('draw_point');
    }
    else if (event.keyCode === 50 && ctx.options.controls.line_string) {
      ctx.api.changeMode('draw_line_string');
    }
    else if (event.keyCode === 51 && ctx.options.controls.polygon) {
      ctx.api.changeMode('draw_polygon');
    }
  };

  events.keyup = function(event) {
    if (isKeyModeValid(event.keyCode)) {
      currentMode.keyup(event);
    }
  };

  events.zoomend = function() {
    ctx.store.changeZoom();
  };

  var api = {
    currentModeName: function() {
      return currentModeName;
    },
    currentModeRender: function(geojson, push) {
      return currentMode.render(geojson, push);
    },
    changeMode: function(modename, opts) {
      currentMode.stop();
      var modebuilder = modes[modename];
      if (modebuilder === undefined) {
        throw new Error(`${modename} is not valid`);
      }
      currentModeName = modename;
      var mode = modebuilder(ctx, opts);
      currentMode = ModeHandler(mode, ctx);

      ctx.map.fire('draw.modechange', {
        mode: modename,
        opts: opts
      });

      ctx.store.setDirty();
      ctx.store.render();
    },
    fire: function(name, event) {
      if (events[name]) {
        events[name](event);
      }
    },
    addEventListeners: function() {
      ctx.map.on('mousemove', events.mousemove);

      ctx.map.on('mousedown', events.mousedown);
      ctx.map.on('mouseup', events.mouseup);

      ctx.map.on('draw.changed', events.drawChanged);

      if (ctx.options.keybindings) {
        ctx.container.addEventListener('keydown', events.keydown);
        ctx.container.addEventListener('keyup', events.keyup);
      }
    },
    removeEventListeners: function() {
      ctx.map.off('mousemove', events.mousemove);

      ctx.map.off('mousedown', events.mousedown);
      ctx.map.off('mouseup', events.mouseup);

      ctx.map.off('draw.changed', events.drawChanged);

      if (ctx.options.keybindings) {
        ctx.container.removeEventListener('keydown', events.keydown);
        ctx.container.removeEventListener('keyup', events.keyup);
      }
    }
  };

  return api;
};
<|endoftext|>


<commit_before>/**
 * Copyright 2013-2020 the original author or authors from the JHipster project.
 *
 * This file is part of the JHipster project, see https://www.jhipster.tech/
 * for more information.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * The default is to use a file path string. It implies use of the template method.
 * For any other config an object { file:.., method:.., template:.. } can be used
 */
const constants = require('../generator-constants');

const faker = require('faker');

const { stringHashCode } = require('generator-jhipster/generators/utils');

const TEST_SRC_DIR = constants.CLIENT_TEST_SRC_DIR;

const cypressFiles = {
    common: [
        {
            condition: generator => generator.cypressTests,
            templates: ['cypress.json'],
        },
    ],
    clientTestFw: [
        {
            condition: generator => generator.cypressTests,
            path: TEST_SRC_DIR,
            templates: [
                'cypress/fixtures/users/user.json',
                'cypress/fixtures/integration-test.png',
                'cypress/plugins/index.ts',
                'cypress/integration/administration/administration.spec.ts',
                'cypress/support/commands.ts',
                'cypress/support/navbar.ts',
                'cypress/support/index.ts',
                'cypress/support/entity.ts',
                'cypress/tsconfig.json',
            ],
        },
        {
            condition: generator => generator.cypressTests && generator.authenticationType !== 'oauth2',
            path: TEST_SRC_DIR,
            templates: ['cypress/integration/account/login-page.spec.ts'],
        },
        {
            condition: generator => generator.cypressTests && generator.authenticationType !== 'oauth2' && generator.databaseType !== 'no',
            path: TEST_SRC_DIR,
            templates: [
                'cypress/integration/account/register-page.spec.ts',
                'cypress/integration/account/settings-page.spec.ts',
                'cypress/integration/account/password-page.spec.ts',
                'cypress/integration/account/reset-password-page.spec.ts',
            ],
        },
        {
            condition: generator => generator.cypressTests && generator.authenticationType === 'oauth2',
            path: TEST_SRC_DIR,
            templates: ['cypress/support/keycloak-oauth2.ts', 'cypress/support/utils.ts'],
        },
    ],
};
module.exports = {
    writeFiles,
};

function writeFiles() {
    return {
        writeFiles() {
            faker.seed(stringHashCode(this.jhipsterConfig.baseName));
            this.faker = faker;
            this.writeFilesToDisk(cypressFiles, this, false, this.fetchFromInstalledJHipster('cypress/templates'));
        },
    };
}
<commit_msg>Fix imports in files.js<commit_after>/**
 * Copyright 2013-2020 the original author or authors from the JHipster project.
 *
 * This file is part of the JHipster project, see https://www.jhipster.tech/
 * for more information.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * The default is to use a file path string. It implies use of the template method.
 * For any other config an object { file:.., method:.., template:.. } can be used
 */

const faker = require('faker');

const constants = require('../generator-constants');

const { stringHashCode } = require('../utils');

const TEST_SRC_DIR = constants.CLIENT_TEST_SRC_DIR;

const cypressFiles = {
    common: [
        {
            condition: generator => generator.cypressTests,
            templates: ['cypress.json'],
        },
    ],
    clientTestFw: [
        {
            condition: generator => generator.cypressTests,
            path: TEST_SRC_DIR,
            templates: [
                'cypress/fixtures/users/user.json',
                'cypress/fixtures/integration-test.png',
                'cypress/plugins/index.ts',
                'cypress/integration/administration/administration.spec.ts',
                'cypress/support/commands.ts',
                'cypress/support/navbar.ts',
                'cypress/support/index.ts',
                'cypress/support/entity.ts',
                'cypress/tsconfig.json',
            ],
        },
        {
            condition: generator => generator.cypressTests && generator.authenticationType !== 'oauth2',
            path: TEST_SRC_DIR,
            templates: ['cypress/integration/account/login-page.spec.ts'],
        },
        {
            condition: generator => generator.cypressTests && generator.authenticationType !== 'oauth2' && generator.databaseType !== 'no',
            path: TEST_SRC_DIR,
            templates: [
                'cypress/integration/account/register-page.spec.ts',
                'cypress/integration/account/settings-page.spec.ts',
                'cypress/integration/account/password-page.spec.ts',
                'cypress/integration/account/reset-password-page.spec.ts',
            ],
        },
        {
            condition: generator => generator.cypressTests && generator.authenticationType === 'oauth2',
            path: TEST_SRC_DIR,
            templates: ['cypress/support/keycloak-oauth2.ts', 'cypress/support/utils.ts'],
        },
    ],
};
module.exports = {
    writeFiles,
};

function writeFiles() {
    return {
        writeFiles() {
            faker.seed(stringHashCode(this.jhipsterConfig.baseName));
            this.faker = faker;
            this.writeFilesToDisk(cypressFiles, this, false, this.fetchFromInstalledJHipster('cypress/templates'));
        },
    };
}
<|endoftext|>


<commit_before>

var customLaunchers = {
    sl_chrome: {
        base: 'SauceLabs',
        browserName: 'chrome',
        platform: 'Windows 7',
        version: '35'
    },
    sl_firefox: {
        base: 'SauceLabs',
        browserName: 'firefox',
        version: '30'
    },
    sl_ios_safari: {
        base: 'SauceLabs',
        browserName: 'iphone',
        platform: 'OS X 10.9',
        version: '7.1'
    },
    sl_ie_11: {
        base: 'SauceLabs',
        browserName: 'internet explorer',
        platform: 'Windows 8.1',
        version: '11'
    }
}

module.exports = function (config) {
    var cfg = {
        basePath: '../',
        frameworks: ['jasmine'],
        files: [
            'src/vendors.ts',
            {pattern: 'src/**/*.spec.ts', watched: false}
        ],
        exclude: [],
        preprocessors: {
            //      'dom-adapter-config.js': ['webpack'],
            'src/vendors.ts': ['webpack', 'sourcemap'],
            'src/**/*.spec.ts': ['webpack', 'sourcemap']
        },
        webpack: require("./webpack.test.conf.js"),
        coverageReporter: {
            dir: 'coverage',
            reporters: [
                {
                    type: 'json',
                    subdir: '.',
                    file: 'coverage.json'
                }
            ]
        },
        webpackServer: {
            noInfo: true //please don't spam the console when running in karma!
        },
        // possible values: 'dots', 'progress'
        reporters: ['mocha', 'coverage'],
        port: 9876,
        colors: true,

        // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
        logLevel: config.LOG_INFO,

        // enable / disable watching file and executing tests whenever any file changes
        autoWatch: true,

        // start these browsers
        // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
        // browsers: ['Firefox'],

        captureTimeout: 60000,
        browserDisconnectTimeout : 10000,
        browserDisconnectTolerance : 1,
        browserNoActivityTimeout : 60000,
        // Continuous Integration mode
        // if true, Karma captures browsers, runs the tests and exits
        singleRun: true,

    };
    if(process.env.SAUCE_ACCESS_KEY) {
        // Use saucelabs for CI tests
        cfg.reporters.push("saucelabs");
        cfg.sauceLabs = {
            testName: "Ng2Datastore unit tests",
            startConnect: true,
            build: process.env.CI_BUILD_ID || "manual"
        };
        cfg.customLaunchers = customLaunchers;
        cfg.browsers = Object.keys(customLaunchers);
    } else {
        // Use PhantomJS for local tests
        cfg.browsers = ["PhantomJS"]
    }

    config.set(cfg);

};<commit_msg>chore(karma): Add some time to timeouts to let saucelabs boot machines<commit_after>

var customLaunchers = {
    sl_chrome: {
        base: 'SauceLabs',
        browserName: 'chrome',
        platform: 'Windows 7',
        version: '35'
    },
    sl_firefox: {
        base: 'SauceLabs',
        browserName: 'firefox',
        version: '30'
    },
    sl_ios_safari: {
        base: 'SauceLabs',
        browserName: 'iphone',
        platform: 'OS X 10.9',
        version: '7.1'
    },
    sl_ie_11: {
        base: 'SauceLabs',
        browserName: 'internet explorer',
        platform: 'Windows 8.1',
        version: '11'
    }
}

module.exports = function (config) {
    var cfg = {
        basePath: '../',
        frameworks: ['jasmine'],
        files: [
            'src/vendors.ts',
            {pattern: 'src/**/*.spec.ts', watched: false}
        ],
        exclude: [],
        preprocessors: {
            //      'dom-adapter-config.js': ['webpack'],
            'src/vendors.ts': ['webpack', 'sourcemap'],
            'src/**/*.spec.ts': ['webpack', 'sourcemap']
        },
        webpack: require("./webpack.test.conf.js"),
        coverageReporter: {
            dir: 'coverage',
            reporters: [
                {
                    type: 'json',
                    subdir: '.',
                    file: 'coverage.json'
                }
            ]
        },
        webpackServer: {
            noInfo: true //please don't spam the console when running in karma!
        },
        // possible values: 'dots', 'progress'
        reporters: ['mocha', 'coverage'],
        port: 9876,
        colors: true,

        // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
        logLevel: config.LOG_INFO,

        // enable / disable watching file and executing tests whenever any file changes
        autoWatch: true,

        // start these browsers
        // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
        // browsers: ['Firefox'],

        captureTimeout: 120000,
        browserDisconnectTimeout : 10000,
        browserDisconnectTolerance : 1,
        browserNoActivityTimeout : 120000,
        // Continuous Integration mode
        // if true, Karma captures browsers, runs the tests and exits
        singleRun: true,

    };
    if(process.env.SAUCE_ACCESS_KEY) {
        // Use saucelabs for CI tests
        cfg.reporters.push("saucelabs");
        cfg.sauceLabs = {
            testName: "Ng2Datastore unit tests",
            startConnect: true,
            build: process.env.CI_BUILD_ID || "manual"
        };
        cfg.customLaunchers = customLaunchers;
        cfg.browsers = Object.keys(customLaunchers);
    } else {
        // Use PhantomJS for local tests
        cfg.browsers = ["PhantomJS"]
    }

    config.set(cfg);

};<|endoftext|>


<commit_before>'use strict';

var path = require('path'),
    format = require('util').format,
    agenda = require(path.resolve('./config/lib/agenda'));

exports.start = function(options, callback) {

  agenda.on('ready', function() {

    // Define jobs

    agenda.define(
      'send email',
      { priority: 'high', concurrency: 10 },
      require(path.resolve('./modules/core/server/jobs/send-email.server.job'))
    );

    agenda.define(
      'check unread messages',
      { lockLifetime: 10000 },
      require(path.resolve('./modules/messages/server/jobs/message-unread.server.job'))
    );

    agenda.define(
      'daily statistics',
      { lockLifetime: 10000 },
      require(path.resolve('./modules/statistics/server/jobs/daily-statistics.server.job'))
    );
    agenda.define(
      'send signup reminders',
      { lockLifetime: 10000, concurrency: 10 },
      require(path.resolve('./modules/users/server/jobs/user-finish-signup.server.job'))
    );

    // Schedule job(s)

    agenda.every('5 minutes', 'check unread messages');
    agenda.every('24 hours', 'daily statistics');

    agenda.every('30 minutes', 'send signup reminders');

    // Start worker

    agenda.start();
    if (process.env.NODE_ENV !== 'test') {
      console.log('Agenda started processing background jobs');
    }

    if (callback) callback();

  });

  // Error reporting and retry logic
  agenda.on('fail', function(err, job) {

    var extraMessage = '';

    if (job.attrs.failCount >= options.maxAttempts) {

      extraMessage = format('too many failures, giving up');

    } else if (shouldRetry(err)) {

      job.attrs.nextRunAt = secondsFromNowDate(options.retryDelaySeconds);

      extraMessage = format('will retry in %s seconds at %s',
        options.retryDelaySeconds, job.attrs.nextRunAt.toISOString());

      job.save();
    }

    if (process.env.NODE_ENV !== 'test') {
      console.error('Agenda job [%s] %s failed with [%s] %s failCount:%s',
        job.attrs.name, job.attrs._id, err.message || 'Unknown error', extraMessage, job.attrs.failCount);
    }

  });

};

function shouldRetry(err) {

  // Retry on connection errors as they may just be temporary
  if (/(ECONNRESET|ECONNREFUSED)/.test(err.message)) {
    return true;
  }
  return false;
}

function secondsFromNowDate(seconds) {
  return new Date(new Date().getTime() + (seconds * 1000));
}
<commit_msg>Clean out worker.js spacing and change signup reminder concurrency to 1<commit_after>'use strict';

var path = require('path'),
    format = require('util').format,
    agenda = require(path.resolve('./config/lib/agenda'));

exports.start = function(options, callback) {

  agenda.on('ready', function() {

    // Define jobs

    agenda.define(
      'send email',
      { priority: 'high', concurrency: 10 },
      require(path.resolve('./modules/core/server/jobs/send-email.server.job'))
    );

    agenda.define(
      'check unread messages',
      { lockLifetime: 10000 },
      require(path.resolve('./modules/messages/server/jobs/message-unread.server.job'))
    );

    agenda.define(
      'daily statistics',
      { lockLifetime: 10000, concurrency: 1 },
      require(path.resolve('./modules/statistics/server/jobs/daily-statistics.server.job'))
    );

    agenda.define(
      'send signup reminders',
      { lockLifetime: 10000, concurrency: 1 },
      require(path.resolve('./modules/users/server/jobs/user-finish-signup.server.job'))
    );

    // Schedule job(s)

    agenda.every('5 minutes', 'check unread messages');
    agenda.every('24 hours', 'daily statistics');
    agenda.every('30 minutes', 'send signup reminders');

    // Start worker

    agenda.start();
    if (process.env.NODE_ENV !== 'test') {
      console.log('Agenda started processing background jobs');
    }

    if (callback) callback();

  });

  // Error reporting and retry logic
  agenda.on('fail', function(err, job) {

    var extraMessage = '';

    if (job.attrs.failCount >= options.maxAttempts) {

      extraMessage = format('too many failures, giving up');

    } else if (shouldRetry(err)) {

      job.attrs.nextRunAt = secondsFromNowDate(options.retryDelaySeconds);

      extraMessage = format('will retry in %s seconds at %s',
        options.retryDelaySeconds, job.attrs.nextRunAt.toISOString());

      job.save();
    }

    if (process.env.NODE_ENV !== 'test') {
      console.error('Agenda job [%s] %s failed with [%s] %s failCount:%s',
        job.attrs.name, job.attrs._id, err.message || 'Unknown error', extraMessage, job.attrs.failCount);
    }

  });

};

function shouldRetry(err) {

  // Retry on connection errors as they may just be temporary
  if (/(ECONNRESET|ECONNREFUSED)/.test(err.message)) {
    return true;
  }
  return false;
}

function secondsFromNowDate(seconds) {
  return new Date(new Date().getTime() + (seconds * 1000));
}
<|endoftext|>


<commit_before>import { later } from '@ember/runloop'
import { module, test } from 'qunit'
import { setupTest } from 'ember-qunit'
import { pilasMock, interpreterFactoryMock, interpreteMock, actividadMock, blocklyWorkspaceMock, componentMock, activityExpectationsMock } from '../../helpers/mocks'
import { findBlockByTypeIn, assertProps, assertWarning, assertNotWarning, assertHasProps, setUpTestLocale } from '../../helpers/utils'
import { declaresAnyProcedure } from '../../../utils/expectations'
import sinon from 'sinon'

module('Unit | Components | pilas-blockly', function (hooks) {
  setupTest(hooks)
  setUpTestLocale(hooks)

  hooks.beforeEach(function () {
    this.owner.register('service:interpreterFactory', interpreterFactoryMock)
    this.owner.register('service:activityExpectations', activityExpectationsMock)
    this.owner.lookup('service:highlighter').workspace = blocklyWorkspaceMock()
    this.owner.lookup('service:blocksGallery').start()

    this.ctrl = this.owner.factoryFor('component:pilas-blockly').create()
    this.ctrl.pilasService = pilasMock //TODO: Injectar como service
    this.ctrl.set('modelActividad', actividadMock)
    this.ctrl.set('exerciseWorkspace', componentMock)
    this.ctrl.set('pilasBloquesApi', sinon.stub(this.ctrl.pilasBloquesApi))
    this.ctrl.set('debeMostrarFinDeDesafio', true)
    sinon.resetHistory()
  })

  //TODO: Ver de agrupar en modules
  test('Al ejecutar se encuentra ejecutando y ejecuta el intérprete', async function (assert) {
    await this.ctrl.send('ejecutar')

    assert.ok(this.ctrl.get('ejecutando'))
    assert.notOk(this.ctrl.get('pausadoEnBreakpoint'))
    assert.ok(interpreteMock.run.called)
  })

  test('Ejecutar paso a paso bloquea la ejecución', async function (assert) {
    await this.ctrl.send('ejecutar', true)

    later(() => {
      assert.ok(interpreteMock.run.calledOnce)
      assert.ok(this.ctrl.get('pausadoEnBreakpoint'))
    })

  })

  test('Step desbloquea el breakpoint', async function (assert) {
    await this.ctrl.send('ejecutar', true)

    later(() => {
      assert.ok(this.ctrl.get('pausadoEnBreakpoint'))
      this.ctrl.send('step')
      assert.notOk(this.ctrl.get('pausadoEnBreakpoint'))
    })

  })

  test('Luego de ejecutar termina de ejecutar', async function (assert) {
    await this.ctrl.send('ejecutar')

    later(() => {
      assert.notOk(this.ctrl.get('ejecutando'))
      assert.ok(this.ctrl.get('terminoDeEjecutar'))
    })

  })

  test('Al resolver el problema muestra el fin del desafío', async function (assert) {
    await this.ctrl.send('ejecutar')
    later(() => {
      assert.ok(this.ctrl.get('mostrarDialogoFinDesafio'))
    })
  })

  test('Al resolver el problema con expectativas fallidas', function (assert) {
    Blockly.textToBlock(filledProgram)
    this.owner.lookup('service:activityExpectations').expectations = declaresAnyProcedure
    this.ctrl.send('ejecutar')
    later(() => {
      assert.notOk(this.ctrl.get('allExpectsPassed'))
    })
  })

  test('Al resolver el problema sin expectativas fallidas', async function (assert) {
    await this.ctrl.send('ejecutar')
    later(() => {
      assert.ok(this.ctrl.get('allExpectsPassed'))
    })
  })

  test('Al reiniciar settea flags y reinicia la escena de pilas', async function (assert) {
    await this.ctrl.send('reiniciar')
    assert.notOk(this.ctrl.get('ejecutando'))
    assert.notOk(this.ctrl.get('terminoDeEjecutar'))
    assert.notOk(this.ctrl.get('errorDeActividad'))
    assert.ok(pilasMock.restartScene.called)
  })

  // PROGRAM EXECUTION
  let filledProgram =
    `<block type="al_empezar_a_ejecutar">
      <statement name="program">
        <block type="MoverACasillaDerecha">
        </block>
      </statement>
    </block>`

  let nonFilledProgram =
    `<block type="al_empezar_a_ejecutar">
      <statement name="program">
        <block type="repetir">
          <value name="count">
            <block type="math_number">
              <field name="NUM">10</field>
            </block>
          </value>
        </block>
      </statement>
    </block>`

  test('Ejecuta cuando todos los bloques están completos', async function (assert) {
    Blockly.textToBlock(filledProgram)
    await this.ctrl.send('ejecutar')
    assert.ok(interpreteMock.run.called)
  })

  test('No ejecuta cuando el programa tiene algún agujero', async function (assert) {
    Blockly.textToBlock(nonFilledProgram)
    await this.ctrl.send('ejecutar')
    assert.notOk(interpreteMock.run.called)
  })

  test('Ejecuta cuando existe algún bloque con agujeros pero no se usa', async function (assert) {
    let bloqueSuelto = `
    <block type="repetir" disabled="true">
      <value name="count">
        <block type="math_number">
          <field name="NUM">10</field>
        </block>
      </value>
    </block>
    `

    Blockly.textToBlock(filledProgram)
    Blockly.textToBlock(bloqueSuelto)
    await this.ctrl.send('ejecutar')
    assert.ok(interpreteMock.run.called)
  })

  let emptyProcedure =
    `<block type="procedures_defnoreturn">
    <field name="NAME">Hacer algo</field>
  </block>`

  let emptyProcedureWithParam =
    `<block type="procedures_defnoreturn">
    <mutation>
      <arg name="parámetro 1"></arg>
    </mutation>
    <field name="NAME">Hacer algo</field>
    <field name="ARG0">parámetro 1</field>
    <statement name="STACK">
      <shadow type="required_statement"></shadow>
    </statement>
  </block>`

  let nonFilledProcedure =
    `<block type="procedures_defnoreturn">
      <field name="NAME">Hacer algo</field>
      <statement name="STACK">
        <block type="GirarGrados">
          <value name="grados">
            <shadow type="required_value"></shadow>
          </value>
        </block>
      </statement>
   </block>`

  test('Ejecuta aún cuando existe procedimiento vacío', async function (assert) {
    Blockly.textToBlock(emptyProcedure)
    await this.ctrl.send('ejecutar')
    assert.ok(interpreteMock.run.called)
  })

  test('Ejecuta aún cuando existe procedimiento vacío con parámetros', async function (assert) {
    Blockly.textToBlock(emptyProcedureWithParam)
    await this.ctrl.send('ejecutar')
    assert.ok(interpreteMock.run.called)
  })

  test('No ejecuta cuando existe procedimiento con algún agujero', async function (assert) {
    Blockly.textToBlock(nonFilledProcedure)
    await this.ctrl.send('ejecutar')
    assert.notOk(interpreteMock.run.called)
  })

  test('Al ejecutar aparecen los warnings de bloques vacíos', async function (assert) {
    let program = Blockly.textToBlock(nonFilledProgram)
    let required = findBlockByTypeIn(program, "required_statement")
    assertNotWarning(assert, required)
    await this.ctrl.send('ejecutar')
    later(() => assertWarning(assert, required, "¡Acá faltan bloques comandos!"))
  })

  // API
  test('Avisa a la api al ejecutar', async function (assert) {
    await this.ctrl.send('ejecutar')
    const staticAnalysis = this.ctrl.pilasBloquesApi.runProgram.lastCall.lastArg.staticAnalysis
    assertProps(assert, staticAnalysis, { couldExecute: true })
  })

  test('Envia metadata a la api al ejecutar', async function (assert) {
    Blockly.textToBlock(filledProgram)
    this.ctrl.send('onChangeWorkspace', filledProgram) // Fire property change :(
    await this.ctrl.send('ejecutar')
    const metadata = this.ctrl.pilasBloquesApi.runProgram.lastCall.lastArg
    assertHasProps(assert, metadata, 'ast', 'staticAnalysis', 'turboModeOn', 'program')
    assert.deepEqual(metadata.staticAnalysis, {
      couldExecute: true,
      expects: [],
    })
  })

  test('Avisa a la api al finalizar la ejecucion', async function (assert) {
    await this.ctrl.send('ejecutar')
    later(() => {
      assertProps(assert, this.ctrl.pilasBloquesApi.executionFinished.lastCall.lastArg, { finished: true })
    })
  })

  test('Avisa a la api al finalizar la ejecucion con error', async function (assert) {
    this.ctrl.errorDeActividad = "ERROR"
    await this.ctrl.send('ejecutar')
    later(() => {
      assertProps(assert, this.ctrl.pilasBloquesApi.executionFinished.lastCall.lastArg, { error: "ERROR" })
    })
  })

  test('Envia metadata a la api al ejecutar', async function (assert) {
    await this.ctrl.send('ejecutar')
    const metadata = this.ctrl.pilasBloquesApi.runProgram.lastCall.lastArg
    assertHasProps(assert, metadata, 'ast', 'staticAnalysis', 'turboModeOn',)
    assert.ok(metadata.program || metadata.program.length === 0)
  })
})

module('Unit | Components | pilas-blockly | ToolboxForBlockTypes', function(hooks) {
  setupTest(hooks)

  const operator = { categoryId: 'operators' }
  const primitive = { categoryId: 'primitives' }

  const separator = { categoryId: 'separator', isSeparator: true }
  const blockId = "A block id"

  const doThis = 'Do this'
  const doThat = 'Do that'

  const myProcedures = { categoryId: 'procedures', blocks: [doThis, doThat] }

  const toolbox = [blockId, myProcedures, separator]

  const emberBlocklySeparator = {
    isSeparator: true
  }

  hooks.beforeEach(function () {
    this.owner.lookup('service:blocksGallery').start()
    this.ctrl = this.owner.factoryFor('component:pilas-blockly').create()
    this.ctrl.set('modelActividad', actividadMock)
    this.ctrl.modelActividad.set('estiloToolbox', 'conCategorias')
    this.ctrl.get('intl').setLocale('en-us') // This is necessary because categories are tied to locale and translation.
  })

  function setCategoriesNotRequired(ctrl) {
    ctrl.modelActividad.set('estiloToolbox', 'sinCategorias')
  }

  test('ToolboxForBlockTypes should fail if blockTypes is undefined', function (assert) {
    assert.throws(() => this.ctrl.toolboxForBlockTypes(undefined))
  })

  test('Toolbox should be sorted based on Pilas Bloques stablished order', function (assert) {
    const alternative = { categoryId: 'alternatives' }
    assert.propEqual(this.ctrl.sortedToolbox([operator, primitive, alternative]), [primitive, alternative, operator])
  })

  test('Uncategorized category should be the last item in the toolbox after sorting', function (assert) {
    const uncategorized = { categoryId: 'uncategorized' }
    assert.propEqual(this.ctrl.sortedToolbox([operator, uncategorized, primitive]), [primitive, operator, uncategorized])
  })

  test('If categories are required in toolbox, it should stay unchanged', function (assert) {
    assert.propEqual(this.ctrl._styledToolbox(toolbox), toolbox)
  })

  test('If categories are not required in toolbox, it should be flattened', function (assert) {
    setCategoriesNotRequired(this.ctrl)
    assert.propEqual(this.ctrl._styledToolbox(toolbox), [blockId, doThis, doThat,separator])
  })

  test('When styling, separators should be left unchanged', function (assert) {
    setCategoriesNotRequired(this.ctrl)
    assert.propEqual(this.ctrl._styledToolbox([separator]), [separator])
  })

  test('When styling, blocks ids should be left unchanged', function (assert) {
    setCategoriesNotRequired(this.ctrl)
    assert.propEqual(this.ctrl._styledToolbox([blockId]), [blockId])
  })

  test('ToolboxForBlockTypes should add one separator', function (assert) {
    assert.equal(
      this.ctrl.toolboxForBlockTypes(['Saludar'])
      .filter(block => block.isSeparator)
      .length,
      1
    )
  })

  test('ToolboxForBlockTypes should add a separator between commands and expressions', function (assert) {
    const moveRight = 'MoverACasillaDerecha'
    const sensor = 'HayObstaculoDerecha'
    const emberBlocklyCommands = {
      category: 'Primitives',
      blocks: [moveRight]
    }
    const emberBlocklySensors = {
      category: 'Sensors',
      blocks: [sensor]
    }
    const emberToolbox = [emberBlocklyCommands, emberBlocklySeparator, emberBlocklySensors]
    assert.propEqual(this.ctrl.toolboxForBlockTypes([sensor, moveRight]), emberToolbox)
  })

  // WARNING: categories are tied to translation.
  test('If a blocktype is not a valid blockly block, is should be uncategorized', function (assert) {
    const nonExistentBlockTypeName = 'NonExistentBlockType'
    const nonExistentEmberBlockly = {
      category: 'Uncategorized',
      blocks: [nonExistentBlockTypeName]
    }
    assert.propEqual(this.ctrl.toolboxForBlockTypes([nonExistentBlockTypeName]), [emberBlocklySeparator, nonExistentEmberBlockly])
  })

  test('ToolboxForBlockTypes should group block types by their category', function (assert) {
    const moveRight = 'MoverACasillaDerecha'
    const moveLeft = 'MoverACasillaIzquierda'
    const repeat = 'Repetir'
    const emberBlocklyPrimitives = {
      category: 'Primitives',
      blocks: [moveRight, moveLeft]
    }
    const emberBlocklyRepetitions = {
      category: 'Repetition',
      blocks: [repeat]
    }
    assert.propEqual(
      this.ctrl.toolboxForBlockTypes([moveRight, repeat, moveLeft]),
      [emberBlocklyPrimitives, emberBlocklyRepetitions, emberBlocklySeparator]
    )
  })

  test('Transformation: _toEmberBlocklyToolboxItem should not change a BlockType', function (assert) {
    const blockType = 'MoverACasillaDerecha'
    assert.propEqual(this.ctrl._toEmberBlocklyToolboxItem(blockType), blockType)
  })

  test('Transformation: Separator to EmberSeparator', function (assert) {
    assert.propEqual(this.ctrl._toEmberBlocklyToolboxItem(separator), emberBlocklySeparator)
  })

  test('Transformation: Category to EmberCategory', function (assert) {
    const moveRight = 'MoverACasillaDerecha'
    const category = {
      categoryId: 'primitives',
      blocks: [moveRight]
    }
    const emberCategory = {
      category: 'Primitives',
      blocks: [moveRight]
    }
    assert.propEqual(this.ctrl._toEmberBlocklyToolboxItem(category), emberCategory)
  })

})
<commit_msg>one more await<commit_after>import { later } from '@ember/runloop'
import { module, test } from 'qunit'
import { setupTest } from 'ember-qunit'
import { pilasMock, interpreterFactoryMock, interpreteMock, actividadMock, blocklyWorkspaceMock, componentMock, activityExpectationsMock } from '../../helpers/mocks'
import { findBlockByTypeIn, assertProps, assertWarning, assertNotWarning, assertHasProps, setUpTestLocale } from '../../helpers/utils'
import { declaresAnyProcedure } from '../../../utils/expectations'
import sinon from 'sinon'

module('Unit | Components | pilas-blockly', function (hooks) {
  setupTest(hooks)
  setUpTestLocale(hooks)

  hooks.beforeEach(function () {
    this.owner.register('service:interpreterFactory', interpreterFactoryMock)
    this.owner.register('service:activityExpectations', activityExpectationsMock)
    this.owner.lookup('service:highlighter').workspace = blocklyWorkspaceMock()
    this.owner.lookup('service:blocksGallery').start()

    this.ctrl = this.owner.factoryFor('component:pilas-blockly').create()
    this.ctrl.pilasService = pilasMock //TODO: Injectar como service
    this.ctrl.set('modelActividad', actividadMock)
    this.ctrl.set('exerciseWorkspace', componentMock)
    this.ctrl.set('pilasBloquesApi', sinon.stub(this.ctrl.pilasBloquesApi))
    this.ctrl.set('debeMostrarFinDeDesafio', true)
    sinon.resetHistory()
  })

  //TODO: Ver de agrupar en modules
  test('Al ejecutar se encuentra ejecutando y ejecuta el intérprete', async function (assert) {
    await this.ctrl.send('ejecutar')

    assert.ok(this.ctrl.get('ejecutando'))
    assert.notOk(this.ctrl.get('pausadoEnBreakpoint'))
    assert.ok(interpreteMock.run.called)
  })

  test('Ejecutar paso a paso bloquea la ejecución', async function (assert) {
    await this.ctrl.send('ejecutar', true)

    later(() => {
      assert.ok(interpreteMock.run.calledOnce)
      assert.ok(this.ctrl.get('pausadoEnBreakpoint'))
    })

  })

  test('Step desbloquea el breakpoint', async function (assert) {
    await this.ctrl.send('ejecutar', true)

    later(() => {
      assert.ok(this.ctrl.get('pausadoEnBreakpoint'))
      this.ctrl.send('step')
      assert.notOk(this.ctrl.get('pausadoEnBreakpoint'))
    })

  })

  test('Luego de ejecutar termina de ejecutar', async function (assert) {
    await this.ctrl.send('ejecutar')

    later(() => {
      assert.notOk(this.ctrl.get('ejecutando'))
      assert.ok(this.ctrl.get('terminoDeEjecutar'))
    })

  })

  test('Al resolver el problema muestra el fin del desafío', async function (assert) {
    await this.ctrl.send('ejecutar')
    later(() => {
      assert.ok(this.ctrl.get('mostrarDialogoFinDesafio'))
    })
  })

  test('Al resolver el problema con expectativas fallidas', function (assert) {
    Blockly.textToBlock(filledProgram)
    this.owner.lookup('service:activityExpectations').expectations = declaresAnyProcedure
    await this.ctrl.send('ejecutar')
    later(() => {
      assert.notOk(this.ctrl.get('allExpectsPassed'))
    })
  })

  test('Al resolver el problema sin expectativas fallidas', async function (assert) {
    await this.ctrl.send('ejecutar')
    later(() => {
      assert.ok(this.ctrl.get('allExpectsPassed'))
    })
  })

  test('Al reiniciar settea flags y reinicia la escena de pilas', async function (assert) {
    await this.ctrl.send('reiniciar')
    assert.notOk(this.ctrl.get('ejecutando'))
    assert.notOk(this.ctrl.get('terminoDeEjecutar'))
    assert.notOk(this.ctrl.get('errorDeActividad'))
    assert.ok(pilasMock.restartScene.called)
  })

  // PROGRAM EXECUTION
  let filledProgram =
    `<block type="al_empezar_a_ejecutar">
      <statement name="program">
        <block type="MoverACasillaDerecha">
        </block>
      </statement>
    </block>`

  let nonFilledProgram =
    `<block type="al_empezar_a_ejecutar">
      <statement name="program">
        <block type="repetir">
          <value name="count">
            <block type="math_number">
              <field name="NUM">10</field>
            </block>
          </value>
        </block>
      </statement>
    </block>`

  test('Ejecuta cuando todos los bloques están completos', async function (assert) {
    Blockly.textToBlock(filledProgram)
    await this.ctrl.send('ejecutar')
    assert.ok(interpreteMock.run.called)
  })

  test('No ejecuta cuando el programa tiene algún agujero', async function (assert) {
    Blockly.textToBlock(nonFilledProgram)
    await this.ctrl.send('ejecutar')
    assert.notOk(interpreteMock.run.called)
  })

  test('Ejecuta cuando existe algún bloque con agujeros pero no se usa', async function (assert) {
    let bloqueSuelto = `
    <block type="repetir" disabled="true">
      <value name="count">
        <block type="math_number">
          <field name="NUM">10</field>
        </block>
      </value>
    </block>
    `

    Blockly.textToBlock(filledProgram)
    Blockly.textToBlock(bloqueSuelto)
    await this.ctrl.send('ejecutar')
    assert.ok(interpreteMock.run.called)
  })

  let emptyProcedure =
    `<block type="procedures_defnoreturn">
    <field name="NAME">Hacer algo</field>
  </block>`

  let emptyProcedureWithParam =
    `<block type="procedures_defnoreturn">
    <mutation>
      <arg name="parámetro 1"></arg>
    </mutation>
    <field name="NAME">Hacer algo</field>
    <field name="ARG0">parámetro 1</field>
    <statement name="STACK">
      <shadow type="required_statement"></shadow>
    </statement>
  </block>`

  let nonFilledProcedure =
    `<block type="procedures_defnoreturn">
      <field name="NAME">Hacer algo</field>
      <statement name="STACK">
        <block type="GirarGrados">
          <value name="grados">
            <shadow type="required_value"></shadow>
          </value>
        </block>
      </statement>
   </block>`

  test('Ejecuta aún cuando existe procedimiento vacío', async function (assert) {
    Blockly.textToBlock(emptyProcedure)
    await this.ctrl.send('ejecutar')
    assert.ok(interpreteMock.run.called)
  })

  test('Ejecuta aún cuando existe procedimiento vacío con parámetros', async function (assert) {
    Blockly.textToBlock(emptyProcedureWithParam)
    await this.ctrl.send('ejecutar')
    assert.ok(interpreteMock.run.called)
  })

  test('No ejecuta cuando existe procedimiento con algún agujero', async function (assert) {
    Blockly.textToBlock(nonFilledProcedure)
    await this.ctrl.send('ejecutar')
    assert.notOk(interpreteMock.run.called)
  })

  test('Al ejecutar aparecen los warnings de bloques vacíos', async function (assert) {
    let program = Blockly.textToBlock(nonFilledProgram)
    let required = findBlockByTypeIn(program, "required_statement")
    assertNotWarning(assert, required)
    await this.ctrl.send('ejecutar')
    later(() => assertWarning(assert, required, "¡Acá faltan bloques comandos!"))
  })

  // API
  test('Avisa a la api al ejecutar', async function (assert) {
    await this.ctrl.send('ejecutar')
    const staticAnalysis = this.ctrl.pilasBloquesApi.runProgram.lastCall.lastArg.staticAnalysis
    assertProps(assert, staticAnalysis, { couldExecute: true })
  })

  test('Envia metadata a la api al ejecutar', async function (assert) {
    Blockly.textToBlock(filledProgram)
    this.ctrl.send('onChangeWorkspace', filledProgram) // Fire property change :(
    await this.ctrl.send('ejecutar')
    const metadata = this.ctrl.pilasBloquesApi.runProgram.lastCall.lastArg
    assertHasProps(assert, metadata, 'ast', 'staticAnalysis', 'turboModeOn', 'program')
    assert.deepEqual(metadata.staticAnalysis, {
      couldExecute: true,
      expects: [],
    })
  })

  test('Avisa a la api al finalizar la ejecucion', async function (assert) {
    await this.ctrl.send('ejecutar')
    later(() => {
      assertProps(assert, this.ctrl.pilasBloquesApi.executionFinished.lastCall.lastArg, { finished: true })
    })
  })

  test('Avisa a la api al finalizar la ejecucion con error', async function (assert) {
    this.ctrl.errorDeActividad = "ERROR"
    await this.ctrl.send('ejecutar')
    later(() => {
      assertProps(assert, this.ctrl.pilasBloquesApi.executionFinished.lastCall.lastArg, { error: "ERROR" })
    })
  })

  test('Envia metadata a la api al ejecutar', async function (assert) {
    await this.ctrl.send('ejecutar')
    const metadata = this.ctrl.pilasBloquesApi.runProgram.lastCall.lastArg
    assertHasProps(assert, metadata, 'ast', 'staticAnalysis', 'turboModeOn',)
    assert.ok(metadata.program || metadata.program.length === 0)
  })
})

module('Unit | Components | pilas-blockly | ToolboxForBlockTypes', function(hooks) {
  setupTest(hooks)

  const operator = { categoryId: 'operators' }
  const primitive = { categoryId: 'primitives' }

  const separator = { categoryId: 'separator', isSeparator: true }
  const blockId = "A block id"

  const doThis = 'Do this'
  const doThat = 'Do that'

  const myProcedures = { categoryId: 'procedures', blocks: [doThis, doThat] }

  const toolbox = [blockId, myProcedures, separator]

  const emberBlocklySeparator = {
    isSeparator: true
  }

  hooks.beforeEach(function () {
    this.owner.lookup('service:blocksGallery').start()
    this.ctrl = this.owner.factoryFor('component:pilas-blockly').create()
    this.ctrl.set('modelActividad', actividadMock)
    this.ctrl.modelActividad.set('estiloToolbox', 'conCategorias')
    this.ctrl.get('intl').setLocale('en-us') // This is necessary because categories are tied to locale and translation.
  })

  function setCategoriesNotRequired(ctrl) {
    ctrl.modelActividad.set('estiloToolbox', 'sinCategorias')
  }

  test('ToolboxForBlockTypes should fail if blockTypes is undefined', function (assert) {
    assert.throws(() => this.ctrl.toolboxForBlockTypes(undefined))
  })

  test('Toolbox should be sorted based on Pilas Bloques stablished order', function (assert) {
    const alternative = { categoryId: 'alternatives' }
    assert.propEqual(this.ctrl.sortedToolbox([operator, primitive, alternative]), [primitive, alternative, operator])
  })

  test('Uncategorized category should be the last item in the toolbox after sorting', function (assert) {
    const uncategorized = { categoryId: 'uncategorized' }
    assert.propEqual(this.ctrl.sortedToolbox([operator, uncategorized, primitive]), [primitive, operator, uncategorized])
  })

  test('If categories are required in toolbox, it should stay unchanged', function (assert) {
    assert.propEqual(this.ctrl._styledToolbox(toolbox), toolbox)
  })

  test('If categories are not required in toolbox, it should be flattened', function (assert) {
    setCategoriesNotRequired(this.ctrl)
    assert.propEqual(this.ctrl._styledToolbox(toolbox), [blockId, doThis, doThat,separator])
  })

  test('When styling, separators should be left unchanged', function (assert) {
    setCategoriesNotRequired(this.ctrl)
    assert.propEqual(this.ctrl._styledToolbox([separator]), [separator])
  })

  test('When styling, blocks ids should be left unchanged', function (assert) {
    setCategoriesNotRequired(this.ctrl)
    assert.propEqual(this.ctrl._styledToolbox([blockId]), [blockId])
  })

  test('ToolboxForBlockTypes should add one separator', function (assert) {
    assert.equal(
      this.ctrl.toolboxForBlockTypes(['Saludar'])
      .filter(block => block.isSeparator)
      .length,
      1
    )
  })

  test('ToolboxForBlockTypes should add a separator between commands and expressions', function (assert) {
    const moveRight = 'MoverACasillaDerecha'
    const sensor = 'HayObstaculoDerecha'
    const emberBlocklyCommands = {
      category: 'Primitives',
      blocks: [moveRight]
    }
    const emberBlocklySensors = {
      category: 'Sensors',
      blocks: [sensor]
    }
    const emberToolbox = [emberBlocklyCommands, emberBlocklySeparator, emberBlocklySensors]
    assert.propEqual(this.ctrl.toolboxForBlockTypes([sensor, moveRight]), emberToolbox)
  })

  // WARNING: categories are tied to translation.
  test('If a blocktype is not a valid blockly block, is should be uncategorized', function (assert) {
    const nonExistentBlockTypeName = 'NonExistentBlockType'
    const nonExistentEmberBlockly = {
      category: 'Uncategorized',
      blocks: [nonExistentBlockTypeName]
    }
    assert.propEqual(this.ctrl.toolboxForBlockTypes([nonExistentBlockTypeName]), [emberBlocklySeparator, nonExistentEmberBlockly])
  })

  test('ToolboxForBlockTypes should group block types by their category', function (assert) {
    const moveRight = 'MoverACasillaDerecha'
    const moveLeft = 'MoverACasillaIzquierda'
    const repeat = 'Repetir'
    const emberBlocklyPrimitives = {
      category: 'Primitives',
      blocks: [moveRight, moveLeft]
    }
    const emberBlocklyRepetitions = {
      category: 'Repetition',
      blocks: [repeat]
    }
    assert.propEqual(
      this.ctrl.toolboxForBlockTypes([moveRight, repeat, moveLeft]),
      [emberBlocklyPrimitives, emberBlocklyRepetitions, emberBlocklySeparator]
    )
  })

  test('Transformation: _toEmberBlocklyToolboxItem should not change a BlockType', function (assert) {
    const blockType = 'MoverACasillaDerecha'
    assert.propEqual(this.ctrl._toEmberBlocklyToolboxItem(blockType), blockType)
  })

  test('Transformation: Separator to EmberSeparator', function (assert) {
    assert.propEqual(this.ctrl._toEmberBlocklyToolboxItem(separator), emberBlocklySeparator)
  })

  test('Transformation: Category to EmberCategory', function (assert) {
    const moveRight = 'MoverACasillaDerecha'
    const category = {
      categoryId: 'primitives',
      blocks: [moveRight]
    }
    const emberCategory = {
      category: 'Primitives',
      blocks: [moveRight]
    }
    assert.propEqual(this.ctrl._toEmberBlocklyToolboxItem(category), emberCategory)
  })

})
<|endoftext|>


<commit_before>var entity = {
    x: 0,
    y: 0,
    color: "#FF00FF",
    attackPower: 3,
    hp: 20,
    needsDestroy: false,

    setTo: function(x,y) {
        this.x = x
        this.y = y
    },

    move: function(offX, offY) {
        const x = this.x + offX
        const y = this.y + offY
        var entityAtPosition = this.entityAt(x, y)
        if(entityAtPosition){
            this.attack(entityAtPosition)
        } else if (isColliderAt(x, y)) {
            var interactable = getInteractableAt(x,y)
            if(interactable && interactable.type == "treasure"){
                console.log("Congratulations! You've found a treasure chest!")
                level[y] = changeStringAtIndex(level[y], x, " ")
            }
        } else {
            this.x += offX
            this.y += offY
        }
    },

    show: function() {
        fill(this.color)
        rect(this.x*cellsize, this.y*cellsize, cellsize, cellsize)
    },

    attack: function (entity){
        entity.takeDamage(this.attackPower)
    },

    takeDamage: function(damage){
        this.hp -= damage
        if(this.hp <= 0){
            this.needsDestroy = true
        }
    },

    entityAt: function(x,y) {
        var entities = enemies.slice()
        entities.push(p)

        var entityToReturn = undefined

        entities.forEach( (entity) => {
            if(entity.x == x && entity.y == y){
                entityToReturn = entity
            }
        })
        return(entityToReturn)
    }
}

var enemy =  {
    x: 0,
    y: 0,
    color: "#FF00FF",
    type: "enemy",
    spottingDistance: 10,

    move: function() {
        this.simplePathfind()
    },

    simplePathfind: function() {
        // simple pathfinding (NOT COLLISIONPROOF!)

        const playerPosition = {x: p.x, y: p.y}

        const possibleMoves = [
            {x: 1, y: 0},
            {x: -1, y: 0},
            {x: 0, y: -1},
            {x: 0, y:1}
        ]

        if(distance({x: this.x, y: this.y}, playerPosition) <= this.spottingDistance){

            var canStillMove = true
            for(var o = 0; o < possibleMoves.length; o++){
                var move = possibleMoves[o]
                var possibleEntity = this.entityAt(move.x + this.x, move.y + this.y)
                if(possibleEntity){
                    console.log("player attacked")
                    if(possibleEntity.type === "player"){
                        this.attack(possibleEntity)
                        canStillMove = false
                        break;
                    }
                }
            }

            if(canStillMove){
                var bestMove = {x: 0, y: 0}

                for(var i = 0; i < possibleMoves.length; i++){
                    var newMove = {x: this.x + possibleMoves[i].x, y: this.y + possibleMoves[i].y}
                    if(distance(newMove,playerPosition)<=distance({x: this.x + bestMove.x, y: this.y + bestMove.y },playerPosition)){
                        bestMove = possibleMoves[i]
                    }
                }

                if(this.entityAt(bestMove.x + this.x, bestMove.y + this.y) == undefined){
                    this.x += bestMove.x
                    this.y += bestMove.y
                }
            }
        } else {
            // move around a bit
            const moveIndex = floor(random(possibleMoves.length + 10))

            if(moveIndex < possibleMoves.length){
                const randomMove = possibleMoves[moveIndex]
                const x = this.x + randomMove.x
                const y = this.y + randomMove.y

                if(!isColliderAt(x,y)){
                    this.x += randomMove.x
                    this.y += randomMove.y
                }
            }
        }
    }
}

var player = {
    x: 2,
    y: 2,
    attackPower: 10,
    hp: 10,
    color: "#00FF00",
    type: "player",
}
<commit_msg>enemies now cant travel through walls (and treasures)<commit_after>var entity = {
    x: 0,
    y: 0,
    color: "#FF00FF",
    attackPower: 3,
    hp: 20,
    needsDestroy: false,

    setTo: function(x,y) {
        this.x = x
        this.y = y
    },

    move: function(offX, offY) {
        const x = this.x + offX
        const y = this.y + offY
        var entityAtPosition = this.entityAt(x, y)
        if(entityAtPosition){
            this.attack(entityAtPosition)
        } else if (isColliderAt(x, y)) {
            var interactable = getInteractableAt(x,y)
            if(interactable && interactable.type == "treasure"){
                console.log("Congratulations! You've found a treasure chest!")
                level[y] = changeStringAtIndex(level[y], x, " ")
            }
        } else {
            this.x += offX
            this.y += offY
        }
    },

    show: function() {
        fill(this.color)
        rect(this.x*cellsize, this.y*cellsize, cellsize, cellsize)
    },

    attack: function (entity){
        entity.takeDamage(this.attackPower)
    },

    takeDamage: function(damage){
        this.hp -= damage
        if(this.hp <= 0){
            this.needsDestroy = true
        }
    },

    entityAt: function(x,y) {
        var entities = enemies.slice()
        entities.push(p)

        var entityToReturn = undefined

        entities.forEach( (entity) => {
            if(entity.x == x && entity.y == y){
                entityToReturn = entity
            }
        })
        return(entityToReturn)
    }
}

var enemy =  {
    x: 0,
    y: 0,
    color: "#FF00FF",
    type: "enemy",
    spottingDistance: 10,

    move: function() {
        this.simplePathfind()
    },

    simplePathfind: function() {
        // simple pathfinding (NOT COLLISIONPROOF!)

        const playerPosition = {x: p.x, y: p.y}

        const possibleMoves = [
            {x: 1, y: 0},
            {x: -1, y: 0},
            {x: 0, y: -1},
            {x: 0, y:1}
        ]

        if(distance({x: this.x, y: this.y}, playerPosition) <= this.spottingDistance){

            var canStillMove = true
            for(var o = 0; o < possibleMoves.length; o++){
                var move = possibleMoves[o]
                var possibleEntity = this.entityAt(move.x + this.x, move.y + this.y)
                if(possibleEntity){
                    console.log("player attacked")
                    if(possibleEntity.type === "player"){
                        this.attack(possibleEntity)
                        canStillMove = false
                        break;
                    }
                }
            }

            if(canStillMove){
                var bestMove = {x: 0, y: 0}

                for(var i = 0; i < possibleMoves.length; i++){
                    var newMove = {x: this.x + possibleMoves[i].x, y: this.y + possibleMoves[i].y}
                    if(distance(newMove,playerPosition)<=distance({x: this.x + bestMove.x, y: this.y + bestMove.y },playerPosition)){
                        bestMove = possibleMoves[i]
                    }
                }
                var x = this.x + bestMove.x
                var y = this.y + bestMove.y
                if(!isColliderAt(x,y) && this.entityAt(x,y) == undefined){
                    this.x += bestMove.x
                    this.y += bestMove.y
                }
            }
        } else {
            // move around a bit
            const moveIndex = floor(random(possibleMoves.length + 10))

            if(moveIndex < possibleMoves.length){
                const randomMove = possibleMoves[moveIndex]
                const x = this.x + randomMove.x
                const y = this.y + randomMove.y

                if(!isColliderAt(x,y)){
                    this.x += randomMove.x
                    this.y += randomMove.y
                }
            }
        }
    }
}

var player = {
    x: 2,
    y: 2,
    attackPower: 10,
    hp: 10,
    color: "#00FF00",
    type: "player",
}
<|endoftext|>


<commit_before>if (typeof (require) != 'undefined') {
	var loader = require('../../../../node_modules/Butterfly-js/dist/butterfly.min.js').loader;
}

/**
 * Module providing game data
 */
loader.addModule('data',
function () {
	var data = {
		maps: [
			{width: 20, height: 15, start: [[2, 7], [1, 7]], map:
			[[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
			 [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,1],
			 [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,1],
			 [1,0,0,4,1,1,1,1,1,1,1,1,1,1,1,1,0,0,2,1],
			 [1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,1,0,0,2,1],
			 [1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,1,0,0,2,1],
			 [1,3,3,1,0,0,1,1,1,1,1,1,1,0,0,1,0,0,0,1],
			 [1,0,0,1,0,0,0,0,0,1,2,2,1,0,0,1,0,0,0,1],
			 [1,0,0,1,0,0,0,0,0,1,1,1,1,0,0,1,1,0,0,1],
			 [1,0,0,1,1,1,4,0,0,1,0,0,0,0,0,1,0,0,0,1],
			 [1,0,0,1,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,1],
			 [1,0,0,1,0,0,0,0,0,1,0,0,4,1,1,1,0,0,2,1],
			 [1,0,0,0,0,0,2,2,1,1,0,0,0,0,0,0,0,0,2,1],
			 [1,0,0,0,0,0,1,1,1,1,0,0,0,0,0,0,0,0,2,1],
			 [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]]}
		],
		resourcesMap: {TILE_ROAD: 0, TILE_WALL: 1, TILE_GRASS: 2, TILE_FINISH: 3, TILE_FLAG: 4, CAR: [5, 6]},
		resources: [
			{url: 'images/road.png', obstacle: false},
			{url: 'images/wall.png', obstacle: true},
			{url: 'images/grass.png', obstacle: false},
			{url: 'images/start.png', obstacle: false},
			{url: 'images/flag.png', obstacle: true},
			{url: 'images/player1car.png'},
			{url: 'images/player2car.png'}
		]
	};

	return data;
});
<commit_msg>RACE - each resource is now flagged as texture or not<commit_after>if (typeof (require) != 'undefined') {
	var loader = require('../../../../node_modules/Butterfly-js/dist/butterfly.min.js').loader;
}

/**
 * Module providing game data
 */
loader.addModule('data',
function () {
	var data = {
		maps: [
			{width: 20, height: 15, start: [[2, 7], [1, 7]], map:
			[[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
			 [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,1],
			 [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,1],
			 [1,0,0,4,1,1,1,1,1,1,1,1,1,1,1,1,0,0,2,1],
			 [1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,1,0,0,2,1],
			 [1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,1,0,0,2,1],
			 [1,3,3,1,0,0,1,1,1,1,1,1,1,0,0,1,0,0,0,1],
			 [1,0,0,1,0,0,0,0,0,1,2,2,1,0,0,1,0,0,0,1],
			 [1,0,0,1,0,0,0,0,0,1,1,1,1,0,0,1,1,0,0,1],
			 [1,0,0,1,1,1,4,0,0,1,0,0,0,0,0,1,0,0,0,1],
			 [1,0,0,1,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,1],
			 [1,0,0,1,0,0,0,0,0,1,0,0,4,1,1,1,0,0,2,1],
			 [1,0,0,0,0,0,2,2,1,1,0,0,0,0,0,0,0,0,2,1],
			 [1,0,0,0,0,0,1,1,1,1,0,0,0,0,0,0,0,0,2,1],
			 [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]]}
		],
		resourcesMap: {TILE_ROAD: 0, TILE_WALL: 1, TILE_GRASS: 2, TILE_FINISH: 3, TILE_FLAG: 4, CAR: [5, 6]},
		resources: [
			{url: 'images/road.png', obstacle: false, texture: true},
			{url: 'images/wall.png', obstacle: true, texture: true},
			{url: 'images/grass.png', obstacle: false, texture: true},
			{url: 'images/start.png', obstacle: false, texture: true},
			{url: 'images/flag.png', obstacle: true, texture: true},
			{url: 'images/player1car.png'},
			{url: 'images/player2car.png'}
		]
	};

	return data;
});
<|endoftext|>