<commit_before>
import Entity from './entity';

const RG = require('./rg.js');
RG.Component = require('./component.js');
RG.Brain = require('./brain.js');
RG.Brain.Player = require('./brain.player.js');
RG.Inv = require('./inv.js');
RG.Spell = require('./spell.js');
const Mixin = require('./mixin');

RG.Actor = {};

/* Object representing a game actor who takes actions.  */
class RGActorRogue extends Mixin.Locatable(Mixin.Typed(Entity)) {
    constructor(name) { // {{{2
        super({propType: RG.TYPE_ACTOR, type: null});

        this._brain = new RG.Brain.Rogue(this);
        this._brain.getMemory().addEnemyType('player');

        this._name = name;
        this._isPlayer = false;
        this._fovRange = RG.FOV_RANGE;

        this._invEq = new RG.Inv.Inventory(this);
        this._maxWeight = 10.0;

        // Components for this entity
        this.add('Action', new RG.Component.Action());
        this.add('Experience', new RG.Component.Experience());
        this.add('Combat', new RG.Component.Combat());
        this.add('Stats', new RG.Component.Stats());
        this.add('Health', new RG.Component.Health(50));

    }

    setName(name) {this._name = name;}
    getName() {return this._name;}

    /* Returns true if actor is a player.*/
    isPlayer() {return this._isPlayer;}

    getFOVRange() {
        let range = this._fovRange;
        if (this.has('EagleEye')) {range += 2;}
        return range;
    }

    setFOVRange(range) {
        this._fovRange = range;
    }

    //---------------------------------
    // Brain-related methods
    //---------------------------------

    addEnemy(actor) {this._brain.addEnemy(actor);}

    isEnemy(actor) {
        return this._brain.getMemory().isEnemy(actor);
    }

    getBrain() {return this._brain;}

    setBrain(brain) {
        this._brain = brain;
        this._brain.setActor(this);
    }

    //---------------------------------
    // Equipment related methods
    //---------------------------------

    getInvEq() { return this._invEq; }

    /* Returns weapon that is wielded by the actor.*/
    getWeapon() {return this._invEq.getWeapon();}

    /* Returns weapon that is wielded by the actor.*/
    getMissileWeapon() {
        return this._invEq.getMissileWeapon();
    }

    /* Returns missile equipped by the player.*/
    getMissile() {
        return this._invEq.getEquipment().getItem('missile');
    }

    getEquipAttack() {
        return this._invEq.getEquipment().getAttack();
    }

    getEquipDefense() {
        return this._invEq.getEquipment().getDefense();
    }

    getEquipProtection() {
        return this._invEq.getEquipment().getProtection();
    }

    setActorClass(classObj) {
        this._actorClass = classObj;
    }

    getActorClass() {
        return this._actorClass;
    }

    setBook(book) {
        this._spellbook = book;
    }

    getBook() {
        return this._spellbook;
    }

    /* Returns carrying capacity of the actor.*/
    getMaxWeight() {
        const statStr = this.get('Stats').getStrength();
        const eqStr = this._invEq.getEquipment().getStrength();
        return 2 * statStr + 2 * eqStr + this._maxWeight;
    }

    /* Marks actor as player. Cannot unset player.*/
    setIsPlayer(isPlayer) {
        if (isPlayer) {
            this._isPlayer = isPlayer;
            this._brain = new RG.Brain.Player(this);
            this.setType('player');
            this.add('StatsMods', new RG.Component.StatsMods());
            this.add('CombatMods', new RG.Component.CombatMods());

            this._spellbook = new RG.Spell.SpellBook(this);
            RG.Spell.addAllSpells(this._spellbook);

            this.add('SpellPower', new RG.Component.SpellPower());
            this.get('SpellPower').setPP(100);
        }
        else {
            RG.err('Actor.Rogue', 'setIsPlayer',
                'Actor cannot be changed from player to mob.');
        }
    }

    /* Used when controlling other actors the "real player" actor .*/
    setPlayerCtrl(isPlayer) {
        if (isPlayer) {
            this._isPlayer = true;
            this._brain = new RG.Brain.Player(this);
            if (!this.has('StatsMods')) {
                this.add('StatsMods', new RG.Component.StatsMods());
            }
            if (!this.has('CombatMods')) {
                this.add('CombatMods', new RG.Component.CombatMods());
            }
        }
        else {
            this._isPlayer = false;
            this.remove('StatsMods');
            this.remove('CombatMods');
        }
    }

    /* Returns the next action for this actor.*/
    nextAction(obj) {
        // Use actor brain to determine the action
        const cb = this._brain.decideNextAction(obj);
        let action = null;

        if (cb !== null) {
            const speed = this.getSpeed();
            const duration = parseInt(
                RG.BASE_SPEED / speed * RG.ACTION_DUR, 10);
            action = new RG.Time.RogueAction(duration, cb, {});
        }
        else {
            action = new RG.Time.RogueAction(0, () => {}, {});
        }

        if (this._brain.hasOwnProperty('energy')) {
            action.energy = this._brain.energy;
        }
        action.actor = this;
        return action;
    }

    /* Returns the cell where this actor is located at.*/
    getCell() {
        const x = this.getX();
        const y = this.getY();
        return this.getLevel().getMap().getCell(x, y);
    }

    toJSON() {
        let levelID = null;
        if (this.getLevel()) {
            levelID = this.getLevel().getID();
        }
        const obj = {
            id: this.getID(),
            name: this.getName(),
            type: this.getType(),
            x: this.getX(),
            y: this.getY(),
            fovRange: this.getFOVRange(),
            levelID,
            components: {
                Combat: this.get('Combat').toJSON(),
                Experience: this.get('Experience').toJSON(),
                Health: this.get('Health').toJSON(),
                Stats: this.get('Stats').toJSON()
            },
            inventory: this.getInvEq().getInventory().toJSON(),
            equipment: this.getInvEq().getEquipment().toJSON(),
            brain: this._brain.toJSON()
        };

        /* TODO: Using this crashes the game unfortunately
        const components = {};
        const thisComps = this.getComponents();
        Object.keys(thisComps).forEach(name => {
            components[thisComps[name].getType()] = thisComps[name].toJSON();
        });
        obj.components = components;
        */

        const simpleComps = ['Hunger', 'Flying', 'Defender', 'Attacker',
            'CounterAttack', 'BiDirStrike', 'MasterEquipper', 'SpellPower',
            'SpiritItemCrafter'];
        simpleComps.forEach(compName => {
            if (this.has(compName)) {
                obj.components[compName] = this.get(compName).toJSON();
            }
        });

        if (obj.type === null) {
            RG.err('Actor.Rogue', 'toJSON',
                `Type null for ${JSON.stringify(obj)}`);
        }

        if (this._spellbook) {
            obj.spellbook = this._spellbook.toJSON();
        }

        return obj;
    }

    //---------------------------------
    // Combat-related methods
    //---------------------------------

    getAttack() {
        let attack = this.get('Combat').getAttack();
        attack += this.getEquipAttack();

        if (this.has('CombatMods')) {
            attack += this.get('CombatMods').getAttack();
        }

        attack += Math.floor(this.getAccuracy() / 2);
        return attack;
    }

    getDefense() {
        let defense = this.get('Combat').getDefense();
        defense += this.getEquipDefense();
        if (this.has('CombatMods')) {
            defense += this.get('CombatMods').getDefense();
        }
        defense += Math.floor(this.getAgility() / 2);
        return defense;
    }

    getProtection() {
        let protection = this.get('Combat').getProtection();
        protection += this.getEquipProtection();
        if (this.has('CombatMods')) {
            protection += this.get('CombatMods').getProtection();
        }
        return protection;
    }

    getDamage() {
        let damage = this.get('Combat').rollDamage();
        let strength = this.getStrength();
        strength += this.getInvEq().getEquipment().getStrength();
        damage += RG.strengthToDamage(strength);
        if (this.has('CombatMods')) {
            damage += this.get('CombatMods').getDamage();
        }
        return damage;

    }

    //-------------------------------------------------------------
    // Stats-related methods (these take eq and boosts into account
    //-------------------------------------------------------------

    getAccuracy() {
        let acc = this.get('Stats').getAccuracy();
        acc += this.getInvEq().getEquipment().getAccuracy();
        if (this.has('StatsMods')) {acc += this.get('StatsMods').getAccuracy();}
        return acc;
    }

    getAgility() {
        let agi = this.get('Stats').getAgility();
        agi += this.getInvEq().getEquipment().getAgility();
        if (this.has('StatsMods')) {agi += this.get('StatsMods').getAgility();}
        return agi;
    }

    getStrength() {
        let str = this.get('Stats').getStrength();
        str += this.getInvEq().getEquipment().getStrength();
        if (this.has('StatsMods')) {str += this.get('StatsMods').getStrength();}
        return str;
    }

    getWillpower() {
        let wil = this.get('Stats').getWillpower();
        wil += this.getInvEq().getEquipment().getWillpower();
        if (this.has('StatsMods')) {
            wil += this.get('StatsMods').getWillpower();
        }
        return wil;
    }

    getSpeed() {
        let speed = this.get('Stats').getSpeed();
        speed += this.getInvEq().getEquipment().getSpeed();
        if (this.has('StatsMods')) {speed += this.get('StatsMods').getSpeed();}
        return speed;
    }

    getPerception() {
        let per = this.get('Stats').getPerception();
        per += this.getInvEq().getEquipment().getPerception();
        if (this.has('StatsMods')) {
            per += this.get('StatsMods').getPerception();
        }
        return per;
    }

    getMagic() {
        let per = this.get('Stats').getMagic();
        per += this.getInvEq().getEquipment().getMagic();
        if (this.has('StatsMods')) {per += this.get('StatsMods').getMagic();}
        return per;
    }
}

RG.Actor.Rogue = RGActorRogue;

/* Spirit actors. They have Ethereal component and cannot be attacked, but they
 * can be captured by SpiritGem-objects.*/
class RGActorSpirit extends RGActorRogue {

    constructor(name) {
        super(name);
        this.setType('spirit');
        this.add('Ethereal', new RG.Component.Ethereal());
        this.setBrain(new RG.Brain.Spirit(this));
    }

}

RG.Actor.Spirit = RGActorSpirit;

module.exports = RG.Actor;
<commit_msg>Trainer and ActorClass are now added in Actor.toJSON().<commit_after>
import Entity from './entity';

const RG = require('./rg.js');
RG.Component = require('./component.js');
RG.Brain = require('./brain.js');
RG.Brain.Player = require('./brain.player.js');
RG.Inv = require('./inv.js');
RG.Spell = require('./spell.js');
const Mixin = require('./mixin');

RG.Actor = {};

/* Object representing a game actor who takes actions.  */
class RGActorRogue extends Mixin.Locatable(Mixin.Typed(Entity)) {
    constructor(name) { // {{{2
        super({propType: RG.TYPE_ACTOR, type: null});

        this._brain = new RG.Brain.Rogue(this);
        this._brain.getMemory().addEnemyType('player');

        this._name = name;
        this._isPlayer = false;
        this._fovRange = RG.FOV_RANGE;

        this._invEq = new RG.Inv.Inventory(this);
        this._maxWeight = 10.0;

        // Components for this entity
        this.add('Action', new RG.Component.Action());
        this.add('Experience', new RG.Component.Experience());
        this.add('Combat', new RG.Component.Combat());
        this.add('Stats', new RG.Component.Stats());
        this.add('Health', new RG.Component.Health(50));

    }

    setName(name) {this._name = name;}
    getName() {return this._name;}

    /* Returns true if actor is a player.*/
    isPlayer() {return this._isPlayer;}

    getFOVRange() {
        let range = this._fovRange;
        if (this.has('EagleEye')) {range += 2;}
        return range;
    }

    setFOVRange(range) {
        this._fovRange = range;
    }

    //---------------------------------
    // Brain-related methods
    //---------------------------------

    addEnemy(actor) {this._brain.addEnemy(actor);}

    isEnemy(actor) {
        return this._brain.getMemory().isEnemy(actor);
    }

    getBrain() {return this._brain;}

    setBrain(brain) {
        this._brain = brain;
        this._brain.setActor(this);
    }

    //---------------------------------
    // Equipment related methods
    //---------------------------------

    getInvEq() { return this._invEq; }

    /* Returns weapon that is wielded by the actor.*/
    getWeapon() {return this._invEq.getWeapon();}

    /* Returns weapon that is wielded by the actor.*/
    getMissileWeapon() {
        return this._invEq.getMissileWeapon();
    }

    /* Returns missile equipped by the player.*/
    getMissile() {
        return this._invEq.getEquipment().getItem('missile');
    }

    getEquipAttack() {
        return this._invEq.getEquipment().getAttack();
    }

    getEquipDefense() {
        return this._invEq.getEquipment().getDefense();
    }

    getEquipProtection() {
        return this._invEq.getEquipment().getProtection();
    }

    setActorClass(classObj) {
        this._actorClass = classObj;
    }

    getActorClass() {
        return this._actorClass;
    }

    setBook(book) {
        this._spellbook = book;
    }

    getBook() {
        return this._spellbook;
    }

    /* Returns carrying capacity of the actor.*/
    getMaxWeight() {
        const statStr = this.get('Stats').getStrength();
        const eqStr = this._invEq.getEquipment().getStrength();
        return 2 * statStr + 2 * eqStr + this._maxWeight;
    }

    /* Marks actor as player. Cannot unset player.*/
    setIsPlayer(isPlayer) {
        if (isPlayer) {
            this._isPlayer = isPlayer;
            this._brain = new RG.Brain.Player(this);
            this.setType('player');
            this.add(new RG.Component.StatsMods());
            this.add(new RG.Component.CombatMods());
            this.add(new RG.Component.SpellPower());
        }
        else {
            RG.err('Actor.Rogue', 'setIsPlayer',
                'Actor cannot be changed from player to mob.');
        }
    }

    /* Used when controlling other actors with the "real player" actor .*/
    setPlayerCtrl(isPlayer) {
        if (isPlayer) {
            this._isPlayer = true;
            this._brain = new RG.Brain.Player(this);
            if (!this.has('StatsMods')) {
                this.add(new RG.Component.StatsMods());
            }
            if (!this.has('CombatMods')) {
                this.add(new RG.Component.CombatMods());
            }
        }
        else {
            this._isPlayer = false;
            this.remove('StatsMods');
            this.remove('CombatMods');
        }
    }

    /* Returns the next action for this actor.*/
    nextAction(obj) {
        // Use actor brain to determine the action
        const cb = this._brain.decideNextAction(obj);
        let action = null;

        if (cb !== null) {
            const speed = this.getSpeed();
            const duration = parseInt(
                RG.BASE_SPEED / speed * RG.ACTION_DUR, 10);
            action = new RG.Time.RogueAction(duration, cb, {});
        }
        else {
            action = new RG.Time.RogueAction(0, () => {}, {});
        }

        if (this._brain.hasOwnProperty('energy')) {
            action.energy = this._brain.energy;
        }
        action.actor = this;
        return action;
    }

    /* Returns the cell where this actor is located at.*/
    getCell() {
        const x = this.getX();
        const y = this.getY();
        return this.getLevel().getMap().getCell(x, y);
    }

    toJSON() {
        let levelID = null;
        if (this.getLevel()) {
            levelID = this.getLevel().getID();
        }
        const obj = {
            id: this.getID(),
            name: this.getName(),
            type: this.getType(),
            x: this.getX(),
            y: this.getY(),
            fovRange: this.getFOVRange(),
            levelID,
            components: {
                Combat: this.get('Combat').toJSON(),
                Experience: this.get('Experience').toJSON(),
                Health: this.get('Health').toJSON(),
                Stats: this.get('Stats').toJSON()
            },
            inventory: this.getInvEq().getInventory().toJSON(),
            equipment: this.getInvEq().getEquipment().toJSON(),
            brain: this._brain.toJSON()
        };

        /* TODO: Using this crashes the game unfortunately
        const components = {};
        const thisComps = this.getComponents();
        Object.keys(thisComps).forEach(name => {
            components[thisComps[name].getType()] = thisComps[name].toJSON();
        });
        obj.components = components;
        */

        const simpleComps = ['ActorClass', 'Hunger', 'Flying', 'Defender',
            'Attacker',
            'CounterAttack', 'BiDirStrike', 'MasterEquipper', 'SpellPower',
            'SpiritItemCrafter', 'Trainer'];
        simpleComps.forEach(compName => {
            if (this.has(compName)) {
                obj.components[compName] = this.get(compName).toJSON();
            }
        });

        if (obj.type === null) {
            RG.err('Actor.Rogue', 'toJSON',
                `Type null for ${JSON.stringify(obj)}`);
        }

        if (this._spellbook) {
            obj.spellbook = this._spellbook.toJSON();
        }

        return obj;
    }

    //---------------------------------
    // Combat-related methods
    //---------------------------------

    getAttack() {
        let attack = this.get('Combat').getAttack();
        attack += this.getEquipAttack();

        if (this.has('CombatMods')) {
            attack += this.get('CombatMods').getAttack();
        }

        attack += Math.floor(this.getAccuracy() / 2);
        return attack;
    }

    getDefense() {
        let defense = this.get('Combat').getDefense();
        defense += this.getEquipDefense();
        if (this.has('CombatMods')) {
            defense += this.get('CombatMods').getDefense();
        }
        defense += Math.floor(this.getAgility() / 2);
        return defense;
    }

    getProtection() {
        let protection = this.get('Combat').getProtection();
        protection += this.getEquipProtection();
        if (this.has('CombatMods')) {
            protection += this.get('CombatMods').getProtection();
        }
        return protection;
    }

    getDamage() {
        let damage = this.get('Combat').rollDamage();
        let strength = this.getStrength();
        strength += this.getInvEq().getEquipment().getStrength();
        damage += RG.strengthToDamage(strength);
        if (this.has('CombatMods')) {
            damage += this.get('CombatMods').getDamage();
        }
        return damage;

    }

    //-------------------------------------------------------------
    // Stats-related methods (these take eq and boosts into account
    //-------------------------------------------------------------

    getAccuracy() {
        let acc = this.get('Stats').getAccuracy();
        acc += this.getInvEq().getEquipment().getAccuracy();
        if (this.has('StatsMods')) {acc += this.get('StatsMods').getAccuracy();}
        return acc;
    }

    getAgility() {
        let agi = this.get('Stats').getAgility();
        agi += this.getInvEq().getEquipment().getAgility();
        if (this.has('StatsMods')) {agi += this.get('StatsMods').getAgility();}
        return agi;
    }

    getStrength() {
        let str = this.get('Stats').getStrength();
        str += this.getInvEq().getEquipment().getStrength();
        if (this.has('StatsMods')) {str += this.get('StatsMods').getStrength();}
        return str;
    }

    getWillpower() {
        let wil = this.get('Stats').getWillpower();
        wil += this.getInvEq().getEquipment().getWillpower();
        if (this.has('StatsMods')) {
            wil += this.get('StatsMods').getWillpower();
        }
        return wil;
    }

    getSpeed() {
        let speed = this.get('Stats').getSpeed();
        speed += this.getInvEq().getEquipment().getSpeed();
        if (this.has('StatsMods')) {speed += this.get('StatsMods').getSpeed();}
        return speed;
    }

    getPerception() {
        let per = this.get('Stats').getPerception();
        per += this.getInvEq().getEquipment().getPerception();
        if (this.has('StatsMods')) {
            per += this.get('StatsMods').getPerception();
        }
        return per;
    }

    getMagic() {
        let per = this.get('Stats').getMagic();
        per += this.getInvEq().getEquipment().getMagic();
        if (this.has('StatsMods')) {per += this.get('StatsMods').getMagic();}
        return per;
    }
}

RG.Actor.Rogue = RGActorRogue;

/* Spirit actors. They have Ethereal component and cannot be attacked, but they
 * can be captured by SpiritGem-objects.*/
class RGActorSpirit extends RGActorRogue {

    constructor(name) {
        super(name);
        this.setType('spirit');
        this.add('Ethereal', new RG.Component.Ethereal());
        this.setBrain(new RG.Brain.Spirit(this));
    }

}

RG.Actor.Spirit = RGActorSpirit;

module.exports = RG.Actor;
<|endoftext|>


<commit_before>
import Entity from './entity';

const RG = require('./rg');
RG.Component = require('./component');
RG.Brain = require('./brain.virtual');
RG.Brain.Player = require('./brain.player');
RG.Inv = require('./inv');
RG.Spell = require('./spell');
RG.Time = require('./time');
const Mixin = require('./mixin');

RG.Actor = {};

const ACTOR_NO_ACTION = Object.freeze(() => {});
const EMPTY_ARGS = Object.freeze({});
const SPEED_COEFF = RG.BASE_SPEED * RG.ACTION_DUR;

class BaseActor extends Mixin.Locatable(Mixin.Typed(Entity)) {

    constructor(name) { // {{{2
        super({propType: RG.TYPE_ACTOR, type: null});
        this._name = name;
        this.add(new RG.Component.Action());
    }

    isPlayer() {return false;}
    isEnemy() {return false;}
    addEnemy() {/* No implementation here */}

    setName(name) {this._name = name;}
    getName() {return this._name;}

    getBrain() {return this._brain;}

    setBrain(brain) {
        this._brain = brain;
        this._brain.setActor(this);
    }

    getSpeed() {
        return RG.BASE_SPEED;
    }

    getEquipProtection() {return 0;}

    /* Returns the next action for this actor.*/
    nextAction(obj) {
        // Use actor brain to determine the action
        const cb = this._brain.decideNextAction(obj);
        let action = null;

        if (cb !== null) {
            const duration = Math.round(SPEED_COEFF / this.getSpeed());
            action = new RG.Time.Action(duration, cb, EMPTY_ARGS);
        }
        else {
            action = new RG.Time.Action(0, ACTOR_NO_ACTION, EMPTY_ARGS);
        }

        if (this._brain.hasOwnProperty('energy')) {
            action.energy = this._brain.energy;
        }
        action.actor = this;
        return action;
    }

    /* Serializes the virtual actor. */
    toJSON() {
        let levelID = null;
        if (this.getLevel()) {
            levelID = this.getLevel().getID();
        }
        const obj = {
            id: this.getID(),
            name: this.getName(),
            type: this.getType(),
            levelID,
            brain: this._brain.toJSON()
        };

        obj.components = RG.Component.compsToJSON(this);

        if (obj.type === null) {
            RG.err('Actor.Virtual', 'toJSON',
                `Type null for ${JSON.stringify(obj)}`);
        }

        return obj;
    }

}
RG.Actor.Base = BaseActor;

/* Virtual actor can be used to spawn more entities or for AI-like effects
 * inside a level. */
class VirtualActor extends BaseActor {

    constructor(name) { // {{{2
        super(name);
        this._brain = new RG.Brain.Virtual(this);
    }

}
RG.Actor.Virtual = VirtualActor;

/* Object representing a game actor who takes actions.  */
class RGActorRogue extends BaseActor {
    constructor(name) { // {{{2
        super(name);

        this._brain = new RG.Brain.Rogue(this);
        this._brain.getMemory().addEnemyType('player');

        this._invEq = new RG.Inv.Inventory(this);
        this._maxWeight = 10.0;

        // Components for this entity
        this.add(new RG.Component.Experience());
        this.add(new RG.Component.Combat());
        this.add(new RG.Component.Stats());
        this.add(new RG.Component.Health(50));

        const perception = new RG.Component.Perception();
        perception.setFOVRange(RG.NPC_FOV_RANGE);
        this.add(new RG.Component.Perception());

    }

    /* Returns true if actor is a player.*/
    isPlayer() {
        return this.has('Player') || this.has('PlayerControlled');
    }

    getFOVRange() {
        let range = this.get('Perception').getFOVRange();
        if (this.has('EagleEye')) {range += 2;}
        return range;
    }

    setFOVRange(range) {
        this.get('Perception').setFOVRange(range);
    }

    //---------------------------------
    // Brain-related methods
    //---------------------------------

    addEnemy(actor) {this._brain.addEnemy(actor);}
    addFriend(actor) {this._brain.addFriend(actor);}

    isEnemy(actor) {
        return this._brain.getMemory().isEnemy(actor);
    }

    isFriend(actor) {
        return this._brain.getMemory().isFriend(actor);
    }

    //---------------------------------
    // Equipment related methods
    //---------------------------------

    getInvEq() { return this._invEq; }

    /* Returns weapon that is wielded by the actor.*/
    getWeapon() {return this._invEq.getWeapon();}

    /* Returns weapon that is wielded by the actor.*/
    getMissileWeapon() {
        return this._invEq.getMissileWeapon();
    }

    /* Returns missile equipped by the player.*/
    getMissile() {
        return this._invEq.getEquipment().getItem('missile');
    }

    getEquipAttack() {
        let att = this._invEq.getEquipment().getAttack();
        if (this.has('Skills')) {
            att += this.get('Skills').getLevel('Melee');
        }
        return att;
    }

    getEquipDefense() {
        let def = this._invEq.getEquipment().getDefense();
        if (this.has('Skills')) {
            def += this.get('Skills').getLevel('Shields');
        }
        return def;
    }

    getEquipProtection() {
        return this._invEq.getEquipment().getProtection();
    }

    getShieldDefense() {
        const shield = this._invEq.getEquipment().getEquipped('shield');
        let bonus = 0;
        if (shield) {
            bonus = shield.getDefense();
            if (this.has('Skills')) {
                bonus += this.get('Skills').getLevel('Shields');
            }
        }
        return bonus;
    }

    setActorClass(classObj) {
        this._actorClass = classObj;
    }

    getActorClass() {
        return this._actorClass;
    }

    setBook(book) {
        this._spellbook = book;
    }

    getBook() {
        return this._spellbook;
    }

    /* Returns carrying capacity of the actor.*/
    getMaxWeight() {
        const statStr = this.get('Stats').getStrength();
        const eqStr = this._invEq.getEquipment().getStrength();
        return 2 * statStr + 2 * eqStr + this._maxWeight;
    }

    /* Marks actor as player. Cannot unset player.*/
    setIsPlayer(isPlayer) {
        if (isPlayer) {
            this._brain = new RG.Brain.Player(this);
            if (!this.has('StatsMods')) {
                this.add(new RG.Component.StatsMods());
            }
            if (!this.has('CombatMods')) {
                this.add(new RG.Component.CombatMods());
            }
            this.add(new RG.Component.Player());
            this.add(new RG.Component.SpellPower());
        }
        else {
            RG.err('Actor.Rogue', 'setIsPlayer',
                'Actor cannot be changed from player to mob.');
        }
    }

    /* Used when controlling other actors with the "real player" actor .*/
    setPlayerCtrl(isPlayer) {
        if (isPlayer) {
            this.add(new RG.Component.PlayerControlled());
            this._actualBrain = this._brain;
            this._brain = new RG.Brain.Player(this);
            if (!this.has('StatsMods')) {
                this.add(new RG.Component.StatsMods());
            }
            if (!this.has('CombatMods')) {
                this.add(new RG.Component.CombatMods());
            }
            this.add(new RG.Component.Possessed());
        }
        else {
            this.remove('PlayerControlled');
            this.remove('StatsMods');
            this.remove('CombatMods');
            this.remove('Possessed');
            this._brain = this._actualBrain;
            delete this._actualBrain;
        }
    }

    /* Returns the cell where this actor is located at.*/
    getCell() {
        const x = this.getX();
        const y = this.getY();
        const level = this.getLevel();
        if (level) {
            return level.getMap().getCell(x, y);
        }
        return null;
    }

    isInLevel(level) {
        if (this.getLevel()) {
            return this.getLevel().getID() === level.getID();
        }
        return false;
    }

    toJSON() {
        let levelID = null;
        if (this.getLevel()) {
            levelID = this.getLevel().getID();
        }
        const obj = {
            id: this.getID(),
            name: this.getName(),
            type: this.getType(),
            x: this.getX(),
            y: this.getY(),
            fovRange: this.getFOVRange(),
            levelID,
            inventory: this.getInvEq().getInventory().toJSON(),
            equipment: this.getInvEq().getEquipment().toJSON(),
            brain: this._brain.toJSON()
        };

        obj.components = RG.Component.compsToJSON(this);

        if (obj.type === null) {
            RG.err('Actor.Rogue', 'toJSON',
                `Type null for ${JSON.stringify(obj)}`);
        }

        if (this._spellbook) {
            obj.spellbook = this._spellbook.toJSON();
        }
        if (this.has('Player')) {
            obj.isPlayer = true;
        }
        if (this._actualBrain) {
            obj.brain = this._actualBrain.toJSON();
        }

        return obj;
    }

    //---------------------------------
    // Combat-related methods
    //---------------------------------

    getAttack() {
        let attack = this.get('Combat').getAttack();
        attack += this.getEquipAttack();
        attack += this._addFromCompList('CombatMods', 'getAttack');
        attack += Math.floor(this.getAccuracy() / 2);
        return attack;
    }

    getDefense() {
        let defense = this.get('Combat').getDefense();
        defense += this.getEquipDefense();
        defense += this._addFromCompList('CombatMods', 'getDefense');
        defense += Math.floor(this.getAgility() / 2);
        return defense;
    }

    getProtection() {
        let protection = this.get('Combat').getProtection();
        protection += this.getEquipProtection();
        protection += this._addFromCompList('CombatMods', 'getProtection');
        return protection;
    }

    getDamage() {
        let damage = this.get('Combat').rollDamage();
        const weapon = this.getWeapon();
        if (weapon) {
            damage = RG.getItemDamage(weapon);
        }
        const strength = this.getStrength();
        damage += RG.strengthToDamage(strength);
        damage += this._addFromCompList('CombatMods', 'getDamage');
        return damage;

    }

    getCombatBonus(funcName) {
        return this._addFromCompList('CombatMods', funcName);
    }

    _addFromCompList(compType, func) {
        const compList = this.getList(compType);
        if (compList.length > 0) {
            return compList.reduce((acc, val) => {
                return acc + val[func]();
            }, 0);
        }
        return 0;
    }

    //-------------------------------------------------------------
    // Stats-related methods (these take eq and boosts into account
    //-------------------------------------------------------------

    getAccuracy() {
        let acc = this.get('Stats').getAccuracy();
        acc += this.getInvEq().getEquipment().getAccuracy();
        acc += this._addFromCompList('StatsMods', 'getAccuracy');
        return acc;
    }

    getAgility() {
        let agi = this.get('Stats').getAgility();
        agi += this.getInvEq().getEquipment().getAgility();
        agi += this._addFromCompList('StatsMods', 'getAgility');
        return agi;
    }

    getStrength() {
        let str = this.get('Stats').getStrength();
        str += this.getInvEq().getEquipment().getStrength();
        str += this._addFromCompList('StatsMods', 'getStrength');
        return str;
    }

    getWillpower() {
        let wil = this.get('Stats').getWillpower();
        wil += this.getInvEq().getEquipment().getWillpower();
        wil += this._addFromCompList('StatsMods', 'getWillpower');
        return wil;
    }

    getSpeed() {
        let speed = this.get('Stats').getSpeed();
        speed += this.getInvEq().getEquipment().getSpeed();
        speed += this._addFromCompList('StatsMods', 'getSpeed');
        return speed;
    }

    getPerception() {
        let per = this.get('Stats').getPerception();
        per += this.getInvEq().getEquipment().getPerception();
        per += this._addFromCompList('StatsMods', 'getPerception');
        return per;
    }

    getMagic() {
        let mag = this.get('Stats').getMagic();
        mag += this.getInvEq().getEquipment().getMagic();
        mag += this._addFromCompList('StatsMods', 'getMagic');
        return mag;
    }

    /* Returns bonuses applied to given stat. */
    getStatBonus(funcName) {
        return this._addFromCompList('StatsMods', funcName);
    }
}

RG.Actor.Rogue = RGActorRogue;

/* Spirit actors. They have Ethereal component and cannot be attacked, but they
 * can be captured by SpiritGem-objects.*/
class RGActorSpirit extends RGActorRogue {

    constructor(name) {
        super(name);
        this.setType('spirit');
        this.add(new RG.Component.Ethereal());
        this.setBrain(new RG.Brain.Spirit(this));
    }

}

RG.Actor.Spirit = RGActorSpirit;

module.exports = RG.Actor;
<commit_msg>Each actor has now Corporeal component (if they have body).<commit_after>
import Entity from './entity';

const RG = require('./rg');
RG.Component = require('./component');
RG.Brain = require('./brain.virtual');
RG.Brain.Player = require('./brain.player');
RG.Inv = require('./inv');
RG.Spell = require('./spell');
RG.Time = require('./time');
const Mixin = require('./mixin');

RG.Actor = {};

const ACTOR_NO_ACTION = Object.freeze(() => {});
const EMPTY_ARGS = Object.freeze({});
const SPEED_COEFF = RG.BASE_SPEED * RG.ACTION_DUR;

class BaseActor extends Mixin.Locatable(Mixin.Typed(Entity)) {

    constructor(name) { // {{{2
        super({propType: RG.TYPE_ACTOR, type: null});
        this._name = name;
        this.add(new RG.Component.Action());
    }

    isPlayer() {return false;}
    isEnemy() {return false;}
    addEnemy() {/* No implementation here */}

    setName(name) {this._name = name;}
    getName() {return this._name;}

    getBrain() {return this._brain;}

    setBrain(brain) {
        this._brain = brain;
        this._brain.setActor(this);
    }

    getSpeed() {
        return RG.BASE_SPEED;
    }

    getEquipProtection() {return 0;}

    /* Returns the next action for this actor.*/
    nextAction(obj) {
        // Use actor brain to determine the action
        const cb = this._brain.decideNextAction(obj);
        let action = null;

        if (cb !== null) {
            const duration = Math.round(SPEED_COEFF / this.getSpeed());
            action = new RG.Time.Action(duration, cb, EMPTY_ARGS);
        }
        else {
            action = new RG.Time.Action(0, ACTOR_NO_ACTION, EMPTY_ARGS);
        }

        if (this._brain.hasOwnProperty('energy')) {
            action.energy = this._brain.energy;
        }
        action.actor = this;
        return action;
    }

    /* Serializes the virtual actor. */
    toJSON() {
        let levelID = null;
        if (this.getLevel()) {
            levelID = this.getLevel().getID();
        }
        const obj = {
            id: this.getID(),
            name: this.getName(),
            type: this.getType(),
            levelID,
            brain: this._brain.toJSON()
        };

        obj.components = RG.Component.compsToJSON(this);

        if (obj.type === null) {
            RG.err('Actor.Virtual', 'toJSON',
                `Type null for ${JSON.stringify(obj)}`);
        }

        return obj;
    }

}
RG.Actor.Base = BaseActor;

/* Virtual actor can be used to spawn more entities or for AI-like effects
 * inside a level. */
class VirtualActor extends BaseActor {

    constructor(name) { // {{{2
        super(name);
        this._brain = new RG.Brain.Virtual(this);
    }

}
RG.Actor.Virtual = VirtualActor;

/* Object representing a game actor who takes actions.  */
class RGActorRogue extends BaseActor {
    constructor(name) { // {{{2
        super(name);

        this._brain = new RG.Brain.Rogue(this);
        this._brain.getMemory().addEnemyType('player');

        this._invEq = new RG.Inv.Inventory(this);
        this._maxWeight = 10.0;

        // Components for this entity
        this.add(new RG.Component.Experience());
        this.add(new RG.Component.Combat());
        this.add(new RG.Component.Stats());
        this.add(new RG.Component.Health(50));
        this.add(new RG.Component.Corporeal());

        const perception = new RG.Component.Perception();
        perception.setFOVRange(RG.NPC_FOV_RANGE);
        this.add(new RG.Component.Perception());
    }

    /* Returns true if actor is a player.*/
    isPlayer() {
        return this.has('Player') || this.has('PlayerControlled');
    }

    getFOVRange() {
        let range = this.get('Perception').getFOVRange();
        if (this.has('EagleEye')) {range += 2;}
        return range;
    }

    setFOVRange(range) {
        this.get('Perception').setFOVRange(range);
    }

    //---------------------------------
    // Brain-related methods
    //---------------------------------

    addEnemy(actor) {this._brain.addEnemy(actor);}
    addFriend(actor) {this._brain.addFriend(actor);}

    isEnemy(actor) {
        return this._brain.getMemory().isEnemy(actor);
    }

    isFriend(actor) {
        return this._brain.getMemory().isFriend(actor);
    }

    //---------------------------------
    // Equipment related methods
    //---------------------------------

    getInvEq() { return this._invEq; }

    /* Returns weapon that is wielded by the actor.*/
    getWeapon() {return this._invEq.getWeapon();}

    /* Returns weapon that is wielded by the actor.*/
    getMissileWeapon() {
        return this._invEq.getMissileWeapon();
    }

    /* Returns missile equipped by the player.*/
    getMissile() {
        return this._invEq.getEquipment().getItem('missile');
    }

    getEquipAttack() {
        let att = this._invEq.getEquipment().getAttack();
        if (this.has('Skills')) {
            att += this.get('Skills').getLevel('Melee');
        }
        return att;
    }

    getEquipDefense() {
        let def = this._invEq.getEquipment().getDefense();
        if (this.has('Skills')) {
            def += this.get('Skills').getLevel('Shields');
        }
        return def;
    }

    getEquipProtection() {
        return this._invEq.getEquipment().getProtection();
    }

    getShieldDefense() {
        const shield = this._invEq.getEquipment().getEquipped('shield');
        let bonus = 0;
        if (shield) {
            bonus = shield.getDefense();
            if (this.has('Skills')) {
                bonus += this.get('Skills').getLevel('Shields');
            }
        }
        return bonus;
    }

    setActorClass(classObj) {
        this._actorClass = classObj;
    }

    getActorClass() {
        return this._actorClass;
    }

    setBook(book) {
        this._spellbook = book;
    }

    getBook() {
        return this._spellbook;
    }

    /* Returns carrying capacity of the actor.*/
    getMaxWeight() {
        const statStr = this.get('Stats').getStrength();
        const eqStr = this._invEq.getEquipment().getStrength();
        return 2 * statStr + 2 * eqStr + this._maxWeight;
    }

    /* Marks actor as player. Cannot unset player.*/
    setIsPlayer(isPlayer) {
        if (isPlayer) {
            this._brain = new RG.Brain.Player(this);
            if (!this.has('StatsMods')) {
                this.add(new RG.Component.StatsMods());
            }
            if (!this.has('CombatMods')) {
                this.add(new RG.Component.CombatMods());
            }
            this.add(new RG.Component.Player());
            this.add(new RG.Component.SpellPower());
        }
        else {
            RG.err('Actor.Rogue', 'setIsPlayer',
                'Actor cannot be changed from player to mob.');
        }
    }

    /* Used when controlling other actors with the "real player" actor .*/
    setPlayerCtrl(isPlayer) {
        if (isPlayer) {
            this.add(new RG.Component.PlayerControlled());
            this._actualBrain = this._brain;
            this._brain = new RG.Brain.Player(this);
            if (!this.has('StatsMods')) {
                this.add(new RG.Component.StatsMods());
            }
            if (!this.has('CombatMods')) {
                this.add(new RG.Component.CombatMods());
            }
            this.add(new RG.Component.Possessed());
        }
        else {
            this.remove('PlayerControlled');
            this.remove('StatsMods');
            this.remove('CombatMods');
            this.remove('Possessed');
            this._brain = this._actualBrain;
            delete this._actualBrain;
        }
    }

    /* Returns the cell where this actor is located at.*/
    getCell() {
        const x = this.getX();
        const y = this.getY();
        const level = this.getLevel();
        if (level) {
            return level.getMap().getCell(x, y);
        }
        return null;
    }

    isInLevel(level) {
        if (this.getLevel()) {
            return this.getLevel().getID() === level.getID();
        }
        return false;
    }

    toJSON() {
        let levelID = null;
        if (this.getLevel()) {
            levelID = this.getLevel().getID();
        }
        const obj = {
            id: this.getID(),
            name: this.getName(),
            type: this.getType(),
            x: this.getX(),
            y: this.getY(),
            fovRange: this.getFOVRange(),
            levelID,
            inventory: this.getInvEq().getInventory().toJSON(),
            equipment: this.getInvEq().getEquipment().toJSON(),
            brain: this._brain.toJSON()
        };

        obj.components = RG.Component.compsToJSON(this);

        if (obj.type === null) {
            RG.err('Actor.Rogue', 'toJSON',
                `Type null for ${JSON.stringify(obj)}`);
        }

        if (this._spellbook) {
            obj.spellbook = this._spellbook.toJSON();
        }
        if (this.has('Player')) {
            obj.isPlayer = true;
        }
        if (this._actualBrain) {
            obj.brain = this._actualBrain.toJSON();
        }

        return obj;
    }

    //---------------------------------
    // Combat-related methods
    //---------------------------------

    getAttack() {
        let attack = this.get('Combat').getAttack();
        attack += this.getEquipAttack();
        attack += this._addFromCompList('CombatMods', 'getAttack');
        attack += Math.floor(this.getAccuracy() / 2);
        return attack;
    }

    getDefense() {
        let defense = this.get('Combat').getDefense();
        defense += this.getEquipDefense();
        defense += this._addFromCompList('CombatMods', 'getDefense');
        defense += Math.floor(this.getAgility() / 2);
        return defense;
    }

    getProtection() {
        let protection = this.get('Combat').getProtection();
        protection += this.getEquipProtection();
        protection += this._addFromCompList('CombatMods', 'getProtection');
        return protection;
    }

    getDamage() {
        let damage = this.get('Combat').rollDamage();
        const weapon = this.getWeapon();
        if (weapon) {
            damage = RG.getItemDamage(weapon);
        }
        const strength = this.getStrength();
        damage += RG.strengthToDamage(strength);
        damage += this._addFromCompList('CombatMods', 'getDamage');
        return damage;

    }

    getCombatBonus(funcName) {
        return this._addFromCompList('CombatMods', funcName);
    }

    _addFromCompList(compType, func) {
        const compList = this.getList(compType);
        if (compList.length > 0) {
            return compList.reduce((acc, val) => {
                return acc + val[func]();
            }, 0);
        }
        return 0;
    }

    //-------------------------------------------------------------
    // Stats-related methods (these take eq and boosts into account
    //-------------------------------------------------------------

    getAccuracy() {
        let acc = this.get('Stats').getAccuracy();
        acc += this.getInvEq().getEquipment().getAccuracy();
        acc += this._addFromCompList('StatsMods', 'getAccuracy');
        return acc;
    }

    getAgility() {
        let agi = this.get('Stats').getAgility();
        agi += this.getInvEq().getEquipment().getAgility();
        agi += this._addFromCompList('StatsMods', 'getAgility');
        return agi;
    }

    getStrength() {
        let str = this.get('Stats').getStrength();
        str += this.getInvEq().getEquipment().getStrength();
        str += this._addFromCompList('StatsMods', 'getStrength');
        return str;
    }

    getWillpower() {
        let wil = this.get('Stats').getWillpower();
        wil += this.getInvEq().getEquipment().getWillpower();
        wil += this._addFromCompList('StatsMods', 'getWillpower');
        return wil;
    }

    getSpeed() {
        let speed = this.get('Stats').getSpeed();
        speed += this.getInvEq().getEquipment().getSpeed();
        speed += this._addFromCompList('StatsMods', 'getSpeed');
        return speed;
    }

    getPerception() {
        let per = this.get('Stats').getPerception();
        per += this.getInvEq().getEquipment().getPerception();
        per += this._addFromCompList('StatsMods', 'getPerception');
        return per;
    }

    getMagic() {
        let mag = this.get('Stats').getMagic();
        mag += this.getInvEq().getEquipment().getMagic();
        mag += this._addFromCompList('StatsMods', 'getMagic');
        return mag;
    }

    /* Returns bonuses applied to given stat. */
    getStatBonus(funcName) {
        return this._addFromCompList('StatsMods', funcName);
    }
}

RG.Actor.Rogue = RGActorRogue;

/* Spirit actors. They have Ethereal component and cannot be attacked, but they
 * can be captured by SpiritGem-objects.*/
class RGActorSpirit extends RGActorRogue {

    constructor(name) {
        super(name);
        this.setType('spirit');
        this.add(new RG.Component.Ethereal());
        this.setBrain(new RG.Brain.Spirit(this));
    }

}

RG.Actor.Spirit = RGActorSpirit;

module.exports = RG.Actor;
<|endoftext|>


<commit_before>var storage = window.sessionStorage, key = '_uikit_style', keyinverse = '_uikit_inverse', themes = {}, $html = $('html');

// try to load themes.json
var request = new XMLHttpRequest();
request.open('GET', '../themes.json', false);
request.send(null);

if (request.status === 200) {
    themes = JSON.parse(request.responseText);
}

var styles = $.extend({
        core: {css: '../dist/css/uikit-core.css'},
        theme: {css: '../dist/css/uikit.css'}
    }, themes),
    component = location.pathname.split('/').pop().replace(/.html$/, '');

if (getParam('style') && getParam('style').match(/\.(json|css)$/)) {
    styles.custom = getParam('style');
}

storage[key] = storage[key] || 'core';
storage[keyinverse] = storage[keyinverse] || 'default';

var dir = storage._uikit_dir || 'ltr';

// set dir
$html.attr('dir', dir);

var style = styles[storage[key]] || styles.theme;

// add style
document.writeln(`<link rel="stylesheet" href="${dir !== 'rtl' ? style.css : style.css.replace('.css', '').concat('-rtl.css')}">`);

// add javascript
document.writeln(`<script src="../dist/js/uikit.js"></script>`);
document.writeln(`<script src="${style.icons ? style.icons : '../dist/js/uikit-icons.js'}"></script>`);

$(() => {

    var $body = $('body');
    var $container = $('

').prependTo('body');
    var $tests = $('<select class="uk-select uk-form-width-small"></select>').css('margin', '20px 20px 20px 0').prependTo($container);
    var $styles = $('<select class="uk-select uk-form-width-small"></select>').css('margin', '20px').appendTo($container);
    var $inverse = $('<select class="uk-select uk-form-width-small"></select>').css('margin', '20px').appendTo($container);
    var $label = $('').css('margin', '20px').appendTo($container);

    // Tests
    // ------------------------------

    [
        'accordion',
        'alert',
        'align',
        'animation',
        'article',
        'background',
        'badge',
        'base',
        'breadcrumb',
        'button',
        'card',
        'close',
        'column',
        'comment',
        'container',
        'countdown',
        'cover',
        'description-list',
        'divider',
        'dotnav',
        'drop',
        'dropdown',
        'flex',
        'form',
        'grid',
        'grid-parallax',
        'heading',
        'height-expand',
        'height-viewport',
        'icon',
        'iconnav',
        'label',
        'lightbox',
        'link',
        'list',
        'margin',
        'marker',
        'modal',
        'nav',
        'navbar',
        'notification',
        'offcanvas',
        'overlay',
        'padding',
        'pagination',
        'parallax',
        'position',
        'placeholder',
        'progress',
        'scroll',
        'scrollspy',
        'search',
        'section',
        'slidenav',
        'sortable',
        'spinner',
        'sticky',
        'subnav',
        'switcher',
        'tab',
        'table',
        'text',
        'tile',
        'toggle',
        'tooltip',
        'totop',
        'transition',
        'utility',
        'upload',
        'visibility',
        'width'
    ].sort().forEach(name => $(`<option value="${name}.html">${name.split('-').map(ucfirst).join(' ')}</option>`).appendTo($tests));

    $tests.on('change', () => {
        if ($tests.val()) {
            var style = styles.custom ? `?style=${getParam('style')}` : '';
            location.href = `../${$html.find('script[src*="test.js"]').attr('src').replace('js/test.js', '')}tests/${$tests.val()}${style}`;
        }
    }).val(component && `${component}.html`);

    $tests.prepend(`<option value="index.html">Overview</option>`);

    // Styles
    // ------------------------------

    Object.keys(styles).forEach(style => $styles.append(`<option value="${style}">${ucfirst(style)}</option>`));

    $styles.on('change', () => {
        storage[key] = $styles.val();
        location.reload();
    }).val(storage[key]);

    // Variations
    // ------------------------------

    var variations = {
        'default': 'Default',
        'light': 'Dark',
        'dark': 'Light'
    };

    Object.keys(variations).forEach(name => $(`<option value="${name}">${variations[name]}</option>`).appendTo($inverse));

    $inverse.on('change', () => {

        $body.removeClass('uk-dark uk-light');

        switch ($inverse.val()) {
            case 'dark':
                $html.css('background', '#fff');
                $body.addClass('uk-dark');
                break;

            case 'light':
                $html.css('background', '#222');
                $body.addClass('uk-light');
                break;

            default:
                $html.css('background', '');
        }

        storage[keyinverse] = $inverse.val();

    }).val(storage[keyinverse]).trigger('change');

    // RTL
    // ------------------------------

    var $rtl = $('<input type="checkbox" class="uk-checkbox" />').on('change', () => {
        storage._uikit_dir = $rtl.prop('checked') ? 'rtl' : 'ltr';
        location.reload();
    }).appendTo($label).after('RTL');

    if (dir == 'rtl') {
        $rtl.prop('checked', true);
    }

    $html.css('padding-top', '');
});

$html.css('padding-top', '80px');

function ucfirst(str) {
    return str.length ? str.charAt(0).toUpperCase() + str.slice(1) : '';
}

function getParam(name) {
    var match = RegExp(`[?&]${name}=([^&]*)`).exec(window.location.search);
    return match && decodeURIComponent(match[1].replace(/\+/g, ' '));
}
<commit_msg>cleanup<commit_after>var storage = window.sessionStorage, key = '_uikit_style', keyinverse = '_uikit_inverse', themes = {}, $html = $('html');

// try to load themes.json
var request = new XMLHttpRequest();
request.open('GET', '../themes.json', false);
request.send(null);

if (request.status === 200) {
    themes = JSON.parse(request.responseText);
}

var styles = $.extend({
        core: {css: '../dist/css/uikit-core.css'},
        theme: {css: '../dist/css/uikit.css'}
    }, themes),
    component = location.pathname.split('/').pop().replace(/.html$/, '');

if (getParam('style') && getParam('style').match(/\.(json|css)$/)) {
    styles.custom = getParam('style');
}

storage[key] = storage[key] || 'core';
storage[keyinverse] = storage[keyinverse] || 'default';

var dir = storage._uikit_dir || 'ltr';

// set dir
$html.attr('dir', dir);

var style = styles[storage[key]] || styles.theme;

// add style
document.writeln(`<link rel="stylesheet" href="${dir !== 'rtl' ? style.css : style.css.replace('.css', '').concat('-rtl.css')}">`);

// add javascript
document.writeln(`<script src="../dist/js/uikit.js"></script>`);
document.writeln(`<script src="${style.icons ? style.icons : '../dist/js/uikit-icons.js'}"></script>`);

jQuery(($) => {

    var $body = $('body');
    var $container = $('

').prependTo('body');
    var $tests = $('<select class="uk-select uk-form-width-small"></select>').css('margin', '20px 20px 20px 0').prependTo($container);
    var $styles = $('<select class="uk-select uk-form-width-small"></select>').css('margin', '20px').appendTo($container);
    var $inverse = $('<select class="uk-select uk-form-width-small"></select>').css('margin', '20px').appendTo($container);
    var $label = $('').css('margin', '20px').appendTo($container);

    // Tests
    // ------------------------------

    [
        'accordion',
        'alert',
        'align',
        'animation',
        'article',
        'background',
        'badge',
        'base',
        'breadcrumb',
        'button',
        'card',
        'close',
        'column',
        'comment',
        'container',
        'countdown',
        'cover',
        'description-list',
        'divider',
        'dotnav',
        'drop',
        'dropdown',
        'flex',
        'form',
        'grid',
        'grid-parallax',
        'heading',
        'height-expand',
        'height-viewport',
        'icon',
        'iconnav',
        'label',
        'lightbox',
        'link',
        'list',
        'margin',
        'marker',
        'modal',
        'nav',
        'navbar',
        'notification',
        'offcanvas',
        'overlay',
        'padding',
        'pagination',
        'parallax',
        'position',
        'placeholder',
        'progress',
        'scroll',
        'scrollspy',
        'search',
        'section',
        'slidenav',
        'sortable',
        'spinner',
        'sticky',
        'subnav',
        'switcher',
        'tab',
        'table',
        'text',
        'tile',
        'toggle',
        'tooltip',
        'totop',
        'transition',
        'utility',
        'upload',
        'visibility',
        'width'
    ].sort().forEach(name => $(`<option value="${name}.html">${name.split('-').map(ucfirst).join(' ')}</option>`).appendTo($tests));

    $tests.on('change', () => {
        if ($tests.val()) {
            var style = styles.custom ? `?style=${getParam('style')}` : '';
            location.href = `../${$html.find('script[src*="test.js"]').attr('src').replace('js/test.js', '')}tests/${$tests.val()}${style}`;
        }
    }).val(component && `${component}.html`);

    $tests.prepend(`<option value="index.html">Overview</option>`);

    // Styles
    // ------------------------------

    Object.keys(styles).forEach(style => $styles.append(`<option value="${style}">${ucfirst(style)}</option>`));

    $styles.on('change', () => {
        storage[key] = $styles.val();
        location.reload();
    }).val(storage[key]);

    // Variations
    // ------------------------------

    var variations = {
        'default': 'Default',
        'light': 'Dark',
        'dark': 'Light'
    };

    Object.keys(variations).forEach(name => $(`<option value="${name}">${variations[name]}</option>`).appendTo($inverse));

    $inverse.on('change', () => {

        $body.removeClass('uk-dark uk-light');

        switch ($inverse.val()) {
            case 'dark':
                $html.css('background', '#fff');
                $body.addClass('uk-dark');
                break;

            case 'light':
                $html.css('background', '#222');
                $body.addClass('uk-light');
                break;

            default:
                $html.css('background', '');
        }

        storage[keyinverse] = $inverse.val();

    }).val(storage[keyinverse]).trigger('change');

    // RTL
    // ------------------------------

    var $rtl = $('<input type="checkbox" class="uk-checkbox" />').on('change', () => {
        storage._uikit_dir = $rtl.prop('checked') ? 'rtl' : 'ltr';
        location.reload();
    }).appendTo($label).after('RTL');

    if (dir == 'rtl') {
        $rtl.prop('checked', true);
    }

    $html.css('padding-top', '');
});

$html.css('padding-top', '80px');

function ucfirst(str) {
    return str.length ? str.charAt(0).toUpperCase() + str.slice(1) : '';
}

function getParam(name) {
    var match = RegExp(`[?&]${name}=([^&]*)`).exec(window.location.search);
    return match && decodeURIComponent(match[1].replace(/\+/g, ' '));
}
<|endoftext|>


<commit_before>require('babel-polyfill')
const EventEmitter = require('events').EventEmitter

const PROTOCOL_VERSION = 'rethinkdb-fusion-v0'

function responseEvent(id){
  return `response:${id}`
}
function errorEvent(id){
  return `error:${id}`
}

class FusionEmitter extends EventEmitter {
  // Returns a function that can be called to remove the listener
  // Otherwise works the same as 'on' for the underlying socket
  register(event, listener){
    this.on(event, listener)
    return () => {
      this.removeListener(event, listener)
    }
  }

  // Similar to `register` but wraps `once` instead of `on`
  registerOnce(event, listener){
    this.once(event, listener)
    return () => {
      this.removeListener(event, listener)
    }
  }

  //Forwards events from the current emitter to another emitter,
  //returning an unregistration function
  fwd(srcEvent, dst, dstEvent=srcEvent){
    return this.register(srcEvent, (...args) => dst.emit(dstEvent, ...args))
  }

  //Create a promise from this emitter, accepts on the given event
  //and rejects on the second event which defaults to 'error'
  getPromise(acceptEvent, rejectEvent='error'){
    let listenerSet = ListenerSet.onEmitter(this)
    return this._makePromise(listenerSet, acceptEvent, rejectEvent)
  }

  // The same as getPromise, but disposes the event emitter when it's
  // resolved or rejected. The underlying EventEmitter shouldn't be
  // used.
  intoPromise(acceptEvent, rejectEvent='error'){
    let listenerSet = ListenerSet.absorbEmitter(this)
    return this._makePromise(listenerSet, acceptEvent, rejectEvent)
  }

  _makePromise(listenerSet, acceptEvent, rejectEvent){
    return new Promise((resolve, reject) => {
      listenerSet
        .onceAndDispose(acceptEvent, resolve)
        .onceAndDispose(rejectEvent, reject)
    })
  }

  // Listens for all 'added' events, adding them to an
  // internal array. Once a response comes in that has state: the
  // complete event, it resolves the promise with all of the values
  // obtained so far.  The promise is rejected if an error event is
  // raised.
  collectingPromise(addEvent='response', completeEvent='complete'){
    let listenerSet = ListenerSet.onEmitter(this)
    return this._collectPromise(listenerSet, addEvent, completeEvent)
  }

  // Same as collectingPromise except disposes the underlying
  // EventEmitter when it is resolved or rejected
  intoCollectingPromise(addEvent='response', completeEvent='complete'){
    let listenerSet = ListenerSet.absorbEmitter(this)
    return this._collectPromise(listenerSet, addEvent, completeEvent)
  }

  _collectPromise(listenerSet, addEvent, completeEvent){
    return new Promise((resolve, reject) => {
      let values = [];
      listenerSet
        .on(addEvent, item => {
          values.push(item)
        }).onceAndDispose(completeEvent, () => {
          resolve(values)
        }).disposeOn('error')
    })
  }
}

// Handles hooking up a group of listeners to a FusionEmitter, and
// removing them all when certain events occur
class ListenerSet {
  constructor(emitter, {absorb: absorb=false}={}){
    this.emitter = emitter
    this.unregistry = []
    this.absorb = absorb
  }

  static onEmitter(emitter){
    return new ListenerSet(emitter)
  }

  static absorbEmitter(emitter){
    return new ListenerSet(emitter, {absorb: true})
  }

  on(event, listener){
    this.unregistry.push(this.emitter.register(event, listener))
    return this
  }

  once(event, listener){
    this.unregistry.push(this.emitter.registerOnce(event, listener))
    return this
  }

  fwd(srcEvent, dst, dstEvent=srcEvent){
    this.unregistry.push(this.emitter.fwd(srcEvent, dst, dstEvent))
    return this
  }

  onceAndDispose(event, listener){
    let wrappedListener = (...args) => {
      this.dispose("ListenerSet.onceAndDispose").then(() => listener(...args))
    }
    this.unregistry.push(this.emitter.registerOnce(event, wrappedListener))
    return this
  }

  disposeOn(event){
    this.unregistry.push(this.emitter.registerOnce(
      event, () => this.dispose("ListenerSet.disposeOn")))
    return this
  }

  dispose(reason){
    return Promise.resolve(() => {
      this.unregistry.forEach(unregister => unregister())
      if(this.absorb){
        return this.emitter.dispose(reason)
      }else{
        return this
      }
    })
  }

}

class Fusion extends FusionEmitter {
  constructor(host, {secure: secure=true}={}){
    super()
    var self = (collectionName) => self.collection(collectionName)
    Object.setPrototypeOf(self, Object.getPrototypeOf(this))

    self.host = host
    self.secure = secure
    self.requestCounter = 0
    self.socket = new FusionSocket(host, secure)
    self.listenerSet = ListenerSet.absorbEmitter(self.socket)
      .on('error', (err) => self.emit('error', err, self))
      .on('connected', () => self.emit('connected', self))
      .on('disconnected', () => self.emit('disconnected', self))
    // send handshake
    self.handshakeResponse = self.socket.getPromise('connected').then(() => {
      let reqId = self.requestCounter++
      self.socket.send({request_id: reqId})
      return self.socket.getPromise(responseEvent(reqId))
    }).catch(event => console.error('Got a connection error:', event))
    return self
  }

  dispose(reason='Fusion object disposed'){
    return this.socket.dispose(reason).then(() => this.listenerSet.dispose(reason))
  }

  collection(collectionName){
    return new Collection(this, collectionName)
  }

  store(collection, documents, options){
    if(documents.length === 1 && Array.isArray(documents[0])){
      //Unwrap if a user passed an array to a spread function
      documents = documents[0]
    }
    let command = Object.assign({data: documents}, collection, options)
    return this._send(`store`, command).intoCollectingPromise('response')
  }

  remove(collection, documents){
    let command = {collection: collection, data: documents}
    return this._send('remove', command).intoCollectingPromise('response')
  }

  query(data){
    return this._send('query', data).intoCollectingPromise('response')
  }

  subscribe(query, updates=true){
    if(updates){
      return this._subscribe(query)
    }else{
      return this._send('query', query)
    }
  }

  endSubscription(requestId){
    return this.handshakeResponse.then((handshake) => {
      this.socket.send({request_id: requestId, type: 'end_subscription'})
    })
  }

  _send(type, data){
    let requestId = this.requestCounter++
    let req = {type: type, options: data, request_id: requestId}
    this.handshakeResponse.then((handshake) => {
      this.socket.send(req)
    })
    return new RequestEmitter(requestId, this.socket)
  }

  // Subscription queries, only handles returning an emitter
  _subscribe(query){
    let requestId = this.requestCounter++
    let req = {type: 'subscribe', options: query, request_id: requestId}
    this.handshakeResponse.then((handshake) => this.socket.send(req))
    return new RequestEmitter(requestId, this.socket)
  }

}

class FusionSocket extends FusionEmitter {
  // Wraps native websockets in an EventEmitter, and deals with some
  // simple protocol level things like serializing from/to JSON, and
  // emitting events on request_ids
  constructor(host, secure=true){
    super()
    let hostString = (secure ? 'wss://' : 'ws://') + host
    this._ws = new WebSocket(hostString, PROTOCOL_VERSION)
    this._openWs = new Promise((resolve, reject) => {
      this._ws.onopen = (event) => {
        this.emit('connected', event)
        resolve(this._ws)
      }
      this._ws.onerror = (event) => {
        this.emit('error', event)
        reject(event)
      }
      this._ws.onclose = (event) => {
        this.emit('disconnected', event)
        reject(event)
      }
    })
    this._ws.onmessage = (event) => {
      let data = JSON.parse(event.data)
      console.debug("Receiving: ", data)
      if(data.request_id === undefined){
        this.emit("error", "Request id undefined", data)
      }else if(data.error !== undefined){
        this.emit(errorEvent(data.request_id), data)
      }else {
        this.emit(responseEvent(data.request_id), data)
      }
    }
  }

  send(message){
    if(typeof message !== 'string'){
      message = JSON.stringify(message)
    }
    console.debug("Sending: ", message)
    return this._openWs.then((ws) => ws.send(message))
  }

  dispose(reason='FusionSocket disposed'){
    return this._openWs.then(
      (ws) => ws.close(1000, reason))
  }

}

// These are created for requests, will only get messages for the
// particular request id. Has extra methods for closing the request,
// and for creating promises that resolve or reject based on events
// from this emitter
class RequestEmitter extends FusionEmitter {
  constructor(requestId, fusion){
    super()
    this.fusion = fusion
    this.requestId = requestId
    this.remoteListeners = ListenerSet.onEmitter(this.fusion)

    //Forwards on fusion events to this emitter's listeners
    this.remoteListeners
      .fwd('connected', this)
      .fwd('disconnected', this)
      .fwd(errorEvent(requestId), this, 'error')
      .on(responseEvent(requestId), (response) => {
        if(Array.isArray(response.data)){
          response.data.forEach(changeObj => {
            if(!this._emitChangeEvent(changeObj)){
              this.emit('response', response.data)
            }
          })
        }else if(response.data !== undefined){
          this.emit("response", response.data)
        }
        if(response.state === 'synced'){
          this.emit('synced')
        }else if(response.state === 'complete'){
          this.emit('complete')
        }
      }).disposeOn('error')
  }

  // Emits an event depending on new_val old_val return value is
  // whether an event was emitted
  _emitChangeEvent(changeObj){
    if(changeObj.new_val === undefined && changeObj.old_val === undefined){
      return false
    }
    if(changeObj.new_val !== null && changeObj.old_val !== null){
      if(this.listenerCount('changed') === 0){
        this.emit('removed', changeObj.old_val)
        this.emit('added', changeObj.new_val)
      }else{
        this.emit('changed', changeObj.new_val, changeObj.old_val)
      }
      return true
    }else if(changeObj.new_val !== null && changeObj.old_val === null){
      this.emit('added', changeObj.new_val)
      return true
    }else if(changeObj.new_val === null && changeObj.old_val !== null){
      this.emit('removed', changeObj.old_val)
      return true
    }else{
      return false
    }
  }
  dispose(reason='RequestEmitter for ${this.requestId} disposed'){
    return this.fusion.endSubscription(this.requestId)
      .then(() => this.getPromise('complete'))
      .then(() => this.remoteListeners.dispose())
      .then(() => super.dispose(reason))
  }
}

class MockConnection extends Fusion {
  constructor(hostString){
    super()
    this.hostString = hostString;
    this.docs = [];
    this.promises = {};
    this.emitters = {};
    this.requestCounter = 0;
  }

  send(type, data){
    //Basically, don't actually send anything just store it in an array,
    // inefficient search and no error handling but fine for mocking

    switch(type){
    case 'update':
      this.docs.forEach(function(doc, index){
        if (doc.id == data.id){
          this.data[index] = data;
        }
      });
      break;
    case 'remove':
      this.docs.forEach(function(doc, index){
        if (doc.id == data.id){
          this.data = this.data.splice(index);
        }
      });
      break;
    case 'store_replace':
    case 'store_error':
      this.docs.push(data);
      break;
    }

    return Promise.resolve(true);
  }

  _onOpen(event){
    for(let emitter of this.emitters){
      emitter.emit('reconnected', event);
    }
  }
}

class TermBase {

  constructor(fusion){
    this.fusion = fusion
  }

  subscribe(updates=true){
    // should create a changefeed query, return eventemitter
    return this.fusion.subscribe(this.query, updates)
  }

  value(){
    // return promise with no changefeed
    return this.fusion.query(this.query)
  }
}

class Collection extends TermBase {

  constructor(fusion, collectionName){
    super(fusion)
    this._collectionName = collectionName
    this.query = {collection: collectionName}
  }

  findOne(id){
    return new FindOne(this.fusion, this.query, id)
  }

  find(fieldName, fieldValue){
    return new Find(this.fusion, this.query, fieldName, fieldValue)
  }

  between(minVal, maxVal, field='id'){
    return new Between(this.fusion, this.query, minVal, maxVal,field)
  }

  order(field='id', ascending=true){
    return new Order(this.fusion, this.query, field, ascending)
  }

  store(...documents){
    let args = {missing: 'insert', conflict: 'replace'}
    return this.fusion.store(this.query, documents, args)
  }

  upsert(...documents){
    let args =  {missing: 'insert', conflict: 'update'}
    return this.fusion.store(this.query, documents, args)
  }

  insert(...documents){
    let args = {missing: 'insert', conflict: 'error'}
    return this.fusion.store(this.query, documents, args)
  }

  replace(...documents){
    let args = {missing: 'error', conflict: 'replace'}
    return this.fusion.store(this.query, documents, args)
  }

  update(...documents){
    let args = {missing: 'error', conflict: 'update'}
    return this.fusion.store(this.query, documents, args)
  }

  remove(...documents){
    if(typeof document === 'number' || typeof document === 'string'){
      document = {id: document}
    }
    return this.fusion.remove(this._collectionName, document)
  }
}

class Find extends TermBase {
  constructor(fusion, query, field, value){
    super(fusion)
    this._field = field
    this._value = value
    this.query = Object.assign({
      selection: {type: 'find', args: [value]},
      field_name: field
    }, query)
  }

  order(field='id', ascending=true){
    return new Order(this.fusion, this.query, field, ascending)
  }

  limit(size){
    return new Limit(this.fusion, this.query, size);
  }
}

class FindOne extends TermBase {
  constructor(fusion, query, docId){
    super(fusion)
    this.id = docId
    this.query = Object.assign({
      selection: {type: 'find_one', args: [docId]}
    }, query)
  }
}

class Between extends TermBase {
  constructor(fusion, query, minVal, maxVal, field){
    super(fusion)
    this.minVal = minVal
    this.maxVal = maxVal
    this.field = field
    this.query = Object.assign({
      selection: {type: 'between', args: [minVal, maxVal]},
      field_name: field
    }, query)
  }

  limit(size){
    return new Limit(this.fusion, this.query, size);
  }
}

class Order {
  constructor(fusion, query, field, ascending){
    this.fusion = fusion
    this.field = field
    this.direction = direction
    this.query = Object.assign({
      order: ascending ? 'ascending' : 'descending',
      field_name: field
    }, query)
  }

  limit(size){
    return new Limit(this.fusion, this.query, size)
  }

  between(minVal, maxVal){
    // between is forced to have the same field as this term
    return new Between(this.fusion, this.query, minVal, maxVal, this.field)
  }
}

class Limit extends TermBase {
  constructor(fusion, query, size){
    super(fusion)
    this._size = size
    this.query = Object.assign({limit: size}, query)
  }
}

module.exports = Fusion
<commit_msg>Don't return a promise from socket send<commit_after>require('babel-polyfill')
const EventEmitter = require('events').EventEmitter

const PROTOCOL_VERSION = 'rethinkdb-fusion-v0'

function responseEvent(id){
  return `response:${id}`
}
function errorEvent(id){
  return `error:${id}`
}

class FusionEmitter extends EventEmitter {
  // Returns a function that can be called to remove the listener
  // Otherwise works the same as 'on' for the underlying socket
  register(event, listener){
    this.on(event, listener)
    return () => {
      this.removeListener(event, listener)
    }
  }

  // Similar to `register` but wraps `once` instead of `on`
  registerOnce(event, listener){
    this.once(event, listener)
    return () => {
      this.removeListener(event, listener)
    }
  }

  //Forwards events from the current emitter to another emitter,
  //returning an unregistration function
  fwd(srcEvent, dst, dstEvent=srcEvent){
    return this.register(srcEvent, (...args) => dst.emit(dstEvent, ...args))
  }

  //Create a promise from this emitter, accepts on the given event
  //and rejects on the second event which defaults to 'error'
  getPromise(acceptEvent, rejectEvent='error'){
    let listenerSet = ListenerSet.onEmitter(this)
    return this._makePromise(listenerSet, acceptEvent, rejectEvent)
  }

  // The same as getPromise, but disposes the event emitter when it's
  // resolved or rejected. The underlying EventEmitter shouldn't be
  // used.
  intoPromise(acceptEvent, rejectEvent='error'){
    let listenerSet = ListenerSet.absorbEmitter(this)
    return this._makePromise(listenerSet, acceptEvent, rejectEvent)
  }

  _makePromise(listenerSet, acceptEvent, rejectEvent){
    return new Promise((resolve, reject) => {
      listenerSet
        .onceAndDispose(acceptEvent, resolve)
        .onceAndDispose(rejectEvent, reject)
    })
  }

  // Listens for all 'added' events, adding them to an
  // internal array. Once a response comes in that has state: the
  // complete event, it resolves the promise with all of the values
  // obtained so far.  The promise is rejected if an error event is
  // raised.
  collectingPromise(addEvent='response', completeEvent='complete'){
    let listenerSet = ListenerSet.onEmitter(this)
    return this._collectPromise(listenerSet, addEvent, completeEvent)
  }

  // Same as collectingPromise except disposes the underlying
  // EventEmitter when it is resolved or rejected
  intoCollectingPromise(addEvent='response', completeEvent='complete'){
    let listenerSet = ListenerSet.absorbEmitter(this)
    return this._collectPromise(listenerSet, addEvent, completeEvent)
  }

  _collectPromise(listenerSet, addEvent, completeEvent){
    return new Promise((resolve, reject) => {
      let values = [];
      listenerSet
        .on(addEvent, item => {
          values.push(item)
        }).onceAndDispose(completeEvent, () => {
          resolve(values)
        }).disposeOn('error')
    })
  }
}

// Handles hooking up a group of listeners to a FusionEmitter, and
// removing them all when certain events occur
class ListenerSet {
  constructor(emitter, {absorb: absorb=false}={}){
    this.emitter = emitter
    this.unregistry = []
    this.absorb = absorb
  }

  static onEmitter(emitter){
    return new ListenerSet(emitter)
  }

  static absorbEmitter(emitter){
    return new ListenerSet(emitter, {absorb: true})
  }

  on(event, listener){
    this.unregistry.push(this.emitter.register(event, listener))
    return this
  }

  once(event, listener){
    this.unregistry.push(this.emitter.registerOnce(event, listener))
    return this
  }

  fwd(srcEvent, dst, dstEvent=srcEvent){
    this.unregistry.push(this.emitter.fwd(srcEvent, dst, dstEvent))
    return this
  }

  onceAndDispose(event, listener){
    let wrappedListener = (...args) => {
      this.dispose("ListenerSet.onceAndDispose").then(() => listener(...args))
    }
    this.unregistry.push(this.emitter.registerOnce(event, wrappedListener))
    return this
  }

  disposeOn(event){
    this.unregistry.push(this.emitter.registerOnce(
      event, () => this.dispose("ListenerSet.disposeOn")))
    return this
  }

  dispose(reason){
    return Promise.resolve(() => {
      this.unregistry.forEach(unregister => unregister())
      if(this.absorb){
        return this.emitter.dispose(reason)
      }else{
        return this
      }
    })
  }

}

class Fusion extends FusionEmitter {
  constructor(host, {secure: secure=true}={}){
    super()
    var self = (collectionName) => self.collection(collectionName)
    Object.setPrototypeOf(self, Object.getPrototypeOf(this))

    self.host = host
    self.secure = secure
    self.requestCounter = 0
    self.socket = new FusionSocket(host, secure)
    self.listenerSet = ListenerSet.absorbEmitter(self.socket)
      .on('error', (err) => self.emit('error', err, self))
      .on('connected', () => self.emit('connected', self))
      .on('disconnected', () => self.emit('disconnected', self))
    // send handshake
    self.handshakeResponse = self.socket.getPromise('connected').then(() => {
      let reqId = self.requestCounter++
      self.socket.send({request_id: reqId})
      return self.socket.getPromise(responseEvent(reqId))
    }).catch(event => {
      console.error('Got a connection error:', event)
      return self.dispose("Fusion got a Connection error")
    })
    return self
  }

  dispose(reason='Fusion object disposed'){
    return this.socket.dispose(reason).then(() => this.listenerSet.dispose(reason))
  }

  collection(collectionName){
    return new Collection(this, collectionName)
  }

  store(collection, documents, options){
    if(documents.length === 1 && Array.isArray(documents[0])){
      //Unwrap if a user passed an array to a spread function
      documents = documents[0]
    }
    let command = Object.assign({data: documents}, collection, options)
    return this._send(`store`, command).intoCollectingPromise('response')
  }

  remove(collection, documents){
    let command = {collection: collection, data: documents}
    return this._send('remove', command).intoCollectingPromise('response')
  }

  query(data){
    return this._send('query', data).intoCollectingPromise('response')
  }

  subscribe(query, updates=true){
    if(updates){
      return this._subscribe(query)
    }else{
      return this._send('query', query)
    }
  }

  endSubscription(requestId){
    return this.handshakeResponse.then((handshake) => {
      this.socket.send({request_id: requestId, type: 'end_subscription'})
    })
  }

  _send(type, data){
    let requestId = this.requestCounter++
    let req = {type: type, options: data, request_id: requestId}
    this.handshakeResponse.then((handshake) => {
      this.socket.send(req)
    })
    return new RequestEmitter(requestId, this.socket)
  }

  // Subscription queries, only handles returning an emitter
  _subscribe(query){
    let requestId = this.requestCounter++
    let req = {type: 'subscribe', options: query, request_id: requestId}
    this.handshakeResponse.then((handshake) => this.socket.send(req))
    return new RequestEmitter(requestId, this.socket)
  }

}

class FusionSocket extends FusionEmitter {
  // Wraps native websockets in an EventEmitter, and deals with some
  // simple protocol level things like serializing from/to JSON, and
  // emitting events on request_ids
  constructor(host, secure=true){
    super()
    let hostString = (secure ? 'wss://' : 'ws://') + host
    this._ws = new WebSocket(hostString, PROTOCOL_VERSION)
    this._openWs = new Promise((resolve, reject) => {
      this._ws.onopen = (event) => {
        this.emit('connected', event)
        resolve(this._ws)
      }
      this._ws.onerror = (event) => {
        this.emit('error', event)
        reject(event)
      }
      this._ws.onclose = (event) => {
        this.emit('disconnected', event)
        reject(event)
      }
    })
    this._ws.onmessage = (event) => {
      let data = JSON.parse(event.data)
      console.debug("Receiving: ", data)
      if(data.request_id === undefined){
        this.emit("error", "Request id undefined", data)
      }else if(data.error !== undefined){
        this.emit(errorEvent(data.request_id), data)
      }else {
        this.emit(responseEvent(data.request_id), data)
      }
    }
  }

  send(message){
    if(typeof message !== 'string'){
      message = JSON.stringify(message)
    }
    console.debug("Sending: ", message)
    this._openWs.then((ws) => ws.send(message))
  }

  dispose(reason='FusionSocket disposed'){
    return this._openWs.then(
      (ws) => ws.close(1000, reason))
  }

}

// These are created for requests, will only get messages for the
// particular request id. Has extra methods for closing the request,
// and for creating promises that resolve or reject based on events
// from this emitter
class RequestEmitter extends FusionEmitter {
  constructor(requestId, fusion){
    super()
    this.fusion = fusion
    this.requestId = requestId
    this.remoteListeners = ListenerSet.onEmitter(this.fusion)

    //Forwards on fusion events to this emitter's listeners
    this.remoteListeners
      .fwd('connected', this)
      .fwd('disconnected', this)
      .fwd(errorEvent(requestId), this, 'error')
      .on(responseEvent(requestId), (response) => {
        if(Array.isArray(response.data)){
          response.data.forEach(changeObj => {
            if(!this._emitChangeEvent(changeObj)){
              this.emit('response', response.data)
            }
          })
        }else if(response.data !== undefined){
          this.emit("response", response.data)
        }
        if(response.state === 'synced'){
          this.emit('synced')
        }else if(response.state === 'complete'){
          this.emit('complete')
        }
      }).disposeOn('error')
  }

  // Emits an event depending on new_val old_val return value is
  // whether an event was emitted
  _emitChangeEvent(changeObj){
    if(changeObj.new_val === undefined && changeObj.old_val === undefined){
      return false
    }
    if(changeObj.new_val !== null && changeObj.old_val !== null){
      if(this.listenerCount('changed') === 0){
        this.emit('removed', changeObj.old_val)
        this.emit('added', changeObj.new_val)
      }else{
        this.emit('changed', changeObj.new_val, changeObj.old_val)
      }
      return true
    }else if(changeObj.new_val !== null && changeObj.old_val === null){
      this.emit('added', changeObj.new_val)
      return true
    }else if(changeObj.new_val === null && changeObj.old_val !== null){
      this.emit('removed', changeObj.old_val)
      return true
    }else{
      return false
    }
  }
  dispose(reason='RequestEmitter for ${this.requestId} disposed'){
    return this.fusion.endSubscription(this.requestId)
      .then(() => this.getPromise('complete'))
      .then(() => this.remoteListeners.dispose())
      .then(() => super.dispose(reason))
  }
}

class MockConnection extends Fusion {
  constructor(hostString){
    super()
    this.hostString = hostString;
    this.docs = [];
    this.promises = {};
    this.emitters = {};
    this.requestCounter = 0;
  }

  send(type, data){
    //Basically, don't actually send anything just store it in an array,
    // inefficient search and no error handling but fine for mocking

    switch(type){
    case 'update':
      this.docs.forEach(function(doc, index){
        if (doc.id == data.id){
          this.data[index] = data;
        }
      });
      break;
    case 'remove':
      this.docs.forEach(function(doc, index){
        if (doc.id == data.id){
          this.data = this.data.splice(index);
        }
      });
      break;
    case 'store_replace':
    case 'store_error':
      this.docs.push(data);
      break;
    }

    return Promise.resolve(true);
  }

  _onOpen(event){
    for(let emitter of this.emitters){
      emitter.emit('reconnected', event);
    }
  }
}

class TermBase {

  constructor(fusion){
    this.fusion = fusion
  }

  subscribe(updates=true){
    // should create a changefeed query, return eventemitter
    return this.fusion.subscribe(this.query, updates)
  }

  value(){
    // return promise with no changefeed
    return this.fusion.query(this.query)
  }
}

class Collection extends TermBase {

  constructor(fusion, collectionName){
    super(fusion)
    this._collectionName = collectionName
    this.query = {collection: collectionName}
  }

  findOne(id){
    return new FindOne(this.fusion, this.query, id)
  }

  find(fieldName, fieldValue){
    return new Find(this.fusion, this.query, fieldName, fieldValue)
  }

  between(minVal, maxVal, field='id'){
    return new Between(this.fusion, this.query, minVal, maxVal,field)
  }

  order(field='id', ascending=true){
    return new Order(this.fusion, this.query, field, ascending)
  }

  store(...documents){
    let args = {missing: 'insert', conflict: 'replace'}
    return this.fusion.store(this.query, documents, args)
  }

  upsert(...documents){
    let args =  {missing: 'insert', conflict: 'update'}
    return this.fusion.store(this.query, documents, args)
  }

  insert(...documents){
    let args = {missing: 'insert', conflict: 'error'}
    return this.fusion.store(this.query, documents, args)
  }

  replace(...documents){
    let args = {missing: 'error', conflict: 'replace'}
    return this.fusion.store(this.query, documents, args)
  }

  update(...documents){
    let args = {missing: 'error', conflict: 'update'}
    return this.fusion.store(this.query, documents, args)
  }

  remove(...documents){
    if(typeof document === 'number' || typeof document === 'string'){
      document = {id: document}
    }
    return this.fusion.remove(this._collectionName, document)
  }
}

class Find extends TermBase {
  constructor(fusion, query, field, value){
    super(fusion)
    this._field = field
    this._value = value
    this.query = Object.assign({
      selection: {type: 'find', args: [value]},
      field_name: field
    }, query)
  }

  order(field='id', ascending=true){
    return new Order(this.fusion, this.query, field, ascending)
  }

  limit(size){
    return new Limit(this.fusion, this.query, size);
  }
}

class FindOne extends TermBase {
  constructor(fusion, query, docId){
    super(fusion)
    this.id = docId
    this.query = Object.assign({
      selection: {type: 'find_one', args: [docId]}
    }, query)
  }
}

class Between extends TermBase {
  constructor(fusion, query, minVal, maxVal, field){
    super(fusion)
    this.minVal = minVal
    this.maxVal = maxVal
    this.field = field
    this.query = Object.assign({
      selection: {type: 'between', args: [minVal, maxVal]},
      field_name: field
    }, query)
  }

  limit(size){
    return new Limit(this.fusion, this.query, size);
  }
}

class Order {
  constructor(fusion, query, field, ascending){
    this.fusion = fusion
    this.field = field
    this.direction = direction
    this.query = Object.assign({
      order: ascending ? 'ascending' : 'descending',
      field_name: field
    }, query)
  }

  limit(size){
    return new Limit(this.fusion, this.query, size)
  }

  between(minVal, maxVal){
    // between is forced to have the same field as this term
    return new Between(this.fusion, this.query, minVal, maxVal, this.field)
  }
}

class Limit extends TermBase {
  constructor(fusion, query, size){
    super(fusion)
    this._size = size
    this.query = Object.assign({limit: size}, query)
  }
}

module.exports = Fusion
<|endoftext|>