├── .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 | ![(Image of the Park Rating Inspector)](https://raw.githubusercontent.com/Basssiiie/OpenRCT2-ParkRatingInspector/master/img/magicmountain.png) 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 | }); --------------------------------------------------------------------------------