UNPKG

rpg-table-randomizer

Version:

Module for random tables for use in roleplaying games

392 lines (379 loc) 14.5 kB
import { isEmpty, isUndefined } from './r_helpers.js'; import RandomTable from './RandomTable.js'; import RandomTableEntry from './RandomTableEntry.js'; import RandomTableResult from './RandomTableResult.js'; import RandomTableResultSet from './RandomTableResultSet.js'; import { getDiceResult } from './dice_roller.js'; import TableError from './TableError.js'; import TableErrorResult from './TableErrorResult.js'; import { randomString } from './randomizer.js'; /** * Define the regex to find tokens * This looks for anything between double brackets. * Note: this is duplicated in RandomTable.findDependencies() so if updated, update it there too */ const tokenRegExp = /({{2}.+?}{2})/g; /** * TableRoller * Handles rolling on tables and tokens in tables/results. * @constructor */ class TableRoller { constructor ({ token_types = {} }) { this.token_types = { roll: this._defaultRollToken.bind(this), table: this._defaultTableToken.bind(this), oneof: this._defaultOneOfToken.bind(this) }; Object.keys(token_types).forEach((token) => { this.token_types[token] = token_types[token]; }); } /** * Return an error result * @param {String} error Error message * @param {String} key RandomTable key where error occured. * @param {String} table Sub/table name if relevant. * @returns {TableErrorResult} */ _getErrorResult (error = '', key = '', table = '') { return new TableErrorResult({ key, table, result: error }); } /** * Return a result set with an error. * @param {String} error Error message * @param {String} key RandomTable key where error occured. * @returns {RandomTableResultSet} */ _getErrorResultSet (error = '', key = '') { return new RandomTableResultSet({ key, results: [ this._getErrorResult(error, key) ] }); } /** * Get a result from a table/subtable in a RandomTable object * DANGER: you could theoretically put yourself in an endless loop if the data were poorly planned * Calling method try to catch RangeError to handle that possibility. * @param {RandomTable} rtable the RandomTable object * @param {String} table table to roll on * @returns {RandomTableResult[]} */ _selectFromTable (rtable, table) { if (!(rtable instanceof RandomTable)) { return [this._getErrorResult('Invalid table.')]; } let o = []; // Results const entry = rtable.getRandomEntry(table); if (entry === null || !(entry instanceof RandomTableEntry)) { return [this._getErrorResult('Invalid subtable name.', rtable.key, table)]; } // if print is false we suppress the output from this table // (good for top-level tables that have subtables prop set) if (entry.print) { // replace any tokens const t_result = this.findToken(entry.label, rtable); o.push(new RandomTableResult({ key: rtable.key, table, result: t_result, desc: entry.description })); } // are there subtables to roll on? if (entry.subtable.length === 0) { // no subtables return o; } // Select from each subtable and add to results. entry.subtable.forEach((subtableName) => { const subresult = this._selectFromTable(rtable, subtableName); // concat because subresult is an array. o = o.concat(subresult); }); return o; } /** * Get results array for macro setting of a table. * @param {RandomTable} rtable Table with macro set. * @returns {RandomTableResult[]} */ _getTableMacroResult (rtable) { let results = []; try { rtable.macro.forEach((macroKey) => { const parts = macroKey.split(':'); const tableKey = parts[0]; const subtable = parts[1] || ''; if (tableKey === rtable.key) { throw new TableError(`Macros can't self reference.`, rtable.key); } try { const mtable = this.getTableByKey(tableKey); const result = this.getTableResult(mtable, subtable); // concat because result is an array. results = results.concat(result); } catch (e) { if (e instanceof TableError) { results.push(this._getErrorResult(e.message, rtable.key, tableKey)); } else { // Rethrow unexpected errors throw e; } } }); } catch (e) { if (e instanceof RangeError) { // This could be an infinite loop of table results referencing each other. results.push(this._getErrorResult(e.message, rtable.key)); } else { throw e; } } return results; } /** * Generate a result from a RandomTable object * @param {RandomTable} rtable the RandomTable * @param {String} [start=''] subtable to roll on * @return {RandomTableResult[]} */ getTableResult (rtable, start = '') { if (!(rtable instanceof RandomTable)) { return [ this._getErrorResult('No table found to roll on.') ]; } let results = []; // if macro is set then we ignore a lot of stuff if (rtable.macro.length > 0) { return this._getTableMacroResult(rtable); } const sequence = rtable.getSequence(start); if (sequence.length === 0) { return results; } try { sequence.forEach((seqKey) => { const r = this._selectFromTable(rtable, seqKey); results = results.concat(r); }); } catch (e) { // In case of infinite recursion if (e instanceof RangeError) { results.push(this._getErrorResult(e.message, rtable.key)); } else { throw e; } } return results; } /** * Get result set from a table based on the key. * @param {String} tableKey * @param {String} table * @returns {RandomTableResultSet} */ getTableResultSetByKey (tableKey, table = '') { try { const rtable = this.getTableByKey(tableKey); const results = this.getTableResult(rtable, table); return new RandomTableResultSet({ key: rtable.key, title: rtable.title, results, displayOptions: rtable.display_opt }); } catch (e) { if (e instanceof TableError) { return this._getErrorResultSet(e.message, e.key); } else { // Rethrow unexpected errors throw e; } } } /** * Get result set from a table based on the key. * @param {RandomTable} rtable Main table object. * @param {String} [table] Subtable * @returns {RandomTableResultSet} */ getResultSetForTable (rtable, table = '') { if (!(rtable instanceof RandomTable)) { return this._getErrorResultSet(`Invalid table data.`); } const results = this.getTableResult(rtable, table); return new RandomTableResultSet({ key: rtable.key, title: rtable.title, results, displayOptions: rtable.display_opt }); } /** * Perform token replacement. Only table and roll actions are accepted * @param {String} token A value passed from findToken containing a token(s) {{SOME OPERATION}} Tokens are {{table:SOMETABLE}} {{table:SOMETABLE:SUBTABLE}} {{table:SOMETABLE*3}} (roll that table 3 times) {{roll:1d6+2}} (etc) (i.e. {{table:colonial_occupations:laborer}} {{table:color}} also generate names with {{name:flemish}} (surname only) {{name:flemish:male}} {{name:dutch:female}} * @param {RandomTable|null} curtable RandomTable the string is from (needed for "this" tokens) or null * @returns {RandomTableResultSet|RandomTableResultSet[]|DiceResult|String|Any} The result of the token or else just the token (in case it was a mistake or at least to make the error clearer) */ convertToken (token, curtable = null) { let parts = token.replace('{{', '').replace('}}', '').split(':'); parts = parts.map((el) => { return el.trim(); }); if (parts.length === 0) { return token; } // look for a token type we can run try { if (this.token_types[parts[0]]) { return this.token_types[parts[0]](parts, token, curtable); } else { return token; } } catch (e) { if (e instanceof RangeError) { // This could be an infinite loop of table results referencing each other. return this._getErrorResultSet(e.message); } else { throw e; } } } /** * Look for tokens to perform replace action on them. * @param {String} entryLabel Usually a label from a RandomTableEntry * @param {RandomTable|null} curtable RandomTable the string is from (needed for "this" tokens) or null * @returns {String} String with tokens replaced (if applicable) */ findToken (entryLabel, curtable = null) { if (isEmpty(entryLabel)) { return ''; } const newstring = entryLabel.replace(tokenRegExp, (token) => { return this.convertToken(token, curtable).toString(); }); return newstring; } /** * Since tables are stored outside of this module, this function allows for the setting of a function which will be used to lookup a table by it's key * @param {Function} lookup a function that takes a table key and returns a RandomTable or null */ setTableKeyLookup (lookup) { this._customGetTableByKey = lookup; } /** * Placeholder that should be replaced by a function outside this module * @param {String} key human readable table identifier * @return {null} nothing, when replaced this function should return a table object */ _customGetTableByKey (key) { return null; } /** * Return a table based on it's key. * This requires calling setTableKeyLookup and setting a lookup method * That returns a RandomTable object or null. * @param {String} key human readable table identifier * @returns {RandomTable} * @throws {TableError} */ getTableByKey (key) { if (!key) { throw new TableError('No table key.'); } const table = this._customGetTableByKey(key); if (!table || !(table instanceof RandomTable)) { throw new TableError(`No table found for key: ${key}`, key); } return table; } /** * Add a token variable * @param {String} name Name of the token (used as first element). * @param {Function} process Function to return token replacement value function is passed the token_parts (token split by ":"), original full_token, current table name */ registerTokenType (name, process) { this.token_types[name] = process; } /** * Dice roll token. * @returns {DiceResult} */ _defaultRollToken (token_parts) { return getDiceResult(token_parts[1]); } /** * Table token lookup in the form: * {{table:SOMETABLE}} {{table:SOMETABLE:SUBTABLE}} {{table:SOMETABLE*3}} (roll that table 3 times) {{table:SOMETABLE:SUBTABLE*2}} (roll subtable 2 times) * @param {String[]} token_parts Token split by : * @param {String} full_token Original token * @param {RandomTable|null} curtable Current table or null. * @returns {RandomTableResultSet|RandomTableResultSet[]} One or more result sets. */ _defaultTableToken (token_parts, full_token, curtable = null) { if (isUndefined(token_parts[1])) { return full_token; } let multiplier = 1; if (token_parts[1].indexOf('*') !== -1) { const x = token_parts[1].split('*'); token_parts[1] = x[0]; multiplier = x[1]; } // what table do we roll on let rtable = null; if (token_parts[1] === 'this') { if (!curtable) { return full_token; } // reroll on same table rtable = curtable; } else { // Table lookup try { rtable = this.getTableByKey(token_parts[1]); } catch (e) { if (e instanceof TableError) { return full_token; } else { // Rethrow unexpected errors throw e; } } } if (typeof token_parts[2] !== 'undefined' && token_parts[2].indexOf('*') !== -1) { const x = token_parts[2].split('*'); token_parts[2] = x[0]; multiplier = x[1]; } const subtable = (isUndefined(token_parts[2])) ? '' : token_parts[2]; const results = []; for (let i = 1; i <= multiplier; i++) { results.push(this.getResultSetForTable(rtable, subtable)); } return results.length === 1 ? results[0] : results; } /** * Simple pick one of the options token: * {{oneof:dwarf|halfling|human pig|dog person}} * @param {String[]} token_parts Token split by : * @param {String} full_token Original token * @returns {String} One of the options or empty. */ _defaultOneOfToken (token_parts, full_token) { if (isUndefined(token_parts[1])) { return full_token; } const options = token_parts[1].split('|'); if (options.length === 1) { return options.shift(); } return randomString(options) || ''; } } export default TableRoller;