Pour tout problème contactez-nous par mail : support@froggit.fr | La FAQ :grey_question: | Rejoignez-nous sur le Chat :speech_balloon:

Skip to content
Snippets Groups Projects
games.js 164 KiB
Newer Older
  • Learn to ignore specific revisions
  • (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
    
    Pierre Jarriges's avatar
    Pierre Jarriges committed
    module.exports = {
        images_url: "/assets/images"
    
    Pierre Jarriges's avatar
    Pierre Jarriges committed
    },{}],2:[function(require,module,exports){
    
    module.exports = {
        build: {
    
    Pierre Jarriges's avatar
    Pierre Jarriges committed
            protected_dirs: ["assets", "style", "views", "standard"],
    
            default_meta_keys: ["title", "description", "image", "open_graph", "json_ld"],
        },
    };
    
    
    Pierre Jarriges's avatar
    Pierre Jarriges committed
    },{}],3:[function(require,module,exports){
    
    module.exports = {
    
        images_url: `/assets/images`,
        data_url: `/assets/data`,
    
    Pierre Jarriges's avatar
    Pierre Jarriges committed
        translations_url: "/assets/translations"
    
    Pierre Jarriges's avatar
    Pierre Jarriges committed
    },{}],4:[function(require,module,exports){
    
    "use strict";
    
    Pierre Jarriges's avatar
    Pierre Jarriges committed
    /**
     * A tiny library to handle text translation.
     */
    
    /**
     * init parameter object type
     * @typedef TranslatorParam
     * @property {String[]} supported_languages - DEFAULT: ["en"] - An array of ISO 639 lowercase language codes
     * @property {String} locale ISO 639 lowercase language code - DEFAULT: "en"
     *
     * @property {Boolean} use_url_locale_fragment - DEFAULT: true -
     * If true, the 1st fragment of the window.location url will be parsed as a language code
     * Example:
     * window.location
     * > https://example.com/en/some-page...
     * then the /en/ part of the url will be taken in priority and locale will be set to "en".
     * window.location
     * > https://example.com/some-page...
     * No locale fragment will be found so locale will not be set from url fragment.
     * window.location
     * > https://example.com/some-page/en
     * Doesn't work, locale fragment must be the first framgment
     *
     * @property {String} local_storage_key  - DEFAULT: "translator-prefered-language"
     *  The key used to saved the current locale into local storage
     *
     * @property {String} translations_url - REQUIRED - the url of the directory containing the static json files for translations
     * Translations files are expected to be named with their corresponding locale code.
     * Example:
     * if supported_languages is set to ["en", "fr", "it"]
     * and translations_url is set to "https://example.com/translations/""
     * Then the expected translations files are
     * https://example.com/translations/en.json
     * https://example.com/translations/fr.json
     * https://example.com/translations/it.json
     * The json resources must simple key value maps, value being the translated text.
     */
    
    module.exports = {
        locale: "en", // ISO 639 lowercase language code
        supported_languages: ["en"],
        translations: {},
        translations_url: "",
        use_url_locale_fragment: true,
        local_storage_key: "translator-prefered-language",
    
        /**
         * Initialize the lib with params
         * @param {TranslatorParam} params 
         * @returns {Promise}
         */
        init(params) {
            Object.entries(params).forEach(k_v => {
                const [key, value] = k_v;
                if ([
                    "supported_languages",
                    "use_url_locale_fragment",
                    "local_storage_key",
                    "translations_url"
                ].includes(key)) {
                    this[key] = value;
                }
            });
    
            this.translations_url = this.format_translations_url(this.translations_url);
            this.supported_languages = this.format_supported_languages(this.supported_languages);
    
            return new Promise((resolve, reject) => {
                const loc =
                    (() => {// Locale from url priority 1
                        if (this.use_url_locale_fragment) {
                            const first_url_fragment = window.location.pathname.substring(1).split("/")[0];
                            const fragment_is_locale = this.supported_languages.includes(first_url_fragment);
                            return fragment_is_locale ? first_url_fragment : "";
                        } else {
                            return "";
                        }
                    })()
                    || localStorage.getItem(this.local_storage_key) // Locale from storage priority 2
                    || (() => { // locale from navigator priority 3
                        const navigator_locale = navigator.language.split("-")[0].toLocaleLowerCase();
                        return this.supported_languages.includes(navigator_locale)
                            ? navigator_locale
                            : this.supported_languages[0] // Default if navigator locale is not supported
                    })();
    
                fetch(`${this.translations_url}${loc}.json`)
                    .then(response => response.json())
                    .then(response => {
                        this.locale = loc;
                        this.translations = response;
                        resolve();
                    })
                    .catch(err => {
                        this.locale = "en";
                        reject(err);
                    });
            });
        },
    
        /**
         * Return a lowercase string without dash.
         * If given locale is en-EN, then "en" will be returned.
         * @param {String} locale 
         * @returns A lowercase string
         */
        format_locale(locale) {
            return locale.split("-")[0].toLowerCase();
        },
    
        /**
         * Appends a slash at the end of the given string if missing, and returns the url.
         * @param {String} url 
         * @returns {String}
         */
        format_translations_url(url) {
            if (url.charAt(url.length - 1) !== "/") {
                url += "/"
            }
            return url;
        },
    
        /**
         * Return the array of language codes formatted as lowsercase language code.
         * if ["en-EN", "it-IT"]is given, ["en", "it"] will b returned.
         * @param {String[]} languages_codes 
         * @returns {String[]}
         */
        format_supported_languages(languages_codes) {
            return languages_codes.map(lc => this.format_locale(lc));
        },
    
        /**
         * Fetches a new set of translations in case the wanted language changes
         * @param {String} locale A lowercase language code
         * @returns {Promise}
         */
        update_translations(locale) {
            locale = this.format_locale(locale);
            return new Promise((resolve, reject) => {
                fetch(`${this.translations_url}${locale}.json`)
                    .then(response => response.json())
                    .then(response => {
                        this.translations = response;
                        this.locale = locale;
    
                        localStorage.setItem(this.local_storage_key, locale);
    
                        const split_path = window.location.pathname.substring(1).split("/");
                        const first_url_fragment = split_path[0];
                        const fragment_is_locale = this.supported_languages.includes(first_url_fragment);
    
                        if (fragment_is_locale) {
                            split_path.splice(0, 1, locale);
                            const updated_path = split_path.join("/");
                            window.history.replaceState(null, "", "/" + updated_path);
                        }
                        resolve();
                    })
                    .catch(err => {
                        reject(err);
                    });
            });
        },
    
        /**
         * Tries to get the translation of the source string, or return the string as it.
         * @param {String} text The source text to translate
         * @param {Object} params Some dynamic element to insert in the text
         * Params can be used if the translated text provide placeholder like {%some_word%}
         * Example:
         * translator.trad("Some trad key", {some_word: "a dynamic parameter"})
         * -> translation for "Some trad key": "A translated text with {%some_word%}"
         * -> will be return as : "A translated text with a dynamic parameter"
         * @returns {String}
         */
        trad: function (text, params = {}) {
            text = this.translations[text] || text;
    
            Object.keys(params).forEach(k => {
                text = text.replace(`{%${k}%}`, params[k]);
            });
    
            return text;
        }
    };
    },{}],5:[function(require,module,exports){
    "use strict";
    
    const MtlAnimation = require("./model/animation");
    const MentaloEngine = require("./mentalo-engine");
    const MtlScene = require("./model/scene");
    const MtlGame = require("./model/game");
    const MtlSoundTrack = require("./model/sound-track");
    const SCENE_TYPES = require("./model/scene-types");
    const MtlChoice = require("./model/choice");
    const MtlGameObject = require("./model/game-object");
    const FrameRateController = require("./lib/frame-rate-controller");
    const font_tools = require("./lib/font-tools");
    const color_tools = require("./lib/color-tools");
    const shape_tools = require("./lib/shape-tools");
    module.exports = {
        MentaloEngine,
        MtlAnimation,
        MtlScene,
        MtlGame,
        MtlSoundTrack,
        SCENE_TYPES,
        MtlChoice,
        MtlGameObject,
        FrameRateController,
        font_tools,
        color_tools,
        shape_tools,
    };
    
    Pierre Jarriges's avatar
    Pierre Jarriges committed
    },{"./lib/color-tools":6,"./lib/font-tools":7,"./lib/frame-rate-controller":8,"./lib/shape-tools":9,"./mentalo-engine":11,"./model/animation":12,"./model/choice":13,"./model/game":15,"./model/game-object":14,"./model/scene":19,"./model/scene-types":18,"./model/sound-track":20}],6:[function(require,module,exports){
    
    "use strict";
    /**
     * Helpers to work with color values
     */
    
    /**
     * Get a RGB color value in String from either "rgb(x,x,x)" or #xxxxxx
     * @param {String} color A RGB color value as String in the form: #ffffff or rgb(x,x,x)
    
    Pierre Jarriges's avatar
    Pierre Jarriges committed
     * @returns {Uint8[]} An array of 3 values from 0 to 256 for [RED, GREEN, BLUE]
    
     */
    function color_str_to_rgb_array(color) {
        color = color.toLowerCase(0);
        if (color.includes("rgb")) {
            return color.replaceAll(/[a-z\(\)w]/g, "")
                .split(",")
                .slice(0, 3)
                .map(n => parseInt(n));
        } else {
            return color.replace("#", "")
                .match(/.{0,2}/g)
                .slice(0, 3)
                .map(hex => parseInt(hex, 16));
        }
    }
    
    /**
     * @param {String} color A color value (#xxxxxx or rbg(x,x,x))
     * @returns The average between each tone of the given color.
     */
    function get_average_rgb_color_tone(color) {
        return color_str_to_rgb_array(color).reduce((tot, n) => tot + n / 3, 0);
    }
    
    /**
     * Gets an array of 3 8bits integers for red green and blue and and return a color value as an hexadecimal string #xxxxxx
    
    Pierre Jarriges's avatar
    Pierre Jarriges committed
     * @param {Uint8[]} rgb 
    
     * @returns {String} The hexacdecimal rgb value of the color including the hash character: #xxxxxx
     */
    function rgb_array_to_hex(rgb) {
        return `#${rgb.slice(0, 3).map(n => {
            const hex = n.toString(16);
            return hex.length < 2 ? '0' + hex : hex;
        }).join('')}`;
    }
    
    /**
     * Gets an array of 4 8bits integers for red green blue and transparency channel and return a color value as an hexadecimal string #xxxxxx
    
    Pierre Jarriges's avatar
    Pierre Jarriges committed
     * @param {Uint8[]} rgb
    
     * @returns {String} The hexacdecimal rgba value of the color including the hash character: #xxxxxxxx
     */
    function rgba_array_to_hex(rgba) {
        rgba = rgba.length === 4 ? rgba : rgba.concat([255])
        return `#${rgba.slice(0, 4).map(n => {
            const hex = n.toString(16);
            return hex.length < 2 ? '0' + hex : hex;
        }).join('')}`;
    }
    
    /**
     * Gets a background color as argument and return a calculated optimal foreground color.
     * For example if the background is dark the function will return a lighten version of the background.
     * @param {String} base_color A RGB color value in hexadecimal form: #ffffff
     * @returns 
     */
    function get_optimal_visible_foreground_color(base_color) {
        // create a 2D 1px canvas context
        const tmp_canvas = document.createElement("canvas");
        tmp_canvas.width = 1;
        tmp_canvas.height = 1;
        const ctx = tmp_canvas.getContext("2d");
    
        // fill it with the given color
        ctx.fillStyle = base_color;
        ctx.fillRect(0, 0, 1, 1);
    
        // Get either a semi-transparent black or semi-transparent white regarding the base color is above or below an avg of 127
        const superpo_color = get_average_rgb_color_tone(base_color) > 127 ? "#0000003f" : "#ffffff3f";
    
        // Fill the pixel with the semi-transparent black or white on top of the base color
        ctx.fillStyle = superpo_color;
        ctx.fillRect(0, 0, 1, 1);
    
        // Return the flatten result of the superposition
        return rgb_array_to_hex(Array.from(ctx.getImageData(0, 0, 1, 1).data));
    }
    
    /**
    
    Pierre Jarriges's avatar
    Pierre Jarriges committed
     * @param {Uint8[]} col1 A RGBA color value as an array of 4 0 to 256 integers
     * @param {Uint8[]} col2 A RGBA color value as an array of 4 0 to 256 integers
    
     * @returns {Boolean} true if the 2 colors are equals
     */
    function same_rgba(col1, col2) {
        return col1[0] === col2[0]
            && col1[1] === col2[1]
            && col1[2] === col2[2]
            && col1[3] === col2[3];
    }
    
    module.exports = {
        get_average_rgb_color_tone,
        get_optimal_visible_foreground_color,
        color_str_to_rgb_array,
        rgb_array_to_hex,
        rgba_array_to_hex,
        same_rgba,
    };
    
    Pierre Jarriges's avatar
    Pierre Jarriges committed
    },{}],7:[function(require,module,exports){
    
    "use strict";
    /**
     * Helpers to works with default web fonts
     */
    
    
    const FONT_FAMILIES = [
        {
            category: "Sans serif", values: [
                { value: "sans-serif", text: "sans-serif" },
                { text: "Arial", value: "Arial, sans-serif" },
                { text: "Helvetica", value: "Helvetica, sans-serif" },
                { text: "Verdana", value: "Verdana, sans-serif" },
                { text: "Trebuchet MS", value: "Trebuchet MS, sans-serif" },
                { text: "Noto Sans", value: "Noto Sans, sans-serif" },
                { text: "Gill Sans", value: "Gill Sans, sans-serif" },
                { text: "Avantgarde", value: "Avantgarde, TeX Gyre Adventor, URW Gothic L, sans-serif" },
                { text: "Optima", value: "Optima, sans-serif" },
                { text: "Arial Narrow", value: "Arial Narrow, sans-serif" }
            ],
        },
        {
            category: "Serif", values: [
                { text: "serif", value: "serif" },
                { text: "Times", value: "Times, Times New Roman, serif" },
                { text: "Didot", value: "Didot, serif" },
                { text: "Georgia", value: "Georgia, serif" },
                { text: "Palatino", value: "Palatino, URW Palladio L, serif" },
                { text: "Bookman", value: "Bookman, URW Bookman L, serif" },
                { text: "New Century Schoolbook", value: "New Century Schoolbook, TeX Gyre Schola, serif" },
                { text: "American Typewriter", value: "American Typewriter, serif" }
            ],
        },
        {
            category: "Monospace", values: [
                { text: "monospace", value: "monospace" },
                { text: "Andale Mono", value: "Andale Mono, monospace" },
                { text: "Courrier New", value: "Courier New, monospace" },
                { text: "Courrier", value: "Courier, monospace" },
                { text: "FreeMono", value: "FreeMono, monospace" },
                { text: "OCR A Std", value: "OCR A Std, monospace" },
                { text: "DejaVu Sans Mono", value: "DejaVu Sans Mono, monospace" },
            ],
        },
        {
            category: "Cursive", values: [
                { text: "cursive", value: "cursive" },
                { text: "Comic Sans MS", value: "Comic Sans MS, Comic Sans, cursive" },
                { text: "Apple Chancery", value: "Apple Chancery, cursive" },
                { text: "Bradley Hand", value: "Bradley Hand, cursive" },
                { text: "Brush Script MT", value: "Brush Script MT, Brush Script Std, cursive" },
                { text: "Snell Roundhand", value: "Snell Roundhand, cursive" },
                { text: "URW Chancery L", value: "URW Chancery L, cursive" },
            ],
        }
    ];
    
    const TEXT_ALIGN_OPTIONS = [
        { value: "left", text: "Left" }, // TODO trad or icon
        { value: "right", text: "Right" },
        { value: "center", text: "Center" },
    ];
    
    const FONT_STYLE_OPTIONS = [
        { value: "normal", text: "Normal" },
        { value: "italic", text: "Italic" }, // TODO trad or icon
    ];
    
    const FONT_WEIGHT_OPTIONS = [
        { value: "1", text: "Thin" },
        { value: "normal", text: "Normal" },
        { value: "900", text: "Bold" },
    ];
    
    /**
     * Gets an object description of a font and returns it formatted as a string that can be given to a 2D drawing context.
     * Example : ctx.font = get_canvas_font({font_family:"monospace", font_size: 32})
     * @param {Object} font_data An object description of the font settings, with font_size, font_family, font_style, font_weight.s
     * @returns {String}
     */
    function get_canvas_font(font_data = {}) {
        const { font_size = 20, font_family = "sans-serif", font_style = "normal", font_weight = "normal" } = font_data;
        const font_variant = "normal";
        return `${font_style} ${font_variant} ${font_weight} ${font_size.toFixed()}px ${font_family}`;
    }
    
    /**
     * @returns {Object} A map of all defined constants for font families, text align options, font styles and font weights.
     */
    function get_font_options() {
        return {
            font_families: FONT_FAMILIES,
            text_align_options: TEXT_ALIGN_OPTIONS,
            font_style_options: FONT_STYLE_OPTIONS,
            font_weight_options: FONT_WEIGHT_OPTIONS,
        };
    }
    
    /**
    
    Pierre Jarriges's avatar
    Pierre Jarriges committed
     * An approximation of the average width and height of a character for a 2D drawing context with a given font configuration.
    
     * @param {CanvasRenderingContext2D} ctx
    
    Pierre Jarriges's avatar
    Pierre Jarriges committed
     * @returns {Object} An object with {width, height, text_line_height} entries
    
    Pierre Jarriges's avatar
    Pierre Jarriges committed
    function get_canvas_char_size(ctx, font_settings) {
        ctx.save();
        ctx.font = get_canvas_font(font_settings);
    
    
        const str = `Lorem ipsum dolor Sit amet, Consectetur adipiscing elit`;
        const width = ctx.measureText(str).width / str.length;
    
    Pierre Jarriges's avatar
    Pierre Jarriges committed
    
        // Scale width by an approximative 1.1 factor to calculate the height, 
        // for example for letters like j q p etc that have a part bellow the text base line
    
        const height = ctx.measureText("M").width * 1.1;
    
    Pierre Jarriges's avatar
    Pierre Jarriges committed
        const text_line_height = height + height / 4;
    
        ctx.restore();
    
    
        return {
            width,
            height,
            text_line_height,
    
    Pierre Jarriges's avatar
    Pierre Jarriges committed
            interline_height: height / 4
    
        }
    }
    
    module.exports = {
        get_canvas_font,
        get_font_options,
        get_canvas_char_size,
        FONT_FAMILIES,
        TEXT_ALIGN_OPTIONS,
        FONT_STYLE_OPTIONS,
        FONT_WEIGHT_OPTIONS
    };
    
    Pierre Jarriges's avatar
    Pierre Jarriges committed
    },{}],8:[function(require,module,exports){
    
    "use strict";
    
    /**
     * A little helper class to control the rendering frame rate
     */
    class FrameRateController {
        /**
         * @param {Integer} fps The wanted frame rate in frame per second
         */
        constructor(fps) {
            this.tframe = performance.now();
            this.interval = 1000 / fps; // convert in milliseconds
            this.initial = true;
        }
    
        /**
         * @returns {Boolean} true if the defined interval is elapsed.
         */
        nextFrameReady() {
            if (this.initial) {
                this.initial = false;
                return true;
            }
    
            const now = performance.now();
            const elapsed = now - this.tframe;
            const ready = elapsed > this.interval;
    
            if (ready) {
                this.tframe = now - (elapsed % this.interval);
            }
    
            return ready;
        }
    }
    
    module.exports = FrameRateController;
    
    Pierre Jarriges's avatar
    Pierre Jarriges committed
    },{}],9:[function(require,module,exports){
    
    "use strict";
    
    /**
     * Draws a rectangle on a canvas 2D context with support of corner rounding and border options.
     * @param {CanvasRenderingContext2D} ctx
     * @param {Integer} x 
     * @param {Integer} y 
     * @param {Integer} width 
     * @param {Integer} height 
     * @param {Object} options 
     */
    function draw_rect(ctx, x, y, width, height, options = {
        rounded_corners_radius: 0,
        border: { width: 0, color: "rgba(0,0,0,0)" },
        fill_color: "black",
    }) {
        const { rounded_corners_radius = 0, border = { width: 0, color: "rgba(0,0,0,0)" }, fill_color = "black", fill_image } = options;
        const smallest_axis = Math.min(width, height);
        const radius = rounded_corners_radius > smallest_axis / 2 ? smallest_axis / 2 : rounded_corners_radius;
    
        ctx.save();
    
        ctx.beginPath();
        ctx.arc(x + radius, y + radius, radius, Math.PI, 3 * Math.PI / 2);
        ctx.lineTo(x + width - radius, y);
        ctx.arc(x + width - radius, y + radius, radius, 3 * Math.PI / 2, 0);
        ctx.lineTo(x + width, y + height - radius);
        ctx.arc(x + width - radius, y + height - radius, radius, 0, Math.PI / 2);
        ctx.lineTo(x + radius, y + height);
        ctx.arc(x + radius, y + height - radius, radius, Math.PI / 2, Math.PI);
        ctx.closePath();
    
        ctx.clip();
    
        if (fill_image) {
            ctx.drawImage(
                fill_image.src,
                0, 0,
                fill_image.src.width, fill_image.src.height,
                fill_image.dx, fill_image.dy,
                fill_image.dw, fill_image.dh,
            )
        } else {
            ctx.fillStyle = fill_color;
            ctx.fillRect(x, y, width, height);
        }
    
        if (border.width > 0) {
            ctx.strokeStyle = border.color;
            ctx.lineWidth = border.width;
            ctx.stroke();
        }
    
        ctx.restore();
    }
    
    module.exports = {
        draw_rect,
    }
    
    Pierre Jarriges's avatar
    Pierre Jarriges committed
    },{}],10:[function(require,module,exports){
    
    Pierre Jarriges's avatar
    Pierre Jarriges committed
    const { get_canvas_font, get_canvas_char_size } = require("./font-tools");
    
    /**
     * A utility function to display text inside of a box coordinates
     * @param {CanvasRenderingContext2D} ctx
     * @param {String[]} lines 
     * @param {Object} bounds A bounding box object like {left, right, top, bottom, width, height}
     * @param {Object} settings An object with styling settings like {padding, font_color, background_color, ...}
     * @param {Object} a_state_ref Any object having a persistent life in the calling instance
     * @param {Boolean} streamed Wether the text should be displayed as a progressive text streaming or all at once
     * @returns {Boolean} true if the text is fully displayed
     */
    function draw_text_in_bounds(ctx, lines, bounds, settings, a_state_ref, streamed) {
        ctx.save();
        const char_size = get_canvas_char_size(ctx, settings);
        const line_height = char_size.text_line_height;
        ctx.font = get_canvas_font(settings);
        ctx.fillStyle = settings.font_color;
        ctx.textAlign = settings.text_align;
        ctx.textBaseline = "top";
    
        const get_text_position = () => {
            const { padding = 0 } = settings;
            const { left, top, width } = bounds;
            switch (ctx.textAlign) {
                case "left":
                    return {
                        x: left + padding,
                        y: top + padding,
                    }
                case "right":
                    return {
                        x: left + width - padding,
                        y: top + padding,
                    }
                case "center":
                    return {
                        x: left + width / 2,
                        y: top + padding,
                    }
                default:
                    return {
                        x: left + padding,
                        y: top + padding,
                    }
            }
        };
    
        const text_pos = get_text_position();
    
        a_state_ref.stream = a_state_ref.stream || {
            line_chars: 0,
            line_index: 0,
            complete: false,
        };
    
        const output_lines = streamed ? lines.map((line, i) => {
            const stream_state = a_state_ref.stream;
            if (i < stream_state.line_index || stream_state.complete) {
                return line;
            } else if (i > stream_state.line_index) {
                return "";
            } else {
                const slice = line.slice(0, ++stream_state.line_chars);
                if (slice.length === line.length) {
                    stream_state.complete = lines.length - 1 === stream_state.line_index;
                    stream_state.line_index = stream_state.complete
                        ? stream_state.line_index
                        : stream_state.line_index + 1;
                    stream_state.line_chars = 0;
                }
                return slice;
            }
        }) : (() => {
            a_state_ref.stream.complete = true;
            return lines;
        })();
    
        output_lines.forEach(line => {
            ctx.fillText(line, text_pos.x, text_pos.y);
            text_pos.y += line_height;
        });
    
        ctx.restore();
    
        return a_state_ref.stream.complete;
    }
    
    module.exports = {
        draw_text_in_bounds,
    }
    },{"./font-tools":7}],11:[function(require,module,exports){
    "use strict";
    
    
    const MtlGame = require("./model/game");
    const SCENE_TYPES = require("./model/scene-types");
    const MtlRender = require("./render/render");
    const { supported_locales, get_translated } = require("./translation");
    
    /**
     * The encapsulating class of all the components of the Mentalo game engine.
     * This is the class that should be used from outside to run a game.
     */
    class MentaloEngine {
        /**
         * Initialize A MtlGame instance, populates it with the gama_data given as param.
         * Initializes the DOM element to contain the rendering of the engine.
         * Defines the listeners for Escape key and initialize a MtlRender instance.
         * @param {Object} params 
         * Parameter params.game_data is required and should be the result of the exportation format of the Mentalo editor app.
         */
        constructor(params) {
            this.game = new MtlGame();
            this.game.load_data(params.game_data);
    
            this.game.on_load_resources(() => this.loading = false);
    
            this.use_locale = params.use_locale && supported_locales.includes(params.use_locale)
                ? params.use_locale
                : "en";
    
            /**
             * Allow to exit game by typing Escape or q
             * @param {Event} e 
             */
            this.escape_listener = e => {
                const k = e.key.toLowerCase();
                (k === "escape" || k === "q") && this.quit();
            };
    
            window.addEventListener("keydown", this.escape_listener);
    
            this.on_quit_game = function () {
                if (this.used_default_container) {
                    this.container.remove();
                }
                window.removeEventListener("keydown", this.escape_listener);
                params.on_quit_game && params.on_quit_game();
            };
    
            this.used_default_container = false;
    
            this.container = params.container || (() => {
                const el = document.createElement("div");
                el.style.display = "flex";
                el.style.justifyContent = "center";
                el.style.alignItems = "center";
                el.style.position = "absolute";
                el.style.top = 0;
                el.style.bottom = 0;
                el.style.left = 0;
                el.style.right = 0;
                el.style.zIndex = 1000;
                el.style.overflow = "hidden";
                document.body.appendChild(el);
                this.used_default_container = true
                return el;
            })();
    
            this.render = new MtlRender({
                container: this.container,
                fullscreen: params.fullscreen,
                frame_rate: params.frame_rate,
                get_game_settings: this.get_game_settings.bind(this),
                get_game_scenes: this.get_game_scenes.bind(this),
                get_scene: this.get_scene.bind(this),
    
    Pierre Jarriges's avatar
    Pierre Jarriges committed
                get_scene_index: this.get_scene_index.bind(this),
    
                get_inventory: this.get_inventory.bind(this),
                on_game_object_click: this.on_game_object_click.bind(this),
                on_drop_inventory_object: this.on_drop_inventory_object.bind(this),
                on_choice_click: this.on_choice_click.bind(this),
            });
    
            this.loading = true;
            this.will_quit = false;
        }
    
        /**
         * Returns a fragment of the game instance ui_options, which are the styling settings of the game interface.
         * Can be the settings for text_boxes, choices_panel, inventory, etc.
         * @param {String} key The key of the wanted interface settings.
         * @returns {Object} The fragment of this.game.game_ui_options for the given key.
         */
        get_game_settings(key) {
            return this.game.game_ui_options[key];
        }
    
        /**
    
    Pierre Jarriges's avatar
    Pierre Jarriges committed
         * @returns {MtlScene[]} Returns an array of the scenes of the game.
    
         */
        get_game_scenes() {
            return this.game.scenes;
        }
    
        /**
         * @returns {MtlScene} The scene that is currently displayed
         */
        get_scene() {
            return this.game.get_scene();
        }
    
    
    Pierre Jarriges's avatar
    Pierre Jarriges committed
        /**
         * @returns {Integer} The index of the scene that is currently displayed
         */
        get_scene_index() {
            return this.game.scenes.indexOf(this.get_scene());
        }
    
    
        /**
         * Returns the game objects currently saved in the inventory.
    
    Pierre Jarriges's avatar
    Pierre Jarriges committed
         * @returns {MtlGameObject[]}
    
         */
        get_inventory() {
            return this.game.state.inventory;
        }
    
        /**
         * Sets the flag this.will_quit to true.
         * This flag will be parsed in the rendering loop and the engine will then effectively exit the game calling __exit().
         */
        quit() {
            this.will_quit = true;
        }
    
        /**
         * Clean the render instance, stops the rendering loop and calls the on_quit_game callback.
         * This is called when the rendering loop reads the this.will_quit flag as true.
         */
        __quit() {
            window.cancelAnimationFrame(window.mentalo_engine_animation_id);
            this.render.exit_fullscreen();
            this.render.clear_event_listeners();
            this.game.get_soundtrack().stop();
            this.on_quit_game();
        }
    
    
        /**
         * Initialize the rendering loop and the MtlRender instance.
         * Also checks that the first loaded scene has an image to load. 
         * If it doesn't the game will exit immediately in order to avoid having a black screen.
         * ( If the first scene doesn't have an image to load, the game.on_load_resource callback will 
         * never be called and the engine will remain in loading state.  )
         */
        init() {
            window.requestAnimationFrame = window.requestAnimationFrame
                || window.mozRequestAnimationFrame
                || window.webkitRequestAnimationFrame
                || window.msRequestAnimationFrame;
    
            window.cancelAnimationFrame = window.cancelAnimationFrame
                || window.mozCancelAnimationFrame;
    
            if (window.mentalo_engine_animation_id) {
                window.cancelAnimationFrame(window.mentalo_engine_animation_id);
            }
    
            this.render.init();
    
    
    Pierre Jarriges's avatar
    Pierre Jarriges committed
            if (this.game.scenes.find(scene => scene.animation.empty)) {
                alert(get_translated("Some scenes have an empty image, game cannot be loaded", this.use_locale));
    
                this.quit();
            }
        }
    
        /**
         * Clear the event listeners and rendering element related to the current scene from the MtlRender instance,
         * And sets the game current scene state to a new index.
         * @param {Integer} index 
         */
        go_to_scene(index) {
            this.render.reset_user_error_popup();
            this.render.clear_event_listeners();
            this.game.get_soundtrack().stop();
            this.game.go_to_scene(index);
            this.render.clear_children();
            this.render.reset_text_box_visibility();
        }
    
        /**
         * Event listener for doubleclick on a game object.
         * Adds the object to inventory.
         * @param {MtlGameObject} obj 
         */
        on_game_object_click(obj) {
            this.game.inventory_has_empty_slot() && this.game.add_object_to_inventory(obj);
        }
    
        /**
         * All callback called from the inventory render component when an object image is clicked in the inventory.
         * Removes the object from inventory.
         * @param {MtlGameObject} obj 
         */
        on_drop_inventory_object(obj) {
            this.game.remove_object_from_inventory(obj);
        }
    
        /**
         * Callback called from the choices_panel render component.
         * Handles the click on a choice.
         * @param {MtlChoice} choice 
         * @returns 
         */
        on_choice_click(choice) {
            const { destination_scene_index, use_objects } = choice;
            if (destination_scene_index === -1) { // -1 is default index when the choice destination scene is not set.
                this.render.set_user_error_popup({
                    text: get_translated("Destination scene has not been set.", this.use_locale),
                });
                return;
            }
    
            if (destination_scene_index === -2) { // -2 is the index used to indicate that destination scene is just the end of program.
                this.quit()
                return;
            }
    
            // Handle the case where the clicked choice requires some objects to be presents in the inventory.
            if (use_objects.value) {
                const inventory = this.get_inventory();
                const objs = [];
                for (const it of use_objects.items) {
                    const ob = Array.from(inventory).find(o => o.name === it.name);
                    if (ob) {
                        objs.push(ob);
                    } else {
                        this.render.set_user_error_popup({
    
    Pierre Jarriges's avatar
    Pierre Jarriges committed
                            text: use_objects.missing_object_message, stream_text: true,
    
                        });
                        return;
                    }
                }
    
                objs.forEach(o => use_objects.items
                    .find(it => o.name === it.name)
                    .consume_object && this.game.consume_game_object(o));
            }
    
            this.go_to_scene(destination_scene_index);
        }
    
        /**
         * Callback called when a scene of type "Cinematic" reaches end.
         */
        on_cinematic_end() {
            const scene = this.game.get_scene();
            if (scene.end_cinematic_options.quit) {
                this.quit();
            } else if (scene.end_cinematic_options.destination_scene_index === -1) {
                this.render.set_user_error_popup({
                    text: get_translated("Next scene has not been set.", this.use_locale),
                    on_close: this.quit.bind(this),
                });
            } else {
                this.go_to_scene(scene.end_cinematic_options.destination_scene_index);
            }
        }
    
        /**
         * The rendering loop.
         * Handles the different states of the engine (quit, loading or runnning normally)
         * @returns The id of the registered window.requestAnimationFrame
         */
        run_game() {
            if (this.will_quit) {
                this.__quit();
                return;
            } else if (this.loading) {
                this.render.draw_loading();
            } else {
                if (this.get_scene()._type === SCENE_TYPES.CINEMATIC) {
                    this.game.update_cinematic_timeout();
                    if (this.game.is_cinematic_ended()) {
                        this.on_cinematic_end();
                    }
                }
    
                const sound_track = this.game.get_soundtrack();
                if (sound_track.loaded && !sound_track.is_playing()) {
                    sound_track.play();
                }
    
                this.render.draw_game();
            }
    
            window.mentalo_engine_animation_id = requestAnimationFrame(this.run_game.bind(this));
        }
    }
    
    module.exports = MentaloEngine;
    
    Pierre Jarriges's avatar
    Pierre Jarriges committed
    },{"./model/game":15,"./model/scene-types":18,"./render/render":23,"./translation":35}],12:[function(require,module,exports){
    
    "use strict";
    const Loadable = require("./loadable");
    
    /**
     * The data type used for a MtlScene animation
     */
    class MtlAnimation extends Loadable {
        /**
         * Initialize an empty instance of Image HtmlElement,
         * and registers the onload callback to update the instance with the loaded image actual dimensions.
         */
        constructor() {
            super(new Image(), "image", "load");
            this.image.onload = () => this.update_dimensions();
    
            this.name = "";
            this.dimensions = {
                width: 0,
                height: 0,
            };
            this.frame_nb = 1;
            this.frame = 0;
            this.speed = 1;
            this.play_once = false;
            this.initialized = false;
            this.finished = false;
        }
    
        /**
         * Copies the dimensions of the loaded image at the root level of the instance.