Source: index.js

/*eslint no-console: 0*/

// /**
//  * A CLI environment-like game
//  * @module cmdpp-core
//  */

import filesize from 'filesize';
import { loadCommands as loadCMDs } from './commands';
import { getStorage } from './storage';
import pJSON from '../package.json';

// TODO: Replace current CMD#storage with an object containing storage data
// TODO: Move CMD#_commands to CMD#Commands
// TODO: Create more JSDoc

/** Class representing the main game logic.
 * @typicalname cmd
 * @class
 * @example <caption>Instantiate a CMD object.</caption>
 * import { CMD } from 'cmdpp-core';
 * import fs from 'fs';
 * var cmdContainer = {
 *   data: 0,
 *   money: 0
 * };
 * var cmd = new CMD({
 *   (...txt) => console.log(...txt),
 *   (cmdData) => fs.writeFileSync('save.json', JSON.stringify(cmdData, null, 2)),
 *   () => return JSON.parse(fs.readFileSync('save.json')),
 *   (cmdObj) => {
 *     cmdContainer.data = cmdObj.data;
 *     cmdContainer.money = cmdObj.money;
 *   },
 *   function() {
 *     return {
 *       stringDesc: {
 *         func: () => this.respond("First test run!"),
 *         desc: "Desc can be a string"
 *       },
 *       functionDesc: {
 *         func: () => this.respond("Second test run!"),
 *         desc: () => "Desc can also be a function that returns a string or an array of strings."
 *       },
 *       buyableCommand: {
 *         func: () => this.respond("buyable command!"),
 *         desc: 'This command must be bought with the "buyCommand" command.',
 *         price: 10
 *       }
 *     };
 *   },
 *   (err) => console.error(err),
 *   true
 * });
 */
class CMD {

    /**
     * Instantiate the CMD object
     * @param {!CMD~respondCallback} respond - Function for responding.
     * @param {!CMD~saveCallback} save - Function for saving.
     * @param {!CMD~loadCallback} load - Function for loading.
     * @param {!CMD~updateCallback} update - Function for updating.
     * @param {!CMD~commandProviderCallback} commandProvider - Function to provide custom commands. Cannot be ES6 arrow function.
     * @param {!CMD~errorHandlerCallback} errorHandler - Function for error handling.
     * @param {boolean} debug=false - Debug mode.
     */
    // constructor(options) {
    constructor(respond, save, load, update, commandProvider, errorHandler, debug) {
        // var defaults = {
        //     debug: false,
        //     funcs: {
        //         respond: (...txt) => console.log(...txt),
        //         save: () => console.warn('No save function has been set.'),
        //         load: () => console.warn('No load function has been set.'),
        //         update: () => console.warn('No update function has been set.'),
        //         // reset: () => console.warn('No reset function has been set.'),
        //         errorHandler: (e) => console.error(e)
        //     },
        //     errorHandler: (e) => console.error(e),
        //     commandProvider: function() {}
        // };
        
        respond = respond || ((...txt) => console.log(...txt));
        save = save || (() => console.warn('No save function has been set.'));
        load = load || (() => console.warn('No load function has been set.'));
        update = update || (() => console.warn('No update function has been set.'));
        
        commandProvider = commandProvider || function() {};
        errorHandler = errorHandler || ((e) => console.error(e));
        debug = debug || false;
        
        var options = {
            funcs: {
                respond,
                save,
                load,
                update
            },
            commandProvider,
            errorHandler,
            debug
        };
        // var opts = Object.assign({}, defaults, options);
        // options = opts;

        this.version = pJSON.version;

        this.loadCommands = loadCMDs;
        this.commandProvider = options.commandProvider;
        this.money = 0;
        this.increment = 1;
        this.autoIncrement = 1;
        this.isAutoMining = false;
        // this.storage = "selectronTube";
        this.data = 0;
        this.counter = 0;

        this.debug = options.debug;

        this.respondFunc = options.funcs.respond;
        this.saveFunc = options.funcs.save;
        this.loadFunc = options.funcs.load;
        this.updateFunc = options.funcs.update;
        this.resetFunc = options.funcs.reset;
        this.errHandlerFunc = options.funcs.errorHandler;
        // this.loadStorage();
        this.storage = getStorage();
        // this.storage = this.storages["selectronTube"];
        this.loadCommands();
        var customCommands = this.commandProvider();
        Object.assign(this._commands, customCommands);

        for (let cmdName in this._commands) {
            var cmd = this._commands[cmdName];
            if (!('price' in cmd)) {
                cmd.price = 0;
            }
            cmd.unlocked = cmd.price === 0;

            this._commands[cmdName] = cmd;
        }

        // this.command("load");
        this.load();
        this.gameLoopInterval = undefined;
        this.gameLoop();
    }
    
    /**
     * Function to handle responses from the CMD object.
     * @callback CMD~respondCallback
     * @param {...*} txt - Responses
     */
    
    /**
     * Function to handle saving progress.
     * @callback CMD~saveCallback
     * @param {!Object} cmdData - Game progress to be saved.
     * @param {number} cmdData.data - Data collected.
     * @param {number} cmdData.money - Money collected.
     * @param {number} cmdData.increment - Increment value for mineData.
     * @param {number} cmdData.autoIncrement - Increment value for autoMine.
     * @param {string} cmdData.storage - Current storage value.
     * @param {string[]} cmdData.unlocked - Commands bought with buyCommand.
     * @return {?Error} An error if encountered.
     */
    
    /**
     * Function to handle saving progress.
     * @callback CMD~loadCallback
     * @return {Object} Game progress loaded from save.
     */
    
    /**
     * Function to handle updating game values.
     * @callback CMD~updateCallback
     * @param {CMD} cmdObj - CMD object.
     */
      
    /**
     * An object representing a command.
     * @typedef {Object} CMD~Command
     * @property {function} func - Function called when command is run.
     * @property {(string|function)} desc - Description for command.
     * @property {?(string|function)} usage=undefined - How to use the command.
     * @property {?number} price=0 - Price to pay in bytes for command.
     */
     
    /**
     * Function to provide custom commands.
     * @callback CMD~commandProviderCallback
     * @return {CMD~Command} Object of custom commands.
     */
     
    /**
     * Function to handle thrown errors.
     * @callback CMD~errorHandlerCallback
     * @param {Error} err - Error thrown.
     */
    
    /**
     * Start the game loop.
     */
    gameLoop() {
        if (this.gameLoopInterval === undefined) {
            this.gameLoopInterval = setInterval(() => {
                this.counter++;
                if (this.counter % 10 === 0) {
                    // this.command.save(false);
                    // this.command("save");
                    this.save();
                }
                if (this.isAutoMining) {
                    if (this.checkStorage(this.autoIncrement)) {
                        this.addData(this.autoIncrement);
                    } else {
                        this.respond("Please upgrade your storage with upgradeStorage");
                        this.command("autoMine stop");
                    }
                    this.update();
                }
            }, 1000);
        } else {
            console.error('Game loop has already been started.');
        }
    }

    /**
     * Send response to respond function from constructor
     * @param {...*} txt - Strings to be sent to respond function.
     */
    respond(...txt) {
        this.respondFunc(...txt);
    }

    /**
     * Check if storage is full.
     * @param {?number} increment - Increment to check against. If undefined, equal to CMD#increment.
     * @return {boolean} If storage has enough space.
     */
    checkStorage(increment = this.increment) {
        // if (increment === undefined) {
        //     increment = this.increment;
        // }
        // return (this.data <= this.storages[this.storage].capacity);
        // return ((this.data + increment) <= this.storages[this.storage].capacity);
        var check = this.storage.checkStorage(this.data, increment);
        if (this.debug) {
            console.log("Current data:", this.data);
            console.log("Increment:", increment);
            console.log("Check storage:", check);
        }
        return check;
    }

    /**
     * Run command
     * @param {!string} str - Command to be ran.
     */
    command(str) {
        if (str) {
            this.runCommand(str);
            if (this.historyBufferEnabled) {
                if (this.historyBuffer[0] !== str) {
                    this.historyBuffer.unshift(str);
                }
                if (this.historyBuffer.length > 10) {
                    this.historyBuffer.pop();
                }
            }
        }
    }

    runCommand(cmd) {
        if (this.debug) {
            console.log("Command:", cmd);
        }
        if (cmd.indexOf(" ") !== -1 && cmd[cmd.indexOf(" ") + 1] === undefined) {
            this.respond("Command not found.");
            if (this.debug) {
                console.log('Command not found.');
            }
        } else {
            var cmdWArgs = cmd.split(' ');
            if (!(cmdWArgs[0] in this._commands)) {
                this.respond("Command not found.");
            } else {
                // console.log(cmdWArgs);
                if (this._commands[cmdWArgs[0]].unlocked) {
                    this._commands[cmdWArgs[0]].func(...cmdWArgs.slice(1));
                }
            }
        }
    }

    /**
     * Run update function from constructor to update game values
     */
    update() {
        this.updateFunc(this);
    }

    /**
     * Save game progress
     */
    save() {
        var saveObj = {
            data: this.data,
            money: this.money,
            increment: this.increment,
            autoIncrement: this.autoIncrement,
            // storage: this.storage,
            storage: this.storage.name,
            unlocked: []
        };
        for (let cmdName in this._commands) {
            var cmd = this._commands[cmdName];
            if ('price' in cmd && cmd.price !== 0 && cmd.unlocked) {
                saveObj.unlocked.push(cmdName);
            }
        }
        this.saveFunc(saveObj);
    }

    /**
     * Load game progress
     */
    load() {
        var loadData = this.loadFunc();

        var previousSave = true;
        if (!loadData) {
            previousSave = false;
        }
        for (var k in loadData) {
            if (loadData[k] === null) {
                previousSave = false;
                break;
            }
        }
        if (previousSave) {
            if (this.debug) {
                console.log(loadData);
            }
            this.data = loadData.data;
            this.money = loadData.money;
            this.increment = loadData.increment;
            this.autoIncrement = loadData.autoIncrement;
            this.storage.current = loadData.storage;
            for (let cmdName in this._commands) {
                var cmd = this._commands[cmdName];
                if (loadData.unlocked.indexOf(cmdName) === -1 && 'price' in cmd && cmd.price !== 0) {
                    this._commands[cmdName].unlocked = false;
                } else {
                    this._commands[cmdName].unlocked = true;
                }
            }
            // for (var unlockedCMD of loadData.unlocked) {
            //     this._commands[unlockedCMD].unlocked = true;
            // }
            this.respond("Save loaded.");
        } else {
            this.respond("No save found.");
        }
        this.update();
    }

    /**
     * Add data
     * @param {number} amt - Amount to add.
     * @return {boolean} if data was able to be added.
     */
    addData(amt) {
        var hasRoom = false;
        if (this.checkStorage(amt)) {
            this.data += amt;
            hasRoom = true;
        }
        this.update();
        return hasRoom;
    }
    /**
     * Remove data
     * @param {number} amt - Amount to remove.
     * @return {boolean} if data was able to be removed.
     */
    removeData(amt) {
        var hasEnough = false;
        if (this.data >= amt) {
            this.data -= amt;
            hasEnough = true;
        }
        this.update();
        return hasEnough;
    }

    /**
     * Add money
     * @param {number} amt - Amount to add.
     */
    addMoney(amt) {
        this.money += amt;
        this.update();
    }

    /**
     * Remove money
     * @param {number} amt - Amount to remove.
     * @return {boolean} if money was able to be removed.
     */
    removeMoney(amt) {
        var hasEnough = false;
        if (this.money >= amt) {
            this.money -= amt;
            hasEnough = true;
        }
        this.update();
        return hasEnough;
    }

    /**
     * Format bytes into a human-readable format
     * @return {string} CMD#data in human-readable format
     */
    formatBytes() {
        return this.formatter(this.data);
    }

    /**
     * Format number into a human-readable format
     * @param {number} size - Number to be formatted.
     * @return {string} size in human-readable format
     */
    formatter(size) {
        return filesize(size);
    }

    /**
     * Reset game progress
     */
    reset() {
        this.saveFunc({
            data: 0,
            money: 0,
            increment: 1,
            autoIncrement: 1,
            storage: "selectronTube",
            unlocked: []
        });
        this.load();
    }
}


export { CMD };
export default CMD;

// /**
//  * Function to handle responses from the CMD object.
//  * @callback respondCallback
//  * @param {...*} txt - Responses
//  */
// 
// /**
//  * Function to handle saving progress.
//  * @callback CMD~saveCallback
//  * @param {Object} cmdData - Game progress to be saved.
//  * @param {number} cmdData.data - Data collected.
//  * @param {number} cmdData.money - Money collected.
//  * @param {number} cmdData.increment - Increment value for mineData.
//  * @param {number} cmdData.autoIncrement - Increment value for autoMine.
//  * @param {string} cmdData.storage - Current storage value.
//  * @param {string[]} cmdData.unlocked - Commands bought with buyCommand.
//  * @return {Error | null} An error if encountered.
//  */
// 
// /**
//  * Function to handle saving progress.
//  * @callback CMD~loadCallback
//  * @return {Object} Game progress loaded from save.
//  */
// 
// /**
//  * Function to handle updating game values.
//  * @callback CMD~updateCallback
//  * @param {CMD} cmdObj - CMD object.
//  */
// 
// /**
//  * Function to handle thrown errors.
//  * @callback CMD~errorHandlerCallback
//  * @param {Error} err - Error thrown.
//  */
// 
// /**
//  * An object representing a command.
//  * @typedef {Object} Command
//  * @property {function} func - Function called when command is run.
//  * @property {string|function} desc - Description for command.
//  * @property {string|function|undefined} usage=null - How to use the command.
//  * @property {number|undefined} price=0 - Price to pay in bytes for command.
//  */
// 
// /**
//  * Function to provide custom commands.
//  * @callback CMD~commandProviderCallback
//  * @return {Command} Object of custom commands.
//  */