<commit_before>/**
 * @license
 * Copyright 2016 Google Inc. All Rights Reserved.
 *
 * 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.
 */

foam.CLASS({
  package: 'foam.core',
  name: 'Slot', // ???: Rename AbstractSlot or make an Interface

  documentation: `
    Slots are observable values which can change over time.

    Slots are simple single-value Model-View-Controller Models, but since
    another meaning of 'Model' is already heavily used in FOAM, Slot is
    used to avoid overloading the term.

Types of Slots include:

PropertySlot

ConstantSlot

ExpressionSlot

  `,

  methods: [
    /**
      Subscribe to the Slot's value, if it has one. If the Slot's
      value changes, then unsubscribe from the previous value and
      resubscribe to the new one.
    */
    function valueSub() {
      var self = this;
      var args = Array.from(arguments);
      var s;
      var l = function() {
        var v = self.get();
        if ( s ) s.detach();
        if ( v ) s = v.sub.apply(v, args);
      };
      l();
      this.sub(l);
    },

    /**
      Create a sub-Slot for this Slot's value. If this Slot's
      value changes, then the sub-Slot becomes the Slot for
      the new value's sub-Slot instead. Useful for creating
      Slot paths without having to rebuild whenever a value
      along the chain changes.
    */
    function dot(name) {
      return foam.core.internal.SubSlot.create({
        parent: this,
        name:   name
      });
    },

    // TODO: remove when all code ported
    function link(other) {
      console.warn('deprecated use of link(), use linkFrom() instead');
      return this.linkFrom(other);
    },

    /**
      Link two Slots together, setting both to other's value.
      Returns a Detachable which can be used to break the link.
      After copying a value from one slot to the other, this implementation
      then copies the value back in case the target slot rejected the value.
    */
    function linkFrom(s2) {
      var s1        = this;
      var feedback1 = false, feedback2 = false;

      // TODO: once all slot types property set 'src', these
      // two listeneners can be merged.
      var l1 = function(e) {
        if ( feedback1 ) return;

        if ( ! foam.util.equals(s1.get(), s2.get()) ) {
          feedback1 = true;
          s2.set(s1.get());
          if ( ! foam.util.equals(s1.get(), s2.get()) )
            s1.set(s2.get());
          feedback1 = false;
        }
      };

      var l2 = function(e) {
        if ( feedback2 ) return;

        if ( ! foam.util.equals(s1.get(), s2.get()) ) {
          feedback2 = true;
          s1.set(s2.get());
          if ( ! foam.util.equals(s1.get(), s2.get()) )
            s2.set(s1.get());
          feedback2 = false;
        }
      };

      var sub1 = s1.sub(l1);
      var sub2 = s2.sub(l2)

      l2();

      return {
        detach: function() {
          sub1 && sub1.detach();
          sub2 && sub2.detach();
          sub1 = sub2 = null;
        }
      };
    },

    function linkTo(other) {
      return other.linkFrom(this);
    },

    /**
      Have this Slot dynamically follow other's value.
      Returns a Detachable which can be used to cancel the binding.
    */
    function follow(other) {
      foam.assert(other, 'Slot.follow requires Slot argument.');
      var self = this;
      var l = function() {
        if ( ! foam.util.equals(self.get(), other.get()) ) {
          self.set(other.get());
        }
      };
      l();
      return other.sub(l);
    },

    /**
     * Maps values from one model to another.
     * @param f maps values from srcValue to dstValue
     */
    function mapFrom(other, f) {
      var self = this;
      var l = function() { self.set(f(other.get())); };
      l();
      return other.sub(l);
    },

    function mapTo(other, f) {
      return other.mapFrom(this, f);
    },

    function map(f) {
      return foam.core.ExpressionSlot.create({code: f, args: [this]});
    },

    /**
     * Relate to another Slot.
     * @param f maps from this to other
     * @param fprime maps other to this
     */
    function relateTo(other, f, fPrime) {
      var self     = this;
      var feedback = false;
      var sub      = foam.core.FObject.create();
      var l1 = function() {
        if ( feedback ) return;
        feedback = true;
        other.set(f(self.get()));
        feedback = false;
      };
      var l2 = function() {
        if ( feedback ) return;
        feedback = true;
        self.set(fPrime(other.get()));
        feedback = false;
      };

      sub.onDetach(this.sub(l1));
      sub.onDetach(other.sub(l2));

      l1();

      return sub;
    },

    function relateFrom(other, f, fPrime) {
      return other.relateTo(this, fPrime, f);
    }
  ]
});

foam.CLASS({
  package: 'foam.core.internal',
  name: 'PropertySlot',
  extends: 'foam.core.Slot',

  documentation: `
    Represents object properties as Slots.
    Created with calling obj.prop$ or obj.slot('prop').
    For internal use only.
  `,

  methods: [
    function initArgs() { },
    function init() { },

    function get() {
      return this.prop.get(this.obj);
    },

    function set(value) {
      return this.prop.set(this.obj, value);
    },

    function getPrev() {
      return this.oldValue;
    },

    function setPrev(value) {
      return this.oldValue = value;
    },

    function sub(l) {
      var s = this.obj.sub('propertyChange', this.prop.name, l);
      s.src = this;
      return s;
    },

    function isDefined() {
      return this.obj.hasOwnProperty(this.prop.name);
    },

    function clear() {
      this.obj.clearProperty(this.prop.name);
    },

    function toString() {
      return 'PropertySlot(' + this.obj.cls_.id + '.' + this.prop.name + ')';
    }
  ]
});

foam.CLASS({
  package: 'foam.core.internal',
  name: 'SubSlot',
  extends: 'foam.core.Slot',

  documentation:
      'For internal use only. Is used to implement the Slot.dot() method.',

  properties: [
    'of',
    'parent', // parent slot, not parent object
    'name',
    'value',
    'prevSub'
  ],

  methods: [
    function init() {
      this.parent.sub(this.parentChange);
      this.parentChange();
    },

    function get() {
      var o = this.parent.get();

      return o && o[this.name];
    },

    function set(value) {
      var o = this.parent.get();

      if ( o ) o[this.name] = value;
    },

    /** Needed? **/
    function getPrev() {
      debugger;
      return this.oldValue;
    },

    /** Needed? **/
    function setPrev(value) {
      debugger;
      return this.oldValue = value;
    },

    function sub(l) {
      return this.SUPER('propertyChange', 'value', l);
    },

    function isDefined() {
      return this.parent.get().hasOwnProperty(this.name);
    },

    function clear() {
      this.parent.get().clearProperty(this.name);
    },

    function toString() {
      return 'SubSlot(' + this.parent + ',' + this.name + ')';
    }
  ],

  listeners: [
    function parentChange(s) {
      this.prevSub && this.prevSub.detach();
      var o = this.parent.get();

      // If the parent object changes class, then don't update
      // because a new class will have different sub-slots.
      if ( this.of ) {
        if ( this.of !== ( o && o.cls_ ) ) {
          s.detach();
          return;
        }
      } else {
        if ( o ) this.of = o.cls_;
      }

      this.prevSub = o && o.slot(this.name).sub(this.valueChange);
      this.valueChange();
    },

    function valueChange() {
      var parentValue = this.parent.get();
      this.value = parentValue ? parentValue[this.name] : undefined;
    }
  ]
});

foam.CLASS({
  package: 'foam.core',
  name: 'ConstantSlot',

  implements: [ 'foam.core.Slot' ],

  documentation: 'An immutable constant valued Slot.',

  properties: [
    {
      name: 'value',
      getter: function() { return this.value_; },
      setter: function() {}
    }
  ],

  methods: [
    function initArgs(args) { this.value_ = args && args.value; },

    function get() { return this.value; },

    function set() {
      throw new Error('Tried to mutate immutable ConstantSlot.');
    },

    function sub(l) { /* nop */ }
  ]
});

/**
*/
foam.CLASS({
  package: 'foam.core',
  name: 'ExpressionSlot',
  implements: [ 'foam.core.Slot' ],

  documentation: `
    Tracks dependencies for a dynamic function and invalidates if they change.

      foam.CLASS({name: 'Person', properties: ['fname', 'lname']});
      var p = Person.create({fname: 'John', lname: 'Smith'});
      var e = foam.core.ExpressionSlot.create({
        args: [ p.fname$, p.lname$ ],
        code: function(f, l) { return f + ' ' + l; }
      });
      log(e.get());
      e.sub(log);
      p.fname = 'Steve';
      p.lname = 'Jones';
      log(e.get());

      Output:
       > John Smith
       > [object Object] propertyChange value [object Object]
       > [object Object] propertyChange value [object Object]
       > Steve Jones

      var p = foam.CLASS({name: 'Person', properties: [ 'f', 'l' ]}).create({f:'John', l: 'Doe'});
      var e = foam.core.ExpressionSlot.create({
        obj: p,
        code: function(f, l) { return f + ' ' + l; }
      });

  `,

  properties: [
    'obj',
    'code',
    {
      name: 'args',
      expression: function(obj) {
        foam.assert(obj, 'ExpressionSlot: "obj" or "args" required.');

        var args = foam.Function.argNames(this.code);
        for ( var i = 0 ; i < args.length ; i++ ) {
          args[i] = obj.slot(args[i]);
        }

        // this.invalidate(); // ???: Is this needed?
        this.subToArgs_(args);

        return args;
      },
      postSet: function(_, args) {
        this.subToArgs_(args);
      }
    },
    {
      name: 'value',
      factory: function() {
        return this.code.apply(this.obj || this, this.args.map(function(a) {
          return a.get();
        }));
      }
    },
    'cleanup_', // detachable to cleanup old subs when obj changes
  ],

  methods: [
    function init() { this.onDetach(this.cleanup); },

    function get() { return this.value; },

    function set() { /* nop */ },

    function sub(l) {
      return arguments.length === 1 ?
        this.SUPER('propertyChange', 'value', l) :
        this.SUPER.apply(this,arguments);
    },

    function subToArgs_(args) {
      this.cleanup();

      var cleanup = foam.core.FObject.create();

      for ( var i = 0 ; i < args.length ; i++ ) {
        cleanup.onDetach(args[i].sub(this.invalidate));
      }

      this.cleanup_ = cleanup;
    }
  ],

  listeners: [
    function cleanup() { this.cleanup_ && this.cleanup_.detach(); },
    function invalidate() { this.clearProperty('value'); }
  ]
});
<commit_msg>Update Slot linking/following to check foam.util.is(a, b) rather than foam.util.equals().<commit_after>/**
 * @license
 * Copyright 2016 Google Inc. All Rights Reserved.
 *
 * 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.
 */

foam.CLASS({
  package: 'foam.core',
  name: 'Slot', // ???: Rename AbstractSlot or make an Interface

  documentation: `
    Slots are observable values which can change over time.

    Slots are simple single-value Model-View-Controller Models, but since
    another meaning of 'Model' is already heavily used in FOAM, Slot is
    used to avoid overloading the term.

Types of Slots include:

PropertySlot

ConstantSlot

ExpressionSlot

  `,

  methods: [
    /**
      Subscribe to the Slot's value, if it has one. If the Slot's
      value changes, then unsubscribe from the previous value and
      resubscribe to the new one.
    */
    function valueSub() {
      var self = this;
      var args = Array.from(arguments);
      var s;
      var l = function() {
        var v = self.get();
        if ( s ) s.detach();
        if ( v ) s = v.sub.apply(v, args);
      };
      l();
      this.sub(l);
    },

    /**
      Create a sub-Slot for this Slot's value. If this Slot's
      value changes, then the sub-Slot becomes the Slot for
      the new value's sub-Slot instead. Useful for creating
      Slot paths without having to rebuild whenever a value
      along the chain changes.
    */
    function dot(name) {
      return foam.core.internal.SubSlot.create({
        parent: this,
        name:   name
      });
    },

    // TODO: remove when all code ported
    function link(other) {
      console.warn('deprecated use of link(), use linkFrom() instead');
      return this.linkFrom(other);
    },

    /**
      Link two Slots together, setting both to other's value.
      Returns a Detachable which can be used to break the link.
      After copying a value from one slot to the other, this implementation
      then copies the value back in case the target slot rejected the value.
    */
    function linkFrom(s2) {
      var s1        = this;
      var feedback1 = false, feedback2 = false;

      // TODO: once all slot types property set 'src', these
      // two listeneners can be merged.
      var l1 = function(e) {
        if ( feedback1 ) return;

        if ( ! foam.util.is(s1.get(), s2.get()) ) {
          feedback1 = true;
          s2.set(s1.get());
          if ( ! foam.util.is(s1.get(), s2.get()) )
            s1.set(s2.get());
          feedback1 = false;
        }
      };

      var l2 = function(e) {
        if ( feedback2 ) return;

        if ( ! foam.util.is(s1.get(), s2.get()) ) {
          feedback2 = true;
          s1.set(s2.get());
          if ( ! foam.util.is(s1.get(), s2.get()) )
            s2.set(s1.get());
          feedback2 = false;
        }
      };

      var sub1 = s1.sub(l1);
      var sub2 = s2.sub(l2)

      l2();

      return {
        detach: function() {
          sub1 && sub1.detach();
          sub2 && sub2.detach();
          sub1 = sub2 = null;
        }
      };
    },

    function linkTo(other) {
      return other.linkFrom(this);
    },

    /**
      Have this Slot dynamically follow other's value.
      Returns a Detachable which can be used to cancel the binding.
    */
    function follow(other) {
      foam.assert(other, 'Slot.follow requires Slot argument.');
      var self = this;
      var l = function() {
        if ( ! foam.util.is(self.get(), other.get()) ) {
          self.set(other.get());
        }
      };
      l();
      return other.sub(l);
    },

    /**
     * Maps values from one model to another.
     * @param f maps values from srcValue to dstValue
     */
    function mapFrom(other, f) {
      var self = this;
      var l = function() { self.set(f(other.get())); };
      l();
      return other.sub(l);
    },

    function mapTo(other, f) {
      return other.mapFrom(this, f);
    },

    function map(f) {
      return foam.core.ExpressionSlot.create({code: f, args: [this]});
    },

    /**
     * Relate to another Slot.
     * @param f maps from this to other
     * @param fprime maps other to this
     */
    function relateTo(other, f, fPrime) {
      var self     = this;
      var feedback = false;
      var sub      = foam.core.FObject.create();
      var l1 = function() {
        if ( feedback ) return;
        feedback = true;
        other.set(f(self.get()));
        feedback = false;
      };
      var l2 = function() {
        if ( feedback ) return;
        feedback = true;
        self.set(fPrime(other.get()));
        feedback = false;
      };

      sub.onDetach(this.sub(l1));
      sub.onDetach(other.sub(l2));

      l1();

      return sub;
    },

    function relateFrom(other, f, fPrime) {
      return other.relateTo(this, fPrime, f);
    }
  ]
});

foam.CLASS({
  package: 'foam.core.internal',
  name: 'PropertySlot',
  extends: 'foam.core.Slot',

  documentation: `
    Represents object properties as Slots.
    Created with calling obj.prop$ or obj.slot('prop').
    For internal use only.
  `,

  methods: [
    function initArgs() { },
    function init() { },

    function get() {
      return this.prop.get(this.obj);
    },

    function set(value) {
      return this.prop.set(this.obj, value);
    },

    function getPrev() {
      return this.oldValue;
    },

    function setPrev(value) {
      return this.oldValue = value;
    },

    function sub(l) {
      var s = this.obj.sub('propertyChange', this.prop.name, l);
      s.src = this;
      return s;
    },

    function isDefined() {
      return this.obj.hasOwnProperty(this.prop.name);
    },

    function clear() {
      this.obj.clearProperty(this.prop.name);
    },

    function toString() {
      return 'PropertySlot(' + this.obj.cls_.id + '.' + this.prop.name + ')';
    }
  ]
});

foam.CLASS({
  package: 'foam.core.internal',
  name: 'SubSlot',
  extends: 'foam.core.Slot',

  documentation:
      'For internal use only. Is used to implement the Slot.dot() method.',

  properties: [
    'of',
    'parent', // parent slot, not parent object
    'name',
    'value',
    'prevSub'
  ],

  methods: [
    function init() {
      this.parent.sub(this.parentChange);
      this.parentChange();
    },

    function get() {
      var o = this.parent.get();

      return o && o[this.name];
    },

    function set(value) {
      var o = this.parent.get();

      if ( o ) o[this.name] = value;
    },

    /** Needed? **/
    function getPrev() {
      debugger;
      return this.oldValue;
    },

    /** Needed? **/
    function setPrev(value) {
      debugger;
      return this.oldValue = value;
    },

    function sub(l) {
      return this.SUPER('propertyChange', 'value', l);
    },

    function isDefined() {
      return this.parent.get().hasOwnProperty(this.name);
    },

    function clear() {
      this.parent.get().clearProperty(this.name);
    },

    function toString() {
      return 'SubSlot(' + this.parent + ',' + this.name + ')';
    }
  ],

  listeners: [
    function parentChange(s) {
      this.prevSub && this.prevSub.detach();
      var o = this.parent.get();

      // If the parent object changes class, then don't update
      // because a new class will have different sub-slots.
      if ( this.of ) {
        if ( this.of !== ( o && o.cls_ ) ) {
          s.detach();
          return;
        }
      } else {
        if ( o ) this.of = o.cls_;
      }

      this.prevSub = o && o.slot(this.name).sub(this.valueChange);
      this.valueChange();
    },

    function valueChange() {
      var parentValue = this.parent.get();
      this.value = parentValue ? parentValue[this.name] : undefined;
    }
  ]
});

foam.CLASS({
  package: 'foam.core',
  name: 'ConstantSlot',

  implements: [ 'foam.core.Slot' ],

  documentation: 'An immutable constant valued Slot.',

  properties: [
    {
      name: 'value',
      getter: function() { return this.value_; },
      setter: function() {}
    }
  ],

  methods: [
    function initArgs(args) { this.value_ = args && args.value; },

    function get() { return this.value; },

    function set() {
      throw new Error('Tried to mutate immutable ConstantSlot.');
    },

    function sub(l) { /* nop */ }
  ]
});

/**
*/
foam.CLASS({
  package: 'foam.core',
  name: 'ExpressionSlot',
  implements: [ 'foam.core.Slot' ],

  documentation: `
    Tracks dependencies for a dynamic function and invalidates if they change.

      foam.CLASS({name: 'Person', properties: ['fname', 'lname']});
      var p = Person.create({fname: 'John', lname: 'Smith'});
      var e = foam.core.ExpressionSlot.create({
        args: [ p.fname$, p.lname$ ],
        code: function(f, l) { return f + ' ' + l; }
      });
      log(e.get());
      e.sub(log);
      p.fname = 'Steve';
      p.lname = 'Jones';
      log(e.get());

      Output:
       > John Smith
       > [object Object] propertyChange value [object Object]
       > [object Object] propertyChange value [object Object]
       > Steve Jones

      var p = foam.CLASS({name: 'Person', properties: [ 'f', 'l' ]}).create({f:'John', l: 'Doe'});
      var e = foam.core.ExpressionSlot.create({
        obj: p,
        code: function(f, l) { return f + ' ' + l; }
      });

  `,

  properties: [
    'obj',
    'code',
    {
      name: 'args',
      expression: function(obj) {
        foam.assert(obj, 'ExpressionSlot: "obj" or "args" required.');

        var args = foam.Function.argNames(this.code);
        for ( var i = 0 ; i < args.length ; i++ ) {
          args[i] = obj.slot(args[i]);
        }

        // this.invalidate(); // ???: Is this needed?
        this.subToArgs_(args);

        return args;
      },
      postSet: function(_, args) {
        this.subToArgs_(args);
      }
    },
    {
      name: 'value',
      factory: function() {
        return this.code.apply(this.obj || this, this.args.map(function(a) {
          return a.get();
        }));
      }
    },
    'cleanup_', // detachable to cleanup old subs when obj changes
  ],

  methods: [
    function init() { this.onDetach(this.cleanup); },

    function get() { return this.value; },

    function set() { /* nop */ },

    function sub(l) {
      return arguments.length === 1 ?
        this.SUPER('propertyChange', 'value', l) :
        this.SUPER.apply(this,arguments);
    },

    function subToArgs_(args) {
      this.cleanup();

      var cleanup = foam.core.FObject.create();

      for ( var i = 0 ; i < args.length ; i++ ) {
        cleanup.onDetach(args[i].sub(this.invalidate));
      }

      this.cleanup_ = cleanup;
    }
  ],

  listeners: [
    function cleanup() { this.cleanup_ && this.cleanup_.detach(); },
    function invalidate() { this.clearProperty('value'); }
  ]
});
<|endoftext|>


<commit_before>import { isWindows, global, readMemberExpression, cjsRequireRegEx } from './common.js';

export default function (loader) {
  loader.set('@@cjs-helpers', loader.newModule({
    requireResolve: requireResolve.bind(loader),
    getPathVars: getPathVars
  }));

  loader.set('@@global-helpers', loader.newModule({
    prepareGlobal: prepareGlobal
  }));

  /*
    AMD-compatible require
    To copy RequireJS, set window.require = window.requirejs = loader.amdRequire
  */
  function require (names, callback, errback, referer) {
    // in amd, first arg can be a config object... we just ignore
    if (typeof names === 'object' && !(names instanceof Array))
      return require.apply(null, Array.prototype.splice.call(arguments, 1, arguments.length - 1));

    // amd require
    if (typeof names === 'string' && typeof callback === 'function')
      names = [names];
    if (names instanceof Array) {
      var dynamicRequires = [];
      for (var i = 0; i < names.length; i++)
        dynamicRequires.push(loader.import(names[i], referer));
      Promise.all(dynamicRequires).then(function (modules) {
        for (var i = 0; i < modules.length; i++)
          modules[i] = modules[i].__useDefault ? modules[i].default : modules[i];
        if (callback)
          callback.apply(null, modules);
      }, errback);
    }

    // commonjs require
    else if (typeof names === 'string') {
      var normalized = loader.decanonicalize(names, referer);
      var module = loader.get(normalized);
      if (!module)
        throw new Error('Module not already loaded loading "' + names + '" as ' + normalized + (referer ? ' from "' + referer + '".' : '.'));
      return module.__useDefault ? module.default : module;
    }

    else
      throw new TypeError('Invalid require');
  }

  function define (name, deps, factory) {
    if (typeof name !== 'string') {
      factory = deps;
      deps = name;
      name = null;

      if (curMetaDeps) {
        deps = deps.concat(curMetaDeps);
        curMetaDeps = undefined;
      }
    }

    if (!(deps instanceof Array)) {
      factory = deps;
      deps = ['require', 'exports', 'module'].splice(0, factory.length);
    }

    if (typeof factory !== 'function')
      factory = (function (factory) {
        return function() { return factory; }
      })(factory);

    // remove system dependencies
    var requireIndex, exportsIndex, moduleIndex;

    if ((requireIndex = deps.indexOf('require')) !== -1) {

      deps.splice(requireIndex, 1);

      // only trace cjs requires for non-named
      // named defines assume the trace has already been done
      if (!name)
        deps = deps.concat(amdGetCJSDeps(factory.toString(), requireIndex));
    }

    if ((exportsIndex = deps.indexOf('exports')) !== -1)
      deps.splice(exportsIndex, 1);

    if ((moduleIndex = deps.indexOf('module')) !== -1)
      deps.splice(moduleIndex, 1);

    function execute (req, exports, module) {
      var depValues = [];
      for (var i = 0; i < deps.length; i++)
        depValues.push(req(deps[i]));

      module.uri = module.id;

      module.config = function () {};

      // add back in system dependencies
      if (moduleIndex !== -1)
        depValues.splice(moduleIndex, 0, module);

      if (exportsIndex !== -1)
        depValues.splice(exportsIndex, 0, exports);

      if (requireIndex !== -1) {
        var contextualRequire = function (names, callback, errback) {
          if (typeof names === 'string' && typeof callback !== 'function')
            return req(names);
          return require.call(loader, names, callback, errback, module.id);
        };
        contextualRequire.toUrl = function (name) {
          return loader.normalizeSync(name, module.id);
        };
        depValues.splice(requireIndex, 0, contextualRequire);
      }

      // set global require to AMD require
      var curRequire = global.require;
      global.require = require;

      var output = factory.apply(exportsIndex === -1 ? global : exports, depValues);

      global.require = curRequire;

      if (typeof output === 'undefined' && module)
        output = module.exports;
      else
        module.exports = output;
    }

    // anonymous define
    if (!name) {
      loader.registerDynamic(deps, execute);
    }
    else {
      loader.registerDynamic(name, deps, execute);

      // if we don't have any other defines,
      // then let this be an anonymous define
      // this is just to support single modules of the form:
      // define('jquery')
      // still loading anonymously
      // because it is done widely enough to be useful
      // as soon as there is more than one define, this gets removed though
      if (lastNamedDefine) {
        lastNamedDefine = undefined;
        multipleNamedDefines = true;
      }
      else if (!multipleNamedDefines) {
        lastNamedDefine = [deps, execute];
      }
    }
  }
  define.amd = {};

  loader.amdDefine = define;
  loader.amdRequire = require;
}

// CJS
if (typeof window !== 'undefined' && typeof document !== 'undefined' && window.location)
  var windowOrigin = location.protocol + '//' + location.hostname + (location.port ? ':' + location.port : '');

function stripOrigin(path) {
  if (path.substr(0, 8) === 'file:///')
    return path.substr(7 + !!isWindows);

  if (windowOrigin && path.substr(0, windowOrigin.length) === windowOrigin)
    return path.substr(windowOrigin.length);

  return path;
}

export function requireResolve (request, parentId) {
  return stripOrigin(this.normalizeSync(request, parentId));
}

export function getPathVars (moduleId) {
  // remove any plugin syntax
  var pluginIndex = moduleId.lastIndexOf('!');
  var filename;
  if (pluginIndex !== -1)
    filename = moduleId.substr(0, pluginIndex);
  else
    filename = moduleId;

  var dirname = filename.split('/');
  dirname.pop();
  dirname = dirname.join('/');

  return {
    filename: stripOrigin(filename),
    dirname: stripOrigin(dirname)
  };
}

var commentRegEx = /(^|[^\\])(\/\*([\s\S]*?)\*\/|([^:]|^)\/\/(.*)$)/mg;
var stringRegEx = /("[^"\\\n\r]*(\\.[^"\\\n\r]*)*"|'[^'\\\n\r]*(\\.[^'\\\n\r]*)*')/g;

// used to support leading #!/usr/bin/env in scripts as supported in Node
var hashBangRegEx = /^\#\!.*/;

// extract CJS dependencies from source text via regex static analysis
// read require('x') statements not in comments or strings
export function getCJSDeps (source) {
  cjsRequireRegEx.lastIndex = commentRegEx.lastIndex = stringRegEx.lastIndex = 0;

  var deps = [];

  var match;

  // track string and comment locations for unminified source
  var stringLocations = [], commentLocations = [];

  function inLocation (locations, match) {
    for (var i = 0; i < locations.length; i++)
      if (locations[i][0] < match.index && locations[i][1] > match.index)
        return true;
    return false;
  }

  if (source.length / source.split('\n').length < 200) {
    while (match = stringRegEx.exec(source))
      stringLocations.push([match.index, match.index + match[0].length]);

    // TODO: track template literals here before comments

    while (match = commentRegEx.exec(source)) {
      // only track comments not starting in strings
      if (!inLocation(stringLocations, match))
        commentLocations.push([match.index + match[1].length, match.index + match[0].length - 1]);
    }
  }

  while (match = cjsRequireRegEx.exec(source)) {
    // ensure we're not within a string or comment location
    if (!inLocation(stringLocations, match) && !inLocation(commentLocations, match)) {
      var dep = match[1].substr(1, match[1].length - 2);
      // skip cases like require('" + file + "')
      if (dep.match(/"|'/))
        continue;
      deps.push(dep);
    }
  }

  return deps;
}

// Global
// bare minimum ignores
var ignoredGlobalProps = ['_g', 'sessionStorage', 'localStorage', 'clipboardData', 'frames', 'frameElement', 'external',
  'mozAnimationStartTime', 'webkitStorageInfo', 'webkitIndexedDB', 'mozInnerScreenY', 'mozInnerScreenX'];

var globalSnapshot;
function globalIterator (globalName) {
  if (ignoredGlobalProps.indexOf(globalName) !== -1)
    return;
  try {
    var value = global[globalName];
  }
  catch (e) {
    ignoredGlobalProps.push(globalName);
  }
  this(globalName, value);
}

export function getGlobalValue (exports) {
  if (typeof exports === 'string')
    return readMemberExpression(exports, global);

  if (!(exports instanceof Array))
    throw new Error('Global exports must be a string or array.');

  var globalValue = {};
  for (var i = 0; i < exports.length; i++)
    globalValue[exports[i].split('.').pop()] = readMemberExpression(exports[i], global);
  return globalValue;
}

export function prepareGlobal (moduleName, exports, globals, encapsulate) {
  // disable module detection
  var curDefine = global.define;

  global.define = undefined;

  // set globals
  var oldGlobals;
  if (globals) {
    oldGlobals = {};
    for (var g in globals) {
      oldGlobals[g] = global[g];
      global[g] = globals[g];
    }
  }

  // store a complete copy of the global object in order to detect changes
  if (!exports) {
    globalSnapshot = {};

    Object.keys(global).forEach(globalIterator, function (name, value) {
      globalSnapshot[name] = value;
    });
  }

  // return function to retrieve global
  return function () {
    var globalValue = exports ? getGlobalValue(exports) : {};

    var singleGlobal;
    var multipleExports = !!exports;

    if (!exports || encapsulate)
      Object.keys(global).forEach(globalIterator, function (name, value) {
        if (globalSnapshot[name] === value)
          return;
        if (value === undefined)
          return;

        // allow global encapsulation where globals are removed
        if (encapsulate)
          global[name] = undefined;

        if (!exports) {
          globalValue[name] = value;

          if (singleGlobal !== undefined) {
            if (!multipleExports && singleGlobal !== value)
              multipleExports = true;
          }
          else {
            singleGlobal = value;
          }
        }
      });

    globalValue = multipleExports ? globalValue : singleGlobal;

    // revert globals
    if (oldGlobals) {
      for (var g in oldGlobals)
        global[g] = oldGlobals[g];
    }
    global.define = curDefine;

    return globalValue;
  };
}

// AMD
var cjsRequirePre = "(?:^|[^$_a-zA-Z\\xA0-\\uFFFF.])";
var cjsRequirePost = "\\s*\\(\\s*(\"([^\"]+)\"|'([^']+)')\\s*\\)";
var fnBracketRegEx = /\(([^\)]*)\)/;
var wsRegEx = /^\s+|\s+$/g;

var requireRegExs = {};

function amdGetCJSDeps(source, requireIndex) {

  // remove comments
  source = source.replace(commentRegEx, '');

  // determine the require alias
  var params = source.match(fnBracketRegEx);
  var requireAlias = (params[1].split(',')[requireIndex] || 'require').replace(wsRegEx, '');

  // find or generate the regex for this requireAlias
  var requireRegEx = requireRegExs[requireAlias] || (requireRegExs[requireAlias] = new RegExp(cjsRequirePre + requireAlias + cjsRequirePost, 'g'));

  requireRegEx.lastIndex = 0;

  var deps = [];

  var match;
  while (match = requireRegEx.exec(source))
    deps.push(match[2] || match[3]);

  return deps;
}

// generate anonymous define from singular named define
var multipleNamedDefines = false;
var lastNamedDefine;
var curMetaDeps;
export function clearLastDefine (metaDeps) {
  curMetaDeps = metaDeps;
  lastNamedDefine = undefined;
  multipleNamedDefines = false;
}
export function registerLastDefine (loader) {
  if (lastNamedDefine)
    loader.registerDynamic(curMetaDeps ? lastNamedDefine[0].concat(curMetaDeps) : lastNamedDefine[0], lastNamedDefine[1]);

  // bundles are an empty module
  else if (multipleNamedDefines)
    loader.registerDynamic([], function () {});
}
<commit_msg>correct amd format handling<commit_after>
import { isWindows, global, readMemberExpression, cjsRequireRegEx } from './common.js';

export default function (loader) {
  loader.set('@@cjs-helpers', loader.newModule({
    requireResolve: requireResolve.bind(loader),
    getPathVars: getPathVars
  }));

  loader.set('@@global-helpers', loader.newModule({
    prepareGlobal: prepareGlobal
  }));

  /*
    AMD-compatible require
    To copy RequireJS, set window.require = window.requirejs = loader.amdRequire
  */
  function require (names, callback, errback, referer) {
    // in amd, first arg can be a config object... we just ignore
    if (typeof names === 'object' && !(names instanceof Array))
      return require.apply(null, Array.prototype.splice.call(arguments, 1, arguments.length - 1));

    // amd require
    if (typeof names === 'string' && typeof callback === 'function')
      names = [names];
    if (names instanceof Array) {
      var dynamicRequires = [];
      for (var i = 0; i < names.length; i++)
        dynamicRequires.push(loader.import(names[i], referer));
      Promise.all(dynamicRequires).then(function (modules) {
        for (var i = 0; i < modules.length; i++)
          modules[i] = modules[i].__useDefault ? modules[i].default : modules[i];
        if (callback)
          callback.apply(null, modules);
      }, errback);
    }

    // commonjs require
    else if (typeof names === 'string') {
      var normalized = loader.decanonicalize(names, referer);
      var module = loader.get(normalized);
      if (!module)
        throw new Error('Module not already loaded loading "' + names + '" as ' + normalized + (referer ? ' from "' + referer + '".' : '.'));
      return module.__useDefault ? module.default : module;
    }

    else
      throw new TypeError('Invalid require');
  }

  function define (name, deps, factory) {
    if (typeof name !== 'string') {
      factory = deps;
      deps = name;
      name = null;

      if (curMetaDeps) {
        deps = deps.concat(curMetaDeps);
        curMetaDeps = undefined;
      }
    }

    if (!(deps instanceof Array)) {
      factory = deps;
      deps = ['require', 'exports', 'module'].splice(0, factory.length);
    }

    if (typeof factory !== 'function')
      factory = (function (factory) {
        return function() { return factory; }
      })(factory);

    // remove system dependencies
    var requireIndex, exportsIndex, moduleIndex;

    if ((requireIndex = deps.indexOf('require')) !== -1) {

      deps.splice(requireIndex, 1);

      // only trace cjs requires for non-named
      // named defines assume the trace has already been done
      if (!name)
        deps = deps.concat(amdGetCJSDeps(factory.toString(), requireIndex));
    }

    if ((exportsIndex = deps.indexOf('exports')) !== -1)
      deps.splice(exportsIndex, 1);

    if ((moduleIndex = deps.indexOf('module')) !== -1)
      deps.splice(moduleIndex, 1);

    function execute (req, exports, module) {
      var depValues = [];
      for (var i = 0; i < deps.length; i++)
        depValues.push(req(deps[i]));

      module.uri = module.id;

      module.config = function () {};

      // add back in system dependencies
      if (moduleIndex !== -1)
        depValues.splice(moduleIndex, 0, module);

      if (exportsIndex !== -1)
        depValues.splice(exportsIndex, 0, exports);

      if (requireIndex !== -1) {
        var contextualRequire = function (names, callback, errback) {
          if (typeof names === 'string' && typeof callback !== 'function')
            return req(names);
          return require.call(loader, names, callback, errback, module.id);
        };
        contextualRequire.toUrl = function (name) {
          return loader.normalizeSync(name, module.id);
        };
        depValues.splice(requireIndex, 0, contextualRequire);
      }

      // set global require to AMD require
      var curRequire = global.require;
      global.require = require;

      var output = factory.apply(exportsIndex === -1 ? global : exports, depValues);

      global.require = curRequire;

      if (typeof output !== 'undefined')
        module.exports = output;
    }

    // anonymous define
    if (!name) {
      loader.registerDynamic(deps, execute);
    }
    else {
      loader.registerDynamic(name, deps, execute);

      // if we don't have any other defines,
      // then let this be an anonymous define
      // this is just to support single modules of the form:
      // define('jquery')
      // still loading anonymously
      // because it is done widely enough to be useful
      // as soon as there is more than one define, this gets removed though
      if (lastNamedDefine) {
        lastNamedDefine = undefined;
        multipleNamedDefines = true;
      }
      else if (!multipleNamedDefines) {
        lastNamedDefine = [deps, execute];
      }
    }
  }
  define.amd = {};

  loader.amdDefine = define;
  loader.amdRequire = require;
}

// CJS
if (typeof window !== 'undefined' && typeof document !== 'undefined' && window.location)
  var windowOrigin = location.protocol + '//' + location.hostname + (location.port ? ':' + location.port : '');

function stripOrigin(path) {
  if (path.substr(0, 8) === 'file:///')
    return path.substr(7 + !!isWindows);

  if (windowOrigin && path.substr(0, windowOrigin.length) === windowOrigin)
    return path.substr(windowOrigin.length);

  return path;
}

export function requireResolve (request, parentId) {
  return stripOrigin(this.normalizeSync(request, parentId));
}

export function getPathVars (moduleId) {
  // remove any plugin syntax
  var pluginIndex = moduleId.lastIndexOf('!');
  var filename;
  if (pluginIndex !== -1)
    filename = moduleId.substr(0, pluginIndex);
  else
    filename = moduleId;

  var dirname = filename.split('/');
  dirname.pop();
  dirname = dirname.join('/');

  return {
    filename: stripOrigin(filename),
    dirname: stripOrigin(dirname)
  };
}

var commentRegEx = /(^|[^\\])(\/\*([\s\S]*?)\*\/|([^:]|^)\/\/(.*)$)/mg;
var stringRegEx = /("[^"\\\n\r]*(\\.[^"\\\n\r]*)*"|'[^'\\\n\r]*(\\.[^'\\\n\r]*)*')/g;

// used to support leading #!/usr/bin/env in scripts as supported in Node
var hashBangRegEx = /^\#\!.*/;

// extract CJS dependencies from source text via regex static analysis
// read require('x') statements not in comments or strings
export function getCJSDeps (source) {
  cjsRequireRegEx.lastIndex = commentRegEx.lastIndex = stringRegEx.lastIndex = 0;

  var deps = [];

  var match;

  // track string and comment locations for unminified source
  var stringLocations = [], commentLocations = [];

  function inLocation (locations, match) {
    for (var i = 0; i < locations.length; i++)
      if (locations[i][0] < match.index && locations[i][1] > match.index)
        return true;
    return false;
  }

  if (source.length / source.split('\n').length < 200) {
    while (match = stringRegEx.exec(source))
      stringLocations.push([match.index, match.index + match[0].length]);

    // TODO: track template literals here before comments

    while (match = commentRegEx.exec(source)) {
      // only track comments not starting in strings
      if (!inLocation(stringLocations, match))
        commentLocations.push([match.index + match[1].length, match.index + match[0].length - 1]);
    }
  }

  while (match = cjsRequireRegEx.exec(source)) {
    // ensure we're not within a string or comment location
    if (!inLocation(stringLocations, match) && !inLocation(commentLocations, match)) {
      var dep = match[1].substr(1, match[1].length - 2);
      // skip cases like require('" + file + "')
      if (dep.match(/"|'/))
        continue;
      deps.push(dep);
    }
  }

  return deps;
}

// Global
// bare minimum ignores
var ignoredGlobalProps = ['_g', 'sessionStorage', 'localStorage', 'clipboardData', 'frames', 'frameElement', 'external',
  'mozAnimationStartTime', 'webkitStorageInfo', 'webkitIndexedDB', 'mozInnerScreenY', 'mozInnerScreenX'];

var globalSnapshot;
function globalIterator (globalName) {
  if (ignoredGlobalProps.indexOf(globalName) !== -1)
    return;
  try {
    var value = global[globalName];
  }
  catch (e) {
    ignoredGlobalProps.push(globalName);
  }
  this(globalName, value);
}

export function getGlobalValue (exports) {
  if (typeof exports === 'string')
    return readMemberExpression(exports, global);

  if (!(exports instanceof Array))
    throw new Error('Global exports must be a string or array.');

  var globalValue = {};
  for (var i = 0; i < exports.length; i++)
    globalValue[exports[i].split('.').pop()] = readMemberExpression(exports[i], global);
  return globalValue;
}

export function prepareGlobal (moduleName, exports, globals, encapsulate) {
  // disable module detection
  var curDefine = global.define;

  global.define = undefined;

  // set globals
  var oldGlobals;
  if (globals) {
    oldGlobals = {};
    for (var g in globals) {
      oldGlobals[g] = global[g];
      global[g] = globals[g];
    }
  }

  // store a complete copy of the global object in order to detect changes
  if (!exports) {
    globalSnapshot = {};

    Object.keys(global).forEach(globalIterator, function (name, value) {
      globalSnapshot[name] = value;
    });
  }

  // return function to retrieve global
  return function () {
    var globalValue = exports ? getGlobalValue(exports) : {};

    var singleGlobal;
    var multipleExports = !!exports;

    if (!exports || encapsulate)
      Object.keys(global).forEach(globalIterator, function (name, value) {
        if (globalSnapshot[name] === value)
          return;
        if (value === undefined)
          return;

        // allow global encapsulation where globals are removed
        if (encapsulate)
          global[name] = undefined;

        if (!exports) {
          globalValue[name] = value;

          if (singleGlobal !== undefined) {
            if (!multipleExports && singleGlobal !== value)
              multipleExports = true;
          }
          else {
            singleGlobal = value;
          }
        }
      });

    globalValue = multipleExports ? globalValue : singleGlobal;

    // revert globals
    if (oldGlobals) {
      for (var g in oldGlobals)
        global[g] = oldGlobals[g];
    }
    global.define = curDefine;

    return globalValue;
  };
}

// AMD
var cjsRequirePre = "(?:^|[^$_a-zA-Z\\xA0-\\uFFFF.])";
var cjsRequirePost = "\\s*\\(\\s*(\"([^\"]+)\"|'([^']+)')\\s*\\)";
var fnBracketRegEx = /\(([^\)]*)\)/;
var wsRegEx = /^\s+|\s+$/g;

var requireRegExs = {};

function amdGetCJSDeps(source, requireIndex) {

  // remove comments
  source = source.replace(commentRegEx, '');

  // determine the require alias
  var params = source.match(fnBracketRegEx);
  var requireAlias = (params[1].split(',')[requireIndex] || 'require').replace(wsRegEx, '');

  // find or generate the regex for this requireAlias
  var requireRegEx = requireRegExs[requireAlias] || (requireRegExs[requireAlias] = new RegExp(cjsRequirePre + requireAlias + cjsRequirePost, 'g'));

  requireRegEx.lastIndex = 0;

  var deps = [];

  var match;
  while (match = requireRegEx.exec(source))
    deps.push(match[2] || match[3]);

  return deps;
}

// generate anonymous define from singular named define
var multipleNamedDefines = false;
var lastNamedDefine;
var curMetaDeps;
export function clearLastDefine (metaDeps) {
  curMetaDeps = metaDeps;
  lastNamedDefine = undefined;
  multipleNamedDefines = false;
}
export function registerLastDefine (loader) {
  if (lastNamedDefine)
    loader.registerDynamic(curMetaDeps ? lastNamedDefine[0].concat(curMetaDeps) : lastNamedDefine[0], lastNamedDefine[1]);

  // bundles are an empty module
  else if (multipleNamedDefines)
    loader.registerDynamic([], function () {});
}
<|endoftext|>


<commit_before>/**
 * Date Functions available inside Mavo expressions
 */

(function($, val, _, $u = _.util) {

var s = {seconds: 1, minutes: 60};
s.hours  = s.minutes * 60;
s.days   = s.hours   * 24;
s.weeks  = s.days    * 7;
s.months = s.days    * 30.4368;
s.years  = s.weeks   * 52;

var numeric = {
	year: d => d.getFullYear(),
	month: d => d.getMonth() + 1,
	day: d => d.getDate(),
	weekday: d => d.getDay() || 7,
	hour: d => d.getHours(),
	minute: d => d.getMinutes(),
	second: d => d.getSeconds(),
	ms: d => d.getMilliseconds()
};

function isPrecision (precision) {
	if (!precision) {
		return false;
	}

	if (precision == "ms") {
		return true;
	}

	let singular = precision.replace(/s$/, "");
	let plural = precision.replace(/s?$/, "s");

	return singular in s || plural in s;
}

function parsePrecision(precision) {
	precision = precision?.trim() || "";
	let keys = Object.keys(s).reverse();
	let ret = {};

	do {
		p = keys.shift();
		ret[p] = true;
	} while(!RegExp(p + "?").test(precision) && keys.length > 0);

	if (precision == "ms") {
		ret.ms = true;
	}

	return ret;
}

$.extend(_, {
	get $now() {
		return new Date();
	},

	$startup: new Date(), // Like $now, but doesn't update

	get $today() {
		return _.date(new Date());
	},

	year: $.extend(function() {
		return $u.dateComponent("year", ...arguments);
	}, {multiValued: true}),

	month: $.extend(function() {
		return $u.dateComponent("month", ...arguments);
	}, {multiValued: true}),

	week: () => s.weeks * 1000,

	day: $.extend(function() {
		return $u.dateComponent("day", ...arguments);
	}, {multiValued: true}),

	weekday: $.extend(function() {
		return $u.dateComponent("weekday", ...arguments);
	}, {multiValued: true}),

	hour: $.extend(function() {
		return $u.dateComponent("hour", ...arguments);
	}, {multiValued: true}),

	minute: $.extend(function() {
		return $u.dateComponent("minute", ...arguments);
	}, {multiValued: true}),

	second: $.extend(function() {
		return $u.dateComponent("second", ...arguments);
	}, {multiValued: true}),

	ms: $.extend(function() {
		return $u.dateComponent("ms", ...arguments);
	}, {multiValued: true}),

	// Return an ISO date & time string
	datetime: $.extend((date, time, precision) => {
		date = $u.date(date);

		if (!date) {
			return "";
		}

		let separateTime;
		if (time !== undefined) {
			if (isPrecision(time)) {
				[time, precision] = [, time];
			}
			else {
				separateTime = true;
			}
		}

		precision ??= "minutes";
		let parts = parsePrecision(precision);
		let ret = _.date(date, precision);

		if (!parts.hours) {
			return ret; // No time
		}

		if (separateTime) {
			// If time is provided separately, and it's empty, we just return a date
			ret += Mavo.value(time) ? `T${ _.time(time, precision) }` : "";
		}
		else {
			ret += `T${_.time(date, precision)}`;
		}

		return ret;
	}, {multiValued: true}),

	// Return an ISO date
	date: $.extend((date, precision = "days") => {
		date = $u.date(date);

		if (!date) {
			return "";
		}

		let parts = parsePrecision(precision);
		let ret = [];

		if (parts.years) {
			ret.push(_.year(date));
		}

		if (parts.months) {
			ret.push(_.month(date, "00"));
		}

		if (parts.days) {
			ret.push(_.day(date, "00"));
		}

		return ret.join("-");
	}, {multiValued: true}),

	// Return an ISO time
	time: $.extend((date, precision = "minutes") => {
		date = $u.date(date);

		if (!date) {
			return "";
		}

		let parts = parsePrecision(precision);
		let ret = "";

		if (parts.hours) {
			ret += _.hour(date, "00") + ":" + (parts.minutes? _.minute(date, "00") : "00");

			if (parts.seconds) {
				ret += ":" + _.second(date, "00");

				if (parts.ms) {
					ret += "." + _.ms(date, "000");
				}
			}
		}

		return ret;
	}, {multiValued: true}),

	readable_datetime: $.extend((date, ...options) => {
		options = options.map(o => typeof o === "string" || o instanceof String? {precision: o} : o);
		options = Object.assign({}, ...options);

		let parts = parsePrecision(options.precision);
		let monthFormat = options.month || parts.days? "shortname" : "long";
		let ret = [];

		if (parts.days) {
			ret.push(_.day(date));
		}

		if (parts.months) {
			ret.push(_.month(date, monthFormat));
		}

		if (parts.years) {
			ret.push(_.year(date));
		}

		if (parts.hours) {
			ret.push(_.time(date, options.precision));
		}

		return ret.join(" ");
	}, {multiValued: true}),

	localTimezone: -(new Date()).getTimezoneOffset(),
});

_.msTo = (what, ms) => Math.floor(Math.abs(ms) / (s[what] * 1000)) || 0;

for (let unit in s) {
	_[unit] = $.extend(function(ms) {
		if (arguments.length === 0) {
			return s[unit] * 1000;
		}

		return _.msTo(unit, ms);
	}, {multiValued: true});
}

_.duration = $.extend(function (ms, terms) {
	// TODO unify code for specific unit with code for auto units to reduce repetition
	// TODO allow multiple units, e.g. ["days", "hours"]
	// TODO allow combining term # and units, e.g. start: days, terms: 2
	if (terms && isNaN(terms)) {
		// Specific term specified
		let unitSingular = terms != "ms" ? terms.replace(/s?$/, "") : terms;
		let unitPlural = terms.replace(/s?$/, "s");

		if (!(unitPlural in s)) {
			throw new TypeError(`Unknown duration unit ${terms}. Please use one of ${ Object.keys(s).join(", ") }`);
		}

		let n = Math.floor(ms / s[unitPlural] / 1000);
		let unitProperPlurality = n === 1 && unitPlural !== "ms" ? unitSingular : unitPlural;
		return n + " " + _.phrase.call(this, unitProperPlurality);
	}
	else if (ms === 0 || terms === undefined) {
		terms = 1;
	}

	let timeLeft = ms || 0;
	let ret = [];

	if (ms < 1) {
		ret = ["0 ms"];
	}
	else {
		let units = [...Object.keys(s).reverse(), "ms"];

		for (let i=0, unit; unit = units[i]; i++) {
			// get largest value of time unit for the remaining
			// time to account for
			let unitMs = unit in s? s[unit] * 1000 : 1; // number of ms in 1 unit
			let unitValue = Math.floor(timeLeft / unitMs); // quotient
			timeLeft = timeLeft % unitMs; // remainder

			if (unitValue > 0 && ret.length < terms) {
				let unitProperPlurality = unitValue === 1 && unit !== "ms" ? unit.slice(0, -1) : unit;
				ret.push(unitValue + " " + _.phrase.call(this, unitProperPlurality));
			}
			else if (ret.length > 0) {
				// Discard any further terms to avoid non-continous terms like e.g. "1 month, 10 ms"
				break;
			}
		}
	}

	return arguments.length === 1 ? ret[0] : ret;
}, {
	needsContext: true,
	multiValued: true
});

$.extend(_.util, {
	fixDateString: function(date) {
		date = date.trim();

		var hasDate = /^\d{4}-\d{2}(-\d{2})?/.test(date);
		var hasTime = date.indexOf(":") > -1;

		if (!hasDate && !hasTime) {
			return null;
		}

		// Fix up time format
		if (!hasDate) {
			// No date, add today’s
			date = _.$today + " " + date;
		}
		else {
			// Only year-month, add day
			date = date.replace(/^(\d{4}-\d{2})(?!-\d{2})/, "$1-01");
		}

		if (!hasTime) {
			// Add a time if one doesn't exist
			date += "T00:00:00";
		}
		else {
			// Make sure time starts with T, due to Safari bug
			date = date.replace(/\-(\d{2})\s+(?=\d{2}:)/, "-$1T");
		}

		// Remove all whitespace
		date = date.replace(/\s+/g, "");

		return date;
	},

	dateComponent: function(component, date, format, locale = Mavo.locale) {
		if (arguments.length === 1 && component + "s" in s) {
			return _[component + "s"]();
		}

		var dateO = $u.date(date);

		if (component === "year") {
			// Why +""? We don't want years to be formatted like 2,017!
			// Why the .match()? For incomplete dates, see #226
			date = date && date.match? date : date + "";
			var ret = dateO? dateO.getFullYear() + "" : (date.match(/\b[1-9]\d\d\b|\d+/) || [])[0];
		}

		if (!ret && !dateO) {
			return "";
		}

		var ret = ret || numeric[component](dateO);

		if (format) {
			if (/^0+$/.test(format)) {
				// Leading zeroes
				return (ret + "").padStart(format.length, "0").slice(-format.length);
			}
			else {
				format = {name: "long", shortname: "short"}[format] || format;
				ret = dateO.toLocaleString(locale, {[component]: format});
				ret = ret.replace(/\u200e/g, ""); // Stupid Edge bug

				return ret;
			}
		}

		return component === "year"? ret : +ret;
	},

	date: function(date) {
		date = val(date);

		if (!date) {
			return null;
		}

		var object = new Date(date);

		// Either arg is not string or is exactly the same as a re-serialization of it as a date
		if ($.type(date) !== "string" || !isNaN(object) && (object + "" == date)) {
			return object;
		}

		date = $u.fixDateString(date);

		if (date === null) {
			return null;
		}

		var timezone = date.match(/[+-]\d{2}:?\d{2}|Z$/)?.[0];

		if (timezone) {
			// parse as ISO format
			date = new Date(date);
		}
		else {
			// construct date in local timezone
			var fields = date.match(/\d+/g);

			date = new Date(
				// year, month, date,
				fields[0], (fields[1] || 1) - 1, fields[2] || 1,
				// hours, minutes, seconds, milliseconds,
				fields[3] || 0, fields[4] || 0, fields[5] || 0, fields[6] || 0
			);
		}

		return isNaN(date)? null : date;
	}
});

})(Bliss, Mavo.value, Mavo.Functions);
<commit_msg>Negative inputs to `duration` now produce negative time outputs (#930)<commit_after>/**
 * Date Functions available inside Mavo expressions
 */

(function($, val, _, $u = _.util) {

var s = {seconds: 1, minutes: 60};
s.hours  = s.minutes * 60;
s.days   = s.hours   * 24;
s.weeks  = s.days    * 7;
s.months = s.days    * 30.4368;
s.years  = s.weeks   * 52;

var numeric = {
	year: d => d.getFullYear(),
	month: d => d.getMonth() + 1,
	day: d => d.getDate(),
	weekday: d => d.getDay() || 7,
	hour: d => d.getHours(),
	minute: d => d.getMinutes(),
	second: d => d.getSeconds(),
	ms: d => d.getMilliseconds()
};

function isPrecision (precision) {
	if (!precision) {
		return false;
	}

	if (precision == "ms") {
		return true;
	}

	let singular = precision.replace(/s$/, "");
	let plural = precision.replace(/s?$/, "s");

	return singular in s || plural in s;
}

function parsePrecision(precision) {
	precision = precision?.trim() || "";
	let keys = Object.keys(s).reverse();
	let ret = {};

	do {
		p = keys.shift();
		ret[p] = true;
	} while(!RegExp(p + "?").test(precision) && keys.length > 0);

	if (precision == "ms") {
		ret.ms = true;
	}

	return ret;
}

$.extend(_, {
	get $now() {
		return new Date();
	},

	$startup: new Date(), // Like $now, but doesn't update

	get $today() {
		return _.date(new Date());
	},

	year: $.extend(function() {
		return $u.dateComponent("year", ...arguments);
	}, {multiValued: true}),

	month: $.extend(function() {
		return $u.dateComponent("month", ...arguments);
	}, {multiValued: true}),

	week: () => s.weeks * 1000,

	day: $.extend(function() {
		return $u.dateComponent("day", ...arguments);
	}, {multiValued: true}),

	weekday: $.extend(function() {
		return $u.dateComponent("weekday", ...arguments);
	}, {multiValued: true}),

	hour: $.extend(function() {
		return $u.dateComponent("hour", ...arguments);
	}, {multiValued: true}),

	minute: $.extend(function() {
		return $u.dateComponent("minute", ...arguments);
	}, {multiValued: true}),

	second: $.extend(function() {
		return $u.dateComponent("second", ...arguments);
	}, {multiValued: true}),

	ms: $.extend(function() {
		return $u.dateComponent("ms", ...arguments);
	}, {multiValued: true}),

	// Return an ISO date & time string
	datetime: $.extend((date, time, precision) => {
		date = $u.date(date);

		if (!date) {
			return "";
		}

		let separateTime;
		if (time !== undefined) {
			if (isPrecision(time)) {
				[time, precision] = [, time];
			}
			else {
				separateTime = true;
			}
		}

		precision ??= "minutes";
		let parts = parsePrecision(precision);
		let ret = _.date(date, precision);

		if (!parts.hours) {
			return ret; // No time
		}

		if (separateTime) {
			// If time is provided separately, and it's empty, we just return a date
			ret += Mavo.value(time) ? `T${ _.time(time, precision) }` : "";
		}
		else {
			ret += `T${_.time(date, precision)}`;
		}

		return ret;
	}, {multiValued: true}),

	// Return an ISO date
	date: $.extend((date, precision = "days") => {
		date = $u.date(date);

		if (!date) {
			return "";
		}

		let parts = parsePrecision(precision);
		let ret = [];

		if (parts.years) {
			ret.push(_.year(date));
		}

		if (parts.months) {
			ret.push(_.month(date, "00"));
		}

		if (parts.days) {
			ret.push(_.day(date, "00"));
		}

		return ret.join("-");
	}, {multiValued: true}),

	// Return an ISO time
	time: $.extend((date, precision = "minutes") => {
		date = $u.date(date);

		if (!date) {
			return "";
		}

		let parts = parsePrecision(precision);
		let ret = "";

		if (parts.hours) {
			ret += _.hour(date, "00") + ":" + (parts.minutes? _.minute(date, "00") : "00");

			if (parts.seconds) {
				ret += ":" + _.second(date, "00");

				if (parts.ms) {
					ret += "." + _.ms(date, "000");
				}
			}
		}

		return ret;
	}, {multiValued: true}),

	readable_datetime: $.extend((date, ...options) => {
		options = options.map(o => typeof o === "string" || o instanceof String? {precision: o} : o);
		options = Object.assign({}, ...options);

		let parts = parsePrecision(options.precision);
		let monthFormat = options.month || parts.days? "shortname" : "long";
		let ret = [];

		if (parts.days) {
			ret.push(_.day(date));
		}

		if (parts.months) {
			ret.push(_.month(date, monthFormat));
		}

		if (parts.years) {
			ret.push(_.year(date));
		}

		if (parts.hours) {
			ret.push(_.time(date, options.precision));
		}

		return ret.join(" ");
	}, {multiValued: true}),

	localTimezone: -(new Date()).getTimezoneOffset(),
});

_.msTo = (what, ms) => Math.floor(Math.abs(ms) / (s[what] * 1000)) || 0;

for (let unit in s) {
	_[unit] = $.extend(function(ms) {
		if (arguments.length === 0) {
			return s[unit] * 1000;
		}

		return _.msTo(unit, ms);
	}, {multiValued: true});
}

_.duration = $.extend(function (ms, terms) {
	// TODO unify code for specific unit with code for auto units to reduce repetition
	// TODO allow multiple units, e.g. ["days", "hours"]
	// TODO allow combining term # and units, e.g. start: days, terms: 2

	let negativeMultiplier = ms < 0 ? -1 : 1; // a multiplier to convert result to negative if needed
	ms = Math.abs(ms); // negative works same way as positive does, just adding negative sign in the front

	if (terms && isNaN(terms)) {
		// Specific term specified
		let unitSingular = terms != "ms" ? terms.replace(/s?$/, "") : terms;
		let unitPlural = terms.replace(/s?$/, "s");

		if (!(unitPlural in s)) {
			throw new TypeError(`Unknown duration unit ${terms}. Please use one of ${ Object.keys(s).join(", ") }`);
		}

		let n = Math.floor(ms / s[unitPlural] / 1000);
		let unitProperPlurality = n === 1 && unitPlural !== "ms" ? unitSingular : unitPlural;
		return negativeMultiplier * n + " " + _.phrase.call(this, unitProperPlurality);
	}
	else if (ms == 0 || terms === undefined) {
		terms = 1;
	}

	let timeLeft = ms;
	let ret = [];

	if (ms == 0) {
		ret = ["0 ms"];
	}
	else {
		let units = [...Object.keys(s).reverse(), "ms"];

		for (let i=0, unit; unit = units[i]; i++) {
			// get largest value of time unit for the remaining
			// time to account for
			let unitMs = unit in s? s[unit] * 1000 : 1; // number of ms in 1 unit
			let unitValue = Math.floor(timeLeft / unitMs); // quotient
			timeLeft = timeLeft % unitMs; // remainder

			if (unitValue > 0 && ret.length < terms) {
				let unitProperPlurality = unitValue === 1 && unit !== "ms" ? unit.slice(0, -1) : unit;
				ret.push(negativeMultiplier * unitValue + " " + _.phrase.call(this, unitProperPlurality));
			}
			else if (ret.length > 0) {
				// Discard any further terms to avoid non-continous terms like e.g. "1 month, 10 ms"
				break;
			}
		}
	}

	return arguments.length === 1 ? ret[0] : ret;
}, {
	needsContext: true,
	multiValued: true
});

$.extend(_.util, {
	fixDateString: function(date) {
		date = date.trim();

		var hasDate = /^\d{4}-\d{2}(-\d{2})?/.test(date);
		var hasTime = date.indexOf(":") > -1;

		if (!hasDate && !hasTime) {
			return null;
		}

		// Fix up time format
		if (!hasDate) {
			// No date, add today’s
			date = _.$today + " " + date;
		}
		else {
			// Only year-month, add day
			date = date.replace(/^(\d{4}-\d{2})(?!-\d{2})/, "$1-01");
		}

		if (!hasTime) {
			// Add a time if one doesn't exist
			date += "T00:00:00";
		}
		else {
			// Make sure time starts with T, due to Safari bug
			date = date.replace(/\-(\d{2})\s+(?=\d{2}:)/, "-$1T");
		}

		// Remove all whitespace
		date = date.replace(/\s+/g, "");

		return date;
	},

	dateComponent: function(component, date, format, locale = Mavo.locale) {
		if (arguments.length === 1 && component + "s" in s) {
			return _[component + "s"]();
		}

		var dateO = $u.date(date);

		if (component === "year") {
			// Why +""? We don't want years to be formatted like 2,017!
			// Why the .match()? For incomplete dates, see #226
			date = date && date.match? date : date + "";
			var ret = dateO? dateO.getFullYear() + "" : (date.match(/\b[1-9]\d\d\b|\d+/) || [])[0];
		}

		if (!ret && !dateO) {
			return "";
		}

		var ret = ret || numeric[component](dateO);

		if (format) {
			if (/^0+$/.test(format)) {
				// Leading zeroes
				return (ret + "").padStart(format.length, "0").slice(-format.length);
			}
			else {
				format = {name: "long", shortname: "short"}[format] || format;
				ret = dateO.toLocaleString(locale, {[component]: format});
				ret = ret.replace(/\u200e/g, ""); // Stupid Edge bug

				return ret;
			}
		}

		return component === "year"? ret : +ret;
	},

	date: function(date) {
		date = val(date);

		if (!date) {
			return null;
		}

		var object = new Date(date);

		// Either arg is not string or is exactly the same as a re-serialization of it as a date
		if ($.type(date) !== "string" || !isNaN(object) && (object + "" == date)) {
			return object;
		}

		date = $u.fixDateString(date);

		if (date === null) {
			return null;
		}

		var timezone = date.match(/[+-]\d{2}:?\d{2}|Z$/)?.[0];

		if (timezone) {
			// parse as ISO format
			date = new Date(date);
		}
		else {
			// construct date in local timezone
			var fields = date.match(/\d+/g);

			date = new Date(
				// year, month, date,
				fields[0], (fields[1] || 1) - 1, fields[2] || 1,
				// hours, minutes, seconds, milliseconds,
				fields[3] || 0, fields[4] || 0, fields[5] || 0, fields[6] || 0
			);
		}

		return isNaN(date)? null : date;
	}
});

})(Bliss, Mavo.value, Mavo.Functions);
<|endoftext|>


<commit_before>define(['ash', 'game/worldcreator/WorldCreatorHelper'], function (Ash, WorldCreatorHelper) {
    var GameState = Ash.Class.extend({

        constructor: function () {
            this.reset();
        },

        reset: function () {
            this.level = 0;
            this.worldSeed = 0;
            this.gameStartTimeStamp = 0;
            this.gamePlayedSeconds = 0;
            this.isPaused = false;
            this.numCamps = 0;
            this.numTradePostCamps = 0;
            this.numVisitedSectors = 0;
            this.isFinished = false;

            this.unlockedFeatures = {
                scavenge: false,
                scout: false,
                vision: false,
                camp: false,
                fight: false,
                investigate: false,
                bag: false,
                upgrades: false,
                projects: false,
                blueprints: false,
                resources: {
                    food: false,
                    water: false,
                    metal: false,
                    rope: false,
                    herbs: false,
                    fuel: false,
                    medicine: false,
                    tools: false,
                    concrete: false,
                },
                sectors: false,
                levels: false,
                trade: false,
                followers: false,
                favour: false,
                evidence: false,
                currency: false,
            };

            this.uiStatus = {
                currentTab: null,
                mapVisited: false,
                isHidden: false,
                leaveCampRes: {},
                leaveCampItems: {},
            };

            this.uiBagStatus = {
                itemsOwnedSeen: [],
                itemsCraftableUnlockedSeen: [],
                itemsCraftableAvailableSeen: []
            },

            this.foundTradingPartners = []; // camp ordinals

            this.actionCooldownEndTimestamps = {};
            this.actionDurationEndTimestamps = {};
        },

        passTime: function (seconds) {
            var cooldownkeys = Object.keys(this.actionCooldownEndTimestamps);
            for (var i = 0; i < cooldownkeys.length; i++) {
                this.actionCooldownEndTimestamps[cooldownkeys[i]] = this.actionCooldownEndTimestamps[cooldownkeys[i]] - seconds * 1000;
            }
        },

        getLevelOrdinal: function (level) {
            return WorldCreatorHelper.getLevelOrdinal(this.worldSeed, level);
        },

        getLevelForOrdinal: function (levelOrdinal) {
            return WorldCreatorHelper.getLevelForOrdinal(this.worldSeed, levelOrdinal);
        },

        getCampOrdinal: function (level) {
            return WorldCreatorHelper.getCampOrdinal(this.worldSeed, level);
        },

        getLevelOrdinalForCampOrdinal: function (campOrdinal) {
            return WorldCreatorHelper.getLevelOrdinalForCampOrdinal(this.worldSeed, campOrdinal);
        },

        getTotalLevels: function () {
            return WorldCreatorHelper.getHighestLevel(this.worldSeed) - WorldCreatorHelper.getBottomLevel(this.worldSeed) + 1;
        },

        getGroundLevel: function () {
            return WorldCreatorHelper.getBottomLevel(this.worldSeed);
        },

        getGroundLevelOrdinal: function () {
            return WorldCreatorHelper.getLevelOrdinal(this.worldSeed, WorldCreatorHelper.getBottomLevel(this.worldSeed));
        },

        getSurfaceLevel: function () {
            return WorldCreatorHelper.getHighestLevel(this.worldSeed);
        },

        getSurfaceLevelOrdinal: function () {
            return WorldCreatorHelper.getLevelOrdinal(this.worldSeed, WorldCreatorHelper.getHighestLevel(this.worldSeed));
        },

        setActionCooldown: function (action, key, cooldown) {
            var actionKey = action;
            if (key.length > 0) actionKey += "-" + key;
            this.actionCooldownEndTimestamps[actionKey] = new Date().getTime() + cooldown * 1000;
        },

        getActionCooldown: function (action, key) {
            var actionKey = action;
            if (key.length > 0) actionKey += "-" + key;
            var timestamp = this.actionCooldownEndTimestamps[actionKey];
            if (timestamp) {
                var now = new Date().getTime();
                var diff = timestamp - now;
                if (diff > 0) return timestamp - now;
            }
            return 0;
        },

        setActionDuration: function (action, key, duration) {
            var actionKey = action;
            if (key.length > 0) actionKey += "-" + key;
            this.actionDurationEndTimestamps[actionKey] = new Date().getTime() + duration * 1000;
        },

        getActionDuration: function (action, key) {
            var actionKey = action;
            if (key.length > 0) actionKey += "-" + key;
            var timestamp = this.actionDurationEndTimestamps[actionKey];
            if (timestamp) {
                var now = new Date().getTime();
                var diff = timestamp - now;
                if (diff > 0) return timestamp - now;
            }
            return 0;
        },

        getActionLocationKey: function (isLocationAction, playerPos) {
            var locationKey = "";
            if (isLocationAction) locationKey = playerPos.level + "-" + playerPos.sectorId();
            return locationKey;
        }

    });

    return GameState;
});
<commit_msg>[save] clean up save by removing old action cooldowns<commit_after>define(['ash', 'game/worldcreator/WorldCreatorHelper'], function (Ash, WorldCreatorHelper) {
    var GameState = Ash.Class.extend({

        constructor: function () {
            this.reset();
        },

        reset: function () {
            this.level = 0;
            this.worldSeed = 0;
            this.gameStartTimeStamp = 0;
            this.gamePlayedSeconds = 0;
            this.isPaused = false;
            this.numCamps = 0;
            this.numTradePostCamps = 0;
            this.numVisitedSectors = 0;
            this.isFinished = false;

            this.unlockedFeatures = {
                scavenge: false,
                scout: false,
                vision: false,
                camp: false,
                fight: false,
                investigate: false,
                bag: false,
                upgrades: false,
                projects: false,
                blueprints: false,
                resources: {
                    food: false,
                    water: false,
                    metal: false,
                    rope: false,
                    herbs: false,
                    fuel: false,
                    medicine: false,
                    tools: false,
                    concrete: false,
                },
                sectors: false,
                levels: false,
                trade: false,
                followers: false,
                favour: false,
                evidence: false,
                currency: false,
            };

            this.uiStatus = {
                currentTab: null,
                mapVisited: false,
                isHidden: false,
                leaveCampRes: {},
                leaveCampItems: {},
            };

            this.uiBagStatus = {
                itemsOwnedSeen: [],
                itemsCraftableUnlockedSeen: [],
                itemsCraftableAvailableSeen: []
            },

            this.foundTradingPartners = []; // camp ordinals

            this.actionCooldownEndTimestamps = {};
            this.actionDurationEndTimestamps = {};
        },

        passTime: function (seconds) {
            var cooldownkeys = Object.keys(this.actionCooldownEndTimestamps);
            for (var i = 0; i < cooldownkeys.length; i++) {
                this.actionCooldownEndTimestamps[cooldownkeys[i]] = this.actionCooldownEndTimestamps[cooldownkeys[i]] - seconds * 1000;
            }
        },

        getLevelOrdinal: function (level) {
            return WorldCreatorHelper.getLevelOrdinal(this.worldSeed, level);
        },

        getLevelForOrdinal: function (levelOrdinal) {
            return WorldCreatorHelper.getLevelForOrdinal(this.worldSeed, levelOrdinal);
        },

        getCampOrdinal: function (level) {
            return WorldCreatorHelper.getCampOrdinal(this.worldSeed, level);
        },

        getLevelOrdinalForCampOrdinal: function (campOrdinal) {
            return WorldCreatorHelper.getLevelOrdinalForCampOrdinal(this.worldSeed, campOrdinal);
        },

        getTotalLevels: function () {
            return WorldCreatorHelper.getHighestLevel(this.worldSeed) - WorldCreatorHelper.getBottomLevel(this.worldSeed) + 1;
        },

        getGroundLevel: function () {
            return WorldCreatorHelper.getBottomLevel(this.worldSeed);
        },

        getGroundLevelOrdinal: function () {
            return WorldCreatorHelper.getLevelOrdinal(this.worldSeed, WorldCreatorHelper.getBottomLevel(this.worldSeed));
        },

        getSurfaceLevel: function () {
            return WorldCreatorHelper.getHighestLevel(this.worldSeed);
        },

        getSurfaceLevelOrdinal: function () {
            return WorldCreatorHelper.getLevelOrdinal(this.worldSeed, WorldCreatorHelper.getHighestLevel(this.worldSeed));
        },

        setActionCooldown: function (action, key, cooldown) {
            this.pruneActionCooldowns();
            var actionKey = action;
            if (key.length > 0) actionKey += "-" + key;
            this.actionCooldownEndTimestamps[actionKey] = new Date().getTime() + cooldown * 1000;
        },

        getActionCooldown: function (action, key) {
            var actionKey = action;
            if (key.length > 0) actionKey += "-" + key;
            var timestamp = this.actionCooldownEndTimestamps[actionKey];
            if (timestamp) {
                var now = new Date().getTime();
                var diff = timestamp - now;
                if (diff > 0) return timestamp - now;
            }
            return 0;
        },

        pruneActionCooldowns: function () {
            var now = new Date().getTime();
            var cooldownkeys = Object.keys(this.actionCooldownEndTimestamps);
            for (var i = 0; i < cooldownkeys.length; i++) {
                var key = cooldownkeys[i];
                var timestamp = this.actionCooldownEndTimestamps[key];
                var diff = timestamp - now;
                if (diff < -30) delete this.actionCooldownEndTimestamps[key];
            }
        },

        setActionDuration: function (action, key, duration) {
            var actionKey = action;
            if (key.length > 0) actionKey += "-" + key;
            this.actionDurationEndTimestamps[actionKey] = new Date().getTime() + duration * 1000;
        },

        getActionDuration: function (action, key) {
            var actionKey = action;
            if (key.length > 0) actionKey += "-" + key;
            var timestamp = this.actionDurationEndTimestamps[actionKey];
            if (timestamp) {
                var now = new Date().getTime();
                var diff = timestamp - now;
                if (diff > 0) return timestamp - now;
            }
            return 0;
        },

        getActionLocationKey: function (isLocationAction, playerPos) {
            var locationKey = "";
            if (isLocationAction) locationKey = playerPos.level + "-" + playerPos.sectorId();
            return locationKey;
        }

    });

    return GameState;
});
<|endoftext|>