reset: change of plans

main
Mari 2 years ago
parent 06fffb7b2b
commit 3faf045af1
  1. 5
      .gitignore
  2. 106
      config/env.js
  3. 66
      config/getHttpsConfig.js
  4. 29
      config/jest/babelTransform.js
  5. 14
      config/jest/cssTransform.js
  6. 40
      config/jest/fileTransform.js
  7. 53
      config/jest/peggyTransform.js
  8. 134
      config/modules.js
  9. 75
      config/paths.js
  10. 37
      config/peggy-loader.js
  11. 35
      config/pnpTs.js
  12. 802
      config/webpack.config.js
  13. 130
      config/webpackDevServer.config.js
  14. 89
      gameScript/intro.nom
  15. 197
      jest.config.ts
  16. 45297
      package-lock.json
  17. 175
      package.json
  18. BIN
      public/favicon.ico
  19. 43
      public/index.html
  20. BIN
      public/logo192.png
  21. BIN
      public/logo512.png
  22. 25
      public/manifest.json
  23. 3
      public/robots.txt
  24. 212
      scripts/build.js
  25. 166
      scripts/start.js
  26. 53
      scripts/test.js
  27. 38
      src/App.css
  28. 9
      src/App.test.tsx
  29. 26
      src/App.tsx
  30. 76
      src/battlers/Action.ts
  31. 321
      src/battlers/Energy.test.ts
  32. 161
      src/battlers/Energy.ts
  33. 558
      src/battlers/HitPoints.test.ts
  34. 314
      src/battlers/HitPoints.ts
  35. 13
      src/index.css
  36. 19
      src/index.tsx
  37. 1
      src/logo.svg
  38. 7
      src/main.test.ts
  39. 5
      src/main.ts
  40. 71
      src/react-app-env.d.ts
  41. 15
      src/reportWebVitals.ts
  42. 103
      src/scripting/BattlerStatement.test.ts
  43. 58
      src/scripting/BattlerStatement.ts
  44. 36
      src/scripting/ExampleScripts.test.ts
  45. 60
      src/scripting/NomScript.peggy
  46. 34
      src/scripting/NomScript.peggy.d.ts
  47. 69
      src/scripting/NomScript.test.ts
  48. 24
      src/scripting/ScriptExpression.test.ts
  49. 3
      src/scripting/ScriptExpression.ts
  50. 27
      src/scripting/ScriptFile.test.ts
  51. 10
      src/scripting/ScriptFile.ts
  52. 88
      src/scripting/ScriptValue.test.ts
  53. 41
      src/scripting/ScriptValue.ts
  54. 59
      src/scripting/TopLevelStatement.test.ts
  55. 23
      src/scripting/TopLevelStatement.ts
  56. 5
      src/setupTests.ts
  57. 174
      src/testing/Assertions.test.ts
  58. 58
      src/testing/Assertions.ts
  59. 12
      tsconfig.json

5
.gitignore vendored

@ -1,10 +1,5 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies # dependencies
/node_modules /node_modules
/.pnp
.pnp.js
/src/scripting/NomScript.ts
# testing # testing
/coverage /coverage

@ -1,106 +0,0 @@
'use strict';
const fs = require('fs');
const path = require('path');
const paths = require('./paths');
// Make sure that including paths.js after env.js will read .env variables.
delete require.cache[require.resolve('./paths')];
const NODE_ENV = process.env.NODE_ENV;
if (!NODE_ENV) {
throw new Error(
'The NODE_ENV environment variable is required but was not specified.'
);
}
// https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use
const dotenvFiles = [
`${paths.dotenv}.${NODE_ENV}.local`,
// Don't include `.env.local` for `test` environment
// since normally you expect tests to produce the same
// results for everyone
NODE_ENV !== 'test' && `${paths.dotenv}.local`,
`${paths.dotenv}.${NODE_ENV}`,
paths.dotenv,
].filter(Boolean);
// Load environment variables from .env* files. Suppress warnings using silent
// if this file is missing. dotenv will never modify any environment variables
// that have already been set. Variable expansion is supported in .env files.
// https://github.com/motdotla/dotenv
// https://github.com/motdotla/dotenv-expand
dotenvFiles.forEach(dotenvFile => {
if (fs.existsSync(dotenvFile)) {
require('dotenv-expand')(
require('dotenv').config({
path: dotenvFile,
})
);
}
});
// We support resolving modules according to `NODE_PATH`.
// This lets you use absolute paths in imports inside large monorepos:
// https://github.com/facebook/create-react-app/issues/253.
// It works similar to `NODE_PATH` in Node itself:
// https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders
// Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored.
// Otherwise, we risk importing Node.js core modules into an app instead of webpack shims.
// https://github.com/facebook/create-react-app/issues/1023#issuecomment-265344421
// We also resolve them to make sure all tools using them work consistently.
const appDirectory = fs.realpathSync(process.cwd());
process.env.NODE_PATH = (process.env.NODE_PATH || '')
.split(path.delimiter)
.filter(folder => folder && !path.isAbsolute(folder))
.map(folder => path.resolve(appDirectory, folder))
.join(path.delimiter);
// Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be
// injected into the application via DefinePlugin in webpack configuration.
const REACT_APP = /^REACT_APP_/i;
function getClientEnvironment(publicUrl) {
const raw = Object.keys(process.env)
.filter(key => REACT_APP.test(key))
.reduce(
(env, key) => {
env[key] = process.env[key];
return env;
},
{
// Useful for determining whether we’re running in production mode.
// Most importantly, it switches React into the correct mode.
NODE_ENV: process.env.NODE_ENV || 'development',
// Useful for resolving the correct path to static assets in `public`.
// For example, <img src={process.env.PUBLIC_URL + '/img/logo.png'} />.
// This should only be used as an escape hatch. Normally you would put
// images into the `src` and `import` them in code to get their paths.
PUBLIC_URL: publicUrl,
// We support configuring the sockjs pathname during development.
// These settings let a developer run multiple simultaneous projects.
// They are used as the connection `hostname`, `pathname` and `port`
// in webpackHotDevClient. They are used as the `sockHost`, `sockPath`
// and `sockPort` options in webpack-dev-server.
WDS_SOCKET_HOST: process.env.WDS_SOCKET_HOST,
WDS_SOCKET_PATH: process.env.WDS_SOCKET_PATH,
WDS_SOCKET_PORT: process.env.WDS_SOCKET_PORT,
// Whether or not react-refresh is enabled.
// react-refresh is not 100% stable at this time,
// which is why it's disabled by default.
// It is defined here so it is available in the webpackHotDevClient.
FAST_REFRESH: process.env.FAST_REFRESH !== 'false',
}
);
// Stringify all values so we can feed into webpack DefinePlugin
const stringified = {
'process.env': Object.keys(raw).reduce((env, key) => {
env[key] = JSON.stringify(raw[key]);
return env;
}, {}),
};
return { raw, stringified };
}
module.exports = getClientEnvironment;

@ -1,66 +0,0 @@
'use strict';
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const chalk = require('react-dev-utils/chalk');
const paths = require('./paths');
// Ensure the certificate and key provided are valid and if not
// throw an easy to debug error
function validateKeyAndCerts({ cert, key, keyFile, crtFile }) {
let encrypted;
try {
// publicEncrypt will throw an error with an invalid cert
encrypted = crypto.publicEncrypt(cert, Buffer.from('test'));
} catch (err) {
throw new Error(
`The certificate "${chalk.yellow(crtFile)}" is invalid.\n${err.message}`
);
}
try {
// privateDecrypt will throw an error with an invalid key
crypto.privateDecrypt(key, encrypted);
} catch (err) {
throw new Error(
`The certificate key "${chalk.yellow(keyFile)}" is invalid.\n${
err.message
}`
);
}
}
// Read file and throw an error if it doesn't exist
function readEnvFile(file, type) {
if (!fs.existsSync(file)) {
throw new Error(
`You specified ${chalk.cyan(
type
)} in your env, but the file "${chalk.yellow(file)}" can't be found.`
);
}
return fs.readFileSync(file);
}
// Get the https config
// Return cert files if provided in env, otherwise just true or false
function getHttpsConfig() {
const { SSL_CRT_FILE, SSL_KEY_FILE, HTTPS } = process.env;
const isHttps = HTTPS === 'true';
if (isHttps && SSL_CRT_FILE && SSL_KEY_FILE) {
const crtFile = path.resolve(paths.appPath, SSL_CRT_FILE);
const keyFile = path.resolve(paths.appPath, SSL_KEY_FILE);
const config = {
cert: readEnvFile(crtFile, 'SSL_CRT_FILE'),
key: readEnvFile(keyFile, 'SSL_KEY_FILE'),
};
validateKeyAndCerts({ ...config, keyFile, crtFile });
return config;
}
return isHttps;
}
module.exports = getHttpsConfig;

@ -1,29 +0,0 @@
'use strict';
const babelJest = require('babel-jest');
const hasJsxRuntime = (() => {
if (process.env.DISABLE_NEW_JSX_TRANSFORM === 'true') {
return false;
}
try {
require.resolve('react/jsx-runtime');
return true;
} catch (e) {
return false;
}
})();
module.exports = babelJest.createTransformer({
presets: [
[
require.resolve('babel-preset-react-app'),
{
runtime: hasJsxRuntime ? 'automatic' : 'classic',
},
],
],
babelrc: false,
configFile: false,
});

@ -1,14 +0,0 @@
'use strict';
// This is a custom Jest transformer turning style imports into empty objects.
// http://facebook.github.io/jest/docs/en/webpack.html
module.exports = {
process() {
return 'module.exports = {};';
},
getCacheKey() {
// The output is always the same.
return 'cssTransform';
},
};

@ -1,40 +0,0 @@
'use strict';
const path = require('path');
const camelcase = require('camelcase');
// This is a custom Jest transformer turning file imports into filenames.
// http://facebook.github.io/jest/docs/en/webpack.html
module.exports = {
process(src, filename) {
const assetFilename = JSON.stringify(path.basename(filename));
if (filename.match(/\.svg$/)) {
// Based on how SVGR generates a component name:
// https://github.com/smooth-code/svgr/blob/01b194cf967347d43d4cbe6b434404731b87cf27/packages/core/src/state.js#L6
const pascalCaseFilename = camelcase(path.parse(filename).name, {
pascalCase: true,
});
const componentName = `Svg${pascalCaseFilename}`;
return `const React = require('react');
module.exports = {
__esModule: true,
default: ${assetFilename},
ReactComponent: React.forwardRef(function ${componentName}(props, ref) {
return {
$$typeof: Symbol.for('react.element'),
type: 'svg',
ref: ref,
key: null,
props: Object.assign({}, props, {
children: ${assetFilename}
})
};
}),
};`;
}
return `module.exports = ${assetFilename};`;
},
};

@ -1,53 +0,0 @@
'use strict';
const babelJest = require('babel-jest');
const makeCacheKey = require('@jest/create-cache-key-function').default();
const peggy = require("peggy");
const hasJsxRuntime = (() => {
if (process.env.DISABLE_NEW_JSX_TRANSFORM === 'true') {
return false;
}
try {
require.resolve('react/jsx-runtime');
return true;
} catch (e) {
return false;
}
})();
const babelJestInstance = babelJest.createTransformer({
presets: [
[
require.resolve('babel-preset-react-app'),
{
runtime: hasJsxRuntime ? 'automatic' : 'classic',
},
],
],
babelrc: false,
configFile: false,
});
const startRuleFinder = /\s*\/\/\s*peggy-loader:startRule\s*(\S+)\s*\n/g
module.exports = {
canInstrument: babelJestInstance.canInstrument,
createTransformer() {
return this;
},
getCacheKey(sourceText, sourcePath, configString, options) {
return makeCacheKey(sourceText, sourcePath, configString, options) + babelJestInstance.getCacheKey(sourceText, sourcePath, configString, options)
},
process(sourceText, sourcePath, configString, options) {
const start = Array.from({ [Symbol.iterator]() { return sourceText.matchAll(startRuleFinder) } }, ([_, rule]) => rule)
const resultSource = peggy.generate(sourceText, {
grammarSource: this.resourcePath,
allowedStartRules: start,
output: "source",
format: "es",
})
return babelJest.process(resultSource, sourcePath, configString, options)
},
}

@ -1,134 +0,0 @@
'use strict';
const fs = require('fs');
const path = require('path');
const paths = require('./paths');
const chalk = require('react-dev-utils/chalk');
const resolve = require('resolve');
/**
* Get additional module paths based on the baseUrl of a compilerOptions object.
*
* @param {Object} options
*/
function getAdditionalModulePaths(options = {}) {
const baseUrl = options.baseUrl;
if (!baseUrl) {
return '';
}
const baseUrlResolved = path.resolve(paths.appPath, baseUrl);
// We don't need to do anything if `baseUrl` is set to `node_modules`. This is
// the default behavior.
if (path.relative(paths.appNodeModules, baseUrlResolved) === '') {
return null;
}
// Allow the user set the `baseUrl` to `appSrc`.
if (path.relative(paths.appSrc, baseUrlResolved) === '') {
return [paths.appSrc];
}
// If the path is equal to the root directory we ignore it here.
// We don't want to allow importing from the root directly as source files are
// not transpiled outside of `src`. We do allow importing them with the
// absolute path (e.g. `src/Components/Button.js`) but we set that up with
// an alias.
if (path.relative(paths.appPath, baseUrlResolved) === '') {
return null;
}
// Otherwise, throw an error.
throw new Error(
chalk.red.bold(
"Your project's `baseUrl` can only be set to `src` or `node_modules`." +
' Create React App does not support other values at this time.'
)
);
}
/**
* Get webpack aliases based on the baseUrl of a compilerOptions object.
*
* @param {*} options
*/
function getWebpackAliases(options = {}) {
const baseUrl = options.baseUrl;
if (!baseUrl) {
return {};
}
const baseUrlResolved = path.resolve(paths.appPath, baseUrl);
if (path.relative(paths.appPath, baseUrlResolved) === '') {
return {
src: paths.appSrc,
};
}
}
/**
* Get jest aliases based on the baseUrl of a compilerOptions object.
*
* @param {*} options
*/
function getJestAliases(options = {}) {
const baseUrl = options.baseUrl;
if (!baseUrl) {
return {};
}
const baseUrlResolved = path.resolve(paths.appPath, baseUrl);
if (path.relative(paths.appPath, baseUrlResolved) === '') {
return {
'^src/(.*)$': '<rootDir>/src/$1',
};
}
}
function getModules() {
// Check if TypeScript is setup
const hasTsConfig = fs.existsSync(paths.appTsConfig);
const hasJsConfig = fs.existsSync(paths.appJsConfig);
if (hasTsConfig && hasJsConfig) {
throw new Error(
'You have both a tsconfig.json and a jsconfig.json. If you are using TypeScript please remove your jsconfig.json file.'
);
}
let config;
// If there's a tsconfig.json we assume it's a
// TypeScript project and set up the config
// based on tsconfig.json
if (hasTsConfig) {
const ts = require(resolve.sync('typescript', {
basedir: paths.appNodeModules,
}));
config = ts.readConfigFile(paths.appTsConfig, ts.sys.readFile).config;
// Otherwise we'll check if there is jsconfig.json
// for non TS projects.
} else if (hasJsConfig) {
config = require(paths.appJsConfig);
}
config = config || {};
const options = config.compilerOptions || {};
const additionalModulePaths = getAdditionalModulePaths(options);
return {
additionalModulePaths: additionalModulePaths,
webpackAliases: getWebpackAliases(options),
jestAliases: getJestAliases(options),
hasTsConfig,
};
}
module.exports = getModules();

@ -1,75 +0,0 @@
'use strict';
const path = require('path');
const fs = require('fs');
const getPublicUrlOrPath = require('react-dev-utils/getPublicUrlOrPath');
// Make sure any symlinks in the project folder are resolved:
// https://github.com/facebook/create-react-app/issues/637
const appDirectory = fs.realpathSync(process.cwd());
const resolveApp = relativePath => path.resolve(appDirectory, relativePath);
// We use `PUBLIC_URL` environment variable or "homepage" field to infer
// "public path" at which the app is served.
// webpack needs to know it to put the right <script> hrefs into HTML even in
// single-page apps that may serve index.html for nested URLs like /todos/42.
// We can't use a relative path in HTML because we don't want to load something
// like /todos/42/static/js/bundle.7289d.js. We have to know the root.
const publicUrlOrPath = getPublicUrlOrPath(
process.env.NODE_ENV === 'development',
require(resolveApp('package.json')).homepage,
process.env.PUBLIC_URL
);
const buildPath = process.env.BUILD_PATH || 'build';
const moduleFileExtensions = [
'web.mjs',
'mjs',
'web.js',
'js',
'web.ts',
'ts',
'web.tsx',
'tsx',
'json',
'web.jsx',
'jsx',
];
// Resolve file paths in the same order as webpack
const resolveModule = (resolveFn, filePath) => {
const extension = moduleFileExtensions.find(extension =>
fs.existsSync(resolveFn(`${filePath}.${extension}`))
);
if (extension) {
return resolveFn(`${filePath}.${extension}`);
}
return resolveFn(`${filePath}.js`);
};
// config after eject: we're in ./config/
module.exports = {
dotenv: resolveApp('.env'),
appPath: resolveApp('.'),
appBuild: resolveApp(buildPath),
appPublic: resolveApp('public'),
appHtml: resolveApp('public/index.html'),
appIndexJs: resolveModule(resolveApp, 'src/index'),
appPackageJson: resolveApp('package.json'),
appSrc: resolveApp('src'),
appTsConfig: resolveApp('tsconfig.json'),
appJsConfig: resolveApp('jsconfig.json'),
yarnLockFile: resolveApp('yarn.lock'),
testsSetup: resolveModule(resolveApp, 'src/setupTests'),
proxySetup: resolveApp('src/setupProxy.js'),
appNodeModules: resolveApp('node_modules'),
swSrc: resolveModule(resolveApp, 'src/service-worker'),
publicUrlOrPath,
};
module.exports.moduleFileExtensions = moduleFileExtensions;

@ -1,37 +0,0 @@
const peggy = require("peggy")
const loaderUtils = require("loader-utils")
const startRuleFinder = /\s*\/\/\s*peggy-loader:startRule\s*(\S+)\s*\n/g
async function loadPeggy(rawSource) {
const callback = this.async();
const tracks = await import("peggy-tracks")
const rawOpts = loaderUtils.getOptions(this)
const opts = typeof rawOpts === "object" && rawOpts !== null ? rawOpts : {}
const peggyOpts = opts.hasOwnProperty("peggy") && typeof opts.peggy === "object" && opts.peggy !== null ? opts.peggy : {}
const plugins = peggyOpts.hasOwnProperty("plugins") && Array.isArray(peggyOpts.plugins) ? peggyOpts.plugins : []
const tracksOpts = opts.hasOwnProperty("tracks") && typeof opts.tracks === "object" && opts.tracks !== null ? opts.tracks : {start: []}
const start = Array.from({ [Symbol.iterator]() { return rawSource.matchAll(startRuleFinder) } }, ([_, rule]) => rule)
const trackStarts = !opts.hasOwnProperty("tracks") ? [] : typeof tracksOpts.start === "string" ? [tracksOpts.start] : Array.isArray(tracksOpts.start) ? tracksOpts.start : start.length > 0 ? start : [undefined]
const source = rawSource.replaceAll(startRuleFinder, "\n")
const resultSource = peggy.generate(source, {
...peggyOpts,
grammarSource: this.resourcePath,
allowedStartRules: peggyOpts.allowedStartRules || start,
output: "source",
format: "es",
plugins,
})
for (const start of trackStarts) {
const diagram = tracks.tracks({
...tracksOpts,
text: source,
start,
})
this.emitFile(`/tracks/${this.resourcePath}${typeof start === "string" ? "." + start : ""}.svg`, diagram.toStandalone(tracks.defaultCSS))
}
// const fixedSource = resultSource.replace("export class SyntaxError", "class SyntaxError")
callback(undefined, resultSource)
}
module.exports = loadPeggy

@ -1,35 +0,0 @@
'use strict';
const { resolveModuleName } = require('ts-pnp');
exports.resolveModuleName = (
typescript,
moduleName,
containingFile,
compilerOptions,
resolutionHost
) => {
return resolveModuleName(
moduleName,
containingFile,
compilerOptions,
resolutionHost,
typescript.resolveModuleName
);
};
exports.resolveTypeReferenceDirective = (
typescript,
moduleName,
containingFile,
compilerOptions,
resolutionHost
) => {
return resolveModuleName(
moduleName,
containingFile,
compilerOptions,
resolutionHost,
typescript.resolveTypeReferenceDirective
);
};

@ -1,802 +0,0 @@
'use strict';
const fs = require('fs');
const path = require('path');
const webpack = require('webpack');
const resolve = require('resolve');
const PnpWebpackPlugin = require('pnp-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin');
const InlineChunkHtmlPlugin = require('react-dev-utils/InlineChunkHtmlPlugin');
const TerserPlugin = require('terser-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const safePostCssParser = require('postcss-safe-parser');
const ManifestPlugin = require('webpack-manifest-plugin');
const InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin');
const WorkboxWebpackPlugin = require('workbox-webpack-plugin');
const WatchMissingNodeModulesPlugin = require('react-dev-utils/WatchMissingNodeModulesPlugin');
const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin');
const getCSSModuleLocalIdent = require('react-dev-utils/getCSSModuleLocalIdent');
const ESLintPlugin = require('eslint-webpack-plugin');
const paths = require('./paths');
const modules = require('./modules');
const getClientEnvironment = require('./env');
const ModuleNotFoundPlugin = require('react-dev-utils/ModuleNotFoundPlugin');
const ForkTsCheckerWebpackPlugin = require('react-dev-utils/ForkTsCheckerWebpackPlugin');
const typescriptFormatter = require('react-dev-utils/typescriptFormatter');
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
const postcssNormalize = require('postcss-normalize');
const appPackageJson = require(paths.appPackageJson);
// Source maps are resource heavy and can cause out of memory issue for large source files.
const shouldUseSourceMap = process.env.GENERATE_SOURCEMAP !== 'false';
const webpackDevClientEntry = require.resolve(
'react-dev-utils/webpackHotDevClient'
);
const reactRefreshOverlayEntry = require.resolve(
'react-dev-utils/refreshOverlayInterop'
);
// Some apps do not need the benefits of saving a web request, so not inlining the chunk
// makes for a smoother build process.
const shouldInlineRuntimeChunk = process.env.INLINE_RUNTIME_CHUNK !== 'false';
const emitErrorsAsWarnings = process.env.ESLINT_NO_DEV_ERRORS === 'true';
const disableESLintPlugin = process.env.DISABLE_ESLINT_PLUGIN === 'true';
const imageInlineSizeLimit = parseInt(
process.env.IMAGE_INLINE_SIZE_LIMIT || '10000'
);
// Check if TypeScript is setup
const useTypeScript = fs.existsSync(paths.appTsConfig);
// Get the path to the uncompiled service worker (if it exists).
const swSrc = paths.swSrc;
// style files regexes
const cssRegex = /\.css$/;
const cssModuleRegex = /\.module\.css$/;
const sassRegex = /\.(scss|sass)$/;
const sassModuleRegex = /\.module\.(scss|sass)$/;
const hasJsxRuntime = (() => {
if (process.env.DISABLE_NEW_JSX_TRANSFORM === 'true') {
return false;
}
try {
require.resolve('react/jsx-runtime');
return true;
} catch (e) {
return false;
}
})();
// This is the production and development configuration.
// It is focused on developer experience, fast rebuilds, and a minimal bundle.
module.exports = function (webpackEnv) {
const isEnvDevelopment = webpackEnv === 'development';
const isEnvProduction = webpackEnv === 'production';
// Variable used for enabling profiling in Production
// passed into alias object. Uses a flag if passed into the build command
const isEnvProductionProfile =
isEnvProduction && process.argv.includes('--profile');
// We will provide `paths.publicUrlOrPath` to our app
// as %PUBLIC_URL% in `index.html` and `process.env.PUBLIC_URL` in JavaScript.
// Omit trailing slash as %PUBLIC_URL%/xyz looks better than %PUBLIC_URL%xyz.
// Get environment variables to inject into our app.
const env = getClientEnvironment(paths.publicUrlOrPath.slice(0, -1));
const shouldUseReactRefresh = env.raw.FAST_REFRESH;
// common function to get style loaders
const getStyleLoaders = (cssOptions, preProcessor) => {
const loaders = [
isEnvDevelopment && require.resolve('style-loader'),
isEnvProduction && {
loader: MiniCssExtractPlugin.loader,
// css is located in `static/css`, use '../../' to locate index.html folder
// in production `paths.publicUrlOrPath` can be a relative path
options: paths.publicUrlOrPath.startsWith('.')
? { publicPath: '../../' }
: {},
},
{
loader: require.resolve('css-loader'),
options: cssOptions,
},
{
// Options for PostCSS as we reference these options twice
// Adds vendor prefixing based on your specified browser support in
// package.json
loader: require.resolve('postcss-loader'),
options: {
// Necessary for external CSS imports to work
// https://github.com/facebook/create-react-app/issues/2677
ident: 'postcss',
plugins: () => [
require('postcss-flexbugs-fixes'),
require('postcss-preset-env')({
autoprefixer: {
flexbox: 'no-2009',
},
stage: 3,
}),
// Adds PostCSS Normalize as the reset css with default options,
// so that it honors browserslist config in package.json
// which in turn let's users customize the target behavior as per their needs.
postcssNormalize(),
],
sourceMap: isEnvProduction ? shouldUseSourceMap : isEnvDevelopment,
},
},
].filter(Boolean);
if (preProcessor) {
loaders.push(
{
loader: require.resolve('resolve-url-loader'),
options: {
sourceMap: isEnvProduction ? shouldUseSourceMap : isEnvDevelopment,
root: paths.appSrc,
},
},
{
loader: require.resolve(preProcessor),
options: {
sourceMap: true,
},
}
);
}
return loaders;
};
return {
mode: isEnvProduction ? 'production' : isEnvDevelopment && 'development',
// Stop compilation early in production
bail: isEnvProduction,
devtool: isEnvProduction
? shouldUseSourceMap
? 'source-map'
: false
: isEnvDevelopment && 'cheap-module-source-map',
// These are the "entry points" to our application.
// This means they will be the "root" imports that are included in JS bundle.
entry:
isEnvDevelopment && !shouldUseReactRefresh
? [
// Include an alternative client for WebpackDevServer. A client's job is to
// connect to WebpackDevServer by a socket and get notified about changes.
// When you save a file, the client will either apply hot updates (in case
// of CSS changes), or refresh the page (in case of JS changes). When you
// make a syntax error, this client will display a syntax error overlay.
// Note: instead of the default WebpackDevServer client, we use a custom one
// to bring better experience for Create React App users. You can replace
// the line below with these two lines if you prefer the stock client:
//
// require.resolve('webpack-dev-server/client') + '?/',
// require.resolve('webpack/hot/dev-server'),
//
// When using the experimental react-refresh integration,
// the webpack plugin takes care of injecting the dev client for us.
webpackDevClientEntry,
// Finally, this is your app's code:
paths.appIndexJs,
// We include the app code last so that if there is a runtime error during
// initialization, it doesn't blow up the WebpackDevServer client, and
// changing JS code would still trigger a refresh.
]
: paths.appIndexJs,
output: {
// The build folder.
path: isEnvProduction ? paths.appBuild : undefined,
// Add /* filename */ comments to generated require()s in the output.
pathinfo: isEnvDevelopment,
// There will be one main bundle, and one file per asynchronous chunk.
// In development, it does not produce real files.
filename: isEnvProduction
? 'static/js/[name].[contenthash:8].js'
: isEnvDevelopment && 'static/js/bundle.js',
// TODO: remove this when upgrading to webpack 5
futureEmitAssets: true,
// There are also additional JS chunk files if you use code splitting.
chunkFilename: isEnvProduction
? 'static/js/[name].[contenthash:8].chunk.js'
: isEnvDevelopment && 'static/js/[name].chunk.js',
// webpack uses `publicPath` to determine where the app is being served from.
// It requires a trailing slash, or the file assets will get an incorrect path.
// We inferred the "public path" (such as / or /my-project) from homepage.
publicPath: paths.publicUrlOrPath,
// Point sourcemap entries to original disk location (format as URL on Windows)
devtoolModuleFilenameTemplate: isEnvProduction
? info =>
path
.relative(paths.appSrc, info.absoluteResourcePath)
.replace(/\\/g, '/')
: isEnvDevelopment &&
(info => path.resolve(info.absoluteResourcePath).replace(/\\/g, '/')),
// Prevents conflicts when multiple webpack runtimes (from different apps)
// are used on the same page.
jsonpFunction: `webpackJsonp${appPackageJson.name}`,
// this defaults to 'window', but by setting it to 'this' then
// module chunks which are built will work in web workers as well.
globalObject: 'this',
},
optimization: {
minimize: isEnvProduction,
minimizer: [
// This is only used in production mode
new TerserPlugin({
terserOptions: {
parse: {
// We want terser to parse ecma 8 code. However, we don't want it
// to apply any minification steps that turns valid ecma 5 code
// into invalid ecma 5 code. This is why the 'compress' and 'output'
// sections only apply transformations that are ecma 5 safe
// https://github.com/facebook/create-react-app/pull/4234
ecma: 8,
},
compress: {
ecma: 5,
warnings: false,
// Disabled because of an issue with Uglify breaking seemingly valid code:
// https://github.com/facebook/create-react-app/issues/2376
// Pending further investigation:
// https://github.com/mishoo/UglifyJS2/issues/2011
comparisons: false,
// Disabled because of an issue with Terser breaking valid code:
// https://github.com/facebook/create-react-app/issues/5250
// Pending further investigation:
// https://github.com/terser-js/terser/issues/120
inline: 2,
},
mangle: {
safari10: true,
},
// Added for profiling in devtools
keep_classnames: isEnvProductionProfile,
keep_fnames: isEnvProductionProfile,
output: {
ecma: 5,
comments: false,
// Turned on because emoji and regex is not minified properly using default
// https://github.com/facebook/create-react-app/issues/2488
ascii_only: true,
},
},
sourceMap: shouldUseSourceMap,
}),
// This is only used in production mode
new OptimizeCSSAssetsPlugin({
cssProcessorOptions: {
parser: safePostCssParser,
map: shouldUseSourceMap
? {
// `inline: false` forces the sourcemap to be output into a
// separate file
inline: false,
// `annotation: true` appends the sourceMappingURL to the end of
// the css file, helping the browser find the sourcemap
annotation: true,
}
: false,
},
cssProcessorPluginOptions: {
preset: ['default', { minifyFontValues: { removeQuotes: false } }],
},
}),
],
// Automatically split vendor and commons
// https://twitter.com/wSokra/status/969633336732905474
// https://medium.com/webpack/webpack-4-code-splitting-chunk-graph-and-the-splitchunks-optimization-be739a861366
splitChunks: {
chunks: 'all',
name: isEnvDevelopment,
},
// Keep the runtime chunk separated to enable long term caching
// https://twitter.com/wSokra/status/969679223278505985
// https://github.com/facebook/create-react-app/issues/5358
runtimeChunk: {
name: entrypoint => `runtime-${entrypoint.name}`,
},
},
resolve: {
// This allows you to set a fallback for where webpack should look for modules.
// We placed these paths second because we want `node_modules` to "win"
// if there are any conflicts. This matches Node resolution mechanism.
// https://github.com/facebook/create-react-app/issues/253
modules: ['node_modules', paths.appNodeModules].concat(
modules.additionalModulePaths || []
),
// These are the reasonable defaults supported by the Node ecosystem.
// We also include JSX as a common component filename extension to support
// some tools, although we do not recommend using it, see:
// https://github.com/facebook/create-react-app/issues/290
// `web` extension prefixes have been added for better support
// for React Native Web.
extensions: paths.moduleFileExtensions
.map(ext => `.${ext}`)
.filter(ext => useTypeScript || !ext.includes('ts')),
alias: {
// Support React Native Web
// https://www.smashingmagazine.com/2016/08/a-glimpse-into-the-future-with-react-native-for-web/
'react-native': 'react-native-web',
// Allows for better profiling with ReactDevTools
...(isEnvProductionProfile && {
'react-dom$': 'react-dom/profiling',
'scheduler/tracing': 'scheduler/tracing-profiling',
}),
...(modules.webpackAliases || {}),
},
plugins: [
// Adds support for installing with Plug'n'Play, leading to faster installs and adding
// guards against forgotten dependencies and such.
PnpWebpackPlugin,
// Prevents users from importing files from outside of src/ (or node_modules/).
// This often causes confusion because we only process files within src/ with babel.
// To fix this, we prevent you from importing files out of src/ -- if you'd like to,
// please link the files into your node_modules/ and let module-resolution kick in.
// Make sure your source files are compiled, as they will not be processed in any way.
new ModuleScopePlugin(paths.appSrc, [
paths.appPackageJson,
reactRefreshOverlayEntry,
]),
],
},
resolveLoader: {
plugins: [
// Also related to Plug'n'Play, but this time it tells webpack to load its loaders
// from the current package.
PnpWebpackPlugin.moduleLoader(module),
],
},
module: {
strictExportPresence: true,
rules: [
// Disable require.ensure as it's not a standard language feature.
{ parser: { requireEnsure: false } },
{
// "oneOf" will traverse all following loaders until one will
// match the requirements. When no loader matches it will fall
// back to the "file" loader at the end of the loader list.
oneOf: [
// TODO: Merge this config once `image/avif` is in the mime-db
// https://github.com/jshttp/mime-db
{
test: [/\.avif$/],
loader: require.resolve('url-loader'),
options: {
limit: imageInlineSizeLimit,
mimetype: 'image/avif',
name: 'static/media/[name].[hash:8].[ext]',
},
},
// "url" loader works like "file" loader except that it embeds assets
// smaller than specified limit in bytes as data URLs to avoid requests.
// A missing `test` is equivalent to a match.
{
test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
loader: require.resolve('url-loader'),
options: {
limit: imageInlineSizeLimit,
name: 'static/media/[name].[hash:8].[ext]',
},
},
// Process parser definitions with Peggy, using the TS plugin.
{
test: /\.peggy$/,
include: paths.appSrc,
use: [
{
loader: require.resolve('babel-loader'),
options: {
customize: require.resolve(
'babel-preset-react-app/webpack-overrides'
),
presets: [
[ require.resolve('babel-preset-react-app') ],
],
plugins: [
[
require.resolve('babel-plugin-named-asset-import'),
{
loaderMap: {
svg: {
ReactComponent:
'@svgr/webpack?-svgo,+titleProp,+ref![path]',
},
},
},
],
isEnvDevelopment &&
shouldUseReactRefresh &&
require.resolve('react-refresh/babel'),
].filter(Boolean),
// This is a feature of `babel-loader` for webpack (not Babel itself).
// It enables caching results in ./node_modules/.cache/babel-loader/
// directory for faster rebuilds.
cacheDirectory: true,
// See #6846 for context on why cacheCompression is disabled
cacheCompression: false,
compact: isEnvProduction,
},
},
{
loader: require.resolve('./peggy-loader'),
},
],
},
// Process application JS with Babel.
// The preset includes JSX, Flow, TypeScript, and some ESnext features.
{
test: /\.(js|mjs|jsx|ts|tsx)$/,
include: paths.appSrc,
loader: require.resolve('babel-loader'),
options: {
customize: require.resolve(
'babel-preset-react-app/webpack-overrides'
),
presets: [
[
require.resolve('babel-preset-react-app'),
{
runtime: hasJsxRuntime ? 'automatic' : 'classic',
},
],
],
plugins: [
[
require.resolve('babel-plugin-named-asset-import'),
{
loaderMap: {
svg: {
ReactComponent:
'@svgr/webpack?-svgo,+titleProp,+ref![path]',
},
},
},
],
isEnvDevelopment &&
shouldUseReactRefresh &&
require.resolve('react-refresh/babel'),
].filter(Boolean),
// This is a feature of `babel-loader` for webpack (not Babel itself).
// It enables caching results in ./node_modules/.cache/babel-loader/
// directory for faster rebuilds.
cacheDirectory: true,
// See #6846 for context on why cacheCompression is disabled
cacheCompression: false,
compact: isEnvProduction,
},
},
// Process any JS outside of the app with Babel.
// Unlike the application JS, we only compile the standard ES features.
{
test: /\.(js|mjs)$/,
exclude: /@babel(?:\/|\\{1,2})runtime/,
loader: require.resolve('babel-loader'),
options: {
babelrc: false,
configFile: false,
compact: false,
presets: [
[
require.resolve('babel-preset-react-app/dependencies'),
{ helpers: true },
],
],
cacheDirectory: true,
// See #6846 for context on why cacheCompression is disabled
cacheCompression: false,
// Babel sourcemaps are needed for debugging into node_modules
// code. Without the options below, debuggers like VSCode
// show incorrect code and set breakpoints on the wrong lines.
sourceMaps: shouldUseSourceMap,
inputSourceMap: shouldUseSourceMap,
},
},
// "postcss" loader applies autoprefixer to our CSS.
// "css" loader resolves paths in CSS and adds assets as dependencies.
// "style" loader turns CSS into JS modules that inject <style> tags.
// In production, we use MiniCSSExtractPlugin to extract that CSS
// to a file, but in development "style" loader enables hot editing
// of CSS.
// By default we support CSS Modules with the extension .module.css
{
test: cssRegex,
exclude: cssModuleRegex,
use: getStyleLoaders({
importLoaders: 1,
sourceMap: isEnvProduction
? shouldUseSourceMap
: isEnvDevelopment,
}),
// Don't consider CSS imports dead code even if the
// containing package claims to have no side effects.
// Remove this when webpack adds a warning or an error for this.
// See https://github.com/webpack/webpack/issues/6571
sideEffects: true,
},
// Adds support for CSS Modules (https://github.com/css-modules/css-modules)
// using the extension .module.css
{
test: cssModuleRegex,
use: getStyleLoaders({
importLoaders: 1,
sourceMap: isEnvProduction
? shouldUseSourceMap
: isEnvDevelopment,
modules: {
getLocalIdent: getCSSModuleLocalIdent,
},
}),
},
// Opt-in support for SASS (using .scss or .sass extensions).
// By default we support SASS Modules with the
// extensions .module.scss or .module.sass
{
test: sassRegex,
exclude: sassModuleRegex,
use: getStyleLoaders(
{
importLoaders: 3,
sourceMap: isEnvProduction
? shouldUseSourceMap
: isEnvDevelopment,
},
'sass-loader'
),
// Don't consider CSS imports dead code even if the
// containing package claims to have no side effects.
// Remove this when webpack adds a warning or an error for this.
// See https://github.com/webpack/webpack/issues/6571
sideEffects: true,
},
// Adds support for CSS Modules, but using SASS
// using the extension .module.scss or .module.sass
{
test: sassModuleRegex,
use: getStyleLoaders(
{
importLoaders: 3,
sourceMap: isEnvProduction
? shouldUseSourceMap
: isEnvDevelopment,
modules: {
getLocalIdent: getCSSModuleLocalIdent,
},
},
'sass-loader'
),
},
// "file" loader makes sure those assets get served by WebpackDevServer.
// When you `import` an asset, you get its (virtual) filename.
// In production, they would get copied to the `build` folder.
// This loader doesn't use a "test" so it will catch all modules
// that fall through the other loaders.
{
loader: require.resolve('file-loader'),
// Exclude `js` files to keep "css" loader working as it injects
// its runtime that would otherwise be processed through "file" loader.
// Also exclude `html` and `json` extensions so they get processed
// by webpacks internal loaders.
exclude: [/\.(js|mjs|jsx|ts|tsx)$/, /\.html$/, /\.json$/],
options: {
name: 'static/media/[name].[hash:8].[ext]',
},
},
// ** STOP ** Are you adding a new loader?
// Make sure to add the new loader(s) before the "file" loader.
],
},
],
},
plugins: [
// Generates an `index.html` file with the <script> injected.
new HtmlWebpackPlugin(
Object.assign(
{},
{
inject: true,
template: paths.appHtml,
},
isEnvProduction
? {
minify: {
removeComments: true,
collapseWhitespace: true,
removeRedundantAttributes: true,
useShortDoctype: true,
removeEmptyAttributes: true,
removeStyleLinkTypeAttributes: true,
keepClosingSlash: true,
minifyJS: true,
minifyCSS: true,
minifyURLs: true,
},
}
: undefined
)
),
// Inlines the webpack runtime script. This script is too small to warrant
// a network request.
// https://github.com/facebook/create-react-app/issues/5358
isEnvProduction &&
shouldInlineRuntimeChunk &&
new InlineChunkHtmlPlugin(HtmlWebpackPlugin, [/runtime-.+[.]js/]),
// Makes some environment variables available in index.html.
// The public URL is available as %PUBLIC_URL% in index.html, e.g.:
// <link rel="icon" href="%PUBLIC_URL%/favicon.ico">
// It will be an empty string unless you specify "homepage"
// in `package.json`, in which case it will be the pathname of that URL.
new InterpolateHtmlPlugin(HtmlWebpackPlugin, env.raw),
// This gives some necessary context to module not found errors, such as
// the requesting resource.
new ModuleNotFoundPlugin(paths.appPath),
// Makes some environment variables available to the JS code, for example:
// if (process.env.NODE_ENV === 'production') { ... }. See `./env.js`.
// It is absolutely essential that NODE_ENV is set to production
// during a production build.
// Otherwise React will be compiled in the very slow development mode.
new webpack.DefinePlugin(env.stringified),
// This is necessary to emit hot updates (CSS and Fast Refresh):
isEnvDevelopment && new webpack.HotModuleReplacementPlugin(),
// Experimental hot reloading for React .
// https://github.com/facebook/react/tree/master/packages/react-refresh
isEnvDevelopment &&
shouldUseReactRefresh &&
new ReactRefreshWebpackPlugin({
overlay: {
entry: webpackDevClientEntry,
// The expected exports are slightly different from what the overlay exports,
// so an interop is included here to enable feedback on module-level errors.
module: reactRefreshOverlayEntry,
// Since we ship a custom dev client and overlay integration,
// the bundled socket handling logic can be eliminated.
sockIntegration: false,
},
}),
// Watcher doesn't work well if you mistype casing in a path so we use
// a plugin that prints an error when you attempt to do this.
// See https://github.com/facebook/create-react-app/issues/240
isEnvDevelopment && new CaseSensitivePathsPlugin(),
// If you require a missing module and then `npm install` it, you still have
// to restart the development server for webpack to discover it. This plugin
// makes the discovery automatic so you don't have to restart.
// See https://github.com/facebook/create-react-app/issues/186
isEnvDevelopment &&
new WatchMissingNodeModulesPlugin(paths.appNodeModules),
isEnvProduction &&
new MiniCssExtractPlugin({
// Options similar to the same options in webpackOptions.output
// both options are optional
filename: 'static/css/[name].[contenthash:8].css',
chunkFilename: 'static/css/[name].[contenthash:8].chunk.css',
}),
// Generate an asset manifest file with the following content:
// - "files" key: Mapping of all asset filenames to their corresponding
// output file so that tools can pick it up without having to parse
// `index.html`
// - "entrypoints" key: Array of files which are included in `index.html`,
// can be used to reconstruct the HTML if necessary
new ManifestPlugin({
fileName: 'asset-manifest.json',
publicPath: paths.publicUrlOrPath,
generate: (seed, files, entrypoints) => {
const manifestFiles = files.reduce((manifest, file) => {
manifest[file.name] = file.path;
return manifest;
}, seed);
const entrypointFiles = entrypoints.main.filter(
fileName => !fileName.endsWith('.map')
);
return {
files: manifestFiles,
entrypoints: entrypointFiles,
};
},
}),
// Moment.js is an extremely popular library that bundles large locale files
// by default due to how webpack interprets its code. This is a practical
// solution that requires the user to opt into importing specific locales.
// https://github.com/jmblog/how-to-optimize-momentjs-with-webpack
// You can remove this if you don't use Moment.js:
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
// Generate a service worker script that will precache, and keep up to date,
// the HTML & assets that are part of the webpack build.
isEnvProduction &&
fs.existsSync(swSrc) &&
new WorkboxWebpackPlugin.InjectManifest({
swSrc,
dontCacheBustURLsMatching: /\.[0-9a-f]{8}\./,
exclude: [/\.map$/, /asset-manifest\.json$/, /LICENSE/],
// Bump up the default maximum size (2mb) that's precached,
// to make lazy-loading failure scenarios less likely.
// See https://github.com/cra-template/pwa/issues/13#issuecomment-722667270
maximumFileSizeToCacheInBytes: 5 * 1024 * 1024,
}),
// TypeScript type checking
useTypeScript &&
new ForkTsCheckerWebpackPlugin({
typescript: resolve.sync('typescript', {
basedir: paths.appNodeModules,
}),
async: isEnvDevelopment,
checkSyntacticErrors: true,
resolveModuleNameModule: process.versions.pnp
? `${__dirname}/pnpTs.js`
: undefined,
resolveTypeReferenceDirectiveModule: process.versions.pnp
? `${__dirname}/pnpTs.js`
: undefined,
tsconfig: paths.appTsConfig,
reportFiles: [
// This one is specifically to match during CI tests,
// as micromatch doesn't match
// '../cra-template-typescript/template/src/App.tsx'
// otherwise.
'../**/src/**/*.{ts,tsx}',
'**/src/**/*.{ts,tsx}',
'!**/src/**/__tests__/**',
'!**/src/**/?(*.)(spec|test).*',
'!**/src/setupProxy.*',
'!**/src/setupTests.*',
],
silent: true,
// The formatter is invoked directly in WebpackDevServerUtils during development
formatter: isEnvProduction ? typescriptFormatter : undefined,
}),
!disableESLintPlugin &&
new ESLintPlugin({
// Plugin options
extensions: ['js', 'mjs', 'jsx', 'ts', 'tsx'],
formatter: require.resolve('react-dev-utils/eslintFormatter'),
eslintPath: require.resolve('eslint'),
failOnError: !(isEnvDevelopment && emitErrorsAsWarnings),
context: paths.appSrc,
cache: true,
cacheLocation: path.resolve(
paths.appNodeModules,
'.cache/.eslintcache'
),
// ESLint class options
cwd: paths.appPath,
resolvePluginsRelativeTo: __dirname,
baseConfig: {
extends: [require.resolve('eslint-config-react-app/base')],
rules: {
...(!hasJsxRuntime && {
'react/react-in-jsx-scope': 'error',
}),
},
},
}),
].filter(Boolean),
// Some libraries import Node modules but don't use them in the browser.
// Tell webpack to provide empty mocks for them so importing them works.
node: {
module: 'empty',
dgram: 'empty',
dns: 'mock',
fs: 'empty',
http2: 'empty',
net: 'empty',
tls: 'empty',
child_process: 'empty',
},
// Turn off performance processing because we utilize
// our own hints via the FileSizeReporter
performance: false,
};
};

@ -1,130 +0,0 @@
'use strict';
const fs = require('fs');
const errorOverlayMiddleware = require('react-dev-utils/errorOverlayMiddleware');
const evalSourceMapMiddleware = require('react-dev-utils/evalSourceMapMiddleware');
const noopServiceWorkerMiddleware = require('react-dev-utils/noopServiceWorkerMiddleware');
const ignoredFiles = require('react-dev-utils/ignoredFiles');
const redirectServedPath = require('react-dev-utils/redirectServedPathMiddleware');
const paths = require('./paths');
const getHttpsConfig = require('./getHttpsConfig');
const host = process.env.HOST || '0.0.0.0';
const sockHost = process.env.WDS_SOCKET_HOST;
const sockPath = process.env.WDS_SOCKET_PATH; // default: '/sockjs-node'
const sockPort = process.env.WDS_SOCKET_PORT;
module.exports = function (proxy, allowedHost) {
return {
// WebpackDevServer 2.4.3 introduced a security fix that prevents remote
// websites from potentially accessing local content through DNS rebinding:
// https://github.com/webpack/webpack-dev-server/issues/887
// https://medium.com/webpack/webpack-dev-server-middleware-security-issues-1489d950874a
// However, it made several existing use cases such as development in cloud
// environment or subdomains in development significantly more complicated:
// https://github.com/facebook/create-react-app/issues/2271
// https://github.com/facebook/create-react-app/issues/2233
// While we're investigating better solutions, for now we will take a
// compromise. Since our WDS configuration only serves files in the `public`
// folder we won't consider accessing them a vulnerability. However, if you
// use the `proxy` feature, it gets more dangerous because it can expose
// remote code execution vulnerabilities in backends like Django and Rails.
// So we will disable the host check normally, but enable it if you have
// specified the `proxy` setting. Finally, we let you override it if you
// really know what you're doing with a special environment variable.
disableHostCheck:
!proxy || process.env.DANGEROUSLY_DISABLE_HOST_CHECK === 'true',
// Enable gzip compression of generated files.
compress: true,
// Silence WebpackDevServer's own logs since they're generally not useful.
// It will still show compile warnings and errors with this setting.
clientLogLevel: 'none',
// By default WebpackDevServer serves physical files from current directory
// in addition to all the virtual build products that it serves from memory.
// This is confusing because those files won’t automatically be available in
// production build folder unless we copy them. However, copying the whole
// project directory is dangerous because we may expose sensitive files.
// Instead, we establish a convention that only files in `public` directory
// get served. Our build script will copy `public` into the `build` folder.
// In `index.html`, you can get URL of `public` folder with %PUBLIC_URL%:
// <link rel="icon" href="%PUBLIC_URL%/favicon.ico">
// In JavaScript code, you can access it with `process.env.PUBLIC_URL`.
// Note that we only recommend to use `public` folder as an escape hatch
// for files like `favicon.ico`, `manifest.json`, and libraries that are
// for some reason broken when imported through webpack. If you just want to
// use an image, put it in `src` and `import` it from JavaScript instead.
contentBase: paths.appPublic,
contentBasePublicPath: paths.publicUrlOrPath,
// By default files from `contentBase` will not trigger a page reload.
watchContentBase: true,
// Enable hot reloading server. It will provide WDS_SOCKET_PATH endpoint
// for the WebpackDevServer client so it can learn when the files were
// updated. The WebpackDevServer client is included as an entry point
// in the webpack development configuration. Note that only changes
// to CSS are currently hot reloaded. JS changes will refresh the browser.
hot: true,
// Use 'ws' instead of 'sockjs-node' on server since we're using native
// websockets in `webpackHotDevClient`.
transportMode: 'ws',
// Prevent a WS client from getting injected as we're already including
// `webpackHotDevClient`.
injectClient: false,
// Enable custom sockjs pathname for websocket connection to hot reloading server.
// Enable custom sockjs hostname, pathname and port for websocket connection
// to hot reloading server.
sockHost,
sockPath,
sockPort,
// It is important to tell WebpackDevServer to use the same "publicPath" path as
// we specified in the webpack config. When homepage is '.', default to serving
// from the root.
// remove last slash so user can land on `/test` instead of `/test/`
publicPath: paths.publicUrlOrPath.slice(0, -1),
// WebpackDevServer is noisy by default so we emit custom message instead
// by listening to the compiler events with `compiler.hooks[...].tap` calls above.
quiet: true,
// Reportedly, this avoids CPU overload on some systems.
// https://github.com/facebook/create-react-app/issues/293
// src/node_modules is not ignored to support absolute imports
// https://github.com/facebook/create-react-app/issues/1065
watchOptions: {
ignored: ignoredFiles(paths.appSrc),
},
https: getHttpsConfig(),
host,
overlay: false,
historyApiFallback: {
// Paths with dots should still use the history fallback.
// See https://github.com/facebook/create-react-app/issues/387.
disableDotRule: true,
index: paths.publicUrlOrPath,
},
public: allowedHost,
// `proxy` is run between `before` and `after` `webpack-dev-server` hooks
proxy,
before(app, server) {
// Keep `evalSourceMapMiddleware` and `errorOverlayMiddleware`
// middlewares before `redirectServedPath` otherwise will not have any effect
// This lets us fetch source contents from webpack for the error overlay
app.use(evalSourceMapMiddleware(server));
// This lets us open files from the runtime error overlay.
app.use(errorOverlayMiddleware());
if (fs.existsSync(paths.proxySetup)) {
// This registers user provided middleware for proxy reasons
require(paths.proxySetup)(app);
}
},
after(app) {
// Redirect to `PUBLIC_URL` or `homepage` from `package.json` if url not match
app.use(redirectServedPath(paths.publicUrlOrPath));
// This service worker file is effectively a 'no-op' that will reset any
// previous service worker registered for the same host:port combination.
// We do this in development to avoid hitting the production cache if
// it used the same host and port.
// https://github.com/facebook/create-react-app/issues/2272#issuecomment-302832432
app.use(noopServiceWorkerMiddleware(paths.publicUrlOrPath));
},
};
};

@ -1,89 +0,0 @@
: start
!> "Next stop, Syrapool. Five minutes. Have your bags ready. Syrapool station next."
> The train conductor's voice cuts through my reverie, and I shake myself awake. I've been zoning out while staring at the scenery passing by outside for a long time, I realize. And here I thought a four-hour train ride was going to be too long for me.
> This is kind of a big moment for me. I haven't been back to Syrapool since my family and I moved out to the country to take care of my grandparents twelve years ago. I remember that it's a big port city, tons of people and goods coming in and out all the time. I remember that I loved sitting with my best friend and watching all the different people go by...
> Well, soon I'm going to see her again. I wonder if she still likes people-watching.
!> I immediately feel out of place as soon as I step off the train. Sure, when I was younger all this bustle felt normal and familiar, and I was able to find my place in the flow.
> But now, seeing the massive throngs of people moving around the enormous station... Hearing the automated announcements over the PA while the digital displays conduct the chaotic symphony around me... (Where are the adorable old-timey flap displays my friend and I used to love looking at?)
> I feel like an outsider already. It doesn't help that with my nap-disheveled dark hair, my slightly askew glasses, my pink, cat-eared "Crazy Cat Lady" hoodie, my hole-y jeans, and my beat-up sneakers, I {{#i}}look{{/i}} like an outsider compared to all these rushing, neatly-attired businesspeople.
!> There are a couple of things around that I don't recognize at all. Near the door to the bathrooms, there's another door. The symbol on it looks like a kidney bean half filled with water. I see what looks like a pregnant woman going inside. Some kind of rest or care room for pregnant people, maybe?
> And there's a room with an icon of a fountain on it, but there's a water fountain literally right next to it. I feel like I'm in another country, not the city I spent the first nine years of my life in.
!> ... My phone vibrates. It's a text from my host-to-be. "Where are you? Come onnnnn, aren't you excited?! I know your train came in! I want to see you again! And I have so much to show you!"
> I smile to myself and begin rolling my suitcase toward the signs pointing me to the exit. At least some things don't change.
!> Her directions were easy enough to follow, and the streets don't feel quite as overwhelmingly different. Near the public bathrooms I pass are small booths with kidney beans and water fountains again. Someone comes out of the water fountain room, wearing some kind of uniform. ... I put it out of my mind. I'll need my host to explain.
> One way or another, I reach the entrance to her apartment building. Which will soon be my apartment building, at least for now. It's kind of overwhelming, with its sleek, clean dark lines, its recessed lighting, its polished tiles. I feel like I'm in a hotel. A hotel for rich people. I approach the front desk. "Uh, hello... I'm the new tenant?"
> The young woman in the clean-cut business suit there looks up from her computer and meets my gaze. "Cute accent. Name?"
$ Miriam = name(Miriam, "A 21-year-old digital artist and professional slacker from the country. I grew up in Syrapool, and have come back after twelve years away to finish my education at the Syrapool Academy of the Fine Arts. What's my name?", "Miriam")
!> Feeling somewhat self-conscious about my accent now, I enunciate my name as clearly as I can manage. "{{Miriam}}. {{Miriam}} Dahl."
> She types it into her computer and looks through the listings for a bit, her scroll wheel clicking quietly in the relative silence of the entrance hall. "{{Miriam}}? We don't have a record of a {{Miriam}} Dahl here. Do you have a contact here?"
$ Klara = name(Klara, "A 23-year-old self-employed filmmaker who's just bursting with energy. My best friend from my time growing up in Syrapool, and for at least the rest of her lease, my new roommate. What's her name?", "Klara")
!> "{{Klara}} Lundell," I say, now getting a little bit nervous. {{Klara}} did say that she was doing all this on the up-and-up, right? She did tell me to go to the front desk? "In apartment 8F, I think?"
> The front desk attendant does some clicking and typing while I wait. "Ah, I see. She put it under her own name." I feel the tension go out of my shoulders. She reaches under her desk and produces an envelope. "Here's your keycard. It will unlock the front door, set the elevator to go to your floor, and open your apartment." She pulls another tool out from under her desk. "Let me see your hand?"
!> I extend my hand across the desk, and she places the tool over my finger. I feel a sudden prick. "Ow!"
> She plugs the tool into her computer. "For the safety of our residents, it's bound to you using the DNA sample you've just provided, so in the event of your digestion, the keycard will be deactivated until you scan it at any Refreshen fountain."
> "Uh, in the event of my digestion? Is that... likely? Doesn't that... you know... mean that I'm not going to be around to scan it?" I feel vaguely uncomfortable thinking about this. I've heard about girls who eat other girls, but mostly only in crime stories...
> She gives me an odd look. "We would certainly hope that you will. In any case, that's all from me. Thank you, {{Miriam}}, and we hope you feel safe at your home here."
> "Thanks..."
!> After some fumbling with the keycard, I make it to the 8th floor and knock on {{Klara}}'s door. "{{Klara}}? I'm here!"
> I barely get the words out before the door is thrown open and Klara launches into a tight hug around me, nearly breaking me in half. "Oh my god! You got tall!" she says. "Remember when I used to tease you about how short you were?"
> I was only a few inches shorter than her... but now, in turn, I have a few inches on her. And I'm not that tall - she got her growth spurt early and never again, it seems. On the other hand, even squished against her, I an feel that her chest puts mine to shame... "Are you reminding me so I can do it back to you? You grew in... other places, I guess..."
> Klara laughs. It feels so good to hear her laughing again... I realize I really missed it. "Come in, come in!"
!> We spend some time catching up. She tells me about her filmmaking business, mostly recording fancy weddings and giving them a highlight reel to remember the event by. I tell her about my studies on 3D modeling at Virliai College, and how I've been trying to get in here at Syrapool but my grades weren't good enough in high school. The conversation gradually peters out as our stomachs start growling.
> "Well, should we go get some dinner?" She reaches into her pocket and flashes me a deck of blank white cards with an odd pearlescent sheen, grinning. Embossed into each card is the water fountain icon I'd seen earlier.
> "What are those?" I ask, confused. "Oh, that's the water fountain I saw before! What are those rooms for, anyway?"
!> She looks at me with some surprise. "You don't know? They don't have Refreshen where you are?"
> "Virliai's the biggest town in its area and it's still tiny compared to Syrapool, we don't have anything there," I say, laughing.
> A sudden grin comes over {{Klara}}'s face. "Oh, that's right... The laws only just changed ten years ago, and you were already gone, weren't you?" I feel like she's a cat eying up a particularly stupid mouse. And I'm the mouse. She covers her lips with the deck of cards, but I can still see the catlike smile in her eyes. "Well, then, maybe I can get dinner here instead..."
$ battle(Tutorial_Klara)
if digestedLastBattle(self)
goto Tutorial_Klara_Digested_Aftermath
else if escapedLastBattle(opponent)
goto Tutorial_Klara_Intimidated_Aftermath
else if digestedLastBattle(opponent)
goto Tutorial_Klara_Reversal_Aftermath
else
# if escapedLastBattle(self)
goto Tutorial_Klara_Escaped_Aftermath
fi
: Tutorial_Klara_start
[] You lock eyes with Klara!
[Klara] Heh heh heh...
[Miriam] ... What is happening? You're looking at me funny.
[Klara] I'm about to give you a hands-on lesson in city living, is what!
select I don't know if I like the sound of that...
cancel default choice (Stay quiet and listen.)
$ forceAction(opponent, Tutorial_Drool)
return
choice I don't need a lesson.
$ skippedTutorial = true
[Miriam] I don't need a lesson! I lived here for nine years!
[Klara] Well, I still need dinner!
return
selected
: Tutorial_Klara_Drool
[Miriam] *swallows hard* C-Come on, {{Klara}}... You're not... really going to...
[Klara] Aw, don't be so scared, {{Miriam}}, have a little more faith in yourself.
$ tutorialHighlight(self, confidence, true)
[Klara] See that bar at the bottom of the screen?
[Klara] That's your Confidence meter. When people threaten you, or tease you, and you worry that they might actually eat you. it gets lower. Like it did just now. If you run out, you're easy pickings for them.
$ tutorialHighlight(self, confidence, false)
[Miriam] Well, what if I DON'T want to be eaten?!
$ tutorialHighlight(opponent, confidence, true)
[Klara] This one on the top is mine. If you get it all the way down, I'll leave you alone. And you might even manage to eat {{#i}}me{{/i}}! Though I doubt it! I'm pretty good at this.
$ tutorialHighlight(opponent, confidence, false)
[Miriam] Yeah, I want to do that. How do I do that?
$ enableActionType(Main)
$ disableActionType(Recovery)
$ disableActionType(Finish)
$ enableAct()
$ disableItems()
$ disableDefend()
$ disableFlee()
[Klara] Well, you fight back! Turnabout is fair play, after all! Here, let me show you.
$ tutorialForceBattleMenu(Act)
[Klara] Choose the "Act" option.
$ tutorialForceActionMenu(Protest)
[Klara] Now choose the "Protest" action.
$ forceAction(self, Tutorial_Protest)

@ -0,0 +1,197 @@
/*
* For a detailed explanation regarding each configuration property and type check, visit:
* https://jestjs.io/docs/configuration
*/
export default {
// All imported modules in your tests should be mocked automatically
// automock: false,
// Stop running tests after `n` failures
// bail: 0,
// The directory where Jest should store its cached dependency information
// cacheDirectory: "/data/data/com.termux/files/usr/tmp/jest_835",
// Automatically clear mock calls, instances, contexts and results before every test
clearMocks: true,
// Indicates whether the coverage information should be collected while executing the test
collectCoverage: true,
// An array of glob patterns indicating a set of files for which coverage information should be collected
collectCoverageFrom: ["src/**.{js,ts}", "!node_modules/**", "!src/*.test.{js,ts}"],
// The directory where Jest should output its coverage files
coverageDirectory: "coverage",
// An array of regexp pattern strings used to skip coverage collection
// coveragePathIgnorePatterns: [
// "/node_modules/",
// "/src/.*\\.test\\.ts",
// "/src/.*\\.test\\.js",
// ],
// Indicates which provider should be used to instrument code for coverage
coverageProvider: "v8",
// A list of reporter names that Jest uses when writing coverage reports
// coverageReporters: [
// "json",
// "text",
// "lcov",
// "clover"
// ],
// An object that configures minimum threshold enforcement for coverage results
// coverageThreshold: undefined,
// A path to a custom dependency extractor
// dependencyExtractor: undefined,
// Make calling deprecated APIs throw helpful error messages
// errorOnDeprecated: false,
// The default configuration for fake timers
// fakeTimers: {
// "enableGlobally": false
// },
// Force coverage collection from ignored files using an array of glob patterns
// forceCoverageMatch: [],
// A path to a module which exports an async function that is triggered once before all test suites
// globalSetup: undefined,
// A path to a module which exports an async function that is triggered once after all test suites
// globalTeardown: undefined,
// A set of global variables that need to be available in all test environments
// globals: {},
// The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
// maxWorkers: "50%",
// An array of directory names to be searched recursively up from the requiring module's location
// moduleDirectories: [
// "node_modules"
// ],
// An array of file extensions your modules use
// moduleFileExtensions: [
// "js",
// "mjs",
// "cjs",
// "jsx",
// "ts",
// "tsx",
// "json",
// "node"
// ],
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
// moduleNameMapper: {},
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
// modulePathIgnorePatterns: [],
// Activates notifications for test results
// notify: false,
// An enum that specifies notification mode. Requires { notify: true }
// notifyMode: "failure-change",
// A preset that is used as a base for Jest's configuration
preset: 'ts-jest',
// Run tests from one or more projects
// projects: undefined,
// Use this configuration option to add custom reporters to Jest
// reporters: undefined,
// Automatically reset mock state before every test
// resetMocks: false,
// Reset the module registry before running each individual test
// resetModules: false,
// A path to a custom resolver
// resolver: undefined,
// Automatically restore mock state and implementation before every test
// restoreMocks: false,
// The root directory that Jest should scan for tests and modules within
// rootDir: undefined,
// A list of paths to directories that Jest should use to search for files in
// roots: [
// "<rootDir>"
// ],
// Allows you to use a custom runner instead of Jest's default test runner
// runner: "jest-runner",
// The paths to modules that run some code to configure or set up the testing environment before each test
// setupFiles: [],
// A list of paths to modules that run some code to configure or set up the testing framework before each test
// setupFilesAfterEnv: [],
// The number of seconds after which a test is considered as slow and reported as such in the results.
// slowTestThreshold: 5,
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
// snapshotSerializers: [],
// The test environment that will be used for testing
// testEnvironment: "jest-environment-node",
// Options that will be passed to the testEnvironment
// testEnvironmentOptions: {},
// Adds a location field to test results
// testLocationInResults: false,
// The glob patterns Jest uses to detect test files
// testMatch: [
// "**/__tests__/**/*.[jt]s?(x)",
// "**/?(*.)+(spec|test).[tj]s?(x)"
// ],
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
// testPathIgnorePatterns: [
// "/node_modules/"
// ],
// The regexp pattern or array of patterns that Jest uses to detect test files
// testRegex: [],
// This option allows the use of a custom results processor
// testResultsProcessor: undefined,
// This option allows use of a custom test runner
// testRunner: "jest-circus/runner",
// A map from regular expressions to paths to transformers
// transform: undefined,
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
// transformIgnorePatterns: [
// "/node_modules/",
// "\\.pnp\\.[^\\/]+$"
// ],
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
// unmockedModulePathPatterns: undefined,
// Indicates whether each individual test should be reported during the run
// verbose: undefined,
// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
// watchPathIgnorePatterns: [],
// Whether to use watchman for file crawling
// watchman: true,
};

45297
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -1,166 +1,23 @@
{ {
"name": "vrpg-system", "name": "vore-rpg",
"version": "0.1.0", "version": "0.0.0",
"description": "Vore RPG core",
"private": true, "private": true,
"dependencies": { "main": "index.js",
"@babel/core": "7.12.3",
"@babel/preset-typescript": "^7.15.0",
"@jest/create-cache-key-function": "^27.3.1",
"@pmmmwh/react-refresh-webpack-plugin": "0.4.3",
"@svgr/webpack": "5.5.0",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^11.2.7",
"@testing-library/user-event": "^12.8.3",
"@types/jest": "^26.0.24",
"@types/node": "^12.20.21",
"@types/react": "^17.0.19",
"@types/react-dom": "^17.0.9",
"@typescript-eslint/eslint-plugin": "^4.5.0",
"@typescript-eslint/parser": "^4.5.0",
"babel-eslint": "^10.1.0",
"babel-jest": "^26.6.0",
"babel-loader": "8.1.0",
"babel-plugin-named-asset-import": "^0.3.7",
"babel-preset-react-app": "^10.0.0",
"bfj": "^7.0.2",
"camelcase": "^6.1.0",
"case-sensitive-paths-webpack-plugin": "2.3.0",
"css-loader": "4.3.0",
"dotenv": "8.2.0",
"dotenv-expand": "5.1.0",
"eslint": "^7.11.0",
"eslint-config-react-app": "^6.0.0",
"eslint-plugin-flowtype": "^5.2.0",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-jest": "^24.1.0",
"eslint-plugin-jsx-a11y": "^6.3.1",
"eslint-plugin-react": "^7.21.5",
"eslint-plugin-react-hooks": "^4.2.0",
"eslint-plugin-testing-library": "^3.9.2",
"eslint-webpack-plugin": "^2.5.2",
"file-loader": "6.1.1",
"fs-extra": "^9.0.1",
"html-webpack-plugin": "4.5.0",
"identity-obj-proxy": "3.0.0",
"jest": "26.6.0",
"jest-circus": "26.6.0",
"jest-resolve": "26.6.0",
"jest-watch-typeahead": "0.6.1",
"loader-utils": "^2.0.0",
"mini-css-extract-plugin": "0.11.3",
"optimize-css-assets-webpack-plugin": "5.0.4",
"peggy": "^1.2.0",
"peggy-tracks": "^1.1.0",
"pnp-webpack-plugin": "1.6.4",
"postcss-flexbugs-fixes": "4.2.1",
"postcss-loader": "3.0.0",
"postcss-normalize": "8.0.1",
"postcss-preset-env": "6.7.0",
"postcss-safe-parser": "5.0.2",
"prettier": "^2.4.1",
"prompts": "2.4.0",
"react": "^17.0.2",
"react-app-polyfill": "^2.0.0",
"react-dev-utils": "^11.0.3",
"react-dom": "^17.0.2",
"react-refresh": "^0.8.3",
"resolve": "1.18.1",
"resolve-url-loader": "^3.1.2",
"sass-loader": "^10.0.5",
"semver": "7.3.2",
"serve": "^12.0.1",
"style-loader": "1.3.0",
"terser-webpack-plugin": "4.2.3",
"ts-pnp": "1.2.0",
"typescript": "^4.3.5",
"url-loader": "4.1.1",
"web-vitals": "^1.1.2",
"webpack": "4.44.2",
"webpack-dev-server": "3.11.1",
"webpack-manifest-plugin": "2.2.0",
"workbox-webpack-plugin": "5.1.4"
},
"scripts": { "scripts": {
"start": "node scripts/start.js", "test": "jest"
"build": "node scripts/build.js",
"test": "node scripts/test.js",
"serve": "serve -s build"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
}, },
"browserslist": { "author": "",
"production": [ "license": "ISC",
">0.2%", "dependencies": {
"not dead", "@reduxjs/toolkit": "^1.8.5",
"not op_mini all" "discord.js": "^14.5.0"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"jest": {
"roots": [
"<rootDir>/src"
],
"collectCoverageFrom": [
"src/**/*.{js,jsx,ts,tsx,peggy}",
"!src/**/*.d.ts"
],
"setupFiles": [
"react-app-polyfill/jsdom"
],
"setupFilesAfterEnv": [
"<rootDir>/src/setupTests.ts"
],
"testMatch": [
"<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}",
"<rootDir>/src/**/*.{spec,test}.{js,jsx,ts,tsx}"
],
"testEnvironment": "jsdom",
"testRunner": "<rootDir>/node_modules/jest-circus/runner.js",
"transform": {
"^.+\\.peggy$": "<rootDir>/config/jest/peggyTransform.js",
"^.+\\.(js|jsx|mjs|cjs|ts|tsx)$": "<rootDir>/config/jest/babelTransform.js",
"^.+\\.css$": "<rootDir>/config/jest/cssTransform.js",
"^(?!.*\\.(js|jsx|mjs|cjs|ts|tsx|css|json|peggy)$)": "<rootDir>/config/jest/fileTransform.js"
},
"transformIgnorePatterns": [
"[/\\\\]node_modules[/\\\\].+\\.(js|jsx|mjs|cjs|ts|tsx)$",
"^.+\\.module\\.(css|sass|scss)$"
],
"modulePaths": [],
"moduleNameMapper": {
"^react-native$": "react-native-web",
"^.+\\.module\\.(css|sass|scss)$": "identity-obj-proxy"
},
"moduleFileExtensions": [
"peggy",
"web.js",
"js",
"web.ts",
"ts",
"web.tsx",
"tsx",
"json",
"web.jsx",
"jsx",
"node"
],
"watchPlugins": [
"jest-watch-typeahead/filename",
"jest-watch-typeahead/testname"
],
"resetMocks": true
}, },
"babel": { "devDependencies": {
"presets": [ "@tsconfig/node18": "^1.0.1",
"react-app" "jest": "^29.1.2",
] "ts-jest": "^29.0.3",
"ts-node": "^10.9.1",
"typescript": "^4.8.4"
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

@ -1,43 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

@ -1,25 +0,0 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

@ -1,3 +0,0 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

@ -1,212 +0,0 @@
'use strict';
// Do this as the first thing so that any code reading it knows the right env.
process.env.BABEL_ENV = 'production';
process.env.NODE_ENV = 'production';
// Makes the script crash on unhandled rejections instead of silently
// ignoring them. In the future, promise rejections that are not handled will
// terminate the Node.js process with a non-zero exit code.
process.on('unhandledRejection', err => {
throw err;
});
// Ensure environment variables are read.
require('../config/env');
const path = require('path');
const chalk = require('react-dev-utils/chalk');
const fs = require('fs-extra');
const bfj = require('bfj');
const webpack = require('webpack');
const configFactory = require('../config/webpack.config');
const paths = require('../config/paths');
const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles');
const formatWebpackMessages = require('react-dev-utils/formatWebpackMessages');
const printHostingInstructions = require('react-dev-utils/printHostingInstructions');
const FileSizeReporter = require('react-dev-utils/FileSizeReporter');
const printBuildError = require('react-dev-utils/printBuildError');
const measureFileSizesBeforeBuild =
FileSizeReporter.measureFileSizesBeforeBuild;
const printFileSizesAfterBuild = FileSizeReporter.printFileSizesAfterBuild;
const useYarn = fs.existsSync(paths.yarnLockFile);
// These sizes are pretty large. We'll warn for bundles exceeding them.
const WARN_AFTER_BUNDLE_GZIP_SIZE = 512 * 1024;
const WARN_AFTER_CHUNK_GZIP_SIZE = 1024 * 1024;
const isInteractive = process.stdout.isTTY;
// Warn and crash if required files are missing
if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) {
process.exit(1);
}
const argv = process.argv.slice(2);
const writeStatsJson = argv.indexOf('--stats') !== -1;
// Generate configuration
const config = configFactory('production');
// We require that you explicitly set browsers and do not fall back to
// browserslist defaults.
const { checkBrowsers } = require('react-dev-utils/browsersHelper');
checkBrowsers(paths.appPath, isInteractive)
.then(() => {
// First, read the current file sizes in build directory.
// This lets us display how much they changed later.
return measureFileSizesBeforeBuild(paths.appBuild);
})
.then(previousFileSizes => {
// Remove all content but keep the directory so that
// if you're in it, you don't end up in Trash
fs.emptyDirSync(paths.appBuild);
// Merge with the public folder
copyPublicFolder();
// Start the webpack build
return build(previousFileSizes);
})
.then(
({ stats, previousFileSizes, warnings }) => {
if (warnings.length) {
console.log(chalk.yellow('Compiled with warnings.\n'));
console.log(warnings.join('\n\n'));
console.log(
'\nSearch for the ' +
chalk.underline(chalk.yellow('keywords')) +
' to learn more about each warning.'
);
console.log(
'To ignore, add ' +
chalk.cyan('// eslint-disable-next-line') +
' to the line before.\n'
);
} else {
console.log(chalk.green('Compiled successfully.\n'));
}
console.log('File sizes after gzip:\n');
printFileSizesAfterBuild(
stats,
previousFileSizes,
paths.appBuild,
WARN_AFTER_BUNDLE_GZIP_SIZE,
WARN_AFTER_CHUNK_GZIP_SIZE
);
console.log();
const appPackage = require(paths.appPackageJson);
const publicUrl = paths.publicUrlOrPath;
const publicPath = config.output.publicPath;
const buildFolder = path.relative(process.cwd(), paths.appBuild);
printHostingInstructions(
appPackage,
publicUrl,
publicPath,
buildFolder,
useYarn
);
},
err => {
const tscCompileOnError = process.env.TSC_COMPILE_ON_ERROR === 'true';
if (tscCompileOnError) {
console.log(
chalk.yellow(
'Compiled with the following type errors (you may want to check these before deploying your app):\n'
)
);
printBuildError(err);
} else {
console.log(chalk.red('Failed to compile.\n'));
printBuildError(err);
process.exit(1);
}
}
)
.catch(err => {
if (err && err.message) {
console.log(err.message);
}
process.exit(1);
});
// Create the production build and print the deployment instructions.
function build(previousFileSizes) {
console.log('Creating an optimized production build...');
const compiler = webpack(config);
return new Promise((resolve, reject) => {
compiler.run((err, stats) => {
let messages;
if (err) {
if (!err.message) {
return reject(err);
}
let errMessage = err.message;
// Add additional information for postcss errors
if (Object.prototype.hasOwnProperty.call(err, 'postcssNode')) {
errMessage +=
'\nCompileError: Begins at CSS selector ' +
err['postcssNode'].selector;
}
messages = formatWebpackMessages({
errors: [errMessage],
warnings: [],
});
} else {
messages = formatWebpackMessages(
stats.toJson({ all: false, warnings: true, errors: true })
);
}
if (messages.errors.length) {
// Only keep the first error. Others are often indicative
// of the same problem, but confuse the reader with noise.
if (messages.errors.length > 1) {
messages.errors.length = 1;
}
return reject(new Error(messages.errors.join('\n\n')));
}
if (
process.env.CI &&
(typeof process.env.CI !== 'string' ||
process.env.CI.toLowerCase() !== 'false') &&
messages.warnings.length
) {
console.log(
chalk.yellow(
'\nTreating warnings as errors because process.env.CI = true.\n' +
'Most CI servers set it automatically.\n'
)
);
return reject(new Error(messages.warnings.join('\n\n')));
}
const resolveArgs = {
stats,
previousFileSizes,
warnings: messages.warnings,
};
if (writeStatsJson) {
return bfj
.write(paths.appBuild + '/bundle-stats.json', stats.toJson())
.then(() => resolve(resolveArgs))
.catch(error => reject(new Error(error)));
}
return resolve(resolveArgs);
});
});
}
function copyPublicFolder() {
fs.copySync(paths.appPublic, paths.appBuild, {
dereference: true,
filter: file => file !== paths.appHtml,
});
}

@ -1,166 +0,0 @@
'use strict';
// Do this as the first thing so that any code reading it knows the right env.
process.env.BABEL_ENV = 'development';
process.env.NODE_ENV = 'development';
// Makes the script crash on unhandled rejections instead of silently
// ignoring them. In the future, promise rejections that are not handled will
// terminate the Node.js process with a non-zero exit code.
process.on('unhandledRejection', err => {
throw err;
});
// Ensure environment variables are read.
require('../config/env');
const fs = require('fs');
const chalk = require('react-dev-utils/chalk');
const webpack = require('webpack');
const WebpackDevServer = require('webpack-dev-server');
const clearConsole = require('react-dev-utils/clearConsole');
const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles');
const {
choosePort,
createCompiler,
prepareProxy,
prepareUrls,
} = require('react-dev-utils/WebpackDevServerUtils');
const openBrowser = require('react-dev-utils/openBrowser');
const semver = require('semver');
const paths = require('../config/paths');
const configFactory = require('../config/webpack.config');
const createDevServerConfig = require('../config/webpackDevServer.config');
const getClientEnvironment = require('../config/env');
const react = require(require.resolve('react', { paths: [paths.appPath] }));
const env = getClientEnvironment(paths.publicUrlOrPath.slice(0, -1));
const useYarn = fs.existsSync(paths.yarnLockFile);
const isInteractive = process.stdout.isTTY;
// Warn and crash if required files are missing
if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) {
process.exit(1);
}
// Tools like Cloud9 rely on this.
const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 3000;
const HOST = process.env.HOST || '0.0.0.0';
if (process.env.HOST) {
console.log(
chalk.cyan(
`Attempting to bind to HOST environment variable: ${chalk.yellow(
chalk.bold(process.env.HOST)
)}`
)
);
console.log(
`If this was unintentional, check that you haven't mistakenly set it in your shell.`
);
console.log(
`Learn more here: ${chalk.yellow('https://cra.link/advanced-config')}`
);
console.log();
}
// We require that you explicitly set browsers and do not fall back to
// browserslist defaults.
const { checkBrowsers } = require('react-dev-utils/browsersHelper');
checkBrowsers(paths.appPath, isInteractive)
.then(() => {
// We attempt to use the default port but if it is busy, we offer the user to
// run on a different port. `choosePort()` Promise resolves to the next free port.
return choosePort(HOST, DEFAULT_PORT);
})
.then(port => {
if (port == null) {
// We have not found a port.
return;
}
const config = configFactory('development');
const protocol = process.env.HTTPS === 'true' ? 'https' : 'http';
const appName = require(paths.appPackageJson).name;
const useTypeScript = fs.existsSync(paths.appTsConfig);
const tscCompileOnError = process.env.TSC_COMPILE_ON_ERROR === 'true';
const urls = prepareUrls(
protocol,
HOST,
port,
paths.publicUrlOrPath.slice(0, -1)
);
const devSocket = {
warnings: warnings =>
devServer.sockWrite(devServer.sockets, 'warnings', warnings),
errors: errors =>
devServer.sockWrite(devServer.sockets, 'errors', errors),
};
// Create a webpack compiler that is configured with custom messages.
const compiler = createCompiler({
appName,
config,
devSocket,
urls,
useYarn,
useTypeScript,
tscCompileOnError,
webpack,
});
// Load proxy config
const proxySetting = require(paths.appPackageJson).proxy;
const proxyConfig = prepareProxy(
proxySetting,
paths.appPublic,
paths.publicUrlOrPath
);
// Serve webpack assets generated by the compiler over a web server.
const serverConfig = createDevServerConfig(
proxyConfig,
urls.lanUrlForConfig
);
const devServer = new WebpackDevServer(compiler, serverConfig);
// Launch WebpackDevServer.
devServer.listen(port, HOST, err => {
if (err) {
return console.log(err);
}
if (isInteractive) {
clearConsole();
}
if (env.raw.FAST_REFRESH && semver.lt(react.version, '16.10.0')) {
console.log(
chalk.yellow(
`Fast Refresh requires React 16.10 or higher. You are using React ${react.version}.`
)
);
}
console.log(chalk.cyan('Starting the development server...\n'));
openBrowser(urls.localUrlForBrowser);
});
['SIGINT', 'SIGTERM'].forEach(function (sig) {
process.on(sig, function () {
devServer.close();
process.exit();
});
});
if (process.env.CI !== 'true') {
// Gracefully exit when stdin ends
process.stdin.on('end', function () {
devServer.close();
process.exit();
});
}
})
.catch(err => {
if (err && err.message) {
console.log(err.message);
}
process.exit(1);
});

@ -1,53 +0,0 @@
'use strict';
// Do this as the first thing so that any code reading it knows the right env.
process.env.BABEL_ENV = 'test';
process.env.NODE_ENV = 'test';
process.env.PUBLIC_URL = '';
// Makes the script crash on unhandled rejections instead of silently
// ignoring them. In the future, promise rejections that are not handled will
// terminate the Node.js process with a non-zero exit code.
process.on('unhandledRejection', err => {
throw err;
});
// Ensure environment variables are read.
require('../config/env');
const jest = require('jest');
const execSync = require('child_process').execSync;
let argv = process.argv.slice(2);
function isInGitRepository() {
try {
execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore' });
return true;
} catch (e) {
return false;
}
}
function isInMercurialRepository() {
try {
execSync('hg --cwd . root', { stdio: 'ignore' });
return true;
} catch (e) {
return false;
}
}
// Watch unless on CI or explicitly running all tests
if (
!process.env.CI &&
argv.indexOf('--watchAll') === -1 &&
argv.indexOf('--watchAll=false') === -1
) {
// https://github.com/facebook/create-react-app/issues/5210
const hasSourceControl = isInGitRepository() || isInMercurialRepository();
argv.push(hasSourceControl ? '--watch' : '--watchAll');
}
jest.run(argv);

@ -1,38 +0,0 @@
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

@ -1,9 +0,0 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

@ -1,26 +0,0 @@
import React from 'react';
import logo from './logo.svg';
import './App.css';
function App() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.tsx</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);
}
export default App;

@ -1,76 +0,0 @@
/** The interface for actions that can be selected during battle. This is also used automatically for the Defend and Flee options. */
export interface ActionData {
/** The name that will be displayed in the action list - a Handlebars template. */
readonly nameTemplate: string
/** The help message that will be displayed when this action is selected - a Handlebars template. */
readonly helpMessageTemplate: string
/** The cost of the action - a script expression. The cost of the action is automatically deducted, and need not be deducted by the effect as well. */
readonly costExpression: string
/** Whether the action can currently be used, and if so, how much of the turn it takes up - a script expression. */
readonly speedExpression: string
/** The effect of the action - a sequence of script statements separated by newlines. */
readonly actionEffect: string
}
/** The state of an action, determining its display in the action menu. */
export enum ActionSpeed {
/**
* Fast actions only cost half the user's turn.
* If a Fast action is used at the start of the user's turn, they can use a single Fast or Normal action afterward.
*/
FAST="Fast",
/**
* Normal actions only cost half (or maybe two thirds) of the user's turn.
* If these actions are used on the user's turn, it ends, but they can be used after a Fast action.
*/
NORMAL="Normal",
/**
* Slow actions cost a user's entire turn.
* They can't be used after Fast actions, and the user's turn ends after using them.
*/
SLOW="Slow",
/**
* Disabled actions are shown in the action list, but cannot be used.
*/
DISABLED="Disabled",
/**
* Hidden actions are not shown in the action list and cannot be used.
*/
HIDDEN="Hidden",
}
/** The counters that actions' costs and damage/healing can deduct from. */
export enum Counter {
CONFIDENCE="Confidence",
HEALTH="Health",
STAMINA="Stamina",
ENERGY="Energy",
}
/** The interface for an action's cost. */
export interface ActionCost {
/** The counter that the cost will be deducted from. */
readonly affects: Counter
/**
* The cost, which the counter must be greater than or equal to for the skill to be used, and
* which will be deducted from the counter when it is used.
*/
readonly cost: number
}
/**
* The interface for an evaluated action. This saves time recalculating actions on every repaint and avoids cases
* where the cost a user sees before choosing the action is not the same as the cost a user sees when using it.
*/
export interface Action {
/** The data this action originated from. */
readonly data: ActionData
/** The name, with any template variables substituted in. */
readonly name: string
/** The help message, with any template variables substituted in. */
readonly helpMessage: string
/** The cost, which has been evaluated into an optimized set of ActionCost with at most one ActionCost per Counter. */
readonly cost: readonly ActionCost[]
/** The speed of the action, determining when it can be used and what other actions can be used with it. */
readonly speed: ActionSpeed
}

@ -1,321 +0,0 @@
import {
checkValidCurrentEnergy,
checkValidEnergy,
checkValidMaxStamina, damageEnergy, damageStamina,
Energy,
energyToString, isValidCurrentEnergy,
isValidEnergy,
isValidMaxStamina, recoverEnergy, recoverStamina, setCurrentEnergy, setCurrentStamina, setMaxStamina
} from "./Energy";
import {assertion, AssertionConfig} from "../testing/Assertions";
let originalConfig: AssertionConfig.Type
beforeEach(() => {
originalConfig = assertion.config
assertion.config = AssertionConfig.Throw
});
afterEach(() => {
assertion.config = originalConfig
});
const InvalidEnergy: Energy = {maxStamina: 0, currentStamina: 0, currentEnergy: 0}
describe("energyToString", () => {
const values: [Energy, string][] = [
[{currentEnergy: 10, currentStamina: 20, maxStamina: 30}, "{Energy 10/20/30}"],
[{currentEnergy: 30, currentStamina: 30, maxStamina: 30}, "{Energy 30/30/30}"],
[{currentEnergy: 100, currentStamina: 5000, maxStamina: 9999}, "{Energy 100/5000/9999}"],
]
values.forEach(([energy, expected]) => {
test(`for ${JSON.stringify(energy)} returns "${expected}"`, () => {
expect(energyToString(energy)).toEqual(expected)
})
})
})
describe("isValidEnergy", () => {
const validValues: Energy[] = [
{currentEnergy: 0, currentStamina: 0, maxStamina: 1},
{currentEnergy: 99999, currentStamina: 99999, maxStamina: 99999},
{currentEnergy: 10, currentStamina: 20, maxStamina: 30},
{currentEnergy: 10, currentStamina: 30, maxStamina: 30},
{currentEnergy: 10, currentStamina: 10, maxStamina: 30},
]
validValues.forEach((energy) => {
test(`for ${energyToString(energy)} returns true`, () => {
expect(isValidEnergy(energy)).toEqual(true)
})
})
const invalidValues: Energy[] = [
{currentEnergy: 0, currentStamina: 0, maxStamina: 0},
{currentEnergy: 5, currentStamina: 3, maxStamina: 3},
{currentEnergy: 5, currentStamina: 5, maxStamina: 3},
{currentEnergy: 99999, currentStamina: 99999, maxStamina: 100000},
]
invalidValues.forEach((energy) => {
test(`for ${energyToString(energy)} returns false`, () => {
expect(isValidEnergy(energy)).toEqual(false)
})
})
})
describe("isValidMaxStamina", () => {
const validValues: number[] = [1, 2, 1000, 99999]
validValues.forEach((maxEnergy) => {
test(`for ${maxEnergy} returns true`, () => {
expect(isValidMaxStamina(maxEnergy)).toBe(true)
})
})
const invalidValues: number[] = [0, -1, 100000, 0.5, NaN]
invalidValues.forEach((maxEnergy) => {
test(`for ${maxEnergy} returns false`, () => {
expect(isValidMaxStamina(maxEnergy)).toBe(false)
})
})
})
describe("isValidCurrentEnergy", () => {
const validValues: [number,number][] = [[0, 0], [0, 1], [2, 1000], [99999, 99999]]
validValues.forEach(([value, max]) => {
test(`for ${value}/${max} returns true`, () => {
expect(isValidCurrentEnergy(value, max)).toBe(true)
})
})
const invalidValues: [number,number][] = [[0, -1], [-1, -1], [0.5, 70], [NaN, 20]]
invalidValues.forEach(([value, max]) => {
test(`for ${value}/${max} returns false`, () => {
expect(isValidCurrentEnergy(value, max)).toBe(false)
})
})
})
describe("checkValidEnergy", () => {
test(`returns the input on success`, () => {
const energy = {currentEnergy: 99999, currentStamina: 99999, maxStamina: 99999}
expect(checkValidEnergy(energy)).toStrictEqual(energy)
})
test(`throws on failure`, () => {
expect(() => checkValidEnergy({currentEnergy: 99999, currentStamina: 99999, maxStamina: 100000})).toThrow()
})
})
describe("checkValidMaxStamina", function () {
test(`returns the input on success`, () => {
expect(checkValidMaxStamina(1)).toStrictEqual(1)
})
test("throws on failure", () => {
expect(() => checkValidMaxStamina(NaN)).toThrow()
})
});
describe("checkValidCurrentEnergy", function () {
test(`returns the input on success`, () => {
expect(checkValidCurrentEnergy(25, 50)).toStrictEqual(25)
})
test("throws on failure", () => {
expect(() => checkValidCurrentEnergy(100, 50)).toThrow()
})
});
describe("setMaxStamina", function () {
const invalidValues: [Energy, number][] = [
[InvalidEnergy, 90],
[{maxStamina: 100, currentStamina: 100, currentEnergy: 100}, 0],
]
invalidValues.forEach(([energy, max]) => {
test(`throws for ${energyToString(energy)} -> ${max}`, () => {
expect(() => setMaxStamina(energy, max)).toThrow()
})
})
const values: [Energy, number, Partial<Energy>][] = [
[{maxStamina: 100, currentStamina: 100, currentEnergy: 100}, 100, {}],
[{maxStamina: 100, currentStamina: 100, currentEnergy: 100}, 10, {maxStamina: 10, currentStamina: 10, currentEnergy: 10}],
[{maxStamina: 100, currentStamina: 50, currentEnergy: 10}, 50, {maxStamina: 50}],
[{maxStamina: 100, currentStamina: 50, currentEnergy: 10}, 20, {maxStamina: 20, currentStamina: 20}],
[{maxStamina: 100, currentStamina: 50, currentEnergy: 10}, 10, {maxStamina: 10, currentStamina: 10}],
[{maxStamina: 100, currentStamina: 50, currentEnergy: 10}, 1, {maxStamina: 1, currentStamina: 1, currentEnergy: 1}],
[{maxStamina: 100, currentStamina: 50, currentEnergy: 10}, 150, {maxStamina: 150}],
]
values.forEach(([energy, max, expectedChanges]) => {
const expected = {...energy, ...expectedChanges}
test(`for ${energyToString(energy)} -> ${max} = ${energyToString(expected)}`, () => {
expect(setMaxStamina(energy, max)).toStrictEqual(expected)
})
})
})
describe("damageStamina", () => {
const invalidValues: [Energy, number][] = [
[InvalidEnergy, 90],
[{maxStamina: 100, currentStamina: 100, currentEnergy: 100}, -1],
]
invalidValues.forEach(([energy, damage]) => {
test(`for ${damage} on ${energyToString(energy)} throws`, () => {
expect(() => damageStamina(energy, damage, {damageEnergy: false})).toThrow()
})
})
const values: [Energy, number, {damageEnergy: boolean}, Partial<Energy>][] = [
[{maxStamina: 100, currentStamina: 80, currentEnergy: 60}, 0, {damageEnergy: true}, {}],
[{maxStamina: 100, currentStamina: 80, currentEnergy: 60}, 0, {damageEnergy: false}, {}],
[{maxStamina: 100, currentStamina: 80, currentEnergy: 60}, 19, {damageEnergy: true}, {currentStamina: 61, currentEnergy: 41}],
[{maxStamina: 100, currentStamina: 80, currentEnergy: 60}, 19, {damageEnergy: false}, {currentStamina: 61}],
[{maxStamina: 100, currentStamina: 80, currentEnergy: 60}, 20, {damageEnergy: true}, {currentStamina: 60, currentEnergy: 40}],
[{maxStamina: 100, currentStamina: 80, currentEnergy: 60}, 20, {damageEnergy: false}, {currentStamina: 60}],
[{maxStamina: 100, currentStamina: 80, currentEnergy: 60}, 21, {damageEnergy: true}, {currentStamina: 59, currentEnergy: 39}],
[{maxStamina: 100, currentStamina: 80, currentEnergy: 60}, 21, {damageEnergy: false}, {currentStamina: 59, currentEnergy: 59}],
[{maxStamina: 100, currentStamina: 80, currentEnergy: 60}, 60, {damageEnergy: true}, {currentStamina: 20, currentEnergy: 0}],
[{maxStamina: 100, currentStamina: 80, currentEnergy: 60}, 61, {damageEnergy: true}, {currentStamina: 19, currentEnergy: 0}],
[{maxStamina: 100, currentStamina: 80, currentEnergy: 60}, 80, {damageEnergy: true}, {currentStamina: 0, currentEnergy: 0}],
[{maxStamina: 100, currentStamina: 80, currentEnergy: 60}, 80, {damageEnergy: false}, {currentStamina: 0, currentEnergy: 0}],
[{maxStamina: 100, currentStamina: 80, currentEnergy: 60}, 81, {damageEnergy: false}, {currentStamina: 0, currentEnergy: 0}],
[{maxStamina: 100, currentStamina: 80, currentEnergy: 60}, 100, {damageEnergy: false}, {currentStamina: 0, currentEnergy: 0}],
[{maxStamina: 100, currentStamina: 80, currentEnergy: 60}, 101, {damageEnergy: false}, {currentStamina: 0, currentEnergy: 0}],
]
values.forEach(([energy, damage, options, expectedChanges]) => {
const expected = {...energy, ...expectedChanges}
test(`for ${damage} on ${energyToString(energy)} with damageEnergy ${options.damageEnergy ? "on" : "off"} equals ${energyToString(expected)}`, () => {
expect(damageStamina(energy, damage, options)).toEqual(expected)
})
})
})
describe("recoverStamina", () => {
const invalidValues: [Energy, number][] = [
[InvalidEnergy, 90],
[{maxStamina: 100, currentStamina: 100, currentEnergy: 100}, -1],
]
invalidValues.forEach(([energy, recovery]) => {
test(`for ${recovery} on ${energyToString(energy)} throws`, () => {
expect(() => recoverStamina(energy, recovery, {recoverEnergy: false})).toThrow()
})
})
const values: [Energy, number, {recoverEnergy: boolean}, Partial<Energy>][] = [
[{maxStamina: 100, currentStamina: 80, currentEnergy: 60}, 0, {recoverEnergy: true}, {}],
[{maxStamina: 100, currentStamina: 80, currentEnergy: 60}, 0, {recoverEnergy: false}, {}],
[{maxStamina: 100, currentStamina: 80, currentEnergy: 60}, 19, {recoverEnergy: true}, {currentStamina: 99, currentEnergy: 79}],
[{maxStamina: 100, currentStamina: 80, currentEnergy: 60}, 19, {recoverEnergy: false}, {currentStamina: 99}],
[{maxStamina: 100, currentStamina: 80, currentEnergy: 60}, 20, {recoverEnergy: true}, {currentStamina: 100, currentEnergy: 80}],
[{maxStamina: 100, currentStamina: 80, currentEnergy: 60}, 20, {recoverEnergy: false}, {currentStamina: 100}],
[{maxStamina: 100, currentStamina: 80, currentEnergy: 60}, 21, {recoverEnergy: true}, {currentStamina: 100, currentEnergy: 80}],
[{maxStamina: 100, currentStamina: 80, currentEnergy: 60}, 21, {recoverEnergy: false}, {currentStamina: 100}],
[{maxStamina: 100, currentStamina: 80, currentEnergy: 60}, 101, {recoverEnergy: true}, {currentStamina: 100, currentEnergy: 80}],
[{maxStamina: 100, currentStamina: 80, currentEnergy: 60}, 101, {recoverEnergy: false}, {currentStamina: 100}],
]
values.forEach(([energy, recovery, options, expectedChanges]) => {
const expected = {...energy, ...expectedChanges}
test(`for ${recovery} on ${energyToString(energy)} with recoverEnergy ${options.recoverEnergy ? "on" : "off"} equals ${energyToString(expected)}`, () => {
expect(recoverStamina(energy, recovery, options)).toEqual(expected)
})
})
})
describe("setCurrentStamina", function () {
const invalidValues: [Energy, number][] = [
[InvalidEnergy, 90],
[{maxStamina: 100, currentStamina: 100, currentEnergy: 100}, -1],
[{maxStamina: 105, currentStamina: 100, currentEnergy: 100}, 106],
]
invalidValues.forEach(([energy, value]) => {
test(`throws for ${energyToString(energy)} -> ${value}`, () => {
expect(() => setCurrentStamina(energy, value)).toThrow()
})
})
const values: [Energy, number, Partial<Energy>][] = [
[{maxStamina: 100, currentStamina: 100, currentEnergy: 100}, 100, {}],
[{maxStamina: 100, currentStamina: 100, currentEnergy: 100}, 10, {currentStamina: 10, currentEnergy: 10}],
[{maxStamina: 100, currentStamina: 50, currentEnergy: 10}, 100, {currentStamina: 100}],
[{maxStamina: 100, currentStamina: 50, currentEnergy: 10}, 10, {currentStamina: 10}],
[{maxStamina: 100, currentStamina: 50, currentEnergy: 10}, 9, {currentStamina: 9, currentEnergy: 9}],
[{maxStamina: 100, currentStamina: 50, currentEnergy: 10}, 0, {currentStamina: 0, currentEnergy: 0}],
]
values.forEach(([energy, value, expectedChanges]) => {
const expected = {...energy, ...expectedChanges}
test(`for ${energyToString(energy)} -> ${value} = ${energyToString(expected)}`, () => {
expect(setCurrentStamina(energy, value)).toStrictEqual(expected)
})
})
})
describe("damageEnergy", () => {
const invalidValues: [Energy, number][] = [
[InvalidEnergy, 90],
[{maxStamina: 100, currentStamina: 100, currentEnergy: 100}, -1],
]
invalidValues.forEach(([energy, damage]) => {
test(`for ${damage} on ${energyToString(energy)} throws`, () => {
expect(() => damageEnergy(energy, damage)).toThrow()
})
})
const values: [Energy, number, Partial<Energy>][] = [
[{maxStamina: 100, currentStamina: 80, currentEnergy: 60}, 0, {}],
[{maxStamina: 100, currentStamina: 80, currentEnergy: 60}, 19, {currentEnergy: 41}],
[{maxStamina: 100, currentStamina: 80, currentEnergy: 60}, 20, {currentEnergy: 40}],
[{maxStamina: 100, currentStamina: 80, currentEnergy: 60}, 21, {currentEnergy: 39}],
[{maxStamina: 100, currentStamina: 80, currentEnergy: 60}, 60, {currentEnergy: 0}],
[{maxStamina: 100, currentStamina: 80, currentEnergy: 60}, 61, {currentEnergy: 0}],
[{maxStamina: 100, currentStamina: 80, currentEnergy: 60}, 80, {currentEnergy: 0}],
[{maxStamina: 100, currentStamina: 80, currentEnergy: 60}, 81, {currentEnergy: 0}],
[{maxStamina: 100, currentStamina: 80, currentEnergy: 60}, 100, {currentEnergy: 0}],
[{maxStamina: 100, currentStamina: 80, currentEnergy: 60}, 101, {currentEnergy: 0}],
]
values.forEach(([energy, damage, expectedChanges]) => {
const expected = {...energy, ...expectedChanges}
test(`for ${damage} on ${energyToString(energy)} equals ${energyToString(expected)}`, () => {
expect(damageEnergy(energy, damage)).toEqual(expected)
})
})
})
describe("recoverEnergy", () => {
const invalidValues: [Energy, number][] = [
[InvalidEnergy, 90],
[{maxStamina: 100, currentStamina: 100, currentEnergy: 100}, -1],
]
invalidValues.forEach(([energy, recovery]) => {
test(`for ${recovery} on ${energyToString(energy)} throws`, () => {
expect(() => recoverEnergy(energy, recovery)).toThrow()
})
})
const values: [Energy, number, Partial<Energy>][] = [
[{maxStamina: 100, currentStamina: 80, currentEnergy: 60}, 0, {}],
[{maxStamina: 100, currentStamina: 80, currentEnergy: 60}, 0, {}],
[{maxStamina: 100, currentStamina: 80, currentEnergy: 60}, 19, {currentEnergy: 79}],
[{maxStamina: 100, currentStamina: 80, currentEnergy: 60}, 20, {currentEnergy: 80}],
[{maxStamina: 100, currentStamina: 80, currentEnergy: 60}, 21, {currentEnergy: 80}],
[{maxStamina: 100, currentStamina: 80, currentEnergy: 60}, 101, {currentEnergy: 80}],
]
values.forEach(([energy, recovery, expectedChanges]) => {
const expected = {...energy, ...expectedChanges}
test(`for ${recovery} on ${energyToString(energy)} equals ${energyToString(expected)}`, () => {
expect(recoverEnergy(energy, recovery)).toEqual(expected)
})
})
})
describe("setCurrentEnergy", function () {
const invalidValues: [Energy, number][] = [
[InvalidEnergy, 90],
[{maxStamina: 100, currentStamina: 100, currentEnergy: 100}, -1],
[{maxStamina: 105, currentStamina: 80, currentEnergy: 60}, 106],
[{maxStamina: 105, currentStamina: 80, currentEnergy: 60}, 81],
]
invalidValues.forEach(([energy, value]) => {
test(`throws for ${energyToString(energy)} -> ${value}`, () => {
expect(() => setCurrentEnergy(energy, value)).toThrow()
})
})
const values: [Energy, number, Partial<Energy>][] = [
[{maxStamina: 100, currentStamina: 100, currentEnergy: 100}, 100, {}],
[{maxStamina: 100, currentStamina: 100, currentEnergy: 100}, 10, {currentEnergy: 10}],
[{maxStamina: 100, currentStamina: 50, currentEnergy: 10}, 9, {currentEnergy: 9}],
[{maxStamina: 100, currentStamina: 50, currentEnergy: 10}, 0, {currentEnergy: 0}],
]
values.forEach(([energy, value, expectedChanges]) => {
const expected = {...energy, ...expectedChanges}
test(`for ${energyToString(energy)} -> ${value} = ${energyToString(expected)}`, () => {
expect(setCurrentEnergy(energy, value)).toStrictEqual(expected)
})
})
})

@ -1,161 +0,0 @@
import {assertion} from "../testing/Assertions";
/** The interface managing a character's Stamina and Energy. */
export interface Energy {
/** The character's maximum Stamina, which they are returned to when they rest. >= 1, <= 99999. */
readonly maxStamina: number
/** The character's current Stamina, which their energy returns to when they complete a battle. >= 0, <= maxStamina. */
readonly currentStamina: number
/** The character's current Energy, used for special abilities. >= 0, <= currentStamina. */
readonly currentEnergy: number
}
/** Formats the given Energy object into a string suitable for logging. Should never throw. */
export function energyToString(energy: Energy): string {
return `{Energy ${energy.currentEnergy}/${energy.currentStamina}/${energy.maxStamina}}`
}
/**
* Returns whether the given Energy object is valid, i.e., it has currentEnergy <= currentStamina <= maxStamina, and
* all three are positive integers or zero.
*/
export function isValidEnergy(energy: Energy): boolean {
return isValidMaxStamina(energy.maxStamina)
&& isValidCurrentEnergy(energy.currentStamina, energy.maxStamina)
&& isValidCurrentEnergy(energy.currentEnergy, energy.currentStamina)
}
/** Returns whether the given value is valid for max stamina, i.e., it's an integer between 1 and 99999. */
export function isValidMaxStamina(maxStamina: number): boolean {
return Number.isSafeInteger(maxStamina) && maxStamina >= 1 && maxStamina <= 99999
}
/** Returns whether the given value is valid for current energy or stamina, i.e., it's an integer between 0 and max. */
export function isValidCurrentEnergy(value: number, max: number): boolean {
return Number.isSafeInteger(value) && value >= 0 && value <= max
}
/** Asserts that the given Energy object is valid (see the documentation for isValidEnergy). */
export function checkValidEnergy(energy: Energy): Energy {
return assertion.check(energy, isValidEnergy, (energy) => `Invalid energy: ${energyToString(energy)}`)
}
/** Asserts that the given value is valid for maxStamina. */
export function checkValidMaxStamina(maxStamina: number): number {
return assertion.check(maxStamina, isValidMaxStamina, (maxStamina) => `Invalid max stamina: ${maxStamina}`)
}
/** Asserts that the given value is valid for currentStamina or currentEnergy. */
export function checkValidCurrentEnergy(value: number, max: number): number {
return assertion.check(value, (value) => isValidCurrentEnergy(value, max), (value) => `Invalid current energy: ${value}`)
}
/**
* Sets the max stamina value on the given Energy. This may result in the currentStamina (and possibly currentEnergy)
* values decreasing to fit under the new ceiling.
*/
export function setMaxStamina(energy: Energy, maxStamina: number): Energy {
checkValidEnergy(energy)
checkValidMaxStamina(maxStamina)
const currentStamina = Math.min(energy.currentStamina, maxStamina)
const currentEnergy = Math.min(energy.currentEnergy, currentStamina)
return checkValidEnergy({...energy, maxStamina, currentStamina, currentEnergy})
}
/**
* Deals damage to the Stamina of the given Energy. currentStamina will be decreased; currentEnergy will also be
* decreased if damageEnergy is true, or if the new Stamina is lower than the Energy. Neither can drop below 0 this way.
*/
export function damageStamina(energy: Energy, damage: number, options: {damageEnergy: boolean}): Energy {
checkValidEnergy(energy)
assertion.checkPositiveIntegerOrZero(damage, () => `Invalid damage: ${damage}`)
const currentStamina = Math.max(0, energy.currentStamina - damage)
const currentEnergy = options.damageEnergy ? Math.max(0, energy.currentEnergy - damage) : Math.min(energy.currentEnergy, currentStamina)
return checkValidEnergy({
...energy,
currentStamina,
currentEnergy,
})
}
/**
* Recovers the Stamina of the given Energy. currentStamina will be increased; currentEnergy will also be increased
* if recoverEnergy is true. Neither can exceed maxStamina this way.
*/
export function recoverStamina(energy: Energy, recovery: number, options: {recoverEnergy: boolean}): Energy {
checkValidEnergy(energy)
assertion.checkPositiveIntegerOrZero(recovery, () => `Invalid healing: ${recovery}`)
const currentStamina = Math.min(energy.maxStamina, energy.currentStamina + recovery)
const currentEnergy = options.recoverEnergy ? energy.currentEnergy + (currentStamina - energy.currentStamina) : energy.currentEnergy
return checkValidEnergy({
...energy,
currentStamina,
currentEnergy,
})
}
/**
* Sets the Stamina of the given Energy. currentEnergy will be decreased if it's lower than the new Stamina.
*/
export function setCurrentStamina(energy: Energy, value: number): Energy {
checkValidEnergy(energy)
checkValidCurrentEnergy(value, energy.maxStamina)
const currentStamina = value
const currentEnergy = Math.min(currentStamina, energy.currentEnergy)
return checkValidEnergy({
...energy,
currentStamina,
currentEnergy
})
}
/**
* Deals damage to the Energy of the given Energy. It can't drop below 0 this way.
*/
export function damageEnergy(energy: Energy, damage: number): Energy {
checkValidEnergy(energy)
assertion.checkPositiveIntegerOrZero(damage, () => `Invalid damage: ${damage}`)
const currentEnergy = Math.max(0, energy.currentEnergy - damage)
return checkValidEnergy({
...energy,
currentEnergy,
})
}
/**
* Recovers the Energy of the given Energy. It can't exceed currentStamina this way.
*/
export function recoverEnergy(energy: Energy, recovery: number): Energy {
checkValidEnergy(energy)
assertion.checkPositiveIntegerOrZero(recovery, () => `Invalid healing: ${recovery}`)
const currentEnergy = Math.min(energy.currentStamina, energy.currentEnergy + recovery)
return checkValidEnergy({
...energy,
currentEnergy
})
}
/**
* Sets the current Energy of the given Energy.
*/
export function setCurrentEnergy(energy: Energy, value: number): Energy {
checkValidEnergy(energy)
checkValidCurrentEnergy(value, energy.currentStamina)
return checkValidEnergy({
...energy,
currentEnergy: value
})
}

@ -1,558 +0,0 @@
import {
checkValidConfidenceLimit,
checkValidCurrentHitPoints,
checkValidHitPoints,
checkValidHitPointsWithoutConfidence,
checkValidMaxHitPoints,
confidenceLimit,
damageConfidence,
damageHealth,
HitPoints,
HitPointsWithoutConfidence,
hitPointsWithoutConfidenceToString,
hitPointsToString, isValidConfidenceLimit,
isValidCurrentHitPoints,
isValidHitPoints,
isValidHitPointsWithoutConfidence,
isValidMaxHitPoints,
recoverConfidence,
recoverHealth, setCurrentConfidence,
setCurrentHealth,
setMaxConfidence,
setMaxHealth, checkConsistentConfidenceDelta
} from "./HitPoints";
import {assertion, AssertionConfig} from "../testing/Assertions";
const InvalidHitPoints = {currentHealth: 999, maxHealth: 99, maxConfidence: 100, currentConfidence: 20};
let originalConfig: AssertionConfig.Type
beforeEach(() => {
originalConfig = assertion.config
assertion.config = AssertionConfig.Throw
});
afterEach(() => {
assertion.config = originalConfig
});
describe("hitPointsWithoutConfidenceToString", () => {
const pairs: [HitPointsWithoutConfidence, string][] = [
[{currentHealth: 99, maxHealth: 100, maxConfidence: 200}, "{Health: 99/100, Confidence: -/-/200}"],
[{currentHealth: -25, maxHealth: 0, maxConfidence: -200}, "{Health: -25/0, Confidence: -/-/-200}"],
]
pairs.forEach(([hp, expected]) => {
test(`for ${JSON.stringify(hp)} = "${expected}"`, () => {
expect(hitPointsWithoutConfidenceToString(hp)).toStrictEqual(expected)
})
})
});
describe("hitPointsToString", () => {
const pairs: [HitPoints, string][] = [
[{currentHealth: 99, maxHealth: 100, maxConfidence: 200, currentConfidence: 10}, "{Health: 99/100, Confidence: 10/198/200}"],
[{currentHealth: -25, maxHealth: 0, maxConfidence: -200, currentConfidence: -15}, "{Health: -25/0, Confidence: -15/<err>/-200}"],
]
pairs.forEach(([hp, expected]) => {
test(`for ${JSON.stringify(hp)} = "${expected}"`, () => {
expect(hitPointsToString(hp)).toStrictEqual(expected)
})
})
});
describe("isValidMaxHitPoints", () => {
const validValues = [1, 2, 99999]
const invalidValues = [0, -1, 1.5, NaN, Infinity, -Infinity, 100000, 999999]
validValues.forEach((value) => {
test(`returns true for ${value}`, () => {
expect(isValidMaxHitPoints(value)).toBe(true)
})
})
invalidValues.forEach((value) => {
test(`returns false for ${value}`, () => {
expect(isValidMaxHitPoints(value)).toBe(false)
})
})
});
describe("isValidCurrentHitPoints", () => {
const validValues: [number, number, number][] = [[1, 1, 0], [-1, 1, -1], [0, 1, 0], [0, 1, -1], [50, 99999, 0]]
const invalidValues: [number, number, number][] = [[2, 1, 0], [-1, 1, 0], [0.5, 1, 0], [-2, 1, -1]]
validValues.forEach(([current, max, min]) => {
test(`returns true for ${current} between ${min} and ${max}`, () => {
expect(isValidCurrentHitPoints(current, max, min)).toBe(true)
})
})
invalidValues.forEach(([current, max, min]) => {
test(`returns false for ${current} between ${min} and ${max}`, () => {
expect(isValidCurrentHitPoints(current, max, min)).toBe(false)
})
})
});
describe("isValidHitPointsWithoutConfidence", () => {
const validValues: HitPointsWithoutConfidence[] = [{
currentHealth: 100,
maxHealth: 100,
maxConfidence: 100,
}, {
currentHealth: 50,
maxHealth: 100,
maxConfidence: 25
}, {
currentHealth: 0,
maxHealth: 1,
maxConfidence: 1,
}]
const invalidValues: HitPointsWithoutConfidence[] = [{
currentHealth: 0,
maxHealth: 0,
maxConfidence: 99,
}, {
currentHealth: 0,
maxHealth: 20,
maxConfidence: 0,
}, {
currentHealth: -300,
maxHealth: 100,
maxConfidence: 99,
}]
validValues.forEach((value) => {
test(`returns true for ${hitPointsWithoutConfidenceToString(value)}`, () => {
expect(isValidHitPointsWithoutConfidence(value)).toBe(true)
})
})
invalidValues.forEach((value) => {
test(`returns false for ${hitPointsWithoutConfidenceToString(value)}`, () => {
expect(isValidHitPointsWithoutConfidence(value)).toBe(false)
})
})
})
describe("isValidHitPoints", () => {
const validValues: HitPoints[] = [{
currentHealth: 100,
maxHealth: 100,
currentConfidence: 100,
maxConfidence: 100,
}, {
currentHealth: 50,
maxHealth: 100,
currentConfidence: 30,
maxConfidence: 60,
}, {
currentHealth: 0,
maxHealth: 100,
currentConfidence: 0,
maxConfidence: 30,
}]
const invalidValues: HitPoints[] = [{
currentHealth: 11,
maxHealth: 10,
currentConfidence: 99,
maxConfidence: 99,
}, {
currentHealth: 10,
maxHealth: 20,
currentConfidence: 20,
maxConfidence: 20,
}, {
currentHealth: 20,
maxHealth: 20,
currentConfidence: 25,
maxConfidence: 20,
}]
validValues.forEach((value) => {
test(`returns true for ${hitPointsToString(value)}`, () => {
expect(isValidHitPoints(value)).toBe(true)
})
})
invalidValues.forEach((value) => {
test(`returns false for ${hitPointsToString(value)}`, () => {
expect(isValidHitPoints(value)).toBe(false)
})
})
})
describe("isValidConfidenceLimit", () => {
const validValues: [number, HitPointsWithoutConfidence][] = [
[200, {
currentHealth: 100,
maxHealth: 100,
maxConfidence: 200,
}], [0, {
currentHealth: 50,
maxHealth: 100,
maxConfidence: 60
}], [10, {
currentHealth: 50,
maxHealth: 100,
maxConfidence: 60
}], [0, {
currentHealth: 0,
maxHealth: 100,
maxConfidence: 100,
}]]
const invalidValues: [number, HitPointsWithoutConfidence][] = [
[195, {
currentHealth: 100,
maxHealth: 100,
maxConfidence: 200,
}], [25, {
currentHealth: 0,
maxHealth: 100,
maxConfidence: 100,
}]]
validValues.forEach(([value, hp]) => {
test(`returns true for ${value} and ${hitPointsWithoutConfidenceToString(hp)}`, () => {
expect(isValidConfidenceLimit(value, hp)).toBe(true)
})
})
invalidValues.forEach(([value, hp]) => {
test(`returns false for ${value} and ${hitPointsWithoutConfidenceToString(hp)}`, () => {
expect(isValidConfidenceLimit(value, hp)).toBe(false)
})
})
})
describe("checkValidMaxHitPoints", () => {
test("returns on valid max HP", () => {
expect(checkValidMaxHitPoints(30)).toEqual(30)
})
test("throws on invalid max HP", () => {
expect(() => checkValidMaxHitPoints(-1)).toThrow()
})
})
describe("checkValidCurrentHitPoints", () => {
test("returns on valid current HP", () => {
expect(checkValidCurrentHitPoints(30, 30, 0)).toEqual(30)
})
test("throws on invalid current HP", () => {
expect(() => checkValidCurrentHitPoints(-10, 30, 0)).toThrow()
})
})
describe("checkValidHitPointsWithoutConfidence", () => {
test("returns on valid hit points base", () => {
expect(checkValidHitPointsWithoutConfidence({currentHealth: 30, maxHealth: 40, maxConfidence: 99})).toStrictEqual({currentHealth: 30, maxHealth: 40, maxConfidence: 99})
})
test("throws on invalid hit points base", () => {
expect(() => checkValidHitPointsWithoutConfidence({currentHealth: -20, maxHealth: 10, maxConfidence: 30})).toThrow()
})
})
describe("checkValidHitPoints", () => {
test("returns on valid hit points", () => {
expect(checkValidHitPoints({currentHealth: 30, maxHealth: 40, maxConfidence: 99, currentConfidence: 30})).toStrictEqual({currentHealth: 30, maxHealth: 40, maxConfidence: 99, currentConfidence: 30})
})
test("throws on invalid hit points", () => {
expect(() => checkValidHitPoints({currentHealth: -20, maxHealth: 10, maxConfidence: 30, currentConfidence: 50})).toThrow()
})
})
describe("checkValidConfidenceLimit", () => {
test("returns on valid confidence limit", () => {
expect(checkValidConfidenceLimit(30, {currentHealth: 15, maxHealth: 20, maxConfidence: 40})).toEqual(30)
})
test("throws on invalid confidence limit", () => {
expect(() => checkValidConfidenceLimit(40, {currentHealth: 15, maxHealth: 20, maxConfidence: 40})).toThrow()
})
})
describe("checkConsistentConfidenceDelta", () => {
const consistentValues: [HitPoints, number, number][] = [
[{currentHealth: 30, maxHealth: 30, currentConfidence: 10, maxConfidence: 20}, -10, 0],
[{currentHealth: 30, maxHealth: 30, currentConfidence: 20, maxConfidence: 20}, 0, 0],
[{currentHealth: 30, maxHealth: 30, currentConfidence: 20, maxConfidence: 20}, 10, 15],
[{currentHealth: 7, maxHealth: 10, currentConfidence: 17, maxConfidence: 25}, 10, 4],
[{currentHealth: 7, maxHealth: 10, currentConfidence: 17, maxConfidence: 25}, 9, 4],
[{currentHealth: 7, maxHealth: 10, currentConfidence: 17, maxConfidence: 25}, 25, 10],
[{currentHealth: 7, maxHealth: 10, currentConfidence: 17, maxConfidence: 25}, 24, 10],
]
consistentValues.forEach(([hp, confidence, health]) => {
const newHealth = {...hp, currentConfidence: confidence, currentHealth: health}
test(`returns for ${hitPointsToString(hp)} -> ${hitPointsToString(newHealth)}`, () => {
expect(checkConsistentConfidenceDelta(confidence, "testing", hp, health)).toEqual(confidence)
})
})
const inconsistentValues: [HitPoints, number, number][] = [
[{currentHealth: 30, maxHealth: 30, currentConfidence: 10, maxConfidence: 20}, 0, 0],
[{currentHealth: 30, maxHealth: 30, currentConfidence: 20, maxConfidence: 20}, 20, 0],
[{currentHealth: 30, maxHealth: 30, currentConfidence: 20, maxConfidence: 20}, 5, 15],
[{currentHealth: 7, maxHealth: 10, currentConfidence: 10, maxConfidence: 25}, 8, 4],
[{currentHealth: 7, maxHealth: 10, currentConfidence: 17, maxConfidence: 25}, 23, 10],
]
inconsistentValues.forEach(([hp, confidence, health]) => {
const newHealth = {...hp, currentConfidence: confidence, currentHealth: health}
test(`throws for ${hitPointsToString(hp)} -> ${hitPointsToString(newHealth)}`, () => {
expect(() => checkConsistentConfidenceDelta(confidence, "testing", hp, health)).toThrow()
})
})
test("throws an error starting with the evaluation of the action function", () => {
expect(() => checkConsistentConfidenceDelta(
30, "frozzing the snagler",
{maxHealth: 10, currentHealth: 0, maxConfidence: 30, currentConfidence: 0},
9
)).toThrow(/^frozzing the snagler /)
})
})
describe("confidenceLimit", () => {
test("throws on invalid input", () => {
expect(() => confidenceLimit({ currentHealth: -99, maxHealth: 30, maxConfidence: 20})).toThrow()
})
const values: [HitPointsWithoutConfidence, number][] = [
[{maxConfidence: 99, maxHealth: 30, currentHealth: 30}, 99],
// Even 1 missing health results in a loss of at least 1 confidence.
[{maxConfidence: 10, maxHealth: 99999, currentHealth: 99998}, 9],
// Confidence is 0 when each point of confidence represents multiple points of health and there aren't enough to make 1 point of confidence
[{maxConfidence: 10, maxHealth: 50, currentHealth: 4}, 0],
// Confidence is 0 when health is 0
[{maxConfidence: 99999, maxHealth: 99, currentHealth: 0}, 0],
// or when it's below 0
[{maxConfidence: 99999, maxHealth: 99, currentHealth: -50}, 0],
]
values.forEach(([hp, expected]) => {
test(`returns ${expected} for ${hitPointsWithoutConfidenceToString(hp)}`, () => {
expect(confidenceLimit(hp)).toBe(expected)
})
})
})
describe("damageHealth", () => {
const invalidValues: [HitPoints, number][] = [
[InvalidHitPoints, 10],
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 10}, -1],
]
invalidValues.forEach(([hp, damage]) => {
test(`for ${damage} on ${hitPointsToString(hp)} throws`, () => {
expect(() => damageHealth(hp, damage, {damageConfidence: false})).toThrow()
})
})
const values: [[HitPoints, number, {damageConfidence: boolean}?], Partial<HitPoints>][] = [
[[{currentHealth: -10, maxHealth: 10, maxConfidence: 10, currentConfidence: 0}, 0], {}],
[[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 0}, 0], {}],
[[{currentHealth: -5, maxHealth: 10, maxConfidence: 10, currentConfidence: 0}, 5], {currentHealth: -10}],
[[{currentHealth: -5, maxHealth: 10, maxConfidence: 10, currentConfidence: 0}, 10], {currentHealth: -10}],
[[{currentHealth: 0, maxHealth: 10, maxConfidence: 10, currentConfidence: 0}, 10], {currentHealth: -10}],
[[{currentHealth: 0, maxHealth: 10, maxConfidence: 10, currentConfidence: 0}, 15], {currentHealth: -10}],
[[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 0}, 15], {currentHealth: -5}],
[[{currentHealth: 10, maxHealth: 10, maxConfidence: 20, currentConfidence: 18}, 5, {damageConfidence: true}], {currentHealth: 5, currentConfidence: 8}],
[[{currentHealth: 10, maxHealth: 10, maxConfidence: 20, currentConfidence: 18}, 5, {damageConfidence: false}], {currentHealth: 5, currentConfidence: 10}],
[[{currentHealth: 10, maxHealth: 10, maxConfidence: 20, currentConfidence: 18}, 15, {damageConfidence: true}], {currentHealth: -5, currentConfidence: 0}],
[[{currentHealth: 10, maxHealth: 10, maxConfidence: 20, currentConfidence: 18}, 15, {damageConfidence: false}], {currentHealth: -5, currentConfidence: 0}],
[[{currentHealth: 100, maxHealth: 100, maxConfidence: 20, currentConfidence: 18}, 7, {damageConfidence: true}], {currentHealth: 93, currentConfidence: 16}],
[[{currentHealth: 100, maxHealth: 100, maxConfidence: 20, currentConfidence: 18}, 7, {damageConfidence: false}], {currentHealth: 93, currentConfidence: 18}],
[[{currentHealth: 93, maxHealth: 100, maxConfidence: 20, currentConfidence: 18}, 2, {damageConfidence: true}], {currentHealth: 91, currentConfidence: 17}],
[[{currentHealth: 93, maxHealth: 100, maxConfidence: 20, currentConfidence: 18}, 2, {damageConfidence: false}], {currentHealth: 91, currentConfidence: 18}],
]
values.forEach(([[hp, damage, options = {damageConfidence: true}], expectedChanges]) => {
const expected = {...hp, ...expectedChanges}
test(`for ${damage} on ${hitPointsToString(hp)} with damageConfidence ${options.damageConfidence ? "on" : "off"} results in ${hitPointsToString(expected)}`, () => {
expect(damageHealth(hp, damage, options)).toStrictEqual(expected)
})
})
})
describe("recoverHealth", () => {
const invalidValues: [HitPoints, number][] = [
[InvalidHitPoints, 10],
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 10}, -1],
]
invalidValues.forEach(([hp, healing]) => {
test(`for ${healing} on ${hitPointsToString(hp)} throws`, () => {
expect(() => recoverHealth(hp, healing, {recoverConfidence: false})).toThrow()
})
})
const values: [[HitPoints, number, {recoverConfidence: boolean}?], Partial<HitPoints>][] = [
[[{currentHealth: -10, maxHealth: 10, maxConfidence: 10, currentConfidence: 0}, 0], {}],
[[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 0}, 0], {}],
[[{currentHealth: -5, maxHealth: 10, maxConfidence: 10, currentConfidence: 0}, 5], {currentHealth: 0}],
[[{currentHealth: -10, maxHealth: 10, maxConfidence: 10, currentConfidence: 0}, 10], {currentHealth: 0}],
[[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 0}, 10], {}],
[[{currentHealth: 7, maxHealth: 10, maxConfidence: 20, currentConfidence: 8}, 2, {recoverConfidence: true}], {currentHealth: 9, currentConfidence: 12}],
[[{currentHealth: 7, maxHealth: 10, maxConfidence: 20, currentConfidence: 8}, 2, {recoverConfidence: false}], {currentHealth: 9, currentConfidence: 8}],
[[{currentHealth: 7, maxHealth: 10, maxConfidence: 20, currentConfidence: 8}, 5, {recoverConfidence: true}], {currentHealth: 10, currentConfidence: 14}],
[[{currentHealth: 7, maxHealth: 10, maxConfidence: 20, currentConfidence: 8}, 5, {recoverConfidence: false}], {currentHealth: 10, currentConfidence: 8}],
[[{currentHealth: -10, maxHealth: 10, maxConfidence: 20, currentConfidence: 0}, 15, {recoverConfidence: true}], {currentHealth: 5, currentConfidence: 10}],
[[{currentHealth: -10, maxHealth: 10, maxConfidence: 20, currentConfidence: 0}, 15, {recoverConfidence: false}], {currentHealth: 5, currentConfidence: 0}],
[[{currentHealth: 0, maxHealth: 10, maxConfidence: 25, currentConfidence: 0}, 3, {recoverConfidence: true}], {currentHealth: 3, currentConfidence: 7}],
[[{currentHealth: 3, maxHealth: 10, maxConfidence: 25, currentConfidence: 7}, 3, {recoverConfidence: true}], {currentHealth: 6, currentConfidence: 14}],
[[{currentHealth: 0, maxHealth: 10, maxConfidence: 25, currentConfidence: 0}, 3, {recoverConfidence: false}], {currentHealth: 3, currentConfidence: 0}],
]
values.forEach(([[hp, damage, options = {recoverConfidence: true}], expectedChanges]) => {
const expected = {...hp, ...expectedChanges}
test(`for ${damage} on ${hitPointsToString(hp)} with recoverConfidence ${options.recoverConfidence ? "on" : "off"} results in ${hitPointsToString(expected)}`, () => {
expect(recoverHealth(hp, damage, options)).toStrictEqual(expected)
})
})
})
describe("setMaxHealth", () => {
const invalidValues: [HitPoints, number][] = [
[InvalidHitPoints, 10],
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 5}, 0],
]
invalidValues.forEach(([hp, newMax]) => {
test(`to ${newMax} on ${hitPointsToString(hp)} throws`, () => {
expect(() => setMaxHealth(hp, newMax)).toThrow()
})
})
const values: [HitPoints, number, Partial<HitPoints>][] = [
[{currentHealth: 8, maxHealth: 10, maxConfidence: 10, currentConfidence: 5}, 10, {}],
[{currentHealth: 8, maxHealth: 10, maxConfidence: 10, currentConfidence: 5}, 5, {maxHealth: 5, currentHealth: 5}],
[{currentHealth: 8, maxHealth: 10, maxConfidence: 10, currentConfidence: 4}, 20, {maxHealth: 20}],
[{currentHealth: 8, maxHealth: 10, maxConfidence: 10, currentConfidence: 4}, 19, {maxHealth: 19}],
[{currentHealth: 8, maxHealth: 10, maxConfidence: 10, currentConfidence: 4}, 21, {maxHealth: 21, currentConfidence: 3}],
]
values.forEach(([hp, newMax, expectedChanges]) => {
const expected = {...hp, ...expectedChanges}
test(`to ${newMax} on ${hitPointsToString(hp)} results in ${hitPointsToString(expected)}`, () => {
expect(setMaxHealth(hp, newMax)).toStrictEqual(expected)
})
})
})
describe("setCurrentHealth", () => {
const invalidValues: [HitPoints, number][] = [
[InvalidHitPoints, 10],
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 5}, -11],
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 5}, 11],
]
invalidValues.forEach(([hp, newValue]) => {
test(`to ${newValue} on ${hitPointsToString(hp)} throws`, () => {
expect(() => setCurrentHealth(hp, newValue)).toThrow()
})
})
const values: [HitPoints, number, Partial<HitPoints>][] = [
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 0}, 0, {currentHealth: 0}],
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 0}, 10, {}],
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 0}, 5, {currentHealth: 5}],
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 0}, 10, {currentHealth: 10}],
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 0}, -5, {currentHealth: -5}],
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 0}, -10, {currentHealth: -10}],
[{currentHealth: 4, maxHealth: 10, maxConfidence: 20, currentConfidence: 5}, 0, {currentHealth: 0, currentConfidence: 0}],
[{currentHealth: 4, maxHealth: 10, maxConfidence: 20, currentConfidence: 5}, 8, {currentHealth: 8}],
]
values.forEach(([hp, newValue, expectedChanges]) => {
const expected = {...hp, ...expectedChanges}
test(`to ${newValue} on ${hitPointsToString(hp)} results in ${hitPointsToString(expected)}`, () => {
expect(setCurrentHealth(hp, newValue)).toStrictEqual(expected)
})
})
})
describe("damageConfidence", () => {
const invalidValues: [HitPoints, number][] = [
[InvalidHitPoints, 10],
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 5}, -1],
]
invalidValues.forEach(([hp, damage]) => {
test(`for ${damage} on ${hitPointsToString(hp)} throws`, () => {
expect(() => damageConfidence(hp, damage)).toThrow()
})
})
const values: [HitPoints, number, Partial<HitPoints>][] = [
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 5}, 0, {}],
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 0}, 10, {}],
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 5}, 6, {currentConfidence: 0}],
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 5}, 5, {currentConfidence: 0}],
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 5}, 4, {currentConfidence: 1}],
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 10}, 9, {currentConfidence: 1}],
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 10}, 11, {currentConfidence: 0}],
]
values.forEach(([hp, damage, expectedChanges]) => {
const expected = {...hp, ...expectedChanges}
test(`for ${damage} on ${hitPointsToString(hp)} results in ${hitPointsToString(expected)}`, () => {
expect(damageConfidence(hp, damage)).toStrictEqual(expected)
})
})
})
describe("recoverConfidence", () => {
const invalidValues: [HitPoints, number][] = [
[InvalidHitPoints, 10],
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 5}, -1],
]
invalidValues.forEach(([hp, healing]) => {
test(`for ${healing} on ${hitPointsToString(hp)} throws`, () => {
expect(() => recoverConfidence(hp, healing)).toThrow()
})
})
const values: [HitPoints, number, Partial<HitPoints>][] = [
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 5}, 0, {}],
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 10}, 10, {}],
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 5}, 6, {currentConfidence: 10}],
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 5}, 5, {currentConfidence: 10}],
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 5}, 4, {currentConfidence: 9}],
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 0}, 9, {currentConfidence: 9}],
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 0}, 11, {currentConfidence: 10}],
[{currentHealth: 5, maxHealth: 10, maxConfidence: 20, currentConfidence: 5}, 6, {currentConfidence: 10}],
[{currentHealth: 5, maxHealth: 10, maxConfidence: 20, currentConfidence: 5}, 5, {currentConfidence: 10}],
[{currentHealth: 5, maxHealth: 10, maxConfidence: 20, currentConfidence: 5}, 4, {currentConfidence: 9}],
]
values.forEach(([hp, healing, expectedChanges]) => {
const expected = {...hp, ...expectedChanges}
test(`for ${healing} on ${hitPointsToString(hp)} results in ${hitPointsToString(expected)}`, () => {
expect(recoverConfidence(hp, healing)).toStrictEqual(expected)
})
})
})
describe("setMaxConfidence", () => {
const invalidValues: [HitPoints, number][] = [
[InvalidHitPoints, 10],
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 5}, 0],
]
invalidValues.forEach(([hp, newMax]) => {
test(`to ${newMax} on ${hitPointsToString(hp)} throws`, () => {
expect(() => setMaxConfidence(hp, newMax)).toThrow()
})
})
const values: [HitPoints, number, Partial<HitPoints>][] = [
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 0}, 10, {}],
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 5}, 10, {}],
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 10}, 10, {}],
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 5}, 6, {maxConfidence: 6}],
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 5}, 5, {maxConfidence: 5}],
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 5}, 4, {maxConfidence: 4, currentConfidence: 4}],
[{currentHealth: 4, maxHealth: 10, maxConfidence: 20, currentConfidence: 5}, 30, {maxConfidence: 30}],
[{currentHealth: 4, maxHealth: 10, maxConfidence: 20, currentConfidence: 5}, 5, {maxConfidence: 5, currentConfidence: 2}],
]
values.forEach(([hp, newMax, expectedChanges]) => {
const expected = {...hp, ...expectedChanges}
test(`to ${newMax} on ${hitPointsToString(hp)} results in ${hitPointsToString(expected)}`, () => {
expect(setMaxConfidence(hp, newMax)).toStrictEqual(expected)
})
})
})
describe("setCurrentConfidence", () => {
const invalidValues: [HitPoints, number][] = [
[InvalidHitPoints, 10],
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 5}, -1],
[{currentHealth: 5, maxHealth: 10, maxConfidence: 10, currentConfidence: 5}, 6],
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 5}, 11],
]
invalidValues.forEach(([hp, newValue]) => {
test(`to ${newValue} on ${hitPointsToString(hp)} throws`, () => {
expect(() => setCurrentConfidence(hp, newValue)).toThrow()
})
})
const values: [HitPoints, number, Partial<HitPoints>][] = [
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 0}, 0, {}],
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 0}, 5, {currentConfidence: 5}],
[{currentHealth: 10, maxHealth: 10, maxConfidence: 10, currentConfidence: 0}, 10, {currentConfidence: 10}],
[{currentHealth: 4, maxHealth: 10, maxConfidence: 20, currentConfidence: 5}, 0, {currentConfidence: 0}],
[{currentHealth: 4, maxHealth: 10, maxConfidence: 20, currentConfidence: 5}, 8, {currentConfidence: 8}],
]
values.forEach(([hp, newValue, expectedChanges]) => {
const expected = {...hp, ...expectedChanges}
test(`to ${newValue} on ${hitPointsToString(hp)} results in ${hitPointsToString(expected)}`, () => {
expect(setCurrentConfidence(hp, newValue)).toStrictEqual(expected)
})
})
})

@ -1,314 +0,0 @@
import {assertion} from "../testing/Assertions";
/**
* HitPointsWithoutConfidence contains the hit points data without the current confidence as is needed for calculating
* the confidence limit itself. It's impossible to check whether a hit points object is valid without checking its
* confidence limit, and when calculating said confidence limit, this would lead to endless recursion.
*/
export type HitPointsWithoutConfidence = Omit<HitPoints, "currentConfidence">
/** Formats the HitPointsWithoutConfidence object into a human-readable string for logging. Should never throw. */
export function hitPointsWithoutConfidenceToString(hp: HitPointsWithoutConfidence): string {
return `{Health: ${hp.currentHealth}/${hp.maxHealth}, Confidence: -/-/${hp.maxConfidence}}`
}
/**
* The full data for a character's hit points - i.e., their counters that limit their survival, and which are primarily
* impacted by the opponent's attacks.
*/
export interface HitPoints {
/** The maximum Health the character can have, which they recover to when resting. Valid values are integers between 1 and 99999. */
readonly maxHealth: number
/** The current Health the character has, after any damage taken since the last rest. Valid values are integers between -maxHealth and maxHealth. */
readonly currentHealth: number
/**
* The maximum Confidence the character can have when maxHealth = currentHealth.
* Below this, the confidence limit is scaled down proportionately.
* Valid values are integers between 1 and 99999.
*/
readonly maxConfidence: number
/** The current Confidence the character has, after any damage taken since the last victorious battle. Between 0 and confidenceLimit(this). */
readonly currentConfidence: number
}
/** Formats the HitPoints object into a human-readable string for logging. Should never throw. */
export function hitPointsToString(hp: HitPoints): string {
let confidenceLimitString: string
try {
confidenceLimitString = confidenceLimit(hp).toString()
} catch (e) {
confidenceLimitString = "<err>"
}
return `{Health: ${hp.currentHealth}/${hp.maxHealth}, Confidence: ${hp.currentConfidence}/${confidenceLimitString}/${hp.maxConfidence}}`
}
/** Returns true if the value given is valid for maxHealth or maxConfidence. See their documentation for what that means. */
export function isValidMaxHitPoints(maxHitPoints: number): boolean {
return maxHitPoints > 0 && maxHitPoints <= 99999 && Number.isSafeInteger(maxHitPoints)
}
/** Returns true if the value given is valid for currentHealth or currentConfidence. See their documentation for what that means. */
export function isValidCurrentHitPoints(currentHitPoints: number, maxHitPoints: number, minHitPoints: number) {
return currentHitPoints <= maxHitPoints && currentHitPoints >= minHitPoints && Number.isSafeInteger(currentHitPoints)
}
/** Returns true if the given HitPointsWithoutConfidence object lives up to all its requirements. See the HitPoints documentation for what that means. */
export function isValidHitPointsWithoutConfidence(hp: HitPointsWithoutConfidence): boolean {
return isValidMaxHitPoints(hp.maxHealth)
&& isValidMaxHitPoints(hp.maxConfidence)
&& isValidCurrentHitPoints(hp.currentHealth, hp.maxHealth, -hp.maxHealth)
}
/** Returns true if the given HitPoints object lives up to all its requirements. See the HitPoints documentation for what that means. */
export function isValidHitPoints(hp: HitPoints): boolean {
return isValidHitPointsWithoutConfidence(hp)
&& isValidCurrentHitPoints(hp.currentConfidence, confidenceLimit(hp), 0)
}
/** Returns true if the given confidenceLimit value is at least plausible. See the confidenceLimit documentation for what that means.*/
export function isValidConfidenceLimit(limit: number, hp: HitPointsWithoutConfidence): boolean {
return (hp.currentHealth > 0 ? limit >= 0 : limit === 0)
&& (hp.currentHealth === hp.maxHealth ? limit === hp.maxConfidence : limit < hp.maxConfidence)
&& Number.isSafeInteger(limit)
}
/** Asserts that the given value is valid for maxHealth or maxConfidence. */
export function checkValidMaxHitPoints(maxHitPoints: number): number {
return assertion.check(maxHitPoints, isValidMaxHitPoints, (maxHitPoints) => `Invalid max hit points: ${maxHitPoints}`)
}
/** Asserts that the given value is valid for currentHealth or currentConfidence. */
export function checkValidCurrentHitPoints(currentHitPoints: number, maxHitPoints: number, minHitPoints: number): number {
return assertion.check(currentHitPoints,
(currentHitPoints) => isValidCurrentHitPoints(currentHitPoints, maxHitPoints, minHitPoints),
(currentHitPoints) => `Invalid current hit points: ${minHitPoints} <= ${currentHitPoints} <= ${maxHitPoints}`)
}
/** Asserts that the given HitPointsWithoutConfidence object lives up to all its requirements. */
export function checkValidHitPointsWithoutConfidence(hp: HitPointsWithoutConfidence): HitPointsWithoutConfidence {
return assertion.check(hp, isValidHitPointsWithoutConfidence, (hp) => `Invalid hit points: ${hitPointsWithoutConfidenceToString(hp)}`)
}
/** Asserts that the given HitPoints object lives up to all its requirements. */
export function checkValidHitPoints(hp: HitPoints): HitPoints {
return assertion.check(hp, isValidHitPoints, (hp) => `Invalid hit points object: ${hitPointsToString(hp)}`)
}
/** Asserts that the given confidenceLimit value is at least plausible. */
export function checkValidConfidenceLimit(limit: number, hp: HitPointsWithoutConfidence): number {
return assertion.check(limit, (limit) => isValidConfidenceLimit(limit, hp), (limit) => {
return `Invalid confidence limit: ${limit} for ${hp.currentHealth}/${hp.maxHealth} Health, ${hp.maxConfidence} Max Confidence`
})
}
/**
* Asserts that the given change in health has resulted in a congruent change in confidence - i.e., that the new
* confidence is the same distance below the new confidence limit as the old confidence was below the old one.
* A delta of 1 is permitted due to the fact that the confidence change will be rounded (up in the case of damage,
* down in the case of healing).
*
* Used by the damageHealth and recoverHealth functions.
*/
export function checkConsistentConfidenceDelta(newConfidenceUnclamped: number, action: string, oldHp: HitPoints, newHealth: number): number {
return assertion.check(newConfidenceUnclamped, (newConfidenceUnclamped) => {
return Math.abs((confidenceLimit({...oldHp, currentHealth: newHealth}) - newConfidenceUnclamped) - (confidenceLimit(oldHp) - oldHp.currentConfidence)) <= 1
}, (newConfidenceUnclamped) => {
const newLimit = confidenceLimit({...oldHp, currentHealth: newHealth})
const oldLimit = confidenceLimit(oldHp)
return (`${action} from ${hitPointsToString(oldHp)} to ${hitPointsToString({...oldHp, currentHealth: newHealth, currentConfidence: newConfidenceUnclamped})} `
+ `resulted in a change in the difference between the confidenceLimit and the currentConfidenceUnclamped `
+ `from ${oldHp.currentConfidence}/${oldLimit} = ${oldLimit - oldHp.currentConfidence} `
+ `to ${newConfidenceUnclamped}/${newLimit} = ${newLimit - newConfidenceUnclamped}`)
})
}
/**
* The effective maximum confidence, based on scaling maxConfidence by the character's current health percentage.
* Always an integer, rounding down if the result is fractional.
* Is 0 if currentHealth <= 0.
* Is maxConfidence if and only if currentHealth === maxHealth, due to rounding down (floor).
* Otherwise, scaled to somewhere between them.
*/
export function confidenceLimit(hp: HitPointsWithoutConfidence): number {
checkValidHitPointsWithoutConfidence(hp)
const result = Math.max(0, Math.floor(hp.currentHealth * hp.maxConfidence / hp.maxHealth))
return checkValidConfidenceLimit(result, hp)
}
/**
* Inflicts the given amount of damage on the character's health.
* The character's health will never be reduced below -maxHealth this way.
* If damageConfidence is true, confidence will be reduced proportionately.
* e.g., if Confidence is 50/80/100 and Health is 40/50, damaging Health by 10 with damageConfidence off
* results in Health 30/50 and Confidence 50/60/100 - the currentConfidence has not changed.
* On the other hand, with damageConfidence on, that same scenario
* results in Health 30/50 and Confidence 30/60/100 - the currentConfidence has fallen by 20 due to scaling the
* 10 Health damage to 20 Confidence damage. The character's confidence will not be reduced below 0 this way.
* If damageConfidence is off, currentConfidence will still change if the health change causes the confidenceLimit to
* drop below the currentConfidence. In this case, currentConfidence will be clamped to the new confidenceLimit.
*/
export function damageHealth(hp: HitPoints, damage: number, options: {damageConfidence: boolean}): HitPoints {
checkValidHitPoints(hp)
assertion.checkPositiveIntegerOrZero(damage, (damage) => `Tried to damage health by negative or non-integer value ${damage}`)
const {damageConfidence} = options
let currentHealth = Math.max(-hp.maxHealth,hp.currentHealth - damage)
let currentConfidence = damageConfidence
? Math.max(0,
checkConsistentConfidenceDelta(
hp.currentConfidence - Math.ceil((Math.max(0, hp.currentHealth) - Math.max(0, currentHealth)) * hp.maxConfidence / hp.maxHealth),
"damageHealth",
hp,
currentHealth))
: Math.min(hp.currentConfidence, confidenceLimit({
...hp,
currentHealth: currentHealth
}))
return checkValidHitPoints({
...hp,
currentHealth,
currentConfidence,
})
}
/**
* Heals the character's health by the given amount.
* The character's health will never be increased above maxHealth this way.
* If recoverConfidence is true, confidence will be increased proportionately to the increase in health.
* e.g., if Confidence is 50/60/100 and Health is 30/50, recovering Health by 10 with recoverConfidence off
* results in Health 40/50 and Confidence 50/80/100 - the currentConfidence has not changed.
* On the other hand, with recoverConfidence on, that same scenario
* results in Health 40/50 and Confidence 70/80/100 - the currentConfidence has risen by 20 due to scaling the
* 10 Health recovery to 20 Confidence recovery.
*/
export function recoverHealth(hp: HitPoints, healing: number, options: {recoverConfidence: boolean}): HitPoints {
checkValidHitPoints(hp)
assertion.checkPositiveIntegerOrZero(healing, (healing) => `Tried to heal health by negative or non-integer value ${healing}`)
const {recoverConfidence} = options
const currentHealth = Math.min(hp.maxHealth, hp.currentHealth + healing)
// We don't need to check against the confidenceLimit here. When recoverConfidence is on, we're raising confidence
// by the same amount the confidenceLimit went up. So if it was under the limit before, it should be the same amount
// under the limit now. And if recoverConfidence is off, we're not raising confidence at all, so we're even more
// under the limit than we were before.
// The assertion checks that the former case holds.
const currentConfidence = recoverConfidence
? checkConsistentConfidenceDelta(
hp.currentConfidence + Math.floor((Math.max(0, currentHealth) - Math.max(0, hp.currentHealth)) * hp.maxConfidence / hp.maxHealth),
"recoverHealth",
hp,
currentHealth)
: hp.currentConfidence
return checkValidHitPoints({
...hp,
currentHealth,
currentConfidence,
})
}
/**
* Sets the character's new maximum health.
* If the new value is below the character's current absolute-value health, the character's current health will be clamped to that value.
* If the new value is above the character's previous maximum health, the character's confidence will be clamped to the new confidence limit, which will be lower.
*/
export function setMaxHealth(hp: HitPoints, maxHealth: number): HitPoints {
checkValidHitPoints(hp)
checkValidMaxHitPoints(maxHealth)
let {currentHealth, currentConfidence} = hp
if (maxHealth <= Math.abs(currentHealth)) {
currentHealth = Math.sign(currentHealth) * maxHealth
}
if (maxHealth > hp.maxHealth) {
currentConfidence = Math.min(currentConfidence, confidenceLimit({...hp, maxHealth}))
}
return checkValidHitPoints({
...hp,
maxHealth,
currentHealth,
currentConfidence
})
}
/**
* Sets the character's new current health.
* The character's confidence will be clamped to the new confidence limit.
*/
export function setCurrentHealth(hp: HitPoints, currentHealth: number): HitPoints {
checkValidHitPoints(hp)
checkValidCurrentHitPoints(currentHealth, hp.maxHealth, -hp.maxHealth)
const currentConfidence = Math.min(confidenceLimit({...hp, currentHealth}), hp.currentConfidence)
return checkValidHitPoints({
...hp,
currentHealth,
currentConfidence
})
}
/**
* Inflicts the given amount of damage on the character's confidence.
* The character's confidence will never be reduced below 0 this way.
*/
export function damageConfidence(hp: HitPoints, damage: number): HitPoints {
checkValidHitPoints(hp)
assertion.checkPositiveIntegerOrZero(damage, (damage) => `Tried to damage confidence by negative or non-integer value ${damage}`)
let newConfidence = Math.max(0, hp.currentConfidence - damage)
return checkValidHitPoints({
...hp,
currentConfidence: newConfidence,
})
}
/**
* Heals the given amount of damage to the character's confidence.
* The character's confidence will never be increased above the current confidenceLimit this way.
*/
export function recoverConfidence(hp: HitPoints, healing: number): HitPoints {
checkValidHitPoints(hp)
assertion.checkPositiveIntegerOrZero(healing, (healing) => `Tried to heal confidence by negative or non-integer value ${healing}`)
let newConfidence = Math.min(confidenceLimit(hp), hp.currentConfidence + healing)
return checkValidHitPoints({
...hp,
currentConfidence: newConfidence,
})
}
/**
* Sets the character's new maximum confidence.
* The current confidence will be clamped to the new confidenceLimit.
*/
export function setMaxConfidence(hp: HitPoints, maxConfidence: number): HitPoints {
checkValidHitPoints(hp)
checkValidMaxHitPoints(maxConfidence)
let {currentConfidence} = hp
currentConfidence = Math.min(currentConfidence, confidenceLimit({...hp, maxConfidence}))
return checkValidHitPoints({
...hp,
maxConfidence,
currentConfidence
})
}
/** Sets the character's new current confidence. */
export function setCurrentConfidence(hp: HitPoints, currentConfidence: number): HitPoints {
checkValidHitPoints(hp)
checkValidCurrentHitPoints(currentConfidence, confidenceLimit(hp), 0)
return checkValidHitPoints({
...hp,
currentConfidence
})
}

@ -1,13 +0,0 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}

@ -1,19 +0,0 @@
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import {parse} from './scripting/NomScript.peggy';
console.log(parse)
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

@ -0,0 +1,7 @@
import { describe, test, expect } from '@jest/globals'
describe("Jest testing", () => {
test("runs", () => {
expect(test).toBeDefined()
})
})

@ -0,0 +1,5 @@
function main() {
console.log("we're up and runnin'")
}
main()

@ -1,71 +0,0 @@
/// <reference types="node" />
/// <reference types="react" />
/// <reference types="react-dom" />
declare namespace NodeJS {
interface ProcessEnv {
readonly NODE_ENV: 'development' | 'production' | 'test';
readonly PUBLIC_URL: string;
}
}
declare module '*.avif' {
const src: string;
export default src;
}
declare module '*.bmp' {
const src: string;
export default src;
}
declare module '*.gif' {
const src: string;
export default src;
}
declare module '*.jpg' {
const src: string;
export default src;
}
declare module '*.jpeg' {
const src: string;
export default src;
}
declare module '*.png' {
const src: string;
export default src;
}
declare module '*.webp' {
const src: string;
export default src;
}
declare module '*.svg' {
import * as React from 'react';
export const ReactComponent: React.FunctionComponent<React.SVGProps<
SVGSVGElement
> & { title?: string }>;
const src: string;
export default src;
}
declare module '*.module.css' {
const classes: { readonly [key: string]: string };
export default classes;
}
declare module '*.module.scss' {
const classes: { readonly [key: string]: string };
export default classes;
}
declare module '*.module.sass' {
const classes: { readonly [key: string]: string };
export default classes;
}

@ -1,15 +0,0 @@
import { ReportHandler } from 'web-vitals';
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;

@ -1,103 +0,0 @@
import {
battlerAttribute,
BattlerAttributeType,
BattlerDeclaration,
battlerDeclaration,
BattlerStatement,
BattlerStatementType,
isBattlerAttribute,
isBattlerDeclaration
} from "./BattlerStatement";
import {TopLevelStatementType} from "./TopLevelStatement";
import {ScriptFile, scriptFile} from "./ScriptFile";
import {parse as parseInternal, SyntaxError} from "./NomScript.peggy";
import {numberValue} from "./ScriptValue";
function parse(text: string): ScriptFile {
return parseInternal(text, {start: "ScriptFile", grammarSource: "testData"})
}
describe("BattlerDeclaration", () => {
describe("constructor", () => {
test("sets id property from input", () => {
expect(battlerDeclaration("myId", []).id).toEqual("myId")
})
});
describe("typecheck", () => {
test("returns true on constructor output", () => {
expect(isBattlerDeclaration(battlerDeclaration("testId", []))).toBeTruthy()
})
test("returns true on hand-crafted instance", () => {
expect(isBattlerDeclaration({
type: TopLevelStatementType.BATTLER,
id: "someId",
contents: []
})).toBeTruthy()
})
});
describe("parsing", () => {
function success(name: string, text: string, ...result: BattlerDeclaration[]) {
test(`succeeds for ${name}`, () => {
expect(parse(text)).toEqual(scriptFile(result))
})
}
success("basic empty instance", "battler nomi\nend battler", battlerDeclaration("nomi", []))
success("comment after opening", "battler nomi // of the nomi crew\nend battler", battlerDeclaration("nomi", []))
success("comments and empty lines inside the block", "battler nomi\n\n// hungry gal...\n\nend battler", battlerDeclaration("nomi", []))
success("nonempty instance", "battler nomi\nhealth 20\nconfidence 90\nend battler",
battlerDeclaration("nomi", [
battlerAttribute(BattlerAttributeType.HEALTH, numberValue(20)),
battlerAttribute(BattlerAttributeType.CONFIDENCE, numberValue(90))]))
function failure(name: string, text: string) {
test(`fails for ${name}`, () => {
expect(() => parse(text)).toThrow(SyntaxError)
})
}
failure("opening statement only", "battler open")
failure("ending statement only", "end battler")
failure("multi identifier", "battler cool girl\nend battler")
failure("containing top level statement", "battler genius\nscript version 3\nend battler")
failure("containing other battler declaration", "battler coolio\nbattler even_cooler\nend battler\nend battler")
});
});
describe("BattlerAttribute", () => {
describe("constructor", () => {
test("forwards the elements to the fields", () => {
const attr = battlerAttribute(BattlerAttributeType.HEALTH, numberValue(20))
expect(attr.attribute).toEqual(BattlerAttributeType.HEALTH)
expect(attr.value).toEqual(numberValue(20))
})
})
describe("typecheck", () => {
test("returns true on constructor input", () => {
expect(isBattlerAttribute(battlerAttribute(BattlerAttributeType.HEALTH, numberValue(20)))).toBeTruthy()
})
test("returns true on hand-crafted instance", () => {
expect(isBattlerAttribute({
type: BattlerStatementType.ATTRIBUTE,
attribute: BattlerAttributeType.STAMINA,
value: numberValue(20)
})).toBeTruthy()
})
})
describe("parsing", () => {
function success(name: string, text: string, ...result: readonly BattlerStatement[]) {
test(`succeeds for ${name}`, () => {
expect(parse(`battler battler\n${text}\nend battler`)).toEqual(scriptFile([battlerDeclaration("battler", result)]))
})
}
success("basic empty instance", "health 20", battlerAttribute(BattlerAttributeType.HEALTH, numberValue(20)))
success("comment between the attribute and the number", "stamina/* 200 */99", battlerAttribute(BattlerAttributeType.STAMINA, numberValue(99)))
function failure(name: string, text: string) {
test(`fails for ${name}`, () => {
expect(() => parse(text)).toThrow(SyntaxError)
})
}
failure("no value after the attribute", "stamina // 99")
})
});

@ -1,58 +0,0 @@
import {TopLevelStatement, TopLevelStatementType} from "./TopLevelStatement";
import {ScriptExpression} from "./ScriptExpression";
export interface BattlerDeclaration {
readonly type: TopLevelStatementType.BATTLER
readonly id: string
readonly contents: readonly BattlerStatement[]
}
export function battlerDeclaration(id: string, contents: readonly BattlerStatement[]): BattlerDeclaration {
return {
type: TopLevelStatementType.BATTLER,
id,
contents,
}
}
export function isBattlerDeclaration(statement: TopLevelStatement): statement is BattlerDeclaration {
return statement.type === TopLevelStatementType.BATTLER
}
export enum BattlerStatementType {
ATTRIBUTE = "attribute",
}
export enum BattlerAttributeType {
HEALTH = "health",
CONFIDENCE = "confidence",
STAMINA = "stamina",
}
export function battlerAttributeType(text: string): BattlerAttributeType {
switch (text) {
case "health":
return BattlerAttributeType.HEALTH
case "confidence":
return BattlerAttributeType.CONFIDENCE
case "stamina":
return BattlerAttributeType.STAMINA
default:
throw Error(`Unrecognized attribute for battler: ${text}`)
}
}
export interface BattlerAttribute {
readonly type: BattlerStatementType.ATTRIBUTE
readonly attribute: BattlerAttributeType
readonly value: ScriptExpression
}
export function battlerAttribute(attribute: BattlerAttributeType, value: ScriptExpression): BattlerAttribute {
return {
type: BattlerStatementType.ATTRIBUTE,
attribute,
value
}
}
export function isBattlerAttribute(statement: BattlerStatement): statement is BattlerAttribute {
return statement.type === BattlerStatementType.ATTRIBUTE
}
export type BattlerStatement = BattlerAttribute

@ -1,36 +0,0 @@
import {parse} from "./NomScript.peggy";
import {scriptFile} from "./ScriptFile";
import {versionStatement} from "./TopLevelStatement";
import {battlerAttribute, BattlerAttributeType, battlerDeclaration} from "./BattlerStatement";
import {numberValue} from "./ScriptValue";
test("integration test", () => {
const result = parse(`
script version 1
battler reya
health 90
confidence 2000
stamina 50
end battler
battler kun_chan
health 500
confidence 1000
stamina 200
end battler
`, {grammarSource: "testData", start: "ScriptFile"})
expect(result).toEqual(scriptFile([
versionStatement(1),
battlerDeclaration("reya", [
battlerAttribute(BattlerAttributeType.HEALTH, numberValue(90)),
battlerAttribute(BattlerAttributeType.CONFIDENCE, numberValue(2000)),
battlerAttribute(BattlerAttributeType.STAMINA, numberValue(50))
]),
battlerDeclaration("kun_chan", [
battlerAttribute(BattlerAttributeType.HEALTH, numberValue(500)),
battlerAttribute(BattlerAttributeType.CONFIDENCE, numberValue(1000)),
battlerAttribute(BattlerAttributeType.STAMINA, numberValue(200))
]),
]))
})

@ -1,60 +0,0 @@
{{
// global (one-time) initializer
import { scriptFile } from "./ScriptFile";
import { versionStatement } from "./TopLevelStatement";
import { battlerDeclaration, battlerAttributeType, battlerAttribute } from "./BattlerStatement";
import { numberValue, identifier } from "./ScriptValue";
}}
{
// per-parse initializer
}
// *********************************************************************************************************************
// * Entry Points
// *********************************************************************************************************************
ScriptFile = statements:TopLevelStatementList { return scriptFile(statements); }
// peggy-loader:startRule ScriptFile
TopLevelExpression = _? @Expression FileEnd
// peggy-loader:startRule TopLevelExpression
// *********************************************************************************************************************
// * Top-level Statements
// *********************************************************************************************************************
TopLevelStatementList = (__ head:TopLevelStatement tail:(StatementEnd __ @TopLevelStatement)* FileEnd { return [head, ...tail]; }) / (FileEnd { return []; })
TopLevelStatement "top-level statement" = VersionStatement / BattlerDeclaration
VersionStatement "version statement" = "script" _ "version" _ version:WholeNumberLiteral { return versionStatement(version.value); }
// *********************************************************************************************************************
// * Battler Declaration + Statements
// *********************************************************************************************************************
BattlerDeclaration "battler declaration" = "battler" _ id:Identifier contents:(StatementEnd __ @BattlerStatement)* StatementEnd __ "end" _ "battler" { return battlerDeclaration(id.value, contents) }
BattlerStatement = BattlerAttribute
BattlerAttribute = attr:BattlerAttributeType _ expr:Expression { return battlerAttribute(attr, expr); }
BattlerAttributeType "attribute name" = attr:("health" / "confidence" / "stamina") { return battlerAttributeType(attr); }
// *********************************************************************************************************************
// * Expressions
// *********************************************************************************************************************
Expression "expression" = WholeNumberLiteral / Identifier
Identifier "identifier" = id:$([a-zA-Z_$] [a-zA-Z_$0-9]*) { return identifier(id); }
WholeNumberLiteral "whole number literal" = digits:$[0-9]+ { return numberValue(digits); }
// *********************************************************************************************************************
// * Whitespace and Comments
// *********************************************************************************************************************
FileEnd = __ LineComment?
__ = StatementEnd* _? { return null; }
StatementEnd = _? LineComment? NewLine { return null; }
_ = (NonToken+) { return null; }
NonToken = (BlockComment / WhiteSpace) { return null; }
LineComment "line comment" = "//" [^\n]* { return null; }
BlockComment "block comment" = "/*" [^*]* BlockCommentTail { return null; }
BlockCommentTail = "*/" / ( "*" !"/" [^*]* BlockCommentTail ) { return null; }
WhiteSpace "whitespace" = [ \r\v\t]+ { return null; }
NewLine "newline" = "\n" { return null; }

@ -1,34 +0,0 @@
import {ScriptFile} from "./ScriptValue.js";
import {ScriptExpression} from "./ScriptExpression";
export interface Options {
/** Whether this is a script or an expression. Required. */
readonly startRule: "ScriptFile"|"TopLevelExpression"
/** The file (or other source) the text is being parsed from. Required.*/
readonly grammarSource: string
}
export interface Position {
readonly offset: number
readonly line: number
readonly column: number
}
export interface Location {
readonly source: string
readonly start: Position
readonly end: Position
}
export function parse(text: string, options: Options & {readonly startRule: "TopLevelExpression"}): ScriptExpression
export function parse(text: string, options: Options & {readonly startRule: "ScriptFile"}): ScriptFile
export class SyntaxError extends Error {
readonly name: "SyntaxError"
readonly message: string
readonly expected: (readonly string[])|null
readonly found: string|null
readonly location: Location
constructor(message: string, expected: (readonly string[])|null, found: string|null, location: Location)
format(mappings: readonly {readonly source: string, readonly text: string}[]): string
}

@ -1,69 +0,0 @@
import { parse, SyntaxError } from "./NomScript.peggy";
describe("parse", () => {
test("throws SyntaxError for invalid text", () => {
expect(() => parse("!! invalid !!", {grammarSource: "testData", startRule: "ScriptFile"})).toThrow(SyntaxError)
});
})
describe("SyntaxError", () => {
describe("constructor", () => {
test("populates the members accordingly", () => {
const location = {
source: "source",
start: {
offset: 0,
line: 1,
column: 1,
},
end: {
offset: 7,
line: 1,
column: 8,
},
};
const err = new SyntaxError("message", ["expected"], "found", location);
expect(err.name).toEqual("SyntaxError");
expect(err.message).toEqual("message");
expect(err.expected).toEqual(["expected"]);
expect(err.found).toEqual("found");
expect(err.location).toEqual(location);
});
});
describe("format", () => {
test("displays a pretty error message for the failure", () => {
const location = {
source: "source",
start: {
offset: 0,
line: 1,
column: 1,
},
end: {
offset: 7,
line: 1,
column: 8,
},
};
const err = new SyntaxError(
"message",
["valid", "cool"],
"errored",
location
);
const result = err.format([
{ source: "source", text: "errored text" },
{ source: "source2", text: "irrelevant" },
]);
expect(result).toMatchInlineSnapshot(`
"Error: message
--> source:1:1
|
1 | errored text
| ^^^^^^^"
`);
});
});
});

@ -1,24 +0,0 @@
import {parse, SyntaxError} from "./NomScript.peggy";
describe("ScriptExpression", () => {
describe("parse", () => {
test("fails with an empty input", () => {
expect(() => parse("", {grammarSource: "testData", startRule: "TopLevelExpression"})).toThrow(SyntaxError)
});
test("fails with empty lines", () => {
expect(() => parse("\n\n\n\n\n", {grammarSource: "testData", startRule: "TopLevelExpression"})).toThrow(SyntaxError)
});
test("fails with lines filled only with whitespace", () => {
expect(() => parse("\n\n \n\t\t \t\n\n", {grammarSource: "testData", startRule: "TopLevelExpression"})).toThrow(SyntaxError)
});
test("fails with lines filled only with comments", () => {
expect(() => parse("\n\n /* */ \n// test \n /* */\n", {grammarSource: "testData", startRule: "TopLevelExpression"})).toThrow(SyntaxError)
});
test("works for a simple expression", () => {
expect(() => parse("73", {grammarSource: "testData", startRule: "TopLevelExpression"})).not.toThrow()
});
test("succeeds with end of line comments", () => {
expect(() => parse("90 // you know", {grammarSource: "testData", startRule: "TopLevelExpression"})).not.toThrow()
});
});
});

@ -1,3 +0,0 @@
import {ScriptValue} from "./ScriptValue";
export type ScriptExpression = ScriptValue

@ -1,27 +0,0 @@
import {parse} from "./NomScript.peggy";
import {scriptFile} from "./ScriptFile";
import {versionStatement} from "./TopLevelStatement";
describe("ScriptFile", () => {
describe("constructor", () => {
test("puts the statements into the list", () => {
expect(scriptFile([versionStatement(3)])).toEqual({
statements: [versionStatement(3)]
})
});
});
describe("parse", () => {
test("works with an empty input", () => {
expect(() => parse("", {grammarSource: "testData", startRule: "ScriptFile"})).not.toThrow()
});
test("works with empty lines", () => {
expect(() => parse("\n\n\n\n\n", {grammarSource: "testData", startRule: "ScriptFile"})).not.toThrow()
});
test("works with lines filled only with whitespace", () => {
expect(() => parse("\n\n \n\t\t \t\n\n", {grammarSource: "testData", startRule: "ScriptFile"})).not.toThrow()
});
test("works with lines filled only with comments", () => {
expect(() => parse("\n\n /* */ \n// test \n /* */\n", {grammarSource: "testData", startRule: "ScriptFile"})).not.toThrow()
});
});
});

@ -1,10 +0,0 @@
import {TopLevelStatementList} from "./TopLevelStatement.js";
export interface ScriptFile {
readonly statements: TopLevelStatementList
}
export function scriptFile(statements: TopLevelStatementList): ScriptFile {
return {
statements
}
}

@ -1,88 +0,0 @@
import {identifier, isIdentifier, isNumberValue, numberValue, ScriptValueType} from "./ScriptValue";
import { parse, SyntaxError } from "./NomScript.peggy";
import {ScriptExpression} from "./ScriptExpression";
describe("NumberValue", () => {
describe("constructor", () => {
test("forwards number parameter to the value property", () => {
expect(numberValue(5)).toEqual({
type: ScriptValueType.NUMBER,
value: 5
})
})
test("parses string parameter to the value property", () => {
expect(numberValue("99")).toEqual({
type: ScriptValueType.NUMBER,
value: 99
})
})
})
describe("typecheck", () => {
test("passes on the output of the constructor", () => {
expect(isNumberValue(numberValue(2))).toBeTruthy()
})
test("passes on a hand-constructed instance", () => {
expect(isNumberValue({
type: ScriptValueType.NUMBER,
value: 21
})).toBeTruthy()
})
})
describe("parsing", () => {
function success(name: string, text: string, result: ScriptExpression) {
test(`succeeds for ${name}`, () => {
expect(parse(text, {startRule: "TopLevelExpression", grammarSource: "testdata"})).toEqual(result)
})
}
success("zero", "0", numberValue(0))
success("leading zeroes on a zero", "00000000", numberValue(0))
success("leading zeroes on a positive number", "0000000050", numberValue(50))
success("simple positive number", "1", numberValue(1))
success("long number", "99999999", numberValue(99999999))
function failure(name: string, text: string) {
test(`fails for ${name}`, () => {
expect(() => parse(text, {startRule: "TopLevelExpression", grammarSource: "testdata"})).toThrow(SyntaxError)
})
}
failure("number containing non-digit characters", "100a0")
failure("number with decimal point", "100.0")
failure("number with negative sign", "-100")
})
})
describe("ScriptIdentifier", () => {
describe("constructor", () => {
test("transfers the value parameter to the attribute", () => {
expect(identifier("naps").value).toEqual("naps")
})
})
describe("typecheck", () => {
test("passes on the output of the constructor", () => {
expect(isIdentifier(identifier("baps"))).toBeTruthy()
})
test("passes on a hand-constructed instance", () => {
expect(isIdentifier({
type: ScriptValueType.IDENTIFIER,
value: "laps"
})).toBeTruthy()
})
})
describe("parsing", () => {
function success(name: string, text: string, result: ScriptExpression) {
test(`succeeds for ${name}`, () => {
expect(parse(text, {startRule: "TopLevelExpression", grammarSource: "testdata"})).toEqual(result)
})
}
success("basic identifier", "basic", identifier("basic"))
success("identifier starting with underscore", "_boop", identifier("_boop"))
success("identifier starting with dollar sign", "$tring", identifier("$tring"))
success("single character identifier", "a", identifier("a"))
success("identifier ending in numbers", "$100", identifier("$100"))
function failure(name: string, text: string) {
test(`fails for ${name}`, () => {
expect(() => parse(text, {startRule: "TopLevelExpression", grammarSource: "testdata"})).toThrow(SyntaxError)
})
}
failure("identifier starting in numbers", "100$")
})
});

@ -1,41 +0,0 @@
export enum ScriptValueType {
NUMBER = "number",
IDENTIFIER = "identifier",
}
export interface NumberValue {
readonly type: ScriptValueType.NUMBER,
readonly value: number,
}
export function numberValue(value: number|string): NumberValue {
if (typeof value === "string") {
return {
type: ScriptValueType.NUMBER,
value: parseInt(value, 10),
}
} else {
return {
type: ScriptValueType.NUMBER,
value,
}
}
}
export function isNumberValue(value: ScriptValue): value is NumberValue {
return value.type === ScriptValueType.NUMBER
}
export interface ScriptIdentifier {
readonly type: ScriptValueType.IDENTIFIER
readonly value: string
}
export function identifier(value: string): ScriptIdentifier {
return {
type: ScriptValueType.IDENTIFIER,
value
}
}
export function isIdentifier(value: ScriptValue): value is ScriptIdentifier {
return value.type === ScriptValueType.IDENTIFIER
}
export type ScriptValue = NumberValue|ScriptIdentifier

@ -1,59 +0,0 @@
import {parse as parseInternal, SyntaxError} from "./NomScript.peggy"
import {isVersionStatement, TopLevelStatementType, VersionStatement, versionStatement} from "./TopLevelStatement";
import {scriptFile, ScriptFile} from "./ScriptFile";
function parse(text: string): ScriptFile {
return parseInternal(text, {startRule: "ScriptFile", grammarSource: "testData"})
}
describe("VersionStatement", () => {
describe("constructor", () => {
test("forwards parameter to the version property", () => {
expect(versionStatement(5)).toEqual({
type: TopLevelStatementType.VERSION,
version: 5
})
})
})
describe("typecheck", () => {
test("passes on the output of the constructor", () => {
expect(isVersionStatement(versionStatement(2))).toBeTruthy()
})
test("passes on a hand-constructed instance", () => {
expect(isVersionStatement({
type: TopLevelStatementType.VERSION,
version: 5
})).toBeTruthy()
})
})
describe("parsing", () => {
function success(name: string, text: string, ...result: VersionStatement[]) {
test(`succeeds for ${name}`, () => {
expect(parse(text)).toEqual(scriptFile(result))
})
}
success("basic example", "script version 1", versionStatement(1))
success("followed by line comment", "script version 3 // and nine tenths", versionStatement(3))
success("preceded by block comment", "/* just because it's a */ script version 2", versionStatement(2))
success("preceded by line comment", "// line comment\nscript version 9999", versionStatement(9999))
success("preceded by spaces", "\n\n\n\t\t\tscript version 2", versionStatement(2))
success("separated by tabs instead of spaces", "script\tversion\t4", versionStatement(4))
success("separated by comments instead of spaces", "script/* or candy */version/* if it can be called that */4", versionStatement(4))
success("separated by multiline comments instead of spaces", "script/* ripped\n */version/* mergin\n */4", versionStatement(4))
success("doubled on separate lines", "script version 1\nscript version 2", versionStatement(1), versionStatement(2))
function failure(name: string, text: string) {
test(`fails for ${name}`, () => {
expect(() => parse(text)).toThrow(SyntaxError)
})
}
failure("separated by newlines", "script\nversion\n1")
failure("not separated", "scriptversion1")
failure("wrong order", "version script 1")
failure("decimal version", "version script 1.5")
failure("followed by garbage", "script version 1 and a half")
failure("doubled on same line", "script version 1 script version 1")
})
})

@ -1,23 +0,0 @@
import {BattlerDeclaration} from "./BattlerStatement";
export enum TopLevelStatementType {
VERSION = "version",
BATTLER = "battler",
}
export interface VersionStatement {
readonly type: TopLevelStatementType.VERSION,
readonly version: number,
}
export function versionStatement(version: number): VersionStatement {
return {
type: TopLevelStatementType.VERSION,
version,
}
}
export function isVersionStatement(statement: TopLevelStatement): statement is VersionStatement {
return statement.type === TopLevelStatementType.VERSION
}
export type TopLevelStatement = VersionStatement | BattlerDeclaration
export type TopLevelStatementList = readonly TopLevelStatement[]

@ -1,5 +0,0 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';

@ -1,174 +0,0 @@
import {AssertionConfig, AssertionMode, Assertions} from "./Assertions";
describe("Assertion", () => {
describe("check", () => {
const defaults = {
value: Symbol("test value"),
config: AssertionConfig.Skip,
conditionImpl: () => true,
messageImpl: () => "Mockery",
} as const
function run<T>({value, config, conditionImpl, messageImpl}: {value: T, config: typeof AssertionConfig.Skip, conditionImpl: (value: T) => boolean, messageImpl: (value: T) => string}): {value: T, condition: jest.Mock<boolean, [T]>, message: jest.Mock<string, [T]>, retval: T}
function run<T>({value, config, conditionImpl, messageImpl}: {value: T, config: typeof AssertionConfig.Throw, conditionImpl: (value: T) => boolean, messageImpl: (value: T) => string}): {value: T, condition: jest.Mock<boolean, [T]>, message: jest.Mock<string, [T]>, retval?: T, error?: Error}
function run<T>({value, config, conditionImpl, messageImpl}: {value: T, config: ReturnType<typeof AssertionConfig.Log>, conditionImpl: (value: T) => boolean, messageImpl: (value: T) => string}): {value: T, log: jest.Mock<void, any[]>, condition: jest.Mock<boolean, [T]>, message: jest.Mock<string, [T]>, retval: T}
function run<T>({value, config, conditionImpl, messageImpl}: {value: T, config: AssertionConfig.Type, conditionImpl: (value: T) => boolean, messageImpl: (value: T) => string}): {value: T, log?: jest.Mock<void, any[]>, condition: jest.Mock<boolean, [T]>, message: jest.Mock<string, [T]>, retval?: T, error?: Error}
function run<T>({value, config, conditionImpl, messageImpl}: {value: T, config: AssertionConfig.Type, conditionImpl: (value: T) => boolean, messageImpl: (value: T) => string}): {value: T, log?: jest.Mock<void, any[]>, condition: jest.Mock<boolean, [T]>, message: jest.Mock<string, [T]>, retval?: T, error?: Error} {
const assertion = new Assertions()
assertion.config = config
let log: jest.Mock<void, any[]>|undefined = undefined
if (config.mode === AssertionMode.LOG) {
log = jest.fn(config.logger)
assertion.config = AssertionConfig.Log(log)
}
const condition = jest.fn(conditionImpl)
const message = jest.fn(messageImpl)
let error: Error|undefined = undefined
let retval: T|undefined = undefined
try {
retval = assertion.check(value, condition, message)
} catch (e) {
if (e instanceof Error && config.mode === AssertionMode.THROW) {
error = e
} else {
throw e
}
}
return {
value, log, condition, message, error, retval
}
}
it(`returns the input and does not throw in SKIP`, () => {
const {value, retval} = run({
...defaults,
config: AssertionConfig.Skip
})
expect(retval).toBe(value)
});
it(`does not call the condition or message in SKIP`, () => {
const {condition, message} = run({
...defaults,
config: AssertionConfig.Skip
})
expect(condition).not.toHaveBeenCalled()
expect(message).not.toHaveBeenCalled()
});
it(`calls the log function with an error, returns the value and does not throw when the condition fails in LOG`, () => {
const {value, log, retval} = run({
...defaults,
config: AssertionConfig.Log(() => {}),
conditionImpl: () => false,
messageImpl: () => "Mockery"
})
expect(log).toHaveBeenCalledTimes(1)
expect(log).toHaveBeenCalledWith(expect.objectContaining({message: "Mockery"}))
expect(log).toHaveBeenCalledWith(expect.any(Error))
expect(retval).toBe(value)
});
it(`throws an error when the condition fails in THROW`, () => {
const {error} = run({
...defaults,
config: AssertionConfig.Throw,
conditionImpl: () => false,
messageImpl: () => "Mockery"
})
expect(error).toEqual(expect.objectContaining({message: "Mockery"}))
expect(error).toBeInstanceOf(Error)
});
it(`does not log and returns the input when the condition succeeds in LOG`, () => {
const {value, retval, log} = run({
...defaults,
config: AssertionConfig.Log(() => {}),
conditionImpl: () => true
})
expect(log).not.toHaveBeenCalled()
expect(retval).toBe(value)
});
it(`does not throw and returns the input when the condition succeeds in THROW`, () => {
const {value, retval, error} = run({
...defaults,
config: AssertionConfig.Throw,
conditionImpl: () => true
})
expect(error).toBeUndefined()
expect(retval).toBe(value)
});
[AssertionConfig.Log(() => {}), AssertionConfig.Throw].forEach((config) => {
it(`calls the condition, but not the message, once with the value in ${config.mode} when the condition succeeds`, () => {
const {value, condition, message} = run({
...defaults,
config,
conditionImpl: () => true
})
expect(condition).toHaveBeenCalledWith(value)
expect(condition).toHaveBeenCalledTimes(1)
expect(message).not.toHaveBeenCalled()
})
it(`calls the condition and message once each with the value in ${config.mode} when the condition fails`, () => {
const {value, condition, message} = run({
...defaults,
config,
conditionImpl: () => false
})
expect(condition).toHaveBeenCalledWith(value)
expect(condition).toHaveBeenCalledTimes(1)
expect(message).toHaveBeenCalledWith(value)
expect(message).toHaveBeenCalledTimes(1)
})
})
})
function testCheck<T>(run: (input: T) => void, valid: T[], invalid: T[]) {
valid.forEach((input) => {
test(`passes ${input}`, () => {
expect(() => run(input)).not.toThrow()
})
})
invalid.forEach((input) => {
test(`fails ${input}`, () => {
expect(() => run(input)).toThrow()
})
})
}
describe("checkInteger", () => {
function run(input: number) {
const assertion = new Assertions()
assertion.config = AssertionConfig.Throw
assertion.checkInteger(input, () => "Assertion failed")
}
const valid: number[] = [0, 1, -1, Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER]
const invalid: number[] = [Number.NaN, Number.NEGATIVE_INFINITY, Number.POSITIVE_INFINITY, Number.MAX_SAFE_INTEGER + 999, Number.MAX_VALUE, Number.MIN_SAFE_INTEGER - 999, Number.EPSILON]
testCheck(run, valid, invalid)
})
describe("checkPositiveIntegerOrZero", () => {
function run(input: number) {
const assertion = new Assertions()
assertion.config = AssertionConfig.Throw
assertion.checkPositiveIntegerOrZero(input, () => "Assertion failed")
}
const valid: number[] = [0, 1, Number.MAX_SAFE_INTEGER]
const invalid: number[] = [-1, Number.MIN_SAFE_INTEGER, Number.NaN, Number.NEGATIVE_INFINITY, Number.POSITIVE_INFINITY, Number.MAX_SAFE_INTEGER + 999, Number.MAX_VALUE, Number.MIN_SAFE_INTEGER - 999, Number.EPSILON]
testCheck(run, valid, invalid)
})
})

@ -1,58 +0,0 @@
export enum AssertionMode {
SKIP = "SKIP",
LOG = "LOG",
THROW = "THROW",
}
/** Configurations for the different assertion modes. */
export namespace AssertionConfig {
/** The types of the assertion configurations. */
export type Type = typeof Skip|typeof Throw|ReturnType<typeof Log>
/** An assertion configuration suitable for production, that skips assertions without evaluating them. */
export const Skip = {mode: AssertionMode.SKIP} as const
/** An assertion configuration suitable for testing, that throws an exception when an assertion fails. */
export const Throw = {mode: AssertionMode.THROW} as const
/** An assertion configuration suitable for debugging, that logs an exception when an assertion fails but allows the program to continue. */
export function Log(logger: (message?: unknown, ...params: unknown[]) => void) {
return {mode: AssertionMode.LOG, logger} as const
}
}
/** Assertion class for test-only checks on preconditions and postconditions. */
export class Assertions {
config: AssertionConfig.Type = AssertionConfig.Skip
/** General assert method. Checks that the condition is true. */
check<T>(value: T, condition: (value: T) => boolean, message: (value: T) => string): T {
if (this.config.mode === AssertionMode.SKIP) {
return value
}
const result = condition(value)
if (result) {
return value
}
const err = Error(message(value))
if (this.config.mode === AssertionMode.THROW) {
throw err
} else {
// All we can do is stand by and watch in horror...
this.config.logger(err)
return value
}
}
/** Checks that the value is an integer. */
checkInteger(value: number, message: (value: number) => string): number {
return this.check(value, Number.isSafeInteger, message)
}
/** Checks that the value is an integer and either positive or zero. */
checkPositiveIntegerOrZero(value: number, message: (value: number) => string): number {
return this.check(value, (value) => {
return Number.isSafeInteger(value) && value >= 0
}, message)
}
}
/** Global assertion object for use by other modules. */
export const assertion = new Assertions();

@ -2,14 +2,12 @@
"compilerOptions": { "compilerOptions": {
"target": "es5", "target": "es5",
"lib": [ "lib": [
"dom",
"dom.iterable",
"esnext" "esnext"
], ],
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": false,
"esModuleInterop": true, "esModuleInterop": true,
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": false,
"strict": true, "strict": true,
"strictNullChecks": true, "strictNullChecks": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
@ -18,10 +16,12 @@
"moduleResolution": "node", "moduleResolution": "node",
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"noEmit": true, "outDir": "build"
"jsx": "react-jsx"
}, },
"include": [ "include": [
"src" "src"
],
"exclude": [
"node_modules"
] ]
} }

Loading…
Cancel
Save