Last commit for game.js: 3301bbd33037444742c54bf21545e2f20d827080

Made CSS file part of project, made subfolders for js and css

Chris Pollett [2023-01-15 05:Jan:th]
Made CSS file part of project, made subfolders for js and css
/**
 * Copyright 2022 Christopher Pollett
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */
 /**
  * Frise Game Library
  */
/**
 *
 */
/**
 *
 */
function elt(id)
{
    return document.getElementById(id);
}
/**
 *
 */
function tag(name)
{
    return document.getElementsByTagName(name);
}
/**
 *
 */
function sel(selector)
{
    return document.querySelectorAll(selector);
}
/**
 *
 */
function xelt(id)
{
    return makeGameObject(elt(id));
}
/**
 *
 */
function xsel(selector)
{
    let tag_objects = sel(selector);
    return makeGameObjects(tag_objects);
}
/**
 *
 */
function xtag(name)
{
    let tag_objects = tag(name);
    return makeGameObjects(tag_objects);
}
/**
 *
 */
function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}
/**
 *
 */
function proceedClick(message)
{
    let game_content = elt("game-content");
    game_content.innerHTML +=
        `<a id='proceed-click' href=''>${message}</a>`;
    return new Promise(resolve => elt('proceed-click').addEventListener(
        "click", resolve));
}
/**
 *
 */
function makeGameObjects(tag_objects)
{
    let game_objects = {};
    for (const tag_object of tag_objects) {
        let game_object = makeGameObject(tag_object);
        if (game_object) {
            game_objects[game_object.id] = game_object;
        }
    }
    return game_objects;
}
/**
 * Upper cases first letter of a string
 * @param String str string to upper case first letter of
 * @return String result of uppercasing
 */
function upperFirst(str)
{
    if (!str) {
        return "";
    }
    let upper_first = str.charAt(0).toUpperCase();
    return upper_first + str.slice(1);
}
/**
 * Used as part of the closure of makeGameObject so as that every game
 * object has an id.
 */
var object_counter = 0;
function makeGameObject(dom_object)
{
    if (!dom_object || dom_object.tagName.substring(0, 2) != "X-") {
        return null;
    }
    let tag_name = dom_object.tagName.slice(2);
    let type = dom_object.getAttribute("type");
    if (type) {
        type = upperFirst(type);
    } else if (tag_name && tag_name != "OBJECT") {
        type = upperFirst(tag_name.toLowerCase());
    } else {
        type = "Object";
    }
    let game_object;
    if (type == "Location") {
        game_object = new Location();
    } else {
        game_object = {};
    }
    if (dom_object.id) {
        game_object.id = dom_object.id;
    } else {
        game_object.id = "oid" + object_counter;
        object_counter++;
    }
    for (const child of dom_object.children) {
        if (child.tagName.substring(0, 2) != "X-") {
            continue;
        }
        let attribute_name = child.tagName.slice(2);
        if (attribute_name) {
            attribute_name = attribute_name.toLowerCase()
            if (attribute_name == 'present') {
                if (!game_object[attribute_name]) {
                    game_object[attribute_name] = [];
                }
                let check = child.getAttribute("ck");
                if (!check) {
                    check = "";
                }
                game_object[attribute_name].push([check, child.innerHTML]);
            } else {
                game_object[attribute_name] = child.innerHTML;
            }
        }
    }
    game_object.type = type;
    return game_object;
}
function toggleDisplay(id, display_type)
{
    if (display_type === undefined) {
        display_type = "block";
    }
    obj = elt(id);
    if (obj.style.display == display_type)  {
        value = "none";
    } else {
        value = display_type;
    }
    obj.style.display = value;
    if (value == "none") {
        obj.setAttribute('aria-hidden', true);
    } else {
        obj.setAttribute('aria-hidden', false);
    }
}
function toggleOptions(elt_id, stop_pos)
{
    let game_content = elt('game-content');
    let elt_obj = elt(elt_id);
    if ((!elt_obj.style.left && !elt_obj.style.right) ||
        elt_obj.style.left == '0px' || elt_obj.style.right == '0px') {
        elt_obj.style.left = (stop_pos - 300) + 'px';
        if (is_mobile) {
            elt_obj.style.width = "240px";
        }
        game_content.style.left = "55px";
        game_content.style.width = "calc(100% - 40px)";
        elt_obj.style.backgroundColor = 'white';
    } else {
        elt_obj.style.left = '0px';
        game_content.style.left = "300px";
        game_content.style.width = "calc(100% - 480px)";
        if (is_mobile) {
            elt_obj.style.width = "100%";
            game_content.style.left = "100%";
        }
        elt_obj.style.backgroundColor = 'lightgray';
    }
}
/**
 *
 */
function addListenersAnchors(anchors)
{
    let call_toggle = false;
    if (arguments.length > 1) {
        if (arguments[1]) {
            call_toggle = true;
        }
    }
    for (const anchor of anchors) {
        let hash = anchor.getAttribute("href");
        if (hash && hash[0] == "#") {
            anchor.addEventListener('click', (event) => {
                if (!anchor.classList.contains('disabled')) {
                    game.takeTurn(hash);
                    if (call_toggle) {
                        toggleOptions('main-nav', 0);
                    }
                }
                event.preventDefault();
                if (window.location.hash) {
                    delete window.location.hash;
                }
            });
        }
    }
}
/**
 *
 */
function disableSavesAndInventory()
{
    let save_links = sel('[href~="#saves"]');
    for (const save_link of save_links) {
        save_link.classList.add('disabled');
    }
    let inventory_links = sel('[href~="#inventory"]');
    for (const inventory_link of inventory_links) {
        inventory_link.classList.add('disabled');
    }
}
/**
 *
 */
function enableSavesAndInventory()
{
    let save_links = sel('[href~="#saves"]');
    for (const save_link of save_links) {
        save_link.classList.remove('disabled');
    }
    let inventory_links = sel('[href~="#inventory"]');
    for (const inventory_link of inventory_links) {
        inventory_link.classList.remove('disabled');
    }
}
/**
 *
 */
function interpolateVariables(text)
{
    let old_text = "";
    while (old_text != text) {
        old_text = text;
        text = text.replace(/obj(?:ect)?\(([^)]+)\);?/,
            "game.objects['$1']");
        text = text.replace(/loc(?:ation)?\(([^)]+)\);?/,
            "game.locations['$1']");
    }
    old_text = "";
    while (old_text != text) {
        old_text = text;
        let interpolate_matches = text.match(/\$\{([^\}]+)\}/);
        if (interpolate_matches && interpolate_matches[1]) {
            let interpolate_var = interpolate_matches[1];
            if (interpolate_var.replace(/\s+/, "") != "") {
                interpolate_var = interpolate_var.replaceAll(/\&gt\;?/g, ">");
                interpolate_var = interpolate_var.replaceAll(/\&lt\;?/g, "<");
                interpolate_var = interpolate_var.replaceAll(/\&amp\;?/g, "&");
                if (interpolate_var) {
                    let interpolate_value = eval(interpolate_var);
                    text = text.replace(/\$\{([^\}]+)\}/, interpolate_value);
                }
            }
        }
    }
    return text;
}
class Location
{
    present = [];
    /**
     *
     */
    async renderPresentation()
    {
        disableSavesAndInventory();
        let game_content = elt("game-content");
        game_content.innerHTML = "";
        for (let section of this.present) {
            if (!section[1]) {
                continue;
            }
            let [check_result, proceed, pause] =
                this.evaluateCheckCondition(section[0]);
            let prepared_section = this.prepareSection(section[1]);
            if (check_result) {
                if (proceed) {
                    let old_inner_html = game_content.innerHTML;
                    event = await proceedClick(proceed);
                    event.preventDefault();
                    game_content.innerHTML = old_inner_html;
                } else if (pause) {
                    await sleep(pause);
                }
                game_content.innerHTML += prepared_section;
            }
        }
        game_content.innerHTML += "<div class='footer-space'></div>";
        let anchors = sel("#game-content a, #game-content x-button");
        addListenersAnchors(anchors);
        enableSavesAndInventory();
    }
    /**
     *
     */
    evaluateCheckCondition(condition)
    {
        let check;
        let proceed = "";
        let pause = 0;
        if (condition == "") {
            check = "";
        } else {
            check = condition;
            let old_check = "";
            while (old_check != check) {
                old_check = check;
                check = check.replace(/obj(?:ect)?\(([^)]+)\);?/,
                    "game.objects['$1']");
                check = check.replace(/loc(?:ation)?\(([^)]+)\);?/,
                    "game.locations['$1']");
                let click_pattern =
                    /(?:clickProceed|waitClick)\(([^)]+)\);?/;
                let click_match = check.match(click_pattern);
                if (click_match) {
                    proceed = click_match[1];
                    check = "";
                    break;
                }
                let pause_pattern = /(?:sleep|delay|pause)\(([^)]+)\);?/;
                let pause_match = check.match(pause_pattern);
                if (pause_match) {
                    pause += parseInt(pause_match[1]);
                }
                check = check.replace(pause_pattern, "");
            }
        }
        let check_result = (check.replace(/\s+/, "") != "") ? eval(check)
            : true;
        return [check_result, proceed, pause];
    }
    /**
     *
     */
    prepareSection(section)
    {
        let old_section = "";
        let quote = `(?:(?:'([^']*)')|(?:"([^"]*)"))`;
        section = interpolateVariables(section);
        while (section != old_section) {
            old_section = section;
            let speaker_pattern = new RegExp(
                "\<x-speaker[^\>]+name\s*\=\s*("+quote+")[^\>]*>", 'i');
            let speaker_match = section.match(speaker_pattern);
            if (speaker_match) {
                let speaker_id = speaker_match[3];
                if (speaker_id && game.objects[speaker_id]) {
                    let speaker = game.objects[speaker_id];
                    let name = speaker.name;
                    let icon = speaker.icon;
                    let html_fragment = speaker_match[0];
                    let html_fragment_mod = html_fragment.replace(/name\s*\=/,
                        "named=");
                    if (icon) {
                        section =
                            section.replace(html_fragment, html_fragment_mod +
                            "<figure><img src='"+speaker.icon+"' " +
                            "loading='lazy' ></figure><div>" + name +
                            "</div><hr>");
                    }
                }
            }
        }
        return section;
    }
}
class Game
{
    timestamp;
    objects;
    locations;
    history;
    future_history;
    constructor()
    {
        this.history = [];
        this.future_history = [];
        this.addMainNav();
        this.initializeObjectsLocations();
    }
    /**
     *
     */
    addMainNav()
    {
        let body_objs = tag("body");
        if (body_objs[0] === undefined) {
            return;
        }
        let body_obj = body_objs[0];
        let main_nav_objs = tag("x-main-nav");
        if (main_nav_objs[0] === undefined) {
            return;
        }
        let main_nav_obj = main_nav_objs[0];
        let game_screen = elt('game-screen');
        if (!game_screen) {
            body_obj.innerHTML = '<div id="game-screen"></div>' +
                body_obj.innerHTML;
            game_screen = elt('game-screen');
        }
        game_screen.innerHTML = `
            <div id="main-bar">
            <button id="main-toggle"
            class="float-left"><span class="main-close">≡</button>
            </div>
            <div id="main-nav">
            <button id="previous-history">←</button>
            <button id="next-history">→</button>
            <div id="game-nav">
            ${main_nav_obj.innerHTML}
            </div>
            </div>
            <div id="game-content"></div>`;
        elt('main-toggle').onclick = (evt) => {
            toggleOptions('main-nav', 0);
        };
        elt('next-history').onclick = (evt) => {
            this.nextHistory();
        }
        elt('previous-history').onclick = (evt) => {
            this.previousHistory();
        }
        let anchors = sel('#game-nav a, #game-nav x-button');
        addListenersAnchors(anchors, is_mobile);
    }
    /**
     *
     */
    initializeScreen()
    {
        if(!is_mobile) {
            return;
        }
        let html = tag("html")[0];
        html.classList.add("mobile");
        let head = tag("head")[0];
        head.innerHTML += `<meta name="viewport" `+
            `content="width=device-width, initial-scale=1.0" >`
        toggleOptions('main-nav', 0);
    }
    /**
     *
     */
    initializeObjectsLocations()
    {
        this.objects = xtag("x-object");
        this.locations = xtag("x-location");
        for (const oid in this.objects) {
            let object = this.objects[oid];
            if (object.position) {
                let location_name = object.position;
                if (this.locations[location_name]) {
                    let location = this.locations[location_name]
                    if (typeof location.items == "undefined") {
                        location.items = [];
                    }
                    location.items.push(object.id);
                }
            }
        }
    }
    /**
     *
     */
    captureState()
    {
        let now = new Date();
        let date = now.getFullYear() + '-' + (now.getMonth() + 1) +
            '-' + now.getDate();
        let time = now.getHours() + ":" + now.getMinutes() + ":"
                + now.getSeconds();
        game.timestamp = date + " " + time;
        return JSON.stringify({
            timestamp: game.timestamp,
            objects: this.objects,
            locations: this.locations
        });
    }
    /**
     *
     */
    restoreState(game_save)
    {
        let game_state = JSON.parse(game_save);
        if (!game_state || !game_state.timestamp ||
            !game_state.objects || !game_state.locations) {
            alert("Does not appear to be a valid game save");
            return false;
        }
        this.timestamp = game_state.timestamp;
        this.objects = game_state.objects;
        let locations = game_state.locations;
        this.locations = {};
        for (const location_name in locations) {
            let location_object = locations[location_name];
            let location = new Location();
            for (const field in location_object) {
                location[field] = location_object[field];
            }
            this.locations[location_name] = location;
        }
        return true;
    }
    /**
     *
     */
    clearHistory()
    {
        this.history = [];
        this.future_history = [];
    }
    /**
     *
     */
    previousHistory()
    {
        if (this.history.length == 0) {
            return;
        }
        let current_state = this.captureState();
        this.future_history.push(current_state);
        let previous_game_state = this.history.pop();
        this.restoreState(previous_game_state);
        sessionStorage.current = previous_game_state;
        this.describeMainCharacterLocation();
        if (this.history.length == 0) {
            elt('previous-history').disabled = true;
        }
        elt('next-history').disabled = false;
    }
    /**
     *
     */
    nextHistory()
    {
        if (this.future_history.length == 0) {
            return;
        }
        let current_state = this.captureState();
        this.history.push(current_state);
        let next_game_state = this.future_history.pop();
        this.restoreState(next_game_state);
        sessionStorage.current = next_game_state;
        this.describeMainCharacterLocation();
        if (this.future_history.length == 0) {
            elt('next-history').disabled = true;
        }
        elt('previous-history').disabled = false;
    }
    /**
     *
     */
    initSlotStates()
    {
        let saves_location = game.locations['saves'];
        for (const field in saves_location) {
            let slot_matches = field.match(/^slot(\d+)/);
            if (slot_matches && slot_matches[1]) {
                let slot_number = parseInt(slot_matches[1]);
                console.log("slot" + slot_number);
                let game_save = sessionStorage.getItem("slot" + slot_number);
                if (game_save) {
                    let game_state = JSON.parse(game_save);
                    saves_location["slot" + slot_number] = 'Load';
                    saves_location["delete" + slot_number] = "";
                    saves_location["filled" + slot_number] = 'filled';
                    saves_location["filename" + slot_number] =
                        game_state.timestamp;
                } else {
                    saves_location["slot" + slot_number] = 'Save';
                    saves_location["filled" + slot_number] = 'not-filled';
                    saves_location["delete" + slot_number] = "disabled";
                    saves_location["filename" + slot_number] = '...';
                }
            }
        }
    }
    /**
     *
     */
    saveLoadSlot(slot_number)
    {
        slot_number = parseInt(slot_number);
        let saves_location = game.locations['saves'];
        let game_state = sessionStorage.getItem("slot" + slot_number);
        if (game_state) {
            this.clearHistory();
            sessionStorage.current = game_state;
            this.restoreState(game_state);
        } else {
            let save_state = this.captureState();
            game_state = this.history[this.history.length - 1];
            this.restoreState(game_state);
            saves_location['filename' + slot_number] =  this.timestamp;
            sessionStorage.setItem("slot" + slot_number, game_state);
            this.restoreState(save_state);
            this.evaluateAction(saves_location['default-action']);
        }
    }
    /**
     *
     */
    deleteSlot(slot_number)
    {
        slot_number = parseInt(slot_number);
        let saves_location = game.locations['saves'];
        sessionStorage.removeItem("slot" + slot_number);
        saves_location['filled' + slot_number] = "not-filled";
        saves_location['delete' + slot_number] = "disabled";
        saves_location['slot' + slot_number] = "Save";
        saves_location['filename' + slot_number] =  "...";
    }
    /**
     *
     */
    deleteSlotAll()
    {
        let i = 1;
        let saves_location = game.locations['saves'];
        while (typeof saves_location['filename' + i] !== 'undefined') {
            this.deleteSlot(i);
            i++;
        }
    }
    /**
     *
     */
    load()
    {
        let file_load = elt('file-load');
        if (!file_load) {
            file_load = document.createElement("input");
            file_load.type = "file";
            file_load.id = 'file-load';
            file_load.style.display = 'none';
            file_load.addEventListener('change', (event) => {
                let to_read = file_load.files[0];
                let file_reader = new FileReader();
                file_reader.readAsText(to_read, 'UTF-8')
                file_reader.addEventListener('load', (load_event) => {
                    let game_state = load_event.target.result;
                    this.clearHistory();
                    sessionStorage.current = game_state;
                    this.restoreState(game_state);
                    game.describeMainCharacterLocation();
                });
            });
        }
        file_load.click();
    }
    /**
     *
     */
    save()
    {
        let game_state = this.history[this.history.length - 1];
        let file = new Blob([game_state], {type: "plain/text"});
        let link = document.createElement("a");
        link.href = URL.createObjectURL(file);
        link.download = "game_save.txt";
        link.click();
    }
    /**
     *
     */
    takeTurn(hash)
    {
        if (hash == "#previous") {
            this.previousHistory();
            return;
        } else if (hash == "#next") {
            this.nextHistory();
            return;
        }
        let new_game_state;
        if (sessionStorage.current) {
            new_game_state = sessionStorage.current;
        }
        if (!this.moveMainCharacter(hash)) {
            console.log("exiting");
            return;
        } else {
            console.log("continuing");
        }
        this.future_history = [];
        elt('next-history').disabled = true;
        if (sessionStorage.current) {
            this.history.push(new_game_state);
        }
        this.evaluateDefaultActions(this.objects);
        this.evaluateDefaultActions(this.locations);
        sessionStorage.current = this.captureState();
        this.describeMainCharacterLocation();
        if (this.history.length == 0) {
            elt('previous-history').disabled = true;
        } else {
            elt('previous-history').disabled = false;
        }
    }
    /**
     *
     */
    evaluateDefaultActions(x_entities)
    {
        for (const object_name in x_entities) {
            let game_entity = x_entities[object_name];
            if (game_entity && game_entity['default-action']) {
                this.evaluateAction(game_entity['default-action']);
            }
        }
    }
    /**
     *
     */
    moveObject(object_id, destination)
    {
        let move_object = this.objects[object_id];
        if (!move_object || !this.locations[destination]) {
            return false;
        }
        let old_position = move_object.position;
        let old_location = this.locations[old_position];
        old_location.items.filter((value) => {
            value != object_id;
        });
        move_object.position = destination;
        let new_location = this.locations[destination];
        if (!new_location.items) {
            new_location.items = [];
        }
        new_location.items.push(object_id);
        return true;
    }
    /**
     *
     */
    moveMainCharacter(hash)
    {
        if (!hash || hash <= 1) {
            return true;
        }
        hash = hash.substring(1);
        let hash_parts = hash.split("=>");
        let destination = hash_parts.pop();
        for (const hash_part of hash_parts) {
            let hash_matches = hash_part.match(/([^\(]+)(\(([^\)]+)\))?\s*/);
            let action = elt(hash_matches[1]);
            let args = [];
            if (typeof hash_matches[3] !== 'undefined') {
                args = hash_matches[3].split(",");
            }
            if (action && action.tagName == 'X-ACTION') {
                let code = action.innerHTML;
                if (code) {
                    this.evaluateAction(code, args);
                }
            }
        }
        if (destination == "exit") {
            this.describeMainCharacterLocation();
            return false;
        }
        if (destination == "previous") {
            this.previousHistory();
            return false;
        }
        if (destination == "next") {
            this.nextHistory();
            return false;
        }
        this.moveObject('main-character', destination);
        return true;
    }
    /**
     *
     */
    evaluateAction(code)
    {
        var args = [];
        if (arguments.length > 1) {
            if (arguments[1]) {
                args = arguments[1];
            }
        }
        let old_code = "";
        while (old_code != code) {
            old_code = code;
            code = code.replace(/obj(?:ect)?\(([^)]+)\);?/,
                "game.objects['$1']");
            code = code.replace(/loc(?:ation)?\(([^)]+)\);?/,
                "game.locations['$1']");
        }
        eval(code);
    }
    /**
     *
     */
    describeMainCharacterLocation()
    {
        let main_character = this.objects['main-character'];
        let position = main_character.position;
                console.log(position);
        let location = this.locations[position];
        location.renderPresentation();
    }
}
/**
 *
 */
var game;
/**
 *
 */
var is_mobile = window.matchMedia("(max-width: 1000px)").matches;
/**
 *
 */
async function initGame()
{
    game = new Game();
    if (sessionStorage.current) {
        game.restoreState(sessionStorage.current);
    }
    game.takeTurn("");
    game.clearHistory();
    game.initializeScreen();
}
ViewGit