Home Reference Source

src/jg/situation.js

import _ from 'lodash';

const nop = () => { };
const tru = () => true;
/**
 */
export default class Situation {
    /**
     * 
     * @param {object} args
     * @param {string} args.id
     * @param {Boolean} args.autosave If true, game will save when scene is
     *                                entered. Default false.
     * @param {Boolean} args.autosave If true, transcript will be cleared when
     *                                scene is entered. Default false.
     * @param {string} args.content
     *      Markdown template to be rendered to the transcript when this
     *      situation is entered. {@see /markup.html}
     * @param {string[]} args.choices
     *      List of situation IDs or tags. See
     *      {@link model#interpretChoices} for how this works.
     * @param {Map<string,string>} args.snippets
     *      Snippets used by writers/replacers. {@see /writers_replacers.html}.
     * @param {object|null} args.input 
     *      If provided, prompts user for input. Looks like
     *      `input: {placeholder: "Your name", next: "situation-id", store: function(model, value)}`
     * @param {string|null} args.input.placeholder
     *      Placeholder value for the HTML input field 
     * @param {string} args.input.next
     *      Situation or action to go to after user enters a value.
     *      Must start with either `@` (for situation IDs) or `>`
     *      (for actions).
     * @param {function(model: model, value: string)} args.input.store
     *      Your chance to do something with the given alue
     * @param {Boolean} args.debugChoices See {@link debugChoices}
     * @param {function(model: model, hostSituation: Situation): Boolean} getCanChoose
     *      If this function is provided and returns `false`, this situation
     *      is not linkified in the choices list.
     * @param {function(model: model, hostSituation: Situation): Boolean} getCanSee
     *      If this function is provided and returns `false`, the situation
     *      will not show up in the choices list for the situation presenting
     *      the choice.
     * @param {number|function(model: model, hostSituation: Situation): number} priority
     *      May be a constant number, or function returning a number. This value
     *      is used by {@link model#interpretChoices}.
     * @param {number|function(model: model, hostSituation: Situation): number} displayOrder
     *      May be a constant number, or function returning a number. This value
     *      is used by {@link model#interpretChoices}.
     * @param {string|function(model: model, hostSituation: Situation): string} optionText
     *      Text shown to user when being presented as a choice.
     * @param {function(model: model, ui: ui, fromSituation: Situation): Boolean} willEnter
     *      This situation will enter, unless this function returns `false`. It
     *      is safe to call `model.do()` from here, as long as you then return
     *      `false`.
     * @param {function(model: model, ui: ui, fromSituation: Situation)} enter
     *      The situation has been entered, and {@link Situation#content} has
     *      been written to the transcript.
     * @param {function(model: model, ui: ui, toSituation: Situation)} exit
     *      The situation is being exited, but the next situation has not yet
     *      been entered.
     * @param {function(model: model, ui: ui, action: String)} act
     *      An action-based link has been clicked. You might just want to use
     *      the `actions` key instead of this function if you're just mapping
     *      action names to functions.
     * @param {Map<string,function>} actions
     *      Map of action name to function that is called when the user invokes
     *      the action.
     * 
     * @example
     *  jumbogrove('#app', {
     *      id: 'situations-example',
     *      autosave: true,
     * 
     *      // stuff related to this situation being a choice in another situation:
     *      optionText: "Proclaim hungriness",
     *      getCanChoose: (model, host) => true,
     *      getCanSee: (model, host) => true,
     *      priority: 1,
     *      displayOrder: 1,
     * 
     *      // stuff related to content and what happens inside the situation:
     *      content: `
     *      I am [very](>replaceself:more_adjectives) hungry.
     * 
     *      [Eat](>eat)
     * 
     *      [Go to restaurant](@restaurant)
     *      `,
     *      snippets: {
     *          more_adjectives: "very, very, very, very"
     *      },
     *      act: (model, ui, action) => console.log("did action", action),
     *      actions: {
     *          eat: () => console.log("OM NOM NOM"),
     *      },
     * 
     *      // going to other situations:
     *      choices: ['next-situation', '#situations-involving-food'],
     *      // normally you wouldn't have 'choices' and 'input' in the same situation.
     *      input: {
     *          placeholder: "Please enter your favorite food.",
     *          next: "@restaurant",
     *      },
     *      debugChoices: false,
     * 
     *      // lifecycle
     *      willEnter: (model, ui, from) => true,
     *      enter: (model, ui, from) => console.log("entered"),
     *      exit: (model, ui, from) => console.log("exited"),
     *  });
     */
    constructor({
        id,
        tags = [],
        totalVisits = 0,
        autosave = false,
        clear = false,
        content = null,
        choices = null,
        snippets = {},
        input = null,
        debugChoices = false,
        getCanChoose = tru,
        getCanSee = tru,
        priority = 0,
        displayOrder = 0,
        optionText = null,
        willEnter = tru,
        enter = nop,
        act = nop,
        actions = {},
        exit = nop,
    }) {
        /**
         * ID of this situation.
         * @type {string}
         */
        this.id = id;

        /**
         * Tags associated with this situation.
         * @type {string[]}
         */
        this.tags = tags;

        /**
         * Number of times this situation has been successfully entered.
         * This value persists when saving and loading.
         * @type {number}
         */
        this.totalVisits = totalVisits;

        /**
         * If `true`, then presenting choices from this situation will call `debugger`
         * so you can step through the code and see what's up.
         * @type {Boolean}
         */
        this.debugChoices = debugChoices;

        Object.assign(this, {
            getCanChoose, getCanSee, priority, clear,
            displayOrder, optionText, enter, act, exit, content, actions, choices,
            snippets, input, willEnter, autosave,
        });
    }

    /**
     * Returns `true` if this situation has the given tag, otherwise `false`.
     * @param {string} tag The tag to check for
     * @returns {bool}
     */
    hasTag(tag) {
        return this.tags.indexOf(tag) !== -1;
    }

    /** @ignore */
    toSave() {
        return _.pick(this, ['totalVisits', 'id']);
    }

    /** @ignore */
    loadSave(obj) {
        _.assign(this, obj);
    }

    /** @ignore */
    doEnter(model, ui) {
        this.totalVisits += 1;
        if (this.clear) {
            ui.clear();
        }
        if (this.content) {
            ui.writeMarkdown(this.content);
        }
        this.enter.apply(this, arguments);
        if (this.input) {
            ui.promptInput({placeholder: this.input.placeholder})
                .then((value) => { 
                    this.input.store(model, value);
                    model.do(this.input.next);
                });
        }
        if (this.choices) {
            ui.presentChoices(this.choices).then(({situationId, itemId}) => {
                model.do(`@${situationId}`, itemId, 'fake');
            });
        }
    }

    /** @ignore */
    doExit(model, ui, toSituation) {
        ui.nextGroup();
        this.exit.apply(this, arguments);
    }

    /** @ignore */
    doAct(model, ui, action, ...args) {
        if (this.actions && this.actions[action]) {
            this.actions[action](model, ui, ...args);
        } else {
            this.act(model, ui, action);
        }
    }

    /** @ignore */
    getOptionText() {
        if (_.isFunction(this.optionText)) {
            return this.optionText.apply(this, arguments);
        } else {
            return this.optionText || this.id;
        }
    }

    /** @ignore */
    getPriority() {
        if (_.isFunction(this.priority)) {
            return this.priority.apply(this, arguments);
        } else {
            return this.priority;
        }
    }

    /** @ignore */
    getDisplayOrder() {
        if (_.isFunction(this.displayOrder)) {
            return this.displayOrder.apply(this, arguments);
        } else {
            return this.displayOrder;
        }
    }
}