├── .eslintignore
├── .github
├── ISSUE_TEMPLATE
│ ├── config.yml
│ ├── feature_request.md
│ └── bug_report.md
└── workflows
│ └── ci.yml
├── img
└── magicmountain.png
├── lib
├── README.md
└── duktape.d.ts
├── .vscode
├── settings.json
└── launch.json
├── tests
├── _setup.js
└── core
│ ├── parkInfo.tests.ts
│ └── parkRating.tests.ts
├── .editorconfig
├── src
├── utilities
│ ├── environment.d.ts
│ ├── array.ts
│ ├── environment.ts
│ └── logger.ts
├── plugin.ts
├── core
│ ├── effects.ts
│ ├── parkRating.ts
│ ├── parkInfo.ts
│ └── influences.ts
└── startup.ts
├── tsconfig.json
├── .eslintrc.json
├── LICENSE
├── rollup.config.dev.js
├── package.json
├── rollup.config.js
├── .gitignore
└── README.md
/.eslintignore:
--------------------------------------------------------------------------------
1 | lib/
2 | dist/
3 | node_modules/
4 | rollup.config.js
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: true
2 |
--------------------------------------------------------------------------------
/img/magicmountain.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Basssiiie/OpenRCT2-ParkRatingInspector/HEAD/img/magicmountain.png
--------------------------------------------------------------------------------
/lib/README.md:
--------------------------------------------------------------------------------
1 | Placeholder file to commit this empty directory.
2 |
3 | Place your `openrct2.d.ts` file inside this folder.
4 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "typescript.tsdk": "node_modules/typescript/lib",
3 | "files.exclude": {
4 | "**/.nyc_output": true
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/tests/_setup.js:
--------------------------------------------------------------------------------
1 | // Set build configuration for unit tests.
2 | // Options: development, production
3 | global.__BUILD_CONFIGURATION__ = "production";
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = tabs
5 | indent_size = 4
6 | charset = utf-8
7 | trim_trailing_whitespace = true
8 | insert_final_newline = true
9 | end_of_line = crlf
10 |
--------------------------------------------------------------------------------
/src/utilities/environment.d.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Specifies whether the current build is for production or development environment.
3 | */
4 | type BuildConfiguration = "production" | "development";
5 |
6 |
7 | /**
8 | * The current active build configuration.
9 | */
10 | declare const __BUILD_CONFIGURATION__: BuildConfiguration;
11 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this plugin
4 | title: ''
5 | labels: 'enhancement'
6 | ---
7 |
8 | **Idea**
9 | Please describe what you'd like to see added or changed.
10 |
11 | **Additional context**
12 | Add any other context, screenshots, related information about the feature request here.
13 |
--------------------------------------------------------------------------------
/src/plugin.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | import { startup } from "./startup";
4 | import { pluginVersion, requiredApiVersion } from "./utilities/environment";
5 |
6 | registerPlugin({
7 | name: "ParkRatingInspector",
8 | version: pluginVersion,
9 | targetApiVersion: requiredApiVersion,
10 | authors: ["Basssiiie"],
11 | type: "local",
12 | licence: "MIT",
13 | main: startup,
14 | });
15 |
--------------------------------------------------------------------------------
/src/utilities/array.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Gets the first matching item, `undefined` if no items match the predicate.
3 | * @param array The array to check.
4 | * @param predicate The function to match the items against.
5 | */
6 | export function find(array: T[], predicate: (item: T) => boolean): T | undefined
7 | {
8 | for (let i = 0; i < array.length; i++)
9 | {
10 | const item = array[i];
11 | if (predicate(item))
12 | {
13 | return item;
14 | }
15 | }
16 | return undefined;
17 | }
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us fix any issues
4 | title: ''
5 | labels: 'bug'
6 | ---
7 |
8 | **Describe the bug**
9 | Please describe clearly and concisely what the bug is.
10 |
11 | **To reproduce**
12 | Steps to reproduce the behavior:
13 | 1. Go to '...'
14 | 2. Click on '....'
15 | 3. Scroll down to '....'
16 | 4. See problem
17 |
18 | **Expected behavior**
19 | A short but clear description of what you expected to happen.
20 |
21 | **Screenshots**
22 | If applicable, please add screenshots or a video to help explain the problem.
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "type": "node",
9 | "request": "launch",
10 | "name": "Debug AVA test file",
11 | "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/ava",
12 | "runtimeArgs": [
13 | "${file}"
14 | ],
15 | "outputCapture": "std",
16 | "skipFiles": [
17 | "/**/*.js"
18 | ]
19 | }
20 | ]
21 | }
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "esModuleInterop": true,
4 | "forceConsistentCasingInFileNames": true,
5 | "module": "ES2020",
6 | "moduleResolution": "node",
7 | "noFallthroughCasesInSwitch": true,
8 | "noImplicitAny": true,
9 | "noImplicitOverride": true,
10 | "noImplicitReturns": true,
11 | "noImplicitThis": true,
12 | "noUnusedLocals": true,
13 | "noUnusedParameters": true,
14 | "outDir": "out",
15 | "strict": true,
16 | "target": "es5",
17 | "lib": [ "es5", "es6" ]
18 | },
19 | "include": [
20 | "./lib/**/*.ts",
21 | "./src/**/*.ts",
22 | "./tests/**/*.ts"
23 | ],
24 | "noEmit": true
25 | }
26 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: ci
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | branches:
9 | - main
10 |
11 | jobs:
12 | lint:
13 | runs-on: ubuntu-latest
14 | steps:
15 | - uses: actions/checkout@v2
16 | - uses: actions/setup-node@v2
17 | with:
18 | node-version: 16
19 | cache: npm
20 | - run: npm ci --prefer-offline --no-audit
21 | - run: npm run lint
22 |
23 | test:
24 | needs: lint
25 | runs-on: ubuntu-latest
26 | steps:
27 | - uses: actions/checkout@v2
28 | - uses: actions/setup-node@v2
29 | with:
30 | node-version: 16
31 | cache: npm
32 | - run: npm ci --prefer-offline --no-audit
33 | - run: npm run test
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es6": true,
5 | "node": true
6 | },
7 | "extends": [
8 | ],
9 | "globals": {
10 | "Atomics": "readonly",
11 | "SharedArrayBuffer": "readonly"
12 | },
13 | "parser": "@typescript-eslint/parser",
14 | "parserOptions": {
15 | "ecmaVersion": 11,
16 | "sourceType": "module"
17 | },
18 | "plugins": [
19 | "@typescript-eslint"
20 | ],
21 | "rules": {
22 | "no-unused-vars": "off",
23 | "semi": "off",
24 | "@typescript-eslint/explicit-function-return-type": ["warn"],
25 | "@typescript-eslint/member-delimiter-style": "warn",
26 | "@typescript-eslint/no-inferrable-types": "off",
27 | "@typescript-eslint/semi": ["warn"],
28 | "@typescript-eslint/triple-slash-reference": "off"
29 | },
30 | "settings": {
31 | "import/resolver": {
32 | "node": { "extensions": [ ".js", ".ts" ] }
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/utilities/environment.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 |
4 | /**
5 | * Returns the current version of the plugin.
6 | */
7 | export const pluginVersion = "0.3";
8 |
9 |
10 | /**
11 | * Returns the required OpenRCT2 API version.
12 | */
13 | export const requiredApiVersion = 38;
14 |
15 |
16 | /**
17 | * Returns the build configuration of the plugin.
18 | */
19 | export const buildConfiguration: BuildConfiguration = __BUILD_CONFIGURATION__;
20 |
21 |
22 | /**
23 | * Returns true if the current build is a production build.
24 | */
25 | export const isProduction = (buildConfiguration === "production");
26 |
27 |
28 | /**
29 | * Returns true if the current build is a production build.
30 | */
31 | export const isDevelopment = (buildConfiguration === "development");
32 |
33 |
34 | /**
35 | * Returns true if the UI is available, or false if the game is running in headless mode.
36 | */
37 | export const isUiAvailable = (typeof ui !== "undefined");
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Basssiiie
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/core/effects.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Value and description of the applied park rating effect.
3 | */
4 | export interface Effect
5 | {
6 | /**
7 | * Indicates whether this effect is currently active and should be shown to the player.
8 | */
9 | active: boolean;
10 |
11 | /**
12 | * The name of the effect.
13 | */
14 | name: string;
15 |
16 | /**
17 | * A stringified version of the current value.
18 | */
19 | value: string;
20 |
21 | /**
22 | * Indicates how much the park rating is affected, as either a negative or positive integer.
23 | */
24 | impact: number;
25 |
26 | /**
27 | * A description of the origin of the effect, or what is causing the impact.
28 | */
29 | note: string;
30 |
31 | /**
32 | * The maximum impact this effect can have.
33 | */
34 | maximum: number | null;
35 |
36 | /**
37 | * A cache storage which can be used to store values or hashes to check if this influence
38 | * has to be refreshed and recalculated. Primarily used to save on UI redraws if nothing
39 | * has changed.
40 | */
41 | cache: unknown;
42 |
43 | /**
44 | * The default order of this effect.
45 | */
46 | order: number;
47 | }
48 |
--------------------------------------------------------------------------------
/rollup.config.dev.js:
--------------------------------------------------------------------------------
1 | import commonjs from "@rollup/plugin-commonjs";
2 | import nodeResolve from "@rollup/plugin-node-resolve";
3 | import replace from "@rollup/plugin-replace";
4 | import typescript from "@rollup/plugin-typescript";
5 | import { terser } from "rollup-plugin-terser";
6 |
7 | export default {
8 | input: "./src/registerPlugin.ts",
9 | output: {
10 | file: "./dist/ParkRatingInspector.js", // CHANGE THIS TO YOUR OPENRCT2 PLUGIN FOLDER FOR HOT RELOAD
11 | // TO IGNORE GIT CHANGES ON THIS FILE: git update-index --skip-worktree rollup.config.dev.js
12 | // TO ACCEPT GIT CHANGES ON THIS FILE AGAIN: git update-index --no-skip-worktree rollup.config.dev.js
13 | format: "iife",
14 | },
15 | plugins: [
16 | // Resolve and compile external libraries...
17 | nodeResolve(),
18 | commonjs(),
19 | // Update environment variables...
20 | replace({
21 | include: "./src/utilities/environment.ts",
22 | preventAssignment: true,
23 | values: {
24 | __BUILD_CONFIGURATION__: JSON.stringify("development")
25 | }
26 | }),
27 | // Compile plugin...
28 | typescript(),
29 | terser({
30 | compress: {
31 | passes: 3
32 | },
33 | format: {
34 | quote_style: 1,
35 | wrap_iife: true,
36 | preamble: "// Get the latest version: https://github.com/Basssiiie/OpenRCT2-ParkRatingInspector",
37 |
38 | beautify: true
39 | },
40 |
41 | // Useful only for stacktraces:
42 | keep_fnames: true,
43 | }),
44 | ],
45 | };
46 |
--------------------------------------------------------------------------------
/lib/duktape.d.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Allows access to the duktape object.
3 | */
4 | declare const Duktape: Duktape;
5 |
6 | /**
7 | * Allows acces to the duktape exposed performance timing api.
8 | * @see {@link https://duktape.org/guide.html#builtin-performance}
9 | */
10 | declare const performance: Performance;
11 |
12 |
13 | /**
14 | * Allows access to the duktape object.
15 | * @see {@link https://duktape.org/guide.html#builtin-duktape}
16 | */
17 | interface Duktape
18 | {
19 | /**
20 | * Returns an entry on the call stack.
21 | */
22 | act(depth: number): DukStackEntry;
23 |
24 |
25 | /**
26 | * Callback that gets triggered after an ECMAScript error has occured.
27 | */
28 | errCreate: (e: Error) => Error;
29 | }
30 |
31 |
32 | /**
33 | * An entry on the call stack.
34 | * @see {@link https://duktape.org/guide.html#builtin-duktape-act}
35 | */
36 | interface DukStackEntry
37 | {
38 | function: DukFunction;
39 | lineNumber: number;
40 | pc: number;
41 | }
42 |
43 |
44 | /**
45 | * A reference to a standard ES5 function in Duktape.
46 | * @see {@link https://duktape.org/guide.html#ecmascript-function-properties}
47 | */
48 | interface DukFunction extends Function
49 | {
50 | name: string;
51 | fileName: string;
52 | }
53 |
54 |
55 | /**
56 | * Allows acces to the duktape exposed performance timing api.
57 | * @see {@link https://duktape.org/guide.html#builtin-performance}
58 | */
59 | interface Performance
60 | {
61 | /**
62 | * Gets the monotonic time in milliseconds, including fractions.
63 | */
64 | now(): number;
65 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "openrct2-park-rating-inspector",
3 | "author": "Basssiiie",
4 | "license": "MIT",
5 | "version": "1.0.0",
6 | "type": "module",
7 | "scripts": {
8 | "start": "nodemon --watch ./src --ext js,ts --exec \"npm run build:dev\"",
9 | "build": "npm run lint && rollup --config rollup.config.js --environment BUILD:production",
10 | "build:dev": "rollup --config rollup.config.js",
11 | "lint": "eslint ./src --ext .js --ext .ts",
12 | "test": "nyc ava"
13 | },
14 | "repository": {
15 | "type": "git",
16 | "url": "git+https://github.com/Basssiiie/OpenRCT2-ParkRatingInspector.git"
17 | },
18 | "homepage": "https://github.com/Basssiiie/OpenRCT2-ParkRatingInspector#readme",
19 | "bugs": {
20 | "url": "https://github.com/Basssiiie/OpenRCT2-ParkRatingInspector/issues"
21 | },
22 | "dependencies": {
23 | "openrct2-flexui": "^0.1.0-prerelease.13"
24 | },
25 | "devDependencies": {
26 | "@ava/typescript": "^4.0.0",
27 | "@rollup/plugin-node-resolve": "^15.1.0",
28 | "@rollup/plugin-replace": "^5.0.2",
29 | "@rollup/plugin-terser": "^0.4.3",
30 | "@rollup/plugin-typescript": "^11.1.1",
31 | "@typescript-eslint/eslint-plugin": "^5.0.0",
32 | "@typescript-eslint/parser": "^5.0.0",
33 | "ava": "^5.3.1",
34 | "eslint": "^8.0.1",
35 | "eslint-plugin-import": "^2.25.2",
36 | "nodemon": "^2.0.22",
37 | "nyc": "^15.1.0",
38 | "openrct2-mocks": "^0.1.0",
39 | "platform-folders": "^0.6.0",
40 | "rollup": "^3.25.3",
41 | "tslib": "^2.3.1",
42 | "tsx": "^3.12.7",
43 | "typescript": "^5.1.3"
44 | },
45 | "ava": {
46 | "extensions": {
47 | "ts": "module"
48 | },
49 | "files": [
50 | "tests/**/*.tests.ts"
51 | ],
52 | "nodeArguments": [
53 | "--loader=tsx"
54 | ],
55 | "require": [
56 | "./tests/_setup.js"
57 | ],
58 | "verbose": true
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import resolve from "@rollup/plugin-node-resolve";
2 | import replace from "@rollup/plugin-replace";
3 | import terser from "@rollup/plugin-terser";
4 | import typescript from "@rollup/plugin-typescript";
5 | import { getConfigHome, getDocumentsFolder } from "platform-folders";
6 |
7 |
8 | // Environment variables
9 | const build = process.env.BUILD || "development";
10 | const isDev = (build === "development");
11 |
12 | /**
13 | * Tip: if you change the path here to your personal user folder,
14 | * you can ignore this change in git with:
15 | * ```
16 | * > git update-index --skip-worktree rollup.config.js
17 | * ```
18 | * To accept changes on this file again, use:
19 | * ```
20 | * > git update-index --no-skip-worktree rollup.config.js
21 | * ```
22 | */
23 | function getOutput()
24 | {
25 | if (!isDev)
26 | return "./dist/ParkRatingInspector.js";
27 |
28 | const pluginPath = "OpenRCT2/plugin/ParkRatingInspector.js";
29 | if (process.platform === "win32")
30 | {
31 | return `${getDocumentsFolder()}/${pluginPath}`;
32 | }
33 | else // for both Mac and Linux
34 | {
35 | return `${getConfigHome()}/${pluginPath}`;
36 | }
37 | }
38 |
39 |
40 | /**
41 | * @type {import("rollup").RollupOptions}
42 | */
43 | const config = {
44 | input: "./src/plugin.ts",
45 | output: {
46 | file: getOutput(),
47 | format: "iife",
48 | compact: true
49 | },
50 | treeshake: "smallest",
51 | plugins: [
52 | resolve(),
53 | replace({
54 | preventAssignment: true,
55 | values: {
56 | __BUILD_CONFIGURATION__: JSON.stringify(build),
57 | ...(isDev ? {} : {
58 | "Log.debug": "//",
59 | "Log.assert": "//"
60 | })
61 | }
62 | }),
63 | typescript(),
64 | terser({
65 | compress: {
66 | passes: 5,
67 | toplevel: true,
68 | unsafe: true
69 | },
70 | format: {
71 | comments: false,
72 | quote_style: 1,
73 | wrap_iife: true,
74 | preamble: "// Get the latest version: https://github.com/Basssiiie/OpenRCT2-ParkRatingInspector",
75 |
76 | beautify: isDev,
77 | },
78 | mangle: isDev ? {}
79 | : {
80 | properties: {
81 | regex: /^_/
82 | }
83 | },
84 |
85 | // Useful only for stacktraces:
86 | keep_fnames: isDev,
87 | }),
88 | ],
89 | };
90 | export default config;
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # OpenRCT2 TypeScript API declaration file
2 | lib/openrct2.d.ts
3 |
4 | # Logs
5 | logs
6 | *.log
7 | npm-debug.log*
8 | yarn-debug.log*
9 | yarn-error.log*
10 | lerna-debug.log*
11 |
12 | # Diagnostic reports (https://nodejs.org/api/report.html)
13 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
14 |
15 | # Runtime data
16 | pids
17 | *.pid
18 | *.seed
19 | *.pid.lock
20 |
21 | # Directory for instrumented libs generated by jscoverage/JSCover
22 | lib-cov
23 |
24 | # Coverage directory used by tools like istanbul
25 | coverage
26 | *.lcov
27 |
28 | # nyc test coverage
29 | .nyc_output
30 |
31 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
32 | .grunt
33 |
34 | # Bower dependency directory (https://bower.io/)
35 | bower_components
36 |
37 | # node-waf configuration
38 | .lock-wscript
39 |
40 | # Compiled binary addons (https://nodejs.org/api/addons.html)
41 | build/Release
42 |
43 | # Dependency directories
44 | node_modules/
45 | jspm_packages/
46 |
47 | # TypeScript v1 declaration files
48 | typings/
49 |
50 | # TypeScript cache
51 | *.tsbuildinfo
52 |
53 | # Optional npm cache directory
54 | .npm
55 |
56 | # Optional eslint cache
57 | .eslintcache
58 |
59 | # Microbundle cache
60 | .rpt2_cache/
61 | .rts2_cache_cjs/
62 | .rts2_cache_es/
63 | .rts2_cache_umd/
64 |
65 | # Optional REPL history
66 | .node_repl_history
67 |
68 | # Output of 'npm pack'
69 | *.tgz
70 |
71 | # Yarn Integrity file
72 | .yarn-integrity
73 |
74 | # dotenv environment variables file
75 | .env
76 | .env.test
77 |
78 | # parcel-bundler cache (https://parceljs.org/)
79 | .cache
80 |
81 | # Next.js build output
82 | .next
83 |
84 | # Nuxt.js build / generate output
85 | .nuxt
86 | dist
87 |
88 | # Gatsby files
89 | .cache/
90 | # Comment in the public line in if your project uses Gatsby and *not* Next.js
91 | # https://nextjs.org/blog/next-9-1#public-directory-support
92 | # public
93 |
94 | # vuepress build output
95 | .vuepress/dist
96 |
97 | # Serverless directories
98 | .serverless/
99 |
100 | # FuseBox cache
101 | .fusebox/
102 |
103 | # DynamoDB Local files
104 | .dynamodb/
105 |
106 | # TernJS port file
107 | .tern-port
108 |
109 | # Visual Studio
110 | .vs/
111 |
--------------------------------------------------------------------------------
/src/core/parkRating.ts:
--------------------------------------------------------------------------------
1 | import { ParkInfo } from "./parkInfo";
2 | import { Effect } from "./effects";
3 | import { Influences } from "./influences";
4 |
5 |
6 | /**
7 | * The effects on the park rating of a single park.
8 | */
9 | export interface ParkRating
10 | {
11 | /**
12 | * The park for which to calculate the effects.
13 | */
14 | readonly park: ParkInfo;
15 |
16 | /**
17 | * Gets the effects applied to this park.
18 | */
19 | readonly effects: Effect[];
20 |
21 | /**
22 | * The total park rating as calculated by the plugin.
23 | */
24 | total: number;
25 |
26 | /**
27 | * Recalculates all effects for the park rating.
28 | * @returns True if anything has changed, or false otherwise.
29 | */
30 | recalculate(): boolean;
31 | }
32 |
33 |
34 | /**
35 | * Helper for calculating park rating effects.
36 | */
37 | export const ParkRating =
38 | {
39 | /**
40 | * Calculates all effects currently applied to the park rating
41 | * of the specified park.
42 | */
43 | for(park: ParkInfo): ParkRating
44 | {
45 | const effects: Record = {};
46 | const result: ParkRating =
47 | {
48 | park: park,
49 | total: 0,
50 | get effects()
51 | {
52 | return Object
53 | .keys(effects)
54 | .filter(k => effects[k].active)
55 | .map(k => effects[k]);
56 | },
57 | recalculate: () => recalculateEffects(effects, result)
58 | };
59 | return result;
60 | }
61 | };
62 |
63 |
64 | /**
65 | * Recalculates all effects on the specified park.
66 | * @returns True if any of the effects has changed, false if nothing has changed.
67 | */
68 | function recalculateEffects(currentEffects: Record, result: ParkRating): boolean
69 | {
70 | result.park.refresh();
71 |
72 | let anyUpdate = false;
73 | let index = 0;
74 | let total = 0;
75 | for (const key in Influences)
76 | {
77 | index++;
78 | let effect = currentEffects[key];
79 | const isNew = (!effect);
80 | if (isNew)
81 | {
82 | effect = ({ order: index } as Effect);
83 | currentEffects[key] = effect;
84 | }
85 |
86 | const influence = Influences[key];
87 | if (influence && influence(effect, result.park))
88 | {
89 | anyUpdate = true;
90 | }
91 | if (effect.active && effect.impact)
92 | {
93 | total += effect.impact;
94 | }
95 | }
96 | result.total = total;
97 | return anyUpdate;
98 | }
--------------------------------------------------------------------------------
/src/utilities/logger.ts:
--------------------------------------------------------------------------------
1 | ///
2 | /* istanbul ignore file */
3 |
4 | import * as Environment from "./environment";
5 |
6 |
7 | /**
8 | * The available levels of logging.
9 | */
10 | type LogLevel = "debug" | "warning" | "error";
11 |
12 |
13 | /**
14 | * Returns true if Duktape is available, or false if not.
15 | */
16 | const isDuktapeAvailable = (typeof Duktape !== "undefined");
17 |
18 |
19 | /**
20 | * Prints a message with the specified logging and plugin identifier.
21 | */
22 | function print(level: LogLevel, message: string): void
23 | {
24 | console.log(` ${message}`);
25 | }
26 |
27 |
28 | /**
29 | * Returns the current call stack as a string.
30 | */
31 | function stacktrace(): string
32 | {
33 | if (!isDuktapeAvailable)
34 | {
35 | return " (stacktrace unavailable)\r\n";
36 | }
37 |
38 | const depth = -4; // skips act(), stacktrace() and the calling method.
39 | let entry: DukStackEntry, result = "";
40 |
41 | for (let i = depth; (entry = Duktape.act(i)); i--)
42 | {
43 | const functionName = entry.function.name;
44 | const prettyName = functionName
45 | ? (functionName + "()")
46 | : "";
47 |
48 | result += ` -> ${prettyName}: line ${entry.lineNumber}\r\n`;
49 | }
50 | return result;
51 | }
52 |
53 |
54 | /**
55 | * Enable stack-traces on errors in development mode.
56 | */
57 | if (Environment.isDevelopment && isDuktapeAvailable)
58 | {
59 | Duktape.errCreate = function onError(error): Error
60 | {
61 | error.message += ("\r\n" + stacktrace());
62 | return error;
63 | };
64 | }
65 |
66 |
67 | /**
68 | * Prints a debug message if the plugin is run in development mode.
69 | */
70 | export function debug(message: string): void
71 | {
72 | if (Environment.isDevelopment)
73 | {
74 | print("debug", message);
75 | }
76 | }
77 |
78 |
79 | /**
80 | * Prints a warning message to the console.
81 | */
82 | export function warning(message: string): void
83 | {
84 | print("warning", message);
85 | }
86 |
87 |
88 | /**
89 | * Prints an error message to the console and an additional stacktrace
90 | * if the plugin is run in development mode.
91 | */
92 |
93 | export function error(message: string): void
94 | {
95 | if (Environment.isDevelopment)
96 | {
97 | message += ("\r\n" + stacktrace());
98 | }
99 | print("error", message);
100 | }
101 |
102 |
103 | /**
104 | * Stringifies the object to json in a compact fashion, useful for logging.
105 | */
106 | export function stringify(obj: unknown): string
107 | {
108 | if (typeof obj !== "object" || obj === null)
109 | return JSON.stringify(obj);
110 |
111 | if (Array.isArray(obj))
112 | return `[${obj.map(stringify).join(", ")}]`;
113 |
114 | const pairs = [];
115 | for (const key in obj)
116 | {
117 | // @ts-expect-error key is fine for indexing object
118 | pairs.push(`${String(key)}: ${stringify(obj[key])}`);
119 | }
120 | return `{ ${pairs.join(", ")} }`;
121 | }
--------------------------------------------------------------------------------
/src/startup.ts:
--------------------------------------------------------------------------------
1 | import { WindowTemplate, label, listview, store, window } from "openrct2-flexui";
2 | import { ParkInfo } from "./core/parkInfo";
3 | import { ParkRating } from "./core/parkRating";
4 | import { isUiAvailable, pluginVersion, requiredApiVersion } from "./utilities/environment";
5 | import * as Log from "./utilities/logger";
6 |
7 |
8 | const viewmodel =
9 | {
10 | parkRating: ParkRating.for(ParkInfo),
11 | nextUpdate: 0,
12 |
13 | currentRatingLabel: store(""),
14 | items: store([]),
15 |
16 | check(): void
17 | {
18 | // Check only four times per second...
19 | const tickCount = date.ticksElapsed;
20 | if (tickCount < this.nextUpdate)
21 | return;
22 |
23 | this.nextUpdate = (tickCount + 10);
24 |
25 | // Only update if something has changed...
26 | if (this.parkRating.recalculate())
27 | {
28 | this.redraw();
29 | }
30 | },
31 |
32 | redraw(): void
33 | {
34 | const rating = this.parkRating.total;
35 | this.currentRatingLabel.set(`Current park rating: ${rating}/999`);
36 |
37 | const effects = this.parkRating.effects.sort((l, r) => (r.impact - l.impact) || (l.order - r.order));
38 | const count = effects.length;
39 | const oldItems = this.items.get();
40 | const newItems = Array(count);
41 |
42 | for (let i = 0; i < count; i++)
43 | {
44 | const effect = effects[i];
45 | const color =
46 | (effect.impact === effect.maximum) ? "{GREEN}+" :
47 | (effect.impact > 0) ? "{CELADON}+" :
48 | (effect.impact < 0) ? "{RED}" : " ";
49 |
50 | const row = (oldItems[i] as string[]) || Array(4);
51 | row[0] = effect.name;
52 | row[1] = effect.value;
53 | row[2] = (color + effect.impact.toString());
54 | row[3] = effect.note;
55 | newItems[i] = row;
56 | }
57 | this.items.set(newItems);
58 | }
59 | };
60 |
61 |
62 | let template: WindowTemplate | undefined;
63 | function getWindow(): WindowTemplate
64 | {
65 | if (template)
66 | return template;
67 |
68 | return (template = window({
69 | title: `Park Rating Inspector (v${pluginVersion})`,
70 | width: { value: 550, min: 350, max: 1200 },
71 | height: { value: 210, min: 150, max: 300 },
72 | spacing: 3,
73 | content: [
74 | listview({
75 | columns: [
76 | { header: "Name", width: "35%" },
77 | { header: "Value", width: "110px" },
78 | { header: "Impact", width: "45px" },
79 | { header: "Notes", width: "65%" }
80 | ],
81 | items: viewmodel.items,
82 | isStriped: true,
83 | }),
84 | label({
85 | text: viewmodel.currentRatingLabel,
86 | alignment: "centred",
87 | height: 12
88 | }),
89 | label({
90 | text: "github.com/Basssiiie/OpenRCT2-ParkRatingInspector",
91 | alignment: "centred",
92 | disabled: true
93 | }),
94 | ],
95 | onUpdate: () => viewmodel.check()
96 | }));
97 | }
98 |
99 |
100 | /**
101 | * Entry point of the plugin.
102 | */
103 | export function startup(): void
104 | {
105 | Log.debug("Plugin started.");
106 |
107 | if (!isUiAvailable)
108 | {
109 | return;
110 | }
111 |
112 | ui.registerMenuItem("Inspect park rating", () =>
113 | {
114 | if (!context.apiVersion || context.apiVersion < requiredApiVersion)
115 | {
116 | const title = "Please update the game!";
117 | const message = "\nThe version of OpenRCT2 you are currently playing is too old for this plugin.";
118 |
119 | ui.showError(title, message);
120 | console.log(`[ParkRatingInspector] ${title} ${message}`);
121 | return;
122 | }
123 |
124 | viewmodel.nextUpdate = 0;
125 | getWindow().open();
126 | });
127 | };
128 |
--------------------------------------------------------------------------------
/src/core/parkInfo.ts:
--------------------------------------------------------------------------------
1 | import * as Log from "../utilities/logger";
2 |
3 | /**
4 | * Base api that keeps track on important info in the park.
5 | */
6 | export interface ParkInfo
7 | {
8 | /**
9 | * Whether the park has the scenario option for difficult park rating enabled.
10 | */
11 | hasDifficultParkRating: boolean;
12 |
13 | /**
14 | * Information about guests in the park.
15 | */
16 | guests: {
17 | /**
18 | * Total amount of guests.
19 | */
20 | total: number;
21 |
22 | /**
23 | * Total amount of happy guests.
24 | */
25 | happy: number;
26 |
27 | /**
28 | * Total amount of lost guests.
29 | */
30 | lost: number;
31 | };
32 |
33 | /**
34 | * Information about all rides in the park.
35 | */
36 | rides: {
37 | /**
38 | * Total amount of rides.
39 | */
40 | total: number;
41 |
42 | /**
43 | * Total amount of uptime of all rides.
44 | */
45 | uptime: number;
46 |
47 | /**
48 | * Total amount of ratings that have ratings.
49 | */
50 | withRatings: number;
51 |
52 | /**
53 | * Total amount of excitement of all rides.
54 | */
55 | excitement: number;
56 |
57 | /**
58 | * Total amount of intensity of all rides.
59 | */
60 | intensity: number;
61 | };
62 |
63 | /**
64 | * Total amount of litter in the park.
65 | */
66 | litter: number;
67 |
68 | /**
69 | * Total penalty gained from guest deaths or ride crashes.
70 | */
71 | casualtyPenalty: number;
72 |
73 | /**
74 | * Refreshes all information about the park.
75 | */
76 | refresh(): void;
77 | }
78 |
79 |
80 | export const ParkInfo: ParkInfo =
81 | {
82 | hasDifficultParkRating: false,
83 | guests: {
84 | total: 0,
85 | happy: 0,
86 | lost: 0,
87 | },
88 | rides: {
89 | total: 0,
90 | uptime: 0,
91 | withRatings: 0,
92 | excitement: 0,
93 | intensity: 0,
94 | },
95 | litter: 0,
96 | casualtyPenalty: 0,
97 |
98 | refresh()
99 | {
100 | // Park info
101 | this.hasDifficultParkRating = park.getFlag("difficultParkRating");
102 |
103 | // Guest information
104 | this.guests.total = park.guests;
105 |
106 | const guests = map.getAllEntities("guest");
107 | let happy = 0, lost = 0;
108 |
109 | for (const guest of guests)
110 | {
111 | if (!guest.isInPark)
112 | continue;
113 |
114 | if (guest.happiness > 128)
115 | happy++;
116 | if (guest.getFlag("leavingPark") && guest.isLost)
117 | lost++;
118 | }
119 |
120 | this.guests.happy = happy;
121 | this.guests.lost = lost;
122 |
123 | // Ride information
124 | let rideCount = 0, ridesUptime = 0, excitement = 0, intensity = 0, withRatings = 0;
125 |
126 | const rides = map.rides;
127 | for (const ride of rides)
128 | {
129 | ridesUptime += (100 - ride.downtime);
130 | rideCount++;
131 |
132 | if (ride.excitement != -1)
133 | {
134 | excitement += Math.floor(ride.excitement / 8);
135 | intensity += Math.floor(ride.intensity / 8);
136 | withRatings++;
137 | }
138 | }
139 |
140 | this.rides.total = rideCount;
141 | this.rides.uptime = ridesUptime;
142 | this.rides.withRatings = withRatings;
143 | this.rides.excitement = excitement;
144 | this.rides.intensity = intensity;
145 |
146 | // Litter
147 | const litter = map.getAllEntities("litter");
148 | const currentTick = date.ticksElapsed;
149 |
150 | let litterCount = 0;
151 | for (const item of litter)
152 | {
153 | let age = (currentTick - item.creationTick);
154 | if (age < 0)
155 | {
156 | age += 4_294_967_296; // Mimic uint32 underflow
157 | }
158 | if (age >= 7680)
159 | {
160 | litterCount++;
161 | }
162 | }
163 | this.litter = litterCount;
164 |
165 | // Casualties
166 | this.casualtyPenalty = park.casualtyPenalty;
167 |
168 | Log.debug(`Park info: ${Log.stringify(this)}`);
169 | }
170 | };
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ParkRatingInspector plugin for OpenRCT2
2 |
3 | This plugin helps you break down your park rating and figure out why it is so low or so high. The inspector will show each of the different possible influences and how much they are currently affecting your rating, which should help you figure out where you can improve.
4 |
5 | **(!) Note: this plugin currently only supports OpenRCT2 v0.3.5 or later.**
6 | Any version released after the 16th of October 2021 should work.
7 |
8 | It also works on multiplayer servers!
9 |
10 | 
11 |
12 | Thanks to Deurklink for the idea and guide.
13 |
14 |
15 | ## Installation
16 |
17 | 1. This plugin requires at least OpenRCT2 version v0.3.5.
18 | 2. Download the latest version of the plugin from the [Releases page](https://github.com/Basssiiie/OpenRCT2-ParkRatingInspector/releases).
19 | 3. To install it, put the downloaded `*.js` file into your `/OpenRCT2/plugin` folder.
20 | - Easiest way to find the OpenRCT2-folder is by launching the OpenRCT2 game, click and hold on the red toolbox in the main menu, and select "Open custom content folder".
21 | - Otherwise this folder is commonly found in `C:/Users//Documents/OpenRCT2/plugin` on Windows.
22 | - If you already had this plugin installed before, you can safely overwrite the old file.
23 | 4. Once the file is there, it should show up ingame in the dropdown menu under the map icon.
24 |
25 | ---
26 |
27 | ## For developers: building the source code
28 |
29 | 1. Install latest version of [Node](https://nodejs.org/en/) and make sure to include NPM in the installation options.
30 | 2. Clone the project to a location of your choice on your PC.
31 | 3. Open command prompt, use `cd` to change your current directory to the root folder of this project and run `npm install`.
32 | 4. Find `openrct2.d.ts` TypeScript API declaration file in OpenRCT2 files and copy it to `lib` folder (this file can usually be found in `C:/Users//Documents/OpenRCT2/bin/` or `C:/Program Files/OpenRCT2/`).
33 | - Alternatively, you can make a symbolic link instead of copying the file, which will keep the file up to date whenever you install new versions of OpenRCT2.
34 | 5. Run `npm run build` (release build) or `npm run build:dev` (develop build) to build the project.
35 | - For the release build, the default output folder is `(project directory)/dist`.
36 | - For the develop build, the project tries to put the plugin into your game's plugin directory.
37 | - These output paths can be changed in `rollup.config.js`.
38 |
39 | ### User interface
40 |
41 | This plugin makes use of the [FlexUI](https://github.com/Basssiiie/OpenRCT2-FlexUI) framework to create and manage the user interface. It is automatically fetched from NPM with `npm install`.
42 |
43 | ### Hot reload
44 |
45 | This project supports the [OpenRCT2 hot reload feature](https://github.com/OpenRCT2/OpenRCT2/blob/master/distribution/scripting.md#writing-scripts) for development.
46 |
47 | 1. Make sure you've enabled it by setting `enable_hot_reloading = true` in your `/OpenRCT2/config.ini`.
48 | 2. Open command prompt and use `cd` to change your current directory to the root folder of this project.
49 | 3. Run `npm start` to start the hot reload server.
50 | 4. Use the `/OpenRCT2/bin/openrct2.com` executable to [start OpenRCT2 with console](https://github.com/OpenRCT2/OpenRCT2/blob/master/distribution/scripting.md#writing-scripts) and load a save or start new game.
51 | 5. Each time you save any of the files in `./src/`, the server will compile `./src/plugin.ts` and place compiled plugin file inside your local OpenRCT2 plugin directory.
52 | 6. OpenRCT2 will notice file changes and it will reload the plugin.
53 |
54 | If your local OpenRCT2 plugin folder is not in the default location, you can specify a custom path in `rollup.config.js`.
55 |
56 | ### Final notes
57 |
58 | Thanks to [wisnia74](https://github.com/wisnia74/openrct2-typescript-mod-template) for providing the template for this mod and readme. Thanks to the community for the enthusiasm for this plugin and their amazing creations.
59 |
--------------------------------------------------------------------------------
/tests/core/parkInfo.tests.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | import test from "ava";
4 | import Mock from "openrct2-mocks";
5 | import { ParkInfo } from "../../src/core/parkInfo";
6 |
7 |
8 | test("Difficult rating is enabled", t =>
9 | {
10 | globalThis.date = Mock.date();
11 | globalThis.park = Mock.park({ flags: [ "difficultParkRating" ] });
12 | globalThis.map = Mock.map();
13 |
14 | ParkInfo.refresh();
15 |
16 | t.true(ParkInfo.hasDifficultParkRating);
17 | });
18 |
19 |
20 | test("Difficult rating is disabled", t =>
21 | {
22 | globalThis.date = Mock.date();
23 | globalThis.park = Mock.park();
24 | globalThis.map = Mock.map();
25 |
26 | ParkInfo.refresh();
27 |
28 | t.false(ParkInfo.hasDifficultParkRating);
29 | });
30 |
31 |
32 | test("Guest total is set", t =>
33 | {
34 | globalThis.date = Mock.date();
35 | globalThis.park = Mock.park({ guests: 1234 });
36 | globalThis.map = Mock.map();
37 |
38 | ParkInfo.refresh();
39 |
40 | t.is(1234, ParkInfo.guests.total);
41 | });
42 |
43 |
44 | test("Happy guests are counted", t =>
45 | {
46 | globalThis.date = Mock.date();
47 | globalThis.park = Mock.park();
48 | globalThis.map = Mock.map({
49 | tiles: Mock.tile(),
50 | entities: [
51 | Mock.guest({ happiness: 255 }), // super happy
52 | Mock.guest({ happiness: 129 }), // just happy
53 | Mock.guest({ happiness: 128 }), // not happy enough
54 | Mock.guest({ happiness: 1 }), // super sad
55 | ]
56 | });
57 |
58 | ParkInfo.refresh();
59 |
60 | t.is(4, ParkInfo.guests.total);
61 | t.is(2, ParkInfo.guests.happy);
62 | });
63 |
64 |
65 | test("Lost guests are counted", t =>
66 | {
67 | globalThis.date = Mock.date();
68 | globalThis.park = Mock.park();
69 | globalThis.map = Mock.map({
70 | entities: [
71 | Mock.guest({ lostCountdown: 255, flags: [ "leavingPark" ] }), // not lost
72 | Mock.guest({ lostCountdown: 90, flags: [ "leavingPark" ] }), // almost lost
73 | Mock.guest({ lostCountdown: 89, flags: [ "leavingPark" ] }), // lost
74 | Mock.guest({ lostCountdown: 30, flags: [ "leavingPark" ] }), // lost
75 | Mock.guest({ lostCountdown: 30, flags: [ "leavingPark" ], isInPark: false }), // already left
76 | Mock.guest({ lostCountdown: 25 }), // lost but not leaving park
77 | Mock.guest({ lostCountdown: 15 }), // lost but not leaving park
78 | ]
79 | });
80 |
81 | ParkInfo.refresh();
82 |
83 | t.is(6, ParkInfo.guests.total);
84 | t.is(2, ParkInfo.guests.lost);
85 | });
86 |
87 |
88 | test("Ride total is calculated", t =>
89 | {
90 | globalThis.date = Mock.date();
91 | globalThis.park = Mock.park();
92 | globalThis.map = Mock.map({
93 | rides: [ Mock.ride(), Mock.ride(), Mock.ride(), Mock.ride(), Mock.ride() ]
94 | });
95 |
96 | ParkInfo.refresh();
97 |
98 | t.is(5, ParkInfo.rides.total);
99 | });
100 |
101 |
102 | test("Ride uptime is calculated", t =>
103 | {
104 | globalThis.date = Mock.date();
105 | globalThis.park = Mock.park();
106 | globalThis.map = Mock.map({
107 | rides: [
108 | Mock.ride({ downtime: 0 }), // 100
109 | Mock.ride({ downtime: 100 }), // 0
110 | Mock.ride({ downtime: 75 }), // 25
111 | Mock.ride({ downtime: 15 }), // 85
112 | ]
113 | });
114 |
115 | ParkInfo.refresh();
116 |
117 | t.is(210, ParkInfo.rides.uptime);
118 | });
119 |
120 |
121 | test("Ride ratings are calculated", t =>
122 | {
123 | globalThis.date = Mock.date();
124 | globalThis.park = Mock.park();
125 | globalThis.map = Mock.map({
126 | rides: [
127 | Mock.ride({ excitement: 310, intensity: 210 }), // 38, 26
128 | Mock.ride({ excitement: -1 }), // no ratings
129 | Mock.ride({ excitement: 820, intensity: 720 }), // 102, 90
130 | Mock.ride({ excitement: -1 }), // no ratings
131 | ]
132 | });
133 |
134 | ParkInfo.refresh();
135 |
136 | t.is(140, ParkInfo.rides.excitement); // after divide by 8
137 | t.is(116, ParkInfo.rides.intensity); // after divide by 8
138 | t.is(2, ParkInfo.rides.withRatings);
139 | t.is(4, ParkInfo.rides.total);
140 | });
141 |
142 |
143 | test("Litter is counted when old", t =>
144 | {
145 | globalThis.date = Mock.date({ ticksElapsed: 100_000 });
146 | globalThis.park = Mock.park();
147 | globalThis.map = Mock.map({
148 | entities: [
149 | Mock({ type: "litter", creationTick: 12_000 }), // super old litter
150 | Mock({ type: "litter", creationTick: 52_000 }), // somewhat old litter
151 | Mock({ type: "litter", creationTick: 92_000 }), // old litter
152 | ]
153 | });
154 |
155 | ParkInfo.refresh();
156 | // Count only litter older than 7680 ticks (> 3 minutes and 12 seconds)
157 | t.is(3, ParkInfo.litter);
158 | });
159 |
160 |
161 | test("Litter is not counted when new", t =>
162 | {
163 | globalThis.date = Mock.date({ ticksElapsed: 100_000 });
164 | globalThis.park = Mock.park();
165 | globalThis.map = Mock.map({
166 | entities: [
167 | Mock({ type: "litter", creationTick: 99_000 }), // very new litter
168 | Mock({ type: "litter", creationTick: 93_000 }), // new litter
169 | ]
170 | });
171 |
172 | ParkInfo.refresh();
173 | // Count only litter older than 7680 ticks (> 3 minutes and 12 seconds)
174 | t.is(0, ParkInfo.litter);
175 | });
176 |
177 |
178 | test("Litter does not count other entities", t =>
179 | {
180 | globalThis.date = Mock.date({ ticksElapsed: 100_000 });
181 | globalThis.park = Mock.park();
182 | globalThis.map = Mock.map({
183 | entities: [
184 | Mock.entity({ type: "balloon" }),
185 | Mock.entity({ type: "explosion_cloud" }),
186 | ]
187 | });
188 |
189 | ParkInfo.refresh();
190 | t.is(0, ParkInfo.litter);
191 | });
192 |
193 |
194 | test("Litter with corrupt futuristic age is counted", t => // some sc6's have corrupted ages
195 | {
196 | globalThis.date = Mock.date({ ticksElapsed: 100_000 });
197 | globalThis.park = Mock.park();
198 | globalThis.map = Mock.map({
199 | entities: [
200 | Mock({ type: "litter", creationTick: 4_294_954_443 }),
201 | Mock({ type: "litter", creationTick: 4_294_887_320 }),
202 | Mock({ type: "litter", creationTick: 4_294_889_391 }),
203 | ]
204 | });
205 |
206 | ParkInfo.refresh();
207 | t.is(3, ParkInfo.litter);
208 | });
209 |
210 |
211 | test("Casualty penalty is retrieved", t =>
212 | {
213 | globalThis.date = Mock.date();
214 | globalThis.park = Mock.park({ casualtyPenalty: 246 });
215 | globalThis.map = Mock.map();
216 |
217 | ParkInfo.refresh();
218 |
219 | t.is(246, ParkInfo.casualtyPenalty);
220 | });
221 |
--------------------------------------------------------------------------------
/src/core/influences.ts:
--------------------------------------------------------------------------------
1 | import { Effect } from "./effects";
2 | import { ParkInfo } from "./parkInfo";
3 |
4 | /**
5 | * Callback for something that can influence a park rating.
6 | * @returns True if the record was updated, false if not.
7 | */
8 | type Influence = (current: Effect, park: ParkInfo) => boolean;
9 |
10 |
11 | /**
12 | * All influences that can be applied to a currently stored effect.
13 | * All influences returns true if the effect has changed, or false
14 | * if it's still the same.
15 | *
16 | * Online guides do mention a starting value of 1150, but throughout the
17 | * various influences it gets subtracted down to zero anyway:
18 | * -150 to include total number of guests penalty;
19 | * -500 to include happy guests penalty;
20 | * -300 to include base ride penalties;
21 | * -200 to include ride total rating penalties;
22 | */
23 | export const Influences: Record =
24 | {
25 | /**
26 | * Penalty for parks with the 'difficult park rating' flag.
27 | */
28 | difficulty(current: Effect, park: ParkInfo): boolean
29 | {
30 | if (current.active === park.hasDifficultParkRating)
31 | return false;
32 |
33 | current.active = park.hasDifficultParkRating;
34 | if (current.active)
35 | {
36 | current.impact = -100;
37 | current.name = "Difficulty";
38 | current.value = "enabled";
39 | current.maximum = null;
40 | current.note = "Setting for 'difficult park rating'";
41 | }
42 | return true;
43 | },
44 |
45 | /**
46 | * The more guests there are, the higher the park rating.
47 | */
48 | numberOfGuests(current: Effect, park: ParkInfo)
49 | {
50 | const guestCount = park.guests.total;
51 | if (guestCount === current.cache)
52 | return false; // same as cache, no update
53 |
54 | current.cache = guestCount;
55 | current.active = true;
56 | current.impact = Math.floor(Math.min(2000, guestCount) / 13);
57 | current.name = "Guests";
58 | current.value = `${guestCount}/2000`;
59 | current.maximum = 153;
60 | current.note = "+1 for every 13 guests, max. +153";
61 | return true;
62 | },
63 |
64 | /**
65 | * Rewards the player for whatever percentage of guests are happy, up to 83%.
66 | */
67 | numberOfHappyGuests(current: Effect, park: ParkInfo)
68 | {
69 | const totalGuests = park.guests.total;
70 | if (totalGuests <= 0)
71 | return disableEffect(current);
72 |
73 | const happyGuests = park.guests.happy;
74 | const hash = (totalGuests | (happyGuests << 16));
75 |
76 | if (hash === current.cache)
77 | return false; // same as cache, no update
78 |
79 | current.cache = hash;
80 | current.active = true;
81 | current.impact = (2 * Math.min(250, Math.floor((happyGuests * 300) / totalGuests)));
82 |
83 | const percentage = Math.floor((happyGuests / totalGuests) * 100);
84 | current.name = "Happy guests";
85 | current.value = `${happyGuests}/${totalGuests} (${percentage}%)`;
86 | current.maximum = 500;
87 | current.note = "+6 for every percent, max. +500 (83%)";
88 | return true;
89 | },
90 |
91 | /**
92 | * Penalizes the player for every lost guest after the first 25 guests who are lost.
93 | */
94 | numberOfLostGuests(current: Effect, park: ParkInfo)
95 | {
96 | const lostGuests = park.guests.lost;
97 | if (lostGuests === current.cache)
98 | return false; // same as cache, no update
99 |
100 | current.cache = lostGuests;
101 | current.active = true;
102 | current.impact = (lostGuests > 25) ? ((lostGuests - 25) * -7) : 0;
103 | current.name = "Lost guests";
104 | current.value = `${lostGuests}`;
105 | current.maximum = null;
106 | current.note = "-7 per lost guest after the first 25";
107 | return true;
108 | },
109 |
110 | /**
111 | * Rewards the player for not having broken rides.
112 | */
113 | rideUptime(current: Effect, park: ParkInfo)
114 | {
115 | const rideCount = park.rides.total;
116 | if (rideCount <= 0)
117 | return disableEffect(current);
118 |
119 | const totalUptime = park.rides.uptime;
120 | if (totalUptime === current.cache)
121 | return false;
122 |
123 | current.cache = totalUptime;
124 | current.active = true;
125 |
126 | const averageUptime = Math.floor(totalUptime / rideCount);
127 | current.impact = (averageUptime * 2);
128 | current.name = "Average ride uptime";
129 | current.value = `${averageUptime}%`;
130 | current.maximum = 200;
131 | current.note = "+2 for every percent, max. +200";
132 | return true;
133 | },
134 |
135 | /**
136 | * Penalizes the player if they only have gentle rides, or only have rides with really high excitement.
137 | */
138 | rideAverageExcitement(current: Effect, park: ParkInfo)
139 | {
140 | const withRatings = park.rides.withRatings;
141 | if (withRatings <= 0)
142 | return disableEffect(current);
143 |
144 | const averageExcitement = (park.rides.excitement / withRatings);
145 | if (averageExcitement === current.cache)
146 | return false;
147 |
148 | current.cache = averageExcitement;
149 | current.active = true;
150 | current.impact = getAverageRatingImpact(averageExcitement, 46);
151 | current.name = "Average ride excitement";
152 | current.value = `${(averageExcitement * 0.08).toFixed(2)}/3.68`;
153 | current.maximum = 50;
154 | current.note = "Closer is better, max. +50";
155 | return true;
156 | },
157 |
158 | /**
159 | * Penalizes the player if they only have gentle rides, or only have rides with really high intensity.
160 | */
161 | rideAverageIntensity(current: Effect, park: ParkInfo)
162 | {
163 | const withRatings = park.rides.withRatings;
164 | if (withRatings <= 0)
165 | return disableEffect(current);
166 |
167 | const averageIntensity = (park.rides.intensity / withRatings);
168 | if (averageIntensity === current.cache)
169 | return false;
170 |
171 | current.cache = averageIntensity;
172 | current.active = true;
173 | current.impact = getAverageRatingImpact(averageIntensity, 65);
174 | current.name = "Average ride intensity";
175 | current.value = `${(averageIntensity * 0.08).toFixed(2)}/5.20`;
176 | current.maximum = 50;
177 | current.note = "Closer is better, max. +50";
178 | return true;
179 | },
180 |
181 | /**
182 | * Rewards the player for owning rides that are exciting.
183 | */
184 | rideTotalExcitement(current: Effect, park: ParkInfo)
185 | {
186 | const totalExcitement = park.rides.excitement;
187 | if (totalExcitement === current.cache)
188 | return false;
189 |
190 | current.cache = totalExcitement;
191 | current.active = true;
192 | current.impact = Math.floor(Math.min(totalExcitement, 1000) / 10);
193 | current.name = "Total ride excitement";
194 | current.value = `${(totalExcitement * 0.08).toFixed(1)}/80.0`;
195 | current.maximum = 100;
196 | current.note = "Max. +100";
197 | return true;
198 | },
199 |
200 | /**
201 | * Rewards the player for owning rides that are intense.
202 | */
203 | rideTotalIntensity(current: Effect, park: ParkInfo)
204 | {
205 | const totalIntensity = park.rides.intensity;
206 | if (totalIntensity === current.cache)
207 | return false;
208 |
209 | current.cache = totalIntensity;
210 | current.active = true;
211 | current.impact = Math.floor(Math.min(totalIntensity, 1000) / 10);
212 | current.name = "Total ride intensity";
213 | current.value = `${(totalIntensity * 0.08).toFixed(1)}/80.0`;
214 | current.maximum = 100;
215 | current.note = "Max. +100";
216 | return true;
217 | },
218 |
219 | /**
220 | * Penalizes the player for every piece of litter in the park.
221 | */
222 | litter(current: Effect, park: ParkInfo)
223 | {
224 | const litterCount = park.litter;
225 | if (litterCount === current.cache)
226 | return false;
227 |
228 | current.cache = litterCount;
229 | current.active = true;
230 | current.impact = (-4 * Math.min(150, litterCount));
231 | current.name = "Amount of litter";
232 | current.value = `${litterCount}/150`;
233 | current.maximum = null;
234 | current.note = "-4 per piece of litter older than 3 minutes";
235 | return true;
236 | },
237 |
238 | /**
239 | * Amount of penalty points from crashed vehicles and recently dead guests.
240 | */
241 | casualties(current: Effect, park: ParkInfo)
242 | {
243 | const effect = park.casualtyPenalty;
244 | if (effect === current.cache)
245 | return false;
246 |
247 | current.cache = effect;
248 | current.active = true;
249 | current.impact = -effect;
250 | current.name = "Casualty penalty";
251 | current.value = `${effect}/1000`;
252 | current.maximum = null;
253 | current.note = "-200 per crashed train until -500, -25 per drowned guest";
254 | return true;
255 | }
256 | };
257 |
258 |
259 | /**
260 | * Calculates the impact from the distance between the average rating and the
261 | * target average.
262 | */
263 | function getAverageRatingImpact(average: number, target: number): number
264 | {
265 | let impact = (average - target);
266 | if (impact < 0)
267 | impact = -impact;
268 |
269 | return (50 - Math.min(Math.floor(impact / 2), 50));
270 | }
271 |
272 |
273 | /**
274 | * Sets the effect to inactive, sets the cache to 0.
275 | * @returns True if the effect has been deactivated, or false if the effect was already inactive.
276 | */
277 | function disableEffect(effect: Effect): boolean
278 | {
279 | if (effect.active)
280 | {
281 | effect.cache = 0;
282 | effect.active = false;
283 | return true;
284 | }
285 | return false;
286 | }
287 |
--------------------------------------------------------------------------------
/tests/core/parkRating.tests.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | import test from "ava";
4 | import { ParkInfo } from "../../src/core/parkInfo";
5 | import { ParkRating } from "../../src/core/parkRating";
6 |
7 |
8 | const difficultyEffect = "Difficulty";
9 | const numberOfGuestsEffect = "Guests";
10 | const happyGuestsEffect = "Happy";
11 | const lostGuestsEffect = "Lost";
12 | const rideUptimeEffect = "uptime";
13 | const rideAverageExcitementEffect = "Average ride excitement";
14 | const rideAverageIntensityEffect = "Average ride intensity";
15 | const rideTotalExcitementEffect = "Total ride excitement";
16 | const rideTotalIntensityEffect = "Total ride intensity";
17 | const litterEffect = "litter";
18 | const casualtyEffect = "Casualty";
19 |
20 |
21 | test("All effects are valid and active", t =>
22 | {
23 | ParkInfo.refresh = (): void => {};
24 | ParkInfo.hasDifficultParkRating = true;
25 | ParkInfo.guests = { total: 100, happy: 50, lost: 20 };
26 | ParkInfo.rides = { total: 10, uptime: 80, excitement: 100, intensity: 50, withRatings: 8 };
27 | ParkInfo.litter = 50;
28 | ParkInfo.casualtyPenalty = 25;
29 |
30 | const rating = ParkRating.for(ParkInfo);
31 | rating.recalculate();
32 |
33 | t.is(rating.effects.length, 11);
34 | t.true(rating.effects.every(e => e.active));
35 | t.true(rating.effects.every(e => e.name));
36 | t.true(rating.effects.every(e => e.value));
37 | t.true(rating.effects.every(e => e.note));
38 | });
39 |
40 |
41 | test("Difficult rating is enabled", t =>
42 | {
43 | ParkInfo.refresh = (): void => {};
44 | ParkInfo.hasDifficultParkRating = true;
45 |
46 | const rating = ParkRating.for(ParkInfo);
47 | rating.recalculate();
48 |
49 | const effect = rating.effects.find(e => e.name.includes(difficultyEffect));
50 | t.is(effect?.value, "enabled");
51 | t.is(effect?.impact, -100);
52 | });
53 |
54 |
55 | test("Difficult rating is disabled", t =>
56 | {
57 | ParkInfo.refresh = (): void => {};
58 | ParkInfo.hasDifficultParkRating = false;
59 |
60 | const rating = ParkRating.for(ParkInfo);
61 | rating.recalculate();
62 |
63 | const effect = rating.effects.find(e => e.name.includes(difficultyEffect));
64 | t.is(effect, undefined); // gone
65 | });
66 |
67 |
68 | test("Difficult rating is enabled then disabled then enabled", t =>
69 | {
70 | ParkInfo.refresh = (): void => {};
71 | ParkInfo.hasDifficultParkRating = true;
72 |
73 | const rating = ParkRating.for(ParkInfo);
74 | rating.recalculate();
75 |
76 | const effect1 = rating.effects.find(e => e.name.includes(difficultyEffect));
77 | t.true(effect1?.active);
78 |
79 | ParkInfo.hasDifficultParkRating = false;
80 | rating.recalculate();
81 |
82 | const effect2 = rating.effects.find(e => e.name.includes(difficultyEffect));
83 | t.is(effect2, undefined); // gone
84 |
85 | ParkInfo.hasDifficultParkRating = true;
86 | rating.recalculate();
87 |
88 | const effect3 = rating.effects.find(e => e.name.includes(difficultyEffect));
89 | t.true(effect3?.active);
90 | });
91 |
92 |
93 | test("Total guests", t =>
94 | {
95 | ParkInfo.refresh = (): void => {};
96 | ParkInfo.guests.total = 1310;
97 |
98 | const rating = ParkRating.for(ParkInfo);
99 | rating.recalculate();
100 |
101 | const effect = rating.effects.find(e => e.name.includes(numberOfGuestsEffect));
102 | t.true(effect?.value.includes("1310"), effect?.value);
103 | t.is(effect?.impact, 100);
104 | });
105 |
106 |
107 | test("Total guests: cap at 2000", t =>
108 | {
109 | ParkInfo.refresh = (): void => {};
110 | ParkInfo.guests.total = 2600;
111 |
112 | const rating = ParkRating.for(ParkInfo);
113 | rating.recalculate();
114 |
115 | const effect = rating.effects.find(e => e.name.includes(numberOfGuestsEffect));
116 | t.true(effect?.value.includes("2600"), effect?.value);
117 | t.is(effect?.impact, 153);
118 | });
119 |
120 |
121 | test("Happy guests: 100% happiness", t =>
122 | {
123 | ParkInfo.refresh = (): void => {};
124 | ParkInfo.guests.total = 500;
125 | ParkInfo.guests.happy = 500;
126 |
127 | const rating = ParkRating.for(ParkInfo);
128 | rating.recalculate();
129 |
130 | const effect = rating.effects.find(e => e.name.includes(happyGuestsEffect));
131 | t.true(effect?.value.includes("500/500"), effect?.value);
132 | t.is(effect?.impact, 500);
133 | });
134 |
135 |
136 | test("Happy guests: 50% happiness", t =>
137 | {
138 | ParkInfo.refresh = (): void => {};
139 | ParkInfo.guests.total = 500;
140 | ParkInfo.guests.happy = 250;
141 |
142 | const rating = ParkRating.for(ParkInfo);
143 | rating.recalculate();
144 |
145 | const effect = rating.effects.find(e => e.name.includes(happyGuestsEffect));
146 | t.true(effect?.value.includes("250/500"), effect?.value);
147 | t.is(effect?.impact, 300);
148 | });
149 |
150 |
151 | test("Happy guests: 0% happiness", t =>
152 | {
153 | ParkInfo.refresh = (): void => {};
154 | ParkInfo.guests.total = 500;
155 | ParkInfo.guests.happy = 0;
156 |
157 | const rating = ParkRating.for(ParkInfo);
158 | rating.recalculate();
159 |
160 | const effect = rating.effects.find(e => e.name.includes(happyGuestsEffect));
161 | t.true(effect?.value.includes("0/500"), effect?.value);
162 | t.is(effect?.impact, 0);
163 | });
164 |
165 |
166 | test("Lost guests: 500", t =>
167 | {
168 | ParkInfo.refresh = (): void => {};
169 | ParkInfo.guests.lost = 500;
170 |
171 | const rating = ParkRating.for(ParkInfo);
172 | rating.recalculate();
173 |
174 | const effect = rating.effects.find(e => e.name.includes(lostGuestsEffect));
175 | t.true(effect?.value.includes("500"), effect?.value);
176 | t.is(effect?.impact, -7 * 475);
177 | });
178 |
179 |
180 | test("Lost guests: 25, no impact", t =>
181 | {
182 | ParkInfo.refresh = (): void => {};
183 | ParkInfo.guests.lost = 25;
184 |
185 | const rating = ParkRating.for(ParkInfo);
186 | rating.recalculate();
187 |
188 | const effect = rating.effects.find(e => e.name.includes(lostGuestsEffect));
189 | t.true(effect?.value.includes("25"), effect?.value);
190 | t.is(effect?.impact, 0);
191 | });
192 |
193 |
194 | test("Ride uptime: 100%", t =>
195 | {
196 | ParkInfo.refresh = (): void => {};
197 | ParkInfo.rides.total = 10;
198 | ParkInfo.rides.uptime = 1000;
199 |
200 | const rating = ParkRating.for(ParkInfo);
201 | rating.recalculate();
202 |
203 | const effect = rating.effects.find(e => e.name.includes(rideUptimeEffect));
204 | t.true(effect?.value.includes("100%"), effect?.value);
205 | t.is(effect?.impact, 200);
206 | });
207 |
208 |
209 | test("Ride uptime: 50%", t =>
210 | {
211 | ParkInfo.refresh = (): void => {};
212 | ParkInfo.rides.total = 10;
213 | ParkInfo.rides.uptime = 500;
214 |
215 | const rating = ParkRating.for(ParkInfo);
216 | rating.recalculate();
217 |
218 | const effect = rating.effects.find(e => e.name.includes(rideUptimeEffect));
219 | t.true(effect?.value.includes("50%"), effect?.value);
220 | t.is(effect?.impact, 100);
221 | });
222 |
223 |
224 | test("Ride uptime: 0%", t =>
225 | {
226 | ParkInfo.refresh = (): void => {};
227 | ParkInfo.rides.total = 10;
228 | ParkInfo.rides.uptime = 0;
229 |
230 | const rating = ParkRating.for(ParkInfo);
231 | rating.recalculate();
232 |
233 | const effect = rating.effects.find(e => e.name.includes(rideUptimeEffect));
234 | t.true(effect?.value.includes("0%"), effect?.value);
235 | t.is(effect?.impact, 0);
236 | });
237 |
238 |
239 | test("Ride average excitement & intensity: perfect", t =>
240 | {
241 | ParkInfo.refresh = (): void => {};
242 | ParkInfo.rides.total = 15;
243 | ParkInfo.rides.withRatings = 10;
244 | ParkInfo.rides.excitement = 460;
245 | ParkInfo.rides.intensity = 650;
246 |
247 | const rating = ParkRating.for(ParkInfo);
248 | rating.recalculate();
249 |
250 | const effect1 = rating.effects.find(e => e.name.includes(rideAverageExcitementEffect));
251 | t.true(effect1?.value.includes("3.68/3.68"), effect1?.value);
252 | t.is(effect1?.impact, 50);
253 |
254 | const effect2 = rating.effects.find(e => e.name.includes(rideAverageIntensityEffect));
255 | t.true(effect2?.value.includes("5.20/5.20"), effect2?.value);
256 | t.is(effect2?.impact, 50);
257 | });
258 |
259 |
260 | test("Ride average excitement & intensity: single Crooked House", t =>
261 | {
262 | ParkInfo.refresh = (): void => {};
263 | ParkInfo.rides.total = 5;
264 | ParkInfo.rides.withRatings = 1;
265 | ParkInfo.rides.excitement = Math.floor(215 / 8);
266 | ParkInfo.rides.intensity = Math.floor(62 / 8);
267 |
268 | const rating = ParkRating.for(ParkInfo);
269 | rating.recalculate();
270 |
271 | // NOTE: Slightly different from input because of divide by 8 and flooring. This reflects the game's algorithm.
272 | const effect1 = rating.effects.find(e => e.name.includes(rideAverageExcitementEffect));
273 | t.true(effect1?.value.includes("2.08/3.68"), effect1?.value);
274 | t.is(effect1?.impact, 40);
275 |
276 | const effect2 = rating.effects.find(e => e.name.includes(rideAverageIntensityEffect));
277 | t.true(effect2?.value.includes("0.56/5.20"), effect2?.value);
278 | t.is(effect2?.impact, 21);
279 | });
280 |
281 |
282 | test("Ride total excitement & intensity: perfect", t =>
283 | {
284 | ParkInfo.refresh = (): void => {};
285 | ParkInfo.rides.total = 15;
286 | ParkInfo.rides.withRatings = 10;
287 | ParkInfo.rides.excitement = 1000;
288 | ParkInfo.rides.intensity = 1000;
289 |
290 | const rating = ParkRating.for(ParkInfo);
291 | rating.recalculate();
292 |
293 | const effect1 = rating.effects.find(e => e.name.includes(rideTotalExcitementEffect));
294 | t.true(effect1?.value.includes("80.0/80.0"), effect1?.value);
295 | t.is(effect1?.impact, 100);
296 |
297 | const effect2 = rating.effects.find(e => e.name.includes(rideTotalIntensityEffect));
298 | t.true(effect2?.value.includes("80.0/80.0"), effect2?.value);
299 | t.is(effect2?.impact, 100);
300 | });
301 |
302 |
303 | test("Ride total excitement & intensity: single Crooked house", t =>
304 | {
305 | ParkInfo.refresh = (): void => {};
306 | ParkInfo.rides.total = 15;
307 | ParkInfo.rides.withRatings = 10;
308 | ParkInfo.rides.excitement = Math.floor(215 / 8);
309 | ParkInfo.rides.intensity = Math.floor(62 / 8);
310 |
311 | const rating = ParkRating.for(ParkInfo);
312 | rating.recalculate();
313 |
314 | const effect1 = rating.effects.find(e => e.name.includes(rideTotalExcitementEffect));
315 | t.true(effect1?.value.includes("2.1/80.0"), effect1?.value);
316 | t.is(effect1?.impact, 2);
317 |
318 | const effect2 = rating.effects.find(e => e.name.includes(rideTotalIntensityEffect));
319 | t.true(effect2?.value.includes("0.6/80.0"), effect2?.value);
320 | t.is(effect2?.impact, 0);
321 | });
322 |
323 |
324 | test("Litter: some", t =>
325 | {
326 | ParkInfo.refresh = (): void => {};
327 | ParkInfo.litter = 50;
328 |
329 | const rating = ParkRating.for(ParkInfo);
330 | rating.recalculate();
331 |
332 | const effect = rating.effects.find(e => e.name.includes(litterEffect));
333 | t.true(effect?.value.includes("50/150"), effect?.value);
334 | t.is(effect?.impact, -4 * 50);
335 | });
336 |
337 |
338 | test("Litter: maximum", t =>
339 | {
340 | ParkInfo.refresh = (): void => {};
341 | ParkInfo.litter = 150;
342 |
343 | const rating = ParkRating.for(ParkInfo);
344 | rating.recalculate();
345 |
346 | const effect = rating.effects.find(e => e.name.includes(litterEffect));
347 | t.true(effect?.value.includes("150/150"), effect?.value);
348 | t.is(effect?.impact, -4 * 150);
349 | });
350 |
351 |
352 | test("Litter: none", t =>
353 | {
354 | ParkInfo.refresh = (): void => {};
355 | ParkInfo.litter = 0;
356 |
357 | const rating = ParkRating.for(ParkInfo);
358 | rating.recalculate();
359 |
360 | const effect = rating.effects.find(e => e.name.includes(litterEffect));
361 | t.true(effect?.value.includes("0/150"), effect?.value);
362 | t.true(0 === effect?.impact, `${effect?.impact}`);
363 | });
364 |
365 |
366 | test("Casualty penalty: maximum", t =>
367 | {
368 | ParkInfo.refresh = (): void => {};
369 | ParkInfo.casualtyPenalty = 1000;
370 |
371 | const rating = ParkRating.for(ParkInfo);
372 | rating.recalculate();
373 |
374 | const effect = rating.effects.find(e => e.name.includes(casualtyEffect));
375 | t.true(effect?.value.includes("1000/1000"), effect?.value);
376 | t.is(effect?.impact, -1000);
377 | });
378 |
379 |
380 | test("Casualty penalty: none", t =>
381 | {
382 | ParkInfo.refresh = (): void => {};
383 | ParkInfo.casualtyPenalty = 0;
384 |
385 | const rating = ParkRating.for(ParkInfo);
386 | rating.recalculate();
387 |
388 | const effect = rating.effects.find(e => e.name.includes(casualtyEffect));
389 | t.true(effect?.value.includes("0/1000"), effect?.value);
390 | t.true(0 === effect?.impact, `${effect?.impact}`);
391 | });
--------------------------------------------------------------------------------